深度解析 Docker 技术内幕:从镜像构建、企业级仓库到 Namespace 与 CGroup 底层实战
深度解析 Docker 技术内幕:从镜像构建、企业级仓库到 Namespace 与 CGroup 底层实战
引言
在云原生时代,Docker 已经彻底改变了软件交付的形态。很多开发者每天都在敲着 docker run 和 docker build,但对其背后的运行逻辑却一知半解。
为了真正掌握这项技术,我们不仅要学会怎么写 Dockerfile,还要懂得如何搭建企业级的高可用仓库(如 Harbor),更要潜下去探究 Linux 内核是如何用 Namespace(命名空间)和 CGroup(控制组)为容器编织出一个完美的“楚门的世界”。
本文基于详尽的实战演练,由浅入深,带您全方位吃透 Docker 的核心技术栈。
第一章:万剑归宗 —— Docker 镜像的底层逻辑与自定义构建
1.1 镜像的基石:rootfs 与 bootfs
要理解 Docker 镜像,首先要回归 Linux 操作系统的本质。Linux 操作系统在宏观上分为两部分:内核空间 (Kernel Space) 和 用户空间 (User Space)。
- bootfs (Boot File System):包含 Bootloader 和 Kernel。Linux 刚启动时会加载 bootfs,加载完毕后,bootfs 会被卸载。
- rootfs (Root File System):包含我们熟悉的
/dev,/proc,/bin,/etc等目录。它是用户空间的文件系统。
Docker 镜像的核心秘密在于:所有的底层镜像(Base Image)直接共用宿主机(Host)的 Kernel,自己只需要提供最基础的 rootfs 即可。
这也是为什么 Ubuntu 镜像和 CentOS 镜像的区别,主要在于它们的软件包管理工具(apt vs yum)、系统库版本等用户空间上的差异,而非内核的差异。这也意味着,在容器中是无法升级或修改内核的,如果您的应用强依赖特定版本的内核,建议使用传统的虚拟机而非容器。
1.2 镜像的“洋葱”模型:分层结构与写时复制 (Copy-on-Write)
Docker Hub 中 99% 的镜像,都是通过在基础镜像上安装和配置软件一层层叠加出来的。
为什么 Docker 镜像要采用这种分层结构?最大好处是:共享资源。
如果有多个镜像都基于 Debian 构建,宿主机磁盘上只需要保存一份 Debian 的基础层。更为神奇的是,无论多少个容器共享这个基础镜像,任何对容器的修改,都不会影响到基础镜像,也不会影响其他容器。
这得益于 Linux 的联合文件系统 (UnionFS) 和 写时复制 (Copy-on-Write) 机制:
- 只读层 (镜像层):所有的镜像层都是只读的。
- 可写层 (容器层):容器启动时,会在最顶层叠加一个可写层。
- 读文件:从上往下依次查找,找到打开读入内存。
- 改文件:如果修改的文件在只读层,Docker 会将其复制到容器层,修改副本,底层的原文件依然保持原样。
- 删文件:在容器层记录下删除操作(whiteout),底层文件被隐藏。
这种设计保证了镜像被完美保护,实现了“一次构建,到处运行”的基础。
1.3 黑盒构建:docker commit (仅限应急)
最简单直接的镜像定制方式,就是基于一个运行中的容器,把它当前的状态“固化成”镜像。
bash
# 拉取基础镜像并启动
docker run -it centos:7
# 进入容器后定制环境
curl -s -o /etc/yum.repos.d/CentOS-Base.repo https://laoma.cloud/class/course-materials/softwares/stage01/repos/CentOS-Base.repo
yum install -y initscripts openssh-server
/usr/sbin/sshd-keygen
sed -i '/UseDNS/cUseDNS no' /etc/ssh/sshd_config
echo "root:redhat" | chpasswd
/usr/sbin/sshd -D &
# 在宿主机上(或者另开终端)执行 commit
docker commit 6e2b17aca785 centos7:sshd
⚠️ 避坑警告:docker commit 属于“黑盒”操作,别人拿到这个镜像,根本不知道里面运行了什么命令、安装了哪些包。因此,在正规的生产环境中,严禁使用 commit 构建镜像,必须使用 Dockerfile。
1.4 白盒构建:Dockerfile 核心指令精讲
Dockerfile 是镜像的源代码,分为四个部分:基础镜像信息、维护者信息、镜像操作指令、容器启动时执行指令。
FROM:必须是第一条指令,指定基础镜像(如FROM centos:7)。MAINTAINER:维护者信息(现在推荐用LABEL)。RUN:构建镜像时执行,每条RUN都会生成新的一层。为了减少镜像体积,尽量用&&将多个命令合并到一个RUN中执行。EXPOSE:声明容器运行的端口(仅是声明,不自动映射)。ENV:设置环境变量。WORKDIR:设置工作目录。COPY/ADD:复制文件。COPY只能复制本地文件,ADD还会自动解压 tar 包并支持远程 URL。推荐使用COPY,意图更明确。
核心辨析:RUN vs CMD vs ENTRYPOINT
这是面试和实战中最容易踩坑的三剑客:
- RUN:
docker build时执行。用于安装软件。 - CMD:
docker run时执行。指定启动容器时默认的命令。如果用户在docker run后面追加了其他命令,CMD 会被完全覆盖! 如果 Dockerfile 中有多个 CMD,只有最后一个生效。 - ENTRYPOINT:
docker run时执行。指定镜像的入口命令。它不会被docker run后面跟的命令覆盖(除非使用--entrypoint),而是将docker run后面的参数作为该命令的参数。
最佳实践组合:
dockerfile
ENTRYPOINT ["nginx", "-g", "daemon off;"]
CMD ["--help"]
当你 docker run mynginx -v 时,实际执行的是 nginx -g "daemon off;" -v。
格式建议:CMD 和 ENTRYPOINT 强烈推荐使用 Exec 格式(JSON 数组格式),这样不会启动子 shell,能够正确接收像
SIGTERM这样的信号,并且在镜像层级中只占有 0B(不生成新层)。
第二章:帝国崛起 —— 从公有云到企业级私有仓库 Harbor
Docker 仓库(Registry)是集中存放镜像的地方。公共仓库以 Docker Hub 为主,国内则有华为云 SWR、阿里云 ACR 等。
2.1 公有云仓库实战
以华为云 SWR 为例,镜像名称需严格遵循格式:swr.{区域代码}.myhuaweicloud.com/{组织名称}/{镜像名称}:{标签}。
bash
# 1. 登录(注意 -p 密码可以在控制台生成临时凭据)
docker login -u cn-east-3@HST3WYM45ZQPF42HAS3W -p b5aaa9f355d108fcbe1beb3e69dd4426cac47928b107ddb427e77ac0275ef253 swr.cn-east-3.myhuaweicloud.com
# 2. 打标签并上传
docker tag centos:8 swr.cn-east-3.myhuaweicloud.com/laoma/os/centos:8
docker push swr.cn-east-3.myhuaweicloud.com/laoma/os/centos:8
2.2 个人私有仓库:Registry
通过官方的 registry 镜像,我们可以快速搭建一个本地私有仓库:
bash
docker run -d -p 80:5000 --restart=always --name registry -v /registry:/var/lib/registry registry
⚠️ 核心坑点:Docker 1.9 之后默认使用 HTTPS 上传镜像。如果我们在内网使用 HTTP,必须在客户端(如 /etc/docker/daemon.json)添加 "insecure-registries": ["registry.laoma.cloud"] 并重启 Docker 服务,同时配置本地 DNS 解析。配置完毕后,通过 docker tag 和 docker push 即可推送到此私服。
2.3 企业级私有仓库:Harbor 部署与架构
Registry 只是一个没有界面的裸服务,缺乏权限管理和镜像扫描功能。企业级方案首选 Harbor。
部署 Harbor(离线版)
Harbor 极度依赖 hostname 解析。部署流程如下:
- 下载并解压
harbor-offline-installer。 - 复制
harbor.yml.tmpl为harbor.yml。 - 修改配置:
- 修改
hostname。 - 修改管理员密码
harbor_admin_password。 - 实验环境可以注释掉所有
https相关的配置,强制使用 80 端口。
- 修改
- 执行
./install.sh进行安装。
安装完成后,它会拉起十几个容器(Nginx, Harbor-core, PostgreSQL, Redis, Registry 等)。
配置为 Systemd 服务
由于 Harbor 是一堆容器的组合,重启 Docker 服务可能会导致 Harbor 崩溃。我们需要将其配置为系统服务,以便开机自启:
ini
[Unit]
Description=Harbor Docker Registry
After=docker.service network-online.target
Requires=docker.service network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/etc/harbor
ExecStart=/usr/libexec/docker/cli-plugins/docker-compose up -d
ExecStop=/usr/libexec/docker/cli-plugins/docker-compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
Harbor 的高级功能
- 权限控制:支持创建项目、公开/私有项目、用户角色分配(如访客、开发者、项目管理员)。
- 日志审计:完整记录所有操作。
- 远程复制:支持多数据中心之间的镜像同步。通过配置“仓库管理”(目标端地址)和“复制规则”(如 Push-based 或 Pull-based,手动/定时/事件驱动触发),实现两地三中心的镜像灾备。
第三章:降维打击 —— 揭秘 Docker 底层技术 (Namespace & CGroup)
很多人把容器比作“轻量级虚拟机”,但这其实是一种误解。容器本质上只是宿主机上受到严格限制的普通进程。 它是如何伪装成独立操作系统的?答案是 Linux 内核的 Namespace 和 CGroup。
3.1 Namespace:障眼法大师,负责“看什么”
Namespace 负责资源隔离(就像把不同班级的学生隔开,各自拥有独立的学号)。
| 命名空间类型 | 作用 |
|---|---|
| PID | 隔离进程号,容器内有自己独立的 PID 1 (init) 进程。 |
| NET | 隔离网络栈,容器有自己的网卡、IP、端口。 |
| MNT | 隔离挂载点,让容器看到独立的文件系统。 |
| UTS | 隔离主机名和域名。 |
| IPC | 隔离信号量、消息队列等进程间通信资源。 |
| USER | 隔离用户和用户组,可以在容器内使用 root,但在宿主机上是普通用户。 |
💡 手撸容器网络实验:
日常中 Docker 会自动帮你配置 veth pair 和网桥,为了真正理解原理,我们可以尝试手动为容器(PID为 9897)配置一个网络:
bash
# 1. 为容器进程创建网络命名空间
ln -s /proc/9897/ns/net /var/run/netns/9897
# 2. 创建一对虚拟网卡 veth pair(A和B)
ip link add A type veth peer name B
# 3. 将 A 绑定到 docker0 网桥
brctl addif docker0 A
ip link set A up
# 4. 将 B 放入容器的命名空间,重命名为 eth0,配置IP并启用
ip link set B netns 9897
ip netns exec 9897 ip link set dev B name eth0
ip netns exec 9897 ip link set eth0 up
ip netns exec 9897 ip addr add 172.17.0.99/16 dev eth0
# 5. 配置默认网关
ip netns exec 9897 ip route add default via 172.17.0.1
此时去容器里执行 ping 1.1.1.1,神奇地发现网络通了。这就是 Docker 网络底层的全部秘密!
3.2 CGroup:铁面无私的管教,负责“用多少”
如果 Namespace 是让进程“以为自己独自存在”,那 CGroup (Control Groups) 就是防止它“胡作非为”。CGroup 可以限制进程组的资源使用。
内存限制
Docker 通过 -m 或 --memory 限制物理内存,通过 --memory-swap 限制内存+Swap 总量。
实验:使用 progrium/stress 压测镜像。
bash
# 限制内存 200M,压测申请 180M,正常运行
docker container run -it -m 200M progrium/stress --vm 1 --vm-bytes 180M
# 限制内存 200M,压测申请 210M,容器直接 OOM 被 Kill
docker container run -it -m 200M progrium/stress --vm 1 --vm-bytes 210M
注意:如果没有限制内存,stress 会吞掉宿主机全部可用物理内存。
CPU 限制
--cpus 0.5:直接限制容器只能使用 0.5 个 CPU 核心(相当于 50% 的单核算力)。-c 或 --cpu-shares:设置 CPU 权重。默认是 1024。如果启动了 A (-c 1024) 和 B (-c 512),当 CPU 资源极其紧张时,A 得到的算力是 B 的两倍。但如果 A 空闲,B 依然可以占满所有 CPU。 权重只有在资源竞争时生效。
磁盘限制
可以通过 --device-write-bps /dev/sda:30MB 限制容器对磁盘的写入速率。
实验:
bash
docker run -it --rm --device-write-bps /dev/sda:30MB centos
# 在容器内用 dd 写入数据,观察速度被限制在 30MB/s 左右
dd if=/dev/zero of=test.io bs=1M count=2048 oflag=direct
总结
从一台裸机到繁荣的容器生态,Docker 的精妙之处在于:“用户态极简,内核态极深”。
我们通过 Dockerfile 像搭积木一样构建镜像,利用联合文件系统实现了极致的存储复用;通过 Harbor 构建了坚固的企业级镜像分发中心;而支撑这一切的,不过是 Linux 内核中原本就有的 Namespace(隔离视野)与 CGroup(限制资源)两大机制,再加上了联合挂载技术。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)