Skip to content

性能优化

HTTPC 从设计上就是高性能的:连接池复用、HTTP/2 多路复用、对象池化、单次分配结果对象。在大多数场景下,直接使用预设配置即可获得优秀性能;需要进一步调优时,理解底层机制才能对症下药。

预设配置对比

HTTPC 提供 5 种预设配置,每种针对不同场景做了系统性调参。下面按类别列出关键字段的精确值,方便对照选型。

超时配置

字段DefaultSecurePerformanceTestingMinimal
TimeoutConfig.Request180s15s60s180s180s
TimeoutConfig.Dial10s5s15s5s5s
TimeoutConfig.TLSHandshake10s5s15s5s5s
TimeoutConfig.ResponseHeader0(禁用)10s0(禁用)0(禁用)0(禁用)
TimeoutConfig.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/Header 校验、TLS 证书验证和 SSRF 防护,仅限本地开发和测试使用。在非测试环境中调用时会输出安全警告。生产环境请使用 SecureConfig()DefaultConfig()

场景选型

场景推荐预设调整建议
通用 Web 服务Default
处理用户提供的 URLSecure
内部微服务高并发Performance按后端数量调大 MaxIdleConns
一次性脚本Minimal
文件下载服务Performance增大 MaxResponseBodySize
金融/医疗 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)

并发模型:一个 Client 服务所有 goroutine

HTTPC 的 ClientDomainClient 都是并发安全的——任意方法可以被多个 goroutine 同时调用,库内设有专门的并发安全集成测试(internal/concurrency)在高并发场景下覆盖公共 API。因此正确的并发模式非常简单:

全局/服务级创建 1 个 Client

        ├── goroutine 1 ──┐
        ├── goroutine 2 ──┼── 共享同一个连接池与对象池
        └── goroutine N ──┘

并发容量与连接池的关系:

场景行为
并发数 ≤ MaxConnsPerHost(HTTP/1.1)每个请求独占一条连接,互不排队
并发数 > MaxConnsPerHost(HTTP/1.1)多余请求在传输层排队等待空闲连接(不报错,但延迟上升)
HTTP/2 开启(默认)同一主机的请求共享单连接多路复用,MaxConnsPerHost 很少成为瓶颈

并发上限的两种调法

  • 控制客户端侧:把 Connection.MaxConnsPerHost 调到 ≥ 峰值并发数(HTTP/1.1 场景);
  • 控制调用侧:用带缓冲 channel 作信号量限制并发(下面的完整示例),主动保护下游服务。 两者常配合使用:信号量按下游承受力限流,连接池按信号量上限配连接。
go
package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"sync"
	"sync/atomic"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	// 本地模拟服务器:每个请求固定耗时 50ms
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(50 * time.Millisecond)
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := httpc.DefaultConfig()
	cfg.Security.AllowPrivateIPs = true // 允许连接 127.0.0.1 本地测试服务器
	client, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	const (
		total       = 20
		maxInFlight = 5 // 信号量:同时在途的请求最多 5 个
	)

	sem := make(chan struct{}, maxInFlight)
	var wg sync.WaitGroup
	var okCount int64
	start := time.Now()

	for i := 0; i < total; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			sem <- struct{}{}                // 获取信号量
			defer func() { <-sem }()         // 释放信号量

			result, err := client.Get(server.URL)
			if err != nil {
				return
			}
			if result.IsSuccess() {
				atomic.AddInt64(&okCount, 1)
			}
		}()
	}
	wg.Wait()

	fmt.Printf("%d/%d 成功,耗时 %v(串行约需 %v\n",
		okCount, total, time.Since(start), total*50*time.Millisecond)
	// 输出示例:20/20 成功,耗时约 250ms(串行约需 1s)——5 路并发带来约 5 倍吞吐
}

批量拉取大量独立 URL 时,另一个常用模式是 worker pool:固定数量的 worker goroutine 从 jobs channel 消费任务,天然把并发压在 worker 数上,无需信号量。完整实现见高级示例

连接池调优原理

连接池是 HTTP 客户端性能的核心。HTTPC 的连接池基于 Go 标准库的 http.Transport,但在其上增加了自动计算逻辑和安全默认值。

空闲连接自动计算

MaxIdleConnsPerHost(每主机空闲连接上限)无需手动设置——HTTPC 根据 MaxConnsPerHost 自动推导:

空闲连接数 = MaxConnsPerHost / 2,限制在 [2, 10] 区间

具体规则(calculateIdleConnsPerHost):

MaxConnsPerHost自动空闲连接数说明
0(无限制)10使用上限默认值
11先取下限 2,再被「不超过最大连接数」拉回 1
22恰好等于下限
52一半取下限
105Default 预设
2010Performance 预设,取上限
10010超出上限取 10

为什么是 MaxConnsPerHost / 2

空闲连接是「连接的缓存」——已建立但暂时未使用的连接。设为最大连接数的一半,在「复用已有连接」(命中缓存)和「新建连接」(缓存未命中时需重新握手)之间取得平衡,避免空闲连接过多占用服务端资源。

TCP Keep-Alive

HTTPC 的连接池固定使用 30 秒 TCP keep-alive 间隔(defaultKeepAlive = 30 * time.Second)。这个值在连接建立后由操作系统周期性发送 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),提供三大性能提升:

特性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 携带三个嵌套结构体:RequestInfo(请求信息)、ResponseInfo(响应信息)、RequestMeta(元数据如耗时)。传统做法需要为 Result 和三个嵌套结构体分别分配——四次堆分配。HTTPC 将它们打包进一个 resultBundle,一次堆分配搞定全部:

传统方式:4 次独立分配(Result + RequestInfo + ResponseInfo + RequestMeta)
HTTPC:1 次分配(resultBundle),Result 的三个指针指向同一块内存

调用方拿到的是 *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 回收。两者互补,各取所长。

低分配热路径

除对象池外,请求热路径上还有一批针对性去分配优化:

优化点机制
头部深拷贝批量分配CloneHeader 先统计全部值数量,一次分配共享底层数组——把「每个头部一次分配(N 次)」降为 1 次
查询参数转义零分配无需转义的字符串原样返回(零分配);需要转义时用池化缓冲区逐字节写出
数值查询参数直写int/float64/bool 等数值经 strconv.Append* 直接写入构建器,不产生中间字符串
请求头所有权转移普通请求与下载路径把引擎 Response 上的 header map 所有权转移Result,而非克隆一份
重定向链内联数组前 8 次重定向记录在池化对象的内联定长数组中,超过 8 次才分配溢出切片——绝大多数请求不为重定向链额外分配
重试休眠定时器复用重试退避用的 time.Timer 池化复用,高频重试场景避免反复建 Timer
池容量防护超过阈值的对象不归还池(如 header map > 64 条、query builder 容量 > 4096),防止大对象长期滞留池中撑大内存

内部指标与健康度

引擎内部以纯原子操作(无锁)收集每请求指标:总请求数、成功/失败数,以及用滑动平均公式 新平均 = (旧平均×9 + 本次延迟) / 10 维护的平滑延迟;错误率低于 10% 视为健康。这些指标用于引擎自身的健康判断,不作为公开 API 暴露——应用层的请求指标请用 MetricsMiddleware(见中间件),它按方法/URL/状态码/耗时回调,可直接对接 Prometheus 等监控系统。

你无需关心这些

以上优化对调用方完全透明。你只需要正常使用 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)
    }

    // 热路径中优先用 RawBody() 而非 Body()
    // RawBody() 返回原始字节切片;Body() 返回预存字符串;String() 是调试格式化(开销最大)
    data := result.RawBody()
    fmt.Printf("响应大小: %d 字节\n", len(data))
    fmt.Printf("请求耗时: %v\n", result.Meta.Duration)
}

工作负载调优示例

超时预算

四个传输层超时(DialTLSHandshakeResponseHeader、隐含的 body 传输)共同受 Timeouts.Request 这个总预算约束。调整预设时保持「分项之和 ≤ 总预算」的层级关系,避免出现「拨号超时比总超时还长」这类无效配置:

Timeouts.Request(总预算,默认 180s)
 ├── Timeouts.Dial          拨号(默认 10s)
 ├── Timeouts.TLSHandshake  TLS 握手(默认 10s)
 ├── Timeouts.ResponseHeader 响应头等待(Default/Performance 为 0=不设传输层限制)
 └── 响应体传输             剩余时间全部可用
工作负载RequestDial/TLS说明
内网微服务5–10s1–2s快速失败,把错误交给上游重试/熔断
公网 API30s5s兼顾跨网延迟与偶发慢响应
AI/长任务300s+10s长响应体占满剩余预算
下载大文件0(用 context 控制)15sDownload 的 ctx 管总时长,WithTimeout 管单请求

ResponseHeader 与 WithTimeout 的相互作用

Default/Performance 预设把 ResponseHeader 设为 0(不在传输层强制),使 WithTimeout() 对长响应拥有完全控制权;Secure 预设设为 10s 用于对抗 slowloris 类攻击。若你手动调窄 ResponseHeader,注意它可能先于 WithTimeout 截断慢响应。

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

TimeoutConfig.ResponseHeader = 0 表示不在传输层强制响应头超时,由 context 级超时(TimeoutConfig.RequestWithTimeout)统一控制。这确保 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))
}

大文件下载(流式)

大文件下载请使用 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)
}

不要对普通请求方法使用 WithStreamBody

WithStreamBody(true) 只对 Download 这类直接消费引擎响应的路径有效。对 Get/Post/Request 等普通方法设置它,响应体仍会被完整读入 Result 后关闭底层流——返回的 Result 请求体为空,且调用方拿不到流。消费大响应体的正确入口就是 Download(详见文件上传与下载)。

爬虫与代理池

爬虫场景使用代理池轮换 IP,HTTPC 会自动提高重试次数确保每个代理至少尝试一次(详见 重试与容错):

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 按并发数调
对普通请求用 WithStreamBody返回的 Result 请求体为空且拿不到流大响应体走 Download
关闭 HTTP/2退化为 HTTP/1.1 串行请求默认保持开启
忽略 Close()连接泄漏defer client.Close()
全局共享后忘记复用反复创建/销毁 Client一次创建、长期持有
用 goroutine 数硬扛限流打挂下游、触发 429/熔断信号量或 worker pool 控制在途请求数

Client 必须复用

HTTP 性能的根基是连接复用。每请求新建 Client 意味着每次都走 TCP 三次握手 + 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))
}

下一步