Open8

logica0419/gasshuku-isucon のソースを読む

hassaku63hassaku63

ISUCON 夏祭り 2023 で発表された ISUCON 個人作問勢のコードから勉強させてもらう場所。

https://isucon.connpass.com/event/288820/

https://github.com/logica0419/gasshuku-isucon

目的意識はおおよそ Golang 自体の実用性あるコードの書き方と、ISUCON のベンチマークのために作られた isucandar の利用方法のキャッチアップ

https://github.com/isucon/isucandar

とりあえずはベンチマーカーを見ていく予定。

hassaku63hassaku63

エントリポイントから。

https://github.com/logica0419/gasshuku-isucon/blob/main/bench/main.go#L11-L18

wire というモジュールを使っているが、↓で練習して簡単な使い方は覚えた。

https://github.com/hassaku63/goscrap

NewBenchmark

NewBenchmark の初期化がこちら

https://github.com/logica0419/gasshuku-isucon/blob/main/bench/benchmark/wire.go#L16-L39

config.NewConfig() で CLI から入力をもらっている

action.NewController() は isucandar の Agent を使っているので個別で見ていく

hassaku63hassaku63

NewController

アプリにアクセスしうる3種類の user agent と、タイムアウトの設定値からなる構造体

agent

  • initializeAgent ... 初期化用
  • libAgents ... 図書館職員
  • searchAgents ... 一般の検索端末利用者

timeout

  • initializeTimeout
  • requestTimeout

https://github.com/logica0419/gasshuku-isucon/blob/main/bench/action/action.go#L33-L70

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 あたりの作成でランダマイズするんだと思われる

hassaku63hassaku63

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 ファイルをソースとして値を引っ張ってくるための関数・構造体らしい。

hassaku63hassaku63

flow.NewController()

https://github.com/logica0419/gasshuku-isucon/blob/main/bench/benchmark/wire_gen.go#L33C2-L36

シグネチャが長い... 。呼び出しをパラメータに対応するように整理するとこうなる

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 も今のところ用途がよくわかっていない。

とりあえず飛ばす

hassaku63hassaku63

scenario.NewScenario()

さっき作った flow.Controller と、チャネルの wc, sc からなる構造体 Scenario を初期化して返すだけの処理。

実際の使い方を見てみないとわからなさそう。

hassaku63hassaku63

benchmark.newBenchmark()

CLI 引数と Scenario を受けとっている。

実体の newBenchmark の中で実際のシナリオを構築しているっぽいので、それはまた個別のセクションに分割して読み進める


ひとまず benchmark の main でやっている大枠が把握できた。

次は benchmark の内部関数である benchmark.newBenchmark() の中を見ていく。
ここで実際に作成しているベンチマークシナリオがどんなものかがわかるはずと想定。

hassaku63hassaku63

benchmark.newBenchmark()

シナリオの構成を担っている部分。

https://github.com/logica0419/gasshuku-isucon/blob/d0a68c6ec22fd19841aa8f4a6ffc5acb6a946ba3/bench/benchmark/benchmark.go#L19-L38

大枠の処理は

[1] (isucandar) Benchmark 初期化

-

[2] シナリオ作成

(isucandar) AddScenario(s)

[3] エラーハンドラ、スコア表示のロジック登録

registerErrorHandler(b) ... gasshuku-isucon で定義しているエラーの種別ごとで step をどう処理するか決めている。Critical ならベンチマーカーが止まるし、そうでないもの (canceled) ならスルーする実装になっているっぽい。
registerScorePrinter(b) ... 中身がよくわからんけどいったん読み飛ばす