Docker入门与核心概念
本文是Docker专栏的开篇之作,将从容器化技术的历史演进出发,全面深入地讲解Docker的核心概念、架构原理、底层技术以及实际应用。无论你是刚接触容器技术的新手,还是有一定经验想要深入理解底层原理的开发者,这篇文章都将为你构建完整的Docker知识体系。全文超过3万字,力求做到既有宏观视角的把控,又有微观细节的深入,帮助你真正理解"容器到底是什么"。
目录
- [第一章 容器化技术发展历程](#第一章 容器化技术发展历程)
- [第二章 什么是Docker](#第二章 什么是Docker)
- [第三章 容器 vs 虚拟机](#第三章 容器 vs 虚拟机)
- [第四章 Docker架构详解](#第四章 Docker架构详解)
- [第五章 Docker核心概念](#第五章 Docker核心概念)
- [第六章 Docker应用场景](#第六章 Docker应用场景)
- [第七章 第一个Docker容器](#第七章 第一个Docker容器)
- [第八章 Docker生态系统](#第八章 Docker生态系统)
- [第九章 容器化思想与DevOps](#第九章 容器化思想与DevOps)
- [第十章 常见问题与最佳实践](#第十章 常见问题与最佳实践)
第一章 容器化技术发展历程
要真正理解Docker,我们不能仅仅停留在"它是一个工具"的层面。任何一项技术的诞生都不是凭空而来的,它是对前人智慧的继承与发展,是对现实问题的回应。容器化技术同样如此------它有着超过四十年的技术积淀,经历了从chroot到LXC,再到Docker和Kubernetes的漫长演进。本章将带你穿越时光,梳理容器化技术的发展脉络,让你明白Docker为什么会出现在这个时代,以及它解决了前人没有解决好的哪些问题。
1.1 软件交付的演进(物理机→虚拟机→容器)
软件交付方式的演进,本质上是人类对计算资源利用率和管理效率不断追求的过程。我们可以将这个过程分为三个主要阶段:物理机时代、虚拟机时代和容器时代。
1.1.1 物理机时代:一机一应用
在计算机发展的早期,软件部署的方式非常直接------将应用程序直接安装在物理服务器上。一台物理服务器运行一个应用程序,这是最原始的部署模式。
┌─────────────────────────────────────┐
│ 物理服务器 │
│ ┌───────────────────────────────┐ │
│ │ 操作系统 (OS) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 应用程序 (App) │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
│ CPU / 内存 / 磁盘 / 网络 │
└─────────────────────────────────────┘
这种模式的问题显而易见:
- 资源利用率极低:一台服务器可能只用了5%~10%的CPU和内存,其余资源白白浪费。就好比你买了一栋别墅,却只在一个房间里住人,其他房间全部空置。
- 部署和维护成本高:每增加一个应用就需要采购新的物理服务器,从下单采购到上架通电,周期可能长达数周。
- 应用之间相互干扰:如果在一台物理机上部署多个应用,它们共享同一个操作系统,一个应用的内存泄漏可能导致另一个应用崩溃。就像几个陌生人合租一套房子,共用厨房和卫生间,一个人的不良习惯会影响所有人。
- 迁移困难:应用与底层硬件深度绑定,更换服务器意味着重新安装和配置所有依赖。
在物理机时代,运维人员最常听到的一句话就是:"在我的机器上是好的啊!"这句经典名言道出了物理机时代最大的痛点------环境不一致。
1.1.2 虚拟机时代:资源池化的开端
2001年,VMware推出了第一款商业化的x86虚拟化产品,标志着虚拟机时代的到来。虚拟化技术的核心思想是:在物理硬件之上引入一层虚拟化软件(Hypervisor),将物理资源抽象为虚拟资源,每台虚拟机都拥有自己完整的操作系统。
┌───────────────────────────────────────────┐
│ 物理服务器 │
│ ┌─────────────────────────────────────┐ │
│ │ Hypervisor (VMM) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ VM 1 │ │ VM 2 │ │ VM 3 │ │ │
│ │ │ OS │ │ OS │ │ OS │ │ │
│ │ │ App │ │ App │ │ App │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ └─────────────────────────────────────┘ │
│ CPU / 内存 / 磁盘 / 网络 │
└───────────────────────────────────────────┘
虚拟机带来了革命性的改变:
- 资源利用率大幅提升:一台物理服务器可以运行多台虚拟机,资源利用率可以从10%提升到60%~70%。相当于把别墅改成了公寓楼,每层都住满了人。
- 隔离性好:每个虚拟机都有独立的操作系统,应用之间完全隔离,互不影响。
- 易于迁移:虚拟机本质上是一组文件(磁盘镜像、配置文件),可以轻松地从一个物理机迁移到另一个物理机。VMware的vMotion技术甚至能在虚拟机运行状态下完成迁移。
- 快照与回滚:可以为虚拟机创建快照,出问题时一键回滚到之前的状态。
但虚拟机也有其固有的缺陷:
- 资源开销大:每个虚拟机都需要运行一个完整的操作系统(Guest OS),仅操作系统本身就要占用几个GB的内存和几十GB的磁盘空间。如果你在一台16GB内存的机器上运行10个虚拟机,光操作系统就要吃掉10~20GB内存,实际能用来跑应用的资源所剩无几。
- 启动缓慢:虚拟机的启动过程和物理机一样,需要经历BIOS自检、引导加载程序、内核初始化、用户空间启动等完整流程,通常需要几十秒到几分钟。
- 许可成本:每个虚拟机都需要一份操作系统许可证(Windows Server尤为明显),这在大规模部署时是一笔不小的开支。
打个比方,虚拟机就像是在一块空地上建了一栋栋独立的别墅,每栋别墅都有自己的地基(操作系统)、水电系统(系统服务)和房间(应用程序)。隔离性确实好,但每栋别墅的建设成本和维护成本都很高。
1.1.3 容器时代:轻量级隔离的突破
容器技术可以看作是虚拟化理念的进一步演进。它保留了虚拟化的隔离性优势,但摒弃了每个实例运行完整操作系统的重量级设计。容器之间共享宿主机的操作系统内核,只隔离进程和资源,因此极度轻量。
┌───────────────────────────────────────────┐
│ 物理服务器 │
│ ┌─────────────────────────────────────┐ │
│ │ 宿主机操作系统 (Host OS) │ │
│ │ │ 容器运行时 (Docker Engine) │ │
│ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │ │C 1 │ │C 2 │ │C 3 │ │C 4 │ │ │
│ │ │ │App │ │App │ │App │ │App │ │ │
│ │ │ │Bins│ │Bins│ │Bins│ │Bins│ │ │
│ │ │ └────┘ └────┘ └────┘ └────┘ │ │
│ └─────────────────────────────────────┘ │
│ CPU / 内存 / 磁盘 / 网络 │
└───────────────────────────────────────────┘
容器的优势:
- 极致轻量:一个容器通常只有几十MB,因为它不需要包含完整的操作系统,只需要应用本身及其依赖的二进制文件和库。
- 秒级启动:容器本质上是宿主机上的一个进程,启动容器就是启动一个进程,通常只需要几百毫秒到几秒。
- 高密度部署:由于资源开销极小,一台物理服务器上可以运行数百甚至上千个容器,远超虚拟机的部署密度。
- 环境一致性:开发、测试、生产环境使用相同的容器镜像,彻底解决了"在我的机器上能跑"的问题。
用同样的比喻来说,容器就像是在一栋大楼里划分出的独立办公室,每间办公室共享大楼的地基和水电系统(操作系统内核),但有自己的门锁(隔离)和家具(应用依赖)。建设一间办公室的成本远低于建一栋别墅,而且可以快速搬迁和扩建。
下面用一个表格来直观对比三种交付方式的差异:
| 特性 | 物理机 | 虚拟机 | 容器 |
|---|---|---|---|
| 隔离级别 | 无隔离 | 硬件级隔离 | 操作系统级隔离 |
| 资源开销 | 无额外开销 | 大(每个VM一个OS) | 小(共享内核) |
| 启动速度 | 分钟级 | 分钟级 | 秒级/毫秒级 |
| 资源利用率 | 5%~10% | 60%~70% | 80%~90% |
| 部署密度 | 1台/机 | 10~20台/机 | 100~1000台/机 |
| 环境一致性 | 差 | 一般 | 优秀 |
| 迁移难度 | 高 | 低 | 极低 |
| 适用场景 | 传统应用 | 混合负载 | 微服务/云原生 |
1.2 容器化技术的起源(chroot、LXC、cgroups、namespace)
很多人以为容器技术是Docker发明的,这是一个常见的误解。事实上,容器技术所依赖的Linux内核特性------namespace和cgroups------早在2000年代初就已经存在了。Docker的贡献在于将这些底层技术进行了优雅的封装,让普通开发者也能方便地使用容器。本节我们来追溯容器技术的几个关键基石。
1.2.1 chroot:容器隔离的鼻祖
1979年,chroot(Change Root)系统调用被引入Unix V7,这是容器隔离思想最早的雏形。chroot的作用是改变进程的根目录,使进程只能看到指定目录下的文件系统,无法访问该目录之外的任何文件。
bash
# 创建一个隔离的根目录环境
mkdir -p /opt/myroot/{bin,lib,lib64,usr,etc}
# 复制必要的程序和库到隔离目录
cp /bin/bash /opt/myroot/bin/
cp /lib/x86_64-linux-gnu/libc.so.6 /opt/myroot/lib/
cp /lib64/ld-linux-x86-64.so.2 /opt/myroot/lib64/
# 使用chroot进入隔离环境
chroot /opt/myroot /bin/bash
# 此时进程的根目录变为 /opt/myroot
# 它无法看到真实文件系统中的其他内容
chroot实现了一种基本的文件系统隔离------进程被"关"在了一个子目录中,看不到外面的世界。这就像在一个大仓库里划出一小块区域,用墙围起来,里面的人只能看到围墙内的东西。
但chroot的隔离非常有限:它只隔离了文件系统,没有隔离进程、网络、用户等资源。被chroot的进程仍然可以看到宿主机上的所有进程(通过/proc),仍然可以消耗所有CPU和内存,安全性很差。
尽管如此,chroot开创了"隔离"这一重要思想,为后续的容器技术奠定了概念基础。
1.2.2 namespace:全方位的视图隔离
Linux namespace是容器隔离的核心技术。它提供了一种机制,使得进程只能看到与自己相关的系统资源,从而实现不同维度的隔离。
Linux内核从2.4.19版本开始逐步引入各种namespace,到3.8版本基本完善。共有6种namespace:
| Namespace | 引入版本 | 隔离内容 | 作用说明 |
|---|---|---|---|
| PID | 2.6.24 | 进程ID | 进程只能看到自己namespace内的进程 |
| NET | 2.6.29 | 网络栈 | 独立的网络设备、IP地址、端口、路由表 |
| IPC | 2.6.19 | 进程间通信 | 独立的System V IPC和POSIX消息队列 |
| MNT | 2.4.19 | 挂载点 | 独立的文件系统挂载视图 |
| UTS | 2.6.19 | 主机名和域名 | 独立的hostname和domain name |
| USER | 3.8 | 用户和用户组 | 进程在namespace内外可以有不同的UID |
可以用一个简单的命令来查看当前进程的namespace:
bash
# 查看当前进程的namespace信息
ls -l /proc/$$/ns
# 输出示例:
# lrwxrwxrwx 1 root root 0 Aug 6 10:00 ipc -> ipc:[4026531839]
# lrwxrwxrwx 1 root root 0 Aug 6 10:00 mnt -> mnt:[4026531840]
# lrwxrwxrwx 1 root root 0 Aug 6 10:00 net -> net:[4026531956]
# lrwxrwxrwx 1 root root 0 Aug 6 10:00 pid -> pid:[4026531836]
# lrwxrwxrwx 1 root root 0 Aug 6 10:00 user -> user:[4026531837]
# lrwxrwxrwx 1 root root 0 Aug 6 10:00 uts -> uts:[4026531838]
每个namespace都有一个唯一的inode号,如果两个进程的某个namespace inode号相同,说明它们在同一个namespace中。
namespace的隔离效果可以这样理解:假设你住在一栋公寓楼里,PID namespace就像是让你只能看到自己家里的人,看不到邻居;NET namespace就像是每户人家有独立的门牌号和信箱;MNT namespace就像是每户人家看到的装修布局不一样。六种namespace组合在一起,就构成了一个相对完整的隔离环境。
1.2.3 cgroups:资源限制的利器
如果说namespace解决的是"能看到什么"的问题(视图隔离),那么cgroups(Control Groups)解决的就是"能用多少"的问题(资源限制)。
cgroups由Google工程师Paul Menage和Rohit Seth于2006年开发,最初叫"process containers",后来为了避免与"container"一词混淆,改名为cgroups。2008年,它被正式合并到Linux 2.6.24内核中。
cgroups可以限制、记录和隔离进程组使用的物理资源,主要包括:
| 子系统(Subsystem) | 功能说明 | 限制示例 |
|---|---|---|
| cpu | 限制CPU使用比例 | 分配50%的CPU时间片 |
| cpuset | 绑定CPU核心 | 只允许使用CPU0和CPU1 |
| cpuacct | 统计CPU使用量 | 记录进程组消耗的CPU时间 |
| memory | 限制内存使用量 | 最多使用512MB内存 |
| blkio | 限制块设备I/O | 限制磁盘读写带宽 |
| net_cls | 标记网络数据包 | 为流量打上classid标记 |
| devices | 控制设备访问 | 禁止访问/dev/sda |
| freezer | 暂停和恢复进程 | 冻结进程组中的所有进程 |
一个简单的cgroups使用示例:
bash
# 创建一个cgroup用于限制内存
mkdir /sys/fs/cgroup/memory/myapp
# 设置内存限制为512MB
echo 536870912 > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes
# 将进程PID为1234的进程加入这个cgroup
echo 1234 > /sys/fs/cgroup/memory/myapp/tasks
# 现在PID 1234的进程最多只能使用512MB内存
# 超过限制会被OOM Killer杀死
cgroups就像是给每个家庭分配了水电气额度:你这个月最多用500度电、50吨水,超出就会自动断供。这样就避免了一个"邻居"耗尽整栋楼的资源。
1.2.4 LXC:第一个真正的容器管理工具
有了namespace和cgroups这两大内核特性,理论上就可以手动创建容器了。但手动操作太复杂了,需要一种工具来简化这个过程,这就是LXC(Linux Containers)。
LXC由IBM于2008年发起,是第一个基于namespace和cgroups的容器管理工具。它提供了一组命令行工具和API,让用户可以方便地创建和管理容器。
bash
# 安装LXC
apt-get install lxc
# 创建一个Ubuntu容器
lxc-create -t ubuntu -n mycontainer
# 启动容器
lxc-start -n mycontainer -d
# 进入容器
lxc-attach -n mycontainer
# 查看运行中的容器
lxc-ls --active
# 停止容器
lxc-stop -n mycontainer
# 删除容器
lxc-destroy -n mycontainer
LXC虽然比手动操作namespace和cgroups方便了很多,但它仍然有一些不足:
- 使用门槛高:需要理解Linux系统管理的很多概念,对普通开发者不友好。
- 镜像管理薄弱:没有统一的镜像格式和分发机制,创建容器时需要在线安装操作系统模板。
- 可移植性差:容器与宿主机环境耦合较紧,迁移不便。
- 生态不完善:缺乏配套的工具链,如编排、网络、存储等。
这些问题恰恰是Docker后来解决的。Docker最初就是基于LXC构建的(早期版本使用LXC作为执行驱动),后来才逐步替换为自己实现的runc。
1.3 Docker的诞生与发展历程
1.3.1 Docker的诞生
2013年3月27日,在加州圣克拉拉举办的PyCon大会上,一家名为dotCloud的公司发布了Docker项目的首个版本。Docker的创始人是Solomon Hykes,他当时是dotCloud公司的CTO。
dotCloud原本是一家提供PaaS(平台即服务)的公司,在运营过程中,他们开发了一套基于LXC的内部工具来管理应用程序的部署。这套工具极大地提高了他们的运维效率。Hykes意识到这套工具有巨大的价值,决定将其开源。
Docker发布的时机非常巧妙。2013年正是云计算高速发展的时期,微服务架构开始流行,开发者迫切需要一种轻量级的应用打包和部署方案。Docker恰好满足了这一需求,加上其简洁优雅的命令行界面和"Build Once, Run Anywhere"的理念,迅速引爆了技术社区。
Docker发布后的发展速度令人惊叹:
- 2013年3月:Docker 0.1版本发布
- 2013年10月:Docker 1.0发布(在DockerCon上宣布)
- 2014年:dotCloud公司正式更名为Docker Inc.
- 2014年:Docker Hub上的镜像下载量超过1亿次
- 2015年:Docker获得9500万美元D轮融资,估值超过10亿美元
1.3.2 Docker发展史上的关键节点
Docker 0.1 ~ 0.9(2013年):探索期
早期的Docker完全基于LXC,使用LXC提供的命令来创建和管理容器。这时的Docker更像是一个LXC的"包装器"。
Docker 1.0(2014年6月):生产就绪
Docker 1.0标志着Docker正式可以用于生产环境。这个版本引入了Docker Hub作为公共镜像仓库,完善了网络和存储驱动。
Docker 1.11(2016年4月):架构拆分
这是Docker历史上一次重要的架构变革。Docker开始将容器运行时拆分为独立的组件:
- 引入containerd作为容器运行时
- 引入runc作为底层OCI运行时
- Docker Daemon不再直接创建容器,而是通过containerd和runc来管理
这次拆分使得Docker的架构更加模块化,也为后续Kubernetes等编排工具支持多种容器运行时奠定了基础。
Docker 1.12(2016年7月):内置Swarm
Docker 1.12将Swarm模式内置到Docker Engine中,用户无需额外安装即可使用Docker原生的集群管理功能。引入了docker swarm init、docker service create等命令。
Moby项目(2017年):开源重组
2017年,Docker将其核心开源项目重命名为Moby,作为Docker社区版的上游项目。这是Docker Inc.公司战略转型的一部分,公司重心从开源社区转向企业级商业产品。
Docker CE / EE(2017年):版本体系改革
Docker引入了Community Edition(CE)和Enterprise Edition(EE)两条产品线:
- Docker CE:免费开源版本,适合开发和小规模部署
- Docker EE:付费商业版本,提供企业级安全和管理功能
containerd成为CNCF毕业项目(2019年)
containerd从Docker中独立出来后,捐献给了CNCF(Cloud Native Computing Foundation),并于2019年正式毕业,成为云原生领域最重要的容器运行时之一。Kubernetes在1.24版本之后默认使用containerd作为容器运行时。
Docker Engine不再依赖LXC
从Docker 0.9版本开始,Docker引入了自己的执行驱动libcontainer,不再依赖LXC。libcontainer后来被捐赠给Open Container Initiative(OCI),成为runc的基础。
1.4 容器编排时代的到来(Docker Swarm、Kubernetes、Mesos)
当容器的数量从几个增长到几十个、几百个甚至上千个时,手动管理就变得不现实了。你需要一种工具来管理大规模容器的调度、部署、扩缩容、故障恢复和网络通信,这就是容器编排(Container Orchestration)。
1.4.1 Docker Swarm
Docker Swarm是Docker原生的容器编排工具。它的优势在于与Docker CLI深度集成,学习成本低。如果你已经熟悉docker run命令,那么学习docker service create几乎没有什么门槛。
bash
# 在管理节点上初始化Swarm集群
docker swarm init --advertise-addr 192.168.1.100
# 在工作节点上加入集群
docker swarm join --token SWMTKN-1-xxx 192.168.1.100:2377
# 创建一个服务,部署3个副本
docker service create --name web --replicas 3 -p 80:80 nginx
# 查看服务状态
docker service ls
# 扩缩容到5个副本
docker service scale web=5
# 滚动更新镜像
docker service update --image nginx:1.25 web
Docker Swarm的优点是简单易用,但功能相对有限,不适合超大规模集群。在Kubernetes的竞争下,Swarm逐渐失去了市场主导地位。
1.4.2 Kubernetes
Kubernetes(简称K8s)起源于Google内部的Borg系统。Google在运维大规模容器方面积累了十余年的经验,2014年将Kubernetes开源。2015年,Google将Kubernetes捐赠给CNCF。
Kubernetes提供了强大的容器编排能力:
- 自动调度:根据资源需求和约束条件,自动将容器调度到合适的节点。
- 自愈能力:当容器崩溃时自动重启,当节点故障时自动迁移容器。
- 水平伸缩:根据CPU使用率或自定义指标自动扩缩容。
- 服务发现与负载均衡:内置DNS和负载均衡,服务之间自动发现和通信。
- 滚动更新与回滚:支持零停机更新和一键回滚。
yaml
# Kubernetes部署示例:部署一个Nginx应用
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3 # 运行3个副本
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests: # 资源请求
cpu: 100m
memory: 128Mi
limits: # 资源限制
cpu: 200m
memory: 256Mi
Kubernetes最终成为了容器编排领域的事实标准。几乎所有主流的云厂商都提供了托管的Kubernetes服务(AWS EKS、GCP GKE、Azure AKS、阿里云ACK等)。
1.4.3 Apache Mesos
Apache Mesos是Apache基金会的开源集群管理器,最初由Twitter开发。Mesos采用两级调度架构,可以同时运行容器化应用和传统的大数据框架(如Hadoop、Spark)。
Mesos + Marathon的组合曾是Docker Swarm和Kubernetes的有力竞争者,但随着Kubernetes的崛起,Mesos在容器编排领域的市场份额逐渐下降。Twitter和Mesosphere(现D2iQ)是Mesos的主要推动者,但后来都转向支持Kubernetes。
1.4.4 编排工具对比
| 特性 | Docker Swarm | Kubernetes | Apache Mesos |
|---|---|---|---|
| 开发者 | Docker Inc. | Google/CNCF | Apache/Twitter |
| 架构复杂度 | 低 | 高 | 中 |
| 学习曲线 | 平缓 | 陡峭 | 中等 |
| 功能丰富度 | 基础 | 非常丰富 | 丰富(大数据场景强) |
| 社区活跃度 | 下降 | 非常活跃 | 下降 |
| 适合规模 | 小到中型 | 中到超大型 | 大型 |
| 生态支持 | Docker原生 | 极其丰富 | 中等 |
| 生产采用率 | 低 | 极高 | 低 |
1.5 云原生与容器生态全景图(CNCF)
1.5.1 什么是云原生
云原生(Cloud Native)是一种构建和运行应用程序的方法论,它利用云计算的弹性伸缩和分布式特性来构建可扩展、高可用的应用。云原生的核心要素包括:
- 容器:应用打包和运行的标准格式
- 微服务:应用拆分为独立的小服务
- 服务网格:服务间通信的管理层
- 不可变基础设施:通过替换而非修改来更新基础设施
- 声明式API:描述期望状态而非执行步骤
2015年,Google牵头成立了CNCF(Cloud Native Computing Foundation),致力于推广云原生技术。CNNF的口号是"Make Cloud Native ubiquitous"(让云原生无处不在)。
1.5.2 CNCF生态全景图
CNCF维护着一张著名的Cloud Native Landscape(云原生全景图),涵盖了云原生技术的各个领域。截至2024年,全景图中收录了超过2000个项目和产品。主要领域包括:
1. 容器运行时
- containerd:轻量级容器运行时,CNCF毕业项目
- CRI-O:Kubernetes专用的容器运行时
- runc:OCI容器运行时参考实现
- Kata Containers:基于虚拟化的安全容器
2. 容器编排
- Kubernetes:容器编排事实标准,CNCF毕业项目
- Nomad:HashiCorp的轻量级编排工具
3. 服务发现与配置
- CoreDNS:DNS服务器,CNCF毕业项目
- etcd:分布式键值存储,CNCF毕业项目
- Consul:HashiCorp的服务发现工具
4. 服务网格
- Istio:Google/IBM/Lyft联合开发的服务网格
- Linkerd:轻量级服务网格,CNCF毕业项目
- Cilium:基于eBPF的网络和安全方案
5. 持续集成与持续交付
- ArgoCD:GitOps持续交付工具,CNCF毕业项目
- Tekton:Kubernetes原生的CI/CD管道
- Jenkins X:基于Jenkins的云原生CI/CD
6. 监控与可观测性
- Prometheus:监控和告警系统,CNCF毕业项目
- Grafana:可视化仪表板
- Fluentd:日志收集,CNCF毕业项目
- Jaeger:分布式链路追踪,CNCF毕业项目
- OpenTelemetry:统一可观测性框架
7. 存储与数据
- Rook:云原生存储编排,CNCF毕业项目
- Vitess:MySQL数据库集群管理,CNCF毕业项目
- TiKV:分布式事务键值存储,CNCF毕业项目
8. 安全
- Falco:运行时安全监控,CNCF毕业项目
- Notary:镜像签名验证
- OPA(Open Policy Agent):通用策略引擎,CNCF毕业项目
CNCF将项目分为三个成熟度等级:
- 毕业项目(Graduated):经过严格审查,被广泛采用,如Kubernetes、Prometheus、etcd等
- 孵化项目(Incubating):正在成长中的项目
- 沙箱项目(Sandbox):早期阶段的项目
这些项目共同构成了一个完整的云原生技术栈,而Docker正是这个技术栈的基石------几乎所有的CNCF项目都以容器为基础运行单元。
第二章 什么是Docker
在了解了容器化技术的发展历程之后,我们现在来正式认识Docker。本章将从定义出发,深入探讨Docker的核心理念、它解决的问题、与传统虚拟化的本质区别,以及它的优缺点。读完本章,你将对"Docker到底是个什么东西"有一个清晰而深刻的理解。
2.1 Docker的定义与核心理念
2.1.1 官方定义
Docker官方给出的定义是:
Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly.
翻译过来就是:Docker是一个用于开发、交付和运行应用程序的开放平台。Docker使你能够将应用程序与基础设施分离,从而快速交付软件。
这个定义包含了三个关键词:
- 开发(Developing):Docker提供了标准化的方式来构建应用镜像,开发者可以通过Dockerfile定义应用的环境和依赖。
- 交付(Shipping):Docker镜像是一种标准化的软件交付物,可以通过Registry进行版本管理和分发。
- 运行(Running):Docker提供了容器运行时,可以在任何安装了Docker的环境中运行容器。
2.1.2 通俗理解
如果用一句话来概括Docker是什么,那就是:Docker是一个让你把应用程序及其所有依赖打包成一个标准化单元(容器)的工具。
打个比方,你可以把Docker想象成一个"集装箱"。在国际贸易中,集装箱的发明 revolutionized(彻底改变了)了物流行业。在集装箱出现之前,各种货物形状各异,装船卸船非常麻烦,需要根据每种货物的特性来处理。集装箱出现后,所有货物都装进标准尺寸的钢箱里,无论是机械零件还是新鲜水果,从卡车到火车再到轮船,全程不需要开箱,无缝转运。
Docker的logo------一条鲸鱼驮着一堆集装箱------正是这个比喻的视觉化表达。应用程序就是"货物",Docker容器就是"集装箱",无论你的应用是Java写的还是Python写的,无论运行在开发者的笔记本上还是云服务器上,它都被装在同样规格的"集装箱"里,保证了环境的一致性。
2.1.3 核心理念
Docker的核心理念可以概括为以下几点:
1. 标准化
Docker定义了容器镜像的标准格式(OCI Image Format),使得任何人构建的镜像都能在任何兼容的容器运行时上运行。这种标准化就像USB接口标准一样------无论哪个厂商生产的USB设备,都能插入任何一台有USB接口的电脑。
2. 分层复用
Docker镜像采用分层存储(Layered File System)的设计。每一层都是只读的,多个镜像可以共享相同的层。比如你有10个基于Ubuntu的镜像,它们共享同一个Ubuntu基础层,只需要存储一份。这大大节省了存储空间和传输带宽。
3. 声明式配置
Docker使用Dockerfile以声明式的方式描述镜像的构建过程。你只需要描述"我想要什么样的环境",Docker会自动完成构建。这种方式可追溯、可版本控制、可复现。
dockerfile
# 一个典型的Dockerfile示例
FROM node:18-alpine # 基础镜像
WORKDIR /app # 设置工作目录
COPY package*.json ./ # 复制依赖配置文件
RUN npm install # 安装依赖
COPY . . # 复制源代码
EXPOSE 3000 # 声明端口
CMD ["node", "app.js"] # 启动命令
4. 不可变性
Docker镜像是不可变的(Immutable)------一旦构建完成就不会再改变。如果要更新应用,你构建一个新的镜像版本,然后替换旧容器,而不是修改运行中的容器。这种不可变性带来了很多好处:可追溯、易回滚、状态一致。
2.2 Docker解决了什么问题
Docker的流行不是偶然的,它精准地解决了一直困扰软件开发团队的几个核心问题。
2.2.1 环境一致性问题
这是Docker解决的最核心的问题。在传统开发模式中,一个软件从开发到上线,需要经过开发环境、测试环境、预发布环境和生产环境。每个环境的操作系统版本、库版本、配置都可能不同,导致应用在一个环境能正常运行,到另一个环境就出问题。
开发者的机器: Ubuntu 22.04 + Python 3.10 + Django 4.2
测试服务器: CentOS 7 + Python 3.6 + Django 3.2
生产服务器: Ubuntu 20.04 + Python 3.8 + Django 4.0
这种环境差异导致的问题数不胜数:
- Python版本不同导致语法不兼容
- 依赖库版本不同导致行为不一致
- 操作系统不同导致文件路径和权限问题
- 环境变量配置不同导致连接错误
有了Docker后,开发者在本地构建一个包含应用和所有依赖的镜像,这个镜像被推送到Registry,然后测试环境和生产环境拉取同一个镜像来运行。由于运行的是完全相同的镜像,环境一致性问题从根本上被解决了。
bash
# 开发者本地构建镜像
docker build -t myapp:1.0 .
# 推送到镜像仓库
docker push myapp:1.0
# 测试环境拉取并运行
docker pull myapp:1.0
docker run -d -p 80:3000 myapp:1.0
# 生产环境拉取并运行(完全相同的镜像)
docker pull myapp:1.0
docker run -d -p 80:3000 myapp:1.0
2.2.2 资源隔离问题
在一台服务器上运行多个应用时,应用之间可能相互干扰。比如应用A产生了大量日志占满了磁盘,导致应用B无法写入数据;应用C出现了内存泄漏,吃光了所有内存,导致应用D被OOM杀死。
Docker通过cgroups技术为每个容器设置资源限制,确保应用之间互不影响:
bash
# 运行一个容器,限制最多使用512MB内存和1个CPU核心
docker run -d \
--name myapp \
--memory=512m \
--cpus=1.0 \
myapp:1.0
# 运行另一个容器,限制256MB内存
docker run -d \
--name another-app \
--memory=256m \
--cpus=0.5 \
another-app:2.0
这样,即使某个应用出现异常,也只会影响自己的容器,不会波及其他应用。
2.2.3 快速部署与弹性伸缩
传统的应用部署需要安装操作系统、配置环境、安装依赖、部署代码、启动服务,整个过程可能需要几十分钟甚至几个小时。而Docker容器的启动只需要几秒钟,因为所有依赖都已经打包在镜像中。
bash
# 传统部署流程(耗时:30分钟~2小时)
1. 安装操作系统
2. 配置网络和防火墙
3. 安装运行时环境(Java/Python/Node.js)
4. 安装数据库客户端
5. 配置环境变量
6. 复制应用代码
7. 安装依赖包
8. 配置系统服务
9. 启动应用
# Docker部署流程(耗时:10秒)
docker run -d -p 80:8080 myapp:1.0
这种快速部署能力使得弹性伸缩成为可能。当流量高峰来临时,可以在几秒钟内启动几十个容器来应对;流量回落后,又可以快速销毁多余的容器,节省资源。
bash
# 使用Docker Swarm快速扩容
docker service scale web=50 # 10秒内启动50个容器实例
2.2.4 简化运维复杂度
Docker将应用的运行环境与应用代码一起打包,运维人员不再需要为每个应用配置特定的运行环境。运维的工作从"管理应用运行环境"转变为"管理容器",复杂度大大降低。
同时,Docker提供了丰富的命令来管理容器的生命周期:
bash
# 查看所有运行中的容器
docker ps
# 查看容器日志
docker logs -f myapp
# 查看容器资源使用情况
docker stats
# 进入容器排查问题
docker exec -it myapp /bin/sh
# 查看容器详细信息
docker inspect myapp
2.3 Docker的设计哲学(一次构建,到处运行)
"Build Once, Run Anywhere"(一次构建,到处运行)是Docker最核心的设计哲学。这个理念源自Java的"Write Once, Run Anywhere",但Docker将其提升到了一个更高的层次。
Java的"一次编写,到处运行"依赖于JVM------只要有JVM,Java字节码就能运行。但问题是,不同环境的JVM版本、JVM参数、系统库版本仍然可能导致行为差异。而且,非Java的应用(如C/C++、Go、Python)无法享受这一好处。
Docker的"一次构建,到处运行"则更加彻底:你构建的Docker镜像包含了应用及其所有依赖(运行时、库、配置文件等),只要目标机器上安装了Docker(或其他兼容OCI的容器运行时),这个镜像就能以完全相同的方式运行。
┌─────────────────────────────────────────────────┐
│ Docker 镜像 │
│ ┌───────────────────────────────────────────┐ │
│ │ 应用代码 + 依赖库 + 运行时 + 配置文件 │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 可以在以下任何地方运行: │
│ ✅ 开发者的MacBook │
│ ✅ 测试环境的CentOS服务器 │
│ ✅ AWS EC2 Ubuntu实例 │
│ ✅ 阿里云ECS实例 │
│ ✅ 本地VMware虚拟机 │
│ ✅ Kubernetes集群 │
│ ✅ 树莓派 │
└─────────────────────────────────────────────────┘
不过需要注意的是,这种可移植性有一个前提条件:目标环境的CPU架构和操作系统内核要兼容。例如,为x86_64架构构建的镜像无法直接在ARM架构(如Apple M1/M2)上运行,需要使用多架构构建(Multi-arch Build):
bash
# 使用docker buildx构建多架构镜像
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:1.0 \
--push .
2.4 Docker与传统虚拟化的本质区别
虽然Docker容器和虚拟机都提供了隔离能力,但它们的实现机制有本质区别。
虚拟机:通过Hypervisor在硬件层面进行虚拟化,每个虚拟机运行一个完整的Guest OS。Hypervisor有两种类型:
- Type 1(裸金属):直接运行在硬件上,如VMware ESXi、Xen、Hyper-V
- Type 2(寄居型):运行在宿主操作系统上,如VMware Workstation、VirtualBox
容器:通过Linux内核的namespace和cgroups在操作系统层面进行隔离,所有容器共享宿主机的内核。没有Hypervisor,没有Guest OS。
用一个更形象的比喻来解释:
- 虚拟机就像是在一块空地上建了一栋栋独立的别墅,每栋别墅都有自己的地基(Guest OS)、水电管道(系统服务)和房间(应用)。建造成本高,但完全独立。
- 容器就像是在一栋大楼里划分出的独立办公室,所有办公室共享大楼的地基和水电管道(Host OS Kernel),但每间办公室有自己的门锁(namespace隔离)和用电额度(cgroups限制)。
核心区别总结:
| 维度 | 虚拟机 | 容器 |
|---|---|---|
| 虚拟化层级 | 硬件级 | 操作系统级 |
| 隔离机制 | Hypervisor | namespace + cgroups |
| 是否需要Guest OS | 是 | 否 |
| 内核共享 | 不共享 | 共享 |
| 资源开销 | 大 | 小 |
| 启动速度 | 分钟级 | 秒级 |
| 安全隔离 | 强 | 中等 |
| 跨平台 | 支持(可运行不同OS) | 不支持(依赖Host内核) |
一个关键的区别是:虚拟机可以在Linux上运行Windows,也可以在Windows上运行Linux,因为它们虚拟化了完整的硬件。而容器只能运行与宿主机内核兼容的应用------Linux容器只能在Linux上运行,Windows容器只能在Windows上运行。
2.5 Docker的优缺点分析
2.5.1 Docker的优点
1. 环境一致性
这是Docker最大的价值。镜像将应用和依赖打包在一起,确保在所有环境中行为一致。
2. 快速启动
容器启动本质上是启动一个进程,通常只需几百毫秒到几秒,远快于虚拟机的分钟级启动。
3. 资源高效
容器不需要运行完整的Guest OS,资源开销极小。一台服务器可以运行数百个容器,而运行数十个虚拟机就已经很吃力了。
4. 版本管理与复用
Docker镜像通过标签(Tag)进行版本管理,每一层都有唯一标识(SHA256),可以精确追溯。基础层可以被多个镜像复用,节省存储和传输成本。
5. 生态丰富
Docker Hub上有数百万个公共镜像,覆盖了几乎所有主流的软件和工具。Docker Compose、Docker Swarm、Kubernetes等工具构成了完整的容器生态。
6. 微服务友好
容器的轻量级特性天然适合微服务架构------每个微服务打包成一个独立的容器,独立部署、独立扩缩容、独立更新。
2.5.2 Docker的缺点
1. 安全性相对较弱
容器共享宿主机内核,如果内核存在漏洞,所有容器都可能受影响。此外,容器内的root用户在未启用User Namespace的情况下,实际上拥有宿主机的root权限,存在提权风险。
2. 不适合所有应用
以下场景不适合使用容器:
- 需要直接访问硬件的应用(如GPU直通)
- 需要特定内核模块的应用
- GUI桌面应用(虽然可以通过X11转发实现,但体验不佳)
- 对性能极度敏感的应用(容器的namespace和cgroups会带来少量开销)
3. 数据持久化复杂
容器本身是无状态且临时的,容器删除后其中的数据也会丢失。虽然可以通过Volume和Bind Mount来解决,但管理持久化数据仍然比传统方式更复杂,尤其是在分布式场景下。
4. 网络复杂度
容器的网络模型比传统应用更复杂。容器间通信、跨主机网络、端口冲突、DNS解析等问题都需要额外处理。在Kubernetes中,网络配置(CNI)是最令新手头疼的部分之一。
5. 学习曲线
虽然Docker的基本用法很简单,但要深入理解分层存储、网络模型、存储驱动、安全隔离等概念,需要投入不少学习时间。在团队中推广Docker也需要一定的培训成本。
6. 监控和调试困难
容器的黑盒特性使得调试比传统应用更困难。查看容器内部的进程、排查网络问题、分析性能瓶颈都需要掌握专门的工具和技巧。
bash
# 容器调试常用命令
docker exec -it <container> /bin/sh # 进入容器
docker logs -f <container> # 查看日志
docker inspect <container> # 查看详细信息
docker top <container> # 查看容器内进程
docker stats <container> # 查看资源使用
docker network inspect <network> # 查看网络配置
2.5.3 何时该用Docker,何时不该用
适合使用Docker的场景:
- Web应用和API服务
- 微服务架构
- CI/CD流水线
- 开发环境标准化
- 批处理和定时任务
- 数据库和中间件(开发/测试环境)
不太适合使用Docker的场景:
- 内核驱动开发
- 需要直接硬件访问的应用
- 对I/O性能要求极高的数据库(生产环境)
- 需要运行不同操作系统内核的应用
- 安全性要求极高的隔离场景(考虑使用Kata Containers)
第三章 容器 vs 虚拟机
在上一章中,我们已经简要对比了Docker与传统虚拟化的区别。本章将更深入、更系统地比较容器和虚拟机这两种技术。理解它们各自的原理和优劣,是做出正确技术选型的基础。很多初学者会陷入一个误区------认为容器将完全取代虚拟机。事实上,容器和虚拟机各有其适用场景,在真实的生产环境中,它们往往是互补而非替代的关系。
3.1 虚拟机工作原理详解(Hypervisor类型)
3.1.1 什么是Hypervisor
Hypervisor,又称虚拟机监视器(Virtual Machine Monitor, VMM),是虚拟化技术的核心组件。它运行在物理硬件和虚拟机之间,负责管理和分配硬件资源,为每个虚拟机提供一个虚拟的硬件环境。
Hypervisor的主要职责包括:
- CPU虚拟化:将物理CPU虚拟化为多个虚拟CPU(vCPU),通过时间片轮转的方式分配给不同的虚拟机。
- 内存虚拟化:维护物理内存到虚拟内存的映射表(影子页表或EPT/NPT),让每个虚拟机认为自己拥有连续的物理内存。
- 设备虚拟化:虚拟化磁盘、网卡、显卡等硬件设备,让虚拟机可以使用虚拟设备进行I/O操作。
- 隔离与调度:确保虚拟机之间互不干扰,合理调度CPU时间片。
3.1.2 Type 1 Hypervisor(裸金属型)
Type 1 Hypervisor直接运行在物理硬件上,不需要底层操作系统的支持。它本身就是一个小型的操作系统,专门用于管理虚拟机。
┌─────────────────────────────────────────┐
│ 物理硬件 │
├─────────────────────────────────────────┤
│ Type 1 Hypervisor │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ VM 1 │ │ VM 2 │ │ VM 3 │ │
│ │Guest │ │Guest │ │Guest │ │
│ │ OS │ │ OS │ │ OS │ │
│ └──────┘ └──────┘ └──────┘ │
└─────────────────────────────────────────┘
常见的Type 1 Hypervisor:
| 产品 | 厂商 | 特点 |
|---|---|---|
| VMware ESXi | VMware | 企业级,功能强大,市场占有率高 |
| Hyper-V | Microsoft | Windows Server内置,与微软生态深度集成 |
| Xen | 开源社区 | 历史悠久,AWS EC2早期使用 |
| KVM | 开源社区 | Linux内核内置,Red Hat主推 |
| AHV | Nutanix | 超融合架构专用 |
Type 1 Hypervisor的优势在于性能------由于直接运行在硬件上,虚拟化开销最小。它适合企业级的数据中心场景,需要运行大量虚拟机的高密度部署。
3.1.3 Type 2 Hypervisor(寄居型)
Type 2 Hypervisor运行在传统的操作系统之上,就像一个普通的应用程序。宿主操作系统负责管理硬件资源,Hypervisor通过宿主操作系统来访问硬件。
┌─────────────────────────────────────────┐
│ 物理硬件 │
├─────────────────────────────────────────┤
│ 宿主机操作系统 │
├─────────────────────────────────────────┤
│ Type 2 Hypervisor │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ VM 1 │ │ VM 2 │ │ VM 3 │ │
│ │Guest │ │Guest │ │Guest │ │
│ │ OS │ │ OS │ │ OS │ │
│ └──────┘ └──────┘ └──────┘ │
└─────────────────────────────────────────┘
常见的Type 2 Hypervisor:
| 产品 | 厂商 | 适用场景 |
|---|---|---|
| VMware Workstation | VMware | 开发者本地使用 |
| VMware Fusion | VMware | macOS平台 |
| VirtualBox | Oracle | 免费开源,跨平台 |
| Parallels Desktop | Parallels | macOS平台,优化好 |
Type 2 Hypervisor性能不如Type 1,因为多了一层宿主操作系统的开销。但它安装和使用更方便,适合个人开发者学习和测试。
3.1.4 硬件辅助虚拟化
现代CPU提供了硬件辅助虚拟化指令集,大大提升了虚拟机的性能:
- Intel VT-x / AMD-V:提供CPU虚拟化加速,减少Hypervisor的陷入(VM Exit/VM Entry)开销
- Intel EPT / AMD NPT:提供内存虚拟化加速,避免影子页表(Shadow Page Table)的开销
- Intel VT-d / AMD-Vi:提供I/O虚拟化,支持设备直通(Passthrough)
bash
# 检查CPU是否支持硬件虚拟化
# Linux下检查
grep -E '(vmx|svm)' /proc/cpuinfo
# 如果输出中包含vmx(Intel)或svm(AMD),说明支持硬件虚拟化
# Windows下可以通过系统信息查看
# 系统信息 -> 虚拟化-based安全: 已启用
3.1.5 虚拟机的完整启动流程
虚拟机的启动过程与物理机几乎相同,需要经历以下步骤:
1. Hypervisor为虚拟机分配资源(CPU、内存、磁盘)
2. 虚拟BIOS/UEFI启动
3. 读取虚拟磁盘的引导扇区
4. 加载Bootloader(GRUB/Windows Boot Manager)
5. 加载Guest OS内核
6. 内核初始化(探测硬件、加载驱动、挂载文件系统)
7. 启动init进程(systemd / SysV init)
8. 启动系统服务(网络、SSH、日志等)
9. 启动用户应用程序
整个流程通常需要30秒~2分钟
这个漫长的启动过程是因为虚拟机需要启动一个完整的操作系统,包括内核加载、设备探测、服务初始化等所有步骤。
3.2 容器工作原理详解(namespace + cgroups)
3.2.1 容器的本质
从操作系统的角度来看,容器不过是一组被namespace隔离、被cgroups限制的进程。容器内的进程实际上是宿主机上的普通进程,只是它们看到的系统资源被namespace"过滤"了,它们能使用的资源被cgroups"限制"了。
可以用一个实验来验证这一点:
bash
# 在宿主机上启动一个容器
docker run -d --name test nginx
# 在宿主机上查看容器进程
docker top test
# 输出类似:
# UID PID PPID ... CMD
# root 12345 12330 ... nginx: master process nginx -g 'daemon off;'
# 可以看到,容器内的nginx进程实际上是宿主机上的PID 12345
# 在宿主机上甚至可以直接通过PID操作容器进程
kill 12345 # 这会杀掉容器内的nginx进程
这说明容器并非虚拟机那种独立的系统,容器内的进程就是宿主机上的进程,只是被"包装"了一层隔离。
3.2.2 namespace详解
前面我们提到了6种namespace,现在来详细讲解每种namespace的隔离效果和实际应用。
PID Namespace(进程隔离)
PID namespace使得容器内的进程只能看到自己namespace内的进程。在容器内部,第一个进程的PID为1(通常是应用的入口进程),但在宿主机上,这个进程有一个完全不同的PID。
bash
# 在容器内查看进程
docker exec -it test ps aux
# 输出:
# USER PID ... COMMAND
# root 1 ... nginx: master process nginx -g 'daemon off;'
# nginx 29 ... nginx: worker process
# 在宿主机上查看同一个进程
docker top test
# 输出:
# UID PID PPID ... COMMAND
# root 12345 12330 ... nginx: master process nginx -g 'daemon off;'
# 101 12346 12345 ... nginx: worker process
# 容器内PID 1 = 宿主机PID 12345
# 这就是PID namespace的映射效果
PID namespace的一个重要特性:容器内的进程无法看到宿主机上的其他进程,也无法通过kill命令影响宿主机进程。这提供了基本的进程隔离。
NET Namespace(网络隔离)
NET namespace为容器提供了独立的网络栈,包括独立的网卡接口、IP地址、路由表、iptables规则、端口号空间等。
bash
# 查看容器的网络配置
docker exec -it test ip addr
# 输出:
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# 8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
# link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.2/16 brd 172.17.255.255 scope host eth0
# 在宿主机上查看网络接口
ip addr
# 可以看到:
# 9: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP>
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# 10: vethxxxx@if8: <BROADCAST,MULTICAST>
# 这是连接容器网络的虚拟网卡对(veth pair)
Docker默认使用bridge网络模式,容器通过虚拟网卡对(veth pair)连接到宿主机的docker0网桥,再通过NAT访问外部网络。
MNT Namespace(文件系统隔离)
MNT namespace隔离了挂载点视图。每个容器看到的文件系统层次结构是独立的,容器内的挂载操作不会影响宿主机或其他容器。
bash
# 查看容器的挂载信息
docker exec -it test mount
# 输出(部分):
# overlay on / type overlay (...)
# proc on /proc type proc (...)
# tmpfs on /dev type tmpfs (...)
# /dev/sda1 on /etc/resolv.conf type ext4 (...)
Docker使用OverlayFS等联合文件系统来构建容器的文件系统。容器看到的是一个合并后的统一视图,但实际上由多个只读层(镜像层)和一个可写层(容器层)组成。
UTS Namespace(主机名隔离)
UTS namespace隔离了hostname和domain name。每个容器可以有自己独立的主机名。
bash
# 设置容器的主机名
docker run -d --name test --hostname my-custom-host nginx
# 进入容器查看主机名
docker exec -it test hostname
# 输出: my-custom-host
# 这与宿主机的主机名完全不同
IPC Namespace(进程间通信隔离)
IPC namespace隔离了System V IPC和POSIX消息队列。容器之间无法通过共享内存、信号量等IPC机制进行通信(除非它们在同一个IPC namespace中)。
USER Namespace(用户隔离)
USER namespace是最后一个被引入的namespace(内核3.8),它提供了用户和用户组ID的映射。在USER namespace中,容器内的root用户(UID 0)可以映射为宿主机上的非特权用户。
bash
# 启用user namespace remapping
# /etc/docker/daemon.json
{
"userns-remap": "default"
}
# 重启Docker后,容器内的root用户实际映射为宿主机的dockremap用户
# 即使容器被攻破,攻击者获得的也只是非特权用户权限
3.2.3 cgroups详解
cgroups的cgroup v2从Linux 4.5开始引入,使用统一的层次结构。但目前大多数系统仍使用cgroup v1。这里以cgroup v1为例进行讲解。
CPU限制
bash
# 方法1: 使用--cpus参数(基于CFS调度器的配额)
docker run -d --cpus=1.5 nginx
# 表示最多使用1.5个CPU核心(150%的CPU时间)
# 方法2: 使用--cpu-quota和--cpu-period(更底层)
docker run -d --cpu-quota=50000 --cpu-period=100000 nginx
# cpu-period=100000微秒(100ms)为一个调度周期
# cpu-quota=50000微秒(50ms)为容器在该周期内可用的CPU时间
# 50ms/100ms = 50%, 即使用0.5个CPU核心
# 方法3: 绑定CPU核心
docker run -d --cpuset-cpus=0,1 nginx
# 只允许使用CPU0和CPU1
内存限制
bash
# 限制最大内存使用量
docker run -d --memory=512m nginx
# 同时设置内存+Swap限制
docker run -d --memory=512m --memory-swap=1g nginx
# 当容器使用超过512m内存时,可以使用Swap
# 但内存+Swap总和不能超过1g
# 设置内核内存限制(较新的内核已弃用)
docker run -d --kernel-memory=256m nginx
# 设置OOM行为
docker run -d --memory=256m --oom-kill-disable=true nginx
# 禁止OOM Killer杀死该容器(危险!可能导致系统不稳定)
IO限制
bash
# 限制磁盘读写速率
docker run -d \
--device-read-bps /dev/sda:10mb \ # 读速度限制10MB/s
--device-write-bps /dev/sda:10mb \ # 写速度限制10MB/s
--device-read-iops /dev/sda:1000 \ # 读IOPS限制1000
--device-write-iops /dev/sda:1000 \ # 写IOPS限制1000
nginx
查看容器的cgroups限制
bash
# 查看容器的cgroup路径
docker inspect test | grep Cgroup
# 在宿主机上查看容器的cgroup配置
cat /sys/fs/cgroup/cpu/docker/<container_id>/cpu.cfs_quota_us
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
3.2.4 联合文件系统(UnionFS)
联合文件系统(Union File System)是Docker镜像分层存储的技术基础。它允许多个文件系统"叠加"在一起,对外呈现为统一的文件系统视图。
Docker目前默认使用OverlayFS(具体来说是overlay2驱动):
镜像层(只读):
┌─────────────────────────┐
│ Layer 3: COPY app.js │ ← 上层
├─────────────────────────┤
│ Layer 2: RUN npm install │
├─────────────────────────┤
│ Layer 1: FROM node:18 │ ← 下层(基础镜像)
├─────────────────────────┤
│ bootfs (内核共享) │
└─────────────────────────┘
容器层(可写):
┌─────────────────────────┐
│ Container Layer (可写) │ ← 顶层,所有修改写到这里
├─────────────────────────┤
│ Layer 3: COPY app.js │ ← 只读
├─────────────────────────┤
│ Layer 2: RUN npm install │ ← 只读
├─────────────────────────┤
│ Layer 1: FROM node:18 │ ← 只读
└─────────────────────────┘
当容器读取文件时:
1. 从顶层(容器层)开始查找
2. 如果找到,返回该文件
3. 如果没找到,向下层(镜像层)查找
当容器修改文件时(Copy-on-Write):
1. 从下层找到文件
2. 复制到容器层
3. 在容器层进行修改
4. 下层的原文件保持不变
Copy-on-Write(写时复制)是联合文件系统的核心机制:只有当文件被修改时,才将其从只读层复制到可写层。这保证了只读层的不可变性,同时最大程度减少了磁盘占用。
3.3 两者架构对比图解(详细的文字描述)
3.3.1 虚拟机架构图
┌────────────────────────────────────────────────────────────────┐
│ 物理服务器 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ VM 1 │ │ VM 2 │ │ VM 3 │ │ VM 4 │ │
│ │┌────────┐│ │┌────────┐│ │┌────────┐│ │┌────────┐│ │
│ ││ App A ││ ││ App B ││ ││ App C ││ ││ App D ││ │
│ ││Bins/Lib││ ││Bins/Lib││ ││Bins/Lib││ ││Bins/Lib││ │
│ ││────────││ ││────────││ ││────────││ ││────────││ │
│ ││Guest OS││ ││Guest OS││ ││Guest OS││ ││Guest OS││ │
│ ││(完整OS) ││ ││(完整OS) ││ ││(完整OS) ││ ││(完整OS) ││ │
│ │└────────┘│ │└────────┘│ │└────────┘│ │└────────┘│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Hypervisor │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ Host OS (可选) │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ CPU / 内存 / 磁盘 / 网络 │ │
│ └────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
在虚拟机架构中,每个虚拟机都包含:
- Guest OS :一个完整的操作系统,有自己的内核、驱动、系统服务。通常占用14GB磁盘和256MB1GB内存。
- Bins/Libs:应用运行需要的二进制文件和库。
- App:应用程序本身。
每个Guest OS都需要独立维护:打补丁、升级、配置。4个虚拟机就意味着4个操作系统需要维护。
3.3.2 容器架构图
┌────────────────────────────────────────────────────────────────┐
│ 物理服务器 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Container1│ │Container2│ │Container3│ │Container4│ │
│ │┌────────┐│ │┌────────┐│ │┌────────┐│ │┌────────┐│ │
│ ││ App A ││ ││ App B ││ ││ App C ││ ││ App D ││ │
│ ││Bins/Lib││ ││Bins/Lib││ ││Bins/Lib││ ││Bins/Lib││ │
│ │└────────┘│ │└────────┘│ │└────────┘│ │└────────┘│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Docker Engine / Container Runtime │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ Host OS (共享内核) │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ CPU / 内存 / 磁盘 / 网络 │ │
│ └────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
在容器架构中:
- 没有Guest OS,所有容器共享宿主机的内核。
- 每个容器只包含应用本身及其依赖的二进制文件和库,通常只有几十到几百MB。
- Docker Engine负责管理容器的生命周期,但它非常轻量,不像Hypervisor那样需要虚拟化硬件。
3.3.3 关键差异总结
| 对比项 | 虚拟机 | 容器 |
|---|---|---|
| 每实例磁盘占用 | 5~40GB | 10~500MB |
| 每实例内存开销 | 256MB~4GB | 10~200MB |
| 每实例CPU开销 | 高(运行完整OS) | 低(就是一个进程) |
| 操作系统数量 | N+1(每VM一个+宿主) | 1(仅宿主机) |
| 维护工作量 | 高(每OS独立维护) | 低(仅维护宿主机) |
| 安全隔离 | 强(硬件级) | 中(内核级) |
3.4 性能对比(启动速度、资源占用、密度)
3.4.1 启动速度对比
| 技术 | 典型启动时间 | 原因 |
|---|---|---|
| 物理机 | 1~5分钟 | 硬件自检 + OS启动 |
| 虚拟机 | 30秒~2分钟 | 完整OS启动流程 |
| 容器 | 0.1~2秒 | 仅启动进程 |
容器启动为什么这么快?因为容器不需要启动操作系统内核,不需要执行BIOS自检,不需要加载驱动,不需要启动系统服务。它只需要:
- 创建namespace和cgroups
- 准备RootFS(联合文件系统挂载)
- 启动应用进程
这三步在毫秒级别就能完成。
3.4.2 资源占用对比
假设我们要在一台64GB内存、16核CPU、1TB磁盘的服务器上部署10个Web应用:
虚拟机方案:
- 每个虚拟机分配4GB内存、1核CPU、40GB磁盘
- Guest OS开销:每个约512MB内存
- 10个虚拟机 = 40GB内存(含5GB Guest OS开销) + 400GB磁盘
- 剩余资源:24GB内存,6核CPU
容器方案:
- 每个容器限制512MB内存、0.5核CPU
- 没有Guest OS开销
- 10个容器 = 5GB内存 + 约2GB磁盘(共享基础镜像层)
- 剩余资源:59GB内存,11核CPU
可以看到,在同样的硬件上,容器方案的资源利用率远高于虚拟机方案。你可以用容器方案在同一台服务器上部署上百个应用,而虚拟机方案最多只能部署十几个。
3.4.3 运行时性能对比
在CPU密集型任务上,容器和原生的性能几乎相同(差距<1%),因为容器内的进程就是宿主机的普通进程。虚拟机的CPU性能损失在2%~10%之间,取决于Hypervisor和是否有硬件辅助虚拟化。
在内存密集型任务上,容器的性能同样接近原生。虚拟机会有一些额外的内存管理开销(EPT页表翻译等)。
在I/O密集型任务上:
-
容器的I/O性能取决于存储驱动。使用OverlayFS时,读性能接近原生,写性能可能因为Copy-on-Write有少量损失。使用Volume时,I/O性能与原生相同。
-
虚拟机的I/O性能取决于虚拟磁盘的实现。设备直通(Passthrough)可以接近原生性能,但虚拟磁盘(Virtual Disk)会有5%~20%的性能损失。
性能对比总结(CPU/内存/IO):
原生 容器 虚拟机(硬件辅助)CPU性能 100% 99%~100% 90%~98%
内存性能 100% 99%~100% 95%~98%
磁盘I/O 100% 97%~100% 80%~95%
网络I/O 100% 97%~100% 85%~95%
3.4.4 部署密度对比
在一台典型的服务器(32核CPU, 128GB内存)上:
| 技术 | 最大实例数 | 每实例资源 |
|---|---|---|
| 虚拟机 | 20~40个 | 4核CPU, 4GB内存 |
| 容器 | 200~500个 | 0.1核CPU, 256MB内存 |
容器的部署密度通常是虚拟机的10倍以上。这意味着在相同的硬件投入下,容器可以承载更多的应用,大幅降低IT成本。
3.5 适用场景对比(何时用VM,何时用容器)
3.5.1 适合使用虚拟机的场景
-
需要运行不同操作系统的应用
- 例如:在Linux服务器上运行Windows应用
- 例如:同时运行Linux和Windows的混合环境
-
需要强安全隔离的场景
- 多租户环境(公有云)
- 不受信任的代码执行
- 合规性要求高的场景(金融、医疗)
-
需要直接硬件访问的应用
- GPU计算(CUDA)
- 特殊硬件设备
- 高性能存储
-
传统单体应用
- 不便于容器化的遗留系统
- 需要特定内核版本的应用
-
开发和测试环境
- 需要模拟完整的网络拓扑
- 需要测试不同OS行为
3.5.2 适合使用容器的场景
-
微服务架构
- 每个微服务独立打包、部署、扩缩容
- 服务实例快速创建和销毁
-
CI/CD流水线
- 构建环境标准化
- 测试环境快速搭建和销毁
-
开发环境统一
- 新成员快速上手
- 跨平台开发环境一致
-
弹性伸缩
- 应对流量波峰波谷
- 快速扩缩容
-
混合云部署
- 同一镜像在不同云平台运行
- 避免云厂商锁定
3.5.3 决策矩阵
| 考虑因素 | 倾向VM | 倾向容器 |
|---|---|---|
| 操作系统 | 需要不同OS | 同一OS内核 |
| 隔离要求 | 极高 | 中等 |
| 启动速度 | 不敏感 | 敏感 |
| 资源效率 | 不敏感 | 敏感 |
| 应用架构 | 单体 | 微服务 |
| 运维能力 | 传统运维 | DevOps |
| 团队规模 | 小团队 | 中大团队 |
3.6 容器与虚拟机的混合使用
在实际的生产环境中,容器和虚拟机往往不是二选一的关系,而是混合使用、各取所长。这种混合模式被称为"VM + Container"模式。
3.6.1 常见的混合架构
架构1:虚拟机作为容器宿主机
这是最常见的混合模式。在物理机上运行虚拟机,获得硬件级隔离和资源隔离;然后在每个虚拟机内运行Docker,获得容器的轻量级和灵活性。
┌────────────────────────────────────────────┐
│ 物理服务器 │
│ ┌──────────────────────────────────────┐ │
│ │ Hypervisor │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ VM 1 │ │ VM 2 │ │ │
│ │ │ Host OS │ │ Host OS │ │ │
│ │ │┌───────┐│ │┌───────┐│ │ │
│ │ ││Docker ││ ││Docker ││ │ │
│ │ ││┌─┐┌─┐ ││ ││┌─┐┌─┐ ││ │ │
│ │ │││C││C│ ││ │││C││C│ ││ │ │
│ │ ││└─┘└─┘ ││ ││└─┘└─┘ ││ │ │
│ │ │└───────┘│ │└───────┘│ │ │
│ │ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
这种架构的好处:
- 虚拟机提供了强隔离,不同租户/团队的容器运行在不同的虚拟机中
- 虚拟机提供了快照、迁移、高可用等企业级功能
- 容器提供了应用的快速部署和弹性伸缩
- 公有云的容器服务(AWS ECS、阿里云ACK)大多采用这种架构
架构2:VM和容器并行运行
在同一个物理机上,部分应用以虚拟机形式运行,部分应用以容器形式运行。
┌────────────────────────────────────────────┐
│ 物理服务器 │
│ ┌──────────────────────────────────────┐ │
│ │ Host OS │ │
│ │ ┌─────────┐ ┌───────────────────┐ │ │
│ │ │ VM 1 │ │ Docker Engine │ │ │
│ │ │ (传统 │ │ ┌─┐ ┌─┐ ┌─┐ │ │ │
│ │ │ 应用) │ │ │C│ │C│ │C│ │ │ │
│ │ └─────────┘ │ └─┘ └─┘ └─┘ │ │ │
│ │ └───────────────────┘ │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
这种架构适合正在从传统架构向容器化迁移的组织------传统应用继续以虚拟机形式运行,新应用采用容器化部署。
3.6.2 安全容器:Kata Containers与gVisor
对于需要容器级别的使用体验但又要求虚拟机级别安全隔离的场景,可以使用安全容器技术:
Kata Containers
- 由Intel和Hyper.sh发起,现在是CNCF项目
- 每个容器运行在一个轻量级虚拟机中
- 兼容OCI标准,可以与Kubernetes无缝集成
- 启动时间比普通容器慢(约1~2秒),但比传统虚拟机快得多
gVisor
- Google开发的安全容器运行时
- 在用户空间实现了一个Linux内核(称为Sentry)
- 拦截容器的系统调用,在用户空间处理
- 安全性介于普通容器和虚拟机之间
bash
# 在Docker中使用Kata Containers作为运行时
docker run --runtime=kata-runtime -d nginx
# 在Kubernetes中使用Kata Containers
# 通过RuntimeClass指定
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
第四章 Docker架构详解
要深入理解Docker,就必须了解它的内部架构。Docker不是单一的一个程序,而是一个由多个组件协同工作的系统。本章将从整体架构到每个组件,再到底层Linux内核技术,层层递进地为你揭开Docker架构的面纱。
4.1 Docker整体架构(C/S架构图解)
Docker采用经典的客户端-服务器(Client/Server, C/S)架构。Docker Client(客户端)通过REST API与Docker Daemon(服务端守护进程)通信,Docker Daemon负责构建镜像、运行容器、管理网络和存储等核心工作。
┌─────────────────────────────────────────────────────────────────┐
│ Docker 架构总览 │
│ │
│ ┌──────────────┐ │
│ │ Docker Client│──── docker build / run / pull / push ────┐ │
│ │ (docker CLI)│ │ │
│ └──────────────┘ │ │
│ │ │ │
│ │ REST API (Unix Socket / TCP) │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ Docker Daemon (dockerd) ││
│ │ ││
│ │ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────────────┐ ││
│ │ │ Image │ │Container│ │ Network │ │ Volume │ ││
│ │ │ Manager │ │ Manager │ │ Manager │ │ Manager │ ││
│ │ └─────────┘ └────┬────┘ └──────────┘ └──────────────┘ ││
│ │ │ ││
│ │ ▼ ││
│ │ ┌───────────┐ ││
│ │ │ containerd│ ││
│ │ │ │ ││
│ │ │ ┌───────┐ │ ││
│ │ │ │ runc │ │ ││
│ │ │ │(OCI) │ │ ││
│ │ │ └───────┘ │ ││
│ │ └───────────┘ ││
│ └──────────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Registry │ │ Linux Kernel │ │
│ │ (Docker Hub) │ │ namespace + cgroups │ │
│ │ │ │ + UnionFS + Network │ │
│ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
整个架构的工作流程:
- 用户通过Docker Client输入命令(如
docker run nginx) - Docker Client将命令转换为REST API请求,发送给Docker Daemon
- Docker Daemon接收请求,进行相应的处理:
- 如果是运行容器,Daemon会调用containerd
- containerd调用runc来创建和启动容器
- runc利用Linux的namespace和cgroups来创建隔离环境
- 容器开始运行,Daemon持续监控容器状态
4.2 Docker Daemon(dockerd)
Docker Daemon是Docker架构中的核心服务端组件,通常以dockerd进程运行。它是Docker的"大脑",负责所有的管理工作。
4.2.1 dockerd的职责
Docker Daemon的主要职责包括:
- 镜像管理:构建(build)、拉取(pull)、推送(push)、删除镜像
- 容器管理:创建、启动、停止、删除容器
- 网络管理:创建和管理Docker网络(bridge、host、overlay等)
- 存储卷管理:创建和管理数据卷(Volume)
- API服务:提供REST API供客户端调用
- 日志管理:收集和存储容器日志
4.2.2 dockerd的配置
Docker Daemon的配置文件通常位于/etc/docker/daemon.json:
json
{
"data-root": "/var/lib/docker", // Docker数据存储路径
"registry-mirrors": [ // 镜像加速器
"https://registry.docker-cn.com"
],
"insecure-registries": [ // 不安全仓库(不验证证书)
"10.0.0.50:5000"
],
"live-restore": true, // Daemon重启时容器不中断
"max-concurrent-downloads": 10, // 最大并发下载数
"max-concurrent-uploads": 5, // 最大并发上传数
"storage-driver": "overlay2", // 存储驱动
"log-driver": "json-file", // 日志驱动
"log-opts": {
"max-size": "10m", // 单个日志文件最大10MB
"max-file": "3" // 最多保留3个日志文件
},
"default-ulimits": { // 默认ulimit设置
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 65536
}
},
"userns-remap": "default", // 用户命名空间重映射
"bip": "172.17.0.1/16", // docker0网桥IP
"dns": ["8.8.8.8", "8.8.4.4"] // 默认DNS
}
修改配置后需要重启Docker服务:
bash
# 重启Docker Daemon
sudo systemctl restart docker
# 查看Docker Daemon的状态
sudo systemctl status docker
# 查看Docker Daemon的运行信息
docker info
# 查看Docker Daemon的实际配置
docker info --format '{{json .}}' | jq .
4.2.3 dockerd的启动流程
1. 读取配置文件 /etc/docker/daemon.json
2. 初始化存储驱动(overlay2等)
3. 加载已存在的镜像和容器元数据
4. 初始化网络子系统(创建docker0网桥)
5. 初始化containerd
6. 注册REST API处理器
7. 监听Unix Socket(/var/run/docker.sock)或TCP端口
8. 如果启用了live-restore,恢复之前运行的容器
9. 开始接受客户端请求
4.3 Docker Client(docker CLI)
Docker Client是用户与Docker交互的主要界面。它是一个命令行工具,用户通过它输入各种Docker命令。
4.3.1 Docker Client的特点
- 轻量级:Docker Client本身不执行任何容器操作,它只是将用户的命令转换为API请求发送给Daemon
- 可远程连接:Client可以连接本地或远程的Docker Daemon
- 多Daemon支持:可以配置多个Docker上下文(Context),快速切换不同的Docker环境
bash
# 连接远程Docker Daemon
docker -H tcp://192.168.1.100:2375 ps
# 或者通过环境变量
export DOCKER_HOST=tcp://192.168.1.100:2375
docker ps
# 使用Docker Context管理多个环境
docker context create remote --docker "host=ssh://user@192.168.1.100"
docker context use remote
docker ps # 现在操作的是远程Docker
# 切换回本地
docker context use default
4.3.2 Docker命令体系
Docker命令分为管理命令(Management Commands)和普通命令:
bash
# 管理命令格式: docker <对象> <操作>
docker image ls # 列出镜像
docker image pull nginx # 拉取镜像
docker image rm nginx # 删除镜像
docker container ls # 列出容器
docker container stop # 停止容器
docker container rm # 删除容器
docker network ls # 列出网络
docker volume ls # 列出数据卷
# 兼容旧格式(简写)
docker images # = docker image ls
docker ps # = docker container ls
docker pull nginx # = docker image pull nginx
docker stop <id> # = docker container stop <id>
4.4 Docker REST API
Docker Daemon提供了完整的REST API,允许开发者通过HTTP请求来管理Docker。所有的docker CLI命令本质上都是对REST API的调用。
4.4.1 API版本
Docker API有版本号,不同版本的Docker支持不同版本的API:
bash
# 查看Docker API版本
docker version
# 输出中包含:
# Server:
# API version: 1.43
# API version: 1.43 (minimum version 1.12)
4.4.2 API使用示例
bash
# 列出所有容器(通过API)
curl --unix-socket /var/run/docker.sock http://localhost/v1.43/containers/json
# 创建一个容器
curl --unix-socket /var/run/docker.sock \
-X POST http://localhost/v1.43/containers/create \
-H "Content-Type: application/json" \
-d '{
"Image": "nginx",
"ExposedPorts": {"80/tcp": {}},
"HostConfig": {
"PortBindings": {"80/tcp": [{"HostPort": "8080"}]}
}
}'
# 启动容器
curl --unix-socket /var/run/docker.sock \
-X POST http://localhost/v1.43/containers/<container_id>/start
# 查看容器日志
curl --unix-socket /var/run/docker.sock \
"http://localhost/v1.43/containers/<container_id>/logs?stdout=true&follow=true"
如果Docker Daemon开启了TCP监听,还可以通过网络访问API:
bash
# 启用TCP监听(在daemon.json中配置)
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
# 警告: tcp://0.0.0.0:2375 没有认证,非常不安全!
# 生产环境应使用 tls:// 或通过SSH隧道
# 通过TCP访问API
curl http://192.168.1.100:2375/v1.43/info
4.4.3 Docker SDK
除了直接调用REST API,还可以使用Docker官方提供的SDK:
Python SDK:
python
# 安装: pip install docker
import docker
client = docker.from_env()
# 运行一个容器
container = client.containers.run("nginx", detach=True, ports={'80/tcp': 8080})
print(f"容器已启动: {container.id}")
# 查看容器日志
for line in container.logs(stream=True):
print(line.strip())
# 停止并删除容器
container.stop()
container.remove()
Go SDK:
go
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}
// 拉取镜像
_, err = cli.ImagePull(ctx, "nginx", types.ImagePullOptions{})
if err != nil {
panic(err)
}
// 创建容器
resp, err := cli.ContainerCreate(ctx,
&container.Config{Image: "nginx"},
&container.HostConfig{},
nil, nil, "",
)
if err != nil {
panic(err)
}
// 启动容器
err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
panic(err)
}
fmt.Printf("容器已启动: %s\n", resp.ID)
}
4.5 containerd与runc
4.5.1 为什么要拆分containerd和runc
在早期版本中,Docker Daemon承担了所有工作:镜像管理、容器生命周期管理、网络管理等。这种"大而全"的设计带来了几个问题:
- 单点故障:Docker Daemon出问题会影响所有容器
- 难以被其他工具复用:Kubernetes等编排工具如果要用Docker的运行时,必须安装完整的Docker
- 违循Unix哲学:一个程序应该只做一件事并做好
因此,Docker将容器运行时拆分成了多个独立的组件:
Docker Daemon (dockerd)
│
▼
containerd (高级运行时)
│ 管理镜像拉取、容器生命周期、网络
▼
runc (低级运行时 / OCI运行时)
│ 实际创建和运行容器
▼
Linux Kernel (namespace + cgroups)
4.5.2 containerd
containerd是一个高级容器运行时,负责:
- 镜像管理:拉取、推送、存储镜像
- 容器生命周期管理:创建、启动、停止、删除容器
- 快照管理:管理容器的文件系统层
- 网络管理:为容器配置网络
- 容器监控:监控容器状态和资源使用
containerd可以从Docker中独立使用:
bash
# 安装containerd
apt-get install containerd
# 使用ctr(containerd的CLI工具)拉取镜像
ctr images pull docker.io/library/nginx:latest
# 使用ctr运行容器
ctr run -d docker.io/library/nginx:latest mynginx
# 查看运行中的容器
ctr containers ls
# containerd的配置文件
cat /etc/containerd/config.toml
containerd还提供了nerdctl(一个兼容docker CLI语法的工具),让你可以用类似docker的命令操作containerd:
bash
# 安装nerdctl后
nerdctl run -d -p 8080:80 nginx
nerdctl ps
nerdctl images
4.5.3 runc
runc是OCI(Open Container Initiative)容器运行时的参考实现。它是一个轻量级的、低级的CLI工具,负责:
- 根据OCI规范创建容器
- 配置namespace和cgroups
- 启动容器进程
runc是containerd调用的最底层运行时。它可以直接使用:
bash
# 使用runc创建容器(需要手动准备rootfs和config.json)
mkdir -p /tmp/mycontainer/rootfs
# 准备rootfs(从镜像导出)
docker export $(docker create nginx) | tar -C /tmp/mycontainer/rootfs -xvf -
# 生成配置文件
cd /tmp/mycontainer
runc spec # 生成config.json
# 运行容器
runc run mycontainer
runc的工作流程:
- 读取
config.json配置文件 - 根据配置创建namespace(PID、NET、IPC、MNT、UTS、USER)
- 根据配置创建cgroups并设置资源限制
- 准备RootFS(挂载联合文件系统)
- 在新的namespace中执行容器的入口进程
4.5.4 OCI标准
OCI(Open Container Initiative)是2015年由Docker、Google、CoreOS等公司联合成立的开放标准组织,旨在制定容器格式的开放标准。OCI包含两个规范:
- OCI Image Spec:定义容器镜像的格式规范
- OCI Runtime Spec:定义容器运行时的行为规范
OCI标准使得任何遵循标准的镜像都可以在任何遵循标准的运行时上运行,实现了真正的可互换性。
4.6 Docker各组件协作流程详解
让我们通过一个完整的例子来追踪docker run nginx命令的完整执行流程:
步骤1: 用户输入命令
docker run -d -p 8080:80 nginx
步骤2: Docker Client处理
- 解析命令参数
- 构造REST API请求: POST /containers/create
- 发送给Docker Daemon
步骤3: Docker Daemon接收请求
- 检查本地是否有nginx镜像
- 如果没有,执行镜像拉取:
a. 向Registry发起拉取请求
b. 下载镜像的所有层(layer)
c. 验证每层的SHA256摘要
d. 解压并存储到/var/lib/docker/overlay2/
- 创建容器配置(根据命令参数)
- 调用containerd创建容器
步骤4: containerd处理
- 接收创建容器的请求
- 准备容器的RootFS(挂载OverlayFS层)
- 生成OCI runtime spec(config.json)
- 调用runc创建容器
步骤5: runc处理
- 读取OCI spec
- clone()创建新进程,同时创建新的namespace:
- CLONE_NEWPID → PID namespace
- CLONE_NEWNET → NET namespace
- CLONE_NEWNS → MNT namespace
- CLONE_NEWUTS → UTS namespace
- CLONE_NEWIPC → IPC namespace
- 设置cgroups限制(CPU、内存等)
- pivot_root切换根文件系统
- exec()执行容器的入口命令(nginx)
步骤6: 容器运行
- nginx进程在隔离的namespace中运行
- Docker Daemon配置端口映射(iptables NAT规则)
- containerd监控容器进程状态
步骤7: 状态回报
- runc退出(容器进程已启动)
- containerd向Docker Daemon报告容器已运行
- Docker Daemon向Client返回容器ID
- Client输出容器ID给用户
用一张时序图来表示这个过程:
User Docker Client Docker Daemon containerd runc Kernel
│ │ │ │ │ │
│─ docker run ─▶│ │ │ │ │
│ │── POST API ────▶│ │ │ │
│ │ │──检查镜像───│ │ │
│ │ │ (如不存在则拉取) │ │
│ │ │──Create()───▶│ │ │
│ │ │ │──准备rootfs │
│ │ │ │──Create()─▶│ │
│ │ │ │ │──clone()│
│ │ │ │ │ 创建namespace
│ │ │ │ │ 设置cgroups
│ │ │ │ │──exec()─▶│
│ │ │ │ │ │──nginx启动
│ │ │ │ │◀──容器运行中
│ │ │ │◀──成功───│ │
│ │ │◀──容器ID────│ │ │
│ │◀──容器ID───────│ │ │ │
│◀──输出ID──────│ │ │ │ │
4.7 Docker底层的Linux内核技术
4.7.1 namespace详解
我们已经在前面的章节中概述了6种namespace,这里进一步深入讲解它们的技术细节。
PID Namespace深入
PID namespace的创建通过clone()或unshare()系统调用配合CLONE_NEWPID标志来实现:
c
// C语言示例:创建一个新的PID namespace
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
// 子进程函数
int child_func(void *arg) {
printf("容器内PID: %d\n", getpid()); // 输出1
printf("父进程PID: %d\n", getppid()); // 输出0
// 在容器内,这个进程是PID 1
// 它看不到宿主机上的其他进程
execlp("sleep", "sleep", "100", NULL);
return 0;
}
int main() {
// 分配子进程的栈空间
char *stack = malloc(1024 * 1024);
char *stackTop = stack + 1024 * 1024;
// 创建子进程,使用新的PID namespace
pid_t child_pid = clone(child_func, stackTop,
CLONE_NEWPID | SIGCHLD, NULL);
printf("宿主机上的子进程PID: %d\n", child_pid);
// 在宿主机上,子进程有一个正常的PID(如12345)
waitpid(child_pid, NULL, 0);
free(stack);
return 0;
}
PID namespace的一个重要特性是嵌套:可以在一个PID namespace内创建子PID namespace。嵌套层次最多为32层。在子namespace中看到的进程,在父namespace中也能看到,但反之不行。
NET Namespace深入
NET namespace隔离了完整的网络协议栈,包括:
- 网络接口(网卡)
- IPv4和IPv6地址
- 路由表
- 防火墙规则(iptables)
- ARP表
- 端口号空间
- /proc/net目录
- /sys/class/net目录
bash
# 使用ip命令创建和操作NET namespace
# 创建一个新的network namespace
ip netns add mynetns
# 在新namespace中执行命令
ip netns exec mynetns ip addr
# 可以看到只有一个lo接口,状态为DOWN
# 在新namespace中启动lo接口
ip netns exec mynetns ip link set lo up
# 创建veth pair连接两个namespace
ip link add veth0 type veth peer name veth1
ip link set veth1 netns mynetns
# 在宿主机配置veth0
ip addr add 10.0.0.1/24 dev veth0
ip link set veth0 up
# 在新namespace中配置veth1
ip netns exec mynetns ip addr add 10.0.0.2/24 dev veth1
ip netns exec mynetns ip link set veth1 up
# 测试连通性
ip netns exec mynetns ping 10.0.0.1
# 删除namespace
ip netns del mynetns
Docker的网络模式正是基于NET namespace实现的:
- bridge模式(默认):容器有独立的NET namespace,通过veth pair连接到docker0网桥
- host模式:容器不创建新的NET namespace,直接使用宿主机的网络栈
- none模式:容器有独立的NET namespace,但不配置任何网络
- container模式:容器与另一个容器共享NET namespace
MNT Namespace深入
MNT namespace隔离了挂载点(mountpoint)的视图。在MNT namespace中的挂载操作不会影响其他namespace。
bash
# 创建新的mount namespace
unshare -m /bin/bash
# 在新namespace中挂载一个tmpfs
mount -t tmpfs none /tmp
# 在这个namespace中,/tmp是一个独立的tmpfs
mount | grep tmp
# none on /tmp type tmpfs (rw)
# 在另一个终端(宿主机的mount namespace)中查看
# /tmp 并没有tmpfs挂载
Docker使用MNT namespace来实现容器文件系统的隔离。容器内的进程只能看到容器自己的文件系统层次结构,看不到宿主机的文件系统(除非通过volume挂载)。
USER Namespace深入
USER namespace允许在namespace内外映射不同的UID/GID。这是实现容器安全的重要特性。
bash
# 启用user namespace
# 在 /etc/docker/daemon.json 中添加:
{
"userns-remap": "default"
}
# 重启Docker
sudo systemctl restart docker
# 现在运行一个容器
docker run -it --rm alpine id
# 输出: uid=0(root) gid=0(root)
# 但在宿主机上,这个进程的实际UID是:
docker inspect $(docker ps -ql) | grep -i uid
# 可以看到实际运行用户是 dockremap (如UID 100000)
这意味着即使容器内的进程是root用户,它在宿主机上也只是一个普通用户。如果攻击者从容器中逃逸到宿主机,他获得的也只是非特权用户的权限,大大降低了安全风险。
4.7.2 cgroups详解
cgroups v1 vs v2
cgroups v1(传统模式):每个子系统有独立的层次结构,一个进程可以属于不同的cgroup(每个子系统一个)。
cgroups v2(统一模式):所有子系统共享同一个层次结构,管理更简单,性能更好。Linux 4.5引入,Docker 20.10+开始支持。
bash
# 检查系统使用的cgroups版本
stat /sys/fs/cgroup/cgroup.controllers
# 如果存在,说明使用cgroups v2
# 如果不存在,但存在 /sys/fs/cgroup/cpu/ 等目录,说明使用cgroups v1
# 在Docker中启用cgroups v2
# /etc/docker/daemon.json
{
"default-cgroupns-mode": "host"
}
CPU限制原理
Docker使用CFS(Completely Fair Scheduler)调度器来实现CPU限制:
-
cpu.cfs_period_us:调度周期(默认100000微秒=100ms) -
cpu.cfs_quota_us:在一个周期内可用的CPU时间 -
cpu.shares:CPU权重(相对值,默认1024)假设系统有4个CPU核心,cpu.cfs_period_us=100000(100ms)
场景1: --cpus=2.0
cpu.cfs_quota_us = 200000 (200ms)
在每100ms周期内,容器可以使用200ms的CPU时间
由于有4个核心,200ms/100ms = 2个核心 = 50%的总CPU场景2: --cpus=0.5
cpu.cfs_quota_us = 50000 (50ms)
在每100ms周期内,容器只能使用50ms的CPU时间
50ms/100ms = 0.5个核心场景3: --cpu-shares=512
cpu.shares = 512 (默认1024)
当CPU繁忙时,该容器获得 CPU时间的比例为 512/(所有容器shares之和)
当CPU空闲时,容器可以使用所有可用CPU
内存限制原理
bash
# Docker内存限制对应的cgroups文件:
# /sys/fs/cgroup/memory/docker/<id>/memory.limit_in_bytes
# /sys/fs/cgroup/memory/docker/<id>/memory.soft_limit_in_bytes
# /sys/fs/cgroup/memory/docker/<id>/memory.memsw.limit_in_bytes
# memory.limit_in_bytes: 硬限制
# 当容器内存使用超过此值时:
# - 如果启用了Swap且未达到Swap限制,使用Swap
# - 如果未启用Swap或达到Swap限制,触发OOM Killer
# memory.soft_limit_in_bytes: 软限制
# 系统内存紧张时,优先回收超过软限制的容器内存
# OOM行为:
# memory.oom_control = 0: 默认,OOM时杀死容器内进程
# memory.oom_control = 1: 禁止OOM Killer,进程会被暂停
bash
# 实际查看容器的内存限制
docker exec -it test cat /sys/fs/cgroup/memory/memory.limit_in_bytes
# 或在宿主机上查看
cat /sys/fs/cgroup/memory/docker/$(docker inspect -f '{{.Id}}' test)/memory.limit_in_bytes
# 输出: 536870912 (512MB)
IO限制原理
bash
# Docker IO限制对应的cgroups文件(blkio子系统):
# /sys/fs/cgroup/blkio/docker/<id>/blkio.throttle.read_bps_device
# /sys/fs/cgroup/blkio/docker/<id>/blkio.throttle.write_bps_device
# 格式: <major>:<minor> <bytes_per_second>
# 例如: 8:0 10485760 表示对/dev/sda(major:8, minor:0)限制读速度为10MB/s
# 查看设备的major:minor
ls -l /dev/sda
# brw-rw---- 1 root disk 8, 0 Jan 1 10:00 /dev/sda
# major=8, minor=0
4.7.3 容器安全的基石:Seccomp与AppArmor/SELinux
除了namespace和cgroups,Docker还利用了另外两个Linux安全机制:
Seccomp(Secure Computing Mode)
Seccomp限制容器内进程可以调用的系统调用(syscall)。Docker默认启用了Seccomp,阻止了约44个危险系统调用(如reboot、mount、pivot_root等)。
json
// Docker默认Seccomp配置(简化版)
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["reboot", "mount", "umount2", "pivot_root"],
"action": "SCMP_ACT_ERRNO"
}
]
}
bash
# 查看容器的Seccomp配置
docker info | grep "Security Options"
# 输出: Security Options: seccomp apparmor
# 自定义Seccomp配置
docker run --security-opt seccomp=/path/to/seccomp.json nginx
# 禁用Seccomp(不安全,仅用于调试)
docker run --security-opt seccomp=unconfined nginx
AppArmor / SELinux
这两个是Linux的强制访问控制(MAC)系统:
- AppArmor:Ubuntu/Debian默认使用,基于路径
- SELinux:RHEL/CentOS默认使用,基于标签
bash
# 查看Docker使用的AppArmor配置
docker info | grep "Security Options"
# 输出: Security Options: seccomp apparmor
# Docker默认加载的AppArmor配置文件: docker-default
# 限制容器内进程能访问的文件路径和能力
# 自定义AppArmor配置
docker run --security-opt apparmor=my-profile nginx
Linux Capabilities
Linux将root权限拆分为约40个细粒度的能力(capabilities),Docker默认只授予容器一小部分:
bash
# 查看容器默认拥有的capabilities
docker run --rm alpine cat /proc/1/status | grep Cap
# Docker默认授予的capabilities:
# CAP_CHOWN - 修改文件所有者
# CAP_DAC_OVERRIDE - 绕过文件权限检查
# CAP_FSETID - 设置setuid位
# CAP_FOWNER - 绕过文件所有者检查
# CAP_MKNOD - 创建特殊文件
# CAP_NET_RAW - 使用RAW套接字
# CAP_SETGID - 设置GID
# CAP_SETUID - 设置UID
# CAP_SETFCAP - 设置文件capabilities
# CAP_SETPCAP - 修改进程capabilities
# CAP_NET_BIND_SERVICE - 绑定1024以下端口
# CAP_SYS_CHROOT - 使用chroot
# CAP_KILL - 发送信号
# CAP_AUDIT_WRITE - 写审计日志
# 添加/删除capabilities
docker run --cap-add SYS_ADMIN --cap-drop NET_RAW nginx
# 授予所有capabilities(危险!)
docker run --privileged nginx
--privileged标志会授予容器所有capabilities,并禁用Seccomp和AppArmor保护。这相当于让容器拥有了宿主机root的所有权限,极度危险,应尽量避免使用。
第五章 Docker核心概念
在前面的章节中,我们已经多次提到了镜像、容器、仓库等概念。本章将对Docker的五大核心概念------镜像(Image)、容器(Container)、仓库(Registry)、数据卷(Volume)和网络(Network)------进行系统而深入的讲解。这些概念是理解和使用Docker的基础,就像学习编程语言必须先理解变量、函数和类一样重要。
5.1 镜像(Image):只读模板、分层存储、Union FS
5.1.1 什么是Docker镜像
Docker镜像是一个只读的模板,它包含了运行应用所需的所有内容:代码、运行时、库、环境变量和配置文件。镜像是创建容器的"蓝图"------你可以把镜像理解为面向对象编程中的"类",而容器是"实例"。
bash
# 查看本地镜像
docker images
# 输出示例:
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx latest 605c77e624dd 2 weeks ago 141MB
# redis 7-alpine 3905c21d8b0f 3 weeks ago 30MB
# mysql 8.0 2a91dfd77e2e 4 weeks ago 521MB
# myapp 1.0 a1b2c3d4e5f6 1 day ago 350MB
每个镜像都有以下关键属性:
- REPOSITORY:镜像仓库名称(如nginx)
- TAG:镜像标签(如latest、1.25-alpine),用于区分不同版本
- IMAGE ID:镜像的唯一标识符(SHA256摘要的前12位)
- CREATED:创建时间
- SIZE:镜像大小
5.1.2 镜像的分层结构
Docker镜像不是一个大文件,而是由多个层(Layer)组成的。每一层对应Dockerfile中的一条指令。
以一个Node.js应用的Dockerfile为例:
dockerfile
FROM node:18-alpine # 基础镜像层:约40MB
WORKDIR /app # 不产生新层(只是元数据修改)
COPY package*.json ./ # 新层:包含package.json文件,约1KB
RUN npm install # 新层:包含node_modules,约50MB
COPY . . # 新层:包含源代码,约5MB
EXPOSE 3000 # 不产生新层(只是元数据)
CMD ["node", "app.js"] # 不产生新层(只是元数据)
这个镜像的分层结构如下:
┌─────────────────────────────┐
│ Layer 5: COPY . . │ 5MB ← 可写层(容器运行时添加)
├─────────────────────────────┤
│ Layer 4: RUN npm install │ 50MB ← 只读
├─────────────────────────────┤
│ Layer 3: COPY package*.json │ 1KB ← 只读
├─────────────────────────────┤
│ Layer 2: node:18-alpine │ 40MB ← 只读(基础镜像)
├─────────────────────────────┤
│ Layer 1: alpine:3.17 │ 7MB ← 只读(更基础的基础镜像)
└─────────────────────────────┘
总大小:约47MB(而非40+50+5+1+7=103MB,因为层之间有复用)
分层存储的核心优势在于复用:
bash
# 假设你有3个应用都基于node:18-alpine
# 传统方式:每个应用都包含完整的环境 = 40MB x 3 = 120MB
# Docker方式:node:18-alpine只存储一份 = 40MB + 各应用差异层
# 查看镜像的分层信息
docker history myapp:1.0
# 输出示例:
# IMAGE CREATED CREATED BY SIZE
# a1b2c3d4e5f6 1 day ago /bin/sh -c #(nop) CMD ["node" "app.js"] 0B
# b2c3d4e5f6a7 1 day ago /bin/sh -c #(nop) COPY . . 5MB
# c3d4e5f6a7b8 1 day ago /bin/sh -c npm install 50MB
# d4e5f6a7b8c9 1 day ago /bin/sh -c #(nop) COPY package*.json ./ 1kB
# e5f6a7b8c9d0 2 days ago /bin/sh -c #(nop) WORKDIR /app 0B
# 605c77e624dd 2 weeks ago /bin/sh -c #(nop) CMD ["node"] 0B
# ...更多层...
5.1.3 联合文件系统(UnionFS)的实现
Docker使用联合文件系统将多个层"叠加"为一个统一的文件系统视图。目前Docker默认使用OverlayFS(overlay2驱动)。
OverlayFS涉及三个概念:
-
LowerDir(下层):只读的镜像层,可以有多个
-
UpperDir(上层):可写的容器层,只有一个
-
MergedDir(合并层):对外呈现的统一视图
读取文件 /app/index.js 的过程:
-
在UpperDir(容器层)中查找 /app/index.js
→ 如果找到,返回该文件
→ 如果没找到,继续向下查找 -
在LowerDir(镜像层)中依次查找
Layer 5 → Layer 4 → Layer 3 → Layer 2 → Layer 1
→ 找到后返回
修改文件 /app/config.json 的过程(Copy-on-Write):
- 从LowerDir中找到 /app/config.json
- 将文件复制到UpperDir
- 在UpperDir中修改该文件
- LowerDir中的原文件保持不变(只读)
删除文件 /app/old.js 的过程:
- 在UpperDir中创建一个whiteout文件
(标记该文件已被删除) - 合并层中看不到该文件
- LowerDir中的原文件仍然存在(只是被"遮蔽"了)
-
可以通过docker inspect查看容器的文件系统层信息:
bash
# 查看容器的OverlayFS信息
docker inspect test --format '{{json .GraphDriver.Data}}' | jq .
# 输出示例:
# {
# "LowerDir": "/var/lib/docker/overlay2/l/ABC...:/var/lib/docker/overlay2/l/DEF...",
# "UpperDir": "/var/lib/docker/overlay2/ghi789/diff",
# "WorkDir": "/var/lib/docker/overlay2/ghi789/work",
# "MergedDir": "/var/lib/docker/overlay2/ghi789/merged"
# }
# 在宿主机上查看容器的可写层
ls /var/lib/docker/overlay2/ghi789/diff/
# 这里存放了容器运行期间所有修改过的文件
5.1.4 镜像的构建方式
方式1:通过Dockerfile构建(推荐)
dockerfile
# Dockerfile
FROM python:3.11-slim
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
bash
# 构建镜像
docker build -t myapp:1.0 .
# 构建时传入变量
docker build -t myapp:1.0 --build-arg ENV=production .
# 不使用缓存构建
docker build -t myapp:1.0 --no-cache .
方式2:从容器创建镜像
bash
# 运行一个容器
docker run -it --name temp ubuntu:22.04 /bin/bash
# 在容器内安装软件
apt-get update
apt-get install -y nginx
# 退出容器
exit
# 将容器提交为镜像
docker commit -m "Added nginx" -a "Author" temp myubuntu:nginx
# 删除临时容器
docker rm temp
这种方式不推荐用于生产环境,因为它不可追溯、不可复现。但在快速测试时很有用。
方式3:从tar文件导入
bash
# 导出镜像为tar文件
docker save -o myapp.tar myapp:1.0
# 从tar文件导入镜像
docker load -i myapp.tar
# 从rootfs tarball导入(不包含元数据)
docker import rootfs.tar myimage:1.0
5.1.5 镜像优化最佳实践
dockerfile
# ❌ 不好的实践:每条RUN都产生一层
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*
# 问题:apt-get update产生的缓存层仍然存在
# 即使后续删除了缓存,层的叠加使得镜像更大
# ✅ 好的实践:合并RUN指令
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y --no-install-recommends nginx && \
rm -rf /var/lib/apt/lists/*
# 优势:更新、安装、清理在一个层中完成,缓存不会残留
# ✅ 使用多阶段构建
# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp -ldflags="-s -w" .
# 运行阶段
FROM alpine:3.18
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]
# 优势:最终镜像只包含alpine和编译后的二进制文件
# 不包含Go编译器等构建工具,镜像极小
5.2 容器(Container):镜像的运行实例、可写层
5.2.1 容器是什么
容器是镜像的运行实例。如果把镜像比作面向对象编程中的"类",那么容器就是"对象"。一个镜像可以创建多个容器实例,它们相互独立。
bash
# 从nginx镜像创建并启动一个容器
docker run -d --name web1 -p 8080:80 nginx
# 从同一个镜像创建第二个容器
docker run -d --name web2 -p 8081:80 nginx
# 两个容器使用相同的镜像,但运行状态完全独立
docker ps
# CONTAINER ID IMAGE PORTS NAMES
# a1b2c3d4e5f6 nginx 0.0.0.0:8080->80/tcp web1
# b2c3d4e5f6a7 nginx 0.0.0.0:8081->80/tcp web2
5.2.2 容器的生命周期
容器从创建到销毁经历以下几个阶段:
created → running → paused → stopped → deleted
│ │ │ │ │
│ │ │ │ └─ docker rm
│ │ │ └─ docker stop / docker kill
│ │ └─ docker pause
│ └─ docker start / docker unpause
└─ docker create
各状态的详细说明:
| 状态 | 说明 | 命令 |
|---|---|---|
| created | 容器已创建但未启动 | docker create |
| running | 容器正在运行 | docker start / docker run |
| paused | 容器进程被暂停(冻结) | docker pause |
| stopped/exited | 容器已停止 | docker stop / docker kill |
| deleted | 容器已被删除 | docker rm |
bash
# 完整的容器生命周期操作
docker create --name myapp nginx # 创建容器(不启动)
docker start myapp # 启动容器
docker pause myapp # 暂停容器(冻结进程)
docker unpause myapp # 恢复容器
docker stop myapp # 停止容器(优雅关闭)
docker kill myapp # 强制杀死容器
docker rm myapp # 删除容器
# docker run = docker create + docker start
docker run --name myapp nginx # 创建并启动
5.2.3 容器的可写层
当容器启动时,Docker在镜像的只读层之上添加一个可写层(Container Layer)。所有对文件系统的修改都发生在这个可写层中。
┌─────────────────────────┐
│ 可写层 (Container Layer) │ ← 容器运行期间的所有修改
│ 大小:默认最大10GB │
├─────────────────────────┤
│ 只读层 (Image Layer 3) │ ← 来自镜像
├─────────────────────────┤
│ 只读层 (Image Layer 2) │
├─────────────────────────┤
│ 只读层 (Image Layer 1) │
└─────────────────────────┘
可写层的重要特性:
- 与容器同生命周期:容器删除时,可写层也会被删除
- Copy-on-Write:修改只读层的文件时,先复制到可写层再修改
- 存储驱动管理:由overlay2等存储驱动管理
bash
# 查看容器的可写层大小
docker ps -s
# CONTAINER ID IMAGE ... SIZE
# a1b2c3d4e5f6 nginx ... 2.3MB (virtual 141MB)
# SIZE = 可写层大小
# virtual = 可写层 + 所有镜像层的总大小
# 查看容器的可写层内容
docker diff myapp
# 输出示例:
# C /etc
# C /etc/nginx/conf.d
# A /etc/nginx/conf.d/default.conf.bak (A=Added)
# C /var/log/nginx
# C /var/log/nginx/access.log (C=Changed)
# D /var/cache/nginx/old_cache (D=Deleted)
5.2.4 容器与镜像的关系
┌──────────┐
│ 镜像 │ ← 只读模板
│ (Image) │
└────┬─────┘
│
┌────────────┼────────────┐
│ docker run │ │ docker commit
▼ │ ▼
┌──────────┐ │ ┌──────────┐
│ 容器 1 │ │ │ 新镜像 │
│(可写层) │ │ │(只读) │
└──────────┘ │ └──────────┘
│ │
│ docker run │
▼ │
┌──────────┐ │
│ 容器 2 │ │
│(可写层) │ │
└──────────┘ │
│
镜像 → docker run → 容器(运行实例)
容器 → docker commit → 新镜像(固化修改)
镜像 → docker push/pull → 仓库(分发)
5.3 仓库(Registry):Docker Hub、私有仓库、镜像推送拉取
5.3.1 什么是镜像仓库
镜像仓库(Registry)是存储和分发Docker镜像的服务。它类似于代码版本控制中的Git仓库,但存储的是Docker镜像而不是代码。
仓库体系结构:
Registry(仓库服务)
└── Repository(仓库)
└── Tag(标签)
└── Image(镜像)
例如:docker.io/library/nginx:1.25
- Registry:
docker.io(Docker Hub) - Repository:
library/nginx - Tag:
1.25
5.3.2 Docker Hub
Docker Hub是Docker官方的公共镜像仓库,地址为https://hub.docker.com。它包含:
- Official Images(官方镜像):由Docker公司维护,如nginx、redis、mysql等
- Verified Publisher(认证发布者):由官方软件厂商维护,如微软的mssql-server
- Community Images(社区镜像):由社区用户上传
bash
# 登录Docker Hub
docker login
# 搜索镜像
docker search nginx
# 输出示例:
# NAME DESCRIPTION STARS OFFICIAL
# nginx Official build of Nginx. 18000 [OK]
# jonasal/nginx-certbot Light-weight Nginx with Certbot... 500
# nginxinc/nginx-unmod Unprivileged NGINX Dockerfiles 100
# 拉取镜像(默认latest标签)
docker pull nginx
# 拉取指定版本
docker pull nginx:1.25-alpine
# 拉取指定平台的镜像
docker pull --platform linux/arm64 nginx:latest
# 推送自己的镜像到Docker Hub
docker tag myapp:1.0 username/myapp:1.0
docker push username/myapp:1.0
5.3.3 私有仓库
在生产环境中,通常需要搭建私有仓库来存储企业内部的镜像。常用的私有仓库方案:
方案1:Docker Registry(轻量级)
bash
# 运行官方的registry容器
docker run -d -p 5000:5000 --name registry \
-v /data/registry:/var/lib/registry \
--restart always \
registry:2
# 给镜像打标签(指向私有仓库)
docker tag myapp:1.0 localhost:5000/myapp:1.0
# 推送到私有仓库
docker push localhost:5000/myapp:1.0
# 从私有仓库拉取
docker pull localhost:5000/myapp:1.0
# 如果使用HTTP(非HTTPS),需要在daemon.json中配置insecure-registries
{
"insecure-registries": ["192.168.1.100:5000"]
}
方案2:Harbor(企业级)
Harbor是VMware开源的企业级镜像仓库,提供了丰富的功能:
yaml
# Harbor的主要功能:
# - 基于角色的访问控制(RBAC)
# - 镜像漏洞扫描(集成Trivy)
# - 镜像签名(集成Notary)
# - 垃圾回收(清理无引用的镜像层)
# - 复制策略(跨数据中心同步)
# - 审计日志
# - RESTful API
# - 图形化管理界面
# 使用docker-compose安装Harbor
wget https://github.com/goharbor/harbor/releases/download/v2.8.0/harbor-offline-installer-v2.8.0.tgz
tar xvf harbor-offline-installer-v2.8.0.tgz
cd harbor
# 修改配置
cp harbor.yml.tmpl harbor.yml
# 编辑 harbor.yml:
# hostname: harbor.mycompany.com
# http:
# port: 80
# harbor_admin_password: Harbor12345
# 安装
./install.sh
# 安装完成后访问 http://harbor.mycompany.com
# 默认账号: admin / Harbor12345
bash
# 使用Harbor仓库
docker login harbor.mycompany.com
# 推送镜像到Harbor
docker tag myapp:1.0 harbor.mycompany.com/dev/myapp:1.0
docker push harbor.mycompany.com/dev/myapp:1.0
方案3:云厂商镜像服务
各大云厂商都提供托管的镜像仓库服务:
- 阿里云:ACR(Container Registry)
- 腾讯云:TCR(Tencent Container Registry)
- AWS:ECR(Elastic Container Registry)
- GCP:Artifact Registry
5.4 数据卷(Volume):数据持久化、数据共享
5.4.1 为什么需要数据卷
容器是短暂的------容器删除后,其可写层中的所有数据都会丢失。但很多应用需要持久化数据(如数据库的数据文件),这就需要数据卷。
bash
# 演示容器数据的临时性
docker run -d --name temp-db mysql:8.0
# ...往数据库写入数据...
docker stop temp-db && docker rm temp-db
# 数据全部丢失!
# 使用数据卷持久化
docker run -d --name persistent-db \
-v mysql_data:/var/lib/mysql \
mysql:8.0
# ...往数据库写入数据...
docker stop persistent-db && docker rm persistent-db
# 数据卷中的数据仍然存在
docker run -d --name new-db \
-v mysql_data:/var/lib/mysql \
mysql:8.0
# 新容器可以使用之前的数据
5.4.2 Docker的三种数据挂载方式
┌──────────────────────────────────────────────────────────┐
│ 宿主机 │
│ │
│ /var/lib/docker/volumes/ /path/on/host │
│ ┌──────────────────┐ ┌──────────────┐ │
│ │ Volume (数据卷) │ │ Bind Mount │ │
│ │ (Docker管理) │ │ (直接挂载) │ │
│ └────────┬─────────┘ └──────┬───────┘ │
│ │ │ │
│ ┌────────┼──────────────────────────┼───────────────┐ │
│ │ │ 容器 │ │ │
│ │ Volume Mount │ Bind Mount│ │ │
│ │ (/data) │ (/config) │ │ │
│ │ │ │ │ │
│ │ tmpfs Mount (内存) │ │ │
│ │ (/tmp) │ │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
方式1:Volume(数据卷)------推荐
Volume由Docker管理,存储在/var/lib/docker/volumes/目录下。
bash
# 创建一个命名数据卷
docker volume create mydata
# 查看数据卷
docker volume ls
# DRIVER VOLUME NAME
# local mydata
# 查看数据卷详细信息
docker volume inspect mydata
# [
# {
# "CreatedAt": "2024-01-01T10:00:00Z",
# "Driver": "local",
# "Mountpoint": "/var/lib/docker/volumes/mydata/_data",
# "Name": "mydata",
# "Options": null,
# "Scope": "local"
# }
# ]
# 使用数据卷启动容器
docker run -d --name myapp \
-v mydata:/app/data \
nginx
# 匿名数据卷(自动生成名称)
docker run -d -v /app/data nginx
# 删除数据卷
docker volume rm mydata
# 清理无引用的数据卷
docker volume prune
方式2:Bind Mount(绑定挂载)
Bind Mount直接将宿主机的目录挂载到容器中,适合开发环境。
bash
# 将宿主机的/data目录挂载到容器的/app/data
docker run -d --name myapp \
-v /data/on/host:/app/data \
nginx
# 使用只读模式
docker run -d --name myapp \
-v /data/on/host:/app/data:ro \
nginx
# 使用--mount语法(更明确)
docker run -d --name myapp \
--mount type=bind,source=/data/on/host,target=/app/data \
nginx
Bind Mount vs Volume的对比:
| 特性 | Volume | Bind Mount |
|---|---|---|
| 存储位置 | Docker管理(/var/lib/docker/volumes/) | 宿主机任意路径 |
| 创建方式 | docker volume create | 自动创建 |
| 跨平台 | 好(路径由Docker管理) | 差(依赖宿主机路径) |
| 备份/迁移 | 方便(docker volume操作) | 需要手动操作文件 |
| 权限管理 | Docker自动处理 | 需要手动处理UID/GID |
| 适用场景 | 生产环境数据持久化 | 开发环境代码挂载 |
方式3:tmpfs Mount(内存挂载)
tmpfs将数据存储在内存中,容器停止后数据消失。适合存储敏感信息或临时文件。
bash
# 使用tmpfs挂载
docker run -d --name myapp \
--tmpfs /app/cache \
nginx
# 或使用--mount语法
docker run -d --name myapp \
--mount type=tmpfs,target=/app/cache,tmpfs-size=100m \
nginx
5.4.3 数据卷的共享
容器间共享数据卷
bash
# 创建共享数据卷
docker volume create shared_data
# 容器1使用该数据卷
docker run -d --name app1 -v shared_data:/data nginx
# 容器2使用同一个数据卷
docker run -d --name app2 -v shared_data:/data nginx
# 现在app1和app2可以读写同一个目录
# 使用--volumes-from继承容器的挂载
docker run -d --name app3 --volumes-from app1 nginx
# app3继承了app1的所有数据卷挂载
数据卷的备份与恢复
bash
# 备份数据卷
docker run --rm \
-v mydata:/source:ro \
-v /backup:/backup \
alpine tar czf /backup/mydata_backup.tar.gz -C /source .
# 恢复数据卷
docker volume create mydata_restored
docker run --rm \
-v mydata_restored:/target \
-v /backup:/backup \
alpine tar xzf /backup/mydata_backup.tar.gz -C /target
5.5 网络(Network):容器间通信、端口映射
5.5.1 Docker网络模型
Docker提供了多种网络驱动,满足不同的通信需求:
| 网络驱动 | 说明 | 适用场景 |
|---|---|---|
| bridge | 默认,通过网桥连接容器 | 单机容器通信 |
| host | 容器使用宿主机网络栈 | 需要最高网络性能 |
| none | 无网络 | 安全隔离 |
| overlay | 跨主机容器通信 | Docker Swarm集群 |
| macvlan | 为容器分配MAC地址 | 需要容器直接出现在物理网络 |
| custom | 自定义网络插件 | 特殊网络需求 |
5.5.2 bridge网络详解
bridge是Docker的默认网络驱动。Docker启动时会自动创建一个名为docker0的虚拟网桥。
外部网络
│
│ NAT (iptables MASQUERADE)
│
┌───┴───────────────────────────────────────┐
│ docker0 (172.17.0.1/16) │ ← 虚拟网桥
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │veth0│ │veth1│ │veth2│ │ ← 虚拟网卡对(宿主机端)
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │
│ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ │
│ │eth0 │ │eth0 │ │eth0 │ │ ← 容器内网卡
│ │172. │ │172. │ │172. │ │
│ │17.0.│ │17.0.│ │17.0.│ │
│ │ 2 │ │ 3 │ │ 4 │ │
│ └─────┘ └─────┘ └─────┘ │
│ 容器1 容器2 容器3 │
└───────────────────────────────────────────┘
bash
# 查看Docker网络
docker network ls
# 输出:
# NETWORK ID NAME DRIVER SCOPE
# a1b2c3d4e5f6 bridge bridge local
# b2c3d4e5f6a7 host host local
# c3d4e5f6a7b8 none null local
# 查看bridge网络详情
docker network inspect bridge
# 创建自定义bridge网络
docker network create --driver bridge \
--subnet 172.20.0.0/16 \
--gateway 172.20.0.1 \
mynetwork
# 使用自定义网络启动容器
docker run -d --name web --network mynetwork nginx
docker run -d --name db --network mynetwork mysql:8.0
# 在自定义网络中,容器可以通过名称互相访问
docker exec web ping db # 通过容器名解析
docker exec web curl http://db:3306
自定义bridge网络相比默认bridge网络的优势:
- DNS解析:自定义网络中的容器可以通过容器名互相访问(默认bridge不支持)
- 更好的隔离:不同自定义网络中的容器互不可见
- 可配置性:可以指定子网、网关、IP范围等
5.5.3 端口映射
bash
# 将容器端口映射到宿主机
docker run -d -p 8080:80 nginx
# 宿主机8080端口 → 容器80端口
# 映射到特定IP
docker run -d -p 127.0.0.1:8080:80 nginx
# 映射UDP端口
docker run -d -p 53:53/udp dns-server
# 随机端口映射
docker run -d -P nginx
# Docker自动分配一个高端口(32768~60999)映射到容器的80端口
# 查看端口映射
docker port myapp
# 80/tcp -> 0.0.0.0:8080
# 多端口映射
docker run -d -p 8080:80 -p 8443:443 nginx
端口映射的底层实现是iptables的DNAT规则:
bash
# 查看Docker创建的iptables规则
sudo iptables -t nat -L -n | grep docker
# 输出示例:
# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
# 这条规则将访问宿主机8080端口的流量转发到容器172.17.0.2:80
5.5.4 host网络模式
bash
# 使用host网络模式
docker run -d --network host nginx
# 容器直接使用宿主机的网络栈
# 不需要端口映射,容器直接监听宿主机端口
# 网络性能最好(没有NAT开销)
# 但端口可能与宿主机上的其他服务冲突
5.5.5 overlay网络(跨主机通信)
overlay网络使用VXLAN技术实现跨主机容器通信,主要用于Docker Swarm集群:
bash
# 在Swarm管理节点创建overlay网络
docker network create --driver overlay --attachable myoverlay
# 在不同主机上的容器可以通过overlay网络通信
# 主机A:
docker run -d --name web --network myoverlay nginx
# 主机B:
docker run -d --name db --network myoverlay mysql:8.0
# web容器可以通过容器名访问db容器
docker exec web ping db
5.6 各概念之间的关系图解
┌─────────────────────────────────────────────────────────────┐
│ Docker 核心概念关系图 │
│ │
│ ┌─────────┐ docker build ┌─────────┐ │
│ │Dockerfile│──────────────────▶│ 镜像 │ │
│ │ │ │ (Image) │ │
│ └─────────┘ └────┬────┘ │
│ │ │
│ ┌─────────────────┼─────────────┐ │
│ │ docker run │ docker push │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 容器 │ │ 容器 │ │ 仓库 │ │
│ │(Container)│ │(Container)│ │(Registry)│ │
│ └────┬────┘ └────┬─────┘ └────┬────┘ │
│ │ │ │ │
│ │ ┌────────────┘ docker pull │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ │
│ │ 数据卷 │ │ 网络 │ │
│ │(Volume) │ │(Network) │ │
│ └─────────┘ └──────────┘ │
│ │
│ 关系说明: │
│ - Dockerfile → 构建 → 镜像 │
│ - 镜像 → 运行 → 容器 │
│ - 镜像 → 推送 → 仓库 → 拉取 → 镜像 │
│ - 容器 → 挂载 → 数据卷(持久化数据) │
│ - 容器 → 连接 → 网络(通信) │
│ - 容器 → 提交 → 镜像(固化为新镜像) │
└─────────────────────────────────────────────────────────────┘
用一个现实世界的类比来总结这些概念的关系:
- Dockerfile = 菜谱(描述如何制作食物)
- 镜像(Image) = 预制食品包(按菜谱制作好的半成品)
- 仓库(Registry) = 超市(存储和销售食品包)
- 容器(Container) = 正在烹饪的菜肴(食品包的运行实例)
- 数据卷(Volume) = 冰箱(持久存储食材,不随菜肴消失)
- 网络(Network) = 餐厅传菜窗口(让不同菜肴之间可以传递信息)
第六章 Docker应用场景
了解了Docker的核心概念之后,你可能会问:"Docker到底能在实际工作中解决什么问题?"本章将通过具体的应用场景和真实案例,展示Docker在不同领域的价值。无论你是开发者、运维工程师还是架构师,都能从中找到Docker适用你工作场景的理由。
6.1 开发环境统一
6.1.1 "在我的机器上能跑"问题的终结
开发环境不统一是困扰开发团队最久的问题之一。不同开发者使用不同的操作系统、不同的软件版本,导致同一个项目在不同开发者的机器上表现不同。
bash
# 传统方式:新成员入职需要花费1~2天配置环境
# 1. 安装Python 3.11
# 2. 安装Node.js 18
# 3. 安装PostgreSQL 15
# 4. 安装Redis 7
# 5. 配置环境变量
# 6. 安装项目依赖
# 7. 初始化数据库
# 8. ...各种坑和报错
# Docker方式:一个命令搞定
docker-compose up -d
# 所有服务自动启动,环境完全一致
一个实际的docker-compose.yml开发环境配置:
yaml
version: '3.8'
services:
# Python后端API
api:
build: ./backend
ports:
- "8000:8000"
volumes:
- ./backend:/app # 代码挂载,支持热重载
- api_deps:/usr/local/lib/python3.11/site-packages
environment:
- DEBUG=1
- DB_HOST=postgres
- DB_NAME=myapp
- DB_USER=postgres
- DB_PASSWORD=secret
- REDIS_URL=redis://redis:6379/0
depends_on:
- postgres
- redis
# Vue前端
frontend:
build: ./frontend
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- frontend_deps:/app/node_modules
environment:
- API_URL=http://localhost:8000
# PostgreSQL数据库
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=secret
volumes:
- postgres_data:/var/lib/postgresql/data
# Redis缓存
redis:
image: redis:7-alpine
ports:
- "6379:6379"
# Nginx反向代理
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
- frontend
volumes:
postgres_data:
api_deps:
frontend_deps:
新成员只需两条命令:
bash
# 克隆代码仓库
git clone https://github.com/mycompany/myapp.git
cd myapp
# 启动所有服务
docker-compose up -d
整个开发环境在1分钟内就绪,每个开发者的环境完全一致。
6.2 微服务架构部署
6.2.1 微服务与容器的天然契合
微服务架构将一个大型应用拆分为多个小型、独立的服务。每个服务有自己的技术栈、数据库和部署周期。Docker容器天然适合这种模式------每个微服务打包为一个独立的容器镜像,独立部署、独立扩缩容。
┌─────────────────────────────────────────────────────────┐
│ 微服务架构示例 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户服务 │ │ 订单服务 │ │ 商品服务 │ │
│ │ (Java) │ │ (Go) │ │ (Python) │ │
│ │ :8081 │ │ :8082 │ │ :8083 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ │
│ │ PostgreSQL│ │ MySQL │ │ MongoDB │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 支付服务 │ │ 通知服务 │ │ 搜索服务 │ │
│ │ (Node.js)│ │ (Python) │ │ (Java) │ │
│ │ :8084 │ │ :8085 │ │ :8086 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ API Gateway (Nginx/Kong) │ │
│ │ :80 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
每个微服务使用不同的编程语言,但通过Docker标准化了部署方式。所有服务都用Docker Compose或Kubernetes管理,统一了运维方式。
yaml
# 微服务docker-compose.yml(简化版)
version: '3.8'
services:
gateway:
image: mycompany/api-gateway:latest
ports:
- "80:80"
depends_on:
- user-service
- order-service
- product-service
user-service:
image: mycompany/user-service:latest
deploy:
replicas: 3
environment:
- DB_URL=postgres://user:pass@user-db:5432/users
depends_on:
- user-db
order-service:
image: mycompany/order-service:latest
deploy:
replicas: 2
environment:
- DB_URL=mysql://root:pass@order-db:3306/orders
depends_on:
- order-db
product-service:
image: mycompany/product-service:latest
deploy:
replicas: 2
environment:
- DB_URL=mongodb://product-db:27017/products
depends_on:
- product-db
user-db:
image: postgres:15
volumes:
- user_db_data:/var/lib/postgresql/data
order-db:
image: mysql:8.0
volumes:
- order_db_data:/var/lib/mysql
product-db:
image: mongo:6
volumes:
- product_db_data:/data/db
6.3 CI/CD流水线
Docker在CI/CD(持续集成/持续交付)中扮演着关键角色。它标准化了构建环境,使得CI流水线可以在任何地方运行。
代码提交 → CI流水线 → CD部署
详细流程:
1. 开发者提交代码到Git
2. CI系统(如Jenkins/GitLab CI)检测到提交
3. 在Docker容器中运行构建
4. 在Docker容器中运行测试
5. 构建Docker镜像
6. 推送镜像到仓库
7. CD系统拉取新镜像
8. 滚动更新生产环境的容器
一个完整的GitLab CI示例:
yaml
# .gitlab-ci.yml
stages:
- test
- build
- deploy
# 测试阶段:在Docker容器中运行单元测试
test:
stage: test
image: python:3.11-slim
services:
- postgres:15 # CI自动启动一个PostgreSQL容器
- redis:7 # CI自动启动一个Redis容器
variables:
DB_HOST: postgres
REDIS_URL: redis://redis:6379/0
script:
- pip install -r requirements.txt
- python manage.py migrate
- python manage.py test
coverage: '/TOTAL.*\s+(\d+\%)$/'
# 构建阶段:构建Docker镜像并推送
build:
stage: build
image: docker:24
services:
- docker:24-dind # Docker in Docker
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 同时打上latest标签
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
# 部署阶段:更新生产环境
deploy:
stage: deploy
image: alpine:3.18
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "
docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA &&
docker-compose up -d --no-deps api &&
docker image prune -f"
only:
- main
environment:
name: production
6.4 持续部署与回滚
Docker的不可变性使得部署和回滚变得极其简单------部署就是用新镜像替换旧容器,回滚就是用旧镜像替换新容器。
bash
# 部署新版本(v2.0)
docker pull myapp:2.0
docker stop myapp-v1
docker rm myapp-v1
docker run -d --name myapp-v2 -p 80:8080 myapp:2.0
# 如果发现问题,一键回滚到v1.0
docker stop myapp-v2
docker rm myapp-v2
docker run -d --name myapp-v1 -p 80:8080 myapp:1.0
# 整个回滚过程不到5秒!
# 使用Docker Swarm的滚动更新和回滚
docker service create --name myapp \
--replicas 5 \
--update-delay 10s \
--update-parallelism 2 \
--update-failure-action rollback \
--rollback-parallelism 2 \
--rollback-delay 5s \
myapp:2.0
# 触发更新
docker service update --image myapp:2.1 myapp
# 手动回滚
docker service rollback myapp
蓝绿部署(Blue-Green Deployment)的Docker实现:
bash
# 蓝环境(当前生产版本)
docker run -d --name app-blue -p 80:8080 myapp:1.0
# 部署绿环境(新版本),使用不同端口
docker run -d --name app-green -p 8081:8080 myapp:2.0
# 测试绿环境
curl http://localhost:8081/health
# 切换流量(修改Nginx配置,将上游指向绿环境)
docker exec nginx nginx -s reload
# 确认无误后,移除蓝环境
docker stop app-blue && docker rm app-blue
# 如需回滚,重新启动蓝环境并切换流量
docker run -d --name app-blue -p 8082:8080 myapp:1.0
# 修改Nginx配置指向蓝环境
docker exec nginx nginx -s reload
6.5 多环境一致性(开发/测试/生产)
Docker确保应用在所有环境中以相同方式运行。不同环境之间的差异通过环境变量注入,而不是修改应用代码或安装不同的软件版本。
bash
# 开发环境
docker run -d --name myapp-dev \
-p 8000:8000 \
-e DEBUG=1 \
-e DB_HOST=dev-db.internal \
-e DB_NAME=myapp_dev \
-e LOG_LEVEL=DEBUG \
-v $(pwd)/src:/app/src \ # 代码热重载
myapp:latest
# 测试环境
docker run -d --name myapp-test \
-p 8000:8000 \
-e DEBUG=0 \
-e DB_HOST=test-db.internal \
-e DB_NAME=myapp_test \
-e LOG_LEVEL=INFO \
myapp:latest
# 生产环境
docker run -d --name myapp-prod \
-p 8000:8000 \
-e DEBUG=0 \
-e DB_HOST=prod-db.internal \
-e DB_NAME=myapp_prod \
-e LOG_LEVEL=WARNING \
-e SENTRY_DSN=https://xxx@sentry.io/xxx \
--memory=1g \
--cpus=2 \
--restart always \
myapp:latest
三种环境使用完全相同的镜像myapp:latest,只是通过环境变量(-e)和运行参数(--memory、--cpus等)来区分。这保证了应用行为的一致性。
6.6 弹性伸缩
当流量增加时,可以快速启动更多容器实例来分担负载;流量回落后,再缩减容器数量以节省资源。
bash
# 使用Docker Swarm进行弹性伸缩
docker service create --name web --replicas 3 -p 80:80 nginx
# 流量高峰,扩容到20个实例
docker service scale web=20
# 流量回落,缩容到5个实例
docker service scale web=5
# 基于CPU使用率自动伸缩(Docker Swarm本身不直接支持,需要配合其他工具)
# 使用Prometheus + Alertmanager + 自定义脚本实现自动伸缩
在Kubernetes中,弹性伸缩更为强大:
yaml
# Kubernetes Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 3 # 最少3个副本
maxReplicas: 50 # 最多50个副本
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU使用率超过70%时扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # 内存使用率超过80%时扩容
6.7 真实企业案例分享
案例一:某电商平台的容器化改造
某中型电商平台原先采用传统的虚拟机部署方式,拥有约200台虚拟机。随着业务增长,面临以下问题:
- 部署一次新版本需要2~4小时
- 开发环境与生产环境差异导致频繁的线上问题
- 服务器资源利用率低(平均15%)
- 扩容响应慢,大促期间经常出现服务不可用
容器化改造后的效果:
| 指标 | 改造前 | 改造后 | 改善 |
|---|---|---|---|
| 部署时间 | 2~4小时 | 5~10分钟 | 提升24倍 |
| 资源利用率 | 15% | 65% | 提升4.3倍 |
| 服务器数量 | 200台VM | 50台物理机 | 减少75% |
| 扩容速度 | 30分钟 | 10秒 | 提升180倍 |
| 线上故障率 | 每月10+次 | 每月1~2次 | 减少85% |
| 新员工上手时间 | 1周 | 1天 | 提升7倍 |
案例二:某金融公司的DevOps实践
某金融公司在实施DevOps过程中,使用Docker作为标准化的交付物:
开发阶段:
- 开发者在本地使用Docker Compose运行完整开发环境
- 代码提交后,GitLab CI在Docker容器中自动构建和测试
- 通过测试后,自动构建Docker镜像并推送到Harbor
测试阶段:
- 测试环境自动拉取最新镜像进行部署
- 自动化测试在独立的Docker容器中运行
- 测试通过后,镜像自动晋升到预发布环境
生产阶段:
- 使用Kubernetes管理生产环境的容器
- 通过蓝绿部署实现零停机更新
- Prometheus + Grafana监控容器状态
- 出现问题时,一键回滚到上一个版本
关键收益:
- 交付周期从2周缩短到2天
- 部署成功率从85%提升到99.5%
- 环境配置问题导致的故障降为零
- 灾难恢复时间从4小时缩短到15分钟
第七章 第一个Docker容器
理论讲了很多,现在到了动手实践的时候了。本章将带你从零开始,一步步运行你的第一个Docker容器。我们会从最简单的Hello World开始,逐步深入到运行Web服务器、数据库、缓存等实际应用。每个命令都会有详细的参数解析,确保你不仅知道"怎么用",还知道"为什么这样用"。
7.1 运行Hello World容器(完整命令解析)
7.1.1 第一个Docker命令
安装好Docker后,第一条要运行的命令就是:
bash
docker run hello-world
这条命令的完整执行过程:
1. Docker Client接收命令
2. Docker Daemon检查本地是否有hello-world镜像
3. 如果没有,从Docker Hub拉取hello-world:latest镜像
4. 基于该镜像创建一个容器
5. 启动容器,执行镜像中的默认命令
6. 容器输出欢迎信息后退出
输出内容:
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
7.1.2 docker run命令详解
docker run是最常用的Docker命令,它的完整语法:
bash
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
# OPTIONS: 运行选项(如端口映射、环境变量等)
# IMAGE: 要使用的镜像
# COMMAND: 覆盖镜像默认的启动命令(可选)
# ARG: 传递给COMMAND的参数(可选)
常用选项详解:
bash
docker run \
-d \ # 后台运行(detached模式)
--name mycontainer \ # 指定容器名称
-p 8080:80 \ # 端口映射 宿主机:容器
-v /host/data:/container/data \ # 数据卷挂载
-e ENV_VAR=value \ # 环境变量
--network mynetwork \ # 指定网络
--memory=512m \ # 内存限制
--cpus=1.0 \ # CPU限制
--restart always \ # 重启策略
--rm \ # 容器退出后自动删除
-it \ # 交互模式(i=交互,t=终端)
nginx:latest # 镜像名:标签
各参数详解:
| 参数 | 说明 | 示例 |
|---|---|---|
-d |
后台运行 | docker run -d nginx |
--name |
容器命名 | docker run --name web nginx |
-p |
端口映射 | docker run -p 8080:80 nginx |
-v |
数据挂载 | docker run -v /data:/app/data nginx |
-e |
环境变量 | docker run -e DEBUG=1 nginx |
--network |
指定网络 | docker run --network mynet nginx |
--memory |
内存限制 | docker run --memory=512m nginx |
--cpus |
CPU限制 | docker run --cpus=1.5 nginx |
--restart |
重启策略 | docker run --restart always nginx |
--rm |
退出后删除 | docker run --rm nginx |
-i |
保持标准输入打开 | docker run -i ubuntu |
-t |
分配伪终端 | docker run -t ubuntu |
-it |
交互终端 | docker run -it ubuntu /bin/bash |
重启策略说明:
bash
# no: 不自动重启(默认)
docker run --restart no nginx
# always: 总是重启(包括容器正常退出)
docker run --restart always nginx
# unless-stopped: 总是重启,但如果手动停止则不重启
docker run --restart unless-stopped nginx
# on-failure: 仅在非零退出码时重启,可指定最大重试次数
docker run --restart on-failure:3 nginx
7.2 运行Nginx Web服务器(端口映射实战)
7.2.1 启动Nginx容器
bash
# 拉取Nginx官方镜像
docker pull nginx:latest
# 运行Nginx容器
docker run -d \
--name my-nginx \
-p 8080:80 \
nginx:latest
# 参数解释:
# -d: 后台运行
# --name my-nginx: 容器命名为my-nginx
# -p 8080:80: 将宿主机的8080端口映射到容器的80端口
# nginx:latest: 使用nginx最新版镜像
# 验证Nginx是否正常运行
curl http://localhost:8080
# 应该返回Nginx的欢迎页面HTML
# 在浏览器中访问 http://localhost:8080
7.2.2 自定义Nginx配置
bash
# 创建网站目录
mkdir -p /data/nginx/html
echo "<h1>Hello Docker Nginx!</h1>" > /data/nginx/html/index.html
# 创建自定义Nginx配置
cat > /data/nginx/nginx.conf << 'EOF'
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# 健康检查端点
location /health {
access_log off;
return 200 "healthy\n";
}
}
}
EOF
# 运行带有自定义配置和网页的Nginx
docker run -d \
--name my-nginx \
-p 8080:80 \
-v /data/nginx/html:/usr/share/nginx/html:ro \
-v /data/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:latest
# 测试自定义页面
curl http://localhost:8080
# 输出: <h1>Hello Docker Nginx!</h1>
# 测试健康检查端点
curl http://localhost:8080/health
# 输出: healthy
7.2.3 Nginx日志查看
bash
# 查看Nginx容器的日志
docker logs my-nginx
# 实时跟踪日志
docker logs -f my-nginx
# 查看最后20行日志
docker logs --tail 20 my-nginx
# 查看指定时间后的日志
docker logs --since "2024-01-01T10:00:00" my-nginx
# 查看最近10分钟的日志
docker logs --since 10m my-nginx
# 查看日志的时间戳
docker logs -t my-nginx
# Nginx日志通常输出到容器的stdout和stderr
# Docker会自动捕获这些输出
7.3 运行MySQL数据库(环境变量配置、数据持久化)
7.3.1 启动MySQL容器
bash
# 运行MySQL 8.0容器
docker run -d \
--name mysql-db \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=MySecurePass123! \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=AppUserPass456 \
-v mysql_data:/var/lib/mysql \
-v /data/mysql/conf:/etc/mysql/conf.d:ro \
--restart unless-stopped \
mysql:8.0
# 参数解释:
# -e MYSQL_ROOT_PASSWORD: root用户密码(必需)
# -e MYSQL_DATABASE: 自动创建的数据库
# -e MYSQL_USER: 自动创建的用户
# -e MYSQL_PASSWORD: 自动创建用户的密码
# -v mysql_data:/var/lib/mysql: 数据持久化(数据卷)
# -v /data/mysql/conf:/etc/mysql/conf.d:ro: 自定义配置文件
# --restart unless-stopped: 除非手动停止,否则自动重启
7.3.2 连接MySQL容器
bash
# 从宿主机连接MySQL
mysql -h 127.0.0.1 -P 3306 -u root -p
# 从另一个容器连接MySQL
docker run -it --rm \
--network bridge \
mysql:8.0 \
mysql -h 172.17.0.2 -u root -p
# 进入MySQL容器内部
docker exec -it mysql-db mysql -u root -p
# 执行SQL命令(非交互式)
docker exec mysql-db mysql -u root -pMySecurePass123! -e "SHOW DATABASES;"
7.3.3 MySQL数据备份与恢复
bash
# 备份数据库(导出SQL文件)
docker exec mysql-db \
mysqldump -u root -pMySecurePass123! --all-databases \
> /backup/all_databases.sql
# 备份指定数据库
docker exec mysql-db \
mysqldump -u root -pMySecurePass123! myapp \
> /backup/myapp.sql
# 恢复数据库
docker exec -i mysql-db \
mysql -u root -pMySecurePass123! myapp \
< /backup/myapp.sql
# 使用数据卷快照备份
docker run --rm \
-v mysql_data:/data:ro \
-v /backup:/backup \
alpine tar czf /backup/mysql_data_$(date +%Y%m%d).tar.gz -C /data .
7.3.4 MySQL配置优化
ini
# /data/mysql/conf/my.cnf - MySQL自定义配置
[mysqld]
# 字符集
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# 性能优化
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# 连接数
max_connections = 200
max_allowed_packet = 64M
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 2
# 二进制日志(用于主从复制)
log-bin = mysql-bin
binlog_format = ROW
server-id = 1
7.4 运行Redis缓存服务
bash
# 运行Redis容器
docker run -d \
--name redis-cache \
-p 6379:6379 \
-v redis_data:/data \
redis:7-alpine \
redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
# 参数解释:
# redis:7-alpine: 使用Alpine版Redis(更小)
# redis-server: 启动Redis服务
# --maxmemory 256mb: 最大内存256MB
# --maxmemory-policy allkeys-lru: 内存满时使用LRU淘汰策略
# 连接Redis
docker exec -it redis-cache redis-cli
# Redis常用操作
docker exec redis-cache redis-cli ping # 测试连接
docker exec redis-cache redis-cli info # 查看信息
docker exec redis-cache redis-cli dbsize # 查看键数量
docker exec redis-cache redis-cli monitor # 监控命令
# 带密码的Redis
docker run -d \
--name redis-secure \
-p 6379:6379 \
-v redis_data:/data \
redis:7-alpine \
redis-server --requirepass "MyRedisPass789" --maxmemory 256mb
# 连接带密码的Redis
docker exec -it redis-secure redis-cli -a MyRedisPass789
7.5 进入容器交互操作(docker exec、docker attach)
7.5.1 docker exec:在运行中的容器中执行命令
docker exec是最常用的进入容器的方式。它会在容器中启动一个新的进程,不会影响容器原有的进程。
bash
# 进入容器的交互式终端
docker exec -it my-nginx /bin/bash
# 如果容器没有bash,使用sh
docker exec -it my-nginx /bin/sh
# 在容器中执行单条命令
docker exec my-nginx ls /etc/nginx
docker exec my-nginx cat /etc/nginx/nginx.conf
docker exec my-nginx ps aux
docker exec my-nginx whoami
# 以root用户进入容器
docker exec -it -u root my-nginx /bin/bash
# 设置工作目录
docker exec -it -w /tmp my-nginx /bin/bash
# 设置环境变量
docker exec -it -e DEBUG=1 my-nginx /bin/bash
7.5.2 docker attach:连接到容器的主进程
docker attach直接连接到容器的主进程(PID 1)的stdin/stdout/stderr。它不创建新进程,而是"附着"到现有进程。
bash
# 以交互模式启动容器
docker run -d --name interactive-nginx -it nginx
# 附着到容器
docker attach interactive-nginx
# 退出attach但不停止容器:
# Ctrl+P, Ctrl+Q (先按Ctrl+P,松开后再按Ctrl+Q)
# 注意:如果直接按Ctrl+C或输入exit,会停止容器!
7.5.3 exec vs attach 对比
| 特性 | docker exec | docker attach |
|---|---|---|
| 创建新进程 | 是 | 否 |
| 影响主进程 | 否 | 是 |
| 退出后容器状态 | 不受影响 | 可能停止 |
| 多个会话 | 支持 | 不支持(共享同一个终端) |
| 推荐使用 | 是 | 特殊场景 |
7.6 查看容器日志与状态
7.6.1 查看容器日志
bash
# 查看全部日志
docker logs my-nginx
# 实时跟踪日志(类似tail -f)
docker logs -f my-nginx
# 查看最后50行
docker logs --tail 50 my-nginx
# 显示时间戳
docker logs -t my-nginx
# 查看指定时间之后的日志
docker logs --since "2024-01-01" my-nginx
docker logs --since 30m my-nginx # 最近30分钟
docker logs --since 1h my-nginx # 最近1小时
# 组合使用
docker logs -f --tail 20 -t my-nginx
# 实时跟踪最后20行,并显示时间戳
# 将日志导出到文件
docker logs my-nginx > /tmp/nginx.log 2>&1
# 只查看错误日志
docker logs my-nginx 2>&1 | grep -i error
7.6.2 查看容器状态
bash
# 查看运行中的容器
docker ps
# 查看所有容器(包括已停止的)
docker ps -a
# 只显示容器ID
docker ps -q
# 显示容器大小
docker ps -s
# 自定义输出格式
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"
# 查看容器详细信息
docker inspect my-nginx
# 查看特定信息
docker inspect -f '{{.State.Status}}' my-nginx # 容器状态
docker inspect -f '{{.NetworkSettings.IPAddress}}' my-nginx # IP地址
docker inspect -f '{{.Config.Image}}' my-nginx # 镜像名
docker inspect -f '{{json .State}}' my-nginx | jq . # 完整状态(JSON)
# 查看容器资源使用情况(实时)
docker stats
# 查看指定容器的资源使用
docker stats my-nginx mysql-db
# 只输出一次(非实时)
docker stats --no-stream
# 自定义输出格式
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
# 查看容器内的进程
docker top my-nginx
docker top my-nginx aux # 使用ps的aux格式
# 查看容器的文件系统变更
docker diff my-nginx
# A = Added(新增)
# C = Changed(修改)
# D = Deleted(删除)
7.7 容器的停止、启动、删除
7.7.1 停止容器
bash
# 优雅停止容器(发送SIGTERM,等待10秒后发送SIGKILL)
docker stop my-nginx
# 设置超时时间(秒)
docker stop -t 30 my-nginx # 等待30秒
# 强制停止容器(直接发送SIGKILL)
docker kill my-nginx
# 停止所有运行中的容器
docker stop $(docker ps -q)
7.7.2 启动容器
bash
# 启动已停止的容器
docker start my-nginx
# 重新启动容器(stop + start)
docker restart my-nginx
# 设置启动超时
docker restart -t 30 my-nginx
7.7.3 删除容器
bash
# 删除已停止的容器
docker rm my-nginx
# 强制删除运行中的容器(先kill再rm)
docker rm -f my-nginx
# 删除容器并删除关联的数据卷
docker rm -v my-nginx
# 删除所有已停止的容器
docker container prune
# 删除所有容器(包括运行中的)
docker rm -f $(docker ps -aq)
7.7.4 容器清理操作
bash
# 清理所有停止的容器
docker container prune
# 清理所有未使用的镜像
docker image prune
# 清理所有未使用的网络
docker network prune
# 清理所有未使用的数据卷
docker volume prune
# 一键清理所有未使用的资源(容器、镜像、网络、数据卷)
docker system prune
# 一键清理所有未使用的资源,包括正在使用的(更彻底)
docker system prune -a
# 查看Docker磁盘使用情况
docker system df
# 输出示例:
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 15 5 5.2GB 3.1GB (59%)
# Containers 10 3 500MB 300MB (60%)
# Local Volumes 8 4 2.1GB 800MB (38%)
# Build Cache 20 0 1.5GB 1.5GB
7.7.5 完整的容器生命周期管理示例
bash
#!/bin/bash
# 容器生命周期管理脚本示例
IMAGE_NAME="myapp"
CONTAINER_NAME="myapp-container"
VERSION="1.0"
PORT=8080
echo "=== 1. 构建镜像 ==="
docker build -t ${IMAGE_NAME}:${VERSION} .
echo "=== 2. 创建并启动容器 ==="
docker run -d \
--name ${CONTAINER_NAME} \
-p ${PORT}:8080 \
-e ENV=production \
--memory=512m \
--cpus=1.0 \
--restart unless-stopped \
${IMAGE_NAME}:${VERSION}
echo "=== 3. 查看容器状态 ==="
docker ps --filter "name=${CONTAINER_NAME}"
echo "=== 4. 查看启动日志 ==="
sleep 3
docker logs ${CONTAINER_NAME}
echo "=== 5. 健康检查 ==="
if curl -s http://localhost:${PORT}/health | grep -q "healthy"; then
echo "容器健康检查通过!"
else
echo "警告: 健康检查失败!"
docker logs --tail 50 ${CONTAINER_NAME}
fi
echo "=== 6. 停止容器 ==="
docker stop ${CONTAINER_NAME}
echo "=== 7. 重新启动 ==="
docker start ${CONTAINER_NAME}
echo "=== 8. 清理(可选) ==="
# docker stop ${CONTAINER_NAME}
# docker rm ${CONTAINER_NAME}
# docker rmi ${IMAGE_NAME}:${VERSION}
第八章 Docker生态系统
Docker不仅仅是一个容器引擎,它催生了一个庞大的技术生态系统。从镜像仓库到编排工具,从监控方案到安全扫描,Docker生态覆盖了容器全生命周期的各个环节。本章将带你了解Docker生态系统中的关键工具和技术,帮助你构建完整的容器技术认知。
8.1 Docker Hub与镜像市场
Docker Hub是Docker官方的公共镜像仓库,是Docker生态中最基础的服务之一。
bash
# Docker Hub的主要功能
# 1. 镜像托管:存储和分发Docker镜像
# 2. 自动构建:与GitHub/Bitbucket集成,代码提交后自动构建镜像
# 3. Webhooks:镜像更新时触发通知
# 4. 组织和团队:支持团队协作管理镜像
# 5. 漏洞扫描:自动扫描镜像中的安全漏洞
# Docker Hub的镜像分类
# Official Images: Docker官方维护(如nginx, redis, mysql)
# Verified Publishers: 认证厂商发布(如microsoft, google)
# Community: 社区用户上传
# Docker Hub的使用统计(2024年):
# - 注册用户: 超过1000万
# - 公共镜像: 超过1500万
# - 镜像拉取量: 超过200亿次/月
除了Docker Hub,还有其他公共镜像服务:
| 服务 | 提供方 | 特点 |
|---|---|---|
| Quay.io | Red Hat | 支持私有仓库,安全扫描 |
| GitHub Container Registry | GitHub | 与GitHub深度集成 |
| GitLab Registry | GitLab | 内置于GitLab CI/CD |
| 阿里云ACR | 阿里云 | 国内加速,免费额度 |
| 腾讯云TCR | 腾讯云 | 国内加速,企业级 |
8.2 Docker Compose
Docker Compose是定义和运行多容器Docker应用的工具。它使用YAML文件来描述应用的各个服务,然后一条命令就能启动所有服务。
yaml
# docker-compose.yml: 一个完整的Web应用示例
version: '3.8'
services:
# 前端Web服务
web:
build:
context: ./frontend
dockerfile: Dockerfile.prod
ports:
- "80:80"
- "443:443"
depends_on:
api:
condition: service_healthy
restart: always
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
# 后端API服务
api:
build: ./backend
ports:
- "8000:8000"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=myapp
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: always
# PostgreSQL数据库
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
restart: always
# Redis缓存
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
restart: always
# Celery Worker(异步任务)
worker:
build: ./backend
command: celery -A myapp worker -l info
environment:
- DB_HOST=postgres
- REDIS_URL=redis://redis:6379/0
depends_on:
- api
- redis
restart: always
volumes:
postgres_data:
redis_data:
bash
# Docker Compose常用命令
docker-compose up -d # 启动所有服务(后台)
docker-compose up -d --build # 重新构建并启动
docker-compose down # 停止并删除所有容器
docker-compose down -v # 同时删除数据卷
docker-compose ps # 查看服务状态
docker-compose logs -f api # 查看API服务日志
docker-compose exec api bash # 进入API容器
docker-compose restart api # 重启API服务
docker-compose stop # 停止所有服务
docker-compose start # 启动所有服务
docker-compose config # 查看解析后的配置
docker-compose top # 查看各服务进程
8.3 Docker Swarm
Docker Swarm是Docker原生的容器编排工具,内置于Docker Engine中。
bash
# === 初始化Swarm集群 ===
# 管理节点初始化
docker swarm init --advertise-addr 192.168.1.100
# 获取加入令牌(worker节点)
docker swarm join-token worker
# 输出: docker swarm join --token SWMTKN-1-xxx 192.168.1.100:2377
# 获取加入令牌(manager节点)
docker swarm join-token manager
# Worker节点加入集群
docker swarm join --token SWMTKN-1-xxx 192.168.1.100:2377
# 查看集群节点
docker node ls
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
# abc123... node1 Ready Active Leader
# def456... node2 Ready Active
# ghi789... node3 Ready Active Reachable
# === 服务管理 ===
# 创建服务
docker service create --name web --replicas 3 -p 80:80 nginx
# 查看服务
docker service ls
# 查看服务详情
docker service ps web
# 扩缩容
docker service scale web=5
# 更新镜像
docker service update --image nginx:1.25 web
# 滚动更新配置
docker service update \
--update-parallelism 2 \
--update-delay 10s \
--update-failure-action rollback \
--image nginx:1.25 web
# 删除服务
docker service rm web
# === 集群管理 ===
# 查看节点详情
docker node inspect node1
# 节点状态管理
docker node update --availability drain node2 # 排空节点
docker node update --availability active node2 # 恢复节点
docker node update --label-add region=east node1 # 添加标签
# 离开集群
docker swarm leave # Worker节点离开
docker swarm leave -f # Manager节点强制离开
# 解散集群
docker swarm leave -f # 在管理节点执行
8.4 Kubernetes
Kubernetes(简称K8s)是容器编排领域的事实标准。这里不展开讲K8s的详细使用,只介绍它与Docker的关系和基本概念。
bash
# Kubernetes与Docker的关系
# - Kubernetes不直接管理容器,而是通过容器运行时(CRI)来管理
# - Kubernetes 1.24之前默认使用Docker作为容器运行时(通过dockershim)
# - Kubernetes 1.24之后移除了dockershim,默认使用containerd
# - Docker构建的镜像仍然可以在Kubernetes上运行(因为都遵循OCI标准)
# Kubernetes核心概念
# - Pod: 最小调度单元,包含一个或多个容器
# - Deployment: 管理Pod的副本和更新
# - Service: 提供稳定的网络访问入口
# - ConfigMap/Secret: 配置和敏感信息管理
# - Ingress: HTTP路由和负载均衡
# - Volume: 持久化存储
# 一个简单的Kubernetes Deployment示例
kubectl create deployment nginx --image=nginx --replicas=3
# 暴露服务
kubectl expose deployment nginx --port=80 --type=LoadBalancer
# 查看资源
kubectl get pods
kubectl get deployments
kubectl get services
# 扩缩容
kubectl scale deployment nginx --replicas=5
# 滚动更新
kubectl set image deployment/nginx nginx=nginx:1.25
# 回滚
kubectl rollout undo deployment/nginx
8.5 容器监控工具(cAdvisor、Prometheus、Grafana)
8.5.1 cAdvisor
cAdvisor(Container Advisor)是Google开源的容器监控工具,自动收集容器的资源使用数据。
bash
# 运行cAdvisor容器
docker run -d \
--name cadvisor \
-p 8080:8080 \
-v /:/rootfs:ro \
-v /var/run:/var/run:ro \
-v /sys:/sys:ro \
-v /var/lib/docker/:/var/lib/docker:ro \
-v /dev/disk/:/dev/disk:ro \
--restart always \
gcr.io/cadvisor/cadvisor:latest
# 访问 http://localhost:8080 查看容器监控数据
# cAdvisor提供Web界面和REST API
# 可以查看每个容器的CPU、内存、网络、磁盘使用情况
8.5.2 Prometheus + Grafana监控栈
yaml
# docker-compose-monitoring.yml: 完整的监控栈
version: '3.8'
services:
# Prometheus: 指标收集和存储
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
# Grafana: 可视化仪表板
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
# cAdvisor: 容器指标采集
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
# Node Exporter: 主机指标采集
node-exporter:
image: prom/node-exporter:latest
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
volumes:
prometheus_data:
grafana_data:
yaml
# prometheus.yml: Prometheus配置
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'docker'
static_configs:
- targets: ['host.docker.internal:9323']
8.6 容器安全工具(Trivy、Clair、Falco)
8.6.1 Trivy:镜像漏洞扫描
bash
# 安装Trivy
# macOS: brew install trivy
# Linux: 参考官方文档
# 扫描镜像漏洞
trivy image nginx:latest
# 扫描指定严重级别的漏洞
trivy image --severity HIGH,CRITICAL nginx:latest
# 仅输出漏洞,不输出修复建议(更简洁)
trivy image --format json nginx:latest
# 扫描本地镜像
trivy image --input myapp.tar
# 扫描文件系统(如容器内的文件)
trivy fs /path/to/dir
# 扫描代码仓库
trivy repo https://github.com/myorg/myrepo
# 在CI/CD中使用(发现高危漏洞则失败)
trivy image --exit-code 1 --severity CRITICAL myapp:latest
8.6.2 Clair:镜像静态分析
Clair是CoreOS开发的容器镜像静态安全分析工具,主要用于检测镜像中的已知漏洞(CVE)。
bash
# Clair通常作为服务运行,通过API被调用
# Harbor集成了Clair/Trivy进行镜像扫描
# 使用Clair CLI扫描镜像
# 安装klar(Claire的CLI客户端)
go install github.com/optiopay/klar@latest
# 扫描镜像
KLAR_TRACE=true CLAIR_ADDR=http://clair:6060 klar nginx:latest
8.6.3 Falco:运行时安全监控
Falco是CNCF的运行时安全工具,可以检测容器中的异常行为。
bash
# 在Kubernetes中安装Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco
# Falco的默认规则可以检测:
# - 在容器中启动shell
# - 容器访问敏感文件(如/etc/shadow)
# - 容器修改系统文件
# - 容器发起异常网络连接
# - 容器提权操作
# 自定义Falco规则
# /etc/falco/rules.local.yaml
- rule: Unexpected Shell in Container
desc: Detect shell execution in a container that shouldn't have one
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, ksh) and
not container.image.repository in (allowed_shell_images)
output: >
Shell opened in container (user=%user.name
container_id=%container.id
container_name=%container.name
image=%container.image.repository
shell=%proc.name)
priority: WARNING
8.7 服务网格(Istio、Linkerd)
服务网格(Service Mesh)是处理服务间通信的基础设施层。它在微服务架构中提供流量管理、安全、可观测性等功能。
8.7.1 Istio
bash
# Istio的主要功能:
# 1. 流量管理:负载均衡、流量分割、金丝雀发布
# 2. 安全:mTLS加密、授权策略
# 3. 可观测性:指标、链路追踪、访问日志
# 安装Istio
curl -L https://istio.io/downloadIstio | sh -
cd istio-*
export PATH=$PWD/bin:$PATH
# 安装Istio到Kubernetes
istioctl install --set profile=demo -y
# 为命名空间启用Istio注入
kubectl label namespace default istio-injection=enabled
# 部署应用(自动注入Sidecar)
kubectl apply -f myapp.yaml
# 配置流量分割(金丝雀发布,90%流量到v1,10%到v2)
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp
http:
- route:
- destination:
host: myapp
subset: v1
weight: 90
- destination:
host: myapp
subset: v2
weight: 10
EOF
8.7.2 Linkerd
bash
# Linkerd是另一个轻量级服务网格
# 比Istio更简单,性能更好,但功能较少
# 安装Linkerd CLI
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install | sh
# 安装Linkerd到Kubernetes
linkerd install | kubectl apply -f -
# 验证安装
linkerd check
# 为应用启用Linkerd
kubectl annotate deployment myapp config.linkerd.io/skip-inbound-ports="4222"
服务网格对比:
| 特性 | Istio | Linkerd |
|---|---|---|
| 实现语言 | C++(Envoy) | Rust |
| 资源消耗 | 高 | 低 |
| 功能丰富度 | 非常丰富 | 基础 |
| 学习曲线 | 陡峭 | 平缓 |
| 性能开销 | 中等 | 极低 |
| 社区活跃度 | 高 | 中等 |
| 适用规模 | 大型 | 中小型 |
第九章 容器化思想与DevOps
Docker不仅是一项技术,更是一种思想的载体。它推动了软件开发和运维方式的变革,催生了DevOps文化、不可变基础设施、基础设施即代码等现代工程实践。本章将从思想层面探讨容器化技术对软件开发的影响,帮助你理解"为什么要用容器"背后的深层逻辑。
9.1 DevOps文化与容器的关系
9.1.1 什么是DevOps
DevOps是Development(开发)和Operations(运维)的组合词,它是一种强调开发与运维紧密协作的文化和实践方法。DevOps的核心理念是打破开发团队和运维团队之间的壁垒,通过自动化和持续反馈来加速软件交付。
传统模式下,开发和运维是两个独立的团队,有着不同的目标和考核标准:
- 开发团队的目标是快速交付新功能
- 运维团队的目标是保持系统稳定
这种目标冲突导致了"开发扔过墙"的问题------开发把代码扔给运维,运维负责部署和维护,出了问题互相推诿。
DevOps通过以下方式解决这个问题:
- 共同目标:开发和运维共同为软件的交付速度和稳定性负责
- 自动化:通过自动化构建、测试、部署来减少人为错误
- 持续反馈:运维的反馈快速传递给开发,形成闭环
- 共享工具:开发和运维使用相同的工具链
9.1.2 容器如何促进DevOps
Docker容器是DevOps实践的基石,它在以下几个方面促进了DevOps文化的落地:
1. 统一交付物
在传统模式下,开发交付的是代码包,运维需要自己搭建运行环境。这经常导致"在我的机器上能跑"的问题。有了Docker后,开发交付的是Docker镜像------一个包含了应用和所有依赖的标准化单元。运维只需要运行镜像,不需要关心环境配置。
传统模式:
开发 → 代码包 + 部署文档 → 运维(自行配置环境) → 部署
问题: 环境不一致,配置出错,互相推诿
DevOps + Docker模式:
开发 → Docker镜像 → CI/CD自动部署 → 运维(只需管理容器)
优势: 环境一致,自动化,责任清晰
2. 自动化流水线
Docker标准化了构建和部署过程,使得CI/CD流水线更容易实现:
代码提交 → 自动构建镜像 → 自动测试 → 自动推送镜像 → 自动部署
│ │ │ │ │
Git触发 docker build docker run docker push docker pull + run
3. 环境一致性
开发、测试、生产使用相同的Docker镜像,消除了环境差异带来的问题。开发者在本地可以完全复现生产环境,大大减少了"测试环境没问题,生产环境出bug"的情况。
4. 快速反馈
Docker容器的快速启动特性使得自动化测试可以在秒级完成环境准备。开发者提交代码后,几分钟内就能得到测试结果反馈,而不是等几个小时才知道是否破坏了什么。
9.2 不可变基础设施理念
9.2.1 什么是不可变基础设施
不可变基础设施(Immutable Infrastructure)是由Chad Fowler在2013年提出的一个理念。其核心思想是:服务器(或容器)一旦创建就不应该被修改。如果需要变更(如更新应用、修改配置),应该创建一个新的实例来替换旧的,而不是在现有实例上修改。
可变基础设施(传统模式):
服务器A → SSH登录 → 安装新版本 → 修改配置 → 重启服务
问题:
- 修改过程可能出错
- 修改后状态不确定(谁知道之前改过什么)
- 难以回滚
- 服务器之间配置漂移(Configuration Drift)
不可变基础设施(Docker模式):
构建新镜像v2.0 → 启动新容器v2.0 → 流量切换 → 停止旧容器v1.0
优势:
- 变更过程可追溯(镜像版本管理)
- 状态确定(镜像内容固定不变)
- 易回滚(切回旧镜像即可)
- 无配置漂移(每次都是全新容器)
9.2.2 Docker如何实现不可变基础设施
Docker的镜像机制天然支持不可变基础设施:
bash
# 不可变的部署流程
# 1. 构建新版本镜像(不修改旧镜像)
docker build -t myapp:2.0 .
# 2. 启动新版本容器(不影响旧版本)
docker run -d --name myapp-v2 -p 8081:8080 myapp:2.0
# 3. 验证新版本
curl http://localhost:8081/health
# 4. 切换流量到新版本
# (通过更新负载均衡配置)
# 5. 停止旧版本
docker stop myapp-v1
docker rm myapp-v1
# 如果需要回滚:
docker run -d --name myapp-v1 -p 8080:8080 myapp:1.0
# 重新切换流量即可
不可变基础设施的关键实践:
- 永不SSH进入容器修改配置:所有变更通过构建新镜像来实现
- 永不手动修改容器内文件:所有文件变更通过Dockerfile记录
- 使用环境变量注入配置:不同环境使用同一镜像,通过环境变量区分
- 使用配置管理工具:如ConfigMap(Kubernetes)或Consul管理配置
9.3 基础设施即代码(IaC)
9.3.1 什么是IaC
基础设施即代码(Infrastructure as Code, IaC)是指使用代码(通常是声明式配置文件)来定义和管理基础设施,而不是通过手动操作。Dockerfile、docker-compose.yml都是IaC的体现。
IaC的核心原则:
- 可版本控制:基础设施定义存储在版本控制系统中(Git)
- 可复现:相同的代码可以产生相同的基础设施
- 可审计:每次变更都有记录,可以追溯谁在什么时候改了什么
- 可测试:基础设施代码可以像应用代码一样进行测试
9.3.2 Docker中的IaC实践
dockerfile
# Dockerfile: 应用环境即代码
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 安装依赖(利用层缓存,依赖不变时不会重新安装)
COPY package*.json ./
RUN npm ci --only=production
# 复制应用代码
COPY . .
# 创建非root用户运行应用(安全最佳实践)
USER node
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# 启动命令
CMD ["node", "server.js"]
yaml
# docker-compose.yml: 基础设施即代码
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:${VERSION:-latest}
ports:
- "${APP_PORT:-3000}:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_URL=redis://redis:6379
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
restart_policy:
condition: on-failure
max_attempts: 3
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
bash
# .env文件: 环境变量(不提交到Git)
VERSION=2.0
APP_PORT=3000
DB_NAME=myapp_prod
DB_USER=myapp_user
DB_PASSWORD=SuperSecretPassword123
# 使用环境文件部署
docker-compose --env-file .env up -d
9.3.3 与其他IaC工具的配合
Docker的IaC可以与其他基础设施管理工具配合使用:
- Terraform:管理云资源(虚拟机、网络、存储),然后在虚拟机上安装Docker
- Ansible:配置管理,在Docker宿主机上安装和配置Docker
- Helm:Kubernetes的应用包管理,将多个K8s资源打包为可复用的Chart
hcl
# Terraform示例: 创建EC2实例并安装Docker
resource "aws_instance" "docker_host" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
user_data = <<-EOF
#!/bin/bash
# 安装Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable docker
systemctl start docker
# 拉取并运行应用
docker pull myapp:latest
docker run -d -p 80:8080 --restart always myapp:latest
EOF
tags = {
Name = "DockerHost"
}
}
9.4 GitOps工作流
9.4.1 什么是GitOps
GitOps是Weaveworks公司提出的一种现代DevOps工作流。它的核心理念是:将基础设施和应用配置的声明式描述存储在Git仓库中,Git仓库成为"唯一可信源"(Single Source of Truth)。
GitOps的核心原则:
- 声明式:系统状态以声明式方式描述(如YAML文件)
- 版本控制:所有配置存储在Git中,有完整的变更历史
- 自动拉取:系统自动从Git拉取变更并应用(而不是推送)
- 持续协调:系统持续比较实际状态和期望状态,发现偏差自动修正
9.4.2 GitOps与Docker
在GitOps工作流中,Docker镜像是应用的标准交付物:
开发者 → 修改代码 → 提交到Git → CI构建Docker镜像 → 推送到Registry
│
▼
Git仓库(配置仓库) ←── 更新镜像版本 ──┘
│
▼
ArgoCD/Flux检测到变更
│
▼
自动拉取新配置并部署到K8s
yaml
# Git仓库中的部署配置(deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: registry.mycompany.com/myapp:2.0.0 # CI自动更新此版本号
ports:
- containerPort: 8080
bash
# CI流水线自动更新Git仓库中的镜像版本
# .gitlab-ci.yml 中的部署阶段
deploy:
stage: deploy
script:
# 构建并推送镜像
- docker build -t $REGISTRY/myapp:$CI_COMMIT_TAG .
- docker push $REGISTRY/myapp:$CI_COMMIT_TAG
# 更新配置仓库中的镜像版本
- git clone https://gitlab.com/mycompany/k8s-configs.git
- cd k8s-configs
- sed -i "s|image:.*|image: $REGISTRY/myapp:$CI_COMMIT_TAG|" deployment.yaml
- git commit -am "Update myapp to $CI_COMMIT_TAG"
- git push
# ArgoCD会自动检测到Git仓库的变更并部署
9.5 容器化对团队协作的影响
容器化不仅仅是技术变革,更是组织和工作方式的变革:
1. 角色边界模糊化
- 开发者需要理解容器、Dockerfile、CI/CD
- 运维需要理解应用架构和容器编排
- 出现了DevOps工程师、SRE(Site Reliability Engineer)等新角色
2. 责任共担
- 开发负责编写Dockerfile和应用代码
- 运维负责Docker基础设施和编排平台
- 双方共同对应用的部署和运行负责
3. 标准化流程
- 所有应用都必须容器化,遵循统一的Dockerfile规范
- 所有部署都通过CI/CD流水线,禁止手动部署
- 所有配置都通过IaC管理,禁止手动修改
4. 加速交付
- 从代码提交到生产部署的时间从天级缩短到分钟级
- 可以更频繁地发布,每次发布的变更更小,风险更低
- 出问题可以快速回滚,影响时间更短
第十章 常见问题与最佳实践
作为本篇的最后一章,我们将总结Docker学习过程中的常见误区、新手避坑指南、学习路线图以及推荐资源。无论你是刚刚开始学习Docker,还是已经有了一些经验,本章的内容都能帮助你少走弯路,更高效地掌握Docker。此外,本章还将补充Docker的安装方法和Dockerfile指令速查表,为你的实际操作提供完整参考。
10.0 补充:Docker安装方法
在开始学习Docker之前,首先需要在你的机器上安装Docker。Docker支持多种操作系统平台,包括Linux、Windows和macOS。下面分别介绍各平台的安装方法。
10.0.1 Linux平台安装
Linux是Docker的原生平台,在Linux上运行Docker性能最好、功能最完整。以Ubuntu为例:
bash
# 方法1: 使用官方安装脚本(最简单)
curl -fsSL https://get.docker.com | sudo sh
# 方法2: 使用apt包管理器(推荐用于生产环境)
# 更新包索引
sudo apt-get update
# 安装必要的依赖
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
# 添加Docker官方GPG密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 添加Docker仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 验证安装
sudo docker run hello-world
# 将当前用户加入docker组(免sudo)
sudo usermod -aG docker $USER
# 注意:需要重新登录才能生效
# 配置镜像加速器(国内用户)
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com"
]
}
EOF
# 重启Docker服务
sudo systemctl daemon-reload
sudo systemctl restart docker
CentOS平台的安装方法类似,主要区别在于使用yum包管理器:
bash
# 安装必要依赖
sudo yum install -y yum-utils
# 添加Docker仓库
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 安装Docker
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 启动并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
10.0.2 Windows平台安装
在Windows上安装Docker需要使用Docker Desktop:
powershell
# 系统要求:
# - Windows 10 64位 (版本2004或更高) 或 Windows 11
# - 启用WSL 2 (Windows Subsystem for Linux)
# - 至少4GB内存
# 步骤1: 启用WSL 2
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# 步骤2: 下载并安装WSL 2内核更新包
# 从微软官网下载: https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi
# 步骤3: 设置WSL 2为默认版本
wsl --set-default-version 2
# 步骤4: 下载Docker Desktop安装包
# 从官网下载: https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe
# 步骤5: 运行安装程序(使用PowerShell)
Start-Process "Docker Desktop Installer.exe" -Wait
# 步骤6: 验证安装(安装完成后重启电脑,然后打开PowerShell)
docker version
docker run hello-world
Docker Desktop for Windows的配置:
- 在系统托盘右键Docker图标,选择Settings
- Resources: 配置CPU、内存、磁盘等资源限制
- Docker Engine: 编辑daemon.json配置
- Registry: 管理镜像仓库认证
10.0.3 macOS平台安装
在macOS上同样使用Docker Desktop:
bash
# 系统要求:
# - macOS 11 (Big Sur) 或更高版本
# - 至少4GB内存
# - Apple Silicon (M1/M2) 或 Intel处理器
# 方法1: 使用Homebrew安装(推荐)
brew install --cask docker
# 方法2: 手动下载安装
# 从官网下载: https://desktop.docker.com/mac/main/amd64/Docker.dmg
# 或Apple Silicon版本: https://desktop.docker.com/mac/main/arm64/Docker.dmg
# 安装后启动Docker Desktop应用
open /Applications/Docker.app
# 验证安装
docker version
docker run hello-world
需要注意的是,在macOS和Windows上,Docker运行在虚拟机(WSL 2或HyperKit)中,因此有一些限制:
- 网络模型与Linux原生不同
- 某些需要特定内核功能的特性可能不可用
- 性能略低于Linux原生(但有WSL 2加速后差距很小)
10.0.4 Docker版本说明
Docker从2017年3月开始采用基于时间的版本号,每年发布两个稳定版本,格式为YY.MM:
Docker 23.0 (2023年第一个版本)
Docker 24.0 (2023年第二个版本)
Docker 25.0 (2024年第一个版本)
版本选择建议:
- 生产环境:使用LTS(长期支持)版本,如Docker 24.0
- 开发环境:可以使用最新版本体验新功能
- 学习环境:任何较新的版本都可以
bash
# 查看Docker版本
docker version
# 查看详细信息
docker info
# 查看Docker Compose版本
docker compose version
# 查看Docker Buildx版本
docker buildx version
10.0.5 补充:Docker存储驱动详解
Docker的存储驱动(Storage Driver)是管理镜像层和容器可写层的核心组件。选择合适的存储驱动对Docker的性能和稳定性至关重要。
Docker历史上使用过多种存储驱动,目前推荐使用overlay2:
| 存储驱动 | 支持的文件系统 | 状态 | 说明 |
|---|---|---|---|
| overlay2 | ext4, xfs | 推荐 | 性能最好,生产环境首选 |
| fuse-overlayfs | 任意 | 备选 | 用于无root权限场景 |
| btrfs | btrfs | 可用 | 需要btrfs文件系统 |
| zfs | zfs | 可用 | 需要zfs文件系统 |
| vfs | 任意 | 兼容 | 无Copy-on-Write,性能差 |
| devicemapper | ext4, xfs | 已弃用 | 旧版本使用,有性能问题 |
| aufs | ext4, xfs | 已弃用 | 早期默认,内核不支持 |
bash
# 查看当前使用的存储驱动
docker info | grep "Storage Driver"
# 输出: Storage Driver: overlay2
# 查看存储驱动的详细信息
docker info | grep -A 5 "Storage Driver"
# Storage Driver: overlay2
# Backing Filesystem: extfs
# Supports d_type: true
# Native Overlay Diff: true
# 修改存储驱动(在daemon.json中配置)
{
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.size=10G" # 限制每个容器的可写层大小
]
}
存储驱动的选择建议:
- 生产环境必须使用overlay2,它提供了最佳的性能和稳定性
- 如果使用btrfs或zfs作为宿主机文件系统,可以考虑对应的驱动
- 永远不要使用vfs,它没有Copy-on-Write功能,会导致镜像和容器占用大量磁盘空间
- 如果从旧版本升级,建议迁移到overlay2
10.0.6 补充:Dockerfile指令速查表
为了方便读者查阅,这里提供Dockerfile所有指令的完整速查表:
| 指令 | 说明 | 示例 |
|---|---|---|
| FROM | 指定基础镜像 | FROM ubuntu:22.04 |
| LABEL | 添加元数据标签 | LABEL maintainer="admin@example.com" |
| RUN | 构建时执行命令 | RUN apt-get update && apt-get install -y nginx |
| CMD | 容器默认启动命令 | CMD ["nginx", "-g", "daemon off;"] |
| ENTRYPOINT | 容器入口命令(不易被覆盖) | ENTRYPOINT ["python", "app.py"] |
| EXPOSE | 声明容器监听端口 | EXPOSE 80 443 |
| ENV | 设置环境变量 | ENV NODE_ENV=production |
| ARG | 构建时变量 | ARG VERSION=latest |
| COPY | 复制文件到镜像 | COPY app.py /app/ |
| ADD | 复制文件(支持URL和解压) | ADD https://example.com/app.tar.gz /app/ |
| WORKDIR | 设置工作目录 | WORKDIR /app |
| USER | 指定运行用户 | USER node |
| VOLUME | 声明数据卷挂载点 | VOLUME /data |
| HEALTHCHECK | 健康检查 | `HEALTHCHECK CMD curl -f http://localhost/ |
| ONBUILD | 触发器(被其他镜像引用时执行) | ONBUILD COPY . /app |
| STOPSIGNAL | 停止容器的信号 | STOPSIGNAL SIGTERM |
| SHELL | 设置默认shell | SHELL ["/bin/bash", "-c"] |
CMD与ENTRYPOINT的区别是Dockerfile中最容易混淆的部分,这里详细说明:
dockerfile
# CMD: 可以被docker run后面的参数覆盖
# Dockerfile
FROM ubuntu
CMD ["echo", "hello"]
# 运行容器
docker run myimage # 输出: hello
docker run myimage echo world # 输出: world (CMD被覆盖)
# ENTRYPOINT: 不会被docker run后面的参数覆盖,而是作为参数追加
# Dockerfile
FROM ubuntu
ENTRYPOINT ["echo"]
# 运行容器
docker run myimage hello # 输出: hello (hello作为参数传给echo)
docker run myimage world # 输出: world
# 最佳实践: ENTRYPOINT + CMD 配合使用
# Dockerfile
FROM python:3.11
ENTRYPOINT ["python"]
CMD ["app.py"]
# 运行容器
docker run myimage # 执行: python app.py
docker run myimage test.py # 执行: python test.py (CMD被覆盖)
docker run myimage --version # 执行: python --version (CMD被覆盖)
10.0.7 补充:Docker日志管理最佳实践
容器日志管理是生产环境中不可忽视的重要环节。如果不对日志进行合理管理,日志文件可能会迅速膨胀,最终占满整个磁盘空间,导致服务不可用。Docker提供了多种日志驱动和配置选项,帮助开发者有效管理容器日志。
Docker默认使用json-file日志驱动,将容器标准输出和标准错误输出保存为JSON格式的文件。如果不配置日志轮转,这些文件会无限增长。生产环境必须配置日志大小限制和文件数量限制,确保日志不会失控。除了json-file驱动外,Docker还支持syslog、journald、fluentd、gelf、awslogs等多种日志驱动,可以将日志直接发送到外部日志收集系统。
在分布式环境中,通常使用ELK(Elasticsearch、Logstash、Kibana)或EFK(Elasticsearch、Fluentd/Fluent Bit、Kibana)技术栈来集中收集和分析容器日志。Fluentd作为日志收集器,以DaemonSet方式运行在每个节点上,自动收集所有容器的日志,然后发送到Elasticsearch进行存储和索引,最后通过Kibana进行可视化查询和分析。这种集中化的日志管理方案使得开发者可以在一个界面中搜索和查看所有容器的日志,极大提升了故障排查的效率。
此外,结构化日志(JSON格式)比纯文本日志更容易被解析和查询。建议在应用中使用结构化日志输出,这样日志收集系统可以自动提取关键字段(如时间戳、日志级别、请求ID等),支持按字段过滤和聚合查询,大幅提升日志分析的效率和价值。
10.1 Docker学习常见误区
误区一:"Docker就是轻量级虚拟机"
这是最常见的误解。Docker容器不是虚拟机,它们有本质区别:
- 虚拟机虚拟化了完整的硬件,运行完整的操作系统
- 容器只是被隔离的进程,共享宿主机内核
- 虚拟机可以运行不同操作系统的应用,容器只能运行与宿主机内核兼容的应用
理解这一点的关键在于:容器内的进程就是宿主机上的进程,只不过被namespace隔离了视图,被cgroups限制了资源。
误区二:"容器比虚拟机更安全"
容器和虚拟机的安全模型不同:
- 虚拟机提供硬件级隔离,安全性较高
- 容器共享内核,如果内核有漏洞,所有容器都可能受影响
- 容器内的root用户在未配置User Namespace时,实际上就是宿主机的root用户
在安全要求高的场景,应该考虑使用Kata Containers等安全容器方案。
误区三:"生产环境一定要用latest标签"
使用latest标签是一个常见的坏习惯:
latest标签是可变的,今天的latest和明天的可能不同- 无法精确知道运行的是哪个版本
- 回滚困难------你不知道上一个版本是什么
正确做法:使用明确的版本号(如nginx:1.25.3-alpine)。
误区四:"Dockerfile中每条指令都一样"
Dockerfile中指令的顺序和写法会显著影响镜像大小和构建效率:
dockerfile
# ❌ 不好:每次代码变更都会触发npm install
FROM node:18
COPY . .
RUN npm install
# 问题:任何文件变更都使COPY层缓存失效,导致npm install重新执行
# ✅ 好:先复制package.json,再安装依赖,最后复制代码
FROM node:18
COPY package*.json ./
RUN npm install
COPY . .
# 优势:代码变更不影响依赖安装层的缓存
误区五:"容器可以存储持久化数据"
容器是临时的,容器删除后数据丢失。持久化数据必须使用Volume或外部存储:
bash
# ❌ 错误:数据存储在容器可写层
docker run -d --name db mysql:8.0
# 容器删除后数据丢失
# ✅ 正确:使用Volume持久化
docker run -d --name db -v mysql_data:/var/lib/mysql mysql:8.0
# 容器删除后数据保留
10.2 新手避坑指南
坑一:端口冲突
bash
# 错误:端口已被占用
docker run -d -p 80:80 nginx
# Error: bind: address already in use
# 解决方法1: 换一个端口
docker run -d -p 8080:80 nginx
# 解决方法2: 找到并停止占用80端口的进程
sudo lsof -i :80
# 或
sudo netstat -tlnp | grep :80
# 解决方法3: 使用随机端口
docker run -d -P nginx
坑二:磁盘空间不足
Docker会占用大量磁盘空间(镜像、容器、数据卷、构建缓存):
bash
# 查看Docker磁盘使用情况
docker system df
# 清理未使用的资源
docker system prune -a
# 配置日志大小限制(防止日志撑满磁盘)
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
坑三:权限问题
bash
# 容器内进程以root运行,可能修改宿主机文件权限
docker run -v /host/data:/data nginx
# 容器内nginx以root运行,创建的文件属于root
# 解决方法1: 指定运行用户
docker run -u 1000:1000 -v /host/data:/data nginx
# 解决方法2: 在Dockerfile中创建非root用户
# Dockerfile
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 解决方法3: 修改挂载目录的权限
chown -R 1000:1000 /host/data
坑四:网络不通
bash
# 容器无法访问外网
docker run -it alpine ping 8.8.8.8
# 如果ping不通:
# 检查1: iptables是否被清理
sudo iptables -t nat -L -n | grep docker
# 如果没有DOCKER链,重启Docker:
sudo systemctl restart docker
# 检查2: DNS配置
docker run -it --dns 8.8.8.8 alpine ping google.com
# 检查3: 代理设置
# 如果在代理网络环境中,需要配置Docker代理
# /etc/systemd/system/docker.service.d/proxy.conf
[Service]
Environment="HTTP_PROXY=http://proxy:8080"
Environment="HTTPS_PROXY=http://proxy:8080"
Environment="NO_PROXY=localhost,127.0.0.1"
坑五:容器启动后立即退出
bash
# 容器启动后立即退出(exited状态)
docker run -d nginx
docker ps -a # 显示Exited
# 原因分析:
# 1. 容器的前台进程( PID 1 )退出了
# 2. Docker容器必须有一个前台进程在运行,否则会退出
# 解决方法:
# 方法1: 使用交互模式
docker run -it nginx
# 方法2: 确保CMD/ENTRYPOINT启动的是前台进程
# 对于nginx,确保使用 daemon off
CMD ["nginx", "-g", "daemon off;"]
# 方法3: 使用tail -f保持容器运行(不推荐,仅用于调试)
docker run -d nginx tail -f /dev/null
坑六:Docker Build缓存问题
bash
# 构建时缓存没有生效,每次都重新构建
docker build -t myapp .
# 可能原因:
# 1. 使用了--no-cache参数
# 2. COPY的文件发生了变化(即使是换行符)
# 3. Dockerfile中指令顺序不合理
# 查看构建缓存命中情况
docker build -t myapp . --progress=plain 2>&1 | grep -E "CACHED|Step"
# 使用BuildKit获得更好的缓存
DOCKER_BUILDKIT=1 docker build -t myapp .
# 使用缓存挂载(节省重复下载)
# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=cache,target=/var/cache/apk \
apk add nginx
10.3 Docker学习路线图
入门阶段(1~2周):
├── 理解容器化概念(阅读本文第一~三章)
├── 安装Docker
├── 学习基本命令(run, ps, images, logs, exec)
├── 运行第一个容器(hello-world, nginx)
└── 理解镜像和容器的关系
进阶阶段(2~4周):
├── 学习Dockerfile编写
│ ├── 常用指令(FROM, RUN, COPY, CMD, ENTRYPOINT)
│ ├── 多阶段构建
│ └── 镜像优化
├── 学习Docker网络
│ ├── bridge, host, none模式
│ ├── 自定义网络
│ └── 端口映射
├── 学习Docker存储
│ ├── Volume, Bind Mount, tmpfs
│ └── 数据持久化和共享
├── 学习Docker Compose
│ ├── 编写docker-compose.yml
│ └── 多容器应用管理
└── 理解Docker架构(Daemon, containerd, runc)
高级阶段(1~2月):
├── 深入理解底层原理
│ ├── namespace详解
│ ├── cgroups详解
│ ├── OverlayFS
│ └── 容器安全(Seccomp, AppArmor, Capabilities)
├── Docker Swarm集群管理
├── CI/CD集成
│ ├── GitLab CI / Jenkins
│ └── 自动化构建和部署
├── 容器监控
│ ├── cAdvisor + Prometheus + Grafana
│ └── 日志管理(ELK/EFK)
└── 私有仓库搭建(Harbor)
专家阶段(持续学习):
├── Kubernetes
│ ├── 核心概念(Pod, Deployment, Service)
│ ├── 存储和网络
│ ├── Helm包管理
│ └── Operator开发
├── 云原生技术栈
│ ├── 服务网格(Istio/Linkerd)
│ ├── GitOps(ArgoCD/Flux)
│ └── Serverless(Knative)
├── 容器安全
│ ├── 镜像扫描(Trivy)
│ ├── 运行时安全(Falco)
│ └── 策略管理(OPA/Gatekeeper)
└── 性能调优和故障排查
10.4 推荐学习资源与社区
官方资源
| 资源 | 地址 | 说明 |
|---|---|---|
| Docker官方文档 | https://docs.docker.com | 最权威的参考文档 |
| Docker博客 | https://www.docker.com/blog | 最新动态和最佳实践 |
| Docker GitHub | https://github.com/docker | 源码和Issue |
| Play with Docker | https://labs.play-with-docker.com | 在线Docker实验环境 |
| Kubernetes官方文档 | https://kubernetes.io/docs | K8s权威文档 |
| CNCF Landscape | https://landscape.cncf.io | 云原生项目全景图 |
推荐书籍
| 书名 | 作者 | 适合阶段 |
|---|---|---|
| Docker技术入门与实战 | 杨保华等 | 入门 |
| Docker Deep Dive | Nigel Poulton | 入门到进阶 |
| Kubernetes in Action | Marko Lukša | 进阶 |
| Cloud Native Patterns | Cornelia Davis | 高级 |
| Site Reliability Engineering | 高级 |
在线课程
- Docker官方培训:https://training.docker.com
- Udemy Docker课程
- Coursera Kubernetes课程
- Linux Foundation CNCF培训
社区
- Docker官方论坛:https://forums.docker.com
- Stack Overflow:搜索
docker标签 - Reddit: r/docker, r/kubernetes
- Kubernetes Slack:https://kubernetes.slack.com
- CNCF Slack:https://cloud-native.slack.com
10.5 本章总结
本章我们讨论了Docker学习中的常见误区和避坑指南,给出了一条从入门到专家的学习路线图,并推荐了丰富的学习资源。关键要点:
- 避免常见误区 :理解容器与虚拟机的本质区别,不滥用
latest标签,不在容器中存储持久化数据。 - 掌握避坑技巧:处理端口冲突、磁盘空间、权限、网络和缓存等常见问题。
- 遵循学习路线:从基础命令到Dockerfile,从网络存储到Compose,再到Kubernetes和云原生。
- 善用学习资源:官方文档是最权威的参考,社区和书籍是深入学习的途径。
- 持续实践:Docker是一门实践性很强的技术,多动手、多踩坑、多总结。
总结
从1979年的chroot到2024年的云原生生态,容器化技术走过了超过四十年的发展历程。Docker作为这一领域的里程碑式工具,不仅将复杂的Linux内核技术(namespace、cgroups、UnionFS)封装成了简洁易用的命令行工具,更推动了软件开发和交付方式的深刻变革。
在这篇超过3万字的长文中,我们从以下十个维度全面系统地学习了Docker:
-
历史脉络:从物理机到虚拟机再到容器,理解了容器化技术发展的必然性。chroot、namespace、cgroups、LXC等技术为Docker的诞生奠定了基础。
-
核心理念:Docker的"一次构建,到处运行"理念,通过将应用及其依赖打包为标准化镜像,从根本上解决了环境一致性问题。
-
技术对比:容器与虚拟机各有优劣,在生产环境中常常互补使用。容器在轻量性和快速部署上有压倒性优势,虚拟机在隔离性和安全性上更胜一筹。
-
架构原理:Docker采用C/S架构,Docker Daemon通过containerd和runc管理容器。底层依赖namespace实现隔离,cgroups实现资源限制,OverlayFS实现分层存储。
-
核心概念:镜像(只读模板)、容器(运行实例)、仓库(镜像分发)、数据卷(持久化)、网络(通信)构成了Docker的核心概念体系。
-
应用场景:从开发环境统一到微服务部署,从CI/CD流水线到弹性伸缩,Docker在现代软件工程中无处不在。
-
实践操作:通过运行Hello World、Nginx、MySQL、Redis等实际应用,掌握了Docker的核心命令和操作流程。
-
生态工具:Docker Compose、Swarm、Kubernetes、Prometheus、Grafana、Trivy、Istio等工具构成了完整的容器生态。
-
思想变革:容器化推动了DevOps文化、不可变基础设施、IaC和GitOps等现代工程实践的发展。
-
最佳实践:避免常见误区,遵循学习路线,善用社区资源,持续动手实践。
Docker的学习是一个循序渐进的过程。作为入门篇,本文为你构建了完整的知识框架。在后续的专栏文章中,我们将深入Dockerfile编写、Docker Compose实战、Docker网络与存储、Docker安全以及Kubernetes等更多主题。
记住:容器技术的核心不是Docker这个工具本身,而是它所代表的标准化、不可变和声明式的思想。理解了这些思想,无论工具如何演进,你都能快速适应。希望这篇文章能成为你容器化之旅的起点,祝你在Docker的世界里探索愉快!
本文是Docker专栏的第一篇文章。后续文章将涵盖Dockerfile最佳实践、Docker Compose多容器编排、Docker网络深入、Docker安全加固、Kubernetes入门等主题,敬请关注。