|
|
爱笑的小刀 · 基于pytorch的针对五分类问题,混淆矩阵 ...· 1 年前 · |
|
|
心软的小虾米 · java - Why can I not ...· 2 年前 · |
|
|
迷茫的红豆 · file handling - ...· 2 年前 · |
|
|
幸福的马铃薯 · javascript - ...· 3 年前 · |
Docker是一个容器化软件,所谓容器化即操作系统级别的虚拟化(Operating-system-level virtualization)。比起硬件虚拟化:
容器化软件允许在 同一个操作系统内核下存在多个相互隔离的用户空间实例 ,这些实例即被称为容器(Container)。从这些容器的所有者/用户的角度来看,它们就像是一个独立的服务器一样。除了隔离机制之外,容器化软件通常提供资源管理功能,限制一个容器的活动对其它容器的影响。
内核中用于支持容器化的特性:
构建(Build)、分发(Ship)、运行(Run) 是Docker提出的宣传口号,它的目标就是高效的完成这三件事,提高开发、测试、运维的效率:
下图阐述了Docker和硬件虚拟化的区别:
从用户的角度来看,使用Docker的典型工作流如下:
Docker的核心组件,它负责创建Docker镜像、运行Docker容器
从1.12版本开始,Docker引擎支持Swarm mode
包括三个组件:
层,或者叫镜像层(Image layer),是指对 镜像的一个变更,或者指一个中间镜像 (intermediate image)。之所以叫层,和UFS(联合文件系统)有关,UFS允许:其包含的文件和目录可以分布在多个其它文件系统中,这些文件/目录(称为Branches)可以被叠加(overlay)形成单个新的文件系统
在Dockerfile中指定的指令,例如FROM/RUN/COPY,会导致先前的镜像发生变化,因而导致创建新的层
层有利于缩短构建的时间,Dockerfile发生变化后,变化之前的中间镜像不需要重新构建,可以作为缓存使用
Docker ContainerDocker容器是Docker镜像的运行时实例
容器的行为取决于镜像如何被配置,可能是简单的执行一条命令,也可能是启动数据库这样的复杂服务
Docker Hub一种服务,用于构建、管理镜像。其角色类似于Maven仓库或者PyPI
在Docker Hub上你可以很轻松下载到大量已经容器化的应用镜像,即拉即用。这些镜像中,有些是Docker官方维护的,更多的是众多开发者自发上传分享的
你可以将Github账号绑定到Docker Hub账号,并配置自动生成镜像的功能。这样,当Github中代码更新时,Docker镜像会自动更新
Docker Trusted Registry DTR,企业级的Docker镜像存储方案,其角色类似于Docker Hub的私服 Docker Cloud 一种服务,能够构建、测试、部署镜像到你的主机上 Docker Universal Control Plane UCP,管理Docker宿主机的集群,让它们整体上表现的像是单台机器 Docker Machine 一个工具,使用它你可以:当前,只有通过Docker Machine,你才能在Mac或者Windows上运行Docker。同时它也是管理 大量 基于各种Linux变体的宿主机的便捷方法
libcontainer 封装了名字空间、控制组、UnionFS的库,提供容器运行时基础功能本章主要介绍Ubuntu 14.04 LTS下安装、配置Docker的步骤。
你需要安装64bit的操作系统,内核的最低版本是3.10。为了支持aufs存储驱动,最好安装额外的内核包:
Docker最初是为Linux开发的,在Windows/Mac系统中,你可以借助Docker Machine来运行Docker。
从1.13开始,Windows和Mac具有基于原生虚拟化机制的Docker实现,不再依赖于Docker Machine。在Windows下,Docker基于Microsoft Hyper-V;在Mac下则基于HyperKit。
为Bash添加自动完成支持:
执行一些必要的配置,可以让Ubuntu和Docker更好的一起工作。Docker配置文件默认为/etc/default/docker。
Docker的守护程序绑定到Unix套接字(而不是TCP/IP套接字)。默认的,该套接字的所有者是root,其它用户要访问它必须sudo。为此,Docker守护程序总是以root身份运行。
为了避免在调用 docker 命令时必须sudo,可以创建一个名为docker的UNIX组,并把你的用户添加到该组中。Docker守护程序启动时会为该组赋予读写权限
执行以下命令:
注意:启用后,即使不使用Docker,也会消耗额外1%左右的内存、降低10%左右的性能。
如果在Docker的宿主机上使用 UFW ,你需要额外的配置——由于UFW的默认行为是丢弃所有转发(路由)包,这会导致Docker无法正常工作。执行以下修改:
Ubuntu及其衍生的桌面版Linux,通常会自动设置/etc/resolv.conf,将127.0.0.1作为默认DNS,同时网络管理器启用dnsmasq,将DNS请求代理给真实的DNS服务器
在这样的配置下,启动Docker容器会导致如下警告:
/ etc / default / docker 设置 DOCKER_OPTS = "--dns 10.0.0.1" ,其中10.0.0.1替换为你的内网DNS服务器。注意 -- dns 选项可以指定多次,对应多个备选DNS服务器你也可以修改网络管理器配置,/etc/NetworkManager/NetworkManager.conf,禁用dnsmasq:
此外,也可以 注册阿里云开发者账号 ,获得自己的Mirror URL。
默认的,当Docker守护程序退出时,所有正在运行的容器被自动关闭。自1.12开始,你可以配置守护程序,让容器在守护程序退出后保持运行状态。
要启用该特性,可以使用选项 sudo dockerd -- live - restore ,或者在/etc/docker/daemon.json中配置:
本节我们以whalesay镜像为基础,学习创建自己的Docker镜像
在前面的章节我们提到过,Dockerfile用于描述镜像如何被构建。创建镜像的第一步就是编写Dockerfile。
新建一个目录作为构建镜像的上下文目录,所谓上下文目录,意味着构建过程所需的全部文件都位于其中:
首先你需要 注册 一个账号, 然后创建一个仓库(Repository),这里的仓库类似于Git仓库,它以你的Docker Hub用户名作为默认的名字空间,例如gmemcc/mywhalesay。
标签(tag)是用于 区分镜像变体 的一种方法。
利用tag命令,你可以把一个本地镜像关联到Docker Hub仓库。首先,查询镜像的ID:
所谓数据卷(Data volumes),是一个或者多个容器中 特定的目录 ,这些目录 绕过容器的联合文件系统 (UFS,可以将不同物理位置合并mount到Linux目录树的同一位置) ,数据卷的一系列特性有利于数据的持久化、共享:
注意: 作为挂载点的容器目录,其原有的文件全部不可见 ,不会进行Overlay。
要添加匿名数据卷,可以在启动容器时指定-v参数:
上面的例子中,如果容器的基础镜像已经包含了/webapp目录,则它会被宿主机的/src/webapp覆盖,但是容器从基础镜像得到的/webapp中的内容不会被删除,一旦数据卷被卸载,则/webapp中的内容恢复原样。
挂载单个宿主机文件也被支持: - v ~ / . bash_history : / root / . bash_history
除了挂载宿主机的本地目录,你也可以挂载共享存储为数据卷,Docker通过Volume plugins来支持iSCSI、NFS、FC等共享存储。使用共享存储的好处是它们是不依赖于主机的。
你可以在启动容器时,即时的在共享存储上创建 命名卷(named volume) :
数据卷可以挂载为只读: - v / src / webapp : / webapp : ro #添加:ro后缀
可以参考下面的命令,来备份一个数据卷:
如果需要在多个容器之间共享持久化数据,或者期望在非持久化容器中使用持久化数据,最好的方法是创建命名数据卷容器——仅仅为了提供共享数据卷的容器,并在其中挂载数据卷供其它容器引用。
首先创建一个命名的容器,但是不需要它执行任何命令:
这样启动db1、db2后,如果postgres镜像包含/dbdata目录,它将被来自dbstore的数据卷mask掉,仅dbstore的/dbdata对于db1、db2可见。
选项--volumes-from 可以指定多次,这样你可以联合使用来自多个容器的数据卷。
选项--volumes-from可以通过链式的结构扩展——链条上的后面的容器可以使用前面任意容器声明的数据卷:
所谓基本镜像(Base image),一般是指没有父镜像的镜像。此类镜像打包一个空白的操作系统。制作基本镜像的步骤依赖于你想打包的Linux发行版。
一般情况下,你可以在这样的Linux操作系统下完成基本镜像的创建——该操作系统就是你希望打包的Linux发行版。
某些工具可以简化镜像的创建,例如 Debootstrap 可以安装一个Debian/Ubuntu的基本操作系统到一个目录中。使用该工具的示例:
该主题包含以下关注点:
如果你使用了桌面版本的Docker,或者使用高于4.8+内核的Linux,你可以直接在x86_64的机器上,运行各种其它体系结构的镜像。
在Linux上启用Docker与QEMU集成的方式:
这是一个Docker CLI插件,可以通过BuildKit进行Docker镜像的构建。使用buildx,你可以为不同体系结构构建镜像,并合并在一个镜像清单中,不需要作Dockerfile或源代码上的变动。
buildx把工作委托给builders:
buildx将使用BuildKit引擎进行构建,不需要设置 DOCKER_BUILDKIT = 1 环境变量。
buildx支持docker build的所有特性,包括19.03引入的输出配置、内联build缓存、指定目标platform。此外,buildx还支持manifest、分布式缓存、导出为OCI格式等docker build不支持的特性。
buildx可以 在不同配置下运行,每个配置称为driver 。默认使用编译到Docker守护进程中的BuildKit库,此驱动称为docker,该驱动使用你本地的Docker守护进程,并提供和docker build相似的体验。此外,你还可以使用docker-container驱动,它在容器中启动BuildKit。
驱动docker的输出,自动在docker images列表中可见。对于其它驱动,输出到何处需要 -- output 来指定。
通过创建新的builder实例,可以得到隔离的构建环境(不改变共享的Docker守护进程的状态),在CI中较为有用。 你甚至可以在远程Docker守护进程上创建多个builder,形成builder farm,并随意在这些builder之间切换。
Docker 19.03引入了类似Kubectl的context特性,可以为一个远程Docker守护进程的API端点提供一个名称。对于每个context,buildx会生成一个默认的builder实例。
执行命令: docker buildx install ,则docker build命令变为docker buildx的别名,这就意味着docker build自动使用docker buildx进行构建。
docker buildx uninstall 移除别名。
Docker读取Dockerfile中的指令以构建新的镜像,Dockerfile中的指令说明了镜像应该如何一步步的被构建。
所谓上下文,是指定PATH(宿主机本地目录)或者URL(Git存储库)中的文件集合。上下文会被递归的处理,即子目录被包含在上下文中。在大部分情况下,最好将空白目录作为上下文,其中仅包含一个Dockerfile和构建过程必备的文件。
要使用上下文中的文件,你必须编写特定的指令,例如COPY。你可以在上下文目录中添加一个 . dockerignore 来排除某些文件、目录以提高构建性能。
按照约定,在上下文根目录中的名为Dockerfile的文件被作为“Dockerfile”,但是你可以指定任意文件作为“Dockerfile”:
Docker守护程序会逐条的执行Dockerfile中的指令,如果必要,将指令的执行结果提交到正在构建的那个新的镜像中去。在最终输出镜像ID之前,守护程序会自动清理客户端发送的上下文。
需要注意的是,每条指令都是 独立的执行 的,因此 RUN cd / tmp 这样的指令不会对下一条指令的“工作目录”产生影响。
为了提高构建的性能,Docker会重用中间的(intermediate )镜像(所谓缓存)。当使用缓存时,Docker会在控制台打印 Using cache 字样。
Dockerfile中只有注释、指令两类元素:
指令名称大小写不敏感,通常使用全大写。 第一条指令必须是FROM ,指定基础镜像。 以#开头的行通常被作为注释看待,除非它是合法的解析器指令(parser directive)。
解析器指令(directive)影响Dockerfile中其它行的处理方式,该指令不会增加额外的层,也不会显示为构建步骤。
解析器指令的语法类似于一种特殊的注释: # directive=value ,单个指令仅能被使用一次。一旦处理过任何注释、空行、指令(instruction),Docker就不再尝试分析任何解析器指令,因此 解析器指令必须位于Dockerfile的最开始处 。
解析器指令同样是大小写不敏感的,但是通常都使用全小写。在解析器指令之后,通常留有空白行。编写解析器指令时不得使用行连接符(\)
目前支持的解析器指令包括:
解析器指令 ENV 指令可以声明环境变量,在Dockerfile中你可以声明Bash风格的变量替换: $ variable_name 或者 $ { variable_name } 。除了这两种基本格式以外,还可以使用某些Bash修饰符:$ { variable : - word } ,如果设置了环境变量variable,表达式的结果是$variable,否则是word $ { variable : + word } ,如果设置了环境变量variable,表达式的结果是word,否则是空串
以下指令 支持变量替换:ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL 。在1.4+,当ONBUILD与前述指令一起使用时,也支持环境变量。
注意:环境变量的值在同一个指令中保持不变,考虑下面的片断:
Docker客户端发送上下文到守护程序之前,会读取上下文根目录下名为.dockerignore的文件。如果此文件存在,则客户端会依据其中声明的规则来排除掉上下文中的部分文件或者目录。使用.dockerignore可以避免过多的、敏感的文件到守护程序,并被ADD/COPY命令复制到镜像中。
此文件中的每行是一个UNIX glob风格的匹配Pattern,上下文根目录被作为此Pattern的根目录,下面是一些示例:
Pattern该指令在当前intermediate镜像之上新建一层,并在其中执行任意命令,然后提交结果。提交结果后形成的新intermediate镜像被下一个指令使用。
运行RUN指令时触发分层(Layering)符合Docker的核心理念——提交(Commit)操作想对廉价,并且可以从镜像历史的任意位置(任意一层)创建容器。分层也是构建过程中镜像缓存的基础。
使用exec格式可以避免Shell字符串相关的陷阱,也可以在Base镜像中没有/bin/sh程序的情况下执行命令。使用exec格式时需要注意:
RUN指令导致的镜像缓存不会自动失效,要禁用缓存,可以在构建时指定 -- no - cache 选项。ADD指令可以导致RUN的缓存失效。
可以用SHELL指令,设置shell格式的RUN指令所使用的Shell程序,其它几个具有exec格式/shell格式区分的指令,也受到SHELL指令的影响。
该指令具有三种格式:
Dockerfile中 仅能包含一个CMD指令 , 如果指定了多个CMD指令则仅最后一个有效。该指令的主要意图是为执行容器(executing container)提供默认值(defaults),这些默认值可以包含一个可执行文件,与ENTRYPOINT指令联用时,则仅仅包含执行选项。
使用CMD指令时要注意:
如果使用了CMD指令,并且运行容器时没有指定需要执行的命令,则CMD中的命令会被执行。要 让容器每次都运行同一个程序 ,应当联合使用ENTRYPOINT、CMD。
指令格式: LABEL < key >= < value > < key >= < value > < key >= < value > . . .
该指令为镜像添加元数据(metadata),每个标签是一个键值对。如果需要为镜像添加多个标签,最好在单个LABEL指令中完成。下面是一些示例:
指令格式: EXPOSE < port > [ < port > . . . ]
该指令声明容器在运行时侦听的端口。该指令并不会让容器和宿主机之间发生端口映射,要真正映射(发布)端口,必须在创建容器时使用选项:
该指令支持两种格式:
设置 构建过程、运行期间均可见的环境变量 ,这些环境变量对任何后代Dockerfile都可用。
部分Dockerfile指令或者指令的某种形式,支持引用环境变量,语法和Bash一致。
在运行期间,可以使用docker inspect查看环境变量。在命令行中,可以使用 -- env < key >= < value > 覆盖环境变量设置。
该指令支持两种格式:
使用入口点,可以如同使用可执行文件那样运行一个镜像。docker run时,镜像名后面的所有参数,都 作为参数传递给ENTRYPOINT指定的命令/可执行文件 ,并且覆盖CMD中指定的默认参数。当镜像名和可执行文件名一致的时候,dock run看起来就好像在执行一个文件,而不是运行镜像。
shell格式的调用,阻止CMD指定的默认参数和dock run指定的参数 。但是这种格式下可执行文件将作为/bin/sh -c的子命令运行,这导致可执行文件的PID不是1并且不能接收UNIX信号。你的可执行文件将无法接收docker stop发送来的SIGTERM信号。
如果该指令使用多次,则仅仅最后一个被使用。
你可以使用 -- entrypoint 选项覆盖ENTRYPOINT,但是只能指定执行的程序:
使用该格式,可以方便的设置容器执行的 默认命令 及其 稳定的默认参数 : ENTRYPOINT [ "top" , "-b" ]
然后,可以联用CMD指令,设置可 覆盖的默认参数 : CMD [ "-c" ]
下面的例子,在前台(PID为1)运行Apache服务器:
这种方式指定的入口点,将在Shell中(默认 /bin/sh -c) 执行指定的程序,能够进行变量替换。该方式会 忽略CMD指令和docker run image 后面的参数 。
为了确保长时间运行的入口点程序能够正确接收docker stop发来的UNIX信号,应当使用exec来执行目标程序,例如: ENTRYPOINT exec top - b
指令格式:
指令格式: USER username - or - uid ,指定RUN,CMD,ENTRYPOINT指令以什么身份运行。
在我机器上尝试MySQL镜像时,设置USER为mysql,生效的ID为999,而mysql用户的真实ID是118。直接设置USER为118,没有问题。
指令格式: WORKDIR / path / to / workdir
指定RUN, CMD, ENTRYPOINT, COPY,ADD指令的工作目录,你可以多次使用该指令,并且可以使用相对(于上一个WORKDIR)路径。示例:
ARG < name > [ = < default value > ] #该指令可以声明多次,也可以指定一个默认值
该指令定义一个 构建时变量 。变量的值可以通过 docker build -- build - arg < varname >= < value > 指定、覆盖。如果用户通过build-arg传递没有通过ARG声明的变量,构建会失败。
ARG定义的变量,对Dockerfile 当前行之后的指令 生效。ARG变量 不会持久化 到镜像中。
使用ENV、ARG都可以向RUN指令传递变量,如果这两个指令声明 同名的变量,则ENV总是覆盖ARG 。
以下ARG是Docker预定义的,不需要声明即可使用:http_proxy、https_proxy、ftp_proxy、no_proxy以及这4个变量的全大写版本。
指令格式: ONBUILD [ INSTRUCTION ]
为镜像添加一个触发器指令( trigger instruction),该指令会在 当前镜像被作为其它构建的Base镜像时执行 。ONBUILD指定的 指令 会在子镜像构建时、 在子镜像的构建上下文中 立即执行,就好像把该 指令 直接插入到FROM指令后面一样。
所有构建指令都可以通过ONBUILD注册为触发器。
当你制作一个专用于被扩展的Base镜像ONBUILD很有用。举例来说,假设你构建了一个Pyhton的应用编译环境(Base镜像),它要求被构建的Python源码被放置在特定的目录,并在之后调用编译脚本。你无法在构建Base镜像的时候使用ADD、RUN完成前述的工作,因为Base镜像的构建者并不知道源码在哪——每个具体应用程序的源码位置都不同。这时,ONBUILD可以帮助你:
下面是一个具体的例子:
该指令告知Docker,如何确认容器仍然在正常工作。正常工作不仅仅要求进程在运行,还要求特定于具体应用的检测可以通过(例如对于Web应用,能够正常处理HTTP登录请求)。
当指定了该指令后,在normal status之外,还多了一个health status。该状态的初始值为starting,当健康检测通过后,变为healthy;当检测不通过、或者若干次检测失败后,变为unhealthy。
command的退出状态用于判断容器是否健康:0 表示健康;1表示不健康;2为暂不使用的保留值。command的输出到stdout/stderr的信息,可以通过docker inspect命令查看到。
每个Dockerfile仅支持一个HEALTHCHECK指令,如果指定了多个,只有最后一个生效。
指令格式: SHELL [ "executable" , "parameters" ]
用于覆盖那些使用shell格式的指令所使用的默认Shell:
在Windows平台上该指令很有用,因为Windows提供了两个常用却完全不同的Shell:cmd和powershell。
SHELL指令可以出现多次,每次均覆盖之前的取值。
你可以在Dockerfile中使用多个FROM语句,每个FROM触发一个新的构建阶段(Stage)。你可以选择性的把某些构建从一个阶段拷贝到另外一个,而把任何不需要引入最终镜像的文件丢弃 —— 例如构建工具、依赖包。
下面是一个例子:
为了编写易用、有效的Dockerfile,Docker官方发布了若干条最佳实践。如果你要构建官方镜像,则必须遵守这些实践。
容器的生命周期应该尽可能的短暂,创建、启动、停止、删除应当消耗最小化的时间。这意味着你应该尽可能的通过Dockerfile把内容放在镜像内。
大部分情况下,可以把Dockerfile放置在空白的目录中,后续仅在此目录中存放构建镜像时所必须的文件。
如果目录中包含一些你需要排除出构建过程的目录/文件,可以编写一个.dockerignore文件,其格式类似于 .gitignore。
尽可能少的安装Linux软件包,这样可以降低镜像的大小、构建过程的耗时
大部分情况下,你应该在单个容器中运行仅单个程序(例如Web服务、DB服务)。将应用程序解耦到不同容器中可以更好的实现容器重用、水平扩展。
当一个程序依赖于另外一个时,可以使用容器链接(container linking)。
应当十分谨慎的考虑Dockerfile使用的层数(Number of layers),在Dockerfile可读性(可维护性)和最小化层数之间寻求平衡。
尽可能的按照字典序对参数(特别是apt安装的软件包参数)进行排序,这样可以避免后续维护者添加重复的安装包。
在构建镜像时,Docker会遍历你的Dockerfile的指令,检查完这些指令后,它会到缓存中寻找已经构建的镜像,而不是构建“重复”镜像。要避免使用缓存,可以在docker build时指定 -- no - cache = true 参数。
如果使用缓存,你必须明白它是如何寻找“重复”镜像的:
当找不到匹配的镜像时,Docker将从头开始,构建一个新镜像。
为了可读性、可维护性,应该把复杂的RUN语句编写在多行中,并用反斜杠分隔
apt-get
大部分RUN指令都是调用apt-get来安装软件包的。注意避免使用upgrade、dist-upgrade子命令,因为某些基本的软件包不能在非特权容器中升级。
应当总是同时使用update子命令和install子命令,例如:
因为单独使用RUN apt-get update指令,会导致软件包元信息驻留Docker缓存,以后的构建可能不更新软件包元信息。当然,你也可以指定软件包版本,这也可能避免上述缓存问题
一个RUN指令规范例子如下:
该指令用于运行镜像中包含的软件,调用格式一般为: CMD [ "executable" , "param1" , "param2" … ]
对于那些作为服务的软件,应该让它在前端执行(而不是在fork出的子进程中执行),例如运行Apache时应该使用指令 CMD [ "apache2" , "-DFOREGROUND"
对于大部分其它软件,CMD指令应该给出一个交互式的Shell,例如 CMD [ "perl" , "-de0" ] 、 CMD [ "python" ]
EXPOSE该指令声明容器用来监听外部连接的端口,你应该使用约定俗成的端口:
可以使用该指令更新PATH环境变量的值,以简化软件的调用。例如: ENV PATH / usr / local / nginx / bin : $ PATH 可以让Nginx通过简单的指令 CMD [ “ nginx ” ] 运行。
某些软件需要环境变量的设置,亦可通过该指令完成
ADD两者功能类似,COPY应该更优先使用,因为它比ADD简单直观。COPY仅支持将本地文件拷贝到容器内这样的基本操作,而ADD则支持一些高级特性:
最常见的需要使用ADD的场景是解压文件到镜像中: ADD rootfs . tar . xz / .
如果在Dockerfile中存在多个步骤需要复制文件,不要把它们合并到单个COPY指令中。这样做的原因还是缓存,分开COPY可以减少不必要的缓存失效
ENTRYPOINT该指令最佳应用场景是设置镜像的main命令,让容器像一个可执行文件那样运行。你可以配合使用CMD指令,来指定main命令的默认选项:
由于镜像名、命令名相同, 省略了其中之一的效果,就好像docker run在直接执行一个命令
也可以将ENTRYPOINT设置为一个脚本文件的路径: ENTRYPOINT [ "/docker-entrypoint.sh" ] ,此脚本中的 $ @ 对应docker run imgname后面的整个参数数组
VOLUME 使用该指令暴露所有数据库的存储区域、配置文件的存储、以及容器创建的文件/目录,总之,所有容易变化的文件系统部分,都应该使用该指令暴露如果容器运行的服务不需要特权,则应该使用该指令指定一个非root用户,但是注意要在Dockerfile中预先添加这样的用户,例如:
要有效利用Docker的存储机制,首先需要搞清楚它是如何构建、存储镜像的,然后要理解镜像是如何被容器使用的。
一个镜像文件由一系列层层叠加的、只读的层(Layers)构成,这些层共同组成了镜像的根文件系统。Docker的存储驱动负责管理这些层,对外提供单一的文件系统视图。
当你创建一个容器时,Docker会在镜像的层叠之上,添加一个薄的、可读写的 容器层 (container layer)。容器运行过程中对文件系统的任何更改(增加、修改、删除文件),都持久化到容器层中。
所有镜像、容器的层都位于本地文件系统中,由存储驱动管理。在Linux下,默认存储目录为/var/lib/docker/。
下图是基于Ubuntu:15.04的容器的文件系统层叠示意图:
Docker从1.10开始,引入了一个新的内容寻址存储(Content addressable storage,CAS)模型。该模型提供了在磁盘上寻址镜像、层数据的全新方法。在此之前,镜像、层数据使用随机的UUID来引用和存储,CAS则使用内容哈希值(content hash)来引用镜像、层数据。
CAS增强了安全性,内置了防止ID冲突的机制,并且能在pull、push、load、save操作后保证数据完整性。它也提供了更好的层共享机制——允许很多镜像自由的共享层,即时这些层来自不同的构建。
使用CAS后,所有层的ID均变成基于其内容的哈希值。但是,容器的ID仍然是随机的UUID。
这一变化导致,从老版本的Docker升级到1.10后:
从存储角度来看,镜像与容器的主要区别就在于后者的Layer栈顶部包含一个薄的可读写层。当容器创建后,该读写层一同创建;当容器删除后,该读写层也被删除;容器运行时,所有对文件系统的变更都落到这个读写层中。
由于一个镜像的所有容器具有自己的可读写层,因而它们能够安全的共享底层的镜像。Docker的存储驱动负责管理所有的层(不管是镜像还是容器的),如何管理取决于具体的驱动,但是两个关键技术是通用的:1、可层叠镜像层(stackable image layers);2、copy-on-write
这个策略在软件系统中很常见。例如在操作系统中,两个使用相同数据块的进程,可以安全共享数据块的单份拷贝。只有当其中一个进程需要对共享数据进行修改(而另外一个进程不进行修改)时,才需要共享数据的额外拷贝。
Docker同时对镜像、容器使用此CoW策略,以便节约存储空间并缩短容器启动时间。CoW依赖于存储驱动的支持,不是所有驱动支持CoW。
执行pull/push子命令时,Docker客户端会报告其操作的Layer:
可以看到,拉取Ubuntu镜像时,实际上下载了4个Layer,它们共同组成了完整的系统镜像。
如果使用AUFS驱动,在1.10版本之前,Layer以其ID为目录名,存放在本地存储的子目录/var/lib/docker/aufs/layers中。
不管Docker引擎的版本是多少,相同的Layer都会被共享,而不是重新拉取。
前面提到,对容器文件系统的所有修改都写在一个薄的顶部层中,来自镜像的层都是只读的,这意味着多个容器可以共享一个镜像。
当容器修改了一个文件后,Docker利用存储引擎来执行CoW策略,具体细节取决于引擎。对于AUFS、OverlayFS这两种存储引擎来说,CoW的大概步骤如下:
Copy-up操作可能带来重大的性能影响,影响程度取决于存储驱动。但是 过大的文件、过多的层、过深的目录树 都会加重影响程度。不过Copy-up操作对一个文件至多发生一次。
CoW让容器共享镜像,这一方面让容器本身的尺寸很小,另一方面则让容器高效的创建、执行,因为不需要牵涉到太多的I/O操作。
当容器被删除后,所有没有存储在数据卷(Data volume)中的数据都会被删除。数据卷是宿主机上的一个目录或者文件,它被直接挂载到容器的目录树中。
多个容器可以共享数据卷,但是要注意并发修改的问题。
数据卷不受存储驱动的控制,对其进行的I/O操作绕过存储驱动,直接由宿主机执行,其速度和宿主机上普通I/O操作一样快。
为了基于实际运行环境选择最好的驱动,Docker设计了可拔插的存储驱动架构。存储驱动基于一个Linux文件系统或者卷管理器,它们可以自由的实现镜像/容器Layer的管理。
在决定使用哪种驱动后,你需要设置Docker守护程序的启动参数。修改/etc/default/docker中的DOCKER_OPTS,添加 -- storage - driver = < sd_name > 选项。
注意一个 Docker守护程序同时只能使用一种存储驱动 。执行命令:
许多企业使用SAN、NAS之类的共享存储技术以提高性能和可靠性,或者实现Thin Provisioning(避免预分配过多的容量)、数据复制、压缩等高级特性。
Docker存储驱动、数据卷均可以在这些共享存储系统之上工作,但是Docker不能直接与底层的共享存储技术集成。你需要选择与共享存储技术匹配的存储驱动。
OverlayFS是目前使用比较广泛的层次文件系统,实现简单, 读写性能较好 ,并且稳定。
与OverlayFS相关的存储驱动有两个:overlay、overlay2。前者的缺陷包括inode耗尽和commit performance,后者就是为了解决这两个问题而生,但是需要4.0或者更高版本的Linux内核。
AUFS是第一个出现的Docker驱动,它非常稳定,在生产环境下有很多部署案例,并且社区支持很好。AUFS的优势包括:
对于PaaS或者其它需要 高密度容器实例 的应用场景,AUFS是很好的选择。但是由于CoW策略,第一次写文件操作可能带来很大的开销。
由于AUFS不在Linux内核主线中,因此某些发行版不会自带AUFS,需要手工下载和安装。
要验证当前系统是否支持AUFS,可以执行命令:
AUFS是一种联合文件系统(UFS),它管理 单个Linux宿主机 上的多个目录,并将其叠加在一起,在单个挂载点形成统一的视图。与UnionFS、OverlayFS类似,AUFS是一种union mounting实现。AUFS管理的每个Linux宿主机目录称为联合挂载点(union mount point)或者分支(branch)。
AUFS的思想与Docker的Layer机制天然吻合——每个branch对应一个镜像/容器的Layer。Copy-up操作实质上就是在宿主机文件系统的不同目录之间复制文件。
由于AUFS在文件级别上操作,这意味着,即使在仅仅修改文件一小部分的情况下,CoW操作也需要复制整个文件。因此,当写入一个体积很大的文件时,AUFS会遭遇一次性的性能问题。
当删除文件时,AUFS在顶层Layer添加一个所谓without文件,该文件命名为 . wh . filename ,用来标记目标文件在容器中被删除。
使用AUFS时,镜像、容器的文件默认被存放到dockerd所在机器的/var/lib/docker/aufs/目录下 。
安装Docker之后,会自动创建bridge、none、host这三个网络,可以通过命令 docker network ls 查看。如果启动容器时不指定 -- network 参数,默认使用bridge网络,bridge在宿主机网络栈中映射为docker0。none表示容器不连接到任何网络,其本地网络设备仅仅lo这个环回网卡。host网络则把容器添加到宿主机网络栈中,在容器中执行ifconfig你会看到输出与宿主机一致。
在三个默认网络中,通常你只会和bridge做交互。这些网络不能被删除,因为Docker本身需要使用。
你可以创建其它自定义网络,并在不需要的时候删除它们。
为了更好的隔离容器,可以创建自定义网络。利用Docker提供的网络驱动,你可以创建桥接(Bridged)、重叠(Overlay)、MACVLAN等类型的网络。
自定义网络可以创建多个,每个容器也可以加入到多个网络中。容器仅仅能在网络内部而不能跨网络通信。当容器连接到多个网络时,其外网连接性由第一个(词法序)具有外部连接能力的网络提供。
Linux内核支持虚拟网桥,可以连接多个网络接口,功能类似于交换机。
在自定义网络中,桥接是最简单的一种。添加到桥接网络的容器,必须位于 同一台宿主机上 。网络中的所有容器可以相互通信,但是这些容器不能访问外部网络。
与Docker0不同,自定义桥接网络不支持--link。但是你可以暴露/发布容器的端口,这样桥接网络的一部分可以被外部访问。 自定义桥接网络嵌入了DNS服务器 ,容器之间可以通过名字访问。
当你需要基于单宿主机建立一个简单的网络时,可以考虑自定义桥接网络。
桥接网络的示例:
这个特殊的本地桥接网络,在以下两种情况下,由Docker自动创建:
你可以提前手工创建docker_gwbridge网络,进行定制化配置。
当使用overlay网络时,docker_gwbridge总是存在。
利用Overlay网络可以跨越多台宿主机组网。
在swarm模式下运行Docker引擎时,你可以在管理节点上创建overlay网络,这种网络不需要外部的key-value存储。
swarm可以让overlay网络仅仅对需要它以提供一个服务的swarm节点可用。当你创建一个使用overlay网络的服务时,管理节点会自动扩展overlay网络以覆盖运行服务的节点。
下面的命令示例如何在swarm管理节点创建overlay网络,并在服务中使用它:
注意:这种overlay网络对docker run启动的容器是不可用的,目标容器必须是swarm模式服务的一部分。
基于swarm模式的overlay网络默认具有安全保证。swarm节点使用gossip协议来交换overlay网络的信息,并且使用GCM模式的AES算法加密gossip协议,管理节点默认每12小时更换密钥。
如果要加密容器通过overlay网络交换的信息,需要使用选项:
上述命令将自动为参与到overlay网络的节点创建IPSEC通道,这些通道也使用GCM模式的AES算法,每12小时更换密钥。
此方式的overlay网络与swarm模式的overlay网络不兼容。主要用于需要考虑兼容性的场景。
当不在swarm模式下使用Docker引擎时,启用overlay网络依赖于外部key-value存储的支持,这些存储包括Consul、Etcd、ZooKeeper。你需要手工安装这些存储,并确保它们可以和Docker宿主机自由通信。
宿主机必须开启4789、7946端口。如果启用加密的overlay网络(--opt encrypted)则需要允许protocol50(ESP)流量。此外KV服务也需要暴露端口。
各宿主机上的Docker引擎守护程序,需要配置dockerd选项以支持overlay网络:
多个容器可以声明相同的别名,这种情况下,其中一个容器(随机)会响应DNS解析。当该容器宕掉后,其它同名容器自动响应解析。这个特性可以用来实现简单的高可用性。
自定义网络中的容器的DNS查找行为与默认bridge网络不同,处于向后兼容的目的,后者的行为与旧版本保持一致。
自1.10版本开始,Docker守护程序嵌入了DNS服务,此服务支持基于容器link、name、net-alias配置的DNS查找。容器还可以通过--dns等选项来指定外部DNS服务器,当内嵌DNS服务器无法解析某个主机名时,会转给外部DNS处理。
默认情况下,容器不被施加资源限制,可以使用宿主机内核调度器允许的最大资源。Docker提供了控制内存、CPU、块I/O用量限制的方法。
你可以为容器施加硬性内存限制,确保容器不占用超过特定值的用户/系统内存。你也可以施加软性限制,允许容器按需使用内存,但当特定条件(例如内核检测到宿主机内存过低)发生时,限制容器内存占用。
内存限制通过docker run命令的选项给出,这些选项的值都是正整数,后面可以跟着b/k/m/g等单位:
默认的,容器可以无限制的使用CPU时钟周期。Docker提供了若干选项,对容器的CPU使用进行限制。这些选项支持CFS调度器,自1.13开始实时调度器也支持某些选项。
CFS是用于大部分Linux进程的调度器,Docker提供以下容器选项:
自1.13版本开始,Docker支持配置容器使用实时调度器。作为前提条件,宿主机内核选项CONFIG_RT_GROUP_SCHED必须开启。
要使用实时调度器运行容器,需要设置dockerd选项 -- cpu - rt - runtime 来指定在某个运行周期内,为实时任务保留的毫秒数。对于默认的10000微秒周期,设置--cpu-rt-runtime=95000意味着至少保留5000微秒给非实时任务使用。
相关配置选项:
典型的容器在启动时,仅仅会发动一个进程——例如Apache或者SSH守护进程。要在容器中运行多个服务,你可以编写脚本,或者使用进程管理工具。
Supervisor是一个流行的进程管理工具,使用它可以更加方便的控制、管理、重启容器中的进程。本章以一个例子来说明如何使用Supervisor来管理容器中的多个服务。
该命令可以实时监控容器的CPU、内存、网络、块I/O资源的占用情况。
Linux容器所依赖的内核机制——控制组(Cgroups),不仅仅支持跟踪进程组,还暴露了度量CPU、内存、块I/O的接口。
控制组通过一个伪文件系统 / sys / fs / cgroup 暴露,每一个子目录对应了一种Cgroup层次(子系统)。某些老的系统挂载位置可能有不同,你可以执行 grep cgroup / proc / mounts 命令以查看。
在每个子系统内部,可以存在多级子目录。这些目录的最深处,会包含1-N个伪文件,其中包含了统计信息。
查看文件/proc/cgroups可以查看系统中已经支持的Cgroup层次。输出中包含Cgroup子系统名称、包含的组数量等信息。
查看文件/proc/$PID/cgroup可以查看某个进程所属的Cgroup。输出 / 表示没有划分到特定的Cgroup,/lxc/pumpkin可能意味着进程属于容器pumpkin的成员。
子系统memory提供内存的统计信息。由于memory控制组会增加一定的overhead,因此某些发行版默认情况下禁用了它。你可能需要添加内核参数:
该子系统对应的伪文件是memory.stat。不包含total_前缀的数据项,与当前Cgroup中的进程有关,包含total_d前缀的数据项,则与当前Cgroup、所有子代Cgroup中的进程有关。
常用数据项列表如下:
cache 使用的系统缓存量,这些缓存代表映射到块文件系统中的内存页。当你读写文件(open/write/read系统调用)、进行内存映射(mmap系统调用)、挂载tmpfs时,该数据项值增加 没有映射到块文件系统的内存页,包括栈、堆、匿名内存映射 mapped_file Cgroup中进程映射的内存的量 pgfault, pgmajfaultCgroup中进程触发页错误(page fault)、页major fault的次数
当进程访问不存在(例如访问无效地址,这可能导致进程接收附带Segmentation fault消息的SIGSEGV信号进而被杀死)、或者被保护(进程读取当前已经被换出的页)的虚拟内存空间时,会触发页错误
Major错误在内核真实的从磁盘读取页时发生
Cgroup中进程当前使用的交换文件大小 active_anon, inactive_anon被内核识别为活动、非活动的匿名内存的量。所谓匿名内存是指没有和磁盘页关联的内存
页一开始的状态是活动的,内核会定期扫描内存,并把某些页标记为非活动。一旦这些页再次被访问,立即重新标记为活动。当内存不足需要交换出磁盘时,非活动页被交换出
rss = active_anon + inactive_anon - tmpfs
active_file, inactive_file被内核识别为活动、非活动的非内存的量
cache = active_file + inactive_file + tmpfs
unevictable 不可交换出磁盘的内存用量。某些敏感信息,例如密钥,会被mlock保护,防止被交换到磁盘上 memory_limit, memsw_limit 不是真正的度量信息,而是应用到Cgroup的资源限制。前者为物理内存用量限制,后者为物理内存+Swap该子系统对应的伪文件是cpuacct.stat,包含容器中进程累计的CPU使用信息。user、system分别表示用户空间、系统空间中代码消耗的时间。时间的单位为jiffies,在X86上通常为10ms。
Cgroup中进程发起的、正在排队的I/O操作数量。如果Cgroup没有执行任何I/O操作,则计数为0,如果Cgroup正在执行IO操作,计数可能为0——例如正在空闲设备上执行纯同步操作
注意此计数是一个相对值。可以用来判断哪个容器在给IO系统施加压力
Cgroup没有直接暴露网络度量信息。尽管内核可以计算出一个Cgroup中进程收发的网络包数量,但是意义不大。你可能需要基于网络接口的度量,因为lo接口上的流量没有太大价值。
单个Cgroup中的进程可以属于多个网络名字空间( network namespace),多个网络名字空间意味着多个lo甚至eth0,这导致难以收集Cgroup的网络流量。
你可以基于iptables规则设置计数器,然后使用命令获取度量。
获取网络接口级别的度量信息是可能的,因为每个容器都关联了宿主机上的一个虚拟以太网接口。但是难以知道这些接口和容器的对应关系。
ip netns exec 允许你在宿主机上,进入任意网络名字空间,执行任意命令。这意味着宿主机可以进入容器的网络名字空间。参考如下命令:
对于每一个容器,在每一个Cgroup子系统中均会创建一个与之对应的组。
如果使用最近版本的LXC工具,Cgroup名字为lxc/$container_name。
对于使用Cgroup的Docker容器,Cgroup路径为/sys/fs/cgroup/$subsystem/docker/$longid/,其中longid为容器完整的ID。
要使用Swarm模式,你可以安装1.12版本以上的Docker。Swarm模式用于管理Docker引擎的集群。你可以使用Docker CLI来创建Swarm、部署应用服务到Swarm、管理Swarm的行为。
Swarm
基于SwarmKit构建的、内嵌在Docker引擎中的集群管理和编排机制。参与到Swarm集群中的Docker引擎运行在Swarm模式。要切换到Swarm模式,你可以新建一个Swarm、或者加入一个既有的Swarm
Swarm也可以指代基于上述机制的Docker引擎(或者叫节点)集群,你在Swarm中部署服务。CLI和Docker API包含管理节点、部署和编排服务的命令
在普通模式下,你执行容器命令;在Swarm模式下,你编排服务。在同一个Docker引擎下,你可以同时运行独立容器、Swarm服务
节点即参与到Swarm中的Docker引擎。你可以在单台物理机器上运行一个或者多个节点。通常生产环境下Swarm由跨越多台物理机器的节点组成
要部署应用到Swarm,你需要把服务定义(service definition)到管理节点。管理节点负责分发称为任务(Task)的工作单元给Worker节点
管理节点也负责执行服务编排、集群管理功能,以维持集群处于期望的状态。管理节点们会推举一个Leader节点来主导编排工作
Worker节点接收、执行管理节点派发的任务,默认情况下管理节点也像Worker节点一样运行服务,但是你可以将其配置为Manager-only节点。Worker节点上运行着一个代理(Agent),此代理负责报告分配给Worker的Task的状态到Manager,这样Manager就可以维持期望状态
Service & Task
所谓Service,是关于需要在Worker节点上执行的Tasks的定义。服务是Swarm系统的核心结构,也是用户和Swarm交互的主要切入点
当定义服务时,你可以指定使用什么镜像,以及当运行容器时需要执行什么命令
在复制服务( replicated services)模型下,Manager节点会基于你在期望状态中设置的Scale,分发一定数量的复制Task
对于全局服务(global services),Swarm在集群中每个可用节点上,运行此服务的单个Task
Task使用一个容器,并在其中执行特定的命令。Task是Swarm的原子调度单元。前面提到过,Swarm根据Service的Scale设定决定Task的数量并分发。分发的Task只能在某个节点上运行或者失败,而不能转移到其它节点
Swarm基于入口负载均衡(ingress load balancing )暴露对外服务。你可以为服务配置PublishedPort,如果不指定Swarm可以自动分配30000-32767之间的端口
诸如云负载均衡器之类的外部组件,可以访问Swarm集群中 任意节点的PublishedPort 以使用服务,不管节点是否运行服务的Task。所有节点都会自动把入口连接路由到运行了Task的节点
Swarm基于内部负载均衡(internal load balancing)将请求分配给服务的实例,其依据是服务的DNS名称。Swarm内置的DNS组件会自动的给所有Service分配DNS条目
这样当前节点就称为新建Swarm集群的管理节点了。管理节点使用通知地址(advertise address)来允许集群中其它节点访问Swarmkit API以及Overlay网络,因此IP地址10.0.0.1必须可以被所有其它节点访问到。如果宿主机具有单个IP地址,你可以不指定--advertise-addr,反之则必须指定。
执行docker info,可以看到当前Swarm的基本信息;执行docker node ls则可以看到集群中的节点列表。
首先在Manager节点上执行:
作为管理节点加入时,执行的操作与上面类似。新的管理节点状态为Reachable,但是Swarm的Leader不变。
所谓令牌,是加入Swarm时需要的一个字符串,作为管理节点/工作节点加入时的令牌是不一样的。管理节点的令牌要特别注意保护。当发生以下情况下,考虑使用join-token子命令更改(rotate)令牌:
下面的命令示例如何更换工作节点令牌:
注意,尽管服务被删除,执行Task的容器可能还需要一段时间执行清理工作
所谓滚动更新,是指逐步的更新服务的每个实例。下面的例子演示如何从Redis 3.0.6滚动更新到Redis 3.0.7。
首先创建服务:
选项--update-delay定义了更新一个/一组任务的延迟时间,你可以使用10m30s这样的形式。 默认情况下是一个接着一个的更新,要每次更新多个Task可以使用选项--update-parallelism。
默认情况下,当正在更新的任务状态变为RUNNING后,调度器会调度下一个任务的更新,此步骤一直执行直到所有任务都被更新,如果更新过程中任何一个任务返回FAILED状态,则调度器暂停更新。选项--update-failure-action可以改变此行为。
执行下面的命令把Redis版本更改为3.0.7:
这样,所有运行my-web服务的Task的节点,都被Overlay网络覆盖。
前面例子中的Worker节点,其可用性(availability)都是ACTIVE。Manager节点可以向ACTIVE派发任务。
某些时候(例如需要维护节点硬件),需要把可用性设置为DRAIN。DRAIN阻止管理节点派发新的任务,并且,停止DRAIN节点上正在运行的任务,在可用的ACTIVE节点上启动对应数量的任务副本。
执行下面的命令可以查看节点可用性:
为了让外部资源能够轻松的访问Swarm中的服务,Docker引擎提供了方便的端口暴露机制。所有Swarm节点均参与到一个入口路由网(Ingress Routing Mesh),此路由网允许Swarm中的任意节点接收某个服务暴露端口的请求——甚至在节点没有运行此服务的任务的情况下。路由网负责把对暴露端口的请求路由到活动的服务容器中。
要正常使用入口路由网,需要确保TCP/UDP端口7946、UDP端口4789开放。当然,Swarm服务暴露的端口也需要被外部资源(例如负载均衡器)正常访问。
要暴露端口,可以在创建服务时使用 -- publish PUBLISHED - PORT : TARGET - PORT 选项。其中TARGET-PORT是运行服务实例(Task)的容器监听的端口,PUBLISHED-PORT则是Swarm对外暴露的端口。示例:
使用入口路由网的端口暴露机制,可能不满足应用需求。你可能需要根据应用程序状态来决定如何路由请求,或者你需要对路由处理过程进行完全的控制。
要直接暴露服务所在运行的节点上的端口,可以使用 -- publish mode = host 选项。如果不和 -- mode = global 联用该选项,将难以知晓哪些节点运行了服务。
在1.13版本之后,在服务创建之后你可以使用 service update -- image 更改服务基于的镜像。较老的版本则只能重新创建服务。
每个镜像Tag对应了一个摘要(Digest),就像Git的Hash一样。某些标签,例如latest,其指向的摘要会改变。当你运行service update --image时,管理节点根据Tag到Docker Hub或者本地私服查询。如果:
你可以为Swarm服务创建两种类型的挂载:volume、bind。要创建挂载,指定--mount选项,如果不指定--type,默认类型为volume。
卷挂载(volume)是当运行任务的容器被移除后,仍然存在的存储。要利用既有的卷时,通常使用此类型的挂载:
基于Raft(一种算法),管理节点维护整个Swarm、所有运行中服务的一致性内部状态。
在测试环境下,你可以使用单管理节点,但是,一点此节点宕机,你需要重新创建Swarm才能恢复。
为了利用Swarm的容错特性,最好建立奇数节点数的管理节点。如果有多个管理节点,Swarm可以自动从管理节点宕机中恢复,没有downtime。当总计3个管理节点时,可以容忍1个宕机;当5个管理节点时,可以容忍2个宕机;当N个管理节点时,可以容忍(N-1)/2个管理节点宕机。Docker推荐每个Swarm中有7个管理节点。
工作节点的唯一任务就是执行容器。默认的,管理节点同时也是工作节点。
要阻止派发任务给管理节点,可以将后者的可用性设置为Drain,调度器会优雅的停止Drain上运行的任务并且在其它节点上重新调度。
要在Swarm模式下部署应用程序,你需要创建一个“服务”。通常情况下,服务是某个较大应用程序的上下文中的某个“微服务“, 例如HTTP服务、数据库服务、或者任何形式的可执行程序。
当创建服务时,你需要指定使用什么镜像,以及在基于此镜像的容器中运行什么命令。你可以同时指定:
当你在Swarm中部署服务时,Swarm接受你给出的服务定义(service definition),作为目标服务的期望状态(desired state )。之后,Swarm调度服务,形成在节点上运行的一个或N个任务。每个任务独立于集群中其它节点运行。
下面是具有三个Replica的HTTP服务的例子:
容器是一个被隔离的进程,在Swarm模式的模型中,每个任务调用仅一个容器。任务就好像是一个插槽,调度器将容器插入其中。一旦容器开始运行,调度器将任务设置为RUNNING状态;如果容器未通过健康检查、停止运行,则任务也被终结。
任务是Swarm调度的原子单元。当你通过创建/更新服务来声明服务的期望状态时,编排器(orchestrator)识别出被调度任务的期望状态——当你声明保持3个HTTP服务一直运行,编排器就会创建三个任务。
任务是一个插槽,调度器产生容器进程并填充到插槽。容器是任务的实例。任务是一种单向的机制,它从:已分配(assigned)、已准备(prepared)、正在运行(running)等一系列状态单向的前进。如果某个任务未通过健康检查或者终结,任务及其容器被编排器移除,新的副本任务以及对应的容器被创建,以满足期望状态。
Swarm模式底层组件包括了一般性用途的调度器、编排器。服务、任务的抽象实现,不理解容器这个概念。理论上你可以实现在非容器中运行的服务。
下图说明Swarm模式如何接受用户创建服务的请求,如何调度服务:
可以配置服务未悬挂的(pending),这样Swarm中没有节点可以运行该服务的任务。如果你仅仅需要防止服务被部署,只需要Scale到0,而不是尝试让服务进入pending状态。
下列情况下,服务会变为pending:
从部署份数的角度来看,服务可以分为复制(replicated)、全局(global)两种。
复制服务,由指定数量的任务构成。全局服务则在每个节点运行单个任务。
Swarm模式下,我们可以使用Docker secrets管理敏感数据。所谓Secret是指一块数据,存放密码、SSH私钥、SSL证书或者其它不应该通过网络传递、明文存放在Dockerfile中的信息。
Compose是用于定义、运行多容器Docker应用程序的工具。通过编辑一个Compose文件,你可以配置应用程序的服务组件,然后,只需要单条命令,你就可以创建、启动所有需要的服务。
使用Compose通常包括以下三大步骤:
工程名称默认为工程的目录名,你可以使用-p选项或者 COMPOSE_PROJECT_NAME 环境变量设置工程名称
使用Docker Machine,你可以:
Docker Machine是一套工具,它允许你在虚拟主机上安装Docker引擎,以及通过docker-machine命令来管理宿主机(Machine,通常是虚拟机,这些虚拟机通常由DM创建)。你可以使用DM在非Linux系统、公司网络、数据中心、云端来创建Docker宿主机。
使用docker-machine命令,你可以启动、查看、停止受管理的宿主机,升级Docker客户端/守护程序。
执行以下命令下载并安装:
Docker容器与LXC容器很类似,它们具有相似的安全特性。当你启动容器时,Docker会为容器创建一系列的名字空间和控制组。
名字空间提供第一级的、最直接的隔离性——容器中运行的进程看不到,甚至不能影响到其它容器、宿主机中运行的进程。内核名字空间机制从2.6.15 - 2.6.26版本开始引入,目前已经非常稳定。
每个容器具有自身的网络栈(network stack),这意味着容器不具有其它容器套接字、网络接口的访问权限。当然,如果宿主机正确的配置,容器之间可以基于各自的网络接口进行交互,就像与外部主机一样。
Cgroups是Linux容器的另一个关键组件,它实现了资源审计和限额,并提供很多有价值的度量信息。利用控制组,可以让容器获得公平的CPU、内存、磁盘I/O等资源。Cgroups能够有效的防范某些DoS攻击。Cgroups于2.6.24被合并到内核。
通过Docker运行容器,意味着需要运行Docker守护程序,后者需要Root权限。只有受信任用户才应该被允许控制守护程序。
由于Docker允许在宿主机、容器之间共享目录,这意味着容器可能任意的修改宿主机文件系统。
当在服务器上运行Docker时,推荐宿主机仅仅运行Docker,而把所有其它服务(除了SSH服务器这类管理工具)都放在容器中运行。
默认的,Docker以一组受限的能力(capabilities)来启动容器。能力把root/非root划分为细粒度的访问控制系统。需要绑定到1024-端口的进程不再需要root权限,而仅需要被授予net_bind_service能力。
容器也不需要被授予真正的root权限。事实上,容器中的root用户缺陷受到很大的限制,例如:
由于这些限制的存在,即使攻击者获得容器的root权限,也难以进行严重的破坏。
你可以增加、删除容器的能力,以提升功能或安全性。
能力(capabilities)仅仅是现代Linux内核提供的众多安全特性之一。其它已知的著名安全系统包括:TOMOYO、AppArmor、SELinux、GRSEC等,它们都可以和Docker协作。
考虑以下建议:
从1.10开始,用户名字空间被Docker直接支持。这一特性允许容器中的root用户直接映射到容器外部的非0 UID的任何用户,进而减少安全风险。这一特性默认没有开启。
默认的,Docker通过非网络化的Unix套接字运行,你也可以基于HTTP套接字与之通信。
如果你期望通过网络安全的访问Docker,应当启用TLS。这样,在守护程序端,仅仅通过CA认证的客户端才允许连接;在客户端,则仅仅允许向通过CA认知的服务器发起连接。
在守护程序上启用TLS的示例:
在使用Docker的过程中,我们常常需要从/到Docker Hub或者私服pull/push镜像。内容信任(Content trust)机制允许验证数据的完整性、镜像的发布者,不论你是从什么渠道获得镜像。
内容信任机制可以强制客户端在与远程服务(Hub或私服,registry)交互时,进行客户端签名和镜像Tag验证。该机制默认情况下是禁用的,要启用,可以设置环境变量 DOCKER_CONTENT_TRUST 为1。
一旦内容信任被启用,镜像发布者就可以对自己的镜像进行签名。镜像的消费者则可以确保镜像是来自发布者,未经篡改。
每一个镜像记录由以下字段唯一的标识: [ REGISTRY_HOST [ : REGISTRY_PORT ] / ] REPOSITORY [ : TAG ] 。一个镜像仓库(REPOSITORY)可以具有多个标签,镜像构建者可以使用仓库+标签的组合多次构建并更新镜像。
内容信任与TAG部分关联,每个REPOSITORY具有一组供发布者签名镜像TAG的密钥。单个REPOSITORY中可以包含签名、未签名的TAG。对于启用内容信任的消费者,未签名的TAG是不可见的。
子命令push、build、create、pull、run与内容信任机制交互。例如,当你执行docker pull someimage:latest时,仅当someimage:latest被正确签名时,命令才会成功。除了指定TAG,你也可以直接指定签名Hash:
与镜像标签信任管理相关的是一系列的签名密钥。当第一次使用到内容信任功能时,密钥被创建。这些密钥包括:
除了通过环境变量来全局性的启用内容信任之外,你还可以在调用docker命令时指定 -- disable - content - trust 来临时的禁用内容信任:
参考 Engine(docker) CLI 。
本质上此命令行通过REST API和守护程序通信,和守护程序一样。该命令支持HTTP_PROXY、HTTPS_PROXY,并且优先使用后者。
docker build [ OPTIONS ] PATH | URL | -
--build-arg value 设置构建时(容器运行时看不到)变量,例如环境变量
--cgroup-parent string 容器的父Cgroup组
--cpu-period int 限制CFS周期
--cpu-quota int 限制CFS配额
-c, --cpu-shares int 限制CPU权重
--cpuset-cpus string 限制允许在哪些CPU上执行
--cpuset-mems string 限制允许使用哪些内存条
--disable-content-trust 禁止镜像验证,默认true
-f, --file string 可选的Dockerfile名称
--force-rm 总是移除中间容器
--isolation string 容器隔离计数
--label value 在镜像上设置元标签
-m, --memory string 设置内存限制
--memory-swap string 内存和交换文件限制
--no-cache 在构建镜像时不使用缓存
--pull 总是尝试拉取更新版本的镜像
-q, --quiet 禁止输出内容,仅在成功时打印镜像ID
--rm 在成功构建后,移除中间容器,默认true
--shm-size string /dev/shm的大小,默认64MB
-t, --tag value 为镜像设置标签
--ulimit value Ulimit选项
列出本地可用的镜像
-a, --all 显示所有镜像,默认情况下中间镜像被隐藏
--digests 显示摘要
-f, --filter value 根据条件过滤输出
--format string 依据Go语言模板指定输出的格式化方式
--no-trunc 不截断输出
-q, --quiet 安静模式,仅显示容器ID,不显示表头
-a, --author string 镜像作者信息,例如Alex <alex@gmem.cc>
-c, --change value 为新镜像应用Dockerfile指令
-m, --message string 提交注释
-p, --pause 在提交期间暂停容器
选项:
--add-host value 添加自定义的主机:IP地址映射
-a, --attach 关联STDIN、STDOUT或者STDERR
--blkio-weight value 块I/O相对权重,10-1000之间
--blkio-weight-device value 块I/O设备相对权重
--cap-add value 添加Linux特性(capabilities)
--cap-drop value 去除Linux特性(capabilities)
--cgroup-parent string 为容器指定父cgroup
--cidfile string 输出容器ID到目标文件
--cpu-percent int CPU占用百分比,仅Windows
--cpu-period int 限制容器使用的CFS周期
--cpu-quota int 限制容器使用的CFS配额
-c, --cpu-shares int 限制容器使用的CPU相对权重
--cpuset-cpus string 限制容器可以使用的CPU的序号,例如0-3, 0,1
--cpuset-mems string 限制容器可以使用的内存的变号,例如0-3, 0,1
-d, --detach 在后端运行容器,并打印容器的ID
--detach-keys 设置解除终端与容器关联的快捷键
--device value 添加一个宿主机设备给容器
--device-read-bps value 以字节/秒为单位限制容器读取一个设备的速度
--device-read-iops value 以次数/秒为单位限制容器读取一个设备的速度
--device-write-bps value 以字节/秒为单位限制容器写入一个设备的速度
--device-write-iops value 以次数/秒为单位限制容器写入一个设备的速度
--disable-content-trust 跳过镜像安全性验证
--dns value 指定容器使用的DNS服务器
--dns-opt value 设置容器的DNS选项
--dns-search 设置自定义的DNS查找域
--entrypoint 覆盖镜像默认的入口点(ENTRYPOINT)配置
-e, --env value 设置环境变量,示例:
-
e
"LIMIT=10"
--env-file value 从文件中读取环境变量
--expose value 暴露一个端口,作用类似于Dockfile中的expose命令。从1.5开始,支持暴露一组端口,例如
--
expose
=
7000
-
8000
--group-add value 指定额外需要加入的组
--health-cmd string 指定容器健康状态检查的命令
--health-interval duration 容器健康状态检查间隔
--health-retries int 报告为不健康之前重新检查的次数
--health-timeout duration 一次健康检查最多执行的时间
-h, --hostname string 容器的主机名
-i, --interactive 即使没有关联到终端,也保持容器的标准输入打开
--io-maxbandwidth string 系统驱动器最大IO带宽限制,仅Windows
--io-maxiops uint 系统驱动器最大IOPS限制,仅Windows
--ip string 设置容器的IPv4地址
--ip6 string 设置容器的IPv6地址
--ipc string 容器使用的IPC名字空间,设置为host则与宿主机共享IPC名字空间
--isolation string 容器隔离技术
--kernel-memory string 内核内存限制
-l, --label value 为容器指定标签(元数据)
--label-file value 从逗号分隔符文件中读取标签
--link value 链接到其它容器,可以允许docker0网络中两个容器通信
--link-local-ip value 设置容器的IPv4/IPv6 link-local地址
--log-driver string 容器的日志驱动(Logging driver)。可选None不显示日志;Json-file默认值,以JSO格式记录日志;Syslog把日志输出到系统日志文件;journald、fluentd、splunk、gelf、awslogs
--log-opt value 容器日志驱动的选项
--mac-address string 设置容器的MAC地址
-m, --memory string 设置容器的内存用量限额
--memory-reservation string 设置容器的内存用量软限制
--memory-swap string 设置交换文件限额,-1表示不限制
--memory-swappiness int 微调容器的swappiness,0-100之间
--name string 为容器分配名称
--network string 连接容器到指定的网络
--network-alias value 为容器指定目标网络范围内的别名
--no-healthcheck 禁止容器特定的健康状态检查
--oom-kill-disable 禁止内存溢出Killer
--oom-score-adj int 微调宿主机的OOM参数,-1000到1000之间
--pid string PID 使用的名字空间类型,取值host则与宿主机共享PID名字空间
--pids-limit int 微调容器的PIDs限制
--privileged 为容器授予扩展的权限
-p, --publish value 发布容器暴露的端口到宿主机,value格式
宿主机端口:容器端口
-P, --publish-all Publish 随机的发布容器暴露的端口到宿主机,通过--expose指定或者在Dockerfile中以EXPOSE指定的任意端口都会被发布。宿主机的端口范围由/proc/sys/net/ipv4/ip_local_port_range这个内核参数确定,默认32768-61000之间
--read-only 以指定方式挂载容器的根文件系统
--restart string 设置容器退出时的重启/宿主机开机后的启动策略,默认no。On-failure当容器命令返回非0时重启,Always 自动重启,并且总是随着守护程序启动,Unless-stopped类似于Always,但是不随着守护程序启动
--rm 在容器退出时自动删除它
--runtime string 设置容器使用的运行时间
--security-opt value 设置安全选项
--shm-size string Size 设置/dev/shm的大小,默认64MB
--sig-proxy 代理接收到的信号给容器进程
--stop-signal string 用于退出容器的信号,默认SIGTERM
--storage-opt value 容器的存储驱动选项,devicemapper、overlay2等支持
--sysctl value 容器的Sysctl选项
--tmpfs value 挂载一个tmpfs目录
-t, --tty 为容器分配一个伪终端
--ulimit value 设置Ulimit选项
-u, --user string 设置容器执行身份,格式<name|uid>[:<group|gid>]
--userns string 设置使用的用户名字空间
--uts string 设置使用的UTS名字空间,设置为host则与宿主机使用相同的hostname和domain
-v, --volume value 挂载一个卷
--volume-driver string 可选的卷驱动
--volumes-from value 从指定的容器挂载卷
-w, --workdir string 设置容器的工作目录
选项:
-a, --attach 关联容器的标准输入/输出到当前终端,并转发信号
-i, --interactive 关联容器的标准输入到当前终端
--detach-keys string 覆盖解除终端与容器关联的快捷键序列
选项:
--detach-keys string 覆盖解除终端与容器关联的快捷键序列
--sig-proxy 代理接收到的信号给容器进程
--no-stdin 不关联标准输入
选项:
-a, --all 显示所有容器,包括已经停止的
-f, --filter value 过滤输出
--format string 依据Go语言模板指定输出的格式化方式
-n, --last int 显示最后int个创建的容器
-l, --latest 显示最后一个创建的容器
-q, --quiet 安静模式,仅显示容器ID,不显示表头
-s, --size 打印总的空间占用
--details 显示额外信息
-f, --follow 跟随日志输出
--since string 显示指定时间戳之后的日志
--tail string 显示末尾N行日志
-t, --timestamps 显示时间戳
通用网络选项:
--internal 禁止通过网络进行外部访问
--ipv6 启用IPv6支持
桥接网络选项:
com.docker.network.bridge.name Linux网桥的名称
com.docker.network.bridge.enable_ip_masquerade,--ip-masq 启用IP遮掩
com.docker.network.bridge.enable_icc,--icc 启用或禁止跨容器连接性(Inter Container Connectivity)
com.docker.network.bridge.host_binding_ipv4 ,--ip 绑定容器暴露的端口时,使用的宿主机IP
com.docker.network.driver.mtu,--mtu 设置最大传输单元
--constraint value 设置约束
--container-label value 设置容器标签
--endpoint-mode string 端点模式dnsrr或者vip
-e, --env value 设置环境变量
-l, --label value 设置服务标签
--limit-cpu value 限制CPU,默认0.000
--limit-memory value 限制内存,默认 0 B
--log-driver string 服务的日志驱动
--log-opt value 日志驱动选项
--mode string 服务模式,replicated(默认)或者 global
--mount value 附加一个挂载到服务
--name string 服务的名称
--network value 加入的网络
-p, --publish value 暴露一个端口,pubPort:targetPort格式
--replicas value 任务的数量,默认0
--reserve-cpu value 保留CPU,默认0.000
--reserve-memory value 保留内存,默认 0 B
--restart-condition string 何时自动重启,none, on-failure, any
--restart-delay value 重启尝试的延迟
--restart-max-attempts value 放弃前重试的重启次数
--restart-window value 用于评估重启策略的窗口
--stop-grace-period value 在杀死容器前,等待的时间
--update-delay duration 更新之间的延迟时间
--update-failure-action string 更新失败后的动作,pause或continue
--update-parallelism uint 同时更新的Task的最大数量,默认1,0表示全部同时更新
-u, --user string 用户名或者UID
--with-registry-auth 发送registry认证详细信息给Swarm代理
-w, --workdir string 设置容器工作目录
查看一个或者多个服务的详细信息
docker service inspect [ OPTIONS ] SERVICE [ SERVICE . . . ]
-f, --format string 基于给定的模板进行格式化
--pretty 优化输出(不以JSON格式显示)
列出服务包含的任务
docker service ps [ OPTIONS ] SERVICE
-f, --filter value 基于给定的选项过滤输出
--no-resolve 不把ID映射为名称
-f, --filter value 基于给定的选项过滤输出
--no-resolve 不把ID映射为名称
移除一个或者多个服务
docker service rm [ OPTIONS ] SERVICE [ SERVICE . . . ]service scale
扩容一个或者多个服务
docker volume create [ OPTIONS ] [ VOLUME ]
--driver, -d 指定卷驱动名称,默认local
--label 为卷添加元数据
--name 设置卷的名称,不指定则随机名称。在不同驱动之间,名称不能重复。使用同一种驱动、同一个名称,被认为是卷重用,不会报错
--opt, -o 驱动特定的选项
当前是Docker客户端的试验特性。用于管理清单(manifest)或清单列表
所谓清单,是关于镜像的信息,包括层、大小、摘要值。docker manifest命令可以在清单中增加镜像所针对的操作系统、体系结构的信息
清单列表则是通过指定1或多个镜像名称来创建的层列表。典型情况下,清单列表是基于一组功能相同、针对不同os_arch的镜像
当拉取镜像时,Docker会检查返回的清单信息,如果发现是支持multi arch的清单列表对象,则在列表中 自动寻找匹配当前体系结构的镜像
创建一个 本地 清单列表,用于后续的标注、推送。清单的存放位置为: $ { HOME } / . docker / manifests / $ ( REGISTRY_DOMAIN ) _ $ ( REGISTRY_PREFIX ) _ $ ( IMAGE ) - $ ( VERSION )
要删除清单,删除上述文件即可。此外你也可以在push的时候指定--purge来删除本地清单
清单必须和镜像一同推送到镜像仓库,否则拉取镜像时会提示找不到manifest
docker manifest create MANIFEST_LIST MANIFEST [ MANIFEST . . . ]
-a, --amend 修改现有清单列表
--insecure 支持和不安全仓库通信
为本地镜像清单标注额外的信息,设置清单列表中的某个清单对应的体系结构、操作系统 docker manifest annotate [ OPTIONS ] MANIFEST_LIST MANIFEST
--arch 标注架构
--variant 标注架构变体
--os 标注操作系统
--os-features 标注操作系统特性
-p, --purge 推送后删除本地清单
--insecure 支持和不安全仓库通信
此外,CNM还依赖于另外两个对象来完成Docker网络管理:
CNM的原生实现是Libnetwork,是Docker团队从Docker核心中分离出来的网络相关功能。
默认情况下,Docker使用Veth对的方案:
隧道网络也叫overlay网络,在IaaS网络中就被大量使用。
优势:几乎不依赖基础网络架构,只要3层互联即可。
基于覆盖网络的插件包括:
隧道方案解决的问题是,主机之间无法直接传递容器IP的封包。如果解决路由的问题,就可以避免二次包装的性能损失。
基于路由的方案包括:
位置在:/run/containerd/io.containerd.runtime.v1.linux/moby/$CONTAINER_ID/config.json
如果容器正在运行,可以通过nsenter进入。
如果容器已经死掉,可以通过命令:
报错信息:certificate has expired or is not yet valid docker
如果测试wget/curl等命令没问题,可能是由于Docker使用的代理导致。
由于NAT(Docker端口映射)导致。默认情况下Docker使用用于空间代理docker-proxy来处理NAT,性能较低。可以设置dockerd参数禁用:
进行docker login时出现此错误,可以把目标仓库的证书拷贝到 / etc / docker / certs . d / 下,以仓库域名为子目录。
如果使用Let's Encrypt签名的证书,务必使用完整证书链。
在CentOS下,启动firewalld后,新创建Docker容器并进行端口映射会出现此错误。重启Docker即可。
如果无法在宿主机上通过 ip netns list 看到容器的网络命名空间,可以调用命令:
用于配置命名空间化的内核参数(namespaced kernel parameters)。
注意,仅仅被 Docker加入白名单 的内核参数可以调整,否则你会收到错误:sysctl '***' is not whitelisted。白名单包括:
此外,被调整的内核参数还必须是:
如果使用--net=host,则使用宿主机的网络栈,直接使用宿主机的内核参数,不需要特殊操作。
访问某通过LVS暴露的接口,20%几率出现Connection Reset By Peer。进一步检查发现:
当卡死时,最终提示:curl: (56) Recv failure: Connection timed out,Docker宿主机抓包如下:
可以看到发送HTTP请求后,出现重传,随后即RST。LVS发来的TCP Dup ACK中的SLE、SRE(选择性ACK时,已经Ack的字节范围,提示对方不再传输这些范围的数据)很不正常。在客户端禁用SACK后,问题消失:
值得注意的是:在NATed的客户端来看,是服务器主动RST,在NAT设备来看,是它主动RST,并谎报给NATed客户端说服务器进行了重置。
可能是DNS配置错误导致的,执行以下步骤修复:
如果防止脚本退出的命令调用了其它程序,例如: tail - f / var / log / myservice . log ,会出现一个子进程,虽然上述脚本的PID为1,但是子进程不会收到信号,因而容器仍然卡在那不会退出。
一个生产环境中入口点脚本的例子: Kurento的入口点脚本
层次过多的文件系统,不但访问效率较低,而且占用更大的磁盘空间。可以export容器并import为镜像,这样产生的镜像只有一个层次:
这种行为会导致潜在的无权访问的问题。
解决办法:容器中的用户,和宿主机的用户,以ID对应。因此,你可以预先创建宿主机目录,并chown给容器中需要使用此目录的用户的ID。
最好确保你的应用程序的PID为1,如果Entrypoint Spawn的子进程中运行应用程序,则Entrypoint必须能够捕获信号并执行适当的命令来停止应用程序进程。
无法捕获信号的常见原因:
报错:OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:297: copying bootstrap data to pipe caused \"write init-p: broken pipe\"": unknown
解决办法:
以Alpine作为基础镜像,运行动态链接的Go应用程序,出现此报错。
原因是,应用程序动态链接到了 GNU libc。Alpine的musl libc库提供了对GNU libc的部分兼容性,可用于尝试解决此问题:
Your email address will not be published. Required fields are marked *