pg_tileservで大量データを配信するタイルサーバーを作る方法
道路の通行実績データをマップに表示する機能を作った 🚗=3
これ。
マップに表示するには通行実績データをタイルサーバーで配信する必要があるので、
今回はサクッとタイルサーバーを起動する方法を試した🗺️
使ったOSSは 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_tileserv
が geometry(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)
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でクエリを作る際に
- 描画するLineString
geometry(LineString,3857)
のカラムが入ったテーブルを用意する
(今回はosmのデータをそのまま取り込んで生成されたplanet_osm_roads) - 表示対象のデータが入った
history{yyyymmdd}
テーブルを用意する ex. history20240225
planet_osm_roads.osm_id と history.osm_id でjoin - ST_Intersectsでタイルサーバーにリクエストがあった領域に交差しているデータのみを返すようにWHERE句を作る
- フロントから呼んでテスト
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