Docker早已渗透到我日常开发测试, 网站部署等各个环节, 也有了一些认识和理解 本篇只做简单的概念认识和基本的使用

什么是容器

容器 ,也就是container, 是一种轻量级的虚拟化技术

它通过namespacescgroups, 在同一台主机上, 为每一组进程提供了相互隔离, 可控资源的运行环境

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 挂载点>
    1. 当容器尝试读取某个文件, overlayFS先在upperdir查找
    2. 没有的话到lowerdir按顺序找出第一个出现该文件的层, 最终返回只读层的内容
    1. 若容器对文件进行写操作, OverlayFs检测到了upperdir不存在这个文件
    2. 先把lowerdir里的文件拷贝一份到upperdir
    3. 然后对upperdir的那份副本进行写入操作, 后续所有读写都走upperdir
  1. 删除
    1. 如果容器删除某个只读层里的文件, overlayFs会在upperdir创建一个特殊的whiteout来屏蔽lowerdir的同名文件

可以通过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 链接成一个大二层网络

工作原理就是

  1. 每个节点在宿主机上跑一个 Agent, 管理veth <-> bridge
  2. agent建立隧道, 把数据包封装后发给目标节点
  3. 另一端解封, 把包送到对应容器的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 .

  1. 将每条指令(FROM、RUN、COPY…)转成一系列 build 操作
  2. 执行中间步骤
    1. runc create/start 一个临时容器(只读层 + 可写层 COW)
    2. 在容器内执行 RUN 指令, 安装或拷贝文件
    3. 结束后 runc commit, 把可写层 diff 导出为一个新的 layer blob,挂到本地存储
  3. 汇总 生成最终镜像的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为例

  1. 通过 REST API 提交 run 请求, 传入 image、命令、端口映射等配置
  2. 在创建容器的时候, 调用clone(CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWNET|…)创建独立的命名空间, 并且在/sys/fs/cgroup/<driver>/.../<containerID>/下创建子组, 写入对应内存/CPU/IO限
  3. 之后mount overlay, 把lowerdir, upperdir, workdir合并成mergedir, 挂载到容器内的根目录下
  4. 创建网络veth对, 设置IP和路由
  5. 最后启动进程

网络和存储

命令 说明
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
                }
            }
        }
    }
]