Codis做者黄东旭细说分布式Redis架构设计和踩过的那些坑们

本次分享的内容主要包括五个大部分:

  • Redis、RedisCluster和Codis; node

  • 咱们更爱一致性; mysql

  • Codis在生产环境中的使用的经验和坑们; git

  • 对于分布式数据库和分布式架构的一些见解; github

  • Q & A环节。 web

  Codis是一个分布式Redis解决方案,与官方的纯P2P的模式不一样,Codis采用的是Proxy-based的方案。今天咱们介绍一下Codis及下一个大版本RebornDB的设计,同时会介绍一些Codis在实际应用场景中的tips。最后抛砖引玉,会介绍一下我对分布式存储的一些观点和见解,望各位首席们雅正。 算法

1、 Redis,RedisCluster和Codis


  Redis:想必你们的架构中,Redis已是一个必不可少的部件,丰富的数据结构和超高的性能以及简单的协议,让Redis可以很好的做为数据库的上游缓存层。可是咱们会比较担忧Redis的单点问题,单点Redis容量大小总受限于内存,在业务对性能要求比较高的状况下,理想状况下咱们但愿全部的数据都能在内存里面,不要打到数据库上,因此很天然的就会寻求其余方案。 好比,SSD将内存换成了磁盘,以换取更大的容量。更天然的想法是将Redis变成一个能够水平扩展的分布式缓存服务,在Codis以前,业界只有Twemproxy,可是Twemproxy自己是一个静态的分布式Redis方案,进行扩容/缩容时候对运维要求很是高,并且很难作到平滑的扩缩容。Codis的目标其实就是尽可能兼容Twemproxy的基础上,加上数据迁移的功能以实现扩容和缩容,最终替换Twemproxy。从豌豆荚最后上线的结果来看,最后彻底替换了Twem,大概2T左右的内存集群。 sql

  Redis Cluster :与Codis同期发布正式版的官方cluster,我认为有优势也有缺点,做为架构师,我并不会在生产环境中使用,缘由有两个: docker

  • cluster的数据存储模块和分布式的逻辑模块是耦合在一块儿的,这个带来的好处是部署异常简单,all-in-the-box,没有像Codis那么多概念,组件和依赖。可是带来的缺点是,你很难对业务进行无痛的升级。好比哪天Redis cluster的分布式逻辑出现了比较严重的bug,你该如何升级?除了滚动重启整个集群,没什么好办法。这个比较伤运维。 数据库

  • 对协议进行了较大的修改,对客户端不太友好,目前不少客户端已经成为事实标准,并且不少程序已经写好了,让业务方去更换Redisclient,是不太现实的,并且目前很难说有哪一个Rediscluster客户端通过了大规模生产环境的验证,从HunanTV开源的Rediscluster proxy上能够看得出这个影响仍是蛮大的,不然就会支持使用cluster的client了。 api

  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其实都是同样的。

2、咱们更爱一致性


  不少朋友问我,为何不支持读写分离,其实这个事情的缘由很简单,由于咱们当时的业务场景不能容忍数据不一致,因为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,主从(半)同步复制,这个以前提到过。

3、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的位置来选择位置最佳的实例,须要修改。

4、对于分布式数据库和分布式架构的一些见解(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查询接口的分布式关系型数据库,提供表支持。

5、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的延迟时间。这个好处就是对业务基本没感知,可是缺点就是慢。


感谢刘世杰的记录与整理,臧秀涛、陈刚的校对,其余多位编辑组志愿者对本文亦有贡献。更多关于架构方面的内容,请长按下面图片,关注“高可用架构”公众号,获取通往架构师的宝贵经验。

相关文章
相关标签/搜索