P2P地震情報の API サーバー (api.p2pquake.net) は、毎日 1000 万リクエスト以上を捌いている。ピーク時は毎秒 300 リクエストを超える。
VPS 1 台でここまで到達するのにそこそこ試行錯誤した。結果として意外性はなくやることやっただけという感じではあるのだが、アーキテクチャーや設定値を整理して記事にしておく。
前提となる構成、及び状況
- Linode Dedicated 4GB プラン x 1 台
- CPU:
AMD EPYC 7601 32-Core Processor
2 コア (専有)
- CPU:
- 公開 API は GET (読み取り) のみ
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: キャッシュ期限切れでアプリケーションサーバーへのリクエストが必要になったときに、同じ URI のリクエストに対し 1 個だけリバースプロキシして他はそれを待つという挙動となる。
- proxy_cache_use_stale: アプリケーションサーバーへのリクエストが失敗した場合、期限切れのキャッシュがあれば使う。
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 アクセスが多い。