尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Etcd在微服务即时通讯系统中的关键角色与实战指南

Etcd在微服务即时通讯系统中的关键角色与实战指南 在微服务架构大行其道的今天只要是搞过分布式系统的人对Etcd这个名字都不会陌生。但你要是问Etcd在微服务架构里到底干嘛的十个人里有八个会回答服务发现或者配置中心。这两个答案没错但远远不够。尤其是当你把场景切到即时通讯IM这类对实时性、一致性、在线状态极其敏感的系统里Etcd的角色比想象中要复杂得多。本文我想结合自己在即时通讯服务端这块的实际经验专门聊聊Etcd在微服务即时通讯系统里扮演的关键角色它解决了什么问题以及实际工程落地时那些文档里不会写明白的坑。这篇文章适合正在搭建或维护微服务架构IM系统的开发同学也适合那些对Etcd只有概念性认识、想深入了解它在具体场景中如何工作的读者。全文不堆概念只讲实际链路里Etcd是怎么介入的以及为什么这些环节缺了它不行。1. 即时通讯系统为什么绕不开Etcd先看架构痛点很多人在刚接触微服务即时通讯时容易把注意力全放在Netty、WebSocket、消息推送这些看得见的技术上而Etcd这种基础组件往往被一笔带过。但我要说的是恰恰是Etcd把微服务IM系统的骨架撑起来的。1.1 微服务化之后IM系统多了哪三类麻烦先回看一下一个典型的微服务IM系统长什么样。它有网关层负责连接维持、协议解析、逻辑层负责登录、加好友、建群、消息收发、状态层负责在线状态维护、消息存储层负责消息落库和拉取。在单体时代这些模块都在一个进程里大家通过函数调用互相协作根本不存在谁在哪儿的问题。但一旦拆成微服务麻烦立刻冒出来。第一类麻烦是服务发现。网关要把消息转给逻辑层但逻辑层有十几个实例每个实例的IP和端口是动态分配的可能随时扩缩容。网关怎么知道当前有哪些实例可用这就是服务发现要解决的问题。第二类麻烦是配置管理。IM系统有太多需要动态调整的开关比如消息长度上限、单聊禁言策略、群成员上限、灰度开关。配置散落在每个服务的本地文件里改一个全局参数要在几十台机器上同步效率极低且容易出错。第三类麻烦是分布式协调。IM系统里有很多节点需要选主、抢锁、协调状态。比如群聊消息要按序写入某个分片这个分片的主节点挂了要从从节点中选出新主又比如同一个用户连接突然漂移需要全局锁来避免状态被并发覆盖。这三类麻烦恰好是Etcd最擅长的领域。Etcd是一个高可用的分布式键值存储基于Raft协议保证强一致性天然就是为解决微服务场景下的这些问题而生的。它对外提供类似Redis一样的KV接口但有严格的一致性保证并且支持Watch机制——客户端可以订阅某个key的变化一旦发生变化立刻收到通知。1.2 即时通讯场景的特殊性比一般微服务更苛刻同样是微服务架构IM系统对Etcd的要求比普通业务系统更高原因在实时性。一个普通订单系统里服务发现失败最多导致一次请求失败重试即可。但IM系统里如果网关不知道某个用户连接在哪个节点上这条消息就没法及时推送用户感知就是对方正在输入了半天一条消息没发出去这种体验是毁灭性的。更关键的是IM系统有在线状态这个特殊数据。用户上线、下线、心跳超时、断线重连这些状态变化的频率远超普通业务数据的变更频率。一台网关机上可能有几十万条长连接其中任意一条断掉都要反映到状态服务里。这个层面的信息同步不能靠轮询反复轮询的压力太大且时延不可控也不能靠广播广播要通知的节点太多。Etcd的Watch机制就像一个专门为这种场景设计的事件总线——谁关心某个事件谁就订阅它事件一变就推送过来时延控制在毫秒级。所以如果你的IM系统规模已经上了微服务这条船Etcd不是选不选的问题而是怎么用好的问题。下面我具体拆解Etcd在IM系统里的几大核心职责。2. Etcd在IM系统里的四大核心职责服务发现、配置中心、分布式锁、元数据存储不同团队对Etcd的使用深度差异很大。有的团队只用它做服务注册发现有的团队把它当万能数据库什么都往里塞。从我这边实际经验来看IM系统里Etcd的核心职责可以归纳成四个按重要程度排序。2.1 服务发现网关如何精准找到目标实例服务发现是Etcd在微服务里的基本功在IM系统里体现得尤其明显。以一个IM网关发送单聊消息为例用户A发送一条消息给用户B消息首先由A所在网关节点GW-A接收GW-A需要找到B当前连接在哪个网关节点上、那个节点上的哪个连接服务实例能处理这条消息。这时候Etcd就展示了它的价值。所有网关节点启动时会把自己注册到Etcd的某个目录下比如/im/gateway/online/{nodeId}value里带上IP、端口、负载、连接数等元信息。同时逻辑层的消息路由服务和状态服务会对这个目录调用Watch。当B上线后状态服务会把B的在线信息更新到Etcd对应的key上路由服务立刻收到变更通知建立用户ID - 网关实例的映射关系。这个映射关系不会直接在逻辑层本地缓存里长期保存而是一边订阅变更一边维护一份本地缓存。Etcd提供的强一致性在这里意味着网关的注册信息一旦写入成功任何Watch该目录的客户端在比较短的时间内就能看到变更不会因为状态不一致而把消息路由到已下线的节点。2.1.1 一个特别实用的设计把连接能力也注册进Etcd服务发现如果只注册IP和端口在IM场景其实不够用。网关节点是一个进程承载大量TCP连接但同一台机器上可能部署多个网关节点不同节点的剩余连接数、CPU负载是不同的。如果路由模块只看IP和端口做调度很可能把新消息推给一个已经快满载的网关导致消息积压。我们当时在Etcd的注册节点value里加了几个关键字段当前连接数、最大连接数、最近1分钟消息吞吐量、节点启动时间。路由模块在选节点时不是随机选或者RoundRobin轮询而是根据负载信息加权选择。这个信息每10秒刷新一次正常情况下够用不会对Etcd造成太大压力又能让消息路由保持在相对均衡的状态。2.2 配置中心IM运行时的动态开关中枢即时通讯业务的特点是规则多、变化快、需要灰度。运营会提出需求某个群聊的老大要限制消息频率直播间要临时开启关键词过滤某个用户被上了单聊禁言。这些开关如果都写在代码里然后发版上线一周得发三次版本运维会疯。Etcd做配置中心本质上是把配置从文件变成带版本的数据。我们把所有和业务相关的动态配置放在/im/config/{namespace}/{key}下面每个微服务启动时拉取一次完整配置然后对配置目录建立Watch。配置变更后Etcd服务端保存新值并递增版本号Watch到变更的客户端拿新配置替换本地旧配置。这里有个细节容易踩坑对一个大型IM系统配置项可能有几百上千个如果全部放在一个目录下任何一条配置变化都会触发所有服务全量拉取浪费带宽且容易造成抖动。合理的做法是分namespace区分业务模块比如/im/config/message管消息相关、/im/config/security管安全策略每个服务只关心自己对应的namespace减少无效推送。2.2.1 灰度发布和配置回滚的经验光有配置下发还不够IM系统上线新策略之前往往需要小流量验证。比如消息审核规则不可能第一次就全量开启。我们的做法是配置里加一个生效范围字段标识这个配置是全局生效、按用户ID哈希生效还是按群ID生效。发布时先按5%的流量放量没有问题再逐步扩大比例。如果线上出了故障把Etcd里的配置恢复成上一个版本的值即可客户端Watch到变化后会立刻生效不需要重启服务。这个能力在生产环境救过我们好几次比赶着急改代码发版本强太多了。2.3 分布式锁与选主高可用IM里的一致性基石即时通讯系统很多环节要求同一时刻只能有一个实例在干活。最常见的就是连接转移、消息分片的主节点选举、定时清理任务的执行者。以我们系统的群消息分片为例。群聊消息量大的群会被分到不同的分片队列每个分片有一个主节点负责写入消息存储。如果主节点宕机需要快速从从节点中选出新的主节点。这里用Etcd的分布式锁来实现选主。Etcd选主的基本思路是抢一个特定key比如/im/persist/shard/leader/{shardId}。谁成功创建了这个key带租约谁就是主节点。比如两个节点同时抢只有一个能创建成功。这个key的持有者需要定期续约一旦持有者挂掉租约会过期key被自动删除其他节点通过Watch感知到变化立刻重新抢。为了保证选举不出问题我还利用Etcd的事务特性来做CAS操作创建key时带上prevExpect条件如果key已经存在则拒绝创建。这种基于版本号和条件的事务操作比单纯用Redis的SETNX要可靠得多因为Redis在分布式环境下没有严格的一致性保证而Etcd是Raft协议同步的多副本数据一定一致。这个选主机制还有个优点——切换速度快。Raft的选主时间加上租约过期检测通常几百毫秒内就能完成。对IM这种对可用性要求极高的系统这个速度完全能接受。2.4 元数据存储长期连接会话的全局定位器最后这点是我觉得很多人容易忽略的。IM系统里除了用户表、消息表这类的业务数据还有一批系统元数据它们的特点是量不大、单条价值极高、变更频繁比如设备当前连接在哪个网关节点上、会话的全局ID分配器当前水位、消息序号生成器的最后分发位置。这些数据用数据库存太重压在Redis里又担心故障恢复后数据不一致Etcd反而是个恰到好处的位置。我们当时把在线会话信息用户ID、设备ID、网关节点ID、连接ID、最后心跳时间存在/im/session/{userId}/{deviceId}下用租约模式管理用户在线时持有租约并周期性续约断线后租约过期key自动失效这正好完美匹配会话的生命周期。3. 选型对比为什么Etcd能和Redis、Zookeeper在IM场景下区分开聊到这里肯定有读者问你说的这些能力Redis不是也能干吗Zookeeper不是也能干吗为什么偏偏是Etcd这一节我认真对比一下帮大家在技术选型时有个清晰的判断。3.1 Etcd vs Redis一致性模型决定命运Redis的生态和性能都很强大作为缓存和数据存储在IM系统里也有大量应用。但Redis在主从复制模式下并不保证强一致性——主节点写入成功从节点可能还没同步完如果此时主节点宕机未同步的数据就丢了。对于在线会话、网关注册这类数据丢失意味着用户连接状态错乱后果相当严重。当然Redis也提供了RedLock这样的一致性方案但RedLock的复杂度高且强依赖时间假设在工程实践中容错空间有限。反观Etcd它的设计目标就是分布式一致性每条写入都需要经过Raft协议在多数副本上持久化才算成功。在失败恢复后所有节点看到的数据必然是同一份。这就是两者最本质的区别一个是数据工具一个天生是一致性的基础设施。在IM系统里Redis适合放那些允许短暂不一致的数据比如用户最近聊天记录缓存、未读数的短时快照而Etcd适合放那些必须精确、必须不丢的数据比如在线会话定位、集群节点注册表、分布式锁。3.2 Etcd vs Zookeeper同为一致性组件但生态和上手度不同Zookeeper早期是分布式协调领域的事实标准Hadoop、Kafka早期都重度依赖ZK。它和Etcd的核心类似都是基于一致性协议ZAB vs Raft提供强一致性的分布式协调服务。但两者的差异在实际使用中体现得很明显。一方面Zookeeper的数据模型是树形结构节点有持久节点和临时节点的区分API偏向底层watcher机制调用方式相对复杂。Etcd是扁平的KV数据结构API简洁用gRPC提供接口自带证书认证、租约、事务等能力从使用体验上来说更现代化。另一方面Etcd的操作和运维比Zookeeper容易不少它内置了v3版本的gRPC接口和HTTP JSON接口调试方便社区活跃度高和Kubernetes生态绑定紧密。放在即时通讯这个场景里ETCD和ZK在功能上其实都能满足需求但说句实在话如果你是新建项目我找不到非要用Zookeeper的理由。Etcd在开发效率、可观测性、社区维护上都占优何况现在Kubernetes已经把Etcd打造成了云原生时代的基础设施标准招人、排障、找资料都相对容易。3.3 选型判断我的实用建议如果你所在的公司已经有成熟的Zookeeper集群团队对它也熟悉那你继续用ZK完全没问题毕竟它能满足需求。但如果你是做技术选型的新项目我建议直接上Etcd理由有三点第一Etcd是云原生标准组件和Kubernetes天然配套第二它的API和分布式模型更简洁学习成本低第三社区和生态处在快速上升期遇到问题能找到的参考方案更多。单就IM系统来说Etcd的lease租约和Watch机制配合得非常好这两个特性组合起来几乎是为在线状态这种有生命周期、需要事件感知的数据量身定做的。4. 一条消息的完整链路看Etcd如何从入场陪跑到离场概念讲多了容易虚我拿一个IM系统里每天发生几百万次的场景——在线用户发送一条单聊消息来完整走一遍Etcd参与的过程。这条链路走完你就明白Etcd在IM系统里不是可有可无的配角而是每个环节都在起作用的主角之一。4.1 从客户端发消息到路由决策的六个步骤第一步用户A登录。登录请求到达网关GW-AGW-A在Etcd中注册一个会话key路径是/im/session/A/primaryvalue是GW-A的节点信息并且带上租约。租约时间我们设的是60秒客户端需要周期性心跳续约。第二步用户A给用户B发消息。消息进入GW-A后GW-A需要把消息转发给逻辑层消息服务。消息服务收到请求第一件事就是查B当前的会话信息。它先从本地缓存查缓存命中就直接路由没有命中就从Etcd读/im/session/B/primary。第三步消息服务拿到B当前在GW-B上把消息转发给GW-B。GW-B根据会话key里记录的连接ID找到对应的TCP连接把消息推送给用户B。第四步如果B不在线消息服务会将消息写入离线消息存储同时把离线消息通知这个事件记录到Etcd用户上线时监听这个key就能及时拉取离线消息。第五步B的客户端收到消息后会回一个ACK这个ACK本质上是一个状态同步。B的客户端如果因为弱网发生了断线重连重连成功后会向新网关注册会话并主动从Etcd删除旧会话记录或者让旧会话租约自然过期。第六步整个过程中会话状态但凡有变更比如B从GW-B漂移到了GW-CEtcd的Watch都会推送给正在监听/im/session/B/primary的节点消息路由模块立马更新本地缓存避免下一条消息仍然发送到旧网关。4.2 链路里Etcd性能压力到底有多大走完这条链路你会发现Etcd的参与频率相当高登录注册写一次、查询一次、心跳续约每几十秒一次、下线删除一次、状态变更通知一次。一个百万日活的IM系统Etcd的QPS可能到几千甚至上万。这算不算压力从实际经验来说Etcd设计的目标就是大规模协调场景单集群支撑上万QPS写入是能扛住的前提是别把不该放进去的业务数据塞给它。我们系统第1版把用户维度的所有在线信息都塞进了Etcd结果才跑了几十万在线Etcd集群的负载就飙上去了。后来做了两件事才缓解一是把会话信息做了分片按照用户ID哈希拆到多个前缀目录二是给Etcd集群单独建了一个专用集群不和配置数据混在一起。另外一个优化的点是减少不必要的Watch。一个服务Watch的key越多Etcd服务端要推送的消息就越多。我们在设计时把Watch的粒度控制在只关注与自己相关的会话key而不是全量Watch所有人的会话变化。这样既保证了路由的实时性又不至于让Etcd变成一台广播服务器。5. 实战排坑Etcd在IM系统里最容易翻车的几个场景理论部分告一段落接下来这部分我想重点讲讲实际操作中遇到的坑。这些坑在我第一次搭建微服务IM系统时几乎全踩过写出来希望后来者能少走弯路。5.1 租约续约心率和过期时间的取舍租约机制是Etcd管理临时数据的重要手段但在IM系统里它有微妙的平衡。租约时间太短客户端稍微网络抖动一下会话key就会被误删用户明明在线却被判定离线消息推送失败。租约时间太长用户真的掉线后旧key迟迟不消失消息会持续往死节点上推送造成消息丢失。我见过不少团队直接把租约时间设为几小时理由是不想让用户轻易掉线。这在技术上省了持续心跳的功夫但副作用极大——用户掉线后状态长时间不更新路由模块总是把消息推到已经失联的网关上。我们最后定的值是60秒租约、每20秒续约一次。这样的好处是续约时间足够短服务器能容忍3次心跳丢失而不会话过期过期时间又足够快一旦真的网络断开最多60秒就能把用户状态归为离线。这里有个技巧值得分享心跳续约不要做成同步的续约-等待-续约每条消息都阻塞等待吧浪费RTT。我们做成异步批量的每20秒一次性把本节点所有需要续约的会话key打包续约这样既保证时效性又极大降低Etcd的写压力。5.2 误删线上数据Etcd的事务版本号救场Etcd的数据不是在数据库里的它本质上是带版本号的多版本存储。在IM系统里我们要更新一个注册信息经常是先读出来看当前版本再写入新值。如果中间有人改了它我们这次的写入就应该失败而不是覆盖。这个场景在有双网关同抢一个用户连接时尤其常见。用户A在Wi-Fi和5G之间切换可能旧网关的连接还没断开新网关的注册请求就过来了。两个节点同时尝试写/im/session/A/primary如果只是简单地把旧值覆盖掉可能导致旧网关认为用户还在线新网关也认为用户归自己管两边同时试着推消息出现消息重复推送。Etcd的事务能力就是为解决这类问题设计的。我们用if语句判断key的当前版本号是否等于我们读到的版本号只有等于才更新否则说明有人抢先更新了我们放弃或者重新读再做决策。这个能力放Redis里做起来麻烦在Etcd里几乎是天然支持的用好了能避免大量线上事故。我现在做IM相关开发时凡是对同一个key的并发写操作一定会带上版本校验这已经成了我的习惯。5.3 集群部署中的脑裂与性能隔离Etcd本身基于Raft理论上是能自动应对网络分区的但在实际部署时还是要注意一些细节。首先是部署规模。Etcd集群建议最小也要3节点有条件就5节点。节点太少一旦一个宕机剩余节点凑不齐多数派整个集群就没法提供写入服务。IM系统是全天候服务Etcd集群一挂全局会话都查不到、注册不了整个消息系统直接停机这种事故我经历过一次非常酸爽。再者是物理隔离。Etcd是IO敏感型应用对磁盘延迟和网络延迟都有要求不要和业务服务混布在同一台机器上更不要和大流量的消息存储共用磁盘。我们最初为了省机器把Etcd节点和网关部署在一起结果网关高峰期把磁盘IO和带宽打满Etcd的写入时延飙到几百毫秒直接拖垮了全部会话注册和状态更新。后来把Etcd集群移到独立机器上时延立刻恢复正常。6. 从Etcd出发一套可迁移的微服务基础设施方法论聊了这么多关于Etcd在即时通讯系统中的具体实践其实如果你想跳出IM这个场景看这套方法论同样适用于其他对实时性和一致性要求较高的微服务系统比如直播弹幕、多人协同编辑、实时游戏对战匹配。核心思想很简单微服务架构下任何需要被多个节点共享、且要求强一致性的小块状态都应该考虑放在像Etcd这样的分布式一致性存储层里而不是塞进业务数据库更不应该放在每个服务的本地内存里独自为政。这个层解决的问题本质上是让哪个节点在线、这个节点在干哪个活、谁拥有这个分片这类基础事实在集群中有唯一且准确的答案。Etcd能成为这套体系的支点靠的是Raft一致性协议保证所有节点看到同一份数据靠Watch机制让变化能以毫秒级时延推送给关心它的人再配合lease租约让临时状态能自动过期这三板斧完美匹配了微服务时代分布式协调的需求。最后我额外分享一个小建议如果你的IM系统刚刚起步流量还不大别急着把Etcd的职责无限放大。它再强大也不是数据库扛不住海量业务数据的存储。把Etcd定位成系统的控制面而非系统的数据面让控制面的信息保持精简、准确、实时让数据面去承载真正的海量消息流量——这样各司其职系统才能又稳又有弹性地成长。这算是我这几年在微服务即时通讯系统上摸爬滚打最想传递给后来者的一个经验。
返回列表