bilibili 缓存解决方案(一) overlord-platform介绍

bilibili 缓存解决方案(一) overlord-platform介绍

随着b站业务的高速发展,对于缓存的依赖也变得越来越大,缓存集群的规模也跟随这业务的发展而告诉增长。随之也带来了一系列的问题。对于高速增长的缓存集群,如何高效地进行运维管理成为了一个迫在眉睫的问题。

对于缓存的管理,主要面临如下几个问题:

如何快速部署新集群:业务新接入集群时,如何在现有的缓存机器集群里面挑选合适的机器进行部署,目前的做法是人工查询机器的剩余内存容量,然后手动选择空闲的机器进行新集群的部署。随着机器规模的增大,这种方式显然没法满足需求,而且对于运维人员也是一种负担。

集群扩容:随着业务量请求量的增大,现有的缓存集群无论是从内存容量还是单节点qps上都已无法满足现在业务的需求,因此必须对集群进行扩容操作。如何挑选合适的机器部署新节点,对于redis-cluster,部署新增节点的机器还必须满足cluster的规则,即不能超过半数节点部署在一台机器上。同样这一过程目前也是通过人肉解决。

缓存故障 : 服务在线上运行时,发生故障是在所难免的,当缓存节点发生故障时,如何快速自动拉起新节点, 服务如何自动感知到新节点的ip并自动完全切换。当机器故障宕机时,如何将故障机器的缓存实例快速迁移至新机器,并对业务生效。之前都还是手动创建新实例,更改配置文件,并寻找相关业务方进行发版使配置生效,整个故障处理的流程相当的冗长。

overlord的诞生就是为了解决如上一些问题。

overlord是bilibili自研的一套缓存解决方案,致力于提供轻量级高可用的缓存代理模式,以及自动高效的自动化缓存管理平台。overlord主体分为两个部分,分别是缓存代理overlord-proxy以及缓存管理平台overlord-platform,本篇文章将主要对overlord-platform进行介绍。

overlord-platform是自动化的缓存管理平台,提供了资源的统一接入管理。支持自动化快速创建部署集群,支持自动扩缩容以及快速故障转移。

image.png

platform总体架构设计

image.png

platform包括如下几个模块

  • scheduler: 资源调度模块,即mesos framework,负责资源的分配获取以及调度任务的下发
  • executor:资源部署模块,负责拉起缓存节点以及缓存实例的健康检查
  • api-server: 管理后台模块,通过后台dashboard进行资源的监控管理以及任务的下发

同时platform依赖了一些第三方开源组件

  • etcd:存储节点元信息以及部署任务信息,api-server将部署任务写入etcd,scheduler监听etcd拿到部署任务信息执行集群创建部署计划,并将最终集群分布信息写入etcd
  • mesos:机器资源管理框架,每一台缓存机器部署一个mesos-agent,并将机器资源信息上报给mesos-master,mesos-master将机器资源信息通过offer下发给scheduler,由scheduler进行二次调度部署集群实例。

资源调度

platform使用mesos进行资源的管理,通过定制的scheduler framework对mesos提供的资源进行二次分发。我们使用chunk对资源进行分发调度,chunk算法保证了资源的分布符合预定的约束条件。

chunk算法

chunk算法参考了饿了么的实现,保证了机器资源的负载均衡以及集群的高可用。

针对redis-cluster集群,chunk算法提出了以下几个约束条件:

  1. 主从不在同一个物理节点
  2. 任意一个物理节点分配的节点数少于总数的一半
  3. 尽可能部署在资源最充足的物理节点

对于非cluster模式的集群(memcache或redis singleton)由于不存在主从的概念,因此约束条件可以简化为2和3。

同时为了实现上的方便,对chunk算法还有以下约定:

  1. master数量必须为偶数
  2. 可用机器数必须不小于三
  3. 机器节点数不能为3且master数量不能为4

chunk go实现

  • 把offer根据节点mem cpu的需求转换成对应的可用资源单元
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

    }
}
  • 根据hostRes寻找chunk组
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
    }
  • 把chunk组转化为最终的主从分布关系
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握手,大大提升了集群创建的速度。

集群伸缩

随着业务的发展,集群资源往往无法满足业务增长的需求,因此对集群进行扩容操作在所难免。对于集群的扩容操作,我们使用了如下三种方式。

  1. 调整单节点容量
  2. 修改节点数
  3. 大集群换集群

调整单节点容量

调整单节点容量分为两步:

  1. 首先需要修改从mesos获取的资源offer,保证资源的充足可用。

    首先需要判断当前机器是否还有剩余足够的资源满足扩容,通过mesos 获取当前机器的offer判断剩余资源是否充足,并重新accept更大的offer。

  2. 修改缓存服务的最大内存限制。因为我们的缓存服务同时使用了redis和memcache,因此修改缓存的容量限制又的细化为修改memcache限制和redis限制:

    修改memcache内存限制: 修改memcache的容量需要对memcache进行重启,并通过设置--memory-limit来指定最大使用的内存

    修改redis内存限制: 可以通过修改配置文件的maxmemory并重启服务,也可以通过redis client下发 config set maxmemory $bytes来进行修改而无需重启服务

修改节点数

对于大多数的线上业务场景,我们大多使用的是动态增加节点数进行集群的扩容。对于单节点qps过高的情况,增加节点数还能分担业务请求降低单节点负载。通过增加节点数进行扩容的缺点就是新增的节点会导致部分的缓存miss,因此新增节点的时候需要控制新节点的加入速度,防止一次性增加大量节点导致了缓存的大量miss。

对于redis-cluster,则不存在新增节点导致缓存miss的问题,因为redis-cluster会自动进行slot的rebalance以及key的迁移。通过cluster meet 将新节点加入集群,再通过集群管理工具进行key以及槽位的迁移即可实现cluster集群的平滑扩容。

大集群换小集群

对于非cluster集群,这是最推荐的一种扩容方式,通过新建一套容量更大的集群,并将旧集群的数据导入新集群来实现集群的扩容。使用这种方式进行扩容有如下几个优点:

  1. 对比增删节点,不会出现缓存的miss回源
  2. 对比第一种,无需对节点进行重启,造成节点访问的短暂失效。
  3. 不存在历史数据的增删操作,新建的集群内存碎片更小。

需要注意的是,大集群换小集群的做法需要对集群数据进行迁移导入,对于redis,我们使用数据迁移工具进行数据的同步迁移。但是对于mc并不存在数据迁移的工具。为了解决这种情况,我们参考mcrouter cold cache warm up的方式进行缓存的集群迁移,业务访问时,首先访问新增的cold集群,如果miss则访问旧的warm集群并将数据写会新的cold集群。通过这种方式可以避免对db的大量回源。

故障恢复

服务运行过程中,可能会出现各种异常,比如节点实例挂掉,机器宕机,mesos服务异常等。为了保证服务的高可用以及故障的快速恢复容灾。我们针对各种异常情况进行了如下处理:

  • 节点本身故障

    服务实例启动的后,executor会启动一个单独线程对服务进行health check,当health check连续多次出现失败的时候,executor会认为当前服务不可用,并向shceduler发送task fail消息。scheduler收到task fail消息后及进入failover流程。

  • 服务实例所在机器故障或网络故障导致机器失联
    如果是机器故障(如机器宕机)导致机器上所有的服务节点都退出,framewrok会收到agent fail事件,进入failover流程。如果只是mesos-agent故障,由于开启了checkpoint,只要及时恢复对服务无影响,如果是网络分区导致的失联,mesos-master会在agent_ping_timeout时间后把agent以及agent所在机器上的所有task设置为lost,并进入fail_over。

failover策略

scheduler检测到服务故障后会进行failover流程,整个failover流程分为三种情况

对于故障的恢复,采取了以下恢复策略:

  1. 原地重启
  2. 寻找新机器恢复
  3. 原有集群机器里恢复

三种恢复策略的优先级为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

你可能感兴趣的:(bilibili 缓存解决方案(一) overlord-platform介绍)