1、导读linux
爱奇艺的社交业务“泡泡”,拥有日活用户6千万+,后台系统每日高峰期间接口QPS能够达到80K+,与视频业务的主要区别是泡泡业务更多地引入了与用户互动相关的数据,读、写的量均很大。不管是庞大的数据量,仍是相对较高的QPS,使得咱们在绝大多数场景下都依赖于高可靠、高性能、以及存储量巨大的在线缓存系统。本文介绍了一些咱们在使用缓存方面的经验以及优化的方法,但愿对你们有所帮助。nginx
2、Couchbase简介算法
Couchbase(下文简称CB)是由CouchOne(创办人包括CouchDB的设计者)和Membase(由memcached的主要开发人员创建)两家公司在2011年初合并而来。Membase公司有一个名为Membase的产品,它是个键/值、持久化、可伸缩的解决方案,使用了memcached wire协议和SqlLite嵌入式存储引擎。CouchOne支持的CouchDB是个文档数据库,提供了端到端的复制方法,这对于移动与分布在不一样位置的数据中心来讲是颇有用的。Couchbase是基于Membase与CouchDB开发的一款新产品,综合了二者的优势。数据库
3、Couchbase vs Redisjson
Couchbase和Redis均是很是优秀的缓存产品,爱奇艺泡泡社交后台根据不一样的适用场景大量地使用了它们,总的来说它们各有优劣,总结以下:后端
Redis优点:api
(1)Redis可以支持更多的数据结构;缓存
(2)Redis支持更多的应用场景(队列、发布订阅、排行榜);tomcat
(3)Redis不少操做能够放到服务端作,减小了网络io;服务器
(4)Redis社区更繁荣一点,文档比较丰富,解决问题更快捷;
(5)Redis支持事务。
Couchbase优点:
(1)CB能支撑更大的容量,Redis最好低于20G,不然failover以后恢复很是慢;
(2)CB有一个比较专业的管理界面;
(3)一致性hash发生在客户端,性能更高;
(4)某台server宕机不影响业务方使用,只是可用内存受影响,可用性更高;
(5)可扩展性强。
Redis 的亮点在于业务契合度高,CB 的亮点在于高可用。如下节选一些具体的功能点对比:
4、基础知识:CB中的vBucket
在 CB 中,咱们所操做的每个bucket会逻辑划分为1024个vBucket,其数据的储存是基于每一个vBucket储存,而且每一个 vBucket都会映射到相对应的服务器节点,这种储存结构的方式叫作集群映射。如图所示,当应用与Couchbase服务器交互时,会经过SDK与服务器数据进行交互,当应用操做某一个bucket的key值时,在SDK中会经过哈希的方式计算,使用公式crc32(key)%1024肯定key 值是属于1024个vBucket中的某个,而后根据vBucket所映射的节点服务器对数据进行操做。
为了保证分布式存储系统的高可靠性和高可用性,数据在系统中通常存储多个副本。当某个副本所在的存储节点出现故障时,分布式存储系统可以自动将服务切换到其它的副本,从而实现自动容错。
5、友善的Web console
CB自带了一套很是友善的Web console,使得运维及监控操做很是地方便。
6、案例1: 点赞系统缓存优化
首先介绍一下点赞系统业务场景:
点赞系统承载了两种业务的点赞,第一种叫作feed点赞:咱们管泡泡圈子承载的信息流叫作feed流,而其中的每一篇帖子叫作一条feed。为了便于理解,你们能够把微信朋友圈当作是个feed流,而其中的每一条状态帖子就是一条feed。这类业务系统的要求是,有feed流漏出的地方就会查询点赞系统,查询方式是一个uid(全站用户惟一id)+一组[feedid]列表;
第二种业务是评论业务的点赞,评论列表漏出的地方就会查询点赞系统,查询方式是一个uid+一组[commentid]列表(每条评论也有一个全局惟一id叫commentid)。
阶段一:Redis缓存
使用Redis作为缓存,缓存30G打满,经过LRU置换key。
点赞系统的第一个阶段,缓存结构中的key是实体id(即feedid或commentid),value是点赞过该实体的uid 集合。按照实体id查询全部给这个实体点过赞的uid,而且更新Redis缓存。采用这种设计的系统状况是成功率常常抖动,响应时间不稳定。
经过业务日志分析,发现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容量可能扛不住。
阶段三:key的优化
采用实体id+shard方式存储(shard=uid%factor ,实体id维度的变种)
缓存中key使用实体id+shard的方式,value是点过赞的用户列表。穿透到hbase后,根据实体id查询出全部给该实体id点过赞的uid,而后分shard更新缓存,没有数据的shard也要更新上标识。这种方案须要考虑shard个数,在空间和时间上取一个平衡。shard数越大,浪费的CB空间越多,可是每次查询结果集越小。
下面用一个例子来解释两种方案:实体id为e1,factor=20。
优化后的效果以下图,可见成功率相对趋于稳定,且缓存命中率已经高于了99%。
至此优化告一段落了,在这个案例中,有个事情是值得思考的:怎样才能得到更高的命中率呢?咱们的答案是谁重复的次数多,以谁为维度缓存。某一个用户可能浏览的feed个数是有限的几十个或者几百个,而某个feed能够被几百万甚至几千万的用户看到。很明显实体重复次数多,因此咱们是以实体id为维度(key)来存储。
7、案例2: 投票系统缓存优化
项目背景:投票计数器缓存结构,一个投票能够简单理解为vid(投票id)+多个oid(选项),须要以下缓存,用作计数器。
这次优化主要是针对某热门综艺活动暴露出来的问题。该活动的投票分多个渠道(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压力。
优化后效果很是明显,对CB的OPS大幅下降:
8、案例3: CB客户端SDK版本升级
项目背景:低版本SDK在CB集群rebalance完以后,须要重启,不然可能会有不少超时状况,此为第一个缘由。此外,某些后台系统发现新扩容的worker机常常抛出以下异常,而后自动重连,而且不断重复这个过程,致使接口成功率降低。对比了新老机器的nginx、tomcat、代码、jdk、能想到的linux系统参数、QPS、目标访问数据等等,都是彻底一致的,且夜间QPS低时候,该问题没有那么明显。在找不到具体触发该异常缘由的状况下,因为异常是在CB的SDK中抛出来的(看起来像是读取某个buffer时候越界),所以最直接的方法确定是尝试升级SDK。
最后咱们的方案是将老版本的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的脏数据,因此采用切换到临时集群的方式:
9、其它注意事项
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。
对于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可能会出现写不进数据的状况,同时集群读取性能受到较大影响。
10、总结
讲了这么多,最后总结一下对Couchbase缓存优化,其实通常就是指:
(1)提升缓存命中率:每每须要很是了解业务层面的用户行为(如点赞系统须了解用户如何刷feed)
(2)尽可能减小OPS vs 一次获取或写入的数据量:取一个平衡
(3)优化缓存用量 vs 提升缓存命中率:取一个平衡
(4)业务复杂,空间不足:按照业务来拆分!
(5)节省空间:先考虑key的设计,再考虑使用protobuf等方式压缩数据(较麻烦)。