ISUCON 9予選の反省

ISUCON 9の予選に敗退しました。 前回のISUCON 8では同じように予選敗退して記事は書かなかったが、今回はよくない動きをしてしまったので戒めとして反省記事を書くことにした。(感想も書きます)

本番前

ISUCON今年はどうしようとTwitterでつぶやいたところ、ISUCON 6や7のときのチームメンバー(mot, syusui)が声を上げてくれたので、チーム3年目として参加することになった。

今まではrubyで参加していたけど、goで参加してみないか?とmotさんから提案があったのでgoで行くことになった。 goは一切書いたことなかったので、A Tour of Goを一通り読んでISUCON 8の過去問を触って、という感じで1週間の夏休みを使って勉強した。 これでISUCONをやるには問題ないぐらいにはgoが書けるようになったと思う。

本番やったこと

  • pprofを使えるようにした
  • 新着アイテム、カテゴリ別新着アイテムの並び順をredisから取れるようにした

以上という感じで、全然取り組めなかった。 pprofとかで遅そうな場所を把握した時間が12時ごろ、新着アイテムの並び順をredisから取れるようになったのが15時半ごろ、カテゴリ別新着アイテムは17時ごろに実装完了した。他に何もできなかった。

カテゴリ別の新着アイテムの取得の部分が遅いと気づてコードを見たときに、N+1クエリがあったのと一覧取得クエリに複数のIN句とOR条件があったのに気づいた。 一覧取得クエリはindexを張ったりIN句やOR句をなるべく使わないように修正する手もあるなと思ったけど、redisのsorted setを使えば一気に解決できていいのでは?と考えてそうすることにした。 この判断が本当によくなかったので大反省をしている。

やったことに対する反省点

  • 実装に時間がかかりすぎてしまった
    • 1, 2時間でできるだろうと思っていたけど、その2倍以上はかかってしまった
    • 練習時には無限に時間がある中でredisを使った実装も試してたので、サクッと行けるだろうと過信してしまっていた
    • そのため自分は他の部分に着手できなかった
    • ときには大規模な改修も必要かもしれないが、競技時間が短かったり改善するとボトルネックが別の場所に移りやすかったりなどの理由から、基本的には短時間で試せる改修を積み重ねて行くほうがいいなと思った
  • 時間がかかった割に効果が薄かった(と思う)
    • どれくらい改善したのかちゃんと計測してないのであれだが、ボトルネックではなかっただろうからおそらくindexを張るだけでも十分だった気がしている
    • 解決すべきボトルネックなのかもわからない状態で大規模な改修を試みるのはよくないと反省
    • index張るのは一瞬でできるわけだからそれを試してみてからでも全然遅くなかったと思う

その他の反省点

  • APIのアクセス傾向やレスポンスタイムをあまり把握してなかった
    • nginxのアクセスログもgo側のWebフレームワークで出力したアクセスログもほとんど見てなかった
    • 外部APIを叩く実装があったりDBのロックを取る箇所があったりとpprofだけではわからない部分が多かったので明らかに見るべきだった
    • アクセスログを見て、遅いエンドポイントから詳しくボトルネックになっている部分を掘り下げていくのがよさそうだなと思った
    • 自分自身はkataribeやalpを触ったことないので次回(あるのなら)までには勉強しておこうと思った
  • 結局DBのindexは全く追加してなかった
    • slow queryは出していて最初ちらっと見ただけで何もしてなかった…
  • 購入周りはロックで詰まったりするだろうなと薄々考えていたけど何もできなかった

その他感想

  • キャンペーン機能でアクセス数をこっち側で制御できるのはおもしろいなと思った
    • 講評の記事を見ると初出ではなかったらしいので過去問対策不足だったかも
    • キャンペーン機能の数値を変えるとログインの負荷が一気に上がることに気がついたのが後半だったので、今後同じような仕組みがあったら早めにいろんな値で試してみたほうがいいなと思った
  • Go Modulesよかった
    • go初心者でも依存モジュールのインストールに全然躓かなかった
  • bcryptの負荷が高かったのが驚いた
    • ログインでこんなに詰まるものなんだと驚いた
    • ログイン専用のサーバに分けて他の処理に影響が出ないようにすることは学びになった(競技中には対応できなかったが)
    • ISUCON 8の本戦で同じような問題が出ていたことを知って、これも過去問対策不足だったなと思った
    • ベンチを回せば回すほどパスワードのハッシュを更新できるのはどうなんだろう…と思ってしまった
  • 椅子が売れた数がポイントになって、椅子が売れるようにある程度調整してもいいというルールは実際のサービスっぽくて面白いなと思った
    • その反面、スピードアップコンテストとはちょっと違ってきている気も…という風にも少し思った(文句はないです)
  • 機能が盛りだくさんで難しかったけど面白かった
    • 商品の出品から決済、発送まで一通り機能があるのがすごいなと思った
    • 決済や発送周りはちゃんと外部APIとして用意されていて気合を感じた
  • 次回があれば今度こそ本戦行きたい…

2017年に趣味とかでやったこと

RCC OBOG Advent Calendar 2017 - Adventar の7日目の記事です.昨日はgol19さんの趣味のこととか作ったものとかです。

2017年に大学を卒業して社会人になったabcangです。ぱちお世代です。 書くネタがないなーと思いつつ登録して、結局書くネタが思いつかなかったので、

RCC老害各位の近況を教えてください。

とあるように近況を書く、とと言うより今年を雑に振り返ってみよう思います。

アニメ

大学のときから継続してアニメを見ています。だいたい趣味の時間の大半はアニメに使っていると思います。(たぶん) 昔は見たアニメをGoogleスプレッドシートに記録していましたが、去年ぐらいからAnnictに移行して、それからはAnnict上で記録しています。

annict.jp

見返してみると、去年放送のアニメは15〜20ぐらい見てたのに対して、今年放送のアニメは10前後とあまり見てないですね。世の中には放送されているアニメをほぼ全部見ている人もいるらしいのでにわかですね。

去年までは単純に見た作品と見たい作品を記録していただけですが、今年の冬頃に大規模なアップデートがあってGitHubの芝のような機能が実装されたので、 2017の春からは話数ごとに記録して芝を生やすようにしてみました。

f:id:abcang:20171206195402p:plain

Twitter連携をしているので見たアニメはどんどん流れていると思います。特定のアプリからのツイートをMastodonにも流すツールを作ったので、それを使ってAnnictの記録をPawooにも流しています。フォローワーさんがアニメの視聴記録を見て、「このアニメのタイトルをよく目にするけど面白いのだろうか」と興味を持つきっかけになってくれればと思って共有しています。邪魔だったらごめんね。

Annictは個人で開発・運用しているということなので応援していきたいです。

作ったもの

今年作ったプログラムとかを紹介したいと思います。

特定のアプリからのツイートをMastodonにも流すツール

上で紹介したツールです。自分のサーバなどで常時起動しておく必要がありますが、ターゲットのアプリを複数指定できるので便利です。

github.com

今はAnnictとはてなブックマークとSwarmをPawooにも流すようにしています。需要があるならWebサービスにするのもいいのかもしれません。

ただ、ユーザーの数だけストリーミングAPIを実行したくないですし、かと言ってポーリングにすると投稿されるまでに時間がかかったりして使いづらくなりそうでうーんと悩んでたりします。とりあえず自分も使いたいと思った人は声をかけてください。考えます。

PCの電源に連動してテレビが付くプログラム

自分は通常のディスプレイは持っておらず、PCをテレビに繋いで使ってます。通常のディスプレイはPCの電源が入ると自動的にディスプレイも付きますよね。テレビでそれを実現しようとしたものです。

github.com

これを使うにはRaspberry Piが必要で、テレビのCECを制御できるcec-clientを使っています。Raspberry Piから定期的にPCにpingを送って、返ってきたらテレビを付けて返ってこなくなったらテレビを消すという非常にシンプルな作りになってます。

そのため、実現しようとしたものと表現したように、使い勝手が悪いです。経験したもので一番多かったのが、テレビの入力をNintendo switchに切り替えてスプラトゥーンをやっていると、勝手にPCがスリープ状態に入ってテレビの電源が切れるというパターンです。

正直、リモコンを使ってテレビをつけるのがだるいだけなので、PCの起動に合わせなくても声で「テレビつけて」って言ったら電源が入ってくれるだけでも個人的にはいいんですよね。

そこでGoogle Homeを検討してみたんですが、どうやらいろいろやるにはIFTTTを経由する必要があるらしく、直接ローカルのマシンに対して通信できないのかーと買うのを迷ってます。

リクエストごとにSQLの実行ログを書き出すgem

Qiitaに書いているのでぜひ見てください。

qiita.com

ニコニコアニメスペシャbot

ニコニコアニメスペシャルbotは今年作ったものではないですが、今年はずいぶんとフォロー数が伸びて1年で450ぐらい増えました。

f:id:abcang:20171206213343p:plain

このグラフはSocialDogというサービスで表示したもので、フォロー管理やグラフの表示ができて便利です。以前はCheetahというサービス名ですが、使っているうちにいつの間にか変わってました。

ちなみにフォローワー数のグラフで段になっているところは、このゆゆ式の一挙放送のツイートが伸びたときです。ゆゆ式すごい、ゆゆ式を見ましょう。

Mastodon

今年はずっとMastodonを触ってました。ずっと前からOSS活動に憧れていましたが、Mastodonでその願いを叶えることができました*1。今ではコラボレーターになって(あまりレビューとかできてないですが)、コントリビュート数も40を超えて上から10番目になりました*2

f:id:abcang:20171206214814p:plain

最近は自分でも検証用のMastodonインスタンスを立てて、Mastodonで発生するエラーを収集したり、SQLの実行ログを取得したり、プロファイラを仕込んだりしてコントリビュートできることを探しています。その話はまたMastodon Advent Calendar 2017で書きたいと思います。

adventar.org

終わりに

今年はMastodonばっかりやってて他のことはあまりできなかったなーと今では思います。あとはアニメやニコ動を見るばっかりであまり生産的なことができてなかった気がします。

大学生のときは面白そうなことに取り組んだりしていた気がする(思い込みじゃないはず)ので、来年はやっていきたいなーと思ったりしてます。

あとは、今年は去年までに比べて全然絵を描けてなかったので来年は頑張ってみようかなと思いながら、iPadを買ったら本当に自分はお絵描きするのか?と自問自答しています。

明日は irgalyさんです。よろしくお願いします。

ISUCON7 本戦で惨敗しました

チーム新卒としてISUCON7 本戦に参加し、見事に惨敗しました。

f:id:abcang:20171126162254p:plain

本戦の問題

本戦の問題は、ルームを作って複数人で協力できるクッキークリッカーみたいなゲーム「Chair Constructor Online」でした。 通信はWebSocketを使用していて、アクションが厳密に同期される非常に複雑なアプリケーションでした。 また、計算が複雑ということでテストが用意されていたのには驚きました。テストは非常に役に立ったのでありがたかったです。

自分が主にやったこと

序盤

WebSocketが使われていたのでどの言語で進めるか悩みましたが、WebSocketがボトルネックになる問題ようなではないだろうと予想して、当初の予定通りRubyで進めることにしました。

最初はプロファイラを仕込んで時間がかかっているところなどを調べていました。 今回はN+1や遅いクエリが全然なく、ほとんどが計算の部分だったのでstackprofが非常に役に立ちました。

あとは、DBのコネクションを使いまわすようにしました。(あとでもとに戻した)

中盤

get_powerやget_priceはcountに対して計算結果はいつも同じなので、計算結果をキャッシュするようにしました。 今思えばredisに入れておけば他のサーバでも使えて、再起動後でも利用できて更に効果があったかも。

あとは1000ミリ秒後の未来をシミュレートする処理が遅かったので最適化しました。

addingとbuyingで一致する時間と、もともとのループで使用していた最初の時間(current_time + 1)と最後の時間(current_time + 1000)だけをループで使用するようにしました(9行目)。 今思えばcurrent_time + 1の方は必要なかった気がします。 また、total_milli_isuは線形増加するので、間の時間をtotal_powerに掛けた値を足すように変更しました(11行目)。

そして、アイテムの購入可能判定では、item_on_saleにその時間を入れるのではなく、一つ前のループの時間から1ミリ秒づつチェックするように変更しました(23-33行目)。 一つ前のループから現在のループまでの線形増加によるtmp_total_milli_isuの値がしきい値を超えていなかった場合、buyingによってtotal_milli_isuが増えていることになるのでそのループ時の時間をitem_on_saleに入れています(32行目)。

終盤

使用されるサーバが偏っていたので、WebSocketの接続先を返すサーバを一つにして、ベンチ対象もその1台にしようとチームメイトに提案しました。 それまではそれぞれのサーバがラウンドロビンで接続先のサーバのホストを返していました。 WebSocketの接続先を返す専用のサーバを1プロセスだけ立てて、すべての接続先の管理を1プロセスに任せることで、より均等に接続先を振り分けることができました。

あと、DBでtoo many connectionsが出ていると言われたので、DBのコネクションを使いまわすのをやめました。 今回はプロセス数とスレッド数、サーバ数も多く、WebSocketで接続されっぱなしになっていたから起きたのだろうと思います。 なんでもかんでも使い回せばいいわけじゃないということがわかって勉強になりました。

チームメイトが主にやったこと

  • motさん
    • mitemをDBから読まないように
    • ミドルウェア周りの調整
    • 全サーバーの使用状況などの監視
  • syusuiくん
    • ある時点での椅子合計数とパワーの合計を保存

反省と感想

今回は、ある時点の計算結果をDBに保存する部分でDBがボトルネックになってしまい、そこからredisに変えようとしたけどうまく動かずにタイムアップという感じでした。最初からオンメモリは危険だろうと思っていたので、最初からオンメモリで考えれていたらもう少し点数が伸びていたのかもしれないです。

いつものISUCONではN+1の解消やDBのindexの改善ばっかりをやっていたので、今回はあまり貢献できなかった気がします。 ただ今回は去年とは違い、初期実装よりも点数が上がり、なんとか10000点も超えることができたのでよかったです。

abcang.hatenablog.com

最後に、運営の皆様ありがとうございました!

ISUCON7の予選を突破しました!

ISUCON6からはや一年…

abcang.hatenablog.com

今年も去年と同じメンバー(@mot, @syusui)で「チーム新卒」という名前で参加して、めでたく予選を突破できました!! この記事では自分が主にやったことを書こうと思います。 ちなみに、ソースコードはgitlabにあげてます。 gitlab.com

全体の流れについては@motさんの記事を見てください。 programmermot.hatenablog.com

始まった直後

今回もチームメンバー全員が使い慣れているrubyを使うことにして、まずはstackprofリクエストごとにSQLのログを取れるお手製ツールを仕込んでボトルネックを探しました。

出力結果を見てみると/fetchや/messageにN+1があったり、画像をDBから取得する処理が遅いことに気が付きました。 画像周りは@syusuiくんに任せて、自分は/fetchや/messageのN+1を改善していきました。

アプリケーション側の改善

/fetchのN+1は、チャンネルごとの既読情報の取得未読メッセージ数をカウントするというものでした。 プロファイラを見たところ、既読情報の取得と、既読情報が一つもない場合の未読メッセージ数(そのチャンネルのメッセージの総数)のカウントが多く呼ばれていました。なのでとりあえず既読情報がある場合の未読メッセージ数のカウントはN+1を残したまま、ほかの部分のN+1を改善しました。

あと、謎のsleep 1.0を見て「なんだこのsleepは!?」となりましたが、レギュレーションを読んでいなかった自分は深く考えもせず消してしまいました。(レギュレーションはちゃんと読みましょう)

その後にMySQLでtoo many connectionsが出ると@motさんに言われたので調べていました。 今までの問題をやっててこんなことはなかったよなーと去年の予選の問題と見比べてみたところ、去年の問題ではThread.currentを使用してスレッド内でDBのコネクションを共有していましたが、今年の問題ではリクエストごとにコネクションを張るようになっていたことに気が付きました。過去問大事ですね。

/messageのN+1は単純だったのでサクッと直しました。 また、ベンチ回してると/historyも遅いと表示されたので見てみると、似たようなN+1があったので直してました。

DBの最適化

@syusuiくんが画像をローカルに保存する変更を加えたり、@motさんがnginxの設定を追えて10万点を超えたあたりで、改めてプロファイリングやDBのスロークエリを見ました。 プロファイリングの結果を見ると明らかにクエリの実行に時間がかかっていて、スロークエリの箇所と一致していました。

スロークエリはmessageのテーブルで発生していて、EXPLAINを実行してみるとindexが効いていませんでした。 クエリではchannel_idとidを使っていましたが、idにしかindexがなかったのでchannel_idとidの複合indexを作成しました。 これで20万点近くまで点数が上がりました。

もう一度スロークエリを見てみると、チャンネルのメッセージの総数のカウントに時間がかかっていることに気が付きました。 メッセージはどんどん増えていくので、総数のカウントにかかる時間もどんどん増えていきます。

だいぶ時間も迫っていてredisを入れたくなかったので、カウンターキャッシュを実装することにしました。 新しくmessage_countというテーブルを作成して、メッセージの追加のたびにチャンネルごとにメッセージ数をインクリメントするように変更しました。 また、何度ベンチマークを回しても大丈夫なように/initializeが呼ばれたときにリセットする機能も実装しました。 これで25万点を超え、最終的には268,588点で1日目の2位になることができました!

感想

今回のISUCONの予選の問題が複数台構成だったのでびっくりしましたが、手も足も出なかったISUCON 6本戦での複数台構成のリベンジができてよかったです。 アプリケーション側のボトルネックは事前に練習していたpixiv社内ISUCONと似ていたので、だいぶスムーズに進めれた気がします。

今回は全然レギュレーションを読んでいなくて、あとから/fetchや/messageの得点について知ったので、ちゃんとレギュレーションは読まないとダメだということを再認識しました。もしちゃんと読んでてsleepを残すという考えがあればもう少し点数が伸びていたのかも?

今回のISUCONの個人的な目標は一般枠で予選通過して本戦に出ることだったので、目標が達成できて本当によかったです! 本戦に出たからには何かしらの功績を残せるように頑張りたいです!

iOS10.2環境に入れたtweakまとめ

iOS 10.2の脱獄環境にインストールしたアプリとかのメモです。 動いてないやつとかは随時更新します。

最終更新日: 2017/02/16

  • 2017/05/04
    • CCQuick Pro X for iOS10を追加
  • 2017/05/10
    • TapToOpenを追加
  • 2017/06/17
    • Browser Changer 10を追加
続きを読む