Java プログラミング

CyberAgentさんのサーバーサイドチューニングインターンに参加しました!

zone

サイバーエージェントさんの2日間でサーバーサイドのチューニングをするインターンに参加させていただきました!ざっくりいうとISUCONみたいなやつです!

ざっくりどんなことをしたのか、どうすれば良かったのかを振り返ってみたいと思います。ちなみに選択言語はjavaでSpringBootでできてました。

Goは16人いたのにjavaは2人しかいませんでした(20人中)

なお2日ともZoneを飲んで挑みました。これを探してスーパー2件とコンビニ1件はしごしました。まぁまぁ美味しかったし頭がさえた気がします。

1日目

午後になればなるほど余裕がなくなってメモが雑になっていますね。

sshの設定

まぁパスワード認証でも良いかなぁ〜って思ったんですがVSCodeのRemote SSHがしたかったので鍵認証にしました。ついでにgitで管理するようにしました。

これgit initをどこでするのがいいんでしょうか? etc/nginxとかも管理したいので 僕は / のrootディレクトリでやりましたが・・

MySQLに秘伝のタレを注入

事前にいろんなところからかき集めた秘伝(?)のタレを注入しました。この時点で1389スコアを出し1位になれていました。

Nginxに秘伝のタレを注入

これもっと効果あると思ったんですが全然効果出ませんでした。Nginxでgzip圧縮するとContent-length ヘッダーがなくなるみたいでむしろベンチ通らなくなりました・・

問題となったのはcssだけだったのでそれ以外にgzipをかけておきました。

お昼ご飯

お昼ご飯は社員さんも声をかけていただいて学生で話しながら食べられました(みなさん忙しいようで中々集まれませんでしたが)

静的ファイルをNginxで配信してみる

画像とかNginxで直接配布したらめっちゃ早いじゃん(フロントエンドスピードハッカソンで決め手になったのもこれだし)って思ったんですがベンチ途中で新規投稿があってそこの処理をしなかったのでfailしました。一旦諦めました。

shuffleする関数にThreadLocalRandom.current()をつける

	Collections.shuffle(list);

	Collections.shuffle(list,ThreadLocalRandom.current());

にするとrandomを有効活用できていいぜ!みたいなのをネットでみたのでやったんですが関連ライブラリをimportしてなくてfailしました(vscode remote sshだとエラー表示でないんですね)

アプリの権限周りをいじる

なぜかアプリのコードが渡されたユーザーだと触れなかったので666にしました。でもいじれないものがあったので777にしました(これがのちの災いのもと)

アプリのコードを読んでみる

ここでじっくりコードを読んでみました。途中できになるところをメモしたりしました。この時点で2時くらいです

Tableにimage binaryが入ってたので変更する

table構造を見てこれは絶対改善される!って思ったので取り組みました。ダメです、しっかりボトルネックかを確認してから取り組んでください・・

初期画像はstatic/photoに入っていたんですが新規画像はバイナリをDBに保存する実装になっていました。バイナリをRedisに入れても良かったんですがパスをRedisで管理して都度ファイルを取りに行く方が実装が楽そうだったのでこっちにしました。

ここでずーーーっとハマりました。

Resource none= resourceLoader.getResource("classpath:" + "/static/photo/hoge.png");

みたいにする初期実装があったのでこれを真似するだけじゃんって思ってやったんですが何度やっても新規画像投稿で落ちる。パスは合ってるのに落ちる。

新規画像だけ落ちるのでパスの保存が間違えてるのか?と無限に時間を費やしました。

SpringBootは実行時jarファイルになってるわけなんですけどこの時に最初からある画像は一緒にjarファイルの中に入るんですよね(多分)。classpathから始めるとjarfileの中を探しに行っちゃうので新規に追加した画像は出てこないわけです・・・

Resource resource = resourceLoader.getResource("file:" + "~/hoge/huga.png");

みたいにfileから始めるとちゃんとディレクトリを見に行ってくれます。これに気づいて修正が終わったのが9時ぐらいです。

中間説明であったindexを貼る

中間説明でこの辺にindexを貼ると良いよ〜って教えてもらったので貼りました。なぜか大して効果は出ませんでした。

ここで大事件です

chmod 777 -R /

をやらかしました。linuxを少し触ったことがある人ならわかると思うんですが全ユーザーに全処理を許可する設定を全ファイルにしたわけです。

MySQLのフォルダを弄ろうと本当は

chmod 777 -R .

が打ちたかったんです。

打った当初はctrl+Cで止めて、まぁ別に良いでしょって思ってたんですが一度sshを切ったら サーバーに入れない・・

これはやらかしたぞって思った時にはvscodeも全itermでもログアウトしており手遅れ・・

sshdは権限が適切に設定されてないとsshを拒否しちゃうらしいんですよね。

社員さんが対応してくださる時間も過ぎてたので次の日に直していただきました・・本当にすいませんでした・・

画像がない場合の画像を先に作っておく

とはいえローカルにコードはあったのでできるところからやることにしました。

画像がないところにアクセスすると初期画像が返されるようになってたんですが毎回byte[]から作ってたので先に作った方が早いんじゃね?ってなりました。

結局大して効果はありませんでした

ページネーションSQLを改善する

ページネーションをoffsetを使って実装しているSQLを2つほど見つけました。offsetを使っていると遅いのは知っていたのでなんとか改善しようとしました。

SELECT * FROM tags ORDER BY tagname LIMIT :limit OFFSET :offset

(:から始まるのはコードで代入するやつ)

みたいなSQL文があったときに、これだと取得したくせにoffsetで捨てる分が多く効率が非常に悪いので、先にページの境目となるもの1つの元のSQLで order by になるものだけを取得します。(今回ならtagname)

SELECT tagname FROM tags ORDER BY tagname ASC LIMIT 1 OFFSET :offset

そのあと tagnameを使いwhere文で絞ってからlimitをかけるとoffsetを使用することがなくなります。

SELECT * FROM tags where tagname >= :tagname ORDER BY tagname LIMIT :limit

最初に取得するカラム数も * ではなく1つだけなのでかなり少ないです。(多分、間違ってたらすいません)

こんな感じのことをやりました、体感すごく速くなったんですがベンチにはあまり反映されなくて、なんで・・ってなってました。

iine数をredisで管理

これはisuconの過去問解説で見た気がしました。その名の通り投稿にいいねをしている数を管理するのですが元は毎回SQLでDBに問い合わせるので/initializeの段階で全て読み出しredisで管理することにします。

実はここでバグを仕込みました。以前画像をredisで管理した際にidをキーにしてたんですがここでも記事のidをキーにしました。はい、重複します。これに気づくのはここから12時間以上後のことです。

この辺りで1日目が終了します。(僕は)サーバーには入れないし眠くなってきたので寝ました。

2日目

午後は焦り過ぎて何もかけてません

昨晩作業した内容の修正

なんかめっちゃ早く目が覚めました。昨晩いじったSQLのやつが色々間違ってたので直しました。記事的には修正後の話をしてるのでここでは省きます。

パスワードのハッシュをsha512からMD5へ変更

これもisucon過去問で見ました。劇的な改善があったぽい書き方をしていたので期待に胸を膨らませて実装しました。結果的には200くらい上がっただけで思ったより上がらないなぁってなりました。

最初これをやろう!って思った時に「初期データのパスワード」をどうしようか非常に悩みました。元のパスワードがないので勝手に変更して入れることはできません。

そこで ログイン時に素のpasswordを手に入れられるのでそのタイミングでMD5にupdateをかける。最初は前のハッシュ関数(sha512)でログインさせる。その後新規登録などはMD5で受け付ける。方法を取りました。一回ベンチを回したらsha512は消して良いと思ってたんですがなぜかfailしました。(ユーザーランダムだったんだろうか)

サーバー復活

10時をすぎ本来の競技時間が始まったところで社員さんがサーバーを初期化(一旦最初に戻りました)してくださいました。ありがとうございます・・

コードはgit管理してるしすぐ治るやろ!と思いとりあえずMySQL,Nginxにタレを注ぎなおし、indexを貼り直しました。

この時点で最高スコアの1736がでました。

その後参照実装を初期実装(PHP?)からjavaに変えたらスコアが1301に落ちました。(なんで?)

しかもその後かなり大きな問題にぶち当たります。

最初 /(マジのrootディレクトリ)でgit initしていたのですが git cloneをしてもうまくフォルダ構造に当てはまらないので上書きできないんですよね。かといって全部手書きで写してもその後ブランチでmergeとかできないし・・

結局 同じく / でgit initをし、同じファイルだけgit addをしてgit fetch -> git merge –allow-unrelated-histories を実行してなんとか治りました。この時点で11時

昨晩の変更でバグを量産し解決

ここで昨晩の実装ではredisに入れたidが重複するのでおかしくなることに気づきます。その上なぜか投稿日時が250年前になっています。(これは本当に最後までわからなかったし、ベンチにも引っかからなかった)

これが解決できたのは多分2時過ぎです。

Http2通信に変えようとする

3月にスピードハッカソンに参加した時にhttp2にするとコネクションが一杯貼れて画像とか取ってくるのが早くなると聞いていた上になぜかNginxにsslの設定が書いてあったのでやってみようと思いました。オレオレ証明書を用意したのですが 301 redirectではなく200で返せとベンチに怒られました。メンターさんに聞きましたが多分それは通らない気がするとお返事いただいたので諦めました。

popularArticleのあたり

一ヶ月間の人気投稿を表示するものがあったんですがこれも毎回SQLへN+1を投げて取得しているのでとても重い・・

ただSQL文がかなり複雑で一発でJOINするのも無理そうでした。

ここでメンターさんからアドバイスをいただきアプリケーションコード側でSQLを軽減することにしました。(なくすわけではない)

ただこれも最後バグらせたせいで4時の終了時刻に間に合いませんでした・・・

しかもこれ解説を聞くとそもそもSQLをとっぱらったほうが良いらしい

その後

この後社員さんがパネルディスカッションしてくださったりISUCON優勝者の方が現在の仕事について話してくださったりしました。サイバーさんってそんなにサーバー持ってたんですね・・すごい・・

社員さんとの交流会も用意してくださりいろんな話が聞けて本当に良かったです。

社員さん同士でも話を振ることはあっても無理強いはしないしちゃんと嫌な気持ちにならないように節度をわきまえた上で上司の方がいても楽しく話されていて本当に楽しい会社だなぁという印象を受けました。

ただチューニングの結果は本当に悔しいです。悔し過ぎます

もっと色々できたという気持ちと自分の実力ではこんなものだという気持ちが両方あります。とにかくもっと精進していきたいと思います。

redisやDBをそもそも取っ払ってメモリに全部載せる発想はありませんでしたし、途中サーバーには入れなかったこともあり計測->改善のプロセスを踏めなかったのが痛いです。後もっと地道にindexを貼っていくべきだったなぁとも思います。

しかし結局 色々改善を加えましたが一番スコアが高かったのは初期実装で MySQLとNginxにタレを加えindexを少し貼ったものだったのが本当に謎です!!!どうして!!!

ISUCONには とさくん、さんぽしくんと一緒に出る予定なのでISUCONで挽回します!!目指せ優勝!!

(僕は[rm -rf]より [chmod 777 -R / ]の方が怖くなりました)