前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >干货|携程注册中心整体架构与设计取舍

干货|携程注册中心整体架构与设计取舍

作者头像
悟空聊架构
发布2024-06-26 12:25:03
880
发布2024-06-26 12:25:03
举报

作者简介

Siegfried,携程软件技术专家,负责携程注册中心的研发。

一、前言

目前,携程大部分业务已经完成了微服务改造,基本架构如图。每一个微服务的实例都需要和注册中心进行通讯:服务端实例向注册中心注册自己的服务地址,客户端实例通过向注册中心查询得知服务端地址,从而完成远程调用。同时,客户端会订阅自己关心的服务端地址,当服务端发生变更时,客户端会收到变更消息,触发自己重新查询服务端地址。

疫情刚过去那会,公司业务回暖迹象明显,微服务实例总数在1个月左右的时间里上涨30%,个别服务的单服务实例数在业务高峰时可达万级别。按照这个势头,预计全公司实例总数可能会在短时间内翻倍。

实例数变大会引起连接数变大,请求量变高,网络报文变大等一系列现象,对注册中心的性能产生挑战。

如果注册中心遇到性能瓶颈或是运行不稳定,从业务视角看,这会导致新增的实例无法及时接入流量,以至被调方紧急扩容见效慢;或者导致下线的实例不能被及时拉出,以至调用方业务访问到已下线的实例产生报错。

如今,业务回暖已经持续接近2年,携程注册中心稳定运行,强劲地支撑业务复苏与扩张,特别是支撑了业务日常或紧急情况下短时间内大量扩缩容的场景。今天就来简单介绍一下携程注册中心的整体架构和设计取舍。

二、整体架构

携程注册中心采用两层结构,分为和数据层(Data)和会话层(Session)。Data负责存放被调方的元信息与实例状态、计算RPC调用相关的路由策略。Session与SDK直接通讯,负责扛连接数,聚合转发SDK发起的心跳/查询请求。

注册 – 定时心跳

微服务架构下,服务端的一个实例( 被调方)想要被客户端(调用方)感知,它需要将自己注册到注册中心里。服务端实例会发起5秒1次的心跳请求,由Session转发到对应分片的Data。如果数据层能够持续不断的收到一个实例的心跳请求,那么数据层就会判断这个实例是健康的。

与此同时,数据层会对这一份数据设置TTL,一旦超过TTL没有收到后续的心跳请求,那么这份数据也就会被判定为过期。也就是说,注册中心认为对应的这个实例不应再被调方继续访问了。

发现 - 事件推送/保底轮询

当收到新实例的第一个心跳时,数据层会产生一个NEW事件,相对应地,当实例信息过期时,数据层会产生一个DELETE事件。NEW/DELETE事件会通过SDK发起的订阅连接通知到调用方。

由于网络等一些不可控的因素,事件推送是有可能丢失,因而SDK也会定时地发起全量查询请求,以弥补可能丢失的事件。

多分片方案

如图所示,Data被分成了多分片,不同分片的数据互不重复,从而解决了单台Data的垂直瓶颈问题(比如内存大小、心跳QPS等)。

Session会对服务ID进行哈希,根据哈希结果将心跳请求、订阅请求、查询请求分发到对应的Data分片中。调用方SDK对多个被调方进行信息查询时,可能会涉及到多个Data分片,那么Session会发起多个请求,并最终负责将所有必要信息聚合起来一并返回给客户端。

单点故障

与很多其他系统类似,注册中心也会遇到故障/维护等场景从而遭遇单点故障。我们把具体情况分为Data单点故障和Session单点故障,在两种情况下,我们都需要保证系统整体的可用性。

单点故障 – Data

如图所示,SDK发起的心跳请求会被复制到多台Data上,以保证同一分片中每一台Data的数据完整性。也就是说,同一个分片的每台Data都会拥有该分片对应的所有服务的数据。当任一Data出现故障,或是参与到日常运维被踢出集群的情况下,其他任一Data能够很好的接替它的工作。

这样的多写机制相比于之前版本注册中心采用的Data间复制机制更加简单。在Data层发生故障时,当前方案对于集群的物理影响会更小,可以做到无需物理切换,因而也更加可靠。

在当前多写机制下,Data层的数据是最终一致的。心跳请求被分成多个副本后是陆续到达各个Data实例的,在实例发生上线或者下线时,每台data变更产生的时间点通常会略有不同。

为了尽可能避免上述情况对调用方产生影响,每台Session会在每个Data分片中选择一台Data进行粘滞。同时,SDK对Session也会尽可能地粘滞。

单点故障 – Session

参考上文提到Data分片方案,任一Session都可以获取到所有Data分片的数据,所有Session节点都具备相同的能力。

因此,任一Session故障时,SDK只需要切换到其他Session即可。

集群自发现

携程注册中心是基于Redis做集群自发现的。如下图所示,Redis维护了所有注册中心实例的信息。当一个注册中心实例被创建时,新实例首先会向Redis索要所有其他实例的信息,同时开始持续对Redis发起心跳请求,于是Redis维护的实例信息中也会新增新实例。新实例还会根据从Redis拿到的数据向其他注册中心实例发起内部的心跳请求。一旦其他实例从Redis获得了新实例的信息,再加上收到的心跳,就会认可新实例加入集群。

如下图所示,当时注册中心实例需要维护或故障时,实例停止运行后不再发起内部心跳。其他实例在该节点的内部心跳过期后,标记该节点为unhealthy,并在任何功能中都不会再使用该节点。这里有一个细节,节点下线不会参考Redis侧的数据,Redis故障无法响应查询请求时,所有注册中心实例都以两两心跳为准。

我们可以了解到,注册中心实例的上线是强依赖Redis的,但是运行时并不依赖Redis。在Redis故障和运维时,注册中心的基本功能不受影响,只是无法进行扩容。

三、设计取舍

新增代理还是Smart SDK?

注册中心设计之初只有Data一层,由于要引入分片机制,才有了Session。那么是不是也可以把分片的逻辑做到SDK,而不引入Session这一层呢?

这也是一种方式,业界也一直有着代理和Smart SDK之争。我们基于注册中心所对应的业务场景,认为新增一层代理是更加合适的。

最重要的一点,注册中心的相关功能运行不在BU业务逻辑主链路上,其响应时间并非直接影响业务的响应时间。因此我们对注册中心的请求响应时间并没有极致的要求,代理层引入的几百微秒的延迟可以被接受。

其次注册中心的请求是一定程度容忍失败的,SDK请求数据失败后可以继续使用内存中的老数据,不会对业务线产生致命影响。因此代理层引入的失败率也可以被接受。

另一侧,代理的加入带来了诸多好处。最直接地,落地分片逻辑不需要所有的SDK升级,分片逻辑迭代时,对业务也是无感。

其次,代理层也隔离了连接数这一瓶颈,当SDK层的实例不断变多,连接数不断增加时,只需要扩容代理层就能解决连接数的问题。这也是我们将它取名为Session的原因。

同时,我们也希望作为物理层的SDK逻辑更加轻量,比较重的逻辑放在逻辑层,这样稳定性更强更不容易出错。比如后续会提到的“Data按业务隔离分组”就是在Session层实现的。

普通哈希还是一致性哈希?

携程注册中心的数据分片是采用普通哈希的,并没有采用一致性哈希。

我们知道,一致性哈希相比普通哈希的最大卖点是当节点数量变化时,不需要迁移所有数据。

结合注册中心的场景,我们用服务ID做哈希,而服务数量(也包括实例数量)是相对稳定的,因此哈希节点的扩容周期会比较长,基本用不到一致性哈希的优势特性。哪怕一段时间内业务迅速扩张,只要提前做好预估,留好余量一次性扩容就好了。

我们选择普通的固定的哈希,并让每一个分片都具备多个备份节点,这样就基本可以认为每个分片都不会彻底挂掉,不用去实现数据迁移的逻辑,整个机制更简单了。

要知道,数据迁移需要对注册请求、查询请求和订阅请求进行同步切换,要处理好各种状态,避免在数据迁移过程中错查到空数据或者丢失变更事件,非常复杂危险。

自发现是否强依赖Redis?

前面也提到,注册中心自发现的运行时是不依赖Redis的。有的同学可能会想到,如果运行时强依赖Redis,就可以去掉两两注册了。

两两注册确实是一个不好的设计,随着集群的节点数越来越大,其产生的性能开销肯定也会更大,影响整个注册中心集群的拓展能力。

但在目前规模下,内部心跳占用的系统资源并不可观。哪怕规模再拓展,通过降低心跳的频率,进一步降低资源开销。

最大的好处是,Redis集群故障或者维护时,并不会对注册中心的功能产生影响。

基于Redis还是用Java写?

目前注册中心的Data是用Java实现的。有的同学可能会想,Data层主要就是维护微服务实例的存活状态,能不能直接用Redis实现呢?如果用Redis,不就可以直接复用Redis体系的扩容/切换能力了吗?

比如基于Redis 6.0的Client Cache功能,通过Invalidate机制通知SDK重新更新服务信息。

不过在携程注册中心设计之初,Redis版本还比较老,没有这些新feature,感觉基于pub/sub机制做注册中心还挺麻烦的。现在注册中心已经稳定运行了好久,加了很多功能,比如路由策略一部分的计算过程就是在Data层完成的,暂时没有必要推倒重建。

总的来说,用Java写更可控,后续自定义程度更高。

四、需要注意的场景

突发流量

在遇到节假日,或是公司促销活动,亦或是友商故障的情况下,公司集群会因为业务量急剧上升而迅速自动扩容,因而注册中心会受到强劲的流量冲击。

期间因为系统资源被榨干,注册/发现请求可能会偶发失败,事件推送延迟和丢失率会上升。严重时,部分调用方业务会无法及时感知到被调方的变动,从而导致请求失败,或流量无法被分摊到新扩容的被调方实例。

我们发现,这些场景产生的流量有着很高的重复度,比如某个被调方实例扩容,调用方的众多实例需要知道的信息是完全一样的,又比如调用方实例扩容,这些新扩的实例部署着相同的代码,它们依赖的被调方信息也是完全一样的。

因此我们针对性的做了不少聚合与去重,大大降低了突发流量情况下的资源开销。

流量不均衡

关于Data粘滞,这里有一个细节。那么多Data机器,Session选谁呢?目前Session是用类似随机的方式选择Data的。那就会有一个场景,我们对Data层进行版本更替,逐个实例重新发布,当一个实例被重置时,Session就会因为丢失粘滞对象而重新随机选择。

我们会发现,最后一个Data实例完成发布时,它不会被任何Session选中。而第一个发布的Data实例,它倾向于被更多的Session选中。

通常来说,越早发布的Data实例,就会被越多的Session选中。也正因为如此,更早发布的Data会承担更多的流量,而最后发布的Data一般不承担流量。这显然是不合理的。

解决这个问题的方法也很简单,我们引入拥有全局视角的第三者,整体调控Session的粘滞,保证Data尽可能地被相同数量的Session选中。

全局风险

前面也提到,Data层被分成了多分片,Session会对服务ID进行哈希,将心跳请求、订阅请求、查询请求分发到对应的Data层分片中。

当程序出现预期外的问题(程序bug,OOM等等)导致某个Data无法正常的履行功能职责时,那些被分配到这个Data实的服务就会受到影响。

如果调配方式是对服务ID做哈希,那么所有业务线的任意服务都可能参与其中,从业务视角去看,就是整个公司都受到了影响。

对服务ID做哈希是有它的优势的,它无需引入过多的外部依赖,只需要一小段代码就能工作。但我们还是认为避免全局故障更加重要。

因此我们最近对Data引入了业务语义,将Data分为多个组,以各个业务线命名。且我们可以按服务粒度对数据进行分配。默认情况下,我们会将服务分配到自己BU的分组上。

这样,我们就具备了以下能力:

1)不同业务线的数据可以被很好的隔离,任一业务线的Data数据出现问题,不会影响到其他业务线。

2)注册中心将获得故障切换的能力,当个别服务的数据出现问题时,我们可以将它单独切走。

3)我们可以将一些不重要的应用单独隔离到一个灰度分组,新代码可以先发布到灰度分组上,尽可能避免新代码引入的问题直接影响核心业务分组。

4)注册中心将获得应用粒度的部署能力。在集群分配上,具备更强的灵活度,针对业务规模的大小合理分配系统资源。

从图中可以看到,我们在引入分组逻辑的同时也兼容老的分片逻辑,这样做是为了在分组逻辑上线过程初期,服务信息在Data层的分布可以尽可能保持不变,可以让少数的服务先灰度切换到新增的分组上进行验证。

当然,从去复杂度的角度考虑,最终分片逻辑还是要下线,垂直扩容的能力也可以由分组实现。

五、后续规划

因为注册中心引入了分组机制,并对各个业务线数据进行了隔离,注册中心的集群规模也在因此膨胀,分组数量较多,运维成本也随之上升。

后续我们计划进一步优化优化单机性能,精简优化一些不必要的机制,降低机器数量。

同时,我们也希望注册中心能够支持弹性,能够在业务高峰时自动扩容,在业务低峰时自动缩容。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 悟空聊架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
微服务引擎 TSE
微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档