2017-10-25

[isucon] ISUCON7 予選通過しました

ISUCON7 にチーム名 円山町 (@hidekiy, @kotaroy, @k_enoki) として出場し、去年に引き続き予選通過しました。当日について、記憶が定かな内にメモしておきます。

今回の予選問題について
isubata という名前で、これは ISUCON 参加者になじみの深い idobata というチャットサービスを参考に作られているようでした。

用意したもの
GitLab のプライベートリポジトリと権限設定
専用の Slack チャンネル
Mackerel オーガニゼーションと公式プラグイン (inode, linux, mysql, nginx, proc-fd, uptime) の設定
ngxtop の使い方

2017/10/22 13:00
無事全員そろって開始
当日用マニュアル熟読 (@hidekiy, @kotaroy)
公開鍵ログイン設定と ssh config 作成 (@k_enoki)
アプリのソースコードを GitLab へアップロード (@k_enoki)
アプリを Go 実装へ切り替えと自動起動設定の修正 (@k_enoki)
isubata アプリの動作確認 (@hidekiy, @kotaroy)
インフラ構成と、nginx, MySQL の動作状況の確認 (@k_enoki)
トラブルシュート用の mackerel-agent を全台に設定 (@hidekiy)

14:00
ローカル開発環境構築 (@hidekiy, @kotaroy)
make deploy で全台にアプリがデプロイ (ローカルで GOOS=linux でクロスコンパイル後、scp して rename して systemctl restart) されるように設定 (@hidekiy)
静的ファイルの nginx 配信化と事前圧縮設定 (@k_enoki)
DB のネットワーク上りがあんまりな状態だったので、一旦 image のオンメモリキャッシュを実装 (@hidekiy)
/icon にインチキ Etag + If-None-Match 実装を試すが、全く 304 を返せず挫折 (@hidekiy)
/profile の画像アップロード機能で、DB に二重に書き込まないように修正、ついでにアップロード内容をオンメモリーキャッシュに乗せるように修正 (@hidekiy)
/profile の2個のアップデート文を1個に統合 (@hidekiy)

16:00
このあたりでようやく API リクエストを順調にさばけるようになり、 ようやく nginx の too many open files を拝めて、チーム内安堵した。
2台構成にすればリーダーボードに載りそうな気配があったので、一時的に2台構成に変更し記念スコア取得 (@k_enoki)
スロークエリログ 0.1 で集めたクエリを pt-query-digest で集計して調査 (@kotaroy)
URL を保存し、初期 icon も静的配信に変更 (@k_enoki)
なぜか2台構成にすると 304 が減ってしまい、1台の時よりもスコアが落ちてしまう問題の調査 (@k_enoki, @hidekiy)
→ 議論の末、2台のサーバー間の差分は mtime しかないので rsync --archive してもらい解決 (@k_enoki)

18:00
/message, /history の N+1 クエリを修正 (@hidekiy)
/message, /history のバグ (IN () の内容が空になって SQL エラー) を修正 (@hidekiy)
/fetch の N+1 クエリを修正 (WHERE で OR して GROUP BY) (@hidekiy)
遅くなったという報告があったので、/fetch の N+1 クエリを修正その2 (初期状態のクエリを UNION ALL) (@hidekiy)

20:00
/fetch に入っているスリープ秒数を色々テストするが特に効果なしと判断して元に戻す (@hidekiy)
不要なカラムの取得をしないように修正 (@hidekiy)
アプリログ出力機能を削除 (@hidekiy)
nginx ログ出力を停止 (@k_enoki)

感想
かなり苦しかったですが、ファイルの mtime を揃える必要があるということが解明できたのが良かったです。
また、チャンネル一覧に未読カウントカラムを追加して、COUNT(*) を節約するという改善には、当日たどり着かなかったので今後精進します。

インフラ、アプリ、データベースの知識がバランスよく必要になる、素晴らしい問題を作成された KLab さま、ありがとうございます。
また、安定したサーバーインスタンスで支えていただいた さくらインターネット さま、ありがとうございます。的確に帯域制限されたローカルネットワークは今までになく面白い ISUCON 予選になっていたと思いました。

リンク
ISUCON7 まとめ
ISUCON7 オンライン予選 全ての順位とスコア(参考値) 予選通過順位は9位でした

過去の記事
ISUCON6 本戦敗退しました #isucon
[isucon] ISUCON6 予選通過しました
[isucon] ISUCON5 予選敗退しました

2017-01-22

[golang] crypto/rand で疑似乱数生成器を初期化する

Go 言語で疑似乱数 math/rand を使うとき、実行ごとに別の乱数列を選択するための方法として、rand.Seed(time.Now().UnixNano()) とするのが人気があるようなのですが、より予測困難さを追加するためには以下のコードのようにすると良いです。

今日では、Perl, Ruby, Python などで、明示的な seed をせずに rand を使った場合に、内部的に行われる自動的な seed では、OS の乱数生成器由来の値が使われるようになっているので、Go でも同じ感じで良いと思います。


package main

import (
    cryptorand "crypto/rand"
    "encoding/binary"
    "math/rand"
)

func main() {
    randSeed()
    println("rand int:", rand.Int())
}

func randSeed() {
    var seed int64
    err := binary.Read(cryptorand.Reader, binary.LittleEndian, &seed)
    if err != nil {
        panic(err)
    }
    rand.Seed(seed)
}

LittleEndian は BigEndian でも良いです。crypto/rand には big.Int のインターフェースもありますが、こちらの方式の方が簡潔に書けると思います。

蛇足ですが、暗号化用の鍵や、何とかトークンのような、長さ分のエントロピーを持つ乱雑な文字列を作りたい場合は、math/rand ではなく、crypto/rand をそのまま使う必要があります。

リンク
math/rand: Deterministic random by default is dangerous #11871