目录

分布式K/V存储方案-Tair

Tair的功能

Tair是一个Key/Value结构数据的解决方案,它默认支持基于内存和文件的两种存储方式,分别和我们通常所说的缓存和持久化存储对应。非持久化的 tair 可以看成是一个分布式缓存. 持久化的 tair 将数据存放于磁盘中。为了解决磁盘损坏导致数据丢失, tair 可以配置数据的备份数目, tair 自动将一份数据的不同备份放到不同的主机上;当有主机发生异常, 无法正常提供服务的时候, 其于的备份会继续提供服务。

Tair除了普通Key/Value系统提供的功能,比如get、put、delete以及批量接口外,还有一些附加的实用功能,使得其有更广的适用场景,包括:

  • Version支持
  • 原子计数器
  • Item支持

Version支持

Tair中的每个数据都包含版本号,版本号在每次更新后都会递增。这个特性有助于防止由于数据的并发更新导致的问题

比如,系统有一个value为“a,b,c”,A和B同时get到这个value。A执行操作,在后面添加一个d,value为 “a,b,c,d”。B执行操作添加一个e,value为”a,b,c,e”。如果不加控制,无论A和B谁先更新成功,它的更新都会被后到的更新覆盖。

Tair无法解决这个问题,但是引入了version机制避免这样的问题。还是拿刚才的例子,A和B取到数据,假设版本号为10,A先更新,更新成功 后,value为”a,b,c,d”,与此同时,版本号会变为11。当B更新时,由于其基于的版本号是10,服务器会拒绝更新,从而避免A的更新被覆盖。 B可以选择get新版本的value,然后在其基础上修改,也可以选择强行更新。

原子计数器

Tair从服务器端支持原子的计数器操作,这使得Tair成为一个简单易用的分布式计数器。

Item支持

Tair还支持将value视为一个item数组,对value中的部分item进行操作。比如有个key的value为[1,2,3,4,5],我们可以只获取前两个item,返回[1,2],也可以删除第一个item,还支持将数据删除,并返回被删除的数据,通过这个接口可以实现一个原子的分布式 FIFO的队列

使用场景

非持久化

使用存储引擎:mdb,rdb

  • 数据可以以key/value的形式存储
  • 数据可以接受丢失
  • 访问速度要求很高
  • 单个数据大小不是很大,一般在KB级别
  • 数据量很大,并且有较大的增长可能性
  • 数据更新不频繁

持久化

使用存储引擎:kdb,ldb

  • 数据可以以key/value的形式存储
  • 数据需要持久化
  • 单个数据大小不是很大,一般在KB级别
  • 数据量很大,并且有较大的增长可能性
  • 数据的读写比例较高

Tair的内部结构

网络拓补

https://gitee.com/lienhui68/picStore/raw/master/null/20200917190405.png

架构

https://gitee.com/lienhui68/picStore/raw/master/null/20200917190418.png

一个Tair集群主要包括client、configserver和dataserver 3个模块。

  • Configserver通过和dataserver的心跳(HeartBeat)维护集群中可用的节点,并根据可用的节点,构建数据在集群中的分布信息(见下文的对照表)。
  • Client在初始化时,从configserver处获取数据的分布信息,根据分布信息和相应的dataserver 交互完成用户的请求。
  • Dataserver负责数据的存储,并按照configserver的指示完成数据的复制和迁移工作

tair 作为一个分布式系统, 是由一个中心控制节点和一系列的服务节点组成。我们称中心控制节点为config server。 服务节点是data server, config server 负责管理所有的data server, 维护data server的状态信息。data server 对外提供各种数据服务,并以心跳的形式将自身状况汇报给config server。config server是控制点,而且是单点,目前采用一主一备的形式来保证其可靠性,所有的 data server 地位都是等价的。

数据的分布

分布式系统需要解决的一个重要问题便是决定数据在集群中的分布策略,好的分布策略应该能将数据均衡地分布到所有节点上,并且还应该能适应集群节点的变化。Tair采用的对照表方式较好地满足了这两点。

对照表的行数是一个固定值,这个固定值应该远大于一个集群的物理机器数,由于对照表是需要和每个使用Tair的客户端同步的,所以不能太大,不然同步将带来较大的开销。我们在生产环境中的行数一般为1023 。

对照表简介

下面我们看对照表是怎么完成数据的分布功能的,为了方便,我们这里假设对照表的行数为6。最简单的对照表包含两列,第一列为hash桶(存放数据的单位,一个节点保存着多个桶的数据,比如192.168.10.1保存3个桶的数据)的索引,第二列为负责该 hash值对应数据的dataserver节点信息。比如我们有两个节点192.168.10.1和192.168.10.2,那么对照表类似:

0 192.168.10.1 1 192.168.10.2 2 192.168.10.1 3 192.168.10.2 4 192.168.10.1 5 192.168.10.2

当客户端接收到请求后,将key的hash值和6取模,然后根据取模后的结果查找对照表。比如取模后的值为3,客户端将和192.168.10.2通信。

对照表如何适应节点数量的变化

我们假设新增了一个节点——192.168.10.3,当configserver发现新增的节点后,会重新构建对照表。构建依据以下两个原则:

  1. 数据在新表中均衡地分布到所有节点上。
  2. 尽可能地保持现有的对照关系。

更新之后的对照表如下所示:

0 192.168.10.1 1 192.168.10.2 2 192.168.10.1 3 192.168.10.2 4 192.168.10.3 5 192.168.10.3

这里将原本由192.168.10.1负责的4和192.168.10.2负责的5交由新加入的节点192.168.10.3负责。如果是节点不可用,则相当于上述过程反过来,道理是一样的。

多备份的支持

Tair支持自定义的备份数,比如你可以设置数据备份为2,以提高数据的可靠性。对照表可以很方便地支持这个特性。我们以行数为6,两个节点为例,2个备份的对照表类似:

0 192.168.10.1 192.168.10.2 1 192.168.10.2 192.168.10.1 2 192.168.10.1 192.168.10.2 3 192.168.10.2 192.168.10.1 4 192.168.10.1 192.168.10.2 5 192.168.10.2 192.168.10.1

第二列为主节点的信息,第三列为辅节点信息。在Tair中,客户端的读写请求都是和主节点交互,所以如果一个节点不做主节点,那么它就退化成单纯的备份节点。因此,多备份的对照表在构建时需要尽可能保证各个节点作为主节点的个数相近

当有节点不可用时,

  • 如果是辅节点,那么configserver会重新为其指定一个辅节点,如果是持久化存储,还将复制数据到新的辅节点上。
  • 如果是主节点,那么configserver首先将辅节点提升为主节点,对外提供服务,并指定一个新的辅节点,确保数据的备份数。

多机架和多数据中心的支持

对照表在构建时,可以配置将数据的备份分散到不同机架或数据中心的节点上。Tair当前通过设置一个IP掩码来判断机器所属的机架和数据中心信息。

比如你配置备份数为3,集群的节点分布在两个不同的数据中心A和B,则Tair会确保每个机房至少有一份数据。假设A数据中心包含两份数据时,Tair会尽可能将这两份数据分布在不同机架的节点上。这可以减少整个数据中心或某个机架发生故障是数据丢失的风险。

轻量级的ConfigServer

从Tair的整体架构图上看,configserver很类似传统分布式集群中的中心节点。整个集群服务都依赖于configserver的正常工作。

但Tair的configserver却是一个轻量级的中心节点,在大部分时候,configserver不可用对集群的服务是不造成影响的。

Tair用户和configserver的交互主要是为了获取数据分布的对照表,当client获取到对照表后,会cache这张表,然后通过查这张表决定数据存储的节点,所以请求不需要和configserver交互,这使得Tair对外的服务不依赖configserver,所以它不是传统意义上的中心节点。

configserver维护的对照表有一个版本号,每次新生成表,该版本号都会增加。当有数据节点状态发生变化(比如新增节点或者有节点不可用了)时,configserver会根据当前可用的节点重新生成对照表,并通过数据节点的心跳,将新表同步给数据节点

当客户端请求数据节点时,数据节点每次都会将自己的对照表的版本号放入response中返回给客户端,客户端接收到response后,会将数据节点返回的版本号和自己的版本号比较,如果不相同,则主动和configserver通信,请求新的对照表。

通过数据节点和版本号完成客户端与configserver的解耦,这样只需要维护数据节点和configserver的连接即可。

所以客户端也不需要和configserver保持心跳,以便及时地更新对照表。这使得在正常的情况下,客户端不需要和configserver通信,即使configserver不可用了,也不会对整个集群的服务造成大的影响。

仅有当configserver不可用,此时有客户端需要初始化,那么客户端将取不到对照表信息,这将使得客户端无法正常工作。

DataServer内部结构

DataServer负责数据的物理存储,并根据configserver构建的对照表完成数据的复制和迁移工作DataServer具备抽象的存储引擎层,可以很方便地添加新存储引擎。DataServer还有一个插件容器,可以动态地加载/卸载插件。

https://gitee.com/lienhui68/picStore/raw/master/null/20200917193609.png

DataServer的内部结构示意图

抽象的存储引擎层

Tair的存储引擎有一个抽象层,只要满足存储引擎需要的接口,便可以很方便地替换Tair底层的存储引擎。比如你可以很方便地将bdb、tc甚至MySQL作为Tair的存储引擎,而同时使用Tair的分布方式、同步等特性。

Tair默认包含两个存储引擎:mdb和fdb。

mdb是一个高效的缓存存储引擎,它有着和memcached类似的内存管理方式。mdb支持使用share memory,这使得我们在重启Tair数据节点的进程时不会导致数据的丢失,从而使升级对应用来说更平滑,不会导致命中率的较大波动。

fdb是一个简单高效的持久化存储引擎,使用树的方式根据数据key的hash值索引数据,加快查找速度。索引文件和数据文件分离,尽量保持索引文件在内存中,以便减小IO开销。使用空闲空间池管理被删除的空间

自动的复制和迁移

为了增强数据的安全性,Tair支持配置数据的备份数。比如你可以配置备份数为3,则每个数据都会写在不同的3台机器上。得益于抽象的存储引擎层,无论是作为cache的mdb,还是持久化的fdb,都支持可配的备份数。

当数据写入一个节点(通常我们称其为主节点)后,主节点会根据对照表自动将数据写入到其他备份节点,整个过程对用户是透明的。

当有新节点加入或者有节点不可用时,configserver会根据当前可用的节点,重新build一张对照表。数据节点同步到新的对照表时,会自动将在新表中不由自己负责的数据迁移到新的目标节点。迁移完成后,客户端可以从configserver同步到新的对照表,完成扩容或者容灾过程。整个过程对用户是透明的,服务不中断。

插件容器

Tair还内置了一个插件容器,可以支持热插拔插件。

插件由configserver配置,configserver会将插件配置同步给各个数据节点,数据节点会负责加载/卸载相应的插件。

插件分为request和response两类,可以分别在request和response时执行相应的操作,比如在put前检查用户的quota信息等。

插件容器也让Tair在功能方便具有更好的灵活性。

更多细节

tair 的负载均衡算法是什么

tair 的分布采用的是一致性哈希算法, 对于所有的key,分到Q个桶中, 桶是负载均衡和数据迁移的基本单位。 config server 根据一定的策略把每个桶指派到不同的data server上。 因为数据按照key做hash算法, 所以可以认为每个桶中的数据基本是平衡的. 保证了桶分布的均衡性, 就保证了数据分布的均衡性。

https://gitee.com/lienhui68/picStore/raw/master/null/20200917195643.png

增加或者减少data server的时候会发生什么

当有某台data server故障不可用的时候, config server会发现这个情况, config server负责重新计算一张新的桶在data server上的分布表, 将原来由故障机器服务的桶的访问重新指派到其它的data server中。这个时候, 可能会发生数据的迁移。比如原来由data server A负责的桶,在新表中需要由 B负责。而B上并没有该桶的数据, 那么就将数据迁移到B上来。

B是A的备份节点

同时config server会发现哪些桶的备份数目减少了, 然后根据负载情况在负载较低的data server上增加这些桶的备份。当系统增加data server的时候, config server根据负载,协调data server将他们控制的部分桶迁移到新的data server上。迁移完成后调整路由。

当然系统中可能出现减少了某些data server 同时增加另外的一些data server。处理原理同上。 每次路由的变更,config server都会将新的配置信息推给data server。在客户端访问data server的时候, 会发送客户端缓存的路由表的版本号。如果data server发现客户端的版本号过旧,则会通知客户端去config server取一次新的路由表。如果客户端访问某台data server 发生了不可达的情况(该 data server可能宕机了),客户端会主动去config server取新的路由表。

发生迁移的时候data server如何对外提供服务

当迁移发生的时候, 我们举个例子, 假设data server A 要把桶 3,4,5 迁移给data server B。因为迁移完成前,客户端的路由表没有变化,客户端对 3, 4, 5 的访问请求都会路由到A。

现在假设 3还没迁移, 4 正在迁移中, 5已经迁移完成。那么如果是对3的访问, 则没什么特别, 跟以前一样。如果是对5的访问, 则A会把该请求转发给B,并且将B的返回结果返回给客户,如果是对4的访问,在A处理,同时如果是对4的修改操作会记录修改log。当桶4迁移完成的时候,还要把log发送到B,在B上应用这些log。最终A B上对于桶4来说, 数据完全一致才是真正的迁移完成。

当然如果是因为某data server宕机而引发的迁移, 客户端会收到一张中间临时状态的分配表。这张表中,把宕机的data server所负责的桶临时指派给有其备份data server来处理。 这个时候服务是可用的,但是负载可能不均衡。当迁移完成之后,才能重新达到一个新的负载均衡的状态。

桶在data server上分布时候的策略

程序提供了两种生成分配表的策略, 一种叫做负载均衡优先, 一种叫做位置安全优先。

我们先看负载优先策略。当采用负载优先策略的时候,config server会尽量的把桶均匀的分布到各个data server上。 所谓尽量是指在不违背下面的原则的条件下尽量负载均衡。

  1. 每个桶必须有COPY_COUNT份数据
  2. 一个桶的各份数据不能在同一台主机上

位置安全优先原则是说, 在不违背上面两个原则的条件下, 还要满足位置安全条件,然后再考虑负载均衡。

位置信息的获取是通过 _pos_mask(参见安装部署文档中关于配置项的解释) 计算得到。一般我们通过控制 _pos_mask 来使得不同的机房具有不同的位置信息。 那么在位置安全优先的时候,必须被满足的条件要增加一条, 一个桶的各份数据不能都位于相同的一个位置(不在同一个机房)。

这里有一个问题, 假如只有两个机房, 机房1中有100台data server, 机房2中只有1台data server。这个时候, 机房2中data server的压力必然会非常大。于是这里产生了一个控制参数 _build_diff_ratio(参见安装部署文档)。 当机房差异比率大于这个配置值时, config server也不再build新表。 机房差异比率是如何计出来的呢? 首先找到机器最多的机房,不妨设使RA, data server数量是SA。 那么其余的data server的数量记做SB。则机房差异比率=|SA – SB|/SA。 因为一般我们线上系统配置的COPY_COUNT是3。 在这个情况下, 不妨设只有两个机房RA和RB, 那么两个机房什么样的data server数量是均衡的范围呢? 当差异比率小于 0。5的时候是可以做到各台data server负载都完全均衡的。这里有一点要注意:假设RA机房有机器6台,RB有机器3台。那么差异比率 = 6 – 3 / 6 = 0.5。这个时候如果进行扩容,在机房A增加一台data server,扩容后的差异比率 = 7 – 3 / 7 = 0.57。也就是说,只在机器数多的机房增加data server会扩大差异比率。 如果我们的_build_diff_ratio配置值是0.5。那么进行这种扩容后, config server会拒绝再继续build新表。

tair 的一致性和可靠性问题

分布式系统中的可靠性和一致性是无法同时保证的,因为我们必须允许网络错误的发生。tair 采用复制技术来提高可靠性,并且为了提高效率做了一些优化,事实上在没有错误发生的时候,tair 提供的是一种强一致性。但是在有data server发生故障的时候,客户有可能在一定时间窗口内读不到最新的数据。 甚至发生最新数据丢失的情况。

tair提供的客户端

tair 的server端是C++写的, 因为server和客户端之间使用socket通信, 理论上只要可以实现socket操作的语言都可以直接实现成tair客户端。 目前实际提供的客户端有java 和 C++。 客户端只需要知道config server的位置信息就可以享受tair集群提供的服务了。

Tair的未来

我们将Tair开源,希望有更多的用户能从我们开发的产品中受益,更希望依托社区的力量,使Tair有更广阔的发展空间。

Tair开源后,有很多用户关心我们是否会持续维护这个项目。我们将Tair开源后,淘宝内部已经不再有私有的Tair分支,所有的开发和应用都基于开源 分支。Tair在淘宝有非常广的应用,我们内部有一个团队,专门负责Tair的开发和维护,相信我们会和社区一起,将Tair越做越好。

有很多用户在淘宝开源平台上申请加入Tair项目,加入项目在我们的开源平台上意味着成为项目的提交者,可以向代码库直接提交代码。所以我们暂时还没有批 准外部用户加入,我们将在大家对Tair有更深入的了解后和社区一起决定是否批准加入项目的申请,在此之前,如果你有对代码的改进,欢迎使用patch的 方式提交给我们,我们将在review后决定是否合并到代码库。