容器在软件开发、测试和部署环节应用的越来越广泛,那么测试人员应该如何掌握容器技术呢?应该掌握哪些基本的容器操作呢?本文通过容器化一个
Python Web 应用,来快速掌握 Docker 容器和镜像的基本操作。
容器技术中两个基本的概念是容器和镜像。可以通过一个类比来理解,容器就是进程,镜像就是程序。程序运行起来就是进程,镜像运行起来就是容器。
程序要想能运行起来,除了有我们自己编写的业务代码还要有依赖,还要借助于操作系统,把代码、依赖和操作系统打包在一起就是镜像,镜像中包含程序运行起来的所有要素,因此镜像可以“Build Once,Run Anywhere”,能够保证一致性。这是容器技术带给我们的非常大的益处。
容器是镜像的动态表现,本质是一个的进程,镜像启动成为进程时,Docker引擎借助Linux Namespace 技术修改了应用进程看待操作系统的“视图”,只能“看到”某些指定的内容,并自以为自己是PID=1的1号进程。Docker引擎还利用Linux Cgroups技术对容器进程能够使用的系统资源,比如CPU、内存等进行了限制。因此,容器就是被Docker引擎加了很多限制的进程。
本文不详细介绍容器和镜像底层原理的更多内容,将聚焦在软件测试工作中常用的对容器和镜像的基础操作。
要想执行本文里面的docker命令,前提是有一台安装了Docker的MacOS或者Linux操作系统的机器。安装方法请参考:https://www.docker.com/get-started
01 — 构建一个镜像
一个完整镜像通常包含应用本身和操作系统,当然还包含需要的依赖软件。
首先准备一个应用。新建一个本文文件,起名叫 app.py,写入下面的内容,实现一个简单的web应用:
在这段代码中,使用 Flask 框架启动了一个 Web 服务器,而它唯一的功能是:如果当前环境中有“NAME”这个环境变量,就把它打印在“Hello”后,否则就打印“Hello world”,最后再打印出当前环境的 hostname。
这个应用的依赖文件requirements.txt存在于与app.py同级目录中,内容是:
将这样一个应用在容器中跑起来,需要制作一个容器镜像。Docker使用Dockerfile文件来描述镜像的构建过程。在本文中,Dockerfile内容定义如下:
这个Dockerfile中用到了很多指令,把包括FROM、WORKDIR、ADD、RUN、EXPOSE、ENV和CMD。指令的具体含义已经以注释的方式写在了Dockerfile中,大家可以查看。通常我们构建镜像时都会依赖一个基础镜像,基础镜像中包含了一些基础信息,我们依赖基础构建出来的新镜像将包含基础镜像中的内容。
需要再详细介绍一下CMD指令。CMD指定了python app.py为这个容器启动后执行的进程。CMD [“python”, “app.py”] 等价于在容器中执行 “python app.py”。
另外,在使用 Dockerfile 时,还有一种 ENTRYPOINT 指令。它和 CMD 都是 Docker 容器进程启动所必需的参数,完整执行格式是:“ENTRYPOINT CMD”。
默认情况下,Docker 会为你提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如在我们这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c “python app.py”,即 CMD 的内容就是 ENTRYPOINT 的参数。正是基于这样的原理,Docker 容器的启动进程为实际为 ENTRYPOINT,而不是 CMD。
需要注意的是,Dockerfile 里的指令并不都是只在容器内部的操作。就比如 ADD,它指的是把当前目录(即 Dockerfile 所在的目录)里的文件,复制到指定容器内的目录当中。
更多能在Dockerfile中使用的指令,可以参考官方文档https://docs.docker.com/engine/reference/builder/#dockerfile-reference。
根据前面的描述,现在我们的整个应用的目录结构应该如下这样:
现在我们执行下面的指令构建镜像:
其中,-t 的作用是给这个镜像加一个 Tag,即:起一个好听的名字。docker build 会自动加载当前目录下的 Dockerfile 文件,然后按照顺序执行Dockerfile文件中的指令。
上面的命令执行完成后,就生成了一个镜像。可以通过下面的指令查看:
还可以通过 docker inspect helloworld:latest 查看镜像的元信息:
元信息中包含了镜像的全部信息,包括镜像的tag,构建时间,环境变量等。
如果镜像不再需要了,可以通过docker image rm删除镜像。
02 — 运行镜像
有了镜像,就可以通过下面的指令来运行镜像得到容器了。
上面命令中,镜像名 helloworld 后面,什么都不用写,因为在 Dockerfile 中已经指定了 CMD。否则,我就得把进程的启动命令加在后面:
从现在看,容器已经正确启动,我们使用curl命令通过宿主机的IP和端口号,来访问容器中的web应用。
不过这里返回的主机名有点怪怪的,其实这个59b607239c3a就是容器的ID,可以通过运行docker ps指令查看运行中的容器。
从输出中可以看到容器的ID,容器是基于哪个镜像的启动的,容器中的进程,容器的启动时间及端口映射情况,以及容器的名字。
使用docker inspect 59b607239c3a命令,可以查看容器的元数据,内容非常丰富。
03 — 分享镜像
大家一定用过代码分享平台GitHub,在Docker世界中分享镜像的平台是Docker Hub,它“学名”叫镜像仓库(Repository)。任何人都可以从上面拉取镜像或者Push自己的镜像上去。
为了能够上传镜像,首先需要注册一个 Docker Hub 账号,然后使用 docker login 命令登录:
在push到Docker Hub之前,需要先给镜像指定一个版本号:
liuchunming是我在Docker Hub 上的账户名。v1是我给这个镜像起的版本号。接着执行下面的指令就可以镜像push到Docker Hub上了:
一旦提交到Docker Hub上,其他人就可以通过docker pull liuchunming/helloworld:v1将镜像下载下来了。
在企业内部,也可以搭建一个跟 Docker Hub 类似的镜像存储系统。感兴趣的话,可以查看VMware 的 Harbor 项目。
04 — 镜像加速
鉴于国内网络问题,从https://hub.docker.com/拉取 Docker 镜像十分缓慢,我们可以需要配置加速器来解决。在Mac电脑任务栏,点击 Docker Desktop应用图标 -> Perferences。在settings页面中进入Docker Engine修改和添加Docker daemon 配置文件即可。
修改完成之后,点击 Apply & Restart 按钮,Docker 就会重启并应用配置的镜像地址了。之后在拉取镜像时,将会快很多。
05 — 进入运行的容器中玩玩
运行web服务的容器,通常是以后台进程启动的。就是在docker run指令后面加上-d选项。比如以后台方式运行上面的web容器:
如果想进入到一个正在运行的容器中做一些操作,可以通过docker exec指令:
-it选项指的是连接到容器后,启动一个terminal(终端)并开启input(输入)功能。-it后面接的是容器的名称,/bin/sh表示进入到容器后执行的命令。还可以通过容器的ID进入容器中,容器的ID可以通过docker ps命令查看。
docker exec 的实现原理,其实是利用了容器的三大核心技术之一的Namespace。一个进程可以选择加入到某个进程(运行中的容器)已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的。更细节的原理这里不在细究。
进入到容器中,就可以在终端上进行一些操作了,比如在容器中新建一个readme.md文件:
这个readme.md文件只会在这个容器中存在,用镜像启动的其他容器中不会有这个文件。
我们还可以将正在运行的容器,commit成新的镜像。
还有一种进入容器的方法是使用docker attach container_id,不过这种方法不建议使用,因为它有个明显的缺点:当多个窗口同时attach到同一个容器时,所有的窗口都会同步的显示,假如其中的一个窗口发生阻塞时,其它的窗口也会阻塞。
当试图进入一个已经停止的容器中时,则会提示你Container is not running:
06 — 与宿主机共享文件
容器技术使用了 rootfs 机制和 Mount Namespace构建出了一个同宿主机完全隔离开的文件系统环境。但是我们使用过程中经常会遇到这样两个问题:
容器里进程新建的文件,怎么才能让宿主机获取到?
宿主机上的文件和目录,怎么才能让容器里的进程访问到?
这正是 Docker Volume 要解决的问题:Volume 机制,允许你将宿主机上指定的目录,挂载到容器里面进行读取和修改。通过-v选项,可以宿主机目录~/work挂载进容器的 /test 目录当中:
这样,在容器flasky中 会创建/test 目录,在/test目录下创建的文件,在宿主机的目录/work中可看到。在宿主机的目录/work中创建的文件,在容器flasky中/test目录下也可以看到。
执行docker inspect CONTAINER_ID命令,命令输出的Mounts字段中Source的值就是宿主机上的目录,Destination是对应的容器中的目录:
强烈建议如上所示指明挂载宿主机的哪个目录。如果不显示声明宿主机目录,那么 Docker 就会在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。
想要查看宿主机临时目录的内容,需要先查看到VOLUME_ID,可以通过下面方式查看:
接着,如果是在MacOS电脑上,则执行下面两个命令:
如果是Linux电脑上,则不需要执行screen那个命令。
下面,实验一下在容器的/test目录下添加一个文件 text.txt 是否在宿主机中可以访问到,首先进入容器创建文件:
回到宿主机,就会发现 text.txt 已经出现在了宿主机上对应的临时目录里了:
将容器的目录映射到宿主机的某个目录,一个重要使用场景是持久化容器中产生的文件,比如应用的日志,方便在容器外部访问。强烈建议在
07 — 给容器加上资源限制
其实容器是运行在宿主机上的特殊进程,多个容器之间是共享宿主机的操作系统内核的。默认情况下,容器并没有被设定使用操作系统资源的上限。
有些情况下,我们需要限制容器启动后占用的宿主机操作系统的资源。Docker可以利用Linux Cgroups机制可以给容器设置资源使用限制。
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。Docker正是利用这个特性限制容器使用宿主上的CPU、内存。
下面启动容器的方式,给这个 Python 应用加上 CPU 和 Memory 限制:
–cpu-period和–cpu-quota组合使用来限制容器使用的CPU时间。表示在–cpu-period的一段时间内,容器只能被分配到总量为 --cpu-quota 的 CPU 时间。-m选项则限制了容器使用宿主机内存的上限。
上面启动容器的命令,将容器使用的CPU限制设定在最高20%,内存使用最多是300MB。
08 — 启动、重启、停止与删除容器
使用过docker ps 查看当前运行中的容器,如果加上 -a 选项,则可以查看运行中和已经停止的所有容器。现在,看一下我的系统中目前的所有容器:
从输出中可以看到目前有四个容器,有两个容器处于Up状态,也就是处于运行中的状态,一个容器处于Exited(0)状态,也就是退出状态,一个处于Created状态。
docker ps -a的输出结果,一共包含7列数据,分别是 CONTAINER ID、IMAGE、COMMAND、CREATED、STATUS、PORTS和NAMES。这些列的含义分别如下所示:
CONTAINER ID:容器ID,唯一标识容器
IMAGE:创建容器时所用的镜像
COMMAND:在容器最后运行的命令
CREATED:容器创建的时间
STATUS:容器的状态
PORTS:对外开放的端口号
NAMES:容器名(具有唯一性,docker负责命名)
获取到容器的ID之后,可以对容器的状态进行修改,比如容器1695ed10e2cb进行停止、启动、重启:
删除容器,有两种操作:
不带-f选项,只能删除处于非Up状态的容器,带上-f则可以删除处于任何状态下的容器。
容器可以先创建容器,稍后再启动。也就是可以先执行docker create 创建容器(处于 Created 状态),再通过docker start 以后台方式启动容器。docker run 命令实际上是 docker create 和 docker start 的组合。
09 — 维持容器始终保持运行状态
docker run指令有一个参数–restart,在容器中启动的进程正常退出或发生OOM时, docker 会根据 --restart 的策略判断是否需要重启容器。但如果容器是因为执行 docker stop 或docker kill 退出,则不会自动重启。
docker支持如下restart策略:
no – 容器退出时不要自动重启。这个是默认值。
on-failure[:max-retries] – 只在容器以非0状态码退出时重启。可选的,可以退出docker daemon尝试重启容器的次数。
always – 不管退出状态码是什么始终重启容器。当指定always时,docker daemon将无限次数地重启容器。容器也会在daemon启动时尝试重启容器,不管容器当时的状态如何。
unless-stopped – 不管退出状态码是什么始终重启容器。不过当daemon启动时,如果容器之前已经为停止状态,不启动它。
在每次重启容器之前,不断地增加重启延迟(上一次重启的双倍延迟,从100毫秒开始),来防止影响服务器。这意味着daemon将等待100ms,然后200 ms, 400 ms, 800 ms, 1600 ms等等,直到超过on-failure限制,或执行docker stop或docker rm -f。如果容器重启成功(容器启动后并运行至少10秒),然后delay重置为默认的100ms。
下面是两种重启策略:
可以通过docker inspect来查看已经尝试重启容器了多少次。例如,获取容器flasky的重启次数:
或者获取上一次容器重启时间:
10 — 总结
本篇文章以容器化 Python web应用为案例,讲解了 Docker 容器使用的主要场景。包括构建镜像、启动镜像、分享镜像、在镜像中操作、在镜像中挂载宿主机目录、对容器使用的资源进行限制、管理容器的状态和如何保持容器始终运行。熟悉了这些操作,也就基本上摸清了 Docker 容器的核心功能,在软件测试过程中遇到使用容器的场景,也就基本能搞定了。
参考资料
https://docs.docker.com/engine/reference/builder/#dockerfile-reference
https://stackoverflow.com/questions/38532483/where-is-var-lib-docker-on-mac-os-x