s平面の左側

左側なので安定してます

ISUCON の過去問に挑戦した(ISUCON3 予選問題編)

仲間内でやっている勉強会の一環で、ISUCON の過去問に挑戦した。

今回は ISUCON 3 のオンライン予選問題。

公式で AMI が提供されている。

isucon.net

github.com

今回のルールは下記の通り

PHP 実装でベンチマークを回すと再現性無く FAIL が発生したため、平均ではなく最高スコアとした

私の最終的なスコアは下記の通り。

Result:   SUCCESS 
RawScore: 3772.6
Fails:    6
Score:    3433.1

ISUCON3 予選突破ラインが約 10000 とのことなのでまだまだ。

インフラ・ミドルウェア周りを疎かにしていることがはっきりと結果に反映された。

目次

やったこと

※施策ごとのスコアは記録を取っておらず、記憶に頼って書いているので不正確であることに留意

最終的なソースはこちらにアップしてある。

github.com

初期スコア測定・環境の準備など

とりあえず PHP 実装に切り替えて、ベンチマークを回す。

Result:   SUCCESS 
RawScore: 1864.2
Fails:    4
Score:    1845.5

先述のとおり、PHP実装では再現性なく FAIL が発生してしまう。(この結果も何回か回した上でやっと出た)

その次に作業環境として

  • SSH 接続のために isucon ユーザの鍵を登録
  • PhpStorm のプロジェクトを作成
  • GitHub リポジトリを作成

あたりをやっておく。

index (雰囲気)を貼ったつもりになる

最初にざっくりテーブル定義を確認し、ソースコードを見渡した後に「雰囲気」で index を貼った。

create index memos_user_id_index on memos (user, is_private);

しかしデータ初期化の仕組みを理解しておらず、ベンチマーク時には index が削除されてしまっている状態だったため、スコアは変動せず。

原因に気づいたのは最後の方だった。

PHP のバージョンアップ(に失敗)

ISUCON3 実施当時は PHP 7 などなかったので少しズルい気がするが、使えるものはなんでも使おう。

ということでまず最初に PHP7.1 のインストールを試みる。

# yum remove php*
# wget http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
# rpm -ivh remi-release-6.rpm
# yum --enablerepo=remi-php71 --disablerepo=amzn-main install php php-mysql php-pecl-memcached

が、 apache2.4 まわりの依存性解決につまずいたので断念。

ここに時間を割きたくなかったので PHP 7.0 で妥協。

# yum remove php*
# yum install php70 php70-mysqlnd php70-pecl-memcached

「今度はうまくいきそう!」とベンチマークを回すも今度は FAIL が多発してスコアが 0 に。

どうやらセッションが保存できていなさそうなことを確認。

いったん元のバージョンに戻す。

memos 取得の order by 指定を created_at から id に変更

色んな所で memos テーブルのレコードを取得する際に created_at カラムで order by していたが、主キーであり auto increment である id を使っても同じやろ!ということで変更。

結構な確信を持ってベンチーマークを回したが、かえってスコアは下がり 1000 を切るようになった。

泣く泣く戻す。

※原因は後ほど

last_accessed の更新をやめる

last_accessed を更新しない · okashoi/isucon3-practice@d33c500 · GitHub

ユーザがログインする都度 last_accessed というカラムの日時を更新していたが、どこにも使われていなかったので更新をやめる。

これで RawScore が 100 ほど改善 (1900)。

PHP7.0 再挑戦(セッション保存先をファイルに変更)

memcached がうまく動かないのでいったん使わないようにしておく · okashoi/isucon3-practice@9648b61 · GitHub

セッションの保存に失敗しているようなので、いったん memcache ではなくファイルに保存するように変更。

これで動くようになった。

PHP7.0 にする前と比べて RawScore が 400 ほど改善 (2300)。

そこまで劇的には変わらなかった。

$older, $newer が見つかったらループを抜ける

と が見つかったら break · okashoi/isucon3-practice@080f712 · GitHub

メモ閲覧画面には、1つ前 ($older)・1つ後 ($newer) のメモへのリンクが存在している。

それを見つけるロジックにて、見つけた後もループし続けていたため、見つけたらループ抜けるように変更。

RawScore が 100 ほど改善 (2400)。

N+1 問題を回避

N+1 を回避 · okashoi/isucon3-practice@76ae871 · GitHub

トップページにて、メモとユーザ名を結びつける部分があからさまに N+1 になっていたので修正。

RawScore が 300 ほど改善 (2700)。

セッションを保存先を Redis に変更

セッションの保存先を Redis に変更 · okashoi/isucon3-practice@ef0ffc3 · GitHub

「memcache がダメなら Redis だ!」という発想で Redis インストール。

sudo yum install --enablerepo=epel install redis
sudo yum install php70-pecl-redis

うまく動いてくれた。

RawScore が 200 ほど改善 (2900)。

view 内のループで preg_split を使っていたので explode に変更

ループで preg_split を使わないようにした · okashoi/isucon3-practice@0b9792e · GitHub

view 内のループの中でメモの1行目を取得するために、贅沢にも preg_split を使っていた。

正規表現を使った処理は重いので、文字列処理である explode (と trim) に変更。

シンプルだが、意外と効いて RawScore が 400 ほど改善 (3300)。

MySQL のクエリキャッシュ設定

デフォルトでクエリキャッシュは効かないようになっているため、効くように設定。

[mysqld]
query_cache_limit=1M
query_cache_min_res_unit=4k
query_cache_size=256M
query_cache_type=1

これで RawScore が 300 ほど改善 (3600)。

※ ただしあくまでクエリキャッシュであるため、本番の ISUCON では使い所が難しいとのこと。強いてやるとしたら、 init のタイミングで沢山実行されそうなクエリを予め実行しておく、など。これも init の時間制限があるの積極的には利用し難い。

改めて、index(雰囲気)

インデックス貼った · okashoi/isucon3-practice@373b2b3 · GitHub

データ初期化の仕組みを理解して、改めて冒頭の雰囲気 index を設定。

RawScore が 200 ほど改善 (3800)。

マシンを再起動し動作確認

残り時間 15 分を切ったあたりでマシンを再起動し、ベンチマークが問題なく回るか確認。

httpd が立ち上がらないようになっていたので、設定。確認してよかった。

そして競技時間終了。

反省・感想など

終わった後は、解説記事を読みつつ反省会。

isucon.net

スコアと作業の記録を結びつける

慣れているメンバーは作業ログを次のようにして残していた。

  • ミドルウェアの設定ファイルも Git 管理下に置き、シンボリックリンクを張るようにして一元管理する
  • 作業を Pull Request にすることで、Pull Request のページがそのまま作業ログのページになる
  • Pull Request のコメントとして、その時点のベンチマーク結果を貼る

これは「何をやったら、どれくらいスコアが上がった」が見えるようになるので、振り返りもしやすく、とても良い。

後でブログ記事を書くのも楽だ。

施策に根拠を持つ

今回はなんとなく「この辺かな?」というところを手当たり次第触った感じだった。

これでは施策の優先順位付けや、「つまずいたときに『いつ』見切りをつけるか」という判断ができなくなる。

とくに SQL まわりはお粗末な対応だったのできちんと slowlog を見たり、explain の結果を見たりした上で施策を考えなきゃな、と痛感した。

「memos 取得の order by 指定を created_at から id に変更」の施策の効果が出なかったのは、クエリの where 句にて is_private を指定していたため。

is_private, created_at の複合 index を貼るのが正しい対応で、他の参加メンバーはみんなきちんとこれをやっていた。

普段の業務とは違うアプローチを身につける

普段の私が業務で扱うシステムは高負荷状態になったり、極端にスピードが求められることが無い。

どちらかと言えば運用保守のし易さや、データの健全性を優先することが多く、考え方もガチガチに正規化テーブルに立脚されたものになりがちである。

上記の解説記事ではテーブルの非正規化によってスコアを改善していく部分(public_memos, public_count テーブルの追加、memos.title カラムの追加等)があるので、こういったスピード優先のアプローチを身に付けたい。

デフォルトの構成を決める

一緒に参加していた勉強会メンバー曰く、デフォルの構成を決めてしまい「いつも通りに作業ができる」土俵に持ち込むのが良い、とのこと。

勉強会メンバーではスキルセット的に PHP7.1 + nginx + php-fpm + MySQL5.6 という構成が良さそう、という話で落ち着いた。

ただ、例年の ISUCON を見るに PHP はやや不遇な扱いを受けているような気がしており「第二の選択肢は準備しておきたいよね」という話にもなった。

「身に付けたいスキル」という観点も含めて「言語としては Go あたりかな〜」という話をした。

まとめ

単独で ISUCON の問題に挑戦するのは今回が初めてだった。

いろいろ勉強になり、自身の課題もたくさん見えてきたので ISUCON に取り組むことは非常にいいことだと思う。

1人だとなかなか「やろう!」という気にならないが、人と一緒ならやる気になるので今のメンバーがいることに感謝。

【資料あり】「Laravel/Vue.js 勉強会 #1」で登壇してきた

connpass.com

会社のつながりで「Laravel/Vue.js 勉強会を開催するので誰か登壇しませんか?」という話が挙がったので真っ先に手を挙げて挑戦してみた。

テーマは「Laravel Mix とは何なのか?」。

Vue.js に興味を持ったきっかけが「Laravel が公式採用した」という話だったが、そういえばその実体をよく把握していなかったなあ、と思ったのがこのテーマを選んだ理由である。

振り返り

発表者4名中、私以外の他3名がサービスでの採用事例だったのに対して、私は知識というか概念の説明だった。 かなり初歩的な内容だったのも併せて、聞いていた人に価値が提供できたのかは少し自信がない。

また、最後のデモで失敗しグダグダになってしまったのは大きな失敗だった。

事前の練習もしており、万が一のときのコピペ用ソースコードも準備していたのだが、ディスプレイを拡張モードにしていたことによる「画面を見ながらコーディングできない」ことが仇となって「ビルドに失敗する」「修正も満足にできない」という非常に後味の悪い終わり方をしてしまった。

次回このような機会があって、デモ(というよりライブコーディング?)を行う際には、

  • ディスプレイはミラーリングにする
  • 失敗したとき用に、成功時の画面イメージをスライドに入れておく(失敗しても結果を伝えられる)
  • 「成功したら、拍手をお願いします!」のような失敗する可能性をほのめかしておく(失敗しても微妙な空気にさせない)

といった布石を打っておくようにしたい。(当然、失敗しないための事前の入念な準備が何より大切なわけだが)

それから、発表時にストップウォッチを起動することを忘れてしまい、ペース配分が感覚に頼ることになってしまったのも反省。

デモ失敗した際に「時間押してますよね?」と確認していたが、実際には発表終わったのはほぼ予定通りの時刻だった(ただし私の発表がきっちり10分だったかはわからない)。

所感

発表を聞いていていろいろ知らない単語が出てきたり、LT 枠に Qiita の記事をめちゃくちゃ参考にさせてもらている nunulk さんが登場したり、懇親会でフロント界隈の情報をいろいろ聞くことができたりと、意義のある勉強会だった。

nunulk さんに話しかけようと思ったが、残念ながら懇親会にいらっしゃらなかったのは心残り。

自身の発表はいろいろと問題があって反省点の多く残る内容であったが、今後も継続的に発信を続けていって精度を上げていく所存。

コーディングするとき・プルリクにコメントするときの観点

2年目、3年目と時が経つにつれて、他人が書いたコード見てコメントをする機会は増えていく。

このときに(主にコーディングに関する)自分の考え方を言語化して他人に伝える、というのはとても骨が折れる作業だ。

そこで、これまでに他人にしてきたコメントを思い返しつつ「普段どんなことを考えながらコーディングしているか」「プルリクを見るときにどんなところを見ているか」という観点を整理してみた。

もちろんただ文字に起こしただけでは伝わらない。 何度も繰り返し改善案のコードとセットにして伝える、という積み重ねの先に観点・感覚が身につくはずだと信じている。

※この記事では主に「バグが発生しにくいこと」「バグが発生しても原因を特定しやすいこと」を目的としている。

※具体的なコード例を載せると長くなるので割愛。文字での説明のみ。わかりにくいのは承知の上。

目次

名付けについて

  • 理解までの時間が最短か
  • 実態を表しているか
  • 誤解を生まないか

正直これについては、感覚的な部分や語彙力(英語)に依存する部分であるので、コメントしたからと言って一朝一夕で身につくものではない。

習慣として他人が書いた「筋の良い」コードを読むのが力を身につけるための王道だろうか。

引数について

  • 引数によって過剰に挙動を制御していないか
  • メソッド名から引数を予想できるか(最も自然な引数になっているか、驚き最小の法則)

前者については別のメソッドやクラスに切り分けよう。

戻り値について

  • 失敗を表すのに null や false を使っていないか
  • 空の結果を返すのに null や false を使ってないか
  • 戻り値に型が混在していないか

1 つめについては例外を使いましょう、というお話。 null や false では「その時点」ではプログラムが動き続けてしまう。 もし戻り値チェックが行われなかったらおかしなデータのまま後段の処理に進んでしまい予期せぬ挙動を引き起こす原因になる。

2 つめについては空配列や空文字列を使いましょう、というお話。 ただし数値の場合は議論の余地があり、場合によって null を使うのもやむなし、ということもある(0 や負値をとり得る数値の場合)。

データとその型には意味があって、意味に沿ったプログラミングをすれば記述は自然なものになる。 null や false を返していたら都度戻り値チェックをしなければいけないが、空配列を返しているなら、配列が来ることを想定している後段の処理にそのまま渡して問題ない(場合が多い)。

余分な処理が増えれば当然コーディングの量が増えて生産性は下がるし、バグが入り込む余地も増える。

3 つめは「処理の結果によって数値が返ってきたり、配列が返ってきたりする」みたいなことになっていないか、というお話。 実は2つめの内容を内包している。

期待される型が 1 つだけなら、後段の処理も1種類の型を想定すれば済む。

オブジェクト指向プログラミングなら継承の仕組みもうまく使いたいところ。

例外について

  • 例外を握りつぶしていないか
  • 失敗等のときにすぐに例外を投げているか

パフォーマンス面を気にするのであれば、また事情は変わってくるが。

責任範囲について

  • 1 メソッドが大きすぎないか
  • 複数箇所のデータにアクセスしていないか
  • 一言で表すことができる名前をつけられるか

名付けに悩んで良い答えが出ないときは、往々にして設計を見直したほうが良いという場合がある。

既存のプログラムについて

  • すでに「それ」を実現できる組み込みまたは公開されている関数・クラスはないか(車輪の再発明
  • 低レイヤーの処理をよしなに wrap してくれているものはないか
  • 既存のものを使うとき、「イケてない」仕様を wrap してあげられないか

2 つめの例を挙げるなら PHP における curl に対する Guzzle のようなもの。

その他

  • 余分な処理をしていないか
  • ネストを少なくできないか
  • クラスやメソッドの階層・粒度が一致しているか
  • 処理が何かの文脈に依存していないか

3 つめの話は設計の領域に入ってくるので、少し抽象度が高く伝わりにくいかも。

4 つめは「暗黙の前提条件を満たさないと正しく動かない」ということがないか、というお話。

「メソッド B は先にメソッド A を呼び出してから使わなければいけない」とか。 この場合、メソッド A の戻り値となるクラスの方にメソッド B を生やすなどして、間違って使われる余地が少なくなるような設計にすると良い。

その他、感覚レベルの話(オカルト含む)

  • コードの文字の密度が高すぎないか

→ 集中力が分散し、「見落とし」が起こりやすくなる。

  • コードに対称性はあるか

→ 対称性があると「異物」を見つけやすい。前節で説明した「階層」「粒度」が揃っているかが分かりやすい。

  • ざっと見渡したときに「違和感」がないか

→ ここまでに説明した内容が守られていると、コードは「美しい」

余談

ここまでまとめてきた内容は、概ね次の3つの資料でカバーされていると思う。