mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

isucon2に参加してきました。

こんにちは、ゾンビ映画が大好きだけど怖くて一時停止しながらじゃないと見れない森本@たんぽぽグループです。 isucon2に参加してきました。

事前調査と方針決定

公開されていたisuconのソースと参加チームのblogを読み、どういう方針で行くかを相談しました。 正攻法だとある程度の改善はできるけども優勝は狙えないだろう。 ということでチートを目的とすることにしました。

チート方法

偽情報

twitterに「AKB48が渋谷ゲリラライブやってるよ! #isucon2」などと流す。
考えただけで実際には実行はしませんでした。

2位狙い

reverse proxyの接続先を優勝候補チームのreverse proxyに向ける。
※今回はIPアドレスの下2桁がチーム番号だったので推測可能でした。

upstream fujiwara {
  server xxx.xxx.xxx.xxx
}
server {
  location / {
    proxy_pass fujiwara;
  }
}

設定しちゃえば何もしなくてもガンガンスコアが伸びていきます!
接続先の負荷が上がってしまい迷惑をかけちゃうので実際には実行しませんでした。

POSTの後は無敵

POSTを行ってくるクライアントが1つだけの場合に有効な方法です。
POSTリクエストのあと1秒後に、POST結果が正しいかどうかを判定するリクエストが来ます。
POSTの時刻を記録しておき、その後1秒間は偽のレスポンスを返します。

これはよく考えてみると、通常のフロントエンドでのキャッシュの劣化版でした orz

80番ポート以外で

なんとかhttp_loadのsignatureを検出できないか調べていたところ、Hostヘッダに違いがありました。

http_loadのHostヘッダ生成部分

bytes += snprintf(
  &buf[bytes], sizeof(buf) - bytes, "Host: %s\r\n",
  urls[url_num].hostname );

node.jsのHostヘッダ生成部分

if (host && !this.getHeader('host') && setHost) {
  var hostHeader = host;
  if (port && +port !== defaultPort) {
    hostHeader += ':' + port;
  }
  this.setHeader('Host', hostHeader);
}

http_loadはポート番号が何であっても 'Host: ' なんですが、node.jsの場合は defaultPortじゃない場合は 'Host: :'と送ってきます。
http_loadと判定できた場合は、 ngx_http_empty_text_module.c で0byteのtextを返します。

この方法を実行するには毎回302を返す必要がありますが、isucon2の最初の説明で「200以外のレスポンスはエラーとみなします」と釘をさされました...
「宗教上の理由で80番ポートは使えないので、うちだけ8000番でお願いします!」と言ってみようかと思ったのですが、今回のスコア算定方法が単純なリクエスト数ではなくて処理したPOSTの数に大きく依存するので断念しました。

処理時間の違い

accept()からリクエストが到着するまでの時間を計測して、その差によってhttp_loadを見分けます。
各種条件によって時間にはばらつきが出るので100%の判定は無理ですが、ある程度の判別が可能です。
http_loadの最短時間 < node.jsの最短時間 なので、一定のしきい値(例えば5μs)以下のリクエストを http_load だと判定して、0byteのtextを返します。

少し試してみたのですが、今回のスコア算出方法では測定誤差以下の結果しか出せませんでした。

全部捕まえる

http_loadを100%の精度で検出する方法です。
これは前日に思いついたので実際の検証はしていません。
当日の電車の中でコードを書いていました。

まずは無限ループでいいので accept() しまくりベンチマークの並列数を調べます。
この時はベンチマークの結果は失敗してしまいます。

並列数が分かったので本番です。

  1. 非http_load捕まえツールを起動します。
  2. nginxは、すべてのリクストを非http_load捕まえツールに振り分けます
  3. 非http_load をすべて捕まえたら、nginxがすべてのリクエストに空テキストを返すように切り替えます
  4. http_loadの接続を開放します。
  5. http_loadがすごい勢いでアクセスしてくれるはず!
  6. nginx の/procを監視して、http_loadの終了を待ちます
  7. nginxが通常のbackendに向くように切り替えます

nginxの切り替えは、if(-f file) とファイルの存在で切り替えようと思っていましたが未検証です。
多分できるはず...

非http_load捕まえツールの中身

accept() しまくり、クライアントを全部捕まえる

    foreach (各クライアントに対して){
      リクエストを受け取る
      バックエンドにリクエストを渡して処理してもらい、レスポンスを受け取る
      クライアントにレスポンスを返す
      時間計測開始
      accept()実行
      時間計測終了
      if(1ms 以下) http_load と判断
    }

nginx に empty_text への切り替えを指示。

foreach (http_load){
  レスポンスを返す
}

while(nginxにhttp_loadがガンガンアクセスしてる){
  foreach (非http_load){
    接続が切れないようにレスポンスを 1byte ずつ返す。
  }
  sleep 1;
}

nginx に通常のbackendへの切り替えを指示。

foreach (非http_load){
  残りのレスポンスを全部返す
}

を実行します。

isucon2のスコア判定だと、この方法は1チケットの購入に3分かかったとみなされるので、とてつもなく悪いスコアが算出されるので実行しませんでした。
いまにして思えばネタとやってみてもよかったかなとちょっと後悔しています。

反省点

考えてきたチートが有効じゃなかった時点で、正攻法に切り替えてしまいました。
正攻法の調査とかロクにしてなかったのに...
変に色気を出さずに、最後までチートでつっぱしってネタを作るべきでした。

まとめ

上記のチートが成功していたとしても、優勝争いにからめたかどうかは怪しいぐらいにすごいスコアが叩き出されてました。 完敗です。
この様な楽しいイベントを開催してくださったNHN Japanの皆様に感謝します。
ありがとうございました!