https://github.com/CodisLabs/codis
Codis是一个分布式Redis解决方案,与官方的纯P2P的模式不同,Codis采用的是Proxy-based的方案。
Redis:想必大家的架构中,Redis已经是一个必不可少的部件,丰富的数据结构和超高的性能以及简单的协议,让Redis能够很好的作为数据库的上游缓存层。但是我们会比较担心Redis的单点问题,单点Redis容量大小总受限于内存,在业务对性能要求比较高的情况下,理想情况下我们希望所有的数据都能在内存里面,不要打到数据库上,所以很自然的就会寻求其他方案。 比如,SSD将内存换成了磁盘,以换取更大的容量。更自然的想法是将Redis变成一个可以水平扩展的分布式缓存服务,在Codis之前,业界只有Twemproxy,但是Twemproxy本身是一个静态的分布式Redis方案,进行扩容/缩容时候对运维要求非常高,而且很难做到平滑的扩缩容。Codis的目标其实就是尽量兼容Twemproxy的基础上,加上数据迁移的功能以实现扩容和缩容,最终替换Twemproxy。从豌豆荚最后上线的结果来看,最后完全替换了Twem,大概2T左右的内存集群。
Redis Cluster :与Codis同期发布正式版的官方cluster,我认为有优点也有缺点,作为架构师,我并不会在生产环境中使用,原因有两个:
cluster的数据存储模块和分布式的逻辑模块是耦合在一起的,这个带来的好处是部署异常简单,all-in-the-box,没有像Codis那么多概念,组件和依赖。但是带来的缺点是,你很难对业务进行无痛的升级。比如哪天Redis cluster的分布式逻辑出现了比较严重的bug,你该如何升级?除了滚动重启整个集群,没什么好办法。这个比较伤运维。
对协议进行了较大的修改,对客户端不太友好,目前很多客户端已经成为事实标准,而且很多程序已经写好了,让业务方去更换Redisclient,是不太现实的,而且目前很难说有哪个Rediscluster客户端经过了大规模生产环境的验证,从HunanTV开源的Rediscluster proxy上可以看得出这个影响还是蛮大的,否则就会支持使用cluster的client了。
Codis:和Redis cluster不同的是,Codis采用一层无状态的proxy层,将分布式逻辑写在proxy上,底层的存储引擎还是Redis本身(尽管基于Redis2.8.13上做了一些小patch),数据的分布状态存储于zookeeper(etcd)中,底层的数据存储变成了可插拔的部件。这个事情的好处其实不用多说,就是各个部件是可以动态水平扩展的,尤其无状态的proxy对于动态的负载均衡,还是意义很大的,而且还可以做一些有意思的事情,比如发现一些slot的数据比较冷,可以专门用一个支持持久化存储的server group来负责这部分slot,以节省内存,当这部分数据变热起来时,可以再动态的迁移到内存的server group上,一切对业务透明。比较有意思的是,在Twitter内部弃用Twmeproxy后,t家自己开发了一个新的分布式Redis解决方案,仍然走的是proxy-based路线。不过没有开源出来。可插拔存储引擎这个事情也是Codis的下一代产品RebornDB在做的一件事情。btw,RebornDB和它的持久化引擎都是完全开源的,见https://github.com/reborndb/reborn和https://github.com/reborndb/qdb。当然这样的设计的坏处是,经过了proxy,多了一次网络交互,看上去性能下降了一些,但是记住,我们的proxy是可以动态扩展的,整个服务的QPS并不由单个proxy的性能决定(所以生产环境中我建议使用LVS/HA Proxy或者Jodis),每个proxy其实都是一样的。
我们用的是Redis Cluster的架构,是属于分片集群的架构。而redis本身在内存上操作,不会涉及IO吞吐,即使读写分离也不会提升太多性能,Redis在生产上的主要问题是考虑容量,单机最多10-20G,key太多降低redis性能.因此采用分片集群结构,已经能保证了我们的性能。其次,用上了读写分离后,还要考虑主从一致性,主从延迟等问题,徒增业务复杂度。
因为我们当时的业务场景不能容忍数据不一致,由于Redis本身的replication模型是主从异步复制,在master上写成功后,在slave上是否能读到这个数据是没有保证的,而让业务方处理一致性的问题还是蛮麻烦的。而且Redis单点的性能还是蛮高的,不像mysql之类的真正的数据库,没有必要为了提升一点点读QPS而让业务方困惑。这和数据库的角色不太一样。所以,你可能看出来了,其实Codis的HA,并不能保证数据完全不丢失,因为是异步复制,所以master挂掉后,如果有没有同步到slave上的数据,此时将slave提升成master后,刚刚写入的还没来得及同步的数据就会丢失。不过在RebornDB中我们会尝试对持久化存储引擎(qdb)可能会支持同步复制(syncreplication),让一些对数据一致性和安全性有更强要求的服务可以使用。
说到一致性,这也是Codis支持的MGET/MSET无法保证原本单点时的原子语义的原因。 因为MSET所参与的key可能分不在不同的机器上,如果需要保证原来的语义,也就是要么一起成功,要么一起失败,这样就是一个分布式事务的问题,对于Redis来说,并没有WAL或者回滚这么一说,所以即使是一个最简单的二阶段提交的策略都很难实现,而且即使实现了,性能也没有保证。所以在Codis中使用MSET/MGET其实和你本地开个多线程SET/GET效果一样,只不过是由服务端打包返回罢了,我们加上这个命令的支持只是为了更好的支持以前用Twemproxy的业务。
在实际场景中,很多朋友使用了lua脚本以扩展Redis的功能,其实Codis这边是支持的,但记住,Codis在涉及这种场景的时候,仅仅是转发而已,它并不保证你的脚本操作的数据是否在正确的节点上。比如,你的脚本里涉及操作多个key,Codis能做的就是将这个脚本分配到参数列表中的第一个key的机器上执行。所以这种场景下,你需要自己保证你的脚本所用到的key分布在同一个机器上,这里可以采用hashtag的方式。
比如你有一个脚本是操作某个用户的多个信息,如uid1age,uid1sex,uid1name形如此类的key,如果你不用hashtag的话,这些key可能会分散在不同的机器上,如果使用了hashtag(用花括号扩住计算hash的区域):{uid1}age,{uid1}sex,{uid1}name,这样就保证这些key分布在同一个机器上。这个是twemproxy引入的一个语法,我们这边也支持了。
在开源Codis后,我们收到了很多社区的反馈,大多数的意见是集中在Zookeeper的依赖,Redis的修改,还有为啥需要Proxy上面,我们也在思考,这几个东西是不是必须的。当然这几个部件带来的好处毋庸置疑,上面也阐述过了,但是有没有办法能做得更漂亮。于是,我们在下一阶段会再往前走一步,实现以下几个设计:
使用proxy内置的Raft来代替外部的Zookeeper,zk对于我们来说,其实只是一个强一致性存储而已,我们其实可以使用Raft来做到同样的事情。将raft嵌入proxy,来同步路由信息。达到减少依赖的效果。
抽象存储引擎层,由proxy或者第三方的agent来负责启动和管理存储引擎的生命周期。具体来说,就是现在codis还需要手动的去部署底层的Redis或者qdb,自己配置主从关系什么的,但是未来我们会把这个事情交给一个自动化的agent或者甚至在proxy内部集成存储引擎。这样的好处是我们可以最大程度上的减小Proxy转发的损耗(比如proxy会在本地启动Redis instance)和人工误操作,提升了整个系统的自动化程度。
还有replication based migration。众所周知,现在Codis的数据迁移方式是通过修改底层Redis,加入单key的原子迁移命令实现的。这样的好处是实现简单、迁移过程对业务无感知。但是坏处也是很明显,首先就是速度比较慢,而且对Redis有侵入性,还有维护slot信息给Redis带来额外的内存开销。大概对于小key-value为主业务和原生Redis是1:1.5的比例,所以还是比较费内存的。
在RebornDB中我们会尝试提供基于复制的迁移方式,也就是开始迁移时,记录某slot的操作,然后在后台开始同步到slave,当slave同步完后,开始将记录的操作回放,回放差不多后,将master的写入停止,追平后修改路由表,将需要迁移的slot切换成新的master,主从(半)同步复制,这个之前提到过。
三、Codis在生产环境中的使用的经验和坑们
来说一些 tips,作为开发工程师,一线的操作经验肯定没有运维的同学多,大家一会可以一起再深度讨论。
关于多产品线部署:很多朋友问我们如果有多个项目时,codis如何部署比较好,我们当时在豌豆荚的时候,一个产品线会部署一整套codis,但是zk共用一个,不同的codis集群拥有不同的product name来区分,codis本身的设计没有命名空间那么一说,一个codis只能对应一个product name。不同product name的codis集群在同一个zk上不会相互干扰。
关于zk:由于Codis是一个强依赖的zk的项目,而且在proxy和zk的连接发生抖动造成sessionexpired的时候,proxy是不能对外提供服务的,所以尽量保证proxy和zk部署在同一个机房。生产环境中zk一定要是>=3台的奇数台机器,建议5台物理机。
关于HA:这里的HA分成两部分,一个是proxy层的HA,还有底层Redis的HA。先说proxy层的HA。之前提到过proxy本身是无状态的,所以proxy本身的HA是比较好做的,因为连接到任何一个活着的proxy上都是一样的,在生产环境中,我们使用的是jodis,这个是我们开发的一个jedis连接池,很简单,就是**zk上面的存活proxy列表,挨个返回jedis对象,达到负载均衡和HA的效果。也有朋友在生产环境中使用LVS和HA Proxy来做负载均衡,这也是可以的。 Redis本身的HA,这里的Redis指的是codis底层的各个server group的master,在一开始的时候codis本来就没有将这部分的HA设计进去,因为Redis在挂掉后,如果直接将slave提升上来的话,可能会造成数据不一致的情况,因为有新的修改可能在master中还没有同步到slave上,这种情况下需要管理员手动的操作修复数据。后来我们发现这个需求确实比较多的朋友反映,于是我们开发了一个简单的ha工具:codis-ha,用于监控各个server group的master的存活情况,如果某个master挂掉了,会直接提升该group的一个slave成为新的master。 项目的地址是:https://github.com/ngaut/codis-ha。
关于dashboard:dashboard在codis中是一个很重要的角色,所有的集群信息变更操作都是通过dashboard发起的(这个设计有点像docker),dashboard对外暴露了一系列RESTfulAPI接口,不管是web管理工具,还是命令行工具都是通过访问这些httpapi来进行操作的,所以请保证dashboard和其他各个组件的网络连通性。比如,经常发现有用户的dashboard中集群的ops为0,就是因为dashboard无法连接到proxy的机器的缘故。
关于go环境:在生产环境中尽量使用go1.3.x的版本,go的1.4的性能很差,更像是一个中间版本,还没有达到production ready的状态就发布了。很多朋友对go的gc颇有微词,这里我们不讨论哲学问题,选择go是多方面因素权衡后的结果,而且codis是一个中间件类型的产品,并不会有太多小对象常驻内存,所以对于gc来说基本毫无压力,所以不用考虑gc的问题。
关于队列的设计:其实简单来说,就是「不要把鸡蛋放在一个篮子」的道理,尽量不要把数据都往一个key里放,因为codis是一个分布式的集群,如果你永远只操作一个key,就相当于退化成单个Redis实例了。很多朋友将Redis用来做队列,但是Codis并没有提供BLPOP/BLPUSH的接口,这没问题,可以将列表在逻辑上拆成多个LIST的key,在业务端通过定时轮询来实现(除非你的队列需要严格的时序要求),这样就可以让不同的Redis来分担这个同一个列表的访问压力。而且单key过大可能会造成迁移时的阻塞,由于Redis是一个单线程的程序,所以迁移的时候会阻塞正常的访问。
关于主从和bgsave:codis本身并不负责维护Redis的主从关系,在codis里面的master和slave只是概念上的:proxy会将请求打到「master」上,master挂了codis-ha会将某一个「slave」提升成master。而真正的主从复制,需要在启动底层的Redis时手动的配置。在生产环境中,我建议master的机器不要开bgsave,也不要轻易的执行save命令,数据的备份尽量放在slave上操作。
关于跨机房/多活:想都别想。。。codis没有多副本的概念,而且codis多用于缓存的业务场景,业务的压力是直接打到缓存上的,在这层做跨机房架构的话,性能和一致性是很难得到保证的
关于proxy的部署:其实可以将proxy部署在client很近的地方,比如同一个物理机上,这样有利于减少延迟,但是需要注意的是,目前jodis并不会根据proxy的位置来选择位置最佳的实例,需要修改。
四、对于分布式数据库和分布式架构的一些看法(one more Thing)
Codis相关的内容告一段落。接下来我想聊聊我对于分布式数据库和分布式架构的一些看法。 架构师们是如此贪心,有单点就一定要变成分布式,同时还希望尽可能的透明:P。就MySQL来看,从最早的单点到主从读写分离,再到后来阿里的类似Cobar和TDDL,分布式和可扩展性是达到了,但是牺牲了事务支持,于是有了后来的OceanBase。Redis从单点到Twemproxy,再到Codis,再到Reborn。到最后的存储早已和最初的面目全非,但协议和接口永存,比如SQL和Redis Protocol。
NoSQL来了一茬又一茬,从HBase到Cassandra到MongoDB,解决的是数据的扩展性问题,通过裁剪业务的存储和查询的模型来在CAP上平衡。但是几乎还是都丢掉了跨行事务(插一句,小米上在HBase上加入了跨行事务,不错的工作)。
我认为,抛开底层存储的细节,对于业务来说,KV,SQL查询(关系型数据库支持)和事务,可以说是构成业务系统的存储原语。为什么memcached/Redis+mysql的组合如此的受欢迎,正是因为这个组合,几个原语都能用上,对于业务来说,可以很方便的实现各种业务的存储需求,能轻易的写出「正确」的程序。但是,现在的问题是数据大到一定程度上时,从单机向分布式进化的过程中,最难搞定的就是事务,SQL支持什么的还可以通过各种mysqlproxy搞定,KV就不用说了,天生对分布式友好。
于是这样,我们就默认进入了一个没有(跨行)事务支持的世界里,很多业务场景我们只能牺牲业务的正确性来在实现的复杂度上平衡。比如一个很简单的需求:微博关注数的变化,最直白,最正常的写法应该是,将被关注者的被关注数的修改和关注者的关注数修改放到同一个事务里,一起提交,要么一起成功,要么一起失败。但是现在为了考虑性能,为了考虑实现复杂度,一般来说的做法可能是队列辅助异步的修改,或者通过cache先暂存等等方式绕开事务。
但是在一些需要强事务支持的场景就没有那么好绕过去了(目前我们只讨论开源的架构方案),比如支付/积分变更业务,常见的搞法是关键路径根据用户特征sharding到单点MySQL,或者MySQLXA,但是性能下降得太厉害。
后来Google在他们的广告业务中遇到这个问题,既需要高性能,又需要分布式事务,还必须保证一致性:),Google在此之前是通过一个大规模的MySQL集群通过sharding苦苦支撑,这个架构的可运维/扩展性实在太差。这要是在一般公司,估计也就忍了,但是Google可不是一般公司,用原子钟搞定Spanner,然后再Spanner上构建了SQL查询层F1。我在第一次看到这个系统的时候,感觉简直惊艳,应该是第一个可以真正称为NewSQL的公开设计的系统。所以,BigTable(KV)+F1(SQL)+Spanner(高性能分布式事务支持),同时Spanner还有一个非常重要的特性是跨数据中心的复制和一致性保证(通过Paxos实现),多数据中心,刚好补全了整个Google的基础设施的数据库栈,使得Google对于几乎任何类型的业务系统开发都非常方便。我想,这就是未来的方向吧,一个可扩展的KV数据库(作为缓存和简单对象存储),一个高性能支持分布式事务和SQL查询接口的分布式关系型数据库,提供表支持。
五、Q & A
Q1:我没看过Codis,您说Codis没有多副本概念,请问是什么意思?
A1:Codis是一个分布式Redis解决方案,是通过presharding把数据在概念上分成1024个slot,然后通过proxy将不同的key的请求转发到不同的机器上,数据的副本还是通过Redis本身保证
Q2:Codis的信息在一个zk里面存储着,zk在Codis中还有别的作用吗?主从切换为何不用sentinel
A2:Codis的特点是动态的扩容缩容,对业务透明;zk除了存储路由信息,同时还作为一个事件同步的媒介服务,比如变更master或者数据迁移这样的事情,需要所有的proxy通过特定zk事件来实现 可以说zk被我们当做了一个可靠的rpc的信道来使用。因为只有集群变更的admin时候会往zk上发事件,proxy到以后,回复在zk上,admin收到各个proxy的回复后才继续。本身集群变更的事情不会经常发生,所以数据量不大。Redis的主从切换是通过codis-ha在zk上遍历各个server group的master判断存活情况,来决定是否发起提升新master的命令。
Q3:数据分片,是用的一致性hash吗?请具体介绍下,谢谢。
A3:不是,是通过presharding,hash算法是crc32(key)%1024
Q4:怎么进行权限管理?
A4:Codis中没有鉴权相关的命令,在reborndb中加入了auth指令。
Q5:怎么禁止普通用户链接Redis破坏数据?
A5:同上,目前Codis没有auth,接下来的版本会加入。
Q6:Redis跨机房有什么方案?
A6:目前没有好的办法,我们的Codis定位是同一个机房内部的缓存服务,跨机房复制对于Redis这样的服务来说,一是延迟较大,二是一致性难以保证,对于性能要求比较高的缓存服务,我觉得跨机房不是好的选择。
Q7:集群的主从怎么做(比如集群S是集群M的从,S和M的节点数可能不一样,S和M可能不在一个机房)?
A7:Codis只是一个proxy-based的中间件,并不负责数据副本相关的工作。也就是数据只有一份,在Redis内部。
Q8:根据你介绍了这么多,我可以下一个结论,你们没有多租户的概念,也没有做到高可用。可以这么说吧?你们更多的是把Redis当做一个cache来设计。
A8:对,其实我们内部多租户是通过多Codis集群解决的,Codis更多的是为了替换twemproxy的一个项目。高可用是通过第三方工具实现。Redis是cache,Codis主要解决的是Redis单点、水平扩展的问题。把codis的介绍贴一下: Auto rebalance Extremely simple to use Support both Redis or rocksdb transparently. GUI dashboard & admin tools Supports most of Redis commands. Fully compatible with twemproxy(https://github.com/twitter/twemproxy). Native Redis clients are supported Safe and transparent data migration, Easily add or remove nodes on-demand.解决的问题是这些。业务不停的情况下,怎么动态的扩展缓存层,这个是codis关注的。
Q9:对于Redis冷备的数据库的迁移,您有啥经验没有?对于Redis热数据,可以通过migrate命令实现两个Redis进程间的数据转移,当然如果对端有密码,migrate就玩完了(这个我已经给Redis官方提交了patch)。
A9:冷数据我们现在是实现了完整的Redissync协议,同时实现了一个基于rocksdb的磁盘存储引擎,备机的冷数据,全部是存在磁盘上的,直接作为一个从挂在master上的。实际使用时,3个group,keys数量一致,但其中一个的ops是另外两个的两倍,有可能是什么原因造成的?key的数量一致并不代表实际请求是均匀分布的,不如你可能某几个key特别热,它一定是会落在实际存储这个key的机器上的。刚才说的rocksdb的存储引擎:https://github.com/reborndb/qdb,其实启动后就是个Redis-server,支持了PSYNC协议,所以可以直接当成Redis从来用。是一个节省从库内存的好方法。
Q10:Redis实例内存占比超过50%,此时执行bgsave,开了虚拟内存支持的会阻塞,不开虚拟内存支持的会直接返回err,对吗?
A10:不一定,这个要看写数据(开启bgsave后修改的数据)的频繁程度,在Redis内部执行bgsave,其实是通过操作系统COW机制来实现复制,如果你这段时间的把几乎所有的数据都修改了,这样操作系统只能全部完整的复制出来,这样就爆了。
Q11:刚读完,赞一个。可否介绍下codis的autorebalance实现。
A11:算法比较简单,https://github.com/wandoulabs/codis/blob/master/cmd/cconfig/rebalancer.go#L104。代码比较清楚,code talks:)。其实就是根据各个实例的内存比例,分配slot好的。
Q12:主要想了解对降低数据迁移对线上服务的影响,有没有什么经验介绍?
A12:其实现在codis数据迁移的方式已经很温和了,是一个个key的原子迁移,如果怕抖动甚至可以加上每个key的延迟时间。这个好处就是对业务基本没感知,但是缺点就是慢。
redis del大key可能带来的风险
redis命令 del 的时间复杂度是o(m), 其中m就是key中所包含的元素个数。比如当del list、set、hash等数据结构的时候,如果这样的key里面的数据达到千万级别,可能会阻塞redis进程超过10s,其他并发的命令都超时。
redis进程阻塞时间过长可能会导致redis的主从切换,引发更大的问题
类似的命令还有hgetall
三套集群架构的演进:
Replication+Sentinel
这套架构使用的是社区版本推出的原生高可用解决方案,其架构图如下!
这里Sentinel的作用有三个:
监控:Sentinel 会不断的检查主服务器和从服务器是否正常运行。
通知:当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知。
自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系 的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点。
工作原理就是,当Master宕机的时候,Sentinel会选举出新的Master,并根据Sentinel中client-reconfig-script脚本配置的内容,去动态修改VIP(虚拟IP),将VIP(虚拟IP)指向新的Master。我们的客户端就连向指定的VIP即可!
缺陷:
(1)主从切换的过程中会丢数据
(2)Redis只能单点写,不能水平扩容
Proxy+Replication+Sentinel
这里的Proxy目前有两种选择:Codis和Twemproxy。我经历这套架构的时间为2015年,当时我好像咨询过我的主管为啥不用Codis和Redis官网的Redis Cluster。原因有二:
据说是因为Codis开源的比较晚,考虑到更换组件的成本问题。毕竟本来运行好好的东西,你再去换组件,风险是很大的。
Redis Cluster在2015年还是试用版,不保证会遇到什么问题,因此不敢尝试。
所以我没接触过Codis,之前一直用的是Twemproxy作为Proxy。
这里以Twemproxy为例说明
工作原理如下
前端使用Twemproxy+KeepAlived做代理,将其后端的多台Redis实例分片进行统一管理与分配
每一个分片节点的Slave都是Master的副本且只读
Sentinel持续不断的监控每个分片节点的Master,当Master出现故障且不可用状态时,Sentinel会通知/启动自动故障转移等动作
Sentinel 可以在发生故障转移动作后触发相应脚本(通过 client-reconfig-script 参数配置 ),脚本获取到最新的Master来修改Twemproxy配置
缺陷:
(1)部署结构超级复杂
(2)可扩展性差,进行扩缩容需要手动干预
(3)运维不方便
Redis Cluster
我经历这套架构的时间为2017年,在这个时间Redis Cluster已经很成熟了!你们在网上能查到的大部分缺点,在我接触到的时候基本已经解决!
比如没有完善的运维工具?可以参照一下搜狐出的CacheCloud。
比如没有公司在生产用过?我接触到的时候,百度贴吧,美团等大厂都用过了。
比如没有Release版?我接触到的时候距离Redis Cluster发布Release版已经很久。
而且毕竟是官网出的,肯定会一直维护、更新下去,未来必定会更加成熟、稳定。换句话说,Redis不倒,Redis Cluster就不会放弃维护。所以,我推荐还是这套架构!
工作原理如下
客户端与Redis节点直连,不需要中间Proxy层,直接连接任意一个Master节点
根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上,然后Redis会去相应的节点进行操作
具有如下优点:
(1)无需Sentinel哨兵监控,如果Master挂了,Redis Cluster内部自动将Slave切换Master
(2)可以进行水平扩容
(3)支持自动化迁移,当出现某个Slave宕机了,那么就只有Master了,这时候的高可用性就无法很好的保证了,万一master也宕机了,咋办呢? 针对这种情况,如果说其他Master有多余的Slave ,集群自动把多余的Slave迁移到没有Slave的Master 中。
缺点:
(1)批量操作是个坑
(2)资源隔离性较差,容易出现相互影响的情况。
Redis集群解决方案有三类,它们都是通过将key分散到不同的redis实例上来提高整体能力, 这种方法称为分片(sharding):
服务端分片: 客户端与集群中任意的节点通信,服务端计算key在哪一个节点上,若不再当前节点上则通知客户端应访问的节点。 典型代表为官方推出的Redis Cluster
客户端分片: 客户端计算key应在集群中的哪一个节点上,并与该节点通信。典型代表为ShardedJedis
代理分片: 客户端与集群中的代理(proxy)通信,代理与节点通信进行操作。典型代表为Codis
单机版的Redis中单条指令的执行总是原子性的,在集群中则难以保证这一性质,某些指令可能无法在集群中使用或者受到限制。
若需要使用这些指令或需要它们保持原子性,可以采用单机版Redis和集群搭配使用的方法。将主要业务部署在集群上,将需要较多支持的服务部署在单机版Redis上
哈希槽
哈希槽(hash slot)是来自Redis Cluster的概念, 但在各种集群方案都有使用。
哈希槽是一个key的集合,Redis集群共有16384个哈希槽,每个key通过CRC16散列然后对16384进行取模来决定该key应当被放到哪个槽中,集群中的每个节点负责一部分哈希槽。
以有三个节点的集群为例:
节点A包含0到5500号哈希槽
节点B包含5501到11000号哈希槽
节点C包含11001到16384号哈希槽
这样的设计有利于对集群进行横向伸缩,若要添加或移除节点只需要将该节点上的槽转移到其它节点即可。
在某些集群方案中,涉及多个key的操作会被限制在一个slot中,如Redis Cluster中的mget/mset操作。
HashTag
HashTag机制可以影响key被分配到的slot,从而可以使用那些被限制在slot中操作。
HashTag即是用{}包裹key的一个子串,如{user:}1, {user:}2。
在设置了HashTag的情况下,集群会根据HashTag决定key分配到的slot, 两个key拥有相同的HashTag:{user:}, 它们会被分配到同一个slot,允许我们使用MGET命令。
通常情况下,HashTag不支持嵌套,即将第一个{和第一个}中间的内容作为HashTag。若花括号中不包含任何内容则会对整个key进行散列,如{}user:。
HashTag可能会使过多的key分配到同一个slot中,造成数据倾斜影响系统的吞吐量,务必谨慎使用。
主从模型
几种流行的Redis集群解决方案都没有将一个key写到多个节点中,若某个节点故障则无法访问访问其上的key这显然是不满足集群的分区容错性的。
Redis集群使用主从模型(master-slave)来提高可靠性。每个master节点上绑定若干个slave节点,当master节点故障时集群会推举它的某个slave节点代替master节点
协议支持
命令 Redis Cluster ShardedJedis Codis
mget/mset 仅限同一个slot 不支持 失去原子性
keys 仅限同一个slot 不支持 不支持
scan 仅限同一个slot 不支持 仅限同一个slot(SLOTSSCAN命令)
rename 仅限同一个slot 不支持 不支持
pipeline 不支持 不支持 支持
事务 支持相同slot 不支持 不支持
发布/订阅 支持 不支持 不支持
eval 仅限同一slot 不支持 支持
redigo redis pool,咋一看没啥问题,
MaxIdle: 池子里的最大空闲连接
IdleTimeout: 超过这个duration的空闲连接,会被关闭
TestOnBorrow: 用之前检查这个连接是不是健康的
问题就在于这个Dial,
Dial is an application supplied function for creating and configuring a connection.
pool结构体里的这个Dial 就是用来创建,并配置一个redis连接的。
坑就在这,我们执行redis 命令是通过connection对象的Do方法
如果你不传入timeout的值,那么默认0值的话,这两个set deadline的逻辑就跳过了。。。
如果不设置read/write timeout 会导致什么问题呢?假如网络有波动,执行一个redis 命令的时候,一直没收到服务器的响应,会导致这次请求一直没有返回,晾在那。直到redis服务器设置的超时时间到了,关闭连接,然后就会读到一个EOF的错误。
单点redis的情况,如果不设置MaxActive,redis pool的连接数是没有上限的,问题就不会暴露出来,这对我们的服务来说,影响也不大,就是在错误日志中,会多几条redis相关的EOF日志,但是这样真的没问题么?当然有问题,如果是从redis读消息,没有设置read timeout,一直读不到,这个协程就卡在那,迟迟不给响应,对用户来说很不好。
使用集群模式,一般redis_proxy 会限制连接数,所以redis pool 就应该用MaxActive限制池子里的最大连接数,这时候如果不设置read/write timeout,问题就来了,池子里的连接会越来越少直到没有。
因此,不管那种情况,我们都应该给redis.Dial这个方法,传入三个超时时间,DialConnectTimeout, DialReadTimeout,DialWriteTimeout。
codis 的架构
这套架构的特点:
分片算法:基于 slot hash桶;
分片实例之间相互独立,每组 一个master 实例和多个slave;
路由信息存放到第三方存储组件,如 zookeeper 或etcd
旁路组件探活
slots 方案:划分了 1024个slot, slots 信息在 proxy层感知; redis 进程中维护本实例上的所有key的一个slot map;
迁移过程中的读写冲突处理:
最小迁移单位为key;
访问逻辑都是先访问 src 节点,再根据结果判断是否需要进一步访问 target 节点;
访问的 key 还未被迁移:读写请求访问 src 节点,处理后访问:
访问的 key 正在迁移:读请求访问 src 节点后直接返回;写请求无法处理,返回 retry
访问的 key 已被迁移(或不存在):读写请求访问 src 节点,收到 moved 回复,继续访问 target 节点处理
官方 redis cluster
数据迁移过程:
基于 key粒度的数据迁移;
迁移过程的读写冲突处理:
从A 迁移到 B;
访问的 key 所属slot 不在节点 A 上时,返回 MOVED 转向,client 再次请求B;
访问的 key 所属 slot 在节点 A 上,但 key 不在 A上, 返回 ASK 转向,client再次请求B;
访问的 key 所属slot 在A上,且key在 A上,直接处理;(同步迁移场景:该 key正在迁移,则阻塞)