分布式Key/Value存储系统---Tair

一、简述

tair 是淘宝高性能、可靠、可扩展的开源的分布式 Key/Value 存储系统。分为持久化和非持久化两种使用方式:非持久化的 tair 可以看成是一个分布式缓存;持久化的 tair 将数据存放于磁盘中。作为一个分布式系统,Tair 由一个中心控制节点(config server)和一系列的服务节点(data server)组成:

  1. config server 负责管理所有的 data server,并维护 data server 的状态信息。为了保证高可用(High Available),config server 可通过 hearbeat 以一主一备形式提供服务。
  2. data server 对外提供各种数据服务,并以心跳的形式将自身状况汇报给 config server。所有的 data server 地位都是等价的。

架构

数据流向

1️⃣一个 tair 集群主要包括:
①三个必选模块:configserver、dataserver 和 client;
②一个可选模块:invalidserver。
通常情况下,一个集群中包含两台 configserver 及多台 dataServer。
2️⃣两台 configserver 互为主备并通过维护和 dataserver 之间的心跳获知集群中存活可用的 dataserver,构建数据在集群中的分布信息(对照表)。
3️⃣dataserver 负责数据的存储,并按照 configserver 的指示完成数据的复制和迁移工作。
4️⃣client 在启动的时候,从 configserver 获取数据分布信息,根据数据分布信息和相应的 dataserver 交互完成用户的请求。
5️⃣invalidserver 主要负责对等集群的删除和隐藏操作,保证对等集群的数据一致。
6️⃣从架构上看,configserver 的角色类似于传统应用系统的中心节点,整个集群服务依赖于 configserver 的正常工作。但实际上相对来说,tair 的 configserver 是非常轻量级的,当正在工作的服务器宕机的时候另外一台会在秒级别时间内自动接管。而且,如果出现两台服务器同时宕机的最恶劣情况,只要应用服务器没有新的变化,tair 依然服务正常。

ConfigServer(中心节点)

1️⃣通过维护和 dataserver 心跳来获知集群中存活节点的信息。
2️⃣根据存活节点的信息来构建数据在集群中的分布表。
3️⃣提供数据分布表的查询服务。
4️⃣调度 dataserver 之间的数据迁移、复制。
5️⃣主备来保证高可用,维护数据的路由信息,通过心跳来监控 dataserver 的存活
6️⃣CS 只有在 client 首次连接和客户端需要更新路由表时才会对 client 提供服务,整体请求量很小,不会成为瓶颈。

Client

1️⃣先请求一次 configServer,通过获取路由信息并缓存到本地,判断需要操作的 DataServer,并进行数据操作。
2️⃣如果没有路由信息更新的话,后续操作,直接查本地缓存路由信息即可。
3️⃣在应用端提供访问 Tair 集群的接口。
4️⃣更新并缓存数据分布表和 invalidserver 地址等。
5️⃣LocalCache,避免过热数据访问影响 tair 集群服务。
6️⃣流控

DataServer(存储数据)

①提供存储引擎,接受 client 的 put/get/remove 等操作,执行数据迁移、复制等。
②插件:在接受请求的时候处理一些自定义功能。
③访问统计。

1️⃣Duplicator
由 DataServer 的主节点调用,做数据备份用,可以配置同步与异步。

2️⃣Migrator
负责数据迁移,可能出现的两个问题:1、新增节点导致数据不平衡,需要迁移 bucket。2、某个节点宕机导致该节点上的数据备份数不足,需要新的副本备份。

###InvalidServer(删除和隐藏,可选)

1️⃣接收来自 client 的 invalid/hide 等请求后,对属于同一组的集群(双机房独立集群部署方式)做 delete/hide 操作,保证同一组集群的一致。
2️⃣集群断网之后的脏数据清理。
3️⃣访问统计。

二、存储引擎与应用场景

1️⃣存储引擎
现在主要有 MDB、RDB、LDB 和 GDB 四种存储引擎。MDB 类似于 Memcache,RDB 类似于 Redis,LDB 是封装的 levelDB,GDB 图数据库。RDB 支持 redis 的方法,支持 redis 里的数据结构,支持持久化。

2️⃣应用场景

三、tair 的基本概念

1️⃣ConfigID【集群层面】

唯一标识一个 tair 集群。每个集群都有一个对应的 configID,在当前的大部分应用情况下 configID 是存放在 diamond 中的,对应了该集群的 configserver 地址和 groupname。业务在初始化 tairclient 的时候需要配置此 configID。

2️⃣Namespace【应用层面】

①又称 area,是 tair 中分配给应用的一个内存或者持久化存储区域,可以认为应用的数据存在自己的 namespace 中,同一集群(同一个 configID)中 namespace 是唯一的。
②通过引入 namespace,可以支持不同的应用在同集群中使用相同的 key 来存放数据,也就是 key 相同,但内容不会冲突。
③同一个 namespace 存放相同的 key,内容会受到影响,在简单 K/V 形式下会被覆盖,rdb 等带有数据结构的存储引擎内容会根据不同的接口发生不同的变化。

3️⃣quota

①配额,对应了每个 namespace 储存区的大小限制,超过配额后数据将面临最近最少使用(LRU)的淘汰。
②持久化引擎(ldb)本身没有配额,ldb 由于自带了 mdb cache,所以也可以设置 cache 的配额。超过配额后,在内置的 mdb 内部进行淘汰。
配额的计算 :配额大小直接影响数据的命中率和资源利用效率,业务方需要给出一个合适的值,通常的计算方法是评估在保证一定命中率情况下所需要的记录条数,这样配额大小即为:记录条数 * 平均单条记录大小

4️⃣expireTime【数据的过期时间】

①当超过过期时间之后,数据将对应用不可见,这个设置同样影响到应用的命中率和资源利用率。
②不同的存储引擎有不同的策略清理掉过期的数据。调用接口时,expiredTime 单位是秒,可以是相对时间(比如:30s),也可以是绝对时间(比如:当天23时,转换成距1970-1-1 00:00:00的秒数)。
③小于0,表示不更改之前的过期时间。如果不传或者传入0,则表示数据永不过期。大于0小于当前时间戳是相对时间过期;大于当前时间戳是绝对时间过期。

5️⃣version

tair 中存储的每个数据都有版本号,版本号在每次更新后都会递增。相应的,在tair put 接口中也有此 version 参数,这个参数是为了解决并发更新同一个数据而设置的,类似于乐观锁

四、路由规则【一致性哈希算法】

1️⃣对照表

类似于一致性 hash 算法,通过 key 算出 bucket,找到 bucket 对应的 DS(DataServer) IP,进行操作,类似于如下表格。其中,每个 bucket 需要对应多个 ip,用户数据备份。如果一台机器挂了,不至于整个 bucket 不可用。

configServer 维护路由表有一些原则:
①数据在新表中均衡地分布到所有节点上。
②尽可能地保持现有的对照关系。 Tair 当前通过设置一个 IP 掩码来判断机器所属的机架和数据中心信息,以达到数据的备份分布在多个机架、机房的目的。

2️⃣路由表的同步更新

  1. 路由表的更新使用到一个version,在configServer和dataServer中每个路由表都会维护一个version
  2. 这个verison在每次configServer更新路由表的时候自增一次,同时会在configServer和dataServer的心跳连接中将这个路由表推出去给dataServer,
  3. client获取到configServer中的路由表之后会将路由表缓存在本地
  4. 当client请求dataServer的时候,dataServer返回的response中会带着当前dataServer中路由表的version
  5. 如果response中的verison与client中version不一致,client会重新获取configServer

3️⃣增加或减少data server

  1. 当有某台 data server 故障不可用的时候,config server 会发现这个情况,config server 负责重新计算一张新的桶在 data server 上的分布表,将原来由故障机器服务的桶的访问重新指派到其它的data server中。
  2. 这个时候,可能会发生数据的迁移。比如原来由 data server A 负责的桶,在新表中需要由 B 负责。而 B 上并没有该桶的数据,那么就将数据迁移到 B 上来。同时 config server 会发现哪些桶的备份数目减少了,然后根据负载情况在负载较低的 data server 上增加这些桶的备份。
  3. 当系统增加 data server 的时候,config server 根据负载,协调 data server 将他们控制的部分桶迁移到新的 data server 上。迁移完成后调整路由。

五、Tair同步与失效

tair同步分为两部分:
①跨zone同步。
②db与tair同步。

缓存与数据库一致性的问题:版本号(解决并发) + 事务操作(解决原子)
缓存集群内部一致性的问题:zab,raft,paxos 等一致性协议(都是基于二阶段提交)

###缓存与数据库一致性

1️⃣缓存一致性(同步)

  • 原子问题
  1. 使用 msgbroker 发送事务消息,将 db 操作与消息发送操作绑定在事务中,保证很大程度上的缓存与数据库数据一致性。

更新时策略:数据更新先操作 db,然后失效缓存;更新 db 和失效缓存在同一个事务中。

tair客户端超时,服务端成功。由于 db 已经回滚,下次查询会从 db 读老数据出来;
tair客户端超时,服务端不成功。由于 db 已经回滚,下次查询从 tair 读到的数据仍然是正确的。

  1. 如果出现缓存没有及时更新(消息订阅段短时间内未接收到消息),使用 drm 手动推送进行应急。
  • 并发问题:确定缓存对象的版本号来判断最新修改时间

2️⃣缓存一致性(异步)

  1. 精卫是一个采用 Single Leader 模型的异步数据复制系统。精卫来监听 db 的 binlog 的变更消息,由精卫异步地去更新或者失效 tair,可以对用户透明,异步流程里加入有效的重试,保证消息被成功消费,做到最终一致。
  2. 精卫使用的是 Row-based Binlog 格式,该格式按照时间顺序精确的记录了数据库中每张表结构每行数据的变更。表结构的变更称为 DDL,数据的变更称为 DML。若数据库从创建开始后的变更都记录 Binlog,那么重放这份 Binlog 可以精确还原该库所有表和数据。

缓存集群一致性

  1. 一般情况下,当应用没有命中缓存时,会从 db 或 hbase 中加载数据,并放入缓存,这种方式 zone 间是独立的,不用做同步。而当有修改时,db 或 hbase 中的数据发生变更,然后清理掉缓存 key,这种情况下如果其他 zone 不感知,则其他 zone 读的仍然是脏数据。
  2. 如果均采用 addp 方式同步,hbase 数据同步如果早于 tair addp 同步,一切正常,但如果 hbase 数据同步晚于 tair addp 同步,则刷新缓存可能存在问题。当 hbase 数据同步晚于 tair addp 同步时,tair 中的 key 先被 remove,当请求进入再次加载时,从 hbase 中加载数据可能为历史数据,然后更新入缓存,造成缓存数据有误。
  3. 时间戳:在 tair 存放的 key/value中,value 内部新增加过期时间和版本, 版本在放入的时候生成。需要做变更的时候, 先设置各 zone tair 的过期时间,然后本 zone 直接移除;当请求的时候,如果有过期时间,则丢入线程池异步刷新。缺点是 value 需要重新包装,增加了存储空间,存储有效比率会下降。

六、version 处理并发

1️⃣version 具体使用案例,如果应用有10个 client 会对 key 进行并发 put,那么操作过程如下:
【如果 get key 成功】在调用 put 的时候将 get key 返回的 verison 重新传入 put 接口,服务端根据 version 是否匹配来返回 client 是否 put 成功。
【如果数据不存在】get key 数据不存在,则新 put 数据。此时传入的 version 必须不是 0 和 1,其他的值都可以(例如1000,要保证所有 client 是一套逻辑)。因为传入0,tair 会认为强制覆盖;而传入 1,第一个 client 写入会成功,但是新写入时服务端的 version 以 0 开始计数,所以此时 version 也是 1,所以下一个到来的 client 写入也会成功,这样造成了冲突。

说明

  1. version 分布式锁,tair 中存在该 key,则认为该 key 所代表的锁已被 lock;不存在该 key,在未加锁时,操作过程和上面相似。业务方可以在 put 的时候增加 expire,以避免该锁被长期锁住。当然业务方在选择这种策略的情况下需要考虑并处理 Tair 宕机带来的锁丢失的情况。
  2. 什么情况下需要使用 version?业务对数据一致性有较高的要求,并且访问并发高,那么通过 version 可以避免数据的意外结果。如果不关心并发,那么建议不传入 version 或者直接传 0。

2️⃣version的改变逻辑

  1. 如果 put 是新增新数据且没有设置版本号,会自动将版本设置成 1。
  2. 如果 put 是更新老数据且没有设置版本号,或者 put 传来的参数版本与当前版本一致,版本号自增 1。
  3. 如果 put 是更新老数据且传来的参数版本与当前版本不一致,更新失败,返回 versionError。
  4. 如果 put 时传入的 version 参数为 0,则强制更新成功,版本号自增 1。version 返回不一致的时候,该如何处理?

七、注入一个TairManager进行缓存的操作

@Component
public class Cache {
    private static finai int NAME_SPACE = 1;
    private static String FLAG = "successs";

    @Autowired
    private TairManager tairManager;
    public ResultCode put(String key, String value) {
        ResultCode code  = tairManager.put(NAME_SPACE, key, value);
        return code;
    }
    public String get(String key) {
        Result<DataEntry> result = tairManager.get(NAME_SPACE, key);
        return (String) result.getValue().getValue();
    }
    public boolean delete(String key) {
        ResultCode code = tairManager.delete(NAME_SPACE, key);
        if (FLAG.equals(code.getMessage())) {
            return true;
        } else {
            return false;
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值