通过容器化一个Python Web应用学习Docker容器技术


【编者的话】容器在软件开发、测试和部署环节应用的越来越广泛,那么测试人员应该如何掌握容器技术呢?应该掌握哪些基本的容器操作呢?本文通过容器化一个 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

构建一个镜像

一个完整镜像通常包含应用本身和操作系统,当然还包含需要的依赖软件。

首先准备一个应用。新建一个本文文件,起名叫app.py,写入下面的内容,实现一个简单的Web应用:
from flask import Flask
import socket
import os

app = Flask(__name__)


@app.route('/')
def hello():
html = "<h3>Hello {name}!</h3>" \
       "<b>主机名:</b> {hostname}<br/>"
return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname())


if __name__ == "__main__":
app.run(host='0.0.0.0', port=8082)

在这段代码中,使用Flask框架启动了一个Web服务器,而它唯一的功能是:如果当前环境中有“NAME”这个环境变量,就把它打印在“Hello”后,否则就打印“Hello world”,最后再打印出当前环境的 hostname。

这个应用的依赖文件requirements.txt存在于与app.py同级目录中,内容是:
$ cat requirements.txt
Flask

将这样一个应用在容器中跑起来,需要制作一个容器镜像。Docker使用Dockerfile文件来描述镜像的构建过程。在本文中,Dockerfile内容定义如下:
# FROM指令指定了基础镜像是python:3.6-alpine,这个基础镜像包含了Alpine Linux操作系统和Python 3.6
FROM python:3.6-alpine
# WORKDIR指令将工作目录切换为/app
WORKDIR /app
# ADD指令将当前目录下的所有内容(app.py、requirements.txt)复制到镜像的 /app 目录下
ADD . /app
# RUN指令运行pip命令安装依赖
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
# EXPOSE指令暴露允许被外界访问的8083端口
EXPOSE 8083
# ENV指令设置环境变量NAME
ENV NAME World
# CMD指令设置容器内进程为:python app.py,即:这个 Python 应用的启动命令
CMD ["python", "app.py"]

这个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 ... rence

根据前面的描述,现在我们的整个应用的目录结构应该如下这样:
$ ls
Dockerfile  app.py   requirements.txt

执行下面的指令可以构建镜像:
$ docker build  -f /path/to/Dockerfile -t helloworld .
Sending build context to Docker daemon  4.608kB
Step 1/7 : FROM python:3.6-alpine
---> 5e7f84829665
Step 2/7 : WORKDIR /app
---> Using cache
---> dbb4a00a8f68
Step 3/7 : ADD . /app
---> fd33ac91c6c7
Step 4/7 : RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
---> Running in 6b82e863d802
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Collecting Flask
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/f2/28/2a03252dfb9ebf377f40fba6a7841b47083260bf8bd8e737b0c6952df83f/Flask-1.1.2-py2.py3-none-any.whl (94 kB)
Collecting click>=5.1
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/dd/c0/4d8f43a9b16e289f36478422031b8a63b54b6ac3b1ba605d602f10dd54d6/click-7.1.1-py2.py3-none-any.whl (82 kB)
Collecting Jinja2>=2.10.1
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/27/24/4f35961e5c669e96f6559760042a55b9bcfcdb82b9bdb3c8753dbe042e35/Jinja2-2.11.1-py2.py3-none-any.whl (126 kB)
Collecting itsdangerous>=0.24
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting Werkzeug>=0.15
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
Collecting MarkupSafe>=0.23
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz (19 kB)
Building wheels for collected packages: MarkupSafe
Building wheel for MarkupSafe (setup.py): started
Building wheel for MarkupSafe (setup.py): finished with status 'done'
Created wheel for MarkupSafe: filename=MarkupSafe-1.1.1-py3-none-any.whl size=12629 sha256=1f965945354a52423078c573deb1a8116965e67b2467c3640264d7f02058b06d
Stored in directory: /root/.cache/pip/wheels/06/e7/1e/6e3a2c1ef63240ab6ae2761b5c012b5a4d38e448725566eb3d
Successfully built MarkupSafe
Installing collected packages: click, MarkupSafe, Jinja2, itsdangerous, Werkzeug, Flask
Successfully installed Flask-1.1.2 Jinja2-2.11.1 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.1 itsdangerous-1.1.0
Removing intermediate container 6b82e863d802
---> d672a00c1a2f
Step 5/7 : EXPOSE 8083
---> Running in b9b2338da3f3
Removing intermediate container b9b2338da3f3
---> e91da5a22e20
Step 6/7 : ENV NAME World
---> Running in d7e5d19f3eed
Removing intermediate container d7e5d19f3eed
---> 4f959f34d486
Step 7/7 : CMD ["python", "app.py"]
---> Running in 99a97bedace0
Removing intermediate container 99a97bedace0
---> 3bc3e537ebb7
Successfully built 3bc3e537ebb7
Successfully tagged helloworld:latest

其中,-t 的作用是给这个镜像加一个 Tag,即:起一个好听的名字。docker build 会自动加载当前目录下的 Dockerfile 文件,然后按照顺序执行Dockerfile文件中的指令。

上面的命令执行完成后,就生成了一个镜像。可以通过下面的指令查看:
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
helloworld          latest              3bc3e537ebb7        2 minutes ago       103MB

还可以通过docker inspect helloworld:latest查看镜像的元信息:
$ docker inspect helloworld:latest
[
{
    "Id": "sha256:3bc3e537ebb79d26c6fdbcf841499f23d0a9c7726ad1f533f585fe677f8a9c6b",
    "RepoTags": [
        "helloworld:latest"
],
    "RepoDigests": [],
    "Parent": "sha256:4f959f34d486fe8c6127fb65609937dbac4923e56f652090e469d51264b5c4e0",
    "Comment": "",
    "Created": "2020-04-13T14:43:15.6562968Z",
    "Container": "99a97bedace054b2a3eee01eced0294e25602f3b53ffa8a39cce00209d051fc0",
    "ContainerConfig": {
        "Hostname": "99a97bedace0",
        "Domainname": "",
        "User": "",
        "AttachStdin": false,
        "AttachStdout": false,
        "AttachStderr": false,
        "ExposedPorts": {
            "8083/tcp": {}
        },
        "Tty": false,
        "OpenStdin": false,
        "StdinOnce": false,
        "Env": [
            "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "LANG=C.UTF-8",
            "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",
            "PYTHON_VERSION=3.6.10",
            "PYTHON_PIP_VERSION=20.0.2",
            "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py",
            "PYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189e",
            "NAME=World"
        ],
        "Cmd": [
            "/bin/sh",
            "-c",
            "#(nop) ",
            "CMD [\"python\" \"app.py\"]"
        ],
        "Image": "sha256:4f959f34d486fe8c6127fb65609937dbac4923e56f652090e469d51264b5c4e0",
        "Volumes": null,
        "WorkingDir": "/app",
        "Entrypoint": null,
        "OnBuild": null,
        "Labels": {}
    },
    "DockerVersion": "19.03.8",
    "Author": "",
    "Config": {
        "Hostname": "",
        "Domainname": "",
        "User": "",
        "AttachStdin": false,
        "AttachStdout": false,
        "AttachStderr": false,
        "ExposedPorts": {
            "8083/tcp": {}
        },
        "Tty": false,
        "OpenStdin": false,
        "StdinOnce": false,
        "Env": [
            "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "LANG=C.UTF-8",
            "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",
            "PYTHON_VERSION=3.6.10",
            "PYTHON_PIP_VERSION=20.0.2",
            "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py",
            "PYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189e",
            "NAME=World"
        ],
        "Cmd": [
            "python",
            "app.py"
        ],
        "Image": "sha256:4f959f34d486fe8c6127fb65609937dbac4923e56f652090e469d51264b5c4e0",
        "Volumes": null,
        "WorkingDir": "/app",
        "Entrypoint": null,
        "OnBuild": null,
        "Labels": null
    },
    "Architecture": "amd64",
    "Os": "linux",
    "Size": 103263332,
    "VirtualSize": 103263332,
    "GraphDriver": {
        "Data": {
            "LowerDir": "/var/lib/docker/overlay2/c349c378637d8211bb08eab95d5e7abdbf6d394c304ba57a64b8664a5c728b2a/diff:/var/lib/docker/overlay2/c042b9e207d25ca167ae375d7a312941f7f88ce6b441ced9eb0cc76556746c8f/diff:/var/lib/docker/overlay2/22bc7eaff7b47078258b461bb65430e13960c3350db7b54191b2174de5ff2dad/diff:/var/lib/docker/overlay2/fc429777fd588295c0e2c495ed3ebdabca23dc62d75b0265e7a4b2a324c33622/diff:/var/lib/docker/overlay2/9e497ccfb39b20ee332dc7c4b2f68de724e6a605a593af1852dc1512602ac35a/diff:/var/lib/docker/overlay2/4453a778a9bf6e17ceee3861a4183e9dc7a5e2a50d2d9fecf4e2cd4c2b042286/diff:/var/lib/docker/overlay2/520410b2e383a10d8c3b2e8d8f47a4e35c290691af2dc99c0fe75666b7eb2dcd/diff",
            "MergedDir": "/var/lib/docker/overlay2/559dbcb8413a066faa40522b411cf4d8712ba680cf89cb6a4e41577a961e5c25/merged",
            "UpperDir": "/var/lib/docker/overlay2/559dbcb8413a066faa40522b411cf4d8712ba680cf89cb6a4e41577a961e5c25/diff",
            "WorkDir": "/var/lib/docker/overlay2/559dbcb8413a066faa40522b411cf4d8712ba680cf89cb6a4e41577a961e5c25/work"
        },
        "Name": "overlay2"
    },
    "RootFS": {
        "Type": "layers",
        "Layers": [
            "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203",
            "sha256:d87eb7d6daff38d5b2dd47afce11b28cda4fb41fd1401f1c154437663ca51145",
            "sha256:00891a9058ec5ca0a3420a0307f4cdfaf6b58b8f1ec05d63e527e12fe3c69351",
            "sha256:9a8b7b2b0c33880049913fb325184f127d74f363102a5ac9bff26f0f0d749e9a",
            "sha256:a9a7f132e4de0299fa104c819e0accb4f2566137ee17f7f53cd8f2c67103e9e4",
            "sha256:46c42cfd4d054eec8c7452c41bbf78abba12a6feddcbf7832b47301c4ee5d413",
            "sha256:1af4857074cc9bd9a060613386068bcfc2ca06fae0df3690d840328070c9f4a0",
            "sha256:fc7b1fecdbe2f45d44d04b33017a2f89d2ac3928d2fb75dfb3db12738416b91f"
        ]
    },
    "Metadata": {
        "LastTagTime": "2020-04-13T14:43:15.6852866Z"
    }
}


元信息中包含了镜像的全部信息,包括镜像的tag,构建时间,环境变量等。

如果镜像不再需要了,可以通过docker image rm删除镜像。
$ docker image rm -f b054a66ef574
$ docker image rm b054a66ef574

运行镜像

有了镜像,就可以通过下面的指令来运行镜像得到容器了。
$ docker run -p 8082:8082 helloworld
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:8082/ (Press CTRL+C to quit)

上面命令中,镜像名helloworld后面,什么都不用写,因为在Dockerfile中已经指定了CMD。否则,我就得把进程的启动命令加在后面:
$ docker run -p 8082:8082 helloworld python app.py

从现在看,容器已经正确启动,我们使用curl命令通过宿主机的IP和端口号,来访问容器中的web应用。
$ curl http://0.0.0.0:8082/
<h3>Hello World!</h3><b>主机名:</b> 59b607239c3a<br/>

不过这里返回的主机名有点怪怪的,其实这个59b607239c3a就是容器的ID,可以通过运行docker ps指令查看运行中的容器。
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                              NAMES
59b607239c3a        helloworld          "python app.py"     3 seconds ago       Up 2 seconds        0.0.0.0:8082->8082/tcp, 8083/tcp   flasky

从输出中可以看到容器的ID,容器是基于哪个镜像的启动的,容器中的进程,容器的启动时间及端口映射情况,以及容器的名字。

使用docker inspect 59b607239c3a命令,可以查看容器的元数据,内容非常丰富。

分享镜像

大家一定用过代码分享平台GitHub,在Docker世界中分享镜像的平台是Docker Hub,它“学名”叫镜像仓库(Repository)。任何人都可以从上面拉取镜像或者Push自己的镜像上去。

为了能够上传镜像,首先需要注册一个 Docker Hub 账号,然后使用docker login命令登录:
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: liuchunming
Password:
Login Succeeded

在push到Docker Hub之前,需要先给镜像指定一个版本号:
$ docker tag helloworld liuchunming/helloworld:v1

liuchunming是我在Docker Hub 上的账户名。v1是我给这个镜像起的版本号。接着执行下面的指令就可以镜像push到Docker Hub上了:
$ docker push liuchunming/helloworld:v1

一旦提交到Docker Hub上,其他人就可以通过docker pull liuchunming/helloworld:v1将镜像下载下来了。

在企业内部,也可以搭建一个跟Docker Hub类似的镜像存储系统。感兴趣的话,可以查看VMware的Harbor项目。

镜像加速

鉴于国内网络问题,从https://hub.docker.com/拉取Docker镜像十分缓慢,我们可以需要配置加速器来解决。在Mac电脑任务栏,点击Docker Desktop应用图标 -> Perferences。在settings页面中进入Docker Engine修改和添加Docker daemon 配置文件即可。
1.jpeg

修改完成之后,点击Apply & Restart按钮,Docker就会重启并应用配置的镜像地址了。之后在拉取镜像时,将会快很多。

进入容器中玩玩

运行Web服务的容器,通常是以后台进程启动的。就是在docker run指令后面加上-d选项。比如以后台方式运行上面的Web容器:
$ docker run -d -p 8082:8082 --name flasky2 helloworld
cc733dd4310d40a10fe8093411abb002dfe18e7737e58c047910a4836424f746

如果想进入到一个正在运行的容器中做一些操作,可以通过docker exec指令:
$ docker exec -it flasky2 /bin/sh
/app #

-it选项指的是连接到容器后,启动一个terminal(终端)并开启input(输入)功能。-it后面接的是容器的名称,/bin/sh表示进入到容器后执行的命令。还可以通过容器的ID进入容器中,容器的ID可以通过docker ps命令查看。

docker exec的实现原理,其实是利用了容器的三大核心技术之一的Namespace。一个进程可以选择加入到某个进程(运行中的容器)已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的。更细节的原理这里不在细究。

进入到容器中,就可以在终端上进行一些操作了,比如在容器中新建一个readme.md文件:
/app # ps
PID   USER     TIME  COMMAND
1 root      0:00 python app.py
24 root      0:00 /bin/sh
29 root      0:00 ps
/app# touch readme.md
/app# exit

这个readme.md文件只会在这个容器中存在,用镜像启动的其他容器中不会有这个文件。

我们还可以将正在运行的容器,commit成新的镜像。
$ docker commit flasky2 liuchunming033/helloworld:v2

还有一种进入容器的方法是使用docker attach container_id,不过这种方法不建议使用,因为它有个明显的缺点:当多个窗口同时attach到同一个容器时,所有的窗口都会同步的显示,假如其中的一个窗口发生阻塞时,其它的窗口也会阻塞。

当试图进入一个已经停止的容器中时,则会提示你Container is not running:
$ docker exec -it flasky2 /bin/sh
Error response from daemon: Container cc733dd4310d40a10fe8093411abb002dfe18e7737e58c047910a4836424f746 is not running

与宿主机共享文件

容器技术使用了Rootfs机制和Mount Namespace构建出了一个同宿主机完全隔离开的文件系统环境。但是我们使用过程中经常会遇到这样两个问题:
  • 容器里进程新建的文件,怎么才能让宿主机获取到?
  • 宿主机上的文件和目录,怎么才能让容器里的进程访问到?


这正是Docker Volume要解决的问题:Volume机制,允许你将宿主机上指定的目录,挂载到容器里面进行读取和修改。通过-v选项,可以宿主机目录~/work挂载进容器的 /test 目录当中:
$ docker run -d -p 8082:8082 -v ~/work:/test --name flasky helloworld
574c252649cb3ef1824ce8b6151b2ce87b4512ba1bac08d0735b1676905e3161

这样,在容器flasky中 会创建/test目录,在/test目录下创建的文件,在宿主机的目录~/work中可看到。在宿主机的目录~/work中创建的文件,在容器flasky中/test目录下也可以看到。

执行docker inspect CONTAINER_ID命令,命令输出的Mounts字段中Source的值就是宿主机上的目录,Destination是对应的容器中的目录:
"Mounts": [
        {
            "Type": "bind",
            "Source": "/Users/chunming.liu/work",
            "Destination": "/test",
            "Mode": "",
            "RW": true,
            "Propagation": "rprivate"
        }
    ],

强烈建议如上所示指明挂载宿主机的哪个目录。如果不显示声明宿主机目录,那么 Docker 就会在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。

想要查看宿主机临时目录的内容,需要先查看到VOLUME_ID,可以通过下面方式查看:
$ docker volume ls
DRIVER              VOLUME NAME
local               24c7e73e88b23bdb198e190d9c3227201827735b1b92872c951f755847ff88ee

接着,如果是在MacOS电脑上,则执行下面两个命令:
$ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty
$ ls /var/lib/docker/volumes/24c7e73e88b23bdb198e190d9c322720182
7735b1b92872c951f755847ff88ee/_data/

如果是Linux电脑上,则不需要执行screen那个命令。

下面,实验一下在容器的/test目录下添加一个文件 text.txt 是否在宿主机中可以访问到,首先进入容器创建文件:
$ docker exec -it flasky /bin/sh
$ cd test/
$ touch text.txt

回到宿主机,就会发现 text.txt 已经出现在了宿主机上对应的临时目录里了:
$ ls /var/lib/docker/volumes/24c7e73e88b23bdb198e190d9c322720182
7735b1b92872c951f755847ff88ee/_data/
text.txt

将容器的目录映射到宿主机的某个目录,一个重要使用场景是持久化容器中产生的文件,比如应用的日志,方便在容器外部访问。强烈建议在

给容器加上资源限制

其实容器是运行在宿主机上的特殊进程,多个容器之间是共享宿主机的操作系统内核的。默认情况下,容器并没有被设定使用操作系统资源的上限。

有些情况下,我们需要限制容器启动后占用的宿主机操作系统的资源。Docker可以利用Linux Cgroups机制可以给容器设置资源使用限制。

Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。Docker正是利用这个特性限制容器使用宿主上的CPU、内存。

下面启动容器的方式,给这个Python应用加上CPU和Memory限制:
$ docker run -it --cpu-period=100000 --cpu-quota=20000 -m 300M helloworld

–cpu-period和–cpu-quota组合使用来限制容器使用的CPU时间。表示在–cpu-period的一段时间内,容器只能被分配到总量为 --cpu-quota 的 CPU 时间。-m选项则限制了容器使用宿主机内存的上限。

上面启动容器的命令,将容器使用的CPU限制设定在最高20%,内存使用最多是300MB。

重启、停止与删除

使用过docker ps查看当前运行中的容器,如果加上-a选项,则可以查看运行中和已经停止的所有容器。现在,看一下我的系统中目前的所有容器:
$ docker ps -a
CONTAINER ID   IMAGE        COMMAND          CREATED      STATUS                 PORTS                  NAMES
525a8c3fc769   helloworld   "python app.py"  4 hours ago  Up 3 minutes           80/tcp                 hardcore_feistel
1695ed10e2cb   helloworld   "python app.py"  4 hours ago  Up 3 minutes           0.0.0.0:5000->80/tcp   focused_margulis7
a242ecaf6cf6   helloworld   "python app.py"  5 hours ago  Exited (0) 4 hours ago                        dazzling_khayyam
be0439b30b2a   helloworld   "python app.py"  5 hours ago  Created                                       vigilant_laland

从输出中可以看到目前有四个容器,有两个容器处于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进行停止、启动、重启:
$ docker stop flasky
$ docker start flasky
$ docker restart flasky

删除容器,有两种操作:
$ docker rm flasky
$ docker rm -f flasky

不带-f选项,只能删除处于非Up状态的容器,带上-f则可以删除处于任何状态下的容器。

容器可以先创建容器,稍后再启动。也就是可以先执行docker create创建容器(处于Created状态),再通过docker start以后台方式启动容器。docker run命令实际上是docker create和docker start的组合。

维持容器运行状态

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,然后200ms,400ms,800ms,1600ms等等,直到超过on-failure限制,或执行docker stop或docker rm -f。如果容器重启成功(容器启动后并运行至少10秒),然后delay重置为默认的100ms。

下面是两种重启策略:
$ docker run --restart=always flasky # restart策略为always,使得容器退出时,Docker将重启它。并且是无限制次数重启。
$ docker run --restart=on-failure:10 flasky #restart策略为on-failure,最大重启次数为10的次。容器以非0状态连续退出超过10次,Docker将中断尝试重启这个容器。

可以通过docker inspect来查看已经尝试重启容器了多少次。例如,获取容器flasky的重启次数:
$ docker inspect -f "{{ .RestartCount }}" flasky

或者获取上一次容器重启时间:
$ docker inspect -f "{{ .State.StartedAt }}" 1695ed10e2cb

总结

本篇文章以容器化Python Web应用为案例,讲解了Docker容器使用的主要场景。包括构建镜像、启动镜像、分享镜像、在镜像中操作、在镜像中挂载宿主机目录、对容器使用的资源进行限制、管理容器的状态和如何保持容器始终运行。熟悉了这些操作,也就基本上摸清了Docker容器的核心功能,在软件测试过程中遇到使用容器的场景,也就基本能搞定了。

参考资料:


原文链接:https://mp.weixin.qq.com/s/0e5BXUGcfJNXu4-mHdoxMA

0 个评论

要回复文章请先登录注册