はじめに
Garden Night の入口チェックは応答を正しく分類できるようになりましたが、最初の実装は割り当てを 1 件ずつ処理します。入口の 1 つが遅いと、ほかの有用な結果が得られるのも遅れます。少数の固定された Go ワーカーで作業を分担し、すべての割り当てに結果を返します。
この独立単元には、単一対象の完成済みチェック関数が含まれます。Go の関数、構造体、スライス、モジュール、基本的なチャネル構文と、前の単元の HTTP 結果、または同等の経験が必要です。ゴルーチン、チャネルのクローズ、ワーカーの終了調整は、使う前に説明します。リクエスト先は準備されたローカル対象だけです。これは単発ツールであり、ベンチマークや監視プラットフォームではありません。読む時間や通常の間違いへの対応を含め、約 60 分を見込んでください。著者による見積もりです。
分担する前に実際の処理を観察する
このステップでは、現在の逐次チェックを実行し、実際のサービス観測記録を調べます。Workspace から始めます。cd はプロジェクトを選び、export は準備された Go のディレクトリを現在のシェルのコマンド検索パスに加えます。新しいターミナルセッションでは、この 2 行を再実行してください。
cd /home/labex/project/site-checker
export PATH=/usr/local/go/bin:$PATH
チェック関数、作業ループ、タイムアウトしない対象一覧を読みます。cat はこれらのファイルを表示するだけです。
cat check.go run.go overlap.json
Check はすでに ok、http_error、content_mismatch、connection_error、timeout を返し、作業の識別情報と受信したステータスを保持して、各本文を閉じます。逐次版の Run は workers パラメーターを無視しています。ここが変更する動作です。
overlap.json の各入口は、有限の 250ms の準備された処理を行います。各対象のタイムアウトを 2 秒にして、この観察をクライアントのタイムアウトではなく並行処理の確認にします。go run . はこのパッケージを実行し、-input は割り当てを選び、-workers は活動中のチェック数の上限を指定し、-timeout 2s はリクエストの予算を設定します。> は標準出力をファイルに書きます。
go run . -input overlap.json -workers 2 -timeout 2s > normal-report.json
ファイルを読みます。
cat normal-report.json
6 件すべてが ok になるはずです。しかし、レポートだけでは処理が重なったかどうかは分かりません。準備された観測器は、実際のサーバーハンドラが開始、終了するときに enter と exit を記録します。active は本当にリクエストを処理中のハンドラ数です。小さな Python 読み取りプログラムは、対象へのリクエストを増やさず、既存の記録を取得します。
python3 observe.py
この新しい逐次処理の環境では、Actual handler peak: 1 が期待されます。開始と終了が交互に現れ、活動中のハンドラは最大 1 つです。観測器はこれらのリクエストより前に準備されていました。有限の 250ms の処理は、並行クライアントが重なる機会を作りますが、遅延や総実行時間だけでは重なった証拠になりません。証拠になるのは記録です。
現状でも完全なレポートは作れますが、同時に動く処理は最大 1 件です。このレポートを本ステップの正常例として残してください。次は run.go だけを置き換えます。イベントサービス、対象定義、チェック関数は引き続き利用できます。
固定数のワーカーに作業を渡す
このステップでは、作業と結果の流れを設計してから、Workspace で Run を編集します。完成した Check と準備済みの入力は変更しません。成果は、複数のゴルーチンを起動するだけでなく、動作するプールです。
各チャネルの責任を誰が持つか決める
ゴルーチンは、呼び出し元と並行して実行される関数です。go func() { ... }() は無名関数を直ちに開始します。起動だけでは、HTTP リクエストが活動中だとは証明できません。チャネルは型付きの値を渡します。make(chan Target) は作業チャネル、make(chan Result) は結果チャネルを作ります。これらはバッファなしのチャネルで、jobs <- target は値を送って受信者を待ちます。for target := range jobs は、チャネルが閉じられ、値がすべて取り出されるまで受信します。close(jobs) は、今後値が送られないことを意味します。すでに受信した値は引き続き有効です。
編集前に、プログラムのどの部分が作業の送信、作業の受信、結果の送信、結果の受信、各チャネルのクローズを担当するか決めます。作業の生成側と結果の収集側は、それぞれ一つにします。ワーカーは作業を受信し、結果を送信します。これ以上作業を送らないと分かるのは生成側だけなので、生成側が jobs を閉じます。別のワーカーがまだ送る可能性がある間、ワーカーが安全に results を閉じることはできません。
sync.WaitGroup は終了を追跡します。var finished sync.WaitGroup のような宣言はゼロから始まります。各ワーカーを起動する前に finished.Add(1) を呼び、そのワーカー内の defer finished.Done() で終了を知らせます。finished.Wait() は、登録した全ワーカーが Done を呼ぶまで待ちます。結果チャネルは、その待機が終わってからしか閉じられません。待機とクローズの責任は別のゴルーチンに持たせます。主ゴルーチンが受信前に待つと、ワーカーが結果の送信でブロックしたままになる可能性があるからです。生成側も別に実行し、作業の割り当て中に主ゴルーチンが結果を受信できるようにします。
要求数のワーカーを開始するには、for i := 0; i < workers; i++ { ... } のような回数を数えるループを使います。i をゼロで初期化し、各反復の前に上限を確認し、反復後に i を増やします。本体は毎回一つのワーカーを開始します。各ワーカーは Check を同期的に呼び、その戻り値を送ってから、次の作業を受け取らなければなりません。これで活動中のクライアントチェックをワーカー数以内に抑えられます。ワーカー内で対象ごとに新しいゴルーチンを開始してはいけません。Timeout: budget を持つ一つの http.Client を共有します。このクライアントは並行リクエストに対応しています。レポートのスライスに追加するのは収集側だけなので、ワーカー同士が共有スライスや map で競合しません。完了順が異なることは有効です。
対象一覧が空でも、生成側は jobs を閉じます。ワーカーが終了し、クローズ担当が results を閉じ、収集側は初期化された空スライスを返せます。コードを書く前にこの順序をたどります。対象一つ、ワーカー三つでも終了する理由と、対象が複数、ワーカー一つなら逐次処理になる理由も説明します。
一つのつながった作業ループを編集する
Go が二つ目の Run 定義をコンパイルしないよう、拡張子が .go ではないバックアップを作ります。cp は元を変更せずファイルをコピーします。
cp run.go run.go.bak
既存のファイルを開きます。
nano run.go
矢印キーで移動し、入力してテキストを挿入し、Backspace または Delete で文字を削除します。Ctrl は Control を押しながら、もう一つのキーを押す意味です。Ctrl+W、func Run、Enter で関数を検索し、Ctrl+C で検索をキャンセルします。Ctrl+K は現在の行を切り取るため、その行を削除するつもりのときだけ使います。Ctrl+O で保存し、run.go を確認して Enter、Ctrl+X で終了します。意図しない変更を破棄するには、Ctrl+X を押し、nano が保存するか尋ねたら N を押します。意図しないファイル名のプロンプトでは、Ctrl+C でそのプロンプトをキャンセルし、編集を直すか、元のファイル名で保存できるようにします。
関数シグネチャと既存の HTTP クライアントは残します。WaitGroup を使うときに、インポートグループに sync を追加します。逐次の対象ループを自分の設計に置き換え、次の責任を順に実装します。
まず、型付きの jobs と results チャネル、ワーカー終了用の WaitGroup を作ります。正確に workers 個のワーカーを登録して起動します。各ワーカーは jobs が閉じられるまで受信し、Check の結果をすべて送信し、戻るときに終了を知らせます。
次に、別の生成側を起動し、異なる ID で URL が同じ対象も含め、すべての対象を送ります。対象がなかった場合も含め、最後の送信後に jobs を閉じます。
さらに、全ワーカーの終了を待って results を閉じる、別の終了調整ゴルーチンを起動します。ワーカーがまだ送信できる間は results を閉じてはいけません。
最後に、主ゴルーチンで空の結果スライスを初期化し、results チャネルが閉じられるまで全結果を受信して、一つずつ追加し、そのスライスを返します。
プログラムを実行する前に、四つの部分をすべて書きます。送信側、クローズ担当、収集側のいずれかがない不完全なプールは、有用な中間実験にはなりません。各部分のつながりは自分で決めます。貼り付けるための完成済み Run は用意されていません。ローカル変数名や、同等の同期方法の選択は採点対象ではありません。
保存して終了し、実際のファイルを確認します。cat は変更せずに表示します。
cat run.go
gofmt -w はソースをその場で整形します。構文エラーにはファイルと行が示されます。run.go を開き直し、該当部分を修正して保存し、整形を繰り返します。完成済みの部分を、新しいファイル全体の解答で上書きしてはいけません。
gofmt -w run.go
実際に重なった処理を確認する
完成したプールを、タイムアウトしない割り当てに二つのワーカーで実行します。二秒の予算なら、有限の 250ms の処理が正常に終わります。
go run . -input overlap.json -workers 2 -timeout 2s
すべての割り当てが ok の結果を返すはずです。観測器の既存の記録を読みます。
python3 observe.py
この環境では、累積記録が Actual handler peak: 2 を示し、別のハンドラが活動中に enter が現れるはずです。ピークが一のままなら、ワーカーの受信方法と Check の呼び出し方を調べます。経過時間では成功を証明できません。スケジューラーに異常な遅れがあると、観察が不確定になることもあります。最新の記録が exit と active=0 で終われば、有限のリクエストは実際に終了しています。その後、この無害なバッチを繰り返し、新しいイベントを読んでもかまいません。
累積ピークが二を超える場合、過去のどこかで上限超過があったことを意味します。以前の誤った実装によるピークは、ソースを直した後も残ります。最新の開始・終了イベントを合わせて読み、元のソースを独立した検証環境で実行する Verify で現在の実装を評価します。以前のピークを消すために、履歴を消去したり VM をリセットしたりしてはいけません。
コマンドが戻らず停止したままなら、Ctrl+C でこのフォアグラウンドの呼び出しを中断します。別に準備されたイベントサービスは利用可能なままです。run.go を開き直し、チャネルの責任のつながりを確認します。作業はすべて送られたか、生成側が jobs を閉じたか、ワーカーが Done を呼ぶか、送信中に収集側が受信できるか、クローズ担当が最後に results を閉じるかを調べます。その箇所を修正して整形します。次の並行処理の観察を始める前に、python3 observe.py を実行し、最新の記録イベントが exit で active=0 であることを確認します。有限のリクエストがまだ活動中なら、記録された条件が満たされるまで読み直します。推測で sleep を入れたり、観測記録を削除したりしてはいけません。クライアントのタイムアウトは、サーバーの処理を直ちに止めるものではありません。
ファイルが見つからない場合は、pwd と最初のステップの cd でディレクトリに戻ります。新しいシェルで go が使えなければ、そのステップのセッション用 export を繰り返します。通常の編集やパスの間違いで、グローバルなシェルファイルの変更や VM のリセットは必要ありません。
各作業をその時間予算で完了する
このステップでは、ワーカープールを使ってイベントのレポートを作り、境界条件を試します。実行前に実際の割り当てを読みます。
cat targets.json
期待する結果は 2 件の ok、ステータス 403 の http_error、200 の content_mismatch、0 の timeout、0 の connection_error です。チケットサービスの遅延は有限の 600ms で、200ms のクライアント予算によって意図的に先にチェックが終了します。同じ受付 URL に対応する両方の ID をレポートに残してください。
2 つのワーカーで新しいレポートを保存します。> が置き換えるのは report.json だけです。エラーはターミナルに表示されます。
go run . -input targets.json -workers 2 -timeout 200ms > report.json
実際に保存したファイルを読みます。
cat report.json
行の順番は入力一覧や実行ごとに異なる場合があります。作業 ID で照合してください。遅いリクエストは、後から割り当てた速いリクエストより後に終わることがあります。各ワーカーは Check が戻るまで次の対象を開始しませんが、タイムアウトしたクライアントが離れた後も、サーバーハンドラは有限の処理を続ける場合があります。そのため、コースは別のタイムアウトしないリクエストでワーカー上限を測定します。タイムアウト後に残るハンドラを、プールの不具合とは判定しません。
プールは、ワーカーより少ない作業や、作業が 0 件の場合も処理する必要があります。以下は準備された一覧を使い、結果を直接表示するため、report.json は変更しません。
go run . -input single.json -workers 3 -timeout 2s
go run . -input empty.json -workers 3 -timeout 2s
最初は 1 件の ok、次は [] が表示され、どちらも終了するはずです。最後に、複数のタイムアウトしない対象に上限 1 を要求します。
go run . -input overlap.json -workers 1 -timeout 2s
結果はすべて ok のままです。複数ワーカーを起動できるコードでも、この呼び出しは逐次処理でなければなりません。観測器の累積ピークには以前の活動も残るため、この呼び出しだけの上限は分離できません。コースの検証は、1、2、3 を含め、各ワーカー予算に対して独立した新しいサービスを作ります。複数対象で予算が 2 または 3 の場合、要求上限を超えず、少なくとも 2 件の実際の重なりを求めます。ピークが正確に 3 である必要はありません。空のセットや単一対象には重なりの条件はありません。
完全なイベントレポートと、元の入力、サービスを残してください。これで、活動中の処理を制限しながら、正直な失敗も含めてすべての結果を返す、再利用可能なコマンドができました。
まとめ
クローズの責任を明確にした作業・結果チャネル、固定数のワーカー、終了同期を使いました。単一の収集者が共有スライスの競合を避け、各作業の結果を保持します。実際のサーバーイベントで、逐次処理、上限のある並行処理、過剰な処理を区別し、独立したタイムアウト検証によって、残存するサーバー処理をワーカー上限違反と誤認しないようにしました。一部の入口が失敗しても、保存したレポートは役立ちます。



