豌豆荚分布式redis设计与实现-刘奇(豌豆荚资深系统架构师)

jopen 9年前

从这个使用情况,现在国内很多的公司在测试,有一部分已经用到线上系统里面去了,现在我们目前没有发现报的BUG,这个蛮容幸的,在开源之前我们很纠结,万一被人喷了呢?现在喷子这么多,后来发布之后我们发现没有人喷,这个好意外,当然也比较小惊喜一点。目前我是在豌豆荚的基础架构部,Storage是里面小的部门,专门做存储和缓存相关的事情。

   

豌豆荚是为了响应速度是高度依赖redis,除了一部分历史原因存在的,大部分的请求都不会走到数据库。我们这个场景里面有这样一些Image-service,手机的转码很麻烦的,首次转码时间比较久,大概需要3点几秒,那个库不能做并行的,要转成WEBP还是M的格式,这个不能放缓存里面,另外一个是好友关系,这个挺夸张的,所有的好友都是在很短的时间之内取到,大概是4点几亿,这个豌豆荚实际上是有它的用户数量的公开的报道,然后还有一个applist,是豌豆荚里面最大的Codis用户,是720g,32instances,这个规划是当初按照静态分片做的规划,后来做了扩容,这个内存还是进一步省一点,后面会做一个收缩。我们现在主要的业务特点后端是Mysql和 hHbase,Mysql是相对简单一点的,或者是对它有一点特性依赖的,可能还有少部分历史遗留的代码,缓存是memcached+redis。 Hbase是分成两个小块专门负责HBase,也会提出一些BUG的提示,最新的0.98的应该打进去了,后面应该会看到豌豆荚会进入到名单里面去。

   

然后介绍一下我们这个使用redis的历程,很多都是会经过这个类似的历程,使用redis的用户举手?这个还是很夸张的,基本上很多的用户都在用这个 redis,可能大部分都是用在这个程度,单实例就可以解决所有的问题,然后当单实例解决不了的时候,采用在代码当中做Sharding然后,如果你的要求是一年有5个9,是不敢随便重启程序的。再进一步使用Twemproxy,流量没有压力的时候你会使用单个的,但是单点的问题很严重,如果挂了很严重的问题,当它流量不够的时候会进一步使用多个Twemproxy,目前豌豆荚绝大多数的都跑到Codis上面。然后使用redis是痛苦的地方,单机内存不够,带宽不够,最恶心的就是动态扩容,对于写服务的人来说只要用这个东西不担心后面的扩容怎么搞,跟运维提出一个要求,这个时候业务肯定没有办法接受的,但是运维也很痛苦,他说你现在扩容没有办法,有一个传统的办法就是说先把整个数据复制一份,复制两份一模一样的,这个时候改配置,如果这种情况下是不能这样干,本身达到单机没有办法承受,不能再把两个复制一份复制成完整的这个不太可能。这个时候就需要解决问题,还有一个磁盘损坏的时候数据抢救,是会修改多少个K,或者是每秒强制flush一次,但是数据不能丢,你直接打到数据库,不管是什么都是立刻挂,如果缓存MISS掉,对于很多的公司来说他们先做一个预热,每秒的处理能力就是那么几千,Mysql就很快挂了。

   

Twemproxy的用户两很大的,在Codis之前没有解决方案的,官方的Cluster发布一个beta版本,后面会提一下即使是正式版也有很大的风险。

   

Twemproxy的问题就是根本没有办法运维,当你装了这个之后业务的特点是什么?运维是不知道,没有办法通过观测Twemproxy观测到什么特点,主要是用什么指令,推ter甚至做的更过一点,我知道你的top 10 的KEY是什么,因为对运维的要求很高,这个需要一个好一点的工具做这件事情,官方的Cluster,首先一个无中心的设计写程序写起来是很痛苦的,老许说状态机的问题,大家看那个是单线程,到处都是状态,然后这个Cluster的实现我们看了一下,这个函数有426行代码,这个是很要命的,还有状态的切换,觉得脑子没有转过来,状态切换的时候很难保证大脑里面能想到所有的情况,这个太难了,作者还是没有想清楚,10年的时候他会说年底会出一个集群的版本,我们等了4年,大话西游都可以重播一次了,即使是刚刚出来的时候我们也是不敢踩坑的毕竟是线上的环境,我们也需要有人列出来注意事项有10条,所有的东西差不多了之后才会考虑一下这个Cluster是不是可以用了?最大的问题是这样,整个的设计是不合理的,因为整个系统是高度耦合的,它把集群和存储全部放进去了,试想一下,如果有BUG你怎么办?整个集群怎么办?怎么升级?

   

为什么一定要redis?现在有很多的方案,Tair,Couchbase,一个是快,二是数据结构非常好,数据结构是更能描述你逻辑的东西,所以 redis是高度的依赖,还有很多的公司为了追求响应速度都把数据放redis里面。终于到了我们这个Codis。这个可能大家有关注我微博我会提了一下,又一个解决方案,为什么是又一个解决方案呢?我们没有好意思说取代官方的,这个会被人喷死的,作者毕竟粉丝很多,所以说我们是替换Twemproxy 的,实际上我们是完全不考虑官方的Cluster,和一些其他的同仁聊了一下,他们觉得官方的设计是不太合理的,我们提供一个HA,这部分是用的 redis自带的同步,然后我们最重要的几个问题就是Scalability没有单点,这个是GO和C的,这个是几百行的C代码,还有一部分是不实用的,就是用于DEBUG。Codis解决这些问题,动态扩容,我第一次说我们动态缩容的时候别人震惊了一下,我们上线之后确实有这个需求,修一个机器磁盘坏了,我们不得不把这个数据倒到另外的机器上面去,缩容对于迁移机器的时候是非常有帮助的,有可能有一些别的情况下需要缩容,预先分配是10台,后来发现8 台就可以,你就很正常可以缩成8台,下次需要可以再加一台变成9台,这样可以省成本,甚至是可以做的再夸张一点,有的业务是有峰值的,内存需要的比较大,但是这部分机器可以共用的,并不是所有的业务峰值都同时发生,需要的时候动态扩一下,业务高峰期一过马上缩回来,这样可以省成本。还有可以解决下面的这个单点故障带宽不足的问题,还有就是我们公司之前会大量使用Twemproxy,为了推动业务的发展我们就写了工具从Twemproxy工具上面无缝倒到这个Codis来,我们有一个很漂亮的运维和监控的页面,就是可以监控里面是可以看到这一个用户的业务使用的哪些指令,以及这些指令使用了多少次,以及响应时间是多少,我们线上观测的数据是所有服务的指令,都是5毫秒内得到响应,整个项目没有遇到任何的go的gc问题,最新的go1.4我们测下来这个gc还是不行,我们线上用的版本是1.3.1,我们整个是Pre-sharding,最大的扩容是1023个实例,整个集群会分成那么多的SLOT,会做一些中间状态的存储,Proxy无状态,增加这个是运维操作,用户这边是没有感知的,使用非常平滑,我们现在基本上是要扩容直接给运维发一个申请要扩容就可以了,整个中间所有的业务都不需要动。我们做第一个版本花了一周的时间,但是我们之前想的比较多,大概想了两周的时间,为什么呢?因为这个分布式系统是比较复杂,而且在我们这个之前是没有能够扩容的方案的,我们要做的就是第一个,那第一个就是你要考虑的问题比较多,而且在做设计的时候当时做了一个决定,这个东西一定要开源的,所以会想的比较多也很正常,然后国内的环境大家也清楚的。首先一个就是分布式系统是复杂的,这个是做分布式系统的人都比较同意,然后我们开发人员不足,尽量拆分,简化每个模块,这个非常重要,为什么我说官方的不足的地方就是这个,他是没有办法处理升级的你的Cluster的管理和存储是绑定一起,如果有BUG,分布式这边有BUG你整个升级怎么升?要把所有的东西停下来,再全部升成新版本再启动组成新集群,这样整个业务要停下来,这个没有办法接受,大家知道redis作为一个单机的存储来讲质量是非常高的,使用也很稳定,我们希望如果存储这一块保持稳定我们希望一直稳定,如果分布式有 BUG我们就需要升级这个模块就可以了,但是官方不可能做到这一点的,整个就是一个应用程序。

   

每个组件只负责自己的事情,每个组件都可以单独测试,分布式的也是可以单独测试,对 Proxy的管理和状态都是可以单独测试的,对于整个分布式系统整个实现来说可以做极大的简化,这个质量也是很有保障的。另外redis做存储引擎,最后 Proxy的状态真正实际的东西一定是有状态的,然后状态实际上是存储在ZK上面的,你只要放这个上面其他客户端会发现新增的他们会做一个自动的均衡,还有一个考量的地方是说,因为公司有自己的运维系统,然后本身也是比较复杂的,我们不希望做一套系统和运维系统是隔离的,我们整个的redis是不是挂掉的是由外部的系统判定,他认为这个已经挂了另外一个还活着会把另外一个提升为管理者,这样会作为一个很好的结合,但是开源之后发现别的公司不愿意自己再写一个和自己的运维系统结合调用我们的API,可能后面要做一个新的模块把这个事情自动做了,因为对很多的公司来说,他们还是没有精力开发自己的运维系统调用我们的API的,但是这样的一个设计确实让整个系统变的非常的可控。

   

还有一个就是分布式系统里面的当前系统的状态,是很纠结的问题,怎么确定当前整个集群的状态是什么样?是不是稳定的?有哪些挂的哪些没有挂的怎么拿到一致性的视图?这个是很纠结的事情,然后我们在这个地方考虑的方案就是说,所有的这个状态的变更在ZK上面统统都是有记录的,只要是我们能够访问ZK整个状态都是清楚的。当前的状态是通过P2P的做慢慢的同步,很难拿到一致的状态,所以我们在这个设计上面就把这个很多的信息放在ZK上面,比如说SLOT的状态,Proxy的状态,group、lock的状态。你所有的行为就是比如说这个时候做扩容的行为,一个命令,ZK上面也有历史记录,这样一来有一个好处,只要能看到ZK你这个行为是可以复现出来的,就相当于ZK做了一个很好分布式的 LOCK,本身扩容和缩容的行为很少,所以用这个ZK控制是很好的。

   

记得去年的时候和刚刚上一场的张虎同学聊了一下,到底是proxy的还是 smart,如果是性能的话会选择一个smart client,有一点恶心的是,smart client会发布自己的客户端,你发现自己的客户端有BUG找他升级的时候就比较痛苦,别人不想升级,而且你这个升级的时候又要重新发布。反正是根本看不到过程,因为全部是透明的,还有一个就是说基于proxy监控很好做,任何时候要禁掉某一条指令这个很好做,但是你的smart client发布出去就没有办法禁止了,不会经过你这边,另外一个就是说后端信息这个是不暴露的,现在给你一个集群后面有多少台是不知道的,你也不应该关心,我告诉你总容量有100g,200g,现在需要扩到300g,你不需要关心后面任何的细节。

   

这个是一个架构图,显示的效果有点问题,我大概说一下,最上面的话是直接可以用 redis-client,中间是做一个负载均衡,你也可以直接去连我们其中任何的一个proxy,至少有一个管理者,如果你对业务的要求没有那么高,可以不配slave,建议一个Group一个maste,一个Slave。

   

这个是正常的流程,当你一个读写过来的时候,首先算一下属于那个Slot,给我们结果我们回客户端,那么这个流程不一样,因为大家知道这个系统里面比较难做的是一致性。

   

前面那个是SLOT这个有一个状态,这个会标记成正在迁移的状态,就有这样的记录,如果是从A迁移到B,那么这个请求是应该发给A还是B呢?这个时候是不知道的,先给A发一个命令要求把这个key从A迁移到B,再把请求发给B,这个K已经过去了,B给我们回一个请求,然后我们再回客户端,这样数据是一致的,这个中间需要保证的就是说迁移这个操作必须是原子的,你如果还有别的并发的操作是有问题的,但是redis是很好的保证这一点,你做这个迁移的时候必须迁移完成之后才会处理下一条命令。

   

我们把Slot状态标记为pre_migrate,然后再标记成正在迁移,大家看这三个是很标准的一个2PC的基本的流程,然后我们后台会有一个迁移进程不断的发送这个命令,把这个迁移到另外一个机器上去,直到迁移完了然后再走正常的流程,如果没有就走刚刚的迁移流程。

   

我们详细细节,这个中间必须要求所有的Proxies都要响应然后提交,这个没有用到传统的比如说我是一个管理程序给Proxies一角,我们会创建一个结点,会描述你的信息,有哪些需要响应,这个Proxies会监听这个结点的变化,发现 需要自己会去响应,于是在原来的结点再创建子结点,表示自己响应,自己响应的状态是什么。然后这样的好处就是说我们刚刚说的任何时候如果这个程序挂了,那这个时候在上面看就知道你做到那个一阶段挂掉了,到底什么程度没有响应?然后下面就是一个原子操作,我们就给redis打了几个很小的,大概几百行的代码,这个也是很经典的2PC的过程。

   

然后这个是codis-redis和原生的redis有甚么差别,这个KEY是怎么操作的,这个会计算,然后就可以跟迁移SLOT的时候,就是根据这个查找他原来的有那些KEY需要迁移然后会随机迁移。这个是我们在ZK上面的结构,大家有兴趣可以用这个客户端连一下看一下他的结构是什么,然后这个是我们内部的一些事例,这个是Applist的业务,这个是在N是group的ID,后面会跟一个详细的server为addr,同时存储一个信息描述一个更详细的情况,大家可以直接ls一下就可以看到详细的信息,这里有一个详细的事例,路由表是什么样的。

   

我们对它的修改就是这么一点,就是一个最核心的就是一个Slot的Support,内部增加了一个TAGS,比如我希望一个UID的对应的数据落到同一个 SLOT上面去,就可以用一个脚本当成这个存储的过程记录详细的变化,如果没有这个TAG的支持就不需要打,本身是DB号做一个SLOT号就可以了,我们提供了一个管理工具,这些有rebalance,直接点一下按纽就自动的做,还提供了一个Dashborrd,我们在网页上面有展示图片,我们现在对它的扩充都是通过API的调用做的,基本不会改变原代码。这样一来就是你这个东西还行,我们能够直接用吗?我说数据已经在Twemproxy上面了,其实是可以直接用的,我们介绍一个我们自己写的一个东西。

   

CodisPort具体的实现很简单,对于每个replication做一个数据协议,这样直接同步会到一个redis上面去,假设你原来的集群是这样的,一个Twemproxy后面跟了三个redis,这三个CodisProt指向三个不同的redis,业务做切换,重新指向我们新的 CodisProxy,通常是几秒的时间,基本是中断几秒的时间,如果迁移到上面的话。所以整个迁移的成本是很低的。

   

因为中间我们提到当它访问一个KEY的时候,正在是迁移状态,我们会发一个 LIST,但是不要太大,如果是上亿个这样压力会很大,迁移时间会很长,然后这个时候如果你这个时候去说我有一个100M的KEY,你迁移的时候是1点几秒,如果是1G的KEY就没有办法了,基本上不会有VALUE特别大,除非是设计上有问题,当然没有遇到这样的场景,所以我们大概提一下,我们发现有一些用户对Codis的功能有误解。

   

所以其实我们内部在做一个版本,用来做这个事情,我们把leveldb或者是rocksdb集成过来,所以在storage为就不是一个 Migration的了,这个后面看实际的需求情况,或者是看社区的要求吧,然后我们刚刚也提出过,有一些公司单独拿这个东西跑的没有很完善的运维系统,也不想自己调API。

   

然后最后一个大家关心的问题,就是Codis和Twemproxy相比性能怎么样,大概会慢20%左右,不同的指令级不一样,mget指令Codis比 Twemproxy慢很多,这个原因是因为为了保证migreate,仍然是非常快,大家可以把自己的业务测试一下,如果大家的业务需要迁移到Codis 的话,需要一个什么样流程,我建议是用先把流量倒过来,确保OK之后再做切换,当然我们这个是肯定开源了的,https://github.com /wandoulabs/codis如果你不愿意敲网址可以扫一下二维码。

PPT:http://qiniuppt.qiniudn.com/liuqi.pdf

视频:http://78rg1k.com1.z0.glb.clouddn.com/ecug-2014-liuqi.mov

来自:http://blog.qiniu.com/?p=871