LoginSignup
13
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-05-20

概略

例の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 にコードをつぎたして、

  1. リクエストのURLがタイルの名前のように見え、かつローカルストレージにまだキャッシュされていない時、だけ
  2. 上流 (OSM 本家とか) にリクエストを投げて
  3. 返事が来たら、出力ストリームのフィルタ (そういう仕組みが 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 のコントリビュータの皆様に感謝いたします。


  1. ただし、レンダリングのスタイルやタイルの更新ポリシーなどを完全にコントロールしたい場合はこの限りではない。 

  2. 少なくとも、OSM Japan の tile.openstreetmap.jp に関しては、適切なアクセスレートの制限などの対策をすれば上流として使っても構わないとのこと。<DEL>ただし、このようなキャッシュサーバをサービス製品に使ってもいいのかは、まだちゃんと確認していない。</DEL> 

13
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14