Docker必会知识点
1.常用命令
1.1 服务
- 查看Docker版本信息
docker version
- 查看docker简要信息
docker -v
- 启动Docker
systemctl start docker
- 关闭docker
systemctl stop docker
- 设置开机启动
systemctl enable docker
- 重启docker服务
service docker restart
- 关闭docker服务
service docker stop
1.2 镜像
- 检索镜像
docker search 关键字
- 拉取镜像
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
- 列出镜像
docker image ls
docker images
- 删除镜像
docker rmi <镜像Id>
- 导出镜像
docker save
- 导入镜像
docker load
Dockerfile常见指令
FROM:指定基础镜像
RUN:执行命令
COPY:复制文件
ADD:更高级的复制文件
CMD:容器启动命令
ENV:设置环境变量
EXPOSE:暴露端口
其它的指令还有ENTRYPOINT、ARG、VOLUME、WORKDIR、USER、HEALTHCHECK、ONBUILD、LABEL等等。
以下是一个Dockerfile实例:
FROM java:8
MAINTAINER "jinshw"<jinshw@qq.com>
ADD mapcharts-0.0.1-SNAPSHOT.jar mapcharts.jar
EXPOSE 8080
CMD java -jar mapcharts.jar
- 镜像构建
docker build
1.3 容器
1.3.1 容器生命周期
- 启动:启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。
# 新建并启动
docker run [镜像名/镜像ID]
# 启动已终止容器
docker start [容器ID]
- 查看容器
# 列出本机运行的容器
$ docker ps
# 列出本机所有的容器(包括停止和运行)
$ docker ps -a
- 停止容器
# 停止运行的容器
docker stop [容器ID]
# 杀死容器进程
docker kill [容器ID]
- 重启容器
docker restart [容器ID]
- 删除容器
docker rm [容器ID]
1.3.2 进入容器
进入容器有两种方式:
# 如果从这个 stdin 中 exit,会导致容器的停止
docker attach [容器ID]
# 交互式进入容器
docker exec [容器ID]
进入容器通常使用第二种方式,docker exec
后面跟的常见参数如下:
-d, —detach 在容器中后台执行命令; - i, —interactive=true I false :打开标准输入接受用户输入命令;-t :分配一个伪终端
1.3.3 导出和导入
- 导出容器
#导出一个已经创建的容器到一个文件
docker export [容器ID]
- 导入容器
# 导出的容器快照文件可以再导入为镜像
docker import [路径]
1.4 其它
- 查看日志
docker logs [容器ID]
这个命令有以下常用参数 -f : 跟踪日志输出
--since :显示某个开始时间的所有日志
-t : 显示时间戳
--tail :仅列出最新N条容器日志
- 复制文件
# 从主机复制到容器
sudo docker cp host_path containerID:container_path
# 从容器复制到主机
sudo docker cp containerID:container_path host_path
2. 容器技术
2.1 从进程说起
什么是进程?
从OS的层面来说,进程就是程序的运行时,代码和数据静态的保存在磁盘上的时候就是程序,一旦这个程序真正运行起来了,那么就成了进程。
而容器技术的核心就是通过约束和修改进程的动态表现,为其创造一个“边界”。对于Docker这种Linux容器来说,通过CGroup技术来制造约束,通过Namespace来修改进程视图。
从一个例子看一下Namespace可以完成什么事情:
先创建一个容器:
docker run -it busybox /bin/sh
/ #
现在在容器中执行ps命令:
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
7 root 0:00 ps
可以看到进程号是从pid=1开始的,并且只有两个进程,这跟在宿主机上的结果一定是不一样的。事实上在宿主机中/bin/sh这条命令一定不是pid=1的,但是Docker通过障眼法让该进程看不到在它之前的进程,这就是利用了Namespace技术。
Namaspace其实只是Linux创建新进程的一个可选参数。在Linux中创建进程通过clone():
int pid = clone(main_functino, stack_size, SIGCHID, NULL);
这样系统调用就会为我们创建一个进程并且返回pid,如果我们指定CLONE_NEWPID参数:
int pid = clone(main_functino, stack_size, CLONE_NEWPID | SIGCHID, NULL);
这样一来该进程就被骗了,它将看到一个船新版本的进程空间,在这个进程空间中它的pid是1,但是在宿主机中它的pid有可能是100。这就是简单的Namespace的使用,用来修改进程视图。当然,Linux还提供了其他的namespace参数选项:MOUNT、UTS、IPC、Network和User,用来做各种障眼法。比如MOUNT用来让当前进程看到当前Namespace的挂载点信息,Network用来来进程只看到当前Namespace的网络设备和配置。
实际上对于Docker来说,用于做容器隔离的手段其实就是在创建容器进程的时候,指定了该进程启动所需要的一组Namespace参数。这样一来,该容器就只能看到当前Namespace下的资源、文件、设备、状态或者配置。
所以么,容器这个高大上的概念的本质其实还是进程。
2.2 隔离与限制
Linux的Namespace技术相对于虚拟化技术其实还是有缺陷的,最主要的问题就是:隔离的不彻底。 既然容器只是一种特殊的进程,那么实际上多个容器之间使用的还是同一个宿主机的操作系统内核,也就是说在windows上运行Linux容器是不现实的。其次,在Linux内核中,有很多资源和对象是不能被Namespace化的,最典型的:时间。比如,如果在容器中调用settimeofday(2)系统调用修改了时间,那么整个宿主机的时间就会被改变,这是不符合预期的。
说完了隔离,下面来说说限制。
虽然容器内的第1号进程可能实际上是宿主机上的第100号进程,并且只能看到容器内的进程空间,但是该进程作为宿主机上的第100号进程与其他进程之间依然是平等的竞争关系。也就是,第100号进程所能用到的资源(比如cpu、内存),可以随时被宿主机上的其他进程或其他容器占用。当然,第100号进程也有可能自己用光宿主机上的所有资源。这些显然都不是一个容器的合理行为。
而Linux Cgoupgs(Linux control groups)就是Linux内核中为进程设置资源限制的一种技术。其最主要的功能就是限制一个进程组能够使用的资源上限,包括CPU、内存、磁盘、网络带宽等等。此外,Cgroup还能对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。这里我们主要说说它的限制功能。
在Linux中,Cgroups向用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的/sys/fs/cgroups路径下。可以用mount命令将其显示:
mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
在/sys/fs/cgrouops目录下的子目录,比如cpu、cpuset、memory,也叫子系统。这些是当前机器可以被Cgroups限制的资源种类。在子系统对应的资源种类下,可以看到这类资源具体可以被限制的方法。
查看cpu目录下:
$ ls /sys/fs/cgroup/cpu
aegis cgroup.clone_children cgroup.procs cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat kubepods release_agent tasks
assist cgroup.event_control cgroup.sane_behavior cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares docker notify_on_release system.slice user.slice
其中有两个文件cpu.cfs_period_us和cpu.cfs_quota_us,用来限制进程在cpu.cfs_period_us的一段时间内,只能被分配到总量为cpu.cfs_quota_us的cpu时间。
那么要如何使用这样的配置文件呢?只需要在对应的子系统下面创建一个目录,比如在/sys/fs/cgroup/cpu创建一个test文件夹:
mkdir test
cd test/
ll
总用量 0
-rw-r--r-- 1 root root 0 12月 6 17:30 cgroup.clone_children
--w--w--w- 1 root root 0 12月 6 17:30 cgroup.event_control
-rw-r--r-- 1 root root 0 12月 6 17:30 cgroup.procs
-r--r--r-- 1 root root 0 12月 6 17:30 cpuacct.stat
-rw-r--r-- 1 root root 0 12月 6 17:30 cpuacct.usage
-r--r--r-- 1 root root 0 12月 6 17:30 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 12月 6 17:30 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 12月 6 17:30 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 12月 6 17:30 cpu.rt_period_us
-rw-r--r-- 1 root root 0 12月 6 17:30 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 12月 6 17:30 cpu.shares
-r--r--r-- 1 root root 0 12月 6 17:30 cpu.stat
-rw-r--r-- 1 root root 0 12月 6 17:30 notify_on_release
-rw-r--r-- 1 root root 0 12月 6 17:30 tasks
这样一个目录称为一个“控制组”,可以看到系统自动在该文件夹下创建了一堆对应资源限制的文件。cpu.cfs_quota_us为-1代表没有限制。
cat cpu.cfs_quota_us
-1
cat cpu.cfs_period_us
100000
现在后台执行脚本启动一个死循环:
while : ; do : ; done &
[1] 20600
使用top命令查看该进程:
top
top - 17:38:50 up 46 days, 17:38, 3 users, load average: 0.82, 0.32, 0.14
Tasks: 187 total, 2 running, 185 sleeping, 0 stopped, 0 zombie
%Cpu(s): 13.1 us, 0.3 sy, 0.0 ni, 86.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32245888 total, 12814256 free, 3753912 used, 15677720 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 28089768 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
20600 root 20 0 115680 708 160 R 99.7 0.0 1:37.50 bash
可以看到该进程的cpu使用率是100%,假如现在想要限制改进程的cpu使用率怎么办?
现在写入文件:
echo 20000 > /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us
cat cpu.cfs_quota_us
20000
这样一来就可以限制某个进程最多只能使用20%的cpu。但是还没有对改进程生效,要将此限制和进程绑定一下:
echo 20600 > /sys/fs/cgroup/cpu/test/task
top
top - 17:40:59 up 46 days, 17:40, 3 users, load average: 0.90, 0.54, 0.25
Tasks: 187 total, 2 running, 185 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.0 us, 0.2 sy, 0.0 ni, 96.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32245888 total, 12814508 free, 3753508 used, 15677872 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 28090172 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
20600 root 20 0 115680 708 160 R 19.9 0.0 3:42.78 bash
再次查看该进程会发现cpu使用率只有20%。
除了cpu子系统之外,Cgroups的每一项子系统都有其独有的资源限制能力,比如:
- blkio,为块设备设定I/O限制,一般用于磁盘等设备。
- cpuset, 为进程分配单独的cpu核和对应的内存节点。
- memory,为进程设定内存的使用限制。
Cgroups使用总结:一个子系统目录加上一组资源限制文件的组合。对于Docker等Linux容器,只需要在每个子系统下面为每个容器创建一个资源组(创建一个新目录),然后在启动容器进程之后,把这个进程的PID填写到对应控制组的tasks文件中即可。
其实用户在使用docker容器的使用想要限制资源非常简单:
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
查看对应的资源限制文件:
cd /sys/fs/cgroup/cpu/docker/b95b0a18ae7eaadcd395a32379aeeeec717ff0b1bdeb5c26697b6566e8ca8e
cat cpu.cfs_quota_us
20000
cat cpu.cfs_period_us
100000
2.3 容器镜像
通过Namespace做隔离,通过Cgroups做限制,那么一个容器进程看到的文件系统是怎么样的?你可能会说,如果创建进程时指定了Mount参数,那么就可以让容器进程看到一个新的文件系统。但事实是,即便开启了Mount参数,容器看到的文件系统也和宿主机的文件系统一致,这是为什么?因为Mount Namespace和其他Namespace不一样,它对容器进程视图的改变一定要伴随着挂载操作才能生效。因此,在容器进程启动之前要重新挂载整个根目录”/“,这样一来容器内的挂载对宿主机不可见,因此容器进程可以在里面随便折腾。
在Linux中,可以通过chroot
命令来执行挂载操作,顾名思义:“change root file system”,改变进程的根路径到任何地方。
为了让容器的根目录看起来更加真实,一般会在容器的根目录下挂载我一个完整操作系统的文件系统,比如Ubuntu16.04的ISO。这样在容器启动以后,在容器里执行ls /命令看到的就是Ubuntu 16.04的所有目录和文件。这个挂载在容器根目录上用来为容器进程提供隔离后执行环境的文件系统,就是“容器镜像”。它还有一个更专业的名字叫:rootfs(根文件系统)。
至此,Docker的核心原理全部都出来了:
- Linux Namespace
- Cgroups
- change root
由于rootfs的存在,容器就有了一个十分重要的特性:一致性。有个rootfs之后,应用打包的不只是应用,而是整个操作系统的文件和目录,这就意味着,应用以及它所需要运行的所有依赖都被封装到一起了。也就是说,有了容器镜像,不管在本地还是云端还是任何一台机器,只要解压打包好的容器镜像,应用以及所需的执行环境就能被重现。
在制作一个rootfs的时候,为了简化制作、不需要重复制作,rootfs是基于增量修改的。这也就是Docker镜像中层(layer)的概念。也就是说,用户制作镜像的每一步操作都会生成一个层,也就是一个增量rootfs。这里主要用到了UnionFS(联合文件系统)的能力,其最主要的功能是将不同位置的目录联合挂载(union mount)到同一个目录下。
假设文件结构如下:
tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
2 directories, 4 files
通过联合挂载的方式将这两个目录挂载到一个公共得分目录C上(Centos 7):
mkdir
mount -t overlay overlay -o lowerdir=A:B ~/test2/C
tree ./C
./C
├── a
├── b
└── x
0 directories, 3 files
此时C文件夹下就能看到A和B文件夹的文件被合并到了一起。并且如果修改C文件夹中的文件,原本文件夹中的文件也会相应修改。
下面以Ubuntu16.04和Docker CE18.05来说明Docker是如何使用这种UnionFS的,在这个环境配置下使用的是Another UnionFS,后来叫Alternative UnionFS,再后来叫Advanced UnionFS。对AuFS来说,最关键的目录结构在/var/lib/docker路径下的diff目录:
/var/lib/docker/aufs/diff/<layer_id>
启动一个容器:
docker run -d ubuntu:latest sleep 3600
查看镜像:
docker inspect image ubuntu:latest
会发现这个镜像由5个层组成,这5层就是5个增量rootfs,每一层都是Ubuntu操作系统文件与目录的一部分;而在使用的时候,Docker会把这些增量联合挂载到一个统一的挂载点上(相当于之前的C目录)。这个挂载点就是/var/lib/docker/aufs/mnt/
容器的rootfs一般由3部分组成:可读可写层(rw)、Init层(ro+wh)、只读层(ro+wh)。
- 只读层
对应原本镜像的层数,比如ubuntu这个镜像的5层 ,readonly+whiteout。
- 可读可写层
Read write,这是rootfs中最上面的一层,在写入文件之前这个文件夹是空的,一旦在容器中进行了写操作,修改的内容就会以增量的方式出现在该层中。如果想执行删除只读层里的一个文件,就会在可读可写层先创建一个whiteout文件,把只读层里的文件遮挡住。比如想删除只读层中一个foo文件,就会现在可读可写层中创建一个foo.whiteout文件,这样当两个层被联合挂载之后,foo文件就被foo.white遮挡住。所以对一个容器中文件的增删改查都只会在可读可写层中操作,并不会影响到原有的镜像只读层。
- Init层
这一层是专门以-init结尾的层,夹在只读层和可读可写层之间,是docker单独生成的一个内部层,专门用来存放/etc/hosts、/etc/resolv.conf等信息。需要这一层的原因是,用户在启动容器时需要写入一些指定值比如hostname,所以需要在可读可写层修改,但是这些修改仅对当前容器生效,在docker commit的时候并不希望连同这些信息一起提交。所以docker修改了这些内容后单独挂载到Init层中,这样在docker commit的时候就不会提交这一层,只会提交可读可写层。
最终这三部分下的layer都被联合挂载到/var/lib/docker/aufs/mnt目录下,表现为一个完整的Ubuntu操作系统目录。