第一章:Go语言并发模型与网络性能的底层优势
Go 语言从设计之初就将高并发、低延迟网络服务作为核心场景,其并发模型与运行时系统深度协同,形成区别于传统线程模型的独特优势。
轻量级 Goroutine 与 M:N 调度器
Goroutine 是用户态协程,初始栈仅 2KB,可轻松创建百万级实例;Go 运行时通过 GMP 模型(Goroutine、OS Thread、Processor)实现 M:N 调度,由 runtime.scheduler 自动将就绪的 G 分配给空闲的 M 执行。相比 pthread 创建线程(通常需 MB 级内存与内核态切换开销),goroutine 的创建/销毁成本降低两个数量级以上。例如:
func spawnMany() {
for i := 0; i < 100_000; i++ {
go func(id int) {
// 每个 goroutine 占用极小栈空间,且阻塞 I/O 时自动让出 M
http.Get("https://example.com") // 非阻塞式系统调用封装
}(i)
}
}
该代码在常规服务器上可稳定运行,而等效的 pthread 实现极易触发 ENOMEM 或调度抖动。
非阻塞网络 I/O 与 netpoller 集成
Go 标准库 net 包底层不依赖 select/poll/epoll/kqueue 的用户态轮询循环,而是通过 runtime.netpoller 将文件描述符事件与 goroutine 生命周期绑定:当 conn.Read() 遇到 EAGAIN,当前 goroutine 被挂起并注册到 epoll/kqueue,待数据就绪后唤醒对应 G——整个过程无 OS 线程阻塞,M 可立即执行其他就绪任务。
内存安全与零拷贝潜力
GC 支持并发标记清除,避免 STW 时间过长;结合 unsafe.Slice(Go 1.17+)与 io.Reader 接口组合,可在 HTTP 处理中复用缓冲区,减少堆分配。典型高性能服务常采用如下模式:
- 复用
sync.Pool管理[]byte缓冲区 - 使用
http.Transport的MaxIdleConnsPerHost控制连接复用 - 启用
GODEBUG=netdns=go强制纯 Go DNS 解析(避免 cgo 阻塞 M)
| 特性 | POSIX 线程模型 | Go 并发模型 |
|---|---|---|
| 单实例内存开销 | ~1–8 MB | ~2–8 KB |
| 上下文切换成本 | 微秒级(内核态) | 纳秒级(用户态) |
| I/O 阻塞影响范围 | 整个线程挂起 | 仅当前 goroutine 挂起 |
| 网络连接承载能力 | 数千级(受限于线程) | 十万至百万级(受限于 FD) |
第二章:TCP栈调优的五大关键维度
2.1 TCP初始拥塞窗口(initcwnd)与慢启动行为实测对比
TCP连接建立初期的拥塞控制行为高度依赖initcwnd(初始拥塞窗口)配置。Linux内核默认initcwnd=10(RFC 6928推荐),但实际吞吐表现受RTT、丢包率及路径容量共同制约。
实测环境配置
# 查看并修改当前接口的initcwnd值
ip route show default | awk '{print $3}' | xargs -I{} ip route change default via {} initcwnd 3
# 验证设置
ip route show
该命令将默认路由的initcwnd设为3个MSS(通常≈4380字节),强制触发更保守的慢启动过程,便于观测早期数据包序列。
慢启动阶段行为差异
| initcwnd | 首RTT发送包数 | 达到cwnd=20所需RTT数 | 适用场景 |
|---|---|---|---|
| 3 | 3 | 4 | 高丢包/低带宽链路 |
| 10 | 10 | 2 | 数据中心短RTT网络 |
拥塞窗口增长逻辑
graph TD
A[SYN-ACK] --> B[initcwnd=3]
B --> C[cwnd=3 → 发送3*1460B]
C --> D[收到3个ACK] --> E[cwnd=6]
E --> F[下一轮发送6包] --> G[指数增长至ssthresh]
调整initcwnd本质是在“快速填充管道”与“避免早期内部丢包”之间权衡。过高值在弱网中引发重传放大,过低则浪费带宽资源。
2.2 TIME_WAIT状态复用与net.ipv4.tcp_tw_reuse调优的边界条件验证
tcp_tw_reuse 并非万能开关,其生效需满足严格时序约束:仅对客户端主动发起连接(即 connect())且目标套接字处于 TIME_WAIT 状态时,才允许复用;服务端 bind()+listen() 场景完全不适用。
触发前提验证
- 必须启用
net.ipv4.tcp_timestamps = 1(PAWS 机制依赖) - 目标
TIME_WAIT套接字需满足:tw_ts_recent + TCP_PAWS_MSL(60s) < now
# 查看当前配置与状态
sysctl net.ipv4.tcp_tw_reuse net.ipv4.tcp_timestamps
ss -tan state time-wait | head -3
逻辑分析:
tcp_tw_reuse=1仅在connect()路径中触发inet_csk_get_port()的tw reuse分支;若tcp_timestamps=0,PAWS 无法校验时间戳单调性,复用将被拒绝——这是硬性边界。
典型失效场景对比
| 场景 | 是否触发复用 | 原因 |
|---|---|---|
| 客户端短连接高频重连 | ✅ | 满足 timestamp + MSL 条件 |
| Nginx 作为反向代理出向连接 | ✅ | 属于客户端角色 |
| Tomcat 端口监听新连接 | ❌ | bind() 不走复用逻辑 |
graph TD
A[connect()] --> B{tcp_tw_reuse == 1?}
B -->|Yes| C{tcp_timestamps == 1?}
C -->|No| D[Reject: PAWS disabled]
C -->|Yes| E{find_timewait_port?}
E -->|Yes| F{tw_ts_recent + 60s < now?}
F -->|Yes| G[Reuse success]
F -->|No| H[Skip: too recent]
2.3 SO_REUSEPORT在多核负载均衡中的内核路径穿透分析与压测数据
SO_REUSEPORT 允许多个 socket 绑定同一端口,由内核在 __inet_lookup_listener() 中基于四元组哈希 + CPU ID 映射实现无锁分发。
内核关键路径节选
// net/ipv4/inet_connection_sock.c
if (sk->sk_reuseport && sk->sk_family == AF_INET) {
hash = inet_ehashfn(net, daddr, dport, saddr, sport);
idx = hash & (so->sk_reuseport_cb->num_socks - 1); // 哈希桶索引
sk = reuseport_select_sock(sk, hash, &iph, &hdr); // per-CPU 负载感知选择
}
该逻辑绕过传统 accept 队列竞争,使每个监听 socket 独立运行于不同 CPU 核心,避免 accept() 系统调用争用。
压测对比(16核机器,10K并发短连接)
| 场景 | QPS | 平均延迟 | CPU sys% |
|---|---|---|---|
| 单 listen socket | 48k | 3.2ms | 82% |
| SO_REUSEPORT × 16 | 112k | 1.1ms | 47% |
负载分发流程
graph TD
A[SYN到达] --> B{__inet_lookup_listener}
B --> C[计算四元组哈希]
C --> D[取模定位reuseport组]
D --> E[根据当前CPU ID选择socket]
E --> F[直接入对应sk->sk_receive_queue]
2.4 TCP快速重传与SACK机制在高丢包率场景下的延迟收敛实验
在模拟丢包率15%的弱网环境中,对比标准TCP Reno与启用SACK的TCP NewReno表现:
实验配置关键参数
- RTT基准:100ms(双程)
- 接收窗口:64KB
- 发送端启用
tcp_sack=1与tcp_fack=0
SACK报文解析示例
# 抓包中SACK块字段(RFC 2018格式)
0x0000: 4500 003c 0000 4000 4006 0000 c0a8 0101 E..<..@.@.......
0x0020: c0a8 0102 0015 0015 0000 0001 0000 0001 ................
0x0030: 5012 2000 fe7a 0000 0101 080a 000a 2c9b P. ..z........,.
0x0040: 000a 2c9b 0101 0402 0000 0000 0000 0000 ..,.............
# SACK块位于选项区:0x0101 0402 → 左边界=0x00000000, 右边界=0x00000002
该SACK块明确告知发送方已收到字节0–1,但缺失2–3,驱动发送端仅重传[2,3]区间,避免盲目重发整个窗口。
延迟收敛对比(单位:ms)
| 机制 | 平均重传延迟 | 乱序恢复轮次 |
|---|---|---|
| TCP Reno | 328 | 4.7 |
| TCP + SACK | 142 | 1.3 |
恢复逻辑流程
graph TD
A[检测3个重复ACK] --> B{是否启用SACK?}
B -->|否| C[触发快速重传整个窗口]
B -->|是| D[解析SACK块]
D --> E[定位首个空洞起始]
E --> F[仅重传该段]
2.5 Nagle算法与TCP_NODELAY协同失效案例:从Wireshark抓包到Go writev优化落地
数据同步机制
当高频率小包写入(如实时日志推送)启用 TCP_NODELAY 后,仍出现 200ms 级延迟——Wireshark 显示 PSH+ACK 间隔异常。根源在于:Nagle 算法未被完全绕过,因内核在 write() 调用间存在微秒级缓冲合并窗口,而 Go 的 conn.Write([]byte) 默认单次 syscall,无法触发底层 writev 批量提交。
Go 优化实践
// 使用 writev 替代连续 Write,显式聚合小包
n, err := syscall.Writev(int(conn.(*net.TCPConn).Fd()), []syscall.Iovec{
{Base: &buf1[0], Len: len(buf1)}, // 日志头
{Base: &buf2[0], Len: len(buf2)}, // JSON body
})
// 参数说明:Base 必须为切片底层数组首地址;Len 为有效字节长度;系统调用零拷贝入内核 SKB 队列
关键对比
| 场景 | 平均延迟 | 是否触发 Nagle | 抓包帧数/秒 |
|---|---|---|---|
连续 Write() |
182 ms | 是(部分) | ~55 |
Writev 批量提交 |
9 ms | 否 | ~1100 |
graph TD
A[Go 应用 write] --> B{单次 Write?}
B -->|是| C[进入 socket 写队列 → 可能等待 ACK]
B -->|否| D[writev 原子提交 → 绕过 Nagle 判定路径]
D --> E[TCP 层直接封装发送]
第三章:epoll封装层的三大隐性开销陷阱
3.1 runtime.netpoll与epoll_wait超时精度丢失导致的伪饥饿问题复现
Go 运行时通过 runtime.netpoll 封装 epoll_wait 实现 I/O 多路复用,但其超时参数经 int64 → int32 截断后,高精度纳秒级定时器在毫秒级转换中发生精度丢失。
精度截断关键路径
// src/runtime/netpoll_epoll.go
func netpoll(delay int64) gList {
var ts timespec
if delay < 0 {
ts.tv_sec, ts.tv_nsec = 0, 0 // indefinite
} else {
// ⚠️ 问题点:delay(ns) → ms → int32 → 可能溢出或截断
ms := nanotimeToMS(delay) // 内部使用 int32 转换
ts.tv_sec = int64(ms / 1000)
ts.tv_nsec = int64((ms % 1000) * 1e6)
}
// ...
}
nanotimeToMS 将纳秒转毫秒时先除 1e6 再强转 int32,当 delay > ~2.15s(即 int32 最大值 2147483 ms),结果被截断为负数,触发 epoll_wait 立即返回空就绪列表,造成 goroutine 频繁轮询——即“伪饥饿”。
典型表现对比
| 场景 | epoll_wait 超时值 | 行为 |
|---|---|---|
delay = 2_147_483_000ns (≈2.147s) |
2147 ms |
正常阻塞 |
delay = 2_147_484_000ns (≈2.1475s) |
-2147 ms(截断) |
立即返回,无事件 |
伪饥饿触发链
graph TD
A[netpoll(delay)] --> B[nanotimeToMS delay]
B --> C[int64 → int32 截断]
C --> D[负超时传入 epoll_wait]
D --> E[立即返回 0 就绪 fd]
E --> F[goroutine 认为无事可做,快速重调 netpoll]
3.2 fd事件注册/注销频次与epoll_ctl系统调用放大效应量化建模
当应用频繁增删同一fd的事件(如反复 EPOLLIN ↔ EPOLLOUT 切换),epoll_ctl(EPOLL_CTL_MOD) 调用会触发内核重计算就绪链表与红黑树节点状态,造成隐式开销放大。
核心放大因子
- 每次
epoll_ctl平均耗时 ≈O(log n) + O(k),其中n为 epoll 实例中总 fd 数,k为该 fd 关联的就绪事件数; - 高频 MOD 操作使
k在就绪队列中反复进出,引发缓存失效与锁竞争。
典型误用模式
// ❌ 危险:在循环中无条件切换事件类型
for (int i = 0; i < 1000; i++) {
struct epoll_event ev = {.events = (i % 2) ? EPOLLIN : EPOLLOUT, .data.fd = sockfd};
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, sockfd, &ev); // 每次都触发完整状态重校验
}
逻辑分析:
EPOLL_CTL_MOD强制内核遍历就绪队列查找并移除旧事件,再按新ev.events重新评估就绪性。即使 fd 状态未变,仍执行rb_erase()+rb_insert()+ 就绪标记刷新,开销远超EPOLL_CTL_ADD/DEL的静态操作。
放大效应量化对比(单fd,10k次操作)
| 操作类型 | 平均延迟(ns) | 内核路径深度 | 缓存行失效次数 |
|---|---|---|---|
EPOLL_CTL_ADD |
850 | 3 | 1 |
EPOLL_CTL_MOD |
2400 | 7 | 3.2 |
EPOLL_CTL_DEL |
620 | 2 | 1 |
graph TD
A[用户调用 epoll_ctl MOD] --> B[查找红黑树中对应 epitem]
B --> C[从就绪链表摘除旧事件]
C --> D[根据新 events 重新评估就绪态]
D --> E[必要时插入就绪链表/更新 rb_node]
E --> F[唤醒等待进程]
3.3 epoll_wait返回就绪列表拷贝开销在百万连接下的内存带宽瓶颈实测
数据同步机制
epoll_wait() 每次返回时需将内核就绪队列(struct epitem 链表)线性拷贝至用户态 struct epoll_event[] 数组,该拷贝由 copy_to_user() 完成,路径为:
// kernel/events/epoll.c(简化逻辑)
for (i = 0; i < maxevents && !list_empty(&txlist); i++) {
struct epitem *epi = list_first_entry(&txlist, struct epitem, rdllink);
// ⬇️ 关键拷贝:每个就绪事件触发一次 cache line 级写入
if (copy_to_user(&events[i], &epi->event, sizeof(epi->event)))
return -EFAULT;
}
→ 每个 epoll_event 占 12 字节(uint32_t events + int data.fd + pad),百万就绪事件即 12 MB 连续内存写入,直击 DDR4-3200 带宽上限(~25 GB/s)的局部瓶颈。
性能对比(单次调用,100 万连接,1% 就绪率)
| 就绪数 | 拷贝数据量 | L3 缓存未命中率 | 平均延迟 |
|---|---|---|---|
| 10,000 | 120 KB | 12% | 8.3 μs |
| 100,000 | 1.2 MB | 67% | 92 μs |
| 500,000 | 6 MB | 94% | 410 μs |
优化路径示意
graph TD
A[epoll_wait 调用] --> B{就绪事件数量}
B -->|≤ 1K| C[cache-friendly 拷贝]
B -->|> 10K| D[DRAM 带宽饱和]
D --> E[零拷贝提案:io_uring 提前注册 event ring]
第四章:net.Conn底层复用失效的四重根源剖析
4.1 conn.readLoop与conn.writeLoop goroutine泄漏导致fd复用中断的pprof追踪链
当连接未被显式关闭,readLoop 和 writeLoop goroutine 持续阻塞在 conn.Read() / conn.Write() 上,无法退出,导致底层文件描述符(fd)无法释放。
pprof定位关键路径
通过 go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2 可观察到大量 runtime.gopark 状态的 goroutine,堆栈指向 net/http.(*conn).readLoop。
典型泄漏代码片段
func (c *conn) readLoop() {
defer c.close() // ❌ close() 未执行:defer 在 panic 或死锁时可能不触发
for {
n, err := c.rwc.Read(c.buf[:])
if err != nil {
return // 正常退出路径
}
// ... 处理逻辑
}
}
逻辑分析:若
c.rwc.Read永久阻塞(如客户端静默断连但 TCP FIN 未到达),goroutine 永驻内存;defer c.close()不执行 → fd 不归还至netFD池 → 后续连接复用失败。
关键诊断指标
| 指标 | 正常值 | 异常表现 |
|---|---|---|
net.Conn 活跃数 |
~QPS × 超时窗口 | 持续增长不回落 |
goroutine 总数 |
> 10k 且 readLoop 占比 > 60% |
graph TD
A[HTTP 请求建立] --> B[启动 readLoop/writeLoop]
B --> C{连接是否正常关闭?}
C -->|是| D[fd 归还 runtime]
C -->|否| E[goroutine 泄漏 → fd 耗尽]
E --> F[accept 返回 EMFILE]
4.2 http.Transport.MaxIdleConnsPerHost配置与底层connPool竞争锁的CPU缓存行伪共享实证
http.Transport 的 MaxIdleConnsPerHost 控制每主机空闲连接上限,其背后由 idleConn 池(p.idleConn)管理,而该池通过 mu sync.Mutex 保护——此锁位于 p.connPool 结构体首部,与高频读写的 idleConn slice 紧邻。
伪共享热点定位
type connPool struct {
mu sync.Mutex // 缓存行起始地址:0x00
idleConn []*persistConn // 紧随其后,通常从0x40开始(64字节对齐)
// ... 其他字段
}
当多 goroutine 并发调用 getConn() 时,mu 锁争用触发 CPU 缓存行(64B)在核心间反复无效化,即使 idleConn 未被修改。
性能影响对比(16核机器,压测 5k QPS)
| 配置 | P99 延迟 | Mutex Contention/sec |
|---|---|---|
MaxIdleConnsPerHost=100 |
18ms | 12,400 |
MaxIdleConnsPerHost=2 |
3ms | 890 |
优化路径
- 使用
sync.Pool替代全局锁池(Go 1.22+ 实验性支持) - 将
mu与idleConn至少间隔 128 字节(_ [128]byte填充)
graph TD
A[goroutine A] -->|acquire mu| B[Cache Line 0x00-0x3F]
C[goroutine B] -->|acquire mu| B
B --> D[Invalidates line on core X]
B --> E[Invalidates line on core Y]
4.3 TLS握手后net.Conn未归还至sync.Pool的GC压力传导路径分析(含go tool trace火焰图)
核心问题定位
当tls.Conn完成握手后未调用putConn()归还至http.Transport.IdleConnTimeout关联的sync.Pool,导致底层net.Conn持续驻留堆上,触发高频对象分配与GC扫描。
压力传导链路
// transport.go 中典型的归还遗漏点(错误示例)
func (t *Transport) putIdleConn(pconn *persistConn) {
if t.IdleConnTimeout == 0 {
return // ❌ 此处未触发 pool.Put,conn 永久泄漏
}
t.idleConnPool.Put(pconn.conn) // ✅ 正确路径需确保执行
}
该分支跳过sync.Pool.Put(),使*net.TCPConn无法复用,每次新请求都新建runtime.mspan,加剧堆碎片。
关键指标对比
| 场景 | GC Pause (ms) | Heap Alloc Rate (MB/s) | sync.Pool Hit Rate |
|---|---|---|---|
| 正常归还 | 0.8 | 12.3 | 92% |
| 归还遗漏 | 4.7 | 89.6 | 31% |
火焰图诊断线索
graph TD
A[goroutine tls.Conn.Handshake] --> B[http.Transport.roundTrip]
B --> C{IdleConnTimeout > 0?}
C -- No --> D[conn leaked → heap growth]
C -- Yes --> E[sync.Pool.Put called]
D --> F[GC mark phase scans conn + buffers]
此路径在go tool trace中表现为runtime.gcMarkWorker持续高占比,且net/http.persistConn.readLoop goroutine 数量线性增长。
4.4 自定义net.Conn包装器绕过io.ReadWriteCloser接口导致pool.Put被跳过的静态检查与运行时拦截方案
当自定义 net.Conn 包装器仅嵌入 io.Reader 和 io.Writer 而故意不实现 io.Closer 时,sync.Pool 的 Put 调用会被静态类型检查忽略——因 *wrappedConn 不满足 io.ReadWriteCloser 接口,无法传入期望该接口的回收逻辑。
核心漏洞路径
type wrappedConn struct {
net.Conn // 仅嵌入,未重写 Close()
}
// ❌ 编译期无报错,但 pool.Put(conn) 因类型不匹配被静默跳过
此处
wrappedConn表面兼容net.Conn(含Close()),但若其字段net.Conn为nil或被覆盖为无Close实现的类型,则运行时conn.Close()panic,且pool.Put因接口断言失败而跳过回收。
运行时防护策略
- 使用
reflect.TypeOf(conn).Implements(reflect.TypeOf((*io.ReadWriteCloser)(nil)).Elem().Type())动态校验; - 在
Put前插入defer func() { if !isRWClose(conn) { log.Warn("skipped Put: missing io.Closer") } }();
| 检查方式 | 静态检查 | 运行时拦截 | 覆盖率 |
|---|---|---|---|
go vet |
✅ | ❌ | 低 |
interface{} 类型断言 |
❌ | ✅ | 高 |
graph TD
A[conn passed to pool.Put] --> B{Implements io.ReadWriteCloser?}
B -->|Yes| C[Execute Put]
B -->|No| D[Log warning + skip]
第五章:高性能Go服务的工程化演进范式
构建可观测性驱动的发布闭环
在某千万级日活的实时消息中台项目中,团队将 OpenTelemetry SDK 深度集成至 Gin 中间件与 GORM 钩子中,统一采集 HTTP 延迟、DB 查询耗时、Redis 调用成功率三类核心指标。通过 Prometheus 抓取 + Grafana 看板联动告警(如 P99 延迟突增 > 200ms 持续 3 分钟触发 PagerDuty),结合 Argo Rollouts 的金丝雀发布策略,实现每次版本灰度期间自动暂停发布——当新 Pod 的错误率超过基线 0.5% 或延迟升高 30%,Rollout 自动回滚并保留现场日志快照。该机制上线后,线上 P0 故障平均恢复时间(MTTR)从 18 分钟降至 2.3 分钟。
基于 eBPF 的零侵入性能诊断体系
放弃传统 pprof 手动注入方式,在 Kubernetes 集群中部署 bpftrace + Pixie 联动方案。以下为捕获 Go runtime GC STW 时间的 eBPF 脚本片段:
# trace-gc-stw.bt
BEGIN { printf("Tracing GC STW... Hit Ctrl-C to end.\n") }
uprobe:/usr/local/go/bin/go:runtime.gcStart {
@start[tid] = nsecs;
}
uretprobe:/usr/local/go/bin/go:runtime.gcDone {
$dur = nsecs - @start[tid];
@stw_us = hist($dur / 1000);
delete(@start, tid);
}
配合 Pixie 自动生成的火焰图,工程师可在 10 秒内定位到某次发布后 STW 从 1.2ms 激增至 47ms 的根因:第三方 gjson 库未复用 []byte 缓冲区,导致频繁堆分配触发高频 GC。
多维度容量治理看板
采用“请求量-资源消耗-业务水位”三维建模法,构建容量健康度评分卡:
| 维度 | 指标项 | 健康阈值 | 当前值 | 状态 |
|---|---|---|---|---|
| 请求负载 | QPS(5min avg) | ≤ 8500 | 9230 | ⚠️ |
| 资源瓶颈 | CPU 使用率(节点级) | ≤ 65% | 78% | ❌ |
| 业务弹性 | 平均处理耗时增长比 | ≤ 1.15×基线 | 1.32× | ❌ |
当三项中任两项进入非健康状态,自动触发 kubectl scale deployment --replicas=12 并向 SRE 群推送容量扩容建议。
协议层渐进式升级路径
为兼容存量 HTTP/1.1 客户端与新增 gRPC-Web 流式调用,在 Envoy Ingress 中配置双协议路由规则,同时启用 grpc_stats 过滤器统计流控效果。实测表明:在 12k 并发下,HTTP/1.1 接口平均延迟 89ms,而 gRPC-Web 流式接口延迟降至 23ms,且连接复用率提升至 99.7%。
工程效能度量反哺架构决策
建立 CI/CD 流水线黄金指标看板:构建失败率(目标 go.etcd.io/etcd/client/v3 升级至 v3.5.12 并重构重试逻辑。
混沌工程常态化验证机制
每周四凌晨 2:00,Chaos Mesh 自动注入网络延迟(+150ms jitter ±30ms)与内存压力(占用 40% 容器内存)组合故障,持续 15 分钟。所有测试用例必须通过 go test -race -timeout=30s ./... 且熔断器在 800ms 内完成降级。2024 年已拦截 7 类潜在雪崩场景,包括 Redis 连接池耗尽未触发 fallback、下游超时未传递 context deadline 等。
