🗾

pg_tileservで大量データを配信するタイルサーバーを作る方法

2024/02/27に公開

道路の通行実績データをマップに表示する機能を作った 🚗=3
これ。

マップに表示するには通行実績データをタイルサーバーで配信する必要があるので、
今回はサクッとタイルサーバーを起動する方法を試した🗺️

使ったOSSは pg_tileserv
https://github.com/CrunchyData/pg_tileserv
pg_tileserv は接続先のPostgreSQL(with PostGIS)にデータを用意すれば、簡単にタイルサーバーを作ってくれるOSS。
今回は誰でもダウンロードできるOpenStreetMapのデータを使ったやり方をメモ。
※実際に通行実績や、列車の運行情報などのデータを組み合わせて可視化したい場合は、それらのデータをosm_idで引っ張ってこれるテーブルを用意する必要がある。

タイルサーバーとDBの起動

docker-compose.ymlを作る

pg_tileservにdocker-compose.ymlのサンプルがあったけども、
少しシンプルな記述で起動するやり方をメモしておく📝
docker-compose.ymlを作ってDBサーバーとpg_tileservサーバーを起動してみる。
環境変数はenv_fileで別ファイルにしているので、それも作る。

version: "3"

services:
  pg_tileserv:
    image: pramsey/pg_tileserv:20231005
    container_name: pg_tileserv
    env_file:
      - pg_tileserv.env
    depends_on:
      - pg_tileserv_db
    ports:
      - 7800:7800

  pg_tileserv_db:
    image: postgis/postgis:15-3.4
    container_name: pg_tileserv_db
    volumes:
      - ./data:/work
      - pg_tileserv_db:/var/lib/postgresql/data
    env_file:
      - pg.env
    ports:
      - 5432:5432

volumes:
  pg_tileserv_db:
DATABASE_URL=postgres://tileserv:tileserv@pg_tileserv_db/tileserv
POSTGRES_USER=tileserv
POSTGRES_PASSWORD=tileserv
POSTGRES_DB=tileserv

起動

docker compose up

ブラウザからアクセス

http://localhost:7800/

表示されたレイヤー一覧をクリックするとこんな感じでpg_tileservの動作確認用地図ページが開く

データを取り込んでないので、真っ白な地図になっている。

osmからデータを取り込んでみる

真っ白だと正常に動いているか分からないため。
OpenStreetMapのデータから道路のポリラインを取り込んでみる。

道路データをダウンロードしておく

今回は関東地方のデータのみ。

wget https://download.geofabrik.de/asia/japan/kanto-latest.osm.pbf

PostgreSQLに取り込みをするコマンドを実行

sudo apt install osm2pgsql # 初回のみ
osm2pgsql --create --database=tileserv --slim --username=tileserv --password --host=localhost kanto-latest.osm.pbf

ブラウザで確認

planet_osm_roadsなど、取り込んだデータが表示されている

クリックして表示。
道路のポリラインが表示されている

ここまでのメモ

これだけでタイルサーバー簡単にできた。
仕組みは、 pg_tileservgeometry(LineString, xxx) のあるテーブルを認識し、配信対象のタイルデータとして自動的に認識するから。
pg_tileservの再起動は必要なく、テーブルを生成したらすぐに配信対象になる。
これだけだと、ただ単にOSMに入っているパスが全て表示されるだけ。

大量データを配信する方法

表示したいものだけを配信するにはいくつか方法がある。

a. view/materialized viewを作って動的に表示
b. pg_tileservのfilter機能を使って絞り込んだデータを配信する(CQL)
c. 配信対象のデータのみに絞ったテーブルを事前に用意する
d. フロント側で表示制御
e. pg_tileservのFunctionLayerを作って対応する

今回の通行実績描画では、c、d、eを組み合わせて実現した。

試したこと

a. view/materialized viewを作って動的に表示
最初に試したのはこれ。
viewやmaterialized viewだとお手軽に実行できるものの、
データの件数が数十万を超えたあたりで遅すぎてしまった。
テーブル分割やindexを生成したりと試したが、1日あたり5億件のレコードを捌くのには無理があり。
さらに、viewの内容から少しでも絞り込みを行おうとすると、viewに対してwhere句がついてくるため、
劇的に遅くなった。
materialized viewの場合は内部にテーブルが生成されるが、テーブル生成に大変時間がかかったのでNG。

b. pg_tileservのfilter機能を使って絞り込んだデータを配信する(CQL)
https://github.com/CrunchyData/pg_tileserv/blob/master/hugo/content/usage/cql.md
こちらは面白い機能で、CQLという言語でフィルタリングの条件を記述すると、
where句のように絞り込みができる。

c. 配信対象のデータのみに絞った道路テーブルを事前に用意する
5分間隔で配信対象データが増えていくため、5分毎にテーブルを分けてしまう案を試した。
パーティショニングも当然試したが、データ生成時間・データ量共にすごいことになってしまったので不採用。
最終的には 配信対象の通行実績でテーブル分割 -> 道路のパスを結合
という構造にした。

d. フロント側で表示制御
描画ライブラリはいつものようにdeck.glを使っている。
deck.glではPolylineの描画時に色を選択できるため、
表示非表示の切り替えをここでも行なっている

const layer = new MVTLayer({
  data: TILE_URL,
  getLineColor: (f) => {
    if (f.properties.hogespeed < 30) {
      return COLOR_ROAD_TRANSPARENT
    }
  }
})

e. pg_tileservのFunctionLayerを作って対応する

配信対象の通行実績でテーブル分割 -> 道路のパスを結合
の、配信対象通行実績の選択 & データの結合 をFunction Layerで実現した。
Function LayerはPostgreSQLのfunctionの実行結果をタイルレイヤーとして配信できる機能。
ここでのクエリの作り方を工夫することでアクセスするデータ量を減らしている。

FunctionLayerの作り方

functionでクエリを作る際に

  1. 描画するLineString geometry(LineString,3857) のカラムが入ったテーブルを用意する
    (今回はosmのデータをそのまま取り込んで生成されたplanet_osm_roads)
  2. 表示対象のデータが入った history{yyyymmdd} テーブルを用意する ex. history20240225
    planet_osm_roads.osm_id と history.osm_id でjoin
  3. ST_Intersectsでタイルサーバーにリクエストがあった領域に交差しているデータのみを返すようにWHERE句を作る
  4. フロントから呼んでテスト
const layer = new MVTLayer({
        data: `http://localhost:7800/public.tile/{z}/{x}/{y}.pbf?yyyymmdd=${yyyymmdd}`
})

といった流れになる。

実際のものとは違うけども、Function Layerを実現するsqlはこれ。
上記でやっている内容がSQLとして記述してある。

create function tile(z integer, x integer, y integer, yyyymmdd text) returns bytea
    stable
    parallel safe
    language plpgsql
as
$$
DECLARE
    result bytea;
    query  text;
BEGIN
    query := 'WITH bounds AS (SELECT ST_TileEnvelope($1, $2, $3) AS geom),
             filtered_roads AS (SELECT planet_osm_roads.way,
                                                bounds.geom AS bounds_geom,
                                         FROM public.planet_osm_roads planet_osm_roads
                                                  JOIN public.' || quote_ident('history') || yyyymmdd || ' history
                                                       ON planet_osm_roads.osm_id = history.osm_id,
                                              bounds
                                         WHERE
                                           ST_Intersects(planet_osm_roads.way, ST_Transform(bounds.geom, 4326))),
             mvtgeom AS (SELECT ST_AsMVTGeom(ST_Transform(geom, 3857), bounds_geom) AS geom,
                         FROM filtered_roads)
             SELECT ST_AsMVT(mvtgeom, ''default'') FROM mvtgeom';

    EXECUTE query INTO result USING z, x, y, yyyymmdd;

    RETURN result;
END;
$$;

まとめ

  • docker-compose.ymlで簡単にタイルサーバーを起動できる
  • geometry(LineString,3857)のデータを用意すればPolyLineを簡単に配信してくれる。
  • 配信対象のデータ制御は複数の方式があるけども、複雑なことをしたい場合はFuncion Layerが使える

感想

pg_tileservでサクッとタイルサーバーを作れたのがとても便利で、
大量のデータを処理しないのであればFunction Layerで実現する必要はなく、
pg_tileservにリクエストを送る際のフィルタリング機能で十分。そして簡単。なんならviewでもいい。

処理するデータが重くなってきたら、段階的にこの記事内に書かれている方式を作っていくとスピードを維持したまま拡張していけることが分かった。
実際にはGoogle Load BalancingにCDNのくっつけてキャッシュを効かせて高速配信する構造にしたけど、
こちらも少ないデータでの検証フェーズであれば必要なく、少しずつ拡張していけて便利。

pg_tileservは簡単にタイルサーバーを構築できて素敵なOSSってことが分かった🕺

レスキューナウテックブログ

Discussion