伸びなかった実験も、次の一手にする
応用編では、きれいに右肩上がりにはなりませんでした。むしろ、伸びなかった変更から「いま何が上限ではないか」を知る場面が増えます。
応用編のscore履歴
主な節目を、測定条件が同じものと違うものに分けて並べます。
1台・同居ベンチ
| 段階 | スコア | 判断 |
|---|---|---|
| 応用編の再基準 | 21,088 | DB改善後の出発点 |
| MariaDB・Go・OS設定 | 23,358 | 小幅な伸び。構造変更へ進む |
| conditionをmemoryへ | 43,682 | 大きく伸び、再測定も4万点台 |
| 過大なmemory確保 | 34,599 | 悪化。1ISU×最大台数で考え直す |
| capacity・GC調整 | 47,190 | memory量とGC負荷を修正 |
| FD枯渇修正 | 48,711 | too many open filesを解消 |
| JSON fast path | 55,572 | parserとGCの仕事を減らす |
| 1台の最高 | 57,208 | pass / deduction 0 |
3台・測定場所も記録
| 段階 | スコア | 測定条件 |
|---|---|---|
| user同期漏れ | 0 | 3台、prepareで401 |
| proxyで2分割 | 70,064 | coordinatorにベンチ同居 |
| condition入口も分割 | 69,136 | 3台同時pprofあり |
| AWS内別node・reboot後 | 74,550 | worker Aにベンチ同居 |
| 外部WSL | 98,004 | 家庭回線→3台の公開IP |
| gzip後の外部WSL | 83,278 | 上と同じ経路だが回線の揺れあり |
98,004だけを大きく太字にしたくなります。でも、1台ベンチはサーバーのCPUをベンチ自身と分け合い、外部ベンチは家庭回線を通ります。競技環境での順位を予言する数字ではありません。
さらに6時間: 外部の同じ経路で比べる
ここからは、WSLのベンチからAWS 3台へ接続する同じ系列です。外部JIAはmainへSSH tunnelでつなぎます。得点の揺れは大きいものの、変更前後を同じ経路で比べられます。
| 段階 | スコア | deduction | 判断 |
|---|---|---|---|
| raw POST server | 77,109 | 0 | 最多のcondition入口だけを薄くした |
| lock・手書きJSON | 80,671 | 0 | AWS内系列。小さな改善の上限が見えた |
| 登録競合あり | 788,253 | 52 | 高得点でも不採用。新規ISUの最初のPOSTを落とした |
| 登録待ちを修正 | 249,058 | 0 | 正しさを回復。ここを外部系列の基準にする |
| JIA tunnelを8本へ | 232,407 | 0 | scoreは揺れた。登録経路の詰まりを分離 |
| 最新conditionをpush | 575,312 | 0 | trend/listごとの内部往復を廃止 |
| activate前の仮登録 | 609,846 | 73 | JWTが期限切れになる滞留。まだ不採用 |
| 3台同時pprof | 1,394,902 | 0 | timeout 43。Go CPUは30秒中2〜4秒 |
| 再現run | 2,868,779 | 0 | GraphGood 13,578 |
| worker側も仮登録を隠す | 2,738,999 | 0 | 二段階公開を厳密にしても性能を維持 |
| 最終reboot直後 | 0 | 0 | serviceはactiveだがinitializeが外部でtimeout |
| 同じreboot後に再試行 | 3,072,854 | 0 | GraphGood 13,039。最終版の最高値 |
目標に置いた30位は92,604、1位は1,464,232でした。手元の最高はそれぞれ約33.2倍、約2.10倍です。最終コードを3台ともrebootした後の値です。
ただし「当時の1位に勝った」とは書きません。当時とはinstance、ベンチ実行場所、network、実行時刻が違います。ここで言えるのは、今回の同じ外部系列で1位スコア水準を越え、再起動後にもdeduction 0で再現したことです。
60万点でも不採用にした理由
仮登録を入れた最初のrunは609,846点でした。ところが終盤、POST /api/authに403が73件出ました。
公式ベンチのコードを読むと、JWTは作成から30秒で期限切れになります。さらにログには、mainから応答がないまま発生したtimeoutも出ていました。認証の仕様を緩めて期限切れJWTを通せば、見かけの点は守れます。でもそれはボトルネックを隠す変更です。
このrunは「mainの前で30秒以上待つrequestがある」という計測結果として使い、採用スコアにはしませんでした。次のrunで3台同時pprofを取り、認証handlerそのものではなく、測定系を含む長い外れ値だと切り分けました。
286万点を、もう一度疑う
最高runではGraphGoodが13,578回まで増えました。あまりに伸びたため、Agentへ「古いtrendを返し続け、ベンチの利用者増加を避けていないか」と疑わせました。
ベンチ中40秒、mainのtrend responseを1秒ごとにhashへ変換しました。hashは中身から作る短い指紋です。40個の指紋は連続して変化し、workerからmainへの最新状態pushとtrend更新が動いていました。
速くなった結果ほど、正しさを疑う。これはAgentの勢いを止める作業ではなく、安心して次のアクセルを踏む作業です。
reboot後の0点も残す
最終コードを3台ともrebootし、nginx、MySQL、Goがactiveへ戻った直後に外部ベンチを始めました。このrunはPOST /initializeが20秒でtimeoutし、0点でした。Goのinitialize handlerが出すログへ到達する前に止まっており、外部networkの準備が間に合わなかったと判断しました。
同じreboot後、コードや設定を変えずに再試行すると3,072,854 / deduction 0でpassしました。だから最初の0点を消してよい、とは考えません。
- systemdの
activeは、外部から採点可能という意味ではない - 再起動後は3つのFQDNへ実際に接続してからベンチを始める
- 競技の再起動試験には、起動待ち時間も含める
速いコードだけでなく、いつ「準備完了」と言えるかも提出手順の一部です。
gzipは効いたのか
JavaScript、CSS、SVGを事前にgzipすると、asset転送量は次のように減りました。
| run | asset取得 | 転送量 | スコア |
|---|---|---|---|
| gzip前 | 2,833回 | 356.57MiB | 98,004 |
| gzip後 | 2,665回 | 94.19MiB | 83,278 |
転送量は約74%減ったのに、スコアは下がりました。
ここで「gzipは遅いから戻す」と結論づけるのは早すぎます。gzip後のAPIはp99 1〜2msで、WSLのCPUも94%以上idleでした。一方、condition POSTは平均34.87msなのにp99 2ms。1%より外側だけが長く止まり、家庭回線の揺れが得点へ強く影響しています。
gzipは転送量を減らした事実を根拠に採用します。ただし「スコアを14,726点下げた」とも「必ず速くした」とも言いません。
access logを止めても伸びなかった
3台proxy構成のaccess logは約47MBになりました。最終runではログ自身のI/Oを減らすためaccess_log offにしました。
結果は70,064点から61,565点。期待とは逆です。
ログ停止がアプリを遅くする理由は考えにくいものの、この1回は「ログ停止で伸びた」という証拠にはなりません。設定は本番候補として残し、効果量は未確定と書きます。
計測には費用がありますが、計測を止めるタイミングも計測して決める。ちょっとややこしいけれど、これが応用編らしいところです。
HTTP/2のGOAWAY
外部ログには、次のエラーがありました。
http2: server sent GOAWAY and closed the connection;
LastStreamID=1999, ErrCode=NO_ERRORhttp2: server sent GOAWAY and closed the connection;
LastStreamID=1999, ErrCode=NO_ERRORGOAWAYは、HTTP/2 serverが「この接続では新しいrequestを受けない」と伝える正常な終了通知です。LastStreamID=1999は、約1,000 requestでNginxが接続を閉じた手がかりでした。
大量POSTでは、正常なつなぎ直しもベンチ側のrequest errorとして観測されます。NginxのHTTP/2 request上限を引き上げるとGOAWAYは消えました。再測定70,056点は分散内で、得点向上は断定しません。ここでも、エラーを消せたことと、scoreが伸びたことを分けて記録します。
失格runを最高記録にしない
3つの公開IPへ直接分散した外部runは、途中で約8.8万点まで伸びました。しかし、その後WSL側からnetwork is unreachableとなり、最終結果はError count over 100、0点です。
途中の数字だけ切り取れば強そうに見えます。でも競技のscoreは最終判定です。このrunは次の仮説を与えてくれましたが、成功扱いにはしません。
- 入口を3つへ分ける構成は、高い負荷を立ち上げられる可能性がある
- 家庭回線は外部ベンチの再現環境として不安定
- 同じAWS region内の独立したベンチ環境が欲しい
- 一部の404も、安定した回線で再検証が必要
Agentへloopの品質を頼む
残り時間は、改善案の数ではなくloopの品質を優先してください。
各loopで次を1行ずつ残してください。
1. 観測: どのログの、どの数字が最大だったか
2. 仮説: 何を減らせば、どの数字が動くはずか
3. 変更: 仮説と直接つながる差分
4. 検証: score、pass、deduction、timeout、status、profileの変化
5. 判断: 採用・保留・revertと、その理由
- scoreが伸びても、測定場所やinstance数が変わったら別系列にする
- 数字が動かなければ「効かなかった」ことを次の絞り込みに使う
- 外部要因が支配したrunは、サーバー性能の比較に使わない
- 途中scoreが高くても最終failなら成功扱いしない
- 失敗runのログも、次の仮説に必要な部分だけ保存する残り時間は、改善案の数ではなくloopの品質を優先してください。
各loopで次を1行ずつ残してください。
1. 観測: どのログの、どの数字が最大だったか
2. 仮説: 何を減らせば、どの数字が動くはずか
3. 変更: 仮説と直接つながる差分
4. 検証: score、pass、deduction、timeout、status、profileの変化
5. 判断: 採用・保留・revertと、その理由
- scoreが伸びても、測定場所やinstance数が変わったら別系列にする
- 数字が動かなければ「効かなかった」ことを次の絞り込みに使う
- 外部要因が支配したrunは、サーバー性能の比較に使わない
- 途中scoreが高くても最終failなら成功扱いしない
- 失敗runのログも、次の仮説に必要な部分だけ保存するISUCONの改善は、正解を一発で当てる競技ではありません。外れた仮説を早く捨て、残った可能性へ時間を使う競技でもあります。