"在我电脑能跑"问题的根源
这是每个开发者都遇到过的经典困境:你的代码在本机运行完美,但部署到测试或生产服务器后就出错了。根源在于两个方面:
依赖版本不同
本机装的是 Node.js 18,服务器上是 Node.js 14。本机是 Python 3.11,服务器是 Python 3.8。第三方库版本、系统库版本都可能不同。
环境配置不同
环境变量、配置文件路径、文件权限、端口占用情况、操作系统版本(甚至 macOS vs Linux 的行为差异)。
传统的解决方案是编写长长的"部署文档",列出所有需要安装的软件和配置步骤。但这种方式脆弱且不可靠——Docker 的出现彻底改变了这一切。
虚拟机 vs 容器
容器不是第一个解决环境隔离问题的技术,虚拟机(VM)更早。了解二者的区别有助于理解容器的价值。
虚拟机架构
物理硬件 (CPU/内存/磁盘)
宿主操作系统 + Hypervisor
Guest OS 1(完整 Linux/Windows)
应用依赖库
应用程序 A
容器架构
物理硬件 (CPU/内存/磁盘)
宿主操作系统内核(共享)
容器运行时(Docker Engine)
应用依赖库(隔离)
应用程序 A
| 对比维度 | 虚拟机 | 容器 |
|---|---|---|
| 隔离级别 | 完整操作系统隔离 | 进程级隔离(共享宿主内核) |
| 启动时间 | 分钟级(需启动完整 OS) | 秒级甚至毫秒级 |
| 占用空间 | 几 GB(含完整 OS) | 几十 MB(仅含应用依赖) |
| 性能开销 | 较大(硬件虚拟化) | 极小(近乎原生) |
| 密度 | 一台宿主机跑数十个 VM | 一台宿主机跑数百个容器 |
| 安全隔离 | 更强(独立内核) | 较强(共享内核,有逃逸风险) |
| 适用场景 | 不同 OS、强安全隔离 | 微服务、快速部署、CI/CD |
它们并不互斥 — 在实际生产中,通常在虚拟机(云服务器)上运行容器。容器提供应用级隔离,虚拟机提供底层安全隔离。
容器技术的底层:Linux 特性
Docker 本身并没有发明什么新技术,它是对 Linux 内核若干已有特性的优雅封装和工具化。
Linux Namespace(命名空间)
Namespace 让每个容器"认为"自己拥有整个系统。Linux 提供多种命名空间:
- PID Namespace:容器内进程有独立的进程 ID 树,容器内的 PID 1 实际上是宿主机上某个普通进程
- Network Namespace:每个容器有独立的网络接口、路由表、端口空间
- Mount Namespace:每个容器有独立的文件系统视图,看不到宿主机的文件
- UTS Namespace:每个容器有独立的主机名(hostname)
- IPC Namespace:隔离进程间通信资源(信号量、消息队列等)
- User Namespace:容器内的 root 用户映射到宿主机的普通用户(rootless 容器)
Control Groups / cgroups(控制组)
Namespace 解决了"看到什么"的问题,cgroups 解决"能用多少"的问题。它限制并统计容器的资源使用:
- CPU:限制容器最多使用多少 CPU 时间(如
--cpus=0.5) - 内存:限制内存上限,超出会被 OOM Kill(如
--memory=512m) - I/O:限制磁盘读写速率
- 网络:流量整形(需要额外工具)
Union File System(联合文件系统)
容器镜像的分层存储依赖联合文件系统。Docker 默认使用 overlay2 驱动:
镜像基础层(只读)— FROM ubuntu:22.04
镜像层 2(只读)— RUN apt-get install curl
镜像层 3(只读)— COPY app /app
容器可写层(运行时创建,删除容器则消失)
多个容器可以共享同一个只读镜像层,节省磁盘空间,同时写时复制(Copy-on-Write)机制确保各容器的修改互不影响。
Docker 核心概念
- Image(镜像) 只读模板,包含应用运行所需的一切(代码、运行时、依赖、配置)。由 Dockerfile 构建而来,可在不同机器间分发。可类比为面向对象的"类"。
- Container(容器) 镜像的运行实例。在镜像只读层之上加一个可写层,运行时可在其中读写文件。容器删除后可写层消失。可类比为"类的实例"。
- Registry(仓库) 存储和分发镜像的服务。Docker Hub 是默认的公共 Registry;也可以自建私有 Registry(Harbor、registry:2)。
- Dockerfile 构建镜像的脚本文件,包含一系列指令(FROM、RUN、COPY 等),每条指令创建一个新的镜像层。
- Docker Engine Docker 的核心运行时,包含 dockerd(守护进程)、containerd、runc 等组件。负责管理镜像、容器、网络、存储。
- Docker Hub Docker 官方的公共镜像仓库(hub.docker.com),提供大量官方镜像(nginx、postgres、node 等)和社区镜像。
- Docker Compose 用于定义和运行多容器应用的工具,通过 YAML 文件描述多个服务的配置关系。
Docker 架构:Client-Server 模式
Docker 采用 Client-Server 架构,各组件职责清晰:
# Docker 架构调用链
docker CLI(客户端)
│ 通过 Unix Socket / TCP 发送 REST API 请求
▼
dockerd(Docker Daemon)
│ 高层管理:镜像、容器、网络、Volume
▼
containerd(OCI 容器运行时管理器)
│ 管理容器的完整生命周期
▼
containerd-shim
│ 进程监管,与 containerd 解耦
▼
runc(OCI 容器运行时)
│ 实际调用 Linux Namespace + cgroups
▼
Linux 内核(Namespace / cgroups / overlayfs)
这种分层架构的好处是:即使 dockerd 重启,正在运行的容器(containerd 管理)不会受影响。
Docker Desktop vs Docker Engine
Docker Desktop(Mac / Windows)
包含一个轻量 Linux 虚拟机(因为 Docker 依赖 Linux 内核),GUI 管理界面,自带 Docker Compose 和 Kubernetes。适合开发环境使用,免费用于个人使用。
Docker Engine(Linux 服务器)
纯命令行,直接运行在 Linux 内核上(无需虚拟机),性能最佳。生产服务器、CI/CD 环境的标准选择,完全开源免费。
镜像分层原理
理解分层是理解 Docker 性能和缓存机制的关键:
# 每条 Dockerfile 指令创建一层
FROM ubuntu:22.04 # 层 1(从 Registry 拉取)
RUN apt-get update # 层 2(新增文件)
RUN apt-get install -y curl # 层 3(新增文件)
COPY ./app /app # 层 4(新增文件)
CMD ["./app"] # 层 5(仅元数据,不创建文件层)
- 所有镜像层都是只读的,被多个镜像/容器共享
- 运行容器时,Docker 在只读层上叠加一个可写层(Container Layer)
- 容器被删除时,可写层消失;通过
docker commit可把可写层保存为新镜像层 - 修改已有文件时使用写时复制(Copy-on-Write):先从只读层复制文件到可写层,再修改
本章小结 — 容器通过 Linux Namespace 隔离进程视图,通过 cgroups 限制资源,通过 overlay2 共享文件系统层。Docker 是这些技术的工具化封装,让容器变得易于使用和分发。