拉取镜像,使用 docker pull 命令拉取远程仓库的镜像到本地 ;
重命名镜像,使用 docker tag 命令“重命名”镜像 ;
查看镜像,使用 docker image ls 或 docker images 命令查看本地已经存在的镜像;
删除镜像,使用 docker rmi 命令删除无用镜像 ;
构建镜像,构建镜像有两种方式。第一种方式是使用 docker build 命令基于 Dockerfile 构建镜像,也是我比较推荐的镜像构建方式;第二种方式是使用 docker commit 命令基于已经运行的容器提交为镜像。
Dockerfile示例:
FROM centos:7 COPY nginx.repo /etc/yum.repos.d/nginx.repo RUN yum install -y nginx EXPOSE 80 ENV HOST=mynginx CMD ["nginx","-g","daemon off;"]以下是参数解析
第一行表示我要基于 centos:7 这个镜像来构建自定义镜像。这里需要注意,每个 Dockerfile 的第一行除了注释都必须以 FROM 开头。第二行表示拷贝本地文件 nginx.repo 文件到容器内的 /etc/yum.repos.d 目录下。这里拷贝 nginx.repo 文件是为了添加 nginx 的安装源。第三行表示在容器内运行yum install -y nginx命令,安装 nginx 服务到容器内,执行完第三行命令,容器内的 nginx 已经安装完成。第四行声明容器内业务(nginx)使用 80 端口对外提供服务。第五行定义容器启动时的环境变量 HOST=mynginx,容器启动后可以获取到环境变量 HOST 的值为 mynginx。第六行定义容器的启动命令,命令格式为 json 数组。这里设置了容器的启动命令为 nginx ,并且添加了 nginx 的启动参数 -g 'daemon off;' ,使得 nginx 以前台的方式启动。镜像的实现原理:
镜像是由一系列的镜像层(layer )组成,每一层代表了镜像构建过程中的一次提交,当我们需要修改镜像内的某个文件时,只需要在当前镜像层的基础上新建一个镜像层,并且只存放修改过的文件内容。分层结构使得镜像间共享镜像层变得非常简单和方便。容器的生命周期是容器可能处于的状态,容器的生命周期分为 5 种。
created:初建状态
running:运行状态
stopped:停止状态
paused: 暂停状态
deleted:删除状态
各生命周期之前的转换关系如图所示2
通过docker create命令生成的容器状态为初建状态,初建状态通过docker start命令可以转化为运行状态,运行状态的容器可以通过docker stop命令转化为停止状态,处于停止状态的容器可以通过docker start转化为运行状态,运行状态的容器也可以通过docker pause命令转化为暂停状态,处于暂停状态的容器可以通过docker unpause转化为运行状态 。处于初建状态、运行状态、停止状态、暂停状态的容器都可以直接删除。
容器十分轻量,用户可以随时创建和删除它。我们可以使用docker create命令来创建容器。
创建容器:docker create -it --name=busybox busybox启动容器:1、使用docker start命令基于已经创建好的容器直接启动 。
2、使用docker run命令直接基于镜像新建一个容器并启动,相当于先执行docker create命令从镜像创建容器,然后再执行docker start命令启动容器。docker run -it --name=busybox busybox
当使用docker run创建并启动容器时,Docker 后台执行的流程为:
Docker 会检查本地是否存在 busybox 镜像,如果镜像不存在则从 Docker Hub 拉取 busybox 镜像;
使用 busybox 镜像创建并启动一个容器;
分配文件系统,并且在镜像只读层外创建一个读写层;
从 Docker IP 池中分配一个 IP 给容器;
执行用户的启动命令运行镜像。
上述命令中, -t 参数的作用是分配一个伪终端,-i 参数则可以终端的 STDIN 打开,同时使用 -it 参数可以让我们进入交互模式。 在交互模式下,用户可以通过所创建的终端来输入命令
$ ps aux PID USER TIME COMMAND 1 root 0:00 sh 6 root 0:00 ps aux我们可以看到容器的 1 号进程为 sh 命令,在容器内部并不能看到主机上的进程信息,因为容器内部和主机是完全隔离的。同时由于 sh 是 1 号进程,意味着如果通过 exit 退出 sh,那么容器也会退出。所以对于容器来说,杀死容器中的主进程,则容器也会被杀死。
容器启动后,如果我们想停止运行中的容器,可以使用docker stop命令。命令格式为 docker stop [-t|--time[=10]]。该命令首先会向运行中的容器发送 SIGTERM 信号,如果容器内 1 号进程接受并能够处理 SIGTERM,则等待 1 号进程处理完毕后退出,如果等待一段时间后,容器仍然没有退出,则会发送 SIGKILL 强制终止容器。
如果你想查看停止状态的容器信息,你可以使用 docker ps -a 命令。
处于终止状态的容器也可以通过docker start命令来重新启动。
此外,docker restart命令会将一个运行中的容器终止,并且重新启动它。
处于运行状态的容器可以通过docker attach、docker exec、nsenter等多种方式进入容器。
使用docker attach命令进入容器使用 docker attach ,进入我们上一步创建好的容器,如下所示。
$ docker attach busybox / # ps aux PID USER TIME COMMAND 1 root 0:00 sh 7 root 0:00 ps aux / #注意:当我们同时使用docker attach命令同时在多个终端运行时,所有的终端窗口将同步显示相同内容,当某个命令行窗口的命令阻塞时,其他命令行窗口同样也无法操作。 由于docker attach命令不够灵活,因此我们一般不会使用docker attach进入容器。下面我介绍一个更加灵活的进入容器的方式docker exec
使用 docker exec 命令进入容器Docker 从 1.3 版本开始,提供了一个更加方便地进入容器的命令docker exec,我们可以通过docker exec -it CONTAINER的方式进入到一个已经运行中的容器,如下所示。
$ docker exec -it busybox sh / # ps aux PID USER TIME COMMAND 1 root 0:00 sh 7 root 0:00 sh 12 root 0:00 ps aux我们进入容器后,可以看到容器内有两个sh进程,这是因为以exec的方式进入容器,会单独启动一个 sh 进程,每个窗口都是独立且互不干扰的,也是使用最多的一种方式。
我们已经掌握了用 Docker 命令创建、启动和终止容器。那如何删除处于终止状态或者运行中的容器呢?删除容器命令的使用方式如下:docker rm [OPTIONS] CONTAINER [CONTAINER...]。
如果要删除一个停止状态的容器,可以使用docker rm命令删除。
docker rm busybox如果要删除正在运行中的容器,必须添加 -f (或 --force) 参数, Docker 会发送 SIGKILL 信号强制终止正在运行的容器。
docker rm -f busybox导出容器
我们可以使用docker export CONTAINER命令导出一个容器到文件,不管此时该容器是否处于运行中的状态。导出容器前我们先进入容器,创建一个文件,过程如下。
首先进入容器创建文件
docker exec -it busybox sh cd /tmp && touch test然后执行导出命令
docker export busybox > busybox.tar执行以上命令后会在当前文件夹下生成 busybox.tar 文件,我们可以将该文件拷贝到其他机器上,通过导入命令实现容器的迁移。
导入容器
通过docker export命令导出的文件,可以使用docker import命令导入,执行完docker import后会变为本地镜像,最后再使用docker run命令启动该镜像,这样我们就实现了容器的迁移。
导入容器的命令格式为 docker import [OPTIONS] file|URL [REPOSITORY[:TAG]]。接下来我们一步步将上一步导出的镜像文件导入到其他机器的 Docker 中并启动它。
首先,使用docker import命令导入上一步导出的容器
docker import busybox.tar busybox:test此时,busybox.tar 被导入成为新的镜像,镜像名称为 busybox:test 。下面,我们使用docker run命令启动并进入容器,查看上一步创建的临时文件
docker run -it busybox:test sh / # ls /tmp/ test可以看到我们之前在 /tmp 目录下创建的 test 文件也被迁移过来了。这样我们就通过docker export和docker import命令配合实现了容器的迁移。
Namespace 是 Linux 内核的一个特性,该特性可以实现在同一主机系统中,对进程 ID、主机名、用户 ID、文件名、网络和进程间通信等资源的隔离。Docker 利用 Linux 内核的 Namespace 特性,实现了每个容器的资源相互隔离,从而保证容器内部只能访问到自己 Namespace 的资源。
Namespace 名称作用内核版本Mount(mnt)隔离挂载点2.4.19Process ID (pid)隔离进程 ID2.6.24Network (net)隔离网络设备,端口号等2.6.29Interprocess Communication (ipc)隔离 System V IPC 和 POSIX message queues2.6.19UTS Namespace(uts)隔离主机名和域名2.6.19User Namespace (user)隔离用户和用户组3.8Control group (cgroup) Namespace隔离 Cgroups 根目录4.6Time Namespace隔离系统时间5.6
虽然 Linux 内核提供了8种 Namespace,但是最新版本的 Docker 只使用了其中的前6 种,分别为Mount Namespace、PID Namespace、Net Namespace、IPC Namespace、UTS Namespace、User Namespace。
它可以用来隔离不同的进程或进程组看到的挂载点。通俗地说,就是可以实现在不同的进程中看到不同的挂载目录。使用 Mount Namespace 可以实现容器内只能看到自己的挂载信息,在容器内的挂载操作不会影响主机的挂载目录。
# 创建一个 bash 进程并且新建一个 Mount Namespace sudo unshare --mount --fork /bin/bash # 已经在主机上创建了一个新的 Mount Namespace,并且当前命令行窗口加入了新创建的 Mount Namespace # 在/tmp 目录下创建一个目录 mkdir /tmp/tmpfs # 创建好目录后使用 mount 命令挂载一个 tmpfs 类型的目录 mount -t tmpfs -o size=20m tmpfs /tmp/tmpfs # 这时候使用以下命令发现已经挂载了/tmp/tempfs目录 df -h # 这时候重新一个连接窗口通过df -h 命令查看没有挂载该目录 # 通过mount namespace就实现了挂载点隔离通过以上结果我们可以得出结论,使用 unshare 命令可以新建 Mount Namespace,并且在新建的 Mount Namespace 内 mount 是和外部完全隔离的。
PID Namespace 的作用是用来隔离进程。在不同的 PID Namespace 中,进程可以拥有相同的 PID 号,利用 PID Namespace 可以实现每个容器的主进程为 1 号进程,而容器内的进程在主机上却拥有不同的PID。
# 使用以下命令创建一个 bash 进程,并且新建一个 PID Namespace [root@mymaster01 ~]# unshare --pid --fork --mount-proc /bin/bash # 主机上创建了一个新的 PID Namespace,并且当前命令行窗口加入了新创建的 PID Namespace [root@mymaster01 ~]# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.6 0.1 115572 2136 pts/0 S 22:25 0:00 /bin/bash root 12 0.0 0.0 155328 1856 pts/0 R+ 22:26 0:00 ps aux # 然后新开一个窗口再执行ps aux会发现看不到bash 进程存在UTS Namespace 主要是用来隔离主机名的,它允许每个 UTS Namespace 拥有一个独立的主机名。例如我们的主机名称为 docker,使用 UTS Namespace 可以实现在容器内的主机名称为 lagoudocker 或者其他任意自定义主机名。
[root@mymaster01 ~]# unshare --uts --fork /bin/bash [root@mymaster01 ~]# hostname -b lagoudocker [root@mymaster01 ~]# hostname lagoudocker # 新开的另一个窗口 [root@mymaster01 ~]# hostname mymaster01IPC Namespace 主要是用来隔离进程间通信的。例如 PID Namespace 和 IPC Namespace 一起使用可以实现同一 IPC Namespace 内的进程彼此可以通信,不同 IPC Namespace 的进程却不能通信。
# 使用 unshare 命令来创建一个 IPC Namespace [root@mymaster01 ~]# unshare --ipc --fork /bin/bash # 用来查看系统间通信队列列表 [root@lagoudocker ~]# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages # 用来创建系统间通信队列。 [root@lagoudocker ~]# ipcmk -Q Message queue id: 0 [root@lagoudocker ~]# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 0x01105f14 0 root 644 0 0 # 另一个新开的窗口 [root@mymaster01 ~]# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messagesUser Namespace 主要是用来隔离用户和用户组的。一个比较典型的应用场景就是在主机上以非 root 用户运行的进程可以在一个单独的 User Namespace 中映射成 root 用户。使用 User Namespace 可以实现进程在容器内拥有 root 权限,而在主机上却只是普通用户。
# CentOS7 默认允许创建的 User Namespace 为 0 [root@mymaster01 ~]# echo 65535 > /proc/sys/user/max_user_namespaces # 这里切换到eason用户 [root@mymaster01 ~]# su eason # User Namesapce 的创建是可以不使用 root 权限的,以普通用户的身份创建一个 User Namespace [eason@mymaster01 root]$ unshare --user -r /bin/bash # 下面看到目前在当前的namespace中已经是root用户了 [root@mymaster01 root]# id uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 # 这里执行只有root用户才能执行的reboot,但是发现失败了。 # 说明:在隔离的 User Namespace 中,并不能获取到主机的 root 权限,也就是说 User Namespace 实现了用户和用户组的隔离。 [root@mymaster01 root]# reboot Failed to open /dev/initctl: Permission denied Failed to talk to init daemon.Net Namespace 是用来隔离网络设备、IP 地址和端口等信息的。Net Namespace 可以让每个进程拥有自己独立的 IP 地址,端口和网卡信息。
# 这里先查看下当前的网络配置 [root@mymaster01 root]# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:0c:29:03:fb:b2 brd ff:ff:ff:ff:ff:ff inet 192.168.52.128/24 brd 192.168.52.255 scope global noprefixroute ens33 valid_lft forever preferred_lft forever inet6 fe80::20c:29ff:fe03:fbb2/64 scope link valid_lft forever preferred_lft forever 3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default link/ether 02:42:08:33:f2:10 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever # 这里创建一个新的Net Namespace [root@mymaster01 root]# unshare --net --fork /bin/bash # 然后再查看当前的网络配置,发现和之前的不一样 [root@mymaster01 root]# ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00cgroups(全称:control groups)是 Linux 内核的一个功能,
cgroups 主要提供了如下功能。
资源限制: 限制资源的使用量,例如我们可以通过限制某个业务的内存上限,从而保护主机其他业务的安全运行。
优先级控制:不同的组可以有不同的资源( CPU 、磁盘 IO 等)使用优先级。
审计:计算控制组的资源使用情况。
控制:控制进程的挂起或恢复。
了解了 cgroups 可以为我们提供什么功能,下面我来看下 cgroups 是如何实现这些功能的。
cgroups功能的实现依赖于三个核心概念:子系统、控制组、层级树。
子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。
控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系。例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组。
层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的。这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组。比如,系统中定义了一个控制组 c1,限制了 CPU 可以使用 1 核,然后另外一个控制组 c2 想实现既限制 CPU 使用 1 核,同时限制内存使用 2G,那么 c2 就可以直接继承 c1,无须重复定义 CPU 限制。
cgroups 的三个核心概念中,子系统是最核心的概念,因为子系统是真正实现某类资源的限制的基础。
它可以实现限制进程或者进程组的资源(如 CPU、内存、磁盘 IO 等)。
事实上,Docker 创建容器时,Docker 会根据启动容器的参数,在对应的 cgroups 子系统下创建以容器 ID 为名称的目录, 然后根据容器启动时设置的资源限制参数, 修改对应的 cgroups 子系统资源限制文件, 从而达到资源限制的效果。
使用 Docker 创建 null 空网络模式的容器时,容器拥有自己独立的 Net Namespace,但是此时的容器并没有任何网络配置。在这种模式下,Docker 除了为容器创建了 Net Namespace 外,没有创建任何网卡接口、IP 地址、路由等网络配置。
使用方式为:
docker run --net=none -it busyboxDocker 的 bridge 网络是启动容器时默认的网络模式,使用 bridge 网络可以实现容器与容器的互通,可以从一个容器直接通过容器 IP 访问到另外一个容器。同时使用 bridge 网络可以实现主机与容器的互通,我们在容器内启动的业务,可以从主机直接请求。 Docker 的 bridge 模式正是由 Linux 的 veth 和 bridge 实现的。
Linux vethveth 是 Linux 中的虚拟设备接口,veth 都是成对出现的,它在容器中,通常充当一个桥梁。veth 可以用来连接虚拟网络设备,例如 veth 可以用来连通两个 Net Namespace,从而使得两个 Net Namespace 之间可以互相访问。
Linux bridgeLinux bridge 是一个虚拟设备,是用来连接网络的设备,相当于物理网络环境中的交换机。Linux bridge 可以用来转发两个 Net Namespace 内的流量。
bridge 就像一台交换机,而 veth 就像一根网线,通过交换机和网线可以把两个不同 Net Namespace 的容器连通,使得它们可以互相通信。bridge 桥接模式是 Docker 的默认网络模式,当我们创建容器时不指定任何网络模式,Docker 启动容器默认的网络模式为 bridge。
容器内的网络并不是希望永远跟主机是隔离的,有些基础业务需要创建或更新主机的网络配置,我们的程序必须以主机网络模式运行才能够修改主机网络,这时候就需要用到 Docker 的 host 主机网络模式。
使用 host 主机网络模式时:
libnetwork 不会为容器创建新的网络配置和 Net Namespace。Docker 容器中的进程直接共享主机的网络配置,可以直接使用主机的网络信息,此时,在容器内监听的端口,也将直接占用到主机的端口。除了网络共享主机的网络外,其他的包括进程、文件系统、主机名等都是与主机隔离的。使用方式为:
docker run -it --net=host busyboxcontainer 网络模式允许一个容器共享另一个容器的网络命名空间。当两个容器需要共享网络,但其他资源仍然需要隔离时就可以使用 container 网络模式,例如我们开发了一个 http 服务,但又想使用 nginx 的一些特性,让 nginx 代理外部的请求然后转发给自己的业务,这时我们使用 container 网络模式将自己开发的服务和 nginx 服务部署到同一个网络命名空间中。
使用方式为:
docker run -d --name=busybox1 busybox sleep 3600 docker run -it --net=container:busybox1 --name=busybox2 busybox sh通过这种方式这两个容器的网络配置都是一样的,且第二个容器使用第一个容器的网络命名空间。
Docker 提供了卷(Volume)的功能,使用docker volume命令可以实现对卷的创建、查看和删除等操作。下面我们来详细了解一下这些命令。
有时候,两个容器之间会有共享数据的需求,很典型的一个场景就是容器内产生的日志需要一个专门的日志采集程序去采集日志内容,例如我需要使用 Filebeat (一种日志采集工具)采集 nginx 容器内的日志,我就需要使用卷来共享一个日志目录,从而使得 Filebeat 和 nginx 容器都可以访问到这个目录,这时就需要用到容器之间共享数据卷的方式。
# 先创建一个卷 docker volume create log-vol # 然后挂载到一个运行的容器中 docker run --mount source=log-vol,target=/tmp/log --name=log-producer -it busybox # 然后再启动一个程序,也挂载到该卷上,这样就实现了容器间的数据共享,volumes-from参数后面跟已经启动的容器名称 docker run -it --name consumer --volumes-from log-producer busyboxDocker 卷的目录默认在 /var/lib/docker 下,当我们想把主机的其他目录映射到容器内时,就需要用到主机与容器之间数据共享的方式了,例如我想把 MySQL 容器中的 /var/lib/mysql 目录映射到主机的 /var/lib/mysql 目录中,我们就可以使用主机与容器之间数据共享的方式来实现。要实现主机与容器之间数据共享,只需要我们在启动容器的时候添加-v参数即可, 使用格式为:-v HOST_PATH:CONTIANAER_PATH。
# 挂载主机的 /data 目录到容器中的 /usr/local/data 中 docker run -v /data:/usr/local/data -it busyboxDocker 卷的实现原理是在主机的 /var/lib/docker/volumes 目录下,根据卷的名称创建相应的目录,然后在每个卷的目录下创建 _data 目录,在容器启动时如果使用 --mount 参数,Docker 会把主机上的目录直接映射到容器的指定目录下,实现数据持久化。