WebSocket 故障排查手册
适用范围:
Mud.Feishu.WebSocket用法:先按"现象"定位,再按"关键日志/指标/状态"取证,最后按"常见根因"处置。
0. 一次性取证清单
csharp
// 连接状态(HealthCheck 同源)
var state = hostedService.GetConnectionState(); // IsConnected / ConnectedTime / ReconnectCount / LastError
var stats = hostedService.GetConnectionStats(); // Uptime / ReconnectCount / LastError
var reconnect = orchestrator.GetReconnectState(); // IsReconnecting / IsCircuitOpen / CurrentAttempt / LastReconnectReason
// 并发与积压
var concurrency = hostedService.GetConcurrencyService(); // MaxConcurrentHandlers / AvailableCount / PendingCount健康检查 data 中的存活探针字段(R2/F1,判断"僵尸连接"必读):
| 字段 | 含义 |
|---|---|
receive_loop_alive | 接收循环任务是否仍在运行 |
last_receive_utc | 最近一次收到帧的时间(never = 连接建立后从未收到帧) |
idle_ms | 距最近一次收帧的毫秒数(-1 = 尚无样本,不代表静默) |
is_zombie | true = 僵尸态(连接为 Open 但接收循环已结束)⇒ 判 Unhealthy |
OTel 指标:feishu.websocket.connections、feishu.websocket.backlog、feishu.websocket.message.duration、 feishu.websocket.reconnect(outcome)、feishu.event.deduplication。
1. 连接建立不起来
| 现象 | 关键日志 | 常见根因与处置 |
|---|---|---|
| 启动期连续失败 | 飞书WebSocket服务初始连接失败 (第 N/3 次) | 凭据/网络/端点获取失败;查 FeishuAppConfig.AppId/AppSecret、GetWebSocketEndpointAsync 错误码 |
连接飞书WebSocket服务超时 | ConnectionTimeoutMs | 网络不可达或代理拦截;核对出网策略 |
WebSocket URL使用不安全的ws://协议 | 抛 ArgumentException | 生产必须 wss://;仅测试环境设 Certificate.AllowInsecureWebSocket = true |
WebSocket 主机 "..." 不在允许的主机白名单内 | 抛 ArgumentException | P2-15:AllowedHostSuffixes 默认 *.feishu.cn;*.larksuite.com;连接自建代理/本地测试端点时把主机加入列表(支持 *. 通配后缀与精确主机名,分号分隔)或置空表示不限制 |
SSL证书验证失败 | SSL证书验证失败(链错误,非自签名根…) | 证书过期/链不完整;自签名需 Certificate.AllowSelfSignedCertificates(且 Certificate.Mode=Dev),名称不匹配需 Certificate.AllowCertificateNameMismatch |
服务端拒绝认证 | WebSocket认证失败: {Code} - {Message} + 错误码明细 | 10009 令牌过期 / 10010 令牌无效 / 10011 权限不足;AuthenticateAsync 内置退避 + 冷却(3 次失败进入 5 分钟冷却) |
| 认证响应超时 | WebSocket认证响应超时(30秒) | 检查 AuthTimeoutMs;若启用了 AuthGateTimeoutMs > 0 且 MaxConcurrentHandlers 很小,长耗时处理器会推迟文本帧处理 |
| 连接建立成功但不收事件(僵尸连接) | 无异常日志;健康检查 data.is_zombie = true / receive_loop_alive = false;HostedService 记 健康检查发现僵尸连接 并触发重连 | R2/P0-1:接收循环已结束而 socket 仍为 Open。先确认是否有代码取消/释放了连接(ConnectAsync 的令牌现在只约束建连阶段;若确认调用方令牌仍传入且被取消,那是 D2 之前的行为,请改用 DisconnectAsync/DisposeAsync 终止连接)。无外部因素时按"内核 socket 假死 / 对端半开"处理:SDK 会自动重连,若持续复现请检查中间设备(NAT/负载均衡)的空闲回收配置与 ProtocolKeepAliveInterval |
| 分片超限被丢弃 | 分片消息大小超过最大限制 + 已排空超限消息的剩余分片… | R2/WS2-03:超限消息现为"排空至消息边界后丢弃",连接保持可用、不回 ACK(服务端按超时重投)。Error 事件的 ErrorType = FragmentSizeExceeded。若日志出现 排空超限消息时达到上界,说明对端持续投递超大消息(协议层异常),SDK 已主动断连重连 |
| 分片超限导致连续解析失败 | 排空超限消息时达到上界 | 同上:此时不排空会污染消息边界(被丢弃消息的尾部会被当作新消息),必须在服务端侧限制单条消息大小 |
2. 频繁断线 / 反复重连
| 现象 | 关键日志 | 处置 |
|---|---|---|
| 断开后长于健康检查间隔才恢复 | 健康检查发现连接断开(无 连接断开事件触发) | 说明 Disconnected 未及时送达:核对是否为"旧连接已关 + 新连接握手失败"场景(应由 SDK 保证送达);并检查 HealthCheckIntervalMs |
| 关闭原因异常 | 飞书WebSocket连接已断开: {Status} - {Description} | EndpointUnavailable=对端不可达/网络中断;ProtocolError=协议错误;NormalClosure=正常关闭 |
| 重连风暴 | 等待 {Delay}ms 后进行第 N 次重连尝试 | 冷却期(Reconnect.Cooldown)+ 指数退避(Reconnect.BaseDelayMs/Reconnect.MaxDelayMs)已生效;若仍过密,检查是否多实例同时重连 |
| 停止重连 | 重连熔断器已打开:已达到重连上限 | 达上限后熔断(IsCircuitOpen = true),健康检查返回 Degraded;仅在连接成功后自动清除;需人工/配置变更介入 |
3. 事件丢失或重复消费
| 现象 | 取证 | 处置 |
|---|---|---|
| 事件重复 | feishu.event.deduplication(outcome=hit)+ 处理日志 | 服务端重投是契约行为(ACK 500 或未回 ACK);处理器必须幂等;确认去重模式(EventDeduplication.Mode)为 InMemory/Redis |
| 事件丢失 | 检查是否出现 消息序号验证失败 / 统一去重检查跳过(此时回 ACK 200,服务端不再重投) | 若是误判重复:核对事件级去重(event_id,带 AppKey 维度)是否被跨应用/跨环境共用同一后端(如共用 Redis)。注意 IFeishuSeqIDDeduplicator 的键是裸 SeqID、进程内全局、无租户隔离(R2/WS2-10 口径纠正:本层不提供按租户/应用分域的隔离键);它不涉及多实例判重,多实例各自独立去重 |
| 处理失败无重投 | 已发送ACK消息: code=500 缺失 | ACK 发送依赖连接可用;断线期间处理失败不会补 ACK,需依赖服务端超时重投 |
| 重启后首条消息异常 | 接收消息时发生错误/ProtoBuf 解析失败 | 重连时会清空半包缓冲(BinaryMessageProcessor.Reset);若仍异常,检查 MessageSizeLimits 是否过小导致分片被丢弃 |
4. 背压与积压(性能)
| 现象 | 取证 | 处置 |
|---|---|---|
| 事件处理延迟增大 | feishu.websocket.backlog 升高;健康检查 concurrent_utilization_pct ≥ 90 → Degraded | 提升 MaxConcurrentHandlers;排查处理器内部同步阻塞/串行依赖 |
| 健康检查 Unhealthy(槽位耗尽) | WebSocket并发槽位已耗尽 (0/N) | 槽位耗尽即接收循环被反压(不再读帧、不回 ACK);优先排查处理器是否卡住 |
| 吞吐不升反降 | 处理器中存在同步 I/O 或长 Task.Delay | 处理器应纯异步;避免在处理器里等待"接收循环推进"(I10 循环等待) |
| 想临时回退旧行为 | — | 设 MaxConcurrentHandlers = 0(无限制)即可让接收侧永不阻塞 |
5. 关停相关
| 现象 | 说明/处置 |
|---|---|
关停时报 ObjectDisposedException | 改造后 Dispose 之后调用连接/发送 API 会确定性抛出;请确保关停顺序为 await StopAsync() → await DisposeAsync() |
关停后仍有 Release() 相关异常 | I9:信号量不随 Dispose 释放,在途租约归还是安全的;若出现,说明访问了已释放的连接对象而非信号量 |
| 关闭握手超时(5s) | 服务端未应答关闭帧:SDK 会强制 Abort 并观察残留任务异常(不再遗留 UnobservedTaskException) |
| 服务端记录为异常断线 | 同步 Dispose() 不执行停止流程;确定性停止请 await StopAsync() 或 await DisposeAsync() |
6. 测试与验证手段
| 目标 | 手段 |
|---|---|
| 重连契约(无死锁、闸门不泄漏) | Tests/.../ReconnectionOrchestratorReentrancyTests(Category=Concurrency) |
| 连接代次/释放竞态 | ConnectionLifecycleRaceTests、DisposeRaceTests |
| 背压与租约不泄漏 | BackpressureTests、BinaryMessageProcessorConcurrencyTests |
| 收发大小边界 | SendSizeLimitTests |
| 真实协议交互(握手/分片/关闭码/P1-3 路径) | Integration/WebSocketConnectionIntegrationTests(Category=Integration,net8.0+,回环 HttpListener) |
| 源码规范守卫 | SourceConventionTests(版权头) |
| 架构不变量守卫(I13–I16 / D5) | ContractGuards/WebSocketContractGuards(R2/WS2-13,源码级断言);dotnet test Tests/Mud.Feishu.WebSocket.Tests --filter "FullyQualifiedName~WebSocketContractGuards" |
| 连接存活性 / 僵尸态 | Core/FeishuWebSocketClientLivenessTests(R2) |
| 压力与长稳(不进全量门禁) | FeishuWebSocketStressTests(Category=Stress,手工执行):dotnet test Tests/Mud.Feishu.WebSocket.Tests -c Release -f net8.0 --filter "Category=Stress"。注意 xUnit 的 Trait 不会自动排除用例,scripts/verify-build.ps1 已在步骤 4 显式传入 --filter "Category!=Stress" |