4. 接入网关水平扩展设计
接入网关是设备与 IoT 云平台之间的第一跳。单台网关即使能承载 10 万连接,也无法同时满足容量、可用性和弹性三方面要求,因此水平扩展是必然选择。但“多开几台”并不等于可扩展:如果网关进程把设备会话、订阅关系、离线消息队列都放在本地内存,那么任何一次扩容、重启或故障转移都会导致大批设备掉线重连,甚至消息丢失。本节围绕四个相互制约的工程问题展开:如何让网关尽量无状态、哪些状态必须保留、请求如何稳定落到同一台网关、以及如何根据负载动态扩缩容。
一、无状态网关:把可外移的状态全部外移
无状态不是指网关完全不保存任何数据,而是指网关实例不持有不可重建的、影响正确性的本地状态。判断标准很简单:任意一台网关宕机后,它承载的设备改由另一台网关接管,业务是否仍然正确。按这个标准,可以把网关状态分为三类:
- 可完全外移:设备身份与权限、订阅关系、会话元数据、QoS 1/2 的未确认消息,放入 Redis 或分布式会话存储,网关只做读写代理。
- 可重建:TCP/TLS 连接本身、协议解析上下文、心跳定时器,属于连接级临时状态,重连后重建即可,不必共享。
- 必须本地保留:单连接内的字节缓冲、TLS 会话密钥、正在进行的报文分片,这些无法跨实例迁移,只能随连接消亡。
工程上常见的做法是:网关只保留“连接级”状态,把“会话级”状态下沉到 Redis。例如设备上线时,网关向 Redis 写入 session:{clientId} 的节点归属、协议版本、cleanSession 标志和最后心跳时间;订阅时写入 sub:{topic} 的订阅者集合。这样新网关接管时,只需读取 Redis 即可恢复会话语义,而不需要原网关配合迁移。
二、有状态会话保持:哪些必须“粘”在网关上
完全无状态在 MQTT 场景下并不现实。以 QoS 1 为例,网关发出 PUBLISH 后必须等待 PUBACK,若此时连接被切到另一台网关,未确认消息的定时重传、报文 ID 分配都会错乱。因此需要区分两类保持:
- 连接级粘性:同一 TCP 连接的所有报文必须由同一网关处理,这是协议栈的硬约束,靠四层负载均衡的源地址哈希或连接 ID 哈希即可满足。
- 会话级粘性:cleanSession=false 的设备重连时,应尽量回到原网关,以复用其本地未确认消息队列和飞行窗口,减少 Redis 往返。
会话级粘性不是正确性要求,而是性能优化。若设备重连到了新网关,新网关从 Redis 拉取会话元数据并重建飞行窗口,业务仍正确,只是多了一次状态加载。因此设计目标应是:连接级粘性必须保证,会话级粘性尽量命中,未命中时能优雅降级。
三、一致性哈希:让设备稳定落到同一网关
要让会话级粘性尽量命中,就需要一个稳定的映射函数:gateway = hash(clientId) mod N。但普通取模在 N 变化时几乎全部重映射,扩容一台机器会导致绝大多数设备换网关。一致性哈希把哈希空间组织成 0~2³²−1 的环,网关节点和 clientId 都映射到环上,clientId 顺时针找到的第一个节点即为其归属网关。增删一个节点只影响环上相邻区段,重映射比例约为 1/N。
为缓解节点分布不均,通常为每个物理网关引入 100~200 个虚拟节点。下面是一个可参考的分配示例(参数为教学假设值):
物理网关: gw-01, gw-02, gw-03
虚拟节点数: 128 / 台
哈希函数: CRC32(clientId)
clientId = "dev-9f3a21"
CRC32("dev-9f3a21") = 0x8C41A7E2 → 环上位置 2353...
顺时针首个虚拟节点: gw-02#v073
归属网关: gw-02
扩容新增 gw-04 后:
仅 gw-02 与 gw-03 之间约 1/4 区段改归 gw-04
其余约 3/4 设备归属不变
需要注意,一致性哈希解决的是“分配稳定性”,不解决“负载均衡”。如果某类 clientId 哈希值集中,仍可能压垮单个网关。因此实际系统常在一致性哈希之上叠加一层负载感知:当某网关连接数超过阈值(例如达到均值的 1.3 倍)时,把新上线设备临时导向低负载节点,已在线设备不迁移。
四、动态扩缩容:从指标到动作的闭环
扩缩容的触发不能只看 CPU。接入网关的瓶颈通常出现在连接数、每秒新建连接数、消息吞吐和内存四个维度。一个可落地的策略是:以“连接数 / 单机容量”为主指标,以“CPU 使用率”和“P99 处理时延”为辅助指标,任一超过阈值即触发扩容,全部低于低水位并持续一段时间才缩容。
扩容动作本身要解决两个问题:新网关如何被感知、设备如何被重新分配。常见做法是网关启动后向注册中心(如 etcd、Nacos)写入自身地址并保持租约,负载均衡器监听注册中心变化并更新一致性哈希环。新节点加入后,只接管新上线设备,已在线设备不主动迁移,避免“扩容引发大规模重连”的次生故障。
缩容则更危险:必须先停止向待下线网关分配新连接,再等待其在线连接自然减少或主动通知设备重连,最后确认连接数归零后再摘除节点。若直接杀掉进程,设备会经历一次非预期断连,QoS 1 消息可能延迟,QoS 0 消息直接丢失。
五、一个可参考的扩缩容判断表
下表给出的是教学示例阈值,实际取值需结合压测结果调整。假设单台网关设计容量为 8 万连接、CPU 4 核。
| 指标 | 扩容触发 | 缩容触发 | 观察窗口 |
|---|---|---|---|
| 连接数 / 设计容量 | > 75% | < 35% | 3 分钟 |
| CPU 使用率 | > 70% | < 30% | 5 分钟 |
| P99 消息处理时延 | > 200 ms | < 80 ms | 5 分钟 |
| 每秒新建连接 | > 3000 | — | 1 分钟 |
扩容步长建议按当前节点数的 25%~50% 增加,避免一次加太多导致哈希环剧烈变化;缩容每次只摘除一台,并串行执行,确认稳定后再处理下一台。所有扩缩容动作都应记录事件日志,包含触发指标、当时连接数、变更前后节点列表,便于事后复盘。
把四件事串起来看:无状态化让网关可替换,会话保持定义了哪些状态必须粘住,一致性哈希让粘性尽量命中且扩容扰动最小,动态扩缩容则把容量变化变成可控的日常操作。四者缺一不可,任何一环缺失都会让“水平扩展”退化为“水平堆机器”。