阿里集团八年容器化演进之路
PouchContainer 现在服务于阿里巴巴集团和蚂蚁金服集团的绝大部分 BU, 包括交易&中间件,B2B/CBU/ICBU,搜索广告数据库,还有收购或入股的一些公司,比如优酷高德、UC等。其中体量最大的是交易和电商平台,在 2017 年双 11 的时候我们支撑了破纪录的峰值,背后的应用都是跑在 PouchContainer 里面,整体容器实例已经到了百万级规模。使用了 PouchContainer 的应用涵盖了各种各样的场景。这些场景从运行模式来看,有标准的在线 App,还有像购物车、广告、测试环境等比较特殊的场景。不同的场景对 PouchContainer 有不同的使用方式和需求。从编程语言看,实际运行着 JAVA、C/C++,Nodejs,GoLang 等语言编写的应用。从技术栈的角度看,包含了电商、DB、流计算、大数据、专有云等场景,每个场景对于容器各方面要求,所用到的特性都不太一样,PouchContainer 针对每个场景的需求都在产品上都做了支持。
PouchContainer 容器技术在阿里的演进过程伴随着阿里技术架构本身的演进。阿里内部技术架构经历了一个从集中式单体应用到分布式微服务化的演进。
淘宝最开始是一个巨石型的应用,一个应用里包含了商品、用户、下单等等所有交易链路的功能。随着功能越来越完善,维护起来也越来越困难。为了提高研发效率,从 2008 年开始我们逐渐把这个应用拆分成了多个分布式应用,商品的,交易的,用户的,前台的,后端的;通过 HSF 远程调用框架,TDDL 分布式数据层和 Notify 分布式消息中间件串联起来。其中每个服务都有多个实例,都可以独立研发演进,并可以进一步继续拆分。于是就逐渐形成了一个庞大的分布式服务集群。从巨石型应用到多个单一功能的轻量级服务型应用,总的应用实例数变多了,每个实例需要的系统资源变少了。于是从最初的每个实例直接使用物理机自然过渡到使用 xen,kvm 等虚拟化技术。VM 使用了一段时间之后,发现整体物理机的利用率还是很低。当时一个 24 核的物理机只能虚出 4 台 4 核的 VM,除了当时虚拟化本身的开销不小外,每个应用实例在 VM 里仍然用不完分到的资源。于是就想能不能不用虚拟机,用更轻量的基于进程级别的资源切分使用方式。
这个时候阿里内部的运维体系已经比较庞大了,从应用的构建部署到分发,到一些运行期的监控告警等管控系统,都依赖于一个应用实例跑在一个独立机器里的假定。这个假定已经不经意间贯穿到了研发运维的各个环节里面,包括系统的设计,运维习惯等都严重依赖这个假定。我们不可能重新搭建集群,把存量的业务停掉再到新的集群里面用新的运维模式去跑起来,这个业务和运维上都是没法接受的,不可能电商交易的研发停几个月,系统停几天来搞这个事情。所以我们首先要做到兼容,新的资源使用方式必须兼容原先的假定。我们经过仔细分析了这个假定的内涵,发现每个应用实例归纳下来无非有如下 4 点要求:
● 有独立IP
● 能够ssh登陆
● 有独立的,隔离的文件系统
● 资源隔离,并且使用量和可见性隔离
首先是有独立 IP,能够 SSH 登录。其次有独立的文件系统,应用程序跑起来,希望程序看到的整个文件系统都是给他专用的,因为现有的代码和配置中必然有很多路径的硬编码,需要满足这个潜在要求。还有不管通过工具还是代码,他只能看到分配给他自己的资源。比如 4 个 CPU,8G 的内存,他能够根据这些资源的用量做一些监控,做一些对自己资源使用量的采集和告警。这四个特点总结下来就是新的资源使用方式要做到和物理机或者 VM 的使用体验一致。能够做到这样的话原先跑在 VM 里的应用就可以很平滑的迁移过来,现有的应用系统和运维系统不需要做很大的改动。
我们为了能达到这四点,最开始是多隆大神手工 Hack 系统调用,glibc 基础库等,实现了一些资源上的隔离。像有独立的 IP 可登录 ,就用虚拟网卡,在每个容器里面起一个 sshd 进程;资源的隔离和可见性上,就用 Cgroup 和 Namespace 等内核特性;后来发现开源的 LXC 项目也在做同样的事情,并且比手工 Hack 更通用化,更优雅一些。于是我们集成 LXC,并且在内核上加了定制的资源可见性隔离的 patch,让用户的实例只能看到分配给他的 CPU和内存,另外还增加了基于目录的磁盘空间隔离的 patch,这样就形成了我们第一代的容器产品。这个产品当时代号是 T4,寓意是第四代淘宝技术,淘宝 4.0;在 2011 年的时候 T4 容器技术灰度上线。T4 相比 VM,完全没有虚拟化 Hypervisor 层的开销,资源切分和分配上更加灵活,可以支持不同程度的资源超卖。这样就很好的支持了业务爆发增长的需求,控制了物理机按业务增长比例膨胀的势头。另外因为 T4 完全兼容了之前研发和运维对物理机和 VM 的使用习惯,绝大多数应用都能够做到透明的切换,应用无感知。因为有这些特性,在接下来的短短几年时间里,T4 逐步接管了交易和电商主体的在线应用。
到 2015 年的时候 Docker 技术火起来了。我们写程序的都知道有个著名的公式,程序=数据结构+算法。从程序交付使用变成一个软件产品的角度来看,我们可以套用这个公式:
● 软件= 文件(集)+ 进程(组);
从静态来看,软件从构建分发到部署,最终形式是一个有依赖层次的文件集。从动态来看,这些文件集,包括二进制和配置,由操作系统加载到内存后执行,就是一个有交互关系的进程组。我们之前的 T4 容器在进程(组),或者说运行时上做的事情和 Docker 基本类似,比如说都使用了 Cgroup、Namespace、linux bridge 等技术。还有些是 T4 特有的,比如基于目录的磁盘空间的隔离,资源可见性隔离,对老版本内核的兼容等。我们从最早物理机演化到 VM,再到现在的容器,内核的升级周期比较漫长,迭代很慢,15年的时候存量的机器上全部都是 2.6.32 内核,T4是兼容 2.6.32 内核的。 但是另一方面在文件(集)的处理上 Docker 做得更好,更加系统化。 T4 只做了很薄的一层镜像,给相同的业务域做了一个基础的运行和配置环境,这个镜像没有深入到每一个特定的应用。 而 Docker 是将每个应用的整个依赖栈打包到了镜像中。因此在 2015 年我们引入了 Docker 的镜像机制来完善自己的容器。
在将 Docker 镜像整合进来之后,原来基于 T4 的研发运维体系受到了很大的冲击。 首先交付方式变了,之前是 build 一个应用的代码包,把代码包交给我们的部署发布系统,后者创建一个空的容器,根据这个业务所在的很薄的模板把一个空的容器跑起来,再到容器里面安装依赖的一些 IPM 包,设置一些配置,按每个应用定好的一个列表一个一个的安装好,然后把应用包解压启动起来。这个应用依赖的软件和配置列表我们内部叫做应用的基线。引入镜像之后,在将 Docker 镜像整合进来之后,原有的交付方式发生了变化。之前是 build 一个应用的代码包,把代码包交给我们的部署发布系统,后者创建一个空的容器,根据这个业务对应的很薄的一个模板,把一个空的容器跑起来,再到容器里面安装依赖的一些 RPM 包,设置一些配置,按每个应用定好的一个清单一个一个的安装好,然后把应用包解压到主目录启动起来。这个应用依赖的软件和配置清单我们内部叫做应用的基线。引入镜像之后,我们应用的代码包和依赖的所有的这些三方软件、二方软件都会打成一个镜像。之前通过基线维护应用依赖环境,现在都放到每个应用自己的 Dockerfile 中了,整个研发构建和分发运维的过程大大简化了。
做了这个事情之后,研发和运维之间的职责和边界就发生了变化。之前研发只需要关注功能,性能,稳定性,可扩展性,可测试性等等。引入了镜像之后,因为要自己去写 Dockerfile,要了解这个技术依赖和运行的环境倒底是什么,应用才能跑起来,原来这些都是相应运维人员负责的。研发人员自己梳理维护起来后,就会知道这些依赖是否合理,是否可以优化等等。研发还需要额外关注应用的可运维性和运维成本,关注自己的应用是有状态的还是无状态的,有状态的运维成本就比较高。这个职责的转换,可以更好的让研发具备全栈的能力,思考问题涵盖运维领域后,对如何设计更好的系统会带来更深刻的理解。所以引入 Docker 之后对研发也提出了新的要求。我们总结新的时期,新的运维模式下对研发能力要求的几个要素,总结起来就是几个原则:
为了更好的把自己的系统建设好,我们要倡导研发从第一天建立系统的时候,就要考量最终的可运维性,比如参数是否可配置,是否可以随时重启。机器每天都有硬件故障产生,这些硬故障不可能每天都人工处理,必须要尽可能自动化处理,自动化处理时,虽然有些故障只影响了一部分实例,另一部分是好的,但是也可能需要一起处理,比如需要物理机上的业务全部迁移走来维修物理机的时候。所以不管当时容器里的业务是好的还是不好的,都要满足随时可重启,可迁移的要求。原来是部分交付,现在要考虑你到底运行环境是什么样的,什么样的运行环境才能跑起来,尽量做标准化的操作。比如说启动,Dockerfile 里面写好启动的路径,不要再搞一些特殊的处理,如果有任何特殊的处理都没法做统一的调度和运维。统一的业务迁移,机器腾挪也没法做。我们的目标其实就是从一开始的比较粗放的运维,到不断的开发自动化的工具和系统,形成一个体系,通过前期人工运维的过程把一些固定的故障处理的流程模式化,最后提取出来一些可以自动处理故障,自动恢复的机制。我们的最终目标是无人职守。所有这些加起来其实就是我们引入镜像化之后,并且要朝着无人值守的方向演进时,对研发和运维的新的要求。
为了更好的把自己的系统建设好,我们要倡导研发从第一天建立系统的时候,就要考量最终的可运维性,比如参数是否可配置,是否可以随时重启。机器每天都有硬件故障产生,这些硬故障不可能每天都人工处理,必须要尽可能自动化处理,自动化处理时,虽然有些故障只影响了一部分实例,另一部分是好的,但是也可能需要一起处理,物理机上的业务全部迁移走来修物理机。所以不管当时容器里的业务是好的还是不好的,都要接受随时可重启,可迁移。原先是部分交付,现在要考虑你到底运行环境是什么样的,什么样的运行环境才能跑起来,尽量做标准化的操作。比如说启动,Dockerfile 里面写好启动的路径,不要再搞一些特殊的处理,如果有任何特殊的处理都没法做统一的调度和运维。统一的业务迁移,机器腾挪也没法做。我们最后的目标其实就是从一开始的比较粗放的运维到很多人都能介入,到最后的自动化不断的开发自动化的工具,形成一个体系,通过前期人工运维的过程把一些固定的故障处理的流程模式化,最后提取出来一些可以自动处理故障自动恢复的机制,最后我们的目标是无人职守。所有这些加起来其实就是我们引入镜像化之后,并且要朝着无人值守的方向演进时,对研发和运维的新的要求。
上面是 PouchContainer 容器的 Roadmap, 2011 年的时候 T4上线 ,到 2015 年 3 月的T4 覆盖了交易的大部分应用。这个时候开始引入了 Docker 镜像机制,这里面做了很多兼容性的工作。比如说原来 T4 轻量化的模板转化成对应的基础镜像,里面兼容了很多之前运维的习惯和运维的工具,如账号推送,安全策略,系统检测。我们在 2016 年初上线了第一个镜像化应用,到 5 月份的时候集团决定主站全部应用容器化。在做镜像之前阿里是有一两百人的团队做每个应用的部署,运维,稳定性控制,后来这个团队都没有了,全部转成了 DevOps,转向开发工具和运维平台,通过代码的方式,工具的方式解决运维的问题。之前专职做运维的同学最大的负担就是线上环境的变更,研发提交变更申请给运维同学,运维同学做线上操作,研发不知道代码运行环境具体依赖了哪些基础软件。做了镜像化的事情后,研发自己负责编写 Dockerfile,运维就把环境变更的事情通过 Dockerfile 的机制移交给了研发。运维和研发之间的边界就非常清楚了,这个边界就是由 Dockerfile 来定义的。研发负责把他代码依赖的环境在 Dockerfile 定义好,运维保证其构建分发时没有问题。我们在 2016 年双11的时候完成了交易核心应用的镜像化 PouchContainer 化改造。在 2017 年双11的时候交易全部应用完成了镜像化改造。然后我们在 2017 年 11 月 19 日的时候宣布了 PouchContainer 的正式开源。
我们的内部 PouchContainer 经过大规模的运行,支持了各种各样的业务场景,各种不同的技术栈,不同的运行形态,积累了非常多的经验。这些经验之前跟阿里内部的环境耦合性比较大。比如说我们的网络模型,我们其实是嵌入到了阿里内部的网络管控平台,包括IP分配在内部都有独立的系统去完成。比如什么时候启用 IP,什么时候下发路由等等,这些是有一个统一的 SDN 网络管理系统来管理的。还有类似的内部存储系统,还有运维的一些指令推送系统。内部系统耦合性比较大,没法直接开源。所以我们最后选择的策略是先在外部孵化一个从零开始全新的项目,把内部的特性一点点搬上去。这个过程中我们内部的版本也会做重构,把内部的依赖做一些插件化解耦合的方式,这样最后全新的项目在外部可以跑得很好;在内部用一些耦合内部环境的插件也可以跑起来,最终的目标是内外用一套开源版本。
那么我们的 PouchContainer 容器相对于其他容器有什么差异呢?主要体现在隔离性、镜像分发优化、富容器模式、规模化应用和内核兼容性几个方面。传统的容器隔离维度就是 namespace、cgroup;在资源可见性方面,我们前几年是通过在内核上打 patch,在容器内看内存和 CPU 利用率等数据时,把统计数值和当前容器的 Cgroup 和 Namespace 关联起来,使容器能使用的资源和已使用的资源都是容器自己的。18年的时候我们引入了社区的lxcfs,这样就不需要对特定内核 patch 的依赖了。磁盘空间的限制也是在低版本内核上加了补丁,支持了基于文件目录的磁盘空间隔离,能够把每个容器的 rootfs 限制住。在 4.9 以上的内核上,我们是用 overlay2 文件系统来完成同样功能的。我们也在做基于 hypervisor 的容器方案,提升容器的隔离性和安全性,我们在 PouchContainer 里面集成了 RunV,用于一些多租户的场景。
阿里内部的离在线混部之所以能推进,在同一个机器上既能跑在线的业务又能跑离线的一些任务,互相之间不会出现太大的干扰,其核心的技术就是 PouchContaienr 容器可以根据优先级,把不同业务的资源使用隔离开来,保证在线业务优先使用资源。这个资源包括很多的维度,比如 CPU、内存,CPU cache、磁盘、网络等等。
这是 PouchContainer 的镜像分发设计。我们内部有很多比较核心的应用,体量比较大,实例会分布在上万台物理机上。发布新版本的时候上万台机器同时拉镜像,任何中心的镜像仓库都扛不住。因此我们设计了一套镜像分发的二级架构,在每个地域建一个 mirror,在同一个地域内拉镜像的时候用 P2P 分发技术---我们内部的产品名叫蜻蜓,已经开源;需要拉镜像的服务器之间可以分散互相拉文件片段,这样就直接化解了中心镜像仓库的服务压力和网络压力。后面其实还有更好的解决镜像分发的思路,我们正在尝试镜像的远程化,通过存储计算分离技术,用远程盘的方式挂载镜像,直接跳过或者说异步化了镜像分发这一步,目前正在内部环境灰度运行中。
这是 PouchContainer 内部版本的体系结构。在最底层的宿主机层面,我们会做一些管理和运维,目的是为了确保容器运行依赖的基础环境是健康的,包括宿主机的一些镜像清理,包括安全控制、权限管理等。OS 的低版本内核我们是适配到最低 2.6.32 内核,包括容器里面的进程管理也做了很多的适配。资源隔离前面讲过了,网络模型我们内部其实主体用的是 Bridge,但是其他各种各样的场景也都支持。我们开发了很多插件,PouchContainer 开源后,我们才将这些插件逐步做了标准化,兼容适配了社区的 CNI 标准。最上层是一个富容器模式的支持,每个容器里面会启动一些跟内部的运维工具,运维系统息息相关的一些组件,包括一些发布模式的优化。可以看到我们内部体系结构是比较复杂的,尤其依赖内部的其他系统比较多,在外部直接跑是跑不起来的,因此也没法直接开源。
所以我们开源版本是重新开始搭建的,这样会比较清爽一些。我们引入了contained,支持不同的 runtime 实现,包括我们自己包装 lxc 开发的 RunLXC 运行时,可以用来支持老版本 2.6.32 内核。开源版 PouchContainer 兼容所有 Docker 的接口,也支持 CRI 协议,这样也就同时支持了比较主流的两种集群管理系统。网络方面我们内部基于 libnetwork 做了增强,包括不同场景暴露出来的一些问题,一些稳定性,规模化的时候各种细节的一些优化。存储方面我们支持了多盘,内存盘,远程盘等各种不同形式的存储。PouchContainer 可以无缝集成到上层编排工具中,包括 Kubelet 和 Swarm 等。我们内部的 Sigma 调度系统,不同的版本Docker 协议和CRI协议都会使用。
这是 PouchContainer 的开源地址:https://github.com/alibaba/pouch
如何贡献:
https://github.com/alibaba/pouch/blob/master/CONTRIBUTING.md
最近 PouchContainer 开源版本 GA 已经发布,PouchContainer 能够在如此短的时间内 GA,离不开容器社区的支持,在超过 2300 个 commit 的背后,有 80 多位社区开发者的踊跃贡献,其中不乏国内一线互联网公司、容器明星创业公司贡献者的参与。
PouchContainer 开源版本发布 GA 之前,此开源容器引擎技术已在阿里巴巴数据中心得到大规模的验证;GA 之后,相信其一系列的突出特性同样可以服务于行业,作为一种开箱即用的系统软件技术,帮助行业服务在推进云原生架构转型上占得先机。
本文作者:林轩
本文来自云栖社区合作伙伴“阿里系统软件技术”,如需转载请联系原作者。