Skip to content

パフォーマンス最適化

HTTPC は設計段階から高性能を目指しています:コネクションプール再利用、HTTP/2 多重化、オブジェクトプール、単一割り当ての結果オブジェクト。ほとんどのシナリオでは、プリセット設定をそのまま使うだけで優れたパフォーマンスが得られます。さらにチューニングが必要な場合は、基盤のメカニズムを理解して的確に対処することが重要です。

プリセット設定の比較

HTTPC は 5 種類のプリセット設定を提供し、それぞれが異なるシナリオ向けに体系的に調整されています。以下はカテゴリ別に主なフィールドの正確な値を示し、選択時の比較に便利です。

タイムアウト設定

フィールドDefaultSecurePerformanceTestingMinimal
Timeouts.Request180s15s60s180s180s
Timeouts.Dial10s5s15s5s5s
Timeouts.TLSHandshake10s5s15s5s5s
Timeouts.ResponseHeader0(無効)10s0(無効)0(無効)0(無効)
Timeouts.IdleConn90s30s120s30s30s

接続設定

フィールドDefaultSecurePerformanceTestingMinimal
MaxIdleConns50201001010
MaxConnsPerHost1052052
EnableHTTP2有効有効有効無効有効
EnableCookies無効無効有効有効無効
EnableDoH無効無効無効無効無効

セキュリティ設定

フィールドDefaultSecurePerformanceTestingMinimal
MaxResponseBodySize10MB5MB50MB10MB1MB
MaxDecompressedBodySize100MB100MB100MB100MB100MB
ValidateURL有効有効有効無効有効
ValidateHeaders有効有効有効無効有効
StrictContentLength有効有効無効有効有効
AllowPrivateIPsfalsefalsefalsetruefalse
InsecureSkipVerifyfalsefalsefalsetruefalse

リトライ設定

フィールドDefaultSecurePerformanceTestingMinimal
MaxRetries31310
Delay1s2s500ms100ms0
BackoffFactor2.02.01.52.01.0
MaxRetryDelay30s30s30s30s30s
EnableJitter有効有効有効無効無効

リクエストデフォルト値

フィールドDefaultSecurePerformanceTestingMinimal
FollowRedirects有効無効有効有効無効
MaxRedirects1010101010
UserAgenthttpc/1.0httpc/1.0httpc/1.0httpc-test/1.0httpc/1.0

TestingConfig は本番使用禁止

TestingConfig() は URL/ヘッダー検証、TLS 証明書検証、SSRF 防護を無効化し、ローカル開発とテストのみに使用します。非テスト環境で呼び出すとセキュリティ警告が出力されます。本番環境では SecureConfig() または DefaultConfig() を使用してください。

シナリオ別選択

シナリオ推奨プリセット調整の提案
汎用 Web サービスDefault
ユーザー提供の URL を処理Secure
内部マイクロサービス高並列Performanceバックエンド数に応じて MaxIdleConns を増加
一度きりのスクリプトMinimal
ファイルダウンロードサービスPerformanceMaxResponseBodySize を増加
金融/医療 APISecure + カスタム監査ミドルウェアを追加
ローカル開発/ユニットテストTesting本番にデプロイしないこと
go
// 高スループットシナリオではプリセットをそのまま使用
client, _ := httpc.New(httpc.PerformanceConfig())

// プリセットをベースに個別フィールドを微調整
cfg := httpc.PerformanceConfig()
cfg.Timeouts.Request = 120 * time.Second
cfg.Connection.MaxIdleConns = 200
client, _ := httpc.New(cfg)

コネクションプールチューニングの仕組み

コネクションプールは HTTP クライアントのパフォーマンスの中核です。HTTPC のコネクションプールは Go 標準ライブラリの http.Transport に基づいていますが、自動計算ロジックとセキュアなデフォルト値をその上に追加しています。

アイドル接続の自動計算

MaxIdleConnsPerHost(ホストごとのアイドル接続上限)は手動で設定する必要はありません——HTTPC が MaxConnsPerHost から自動的に導出します:

アイドル接続数 = MaxConnsPerHost / 2、区間 [2, 10] に制限

具体的なルール(calculateIdleConnsPerHost):

MaxConnsPerHost自動アイドル接続数説明
0(無制限)10上限のデフォルト値を使用
12下限を下回らない
22下限にちょうど等しい
52半分が下限になる
105Default プリセット
2010Performance プリセット、上限を採用
10010上限を超えるので 10

なぜ MaxConnsPerHost / 2 なのか

アイドル接続とは「接続のキャッシュ」——確立済みだが一時的に未使用の接続です。最大接続数の半分に設定することで、「既存接続の再利用」(キャッシュヒット)と「新規接続の確立」(キャッシュミス時に再ハンドシェイクが必要)のバランスを取り、アイドル接続が多すぎてサーバー側リソースを占有するのを防ぎます。

TCP Keep-Alive

HTTPC のコネクションプールは固定で 30 秒の TCP keep-alive 間隔(defaultKeepAlive = 30 * time.Second)を使用します。この値は接続確立後に OS が周期的に keep-alive プローブパケットを送信し、デッド接続を検出します。IdleConn タイムアウトはアイドル接続がプール内で生存する時間(Default では 90s)を制御し、両者が連携して動作します。

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // マイクロサービスの高 QPS シナリオ:コネクションプールを拡大
    cfg := httpc.PerformanceConfig()
    cfg.Connection.MaxIdleConns = 200   // グローバルアイドル接続上限
    cfg.Connection.MaxConnsPerHost = 50 // ホストごとの最大接続(アイドルは自動計算で 10)
    cfg.Timeouts.IdleConn = 300 * time.Second // アイドル接続の生存時間を延ばし、再利用率を向上

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // ホットパスのリクエストはコネクションプール内の接続を直接再利用
    for i := 0; i < 100; i++ {
        result, err := client.Get("https://api.example.com/data")
        if err != nil {
            log.Printf("リクエスト %d 失敗: %v", i, err)
            continue
        }
        fmt.Printf("リクエスト %d: %d\n", i, result.StatusCode())
    }
}

HTTP/2 のパフォーマンス優位性

HTTP/2 はデフォルトで有効(EnableHTTP2 = true)であり、3 つの主要なパフォーマンス向上をもたらします:

特性HTTP/1.1HTTP/2
多重化各リクエストが接続を独占複数リクエストが単一接続を共有
ヘッダー圧縮平文で重複送信HPACK がヘッダーを圧縮
接続再利用Keep-alive は直列並列ストリーム(stream)

HTTP/2 とコネクションプールの関係

HTTP/2 の多重化により、単一の TCP 接続で複数のリクエストを同時に処理でき、接続確立のオーバーヘッドを大幅に削減できます。同じホストに対する高並列シナリオでは、HTTP/2 のスループットは HTTP/1.1 を大きく上回ります。TestingConfig()(HTTP/2 を明示的に無効化)を使用するか、接続が ALPN ネゴシエーションをサポートしない場合にのみ HTTP/1.1 にフォールバックします。

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // デフォルト設定で HTTP/2 は有効
    cfg := httpc.DefaultConfig()
    cfg.Connection.EnableHTTP2 = true // デフォルトで true、明示的に宣言するとより明確

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // HTTP/2 をサポートするサイト(ほとんどの CDN/クラウドサービスなど)に並列リクエストを送信
    // 単一の TCP 接続を再利用でき、各リクエストで新規接続を確立する必要がない
    start := time.Now()
    for i := 0; i < 10; i++ {
        result, err := client.Get("https://http2.golang.org/")
        if err != nil {
            log.Printf("リクエスト %d 失敗: %v", i, err)
            continue
        }
        // Proto() はプロトコルバージョンを返す(例:"HTTP/2.0")
        fmt.Printf("リクエスト %d: %s, ステータスコード %d\n", i, result.Proto(), result.StatusCode())
    }
    fmt.Printf("10 リクエストの所要時間: %v\n", time.Since(start))
}

メモリ最適化メカニズム

HTTPC はメモリ管理において多層の最適化を行っており、中核となる考え方はヒープ割り当ての削減とオブジェクトの再利用です。

resultBundle の単一割り当て

各リクエストが返す *Result は 3 つのネストされた構造体を保持します:RequestInfo(リクエスト情報)、ResponseInfo(レスポンス情報)、RequestMeta(所要時間などのメタデータ)。従来の方法では Result と 3 つのネスト構造体それぞれに割り当てが必要で、4 回のヒープ割り当てが発生します。HTTPC はこれらを 1 つの resultBundle にパッケージ化し、1 回のヒープ割り当てで全体を済ませます:

従来方式:4 回の独立割り当て(Result + RequestInfo + ResponseInfo + RequestMeta)
HTTPC:1 回の割り当て(resultBundle)、Result の 3 つのポインタが同一メモリブロックを指す

呼び出し元が受け取るのは *Result で、その RequestResponseMeta フィールド(ポインタ)は bundle 内の対応する構造体を指しており、完全に透過です。呼び出し元が *Result を長期にわたり保持する可能性があるため、ここではオブジェクトプールは適しません(プール化はデータ競合を引き起こすため)、GC による自動回収に任せます。

エンジンのオブジェクトプール

HTTPC のエンジン層は sync.Pool を広く使用して短いライフサイクルのオブジェクトを再利用し、GC 負荷を軽減します:

プール対象オブジェクト用途説明
engine.Responseレスポンスオブジェクトリクエスト完了後にプールへ返却、次回リクエストで再利用
engine.Requestリクエストオブジェクト同上
strings.Builder文字列構築URL 構築、エラー整形、Config シリアライズ
http.HeaderHTTP ヘッダー mapリクエスト/レスポンスヘッダー処理
bytes.BufferJSON/multipart エンコード初期容量で事前割り当て
time.Timerリトライタイマー頻繁なタイマー生成を回避
gzip/flate reader解凍解凍器を再利用

オブジェクトプールと resultBundle の役割分担

エンジン内部オブジェクト(Response/Request/Builder)はライフサイクルが短く、リクエスト内部で borrow-return サイクルが完結するため、プール化に適しています。呼び出し元に返される *Result はライフサイクルが不確定なため、単一割り当て + GC 回収に適しています。両者は互补し、それぞれの長所を活かしています。

意識する必要はありません

上記の最適化は呼び出し元にとって完全に透過です。通常通り API を使用するだけで、接続再利用、オブジェクトプール、単一割り当てはすべて内部で自動的に行われます:

go
package main

import (
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Result はリクエストごとに新規作成、GC が自動回収、手動解放不要
    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }

    // ホットパスでは Body() ではなく RawBody() を優先使用
    // RawBody() は生のバイトスライスを返す;Body() は事前格納された文字列を返す;String() はデバッグ用整形(オーバーヘッド最大)
    data := result.RawBody()
    fmt.Printf("レスポンスサイズ: %d バイト\n", len(data))
    fmt.Printf("リクエスト所要時間: %v\n", result.Meta.Duration)
}

ワークロードチューニングの例

AI API のロングポーリング

AI 推論 API の応答時間は数分に達する可能性があり、タイムアウト制限を緩める必要があります:

go
// AI API は 5-15 分の応答が必要な場合があり、デフォルトの 180s タイムアウトで打ち切られないようにする
result, err := httpc.Post("https://api.ai.example.com/v1/completions",
    httpc.WithJSON(payload),
    httpc.WithTimeout(900*time.Second), // 15 分
)

Default の ResponseHeader が 0 である理由

Timeouts.ResponseHeader = 0 はトランスポート層でレスポンスヘッダータイムアウトを強制しないことを意味し、context レベルのタイムアウト(Timeouts.Request または WithTimeout)で一元管理します。これにより WithTimeout() が長いレスポンスのリクエストに対して完全な制御権を持ちます。slowloris 攻撃に対するトランスポート層防御が必要な場合は SecureConfig()(10s に設定)を使用してください。

マイクロサービスの高 QPS

内部マイクロサービス間の高頻度呼び出しには大規模なコネクションプールが必要です:

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    // コネクションプールをバックエンドインスタンス数に応じてチューニング
    cfg.Connection.MaxIdleConns = 300   // 総アイドル接続
    cfg.Connection.MaxConnsPerHost = 30 // バックエンドインスタンスごと
    // マイクロサービスの応答は通常高速、タイムアウトを短くして高速に失敗させる
    cfg.Timeouts.Request = 10 * time.Second
    cfg.Retry.Delay = 200 * time.Millisecond
    cfg.Retry.BackoffFactor = 2.0
    cfg.Retry.MaxRetries = 2

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start := time.Now()
    // 高頻度リクエストがコネクションプールを再利用、TCP/TLS の再確立不要
    for i := 0; i < 50; i++ {
        result, err := client.Get("http://user-service:8080/api/users")
        if err != nil {
            log.Printf("リクエスト %d 失敗: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("50 リクエストの所要時間: %v\n", time.Since(start))
}

大容量ファイルダウンロード(ストリーミング)

大容量ファイルのダウンロードでは WithStreamBody(true) を使用してレスポンス全体がメモリに読み込まれるのを回避し、Download() メソッドと組み合わせてレジュームをサポートします:

go
package main

import (
    "context"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    cfg.Security.MaxResponseBodySize = 500 * 1024 * 1024 // 500MB 上限

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    dcfg := httpc.DefaultDownloadConfig()
    dcfg.FilePath = "/tmp/large-file.zip"
    dcfg.ResumeDownload = true // レジューム

    result, err := client.Download(
        context.Background(),
        "https://example.com/large-file.zip",
        dcfg,
    )
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("ダウンロード完了: %d バイト", result.BytesWritten)
}

クローラーとプロキシプール

クローラーシナリオではプロキシプールで IP をローテーションし、HTTPC は各プロキシが少なくとも 1 回試行されるようリトライ回数を自動的に引き上げます(詳細は リトライとフォールトトレランス を参照):

go
cfg := httpc.DefaultConfig()
cfg.Connection.ProxyPool = []string{
    "http://proxy1:8080",
    "http://proxy2:8080",
    "http://proxy3:8080",
    "http://proxy4:8080",
    "http://proxy5:8080",
}
cfg.Connection.ProxyRotateOnStatus = []int{403} // 403 でプロキシ切替
cfg.Connection.ProxyPoolStrategy = httpc.ProxyStrategyRoundRobin
// MaxRetries は自動的に 4 に引き上げられ(プロキシ数-1)、5 つのプロキシすべてが試行される

パフォーマンスアンチパターン

アンチパターン原因正しいアプローチ
リクエストごとに Client を新規作成接続を再利用できず、毎回 TCP/TLS ハンドシェイクが必要グローバルで単一の Client インスタンスを再利用
MaxResponseBodySize が大きすぎる無駄にメモリ上限を緩める実際のレスポンスサイズに応じて設定
ホットパスで result.String() を使用追加の文字列構築オーバーヘッドresult.Body() または result.RawBody() を使用
コネクションプールが小さすぎる高並列時に接続が不足し、キューで待機MaxConnsPerHost を並列数に応じて調整
HTTP/2 を無効化HTTP/1.1 の直列リクエストに退化デフォルトで有効のまま維持
Close() を無視接続リークdefer client.Close()
グローバル共有後に再利用を忘れるClient を繰り返し作成/破棄一度作成、長期保持

Client は必ず再用

HTTP パフォーマンスの基盤は接続の再利用です。リクエストごとに Client を新規作成すると、毎回 TCP 3 ウェイハンドシェイク + TLS ハンドシェイクが必要になり、レイテンシがサブミリ秒から数十ミリ秒に跳ね上がります。マイクロサービスシナリオでは、Client をシングルトンとしてサービス構造体に注入し、サービスのライフサイクルに合わせて存続させます。

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

// アンチパターンデモ:リクエストごとに Client を新規作成
func main() {
    start := time.Now()

    for i := 0; i < 5; i++ {
        // ❌ 毎回ループで Client を新規作成——接続を再利用できない
        client, err := httpc.NewDefault()
        if err != nil {
            log.Fatal(err)
        }
        result, err := client.Get("https://httpbin.org/get")
        client.Close() // 毎回クローズ、コネクションプールがクリアされる
        if err != nil {
            log.Printf("リクエスト %d 失敗: %v", i, err)
            continue
        }
        _ = result
    }
    // 5 リクエストの所要時間は Client 再用の方式よりはるかに高い
    fmt.Printf("アンチパターンの所要時間: %v\n", time.Since(start))

    // ✅ 正しいアプローチ:Client を再用
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start = time.Now()
    for i := 0; i < 5; i++ {
        result, err := client.Get("https://httpbin.org/get")
        if err != nil {
            log.Printf("リクエスト %d 失敗: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("再用パターンの所要時間: %v\n", time.Since(start))
}

次のステップ