跳转至

Docker相关操作

一. Docker与操作系统的关系

Docker是系统内的一个进程,靠namespace隔离。

容器没有自己的单独的内核,使用的内核是宿主机的。

对操作系统来说,容器的运行就是一个进程。

将容器所对应的进程结束掉,容器也会结束。

容器的生命周期完全取决于1号进程,当1号进程结束时,容器退出。

二. Docker的隔离机制

Docker 的隔离基于 Linux 内核的四种机制,而非虚拟机。四者从不同维度协同工作:

Docker 隔离 = Namespace(视角隔离)+ Cgroups(资源限制)+ UnionFS(文件分层)+ Capabilities/Seccomp(安全裁剪)


1. Namespace(命名空间)— 「我能看到什么」

Namespace 管的是可见性——每个容器拿到自己独立的"视图",看不到宿主机和其他容器的东西。

# 先启动一个虚拟机
[root@4 ~]# docker run -d --rm --name 123 httpd:latest
3d04873fa53c85b3af0100a3df8ff48b129fc3323d843baf3159ae2ae153e84e
# 获取PID号
[root@4 ~]#  docker inspect 123 --format '{{.State.Pid}}'
5836
# 查看命名空间
[root@4 ~]# ls -l /proc/5836/ns/
总用量 0
lrwxrwxrwx 1 root root 0  7月 15 10:42 cgroup -> 'cgroup:[4026532968]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 ipc -> 'ipc:[4026532966]'
lrwxrwxrwx 1 root root 0  7月 15 10:37 mnt -> 'mnt:[4026532964]'
lrwxrwxrwx 1 root root 0  7月 15 10:37 net -> 'net:[4026532969]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 pid -> 'pid:[4026532967]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 pid_for_children -> 'pid:[4026532967]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 time -> 'time:[4026533023]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 time_for_children -> 'time:[4026533023]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0  7月 15 10:42 uts -> 'uts:[4026532965]'
Namespace 作用 效果
pid 进程ID隔离 容器内进程从PID=1开始,容器内看不到宿主机进程
mnt 挂载点隔离 容器有自己的根目录文件系统
net 网络栈隔离 容器有独立的网卡、IP和端口空间
UTS 主机名/域名隔离 容器可以使用自己的hostname,不影响宿主机
IPC 进程间通信隔离 信号量、消息队列互不干扰
user 用户/组ID隔离 容器内的root映射到宿主机的普通用户
cgroup cgroup视图隔离 容器只能看到自己的资源限制
time 时间视图隔离 容器内的 /proc/uptime 可能不同于宿主(墙上时钟 date 不受影响)

2. Cgroups(控制组)— 「我能用多少资源」

Namespace 让你看不见别人,Cgroups 让你伤害不到别人。

没有 Cgroups 时,一个容器的内存泄漏就能吃光宿主机内存,导致其他容器被 OOM Killer 随机杀掉——这不叫隔离。Cgroups 在资源层面围了一个围栏:

  • CPU:限制容器能用几个核(--cpus=2)
  • 内存:硬限制 + OOM 只杀自己容器内的进程(-m 300MB)
  • 磁盘 I/O:限制读写速率
  • 进程数:限制容器内能 fork 的进程数上限

有了 Cgroups,一个容器资源超限只会影响自己,不会连累其他容器。详细操作见第三节。


3. UnionFS / OverlayFS(联合文件系统)— 「我的文件在哪」

Docker 镜像由只读层堆叠而成,容器启动时在最上面加一个可写层:

┌──────────────────────┐
│ 容器层(可写)         │  ← 修改的文件都写在这里
├──────────────────────┤
│ 层3 COPY index.html  │  ← 只读
│ 层2 apt install nginx│  ← 只读
│ 层1 ubuntu:22.04     │  ← 只读
└──────────────────────┘

写时复制(Copy-on-Write):修改只读层的文件时,系统先把文件拷贝到容器层再修改,原文件在只读层保持不变。

删除:在容器层创建一个"已删除"标记,只读层的原文件不受影响。

带来的好处:

  • 镜像复用,省磁盘:10 个容器共享同一份只读层,各自只有薄薄的可写层
  • 快速启动:新容器不需要复制整个文件系统
  • 删容器不影响镜像:删除可写层,镜像毫发无伤

4. Capabilities + Seccomp — 「即使 root 也不是真正的 root」

容器里的 root 能做什么,不能做什么?

Linux 传统上 root(UID=0)拥有全部权限。Docker 把 root 的权力拆成了 40+ 个小开关(Capabilities),默认只保留最安全的 ~14 个,其余全部拿走:

能力 能做什么 Docker 默认
CAP_SYS_ADMIN 挂载文件系统、修改内核参数 ❌ 拿走
CAP_SYS_MODULE 加载/卸载内核模块 ❌ 拿走
CAP_SYS_TIME 修改系统时间 ❌ 拿走
CAP_SYS_BOOT 重启机器 ❌ 拿走
CAP_NET_RAW 使用原始套接字(如 ping) ✅ 保留
CAP_NET_BIND_SERVICE 绑定 1024 以下的端口 ✅ 保留
CAP_CHOWN 修改文件所有者 ✅ 保留

Seccomp 在系统调用层面再加一层拦截——就算某个 Capability 保留了,它底层对应的危险系统调用也会被 Seccomp 过滤掉。Docker 默认禁用了约 44 个系统调用(如 reboot、kexec_load)。

所以:默认情况下容器 root 就是宿主机 root(UID 都是 0),但权力被 Capabilities 和 Seccomp 大幅裁剪了。如果再加上 User Namespace 映射(--userns-remap),则容器 UID=0 映射到宿主机普通用户 UID=65536,连 root 身份都不是了。

三. Docker的资源限制

1. 设置内存限制

a. 只设置内存

-m或--memory: 设置内存的使用限额,比如100MB,2GB

[root@4 ~]# docker run -itd --name test01 -m 300MB httpd:latest
b58697d745eaec4e06343e57f256d0502ad7d907e9ce1eff5b56008c48942bd6
[root@4 ~]# docker run -itd --name test02 -m 500MB httpd:latest
496e3b2375b76cdac050405ca3ce7e7134fe73d15b1a219564b517f3f7cfc1b7
[root@4 ~]# docker stats
CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT   MEM %     NET I/O         BLOCK I/O        PIDS
496e3b2375b7   test02    0.00%     12.55MiB / 500MiB   2.51%     936B / 126B     0B / 4.1kB       82
b58697d745ea   test01    0.00%     18.14MiB / 300MiB   6.05%     1.28kB / 126B   13.4MB / 4.1kB   82

b. 设置内存和swap

--memory-swap: 设置内存+swap的使用限额。

[root@4 ~]# docker run -itd --name test03 -m 300MB --memory-swap 400MB httpd:latest
688c0d219fe2855c139ec8492ce021fff70bed623594d31ba4d2a6e4644823b0
[root@4 ~]# docker run -itd --name test04 -m 200MB --memory-swap 500MB httpd:latest
0c12d904f1a7ebe4197152547c80b1e83bec6a64f5aff81b74e8bffe436bed64
[root@4 ~]# docker inspect test03 | grep -E "Memory|MemorySwap"
            "Memory": 314572800,
            "MemoryReservation": 0,
            "MemorySwap": 419430400,
            "MemorySwappiness": null,
[root@4 ~]# docker inspect test04 | grep -E "Memory|MemorySwap"
            "Memory": 209715200,
            "MemoryReservation": 0,
            "MemorySwap": 524288000,
            "MemorySwappiness": null,

注: 设置--memory-swap必须和--memory一起设置,不设置--memory而只设置--memory-swap会报错。

--memory设置的是内存限制,--memory-swap设置的是内存和swap的总量,后者一定要大于前者。

[root@4 ~]# docker stop $(docker ps -a -q)
0c12d904f1a7
688c0d219fe2
496e3b2375b7
b58697d745ea
[root@4 ~]# docker rm $(docker ps -a -q)
0c12d904f1a7
688c0d219fe2
496e3b2375b7
b58697d745ea

2. 设置CPU限制

a. 对CPU份额的控制

一般来说,我们设置CPU限制,设置的都是CPU份额控制,用-c或--cpu-shares控制

--cpu-shares 的默认值是 1024,它是一个相对权重而非绝对配额。当多个容器争抢 CPU 时,按各自权重比例分配;没有竞争时,单个容器可以用满所有 CPU。

例如a分配1024,b分配512。那么他们争抢CPU时的占用时间比例就是2比1

[root@4 ~]# docker run -itd --name "cont-A" --rm -c 1024 httpd:latest
d172bbc02a03b6c157beb42eb07d8025c36b22af4e85c6a36224b5abacb3a483
[root@4 ~]# docker run -itd --name "cont-B" --rm -c 512 httpd:latest
7f54eb2f04295efcb083b40694244cfa6f9eda989116bb1e7e3821241524c472
[root@4 ~]# docker inspect cont-A cont-B --format='容器: {{.Name}} , CPU份额: {{.HostConfig.CpuShares}}'
容器: /cont-A , CPU份额: 1024
容器: /cont-B , CPU份额: 512
# 我们也可以用docker stats来查看他们的相对比率

b. 对具体CPU核的控制

--cpuset-cpus=""

我们首先要查看CPU的核心数,CPU的核心从0开始计数,0,1代表有两核。0,1,2,3代表CPU有四核

[root@4 ~]# lscpu|grep NUMA
NUMA 节点:                           1
NUMA 节点0 CPU:                      0,1
[root@4 ~]# docker run -itd --name 'cont' --cpuset-cpus="1,0" busybox:latest 
6faa140adcde55331c028ffa8b2af9733a84e736038112bdcba369b2127634d4

表示容器可以在cpu0和1这两核上运行

c. 对CPU核心数的限制

--cpus=

例如--cpus=2就表示容器最多使用两个CPU,也可以用小数,比如--cpus=1.5

[root@4 ~]# docker run -itd --name "cont-c" --rm --cpus=1.5 busybox:latest
[root@4 ~]# docker exec -d cont-c sh -c "while :; do :; done & while :; do :; done &"
[root@4 ~]# docker stats cont-c --no-stream
CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O    PIDS
a98e546d67cd   cont-c    151.85%   1.398MiB / 1.698GiB   0.08%     876B / 126B   131kB / 0B   3

d. 绝对的CPU周期控制

--cpu-period或--cpu-quota单位为微秒us。1000us=1ms 1000ms=1s

  • --cpu-period(时间周期/尺子):规定“多久算一个结算周期”。单位是微秒(μs)。默认值是 100000(即 0.1 秒)。
  • --cpu-quota(时间配额/预算):规定“在每个周期里,最多能跑多长时间”。单位也是微秒。
[root@4 ~]# docker run -itd --name mongo1 --rm --cpuset-cpus 1 --cpu-quota=5000 --cpu-period=10000 busybox:latest
        docker run -itd --name mongo2 --rm --cpuset-cpus 1 --cpu-quota=5000 --cpu-period=50000 busybox:latest
9ca74ceac630a32dc07e94687841e3072e0b0365c3c3891ca69d6f72b4b075b2
dfb2d0c2fbc19e095683f52b09b4a9bbf04d59c3153647d449b1846ccc556b98
[root@4 ~]# docker exec -d mongo1 sh -c "while :; do :; done & while :; do :; done &"
docker exec -d mongo2 sh -c "while :; do :; done & while :; do :; done &"
[root@4 ~]# docker stats --no-stream
CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O   PIDS
dfb2d0c2fbc1   mongo2    10.17%    1.328MiB / 1.698GiB   0.08%     740B / 126B   0B / 0B     3
9ca74ceac630   mongo1    49.92%    1.328MiB / 1.698GiB   0.08%     1kB / 126B    0B / 0B     3
[root@4 ~]# docker stats --no-stream
CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O   PIDS
dfb2d0c2fbc1   mongo2    9.99%     1.328MiB / 1.698GiB   0.08%     880B / 126B     0B / 0B     3
9ca74ceac630   mongo1    50.09%    1.328MiB / 1.698GiB   0.08%     1.14kB / 126B   0B / 0B     3

docker实际占用CPU资源时间为--cpu-quota除以--cpu-period,因此我的mongo2占用时间比率约为百分之10,而mongo1为百分之50

e. 小结

--cpu-shares → 相对权重,竞争时生效(软限制)

--cpus → 硬上限,绝对限制

--cpuset-cpus → 绑核,限定只能跑在哪些核上

--cpu-quota/period → 精确到微秒的CPU时间控制

四. 使用export和import命令制作镜像

1. 先启动一个镜像

[root@4 ~]# docker run -d --name test busybox:latest sleep 99999999
caa6c145f9137d126c3ade05f9c1f72406c0ad0ec79d32240c79c205dab47142

2. 进入镜像,然后创建两个目录

[root@4 ~]# docker exec -it test sh
/ # ls
bin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var
/ # mkdir /abc /test
/ # ls
abc    bin    dev    etc    home   lib    lib64  proc   root   sys    test   tmp    usr    var
/ # exit

3. 导出容器文件

[root@4 ~]# docker export test>test-busybox.tgz

4. 利用导出的文件制作成新镜像

[root@4 ~]# docker import test-busybox.tgz 123:import
sha256:4fd901188fc746f76f03053c6a96353c9f3670d6fac7621da67328a3d5a97278

5. 利用新镜像制作容器

[root@4 ~]# docker run -it --rm 123:import sh
/ #