2020/01/01ツイート2012/11/04Perlの上級初心者がISUCON2に行ってきた話あーどうもどうもこんにちは、acidlemonです。Japan主催のISUCON2(いい感じにスピードアップコンテスト2)に参加してきた話です。2〜3人で構成した25チームがスコアを競うために実サーバ60台、仮想マシン120台(1チームに仮想マシン4台)を用意して行われるWebサービスのスピードアップコンテストということでこれだけの量を用意出来る主催のNHNさんマジすげぇー。こんな大規模でスピード感あふれるイベントに参加できたことはかなり貴重な経験でした。ありがとうございます。1ヶ月くらい前うちの会社にはディフェンディングチャンピオンのfujiwara組がおり、もう1チームくらい出てみたらってことで新卒の若者チームが出るような雰囲気だったのですが結局出なかったようで、中途のおっさん2人+年齢詐称気味の新卒というチームで参加することにしました。(劣化型@typester)とまぁこんな感じになっておりfujiwara組を劣化させるとこうなるみたいなチームになっていたことに私驚きを隠せません。チーム名は「チームぽわわ」となっており「またぽわわか」と思われる読者の方もおられるかもしれません。barやhogeとかの変数名の代わりにpowawaを使いますし、チャットでとぼけるときにぽわわとかぽわぽわとか言いますし、ましてや過去にはネトゲでぽわぽわっていうギルドでオンラインイベントに挑んだこともありましたが今回は@kenjiskywalkerにエントリーを任せたらチームぽわわになってたという話です。普通にkenji組とかでよかったのに。プログラムを書くこと自体はもう10年以上のキャリアがあるため、先日typesterには「れもんちゃんはPerlの文法理解が怪しいけどコード自体はバリバリ書けるので、上級初心者ぐらいまでは行ってるし特にこちらからはもう教えることないですね」みたいな感じの謎の突き放しを食らっております。上級初心者ってなんだよ。前日までそもそも前回のISUCONがあった2011年8月の時点ではWeb業界にいなかったという私ですのでとりあえず社内IRCの#powawaにディフェンディングチャンピオンを呼び出していろいろ聞いてみたりしつつレギュレーションを読んでみると前回は3分間に回った回数をスコアとする感じだったのが今回は規定回数実行までの時間を競う形式になっていることに気付きました。いうことは、たぶんGETの負荷をガンガンかけながらPOSTを規定回実行するというWebアプリになってることは容易に想像できます。NHNのサービスを見回すとそういうサービスはなかったような気がするんですが、まぁ去年はBlogだったから今年はNAVERまとめみたいなやつかなぁ...だとするとGETの嵐に耐えながら画像をアップロードしたりするのかなぁと想像していました。レギュレーション読めばそりゃわかるか。前日の夜くらいまでに社内の雑談は終わりにして前日の相談を開始...する前にライバルを蹴り出します。(スパイよろしく)スパイはぜんぜんよろしくしてくれませんでした。このあとRedisはまぁ使うでしょって話と、varnish使いたいと言われたけどそもそも何それって言ってぐぐったりしているうちに夜が明けていました。私は24時くらいにとっとと寝ており前日の相談をほとんど読んでおりませんでしたが...11:00〜12:00とりあえずざっとPerlのアプリを眺めてみると、生でSQLが書いてあるのでちょっと面食らいました。nodeやPHPでSQLを直書き(プレースホルダはもちろん使うよ)してたことはあるので読む分にはなんとかなりました。RAND()を外してもエラーにならないもののスコアも改善されない。購入処理はネックになってないまぁすべて感想であって何も計測していないんですけどね。この時点ではXslateのテンプレートを読んでなかったので座席に色をつけるところで4096回のループが回っていることに気付いてませんでした。12:30ここらへんで方針が決まります。会場が鯖弁当の匂いに包まれたところでチームでいろいろ話をしてみて、マスターデータがそんなにでかくない、購入データも高々40000件程度にしかならないということでRedisに全部突っ込んで問題なさそうだねという結論に。マスターデータはアプリのメモリに乗っけても問題ないサイズなのでMySQLを捨ててアプリをオーバーホールしましょうということになりました。ここまでの考え方は2位だった山形組さんによく似ています。心配性おじさんになってきたよ@mackee_wがのんびり弁当食ってるのでとりあえずぼくがアプリを書き直し始めます。13:00〜16:00アプリを全部書き直すことにしたので、まずはマスターデータをハッシュデータにするところから。細かい単位はvariationが10個ということと、URLのパスがトップページ、アーティストページ、チケットページになっていることからとりあえずartistとそのID、ticketとそのID、variationとそのIDを持たせた配列をハッシュにvariationというキーで突っ込んで、artistsキーとticketsキーに参照を入れた配列を作りました。このアプリの処理はマスターデータを引く部分と購入してデータを格納するとこに別れているので、初期データを作るところと購入処理を書いてしまえば扱うべきデータを作る部分が全部おわるので、あとは読み出し処理で必要に応じて必要なデータを追加すればOKだなーという感じですね。シートの連番割り当てが可能なのでそのほうがやりやすいだろうということで、連番割り当てで行くことに決定。いうかやっぱ早い者勝ちでいい席がとれたほうがいいかなーと思って最前列から埋めていったほうがいいよねと作っていたときは(ユーザ目線で)考えてました。これ運用者視点だとアクセスの殺到を減らすために席の割り当てはランダムに作った方がいいですね。懇親会の時に「CMとかで時間指定でサービス開始をアナウンスしないでくれ、殺到させてサーバに負荷がかかるだけだー」という雑談を聞いたのを思い出しました。いやでもいい席になるまでキャンセルして取り直すっていうプレイができるようになるのはよくないかな...(完全に話が脱線しています)。データの保存はもちろんRedisです。購入データと件数と00-00から割り当てはじめて今どこまで行ったかの割り当て済カーソルデータを保存しました。売れたseatの個数はvariationごと、ticketごとにキーを作って$redis->incrしまくる感じで作り、購入データはシートごとに固有のキーを作ってそこにメンバーIDをいれる感じで作ります。本来はトランザクションの必要な処理なので、Redisをmultiしてexecするというプレイが必要だったんですが、心の余裕がなかったのでそのまま垂れ流してやっています(シングルワーカー脳)。このRedisの使い方はベストプラクティスからほど遠い使い方で、あとでtypesterに聞いたら「全座席の番号を空いてる座席として最初にSetに突っ込んで、Setから1個要素を取り出して削除するというspopコマンドがランダムに取れてくることを利用してそれをランダムに席を割り当てるルーチンとして使う」というのがRedis的な使い方だったようですね。たしかにぼくの作り方だと在庫管理的なところが非常に脆弱に感じになっている感じがします。書き直したところで14時半くらいになってとりあえずチェック。手元では動いています。なんと座席表をレンダリングするために4096回のループが回っているではないか...でもまぁこの時点でこれをどうにかすることに頭を使うよりもひとまずオーバーホールを終わらせないと話にならないのでそれをなんとか16時過ぎに終わらせてサーバにデプロイしはじめました。デプロイしてもらっている間レンダリング問題を考えていたんですが、まぁアプリ側で出来ることはないだろう、と。MySQLなしの構成にした都合上DBネックがないのでスペックの高いDBサーバでアプリを動かせばパワープレイでレンダリングできるだろうからとりあえずそれでやるしかないねと。それでもダメならアプリに来る回数を減らす(キャッシュする)しかないねという感じでした。来たらアプリに渡さずにキャッシュを返す」というブラフプレイをしたところ売れたチケット数が4095でFAILするという事案が発生し、周りの動揺を誘っていたようです。id:01-20これはひどい。16:00〜17:00デプロイが終わったんですがベンチマークをスタートするとベンチがいきなり止まります。/artist/1も200を返してるのでダメっぽいところが見当たらず。これで結構時間を食ってしまったんですが、「ワーカーの数が...」と言われて私たいへんな事実に気付きました。/adminでやってたので、ワーカーが複数いたら1ワーカーしかマスターデータが初期化されない。いやーまぁそのぐらいは別にどうにかなるんですが、いろんなものをアプリのメモリ上に持っていたのでワーカーが1じゃないと動かない設計になっていました。@mackee_wを半分遊ばせておいて3時間ずっとコード書いてたという自分も相当シングルワーカープレイでしたが、アウトプットしたものまでシングルワーカーなのはもはや笑うしかないですね。17:00〜18:00とりあえずStarmanのワーカー数を1にしたら無事テストがスタートし、csv吐き出しを実装してなかったのでさすがに最後にFAILしましたが1分間の走破はしたようです。スコアは1分で200チケット程度しかさばけてなかったので、せっかくRedisを使ってもさすがにワーカーが1じゃどうしようもないよねということでこれを修正しにかかります。この時点でアプリが抱えていた問題は少なくとも2個ありました。ベンチマークが始まらない理由であったマスターデータの初期化は初回アクセス時にハッシュを初期化すればいいとして、もう1個の問題は「最近売れたアイテムをアプリのオンメモリにおいていた」ことでした。いうことで大変なパニックに。決めて作業開始しましたが、Redisへの入れ方がよくなくて躓いているままタイムアップ。SetじゃなくてListに入れるべきだったのと、購入データをキーごとに分けるのは(本来)ダメでハッシュをJSON化して1キーに入れるべきだったというのは終わった後にわかったことでした。おわりにそんなわけで、Redis力が足りなかったのと純粋なPerlコーディング力が足りずにオーバーホールに失敗してスコア無しという最低の結果に終わりましたが、アイディアとしては特別賞チームにやり方にかなり似ていただけに非常に残念。山形組との違いとしてはシングルプロセスマルチスレッドではなくマルチワーカープロセスを目指してたオールオンメモリじゃなくて永続データのストアとしてredisを使ってたフロントにvarnishを置こうとしてたといったあたりでしょうか。アプリ側をちゃんと書き切れたところで4096マスのtableをレンダリングするところがやはり詰まるところは予想できるので、そこをなんとかしないことにはスコアは伸びなかったと思います。ところでこの記事を書くためにIRCのログを読み返してたんですが、前日にこういう発言がありました。素直にアプリ書くのは@mackee_wに任せればよかったか。acidlemonです。鎌倉市在住の普通のITエンジニアです。30年弱住んだ北海道を離れ、鎌倉でまったりぽわぽわしています。