本文内容较多,分为如下部分
警告:任何情况下都优先考虑使用无状态服务,及时你阅读本文,包括以后写的有状态服务实现模式后
分布式系统里,节点服务包含了业务处理和数据俩部分, 如果数据是从持久化系统加载,则认为是此节点是状态的。 如果数据常驻于节点内,业务逻辑是从外部加载进来的,则认为是有状态节点。

绝大多数系统都是无状态服务,服务不需要在服务本地(JVM)记录同一个客户端(如用户,设备,或者其他作为客户端的服务等)请求的的历史状态或者记录客户端的信息,服务需要的这些数据将从数据库或者Redis,MongDB加载。这些服务类似如下
无状态服务的技术实现非常成熟,如果你的服务是无状态,仅仅需要增加节点既可实现系统高可用和高性能。 如下图,代理服务采用轮询(Round Robin)或则随机,对于客户端C,每个服务节点都能提供服务。如果需要扩容,简单增加服务节点4即可

需要注意的是,随着流量增加,而服务节点增加,导致代理服务和数据库本身可能是高可用高性能的瓶颈。因此代理服务和数据库节点也应该支持扩展节点
无状态服务的主要优点
| 优点 | 说明 |
|---|---|
| 实现简单 | 像编写CURD代码那样简单 |
| 扩容简单 | 增加节点即可,代理服务可以使用轮询,随机等路由技术实现。 |
与无状态是服务对应的是有状态服务,要求服务端记录同一个客户端的交互历史数据和状态数据,有状态服务通常从外部加载执逻辑而不是数据。比如
有状态需要客户端始终连接到同一个服务节点(持久化连接),需要客户端或者代理服务采取的路由策略保证同一个客户任何请求都在同一个节点执行,比如来自同一个IP的用户

有状态服务的主要优点如下
| 优点 | 说明 |
|---|---|
| 高性能 | 主要避免了从数据库加载数据的延迟; 也避免了从持久化系统反复加载数据,比如节点1加载数据执行业务逻辑完毕后,当请求路由到节点2的时候,又会在节点2再次加载同样数据 |
| 任意状态数据结构 | 不再需要持久化系统保存状态数据,其状态不限制于JSON,表数据结构。 |
| CAP | 分布式常见的AP能得到满足外,由于数据在同一个节点,其C也能满足。需要注意,但是如果主节点宕机(或者客户端认为C宕机路由到从节点),从节点接管,则C不成立。 |
| 避免使用性能不佳的数据库 | 无状态服务的扩容,也会导致其用的数据库节点扩容。无状态节点因为不需要数据库,则省去了数据库的使用成本,以及其导致的性能瓶颈 |
有状态服务不难实现,但要达成高可用目标,则难得多。 下图是一个通常有状态服务高可用的部署架构。

这个架构里,有非常多的技术挑战性,列表如下。 这些挑战是分布式系统高可用的深水区,后面章节简述如何攻克这些挑战。
| 难点 | 说明 | 举例 |
|---|---|---|
| 持久连接 | 代理服务采用何种策略让客户端保持持久连接到同一个节点,包括客户端的每次请求,客户端重启,服务节点重启,以及扩容缩容后通知客户端重连新节点。如果没有代理服务,则需要客户端实现持久化连接 | 设备总是链接到同一个网关,游戏玩家总是链接到同一个区域服务器 |
| 复制 | 为节点增加一个从节点,数据从主节点备份到从节点。当节点宕机,,从节点接替主节点工作。从节点也可收接收只读服务请求,缓解主节点负载 | Redis主从数据复制,Zk的主从数据复制 |
| 读写一致 | 如果从节点提供读服务,如何保证客户端读到最新写到此分区的数据。 | 阿里云Redis可以配置只有主分片提供服务以避免读写不一致。 |
| 分区 | 把有状态服务分成多个节点,解决有状态服务的存储瓶颈和访问性能。 | 物联网网关集群,Redis数据分片,数据库分区,Kafka分区等 |
| 分区再平衡 | 在扩容时候,如果不差钱,每个分区都再次拆分2个分区,此分区数据只需要复制到俩分区。这种扩容成本极高但数据均匀。如果扩容时候是增加少量节点。则因为持久化连接缘故,新增节点负载长期都较低。扩容效果不能立即显现出来。 | 阿里云的Redis扩容采用的是成倍增长,支持4,8,16....2048个分片。不支持增加少量分区 |
| 分布式共识 | 数据备份给多个节点,当主节点挂掉,采用那个节点的数据为主节点。另外,新当选的的主节点需要继续同步数据到从节点 | ZK通过ZAB实现分布式共识 |
| 集群管理 | 需要感知整个集群的节点状态,其主从的工作状态。代理服务需要获取这些数据以实现路由 | 物联网设备的主网关,Redis集群的代理 |
| 业务逻辑加载 | 如果有状态服务的业务逻辑通过外部加载实现,如果管理这些业务逻辑 | 物联网的规则链,通过配置其规则节点和执行逻辑实现设备在离线,属性上报等执行逻辑。Reids作为有状态节点,可以加载和执行Lua脚本 |
基于无状态服务和有状态服务各自的优缺点,下表总结了俩种服务的架构质量
| 架构特点 | 关注点 | 无状态服务 | 有状态服务 |
|---|---|---|---|
| 高可用 | API网关 | API网关可以实现各种负载均衡策略。如随机,轮询,负载 | API网关实现较为复杂,需要识别客户端,保证每次路由到同一个有状态服务,依据信息可能是客户端IP和端口,也可能是HTTP头中包含的客户ID |
| 故障恢复 | 重启服务即可恢复故障 | 有状态服务重启后,状态丢失。需要重新建立这些状态,如设备的版本信息,用户的购物车。因此故障恢复较慢这些状态需要从持久化系统加载,或者依赖客户端重置状态。 | |
| 故障容错 | 无状态服务支持重试,当服务宕机,API网关可以将请求路由到另外其他服务。不存在单机故障 | 有状态服务宕机,状态丢失。需要在其他节点重建状态,比如有状态服务保持的有用户登录信息,当宕机后,用户再访问其他服务前,需要再次登录。 | |
| 高性能 | 启动性能 | 无状态服务不需要加载状态,因此重启后访问较快。 | 有状态服务,服务重启后,需要初始化端状态,访问性能第一次较慢。重启可能造成服务响应延迟 |
| 访问性能 | 无状态服务通常将状态维护在Redis中,性能稍微慢一些,但考虑到Redis这些延迟都是毫秒级的,整体性能很高。如果状态数据维护在传统数据,则延迟较高 | 有状态服务在JVM内存在保持状态,访问速度更快。如游戏地图,所有玩家都在一个游戏地图(JVM)里有状态服务适合游戏,导航等场景。 | |
| 可修改 | 易于修改 | 通常无状态服务,不需要在JVM里维护状态,实现更简单,易于修改。 | 在JVM里维护的状态,数据结构私有,难以修改,另外涉及到并发访问,编写代码容易出错 |
| 热发布 | 无状态服务可以采用任何热发布技术,而没有风险 | 有状态服务,需要把状态数据迁移到热发布后的结构,难度较大 | |
| 可伸缩性 | 节点扩容 | 集群环境,无状态服务可以任意增加或者减少,无状态服务扩容后,API网关可以通过负载均衡策略,让负载少的这些扩容节点能很快达到满负荷 | 容易造成单点过载。有状态服务扩容后,由于持久化链接需要,至少少量请求能立即路由到新的节点。扩容后需要较长时间新老节点的容量才能达到一个均衡值。 |
既然无状态服务有如此多的优点,除非有高性能要求,架构中应该优先使用无状态服务,如果是有状态的服务,需要改成无状态服务,这里有4个办法
下图是Spring Boot提供的Session实现方式,代替传统的保存会话到内存,Spring Boot 配置spring.session.store-type
spring.session.store-type=Redis配置后,存在内存中的的用户会话数据将序列化后存放在redis中。

其他配置还允许使用数据库,Hazelcast等 存储系统。需要注意,必须确保存储系统的高性能和高可用。在我的一个项目里,用此方案把有状态服务改成无状态服务,额外引入了一个512分片,容量是1024G的Redis集群以避免系统性能问题
另外一种把有状态服务改成无状态服务的方法是服务器每次把状态回传给客户端。适合用户状态数据较少情况

交互过程如下
第三种是在设计有状态服务时候,拆分有状态服务。这样好处是让大部分功能保持在无状态服务里。

第四种方法与第三种类似,使用zookeeper来管理有状态,相比于自己实现有状态服务,zookeeper/etcd这些基础中间件更为可靠

有状态服务的高可用涉技术实现包括大量内容,本书将用后续一节内容说明实现有状态服务的高可用有哪些模式,再次警告,有状态服务高可用实现难度较大。类似你正在实现一个Redis,Kafka这样的中间件。你需要承担为了性能引入的复杂性
",\uD83E\uDD80 用 OpenClaw 实现一个 C++ 复刻版 QuantClaw
Prisma 实战指南:像搭积木一样设计古诗词数据库
SpreadJS V19.0 新特性解密:报表导出黑科技,公式逻辑全保留 | 葡萄城技术团队
RAG 系列(六):向量数据库——存储与检索的基础设施
多线程的这12种用途,99%的人不知道!
kingbase备份与恢复实战(六)—— 备份自动化与保留策略:Windows任务计划+日志追溯