前阵子折腾一个实时通知类项目,WebSocket 单机服务从测试环境里几千连接,突然被放到了线上几万甚至几十万连接面前。单机版本跑起来倒是轻快,可真要顶着在线量做分布式集群,才发现要解决的全都是单机时代根本不存在的痛点:连接存在哪台机器上别人怎么知道、消息怎么从业务服务转发到正确的连接、连接断了怎么恢复。这篇文章就把这段时间踩过的坑、用过的方案、最终落地的一些判断依据完整分享出来,项目里直接能抄。
1. 单机方案:看起来够用,实际藏了一堆雷
很多项目最开始做 WebSocket 都是这个套路:Spring Boot 或者 Netty 起一个服务,客户端连上来,服务端往一个 ConcurrentHashMap 里塞 connection,收到消息就遍历 map 找到目标连接直接 send。业务简单,代码量少,联调测试也完全没问题。我最初那版就是这么干的,一个 JVM 里跑个 Nginx 做端口转发,后端起两个 WebSocket 服务实例,靠 Nginx 轮询把连接打散,表面上是集群,实际上两个实例各管各的连接,互相看得见对方的连接不存在,消息推给 A 实例的用户,如果用户连在 B 实例上就永远收不到。
单机方案真正的风险不在代码逻辑,而是它把“连接”当成了一种无限资源在消耗。每条 WebSocket 连接在服务器上都不只是套接字文件描述符,它背后还有 TCP 收发缓冲区、应用层的心跳定时器、加密握手上下文、业务侧的用户会话对象。我后来按实际内存占用估算过,一条空闲连接光服务端对象和缓冲区加起来大概要吃掉 10KB 到 30KB 的内存,这还不算框架自身的封装。一台 8G 内存的云主机,跑默认 JVM 堆配置,就算文件描述符放开到十万,连接撑到三四万的时候 GC 就已经开始肉眼可见地不健康了。
单机方案的另一个硬伤是发布和重启。只要服务需要发新版、改配置、调参数,就必然会出现一段窗口期,所有连接同时切断。客户端如果没有重连机制,用户在那一瞬间感受到的就是“页面突然收不到消息了”。如果项目里还挂着定时任务在凌晨触发,重启那一下正好赶上消息推送窗口,丢消息基本是板上钉钉的事。
所以等到在线量真正上来,或者业务上开始要求发布不中断、单节点故障不影响全局的时候,就不是“要不要上分布式”的问题,而是“必须怎么上”的问题。只要做了这个决定,后面所有的架构调整都会围绕一个核心目标来组织:让“连接在哪台机器上”这个状态变成集群内所有节点都能感知并查询到的信息,同时让消息的转发路径可以被设计、被监控、被兜底。
2. 分布式集群要先看清瓶颈在哪里
在动手拆集群之前,先把单机的瓶颈拆清楚特别重要,否则很容易把“分布式”做成本地 map 换成了 Redis map,连接还在单机上,只是把会话信息搬了个地方,该挂还是挂。
先列一下影响连接规模的主要因素:
- 文件描述符数量:操作系统默认一般 1024,改到 65535 是很基础的一步,百万连接需要评估单机端口和 fd 的极限。
- 内存:每连接内存成本,上面提过。Netty 的内存池设计会省一些,但业务对象的开销省不掉。
- CPU:心跳包处理、消息编解码、业务回调,连接多了 CPU 会成为瓶颈,尤其 JSON 序列化这种。
- 单点故障:无论单机配置多高,宕机、断网、发布,都意味着服务不可用或连接全断。
- 框架自身的扩展边界:Tomcat 原生 WebSocket 到一定连接数后线程模型开始吃力,Netty 要好很多,但也不是无上限。
分布式集群要解决的不只是“加的机器更多”,而是让连接状态在集群里有一个统一的视图。这句话是整篇文章的题眼。你要知道任何一个用户 ID 对应的连接在哪个节点上,才能把消息准确地送过去;你要知道集群里所有节点的健康状态,才能决定新连接往哪里分;你要知道某个节点挂了之后它身上的连接要怎么重新分配,才能做故障转移。
这个阶段最容易犯的错是把单机的Map<String, Session>直接替换成Map<String, String>存到 Redis,然后所有节点都去 Redis 查“这个用户连在哪”。方向是对的,但只做了会话共享,消息路由、连接生命周期管理、节点摘除通知这些配套全没跟上,最后 Redis 成了单点,Redis 一抖动整个推送链路就跟着颤。
所以在设计分布式方案时,我建议从三个维度去拆:
- 会话注册与发现(连接状态怎么维护、怎么查)
- 消息路由与转发(消息怎么到达正确节点、到达正确连接)
- 生命周期管理(连接断开、节点上下线、客户端重连怎么协同)
下面几节就按这个框架逐个展开。
3. 会话注册与发现:不要把所有连接都塞进 Redis
先说结论:连接本身是进程内的资源,不可能也不应该序列化到 Redis。Redis 里存的只是“路由信息”,也就是这个用户当前连接在哪台机器上。连接对应的 WebSocket Session 对象,永远只存在于某个节点的本地内存里。
这样一来,分布式环境下的会话模型就变成了两层:
- 第一层:本地会话表,存在于每个节点内存。就是一个 Map,key 是 userId 或者连接 ID,value 是本地 WebSocket Session。
- 第二层:全局路由表,存放在 Redis。key 也是 userId/连接 ID,value 是节点标识,比如
ws-node-002,同时设置 TTL 做自动过期。
连接建立流程大致是这样:
- 客户端通过 Nginx 或被网关路由到某个 WebSocket 节点。
- 节点建立连接后,往本地会话表写入
userId -> Session。 - 节点往 Redis 写入
SET user:1001 ws-node-002 EX 90。 - 节点启动一个定时任务,每隔 30 秒为所有活跃连接续期,也就是重新
EXPIRE user:1001 90。 - 连接关闭时,删除本地会话,并删除 Redis 中的对应 key。
这样做的好处是整个集群的任意节点都能通过一次 Redis 查询知道目标用户连接在哪台机器上。而且 Redis 只存字符串键值对,容量和性能都足够支撑在线用户量几倍甚至十几倍的会话记录。我后来在十万在线用户的压测环境里,Redis 这个表的 QPS 峰值也就几千,完全不是瓶颈。
这里要补一个关键判断:TTL 为什么要设成 90 秒而不是更长或更短?这个值要同时满足两个条件:一是比心跳上报间隔大,至少 3 倍以上;二是节点宕机后,路由信息需要尽快自动失效。如果心跳是 30 秒一次,TTL 设 90 秒就是留出了 3 次续期的容错空间。网络闪断、Redis 连接池暂时拿不到连接这类的偶发情况,不至于让会话被误删。但如果 TTL 设太长,比如 10 分钟,一个节点宕机之后,别的节点依然会往这个死节点转发消息,客户端重连后又会遇到新连接写入和旧路由残留的冲突。
选择 Redis 的时候要注意一个细节:不要用默认的单实例。会话表是核心链路的一部分,Redis 挂了对推送来说是致命的。至少用主从 + 哨兵,有条件就上 Redis Cluster。这个表的写入是SET/EXPIRE/DEL这种简单操作,Cluster 模式下按 key 的 hash slot 分散就好,不需要担心事务问题。
会话表还有一个容易被忽略的用途:顶号处理。同一个用户 ID 在新设备上登录,新连接建立后,正常逻辑应该把旧连接踢下线。做法是:新节点先写 Redis 会话表,GET之前的节点标识,如果存在且不是自己,就向那个节点发一个内部命令,让它把旧连接主动关闭。旧节点再通过本地会话表找到对应 Session 执行close,同时把 Redis 里的 key 覆盖成新节点。这个顺序不能反,先踢旧连接再覆盖路由,否则可能一瞬间两个设备都认为自己是有效连接。
4. 消息路由与推送:定向推送和广播推送要分开设计
会话表解决了“找到连接”的问题,接下来要解决“把消息送过去”的问题。这两个问题不能混在一起讨论,因为定向推送和广播推送在后端实现上走的完全是两条路。
4.1 定向推送:查路由表 + 内部调用
一对一的消息,比如聊天私信、单用户通知,流程最简单:
- 业务服务收到消息。
- 通过 userId 查 Redis 会话表,拿到节点标识。
- 如果节点标识不是自己,走内部 HTTP/RPC 调用目标节点的一个接口,比如
POST /internal/push,把 userId 和消息体带过去。 - 目标节点收到请求后,从本地会话表拿 Session,执行
sendMessage。
这里有一个绕不开的组件:内部调用通道。不管你是用 OpenFeign 调 HTTP 接口,还是直接发 Netty 自定义协议,甚至是走 Dubbo/gRPC,都得有一套节点间通信的能力。我建议不要走公网地址,而是把节点间的内网地址写到服务发现配置里,用独立端口来做内部 API,方便做鉴权和隔离。
这个方案最大的风险点是:如果调用链路上没有兜底,Redis 里查到节点,但请求发过去的时候连接刚好断了,消息就会在这个环节丢。所以我在内部推送接口里通常加一个返回值设计:目标节点收到推送请求后,先查本地会话,如果本地也没有这个连接,返回一个明确的状态码,调用方拿到这个状态码后就可以触发离线消息保存逻辑。这样不会把错误留到调用链的最末端才发现。
4.2 广播推送:用 Pub/Sub 还是 MQ?
广播场景,比如直播弹幕、系统公告、全站广播,如果每个节点都去 Redis 查一遍全量会话再逐个推送,会有两个问题:一是全量查询压力大,二是消息到达不同节点的时间不一致,尾延迟不可控。更常见的做法是:让每个节点都订阅一个同一个消息通道,广播消息发到这个通道上,所有节点同时收到,各自负责推送自己本地连接的这部分用户。
这个“通道”选型,我在 Redis Pub/Sub 和 MQ 之间纠结了挺久,最终建议按场景分开:
| 维度 | Redis Pub/Sub | MQ(RabbitMQ / RocketMQ) |
|---|---|---|
| 消息堆积 | 不支持,消费慢就丢 | 支持,可以缓冲 |
| 消费状态确认 | 无 | 有 ACK,可重试 |
| 持久化 | 无 | 有 |
| 适合场景 | 弹幕、实时通知、可容忍少量丢失 | 系统公告、订单通知、不允许丢消息 |
| 运维成本 | 低,复用 Redis | 高,需部署维护 |
如果项目里有现成的 RocketMQ 或者 RabbitMQ,优先用 MQ,因为订阅通知、死信、消费重试这些能力都是直接自带。如果纯粹是轻量级实时推送,不想引入额外中间件,Redis Pub/Sub 也能跑,前提是你能接受客户端重连后漏掉几条消息,以及 Redis 本身没有持久化堆积能力这一点。
我在线上项目里最终是两类通道都用了:广播公告类走 RocketMQ,弹幕这类高频低价值消息走 Redis Pub/Sub。这样既保证重要消息不丢,又不让高吞吐场景把 MQ 的消费压力拉满。
广播推送时还需要注意一点:消息体尽量用二进制或紧凑 JSON,避免在每个节点上做重复的序列化。最好是业务服务只发一次,序列化之后的字节数组直接丢到通道里,各节点消费时直接 send 给客户端。我见过一个项目广播消息里嵌套了两层 JSON 字符串,客户端拿到之后自己又做了一次JSON.parse,一个消息白白浪费了两次序列化开销,高并发下 CPU 损耗非常明显。
4.3 顺序与去重:同一个用户的多个消息别乱序
广播场景下消息顺序问题不敏感,但定向推送场景下,同一个用户的多条消息如果经过多个线程或多次内部调用,很可能出现后发的先到。解决办法有两种:一种是在业务层给消息分配单调递增的序列号,客户端根据序列号做排序;另一种是在推送链路里保证同一个 userId 的消息走同一个线程池,或者用单连接顺序发送,避免并发写同一 Session。
Redis 会话表在这种场景下还能再发挥一个作用:给每个用户记录一个lastMsgId,每次推送前先自增拿 ID,客户端带上这个 ID 做幂等处理。如果消息重复推送,客户端可以根据 ID 去重,不会出大问题。
5. 两种落地结构:从轻量网关到独立接入层
会话模型和路由方案确定之后,整个分布式 WebSocket 系统的骨架就能看到全貌了。根据团队规模和项目复杂度,我实践下来有两种比较顺手的落地结构。
5.1 轻量接入结构:适合中小规模
这种结构的核心是把 WebSocket 接入和业务逻辑放在同一个进程里,通过 Nginx 做负载均衡和粘滞路由,会话表存 Redis,广播走 MQ。
架构长这样:
- Nginx 四层负载均衡,监听 8080,后端是多个 WebSocket 节点。
- 客户端连接请求进入 Nginx,Nginx 根据用户 ID 做一致性哈希,保证同一个用户的所有连接都打到同一个节点。
- 每个节点启动时向注册中心上报自己的节点标识和地址。
- 节点内部的本地会话表 + Redis 路由表同时维护。
- 业务服务需要推消息时,先查 Redis 路由表,决定走本地发送还是内部调用。
之所以 Nginx 要做一致性哈希而不是简单轮询,关键是因为 WebSocket 连接是有状态的,如果同一用户刷新页面后连接被轮到另一台机器,旧连接还没断开,新连接已经建立,就会出现一个用户两个连接并存,推送时很容易发到旧连接上,造成消息“消失”的假象。
Nginx 配置里可以用hash $remote_addr,但更严谨的是用用户 ID 的哈希。让客户端在 URL 里带上 userId,Nginx 从请求参数里取值做 hash。配置片段大概是:
stream { upstream ws_backend { hash $arg_uid consistent; server 10.0.0.1:9001; server 10.0.0.2:9002; } server { listen 8080; proxy_pass ws_backend; proxy_timeout 900s; proxy_connect_timeout 10s; } }如果客户端连接地址里没有 userId 这种参数,就只能退而求其次用hash $remote_addr。这种方案的缺点是同一用户在不同网络环境下可能落到不同节点,所以最好还是业务层把 userId 放到查询参数里。
这种结构的优点是改动量最小。原来已经有单机 WebSocket 服务的团队,把本地 map 换成“本地 map + Redis 路由表”,再给节点间加一条内部调用通道,两天之内就能上线分布式版本。缺点是接入和业务耦合在一起,节点发布时要同时处理连接断开和业务逻辑迁移,后期如果连接规模继续膨胀,每个节点的压力还是会比较平均地上涨。
5.2 独立接入网关结构:适合规模化
如果项目已经明显是“连接数量大、业务消息少”的形态,比如物联网设备接入、IM 长连接,我建议把 WebSocket 接入拆成一个独立的网关层。这样做的核心逻辑是:连接管理是一项与业务无关的基础能力,它自己的扩缩容、生命周期、节点摘除逻辑应该和业务服务彻底解耦。
网关层只做三件事:
- 接受客户端连接,维护连接状态。
- 心跳保活,把会话路由信息写到 Redis。
- 接收业务层通过内部 API 下发的推送指令,从本地会话表找到连接并发送。
业务服务不感知 WebSocket 连接在哪台机器上,它只调用一个“推送服务”的接口,由推送服务内部去查路由表并调用对应网关节点。网关层的节点可以独立扩容,业务服务完全不受影响。这种结构在发布时也可以做到连接无损迁移:网关滚动升级时,新节点先加入集群,旧节点把连接平滑迁移过去或者等客户端重连。
独立网关层的实现语言我建议用 Netty 或者 Go 的 gorilla/websocket,性能和并发模型都比 Spring Boot 自带的 Tomcat WebSocket 好很多。Spring Boot 里的 WebSocket 虽然方便,但连接量过万之后上下文切换和内存开销就会比较明显,Netty 在这方面的资源控制粒度细得多。
5.3 不同技术栈方案选择的小结
简单整理一下我个人的选型参考:
| 项目情况 | 推荐路径 |
|---|---|
| Java 技术栈,连接量不大,团队人少 | Spring Boot + Tomcat WebSocket,加 Redis 路由表 |
| Java 技术栈,连接量大,需要细粒度控制 | Netty 实现 WebSocket 网关 |
| Node.js 技术栈 | socket.io 自带多节点适配,用 Redis adapter |
| Go 技术栈 | gorilla/websocket 单节点,配合自研路由表 |
| 物联网、设备接入为主 | 直接考虑 EMQX 这类专业 MQTT Broker,别用 WebSocket 硬扛 |
6. 容器化部署、网关路由与稳定性细节
架构方案只是第一步,真正让分布式 WebSocket 在线上稳定运行,靠的是各种细节。下面这几块是实际部署和压测过程中反复调整过的地方。
6.1 容器网络与 Nginx 配置要注意的地方
有了多节点之后,Nginx 的配置就不再只是简单转发。除了前面说的一致性哈希,还有几个参数必须根据场景调整。
proxy_timeout这个值建议设置成大于客户端心跳间隔。如果客户端每 30 秒发一个 ping 帧,proxy_timeout至少要大于 35 秒,否则 Nginx 会先于应用层判定连接超时,把连接踢掉。我见过一个项目,客户端心跳是 60 秒一次,Nginx 默认的proxy_timeout是 60s,两边在极限值上拉锯,客户端频繁掉线,排查了三天才发现是这里的问题。
Nginx 四层负载均衡需要用到 stream 模块,默认编译不一定带。使用前先确认nginx -V输出里有没有--with-stream,没有的话要重新编译或换 OpenResty。WebSocket 的握手升级是 HTTP 协议,但后续的帧传输是 TCP 流,四层转发最稳妥,不要用普通的 HTTP 反向代理去处理升级请求。
容器化部署时,不要把 Nginx 和 WebSocket 服务放在同一个 Pod 里用 localhost 通信,除非你有充分的理由。用 K8s Service 做负载均衡时,externalTrafficPolicy建议设置成Local,保留客户端来源 IP,方便做哈希路由和日志分析。如果设置成Cluster,来源 IP 变成节点 IP,哈希策略就失效了,而且跨节点转发会多一跳,延迟和故障面都会变大。
6.2 容器环境下的会话生命周期管理
K8s 环境下做滚动发布时,Pod 会被逐个替换。如果直接kubectl rollout restart,旧 Pod 的 WebSocket 连接会瞬间全部切断,客户端体验到的是“集体掉线”。正确做法是给 Pod 配置preStop钩子和优雅退出。
顺序大概是:
- 下发滚动发布指令,K8s 逐个终止旧 Pod。
- 旧 Pod 收到 SIGTERM 前,先执行
preStop,调用一个内部接口,把自身状态标记为“排空”。 - “排空”状态下的节点不再接受新连接,Redis 路由表里的会话 TTL 也会因心跳停止而迅速过期。
- 等待存量连接自然断开,或者给一个宽限期,比如 30 秒,之后强制关闭进程。
这样新连接会打到健康节点,旧连接在宽限期内会陆续被客户端重连到新节点,整个发布过程不会出现“全部连接同时断开然后同时重连”的连接风暴。
客户端侧也必须有对应的重连退避逻辑。假设一个群发通知触发 10000 个客户端同时重连,如果每个客户端都在 1 秒内重试,新节点瞬间就要扛住一万个握手请求。最基础的做法是随机退避,重连间隔取0~3秒随机值。更平滑的做法是每次重连间隔指数增长,最多加到 30 秒,连接失败时重置。
6.3 心跳、缓冲区与消息体约束
应用层心跳是必须做的,TCP keepalive 不能替代,因为 TCP keepalive 默认探测周期太长,而且只能在连接级别感知对端不可达,不能感知业务层卡死。我做心跳保活的经验值是:客户端每 30 秒发一个 ping 帧,服务端收到后返回 pong;服务端每 60 秒检查一次连接池,把超过 90 秒没活动(没收到 ping 也没发消息)的连接主动关闭。这三个数值之间留出了足够的容错空间,网络抖动不会误杀连接,死连接也不会赖着不清理。
WebSocket 数据帧的大小也要做限制。框架默认对消息体大小的限制各不相同,有的很小,有的不限制。我建议在网关层统一设置一个上限,比如单条消息 64KB。超过这个值的消息要么压缩,要么拆分成多条。这样做的好处是防止恶心客户端发超大数据把节点内存打爆。Netty 里对应的是WebSocketServerProtocolHandler的maxFramePayloadLength参数,Spring Boot 里对应的是container.addCustomHandler里的相同逻辑。
7. 常见问题与排查技巧实录
分布式 WebSocket 的问题排查,难点在于链路长,涉及前端、网关、Nginx、Redis、MQ、业务服务。我把实际过程中遇到的高频问题整理成一张速查表,排查时可以按图索骥。
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 客户端频繁断开重连 | 心跳超时、Nginx 超时过短、机器负载过高 | 看 Nginx 日志、客户端重连时间、服务端活跃连接曲线 |
| 单节点连接数不均衡 | 哈希策略不生效、新节点上线没调整权重 | 检查 Nginx hash 配置,看连接数监控 |
| 消息推送到用户但客户端没收到 | 路由表指向旧节点、连接被顶号、消息丢失 | 查 Redis 路由表,查看目标节点本地会话是否存在 |
| 广播消息延迟高 | MQ 消费慢、某个节点消费阻塞 | 看 MQ 积压量,检查是否有个别节点线程池打满 |
| Redis 抖动导致推送失败 | 路由表查询超时 | 给 Redis 配置连接池超时、重试;会话表加本地缓存 |
| 节点重启后客户端重连风暴 | 没有平滑下线、没有退避 | 配置优雅退出,客户端加随机重连间隔 |
| 顶号后旧连接收不到下线通知 | 踢下线的内部调用没有确认机制 | 新节点发送下线命令,旧节点执行成功后返回结果,失败重试 |
这里单独展开两个我踩得最深的坑。
第一个是本地缓存加不合理导致的会话表不一致。最初我为了减少 Redis 查询压力,在本地做了一层路由缓存,key 是 userId,value 是节点标识,缓存时间设了 5 分钟。结果用户换设备登录后,新节点写了新路由,旧节点还按本地缓存认为用户在自己这里,消息直接发到旧节点,旧节点的本地会话已经没有这个用户,消息就丢了。这个问题的教训是:路由缓存只能用于“查询本地节点是否存在连接”的场景,不能用于“这个用户的连接一定在哪”的强一致判断。最终我把缓存时间缩短到 30 秒,并且每次推送前还是要查一次 Redis 确认,才算稳定。
第二个是广播消息时本地连接遍历效率太差。初版广播实现直接遍历本地会话 Map,每条连接调用一次send,结果 Map 里两万条连接,一条广播消息就会占住线程两秒,其他消息全部排队。后来改成批量预取连接列表,再用连接组或者消息扇出的方式并行发送,延迟从秒级降到百毫秒级。遍历时还要注意 session 为空、连接已关闭这些脏数据,发送前做一次状态检查,否则会出现ClosedChannelException填满日志的情况。
还有一个小技巧值得分享:在 WebSocket 网关进程里打印日志时,一定要把 userId、连接 ID、节点标识放进去,并且用异步日志或日志队列,防止磁盘 IO 拖垮推送链路。我见过一个生产事故,服务本身没有性能问题,结果日志同步写盘太慢,连接事件日志排队排了两百万条,最后内存被日志撑爆,进程直接崩溃。日志是好东西,但别让它成为系统的阿喀琉斯之踵。
8. 方案评价与演进思路
最后从个人角度评价一下这一整套方案。用 Redis 做会话表、用 MQ 做广播、用 Nginx 做路由,这套组合最大的优点是每个组件都是成熟稳定的,没有自研黑科技,出了问题好排查,团队也容易上手。对于绝大多数中小型实时通信场景,比如客服系统、消息中心、弹幕、协同工具,这个架构的性价比是最高的。
它也有天花板。连接量继续往上走,达到百万级甚至更高时,Redis 会话表本身的 QPS、存储容量和网络带宽都会成为瓶颈。这时候一般往两个方向演进:
一是引入专业的实时通信基础设施,比如用 MQTT Broker 替代自研 WebSocket 网关,设备接入和消息路由全部交给 Broker 体系处理,业务只需要做消息的上层业务逻辑。
二是做多级路由,把路由表按用户维度分片,比如按 userId 的 hash 再拆成多个路由集群,每个路由集群只负责一部分用户的连接信息。查询压力被分散到不同的路由集群,单个 Redis 的负载就降下来了。
这两种方向都是架构工程问题,不是普通业务团队在初期需要面对的。如果一个项目每天的活跃连接还没超过十万,核心精力还是应该放在客户端体验和消息可靠性上,不要为了分布式而分布式。分布式本质上是在为故障和流量买单,你买的是稳定性和扩展性,付出的代价是架构复杂度、运维成本、排查问题的时间。判断是否要做的标准很简单:单机方案是否已经出现你无法接受的故障,或者发布流程是否已经严重拖累了迭代速度。
我在实际项目里最深的体会是:WebSocket 分布式集群的难点从来不在 WebSocket 协议本身,而在于如何把“连接状态”这个强有状态的东西,用各种手段变成一个可以被分布式系统管理和调度的对象。会话表、消息通道、节点通信、生命周期钩子,四件事环环相扣,少了任何一个环节,系统都可以运行,但线上一定会出问题。按照这套思路把四件事都补齐,剩下的就是抄配置和压测调优的活儿了。