にゃみかんてっくろぐ

猫か百合を見守る壁になりたい

毎日 1000 万リクエストを捌く 1 台の API サーバー

P2P地震情報API サーバー (api.p2pquake.net) は、毎日 1000 万リクエスト以上を捌いている。ピーク時は毎秒 300 リクエストを超える。

VPS 1 台でここまで到達するのにそこそこ試行錯誤した。結果として意外性はなくやることやっただけという感じではあるのだが、アーキテクチャーや設定値を整理して記事にしておく。

前提となる構成、及び状況

  • Linode Dedicated 4GB プラン x 1 台
    • CPU: AMD EPYC 7601 32-Core Processor 2 コア (専有)
  • 公開 API は GET (読み取り) のみ
    • 書き込みは地震情報・津波予報の発表時などに限定

Nginx, Go + Gin, MongoDB の 3 層構造

DB: MongoDB Capped Collections

DB はドキュメント指向データベースである MongoDB を使用していて、 API のレスポンスほぼそのままの形でデータを格納している。 2015 年頃から運用中(それ以前は CSV ファイルベース)。

> db.whole.find({ code: 551 }).sort({ $natural: -1 }).limit(1)
{ "_id" : ObjectId("63244a1551d38bb17b2e7c91"), "issue" : { "time" : "2022/09/16 19:04:04", "type" : "DetailScale", "correct" : "None", "source" : "気象庁" }, "timestamp" : { "convert" : "2022/09/16 19:04:05.177", "register" : "2022/09/16 19:04:05.204" }, "user_agent" : "jmaxml-seis-parser-go, relay, register-api", "ver" : "20220813", "earthquake" : { "foreignTsunami" : "Unknown", "time" : "2022/09/16 19:00:00", "hypocenter" : { "name" : "東京湾", "latitude" : 35.5, "longitude" : 140, "depth" : 20, "magnitude" : 2.5 }, "maxScale" : 10, "domesticTsunami" : "None" }, "points" : [ { "pref" : "千葉県", "addr" : "市原市姉崎", "scale" : 10, "isArea" : false } ], "code" : 551, "time" : "2022/09/16 19:04:05.204" }

最大の特徴は Capped Collections を使用していること。これはリングバッファー相当のもので、コレクションを一定サイズに保つことができ、古いドキュメントは自動的に消えていく(上書きされる)。直近の情報を提供できればよいP2P地震情報にぴったり合う。

ストレージエンジン WiredTiger のキャッシュは最小の 256 MB 。

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
systemd+    1750 11.9  9.1 2210568 366816 ?      Ssl  Jul09 10836:08 mongod --wiredTigerCacheSizeGB=0.25 --auth --bind_ip_all

速度

速い。

P2P地震情報では 32 MB の小さな Capped Collection で運用していて、直近 10 件の地震情報を探す時間は 1 ms くらい。

> db.whole.stats()
{
        "ns" : "p2pquake.whole",
        "size" : 33553274,
        "count" : 12057,
        "avgObjSize" : 2782,
        "storageSize" : 8462336,
        "freeStorageSize" : 2072576,
        "capped" : true
        (略)
}
> db.whole.find({ code: 551 }).sort({ $natural: -1 }).limit(10).explain("executionStats")["executionStats"]["executionTimeMillis"]
1
> db.whole.find({ code: 551 }).sort({ $natural: -1 }).limit(10).explain("executionStats")["executionStats"]["executionTimeMillis"]
0

Tailable Cursors

Capped Collections を使うメリットはもう 1 つあって、それが Tailable Cursors 。これは tail -f 相当で、簡易的な Pub/Sub として使える。

プッシュ通知、 WebSocket API 配信などで活用している。厳密には計測していないが、レイテンシは 1 ms あるかないか程度。

アプリケーションサーバー: Go

もともと Ruby + Sinatra だったが、新しい API については Go + Gin で作っている。古い API も徐々に Go + Gin に移行したい。

[GIN] 2022/09/10 - 08:42:13 | 200 | 2.064529ms | xx.xxx.xxx.xxx | GET "/v2/history?codes=551"
[GIN] 2022/09/10 - 08:42:14 | 200 | 19.144558ms | xx.xxx.xxx.xxx | GET "/v2/history?codes=551&limit=40"

Web サーバー: Nginx

アプリケーションサーバーへのリクエストをとにかく減らす ため、キャッシュ設定を諸々行っている。

proxy_cache_lock イメージ. アプリケーションサーバへのリクエストは 1 つだけ

server ブロック抜粋:

proxy_cache_use_stale timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 30s;
proxy_cache_valid 200 1s;
proxy_cache_valid 302 1h;
proxy_connect_timeout 5s;

proxy_next_upstream error timeout http_504 http_502;

location /v2/ {
    limit_req zone=apiv2 burst=10;
    proxy_cache cache;
    proxy_pass http://127.0.0.1:10001;
}
location /v2/jma {
    limit_req zone=apiv2jma burst=10;
    proxy_cache cache;
    proxy_cache_valid 200 10s;
    proxy_pass http://127.0.0.1:10001;
}

Nginx に 142 requests/s 来ているが、 アプリケーションサーバーに到達しているのはわずか 6% の 8.13 requests/s である。

あとはファイルディスクリプタ数の上限worker_connections を引き上げたり、 net.ipv4.tcp_tw_reuse を 1 にしたり、くらい。

課題

TLS の負荷(たぶん)

諸々の結果、いま最も CPU リソースを消費しているのは Nginx になっている。

HTTPS ではなく HTTP でベンチマークを流すとこんな負荷は掛からないので、 TLS の負荷だと推測している。

top - 11:40:50 up 63 days, 15:47,  1 user,  load average: 0.42, 0.84, 1.05
Tasks: 247 total,   2 running, 245 sleeping,   0 stopped,   0 zombie
%Cpu(s): 15.2 us, 14.3 sy,  0.0 ni, 68.0 id,  0.0 wa,  0.0 hi,  2.5 si,  0.0 st
MiB Mem :   3931.3 total,    358.7 free,   2375.9 used,   1196.6 buff/cache
MiB Swap:   4608.0 total,   3882.1 free,    725.9 used.   1208.3 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
1676684 systemd+  20   0  163092  79328  32748 S  12.5   2.0 496:19.37 nginx: worker process
   1750 systemd+  20   0 2210568 379080  10656 S   8.2   9.4  10913:42 mongod --wiredTigerCacheSizeGB=0.25 --auth --bind_ip_all
2802253 root      20   0  749316  67716   9608 S   5.2   1.7   3185:14 /usr/bin/cadvisor -logtostderr --global_housekeeping_interval=5m0s --housekeeping_in+
    524 root      20   0 2640424 151960  18512 S   3.8   3.8   3667:06 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
2243005 root      20   0  721888  26240  14152 S   2.7   0.7  25:08.70 ./web-api-v2
2242527 root      20   0  721056  28592  14348 S   1.9   0.7  25:02.20 ./web-api-v2

今後負荷が問題になってきたら、 ECDSA 証明書を試すか、 Load Balancer に TLS を終端してもらうかで考えている。

数百万件残っていたHTTPのはてなブログを4年越しにすべてHTTPS化させた話 - Hatena Developer Blog

最後に

まだまだ IPv4 アクセスが多い。

CPU, Disk I/O, Network I/O の状況. CPU は Max 200% であることに注意