TIKV PD Server架构解析
1. pd-server架构图
TIKV集群主要由两个核心组件组成,分别是pd-server和tikv-server。
pd-server可以看做是整个集群的大脑,里面保存着集群的关键元信息,比如集群配置、全局的region信息和store信息等。类似于Ceph的mon组件。
pd节点之间会内部维护一个etcd集群,pd的leader选举也是依赖内部etcd集群作为仲裁者选出来的,只有成为leader的pd节点才能提供服务,follower角色的pd节点更多是作为高可用节点。
pd-server主要提供的功能有:
(1)TSO的分配:client端在进行事务读写时都需要先获取全局唯一的TSO时间戳,然后再进行请求,如果是写入请求,后续还需要再获取一次TSO作为commitTSO进行提交;
(2)ID的分配:uint64自增的分配器,对于Region、Store等这都会需要有个id的概念,即regionId和storeId,由pd来进行分配维护;
(3)调度器:有region容量均衡调度器、region leader数量调度器等,主要是确保集群处于均衡的状态,发挥集群最大能力;
(4)检查器:主要是检查region的健康状态等,比如缺少副本了进行副本补充等;
(5)任务下发控制器:调度器和检查器会生成operator对象,都会先加入到任务下发控制器的等待队列里,由它进行调度下发给对应的tikv-server;比如在心跳回复时顺带下发和定期取出进行下发;
(6)pd存储引擎:主要是leveldb和etcd kv。
leveldb主要是保存region信息相关的,这部分量比较大,而且变动也比较频繁,同时不需要保证强同步,因为其它follower节点即使成为leader后缺少某部分region信息,它也可以通过region的心跳上报获取得到,所以leveldb是本地的存储,region信息的同步则是由leader同步给follower;
etcd kv主要是保存核心的信息,比如store节点元信息、集群配置信息和调度器信息等,这些是pd节点间内置etcd提供的kv存储,所以本身就是强同步的,不需要再额外的同步。
2. pd-server核心数据结构
pdServer:代表pd节点服务,保存pd节点基本信息,管理协调各个组件
member.Member:主要用于pd leader竞选和leader keeplive
idAllocator:用于分配全局唯一的ID,比如分配RegionId、分配PeerId等,全局自增
core.Storage:管理kv存储引擎,包括leveldb-kv和etcd-kv对信息的保存
core.BasicCluster:管理内存形式上的store和region信息,方便进行快速查找,用于计算热点,统计速率,状态保存等
tso.AllocatorManager:用于分配tso
cluster.RaftCluster:协调管理集群的,比如region的同步复制、协调启动时的原信息加载,调度器和checker的加载等
coordinator:协调管理调度器、checker和operator的下发控制等
3. pd leader
3.1. pd leader选举
pd项目代码里其实引用了etcd库,服务在启动时几个pd节点会组建一个内置的etcd集群,作为仲裁集群。
pd leader的选举优先将etcd leader所在的pd节点选为leader。
我猜想这么做的原因是pd leader在读写etcd里的键值时可以更快速。
选举方式依赖etcd client写入一个leader key,看谁先写成功,谁就是leader,写成功后会通过lease机制进行续期。
选举key是:/pd/{clusterId}/leader
被选举为leader的pd节点会负责执行特定的任务,比如tso的分配、region信息上报收集、调度器调用进行均衡等。其它非leader节点则会watch这个leader key,当它发生删除时会进行leader竞选。
3.2. pd leader职责
除了图上的,pd leader和其它pd节点还会进行region信息的同步,因为region信息本身是保存在本地leveldb的。
4. TSO
4.1. TSO分配模式
TSO可以理解为是一个时间戳,每个事务开始前都是需要先获取一个TSO的,commit时再获取一个TSO,方便用来检测是否有事务冲突。
TIKV的TSO值由物理时间和逻辑时间组成,是一个64位的值,前46位是物理时间戳,自1970年1月1日以来的UNIX时间戳,单位为毫秒,后18位是逻辑时间戳,是内存维护的自增的。pd里对tso的物理时间推进每次是3s(50ms会检测一次是否要推进)
在pd里TSO的分配模式有两种,分别是Global TSO和DC Location TSO。
引入DC Local TSO的目的是想假设有多个DC中心用了同一个PD服务,那么它们之间的TSO其实可以做逻辑隔离的,各自分配自己DC的TSO即可,同时也支持获取全局TSO,但这个TSO值需要比所有local TSO当前分配到的都大,所以在这种场景下如果需要分配全局TSO,就会逻辑处理变得复杂一点。
DC Local TSO的好处还有是可以利用多个节点分配TSO,比如pd节点1管理1区TSO,pd节点2管理2区TSO,这样不至于所有区都使用的一个节点的TSO,不利于资源利用。
这里只分析Global TSO的分配情况。
4.2. Global TSO工作机制
TSO时间的推进检查50ms会进行一次检查,看是否需要进行推进。
默认是50ms才定时触发一次这个函数,如果50毫秒内就将逻辑的用完了会不会有问题?
不会有问题,在分配逻辑里,假设出现这种情况,则会等待50毫秒进行重试,但很明显分配延时会飙升。
考虑极端情况,所有pd节点的物理时间突然间都往前调1个月,那么tso时间的推进就只能依靠逻辑数来进行推进了,此时如果请求数特别少的话,就会发现tso的时间也是远远落后真实时间的。
分配tso流程图:
5. Region的管理
5.1. region信息的管理
region是tikv里的最小管理存储单元,它表示一段范围,以字节序作为分割点,副本也是以region为单位的,每个region都会有它自己的raft group,使用raft协议进行数据强一致性保证。
有关概念的介绍可以参考这篇文章:TIKV读写流程浅析
所以对于region来说就会有peer的概念,比如3副本的region,那么它就会有3个peer,其中一个peer是leader,其它是follwer或者learner。
peer的状态又有downPeer、PendingPeer和正常Peer,以此来表明该region的各个副本是否是健康的。
region信息的收集是来自tikv-server的region心跳上报,心跳上报流程:
region信息持久化是保存在leveldb中的,但不是每次写入都直接进行写入,而是做了批量处理,进行定期刷,这也是因为region并不需要保证它需要实时保存它的信息,即使丢失,后续也能通过心跳上报逐步恢复。
存放region元信息的leveldb的目录路径:{部署路径}/region-meta/
举例:/home/lhx/tikv-data/pd-1379/region-meta/
存放region的leveldb的key格式:raft/r/{regionId}
region的内存状态数据的维护是采用btree进行管理的(内部自己实现的b+树数据结构,方便进行快速的增删改查),方便进行搜索。
5.2. region信息同步
上面说到当region心跳上报发现有信息更改时会将更新region塞入到一个channel中,会有个专门的goroutine接收来进行处理,并将对follower节点进行region信息的同步。
同步模型:
(1)follower节点作为client节点会发起跟leader节点的连接,leader充当server,client发送自己的index过去,接着client便是for循环里不断的进行recv;
(2)如果index能匹配上,则无须同步;如果index落后一部分,则从history里将这部分发送过去进行同步(一万以内的落后);如果落后太多则从leveldb中加载出来进行全同步;
(3)当leader节点通过心跳上报收集到region信息变更时则先保存到本地,然后leader会根据follower的index值找到还未同步的记录,然后将这些记录发送给follower进行同步。
对于index数值会持久化保存到leveldb的key中。
6. 调度器
目前主要有balance-hot-region-scheduler、balance-leader-scheduler和balance-region-scheduler。
balance-hot-region-scheduler:主要用于打散热点region,不让热点region都集中在某些store上,不利于host资源的充分利用;
balance-leader-scheduler:主要是均衡每个store的leader个数,因为读的情况下,都是从leader读的,所以均衡leader region也有助于防止读热点问题;
balance-region-scheduler:主要是均衡每个store的承载的容量,保证每个host的容量分布是均衡的,防止容量都堆积在某些host上;
除了这种调度器,还会有一些checker,用来检查region的,比如检查region是否需要进行分裂或合并,检查region是否缺失peer或者多余peer,对于这些情况都需要定期检查,然后生成operator进行调整。
常用的checker有:replica_checker、split_checker、merge_checker、learner_checker等等。
备注:store是存储服务对象,store上面会保存很多region,一个一个store会管理一个盘。
这些调度信息也是存放在leveldb中。跟region共享一个leveldb,不过它的保存key的前缀是scheduler_config/{schedulerName}
每个scheduler在服务init时就会将自己注册进去,然后cordinator在run的时候就根据配置去找对应的scheduler,然后运行它的Schedule函数。
这里以balance-region-scheduler来举例说明它的调度过程。
balance-region-scheduler的参考标准是容量,所以它会根据每个store上的容量,来生成region的迁移任务,从而将高容量的store迁移到低容量store上。
调度流程:
(1)统计每个store的得分,分数越高表示越优先进行迁移,所以按照store得分从高到低排序;
(2)遍历排好序的store列表,从该store中按照pending、follower、leader、learner的优先级顺序从region btree中找到一个region;
(3)筛选目标store,需要经过的过滤器:
<1>去掉目前这个region所在的store;
<2>不会违背region分布规则,比如region的两副本不能都在一个store上;
<3>目标的store的得分应该比当前region所在的store的得分低;
<4>对一些打了特殊标签的store进行过滤,比如默认的hotRegion所在的节点;
<5>筛选掉store状态异常的,比如down、offline的。
(4)经过步骤(3)得到一个store候选列表,然后遍历各个store,尝试假设让该store成为target store是否可以,比如迁移过去后,目标store的得分应该还是低于源store;
(5)筛选得到目标store后,创建一个迁移plan,即一个operator对象,然后加入到opController对象中,由它进行调度执行。
operator对象里会包括多个steps,每次下发时是取一个当前还没完成的step来下发到region,调用SendScheduleCommand进行下发,只有一个step完成才能执行下一个step(可能是下一次的心跳进行下发)。
step比如可以是TransferLeader、AddPeer、AddLearner、PromoteLearner、RemovePeer、MergeRegion、SplitRegion、ChangePeerV2Enter、ChangePeerV2Leave。
判断step完成的方式就是检查当前内存里收集上来的情况是不是已经满足该step想要的结果了(比如要添加peer,则判断下这个region是否已经有这个peer了),这样就可以将operator的currentStep向前挪动了。
7. operator调度机制
生成的operator会被opController进行管理,由它来进行调度下发。
大致的流程:
(1)生成的op都会先加入到opController对象的等待队列里,可以看做是个bucket槽,按照优先级分成了不同的bucket槽;
(2)每当放进去一个op时,同时就会触发从bucket槽中取出一个op;
(3)取出后都会先判断下op对应的region对应的store是否超限了,这个通过限速器进行判断,限速器依据是以op的step计算总分进行比较;
(4)如果限速通过,会执行以下几步:
<1>将op记录到一个map字典中,key是regionId,value是op,方便知道regionId就可以快速找到这个region要处理的op;
<2>根据op的step生成下发命令进行下发;
<3>将op加入到最小堆队列中,加入方式是计算该op当前step的得分,然后以分数为排序规则插入到最小堆;
(5)在Dispatch中会检查这个op是否已经结束了,这个结束包括顺利完成,超时等情况,如果完成则该op可以销毁了,然后重新从队列里拿出op来执行;
(6)触发从等待队列取出op的方式只有两种:
<1>加入了新的op;
<2>有op处理完了;
(7)触发下发op的step的方式有:
<1>region上报心跳后,给它顺带返回该region的op的当前step任务;
<2>定时任务定时从最小堆里取出op进行处理;
判断op的step是否完成了的方式:检查当前op的当前step的任务是否完成了,比如这个step是指需要为这个region新加一个peer,所以只需要检查这个peer是否已经加成功了,因为region会一直心跳上报,所以如果加成功了的情况下,心跳上报会包括这个peer的,所以只需要检查pd里维护的region btree就可以知道了,如果完成则将step进行+1开始执行下一个step。
8.region的分裂合并逻辑
分裂逻辑:
分裂的触发方式有两个地方:
(1)Tikv-Server里检查决定进行分裂,比如可以根据region的容量进行分裂,根据region是否是读写热点进行分裂;
(2)PD里通过pd-ctl或者tikv-ctl进行人工手动触发;
所以在日常live中分裂的主要逻辑还是在tikv-server中,可以在tikv-server架构解析中分析这个。
合并逻辑:
merge的触发完全是有PD来决定的。
巡检对象会迭代scan所有的region(注释里说明是巡检一遍1百万个region需要14分钟的时间),然后逐个region调用checker进行检查,其中就会调用到merge的checker,当发现这个region的size比较小时,则尝试看是否可以合并到左边或者右边的region上去,如果可以则会生成merge的operators,它会生成两个op,因为这个操作是关联到两个region的,每个op是对应一个region,将两个op都加入到opController的等待队列中。
在opController进行merge op的取出时也会一次性就取出连续的两个op,然后将它们都进行op的处理。
合并的条件:
以下条件只要满足一个就不进行合并:
(1)该region的size大于20MB;
(2)该region里包含的key个数大于20万个;
(3)该region是个热点region;
(4)该region是不健康的,比如缺peer或者有down/pending peer;
(5)如果这个region是刚分裂出来不超过1h的(split-merge-interval参数);
通过上面条件后,会获取该region的左右两个region,然后选择左右两个size较小的那个region,然后检查这个target region是否健康,同时size不应该大于500MB。
通过后则开始生成合并operators。
总的来说,在tikv-server里真正处理分裂和合并的逻辑时,因为分裂只涉及到一个raft gorup,逻辑会比较好处理,merge region因为涉及到两个raft group处理的会更复杂。