首页 > 云计算 > 虚拟化 > 斗鱼直播云原生实践——注册中心篇
孔令圳,斗鱼首席架构师,全面负责斗鱼全站技术架构体系规划和建设,10 余年中大型互联网产品架构经验,擅长高并发、高可用场景下的架构与方案设计。
于竞,斗鱼技术保障运维专家,负责斗鱼高可用基础架构建设,擅长注册中心、监控体系等技术领域,同时也是斗鱼多活基础保障负责人。
唐聪,腾讯云资深工程师,极客时间专栏《etcd 实战课》作者,etcd 活跃贡献者,主要负责腾讯云大规模 k8s/etcd 平台、有状态服务容器化、在离线混部等产品研发设计工作。
陈鹏,腾讯云容器服务产品架构师,多年专注云原生领域,帮助了大量用户云原生容器化改造和生产落地,拥有丰富的一线实践经验,也发表了海量的云原生技术文章。
斗鱼直播作为业界领先的游戏直播平台,每天为数以亿计的互联网用户提供优质的游戏直播观看、互动和娱乐等服务。
随着近年直播市场的火热,斗鱼直播平台作为业内口碑和体验俱佳的互联网公司,用户量也出现井喷式增长。海量用户给平台带来的稳定性技术挑战也越发强烈,斗鱼的老架构如下图所示,无论是业务支撑还是架构设计,均存在一定的风险和隐患。

图一 斗鱼老架构
为了给用户带来更好的可用性体验,斗鱼急需解决单一数据中心的问题,将老架构从单数据中心升级到多数据中心。
在实现单活升级为多活的过程中,为了确保无故障的迁移升级,我们面临一系列挑战,比如:
因单活升级到多活的过程中,涉及系统众多,本文将是斗鱼直播多活改造系列的第一篇,只聚焦于注册中心模块,因此我们先和你介绍下注册中心背后的 etcd 和 zookeeper。
zk/etcd 承担的角色
dubbo 通过注册中心来解决大规模集群下的服务注册与发现问题,以下是注册中心架构图:

dubbo 默认支持 zookeeper 注册中心,虽然新版也有 etcd 实现,但该实现尚缺乏大规模投产的先例,Java 技术栈采用 etcd 作为注册中心的案例也比较罕见。
当采用 zookeeper 作为 dubbo 注册中心时,其注册关系为树形结构,详细结构如下图所示:

因为 zookeeper 是基于类似文件系统的树形结构来存储数据,但 etcd 却是采用键值对存储,二者之间的差异会给注册关系同步带来较大困难。
此外,如果从 zookeeper 迁移到 etcd,则在整个迁移过程中:已有的线上服务不能受损,更不能停服;如果迁移失败,还要能回退到到 zookeeper。
为了实现多活,我们通过跨数据中心的同步服务、服务依赖梳理与边界划分、可控变更等技术手段和运维理念,成功解决了以上挑战,设计了如下一套新的架构来实现多活,如下图所示:

图二 斗鱼多活新架构
在新的架构下,可以按域名甚至是 URL 来细粒度的调度流量,RPC 层面也具备了自动就近调用的能力,其中注册中心的局部架构图如下:

图三 斗鱼注册中心老架构
在注册中心多活改造过程中,我们面临多个方案,如下表所示:


由于历史原因,我们有 zookeeper(以下简称 zk)和 etcd 这 2 套注册中心,加上我们有 Java、Go、C++、PHP 这 4 个技术栈,因此在注册中心领域仍然一些不足,希望能统一到 etcd 来解决痛点问题,并达到以下目标:
降低维护成本:此前需要运维 zk+etcd 两套注册中心,更困难的是做多活解决方案时也需要适配 zk+etcd,这导致注册中心多活研发成本翻倍。由于 etcd 是 k8s 的一部分,运维 etcd 又不可避免,这是选择 etcd 的第 1 个原因。
拥抱更繁荣的生态:etcd 有云原生托管解决方案,有厂商通过 etcd 管理 10K node 级别的 k8s 集群,etcd 还自带 proxy、cache、mirror 等各种周边工具,java 侧 dubbo 也支持以 etcd 作为注册中心,etcd 相对于 zk 来说发展前景更好,这是选择 etcd 的第 2 个原因。
增强跨语言能力:etcd 可基于 http 或 grpc 协议通讯,并且支持长轮询,具有较强的跨语言能力。而 zk 需要引入专用客户端,除 java 客户端之外,其它语言客户端尚不成熟。而我们有 JAVA、Go、C++、PHP 等 4 种研发语言,这是选择 etcd 的第 3 个原因。
基于以上原因,我们选择了方案四,方案四大新架构如下图所示:

图四 斗鱼注册中心新架构
为了实现新注册中心,达到我们期望的设计目标,注册中心在改造过程中,面临以下难点与挑战:
我们很多 java 开发的业务使用 dubbo 框架做服务治理,注册中心是 zookeeper,我们希望 java 和 go 开发的业务全部都统一使用 etcd 作为注册中心,也为跨语言调用的可能性做好铺垫。
由于业务众多,改造和迁移的周期会很长,预计持续 1~2 年,在此过程中我们需要将 zookeeper 中的注册数据同步到 etcd 中,实时同步,而且要保证数据一致性以及高可用,当前市面上没有找到满足我们需求的工具,于是我们和腾讯云 TKE 团队合作开发了一个 zk2etcd 来同步实现 zookeeper 数据到 etcd,并且已将其开源,整体方案落地篇我们将详细介绍。
通过 zk2etcd 同步服务,我们成功解决了 zookeeper 数据迁移问题,使得新老业务的注册中心数据都使用 etcd 来存储。
因此,etcd 的重要性不言而喻,它的可用性决定着我们的整体可用性,而斗鱼直播目前的部署架构又严重依赖某核心机房,一旦核心机房出现故障,将导致整体不可用。因此斗鱼直播下一个痛点就是提升 etcd 的可用性,期望实现 etcd 跨城容灾、异地容灾能力。
斗鱼直播理想中的 etcd 跨城同步服务应该具备如下特性:
那么有哪些方案呢?各个方案又有哪些优缺点呢?最终评估了如下几种方案:
单集群多地部署方案图如下:

在此方案中,etcd Leader 节点通过 Raft 协议将数据复制到各个地域的 Follower 节点。
此方案它的优点如下:
各地域网络互通后,部署简单,无需运维额外组件
数据跨城强一致同步,3 节点部署场景中,可容忍任一城市故障,并且不丢失任何数据
介绍完它的优点后,我们再看看它的缺点,如下所示:
在 3 节点部署的场景下,任意写请求至少需要两个节点应答确认,而不同节点部署在各地,ping 延时会从几毫秒上升到 30ms 左右(深圳 - 上海),因此会导致写性能急剧下降。
etcd 默认的读请求是线性读,当 Follower 节点收到 Client 发起的读请求后,它也需要向 Leader 节点获取相关信息,确认本地数据追赶上 Leader 后才能返回数据给 client,避免读取到旧数据等,在这过程中也会导致 etcd 读延时上升、吞吐量下降。
跨城部署网络之间质量也较容易波动,导致服务质量抖动等。
client 访问 etcd 集群的配置,为了防止单点故障,必须配置多个 etcd 节点,而这又可能导致 client 访问到异地的 etcd 节点,导致服务请求延时增大等。
介绍完单集群多地部署方案后,我们再看看 etcd 社区提供的 make-mirror 方案,它的原理图如下:

在此方案中,我们分别在不同城市部署了一套独立的 etcd 集群,通过 etcd 社区提供的 make-mirror 工具实现跨城数据复制。
make-mirror 工具原理如下:
此方案它的优点如下:
介绍完它的优点后,我们再看看它的缺点,如下所示:
介绍完 etcd 社区的 make-mirror 方案后,我们再看看 etcd 社区提供的 learner 方案,它的原理图如下:

它的核心原理如下:
此方案它的优点如下:
介绍完它的优点后,我们再看看它的缺点,如下所示:
介绍完已有的几种方案后,我们发现它们都无法满足业务生产环境诉求,于是我们自研完成了生产环境可用的 etcd 同步服务落地,在整体方案落地章节将详细介绍。
为了确保 etcd、zk 同步服务的稳定性,模拟 5 类常见的故障,检验服务在这些典型故障场景下的自愈能力,详细测试方案如下。
redis 闪断 (zk2etcd 服务依赖),例如:redis 版本升级、非平滑扩容。
zk 2etcd 离 线,例如:OOM、容器驱逐、宿主机故障。
etc d2etcd 离线 ,例如:OOM、容器驱逐、宿主机故障
网络闪断,例如:OOM、容器驱逐、宿主机故障。
弱网环境,例如:专线断掉后临时用公网顶替。
上述 5 种场景的实际触发原因有多种多样,只需要模拟出一种情况。
redis 闪断:通过改 host 模拟 redis 不可达,此时自动订正停止;模拟 redis 恢复后,自动订正亦自动恢复。
z k2etcd 离线:通过杀容器节点模拟 zk2etcd 挂掉,15 秒内 k8s 自动拉起,拉起完成后同步正常、数据一致。
etc d2e tcd 离线 :通过杀容器节点模拟 zk2etcd 挂掉,15 秒内 k8s 自动拉起,拉起完成后同步正常、数据一致。
网络闪断: 通过改 host 模拟 zk、etcd 不可达,此时同步中断,后去掉 host 模拟网络恢复,恢复后同步正常、数据一致。
弱网环境: 通过切公网模拟弱网环境,切公网后同步效率降低在 4 倍以内,1 次全量同步仍然可在 1 分钟内完成。
另外针对可运维性问题,无论是 etcd 还是 zk 同步服务,都提供了详细的 metrics、日志,我们针对各个核心场景、异常场景都配置了可视化的观测视图,并配置了告警策略。
etcd 集群多活架构图如下所示:

说明
黑实线:正常情况下的专线访问
黑虚线:切公网方式访问
红实线:etcd 集群,发生主备切换后的专线访问
红虚线:etcd 集群,发生主备切换后的公网访问
etcd2etcd/zk2etcd 数据同步服务图如下所示:


zk同步服务工程化实践
zookeeper 与 etcd 存储结构不一致,加大了同步的实现难度。zookeeper 存储是树状结构,而 etcd v3 是扁平结构。zookeeper 无法像 etcd 一样按照 prefix 来 list 所有 key;etcd 无法像 zookeeper 一样通过 list chilren 来查询某个目录下的子节点,也加大了实现同步的难度。
如何感知 zookeeper 中的数据变化?zookeeper 的 watch 不像 etcd 一样可以简单的感知到任意 key 的新增,需要递归的 watch 所有的节点,收到 ChildrenChanged 事件后拿到该事件对应节点下的所有子节点,再与 etcd 中的数据进行比对,就可以得到新增的数据,并将其同步 put 到 etcd 中。类似的,可以用递归的方法 watch 所有节点的删除事件,并同步删除 etcd 中的数据。
另外 zookeeper 的 watch 有着先天性的缺陷,watch 是一次性的,所以每次收到事件后又必须重新 watch,两次 watch 之间理论上是可能丢事件的,主要是在同一个 key 连续多次变更的时候可能会发生。如果丢事件发生就会破坏了数据一致性,我们引入了自动 diff 和订正的能力,即计算 zookeeper 和 etcd 中数据存在的差异,每次都会经过两轮 diff 计算,因为在频繁变更数据的情况下,一轮 diff 计算往往存在一些因不是强一致性同步导致的"伪差异",当 diff 计算出了结果就会自动 fix 掉这些差异。
如何解决与 etcd2etcd 共存?当同一个路径下,即有 etcd2etcd 同步写入的数据,又有 zk2etcd 写入的数据,在 zk2etcd 的自动订正逻辑里面,会计算出差异并订正差异,但我们不希望因此而误删 etcd2etcd 写入的数据。我们通过为 zk2etcd 引入了 redis 来存储状态解决了这个问题,在 zk2etcd 往 etcd 中同步写入或删除数据时,也同步在 redis 中记录和删除:

然后 zk2etcd 在自动订正计算差异的时候,只考虑本工具写入过的数据,避免误删其它同步工具写入的数据。

为了解决 etcd 同步难题,我们调研了如下两种方案,接下来我们就详细介绍下它的原理:
首先我们介绍下 etcd-syncer 的 mirror-plus 方案,顾名思义,它是 etcd 社区 make-mirror 的加强版。为了解决 make-mirror 的各种缺陷,它实现了以下特性、优点:
那么它的缺点是什么呢?因为它核心原理依然是依赖 etcd 的 mvcc+watch 特性,因此数据无法保证强一致性和只同步 key-value 数据。
不支持同步非 key-value 数据,如 auth 鉴权相关数据、lease 数据等。
为了解决所有类型的数据同步问题以及消除对 etcd mvcc 历史数据的依赖,腾讯云还可提供基于 Raft 日志的同步方案 etcd-syncer 之 raft 版本。
它的部署图如下所示,etcd-syncer 同步服务作为一个类似 learner 节点的身份,加入主 etcd 集群。

主 etcd 集群 Leader 将 Raft 日志数据通过 MsgApp/Snapshot 等消息同步给 etcd-syncer, etcd-syncer 解析 Raft 日志,将 Raft 日志条目对应的 Txn/Delete/Auth 等请求应用到目的 etcd 集群。
它具备如下优点:
具备 etcd-syncer 之 mirror-plus 版本的所有特性和优点,同时不依赖 etcd mvcc 历史数据。
基于 etcd 底层的 Raft 日志同步数据,可以同步 key-value、auth、lease 等各种类型的数据。
不依赖高版本的 etcd。
此方案引入了 grpc-proxy 代理服务,也是头一次使用。为了了解此代理服务的性能情况,我们使用 etcd 自带的 benchmark 进行了读和写的测试,另外手写了一个小工具做了一下 watch 测试。以下为部分测试内容。
写入测试
直接访问 etcd 服务的负载均衡入口

走 grpc-proxy 代理访问 etcd 服务的情况

读取测试
直接访问 etcd 服务的负载均衡入口

走 grpc-proxy 代理访问 etcd 服务的情况

grpc-proxy 代理在 endpoints 配置走专线或公网情况下,都能正常读取
走 grpc-proxy 代理的平均耗时相比直连有所增加,但在可接受范围
watch 测试
根据我们自己写的一个 etcdwatcher 服务对 grpc-proxy 进行 watch 测试:可以设置总 watcher 数量,更新频率,以及测试时间,结束时打印出简报
./etcdwatch -num=100 -span=500 -duration=10 -endpoint= test done total 100 task 0 task failed current revision is 631490 least revision is 631490 0 task is not synced
参数说明:
num 任务数量
span 更新间隔,单位毫秒
duration 总测试时间,单位秒
current revision:代表写入的 revision
least revision:表示 num 个任务中同步最慢的 revision
failed 为 0 说明正常;如果过出现 task not sync 说明 watch 和 put 不同步
以上测试结果来看:failed 数为 0,watch 测试正常
我们使用的是 1.2.5 版本,通过 k8s 的 deployment 方式部署
模拟 zk server 失联
通过将 hosts 中注入错误解析地址

2.模拟 redis 失联

3. 模拟etcd失联
16:15:10 etcd server 失联
16:30 恢复
16:45 重启 pod

期间 fixed operation 数量没有增长,其他监控指标未发现明显异常
此后重启,fixed operation 数量有所增涨(不能确定是 full sync 未生效,还是重启后刚好有更新修复导致)
总结
只要 full sync 机制工作正常,各异常场景发生后,都能在下一个 full sync 触发后被恢复
恢复的最小时间间隔取决于设置的 full sync 定时执行间隔时间(默认为 5m),业务对此间隔时间容忍情况自行调整参数
此外,为了避免异常发生后,full sync 机制定时运行但也没能感知到情况发生,保险起见事后可以第一时间重启一下 zk2etcd 服务
对于追加的 etcd 公网测试,full sync completed 和 zk、etcd 操作耗时,相比内网情况有一定(秒级)增长
etcd2etcd 的同步服务,我采用 deployment 双副本部署
多副本 backup 能力
⼯作节点故障后备⽤节点会在 5s 后接管同步任务
etcd syncer 双实例部署
杀掉正在运行的工作节点进行观察
不论是增量同步还是全量同步过程中,主备切换都能正常工作(需要注意的是,当全量同步中发生主备切换后会变为增量同步,从而可能导致比对较慢)
断点续传能力
故障恢复后能从断点继续开始同步
其实在第 1 部分,备节点切换为主后接管同步工作,fast_path 变为 1 也证明了断点续传能力,我们还额外补充几个验证场景:
(a) 短时间故障
故障场景
中心 etcd 集群到热备集群的同步过程中,因作为源的中心 etcd 集群中也存在 -etcd-syncer-meta- 的 key,触发了同步服务报错(同 txn 中不能包含相同的 key),出现了数据差异
现象
将同步服务运行参数添加对 -etcd-syncer-meta- 的过滤,然后观察进过一段时间追赶数据后,最终 miss 数降去达到一致
(b) 长时间故障
故障场景
停止同步服务的部署 deployment
等待两边 etcd 集群产生数据差异,并发生一次 compact 后再启动同步服务
现象
等产生数据差异,并发生 compact 后,重新启动同步服务,其日志如下:因 compacted 发生,触发全量同步
同步服务监控指标:(a) dst miss key 很快降下去;(b) src miss key 有所增加,并持续不降
分析

同步服务停止以后,源 etcd 的 key 数量发生不少变化,监控图看出期间有下降,说明发生过 key 的删除
这里也暴露出一个小问题,当出现 src miss key 的时候,目前不能自动修复,需要人工接入清理多余的 key
当同步发生重大差异(如,发生 dst miss)进行紧急修复的时候,通过配置 --reset-last-synced-rev 参数删除断点续传信息,来触发全量同步修复差异
因某种异常,同步出现 dst miss(图中黄线实例)的情况。为了进行修复,新实例添加 --reset-last-synced-rev 参数后运行


分析
slow_path 为 1,说明触发全量同步(图中绿线实例)
绿线实例的 dst miss 值没有增长起来,说明已经达到一致
两 etcd 集群之间专线中断
增量同步中
全量同步中
测试方案
当专线中断切换公网时,需要修改运行参数中的 etcd 集群访问地址,即:必会发生重启(重启场景测试前面已经涵盖,这里不再重复)
总结
etcd-syncer 同步服务有较好的主备机制,能够及时有效的进行切换
短时间故障后的断点续传表现符合预期;对于长时间故障,同时发生 compact 的复杂情况时,恢复同步后出现 src miss 的情况,可能需要人工接入
通过配置 --reset-last-synced-rev 参数对 src miss 的异常修复有较好的效果
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69984638/viewspace-2794360/ ,如需转载,请注明出处,否则将追究法律责任。