mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
mod
This commit is contained in:
139
极客时间专栏/持续交付36讲/构建集成/14 | 如何做到构建的提速,再提速!.md
Normal file
139
极客时间专栏/持续交付36讲/构建集成/14 | 如何做到构建的提速,再提速!.md
Normal file
@@ -0,0 +1,139 @@
|
||||
<audio id="audio" title="14 | 如何做到构建的提速,再提速!" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d9/f1/d9fa802928c98d618d124ee5cfc588f1.mp3"></audio>
|
||||
|
||||
在前面几篇文章中,我分享了很多关于构建的观点,然而天下武功唯为快不破,构建的速度对于用户持续交付的体验来说至关重要。
|
||||
|
||||
在实施持续交付的过程中,我们经常会遇到这样的情况:只是改了几行代码,却需要花费几分钟甚至几十分钟来构建。而这种情况,对于追求高效率的你我来说,是难以容忍的。
|
||||
|
||||
那么,今天我就带你一起看看,还有哪些手段可以帮助构建提速。
|
||||
|
||||
## 升级硬件资源
|
||||
|
||||
构建是一个非常耗时的操作,常常会成为影响持续交付速度的瓶颈。原因是,构建过程,会直接消耗计算资源,而且很多构建对硬件的要求也非常高。那么,升级硬件资源就是构建过程提速的最为直接有效的方式。
|
||||
|
||||
需要注意的是,这里的硬件资源包括 CPU、内存、磁盘、网络等等,具体升级哪一部分,需要具体情况具体分析。
|
||||
|
||||
比如,你要构建一个 C 语言程序,那么 CPU 就是关键点。你可以增加 CPU 的个数或者提升 CPU 主频以实现更快的编译速度。
|
||||
|
||||
再比如,你要用Maven构建一个 Java 应用,除了 CPU 之外,Maven 还会从中央仓库下载依赖写在本地磁盘。这时,网络和磁盘的 I/O 就可能成为瓶颈,你可以通过增加网络带宽提升网络吞吐,使用 SSD 代替机械硬盘增加磁盘 I/O ,从而到达提升整个构建过程速度的目的。
|
||||
|
||||
**总之,当你使用成熟的构建工具进行构建时,如果无法通过一些软件技术手段提升软件本身的构建速度,那么根据构建特点,有针对性地升级硬件资源,是最简单粗暴的方法。**
|
||||
|
||||
## 搭建私有仓库
|
||||
|
||||
构建很多时候是需要下载外部依赖的,而网络 I/O 通常会成为整个构建的瓶颈。尤其在当前网络环境下,从外网下载一些代码或者依赖的速度往往是瓶颈,所以在内网搭建各种各样的私有仓库就非常重要了。
|
||||
|
||||
目前,我们需要的依赖基本上都可以搭建一套私有仓库,比如:
|
||||
|
||||
- 使用 createrepo 搭建CentOS 的 yum 仓库;
|
||||
- 使用 Nexus 搭建 Java 的 Maven 仓库;
|
||||
- 使用cnpm搭建 NodeJS 的 npm 仓库;
|
||||
- 使用 pypiserver 搭建 Python 的 pip 仓库;
|
||||
- 使用 GitLab 搭建代码仓库;
|
||||
- 使用 Harbor 搭建 Docker 镜像仓库
|
||||
- ……
|
||||
|
||||
除了提升构建时的下载速度外,更重要的是,你还可以用这些工具存储辛勤工作的成果,保护知识产权。
|
||||
|
||||
总之,搭建私有仓库一定物超所值。当然,维护和管理这一大批工具需要投入不少人力和经济成本,在公司/团队没有成一定规模的前提下,会有一定的负担。
|
||||
|
||||
所以,**如果你的团队暂时没有条件自己搭建私有仓库的话,可以使用国内已有的一些私有仓库,来提升下载速度。当然,在选择私有仓库时,你要尽量挑选那些被广泛使用的仓库,避免安全隐患。**
|
||||
|
||||
## 使用本地缓存
|
||||
|
||||
虽然搭建私有仓库可以解决代码或者依赖下载的问题,但是私有仓库不能滥用,还是要结合构建机器本地的磁盘缓存才能达到利益最大化。
|
||||
|
||||
如果每次依赖拉取都走一次网络下载,一方面网络下载的速度通常会比本地磁盘慢很多,另一方面在构建量很大时,并发请求会导致私有仓库出现网卡打爆或者出现莫名其妙的异常,从而导致所有的构建过程变得不稳定,甚至影响其他工具的使用。
|
||||
|
||||
所以,妥善地用好本地缓存十分重要。这里说的“妥善”,主要包括以下两个方面:
|
||||
|
||||
<li>
|
||||
对于变化的内容,增量下载;
|
||||
</li>
|
||||
<li>
|
||||
对于不变的内容,不重复下载。
|
||||
</li>
|
||||
|
||||
目前,很多工具都已经支持这两点了。
|
||||
|
||||
对于第一点,项目的源码是经常变化的内容,下载源码时,如果你使用 Git 进行增量下载,那么就不需要在每次构建时都重复拉取所有的代码。Jenkins 的 Git 插件,也默认使用这种方式。
|
||||
|
||||
对于第二点,Maven 每次下载依赖后都会在本地磁盘创建一份依赖的拷贝,在构建下载之前会先检查本地是否已经有依赖的拷贝,从而达到复用效果。并且,这个依赖的拷贝是公共的,也就是说每个项目都可以使用这个缓存,极大地提升了构建效率。
|
||||
|
||||
如果你使用 Docker,那么你可以在宿主机上 mount 同一个依赖拷贝目录到多个 Slave 容器上,这样多个容器就可以共享同一个依赖拷贝目录。你可以最大程度地利用这一优势,但要注意不要让宿主机的磁盘 I/O 达到瓶颈。
|
||||
|
||||
## 规范构建流程
|
||||
|
||||
程序员的祖训说:Less is More,Simple is Better,这与大道至简的含义不谋而合。
|
||||
|
||||
程序的追求是简约而不简单,但随着业务越来越复杂,构建过程中各种各样的需求也随之出现,虽然工具已经封装了很多实用的功能,但是很多情况下,你都需要加入一些自定义的个性化功能,才能满足业务需求。
|
||||
|
||||
在携程,Java 构建过程中就有大量的额外逻辑,比如 Enforcer 检查、框架依赖检查、Sonar 检查、单元测试、集成测试等等,可以说是无所不用其极地去保证构建产物的质量。
|
||||
|
||||
因此,当前复杂的构建过程再也回不到仅仅一条 mvn 或者 gcc 命令就能搞定的年代。而这一套复杂的流程下来必定会花费不少时间,让程序员们有更多喝茶和去厕所的时间。
|
||||
|
||||
追求高效的同时,又不舍弃这些功能,是一个现实而又矛盾的命题,我们能否做到二者兼顾呢?答案,当然肯定的。
|
||||
|
||||
以Java 构建为例,Enforcer 检查、框架依赖检查、Sonar 检查、单元测试、集成测试这些步骤,并没有放在同一个构建过程中同步执行,而是通过异步的方式穿插在 CI/CD 当中,甚至可以在构建过程之外执行。
|
||||
|
||||
比如, Sonar 扫描在代码集成阶段执行,用户在 GitLab 上发起一个合并请求(Merge Request),这时只对变更的代码进行对比 Sonar 扫描,只要变更代码检查没有问题,那么就可以保证合并之后主干分支的代码也是没问题的。
|
||||
|
||||
所以,用户发布时就无需再重复检查了,只要发布后更新远端 Sonar Qube 的数据即可,同时,这个过程完全不会影响用户的构建体验。
|
||||
|
||||
通过以上一些规范构建流程的做法,可以进一步提高构建速度。
|
||||
|
||||
## 善用构建工具
|
||||
|
||||
正如我前面所说的,目前很多构建工具已经具备了非常多的功能来帮助我们更好地进行构建,因此,充分理解并用好这些功能就成了我们必须要掌握的武林绝学。
|
||||
|
||||
以 Maven 为例,我来带你看看有哪些提速方式,当然其他的构建工具,如Gradle等也都可以采用类似的方法:
|
||||
|
||||
<li>
|
||||
**设置合适的堆内存参数。** 过小的堆内存参数,会使Maven增加 GC 次数,影响构建性能;过大的堆内存参数,不但浪费资源,而且同样会影响性能。因此,构建时,你需要反复试验,得到最优的参数。
|
||||
</li>
|
||||
<li>
|
||||
**使用 -Dmaven.test.skip = true 跳过单元测试。** Maven默认的编译命令是 mvn package,这个命令会自动执行单元测试,但是通常我们的构建机器无法为用户提供一套完整的单元测试环境,特别是在分布式架构下。因此如果单元测试需要服务依赖,则可以去掉它。
|
||||
</li>
|
||||
<li>
|
||||
**在发布阶段,不使用 Snapshot 版本的依赖。** 这就可以在 Maven 构建时不填写 -U 参数来强制更新依赖的检查,省下因为每次检查版本是否更新而浪费的时间。
|
||||
</li>
|
||||
<li>
|
||||
**使用 -T 2C 命令进行并行构建。** 在该模式下 ,Maven 能够智能分析项目模块之间的依赖关系,然后并行地构建那些相互间没有依赖关系的模块,从而充分利用计算机的多核 CPU 资源。
|
||||
</li>
|
||||
<li>
|
||||
**局部构建。** 如果你的项目里面有多个没有依赖关系的模块,那么你可以使用 -pl 命令指定某一个或几个模块去编译,而无需构建整个项目,加快构建速度。
|
||||
</li>
|
||||
<li>
|
||||
**正确使用clean参数。** 通常情况下,我们建议用户在构建时使用 clean 参数保证构建的正确性。clean 可以删除旧的构建产物,但其实我们大多数时间可能不需要这个参数,只有在某些情况下(比如,更改了类名,或者删除了一些类)才必须使用这个参数,所以,如果某次变更只是修改了一些方法,或者增加了一些类,那么就不需要强制执行 clean 了。
|
||||
</li>
|
||||
|
||||
总之,如果你能熟练运用各种构建工具,那么你的效率一定会比其他人高,你的构建速度一定比其他人快。
|
||||
|
||||
## 总结
|
||||
|
||||
我介绍了五种常见的构建提速的方式,分别是:
|
||||
|
||||
<li>
|
||||
升级硬件资源,最直接和粗暴的提速方式;
|
||||
</li>
|
||||
<li>
|
||||
搭建私有仓库,避免从外网下载依赖;
|
||||
</li>
|
||||
<li>
|
||||
使用本地缓存,减少每次构建时依赖下载的消耗;
|
||||
</li>
|
||||
<li>
|
||||
规范构建流程,通过异步方式解决旁支流程的执行;
|
||||
</li>
|
||||
<li>
|
||||
善用构建工具,根据实际情况合理发挥的工具特性。
|
||||
</li>
|
||||
|
||||
然而,每个公司持续交付的构建流程不太一样,面临的问题与挑战也都不太一样,所以在优化前,一定要先了解问题原因,再对症下药。
|
||||
|
||||
## 思考题
|
||||
|
||||
你所在公司的构建流程是什么样的?是否也面临性能的问题?你又是是如何解决这些问题的?
|
||||
|
||||
欢迎你给我留言。
|
||||
|
||||
|
||||
152
极客时间专栏/持续交付36讲/构建集成/15 | 构建检测,无规矩不成方圆.md
Normal file
152
极客时间专栏/持续交付36讲/构建集成/15 | 构建检测,无规矩不成方圆.md
Normal file
@@ -0,0 +1,152 @@
|
||||
<audio id="audio" title="15 | 构建检测,无规矩不成方圆" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/8e/4e764dc95e8dd9fec14404e6ba8a1a8e.mp3"></audio>
|
||||
|
||||
在这个专栏的第5篇文章《手把手教你依赖管理》中,我介绍了构建 Java 项目的一些最佳实践,同时也给你抛出了一个问题:如果用户偷懒不遵循这些规范该怎么办?
|
||||
|
||||
所谓没有规矩不成方圆,构建是持续交付过程中非常重要的一步,而好的构建检测则可以直接提升交付产物的质量,使持续交付的流水线又快又稳。所以,也就有了 Maven 构建中的大杀器:Maven Enforcer 插件。
|
||||
|
||||
## 什么是 Maven Enforcer 插件?
|
||||
|
||||
Maven Enforcer 插件提供了非常多的通用检查规则,比如检查 JDK 版本、检查 Maven 版本、检查依赖版本,等等。下图所示就是一个简单的使用示例。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/86/cc332605af04d20bda53d44f8d16cc86.png" alt="" />
|
||||
|
||||
上述的配置会在构建时(准确的说是在 validate 时)完成三项检查:
|
||||
|
||||
- requireMavenVersion检查 Maven 版本必须大于 3.3.9;
|
||||
- requireJavaVersion检查 JDK 版本必须大于等于 1.9;
|
||||
- requireOS检查 OS 必须是 Windows 系统。
|
||||
|
||||
如果你使用 Java 1.8, Maven 3.3.3, 在 Linux 上构建, 便会出现如下的错误:
|
||||
|
||||
- Rule 0: org.apache.maven.plugins.enforcer.RequireMavenVersion failed with message: Detected Maven Version: 3.3.3 is not in the allowed range 3.3.9.
|
||||
- Rule 1: org.apache.maven.plugins.enforcer.RequireJavaVersion failed with message: Detected JDK Version: 1.8.0-77 is not in the allowed range 1.9.
|
||||
- Rule 2: org.apache.maven.plugins.enforcer.RequireOS failed with message: OS Arch: amd64 Family: unix Name: linux Version: 3.16.0-43-generic is not allowed by Family=windows
|
||||
|
||||
从而导致构建失败。
|
||||
|
||||
那么,是否有办法在所有应用的构建前都执行Enforcer的检查呢。
|
||||
|
||||
我在专栏的第5篇文章《手把手教你依赖管理》中,也已经介绍了在携程内部,一般 Java 应用的继承树关系,每个项目都必须继承来自技术委员会或公司层面提供的 super-pom。携程在 super-pom 之上又定义了一层 super-rule 的 pom,这个pom 中定义了一系列的 Enforcer 规则。 这样,只要是集成了 super-pom 的项目,就会在构建时自动运行我们所定义的检查。
|
||||
|
||||
也许你会问了, 如果用户不继承 super-pom 是不是就可以跳过这些规则检查了?是的, 继承 super-pom 是规则检查的前提。
|
||||
|
||||
但是,我们不会给用户这样的机会, 因为上线走的都是统一的构建系统。
|
||||
|
||||
构建系统在构建之前会先检查项目的继承树,继承树中必须包含 super-pom, 否则构建失败。并且,构建系统虽然允许用户自定义 Maven 的构建命令,但是会将 Enforcer 相关的参数过滤掉,用户填写的任何关于Enforcer的参数都被视为无效。Enforcer会被强制按照统一标准执行,这样就保证了所有应用编译时都要经过检查。
|
||||
|
||||
因为携程的构建系统只提供几个版本的 Java 和 Maven,并且操作系统是统一的 Linux CentOS版本,所以就不需要使用之前例子中提到的三个检查,一定程度的缩小标准化范围,也是有效的质量保证手段。
|
||||
|
||||
了解了Maven Enforcer插件,我再从Maven Enforcer内置的规则、自定义的Enforcer检查规则,以及构建依赖检查服务这三个方面,带你一起看看构建监测的“豪华套餐”,增强你对交付产物的信心。
|
||||
|
||||
## 丰富的内置的 Enforcer 规则
|
||||
|
||||
Maven Enforcer 提供了非常丰富的内置检查规则,在这里,我给你重点介绍一下bannedDependencies 规则、dependencyConvergence 规则,和banDuplicateClasses 规则。
|
||||
|
||||
**第一,bannedDependencies 规则**
|
||||
|
||||
**该规则表示禁止使用某些依赖,或者某些依赖的版本**,使用示例:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/ba/27f936902309c3ee2e71a8dd007018ba.png" alt="" />
|
||||
|
||||
该代码检查的逻辑是,只允许使用版本大于等于 1.8.0 的 org.slf4j:slf4j-api 依赖,否则将会出现如下错误:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/8b/8feb3a6d98ed61748525dd53dcadf48b.png" alt="" />
|
||||
|
||||
bannedDependencies 规则的常见应用场景包括:
|
||||
|
||||
<li>
|
||||
当我们知道某个 jar 包的某个版本有严重漏洞时,可以用这种方法禁止用户使用,从而避免被攻击;
|
||||
</li>
|
||||
<li>
|
||||
某个公共组件的依赖必须要大于某个版本时,你也可以使用这个方法禁止用户直接引用不兼容的依赖版本,避免公共组件运行错误。
|
||||
</li>
|
||||
|
||||
**第二,dependencyConvergence 规则**
|
||||
|
||||
在《手把手教你依赖管理》一文中,我介绍了Maven 的依赖仲裁的两个原则:最短路径优先原则和第一声明优先原则。
|
||||
|
||||
但是,Maven 基于这两个原则处理依赖的方式过于简单粗暴。毕竟在一个成熟的系统中,依赖的关系错综复杂,用户很难一个一个地排查所有依赖的关系和冲突,稍不留神便会掉进依赖的陷阱里,这时 dependencyConvergence 就可以粉墨登场了。
|
||||
|
||||
**dependencyConvergence规则的作用是: 当项目中的 A 和 B 分别引用了不同版本的C时, Enforce 检查失败。** 下面这个实例,可以帮你理解这个规则的作用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/d0/ef9194165537330d5d8e0bbc6ce1ded0.png" alt="" />
|
||||
|
||||
org.slf4j:slf4j-jdk14:1.6.1依赖了 org.slf4j:slf4j-api:1.6.1, 而 org.slf4j:slf4j-nop:1.6.0依赖了 org.slf4j:slf4j-api:1.6.0,当我们在构建项目时, 便会有如下错误:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/8d/e551db93e22b7e1b077025136d9b8f8d.png" alt="" />
|
||||
|
||||
这时就需要开发人员介入了,使用 dependecy 的 exclusions 元素排除掉一个不合适的版本。 虽然这会给编程带来一些麻烦, 但是非常必要。因为,我始终认为你应该清楚地知道系统依赖了哪些组件, 尤其是在某些组价发生冲突时,这就更加重要了。
|
||||
|
||||
**第三,banDuplicateClasses 规则**
|
||||
|
||||
**该规则是 Extra Enforcer Rules 提供的,主要目的是检查多个jar 包中是否存在同样命名的 class,如果存在编译便会报错。** 同名 class 若内容不一致,可能会导致 java.lang.NoSuchFieldError,java.lang.NoSuchMethodException 等异常,而且排查起来非常困难,因为人的直觉思维很难定位到重复类这个非显性错误上,例如下面这种情况:
|
||||
|
||||
org.jboss.netty包与io.netty包中都包含一个名为NettyBundleActivator的类,另外还有2个重复类:spring/NettyLoggerConfigurator 和 microcontainer/NettyLoggerConfigurator。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/55/f2a50ca7bc448a6673fb6bef5a925055.png" alt="" />
|
||||
|
||||
当激活了 banDuplicateClasses 规则之后,Enforcer检查,便会有如下的报错:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/90/417ae44052d9cdf0a1e083910cab1990.png" alt="" />
|
||||
|
||||
通常情况下,用户需要排除一个多余的 jar 包来解决这个问题,但有些情况下两个 jar 包都不能被排除,如果只是个别类名冲突了,那么可以通过 ignoreClasses 去忽略冲突的类,类名可以使用通配符(*),如: org.jboss.netty.container.*。
|
||||
|
||||
但是,用户不能随意更改这个配置,因为它必须得到一定的授权,否则随意忽略会产生其他不确定的问题。因此我们将这个插件做了一些改动,通过API来获取 ignoreClasses 的内容。当用户有类似的需求时,可以提交 ignoreClasses ,但必须申请,经过 Java 专家审批之后才可忽略掉。
|
||||
|
||||
## 自定义的 Enforcer 检查规则
|
||||
|
||||
除了上述的官方规则,实际上携程还做了若干个扩展的规则,如:
|
||||
|
||||
<li>
|
||||
CheckVersion,用于检查模块的版本号必须是数字三段式,或者带有 SNAPSHOT 的数字三段式;
|
||||
</li>
|
||||
<li>
|
||||
CheckGroupId,用于检查 GroupId 是否符合规范,我们为每个部门都分别指定了GroupId;
|
||||
</li>
|
||||
<li>
|
||||
CheckDistributionManagementRepository,用于检查项目的 distributionManagement 中的 repository 节点,并为每个部门都指定了他们在 Nexus 上面的 repositroy;
|
||||
</li>
|
||||
<li>
|
||||
CheckSubModuleSaveVersion,用于检查子模块版本号是否与父模块版本号一致。
|
||||
</li>
|
||||
|
||||
以上,便是携程基于 Maven Enforcer 在构建检查上的一些实践,你可以借鉴使用。
|
||||
|
||||
但是,有时候 Maven Enforcer 也无法满足我们所有的需求,比如,它无法完成非 Java 项目的检查。因此,我们还有一个通用的依赖检查服务。
|
||||
|
||||
## 构建依赖检查服务
|
||||
|
||||
其他语言, 比如 C#,NodeJS 等,没有 Maven Enforcer 这样成熟的工具来做构建时的依赖检查。对于这类语言我们的做法是:构建后,收集该项目所有的依赖及其版本号,将这些数据发送给依赖检查服务 Talos,Talos 根据内置的规则进行依赖检查。Talos是一套携程自研的,独立的,组件依赖检查系统,其中包含的检查逻辑,完全可以自由定义。
|
||||
|
||||
而且,Talos依赖检查的逻辑更新非常灵活,可以直接在平台内使用 Java 代码在线编写检查逻辑,提交后便可实时生效。
|
||||
|
||||
以下是一段 .NET 项目检查逻辑的示例代码:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/95/3db4fbd6d9f1118fa5fb794e972c9d95.png" alt="" />
|
||||
|
||||
该逻辑的含义是: 当项目的依赖存在 foo.dll 和 bar.dll 时,bar.dll 的版本号必须大于 1.0.0.0。看, 是不是非常方便快捷通用!
|
||||
|
||||
这样一套组合拳下来,构建检测以及项目依赖的问题已不再那么让人望而生畏了。因此,工欲善其事必先利其器, 好的工具可以解放大量的生产力,最重要的是构建检测后的交付让你我更有信心了。有条不紊的流程与规范,就像一列高速列车下的枕木,时刻保证着整个系统稳定而可靠地推进。
|
||||
|
||||
## 总结与实践
|
||||
|
||||
我围绕着构建检测,和你一起学习并介绍了:
|
||||
|
||||
<li>
|
||||
Maven Enforcer 插件可以帮我们更好地完成编译检测;
|
||||
</li>
|
||||
<li>
|
||||
可以使用内置的 Maven Enforcer 规则,覆盖常规检测;
|
||||
</li>
|
||||
<li>
|
||||
可以使用自定义 Maven Enforcer 检查规则的方式,增加版本号规则等的检查;
|
||||
</li>
|
||||
<li>
|
||||
Maven Enforcer 之外,你还可以自己丰富一些例如依赖版本检测这样的服务,以提高检测效果。
|
||||
</li>
|
||||
|
||||
Maven Enforcer 提供了非常丰富的内置检查规则,感兴趣的话,你可以通过 [https://maven.apache.org/enforcer/enforcer-rules/index.html](https://maven.apache.org/enforcer/enforcer-rules/index.html) 以及 [http://www.mojohaus.org/extra-enforcer-rules/](http://www.mojohaus.org/extra-enforcer-rules/) 逐个尝试这些规则,并说说哪些规则是你工作总最最需要的。
|
||||
|
||||
欢迎你给我留言。
|
||||
|
||||
|
||||
165
极客时间专栏/持续交付36讲/构建集成/16 | 构建资源的弹性伸缩.md
Normal file
165
极客时间专栏/持续交付36讲/构建集成/16 | 构建资源的弹性伸缩.md
Normal file
@@ -0,0 +1,165 @@
|
||||
<audio id="audio" title="16 | 构建资源的弹性伸缩" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/87/8f/87044e399a97184218a75a24afa9288f.mp3"></audio>
|
||||
|
||||
在前面的文章中,我已经介绍了构建在整个持续交付过程中扮演的重要角色,并且详细讨论了依赖管理和构建检测等方面的内容。在这篇文章中,我将带你搭建一套高可用、高性能的构建系统。
|
||||
|
||||
## 持续集成工具
|
||||
|
||||
目前市面上已经有很多持续集成工具了,它们已经替我们解决了很多实际问题,所以我们也就没有必要去再重复造轮子了。这些持续集成工具,最流行的应属 Travis CI、Circle CI、Jenkins CI这三种。
|
||||
|
||||
**第一,Travis CI**
|
||||
|
||||
Travis CI 是基于GitHub的CI托管解决方案之一,由于和GitHub的紧密集成,在开源项目中被广泛使用。
|
||||
|
||||
Travis CI 的构建,主要通过 .travis.yml 文件进行配置。这个 .travis.yml 文件描述了构建时所要执行的所有步骤。
|
||||
|
||||
另外,Travis CI 可以支持市面上绝大多数的编程语言。但是,因为Travis只支持GitHub,而不支持其他代码托管服务,所以官方建议在使用前需要先具备以下几个条件:
|
||||
|
||||
<li>
|
||||
能登录到GitHub;
|
||||
</li>
|
||||
<li>
|
||||
对托管在GitHub上的项目有管理员权限;
|
||||
</li>
|
||||
<li>
|
||||
项目中有可运行的代码;
|
||||
</li>
|
||||
<li>
|
||||
有可以工作的编译和测试脚本。
|
||||
</li>
|
||||
|
||||
Travis CI的收费策略是,对公共仓库免费,对私有仓库收费。
|
||||
|
||||
**第二,CircleCI**
|
||||
|
||||
CircleCI 是一款很有特色,也是比较流行的,云端持续集成管理工具。CircleCI 目前也仅支持 GitHub 和Bitbucket管理。
|
||||
|
||||
CircleCI 与其他持续集成工具的区别在于,它们提供服务的方式不同。CircleCI 需要付费的资源主要是它的容器。
|
||||
|
||||
你可以免费使用一个容器,但是当你发现资源不够需要使用更多的容器时,你必须为此付费。你也可以选择你所需要的并行化级别来加速你的持续集成,它有 5 个并行化级别(1x、4x、8x,、12x,和16x)可供选择,分别代表利用几个容器同时进行一个项目的构建,如何选择就取决于你了。
|
||||
|
||||
**第三,Jenkins CI**
|
||||
|
||||
Jenkins 是一款自包含、开源的用于自动化驱动编译、测试、交付或部署等一系列任务的自动化服务,它的核心是Jenkins Pipline 。Jenkins Pipline可以实现对持续交付插件的灵活组合,以流水线的方式接入到Jenkins服务。
|
||||
|
||||
Jenkins 还提供了一整套可扩展的工具集,程序员可以通过代码的方式,定义任何流水线的行为。另外,经过多年的发展,Jenkins已经包含了很多实用的第三方插件,覆盖了持续交付的整个生命周期。
|
||||
|
||||
目前,绝大多数组织都选择了 Jenkins 作为内部的持续集成工具,主要原因是:
|
||||
|
||||
- 代码开源, 插件完善,系统稳定;
|
||||
- 社区活跃,成功实践与网上资源比较丰富;
|
||||
- Jenkins Pipeline 非常灵活好用。
|
||||
|
||||
大致了解了集成工具之后,携程和绝大部分企业一样,选择了最开放、最易于扩展的Jenkins作为集成构建的引擎,而且分别从实现横向的Master高可用和纵向的Slave弹性伸缩两方面,使构建系统更为强大和高效。
|
||||
|
||||
## Jenkins Master 高可用架构的
|
||||
|
||||
目前普遍的 Jenkins 搭建方案是:一个 Jenkins Master 搭配多个 Jenkins Slave。大多数情况下,这种方案可以很好地工作,并且随着构建任务的增加,无脑扩容 Jenkins Slave 也不是一件难事。另外,不管是 Linux Slave 还是 Windows Slave ,Jenkins 都可以很好地支持,并且非常稳定。
|
||||
|
||||
但是,随着业务的增长,微服务架构的流行,持续交付理念的深入人心,构建会变得越来越多,越来越频繁,单个 Jenkins Master 终究会成为系统中的瓶颈。
|
||||
|
||||
遗憾的是,开源的 Jenkins 并没有给我们提供一个很好的 Master 高可用方案,CloudBees 公司倒是提供了一个高可用的插件,但是价格不菲。
|
||||
|
||||
所以,为了鱼与熊掌兼得,最终携程决定自己干。下面是我们构建系统的基本架构:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/7d/6c084ce3200db08c78db5bbc027b8b7d.png" alt="">
|
||||
|
||||
携程的解决思路是在 Jenkins 上面再封装两层: Build Service 暴露构建的 HTTP 接口,接收请求后将任务丢给异步队列 Build Worker,Build Worker 根据不同的策略将任务分发给符合条件的 Jenkins Master。
|
||||
|
||||
这里的分发条件,可以是编译任务的平台或语言,比如可以将基于 Windows 和 Linux 的任务分别放在不同的 Jenkins Master 上,也可以将 Java 构建和 NodeJS 构建任务放在不同的 Jenkins Master 上。
|
||||
|
||||
除此之外,携程的这个构建系统还可以满足的一种需求是:一些比较复杂且重要的业务线,有时也会提出独立构建资源的需求,以达到独占编译资源的目的。
|
||||
|
||||
总而言之,构建任务分发的策略可以是非常灵活的:构建 Worker 和 Jenkins Master 之间有“心跳监测”,可以时刻检查 Jenkins Master 是否还健康,如果有问题就将任务分发到其他等价的 Jenkins Master 上,并给相关人员发送告警通知。
|
||||
|
||||
这种拆解 Jenkins Master 主要有以下几个好处:
|
||||
|
||||
<li>
|
||||
每个 Job 都可运行在至少两个 Jenkins Master 之上, 保证高可用;
|
||||
</li>
|
||||
<li>
|
||||
根据不同的策略将 Job 做 Sharding, 避免积压在同一个 Master 上;
|
||||
</li>
|
||||
<li>
|
||||
Jenkins Master 按需配置,按需安装不同的插件,便于管理。
|
||||
</li>
|
||||
|
||||
利用这套方案,携程就可以做到Master层面的伸缩了。这套方案的实现成本并不是很大,简单易懂,小团队也完全可以掌握和实施。
|
||||
|
||||
## Jenkins Slave 弹性伸缩方案
|
||||
|
||||
解决了Jenkins Master的高可用问题,接着就要去思考如何才能解决Slave资源管理和利用率的问题了。因为,你会发现一个组织的集成和构建往往是周期性的,高峰和低谷都比较明显,而且随着组织扩大,幅度也有所扩大。所以,如果按照高峰的要求来配备Slave实例数,那么在低谷时,就很浪费资源了。反之,又会影响速度,造成排队。
|
||||
|
||||
因此,我们需要整个Slave集群具有更优的弹性:既要好管理,又要好扩展。在携程,我们尝试过多种虚拟机方案,比如全Windows类型、金映象方案等等。最后,根据容器的特性,选择了容器作为解决方案。期间也经历了Mesos 到 K8s的两套方案。
|
||||
|
||||
**第一,最初的虚拟机**
|
||||
|
||||
在最初构建种类不多并且场景不复杂的情况下,我们的 Slave 全部使用 Windows 虚拟机。
|
||||
|
||||
我们把所需的构建软件像大杂烩一样一股脑地安装在虚拟机上,比如,编译 .NET 所需的 MSBuild ,编译 Java 所需的 Maven。我们维护着一份啰嗦冗长的安装手册,并小心翼翼地这些文档保存在服务器上。
|
||||
|
||||
这时,最怕的就是构建环境的变更,比如某个软件要升级,要添加对某个新软件的支持。这些变更需要我们对所有机器的操作都重来一遍,甚至还需要关机重启,十分费时折腾。
|
||||
|
||||
后来,我们尝试了将虚拟机维护成镜像,并使用 SaltStack 做自动化变更。虽然日子好过了点,但升级一次环境还是需要投入不少人工成本。另外,文档的更新始终一件苦差事,从来不敢怠慢就怕某次变更没有记录在案。
|
||||
|
||||
你我都清楚,写文档从来都不像写代码那么舒服。程序员最讨厌的两件事也都和文档相关:一是给自己的软件写文档,二是别人的软件没有文档。
|
||||
|
||||
这让我们非常困扰,我们做的是 DevOps 与持续交付,但是自己的工具管理却如此混乱无章,这也使我们感觉十分蒙羞与窘迫。
|
||||
|
||||
**第二,容器化的甜头**
|
||||
|
||||
随着容器越来越流行,我们发现:使用容器镜像保存构建环境是一个非常不错的选择。相对于虚拟机,容器技术主要有以下几个优势:
|
||||
|
||||
<li>
|
||||
使用 Dockerfile 描述环境信息相对于之前的文档更加直观,并且可以很自然地跟 Git 结合做到版本化控制,先更新 Dockerfile 再更新镜像是很自然的事。
|
||||
</li>
|
||||
<li>
|
||||
镜像更容易继承,你可以配置一个 Base 镜像,然后根据不同的需求叠加软件。比如,你的所有构建都需要安装 Git 等软件,那么就可以把它写到 Base 镜像里面。
|
||||
</li>
|
||||
<li>
|
||||
Docker 镜像可以自由控制,开发人员可以自己推送镜像,快速迭代。重建容器的代价比重建虚拟机小得多,容器更加轻量,更容易在本地做测试。
|
||||
</li>
|
||||
|
||||
目前,携程的构建系统已经支持了包括: Java, NodeJS,Golang,Erlang,Python 等多种语言的构建,并且维护起来非常轻松,完全没有负担。
|
||||
|
||||
在尝到了 Linux 容器带来的甜头之后,我们毅然决然地开始研究 Windows 容器技术。经过不断地尝试与探索,终于把它应用到了生产环境,并且取得了非常不错的效果,目前为止运行也十分稳定。
|
||||
|
||||
**第三,让资源弹起来**
|
||||
|
||||
容器化在很大程度上解决了运维成本的问题,虽然通过 Docker 管理容器比虚拟机要方便一些,但是管理大量的容器却也没那么得心应手。
|
||||
|
||||
此外,我们之前使用容器的方式几乎和使用虚拟机一样,也就是所谓的 “胖容器”,一旦创建,不管用不用,它都在那里。而构建是一个周期性的行为,一般跟着程序员的工作时间走:工作日比周末多,白天比晚上多,甚至还有明显的午饭和晚饭空闲期。
|
||||
|
||||
后来 Mesos 与 Kubernetes等主流的容器集群管理工具渐渐浮出水面,出现在我们的视野中。
|
||||
|
||||
基于 Borg 成熟经验打造的 Kubernetes,为容器编排管理提供了完整的开源方案,并且社区活跃,生态完善,积累了大量分布式、服务化系统架构的最佳实践。在2017 年, 携程尝试将 Jenkins 和Kubernetes集成在了一起 。
|
||||
|
||||
目前,Jenkins 社区已经提供了一个Kubernetes插件,而且是免费的,使得 Jenkins 与 K8s 的集成变得非常简单轻松。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/18/4a14b6be6f0cc000ff521ca61c8ad318.png" alt="">
|
||||
|
||||
上图就是我们某台宿主机 24 小时的构建数量与所创建容器的对比图,可以看出两条曲线呈现的趋势基本是一致的。我们在夜晚闲置的资源上,调度了一些其他的离线 Job,大幅提升了资源利用率。
|
||||
|
||||
所以,携程利用容器技术,也顺利实现了Slave节点的弹性伸缩。对于中小型企业,初期完全可以利用Jenkins及其Kubernetes插件,做到Slave节点的资源弹性伸缩。至于与离线Job混部,因为要考虑的因素较多,可以在应用容器化之后再考虑。
|
||||
|
||||
## 总结与实践
|
||||
|
||||
我主要介绍了几种流行的持续集成工具,以及基于 Jenkins 的高可用构建系统的一些基本设计理念和我们系统的演变过程。
|
||||
|
||||
<li>
|
||||
通常建议使用成熟的CI产品(比如,Travis CI、Circle CI、Jenkins CI)来作为平台的基础;
|
||||
</li>
|
||||
<li>
|
||||
虽然这些CI工具是成熟产品,但面对日新月异的技术需求,高可用和伸缩问题还是要自己解决;
|
||||
</li>
|
||||
<li>
|
||||
通过请求分发等设计,可以实现Master节点的横向伸缩及高可用问题;
|
||||
</li>
|
||||
<li>
|
||||
利用容器技术,可以解决Salve节点的弹性伸缩和资源利用率问题。
|
||||
</li>
|
||||
|
||||
最后,你可以尝试搭建一套 Jenkins 与Kubernetes 服务,让你的任务跑在动态创建出来的容器上,并思考一下这个方案有没有什么缺点和不足。
|
||||
|
||||
感谢你的收听,欢迎你给我留言。
|
||||
|
||||
|
||||
186
极客时间专栏/持续交付36讲/构建集成/17 | 容器镜像构建的那些事儿.md
Normal file
186
极客时间专栏/持续交付36讲/构建集成/17 | 容器镜像构建的那些事儿.md
Normal file
@@ -0,0 +1,186 @@
|
||||
<audio id="audio" title="17 | 容器镜像构建的那些事儿" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/54/b9f369de8001dbe63079e36f5fb88754.mp3"></audio>
|
||||
|
||||
随着容器发布越来越流行,持续交付最后一公里的产物,逐渐由之前的代码包变成了容器镜像。然而,容器镜像构建与传统的代码构建有很多不同之处,也增加了很多新鲜的技术领域和内容需要我们去学习。
|
||||
|
||||
所以,今天我们就一起来聊聊容器镜像构建的那些事儿,打通容器镜像构建的各个环节。
|
||||
|
||||
## 什么是容器镜像?
|
||||
|
||||
在虚拟机时代就有镜像的说法,当我们创建一个虚拟机时,通常会去网上下载一个ISO格式的虚拟机镜像,然后经过 VirtualBox 或者 VMware 加载,最终形成一个包含完整操作系统的虚拟机实例。
|
||||
|
||||
而容器镜像也是类似的意思,只不过它不像虚拟机镜像那么庞大和完整,它是一个只读的模板,一个独立的文件系统,包含了容器运行初始化时所需要的数据和软件,可以重复创建出多个一模一样的容器。
|
||||
|
||||
容器镜像可以是一个完整的 Ubuntu 系统,也可以是一个仅仅能运行一个 sleep 进程的独立环境,大到几 G 小到几 M。而且Docker 的镜像是分层的,它由一层一层的文件系统组成,这种层级的文件系统被称为 UnionFS。下图就是一个 Ubuntu 15.04 的镜像结构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/97/0f/97c622c247ef510b06fee1aff4ec890f.jpg" alt="" />
|
||||
|
||||
图中的镜像部分画了一个锁的标记,它表示镜像中的每一层都是只读的,只有创建容器时才会在最上层添加一个叫作Container layer的可写层。容器运行后的所有修改都是在这个可写层进行,而不会影响容器镜像本身。
|
||||
|
||||
因为这一特性,创建容器非常节省空间,因为一台宿主机上基于同一镜像创建的容器只有这一份镜像文件系统,每次创建多出来的只是每个容器与镜像 diff 的磁盘空间。而虚拟机每增加一个实例,都会在宿主机上占用一个完整的镜像磁盘空间。
|
||||
|
||||
## 什么是 Dockerfile
|
||||
|
||||
了解了什么是容器的镜像,以及与虚拟机镜像的区别后,可以清楚地看到:容器都是基于镜像产生的,没有镜像就没有容器。那么,我们应该怎么创建一个镜像呢?
|
||||
|
||||
[Docker Hub](https://hub.docker.com/)上提供了非常多的常用镜像,比如 Ubuntu 镜像,CentOS 镜像,或者仅仅是一个包含 Java 程序的镜像,你可以通过 docker pull 命令把它们下载到本地使用。当然你也可以自己在本地通过docker build 制作镜像。
|
||||
|
||||
如果你想要修改或者加工这些镜像,可以找到文件系统中对应的 layer 目录,然后进行修改。按照这种方式操作的话,如果我要添加一个文件还好说,但如果要安装一个软件,那就要拷贝一堆文件到各个目录中,相当麻烦。
|
||||
|
||||
如果真要这样操作的话,容器镜像也就不会有今天如此庞大的用户群体了。Docker帮我们解决这个问题的方式,就是提供了Dockerfile。
|
||||
|
||||
简单来说,**Dockerfile 第一个好处就是,可以通过文本格式的配置文件描述镜像,这个配置文件里面可以运行功能丰富的指令,你可以通过运行 docker build 将这些指令转化为镜像。**
|
||||
|
||||
比如,我要更改 Ubuntu 镜像安装一个 Vim 编辑器,那么我的 Dockerfile 可以这样写:
|
||||
|
||||
```
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get install vim -y
|
||||
|
||||
```
|
||||
|
||||
其中,FROM 指令说明我们这个镜像需要继承 Ubuntu 镜像,RUN 指令是需要在镜像内运行的命令。
|
||||
|
||||
因为 Ubuntu 镜像内包含了 apt-get 包管理器,所以相当于启动了一个 Ubuntu 镜像的容器,然后在这个容器内部安装 Vim。这期间会产生一个新的 layer,这个新的 layer 包含安装 Vim 所需的所有文件。
|
||||
|
||||
运行docker build 后会产生一个新镜像,我们可以通过 docker tag 给这个新镜像起一个名字,然后 docker push 到仓库,就可以从仓库下载这个镜像了,后续的其他镜像也可以继承这个镜像进行其他改动。
|
||||
|
||||
镜像就是这样通过 Dockerfile 一层一层的继承,不断增加新的内容,直到变成你想要的样子。
|
||||
|
||||
**Dockerfile 的另外一个好处就是可以描述镜像的变化**,通过一行命令就可以直观描述出环境变更的过程,如果再通过 git 进行版本控制,就可以让环境的管理更加可靠与简单。
|
||||
|
||||
了解了Dockerfile之后,你就可以利用它进行代码更新了,最主要的步骤就以下三步:
|
||||
|
||||
<li>
|
||||
将代码包下载到构建服务器;
|
||||
</li>
|
||||
<li>
|
||||
通过Dockerfile 的 ADD 命令将代码包加载到容器里;
|
||||
</li>
|
||||
<li>
|
||||
Docker build 完成新的镜像。
|
||||
</li>
|
||||
|
||||
## 镜像构建优化
|
||||
|
||||
原则上,我们总是希望能够让镜像保持小巧、精致,这样可以让镜像环境更加清晰,不用占用过多空间,下载也会更快。
|
||||
|
||||
那么,如何做好镜像的优化呢?你可以从3个方面入手:
|
||||
|
||||
<li>
|
||||
**选择合适的 Base 镜像;**
|
||||
</li>
|
||||
<li>
|
||||
**减少不必要的镜像层的产生;**
|
||||
</li>
|
||||
<li>
|
||||
**充分利用指令的缓存。**
|
||||
</li>
|
||||
|
||||
**为什么第一条说要选择合适的Base镜像呢?因为,这是最直接和有效的方式。**
|
||||
|
||||
举个例子就更好理解了。比如,我只想运行一个 Java 进程,那么镜像里就只有这个 Java 进程所需的环境就可以了,而没必要使用一个完整 Ubuntu 或者 CentOS 镜像。
|
||||
|
||||
**关于第二点,减少不必要的镜像层,是因为使用 Dockerfile 时,每一条指令都会创建一个镜像层,继而会增加整体镜像的大小。**
|
||||
|
||||
比如,下面这个 Dockerfile:
|
||||
|
||||
```
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get install vim -y
|
||||
|
||||
RUN apt-get remove vim -y
|
||||
|
||||
```
|
||||
|
||||
虽然这个操作创建的镜像中没有安装 Vim,但是镜像的大小和有 Vim是一样的。原因就是,每条指令都会新加一个镜像层,执行install vim 后添加了一层,执行remove vim 后也会添加一层,而这一删除命令并不会减少整个镜像的大小。
|
||||
|
||||
因此,当我们编写 Dockerfile 时,可以合并多个 RUN 指令,减少不必要的镜像层的产生,并且在之后将多余的命令清理干净,只保留运行时需要的依赖。就好比我买了两斤橘子,只需要把橘子肉保留下来就好,橘子皮可以直接丢掉,不用保留在房间里。
|
||||
|
||||
**Dockerfile 构建的另外一个重要特性是指令可以缓存,可以极大地缩短构建时间。** 因为之前也说了,每一个RUN都会产生一个镜像,而Docker在默认构建时,会优先选择这些缓存的镜像,而非重新构建一层镜像。比如,一开始我的 Dockerfile 如下:
|
||||
|
||||
```
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get install vim -y
|
||||
|
||||
```
|
||||
|
||||
使用一段时间之后,我发现需要添加新的特性,Dockerfile 变成了如下的样子:
|
||||
|
||||
```
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get install vim -y
|
||||
|
||||
ADD java /usr/local/java
|
||||
|
||||
```
|
||||
|
||||
重新 build 时,前面安装 Vim 那步可以使用缓存,而不需要重新运行。当我们需要构建一个新镜像时,这个特性非常有用,可以快速跳过前面构建通过的步骤,而不需要每次都重新构建,尤其适用于在 Docker 里面编译一些大型软件的情况,可以帮你节省大量时间。
|
||||
|
||||
## 镜像构建环境
|
||||
|
||||
当我们学会了使用 Dockerfile 构建镜像之后,下一步就是如何搭建构建环境了。搭建构建环境最简单的方式就是在虚拟机上安装 Docker Daemon,然后根据你所使用的语言提供的 Docker 客户端与 Docker Daemon 进行交互,完成构建。
|
||||
|
||||
但是,我们推崇构建环境容器化,因为我们的构建环境可能除了 Docker 外,还会有一些其他的依赖,比如编程语言、Git 等等。
|
||||
|
||||
上面我也分析了Docker 镜像的各种好处,那如果环境还没有实现容器化,是不是就有点说不过去了?
|
||||
|
||||
接下来,我们就看看构建环境如何实现容器化。一般情况下,用容器来构建容器镜像有两种方式:
|
||||
|
||||
<li>
|
||||
Docker Out Of Docker(DooD)
|
||||
</li>
|
||||
<li>
|
||||
Docker In Docker(DinD)
|
||||
</li>
|
||||
|
||||
**第一,Docker Out Of Docker(DooD)**
|
||||
|
||||
这种方式比较简单,首先在虚拟机上安装 Docker Daemon,然后将你的构建环境镜像下载下来启动一个容器。
|
||||
|
||||
在默认情况下,Docker 客户端都是通过 /var/run/docker.sock 与 Docker Daemon 进行通信。我们在创建 Docker 实例时,把外部的 /var/run/docker.sock mount 到容器内部,这样容器内的 Docker 客户端就可以与外部的 Docker Daemon 进行通信了。
|
||||
|
||||
另外,你还需要注意权限问题,容器内部的构建进程必须拥有读取 /var/run/docker.sock 的权限,才可以完成通信过程。
|
||||
|
||||
这种方式的好处很明显,我们可以将镜像构建环境打包复用,对宿主机来说,只要安装 Docker Daemon 就可以了。但是这种方式的缺点是,内部的环境必须要与外部保持一致,不然就会报错,比如缺少库文件。此外,如果构建容器时不小心把 Docker Daemon 搞挂了,那么就会影响该宿主机上的其他容器。
|
||||
|
||||
为了解决这个问题,我们是否可以在容器内部使用 Docker Daemon 呢?
|
||||
|
||||
**第二,Docker In Docker(DinD)**
|
||||
|
||||
Docker In Docker ,就是在容器内部启动一个完整的 Docker Daemon 进程,然后构建工具只需要和该进程交互,而不影响外部的 Docker 进程。
|
||||
|
||||
默认情况下,容器内部不允许开启 Docker Daemon 进程,必须在运行容器的时候加上 --privileged 参数,这个参数的作用是真正取得root的权限。另外,Docker 社区官方提供了一个 docker:dind 镜像可以直接拿来使用。
|
||||
|
||||
这样一来,容器内部 Docker Daemon 就和容器外部的 Docker Daemon 彻底分开了,容器内部就是一个完整的镜像构建环境,是不是很神奇。
|
||||
|
||||
然而 DinD 也不是百分之百的完美和健壮,它也有一些关于安全和文件系统的问题。此外,因为每个容器都有独立的 /var/lib/docker 用来保存镜像文件,一旦容器被重启了,这些镜像缓存就消失了,这可能会影响我们构建镜像的性能。
|
||||
|
||||
通过以上两个方法,你就可以做到用容器来构建容器镜像了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我针对容器镜像构建的那些事儿,和你进行了讨论。
|
||||
|
||||
首先,容器镜像是一个独立的文件系统,它包含了容器运行初始化时所需要的数据或软件。Docker容器的文件系统是分层的、只读的,每次创建容器时只要在最上层添加一个叫作Container layer的可写层就可以了。这种创建方式不同于虚拟机,可以极大的减少对磁盘空间的占用。
|
||||
|
||||
其次,Docker提供了Dockerfile这个可以描述镜像的文本格式的配置文件。你可以在Dockerfile中运行功能丰富的指令,并可以通过docker build 将这些指令转化为镜像。
|
||||
|
||||
再次,基于Dockerfile的特性,我分享了Dockerfile 镜像构建优化的三个建议,包括:选择合适的Base镜像、减少不必要的镜像层产生,以及善用构建缓存。
|
||||
|
||||
最后,用容器来构建容器镜像,主要有DooD 和 DinD 两种方案。这两种方案,各有优劣,你可以根据自身情况去选择。
|
||||
|
||||
## 思考题
|
||||
|
||||
<li>
|
||||
除了上述的 DooD 和 DinD 之外,你还知道哪些其他的Docker构建方案吗?它们分别有什么特点?
|
||||
</li>
|
||||
<li>
|
||||
Docker构建的缓存机制的基本规则是怎样的,如果ADD或COPY命令后是不同的文件,缓存机制会怎么处理?
|
||||
</li>
|
||||
|
||||
欢迎你给我留言。<br />
|
||||
|
||||
176
极客时间专栏/持续交付36讲/构建集成/18 | 如何做好容器镜像的个性化及合规检查?.md
Normal file
176
极客时间专栏/持续交付36讲/构建集成/18 | 如何做好容器镜像的个性化及合规检查?.md
Normal file
@@ -0,0 +1,176 @@
|
||||
<audio id="audio" title="18 | 如何做好容器镜像的个性化及合规检查?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/3d/8626e94da3b0bf9a1fb23f429a54713d.mp3"></audio>
|
||||
|
||||
你好,我是王潇俊。我今天分享的主题是:如何做好容器镜像的个性化及合规检查。
|
||||
|
||||
你是否还记得我在第13 讲篇文章《容器技术真的是环境管理的救星吗?》中说到:容器不是银弹,镜像发布无法很好地满足用户的个性化需求?
|
||||
|
||||
在携程的发布标准化中,容器内的环境也是由发布系统定义的,用户即使登录到容器上去做变更,下一次发布之后还是会被回滚回来。但是,对 Dockerfile 的编写和控制需要一定的学习成本,因此我们又不可能将镜像的内容与构建流程完全交给用户来自定义。
|
||||
|
||||
于是,就有了我今天的分享,即如何做好容器镜像的个性化及合规检查?根据我在持续交付道路上摸爬滚打的实践经验,总结了以下三种方法来满足用户对容器镜像个性化需求:
|
||||
|
||||
<li>
|
||||
自定义环境脚本;
|
||||
</li>
|
||||
<li>
|
||||
平台化环境选项与服务集市;
|
||||
</li>
|
||||
<li>
|
||||
自定义镜像发布。
|
||||
</li>
|
||||
|
||||
接下来的内容,我将根据这三种方法展开,并将介绍如何通过合规检查来规避个性化带来的风险。
|
||||
|
||||
## 用户自定义环境脚本
|
||||
|
||||
我们允许用户在编译后的代码包内放入包含自定义环境脚本的 .paas 目录(这是一个自定义的隐藏目录),来满足用户对环境的个性化需求。
|
||||
|
||||
这个.paas目录中,可能会存在build-env.sh和image-env.sh两个文件,分别运行于构建代码和构建镜像的过程中。
|
||||
|
||||
其中,build-env.sh是在构建代码之前运行,image-env.sh是在构建镜像的时候插入到我们规范的 Dockerfile 中,从而被打到容器内部。
|
||||
|
||||
这样就不仅可以满足用户对发布的镜像的个性化需求,同时还能满足对构建代码镜像的个性化需求。
|
||||
|
||||
比如,某个Python应用依赖一些动态链接库,那么这个依赖在构建代码和构建镜像环节都是必须的。这时,用户就需要在 build-env.sh和 image-env.sh这两个文件中都写入安装依赖的步骤,构建系统会在不同阶段判断是否有这两个文件,如果有就运行。
|
||||
|
||||
通常情况下,自定义环境脚本的方式,可以满足大部分用户的普通需求。但是,这个方式有两个缺点:
|
||||
|
||||
<li>
|
||||
构建镜像需要用完就删,因为我们无法感知用户在构建中修改了什么内容,是否会对下一次构建产生影响。这就要求每次构建都要生成新的容器,会在一定程度上降低构建性能。
|
||||
</li>
|
||||
<li>
|
||||
如果多个项目有同样的需求,那么这些项目就都要引用这个脚本文件,不但啰嗦,而且后面也不好维护,如果脚本内容变化,还需要通知所有引用的项目都改一遍。
|
||||
</li>
|
||||
|
||||
好的工具就是要解决用户的一切痛点,因此针对第二个问题,我们在系统上通过平台化环境选项和服务集市的方式做了统一处理。
|
||||
|
||||
## 平台化环境选项与服务集市
|
||||
|
||||
**环境选项,** 是携程在持续交付平台为用户提供的一些环境变更的常用功能,表现为构建镜像时的一些附加选项。
|
||||
|
||||
在上一篇文章《容器镜像构建的那些事儿》中,我介绍了构建镜像一个很重要的原则是:镜像要尽可能得小巧精简,因此我们没有在镜像中为用户安装太多的软件。但是,很多时候用户可能需要这些软件,于是我们就在平台上提供了环境选项的功能。
|
||||
|
||||
比如,很多用户需要用到Wget软件,于是我们就在交付平台上提供了一个 “安装 Wget ” 的环境选项。其实,这个环境选项对应的就是一条 shell 命令:
|
||||
|
||||
```
|
||||
yum install wget -y
|
||||
|
||||
```
|
||||
|
||||
如果某次发布时,用户需要这个工具,可以勾选这个选项,那么就可以在构建镜像时作为参数传给构建系统。如果搭建系统判断出有这个参数,就将会其插入到规范的 Dockerfile 中,从而这个参数就可以被打到容器内部。
|
||||
|
||||
环境选项虽然好用,但是只适合一些简单的需求,比如安装一些软件、更改一些配置等。而对一些复杂的需求,则需要创建一个叫作**服务集市**的功能。举个例子:
|
||||
|
||||
携程的服务集市中有一个 JaCoCo 服务,它的作用就是在 Tomcat 启动时更改 JVM 参数,收集应用的覆盖率并发送给外部系统。同时,外部系统可以控制这个JaCoCo服务的启停,并将收集结果处理成可视化的页面。
|
||||
|
||||
服务集市功能的使用,会涉及到以下两个关键步骤:
|
||||
|
||||
<li>
|
||||
勾选 JaCoCO 服务之后,会在容器中注入 jacocoagent.jar 和启停脚本;
|
||||
</li>
|
||||
<li>
|
||||
通过对外暴露的API,控制在容器中运行启停脚本。
|
||||
</li>
|
||||
|
||||
像JaCoCo这样的复杂功能,我们会抽象成服务,供用户使用。他们只要在构建镜像时选择对应的服务,和该服务起效的环境就可以了。
|
||||
|
||||
而实际系统要完成的任务则复杂得多,首先要通过改写Dockerfile完成以上所说的“勾选JaCoCo服务”,同时还要改写镜像中JVM的启动参数等,并完成对JaCoCo服务中心的注册。具体的操作各个服务有所不同,根据实际需求而定,原则就是把这些服务内容增加到对应的环境镜像中去。
|
||||
|
||||
通过这种方式构建的镜像,不同环境就拥有了不同的服务。比如,用户在构建镜像时,选择了JaCoCo服务起效的环境是测试环境,那么JaCoCo就只在测试环境的镜像中起效,而不会在生产环境中起效。
|
||||
|
||||
除了JaCoCo以外,携程还提供了许多其他与环境有关的服务,组成了一个服务集市,用户可以按照具体需求组合使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/be/c67b37efe16c5b5a7f02ce7fef5a39be.png" alt="" />
|
||||
|
||||
## 自定义镜像发布
|
||||
|
||||
**用户自定义环境脚本、平台化环境选项与服务集市,这两种方式有一个共同的缺点:自定义的部分都需要插到 Dockerfile中,因此每次打镜像时都需要运行一次。** 这对一些比较快的操作,没有问题,但如果需要安装很多软件,甚至需要编译一些软件时,每次发布都重复运行一次的效率就会非常低下。
|
||||
|
||||
为此,我们提供了用户自定义镜像的功能,该功能分为自定义Base镜像和完全自定义镜像发布两种。
|
||||
|
||||
1. **自定义 Base 镜像**
|
||||
|
||||
自定义 Base 镜像,就是如果基础镜像无法满足用户需求,并且自定义的部分非常重,运行比较久,我们就会建议用户使用自定义的 Base 镜像。但是,这个自定义的 Base 镜像,必须基于官方提供的 Base 镜像,因为很多工具和功能都是基于官方Base 镜像的。
|
||||
|
||||
虽然 Base 镜像是自定义的,但是应用还是标准的应用,因此发布方式和普通的发布方式没有区别。只是解决了自定义环境脚本与平台化环境选项的运行速度问题,反映到实际的 Dockerfile 上,就只是 FROM指令的指向改变了,变成了用户自定义的 Base 镜像地址。
|
||||
|
||||
1. **完全自定义镜像发布**
|
||||
|
||||
但是,用户的需求是永无止境的。比如,特殊启动方式的应用,自定义Base镜像就无法解决。
|
||||
|
||||
原则上来说,我不建议使用一些非标准的应用,因为这是不可控的,对生产环境非常危险。但是 Docker 的镜像是如此方便,用户如果只是想在测试环境中使用一些测试工具,虽然这个工具来自于社区,也不是标准的应用,但我们也没有理由全部拒绝。否则,用户很可能会以虚拟机上可以安装任何工具为由,要求退回到虚拟机时代。
|
||||
|
||||
但是,这样的退化怎么能被允许呢!
|
||||
|
||||
因此,一定要支持完全自定义镜像发布,也就是说用户可以发布任何镜像,只要这个镜像能够跑起来。对私有云来说,这应该是能接受的最大化的自由了。
|
||||
|
||||
对于完全自定义发布我们使用 Docker 多阶段构建(multi-stage build),也就是说用户可以将构建代码和构建镜像合并成一个步骤,在同一个 Dockerfile 中完成。
|
||||
|
||||
## 镜像安全合规检查
|
||||
|
||||
满足了用户对镜像的个性化需求,也就意味着会引入不可控因素,因此对镜像的安全合规检查也就变得尤为重要了。我们必须通过合规检查,来确认用户是否在容器里做了危险的事情。
|
||||
|
||||
只有这样,用户个性化的自由,才不会损害整个环境。毕竟,有克制的自由才是真正的自由。
|
||||
|
||||
**对自定义镜像,首先必须保证它是基于公司官方Base 镜像的,这是携程最不可动摇的底线。** 在其他情况下,就算真的不继承公司官方 Base 镜像,建议也必须要满足 Base 镜像的一些强制性规定,比如应用进程不能是 root 等类似的安全规范。
|
||||
|
||||
**关于自定义镜像是否继承了公司官方镜像,我们采取的方法是对比镜像 Layer,即自定义镜像的 Layer 中必须包含官方Base镜像的 Layer。**
|
||||
|
||||
但是,对比 Layer 也不是最靠谱的方式,因为用户虽然继承了 Base 镜像,但还是有可能在用户创建的上层 Layer中破坏镜像结构。目前,Docker 的部署流程中,还有许多潜在漏洞,有可能让一些有企图的人有机可乘,发起攻击。
|
||||
|
||||
因此,我们需要一些强制手段来确保镜像的安全,好的安全实践意味着要对可能出现的事故未雨绸缪 。
|
||||
|
||||
目前,市面上有很多工具可以为 Docker 提供安全合规检查,如 CoreOS Clair,Docker Security Scanning,Drydock 等等。
|
||||
|
||||
在安全合规检查方面,携程的方案是 Harbor 与 CoreOS Clair 结合使用:当构建系统 Push 一个新的镜像或者用户 Push 一个自定义镜像之后,Harbor 会自动触发 CoreOS Clair 进行镜像安全扫描。Clair 会对每个容器 Layer 进行扫描,并且对那些可能成为威胁的漏洞发出预警。
|
||||
|
||||
漏洞分严重级别,对于一些非破坏性的漏洞,我们是允许发布的。检查的依据是 Common Vulnerabilities and Exposures 数据库(常见的漏洞和风险数据库,简称 CVE),以及Red Hat、Ubuntu 、Debian 类似的数据库。
|
||||
|
||||
这些数据库中,包含了一些常见的软件漏洞检查。比如, libcurl 7.29.0-25.el7.centos 存在如下漏洞:
|
||||
|
||||
>
|
||||
The curl packages provide the libcurl library and the curl utility for downloading files from servers using various protocols, including HTTP, FTP, and LDAP. Security Fix(es): * Multiple integer overflow flaws leading to heap-based buffer overflows were found in the way curl handled escaping and unescaping of data. An attacker could potentially use these flaws to crash an application using libcurl by sending a specially crafted input to the affected libcurl functions. (CVE-2016-7167) Additional Changes: For detailed information on changes in this release, see the Red Hat Enterprise Linux 7.4 Release Notes linked from the References section.
|
||||
|
||||
|
||||
注:攻击者可以利用libcurl缓冲区溢出的漏洞,在应用的上下文中执行任意代码。
|
||||
|
||||
Clair 是一种静态检查,但对于动态的情况就显得无能为力了。所以,对于镜像的安全规则我还总结了如下的一些基本建议:
|
||||
|
||||
<li>
|
||||
基础镜像来自于 Docker 官方认证的,并做好签名检查;
|
||||
</li>
|
||||
<li>
|
||||
不使用 root 启动应用进程;
|
||||
</li>
|
||||
<li>
|
||||
不在镜像保存密码,Token 之类的敏感信息;
|
||||
</li>
|
||||
<li>
|
||||
不使用 --privileged参数标记使用特权容器;
|
||||
</li>
|
||||
<li>
|
||||
安全的 Linux 内核、内核补丁。如 SELinux,AppArmor,GRSEC等。
|
||||
</li>
|
||||
|
||||
这样能使你的镜像更加安全。
|
||||
|
||||
## 总结与实践
|
||||
|
||||
在这篇文章中,我分享了携程满足用户对镜像个性化需求的三种方式:
|
||||
|
||||
<li>
|
||||
用户自定义环境脚本,通过build-env.sh和image-env.sh两个文件可以在构建的两个阶段改变镜像的内容;
|
||||
</li>
|
||||
<li>
|
||||
平台环境选项与服务集市,利用这两个自建系统,可以将个性化的内容进行抽象,以达到快速复用,和高度封装的作用;
|
||||
</li>
|
||||
<li>
|
||||
自定义镜像,是彻底解决镜像个性化的方法,但也要注意符合安全和合规的基本原则。
|
||||
</li>
|
||||
|
||||
关于对镜像的安全合规检查,携程采用的方案是 Harbor 与 CoreOS Clair 结合使用。除此之外,我还给出了在实践过程中总结的5条合规检查的基本建议,希望这些实践可以帮到你。
|
||||
|
||||
除了 Clair 进行 CVE 扫描之外,还有其他一些关于镜像安全的工具也可以从其他方面进行检查,你也可以去尝试一下。
|
||||
|
||||
感谢你的收听,欢迎你给我留言。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user