原创作者:清如许
导读
作为移动互联新时代的程序员,经常会把程序装进容器内运行,但是慢悠悠的镜像构建过程、国际网络的不稳定、移动联网时的流量狂奔,都让人又爱又恨。本文介绍的两步快速构建容器镜像方法,速度快到以秒为单位,能缓解镜像构建痛点。
一、为什么用容器技术
写作本文源于编写一个 SSH 信任登录认证程序,而信任登录认证又是 K8s 故障诊断程序的一部分。
本文的内容也是从一篇长文中摘录出来的,为了适应互联网读者的阅读习惯(碎片时间阅读),做成几个小专题,内容聚焦更聚焦,也许效果会好一点。少食多餐的好,暴饮暴食万一撑爆了,那就是遗憾加罪过了。
信任登录认证程序运行在 Linux/Unix 环境下,如果要适应不同的操作系统版本,需要考虑一些环境细节,或者说为不同的操作系统版本编写不同的代码,至少某些功能点是这样的。随着操作系统版本升级,还要做适应性升级,否则程序运行时可能会发生异常。这是不希望看到的。
容器化技术在程序与宿主机之间引入新的容器层,把程序与容器的关系,变为程序与容器、容器与宿主机两层关系。程序员只需要考虑程序运行时的容器环境,容器与宿主机之间的关系交由类似于 Docker 的容器管理层实现。容器化技术把程序与运行时的宿主机环境隔离开来,这样程序就能适应不同的宿主机操作系统。例如,假如建立信任登录的程序选择CentOS 作为容器环境,CentOS 容器能运行在 Windows、RedHat Linux、Debian Linux 和 AIX 等操作系统上,则应用程序也就间接地能在这些操作系统上运行。
信任认证程序只是故障诊断系统的一个功能,如果说修改信任认证程序的源代码以适应不同的运行环境,只需要花费少量的时间,那么修改故障诊断程序适应环境,代价就太高昂了。所以容器化是不得不走的一步,晚走不如早一点走。
二、容器技术选择
从早期的 chroot(1979)、FreeBSD Jails(2000)、Linux VServer(2001)、Solaris Containers(2004)和 LXC(2008),到如今的 Warden(2011)、Docker(2013)、Rocket(2014)和Windows Containers(2016),容器技术经过四十年的发展,已经是遍地开花,得到广泛地应用。
其中,Docker 容器技术挟后发优势,因为其功能强大、性能优良、开源免费(社区版)、广泛适应性(所有主流操作系统),而受到业界广泛推崇。作者在 Docker 基础上做过大量的开发和应用,所以优先选择 Docker 容器技术。
三、基础镜像选择
选好容器技术后,就要选择容器内运行的操作系统。RedHat Linux 是经典的 Linux 发行版,在国内拥有广泛的用户基础。但是因为版本注册和收费等原因,Redhat 并不适合用作容器操作系统。CentOS 是 Redhat 的社区开源免费版,与 Redhat 有相似的命令集和用户操作习惯。Alpine Linux 是面向安全的轻量级 Linux 发行版,主要用于面向 Serverless 服务而无需用户操作的容器操作系统。
本程序和其所属的故障诊断程序需要用户管理维护,所以既需要一个程序运行环境,也需要用户操作环境,CentOS 恰好能满足这两点需求。CentOS 的版本选择比较新的CentOS 7.5,开源社区已经有好心人把 CentOS 操作系统构建成公共镜像,从网上拉取对应版本的镜像即可,开箱即用。
基础容器镜像包含操作系统最核心的版本,如果需要更多软件包,例如 SSH 客户端和SSH 服务器,则需要安装附加软件包,本程序也需要安装进去。这些软件包的安装指令在Dockerfile 文件中描述。
四、容器驻留程序
在运行时,Docker 从容器镜像启动容器实例后,会自动启动入口点 Entry Point 或者 CMD 声明的程序,入口点程序执行结束退出,容器也就退出了。我们希望容器始终在运行,直到用户主动停止容器。因为诊断程序可能通过 crontab 调度定时运行,也可能在需要时手工启动运行,但是不会始终在运行,缺少运行状态的进程会引起容器退出。
引入 supervisord 程序,解决容器缺少驻留服务进程问题。supervisord 程序是一个运行在容器内的驻留服务进程,对外开放 Web 管理端口,通过浏览器能访问查询、管理容器内进程等资源。
supervisord 的配置文件 supervisord.conf 的内容如下:
## supervisord.conf [unix_http_server]file = /supervisord/supervisor.sock[inet_http_server] port = *:9001 username = admin password = adminpass [supervisord]logfile = /supervisord/logs/supervisord.log logfile_maxbytes = 50MBlogfile_backups = 20 loglevel = infopidfile = /supervisord/supervisord.pidnodaemon = falseminfds = 1024minprocs = 200 [supervisorctl] serverurl=unix://supervisord/supervisor.sock[include]files = /supervisord/include/*.ini
五、容器镜像构建
容器镜像的构建过程包含以下步骤:声明基础镜像,这里是 CentOS 7.5;安装附加软件包;编译源代码,这里是 Shell 脚本,不需要编译,这一步忽略;复制 Shell 脚本、上一步编译好的程序文件和其他资源文件到容器内的相应目录下;声明容器启动的入口点程序(Entry Point);其他操作。
容器镜像的构建过程中,拉取基础镜像、安装附加软件包的比较耗时,因为基础镜像比较大,大约几百兆字节,附加软件包也比较大,大部分从互联网下载(下载过程自动完成),甚至从国外站点下载,受到接入带宽和提供镜像服务站点带宽的约束,所以构建过程的这部分是比较慢的。复制 Shell 脚本在构建环境本地进行,文件比较小,所以非常快。
在开发过程中,因为发布新版本程序,镜像构建过程经常发生,如果每次花费几分钟、十几分钟等待构建完成,这是漫长的煎熬,尤其是对追求极致的架构师,是无法忍受、无法接受的。
如何缩短日常开发过程中的镜像构建时间,是一个亟待解决的问题。前面分析了镜像构建过程,拉基础镜像、安装附加软件包这些步骤耗时很长,而且这些步骤执行的结果每次都是一样的。所以让这些耗时且每次输出结果无变化的步骤只在第一次构建时进行,而在第二次、第三次...的构建过程不执行这几步,只执行有变化的步骤,将大大缩短镜像构建时间。
六、两步法快速构建镜像
具体实现是将整个构建过程拆分为两步进行,第一步预处理拉基础镜像、安装附件软件包,第二步编译、复制应用程序。因为有两个步骤,称作两步法构建镜像。第一步的输出结果是中间镜像。
Dockerfile 是顺序执行的过程框架,但是逻辑控制能力比较弱。Makefile 能执行编译、docker build 等指令,而且支持 if..else 等控制语句。所以考虑用 Makefile 控制镜像构建过程。
两步法包含一个 Makefile 文件和两个 Dockerfile 文件,也可以称作 1M2D 方法。
先画一张两步法构建容器镜像的时序图,如下图所示:
Makefile 包含以下阶段:
1、all: pre-build-image:
阶段 all 是 Makefile 的总入口,依赖于预构建(中间)镜像,会先调用 pre-build-image 阶段。
2、pre-build-image:
阶段 pre-build-image 构建(中间)镜像阶段。判断中间镜像在本地 Docker 镜像仓库是否存在。如果中间镜像存在,则跳过构建中间镜像阶段。这是两步法的关键点。
3、cleanup: cleanup-diag:
阶段 cleanup 删除中间镜像,只有在中间镜像依赖基础镜像或者附件软件包发生变化时才会执行,这种情况很少发生。
依赖于阶段 cleanup-diag,会预先执行所依赖的阶段,删除最终的应用镜像。
4、cleanup-diag:
阶段 cleanup-diag 删除最终的应用镜像。本阶段没有依赖阶段,可以单独执行,删除最终的应用镜像。
5、push:
阶段 push 推送最终镜像到云端镜像仓库。本阶段没有依赖阶段。
Makefile 文件的内容如下:
## Define constantsVERSION=latest TAG_PRE_BUILD_IMAGE=k8s-diag-pre K8S_DIAG_IMAGE=k8s-diagnose#FULL_URL_IMAGE=registry.cn-hangzhou.aliyuncs.com/wise2c-dev/$(K8S_DIAG_IMAGE):$(VERSI ON)FULL_URL_IMAGE=registry.cn-hangzhou.aliyuncs.com/solomonxu/$(K8S_DIAG_IMAGE):$(VERSIO N) ## Check docker image pre-build-image if existedpre-image-count := $(shell docker images | grep $(TAG_PRE_BUILD_IMAGE) | wc -l)pre-image-id := $(shell docker images | grep $(TAG_PRE_BUILD_IMAGE) | grep centos | awk'{print $$3}')diag-image-id := $(shell docker images | grep $(K8S_DIAG_IMAGE) | grep $(VERSION) | awk'{print $$3}') ## .PHONY .PHONY: all image-pre## Build all all: pre-build-image # echo "This will make Docker image $(K8S_DIAG_IMAGE) ..." docker build -f Dockerfile -t $(K8S_DIAG_IMAGE):$(VERSION) . ## Build pre-build-image pre-build-image: @if [ "$(pre-image-count)" = "0" ]; then docker build -f Dockerfile.pre -t centos:$(TAG_PRE_BUILD_IMAGE) . ; else echo ""; fi ## Cleanup images cleanup: cleanup-diag @if [ -n "$(pre-image-id)" ]; then echo "This will remove Docker image centos:$(TAG_PRE_BUILD_IMAGE) now, IMAGE ID: $(pre-image-id)."; docker rmi $(pre-image-id) ; fi ## Cleanup image k8s-diagnose cleanup-diag: @if [ -n "$(diag-image-id)" ]; then echo "This will remove Docker image $(K8S_DIAG_IMAGE) now, IMAGE ID:$(pre-image-id)."; docker rmi $(diag-image-id) ; fi ## Push to repository push: docker tag $(K8S_DIAG_IMAGE):$(VERSION) $(FULL_URL_IMAGE) docker push $(FULL_URL_IMAGE)
第一步预处理阶段拉基础镜像、安装软件包,Dockerfile.pre 文件内容如下:
FROM docker.io/centos:7.5.1804 MAINTAINER solomonxu## Update CentOS RUN yum -y update ## Install supervisor etc.RUN yum install -y openssh-clients openssh-server sshpass RUN yum install -y net-toolsRUN yum install -y epel-release RUN yum install -y supervisorRUN yum install -y which mailx expect
第二步复制应用 shell 脚本、暴露端口、声明容器启动入口点,Dockerfile 文件内容如下:
FROM centos:k8s-diag-preMAINTAINER solomonxu#ARG gitCommit## Copy file for k8s-diagnose ADD ./bin /k8s-diagnose/bin ADD ./conf /k8s-diagnose/conf ## Add permissions of shellRUN chmod -R a+x /k8s-diagnose/bin/ ## Make dirs for supervisord RUN mkdir -p /supervisordRUN mkdir -p /supervisord/include RUN mkdir -p /supervisord/logs ## Add config for supervisord ADD ./docker/supervisord.conf /supervisord USER root EXPOSE 22 80 9001 ## COMMAND CMD [ "supervisord