Bebebus 集群完善记:从 Crude 到 Production-Ready 的最后一段路
前三篇文章记录了集群搭建、故障注入、核心修复。本文记录最后的完善——把"能跑"的集群做到"生产级"。
1. MQTT 5.0 协议完整性
审计发现 5 个 CRITICAL 级别缺口:
PUBACK/PUBREC/PUBREL/PUBCOMP V5 decoder 缺失
MQTT 5.0 客户端发来的 QoS2 ACK 包的 ReasonCode 和 Properties 被静默丢弃——DecodePacketV5 里漏了这 4 个 packet type。
// 修复:在 switch 里补上 4 个 case
case *PubAckPacket: return p.decodeV5(bodyReader, header)
case *PubRecPacket: return p.decodeV5(bodyReader, header)
case *PubRelPacket: return p.decodeV5(bodyReader, header)
case *PubCompPacket: return p.decodeV5(bodyReader, header)
// 新增 decoder: 读 PacketID + ReasonCode + Properties
func (p *PubAckPacket) decodeV5(r io.Reader, header FixedHeader) (*PubAckPacket, error) {
p.PacketID, _ = DecodeUint16(r)
if header.RemainingLength > 2 {
p.ReasonCode, _ = DecodeByte(r)
p.Properties, _ = DecodeProperties(r)
}
return p, nil
}
CONNACK 不发送服务器能力
MQTT 5.0 客户端连上后不知道 broker 的限制——ReceiveMaximum=64? TopicAlias=10? Retain 支持? 全部没告诉客户端。
// 修复:CONNACK properties 包含全部能力
ack.Properties.Add(packet.PropReceiveMaximum, uint16(64))
ack.Properties.Add(packet.PropTopicAliasMaximum, uint16(10))
ack.Properties.Add(packet.PropMaximumQoS, byte(2))
ack.Properties.Add(packet.PropRetainAvailable, byte(1))
ack.Properties.Add(packet.PropWildcardSubscriptionAvailable, byte(1))
ack.Properties.Add(packet.PropSubscriptionIdentifierAvailable, byte(1))
ack.Properties.Add(packet.PropSharedSubscriptionAvailable, byte(1))
ack.Properties.Add(packet.PropMaximumPacketSize, uint32(268435456))
UNSUBACK 无 reason codes、Topic Alias 未解析、ReceiveMaximum 流控未执行
全部修复。
2. OpenTelemetry 全链路 Tracing
从零到完整 OTEL 集成:
配置层:
--trace-exporter none|stdout|otlp
代码层:
handleClient 入口:
sessionCtx = WithTrace(bg) → trace_id=AAA (无 span)
read loop:
pktCtx, pktSpan = Tracer.Start(sessionCtx, "mqtt.recv")
handlePacket(pktCtx, pkt) → ctx 作为第一参数透传
pktSpan.End()
handler:
FromCtx(ctx).Info("processing") → 自动带 trace_id
跨节点:
ForwardMessageRequest.trace_id ← TraceIDFromCtx(ctx)
关键设计决策:
- session 级不创建 span(避免跨小时的 span)— 仅注入 trace_id
- 每个 MQTT 包创建短 span(<10ms)
- ctx 通过函数参数传递(不挂 struct field)
- exporter 默认 none(日志带 trace),可切换 stdout/OTLP
3. 可观测性:API + Health + Dashboard
Cluster Info API 之前硬编码 nodes: 1。现在:
mgrServer.WithClusterProvider(&grpc.ClusterProvider{
NodeCount: func() int { return len(clusterNode.Membership().Members()) },
HealthChecks: func() map[string]string {
return map[string]string{"cluster": "ok"}
},
})
Dashboard 自动生效——之前请求 /api/v1/cluster 永远返回 1 节点,修完后自动显示 3 节点。
4. 并发控制 + goroutine 安全
| 位置 | 修复 |
|---|---|
router.forwardToNode | 去掉了每条消息 go 的开销(Batcher.Send 是非阻塞的) |
shard_store session sync | asyncSubmit semaphore 限制 16 并发 |
RaftNode.Stop | select/channel 防 double-close panic |
handleDisconnect | nil session 检查移到 trace 创建之前 |
5. 测试体系
| 层级 | 现有 | 新增 |
|---|---|---|
| 单元测试 | 77 文件 | core/node_test.go, transport/grpc/server_test.go |
| 集成测试 | integration_test.go | scripts/integration-test-phase1.sh |
| 压力测试 | 手动 | 持续运行脚本 |
最终清单
| 能力 | 状态 |
|---|---|
| MQTT 3.1.1 + 5.0 全协议 | ✅ CRITICAL 0 残留 |
| QoS 0/1/2 跨节点 | ✅ 批量 stream + 同步确认 |
| MultiRaft 选举 | ✅ 跨节点 leader election |
| Session 跨节点 | ✅ Raft async replication |
| OTEL Tracing | ✅ 三档 exporter |
| 日志系统 | ✅ 组件 logger + trace ctx |
| Dashboard | ✅ 3 节点显示 |
| 压力测试 | ✅ 4800 msg/s, 0 errors |
| 测试覆盖 | ⚠️ 核心路径已覆盖 |
| 文档 | ✅ 3 篇掘金 + README + 测试计划 |