rch850 の上澄み

技術的な話題とか、雑談とか。タイトルを上澄みに変えました @ 2020/09/02

ISUCON 10 予選を nodejs 実装で突破しました(へしこず)

「へしこず」の rch850 です。ISUCON 10 の予選を nodejs 実装で通過しました。奇跡的な予選通過の ISUCON 5 以来、2度目の本選進出です。やったぜ!チームメイトは machosita と emittam です。いつもありがとうございます。

個人的には nodejs で予選を突破できたのがうれしいです。 nodejs への思いは最後に。

macoshita の予選レポートが実況風味なので、こちらは箇条書きにします。実況風味はまた時間があるときに。

なおリポジトリはこちらです。

github.com

スコア推移はこう。

やったこと

サーバ構成

シンプルにこうしました。DB 分割したチームが結構いたようですが、そこまで考えてませんでした。しなくても勝てたし、して勝てたかは分かりません。

  • .101: ベンチマーカーのアクセス先。nginx, API サーバ, redis
  • .102: MySQL 専任
  • .103: API サーバ

効果が大きかったかもしれないもの

  • bot 対策 (nginx側, nodejs 側)
  • /api/estate/low_price と /api/chair/low_price をキャッシュ (commit)
    • どのベンチマーカーも最初にアクセスしてくる。そして更新頻度がとても低い
    • 物件は入稿時のみ、椅子は入稿時と在庫が切れた時にキャッシュを破棄
    • あとから Redis に乗せた (commit)
  • nazotte をワンクエリにして spatial index 追加 (commit)
    • 当初は latlon をレスポンスに含めていたけど、なんかベンチマークが不安定な感じがしたので念の為削除した (commit)

効果がそこまで大きくなかったかもしれないもの

  • /api/estate/req_doc/:id で id 29500 以下なら即 OK、それ以外は Redis に id をキャッシュしてその後のアクセスはキャッシュを見て OK (commit)
  • /estats/:id のキャッシュ (commit)
  • nazotte に limit 指定が無かったので追加 (commit)
    • これに気づいたのが残り10分切ってから。今からこれやる!?って10秒ほど相談して投入した。これが最後のコミット
  • rent, (stock, price) のインデックス追加 (commit)
  • (door_width, door_height), door_height, popularity のインデックス追加 (commit)
  • (door_width, door_height, rent), (kind, stock, price) のインデックス追加 (commit)
  • search 系のカウントと検索を Promise.all で並列化 (commit)
  • 不必要な SELECT * の改善
    • req_doc(資料請求)のクエリを念の為 SELECT 1 に (commit)
    • estate/search のクエリを念の為 SELECT id に (commit)

やろうとしたけどできなかったこと

自分がやってたとこ。悔しいので晒します。

  • recommended の OR を減らす
    • 短い2辺 (len1, len2 とする) が通ればいいから (door_width >= len1 AND door_height>= len2) OR (door_width >= len2 AND door_height>= len1) だけでいいよね?
    • const [len1, len2] = [chair.width, chair.height, chair.depth].sort() すればいいんだな。はいコミット (commit)
    • あれ?検証エラー?
    • ソート順間違えたかーー???えいやー const [_, len1, len2] = [chair.width, chair.height, chair.depth].sort() (パニック状態) (commit)
    • また検証エラー??????完全に正解してるでしょ????
    • MDN 見たら sort は「デフォルトではUnicodeコードポイントの昇順にソート」って書いてある!parseInt したら通るやろ! (commit)
      • [1, 10, 4].sort()[1, 4, 10] になると思い込んでたけど、実際は [1, 10, 4]
      • [1, 10, 4].sort((a, b) => a - b) なら [1, 4, 10] になる
    • 通らない……もう限界。revert します
    • ラスト数分でこうすればいいことに気づくが、もうコードを変える時間は残っておらず終了

      // ここまでのコード
      const [_, len1, len2] = [parseInt(chair.width), parseInt(chair.height), parseInt(chair.depth)].sort((a, b) => a - b);
      // 多分これが正解
      const [len1, len2] = [parseInt(chair.width), parseInt(chair.height), parseInt(chair.depth)].sort((a, b) => a - b);
      

どういう方向性で動いたか

今回はこれまでの ISUCON の動き方からちょっと変えてみました。

  • 落ち着いて取り組むようにした(というのは方針で抽象的なので、以降具体的な話)
    • 自信がある nodejs を選んだことで落ち着けたというのはあると思う
  • アクセスログからベンチマーカーの動きを見て、どうやって得点を稼げるか考えた(詳細は次のセクションに)
    • これは ISUCON 9 予選の反省からです
    • UA に UUID が無かったらやれてなかったかもしれない
    • これまでは slowlog (mysqldumpslow や pt-query-digest) や kataribe だけを見て、ボトルネックを解消していました。ボトルネックが得点に結びつくとは限らない、と考えて、今回は得点に関係しそうなボトルネックに注力しました
  • ウェブサービスを自分で触って挙動を理解するようにした
    • これも上と関係した話ですね
  • Redis を使うようにしたのが19時過ぎ(残り2時間弱)だったり、API サーバを増やしたのが20時過ぎ(残り1時間切ってる)だったりと、普段は早めにやっていた作業を、かなり後回しにした
    • 「早すぎる最適化」を懸念したり、ぬか喜びしたくないなという考えから、意図的に後回しにしました。結果にどう影響したかは分からないけど、たぶん良かったんでしょう

ベンチマーカーの行動パータン

nginx のアクセスログから調べました。User-Agent に UUID らしきものがついていたので、それで絞り込んでパターンを分類しました。

3パターンの行動が確認できました。他にもあったかもしれませんが、ひとまずこの3つで。末尾の数字はその時点での req_time です。これを見て、あるパターンのユーザーが得点に至るまでの合計時間を意識するようにしました。low_price は全パターンに効いてくるとか、search は回数が多いから大事とか。

ユーザーパターン 1(条件検索して資料請求)

"GET /api/estate/low_priced HTTP/1.1" 0.003
"GET /api/chair/low_priced HTTP/1.1" 0.071
"GET /api/estate/search/condition HTTP/1.1" 0.001
"GET /api/estate/search?page=0&perPage=25&rentRangeId=2 HTTP/1.1" 0.079
"GET /api/estate/search?page=3&perPage=25&rentRangeId=2 HTTP/1.1" 0.114
"GET /api/estate/search?page=3&perPage=25&rentRangeId=2 HTTP/1.1" 0.118
"GET /api/estate/search?doorWidthRangeId=1&page=0&perPage=25 HTTP/1.1" 0.168
"GET /api/estate/search?doorWidthRangeId=1&page=4&perPage=25 HTTP/1.1" 0.185
"GET /api/estate/search?doorWidthRangeId=1&page=3&perPage=25 HTTP/1.1" 0.180
"GET /api/estate/27131 HTTP/1.1" 0.002
"GET /api/estate/24611 HTTP/1.1" 0.001
"POST /api/estate/req_doc/24611 HTTP/1.1" 0.002

ユーザーパターン 2(なぞって資料請求)

"GET /api/estate/low_priced HTTP/1.1" 0.010
"GET /api/chair/low_priced HTTP/1.1" 0.176
"POST /api/estate/nazotte HTTP/1.1" 2.000
"GET /api/estate/low_priced HTTP/1.1" 0.016
"GET /api/chair/low_priced HTTP/1.1" 0.095
"POST /api/estate/nazotte HTTP/1.1" 0.223
"GET /api/estate/16302 HTTP/1.1" 0.005
"POST /api/estate/req_doc/16302 HTTP/1.1" 0.002

ユーザーパターン 3 (条件検索して購入)

"GET /api/estate/low_priced HTTP/1.1"
"GET /api/chair/low_priced HTTP/1.1"
"GET /api/chair/search/condition HTTP/1.1"
"GET /api/chair/search?depthRangeId=1&page=0&perPage=25 HTTP/1.1"
"GET /api/chair/search?depthRangeId=1&page=2&perPage=25 HTTP/1.1"
"GET /api/chair/search?depthRangeId=1&page=0&perPage=25 HTTP/1.1"
"GET /api/chair/search?color=%E3%83%8D%E3%82%A4%E3%83%93%E3%83%BC&page=0&perPage=25 HTTP/1.1"
"GET /api/chair/search?color=%E3%83%8D%E3%82%A4%E3%83%93%E3%83%BC&page=3&perPage=25 HTTP/1.1"
"GET /api/chair/search?color=%E3%83%8D%E3%82%A4%E3%83%93%E3%83%BC&page=2&perPage=25 HTTP/1.1"
"GET /api/chair/2032 HTTP/1.1"
"GET /api/recommended_estate/2032 HTTP/1.1"
"GET /api/chair/19330 HTTP/1.1"
"GET /api/recommended_estate/19330 HTTP/1.1"
"POST /api/chair/buy/19330 HTTP/1.1"

また、これらのパターンに加えて、資料請求とイス購入のどちらの得点が多いかも調べました。序盤は 5:1 で資料請求が多め。後半はさらに顕著になって 10:1 ぐらいになっていました。これをもとに、資料請求大事という方向性が見えました。

なぜ nodejs 実装にしたのか?

これは予選突破した今だから言えることだし、とてもとてもおこがましい話なのですが、「Go じゃないと ISUCON は戦えない」というハードルができてしまうのが嫌でした。これは自分たちが ISUCON 9 を迎える時に感じていたハードルでした。結果、Go を選んでも勝てなかった(そもそも書けないんだけど)。Go でもダメなら諦めないとダメかな。そう思った時に、慣れた nodejs 実装のことを考えました。

そこで、これまでの ISUCON を振り返りました。

初めて ISUCON に参加した ISUCON 5 から一昨年の ISUCON 8 まで、ずっと Ruby 実装で挑戦していました。3人とも Ruby ばっかり書いているというタイプではなかったのですが、それなりに書けてました。

ISUCON 5 では奇跡的に予選突破できたのですが、そのあと Ruby で書き続けても、予選突破できない年が続きました。ISUCON 9 まで来て、さすがに Golang にしないと勝てないかな、と思って Golang に切り替えたのですが、まぁ書けない書けない。それなりに予習したつもりでしたが、それでも競技時間中に「これってどうやって書くんだっけ」と調べることが多かったのが反省です。

そして ISUCON 10。正直、一番書けるのは nodejs 実装だというのは確信してました。でもそれで勝てるのか?というのが不安でした。実際、nodejs で予選突破したチームは非常に少ないです。

これで大丈夫なんだろうか……

そんな心配をしながら、昨年予選の nodejs 実装を開いてみると、なんと TypeScript 実装。これは捗る!しかし手元でベンチマークしてみると Go や Ruby の半分程度のスコア。これは厳しいか……と思ったのですが cluster で fork したらあっけなく他言語に並びました。これは行ける!!!!

そう考えて今年の ISUCON 10 に挑み、なんとか予選突破できました。

これで「nodejs でも予選突破できるんやで」って胸を張って言える……そう思っていました。

Nodejs  2組   6.5%

ISUCON10 オンライン予選の利用言語比率 : ISUCON公式Blog

他にもいたんだね。どこのチームだったのかな?

fccpc.hateblo.jp

ponyopoppoが普段から使っているNode.jsです。

( ゚д゚)……………

「nodejs で予選1位通過しました」に勝てるわけないやーん。

ま、それを証明するのは自分じゃなくてもいいよね。nodejs でも ISUCON 戦えるぞ!立ち上がれ!nodejs の民よ!来年の ISUCON で待ってるぞ!