12

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

 4 years ago
source link: http://dockone.io/article/10099
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

2MNRnaQ.jpg!web

修改完成之后,点击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容器的核心功能,在软件测试过程中遇到使用容器的场景,也就基本能搞定了。

参考资料:

原文链接:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK