在第13期云栖TechDay活动上,唐容为大家分享了《应用Docker进行持续交付》话题。他先谈了传统CD(持续交付)过程中遇到的问题,然后解释Docker的原理及它为什么能够改变持续交付,最后分享了应用Docker化交付的过程和适用场景。
下面是演讲内容整理。
背景
互联网行业都是比较新兴的产业。市场需求量不断变化,产品不断变化,导致我的开发须不断变化,最后导致我必须持续不断的去交付、更新我的生产环境。
时间长了就会产生一系列的问题。其中最主要的问题是,老的运维人员把生产环境做好,一旦他离开,就会导致继任者不知道该对这个环境应该做怎么样的操作,因为他不知道前面的人做了什么。
还有一种情况,通常生产发布往往是在生产环境调试非常多次,最后才能发布下去。运维最辛苦,但是需求又不断的变化,产品又在不断的变化,我们还是必须不断更新。
传统CD过程中遇到的问题
传统解决方法是CI(持续集成)或CD (持续交付)。即把开发生产交付的过程变成一个环,一个可以不断循环的过程。
- 集成:就是两个东西在一起:提交文档时代码和代码在一起,编译时代码和逻辑在一起,测试时代码和功能在一起,生成到部署和生成发布时就是代码和系统,系统和系统之间在一起。每次两个东西在一起就是集成。
- 持续:它就跟体检一样,它只是能帮你很快速的在前面发现你的系统中出现的问题,让你在前面就把这个问题解决了,不至于到最后发布的时候发现:有功能没测过就被合起来了,漏了一个功能,哪块代码有点问题。总之就是说要持续做这个事情。
- 有效反馈:在持续集成管道的流程图里,每一个结点就是一次有效的反馈。
举个最简单的例子,开发人员提交完代码之后,你要做到的第一点是和别的代码没有冲突,并且能编译通过,然后单元测试能通过,这个时候确实没有这个工程师的事情了,可以去做别的事情了,这个就叫有效的反馈。
我只是告诉你说我的代码没有冲突了,我去干别的事情,突然别人告诉我,你代码提交上来,感觉整个系统编译最后失败了,还是得去修复。所以说有效的反馈和持续进行,是整个里面最重要的两件事情。
我每做一个结点得到一个有效的反馈,这个有效的反馈需要非常及时的去恢复到这个development的地方。然后恢复到开发人员这里的时候,他就知道后面该做什么样的事情。
还有一种情况,一般来说开发人员只要保证自己的单元测试通过了,去干别的事情;测试人员来负责系统的功能,或者集成的测试。可能开发提交了10次代码才进行一次功能测试。两个人节奏不同,所需要得到反馈节奏也不同的,所以两个结点是分开的。
构建CD过程所需的环境
一般来说在一个公司里面,或者一个项目,需要搭建一个不同的环境。这个环境首先有托管,单位测试,有一套打包build的环境,最后还会有一个部署的服务。
企业生产环境是在公有云上,是一个公有云环境。我们开发过程是在线下环境,这就造成了最后发布的时候,一是我发布的时候需要一个环境,二是公有云跟线下开发环境不一致,可能导致很多问题。
CD过程中遇到的问题:
即便按照流程去做持续交付,系统地搭建出来整个持续集成的系统环境,一样会遇到七七八八这样的问题。就算是同一种语言,每一个开发者所依赖语言环境、依赖的包不一样,就会导致有非常多的编译环境,维护起来就很困难。
问题的根源:
开发者交付的只有代码和代码的依赖,而运维需要的除了代码,还需要环境,环境描述,依赖,数据库,缓存,而不仅仅是代码。
变革软件交付方式的技术:Docker
如果交付的代码中有一个描述文件,不仅能描述代码本身的依赖,还描述整个环境的依赖以及环境的描述,catch,config,变量,容器,生成环境需要的jar包等。
这就像把一堆零散的代码和我要的所有东西装在集装箱里,真正去交付给运维时,相当于把这个集装箱运行起来。集装箱最核心的特点,是有一个集装箱的方式去运行,它包含所有的环境依赖,所以在任何地方运行集装箱所达到的结果都是一样的。因为达到了环境一致性。
没有Docker之前我们有很多规范用来约束。Docker是通过技术手段约定,甚至通过技术能力从底层改造。
Docker的能力
- 描述环境的能力(Docker file文件,这个文件能描述软件所需要的整个环境)
- 分层文件系统 (分层文件镜像Docker image,解决包管理问题,我们每做一个操作,都能够描述出来作为一层,这一层就是每一层版本管理。)
- 运行时屏蔽操作系统差异
Docker是一种容器技术
容器技术与传统虚拟机技术的最大差别在于 : 所有容器是进程级的,它用到的操作系统的内核是父母机的操作系统内核,它没有完整的os层。
(虚拟化技术在物理机层面上有一层软硬件结合的虚拟化技术,虚拟化技术上面每一个虚拟机是一个完整的操作系统,有很好的隔离性,但可能启动需要几分钟。)
容器技术最大的好处在于它是进程级的,是一个秒级启动,没有传统的一虚几的概念。理论上宿主机上能启动无数个Docker进程。
Docker进行软件交付的三步:
- Build :描述依赖操作系统,环境描述,启动端口,执行脚本。描述文件存储为Docker镜像(Docker image),放在本地存储仓库里
- Ship:把镜像传递(Push)给远端的Docker Registry里。
- Run:运行时从公共Registry获取(Pull)镜像。这个集装箱,是一个环境描述的本身,是一个完整体,在任何环境运行的结果应该是一样的。
官方案例:BBC news
BBC News是一个全球新闻网站的一个公司。他们当时遇到问题是,他们全球有500多个开发,分布在全球的各地。然后有十多种CI的环境,因为全球各地的人用的语言也不一样。从代码到交付这个过程,它都需要没办法运行,需要等待,然后十多种CI环境无法统一的管理,这是最大的问题。
如何使用Docker
第一步:安装Docker运行环境。
Docker运行环境只有一个限制,即Linux操作内核必须高于2.6.3的某个版本。Docker公司推出Docker machine技术,有一个toolkit,这个工具集可以区分你的操作系统,提供不一样的安装包,一键安装。这个工具屏蔽了底下OS的差异。同时有不同的云驱动,就是说像阿里云上面有ECS driver、AWS、GCE等等,每一个运营厂商一般都提供将Docker安装在他们上面的驱动。
第二步:用Dockerfile 描述你的应用环境
- 方案一
第一种是运维把原来搭建一个环境要的所有东西都列出来,变成Dockerfile这种描述语言。这个语言它其实不是某一种开发语言,只是定义出来的一个描述语言。这个描述语言其实很简单,它必须有一个FROM, FROM上一个环境。有了这个概念,也可以很好的传承这件事情,去分割工作职责。run执行命令, event环境变量,然后还有add expose cmd 等等的命令。
- 方案二
如果有人已经写好标准的Java环境,就能直接把环境拿过来,把jar放进去就能用。
如果服务被拆成微服务的架构 ( 每一个服务不是一个war包而是一个jar ) ,那么所有的微服务共用一个Docker 镜像。(不同语言提供API功能,许多并不需要依赖,只需要语言的运行环境就可以了。)
现在官方提供各种环境的Docker 镜像,FROM官方的镜像,把jar放进去就可以。有一些开源软件,把镜像下下来就能直接运行了,如Gitlab。有了Docker技术,搭一些开源的工具系统也会变得非常容易。
第三步:用Docker搭建编译/单元测试环境
- 应用本身的编译和Docker环境的编译分开
如果放在一起的话,势必要把代码拷到Docker环境里面去。在Docker build的过程中,先build了我的代码,然后再把我的代码删掉,因为Docker(统一下全文大小写) 有分层的原理哪怕我在我Docker file里面写,我先拷贝我的一些.java连接过来之后, build一个jar,再把.java删掉。只要获取到你拷贝.java这一层,就可以获取到你源代码。而这个Docker容器真正运行在生产环境时,我们是不希望代码在生产环境里面出现的。
- 编译和测试的环境分开
本着这个叫做Docker镜像最小化的原则(就是我要什么我装什么,我不要什么我就不装)一个Java(统一下大小写) + NodeJS运行环境,我生产环境运行起来了之后,我不需要用到ping。ping是运维调试的时候需要的。你编译的时候可能需要很多编译环境的依赖,编译环境的依赖在生产环境运行是不需要的,所以基于这一点,我们也建议把环境分开。建议你们编译环境也通过Docker实现。
编译环境是一个build环境,测试的环境是一个run的环境。我对程序build的一个环境,简单来说下过程:
代码可以通过-v的方式就是加载到容器里,有一个叫build的container,针对不同的语言进行build,变成binary依赖包这样的文件;然后再把这些文件add拷贝到Docker file里面去,再进行Docker build。两步build变成最终可以运行的Docker image。
通过第一步build的时候,Docker file里面拷贝过来的是binary,是真正要运行的东西,而不是源代码。
然后image build出来之后,可以去启动不同的脚本(让真正的运行跑生产,我可能有一个跑生产的脚本;我可以运行一个跑测试,然后去执行测试脚本)。用Docker去跑集成测试其实也不是很难。因为应用本身已经Docker化了,只要Docker跑起来,去执行一个测试的命令,不管是执行数据库还是执行什么,它都是执行测试的过程。人工测试之外的,只要是能够脚本化测试的东西,都可以拿Docker去编写。
第四步:用Docker描述依赖环境
应用程序并不是有image就可以了,即使不依赖其他人的应用程序,都一定会依赖数据库。测试的时候要造一个数据就得有数据库在。
Docker公司还提供了一个Docker compose。Compose即编排,编排的意思是可以管理多个Docker image。
这个图就是Docker compose的官方介绍图,简单来说分三步:
第一步:写Dockerfile描述应用环境。
里面写工程目录是什么,拷贝了一个requirement一个文件到目录code下,pip install 一个ruby程序,运行这个程序。
第二步:通过.yml去描述依赖关系
绝大多数的情况下,单容器是无法直接解决问题的。跑测试的时候我需要依赖一个DB,只要有一个DB能够提供存储的功能,当我要的时候它能够提供给我就可以了。这个东西就是来描述一个或多个web程序,DB等等非常多的东西的。
当我这个程序需要依赖一个DB的时候就写一个link,起一个名字叫link DB。通过Docker compose这个命令,去把一组Docker image一起运行起来,它里面依赖关系是通过compose .yml去描述的。
第三步:用Docker把整体的集成或者运行环境都描述出来
Docker运行起来了之后,除了Docker本身,APP本身前面需要负载均衡后面需要数据库,然后可能还需要文件存储,还需要缓存,然后需要其他的一些东西。
- 数据库
Docker其实根本不关心数据库搭在哪,也不关心怎么搭的。开发控制过程中,去依赖启动一个DB,所有的DB官方已经提供了image,只要把这个DB container从官方拉下来。一个MySQL DB5.7的应用,在真正使用之前启动这个MySQL DB的container,看一下配置,可能改一些语言描述。比如说我可以在compose里面去运行,第一行写依赖,运行MySQL 5.7;第二行写环境变量,把我的配置文件替换成它的配置文件,设置一下启动密码,然后启动的端口是3306还是多少。
并且在web程序里面,测试的时候不用去写测试依赖的MySQL id是什么,写MySQL就直接能通过,因为它就是compose里面描述的这个MySQL。通过之后我只要告诉它密码是什么就能连上数据库。而Radis起个名字提供端口就可以了,它只要缓存服务。
- 负载均衡
负载均衡也挺简单,不管是Ngix , Haproxy也有官方的东西。最后开发人员真正关心的,就是怎么把自己的应用程序拿Docker描述出来。描述出来之后,写好compose,前面Nginx用的什么版本,什么样配置,后面的这个APP启动。
- 存储
一是我通过-v挂在宿主机,二是把数据库的存储,和我初始化的数据,挂在我本地的一个目录下面,这个时候每次运行就不用再初始化,只要去点APP就可以了。
而真正运行生产环境的时候,数据肯定是在一个云存储或者说存储在云设备上。这个时候就可以选择一些云存储的方案,比如说云存储不管你在程序里面做,还是把它挂到宿主机上,让宿主机去做;不管你是用NAS还是用阿里云OSS,把某一个目录挂在本地的一个目录下面,而不用通过SDK或者API再去把你的数据上传到云存储设备。
容器要解决负载均衡等等的问题,这些问题在数据库里面本身已经做好了,不必要去容器化。
应用Docker化交付的过程
开发人员在除了代码 、Config、Test脚本还要写Dockerfile。把这些代码传(Push)到代码仓库,有一个CI service通过代码仓库hook告诉你代码仓库有一个新的提交。把代码拉下来复制,进行两件事情 : Build和UT。在build的过程中,会从Docker Registry上面去拉下来( Pull )依赖的image,当build通过了之后会push image到这个Docker Registry单位里面去。然后CI 会有一个hook去通知CD,deploy Service根据Docker和image描述以及compose描述,把image部署到预发、测试或者生产环境。
整个部署过程就是从Registry里拉下来一份build好的image然后重新run起来。最复杂的东西都集中在image, compose两个描述文件里面,其他东西整个过程都会变得比较简单。
结束语
Docker交付带来什么?
开发可以更清楚更灵活掌握。以前我只关心Java依赖是什么,现在我关心整个的环境依赖是什么;以前我在本地搭一个Java环境非常困难。现在非常容易,运维再也不用因为软件依赖改变自己的时间。不用今天改一个什么配置明天改一个配置。Docker它能够做到什么呢?每人每项目每环境。下一个它是DevOps最好的诠释 : 怎么样把编程用到运维里面,把环境运维以开发思想管理起来,这个才是我认为才是compose的经典。
我们该把什么场景Docker化?
这到今天还没有非常明确的定论。
这是我自己的理解 : 越短生命周期以及越无状态的这种东西,容器化带来最大的好处。比如像web应用,REST API , CI/CD这种,需要跑测试的时候,就把整个Docker容器拉下去去跑,跑完了就扔掉不占有资源了。它是一个任务行为的一个过程,这种场景下用Docker去做,就能节省资源等等非常多的好处。
最不容易Docker化的东西,是数据库,存储这种。容器对这一类并不能带来非常直接的好处。因为假如服务器承载非常强烈的状态下,这就是一个文件存储器。并不是说不能做,意义不大。
关于分享者
唐容
目前负责工程效能团队产品开发工作,主要包含持续交付平台( CRP )及云 Code 平台,在互联网研发团队效率提升和产品交付方面有近 10 年经验。曾负责淘宝日常测试环境,集团源码管理,编译系统等基础设施的建设,以及阿里云云计算等部门的配置管理工作。