百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

一文扫盲Docker

ztj100 2025-02-06 17:14 9 浏览 0 评论

输麻了,

隔壁组的大哥给实习生妹妹讲Docker

把实习生妹妹忽悠得一愣一愣的,

实习生妹妹看隔壁组大哥那崇拜的小眼神,

我属实是特别嫉妒,

为了扳回一局,

我决定好好整理一下Docker的文档。


前言

本文将对Docker的镜像,Dockerfile的关键指令,Docker的容器进行关键概念总结,整体内容不会很深,但也绝不是停留在指令介绍的层面。

本文将重点分析如下内容。

  1. 镜像的分层结构及大小;
  2. CMD指令和ENTRYPOINT指令的关系;
  3. 如何让容器中的主进程的PID是1;
  4. 容器的分层结构及大小。

参考:Docker官方文档

正文

一. Linux Namespace

1. 概念说明

NamespaceLinux资源隔离方案,用于将某种全局资源通过Namespace进行隔离,使得Namespace下的资源仅由该Namespace下的进程共享。

Linux内核实现了如下六种Namespace

Namespace类型

隔离的资源

容器下的效果

Mount

文件系统挂载点

不同容器中可以看到不同的目录结构

Network

例如网络设备,端口等网络相关资源

容器拥有独立的网络设备,IP地址,端口号等,这就使得同一宿主机上不同容器的进程可以绑定到相同端口上

UTS

HostnameDomainname

容器可以拥有自己的主机名和域名

IPC

信号量,消息队列和共享内存等进程间通信资源

不同容器里的进程无法直接进行进程间通信

PID

进程编号

容器中的进程可以有独立的PID,以及容器中的进程会有两个PID:宿主机上一个PID和容器里一个PID

User

用户和用户组

宿主机和容器中可以有两套用户和用户组

2. Docker中的使用

Docker创建一个容器时,Docker就会去创建上述六种Namespace实例,然后容器中的所有进程就会被放到创建出来的Namespace中,从而容器中的进程只能使用当前容器的Namespace下的资源,不同容器之间实现了资源隔离。

二. Linux Control Groups

1. 概念说明

Namespace让容器中的进程拥有了独立的运行环境,现在还需要限制这些进程使用的CPU内存磁盘网络等资源,而cgroupControl Groups)就能实现这样的功能。

cgroupLinux提供的用于将进程分组管理的功能,针对每个组里的进程,提供如下能力。

  1. 限制组里进程使用的CPU核数和使用率;
  2. 限制组里进程使用的内存大小;
  3. 限制组里进程对物理设备的使用,例如磁盘
  4. 为组里进程分配网络带宽

可以进入到宿主机的/sys/fs/cgroup目录,观察到有如下目录。

每个目录都是cgroup可以控制的资源类型,说明如下。

  1. blkio。限制cgroup中的进程对块设备(例如磁盘)的IO速度;
  2. cpu。限制cgroup中的进程对CPU的使用率;
  3. cpuacct。统计cgroup中的进程对CPU的使用率;
  4. cpuset。为cgroup中的进程分配CPU核数;
  5. devices。限制cgroup中的进程使用物理设备的权限;
  6. freezer。挂起或恢复cgroup中的所有进程;
  7. hugetlb。限制cgroup中的进程对大页的使用数量(4KB称为小页,反之例如2MB或1GB称为大页);
  8. memory。限制cgroup中的进程使用的内存大小,并生成使用报告;
  9. net_cls。为cgroup中的进程的所有网络包添加classid标记符号,用于iptables(网络包过滤)和TCTraffic Control,流量控制);
  10. perf_event。监控cgroup中的进程的性能。

2. Docker中的使用

Docker中,启动一个容器后,会在宿主机的/sys/fs/cgroup目录下的相关资源目录下创建以容器id为名字的目录,如下所示。

容器中的进程的PID(容器进程在宿主机中的PID)会被记录到tasks文件中,如下所示。

现在再查看一下cpu.cfs_quota_us,如下所示。

-1表示无限制,可以在使用docker run时通过-c来指定CPU限制相关参数,参数说明如下所示。

三. 镜像Image

镜像,是Docker中的容器的静态表示。镜像中包含着容器运行时的代码运行依赖,是一个多层结构,且每一层都是只读的,所以镜像一旦创建,就无法被修改。

下面将从多个维度来说明镜像的概念。

1. 基础镜像

基础镜像,有如下两个特点。

  1. 不依赖其它镜像
  2. 其它镜像可基于基础镜像做扩展

那么可以联想到,通常基础镜像就是那些系统级镜像例如CentOS镜像,Ubuntu镜像等,这些镜像会作为地基,为容器中的程序运行提供操作系统(下面讨论的基础镜像均指系统级镜像)。

那么现在宿主机上有一个安装好的操作系统,宿主机上运行的一个容器中也有基础镜像提供的操作系统,这两个操作系统是否是一样的呢。说一样也行,说不一样也行,说明如下。

  1. 一样。是因为容器中由镜像提供的操作系统底层使用的内核(Kernel)其实就是宿主机安装好的操作系统的内核。那么试问Linux的虚拟机上能运行使用Windows作为基础镜像的容器吗,那自然是不能的;
  2. 不一样。是因为例如Red Hat系统的虚拟机上,可以运行使用Ubuntu作为基础镜像的容器,容器和宿主机上的Linux是不一样的发行版

Linux的不同发行版本都会采用相同的Linux内核,然后不同的发行版会在Linux内核的基础上加入各自的改动)

既然都能使用宿主机的操作系统内核了,那么为啥还需要基础镜像呢。这里其实就又会涉及到两个概念:bootfsrootfs

  1. bootfs,是Linux启动时需要加载的内容,由内核提供,属于内核空间。所以基础镜像中的操作系统需要依赖宿主机操作系统中的内核所提供的bootfs,才能将容器中的操作系统运行起来;
  2. rootfs,是基本的命令,类库或者工具(例如包管理工具yumapt-get等),属于用户空间。由于容器中的文件系统通常是和宿主机上的文件系统隔离的,所以容器中运行的应用程序是无法访问到宿主机上的相关类库或者工具,基于这样的情况,基础镜像中就需要包含rootfs相关的内容。

到这里,基本就理清了基础镜像中的操作系统和宿主机上的操作系统的关系,简单点说就是:基础镜像中的操作系统是不完整的,缺少内核,需要使用宿主机上的操作系统的内核。(一个Ubuntu镜像才201MB,一个Ubuntu 18.4的安装包2GB,少了啥自己想)

2. 镜像组成

现在通过docker save -o filename.tar imagename:version将一个镜像打为tar并解压,解压后目录如下所示。

每种文件说明如下。

  1. repositories。包含镜像名称,版本号,以及最上层的哈希值;
  2. manifest.json。镜像(假)元数据Json文件,包含镜像真正的元数据文件的文件名,以及镜像每一层的哈希值等;
  3. 各种文件夹。镜像每一层对应的文件夹,是一个镜像真正占用空间大小的内容;
  4. ac0f3e4255c186822ea64dcb267bea7c897d44ace98c61cd3faaffdb9479114f.json。镜像真正的元数据文件。

基于Dockerfile构建出来的镜像,真正占用磁盘空间的层,要么是基础镜像层,要么是使用了ADD,COPY或者RUN指令而构建出来的层,而像ENVCMD等指令,虽然也会构建镜像的层,但其实是一个空层,这些指令的真正作用是会修改构建出来的镜像的元数据。

3. RUN指令对镜像大小的影响

首先上一张图。

Dockerfile中要基于ubuntu:18.04基础镜像来构建镜像,其中会通过RUN指令执行两次api-get update,假设每次执行都会更改到ubuntu:18.04基础镜像中的File1File2,那么此时就会出现每个RUN指令都会在镜像上增加一层,且里面会保存发生了更新的文件(File1File2)。

现在再看如下一张图。

现在只会有一个RUN指令,且在一个RUN指令中执行两次api-get update,结果就是只会在镜像上增加一层,最终镜像大小相较于上面的分开写RUN指令会少40M,且镜像总层数也会减1。

所以,针对文件的相关操作,尽量放在一个RUN指令中执行。

4. 镜像构建

通常,构建镜像有两种方式。

  1. 创建容器,执行命令,最后通过docker commit指令基于容器生成一个镜像;
  2. 提供Dockerfile文件,通过docker build指令创建一个镜像。

实际上第2点本质的创建思路就是基于第一点。

现在有如下一个Dockerfile文件。

FROM ubuntu:latest
WORKDIR /
COPY ./record.log /
ENV KEY="VALUE"
EXPOSE 8080
ENTRYPOINT top -b
复制代码

如果基于Dockerfile构建镜像,那么一个构建出来的镜像的分层结构示意图如下。

Dockerfile中每一个指令执行,都会生成一个中间镜像,而这个中间镜像是会被Docker进行缓存,只能通过docker images -a才能将中间镜像查询出来。还有就是当前指令执行后生成的中间镜像,会比上一条指令生成的中间镜像多一层。

现在基于docker build来创建一个镜像,打印如下。

在上述构建日志中,出现了如下两种情况。

  1. Using cache ...。表明本地已经缓存了这条指令执行后生成的中间镜像,所以直接从缓存中获取中间镜像;
  2. Running in ... Removing intermediate container ...。表明没有匹配到缓存,此时会先创建一个临时容器,然后执行COPY指令,接着使用docker commit基于临时容器生成中间镜像,最后移除临时容器。

5. 镜像的大小

已知镜像是一个层级结构,那么镜像的总大小实际就是每一层的大小相加

那么在构建一个镜像时,可以观察一下什么时候会增加构建出来的镜像的大小。

  1. FROM。因为会引用基础镜像,那么构建出来的镜像大小首先就会包含基础镜像的大小;
  2. ADDCOPY。这两个指令主要实现构建镜像时向镜像中添加文件或目录,添加成功则在原镜像基础上新增一层,并且新增的这一层的大小就是添加的内容的大小;
  3. RUN。运行指令,运行结果如果对文件系统产生了更改(新增,删除和更新文件),那么更新的这部分内容会作为镜像新的一层。注意,RUN指令不一定会增加镜像的大小,如果没有对文件系统产生影响,那么增加的镜像的层的大小会为0。

那么现在假如有5个镜像,每个镜像1G,那么这5个镜像在磁盘上占用的空间就是5G吗,那自然不是的。前面分析已经知道,镜像是一个层级结构,一个镜像是一层一层叠加起来的一个整体,但是每一层的内容实际是可以被复用的,图示如下。

所以关于镜像的大小,可以总结如下。

  1. 镜像的总大小由每一层的大小相加得到;
  2. FROMADDCOPYRUN指令增加的层会导致镜像大小增加;
  3. 多个镜像总大小不等于多个镜像在磁盘上占用的总大小;
  4. 镜像的复用其实是镜像之间可以复用层。

四. Dockerfile常用指令

1. ADD

ADD指令用于从源地址src复制文件,目录或远程文件,然后添加到目标地址dest的文件系统中。

ADD指令有如下两种使用格式。

  1. ADD [--chown=<user>:<group>] [--checksum=<checksum>] <src>... <dest>
  2. ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

说明如下。

  1. 如果路径中有空格,请使用上述的第2种格式;
  2. --chown 用于给添加内容指定所有权。其中usergroup都可以使用UIDGID来表示;
  3. --checksum 用于校验远端文件。例如ADD --checksum=sha256:24454f830cdb571e2c4ad15481119c43b3cafd48dd869a9b2945d1036d1dc68d mirrors.edge.kernel.org/pub/linux/k… /
  4. src路径需要在当前构建上下文中。例如 ./learn-docker-1.0-SNAPSHOT.jar,则需要在当前目录下存在learn-docker-1.0-SNAPSHOT.jar文件,这是因为在docker build的第一步,就是将当前目录下的所有文件和子文件都上传到Docker并作为当前构建上下文(所以当前目录下的内容就称为当前构建上下文);
  5. src如果是URL,而dest不以斜杠结尾,则会从URL下载文件然后复制到dest中;
  6. src如果是URL,同时dest是以斜杠结尾的目录,则会从URL推断出文件名然后将文件下载为dest中的对应文件。例如ADD example.com/foobar /,那么会从example.com/foobar 下载文件为 /foobar
  7. src如果是目录,则会复制目录里全部内容到dest
  8. src如果是常用压缩格式(gzipbzip等)的tar压缩包,那么ADD指令会解压这个tar压缩包,并且识别压缩格式仅以文件内容为准;
  9. 如果指定了多个src或者通配符匹配到了多个src,则dest必须是一个以斜杠结尾的目录;
  10. 如果dest不以斜杠结尾,则dest会被识别为文件,那么就会将src的内容写到dest中;
  11. 如果dest不存在,则会在路径中创建dest以及相关的目录。

2. COPY

COPY指令用于从src复制文件或目录,然后添加到目标地址dest的文件系统中。

COPY指令有如下两种使用格式。

  1. COPY [--chown=<user>:<group>] <src>... <dest>
  2. COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

上述的格式中相较于ADD少了 --checksum,是因为COPY不支持从URL下载文件,也就无需校验远端文件。

对于COPY指令,说明如下。

  1. 如果路径中有空格,请使用上述的第2种格式;
  2. --chown用于给添加内容指定所有权。其中usergroup都可以使用UIDGID来表示;
  3. src路径需要在当前构建上下文中。例如 ./learn-docker-1.0-SNAPSHOT.jar,则需要在当前目录下存在learn-docker-1.0-SNAPSHOT.jar文件,这是因为在docker build的第一步,就是将当前目录下的所有文件和子文件都上传到Docker并作为当前构建上下文(所以当前目录下的内容就称为当前构建上下文);
  4. src如果是目录,则会复制目录里全部内容到dest,但是目录本身不会被复制;
  5. 如果指定了多个src或者通配符匹配到了多个src,则dest必须是一个以斜杠结尾的目录;
  6. 如果dest不以斜杠结尾,则dest会被识别为文件,那么就会将src的内容写到dest中;
  7. 如果dest不存在,则会在路径中创建dest以及相关的目录。

关于ADD指令和COPY的区别,有如下两点。

  1. ADD指令支持从URL下载文件到目标路径,而COPY不支持;
  2. ADD指令会识别压缩文件并解压到目标路径中,而COPY不支持。

3. CMD

CMD指令用于指定容器被创建时所需要执行的命令。

CMD指令有如下三种格式。

  1. CMD ["executable","param1","param2"]exec格式,不会以shell形式来执行;
  2. CMD ["param1","param2"]。为ENTRYPOINT提供参数;
  3. CMD command param1 param2shell格式,以shell来执行command的命令。

对于CMD指令的说明如下。

  1. Dockerfile中只能有一条CMD指令,如果出现多条则以最后一条为准;
  2. CMD指令主要是用来为容器提供默认值(上述第一种和第二种格式),这些默认值中可以有可执行文件(第一种格式中的"executable"),也可以没有(第二种格式就没有可执行文件),如果没有可执行文件时,则需要再指定ENTRYPOINT指令;
  3. 如果CMD指令用于给ENTRYPOINT提供参数,那么CMD需要使用上述第二种格式,ENTRYPOINT也需要使用ENTRYPOINT ["executable", "param1", "param2"] 这种格式;
  4. 如果CMD使用shell格式,则命令将以 /bin/sh -c 'command param1 param2' 形式执行。

CMDENTRYPOINT有很大的相似性和关联,将在下面介绍完ENTRYPOINT后一起进行说明。

4. ENTRYPOINT

ENTRYPOINT指令用于为容器配置可执行文件(什么是可执行文件,topecho这种就是可执行文件)(官网的解释)。其实就是配置每次创建容器时需要执行的指令,和CMD较为相似。

ENTRYPOINT指令有如下两种格式。

  1. ENTRYPOINT ["executable", "param1", "param2"]。这种称为exec格式;
  2. ENTRYPOINT command param1 param2。这种称为shell格式。

ENTRYPOINT指令,有如下说明。

  1. docker run <image> <command line arguments>里面的command line arguments会作为exec格式的ENTRYPOINT的参数,并附加在最后面;
  2. docker run <image> <command line arguments>里面的command line arguments会覆盖CMD指令的所有内容;
  3. 如果要覆盖ENTRYPOINT,需要使用docker run --entrypoint,覆盖后是exec格式;
  4. shell格式的ENTRYPOINT不会使用任何command line arguments或者任何CMD指令提供的内容;
  5. shell格式的ENTRYPOINT会以 /bin/sh -c <ENTRYPOINT> 的形式执行,此时可执行文件不是容器中的PID为1的进程,PID不是1的进程将无法接收宿主机发送的SIGTERM信号,也就无法实现优雅退出(docker stop会从宿主机向容器发送SIGTERM信号),此时只能通过SIGKILL信号被强行退出(docker kill会从宿主机向容器发送SIGKILL信号);
  6. Dockerfile中只能有一条ENTRYPOINT指令,如果出现多条则以最后一条为准。

(下面将对官网上对于ENTRYPOINT的使用进行翻译)


可以使用ENTRYPOINT指令的exec格式来设置稳定的默认命令和参数,然后使用CMD指令的任何一种格式来设置可能更改的默认命令和参数。

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
复制代码

当运行容器时,可以看到top是唯一的进程(且PID是1)。

docker run -it --rm --name test top -H

top - 08:25:00 up  7:27,  0 users,  load average: 0.00, 0.01, 0.05
Threads:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.1 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:   2056668 total,  1616832 used,   439836 free,    99352 buffers
KiB Swap:  1441840 total,        0 used,  1441840 free.  1324440 cached Mem

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    1 root      20   0   19744   2336   2080 R  0.0  0.1   0:00.04 top
复制代码

要进一步验证,可以使用docker exec

docker exec -it test ps aux

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.6  0.1  19752  2352 ?        Ss+  08:24   0:00 top -b -H
root         7  0.0  0.1  15572  2164 ?        R+   08:25   0:00 ps aux
复制代码

此时可以使用docker stop test来优雅的停止top指令。

下面的Dockerfile展示了使用ENTRYPOINT在前台运行ApachePID是1)。

FROM debian:stable
RUN apt-get update && apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
复制代码

如果需要为可执行文件编写启动脚本,可以使用execgosu命令确保最终可执行文件接收到宿主机发送的信号。

#!/usr/bin/env bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"
复制代码

(本段话非翻译)上述是我们如果一定要基于一个脚本来启动我们的程序(可执行文件),并且还希望我们的程序的进程的PID是1,则Docker官方建议我们在脚本中最终启动程序时,要使用execgosu来启动程序。

  1. 因为脚本在运行的时候也是一个进程(会占用PID为1的进程号),所以使用exec可以使得我们程序的进程可以取代运行脚本的进程从而成为PID为1的进程;
  2. 如果运行程序时权限不够,通常可以使用sudo提升权限,但是使用sudo会创建sudo进程(会占用PID为1的进程号),而gosu不会创建新的进程(不会占用PID为1的进程号),所以建议使用gosu来提升权限。

(继续翻译)最后,如果需要在关闭进程时做一些额外的清理工作,那么需要启动脚本能够接收到宿主机发送的信号。

#!/bin/sh
trap "echo TRAPed signal" HUP INT QUIT TERM

# 在此处后台启动服务
/usr/sbin/apachectl start

echo "[hit enter key to exit] or run 'docker stop '"
read

# 在此处停止服务并进行清理工作
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"
复制代码

现在使用run -it --rm -p 80:80 --name test apache来运行容器,此时可以基于docker execdocker top来查看容器里进程情况。

docker exec -it test ps aux

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.0   4448   692 ?        Ss+  00:42   0:00 /bin/sh /run.sh 123 cmd cmd2
root        19  0.0  0.2  71304  4440 ?        Ss   00:42   0:00 /usr/sbin/apache2 -k start
www-data    20  0.2  0.2 360468  6004 ?        Sl   00:42   0:00 /usr/sbin/apache2 -k start
www-data    21  0.2  0.2 360468  6000 ?        Sl   00:42   0:00 /usr/sbin/apache2 -k start
root        81  0.0  0.1  15572  2140 ?        R+   00:44   0:00 ps aux
复制代码

docker top test

PID                 USER                COMMAND
10035               root                {run.sh} /bin/sh /run.sh 123 cmd cmd2
10054               root                /usr/sbin/apache2 -k start
10055               33                  /usr/sbin/apache2 -k start
10056               33                  /usr/sbin/apache2 -k start
复制代码

/usr/bin/time docker stop test

real	0m 0.27s
user	0m 0.03s
sys	0m 0.03s
复制代码

ENTRYPOINTshell格式中,可以将ENTRYPOINT指定为纯字符串,此时ENTRYPOINT将基于 /bin/sh -c来执行。这种情况可以使用shell的方式来进行shell环境变量替换(比如echo $HOME),并且不会使用任何CMDdocker run传递的命令行参数。为了确保docker stop能够正确的向任何长期运行的可执行文件传递SIGTERM信号,那么启动可执行文件时,需要用exec来启动。

FROM ubuntu
ENTRYPOINT exec top -b
复制代码

当基于上述Dockerfile构建的镜像来启动容器时,只能看到一个PID为1的进程。

docker run -it --rm --name test top

Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached
CPU:   5% usr   0% sys   0% nic  94% idle   0% io   0% irq   0% sirq
Load average: 0.08 0.03 0.05 2/98 6
  PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND
    1     0 root     R     3164   0%   0% top -b
复制代码

这个容器可以使用docker stop来优雅的退出。

/usr/bin/time docker stop test

test
real	0m 0.20s
user	0m 0.02s
sys	0m 0.04s
复制代码

如果没有在ENTRYPOINT的开头添加exec,就像下面这样。

FROM ubuntu
ENTRYPOINT top -b
CMD -- --ignored-param1
复制代码

当基于上述Dockerfile构建的镜像来启动容器时,显示如下。

docker run -it --name test top --ignored-param2

top - 13:58:24 up 17 min,  0 users,  load average: 0.00, 0.00, 0.00
Tasks:   2 total,   1 running,   1 sleeping,   0 stopped,   0 zombie
%Cpu(s): 16.7 us, 33.3 sy,  0.0 ni, 50.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   1990.8 total,   1354.6 free,    231.4 used,    404.7 buff/cache
MiB Swap:   1024.0 total,   1024.0 free,      0.0 used.   1639.8 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0    2612    604    536 S   0.0   0.0   0:00.02 sh
    6 root      20   0    5956   3188   2768 R   0.0   0.2   0:00.00 top
复制代码

可以发现可执行文件topPID不是1,PID为1的进程是sh

如果此时执行docker stop test,那么容器将无法优雅退出(top无法接收到SIGTERM指令),最终在超时后docker stop被迫发送SIGKILL指令来强制终止top

docker exec -it test ps waux

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.4  0.0   2612   604 pts/0    Ss+  13:58   0:00 /bin/sh -c top -b --ignored-param2
root         6  0.0  0.1   5956  3188 pts/0    S+   13:58   0:00 top -b
root         7  0.0  0.1   5884  2816 pts/1    Rs+  13:58   0:00 ps waux
复制代码

/usr/bin/time docker stop test

test
real	0m 10.19s
user	0m 0.04s
sys	0m 0.03s
复制代码

(翻译结束,感觉官网写得很清晰了)


现在是不是对CMDENTRYPOINT有了较为清晰的认识,下面是对CMDENTRYPOINT的(说人话的)总结。

  1. CMDENTRYPOINT指令都可以定义运行容器时需要执行的命令;
  2. CMD会被docker run <arguments>指定的arguments内容覆盖,ENTRYPOINT不会;
  3. 如果想覆盖ENTRYPOINT,需要通过docker run --entrypoint来实现,此时覆盖后的ENTRYPOINT默认是exec格式;
  4. CMDENTRYPOINTDockerfile中只能有一个生效,并且都是最后一个生效;
  5. 当要在启动容器时通过CMDENTRYPOINT直接运行一个程序,使用exec格式能让启动的程序的进程PID是1;
  6. 当要在启动容器时通过CMDENTRYPOINT直接运行一个程序,使用shell格式会导致启动的程序的进程PID不是1,但是也有例外,在shell格式中也可以使用exec,例如ENTRYPOINT exec top -b,此时效果和exec格式的ENTRYPOINT ["top" "-b"] 效果是一样的;
  7. 如果是通过CMD或者ENTRYPOINT来运行一个脚本,然后在脚本中来启动我们的程序,那么脚本中启动的地方需要使用execgosu,才能确保我们的程序的进程PID是1;
  8. CMDENTRYPOINTDockerfile中同时存在时,CMD的作用就是给ENTRYPOINT提供参数,此时CMDENTRYPOINT都应该是exec格式;
  9. CMDENTRYPOINTDockerfile中同时存在时,如果ENTRYPOINTshell格式,那么CMD的全部内容会被忽略。

最后是CMD的三种格式和ENTRYPOINT的两种格式交叉使用时的一个测试结果。


Dockerfile如下所示(v1)。

FROM ubuntu:latest
CMD ["cmd"]
ENTRYPOINT echo entrypoint
复制代码

启动时打印如下。

entrypoint
复制代码

启动命令如下所示。

/bin/sh -c 'echo entrypoint' cmd
复制代码

Dockerfile如下所示(v2)。

FROM ubuntu:latest
CMD ["cmd"]
ENTRYPOINT ["echo","entrypoint"]
复制代码

启动时打印如下。

entrypoint cmd
复制代码

启动命令如下所示。

echo entrypoint cmd
复制代码

Dockerfile如下所示(v3)。

FROM ubuntu:latest
CMD ["echo","cmd"]
ENTRYPOINT echo entrypoint
复制代码

启动时打印如下。

entrypoint
复制代码

启动命令如下所示。

/bin/sh -c 'echo entrypoint' echo cmd
复制代码

Dockerfile如下所示(v4)。

FROM ubuntu:latest
CMD ["echo","cmd"]
ENTRYPOINT ["echo","entrypoint"]
复制代码

启动时打印如下。

entrypoint echo cmd
复制代码

启动命令如下所示。

echo entrypoint echo cmd
复制代码

Dockerfile如下所示(v5)。

FROM ubuntu:latest
CMD echo cmd
ENTRYPOINT echo entrypoint
复制代码

启动时打印如下。

entrypoint
复制代码

启动命令如下所示。

/bin/sh -c 'echo entrypoint' /bin/sh -c 'echo cmd'
复制代码

Dockerfile如下所示(v6)。

FROM ubuntu:latest
CMD echo cmd
ENTRYPOINT ["echo","entrypoint"]
复制代码

启动时打印如下。

entrypoint /bin/sh -c echo cmd
复制代码

启动命令如下所示。

echo entrypoint /bin/sh -c 'echo cmd'
复制代码

那么,可以CMDENTRYPOINT都没有吗,自然是不可以的。

5. EXPOSE

EXPOSE指令用于告诉Docker容器在运行时需要监听的网络端口,默认监听TCP端口。

EXPOSE指令实际上没有为Docker容器发布端口,该指令更像是一种约定,镜像构建方通过EXPOSE约定基于当前镜像构建的容器应该发布哪个端口,这份约定保存在镜像的元数据中,那么容器创建方就可以通过docker run -p hostPort:containerPort来进行容器端口的发布和与宿主机端口的映射,或者通过docker run -P来将约定的端口发布并且与宿主机上49000-49900之间的随机一个端口映射。

那么现在对EXPOSE做一个总结。

  1. EXPOSE用于提供容器运行时需要监听的端口的元数据信息,并不是真正的为容器发布端口,举个例子,EXPOSE 8080,首先并没有为容器发布8080端口,也并不保证容器中运行的程序是否在监听容器的8080端口;
  2. docker -p hostPort:containerPort其实是比较粗暴的,就是单纯的先发布容器的containerPort端口(如果容器的containerPort端口已经被发布则这一步不做),然后完成容器的containerPort端口与宿主机上hostPort端口的绑定;
  3. docker -P会去检测元数据并拿到被EXPOSE约定的容器端口,然后为容器发布这个端口,然后将容器发布的这个端口与宿主机上49000-49900之间的随机一个端口做映射。

6. WORKDIR

CD:今天我画了一个妆,你觉得我好看吗。

WORKDIR指令用于为Dockerfile中任何RUNCOPYADDCMDENTRYPOINT指令设置工作目录(进入这个目录),并且当WORKDIR指定的目录不存在时会创建指定的目录。

WORKDIR可以在一份Dockerfile中使用多次,比如。

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
复制代码

那么最终pwd的输出结果是**/a/b/c**,也就是RUN指令执行时的目录是**/a/b/c**。

WORKDIR在设置工作目录时,可以使用在Dockerfile中通过ENV设置的环境变量。

ENV DIRPATH=/path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
复制代码

那么最终pwd的输出结果是/path/$DIRNAME

7. ENV

ENV格式如下。

ENV = ...
复制代码

ENV指令用来设置环境变量,ENV指令设置的环境变量,可用在镜像构建阶段的后续的所有指令中例如RUNWORKDIR等指令,并且最终会作为容器中的环境变量。

注意,关于设置的环境变量,有如下注意点。

  1. 引号如果没有被转义,那么会被删除;
  2. docker run --env <key>=<value>可以覆盖ENV设置的环境变量;
  3. ENV设置的环境变量会作用于构建阶段和最终的容器中,可能会导致一些副作用,如果只是希望设置作用于构建阶段的环境变量,可以使用ARG指令;
  4. ENV设置的环境变量中,<value>中的空格要么存在于引号(单双引号)中,要么被转义。

下面是针对ENV指令的一些测试。


Dockerfile如下所示(v1)。

FROM ubuntu:latest
ENV TEST_MSG="HolloGo"
ENTRYPOINT echo $TEST_MSG
复制代码

启动打印如下。

HolloGo
复制代码

也就是没有加转义符的引号会被删除。


Dockerfile如下所示(v2)。

FROM ubuntu:latest
ENV TEST_MSG=\"HolloGo\"
ENTRYPOINT echo $TEST_MSG
复制代码

启动打印如下。

"HolloGo"
复制代码

加了转义符的引号才会被保留。


Dockerfile如下所示(v3)。

FROM ubuntu:latest
ENV TEST_MSG=Hello\ Go\ Lang
ENTRYPOINT echo $TEST_MSG
复制代码

启动打印如下。

Hello Go Lang
复制代码

空格需要使用转义符转义。


Dockerfile如下所示(v4)。

FROM ubuntu:latest
ENV TEST_MSG=Hello Go Lang
ENTRYPOINT echo $TEST_MSG
复制代码

上述方式是非法的,空格要么在双引号中间,要么使用转义符进行转义。


Dockerfile如下所示(v5)。

FROM ubuntu:latest
ENV TEST_MSG='Hello Go Lang'
ENTRYPOINT echo $TEST_MSG
复制代码

启动打印如下。

Hello Go Lang
复制代码

单引号和双引号一样,会被删除。


Dockerfile如下所示(v6v7v8v9)。

FROM ubuntu:latest
ENV TEST_MSG=\'Hello Go Lang\'
ENTRYPOINT echo $TEST_MSG
复制代码
FROM ubuntu:latest
ENV TEST_MSG=\"Hello Go Lang\"
ENTRYPOINT echo $TEST_MSG
复制代码

上述方式都是非法的。

FROM ubuntu:latest
ENV TEST_MSG=\'Hello\ Go\ Lang\'
ENTRYPOINT echo $TEST_MSG
复制代码

启动打印如下。

'Hello Go Lang'
复制代码
FROM ubuntu:latest
ENV TEST_MSG=\"Hello\ Go\ Lang\"
ENTRYPOINT echo $TEST_MSG
复制代码

启动打印如下。

"Hello Go Lang"
复制代码

8. VOLUME

VOLUME用于设置挂载点。

首先准备如下的Dockerfile

FROM ubuntu:latest
VOLUME ["/data"]
ENTRYPOINT echo "Hello Go Lang"
复制代码

启动容器,然后使用docker inspect来查看容器。

上述中Source表示宿主机上某一个路径,Destination表示容器中的挂载点。

9. USER

USER指令用于设置用户名(UID)和可选的用户组(GID),用作容器的主进程的用户名和用户组,以及用作当前构建阶段后续的RUNCMDENTRYPOINT指令执行时的用户。

五. 容器Contianer

1. 容器存储分层结构

当基于一个镜像,运行一个容器时,这个容器的存储分层结构如下。

创建容器时,镜像会作为容器的镜像层(基础层),是只读的,然后在镜像层上会添加一层可写层,称为容器层,在容器运行时,对容器所作的更改(增删改文件)都会写入到容器层。

对于容器层,有如下注意点。

  1. 容器层是可写的。在容器中做的文件增删改都会写入到容器层;
  2. 容器层在容器删除时也会被删除。容器层是不做持久化的,容器销毁,容器层也会一并被销毁,相应的对容器的修改也会删除,但是镜像层可以保持不变,不会随容器销毁而销毁;
  3. 每个容器都有自己的容器层。基于相同镜像构建的容器会共享同一个底层镜像,同时每个容器自己的变化会写入到容器自己的容器层,因此多个容器可以共享同一个底层镜像,同时也拥有自己的状态。

2. 容器的大小

要查看运行中的容器的大小,可以使用docker ps -s命令,一个示例如下。

说明如下。

  1. SIZE。表示容器的容器层的大小;
  2. virtual。表示镜像层加上容器层的大小。

也就是多个基于相同镜像的容器占用的磁盘实际大小是远小于这些镜像的virtual大小之和的。

3. CoW写时复制策略

关于CoW写时复制策略的概念如下所示。

如果一个文件在镜像的较底层,而镜像的较高层或者容器层要读取这个文件,则直接读取这个文件即可。但如果镜像的较高层或者容器层要修改这个文件(构建镜像或者容器运行的时候),则需要将这个文件从镜像较低层拷贝到当前层再做修改。

已知在容器运行时,在镜像层之上会有一个容器层。还已知容器对文件系统所作的更改会存储在容器层,容器未更改的文件则不会拷贝到容器层,这样的策略使得容器层非常小,非常薄。

当容器要修改容器中的文件时(无论容器层还是镜像层中的文件),整体的一个执行策略如下。

  1. 修改的文件在容器层中,则直接修改;
  2. 从镜像层的上层到下层依次遍历,直到找到需要修改的文件(这一步操作的结果会添加缓存,用于下次遍历提速);
  3. 对遍历到的第一个文件执行复制操作,并复制到容器层;
  4. 后续这个文件的用户视角只会是容器层中修改后的文件。

特别注意:如果容器存在大量的写操作,则尽量不要写在容器(容器层)中,而应该为容器挂载持久化卷并写在持久化卷中。

4. docker commit

将容器的容器层中的文件更改作为镜像新的一层然后生成新镜像。注意点如下。

  1. 镜像新的一层不包含挂载的持久化卷里面的数据;
  2. docker commit时会暂停容器中的进程。

六. 其他内容

1. 构建缓存的使用

在构建镜像时,中间镜像是可以被缓存和复用的,规则如下。

  1. ADDCOPY指令。如果某一个中间镜像(A镜像)已经是被缓存的,而此时正要在A镜像基础上执行下一条指令(m指令),那么会遍历A镜像的所有子镜像,以查看这些子镜像中是否有一个子镜像是基于m指令构建的;
  2. ADDCOPY指令。会基于父镜像的所有文件和本次指令添加的文件计算checksum(校验和),然后和当前所有中间镜像的checksum做比较,有相等的则命中缓存的镜像;
  3. 一旦某一层没有命中缓存,那么从这一层开始,后续所有指令都不会使用缓存。

2. RUN指令使用建议

  1. 复杂的RUN语句需要拆分到多行,并用反斜杠 \ 分隔,以提升RUN语句的可读性;
  2. 需要将apt-get updateapt-get install组合到同一个RUN语句中(缓存破坏技术)。
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
复制代码
FROM ubuntu:18.04
RUN apt-get update	# 这里的apt-get update会去使用缓存里的中间镜像,导致apt-get没有被更新
RUN apt-get install -y curl nginx
复制代码
  1. 如下是一个apt-get的最佳编写实践
RUN apt-get update && apt-get install -y \	# apt-get update和apt-get install组合
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \		# 指定固定版本
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*		# 清理apt缓存,可以减小层大小
复制代码

3. 多阶段构建

假如要构建一个GO语言程序的镜像,并且也不想将GO源代码打到镜像中,最终还希望镜像大小尽可能的小。

要实现上述要求,有一种做法如下。

先在标准的容器环境中完成GO的编译,build.Dockerfile如下所示。

FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go ./
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 go build -a -installsuffix cgo -o app .
复制代码

编译得到GO可执行文件后,最终基于alpine的镜像的Dockerfile如下所示。

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app ./
CMD ["./app"]
复制代码

同时还需要一个shell脚本来完成将编译得到的GO可执行文件从容器中拷贝到宿主机上,然后再执行最终镜像的Dockerfile,脚本如下所示。

#!/bin/sh
echo Building alexellis2/href-counter:build
docker build -t alexellis2/href-counter:build . -f build.Dockerfile

docker container create --name extract alexellis2/href-counter:build  
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker container rm -f extract

echo Building alexellis2/href-counter:latest
docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app
复制代码

上述做法是繁琐且容易出错的,如果使用docker的多阶段构建(Multi-stage builds),那么可以简化如下。

FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go ./
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]  
复制代码

也就是允许在一个Dockerfile中使用多个FROM语句,每个FROM语句都可以使用一个基础镜像并开启构建的一个新阶段,最为有用的就是可以方便的将一个构建阶段的产出内容复制到另外一个构建阶段(未复制的内容全部丢弃)。

4. K8S command和args

  1. 如果设置了K8S commandK8S args,则容器启动时会使用K8S commandK8S args
  2. 如果没设置K8S commandK8S args,则容器启动时使用Docker镜像中的启动命令;
  3. 如果设置了K8S command,没设置K8S args,则容器启动时只会使用K8S command
  4. 如果没设置K8S command,设置了K8S args,则镜像如果有设置ENTRYPOINT,则K8S args作为ENTRYPOINT的参数(并覆盖CMD),如果镜像只设置了CMD,则K8S args覆盖CMD作为启动命令。

(本文没有总结)

如果觉得本篇文章对你有帮助,求求你点个赞,加个收藏最后再点个关注吧。创作不易,感谢支持!

链接:
https://juejin.cn/post/7225269942756016185

相关推荐

利用navicat将postgresql转为mysql

导航"拿来主义"吃得亏自己动手,丰衣足食...

Navicat的详细教程「偷偷收藏」(navicatlite)

Navicat是一套快速、可靠并价格适宜的数据库管理工具,适用于三种平台:Windows、macOS及Linux。可以用来对本机或远程的MySQL、SQLServer、SQLite、...

Linux系统安装SQL Server数据库(linux安装数据库命令)

一、官方说明...

Navicat推出免费数据库管理软件Premium Lite

IT之家6月26日消息,Navicat推出一款免费的数据库管理开发工具——NavicatPremiumLite,针对入门级用户,支持基础的数据库管理和协同合作功能。▲Navicat...

Docker安装部署Oracle/Sql Server

一、Docker安装Oracle12cOracle简介...

Docker安装MS SQL Server并使用Navicat远程连接

...

Web性能的计算方式与优化方案(二)

通过前面《...

网络入侵检测系统之Suricata(十四)——匹配流程

其实规则的匹配流程和加载流程是强相关的,你如何组织规则那么就会采用该种数据结构去匹配,例如你用radixtree组织海量ip规则,那么匹配的时候也是采用bittest确定前缀节点,然后逐一左右子树...

使用deepseek写一个图片转换代码(deepnode处理图片)

写一个photoshop代码,要求:可以将文件夹里面的图片都处理成CMYK模式。软件版本:photoshop2022,然后生成的代码如下://Photoshop2022CMYK批量转换专业版脚...

AI助力AUTOCAD,生成LSP插件(ai里面cad插件怎么使用)

以下是用AI生成的,用AUTOLISP语言编写的cad插件,分享给大家:一、将单线偏移为双线;;;;;;;;;;;;;;;;;;;;;;单线变双线...

Core Audio音频基础概述(core 音乐)

1、CoreAudioCoreAudio提供了数字音频服务为iOS与OSX,它提供了一系列框架去处理音频....

BlazorUI 组件库——反馈与弹层 (1)

组件是前端的基础。组件库也是前端框架的核心中的重点。组件库中有一个重要的板块:反馈与弹层!反馈与弹层在组件形态上,与Button、Input类等嵌入界面的组件有所不同,通常以层的形式出现。本篇文章...

怎样创建一个Xcode插件(xcode如何新建一个main.c)

译者:@yohunl译者注:原文使用的是xcode6.3.2,我翻译的时候,使用的是xcode7.2.1,经过验证,本部分中说的依然是有效的.在文中你可以学习到一系列的技能,非常值得一看.这些技能不单...

让SSL/TLS协议流行起来:深度解读SSL/TLS实现1

一前言SSL/TLS协议是网络安全通信的重要基石,本系列将简单介绍SSL/TLS协议,主要关注SSL/TLS协议的安全性,特别是SSL规范的正确实现。本系列的文章大体分为3个部分:SSL/TLS协...

社交软件开发6-客户端开发-ios端开发验证登陆部分

欢迎订阅我的头条号:一点热上一节说到,Android客户端的开发,主要是编写了,如何使用Androidstudio如何创建一个Android项目,已经使用gradle来加载第三方库,并且使用了异步...

取消回复欢迎 发表评论: