开发者学堂课程【ALPD 云架构师系列-云原生 DevOps36计:不可变构建及如何提升构建效率(一)】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/82/detail/1269
不可变构建及如何提升构建效率(一)
内容介绍:
一、可预期的系统,始于可预期的制品产物
二、几个常见的构建问题
三、不可变构建
四、构建效率问题:积少成多
五、提升构建效率的实践建议
其实做软件肯定是面向终态的,终态是什么,就提供稳定、可预期的系统,用户需要服务提供了相应的请求,可以给用户相应预期的范围。但是可预期的系统,需要确保环境和软件制品(例如:容器镜像)一致性,不然系统将很模糊向,不清楚其究竟如何。
所以这里提到问题,怎么去确保环境和软件制品的一致性,这里先考虑如何确保软件制品的一致性,也就是说如果需要将东西分发出去,那分发出去的东西应该如何确保其版本的一致性。可能其他为不变量,但是最后结果前后不一致,这里是不可以的,会影响整个的稳定交付。
一、可预期的系统,始于可预期的制品产物
所以这里认为可预期的系统,始于可预期的制品产物。其中制品产物是一个新的概念。
其实熟悉制品交付的同学知道,例如代码也属于制品,编出来的包,镜像等等都属于制品。但在这里把构建出来的软件制品当做容器镜像,那在传统的构建过程里,基本是代码以及代码的依赖。比如说依赖的 ip 包、so 文件,然后在一个确定的构建环境里,有确定构建脚本,然后执行构建动作,最后生产出来软件制品,这是一个非常常见的操作。
那例如本地使用 PPT 和 C 的情况下,计算那些构建可以计算产出一个二进制文件,而二进制文件也是软件制品。
1、软件制品所必须包含的东西:
(1)确定的格式:到底是逻辑意向,还是 ip 包,或者是二进制的 adout ,可以具有明确印象。
(2)有唯一的版本:对每次构建出来的版本都不一样,但是有很多时候,其版本和代码的版本,依赖版本,环境版本或者构建脚本的版本是恰恰相关的,比如说这些内容不一样,导致产生出来版本也不一样。所以软件制品是有版本的。
(3)能够追溯到源码:明确是构建出来的地点及方式。如果制品溯源存在困难,就会产生很多的制品管理的问题。比如说现在在某一家生产环境里,二进制文件出现问题,但是不知道由于什么原因导致。比如说这里本身构建混乱,在有些企业里,谁都可以直接替换生产文件,就并不知道目前跑的代码是谁为源头。
(4)通过制品能够追溯到生产和消费过程:制品通过哪些阶段,经历哪些活动,最后将其产生出来,可以进行溯源。制品被用在哪些环境,被谁利用了,也可以进行溯源进行了解。这样其实相当于给制品贴标签,知道它是什么,拥有哪些特性。
二、几个常见的构建问题
构建对大家来说一点都不陌生,因为在电脑里,每天要执行很多次,那么简单的列举一下,就是构建当中可能常常遇到的一些问题。
第一个问题是应用代码库里没有 makefile/package.json/...,也就是所认知的的东西其都不具备,这时该如何构建呢?很多初学者可能拿到代码符时,面临到第一个问题就如此。其实很多时候拿到代码,首先需要将其构建出来。
第二个问题是例如 make build 执行成功,但制品却缺少了几个依赖的组件。这些东西怎么来,在何处定义,并不知道。
第三个问题是同一份代码,上周还可以编过,今天就报编译错误了。
第四个问题是电脑上编译成功,但工具(服务器)上不行。也就是换个环境,编译就失败了,并且并不清楚导致失败的原因。
最后一个问题是这个包在测试环境下可以正常运行,但在生产环境就会出错。也就是编译出来后,运行环境不一样,导致其也不一样。例如以前写过一段代码,在 A 电脑上一直没有出错,在 B 机器上运行就会出错。这时找了一台共同的另外的 C 机器去操作,发现并没有出错。那其实就是由于运行环境,构建环境,或者不同依赖的版本,导致遇到类似的问题。所以这些问题归根结底是构建本身是可变的。当构建可变的时候,会带来一系列的问题。解决这个问题的方式就是不可变构建。
三、不可变构建
不可变构建的含义,同样的代码以及同样的依赖,在同样构建环境的描述下,和同样构建脚本的描述下,构建的软件制品应该是相同的。所以这里强调就是,所有东西都要保证一致性,也就是如果前三者一样的话,那产生出来的制品也应该是一样。哪怕它是不同时间构建出来的,也应该一样。
所以这是不可变构建的基本的理解,这并不是废话,并不是所有东西都一样,其就会一样。不然,看起来很简单,但有些地方并不能完成。例如可能代码一样,但是构建环境,还有就是依赖表面上一样。但其实可能是不同的。
例如这里可能使用了很多 snapshot,比如说 Java 等等,那么这时 snapshot 永远都是 latest 这样的版本,此时并不知道这个版本和与其他版本是否为同一个版本。所以这里需要做好的不可变构建,并不是要使下图中绿色的相同的软件制品一样,而是说如何确保左边蓝色的几个可以满足相应的条件。
1.确定的依赖:依赖是什么?依赖的版本是什么?
接下来分别来看一下相同的代码和相同的依赖,就是怎么样可以被称为相同的依赖?依赖其实分两类:一类是源码的依赖,一类是依赖组件或者依赖制品。依赖源码,可能像 go 中很多都是源码,但是 Java很多时候,总仓库里面依赖的是 class+包,每个依赖其实都有一个明确的版本要求(依赖的什么的什么版本),这个源码或者这个制品加上这个版本,应该是能够唯一描述这个依赖的。
如果这个依赖的源码或者制品,加上这个版本是可变的,那这个依赖其实就是可变的。就做不到它是一个相同的依赖性的概念。可以分享常见的描述方式,比如说需要 go.mod,需要 package.json,但其实可以知道 package.json 本身里面东西其实是可变的,就里面由于一个版本,它其实区别其他版本比较新,底力范围也都可以。但一般会有 androck,要把它固定下来,只有将这两个加起来,它才是不可变的。
那 home 里面如果有 step short 是 release 版本。一般来说,也认为它是一个确定版本,如果不是,它也会有问题。那最后 requirements.txt,就是 python 里面,在现在 demo 事例的代码里它里面 requirements.txt,它其实具有依赖,具有可变性。其并未做到有依赖项 requirements+特定版本,其并不具有版本。这样可能存在随时间变化的问题。
举例子之前有一个做外部服务的库,然后隔了一周的时间再去构建,发现了它有内存溢出的 bug,后来发现是在构建的前一天,其提交上去了一个 bug。提交带有一个 bug,去源码提交才发现这个问题的,并不是只有高级语言才会遇到类似的问题。其实对类似意言类同样会遇到类似的问题,有时候 the library 的兼容性可能会出现问题时,这时是最危险的。
所以大家可以去对照一下,就是去查看构建的依赖文件。在依赖文件里,首先是否指明依赖代码具体的版本,或者说依赖的组件,依赖的 library 具体的某个版本。如果在空文件,或者在依赖文件里没有明确表达,这个构成是可变的。也就是如果构成可变,意味着就会产生一个不符合预期的最终的产物,最终在系统上也有可能会带来风险,并且风险不可预知。其实就在整个安全里,也要相应的去考虑一些问题,这里只点到为止。
描述代码:go.mod、package.json、package-lock.json、pom.xml、reqirements.txt)
2.相同的构建环境
相同的构建环境,这里举 Dockerfile 的例子,如何去找到相同的构建环境。
例如像自己的电脑,也不能保证昨天与今天是相同的,因为存在系统升级的问题,像上文提到的应用环境要描述一样,构建环境本身其实也需要描述。
而且描述方式也是相同的,通过 tkmapper 去描述,就是应用应该在什么环境下去构建,构建环境里有哪些组件,组件的版本是什么,都可以描述清楚。这问题与上文的依赖问题是相同的,例如说安装包,包的版本是否确定,还是其也是可变的,不同时间去构建也可能不相同,但这也具备风险。
所以构建环境和运行环境一样,都要通过 Dockerfile 描述。这里提出问题,构建环境的 Dockerfile 不一定就是运行环境的 Dockerfile。现在使用 demo 的应用时,其实使用的是同样 Dockerfile,这是为了操作的简洁化,这样不需要找工具链去解决这个问题。现在在里面又做构建,又做运行。但是如果仔细研究 Dockerimage 的 layo,也就是层次,会发现其中面积都达到了好几百兆,都比较大。但里面构建的一层占了很大一部分。
就是如果说需要给它“瘦身”,使用很简单的方式,就是将其分开。而且当构建时,会引入很多不必要的依赖,这也会带来一些风险。如果有同学感兴趣,可以尝试将文件修改,也就是将其分成两个:一个是构建环境里,其应该包含哪些,应该有哪些 to side,然后包括相应的依赖环境、依赖包。另一个,可以思考如果需要硬件环境的情况,每个必须或不需要在的位置。如果最终生成了可交付的镜像,相对来说会比较轻量级。可以想象如果构建环境不一样时,应该怎么去写,可以下去尝试,这相当于课后练习或者实验。
3. 相同的构建脚本
构建脚本分为三部分,与上文构建环境相对应。也就是确定的构建依赖、语言无关的构建脚本、机器无关的构建环境。
构建依赖里是 go.mod 文件,其实可以看到里面,这里依赖的模块,与其对应的版本是非常明确的版本,比如说有对应的 TAI,或对应的permit ip,这是明确的依赖。
目前这里包含构建,可以看到 go build,go build 的部分包含构建,同时也包含运行,这就是整个进程。
目前构建环境,其实在 go long 这里,构建环境也是不唯一的,这里并没有使用确定的版本,是使用 latest 或者类似的版本,也就是永远使用最新版本,不同时间也是不同的,而且 go long 版本也是不确定的。.其实这是不好的事情。
所以说刚才简单提到了,重复这边基础设施以后,第一个要保证标准化,也就是交付部最终交付时使用什么样的形态,进行交付并且部署的运维。第二个是如果要保证不可变,首先要保证和预期的系统运行一致,所以需要一个可预期的构建制品。可预期的构建制品必须从三个角度来做好不可变的构建,然后才能做一致的的软件制品。