docker学习纪要

1、容器 ≠ docker ? 版本规划如何?

容器不光是 Docker,还有其他容器,比如 CoreOS 的 rkt。为了保证容器生态的健康发展,保证不同容器之间能够兼容,包含 Docker、CoreOS、Google在内的若干公司共同成立了一个叫 Open Container Initiative(OCI) 的组织,其目是制定开放的容器规范。

版本:年.月.补丁版本号

docker version
#Version:           19.03.3
#即为19年3月的版本,补丁版本为3



2、Docker可以在任何场景运行?

Docker镜像是一个不包含Linux内核而且又精简的Linux操作系统(内核是共享宿主机的)。docker的运行要依赖当前操作系统的kernel,镜像只是提供了rootfs。故不同架构的镜像(比如X86下生成的基础镜像在ARM中)是不通用的。

·bootfs (boot file system) 主要包含 bootloader 和 kernel, bootloader主要是引导加载kernel, 当boot成功后 kernel 被加载到内存中后 bootfs就被umount了.

·rootfs (root file system) 包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。

是否有专门跑容器的OS? 有的!CoreOS、atomic 和 ubuntu core等


3、Docker和虚拟机区别

docker:是宿主机上的线程,使用宿主机的kernel也就是bootfs驱动,自己提供上层的用户空间(userland)=rootfs及文件。所有修改在容器层,不会修改镜像层更不会修改到宿主机,所以他更轻(rootfs),可共享。

虚拟机:是从宿主机上划分相应的硬件资源,运行创建出的完整OS,包含独立的kernel层及userland层,自己更完整(rootfs/bootfs/硬件)、独立,但更臃肿。

/uploads/files_user1/question/5ef1e5c84513c388060.png

/uploads/files_user1/question/5ef1e5d3bed8e716835.png



4、镜像和容器什么关系?镜像是分层的?镜像可以被多个容器共享?

镜像=只读的,容器=镜像+一个读写层


典型的Linux在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。

在docker中,起初也是将 rootfs 以readonly方式加载并检查,然而接下来将一个 readwrite 文件系统挂载在 readonly 的rootfs之上,并且允许再次将下层的 file system设定为readonly 并且向上叠加, 这样一组readonly和一个writeable的结构构成一个container的运行目录。


镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。

  1. 添加文件:在容器中创建文件时,新文件被添加到容器层中。

  2. 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。

  3. 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。

  4. 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

http://nccloud.yytimes.com/uploads/files_user1/answer/5eea27038fa3e949525.png


4.1 从dockerbuild看,首先原目录build,根据dockerfile逐层创建,且历史创建过镜像的相同操作中多级使用了缓存

/uploads/files_user1/question/5ef1702752f1b759570.png

在修改/data/ncc/nchome_uap-fs/nchome_uap-fs/temp/stage/ROOT/下增加了随便一个文件之后,没有使用cache

/uploads/files_user1/question/5ef17280912ad180762.png

4.2从docker history看,修改文件前

/uploads/files_user1/question/5ef170b206c40822900.png

修改文件后

/uploads/files_user1/question/5ef172d1b711d201528.png


结合实例来看,镜像的文件在宿主机上是如何保存的?运作机制是?

1、查看镜像的详细信息,这里定义了一系列镜像运行时的参数,其中这里重点关注GraphDriver项

docker image inspect reg.yyuap.io:81/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/uap-init:20200612203405

/uploads/files_user1/question/5ef1ad749e209321353.png

/uploads/files_user1/question/5ef1ad89f0c1d840187.png

/uploads/files_user1/question/5ef1ada6e9890699686.png

其中:GraphDriver是什么?----镜像管理驱动,主要用于管理和维护镜像,包括把镜像从仓库下载下来,到运行时把镜像挂载起来可以被容器访问等,都是graph driver做的。涉及的命令有Docker push import export  load save build。


维基词条:联合文件系统(union filesystem),在操作系统中,联合挂载(union mounting)是一种将多个目录结合成一个目录的方式,这个目录看起来就像包含了他们结合的内容一样。


  1. 实现通常辅有“写时复制(CoW)”的实现技术,这样任何对于底层文件系统分层的更改都会被“向上拷贝”到文件系统的一个临时、工作、或高层的分层里面。这个可写的层然后可以被看做是一个“改动(diff)”,能将之应用到下层只读的层,而这些层很可能作为底层被很多容器的进程中共享。这是一个很重要的点。

  2. 一个Docker中使用分层文件系统的好处就是,1000个运行着bash的ubuntu:latest容器的副本,会共享一个底层的镜像,而并不会产生1000个文件系统的副本。

  3. 并且同样重要的是,对于aufs和overlay的实现,用来读取或执行共享库的共享内存也在所有运行的容器之间共享,大大的减少了通用库如'libc'的内存占用。这是一个分层策略的巨大优势,同时也是Docker的graphdriver是引擎中相当重要的一部分的原因之一。


我们的GraphDriver=overlay2,还有如下vfs、aufs、overlay、overlay2、btrfs、zfs、devicemapper和windows。


"LowerDir": "/data/docker/overlay2/0692d0cadd2c300813a3343a3f6077b6e0c35603409499c6c961b79d794609ab/diff:/data/docker/overlay2/0b03c61e8dc6c2c8136fe986c6b83acc70a929a5dbffaf53f0caee7a7a9e993b/diff:/data/docker/overlay2/0395e2f86eb0e796574e5ba12d72912803e6d20fc09e1b6173e62a1a03f65305/diff:/data/docker/overlay2/315b934acc4a0538d2fd00ab2a2709aa2df209292584e050ce66f6c1f899b92b/diff:/data/docker/overlay2/3b920b7c5277f464c39d6725c6f7728c715d82e7c6b472b226298aaf588c64dc/diff:/data/docker/overlay2/68b33d4136d503e111d2dd86f114c6c4cd4a2135ef1eed9858eb2508670b9e25/diff",
"MergedDir": "/data/docker/overlay2/68b7990f2f31cd3f97a891be6a499cef9aed5e56b42ee73af47e4e357bead299/merged",
"UpperDir": "/data/docker/overlay2/68b7990f2f31cd3f97a891be6a499cef9aed5e56b42ee73af47e4e357bead299/diff",
"WorkDir": "/data/docker/overlay2/68b7990f2f31cd3f97a891be6a499cef9aed5e56b42ee73af47e4e357bead299/work"


LowerDir:展示了镜像的层叠关系,从左至右、层叠顺序为从上到下

  1. /data/docker/overlay2/0692d0cadd2c300813a3343a3f6077b6e0c35603409499c6c961b79d794609ab/diff下只有/usr/local/bin/patch.sh 即补丁平台脚本

  2. /data/docker/overlay2/0b03c61e8dc6c2c8136fe986c6b83acc70a929a5dbffaf53f0caee7a7a9e993b/diff下只有/usr/local/bin/offline.sh 即优雅下线脚本

  3. /data/docker/overlay2/0395e2f86eb0e796574e5ba12d72912803e6d20fc09e1b6173e62a1a03f65305/diff下只有/usr/local/bin/entrypoint.sh即初始化脚本

  4. /data/docker/overlay2/315b934acc4a0538d2fd00ab2a2709aa2df209292584e050ce66f6c1f899b92b/diff下有bin etc root lib lib64  usr 等,即用户文件系统rootfs(这里是我们的基础镜像)

  5. /data/docker/overlay2/3b920b7c5277f464c39d6725c6f7728c715d82e7c6b472b226298aaf588c64dc/diff下有/usr/local/tomcat/目录

  6. /data/docker/overlay2/68b33d4136d503e111d2dd86f114c6c4cd4a2135ef1eed9858eb2508670b9e25/diff下是基础镜像的基础镜像alpine:latest层


每一层“变化”的文件,都在diff目录下,每一层还有一个link文件,是该层的短名称。所有的短名称都会在/data/docker/overlay2/l/下有相应的软连接,可以快速找到该层的diff目录。同时,这个文件存储的是指向本layer的一个符号链接文件。overlayfs的原理就是将多个layer挂载到一个目录下,而挂载的多个layer是通过mount options指定的,这个选项的最大长度有限制,通常为4096,如果我们直接使用上面的sha256目录,mount的layer个数就太少了,因此实际mount时使用的是这个符号链接,就能够多支持一些layer了


而相应的,一个所谓的“镜像”就是这样从底层到上层的N个层组合出来的。而相同的层,在不同的镜像之间是共享的。


2、从docker history 看分层

/uploads/files_user1/question/5ef1fdecd827a914327.png

对应dockerfile

FROM tomcat:9.0.31-jdk8u202-alpine
COPY ROOT /usr/local/tomcat/webapps/ROOT
EXPOSE 8888

可以看到以下3层,是通过本dockerfile创建的,但是后面的呢? 是基础镜像。

/uploads/files_user1/question/5ef1fe7905a92511854.png

再看下基础镜像的dockerfile,就知道后面的层是如何来的了

/uploads/files_user1/question/5ef1fed3b3971866757.png


5、docker镜像在本机是如何保存的?


/data/docker/image/overlay2/
├—— distribution
├—— imagedb
├—— layerdb
└—— repositories.json
  • repositories.json文件存储了当前所有的镜像和其镜像仓库的信息。

  • imagedb/content/sha256/<IMAGE-ID>目录下是每个镜像的详细信息,文件名即为镜像ID.这里面除了上面提到RootFS信息外,还有镜像的构建历史命令信息。

  • 然后是layerdb/sha256/<ChainID>:这里面的每个目录即为镜像每layer的信息,文件名是layer的ChainID.


通过repositories.json找到镜像的sha256,通过相应的算法即可找到该镜像第一层的位置。

/uploads/files_user1/question/5ef7e5e3c244d291783.png


6、镜像制作

6.1通过dockerfile

以merge为例,目前的形态是

cd /data/ncc/nchome_merge/
docker build -f Dockerfile . -t  reg.yyuap.io/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/merge:tag

制作过程:

  • Sending build context to Docker daemon ---会将Dockerfile目录当前目录的文件夹都发送给Dockerdaemon,文件会临时拷贝到/data/docker/tmp/docker-builderXXXXXX/下

#可以通过在dockerfile同级目录配置.dockerignore进行忽略:http://nccloud.yytimes.com/q_322.html

#这里既有的模式可以优化这块,将dockerfile放到ROOT目录再build,实现本环节数据操作量减半

/uploads/files_user1/question/5ef1ed0886a17204910.png

/uploads/files_user1/question/5ef1ed1ea9a21572877.png

2、以当前运行的镜像为模板,生成新镜像

背景:当前镜像打了很多补丁,补丁和原始镜像是分离的,如何把带着补丁的镜像生成新镜像?

核心命令:

docker commit -a="nxz" -m="niuxiaozheng" 7b2d91a27fa6 reg.yyuap.io/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/fi-gl:v2020

参考:http://nccloud.yytimes.com/q_209.html

PS.不推荐通过docker commit提交,由于通过本指令提交的镜像,其修改无法通过分层文件体现,一方面涉及镜像的修改不可知;一方面会导致存储方面的浪费,再者由于存储分层机制,在既有层删除、增加的内容,均会保留,导致镜像体积膨胀;


6.2 "docker build -f Dockerfile . -t  reg.yyuap.io/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/merge:test"其中-t前的“.”是做什么用的?

答:镜像构建过程中的上下文环境目录。在数据第一步打包传送给dockerd时取的数据目录,以及之后它基于docker在做处理COPY等命令时取的相对路径。

#同理,这块延伸上述5.1的build优化思路,可以不改变dockerfile位置,但是修改dockerfile内容及docker build命令,指定上下文路径,参考如下

#dockerfile第2行改为COPY ROOT /usr/local/tomcat/webapps/ROOT
docker build -f Dockerfile nchome_merge/temp/stage -t  reg.yyuap.io/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/merge:test

/uploads/files_user1/question/5ef1f22831166482451.png

6.3如何理解docker的tag

reg.yyuap.io/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/scm-scm:20200612203405
仓库名/项目名/镜像名:TAGS

/uploads/files_user1/question/5ef860c357203341007.png


7、容器的启动过程

业务过程:

  • 拉取镜像,若本地已经存在该镜像,则不用到Docker仓库去拉取

  • 使用镜像创建新的容器

  • 分配union文件系统并且挂载一个可读写的层,任何修改容器的操作都会被记录在这个读写层上,你可以保存这些修改成新的镜像,也可以选择不保存,那么下次运行改镜像的时候所有修改操作都会被消除

  • 分配网络/桥接接口,创建一个允许容器与本地主机通信的网络接口

  • 设置ip地址,从池中寻找一个可用的ip地址附加到容器上,换句话说,localhost并不能访问到容器

  • 运行你指定的程序

  • 捕获并且提供应用输出:连接并且记录标准输出、输入和错误让你可以看到你的程序是如何运行的。


技术过程:

http://nccloud.yytimes.com/uploads/files_user1/answer/5ef1a11131ef1493791.png


  1. 在Libcontainer中,p.cmd.Start创建子进程,就进入了pipe wait等待父写入pipe,p.cmd.Start创建了新的Namespace,这时子进程就已经在新的Namespace里了。

  2. daemon线程在执行p.manager.Apply,创建新的Cgroup,并把子进程放到新的Cgroup中。

  3. daemon线程做一些网络配置,会把容器的配置信息通过管道发给子进程。同时让子进程继续往下执行。

  4. daemon线程则进入pipe wait阶段,容器剩下的初始化由子进程完成了。

  5. rootfs的切换在setupRootfs函数中。(首先子进程会根据config,把host上的相关目录mount到容器的rootfs中,或挂载到一些虚拟文件系统上,这些挂载信息可能是-v指定的volume、容器的Cgroup信息、proc文件系统等)。

  6. 完成文件系统操作,就执行syscall.PivotRoot把容器的根文件系统切换rootfs

  7. 再做一些hostname及安全配置,就可以调用syscall.Exec执行容器中的init进程了

  8. 容器完成创建和运行操作,同时通知了父进程,此时,daemon线程会回到Docker的函数中,执行等待容器进程结束的操作,整个过程完成



已邀请:

基于运维场景的常用操作


1、开发要看容器里的日志和代码?

http://nccloud.yytimes.com/q_323.html


2、基于已有的镜像创建新镜像?

http://nccloud.yytimes.com/q_209.html


3、docker命令(http://nccloud.yytimes.com/q_321.html),及运维场景常用

https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181014202945937-1677031749.png

3.1当前做盘服务器的docker安装流程,位于(1init_forbuild.sh)

command -v docker|grep docker 2>&1 >/dev/null
    if [ $? -eq 0 ]; then
        echo "本机已经安装docker"
    else
        echo "安装docker..."
        cd /data/ncc/base/docker_rpm
        yum install -y libcgroup
        rpm -ivh  containerd.io-1.2.6-3.3.el7.x86_64.rpm --nodeps
rpm -ivh  docker-ce-cli-19.03.3-3.el7.x86_64.rpm
rpm -ivh docker-ce-19.03.3-3.el7.x86_64.rpm --nodeps
    fi
    if [ `systemctl is-active docker` != "active" ]; then
        echo "配置启动docker服务..."
        mkdir -p /etc/systemd/system/docker.service.d
echo '[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --storage-driver=overlay2 --insecure-registry 0.0.0.0/0  --ip-forward=true -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock -g=/data/docker'>/etc/systemd/system/docker.service.d/docker.conf
        systemctl daemon-reload
        systemctl enable docker
        systemctl restart docker
        echo "docker服务启动成功。"
        
    else
        echo "本机docker服务已启动"
    fi

3.2查找当前主机上的镜像

docker images [OPTIONS] [REPOSITORY[:TAG]]

OPTIONS说明:
-a :列出本地所有的镜像(含中间映像层,默认情况下,过滤掉中间映像层);
--digests :显示镜像的摘要信息;
-f :显示满足条件的镜像;
--format :指定返回值的模板文件;
--no-trunc :显示完整的镜像信息;
-q :只显示镜像ID。

3.3查找当前主机上运行的镜像

docker ps [OPTIONS]

OPTIONS说明:
-a :显示所有的容器,包括未运行的。
-f :根据条件过滤显示的内容。
--format :指定返回值的模板文件。
-l :显示最近创建的容器。
-n :列出最近创建的n个容器。
--no-trunc :不截断输出。
-q :静默模式,只显示容器编号。
-s :显示总的文件大小。

3.4登录容器

docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

OPTIONS说明:
-d :分离模式: 在后台运行
-i :即使没有附加也保持STDIN 打开
-t :分配一个伪终端

#常用的:docker exec -it 容器ID /bin/bash
#查询当前主机运行的容器后,以伪终端的方式进入当前主机执行/bin/bash

3.5拷贝文件从容器或到容器

docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH

OPTIONS说明:
-L :保持源目标中的链接
#这里的SRC_PATH和DEST_PATH均为绝对路径

#常用的:拷贝资源池主机文件到容器,或从容器拷贝文件出来

3.6清理删除指定镜像

docker rmi [OPTIONS] IMAGE [IMAGE...]

OPTIONS说明:
-f :强制删除;
--no-prune :不移除该镜像的过程镜像,默认移除;

3.7清理docker镜像,释放空间

Docker prune 命令
#删除 所有未被 tag 标记和未被容器使用的镜像:
docker image prune
 
#删除 所有未被容器使用的镜像:
docker image prune -a
 
#删除 所有停止运行的容器:
docker container prune
 
#删除 所有未被挂载的卷:
docker volume prune
 
#删除 所有网络:
docker network prune
 
#删除 docker 所有资源:
docker system prune

3.8运行一个镜像

docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

OPTIONS说明:
-a stdin: 指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项;
-d: 后台运行容器,并返回容器ID;
-i: 以交互模式运行容器,通常与 -t 同时使用;
-P: 随机端口映射,容器内部端口随机映射到主机的端口
-p: 指定端口映射,格式为:主机(宿主)端口:容器端口
-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
--name="nginx-lb": 为容器指定一个名称;
--dns 8.8.8.8: 指定容器使用的DNS服务器,默认和宿主一致;
--dns-search example.com: 指定容器DNS搜索域名,默认和宿主一致;
-h "mars": 指定容器的hostname;
-e username="ritchie": 设置环境变量;
--env-file=[]: 从指定文件读入环境变量;
--cpuset="0-2" or --cpuset="0,1,2": 绑定容器到指定CPU运行;
-m :设置容器使用内存最大值;
--net="bridge": 指定容器的网络连接类型,支持 bridge/host/none/container: 四种类型;
--link=[]: 添加链接到另一个容器;
--expose=[]: 开放一个端口或一组端口;
--volume , -v: 绑定一个卷

#常用的:docker run -it 镜像名:TAG /bin/bash
#以交互模式,启动一个镜像,并通过伪终端执行/bin/bash

3.9保存一个docker镜像

将指定镜像保存成 tar 归档文件。

语法
docker save [OPTIONS] IMAGE [IMAGE...]

OPTIONS 说明:
-o :输出到的文件。

#常用的:做盘时先制作镜像,然后再把镜像保存成tar包放进安装盘
#docker save -o ncc-sysconfig.tar  reg.yyuap.io/c87e2267-1001-4c70-bb2a-ab41f3b81aa3/ncc-sysconfig:2005

3.10加载一个镜像到本地

语法
docker load [OPTIONS]
OPTIONS 说明:
--input , -i : 指定导入的文件,代替 STDIN。
--quiet , -q : 精简输出信息。

#常用的:做盘服务器,拆分制作镜像阶段,需要加载“基础”镜像。
    cd /data/ncc/base/images/
    docker load -i tomcat-9.0.31-jdk8u202-alpine.tar.gz
    docker load -i nginx-1.13.5.tar
    docker load -i jdk-8u202-alpine.tar.gz

4、基于实际项目已有的及可能的问题点

4.1资源池主机上的镜像迭代、过期资源释放以及镜像仓库的清理

4.2大型机多容器场景下的日志的IO开销,及日志归档存储

4.3底座的升级备份以及资源池主机docker的升级

4.4海量容器下数据库的连接数开销及限制


Dockerfile是什么——是构建 docker 镜像的配置文件

 Dockerfile 分为四个部分:

  • 基础镜像(父镜像)信息指令 FROM

  • 维护者信息指令 MAINTAINER

  • 镜像操作指令 RUN 、 EVN 、 ADD 和 WORKDIR 等

  • 容器启动指令 CMD 、 ENTRYPOINT 和 USER 等


FROM
FROM 是用于指定基础的 images ,一般格式为 FROM <image> or FORM <image>:<tag> ,所有的 Dockerfile 都用该以 FROM 开头,FROM 命令指明 Dockerfile 所创建的镜像文件以什么镜像为基础,FROM 以后的所有指令都会在 FROM 的基础上进行创建镜像。可以在同一个 Dockerfile 中多次使用 FROM 命令用于创建多个镜像。比如我们要指定 python 2.7 的基础镜像,我们可以像如下写法一样:
FROM python:2.7


MAINTAINER
MAINTAINER 是用于指定镜像创建者和联系方式,一般格式为 MAINTAINER <name> 。
MAINTAINER XXXXXX


COPY
COPY 是用于复制本地主机的 <src> (为 Dockerfile 所在目录的相对路径)到容器中的 <dest>。
当使用本地目录为源目录时,推荐使用 COPY 。一般格式为 COPY <src><dest> 。例如我们要拷贝当前目录到容器中的 /app 目录下,我们可以这样操作:
COPY . /app


WORKDIR
WORKDIR 用于配合 RUN,CMD,ENTRYPOINT 命令设置当前工作路径。可以设置多次,如果是相对路径,则相对前一个 WORKDIR 命令。默认路径为/。一般格式为 WORKDIR /path/to/work/dir 。例如我们设置/app 路径,我们可以进行如下操作:
WORKDIR /app


RUN
RUN 用于容器内部执行命令。每个 RUN 命令相当于在原有的镜像基础上添加了一个改动层,原有的镜像不会有变化。一般格式为 RUN <command> 。例如我们要安装 python 依赖包,我们做法如下:
RUN pip install -r requirements.txt


EXPOSE
EXPOSE 命令用来指定对外开放的端口。一般格式为 EXPOSE <port> [<port>...]
例如上面那个例子,开放5000端口:
EXPOSE 5000


ENTRYPOINT
ENTRYPOINT 可以让你的容器表现得像一个可执行程序一样。一个 Dockerfile 中只能有一个 ENTRYPOINT,如果有多个,则最后一个生效。

ENTRYPOINT 命令也有两种格式:
ENTRYPOINT ["executable", "param1", "param2"] :推荐使用的 exec形式
ENTRYPOINT command param1 param2 :shell 形式
例如下面这个,我们要将 python 镜像变成可执行的程序,我们可以这样去做:
ENTRYPOINT ["python"]


CMD
CMD 命令用于启动容器时默认执行的命令,CMD 命令可以包含可执行文件,也可以不包含可执行文件。不包含可执行文件的情况下就要用 ENTRYPOINT 指定一个,然后 CMD 命令的参数就会作为ENTRYPOINT的参数。

CMD 命令有三种格式:
CMD ["executable","param1","param2"]:推荐使用的 exec 形式。
CMD ["param1","param2"]:无可执行程序形式
CMD command param1 param2:shell 形式。
一个 Dockerfile 中只能有一个CMD,如果有多个,则最后一个生效。而 CMD 的 shell 形式默认调用 /bin/sh -c 执行命令。
CMD 命令会被 Docker 命令行传入的参数覆盖:docker run busybox /bin/echo Hello Docker 会把 CMD 里的命令覆盖。
例如我们要启动 /app ,我们可以用如下命令实现:
CMD ["app.py"]


  • 一个原生的 Linux 容器格式,Docker 中称为 libcontainer。

  • Linux 内核的命名空间(namespace),用于隔离文件系统、进程和网络。

  • 文件系统隔离:每个容器都有自己的 root 文件系统。

  • 进程隔离:每个容器都运行在自己的进程环境中。

  • 网络隔离:容器间的虚拟网络接口和IP地址都是分开的。

  • 资源隔离和分组:使用 cgroups(即control group,Linux 的内核特性之一)将 CPU 和内存之类的资源独立分配给每个 Docker 容器。

  • 写时复制(Copy on write):文件系统都是通过写时复制创建的,这就意味着文件系统是分层的、快速的,而且占用磁盘空间更小。

  • 日志:容器产生的STDOUT、STDERR和STDIN这些IO流都会被收集并计入日志。用来进行日志分割和故障排错。

  • 交互式shell:用户可以创建一个伪 tty 终端,将其连接到STDIN,为容器提供一个交互式shell。





资源隔离


文件系统隔离

每个进程容器运行在完全独立的根文件系统里。

容器中的文件系统都是挂载到了宿主机真实系统中的一个目录下面的。对于不同的容器,挂载点是不一样的,而容器不能穿越根目录上一级去访问, 所以这里对每一个容器都做到了文件系统隔离。

外挂数据卷:

可以把war包等文件的放置路径映射到主机上的某个路径,达到业务逻辑和数据持久化分割开(类似MVC思想)。

资源隔离

Cgroups:Cgroups是Linux内核功能,它让两件事情变成可能:限制Linux进程组的资源占用(内存、CPU);为进程组制作 PID、UTS、IPC、网络、用户及装载命名空间。

docker使用cgroup为每个进程容器分配不同的系统资源,将不同进程的资源使用隔离开。

命名空间隔离

Docker充分利用了一项称为namespaces的技术来提供隔离的工作空间,我们称之为container(容器)。当你运行一个容器的时候,Docker为该容器创建了一个命名空间。这样提供了一个隔离层,每一个应用在它们自己的命名空间中运行而且不会访问到命名空间之外。

一些Docker使用到的命名空间有:

  • pid命名空间: 进程隔离(PID: Process ID)

  • net命名空间: 管理网络接口(NET: Networking)

  • ipc命名空间: 管理进程间通信资源 (IPC: InterProcess Communication)

  • mnt命名空间: 管理挂载点 (MNT: Mount)

  • uts命名空间: 隔离内核和版本标识 (UTS: Unix Timesharing System)

namespace让进程隔离更灵活:

linux实现进程的方法为fork,实现的方式分为两个步骤:

  1. 在内存中复制一个父进程,得到“子进程”,此时子进程就是父进程上下文的简单克隆,内容完全一致

  2. 设置子进程的 pid,parent_pid,以及其他和父进程不一致的内容

从进程被制造的步骤可以看出,进程大部分资源和父进程共享,如果需要制造一个看起来像虚拟机的进程,我们需要比普通的进程多做几步:

  • 可以自定义rootfs,比如我们把整个ubuntu发行版的可执行文件以及其他文件系统都放在目录/home/admin/ubuntu/ 下,当我们重定义rootfs = /home/admin/ubuntu 后,则该文件地址被印射为 "/"

  • 把自身pid 印射为0,并看不到其他任何的pid,这样自身的pid成为系统内唯一存在pid,看起来就像新启动了系统

  • 用户名隔离,可以把用户名设置为“root”

  • hostname隔离,可以另取一个hostname,成为新启动进程的hostname

  • IPC隔离,隔离掉进程之间的互相通信

  • 网络隔离,隔离掉进程和主机之间的网络

这些隔离资源需要的方法代码在linux系统内核中已经有提供支持。

所以虽然docker帮助我们准备好了rootfs地址,镜像里面的文件,以及各种资源隔离的配置,但是在启动一个容器的时候,它只是调用系统中早已内置的可以隔离资源的方法,而kernel支持这些方法,也是在创建进程的方法上做了一层资源隔离的扩展而已。这就解释了docker两个特性:

  • 启动速度快,因为本质来说容器和进程差别没有想象中的大,共享了很多代码,流程也差的不多

  • linux内核版本有最低的要求,因为linux是在某个版本后开始支持隔离特性

网络隔离

每个进程容器运行在自己的网络命名空间里,拥有自己的虚拟接口和IP地址

在安装好docker后,会默认初始化一个docker0的网桥。在host机器上,会为每一个容器生成一个默认的网卡,这个网卡的一端连接在容器的eth0,一端连接到docker0。这样就实现了每个容器有一个单独的IP。

如果需要外部能够访问容器,需要做端口映射规则,和配置虚拟机一样的道理, 只不过这里的80端口并没有占用了本地端口,而是在容器内部做了监听,外部是通过docker0桥接过去的,每个容器间也做到了端口和网络隔离。


1. Docker System命令

在《谁用光了磁盘?Docker System命令详解》中,我们详细介绍了Docker System命令,它可以用于管理磁盘空间。

docker system df命令,类似于Linux上的df命令,用于查看Docker的磁盘使用情况:

docker system df
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              147                 36                  7.204GB             3.887GB (53%)
Containers          37                  10                  104.8MB             102.6MB (97%)
Local Volumes       3                   3                   1.421GB             0B (0%)
Build Cache                                                 0B                  0B


可知,Docker镜像占用了7.2GB磁盘,Docker容器占用了104.8MB磁盘,Docker数据卷占用了1.4GB磁盘。

docker system prune命令可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及dangling镜像(即无tag的镜像)。docker system prune -a命令清理得更加彻底,可以将没有容器使用Docker镜像都删掉。注意,这两个命令会把你暂时关闭的容器,以及暂时没有用到的Docker镜像都删掉了……所以使用之前一定要想清楚吶。

执行docker system prune -a命令之后,Docker占用的磁盘空间减少了很多:

docker system df
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              10                  10                  2.271GB             630.7MB (27%)
Containers          10                  10                  2.211MB             0B (0%)
Local Volumes       3                   3                   1.421GB             0B (0%)
Build Cache                                                 0B                  0B


2. 手动清理Docker镜像/容器/数据卷

对于旧版的Docker(版本1.13之前),是没有Docker System命令的,因此需要进行手动清理。这里给出几个常用的命令:

删除所有关闭的容器

docker ps -a | grep Exit | cut -d ' ' -f 1 | xargs docker rm


删除所有dangling镜像(即无tag的镜像)

docker rmi $(docker images | grep "^<none>" | awk "{print $3}")


删除所有dangling数据卷(即无用的Volume)

docker volume rm $(docker volume ls -qf dangling=true)


3. 限制容器的日志大小

有一次,当我使用1与2提到的方法清理磁盘之后,发现并没有什么作用,于是,我进行了一系列分析。

在Ubuntu上,Docker的所有相关文件,包括镜像、容器等都保存在/var/lib/docker/目录中:

du -hs /var/lib/docker/
97G /var/lib/docker/


Docker竟然使用了将近100GB磁盘,这也是够了。使用du命令继续查看,可以定位到真正占用这么多磁盘的目录:

92G  /var/lib/docker/containers/a376aa694b22ee497f6fc9f7d15d943de91c853284f8f105ff5ad6c7ddae7a53


docker ps可知,Nginx容器的ID恰好为a376aa694b22,与上面的目录/var/lib/docker/containers/a376aa694b22的前缀一致:

docker ps
CONTAINER ID        IMAGE                                       COMMAND                  CREATED             STATUS              PORTS               NAMES
a376aa694b22        192.168.59.224:5000/nginx:1.12.1            "nginx -g 'daemon off"   9 weeks ago         Up 10 minutes                           nginx


因此,Nginx容器竟然占用了92GB的磁盘。进一步分析可知,真正占用磁盘空间的是Nginx的日志文件。那么这就不难理解了。我们Fundebug每天的数据请求为百万级别,那么日志数据自然非常大。

使用truncate命令,可以将Nginx容器的日志文件“清零”:

truncate -s 0 /var/lib/docker/containers/a376aa694b22ee497f6fc9f7d15d943de91c853284f8f105ff5ad6c7ddae7a53/*-json.log


当然,这个命令只是临时有作用,日志文件迟早又会涨回来。要从根本上解决问题,需要限制Nginx容器的日志文件大小。这个可以通过配置日志的max-size来实现,下面是Nginx容器的docker-compose配置文件:

nginx:
image: nginx:1.12.1
restart: always
logging:
driver: "json-file"
options:
  max-size: "5g"


重启Nginx容器之后,其日志文件的大小就被限制在5GB,再也不用担心了~

4. 重启Docker

有一次,当我清理了镜像、容器以及数据卷之后,发现磁盘空间并没有减少。根据Docker disk usage提到过的建议,我重启了Docker,发现磁盘使用率从83%降到了19%。根据高手指点,这应该是与内核3.13相关的Bug,导致Docker无法清理一些无用目录:

it's quite likely that for some reason when those container shutdown, docker couldn't remove the directory because the shm device was busy. This tends to happen often on 3.13 kernel. You may want to update it to the 4.4 version supported on trusty 14.04.5 LTS.


The reason it disappeared after a restart, is that daemon probably tried and succeeded to clean up left over data from stopped containers.

我查看了一下内核版本,发现真的是3.13:

uname -r
3.13.0-86-generic


如果你的内核版本也是3.13,而且清理磁盘没能成功,不妨重启一下Docker。当然,这个晚上操作比较靠谱。


从 Docker 1.10 开始,COPYADDRUN语句会向镜像中添加新层。前面的示例创建了两个层而不是一个。  

要回复问题请先登录注册