当前位置:首页 » 存储配置 » 分布式kv存储

分布式kv存储

发布时间: 2023-02-13 10:12:49

① 海量分布式存储系统Doris原理概述

Doris( https://github.com/itisaid/Doris )是一个海量分布式 KV 存储系统,其设计目 标是支持中等规模高可用可伸缩的 KV 存储集群。
Doris可以实现海量存储,线性伸缩、平滑扩容,自动容错、故障转移,高并发,且运维成本低。部署规模,建议部署4-100+台服务器。

Doris采用两层架构,Client 和 DataServer+Store。
有四个核心组件,Client、DataServer、Store、Administration。
应用程序通过Client SDK进行Doris的访问
每台服务器上部署一个Data Sever做服务器的管理,每台服务器上有自己的存储Store,整个集群的数据存储,每台机器独立部署。数据通过路由选择写入到不同的机器中。
Administration为管理中心,提供配置、管理和监控。
config指,应用程序启动一个Data Server,在启动时要配置管理中心的ip地址,通关管理中心。管理中心会修改配置项感知到集群中加了新机器,对新机器管理,扩容等。待机器处于可用状态,将该机器的配置项通知给KV Client。从而KV Client进行新的路由选择。
扩容、下线机器等的控制台界面通过Management管理。
Monitor监控机器是否正常。

client写数据,绑定产品的namespace(逻辑隔离),构成新key,路由到具体机器上读写。

路由解析算法是设计的一个关键点,决定集群的管理方式,也决定了集群扩容的复杂性和难度。
Doris的算法类似redis,有桶的概念,key映射到1w个虚拟节点,虚拟节点在映射到物理节点。
由于Doris设计时,用于4-100+规模的集群。因此,Doris分了1w个虚拟节点,当服务器超过100会导致负载不均衡,1000会更差,相当于每一个集群上有10个虚拟节点,虚拟节点会有10%的影响。
扩容时,需要调节虚拟节点指向新的位置。具体过程为,暴利轮询新节点添加后,一个服务器上应该承载的虚拟节点个数,将超出的虚拟节点迁移到新机器即可。如上图左图有2个物理节点,扩容后,有3个物理节点,变为右图。

为了保证高可用。doris所有服务分成2个组,两组服务器对等。两个group是可以有不同数量的服务器。
写操作时,client的路由算法在两个group分别选2个服务器,分别(同时)写入,两个服务器全部返回后,再继续向下进行。读操作时,从两个服务器随机选一个读。这样,提高可用性,数据持久性,不会丢失。

集群管理的重要角色Config Server,有一个功能是负责发现故障服务器。
发现故障的方式有2种:

节点失效分为:瞬间失效、临时失效、永久失效
应用服务器向服务器写,如果写失败,为 瞬间失效 。接着应用服务器进行3次重试。3次都失败,通知管理服务器,进行服务的失效判断。
管理服务器再写一次,如果写成功,认为是客户端自己通信通信问题。如果写入失败,判断为 临时失效 ,通知所有client,服务器失效,不要写,也不读。
如果2小时恢复,则节点为临时失效。如果2小时没有恢复,认为是 永久失效

如图,如果节点2失效,进入临时失效阶段。

如图,节点2临时失效2个小时还未恢复,判定为永久失效。进入永久失效的恢复。

设计中,有临时日志节点(备份节点),有空白节点。实际使用中没有节点3空白节点。原因:1 自动迁移有风险,还是需要手动迁移。2 几年宕机1台,一直有一个空白节点standby浪费。一般晚上报警失效也没有事情,第二天,找机器扩容即可。认为24小时之内,同样编号的2台机器连续down掉,概率很低。

物理节点分成2个group,写的时候,向2个group同时写。当其中一个group扩容机器时,该group上的所有节点进入临时失效状态。停止读写,将数据迁移到新的服务器上。
由于是虚拟节点的映射在调整,所以迁移是按照虚拟节点调整。为了迁移方便,虚拟节点物理化,一个虚拟节点对应一个文件。迁移时其实就是拷贝文件。这时,如果group1有节点失效也会出现不一致,但是,通常扩容的过程很快,因为,是scp拷贝文件,瓶颈为网络带宽,通常几十T数据,几分钟迁移完成,十来分钟进行数据恢复。

② 如何实现 Docker 与分布式数据库结合

那么Docker是什么呢?
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。几乎没有性能开销,可以很容易地在机器和数据中心中运行。最重要的是,他们不依赖于任何语言、框架包括系统。
这是对Docker的一个官方解释,简单说,有两个部分:
1) 对于应用程序,曾经我们需要为了不同的系统专门的调整应用程序的代码或者是构造相应的依赖包驱动等等,大大增加了开发量以及开发的难度。现在,Docker向不同的应用程序,提供了一个统一的环境。
2) 对于服务器,为了支持不同版本的应用,曾经可能需要在物理机上安装多个版本或者不同的GuestOS或者说虚拟机。这就大大占用了物理机的性能,影响了最终程序的表现,提高了资源的成本。

使用Docker容器的方式,对于应用程序,不需要开发多种多样的版本或者是针对OS每个版本的升级再进行代码方面的调整,实现了广泛的兼容性和开发的最简性。同时对于物理机,部署的环境“瘦身”也节约了更多的资源,将更多的资源用于提高应用程序本身的性能。
CoreOS是Docker的不二之选?
之前大概介绍了Docker,那么服务器上面还是需要最基本的应操作系统才能支撑Docker容器,那么这么多中的Linux内核OS究竟哪一个好呢?笔者和很多Docker技术专家的的观点就是Core OS。
CoreOS是一个基于Linux 内核的轻量级操作系统,为了计算机集群的基础设施建设而生,专注于自动化,轻松部署,安全,可靠,规模化。作为一个操作系统,CoreOS 提供了在应用容器内部署应用所需要的基础功能环境以及一系列用于服务发现和配置共享的内建工具。
简单说,CoreOS去掉了大量的非必要的功能,只保留了Server端需要的最基本功能,真正意义做到了“轻量化”。
此外,CoreOS还做到了:整体系统升级/回滚方案;容器化所有非系统应用、无包管理器;集群化调度器Fleet;分布式高可靠的KV存储系统ETCD
这些特性都让它成为Docker生态的首选操作系统。不过最新的消息是,CoreOS不满足于做Docker生态下的一环,它正在推出自己的容器AppC计划,想对Docker来一招“釜底抽薪”。当然,现阶段并没有出现完全的两者 “分手”,所以对于普通使用者,并没有太大影响。

Docker+分布式数据库
数据库是每一个软件项目必须的一个部分,作为这样的一类底层基础软件,兼容性、通用性、易用度都是需要考虑的重点。非常遗憾的是,现在的操作系统以及数据库都没有完全的实现完全的通用。特别对于NoSQL数据库这样的分布式系统,需要部署在多台物理机时,对于通用性要求就更高了。
目前,像SequoiaDB已经实现了自动化的安装,大大提升了部署的效率,但是考虑到部署之后的配置以及不同环境下的调试问题,仍然可能会耗费不小的人力物力。所以基于刚刚提到的Docker的优点,作为一个通用的基础软件,NoSQL数据库的Docker化就成了必须。

一个简单的例子,你可以用docker把数据库的数据与数据库程序本身分离开:用一个container A作为数据存储,然后另一个container B运行数据库。当你想升级数据库时,用新的container C替换掉container B即可。
Docker+分布式数据库的结合,带来诸多的好处:
1) 部署简单,使用镜像部署非常简单,特别是对集群环境,使用Docker镜像的部署还可以再数据库上提前集成Hadoop、Spark等架构,真正实现“一步到位”。
2) 方便应用的更新,应用的更新只需要考虑制作一个新的镜像就可以与容器适配,无需重新再调整与底层的配置。数据和程序的分离,这样升级替换等等都不会影响到数据。
3) 操作简单方便,除了底层免除了复杂的与环境进行配置的工作,操作也更加方便,配置好的Docker镜像在部署时候只需要一条指令就可以了。
4) 开发、应用环境一致,Docker让数据库能做到 开发---测试---实施应用 三个阶段的环境是完全一致的。降低开发到应用过程中的工作量,开发出来就能保证实际应用环境上能同样的运行。
5) 系统稳定,因为Docker的隔离作用,将应用与OS独立开,这样能更好保证整个系统的稳定性。
6) 节省系统资源,系统只需要运行一个统一的环境就可以,不需要占用太多性能去支持运行环境本身,能将更多的系统资源投入到应用当中。
有了这些特性, Docker+数据库,将成为一个数据库发展的新方向,Docker这样的通用性和简单操作解决方案,大大提高了数据库使用的效率,帮助使用者节约了大量成本。
Docker是如今技术圈的新潮流,开发人员是最乐见于Docker的这种应用部署模式,因为应用的生命周期起始于开发人员的开发系统,经过开发,测试,压力测试,等过程,最终应用发布到生产系统,并可能在不同的生产系统中迁移。应用开发人员对此都会有切身的体会,任何微小的运行环境的错误都会导致应用出现问题,尤其在讲究快速敏捷的今天,应用模块,新的代码,新的配置,被快速的加入应用的环境中,可能还没等写入到文档,新特性就已经被推送到生产上了。作为一个新的技术,笔者也希望更多的产品能加强与Docker的结合,帮助产品更好的使用。
博文出处:http://segmentfault.com/a/1190000002930030

③ oss中kvengine模块的作用

存储数据的分布式管理。oss中kvengine模块的作用是存储数据的分布式管理。OSS也叫对象存储服务(Object Storage Service),是阿里云提供的一种存储服务,随着智能设备越来越普及,越来越多终端的内容需要存储在云端

④ Redis百亿级Key存储设计方案

该应用场景为DMP缓存存储需求,DMP需要管理非常多的第三方id数据,其中包括各媒体cookie与自身cookie(以下统称supperid)的mapping关系,还包括了supperid的人口标签、移动端id(主要是idfa和imei)的人口标签,以及一些黑名单id、ip等数据。

在hdfs的帮助下离线存储千亿记录并不困难,然而DMP还需要提供毫秒级的实时查询。由于cookie这种id本身具有不稳定性,所以很多的真实用户的浏览行为会导致大量的新cookie生成,只有及时同步mapping的数据才能命中DMP的人口标签,无法通过预热来获取较高的命中,这就跟缓存存储带来了极大的挑战。

经过实际测试,对于上述数据,常规存储超过五十亿的kv记录就需要1T多的内存,如果需要做高可用多副本那带来的消耗是巨大的,另外kv的长短不齐也会带来很多内存碎片,这就需要超大规模的存储方案来解决上述问题。

人⼝标签主要是cookie、imei、idfa以及其对应的gender(性别)、age(年龄段)、geo(地域)等;mapping关系主要是媒体cookie对supperid的映射。以下是数据存储⽰示例:

媒体编号-媒体cookie=>supperid

supperid => { age=>年龄段编码,gender=>性别编码,geo=>地理位置编码 }

imei or idfa => { age=>年龄段编码,gender=>性别编码,geo=>地理位置编码 }

显然PC数据需要存储两种key=>value还有key=>hashmap,⽽而Device数据需要存储⼀一种

key=>hashmap即可。

存储吃紧的一个重要原因在于每天会有很多新数据入库,所以及时清理数据尤为重要。主要方法就是发现和保留热数据淘汰冷数据。

网民的量级远远达不到几十亿的规模,id有一定的生命周期,会不断的变化。所以很大程度上我们存储的id实际上是无效的。而查询其实前端的逻辑就是广告曝光,跟人的行为有关,所以一个id在某个时间窗口的(可能是一个campaign,半个月、几个月)访问行为上会有一定的重复性。

数据初始化之前,我们先利用hbase将日志的id聚合去重,划定TTL的范围,一般是35天,这样可以砍掉近35天未出现的id。另外在Redis中设置过期时间是35天,当有访问并命中时,对key进行续命,延长过期时间,未在35天出现的自然淘汰。这样可以针对稳定cookie或id有效,实际证明,续命的方法对idfa和imei比较实用,长期积累可达到非常理想的命中。

Hash表空间大小和Key的个数决定了冲突率(或者用负载因子衡量),再合理的范围内,key越多自然hash表空间越大,消耗的内存自然也会很大。再加上大量指针本身是长整型,所以内存存储的膨胀十分可观。先来谈谈如何把key的个数减少。

大家先来了解一种存储结构。我们期望将key1=>value1存储在redis中,那么可以按照如下过程去存储。先用固定长度的随机散列md5(key)值作为redis的key,我们称之为BucketId,而将key1=>value1存储在hashmap结构中,这样在查询的时候就可以让client按照上面的过程计算出散列,从而查询到value1。

过程变化简单描述为:get(key1) -> hget(md5(key1), key1) 从而得到value1。

如果我们通过预先计算,让很多key可以在BucketId空间里碰撞,那么可以认为一个BucketId下面挂了多个key。比如平均每个BucketId下面挂10个key,那么理论上我们将会减少超过90%的redis key的个数。

具体实现起来有一些麻烦,而且用这个方法之前你要想好容量规模。我们通常使用的md5是32位的hexString(16进制字符),它的空间是128bit,这个量级太大了,我们需要存储的是百亿级,大约是33bit,所以我们需要有一种机制计算出合适位数的散列,而且为了节约内存,我们需要利用全部字符类型(ASCII码在0~127之间)来填充,而不用HexString,这样Key的长度可以缩短到一半。

下面是具体的实现方式

参数bit决定了最终BucketId空间的大小,空间大小集合是2的整数幂次的离散值。这里解释一下为何一个字节中只有7位可用,是因为redis存储key时需要是ASCII(0~127),而不是byte array。如果规划百亿级存储,计划每个桶分担10个kv,那么我们只需2^30=1073741824的桶个数即可,也就是最终key的个数。

碎片主要原因在于内存无法对齐、过期删除后,内存无法重新分配。通过上文描述的方式,我们可以将人口标签和mapping数据按照上面的方式去存储,这样的好处就是redis key是等长的。另外对于hashmap中的key我们也做了相关优化,截取cookie或者deviceid的后六位作为key,这样也可以保证内存对齐,理论上会有冲突的可能性,但在同一个桶内后缀相同的概率极低(试想id几乎是随机的字符串,随意10个由较长字符组成的id后缀相同的概率*桶样本数=发生冲突的期望值<<0.05,也就是说出现一个冲突样本则是极小概率事件,而且这个概率可以通过调整后缀保留长度控制期望值)。而value只存储age、gender、geo的编码,用三个字节去存储。

另外提一下,减少碎片还有个很low但是有效的方法,将slave重启,然后强制的failover切换主从,这样相当于给master整理的内存的碎片。

推荐Google-tcmalloc, facebook-jemalloc内存分配,可以在value不大时减少内存碎片和内存消耗。有人测过大value情况下反而libc更节约。

1)kv存储的量级必须事先规划好,浮动的范围大概在桶个数的十到十五倍,比如我就想存储百亿左右的kv,那么最好选择30bit 31bit作为桶的个数。也就是说业务增长在一个合理的范围(10 15倍的增长)是没问题的,如果业务太多倍数的增长,会导致hashset增长过快导致查询时间增加,甚至触发zip-list阈值,导致内存急剧上升。

2)适合短小value,如果value太大或字段太多并不适合,因为这种方式必须要求把value一次性取出,比如人口标签是非常小的编码,甚至只需要3、4个bit(位)就能装下。

3)典型的时间换空间的做法,由于我们的业务场景并不是要求在极高的qps之下,一般每天亿到十亿级别的量,所以合理利用CPU租值,也是十分经济的。

4)由于使用了信息摘要降低了key的大小以及约定长度,所以无法从redis里面random出key。如果需要导出,必须在冷数据中导出。

5)expire需要自己实现,目前的算法很简单,由于只有在写操作时才会增加消耗,所以在写操作时按照一定的比例抽样,用HLEN命中判断是否超过15个entry,超过才将过期的key删除,TTL的时间戳存储在value的前32bit中。

6)桶的消耗统计是需要做的。需要定期清理过期的key,保证redis的查询不会变慢。

人口标签和mapping的数据100亿条记录。

优化前用2.3T,碎片率在2左右;优化后500g,而单个桶的平均消耗在4左右。碎片率在1.02左右。查询时这对于cpu的耗损微乎其微。

另外需要提一下的是,每个桶的消耗实际上并不是均匀的,而是符合多项式分布的。

上面的公式可以计算桶消耗的概率分布。公式是唬人用的,只是为了提醒大家不要想当然的认为桶消耗是完全均匀的,有可能有的桶会有上百个key。但事实并不没有那么夸张。试想一下投硬币,结果只有两种正反面。相当于只有两个桶,如果你投上无限多次,每一次相当于一次伯努利实验,那么两个桶必然会十分的均匀。概率分布就像上帝施的魔咒一样,当你面对大量的桶进行很多的广义的伯努利实验。桶的消耗分布就会趋于一种稳定的值。接下来我们就了解一下桶消耗分布具体什么情况:

通过采样统计

31bit(20多亿)的桶,平均4.18消耗

100亿节约了1.8T内存。相当于节约了原先的78%内存,而且桶消耗指标远没有达到预计的底线值15。

对于未出现的桶也是存在一定量的,如果过多会导致规划不准确,其实数量是符合二项分布的,对于2 30桶存储2 32kv,不存在的桶大概有(百万级别,影响不大):

Math.pow((1 - 1.0 / Math.pow(2, 30)), Math.pow(2, 32)) * Math.pow(2, 30);

对于桶消耗不均衡的问题不必太担心,随着时间的推移,写入时会对HLEN超过15的桶进行削减,根据多项式分布的原理,当实验次数多到一定程度时,桶的分布就会趋于均匀(硬币投掷无数次,那么正反面出现次数应该是一致的),只不过我们通过expire策略削减了桶消耗,实际上对于每个桶已经经历了很多的实验发生。

总结:信息摘要在这种场景下不仅能节约key存储,对齐了内存,还能让Key按照多项式分布均匀的散列在更少量的key下面从而减少膨胀,另外无需在给key设置expire,也很大程度上节约了空间。

这也印证了时间换空间的基本理论,合理利用CPU租值也是需要考虑的。

关注分布式存储技术以及分布式计算方法

⑤ 什么是kv数据库

kv数据库是指Key-value数据库,是一种以键值对存储数据的一种数据库,类似java中的map。可以将整个数据库理解为一个大的map,每个键都会对应一个唯一的值。

key-value分布式存储系统查询速度快、存放数据量大、支持高并发,非常适合通过主键进行查询,但不能进行复杂的条件查询。

如果辅以实时搜索引擎进行复杂条件检索、全文检索,就可以替代并发性能较低的MySQL等关系型数据库,达到高并发、高性能,节省几十倍服务器数 量的目的。以MemcacheDB、Tokyo Tyrant为代表的key-value分布式存储,在上万并发连接下,轻松地完成高速查询。

(5)分布式kv存储扩展阅读:

数据库的安全直接关系到整个数据库系统的安全,其防护手段主要有以下八点:

1、使用正版数据库管理系统并及时安装相关补丁。

2、做好用户账户管理,禁用默认超级管理员账户或者为超级管理员账户设置复杂密码;为应用程序分别分配专用账户进行访问;设置用户登录时间及登录失败次数限制, 防止暴力破解用户密码。

3、分配用户访问权限时,坚持最小权限分配原则,并限制用户只能访问特定数据库,不能同时访问其他数据库。

4、修改数据库默认访问端口,使用防火墙屏蔽掉对 外开放的其他端口,禁止一切外部的端口探测行为。

5、对数据库内存储的重要数据、敏感数据进行加密存储,防止数据库备份或数据文件被盗而造成数据泄露。

6、设置好数据库的备份策略,保证数据库被破坏后能迅速恢复。

7、对数据库内的系统存储过程进行合理管理,禁用掉不必要的存储过程,防止利用存储过程进行数据库探测与攻击。

8、启用数据库审核功能,对数据库进行全面的事件跟踪和日志记录。

参考资料来源:

网络-Key-Value

网络-数据库

⑥ 分布式容易发论文吗

没有那么容易,比较难
分布式工程学是一门实践性很强的工科学。所以会出现与其他工科一样的现象就是实践会先于理论。在1960年末被公认为是第一个分布式系统的ARPANET就诞生于美国[1]。在美国50年代到60年受曼哈顿计划的影响,计算机理论迎来了大爆炸的时代。在那个年代发明了我们今天所用到的大部分计算机理论。作为一个新兴学科,当年的科学家大都是刚刚毕业正是壮年。而今他们大多已经是高龄老人,有些科学家则已经离世。在这里向哪些为计算机理论作出贡献的科学家们表示敬意。

⑦ B站分布式KV存储实践

在B站的业务场景中,存在很多种不同模型的数据,有些数据关系比较复杂像:账号、稿件信息。有些数据关系比较简单,只需要简单的kv模型即可满足。此外,又存在某些读写吞吐比较高的业务场景,该场景早期的解决方案是通过MySQL来进行数据的持久化存储,同时通过redis来提升访问的速度与吞吐。但是这种模式带来了两个问题,其一是存储与缓存一致性的问题,该问题在B站通过canal异步更新缓存的方式得以解决,其二则是开发的复杂度,对于这样一套存储系统,每个业务都需要额外维护一个任务脚本来消费canal数据进行缓存数据的更新。基于这种场景,业务需要的其实是一个介于Redis与MySQL之间的提供持久化高性能的kv存储。此外对象存储的元数据,对数据的一致性、可靠性与扩展性有着很高的要求。

基于此背景,我们对自研KV的定位从一开始就是构建一个高可靠、高可用、高性能、高拓展的系统。对于存储系统,核心是保证数据的可靠性,当数据不可靠时提供再高的可用性也是没用的。可靠性的一个核心因素就是数据的多副本容灾,通过raft一致性协议保证多副本数据的一致性。

分布式系统,如何对数据进行分片放置,业界通常有两种做法,一是基于hash进行分区,二是基于range进行分区,两种方式各有优缺点。hash分区,可以有效防止热点问题,但是由于key是hash以后放置的,无法保证key的全局有序。range分区,由于相邻的数据都放在一起,因此可以保证数据的有序,但是同时也可能带来写入热点的问题。基于B站的业务场景,我们同时支持了range分区和hash分区,业务接入的时候可以根据业务特性进行选择。大部分场景,并不需要全局有序,所以默认推荐hash分区的接入方式,比如观看记录、用户动态这些场景,只需要保证同一个用户维度的数据有序即可,同一个用户维度的数据可以通过hashtag的方式保证局部有序。

整个系统核心分为三个组件:
Metaserver用户集群元信息的管理,包括对kv节点的健康监测、故障转移以及负载均衡。

Node为kv数据存储节点,用于实际存储kv数据,每个Node上保存数据的一个副本,不同Node之间的分片副本通过raft保证数据的一致性,并选出主节点对外提供读写,业务也可以根据对数据一致性的需求指定是否允许读从节点,在对数据一致性要求不高的场景时,通过设置允许读从节点可以提高可用性以及降低长尾。

Client模块为用户访问入口,对外提供了两种接入方式,一种是通过proxy模式的方式进行接入,另一种是通过原生的SDK直接访问,proxy本身也是封装自c++的原生SDK。SDK从Metaserver获取表的元数据分布信息,根据元数据信息决定将用户请求具体发送到哪个对应的Node节点。同时为了保证高可用,SDK还实现了重试机制以及backoff请求。

集群的拓扑结构包含了几个概念,分别是Pool、Zone、Node、Table、Shard 与Replica。

基于不同的业务场景,我们同时支持了range分区和hash分区。对于range场景,随着用户数据的增长,需要对分区数据进行分裂迁移。对于hash分区的场景,使用上通常会根据业务的数据量做几倍的冗余预估,然后创建合适的分片数。但是即便是几倍的冗余预估,由于业务发展速度的不可预测,也很容易出现实际使用远超预估的场景,从而导致单个数据分片过大。

之所以不在一开始就创建足够的分片数有两个原因:其一,由于每一个replica都包含一个独立的engine,过多的分片会导致数据文件过多,同时对于批量写入场景存在一定的写扇出放大。其二,每一个shard都是一组raftgroup,过多的raft心跳会对服务造成额外的开销,这一点后续我们会考虑基于节点做心跳合并优化减少集群心跳数。

为了满足业务的需求场景,我们同时支持了range和hash两种模式下的分裂。两种模式分裂流程类似,下面以hash为例进行说明。

hash模式下的分裂为直接根据当前分片数进行倍增。分裂的流程主要涉及三个模块的交互。

metaserver
分裂时,metaserver会根据当前分片数计算出目标分片数,并且下发创建replica指令到对应的Node节点,同时更新shard分布信息,唯一不同的是,处于分裂中的shard状态为splitting。该状态用于client流量请求路由识别。当Node完成数据分裂以后上报metaserver,metaserver更新shard状态为normal从而完成分裂。

Node
node收到分裂请求以后,会根据需要分裂的分片id在原地拉起创建一个新的分片。然后对旧分片的数据进行checkpoint,同时记录旧分片checkpoint对应的logid。新分片创建完成后,会直接从旧分片的checkpoint进行open,然后在异步复制logid之后的数据保证数据的一致性。新分片加载完checkpoint后,原来的旧分片会向raftgroup提交一条分裂完成日志,该日志处理流程与普通raft日志一致。分裂完成后上报分裂状态到metaserver,同时旧分片开始拒绝不再属于自己分片的数据写入,client收到分片错误以后会请求metaserver更新shard分布。
完成分裂以后的两个分片拥有的两倍冗余数据,这些数据会在engine compaction的时候根据compaction_filter过滤进行删除。

Client
用户请求时,根据hash(key) % shard_cnt 获取目标分片。表分裂期间,该shard_cnt表示分裂完成后的最终分片数。以上图3分片的分裂为例:
hash(key) = 4, 分裂前shard_cnt为3,因此该请求会被发送到shard1. 分裂期间,由于shard_cnt变为6,因此目标分片应该是shard4, 但是由于shard4为splitting,因此client会重新计算分片从而将请求继续发送给shard1. 等到最终分裂完成后,shard4状态变更为Normal,请求才会被发送到shard4.
分裂期间,如果Node返回分片信息错误,那么client会请求metaserver更新分片分布信息。

类似于MySQL的binlog,我们基于raftlog日志实现了kv的binlog. 业务可以根据binlog进行实时的事件流订阅,同时为了满足事件流回溯的需求,我们还对binlog数据进行冷备。通过将binlog冷备到对象存储,满足了部分场景需要回溯较长事件记录的需求。

直接复用raftlog作为用户行为的binlog,可以减少binlog产生的额外写放大,唯一需要处理的是过滤raft本身的配置变更信息。learner通过实时监听不断拉取分片产生的binlog到本地并解析。根据learner配置信息决定将数据同步到对应的下游。同时binlog数据还会被异步备份到对象存储,当业务需要回溯较长时间的事件流的时候,可以直接指定位置从S3拉取历史binlog进行解析。

基于上述提到的binlog能力,我们还基于此实现了kv的多活。learner模块会实时将用户写入的数据同步到跨数据中心的其他kv集群。对于跨数据中心部署的业务,业务可以选择就近的kv集群进行读取访问,降低访问延时。

kv的多活分为读多活和写多活。对于读多活,机房A的写入会被异步复制到机房B,机房B的服务可以直接读取本机房的数据,该模式下只有机房A的kv可以写入。对于写多活,kv在机房A B 都能同时提供写入并且进行双向同步,但是为了保证数据的一致性,需要业务上做数据的单元化写入,保证两个机房不会同时修改同一条记录。通过将用户划分单元,提供了写多活的能力。通过对binlog数据打标,解决了双向同步时候的数据回环问题。

对于用户画像和特征引擎等场景,需要将离线生成的大量数据快速导入KV存储系统提供用户读取访问。传统的写入方式是根据生成的数据记录一条条写入kv存储,这样带来两个问题。其一,大批量写入会对kv造成额外的负载与写入带宽放大造成浪费。其次,由于写入量巨大,每次导入需要花费较长的时间。为了减少写入放大以及导入提速,我们支持了bulk load的能力。离线平台只需要根据kv的存储格式离线生成对应的SST文件,然后上传到对象存储服务。kv直接从对象存储拉取SST文件到本地,然后直接加载SST文件即可对外提供读服务。bulk load的另外一个好处是可以直接在生成SST后离线进行compaction,将compaction的负载offload到离线的同时也降低了空间的放大。

由于LSM tree的写入特性,数据需要被不断的compaction到更底层的level。在compaction时,如果该key还有效,那么会被写入到更底层的level里,如果该key已经被删除,那么会判断当前level是否是最底层的,一条被删除的key,会被标记为删除,直到被compaction到最底层level的时候才会被真正删除。compaction的时候会带来额外的写放大,尤其当value比较大的时候,会造成巨大的带宽浪费。为了降低写放大,我们参考了Bitcask实现了kv分离的存储引擎sparrowdb.

sparrowdb 介绍

用户写入的时候,value通过append only的方式写入data文件,然后更新索引信息,索引的value包含实际数据所在的data文件id,value大小以及position信息,同时data文件也会包含索引信息。与原始的bitcask实现不一样的是,我们将索引信息保存在 rocksdb。

更新写入的时候,只需要更新对应的索引即可。compaction的时候,只需将索引写入底层的level,而无需进行data的拷贝写入。对于已经失效的data,通过后台线程进行检查,当发现data文件里的索引与rocksdb保存的索引不一致的时候,说明该data已经被删除或更新,数据可以被回收淘汰。

使用kv存储分离降低了写放大的问题,但是由于kv分离存储,会导致读的时候多了一次io,读请求需要先根据key读到索引信息,再根据索引信息去对应的文件读取data数据。为了降低读访问的开销,我们针对value比较小的数据进行了inline,只有当value超过一定阈值的时候才会被分离存储到data文件。通过inline以及kv分离获取读性能与写放大之间的平衡。

在分布式系统中,负载均衡是绕不过去的问题。一个好的负载均衡策略可以防止机器资源的空闲浪费。同时通过负载均衡,可以防止流量倾斜导致部分节点负载过高从而影响请求质量。对于存储系统,负载均衡不仅涉及到磁盘的空间,也涉及到机器的内存、cpu、磁盘io等。同时由于使用raft进行主从选主,保证主节点尽可能的打散也是均衡需要考虑的问题。

副本均衡
由于设计上我们会尽量保证每个副本的大小尽量相等,因此对于空间的负载其实可以等价为每块磁盘的副本数。创建副本时,会从可用的zone中寻找包含副本数最少的节点进行创建。同时考虑到不同业务类型的副本读写吞吐可能不一样导致CPU负载不一致,在挑选副本的时候会进一步检查当前节点的负载情况,如果当前节点负载超过阈值,则跳过该节点继续选择其他合适的节点。目前基于最少副本数以及负载校验基本可以做到集群内部的节点负载均衡。
当出现负载倾斜时,则从负载较高的节点选择副本进行迁出,从集群中寻找负载最低的节点作为待迁入节点。当出现节点故障下线以及新机器资源加入的时候,也是基于均值计算待迁出以及迁入节点进行均衡。

主从均衡
虽然通过最少副本数策略保证了节点副本数的均衡,但是由于raft选主的性质,可能出现主节点都集中在部分少数节点的情况。由于只有主节点对外提供写入,主节点的倾斜也会导致负载的不均衡。为了保证主节点的均衡,Node节点会定期向metaserver上报当前节点上副本的主从信息。
主从均衡基于表维度进行操作。metaserver会根据表在Node的分布信息进行副本数的计算。主副本的数量基于最朴素简单的数学期望进行计算: 主副本期望值 = 节点副本数 / 分片副本数。下面为一个简单的例子:
假设表a包含10个shard,每个shard 3个replica。在节点A、B、C、D的分布为 10、5、6、9. 那么A、B、C、D的主副本数期望值应该为 3、1、2、3. 如果节点数实际的主副本数少于期望值,那么被放入待迁入区,如果大于期望值,那么被放入待迁出区。同时通过添加误差值来避免频繁的迁入迁出。只要节点的实际主副本数处于 [x-δx,x+δx] 则表示主副本数处于稳定期间,x、δx 分别表示期望值和误差值。
需要注意的是,当对raft进行主从切换的时候,从节点需要追上所有已提交的日志以后才能成功选为主,如果有节点落后的时候进行主从切换,那么可能导致由于追数据产生的一段时间无主的情况。因此在做主从切换的时候必须要检查主从的日志复制状态,当存在慢节点的时候禁止进行切换。

3.7 故障检测&修复
一个小概率的事件,随着规模的变大,也会变成大概率的事件。分布式系统下,随着集群规模的变大,机器的故障将变得愈发频繁。因此如何对故障进行自动检测容灾修复也是分布式系统的核心问题。故障的容灾主要通过多副本raft来保证,那么如何进行故障的自动发现与修复呢。

健康监测
metaserver会定期向node节点发送心跳检查node的健康状态,如果node出现故障不可达,那么metaserver会将node标记为故障状态并剔除,同时将node上原来的replica迁移到其他健康的节点。
为了防止部分node和metaserver之间部分网络隔离的情况下node节点被误剔除,我们添加了心跳转发的功能。上图中三个node节点对于客户端都是正常的,但是node3由于网络隔离与metaserver不可达了,如果metaserver此时直接剔除node3会造成节点无必要的剔除操作。通过node2转发心跳探测node3的状态避免了误剔除操作。
除了对节点的状态进行检测外,node节点本身还会检查磁盘信息并进行上报,当出现磁盘异常时上报异常磁盘信息并进行踢盘。磁盘的异常主要通过dmesg日志进行采集分析。

故障修复
当出现磁盘节点故障时,需要将原有故障设备的replica迁移到其他健康节点,metaserver根据负载均衡策略选择合适的node并创建新replica, 新创建的replica会被加入原有shard的raft group并从leader复制快照数据,复制完快照以后成功加入raft group完成故障replica的修复。
故障的修复主要涉及快照的复制。每一个replica会定期创建快照删除旧的raftlog,快照信息为完整的rocksdb checkpoint。通过快照进行修复时,只需要拷贝checkpoint下的所有文件即可。通过直接拷贝文件可以大幅减少快照修复的时间。需要注意的是快照拷贝也需要进行io限速,防止文件拷贝影响在线io.

过期数据淘汰
在很多业务场景中,业务的数据只需要存储一段时间,过期后数据即可以自动删除清理,为了支持这个功能,我们通过在value上添加额外的ttl信息,并在compaction的时候通过compaction_filter进行过期数据的淘汰。level之间的容量呈指数增长,因此rocksdb越底层能容纳越多的数据,随着时间的推移,很多数据都会被移动到底层,但是由于底层的容量比较大,很难触发compaction,这就导致很多已经过期的数据没法被及时淘汰从而导致了空间放大。与此同时,大量的过期数据也会对scan的性能造成影响。这个问题可以通过设置periodic_compaction_seconds 来解决,通过设置周期性的compaction来触发过期数据的回收。

scan慢查询
除了上面提到的存在大批过期数据的时候可能导致的scan慢查询,如果业务存在大批量的删除,也可能导致scan的时候出现慢查询。因为delete对于rocksdb本质也是一条append操作,delete写入会被添加删除标记,只有等到该记录被compaction移动到最底层后该标记才会被真正删除。带来的一个问题是如果用户scan的数据区间刚好存在大量的delete标记,那么iterator需要迭代过滤这些标记直到找到有效数据从而导致慢查询。该问题可以通过添加 CompactOnDeletionCollector 来解决。当memtable flush或者sst compaction的时候,collector会统计当前key被删除的比例,通过设置合理的 deletion_trigger ,当发现被delete的key数量超过阈值的时候主动触发compaction。

delay compaction
通过设置 CompactOnDeletionCollector 解决了delete导致的慢查询问题。但是对于某些业务场景,却会到来严重的写放大。当L0被compaction到L1时候,由于阈值超过deletion_trigger ,会导致L1被添加到compaction队列,由于业务的数据特性,L1和L2存在大量重叠的数据区间,导致每次L1的compaction会同时带上大量的L2文件造成巨大的写放大。为了解决这个问题,我们对这种特性的业务数据禁用了CompactOnDeletionCollector 。通过设置表级别参数来控制表级别的compaction策略。后续会考虑优化delete trigger的时机,通过只在指定层级触发来避免大量的io放大。

compaction限速
由于rocksdb的compaction会造成大量的io读写,如果不对compaction的io进行限速,那么很可能影响到在线的写入。但是限速具体配置多少比较合适其实很难确定,配置大了影响在线业务,配置小了又会导致低峰期带宽浪费。基于此rocksdb 在5.9以后为 NewGenericRateLimiter 添加了 auto_tuned 参数,可以根据当前负载自适应调整限速。需要注意的是,该函数还有一个参数 RateLimiter::Mode 用来限制操作类型,默认值为 kWritesOnly,通常情况该模式不会有问题,但是如果业务存在大量被删除的数据,只限制写可能会导致compaction的时候造成大量的读io。

关闭WAL
由于raft log本身已经可以保证数据的可靠性,因此写入rocksdb的时候可以关闭wal减少磁盘io,节点重启的时候根据rocksdb里保存的last_apply_id从raft log进行状态机回放即可。

降副本容灾
对于三副本的raft group,单副本故障并不会影响服务的可用性,即使是主节点故障了剩余的两个节点也会快速选出主并对外提供读写服务。但是考虑到极端情况,假设同时出现两个副本故障呢? 这时只剩一个副本无法完成选主服务将完全不可用。根据墨菲定律,可能发生的一定会发生。服务的可用性一方面是稳定提供服务的能力,另一方面是故障时快速恢复的能力。那么假设出现这种故障的时候我们应该如何快速恢复服务的可用呢。
如果通过创建新的副本进行修复,新副本需要等到完成快照拷贝以后才能加入raft group进行选举,期间服务还是不可用的。那么我们可以通过强制将分片降为单副本模式,此时剩余的单个健康副本可以独自完成选主,后续再通过变更副本数的方式进行修复。

RaftLog 聚合提交
对于写入吞吐非常高的场景,可以通过牺牲一定的延时来提升写入吞吐,通过log聚合来减少请求放大。对于SSD盘,每一次写入都是4k刷盘,value比较小的时候会造成磁盘带宽的浪费。我们设置了每5ms或者每聚合4k进行批量提交。该参数可以根据业务场景进行动态配置修改。

异步刷盘
有些对于数据一致性要求不是非常高的场景,服务故障的时候允许部分数据丢失。对于该场景,可以关闭fsync通过操作系统进行异步刷盘。但是如果写入吞吐非常高导致page cache的大小超过了 vm.diry_ratio ,那么即便不是fsync也会导致io等待,该场景往往会导致io抖动。为了避免内核pdflush大量刷盘造成的io抖动,我们支持对raftlog进行异步刷盘。

透明多级存储,和缓存结合,自动冷热分离,通过将冷数据自动搬迁到kv降低内存使用成本。
新硬件场景接入,使用SPDK 进行IO提速,使用PMEM进行访问加速。

参考文献
[1] Bitcask A Log-Structured Hash Table for Fast Key/Value Data
[2] Lethe: A Tunable Delete-Aware LSM Engine

⑧ 为什么分布式数据库这么喜欢用kv store

大部分数据库都有KV存储这个抽象,但仍然存在很大的设计空间,例如单机的KV是否需要支持事务,是否需要感知schema,是否需要暴露多版本的接口。因此,不能笼统地说分布式数据库都喜欢用KV store。

分布式数据库系统通常使用较小的计算机系统,每台计算机可单独放在一个地方,每台计算机中都可能有DBMS的一份完整拷贝副本,或者部分拷贝副本,并具有自己局部的数据库,位于不同地点的许多计算机通过网络互相连接,共同组成一个完整的、全局的逻辑上集中、物理上分布的大型数据库。

结构模式

根据我国制定的《分布式数据库系统标准》,分布式数据库系统抽象为4层的结构模式。这种结构模式得到了国内外的支持和认同。

4层模式划分为全局外层、全局概念层、局部概念层和局部内层,在各层间还有相应的层间映射。这种4层模式适用于同构型分布式数据库系统,也适用于异构型分布式数据库系统。

⑨ WTable:RocksDB使用技巧之分布式存储扩容演进

RocksDB是由Facebook公司开源的一款高性能Key Value存储引擎,目前被广泛应用于业界各大公司的存储产品中,其中就包括58存储团队自研的分布式KV存储产品WTable。

RocksDB基于LSM Tree存储数据,它的写入都是采用即时写WAL + Memtable,后台Compaction的方式进行。当写入量大时,Compaction所占用的IO资源以及对其读写的影响不容忽视。而对于一个分布式存储系统来说,扩展性尤为重要,但是在扩展的过程中,又不可避免地会涉及到大量的数据迁移、写入。

本篇文章将会着重介绍WTable是如何利用RocksDB的特性对扩容流程进行设计以及迭代的。

WTable为了实现集群的可扩展性,将数据划分成了多个Slot,一个Slot既是数据迁移的最小单位。当集群的硬盘空间不足或写性能需要扩展时,运维人员就可以添加一些机器资源,并将部分Slot迁移到新机器上。这就实现了数据分片,也就是扩容。

具体哪些数据被分到哪个Slot上,这是通过对Key做Hash算法得到的,伪算法如下:
SlotId = Hash(Key)/N 其中的N就是Slot的总量,这个是一个预设的固定值。

另外,为了让同一个Slot中所有Key的数据在物理上能够存储在一起,底层实际存储的Key都会在用户Key的前面加上一个前缀:该Key对应的SlotId。具体方式是将SlotId以大端法转换成2个字节,填充到Key字节数组的前面。

在RocksDB中,除了level 0外,其余level中的sst文件,以及sst文件内部都是有序存储的。这样一来,WTable也就实现了单个Slot内数据的连续存储,以及所有Slot整体的有序性。

WTable初始的扩容方式如下:

如上图所示,迁移一个Slot可以分成3个阶段:全量迁移、增量迁移、路由信息切换。

其中全量迁移会在该Slot所在的老节点上创建一个RocksDB的Iterator,它相当于创建了一份数据快照,同时Iterator提供了seek、next等方法,可以通过遍历Iterator有序地获取一定范围内的数据。对应到这里,就是一个Slot在某一时刻的全量快照数据。老节点通过遍历Slot数据,将每个Key,Value传输到新节点,新节点写入到自己的RocksDB中。

增量迁移则会利用老WTable节点记录的Binlog,将全量迁移过程中新的写入或更新,传输到新的节点,新节点将其应用到RocksDB。

最后,当发现新老节点上待迁移Slot的数据已经追平之后,则在ETCD上修改该Slot对应的节点信息,也就是路由信息切换。从此以后,该Slot中数据的线上服务就由新节点提供了。

然而,上述的扩容方式在线上运行过程中存在一个问题:当数据迁移速度较高(如30MB/s)时,会影响到新节点上的线上服务。

深究其具体原因,则是由于一次扩容会串行迁移多个Slot,率先迁移完成的Slot在新节点上已经提供线上服务,而迁移后续的Slot还是会进行全量数据、增量数据的迁移。

通过上个章节的描述,我们可以得知,全量数据是用RocksDB Iterator遍历产生,对于一个Slot来说,是一份有序的数据。而增量数据则是线上实时写入的数据,肯定是无序的数据。所以当新节点同时写入这两种数据时,从整体上就变成了无序的数据写入。

在RocksDB中,如果某一个level N中的文件总大小超过一定阈值,则会进行Compaction,Compaction所做的就是:将level N中的多个sst文件与这些文件在level N+1中Key范围有重叠的文件进行合并,最终生成多个新文件放入level N+1中。合并的方式可以简单表述为:如果多个文件中的Key确实有交集,则按照规则进行归并排序,去重,按大小生成多个新sst文件;如果Key没有交集(说明这次合并,就没有level N+1中的文件参与),则直接将level N中的文件move到levelN+1。

这样我们就可以看出,当大量的整体无序的数据写入迁移新节点时,各level之间的sst文件Key的范围难免会重叠,而其上的RocksDB则会频繁进行大量的,需要归并排序、去重的Compaction(而不是简单的move)。这势必会占用大量的IO,进而影响读、写性能。

另外,Compaction过多、过重造成level 0层的文件无法快速沉淀到level 1,而同时数据迁移使得新节点RocksDB的写入速度又很快,这就导致level 0中的文件个数很容易就超过阈值level0_stop_writes_trigger,这时则会发生write stall,也就是停写,这时就会严重影响写服务。

根据前面的问题描述,我们深入分析了RocksDB Compaction的特点,提出了两点改进思路:

根据以上分析,我们最终将扩容分为了3个大的阶段:

整体流程如下图所示:

经过上述扩容方式的改进,目前线上WTable集群已经可以进行较高速的扩容,比如50~100MB/s,并且在整个流程中不会对线上服务有任何影响。

在制定方案之初,我们也调研过其他的方案,比如老节点传输sst文件给新节点,后者通过IngestExternalFile的方式将sst文件导入RocksDB。

但是WTable的主备同步是通过Binlog进行的,而当主机通过IngestExternalFile的方式导入数据时,并不会有Binlog产生,也就无法通过正常流程同步数据给备机。因此如果以此方式实现数据迁移,需要增加新的主备同步方式,这对原有流程是一个破坏,另外也需要比较大的工作量,效率方面也无法保证。

因此我们最终利用RocksDB Compaction的特点设计了适合WTable的快速扩容方案,解决了这个痛点。

⑩ java集合存储统计部门的职位

java集合存储统计部门的职位是后端Java开发工程师。根据查询相关公开资料信息显示,后端Java开发工程师参与公司存储平台、分布式数据库平台建设,新技术研究以及实施。主要技术方向包括分布式对象存储、分布式KV存储、共享文件系统、分布式关系型数据库。负责分布式系统的需求分析,技术调研,方案设计,代码编写以及优化等工作。归于java集合存储统计部门。

热点内容
云服务器商家怎么购买 发布:2025-07-14 08:05:34 浏览:45
c语言convert函数 发布:2025-07-14 07:56:34 浏览:483
sql存储过程打印 发布:2025-07-14 07:56:26 浏览:658
弄服务器需要什么语言 发布:2025-07-14 07:54:27 浏览:507
金土火的三才配置是什么意思 发布:2025-07-14 07:51:21 浏览:774
服务器带宽怎么计费 发布:2025-07-14 07:51:16 浏览:831
接龙编程 发布:2025-07-14 07:48:17 浏览:172
oppoa935g配置怎么样 发布:2025-07-14 07:44:47 浏览:777
c语言实训目的 发布:2025-07-14 07:44:43 浏览:691
如何设定三星密码锁 发布:2025-07-14 07:43:15 浏览:365