专栏策划|雅纯
志愿编辑|冯朝凯、橙蜂
之前我们举了《集装箱改变世界》(作者:马克.莱文森)中的一个例子,书中提到上世纪五六十年代,集装箱的使用,使得整体货运成本降低了95%,大部分的码头工人都面临着失业。
这件事情看起来很简单,但却给经济全球化带来了非常大的影响。后面美国企业的订单可以下到中国、以及中国成为“世界工厂”,都与之有很大的关系。集装箱的背后是标准化和基于统一标准的产业链,这里有两点比较重要的,一个是标准化,另外一个是不可变。
那么,在软件开发的过程中,我们怎样才能享受产业生态的红利,实现软件交付过程的标准化呢?软件交付当中的集装箱应该是什么样的?
如何保证软件交付过程的标准化
近十几年,软件交付形态发生了很大的变化,从最开始买物理机、建机房到虚拟机再到现在的容器。这中间为什么会产生这样的变化呢?
容器本身的底层技术是namespace和cgroup,然而这两个东西在十几二十年前就出现了。最早应用这些技术的是对资源利用率和隔离有明确诉求的云厂商,比如说阿里云不希望跑在机器上不同用户的东西互相串,最好的办法就是能限制每个用户的资源,如CPU、内存等。有了这个诉求,就会用LXC等方式去隔离,去限制资源。但是这还是没有产生容器。为什么呢?问题是各个云厂商只能在自己内部做,但是不能对外分发。所以Docker的伟大之处并不是在底层做了多大地创新,而是提供了一个可以对外分发的容器镜像。
容器镜像是一个分发的形式,我们可以把容器镜像分发给别人,或者是让别人继承我们的镜像。同时Docker又提供了Dockerfile。Dockerfile允许我们通过一个文件的形式去描述镜像。一旦能够定义镜像就可以协作了。有了这样的能力以后,容器就很快被大家所接受了。所以容器的接受看起来好像是技术发展的过程,其实是随着云原生、云市场的发展必然带来的结果。
我们很多人认为的“集装箱”就是容器,这个容器很多时候我们都认为是docker容器。在K8s里面支持很多个容器运行,大部分的情况都是用docker容器。docker容器的优势就是刚才说的两点:镜像和Dockerfile。这两点使得docker镜像可以像集装箱一样做分发。
此外,容器还提供了很好的资源隔离,可以在比较小的粒度上进行隔离。虚拟机虽然也做了隔离,但是它的粒度比较大。不仅如此,容器还提供了非常弹性的资源管理方式,这点比虚拟机和物理机都有非常大的改善。本质上它就是物理机上的一个进程,这是它和虚拟机的本质的差别。
了解了软件集装箱是什么后,然后我们再来了解下容器镜像的组成。
如上图所示,这张图非常形象地展示了容器镜像的内部结构。当我们自己执行dockerbuild构建镜像的时候,你会发现它出来的日志有很多hash值,一层一层的。实际上它是由很多层组成的,我们通过LXC或者其他的技术,把容器的进程创建出来,这个进程通过namespace和cqroup做了资源隔离和限制。容器镜像都有一个BaseImage,我们知道运行一个程序,对操作系统的环境是要求的,比如依赖的library等。这个程序如果随便在一台物理机和虚拟机部署,会随着机器的环境不同而不同,有可能导致风险。所以容器镜像给了一个基本镜像,把这个东西放里面了。再往上是Addemacs和Addapache,这两层我们会在Dockerfile中去写。然后最上面的是Writable,就是我们在容器Container运行的时候真正可以去写的东西。
那么容器镜像的特点是什么呢?它是分层的,每一层都是可以复用的,我们在某个机器上有很多个容器,如果Base镜像一样,只要下一次就行了。可以看到镜像的大小是所有的层堆起来的,堆的东西越少,这个镜像就会越小。容器镜像有一个最小的镜像叫scratch,就是一个最原始的基础镜像。这里面几乎什么都没有,基于它构建一个非常非常小的容器的话,可能就是几兆的大小。但是如果你基于CentOS基础镜像可能就是上G的大小。
容器镜像有一个非常重要的概念叫“One process per container”(容器生命周期=进程生命周期)。我们可以认为容器就是K8s上的一个进程,如果把K8s比作操作系统,那么容器就是它上面运行的一个进程。进程的生命周期是可以被管理的。虽然容器有这么多的优点,但实际在用的时候也会遇到很多的问题。
下面我们聊一聊容器镜像的一些常见的问题和建议。
容器镜像常见问题及实践建议
容器镜像常见的问题:
- 把所有的东西都装到一个容器里面,把容器当虚拟机来用。
- 把ENTRYPOINT设置为systemd:systemd管理的进程运行的结果和状态和的容器状态是不一致的,有可能里面的进程已经僵死了,或者Crash了,但是systemd还活着,从外部看起来这个容器没问题。
- 私有化部署的时候带一堆导出的镜像tar包。tar包是不分层的,它不知道里面是有很多层。
- 每次把基础镜像下发到整个集群,导致网络变得特别拥堵
我们的实践建议是:
- 尽量采用轻量的基础镜像和确定的镜像版本。
- 通过分层来复用镜像内容,避免重复拉取。
- 避免采用systemd,包括supervisord和类似这样的daemon管理服务来做ENTRYPOINT。
- 采用本地的dockerregistry等以层为粒度来离线拷贝镜像。
- 避免同时要做大量的pull,可采用P2P的方式(如使用dragonfly)提升镜像分发效率。
容器镜像可以实现软件交付过程的标准化。标准化是手段不是目的,标准化是帮助我们更高效的复用的技术。
回到软件交付的终态,我们的目的是希望提供一个稳定可预期的系统。
而达成这个目标的前提是,要有确定的运行环境和软件制品。确定的环境是指代码(及其依赖)、构建环境、构建脚本与预期一致的产出软件制品,这一点如何做到我们后面再作分享。我们先看如何保证软件制品的一致性。
如何保证软件制品的一致性
要保证软件制品的一致性,软件制品应该有确定的格式、唯一的版本、能够追溯到源码、能够追溯到生产和消费过程,这样才能使持续交付更好地服务于企业的制品管理与开发。
在制品构建过程中,经常会遇到一些问题。例如应用的代码库里没有Makefile,package.json,go.mod而没法确定依赖,或者制品能构建成功但缺失几个依赖,又或是在自己的开发环境运行正常而在生产环境出现了开发环境没有的bug。导致这些问题出现的原因是因为构建本身是可变的,当你构建可变时,就会带来一系列的问题。为此,我们需要通过不可变构建来使制品与预期一致。
要实现不可变构建,我们需要保证有:
- 相同的代码
- 相同的构建环境
- 相同的构建脚本
相同的代码
例如程序员开发时,不在依赖描述文件(如go.mod,package-lock.json,pom.xml,requirements.txt等)中指定依赖的版本,则会默认使用最新的版本作为依赖,这样产出的制品会随着依赖的更新而不能保持一致,这将带来完全不在预期内的风险。
相同的构建环境
对于构建环境来说,Dockerfile可以用来在容器平台下描述环境,通过Dockerfile我们能为制品使用一致的环境。很多时候我们并不需要在运行中使用构建环境的很多依赖,而构建镜像的体积往往比较惊人,这个时候我们就需要将构建环境与运行环境分开,以得到尽可能轻量的镜像制品。
相同的构建脚本
对应的,使用相同的,与代码实现无关的构建脚本也是非常重要的,在Dockerfile的环境中必须指定确定的环境依赖版本。
只有在同一份代码(及同一个依赖)、同样构建环境的描述、和同样构建脚本的环境下,所产生的软件制品才是相同的。这里强调的是说所有的东西都要保证一致性,如果说三者是一样的话,那产生出来的制品也是一样的,即使构建时间不同,产出的制品也是相同的。
做好不可变基础设施,首先要标准化最终交付制品的形态,并且明确此交付形态的运维管理方式。而要保证不可变,那首先要做好不可变的构建,然后才能有一致的软件制品。
NOTE:构建准确性,永远比构建更快重要。制品的构建信息不准确,导致构建制品不一致、版本不可控,所有后续的工作都是浪费。
如何提升构建效率
在构建这块,一个需要关注的点的是如何提升构建效率。我们先看一个简单的计算问题:
这是一个非常大的数据,也是非常大的损耗。很多时候一个项目的工程效率太低的原因就是因为构建太慢。构建耗时过长使得制品迭代非常慢,功能更新和bug修复也会受到影响。
那我们如何提升构建的效率呢?下面是我们的一些实践建议:
1个基本原则:保证构建的准确性,构建的准确性永远优于构建的效率。只有在保证准确性的前提下提升效率才有意义。
5点建议:
- 应用瘦身:检查应用的依赖情况,应用包体积是否过大,依赖项是否过多,能否去除不必要的依赖,能否构建更小的镜像。
- 分层构建:底层的东西先构建出来以后被上层所复用,然后就可以做增量式的了。
- 构建缓存:构建过程中拉取依赖是很耗时的,要避免重复拉取。
- 网络优化:主要是保证代码、构建机器和制品库之间的低网络延时。代码和构建机器是否是在同一个低时延链路中。例如代码在Github上而使用云效构建,此时的延时相对于内网会高出许多。
- 仓库镜像:仓库镜像可以极大地减少拉取依赖项的时间。在国内的网络环境下,如果从源仓库获取依赖,可能延时会非常长,这时可以使用镜像网络降低延时。例如nodejs开发者常使用淘宝的npm镜像源,而Python开发者使用清华的镜像源。对于企业来说也可以构建自己的镜像仓库以提升可靠性与降低延时。云效也使用了镜像仓库,来减少拉取的时间。
(小编推荐:云效流水线Flow 是一款云原生时代的流水线工具,通过容器技术让企业摆脱对虚拟机构建环境的依赖。您甚至可以根据您的使用需求,在同一条流水线上使用不同的构建环境。此外,云效流水线Flow 还提供了各种语言的容器环境,满足不同的构建使用场景。点击文末阅读原文,了解详情)
总结
本篇文章,我们从软件交付的终态出发,提出了不可变构建的概念。希望通过:相同的源码+相同的环境+相同的构建脚本=>带来一致的软件制品。而这些东西都是保存在源代码里的,所以源代码的管理非常重要。
下篇文章,我们将分享如何对源代码进行有效管理。
阅读上篇:做到这4点,才是真正的持续交付
点击下方链接立即体验云效持续交付流水线Flow。
https://www.aliyun.com/product/yunxiao/flow?channel=yy_rccb_36