Docker早已渗透到我日常开发测试, 网站部署等各个环节, 也有了一些认识和理解 本篇只做简单的概念认识和基本的使用
什么是容器
容器 ,也就是container, 是一种轻量级的虚拟化技术
它通过namespaces
和cgroups
, 在同一台主机上, 为每一组进程提供了相互隔离, 可控资源的运行环境
Namespace 隔离
命名空间隔离
每个容器都有自己的PID, 网络, 挂载, IPC, UTS等命名空间,让容器内的进程以为自己在一台独立的主机上运行
如上, 不同容器之间的命名空间互不干扰
Docker在Linux下容器共用宿主机内核, 只是做了PID namespace的隔离 但是在OSX/Windows下, 实际上是运行在LinuxKit VM下启动的容器进程, 也就是套了一层虚拟机
我们在Docker里执行sleep 114514
在宿主机看到的进程PID是1561790, kill掉的同时,docker里的进程也被kill掉
和namespace有关的clone标志, 除了上面相关的CLONE_NEWPID
还有以下
CLONE_NEWNS /* 挂载(Mount)命名空间 */
CLONE_NEWUTS /* UTS(主机名/域名)命名空间 */
CLONE_NEWIPC /* IPC 命名空间 */
CLONE_NEWUSER /* 用户命名空间 */
CLONE_NEWNET /* 网络命名空间 */
CLONE_NEWCGROUP /* cgroup 命名空间(Linux ≥4.6) */
CLONE_NEWTIME /* 时间命名空间(Linux ≥5.6) */
如果想加入而不是新建一个已经有的PID Namespace, 可以先打开/proc/[pid]/ns/pid
, 然后用 setns(fd, CLONE_NEWPID)
cgroups资源限制
在拥有Namespace的情况下, 我们实现了容器和容器, 容器和宿主机的隔离
但是我们还需要对容器进行资源控制,限制每个容器的资源消耗
cgroups作用
- 资源限制: 为一组进程设置CPU, 内存, 网络带宽上限或权重, 以防某个容器或进程独占宿主机资源
- 资源隔离: 不同cgroup中的进程在使用资源的时候互不影响, 比如一个容器用满了80%, 不影响另一个容器的20%
- 资源计量: 统计某个进程组在一段时间内消耗了多少CPU时间, 多少内存等
- 资源控制策略: 可以按需调整策略, 按照不同的优先级, 权重, 速率来分配资源
cgroup有v1和v2
最主要的区别在层级结构, 控制器管理和接口一致性
cgroupv1 | cgroupv2 | |
---|---|---|
层级结构 | 各个资源控制器(cpumemory,blkio,pids)各自挂一棵树,彼此独立 | 单一统一层级,所有控制器共用一棵树 |
资源接口 | 各 controller 的接口名称和行为不完全一致 | 统一接口文件(cpu.max ,memory.max ,io.max,pids.max ) |
子目录生效 | 每个控制器由自己独立管理,创建子目录后要分别在各挂载中配置 | 通过顶层的 cgroup.subtree_control 一次性开启, 下级目录自动继承 |
挂载点 | /sys/fs/cgroup/cpu , /sys/fs/cgroup/memory 等一堆 |
只挂 /sys/fs/cgroup |
... | ... | ... |
由于层级结构的不同, v1的事件通知, OOM/ swap管理等一般都需要分别配置
v2用同一的文件或结构进行管理
由于本地是v2的环境, 以v2环境做演示cgroup效果
echo "+cpu +memory +io +pids" > cgroup.subtree_control
CPU限速
mkdir demo-cpu
cd demo-cpu
echo "20000 100000" > cpu.max
运行一个全核100%的程序
yes > /dev/null &
echo $! > cgroup.procs
top下看到这个进程消耗就是20%
内存限制
同理创建mkdir demo-mem; cd demo-mem
echo $((50*1024*1024)) > memory.max
限制最大内存为50MiB
启动一个分配100MiB的程序
(sleep 3 && stress --vm 1 --vm-bytes 100M --vm-hang 0) &
echo $! > cgroup.procs
可以观察到他被OOM杀死 dmesg -w
进程数限制
echo 5 > pids.max
启动10个shell
( sleep 3 &&
for i in $(seq 1 10); do
sleep 60 &
done
wait
) &
echo $! > cgroup.procs
结果如下, 报错资源不可用
I/O限速
# 每秒最多读 1MiB,写 512KiB
echo "8:0 rbps=1048576 wbps=524288" > io.max
然后运行
( sleep 5 && dd if=/dev/zero of=/root/test.file bs=1M count=100 oflag=direct ) &
echo $! > cgroup.procs
得到结果
......
容器的文件系统
Docker镜像是基于分层镜像 + 联合挂载 + 写时复制的轻量化存储方案
在此之前先说说rootfs
rootfs
rootfs是根文件系统
在linux启动过程中, 最开始会挂载一个最基础的根文件系统, 叫做rootfs
这通常是一个空的临时文件系统,内核启动完成后会用真正的根分区来替换它
对于容器来说, rootfs 就是这个容器进程看到的/
容器进入到自己独立的Mount Namespace, 会把某个目录, /var/lib/docker/overlay2/xxx/
,以 pivot_root 或 chroot 的方式挂到/,变成容器的rootfs
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301-init/diff:/var/lib/docker/overlay2/2c4ab54bd18eecb1c2051ff9bc89b2d17264474f72dcc6a97c9c784e9e1a7878/diff",
"MergedDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/merged",
"UpperDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/diff",
"WorkDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/work"
},
"Name": "overlay2"
},
如上, 这个/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/merged
就是当前容器内的根目录
镜像层与存储驱动
Docker镜像由若干layer叠加而成, 每一层本质上是一个只读的tar包, 记录了某次docker build里的所有文件改动
每个层可能被多个镜像引用,两套目录来管理层和镜像,
# content/sha256/ 下存着每个镜像的config JSON,文件名就是镜像Config digest
/var/lib/docker/image/overlay2/imagedb
#每个子目录对应一个DiffID,名字就是那个SHA256
/var/lib/docker/image/overlay2/layerdb/sha256/
启动容器的时候, Docker 从镜像的 config 拿到那串 diff_ids
,然后对每个 DiffID都去这layerdb/sha256/<DiffID>/cache-id
读出对应diff目录, 依照parent关系组装成一个lowerdir列表
不同驱动实现了对这些层的管理和联合挂载.常见的有
- overlay2
- aufs
- btrfs, zfs
overlay2最为常见
联合文件系统和写时复制
三个关键目录
- lowerdir: 由一系列只读的镜像层目录组成, 通过冒号分隔传给 OverlayFS
- upperdir: 容器运行时所在的可写层, 对应着
containerID/diff
- workdir : OverlayFS 内部使用的工作目录, 用于完成写时复制时的中转
然后 OverlayFS 把它们挂载到一个 merged 目录
mount -t overlay overlay \
-o lowerdir=<镜像层1>:<镜像层2>:…,\
upperdir=<容器可写层路径>/diff,\
workdir=<容器可写层路径>/work \
<容器 merged 挂载点>
- 读
- 当容器尝试读取某个文件, overlayFS先在upperdir查找
- 没有的话到lowerdir按顺序找出第一个出现该文件的层, 最终返回只读层的内容
- 写
- 若容器对文件进行写操作, OverlayFs检测到了upperdir不存在这个文件
- 先把lowerdir里的文件拷贝一份到upperdir
- 然后对upperdir的那份副本进行写入操作, 后续所有读写都走upperdir
- 删除
- 如果容器删除某个只读层里的文件, overlayFs会在upperdir创建一个特殊的
whiteout
来屏蔽lowerdir
的同名文件
- 如果容器删除某个只读层里的文件, overlayFs会在upperdir创建一个特殊的
可以通过mount | grep overlay
来查看overlay挂载
通过docker inspect -f
来查看对应层级路径
整个层级大概如下
/var/lib/docker/overlay2/ ← 根挂载目录
├── <imageLayerID1>/
│ ├── diff/ ← 只读层1 的文件快照
│ └── metadata… ← link、lower、committed 等元数据
├── <imageLayerID2>/
│ └── diff/ ← 只读层2 的文件快照
├── … ← 每个镜像层一个目录
│
├── <containerID>-init/ ← 容器的init layer, 也是只读层,只不过只有在container创建时才有
│ └── diff/ ← init layer 的改动
│
├── <containerID>/ ← 容器专属的 overlay mount 实例
│ ├── diff/ ← upperdir:容器写入层(Copy-On-Write 写到这里)
│ ├── work/ ← workdir:OverlayFS 内部做 COW 时用的工作目录
│ └── merged/ ← merged:真正挂给容器 `/` 的合并视图
└──
网络
clone的时候加上 CLONE_NEWNET
参数, Linux 支持把网络资源分到不同的 namespace
每个容器都有自己独立的网络视图
veth
veth, 也就是虚拟以太网对, 一端在宿主机叫vethxxx, 另一端在容器里, 叫eth0
数据写入到一端, 瞬间从另一端跑出来
默认bridge 与 NAT
docker0
Docker启动的时候 在宿主机创建一个linux bridge, 默认名为docker0,并把所有容器端 veth 接到这个 bridge
bridge相当于一个二层交换机, 容器间可以互相ping
NAT 和 端口映射
容器在私有网段与外网隔离
当使用docker run -p 8080:80
进行端口映射的时候,会在nat表里加一条
PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
Host 和 None 模式
- --network host
- 容器内不创建net Namespace, 直接使用宿主机网络栈
- 内部eth0就是宿主机eth0, 端口冲突要小心
- --network none
- 容器内只有一个lo接口, 没有任何外部网卡, 完全与外界断网
overlay网络跨主机互联
Docker Swarm 或 Kubernetes 常用 Overlay Network,在每台宿主机上起一个 VXLAN 或 GRE tunnel,将不同物理机的 docker0/bridge 链接成一个大二层网络
工作原理就是
- 每个节点在宿主机上跑一个 Agent, 管理veth <-> bridge
- agent建立隧道, 把数据包封装后发给目标节点
- 另一端解封, 把包送到对应容器的veth
容器内DNS和hosts
/etc/hosts
/etc/resolv.conf
指向宿主机或集群DNS
常见命令
镜像管理
命令 | 说明 |
---|---|
docker pull <镜像>[:<标签>] | 从镜像仓库下载官方或私有镜像 |
docker images | 列出本地所有镜像 |
docker rmi <镜像ID> <仓库:标签> | 删除本地镜像 |
docker build -t <名称>:<标签> . | 根据当前目录下的 Dockerfile 构建镜像,并打上标签 |
docker tag <镜像ID> <仓库>/<名称>:<标签> | 为已有镜像添加新的仓库/标签 |
docker push <仓库>/<名称>:<标签> | 将本地镜像推送到远程仓库 |
构建镜像
docker build -t myapp:1.0 .
- 将每条指令(FROM、RUN、COPY…)转成一系列 build 操作
- 执行中间步骤
runc create/start
一个临时容器(只读层 + 可写层 COW)- 在容器内执行 RUN 指令, 安装或拷贝文件
- 结束后
runc commit
, 把可写层 diff 导出为一个新的 layer blob,挂到本地存储
- 汇总 生成最终镜像的config, 生成minifest, 写入镜像库
容器管理
命令 | 说明 |
---|---|
docker run [选项] <镜像> [命令] | 启动并运行一个新容器 常用选项:-d :后台运行; -it :交互式+TTY--name :指定容器名; -p 主机端口:容器端口:端口映射; -v 主机路径:容器路径:挂载卷 |
docker ps | 列出正在运行的容器 |
docker ps -a | 列出所有容器 |
docker stop <容器> | 发送 SIGTERM 停止容器 |
docker kill <容器> | 发送 SIGKILL 强制停止容器 |
docker rm <容器> | 删除已停止的容器 |
docker logs [选项] <容器> | 查看容器输出日志.常用 -f 实时追踪 |
docker exec [选项] <容器> [命令] | 在运行中的容器里执行命令.常用:-it /bin/sh 进入交互式 shell |
docker inspect <容器ID> | 列出容器配置 |
docker port <容器> | 列出容器的端口映射 |
docker cp <源> <容器>:<目标> | 在宿主机和容器间复制文件 |
以docker run -d --name web -p 8080:80 nginx
为例
- 通过 REST API 提交 run 请求, 传入 image、命令、端口映射等配置
- 在创建容器的时候, 调用
clone(CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWNET|…)
创建独立的命名空间, 并且在/sys/fs/cgroup/<driver>/.../<containerID>/
下创建子组, 写入对应内存/CPU/IO限 - 之后mount overlay, 把lowerdir, upperdir, workdir合并成mergedir, 挂载到容器内的根目录下
- 创建网络veth对, 设置IP和路由
- 最后启动进程
网络和存储
命令 | 说明 |
---|---|
docker network ls | 列出所有 Docker 网络 |
docker network inspect <网络名> | 查看某个网络的详细配置 |
docker network create <网络名> [驱动器选项] | 创建自定义网络 |
docker network connect <网络> <容器> | 将容器加入到指定网络 |
docker network rm <网络> | 删除自定义网络 |
docker volume ls | 列出所有数据卷 |
docker volume create <卷名> | 创建数据卷 |
docker volume inspect <卷名> | 查看卷的挂载点与驱动器 |
docker volume rm <卷名> | 删除不再使用的数据卷 |
...下面为inspect的内容, 暂存
[
{
"Id": "997687ec789f2ee0097cf9e15a1882b76a97d931308a736426bbee55037368e0",
"Created": "2025-06-08T08:01:17.186074251Z",
"Path": "/bin/sh",
"Args": [],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 1560068,
"ExitCode": 0,
"Error": "",
"StartedAt": "2025-06-08T08:01:17.328199263Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"Image": "sha256:2abc5e83407155714f171c293f197e1310176959e106f8ad63ffa2e7e9635d4a",
"ResolvConfPath": "/var/lib/docker/containers/997687ec789f2ee0097cf9e15a1882b76a97d931308a736426bbee55037368e0/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/997687ec789f2ee0097cf9e15a1882b76a97d931308a736426bbee55037368e0/hostname",
"HostsPath": "/var/lib/docker/containers/997687ec789f2ee0097cf9e15a1882b76a97d931308a736426bbee55037368e0/hosts",
"LogPath": "/var/lib/docker/containers/997687ec789f2ee0097cf9e15a1882b76a97d931308a736426bbee55037368e0/997687ec789f2ee0097cf9e15a1882b76a97d931308a736426bbee55037368e0-json.log",
"Name": "/admiring_boyd",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "default",
"PortBindings": {},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"ConsoleSize": [
0,
0
],
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"KernelMemory": 0,
"KernelMemoryTCP": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301-init/diff:/var/lib/docker/overlay2/2c4ab54bd18eecb1c2051ff9bc89b2d17264474f72dcc6a97c9c784e9e1a7878/diff",
"MergedDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/merged",
"UpperDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/diff",
"WorkDir": "/var/lib/docker/overlay2/cee635ffcda3b1ded803ffcf84bf3ac487938e932fc9b71621ab863613957301/work"
},
"Name": "overlay2"
},
"Mounts": [],
"Config": {
"Hostname": "997687ec789f",
"Domainname": "",
"User": "",
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"Tty": true,
"OpenStdin": true,
"StdinOnce": true,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh"
],
"Image": "alpine",
"Volumes": null,
"WorkingDir": "/",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "72e304078210e4de63878b21ad6fab06136730011dfa9ec8a6c5c58ccde61f3f",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {},
"SandboxKey": "/var/run/docker/netns/72e304078210",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "2e4bdb7e12cbcd0ca77d86a1553f688845f533bc725b67bd1c62e6196d3db404",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:02",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "d6b88a7aab0a170d29261de649cc344ff8e9e5287037fe327a53feae9e7c9e54",
"EndpointID": "2e4bdb7e12cbcd0ca77d86a1553f688845f533bc725b67bd1c62e6196d3db404",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
}
}
]