1.背景携程技术支持部这几年在Redis治理方面做了大量工作,解决了运维问题,也有很多的经验。后来通过引入Kvrocks,实现了在公有云上降本增效的目的,从而支撑了公司的国际化战略。同时,国内业务部门有降低基础设施成本的客观需求。部分业务方期望提供非传统的关系型数据库来解决某些高性能、海量空间的业务需求,并在此基础上支持特殊定制,迎接后疫情时代的挑战。这些变化让我们思考是否可以参考公有云上的思路,在私有云上构建持久化数据库,满足业务方对高性能、低成本、海量、持久化的需求。2.面临的问题回顾一下之前公有云上的解决方案,目的很明确。由于公有云内存较贵,我们将Redis数据存储在SSD上以降低成本,选择了Kvrocks,并实现了自研支持Redis的复制协议,在公有云上成本降低了60%(图1)。图1随着业务的发展和Redis集群的不断壮大,需求更加多元化。在私有云上提供服务需要有持久化的KV存储系统,包括:KV存储和读写场景,Redis可以提供存储上限太低,需要大容量的KV存储系统;数据持久化,而不是像Redis那样重启数据就丢失;节省使用Redis的成本,毕竟私有云上的Redis集群非常大;提供类似于selectforudpate的语义来实现库存等字段的扣除,而不是依赖一些外部组件,如分布式锁;数据可以提供比Redis更高的一致性,比如支持同步复制。我们仔细分析了业界的业务需求和备选方案,希望找到兼容Redis的持久化KV数据库,满足大容量和降本需求,并且不局限于Redis,能够提供更多样化的能力支持业务需求。3.调研与选型我们调研了业界大部分的NoSQL/NewSQL数据库,主要考虑以下几个方面。是否是行业主流有两层含义:第一,是否流行,比如github上的star数,是否是顶级开源基金的项目,是否有大厂背书;第二,它的概念是否主流,比如目前使用最广泛的关系型数据库MySQL和NewSQLTiDB,其相关概念如半同步复制、GTID、raft、计算存储分离等在人心。是否有成熟的中间件中间件的成熟度是一个很重要的能力。一旦选择了不合适的数据库,中间件相关的路由、管理、监控、降级、熔断、DR切换等都需要投入大量的人力物力来做。另外,稳定的中间件需要经过长时间的打磨,才能得到业务方的信任。如果可以复用现有中间件的大部分能力,可以节省大量的人力物力。集群运维管理支持是否完善在选择KV数据库时,除了中间件,还需要考虑集群扩容、缩容、实例迁移、资源利用等治理相关问题。不管是哪种数据库,都关系到部署后的运维管理。最好重用现有的能力。如果不能重复使用,需要考虑:扩容到10倍需要多长时间,能不能缩减?容易迁移吗?透明的?大规模部署后,能否提高资源利用率?性能是否满足要求?支持10倍扩展吗?否决。性能也是一个重要的考虑因素,我们希望找到一个性能优秀的KV数据库。二次开发和自主进化是否可能?对于携程这样规模或者类似规模的公司来说,持久化KV数据库大多是自研或者基于开源二次开发的,比如美团的地窖,你饿了吗?360的Tidis,360的pika等等,我们还需要选择易于二次开发或者易于扩展的数据库来开发自定义特性来支撑业务。研究过程限于篇幅,就不一一展开了。最终,我们继续选择Kvrocks作为治理进化的对象。其他NoSQL/NewSQL存在各种不足,而Kvrocks得益于Redis运维治理的成熟,可以复制。以现有的大部分Redis中间件和运维管理能力,携程和Redis的部署/使用方式几乎没有区别,无疑是目前最适合的持久化KV数据库。4、经过从Kvrocks到TRocks的不断开发迭代和使用,我们最终将新系统命名为TRocks(Trip+Kvrocks)作为携程自己的持久化KV数据库。与原来的Kvrocks相比,除了可以和Redis通信外,主要有以下改进。1.功能增强排他锁部分业务方有流程协同和执行顺序限制,经常需要用到分布式锁,比如扣库存的逻辑。一种常见的方式是引入第三方分布式系统,将锁ID存储在那里,供共享访问,从而达到锁的目的。虽然这很常见,但也存在一些问题。首先,需要引入额外的系统,分别考虑各种异常的处理,增加了整个应用的复杂度。其次,标识位往往具有一定的含义或者可以与当前的业务数据相关联,相当于存储了一个额外的业务数据,存在一定的安全隐患。同时,多个应用可能会共用一个外部分布式系统来处理锁,无形中增加了系统的访问压力。一旦出现问题,会影响多个依赖方,缺乏隔离性。为了解决此类问题,TRocks内部实现了基于Key强度的锁功能。当以分布式方式部署,作为应用的业务数据库时,它本身就具备了分发锁的能力(图2)。锁的处理与业务数据融合,无需引入冗余系统,降低复杂度,帮助业务方专注于业务代码开发。图2为了保证请求的唯一性,支持类似raft的幂等重试功能,每个请求都需要携带唯一的clientid和自增seq。这些元数据和自身的数据会作为一个writebatch写入rocksdb中,稍后会同步到slave,从而保证整个链路上请求的原子性。由于Redis命令本身的限制,有业务方反映复合命令实现一个功能。比如hashkey的超时处理,需要两次操作,一次设置value,一次设置timeout。中间件虽然封装了这一层逻辑后只提供了一个API,但是内部执行的仍然是两条命令,可能存在原子性问题。TRocks针对这种情况增加了一些复合功能命令。调用这些命令可以达到同样的效果并保证原子性。同时,这些功能对用户是透明的,直接调用客户端相应的API即可使用。2.可用性增强和一致性可调Kvrocks本身的主从复制逻辑和Redis类似,都是异步执行的。这样如果网络断了或者master挂了,以后数据就不会同步了,数据就会丢失。为了避免此类问题,TRocks加入了类似Mysql的半同步复制来提高数据的一致性。我们可以通过启用半同步模式并指定至少需要参与的半同步slave的数量来启用该功能,以提高容灾能力。比如在一个1master4slave的集群中,设置为等待任意2个slave响应。如图3所示,当满足响应的slave个数为2时,可以认为半同步完成,尽管此时另外两个slave可能还没有完成同步工作。图3但是这种方式在多机房部署的情况下可能还是有问题。因为距离远,同一机房内的数据传输速率会更高,所以master复制到同一机房内的slave通常会更快(图4)。如图4所示,很容易出现同机房slave的数据复制进度比异地机房slave快的情况。如果发生机房级故障,master所在集群的所有服务都无法正常工作,此时可能会出现数据丢失的情况。为此,我们在半同步复制的基础上加入了IDC模式,这样即使初始条件已经具备,至少也要有相关IDC的slave反馈,才能完成整个复制过程。IDC有两种模式,本地复制和远程复制。以remote模式为例,如果返回的slave数量满足条件,并且至少包含一个master所在的不同机房的slave,则完成半同步复制。如果当前响应不包括非master集群的slave,则继续等待master收到远程slave的反馈,即可完成半同步(图5)。图5虽然异地模式下数据的安全性更高,但也会影响整个系统的性能。这种糟糕的性能通常取决于不同机房之间的网络延迟。根据对性能和数据可用性的不同要求,用户可以酌情选择全异步复制(即关闭半同步)、半同步&半同步(本地)复制或半同步(异地)复制.全同步复制抑制上面说到异常情况下异步复制可能会出现数据丢失的情况。如果运维系统调整了主从关系,就会出现数据冲突。而我们现在的TRocks版本还在快速迭代中,希望每次升级的版本都能对用户透明,但事实并非如此。假设有masterA和slaveB,正常情况下A和B的数据是一致的(绿色部分),但是当A宕机的时候,B可能还没有同步到A的最新数据,此时数据B的不再增加。但是随后sentinel发现master不可访问,于是将B提升为master,开始处理写入的数据(蓝色部分)。一段时间后,系统A恢复并重新加入集群。此时A会成为masterB的slave,并尝试从B同步数据,这里可能存在冲突区(图6)。图6按照Kvrocks原来的复制逻辑,A会认为自己的数据有问题,而放弃所有数据,然后从头开始完全同步B的数据。这种行为本身并没有错。但是在实际生产环境中,如果数据量很大,全量同步的时间会比较长,而且硬盘的带宽至少比内存小两个数量级,因为我们的实例部署在容器中,可能导致灾难A在同步数据时会产生大量的IO,可能会影响到A/B所在主机上的所有实例。在数据一致性要求不是那么高的场景下,仅仅因为少量可能的数据不一致就全量重新同步的代价是非常大的。因此,我们希望在非强一致性的情况下,系统能够容忍极少量的数据差异,尽可能避免完全同步,以充分利用资源。我们的方案是,当检测到数据不一致时,master和slave会交互协调,计算出冲突区域的范围,从冲突区域之后的第一条数据开始同步。为什么不直接从冲突区后面开始同步呢?这里有一个概念。TRocks/Kvrocks的数据是appending的形式。增删改查会在日志文件中增加一条记录,并提供起始位置(Sequence),对应Redis类型不同的Records会有不同的长度(Count)。比如SET指令对应的Sequence会累加1,而HSET指令会累加2。从Sequence到Sequence+Count就是一条记录的数据范围。重同步时,如果冲突区的结束位置在正常数据的中间,则无法获取到完整的数据,需要从冲突区之后的第一条数据开始。图7冲突区和同步开始之间的区域是互补区(图7),我们通过插入空白数据来填充它,所以对于A和B来说,它们之间的不一致区域就是冲突区和同步开始区之和互补区。至于冲突的部分,我们会记录下双方的不同之处。当有差异时,参考git解决冲突的思路,将选择数据的权利交给用户。该功能上线后,版本升级变得更加容易。大多数情况下,版本升级只是实例的拉出,实例也是秒up的。升级过程对业务基本透明。在解决这个问题的同时,我们也注意到,在某些情况下,当主从数据对齐时,也会出现全同步。检查后发现是pub/sub命令的问题。这个命令是Sentinels用来订阅服务消息的,但是kvrocks的pub/sub是写操作,会造成不断的数据写入,积累rocksdb的Sequence,这样如果一个slave宕机恢复了,还没有time在和master同步的时候,sentinel写了一条无关紧要的pub消息,积累了Seq,触发了不必要的全同步,但实际上这个功能是没有必要的,所以我们修改了Kvrocks处理sentinelpubsub消息的规则,不写了之后这条命令只在内存中起作用,自然不会累加rocksdb的Sequence,排除了这种情况下完全同步的可能。3.运维治理能力提升横向扩缩容图8在上一篇Redis治理演进的文章中,我们介绍了一种新的扩缩容方案,解决Redis集群版本升级和扩容缩容的问题(图8),参照同样的思路,我们继续对BinlogServer进行改造,实现TRocks集群的横向扩缩容。该方案不仅解决了扩容和缩容的问题,还解决了Redis到Redis、TRocks到TRocks的数据迁移Redis和TRocks之间的数据迁移,也可以帮助用户从Redis访问平滑过渡到TRocks访问。但是相比Redis的扩缩容,基本不用考虑内存带宽,硬盘带宽太窄,数据迁移时的流量太大。由于所有的数据最终都需要刷到新的集群上,迁移过程中目标集群的磁盘读写会非常大,而且因为我们都是容器化部署,大量的磁盘读写也可能影响统一宿主其他无关应用,所以我们调整了TRocks的写入限流设置,避免大量写入影响磁盘性能,修改BinlogServer增加限流功能,减慢数据传输速率。多机房部署哨兵为了保证TRocks集群能够跨机房容灾,需要在多机房部署哨兵。目前,我们部署在三个机房。如下图(图9)所示:图9在部署的时候遇到了问题。我们发现在sentinel中无法选举出leader,需要等待下一个选举周期(6分钟)重新选举,造成长期的不确定性。TRocks大师。这个问题本身与TRocks无关,但是在实际使用中给我们排查问题带来了很大的麻烦。无法选举出leader的原因是多个sentinel同时发起选举,希望成为leader。结果每个哨兵最终都选择了自己,无法达成共识。查看源码发现官方在发起选举前设置了一个随机间隔(50~100ms)。但在实际操作中发现,这种随意的间隔增加了铸造失败的可能性。考虑到随机时间太短,所以我们把follow-up的间隔改成100~200ms,同时sentinel发现master宕机后立即发起选举,避免不能的问题尽可能多地选举主人。五、部分数据1、性能数据TRocks在内网上线后,已经广泛应用于各个业务线。除去公有云部分,私有云上已经有近2K个实例,数据量10T+,如下图(图10,图11)可以看到TRocks和Redis写的时候的性能对比相同的数据。同级平均响应时间为99.9%,同时我们也可以看到,得益于自定义命令,同级功能比Redis更加简洁。图10和图11根据与业务方的压力测试,一块40C和2块RAID0SATASSD在保证良好响应(99.9%<10ms)的前提下,可以提供大约8-10W的读写QPS,其中value<1千。而如果换成NVMESSD,QPS可以提升3-5倍。2.成本数据假设TRocks都是容器化部署,一台40C的主机可以部署20个实例,每个实例大小为40G,因为TRocks相对于Redis有很大的压缩功能(压缩率大约是3-7倍),如果导入Redis数据能够顺利运行,那么TRocks相比Redis可以节省90%左右的成本。既然可以节省这么多成本,那么是不是所有的Redis都换成TRocks,是否需要把私有云上的Redis都换成TRocks呢?答案是否定的,推广TRocks也不是我们的初衷。原因如下两点:如前所述,我们希望TRocks能够具备Redis的大部分能力,而不仅仅局限于Redis,而是一个通用的KV数据库,能够提供更多样化的能力来支持业务需求.硬盘的带宽和内存之间存在两个数量级的差距,这些先天的不足无法满足部分Redis场景的需求。比如大Key(>100K)的响应和Redis还是有一定的差距。另外,一些数据量小,单实例访问QPS高的实例不适合替换为TRocks。因为大规模的运维治理,我们需要考虑整个主机和每个实例是否能够顺利运行。一般来说,单实例>10G,QPS<5K比较合适。当然,NVMESSD可以大大缩短大key的响应时间,提高单实例QPS上限。六、未来规划1、复合命令的增强我们研究发现,业务往往需要多次查询TRocks才能得到一条数据。类似于从二级联系人中获取数据的逻辑,多个网络IO会导致耗时增加,设计一个通用的Commands来支持业务需求,减少网络IO就变得非常重要。另外,有用户询问TRocks的hash类型中的subkey是否也可以过期。由于hash函数仍然遵循Redis的规则,所以根据整个hashkey过期,无法实现内部数据项的过期。这个要求有一定的价值。以后我们会提供一种特殊的哈希结构来实现这种功能。2.引入checkpoint当Kvrocks1.X进行全量复制时,master会产生硬备份,复制文件会产生大量IO,而2.0正式版已经用Rocksdbcheckpoint解决了这个问题,我们也合并了2.0版本到测试准备及时升级上线。3、使用NVMESSD目前携程的大量TRocks还是运行在SATA接口的SSD上,根据我们的测试,两个SATAraid0的带宽在800MB/S左右,这使得硬盘非常容易跑满。相比之下,NVMESSD的带宽基本从几个G开始,我们测试过NVMESSD在小压力下可以提升SATASSD性能3-5倍,而且对于大key(100K以上)的情况和高压,NVMESSD的性能提升可高达10-100倍。因此,我们计划将所有SATASSD换成NVMESSD,进一步提升TRocks的性能。
