精通容器 — 生命周期、日志与解剖
精通容器 — 生命周期、日志与解剖
这一章讲两件事: 动手——容器的完整生命周期命令(run/stop/start/rm/inspect/exec/attach/logs); 动脑——容器到底是什么(namespace、cgroup、unionfs、runc/containerd)。 主线例子只有一个:一个叫 trivia 的容器,每隔 2 秒从网上拉一道冷知识题打印到屏幕, 我们从运行它一路走到拆开它。
1. 顶层全景:一条命令拆开看
先解剖全书用得最多的命令:
$ docker container run alpine echo "Hello World"
│ │ │ └─ 容器里要跑什么(主进程)
│ │ └─ 用哪个镜像(alpine:约 8MB 的最小 Linux)
│ └─ 操作哪个对象类型(container;还可能是 image、volume……)
└─ 客户端工具,通过 REST 接口和 Docker Engine 说话[^1]
第一次运行:输出多几行 —— 本地找不到镜像 → 从注册表下载 → 再跑
第二次运行:只输出 Hello World(镜像已缓存)[^2]
图说:run 一个命令 = 下载镜像(如需)+ 创建容器 + 启动主进程。这条命令第二次运行比第一次少几行输出,书里把它留作思考题,答案是: 多出来的几行是下载镜像的过程;本地缓存有了就不再生成了1。 原书还补了一张"run 背后发生什么"的清单:检查本地镜像、分配文件系统、设置网络接口、 分配 IP、配置端口映射、执行指定命令2——本章后面每一节对应清单上的一项。
2. 核心原理:容器的生命周期
2.1 主线例子第一步:后台跑起 trivia
需要一个一直在跑、持续有输出的容器做实验。原书选了 trivia:一个脚本每 2 秒调
https://the-trivia-api.com 的接口取一道题,用 jq(格式化 JSON 的命令行工具)
取出题目文本打印到 STDOUT3。作者把它打成镜像 fundamentalsofdocker/trivia:ed4
(ed4 = 第四版),后台运行:
$ docker container run --detach --name trivia fundamentalsofdocker/trivia:ed4
│ --detach:让主进程作为后台服务(daemon)跑
│ --name trivia:显式起名;不起名 Docker 会随机分配
│ (科学家名 + 形容词,比如 boring_borg)[^5]验证它在跑:docker container ls -l 输出里 STATUS 列是 Up 8 seconds——这列是
判断容器活没活的第一现场4。
2.2 列容器:ls 只显示在跑的
docker container ls 默认只列正在运行的容器;要看包括已退出在内的全部,
加 --all(或 -a)5。输出七列,书里给了完整释义,其中最要紧的三个:
| 列 | 是什么 |
|---|---|
| Container ID | 容器唯一 ID:SHA-256(Secure Hash Algorithm 256-bit,一种密码学哈希)的缩写形式,完整 ID 有 64 个字符6 |
| Status | 状态:created / running / exited / dead 等;Exited 不是故障,是主进程跑完了6 |
| Names | 容器名,系统内唯一6 |
两个组合拳命令:ls --quiet 只输出 ID 列;docker container rm --force $(docker container ls --all --quiet)
一条命令删光系统上所有容器(含停止的)——--force 让 rm 能删正在跑的容器7。
2.3 停止与启动:那 10 秒是怎么回事
$ docker container stop trivia ← 要整整 10 秒才返回,为什么?书里的解释是本章第一个"机制级"答案:Docker 先给容器主进程发 SIGTERM 信号
(礼貌地请求退出);进程若不理,Docker 等 10 秒,再发 SIGKILL(强制杀死)8。
停止后 Status 变为 Exited;docker container start 可以把它重新拉起来——
容器停了不是没了,数据和状态还在,直到 rm 才真正删除8。
判断(我们的,不是书里的): 这 10 秒在生产里是个常见的坑的来源—— 应用收到 SIGTERM 后做清理(关连接、flush 缓冲),如果清理超过 10 秒会被 SIGKILL 腰斩。 编排层(Kubernetes)有类似的宽限期且可以配置。写容器应用时,"退出要快"是硬要求。 如果错,会错在: 个别语言的运行时或框架会自己拦截 SIGTERM 并立即优雅退出, 使宽限期的细节不重要;但"默认 10 秒"这个行为本身是 Docker 的公开默认,不会错。
2.4 inspect:容器的体检报告
docker container inspect trivia 输出一大坨 JSON(一种通用的结构化文本格式,大括号键值对):容器 ID、创建时间、来源镜像、
挂载点、网络设置、环境变量、启动命令……书里叫它元数据(metadata),
是调试和审计的利器9。太多了看不动的解法:-f "{{json .State}}" 用 Go 模板语法
过滤出只要的节点,再管道给 jq 美化10。
2.5 exec 与 attach:进容器有两种"进法",差别致命
exec = 在一个正在跑的容器里新开一个进程,主进程不受影响:
$ docker container exec -i -t trivia /bin/sh
│ -i(--interactive):交互式 -t(--tty):分配一个终端
→ 提示符变成 /app #,你在容器里面了
→ /app # ps ← 能看到 PID 1 就是容器的主进程[^13]attach = 把当前终端接到容器主进程的输入输出错误流上,像坐在应用面前11。 attach 有个惊险操作:退出用 Ctrl+P 然后 Ctrl+Q——这是"脱离"(detach), 容器继续跑;而按 Ctrl+C 是直接停掉主进程。书里专门提醒: VS Code 的内置终端会拦截 Ctrl+P,要换独立终端练这个组合键11。
一句话区分:exec 是"另开一个窗口进房间",attach 是"坐到正在开车的人旁边"。 前者安全(随时可以退出),后者动的是主进程本身。debug 服务、查状态用 exec; 要看应用实时输出才 attach。
2.6 日志:应用该往哪儿写,Docker 怎么收
容器日志的规则,书里一句话钉死:应用应该把日志写到 STDOUT 和 STDERR, 不要写文件。理由是:Docker 会收集这两个流,存好,随时可取——写到文件里, Docker 就收不到了12。
$ docker container logs trivia ← 从容器诞生以来的全部日志
$ docker container logs --tail 5 trivia ← 只要最后 5 行
$ docker container logs --tail 5 --follow trivia ← 先给 5 行,然后持续追着新日志输出一个诚实的更正书里有:说"全部日志"不严格——默认的 json-file 日志驱动器
把日志存进文件,如果配置了滚动(rolling),老文件里的部分 docker logs 是看不到的13。
**日志驱动器(logging drivers)**是 Docker 把日志送往哪里的可插拔机制,默认 json-file,
原生支持的还有:none(不记)、journald、syslog、gelf(Graylog/Logstash)、
fluentd、awslogs(CloudWatch)、splunk 等14。注意一个陷阱:
换了非 json-file/journald 的驱动器,docker container logs 命令就失效了
(Docker 20.10 起有 dual logging 缓解)14。书里用 --log-driver none 演示:
跑一个打印 Hello 1/2/3 的 busybox 容器,再 docker logs 会报错
"configured logging driver does not support reading"——日志根本没被存15。
想全局改默认驱动器,改 Docker 守护进程的 /etc/docker/daemon.json,例如:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }含义:单文件到 10 MB 就滚动,最多留 3 个文件,最老的清除;改完给 dockerd 发 SIGHUP 热加载,不用重启16。这一节是第 12 章(集中式日志)的地基——单机日志能看了, 才知道为什么要往中心送。
3. 核心原理:容器的解剖
开头那句话再重复一遍,因为它是全章的题眼:容器不是轻量虚拟机, 是宿主机上被特殊封装和加固的进程17。
3.1 namespace:一份"假的私有视图"
namespace(命名空间)是对全局资源——文件系统、网络、进程树(PID)、用户组/用户 ID 等—— 的抽象。系统初始化时每种 namespace 只有一个实例,之后可以创建新实例或加入已有实例18。
它的效果用一句话说清:把一个进程包进某个 namespace,它就以为自己独占那份资源。 包进文件系统 namespace,进程就以为自己拥有完整文件系统(其实只是宿主机文件系统的一个 屏蔽子集,"文件系统中的文件系统")18。最实用的一例是 PID namespace: 容器里的进程可能显示为 PID 1(所以它叫主进程),从宿主机看,它只是一个普通 PID, 比如 33418。
时间线也给了:namespace 2002 年进 2.4.19 内核;3.8 加入 user namespace 后, 容器才真正能用上18。一个 namespace 里可以跑一到多个进程——这解释了 2.5 节 exec 为什么能在已运行的容器里再开进程。
3.2 cgroup:配额,不是隔离
**cgroup(控制组)**管资源限量:CPU 时间、内存、网络带宽19。 它 2006 年由 Google 工程师实现,2008 年 1 月并入内核 2.6.2419。 作用就是防"吵邻居":一个失控进程吃光 CPU 或内存,饿死宿主机上其他进程 (无论是不是容器里的)19。
namespace 与 cgroup 的分工,值得背下来:namespace 管"看见什么"(隔离视图), cgroup 管"能用多少"(资源配额)。 一个是遮眼布,一个是限速带。
3.3 unionfs:镜像的骨架
union filesystem(unionfs,联合文件系统)允许把多个不同的文件系统叠加成 一个统一的虚拟文件系统,被叠加的个体叫 branch(分支);路径相同的目录内容合并显示, 同名文件按优先级决定显示谁20。这就是下一章"镜像是分层 tarball"的技术基础—— 每一层就是一个 branch。
3.4 runc 与 containerd:OCI 世界的两块积木
原书在本章末尾把第 01 章提过的两个名字讲透了21:
| 组件 | 是什么 |
|---|---|
| runc | 轻量、可移植的低层运行时:完整支持 namespace 和 SELinux/AppArmor/seccomp/cgroups 等 Linux 安全特性;按 OCI(Open Container Initiative,Linux 基金会旗下的开放容器倡议)规范创建和运行容器 |
| containerd | 建在 runc 之上的高层运行时:镜像传输与存储、容器执行与监管、网络和存储挂载;管理容器完整生命周期;OCI 规范的参考实现 |
历史脉络:Docker 最早是单体;后来拆组件,runc 和 containerd 成为地基。 containerd 2017 年捐赠给 CNCF;替代实现还有 CoreOS 的 rkt、Red Hat 的 CRI-O、 LXD,但 containerd 是最流行的——它是 Kubernetes 1.8+ 和 Docker 平台的默认运行时21。
4. 作者的判断与证据
这一章以操作和机制为主,判断少而硬:
- "容器不是轻量 VM"是作者明确的立场,依据是机制(共享内核 vs 各带整套 OS) 和量化对比(容器启动以毫秒计,VM 要数秒到几分钟;VM 求长寿,容器天生朝生暮死)17。
- "应用日志写 STDOUT/STDERR 而非文件"是业界最佳实践,作者给出机制理由 (Docker 只收这两个流)12,第 12 章会把它扩展成完整的集中式日志方案。
- exec/attach 的区分、stop 的 10 秒宽限,都是 Docker 的可验证行为,不是观点。
5. 边界与局限
- attach 的 Ctrl+P/Q 组合键在 VS Code 终端里不可用,书里的提醒只说了换终端, 没说为什么——原因是按键被编辑器拦截(补充,不在书里,来自通用知识)。
- 日志驱动器清单会随版本变;原书按 Docker Engine 27.x 时代写的,新驱动器可能出现。
- unionfs 一节明显是给下一章铺垫,只讲了"叠加"概念,没有动手演示——别指望在这里就搞懂镜像层。
- 书中删除全部容器的命令(rm --force $(ls -aq))在生产系统上是核武器,练习可以,真机上先想清楚。
6. 可带走的
- run = 找镜像(没有就下载)→ 创建容器 → 跑主进程;第一次和第二次运行的输出差异就是下载行1。
- ls 默认只列运行中的;
-a看全部,-q只要 ID,-l看最后一个5。 - stop 不是立刻杀:SIGTERM → 等 10 秒 → SIGKILL;应用退出要快8。
- Exited 是正常状态,停了还能 start;rm 才是真删除8。
- exec 开新进程(安全),attach 接主进程(动真格);detach 是 Ctrl+P、Ctrl+Q2211。
- 容器里 PID 1 是主进程,从宿主机看它是普通 PID(比如 334)18。
- 日志写 STDOUT/STDERR;
logs --tail N --follow是日常;换日志驱动器会让docker logs失效1214。 - namespace = 看见什么,cgroup = 能用多少;unionfs 是把多层文件系统叠成一个视图的机制181920。
- runc 低层(按 OCI 规范起容器)、containerd 高层(管生命周期),K8s 1.8+ 默认用 containerd21。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| run 命令四段结构 | Starting, stopping, and removing containers | text/39-fm-starting-stopping-and-removing-containers.txt:32(搜「multiple parts」) · text/39-fm-starting-stopping-and-removing-containers.txt:47(搜「8 MB」) |
| 第一次/第二次运行差异、run 背后清单 | Starting, stopping, and removing containers | text/39-fm-starting-stopping-and-removing-containers.txt:16(搜「only this output」) · text/39-fm-starting-stopping-and-removing-containers.txt:105(搜「several actions」) |
| quay.io 拉取输出、latest 标签 | Starting, stopping, and removing containers | text/39-fm-starting-stopping-and-removing-containers.txt:77(搜「Unable to find image」) · text/39-fm-starting-stopping-and-removing-containers.txt:96(搜「latest」) |
| trivia 脚本与镜像 | Running a random trivia question container | text/40-fm-running-a-random-trivia-question-container.txt:8(搜「produces a new random trivia」) · text/40-fm-running-a-random-trivia-question-container.txt:51(搜「fundamentalsofdocker/trivia」) |
| --detach/--name、随机名 | Listing containers | text/41-fm-listing-containers.txt:1(搜「--detach」) · text/41-fm-listing-containers.txt:7(搜「boring_borg」) |
| ls 七列释义、--all | Listing containers | text/41-fm-listing-containers.txt:57(搜「seven columns」) · text/41-fm-listing-containers.txt:88(搜「--all」) |
| 删光全部容器 | Stopping and starting containers | text/42-fm-stopping-and-starting-containers.txt:7(搜「rm --force」) |
| stop 的 10 秒、SIGTERM/SIGKILL | Stopping and starting containers | text/42-fm-stopping-and-starting-containers.txt:38(搜「10 seconds」) · text/42-fm-stopping-and-starting-containers.txt:39(搜「SIGTERM」) |
| inspect 与元数据 | Removing containers | text/43-fm-removing-containers.txt:37(搜「JSON」) · text/43-fm-removing-containers.txt:63(搜「metadata」) |
| -f Go 模板过滤 | Executing commands in a running container | text/44-fm-executing-commands-in-a-running-container.txt:7(搜「format」) |
| exec -it、PID 1 | Executing commands in a running container | text/44-fm-executing-commands-in-a-running-container.txt:30(搜「exec -i -t」) · text/44-fm-executing-commands-in-a-running-container.txt:51(搜「PID 1」) |
| attach、Ctrl+P/Q | Attaching to a running container | text/45-fm-attaching-to-a-running-container.txt:35(搜「Attaching to a running」) · text/45-fm-attaching-to-a-running-container.txt:62(搜「Ctrl + P」) |
| 日志写 STDOUT/STDERR | Retrieving container logs | text/46-fm-retrieving-container-logs.txt:32(搜「STDOUT」) |
| json-file 默认驱动、滚动更正 | Retrieving container logs | text/46-fm-retrieving-container-logs.txt:51(搜「json-file」) · text/46-fm-retrieving-container-logs.txt:51(搜「rolling」) |
| 驱动器清单、docker logs 限制 | Retrieving container logs | text/46-fm-retrieving-container-logs.txt:74(搜「logging drivers」) · text/46-fm-retrieving-container-logs.txt:119(搜「json-file and journald」) |
| --log-driver none 演示 | Retrieving container logs | text/46-fm-retrieving-container-logs.txt:132(搜「log-driver」) · text/46-fm-retrieving-container-logs.txt:148(搜「does not support reading」) |
| daemon.json 滚动配置 | Retrieving container logs | text/46-fm-retrieving-container-logs.txt:284(搜「max-size」) · text/46-fm-retrieving-container-logs.txt:294(搜「SIGHUP」) |
| 容器=封装受限的进程、毫秒级启动 | The anatomy of containers | text/47-fm-the-anatomy-of-containers.txt:5(搜「specially encapsulated」) · text/47-fm-the-anatomy-of-containers.txt:12(搜「milliseconds」) |
| namespace 定义与历史 | The anatomy of containers | text/47-fm-the-anatomy-of-containers.txt:37(搜「abstraction of global resources」) · text/47-fm-the-anatomy-of-containers.txt:42(搜「2002」) |
| PID namespace、宿主 PID 334 | The anatomy of containers | text/47-fm-the-anatomy-of-containers.txt:59(搜「PID namespace」) · text/47-fm-the-anatomy-of-containers.txt:64(搜「334」) |
| cgroup 历史与吵邻居 | The anatomy of containers | text/47-fm-the-anatomy-of-containers.txt:73(搜「limit, manage, and isolate」) · text/47-fm-the-anatomy-of-containers.txt:77(搜「2006」) |
| unionfs 叠加 | The anatomy of containers | text/47-fm-the-anatomy-of-containers.txt:85(搜「backbone」) |
| runc/containerd、OCI、K8s 默认 | Summary | text/48-fm-summary.txt:10(搜「runc」) · text/48-fm-summary.txt:15(搜「OCI」) · text/48-fm-summary.txt:28(搜「donated」) · text/48-fm-summary.txt:30(搜「Kubernetes 1.8」) |
Footnotes
-
出处:「Starting, stopping, and removing containers」第 3–24 段(
text/39-fm-starting-stopping-and-removing-containers.txt:16,搜「only this output」;:20,搜「reason why」)。 ↩ ↩2 -
出处:「Starting, stopping, and removing containers」第 103–111 段(
text/39-fm-starting-stopping-and-removing-containers.txt:105,搜「several actions」;:108,搜「allocating a filesystem」)。 ↩ -
出处:「Running a random trivia question container」第 1–15 段(
text/40-fm-running-a-random-trivia-question-container.txt:8,搜「produces a new random trivia」;:13,搜「the-trivia-api.com」)。 ↩ -
出处:「Listing containers」第 14–25 段(
text/41-fm-listing-containers.txt:17,搜「ls -l」;:23,搜「STATUS column」)。 ↩ -
出处:「Listing containers」第 33–56 段(
text/41-fm-listing-containers.txt:48,搜「container ls」;:54,搜「Exited state」)。 ↩ ↩2 -
出处:「Listing containers」第 57–83 段(
text/41-fm-listing-containers.txt:57,搜「seven columns」;:61,搜「SHA-256」;:73,搜「Status」)。 ↩ ↩2 ↩3 -
出处:「Stopping and starting containers」第 1–13 段(
text/42-fm-stopping-and-starting-containers.txt:3,搜「--quiet」;:7,搜「rm --force」)。 ↩ -
出处:「Stopping and starting containers」第 22–43 段(
text/42-fm-stopping-and-starting-containers.txt:35,搜「container stop」;:38,搜「10 seconds」;:39,搜「SIGTERM」;:41,搜「SIGKILL」;:75,搜「Exited」;:78,搜「start」)。 ↩ ↩2 ↩3 ↩4 -
出处:「Removing containers」第 33–43 段(
text/43-fm-removing-containers.txt:37,搜「JSON」;:39,搜「debugging and auditing」)。 ↩ -
出处:「Executing commands in a running container」第 1–11 段(
text/44-fm-executing-commands-in-a-running-container.txt:7,搜「format」;:8,搜「Go template」)。 ↩ -
出处:「Attaching to a running container」第 35–71 段(
text/45-fm-attaching-to-a-running-container.txt:39,搜「standard input, output, and error」;:62,搜「Ctrl + P」;:70,搜「intercepted by VS Code」)。 ↩ ↩2 ↩3 -
出处:「Retrieving container logs」第 26–34 段(
text/46-fm-retrieving-container-logs.txt:32,搜「STDOUT」)。 ↩ ↩2 ↩3 -
出处:「Retrieving container logs」第 46–54 段(
text/46-fm-retrieving-container-logs.txt:51,搜「json-file」;:51,搜「rolling」)。原文:"By default, Docker uses the so-called json-file logging driver... If there is a file rolling policy defined, then docker container logs only retrieves what is in the currently active log file." ↩ -
出处:「Retrieving container logs」第 72–120 段(
text/46-fm-retrieving-container-logs.txt:74,搜「logging drivers」;:79,搜「default logging driver」;:119,搜「json-file and journald」)。 ↩ ↩2 ↩3 -
出处:「Retrieving container logs」第 121–154 段(
text/46-fm-retrieving-container-logs.txt:132,搜「log-driver」;:148,搜「does not support reading」)。 ↩ -
出处:「Retrieving container logs」第 279–299 段(
text/46-fm-retrieving-container-logs.txt:284,搜「max-size」;:291,搜「purged」;:294,搜「SIGHUP」)。 ↩ -
出处:「The anatomy of containers」第 1–16 段(
text/47-fm-the-anatomy-of-containers.txt:5,搜「specially encapsulated」;:12,搜「milliseconds」;:14,搜「ephemeral」)。 ↩ ↩2 -
出处:「The anatomy of containers」第 33–70 段(
text/47-fm-the-anatomy-of-containers.txt:37,搜「abstraction of global resources」;:42,搜「2002」;:49,搜「illusion」;:61,搜「PID 1 inside a container」;:64,搜「334」)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
出处:「The anatomy of containers」第 72–83 段(
text/47-fm-the-anatomy-of-containers.txt:73,搜「limit, manage, and isolate」;:77,搜「2006」;:78,搜「2.6.24」;:80,搜「noisy neighbor」)。 ↩ ↩2 ↩3 ↩4 -
出处:「The anatomy of containers」第 84–92 段(
text/47-fm-the-anatomy-of-containers.txt:85,搜「unionfs」;:89,搜「branches」;:92,搜「higher priority」)。 ↩ ↩2 -
出处:「Summary」第 1–32 段(该文件在本章承担"Container plumbing"小节:
text/48-fm-summary.txt:11,搜「lightweight, portable container runtime」;:14,搜「SELinux」;:15,搜「OCI」;:23,搜「higher-level」;:28,搜「2017」;:30,搜「Kubernetes 1.8」)。 ↩ ↩2 ↩3 -
出处:「Executing commands in a running container」第 16–52 段(
text/44-fm-executing-commands-in-a-running-container.txt:30,搜「exec -i -t」;:36,搜「tty」;:51,搜「PID 1」)。 ↩