一、前言
携程的微服务框架产品从 2013 年发展至今,已经历了 7 年多的打造。其中所使用的服务注册中心也从最开始人工数据维护架构演进到了现在全自动、百万容量级的架构。本文将逐一回顾携程服务注册中心所经历的三轮迭代过程,并重点介绍最新的第三版架构的设计与实现。
二、服务注册中心是什么?
图 2-1 微服务架构
微服务架构中所要解决的最核心的技术问题有两点,一个是服务发现,另一个是负载均衡。而服务注册中心就是用来解决服务发现问题的。
如图 2-1 所示,在微服务架构中,服务提供方(ServiceProvider),需要手动或自动地将服务地址注册到服务注册中心(Registry)。注册的信息包括但不限于 ServiceID 和 URL。服务消费方(ServiceConsumer)在首次调用服务前,需要先从服务注册中心查询对应服务的注册信息,然后依据返回的服务地址信息来发起调用。
三、携程服务注册中心演变史
3.1 人工数据维护阶段
在携程微服务架构推广初期,为了快速搭建微服务体系,服务的调用过程继续使用传统的域名方式来进行。在服务治理层面,服务提供方首先需要将服务的一个完整 URL 提交到注册中心。服务消费方在运行时会定期从注册中心同步最新的 URL,并发起服务调用。而这个 URL 与应用服务器的关联关系则由运维人员人工在负载均衡设备上配置。
这种模式下的服务注册中心的优点是结构简单、容易实现且运维工作量小,有利于微服务架构快速推广。而缺点则主要集中在以下几个方面:
配置复杂:负载均衡和服务发现的数据依赖人工维护,影响开发效率和体验。
单点问题:服务调用强依赖于负载均衡设备,该设备的可用性会直接影响到微服务体系的可用性。
性能问题:服务调用需要经过一层负载均衡设备,存在额外的网络开销,会直接影响到性能。
3.2 基于 etcd 的服务注册中心
在携程微服务体系扩展到 Java 平台时,我们希望能够解决前面由于使用外部负载均衡设备所带来的各种缺陷,所以计划将负载均衡设备的功能集成到微服务的 SDK 中,同时由注册中心下发的服务注册信息从之前的固定使用域名的 URL,改为服务集群各台服务器 IP 所对应的 URL。
改进后的工作流程是这样的:服务提供方启动后,SDK 会把包含本机 IP 的服务实例地址上报给注册中心;而服务消费方启动后,SDK 会定期从注册中心获取最新的服务地址列表,并使用内置的负载均衡算法选出一个地址来发起请求。同时,为了保证服务注册数据的有效性,其中设置有“存活时间”(TTL,Time to live)。所以需要服务注册中心支持清理过期的注册数据。在设计新的架构时,综合以上这些考虑,我们选择了 etcd 来存储服务注册数据。
图 3-1 基于 etcd 的服务注册中心架构
基于 etcd 的服务注册中心整体架构,如图 3-1 所示,包含三个角色。
Client
提供应用接入服务注册中心的基本 API。应用通过嵌入到应用程序内的 SDK,实现服务的自注册和自发现。
Session
负责处理 Client 提交的服务注册和发现请求。Client 的请求经 Session 协议转换后,直接转发给 etcd。etcd 的响应数据经 Session 的服务治理逻辑处理后,再返回给 Client。
etcd
负责存储服务注册数据。集群内各节点间使用 Raft 协议来进行数据同步。在没有网络分区的情况下,节点上数据可以做到完全一致。
etcd 满足 CAP(Consistency:数据一致性、Availability:服务可用性、Partition-tolerance:分区容错性)中的 CP,即优先考虑分布式缓存的数据一致性。从其设计的出发点来看,etcd 不适合对读写性能要求特别高的场景,而是适合量小且需要高可靠和一致性数据存储服务,比如配置数据、K8s 中的集群元数据等等。
在经过一段时间的线上部署和运维后,我们发现 etcd 中存在潜在的可用性和性能问题。
先说下可用性问题。假设 etcd 集群存在 A、B、C、D 和 E 五个节点,A 是当前集群的 Leader 节点。如果此时发生网络分区故障,其中 A、B 在一个分区,而 C、D 和 E 在另一个分区。Leader A 向所有的 Follower 发送心跳,但无法获取到大多数节点响应(计算公式为(N+2)/2,即在拥有五个节点的集群中需要至少获得三个节点的响应)。心跳超时后,集群进入选举阶段。但受到网络分区的影响,A 和 B 都无法获得大多数节点投票。所以由于缺少 Leader,A 和 B 所在的分区会处于不可用的状态,无法写入数据。
再说下性能问题。etcd 所有的写操作都由 Leader 节点负责执行。而自注册服务实例的健康检测,是依赖注册中心数据中的过期机制实现的。所以各个服务实例需要不断的发送心跳,来保持数据的活跃和有效。但这样就会产生大量的写操作,对 Leader 节点的性能和网络带宽都是一个极大的挑战。
在服务发现的场景下,服务注册中心的可用性比数据一致性更加重要。数据不一致可以通过客户端容错(比如熔断或踢出不可用服务器等),来把影响降到最低,甚至可以忽略不计。而可用性的下降将直接会导致服务的注册和发现异常,甚至会引发大规模的生产故障。
综合以上问题,并考虑到 etcd 无法很好的接入携程当时的运维和监控体系,我们走上了自研服务注册中心的道路。
3.3 携程自研的服务注册中心
在设计这套自研的服务注册中心时,我们参考了当时业界使用比较广泛的由 Netflix 开源的 Eureka。新版注册中心同样没有使用外部存储,而是将服务的注册数据保存在内存中。节点间采用对等的架构设计。所有节点都可以接受客户端的读写请求。节点间会进行数据同步,实现数据最终一致。
在基本的服务注册和发现功能外,为了提升效率,我们还在其中增加了服务变更通知推送功能。这样客户端可以以最快的速度获取到更新的服务注册信息,目前已经实现了服务实例的秒级上下线。
我们将这套全新的服务注册中心的开发代号起名为 Artemis。为了简单,后文中均以该开发代号来进行指代。
四、Artemis 架构说明
4.1 总体架构
图 4-1 Artemis 架构
Artemis 的整体架构与基于 etcd 的服务注册中心类似。如图 4-1 所示,一共包含四个角色:
Client
提供应用接入注册中心基本 API。应用通过引用 Artemis 对外提供 SDK,以编程方式实现服务注册和发现。
Session
负责接受 Client 的服务注册和发现请求。Session 作为中间层将服务提供方的注册请求复制分发给 Data,并从 Data 上查询服务注册数据或推送数据变化给服务消费方。Session 节点自身是无状态的,集群规模可随着 Client 的规模增长而扩容,支持 Artemis 服务能力的水平扩展。
Data
负责存储服务注册数据,数据按 ServiceId 进行一致性哈希分片存储,通过多副本备份保证数据的高可用。Data 集群规模可随着注册数据量增长而持续扩容,从而支持 Artemis 数据存储容量的水平扩展。
MetaServer
负责从 K8s 同步 Artemis 集群服务器地址列表。在 Artemis 集群发生变化时,MetaServer 会实时通知到 Session。Session 在程序启动或者收到 Artemis 集群变化通知时,将主动从 MetaServer 拉取最新的 Artemis 地址列表并缓存到本地。
4.2 如何支持海量数据
分布式系统在处理海量数据时,首先是考虑如何拆分数据,其次是在数据拆后的如何保障系统的可用性。
Artemis 使用一致性哈希环来拆分数据。一致性哈希环的基本使用方式是通过一个哈希函数来计算数据或节点的哈希值,令该哈希函数的数据值域为一个环,即哈希函数输出的最大值是最小值的前序,节点依据其哈希函数计算结果分布在环上,每个节点负责处理从自己开始逆时针至下一个节点全部哈希值域上的数据。Artemis 使用服务注册数据的 ServiceId 来计算哈希值,这样可以保证同一个服务的注册数据可以被存储在相同的节点上,减少网络调用的操作量。
例:假设一致性哈希函数值域是[0, 8),系统中有三个节点 A、B、C,分别处于一致性哈希环的 2、5、6 位置。由此可知,节点 A 的负责范围为 [7,8)和 [0,3),节点 B 的负责范围为[3, 6),节点 C 的负责范围为[6, 7),如图 4-2 所示。
图 4-2 一致性哈希环
使用一致性哈希环拆分数据的优点在于可以任意动态扩容或缩容节点。对集群进行扩容或缩容的操作,仅会影响与被操作节点相邻的节点上的数据分布。
但最基本的一致性哈希环用法存在一个很明显的缺陷,那就是环上的节点分布不均匀。由此所带来另一个较为严重的问题是,当一个节点出现异常时,该节点的压力会全部转移到相邻的一个节点;而当一个新节点加入时,只能为一个相邻节点分摊压力。
一种常见的改进算法是引入虚拟节点(virtual node)的概念。系统在初始化时,每个真实节点都会对应的创建多个虚拟节点。虚拟节点的个数一般远大于集群中服务器的个数。依据虚拟节点的哈希值,系统将它们分布到环上。在操作数据时,首先需要通过数据的哈希值在环上找到对应的虚拟节点,然后从元数据中查找到对应的真实节点,再进行数据读写操作。使用虚拟节点有多个好处。首先,一旦系统中某个节点出现不可用,其对应的所有虚拟节点也会同时变为不可用,从而它的服务压力会被均衡的分配到多个相邻的真实节点上。同理,一旦系统中加入一个新节点,也将在环上引入多个虚拟节点,从而使得新节点可以均衡的分担多个真实节点的压力。从全局看,这种实现方式更加容易实现集群扩容时的负载均衡。
Artemis 使用一致性哈希环加虚拟节点的方法,实现了海量数据的分片存储和集群扩缩容时的负载均衡。而在数据拆分后的集群可用性方面,Artemis 则是通过数据副本策略来保障的。每一条服务注册数据同时被存储在多个节点上,其中一个是主副本节点,其余的是从副本节点。假如我们需要在集群中选择 N 个节点来存储同一条数据,那么在根据哈希函数计算出数据中 ServiceID 的哈希值后,系统会从哈希值落到环上的位置开始顺时针依次选择连续 N 个不同的真实节点来存储这一份数据的各个副本。
图 4-3 一致性哈希环(6 个虚拟节点,2 个注册数据副本)
4.3 服务实例秒级上下线
在设计服务注册中心时,一个重要的考量指标就是服务实例的上下线延迟。这个延迟是指从服务注册中心确定服务实例发生了上下线变更起,到服务消费方收到更新后的注册数据的时间间隔。延迟越高,则服务流量切换所需时间就越长,涉及到流量切换的场景(故障转移、服务发布)给用户带来的体验就越差。
为了降低服务实例的上下线延迟,Artemis 基于 WebSocket 实现了服务实例的上下线通知功能。通知可以秒级送达到服务消费方。这一功能的具体实现过程如下:
服务消费方在初始化过程中,会先经 Session 域名查询 Session 的 IP 地址列表并缓存到本地,然后再从列表中选择一台 Session 服务器与之建立 WebSocket 长连接,并发送服务订阅请求。
Session 在收到服务订阅请求后,先会将服务订阅信息和 WebSocket 连接的映射关系存储到本地。后续当 Session 收到 Data 推送的服务变更消息时,它会先从上述映射关系中查询该服务对应的变更订阅方(即对应的 WebSocket 连接列表),然后将消息通过这些连接推送出去。
在收到服务变更消息后,服务消费方会根据消息的内容更新本地缓存中的服务地址列表。
4.3.1 服务实例上线过程
图 4-4 服务实例上线过程
如图 4-4 所示,这是一次服务实例正常上线过程,其中包含了服务注册数据在 Artemis 内部的流转过程。
服务提供方发送注册数据给 Session。
Session 收到服务实例的注册数据,依据 ServiceID 在环上查找到相应的 Data 节点列表,再将数据写到对应的数个 Data 节点上。
Data 在收到数据后,先将数据写入本地缓存,然后推送服务实例上线消息给所有的 Session 节点。
Session 在收到服务实例上线消息后,将消息推送给对应的服务消费方。
服务消费方在收到服务实例上线消息后,将消息中所包含的服务地址加入到本地缓存中的服务地址列表,后续客户端 SDK 中的负载均衡模块将分配部分流量给新上线的服务实例。
4.3.2 服务实例下线过程
图 4-5 服务实例正常下线过程
服务实例下线分为正常下线和异常下线两种情况。正常下线并不会在服务消费方引起调用异常,而异常下线则可能会导致服务消费方出现短时间的调用异常。
服务实例正常下线,一般是通过监听应用程序关闭事件(如 JVM 的 Shutdown Hook),主动触发服务实例注销操作,将服务实例从 Artemis 中删除。服务下线大致过程与服务上线过程类似,这里就不再赘述了。
图 4-6 服务异常下线过程
服务实例异常下线,是指服务因意外情况(如宕机、网络中断或断电等)而不可用,但没有将注册数据从 Artemis 中删除。这些异常的注册数据,依赖 Artemis 的健康检测机制进行处理。类似于 Eureka 和 etcd 等系统中的数据过期机制,Artemis 中的服务实例注册数据以 Lease(租约)的形式存在,需要服务提供方不断发送心跳来续约。同时 Artemis 内部会运行一个异步线程来自动踢出到期的 Lease。异常下线的服务实例由于不会再继续上报心跳,它的注册数据在一段时间后(TTL)将自动被 Artemis 清理掉。
Artemis 也支持用户在客户端自定义健康检测逻辑,当应用程序不健康时,应用程序可以主动更新服务提供方的状态或停止上报心跳。那么服务提供方状态又是如何被服务消费方感知到的呢?当服务提供方注册数据修改后,服务注册数据会生成一个新的版本号(单调递增),并在下一次上报心跳时,发送给 Artemis。Artemis 在收到服务提供方的心跳后,会先检查心跳中服务注册数据的版本号。如果该版本号大于本地 Lease 中的服务注册数据版本号,Artemis 就会更新 Lease 中的服务注册数据,并生成一条服务变化消息,逐级经 Data、Session 推送给服务消费方。
五、小结
本文介绍了携程的微服务注册中心架构演进迭代过程,并重点介绍了当前版本服务注册中心(Artemis)的架构,以及海量数据存储和服务秒级上下线机制的实现。在携程微服务架构演进的过程中,服务发现的主要方式从手工维护,逐步过渡到自注册和自发现,解决了注册地址的维护复杂性问题。负载均衡的主要实现方式也从外部设备的负载均衡,逐步过渡到在应用程序内嵌代理(SDK)的软件负载均衡,解决了单点问题和性能问题,但同时也带来了多语言、多版本 SDK 维护的复杂性问题。
现在,携程正在构建全新的 ServiceMesh 平台,计划以 K8s 替换 Artemis 来作为服务的注册中心,并通过 sidecar 模式将服务发现、负载均衡以及一些切面功能(例如熔断、限流、监控等)从 SDK 中剥离出来,使得这些功能可以独立于用户应用外进行更新升级。ServiceMesh 与云原生技术的推广,将极大的提升服务的治理效率,为携程的微服务开发者和使用者带来更上一层楼的使用体验。
参考文档
1、https://github.com/netflix/eureka
2、https://github.com/sofastack/sofa-registry
作者简介
Alex,携程资深软件工程师,关注微服务架构及分布式缓存技术。
本文转载自:携程技术(ID:ctriptech)
评论