第一章:Golang WebSocket长连接断连率飙升的典型现象与根因定位
当基于 gorilla/websocket 或 gobwas/ws 构建的高并发 WebSocket 服务突然出现断连率从
常见表征现象
- 客户端频繁触发
onclose事件,错误码多为1006(abnormal closure)或1001(going away); - 服务端日志中大量出现
websocket: write deadline exceeded或i/o timeout; netstat -an | grep :<port> | grep ESTABLISHED | wc -l显示活跃连接数未显著下降,但ss -i显示大量连接处于retransmit状态;- Prometheus 中
websocket_connections_total{state="closed"}指标突增,同时go_goroutines持续高位震荡。
根因高频分布
| 根因类别 | 典型诱因 | 验证方式 |
|---|---|---|
| 写超时未重置 | conn.SetWriteDeadline() 仅设一次,后续心跳/消息发送复用过期时间 |
检查 WriteMessage 前是否调用 SetWriteDeadline(time.Now().Add(30 * time.Second)) |
| 读缓冲区溢出 | 客户端恶意发送超长帧(如 10MB+ ping payload)导致 conn.ReadMessage() 阻塞 |
启用 conn.SetReadLimit(4 * 1024 * 1024) 限制单帧大小 |
| 心跳机制缺失 | 无 pong 响应逻辑,NAT/防火墙主动回收空闲连接 |
在 SetPingHandler 中添加 conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(30 * time.Second)); return nil }) |
关键代码修复示例
// 初始化连接时必须设置读写超时并绑定心跳处理
conn.SetReadLimit(4 * 1024 * 1024) // 防止大帧阻塞
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
// 必须注册 pong handler,否则默认不响应 ping,连接被中间设备切断
conn.SetPingHandler(func(appData string) error {
// 每次收到 ping,重置读超时,维持连接活性
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
return nil
})
// 发送消息前务必刷新写超时
err := conn.WriteMessage(websocket.TextMessage, data)
if err != nil {
log.Printf("write failed: %v", err)
return
}
conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) // 刷新超时
第二章:net.Conn.SetReadDeadline底层机制深度剖析
2.1 TCP连接状态机与ReadDeadline触发时机的内核级分析
TCP连接状态迁移直接影响 ReadDeadline 的触发边界。当套接字处于 TCP_ESTABLISHED 状态且接收缓冲区为空时,recv() 系统调用进入可中断等待;若超时时间到期,内核在 tcp_rcv_state_process() 中通过 sk_timer 触发 sock_def_readable() 唤醒阻塞线程。
ReadDeadline 内核路径关键节点
setsockopt(SO_RCVTIMEO)→sk->sk_rcvtimeo更新sys_recv()→sock_wait_for_data()→sk_wait_event()- 超时回调:
tcp_fin_timeout_handler()(实际由sk_timer驱动)
状态机与超时协同示意
// net/ipv4/tcp_input.c 片段(简化)
if (sk->sk_rcvtimeo && !skb_queue_empty(&sk->sk_receive_queue)) {
// 仅当有数据可读时才跳过等待
goto data_ready;
}
// 否则进入带超时的 wait_event_interruptible_timeout()
该逻辑表明:ReadDeadline 并非在状态变更瞬间触发,而是在下一次 recv 尝试时,依据当前 sk->sk_rcvtimeo 和接收队列状态联合判定。
| 状态 | 是否可能触发 ReadDeadline | 说明 |
|---|---|---|
| TCP_SYN_SENT | 否 | 连接未建立,返回 EINPROGRESS |
| TCP_ESTABLISHED | 是(空缓存时) | 标准超时等待路径 |
| TCP_FIN_WAIT2 | 是 | 半关闭后仍可读,超时有效 |
graph TD
A[recv() syscall] --> B{sk_receive_queue empty?}
B -->|Yes| C[wait_event_timeout<br>with sk->sk_rcvtimeo]
B -->|No| D[data copied immediately]
C --> E{timeout expired?}
E -->|Yes| F[return -1, errno=ETIME]
E -->|No| G[wake on skb arrival]
2.2 Go runtime netpoller中deadline定时器的注册与唤醒逻辑
Go 的 netpoller 通过 runtime.timer 管理连接的读写 deadline,其核心在于将 timer 与 pollDesc 绑定,并在超时触发时唤醒对应 goroutine。
定时器注册关键路径
- 调用
setDeadline→pd.setDeadline→addTimer - 最终调用
addtimer(&timer{...}),插入到timer heap中 - 每个
timer关联pd.runtimeCtx(指向pollDesc),用于唤醒时定位 goroutine
唤醒机制
// src/runtime/netpoll.go 中 timer 触发回调
func (t *timer) f(arg interface{}) {
pd := arg.(*pollDesc)
netpollready(&pd.rg, pd, 'r') // 或 'w',唤醒读/写等待队列
}
该回调由 timerproc 在系统监控 goroutine 中执行;netpollready 将 pd.rg(goroutine ID)加入就绪队列,由调度器恢复执行。
| 字段 | 含义 | 示例值 |
|---|---|---|
pd.rg |
阻塞在读操作上的 goroutine ID | 0x123456 |
t.period |
定时器周期(deadline 为一次性,故为 0) | |
t.f |
超时处理函数 | (*pollDesc).expire |
graph TD
A[setReadDeadline] --> B[pd.setDeadline]
B --> C[initTimerWith pd]
C --> D[addtimer t]
D --> E[timerproc 扫描到期]
E --> F[t.f(pd) 唤醒]
F --> G[netpollready → goroutine 可运行]
2.3 SetReadDeadline在TLS握手、协议升级及分帧读取中的行为差异验证
TLS握手阶段的Deadline表现
SetReadDeadline 在 tls.Conn 上调用后,仅对应用层读操作生效,而握手本身由底层 net.Conn.Read 驱动,不受其约束。需显式设置 tls.Config.Timeouts.HandshakeTimeout。
协议升级(如HTTP/1.1 → WebSocket)
升级过程中,底层连接未重置,SetReadDeadline 持续作用于后续 Read();但若升级后切换至自定义分帧逻辑,则需重新校准超时。
分帧读取场景下的语义偏移
| 场景 | Deadline 是否中断帧内读取 | 是否可被 Read() 返回 ioutil.ErrUnexpectedEOF |
|---|---|---|
| TLS握手 | 否 | 否(触发 tls: handshake error) |
| HTTP Upgrade完成 | 是 | 是(若帧头已收全但body超时) |
| 自定义二进制分帧 | 是 | 是(需手动检查 n < expected + err == nil) |
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf[:4]) // 读帧头
if err != nil {
log.Fatal(err) // 可能是 timeout 或 EOF
}
// 若 n==4,继续读 payload;此时 deadline 已更新或需重设
此处
SetReadDeadline作用于本次Read调用,不自动延续;分帧逻辑中必须在每次Read前显式重置 deadline,否则后续读取将沿用过期时间点。
2.4 高并发场景下deadline误触发与time.Now()精度缺陷的实测复现
在 Linux 5.10+ 内核、Go 1.21 环境下,time.Now() 在高负载时返回重复时间戳(纳秒级抖动达 ±15μs),导致 context.WithDeadline 过早取消。
复现关键代码
func testDeadlineDrift() {
deadline := time.Now().Add(10 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// 高频轮询触发精度丢失
for i := 0; i < 10000; i++ {
if time.Now().After(deadline) { // ❗此处可能因时钟回跳/重复而误判
log.Printf("EARLY TRIGGER at #%d", i)
break
}
runtime.Gosched()
}
}
逻辑分析:time.Now() 底层调用 vDSO clock_gettime(CLOCK_MONOTONIC),但在 CPU 频率动态调整或中断密集时,vDSO fallback 至系统调用,引入微秒级不确定性;After() 比较依赖绝对时间精度,毫秒级 deadline 在纳秒抖动下极易误触发。
典型误差分布(10万次采样)
| 环境负载 | 平均抖动 | 误触发率 | 最大偏差 |
|---|---|---|---|
| 空闲 | 32 ns | 0.002% | 89 ns |
| 80% CPU | 12.7 μs | 18.3% | 41 μs |
根本路径
graph TD
A[goroutine 调用 time.Now] --> B{vDSO 是否可用?}
B -->|是| C[读取 TSC 寄存器]
B -->|否| D[陷入内核 clock_gettime]
C --> E[受 TSC 同步延迟影响]
D --> F[受调度延迟 & 中断延迟影响]
E & F --> G[返回非单调/重复时间戳]
2.5 基于pprof+gdb追踪ReadDeadline导致goroutine阻塞的完整链路
当 net.Conn.Read 遇到 ReadDeadline 超时,底层会进入 runtime.netpollblock 等待,而非立即返回。此时 goroutine 状态为 syscall 或 IO wait,易被误判为健康。
pprof 定位阻塞点
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2
筛选 net.(*conn).Read 及 internal/poll.(*FD).Read 调用栈,确认阻塞在 runtime.gopark。
gdb 深入内核态
gdb ./myserver $(pgrep myserver)
(gdb) goroutines
(gdb) goroutine <id> bt # 查看目标 goroutine 的 runtime.pollDesc.wait 链
关键字段:pd.rd(read deadline)、pd.rt.fdmu(文件描述符锁)。
阻塞链路还原
graph TD A[Conn.Read] –> B[fd.Read] –> C[pollDesc.waitRead] –> D[runtime.netpollblock] –> E[epoll_wait]
| 字段 | 含义 | 典型值 |
|---|---|---|
pd.rd |
读截止时间(纳秒) | 1712345678901234567 |
pd.rseq |
读事件序列号 | 42 |
fd.pd |
关联 pollDesc 地址 | 0xc000123000 |
阻塞根源常是 pd.rd 已过期但 runtime.netpoll 未及时唤醒,需结合 epoll 事件循环与 timerproc 协同分析。
第三章:WebSocket心跳保活的核心设计原则
3.1 心跳周期、超时阈值与网络RTT的量化建模与动态适配
心跳机制并非固定节拍,而是需随网络波动实时校准的反馈闭环。
RTT采样与指数加权移动平均(EWMA)
# 动态RTT估计:α=0.85平衡响应性与稳定性
rtt_ewma = alpha * rtt_sample + (1 - alpha) * rtt_ewma_prev
alpha 越高,模型对突发延迟越敏感;生产环境推荐 0.7–0.9 区间,兼顾抖动抑制与收敛速度。
超时阈值自适应公式
| 变量 | 含义 | 典型取值 |
|---|---|---|
RTTₘₑₐₙ |
EWMA平滑后RTT | 42ms |
RTTᵥₐᵣ |
RTT方差估计 | 18ms² |
Timeout |
实际超时值 | RTTₘₑₐₙ + 4×√RTTᵥₐᵣ |
心跳周期决策逻辑
graph TD
A[采集最近8次RTT] --> B{RTT变异系数 > 0.3?}
B -->|是| C[心跳周期 = max(500ms, 2×RTT₉₅)]
B -->|否| D[心跳周期 = RTTₘₑₐₙ × 1.5]
该建模使心跳在弱网下延长以减少误判,在低延迟链路中加速故障发现。
3.2 客户端不可靠性下的服务端主动探测策略与状态机收敛保障
当客户端频繁断连、假在线或心跳失序时,仅依赖客户端上报易导致服务端状态陈旧。需构建双向可信状态通道。
主动探测状态机设计
服务端对高优先级客户端启动分级探测:
- T1(5s):轻量 TCP 连通性探测(SYN 半开检测)
- T2(30s):应用层心跳探针(带 nonce 验证)
- T3(120s):触发全量状态同步请求
状态收敛保障机制
| 状态 | 转移条件 | 收敛超时 | 安全动作 |
|---|---|---|---|
ONLINE |
连续3次心跳失败 | 15s | 降级为 PROBING |
PROBING |
探针响应成功且 nonce 匹配 | 60s | 恢复 ONLINE |
OFFLINE |
T3超时未响应 | 300s | 清理会话,广播下线事件 |
def probe_client(client_id: str) -> bool:
# 发起带时间戳与随机数的加密探针
nonce = secrets.token_hex(8)
timestamp = int(time.time() * 1000)
payload = encrypt(f"{nonce}:{timestamp}", key=client_key)
response = send_udp_probe(client_id, payload, timeout=2.0)
if not response:
return False
# 验证响应中的 nonce 回显与时效性(≤5s偏差)
decrypted = decrypt(response, key=client_key)
resp_nonce, resp_ts = decrypted.split(":")
return resp_nonce == nonce and abs(int(resp_ts) - timestamp) <= 5000
该探针函数通过 nonce 绑定+时间窗校验,杜绝重放攻击;超时设为 2s 适配弱网场景,避免阻塞主状态机。加密密钥按客户端隔离,确保探针信道不可伪造。
graph TD
A[ONLINE] -->|心跳失败×3| B[PROBING]
B -->|探针成功| A
B -->|T3超时| C[OFFLINE]
C -->|新连接| A
3.3 PING/PONG帧与自定义业务心跳的语义分离与错误隔离实践
WebSocket 协议内建的 PING/PONG 帧专用于链路存活探测,不承载业务语义;而业务层需独立设计心跳(如 HEARTBEAT_REQ/HEARTBEAT_ACK),用于会话保活、状态同步等场景。
语义冲突风险示例
// ❌ 错误:复用 PONG 帧携带业务字段(违反 RFC 6455)
ws.on('pong', () => {
updateLastActive(); // 隐式耦合,无法区分网络层抖动与业务失联
});
逻辑分析:pong 事件仅表示底层 TCP 连接未断开,但业务服务可能已崩溃或卡死;该监听无法捕获应用级超时,导致故障定位延迟。
推荐隔离方案
| 维度 | PING/PONG 帧 | 自定义业务心跳 |
|---|---|---|
| 触发主体 | WebSocket 实现自动发送 | 应用层定时器主动发起 |
| 超时阈值 | 通常 30s(可配置) | 可差异化(如登录态 90s) |
| 失败处理 | 关闭连接 | 重连+本地降级+告警上报 |
错误隔离流程
graph TD
A[收到 PONG] --> B{链路层健康?}
B -->|是| C[忽略,不触发业务逻辑]
B -->|否| D[关闭 WebSocket 连接]
E[收到 HEARTBEAT_ACK] --> F{业务态健康?}
F -->|是| G[刷新业务活跃时间]
F -->|否| H[启动业务重连+熔断]
第四章:三种生产级心跳保活方案的工程实现
4.1 基于context.WithTimeout + goroutine协程池的轻量心跳调度器
传统长连接心跳常采用 time.Ticker 配合阻塞 select,易导致 goroutine 泄漏或超时不可控。本方案融合上下文超时与固定协程池,实现低开销、可取消、可复用的心跳调度。
核心设计优势
- ✅ 超时由
context.WithTimeout统一管控,避免 goroutine 永久挂起 - ✅ 复用预启动的 goroutine 池,规避高频启停开销
- ✅ 心跳任务无状态、幂等,天然适配池化调度
心跳调度流程(mermaid)
graph TD
A[启动心跳任务] --> B[WithContextTimeout生成ctx]
B --> C{ctx.Done?}
C -->|否| D[提交至worker池执行SendPing]
C -->|是| E[自动清理并退出]
D --> F[返回err或重入]
示例调度器实现
func NewHeartbeatScheduler(pool *WorkerPool, timeout time.Duration) func() error {
return func() error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() // 确保资源释放
return pool.Submit(func() error {
select {
case <-ctx.Done():
return ctx.Err() // 超时错误透出
default:
return sendPing() // 实际心跳逻辑
}
}).Await(ctx) // Await支持ctx中断
}
}
timeout 控制单次心跳最大等待时长;pool.Submit 返回可等待的异步任务句柄;Await(ctx) 使调用方能响应上级超时信号,形成超时传递链。
4.2 利用http.Server.Handler劫持与WebSocket连接生命周期钩子的自动保活中间件
核心设计思想
通过包装 http.Handler,在请求分发前注入连接状态跟踪与心跳调度逻辑,将 WebSocket 升级流程纳入统一生命周期管理。
自动保活中间件实现
type KeepAliveMiddleware struct {
Next http.Handler
}
func (k *KeepAliveMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "upgrade failed", http.StatusBadRequest)
return
}
defer conn.Close()
// 启动双向心跳:服务端每30s发ping,客户端pong超时5s则断连
go k.startHeartbeat(conn)
k.Next.ServeHTTP(w, r) // 实际业务Handler(如消息路由)
}
逻辑说明:
upgrader.Upgrade触发后立即接管连接;startHeartbeat在独立 goroutine 中维持conn.SetPingHandler与定时conn.WriteMessage(websocket.PingMessage, nil),参数30s/5s可热配置。
生命周期钩子能力对比
| 钩子阶段 | 支持自定义 | 可中断连接 | 适用场景 |
|---|---|---|---|
| Upgrade前 | ✅ | ✅ | 认证、限流、路由决策 |
| 连接建立后 | ✅ | ❌ | 初始化上下文、日志埋点 |
| 心跳异常时 | ✅ | ✅ | 清理资源、触发告警 |
数据同步机制
- 所有心跳事件通过 channel 推送至中心状态机
- 连接元数据(ID、IP、上线时间)实时写入内存 Map + Redis 持久化双写
graph TD
A[HTTP Request] --> B{Upgrade?}
B -->|Yes| C[Wrap Conn with Heartbeat]
B -->|No| D[Pass to Next Handler]
C --> E[Start Ping/Pong Loop]
E --> F[On Ping Timeout → Close & Cleanup]
4.3 结合Go 1.22+ io/net.Conn.Read/Write deadlines与自适应重传的混合保活框架
Go 1.22 强化了 net.Conn 的 deadline 语义一致性,SetReadDeadline/SetWriteDeadline 现在对底层 io.Reader/io.Writer 操作具备更可预测的中断行为,为保活机制提供确定性基础。
核心设计原则
- 利用 deadline 触发被动探测(超时即断连)
- 结合 RTT 估算动态调整
KeepAlive心跳间隔与重传次数 - 失败后不立即退避,而是基于丢包率切换至「快速探测模式」
自适应重传状态机
graph TD
A[Idle] -->|心跳超时| B[Probe-1]
B -->|失败| C[Probe-2 RTT×1.5]
C -->|失败| D[Fast Mode: 连续3次短间隔探测]
D -->|任一成功| A
D -->|全失败| E[Close Conn]
关键参数对照表
| 参数 | 默认值 | 说明 |
|---|---|---|
baseInterval |
30s | 初始心跳间隔 |
maxRetries |
3 | 单次保活周期最大重试数 |
rttAlpha |
0.125 | EWMA RTT 平滑系数(RFC 6298) |
示例:带 deadline 的探测写入
func (c *HybridKeepAlive) probe() error {
// Go 1.22+:WriteDeadline 精确作用于本次 Write 调用
c.conn.SetWriteDeadline(time.Now().Add(c.nextTimeout()))
_, err := c.conn.Write(heartbeatPkt)
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return fmt.Errorf("probe timeout: %v", netErr)
}
return err
}
nextTimeout() 返回基于当前 RTT 与丢包率计算的动态超时值(如 RTT × (1 + 0.5 × lossRate)),确保探测既不过于激进也不迟钝。SetWriteDeadline 在 Go 1.22 中不再受系统调用中断影响,保障超时精度。
4.4 基于Prometheus指标驱动的心跳健康度实时评估与熔断降级机制
心跳健康度建模
将服务心跳抽象为时序信号:health_score = 1 - (error_rate + latency_ratio + unresponsive_ratio),各分量均来自Prometheus直采指标。
实时评估流水线
# Prometheus告警规则片段(用于触发健康度重计算)
ALERT ServiceHealthDegrade
IF 100 * (rate(http_request_total{status=~"5.."}[2m])
/ rate(http_request_total[2m])) > 5
FOR 30s
LABELS {severity="warning"}
ANNOTATIONS {summary="Health score falling below threshold"}
该规则基于2分钟滑动窗口错误率,延迟敏感且避免瞬时抖动误判;FOR 30s确保状态持续性,防止毛刺触发。
熔断决策矩阵
| 健康度区间 | 行为 | 持续时间阈值 |
|---|---|---|
| ≥ 0.8 | 全量放行 | — |
| 0.5–0.79 | 限流(QPS ↓30%) | ≥60s |
| 自动熔断+降级响应 | ≥15s |
执行流程
graph TD
A[Prometheus拉取指标] --> B[HealthScore实时计算]
B --> C{健康度 < 0.5?}
C -->|是| D[触发熔断器状态切换]
C -->|否| E[维持正常路由]
D --> F[返回预置降级JSON]
第五章:从断连治理到连接治理的架构演进思考
在某大型金融云平台的高可用改造项目中,团队最初聚焦于“断连治理”——即通过心跳探测、重试熔断、超时兜底等手段应对下游服务偶发性不可用。典型策略包括:HTTP客户端配置maxRetries=3、readTimeout=2s,结合Hystrix降级返回缓存数据。然而上线后发现,TP99延迟仍频繁突破800ms,日志中大量出现Connection reset by peer与Too many open files错误。
连接生命周期失控的真实代价
该平台微服务间日均建立连接超2.4亿次,但平均连接复用率仅17%。抓包分析显示:63%的HTTP/1.1请求未启用Connection: keep-alive;gRPC客户端未设置KeepAliveParams,导致每分钟新建连接达12万+。Linux系统层面netstat -an | grep :8080 | wc -l峰值达3.8万,远超ulimit -n 65536软限制的60%阈值。
连接池参数与业务流量的错配现象
对比A/B测试结果:
| 服务模块 | 初始连接池大小 | 平均并发请求数 | 连接创建耗时(ms) | 拒绝连接率 |
|---|---|---|---|---|
| 支付网关 | 10 | 85 | 42 | 12.7% |
| 账户查询 | 200 | 42 | 18 | 0.0% |
根源在于连接池未按QPS-RT乘积(即连接需求强度)动态配置。支付网关因RT长(平均310ms)、QPS高(275),理论最小连接数应≥86,而账户查询RT仅92ms,200连接明显冗余。
基于eBPF的连接行为可观测实践
部署bcc-tools中的tcpconnect和tcplife探针,实时捕获连接生命周期事件:
# 捕获所有新建连接及耗时
sudo /usr/share/bcc/tools/tcplife -t -D 5 | head -10
PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
12487 java 10.244.3.12 54382 10.244.1.8 8080 0 0 12.4
12487 java 10.244.3.12 54383 10.244.1.8 8080 0 0 8.9
数据揭示:37%的连接存活时间maxIdleTime),造成资源假性饱和。
连接治理的架构落地四步法
- 连接抽象层统一:基于Netty封装
SmartConnectionManager,自动识别HTTP/gRPC/MySQL协议并应用差异化保活策略; - 动态连接池引擎:集成Prometheus指标(
http_client_request_duration_seconds_bucket),每30秒计算targetPoolSize = ceil(QPS × P95_RT × 1.5); - 内核级连接复用优化:在Kubernetes DaemonSet中部署
ipvsadm规则,将同集群服务调用强制走LOCAL路由,规避NAT表连接跟踪开销; - 连接泄漏根因追踪:通过Java Agent注入
ConnectionLeakDetector,当Socket对象finalize时打印完整调用栈,定位到某SDK中CloseableHttpClient未被try-with-resources包裹的3处代码缺陷。
治理成效量化对比
上线3周后核心指标变化:
- 连接创建速率下降68%(从12.4万/分钟→3.9万/分钟)
TIME_WAIT状态连接数稳定在2300±150(原峰值18600)- 支付网关P99延迟从792ms降至214ms
- 因连接耗尽导致的5xx错误归零
该平台已将连接治理能力沉淀为内部ConnGuard中间件,支持通过Kubernetes CRD声明式定义连接策略:
apiVersion: conn.guard/v1
kind: ConnectionPolicy
metadata:
name: payment-svc-policy
spec:
targetService: "payment.default.svc.cluster.local"
http:
keepAliveTimeout: "300s"
maxConnectionsPerHost: 200
grpc:
keepAliveTime: "10s"
keepAliveTimeout: "3s" 