首页 > 代码库 > 大规模排行榜系统实践及挑战
大规模排行榜系统实践及挑战
版权声明:本文由唐聪原创文章,转载请注明出处:
文章原文链接:https://www.qcloud.com/community/article/154
来源:腾云阁 https://www.qcloud.com/community
排行榜满足了人的攀比、炫耀心理,几乎每个产品都会涉及。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如何取舍? 尽量保证各区域数据一致性,不丢失数据,高可用性(如其中一节点宕机不影响服务读),在出现网络分区时(如深圳、上海网络中断),集群少数成员一方(上海地区),能够降级提供只读模式。
常用的开源解决方案,以及各方案的优势、不足如下:
存储 | 原理 | 优势 | 不足 |
---|---|---|---|
Mysql | Mysql binlog | 稳定性、友好的sql接口、可根据业务场景需要配置复制模式 | 系统可用性不如etcd、zookeeper |
etcd | Raft | 强一致性、高可用、 | 较成熟,kubernetes等大型项目应用广泛,但是目前还处在快速发展中,团队内暂未在生产环境应用 |
zookeeper | Zab | 高可用、高性能读、团队已在生产环境应用多年、有配套的web配置系统 | 部署维护、C API使用等相比etcd不够友好 |
结合以上各解决方案优缺点,再根据我们的业务场景需求,我们选择了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资源隔离方案的优缺点。
优点:
-
各业务资源隔离,通过cgroup限制各业务使用的内存、CPU等资源,同时可以根据业务最终数据量、请求量在线动态调整资源大小。
-
简化部署,一个image镜像,任意一台存储机器,简单快速起多个redis容器,充分利用多核机器的资源,各业务相互隔离。
-
提高机器资源利用率。之前实体机一主一备模式,备机负载很低,容器化部署后,各容器主备可混合部署在一台机器上,调度时只需保证主备容器不在同一台机器。
缺点:
-
所有容器进程共享同一个内核空间,若某容器触发内核BUG,将导致内核崩溃,影响机器上所有容器进程,因此在生产环境实践上应吸收业界的最佳实践经验,主备容器数据热同步,定时备份数据,加强监控、容灾能力。
-
低版本的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实例等,更重要的是要支持实现排行榜级别的在线迁移。
大规模排行榜系统实践及挑战