腾讯亿级排行榜系统实践及挑战

阅读数:4883 2016 年 12 月 26 日 16:12

一. 背景

排行榜满足了人的攀比、炫耀心理,几乎每个产品都会涉及。SNG 增值产品部的 QQ 会员、QQ 动漫、企鹅电竞、游戏赛事等大量业务都对排行榜有强烈需求,特别是企鹅电竞等业务的发展壮大对我们排行榜系统提出了更多要求和挑战。在过去的一年中,排行榜系统从无到有,接入的业务从单一的 QQ 会员到企鹅电竞动漫等 20 几个各类业务,接入的排行榜数实现了从几个到数万的突破,单个排行榜用户数最大 9000 万, 排行榜存储集群活跃用户量数亿,而在这过程中,排行榜系统也遇到了以下挑战:

  • 如何支持业务就近接入?低延时?

  • 如何支撑数万乃至几十万级排行榜自动化申请调度?

  • 如何降低机器成本?选择合适存储引擎?

  • 如何避免各业务资源抢占,相互影响?

接下来的各小节会详细讨论目前我们在实践中是如何解决这些挑战的。

腾讯亿级排行榜系统实践及挑战

二. 排行榜系统基本架构

在讨论我们如何解决面对的挑战之前,先简单了解下排行榜系统基本架构、以及业务是如何接入、使用排行榜系统的。排行榜系统基本架构如图三所示,当前的排行榜系统架构不是设计出来的,而是根据我们业务场景、需求、发展等不断演化,不断优化而形成,其主要由以下几个服务组成:

  • 接入服务(无状态,提供访问修改排行榜数据的所有 RPC 接口给业务使用,如查询名次、topN 等)

  • 存储服务(有状态,主备部署,排行榜数据存储)

  • APIServer 服务(提供申请排行榜、业务接入、排行榜配置信息、存储机容量等接口)

  • 调度服务(从存储机中筛选满足业务申请条件的存储机,并择优分配)

  • Agent(上报存储机容量信息、存储服务监控数据等)

  • ZooKeeper(排行榜路由数据存储、存储机容量数据存储等,为什么我们选择 zookeeper 将在第三部分说明)

  • Mysql(业务接入信息、各排行榜配置、用户量、存储服务核心参数监控等存储)

业务接入排行榜系统时,首先会为每个业务分配一个 ID,其次需要申请排行榜 ID,业务排行榜 server 通过 l5 调用排行榜系统的接入服务,接入服务每个接口都包含业务 ID、排行榜 ID,接入服务通过业务 ID、排行榜 ID 查询 zookeeper 获取该业务排行榜 ID 的存储服务实例,最终通过存储服务 API 操作排行榜数据,返回结果给业务 Server。

上面提到的 L5 是什么呢? L5(Load Balancer,5 代指 Level5,即 99.999% 的可用性) 是一套兼具负载均衡和过载保护的容错系统, 业务服务接入 L5 时,会分配一个标识(modid、cmdid),此标识映射若干业务服务器 IP:PORT,业务机器需要部署 L5 agent, l5 最大的缺点是业务服务若要通过 L5 调用被调, 就必须修改代码,每次网络调用之前都需要通过 L5 agent api 获取被调 IP:PORT, 调用结束之后上报调用延时、返回码等。

腾讯亿级排行榜系统实践及挑战

三. 如何支持业务就近接入?低延时?

因部门产品业务较多, 服务部署区域也各异,有的业务部署在深圳地区、有的业务部署在上海地区。深圳、上海内网机房 ping 延时约 30ms, 这么高的延时是部分业务无法容忍的,作为平台支撑方,我们也希望提供的各接口平均延时应在 5ms 内,所以要尽量避免跨城访问。

如何避免跨城访问呢? 当然是各区域自治,早期因接入业务、排行榜数都较少, 只有接入服务、存储服务机器是按地区部署的,排行榜路由数据存储只部署在深圳,排行榜路由也只会在没命中 localcache 的情况下才会跨城查询深圳的路由数据存储集群,当时延时也能满足业务需求, 因此我们并未完全支持业务就近接入。

但是后面随着各类业务快速接入、排行榜的快速增加,特别是当 localcache 失效时,上海地区服务质量、延时波动较大,频繁告警,于是我们不得不推进各区域全面自治方案。

业务场景决定着我们选择什么存储方案来解决跨城访问导致的高延时问题。

简单分析下我们的业务场景 (业务核心链路请求):

  • 存储的是什么数据? 数据量多大?  路由、存储节点容量数据, 预计几十万条,key、value 长度较小((当然也可以采用表结构存储)

  • 读写比例? 读为主, 极少量写 (每分钟几个, 申请排行榜时才会添加路由配置)

  • CAP 如何取舍? 尽量保证各区域数据一致性, 不丢失数据,高可用性 (如其中一节点宕机不影响服务读),在出现网络分区时(如深圳、上海网络中断),集群少数成员一方 (上海地区),能够降级提供只读模式。

常用的开源解决方案,以及各方案的优势、不足如下:

腾讯亿级排行榜系统实践及挑战

结合以上各解决方案优缺点,再根据我们的业务场景需求,我们选择了zookeeper 作为核心的路由、存储节点容量数据存储,然后我们还面临着深圳、上海各部署一套还是只部署一套的选择。若深圳、上海各部署一套,我们的方案是深圳集群为主写,主写成功后,write opration log(create/set) 可以写入 MQ,MQ 需支持 at-least-once 语义, 由消费者异步写入上海集群,因为 create/set/del 都是幂等性接口,对于网络波动、中断等消费者写入上海集群失败的情况下,可以无限重试,确保两集群数据最终一致性。

但是鉴于我们业务场景极低的写请求,以及当出现网络分区时,zookeeper 上海地区集群可以开启 read only mode, 同时我们还有 zk proxy cache,local cache 等,我们最终选择了部署一套, 在生产环境中我们部署了 7 台 zookeeper 节点 (分布在深圳、上海 4 个 IDC),通过 zookeeper 本身的 zab 算法实现深圳、上海地区数据同步,各区域实现完全自治,整体部署方案见图四,部署之后业务调用延时见图五 (低于 2ms)。

腾讯亿级排行榜系统实践及挑战

腾讯亿级排行榜系统实践及挑战

四. 如何支撑数万乃至百万级排行榜申请?

和区域自治的低延时部署方案类似, 当前的调度系统也是排行榜的申请流程的不断优化衍变而来,排行榜的申请可分为以下三个时期:

  • 石器时代: 系统刚上线时,几个排行榜,手动配置。

  • 青铜时代: 几十个排行榜,通过 Web 界面人工审批排行榜。

  • 铁器时代: 数万个排行榜,通过调度服务筛选、打分选择最优的存储机,自动化容量规划,无需人工干预

如上所述,排行榜系统提供了两种申请排行榜方法,一种是在 web 管理平台提交申请单,一种是通过 API Server 提供的 API 实时申请排行榜,前者试用于排行榜数申请不频繁的业务场景,后者适用于需要大量排行榜的业务场景。

那么我们又是如何设计实现调度服务的呢? 通过分析我们的业务场景,业务在申请排行榜时一般需要填写部署地区(深圳、上海)、预计用户量、请求量、排行榜类型、是否容器部署、存储引擎类型等条件参数,因此排行榜调度服务的核心职责就是从有效的存储节点中筛选出满足业务申请条件的候选节点,并按某种策略对候选节点进行评分,选择最优分数节点分配。

基于以上业务场景,我们设计实现了调度服务,调度服务流程如图六所示,由两部分组成,筛选和打分。

筛选模块会根据配置运行一系列模块,比如健康检查模块、标签匹配模块、容量检查匹配模块。

健康检查模块会检查所有候选节点的存活性,存活性通过 zookeeper 的临时节点来判断,当一台机器挂掉时,临时节点会自动删除。

标签匹配模块非常灵活,提供了强大的筛选能力,比如筛选部署地区、实体机部署还是容器部署、存储引擎类型等等。

容量匹配模块检查候选节点是否容量满、剩余容量是否能支撑当前排行榜等。那么一个若干 G 内存的容器能支撑多少排行榜呢?

首先一个容器总容量能支撑多少用户量,我们可以根据线上数据,计算出一个经验值作为上限;其次,容量规划目前我们采用两种策略,一种是 hard limit,,适合业务能较精准预测排行榜的用户量,若业务指定这种资源分配, 就会实际预分配指定的用户量, 另外一种是 soft limit,业务无法预测排行榜用户量, 调度服务会根据此业务历史排行榜用户量计算一个平均值,若平均值低于该业务配置的最低用户量阀值,就预分配阀值,默认我们采用这种策略。

通过以上过滤模块筛选后的候选节点就会进入下一轮的评分模块,评分模块支持多种调度算法,如最小资源调度(选择内存资源剩余最多的节点)、多权重混合调度(根据用户申请参数,cpu、memory 赋予不同的权重),排行榜最终被调度到节点将是评分最高的节点。调度数据存储保存在 zookeeper, 各个存储机都部署了 agent, 定时上报存储机容量信息到 zookeeper 集群。

腾讯亿级排行榜系统实践及挑战

腾讯亿级排行榜系统实践及挑战

五. 如何降低机器成本?选择合适存储引擎?

选择什么样的存储引擎来存储排行榜数据? 我们分析下排行榜的基本操作,查询用户名次 / 分数,更新用户名次,查询前若干名,删除用户等,有些业务需要用到全部这些接口,有些业务只需要用到其中部分接口(比如更新用户分数、获取前若干名)。

我们可选的存储引擎有内存型存储 redis, 磁盘型存储 leveldb,rocksdb 等,redis 提供了多种丰富的数据结构,其中的 sorted sets(zset) 能完全满足我们各接口需求, zset 的核心数据结构是哈希表 + 跳跃表,其中哈希表保存各个用户的分数,跳跃表维护各个分数对应的排名,增加用户分数和查询用户排名的时间复杂度都是 LOG(N),因此适合对性能要求较高的业务使用。

而 leveldb、rocksdb 只提供了 key、value 型接口,为什么也可以在部分业务场景 (无需查询用户名次) 也可以使用呢? 图八是 leveldb 架构,从图中可知, leveldb 是 key 都是有序存储的,ssdb 就是通过将分数通过一定规则编码也作为一个 key 写入 leveldb 以支持 redis zset 数据结构,不过 ssdb 的查询排名时间复杂度是 O(N), 在生产环境中仅适合不查询用户排名的业务使用,但可以支持查询整个排行榜前 N 名(N 一般小于等于 200)。

腾讯亿级排行榜系统实践及挑战

因此降低成本第一个方法就是根据各业务特点、类型按需选择合适的存储引擎! 不需要查询名次的业务可以使用 ssdb(leveldb) 磁盘性存储引擎!

再看降低成本第二个方法,随着排行榜数越来越多,redis 排行榜存储机也逐渐增多,存储机器资源紧张,申请周期长,成本也较高,通过分析线上存量排行榜,发现部分业务具有明显的周期性,如各类业务的活动、赛事排行榜等上线推广时流量较大,活动结束时几乎无流量访问但是又不能清空整个排行榜,对于这类业务,排行榜系统提供了冷热分离机制,将冷数据从 redis 内存中迁移到 ssdb(leveldb) 的硬盘中,从而释放宝贵的内存资源,提高机器资源使用率,节省成本。

数据冷热分离方案如图九所示,通过 agent 采集现网所有排行榜流量,每日定时分析是否有排行榜满足迁移策略(如排行榜用户数大于 1 万,近两周流量小于某阀值等),若满足策略,就生成迁移任务,记录迁移的排行榜一系列元数据信息,写入 MQ。迁移服务轮询 MQ, 若发现有迁移任务, 就开始迁移工作,将全量排行榜数据备份到 SSDB(leveldb) 存储节点,缩容或清空现网 redis 排行榜数据,释放内存资源,图十是冷数据、热数据占比分析,冷数据占比高达 17%。

腾讯亿级排行榜系统实践及挑战

腾讯亿级排行榜系统实践及挑战

六. 如何避免各业务资源抢占,相互影响?

越来越多的业务接入同时,各业务排行榜之前资源争夺、相互影响成了一个不可忽视的问题,比如上海地区某业务排行榜申请了大量排行榜,触发上海地区存储机资源容量限制,所有业务申请上海地区的排行榜都失败。

为了解决各业务之间相互影响,排行榜系统实现了业务资源配额、资源隔离方案。排行榜资源隔离容器方案如图十一所示,容器 daemon 引擎使用了 docker, 网络模式使用了 host 模式,没有性能损失,简单可控,数据卷使用主机映射,容器重启、宕机数据都不丢失,后续也将测试使用分布式文件系统 ceph。

镜像存储驱动选择公司 tlinux2 操作系统自带的 aufs, aufs 不适合对容器内的文件进行频繁写操作,首次写需要 copy up 和多层 branch 检索开销,而我们的业务场景对容器镜像文件读写操作都极少,也将最大程度避免存储镜像的潜在的内核 BUG,各种镜像存储驱动优缺点对比见图十二。

registry 使用公司内部的通用仓库,docker daemon 使用内部 gaia 团队维护的 docker 1.9.1,使用内部维护版本的好处是经过了公司大量其他业务的应用,相比 docker 最新版本而言比较稳定,同时对于我们遇到的一些 BUG、需求功能,反馈给 gaia 团队之后,他们也会较快修复、合并新版本中我们需要的功能。比如我们之前遇到的两个 docker daemon bug,在内部版本中都较快解决或避免了。

https://github.com/docker/docker/pull/22932

https://github.com/docker/containerd/pull/265

腾讯亿级排行榜系统实践及挑战

腾讯亿级排行榜系统实践及挑战

图十二 存储镜像驱动优缺点(引用 docker 官方)

相比之前的混合部署,容器方案推出后,各业务接入时需要简单估算此业务未来排行榜总用户量数,预计需要的资源量,调度服务通过一定的策略从容器存储节点中,筛选一个最优的节点,动态创建一个若干 G 的容器分配给此业务使用,后续此业务下所有排行榜将调度到该容器,若容器资源不够,一方面可以在线提高容器资源大小,另一方面可以新增容器,一业务对应多容器。

最后总结下 Docker container 资源隔离方案的优缺点。

优点:

  1. 各业务资源隔离,通过 cgroup 限制各业务使用的内存、CPU 等资源,同时可以根据业务最终数据量、请求量在线动态调整资源大小。

  2. 简化部署,一个 image 镜像,任意一台存储机器,简单快速起多个 redis 容器,充分利用多核机器的资源,各业务相互隔离。

  3. 提高机器资源利用率。之前实体机一主一备模式,备机负载很低,容器化部署后,各容器主备可混合部署在一台机器上,调度时只需保证主备容器不在同一台机器。

缺点:

  1. 所有容器进程共享同一个内核空间, 若某容器触发内核 BUG,将导致内核崩溃,影响机器上所有容器进程,因此在生产环境实践上应吸收业界的最佳实践经验,主备容器数据热同步,定时备份数据,加强监控、容灾能力。

  2. 低版本的 docker daemon 退出后将导致所有容器进程死亡,docke1.12 版本增加了 --live-restore 参数,若指定此参数,docker daemon 关闭后容器进程将继续运行,https://github.com/docker/docker/pull/23213.

七. 总结

在解决以上问题的过程中,排行榜系统逐步实现了一套较高可用性、低延迟、低成本的排行榜解决方案(涵盖自动接入、调度、容灾、资源隔离、监控、扩缩容、数据冷热分离等),后续将加强系统自愈能力的建设,比如对于写流量不大的主备实例的全自动切换(对于写流量较大的实例因 redis 是异步复制,master_repl_offset 与 slave offset 存在几十万的差异),还有数据卷使用 ceph-rbd, 即便出现主备容器都挂,通过系统监控探测到后,可以动态创建新容器、挂载 ceph-rbd 数据卷,重建 redis 实例等,更重要的是要支持实现排行榜级别的在线迁移。

作者简介

唐聪,  2014 年毕业加入腾讯,SNG 增值产品部后台开发工程师,一直从事 QQ 会员后台基础系统、复杂业务系统研发,主要关注分布式系统、存储引擎、容器等技术。


感谢杜小芳对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

评论

发布
用户头像
zset如果数据量很大会不会在redis形成热点以及大key
2019 年 06 月 30 日 12:14
回复
没有更多了