泡泡后台Couchbase缓存使用经验分享
一、导读
爱奇艺的社交业务“泡泡”,拥有日活用户6千万+,后台系统每日高峰期间接口QPS可以达到80K+,与视频业务的主要区别是泡泡业务更多地引入了与用户互动相关的数据,读、写的量均很大。无论是庞大的数据量,还是相对较高的QPS,使得我们在绝大多数场景下都依赖于高可靠、高性能、以及存储量巨大的在线缓存系统。本文介绍了一些我们在使用缓存方面的经验以及优化的方法,希望对大家有所帮助。
二、Couchbase简介
Couchbase(下文简称CB)是由CouchOne(创办人包括CouchDB的设计者)和Membase(由memcached的主要开发人员建立)两家公司在2011年初合并而来。Membase公司有一个名为Membase的产品,它是个键/值、持久化、可伸缩的解决方案,使用了memcached wire协议和SqlLite嵌入式存储引擎。CouchOne支持的CouchDB是个文档数据库,提供了端到端的复制方法,这对于移动与分布在不同位置的数据中心来说是很有用的。Couchbase是基于Membase与CouchDB开发的一款新产品,综合了两者的优点。
三、Couchbase vs Redis
Couchbase和Redis均是非常优秀的缓存产品,爱奇艺泡泡社交后台根据不同的适用场景大量地使用了它们,总的来讲它们各有优劣,总结如下:
Redis优势:
(1)Redis能够支持更多的数据结构;
(2)Redis支持更多的应用场景(队列、发布订阅、排行榜);
(3)Redis很多操作可以放到服务端做,减少了网络io;
(4)Redis社区更繁荣一点,文档比较丰富,解决问题更快捷;
(5)Redis支持事务。
Couchbase优势:
(1)CB能支撑更大的容量,Redis最好低于20G,否则failover之后恢复非常慢;
(2)CB有一个比较专业的管理界面;
(3)一致性hash发生在客户端,性能更高;
(4)某台server宕机不影响业务方使用,只是可用内存受影响,可用性更高;
(5)可扩展性强。
Redis 的亮点在于业务契合度高,CB 的亮点在于高可用。以下节选一些具体的功能点对比:
![](http://pic3.iqiyipic.com/common/20171113/1f7c5c3e87874ca88b8a4918965c5704.jpg)
![](http://pic1.iqiyipic.com/common/20171113/031ffa30f01b4fe5b951821b6c3148b4.jpg)
四、基础知识:CB中的vBucket
在 CB 中,我们所操作的每一个bucket会逻辑划分为1024个vBucket,其数据的储存是基于每个vBucket储存,并且每个 vBucket都会映射到相对应的服务器节点,这种储存结构的方式叫做集群映射。如图所示,当应用与Couchbase服务器交互时,会通过SDK与服务器数据进行交互,当应用操作某一个bucket的key值时,在SDK中会通过哈希的方式计算,使用公式crc32(key)%1024确定key 值是属于1024个vBucket中的某个,然后根据vBucket所映射的节点服务器对数据进行操作。
为了保证分布式存储系统的高可靠性和高可用性,数据在系统中一般存储多个副本。当某个副本所在的存储节点出现故障时,分布式存储系统能够自动将服务切换到其它的副本,从而实现自动容错。
![](http://pic0.iqiyipic.com/common/20171113/93bee2b131884ce9972dac76de3134bc.jpg)
五、友善的Web console
CB自带了一套非常友善的Web console,使得运维及监控操作非常地方便。
![](http://pic1.iqiyipic.com/common/20171114/4.jpg)
![](http://pic3.iqiyipic.com/common/20171206/0e8510e106c84820aede40d6b9137adb.png)
六、案例1: 点赞系统缓存优化
首先介绍一下点赞系统业务场景:
点赞系统承载了两种业务的点赞,第一种叫做feed点赞:我们管泡泡圈子承载的信息流叫做feed流,而其中的每一篇帖子叫做一条feed。为了便于理解,大家可以把微信朋友圈当做是个feed流,而其中的每一条状态帖子就是一条feed。这类业务系统的要求是,有feed流漏出的地方就会查询点赞系统,查询方式是一个uid(全站用户唯一id)+一组[feedid]列表;
第二种业务是评论业务的点赞,评论列表漏出的地方就会查询点赞系统,查询方式是一个uid+一组[commentid]列表(每条评论也有一个全局唯一id叫commentid)。
![](http://pic0.iqiyipic.com/common/20171114/6.jpg)
![](http://pic0.iqiyipic.com/common/20171114/7.jpg)
阶段一:Redis缓存
使用Redis做为缓存,缓存30G打满,通过LRU置换key。
点赞系统的第一个阶段,缓存结构中的key是实体id(即feedid或commentid),value是点赞过该实体的uid 集合。按照实体id查询所有给这个实体点过赞的uid,并且更新Redis缓存。采用这种设计的系统情况是成功率经常抖动,响应时间不稳定。
![](http://pic3.iqiyipic.com/common/20171206/a090d9f1e1524355b1f92df84c4abcc3.png)
通过业务日志分析,发现hbase穿透很频繁,缓存命中率不高,原因是缓存容量过低。由于公司云平台提供的是主从模式非集群的Redis,理论上最大缓存量(max_memory)不宜过大(30G已经是上限了),否则会引起主节点故障后failover到备用节点时间过长、甚至循环failover的情况。
当时考虑了两种可能的优化方案:使用CB;或使用 Redis cluster。调研之后发现,原生的Redis cluster不支持批量查询,而如果使用Redis cluster proxy的话可能会有较大性能损耗,并且Redis cluster不支持cluster集群之间的数据同步(我们的场景会用到三个IDC之间的集群同步),所以最终舍弃掉这个方案,考虑使用CB。
阶段二:使用CB替换Redis
缓存key的考虑:uid维度,浪费空间,很多过时的数据会被加载到内存,并且受制于hbase表结构,无法实时拿到某用户点过的所实体。而使用实体id维度,CB不支持server端查询,需要把数据拉到客户端来判断,对于比较热门的实体,uid可能成千上万,数据量太大。uid+实体id维度,CB用量可能比较大,不过查询数据量很小。
最终我们的key采用了uid+实体id的维度,value为是否点过赞。根据uid+实体id进行exist操作(缓存没命中则找不到key,命中的话会返回true或false表明是否点过赞),查询结果更新缓存。
但是很快发现了一些问题:由于缓存命中率低(同一个人再次访问同一个feed才会命中,这种情况比较少见),造成hbase压力过大。不过因为查询hbase使用exist操作,不需要读取数据,所以响应时间很短,成功率基本没有受影响。同时分析了当时的缓存用量的增长趋势,发现CB容量可能扛不住。
![](http://pic1.iqiyipic.com/common/20171206/4f26c0a1d5094b9a86053cfded174745.jpg)
阶段三:key的优化
采用实体id+shard方式存储(shard=uid%factor ,实体id维度的变种)
缓存中key使用实体id+shard的方式,value是点过赞的用户列表。穿透到hbase后,根据实体id查询出所有给该实体id点过赞的uid,然后分shard更新缓存,没有数据的shard也要更新上标识。这种方案需要考虑shard个数,在空间和时间上取一个平衡。shard数越大,浪费的CB空间越多,但是每次查询结果集越小。
下面用一个例子来解释两种方案:实体id为e1,factor=20。
![](http://pic1.iqiyipic.com/common/20171114/10.jpg)
优化后的效果如下图,可见成功率相对趋于稳定,且缓存命中率已经高于了99%。
![](http://pic3.iqiyipic.com/common/20171206/4ce9e29a51474f59a5a884e128e9b907.png)
![](http://pic1.iqiyipic.com/common/20171114/12.jpg)
至此优化告一段落了,在这个案例中,有个事情是值得思考的:怎样才能获得更高的命中率呢?我们的答案是谁重复的次数多,以谁为维度缓存。某一个用户可能浏览的feed个数是有限的几十个或者几百个,而某个feed可以被几百万甚至几千万的用户看到。很明显实体重复次数多,所以我们是以实体id为维度(key)来存储。
七、案例2: 投票系统缓存优化
项目背景:投票计数器缓存结构,一个投票可以简单理解为vid(投票id)+多个oid(选项),需要如下缓存,用做计数器。
![](http://pic1.iqiyipic.com/common/20171114/13.jpg)
此次优化主要是针对某热门综艺活动暴露出来的问题。该活动的投票分多个渠道(4个),每个渠道对应一个投票,同时还有一个汇总的投票,每个渠道的选项数是70个。每次业务查询投票接口,各渠道带过来的投票vid都是自己渠道的投票vid,后端需要查询子渠道投票和汇总投票信息,整合之后返回给App端上。
计数器读写模型如下:
之所以每个选项都是一个key,是因为这里考虑到分布式并发操作,需要用到原子的incr操作来增加投票计数。该模式的问题是业务高峰期间对CB的OPS达到到1,400,000次/秒,CB物理机CPU报警。经排查监控分析,发现该综艺活动查询接口QPS有明显变化,并且发现问题时候,大部分流量来自该接口。分析该接口相关代码,发现如果选项过多,一次业务查询,该段逻辑需要批量查300个左右的key,如果业务QPS达到5k(其实并不高),那么CB的OPS将达到1,400,000次/秒左右,确认OPS增加是导致该问题的原因。
定位了原因,优化就很简单了:用一个异步task任务,每隔3s(具体时间间隔需根据业务情况确认)汇总一次所有CB的key,相当于业务的一次查询请求,只需要读一次CB的汇总key的操作,而写请求基本不变(只多了每3秒一次的汇总操作,基本可以忽略),OPS大大降低,减轻物理机CPU压力。
![](http://pic2.iqiyipic.com/common/20171114/15.jpg)
优化后效果非常明显,对CB的OPS大幅降低:
![](http://pic0.iqiyipic.com/common/20171114/16.jpg)
八、案例3: CB客户端SDK版本升级
项目背景:低版本SDK在CB集群rebalance完之后,需要重启,否则可能会有很多超时情况,此为第一个原因。此外,某些后台系统发现新扩容的worker机经常抛出如下异常,然后自动重连,并且不断重复这个过程,导致接口成功率下降。对比了新老机器的nginx、tomcat、代码、jdk、能想到的linux系统参数、QPS、目标访问数据等等,都是完全一致的,且夜间QPS低时候,该问题没有那么明显。在找不到具体触发该异常原因的情况下,由于异常是在CB的SDK中抛出来的(看起来像是读取某个buffer时候越界),因此最直接的方法肯定是尝试升级SDK。
![](http://pic3.iqiyipic.com/common/20171206/ad65cab4c9584544b5037b688aaf6bca.jpg)
最后我们的方案是将老版本的SDK 1.4.11 升级到 2.3.2。由于操作的都是线上系统,因此升级时需要考虑几点:
(1)新老API兼容问题;
(2)升级不能污染老数据,保证有问题可以回滚;
(3)由于新版本SDK提供了很多新功能,可以考虑顺便优化缓存设计
最终我们根据两个系统的不同情况,采用了两种不同的升级策略:
系统A:缓存中的value都是string,可以跟CB 2.x中的StringDocument完美兼容,所以直接在原来的CB集群上面操作上线。
系统B:缓存中的value类型比较多,如果直接在线上CB集群操作,风险较大,可能会造成数据被污染,且无法回滚,并且因为历史原因线上集群中有一些ttl=0的脏数据,所以采用切换到临时集群的方式:
![](http://pic0.iqiyipic.com/common/20171114/18.jpg)
九、其它注意事项
1.关于热缓存中的数据,有两种方式
需要根据具体业务来选择:
方式1:用线上虚机热数据,即挑选1~2台worker机切换到新CB,相当于一上来这几台worker机的请求是100%到存储上的,但是穿透完后会将数据种回缓存。待缓存命中率达到80%左右就可以继续增加worker机,直到所有worker机都切换到新缓存上。这种方案的优点是简单,缺点就是过程可能会有点慢,且切换过程中可能因为新、旧缓存不一致导致不同worker查出来的数据短暂不同步。
方式2:用作业热缓存。根据指定的规则跑MapReduce作业(比如持久化数据是像我们一样存在hbase中的),一次性把数据刷到缓存中,并且需要把刷存量数据期间产生的增量数据做一个特殊记录,在刷完存量数据后再刷一遍增量的数据。这种方案的优点是过程比较快,缺点是复杂度高。
2.关于跨IDC数据同步的常用两种方式
Couchbase本身就是集群,但通常所谓的集群指的是单一IDC内部的集群,由于机制的限制通常也不会推荐一个集群中存在跨IDC的CB实例。因此像我们这样比较大型的、牵扯到多IDC的后台系统,就需要考虑CB的跨IDC同步问题了。通常有两种方案:
(1)使用CB自带的XDCR,简单快捷,对业务方透明,一致性有保障。比较推荐
(2)业务自己做同步数据。如使用kafka等消息服务来同步数据,即更新CB的worker机发送一条消息到kafka队列,每个IDC都部署一台consumer消费这条kafka消息,种自己本地IDC的CB缓存。这样,相当于多个IDC之间的CB集群本身是独立的,仅仅通过业务方自己的consumer来进行数据同步。该方式比较依赖消息服务,稳定性不是很高,一致性保障难度比较高,业务方需衡量。在早期的XDCR不是很稳定时我们曾有业务系统使用了这种方案,后续也逐步切换成了XDCR的方案。
3.关于遍历所有key
CB不像Redis那样有个keys(*)方法,可以用于遍历所有缓存中的key。因此识别并清除CB中的脏数据(比如早期存了一些ttl=0的数据,后来又不用了)是一件比较难的事情。但是有一种替代方案:同步线上数据到离线CB集群(可以用XDCR),构建view,使用restful api分页查询,遍历所有key。找到脏数据,删除在线集群对应的key。注意该方式value必须是json。
![](http://pic3.iqiyipic.com/common/20171114/19.jpg)
对于value为非json类型的数据,处理起来就很麻烦了。如果业务上有其它能够用来索引key的方式,那么可以采用这样的业务上的数据来遍历所有key。否则,只有像前文所述,申请一个新的CB集群->热缓存->废弃旧的CB集群。
4.开发运维的注意事项
还有一些开发的注意事项,再此一并分享一下:
(1)incr方法,要用String;
(2)ttl超过一个月,需要设置为绝对时间;
(3)新业务请用新版本client,功能更强大,bug更少,低版本坑太多
运维注意事项:
(1)最好装一个ping工具监控worker机与CB集群之间的ping延时;
(2)订阅报警,将问题解决在萌芽状态;
(3)关注CB容量,CB使用内存要低于分配内存的75%,当bucket使用内存达到分配内存的75%时,由于内存不足,Couchbase会通过LRU算法将部分数据从内存中踢出,只存储在磁盘上,下一次读取这部分数据时,再从磁盘取出并加载到内存。从磁盘取数据会使Couchbase的读取性能降低。当bucket使用内存达到或接近分配内存的85%时,bucket可能会出现写不进数据的情况,同时集群读取性能受到较大影响。
十、总结
讲了这么多,最后总结一下对Couchbase缓存优化,其实一般就是指:
(1)提高缓存命中率:往往需要非常了解业务层面的用户行为(如点赞系统须了解用户如何刷feed)
(2)尽量减少OPS vs 一次获取或写入的数据量:取一个平衡
(3)优化缓存用量 vs 提高缓存命中率:取一个平衡
(4)业务复杂,空间不足:按照业务来拆分!
(5)节省空间:先考虑key的设计,再考虑使用protobuf等方式压缩数据(较麻烦)。