Skip to content

伸びなかった実験も、次の一手にする

応用編では、きれいに右肩上がりにはなりませんでした。むしろ、伸びなかった変更から「いま何が上限ではないか」を知る場面が増えます。

応用編のscore履歴

主な節目を、測定条件が同じものと違うものに分けて並べます。

1台・同居ベンチ

段階スコア判断
応用編の再基準21,088DB改善後の出発点
MariaDB・Go・OS設定23,358小幅な伸び。構造変更へ進む
conditionをmemoryへ43,682大きく伸び、再測定も4万点台
過大なmemory確保34,599悪化。1ISU×最大台数で考え直す
capacity・GC調整47,190memory量とGC負荷を修正
FD枯渇修正48,711too many open filesを解消
JSON fast path55,572parserとGCの仕事を減らす
1台の最高57,208pass / deduction 0

3台・測定場所も記録

段階スコア測定条件
user同期漏れ03台、prepareで401
proxyで2分割70,064coordinatorにベンチ同居
condition入口も分割69,1363台同時pprofあり
AWS内別node・reboot後74,550worker Aにベンチ同居
外部WSL98,004家庭回線→3台の公開IP
gzip後の外部WSL83,278上と同じ経路だが回線の揺れあり

98,004だけを大きく太字にしたくなります。でも、1台ベンチはサーバーのCPUをベンチ自身と分け合い、外部ベンチは家庭回線を通ります。競技環境での順位を予言する数字ではありません。

さらに6時間: 外部の同じ経路で比べる

ここからは、WSLのベンチからAWS 3台へ接続する同じ系列です。外部JIAはmainへSSH tunnelでつなぎます。得点の揺れは大きいものの、変更前後を同じ経路で比べられます。

段階スコアdeduction判断
raw POST server77,1090最多のcondition入口だけを薄くした
lock・手書きJSON80,6710AWS内系列。小さな改善の上限が見えた
登録競合あり788,25352高得点でも不採用。新規ISUの最初のPOSTを落とした
登録待ちを修正249,0580正しさを回復。ここを外部系列の基準にする
JIA tunnelを8本へ232,4070scoreは揺れた。登録経路の詰まりを分離
最新conditionをpush575,3120trend/listごとの内部往復を廃止
activate前の仮登録609,84673JWTが期限切れになる滞留。まだ不採用
3台同時pprof1,394,9020timeout 43。Go CPUは30秒中2〜4秒
再現run2,868,7790GraphGood 13,578
worker側も仮登録を隠す2,738,9990二段階公開を厳密にしても性能を維持
最終reboot直後00serviceはactiveだがinitializeが外部でtimeout
同じreboot後に再試行3,072,8540GraphGood 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転送量は次のように減りました。

runasset取得転送量スコア
gzip前2,833回356.57MiB98,004
gzip後2,665回94.19MiB83,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

外部ログには、次のエラーがありました。

text
http2: server sent GOAWAY and closed the connection;
LastStreamID=1999, ErrCode=NO_ERROR
http2: server sent GOAWAY and closed the connection;
LastStreamID=1999, ErrCode=NO_ERROR

GOAWAYは、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の品質を頼む

text
残り時間は、改善案の数ではなく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の改善は、正解を一発で当てる競技ではありません。外れた仮説を早く捨て、残った可能性へ時間を使う競技でもあります。