Contents
重要用語と参照
設計議論で使う主要用語を短く定義します。Bounded Context、Saga、Outbox、Event Sourcing などの要点と、詳しく学べる参照リンクを併記して前提知識を揃えます。
Bounded Context(境界)
ドメイン駆動設計の用語で、あるモデルが自明に使える「意味の境界」を指します。チームとデータ所有権が一致する単位でサービス化するのが実務的です。参考: Martin Fowler(Bounded Context)。
Saga(分散トランザクション)
複数サービスにまたがる整合性を、補償(compensation)またはオーケストレーションで保つパターンです。主にオーケストレーション型とコレオグラフィ型の2つがあり、選択は運用・可観測性の要件に依存します。参考: microservices.io(Sagaパターン)。
Outbox(トランザクショナルアウトボックス)
DBトランザクション内でイベントをアウトボックステーブルに書き、別プロセスがそれを読み出して外部メッセージ基盤に送る手法です。DB一貫性とイベント発行の連携に使います。参考: microservices.io、Debezium。
Event Sourcing
状態を最新状態として格納するのではなく、イベントの系列として永続化する手法です。履歴再構築や監査に有利ですが、実装と運用の複雑度が高く、用途を選びます。参考: Martin Fowler(Event Sourcing)。
設計の基本とGoを選ぶ理由
境界設計と実装言語の選定は運用コストと性能に直結します。本節では設計判断基準と、Goを選ぶときのメリットと注意点を示します。
粒度と境界の決め方
まずはビジネス上の責任範囲(Bounded Context)とデータ所有権で切り分けます。独立デプロイ性、スケーリング単位、運用コスト、トランザクション要件を基準に優先順位を付け、読み取り中心や非クリティカル機能から分割するのが実務的です。
- チーム単位でデプロイ可能か
- 個別にスケールできるか
- 横断クエリや強い整合性が本当に必要か
Goを選ぶメリット(実務観点)
Goは静的バイナリで起動が速く、コンテナやCIで扱いやすい点が実務で評価されます。標準ライブラリが成熟しており、goroutine/チャネルによる並行処理設計がシンプルです。一方でGCや大量goroutineの管理は実測が必須です。
GoのGCとgoroutine対策(挙動の補足と実務的注意)
GoのGCは「並行トライカラー・マーク・アンド・スイープ(concurrent tri-color mark-and-sweep)」で、書き込みバリアによる安全な並行マークを行います。長時間のStop-the-World(STW)を避ける設計ですが、割り当てパターンや大きなヒープで短いSTWのスパイクが発生します。以下は実務で押さえる点です(説明は Go 1.20〜1.22 で確認された一般的挙動に基づきます。詳細はGoの公式ブログやリリースノートを参照してください)。
- go_gc_duration_seconds は Prometheus のランタイムコレクタが公開する GC の STW pause のヒストグラムです。総GC CPU時間ではなく、STWの長さを示す点に注意してください。
- GOGC(runtime/debug.SetGCPercent)を調整すると GC 発生頻度とヒープサイズのトレードオフを変えられます。頻繁にGCを回すとCPUが増えるがヒープは小さくなります。
- sync.Pool やバッファ再利用で一時的なアロケーションを削減できますが、過剰なキャッシュはメモリ上限を押し上げる場合があります。
以下に実務で使えるミニコード例を示します。
goroutine を errgroup とコンカレンシー制限で管理する例(最小実装):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package main import ( "context" "database/sql" "fmt" "time" "golang.org/x/sync/errgroup" ) func processItems(ctx context.Context, items []int) error { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() g, ctx := errgroup.WithContext(ctx) sem := make(chan struct{}, 10) // 同時実行上限 for _, it := range items { it := it sem <- struct{}{} g.Go(func() error { defer func() { <-sem }() // キャンセルに対応した処理 select { case <-ctx.Done(): return ctx.Err() default: } // 実処理(例: DBアクセス) _ = it time.Sleep(100 * time.Millisecond) return nil }) } // 全ゴルーチンの完了を待つ return g.Wait() } func main() { _ = processItems(context.Background(), []int{1, 2, 3, 4, 5}) } |
GOGC と sync.Pool の例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package main import ( "runtime/debug" "sync" ) var bufPool = sync.Pool{ New: func() interface{} { return make([]byte, 0, 1024) }, } func init() { // デフォルトは 100。アプリ特性に応じて調整する(テストで効果測定必須)。 debug.SetGCPercent(100) } |
gc や goroutine の問題は pprof と runtime/trace、Prometheus のランタイムメトリクスで実測・比較してください。具体的な取得手順は後段の Observability で示します。
通信手段とAPI設計
通信プロトコル選定は性能と運用性に直結します。ここでは HTTP/REST、gRPC、GraphQL、メッセージングの特性と契約ファーストの実務ワークフローを示します。
HTTP/REST(JSON)
短い導入文: 汎用性が高くブラウザや外部クライアント向けに使いやすい一方、バイナリ効率は低くスキーマ検査が弱いです。
- 外部公開 API やブラウザ互換が必要な場合に最適です。
- ペイロード圧縮やJSON Schemaでの検査を併用してください。
gRPC+Protobuf
短い導入文: 内部RPCで型安全かつ効率重視の設計に向いています。IDLを中心にCIで互換性チェックするのが実務的です。
- バイナリプロトコルで高スループット・低遅延。
- ストリーミングや deadline/metadata が組み込みで利用可能。
- ブラウザ互換は grpc-web 等の追加が必要です。
最小の proto と Go サーバ例:
|
1 2 3 4 5 6 7 8 9 |
// service.proto syntax = "proto3"; package demo; service Greeter { rpc SayHello(HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string msg = 1; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// server.go (抜粋) import ( "context" pb "path/to/generated/proto" ) type server struct{ pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Msg: "hello " + req.Name}, nil } |
Buf を使ったワークフロー(CI 例):
- buf.yaml / buf.lock をリポジトリで管理
- CI で次を実行: buf lint, buf breaking --against
, buf generate - buf の詳細: https://docs.buf.build/
GraphQL
短い導入文: クライアントが必要なデータだけ取得できる利点がある反面、サーバ実装とキャッシュ設計が複雑になりやすいです。
- 多様なクライアント要件があるフロントエンド主導の API に向きます。
- サーバ側で DataLoader 等の N+1 対策が必須です。
メッセージング(Kafka、NATS 等)
短い導入文: 非同期処理やストリーム処理に強く、高スループット・耐障害性を実現できますが運用コストは増えます。
- Kafka は永続化された順序付きストリーム、NATS は低レイテンシを重視。
- スキーマレジストリや DLQ、冪等性設計が重要です。
API契約設計と Buf ワークフロー
短い導入文: 契約ファーストにより破壊的変更の検出とクライアント・サーバの整合が得られます。CI に lint/互換性チェックを組み込んでください。
- proto を共有リポジトリに置くか、サービス毎に分けるかは組織ポリシーで判断。
- buf lint と buf breaking を CI に入れ、生成コードでの契約テストを自動化します。
フレームワークとアーキテクチャパターン
フレームワーク選定は組織の規模と運用方針で変わります。ここでは代表的選択肢の特徴と、採用リスク確認ポイントを提示します。
主要フレームワークの比較(概観)
下表は代表的な選択肢と使い分けの要点です。公開先のレンダリング要件によりテーブル表示が崩れる場合があるため、出力先での確認を推奨します。
| フレームワーク | 特徴 | 典型ユースケース | トレードオフ |
|---|---|---|---|
| go-kit | transport-agnostic、エンドポイント・ミドルウェア設計重視 | 大規模組織の標準化 | ボイラープレート増 |
| Kratos | gRPC/Protobuf 前提の意見強め | gRPC中心の内部サービス | gRPC依存、学習コスト |
| go-micro | サービス発見等を内包(経緯あり) | 小〜中規模の迅速構築(要メンテ確認) | プラグイン依存、メンテ状況確認必要 |
| Gin / Echo / Fiber | 高速・軽量 HTTP ルーター | 外部公開 REST、軽量サービス | 横断機能は自前実装 |
採用リスク評価チェックリスト
短い導入文: フレームワーク採用前にメンテ状況やコミュニティを定量的に評価してください。以下は実務でチェックすべき具体項目です。
- リリース頻度(過去12か月にリリースがあるか)
- コミット活動(過去6か月のコミット数)
- Open Issues / PR の放置状況(90日以上放置が多くないか)
- CI の状況(main/master に対する CI が通っているか)
- テストの存在(ユニット/統合テストのカバレッジ)
- セキュリティアドバイザリ(脆弱性が未対応でないか)
- ライセンス(商用利用に問題ないか)
- ドキュメントと移行ガイドの有無
GitHub API やリポジトリの Release/Commits を見て定量的に評価し、必要なら社内で小規模 PoC を回して運用負荷を確認してください。
データ整合性とセキュリティ
データ戦略はアーキテクチャ全体に影響します。ここでは整合性パターン、アウトボックスの実装例、Saga、そしてセキュリティ運用コストを示します。
データ整合性パターンのまとめ
短い導入文: 分散環境では強整合を維持するコストが高く、妥当な妥協点を設計に組み込むことが重要です。
- DB-per-service:独立性は得られるが横断クエリが複雑化
- Saga:2PCを避け補償で整合性を保つ
- Outbox / CDC:トランザクションとイベント発行の連携に有効
- Event Sourcing:履歴再構築と監査に強いが複雑
アウトボックス実装例(最小再現コード)
短い導入文: ここでは PostgreSQL を想定したシンプルで実務的なアウトボックスの最小実装パターンを示します。重要なのは「DBトランザクション内でイベントを書き、別プロセスが安全に送信して完了フラグを設定する」点です。
DDL(例):
|
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE TABLE outbox ( id UUID PRIMARY KEY, topic TEXT NOT NULL, payload JSONB NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), processed BOOLEAN DEFAULT FALSE, processing BOOLEAN DEFAULT FALSE, published_at TIMESTAMPTZ ); CREATE INDEX idx_outbox_unprocessed ON outbox (processed, processing, created_at); |
ビジネス処理内での書き込み(トランザクション)例(Go + database/sql、エラーハンドリングは簡略):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func SaveEntityAndOutbox(ctx context.Context, db *sql.DB, entityJSON []byte, eventID string, topic string, payload []byte) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() _, err = tx.ExecContext(ctx, "INSERT INTO entities (data) VALUES ($1)", entityJSON) if err != nil { return err } _, err = tx.ExecContext(ctx, "INSERT INTO outbox (id, topic, payload) VALUES ($1, $2, $3)", eventID, topic, payload) if err != nil { return err } return tx.Commit() } |
アウトボックスワーカー(読み取り→送信→更新の安全パターン):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
func RunOutboxWorker(ctx context.Context, db *sql.DB, publisher Publisher, batch int) error { for { // 1) トランザクションで未処理レコードをロックして取得し、processing=true にする tx, _ := db.BeginTx(ctx, nil) rows, _ := tx.QueryContext(ctx, ` SELECT id, topic, payload FROM outbox WHERE processed = FALSE AND processing = FALSE ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT $1`, batch) var events []Event for rows.Next() { var e Event rows.Scan(&e.ID, &e.Topic, &e.Payload) events = append(events, e) } if len(events) == 0 { tx.Commit() select { case <-ctx.Done(): return ctx.Err() case <-time.After(time.Second): } continue } // mark as processing ids := make([]interface{}, 0, len(events)) query := "UPDATE outbox SET processing = TRUE WHERE id = ANY($1)" idArr := make([]string, 0, len(events)) for _, e := range events { idArr = append(idArr, e.ID) } _, _ = tx.ExecContext(ctx, query, pq.Array(idArr)) tx.Commit() // 2) publish(同期的に行うか、確認可能な非同期で行う) for _, e := range events { err := publisher.Publish(ctx, e.Topic, e.Payload) if err != nil { // 失敗した場合は retry/metrics 記録。processing を false に戻すロジックを別途入れる // ここでは簡略にログを出して続行 continue } // 3) 成功したら processed=true をセット _, _ = db.ExecContext(ctx, "UPDATE outbox SET processed = TRUE, processing = FALSE, published_at = now() WHERE id = $1", e.ID) } } } |
注意点:
- 長いDBトランザクション中に外部送信を行わない。SKIP LOCKED と processing フラグの組合せで同時実行を制御する。
- 送信は少なくとも1回の送信となるため、消費側の冪等化が必要。
- Kafka のトランザクション機能を利用して「Exactly-once-ish」パターンを採ることも可能です。
Saga と補償処理(簡潔)
短い導入文: Saga は各サービスの局所トランザクションと補償トランザクションで整合を保ちます。オーケストレーター(中央制御)かコレオグラフィ(イベント駆動)を選びます。
- オーケストレーション型は実行フローが分かりやすいが central orchestrator が必要。
- コレオグラフィ型は疎結合だが障害時のトラブルシュートが難しい。
セキュリティ設計と運用コスト(mTLS・シークレット管理)
短い導入文: サービス間認証に mTLS を使うと盗聴・改竄リスクを低減できますが、証明書の発行・ローテーション・監査が運用コストになります。
主な選択肢と運用負荷:
- cert-manager(Kubernetes)+内部CA or ACME: Kubernetes 環境で導入が容易。証明書発行・更新を自動化できます。
- SPIFFE/SPIRE: ワークロードアイデンティティを発行管理し、証明書の自動ローテーションやアテステーションが可能。より大規模な環境で有効。
- HashiCorp Vault PKI / AWS ACM: 管理型 or 中央集中管理で証明書の発行/ローテーションを行う場合に選択。
運用上の検討点:
- ルートCAの保護とローテーション手順
- 証明書のTTL(短めにして自動ローテーションを前提)
- 失敗時のフォールバック戦略(例: 一時的に信頼チェーンを保持)
- シークレットの配布方法(環境変数ではなくボリューム/CSI/Vaultサイドカー)
導入コストを評価するため、証明書のライフサイクル(発行→配布→ローテーション→失効)にかかる手作業・自動化の割合を明確にしてください。
観測性・テスト・CI/CD・ベンチマーク(再現性)
運用性評価はPoCの主要目的です。ここではログ・メトリクス・トレース、pprof の取得・解析手順、ベンチマーク再現性の要件を示します。
ログ・メトリクス・トレース設計
短い導入文: ログ・メトリクス・トレースは目的別に分離し、SLOに紐づけて運用してください。
- ログ:構造化(JSON)。共通フィールド(timestamp、level、service、request_id、trace_id)を必須化。
- メトリクス:Prometheus エクスポート(ビジネス指標とランタイム指標を分離)。ラベルのカーディナリティに注意。
- トレース:OpenTelemetry を用い、Jaeger/Zipkin に送る。サンプリング方針を事前に決める。
- アラート:SLO/SLI を定義し、症状ベースでアラートを作る。
pprof の取得と解析手順(実践手順)
短い導入文: CPU/heap/goroutine のプロファイルを pprof と runtime/trace で取得し、問題の根本原因を特定します。
サーバ側で pprof を有効にする(最小例):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import ( _ "net/http/pprof" "log" "net/http" ) func init() { go func() { log.Println("pprof listening on :6060") http.ListenAndServe("localhost:6060", nil) }() } |
代表的な取得コマンドと解析:
- CPU プロファイル(30秒収集):
- go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
- pprof 内で
top/list <func>/webを使う - Heap(メモリ):
- go tool pprof http://localhost:6060/debug/pprof/heap
- Goroutine スナップショット:
- curl http://localhost:6060/debug/pprof/goroutine?debug=2
- Trace(詳細イベント):
- curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out
- go tool trace trace.out
Prometheus と組み合わせる場合、go_gc_duration_seconds などのランタイム指標を PromQL で解析します。例: p99 のGC pause を得るクエリ:
|
1 2 |
histogram_quantile(0.99, sum(rate(go_gc_duration_seconds_bucket[5m])) by (le)) |
解析のポイント:
- pprof の
topで占有割合の高い関数を確認する - heap プロファイルで alloc_objects / inuse_space を比較
- goroutine スナップショットでリークの候補を探す
ベンチマークの再現性要件とコマンド例
短い導入文: 再現性あるベンチマークには環境スペック、ネットワーク条件、障害注入手順、完全な負荷コマンドを明示する必要があります。
必須記載(テンプレート):
- ハードウェア/VM: CPU 型番とコア数、メモリ、ディスク種別(例: 8 vCPU, 32GB, NVMe)
- OS とカーネル: 例 Ubuntu 22.04, Linux kernel 5.15
- Go バージョン: 明記(例 Go 1.21)。Go の挙動はバージョン差で変わるため必須
- コンテナ基盤: Docker/CRICTL バージョン、Kubernetes バージョン(必要時)
- ネットワーク: 帯域、基礎遅延、ネットワークシェーピングの有無
- 負荷発生源: 別ホストで実行(負荷元と被負荷側は分離)
負荷試験コマンド例:
- wrk2(HTTP):
|
1 2 |
wrk2 -t12 -c400 -d60s -R10000 -s script.lua --latency http://<host>:8080/path |
- ghz(gRPC):
|
1 2 |
ghz --proto ./service.proto --call pkg.Service.Method -n 100000 -c 100 <host>:<port> |
障害注入例:
- ネットワーク遅延・パケットロス(テストホストで):
|
1 2 3 |
sudo tc qdisc add dev eth0 root netem delay 50ms loss 0.5% sudo tc qdisc del dev eth0 root |
- Podを強制停止: pumba / kubectl delete pod / chaos-mesh など
計測すべき項目:
- p50/p95/p99 レイテンシ、スループット、エラー率
- go_gc_duration_seconds(p99/avg)
- heap_alloc_bytes, goroutines, CPU%, メモリ%
- 外部依存(DB/ブローカー)の影響指標
実験は少なくともウォームアップ→定常測定→障害注入→回復の各フェーズを含め、プロファイルとメトリクスを同期して収集してください。
PoC の簡潔手順と評価軸
短い導入文: PoC は短期間で意思決定に十分な差分を検出することが目的です。性能だけでなく運用性・生産性を重み付けして評価します。
主要ステップ(簡潔):
- 要件定義(SLA、p50/p95/p99、スループット、運用上限)
- 候補選定(プロトコル/フレームワーク/メッセージング)
- 標準テストアプリを用意(認証→処理→DB書込/イベント発行)
- 同一スペック環境で比較(メトリクス・プロファイル収集)
- 負荷シナリオ(安定/バースト/長時間/ストリーミング/障害注入)
- 分析(Prometheus, pprof, trace の突合)
- 意思決定(性能・運用性・開発生産性の重み付け)
評価軸はチームで重みを決め、CSV やスプレッドシートで可視化してください。出力先のレンダリング要件に応じて表形式を調整することを推奨します。
まとめ
設計と実装を分離して評価することで、運用負荷を抑えつつ性能要件を満たせます。Go はコンパクトなバイナリとシンプルな並行モデルでマイクロサービスに向きますが、GC と goroutine の実挙動は Go のバージョンやワークロードに依存するため、必ず pprof とメトリクスで定量評価してください。
- 境界と粒度は Bounded Context とデータ所有権を基準に決める。
- gRPC/Protobuf は内部低遅延向け、REST は外部公開向け、メッセージングは非同期・スケーラブル処理向け。
- Outbox は DB トランザクションとイベント発行を安全に接続する実務的パターン。実装は SKIP LOCKED/processing フラグ等で同時実行を制御する。
- GC や goroutine の最適化は GOGC 調整、sync.Pool、errgroup による管理で改善できるが、必ず実環境で再測定する。
- フレームワーク採用時はリリース頻度・コミット頻度・未解決Issueの状況・CI状況などを定量的に評価する。
- PoC は性能だけでなく運用性・開発生産性・互換性を評価軸にして短期間で比較を行う。ベンチマークは環境仕様とネットワーク条件、障害注入手順を明記して再現性を確保する。
参照(主要):
- Go 公式ブログ(GC・パフォーマンス関連記事): https://go.dev/blog/
- runtime/doc: https://pkg.go.dev/runtime
- Martin Fowler(Bounded Context / Event Sourcing 等)
- microservices.io(Saga / Outbox パターン)