一、Docker简介
1.1、Docker是什么:
- 使用最广泛的开源容器引擎
- 一种操作系统级的虚拟化技术
- 依赖于Linux内核特性:Namespace(资源隔离)和Cgroups(资源限制)
- 一个简单的应用程序打包工具
1.2、Docker设计目标:
- 提供简单的应用程序打包工具
- 开发人员和运维人员职责逻辑分离
- 多环境保持一
1.3、Docker基本组成:
- Docker Client:客户端
- Ddocker Daemon:守护进程
- Docker Images:镜像
- Docker Container:容器
- Docker Registry:镜像仓库
各种关系如下图:
1.4、Docker既然是虚拟化技术,那不免和VM进行一番比较,话不多说,直接看图吧:
Docker容器技术是一个与传统的虚拟化技术有些本质上的差别,传统的虚拟化技术,是站硬件物理资源的基础上,虚拟出多个OS,然后在OS的基础上构建相对独立的程序运行环境,而Docker则是在OS的基础上进行虚拟,所以,Docker轻量很多,因此其资源占用、性能消耗相比传统虚拟化都有很大的优势。
Docker容器很快,启动和停止可以在秒级实现,比传统的虚拟化技术要快很多,docker核心解决的问题是利用容器来实现类似VM的功能,从而节省更多的硬件资源,docker容器除了运行其中的应用之外,基本不消耗额外的系统资源,从而在保证性能的同时,减小系统开销,同时,它还可以达到“一次封装,到处运行”的目的。
1.5、Docker应用场景:
- 应用程序打包和发布
- 应用程序隔离
- 持续集成
- 部署微服务
- 快速搭建测试环境
- 提供PaaS产品(平台即服务)
二、Docker三个容器隔离核心技术:Namespace、Cgroups、Rootfs
2.1、Namespace:Linux 容器中用来实现“隔离”的技术手段
虚拟化的技术就是用来解决宿主机与虚拟机之间的耦合问题(简称“解耦”),传统虚拟化技术是属于完全解耦的,而docker这种虚拟化技术是属于半解耦的。
- 耦合:就是指两个或两个以上的体系或两种运动形式间通过相互作用而彼此影响以至联合起来的现象;
- 解耦:接触耦合、冲突现象;
Docker是如何解耦的呢?这就需要使用到这里所说的:Namespace(命名空间)。
Namespace(命名空间):是Linux为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。
Namespace(命名空间)在docker中主要实现了六项隔离,如图:
Docker通过使用Namespace(命名空间)这项技术实现了容器与容器之间、容器与docker host之间的隔离。
当Docker创建一个容器时,它会创建新的以上六种NameSpace的实例,然后把容器中的所有进程放到这些NameSpace之中,使得容器这个父进程只对自己的子进程有感知,而对于宿主机其他进程一无所知,从而产生一种它就是一个独立的系统的“错觉”。
如果docker 宿主机是centos系统,运行docker容器时,容器也是centos 系统,所必需的目录、文件就是通过docker宿主机进行软连接提供的,包括宿主机的内核;但如果运行的docker容器是Ubuntu系统,其中目录、文件与centos 系统始终是会有一些区别的,那么这就需要使用到——Busybox(欺骗层)。
如果需要使用虚拟机来部署一些服务时,这些服务对内核版本有要求,那么就不太适合使用docker这种虚拟化技术,建议使用KVM等虚拟化技术。
docker这种服务本身并不占用端口,只是保持后台运行。
2.2、Cgroup:控制程序对资源的占用
Cgroup的具体作用如下:
- 限制资源的使用:Cgroup可以对进程组使用的资源总额进行限制;
- 优先级控制:通过分配CPU时间片数量及磁盘IO带宽大小,实际上就是相当于控制子进程运行的优先级。
- 资源统计:Cgroup可以统计系统资源使用量,比如CPU使用时间,内存使用量等。可用于按量计费。
- 进程控制:恢复执行进程;
使用Cgroup,我们可以更具体地控制对系统资源的分配、优先顺序、拒绝、管理和监控。可更好地根据任务和用户分配硬件资源,提高总体的效率,这样可以在docker容器中的服务受到外部干扰时,可以将其限制在容器之中,而不会影响宿主机或其他容器的运行,提高了安全性。
那么,docker是如何来定义容器使用的资源呢?
(1)Cgroup的应用举例
1、对CPU进行限制:
- 先来看看其不进行CPU限制时,宿主机和Docker它是什么样子的?
1 root@ubuntu:~# cat /sys/fs/cgroup/cpu/cpu.shares # 查看宿主机CPU的权重 2 1024 3 root@ubuntu:~# docker run -it --rm --name mybsx busybox 4 / # cat /sys/fs/cgroup/cpu/cpu.shares 5 1024 # 可以看到和宿主机是一样的:Docker容器默认的CPU权重也是1024 6 / # exit
- 再看基于busybox镜像运行一个容器,要求CPU使用权重为512:
1 root@ubuntu:~# docker run -it --rm --name mybusybox1 -c 512 busybox 2 / # cat /sys/fs/cgroup/cpu/cpu.shares 3 512 # 可以看到效果了 4 / # exit 5 root@ubuntu:~# 6 # 基于centos镜像运行一个名为test1的容易,其CPU使用的权重为512 7 # 设置方法相对来说,十分简单,就是添加了一个“-c”的选项而已!
- 其它部分相关限制参数如下:
1 --cpuset-cpus="" 允许使用的CPU集,值可以为0-3或0,3 2 -c,--cpu-shares=0 CPU权重 3 --cpu-period=0 CPU CFS周期,范围从100ms~1s,即[1000, 1000000] 4 --cpu-quota=0 CPU CFS配额,必须不小于1ms,即大于等于1000 5 --cpuset-mems="" 允许在容器上执行的内存节点,只对NUMA系统有效 6 --cpus=1.5 指定容器可以使用的CPU资源量。如果设置--cpus="1.5",则容器最多可以使用1.5个CPU。 7 --cap-add=sys_nice 开启Docker容器CAP_SYS_NICE功能,允许容器引发进程良好值,设置实时调度策略,设置CPU亲和性以及其他操作。 8 --cpu-rt-runtime=value 容器可以在Docker守护程序的实时调度程序周期内以实时优先级运行的最大微秒数,需要开启CAP_SYS_NICE功能 。 9 --ulimit rtprio=99 容器允许的最大实时优先级,需要开启CAP_SYS_NICE功能。
2、基于busybox镜像运行一个容器,要求MEM为200M,MEM-SWAP为200M:
- 以下是宿主机和Docker容器内存、交换空间的未限制的情况:
1 # 物理情况: 2 root@ubuntu:~# cat /sys/fs/cgroup/memory/memory.limit_in_bytes 3 9223372036854771712 4 root@ubuntu:~# cat /sys/fs/cgroup/memory/memory.memsw.limit_in_bytes 5 9223372036854771712 6 root@ubuntu:~# 7 # 容器情况: 8 root@ubuntu:~# docker container run -it --rm --name b1 busybox 9 / # cat /sys/fs/cgroup/memory/memory.limit_in_bytes 10 9223372036854771712 11 / # cat /sys/fs/cgroup/memory/memory.memsw.limit_in_bytes 12 9223372036854771712 13 / # exit 14 root@ubuntu:~#
- 以下是对容器中的内存和交互空间进行限制:
1 root@ubuntu:~# docker container run -it --rm --name b1 -m200M --memory-swap 400M busybox 2 / # cat /sys/fs/cgroup/memory/memory.limit_in_bytes 3 209715200 # 内存限制:单位是——bytes 4 / # cat /sys/fs/cgroup/memory/memory.memsw.limit_in_bytes 5 419430400 # 交换空间限制:单位是——bytes 6 / #
3、基于busybox镜像运行一个容器,要求写入速度为40M:
- 默认运行一个容器,不进行限制:
1 root@ubuntu:~# docker container run -it --rm --name b1 busybox 2 / # time dd if=/dev/zero of=a.txt bs=1M count=200 oflag=direct 3 200+0 records in 4 200+0 records out 5 209715200 bytes (200.0MB) copied, 0.175974 seconds, 1.1GB/s 6 real 0m 0.17s 7 user 0m 0.00s 8 sys 0m 0.15s 9 / #
- 可以看到,如果不对其进行限制,那么会使用宿主机最大的写速度,那么怎么限制呢?
1 root@ubuntu:~# docker container run -it --rm --name b1 --device-write-bps /dev/sda:40MB busybox 2 / # time dd if=/dev/zero of=a.txt bs=1M count=200 oflag=direct 3 200+0 records in 4 200+0 records out 5 209715200 bytes (200.0MB) copied, 4.933736 seconds, 40.5MB/s # 写入速度被限制在了40MB左右 6 real 0m 4.93s 7 user 0m 0.00s 8 sys 0m 0.14s 9 / #
三、Dockerfile文件详解
Dockerfiile是什么、其基本结构是什么,已经各种常用的指令有哪些(包括这些指令使用方法),请看如下地址:https://www.cnblogs.com/panwenbin-logs/p/8007348.html
我也应用其中一张图片说明情况:
下面我主要想谈谈 :Dockerfile 中的 CMD 与 ENTRYPOINT 两条指令
CMD 和 ENTRYPOINT 指令都是用来指定容器启动时运行的命令。
单从功能上来看,这两个命令几乎是重复的。单独使用其中的一个就可以实现绝大多数的用例。但是既然 doker 同时提供了它们,为了在使用中不至于混淆,本文试图把它们的用法理清楚。
exec 模式和 shell 模式
CMD 和 ENTRYPOINT 指令都支持 exec 模式和 shell 模式的写法,所以要理解 CMD 和 ENTRYPOINT 指令的用法,就得先区分 exec 模式和 shell 模式。这两种模式主要用来指定容器中的不同进程为 1 号进程。了解 linux 的朋友应该清楚 1 号进程在系统中的重要地位。笔者也在《在 docker 容器中捕获信号》一文中介绍过 1 号进程对容器中信号处理的重要性,感兴趣的朋友可以移步这里进行了解。下面我们通过 CMD 指令来学习 exec 模式和 shell 模式的特点。
exec 模式
使用 exec 模式时,容器中的任务进程就是容器内的 1 号进程,看下面的例子:
1 root@ubuntu:~/test01# cat Dockerfile 2 FROM busybox 3 CMD [ "top" ] 4 root@ubuntu:~/test01# 5 root@ubuntu:~/test01# docker build -t test01 . 6 Sending build context to Docker daemon 2.048kB 7 Step 1/2 : FROM busybox 8 ---> 42b97d3c2ae9 9 Step 2/2 : CMD [ "top" ] 10 ---> Running in 3abe67d620dd 11 Removing intermediate container 3abe67d620dd 12 ---> 570d8d5676ad 13 Successfully built 570d8d5676ad 14 Successfully tagged test01:latest 15 root@ubuntu:~/test01# 16 root@ubuntu:~/test01# docker run -d --name r1 test01 17 724ca03f8840521c76a1a5cc435cb5bc55f49b19986810ff241a83fd4c9bab26 18 root@ubuntu:~/test01# docker ps 19 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 20 724ca03f8840 test01 "top" 4 seconds ago Up 3 seconds r1 21 root@ubuntu:~/test01# docker exec -it r1 sh 22 / # ps 23 PID USER TIME COMMAND 24 1 root 0:00 top # 这里进程为:1 25 6 root 0:00 sh 26 11 root 0:00 ps 27 / #
从图中我们看到运行 top 命令的进程 ID 为 1。
exec 模式是建议的使用模式,因为当运行任务的进程作为容器中的 1 号进程时,我们可以通过 docker 的 stop 命令优雅的结束容器(详情请参考《在 docker 容器中捕获信号》)。
exec 模式的特点是不会通过 shell 执行相关的命令,所以像 $HOME 这样的环境变量是取不到的:
1 FROM busybox 2 CMD [ "echo", "$HOME" ]
把上面的代码保存到 test1 目录的 Dockerfile 中,然后进入 test1 目录构建镜像并启动一个容器:
1 # docker build --no-cache -t test1 . 2 # docker run --rm test1
通过 exec 模式执行 shell 可以获得环境变量:(下面我也不一一验证了,贴一下原作者的图文吧)
FROM ubuntu CMD [ "sh", "-c", "echo $HOME" ]
把上面的代码保存到 test1 目录的 Dockerfile 中,然后进入 test1 目录构建镜像并启动一个容器:
1 # docker build --no-cache -t test1 . 2 # docker run --rm test1
这次正确取到了 $HOME 环境变量的值。
shell 模式
使用 shell 模式时,docker 会以 /bin/sh -c "task command" 的方式执行任务命令。也就是说容器中的 1 号进程不是任务进程而是 bash 进程,看下面的例子:
FROM ubuntu
CMD top
把上面的代码保存到 test2 目录的 Dockerfile 中,然后进入 test2 目录构建镜像并启动一个容器:
1 # docker build -t test2 . 2 # docker run -itd --name testcon2 test2
然后查看容器中的进程 ID:
1 # docker exec testcon2 ps aux
1 号进程执行的命令居然是 /bin/sh -c top。而我们指定的 top 命令的进程 ID 为 7。这是由 docker 内部决定的,目的是让我们执行的命令或者脚本可以取到环境变量。
CMD 指令
CMD 指令的目的是:为容器提供默认的执行命令。
CMD 指令有三种使用方式,其中的一种是为 ENTRYPOINT 提供默认的参数:
CMD ["param1","param2"]
另外两种使用方式分别是 exec 模式和 shell 模式:
CMD ["executable","param1","param2"] // 这是 exec 模式的写法,注意需要使用双引号。
CMD command param1 param2 // 这是 shell 模式的写法。
注意命令行参数可以覆盖 CMD 指令的设置,但是只能是重写,却不能给 CMD 中的命令通过命令行传递参数。
一般的镜像都会提供容器启动时的默认命令,但是有些场景中用户并不想执行默认的命令。用户可以通过命令行参数的方式覆盖 CMD 指令提供的默认命令。比如通过下面命令创建的镜像:
FROM ubuntu CMD [ "top" ]
在启动容器时我们通过命令行指定参数 ps aux 覆盖默认的 top 命令:
从上图可以看到,命令行上指定的 ps aux 命令覆盖了 Dockerfile 中的 CMD [ "top" ]。实际上,命令行上的命令同样会覆盖 shell 模式的 CMD 指令。
ENTRYPOINT 指令
ENTRYPOINT 指令的目的也是为容器指定默认执行的任务。
ENTRYPOINT 指令有两种使用方式,就是我们前面介绍的 exec 模式和 shell 模式:
ENTRYPOINT ["executable", "param1", "param2"] // 这是 exec 模式的写法,注意需要使用双引号。
ENTRYPOINT command param1 param2 // 这是 shell 模式的写法。
exec 模式和 shell 模式的基本用法和 CMD 指令是一样的,下面我们介绍一些比较特殊的用法。
指定 ENTRYPOINT 指令为 exec 模式时,命令行上指定的参数会作为参数添加到 ENTRYPOINT 指定命令的参数列表中。用下面的代码构建镜像 test1:
FROM ubuntu ENTRYPOINT [ "top", "-b" ]
运行下面的命令:
1 # docker run --rm test1 -c
我们在命令行上添加的参数被追加到了 top 命令的参数列表中。
由 CMD 指令指定默认的可选参数:
FROM ubuntu ENTRYPOINT [ "top", "-b" ] CMD [ "-c" ]
使用这段代码构建镜像 test2 并不带命令行参数启动容器:
# docker run --rm test2
这时容器中运行的命令为:top -b -c。
如果我们指定命令行参数:
# docker run --rm test2 -n 1
-n 1 会覆盖 通过 CMD [ "-c" ] 指定的参数,容器执行的命令为:top -b -n 1
注意上图的输出显示 -c 参数被覆盖了。
指定 ENTRYPOINT 指令为 shell 模式时,会完全忽略命令行参数:
FROM ubuntu
ENTRYPOINT echo $HOME
把上面的代码编译成镜像 test2,分别不带命令行参数和使用命令行参数 ls 执行命令:
我们看到 ls 命令没有被执行,这说明命令行参数被 ENTRYPOINT 指令的 shell 模式忽略了。
覆盖默认的 ENTRYPOINT 指令:
ENTRYPOINT 指令也是可以被命令行覆盖的,只不过不是默认的命令行参数,而是需要显式的指定 --entrypoint 参数。比如我们通过下面的方式覆盖上面镜像中的 echo $HOME 命令:
# docker run --rm --entrypoint hostname test2
Dockerfile 中至少要有一个
如果镜像中既没有指定 CMD 也没有指定 ENTRYPOINT 那么在启动容器时会报错。这不算是什么问题,因为现在能见到的绝大多数镜像都默认添加了 CMD 或 ENTRYPOINT 指令。
指定任意一个,效果差不多
从结果上看,CMD 和 ENTRYPOINT 是一样的,我们可以通过它们实现相同的目的。下面我们分别用 CMD 和 ENTRYPOINT 设置 top -b 命令,然后观察容器运行时的 metadata 信息:
或者:
虽然实现方式不同,但最终容器运行的命令是一样的。
同时使用 CMD 和 ENTRYPOINT 的情况
对于 CMD 和 ENTRYPOINT 的设计而言,多数情况下它们应该是单独使用的。当然,有一个例外是 CMD 为 ENTRYPOINT 提供默认的可选参数。
我们大概可以总结出下面几条规律:
• 如果 ENTRYPOINT 使用了 shell 模式,CMD 指令会被忽略。
• 如果 ENTRYPOINT 使用了 exec 模式,CMD 指定的内容被追加为 ENTRYPOINT 指定命令的参数。
• 如果 ENTRYPOINT 使用了 exec 模式,CMD 也应该使用 exec 模式。
真实的情况要远比这三条规律复杂,好在 docker 给出了官方的解释,如下图所示:
当我们无法理解容器中运行命令的行为时,说不定通过这个表格可以解开疑惑!注:有空好好的读读 —— https://docs.docker.com/engine/reference/builder/
总结
对于 Dockerfile 来说,CMD 和 ENTRYPOINT 是非常重要的指令。它们不是在构建镜像的过程中执行,而是在启动容器时执行,所以主要用来指定容器默认执行的命令。但是提供两个功能类似的指令,必然会给用户带来理解上的困惑和使用中的混淆。希望本文能够帮助大家理解二者的区别与联系,并更好的使用二者。(参考地址:https://www.cnblogs.com/sparkdev/p/8461576.html)
这里严重提出:dockerfile中的语句能合并的合并:
- ENV 定义好多行:一行写一个;
- RUN命令也是
不行:越看越不对劲啊——还是有很多东西要实践:比如这篇文章:http://www.dockone.io/article/2338 ;好了不扯了,做作业吧!!!
DockerFile 制作Nginx镜像
本次实验使用的基础镜像为:CentOS7
1 root@ubuntu:~/nginx# cat Dockerfile 2 FROM centos:7 3 LABEL author=zhengweimin email=307762192@qq.com 4 RUN yum install epel-release -y && yum install nginx -y && yum clean all && rm -rf /usr/share/nginx/html/index.html && echo 'nginx test image' > /usr/share/nginx/html/index.html 5 EXPOSE 80 443 6 ENTRYPOINT ["/usr/sbin/nginx"] 7 CMD ["-g","daemon off;"]
具体过程如下:
1 root@ubuntu:~/nginx# docker build -t nginx:v0.2 . 2 Sending build context to Docker daemon 2.048kB 3 Step 1/6 : FROM centos:7 4 ---> 8652b9f0cb4c 5 Step 2/6 : LABEL author=zhengweimin email=307762192@qq.com 6 ---> Using cache 7 ---> 468418ed292a 8 Step 3/6 : RUN yum install epel-release -y && yum install nginx -y && yum clean all && rm -rf /usr/share/nginx/html/index.html && echo 'nginx test image' > /usr/share/nginx/html/index.html 9 ---> Using cache 10 ---> 2c0fc5a0f9cb 11 Step 4/6 : EXPOSE 80 443 12 ---> Using cache 13 ---> 09d78c1fec67 14 Step 5/6 : ENTRYPOINT ["/usr/sbin/nginx"] 15 ---> Using cache 16 ---> 423a5a3b9c34 17 Step 6/6 : CMD ["-g","daemon off;"] 18 ---> Using cache 19 ---> 802f05f49216 20 Successfully built 802f05f49216 21 Successfully tagged nginx:v0.2 22 root@ubuntu:~/nginx# cat Dockerfile 23 FROM centos:7 24 LABEL author=zhengweimin email=307762192@qq.com 25 RUN yum install epel-release -y && yum install nginx -y && yum clean all && rm -rf /usr/share/nginx/html/index.html && echo 'nginx test image' > /usr/share/nginx/html/index.html 26 EXPOSE 80 443 27 ENTRYPOINT ["/usr/sbin/nginx"] 28 CMD ["-g","daemon off;"] 29 root@ubuntu:~/nginx#