Go言語マイクロサービス性能ベンチマークの設計と実装
Go言語で作られたマイクロサービスは、軽量な実行ファイル、並行処理、標準試験機能を活かしやすく、性能重視のサービスにもよく使われます。しかし、単体の関数が速いことと、マイクロサービス全体が本番負荷に耐えられることは別問題です。実際の性能は、ネットワーク、データベース、外部サービス、コンテナ資源、通信待ち、ガベージコレクション、ロック競合、配備環境によって大きく変わります。
Goには標準の testing パッケージがあり、go test と組み合わせて試験やベンチマークを実行できます。また、Go 1.24では、従来の b.N ループより意図を表しやすい testing.B.Loop が導入されています。これはベンチマークコードの書き方をより堅牢にするための機能としてGo公式ブログで説明されています。
この記事では、Go言語マイクロサービスの性能ベンチマークを、実務で使える形で整理します。関数単位のベンチマーク、HTTP負荷試験、サービス間通信、プロファイリング、分散追跡、資源制限、継続的性能検証までを含め、単なる数値測定ではなく、改善につながるベンチマーク設計を目指します。
1. Go言語マイクロサービス性能ベンチマークとは
Go言語マイクロサービス性能ベンチマークとは、Goで実装された個別サービスやサービス群に対して、応答時間、処理量、資源使用量、安定性、失敗率を測定し、改善判断に使える数値として整理する取り組みです。単に「速いか遅いか」を見るのではなく、どの条件で、どの負荷に対して、どの部分が限界になっているかを明らかにします。
マイクロサービスでは、ひとつの処理が複数サービスをまたぐことが多くなります。そのため、単一プロセスのベンチマークだけでは不十分で、関数、API、サービス間通信、データベース、キュー、コンテナ資源、クラスター配備後の挙動まで段階的に測定します。
1.1 性能ベンチマークの目的
性能ベンチマークの目的は、現在の性能を数値化し、変更によって改善したのか悪化したのかを判断できるようにすることです。Go言語のマイクロサービスでは、関数単位の処理時間だけでなく、HTTP応答時間、同時接続数、メモリ使用量、CPU使用率、待ち時間の分布も重要です。
また、性能ベンチマークは障害予防にも役立ちます。本番で利用者が増えてから限界を知るのではなく、事前にどの負荷で遅延が増え、どの資源が詰まるのかを把握しておくことで、配備前に改善できます。
1.2 Go言語で測りやすい領域
Go言語は標準の testing パッケージでベンチマーク関数を書けるため、関数や内部処理の性能を測りやすいです。go test と組み合わせることで、通常の試験と同じ流れでベンチマークを実行できます。
一方で、マイクロサービスの性能はアプリケーション内部だけで決まりません。HTTP通信、JSON処理、データベース接続、外部API待ち、コンテナ資源制限なども含めて測る必要があります。
| 測定対象 | Go言語での測定方法 | 確認できる内容 |
|---|---|---|
| 関数単位の処理 | testing.B を使ったベンチマーク | 実行時間、処理回数、メモリ割り当て |
| JSON処理 | エンコード・デコード処理のベンチマーク | JSON変換にかかる時間、メモリ使用量 |
| HTTPハンドラ | httptest やベンチマーク関数 | リクエスト処理時間、レスポンス生成速度 |
| データベース処理 | DBアクセス処理のベンチマーク | クエリ実行時間、接続処理の影響 |
| 外部API連携 | モックサーバーや統合テスト | API待ち時間、通信遅延の影響 |
| コンテナ環境 | Docker上での負荷試験 | CPU・メモリ制限による性能変化 |
1.3 マイクロサービスで測定が難しくなる理由
マイクロサービスでは、ひとつの利用者操作が複数のサービスを通過します。注文サービス、決済サービス、在庫サービス、通知サービスのように分かれている場合、遅延の原因がどこにあるのかを単純な平均値だけで判断できません。
さらに、ネットワーク遅延、再試行、タイムアウト、キュー滞留、データベース接続枯渇が重なると、性能問題は連鎖します。そのため、個別サービスと全体経路の両方を測定する必要があります。
1.4 測定対象を分ける重要性
性能ベンチマークでは、測定対象を小さく分けることが重要です。関数単位、HTTP処理単位、サービス単位、利用者経路単位に分けることで、問題箇所を絞り込みやすくなります。
最初から全体負荷試験だけを行うと、遅いことは分かっても原因が分かりません。まず小さく測り、次に実際の通信経路で測る流れが効果的です。
1.5 判断できる数値にする
ベンチマーク結果は、単にログとして残すだけでは意味が弱くなります。過去の結果と比較できるように、測定条件、版、コミット、実行環境、負荷条件を一緒に保存する必要があります。
特にGo言語マイクロサービスでは、コンパイラ版、コンテナ画像、CPU制限、メモリ制限、依存サービスの状態によって数値が変わります。数値だけでなく、条件もセットで管理します。
2. 測定指標
Go言語マイクロサービスの性能を正しく見るには、どの指標を測るかを先に決める必要があります。平均応答時間だけを見ていると、一部の利用者が非常に遅い体験をしていても見逃す可能性があります。
性能ベンチマークでは、応答時間、処理量、失敗率、資源使用量、待ち行列、ガベージコレクション、接続数を組み合わせて判断します。ひとつの数値に頼らず、複数の指標から原因を読み解くことが大切です。
2.1 応答時間
応答時間は、要求を送ってから応答を受け取るまでの時間です。マイクロサービスでは、利用者に近いAPIの応答時間だけでなく、内部サービス間の応答時間も測定します。
平均値だけでは、遅い要求が隠れることがあります。そのため、中央値、95パーセンタイル、99パーセンタイルを併せて見ると、利用者体験に近い判断ができます。
| 指標 | 意味 | 確認する理由 |
|---|---|---|
| 平均値 | 全要求の応答時間の平均 | 全体的な傾向を把握するため |
| 中央値 | 半分の要求がこの時間以内に終わる値 | 一般的な応答時間を確認するため |
| 95パーセンタイル | 95%の要求がこの時間以内に終わる値 | 遅い要求の影響を確認するため |
| 99パーセンタイル | 99%の要求がこの時間以内に終わる値 | 利用者体験を悪化させる極端な遅延を確認するため |
応答時間は平均値だけで判断せず、中央値やパーセンタイルと合わせて確認することが重要です。特にマイクロサービスでは、一部の遅い処理が全体の利用者体験に影響するため、遅延のばらつきを見る必要があります。
2.2 処理量
処理量は、一定時間あたりに処理できる要求数です。秒間要求数や秒間処理件数として表すことが多く、サービスがどれだけの負荷に耐えられるかを見る指標になります。
ただし、処理量だけが高くても、応答時間が極端に長ければ良い性能とは言えません。処理量と応答時間は必ず一緒に見ます。
| 指標 | 表し方 | 確認できる内容 |
| RPS | Requests Per Second | 1秒あたりに処理できるリクエスト数 |
| TPS | Transactions Per Second | 1秒あたりに処理できる業務処理数 |
| スループット | 一定時間あたりの処理量 | サービス全体の処理能力 |
| 最大処理量 | 限界時の処理量 | 負荷に対する耐性 |
処理量はサービスの処理能力を確認するための基本的な指標です。しかし、処理量だけで性能を評価するのではなく、応答時間や失敗率と組み合わせて、安定して処理できているかを判断する必要があります。
2.3 失敗率
失敗率は、要求全体のうち失敗した割合です。性能試験では、負荷が上がると応答時間だけでなく、タイムアウト、接続失敗、内部エラーも増えることがあります。
マイクロサービスでは、一つのサービスの失敗が別サービスの失敗として見える場合があります。どの層で失敗したのかを記録し、利用者向けエラーと内部エラーを分けて確認します。
| 失敗の種類 | 例 | 確認する内容 |
| タイムアウト | 応答が制限時間内に返らない | 処理遅延や外部サービス待ちの有無 |
| 接続失敗 | DBや外部APIに接続できない | ネットワークや接続数の問題 |
| 内部エラー | 500エラーなど | アプリケーション内部の異常 |
| 利用者向けエラー | 400系エラーなど | 入力値やAPI利用方法の問題 |
失敗率を見ることで、負荷が高くなったときにサービスがどの程度安定して動作できるかを確認できます。特にマイクロサービスでは、失敗が連鎖する可能性があるため、エラーの発生場所と種類を分けて記録することが大切です。
2.4 資源使用量
資源使用量には、CPU、メモリ、ネットワーク、ディスク、接続数などがあります。Kubernetesでは、コンテナにCPUやメモリの要求値と上限値を設定でき、要求値はスケジューリング、上限値は実行時の制限に関係します。
Go言語サービスでは、メモリ割り当て、ガベージコレクション、ゴルーチン数、接続プールも重要です。CPU使用率だけでなく、待ちや競合がないかを確認します。
| 資源 | 確認する内容 | Go言語サービスでの注意点 |
| CPU | 使用率、処理待ち | 高負荷時に処理が詰まっていないか |
| メモリ | 使用量、割り当て量 | 不要なメモリ割り当てが増えていないか |
| GC | ガベージコレクション回数、停止時間 | GCによる遅延が発生していないか |
| ゴルーチン | ゴルーチン数 | ゴルーチンリークがないか |
| 接続数 | DB接続、HTTP接続 | 接続プールが不足していないか |
| ネットワーク | 通信量、遅延 | サービス間通信がボトルネックになっていないか |
資源使用量を確認することで、性能低下の原因がアプリケーション処理、メモリ使用、通信、接続数などのどこにあるかを把握できます。Go言語のマイクロサービスでは、CPUやメモリだけでなく、GCやゴルーチン、接続プールも重要な確認対象になります。
2.5 分布を見る
性能測定では、単一の平均値ではなく分布を見ることが重要です。Prometheusのヒストグラムは観測値をバケットに分けて扱うため、応答時間の分布やパーセンタイル推定に利用できます。
分布を見ることで、ほとんどの要求は速いが一部だけ極端に遅い、という状況を把握できます。マイクロサービスではこの「一部だけ遅い」問題が利用者体験を大きく悪化させることがあります。
| 見る項目 | 内容 | 分かること |
| ヒストグラム | 応答時間を範囲ごとに集計する | 遅延の分布 |
| パーセンタイル | 95%や99%の要求が収まる時間 | 遅い要求の影響 |
| 外れ値 | 極端に遅い要求 | 一部の異常な処理 |
| バケット | 観測値を分ける範囲 | どの時間帯に要求が集中しているか |
分布を見ることで、平均値だけでは分からない遅延の偏りや外れ値を確認できます。マイクロサービスの性能評価では、少数の遅い要求が全体の品質に影響するため、分布を使って性能を判断することが重要です。
3. Go標準ベンチマーク
Go言語では、標準の testing パッケージを使ってベンチマークを書けます。これは関数や内部処理の性能を測るときに非常に便利です。外部の負荷試験に進む前に、まず重要な処理単位を小さく測ることで、改善の方向が分かりやすくなります。
Go標準ベンチマークは、HTTPサーバー全体ではなく、JSON変換、署名検証、データ整形、価格計算、権限判定のような内部処理を測るのに向いています。
3.1 Benchmark関数
Goのベンチマーク関数は、BenchmarkXxx という名前で作成します。testing パッケージは通常の試験だけでなく、ベンチマークを実行するための仕組みも提供しています。
マイクロサービスでは、API処理の中で何度も呼ばれる関数を対象にすると効果的です。利用頻度の低い処理より、要求ごとに必ず通る処理を先に測るべきです。
| 測定対象 | 例 | 測定する理由 |
|---|---|---|
| 入力値の検証処理 | リクエストパラメータのチェック | すべての要求で実行されるため |
| JSON変換処理 | json.Marshal、json.Unmarshal | API通信で頻繁に利用されるため |
| 業務ロジック | 計算処理、判定処理 | サービスの中心処理になるため |
| データ整形処理 | レスポンス用の構造体作成 | 応答時間に影響するため |
| 共通関数 | 認証、ログ出力、ID生成など | 複数のAPIで繰り返し使われるため |
Benchmark関数は、マイクロサービスの中で繰り返し実行される処理の性能を確認するために有効です。特に、リクエストごとに必ず通る処理を先に測定することで、サービス全体の応答時間を改善しやすくなります。
3.2 b.Nによる繰り返し
従来のGoベンチマークでは、b.N を使って処理を繰り返します。Goの試験実行側が適切な繰り返し回数を調整し、実行時間を測定します。
ただし、初期化処理を測定対象に含めてしまうと、本来見たい処理時間が歪みます。測定対象のループ内に何を入れるかを慎重に決めます。
3.3 testing.B.Loop
Go 1.24では testing.B.Loop が導入され、従来の b.N 形式よりも意図を明確にしやすくなりました。Go公式ブログでは、testing.B.Loop はより予測しやすいベンチマークを書くための新しい方法として紹介されています。
新しいGo版を使える環境であれば、testing.B.Loop を使った書き方も検討できます。ただし、チームのGo版が古い場合は、従来の b.N 形式との互換性を確認します。
3.4 メモリ割り当ての確認
Goのベンチマークでは、メモリ割り当て回数や割り当て量も確認できます。マイクロサービスでは、要求ごとの割り当てが多いとガベージコレクションの負荷が増え、応答時間の揺れにつながります。
-benchmem を使うと、処理ごとの割り当て情報を確認できます。特にJSON処理、文字列連結、構造体変換、ログ出力周辺で有効です。
3.5 Goベンチマーク例
以下は、注文データを応答用の構造へ変換する処理を測る例です。実務では、実際のデータ量に近い入力を使い、空の小さなデータだけで測らないことが重要です。
Goベンチマーク例
package order
import "testing"
type Order struct { ID string UserID string Amount int64 Currency string Status string}
type OrderResponse struct { ID string `json:"id"` AmountText string `json:"amountText"` StatusLabel string `json:"statusLabel"`}
func BuildOrderResponse(o Order) OrderResponse { return OrderResponse{ ID: o.ID, AmountText: o.Currency + " " + formatAmount(o.Amount), StatusLabel: o.Status, }}
func formatAmount(v int64) string { return "1000"}
func BenchmarkBuildOrderResponse(b *testing.B) { order := Order{ ID: "ord_001", UserID: "user_001", Amount: 1000, Currency: "JPY", Status: "paid", }
b.ReportAllocs()
for i := 0; i < b.N; i++ { _ = BuildOrderResponse(order) }}
4. HTTP負荷試験
Go言語マイクロサービスでは、関数単位のベンチマークだけでなく、HTTP APIとしての性能も測る必要があります。実際の利用者は関数を直接呼ぶのではなく、ネットワーク越しにAPIへ要求を送るからです。
HTTP負荷試験では、応答時間、秒間要求数、失敗率、同時利用者数、タイムアウト、接続再利用の影響を確認します。k6の公式文書は、k6を負荷試験と性能試験のための道具として説明しています。
4.1 API単位で測る理由
マイクロサービスの外部境界はAPIです。内部処理が速くても、HTTPハンドラー、認証、JSON変換、データベース接続で遅くなることがあります。
API単位で測ることで、利用者に近い性能を確認できます。特に公開API、内部の高頻度API、他サービスから呼ばれるAPIは優先して測定します。
4.2 負荷シナリオ
負荷シナリオでは、どのAPIを、どの割合で、どれくらいの同時数で呼ぶかを決めます。単一APIだけを大量に叩く試験と、実際の利用者行動を再現する試験は目的が異なります。
マイクロサービスでは、実際の業務流れに近いシナリオが重要です。注文作成、決済、在庫確認、通知のような一連の流れを組み合わせると、サービス間の影響を測りやすくなります。
4.3 ウォームアップ
Go言語サービスでは、起動直後としばらく稼働した後で性能が異なることがあります。接続プール、キャッシュ、ジャストインタイムではないものの初回処理、データ読み込みなどの影響があるためです。
そのため、測定前にウォームアップを入れると、より実運用に近い数値になります。ウォームアップ時間と本測定時間は分けて記録します。
4.4 タイムアウト設定
負荷試験では、クライアント側のタイムアウトを明示します。タイムアウトが長すぎると、遅い処理が大量に残り、結果が読みづらくなります。
マイクロサービスでは、呼び出し元と呼び出し先のタイムアウト設計が重要です。負荷試験でも本番に近いタイムアウトを使うことで、実際の障害挙動を確認できます。
4.5 k6負荷試験例
以下は、注文APIへ段階的に負荷をかけるk6の例です。しきい値を設定して、応答時間や失敗率が基準を超えた場合に失敗扱いにします。
k6スクリプト例
import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { stages: [ { duration: '1m', target: 20 }, { duration: '3m', target: 100 }, { duration: '1m', target: 0 }, ], thresholds: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<300'], },};
export default function () { const payload = JSON.stringify({ userId: 'user_001', itemId: 'item_001', quantity: 1, });
const params = { headers: { 'Content-Type': 'application/json', }, timeout: '2s', };
const res = http.post('http://localhost:8080/orders', payload, params);
check(res, { 'status is 201': (r) => r.status === 201, });
sleep(1);}
5. ベンチマーク環境
性能ベンチマークでは、コードだけでなく環境も測定結果に大きく影響します。同じGo言語サービスでも、CPU、メモリ、コンテナ制限、ネットワーク、データベース、依存サービスの配置によって数値は変わります。
そのため、ベンチマーク環境はできるだけ再現可能にし、測定ごとの差分を記録する必要があります。環境が不安定なまま数値を比較すると、改善したのか偶然なのか判断できません。
5.1 ローカル環境
ローカル環境は、開発者が素早く測るには便利です。関数単位のGoベンチマークや小さなHTTP試験であれば、ローカルでも十分に役立ちます。
ただし、ローカル環境は他のアプリケーションや端末状態の影響を受けやすいです。本番に近い判断をする場合は、専用の検証環境で測る必要があります。
ローカル環境で確認しやすい内容は、次のとおりです。
- 関数単位の処理時間
- メモリ割り当て回数
- 小規模なHTTPリクエスト処理
- 実装変更前後の相対的な性能差
- 開発中の簡単な性能確認
ローカル環境は素早く性能を確認するために有効です。しかし、本番環境とは条件が異なるため、最終的な性能判断には向いていません。
5.2 継続的統合環境
継続的統合環境でベンチマークを実行すると、変更ごとの性能劣化を検出しやすくなります。短時間で終わるベンチマークを品質門に入れると、性能退行を早く見つけられます。
ただし、共有実行環境では隣の処理の影響を受けることがあります。厳密な数値比較には、専用実行環境や繰り返し測定が必要です。
継続的統合環境で確認する内容は、次のとおりです。
- コード変更による性能劣化
- ベンチマーク結果の継続的な記録
- 主要な関数や処理の性能変化
- 短時間で実行できる性能試験
- 性能退行を早期に検出する仕組み
継続的統合環境は、性能劣化を早く見つけるために有効です。ただし、実行環境が安定しない場合もあるため、細かい数値よりも大きな性能変化を確認する目的で使うべきです。
5.3 検証環境
検証環境では、本番に近い構成で負荷試験を行います。コンテナ数、データベース、キャッシュ、メッセージキュー、外部サービスの模擬をできるだけ本番に近づけます。
本番と完全に同じ負荷を再現できない場合でも、相対比較には役立ちます。重要なのは、毎回同じ条件で測ることです。
検証環境で確認する内容は、次のとおりです。
- 本番に近い構成での応答時間
- 高負荷時の処理量
- エラーやタイムアウトの発生
- CPU、メモリ、ネットワークなどの資源使用量
- データベースや外部サービスを含めた全体性能
- 設定変更前後の性能差
検証環境は本番に近い条件で性能を確認するために重要です。ローカル環境や継続的統合環境よりも現実に近い結果を得られるため、リリース前の性能判断に適しています。
5.4 本番影響を避ける
本番環境で直接大きな負荷試験を行うのは危険です。利用者への影響、外部サービス費用、データ汚染、監視アラートの誤発報が起こる可能性があります。
本番で測る場合は、低リスクな読み取りAPI、限定的なカナリア通信、影響範囲の小さい時間帯などに制限します。通常は、検証環境で基準を作り、本番では監視値と照合します。
5.5 環境記録例
測定時には、実行環境の情報を結果と一緒に保存します。Go版、コミット、画像タグ、CPU制限、メモリ制限、Pod数、データ量を残すと、後から比較しやすくなります。
環境記録例
benchmark: service: order commit: 8f3a21c go_version: go1.24 image: registry.example.com/order:8f3a21c replicas: 3 cpu_request: 500m cpu_limit: "1" memory_request: 256Mi memory_limit: 512Mi database_rows: 1000000 test_duration: 10m
6. ベンチマーク方式の違い
Go言語マイクロサービスの性能測定には、複数の方式があります。関数ベンチマーク、HTTP負荷試験、サービス間試験、プロファイリング、分散追跡は、それぞれ見えるものが違います。
違いを理解せずにひとつの方式だけに頼ると、見たい問題を見逃します。速い関数でもAPI全体は遅いことがあり、APIが速くてもサービス間連携で遅くなることがあります。
6.1 関数ベンチマーク
関数ベンチマークは、Go内部の特定処理を測る方式です。JSON変換、署名検証、価格計算、データ整形のような処理に向いています。
この方式は速く実行でき、原因を絞り込みやすい一方で、ネットワークや外部依存の影響は分かりません。局所的な改善に使う方式です。
6.2 HTTP負荷試験
HTTP負荷試験は、APIとしての応答時間や処理量を見る方式です。利用者や他サービスから見える性能に近いため、実運用に近い判断ができます。
ただし、HTTP負荷試験だけでは、内部のどこが遅いかまでは分かりにくいです。遅延の原因を特定するには、プロファイリングや追跡と組み合わせます。
6.3 サービス間ベンチマーク
サービス間ベンチマークは、複数サービスを接続して処理経路全体を見る方式です。注文から決済、在庫、通知までのように、実際の業務処理を測ります。
この方式は現実に近い一方で、結果が複雑になります。どのサービスが遅いのかを見分けるために、分散追跡やサービス別指標が必要です。
6.4 プロファイリング
プロファイリングは、CPU、メモリ、ゴルーチン、ロック競合など、Goプロセス内部の消費箇所を見る方式です。Go公式診断文書では、Goランタイムがpprof形式のプロファイリングデータを提供し、go test や net/http/pprof 経由で収集できると説明されています。
負荷試験で遅いことが分かった後、プロファイリングでどこに時間やメモリが使われているかを確認します。測定と原因分析をつなぐ重要な方法です。
6.5 比較表
以下は、代表的なベンチマーク方式の違いです。実務では、ひとつを選ぶのではなく、目的に応じて組み合わせます。
| 方式 | 主な対象 | 分かること | 苦手なこと | 実行頻度 |
|---|---|---|---|---|
| 関数ベンチマーク | Go内部処理 | 処理時間、割り当て量 | 通信や外部依存 | 毎回または定期 |
| HTTP負荷試験 | API | 応答時間、処理量、失敗率 | 内部原因の特定 | 統合前、配備前 |
| サービス間試験 | 業務経路 | 連携時の遅延、ボトルネック | 準備と調査が重い | 主要分岐、定期 |
| プロファイリング | Goプロセス | CPU、メモリ、競合 | 利用者経路全体 | 問題調査時 |
| 分散追跡 | 複数サービス | 経路ごとの遅延分布 | 導入設計が必要 | 常時または負荷試験時 |
7. pprofによる原因分析
Go言語マイクロサービスの性能問題を深掘りする場合、pprof が非常に役立ちます。負荷試験で「遅い」と分かった後、どの関数でCPUを使っているのか、どこでメモリを割り当てているのかを確認できます。
Go公式ブログでは、go tool pprof がプロファイルを分析し、上位のサンプルを表示する topN のようなコマンドを使えることが説明されています。
7.1 CPUプロファイル
CPUプロファイルは、実行中にどの関数がCPU時間を使っているかを見るための情報です。負荷試験中にCPUプロファイルを取得すると、実際の高負荷時に重い処理を見つけやすくなります。
マイクロサービスでは、JSON処理、暗号処理、ログ整形、テンプレート生成、不要なループがCPUを消費していることがあります。推測ではなく、プロファイルを見て判断します。
CPUプロファイルで確認する主な内容は、次のとおりです。
- CPU時間を多く使っている関数
- 高負荷時に実行回数が多い処理
- 不要なループや重複処理
- JSON変換やログ整形などの重い処理
- 改善前後のCPU使用時間の変化
| 確認項目 | 例 | 分かること |
|---|---|---|
| CPU時間の多い関数 | JSON処理、暗号処理 | どの関数が重いか |
| 呼び出し回数 | 共通関数、ログ処理 | 頻繁に実行される処理 |
| 不要な処理 | 重複ループ、無駄な変換 | 削減できる処理 |
| 改善前後の比較 | 修正前後のプロファイル | 最適化の効果 |
CPUプロファイルを使うことで、CPUを多く使っている処理を具体的に確認できます。性能改善では推測で修正するのではなく、実際のプロファイル結果をもとにボトルネックを判断することが重要です。
7.2 メモリプロファイル
メモリプロファイルは、どこでメモリが割り当てられているかを見るための情報です。要求ごとの割り当てが多いと、ガベージコレクションの回数や停止時間が増え、応答時間に影響することがあります。
Goサービスでは、文字列結合、JSON変換、大きなスライス、ログ用の構造生成が割り当て増加の原因になることがあります。-benchmem と組み合わせると、局所と実運用の両方で確認できます。
メモリプロファイルで確認する主な内容は、次のとおりです。
- メモリ割り当てが多い関数
- 要求ごとの割り当て回数
- 大きなオブジェクトやスライスの生成
- ガベージコレクションへの影響
-benchmemによる局所的な割り当て量
| 確認項目 | 例 | 分かること |
|---|---|---|
| 割り当て量 | 大きなスライス、構造体生成 | メモリを多く使う処理 |
| 割り当て回数 | 文字列結合、JSON変換 | GCが増える原因 |
| GCの影響 | 停止時間、GC回数 | 応答時間への影響 |
-benchmem | B/op、allocs/op | 関数単位のメモリ効率 |
メモリプロファイルはメモリ割り当ての多い処理を見つけるために有効です。Goのマイクロサービスでは、メモリ割り当てが増えるとGCの影響で応答時間が悪化するため、CPUだけでなくメモリの使い方も確認する必要があります。
7.3 ゴルーチンの確認
ゴルーチンプロファイルを確認すると、ゴルーチンが大量に残っていないか、どこで待っているかを調べられます。マイクロサービスでは、タイムアウト忘れやチャネル待ちでゴルーチンが増え続けることがあります。
負荷試験中にゴルーチン数が増え続ける場合、処理完了後も残る漏れを疑います。これは短時間の単体試験では見つけにくい問題です。
ゴルーチン確認で見る主な内容は、次のとおりです。
- ゴルーチン数が増え続けていないか
- チャネル待ちが発生していないか
- 外部APIやDB接続で待ち続けていないか
- タイムアウトやキャンセル処理が設定されているか
- 負荷終了後にゴルーチン数が戻るか
| 確認項目 | 例 | 分かること |
|---|---|---|
| ゴルーチン数 | 負荷中・負荷後の数 | ゴルーチンリークの有無 |
| 待ち状態 | チャネル待ち、ロック待ち | 処理が止まっている場所 |
| タイムアウト | HTTP、DB、外部API | 待ち続ける処理の有無 |
| キャンセル処理 | context.Context | 処理終了時に正しく止まるか |
ゴルーチンの確認は、短時間の試験では見つけにくい待ちやリークを発見するために重要です。特にマイクロサービスでは外部通信が多いため、タイムアウトやキャンセル処理を含めて確認する必要があります。
7.4 ロック競合
高負荷時に応答時間が伸びる原因として、ロック競合があります。共有マップ、共通キャッシュ、単一の接続管理、グローバルな状態を守るミューテックスが詰まりになることがあります。
ロック競合はCPU使用率だけでは分かりにくい場合があります。プロファイルや実行追跡を使い、待ち時間がどこで発生しているかを確認します。
ロック競合で確認する主な内容は、次のとおりです。
- ミューテックスで待っている時間
- 共有データへの同時アクセス
- 単一のキャッシュや接続管理への集中
- 高負荷時だけ発生する待ち時間
- ロック範囲が広すぎないか
| 確認項目 | 例 | 分かること |
|---|---|---|
| ロック待ち時間 | Mutex、RWMutex | 競合が発生している場所 |
| 共有データ | 共有マップ、共通キャッシュ | 同時アクセスの集中 |
| ロック範囲 | 大きな処理をロック内で実行 | 待ち時間が長くなる原因 |
| 実行追跡 | Go trace | CPU以外の待ち時間 |
ロック競合は高負荷時の応答時間悪化の原因になります。CPU使用率だけでは判断しにくいため、プロファイルや実行追跡を使って、どこで待ちが発生しているかを確認することが重要です。
7.5 pprof導入例
以下は、GoのHTTPサービスにpprofを導入する簡単な例です。本番で公開する場合は、認証やネットワーク制限を必ず設けてください。
pprof導入例
package main
import ( "log" "net/http" _ "net/http/pprof")
func main() { go func() { log.Println("pprof server started at :6060") if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil { log.Fatal(err) } }()
mux := http.NewServeMux() mux.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte(`{"status":"created"}`)) })
log.Fatal(http.ListenAndServe(":8080", mux))}
8. 実行追跡
Goの実行追跡は、ゴルーチンの生成、ブロック、システムコール、ガベージコレクション、プロセッサの動きなど、実行時の細かい出来事を見るために使えます。runtime/trace の公式文書では、ゴルーチン作成、ブロック、解除、システムコール、GC関連イベントなど幅広い実行イベントを取得でき、go tool trace で解釈できると説明されています。
マイクロサービスの性能問題では、単にCPUが高いだけでなく、待ち、ブロック、スケジューリング、GCが絡むことがあります。実行追跡はそのような問題の理解に役立ちます。
8.1 traceで分かること
traceでは、ゴルーチンがいつ動き、いつ待ち、どのタイミングでブロックしたかを確認できます。CPUプロファイルだけでは見えない待ち時間の原因を調べるときに有効です。
たとえば、HTTP要求が増えたときにゴルーチンは増えているのに処理が進まない場合、ロック、チャネル、システムコール、GCの影響を疑えます。
8.2 負荷試験と合わせる
実行追跡は、負荷がかかっている状態で取得すると意味が出ます。通常時には問題が見えなくても、高負荷時だけスケジューリングや待ちが悪化することがあるからです。
ただし、追跡は情報量が多く、長時間取りすぎると扱いづらくなります。問題が出る短い時間帯に絞って取得します。
8.3 ゴルーチン待ちの分析
Go言語マイクロサービスでは、外部API待ち、データベース待ち、チャネル待ち、ロック待ちが遅延の原因になることがあります。traceを使うと、実際に待っている場所を把握しやすくなります。
特に、同時要求が増えたときだけ遅くなる問題では、ゴルーチンが増えすぎているのか、共有資源で詰まっているのかを見分ける必要があります。
8.4 GCとの関係
Goのガベージコレクションは通常自動で動きますが、割り当てが多いサービスでは応答時間の揺れに影響することがあります。traceではGC関連イベントも確認できるため、高負荷時の挙動を把握できます。
メモリ割り当てを減らす改善は、CPU削減だけでなく、応答時間の安定化にもつながる場合があります。数値改善だけでなく、分布の改善も確認します。
8.5 trace取得例
以下は、短時間だけtraceを取得する例です。実務では、負荷試験と同じタイミングで取得し、結果を保存します。
trace取得例
package main
import ( "os" "runtime/trace" "time")
func main() { f, err := os.Create("trace.out") if err != nil { panic(err) } defer f.Close()
if err := trace.Start(f); err != nil { panic(err) } defer trace.Stop()
runWorkload()
time.Sleep(5 * time.Second)}
func runWorkload() { for i := 0; i < 1000; i++ { go func() { time.Sleep(10 * time.Millisecond) }() }}
9. 分散追跡
マイクロサービスでは、ひとつの要求が複数サービスを通過します。分散追跡を使うと、要求がどのサービスを通り、それぞれでどれだけ時間を使ったかを確認できます。
OpenTelemetryのGo文書では、Goアプリケーションからメトリクス、ログ、トレースなどのテレメトリデータを生成・収集するためのOpenTelemetry APIとSDKについて説明されています。
9.1 分散追跡が必要な理由
HTTP負荷試験で全体の応答時間が遅いと分かっても、どのサービスが遅いのかは分かりません。分散追跡があれば、注文サービスで遅いのか、決済サービスで遅いのか、在庫サービスで待っているのかを見られます。
マイクロサービスでは、単一サービスのログだけを見ても全体像が分かりにくいです。要求単位で流れをつなげる仕組みが必要です。
9.2 トレース識別子
分散追跡では、要求ごとに識別子を持たせ、サービス間で引き継ぎます。この識別子がないと、複数サービスのログや計測結果を同じ要求として結び付けられません。
ベンチマーク時にもトレース識別子を付けると、遅い要求だけを後から抽出できます。平均ではなく、遅延の大きい個別要求を調査できる点が重要です。
9.3 スパン設計
スパンは、処理のひとまとまりを表す単位です。HTTP受信、データベース問い合わせ、外部API呼び出し、メッセージ送信などをスパンとして記録します。
細かくしすぎると情報量が多くなり、粗すぎると原因が分かりません。ベンチマークでは、遅延原因になりやすい境界を中心に記録します。
9.4 性能への影響
計測を入れると、わずかでも処理に影響が出る可能性があります。負荷試験では、計測ありと計測なしの差も確認しておくと安心です。
ただし、計測を完全に外すと本番に近い観測ができません。重要なのは、運用時と同じ条件で測り、計測自体の負荷も理解することです。
9.5 OpenTelemetry計装例
以下は、HTTPハンドラー内で簡単にスパンを作る例です。実務では自動計装や共通ミドルウェアを使い、サービス全体で一貫した命名規則にします。
OpenTelemetry計装例
package main
import ( "context" "net/http"
"go.opentelemetry.io/otel")
var tracer = otel.Tracer("order-service")
func createOrderHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context()
ctx, span := tracer.Start(ctx, "CreateOrder") defer span.End()
if err := saveOrder(ctx); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
w.WriteHeader(http.StatusCreated)}
func saveOrder(ctx context.Context) error { _, span := tracer.Start(ctx, "SaveOrder") defer span.End()
return nil}
10. データベース性能
Go言語マイクロサービスの性能問題では、データベースがボトルネックになることが多くあります。アプリケーションコードが速くても、問い合わせ、接続プール、トランザクション、索引、ロックで遅くなることがあります。
データベース性能は、単体のGoベンチマークだけでは判断できません。実際のデータ量、接続数、並行要求、問い合わせ計画、ロック状態を含めて測定する必要があります。
10.1 接続プール
Goのデータベース利用では、接続プールの設定が性能に影響します。接続数が少なすぎると待ちが増え、多すぎるとデータベース側の負荷が増えます。
マイクロサービスでは、サービス数が増えるほど全体の接続数も増えます。各サービス単体では問題なくても、全体ではデータベース接続が枯渇することがあります。
接続プールで確認する主な内容は、次のとおりです。
- 最大接続数が適切か
- アイドル接続数が多すぎないか
- 接続待ち時間が発生していないか
- サービス全体での接続数が増えすぎていないか
- データベース側の接続上限に近づいていないか
| 確認項目 | 例 | 分かること |
|---|---|---|
| 最大接続数 | SetMaxOpenConns | 同時に使える接続数 |
| アイドル接続数 | SetMaxIdleConns | 待機中の接続数 |
| 接続待ち | 接続取得までの時間 | 接続不足の有無 |
| DB側の上限 | 最大接続数、接続使用率 | 全体の接続枯渇リスク |
| サービス数 | 複数マイクロサービス | システム全体の接続増加 |
接続プールはGoサービス単体だけでなく、システム全体で確認する必要があります。接続数が少なすぎると待ちが増え、多すぎるとデータベースに負荷が集中するため、負荷試験を通して適切な値を決めることが重要です。
10.2 問い合わせ時間
API応答時間の大部分がデータベース問い合わせに使われることがあります。ベンチマークでは、HTTP全体の時間だけでなく、問い合わせごとの時間も記録します。
遅い問い合わせは、索引不足、不要な結合、大量データ取得、N+1問い合わせなどが原因になることがあります。負荷試験中の実データに近い状態で確認します。
問い合わせ時間で確認する主な内容は、次のとおりです。
- 問い合わせごとの実行時間
- 遅いSQLが発生していないか
- 索引が適切に使われているか
- 不要な結合や大量取得がないか
- N+1問い合わせが発生していないか
| 確認項目 | 例 | 分かること |
|---|---|---|
| 問い合わせ時間 | SQLごとの実行時間 | 遅い処理の特定 |
| 索引 | WHERE句、ORDER BY | 検索効率の問題 |
| 結合 | JOINの数、結合条件 | 不要な処理の有無 |
| 取得件数 | 大量データ取得 | メモリや通信量への影響 |
| N+1問い合わせ | ループ内SQL | 問い合わせ回数の増加 |
問い合わせ時間を記録することで、API全体の遅延がどのSQLから発生しているかを確認できます。HTTP全体の応答時間だけでは原因が分かりにくいため、データベース問い合わせごとの時間を分けて測定することが大切です。
10.3 トランザクション
トランザクションは整合性を守るために重要ですが、長すぎるとロックや待ちを引き起こします。高負荷時には、トランザクション時間が少し伸びるだけで全体の待ちが増えることがあります。
マイクロサービスでは、ひとつのサービスが長いトランザクションを持つと、別サービスの処理にも影響する場合があります。トランザクション範囲を小さくし、外部API呼び出しを中に入れない設計が重要です。
トランザクションで確認する主な内容は、次のとおりです。
- トランザクション時間が長すぎないか
- ロック待ちが発生していないか
- 更新対象の範囲が広すぎないか
- 外部API呼び出しを含んでいないか
- 高負荷時に待ち時間が増えていないか
| 確認項目 | 例 | 分かること |
|---|---|---|
| トランザクション時間 | 開始からコミットまで | 処理範囲の大きさ |
| ロック待ち | 行ロック、テーブルロック | 他処理への影響 |
| 更新範囲 | 複数テーブル更新 | 競合の発生しやすさ |
| 外部API呼び出し | 決済API、通知API | 待ち時間増加の原因 |
| 高負荷時の変化 | 負荷試験中の待ち時間 | 性能劣化の発生点 |
トランザクションは短く保つことが重要です。特にマイクロサービスでは、一つの長いトランザクションが他の処理にも影響するため、ロック時間や外部API呼び出しの有無を確認する必要があります。
10.4 読み取りと書き込み
読み取りが多いサービスと書き込みが多いサービスでは、見るべき指標が違います。読み取りではキャッシュや索引、書き込みではロック、待ち、整合性制御が重要になります。
ベンチマークでは、読み取りだけの理想条件ではなく、本番に近い読み書き比率を再現します。特に注文や決済のような書き込み処理は慎重に測ります。
読み取りと書き込みで確認する主な内容は、次のとおりです。
- 本番に近い読み書き比率になっているか
- 読み取り処理でキャッシュや索引が効いているか
- 書き込み処理でロック待ちが発生していないか
- 更新時の整合性が保たれているか
- 注文や決済など重要処理の遅延がないか
| 種類 | 確認項目 | 重要な理由 |
|---|---|---|
| 読み取り | キャッシュ、索引、取得件数 | 応答時間を短くするため |
| 書き込み | ロック、待ち、更新件数 | 競合や遅延を防ぐため |
| 読み書き比率 | 本番に近い比率 | 現実に近い性能を測るため |
| 整合性制御 | トランザクション、排他制御 | データ不整合を防ぐため |
| 重要処理 | 注文、決済、在庫更新 | 利用者影響が大きいため |
読み取りと書き込みでは性能を見る観点が異なります。正確な性能評価を行うためには、読み取りだけの理想的な条件ではなく、本番に近い読み書き比率を再現して測定することが重要です。
10.5 データベース時間計測例
以下は、GoのHTTPハンドラー内でデータベース問い合わせ時間を測る簡略例です。実務ではメトリクスとしてヒストグラムに送ると比較しやすくなります。
問い合わせ時間計測例
package main
import ( "context" "database/sql" "log" "time")
func findOrder(ctx context.Context, db *sql.DB, id string) error { start := time.Now()
row := db.QueryRowContext(ctx, ` SELECT id, user_id, amount, status FROM orders WHERE id = ? `, id)
var orderID, userID, status string var amount int64
err := row.Scan(&orderID, &userID, &amount, &status) elapsed := time.Since(start)
log.Printf("query=find_order elapsed_ms=%d", elapsed.Milliseconds())
return err}
11. メモリとガベージコレクション
Go言語サービスでは、メモリ割り当てとガベージコレクションが応答時間に影響することがあります。特に高頻度APIでは、要求ごとの割り当てが少し増えるだけでも、全体では大きな負荷になります。
性能ベンチマークでは、CPU時間だけでなく、割り当て回数、割り当て量、ヒープ使用量、GC頻度、GC時間を確認します。Go公式診断文書でも、プロファイリングは頻繁に呼ばれる箇所や高コストな箇所を特定するのに有用だと説明されています。
11.1 割り当て回数
要求ごとのメモリ割り当てが多いと、ガベージコレクションの負担が増えます。JSON変換、文字列処理、スライス拡張、ログ用の一時オブジェクト生成はよく確認すべき箇所です。
Goベンチマークでは b.ReportAllocs() や -benchmem を使うことで、割り当ての傾向を見られます。内部処理の改善には非常に有効です。
11.2 オブジェクト再利用
一時オブジェクトを減らすために、バッファの再利用や構造体の使い回しを検討することがあります。ただし、過度な最適化はコードを複雑にし、バグの原因にもなります。
まずプロファイルで本当に割り当てが問題か確認します。測定なしに再利用を増やすと、保守性だけが悪化する可能性があります。
11.3 大きな応答
大きなJSON応答や大量データ取得は、メモリ使用量を増やします。ページング、ストリーミング、必要項目だけの取得を検討します。
マイクロサービスでは、内部APIだからといって巨大な応答を返すと、呼び出し元のメモリやネットワークにも影響します。サービス境界では、応答サイズも性能指標として扱います。
11.4 GCの観測
GCの影響を見るには、メモリプロファイル、実行追跡、ランタイムメトリクスを組み合わせます。応答時間の揺れが大きいとき、GCが関係しているか確認します。
ただし、GCを単純に悪者にするのは避けます。多くの場合、根本原因は過剰な割り当てや大きすぎるデータ構造にあります。
11.5 割り当て削減例
以下は、文字列結合を単純に繰り返す処理を、バッファ利用へ変える例です。実際に速くなるかは入力サイズと利用頻度によるため、必ずベンチマークで確認します。
割り当て比較例
package textutil
import ( "strings" "testing")
func JoinSlow(values []string) string { result := "" for _, v := range values { result += v + "," } return result}
func JoinFast(values []string) string { var b strings.Builder for _, v := range values { b.WriteString(v) b.WriteByte(',') } return b.String()}
func BenchmarkJoinSlow(b *testing.B) { values := []string{"order", "payment", "inventory", "notification"}
b.ReportAllocs()
for i := 0; i < b.N; i++ { _ = JoinSlow(values) }}
func BenchmarkJoinFast(b *testing.B) { values := []string{"order", "payment", "inventory", "notification"}
b.ReportAllocs()
for i := 0; i < b.N; i++ { _ = JoinFast(values) }}
12. 並行処理と待ち
Go言語の強みのひとつはゴルーチンを使った並行処理ですが、並行にすれば必ず速くなるわけではありません。マイクロサービスでは、外部API待ちやデータベース待ちを並行化できる一方で、共有資源の競合や過剰な同時処理で遅くなることもあります。
性能ベンチマークでは、同時数を増やしたときにどこで限界が来るかを確認します。CPUが足りないのか、接続数が足りないのか、ロックで詰まるのかを分けて見ます。
12.1 ゴルーチン数
ゴルーチンは軽量ですが、無限に増やしてよいわけではありません。外部サービスやデータベースの処理能力を超えて同時処理を増やすと、待ち時間や失敗率が増えます。
負荷試験では、ゴルーチン数、同時要求数、接続数の関係を確認します。内部でゴルーチンを起動している処理は、要求数が増えたときに爆発的に増えないか注意します。
12.2 ワーカー制御
大量の処理を行う場合は、ワーカー数を制御する設計が有効です。無制限にゴルーチンを作るのではなく、一定数のワーカーで処理すると資源を安定させやすくなります。
マイクロサービスでは、下流サービスを守るためにも同時数制御が重要です。自サービスだけが速くても、下流を過負荷にすれば全体障害につながります。
12.3 コンテキストとタイムアウト
Goでは context.Context を使ってキャンセルやタイムアウトを伝える設計が一般的です。マイクロサービスでは、呼び出し元が諦めた処理を下流で続けないことが重要です。
タイムアウトがない処理は、負荷が高いときにゴルーチンや接続を長く占有する原因になります。ベンチマークでは、遅延時に正しくキャンセルされるかも確認します。
12.4 ロック範囲
共有データを守るためにロックを使う場合、ロック範囲が広いと高負荷時に詰まります。特にグローバルなキャッシュや集計値を扱う処理では注意が必要です。
ロックを減らす前に、実際に競合しているかをプロファイルで確認します。ロックを避けるために複雑な設計にすると、別の問題が生まれる場合があります。
| 確認項目 | 例 | 注意点 |
|---|---|---|
| ロック範囲 | ミューテックス内で大きな処理を行う | ロック中の処理時間が長いと待ちが増える |
| 共有データ | グローバルキャッシュ、集計値 | 高負荷時に同時アクセスが集中しやすい |
| 競合の有無 | Mutexプロファイル、Go trace | 推測ではなく実測で判断する |
| ロック削減 | ロック分割、読み取り専用化 | 設計が複雑になりすぎないようにする |
| 代替手段 | コピーオンライト、局所キャッシュ | データ整合性への影響を確認する |
ロック範囲は小さく保つことが重要ですが、必ずしもロックをなくせばよいわけではありません。実際の競合状況を確認し、単純さと性能のバランスを取りながら改善する必要があります。
12.5 同時処理制御例
以下は、下流サービス呼び出しの同時数を制限する簡略例です。負荷試験で制限なしと比較し、応答時間と失敗率の変化を確認します。
同時数制御例
package client
import ( "context" "net/http")
type LimitedClient struct { client *http.Client sem chan struct{}}
func NewLimitedClient(maxConcurrent int) *LimitedClient { return &LimitedClient{ client: &http.Client{}, sem: make(chan struct{}, maxConcurrent), }}
func (c *LimitedClient) Do(ctx context.Context, req *http.Request) (*http.Response, error) { select { case c.sem <- struct{}{}: defer func() { <-c.sem }() case <-ctx.Done(): return nil, ctx.Err() }
return c.client.Do(req.WithContext(ctx))}
13. コンテナ資源とクラスター
Go言語マイクロサービスはコンテナで動かすことが多く、コンテナ資源の設定は性能に直結します。CPU上限、メモリ上限、Pod数、配置、ネットワーク、オートスケーリングが、負荷試験の結果を左右します。
Kubernetesでは、コンテナにCPUやメモリの要求値と上限値を設定できます。CPU上限を設定したコンテナは、その上限を超えてCPUを使えません。
13.1 CPU要求値
CPU要求値は、スケジューリング時に必要なCPU量を示します。要求値が低すぎると、ノード上で他のPodと競合しやすくなり、性能が不安定になることがあります。
ベンチマークでは、要求値を変えたときに応答時間と処理量がどう変わるかを確認します。単に小さくして費用を下げるだけではなく、安定性とのバランスを見ます。
13.2 CPU上限値
CPU上限値は、コンテナが使えるCPUの最大量に関係します。上限が低すぎると、高負荷時に処理が詰まり、応答時間が伸びることがあります。
Goサービスでは、CPU上限と GOMAXPROCS の関係も考慮します。コンテナで使えるCPU量とGoランタイムの並列実行設定が大きくずれると、期待した性能にならない場合があります。
13.3 メモリ上限値
メモリ上限値を超えると、コンテナが終了させられる可能性があります。高負荷時に一時的なメモリ使用量が増えるサービスでは、負荷試験で余裕を確認します。
Goではヒープ使用量だけでなく、スタック、接続、バッファ、外部ライブラリのメモリも含めて見る必要があります。ベンチマークではピーク使用量を記録します。
13.4 Pod数
Pod数を増やすと処理量は上がる可能性がありますが、必ず線形に伸びるわけではありません。データベース、キャッシュ、下流サービスがボトルネックになると、Podを増やしても効果が弱くなります。
性能ベンチマークでは、1 Pod、2 Pod、4 Podのように段階的に増やし、どこで伸びが鈍るかを確認します。伸びが鈍った場所に全体の制約があります。
13.5 Kubernetes資源設定例
以下は、Go言語マイクロサービスの資源設定例です。実際の値はベンチマーク結果と本番監視値をもとに調整します。
Kubernetes設定例
apiVersion: apps/v1kind: Deploymentmetadata: name: order-servicespec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/order-service:8f3a21c ports: - containerPort: 8080 resources: requests: cpu: "500m" memory: "256Mi" limits: cpu: "1" memory: "512Mi"
14. 継続的性能検証
性能ベンチマークは、一度だけ実行して終わるものではありません。コード、依存関係、Go版、コンテナ画像、データ量、通信先が変われば、性能も変わります。
そのため、継続的統合・継続的デリバリーの流れに性能検証を組み込み、性能劣化を早く見つけることが重要です。軽いベンチマークは毎回、重い負荷試験は定期または配備前に実行します。
14.1 性能退行
性能退行とは、機能的には正しくても性能が悪化することです。新しいログ追加、JSON項目追加、データベース問い合わせ変更、依存ライブラリ変更で起こることがあります。
Goの単体試験だけでは性能退行は検出できません。ベンチマーク結果を保存し、前回や基準値と比較する仕組みが必要です。
14.2 軽量ベンチマーク
軽量ベンチマークは、短時間で終わる関数単位や小さなAPI単位の測定です。統合要求ごとに実行しても開発速度を大きく落とさない範囲にします。
たとえば、重要な変換処理や認証処理のGoベンチマークを実行し、極端に遅くなった場合だけ失敗にします。厳しすぎる基準にすると誤検知が増えるため注意します。
14.3 重量負荷試験
重量負荷試験は、複数サービスを起動し、本番に近い負荷をかける試験です。時間と資源が必要なため、主要分岐、夜間、リリース前などに実行します。
この試験では、応答時間、処理量、失敗率、資源使用量、分散追跡をまとめて確認します。結果はチームが見られる場所に保存します。
14.4 しきい値
性能しきい値は、パイプラインを成功または失敗にする基準です。たとえば、95パーセンタイル応答時間が300ミリ秒未満、失敗率が1%未満、CPU使用率が一定以下という形です。
しきい値は、理想ではなく利用者体験とサービス目標から決めます。Prometheusのヒストグラムやサマリーは遅延分布の観測に使われ、特にヒストグラムは集約しやすい指標として説明されています。
14.5 CI設定例
以下は、軽いGoベンチマークとk6負荷試験を分ける例です。実務では、重い負荷試験を手動またはスケジュール実行にすることもあります。
CI設定例
stages: - benchmark - load_test
go_benchmark: stage: benchmark script: - go test ./... -bench=. -benchmem -run=^$ | tee benchmark.txt artifacts: paths: - benchmark.txt
load_test_staging: stage: load_test script: - k6 run tests/load/order.js rules: - if: '$CI_COMMIT_BRANCH == "main"'
15. 改善判断と運用
Go言語マイクロサービスの性能ベンチマークで最も重要なのは、測定結果を改善判断に使うことです。数値を集めるだけではなく、どこを直すべきか、どの改善が本当に効いたのか、どこまでで十分なのかを判断します。
性能改善は、コードだけでなく、データベース、通信、コンテナ資源、キャッシュ、設計、運用にも関係します。局所最適ではなく、利用者体験と運用費用の両方を見る必要があります。
15.1 ボトルネックを決める
性能改善では、最初にボトルネックを決めます。CPUが詰まっているのか、データベースが遅いのか、下流サービス待ちなのか、メモリ割り当てが多いのかを測定で確認します。
推測で改善を始めると、効果の薄い部分に時間を使ってしまいます。負荷試験、pprof、分散追跡、メトリクスを組み合わせて判断します。
15.2 改善前後を比較する
改善を入れたら、同じ条件で再測定します。条件が変わっていると、改善したのか環境差なのか判断できません。
比較では、平均だけでなく、95パーセンタイル、99パーセンタイル、失敗率、CPU、メモリも見ます。マイクロサービスでは、一部の改善が別サービスへ負荷を移しているだけの場合もあります。
15.3 過剰最適化を避ける
性能改善は重要ですが、過剰最適化は保守性を下げます。読みにくいコード、複雑なキャッシュ、特殊な並行処理は、将来の変更を難しくすることがあります。
利用者体験やサービス目標を満たしているなら、それ以上の最適化より、安定性、監視、戻しやすさを優先すべき場合もあります。
15.4 結果を共有する
ベンチマーク結果は、開発者だけでなく、運用担当、プロダクト担当、信頼性担当が見られる形にします。性能の現状と限界を共有することで、リリース判断や容量計画がしやすくなります。
結果には、条件、グラフ、比較、原因、次の改善案を含めます。単なる数値一覧より、判断に使える説明が重要です。
15.5 ベンチマーク報告例
以下は、チームで共有するベンチマーク報告の簡略例です。毎回同じ形式にすると、過去との比較がしやすくなります。
報告テンプレート例
# order-service 性能ベンチマーク報告
## 対象- サービス: order-service- コミット: 8f3a21c- 画像: registry.example.com/order-service:8f3a21c- 環境: staging- Pod数: 3
## 条件- 試験時間: 10分- 最大同時利用者: 100- データ件数: 1,000,000- 負荷シナリオ: 注文作成 70%, 注文参照 30%
## 結果- 95パーセンタイル応答時間: 240ms- 99パーセンタイル応答時間: 410ms- 失敗率: 0.2%- 最大CPU使用率: 78%- 最大メモリ使用量: 390Mi
## 判断- 現在の基準を満たす。- 99パーセンタイルで在庫サービス待ちが増えているため、次回は在庫APIの追跡を強化する。
おわりに
Go言語マイクロサービスの性能ベンチマークでは、ひとつの数値だけで判断しないことが重要です。関数ベンチマーク、HTTP負荷試験、サービス間試験、プロファイリング、分散追跡、メトリクスを組み合わせることで、どこが遅く、なぜ遅く、どう改善すべきかが見えてきます。
Go標準の testing パッケージは内部処理の測定に役立ち、pprof や runtime/trace はGoプロセス内部の原因分析に役立ちます。さらに、OpenTelemetryやPrometheusのような観測基盤を組み合わせることで、マイクロサービス全体の遅延経路や分布を把握しやすくなります。
最終的に大切なのは、測定を継続し、同じ条件で比較し、改善判断に使うことです。性能ベンチマークを継続的統合・継続的デリバリーの一部に組み込めば、Go言語マイクロサービスの性能退行を早く見つけ、利用者体験と運用安定性を守りやすくなります。
EN
JP
KR