vlambda博客
学习文章列表

从Nacos到完全自研|得物的注册中心演进之路

      近几年,随着得物业务的快速发展,应用数目与实例规模在快速地增加。 得物的注册中心也在不断的演进优化,以支撑业务的发展。 从最初的 Nacos 到 Nacos Proxy 引进再到完全自研的 Sylas 替换 Nacos,期间遇到了哪些问题,如何解决这些问题,以及未来的演进方向,是本文主要探讨的内容。

Nacos架构演进

1.1  单集群 Nacos

直到2021年上半年之前,得物团队交易域使用的都是3节点的 Nacos 集群作为注册中心,随着得物的业务发展,实例规模越来越大,导致原本的 Nacos 集群已经无法稳定支撑,甚至出现过数次集群雪崩的现象。


1.2 Nacos Proxy + 多集群 Nacos

由于 Nacos 单集群无法支撑业务规模,增加了 Nacos Proxy 用来对请求做分片,将不同的服务请求打到不同的 Nacos 集群上。



在这种部署模式下,性能问题暂时得到缓解,但实际上没有根本解决问题,只是降低了集群出现雪崩的概率,在大规模的场景下需要做进一步的优化和思考。


自研注册中心Sylas

2. 1  背景

由于上述 Nacos 的种种问题,并且随着得物容器化推进、业务扩张对注册中心的要求会越来越高,开源的 Nacos 已经不能满足公司的需要了,并且 Nacos 本身的代码扩展性较差,而且无用功能和复杂逻辑较多,直接对 Nacos 进行二次开发的成本较大,基于种种考虑决定自研注册中心彻底替换 Nacos。

2.2  架构

注册中心整体组件架构如下:


从Nacos到完全自研|得物的注册中心演进之路


自顶向下依次为客户端层、API层、代理层、服务端层,SRE Portal 作为控制面管控全局,Discover Sync 作为跨集群同步组件,支撑多活、小得物【1】等注册中心同步场景。


客户端目前主要支持原生 Nacos SDK、自研 Sylas SDK,也可以支持直接用 HTTP 协议访问。


Proxy 的核心功能是分片、路由、对 Sylas 节点的健康检测、多分片聚合、广播等等,Proxy 作为无状态组件,承接海量的客户端请求,可横向扩展分摊压力。


Sylas Server 作为注册中心核心组件,保存服务实例信息,并且提供丰富的查询能力,并且支持增量订阅、快照等功能。


部署架构如下图所示:

从Nacos到完全自研|得物的注册中心演进之路


Sylas 集群通过 Raft 协议保证数据一致性,最大容忍 N/2-1 的集群故障,Nacos Proxy 保留原来的分片功能,保证注册中心的横向扩展能力,并且增强路由、健康检测等能力,默认 Proxy 会做读写分离,写请求路由给 leader,读请求时则根据客户端的 IP 做 Hash 取余选择 Follower 节点,实现了顺序一致性。


2. 2. 1 为什么要用 Raft

Nacos 默认使用的是 AP 的 Distro 协议,如果有3节点 Nacos 集群,意味着每个节点负责 1/3 服务的写入操作,每个服务端节点都存储所有数据,但每个节点只负责其中一部分服务,在接收到客户端的 “写”(注册、心跳、下线等)请求后,服务端节点判断请求的服务是否为自己负责,如果是,则处理,否则转发由负责的节点处理。


Distro 协议的优势是每个节点可以承担一部分写请求;并且在网络分区情况下不处理脑裂问题,所有节点仍可写,但是也引入的脑裂本身的问题;缺点是由于AP无法保证数据可靠性,并且大量请求的相互转发在高并发实践下性能较差,并且本身不支持持久化功能。


Raft属于强一致性,可以在同时数据的强一致性、分区容错、节点故障()。Raft 有 leader 的概念,所有写请求都由必须 leader 处理, Proxy 基于这个特性很容易做读写分离;Raft 虽然是强一致性协议,但是可以通过不同的读取策略来实现不同的读一致性来调整性能;Raft是基于日志复制的协议,并且有快照功能,所以很容易做数据恢复;最重要的一点,一切纸上谈兵都是纸老虎,而 Raft 是现在业界用的最多的一致性协议,成熟的落地组件非常多如 etcd、consul、tidb,有大量的实践资料,基于上述综合原因选择了 Raft 算法作为集群的数据一致性协议,但是也不代表这是最完美的选择,每一种方案都有其优缺点,重要的是权衡清楚利弊带来的影响


3.3 优化

3. 3. 1 兼容性

目前业务方都接入了Nacos- Client 访问 Nacos,替换成自研的 Sylas 后,为了让业务方无感知,Sylas 兼容了之前用到的 Nacos API。


目前兼容的主要API有: 实例注册、实例反注册、心跳、实例列表、服务列表等。


3. 3. 2 快照机制

原生的 Nacos 的临时实例都是没有持久化的,这意味着每次重启后会丢失所有的实例数据,需要等客户端自己心跳上报时发现实例不存在重新注册一遍,这样会导致重启后的一段时间内产生大量的写请求,并且会返回客户端不完整的实例列表,虽然得物的服务框架中都加了接收到空实例列表中不更新本地缓存的处理策略,但还是会导致一段时间内明显的流量倾斜问题。


从Nacos到完全自研|得物的注册中心演进之路


Raft 是基于日志复制的,在一个实际的 Raft 集群中,不可能让节点中的日志无限增加。冗长的日志导致系统重启时需要花费很长的时间进行回放,影响系统整体可用性。Raft与Chubby、Zookeeper 等类似,都采用了 Snapshot 技术进行日志压缩,丢弃 Snapshot 之前的日志。


Sylas 会根据写入请求次数阈值将内存中的状态机数据保存到本地磁盘上,现在配置的策略是每发生 50w 次写请求后保存一次快照。同时保存快照时 Raft 会记录当前 Raft 状态机的 Committed Index,恢复好数据的时候首先读取快照,然后 Raft 会回放快照保存的 Committed Index 之后的 Raft log 应用到状态机,这样就保证了数据和重启之前的完全一致。


3. 3. 3  一致性读

虽然 Raft 是强一致性协议,但是我们可以根据不同的读取策略来调整一致性强度从而提升性能,读操作可以分四种策略,做成配置化:


ReadIndex

当 leader 接收到一个读取请求时:

    • 将当前日志的 Commit Index 记录到一个本地变量 ReadIndex 中,封装到消息体中;

    • 首先需要确认自己是否仍然是 leader,因此需要向其他节点都发起一次心跳;

    • 如果收到了大多数节点的心跳响应,那么说明该 Server 仍然是 leader 身份;

    • 在状态机执行的地方,判断下 Apply Index 是否超过了 ReadIndex,如果超过了,那么就表明发起该读取请求时,所有之前的日志都已经处理完成,也就是说能够满足线性一致性读的要求;

    • 从状态机去读取结果,返回给客户端 我们可以看到,和刚刚的方法相比,Read Index 采用心跳的方式首先确认自己仍然是 leader,然后等待状态机执行到了发起读取时所有日志,就可以安全的处理客户端请求了,这里虽然还有一次心跳的网络开销,但一方面心跳包本身非常小,另外处理心跳的逻辑非常简单,比如不需要日志落盘等,因此性能相对之前的方法会高非常多。

        使用了Read Index的方法,我们还可以提供 Follower 节点读取的功能,并可以在 Follower 上实现线性一致性读,逻辑和 leader有些差异:


  • Follower 向 leader 查询最新的 Read Index;

  • leader 会按照上面说的,走一遍流程,但会在确认了自己 leader 的身份之后,直接将 Read Index 返回给 Follower;

  • Follower 等待自己的状态机执行到了 ReadIndex 的位置之后,就可以安全的处理客户端的读请求。

        该方式保证每次读取的数据都是最新的(这里最新是相对的,满足线性一致性的最新), 但是由于每次读取都要向所有节点发送心跳,性能开销比较大,所以默认不走这种方式。


LeaseRead

每次读取都直接转发 Leader 节点,Leader 节点直接返回。只有在出现网络分区和时钟漂移的情况下有可能返回旧数据。


在 Raft 论文里面,提到了一种通过 Clock +  Heartbeat 的 Lease  Read 优化方法。Leader 在发送 Heartbeat 的时候,会首先记录一个时间点 Start,当系统大部分节点都回复了 Heartbeat Response,那么我们就可以认为 Leader 的 Lease 有效期可以到  Start + Election  Timeout / Clock Drift Bound 这个时间点。


        为什么能够这么认为呢?主要是在于 Raft 的选举机制,因为 Follower 会在至少 Election Timeout 的时间之后,才会重新发生选举,所以下一个 Leader 选出来的时间一定可以保证大于 Start + Election  Timeout / Clock Drift Bound。


虽然采用 Lease 的做法很高效,但仍然会面临风险问题,也就是我们有了一个预设的前提,各个服务器的 CPU Clock 的时间是准的,即使有误差,也会在一个非常小的 Bound 范围里面,如果各个服务器之间 Clock 走的频率不一样,有些太快,有些太慢,这套 lease 机制就可能出问题。


        同时出现网络分区和时钟漂移概率较低,并且该方式不需要每次都向其他节点发送HeartBeat,所以一般需要实现线性一致性采用这种策略。


SequentialRead

顺序一致性读,即保证每个 Client 读取到的结果的顺序都是相同的。比如一个 Client 发送了 2 个写请求 a=2、a=3,虽然不能写入请求完成后其他 Client 能马上就能读取到结果,但能保证其他的 Client 一定是先读取到 a=2 再读取到 a=3,或者直接读取到 a=3。

        目前实现这种方案的做法是根据 Client 的 IP 做分片,每个 Client 只往固定的节点上读取数据,这样 Follower 节点也能分担读压力,默认采用这种方式读取。


        另外一种方法是 Client 端维护一个全局 AppyIndex,Proxy Server 收到这个找到 ApplyIndex 大于 Txid 的节点读取,没找到直接读取 Leader,这种需要新的 SDK 支持,所以暂时使用不了,但是是后期考虑的一种优化手段。


UnsafeRead

        每个节点都能读取数据,不保证任何一致性。


3. 3. 4 IP、应用维度查询

得物的主要 RPC 框架为 Dubbo2.x,主要是用的是接口级的注册发现,这其实不便于对应用实例数据进行维护,而很多场景是需要对应用维度的实例进行管理。Sylas 提供了本身的服务维度的查询之外,又构建了 IP 维度和应用维度的数据查询,其中应用维度的数据是根据 Dubbo 注册的接口中元信息里面携带的 Application 字段进行聚合而得出的数据。


IP维度查询

从Nacos到完全自研|得物的注册中心演进之路


应用维度查询

从Nacos到完全自研|得物的注册中心演进之路


3. 3. 5 性能提升

Nacos 使用的是 SpringMVC + Tomcat 构建的 HTTP 服务器,该技术选型其实在高并发、低延迟的场景下性能较差,而注册中心恰好主要是这种场景。


Sylas 使用 Golang 开发,在技术选型上使用轻量级的 Gin 框架,性能损耗较小。而且注册中心的场景 99% 以上的压力来自于心跳上报接口和实例列表接口,优化这 2 个核心接口是性能提升的关键因素。


Nacos 对于心跳上报的处理,会把心跳时间的更新交给线程池处理,线程池默认使用 BlockingQueue 会产生锁开销,Sylas 则是直接在当前协程中更新心跳时间,性能开销更小。


以下是心跳接口的压测结果对比:

从Nacos到完全自研|得物的注册中心演进之路


对于实例列表查询接口,返回数据量较大,性能开销主要集中在相应数据的反序列化上,Nacos 中则依靠的是 SpringMVC 默认的 Jackson 框架来实现反序列化,Jackson 使用的是反射的方式,会产生较大的性能开销。Sylas 使用 Easyjson 预先生成编译好的代码进行反序列化,避免反射的开销,实际测试过程中,预编译的反序列化开销相比反射性能提升有2.5倍左右。


以下是实例列表的压测结果对比:

从Nacos到完全自研|得物的注册中心演进之路


实例心跳接口 Sylas 比 Nacos 性能提升 4~6 倍,效果显著,实例列表接口性能提升大约 2.5 倍。

3. 3. 6 批量心跳

由于 Dubbo 的接口级注册发现会导致每个应用要同时上报多个服务的心跳,会产生大量网络消耗,Sylas 在新的 SDK 中支持了批量心跳 API,能够一次性发送多个服务的心跳,大大提升性能开销。

3. 3. 7 增量订阅

原生 Nacos 不支持真正的增量订阅,虽然有 udp 的服务推送,但是推送的事件只包含发生变更的服务名,不包含变更的具体数据,客户端会根据事件中的服务名去全量拉取实例信息,在大规模的扩缩容场景下会大量重复全量拉取实例列表,性能开销极高。Sylas 通过 grpc 长连接的方式,实时推送发生变更的服务及其具体变更内容,不需要客户端实时的去全量拉取,实现了真正的增量订阅,推送失败会以指数退避的方式重试一定次数,同时客户端仍保持定时拉取的功能,防止在极端的网络环境下推送一直失败导致的数据不一致。


有了增量的订阅,也为异地双活场景下得物的多机房注册中心同步打下了良好基础。

3. 3. 8 SQL 查询

由于注册中心的数据结构一般相对固定,并且都是存在内存中,一般不具备灵活的查询能力。Sylas 内嵌了轻量级的 SQL 引擎,能够用 SQL 查询实例数据。


未来演进

4. 1  数据治理

当前注册中心支持服务、IP、应用等维度的简单查询,但是服务的注册信息目前利用的并不是很完善,例如服务的上下游关系关系、订阅关系、机房维度的查询等等,构建完整的数据治理能力闭环有利于公司对服务治理方向更多的探索。


4. 2 Dubbo元数据与实例元数据分离

目前 Dubbo 注册的实例中的 Metadata 信息都比较多,比如 Category、Side、Dubbo、Interface 等等,因为同一个服务下的实例大部分的 Dubbo 元数据都是完全一致的,所以产生了大量的元数据冗余,再加上 Dubbo 本身的接口级注册发现,数据量很容易膨胀。未来将把这些冗余的冗余的 Dubbo 元数据放到服务元数据中,这样每次查询实例列表就不用返回大量的重复元数据,将会大大减少注册中心的带宽占用,提升性能。如果某些实例 Dubbo 元数据需要指定,直接在自身实例元数据覆盖即可。



总结

本文介绍了得物近几年服务注册与发现架构演进过程,主要包括三个阶段:Nacos单集群架构、Nacos Proxy + 多集群Nacos架构、Proxy + 多集群Sylas架构。虽然这期间整体架构变化比较大,但是我们做到了平稳、平滑地演进,期间上层业务应用无感知,为公司的服务治理演进打下了良好基础。虽然,得物的注册中心演进之路不一定适合其他公司,但也希望能为大家提供一种思路。



注释:

【1】「小得物环境」是得物内部的一套[全新搭建]独立[物理隔离]的[单地域]的[小流量][生产环境],覆盖了从网络、接入层、中间件、核心应用的系统和服务,为各类产品研发和业务发展的稳定性提供了丰富工具和应用场景



*文/熊仁杰

 关注得物技术,每周一三五晚18:30更新技术干货