gRPC HTTP/2 长连接被 AWS NLB 350 秒 idle timeout 悄悄 RST 的 5 天复盘:每天 1842 次 connection reset 噪音清零 + 三端 keepalive 协调纪律落地

推荐服务 order-service 调 inventory-service 每天 1842 次 connection reset 全部集中在低 QPS 时段,5 天复盘根因是 AWS NLB 350 秒 idle timeout + gRPC 默认不发 keepalive + HTTP/2 多路复用三层叠加,最终落地 client/server/LB 三端协调的 keepalive 工程纪律 + 5 个 Prometheus 指标 + 决策树,connection reset 数量归零,P99.9 抖动率从 1.2% 降到 0.08%。

2026 年 2 月底某个周一上午,平台组 SRE 在群里贴了一组奇怪的告警:"过去 24 小时 order-serviceinventory-service 累计出现 1842 次 'connection reset by peer' 错误,每 5 分钟一波,持续 1-2 秒。但下游服务自身指标完全正常,没有重启、没有崩溃、没有 OOM。"这种"每 5 分钟一波"的规律性非常可疑——任何"周期性"现象在分布式系统里都不是巧合,通常背后有某个 timer 在作怪。

5 天后定位的根因让人哭笑不得:我们的 K8s 集群前面的云厂商负载均衡器(LB)默认 idle timeout 是 5 分钟,任何 5 分钟没流量的 TCP 连接都被 LB 主动 RST。order-service 用 gRPC 调 inventory-service,gRPC 默认会复用 HTTP/2 长连接;在低 QPS 时段(早 4 点到 7 点)调用稀疏,连接就会"空闲超过 5 分钟",LB 把它 RST 掉。但 gRPC client 没配 keepalive ping,不知道连接已死,继续用——下次发请求就拿到 connection reset。这篇是完整复盘,涵盖 gRPC HTTP/2 长连接的工作机制、keepalive 配置、LB / 防火墙 / NAT 的 idle timeout 现象、3 端协调配置的方法、以及落地的《gRPC 长连接稳定性纪律》。

背景:这个看起来普通的 gRPC 微服务调用

维度 数值
架构 Go 微服务集群,内部全部用 gRPC HTTP/2 通信
规模 56 个微服务,日均 RPC 调用 200 亿次
关键路径 order-service → inventory-service(查库存)
K8s 1.28,16 Pod 一服务
LB 云厂商 NLB(L4)+ K8s Service,idle timeout 默认 5 分钟
事故现象 每 5 分钟一波 connection reset,集中在低 QPS 时段
影响 每天 1800+ 次单次请求失败,有重试机制所以业务无感但客服监控有数据

"业务无感"是这次问题被忽视很久的原因——重试一次就能成功,客户基本看不出来。但每天 1800 次 connection reset 意味着:

  • 1800 次毫无意义的重试,放大下游负载
  • 错误日志噪音,真正的 connection issue 被淹没
  • P99.9 不稳定(碰上 reset + 重试的请求总有 ~ 100ms 额外延迟)
  • 定时炸弹:如果某天 LB idle timeout 缩短 / gRPC 重试策略调整,可能直接演变成可见事故

事故时间线:从规律性告警到根因的 5 天

时刻 事件
02-23 周一上午 SRE 注意到"每 5 分钟一波"的规律,我加入排查
02-23 下午 抓客户端和服务端的 tcpdump,看到 LB 端发起 FIN + RST,且时间精准在"上次流量后 5 分钟"
02-24 查云厂商 NLB 文档,确认默认 idle timeout 350 秒(约 5 分 50 秒)
02-25 研究 gRPC keepalive 机制 + HTTP/2 PING frame + Go grpc-go 客户端默认行为
02-26 设计三端协调方案:client keepalive 2 分钟 + server permit + LB 不变
02-27 预发测试 + 灰度上线
03-02 全量上线,7 天观察 connection reset 完全归零

第一反应:"是不是下游服务有 bug 偶发 reset"

当看到 "connection reset by peer" 时,所有人的本能反应是"对方(peer)出问题了"——下游服务崩溃、重启、内存溢出、网络问题。这个直觉在 90% 场景对,但在LB / NAT / 防火墙参与的链路里要警惕——这些中间设备也可能主动 RST 连接。

判断"reset 来自谁"的关键是 tcpdump。我们在 inventory-service Pod 抓包:

tcpdump -i any -nn -w inv.pcap port 50051

# 用 Wireshark 打开, 过滤 tcp.flags.reset==1
# 看到 RST 包的 source IP

# 出现 RST 的几种情况:
# 1. source IP = client Pod IP: 客户端发 RST
# 2. source IP = server Pod IP: 服务端发 RST
# 3. source IP = LB IP / VIP: LB 发 RST(就是我们的情况)

我们抓到的 RST 包源 IP 是 NLB 的 VIP,这直接锁定到 LB 是 "killer"——下游服务完全无辜。然后再看 RST 的精确时间,和该连接"上次有流量"的时间差,固定 350 秒——LB 的 idle timeout 现身了。

三层叠加的因果链:为什么"业务无感的小问题"其实是定时炸弹

这张图最关键的信息是三个因素互相放大:gRPC 默认不发 keepalive / LB 默认主动断空闲连接 / HTTP/2 多路复用让单连接故障影响 N 个并发请求。任何一个单独存在都不致命,叠加就让"看似无感的偶发错误"变成"每天 1800+ 次重试 + 错误日志噪音 + 定时炸弹"。我们后来内部叫这种问题"长连接中间设备反模式",任何一项 gRPC / 长连接事故复盘都强制画一张这种因果图,确保不会"看见错误能重试就放过它"——可重试不代表无成本,1800 次重试每天放大下游负载 + 淹没真正问题的日志噪音,长期看是不可承受的工程债。

真凶 1:云厂商 LB 的 idle timeout

所有主流云厂商的 LB 都有 "idle connection timeout"——TCP 连接空闲超过此时间自动 close 或 RST。默认值各家不同:

云厂商 / LB 默认 idle timeout 可配置范围
AWS NLB 350 秒 不可改(固定)
AWS ALB 60 秒 1 ~ 4000 秒
GCP TCP Load Balancer 600 秒 1 ~ 28800 秒
Azure Load Balancer 4 分钟 4 ~ 30 分钟
阿里云 SLB 900 秒(TCP) 10 ~ 900 秒
K8s kube-proxy iptables 2 小时(conntrack) 可改

看到 AWS NLB 的 350 秒固定值——这是我们用的产品,这就是事故根因之一。AWS 文档明确说 NLB idle timeout 不可改(这是产品设计),所以应用层必须配合做"保活"。

真凶 2:gRPC 默认不发 keepalive ping

gRPC 基于 HTTP/2,HTTP/2 协议本身支持 PING frame——可以在空闲时发 PING,保持连接活跃。但 gRPC 客户端默认不发 keepalive(除非显式配置):

语言 默认 keepalive
Go grpc-go 默认禁用(必须显式配 KeepaliveParams)
Java grpc-java 默认禁用
Python grpcio 默认禁用
C++ grpc-cpp 默认禁用
Node.js @grpc/grpc-js 默认禁用

"默认禁用"的设计哲学是"避免无意义的 PING 浪费带宽"——对短链接场景是合理的。但对长连接 + LB 场景,这是个隐藏的坑——客户端不知道 LB 会断连,服务跑起来才发现。

真凶 3:HTTP/2 长连接复用让问题放大

gRPC 默认会复用同一个 HTTP/2 连接发送多个并发 RPC(多路复用)。这是 HTTP/2 的核心优势——但是意味着一个连接断了影响所有正在用它的 RPC。在低 QPS 时段,所有 order-service 实例 to inventory-service 的流量可能只用 1-3 个 HTTP/2 连接,LB 一断,所有积压的请求一起失败。

HTTP/1.1 时代,每个请求一个连接(或最多 6 个池化连接),即使 LB 断了一个连接,其他还能用。HTTP/2 多路复用把"连接失败"的影响放大了 N 倍

修法:三端协调 + 容错

修法 1:client 配 keepalive,2 分钟一次 ping

// Go grpc-go client
import "google.golang.org/grpc/keepalive"

kacp := keepalive.ClientParameters{
    Time:                2 * time.Minute,   // 2 分钟没收发就发 PING
    Timeout:             20 * time.Second,  // PING 20 秒没响应认为连接死
    PermitWithoutStream: true,              // 即使没活跃 stream 也发 PING
}

conn, err := grpc.NewClient(
    target,
    grpc.WithKeepaliveParams(kacp),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)

关键参数:

  • Time: 2 分钟:比 LB 的 350 秒 idle timeout 短得多,保证连接不会被认为空闲
  • PermitWithoutStream: true:即使当前没 RPC 进行,也发 PING。这个是关键!否则空闲连接(没 stream)不会被保活
  • Timeout: 20s:PING 20 秒收不到响应认为连接死,主动关闭并重连

没有 PermitWithoutStream=true 的话,keepalive 形同虚设——连接闲着没 stream,client 不发 ping,LB 还是会断。这个默认值是 false(为了"礼貌"),实际生产环境必须改 true。

修法 2:server 端 permit keepalive(否则 GOAWAY)

gRPC server 默认会拒绝过于频繁的 keepalive ping——这是为了防止恶意客户端通过频繁 ping 发 DoS。如果 client 发 ping 太频繁,server 会发 GOAWAY 关连接。配套配置:

// Go grpc-go server
import "google.golang.org/grpc/keepalive"

kasp := keepalive.ServerParameters{
    MaxConnectionIdle:     5 * time.Minute,   // server 主动关闭超过 5 分钟空闲的连接
    MaxConnectionAge:      30 * time.Minute,  // 强制重连周期(避免连接老化)
    MaxConnectionAgeGrace: 30 * time.Second,  // 关闭前给 graceful period
    Time:                  3 * time.Minute,   // server 自己也发 ping
    Timeout:               20 * time.Second,
}

kep := keepalive.EnforcementPolicy{
    MinTime:             1 * time.Minute,   // 客户端最频繁 1 分钟一次 ping
    PermitWithoutStream: true,
}

server := grpc.NewServer(
    grpc.KeepaliveParams(kasp),
    grpc.KeepaliveEnforcementPolicy(kep),
)

关键:server 端 EnforcementPolicy.MinTime 必须 client 端的 Time。我们 client 是 2 分钟一次 ping,server MinTime 设 1 分钟,留余量。

如果 MinTime 设得比 client Time 大,会出现"client 发 ping → server 认为太频繁 → 发 GOAWAY → 连接关闭"的灾难。这是经典的"双方配置不协调"问题。

修法 3:LB 配合(如果可改)

AWS NLB 不可改,但 ALB / GCP / 阿里云都能调长 idle timeout。建议:

idle timeout 说明
client keepalive Time 2 分钟 最短
server MinTime 1 分钟 允许 client 至少 1 分钟一次
LB idle timeout 5 ~ 10 分钟 比 client 长(LB 不该比 client 先动手)
conntrack timeout 2 小时 K8s 默认,通常不用改

核心原则:越靠近应用层的 timeout 越短,越靠近网络层的越长。这样应用层先主动管理连接,网络层不会"先动手"。

修法 4:容错重试

就算 keepalive 配好了,connection reset 仍可能因为网络抖动 / Pod 重启 / LB 维护偶发。所以应用层必须有重试:

// Go grpc-go 客户端 retry policy(通过 service config)
serviceConfig := `{
    "methodConfig": [{
        "name": [{"service": "inventory.InventoryService"}],
        "retryPolicy": {
            "MaxAttempts": 3,
            "InitialBackoff": "0.05s",
            "MaxBackoff": "1s",
            "BackoffMultiplier": 2.0,
            "RetryableStatusCodes": ["UNAVAILABLE"]
        }
    }]
}`

conn, _ := grpc.NewClient(target,
    grpc.WithDefaultServiceConfig(serviceConfig),
    grpc.WithKeepaliveParams(kacp),
)

注意只重试幂等的 RPC(GET 类查询),写操作要业务保证幂等才能重试。

修法 5:关键指标监控

修完之后还要把"连接健康"这件事变成可观测指标,否则下次坏了也是被动等业务方报。我们接入 prometheus,采的关键指标:

# gRPC 客户端关键指标(via grpc-go interceptor + prometheus)

# 1. connection reset 计数(按 service / target / error_code 拆维度)
grpc_client_connection_reset_total{service="inventory",target="..."}

# 2. keepalive ping 失败计数(ping 发了但没收到 ack)
grpc_client_keepalive_ping_failed_total{target="..."}

# 3. GOAWAY 接收计数(server 主动让我重连)
grpc_client_goaway_received_total{target="...",reason="..."}

# 4. 当前活跃连接数(grpc.ClientConn level)
grpc_client_active_connections{target="..."}

# 5. 重试触发计数(按 status code 拆)
grpc_client_retry_total{service="...",status_code="UNAVAILABLE"}

# Grafana 告警规则
# - connection_reset rate > 0.1/s 持续 5min: P3
# - keepalive_ping_failed rate > 0.05/s: P2
# - goaway_received rate > 0.5/s: P1
# - retry rate > 1% of total RPC: P3

这五条指标是 gRPC 长连接健康的"心电图",任何持续异常都说明 client / server / LB 三端配置不协调。建议把这套监控做成统一 dashboard,全公司 gRPC 服务共用。

验证:7 天观察

指标 修复前 修复后
connection reset / 天 1842 次 0 次
低 QPS 时段 P99 偶发 200-500ms 尖峰 稳定 25ms
重试触发次数 / 天 1800+ 5-10(真实网络抖动)
下游负载 有 5 分钟一波的小峰 平稳
错误日志噪音 1800+/天 0/天

5 天里被否决的方案

方案 看似可行 否决理由
把 gRPC 改回 HTTP/1.1 调用 避开 HTTP/2 多路复用问题 HTTP/1.1 短连接 LB 断了影响小 一次失败只重试一个 RPC 56 个微服务全部 gRPC 改回 HTTP/1.1 是 6 个月级重构 + 失去 HTTP/2 多路复用的性能优势 我们 P99 25ms 大部分来自 HTTP/2 改回去 P99 直接翻倍 ROI 极差
业务层定时发"健康检查 RPC"模拟流量 保活 1 天可上线 不动 gRPC 配置 每 2 分钟向 56 个下游各发一次 health RPC 多余 RPC 量约 40 万次/天 + 业务方代码侵入 + 还是治标 不如直接用 gRPC 内置 keepalive
关掉 NLB 让 K8s Service ClusterIP 直连 消除中间 LB 环节 NLB 是跨集群服务发现 + AZ 容灾 + 限流入口 关掉等于推倒整个流量入口架构 + ClusterIP 跨集群不通 工程量月级 不是这个事故能解决的
客户端每 5 分钟定时关闭再重建 gRPC 连接 暴力但简单 每次重建连接 TLS 握手 100-300ms 业务方接受不了 + 重建期间正在用该连接的并发 RPC 全部失败 + 反向放大问题
所有下游调用包一层重试中间件 错误都重试 N 次 对业务透明 错误隐藏 方向反了 错误不该被隐藏 否则真正的下游故障也被吞 + 重试放大下游负载 一旦下游真挂了重试雪崩 + 不解决根因
把 gRPC 换成 NATS / Kafka 异步消息 消息中间件天然有重连机制 同步 RPC 改异步消息是业务模型大改 order/inventory 查库存需要立即返回 异步不合适 + 团队对消息中间件运维不熟 工程量季度级

每条否决都让我们更清楚"真正要修什么"。最后选定的"client keepalive + server permit + 重试策略"既是技术最优,也是组织成本最低——所有改动都在 gRPC 配置层,基础设施不动、业务代码不动。后来产品和老板问"为什么不一次性换掉 NLB 一劳永逸",我们直接甩这张表 5 分钟说服全场。这种"否决记录"在长期来看比"选定方案"价值还大,新人入职第二周遇到类似问题翻一下表就有思路,不需要从头讨论。

决策树:新建一个 gRPC 服务该怎么配 keepalive

这棵决策树后来嵌进了平台组的 gRPC 服务接入 SOP:任何新建 gRPC 服务的 PR,作者必须在 description 里说清楚走了哪条分支。这个小改动让团队对"长连接稳定性"的纪律性提升一个量级——以前是"抄一份 grpc.NewClient(addr) 就 merge",现在是"先确认 LB 类型再选 keepalive 参数"。code review 也因此变得更有抓手,新人入职第二周就能跟着这棵树独立做配置评估,不再凭"我之前是这样写的"做决策。半年下来类似的"connection reset 噪音"工单从月均 6-8 起降到 0 起。

类似问题的其他场景

这类"中间设备 idle timeout 杀连接"问题不局限于 gRPC。在以下场景都会出现:

场景 表现 修法
MySQL 长连接 + 阿里云 SLB 中间 偶发"Lost connection to MySQL server during query" 客户端 wait_timeout < LB idle
Redis 长连接 + 防火墙 偶发"connection reset by peer" tcp_keepalive_time 调小
RabbitMQ 长连接 + 公网 偶发 frame_error 断连 heartbeat 设 30s
WebSocket + Cloudflare 100 秒后断连 客户端定期发 ping
SSH 长会话 + NAT 路由器 30 分钟空闲断 ServerAliveInterval 60

都是同一个套路:中间设备杀连接,应用层不知道,下次用时失败。修法都是应用层主动发 keepalive。

立的《gRPC 长连接稳定性纪律》

  • 所有 gRPC client 必须显式配 KeepaliveParams:Time=2min,Timeout=20s,PermitWithoutStream=true。
  • 所有 gRPC server 必须配 KeepaliveEnforcementPolicy:MinTime=1min,PermitWithoutStream=true。
  • 必须了解 LB 的 idle timeout 值,LB 不可改的(如 AWS NLB)必须 client keepalive 配合;LB 可改的统一调到 10 分钟+。
  • client keepalive Time < LB idle timeout < conntrack timeout,严格保证顺序。
  • 所有 gRPC 调用必须有重试,UNAVAILABLE / DEADLINE_EXCEEDED 重试 3 次,指数退避。
  • connection reset / GOAWAY 类错误必须监控,任何持续出现都是配置 bug。
  • 新服务接入 gRPC 时必须 review:client / server / LB 三端配置协调。
  • 慎用 HTTP/2 多路复用:虽然性能好,但单连接故障影响放大,在不稳定链路上考虑 HTTP/1.1 + 连接池。

给读者的几条自查清单

  1. 用 tcpdump 抓你某个 Pod 端口 30 分钟,看 RST 包的源 IP——如果不是 client 或 server,就是中间设备在杀。
  2. 查云厂商 LB 文档,确认 idle timeout。AWS NLB 不可改,GCP 可改,阿里云可改 max 15 分钟。
  3. 看你的 gRPC client 代码,是不是只 grpc.NewClient(addr) 然后就用?基本上没配 keepalive。
  4. 看错误日志,有没有"每 N 分钟规律性出现"的连接错误。"每 5 分钟"或"每 4 分钟"或"每 60 分钟"都是 timeout 嫌疑。
  5. 测试一下:把 client 启动后让它空闲 6 分钟(超过 NLB 5 分钟),然后发请求看是否成功。失败就是没配 keepalive。
  6. 所有 long-lived TCP 连接(DB / Cache / MQ / RPC)都问自己同样的问题。这是个通用模式。
  7. 对接公网 SaaS 服务(Slack / Twilio 等)的长连接,关注他们的 keepalive 要求(API 文档里通常有)。

这次事故让我再次确认一个分布式系统的铁律:"沉默的中间设备最危险"。LB、防火墙、NAT、proxy——这些设备不会发 error 信号给你,它们只是悄悄断连接。如果你的应用不主动检测,就只能在"下次用"时发现连接已死。设计稳定的长连接系统,本质是要"假设链路任意时刻都可能死",然后用 keepalive + 心跳 + 重连 + 重试构建韧性。

另一个心得:"配置协调"是分布式系统最容易被忽视的环节。client、server、LB、防火墙、NAT、conntrack——每个都有自己的 timeout 配置,而且默认值都是"为它自己的场景"优化的,合在一起经常打架。系统设计 / SRE 团队的一个重要责任就是把这些配置"通盘审视",建立一致的纪律(比如"应用层 timeout < 中间层 < 网络层"),避免相互矛盾。

整体效果 + 长期收益

维度 修复前 修复后 90 天
connection reset / 天 1842 次 5 分钟规律一波 0 次 90 天累计 0
低 QPS 时段 P99 偶发 200-500ms 尖峰 稳定 25ms 无尖峰
下游重试触发 / 天 1800+ 次 全部 connection reset 引发 5-10 次 真实网络抖动
下游 inventory-service 负载 每 5 分钟一波 重试小峰 CPU 15-20 percent 上下波动 平稳 CPU 12 percent 无波动
错误日志噪音 1800+/天 真问题被淹没 0/天 任何错误都是真错误
SRE 长连接相关工单 月均 6-8 起 重复值班 月均 0 起
新服务 gRPC 配置决策时间 15-30 分钟讨论 抄隔壁服务 5 分钟按决策树走
顺手扫到的同类隐患服务 0 主动扫到 11 个 gRPC 服务 + 4 个 MySQL 长连接 + 2 个 Redis 全部已配 keepalive
P99.9 抖动率 1.2 percent 与 LB idle 强相关 0.08 percent 与业务真实抖动一致
云成本(重试放大下游) + 约 0.8 万元 / 月 多余 RPC 0 多余成本

P99.9 从 1.2% 抖动率降到 0.08% 这一项是意外收获——原以为修复是"消除错误日志噪音",结果是"消除错误 + 显著改善长尾延迟"。原因是重试本身有 50-100ms 的额外延迟,1800 次重试/天分摊到所有请求上拉高了 P99.9。一次 5 天的深度调优换来"错误清零 + 延迟改善 + 下游负载平稳 + 同类隐患全清"四重收益,这种 ROI 在 SRE 项目里很难得。

认知更新:对 gRPC / 长连接 / 中间设备的 4 个新认知

  1. "沉默的中间设备"是分布式系统最危险的角色。LB、防火墙、NAT、proxy 这类设备的特点是"不会主动告诉你它做了什么"——它们悄悄断连接,客户端只有在"下次用"时才发现。和"应用层服务挂了立刻报 error"完全不同的故障模式,这种"延迟到下次操作才暴露"的特性让排查难度极高。任何依赖长连接的系统设计,都必须假设"链路任意时刻都可能死,但不告诉我",然后用 keepalive + 心跳 + 重连 + 重试构建韧性。这个认知没建立的团队,长期都会被"偶发 connection reset"困扰,而且查不到根因。
  2. HTTP/2 多路复用是双刃剑。性能上它把"每请求一连接"的开销消除,延迟和资源利用都好得多。但故障域上它把"一个连接断"放大成"所有用这个连接的并发 RPC 全失败",影响范围×N。这个权衡在稳定链路上是赚的(多路复用收益远大于偶发故障代价),在不稳定链路(公网 / 多层 NAT / idle timeout LB)上是亏的。选 gRPC 时这个 trade-off 要明确考虑,不是"HTTP/2 一定比 HTTP/1.1 好"——是"在你的链路上 HTTP/2 是否比 HTTP/1.1 好"。这条认知后来写进我们 gRPC 接入文档,新服务上线前必须确认。
  3. "默认配置"几乎从来不是你的最佳配置。gRPC client 默认不发 keepalive(避免无意义带宽)、server 默认拒绝高频 ping(防 DoS)、PermitWithoutStream 默认 false(礼貌)——每个默认值都是为"通用场景"优化,但你的具体场景可能完全不通用。任何长连接库都该先 audit 一遍它的默认值,问"在我的场景下这个默认是对的吗"。我们 audit 完 gRPC 后顺手 audit 了 MySQL driver / Redis client / Kafka consumer 的默认 keepalive 设置,挖出 7 个有类似隐患的服务——这种"配置 audit"半年做一次,投入产出比极高。
  4. "修这个事故"和"修这类事故"是两件事。原本我们计划改完 order-service → inventory-service 这条链路就收工,后来主动扫了公司所有 gRPC 服务的 keepalive 配置,挖出 11 个有类似隐患的服务,顺手又扫了 MySQL / Redis / Kafka 等长连接,又挖出 6 个。一次复盘的真正价值不是修当下,是把同类问题在它们爆雷前都摸出来。这种"主动扫雷"耗时大约是修一个 bug 的 4 倍,但避免 17 次类似事故 + 17 次值班半夜起夜——ROI 极其划算。我们后来在 SRE 团队设了固定流程,每次 P1 / P2 事故复盘后必须做"同类扫雷",这套流程半年下来主动避免了 19 次潜在事故,口碑提升非常明显。

第三个心得是关于"benchmark 的真实性"。gRPC 官方 benchmark 跑的是"持续高 QPS"场景,在这种场景下连接永远有 stream,LB idle timeout 根本不会触发——所以官方 benchmark 完美。但生产场景往往是"高 QPS + 低 QPS 时段交替"(凌晨 4-7 点几乎无流量),官方 benchmark 完全没覆盖这个模式。"官方 benchmark 漂亮"和"你的生产负载稳定"是两件事,选型 / 配置时必须用自己的真实流量模式(包括低谷期)跑一遍。这个习惯后来扩展到所有长连接组件——MySQL / Redis / Kafka 都先模拟"低流量 + LB idle"场景跑一遍,再做决策。半年下来挡掉了 3 次"看官方文档调成最优但生产爆"的坑。

最后再补一个工程文化层面的反思:这次事故触发前其实有过很多次小信号——SRE 同事注意过"凌晨告警有规律"、监控大盘上 connection reset 这个指标月增长肉眼可见、新人 onboarding 时问过"为什么错误日志这么多"、业务方偶尔抱怨过"凌晨第一笔订单有时候慢",每次大家都用"还能跑"、"是历史代码"、"重试能兜住"绕过去。所有大事故都有它的"预热信号",区别只在团队有没有把它当回事。我们后来在事故管理里加了"小信号月度复盘"机制——把过去 30 天的所有低优先级告警 + 业务抱怨 + 新人提的"为什么这样"问题集中拉一遍,挑出可能值得深挖的提前修。半年下来这个机制至少提前避免了 6 次类似量级的问题,投入产出比远超事后排查。希望读到这里的你也能在自己团队里建立类似的"小信号雷达",别再让一个看似无害的"connection reset 但能重试"把团队 6 个月后的某个忙碌下午毁掉。

下次有人在代码里写 grpc.NewClient(addr) 时,别只想着"反正能连上",顺手把 KeepaliveParams 也配上(Time=2min / Timeout=20s / PermitWithoutStream=true)、server 端也加上 EnforcementPolicy(MinTime=1min)、retry policy 也写好。这套配置花你 10 分钟,但能让你未来 6 个月不被"凌晨规律性 connection reset"工单叫起来。修完之后你会发现,同样的业务逻辑、同样的下游服务,链路突然就稳定到"再也没人吐槽过它"——其实代码没变多少,变的只是你终于让 gRPC 知道"中间有个 LB 会断连接,要主动保活"。这种"零业务侵入却带来 SLO 跃迁"的工程红利,在长连接场景里非常常见,值得每个团队投入一次彻底的复盘。如果你在自家 gRPC 链路上也做了类似的 keepalive 治理,欢迎在评论区分享你的 tcpdump 截图、最终的 connection reset 数据,以及踩到的其他长连接反模式——长连接稳定性这块,中文社区沉淀的实战经验还很稀缺,每一份数据都是后来者的灯塔,愿我们的 5 天踩坑能换你 30 分钟就内化成自己团队的工程默认值,把每一次 RPC 都用在真正的业务价值上,而不是浪费在本可以避免的"连接已死但客户端不知道"的应急重试里。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

ClickHouse 核心表 parts 飙到 47832 看板全线超时的 6 天复盘:四因素叠加根因 + buffer/分区/merge 三层治理 + 接入决策树

2026-5-26 12:46:33

技术教程

Node.js+TSmonorepoCI/CD流水线从28分钟压到4分钟的3周复盘:6大方向30项优化+Turborepo/oxlint/BuildKit实战

2026-5-26 17:34:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索