微博缓存服务化的设计与实践

导读:高可用架构 8 月 20 日在深圳举办了『互联网架构:从 1 到 100』为主题的闭门私董会研讨及技术沙龙,本文是陈波分享的微博缓存服务的演进历程。

陈波,08 年加入新浪,参与 IM 系统的后端研发。09 年之后从事新浪微博的系统研发及架构工作,在海量数据存储、峰值访问、规模化缓存服务及开放平台等方面参与技术架构改进,当前主要负责微博平台的基础设施、中间件的研发及架构优化工作,经历新浪微博从起步到成为数亿用户的大型互联网系统的技术演进过程。

在所有介绍微博架构演进的使用场景都离不开缓存,今天上午腾讯分享的 CKV 也同样提到了缓存服务在腾讯社交产品的重要性。缓存的设计为什么重要,我们先介绍其使用场景。

1

微博的缓存业务场景

微博几乎所有的接口都是实时组装的,用户请求最终转化到资源后端可能会存在 1 – 2 个数量级的读放大,即一个用户请求可能需要获取几十上百个以上的资源数据进行动态组装。

比如大家刷微博的时候,会触发一个 friends_timeline 的接口请求,然后服务端会聚合并组装最新若干条(比如 15 条)微博给用户,那这个过程后端服务需要到资源层拿哪些数据来组装?

  • 首先是后端服务会从资源层去获取用户的关注列表;

  • 然后根据关注列表获取每一个被关注者的最新微博 ID 列表 以及 用户自己收到的微博 ID 列表(即 inbox);

  • 通过对这些 ID 列表进行聚合、排序和分页等处理后,拿到需要展现的微博 ID 列表;

  • 再根据这些 ID 获取对应的微博内容;

  • 如果是转发微博还要获取源微博的内容;

  • 然后需要获取用户设置的过滤词并进行过滤。

  • 此后还需要获取微博作者、包括源微博作者的 user 信息进行组装;

  • 还需要获取这个用户对这些微博是不是有收藏、是否赞,

  • 最后还需要获取这些微博的转发、评论、赞的计数等进行组装。

(点击图片可放大浏览)

从以上过程可以看到,用户的一个首页请求,最终后端 server 可能需要从资源层获取几百甚至几千个数据进行组装才能得到返回的数据。

微博线上业务的很多核心接口响应需要在毫秒级,可用性要求达到 4 个 9。因此,为了保证资源数据的获取性能和可用性,微博内部大量使用缓存,而且对缓存是重度依赖的,不少核心业务的单端口缓存访问 QPS 已经达到了百万级以上。

微博使用的缓存主要是 Memcache 和 Redis,因为 Memcache 的使用场景、容量更大,而且目前推的缓存服务化也是优先基于 Memcache,然后再扩展到 Redis、 ssdcache 等其他缓存,所以今天的缓存服务讨论也是以 Memcache 为存储标的来展开的。我们最早使用的缓存架构就是直接利用开源版本的 Memcache 运行在物理机上,我们称之为裸资源。

2

缓存的裸资源架构演进

首先看一下微博 Memcache 缓存的裸资源架构的演进过程。微博上线之初,我们就对核心业务数据进行分池、分端口的,把 size 接近的数据放在相同的池子里面。业务方通过 Hash 算法访问缓存池里的节点。同时,每个 IDC 部署使用独立的缓存资源,为了加速,业务前端也会在本地启用 local-Cache。

上线几个月之后,随着业务量和用户量的急聚增加,缓存节点数很快增加到数百个。这段时间,时常会因为网络异常、机器故障等,导致一些缓存节点不可用,从而导致缓存 miss。这些 miss 的请求最终会穿透到 DB 中。

如果某个时间点,核心业务的多个缓存节点不可用,大量请求穿透会给 DB 带来巨大的压力,极端情况会导致雪崩场景。于是我们引入 Main-HA 双层架构。

对后端的缓存访问时,会先访问 Main 层,如果 miss 继续访问 HA 层,从而在获得更高的命中率的同时,即便部分 Main 节点不可用,也可以保证缓存的命中率,并减少 DB 压力。

这一阶段我们对业务资源进一步的分拆,每一种核心数据都分拆到独立的端口。同时,根据不同的访问频率、容量进行缓存搭配部署,对 Memcache 资源的端口进行统一规划,确保缓存层的性能和可用性。同时我们发现,在各种海量业务数据的冲刷下,前端使用 local-Cache,命中率不高,性能提升不明显,所以我们把 local Cache 层去掉了。

随着业务访问量进一步增加,特别是一些突发事件爆发式的出现并传播, Main-HA 结构也出现了一些问题,主要是很多缓存节点的带宽被打满, Memcache 的 CPU 比较高, Memcache 响应变慢。

通过分析,我们发现主要是大量热数据的集中访问导致的服务过载,单个端口不能承载热数据的访问(比如明星发的微博所在的端口),于是我们引入了 L1 结构。

通过部署 3 – 4 组以上的小容量 L1 缓存,每个 L1 组等价存储热数据,来满足业务要求。

总结一下微博的缓存架构演进过程:

  1. 在直接使用裸缓存资源的过程中,我们通过 Main-HA 双层结构,消除了单点问题;

  2. 通过热数据的多 L1 副本,可以用较低的成本即可应对高峰、突发流量;

  3. L1s-M-H 三层缓存结构消除了缓存层出现的带宽和 CPU 过载的情况,使整个系统的读取性都、可用性得了很大的提高。

在以上 3 阶段的演进过程中,我们较好的解决了访问性能与访问峰值的压力,不过在服务的可管理性方面依然存在可管理空间。不同业务之间只有经验可以复用,在缓存的实现方面经常需要各种重复的劳动。我们需要把缓存的使用服务化才能把可管理性带到一个新的阶段。

3

缓存服务的设计与实践

直接使用裸缓存资源也存在一系列问题:

  1. 首先,随着业务的发展,微博缓存的访问量、容量都非常大。线上有数千个缓存节点,都需要在业务前端要去配置,导致缓存配置文件很大也很复杂。

  2. 同时,如果发生缓存节点扩容或切换,需要运维通知业务方,由业务方对配置做修改,然后进行业务重启,这个过程比较长,而且会影响服务的稳定性。

  3. 另外,微博平台主要采用 Java 语言开发,我们定制了 Java Memcache 缓存层来访问三层缓存结构,内置了不少访问策略。这时候,如果公司其他部门也想使用,但由于用的是其他开发语言如 PHP,就没法简单推广了。

  4. 最后,资源的可运维性也不足,基于 IP、端口运维复杂性比较高。比如一个线上机器宕机,在这个机器上部署了哪些端口、对应了哪些业务调用,没有简单直观的查询、管理入口。

于是我们开始考虑缓存的服务化,主要的过程及策略如下:

  1. 首先是对 Memcache 缓存引入了一个 proxy 层,基于 Twitter 的 twemproxy 进行改造。

  2. 引入 cluster,并内嵌了 Memcache Cluster 访问策略,包括三层的一些更新、读取,以及 miss 后的穿透、回写等。

  3. 我们通过单进程单端口来对多个业务进行访问,不同业务通过 namespace Prefix 进行区分。

  4. 在 Cache-proxy 也引入了 LRU,在某些业务场景减少热点数据的穿透。

通过 cacheProxy,简化了业务前端的配置,简化了开发,业务方只需要知道 cacheProxy 的 IP 和端口,即可实现对后端各种业务的多层缓存进行访问。

我们对缓存服务的服务治理也做了不少工作。

接入配置中心

首先,把 Cache 层接入了配置中心 configServer(内部叫 vintage)。实现了 Memcache 缓存、 cacheProxy 的动态注册和订阅,运维把 Memcache 资源的 IP 端口、 Memcache 访问的 hash 方式、分布式策略等也以配置的形式注册在配置中心, cacheProxy 启动后通过到配置中心订阅这些资源 IP 及访问方式,从而正确连接并访问后端 Memcache 缓存资源。而且 cacheProxy 在启动后,也动态的注册到配置中心, client 端即可到配置中心订阅这些 cacheProxy 列表,然后选择最佳的 cacheProxy 节点访问 Memcache 资源。同时,运维也可以在线管理 Memcache 资源,在网络中断、 Memcache 宕机,或业务需要进行扩容时,运维启动新的 Memcache 节点,同时通知配置中心修改资源配置,就可以使新资源快速生效,实现缓存资源管理的 API 化、脚本化。

监控体系

其次,把 cacheProxy、后端 Memcache 资源也纳入到了 Graphite 体系,通过 logtailer 工具将缓存的访问日志、内部状态推送到 Graphite 系统,用 dashboard 直接展现或者按需聚合后展现。

Web 化管理

同时,我们也开发了缓存层管理组件 clusterManager(内部也叫 captain),把之前的 API 化、脚本化管理进一步的升级为界面化管理。运维可以通过 clusterManager,界面化管理缓存的整个生命周期,包括业务缓存的申请、审核,缓存资源的变更、扩缩容、上下线等。

监控与告警

ClusterManager 同时对缓存资源、 cacheProxy 等进行状态探测及聚合分析,监控缓存资源的 SLA,必要时进行监控报警。

我们也准备将 clusterManager 整合公司内部的 jpool(编排发布系统)、 DSP(混合云管理平台) 等系统,实现了对 cacheProxy、 Memcache 节点的一键部署和升级。

开发工具

对于 client 端,我们基于 Motan(微博已开源的 RPC 框架)扩展了 Memcache 协议,使 client 的配置、获取服务列表、访问策略更加简洁。方便开发者实现面向服务编程,比如开发者在和运维确定好缓存的 SLA 之后,通过 spring 配置 ,即可访问 unread-feed 业务对应的 Memcache

资源,后续的扩容、节点切换等都不需要开发者介入,也不需要重启。

部署方式

对于 cacheProxy 的部署,目前有两种方式,一种是本地化部署,就是跟业务前端部署在一起的,在对 cacheProxy 构建 Docker 镜像后,然后利用 jpool 管理系统进行动态部署。另外一种是集中化部署,即 cacheProxy 在独立的机器上部署,由相同的业务数据获取方进行共享访问。

Cache 服务化后的业务处理流程如图。

首先运维通过 captain 把 Memcache 资源的相关配置注册到 configServer, cacheProxy 启动后通过 configServer 获取 Memcache 资源配置并预建连接; cacheProxy 在启动准备完毕后将自己也注册到 configServer,业务方 client 通过到 configServer 获取 cacheProxy 列表,并选择最佳的 cacheProxy 发送请求指令, cacheProxy 收到请求后,根据 namespace 选择缓存的 cluster,并按照配置中的 hash 及分布策略进行请求的路由、穿透、回写。 Captain 同时主动探测 cacheProxy、 Memcache 缓存资源,同时到 Graphite 获取历史数据进行展现和分析,发现异常后进行报警。

(点击图片可放大浏览)

在业务运行中,由于各个业务的访问量的不断变化、热点事件的应对,需要根据需要对缓存资源进行扩缩,有两种扩缩方式:集群内的扩缩 和 集群的增减。

对于集群内的扩缩,线上操作最多的是增减 L1 组或扩容 main 层。

对于 L1,通常直接进行上下线资源,并通过 captain 对配置中心的配置做变更即可生效。而 main 层扩缩有两种方式,一是通过 L1、 Main 的切换,即新的 main 层先做为 L1 上线,命中率达到要求后,再变更一次配置,去掉老的 main,使用新的 main 层 ; 另外一种方式是使用 main-elapse 策略,直接上线 main,把老的 main 改为 main-elapse, main 层 miss 后先访问 main_elapse 并回种, set 时对 main-elapse 做删除操作。

对于集群的增减,我们增加了一个新组件 updateServer,然后通过复制来实现,目前还在内部开发测试状态。为什么会有集群增减,因为微博的访问存在时间上的规律性,比如晚上 9 点到 0 点的高峰期、节假日、奥运等热点出现,流量可能会有 30-50% 以上 变化,原有集群可能撑不住这么大的量,我们可能需要新建一个前端 + 资源集群,来满足业务需要,这时可以提前 1-2 个小时在公有云部署资源服务并加热,供新集群的业务方使用,待峰值过去后,再做下线处理,在提供更好地服务的同时,也可以降低成本。

如何 updateServer 进行集群间复制?可以结合下面这张图来看。

缓存集群分为 master 集群、 slave 集群。 Master 集群的 cacheProxy 收到 client 的请求后,对于读请求直接访问 L1-m-h 三层结构,但对写请求会发往本地的 updateServer ; slave 集群的 cacheProxy 除了做 master 集群的相同的动作,还会同时将写请求路由到 master 集群的 updateServer。只要 cacheProxy 更新 master、 local 集群中任何一个 updateServer 成功则返回成功,否则返回失败。 updateServer 收到写请求,在路由到后端缓存资源的同时,会日志记录到 aof 文件, slave 集群的缓存即通过 updateServer 进行同步。为什么引入 updateServer 这个角色,主要是更好的应对前端本地部署,由于本地部署方式 cacheProxy 节点特别多,前端机器配置较差,更重要的原因前端 Docker 镜像随时可能会被下线清理,所以需要把写请求发送到独立部署的 updateServer 进行更新。而对于集中化部署, Cache proxy 和 updateServer 的角色也可以合二为一,变为一个进程。

(点击图片可放大浏览)

缓存服务化的推进,性能也是业务方考虑的一个重要因素。

  • 我们对原来的 pipeline 请求中的读取类请求,进行了请求合并,通过 merge req 机制提高性能;

  • 把单进程升级为多进程(这一块也在内部开发中);

  • 对于 LRU 我们升级为 LS4LRU,线上数据分析发现,相同容量及过期时间, LS4LRU 总体命中率能进一步提高 5% – 7%

LS4LRU 简介

这里对 LS4LRU 做个简单地介绍。首先介绍 S4LRU,它是分成四个子 LRU: LRU0-LRU3。 Key miss 或新写入一个 key 时,把这个 key 放在第一层 LRU0,如果后来被命中则移到 LRU1 ;如果在 LRU1 又一次被命中则移到 LRU2,依此类推,一直升级到 LU3。如果它四次以上命中,就会一直把它放在 LU3。如果发现 LU3 的数据量太多需要 evict,我们先把待 evict 的 key 降级到 LU2 上,如此类推。同时每个 kv 有过期时间,如果发现它过期就清理。

而 LS4LRU 是在 S4LRU 的基础上增加一个分级的过期时间,每个 KV 有两个过期时间 exp1 和 exp2。比如说某业务, exp1 是一秒, xep2 是三秒, LS4LRU 被命中的时候,如果发现它是在一秒内的数据,则直接反给客户端的,如果是在 1 秒到 3 秒的时候,则会首先返回到客户端,然后再从异步获取最新的数据并更新。如果是 3 秒以上的,就直接去清理,走 key miss 流程。

服务化的总结

我们再看一下服务化的其他一些方面的实践总结。

  • 对于容灾, Memcache 部分节点故障,我们有多级 Cache 解决;

  • 对于 proxy/Memcache 较多节点异常,我们通过重新部署新节点,并通过 captain 在线通知配置中心,进而使新节点快速生效;

  • 对于配置中心的故障,可以访问端的 snapshot 机制,利用之前的 snapshot 信息来访问 Cache proxy 或后端缓存资源。

  • 对于运维,我们可以通过 Graphite、 captain,实现标准化运维;对于节点故障、扩缩容按标准流程进行界面操作即可。运维在处理资源变更时,不再依赖开发修改配置和业务重启,可以直接在后端部署及服务注册。对于是否可以在故障时直接部署并进行配置变更,实现自动化运维,这个我们也还在探索中。

历年的演进经验可以看到,缓存服务化的道路还是很长,未来还需要进一步的对各 Cache 组件进行打磨和升级,我们也会在这条路上不断前行。大家对于缓存的设计有各种建议的,欢迎在文后留言进行探讨。

Q&A;

提问: L1 和 main 是如何协作的,什么时候可以把数据升级到了 L1,什么时候淘汰?为什么要使用这样的机制, L1 和 main 的访问速度应该差不了很多吧?为什么要另外再加一个热点数据放在 L1 里面? L1 跟 main 怎么做数据同步?

陈波:首先 L1 的容量比 Main 小很多,同时 L1 会有很多组,线上核心也有一般在 4-6 组以上,每组 L1 的数据基本上是热数据。如果部署了 L1,所有的写请求、 L1 的读 miss 后的回写,都会把数据写入 L1,淘汰方式是 L1 组在容量满了之后由 Memcache 自动剔除。

对于为什么需要 L1,因为对于微博业务来说,它是一个冷热非常明显的业务场景,一般来讲,新发的微博请求量大,之前发的微博请求量小,另外在峰值期请求量会特别大,在高峰访问期间、节假日时,核心业务单端口的访问 QPS 会有百万级,这时单层或双层 main-ha 结构的 Memcache 缓存性能上无法满足要求,主要表现就是带宽被打满、 CPU 过高、请求耗时增加。另外在突发事件爆发时,比如最近的宝宝事件,如果对部分热 key 有数十万级以上的并发访问,再加上其他不同 key 的请求,双层缓存结构是完全无法满足性能要求的,缓存节点过载,读取性能下降,超时会???量出现。因此我们增加 L1 层,通过多个 L1 组,把这些热数据分散到不到 L1 组来访问,从而避免 Cache 层过载。这样 L1 层就分担了 Main 层对热数据的大部分访问,一些温热的数据访问才会落到 Main 和 slave 层,为了保持 main 层数据的热度,实际线上运行中,我们也会把 main 层作为一个 L1 组来分担部分热数据的访问,只是这种情况下, key miss 后会直接访问 slave。

数据同步是通过多写和穿透回写的方式进行。在更新数据的时候,直接对所有的 L1、 Main、 slave 层进行更新,从而保证各层的数据是最新的。另外,进行数据读取的时候,存在 L1-main-ha、 DB 四层的穿透回写机制,如果前面读取的缓存层 miss 了,后面缓存层、 DB 层命中了,然后就可以进行原路回写,从而对前面的缓存层都写入相同的 kv。

提问:什么样的数据放在 L1 里面?

陈波:最热的数据存在 L1 中,它通过 Memcache 层的淘汰机制进行的。因为 L1 容量比 main 小很多,最热的数据、访问频率最高的数据基本都在 L1 里面,而稍冷的数据会很快的从 L1 里面踢走。所以直观上,你可以认为最热的、当前访问量最大数据就在 L1 层。比如说可以认为姚晨、宝强的最新数据都在 L1 层,我们普通用户的数据大多靠 Main 层命中。

提问:你们线上 Redis 的内存碎片情况如何?

陈波:我们去年和前年对部分业务的 Redis 有做过分析,一般有效内存负荷在 85% 到 90% 以上,也就是碎片率小于 1.1-1.2,很多是 1.0x,有些跑了半年或者一年以上的部分实例可能会稍微高一点。

提问: Redis 碎片率过高的话你们是怎么来优化的?

陈波:如果发现碎片率比较高,比如 master,我们会切换一个新 maste,然后把老的 maste 进行下线,然后通过重启解决,也可以通过我们的热升级机制解决。

未经允许不得转载:氢网 » 微博缓存服务化的设计与实践

支付宝扫码打赏 微信打赏

欢迎点击上方按钮对我打赏

分享到:更多 ()

评论 抢沙发

评论前必须登录!