随着 b 站业务的高速发展,对于缓存的依赖也变得越来越大,缓存集群的规模也跟随这业务的发展而告诉增长。随之也带来了一系列的问题。对于高速增长的缓存集群,如何高效地进行运维管理成为了一个迫在眉睫的问题。
对于缓存的管理,主要面临如下几个问题。
如何快速部署新集群:业务新接入集群时,如何在现有的缓存机器集群里面挑选合适的机器进行部署,目前的做法是人工查询机器的剩余内存容量,然后手动选择空闲的机器进行新集群的部署。随着机器规模的增大,这种方式显然没法满足需求,而且对于运维人员也是一种负担。
集群扩容:随着业务量请求量的增大,现有的缓存集群无论是从内存容量还是单节点 qps 上都已无法满足现在业务的需求,因此必须对集群进行扩容操作。如何挑选合适的机器部署新节点,对于 redis-cluster,部署新增节点的机器还必须满足 cluster 的规则,即不能超过半数节点部署在一台机器上。同样这一过程目前也是通过人肉解决。
缓存故障 : 服务在线上运行时,发生故障是在所难免的,当缓存节点发生故障时,如何快速自动拉起新节点, 服务如何自动感知到新节点的 ip 并自动完全切换。当机器故障宕机时,如何将故障机器的缓存实例快速迁移至新机器,并对业务生效。之前都还是手动创建新实例,更改配置文件,并寻找相关业务方进行发版使配置生效,整个故障处理的流程相当的冗长。
overlord 的诞生就是为了解决如上一些问题。
overlord是 bilibili 自研的一套缓存解决方案, 致力于提供轻量级高可用的缓存代理模式,以及自动高效的自动化缓存管理平台。overlord 主体分为两个部分,分别是缓存代理 overlord-proxy 以及缓存管理平台 overlord-platform, 本篇文章将主要对 overlord-platform 进行介绍。
overlord-platform 是自动化的缓存管理平台,提供了资源的统一接入管理。支持自动化快速创建部署集群,支持自动扩缩容以及快速故障转移。
platform 总体架构设计
platform 包括如下几个模块
同时 platform 依赖了一些第三方开源组件
资源调度
platform 使用 mesos 进行资源的管理,通过定制的 scheduler framework 对 mesos 提供的资源进行二次分发。我们使用chunk对资源进行分发调度,chunk 算法保证了资源的分布符合预定的约束条件。
chunk 算法
chunk 算法参考了饿了么的实现, 保证了机器资源的负载均衡以及集群的高可用。
针对 redis-cluster 集群,chunk 算法提出了以下几个约束条件:
对于非 cluster 模式的集群(memcache 或 redis singleton)由于不存在主从的概念,因此约束条件可以简化为 2 和 3。
同时为了实现上的方便,对 chunk 算法还有以下约定:
chunk go 实现
复制代码
func mapIntoHostRes(offers []ms.Offer, mem float64, cpu float64) (hosts []*hostRes) { // 具体实现 chunk.go/mapIntoHostRes } type hostRes struct { name string // 机器名 count int // 当前机器可分配的节点数 }
复制代码
func dpFillHostRes(chunks []*Chunk, disableHost map[string]struct{}, hrs []*hostRes, count int, scale int) (hosts []*hostRes) { for { // 寻找已部署该集群节点数最少的机器 i := findMinHrs(hrs, hosts, disableHost, all, scale) if left == 0 { return } hosts[i].count = scale left -= scale } }
复制代码
for { // 找到剩余资源最多的机器 name, count := maxHost(hrs) if count == 0 { break } m := hrmap[name] // 寻找与该机器 chunk 对最少的对端机器,组成一组 chunk llh := findMinLink(linkTable, m) if hrs[llh].count < 2 { linkTable[llh][m] linkTable[m][llh] continue } llHost := hrs[llh] links = append(links, link{Base: name, LinkTo: llHost.name}) linkTable[llh][m] linkTable[m][llh] hrs[m].count -= 2 hrs[llh].count -= 2 }
复制代码
func links2Chunks(links []link, portsMap map[string][]int) []*Chunk { // ... }
集群部署
万事俱备只欠东风,当环境资源准备完毕后,如何快速创建一套集群呢,从资源的分配,获取到集群的部署交付是怎样一个流程。同时这一部分还会介绍一种快速创建 redis cluster 的方法
资源获取
当 framework 收到集群创建任务的时候,首先要向 mesos 请求恢复资源 (call.Revive),确保 mesos 集群有足够的资源能够部署当前任务。收到 revice 后,mesos 会向 framework 回复当前平台可用的资源 offer,scheduler 根据当前 offer 判断是否满足集群部署的需求。
资源分布
当获取到的 offer 有足够的资源可以部署当前创建任务时,scheduler 根据算法规则chunk进行集群节点分布规划。节点分布规划包括两部分 每个主机上应该部署几个节点;cluster 的主从要如何分布。规划节点分布的同时,scheduler 会生成具体的节点创建计划,并下发到 executor 创建具体的节点实例。
集群一致性校验
redis cluster 启动后,为了保证集群的真实可用,以及集群的分布的确服务预先创建的 chunk 规则,我们还需要对进群进行一致性校验,确保所有节点已经通过 gossip 达成了最终一致,并且 cluster status 处于 ok 状态。
一种利用 nodes.conf 快速创建集群的办法
传统的 redis cluster 创建方式是各个节点以 cluster 的模式启动,节点启动完毕后,通过 redis-trib.rb 工具进行节点间的握手。redis-trib 通过向每一个节点发送 CLUSTER MEET 请求,来进行节点间的握手,节点握手成功后,通过 Gossip 协议进行集群间 nodes 节点信息的同步。但是,单节点数增长到一定程度的时候,通过 CLUSTER MEET 进行集群握手将会有巨大的效率问题,由于 O(n^2) 的复杂度,当节点数量进一步增长时,握手效率也会急速下降。
为了解决这一问题,我们模拟了集群恢复的流程,通过 mock nodes.conf 的方式提前创建好 nodes 文件。通过 mock nodes.conf 同样是创建 600 个主节点,使用新方法仅仅需要 10s 的时间,速度提升了百倍不止。
以下是一个 mock nodes.conf 的例子
复制代码
0000000000000000000000000000000000225157 172.22.33.199:31000@41000 master - 0 0 0 connected 0-4096 0000000000000000000000000000000000225158 172.22.33.199:31001@41001 myself,slave 0000000000000000000000000000000000225159 0 0 0 connected 0000000000000000000000000000000000225159 172.22.33.184:31000@41000 master - 0 0 0 connected 4097-8193 0000000000000000000000000000000000225160 172.22.33.184:31001@41001 slave 0000000000000000000000000000000000225157 0 0 0 connected 0000000000000000000000000000000000225161 172.22.33.192:31000@41000 master - 0 0 0 connected 8194-12290 0000000000000000000000000000000000225162 172.22.33.192:31001@41001 slave 0000000000000000000000000000000000225163 0 0 0 connected 0000000000000000000000000000000000225163 172.22.33.187:31000@41000 master - 0 0 0 connected 12291-16383 0000000000000000000000000000000000225164 172.22.33.187:31001@41001 slave 0000000000000000000000000000000000225161 0 0 0 connected vars currentEpoch 0 lastVoteEpoch 0
通过提前为每一个节点生成 runnid,并渲染 nodes.conf 模板,节点启动时,只需要根据 nodes.conf 进行集群的恢复,而无需进行漫长的 meet 握手,大大提升了集群创建的速度。
集群伸缩
随着业务的发展,集群资源往往无法满足业务增长的需求,因此对集群进行扩容操作在所难免。对于集群的扩容操作,我们使用了如下三种方式。
调整单节点容量
调整单节点容量分为两步:
修改节点数
对于大多数的线上业务场景,我们大多使用的是动态增加节点数进行集群的扩容。对于单节点 qps 过高的情况,增加节点数还能分担业务请求降低单节点负载。通过增加节点数进行扩容的缺点就是新增的节点会导致部分的缓存 miss,因此新增节点的时候需要控制新节点的加入速度,防止一次性增加大量节点导致了缓存的大量 miss。
对于 redis-cluster,则不存在新增节点导致缓存 miss 的问题,因为 redis-cluster 会自动进行 slot 的 rebalance 以及 key 的迁移。通过 cluster meet 将新节点加入集群,再通过集群管理工具进行 key 以及槽位的迁移即可实现 cluster 集群的平滑扩容。
大集群换小集群
对于非 cluster 集群,这是最推荐的一种扩容方式,通过新建一套容量更大的集群,并将旧集群的数据导入新集群来实现集群的扩容。使用这种方式进行扩容有如下几个优点:
需要注意的是,大集群换小集群的做法需要对集群数据进行迁移导入,对于 redis,我们使用数据迁移工具进行数据的同步迁移。但是对于 mc 并不存在数据迁移的工具。为了解决这种情况,我们参考mcrouter cold cache warm up的方式进行缓存的集群迁移,业务访问时,首先访问新增的 cold 集群,如果 miss 则访问旧的 warm 集群并将数据写会新的 cold 集群。通过这种方式可以避免对 db 的大量回源。
故障恢复
服务运行过程中,可能会出现各种异常,比如节点实例挂掉,机器宕机,mesos 服务异常等。为了保证服务的高可用以及故障的快速恢复容灾。我们针对各种异常情况进行了如下处理:
failover 策略
scheduler 检测到服务故障后会进行 failover 流程,整个 failover 流程分为三种情况
对于故障的恢复,采取了以下恢复策略:
三种恢复策略的优先级为 1>2>3, 每一种恢复策略都会累计故障重试次数,当故障重试次数超过 6 次后,则会认为该故障无法自动 failover,scheduler 会停止自动重试,需要由人工介入
scheduler 收到 task 失败的消息后,首先会尝试从 task 原先所在机器恢复 task,并将 task id 加 1,用于表示 task 历史失败的次数。如果从原机器恢复 task 成功并成功重启服务节点,则 failover 流程结束。
如果无法从原机器恢复节点,则 scheduler 会尝试寻找当前节点所在集群没有使用过的机器,并将故障节点部署在该机器上。如果从 mesos 获取到的 offer 所在机器都已经部署有该集群的节点,那么则进入下一步。
当无法找到该集群未部署任何节点的机器的时候,scheduler 会尝试从现在集群所在机器里寻找最合适的机器部署新节点,新机器的寻找必须依然遵循 chunk 算法约束。
总结
overlord-platfrom 是 b 站为了面对高速发展的业务对缓存的需求而自研的一套自动化的缓存管理平台,目前已经托管了 b 站内部缓存资源的运维管理,支持 redis,memcache 缓存集群的创建及动态伸缩。更多详细信息参见overlord。
转自:https://www.infoq.cn/article/Xq1kyp8_7Ke47Zkr4Ak1
Copyright © 2008-2022 秒下下载站
m.down10s.com .All Rights Reserved