作者介绍

本文作者系资深底层技术爱好者与实战派架构师,WWAIC(全周 AI 编程)范式创始人,专注计算机体系结构、操作系统内核、Java 虚拟机实现原理与自研框架落地。本文手撕容器底层:不讲空话,只从 CPU 指令 → 操作系统 → 进程 → Namespace → Cgroups 这条铁链,彻底讲透 Docker 是什么、凭什么能隔离、和普通进程到底有什么区别。


本文你能得到什么?

读完本文,你将彻底搞懂:

  • Docker 隔离的本质:不是虚拟机,而是依赖 Linux 内核的 Namespace 和 Cgroups 技术,把普通进程"包装"成容器

  • CPU 特权级与系统调用:为什么用户程序不能随便访问硬件,必须通过内核

  • 6 大 Namespace 的作用:PID、Network、Mount、UTS、IPC、User 各自隔离什么

  • 镜像分层存储的原理:为什么 Docker 镜像要分层、为什么需要系统层

  • Docker 与虚拟机的本质区别:共享内核 vs 独立内核,轻量 vs 重量级

  • 离线场景下 Docker 镜像的迁移与运行

  • K8s 集群部署与 Pod 管理机制

一句话记住:Docker 容器 = 打了隔离标签 + 上了资源枷锁的普通进程


目录

  • 一、为什么 Docker 本质是依赖内核 Namespace 和 Cgroups 技术的普通进程?

  • 二、Docker 镜像为什么分层管理?为什么需要系统层?

  • 三、为什么打包一个 Java 程序镜像会包含系统依赖和 JDK 镜像?

  • 四、如何把一个开发环境的 Docker 镜像在离线场景下部署到生产环境?

  • 五、Load 镜像的时候为什么会分层处理?

  • 六、K8s 集群如何部署?默认是什么管理 Pod 节点、运行 Docker 镜像?


一、为什么 Docker 本质是依赖内核 Namespace 和 Cgroups 技术的普通进程?

1.1 从 CPU 指令集说起

CPU 只认机器指令。有一类叫特权指令(读写硬盘、改内存映射表),一旦用错,整个操作系统会直接宕机。Intel CPU 用 ring 0(内核态)和 ring 3(用户态)划分权限。普通程序要访问硬件,必须通过系统调用陷入内核,让内核代劳。

这一层奠定了"权限分级控制"的基础——用户程序无法为所欲为,必须经由内核。

1.2 进程的"虚拟地址空间"幻觉

操作系统给每个进程分配一个虚拟地址空间(32 位 Linux 为 4GB),通过 MMU 映射到真实物理内存。进程以为自己独占整台电脑,实际上只隔离了内存视图

但问题来了——其他资源(端口、文件、PID、主机名)默认完全不隔离。比如两个 Java 程序分别需要 Java 8 和 Java 11,如果直接跑在同一个宿主机上,环境变量、端口、文件路径都可能冲突。

1.3 Namespace:给进程"打标签"制造隔离视图

Docker 利用 Linux 内核的 6 大 Namespace 为进程制造"独占"的幻觉:

Namespace 隔离内容
PID 进程 ID 空间,容器内进程从 1 开始编号
Network 网络设备、IP 地址、端口、路由表
Mount 文件系统挂载点
UTS 主机名和域名
IPC 进程间通信资源(信号量、消息队列等)
User 用户和用户组 ID

每个 Namespace 就像给进程戴上了一副"特制眼镜"——进程看到的世界是被"篡改"过的视图。

1.4 Cgroups:给进程上"资源枷锁"

光有 Namespace 还不够。如果没有资源限制,一个容器进程可能把整个宿主机的 CPU 和内存吃光。Cgroups(Control Groups)就是 Linux 内核提供的"资源锁"机制,可以限制进程的 CPU、内存、磁盘 IO 等资源使用量。

1.5 结论

Docker 容器 = 内核的 Namespace(隔离视图)+ Cgroups(限制资源)+ UnionFS(分层镜像)

没有自己的操作系统内核,它就是宿主机上一个被打了"隔离标签"的普通进程。这就是为什么 Docker 比虚拟机轻量——虚拟机需要模拟完整硬件、运行独立内核,而 Docker 直接共享宿主机内核。


二、Docker 镜像为什么分层管理?为什么需要系统层?

2.1 分层存储的核心设计

Docker 镜像采用 UnionFS(联合文件系统) 技术,将多个目录层叠在一起,对外呈现为一个统一的文件系统视图。每一层只记录相对于上一层的文件变化(新增、修改、删除)。

2.2 分层的好处

① 复用与共享

假设你有 10 个 Java 微服务,都基于 openjdk:11 构建。如果没有分层,每个镜像都要完整打包一份 JDK(约 300MB),10 个镜像就是 3GB。有了分层,openjdk:11 这一层只需存储一份,所有镜像共享——大幅节省磁盘空间

② 构建加速

修改代码后重新构建镜像时,只需重建变化的那一层(通常是应用代码层),下面的系统层和 JDK 层直接从缓存复用,构建速度从分钟级降到秒级。

③ 网络传输优化

拉取镜像时,只下载本地没有的层。更新应用时只需下载新增的几 MB 代码层,而不是整个镜像。

2.3 为什么需要"系统层"?

系统层(Base Image) 是镜像的最底层,通常是一个精简的 Linux 发行版(如 Alpine、Ubuntu、CentOS)。为什么需要它?

  • 提供系统调用入口:容器内的程序虽然是跑在宿主机内核上,但程序本身依赖标准 C 库(glibc/musl)来发起系统调用。系统层提供了这些基础库。

  • 提供文件系统骨架/bin/etc/usr 等目录结构、基础命令(lscatsh)都来自系统层。

  • 依赖链传递:JDK 镜像本身也是基于某个系统层构建的,Java 程序依赖 glibc、动态链接器等,这些都必须由系统层提供。

一句话:系统层是容器内进程能"正常运转"的基石——没有它,连 /bin/sh 都跑不起来。


三、为什么打包一个 Java 程序镜像会包含系统依赖和 JDK 镜像?

3.1 Java 程序的运行依赖链

一个 Java 程序要运行,依赖链是这样的:

text

Java 程序(.class / .jar)
    ↓ 依赖
JVM(Java 虚拟机)
    ↓ 依赖
glibc / 动态链接器 / 系统库
    ↓ 依赖
Linux 内核(由宿主机提供)

3.2 为什么必须"打包进去"?

① JVM 必须打包进去

JVM 是 Java 程序运行的"容器",没有 JVM,字节码无法执行。所以 JDK/JRE 必须作为镜像的一部分打包进去。

② 系统依赖必须打包进去

JVM 本身是 C++ 写的,它依赖 glibc、libstdc++、动态链接器(ld-linux.so)等系统库。这些库来自操作系统层,不同发行版、不同版本的库可能不兼容。为了保证"一次构建,到处运行",必须把这些依赖也锁定在镜像里。

③ 隔离性要求

如果复用宿主机的 JDK,不同容器可能需要不同版本的 JDK(Java 8 vs Java 11 vs Java 17),必然冲突。把 JDK 打包进镜像,每个容器拥有自己独立的 JVM 环境。

3.3 Dockerfile 示例

dockerfile

FROM openjdk:11-jre-slim   # 系统层 + JDK 层
COPY app.jar /app/
CMD ["java", "-jar", "/app/app.jar"]

这条指令的每一层都在"打包依赖"——openjdk:11-jre-slim 已经包含了系统层和 JDK 层,应用层只放你自己的代码。


四、如何把一个开发环境的 Docker 镜像在离线场景下部署到生产环境?

4.1 核心思路:镜像导出 + 导入

离线场景下无法访问 Docker Hub 或私有镜像仓库,解决方案是在能联网的环境导出镜像为 tar 文件,然后拷贝到离线生产环境导入

4.2 步骤详解

步骤一:在开发/构建环境导出镜像

bash

# 查看本地镜像列表,确认要导出的镜像
docker images

# 导出镜像为 tar 文件
docker save -o myapp.tar myapp:latest

# 可选:压缩以节省传输空间
gzip myapp.tar   # 生成 myapp.tar.gz

步骤二:传输到生产环境

使用 U 盘、SCP、FTP 或其他离线传输方式,将 myapp.tar(或压缩包)拷贝到生产服务器。

步骤三:在生产环境导入镜像

bash

# 如果是压缩包,先解压
gunzip myapp.tar.gz

# 导入镜像
docker load -i myapp.tar

# 验证导入成功
docker images | grep myapp

步骤四:运行容器

bash

docker run -d -p 8080:8080 --name myapp myapp:latest

4.3 注意事项

注意点 说明
架构一致性 开发机和生产机的 CPU 架构必须一致(都是 x86_64 或都是 ARM),否则镜像无法运行
内核版本 宿主机内核版本不能低于镜像内程序要求的最低内核版本
依赖完整性 docker save 会完整保存所有层,确保依赖不丢失
镜像体积 大镜像传输耗时,建议使用多阶段构建或 Alpine 基础镜像减小体积

五、Load 镜像的时候为什么会分层处理?

5.1 分层加载的原理

执行 docker load -i myapp.tar 时,Docker 会解析 tar 文件中的每一层(Layer),并逐层加载到本地存储(/var/lib/docker/ 下)。

每个层都有一个唯一的 ChainID(基于层内容的哈希值),Docker 通过这个 ID 来识别和复用层。

5.2 为什么要分层加载?

① 去重与复用

假设你导入的镜像包含 5 层:系统层 → JDK 层 → 依赖层 → 配置层 → 代码层。如果本地已经存在相同 ChainID 的系统层和 JDK 层(来自其他镜像),Docker 直接跳过,只加载本地没有的层。

这就是为什么 docker load 时你会看到类似这样的输出:

text

Loaded image: myapp:latest
22a4f2b...: Loading layer [========================================>]  312MB/312MB
3b5c8d9...: Loading layer [========================================>]  45.6MB/45.6MB
7e9f1a2...: Layer already exists   ← 本地已有,跳过

② 存储效率

分层存储意味着相同的层在磁盘上只存一份。如果 10 个镜像共享同一个 Alpine 系统层,磁盘上只有一份 Alpine 层的数据,而不是 10 份。

③ 导入速度

已有层直接跳过,只需传输和写入新层,大幅缩短导入时间。

5.3 层与层的关系

每一层只记录相对于上一层的差异(新增文件、修改文件、删除文件)。加载时,Docker 按照顺序把这些层"叠加"起来,通过 UnionFS 呈现为一个完整的文件系统。删除操作在上一层用"白名单文件"标记,而不是真的从下层物理删除——这也是分层存储的巧妙之处。


六、K8s 集群如何部署?默认是什么管理 Pod 节点、运行 Docker 镜像?

6.1 K8s 集群部署方式

Kubernetes(K8s)集群的部署有多种方式,从简单到生产级:

部署方式 适用场景 特点
Minikube 本地开发/测试 单节点,一键启动
Kubeadm 生产环境标准部署 官方推荐,可定制,支持高可用
二进制部署 深度定制场景 最灵活,但最复杂
托管 K8s 云上生产 阿里云 ACK、腾讯云 TKE、AWS EKS,免运维控制面

Kubeadm 部署核心步骤(以 Ubuntu/CentOS 为例):

bash

# 1. 所有节点:安装 Docker 和 kubeadm/kubelet/kubectl
# 2. 初始化 Master 节点
kubeadm init --pod-network-cidr=10.244.0.0/16

# 3. 配置 kubectl
mkdir -p $HOME/.kube
cp /etc/kubernetes/admin.conf $HOME/.kube/config

# 4. 安装 Pod 网络插件(如 Flannel)
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

# 5. Worker 节点加入集群
kubeadm join <Master_IP>:6443 --token <token> --discovery-token-ca-cert-hash <hash>

6.2 默认管理 Pod 节点的组件

K8s 管理 Pod 的核心是 kubelet——运行在每个节点上的"代理人"。

默认调度器(kube-scheduler) 负责决定 Pod 放在哪个节点上运行,依据是资源请求、节点亲和性、污点/容忍等策略。

默认容器运行时是什么?

  • 早期 K8s 默认使用 Docker 作为容器运行时

  • 从 K8s 1.20 开始,官方宣布弃用 Docker 作为运行时(通过 dockershim)

  • 从 K8s 1.24 开始,彻底移除了 dockershim,默认不再支持 Docker

  • 现在的默认/推荐运行时是 containerd(Docker 本身也是基于 containerd 的)

注意:虽然 K8s 不再默认使用 Docker 作为运行时,但你构建的 Docker 镜像依然可以在 K8s 中运行——因为镜像格式是 OCI(Open Container Initiative)标准,containerd 同样可以拉取和运行 Docker 构建的镜像。

6.3 Pod 与 Docker 镜像的关系

text

K8s 集群
    ├── Master 节点(控制面)
    │   ├── kube-apiserver
    │   ├── kube-scheduler(默认调度器)
    │   └── kube-controller-manager
    └── Worker 节点
        ├── kubelet(节点代理人,管理 Pod 生命周期)
        └── 容器运行时(containerd / Docker)
            └── Pod
                └── 容器(基于 Docker 镜像运行)

Pod 是 K8s 的最小调度单元,一个 Pod 可以包含一个或多个容器。这些容器共享同一个 Network Namespace 和 IPC Namespace,可以互相通过 localhost 通信。

部署应用时,你编写 Deployment YAML,指定镜像地址(如 myapp:latest),kubelet 会调用容器运行时拉取并运行该镜像。


总结

问题 核心答案
Docker 本质是什么? 依赖 Namespace + Cgroups 的普通进程
镜像为什么分层? 复用、加速构建、节省存储
为什么需要系统层? 提供基础库和文件系统骨架
Java 镜像为何包含 JDK 和系统依赖? 依赖链传递 + 环境隔离
离线如何部署? docker save → 传输 → docker load
Load 为何分层? 去重复用、加快导入
K8s 默认如何管理 Pod? kubelet + kube-scheduler,运行时默认 containerd

如果本文对你有帮助,欢迎点赞、收藏、转发! 有任何疑问欢迎在评论区留言讨论~

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐