#概略
例の、H2O に手を加えた OpenStreetMap のタイルサーバに、caching proxy の機能を実装しました。
#背景
OpenStreetmap (以下 OSM) の地図を使ったサービスで使う場合、OSM は規約上、
- 商用アプリとかでは http://openstreetmap.org でホストされている地図に直接アクセスしないでね。
というタテマエになっているので、真面目にやるなら、自前でタイルサーバを立てなければいけない (復習)。
ところが、自前で生データからタイル画像を生成するサーバを真面目に立てようとすると、
- 30GB ぐらいの OSM の生データをダウンロードする
- それを 600GB ぐらいの PostGIS のデータベースに落とす
- PostGIS と HTTPD の間をよろしく取り持ってくれる拡張モジュールなりミドルウェアなりなんなりを入れる
- 表示かくにん用の HTML を用意して、ちゃんと地図が表示されるかかくにん (大抵、地球全部沈没した真っ青な世界地図がかくにんされる。よかった!)
といった非常に面倒くさい手続きを踏まねばならず、各ステップでそれぞれ1つ、命、落とす。
モチベーション
地図のサービスを作るなら、要は地図が出せればそれでいいんです1。
というわけで、OSM本家などのタイルサーバから取ってくる caching proxy タイプのものを作ってみた2。
#目標
OSMの生データや PostGIS のデータベースを 一切 使わずに地図を出せること。
#方針
H2O の proxy handler にコードをつぎたして、
- リクエストのURLがタイルの名前のように見え、かつローカルストレージにまだキャッシュされていない時、だけ
- 上流 (OSM 本家とか) にリクエストを投げて
- 返事が来たら、出力ストリームのフィルタ (そういう仕組みが H2O にはある) でローカルに保存
という方針で実装した。
適当に抜粋する。初期化とかエラー処理とかよけえなところは省く:
/* 上流からレスポンスがきたらここを通す */
static void store_data(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, int is_final)
{
struct st_store_tile_t *self = (void *)_self;
int i;
/* ファイルを開いて、やってきたバッファを書いていく */
if (self->fd < 0) {
/* Race 対策として、適当な名前の tmp ファイルにまず保存して、全部終わったら「本当の」名前に rename() */
self->fd = open(self->tmp_tile_path.base, O_WRONLY | O_TRUNC | O_CREAT, 0666);
}
if (self->fd < 0) {
/* エラー。もうあきらめろ */
...
goto Cont; /* 失敗したら、保存はあきらめて、クライアントに返事を投げる人に仕事を丸投げだけはしておく。以下同様 */
}
for (i=0; i != inbufcnt; ++i) {
int v = write(self->fd, inbufs[i].base, inbufs[i].len);
if (v != inbufs[i].len) {
/* エラー。もうあきらめろ */
...
goto Cont;
}
}
if (is_final) {
close(self->fd);
if (rename(self->tmp_tile_path.base, self->local_tile_path.base) != 0) {
h2o_req_log_error(req, "lib/handler/tile-proxy.c", "Failed to rename the tmp file %s to %s: %s\n", self->tmp_tile_path.base, self->local_tile_path.base, strerror(errno));
/* tmp ファイルの rename に失敗したら何回かリトライさせているが、ここはこんなことしていいのか? */
for (i=0; i<32; ++i) {
usleep(10);
if (rename(self->tmp_tile_path.base, self->local_tile_path.base) == 0) {
}
h2o_req_log_error(req, "lib/handler/tile-proxy.c", "Failed to rename the tmp file %s to %s: %s\n", self->tmp_tile_path.base, self->local_tile_path.base, strerror(errno));
}
}
}
Cont:
h2o_ostream_send_next(&self->super, req, inbufs, inbufcnt, is_final);
}
/* transfer-encoding: chunked のレスポンスがきたらここを通す */
static void store_chunked_data(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, int is_final)
{
...
/* とりあえずメモリに全部ためておいて、最後に phr_chunked_decoder でゴミとり、という雑な方針 */
/* タイルの PNG 画像で何MBもいかないしこれでええやろ */
/* calc chunk size */
for (i = 0; i != inbufcnt; ++i) {
self->chunked_content_buf = h2o_concat(&req->pool, self->chunked_content_buf, inbufs[i]);
}
if (is_final) {
struct phr_chunked_decoder chunked_decoder = {};
char* buf = self->chunked_content_buf.base;
size_t newsz = self->chunked_content_buf.len;
switch (phr_decode_chunked(&chunked_decoder, buf, &newsz)) {
case -1: /* error */
/* エラー。もうあきらめろ */
...
goto Cont;
case -2: /* incomplete */
/* エラー。もうあきらめろ */
...
goto Cont;
default: /* complete */
break;
}
write(self->fd, buf, newsz);
close(self->fd);
if (rename(self->tmp_tile_path.base, self->local_tile_path.base) != 0) {
/* ここも rename を何回かリトライさせているが、こんな実装で大丈夫か? */
...
}
}
Cont:
h2o_ostream_send_next(&self->super, req, inbufs, inbufcnt, is_final);
}
static void on_setup_ostream(h2o_filter_t *_self, h2o_req_t *req, h2o_ostream_t **slot)
{
...
/*
req->path_normalized.base is of the form "/z/x/y.png"
*/
if ( likely(
req->res.status == 200 &&
tile_rewrite_path(req->path_normalized.base, "/", 1, physical_tile_path, 28, &z, &x, &y)) ) {
/* 上流からのお返事で、タイルのように見えるURL z/xxx/yyy.png なら、ローカルに保存する */
...
/*
Now, physical_path is of the form /z/nnn/nnn/nnn/nnn/nnn.png
*/
...
store_tile = (void *)h2o_add_ostream(req, sizeof(struct st_store_tile_t), slot);
...
store_tile->super.do_send = store_data; /* 上流からのお返事は store_data() を通す */
if ((txfer_enc_idx = h2o_find_header(&(req->res.headers), H2O_TOKEN_TRANSFER_ENCODING, SIZE_MAX)) != -1) {
h2o_iovec_t *txfer_enc = &req->res.headers.entries[txfer_enc_idx].value;
if (h2o_memis(txfer_enc->base, txfer_enc->len, H2O_STRLIT("chunked"))) {
/* ただし、transfer-encoding: chunked の場合は、store_chunked_data() を通す */
store_tile->super.do_send = store_chunked_data;
...
}
}
...
}
h2o_setup_next_ostream(&self->super, req, slot);
}
static int on_req_tile(struct st_h2o_handler_t *_self, h2o_req_t *req) {
...
char* tile_path = req->path_normalized.base + req->pathconf->path.len; /* is of the form z/x/y.png */
/* タイルのように見えるURL z/xxx/yyy.png で */
if (likely(tile_rewrite_path(tile_path, "", 0, physical_tile_path, 28, &z, &x, &y))) {
h2o_iovec_t full_path = h2o_concat(&req->pool, self->local_base_path, h2o_iovec_init(physical_tile_path, strlen(physical_tile_path)));
/* だがすでにローカルにあるファイルはそれを返す */
if (likely(access(full_path.base, F_OK) == 0)) {
return h2o_file_send(req, 200, "OK", full_path.base, h2o_iovec_init(H2O_STRLIT("image/png")), 0);
}
}
/* proxy.c 本来の実装に丸投げ で */
return self->on_req_delegate(_self, req);
}
h2o_tile_proxy_handler_t *h2o_tile_proxy_register(h2o_pathconf_t *pathconf, const char *local_base_path, const char *proxy) {
h2o_iovec_t local_base_path_v = h2o_strdup_slashed(NULL, local_base_path, SIZE_MAX);
h2o_tile_proxy_handler_t *self;
do { /* scoping */
/* Initialize the handler */
h2o_proxy_config_vars_t proxy_config = { 10*1000, 0, 2000 };
self = (void*)h2o_create_handler(pathconf, sizeof(*self));
h2o_url_parse(proxy, strlen(proxy), &self->super.upstream);
self->super = *(h2o_proxy_register_reverse_proxy(pathconf, &self->super.upstream, &proxy_config));
self->local_base_path = local_base_path_v;
self->on_req_delegate = self->super.super.on_req;
self->super.super.on_req = on_req_tile;
} while (0);
do { /* scoping */
struct st_h2o_tile_store_filter_t *self = (void *)h2o_create_filter(pathconf, sizeof(*self));
self->local_base_path = local_base_path_v;
self->super.on_setup_ostream = on_setup_ostream;
} while (0);
return self;
}
#現状 (げんじつ)
上流を複数設定してラウンドロビンさせたり、上流が落ちてたら自前のレンダラーに投げたり (またはその逆) したかったのだが、H2O のリクエスト処理の設計で、どうやれば実現できるのかわからなかった。
ドキュメント等は、今後ゆっくりと整備していく。明日から絶対がんばる。本当だよ。
#デモ
以前 AWS に立てたデモサーバを入れ替えた: http://h2o-tile-demo.ddns.net:8080/
今までは、東京周辺の一部以外は真っ白な哀しい状態だったのが、どこにアクセスしてもその場所の地図が出てくる...ハズだ。
ただし、ストレージが 20G しかないのであんまりいじめないでね。
#謝辞
@kazuho さんをはじめ、本家 H2O 開発者の皆様、タイルサーバの利用規約についてアドバイスを頂いた、@nyampireさんをはじめとするOSMFJの皆様、および OpenStreetMap のコントリビュータの皆様に感謝いたします。