很久很久以前,有一个 PE 叫川小生,有一个开发叫子小嘉。双 11 前,他们按照业务的要求给天猫准备了 14 倍余量,给主搜准备了 1 倍余量。结果 11 号上午流量涨势喜人啊,嗖嗖往上涨。川小生和子小嘉说不对啊,怎么主搜涨这么厉害天猫只涨 4 倍呢,川小生掐指一算,干,到晚上主搜就挂了啊。俩人怂了,把天猫机器迁一点给主搜吧。于是改 clustermap 下机器,发 binary,发依赖数据,发全量,追增量,起进程,改 clustermap 加机器,一通折腾一个半小时过去,总算有惊无险。满心期待到了晚上,主搜流量却木有来,有木有,木有来啊。。。
上面是一个发生某年双 11 的一个真实的关于搬机器的故事, 好像每年的双 11 总会发生点超出我们预期的事情,而每次系统变动又总是让人胆战心惊。2014 年的双 11 我们内心变得很平静,这些事情再也不需要咱自己去干了,因为咱有了自动化的解决方案——来自引擎调度系统小组的 Hippo。
一、来自一线的诉求
这些诉求是我们与相关同学长时间深入沟通拿到的(真是很难感觉到幸福啊),转换到对我们的调度系统的需求:集群集中管理、资源调度、应用托管、高效自动部署/变更、应用安全、应用隔离。
二、为什么造轮子
笔者最早接触的是 Condor/HTCondor,搞过网格计算的同学应该比较了解;Goolge 的 Borg 应该算是一开始借鉴了很多 Condor 的东西,Omega 则是在解决 borg 的单 master 调度的瓶颈问题;Tencent 的 Tborg/Torca 则是和 Borg 系统有很深的渊源;Yarn 和 Mesos 应该是被更多的人所熟知,都支持多种计算框架;对 AutoPilot 的认知更多来自于相关的论文;Baidu 的系统其实蛮有意思,特别是 IDLE(有个组件可以随意种植在任何机器上,当机器空闲的时候则调度一些低优先级并且可以随时K掉的计算任务上去执行,而且他们的 PE 人员身背机器利用率的 KPI,大家都求着调度任务上去,这和咱们的现状完全是两样);FUXI 和 T4 是集团内的系统,大家想要了解可以在内网找到他们。
搜索中心的在线系统基本都是基于 C++ 的,跑在 Yarn/JAVA 上不免显得有点重了,所以 Yarn 其实在意开始就被我们淘汰掉了,但我们重点研究了它的协议。曾经一度离我们最近的是 FUXI 和 Mesos,再看看我们在线服务系统的基础要求:
我们要的不仅仅是一个资源管理统一调度系统,而是支撑整个在线服务系统的一揽子工程。回到资源管理上来,FUXI 和 Mesos 在资源回收协议上都是强制回收的,异常情况下会影响服务的完整性,这是不能接受的。对于二进制包/依赖数据的分发 FUXI 和 Mesos 都是集中式的拉取对于在线的多 replica 效率没法满足需求。另外考虑到服务迁移的成本,我们希望调度框架是非植入的(目前 FUXI 在不继承框架的时候只能进行简单的进程起停,不能监控服务进程的健康),能够做到应用的无痛迁移。针对这些问题,我们与 FUXI 的同学做过深入的交流,对于我们的这需求暂时还不能满足;Mesos 来讲,评估过后发现如果我们去改造 Mesos 的成本并不比全新开发来得低,至于 Docker 流的 Kubernetes 以及集成到 Mesos 那已经是后话了,Hippo 启动的时候 Docker 还没那么火。最终我们的 Hippo 定位于专注在线服务管理及支撑,并把资源管理管理做薄,一旦 FUXI 成熟我们可以将整个 Hippo 系统跑在去,顺带也将上面的服务平滑迁移到云上(是的,上了 Hippo 咱们服务就提前上云了),我们的目标是尽量不要重复造轮子。
三、Hippo 系统架构
Hippo 实现采用两层系统架构:一层(Master)负责资源管理以及核心调度器调度,二层(AM)为具体应用调度。各应用可定制开发或者使用默认调度器调度。
Hippo 系统采用典型的 master-slave 中心节点架构,包含两个角色:
四、Hippo 核心特性
Hippo 作为服务器与在线服务应用的中间层,其核心的功能则是向下抽象管理资源、向上抽象在线服务需求并保证两者有效安全的衔接。这一节将按照这个思路来简单介绍一下 Hippo 的核心特性。
4. 1 资源管理
4. 1.1 资源
资源是对 Hippo 中 Slave 这些工作节点服务能力的一个抽象。SCALAR 数值型(用于描述 CPU 核数/MEM 大小/DISK 大小/网络带宽等可计量资源)、TEXT 文本型(用于描述不可计量资源,比如描述磁盘是否是 SSD)。其中 CPU/MEM 作为系统内置资源由 Hippo 自动探测,其余资源由外部设置。Hippo 借鉴 FUXI 对资源的描述支持任意的虚拟资源,特别的是 Hippo 引入了一个“EXCLUDE_TEXT ”排他资源类型用于对资源可申请对象的约束。Hippo 支持动态修改 Slave 的自定义资源描述。
4. 1.2 SLOT(槽)/机器分槽
Hippo 以 SLOT 为粒度进行资源的管理分配,一个 SLOT 代表了一组资源组合。主要分为两类:普通槽,主要分给普通应用使用,每个槽对有一个槽号(>=0 的整数)标识;系统槽,主要给 Hippo 自身内置服务组件使用,与普通槽的差别是它不占用逻辑资源,槽号为<0 的整数。
一台机器(slave)被加入 Hippo 的时候可以申明一组资源并需指定槽数 slot_count,Hippo 将 slave 划分成 slot_count 个普通槽,这些槽均分 slave 上申明的数值型资源并继承文本型资源(内置自动探测资源属于数值型,会将 Slave 自动探测汇报的资源均分到每个槽),并自动分配 system_slot_count 个系统槽(系统槽个数在 Hippo 启动时指定)。举个例子,在一个配置了一个系统槽的 Hippo 系统中一个 32 核 64G 的机器分两个槽的情形:
Hippo 目前只支持这种静态分槽(资源切分)的方式,动态分槽的需求目前还不是很明确,后续会根据需求引入。
4. 1.3 资源的申请/分配/回收
应用对机器的需求全部抽象成资源需求,Hippo 支持多 TAG 全量资源申请协议。一般应用系统都存在不同的角色 ROLE,而每个角色对资源的需求也都会不一样,比如一个典型的二层调度应用就至少包含两种角色:应用 Admin(二层调度器)+ 应用业务 Worker,资源申请 TAG 则对应了应用 ROLE 的概念。每个 TAG 的资源需求描述支持或逻辑,比如某个 TAG 的资源需求描述可以是这个样子{ {CPU3200, MEM 65536} or {MACHINETYPE_A71}}: 要求分配的 SLOT 的 CPU 大于等于 32 核并且内存大于等于 64G,或者分配的 SLOT 要有自定义的 MACHINETYPE_A7 资源。采用全量协议而不是 FUXI 的增量式协议主要原因有两:一是全量协议实现简单,不容易出错;二是在线服务的应用数目与 FUXI 的 JOB 数目差几个数量级,全量协议的资源消耗在今后很长一段时间都不会是瓶颈。
Hippo 目前不存在应用优先级所以资源分配遵循最简单的 FIFO 逻辑,采用二维打分机制来进行最终的分配,一维为资源分(根据请求与资源的最小满足原则计算得分);二维为 Hippo 引入的“资源亲近性”得分,“资源亲近性”用于描述一个应用对某台机器(SLOT)的“喜好”,Hippo 支持三种类型的亲近性:_PREFER、_BETTERNOT、_PROHIBIT,这关系依附于<app, resourceTag, slaveAdrees>三元关系组,分别表示“对应用 app 的 reourceTag 尽量分配 slaveAdrees 上的槽”、“对应用 app 的 reourceTag 尽量不要分配 slaveAdrees 上的槽”、“对应用 app 的 reourceTag 不能分配 slaveAdrees 上的槽”。同时为了更好控制,Hippo 给“资源亲近性”加上了失效时间。
Hippo 中 SLOT 的回收有两个发起点:一是应用主动释放;二是 Hippo 协议应用释放。在 Hippo 上除了机器异常情况下可以强制下线机器外,不允许 SLOT 当前 Owner 以外的角色发起 SLOT 回收,以保证应用的安全。一个正常的机器回收逻辑 Hippo 中是这样实现的:(1)将要回收的机器打上 offline 标签,该机器上未分配出去的 Slot 就不能再被分配;(2)hippo master 要求使用该机器的应用回收对应的 Slot;(3)应用额外申请一批 Slot,将 Worker 迁移上去,保证服务的完整后主动释放要求回收的 Slot;(4)Hippo Master 发现该机器上所有的 Slot 已经回收则标识该机器可回收。
Hippo 的整体资源管理方案还比较初级,例如每次分配都追求局部最优并不能做到全局最优,后续会考虑抽象出更多的决策因子(比如包/数据分布)争取提高 Hippo 的整体效率。
4. 2 服务托管
Hippo 系统内对在线服务做了最基础的一层需求抽象—-进程组模型,进程组是在线系统的最小调度单位,我们可以以此为基础构建更高层次的服务抽象。Hippo 的进程组模型由<依赖包,进程描述,依赖数据>这样一个三元组来描述,一个进程组包含 0 个或者多个依赖包、0 个或者多个进程实例、0 个或者多个依赖数据,特殊的一个空的进程组在 Hippo 中也是支持的,它不执行任何操作只是持有资源。一个进程组运行在一个 Slot 上,Hippo Slave 提供接口给应用在一个 Slot 上启动、停止一个进程组,Hippo 会根据应用提交的进程组描述自动进行包安装、依赖数据拉取、进程启动管理或者执行相应的退出流程。本章后面部分会分别针对依赖包,进程描述,依赖数据进行一个简单的介绍。
4. 2.1 依赖包抽象与运行时支持
Hippo 支持三种类型的依赖包格式:普通文件/目录、RPM 包、压缩包。其中普通文件/目录与压缩包需要是一个完整的可执行环境,Hippo 只负责将这些数据拷贝到当前应用的安装目录下,而对于 RPM 包,Hippo 首先会将包下载下来然后使用内置的 Ainst2 工具(一个强大的 rpm 包解依赖安装工具,支持远程并行安装,目前我们团队在维护)来安装到相应的目录。Hippo 中任何依赖包的变化都意味着进程组的重新启动,可以把依赖包视为一种静态依赖。任何一次进程组执行都首先从下载依赖包开始(下载主要分两种方式,一种是通过 yum,另外一种是通过 Hippo 内置的数据链式分发服务 DP2,因此包可以发布在任何 DP2 支持的数据源上),如果下包失败 Hippo 会自动重试直到最后报告安装包失败。
4. 2.2 进程描述抽象与运行时支持
Hipppo 抽象了进程描述,包括进程的启动方式(Daemon/非 Daemon)、命令行及参数、环境变量,Hippo 特殊的是可以对每个进程指定一个名字和一个 instanceId, instanceId 对应了一次显示的启动,Hippo 如果发现新提交的进程 instanceId 与当前运行的不一致则会重新执行一次(通常用于有变更需要进程重启生效的场景)。所有的进程由 Hippo 启动,对于非 Daemon 进程,Hippo 会保证其正常的执行一次;对于 Daemon 进程只要 Hippo 发现不存在就会重新拉起(启动时会有重试次数限制,超过限制将直接报告失败),并记录失败信息。
4. 2.3 数据依赖抽象与运行时支持
Hippo 内置了一个数据管理模块,负责应用数据的自动拉取(使用 DP2 服务)及版本管理,应用层面不需要显示的执行数据准备工作。针对在线服务一般依赖的数据比较多的特点,数据管理借鉴了 git 的全量 snapshot 版本管理模式同时借助 DP2 的增量传输协议高效的解决了数据依赖问题,通常数据管理先会把依赖的数据拉取到一个全局缓存然后再链接到应用的工作目录。数据管理同时负责数据的生命周期管理,自动清除过期的数据。当应用有数据变更时,只需要修改数据依赖描述并提交给 Hippo,Hippo 会反馈数据准备的状态,应用一旦发现数据准备好重新启动进程新数据即生效。
进程组模型提供了解决服务托管的基础支持,对于那些一维的简单应用使用 Hippo 内置的 Global App Master 即可很好的工作,但是对于那些复杂的应用来讲还需要自己编写二层调度器,虽然 Hippo 现在提供了一个高度封装的 SDK 但是门槛还是挺高。目前 Hippo 正在探索更高层次服务抽象,比如最近团队正在开发的 App Framework, 而笔者最终希望能够抽象出一个通用的服务模型(以及服务能力模型)通过简单的配置就能完成服务的所有自动化调度(匹之于离线系统的 JOB 模型)。
4. 3 服务安全保障
由于 Hippo 调度的都是在线服务系统,所以应用的安全性(可用性)至关重要。简单的从下面几点看看 Hippo 是如何来保障应用安全的:
五、Hippo 在线
双 11 前我们完成了多个机房的 Hippo 集群部署上线,将主搜和天猫的后台引擎完全迁移上了 Hippo, 并且很好的的支撑了双 11 需求。Hippo 过去时间主要精力在搭建基础平台,业务层面更多的关注了“自动化运维”的一些需求,在未来一段时间我们将主要关注在线服务抽象,针对系统应用开发人员提供更高层次的抽象以降低业务迁移成本,目标是一年后 90% 的搜索机器(在线服务系统)跑在 Hippo 上。