Skip to main content

Go 高并发实战(3):生产级 HTTP、RPC 与数据库并发

Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡
Rainy
8 MIN READ... VIEWS

Handler 只是入口;生产并发的真正边界位于连接池、下游 deadline、响应体和停机协议。

本期把前两期的容量与生命周期原则落到 net/http 服务,完成一个有界高扇出聚合 API。

架构图:数据面、控制面与诊断面

实线是业务数据面,429 是过载控制面,虚线是观测面。pprof 走独立受保护端口:业务端口饱和时仍能保存证据,同时不向公网暴露栈和运行信息。

一、三层 deadline

入口总 deadline 250 ms
├─ 准入等待 20 ms
├─ cache 30 ms
├─ database 80 ms
└─ RPC 120 ms

子 deadline 必须小于入口剩余时间。不能让每层各等 250 ms,也不能只设置 http.Server.WriteTimeout:server timeout 控制连接阶段,业务 Context 才能传播到 SQL/RPC。

二、入口准入与下游隔离

type Limiter chan struct{}

func (l Limiter) Acquire(ctx context.Context) bool {
select {
case l <- struct{}{}:
return true
case <-ctx.Done():
return false
}
}

func (l Limiter) Release() { <-l }

type App struct {
requests Limiter // 保护进程
database Limiter // 保护 DB
rpc Limiter // 保护 RPC 下游
httpClient *http.Client
db *sql.DB
}

每类依赖用独立 bulkhead。一个慢 RPC 不应耗尽数据库许可,低优先级导出任务也不应占满在线请求池。

三、聚合 Handler

func (a *App) Aggregate(w http.ResponseWriter, r *http.Request) {
admission, cancelAdmission := context.WithTimeout(r.Context(), 20*time.Millisecond)
defer cancelAdmission()
if !a.requests.Acquire(admission) {
w.Header().Set("Retry-After", "1")
http.Error(w, "overloaded", http.StatusTooManyRequests)
return
}
defer a.requests.Release()

ctx, cancel := context.WithTimeout(r.Context(), 250*time.Millisecond)
defer cancel()

type answer struct {
name string
value any
err error
}
answers := make(chan answer, 3)

go func() {
value, err := a.queryDatabase(ctx)
answers <- answer{name: "database", value: value, err: err}
}()
go func() {
value, err := a.callRPC(ctx)
answers <- answer{name: "rpc", value: value, err: err}
}()
go func() {
value, err := a.readCache(ctx)
answers <- answer{name: "cache", value: value, err: err}
}()

result := make(map[string]any, 3)
for range 3 {
select {
case answer := <-answers:
if answer.err != nil {
cancel()
http.Error(w, "dependency failed", http.StatusBadGateway)
return
}
result[answer.name] = answer.value
case <-ctx.Done():
http.Error(w, "deadline exceeded", http.StatusGatewayTimeout)
return
}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}

生产版还要定义“部分结果是否可接受”。可降级依赖失败时,不应和核心数据库失败采用相同策略;错误类型要映射成可监控的 result class。

四、HTTP Client 连接复用

transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 1024,
MaxIdleConnsPerHost: 256,
MaxConnsPerHost: 512,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ExpectContinueTimeout: time.Second,
ResponseHeaderTimeout: 150 * time.Millisecond,
}

client := &http.Client{Transport: transport}

复用全局 client/transport;每请求创建 client 会丢失连接复用并制造端口、TLS 和 GC 压力。Client.Timeout 是总上限,细粒度控制仍应来自 request Context 与 Transport 阶段超时。

响应必须关闭 body;若希望复用 HTTP/1.1 连接,还要按协议和大小限制正确读取/丢弃 body。永远不要无界读取不可信响应。

五、数据库连接池不是越大越好

db.SetMaxOpenConns(100)
db.SetMaxIdleConns(50)
db.SetConnMaxIdleTime(5 * time.Minute)
db.SetConnMaxLifetime(30 * time.Minute)

ctx, cancel := context.WithTimeout(parent, 80*time.Millisecond)
defer cancel()
rows, err := db.QueryContext(ctx, query, args...)

监控 DB.Stats()InUseWaitCountWaitDurationMaxIdleClosed。100 个副本各开 100 连接可能击穿数据库;总预算要由数据库容量反推到单实例。

事务期间不要做远程 RPC。事务占用一条连接,RPC P99 会直接转化为连接池饥饿和锁持有时间。

六、服务端边界与停机

server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}

root, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
<-root.Done()
shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = server.Shutdown(shutdown)
}()

Kubernetes/负载均衡环境的顺序应是:readiness 失败 → 等待摘流传播 → 停止接收新连接 → drain 在途请求 → 超时强退。只监听 SIGTERM 而不摘流,会在关闭窗口继续收到新请求。

七、请求生命周期的完整时间线

每一段都记录 queue wait 与 execution time。只有总 duration 时,无法区分“许可等了 70 ms、SQL 执行 10 ms”和“立即拿到连接、SQL 执行 80 ms”。

八、HTTP Transport 的隐藏并发细节

连接池设置需要按目标 host 分析:

  • MaxConnsPerHost 限制拨号中、活跃与 idle 连接总数;
  • MaxIdleConnsPerHost 太小会在流量毛刺后频繁重建连接;
  • HTTP/2 会在较少 TCP 连接上多路复用 stream,瓶颈可能变成 peer stream 限制;
  • 不读取/关闭响应体会破坏复用并耗尽连接;
  • DNS、dial、TLS、response header 都有各自的延迟分布。

使用 httptrace.ClientTrace 对单请求分解 DNS、connect、TLS 和 first-byte,避免把所有时间归咎于对方 handler。

trace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
metrics.ObserveConnReuse(info.Reused, info.WasIdle)
},
GotFirstResponseByte: func() {
metrics.ObserveTTFB(time.Since(started))
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

高采样 tracing 有开销,生产按错误、慢请求或低比例采样。

九、数据库事务与并发不变量

连接池等待、SQL 执行、行扫描和事务锁等待是四个不同阶段。实践规则:

  • BeginTxQueryContext/ExecContext 都传入同一请求树的 Context;
  • defer rows.Close(),循环后检查 rows.Err()
  • 事务尽量短,不在事务内等待 RPC、消息发布或用户输入;
  • 使用条件更新保护库存/余额,而非“先查再改”;
  • 重试事务时考虑隔离级别、死锁错误和幂等性。
UPDATE inventory
SET available = available - $1, version = version + 1
WHERE sku = $2 AND available >= $1 AND version = $3;

影响行数为 0 表示条件失败,应重新读取或返回冲突,而不是盲目覆盖。

十、响应写出后的错误边界

一旦写入 header/body,通常不能再把状态码改成 500。聚合响应应先在内存中构造到合理上限,再统一编码;流式响应则要定义中途错误协议。

保护输入/输出大小:MaxBytesReader 限制请求 body,io.LimitReader 限制下游,JSON decode 拒绝未知字段视 API 策略决定。高并发下单个超大 body 会迅速放大内存压力。

十一、生产配置审计

层级必查项失败指标
Listenerheader/read/write/idle timeoutaccept、TLS、slow client
Admission许可与等待预算rejected、queue wait
HTTP clienthost 连接池、阶段 timeoutdial/TLS/TTFB
Database总连接、idle、lifetimewait count/duration
Shutdown摘流、drain、强退deployment 5xx、unfinished

11.1 Deadline 如何逐层缩短

入口剩余 250 ms,计划给 DB 80 ms、RPC 120 ms、序列化 20 ms,并预留 30 ms。RPC 开始前如果已经消耗 70 ms,它不能忽略父 deadline 重新获得完整预算:

func withBudget(parent context.Context, maximum time.Duration) (context.Context, context.CancelFunc) {
if deadline, ok := parent.Deadline(); ok {
remaining := time.Until(deadline)
if remaining <= 0 {
return context.WithCancel(parent)
}
if remaining < maximum {
maximum = remaining
}
}
return context.WithTimeout(parent, maximum)
}

还应设置“是否值得开始”的最小预算:剩余不足 15 ms 时跳过 optional recommendation,而不是启动一个几乎必超时的 RPC。

11.2 从数据库总容量反推单实例连接

数据库允许 2,000 个总连接,预留 20% 给迁移、运维和其他服务;滚动发布最多同时存在 25 个实例:

应用可用总连接 = 2000 × 0.8 = 1600
单实例 MaxOpenConns = floor(1600 / 25) = 64

如果按稳定态 20 副本配置 80,滚动发布时会瞬间回到 2,000,挤掉保留容量。连接预算必须使用部署过程中的最大副本数。

十二、本期实战验收

  • 给入口、DB、cache、RPC 分别注入延迟和错误;
  • 确认任何子 deadline 不会超过入口剩余预算;
  • 压满一种下游时,其他依赖许可池仍可用;
  • 检查连接复用、DB wait、响应 body 和取消后的 in-flight;
  • 滚动停机期间不出现明显 5xx 峰值。

上一篇:Go-2:结构化并发与 Worker Pool · 下一篇:Go-4:背压、限流与系统韧性

参考资料

Logo
RainLib

Exploring the frontiers of technology, design, and distributed systems. Building tools for the future developers.

Suggestions & Feedback

© 2026 RainLib. Built for the Future.
All rights reserved.
System Normal