Redis cluster 的使用经验

ygw3 10年前

原文出处: 杨肉的演讲台   

马上要从有道离职。除了MSRA实习外人生第一份正式工作即将结束,在这个隆重的时刻自然是需要写点东西纪念一番。感性的文字不着急写,作为一个搞技术的,当然还是先写点技术文章争取对同行有所帮助。所以第一篇呢,凑个热闹,redis3.0正式版刚发布,就先说说redis cluster吧。

我在有道引入redis cluster是14年8月,到现在已经8个月了。在当时那个时间点,有道至少是词典在缓存这块的基础设施搭建还是比较薄弱的,翻译用memcache,简单的客户端写死配置来分片;词典的各种服务如果需要缓存基本上是单独搭一个redis实例,因为公司机器比较弱,大内存机器太少,所以通常是几个服务用一个实例,没有主从,纯单点。于是N个服务有M个redis实例,每个示例数据量、qps完全无法维护,基本上是某个服务的某个开发记得哪个redis的host和port,就在自己维护的服务上用哪个的节奏。

当然因为我们也不把redis当数据库,只当做一个单纯的缓存,所以挂了的结果就是redis超时之后请求全落在下层存储上。感谢redis还是足够稳定,也感谢贵司的挫机器挂了这么多也没在redis所在机器上挂过,至少我印象中redis单点挂掉这种事情还没发生,即使后来因为个人风格问题有的人写的服务是一旦redis挂了彻底不能用,也暂时没出过这个问题。倒是惠惠前段时间(那边暂时没用任何redis的集群方案)因为redis占用内存满了然后挂过……

然后是那年7月底,redis的3.0出了beta8,后来证明是最后一个beta,微博上有些号就发了类似新闻的东西,大概介绍了下3.0开始支持cluster。因为词典实际上除了主查询服务和翻译的访问量非常大之外(而词典不用独立的缓存服务,翻译用memcache),其他服务的访问量和缓存的数据量基本上单机(即使是有道那些稍微挫了点的机器)的redis全都能搞定。我对cluster感兴趣的主要原因其实是为了把散乱的缓存资源整合到一起,大家所有服务公用一个redis集群,实现资源利用的最大化。于是简单看了下redis cluster的设计:P2P,gossip,smart client。前两者因为跟Cassandra一样,对我来说比较亲切,而不像一些人对去中心化的结构总是抱有怀疑的态度。至于smart client,就意味着客户端连接redis的driver必须额外开发支持redis cluster的协议才能用,而这也是我认为当前甚至中短期内redis cluster最大的问题。当然这也意味着他理论上的延迟会比其他proxy的方案低(毕竟不需要多一次请求和数据的转发)。

然后我就搭了个测试用的redis集群,redis cluster的设计在这块有点奇葩,跟集群相关的操作需要一个外部的ruby脚本来协助(当然可能是为了让主程序的代码足够简洁?),然后那个脚本还只支持填实例的ip不支持host,还不告诉你不支持让你用host之后各种莫名其妙(不知道后来改进没)。不过反正也不是很经常用到,也无所谓了。还是那个原因——机器比较少——于是所有节点都是master,没slave。做了各种测试,压力测试遇到个问题是max和.99的响应时间高的莫名其妙,然后后来发现是因为默认开了bgsave,在fork的时候会导致停止响应,关掉bgsave开aof就搞定了。然后试了下让其中1个实例挂掉,发现整个redis cluster都不可用了,即使是有active的节点所服务的slot也不能读写,而且这是故意这么干的,这设计简直脑残。但我权衡了下利弊,无视了这个脑残设计,决定还是找个访问量即使是全落在mysql也能抗住的线上服务先试试……(当然好在后来10月份rc1发布的时候添加了一个“cluster-require-full-coverage no”的配置允许某些slot没有active节点的时候其余slot还能用。)于是从当时是全公司最牛逼的一批机器(64G内存、E5620的CPU……)里找了两台比较闲的(还有其他低load的服务在跑),各搭了8个实例,一共16个,搭出了准备给一套线上用的集群……我很好奇这是不是全球用户量超过千万的公司中第一批甚至第一个用于生产环境的redis cluster……

cluster搭好了,上层应用该迁移了。幸亏我们是个java公司,jedis可能是各种语言的redis driver里第一个能用来连cluster的(官方出了个ruby的当例子不算),没准至今还是唯一一个,但实际使用的时候发现非常坑爹——很多功能支持不全。比如JedisCluster作为接口类,各种byte[]相关的接口不支持只能String;比如无论你的timeout设成多少,JedisCluster请求的时候timeout永远是2000ms(这个在今年3月出的2.7.0才改对)。虽然说框架写好之后基于单机版本把JedisCluster改成自己想要的功能不算很难也不麻烦(我们在迁移的时候也确实这么做了),但终究是有工作量的,对技术能力弱一些的公司,完全就不现实了。更别说其他语言根本没法用了。总之就是一顿改jedis后,在一段时间内冒着一旦某个实例挂掉整个集群都不可用的风险(反正就两台机器,之前的单机也一样是单点一直也没啥事,所以非常淡定……),各种服务陆续切换上来了。然后翻译看我们这边基本靠谱就也在好像是9月或者10月也迁移过来了。也因为我们只当他是缓存,所以基本不存在数据迁移的问题,缓存预热的时候稍微控制下就可以抗住。然后我们就准备过上幸福的生活了……

但是,突然有一天,翻译的服务挂了,无任何响应。

打个jstack看,最底下醒目的deadlock。一看,jedis干的。然后看代码,发现维护集群meta信息的类里一堆synchronized方法和一堆非synchronized方法中间共用了一个读写锁,一个线程把WriteLock锁住后若干行会试图执行一个synchronized方法,另一个线程执行别的synchronized方法时会在某行试图获取ReadLock,然后就喜闻乐见的死锁了,这简直太……了。更……的是其实那个类里所有的synchronized都是多余的,而最新的代码里我发现他们已经把synchronized去掉了,理由是为了提升性能。于是开issue跟他们说了下旧的代码会死锁,建议他们尽快把最新代码发布新版,然后有人说虽然这是bug,但只要timeout别设成无穷,死锁的代码会自动超时释放的,可我们明明把timeout设的很短好不好……总之懒得理论这些事情了,改了bug之后死锁问题没了,但翻译被吓尿了,切回memcache,也因为事多人少,直到现在也没功夫重新换回redis……

后来就没遇到过问题了。于是开始总结吧。

首先先说前提:twemproxy作为老牌的redis集群方案,他确实在特定历史阶段实现了他的价值,但他肯定是不如现在的codis,具体codis哪好可以看很多文章介绍。

然后是官方cluster的优点,其实真的只有一个,就是没有proxy转发之后极限性能好,但绝大多数场景真的不重要。非说第二个优点就是他是官方的,只要redis还在维护,redis cluster被弃坑的概率就比较低,项目会持续有人维护,而第三方的方案理论上确实开发者弃坑的概率会比redis官方要大。不过只要第三方的方案真正成熟到一定程度,就算弃坑不更新大家也还是可以用。就像redis如果截止2.8.x就不开发了,大家照样会用一样。

至于缺点,就非常严重了。

第一个缺点就是严格依赖客户端driver的成熟度,redis单机方案之所以火很大程度是因为一整套方案都成熟稳定,目前各个语言的redis单机client基本非常成熟。而redis cluster的client功能不完备或者功能完备但有bug都不能忍,自己开发维护cluster client的代价又太高,大多数团队也不能忍,更何况可能一样有bug。如果把redis cluster设计成类似Cassandra,请求集群中任何一个节点都可以负责转发请求,client会好写一些,甚至可能支持用单机driver来请求cluster实现平滑升级,但多一次转发之后相对于proxy的方案就完全没有性能优势了。这个缺点在当前很严重,业务等不起,几个月后可能java不是问题、一两年后可能其他主流语言也不是问题,但还是那句话,业务不等人,你这一两年怎么办?当然不如直接用codis。

第二个缺点完全是设计问题了,就是一个redis进程既负责读写数据又负责集群交互,虽然设计者已经尽可能简化了代码和逻辑,但还是让redis从一个内存NoSQL变成了一个分布式NoSQL。分布式系统很容易有坑,一旦有坑必须升级redis,这就会涉及到某段时间内不同版本共存的问题。即使是相对比较成熟的Cassandra,也在最近的版本中出现过当集群中存在不止一个版本的节点时一定概率meta信息无法正常获取的bug,何况刚发布第一个正式版的redis。这还只是其中一种可能的坑,分布式系统的坑多了去了……

关于redis cluster的设计,Gossip/P2P的去中心化架构本身不是问题,但一旦有了中心节点,能做的事情就多了,比如sharding不均匀是很容易自动rebalance的,而无中心的只能靠外界来搞。然后redis cluster又是slot的形式而非C*式的一致性哈希,新节点分slot又不自动,依赖外界(ruby脚本)来分配显得不方便更不优美和谐。而且因为是master-slave的系统而非W+R>N的那种,master挂掉之后尽快发现是比较重要的,gossip对于节点挂掉的发现终究没有中心节点/zookeeper方便快速。不知道有没有其他系统是gossip+主从的模式。

redis作为一个非常成功的NoSQL,其协议其实是可以发扬光大的,基于proxy做转发意味着屏蔽了下层存储,完全可以根据前缀/tag/冷热程度,来把部分甚至大多数数据放在磁盘从而节约成本又保证一致性,这都是有中心节点所带来的好处。前段时间跟刘奇聊的时候发现codis也确实是这么打算的。对于只需要NoSQL的业务来说,将持久层和缓存简化成一个显然是最方便的,一个set、一个get就能搞定,并且不需要业务自己维护缓存和持久化的一致性,也更安全。当然这种让redis协议支持磁盘读写的竞争对手就是那些原本就是磁盘上的NoSQL直接开内存缓存,比如Cassandra这种LSM的数据库,memtable天生就是放最近写入的数据,通常最近写入也可能被读取;加上本身支持row cache就是个缓存,理论上干掉独立的“缓存服务”是完全可行的。