容器与 Kubernetes — 开发者视角的运行时世界¶
这一章讲什么: 你的代码打包成容器之后,进了 Kubernetes 集群,日子是怎么过的—— 谁来保证副本数量、挂了谁重启、流量怎么找到它、配置和密钥怎么给、出事了从哪查起。 本章全部从开发者的位置写:不是集群管理员手册,是「我的服务在集群里」的说明书。
1. 主线例子:一个支付服务在集群里的一天¶
本章的主线例子,是一个支付微服务从镜像到排障的完整旅程。
早上:你写好 Dockerfile(两段式:构建段装依赖,运行段只留运行时;非 root 用户)
→ docker build 出一个几百 MB → 几十 MB 的镜像
上午:kubectl apply 一个 Deployment,声明「请保持 3 个副本」
→ 某个 Pod 崩了,K8s 自动补一个新的(自愈)
中午:流量上来,HPA 发现 CPU 超过 80%,副本从 3 扩到 8
下午:出事了——支付 Pod 反复重启(CrashLoopBackOff)
→ kubectl get pods 看到状态
→ kubectl describe pod 看到事件
→ kubectl logs 看到:ValueError: Missing API_KEY environment variable
→ 发现 Deployment 没挂载 Secret,补上,恢复
晚上:helm upgrade 发新版;指标异常,helm rollback 一条命令退回上个版本
图说:集群替你干的是「保持期望状态」;你干的是「声明期望状态 + 出事时按套路查」。
路径与命令沿原书;副本数与扩缩量级是原书示例数字。
2. Dockerfile:又小又安全的镜像怎么写¶
2.1 六条指令与层¶
Dockerfile 是镜像的图纸,每条指令生成镜像的一层(layer)。六条基础指令(照原书):
FROM(基础镜像)、WORKDIR(工作目录)、COPY(拷文件进容器)、
RUN(构建期执行命令,如装依赖)、EXPOSE(声明端口)、CMD(启动命令)1。
2.2 优化三招¶
镜像越大:构建慢、部署慢、存储贵、攻击面大。三招2:
- 选瘦基础镜像:
python:3.10-slim而不是完整 Ubuntu——只带运行必需的; - 多阶段构建(multi-stage build):编译器和构建工具只在构建段出现,
最终镜像只拿编译产物。原书的 Go 例子:构建段用
golang:1.20编出二进制, 运行段换成alpine:3.17只放二进制——镜像又小,又没有编译器可被利用; - .dockerignore:像 .gitignore 一样,把
.git、日志、node_modules、__pycache__挡在构建之外。
2.3 安全四招¶
- 基础镜像只从官方或可信仓库拉——来路不明的公共镜像可以藏着恶意代码;
- 别用 root 跑容器:容器默认以 root 运行,被攻破就等于拿到高权限;
修复(照原书):
addgroup/adduser建一个appuser,USER appuser; - 攻击面最小化:编译器、调试器、包管理器不该出现在运行镜像里(多阶段天然解决);
- 镜像扫描进 CI:
trivy image myapp:latest(第 04 章的 Trivy 在这里接上); 别用latest标签,锁死具体版本(如python:3.10-slim)——上游镜像一变, 你的构建就不可复现3。
原书给了一个「全优」Dockerfile 样板(照抄结构):builder 段 pip install --user,
运行段只复制装好的依赖 + 非 root 用户4。
3. Kubernetes:对象地图与架构¶
Kubernetes(K8s):谷歌发起、现由 CNCF(云原生计算基金会)维护的容器编排系统, 提供自动扩缩、自愈、服务发现、滚动更新5。架构一句话:控制面管集群,工作节点跑应用。 控制面四件:API Server(对外的门)、Scheduler(把 Pod 分派到节点)、 Controller Manager(让现实追上期望)、etcd(存集群状态的键值数据库)6。
开发者的最小对象集(原书表 8.1,全留名)7:
| 对象 | 干什么 |
|---|---|
| Pod | 最小部署单元,一个或多个容器 |
| Node | 干活的工作机器(Pod 调度到这上面) |
| Deployment | 声明「要几个副本」,维持数量、滚动更新 |
| Service | 给会死的 Pod 一个稳定入口(负载均衡 + 固定名字) |
| ConfigMap | 放不敏感配置 |
| Secret | 放敏感数据(密码、token 即访问令牌) |
三个关键机制,按主线例子走一遍:
- Deployment 的副本数:声明
replicas: 3,K8s 保证永远 3 个在跑, 崩一个补一个,更新分批滚动8; - Service 解决 Pod 的临时性:Pod 会死、IP 会变,Service 用标签选中一组 Pod, 给它们一个稳定的名字和 IP——别的服务访问它,不用管背后是哪个 Pod9;
- HPA 自动扩缩:声明 min 2 / max 10、CPU 超 80% 就加副本—— 高峰自动扩、空闲自动缩10。
加上自愈三件套:重启失败的容器、节点挂了重排 Pod、健康检查不过的杀掉重来11。
4. 一摞 YAML 怎么管:Helm 还是 Kustomize¶
一个应用要 Deployment + Service + ConfigMap + Secret 好几个文件, 再乘上 dev/staging/prod 三套环境,手写 YAML 必然失控。两个工具,两种范式12:
Helm:K8s 的包管理器。chart = 一组模板文件 + values.yaml(默认值),
模板里用 {{ .Values.replicaCount }} 占位;安装(照原书)时:
读 Chart.yaml → 合并 values → 渲染模板 → 提交给 K8s API → 记录 release。
Helm 记住每次发行的历史,所以 helm upgrade 只应用差异、
helm rollback flask-release 1 一键回退13。
Kustomize:kubectl 内置,不用装。不走模板,走叠加(overlay):
一份 base 清单 + 各环境一个 patch(如生产把 replicas 改成 5),
kubectl apply -k ./prod14。
对照(原书表 8.3)15:
| Helm | Kustomize | |
|---|---|---|
| 思路 | 模板 + 参数化 | 叠加 + 直接改 YAML |
| 学习曲线 | 陡(要懂模板) | 平(就是 YAML) |
| 发布管理 | 记发行历史,可升级回滚 | 无状态,靠 Git 管版本 |
| 适合 | 复杂应用、可复用包 | 简单的环境差异定制 |
5. 微服务最佳实践五件套¶
原书把「上了容器之后还该做什么」收成五件16:
- 无状态设计:状态全放外部(Redis/Memcached 这类分布式缓存), 任何副本能处理任何请求,扩缩容不掉会话;
- 安全四招(即 §2.3,此处再列一遍成表:最小镜像、非 root、常更新、扫描进 CI);
- 资源 requests/limits:每个容器都要声明「保证给我多少、最多用多少」 (原书例子:request 500m CPU/256Mi 内存,limit 1 CPU/512Mi)—— 调度器好排兵、集群不浪费、邻舱互不踩踏;
- 可观测性三支柱进容器(第 03 章的展开版):Prometheus 指标 (原书给了 Flask + prometheus_client 的最小例子)、ELK/OpenSearch 日志、Jaeger 追踪;
- 配置与密钥分离:ConfigMap 放非敏感配置、Secret 放敏感数据, 镜像里不硬编码任何东西——同一张镜像,换个环境配置就能跑17。
6. 排障:容器世界的六步套路¶
容器是短命的,随时可能被重建——「登录服务器看日志」那套老办法在这里失效。 原书给的排障顺序,值得背下来18:
| 步 | 命令 | 看什么 |
|---|---|---|
| 1 状态 | kubectl get pods |
CrashLoopBackOff = 容器起来就死、反复重启 |
| 2 事件 | kubectl describe pod X |
镜像拉不到(ErrImagePull)、资源不够、探针配错 |
| 3 日志 | kubectl logs X(-f 实时跟) |
应用层错误,如数据库连接被拒 |
| 4 进容器 | kubectl exec -it X -- /bin/sh |
看环境变量、依赖、网络连通性 |
| 5 网络 | kubectl get svc / get endpoints |
endpoints 为空 = Service 的选择器和 Pod 标签对不上 |
| 6 性能 | kubectl describe pod 看 Last State |
OOMKilled = 超内存 limits,调整配额或优化内存 |
外加两件利器:kubectl port-forward pod/x 8080:5000 把本地端口指到 Pod,
绕开集群网络直接测;临时调试容器 kubectl debug pod/x -it --image=busybox——
往运行中的 Pod 里注射一个带工具(curl/netstat)的临时容器,
不用改原镜像就能调试19。
案例(原书):支付服务 CrashLoopBackOff → 日志报
Missing API_KEY environment variable → 查 Deployment YAML 发现没引用
存 API key 的 Secret → 补上挂载,恢复。排障是拼信息:
状态、日志、配置三块拼出完整因果20。
判断(我们的,不是书里的): 本章六步排障套路的本质是从外到内、 从便宜到贵:状态和事件是免费的,日志是便宜的,exec 进容器是贵的, 改配置重启是最贵的。真正值得带走的不是命令本身,是「永远先花免费的调查预算」 这个顺序纪律——反过来(先 exec 进去乱翻)是最常见的萌新路径。 如果错,会错在: 在配置了完善可观测性的集群里(第 03 章那套), 最快的起点常常是直接查 trace/日志平台而不是 kubectl—— 「从外到内」的起点会随可观测性成熟度上移,但「先便宜后贵」的排序不变。
7. 边界与局限¶
- K8s 概念只讲了最小集:没有 Namespace、RBAC、NetworkPolicy、Ingress、 StatefulSet——生产集群的另一半地图缺失,本章定位是「够开发者起步」。
- Helm/Kustomize 之争给得公允但没有裁决:真实团队常用 Helm 打包 + Kustomize 做环境叠加的混合体,原书没提这个常见组合。
- 调试方法对特权 Pod 与 distroless 镜像的摩擦没提:非 root + 无 shell 的
distroless 镜像(本章自己推荐的)恰恰让
exec进不去——要靠 ephemeral container, 这条因果链原书留了半截。 - 资源配额的推荐值方法论缺失(requests 到底设多少?靠压测还是靠观察), 只给了格式没给依据。
8. 可带走的¶
- Dockerfile 六指令;每条指令是一层,层越多镜像越大;
- 优化三招:瘦基础镜像、多阶段构建、.dockerignore;安全四招:可信源、非 root、 最小攻击面、扫描进 CI、锁版本不用 latest;
- K8s 最小对象集:Pod(住)、Deployment(管数量)、Service(管入口)、 ConfigMap/Secret(管配置)、HPA(管扩缩);
- Service 的存在理由是 Pod 必死、IP 必变;选择器和标签对不上 = 流量进不来;
- Helm 记发行历史(可回滚),Kustomize 不记(靠 Git);模板派 vs 叠加派;
- requests/limits 必须写;OOMKilled 是超内存限,不是灵异事件;
- 排障六步:状态 → 事件 → 日志 → exec → 网络 → 资源,永远先花免费的调查预算;
kubectl debug的临时容器是 distroless 镜像时代的关键技能。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| Dockerfile 六指令 | Introduction(第 8 章) | text/26-fm-introduction.txt:64(搜「creates a layer」) · text/26-fm-introduction.txt:107(搜「FROM command」) |
| 多阶段构建与 .dockerignore | Introduction(第 8 章) | text/26-fm-introduction.txt:147(搜「golang:1.20 as builder」) · text/26-fm-introduction.txt:177(搜「.gitignore」) |
| 非 root 用户与镜像扫描 | Introduction(第 8 章) | text/26-fm-introduction.txt:219(搜「addgroup」) · text/26-fm-introduction.txt:240(搜「trivy image」) |
| K8s 架构与对象 | Introduction(第 8 章) | text/26-fm-introduction.txt:323(搜「API Server」) · text/26-fm-introduction.txt:369(搜「Pod」) |
| Deployment/Service/HPA 例子 | Introduction(第 8 章) | text/26-fm-introduction.txt:525(搜「replicas: 3」) · text/26-fm-introduction.txt:685(搜「minReplicas」) |
| 自愈三件套 | Introduction(第 8 章) | text/26-fm-introduction.txt:725(搜「Restarting containers」) |
| Helm chart 结构与安装流程 | Introduction(第 8 章) | text/26-fm-introduction.txt:797(搜「Chart.yaml」) · text/26-fm-introduction.txt:1019(搜「reads the chart metadata」) · text/26-fm-introduction.txt:1050(搜「helm rollback」) |
| Kustomize 与对照表 8.3 | Introduction(第 8 章) | text/26-fm-introduction.txt:1110(搜「resources」) · text/26-fm-introduction.txt:1290(搜「Table 8.3」) |
| 无状态设计与资源配额 | Introduction(第 8 章) | text/26-fm-introduction.txt:1318(搜「Redis or Memcached」) · text/26-fm-introduction.txt:1452(搜「500m」) |
| 排障六步与临时容器 | Introduction(第 8 章) | text/26-fm-introduction.txt:1597(搜「CrashLoopBackOff」) · text/26-fm-introduction.txt:1636(搜「exec -it」) · text/26-fm-introduction.txt:1693(搜「kubectl debug」) |
| 支付服务排障案例 | Introduction(第 8 章) | text/26-fm-introduction.txt:1717(搜「Missing API_KEY」) |
-
出处:「Introduction」(第 8 章)第 64–122 段(
text/26-fm-introduction.txt:64,搜「creates a layer」;text/26-fm-introduction.txt:70,搜「python:3.10-slim」)。 ↩ -
出处:「Introduction」(第 8 章)第 135–195 段(
text/26-fm-introduction.txt:147,搜「golang:1.20 as builder」;text/26-fm-introduction.txt:177,搜「.dockerignore」)。 ↩ -
出处:「Introduction」(第 8 章)第 204–243 段(
text/26-fm-introduction.txt:219,搜「addgroup」;text/26-fm-introduction.txt:243,搜「latest tag」)。 ↩ -
出处:「Introduction」(第 8 章)第 249–294 段(
text/26-fm-introduction.txt:255,搜「as builder」;text/26-fm-introduction.txt:285,搜「USER appuser」)。 ↩ -
出处:「Introduction」(第 8 章)第 300–303 段(
text/26-fm-introduction.txt:303,搜「CNCF」)。 ↩ -
出处:「Introduction」(第 8 章)第 309–339 段(
text/26-fm-introduction.txt:323,搜「API Server」;text/26-fm-introduction.txt:332,搜「etcd」)。 ↩ -
出处:「Introduction」(第 8 章)第 345–447 段(
text/26-fm-introduction.txt:447,搜「Table 8.1」)。 ↩ -
出处:「Introduction」(第 8 章)第 504–577 段(
text/26-fm-introduction.txt:525,搜「replicas: 3」;text/26-fm-introduction.txt:571,搜「three Pods are always running」)。 ↩ -
出处:「Introduction」(第 8 章)第 584–639 段(
text/26-fm-introduction.txt:584,搜「ephemeral」)。 ↩ -
出处:「Introduction」(第 8 章)第 643–712 段(
text/26-fm-introduction.txt:685,搜「minReplicas」;text/26-fm-introduction.txt:709,搜「averageUtilization」)。 ↩ -
出处:「Introduction」(第 8 章)第 715–735 段(
text/26-fm-introduction.txt:725,搜「Restarting containers」)。 ↩ -
出处:「Introduction」(第 8 章)第 738–766 段(
text/26-fm-introduction.txt:744,搜「package manager for Kubernetes」)。 ↩ -
出处:「Introduction」(第 8 章)第 1003–1053 段(
text/26-fm-introduction.txt:1019,搜「reads the chart metadata」;text/26-fm-introduction.txt:1041,搜「helm upgrade」;text/26-fm-introduction.txt:1050,搜「helm rollback」)。 ↩ -
出处:「Introduction」(第 8 章)第 1056–1146 段(
text/26-fm-introduction.txt:1062,搜「overlays」;text/26-fm-introduction.txt:1143,搜「apply -k」)。 ↩ -
出处:「Introduction」(第 8 章)第 1149–1290 段(
text/26-fm-introduction.txt:1290,搜「Table 8.3」)。 ↩ -
出处:「Introduction」(第 8 章)第 1303–1576 段(
text/26-fm-introduction.txt:1315,搜「stateless」;text/26-fm-introduction.txt:1452,搜「500m」;text/26-fm-introduction.txt:1532,搜「ConfigMaps for non-sensitive」)。 ↩ -
出处:「Introduction」(第 8 章)第 1532–1576 段(
text/26-fm-introduction.txt:1569,搜「Security」)。 ↩ -
出处:「Introduction」(第 8 章)第 1588–1708 段(
text/26-fm-introduction.txt:1597,搜「CrashLoopBackOff」;text/26-fm-introduction.txt:1603,搜「describe pod」;text/26-fm-introduction.txt:1660,搜「get endpoints」;text/26-fm-introduction.txt:1681,搜「OOMKilled」)。 ↩ -
出处:「Introduction」(第 8 章)第 1666–1696 段(
text/26-fm-introduction.txt:1669,搜「port-forward」;text/26-fm-introduction.txt:1693,搜「kubectl debug」)。 ↩ -
出处:「Introduction」(第 8 章)第 1711–1723 段(
text/26-fm-introduction.txt:1717,搜「Missing API_KEY」)。 ↩