Py学习  »  docker

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

分布式实验室 • 4 年前 • 449 次点击  

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

  1. from flask import Flask

  2. import socket

  3. import os


  4. app = Flask(__name__)



  5. @app.route('/')

  6. def hello():

  7. html = "

    Hello {name}!

    " \

  8. "主机名: {hostname}
    "

  9. return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname())



  10. if __name__ == "__main__":

  11. app.run(host='0.0.0.0', port=8082)


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

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

  1. $ cat requirements.txt

  2. Flask


将这样一个应用在容器中跑起来,需要制作一个容器镜像。Docker使用Dockerfile文件来描述镜像的构建过程。在本文中,Dockerfile内容定义如下:

  1. # FROM指令指定了基础镜像是python:3.6-alpine,这个基础镜像包含了Alpine Linux操作系统和Python 3.6

  2. FROM python:3.6-alpine

  3. # WORKDIR指令将工作目录切换为/app

  4. WORKDIR /app

  5. # ADD指令将当前目录下的所有内容(app.py、requirements.txt)复制到镜像的 /app 目录下

  6. ADD . /app

  7. # RUN指令运行pip命令安装依赖

  8. RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

  9. # EXPOSE指令暴露允许被外界访问的8083端口

  10. EXPOSE 8083

  11. # ENV指令设置环境变量NAME

  12. ENV NAME World

  13. # CMD指令设置容器内进程为:python app.py,即:这个 Python 应用的启动命令

  14. 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/reference/builder/#dockerfile-reference。

根据前面的描述,现在我们的整个应用的目录结构应该如下这样:

  1. $ ls

  2. Dockerfile app.py requirements.txt


执行下面的指令可以构建镜像:

  1. $ docker build -f /path/to/Dockerfile -t helloworld .

  2. Sending build context to Docker daemon 4.608kB

  3. Step 1/7 : FROM python:3.6-alpine

  4. ---> 5e7f84829665

  5. Step 2/7 : WORKDIR /app

  6. ---> Using cache

  7. ---> dbb4a00a8f68

  8. Step 3/7 : ADD . /app

  9. ---> fd33ac91c6c7

  10. Step 4/7 : RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

  11. ---> Running in 6b82e863d802

  12. Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple

  13. Collecting Flask

  14. Downloading https://pypi.tuna.tsinghua.edu.cn/packages/f2/28/2a03252dfb9ebf377f40fba6a7841b47083260bf8bd8e737b0c6952df83f/Flask-1.1.2-py2.py3-none-any.whl (94 kB)

  15. Collecting click>=5.1

  16. Downloading https://pypi.tuna.tsinghua.edu.cn/packages/dd/c0/4d8f43a9b16e289f36478422031b8a63b54b6ac3b1ba605d602f10dd54d6/click-7.1.1-py2.py3-none-any.whl (82 kB)

  17. Collecting Jinja2>=2.10.1

  18. Downloading https://pypi.tuna.tsinghua.edu.cn/packages/27/24/4f35961e5c669e96f6559760042a55b9bcfcdb82b9bdb3c8753dbe042e35/Jinja2-2.11.1-py2.py3-none-any.whl (126 kB)

  19. Collecting itsdangerous>=0.24

  20. Downloading https://pypi.tuna.tsinghua.edu.cn/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)

  21. Collecting Werkzeug>=0.15

  22. Downloading https://pypi.tuna.tsinghua.edu.cn/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)

  23. Collecting MarkupSafe>=0.23

  24. Downloading https://pypi.tuna.tsinghua.edu.cn/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz (19 kB)

  25. Building wheels for collected packages: MarkupSafe

  26. Building wheel for MarkupSafe (setup.py): started

  27. Building wheel for MarkupSafe (setup.py): finished with status 'done'

  28. Created wheel for MarkupSafe: filename=MarkupSafe-1.1.1-py3-none-any.whl size=12629 sha256=1f965945354a52423078c573deb1a8116965e67b2467c3640264d7f02058b06d

  29. Stored in directory: /root/.cache/pip/wheels/06/e7/1e/6e3a2c1ef63240ab6ae2761b5c012b5a4d38e448725566eb3d

  30. Successfully built MarkupSafe

  31. Installing collected packages: click, MarkupSafe, Jinja2, itsdangerous, Werkzeug, Flask

  32. 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

  33. Removing intermediate container 6b82e863d802

  34. ---> d672a00c1a2f

  35. Step 5/7 : EXPOSE 8083

  36. ---> Running in b9b2338da3f3

  37. Removing intermediate container b9b2338da3f3

  38. ---> e91da5a22e20

  39. Step 6/ 7 : ENV NAME World

  40. ---> Running in d7e5d19f3eed

  41. Removing intermediate container d7e5d19f3eed

  42. ---> 4f959f34d486

  43. Step 7/7 : CMD ["python", "app.py"]

  44. ---> Running in 99a97bedace0

  45. Removing intermediate container 99a97bedace0

  46. ---> 3bc3e537ebb7

  47. Successfully built 3bc3e537ebb7

  48. Successfully tagged helloworld:latest


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

上面的命令执行完成后,就生成了一个镜像。可以通过下面的指令查看:

  1. $ docker image ls

  2. REPOSITORY TAG IMAGE ID CREATED SIZE

  3. helloworld latest 3bc3e537ebb7 2 minutes ago 103MB


还可以通过docker inspect helloworld:latest查看镜像的元信息:

  1. $ docker inspect helloworld :latest

  2. [

  3. {

  4. "Id": "sha256:3bc3e537ebb79d26c6fdbcf841499f23d0a9c7726ad1f533f585fe677f8a9c6b",

  5. "RepoTags": [

  6. "helloworld:latest"

  7. ],

  8. "RepoDigests": [],

  9. "Parent": "sha256:4f959f34d486fe8c6127fb65609937dbac4923e56f652090e469d51264b5c4e0",

  10. "Comment": "",

  11. "Created": "2020-04-13T14:43:15.6562968Z",

  12. "Container": "99a97bedace054b2a3eee01eced0294e25602f3b53ffa8a39cce00209d051fc0",

  13. "ContainerConfig": {

  14. "Hostname": "99a97bedace0",

  15. "Domainname": "",

  16. "User": "",

  17. "AttachStdin": false,

  18. "AttachStdout": false,

  19. "AttachStderr": false,

  20. "ExposedPorts": {

  21. "8083/tcp": {}

  22. },

  23. "Tty": false,

  24. "OpenStdin": false,

  25. "StdinOnce": false,

  26. "Env": [

  27. "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",

  28. "LANG=C.UTF-8",

  29. "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",

  30. "PYTHON_VERSION=3.6.10",

  31. "PYTHON_PIP_VERSION=20.0.2",

  32. "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py",

  33. "PYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189e",

  34. "NAME=World"

  35. ],

  36. "Cmd": [

  37. "/bin/sh",

  38. "-c",

  39. "#(nop) ",

  40. "CMD [\"python\" \"app.py\"]"

  41. ],

  42. "Image": "sha256:4f959f34d486fe8c6127fb65609937dbac4923e56f652090e469d51264b5c4e0",

  43. "Volumes": null,

  44. "WorkingDir": "/app",

  45. "Entrypoint": null,

  46. "OnBuild": null,

  47. "Labels": {}

  48. },

  49. "DockerVersion": "19.03.8",

  50. "Author": "",

  51. "Config": {

  52. "Hostname": "",

  53. "Domainname": "",

  54. "User": "",

  55. "AttachStdin": false,

  56. "AttachStdout": false ,

  57. "AttachStderr": false,

  58. "ExposedPorts": {

  59. "8083/tcp": {}

  60. },

  61. "Tty": false,

  62. "OpenStdin": false,

  63. "StdinOnce": false,

  64. "Env": [

  65. "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",

  66. "LANG=C.UTF-8",

  67. "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",

  68. "PYTHON_VERSION=3.6.10",

  69. "PYTHON_PIP_VERSION=20.0.2",

  70. "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py",

  71. "PYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189e",

  72. "NAME=World"

  73. ],

  74. "Cmd": [

  75. "python",

  76. "app.py"

  77. ],

  78. "Image": "sha256:4f959f34d486fe8c6127fb65609937dbac4923e56f652090e469d51264b5c4e0",

  79. "Volumes": null,

  80. "WorkingDir": "/app",

  81. "Entrypoint": null,

  82. "OnBuild": null,

  83. "Labels": null

  84. },

  85. "Architecture": "amd64",

  86. "Os": "linux",

  87. "Size": 103263332,

  88. "VirtualSize": 103263332,

  89. "GraphDriver": {

  90. "Data": {

  91. "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",

  92. "MergedDir": "/var/lib/docker/overlay2/559dbcb8413a066faa40522b411cf4d8712ba680cf89cb6a4e41577a961e5c25/merged",

  93. "UpperDir": "/var/lib/docker/overlay2/559dbcb8413a066faa40522b411cf4d8712ba680cf89cb6a4e41577a961e5c25/diff",

  94. "WorkDir": "/var/lib/docker/overlay2/559dbcb8413a066faa40522b411cf4d8712ba680cf89cb6a4e41577a961e5c25/work"

  95. },

  96. "Name": "overlay2"

  97. },

  98. "RootFS": {

  99. "Type": "layers",

  100. "Layers": [

  101. "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203",

  102. "sha256:d87eb7d6daff38d5b2dd47afce11b28cda4fb41fd1401f1c154437663ca51145",

  103. "sha256:00891a9058ec5ca0a3420a0307f4cdfaf6b58b8f1ec05d63e527e12fe3c69351",

  104. "sha256:9a8b7b2b0c33880049913fb325184f127d74f363102a5ac9bff26f0f0d749e9a",

  105. "sha256:a9a7f132e4de0299fa104c819e0accb4f2566137ee17f7f53cd8f2c67103e9e4",

  106. "sha256:46c42cfd4d054eec8c7452c41bbf78abba12a6feddcbf7832b47301c4ee5d413",

  107. "sha256:1af4857074cc9bd9a060613386068bcfc2ca06fae0df3690d840328070c9f4a0",

  108. "sha256:fc7b1fecdbe2f45d44d04b33017a2f89d2ac3928d2fb75dfb3db12738416b91f"

  109. ]

  110. },

  111. "Metadata": {

  112. "LastTagTime": "2020-04-13T14:43:15.6852866Z"

  113. }

  114. }

  115. ]


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

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

  1. $ docker image rm -f b054a66ef574

  2. $ docker image rm b054a66ef574


运行镜像


有了镜像,就可以通过下面的指令来运行镜像得到容器了。

  1. $ docker run -p 8082:8082 helloworld

  2. * Serving Flask app "app" (lazy loading)

  3. * Environment: production

  4. WARNING: This is a development server. Do not use it in a production deployment.

  5. Use a production WSGI server instead.

  6. * Debug mode: off

  7. * Running on http://0.0.0.0:8082/ (Press CTRL+C to quit)


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

  1. $ docker run -p 8082:8082 helloworld python app.py


从现在看,容器已经正确启动,我们使用curl命令通过宿主机的IP和端口号,来访问容器中的Web应用。

  1. $ curl http://0.0.0.0:8082/

  2. Hello World!</h3>主机名:b> 59b607239c3a<br/>


不过这里返回的主机名有点怪怪的,其实这个59b607239c3a就是容器的ID,可以通过运行docker ps指令查看运行中的容器。

  1. $ docker ps

  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

  3. 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命令登录:

  1. $ docker login

  2. 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.

  3. Username: liuchunming

  4. Password:

  5. Login Succeeded


在push到Docker Hub之前,需要先给镜像指定一个版本号:

  1. $ docker tag helloworld liuchunming/helloworld:v1


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

  1. $ 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 配置文件即可。


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


进入容器中玩玩


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

  1. $ docker run -d -p 8082:8082 --name flasky2 helloworld

  2. cc733dd4310d40a10fe8093411abb002dfe18e7737e58c047910a4836424f746


如果想进入到一个正在运行的容器中做一些操作,可以通过docker exec指令:




    
  1. $ docker exec -it flasky2 /bin/sh

  2. /app #


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

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

进入到容器中,就可以在终端上进行一些操作了,比如在容器中新建一个readme.md文件:

  1. /app # ps

  2. PID USER TIME COMMAND

  3. 1 root 0:00 python app.py

  4. 24 root 0:00 /bin/sh

  5. 29 root 0:00 ps

  6. /app# touch readme.md

  7. /app# exit


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

我们还可以将正在运行的容器,commit成新的镜像。

  1. $ docker commit flasky2 liuchunming033/helloworld:v2


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

当试图进入一个已经停止的容器中时,则会提示你Container is not running:

  1. $ docker exec -it flasky2 /bin/sh

  2. Error response from daemon: Container cc733dd4310d40a10fe8093411abb002dfe18e7737e58c047910a4836424f746 is not running


与宿主机共享文件


容器技术使用了Rootfs机制和Mount Namespace构建出了一个同宿主机完全隔离开的文件系统环境。但是我们使用过程中经常会遇到这样两个问题:

  • 容器里进程新建的文件,怎么才能让宿主机获取到?

  • 宿主机上的文件和目录,怎么才能让容器里的进程访问到?


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

  1. $ docker run -d -p 8082:8082 -v ~/work:/test --name flasky helloworld

  2. 574c252649cb3ef1824ce8b6151b2ce87b4512ba1bac08d0735b1676905e3161


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

执行docker inspect CONTAINER_ID命令,命令输出的Mounts字段中Source的值就是宿主机上的目录,Destination是对应的容器中的目录:

  1. "Mounts": [

  2. {

  3. "Type": "bind",

  4. "Source": "/Users/chunming.liu/work",

  5. "Destination": "/test",

  6. "Mode": "",

  7. "RW": true,

  8. "Propagation": "rprivate"

  9. }

  10. ],


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

想要查看宿主机临时目录的内容,需要先查看到VOLUME_ID,可以通过下面方式查看:


  1. $ docker volume ls

  2. DRIVER VOLUME NAME

  3. local 24c7e73e88b23bdb198e190d9c3227201827735b1b92872c951f755847ff88ee


接着,如果是在MacOS电脑上,则执行下面两个命令:

  1. $ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty

  2. $ ls /var/lib/docker/volumes/24c7e73e88b23bdb198e190d9c322720182

  3. 7735b1b92872c951f755847ff88ee/_data/


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

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

  1. $ docker exec -it flasky /bin/sh

  2. $ cd test/

  3. $ touch text.txt


回到宿主机,就会发现 text.txt 已经出现在了宿主机上对应的临时目录里了:

  1. $ ls /var/lib/docker/volumes/24c7e73e88b23bdb198e190d9c322720182

  2. 7735b1b92872c951f755847ff88ee/_data/

  3. text.txt


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


给容器加上资源限制


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

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

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

下面启动容器的方式,给这个Python应用加上CPU和Memory限制:

  1. $ 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选项,则可以查看运行中和已经停止的所有容器。现在,看一下我的系统中目前的所有容器:

  1. $ docker ps -a

  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

  3. 525a8c3fc769 helloworld "python app.py" 4 hours ago Up 3 minutes 80/tcp hardcore_feistel

  4. 1695ed10e2cb helloworld "python app.py" 4 hours ago Up 3 minutes 0.0.0.0:5000->80/tcp focused_margulis7

  5. a242ecaf6cf6 helloworld "python app.py" 5 hours ago Exited (0) 4 hours ago dazzling_khayyam

  6. 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进行停止、启动、重启:


  1. $ docker stop flasky

  2. $ docker start flasky

  3. $ docker restart flasky


删除容器,有两种操作:

  1. $ docker rm flasky

  2. $ 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。

下面是两种重启策略:

  1. $ docker run --restart=always flasky # restart策略为always,使得容器退出时,Docker将重启它。并且是无限制次数重启。

  2. $ docker run --restart=on-failure :10 flasky #restart策略为on-failure,最大重启次数为10的次。容器以非0状态连续退出超过10次,Docker将中断尝试重启这个容器。


可以通过docker inspect来查看已经尝试重启容器了多少次。例如,获取容器flasky的重启次数:

  1. $ docker inspect -f "{{ .RestartCount }}" flasky


或者获取上一次容器重启时间:

  1. $ docker inspect -f "{{ .State.StartedAt }}" 1695ed10e2cb


总结


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

文章来源:明说软件测试,点击查看原文

扫描下方二维码进群,一起学习Python。


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/113616
 
449 次点击