容器和K8s入门

2025-11-02T14:11:02+08:00 | 36分钟阅读 | 更新于 2025-11-02T14:11:02+08:00

@

学习目标

学完本章你应该能够:

  1. 讲清容器不是虚拟机,而是一个运行在宿主机上的进程,靠 Linux Namespace(隔离能看什么) 与 Cgroups(限制用多少资源) 两大机制实现隔离,并说出从 1979 年 chroot 到 2013 年 Docker 的演进路线。
  2. 写出 FROM / RUN / COPY / CMD(ENTRYPOINT) 的核心 Dockerfile,理解镜像是一层一层堆叠的、联合文件系统的写时复制(Copy-on-Write)原理,以及 layer 缓存为什么能加速构建。
  3. 用多阶段构建 + 选择合适基础镜像把镜像体积从几百兆压到几十兆甚至十几兆,并理解为什么生产环境要慎用 Alpine、以及 CGO_ENABLED=0 与 scratch 的关系。
  4. 讲清镜像依赖与运行时底层链路(docker run → dockerd → containerd → runc、K8s 的 CRI 抽象层),理解 multi-arch 多平台镜像的机制,并会用 docker build --platform 构建多架构镜像。
  5. 借助 kubectl create --dry-run=client -o yaml 在本地生成 Manifest 模板,并避开 YAML 的保留字、数字精度、多行字符串等经典坑。
  6. 读懂一个典型微服务示例应用(vote / worker / result + redis + db)的架构,掌握用 initContainer 控制服务启动顺序以解决中间件依赖问题的做法。
  7. 掌握 Helm 的 chart / release / repository 三大核心概念,会使用 install / upgrade --install / uninstall / template / dependency 命令,并会用模板变量与条件控制把示例应用改造成 Helm chart。
  8. 掌握 Kustomize 的 base / overlay 模式,会用 resources、patches、image、helmCharts 复写任意字段,实现多环境差异化部署,并能在 Helm 与 Kustomize 之间按场景选型。

前置知识:会使用基本命令行(bash);知道 Docker、Kubernetes 的基本概念;对镜像、容器有模糊印象即可;建议先回看上一章「云原生基础」的 IaC 部分,因为本章实验需要开通一台虚拟机。

本章你会动手做的事:

  • 在一台 Linux 虚拟机上用 nsenter、mount、runc 亲手体验 Namespace 隔离与 OverlayFS 联合文件系统。
  • 从一个 Go 应用出发,循序渐进地把镜像从 140MB 压到 11MB(多阶段 + scratch),并体验 Alpine 因 glibc/musl 不匹配而报错。
  • 用 kubectl create --dry-run=client -o yaml 生成 Manifest,部署一套示例投票应用,并用 initContainer 实现 redis → db → worker → vote → result 的链式启动。
  • 把示例应用分别改造成 Helm chart 与 Kustomize 工程,体验一条命令 install / upgrade 与 kubectl apply -k 的部署方式。
容器和K8s入门容器底层原理Dockerfile 构建镜像优化·最佳实践Manifest·微服务Helm·KustomizeNamespaces·CgroupsOverlayFS·runcFROM·RUN·COPY·CMDLayer 缓存·写时复制多阶段构建·Alpine 陷阱multi-arch·安全属性投票示例·initContainer链式依赖·中间件Helm 打包·模板变量Kustomize base/overlay

深入 Dockerfile 和镜像构建

这一讲是整个云原生方向"贯穿始终的核心基础"。我们要回答一个问题:容器到底是什么?它的隔离是怎么实现的?我们又该怎么把一个应用打包成镜像?

容器并不新鲜:一段 30 多年的历史

很多人以为容器是 Docker 发明的,其实容器底层的核心技术早就在 Linux 内核里存在了,Docker 只是把它工程化、产品化、好用化.我们快速过一遍里程碑:

时间事件意义
1979chroot作用在进程上,改变进程的根目录;chroot 后的程序不能对根目录以外的文件读写 —— 已经有点像容器文件系统隔离
2002Linux namespace 机制限制进程所能"看到"和使用的系统资源,是容器的核心技术之一
2006Google 的 process container为了管理庞大集群,隔离进程的 CPU / 内存 / IO / 网络
2007被社区合并进内核并重命名为 Cgroup(control groups)至此 Namespace 与 Cgroup 两大核心都已就绪
2013Docker 发布真正把容器变成工程实践,让容器变得流行
复习提示:类比:Namespace 管的是"你能看到什么"——文件、进程、网络、hostname 都各有一层虚拟视野;Cgroup 管的是"你能用多少"——CPU、内存、网络 IO 都给定量。一个像给进程戴了不同的"眼镜",一个像给它发"资源配额"。

容器本质就是宿主机上的一个进程。不管 Namespace 还是 Cgroup,作用对象都是进程;通过它们给这个进程提供各种隔离机制,让每个容器看起来像一台独立的虚拟机。Namespace 有 6 种类型:

  • mount(文件系统,能看到什么)
  • pid(进程,能看到哪些进程)
  • net(网络,网络栈)
  • uts(hostname 主机名)
  • user(用户)
  • ipc(进程间通信)

这张图在讲什么:容器隔离全靠这两套 Linux 内核机制,一个管"视野"、一个管"配额",作用对象都是同一个进程。

flowchart LR
    NS[Namespace
管你能看到什么
文件 / 进程 / 网络 / 主机名 / 用户 / 通信] CG[Cgroups
管你能用多少资源
CPU / 内存 / 网络 IO] NS --> P[容器 = 宿主机上的一个进程] CG --> P

后面我们会真的在 Linux 里用 nsenter 逐个体验一遍,而不只是停留在概念上。

镜像和容器:互为"定义"与"实例"

对刚接触的同学来说,镜像和容器最容易混淆。记住一句话:

  • 镜像:关于一个容器的定义,包括文件系统、环境变量、默认启动命令等。
  • 容器:镜像的实例化。用同一个镜像可以启动多个容器,它们互相隔离。
复习提示:类比:镜像像编程里的 class(或 interface),负责"定义";容器像 new 出来的实例,负责"实现"。一个定义,多个实例。

容器还有两个重要特性值得记住:

  1. 不可变(immutable):解决了"在我电脑上运行正常,生产环境不行"的经典打包交付问题 —— 因为这正是不可变技术解决打包与交付问题的重要基础。
  2. 无状态:容器理念是一次性、临时的,因此不提供持久化存储能力,持久化数据要存储在容器外部(比如后面会遇到的 K8s PVC)。把容器当虚拟机长期跑是典型的错误用法。

Dockerfile:如何构建一个镜像

构建镜像需要编写 Dockerfile —— 一组指令组成的描述文件。执行这些指令,就可以在一个基础镜像之上创建一个新的镜像,然后用 docker build 构建。基本逻辑是:FROM 一个 Ubuntu 之类的基础镜像 → 安装工具 → COPY 代码进去 → 安装应用依赖 → 设置启动命令。

# 最简 Dockerfile
FROM ubuntu:22.04          # 引用基础镜像
RUN apt-get update && apt-get install -y nginx   # 安装应用
COPY . /app                # 把本地代码复制进镜像
CMD ["nginx", "-g", "daemon off;"]   # 声明镜像的启动命令

docker build 的关键参数:

# -t 指定镜像名:版本;末尾的 . 表示"构建上下文"(当前目录)
docker build -t my-app:1.0 .
# 通过 -f 指定 Dockerfile 文件名(默认在根目录就叫 Dockerfile 时可省略)
复习提示:.dockerignore:类似 .gitignore,用来忽略不必要进构建上下文的目录(如 node_modules),可以明显提升构建速度、减小上下文。这是很容易被忽略的一个优化点。

真实实验:用 Linux Namespace 亲手体验隔离

不开 Docker,纯用 Linux 原生技术体会"容器 = 进程"。先在腾讯云开通一台虚拟机(这一步会用到上一章的 IaC 能力),然后启动一个 Nginx 容器,找出它在宿主机上对应的进程 PID,再 nsenter 进它的 namespace:

docker run -d nginx                 # 启动容器
docker ps                           # 找容器 PID
ps aux | grep 13082                 # 该 PID 的启动命令就是镜像 Dockerfile 里的 CMD
# 查看该 PID 的全部 namespace 类型(mount/pid/net/uts/user/ipc + cgroup)
ls -l /proc/13082/ns

# 进入该 PID 的 mount namespace,体验文件系统隔离
nsenter --target 13082 --mount
ls /                                # 你会看到这是"容器里的文件系统",跟宿主机完全不同
exit

# 只进 mount 就改 hostname,宿主机也会被改(因为没进 uts!)
nsenter --target 13082 --mount hostname my-container
hostname                            # 宿主机变了

# 进入全部 namespace 再改 hostname,宿主机才不受影响
nsenter --target 13082 --mount --uts --pid --net --ipc --user hostname my-container2
复习提示:踩坑:第一次只进 mount namespace 就改 hostname,结果发现宿主机 hostname 也变了,很困惑。原因正是 hostname 由 uts namespace 隔离 —— mount 管文件系统、uts 管主机名,各司其职。这也说明"容器是一个进程"这句话里,各种 namespace 是能被单独进入的。

镜像的本质:一堆目录文件的打包 + 联合文件系统

如果镜像只是"目录和文件的打包",那么一旦修改或想往里面 copy 业务源码,就得重新传输整个包,效率很低。于是有了联合文件系统(UnionFS):

  • 写 Dockerfile 时,每一条命令都会生成一个 layer(层),镜像是逐层堆叠的。
  • 要修改某个 layer 时,不会改动旧层,而是创建一个新层。
  • 传输时只需传输差异层即可。
  • layer 以哈希命名,内容不变哈希就不变,从而能复用构建缓存。

这张图在讲什么:每条 Dockerfile 指令盖一层,越靠上的层覆盖越靠下的层,最顶上那一层就是容器运行时"看到"的文件系统。

flowchart TB
    L1[FROM 基础镜像
只读层 1] --> L2[RUN 安装依赖
只读层 2] L2 --> L3[COPY 代码
只读层 3] L3 --> L4[容器可写层 upperdir
运行时的修改]

常见的联合文件系统有两种:AUFS 和 OverlayFS。OverlayFS 的核心是四个目录:lowerdir(只读层,类似镜像地只读层)、upperdir(可写层)、mergedir(用户真实看到的文件系统)、workdir(中间目录)。

cd /tmp/overlay     # 实验目录:lowerdir/ upperdir/ mergedir/ workdir/
ls lower upper merge work

# 组装 OverlayFS:把只读层 + 可写层 合并到 mergedir
sudo mount -t overlay overlay \
     -o lowerdir=lower,upperdir=upper,workdir=work mergedir

# lower 有 1.txt 2.txt,upper 有 1.txt(同名)
ls mergedir         # 看到同名文件时上层 upper 覆盖下层 lower

写时复制(Copy-on-Write) 是理解容器文件系统变更的关键:

  • 在 mergedir 里修改一个来自只读 lowerdir 的文件 → 新内容写入 upperdir(upperdir 里"多出来"一个文件),lowerdir 完全不受影响。
  • 在 mergedir 里删除一个 lowerdir 的文件 → 实际上在 upperdir 生成一个标记为已删除的占位文件(用 ls -la 看会显示一个特殊字符),lowerdir 里的原文件仍然在。
复习提示:类比到 Docker:镜像的只读层就是 lowerdir,容器启动后的可写层就是 upperdir,你在容器里看到的是 mergedir。你在容器里做的修改(写可写层),不会影响镜像本身 —— 这也呼应了镜像"不可变"的特性。

想真正把"对容器的修改"固化回镜像,用:

docker commit <容器> 新镜像名:tag

从 Layer 特性看镜像优化

用 Nginx 做例子:镜像构建出来会有 6 个层(FROM 一个层、RUN 一个层、RUN 又一个层……),而 CMD 不会产生新的层,它只影响元数据。

Layer 缓存带来的关键优化点是指令顺序:

  • 如果 COPY 的源码没变,它下面所有层缓存都能复用。
  • 一旦源码变了,COPY 这层及其之后的所有层都失效,即使后面的命令没变也要重新构建。
# 错误示范:把可能频繁变化的 COPY 放前面,坏处是源码一改缓存全失效
FROM node:18
COPY . /app
RUN npm install

# 正确示范:把"不变的依赖安装"放前面,COPY 靠后,源码变更只影响最后几层
FROM node:18
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app

此外还有减少层数:把多条 RUN 合并成一条(用 && 连接),避免生成多个层、减小体积。

一句话总结 Dockerfile 优化原则:最不易变的放最前、最易变的放最后,并合并 RUN 减少层数。

Dockerfile 的最佳实践

这一讲把 Dockerfile 从"能跑"提升到"生产级"。核心围绕四个词:忽略、减体积、安全、多平台。

减体积:选对基础镜像 + 多阶段构建

.dockerignore 用于忽略不必要文件(node_modules 等)以减少构建上下文。而减镜像体积最有效的手段是:

  1. 选择合适的基础镜像:同样情况下 node:alpine 和 node:slim 体积差别很大。Alpine、distroless 都是出了名的"小"。
  2. 多阶段构建:把构建环境和运行环境分开。以 Go 为例,构建需要完整的 golang 编译工具链,但运行时只需小镜像(如 debian slim)。多阶段构建能把镜像从 900 多兆直接减到几十兆。
# 多阶段构建:第一阶段编译,第二阶段运行
FROM golang:1.21 AS build        # 第一阶段:构建环境
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -o bin/app .

FROM debian:slim                 # 第二阶段:运行环境(无编译工具,很小)
COPY --from=build /src/bin/app /app
CMD ["/app"]

COPY --from=build(或 --from=0)用于把上一阶段构建的产物拷过来。

为什么生产环境要慎用 Alpine

Alpine 以"极小"著称,最小只有几兆。但它的核心坑在于 C 库不同:

  • 主流镜像用 glibc(GNU C 库,libc.so.6)。
  • Alpine 用 musl。

如果应用引用 / 调用了 C 库(比如一个依赖 CGO 的 Go 程序、Node 的原生模块),在第二阶段因为没有对应的 glibc 而运行不起来。

# 会报错:no such file or directory,但文件明明在 —— 其实是 C 库不匹配
FROM golang:1.21 AS build
COPY . /src
WORKDIR /src
RUN go build -o app main.go      # 默认 CGO_ENABLED=1,链接了 glibc

FROM alpine
COPY --from=build /src/app /app
CMD ["/app"]                     # 运行报 EXEC no such file,因为缺 glibc

解决办法:Go 编译时关掉 CGO,编译纯静态链接的二进制,让它不再依赖任何 C 库:

FROM golang:1.21 AS build
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -o app main.go   # 静态链接

FROM alpine
COPY --from=build /src/app /app
CMD ["/app"]
复习提示:关于 Alpine 的真正结论:Alpine 在某些场景(纯静态应用)确实能跑且运行正常,但很多业务多少会引用 C 库,可能只是没执行到那条路径 —— 它始终是个隐患。所以生产环境建议慎用 Alpine,必要时用 CGO_ENABLED=0 编译成静态二进制。

极限压缩:用 scratch 空镜像

scratch 是一个空镜像,镜像就只剩下你的二进制可执行文件。把前面 Go 程序的运行阶段换成 scratch,镜像能压到 11MB 左右(基本等于二进制本身大小)。

FROM golang:1.21 AS build
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -o app main.go

FROM scratch                     # 空镜像
COPY --from=build /src/app /app
CMD ["/app"]
复习提示:注意:scratch 里没有任何 C 库,所以使用它时(无论 Go 还是其它语言)一定要确认产物已禁用 CGO / 静态链接,否则会在运行时崩溃。

安全属性清单

  • 尽量避免使用 root 用户:在 dockerfile 里用 USER 声明非 root 用户,控制权限范围。
  • 固定镜像 tag:FROM 时尽量不要用 latest,因为上游一直在更新,某天上游更新可能导致你的业务逻辑出问题。
  • 使用官方认证的基础镜像:在 Docker Hub 上标有官方认证的镜像相对安全可信;尽量避免第三方镜像,因为你不知道里面装了什么、可能存在安全漏洞或隐患。
  • 设置时区:时区不正确,业务获取时间也会不正确。

构建镜像的进阶:DIND 与 BuildKit

docker build 依赖 dockerd 这个 daemon 进程。很多团队在 CI 里构建镜像时,直接把 docker daemon 挂到容器里 —— 这就是 DIND(Docker-in-Docker)。但 docker 权限很大,可能带来安全隐患,严重时甚至导致宿主机被入侵。

解决思路是不依赖 dockerd 构建镜像,即 BuildKit(以及同类工具)。BuildKit 可以让你在容器里(甚至在 K8s 里)构建镜像,不需要本机的 docker daemon,是更安全、更适合 CI 的做法。

循序渐进压缩镜像的完整实验

用一个 Go 例子串起来:最初多阶段构建(golang → 第二阶段基础镜像),镜像约 140MB。优化路径:

  1. 第二阶段基础镜像换成最小镜像 → 部分场景可行但埋下 C 库隐患。
  2. 换成写 CGO_ENABLED=0 关掉 CGNU C 依赖 → 可在 Alpine 上正常运行。
  3. 第二阶段用 scratch 空镜像 → 压到 约 11MB(几乎就是二进制文件大小)。

镜像真的"一次构建到处运行"吗?

答案是否。现代 CPU 架构有 amd64、arm64,而镜像里包含可执行文件,编译时是与平台相关的,所以镜像本质不是跨平台的。那么 Docker 是怎么实现"找对架构"的?multi-arch(多架构镜像)机制:

  • 在 Docker Hub 拉镜像时,会告诉 registry 想要哪个平台(amd64 / arm64)。
  • 镜像的架构信息存放在镜像的 manifest 文件里,该 manifest 是一个"索引",记录了各平台对应的 digest。

可以用 Google 开源的镜像工具 crane 查看:

crane manifest alpine   # 看到 architecture 字段,以及 amd64/arm/arm64/386 等各平台 digest

构建多架构镜像:

docker build --platform linux/amd64,linux/arm64 -t my-app:1.0 .
# 若用 Docker Desktop 需在设置里打开"多平台"开关,否则会提示无法构建

容器运行时底层链路

运行时涉及的几个层,搞懂它们就知道"谁在干什么":

docker run ──► dockerd ──► containerd ──► runc(真正启动容器的底层工具)
Kubernetes ──► CRI 抽象层 ──► containerd / CRI-O ──► OCI 配置 ──► runc

这张图在讲什么:Docker 和 Kubernetes 虽然入口不同,但最终都会汇到同一条由 containerd → runc 组成的底层链路;runc 是真正拉起进程的那个工具。

flowchart LR
    A[docker run] --> C[containerd]
    K[kubectl / K8s 节点] --> I[CRI 抽象层
统一 K8s 与底层运行时] --> C C --> R[runc
OCI runtime 标准实现] R --> P[启动容器进程
需要 rootfs + config.json]
  • runc 是整条链路的核心,也是 OCI runtime 的标准实现。它不能直接"运行一个镜像",而是需要两样东西:文件系统(镜像解压出来的 Linux 文件系统)+ config.json(运行配置文件,含容器名、环境变量、启动命令与参数、各种钩子)。
# 用 runc 直接启动容器:需要 rootfs + config.json
runc spec                 # 生成默认 config.json
runc run my-container     # 启动容器,进入后 ps / hostname 都是隔离的
复习提示:结论:runc 就是容器技术最底层的工具,容器本质是 runc 的一个进程。Docker 真正的创新不在于底层技术(2007 年就已成熟),而在于简化了使用门槛、用 Docker Hub 解决了镜像分发 —— 这也是它流行的根本原因。

初级应用定义:Manifest

这一讲快速回顾 Kubernetes 的应用定义——用 Manifest(YAML)来描述应用。重点不是学完所有 Kubernetes 对象,而是掌握工程实践里的几个实用技巧。

工程实践一:用 kubectl 在本地生成模板

很多人写 Manifest 会去百度、Google 文档,其实有更简单的办法:用 kubectl 在本地直接生成一个 YAML 模板,再根据模板修改,而不是部署到集群。

kubectl create deployment nginx --image=nginx:1.25 \
    --dry-run=client -o yaml        # 本地输出部署模板
kubectl create service nodeport nginx --tcp=80:80 \
    --dry-run=client -o yaml        # 生成 Service 模板
kubectl create configmap my-config \
    --from-literal=key=value --dry-run=client -o yaml   # 生成 ConfigMap

原理上,凡是工作负载或 Kubernetes 对象,都能用 --dry-run=client -o yaml 在本地拿模板再改。另外在 VS Code 里装 Kubernetes 插件,写 YAML 时会有模板内容提示。

工程实践二:YAML 多文件合并

一个 YAML 文件里可以用三个横杠 --- 分隔一次部署多个对象,可实现"一个文件多个对象"的集中部署:

apiVersion: apps/v1
kind: Deployment
spec: {}
---
apiVersion: v1
kind: Service
spec: {}

工程实践三:YAML 的几个经典坑

  1. 保留字变布尔值:写 no 时渲染会被当成布尔值 false 而不是字符串 "no"。要写字符串,用双引号包起来。
  2. 数字精度:1.10 会被自动转成 1.1。如果 Version 是 1.10,同样要加双引号。
  3. 多行内容的 4 种写法有区别(常用于写 ConfigMap):
    • |:保留文字间换行,保留末尾换行。
    • |-:保留文字间换行,不保留末尾换行。
    • >:不保留文字间换行(折叠),保留末尾换行。
    • >-:不保留文字间换行,不保留末尾换行。
    • 横杠代表"不保留末尾换行"。
  4. 锚点引用:可以用 & 定义一个块,再用 *粗体关键字 引用,不过生产实践中用得较少。

常用工具

  • 3Y / YAML Viewer:校验 YAML 写法是否正确。
  • yq:命令行修改 YAML,很多团队在 CI 里用它来改镜像版本或任意 key,非常实用:
yq eval '.spec.template.spec.containers[0].image = "app:2.0"' -i deploy.yaml

微服务示例应用的设计和实现

这一讲我们部署一个开源的示例投票应用(典型微服务架构),用它来复习 Manifest 定义,也用它作为后面 Helm / Kustomize 改造的对象。

应用架构:解耦的生产者-消费者

这是一个用 Redis 解耦、异步生产者-消费者的经典设计,非常值得借鉴:

服务语言/框架职责
votePython / Flask投票页面,把投票 POST 到 Redis 列表尾部(rpush)
worker.NET / ASP.NET消费 Redis 队列,把结果写入 PostgreSQL
resultNode.js / Express展示投票结果,通过 WebSocket 实时刷新
redis中间件用列表做异步队列,解耦生产与消费
dbPostgreSQL持久化投票结果

关键点:

  • vote:只定义了一个根路由,接收 POST 后 rpush 到 Redis 列表"尾部"。它用 Python 写的,虽然依赖 Redis,但只有请求到来时才连接,所以能正常启动。
  • worker:用 while 死循环,每 100ms 从 Redis 列表"头部"(lpop)读数据,因为是"尾部进、头部出",就实现了先进先出(FIFO)队列;最后通过 SQL insert 写入 PG。
  • result:不用 HTTP 轮询,而是用 WebSocket(socket.io) 实时推送,所以点完投票结果立即刷新;它用 setInterval 每 1 秒循环查询 PG 并写入 socket。

容器化 result 的坑:PID 1 与僵尸进程

result 是 Node.js 服务,容器化后进程以 PID = 1 启动。PID 1 在接收退出信号时有一个经典问题:普通信号处理 + 产生"僵尸进程"(zombie),导致容器无法优雅退出、子进程无法回收。

解决办法是在 drivefile 里用 tini 这种"init 组件"来启动 Node.js:

# result 的 Dockerfile(核心是让 tini 成为 PID 1 去启动 node)
FROM node:18
ENV TINI_VERSION v0.19.0
RUN apt-get update && apt-get install -y tini
COPY . /app
WORKDIR /app
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]
复习提示:容器化 Node.js / Java 应用一定要关注 PID 1 问题:让一个 init 进程(tini 等)作为 PID 1,既能防止产生僵尸进程,又能正确捕获退出信号。这类 init 组件也可用于 Java 项目,是生成可用的。

服务间依赖:从重启机制到 initContainer

result 依赖 PG、worker 依赖 Redis。如果业务优先于中间件启动、又连不上中间件,会抛异常、Pod 崩溃,然后 K8s 自动重启它:

  • 方案一:业务代码里做重试。result 就是这么做的,每秒重试直到连上 PG。但现实中很多业务写不了这么健壮的代码。
  • 方案二:靠 K8s 自动重启机制。容器异常退出(状态码非零)时 K8s 自动重启 Pod。但没有强烈依赖,因为它用的是指数退避(下一次重启时间是上一次的两倍),会让应用整体启动时间变得很长,影响到环境拉起速度(尤其开发/测试环境)。
  • 方案三(推荐):用 initContainer 控制服务启动顺序,让业务容器"等待被依赖的服务就绪"后才启动,实现真正的启动依赖控制。

initContainer 实战 + RBAC 前置

wait 是开源工具,可在 initContainer 里等待某个 Label 的 Pod 就绪。因为 K8s 对 initContainer 有权限要求,先给 Pod / Service / Deployment 授一个 Role 并绑到 default ServiceAccount:

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: pod-reader
rules:
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: wait-for-db
subjects:
  - kind: ServiceAccount
    name: default
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

然后在 result 的 Deployment 里加一个 initContainer,让它等 app=db 的 Pod 就绪:

spec:
  initContainers:
    - name: wait-for-db
      image: ghcr.io/kyverno/cli/wait:latest   # wait 开源工具
      args:
        - pod
        - app=db      # 等待 label app=db 的 Pod ready

查看效果:删掉 db 后部署 result,Pod 会一直处于 init 状态(业务容器没启动,0/1);部署 db 后,result 日志里出现 “Pod app=db ready”,业务容器才真正启动。这就实现了不改业务源码、纯 K8s 层的启动依赖控制。

复习提示:连 wait 工具也一样要有权限:initContainer 内部去查询 Pod 状态,K8s 默认 ServiceAccount 没有对应权限,所以要先用 Role + RoleBinding 授权给 default ServiceAccount,否则会一直因权限问题卡住。

链式启动整个微服务

有了 initContainer,就能控制整个微服务的启动顺序,实现链式依赖启动:依次 redis → db → worker → vote → result。做法就是在每个服务的 Deployment 里加对应的 initContainer:

redis   无依赖,第一个启动
db      等待 app=redis 就绪
worker  等待 app=db 就绪
vote    依赖 redis(请求时才连),可正常启动,但要等 worker
result  等待 app=vote 就绪

这张图在讲什么:每个服务都通过 initContainer 监听"依赖项对应 label 的 ready",于是整条链像多米诺骨牌一样依次被拉起。

flowchart LR
    REDIS[redis
无依赖
第一个启动] --> DB[db
等待 app=redis 就绪] DB --> WORKER[worker
等待 app=db 就绪] WORKER --> VOTE[vote
等待 worker 就绪] VOTE --> RESULT[result
等待 app=vote 就绪]

一句话:把 initContainer 对应地写进每个工作负载,前端监听后端 label 的 ready,就能控制整条链的启动顺序。

数据表结构与初始化数据怎么处理

两种典型方式:

  1. 业务代码里直接初始化 + 填充种子数据:小业务、没有专职 DBA 时常用,简单直接(示例投票应用的 worker 服务就是这么初始化 PG 表结构的)。
  2. 用 Kubernetes Job 做初始化:在 Job 里 clone 一个 SQL Git 仓库,执行 SQL 语句初始化库。这是大项目推荐的方式,DBA 可以维护统一的 SQL schema 仓库,基于它做权限控制、回滚、门禁、review,等等。

中间件的最佳实践

示例应用里 Redis / PG 是用单实例测试方式部署的,生产不推荐——Pod 重启数据就丢了。生产建议:

  • 用社区封装好的高可用 Helm chart:这些 chart 提供高可用、多实例(StatefulSet + 分片)部署,一条命令就能拉起。
  • 更进一步,中间件尽量使用云服务,自托管是下策。理由:云厂商对中间件的高可用定制更深(自动备份等),出了问题是大规模问题、不是你们运维团队的锅;还能通过工单获得厂商工程师服务。

应用定义:Helm 入门和实战

在 K8s 上部署微服务,三个东西缺一不可:应用代码(源码)、Dockerfile(把代码打包成镜像)、K8s Manifest(定义镜像怎么部署到集群)。当服务数量少时 Manifest 还可控;到十几个、几十个时,原生 Manifest 就难以管理了。

Helm 是什么:K8s 的"应用安装包"

想想在 Windows / Mac 上装软件:你只需要拿到安装包,双击安装,可能暴露少数几个必要参数让你覆盖。Helm 之于 K8s,就是这种"应用安装包":它把复杂的逻辑整体内聚在包里,对用户只暴露必要的参数,一条命令完成安装。

Helm 最大的能力是把一个 K8s 应用打包,并对 Manifest 模板化。原生 Manifest 是硬编码的,无法实现动态逻辑;有了模板,部署时只需覆盖个别参数就能实现动态部署(比如不同环境的参数差异)。

三大核心概念

概念含义类比
ChartK8s 应用的安装包,包含应用的所有 K8s 对象,用于创建 releaseWindows 的 .msi / Mac 的 .dmg 安装包
ReleaseChart 安装后的实例,可在集群不同命名空间安装多次“双击执行安装包"这个安装行为 / 结果
Repository托管、分发 Chart 的仓库,提供存储与拉取,可存 Git 或 OCI 仓库应用商店 / 软件源

这张图在讲什么:chart 是"安装包”、release 是"装好的实例"、repository 是"下载源"——三者构成 Helm 的完整分发与运行闭环。

flowchart LR
    RE[Repository
软件源:托管 / 分发 chart] -->|拉取 chart| CH[Chart
应用的安装包] CH -->|helm install 模板化渲染| RL[Release
安装后的一整套 K8s 对象
可在多个命名空间各装一次]

常用命令

helm install <release> <chart路径>            # 安装,安装后成为 release
helm uninstall <release> -n <ns>              # 卸载
helm upgrade --install <release> <chart>      # 有则升级、无则安装(幂等,CI 常用)
helm repo add <name> <url>                    # 添加远端仓库
helm list -n <ns>                             # 列出当前 release
helm template .                               # 本地渲染模板调试(不安装)
helm dependency build / update                # 拉取并重新生成依赖

从零创建一个 Chart

helm create demo            # 生成一个名为 demo 的 chart

生成的目录结构三大块:

demo/
├── templates/              # 放所有 K8s Manifest(deployment.yaml / service.yaml / ...)
├── chart.yaml              # 描述 chart 元数据:名称、版本、依赖 dependency、作者
└── values.yaml             # 整个 chart 可配置的参数(默认值),安装时可覆盖

values.yaml 就是模板化的关键:里面配置的每个参数都可在安装时覆盖,不覆盖就用默认值。模板变量在 templates/ 里用 Go template 语法读取:

# values.yaml
image:
  repository: nginx
  tag: v1.0.0
# templates/deployment.yaml(片段)
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}   # 渲染后 = nginx:v1.0.0

安装、验证、卸载

helm install nginx ./demo \
    -n myapp \
    --create-namespace \
    --set image.tag=latest      # 临时覆盖 values.yaml 里的字段

helm list -n myapp              # 查看 release
helm uninstall nginx -n myapp   # 卸载

upgrade –install:CI 里的幂等用法

在 CI 流水线里每次都会跑部署,如果用 helm install,第二次运行时因为 release 已存在会直接抛异常。解决办法是用 helm upgrade --install:没有该 release 就安装,有就升级;若参数不变,升级就相当于什么都不做。这比 install 更常用。

helm upgrade --install nginx ./demo -n myapp --create-namespace

安装远端 Chart

除了本地 chart,还可以直接安装远端仓库的 chart(下面用 Ingress 举例):

helm upgrade --install ingress ingress/ingress-nginx \
    -n ingress --create-namespace

Helm 会先从远端 repository 找到对应的 chart,再安装。

实战:把示例应用改造成 Helm chart

改造时需要抓住几个问题:抽哪些变量、如何调试、多环境怎么处理、能否控制中间件部署。

  1. 建立目录 & 拷入 Manifest:创建 demo6,建好 templates/、chart.yaml、values.yaml,把示例应用原来的 K8s YAML 拷进 templates/(原生 K8s 对象即使不模板化也能直接放进去)。
  2. 抽变量:把 vote / worker / result 三个服务的 image 和 tag 抽到 values.yaml,并在对应工作负载里改成模板变量:
    image: {{ .Values.result.image }}:{{ .Values.result.tag }}
    
  3. 条件控制中间件部署:给 postgresql.deploy / redis.deploy 这类布尔开关,用 Go template 的 if 控制是否渲染中间件对象。生产环境可能用云厂商中间件,就不该部署自己的。
# templates/db.yaml 开头
{{- if .Values.postgresql.deploy }}
apiVersion: apps/v1
kind: Deployment
# ... PG 的完整定义
{{- end }}

调试渲染(不实际安装)用 helm template .,能直观看到替换后的结果,也可以检查 if 条件是否生效。改造后 helm install vote ./demo6 -n myapp --create-namespace 一条命令完成安装/卸载/升级。

高级主题 1:Chart 依赖(Dependency)

如果一个应用不想自己写 PG / Redis,而想引用社区封装的高可用 chart,就可以用 dependencies。在 chart.yaml 里声明:

# chart.yaml
dependencies:
  - name: redis
    version: 17.16.0
    repository: https://charts.bitnami.com/bitnami
    condition: redis.deploy    # 用 values 里的开关控制是否启用
  - name: postgresql-ha
    version: 11.9.0
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.deploy

安装前必须从远端拉取依赖:

helm dependency update ./demo7     # 把声明的子 chart 下载到 charts/

dependency 机制其实是借助**子 chart(subchart)**实现的:运行 dependency 命令时会把声明依赖的 chart 下载到 charts/ 目录,安装时一并安装。对于复杂应用,可以拆成多个 chart 再用依赖方式引用。

复习提示:能复写子 chart 的 values,但不能(在这里)复写模板:要在父应用里覆盖所引用的第三方 chart 的默认值(比如改掉 PG 的用户名密码方更连接结构),就在父 values.yaml 里用一个与子 chart 同名的根节点来复写。先用 helm inspect values <chart> 查看第三方 chart 有哪些可覆盖参数。
# 父 values.yaml:复写 redis / postgresql-ha 子 chart 的配置
redis:
  fullnameOverride: redis
  auth:
    enabled: false     # 关掉认证,方便直连

postgresql:
  postgresqlUsername: vote
  postgresqlPassword: vote123

高级主题 2:内置变量、global 与 Hooks

  • 内置变量:Release(如 Release.Name、Release.Namespace)、Values(读取 values.yaml)、还有 chart.yaml 里的内容也能引用。

  • include / 模板内置函数:Go template 里有 printf 等大量内置函数,可用于字符串处理等;需要时查文档即可。

  • global 保留关键字:在 values.yaml 里声明一个 global 根节点,父子 chart 都能读取,适合放跨图表共享的全局值。

  • Hooks(钩子):安装过程中的一系列执行点,比如:

    • pre-install:模板渲染后、创建任何资源前执行(如先拉起数据库、做前端初始化数据)。
    • post-install:资源部署到集群后执行。
    • pre-delete / post-delete:删除前/后执行。

    Hooks 通过 annotation 声明用哪个钩子,并可用 weight 权重控制多个钩子的执行顺序(权重越小越早执行,可为负数)。

kind: Job
metadata:
  annotations:
    "helm.sh/hook": post-install    # 安装完成后执行
    "helm.sh/hook-weight": "0"

常用命令速查

helm upgrade --install ...    # 安装/升级(幂等)
helm inspect values <chart>   # 查看 chart 可复写的 values
helm repo list                # 查看本地已添加的仓库
helm repo add <name> <url>    # 添加仓库
helm dependency update        # 升级应用前更新依赖
helm rollback <release> <rev> # 回滚到上一个版本

应用定义:Kustomize 入门和实战

Helm 之后我们再学 Kustomize。它(这就是后续开发 AIOps 会用到的工具),值得多花点时间。

什么是 Kustomize

Kustomize 是一个扩展 k8s 自带的工具,核心能力是对 Manifest 的任何字段进行复写(override)。和 Helm 不同:Helm 要抽模板变量才能复写,而 Kustomize 不需要抽模板变量,对任意字段都能复写。它尤其适合多环境场景,并且直接内置到了 kubectl 里(kubectl apply -k)。

布局:base + overlay

最基础的 Kustomize 定义有 base 目录 + 若干 overlay 环境目录,每个目录都有自己的 kustomization.yaml:

myapp/
├── base/                      # 所有环境通用的基准
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml     # 声明 resources 引用哪些文件
└── overlay/
    ├── dev/
    │   ├── redis.yaml         # 开发环境自己写的中间件
    │   ├── db.yaml
    │   └── kustomization.yaml # 声明引用 base + 本目录差异
    └── prod/
        └── kustomization.yaml # 生产环境:用第三方 helm chart
  • 每个环境目录除了公共的基础对象,还可以有自己的特殊对象(如 HPA、更多 ConfigMap)。
  • base 负责公共部分、是所有环境的基准;overlay 用 kustomization.yaml 描述"在这些基础上我叠加 / 覆盖什么"。

这张图在讲什么:一次定义 base,多个 overlay 各自"只写差异",最终 kubectl apply -k 把 base + overlay 拼成各自环境的完整 Manifest。

flowchart TB
    BASE[base
所有环境公共的基准对象] --> DEV[overlay/dev
叠加差异 + 自己写的中间件] BASE --> PROD[overlay/prod
叠加差异 + 第三方 helm chart] DEV --> K1[kubectl apply -k
dev 完整 Manifest] PROD --> K2[kubectl apply -k
prod 完整 Manifest]

kustomization.yaml 能配置什么

kustomization.yaml 是 Kustomize 的核心,常用配置项:

resources:                 # 引用要组合哪些 manifest 文件
  - deployment.yaml
  - service.yaml

secretGenerator:           # 从文件/字面量生成 Secret
  - name: my-secret
    literals:
      - password=123456

configMapGenerator:        # 生成 ConfigMap
  - name: my-config
    literals:
      - key=value

images:                    # 快捷复写镜像仓库与 tag
  - name: myapp
    newName: registry.example.com/myapp
    newTag: v2.0.0

patches:                   # 通用复写:改任意 manifest 里的任意 key
  - target:
      kind: Deployment
      name: vote
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 3

helmCharts:                # 引第三方 helm chart(Helm + Kustomize 可一起用)
  - name: redis
    version: 17.16.0
    repo: https://charts.bitnami.com/bitnami

实战一:改造成 base + overlay/dev

把示例应用改造成 Kustomize:

  1. 创建 base/,把 vote / worker / result 这些公共业务工作负载放进去,并在 base/kustomization.yaml 里声明 resources。
  2. 创建 overlay/dev/,把开发环境需要的、自己写的 redis / db 中间件放进去,在 overlay/dev/kustomization.yaml 里声明引用 ../base 的资源加上本目录的 redis/db。
  3. 渲染与调试(Kustomize 内置到 kubectl):
cd overlay/dev
kubectl kustomize .            # 只渲染不安装,查看复写后的完整结果
kubectl apply -k .            # 一步安装(-k 触发 Kustomize 原生渲染)

这样 myapp/base + overlay/dev 的组合就完成了。“lib层结果安装 dev 环境 = base 的所有对象 + dev 目录的中间件。”

实战二:overlay/prod 引用第三方 Helm chart

生产环境更严肃——不想自己写的中间件,而是引用第三方 helm 的 PG/Redis。方法是在 overlay/prod/kustomization.yaml 里:

resources:
  - ../base
helmCharts:
  - name: redis
    version: 17.16.0
    repo: https://charts.bitnami.com/bitnami
  - name: postgresql-ha
    version: 11.9.0
    repo: https://charts.bitnami.com/bitnami
# 通过 valueInline 复写第三方 chart 的 values;也常用 patches 复写任意字段

渲染时用 kubectl kustomize ./overlay/prod --enable-helm(加 --enable-helm 让 Kustomize 帮你下载并启用第三方 helm chart),确认生效后再 kubectl apply -k . 部署。

为什么还需要 Kustomize:选型得失

维度HelmKustomize
复写能力需抽模板变量 + 提供值任字段直接复写,不学模板语法
上手难度需学 Go template 模板语法低,只需 patches / images
多环境通过不同 values 文件通过 base + overlay 复用,只关心差异部分
对用户友好度对终端用户隐藏应用细节更面向 K8s 开发者
分发很方便,自带 OCI/仓库、生态大分发不便,生态比 Helm 差
依赖目录结构相对宽松强依赖目录结构,结构决定布局是否合理
模板强大、支持动态逻辑无模板变量,纯补丁复写
复习提示:选型直觉:需要"打包分发一套成熟应用/中间件 + 复杂动态模板"用 Helm;需要在多环境之间复用并给同一份 Manifest 打补丁、改字段(更贴近 K8s 原生)用 Kustomize。很多团队会 Helm 装第三方应用 + Kustomize 做环境差异,两者并不冲突。

总结

容器和 K8s 是 AIOps 的运算底座。这一章我们从最底层的原理一路走到应用定义工具,主线是:

  • 容器本质:不是虚拟机,宿主机上的一个进程;靠 Namespace(隔离视野)+ Cgroups(限额资源)+ UnionFS/OverlayFS(文件系统分层与写时复制)实现。历史脉络 chroot(1979) → namespace(2002) → cgroup(2007) → Docker(2013)。
  • 镜像构建:Dockerfile 的核心指令 FROM/RUN/COPY/CMD;镜像逐层堆叠、layer 缓存、.dockerignore;优化原则是"最不变放最前、合并 RUN 减层"。
  • 最佳实践:多阶段构建分离构建/运行环境;慎用 Alpine(glibc vs musl);CGO_ENABLED=0 静态链接 + scratch 极限压缩(Go 可从 900M 压到 11M);安全清单 + 固定 tag + 官方镜像 + 非 root;镜像并非真正跨平台,靠 multi-arch manifest 按平台拉取;底层链路 dockerd→containerd→runc。
  • Microservice 应用定义:vote/worker/result+redis+db 的异步生产者-消费者设计;result 的 PID 1 用 tini 解决僵尸进程;用 initContainer 实现链式启动依赖控制;数据初始化用 Job、中间件尽量云服务。
  • Helm:chart/release/repository 三大概念;template 模板变量与条件控制;upgrade --install 幂等、dependency 引用子 chart、hooks;把示例应用改造成一条命令可装/卸/升的 chart。
  • Kustomize:base/overlay 多环境模式;resources / images / patches / helmCharts 复写任意字段,无需模板;kubectl apply -k 原生支持;与 Helm 按场景互补。

这一章最怕"似懂非懂"——概念翻来覆去听懂了就觉得很对啊,但真要动手写一个 Dockerfile、排一个镜像起不来的错、把一个 Manifest 拆成 base/overlay,每一处都有坑。建议所有命令都亲手敲一遍,从底层原理到 Helm/Kustomize 的改造闭环都走完,后面写 AIOps 应用、调 Operator 时才会游刃有余。


自测题与动手练习

面试官
容器的本质是什么?Namespace 和 Cgroups 分别解决什么问题?它为什么说"容器 = 宿主机上的一个进程"?
候选人
容器本质是宿主机上的一个进程,通过 Linux 机制做隔离。
Namespace(6 种:mount/pid/net/uts/user/ipc)管"进程能看到什么",隔离文件系统、进程、网络、hostname 等视角;
Cgroups 管"进程能用多少资源",约束 CPU、内存、网络 IO。
二者作用对象都是"进程",所以容器就是被隔离看视野、被限制配额的进程 —— 每个容器看起来像一台独立虚拟机。
面试官
镜像和容器是什么关系?联合文件系统(OverlayFS)里的 lowerdir / upperdir / mergedir 分别对应什么?写时复制为什么重要?
候选人
镜像是"定义"(文件系统+环境变量+启动命令),容器是镜像的"实例化",类似 class 与实例。多个容器相互隔离。
OverlayFS 四个目录:lowerdir(只读层,= 镜像只读层)、upperdir(可写层,= 容器启动后的可写层)、mergedir(用户看到的文件系统)、workdir(中间目录)。
写时复制:在 mergedir 改/删 lowerdir 的文件时,新内容 / 删除标记落在 upperdir,lowerdir 完全不受影响 —— 所以"在容器里的修改不污染镜像",镜像保持不可变。
面试官
为什么 COPY . /app 放前面会导致每次源码变更都重建缓存?正确的 Dockerfile 排序原则是什么?
候选人
每条 Dockerfile 指令生成一个 layer,layer 以内容哈希命名,内容不变哈希不变即可复用缓存;但一条指令变了,它及其之后的层缓存都失效。
COPY . /app 打包的是源码,最容易变,放前面会导致依赖安装那几层每次作废。
正确原则:最不易变的放最前、最易变的放最后 —— 先 COPY package.json / go.mod 装依赖,最后再 COPY . 拷源码;同时合并多条 RUN 减少层数、用 .dockerignore 忽略不必要文件。
面试官
为什么生产环境要慎用 Alpine 镜像?用 Go 做例子,如何解决在 Alpine/scratch 上"文件存在却运行报 no such file"的问题?
候选人
Alpine 用 musl C 库,主流镜像用 glibc。应用若链接/调用了 glibc C 库(如 CGO 的 Go、Node 原生模块),在 Alpine 上没有对应 glibc 就起不来 —— 报的却是"文件不存在",极具迷惑性。
解法:Go 编译时设 CGO_ENABLED=0(go build 时禁用 CGO),编译成纯静态链接二进制,不依赖任何 C 库,即可在 Alpine 甚至 scratch 空镜像上运行。用 scratch 可把镜像极限压到约 11MB(基本=单个二进制文件)。
面试官
“一次构建到处运行"的说法对吗?多架构镜像(multi-arch)是怎么实现的?运行一层的链路 docker run → … → runc 各是什么?
候选人
不对。镜像内含可执行文件,编译即与 CPU 架构绑定(amd64/arm64)。多架构机制:镜像的 manifest 记录了各平台对应的 digest,拉镜像时告诉 registry 目标平台,registry 分发对应架构版本。
构建用 docker build --platform linux/amd64,linux/arm64 ...。
运行时链路:docker run → dockerd → containerd → runc;K8s 侧走 CRI 抽象层 → containerd/CRI-O → OCI 配置 → runc。runc 需"文件系统 + config.json"两个 OCI 产物来真正启动进程,是容器技术最底层工具。
面试官
YAML 编写有哪些经典坑?kubectl create ... --dry-run=client -o yaml 有什么用?
候选人
经典坑:① no 这种保留字会被渲染成布尔值而非字符串,需加双引号;② 1.10 会自动变 1.1,Version 要加引号;③ 多行内容 |/|-/>/>- 四种写法在"是否保留文字间换行 / 末尾换行"上不同,写 ConfigMap 常踩。
kubectl create <对象> ... --dry-run=client -o yaml 能在本地生成 YAML 模板而不实际部署,再基于模板修改即可写出 Manifest,不用到处去搜文档。
面试官
微服务示例应用里,result/redis 等服务的依赖问题是怎么通过 initContainer 解决的?它相比"靠 K8s 自动重启"有什么优势?
候选人
result 依赖 PG、worker 依赖 Redis,若业务先于中间件启动会连不上而崩溃、被 K8s 自动重启(指数退避,下次重启时间是上次两倍,导致整体启动很慢)。
用 initContainer(如开源 wait 工具)等待 app=db 这类 label 的 Pod 就绪后才启动业务容器,即在 K8s 层、不改源码地控制启动顺序。配合 RBAC 给 default ServiceAccount 授权 Pod/Service 查询权限。
同理把 initContainer 链式写进每个工作负载,就能实现 redis→db→worker→vote→result 的链式启动。
面试官
Helm 的 chart / release / repository 分别指什么?helm upgrade --install 相比 helm install 有什么好处?
候选人
chart = K8s 应用的安装包(含所有对象+模板,如 .msi);release = chart 安装后的实例,可在多个命名空间各装一次;repository = 托管/拉取 chart 的仓库。
helm upgrade --install 在 repo 无 canary 时会自动安装、有则升级;CI 每次运行不会像 helm install 那样因 release 已存在而抛异常,是更推荐的幂等安装方式。
面试官
一条现有业务,你要在多环境(dev/prod)之间复用同一份 Manifest 并只做差异化。你会选 Helm 还是 Kustomize?为什么?
候选人
偏向 Kustomize,因为它的典型场景就是多环境:用 base + overlay 模式复用公共对象,overlay 里通过 resources / images / patches / helmCharts 只描述"差异部分",不用抽模板变量、不用学 Go template 语法,kubectl apply -k 原生支持。
若还要"打包分发整套成熟应用 + 动态复杂模板 + 依赖管理",则用 Helm(chart/release/dependency/hooks)。二者可组合:Helm 装第三方中间件、Kustomize 做环境差异。

动手练习(建议真做一遍):

  1. 在 Linux 虚拟机上启动一个 Nginx 容器,用 ps 找到进程 PID,分别只用 nsenter --mount 和全部 namespace 改 hostname,观察宿主机是否受影响;再用 mount -t overlay 手动拉一个 OverlayFS,体会写时复制与删除占位文件。
  2. 用一个 Go HTTP 服务,走完"基础多阶段构建(140M) → 换 Alpine 报 no such file → CGO_ENABLED=0 修复 → 换 scratch 压到 ~11M"的完整压缩链路;再用 docker build --platform linux/amd64,linux/arm64 构建一次多架构镜像。
  3. 用 kubectl create deployment/service --dry-run=client -o yaml 生成模板改出示例投票应用,并给 redis/db/worker/vote/result 逐个加 initContainer,实现并观察链式启动顺序。
  4. 把示例应用改造成 Helm chart(抽 image/tag + 条件控制中间件 + dependency 引用第三方 redis/pg),用 helm template . 调试、helm upgrade --install 部署、helm rollback 回滚一次。
  5. 把同一套 Manifest 改造成 Kustomize 的 base + overlay/dev + overlay/prod,dev 用自己的 redis/db,prod 用第三方 helm chart,分别用 kubectl kustomize . 和 kubectl apply -k . 完成渲染与部署。

本章小结

复习提示:
  • 容器 = 宿主机上的进程,靠 Namespace(看什么)+ Cgroups(用多少)+ UnionFS/OverlayFS(文件分层与写时复制) 隔离;历史 chroot(1979)→namespace(2002)→cgroup(2007)→Docker(2013),Docker 的创新在工程化与分发。
  • 镜像是同类定义、容器是实例化(class vs new);镜像逐层堆叠、layer 按内容哈希缓存,所以 Dockerfile 最不易变放最前、最易变放最后,并合并 RUN 减层、用 .dockerignore。
  • 最佳实践:多阶段构建分离构建/运行环境;慎用 Alpine(glibc vs musl),Go 用 CGO_ENABLED=0 静态链接后可在 scratch 上把镜像压到约 11M;固定 tag、用官方镜像、非 root、设时区。
  • 镜像并非真正跨平台,靠 multi-arch manifest 按平台分发;运行时底层 dockerd → containerd → runc,K8s 走 CRI → OCI → runc;runc 由"文件系统 + config.json"启动进程。
  • 示例投票应用是 Redis 异步解耦的生产者-消费者;Node 容器化用 tini 解决 PID 1 僵尸进程;用 initContainer 在 K8s 层实现 redis→db→worker→vote→result 链式依赖;数据初始化可用 Job、中间件尽量云服务。
  • Helm:chart/release/repository 三大概念;模板变量 + if 条件控制;upgrade --install 幂等、dependency 引用子 chart、hooks;适合打包分发成熟应用、复杂模板。
  • Kustomize:base/overlay 多环境复用,resources/images/patches/helmCharts 复写任意字段且无需模板语法,kubectl apply -k 原生支持;适合多环境打补丁,可与 Helm 组合使用。
  • 本章是容器和 K8s 的应用定义底座,后面的 AIOps 故障恢复、Operator、流量预测都会建立在这些能力之上。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!