logica0419/gasshuku-isucon のソースを読む
ISUCON 夏祭り 2023 で発表された ISUCON 個人作問勢のコードから勉強させてもらう場所。
目的意識はおおよそ Golang 自体の実用性あるコードの書き方と、ISUCON のベンチマークのために作られた isucandar の利用方法のキャッチアップ
とりあえずはベンチマーカーを見ていく予定。
エントリポイントから。
wire というモジュールを使っているが、↓で練習して簡単な使い方は覚えた。
NewBenchmark
NewBenchmark の初期化がこちら
config.NewConfig()
で CLI から入力をもらっている
action.NewController()
は isucandar の Agent を使っているので個別で見ていく
NewController
アプリにアクセスしうる3種類の user agent と、タイムアウトの設定値からなる構造体
agent
- initializeAgent ... 初期化用
- libAgents ... 図書館職員
- searchAgents ... 一般の検索端末利用者
timeout
- initializeTimeout
- requestTimeout
initialize エンドポイントを叩く用の User agent を作成している。
// import (
// "github.com/isucon/isucandar/agent"
// "github.com/isucon/isucandar/failure"
// )
initializeAgent, err := agent.NewAgent(
agent.WithBaseURL(c.BaseURL),
agent.WithDefaultTransport(),
agent.WithTimeout(time.Duration(c.InitializeTimeout)*time.Millisecond),
)
3つの引数は Go でよく見られる Functional Option (?) のやつ。
BaseURL は引数の URL 文字列を URL 構造体としてパースして自身で格納するくらいで大きな仕事はしてない。他のやつらも似たようなもんなのでさほど気にする必要なさそうだが、
WithDefaultTransport で出てくる *http.Transport
についてはあんまりよくわからん。字面的に TCP っぽいのと、定義元のコード を見た感じからもだいたいそういう雰囲気がある。
// DefaultTransport is the default implementation of Transport and is
// used by DefaultClient. It establishes network connections as needed
// and caches them for reuse by subsequent calls. It uses HTTP proxies
// as directed by the environment variables HTTP_PROXY, HTTPS_PROXY
// and NO_PROXY (or the lowercase versions thereof).
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
※ へなちょこエンジニア(私)の個人的感想としては、ちゃんと HTTP より低いレイヤーにも降りていくんだな、と思った。ベンチマーカーともなれば TCP 関係の設定も想定するもんなんやなと。
残りのコードは実際の web アプリのユーザーの数だけ Agent を作成している。職員と一般ユーザーをそれぞれ10名で想定している。
図書館職員の UA ... libAgentsNum = 10
検索端末の UA ... searchAgentsNum = 10
それぞれの agent でやってることはほぼ同じで、initializeAgent の内容ともほぼ同じ。
initializeAgent と違うのは、複数いるユーザーごとで重み付けするために別の構造体 util.Choice
でラップされているところ。
たぶん Scenario あたりの作成でランダマイズするんだと思われる
repository.NewRepository()
たぶんドメイン駆動設計で言うところの Repository に相当する構造体
3種類のデータ構造を扱っている。値を slice, map で保持しているのと、それぞれに対する Mutex によるロックを保持する
- member ... 利用者?(非アクティブ = 退会状態(?) らしきユーザーのリストも持っている)
- book ... 蔵書?
- lending ... 貸出情報?
ゼロ値で初期化して、 go:embed
で読み込んだ初期化用データ init_data.json
からデータを読み込んでいる。
実際の json のデータはこんな感じ
$ cat repository/init_data.json | jq -r ".members[0]"
{
"id": "01H8Q45VZ6YBVT40JZNWWN9KCQ",
"name": "金田 正孝",
"address": "熊本県三島郡出雲崎町吾妻橋2-62-473",
"phone_number": "03-8096-3152",
"banned": false,
"created_at": "2023-08-26T04:54:13.410575+09:00",
"lending": false
}
$ cat repository/init_data.json | jq -r ".books[0]"
{
"id": "01H8Q487WJ5VW54J3SM3R3SMN4",
"title": "nihil velit voluptas in voluptas reiciendis quisquam",
"author": "Jean Fields",
"genre": 2,
"created_at": "2023-08-26T04:55:31.090504+09:00",
"lending": false
}
初期状態としては利用者全員が inactive 扱いっぽい。
他は特に特記すべき事項はない。slice と map のどちらでも値を保持するようにしているだけで特別なことはなさそう。
ひとまず NewBenchmark の中で見る限り NewRepository は json ファイルをソースとして値を引っ張ってくるための関数・構造体らしい。
flow.NewController()
シグネチャが長い... 。呼び出しをパラメータに対応するように整理するとこうなる
flow.NewController(
wc, // chan worker.WorkerFunc
sc, // chan struct{},
controller,// action.InitializeController,
controller, // action.MemberController,
controller, // action.BookController,
controller, // action.LendingController,
repositoryRepository, // repository.MemberRepository,
repositoryRepository, // repository.BookRepository,
repositoryRepository,// repository.LendingRepository,
)
initialize, member, book, lending の4区分でそれぞれ Controller を区別している意図は、この時点ではまだ読み取れない。
2つのチャネル wc, sc も今のところ用途がよくわかっていない。
とりあえず飛ばす
scenario.NewScenario()
さっき作った flow.Controller と、チャネルの wc, sc からなる構造体 Scenario を初期化して返すだけの処理。
実際の使い方を見てみないとわからなさそう。
benchmark.newBenchmark()
CLI 引数と Scenario を受けとっている。
実体の newBenchmark の中で実際のシナリオを構築しているっぽいので、それはまた個別のセクションに分割して読み進める
ひとまず benchmark の main でやっている大枠が把握できた。
次は benchmark の内部関数である benchmark.newBenchmark() の中を見ていく。
ここで実際に作成しているベンチマークシナリオがどんなものかがわかるはずと想定。
benchmark.newBenchmark()
シナリオの構成を担っている部分。
大枠の処理は
[1] (isucandar) Benchmark 初期化
-
[2] シナリオ作成
(isucandar) AddScenario(s)
[3] エラーハンドラ、スコア表示のロジック登録
registerErrorHandler(b) ... gasshuku-isucon で定義しているエラーの種別ごとで step をどう処理するか決めている。Critical ならベンチマーカーが止まるし、そうでないもの (canceled) ならスルーする実装になっているっぽい。
registerScorePrinter(b) ... 中身がよくわからんけどいったん読み飛ばす