ISUCON4 予選の参戦記録と延長戦

まさに今 ISUCON 本戦が開催中で今更感満載ですが、まだ書けていなかった予選の感想を記しておきます。

参加申し込みの期限前日にカープエンジニアの @chiastolite さんに声をかけて参加しました。チーム名は赤道直下です。

tl;dr;

やったこと

  • 静的ファイル(JS, CSS, png)を Nginx で返す
  • 全て Redis に乗せて MySQL を使わなくする(時間切れ)

思いついたけどやらなかったこと

  • ドキュメントルート(/)を静的ファイルにして返す(挑戦するも失敗)
  • DB 書き込みを非同期にして /report で sleep 20 とかする

思いつかなかったこと

  • erb を使わない
  • ユーザー情報をソース内にベタ書き

お一人様について

来たれ!おひとり様! ISUCON4 参加メンバー募集エリアを用意しました : ISUCON公式Blog

まずは予選当日の前に、こちらのメンバー募集企画について。

Twitter では「ISUCON 一緒に出る人いないかなー」みたいなつぶやきがぼちぼち観測されたので、運営側がそんな人たちのために企画したんだろうなと思われます。ただ、Github での募集は残念ながら機能してなかった……。

ハッカソンでの即席チームとかはうまく行ってる気がするので、おひとり様向け夏期講習みたいなイベント兼スカウト会みたいなのしたら面白いかもしれないですね。ただ、今回もかなり応募数多かったので、来年公式でやる必要も動機もないと思いますが。

当日まで

事前予想

特にメンバー間で意識合わせをしてなかったけど、前回の予選や ISUCON という競技の性質から、こんな感じ問題になるのではと想定はしていました。

  • いかにレスポンスを事前にキャッシュするかの Varnish 合戦
  • DB (おそらく MySQL)にアクセスしたら負け

予習

とりあえず最初の1時間は計測するしかないと思っていたので、アクセスログMySQL 周りのいつものやつをまとめておきました。

言語は Ruby で行くことに決まってたので*1、プロファイリング用に rack-lineprof を使ってみたんですが、これがとてもわかりやすくて本番でも重宝しました。 ただ、line-prof 便利すぎるんですが、無効化するの忘れてベンチ走らせて 500 エラー連発して混乱するってことを繰り返してました。

ISUCON4 予選前の予習結果

当日

予選の出題

予選の内容と、問題に使用された AMI はこちらで公開されてます。 ISUCON4 予選問題の解説と講評 & AMIの公開 : ISUCON公式Blog

最初の1時間

@chiastolite さんにインスタンスを立ててもらって、まず最初にアプリを触って感じたのは、画面数少なすぎる(2枚)上に /mypage は動的に変わりそうなのでキャッシュは難しいということです。ドキュメントルートくらいしかキャッシュ化できなさそうでした。。

アクセスログのチェック

なにわともあれアクセスログのチェックをしましたが、ドキュメントルートへのアクセスが結構あるくらいしかわからなかったのであまり深く見てません。

MySQL 周り

次は3306 を開けてもらって、Sequel Pro で MySQL につなぎます。

スロークエリログ出す

set global slow_query_log_file='mysql-slow.log';
set global long_query_time=0.1
set global slow_query_log = 1;

全然クエリが引っかからなくてマジかーってなってました。

アプリ・Nginx 周り

アプリと Nginx 周りのチューニングは @chiastolite さんお任せでした。

ルートを静的ファイルで返す

ドキュメントルートへのアクセスが結構なウエイトを占めてるのと、そこくらいしかキャッシュ化できそうになかったので、ルートを静的に返そうという提案をしました。

以下、やったこと

  1. ドキュメントルートのレスポンスをコピペして .html ファイルを作り、それを静的に返すようにする
  2. ログイン失敗時にアラートメッセージの表示で引っかかる (view の実装を見てなかった >< )
  3. エラー内容毎に .html ファイルを作成し、パスを変えてエラー毎にリダイレクト先を振り分けようとする
  4. URL チェックに引っかかる

URL変えちゃダメならクエリパラメータ付与してって手もあるけど、それも含めてベンツマークツールがチェックしてると思ったのであきらめました。*2

全Redis化への道

初期データインポート

MySQL の初期化ようファイルから変換するのが面倒だったので、一度 MySQL に入れてから1行ずつ処理をトレースして Redis に突っ込むという形にしました。

ただ、毎回1から作ってるので時間がかかり、終了間際とかに繰り返しベンチを取るときにストレスが溜まりました。早めに Redis 用のダンプファイルを作るなどしておくべきでした。

db.xquery('SELECT * FROM login_log').each do |log|
  if log['succeeded'] == 1
    redis.pipelined do
      redis.set "#user_id_{log['user_id']}_locked", 0
      redis.set "ip_#{log['ip']}_locked", 0
    end
  else
     redis.incr "user_id_#{log['user_id']}_locked"
     redis.incr "ip_#{log['ip']}_locked"
  end
end

ip_banned?, user_locked? を Redis に移す

その後、rack-lineprof でアプリの重い箇所を探したところ、予想通り MySQL のところで時間がかかってました。とりあえず重そうな参照部分から順々に Redis に移していきました。

Ban 系の参照部分を全部 Redis に移し、残りはレポート用に login_log を書き込んでるところとユーザ情報を取っている所だけとした状態でスコアはだいたい 8,800 程でした(workload=1)。

レポート用のデータをどうするか

次に login_log の DB 書き込みをやめて、Redis のみでレポートを作り上げるところに手を出したのですが、SQL の読み間違え等でレポートのチェックが通ることなく時間切れとなりました。

延長戦

その後の懇親会でチューニングの方向性は間違ってなかったという手応えを得たので、一人延長戦をしてみました。

login_log の書き込みを無くす

レポートのエラーを修正してもスコアは 9,029 と微増に止まりました。*3

workload を 4 にあげるとスコアは 16,018 まで上がるものの、 dial tcp 127.0.0.1:80: cannot assign requested address が頻発してレポートのチェックで落ちました。

ミドルのチューニング

あまり手をつけてなかったネットワーク周りのチューニングのためにググって見つかった設定をとりあえず適応。

sysctl

vi /etc/sysctl.conf
# add
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 2
net.ipv4.tcp_rmem = 16384 131072 262144
net.ipv4.tcp_wmem = 16384 131072 262144
net.ipv4.tcp_mem  = 2048000 4096000 4096000
net.ipv4.ip_local_port_range = 10240 65000
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.core.netdev_max_backlog = 30000
net.ipv4.tcp_no_metrics_save=1
net.core.somaxconn = 262144
net.ipv4.tcp_syncookies = 0
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_max_tw_buckets = 56384
fs.file-max = 70000

kernel.panic = 10

反映

sudo sysctl -p

limits.conf

ファイルディスクリプタ関連

vi /etc/security/limits.conf
isucon soft nofile 1000000
isucon hard nofile 1000000
# 一度ログアウトし、再度 ssh 接続してから確認
ulimit -n

Redis

Unix socket 経由で Redis を使うようにします。

vi /etc/redis.conf
unixsocket /tmp/redis.sock
unixsocketperm 777
sudo /etc/init.d/redis restart

score: 21,680 (workload=8)

mysql からの user 情報 SELECT をなくしてすべて Redis で処理

converter.rb に以下を追加し、

db.xquery('SELECT * FROM users').each do |user|
  redis.pipelined do
    redis.set "#{user['id']}_login", user['login']
    redis.set "#{user['login']}_id", user['id']
    redis.mapped_hmset "#{user['login']}_user", user
    redis.mapped_hmset "#{user['id']}_user", user
  end
end

アプリを書き換えました。

app.rb

        # user = db.xquery('SELECT * FROM users WHERE login = ?', login).first
        user = redis.hgetall "#{login}_user"
        # @current_user = db.xquery('SELECT * FROM users WHERE id = ?', session[:user_id].to_i).first
        @current_user = redis.hgetall "#{session[:user_id].to_s}_user"

score: 22,255 (workload=16)

'/' の静的ページ化

最後に、クエリパラメータ付与すればベンチマークのチェックをくぐり抜けることができるということを懇親会で確認したので、やってみました。

app.rb

        case err
        when :locked
          # flash[:notice] = "This account is locked."
          redirect '/?err=locked'
        when :banned
          # flash[:notice] = "You're banned."
          redirect '/?err=banned'
        else
          # flash[:notice] = "Wrong username or password"
          redirect '/?err=wrong'
        end
        redirect '/'

nginx.conf

    location / {
      if ( $arg_err = "locked" ) {
        root /home/isucon/webapp/public/locked;
      }
      if ( $arg_err = "banned" ) {
        root /home/isucon/webapp/public/banned;
      }
      if ( $arg_err = "wrong" ) {
        root /home/isucon/webapp/public/wrong;
      }
      root /home/isucon/webapp/public;
    }
    location /login {
      proxy_pass http://app;
    }
    location /report {
      proxy_pass http://app;
    }
    location /mypage {
      proxy_pass http://app;
    }

sudo /etc/init.d/nginx restart

score: 39,032 (workload=16)

まとめと反省点

結果を残すことができなくて残念ですが、来年も開催するとのことなので、来年こそは本戦に出場できるよう精進します。

以下、来年を見越した反省点

  • コードの管理をしよう
  • 作業者ごとに複数インスタンスを立てよう
  • 繰り返しベンチを取ることになるので、初期化処理はサクッと終わるようにしよう

*1:去年と違い、そこそこ Ruby も読めるようになった

*2:後で聞いた話では実はチェックしてなかった!!

*3:本番中にチェックが通らなかった原因は、初期化スクリプトのタイポでした><

mac でメニューバーのサードパーティアプリのアイコンを復活させる方法

メニューバーには最低限のアイコンのみ表示していたいので、普段は ClipMenu のステータスアイコンは非表示にしている。今回、間違ってホットキーの設定を解除した状態で環境設定画面を閉じてしまい、再び環境設定画面を表示できなくなり困っていた時の対処法を残しておく。

tl;dr;

plist を編集してメニューバーのステータスアイコンを復活させる

plist の編集

以下のファイルを編集する。

plist の編集はツールXcode に付属しているので、無ければインストールする。

open ~/Library/Preferences/com.naotaka.ClipMenu.plist

showStatusItem の値を 0 -> 1 に変更する。

アプリの再起動

Activity Monitor とかで一度アプリを落として再度立ち上げると、ステータスアイコンが復活しているはず。

まとめ

plist でステータスアイコンの表示・非表示を制御しているアプリの場合はこれでなんとかなる。

アプリごとに設定の名称は違うみたいなので、それっぽい設定の値を変えてみるといい。(ステータスアイコンが復活しなかった場合は、変更した値を元に戻しておいたほうが良さそう)

YAPC::Asia Tokyo 2014 に参加しました

そろそろ今年の YAPC を終わらせないといけないのでブログ書きます。*1

tl;dl

  • 今年も YAPC::Asia 有ってよかった!
  • キャパオーバー
  • 無限ビール
  • かき氷!かき氷!\\\ ٩( ˘ω˘ )و ////

当日ボランティアスタッフ

ボランティアスタッフでの参加(2年連続2回目)です。

今年は2日で締め切られるほど応募が殺到したので、気がついたら募集が終わっていて参加できなかった人もそこそこ居たみたいです。*2

去年と同じ会場で、担当した持ち場も去年と同じだったので今年は慣れた感じでした。941さんと牧さんが居ない*3のが寂しい感じでしたが、大きなトラブルも無く終えられたように思います。*4

会場のキャパ問題

去年はそれほどでもなかった気がしますが、今年は来場者が増えたこともあって多くのセッションで立ち見/座り見が出てしまいました。特に多目的教室2 は人気セッションも多く、かなり詰めていただいたにもかかわらず聴講をお断りすることもありました。*5

キャパ問題は、セッションを並行で持っている以上読みきれない部分もあって難しいところですね。サテライト視聴用のスペースを確保して、バッファを設けるのは一つの手じゃないかと思います。

無限ビール

会場になった協生館の 1F にはブリティッシュパブ「HUB」があり、去年はイベントが終わると必然的にそこで打ち上げが行われていたのですが、今年は何と HUB 貸し切りと 1,000杯フリードリンクという完璧な制度ができてました。*6

ビールサーバーの中身が冷えてなくて泡しか出なくて注げないなんてこともありましたが、ボトルワインに切り替えるという手で乗り切ったりもしました。

無限かき氷

DMM 様からのラムネ入りの美味しいかき氷は Twitter でもかなり話題にのぼりました。まだ数十万個の在庫を抱えているという噂も聞きますので、イベント主催者の方は寒くなる前に声をかけてみるのもいいのではないでしょうか。

まとめ

イベントから時間が開いてしまったわりに内容の薄い感想エントリになってしまいました。ただ、今年も十分に楽しかったので、来年も参加できるといいですね。

*1:ブログを書くまでが YAPC::Asia です

*2:まさかこれが原因ではないと思いたい https://twitter.com/941/status/473016217087647744

*3:牧さんは海外ゲストとのコミュニケーションの関係で実質スタッフ状態でしたが

*4:自分が知らないだけかもしれませんが

*5:防災上の都合で出入り口を立ち見で塞いでは行けないという事情もありました

*6:リクルートホールディングス様++ http://yapcasia.org/2014/08/yapcasia-party-and-hub.html