无状态与有状态服务大揭秘:定义、场景、架构对比及有状态服务重构方法
无状态与有状态服务大揭秘定义、场景、架构对比及有状态服务重构方法本文内容较多分为如下部分无状态服务和有状态服务定义、无状态服务应用场景、有状态服务应用场景、有无状态俩种服务的架构质量对比、实现有状态服务的挑战、有状态服务重构成无状态服务。警告任何情况下都优先考虑使用无状态服务及时你阅读本文包括以后写的有状态服务实现模式后。在分布式系统里节点服务包含了业务处理和数据俩部分。如果数据是从持久化系统加载则认为此节点是有状态的如果数据常驻于节点内业务逻辑是从外部加载进来的则认为是有状态节点。绝大多数系统都是无状态服务服务不需要在服务本地JVM记录同一个客户端如用户设备或者其他作为客户端的服务等请求的历史状态或者记录客户端的信息服务需要的这些数据将从数据库或者RedisMongDB加载。这些服务类似如下登录服务服务节点根据请求用户名从数据库查询用户名和HASH后的密码然后判断请求的密码是否与数据库查询的密码一致搜索服务 服务节点会把查询条件转为ElasticSearch查询JSON请求Elastic Search查询结果电商中库存查询服务节点会根据商品SKU以及查询所在的地区查询存放在Redis的库存信息。同时库存服务节点也会请求商品查询服务查询商品的名称和价格等信息物联网中查询家庭设备的状态服务节点会查询俩个Redis一个是缓存设备信息的Redis一个设备在离线状态的Redis。无状态服务的技术实现非常成熟如果你的服务是无状态仅仅需要增加节点既可实现系统高可用和高性能。 如下图代理服务采用轮询Round Robin或则随机对于客户端C每个服务节点都能提供服务。如果需要扩容简单增加服务节点4即可。需要注意的是随着流量增加而服务节点增加导致代理服务和数据库本身可能是高可用高性能的瓶颈。因此代理服务和数据库节点也应该支持扩展节点。无状态服务的主要优点优点说明实现简单像编写CURD代码那样简单扩容简单增加节点即可代理服务可以使用轮询随机等路由技术实现。与无状态服务对应的是有状态服务要求服务端记录同一个客户端的交互历史数据和状态数据有状态服务通常从外部加载执逻辑而不是数据。比如用户前端流程表单在最终提交前保留在服务端的临时表单数据用户登录后的会话信息放在SessionSession信息存放在JMV内存里物联网的接入网关需要记录长链接到此网关的设备的版本信息。网关服务有可能加载规则脚本规则引擎会根据设备上报的数据来执行特定的业务逻辑游戏服务一群用户会被路由到特定的地图服务器上协作完成游戏通过互联网协作编写一份文档导航系统用户的信息和旅程信息都在一个服务器上。实时导航信息也在这一台服务器上更新Zookeeper内存中保持的有ZTree数据结构它通过ZAB协议保持所有节点数据都一致Redis本身也是有个有状态服务节点它支持在内存存储各种数据结构以及加载执行脚本。它通过从节点来实现高可用。有状态需要客户端始终连接到同一个服务节点持久化连接需要客户端或者代理服务采取的路由策略保证同一个客户任何请求都在同一个节点执行比如来自同一个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个办法有状态服务的状态数据存放在Redis等更为可靠的存储介质。比如用户Session订单购物车等信息存放到redis一些长期存在的数据存放到数据库。 一些高可用基础设施的改进采用了此方案比如Kafka新一代方案[Diskless Kafka](https://www.toutiao.com/article/7580613850573636096/?appnews_article×tamp1766026761use_new_style1req_id202512181059202FECEDBD55AB8CE8CEF8group_id7580613850573636096req_id_new202512181059202FECEDBD55AB8CE8CEF8chn_id-3share_didMS4wLjACAAAAVfSFXxfg6-hPXBlA91hyWI_ffD_1qQBvN0-ctQ0FziQ-e9QwR5XZSLS-3frGcjyhshare_uidMS4wLjABAAAAmteeVOpFKtElQwG-v4vK9j0tOsx0sCxFUFgS5VUJwUoupstream_bizharmony_share_osoriginWechatsourcem_redirectwid1766300898062)的实现AutoMQ 消息本地存储变成存储到对象存储服务里。 Nacos将配置数据放到Mysql数据库中服务端的状态每次都回传给客户端客户端下次调用携带这些状态比如JWTCookie有状态服务把有状态部分单独隔离出来把其他部分放在无状态服务里使用Zookeeper数据库等强一致工具来实现投票元数据管理二阶段提交等而无需自己实现。下图是Spring Boot提供的Session实现方式代替传统的保存会话到内存Spring Boot 配置spring.session.store-typespring.session.store-typeRedis配置后存在内存中的的用户会话数据将序列化后存放在redis中。其他配置还允许使用数据库、Hazelcast等存储系统。需要注意必须确保存储系统的高性能和高可用。在我的一个项目里用此方案把有状态服务改成无状态服务额外引入了一个512分片容量是1024G的Redis集群以避免系统性能问题。另外一种把有状态服务改成无状态服务的方法是服务器每次把状态回传给客户端。适合用户状态数据较少情况。交互过程如下服务在接受客户端请求后将数据放回到Cookie中如加密的用户的信息或者订单信息客户端下次请求服务端从Cookie中取出的状态数据处理请求后新的状态数据再次保存到Cookie中。在还没有无纸办公时代我在派出所办理业务的时候按照要求需要跑多次派出所和其他相关单位派出所工作人员会把需要跑的单位记录在一张纸上每次交互后的结果和剩下需要办的事情都有记录再次去派出所其他工作人员即使没有给我办理过业务也会让我拿出这张纸查看以了解我办理的进度。第三种是在设计有状态服务时候拆分有状态服务。这样好处是让大部分功能保持在无状态服务里。第四种方法与第三种类似使用zookeeper来管理有状态相比于自己实现有状态服务zookeeper/etcd这些基础中间件更为可靠。有状态服务的高可用涉技术实现包括大量内容本书将用后续一节内容说明实现有状态服务的高可用有哪些模式。再次警告有状态服务高可用实现难度较大。类似你正在实现一个RedisKafka这样的中间件。你需要承担为了性能引入的复杂性。