编程语言研究书库

容器与 Kubernetes — 开发者视角的运行时世界

容器与 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:

  1. 选瘦基础镜像:python:3.10-slim 而不是完整 Ubuntu——只带运行必需的;
  2. 多阶段构建(multi-stage build):编译器和构建工具只在构建段出现, 最终镜像只拿编译产物。原书的 Go 例子:构建段用 golang:1.20 编出二进制, 运行段换成 alpine:3.17 只放二进制——镜像又小,又没有编译器可被利用;
  3. .dockerignore:像 .gitignore 一样,把 .git、日志、node_modules、 __pycache__ 挡在构建之外。

2.3 安全四招

  1. 基础镜像只从官方或可信仓库拉——来路不明的公共镜像可以藏着恶意代码;
  2. 别用 root 跑容器:容器默认以 root 运行,被攻破就等于拿到高权限; 修复(照原书):addgroup/adduser 建一个 appuser,USER appuser;
  3. 攻击面最小化:编译器、调试器、包管理器不该出现在运行镜像里(多阶段天然解决);
  4. 镜像扫描进 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:

HelmKustomize
思路模板 + 参数化叠加 + 直接改 YAML
学习曲线陡(要懂模板)平(就是 YAML)
发布管理记发行历史,可升级回滚无状态,靠 Git 管版本
适合复杂应用、可复用包简单的环境差异定制

5. 微服务最佳实践五件套

原书把「上了容器之后还该做什么」收成五件16:

  1. 无状态设计:状态全放外部(Redis/Memcached 这类分布式缓存), 任何副本能处理任何请求,扩缩容不掉会话;
  2. 安全四招(即 §2.3,此处再列一遍成表:最小镜像、非 root、常更新、扫描进 CI);
  3. 资源 requests/limits:每个容器都要声明「保证给我多少、最多用多少」 (原书例子:request 500m CPU/256Mi 内存,limit 1 CPU/512Mi)—— 调度器好排兵、集群不浪费、邻舱互不踩踏;
  4. 可观测性三支柱进容器(第 03 章的展开版):Prometheus 指标 (原书给了 Flask + prometheus_client 的最小例子)、ELK/OpenSearch 日志、Jaeger 追踪;
  5. 配置与密钥分离:ConfigMap 放非敏感配置、Secret 放敏感数据, 镜像里不硬编码任何东西——同一张镜像,换个环境配置就能跑17。

6. 排障:容器世界的六步套路

容器是短命的,随时可能被重建——「登录服务器看日志」那套老办法在这里失效。 原书给的排障顺序,值得背下来18:

步命令看什么
1 状态kubectl get podsCrashLoopBackOff = 容器起来就死、反复重启
2 事件kubectl describe pod X镜像拉不到(ErrImagePull)、资源不够、探针配错
3 日志kubectl logs X(-f 实时跟)应用层错误,如数据库连接被拒
4 进容器kubectl exec -it X -- /bin/sh看环境变量、依赖、网络连通性
5 网络kubectl get svc / get endpointsendpoints 为空 = Service 的选择器和 Pod 标签对不上
6 性能kubectl describe pod 看 Last StateOOMKilled = 超内存 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. 边界与局限

  1. K8s 概念只讲了最小集:没有 Namespace、RBAC、NetworkPolicy、Ingress、 StatefulSet——生产集群的另一半地图缺失,本章定位是「够开发者起步」。
  2. Helm/Kustomize 之争给得公允但没有裁决:真实团队常用 Helm 打包 + Kustomize 做环境叠加的混合体,原书没提这个常见组合。
  3. 调试方法对特权 Pod 与 distroless 镜像的摩擦没提:非 root + 无 shell 的 distroless 镜像(本章自己推荐的)恰恰让 exec 进不去——要靠 ephemeral container, 这条因果链原书留了半截。
  4. 资源配额的推荐值方法论缺失(requests 到底设多少?靠压测还是靠观察), 只给了格式没给依据。

8. 可带走的

  1. Dockerfile 六指令;每条指令是一层,层越多镜像越大;
  2. 优化三招:瘦基础镜像、多阶段构建、.dockerignore;安全四招:可信源、非 root、 最小攻击面、扫描进 CI、锁版本不用 latest;
  3. K8s 最小对象集:Pod(住)、Deployment(管数量)、Service(管入口)、 ConfigMap/Secret(管配置)、HPA(管扩缩);
  4. Service 的存在理由是 Pod 必死、IP 必变;选择器和标签对不上 = 流量进不来;
  5. Helm 记发行历史(可回滚),Kustomize 不记(靠 Git);模板派 vs 叠加派;
  6. requests/limits 必须写;OOMKilled 是超内存限,不是灵异事件;
  7. 排障六步:状态 → 事件 → 日志 → exec → 网络 → 资源,永远先花免费的调查预算;
  8. kubectl debug 的临时容器是 distroless 镜像时代的关键技能。

9. 原文地图

主题原书章原文位置
Dockerfile 六指令Introduction(第 8 章)text/26-fm-introduction.txt:64(搜「creates a layer」) · text/26-fm-introduction.txt:107(搜「FROM command」)
多阶段构建与 .dockerignoreIntroduction(第 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.3Introduction(第 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」)

Footnotes

  1. 出处:「Introduction」(第 8 章)第 64–122 段(text/26-fm-introduction.txt:64,搜「creates a layer」;text/26-fm-introduction.txt:70,搜「python:3.10-slim」)。 ↩

  2. 出处:「Introduction」(第 8 章)第 135–195 段(text/26-fm-introduction.txt:147,搜「golang:1.20 as builder」;text/26-fm-introduction.txt:177,搜「.dockerignore」)。 ↩

  3. 出处:「Introduction」(第 8 章)第 204–243 段(text/26-fm-introduction.txt:219,搜「addgroup」;text/26-fm-introduction.txt:243,搜「latest tag」)。 ↩

  4. 出处:「Introduction」(第 8 章)第 249–294 段(text/26-fm-introduction.txt:255,搜「as builder」;text/26-fm-introduction.txt:285,搜「USER appuser」)。 ↩

  5. 出处:「Introduction」(第 8 章)第 300–303 段(text/26-fm-introduction.txt:303,搜「CNCF」)。 ↩

  6. 出处:「Introduction」(第 8 章)第 309–339 段(text/26-fm-introduction.txt:323,搜「API Server」;text/26-fm-introduction.txt:332,搜「etcd」)。 ↩

  7. 出处:「Introduction」(第 8 章)第 345–447 段(text/26-fm-introduction.txt:447,搜「Table 8.1」)。 ↩

  8. 出处:「Introduction」(第 8 章)第 504–577 段(text/26-fm-introduction.txt:525,搜「replicas: 3」;text/26-fm-introduction.txt:571,搜「three Pods are always running」)。 ↩

  9. 出处:「Introduction」(第 8 章)第 584–639 段(text/26-fm-introduction.txt:584,搜「ephemeral」)。 ↩

  10. 出处:「Introduction」(第 8 章)第 643–712 段(text/26-fm-introduction.txt:685,搜「minReplicas」;text/26-fm-introduction.txt:709,搜「averageUtilization」)。 ↩

  11. 出处:「Introduction」(第 8 章)第 715–735 段(text/26-fm-introduction.txt:725,搜「Restarting containers」)。 ↩

  12. 出处:「Introduction」(第 8 章)第 738–766 段(text/26-fm-introduction.txt:744,搜「package manager for Kubernetes」)。 ↩

  13. 出处:「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」)。 ↩

  14. 出处:「Introduction」(第 8 章)第 1056–1146 段(text/26-fm-introduction.txt:1062,搜「overlays」;text/26-fm-introduction.txt:1143,搜「apply -k」)。 ↩

  15. 出处:「Introduction」(第 8 章)第 1149–1290 段(text/26-fm-introduction.txt:1290,搜「Table 8.3」)。 ↩

  16. 出处:「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」)。 ↩

  17. 出处:「Introduction」(第 8 章)第 1532–1576 段(text/26-fm-introduction.txt:1569,搜「Security」)。 ↩

  18. 出处:「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」)。 ↩

  19. 出处:「Introduction」(第 8 章)第 1666–1696 段(text/26-fm-introduction.txt:1669,搜「port-forward」;text/26-fm-introduction.txt:1693,搜「kubectl debug」)。 ↩

  20. 出处:「Introduction」(第 8 章)第 1711–1723 段(text/26-fm-introduction.txt:1717,搜「Missing API_KEY」)。 ↩

On this page