13.深入分布式缓存:从原理到实践 --- 缓存在社交网络Feed系统中的架构实践

1.Feed 系统架构
	类似微博的信息条目在技术上也称为status或feed,其技术的核心主要包含3个方面:
	1.feed的聚合和分发,用户打开feed首页后能看到关注的人feed信息列表,同时用户发表的feed需要分发给粉丝或者指定用户
	2.feed信息的组装和展现
	3.用户关系的管理,即用户及其关注/粉丝关系的管理

	对于中小型的feed系统,feed数据可以通过同步push模式进行分发。用户的每一条feed,后端系统根据用户的粉丝列表进行全量推送,粉丝用户
  通过自己的inbox来查看最新的feed。

    随着业务规模的增长,用户的平均粉丝不断增加,特别是大v用户的粉丝大幅度增长,信息延迟就会时有发生,单纯的push无法满足性能要求。同时
  考虑社交网络中多种接入源,移动端,PC端,第三方都需要接入业务系统。于是就需要对原有架构进行规模化,平台化改进,把底层存储建为基础服务,
  然后基于基础服务构建业务服务平台。对数据存储进行了多维度拆分,并大量使用cache进行性能加速,同时将同步push模式改成了异步hybrid模式,
  即push+pull模式。用户发表feed后首先写入消息队列,由队列处理机进行异步更新,更新时不再push到所有粉丝的inbox,而是存放到发表者自己的
  outbox;用户查看时,通过push模式对关注人的outbox进行实时聚合获取。基于性能方面的考虑,部分个性化数据仍然先push到目标用户的inbox。
  用户访问时,系统将用户自己的inbox和TA所有的关注人outbox一起进行聚合,最终得到feed列表。(即自己的inbox + 他人的outbox 聚合)

  	其对外主要以移动客户端,web主站,开放平台三种方式提供服务,并通过平台接入层访问feed平台体系。其中平台服务层把各种业务进行模块化拆分,
  把诸如feed计算,feed内容,关系,用户,评论等分解为独立的服务模块,对每个模块实现服务化架构,通过标准化协议进行统一访问。中间层通过各种
  服务组件来构建统一的标准化服务体系,如motan提供统一的rpc远程访问,configService提供统一的服务发布,订阅,cacheService 提供通用的
  缓存访问,SLA体系,Trace体系,TouchStore体系提供系统通用的健康监测,跟踪,测试及分析等。存储层主要通过MySQL,HBase,Redis,分布式
  文件等对业务数据提供落地存储服务。
    为了满足海量用户的实时请求,大型feed系统一般都有着严格的SLA,如微博业务中核心接口可用性要达到99.99%,响应时间在10~40ms以内,对应到后端
  资源,核心单个业务的数据访问高达百万级QPS,数据的平均获取时间在5ms以内。因此在整个feed系统中,需要对缓存体系进行良好的架构并不断的改进。

2.Feed 缓存模型
	feed系统的核心数据主要包括:用户/关系,feed id/content 以及包含计数在内的各种feed状态。feed系统处理用户的各种操作的过程,实际是一个以
  核心数据为基础的实时获取并计算更新的过程。
  	以刷微博首页为例,处理用户的一个请求操作,主要包括关注关系的获取,feed id的聚合,feed内容的聚合三部分,最终转换到资源后端是一个获取各种关系,
  feed,状态等资源数据并进行聚合组装的过程。
    这个过程中,一个前端请求会触发一次对核心接口 friends_timeline 的请求,到资源后端可能会存在 1-2+ 个数量级的请求数据放大,即feed系统收到一个
  此类请求后,可能需要到资源层获取几十数百甚至上千的资源数据,并聚合组装出最新若干条(如15条)微博给用户。整个feed流构建过程中,feed系统主要进行了如下
  操作:
  	1.根据用户uid获取关注列表
  	2.根据关注列表获取每一个被关注着的最新微博id列表
  	3.获取用户自己收到的微博id列表(即inbox)
  	4.对这些id进行合并,排序以及分页处理后,拿到需要展示的微博id列表
  	5.根据这些id获取对应的微博内容
  	6.对于转发feed进一步获取源feed的内容
  	7.获取用户设置的过滤条件进行过滤
  	8.获取feed/源 feed 作者的user信息并进行组装
  	9.获取请求者对这些feed是否收藏,是否赞等进行组装
  	10.获取这些feed的转发,评论,赞等技术进行组装
  	11.组装完毕,转发成标准格式返回给请求方

  	feed请求需要获取并组装如此多的后端资源数据,同时考虑用户体验,接口请求耗时要在100ms(微博业务要求40ms)以下,因此feed系统需要大量的使用缓存,并对
  缓存体系进行良好的架构。缓存体系在feed系统中占有重要的位置,可以说缓存设计决定了一个feed系统的优劣。
  	一个典型的feed系统的缓存设计主要分为 inbox,outbox,social graph,content,existence,content 共6个部分。
  	feed id 存放在 inbox cache 和 outbox cache 中,存放的格式是 vector(有序数组).
  	其中 inbox 缓存层用户存放聚合效率低的 feed id(类似于微博的directed feed)。当用户发表只展现给特定粉丝,特定成员组织的feed时,feed系统首先会拿到
  待推送(push)的用户列表,然后将这个feed id推送(push)给对应的粉丝的inbox。因此inbox是以访问者uid来构建key的,其更新方式是先gets到本地,变更后再cas到
  异地的memcached缓存。
    outbox 缓存层用于直接缓存用户发表的普通类型的feed id,这个cache以发表者uid来构建key。其中outbox又主要分为vector cache 和 archive data cache;
  vector cache 用于缓存最新发表的feed id,content id 等,按具体的业务类型分池存放。如果用户最近没有发表新feed,vector cache为空,就要获取archive data
  的feed id。
    social graph 缓存层主要包括用户的关注关系以及用户的user信息。用户的关注关系主要包括用户的关注(following)列表,粉丝(follower)列表,双向列表等。
    content 缓存层主要包括热门feed的content,全量feed的content。热门feed是指热点事件爆发时,引发热点事件的源feed。由于热门feed被访问的频率远大于普通feed,
  比如微博中单条热门feed的qps可能达到数十万的级别,所以热门feed需要独立缓存,并缓存多份,以提高缓存的访问性能。
    existence 缓存层主要用于缓存各种存在性判断的业务,诸如是否已赞(liked),是否已阅读(readed)这类需求。
    counter缓存用于缓存各种计数。feed系统中计数众多,如用户的feed发表数,关注数,粉丝数,单条feed的评论数,转发数,阅读数,话题相关的计数等。
    feed系统中的缓存一般可以直接采用memcache,redis,pika等开源组件,必须时根据业务需要进行缓存组件的定制自研。新浪微博也是如此,在feed平台内,memcache使用最广泛。
  占用60%以上的内容容量和访问量,redis,pika主要用户缓存social graph 相关数据,自研类组件主要用于计数,存在性判断等业务。结合feed cache架构,inbox,outbox,content,
  主要用memcache来缓存数据,social graph 根据场景同时采用memcache,redis,pika作为缓存组件,existence采用自研的缓存组件Phantom,counter采用自研的计数服务组件
  CounterService。

3.Feed 缓存架构的设计
	上面提到feed平台缓存模型有6个主要层次,各个缓存层的数据类型,缓存格式各异,而且即便在同一层缓存层内,不同业务的数据类型,size,命中率也会不同。因此在业务数据进行
  缓存架构设计时,首先需要根据业务需求,数据结构确定缓存访问模型,然后确定缓存组件的选型,最后还要根据缓存数据的size,命中率,sps等进一步进行缓存架构的细化调整。

    1.简单数据类型的缓存设计
    	一般系统中的大部分数据都是简单的kv数据类型,feed系统也是如此,比如feed content,user信息等。这些简单的数据只需要get,set操作,不会用于特殊的计算操作,最适合以
      memcached作为缓存组件。
        基于memcached的海量数据,大并发访问场景,不能简单将多种数据进行混存,而需要首先进行容量评估,分析业务待缓存数据的平均size,数量,峰值读写qps等,同时结合业务特性,
      如过期时间,命中率,cache穿透后的加载时间等,最终确定memcached的容量,分布策略。比如缓存数据的size分布会影响memecache的slab分布,size差异大的不同数据不能混存,
      另外对于读写qps,命中率等要求比较高的业务需要独立部署。
        如微博feed系统上线业务缓存时,会使用如下指标进行容量规划,通过分析业务数据的size,cache数量,峰值读写等,来确定memcache的容量大小,节点数,部署方式等。
        1.业务名称
        2.用途
        3.单位(user)
        4.平均size
        5.数量
        6.峰值读(qps)
        7.峰值写(qps)
        8.命中率
        9.过期时间
        10.平均穿透加载时间
        系统上线之初,需要把业务数据按容量评估属性进行分类。属性接近的数据尽量分配在相同的端口的memcache内存池,此阶段不同的业务数据如果缓存属性接近可以缓存在相同的内存池。
      但缓存属性差异比较大的不同类型数据就要尽早使用独立端口的内存池了。memcache内存池一般可以设4~6个节点,通过取模或一致性hash进行分布式存储。考虑到运维性,相同的内存池
      的节点实例在内存大小,端口,启动参数可以设置为完全相同。
        随着业务量和用户量的不断快速增加,基于缓存数据的安全性,访问性能等的考虑,系统初期混部的核心业务缓存数据需要进行拆分及统一规划。微博feed系统内部也是如此,这一阶段
      很多业务数据拆分到了独立缓存,memcache总的缓存实例数很快增加到了数百个,一些业务缓存由于机架紧张甚至同时部署多个IDC。数据总量及访问量增大,缓存数据独立拆分,机器节点
      大量增加,如果再涉及到多IDC访问,就可能时常会遇到机器故障/宕机,网络异常等情况,一些缓存节点不可用,从而导致缓存访问miss,这些miss请求最终会穿透到db中。
        对后端数据的缓存访问,会首先访问Main层,如果miss继续访问HA层,如果HA层命中,则返回client结果后,再将value回写到Main层,后续对相同的key的访问可以直接在Main层命中。
      由于Main-HA两层cache中数据不尽相同,通过Main-HA结构,业务可以获得更高的命中率。同时,即便出现部分Main节点不可用,也可以通过HA层保证缓存的命中率,可用性。因为主要压力
      在main层,为了降低机器成本,feed系统一般将其他相邻IDC的Main层互作对方的HA层。
      	同时还可以进一步采取其他措施,来提升memcache缓存层的可用性以及运维性:
      	1.memcache内存池提前设置足够的分片数,并采用取模hash分布,在部分节点故障时,立即采用新的节点替换异常节点,避免数据飘逸引入脏数据。
      	2.根据不同的访问频率,容量,对memcache实例进行搭配部署,提高机器的使用率
      	3.对memcache资源进行统一的监控,并提供各种维度的查询和预警
      	随着业务量进一步增加,特别是在峰值期间,社交网络中的突发事件出现爆发式的传播,Main-HA结构也会出现问题,主要是部分缓存节点的带宽被打满,cpu/负荷过载,导致memcache响应
      被严重变慢。经过深入分析,我们发现问题的主因是大量热数据的集中访问,导致缓存服务节点过载,单个节点不能承载热数据的访问量(比如明星发的微博所在的节点),于是进一步引入L1结构。
        新的memcache,新增部署3组以上的小容量L1缓存,每组L1缓存的容量为Main层的 1/3 ~ 1/6,构成L1-Main-HA 三层架构。client访问时,首先随机选取一个L1缓冲池进行访问,如果
      miss则再按 Main->HA 的顺序依次访问,如果中途命中数据,则在返回结果后原路径进行数据回写。新数据的写入缓存时,在写Main,HA内存池的同时,也会写所有的L1缓冲池。由于L1的内存
      容量远远小于Main,稍冷的数据会迅速剔除,所有L1中会持续存储最热的数据,同时由于L1有多组,大量热数据访问会平均分散到多个L1。通过L1层的加入,memcache缓冲层节点的负荷,带宽
      消耗得到有效控制,响应性能得到明显提升。

    2.集合类数据的缓存设计
    	对于需要进行计算的集合类数据,如feed系统中用户关系中的关注列表,分组,双向关注,最新粉丝列表等,需要进行分页获取,关系计算,从而满足诸如'共同关注','我的关注人里谁也
      关注了TA'等。如果直接使用memcache作为简单的二进制value进行缓存,任何计算类请求需要获取全量数据在本地进行,即一个微小的变更也需要全量获取并更新回写,由于计算量大,列表
      数量大,变更频繁,缓存服务性能都存在严重挑战。
      	redis提供了丰富的集合类存储结构,如list,set,zset,hash,并提供了丰富的api接口用于服务端计算,可以更好的满足上述业务需求。微博feed系统内部也广泛的使用redis,当前有
      数千个redis实例,存储了千亿条记录,每天提供万亿级的读写操作。
        feed系统使用redis时,可以采用典型的master-slave方式进行部署。每个业务数据提前分拆到多个hash节点(如8,16个),每个hash节点使用独立的端口,每个hash节点有一个master 
      和多个slave。监控系统实时监控master,slave的状态,在必要时进行主从切换。master,多个slave都采用域名方式对外暴露服务,这样master,slave变更后,只更新域名服务器即可。
      因为根据域名访问多个slave可能存在请求不均匀的现象,同时主从切换后client需要能够快速感知,所有需要在client端实现负载均衡和主从切换后的ip感知,微博采用clientBalancer
      组件来实现。
      	redis的数据访问基本都落在内存,缓存数据会以aof,rdb落在磁盘上,供重启的数据恢复或者主从复制使用,因此单个redis实例不能分配过大的内存空间,否则会因为重启,rewrite
      时间特别长而影响服务的可用性。对于普通硬盘而言,redis加载处理1G数据大概需要1分钟,所以线上redis单个实例内存最好不要超过20G,避免redis重启加载数据的耗时过长。同时,
      因为redis属于单进程/线程模型,为了提高机器的效率,还可以根据cpu核数进行单机多redis实例部署,具体实例数最高可以为N(cpu)-1。
        纯粹使用redis的master-slave方式在一些场景下没法很好的满足业务需求。
        首先是耗时特别大的复杂关系计算业务,如'我关注的人里谁也关注了TA'的业务,需要基于用户关注列表,多次请求计算哪些关注人也关注了目标UID,整个处理过程可能长达数时ms,不仅本次
      请求处理耗时较长,还会延迟其他请求。即便增加更多的slave,对整个计算过程的性能提升还是非常有限。于是我们把计算的中间结果缓存到memcached,同时对活跃的用户进行预先计算,从而
      大幅度提升请求性能。
      	其次在大集合数据进行全量读取的场景,如用hgetAll获取整个关注列表,单词请求可能需要返回数百上千个元素,但单进程/线程模型的redis,这个响应结果的拼装及发送是一个重量级操作,
      如果要保证峰值期间的可用性,需要增加更多套的redis slave,成本开销比较大。此时也可以在redis前面加一层memcached,全列表读取采用memcached的get来抗读,memcached miss 后,
      才读取redis或db层并回写,由于memcached的多线程实现方式对纯粹二进制kv的高效读取,可以用较少的memcached内存换取全列表获取性能的大幅提升。
      	因此,通过在redis前端部署memcached,来缓存中间计算结果,全量集合数据,可以很好的提升系统的读取性能,还可以减少slave数量来降低整体内存占用。
      	除了用memcached作前置缓存,还可以在调用端增加local-cache,进一步提升获取效率。在使用多种组合cache时,多种cache存在缓存穿透,回写策略,这些策略比较通用,可以在client
      端进行抽象封装,使业务开发者使用起来像单个cache一样,从而提高开发效率。
      	另外对于容量巨大,冷热分区比较明显的集合类数据业务,还可以用Pika来替代redis作为缓存组件。pika可以将极热数据存到内存,其他数据存放在磁盘,单个接口可以存放百G级别的数据,
      同时兼容redis协议,业务方可以无感知的从redis访问切换到pika访问。

    3.其他类型数据的缓存设计
    	在海量数据的缓存访问模型中,feed系统还有一些业务数据,无法直接当做简单数据类型或集合类数据类型来缓存,也就无法直接使用传统的cache组件(如memcache,redis),因为会存在巨大
      的成本挑战。
      	首先是存在性判断业务,判断一个用户是否赞了某条feed,是否阅读了某条feed等。如果直接缓存,存储容量几乎是0(用户数*Feed数),即使只存储最近几天的数据,也要耗费巨大的内存。
      微博的feed系统最初利用redis来缓存最近3天的'是否已赞'记录,结果发现即使做了极度的存储优化,也要数T的内存空间,而且db层有大量的缓存穿透访问。
      	其次还有计数业务,存储一个key为8字节,value为4字节的计数,redis需要耗费65+字节,内存有效负荷小于(8+4)/65=18.5%。feed系统存在海量计数,总内存消耗巨大,如按照传统redis
      缓存方案,微博计数每日新增十亿条记录,每日新占用内存高达数百G,这在成本考量上很难接受的。
      	对此,需要进行定制化缓存组件开发。微博开发了用于存在性判断的cache组件Phantom,内存占用降为原有的10%~20%,读写性能基本不变;开发了用于计数的cache组件CounterService,
      内存占用降为原有的10%以下,同时通过ssd的引入,单机支撑容量进一步增大1个数量级。

4.Feed 缓存的扩展
	通用缓存组件在社交网络系统中应用广泛,在feed系统的性能,可用性保障中也占用重要地位。但直接使用通用cache组件,在运维性,成本控制等方面仍然有各种痛点,于是进行了大量的优化,
  扩展,甚至全新定制化开发。

  	1.redis的扩展
  		在redis 2.8 版本之前,几乎每次slave 连接master都会导致一次全量复制。在一些特殊场景下,如网络异常断开,redis升级重启,短时间多个主从同步中断并重连,如果影响的slave
  	  数量较大,就会导致网络流量暴增甚至被打满,从而导致同步初期网络内所有的服务不可用。同时slave同步到全量rdb数据后,在加载过程中也无法对外提供访问(一个10G的redis实例,加载
  	  rdb过程会阻塞10+分钟)。自2.8版本之后,redis通过psync实现了增量复制,一定程度上缓解了主从连接断开后会引发全量复制的问题,但是这种机制仍然受限于复制积压缓冲区大小,同时
  	  在主库故障需要执行切换的场景下,主从仍然需要进行全量复制。
  	  	于是微博feed首先调整了redis的持久化机制,将全量数据有机的保存在rdb和aof中;然后基于新的持久化机制对同步方式也做了全面调整,实现了完全增量复制。
  	  	持久化全量数据的过程如下,通过bgsave构建rdb并落地,同时将当前aof文件及position也记录在rdb中,新数据写入aof,aof按照固定size不断滚动存储,这样rdb和此后的所有aof文件
  	  构成了一份全量的redis数据记录。为了避免滚动的aof占用磁盘空间过大的问题,可以在构建新的rdb完毕后,在保留一定余量的基础上将rdb记录之前的aof文件进行清理(比如最近两天的aof)。
  	  由于redis是单机多实例部署,微博feed同时通过定时持久化配置cronsave,将单机部署的多个redis实例分散在不同时间点进行错峰持久化。	
  	  	slave 第一次请求复制时向master发出sync指令,master将rdb传给slave,同时找到rdb记录的aof及position,将其之后的所有aof文件数据传给slave,即可实现全量同步。后续持续读取
  	  最新的aof文件数据并传给slave,即可实现实时同步。当因为任何原因发生中断并再次重连时,slave只需通过syncfrom要告诉master自己复制位置对应的aof文件以及position,master即可
  	  找到对应位置并将之后的aof记录持续发给slave,即可完成增量同步。
  	  	微博的关注关系最初保存在redis的 hash 结构中,在redis cache 发生miss后,重建关注关系的hash结构是一个重量级操作。对关注较多的用户,一次重建过程需要数十毫秒,这对单进程
  	  的redis是无法接受的;同时redis的hash结构内存效率并不高,为了保证命中率需要的cache容量仍然比较大,于是微博feed扩展了longset数据结构。通过一个'固定长度开放寻址的hash数组'
  	  数据结构,在大大降低内存占用的同时,常规读写性能几乎相同。对于miss后的数据重建,可以通过client端构建longset二进制结构一次性写入,实现O(1)的时间复杂度。
  	  	微博feed还对redis做了很多其他方面的扩展,比如热升级等。各种新功能扩展及版本发布后,会产生运维问题,因为每次升级需要重启,而重启过程需要十分钟以上的服务中断,这对线上业务
  	  是无法忍受的。于是微博feed将redis的核心处理逻辑封装到lib.so文件,缓存数据保存在全局变量中,通过调用lib.so中的函数来实现操作缓存数据,实现热升级功能。redis版本升级时,
  	  只需要替换芯的lib.so文件,无需重新加载数据,实现毫秒级的升级,升级过程基本对客户端请求无任何影响。

  	2.计数器的扩展
  		feed 内部有大量的计数场景,如用户维度的有关注数,粉丝数,feed发表数,feed维度有转发数,评论数,赞数以及阅读数等。前面提到,按照传统的redis,memcached技术缓存方案,
  	  单单存每日新增的十亿级的计数,就需要新占用数百G的内存,成本开销巨大。因此开发了技术服务组件CounterService。
  	  	对于计数业务,典型的构建模型有2种:
  	  		1.db+cache模式,全量计数存在db,热数据通过cache加速;
  	  		2.全量存在redis种。
  	  	方案1成熟,,但对于一致性要求高的技术服务,以及在海量数据和高并发的场景下,支持度不够友好,运维成本和硬件成本较高,微博上线之初曾使用该方案,在redis面世后很快用新方案
  	  代替了。方案2基于redis的计数接口 incr,decr,能很方便的实现通用的计数缓存模型,再通过hash分表,master-slave部署方式,可以实现一个中小规模的计数服务。
  	  	但在面对千亿级的历史海量计数以及每天十亿的新增计数,直接使用redis的计数模型存在严重的成本和性能问题。首先redis技术作为通用的全内存计数模型,内存效率并不高。存储一个
  	  key 为8字节(long型id),value为4字节的计数,redis至少需要耗费65字节。1000亿计数需要 100G*65=65T以上的内存,算上一个master配备3个slave的开销,总共需要26T以上的内存,
  	  按单机96G计算,扣掉redis其他内存管理开销,系统占用,需要300~400台机器。如果算上多机房,需要的机器会更多。其次redis计数模型的获取性能不高。一条微博至少需要3个计数查询,
  	  单次feed请求如果包含15条微博,仅仅微博计数就需要45个计数查询。
  	  	在feed系统的计数场景,单条feed的各种计数都有相同的key(即微博id),把这些计数存储在一起,就能节省大量的key的存储空间,让1000亿计数变成330亿条记录。近一半的微博没有转发,
  	  评论,赞,抛弃db+cache的方案,改用全量存储的方案,对于计数为0的微博不再存储,如果查询不到就返回0,这样330亿条记录只需要存160亿条记录。然后又对存储结构做了进一步的优化,
  	  三个计数和key一起一共只需要8+4*3=20字节。总共只需要16G*20=320G,算上1主3从,总共也就需要1.28T,只需要15台机器即可。同时进一步通过CounterService 增加ssd扩展支持,
  	  按照table滚动,老数据落在ssd,新数据,热数据在内存,1.28T的容量几乎可以用单台机器来承载(当然考虑到访问性能,可用性还是需要hash到多个缓存节点)。
  	  	计数器组件的架构如下,主要特性:
  	  		1.内存优化:通过预先分配的内存数组Table存储计数,并且采用double hash 解决冲突,避免redis实现中的大量指针开销
  	  		2.Schema支持多列:一个feed id对应的多个计数可以作为一条计数记录,还支持动态增减计数列,每列的计数内存使用精简到bit
  	  		3.冷热数据分离,根据时间维度,近期的热数据放在内存,之前的冷数据放在磁盘,降低机器成本
  	  		4.LRU缓存,之前的冷数据如果被频繁访问则放到LUR缓存进行加速
  	  		5.异步IO线程访问冷数据,冷数据的加载不影响服务的整体性能
  	  	通过上述扩展,内存占有率降为之前的5%~10%以下,同时一条feed的评论/赞多个计数,一个用户的粉丝/关注/微博等多个计数都可以一次性获取,读取性能大幅提升,基本彻底解决了
  	  计数业务的成本及性能问题。

  	3.存在性判断的扩展
  		feed系统中有不少'存在性判断'业务,比如判断用户对某条feed是否已'赞',是否'收藏',是否'阅读'等。
  		由于越新的feed访问量越大,所以最初直接考虑使用redis或者CounterService来存储这些记录,上线后发现记录数增长太快,仅仅每日新增的阅读记录数就高达百亿级别,redis
  	  的存储结构根本无法支撑,即便使用CounterService存储,每天新增的数据也需要占用数百G内存,机器成本开销太大。
  	  	对于存在性判断业务,直接记录的机器成本太大,如果采用 Bloomfilter算法,在业务能容忍一定程度误判的前提下,可以大幅降低内存占用。
  	  	BloomFilter利用bit数组来表示一个集合(如阅读了某条微博的所有用户),可以快速判断一个元素是否属于这个集合。写入之前,BloomFilter 是每一位都是0的bit数组,加入元素
  	  时,采用k个互相独立的hash函数计算,将元素分别映射到k个位置设置为1。BloomFilter 将x1,x2用3个hash函数映射到bit数组。判断元素是否在集合中存在时,只需要对目标元素做k
  	  次hash,如果每次hash计算的bit位都是1,则认为目标元素是集合中的元素,否则就不是集合中的元素。
  	  	这种做法存在一定的误判率,因为可能存在一个元素虽然不在集合中,但k次hash都被命中的情况。BloomFilter 占用内存空间降低,且误判率可控,平均每条记录占用1.2字节时仅有
  	  1%的误判率,而且这个误判率还可以根据调整记录占用的内存空间进一步降低。
  	  	于是微博feed基于BloomFilter算法开发了Phantom。
  	  	主线程采用循环队列实现缓存过期策略,通过将所有的key排序并划分区间,依次将所有的key存放在不同的table中并滚动,数值越大的key放在越新的table中。过期时根据配置将最老
  	  的表一次性删除或落地,再将该表的内存空间初始化为最新的table进行新数据的存储。Phantom可以很好的满足feed业务场景,因为feed id也是随着时间增长,且越新的feed访问量越大。
  	    Phantom落地也采用 rdb+aof 的模式,执行落地操作时,将循环队列中的所有table写入rdb文件,并在rdb文件中记录当前aof文件及位置,后续的数据恢复可以直接加载rdb和aof即可。
  	  基于这种落地方式,也可以方便的支持完全增量复制。同时,Phantom 采用System V 共享内存方案,将数据table放在共享内存,进程升级,重启甚至crash 都不会丢失内存的数据。
  	  	为了方便业务使用,Phantom 采用了redis协议格式,目前主要支持bfset,bfget,bfmset,mfmget4组命令,这样可以通过 redis client 做简单的新指令扩展,即可访问Phantom服务。
  	  	通过使用Phantom组件,读写性能基本不变,内存占用却降为原有的10%~20%,可以很好的支持feed系统的判断性业务。

5.缓存的服务化	
	前面介绍的多级memcached缓存结构,混合memcached+redis结构,以及扩展的计数器,Phantom组件等,可以较好的解决访问性与访问峰值的压力,大大降低内存占用。不过在缓存
  的运维性,可管理性方面依然存在不足。不同业务之间只有经验,缓存组件可以复用,在缓存的可用性,运维性方面进程需要各种重复性劳动。
  	首先随着业务的发展,feed缓存的访问量,容量都会很大。线上有成千上万个缓存节点,都需要在业务前端去配置,导致缓存配置文件很大也很复杂。同时如果发生缓存节点扩容或切换,
  需要运维通知业务方,由于业务方对配置做修改,再进行业务重启上线,这个过程比较长,而且会影响服务的稳定性。
  	其次,系统开发一般会主要选择一种语言,如微博的feed平台主要采用java开发,我们基于java语言定制了缓冲层client来访问各种缓冲结构,缓冲组件,内置了不少访问策略。这时候
  如果公司的其他部门用的是其他语言如PHP,就没办法简单的推广了。
  	最后,资源的可运维性也不足,基于ip,端口运维复杂性比较高。比如一个线上机器宕机,在这个机器上部署了哪些端口,对应了哪些业务调用,没有简单直观的查询,管理入口。
  	于是就需要开始考虑缓存的服务化,主要的方案如下:
  	1.引入一个proxy层,用于接受并路由业务对资源的请求,通过cacheProxy支持多种协议(memcached,redis等),多种业务访问,不同业务通过namespace Prefix 进行分区。
  	2.引入一个updateServer层,用于处理写请求和数据同步
  	3.读写及数据复制策略,cacheProxy 收到资源请求后,对read垒请求直接路由到后端资源,对write类请求路由到本地idc的updateServer;updateServer首先更新本地资源,
  	成功后再路由到master idc 的updteServer。master idc 的updateServer接受所有的write请求,记录到aof,并逐级复制到其他idc的updateServer,实现idc之间的cache同步。
  	4.引入cluster,并内嵌了memcached cluster,redis cluster 等访问策略,包括多层的更新,读取,以及miss后的穿透,回写等。
  	5.接入配置中心,可以方便的支持api化,脚本管理资源和proxy
  	6.接入监控体系,方便查看缓存体系的服务状态
  	7.web话管理,通过web界面管理缓存的整个生命周期
  	8.其他服务化策略
  	通过上述方案及策略,可以大大简化业务前端的配置,简化开发,运维。业务方只需要知晓namespace,即可实现对后端各种业务的多层缓存进行访问。

  	1.接入配置中心
  		将缓存资源层,proxy层,updateServer层接入配置中心configServer(微博内部叫vintage),实现缓存,cacheProxy,updateServer 的动态注册与订阅。运维把cache
  	  资源/updateServer的ip端口,hash算法,分布式策略等也以配置的形式注册在配置中心,cacheProxy,updateServer启动后通过到配置中心订阅这些资源的ip及访问方式,从而
  	  实现正确连接并访问后端缓存资源。通过cacheProxy在启动后,也把自身动态的注册到配置中心;client端接口到配置中心订阅这些cacheProxy列表,然后选择最佳的cacheProxy
  	  节点访问各种缓冲资源。运维也可以在线管理缓存资源,在网络中断,机器宕机,或业务需要进行扩容时,只需要启动新的缓存节点,并通过脚本调用api通知配置中心修改资源配置,
  	  就可以使新资源快速生效,从而实现缓存资源管理api化,脚本化。

  	2.IDC数据复制
  		通过updateServer 实现不同的idc间的cache数据复制。不同的idc的updateServer数量相同,aof文件也相同;updateServer的slave节点记录同步的aof文件名及位置,在
  	  连接断开重连后,通过aof文件名及位置确定复制位置,从而实现完全增量复制。
  	  	利用公有云部署服务应对日常峰值或突发峰值流量时,公有云上的cache资源不再需要持续部署及更新,只需要提前一个小时左右部署并完成数据同步,即可对公有云业务提供服务,
  	  从而较好的降低服务成本。

  	3.web化管理	
  		通过缓存管理组件cluseterManager(内部叫captain),把之前api化,脚本化管理进一步升级为界面管理。

  	4.监控与警告
  		把cacheProxy,updateServer,后端各种缓冲资源纳入了Graphite体系,通过日志工具将缓存的访问日志,内部状态推送到Graphite系统,用
  	  dashboard直接展示或者按需聚合后展示。
  	    通过clusterManager对缓存资源,cacheProxy 等进行实时状态探测及聚合分析,结合Graphite 的历史数据,监控缓存资源的SLA,必须时进行
  	  监控报警。
  	  	在微博feed系统内部,clusterManeger后续继续整合jpool(编排发布系统),DSP(混合云管理平台)等系统,实现了对cacheProxy,updateServer,
  	  各种缓冲资源的一键部署和升级。

  	5.开发工具
  		对于client端,可以基于分布式服务框架(如微博的motan)扩展memcached,redis协议,使client与后端资源解耦,后端资源变更不会导致业务系统重启,
  	  同时获得了服务列表,调整访问策略也更加方便。方便开发者面向服务编程。

  	6.部署方式
  		由于updateServer需要将write类请求落地aof,并承担复制任务,所以只能选择独立的机器部署。而对于updateServer的部署,目前有2种方式:一种是
  	  本地化部署,就是跟着业务前端部署在一起的,在对cacheProxy构建docker镜像后,利用jpool管理系统进行动态部署。另外一种就是集中部署,即cacheProxy
  	  在独立的机器上部署,由不同的业务方进行共享访问。

  	7.处理流程与总结
  		首先运维通过clusterManager 把缓存资源的相关配置注册到configServer。cacheProxy/updateServer 启动后通过configServer获取资源配置并预建
  	  连接;cacheProxy 在启动准备完毕后将自己也注册到configServer,业务方client通过configServer获取cacheProxy列表,并选择最佳的cacheProxy发送
  	  请求指令;cacheProxy收到请求后,对于write类请求直接路由到updateServer,对于read类请求则直接根据namespase选择缓存的cluster,并按照配置中的
  	  hash及分布策略进行read请求的路由,穿透,回写。updateServer根据namespace选择cluster 完成写请求及数据同步。clusterManager 同时主动探测cacheProx,
  	  updateServer,缓存资源等,同时到Graphite 获取历史数据进行展现和分析,发现异常后进行报警。

  	  总结实践:
  	  	1.对于部分缓存节点故障,memcache可以通过多级cache解决,redis CounterService,Phantom 等通过master-slave 切换,多slave解决
  	  	2.对于较多缓存/cacheProxy 节点异常,我们通过重新部署新节点来替换异常节点,并通过captain在线通知配置中心,进而使得新节点快速生效解决。
  	  	3.对于updateServer节点异常,cacheProxy 会将请求通过一致性hash均匀路由到其他节点处理
  	  	4.对于配置中心的故障,可以通过访问端的snapshot机制,利用之前的snapshot信息来访问cacheProxy或后端缓存资源
  	  	5.对于运维,可以通过Graphite,clusterManager 实现标准化运维,节点故障,扩容按照标准流程界面操作即可。在运维处理资源变更时,不再依赖开发修改配置
  	  	和业务重启,可以直接在后端部署及服务注册。对于是否可以通过系统自动判断故障,并由系统直接部署资源/组件,变更配置,实现自动化运维,还在探索中。

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值