之前,我们开源了腾讯云数据库Tendis存储版,同时又对这个产品的适用场景、架构、特性和发展历程进行了分享。
而这次,我们还对Tendis存储版的技术特性进行深度解读。
进入公众号,后台回复“0120刘锐”,即可下载分享PPT。本文将主要分为简介,架构,和特性三个部分展开。
tendis存储版是一款支持redis协议,数据存放在磁盘的存储引擎。架构可以简单的分为三层,一个是tendis的server层,然后是rocksdb的引擎层,底层我们通常采用ssd来提高io速度,当然sata盘也是可以的。我们通过集群的方式进行分布式部署,可以使得集群有pb级的存储能力,可以进行动态扩展,自适应容灾等功能。
同时,我们也在2020年12月20日进行了开源。
可能有的人不太清楚,我们为什么要提供tendis存储版这么一款产品?
这里最主要的原因是,redis为我们提供高性能的同时,因为是数据全部存于内存,所以成本是非常高的。tendis存储版通过讲数据存放在磁盘上面,来大幅度的减少成本。举个简单的例子,如果用户有640G的数据,每台服务器提供64G的内存,那么需要10台服务器。如果用tendis存储版,可以将640G的数据全部存放于磁盘中,这样就只需要1台服务器就可以了。
既然我们把数据存放在磁盘上面,这里肯定会带来性能的牺牲。但是因为我们采用了多线程,而redis是单线程,所以我们利用线程的优势可以获得单实例高于redis的性能。举例来说,一台32核的服务器,tendis可以获得大约30万的qps,而redis单实例大约为10万qps。当然,redis通过部署多实例的话,性能会比tendis快。
所以:tendis适合数据量大,同时对性能要求不是非常高的场景。
当然这里并不是说tendis的性能就不高,我们假设提供100台服务器的集群,那么可以提供3000万每秒的QPS,我们可以支持大约1000台服务器规模的集群,服务器数量和总qps大约成线性增长关系,相信绝大部分应用是足以支撑的。
我们支持redis的所有数据结构,采用redis同样的resp协议,支持redis的绝大部分命令,同时支持redis-cluster同样的集群管理功能,支持lua功能。所以使用和管理tendis是非常容易和方便的。
在性能上,通过优秀的设计,和一系列的优化,最后我们的性能是非常突出的。具体的数据是:在48核上,set 性能大约48万,延迟p99.9大约是2ms。
因此可以看到我们的性能对比市面上的类似产品,性能还是有较大优势的。
我们线程模型主要分为网络io线程,命令处理线程,分片模块,锁模块,命令处理模块,底层rocksdb引擎。主从复制线程,cluster集群管理模块,搬迁模块,底层compaction线程,过期线程。
我们的命令处理是多线程的,主要的目的是发挥多线程的性能优势,同时解决单线程模型下,慢查询卡住所有请求的问题。这样也可以减少实例,方便运维,同时,相比单线程的多实例部署,单个节点存储的数据更多,对gossip也有好处。
Rocksdb我们采用多实例:这样可以减少资源竞争,充分发挥存储引擎的性能优势。这样我们的key就需要按hash模以store的个数。
因为我们采用了多线程,而rocksdb内部的锁是kv级别的,如果两个线程同时操作一个redis层的key,会有并发问题。所以这里引入了数据库锁。
同样,在server层一些模块的资源需要加上mutex锁。另外rocksdb内部会有key级锁。
这里有一个死锁的问题要特别小心。这里的解决方法是,对于一个命令,要先上SX锁,再上模块锁,对于多key来说,需要排序,排序的时候,先按store排序,再按chunk排序,最后按key排序。通过固定排序的规则,来保证这些资源上锁的顺序相同,从而避免死锁的产生。
因为我们引入了SX锁,所以在rocksdb层锁冲突基本是不存在的,所以我们可以直接采用悲观事务,这样写代码就不需要retry,会简化相关逻辑。
另外,我们增加了动态调整线程数的功能,这样可以在线上环境进行方便的调整,来达到最高的性能。
这里讲解一下我们的主从复制实现方案:
这里我们采用binlog进行同步,里面存储的是物理kv。我们没有采用逻辑命令同步,主要是为了使得实现更简单,不需要对不确定性命令进行特殊处理。同时我们把binlog跟普通kv在rocksdb的同一个事务里面提交,这样的好处是,可以保证binlog与kv的原子性。
主从复制一般是先把全量备份先复制给从,然后把增量binlog发送给从,从而实现主从复制功能。如果连接断开,从可以告诉主自己的最新binlogid给主,只要这些binlog都还在db里面,就可以实现断点续传。当然,如果断开时间太长,就需要重新全量同步。
Binlog落地,就是将binlog落地到磁盘,然后定期拷贝到冷备中心,主要是用来回档的时候使用。这里为了性能的考虑,master可以不用落地,slave落地。
Binlog因为存储在db目录,如果不回收的话,是非常浪费资源的,所以需要定期回收删除。这里的删除没有采用逐个删除的方式,采用了rocksdb的deleterange接口,来提高删除的效率。
前期存储的时候,我们的binlog和kv是在同一个cf里面的,这里会有一些性能的问题,所以我们将他们拆开了不同的cf,当然,还是在同一个事务进行提交,因为rocksdb是支持事物跨cf的。简单提一下,这个拆分对性能提升是比较大的,这里具体不展开了。
为了实现回档功能,我们需要定期生成备份,回档的时候,先加载备份,然后加载binlog,这样就可以实现秒级的定点回档。为了效率,我们生成备份的时候,可以采用ckpt备份,是通过文件硬链接的方式实现的,所以只需要毫秒级别的时间。如果文件系统不支持硬链,可以采用copy备份。其中,Binlog加载的时候,我们可以按照kvstore采用多线程并行,从而提高回档效率。
为了方便集群管理,我们引入了cluster能力,并尽量保持原生redis的cluster命令。元数据的同步同样采用gossip协议。这样实现了去中心化,自动故障转移,扩缩容的能力。
在扩缩容的时候,我们需要对数据进行节点间的搬迁。原生搬迁的逻辑,有一些缺点,主要体现在,性能差,复杂结构阻塞服务,需要错误重试,依赖第三方工具。
所以tendis在内核层实现了搬迁方案,以slot为单位,通过发送快照,增量发送binlog的方式实现数据迁移,迁移成功后源节点删除数据。这样带来的好处就是,使用起来很简单,一个命令就可以了。交互少,性能高,slot的归属可以实现原子切换,失败容易回滚,不影响服务,这一系列的好处。
这是我们搬迁的时序图。可以看到我们的搬迁源节点的服务是不中断的。复杂结构也不需要上锁,搬迁成功与否是可以保证原子性的。元数据和普通数据的一致性也可以得到保证。如果搬迁失败,我们可以直接回滚,也可以尝试继续搬迁。
为了快速降低源节点的压力,我们可以把搬迁任务分解成小任务,从而实现快速的,逐步的搬迁。
同时,为了降低对在线服务的影响,我们对搬迁进行了限速,可以设置搬迁线程数,设置扫描的速度,设置搬迁数据的流量。通过这些能力,我们可以准确的控制搬迁任务对服务的影响。
对于过期功能,我们分为被动过期和主动过期。被动过期就是用户访问某个key的时候,如果发现它已经过期了,则立即进行删除操作。
主动过期如果是string结构的话,我们通过rcoskdb提供的compactFilter功能进行过期删除。对于复杂结构,因为二级key非常多,而过期信息是存储在一级key上面的,所以无法使用compactfileter来实现。如果进行全量扫描,性能又不允许。所以我们生成了一个过期索引,启动一个线程定期扫描这个索引,发现有过期的,则进行一个删除。
Tendis对lua功能也提供了支持,支持了lua的核心功能。因为我们是多线程,所以我们创建了多个lua的环境lua_state,这样可以发挥多线程的优势,从而实现lua的性能跟普通命令差不多的性能qps。为了保证lua的原子性,我们是在整个eval命令开始的时候,对所有key上一个X锁,从而实现命令的隔离性。
另外,存储版在北京地域上线,大家可以自行访问腾讯云官网进行了解和购买,基于开源的力量,欢迎各位项目共建者,将Tendis打造成能业界最受欢迎的KV存储之一。欢迎点击「阅读原文」,在开源社区中提出您宝贵的Issue和Pull Requests!