ISUCON9の予選を突破し、失格になったのちに通過しました

参考:チームメイトの記録です

本番前に行ったこと

競技環境の把握

  • Alibaba Cloudの仕様を把握する
    • インスタンスタイプやネットワーク帯域を料金プランから眺めることで、本番で使用されるものをなんとなく想像しておく
    • ということを行いたかったのだが、何故かアカウントの作成に数回失敗してしまって、戦意喪失した。アカウント作れなくても見るくらいならできたと思う
  • 予選で使用されるOSを確認する
  • 雑に apt で Go, Nginx, MySQL をインストールしたとき、どのバージョンでインストールされてしまうのかを調べる
    • MySQL5.7系がインストールされるので、本番では8系にアップグレードするか?などと妄想していた
    • 何の考えもなしにアップグレードすると詰みそう、という感想を持つ(ユーザの認証方式が異なるため)
    • 結局、自分の手を動かして様々な実験をすることはできなかった。ここが後の後悔ポイント

重要文献を読んでおく

盆栽(本番で使用する ansible role)に手を入れる

  • 基本的に、同じものを使い回しているが、Goが半年に1度のペースでアップグレードされるため、本番でどのバージョンを使用するかで悩んだ
    • 1.13がリリースされそうな雰囲気がある。isucon予選までにリリースされたとしたら、いきなり使うのか?
    • Go Modules を完全に理解していないが俺は大丈夫なのか...という気持ちになり、断念
    • このあと本当に、予選のちょっと前くらいのタイミングで 1.13 がリリースされてしまった

チーム練習をする

  • 8月末にチームメイトと集まってisucon8-qualを解いた
    • 解くために環境を作る必要があって、これもけっこうたいへん
    • が、環境構築をやっているうちに勘というか、「手癖」みたいなものを思い出せた

本番で行ったこと

初期状態の把握

  • こんなこと書く必要があるのか?と思われるかもしれませんが、本当に重要だと考えているので書きます。
  • メモリ、CPUのコア数(と周波数も一応)を知る
  • 使用する言語(luminousはGoです)がインストールされているか確認する
    • Goがインストールされているならどのバージョンなのか?
    • 参考実装では何を使ってライブラリの依存関係を管理しているのか?dep?Go Modules?
    • フレームワーク何?それとも素のnet/http?
  • Webサーバのアプリケーションは何?
    • Nginx?h2o?
  • DBMSは何が使用されていて、バージョンは何?
    • MySQLなら5.xか8のどちら?
  • DBのテーブルは何があって、初期状態でどれくらいのデータがありますか?
  • isuconというユーザは存在していますか?また、そのホームディレクトリは /home/isucon ですか?
  • バックアップは取れそうですか?
    • アプリのコードは当然だが、NginxやMySQLがインストールされているならば、それら設定ファイルを退避させるかgitの管理下に置く
  • $systemctl list-unit-files
    • デフォルトがGo実装だった曖昧な記憶がある
    • 使用しないものを適宜 disable しましょう。今年はやらなかったかも
  • httpsでアクセスできる
    • http2が使えるかもしれない
    • Go1.13の新機能にTLS周りの何かがあったような覚えが...

初回のベンチマーク実行

  • このスコアがわからないと、このあとの改変の良し悪しがわからない

初期セットアップ

  • 使用するツールや言語(Go)をインストールしていく
    • ここで先程の「初期状態の把握」が効いてくる
    • 元の状態に戻せる自信がないと、不安が押し寄せてきて競技どころの話ではなくなる
    • 最悪でしょう?provisioningしたつもりが、そこにミスがあって、しかも元に戻せないって

各種マニュアルを読む

  • この段階でようやくマニュアルに目を通す
    • キャンペーンとか面白そう
    • 「新着一覧」について一定の条件を満たすことで商品のソート順を変更してもいいのか。などと思う
    • 今の段階でこれを考えても仕方がない
    • しかし、なんかマニュアル多し長くない???

モニタリングツールを実行しながらのベンチマーク実行

pprof 導入後のベンチにおいて

  • チームメイトが「なんかこの部分めっちゃコストかかってる」と言っていたので見に行く
    • bcrypt の処理でアプリのCPUが持っていかれている
    • 暗号強度を下げることによって多少は負荷を下げることも可能だろうけど、どこにパラメータ仕込んでるのかな。と思ってコードを漁る
  • 「これなんでこんなにコストかかってるの?そもそもisuconでこんなことやる意味あるのか?isuconなんだから平文でいいだろ!」という話になる
  • ユーザに対して平文のパスワードを収集する方法を考える、ちゃんとログインに成功したものだけを収集しないとダメで、わざとベンチマーカがログインに失敗してくることを考慮する必要がある。などの議論を行う

完走しないベンチマーク

  • 爆速でコードが書ける kyuridenamida が平文保存機構を実装した。しかし、コンパイルは通っているものの、ベンチマークを実行するたびに panic する
  • テメー、平文保存用の構造体の変数名が小文字で始まってるじゃねーか!!!!なんでIDだけ大文字で始まってて他は小文字で始めているんだ!!!気持ち悪いと思わなかったのか!!!
  • これでベンチマークを実行するごとに平文を保存していく機構が完成した

収集する

  • ベンチマークを何度も実行することでパスワードを収集する
    • li_saku が「キャンペーン機能があったから、これを使えばもっと早く集められるのでは?」と重要な思い出しをして爆速で集まるようになった

順位表を確認する

  • 3位だった(やや時系列が怪しいです) f:id:Akensho:20190914102808p:plain
    • ここまで全く順位表をみていなかったため、ややびっくりする
    • Go 使ってるチームが多いだろうし、pprof の結果を見たら bcrypt の処理に手を出したくなるだろうから、俺たちはたまたま手が早かっただけで、まだまだ全然安心できないなと気を引き締めた

ボトルネックの再調査とそれを潰す作業

  • kyuridenamida と li_saku に getNewCategoryList を任せる。N+1らしい。それなら練習で潰せていたし、任せて大丈夫だろうという確信がある
  • 同時に、MySQL周りで不穏なエラーが発生している。どうやらコネクション周りのメッセージだった。
    • このあたりから仕事と準備の疲れで非常に眠くなってしまい、記憶が怪しい。頭もまわっていなかった
    • 冷静にエラーメッセージと /etc/mysql/mysql.conf.d/my.cnf などを見ていく
    • max_connections が少なかったため変更した(はず)。適当に256とか(これも覚えていない)
    • $lsof -i:3306 や $netstat -pantsu | grep 3306 などしていたような気もする(本当に覚えていない)
  • ベンチマークが完走するようになってきたのでまたモニタリングする
    • この辺のタイミングで innodb_buffer_pool_size を 2GB 程度にした。効果は覚えていない。時系列も怪しい
  • mysql が cpu を持っていってて、index 周りもそんなに見てなかったなと思い調べ始める
    • li_saku に任せた。眠い...

複数台構成を考え始める

  • いまの順位は良い。勝っている状態である。その状態で大幅な変更を加える必要あるのか?
    • 切り戻しできなかったときが悲惨だ
    • /initialize を Nginx で適切にハンドリングしてDBサーバに飛ばす、という方針で考えればそこまで大きな工事にはならないはずなんだが、練習していないことはやらない
  • どうせ1台なんだから Nginx <-> app を unix domain socket で試してみるかと考えていた頃(そこが詰まっていたわけではないためスコアに影響することはないだろうが)

平文保存はダメです

  • li_saku が「平文保存はダメってマニュアルに書いてる!!」と気がつく。たぶん同じようなタイミングでdiscord経由で運営チームから通達がきた。「パスワードを平文で保存する改変を行ってはなりません」と
  • 「それはそう」って感じたものの、気になる点もある
    • 現時点で順位を上げてきているチームは暗号化と復号処理に全く手を入れていないのか?
    • 「パスワードを平文で保存する」ということをしていないだけで、bcrypt から他のアルゴリズムに変更している可能性はある
    • だってこれは isucon だから
    • 変更しているとしたら、どこまで暗号強度を落としている??また、どこまで許可されるのか?
  • 暗号強度を考えた場合、攻撃者(プログラム)にとって、適当な処理で生成された暗号文の強度は、平文と変わらないのではないか?たとえその暗号文に平文が全く含まれていないとしても。
  • 本当に暗号化アルゴリズムをどこまで変更してよいのか、悩ましい
  • 「入力されたパスワードの末尾に#をつけたものは平文ですか?」と直接質問できればよいのだが、それは絶対にできない
    • なぜならば、基本的に discord の公開チャンネルでしか運営チームとのやり取りはできないから
    • DM を行っていいのは Alibaba Cloud 関連でハマって個人情報(などの他人に知られてはいけない情報)を運営チームに伝えてサポートしてもらう必要があるときのみ
    • こんな質問を送ってしまうと他のチームへのネタバレとなってしまって、失格処分になる可能性は十分にあり、それだけは避けたい
    • 何を「ネタバレ」とするか?の話もあるのだが、、、
  • 「"パスワードを平文で保存する"という意味は、ユーザが入力したパスワードをそっくりそのまま保存する、という意味と同値ですか?」という質問で、「平文で保存する」の定義を問うことにする
    • 「はい」「いいえ」で回答可能で、この質問はネタバレに相当しないだろうと判断した
  • このあたりから頭が起きてくる

Index 関連の作業が終わり、二度目の質問

  • li_saku が頑張ってくれた
  • そろそろ今後の展望と動き方、何を諦めて何をするかだけはチーム内で話す
    • どう考えても、現在は「パスワードを平文で保存している」のでアウト
  • もう一度、先ほどと同じ質問をする。返事はない。ただ、運営チームは他の参加者からの質問には回答しているため、見逃しているわけではないと判断した
  • 質問に答えてしまうということが、他の参加者に対して重要なヒント(ネタバレ)に繋がりかねないと運営チームは考えているかもしれない
    • であれば、DMしてくれればいいのに。しかし、それをやると他のチームから不公平だと声があがる可能性もあり、難しい

getSimpleUserByID のデバッグをする

  • kyuridenamida が担当していた場所がどうやらバグっているらしい
    • こいつそもそもGoの基本構文を覚えてないからな...と思いきや、SQL を記述した文字列の中で typo していただけだった。コンパイル時に検出できないが、実行してから落ち着いてエラーメッセージを読んだら潰せるタイプのものでよかった

例のアレ

  • そろそろ終了まで時間がない。あと1時間半~1時間程度。パスワードの再収集を考えると、時間的に大きな余裕はない
  • 質問は未だに返ってこないため、もう諦めることにする。回答不能なものなんでしょう。
  • いま最も簡単に実装できて、CPUとメモリに負荷をかけず、暗号化と復号に時間がかからないであろう暗号化関数は何か?
    • やはり先程の $password + "#" か...
func insertPlainPassword(userID int64, password string) error {
    _, err := dbx.Exec(
        "INSERT INTO `password_list` (`user_id`, `plain_password`) VALUES (?, ?)",
        userID,
        password + "#", // 暗号化
    )
    return err
}
plainPass, err := getPlainPassword(u.ID) //実ははプレインではありません。

if plainPass != password + "#" { // 復号化
    err = bcrypt.ErrMismatchedHashAndPassword
} else {
    err = nil
}
  • 本当に最悪だなこれ

再収集+再起動試験+ログを全部切る

  • 新方式によるパスワードの再収集を行う
  • kyuridenamida にアプリ側のログを全部切るようお願いする
  • 俺と li_saku でその他のログを手分けして切る
  • reboot
    • MySQL, isukari, Nginx の順で起動していないため再起動後にアプリに繋がらなくなる
    • isukari.golang.service の設定ファイルを書き換えて順序関係を指定する
    • かなり焦って嫌な汗が流れたが、落ち着いて対応できたので、ものの数分で解決した
  • 目視だけど、オンメモリにしたところも問題なさそう

最終ベンチを実行する

  • 11700前後だった。例年では、終了直前に行うベンチマークのキューが詰まったり(体感です)、結果が非常に不安定になるんですけど、今年は全くそんなことがなくて、本当にすごいなと思う

以上が純粋な競技記録です

ここから先は競技終了後の話です

  • 帰ってから15時間寝た
  • 起きた頃には2日目の終了時間になっていて、緊張しながら結果を待つ
  • 通過する
  • メールで失格に関する通知がくる
  • 失格が取り消される(本選に行けるようになる)

失格の根拠について

  • 「パスワードを平文で保存する」改変を行ったため

失格取り消しの根拠について

  • ISUCON9 運営実行委員会 櫛井様より連絡をいただいてますが、一般に公開されている情報なのかどうか、判断できませんので、ここで公開することは控えさせていただきます

運営チームの取れる・果たせる責任について

俺たちは金銭を支払って大会へ参加する権利を得ているわけではありませんし、運営チームをそれで飯食ってるわけでもありません。あの人達はボランティアです。それゆえに、全ての裁量は運営チームに委ねられている。と俺は考えています。

そのような状態でいったいどのような「責任」とやらが発生するのでしょうか。今回の利用規約を読むと、「主催者等は、本イベントに起因して参加者に生じたあらゆる損害について一切の責任を負いません。」と書いてあります。これが酷いとは全く思いません。取れる責任はありますか?何も問題ないと思います。

しかし、彼ら/彼女らの発言や行動から、「参加者には楽しんでもらいたい!」といった強い ""責任感"" をもって運営されているなと俺は感じていて、実際のところ、「isuconを運営する」という責任は果たしていると思います。この点についてみなさまはいかがお考えになっていますか。

様々な誤解/意見について

  1. 「パスワードを平文で保存する」ことと、「パスワードに平文が含まれた状態で保存する」は異なる概念だと考えています。

  2. 例の最悪な処理を行うことに、他の参加者や運営チームを馬鹿にする意図はありません。「予選を突破する」という点に関して俺は必死でした。必死なら何をしてもいいのか?という主張をするつもりもなく、レギュレーションにおける一部(「パスワードを平文で保存する」)の定義が不明だったため、レギュレーション(他チームへネタバレをしてはいけない)に抵触しないよう、質問する場所がdiscordの公開チャンネルであっため、細心の注意を払ってその部分に関する質問を行いました。突破したいと真剣に考えていましたから。ですので、悪意を持って行動したと見なされるのは心外です。

  3. 俺たち全員を熱心な競プロ勢とカテゴライズして変な偏見もつのやめてもらっていいですか?俺はそういう会社の人間だからカテゴライズされるのは仕方がないと思いますが、自分のことを競プロ勢だと認識していないメンバーもいます。競プロ勢に対して偏見(意見)を持つのはご自由なので好きにしてください。

  4. どういった基準で失格/取り消しのご判断をなされたのか知りたかったものの、謝罪などは要求していません。それを求めるのは筋違いです。また、運営チームの方が私達に謝罪を要求するなど、そのようなことは一切ありません。

  5. 業務と isucon を混同する方々の意見を大量に散見しましたが、みなさまはゲームと現実の区別をつけられないタイプの人間ですか?FPSでバンバン人間を撃っていると、現実でも人間を撃つだろうから規制するべき、などとお考えになっていますか?俺は業務とisuconを区別しています。俺は公式ブログの過去の記事を読んでそう結論しています。

  6. 作問チームは毎回変わるのだから、過去がどうであれ今年は今年だろう、という意見があればそれは聞き入れようと考えています。

  7. 運営チームに対して、「名誉回復してあげることが一番の筋だ」という意見をインターネットで見ましたが、そんな義務はないだろうと思います。だからこうして自分で誤解を解こうと長文を書いています。

  8. 例の最悪な処理を行っただけでは予選を突破できたとは思えません。

  9. 平文どうこうの話、もうこれ以上は消耗しますね...

isucon という競技について

まず、前提として、参加者/運営チーム/isuconという競技 の3つに対して強いリスペクトの気持ちがあります。同年代や年下のエンジニアが予選を突破している姿をみて、キャリアを積んだベテランの方が俺にとっては未知のパラメータを変更することでスコアを上昇させている姿をみて、何も感じない人間ではありませんでした。

数年前から、白金動物園、山形組など、複数回予選突破経験があるチームに対してある種の憧れのようなものを抱いていて、そういった人々に追いつけるよう、万全とは言えないものの、準備を行ってから参加をしています。2年前にギリギリ予選落ちしてしまい、本当に悔しかったのですが、今年はそのときと同じメンバーだったということもあって、リベンジしたい気持ちがありました。

個人的な事情は2年前とは大きく変わりました。俺には妻がいます。2年前は新婚でした。いまはまだ子供はいません。今後、どうなるかはわかりません。運良く子供が産まれた場合、数年間参加できない可能性もあります。また、仕事の状況も年々代わっています。明らかにソースコードを眺める時間は減りました。いまは会社の外にいる時間のほうが長いなんてこともザラにあります。マシンを立ち上げても、slackの返信と資料作成、メールの返信だけで数時間経つことも珍しくありません。そのような状況は今度も続くでしょう。嘆いているわけではありません。自分のやってる事業が拡大しているという実感があり、大変幸せなことです。

しかし、isucon という大会に参加して、チームに対して貢献できるような働きをいつまで行えるかはわかりません。今年が最後かもしれません。いまのところ、本選に参加できるようですので参加するつもりですが、チームメイトがどう考えているかは別の話ですし、冷静に考えたいと思います。