H2O をいじくったタイルサーバの性能かくにん

#概略
[先日投稿した](http://qiita.com/ntabee/items/5f5394523b4760b7a4d6)、[H2O](https://github.com/h2o/h2o)をいじった[OpenStreetMap 用のタイルサーバ](https://github.com/ntabee/h2o-tile)の性能を測ってみた。

#tl;dr

- H2Oベースの実装は、既存の nginx ベースのタイルサーバより**3倍**速い
- ただし、この性能差はソフトウェア **ではなく**、主に**データ形式の違い**によるもの

#実験条件

##実行環境

- 仮想4コア・メモリ8G の VMWare の仮想マシン
- ゲスト OS は Debian 8.0: 
  `Linux debian 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt9-3 (2015-04-23) x86_64 GNU/Linux`
- ホストは i7-4770K の Windows 8.1

[nginxのパラメータチューニングとh2o](http://qiita.com/cubicdaiya/items/235777dc401ec419b14e)を~~パク~~参考に、`net.*` 系のカーネルパラメタを設定しておく。

##比較対象

nginx に Lua をバインドして、タイルサーバ用のスクリプトを入れたもの。具体的には、[OSM Japan](http://osm.jp/) が公開している [tileman](https://github.com/osmfj/tileman) から、コードと設定を借りてくる。

nginx 自体は、[ngx_openresty](http://openresty.org/) (Lua バインドが最初から入ってる) の[最新版 (1.7.10.1)](http://openresty.org/download/ngx_openresty-1.7.10.1.tar.gz) をソースで落として、`gcc -mtune=native -march=native -O3` でビルドする:

```bash
$ wget http://openresty.org/download/ngx_openresty-1.7.10.1.tar.gz
$ export CFLAGS="-O3 -mtune=native -march=native"
#sudo so that /sbin/ldconfig is visible
$ sudo ./configure --with-luajit --with-cc=gcc --with-cc-opt="$CFLAGS" --with-luajit-xcflags="$CFLAGS" --prefix=/usr --conf-path=/etc/nginx/nginx.conf
$ sudo make -j 4
$ sudo make install
```

##実験方法

### リクエストするURL
あらかじめレンダリングが済んだ東京周辺の地図に対して、ズームレベル 0～17 までのタイル[^1] (約77万ファイル) をひたすらリクエストする:
![image](https://qiita-image-store.s3.amazonaws.com/0/49976/21197434-8ae9-9cf9-59f4-4ba5d14b85f9.png)

[^1]: 今更だが、「タイル」というのは地図の一部分を表す画像データだ。大抵は 256x256 ピクセルの PNG を使う。この PNG をブラウザ側でがんばってずらっと縦横に並べることで、地図として見えるわけだ。

タイルの URL (の、パス名部分) はこんな感じだ:

```bash
$ cat paths.txt
/tiles/0/0/0.png
/tiles/1/1/0.png
/tiles/2/3/1.png
/tiles/3/7/3.png
...
/tiles/17/116822/51974.png
/tiles/17/116823/51974.png
/tiles/17/116824/51974.png
$ wc -l paths.txt
770534 paths.txt
``` 

### 負荷のかけ方
wrk に以下のスクリプトを食わせて、リストファイルのパスを順番にフェッチさせていく:

```lua
-- traverse.lua
paths = io.lines("paths.txt")
request = function()
    p = paths()
    if (p == nil) then
      paths = io.lines("paths.txt")
      p = paths()
    end
    return wrk.format(nil, p)
end
```

`$ wrk --latency -c 1000 -t 4 -d 10 -s traverse.lua http://localhost:8080/`

なお、wrk を別ホストにするのもやってみたが、VLAN帯域を食いつぶしてマトモな測定にならなかったので、サーバと同じホストで動かすことにした。

## タイルデータの形式

H2O でサーブするタイルデータは、各タイルが個別の .png ファイルになっている。つまり、今回の場合だと、約77万個の .png が特定のディレクトリの下に存在していることになる (もちろん、階層はフラットではないが)。

一方、nginx の実装は、「**メタタイル**」といって、8x8 = 64 枚の .png を tar のようにくっつけた形式のファイルを使う。
このくっつけたメタタイルの中から、「しかるべき .png 一枚分」を切り取ってクライアントに返すわけだ。

というわけで、H2O用の .png 77万枚に加えて、それと等価な nginx 用のメタタイルを用意した。
メタタイルの作り方は割愛するが、[mod_tile](https://github.com/openstreetmap/mod_tile) という Apache モジュールに付属のオフラインレンダラーを使う。

なお、公平のため、上記2種類のデータセットは同じファイルシステムに置いた。

## サーバソフトウェアの設定

各サーバソフトの設定は、以下のようにした。これも[比較記事](http://qiita.com/cubicdaiya/items/235777dc401ec419b14e)の設定を大いに~~パク~~参考にした。

### H2O

`tiles.conf`

```
# to find out the configuration commands, run: h2o --help
num-threads: 8
#num-name-resolution-threads: 1
max-connections: 10240
mapnik-datasource: /usr/local/lib/mapnik/input
mapnik-fonts: /usr/local/lib/mapnik/fonts
mapnik-fonts: /usr/share/fonts

listen: 8080
hosts:
  "127.0.0.1.xip.io:8080":
    paths:
      /tiles:
        tile.dir: /opt/osm/tiles
        tile.style: /opt/osm/openstreetmap-carto/osm.xml
        expires: 1 day
      /:
        file.dir: /opt/osm/www
    access-log: /dev/null
#    access-log: /opt/osm/log/access_log
```

- スレッド数 = 8
- 最大コネクション = 10240
- アクセスログはとらない
- タイルは `/opt/osm/tiles` の下にあるよ

というのが大事で、あとはわりとどうでもいい。

### nginx

`/etc/nginx/nginx.conf`

```
worker_processes  8;

events {
    worker_connections  10240;
    accept_mutex_delay 100ms;
}

http {
lua_package_path "/usr/local/lib/lua/?.lua";
    include       mime.types;
    default_type  application/octet-stream;

    access_log off;

    sendfile on;
    open_file_cache max=100 inactive=20s;
    tcp_nopush on;

    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;
        location / {
            root   /opt/osm/www;
            index  index.html;
        }

        location /tiles {
            access_by_lua '
              local osm_tile = require "osm.tile"
              local tirex = require "osm.tirex"
              local map = ""
              local x, y, z = osm_tile.get_cordination(ngx.var.uri, "/tiles", "png")
              -- DO NOT ask tirex to render it (tirex is not set-up properly)
              --local priority = 1
              --local ok = tirex.request(map, x, y, z, z, priority)
              --if not ok then
              --    return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
              --end
              local tilefile = osm_tile.xyz_to_metatile_filename(x, y, z)
              local tilepath = "/opt/osm/ng_tiles/"..map.."/"..tilefile
              local png, err = osm_tile.get_tile(tilepath, x, y, z)
              if png then
                  ngx.header.content_type = "image/png"
                  ngx.print(png)
                  return ngx.OK
              end
              return ngx.exit(ngx.HTTP_NOT_FOUND)
            ';
            root /opt/osm/tiles/;
        }   
    }
}
```

スレッド数などは当然 H2O と合わせてある。
本題は `access_by_lua` のところで、ざっくり言うと

1. リクエストURLをパーズして `x, y, z` (座標とズームレベル) を求めよ
2. それを元に、しかるべきメタタイルの、しかるべき部分を切り出せ
3. 切り出した画像を送り返せ
4. メタタイルがなかったり、切り出せなかったら単に 404 を返せ。レンダラーに仕事を投げるとか余計なことは今回はしなくてもいい

ということをやっている。
本来は、メタタイルがなかったらバックエンドのレンダラーにお仕事を投げるのだが、今回、そのバックエンドを用意できなかったので該当箇所をコメントアウトした。
余計な仕事をコメントアウトしただけなので、これで nginx 不利になったりはしないハズだ。

#結果

`$ wrk --latency -c 1000 -t 4 -d 10 -s ...` を、H2O と nginx それぞれに対して 10 回ずつ測定した。10回の中で最も結果が良かったものを下に載せる:

## H2O

```
Running 10s test @ http://localhost:8080/
  4 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.08ms    4.70ms 210.40ms   91.20%
    Req/Sec    38.46k    12.43k   99.92k    77.72%
  Latency Distribution
     50%    2.98ms
     75%    5.07ms
     90%    8.32ms
     99%   18.54ms
  1535421 requests in 10.07s, 7.11GB read
Requests/sec: 152439.26
Transfer/sec:    722.99MB
```

**152k req./秒** ぐらい。

## nginx

```
Running 10s test @ http://localhost/
  4 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    92.52ms  232.91ms   1.75s    91.27%
    Req/Sec    11.69k     5.73k   28.45k    67.00%
  Latency Distribution
     50%    5.69ms
     75%   48.56ms
     90%  257.60ms
     99%    1.17s 
  466463 requests in 10.03s, 3.20GB read
  Socket errors: connect 0, read 0, write 0, timeout 271
Requests/sec:  46499.53
Transfer/sec:    327.15MB
```

**46.5k req./秒** ぐらい。

#考察

というわけで、今回の測定では、H2O をいじったタイルサーバは、既存の nginx の実装を3倍ぐらい outperform しており、実際スゴイ。
ただし、この差は **ソフトウェアの性能差ではなく**、ただの .png とメタタイルという **データ形式の違い** が大きいと考えられる。
「考えられる」といいつつ実証とかしてないのだが・・・メタタイルを毎回 `fseek()` して切り取って・・・っていかにも遅そうやん？

#まとめ

- メタタイルを使わないタイルサーバはすごく速くできる、可能性がある
- じゃあ、メタタイルっていらないんじゃ・・・

なぜ「メタタイル」という一見余計なレイヤーが標準的になったのか、は、機会があれば解説する。明日から頑張る。本当だよ。