Rubyエンジニアはsleep 1で殺せる、をngx_mrubyのAsync.sleepで乗り越える
そろそろ今年のISUCON、ISUCON8まであと1ヶ月ちょっとになったのでひとりリハーサルをそろそろはじめてるさぼです。去年ISUCON7に初挑戦してRubyを選択してsleep 1が乗り越えられず人権を失いました(原因は他にもあるけど...)。
ngx_lua での sleep
去年、予選終わったあとに感想戦で聞いた、Openresty、ngx_luaを使ったsleep方法を試したりしました。Rubyでsleepを行うと処理をブロッキングしてしまうので、nginx側で1秒待った上でRubyに処理を渡すことでRuby側では処理を待つ必要がなくって捌けるリクエストを増やす算段です。
昨日はnginx-buildでlua使ってngx.sleep実装出来たので満足。だいたい今の実力でできそうなところ、だいたいやりきったので今回のISUCONはおわりかなー
— さぼ@ギークハウス沖縄 (@saboyutaka) 2017年10月26日
Async.sleepを実装した ngx_mruby v2 がリリースされる
今年のRubyKaigi2018での @matsumotory さんの発表で、nginxでのmruby実装である ngx_mruby module のv2をリリースする話をしていて、その中でnon-blockingなsleepが出来る Nginx::Async.sleep が出来るという話でした。今回はこれを次のISUCONまでには使えるようにして sleep 1 に対策出来るようにしておこうと思って実装してみました。
実装
@matsuu さんが公開しているVagrantfileからアプリケーションを立ち上げて、なんとなくDocker化するのが最近趣味なのでDocker composeで置き換えて実装しました。
dokcer-compose の設定
isucon7q/docker-compose.yml at 201808 · saboyutaka/isucon7q · GitHub
# dokcer-compose.yml 抜粋
version: '3'
services:
web:
build: .
command: "bundle exec rackup -p 8000 -o 0.0.0.0"
volumes:
- .:/app
ports:
- 8000:8000
ngx-mruby:
image: saboyutaka/ngx-mruby
ports:
- 80:80
volumes:
- ./public:/var/www/public
- ./config/ngx_mruby/nginx.conf:/usr/local/nginx/conf/nginx.conf
- ./ngx_mruby/hook:/usr/local/nginx/hook
https://hub.docker.com/r/saboyutaka/ngx-mruby/
@matsumotoryさんもDockerfileを公開しているけど、latestが ngx_mruby 1.19.5 だった(気がする)ので2.1.1で作り直しました。
Ruby app.rb の処理を変更
def fetch ... + # sleep をコメントアウトする + # sleep 1 - sleep 1 ... end
nginx の設定
# nginx.confの抜粋
http {
...
upstream app {
server web:8000;
}
server {
...
location /fetch {
mruby_rewrite_handler_code '
Nginx::Async.sleep 1000
Nginx.return Nginx::DECLINED
';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app;
}
}
mruby_rewrite_handler_code directive 内でngx_mrubyを実行しています。ここでNginx::Async.sleep 1000を記述することで、nginxでリクエストをnon-blockingで止めて、時間経過後に処理を再開します。ちょっとハマった点は、rewriteを複数回行う場合、今回は proxy_pass も使っているので、明示的にNginx.return Nginx::DECLINED を書く必要があってそれに気づくのに時間がかかりました。nginxもうちょっと勉強して理解したい.. matsumotoryさん本人からリプライ頂いてありがたい🙏
このケースでは次のrewrite処理のフェーズに明示的に引き継がないといけないのでDECLINEDが必要ですね。ある程度ngx_mrubyの中でそのあたりは自動化しているのですが、このケースはrewriteを2つ使っているので、DECLINEDが無いと同じrewriteフェーズ内の次のrewriteに遷移できない感じになります。
— 松本 亮介 / まつもとりー (@matsumotory) 2018年8月12日
実行結果
Before

realtime: 1.157 apptime: 1.150
After

realtime: 1.150 apptime: 0.140
apptime(Sinatraの処理時間)が1.150から0.140に変わりました!Rubyの処理が約1秒減ったのがわかります。これで/fetchへのリクエストが大量にきてもRubyの代わりにNginxが処理を止めるので、Rubyでリクエストが詰まることがなくなりました。
おわり
今年も去年と同じメンバー(@tompng, @_simanman, @saboyutaka)で挑戦しようという話になっていてRubyと今年はngx_mrubyも持っていけるようにしておきたいなと思ってやってみました。sleep 1 が2年連続は出ないだろーと思いつつも対策は万全で行きたいと思いますー💪