H2O をいじくって OpenStreetMap の Caching Proxy にしてみた

#概略

[例の](http://qiita.com/ntabee/items/5f5394523b4760b7a4d6)、[H2O](https://github.com/h2o/h2o) に手を加えた OpenStreetMap のタイルサーバに、caching proxy の機能を実装しました。

#背景

[OpenStreetmap](http://openstreetmap.org) (以下 OSM) の地図を使ったサービスで使う場合、OSM は規約上、

- 商用アプリとかでは http://openstreetmap.org でホストされている地図に直接アクセスしないでね。

というタテマエになっているので、真面目にやるなら、**自前でタイルサーバ**を立てなければいけない (復習)。

ところが、自前で生データからタイル画像を生成するサーバを真面目に立てようとすると、

- 30GB ぐらいの OSM の**生データ**をダウンロードする
- それを 600GB ぐらいの **PostGIS** のデータベースに落とす
- **PostGIS と HTTPD の間**をよろしく取り持ってくれる拡張モジュールなりミドルウェアなりなんなりを入れる
- **表示かくにん**用の HTML を用意して、ちゃんと地図が表示されるかかくにん (大抵、地球全部沈没した真っ青な世界地図がかくにんされる。よかった！)

といった非常に面倒くさい手続きを踏まねばならず、各ステップでそれぞれ1つ、命、落とす。

## モチベーション

地図のサービスを作るなら、要は**地図が出せればそれでいいんです**[^1]。
というわけで、OSM本家などのタイルサーバから取ってくる caching proxy タイプのものを作ってみた[^2]。

[^1]: ただし、レンダリングのスタイルやタイルの更新ポリシーなどを完全にコントロールしたい場合はこの限りではない。
[^2]: 少なくとも、**OSM Japan の tile.openstreetmap.jp** に関しては、**適切なアクセスレートの制限などの対策**をすれば上流として使っても構わないとのこと。~~＜DEL＞ただし、このようなキャッシュサーバをサービス製品に使ってもいいのかは、**まだちゃんと確認していない**。＜/DEL＞~~

#目標

OSMの生データや PostGIS のデータベースを **一切** 使わずに地図を出せること。

#方針

H2O の [proxy handler](https://github.com/h2o/h2o/blob/master/lib/handler/proxy.c) にコードをつぎたして、

1. リクエストのURLがタイルの名前のように見え、かつローカルストレージにまだキャッシュされていない時、だけ
2. 上流 (OSM 本家とか) にリクエストを投げて
3. 返事が来たら、出力ストリームのフィルタ (そういう仕組みが H2O にはある) でローカルに保存

[という方針で実装](https://github.com/ntabee/h2o-tile/blob/tile/lib/handler/tile-proxy.c)した。
適当に抜粋する。初期化とかエラー処理とかよけえなところは省く:

```c
/* 上流からレスポンスがきたらここを通す */
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 のコントリビュータの皆様に感謝いたします。