《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 百度Geek说 「百度小程序团队」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

导读: 服务治理目前越来越被企业建设所重视,特别现在云原生,微服务等各种技术被更多的企业所应用,本文内容是百度小程序团队基于大模型服务治理实战经验的一些总结,同时结合当前较火的分布式开源kv产品etcd,不仅会深入剖析ectd两大核心技术Raft与boltdb的实现原理,也会披露服务治理真实实践的经验,希望帮助大家在服务治理的道路上获得更多帮助。

一、服务治理概念介绍

服务治理是IT治理的一部分,它关注服务生命周期中的相关要素,其重点环节包括服务注册和发现、服务平滑升级、流量监控、流量管控、故障定位、安全性等。

服务是需要"治理"的,但是治理是需要成本的,如果一个服务的业务逻辑简单,运行流程清晰,出现问题也能及时定位和回滚,那么该服务治理的成本可能非常低,甚至只需要人工处理就行,但是在复杂业务中,服务的提供者和服务的使用者可能分别运行在不同的进程中(甚至在不同的物理节点上),并由不同的团队开发和维护。团队协作和服务协同,都需要进行大量的协调工作。协调工作越多,复杂度越高,这样就有了服务治理的需求,通过建设统一的服务治理平台,就可以有有效的提升业务的服务治理能力,包括协同的规范化、实时监控,不断优化调用链路的效率,以及辅助降低依赖复杂度,规避风险等。在大型业务系统中,服务治理已经是技术架构中必不可缺的一部分,也是整个业务系统最重要的基础设施之一。

上图是网上流传的netflix的服务拓扑图,图中密密麻麻的白色小点就是netflix的服务节点,节点之间的连线表明服务之间有调用,节点和连线构成了复杂的服务调用链,如此庞大的应用系统必须要通过一个强力的服务治理平台来进行管理。

服务治理本质上是对服务生命周期的管控,因而服务治理平台的核心需求就是如何解决服务生命周期中的痛点问题,其包括以下几个方面:

1、注册和发现

服务调用方在调用服务之前必须要得到服务提供方的地址,也就是调用方需要通过一种方式“发现“服务提供方,这就是服务发现。而要完成服务发现,就需要将服务提供方的信息存储到某个载体,这个存储的动作即是”服务注册“,而存储的载体则称为”服务注册中心“。在任何一个服务治理平台中,"注册中心"是必不可少的一个模块。服务注册和发现是服务治理中最基础的功能,在服务生命周期中,它负责服务的初始环节。

2、流量监控 在服务注册发现之后,就是服务的调用,大量的服务调用,形成了流量。流量监控就是对众多服务间的调用关系、状态的清晰掌控。其主要包括了调用拓扑关系、调用追踪、日志、监控告警等,服务治理通过调用拓扑来整体监控服务调用关系,通过建立监控体系来快速发现、定位问题,从而在整体上感知业务系统的运行状况。在服务生命周期中,流量监控负责服务的运行态感知。

3、流量调度 在业务系统运行过程中,经常会有比如促销、秒杀、明星绯闻等热点问题,或者机房断网、断电、系统大范围升级等突发事件,带来业务系统中局部服务的流量突增突降,这样就需要对服务的流量进行调度和管理。流量管理包括两个方面:从微观的单个服务来说,就是服务调用过程的管理,包括在何时采用何种均衡负载策略、路由策略以及熔断限流策略,这些策略统称为调用策略;从宏观上来说,就是流量分发的管理,可以根据某些流量特征和流量占比进行灰度发布、蓝绿发布等,这些称为流量分发策略。服务调用策略、流量分发策略,都需要通过流量监控收集的调用数据进行分析,从而制定出决策,然后在服务治理平台上落地。流量调度负责服务的运行态管理。

4、服务控制 流量调度的策略如何在服务的提供方和调用方生效,可以重启生效,也可以在运行态实时生效,这就是看服务治理平台对服务的控制力度,服务治理平台在充分建设服务治理能力后,能实时把服务治理的策略向服务进行分发并立即生效。

5、服务安全 每个服务都承载自身的业务职责,一些业务敏感的服务,需要对其他服务的访问进行认证与鉴权,也就是安全问题。

本文把拥有成千上万的服务称之为大型应用系统,该系统的特征是大量的服务、大量的服务实例、以及海量的服务调用,服务治理平台在管理这类业务系统的服务时,需要面对以下巨大的挑战:

1. 高可靠性 大型业务系统,海量的服务调用,错综复杂的调用关系,对服务的可靠性要求很高,很多基层的服务都要求99.99%的可靠性,因而维护这些服务的服务治理平台,其可靠性的要求也非常高,而要达到这么高的可靠性,服务治理平台本身也需要做到多级部署、多地热备、降级隔离、平滑上线等方案。

2. 高性能 在保证可靠性的前提下,服务治理还必须有很高的性能,比如在监控数据中,快速准确的感知到某个服务的出现了单点故障,从而能够将流量分发到该服务的其他进程上去。如果业务系统的服务数不多,调用量不高,那么监控数据量也不会很大,服务的单点故障很容易就能查到,但是在实时的海量调用数据中,一些常规的查询手段要花费大量的时间,等感知到单点故障时,可能已经造成了不可挽回的业务损失。所以性能是考量服务治理平台治理能力的一项重要指标,如何保证高性能,高速的存储、多级缓存、线性部署都是必不可少的。

3. 高扩展 高扩展包含两个方面:大型应用系统的服务,可能是由多个团队在开发运维,其水平和技术能力也是参差不齐的,因而服务治理平台需要提供兼容和扩展的能力,通过扩展性,尽可能的把不同的服务治理起来;同时,在业务系统服务量增长时,服务治理平台也应该具备同步扩展的能力,来保证其高可靠和高性能。

面对海量服务的治理挑战,服务治理平台也需要有一个强大好用的存储工具来应对,etcd就是一个不错的选择。

二、etcd介绍

2.1 etcd发展背景与相关竞品介绍

2013年CoreOS创业团队在构建一款开源,轻量级的操作系统ContainerLinux时,为了应对用户服务多副本之间协调的问题,自研开发的一款用于配置共享和服务发现的高可用KV分布式存储组件——ETCD。

下面我们也针对Zookeeper和Consul两个选型做了一下对比:

ZooKeeper

ZooKeeper从高可用性,数据一致性,功能这三个方面而言是完全符合需求的,但CoreOS还是坚持自研etcd的原因总结有以下两点:

  1. ZooKeeper不支持通过API安全地变更成员,需要人工修改节点配置并重启进程.如果操作有误,有可能导致脑裂等线上故障,同时CoreOS对适配云环境,集群规模的平滑调整,运行时配置的在线变更都是有期望目标的,这方面ZooKeeper的维护成本比较高。

  2. 高负载读写性能,ZooKeeper在大规模的实例连接情况下性能表现并不佳。

etcd名字是由“/etc”文件夹和”d”分布式系统组成。“/etc”文件夹是用来存储单系统配置数据的,而“etcd”用于存储大规模分布式系统的配置数据,etcd集群可提供高稳定性,高可靠性,高伸缩性和高性能的分布式KV存储服务。etcd是基于复制状态机实现的,由Raft一致性模块,日志模块,基于boltdb持久化存储的状态机组成,可应用于分布式系统的配置管理,服务发现,分布式一致性等等。

ZooKeeper与etcd一样,可解决分布式系统一致性和元数据存储等问题,但是etcd相较于ZooKeeper有以下几点优势:

  1. 动态集群成员关系重新配置

  2. 高负载下稳定的读写能力

  3. 多版本并发控制数据模型

  4. 可靠的键监控

  5. Lease(租约)原语将连接和会话分离

  6. 分布式锁保证API安全性

  7. ZooKeeper使用自己的RPC协议,使用受限;而etcd客户端协议是基于gRPC的,可支持多种语言。

Consul

Consul与etcd解决的是不同的问题,etcd用于分布式一致性KV存储,而Consul侧重于端到端的服务发现,它提供了内置的健康检查,失败检测和DNS服务等等,另外Consul通过RESTfulHTTPAPIs提供KV存储能力.但是当KV使用量达到百万级时,会出现高延迟和内存压力等问题。

一致性算法方面,etcd、Consul基于Raft算法实现数据复制,ZooKeeper则是基于Zab算法实现。Raft算法由Leader选举,日志同步,安全性组成,而Zab协议则由Leader选举、发现、同步、广播组成。

分布式CAP方面,etcd、Consul和ZooKeeper都是CP系统,发生网络分区时,无法写入新数据。

下表是针对三者的关键能力做了一下对比分析:

2.2 etcd核心技术介绍

基于Raft协议实现数据高可用和强一致性

早期数据存储服务引入多副本复制技术方案来解决单点问题,但是无论是主从复制还是去中性化复制,都存在一定的缺陷。主从复制运维困难,且一致性与可用性难以兼顾;去中心化复制,存在各种写入冲突问题需要业务处理。而分布式一致性算法,正是解决多副本复制存在问题的关键。分布式一致性算法,又称为共识算法,最早是基于复制状态机背景下提出来的。Paxos作为第一个共识算法,过于复杂,不容易理解,难以在工程上落地。斯坦福大学的Diego提出的Raft算法,通过将问题拆解为三个子问题,易于理解,降低了工程落地难度。这三个子问题是:Leader选举,日志复制,安全性。

Leader选举

etcd(版本3.4+)中Raft协议定义集群节点有4种状态:Leader、Follower、Candidate、PreCandidate。

正常情况下,Leader节点会按照心跳间隔时间,定时广播心跳消息给Follower节点,以维持Leader身份。Follower收到后回复心跳应答包消息给Leader。Leader都会带有一个任期号(term),任期表示从一次选举开始,赢得选举的节点在该任期内担当Leader。任期号单调递增,在Raft算法中充当逻辑时钟,用于比较各个节点数据新旧,识别过期Leader等等。

当Leader节点异常时,Follower节点会接收Leader的心跳消息超时,当超时时间大于竞选超时时间后,会进入PreCandidate状态,不自增任期号,仅发起预投票(民意调查,防止由于节点数据远远落后于其他节点而发起无效选举),获得大多数节点认可后,进入Candidate状态.进入Candidate状态的节点,会等待一个随机时间,然后发起选举流程,自增任期号,投票给自己,并向其他节点发送竞选投票信息。

当节点B收到节点A竞选消息后,有2种情况:

  1. 节点B判断节点A的数据至少和自己一样新,节点A任期号大于节点B任期号,并且节点B未投票给其他候选者,即可投票给节点A,节点A获得集群大多数节点支持,可成为新Leader。

  2. 如果节点B也发起了选举,并投票给自己,那么它将拒绝投票给节点A。此时若没有节点可以得到大多数投票支持,则只能等待竞选超时,开启新一轮选举。

日志复制

Raft日志结构如下图所示:

Raft日志由有序索引的一个个条目组成,每个日志条目包含了任期号和提案内容.Leader通过维护两个字段来追踪各个Follower的进度信息.一个是NextIndex,表示Leader发送给该Follower节点的下一个日志条目索引;另一个是MatchIndex,表示该Follower节点已复制的最大日志条目索引。

本文以Client提交“hello=world”提案,至接收到响应的整个流程为例,简单介绍etcd日志复制流程:

  1. 当Leader接收到Client提交的提案信息后,生成日志条目,同时遍历各个Follower的日志进度,生成对各个Follower追加日志的RPC消息;

  2. 通过网络模块将追加日志的RPC消息广播给各个Follower;

  3. Follower接收到追加日志消息并持久化之后,回复Leader已复制最大日志条目索引,即MatchIndex;

  4. Leader接收到Follower应答后,更新对应Follower的MatchIndex;

  5. Leader根据各个Follower提交的MatchIndex信息,计算出日志条目已提交索引位置,该位置代表日志条目被一半以上节点持久化;

  6. Leader通过心跳告知各个Follower已提交日志索引位置;

  7. 当Client的提案,被标识为已提交后,Leader回复Client该提案通过。

通过以上流程,Leader同步日志条目给各个Follower,保证etcd集群的数据一致性。

安全性

etcd通过给选举和日志复制增加了一系列规则,来保证Raft算法的安全性。

选举规则:

  1. 一个任期号,只能有一个Leader被选举,Leader选举需要集群一半以上节点支持;

  2. 节点收到选举投票时,如果候选者最新日志条目的任期号小于自己,拒绝投票,任期号相同但是日志比自己短,同样拒绝投票。

日志复制规则:

  1. Leader完全特性,如果某个日志条目在某个任期号中已被提交,则这个日志条目必然出现在更大任期号的所有Leader中;

  2. 只附加原则,Leader只能追加日志条目,不能删除已持久化的日志条目;

  3. 日志匹配特性,Leader发送日志追加信息时,会带上前一个日志条目的索引位置(用P表示)和任期号,Follower接收到Leader的日志追加信息后,会校验索引位置P的任期号与Leader是否一致,一致才能追加。

boltdb存储技术

ectd的另一个核心技术是boltdb存储,提供高效的b+树的检索能力,同时支持事务操作,他是支撑etcd高性能读写的关键能力之一。

boltdb的实现参见了LMDB(LightningMemory-MappedDatabase)设计思路,基于高效快速的内存映射数据库方案.基于B+树的结构设计。数据文件设计上bolt使用一个单独的内存映射的文件,实现一个写入时拷贝的B+树,这能让读取更快。而且,BoltDB的载入时间很快,特别是在从crash恢复的时候,因为它不需要去通过读log(其实它压根也没有)去找到上次成功的事务,它仅仅从两个B+树的根节点读取ID。

文件存储设计

由于采用了单文件映射存储,所以bolt对文件按指定长度进行分块处理,每块存储不同的内容类型。默认使用4096字节的长度进行分块。每一块的开头有单独的pageid(int64)标识。

文件块的类型有以下几种:

类型 Flag标识 个数 长度 说明
meta 0x04 2 80字节。其余为空 元数据信息。包含存储B+树root buck位置,可用块的数量freelist,当前最大块的offset等。分为metaA与metaB, 用来控制进行中的事务与已完成的事务(写事务只有一个进行中). 根据meta中的txid的值的大小来判断当前生效的是哪个meta。(txid会根据事务操作递增)
freelist 0x10 1或多个Overflow引用 变长 Freelist用于管理当前可用的pageid列表。由Meta元数据信息中的freelist字段来定位所在的pageid位置。
【data】bucket 0x01 1或多个Overflow引用 变长 存储bucket名称数据,bucket名称也是采用B+树结构存储。根root bucket的pageid由meat的root字段指定
【data】branch 0x01 1或多个Overflow引用 变长 存储分支数据内容。分支数据结构中只有key信息,没有value信息。指向的都是下一级节点的pageid信息
【data】leaf 0x02 1或多个Overflow引用 变长 存储叶子数据内容。

数据文件全景结构

说明:

  • metapage固定在page0与page1位置

  • Pagesize大小固定在4096字节

  • bolt的文件写入采用了本地序(小端序)的模式,比如16进制0x0827(2087)写入的内容为2708000000000000

单文件方案的优势就是不需要做文件的合并删除等操作,只需要在原文件上追加扩展长度就可以了。

查询设计

boltdb提供了非常高效的查询能力,可以先看一下它的对象设计:

从对象设计上,boltdb在加载时,会先loadmeta数据进内存,然后根据bucket,来定位数据块所在的位置,然后再根据key的值,来定位branchnode的位置,然后定位到叶子值节点。

我们以查询为例,来讲解一下,下面是一个基本的查询示例代码:

tx, err := db.Begin(true) // 开启事务

if err != nil {

return

}

b := tx.Bucket([]byte("MyBucket")) // 依据名称查问bucket

v := b.Get([]byte("answer20")) // 依据key进行查问

fmt.Println(string(v))

tx.Commit()

对应上面的代码,下面的序列图,可以更详细的了解一次查询的操作流程:

上面最关键的代码就是search方法,下面是主要的代码片断,已添加了注释说明方便阅读。

func (c *Cursor) search(key []byte, pgid pgid) {
p, n := c.bucket.pageNode(pgid)
if p != nil && (p.flags&(branchPageFlag|leafPageFlag)) == 0 {
panic(fmt.Sprintf("invalid page type: %d: %x", p.id, p.flags))
}
// 把以后查问节点(page,node)压入栈
e := elemRef{page: p, node: n}
c.stack = append(c.stack, e)

// If we're on a leaf page/node then find the specific node.
if e.isLeaf() {
c.nsearch(key)
return
}
// if node cached seach by node's inodes field
if n != nil {
c.searchNode(key, n)
return
}
// recursively to load branch page and call search child node again
c.searchPage(key, p)
}

三、百度基于etcd打造大规模服务治理建设思路

3.1 具体的挑战

天路是百度小程序团队开发打造的面向大型业务服务治理需求的一套解决方案,其目标之一就是打造成百度的服务治理规范样板。天路由注册中心、可视化管理平台、SDK框架、统一网关、tianlu-mesher五个部分组成,目前已经接入了150+产品线,实例数已达数十万级别。随着接入平台的团队数增多、以及服务实例的快速增长,大量团队间如何轻松的协作以及实现大规模服务治理平台的高可用、高性能一直是天路持续面临的挑战。

3.2 整体架构建设思路与方案

天路作为一个服务治理平台,核心理念是为所有的服务提供便捷的调用,统一的服务监控管理,简化服务的开发和维护成本。我们从以下不同的方面思考基于etcd打造大规模服务治理平台:高可用、高性能、高扩展、易用性。

  • 高可用

    • 考虑到etcd跨机房调用的高网络延时,我们采用单机房部署,同时我们也实现了主备集群切换的方案,解决在单机房实例全部宕机的情况下,etcd集群不可用的问题。

    • 为了降低平台对etcd的强依赖,我们做了etcd降级使用缓存的方案。在监控到etcd集群的性能无法支持当前平台的时候,使用缓存存储实例数据,能够让运维人员在恢复etcd之前,系统不受影响正常运行;etcd恢复之后,切换回etcd集群继续工作。

  • 高性能

    • etcd集群的kv查询性能很高,qps能达到10000以上。为了解决在极端并发下的性能问题,注册中心采用多级缓存提升查询效率,降低对etcd的访问压力。
    • 服务间调用使用直连的方式,请求不需要经过注册中心进行转发,调用基本没有时间损耗。
  • 高扩展

    • 考虑到将来服务实例数达到百万级别,我们需要考虑架构的高扩展性。
  • 易用性

    • 用户通过可视化的管理平台可以查看已注册的服务,也可通过管理平台实时更新服务治理策略的配置,实时调整服务治理策略。
    • 将调用日志接入trace平台,用户可通过traceId在trace平台查到整个调用链的记录,便于出错时进行快速的问题定位。
    • 多语言 SDK,支持多种rpc技术,包括百度自研的rpc技术brpc(https://github.com/baidu/Jprotobuf-rpc-socket【java/go sdk】)和http jsonrpc协议等

架构方案图

3.3 关键的指标与运维目标

此外针对更好的实施服务治理平台的运维,还需要以下的关键考核指标与运维要求。

关键指标:

  • 可用性达99.99以上;

  • 平响100ms以下。

运维目标:

  • 故障发现早

    • 配置监控告警,包括注册中心实例健康、etcd平响、内存和cpu监控。
  • 故障处理快

    • 自动处理:通过noah的回调机制,自动处理一些故障,提高处理速度。

    • 手动处理:值班机制。

四、总结

服务治理目前越来越被企业建设所重视,特别现在云原生,微服务等各种技术被更多的企业所应用,但是要真正在应用好,融合好,还是有非常多的挑战,除了一套成熟的服务治理产品外,包括团队整体对服务治理的认知,技术经验的深淀,遵循服务化的设计能力水平的能力等,都会影响到最终的实施效果。本文也仅在服务治理产品选型上给大家一些启发,希望在服务治理的道路上帮大家走得更好更稳。

文章目录
  1. 1. 一、服务治理概念介绍
  2. 2. 二、etcd介绍
    1. 2.1. 2.1 etcd发展背景与相关竞品介绍
      1. 2.1.1. ZooKeeper
      2. 2.1.2. Consul
    2. 2.2. 2.2 etcd核心技术介绍
      1. 2.2.1. 基于Raft协议实现数据高可用和强一致性
        1. 2.2.1.1. Leader选举
        2. 2.2.1.2. 日志复制
        3. 2.2.1.3. 安全性
      2. 2.2.2. boltdb存储技术
        1. 2.2.2.1. 文件存储设计
        2. 2.2.2.2. 查询设计
  3. 3. 三、百度基于etcd打造大规模服务治理建设思路
    1. 3.1. 3.1 具体的挑战
    2. 3.2. 3.2 整体架构建设思路与方案
    3. 3.3. 3.3 关键的指标与运维目标
      1. 3.3.1. 关键指标:
      2. 3.3.2. 运维目标:
  4. 4. 四、总结