The paradigm shift

saboyutaka’s diary なんかかく(ブログn回目)

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側では処理を待つ必要がなくって捌けるリクエストを増やす算段です。

Async.sleepを実装した ngx_mruby v2 がリリースされる

rubykaigi.org

hb.matsumoto-r.jp

今年のRubyKaigi2018での @matsumotory さんの発表で、nginxでのmruby実装である ngx_mruby module のv2をリリースする話をしていて、その中でnon-blockingなsleepが出来る Nginx::Async.sleep が出来るという話でした。今回はこれを次のISUCONまでには使えるようにして sleep 1 に対策出来るようにしておこうと思って実装してみました。

実装

github.com

@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 の処理を変更

https://github.com/saboyutaka/isucon7q/blob/0e6cc831d5cf1011d117598ab38536b377c53b34/ruby/app.rb#L154

def fetch
  ...

+  # sleep をコメントアウトする
+  # sleep 1
-  sleep 1

  ...
end

nginx の設定

https://github.com/saboyutaka/isucon7q/blob/0e6cc831d5cf1011d117598ab38536b377c53b34/config/ngx_mruby/nginx.conf

# 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さん本人からリプライ頂いてありがたい🙏

実行結果

Before

f:id:saboyutaka:20180813215213p:plain

realtime: 1.157 apptime: 1.150

After

f:id:saboyutaka:20180813215021p:plain

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年連続は出ないだろーと思いつつも対策は万全で行きたいと思いますー💪