本文源自 3 月 2 日『高效开发运维』微信群的在线分享《阿里妈妈在线投放系统构建之路》,分享者为阿里巴巴杨兵兵先生,转载请在文章开头注明来自『高效开发运维』公众号。加群学习请关注『高效开发运维』公众号,并点击菜单中的“加群学习”或直接回复“加群”。
阿里妈妈是阿里集团下的广告业务 BU,广告在线投放系统是广告业务中的最前端,是业务变化的必经之地,绝大部分的业务变更都会导致投放系统的变更。因此,投放系统的迭代效率直接影响了广告业务的迭代效率。系统构建是整个交付体系中的基础,业务的变更、新业务的接入、业务计算规模的扩张效率都依赖着强大的系统构建能力。本文介绍阿里妈妈在线投放系统的构建演进过程,讲解基础技术架构的变革如何给业务带来迭代效率的提升。
业务系统演进
最开始业务系统是
一两个子服务组成投放引擎,几十台机器支撑业务流量。
这时,原始的系统构建方式即可满足业务的需要,开发写个部署手册,运维根据手册构建系统(升级系统),一个人可以搞定了,上线也就分分钟的事。
后来变成了这样
子服务数量扩展到四五个,机器规模扩展到百台,集群规模扩充到两个。
这个阶段下,运维人员发现不好搞了,我需要了解好多手册,分批处理好多机器,弄不好就错了,一个上线几个小时都是有可能事。大家就在想怎么让构建过程能够自动化起来,随之演变的就是工具脚本时代,处理变更的过程用工具进行过程串联,点几个按钮等待做完就行了。
现在是这样
子服务数 N 个,机器规模千台,集群规模 M 个。
这时运维发现,工具维护好头疼,因为应用太多,不同的应用需要定制不同的工具;错误难以恢复,因为过程的串联处理在任意一个地方都可能发生异常,各种异常产生不同的状态,不同的状态下恢复的手段都不一样。广告这种场景,分钟级的异常都是庞大的损失。这时,我们进入了当前的时代,面向错误的分布式调度。我们的目标是无论系统当前处于什么状态,都可以通过简单唯一的方式进行快速恢复。
系统构建分解
构建系统的核心目标是极致的效率,其体现在以下两个方面:
- 以最快的速度构建业务系统
- 在任何状态下,以最快的速度恢复系统异常
关键问题分解
1 交付物
1.1 代码
我们使用 Docker 作为业务代码交付的载体,其解决了以下几个问题:
- 在交付物版本管理上,Docker 镜像以集装箱的形式方便我们去管理版本,Docker 运行生态方便我们对运行实例进行简单的搬迁。
- 在效率方面,Docker 镜像的分层模式带来了差异化构建及部署的能力。复杂的业务应用交付全集可以达到百兆甚至上 G 的体量,全量构建或部署一个交付物的时间会很长,而实际业务的升级往往只会改变其中很小一部分内容。差异化的构建部署方式会带来百倍的效率提升。
1.2 数据
投放系统要求服务的请求响应时要控制在毫秒级别,因此数据通常是放在内存中进行访问。而我们的数据规模又非常的庞大,离线产出的数据会存储在 DFS 中。数据的交付构建过程就是从 DFS 中加载数据到计算节点的内存中,这个效率直接影响系统构建效率。
数据调度方法需要解决的核心问题是:访问端与存储端的负载均衡。
我们通常认为计算集群与数据集群是物理和逻辑上独立的,比如搜索引擎可以将索引数据放在 HDFS 上进行管理,访问的时候从 HDFS 拖到本地,而二者本身不存在任何交叉关系。这时面临的问题是,计算节点数以千计,而存储节点因为只需要磁盘,数十个节点即可完成存储,如何将数据从远端加载到所有计算节点的内存中呢?
因为计算节点远超存储节点,为了解决网络传输的效率问题,我们使用链式分发组件进行数据传输。链式分发可以解决对存储端网络传输并发的问题,使得整体网络负载得到控制,但也带来了一个严重的问题:如果一个节点断了,整个任务就断了,即使通过技术方案去做到重新组链避免这个问题,也会影响到全局任务的效率。这个问题的本质是每个单点状态直接关系到最终状态的结果。另外,在数据传输效率有限的情况下,数据还需要考虑在计算节点本地进行物理备份,以解决重新远程恢复的效率问题。
为了解决链式分发带来的复杂依赖问题,我们利用万兆网络环境,采取 DFS 与在线节点混部的方式,做到数据读取端与存储端在节点数量上一比一。另外,在数据读取模式上我们进行了随机读取打散的策略,让所有节点可以同时进行 DFS 数据加载,而避免网络集中阻塞的问题。百 G 的数据可以做到在分钟级别加载完毕。
这时,计算对数据的访问链路直接变为 DFS 到内存,虽然数据仍然整体在内存中,但站在外部调度看,应用对数据的描述直接是 DFS 的源地址。可以说这个方案的本质是计算与存储的分离。
2 服务间关系维护
完整的投放系统由多个子服务组成,子服务间存在 Client - Server 关系。随着集群数量的扩展,维护独立集群中服务间关系成为很大的负担,靠人做静态化配置显然无法支撑集群及子服务的动态变化。因此,我们需要提供服务间自动发现的能力。
我们使用 Zookeeper 实现了服务注册发现组件,使用该组件可以让 Client 自动感知 Server 列表及其实时变化,决策请求发向哪些物理节点。
3 机器资源与业务计算的耦合
在实际生产环境中,我们经常会面临对机器资源的调整场景,比如扩容、坏节点处理、机型换代、新机房建设等。人肉的运维状态下,机器资源的调整一定会带来对跑在上面的应用进行调整。扩容会带来新机器的重新部署;机型换代会带来被替换机器上应用的清除以及新机器上应用的重新部署。我们要做到的是如何让运维人员只需要关心数量,而不需要关心重新部署。
这里我们采用业界的设计思想,对计算调度进行了二层划分:应用调度 & 资源调度。资源调度层只负责维护机器,响应上层的资源分配请求,分配 Docker 容器,并进行全局管理。运维人员只需要做好业务预算,将机器资源放入资源池,通过调整应用的资源依赖数量,完成应用的资源调整。
4 应用调度能力及效率
在线投放系统对业务的运维需求主要有以下几种:
-
集群创建
输入:Docker Img、数据 DFS 源位置、资源描述及数量
-
版本更新
输入:Docker Img、数据 DFS 源位置特点:灰度更新(金丝雀)、可服务数量保持、灰度更新过程中再次提交更新
-
容量调整
输入:Replica 数
-
状态查询
输入:应用标签名
-
坏节点自动替换
这里值得着重介绍的是版本更新和换节点自动替换能力。
在更新能力上,我们实现了:灰度更新(金丝雀),即不停流量的批次更新能力;可服务数量保持,即在更新过程中的异常不影响对外服务;更新过程中再次提交更新,即随时可以打断当前阶段,指定到最终目标。
坏节点自动替换提供了任何异常状态下无需人工干预快速修复的能力,比如机器坏掉了,应用 core dump 了等,从而提高了系统错误修复效率。另外,这些调度功能可以随时并发执行,比如在扩容过程中升级应用。这样,调度系统为业务提供了极致的运维效率。
5 资源成本
除了交付物的管理,Docker 帮助我们解决的另一个问题是资源隔离。从前我们在物理机层面进行应用部署,我们不敢把两个应用部署在一个物理机上,因为可能产生部署和资源竞争的冲突。另外,维护起来也很麻烦,比如换一个物理机要知道上面有几个应用受到影响。Docker 加上基于其上的调度系统,可以让用户在资源上进行细粒度描述,管理应用无需再关心宿主机在哪里。容器间资源隔离,相互不受影响;物理机变更无需关心应用状态,调度系统可以自动完成应用容器的搬迁。这样,物理机资源可以得到最大程度的利用,而且管控能力也极其简单。
系统架构
- Docker Registry 负责管理 Docker Img
- 在线数据中心负责管理在线数据的版本及存储信息
- 集群业务策略管控平台是构建系统的入口,负责管理集群逻辑、集群规模、业务升级策略等运维需求
- 数据调度系统、应用调度系统、资源调度系统共同组成基础调度层,负责单应用的创建、升级、容量调整等基础调度操作
- 运行环境由资源调度系统管理机器资源,应用 Run 在 Docker 容器中提供服务
写在最后
广告基础技术架构的核心是解决业务效率及计算规模问题,高效的系统构建能力对于复杂分布式计算场景有着极为重要的作用。业务的增长带来了业务系统的快速变化:版本快速迭代、计算资源扩张、存储扩张、部署结构调整等。快速响应业务的变化是系统构建要解决的核心问题。
我们正在不断完善构建能力、降低构建难度、提升构建效率。在我们的构建体系下,能够做到:5 分钟完成计算容量调整;20 分钟完成版本迭代;1 天部署一个投放系统;3 天完成业务架构升级。
QA 环节
Q1:请问什么时候采用 Docker 的?在这之前你们采用的是什么技术?Docker 有遇到什么坑吗?
A1:我们目前正在整个在线投放系统上推广 Docker,Docker 只是整个构建系统中的一个组成部分,应该说我们正在让广告在线业务跑在构建系统上;在这之前,我们经历了物理机运维时代,后来直接使用过 cgroup 进行隔离产生容器概念;Docker 方面我们需要解决的核心问题是各种资源的彻底隔离问题,比如磁盘带宽、网络带宽,我们的应用都是服务,有高实时响应的要求,不能产生瞬间的争抢,要做到比较彻底的隔离。
Q2:可否透漏你们有多少个子服务?多少个投放系统?刚才总结说了未来的目标,那现在的数量级是怎样的?
A2:大于 10 个子服务;大于 3 个投放系统;目前我们的系统正在逐步的进行接入,对比之前的状态,数量级提升了 10 倍,后续的工作还会有倍数的提升
Q3:你说的运行在容器中的应用 ,包不包含数据库,MQ,文件系统这些东西。 有没有哪些特殊的系统 是你们实际使用中发现不适合部署在容器中的?
A3:我们的场景面向的是在线服务,不包含数据库,MQ 我们有独立的维护系统,未来可能会在部署层面进行统一,文件系统我在文章中数据部分进行了描述,我们正在将分布式文件系统用 Docker 与在线服务进行混部;我们面向的业务只在分布式服务上,Docker 还是能够满足的
Q4: 感谢杨老师分享!!“完整的投放系统由多个子服务组成”,你们使用了 ZK。但是听说,其实更好的情况是,在编写子服务本身,就要考虑到服务注册和发现的问题,从框架入手。杨老师怎么看?
A4 : 我们正是这样做的,服务研发框架直接嵌入服务注册发现组件,组件即是对 zk 的封装。
Q5:可否展开讲讲如何进行坏节点的自动替换?
A5:这个问题真是问道技术点上了,要描述清楚需要功夫。
首先,我们要定义清楚什么是坏节点。在文章里我描述了比如宿主机器挂了,应用进程 core dump 了,如果用这样去监控异常在决定如何处理,比如换个宿主机?重新拉起应用进程?我们是无法处理所有坏的情况的。
这带来了一个系统设计理念,面向错误的设计,这也是构建系统的核心面向错误的设计就是要解决任何错误状态下进行恢复的能力,关键问题是如何解决无法预料的错误状态。
我们进行了这样的状态切分:
- 应用调度只关注一个状态:容器死活,死了就重新创建
- 容器关注运行在其内部的进程死活,进程死了自己就死掉
- 应用进程完成对数据、子进程等状态的判定,出现异常就自己死掉
这样,一层一层进行了状态的独立划分,每一层只关注一个状态,最终的恢复方式就是 restart
总结一点:我们面临的核心问题是复杂状态的转移和处理,解法就是简化状态集合 & 状态间解耦。
Q6:请问杨老师:关于调度系统,你们采用了什么技术实现?如何实现调度任务之间的依赖;调度的隔离是独立封装还是用的 docker 自带的呢?隔离
A6:主要技术载体是 Docker,解决应用状态的封装及资源隔离,其它技术不太容易描述,就像上面坏节点自动替换的问题,核心是系统设计;调度间依赖、调度隔离,这个问题我的理解是不是在问调度任务间并行的问题?调度任务的并行能力也是我们系统的核心能力,你可以一边扩容一边发布版本,A 版本没法完就可以继续发 B 版本。这个能力的核心还是状态问题,如何让状态解耦。
Q7:MQ 独立的维护系统是指共用的阿里集团内部的 MQ 吗?(RocketMQ 吗)
A7:不是,我们有自己的 MQ 系统,内部自己维护,今后有考虑基于我们的构建体系统一进行运维。
Q8:请问杨老师,访问端与存储端的负载均衡采用什么样的技术方案,目前业务对于负载的压力是否足够,如果以后压力暴涨,是否有成熟的方案?
A8:这个问题的描述是文章中数据调度部分的描述。这部分表达的是多个 应用进程远程从 DFS 中加载数据时,如何保证有足够的加载速度,因为很多节点同时对 DFS 并行读,如何让 DFS 网络不成为瓶颈。解法的核心是让并发访问端不在同一时刻读取一个存储块的数据,做到分散访问,带来两个解法:
1 让存储端节点数量匹配访问端数量
2 让访问端对数据块的访问尽量随机。
数据访问的压力基本是恒定的如果是问服务外部压力的话,应用系统的负载均衡策略就是轮询,因为每个请求的计算消耗基本是平均的,不需要复杂的策略。
感谢木环对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论