コンテンツにスキップ

システム設計:Go で実装する高スループット Webhook 配信システム

問題

Design a high-throughput webhook delivery system that can send millions of webhooks reliably to external customers.

シニア Go エンジニア向けシステム設計面接問題。

解答

イベント発生時にコアサービスが直接 HTTP 呼び出しをするのではなく、キューを介して非同期配信する設計が基本。

アーキテクチャ概要

[Event Producer]
  → イベント発生(payment_succeeded / order_created / user_deleted)
  → Durable Store に保存 + Kafka/SQS/NATS へ publish


[Webhook Dispatcher]
  → 購読しているカスタマーを特定
  → ペイロードに署名(HMAC-SHA256)
  → カスタマーごと / 優先度別のキューへ配信ジョブを push


[Worker Pool]
  → HTTP リクエストをカスタマーエンドポイントへ送信
  → 失敗時はリトライ or DLQ

解説

配信ステータスの管理

全配信試行を永続化し、ステータスを追跡する。

CREATE TABLE webhook_deliveries (
  id          UUID PRIMARY KEY,
  webhook_id  UUID NOT NULL,
  customer_id UUID NOT NULL,
  status      ENUM('pending', 'delivered', 'failed', 'retrying', 'dead'),
  attempt     INT DEFAULT 0,
  next_retry  TIMESTAMP,
  created_at  TIMESTAMP,
  delivered_at TIMESTAMP
);

リトライ戦略(Exponential Backoff)

試行 1: 即時
試行 2: 1分後
試行 3: 5分後
試行 4: 15分後
試行 5: 1時間後
試行 6: 24時間後
→ 最終失敗 → Dead Letter Queue へ

ただし 400 はリトライしない(ペイロードが不正なため、同じ内容を再送しても意味がない)。

カスタマーごとの分離

// カスタマー単位のワーカープールで他カスタマーへの影響を遮断
type CustomerWorkerPool struct {
    customerID string
    sem        chan struct{} // 同時実行数の制限
    jobs       chan DeliveryJob
}

func (p *CustomerWorkerPool) process(ctx context.Context) {
    for job := range p.jobs {
        p.sem <- struct{}{}
        go func(j DeliveryJob) {
            defer func() { <-p.sem }()
            deliver(ctx, j)
        }(job)
    }
}

1カスタマーのエンドポイントが遅くても、他のカスタマーへの配信がブロックされない。

HTTP クライアントの適切な設定(Go)

// http.Client は使い回す(per-request 生成は NG)
var httpClient = &http.Client{
    Timeout: 5 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        1000,
        MaxConnsPerHost:     100,
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 5 * time.Second,
        ResponseHeaderTimeout: 3 * time.Second,
    },
}

ペイロード署名(セキュリティ)

func signPayload(secret, payload string) string {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(payload))
    return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
// X-Webhook-Signature ヘッダーに付与

カスタマーはシークレットで署名を検証し、なりすましを防止する。

スケール見積もり

DAU: 1000万
1ユーザー/日のイベント: 10件
→ 1億 webhook/日 ≈ 1,157 webhook/秒(平均)
→ ピーク: 10,000 webhook/秒

Worker 構成:
  - カスタマー A(高トラフィック): 50並列
  - カスタマー B(標準):           10並列
  - カスタマー C(低量):            2並列

面接でのポイント

  • 「非同期キュー分離」「per-customer ワーカー分離」の2点は必須
  • リトライは 4xx と 5xx で戦略を分ける(4xx はリトライしない)
  • HMAC 署名によるセキュリティへの言及で加点
  • Go 固有: http.Client の使い回し・Transport 設定・context.Context によるタイムアウト管理
  • DLQ・ステータス永続化でオブザーバビリティも評価される