☁️ 云计算
本章收录云计算相关知识,包括:容器(Docker/Podman/Kubernetes)、虚拟机(JVM/KVM)、虚拟化、云原生等。
来源:从
linux.md(容器管理)、rhcsa.md(容器题)、rhce.md(ansible-navigator)、redhat.md(容器考试变化)、network.md(OpenVPN Docker)汇总整理,原文件内容保留不变。
📑 目录索引
- 1. 容器基础概念
- 2. 镜像管理
- 3. 容器操作
- 4. 容器网络
- 5. 容器持久化与开机自启
- 6. 容器实战案例
- 7. 容器编排与执行环境
- 8. 虚拟机与虚拟化
- 9. JVM 虚拟机
- 10. 云服务与云原生
- 11. RHCE/RHCSA 容器考试专项
- 12. Dockerfile 指令详解(ENV / ADD / EXPOSE / CMD)
- 13. Docker 镜像存储位置 & 空间清理
- 14.
docker system df输出解读 - 15. Kubernetes 核心概念与操作
1. 容器基础概念
1.1 容器三大核心概念
Containerfile/Dockerfile ← 配方(怎么装软件、配什么环境)
↓ podman build
镜像 (Image) ← 打包好的成品(只读,不动)
↓ podman run
容器 (Container) ← 跑起来的实例(活的,占内存)| 概念 | 定义 | 类比 |
|---|---|---|
| Containerfile / Dockerfile | 镜像构建脚本,写清楚要什么材料、怎么做 | 菜谱 |
| 镜像 (Image) | 只读模板,包含 OS 基础层 + 软件 + 配置文件,存在本地磁盘上,一个镜像可同时跑多个容器 | 做好的菜,放冰箱里不动 |
| 容器 (Container) | 镜像跑起来的实例,在镜像基础上加可写层,占用 CPU 和内存 | 餐桌上正在吃的那盘菜 |
核心要点:
- 镜像是只读的,容器是可写的(在镜像上加一层可写层)
- 一个镜像可以同时跑多个容器
- 镜像存磁盘,容器占内存
1.2 容器运行时(Container Runtime)
容器运行时:真正负责把容器跑起来的那一层(创建 namespace、cgroup、挂载 rootfs、启动进程)。
| 运行时 | 简介 | 特点 | 适用场景 |
|---|---|---|---|
| LXC(Linux Containers) | 早期容器运行时,用 Linux 内核的 Namespace 和 Cgroup 实现进程隔离和资源管理 | 提供完整 Linux 系统环境;支持多种 Linux 发行版;早期 Docker 曾用 LXC 作为默认运行时 | 需要完整 Linux 系统环境的容器化应用 |
| Runc | 目前 Docker 默认的容器运行时,轻量级 CLI 工具,用于运行和管理容器 | 完全遵循 OCI Runtime Spec;轻量级高性能;是许多容器运行时的底层实现 | 需要高性能和轻量级容器运行环境的场景 |
| Rkt(Rocket) | CoreOS 开发的容器运行时,提供安全、可靠且符合 OCI 规范的运行环境 | 独立运行时,不依赖 Docker 守护进程;通过 AppArmor 和 SELinux 提供更好的安全性 | 对安全性要求较高的容器化应用 |
| Podman | Red Hat 开发的下一代容器运行时,RHEL 8/9 默认容器工具,RHCSA/RHCE 2026 官方指定 | 无守护进程(Daemonless);Rootless 原生支持;完全兼容 Docker CLI;原生支持 Pod 概念;podman generate systemd 一键集成 systemd | RHEL/CentOS 环境的无守护进程容器、rootless 安全容器、RHCSA/RHCE 考试 |
Podman 关键特性详解:
- 无守护进程(Daemonless):直接通过 runc 运行容器,不需要 Docker 那种常驻后台进程,部署更轻量、启动更快、故障域更小
- Rootless 原生支持:普通用户无需 root 权限就能运行容器,安全性更高(考试场景的核心要求)
- 完全兼容 Docker CLI:
alias docker=podman即可无缝切换;同时支持 Docker 镜像(docker.io)和 Podman 默认(localhost) - 原生 Pod 概念:除了单容器,还能管理 Pod(一组共享网络/存储的容器),与 K8s 的 Pod 概念对齐
- Systemd 集成:
podman generate systemd一键把容器转成 systemd 服务,开机自启很方便
一句话选型:
- 学习/考试(RHCSA/RHCE 2026):选 Podman
- 个人开发/快速上手:选 Docker(生态成熟)
- 生产 RHEL 环境:选 Podman(无 daemon、rootless 安全)
- 已有 Docker Compose 大量配置:继续用 Docker,或 podman-compose 平迁
1.3 Docker vs Podman
| 特性 | Docker | Podman |
|---|---|---|
| 架构 | C/S 架构(需要 daemon 守护进程) | 无守护进程(直接通过 runc) |
| Rootless | 需要额外配置 | 原生支持(推荐) |
| CLI 兼容性 | 自己的命令 | 完全兼容 Docker 命令(别名 alias docker=podman) |
| RHEL 推荐 | ❌ | ✅(RHCSA/RHCE 考试用 podman) |
| 配置文件 | /etc/docker/daemon.json | /etc/containers/registries.conf |
| 镜像构建 | docker build | podman build(用 Containerfile 或 Dockerfile) |
| 镜像命名 | docker.io/xxx | localhost/xxx(默认) |
考试场景:RHCSA/RHCE 2026 改用 Podman,所有容器操作都用 podman 命令。
1.4 镜像构建隔离性(Build once, run anywhere)
构建时:pip 包装到镜像里,与物理机隔离
问题: pip install -r requirements.txt 装的包是下载到镜像里吗?和物理机隔离吗?
答案: 都对 ✅
构建过程:
docker build启动临时容器- 在临时容器内执行
RUN pip install - 包安装到容器的
/usr/local/lib/python3.x/site-packages/ - 临时容器销毁,结果冻结成一层固化到镜像
两个核心点:
- 下载到镜像里:装的包是镜像文件系统的一部分,可
docker save/load分发 - 与物理机隔离:物理机没装 Python 也能跑,装了不同版本也不冲突
原理:分层文件系统 UnionFS
- 镜像多层叠加(基础镜像层 → COPY → pip install → ...)
- 容器在最上面加可写层
- 容器看到的是所有层的合并视图
- 物理机原生文件系统不参与
不隔离会怎样?
| 场景 | 冲突结果 |
|---|---|
| 物理机 Python 3.9,容器想用 3.12 | ❌ 版本冲突 |
| 物理机包 A v1.0,容器想用 v2.0 | ❌ 包冲突 |
| 项目 A 依赖 X v1,项目 B 依赖 X v2 | ❌ 多项目依赖冲突 |
核心结论
镜像 = 应用 + 依赖 + 运行时 + 配置,全部打包。 物理机只负责 Linux 内核 + Docker daemon。 这就是 Build once, run anywhere 的核心。
一句话总结: 构建时临时容器内 RUN,pip 包装到镜像某一层;运行时容器看到的 Python 和包都是镜像里的,和物理机完全隔离。
2. 镜像管理
2.1 镜像仓库配置
配置文件:/etc/containers/registries.conf
模板一(Toml 格式,RHCSA 考试用)
unqualified-search-registries = ["registry.lab.example.com"]
[[registry]]
location = "registry.lab.example.com"
insecure = true
blocked = false| 参数 | 含义 |
|---|---|
unqualified-search-registries | 不带域名前缀时默认搜索的仓库 |
insecure = true | 允许 HTTP 连接(考试内网无证书) |
blocked = false | 不阻止该仓库 |
模板二(INI 格式,RHCE ansible-navigator 用)
# /etc/containers/registries.conf
[registries.search]
registries = ['utility.lab.example.com']
[registries.insecure]
registries = ['utility.lab.example.com']2.2 构建镜像
# 完整流程(sashat 用户)
ssh sashat@servera
# 1. 下载 Containerfile
wget http://content.example.com/Containerfile
# 2. 登录镜像仓库
podman login registry.lab.example.com
# Username: admin / Password: 321
# 3. 构建镜像
podman build -t modify_file:latest .
# ^ 点不能忘!
# 4. 查看构建结果
podman imagespodman build 关键参数:
| 参数 | 含义 |
|---|---|
-t NAME:TAG | 指定镜像名和标签 |
. | 构建上下文(当前目录),漏掉会报错 |
-f Containerfile路径 | 指定 Containerfile 路径(默认当前目录) |
2.3 镜像导入/导出/标签/推送
# 1. 从 tar 包导入镜像
podman load < rsyslog.tar
# ⚠️ 用 < 重定向读取,不能直接跟文件名
# 2. 打标签(镜像名必须匹配仓库地址)
podman image tag localhost/rsyslog:latest \
registry.lab.example.com/library/rsyslog:latest
# 3. 登录仓库
podman login registry.lab.example.com
# 4. 推送到仓库
podman push registry.lab.example.com/library/rsyslog:latest
# 5. 拉取远程镜像
podman pull registry.lab.example.com/library/watch:latest
# 6. 搜索镜像(确认路径)
podman search registry.lab.example.com watch镜像命名格式:
[仓库地址/][命名空间/]镜像名[:标签]
localhost/rsyslog:latest # 本地镜像
registry.lab.example.com/library/rsyslog:latest # 远程仓库2.4 删除远程 Registry 中的错误镜像
问题场景:tag 错了本地镜像,push 到了远程 registry,需要删除
# 方法1:skopeo 删除(推荐)
skopeo delete --force docker://registry.lab.example.com/library/rsyslog:latest
# 方法2:curl 调 Registry API 删除
DIGEST=$(skopeo inspect docker://registry.lab.example.com/library/rsyslog:latest \
| python3 -c "import json,sys; print(json.load(sys.stdin)['Digest'])")
curl -X DELETE -k https://registry.lab.example.com/v2/library/rsyslog/manifests/$DIGEST
# 本地取消 tag
podman rmi -f <image>⚠️ 前提条件:Registry 需要开启 delete 功能(storage.delete.enabled: true),否则返回 405。
2.5 rm vs rmi(常考易混点)
| 命令 | 用途 | 删除对象 |
|---|---|---|
podman rm | 删除容器 | 运行中/已停止的容器 |
podman rmi | 删除镜像(i = image) | 镜像 |
podman rmi localhost/rsyslog:latest # 删除单个镜像
podman rmi --all # 删除所有镜像
podman images # 确认镜像已删除2.6 docker build 命令详解
命令:docker build -t web_game:v1 .
参数拆解:
| 部分 | 含义 |
|---|---|
docker build | 根据 Dockerfile 构建镜像 |
-t web_game:v1 | 打标签,格式 镜像名:版本(不写 tag 默认 :latest) |
. | 构建上下文路径(build context)= 当前目录 |
⚠️ 重要:. 不是单纯的"当前目录"
真实含义:把当前目录下的所有内容打包,发给 Docker daemon,作为构建上下文的根。
Dockerfile 里 COPY / ADD 的源路径都相对这个上下文根:
COPY . /app # 把上下文根下所有文件 → 容器 /app
COPY ./src /app/src # 上下文根/src → 容器 /app/src坑:如果当前目录有 .git、node_modules、.env,构建会:
- 慢(大文件全发给 daemon)
- 泄露(.env、密钥可能被打进镜像)
解决:在构建目录加 .dockerignore(语法同 .gitignore):
.git
node_modules
*.log
.env
target/
__pycache__/不打 -t 会怎样? 构建完只有临时镜像 ID(<none>:<none>),操作起来很麻烦。
常用参数:
| 参数 | 含义 |
|---|---|
-f / --file Dockerfile.dev | 指定 Dockerfile 路径(默认 ./Dockerfile) |
--build-arg KEY=*** | Build 参数(对应 Dockerfile 里 ARG) |
--no-cache | 不用构建缓存,强制全重编 |
--pull | 总是拉最新基础镜像(避免用本地过期缓存) |
--target dev-stage | 多阶段构建选阶段 |
-t name:v1 -t name:latest | 一次打多个标签 |
完整流程示例:
# 目录结构
.
├── Dockerfile
├── .dockerignore
├── app.py
└── requirements.txt
docker build -t web_game:v1 .
docker images | grep web_game
docker run -d -p 8080:8080 --name web web_game:v12.7 镜像与容器元数据(docker inspect)
2.7.1 docker inspect 基础
用途:查任何 docker 对象的详细元数据,返回 JSON。默认刷屏,配合 -f 用 Go template 取单字段才是日常姿势。
支持 inspect 的对象:
docker inspect nginx:1.27 # 镜像
docker inspect web # 容器
docker inspect bridge mynet # 网络
docker inspect myvol # 卷
docker inspect node1 # swarm 节点后接镜像:看的是「静态构建信息」
| 字段 | 含义 |
|---|---|
Id | 完整 SHA256 |
RepoTags | 所有 tag |
RepoDigests | 所有 digest(内容寻址) |
Created | 镜像构建时间(不是 pull 时间) |
Architecture / Os | 架构、系统 |
Size | 压缩后所有层总和(byte) |
RootFS.Layers | 每层 SHA256(构建顺序) |
Config | 镜像「运行配置」(建好就定了):Cmd、Entrypoint、Env、WorkingDir、ExposedPorts、Volumes、Labels |
后接容器:看的是「静态 + 运行时」
容器比镜像多了一堆运行时状态:
| 字段 | 含义 |
|---|---|
State.Status | running/exited/paused/restarting/dead |
State.Pid | 容器 PID 1 在宿主机的进程号(debug 用) |
State.ExitCode | 上次退出码(exited 时看) |
State.OOMKilled | 是否被 OOM 杀掉 |
Mounts | 所有挂载点(type/source/destination) |
NetworkSettings | IP、Gateway、Ports(端口映射) |
HostConfig | 启动时的资源/策略(内存/CPU/重启策略/日志驱动) |
LogPath | 日志文件路径 |
Env | 环境变量 |
核心:-f Go template 用法(重点记)
| 写法 | 含义 |
|---|---|
{{.Id}} | 取顶层字段 |
{{.State.Pid}} | 嵌套字段(点路径) |
{{json .Mounts}} | 转 JSON(接 jq 美滋滋) |
{{range .X}}{{.Y}}{{end}} | 遍历数组/切片 |
{{range .X}}{{println .}}{{end}} | 每行打一项 |
{{index .X "key"}} | map 取值 |
容器常用 -f 例子:
# 查容器 IP(最常用)
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
# 查主进程 PID
docker inspect -f '{{.State.Pid}}' web
# 查所有挂载点
docker inspect -f '{{json .Mounts}}' web | jq .
# 查退出码(排查为什么起不来)
docker inspect -f '{{.State.ExitCode}}' web
# 查 OOM
docker inspect -f '{{.State.OOMKilled}}' web
# 查端口映射
docker inspect -f '{{json .NetworkSettings.Ports}}' web | jq .镜像常用 -f 例子:
# 完整 SHA256
docker inspect -f '{{.Id}}' nginx:latest
# 默认 CMD
docker inspect -f '{{json .Config.Cmd}}' centos:7
# ENTRYPOINT
docker inspect -f '{{json .Config.Entrypoint}}' nginx:1.27
# 所有环境变量
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' nginx:1.27
# 所有 tag
docker inspect -f '{{json .RepoTags}}' nginx:1.27
# 所有层
docker inspect -f '{{range .RootFS.Layers}}{{println .}}{{end}}' nginx:1.27实战排查三连(背下来):
# 容器起不来三连:
docker ps -a | grep web # 1. 看在不在
docker logs --tail 50 web # 2. 看日志
docker inspect -f '状态={{.State.Status}} 退出码={{.State.ExitCode}} OOM={{.State.OOMKilled}}' web # 3. 看状态
# 端口映射没生效?
docker inspect -f '{{json .NetworkSettings.Ports}}' web | jq .
# 环境变量问题排查
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web2.7.2 GraphDriver.Data 消失问题(Docker 版本演进)
现象:老教程里 docker inspect centos:7 输出长这样:
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/xxx/merged",
"UpperDir": "/var/lib/docker/overlay2/xxx/diff", ← 可写层
"WorkDir": "/var/lib/docker/overlay2/xxx/work",
"LowerDir": "/var/lib/docker/overlay2/.../diff"
},
"Name": "overlay2"
}但新版只看到 RootFS,没有 GraphDriver.Data:
"RootFS": {
"Type": "layers",
"Layers": ["sha256:174f56854..."] ← 只有这一条
}原因:Docker 迁移到 containerd snapshotter
Docker 25.0 起开始迁移到 containerd snapshotter(新版镜像存储系统),到 27/28 版完成。
新版用 snapshotter 管理内容存储,不再暴露 overlay2 的文件系统路径。/var/lib/docker/overlay2/<id>/ 这种老路径可能整个没了。
老版本四件套含义:
| 字段 | 含义 |
|---|---|
UpperDir | 容器运行时产生的可写层 |
LowerDir | 镜像所有只读层的合并视图 |
MergedDir | Lower + Upper 联合挂载出来的虚拟合并盘 |
WorkDir | overlay2 内部工作目录(atomic rename 用) |
版本差异对比:
| 项 | 老版 overlay2 | 新版 snapshotter |
|---|---|---|
| 路径 | /var/lib/docker/overlay2/<id>/ | snapshotter 统一管理,不公开 |
| inspect | GraphDriver.Data 有四件套 | 只有 RootFS.Layers |
| 进 MergedDir 翻文件 | ✅ 可以 | ❌ 普通方法不行 |
| 启动速度 | 一般 | 略快(lazy pull、按需加载) |
验证自己用的是哪个版本:
docker version # client & server 版本
docker info | grep -i "storage driver" # 看 storage driver 关键!
ls /var/lib/docker/ # 新版可能没 overlay2/ 目录这影响什么?
直接进 MergedDir 翻文件这条老技巧死了
想看镜像内文件的新办法:
bash# 方法 1:临时容器看 docker run --rm -it centos:7 ls /etc/ # 方法 2:create + cp docker create --name tmp centos:7 docker cp tmp:/etc/passwd /tmp/passwd docker rm tmp # 方法 3:dive 工具(强烈推荐) dive centos:7别再按老教程查
/var/lib/docker/overlay2/——查不到
3. 容器操作
3.1 运行容器(podman run)
# 创建挂载目录
mkdir /home/sashat/syslog
# 运行容器(带特权模式和卷挂载)
podman run -d --privileged \
-v /home/sashat/syslog:/var/log:Z \
--name logserver \
registry.lab.example.com/library/rsyslog:latestpodman run 核心参数:
| 参数 | 含义 |
|---|---|
-d | 后台运行(detach) |
--privileged | 特权模式,允许容器访问宿主机设备 |
-v 宿主机:容器:Z | 挂载目录,:Z 为 SELinux 标签(必须加,不加 SELinux 会拒访) |
--name NAME | 给容器命名(不加会随机生成) |
-it | 交互模式 + 伪终端(用于 exec 进入容器) |
-p 主机端口:容器端口 | 端口映射 |
--rm | 容器退出后自动删除(不能和 -d 同时用) |
-e KEY=VALUE | 设置环境变量 |
3.1.1 docker run 详解
语法:docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
参数拆解(以 docker run -itd centos:7 为例):
| 参数 | 含义 |
|---|---|
-i(--interactive) | 保持 STDIN 打开,没附加终端也能往容器里输东西 |
-t(--tty) | 分配伪终端(pseudo-TTY),让容器以为自己在和 shell 对话(提示符、颜色等) |
-d(--detach) | 后台模式,启动后立刻把终端还给你,只输出容器 ID |
-itd 组合含义:后台启动一个交互式容器(不阻塞终端),但保留交互能力,给之后 docker exec -it 进去用。
典型场景:起一个后台运行的开发环境,需要时 docker exec -it <id> bash 进去操作(vim、mysql 客户端这种需要 TTY 的命令才好用)。
docker run -itd --name mydev centos:7
docker exec -it mydev bash⚠️ 踩坑提醒:centos:7 在 2024 EOL 后,国内大部分镜像站已清理该镜像。能用是有老缓存或特定源还有,新机器 pull 大概率失败。
docker run 常用参数大全:
| 类别 | 参数 | 含义 |
|---|---|---|
| 🏷️ 基础 | --name xxx | 给容器起名(不写随机) |
| 🏷️ 基础 | --rm | 容器退出后自动删除(一次性任务用) |
| 🏷️ 基础 | --hostname xxx | 容器内主机名 |
| 🏷️ 基础 | --restart no|on-failure[:max]|always|unless-stopped | 重启策略 |
| 🌐 网络 | -p / --publish 宿主机:容器[:协议] | 端口映射(如 -p 8080:80、-p 53:53/udp) |
| 🌐 网络 | -P / --publish-all | 自动映射所有 EXPOSE 端口到随机端口 |
| 🌐 网络 | --network bridge|host|none|container:xxx|mynet | 网络模式 |
| 🌐 网络 | --add-host=host:ip | 往 /etc/hosts 塞条目 |
| 🌐 网络 | --dns 8.8.8.8 | 指定 DNS |
| 💾 存储 | -v /host:/container | 路径挂载 |
| 💾 存储 | -v vol:/data | 具名卷挂载(推荐用于持久化数据) |
| 💾 存储 | --mount type=bind/tmpfs/volume,... | -v 的规范写法 |
| 💾 存储 | --tmpfs /path | 临时文件系统(数据存内存) |
| 💾 存储 | --read-only | 根文件系统只读 |
| ⚙️ 环境 | -e KEY=VALUE | 设置环境变量 |
| ⚙️ 环境 | --env-file .env | 从文件批量读环境变量 |
| ⚙️ 环境 | -w /app | 工作目录 |
| ⚙️ 环境 | -u 1000:1000 / -u root | 运行用户(避免用 root 是最便宜的安全加固) |
| ⚙️ 资源 | --cpus 1.5 | CPU 核数限制 |
| ⚙️ 资源 | -m 512m | 内存限制 |
| ⚙️ 资源 | --memory-swap -1 | swap 限制(-1 = 不限) |
| ⚙️ 资源 | --cpuset-cpus 0,1 | 绑死特定 CPU |
| 🔐 安全 | --privileged | 特权模式(慎用,几乎能调所有内核接口) |
| 🔐 安全 | --cap-add NET_ADMIN | 增加 capability |
| 🔐 安全 | --cap-drop ALL | 去掉所有 capability(最小权限) |
| 🔐 安全 | --security-opt seccomp=unconfined | seccomp/apparmor |
| 🔐 安全 | --user | 跑非 root 用户(首选安全措施) |
| 🪵 日志 | --log-driver json-file|syslog|fluentd | 日志驱动 |
| 🪵 日志 | --log-opt max-size=10m | 日志驱动参数(用于切割) |
| 🪵 日志 | --entrypoint /bin/sh | 覆盖镜像默认入口 |
实战模板:
# 起后台的交互式开发环境
docker run -itd --name dev -v /root/work:/work centos:7
# 一次性前台命令
docker run -it --rm centos:7 cat /etc/redhat-release
# 长期跑的服务(自启 + 资源限制)
docker run -d --name web --restart unless-stopped -p 80:80 -m 256m --cpus 1 nginx:latest
# 跨容器通信(自定义网络)
docker network create mynet
docker run -d --name db --network mynet -e MYSQL_ROOT_PASSWORD=123 mysql:8
docker run -d --name web --network mynet -p 8080:8080 myapp3.1.2 docker run 最后的 /usr/sbin/nginx 是啥?
完整语法:docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
镜像名之后的所有位置参数,都是给容器执行的进程(不是挂载、不是参数)。
docker run -d -p 80:80 --name my_nginx rocky_nginx:v1 /usr/sbin/nginx
# ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
# IMAGE COMMAND/usr/sbin/nginx 就是说容器起来后执行这个二进制,作为容器主进程(PID 1)。
为什么不写也能跑? 镜像 Dockerfile 通常定义了默认启动命令:
ENTRYPOINT ["/usr/sbin/nginx"] # ENTRYPOINT 形式
CMD ["/usr/sbin/nginx"] # CMD 形式什么时候要写命令行版本?
- 带自定义参数启动:
docker run rocky_nginx:v1 /usr/sbin/nginx -g "daemon off;" - 覆盖默认进程(调试):
docker run -it --rm rocky_nginx:v1 /bin/bash - 镜像没设 ENTRYPOINT/CMD 时,必须命令行给命令,否则起不来
ENTRYPOINT vs CMD vs 命令行:
- ENTRYPOINT:命令行参数作为它的追加参数(不容易覆盖)
- CMD:命令行参数整体替换它
- 都没设:必须命令行给命令
跟挂载点 -v 的区别:
# -v 是路径映射(宿主机 → 容器)
# 最后的 /usr/sbin/nginx 是启动命令
docker run -d \
-v /root/nginx.conf:/etc/nginx/nginx.conf \
--name web rocky_nginx:v1 /usr/sbin/nginx一个是文件映射,一个是进程启动,别混了。
3.2 进入容器执行命令
# 进入容器交互式 shell
podman exec -it logserver /bin/bash
# 在容器内执行 logger
logger "This is a rhcsa exam"
# 退出容器
exit验证:
grep rhcsa /home/sashat/syslog/messages # 看日志是否写到宿主机
skopeo inspect docker://registry.lab.example.com/library/rsyslog:latest # 查远程镜像信息3.2.1 docker exec 报错 bash: executable file not found(瘦身镜像坑)
报错:
$ docker exec -it pv-ac-sim bash
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH结论:是的,大概率是镜像里没 bash。 这是瘦身镜像(alpine、distroless)的常见坑。
三大类镜像对照:
| 镜像类型 | 有 bash? | 进入命令 |
|---|---|---|
ubuntu / debian / centos | ✅ | docker exec -it <容器> bash |
alpine / node:alpine | ❌ 只有 ash | docker exec -it <容器> sh |
gcr.io/distroless/* | ❌ 完全没 shell | 🚫 exec 进去不了 |
scratch | ❌ 空 | 🚫 exec 进去不了 |
快速判断镜像类型:
# 看镜像基础
docker inspect pv-ac-sim | grep -i 'image\|os'
# 看容器里有什么 shell
docker exec pv-ac-sim ls /bin/
# 验证是不是 distroless(连 sh 都没)
docker exec pv-ac-sim sh
# 如果也报 "executable file not found" → distroless,exec 废了三种解法:
① 改用 sh(最快)
docker exec -it pv-ac-sim sh② alpine 装 bash
docker exec -it pv-ac-sim apk add bash
docker exec -it pv-ac-sim bash⚠️ 装 bash 不会写回镜像,容器重启就丢。
③ 改 Dockerfile 用完整镜像(推荐永久)
# 从
FROM node:alpine
# 改成
FROM node:20-bookworm # debian 完整版distroless 镜像怎么 debug:
- 看应用日志:
docker logs <容器>/kubectl logs - sidecar + shareProcessNamespace:yamlexec 进带 shell 的 sidecar 看主容器进程
shareProcessNamespace: true - 临时换镜像:本地调试用
node:20-bookworm替代 distroless - 远程调试:Node.js 加
--inspect=0.0.0.0:9229,Chrome DevTools 连接
一句话速记: 瘦身镜像(alpine/distroless)没 bash,先用 docker exec -it <容器> sh 试一下;distroless 连 sh 都没有,exec 完全失效,只能靠日志和 sidecar。
3.3 端口映射 -p 主机端口:容器端口
命令示例:
docker run --name nginx_container_test -d -p 8080:80 nginx参数拆解:
| 部分 | 含义 |
|---|---|
--name nginx_container_test | 给容器起名字 |
-d | 后台运行 |
-p 8080:80 | 端口映射(8080 = 宿主机,80 = 容器) |
nginx | 用 nginx 镜像 |
核心原理:
宿主机(你的电脑) 容器(隔离的小盒子)
8080 ←————— 映射 —————→ 80
↑ ↑
外部访问入口 Nginx 内部监听端口- 8080 = 宿主机端口,外部能访问
- 80 = 容器内部端口,外部访问不到
- Docker 做中转:访问宿主机 8080 → 自动转到容器的 80
为什么不直接用 80? 容器是网络隔离的,容器内 80 端口外面访问不到,必须通过 -p 暴露。而且可以避免端口冲突(多容器都用 80,映射到主机不同端口)。
常用写法:
-p 8080:80 # 指定映射(最常用)
-p 127.0.0.1:8080:80 # 只允许本机访问(绑定指定 IP,更安全)
-P # 大写 P:随机映射端口(自动选空闲端口)查看实际映射:
docker port nginx_container_test
# 输出:80/tcp -> 0.0.0.0:8080实测访问: http://localhost:8080
3.3.1 端口映射 vs 服务监听端口(核心澄清)
常见误解: "docker run -p 8000:8000 已经规定了 8000,所以 python3 -m http.server 不用加端口号" ❌
真实情况: 两个 8000 是完全独立的,只是恰好一样:
-p 8000:8000前面的 8000 = 主机端口-p 8000:8000后面的 8000 = 容器端口http.server默认 8000 = 服务实际监听的端口
端口对应关系(三段链路):
主机端口 → Docker 转发 → 容器端口 → 服务监听端口三者必须一致才能通。
验证例子:
-p 9000:8000 + http.server✅ 主机9000 → 容器8000 → 服务8000(默认)-p 9000:7000 + http.server❌ 容器7000 没服务监听-p 9000:7000 + http.server 7000✅ 显式指定服务端口
建议写法: 显式写出端口号更清晰:
python3 -m http.server 8000虽然默认就是 8000,但显式写不容易出错。
一句话总结: 不加端口号能跑是因为 http.server 默认 8000,和 -p 前面的 8000 没关系。三个端口要一致:主机端口 → 容器端口 → 服务监听端口。
3.4 Docker 临时容器(--rm)
场景:用 Docker 运行一次性容器执行某个操作,容器自动删除。
# 生成 UUID(以 sing-box 代理工具为例)
docker run --rm ghcr.io/sagernet/sing-box:latest generate uuid| 参数 | 含义 |
|---|---|
--rm | 容器执行完后自动删除,不留垃圾 |
ghcr.io/sagernet/sing-box:latest | Docker 镜像地址(GitHub Container Registry) |
generate uuid | 容器内执行的命令 |
原理: Docker 拉取镜像 → 启动容器 → 执行命令 → 输出结果 → 容器自动销毁。
⚠️ --rm 和 -d 不能同时用:--rm 在容器退出时删除,-d 是后台运行(不会退出)。
3.5 docker ps 与 ps -qa 过滤
命令:docker ps -qa
参数拆解:
| 部分 | 含义 |
|---|---|
docker ps | 列出容器(默认只看 running 状态) |
-a(--all) | 显示所有状态的容器(running/exited/created/restarting 等) |
-q(--quiet) | 静默模式,只输出容器 ID,不显示容器名、镜像、状态、端口等列 |
效果:每行一个容器 ID,覆盖机器上所有容器。
典型用途(这个命令几乎都是配合管道/命令替换用):
# 停止所有容器
docker stop $(docker ps -qa)
# 删除所有容器(包括运行中的)
docker rm -f $(docker ps -qa)
# 用 xargs 更安全(参数为空时不会报错)
docker ps -qa | xargs -r docker rm -f
# 只删已停止的容器(更精准)
docker ps -qa --filter "status=exited" | xargs -r docker rm对比速记:
| 命令 | 含义 |
|---|---|
docker ps | 运行中 + 完整信息 |
docker ps -q | 运行中 + 只 ID |
docker ps -a | 所有 + 完整信息 |
docker ps -qa | 所有 + 只 ID |
常用 --filter 过滤:
docker ps --filter "status=running/exited/paused" # 按状态过滤
docker ps --filter "name=web" # 按名称
docker ps --filter "label=env=dev" # 按 label(最推荐批量方案)3.6 容器管理全生命周期(含批量操作)
3.6.1 查看
| 命令 | 含义 |
|---|---|
docker ps | 运行中 |
docker ps -a | 全部(含已停止) |
docker ps -qa | 只显示 ID(配合批量用) |
docker ps --filter "status=..." | 按状态/名称/label 过滤 |
docker stats | 实时资源(CPU/内存/网速) |
docker top <id> | 容器内进程 |
docker port <id> | 端口映射 |
3.6.2 启动 / 重启
| 命令 | 含义 |
|---|---|
docker run -d --name web nginx | 创建 + 启动(一次性) |
docker start <id> | 启动已 stop 的 |
docker restart <id> | 重启(stop + start) |
docker restart -t 30 <id> | 给 30s 缓冲 |
⚠️ run vs start vs create:run=创建+启动、start=只启动已有、create=只创建不启动。
3.6.3 停止 / 关闭
| 命令 | 含义 |
|---|---|
docker stop <id> | 发 SIGTERM → 等 10s → SIGKILL(优雅停) |
docker stop -t 30 <id> | 自定义缓冲 |
docker kill <id> | 强制 SIGKILL(应用卡死时才用) |
docker pause <id> / unpause <id> | 冻结(cgroup freezer,内存保留,类似休眠) |
⚠️ stop vs kill:stop 给应用清理缓冲时间;kill 暴力用。
3.6.4 删除
| 命令 | 含义 |
|---|---|
docker rm <id> | 删已停止的 |
docker rm -f <id> | 强删(含运行中等价 stop+rm) |
docker rm -f <id1> <id2> | 一次删多个 |
docker container prune -f | 清所有已停止 |
3.6.5 进入容器 / 文件系统操作
| 命令 | 含义 |
|---|---|
docker exec -it <id> bash | 走进运行中容器(最常用) |
docker exec -it <id> sh | 镜像无 bash 时(alpine) |
docker attach <id> | 附加主进程 stdin/stdout(一般不用,会冲突) |
docker cp 主机路径 <id>:容器路径 | 双向复制 |
docker export <id> > foo.tar | 导出文件系统(丢历史+元数据) |
docker import foo.tar myimage:latest | import 回来(变镜像一层) |
3.6.6 日志 & 资源
docker logs <id>/-f:看 / 跟踪docker logs --tail 100/--since 10m:最近 100 行 / 最近 10 分钟docker logs -t:带时间戳
3.6.7 批量操作(重点)
方案 1:直接传多个 ID
docker stop $(docker ps -qa)
docker rm -f $(docker ps -qa)方案 2:先过滤再操作(精准)
docker stop $(docker ps -qa --filter "status=exited") # 只停已退出的
docker stop $(docker ps -qa --filter "name=web_") # 只停 name 前缀匹配
docker run -d --label env=dev nginx # 启动打 label
docker stop $(docker ps -qa --filter "label=env=dev") # 按 label 批量方案 3:xargs 写法(更稳健)
$(docker ps -qa)在没容器时会变docker stop→ 新版报错,老版本试图 stop 全部镜像xargs -r:输入为空就不执行
docker ps -qa | xargs -r docker stop
docker ps -qa | xargs -r docker rm -f
docker ps -qa | xargs -I{} docker inspect -f '{{.State}}' {} # -I{} 引用每行 ID方案 4:prune 系列
| 命令 | 含义 |
|---|---|
docker container prune -f | 清已停止容器 |
docker image prune -f | 清悬空镜像(<none>) |
docker network prune -f | 清无用网络 |
docker volume prune -f | 清无用卷 ⚠️ 数据丢失 |
docker system prune -f | 上面四件套全清(除 volume) |
docker system prune -af --volumes | 连 volume 也清 ⚠️⚠️⚠️ |
⚠️ docker system prune --volumes 会删带数据的卷,执行前必先 docker volume ls。
方案 5:高级批量
- docker-compose:编排多容器(up/down/restart)
- docker swarm:集群(service update)
适合多容器应用,单机用方案 1~4。
3.6.8 实战速查(背下来)
# 清场:全部停 + 全部删
docker stop $(docker ps -qa) ; docker rm -f $(docker ps -qa)
# 批量清已退出的(保留运行中)
docker container prune -f
# 按 label 停一批
docker stop $(docker ps -qa --filter "label=project=blog")
# 按日期清理停超 24h 的
docker ps -qa --filter "status=exited" --filter "since=24h" | xargs -r docker rm
# 全栈清理(含构建缓存)
docker system prune -af3.7 容器进入与分离(exit vs Ctrl+P+Q)
问题: docker run -it ubuntu 进入容器后,直接 exit 或 Ctrl+D 会让容器停止(因为 bash 是 PID 1)。
三种保持容器运行的退出方式:
| 方式 | 操作 | 说明 |
|---|---|---|
| Ctrl + P + Q(推荐) | 按住 Ctrl,先按 P 再按 Q,顺序不能错 | Docker 设计的"分离"快捷键 |
| docker detach | 另开终端 docker detach <容器名或ID> | 外部 detach 命令 |
| 启动时避免 PID 1 问题 | docker run -itd ubuntu + docker exec -it bash | exec 进入,bash 不是 PID 1 |
关键区分:
exit/Ctrl+D→ 退出 bash → 容器停止 ❌Ctrl+P+Q/docker detach→ 分离终端 → 容器继续运行 ✅
退出后重新进入:
| 方式 | 说明 |
|---|---|
docker attach <容器> | 重连 PID 1 进程 |
docker exec -it <容器> bash | 开新进程(推荐) |
3.8 容器资源限制(-m / --cpus / --cpu-shares)
内存限制(硬限制)
docker run -m 256m tylersmith22/docker-stress-ng --vm 2 --vm-bytes 256m-m 256m:Docker 给容器的硬上限(256MB)--vm 2 × --vm-bytes 256m= 容器内想要 512MB- 结果:OOM 被 kill(想用 > 限制)
内存是硬限制:设多少就是多少,超了直接 OOM kill。
CPU 限制三剑客
| 参数 | 类型 | 作用 |
|---|---|---|
--cpus N | 硬限制 | 最多用 N 个核心 |
--cpuset-cpus N | 硬限制 | 绑定到指定核心 |
--cpu-shares N | 软限制 | 权重(抢资源时按比例分配) |
CPU 限制对比示例:
# test1: cpu-shares=1024
docker run -d --cpus 1 --cpuset-cpus 0 --cpu-shares 1024 --name test1 ... --cpu 1
# test2: cpu-shares=2048(test1 的两倍)
docker run -d --cpus 1 --cpuset-cpus 0 --cpu-shares 2048 --name test2 ... --cpu 1- 两个都被限制在 CPU 0,最多 1 核
- 抢资源时 test2 拿两倍时间片
docker stats显示 test1 ~33%,test2 ~66%
关键理解:
- 内存:硬限制(设多少就是多少,超了 OOM kill)
- CPU:硬限制(
--cpus/--cpuset-cpus)+ 软限制(--cpu-shares权重) cpu-shares只在抢资源时才按比例生效
一句话总结: 内存限制是硬上限(-m),CPU 限制有绝对(--cpus)和相对(--cpu-shares)两种。
4. 容器网络
4.1 容器内部 DNS(127.0.0.11)
问题: OpenVPN Docker 容器推送 DNS 127.0.0.11 给客户端,客户端解析失败。
原因:
- Docker 容器内部有内置 DNS 解析器,监听
127.0.0.11 - 这个地址只在容器内有效,宿主机和外部客户端都不存在
- 客户端拿到这个 DNS 配置后无法解析域名
解决:
# 修改容器 /etc/resolv.conf 为公共 DNS
docker exec openvpn sh -c "echo 'nameserver 8.8.8.8' > /etc/resolv.conf"
# OpenVPN AS 推送 DNS 设置(用 sacli 工具)
sacli ConfigPut "vpn.client.routing.dns.0/1" "8.8.8.8,1.1.1.1"
sacli Start4.2 容器 NAT(MASQUERADE)
问题: VPN 客户端连接后能访问部分网站,但部分网站访问不了。
原因: VPN 子网 172.27.0.0/16 没做 NAT,包出去了回不来。
解决:
# 容器内添加 iptables NAT
iptables -t nat -A POSTROUTING -s 172.27.0.0/16 -o eth0 -j MASQUERADE
# 持久化:修改 entrypoint 脚本,使重启后规则还在⚠️ 容器重启后 iptables 规则会丢失,必须修改 entrypoint 让规则持久化。
4.3 Docker --link 与容器间通信
4.3.1 --link 详细解读
格式: --link 源容器:别名
docker run ... --link web01:web01 --link web02:web02 --link web03:web03| 位置 | 含义 | 是否必填 |
|---|---|---|
左 web01 | 被链接的源容器名(已存在的容器) | 必填 |
右 web01 | 在新容器里用什么名字访问它 | 可省,省略则等于左边 |
省略右边时,默认就用源容器名。
格式:--link 源容器:别名
docker run ... --link web01:web01| 位置 | 含义 | 是不是一定要写 |
|---|---|---|
左 web01 | 被链接的源容器名(已存在的容器) | 必填 |
右 web01 | 在新容器里用什么名字访问它 | 可省,省略就等于左边 |
省略右边等价:
--link web01 # 等价于 --link web01:web01
--link web01:myweb # lb01 里访问要写 myweb
--link web01:web01.backend.local # 用子域名访问Docker 在背后干了什么
在 lb01 容器里 cat /etc/hosts,会看到(Docker 自动注入):
172.17.0.2 web01
172.17.0.3 web02
172.17.0.4 web03左边是源容器在 default bridge 网络的 IP。
本质: --link 就是往新容器的 /etc/hosts 里塞几条解析记录。
附带效果(一般用不上):
- 注入一堆环境变量,如
WEB01_PORT_80_TCP=tcp://172.17.0.2:80 - 老容器可以看到新容器反向连过来(默认单向,只有 link 发起方能连源容器)
--link 的三大缺点
- 只能在 default bridge 网络用:其他网络(host / none / 自定义)直接失效
- Docker 官方已废弃:未来版本会移除
- 启动顺序敏感:lb01 启动时,web01/02/03 必须已经存在
Docker 自定义网络(替代 --link)
第 1 步:建网络
docker network create lnmp-net任意名字都可。这是一个虚拟网桥,凡挂上来的容器互通。
docker network ls
# 能看到:
# NETWORK ID NAME DRIVER SCOPE
# abc123... lnmp-net bridge local第 2 步:所有容器接进来
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03connect 可以把运行中的容器插到网络上,不需要重启。
第 3 步:启动 lb01,挂网络
docker run -d --restart=always \
-p 80:80 \
--name lb01 \
-v `pwd`/lb01:/etc/nginx \
--network lnmp-net \
nginx:latest背后原理
自定义网络里 Docker 自带一个内置 DNS 服务器(通常 127.0.0.11)。
所有接入容器的 /etc/resolv.conf 自动注入:
nameserver 127.0.0.11lb01 nginx 访问 web01 时:
- 查
/etc/resolv.conf→127.0.0.11 - Docker DNS 返回 web01 在 lnmp-net 上的 IP(如
172.18.0.3) - 请求发过去
验证:
docker exec -it lb01 bash
cat /etc/resolv.conf
# nameserver 127.0.0.11 说明 DNS OK
getent hosts web01
# 输出:172.18.0.3 web01
ping -c 1 web01两种方式对比
| 维度 | --link | 自定义网络 |
|---|---|---|
| 实现原理 | 改 /etc/hosts(硬塞 IP 解析) | Docker 内置 DNS(动态解析) |
| 容器必须先存在? | 是(启动顺序敏感) | 否 |
| 能跨自定义网桥? | 否,只能 default bridge | 是 |
| 重启后解析还在? | 在(IP 可能变,要重建) | 在(DNS 自动跟上) |
| 支持别名? | web01:别的名字 | --network-alias 参数 |
| Docker 推荐? | ❌ 已废弃 | ✅ 推荐 |
网络别名(替代 --link 别名)
# 给 web01 多挂一个别名
docker network connect --alias api.local lnmp-net web01
# 启动时挂别名
docker run --network lnmp-net --network-alias api.local nginx完整的从 --link 迁移到自定义网络
# 1. 停 lb01(web01-03 保持运行)
docker stop lb01
# 2. 删 lb01(--link 配置在容器里,重建最干净)
docker rm lb01
# 3. 建网络
docker network create lnmp-net
# 4. web01/02/03 加入新网络(不需要停)
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03
# 5. 重启 lb01,挂自定义网络
docker run -d --restart=always \
-p 80:80 \
--name lb01 \
-v `pwd`/lb01:/etc/nginx \
--network lnmp-net \
nginx:latest
# 6. 验证
docker exec -it lb01 getent hosts web01
curl http://localhost跑完这几步,原来的 WARNING 就再也不会出现。
4.3.2 --link "容器外:容器里"的本质
一句话本质:
--link= 启动新容器时,往新容器内部的/etc/hosts里塞一行 IP+名字。
| 位置 | 真实含义 | 在哪生效 |
|---|---|---|
左 web01(容器外) | Docker daemon 这边告诉 docker:「我要链接源容器 web01」 | dockerd 这边处理 |
右 web01(容器里) | 在新容器内部 /etc/hosts 里写下的别名 | 新容器内 |
容器外并不是物理意义"宿主机",而是 docker daemon 视角的源容器侧;右边才是新容器内解析时看到的名字。
一句话本质
--link = 启动新容器时,往新容器内部的 /etc/hosts 里塞一行 IP+名字。
拆开左右两半
| 位置 | 真实含义 | 在哪生效 |
|---|---|---|
左 web01(容器外) | docker daemon 找到叫 web01 的容器,取它的 IP | 宿主机/Docker CLI 侧 |
右 web01(容器里) | 把那个 IP + 这个名字写进新容器的 /etc/hosts | 新容器内部 |
两个动作:
- 外侧:找到源容器(源容器必须先存在)
- 内侧:写 /etc/hosts
左右可以不一样:
--link web01:phpbackend
# 进 lb01 后:cat /etc/hosts 会看到 "172.17.0.2 phpbackend"(没有 web01 这一行)这台架构每个容器内的 /etc/hosts 长啥样
lb02 启动后
docker exec lb02 cat /etc/hosts127.0.0.1 localhost
172.17.0.5 php-fpm01
172.17.0.6 php-fpm02web01 启动后(web02/03 一样)
docker exec web01 cat /etc/hosts127.0.0.1 localhost
172.17.0.4 lb02lb01 启动后
docker exec lb01 cat /etc/hosts127.0.0.1 localhost
172.17.0.2 web01
172.17.0.3 web02
172.17.0.4 web03为啥这台架构所有东西都写主机名而不是 IP
容器 IP 是动态的:
docker rm -f web01
docker run ... --name web01 ... # 新容器 IP 跟之前可能不一样- 写 IP(如
172.17.0.2)→ 重启后失效 - 写名字(如
web01)→ nginx/OS 每次查 /etc/hosts 总能找到当下正确 IP
所有负载均衡配置用主机名是 Docker 场景的标配写法(不管 --link / compose / k8s)。
一次完整请求,名字解析在哪台机器发生
curl http://localhost/index.php:
【客户端/宿主机】
curl http://localhost
→ "localhost" 在宿主机 /etc/hosts 解析到 127.0.0.1
【LB01 容器内】
nginx 收到请求 → proxy_pass http://web01;
→ OS 解析 "web01" → 查 lb01 内的 /etc/hosts → 命中 172.17.0.2
→ TCP 发到 172.17.0.2:80
【WEB01 容器内】
web01 nginx 收到 → proxy_pass http://lb02;
→ OS 解析 "lb02" → 查 web01 内的 /etc/hosts → 命中 172.17.0.4
→ TCP 发到 172.17.0.4:80
【LB02 容器内】
lb02 nginx 收到 → proxy_pass http://php-fpm01:9000;
→ OS 解析 "php-fpm01" → 查 lb02 内的 /etc/hosts → 命中 172.17.0.5
→ TCP 发到 172.17.0.5:9000
【PHP-FPM01 容器内】
PHP-FPM 处理 index.php,返结果每跳都是"新容器内的 nginx"查"新容器内的 /etc/hosts"。 不在宿主机查,不在原容器查,每台容器独立解决自己的 DNS。
--link vs 自定义网络 解析路径对比
--link(静态 /etc/hosts)
nginx → OS getaddrinfo("web01") → 读 /etc/hosts → 拿 IP → connect自定义网络(动态 DNS)
nginx → OS getaddrinfo("web01") → /etc/resolv.conf → 127.0.0.11
↓
Docker 内置 DNS 实时查 daemon → 拿 IP → 返回关键差别:
/etc/hosts静态文件,启动时写啥就是啥,容器重启 IP 变了不更新 → 错的解析- Docker DNS 实时查 daemon,重启后下次自动用新 IP → 永远正确
--link 弃用的本质:用的是静态文件,不是动态 DNS。
两边写不一样会变成啥样
# 例:web01 启动时改成 --link lb02:phpapp
docker run ... --link lb02:phpapp --name web01 ...那 web01 内部 /etc/hosts:
172.17.0.4 phpapp ← 改名了web01 的 nginx 配置也得跟着改:
proxy_pass http://phpapp; # 不再是 lb02应用层(nginx 配置)用的名字 = link 时写的右半边。内外要一致。
你这套架构全用同名写法(外叫啥内也叫啥),本质是"省心"。
一句话总结
--link 容器外:容器里 整个链路逻辑:
"去外侧把
源容器找到,把它当前的 IP 拿到,把它和容器内这个名字绑成一条解析,塞进新容器的 /etc/hosts 里。"
"都是主机名" = 容器名当 hostname 用,省得 IP 写死,nginx 配置就写容器名,运行时 OS 查 /etc/hosts 拿当下 IP。
nginx 配置对应写法的猜测(你这套架构)
lb01 里的 nginx 配置(应该是 http 块或 stream 块)
upstream web_cluster {
server web01:80;
server web02:80;
server web03:80;
}
server {
listen 80;
proxy_pass web_cluster;
}web01/02/03 里的 nginx 配置(每个容器都一样的)
location / {
proxy_pass http://lb02;
}lb02 里的 nginx 配置
upstream php_backend {
server php-fpm01:9000;
server php-fpm02:9000;
}
location ~ \.php$ {
proxy_pass http://php_backend;
}每一处的"目标名字"(web01、lb02、php-fpm01)都必须是 link 时绑进本机 /etc/hosts 的那个名字。
4.3.3 Docker 自定义网络(替代 --link 的现代方法)
⚠️ --link 在默认 bridge 网络上已弃用(会有 WARNING),必须用自定义网络替代。
第 1 步:建网络
docker network create lnmp-net任意名字都可。这是一个虚拟网桥,凡挂上来的容器互通。
docker network ls
# 能看到:
# NETWORK ID NAME DRIVER SCOPE
# abc123... lnmp-net bridge local第 2 步:所有容器接进来
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03connect 可以把运行中的容器插到网络上,不需要重启。
第 3 步:启动 lb01,挂网络
docker run -d --restart=always \
-p 80:80 \
--name lb01 \
-v `pwd`/lb01:/etc/nginx \
--network lnmp-net \
nginx:latest背后原理
自定义网络里 Docker 自带一个内置 DNS 服务器(通常 127.0.0.11)。
所有接入容器的 /etc/resolv.conf 自动注入:
nameserver 127.0.0.11lb01 nginx 访问 web01 时:
- 查
/etc/resolv.conf→127.0.0.11 - Docker DNS 返回 web01 在 lnmp-net 上的 IP(如
172.18.0.3) - 请求发过去
验证:
docker exec -it lb01 bash
cat /etc/resolv.conf
# nameserver 127.0.0.11 说明 DNS OK
getent hosts web01
# 输出:172.18.0.3 web01
ping -c 1 web01两种方式对比
| 维度 | --link | 自定义网络 |
|---|---|---|
| 实现原理 | 改 /etc/hosts(硬塞 IP 解析) | Docker 内置 DNS(动态解析) |
| 容器必须先存在? | 是(启动顺序敏感) | 否 |
| 能跨自定义网桥? | 否,只能 default bridge | 是 |
| 重启后解析还在? | 在(IP 可能变,要重建) | 在(DNS 自动跟上) |
| 支持别名? | web01:别的名字 | --network-alias 参数 |
| Docker 推荐? | ❌ 已废弃 | ✅ 推荐 |
网络别名(替代 --link 别名)
# 给 web01 多挂一个别名
docker network connect --alias api.local lnmp-net web01
# 启动时挂别名
docker run --network lnmp-net --network-alias api.local nginx完整的迁移步骤:
# 1. 停 lb01(web01-03 保持运行)
docker stop lb01
# 2. 删 lb01(--link 配置在容器里,重建最干净)
docker rm lb01
# 3. 建网络
docker network create lnmp-net
# 4. web01/02/03 加入新网络(不需要停)
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03
# 5. 重启 lb01,挂自定义网络
docker run -d --restart=always \
-p 80:80 \
--name lb01 \
-v `pwd`/lb01:/etc/nginx \
--network lnmp-net \
nginx:latest
# 6. 验证
docker exec -it lb01 getent hosts web01
curl http://localhost跑完这几步,原来的 WARNING 就再也不会出现。
4.3.4 多层 nginx + --link 架构解读(run.sh 拆解)
整体架构
Client 请求
↓ :80
┌─────── LB01 ───────┐ ← 总入口(stream / 反代)
↓ ↓ ↓
web01 web02 web03 ← 三台 web(nginx 容器)
↓ ↓ ↓ ← 各自 nginx 反代给 lb02
┌──── LB02 ────┐ ← 第二层负载均衡
↓ ↓
php-fpm01 php-fpm02 ← 真正的 PHP 处理一共 3 层 nginx 反代:
client → LB01 → web01/02/03 → LB02 → php-fpm逐个 --link 解读
1. lb02 link php-fpm01/02 ✅
docker run -d --name lb02 ...
--link php-fpm01:php-fpm01
--link php-fpm02:php-fpm02- 作用:让 lb02 容器里的 nginx 解析
php-fpm01:9000/php-fpm02:9000 - 性质:正向——lb02 是反代目标,要转发到上游(php-fpm)
- 依赖:php-fpm 必须先启动
对应 lb02 里的 nginx 配置(推测):
upstream php_backend {
server php-fpm01:9000;
server php-fpm02:9000;
}
location ~ \.php$ {
proxy_pass http://php_backend;
}2. web01/02/03 link lb02 ⚠️(看架构)
docker run -d --name web01 ...
--link lb02:lb02- 作用:让 web01-03 解析
lb02 - 性质:web01-03 内的 nginx 把请求再反代给 lb02 处理 PHP
- 依赖:lb02 必须先启动
3. lb01 link web01/02/03 ✅
docker run -d --name lb01 ...
--link web01:web01
--link web02:web02
--link web03:web03- 作用:让 lb01 解析三台 web
- 性质:正向——总入口转轮询到后端
- 依赖:web01/02/03 必须先启动
总结:必须的 link
| 容器 | link 目标 | 必要? | 说明 |
|---|---|---|---|
| lb02 | php-fpm01/02 | ✅ 必须 | 反代目标,靠 DNS 解析 |
| web01/02/03 | lb02 | ⚠️ 看你架构 | 如果 web 直接处理 PHP 就可省 |
| lb01 | web01/02/03 | ✅ 必须 | 反代目标,三台要轮询 |
怪点 1:架构绕弯路
有 3 层 nginx 反代,web01/02/03 自己也是 nginx 容器,但自己不处理 PHP,反而把所有请求甩给 LB02,再分给两个 php-fpm。
web01/02/03 里那个 nginx 完全是传话筒,啥也没干。
生产常见做法:
- 简化 A:web01/02/03 每台是完整的 nginx + php-fpm 一体(LEMP),自己处理 PHP,根本不需要 LB02 这一层
- 简化 B:web01/02/03 只装 nginx 起静态或反代到 Apache/PHP-FPM 一体,不会再反代到 LB02 这种"中间再 LB 一次"
这套脚本很像练手用——把三层负载全部分离,每层独立容器,能看清每层干嘛。 生产里叫"过度架构",实验场景直观,生产去掉中间层。
怪点 2:--link 启动顺序被强绑死
php-fpm01/02 ──先起──┐
↓ 必须存在
lb02 ──先起──┐
↓ 必须存在
web01/02/03 ──先起──┐
↓ 必须存在
lb01整个 run.sh 严格串行,靠 sleep 3 凑合缓冲。某一台启动慢,后面全挂。
自定义网络方案:
docker network create lnmp-net
# 任意顺序启动都行
docker run --network lnmp-net php-fpm01 ...
docker run --network lnmp-net lb02 ...
docker run --network lnmp-net web01 ...
docker run --network lnmp-net lb01 ...启动顺序完全无关。
怪点 3:docker rm -f 断 link 关系
docker rm -f web01 # ← link 关系一起销毁
docker run -d --link lb02:lb02 --name web01 ...容器本身不持久化 link,每条 run 命令都要带。
正经做法:写 docker-compose.yml:
version: '3'
services:
php-fpm01:
image: php:7.4-fpm
volumes:
- ./blog/www:/usr/share/nginx/html
lb02:
image: nginx:latest
volumes:
- ./lb02:/etc/nginx
depends_on:
- php-fpm01
- php-fpm02
web01:
image: nginx:latest
volumes:
- ./blog/nginx:/etc/nginx
- ./blog/www:/usr/share/nginx/html
depends_on:
- lb02
lb01:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./lb01:/etc/nginx
depends_on:
- web01
- web02
- web03
networks:
default:
name: lnmp-netdepends_on 表达依赖顺序,声明式而不是命令式。 docker compose up -d 一键起。
| 维度 | --link | 自定义网络 |
|---|---|---|
| 实现原理 | 改 /etc/hosts(硬塞 IP 解析) | Docker 内置 DNS(动态解析) |
| 容器必须先存在? | 是(启动顺序敏感) | 否 |
| 能跨自定义网桥? | 否,只能 default bridge | 是 |
| Docker 推荐? | ❌ 已废弃 | ✅ 推荐 |
完整迁移命令:
docker stop lb01
docker rm lb01
docker network create lnmp-net
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03
docker network connect lnmp-net lb02
docker network connect lnmp-net php-fpm01
docker network connect lnmp-net php-fpm02
docker run -d --restart=always \
-p 80:80 \
--name lb01 \
-v `pwd`/lb01:/etc/nginx \
--network lnmp-net \
nginx:latest💡 一句话:
--link在新容器里塞 hosts 解析;自定义网络用 docker 内置 DNS 解析(127.0.0.11)。前者是早期方案已弃用,后者是现代标准做法。
4.4 在宿主机(容器外)查容器 IP 的几种方法
1. docker inspect --format(最实用)
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web01
# 输出:172.17.0.2--format 是 Go template,把字段抠出来。
多个容器一起查:
docker inspect \
--format='{{.Name}} -> {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
web01 web02 web03
# /web01 -> 172.17.0.2
# /web02 -> 172.17.0.3
# /web03 -> 172.17.0.42. 完整 docker inspect(信息巨多)
docker inspect web01
# 一坨 JSON:IP、挂载、端口、网络、环境变量……太长的话 grep:
docker inspect web01 | grep -i ipaddress3. 自定义网络的 IP(多网络容器必看)
容器挂多个网络时,每个网络一个 IP,必须带网络名过滤:
# 容器同时挂 default bridge 和 lnmp-net
docker inspect --format='{{.NetworkSettings.Networks.lnmp_net.IPAddress}}' web01⚠️ 坑: lnmp-net 在 Go template 里点 . 写不了,自动替换成下划线(lnmp-net → lnmp_net)。
4. docker network inspect(从网络视角看所有容器)
docker network inspect bridge # default bridge
docker network inspect lnmp-net # 自定义网络输出 JSON 里有 Containers 字段,每个容器对应一个 IP。批量看最合适。
5. 一行列出所有容器的 IP + 状态
docker inspect \
--format='{{.Name}} | {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} | {{.State.Status}}' \
$(docker ps -aq)
# 输出:
# /php-fpm01 | 172.17.0.5 | running
# /php-fpm02 | 172.17.0.6 | running
# /lb02 | 172.17.0.7 | running
# /web01 | 172.17.0.2 | running
# /web02 | 172.17.0.3 | running
# /web03 | 172.17.0.4 | running
# /lb01 | 172.17.0.8 | runningdocker ps -aq 取所有容器 ID(包括停的)。
6. 进容器内部看(验证 --link)
docker exec lb01 cat /etc/hosts
# 这就是 --link 注入到容器内的解析表
# 能看到 Docker 把 link 源容器的 IP 写进去总结:常用的就这 3 个
| 命令 | 用途 |
|---|---|
docker inspect --format '...' <容器> | 最常用,精确抠字段 |
docker network inspect <网络> | 看某网络上所有容器的 IP |
docker exec <容器> cat /etc/hosts | 进容器看 --link 注入表 |
万能模板(多网络、要看状态都行):
docker inspect --format='{{.Name}}: {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}-{{.State.Status}}' <容器>5. 容器持久化与开机自启
5.1 为什么需要先运行临时容器?
podman generate systemd 需要读取已存在的容器配置来生成 .service 文件。
正确流程:
run 临时容器(配好所有参数) → generate systemd 生成服务文件 → 删除临时容器 → systemd 接管5.2 完整操作流程(rootless 容器)
# 1. 准备挂载目录(root)
mkdir /opt/txt.pdf
chown sashat:sashat /opt/txt.pdf
# 2. 切换到用户
ssh sashat@servera
# 3. 运行临时容器
podman run -d --name txt2pdf \
-v /opt/txt/:/opt/txt:Z \
-v /opt/pdf:/opt/pdf:Z \
localhost/modify_file:latest
# 4. 生成 systemd 服务文件
mkdir -p ~/.config/systemd/user
cd ~/.config/systemd/user
podman generate systemd txt2pdf --files --name new
# 5. 启用开机自启
systemctl --user daemon-reload
systemctl --user enable container-txt2pdf.service
# 6. 清理临时容器并重启服务
podman rm -f txt2pdf
systemctl --user restart container-txt2pdf.service
# 7. 验证
podman ps
echo "hello rhcsa" > /opt/txt/rhcsa.txt
ls /opt/pdf/podman generate systemd 关键参数:
| 参数 | 含义 |
|---|---|
--files | 生成 .service 文件 |
--name | 使用容器名命名服务 |
--new | 每次启动创建新容器(推荐,避免容器已存在的错误) |
5.3 Rootless 容器要点
Rootless 容器: 普通用户(非 root)运行的容器
| 项目 | 路径/命令 |
|---|---|
| systemd 服务文件位置 | ~/.config/systemd/user/container-xxx.service |
| 服务管理命令 | systemctl --user <动作> container-xxx.service |
| 服务启动用户 | 当前用户 |
| 用户退出后保留 | 需要 loginctl enable-linger 用户名 |
Lingering(持久化):
loginctl enable-linger sashat
# 确保用户退出后(SSH 断开)服务仍在运行⚠️ Rootless 容器必须用用户级 systemd(--user),不能用系统级(会报权限错误)。
6. 容器实战案例
6.1 OpenVPN Docker 容器故障排查
故障现象: 手机通过 OpenVPN 连接后能访问 GitHub,但 Google/YouTube 无法访问。
根本原因:
| 故障点 | 原因 |
|---|---|
| DNS 配置错误 | OpenVPN AS 推送的 DNS 是 127.0.0.11(Docker 内部 DNS),手机上不存在 |
| NAT 缺失 | VPN 子网 172.27.0.0/16 没做 MASQUERADE,包出去了回不来 |
完整解决步骤:
# 1. 修改容器 DNS 为公共 DNS
docker exec openvpn sh -c "echo 'nameserver 8.8.8.8\nnameserver 1.1.1.1' > /etc/resolv.conf"
# 2. OpenVPN AS 推送 DNS 配置(用 sacli 写入数据库)
sacli ConfigPut "vpn.client.routing.dns.0/1" "8.8.8.8,1.1.1.1"
# 3. 容器内添加 iptables NAT
docker exec openvpn iptables -t nat -A POSTROUTING -s 172.27.0.0/16 -o eth0 -j MASQUERADE
# 4. 修改 entrypoint 脚本,让修复持久化(容器重启后规则不丢)关键知识点:
- Docker 容器
127.0.0.11是内部 DNS 解析器,只在容器内有效 - OpenVPN AS 配置要用
sacli工具写入数据库,直接改 config-local.json 不生效 sacli ConfigPut→ 写入配置;sacli Start/Stop→ 重启服务- VPN 子网需要 NAT 才能访问外网,容器重启后 iptables 规则会丢失
6.2 Nginx 测试容器(一行启动)
# 启动一个临时 Nginx 容器,端口映射到 8080
docker run --name nginx_container_test -d -p 8080:80 nginx
# 浏览器访问测试
# http://localhost:8080 → 看到 Nginx 欢迎页
# 查看容器日志
docker logs nginx_container_test
# 进入容器调试
docker exec -it nginx_container_test /bin/bash
# 停止并删除
docker stop nginx_container_test
docker rm nginx_container_test6.3 LNMP 容器化项目:挂载点的关键逻辑
核心思想
让容器本身"无状态",把配置、代码、数据全部从容器内部拎出来放到宿主机。
6.3.1 挂载点对照表
挂载点对照表
| 容器 | 宿主机路径 | 容器内路径 | 挂的是啥 |
|---|---|---|---|
| redis | ./redis/config | /usr/local/etc/redis | redis.conf 配置 |
| redis | ./redis/data | /data | RDB/AOF 持久化数据 |
| mysql | ./mysql/config | /etc/mysql/conf.d | 覆盖式配置(追加生效) |
| mysql | ./mysql/data | /var/lib/mysql | 库、表、binlog |
| php-fpm01/02 | ./blog/www | /usr/share/nginx/html | PHP 应用代码 |
| php-fpm01/02 | ./blog/php | /usr/local/etc | php.ini + pool 配置 |
| web01/02/03 | ./blog/nginx | /etc/nginx | web 容器的 nginx 配置 |
| web01/02/03 | ./blog/www | /usr/share/nginx/html | 代码(5 容器同一份) |
| lb01 / lb02 | ./lb01 ./lb02 | /etc/nginx | 各自 nginx 配置 |
几个容易迷糊的细节
1. 为什么 mysql 挂 /etc/mysql/conf.d 而不是 /etc/mysql/my.cnf
mysql 启动读 /etc/mysql/my.cnf,里面一行:
!includedir /etc/mysql/conf.d/自动 include conf.d 下所有 .cnf,与主配置共存。
直接挂 my.cnf 会整个覆盖 → 可能漏 socket、pid-file → 容器起不来。 挂 conf.d 是最稳的自定义配置方式。
2. redis 命令行还有 redis-server --save 60 1 --loglevel warning
redis-server --save 60 1 --loglevel warning
# ├ 每 60 秒 ├ 至少 1 个 key 变更 → 触发 RDB
# └ warning 日志级别也可以写进 redis.conf 挂进去。命令行更直观,两条路都行。
3. ./blog/www 同时被 5 个容器挂载
./blog/www → /usr/share/nginx/html
# php-fpm01、php-fpm02、web01、web02、web03意义:
- 改一份代码,5 个容器全部生效
- 不需要 re-build 镜像
- 生产里这目录应该是 git pull + rsync,容器只是消费者
6.3.2 容器化部署的核心思想
① 容器是"无状态"的
容器随时可删,"会变的东西"必须留在容器外。
② 配置和数据生命周期不同,分开存放
mysql/
├── config/ # 文本配置,改动少
└── data/ # 二进制数据,时刻在增长- 备份:data 必须,config 次要
- 扩容:data 占磁盘,config 大小稳定
- 混在一起 → 没法单独管理
- 这是运维原则:"动静分离、按价值分级"
③ 配置不进容器,从外部注入
容器内配置默认是镜像里固化的。 这套用 -v 覆盖到宿主机:
- 改配置 = 改宿主机文件,不重建镜像
- 同一镜像 + 不同挂载 = 不同实例(dev/stage/prod)
- 这就是 12-Factor App Config 原则:
"配置应该存在环境(env)或外部文件,不进代码"
④ 宿主机拿到全权(运维真正的自由)
| 能做的事 | 没挂载宿主机 | 这个项目 |
|---|---|---|
| 直接 vim 改配置后热重载 | 得进容器改/重建 | ✅ 直接改宿主机 |
| 备份数据库 | 进容器 mysqldump | ✅ 直接 tar ./mysql/data |
| Git 管理配置 | 镜像里拿不出 | ✅ ./blog/nginx 全在宿主机,git init 即可 |
| 跨机迁移 | 整容器打包 | ✅ 整个目录 scp 过去,docker run 起来 |
| 实时看日志 | 进容器 tail | ✅ 挂个 ./blog/log |
容器只是"工人",宿主机才是"真"环境。
⑤ 容器没挂载部分 = 临时/不可信任
- 应用代码、数据库、配置 → 必须挂(✅ 这项目做了)
- 日志 → 建议挂(❌ 这项目没挂,是优化点)
- /tmp 临时文件 → 可不挂(丢了无所谓)
6.3.3 改进方向
1. 用 docker-compose.yml 替代 run.sh
version: '3.8'
services:
redis:
image: redis:7.4
command: redis-server --save 60 1 --loglevel warning
volumes:
- ./redis/config:/usr/local/etc/redis
- ./redis/data:/data
restart: always
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ***
volumes:
- ./mysql/config:/etc/mysql/conf.d
- ./mysql/data:/var/lib/mysql
restart: always
php-fpm01:
image: myphp:7.4-fpm
volumes:
- ./blog/www:/usr/share/nginx/html
- ./blog/php:/usr/local/etc
depends_on: [mysql, redis]一行 docker compose up -d 全起。
2. --link 改自定义网络
前面讲过,自定义网络替代 --link,去掉 deprecated 警告。
3. web01-03 容器"浪费"
web01-03 是 nginx:latest,但它们不服务用户(还得反代给 lb02)。 生产里 web 应是 nginx + php-fpm 一体(LEMP),整套少一层反代。
💡 一句话: 容器无状态化 = 配置 + 代码 + 数据全部从容器内部拎到宿主机,容器本身只跑进程逻辑。
7. 容器编排与执行环境
7.1 ansible-navigator(容器执行环境)
是什么: RHCE 考试用的 Ansible 执行环境,跑在 Podman 容器里。
对比 ansible-playbook:
| ansible-playbook | ansible-navigator | |
|---|---|---|
| 执行环境 | 宿主机本地 | 容器内(Podman) |
| 输出 | 直接打到终端 | 默认进交互界面,加 -m stdout 才打到终端 |
| 环境隔离 | ❌ | ✅ |
| 配置文件 | ansible.cfg | ansible-navigator.yml(指定容器镜像) |
| PATH 环境 | 直接加载 | 需要完整 PATH(su - devops 才能加载) |
考试关键: 配置文件 ansible-navigator.yml 指定容器镜像地址,考试镜像在 registry.lab.example.com。
# ansible-navigator.yml
---
ansible-navigator:
execution-environment:
image: registry.lab.example.com/rhel9-ee:latest
playbook-artifact:
enable: true常用命令:
ansible-navigator run playbook.yml -m stdout # 跑 playbook
ansible-navigator run playbook.yml -i inventory -m stdout # 显式指定 inventory
ansible-navigator logs # 查看日志
ansible-navigator explore # 交互式调试
ansible-navigator images # 获取导航器镜像
podman login utility.lab.example.com # 登录镜像仓库7.2 为什么加 -i inventory?
ansible-playbook(本机执行)→ 读ansible.cfg里的 inventory 配置,自动找到ansible-navigator(容器里执行)→ 容器内"当前目录"可能和宿主机不同,ansible.cfg里的相对路径找不到-i inventory是显式指定 inventory 路径的保险写法,防止No inventory was parsed警告- 简单记:本地跑一般不用加,容器跑加
-i更稳
8. 虚拟机与虚拟化
暂无内容(KVM 虚拟化知识待归档)
9. JVM 虚拟机
暂无内容
10. 云服务与云原生
暂无内容
11. RHCE/RHCSA 容器考试专项
11.1 模拟题 vs 正式考试对比
| 对比项 | 模拟题 | 正式考试(2026) |
|---|---|---|
| 容器数量 | 3 道独立容器题 | 3 合 1,一道题考完 |
| 镜像来源 | 自定义构建(Containerfile + build) | 自行拉取(podman pull) |
| 镜像地址 | 固定地址 | 每人不同,和座位号相关 |
11.2 镜像地址规律
座位号 10 → registries.server10.example.com/library/...
座位号 5 → registries.server5.example.com/library/...⚠️ 镜像地址每人不同,务必先查考前须知,用 podman search 确认路径后再拉取。
11.3 完整考试操作流程
# 1. 登录仓库(地址看考前须知)
podman login registries.serverXX.example.com
# 2. 搜索镜像确认路径
podman search registries.serverXX.example.com watch
# 3. 拉取镜像
podman pull registries.serverXX.example.com/library/watch:latest
# 4. 准备挂载目录
mkdir -p /opt/txt /opt/pdf
# 5. 运行临时容器
podman run -d --name txt2pdf \
-v /opt/txt:/opt/txt:Z \
-v /opt/pdf:/opt/pdf:Z \
registries.serverXX.example.com/library/watch:latest
# 6. 生成 systemd 服务文件
mkdir -p ~/.config/systemd/user
cd ~/.config/systemd/user
podman generate systemd txt2pdf --files --name new
# 7. 启用开机自启
systemctl --user daemon-reload
systemctl --user enable container-txt2pdf.service
# 8. 清理并重启
podman rm -f txt2pdf
systemctl --user restart container-txt2pdf.service
# 9. 验证
podman ps11.4 必背命令速查
| 操作 | 命令 |
|---|---|
| 登录仓库 | podman login <registry> |
| 搜索镜像 | podman search <registry> <keyword> |
| 拉取镜像 | podman pull <full-image-path> |
| 列出镜像 | podman images |
| 打标签 | podman tag <src> <dst> |
| 删除镜像 | podman rmi <image> |
| 运行容器 | podman run -d --name <name> <image> |
| 列出容器 | podman ps / podman ps -a(含停止的) |
| 进入容器 | podman exec -it <name> /bin/bash |
| 删除容器 | podman rm <name> |
| 生成 systemd | podman generate systemd <name> --files --name new |
| 启用自启 | systemctl --user enable container-<name>.service |
| 查远程镜像 | skopeo inspect docker://<image> |
| 删除远程镜像 | skopeo delete docker://<image> |
12. Dockerfile 指令详解(ENV / ADD / EXPOSE / CMD)
12.1 ENV 指令详解
语法:
ENV <key>=<value> ...两个阶段都可用:
- 构建时:RUN / CMD / ENTRYPOINT 里用
$VAR引用 - 运行时:作为环境变量传给容器内进程
ENV vs ARG 对比:
| 指令 | 构建时 | 运行时 |
|---|---|---|
ENV | ✅ 可用 | ✅ 可用 |
ARG | ✅ 可用 | ❌ 不可见 |
实战 Dockerfile 解析(ubuntu:26.04 sshd 镜像):
FROM ubuntu:26.04
ENV DEBIAN_FRONTEND=noninteractive # apt 隐性读取
ENV TZ=Asia/Shanghai # 显式 $TZ + 运行时透传
ENV APP_VERSION=v1.0 # 只运行时给程序读
RUN apt update \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo "$TZ" > /etc/timezone \
&& apt install -y vim net-tools openssh-server iputils-ping iproute2 python3 \
&& sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin/" /etc/ssh/sshd_config \
&& echo 'root:1' | chpasswd \
&& apt clean
EXPOSE 22 8000 # 只是文档声明,不真发布端口
CMD ["/usr/sbin/sshd", "-D"] # 前台启动 sshd三个 ENV 实际用法区分:
DEBIAN_FRONTEND=noninteractive— apt 工具自动读取(约定俗成的隐性引用)TZ=Asia/Shanghai— Dockerfile 里显式$TZ设置时区 + 运行时透传给应用APP_VERSION=v1.0— Dockerfile 里不引用,只在运行时给应用程序读
运行时验证:
docker exec -it <容器> env # 查看所有环境变量
docker exec -it <容器> echo $APP_VERSION # 输出 v1.012.2 ADD 指令详解
语法:
ADD <src> <dest>src 源路径:
- 必须在构建上下文(
docker build的目录)内 - 不能写系统绝对路径(如
/etc/passwd) - 类型:文件、目录、相对路径、URL、通配符
dest 目标路径:
- 必须是容器内绝对路径
- 不能是相对路径
结尾斜杠的关键差别(重点):
| 写法 | 结果 |
|---|---|
ADD mydir /app/ | 把 mydir 的内容放进 /app/(不含 mydir 这一层) |
ADD mydir /app | 把 mydir 放到 /app 下,结果是 /app/mydir/ |
ADD 的特殊功能(COPY 没有):
- 自动解压 tar 文件:
ADD archive.tar.gz /app/ - URL 下载:
ADD https://example.com/file /tmp/(不推荐,建议用 RUN curl)
12.3 ADD vs COPY
| 维度 | ADD | COPY |
|---|---|---|
| 基础复制 | ✅ | ✅ |
| 自动解压 tar | ✅ | ❌ |
| URL 下载 | ✅ | ❌ |
| 官方推荐度 | 需要解压/下载才用 | 能 COPY 就 COPY |
官方建议:能 COPY 就 COPY,需要解压 tar 或下载时才用 ADD。
12.4 EXPOSE 真相
只是文档声明,告诉用户端口用途,不会真发布端口,必须 docker run -p 映射才生效。
EXPOSE 22 8000 # 仅文档作用,实际发布靠 -p12.5 CMD vs ENTRYPOINT(速记)
| 指令 | 作用 | docker run 后参数会怎样 |
|---|---|---|
CMD | 默认启动命令 | 被覆盖 |
ENTRYPOINT | 固定入口 | 作为参数追加给 ENTRYPOINT |
13. Docker 镜像存储位置 & 空间清理
默认存储位置
- 根目录:
/var/lib/docker/ - 镜像层(按存储驱动):
- overlay2(现代默认):
/var/lib/docker/overlay2/ - aufs(旧):
/var/lib/docker/aufs/ - devicemapper(旧):
/var/lib/docker/devicemapper/
- overlay2(现代默认):
/var/lib/docker/ 子目录:
overlay2/ ← 镜像和容器层都在这(最大头)
image/ ← 镜像元数据
volumes/ ← 匿名卷
containers/ ← 容器元数据
network/ ← 网络配置
tmp/查看空间占用
# 一键看 docker 占的各类资源
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 18 5 8.732GB 4.512GB (51%)
# Containers 8 2 1.234GB 987.5MB (80%)
# Local Volumes 12 4 567.8MB 123.4MB (21%)
# Build Cache 0 0 0B 0B
docker system df -v # 详细模式
# 看具体根目录
docker info | grep "Docker Root Dir"
# Docker Root Dir: /var/lib/docker
# 系统层看大小
du -sh /var/lib/docker/
du -sh /var/lib/docker/*/Docker 占空间的四类
- Images - 拉下来的镜像
- Containers - 创建过的容器层(rm 后还在)
- Local Volumes - 容器卷
- Build Cache - build 中间层(不用 build 几乎不占)
清理命令(按风险从小到大)
1. 清 dangling 镜像(标签 <none>)
# dangling = 无主镜像,build 中间产物
docker images
# REPOSITORY TAG IMAGE ID
# <none> <none> abc123... ← 这种
docker image prune # 清 dangling
docker image prune -a # 清 dangling + 所有无用的已命名镜像2. 激进清(全清)
docker system prune # 清停止的容器/无用网络/dangling/构建缓存
docker system prune -a # 上面的 + 所有未被容器用的镜像
# ⚠️ -a 威力大,会删 php:7.4-fpm 等没起容器的镜像3. 按时间条件
docker image prune -a --filter "until=24h" # 24h 前
docker image prune -a --filter "until=168h" # 7 天前
docker builder prune --filter "until=24h" # 构建缓存4. 单独清某种资源
docker container prune # 停止的容器
docker volume prune # 无主卷 ⚠️ 会丢数据
docker network prune # 无用网络这台 LNMP 架构主要吃的空间
| 资源 | 估算 | 处理建议 |
|---|---|---|
| php:7.4-fpm | ~400MB | image prune -a |
| nginx:latest | ~180MB | image prune -a |
| mysql:5.7 | ~500MB | 镜像大但常驻,清了还得拉 |
| redis:7.4 | ~130MB | 同上 |
| 容器停止后的层 | 几百 MB | system prune 一次清掉 |
| dangling 镜像 | 看 build 频率 | image prune 必清 |
没用 Dockerfile build,构建缓存基本不占。
直接 docker system prune -a 通常能回收 2-3GB。
搬 docker 到大盘(操作前要老大同意)
# 1. 停 docker
systemctl stop docker
# 2. 同步到新盘(不是 mv,保留权限)
rsync -aP /var/lib/docker/ /mnt/newdisk/docker/
# 3. 改配置
vim /etc/docker/daemon.json{
"data-root": "/mnt/newdisk/docker"
}# 4. 起回去
systemctl start docker
# 5. 确认
docker info | grep "Docker Root Dir"⚠️ 改 docker 根目录属于"系统/服务改动",按 MEMORY 铁律必须先经老大同意。
14. docker system df 输出解读
不是定死的
docker system df 的 SIZE 是当下实际占硬盘多少字节,每次执行现算的。
字段含义
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 14 0 7.845GB 7.061GB (90%)
Containers 0 0 0B 0B
Local Volumes 2 0 206.9MB 206.9MB (100%)
Build Cache 41 0 1.5GB 482.8MB| 列 | 含义 |
|---|---|
| TYPE | 资源类型 |
| TOTAL | 此类型总个数(含使用 + 未使用) |
| ACTIVE | 正在被用的资源数(容器在跑/镜像被任何容器引用) |
| SIZE | 实际占磁盘字节数(动态算的) |
| RECLAIMABLE | prune 后能省下多少 |
RECLAIMABLE 怎么算
| 类型 | 啥算"可回收" |
|---|---|
| Images | 没被任意容器引用(running/stopped 都不算占用) |
| Containers | 状态是 exited/dead |
| Volumes | 没被任意容器挂载 |
| Build Cache | 永远可回收(下次 build 重生成) |
⚠️ 坑:Container stopped 状态占的层不会自动释放,要 docker container prune 或 docker rm 才释放。
SIZE 何时变化
| 操作 | SIZE 变化 |
|---|---|
docker pull xxx | Images ↑ |
docker run xxx 容器内写文件 | Containers ↑ |
docker stop 容器 | Container SIZE 不变 |
docker rm -f | Container SIZE 可能不立即变小(layer 在 overlay) |
docker compose up -d --build | Build Cache ↑ |
docker image prune -a | Images ↓ |
docker system prune | 各类 ↓ |
"用过的就还占着",prune 才释放。
ACTIVE 全 0 的解读
# 输出里 ACTIVE 全是 0 意味着:
# - 14 个镜像没一个被容器引用
# - 2 个卷没一个被容器挂载
# - build cache 41 个最近没 build不正常 —— LNMP 项目跑着的话 LB01/web01 的容器应该在 ACTIVE > 0。
可能情况:
- 跑完实验后
docker stop+docker rm全清过,镜像留着没删 - dockge 项目本身就在这目录,但容器没起来
- 看的是另一台机器
清理前 ⚠️ 检查 volumes 不是数据库
mysql/redis 的数据如果在 volumes 里,prune 一下全没! 项目用的是 -v bind mount,不容易踩这坑,但 anonymous 卷(没用 -v 也没用 named volume)可能产生。
docker volume ls
docker volume inspect <卷名> | grep Mountpoint
ls /var/lib/docker/volumes/<卷名>/_data/
# 看到 mysql/ redis 文件夹 → ⚠️ 是数据库数据,不能清这个场景的清理顺序
ACTIVE 全 0 → 全清风险低:
# 1. 清镜像和 dangling
docker image prune -a # 释放 ~7GB
# 2. 清 build cache
docker builder prune # 释放 ~0.5GB
# 3. 清无主卷(要先确认不是数据库)
docker volume prune # 释放 ~0.2GB
# 4. 看效果
docker system df清理完理论上 SIZE 总和 9.7GB → 1GB 以内。
💡 一句话: SIZE 是当下实际占盘字节数(动态算);RECLAIMABLE 是理论值,跑了 prune 才真释放;ACTIVE 0 是异常信号;Volumes prune 前必查 Mountpoint 别误清数据库。
15. Kubernetes 核心概念与操作
15.1 Pod command 与三大探针(liveness/readiness/startup)
Pod command 是什么
严格说叫入口命令——容器启动时的执行命令由 command(覆盖镜像 ENTRYPOINT)+ args(覆盖镜像 CMD)共同决定:
| 写法 | 效果 |
|---|---|
只写 command | 镜像原 CMD 被丢弃 |
command 和 args 都写 | 完全覆盖镜像原值 |
| 都不写 | 用镜像默认 ENTRYPOINT + CMD |
演示 YAML
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec-pod
spec:
containers:
- name: liveness-exec-container
image: aaronxudocker/tools:busybox
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c", "touch /tmp/live; sleep 60; rm -rf /tmp/live; sleep 3600"]
livenessProbe:
exec:
command: ["test", "-e", "/tmp/live"]
initialDelaySeconds: 1
periodSeconds: 3
resources:
limits:
memory: "128Mi"
cpu: "500m"command 等价于:
/bin/sh -c "touch /tmp/live; sleep 60; rm -rf /tmp/live; sleep 3600"- 创建存活标记
/tmp/live - 撑 60 秒
- 删除标记
- 长睡(进程还在,但标记没了)
livenessProbe 行为
exec.command跑test -e /tmp/live:文件存在返回 0(活),不存在返回非 0(死)initialDelaySeconds: 1:容器启动 1 秒后才开始探测periodSeconds: 3:每 3 秒探测一次
完整循环:
- 0s:容器启动 → 创建标记 → 探针通过
- 1s 起:每 3s 探测一次
- 60s 后:标记被删 → 探针失败累计到
failureThreshold(默认 3)次 → K8s 重启容器 - 重启后脚本重新跑,标记又出现,循环往复
这是典型的 livenessProbe 演示用例:故意制造"假死",看 K8s 探针失败触发重启的效果。
探针参数速查
| 字段 | 默认值 | 作用 |
|---|---|---|
initialDelaySeconds | 0 | 容器启动后多久开始第一次探测 |
periodSeconds | 10 | 探测间隔 |
timeoutSeconds | 1 | 单次探测超时 |
failureThreshold | 3 | 连续失败多少次判死 |
successThreshold | 1 | 连续成功多少次判活(liveness 必须是 1) |
生产建议 periodSeconds 从 10s 起,演示里 3s 偏激进但合理。
三大探针类型
- exec:在容器内执行命令,返回 0 算活
- httpGet:访问 HTTP 端点,2xx/3xx 算活
- tcpSocket:能建立 TCP 连接算活
15.2 httpGet 探针 path 的真实解析位置
核心结论
看 Nginx 配置,不是看 K8s。 httpGet 不是文件系统访问,是 HTTP 请求:
- path 拼到 URL 里发给容器 80 端口(
http://<pod-ip>:80/<path>) - 路径怎么解析 → 完全由容器内 Nginx 的
root/alias/location决定 - K8s 只管发请求,不管对应哪个文件系统位置
演示 YAML
readinessProbe:
httpGet:
port: 80
path: /index2.html
initialDelaySeconds: 1
periodSeconds: 3
startupProbe:
httpGet:
path: /index1.html
port: 80
periodSeconds: 10
failureThreshold: 30nginx:latest 默认 nginx.conf 里有 root /usr/share/nginx/html;,所以:
/index2.html→ 访问容器内/usr/share/nginx/html/index2.html/index1.html→ 访问容器内/usr/share/nginx/html/index1.html
只有当 Nginx 配置改成 root / 或 alias /,才走容器根目录。默认镜像下结论是 Nginx 的 html 目录。
这是个"故意失败的演示"
默认 nginx 镜像里只有 index.html,没有 index1.html / index2.html:
- startupProbe:访问
/index1.html→ 404 → 失败累计到failureThreshold: 30(每 10s 一次 = 5 分钟)→ K8s 认为启动失败 → 重启 Pod - readinessProbe:访问
/index2.html→ 404 → 探针失败 → Pod 不会被加入 Service Endpoints,收不到流量
目的是让你看 startupProbe 触发重启、readinessProbe 隔离流量的效果。生产里 path 必须写真实存在的文件。
一句话速记
httpGet.path = URL 路径(不是文件系统路径),由容器内 Web 服务的 root/alias/location 解析为实际文件位置。
探针失败后果对照
- startupProbe 失败 → 重启 Pod
- readinessProbe 失败 → 隔离流量(不重启)
- livenessProbe 失败 → 重启 Pod
15.3 lifecycle 钩子(postStart / preStop)
钩子是什么
容器生命周期关键时刻的自定义回调,类似 Java 的 init() / destroy():
| 钩子 | 触发时机 | 类比 |
|---|---|---|
postStart | 容器创建后立即执行(ENTRYPOINT 之后) | 启动回调 |
preStop | 容器被终止之前执行(SIGTERM 之前) | 销毁回调 |
支持 exec(容器内跑命令)和 httpGet(发 HTTP 请求)两种方式。
演示 YAML
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo postStart > /usr/share/message"]
preStop:
exec:
command: ["/bin/sh", "-c", "echo preStart > /usr/share/message"]lifecycle 下有两个不同类型的钩子:postStart 1 个 + preStop 1 个,互不干扰。
postStart 详解
- 触发时机:容器进程启动后异步触发,不保证先于 ENTRYPOINT 完成(常见误区)
- 失败后果:钩子失败 → 容器被杀死并按重启策略重启
- 常见用途:
- 注册服务到注册中心
- 复制配置文件 / 预热缓存
- 通知外部系统"我起来了"
- 初始化数据库 schema
preStop 详解
- 触发时机:K8s 决定销毁容器时,先执行 preStop,再发 SIGTERM
- 阻塞:K8s 等 preStop 跑完才往下走,但默认宽限期 30 秒,超时被 SIGKILL 强杀
- 常见用途:
- 优雅关闭(关闭数据库连接、刷新内存数据到磁盘)
- 反注册服务(从负载均衡摘除、注销 Eureka)
- 保存状态、上报监控
容器终止完整顺序
K8s 决定终止 Pod
→ preStop 钩子执行(阻塞)
→ SIGTERM 发给主进程(容器自己处理)
→ 等待 terminationGracePeriodSeconds(默认 30s)
→ 还没死? → SIGKILL 强杀演示脚本的行为
两个钩子写同一个文件 /usr/share/message,时序不同 → 值不同:
- 容器正常运行期间看 → 内容是
postStart(preStop 还没跑) - 容器被销毁前的瞬间 → preStop 覆盖 → 内容变成
preStart - 想同时看到两个值 → 不可能(除非在 preStop 执行后立刻抓快照)
是钩子的经典演示:让你观察钩子到底有没有跑、什么时候跑的。生产里两个钩子的 command 当然不能写同一个文件——典型套路是 postStart 写启动日志、preStop 写清理日志。
15.4 ReplicationController 与 Pod 模板
RC 是什么
K8s 最老一代的 Pod 副本控制器(现在被 ReplicaSet 取代,ReplicaSet 又被 Deployment 包装,但底层逻辑一样)。
核心职责:保证 spec.replicas 指定的副本数始终成立:
- Pod 挂了 → 自动补
- Pod 多出来 → 自动删
- 核心机制:控制循环(reconciliation loop / 调谐循环)
观察实际状态 vs 对比期望状态 → 不一致 → 执行调谐演示 YAML
apiVersion: v1
kind: ReplicationController
metadata:
name: rc-demo
spec:
replicas: 3
selector:
app: rc-demo
template:
metadata:
name: rc-demo
labels:
app: rc-demo
spec:
containers:
- name: rc-demo-container
image: aaronxudocker/myapp:v1.0
ports:
- containerPort: 80Pod 模板(template)怎么理解
template = Pod 的样板/模具,告诉控制器"以后要补 Pod 就按这个规格创建"。
类比:模具(template) vs 产品(Pod);class(template) vs instance(Pod)。
关键细节:
- template.metadata.labels 必须包含 selector 的全部 label,否则 K8s 拒绝创建(防止孤儿 Pod)
- template.metadata.name 写不写无所谓:K8s 创建 Pod 时会强制清空,然后生成
rc-demo-xxxxx(带随机后缀)。最佳实践是不写 - template.spec 会被原样复制成 Pod 的 spec
metadata 和 spec 为啥平级
这是 K8s API 对象的标准结构,所有资源都遵循:
apiVersion: v1 # API 版本
kind: <资源类型>
metadata: # "我是谁" — 身份信息
name, namespace, labels, annotations, uid
spec: # "我想要什么" — 期望状态(你写的)
...
status: # "我现在怎样" — 实际状态(控制器自动写,不让你写)类比:
| 字段 | 简历类比 | 订单类比 |
|---|---|---|
| metadata | 名字、联系方式(你是谁) | 订单号、买家(谁下的) |
| spec | 期望岗位、期望薪资(你想要啥) | 商品规格、数量(你要买啥) |
| status | 入职后实际岗位、薪资 | 实际发货/收货状态 |
设计精髓:声明式 API。用户只管写 spec(期望),控制器持续对比 spec 和 status,不一致就调谐让 status 趋近 spec。用户永远不手动写 status。
YAML 的隐藏陷阱
image: aaronxudocker/myapp:v1.0— 私人镜像需要imagePullSecrets,不然ImagePullBackOff- selector 只有 1 个 label — 生产里通常多个 label 区分版本/环境
- RC 已被淘汰 — K8s 1.18 后官方建议用 Deployment(自带滚动更新、回滚),但学 RC 是为了理解 Deployment 的底层
15.5 Service 删除命令详解
最常用
kubectl delete svc <service-name>svc = service 简写,完全等价。
批量/选择删除
# 一次删多个
kubectl delete svc nginx-svc redis-svc
# 按 label 选择
kubectl delete svc -l app=nginx
# 删某个 namespace 下所有 svc(危险)
kubectl delete svc --all -n my-namespace
# 跨所有 namespace(超级危险)
kubectl delete svc --all --all-namespaces通过 YAML 文件删
kubectl delete -f service.yaml需要强制删除吗?
不需要。Service 是集群内逻辑对象(不是进程,没有"清理"动作),删除立即生效,没有 grace period 概念。Pod 那种 -f --grace-period=0 --force 套路对 Service 不适用。
删除前的"看一眼"
kubectl get svc # 当前 namespace
kubectl get svc -A # 所有 namespace
kubectl get svc -n kube-system # 指定 namespace三个关键提醒
① 删除 Service 不会删 Pod
Service 是逻辑对象,后端 Pod 完全不受影响——Pod 还活着,只是不再有 Service 把流量转给它了。要彻底清理得手动 kubectl delete pod 或 kubectl delete deployment。
② selector 自动管理 Endpoints
如果 Service 用了 selector:
selector:
app: nginxService 会自动维护 Endpoints 对象(Pod IP 列表)。删 Service 时 Endpoints 自动跟着没,不用单独处理。
如果是 headless service(clusterIP: None)或手动指定 endpoints,删除时要注意 Endpoints 是否遗留。
③ 别手贱 --all
生产里 kubectl delete svc --all -n <业务ns> 前必须确认 namespace 里没别人部署的关键服务(比如 kube-system 里那些),不然 cluster DNS、metrics-server 全没,集群能炸半天。
15.6 Deployment 三层标签结构(metadata.labels vs selector)
核心结论
Deployment 的 metadata.labels ≠ spec.selector.matchLabels,是两码事:
metadata.labels= 这个 Deployment 对象本身的标签(给人/工具看的)spec.selector.matchLabels= 我这个 Deployment 管哪些 RS(找别人的标签)
可以一样也可以不一样,但最佳实践是保持一致。
演示 YAML
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myapp-deploy # ⚠️ 跟 selector 不一致
name: myapp-deploy
spec:
replicas: 3
selector:
matchLabels:
app: myapp # ✅ 跟 RS 匹配
template:
metadata:
labels:
app: myapp # ✅ 创建的 Pod 带这个
spec:
containers:
- name: myapp
image: aaronxudocker/myapp:v1.0三层标签完整图
Deployment
├─ metadata.labels: app=myapp-deploy ← 对象自己的标识
├─ spec.selector.matchLabels: app=myapp ← 管哪些 RS
└─ spec.template.metadata.labels: app=myapp ← 创建的 Pod 带什么 label
│
▼ Deployment 创建 RS
ReplicaSet
├─ metadata.labels: app=myapp ← 被 Deployment selector 匹配
└─ spec.template.metadata.labels: app=myapp ← 创建的 Pod 带什么 label
│
▼ RS 创建 Pod
Pod
└─ metadata.labels: app=myapp ← 被 Service selector 匹配每一层 selector 都在找下一层的 label:
- Deployment selector → 找 RS(用 RS labels)
- RS selector → 找 Pod(用 Pod labels)
- Service selector → 找 Pod(用 Pod labels)
为啥这段 YAML 这样写能工作
- Deployment 拿
app=myapp找 RS → 创建的 RS label 就是app=myapp→ 匹配成功 - RS 拿
app=myapp找 Pod → 创建的 Pod label 就是app=myapp→ 匹配成功 - Service 拿
app=myapp找 Pod → 匹配成功
Service 根本不关心 Deployment 的 metadata.labels 是啥——Service 只认 Pod labels。
Deployment 的 metadata.labels 到底是干嘛的
纯粹是给"Deployment 对象本身"贴的标签,用于:
- 给人查:
kubectl get deploy -l app=myapp-deploy - 给工具引用:Service Mesh(Istio/Linkerd)、监控、CI/CD 识别资源
- 跨 controller 关联:某些 operator 按 label 找 Deployment
绝对不会参与 Deployment → RS 的匹配。这是新手最大误区。
YAML 的隐藏陷阱 + 最佳实践
metadata:
labels:
app: myapp-deploy # ⚠️ 跟 selector 不一致,容易误导虽然功能没问题,但非常容易把人搞糊涂。最佳实践:
# ✅ 推荐 1:保持一致,避免歧义
metadata:
labels:
app: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
# ✅ 推荐 2:想区分就用不冲突的 label key
metadata:
labels:
app: myapp # 核心标签,跟 selector 一致
deployment: myapp-deploy # 额外标识15.7 Service 工作机制与"Myapp Version 1.0"来源
实验现象
$ kubectl get deploy
myapp-deploy 3/3 3 3 22s
$ kubectl get service
myapp ClusterIP 10.3.47.41 <none> 80/TCP 23h
$ while true; do curl 10.3.47.41; sleep 1; done
Myapp Version 1.0
Myapp Version 1.0
...① 为啥改 label 就能跟 Service 关联
Service 只通过 selector 找 Pod,不管 RS。完整链路:
Service (selector: app=myapp)
↓ Endpoints Controller watch
Endpoints 对象(自动维护 Pod IP 列表)
↓ kube-proxy 持续 watch
配置 iptables / IPVS 规则
↓ 访问 ClusterIP
DNAT 到真实 Pod IP详细步骤:
- Endpoints Controller(kube-controller-manager 组件)持续 watch Service selector 和 Pod labels
- 把匹配的 Pod IP 写入 Endpoints(v1.21+ 默认 EndpointSlice)
- kube-proxy(每 Node 跑一个)监听 Endpoints 变化
- 生成 iptables 规则(轮询负载均衡)
- 访问 ClusterIP 时 iptables DNAT 到 Pod IP
$ kubectl describe svc myapp
Selector: app=myapp ← Service 在找这个 label
IP: 10.3.47.41
Endpoints: 10.244.1.5:80,10.244.2.3:80,10.244.3.7:80 ← 自动维护的 Pod 列表② Service ClusterIP 是"虚拟 IP"
- ClusterIP(10.3.47.41)不绑任何网卡——纯虚拟
ping 10.3.47.41不通(ICMP 包没匹配 iptables 规则被丢)curl 10.3.47.41能通(TCP 包被 iptables 抢在前面 DNAT)- iptables 在 OUTPUT 阶段拦截(不是 PREROUTING,因为没路由)
③ "Myapp Version 1.0" 是哪来的
应用代码自己返回的字符串,跟 K8s 完全无关。
镜像 aaronxudocker/myapp:v1.0 里的 Web 应用代码大概类似:
@app.route("/")
def hello():
return "Myapp Version 1.0" # 应用代码里硬编码这就是 K8s 滚动更新的经典演示:换 aaronxudocker/myapp:v2.0,curl 返回 "Myapp Version 2.0",逐步替换 Pod。
④ "Myapp" 名字到底是哪层的
4 个独立的"Myapp",互相之间没关联:
| 出现位置 | 是什么 | 谁起的 |
|---|---|---|
镜像名 aaronxudocker/myapp:v1.0 | Docker Hub 仓库名 | 镜像作者 |
Service 名 myapp | K8s 资源名 | 你 |
Pod label app=myapp | K8s label 值 | 你 |
| 返回字符串 "Myapp Version 1.0" | 应用代码硬编码 | 镜像作者 |
K8s 永远不会把 Service 名、label 塞进 HTTP 响应 body。4 个"Myapp"只是恰好同名让你产生错觉。
⑤ 这套机制的"自愈"特性
Pod IP 是动态的,Service 自动跟上:
Pod 重启 → IP 变了 → Endpoints Controller 检测 → 更新 Endpoints
→ kube-proxy 重新生成 iptables → 客户端无感知继续访问这就是 Service 的核心价值:入口稳定,后端可换。
⑥ 验证方法
kubectl get endpoints myapp # 看自动维护的 Pod IP 列表
kubectl describe svc myapp # 看 selector 和 endpoints 详情
kubectl get pod -o wide -l app=myapp # 看 Pod 实际 IP 和 label
# 直接 curl 某个 Pod IP(不走 Service),验证返回内容一致
curl 10.244.1.5 # 同样返回 "Myapp Version 1.0"
# 说明返回内容确实来自 Pod 内应用,跟 Service 无关💡 一句话: K8s 的整套 Service 机制 = selector 找 Pod → 写 Endpoints → kube-proxy 生成 iptables → DNAT 到真实 Pod。"Myapp Version 1.0" 是应用代码硬编码字符串,跟 Service 名、label 名完全无关。
15.8 K8s Job 完整指南(completions / parallelism / backoffLimit)
场景
配置 completions=10, parallelism=5, restartPolicy=Never,镜像随机返回退出码。 现象:Pod 数远超 completions(看到 15 个 Pod),Error/Completed 混杂,COMPLETIONS 卡在 8/10 跑不动。
YAML 关键字段
spec:
completions: 10 # 目标成功 Pod 数
parallelism: 5 # 并行上限
backoffLimit: 6 # ⚠️ 默认 6,累计失败 Pod ≥6 就 Failed
activeDeadlineSeconds: # 整体超时(建议显式设置)
template:
metadata:
name: rand # ⚠️ 反模式:pod template name 跟 Job 同名,应删掉
spec:
restartPolicy: Never # 失败 Pod 不原地重启,由 Job Controller 补新 PodJob Controller 核心循环
if .status.successful < completions 且 活跃 Pod < parallelism:
立即创建新 Pod 填补- 失败的 Pod 也算占过并行槽位,所以总 Pod 数 > completions
- Completions 只数退出码=0 的 Pod
- Error 之后紧跟 Pending → ContainerCreating 不是延迟,是槽位释放后立即被新 Pod 抢占
restartPolicy 对比
| Policy | 行为 | 适用场景 |
|---|---|---|
| Never | 失败不重启,Job Controller 创建新 Pod 补 | 跑一次性离线任务 |
| OnFailure | ✅ 容器原地重启,不消耗并行名额 | 镜像可能偶发失败,推荐 |
| Always | 不允许(Job 强制只能是 Never 或 OnFailure) | — |
backoffLimit vs backoff period(易混两个概念)
| 概念 | 作用 | 默认值 |
|---|---|---|
spec.backoffLimit | 累计失败 Pod 上限,达到就判 Job Failed | 6 |
| backoff period | 失败后等多久再创建新 Pod(指数退避) | 初始 10s 翻倍 |
数法:每个退出码非 0 的 Pod 直接 +1 到 .status.failed,所以 N 个 Error Pod = .status.failed = N。 判定:.status.failed >= backoffLimit → Job 标 Failed,不再创建新 Pod。
"卡住"的真实原因(两个机制叠加):
- 失败后 backoff period 指数增长(10s → 20s → 40s → 80s...),看起来像卡住
- backoffLimit 累计达到 → Job 判 Failed → 停止创建 Pod,COMPLETIONS 永远卡在 <10
验证 backoffLimit 是否触发
# 看 Job 详细状态(Conditions 和 Events)
kubectl describe job rand
# 期望看到:
# Conditions:
# Type: Failed
# Status: True
# Reason: BackoffLimitExceeded
# 看原始 status 字段
kubectl get job rand -o jsonpath='{.status}' | jq .
# 看 Pod 数量是否稳定(Failed 后不再涨)
kubectl get pods -l job-name=rand --no-headers | wc -l为什么 get job 还显示 8/10 而不是 Failed?
- Job Controller 是周期性 reconcile(默认几秒~十几秒)
- 8/10 那一刻可能是 reconcile 间隙,再等几秒 get 就是 Failed: True
kubectl describe比kubectl get更早反映真实状态
改进模板
spec:
backoffLimit: 20 # 提高容错
activeDeadlineSeconds: 300 # 整体 5 分钟超时防死循环
template:
spec:
restartPolicy: OnFailure # ✅ 推荐:容器原地重试不消耗并行槽位
containers:
- image: <image>⚠️ 删掉 template.metadata.name(反模式,Job Controller 会自动生成 <job-name>-<random>)
核心要点
backoffLimit数的是退出码非 0 的 Failed Pod 数,跟restartPolicy: Never直接挂钩Never下每个 Failed Pod 必 +1;OnFailure下失败由容器自己重试,不增加.status.failed- 想"重试到成功" → 用
OnFailure+ 高backoffLimit+activeDeadlineSeconds三件套
一句话速记: restartPolicy: Never + 随机退出镜像 → Job 疯狂补 Pod 凑数,但 backoffLimit: 6 会让 Job 半路 Failed。推荐 OnFailure + 提高 backoffLimit + 加 activeDeadlineSeconds。
15.9 K8s Namespace 机制
直接答案
-n = --namespace 缩写,指定查哪个 namespace。K8s 用 namespace 把资源切成逻辑组(类似虚拟集群),做隔离和权限控制。
- 不加
-n→ 查defaultnamespace -n kube-system→ 查 kube-system namespace-A=--all-namespaces→ 查全部
4 个内置 namespace
| Namespace | 作用 |
|---|---|
default | 默认,用户资源默认落这 |
kube-system | K8s 系统组件(calico、kube-proxy、coredns...) |
kube-public | 公开可读,放集群公共信息 |
kube-node-lease | 节点心跳(1.14+),节点健康检测 |
常用 namespace 命令
kubectl get ns # 列所有 namespace
kubectl get pod -A # ✅ 推荐:看所有 namespace 的 Pod
kubectl get ds -A # 查所有 namespace 的 DaemonSet
kubectl config set-context --current --namespace=kube-system # 切换默认 ns
kubectl create ns my-ns # 创建 namespace
kubectl apply -f app.yaml -n my-ns # 指定 ns 部署YAML 里指定 namespace
metadata:
name: my-daemonset
namespace: kube-system # 不写默认 default
spec:
...一句话速记: -n 指定 namespace,不写默认 default;系统组件住 kube-system,用户资源默认 default;想一次看全用 -A。
15.10 K8s Headless Service 详解(无 VIP,DNS 直接返 Pod IP)
Headless vs 普通 Service
| 维度 | 普通 Service | Headless Service |
|---|---|---|
clusterIP | 有(自动分配 VIP) | None |
| DNS 解析 | 单条 A 记录 → ClusterIP | 多条 A 记录 → 每个 Pod IP |
| 负载均衡 | K8s 自动(iptables/IPVS) | 客户端自己做 |
| 配套 StatefulSet | ❌ | ✅(标准用法) |
核心区别:Headless 不要 VIP,直接把后端 Pod IP 全部暴露,客户端自己挑 Pod。
三种典型用途
- StatefulSet 配套:每个 Pod 要稳定 DNS(如
mysql-0.mysql,mysql-1.mysql) - 客户端自定义负载:要看到所有后端,自己选(轮询/最少连接/权重)
- 服务发现:拿所有 Pod IP 做注册中心、监控、流量调度
K8s DNS 域名结构
<service-name>.<namespace>.svc.<cluster-domain>
↑ ↑
svc 固定 默认 cluster.local例:service-headliness.default.svc.cluster.local 拆解:service 名 . default ns . svc . cluster.local
Pod 内 /etc/resolv.conf 自动补全
nameserver 10.0.0.10 # CoreDNS
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5search列表:Pod 里ping service-headliness(短域名)时自动按顺序补全ndots:5:域名中点数 < 5 才走 search 补全
实际场景:StatefulSet 配 Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # ← Headless 关键配置
selector:
app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # ← 关联 Headless Service
replicas: 3DNS 解析能力:
mysql.default.svc.cluster.local→ 3 个 Pod IPmysql-0.mysql.default.svc.cluster.local→ 单个 Pod 的稳定 IP(StatefulSet 核心)
验证 Headless
dig @10.0.0.10 <service-name>.<ns>.svc.cluster.local
# Headless 返回 N 条 A 记录(N = Pod 数)
# 普通 Service 返回 1 条 A 记录(ClusterIP)通俗版讲解(公司打电话比喻)
| 模式 | 比喻 | DNS 解析结果 |
|---|---|---|
| 普通 Service | 公司有总机,K8s 帮你转接 | 1 个总机号(ClusterIP) |
| Headless Service | 没有总机,直接发通讯录 | N 个员工号(所有 Pod IP) |
三个生活场景:
- 外卖平台挑厨师:顾客想自己选评价最高的 → Headless(系统告诉 3 个厨师,自己选)
- StatefulSet 稳定卡号:Pod 挂了重建名字不能变 → Headless(
mysql-0永远是mysql-0) - 监控系统数机器:要看所有后端 IP → Headless(一眼看到所有 Pod)
一句话速记: Headless = 关掉自动总机,给你员工名单自己打电话。
15.11 K8s Service 四种类型 DNS 对照 + ExternalName
K8s Service 4 种类型 DNS 对照
| 类型 | dig 返回 | 后端 |
|---|---|---|
| ClusterIP | A <ClusterIP> (1 条) | 集群内 Pod,K8s 代理 |
| Headless | A <PodIP1> A <PodIP2> A <PodIP3> (N 条) | 集群内 Pod,无代理 |
| ExternalName | CNAME <外部域名> (链式解析) | 集群外服务,无代理 |
| NodePort/LoadBalancer | A <ClusterIP> (1 条) | 集群内 Pod,K8s 代理 + 端口暴露 |
"无 ClusterIP" 的 3 种 Service
| 类型 | ClusterIP | selector | DNS 记录 |
|---|---|---|---|
| Headless | None | 有 | 多 A → Pod IP |
| ExternalName | 无 | 无 | CNAME → 外部域名 |
| 无 selector Service | 有/无 | 无 | A → 手动建 Endpoints |
Headless vs ExternalName 对比
| 维度 | Headless | ExternalName |
|---|---|---|
| 目的 | 暴露集群内 Pod | 暴露集群外外部服务 |
| DNS 记录 | A 记录(Pod IP 列表) | CNAME(别名到外部域名) |
| 后端来源 | selector 选中的 Pod | externalName 指定的域名 |
| 典型场景 | StatefulSet、稳定标识 | 集群内访问外部 DB |
共性:都是"无代理" Service
- 都没 ClusterIP(
<none>) - 都不走 iptables/IPVS 代理
- 都是纯 DNS 层面的服务发现
ExternalName 详解
spec:
type: ExternalName
externalName: www.baidu.comdig 返回 CNAME 记录(不是 A 记录),链式解析到外部 IP:
service-externalname.default.svc.cluster.local
↓ CNAME
www.baidu.com
↓ CNAME
www.a.shifen.com
↓ A 记录
180.101.50.242
180.101.50.188ExternalName 特点:
- 不分配 ClusterIP(
EXTERNAL-IP列显示外部域名) - 不需要 selector(没有 Pod 关联)
- DNS 返回 CNAME 记录(域名别名)
- 不转发流量,纯"域名翻译"
ExternalName 实战场景
场景 1:集群内访问外部数据库(最常用)
apiVersion: v1
kind: Service
metadata:
name: external-mysql
namespace: prod
spec:
type: ExternalName
externalName: db.prod.example.comPod 里 mysql -h external-mysql.prod.svc.cluster.local → 连 db.prod.example.com好处:代码写死集群内域名,外部地址变了只改 Service。
场景 2:环境切换(测试/生产)
# 测试环境
externalName: mock-db.testing.example.com
# 生产环境(同一份代码)
externalName: real-db.prod.example.com一句话速记: Headless 用 A 记录把内网 Pod IP 列表给你;ExternalName 用 CNAME 把外网域名"翻译"给你;两者都跳过了 K8s 的流量代理。
15.12 Pod 网络基础:Pod IP + containerPort + 共享 netns
核心结论
- Pod IP 是 K8s 给 Pod 分配的虚拟 IP(flannel 默认
10.244.0.0/16),节点物理网卡上根本看不到这个 IP,是 CNI 在节点上撑起的虚拟网络分配的 containerPort只是声明性 metadata,不等于端口映射,不会创建 iptables 规则- nginx 监听
0.0.0.0:80天然就在 Pod IP:80 上,这是物理层面绑在一起的结果,不是 K8s "映射"出来的
Pod IP 的实质(以 10.244.140.106 为例)
- 任何真实网卡上都看不到这个 IP,它是 CNI 虚拟网络分配的
- 安装 K8s 时声明 Pod 网络 CIDR(flannel 默认
10.244.0.0/16,calico 经常192.168.0.0/16等) - 节点上会多出虚拟网络设备:
flannel.1、cni0、veth pair... - Pod 内的 eth0 在宿主机上对应一段 veth pair 的另一头
- 节点间通过 VXLAN / IPIP / host-gw 等隧道转发流量
- 集群内 Pod IP 全可达,集群外不可达(私有 IP + 无路由)
为什么 80 端口会"到 Pod IP 上":pause 容器 + 共享 netns
每个 Pod 启动时 kubelet 都先拉起一个超小的 pause 容器(啥也不干、占着一个 netns 不退出),业务容器全部 --netns=pause 加入到这个 netns:
┌──── Pod: volume-emptydir (共享一个 netns)──────────┐
│ │
│ eth0 ←─── Pod IP 10.244.140.106 绑在这里(pause 持有)│
│ ↑ │
│ │ 同一张网卡 / 同一张路由表 / 同一个 localhost │
│ ├────────────────────┐ │
│ │ │ │
│ ┌───────┐ ┌───────────┐ │
│ │ nginx │ │ busybox │ │
│ │ :80 │ localhost │ tail -f │ │
│ │ (eth0) │ │ │ │
│ └───────┘ └───────────┘ │
└────────────────────────────────────────────────────────────┘推论:
- nginx 默认监听
0.0.0.0:80→ 自动监听在 Pod IP(eth0):80 - busybox 因共享 netns,
curl localhost:80就能访问 nginx - Pod 内所有容器看到的 eth0 = localhost = 路由表 = iptables = Pod 级"网络面板"
containerPort 本质
ports:
- containerPort: 80- 纯声明性 metadata,不打端口、不做 DNAT、不建 iptables
- 唯一作用:给 Service / Ingress 等控制器做匹配依据、
kubectl get pod -o yaml展示意图、给 port-forward 智能提示参考 - 真正把流量引到 Pod 内的是 Service / NodePort / Ingress / hostPort 等独立资源,跟 containerPort 是两码事
Pod 多种访问方式对照
| 访问方 | 命令 | 通否 | 原理 |
|---|---|---|---|
| 同 Pod 内其他容器 | curl localhost:80 | ✅ | 共享 netns |
| 集群内 Pod / 节点 | curl http://10.244.140.106/ | ✅ | CNI 路由打通 |
| 集群外(笔记本) | 直接访问 10.244.140.106 | ❌ | 私有 IP + 无外部路由 |
让集群外能访问 nginx 的 4 种方案
# 方案 1:Service(集群内 VIP,最基础)
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
selector: { ... } # 匹配 Pod 的 label
ports:
- port: 80 # Service 自身端口
targetPort: 80 # 转发到 Pod 的 80
# 方案 2:NodePort(每个节点开 30000-32767 固定口)
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080 # 访问 <NodeIP>:30080 进来
# 方案 3:kubectl port-forward(调试用,不生产)
kubectl port-forward pod/volume-emptydir 8080:80
# 本机访问 http://localhost:8080
# 方案 4:Ingress(七层路由,需要先有 NodePort Service)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: nginx-ing }
spec:
rules:
- host: nginx.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port: { number: 80 }Pod 像一台小虚拟机:端口是所有容器的并集
| 类比维度 | Pod | 普通虚拟机 |
|---|---|---|
| IP | Pod IP(绑在 pause eth0 上) | 虚拟机 IP |
| 内部进程 | 多个容器(共享 netns) | 多个进程 |
| 监听端口 | 所有容器监听端口的并集 | 所有进程监听端口的并集 |
| 端口冲突 | 不允许同端口两个容器(同 netns) | 不允许同端口两个进程 |
| 内部通信 | localhost:<port> | localhost:<port> |
⚠️ 端口冲突 = Pod 起不来:共享 netns,同一端口只能被一个容器 listen。两个容器都 listen 80 → Pod 启动报错 bind: address already in use → CrashLoopBackOff。
一句话速记: Pod IP + 80 端口 = 天然可用,不是映射是共享;containerPort 是声明标签不是真映射;集群外访问必须经 Service / NodePort / Ingress 这层"搬运工"。
15.13 EmptyDir Volume 与 Sidecar(边车)模式
完整双容器示例
apiVersion: v1
kind: Pod
metadata:
name: volume-emptydir
spec:
containers:
- name: nginx
image: aaronxudocker/myapp:v1.0
ports:
- containerPort: 80
volumeMounts:
- name: logs-volume
mountPath: /var/log/nginx # nginx 挂到日志目录
- name: busybox
image: aaronxudocker/tools:busybox
command: ["/bin/sh","-c","tail -f /logs/access.log"]
volumeMounts:
- name: logs-volume
mountPath: /logs # busybox 挂到 /logs
volumes:
- name: logs-volume
emptyDir: {} # Pod 级声明的空目录卷YAML 字段含义对照
| 字段层级 | 写法 | 含义 |
|---|---|---|
apiVersion: v1 | core 组 | 不带 /v1xxx 前缀 |
kind: Pod | 资源类型 | 最小调度单位 |
metadata.name | Pod 名字 | 同 namespace 内唯一 |
spec.containers[] | 容器数组 | 本 Pod 含 2 个容器 |
spec.containers[].ports | 端口声明 | containerPort 是声明性 |
spec.containers[].volumeMounts | 容器内挂载点 | 挂哪个卷、挂哪里 |
spec.volumes | Pod 级卷定义 | 整个 Pod 共享 |
spec.volumes[].emptyDir | 空目录卷 | Pod 启动创建、删除时清理 |
双容器共享外部目录的核心三步
第一步:volumes 是 Pod 级资源,对所有容器可见 写在 Pod 身上而非某个容器里,相当于"在 Pod 这个沙盒里申请了一块共享存储",所有声明了同名 volume 的容器都能看到。
第二步:每个容器通过 volumeMounts 把同一个卷挂到自己内部不同路径
- nginx 把
logs-volume挂到/var/log/nginx(覆盖 nginx 默认日志目录,写入访问日志) - busybox 把同一个
logs-volume挂到/logs(不同容器内路径)
挂载路径不同,但底层是同一份"块儿"。
第三步:共享生效 = 同一份文件 = 实时同步/var/log/nginx/access.log(nginx 视角) == /logs/access.log(busybox 视角)
nginx 写一行 → busybox 的 tail -f 立刻就能看到这行。
数据流图
┌────────────── Pod: volume-emptydir ──────────────┐
│ │
│ ┌── logs-volume (emptyDir 共享存储) ──┐ │
│ │ access.log ←── nginx 写入 │ │
│ │ error.log │ │
│ └─────────────────────────────────────┘ │
│ ▲ ▲ │
│ │ 挂到 /var/log/nginx │ 挂到 /logs │
│ ┌────────┐ ┌──────────┐ │
│ │ nginx │ ─── 共享卷 ──→ │ busybox │ │
│ │ 容器 │ 同一份文件 │ tail -f │ │
│ └────────┘ └──────────┘ │
└────────────────────────────────────────────────────┘这个模式叫 Sidecar(边车模式)
| 角色 | 容器 | 职责 |
|---|---|---|
| 主容器 | nginx | 处理业务请求、写日志 |
| 边车容器 | busybox | 实时消费/转发日志 |
核心思想:一个容器产生数据,另一个容器消费/加工/转发,通过共享 volume 解耦。
生产环境 busybox 通常换成:
fluentd/filebeat→ 采集日志推 ELKpromtail→ 采集日志推 Loki- 自定义 agent → 上报监控埋点
istio-proxy→ Service Mesh 边车代理流量
EmptyDir 易踩坑清单
- 生命周期 = Pod 生命周期:Pod 一删数据清空;持久化必须用
hostPath/PV/PVC - 挂载会"遮蔽"容器原有目录:nginx 把卷挂到
/var/log/nginx后,原有文件被藏(不是删,umount 后可见) - 多容器共享 = 要考虑读写冲突:两边同时操作同份文件可能锁竞争/读写不一致
- 不跨节点:和
hostPath不同,emptyDir严格在 Pod 内共享;跨 Pod 共享得用 PV - 默认存在节点本地磁盘:节点挂了数据丢;可指定
medium: Memory走 tmpfs(飞快但吃内存,不算入容器内存 limit) - 必须先在
volumes里定义,容器才能挂:先有卷声明再有容器挂载 - 挂载路径建议用绝对路径:相对路径在不同镜像下行为不一致
15.14 K8s Pod 启动报错:BusyBox/瘦身镜像找不到 /bin/bash
报错信息
Error: failed to start container "busybox": ...
OCI runtime create failed: runc create failed: unable to start container process:
error during container init: exec: "/bin/bash": stat /bin/bash: no such file or directory
Back-off restarting failed container busybox根本原因
- BusyBox / Alpine 镜像默认 shell 是
ash(路径/bin/sh),镜像内没有/bin/bash - Pod spec 里把入口写成了
/bin/bash(常见错配,照搬 Debian/Ubuntu/CentOS 习惯) - runtime 去找
/bin/bash→ 该文件在 BusyBox 镜像里不存在 → 启动失败 - kubelet 检测到失败 → 指数退避重试(BackOff),典型表现
x5 over 4m
BusyBox 镜像特点
- 大小约 1~2 MB,只保留最常用的 Unix 工具
- 默认 shell 是
ash,不是 bash,/bin/sh才是入口 - 不包含 bash、glibc、GNU coreutils,很多 Debian/Ubuntu 脚本直接搬过去会报缺命令
- alpine:xxx 镜像也用
ash(基于 busybox + musl libc),同一套坑
解决方案(三种写法任选)
# ✅ 写法 1:改用 /bin/sh(最常见)
- name: busybox
image: aaronxudocker/tools:busybox
command: ["/bin/sh", "-c", "echo hello > /cache/index.html; sleep 3600"]
# ✅ 写法 2:K8s 1.27+ 用 shell 字段声明
spec:
containers:
- name: busybox
image: aaronxudocker/tools:busybox
shell: /bin/sh # 1.27+ 才支持,需检查版本
command: ["echo hello > /cache/index.html; sleep 3600"]
# ✅ 写法 3:留空入口,让镜像自带 ENTRYPOINT 兜底
- name: busybox
image: aaronxudocker/tools:busybox
# 不写 command,让镜像 Dockerfile 的 ENTRYPOINT/CMD 生效镜像家族与 shell 选型对照
| 镜像家族 | 默认 shell | Pod spec 里应该写 |
|---|---|---|
| busybox | ash | /bin/sh |
| alpine | ash | /bin/sh |
| debian / ubuntu | bash | /bin/bash |
| centos / rhel | bash | /bin/bash |
排查标准动作
- 看 events:
kubectl describe pod <name>→ 找 Failed 行的exec: "xxx"信息 - 验证镜像里到底有没有这二进制:bash
docker run --rm --entrypoint ls aaronxudocker/tools:busybox /bin/ # 或 docker run --rm aaronxudocker/tools:busybox which sh - 临时进入容器测试:用
kubectl run --rm -it --image=busybox test -- sh跑一下 - 改 yaml 重 apply:
command改为/bin/sh -c "...",或换基础镜像(busybox → alpine / debian-slim)
⚠️ 跟 §3.2.1 的区别:§3.2.1 讲的是运行中容器用 docker exec 进不去(瘦身镜像 exec 失效);本节讲的是 Pod 启动阶段 runtime exec entrypoint 找不到,是不同场景不同排错思路。
15.15 Deployment 滚动更新:kubectl patch + annotations 触发 + 替换粒度
命令本身
kubectl patch deployment hotupdate-deploy \
--patch '{"spec": {"template": {"metadata": {"annotations": {"version/config": "v1.0" }}}}}'| 部分 | 含义 |
|---|---|
kubectl patch | 补丁式更新,只改指定路径字段 |
deployment hotupdate-deploy | 目标资源 |
--patch '{...}' | 默认 strategic merge patch(智能合并,不删其他字段) |
| JSON 路径 | 4 层嵌套:spec → template → metadata → annotations → version/config |
合并后效果(不会删除同级别其他 annotation,比如 kubectl.kubernetes.io/last-applied-configuration):
spec:
template:
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: ...
version/config: "v1.0" # 新增为什么改 annotations 能触发滚动更新
核心机制:Deployment controller 对 spec.template 整体计算 hash,hash 变 → 创建新 ReplicaSet → 滚动更新。
被纳入 hash 的字段:
spec.template.spec.containers(image / env / resources / command ...)spec.template.spec.volumes/volumeMountsspec.template.metadata.labelsspec.template.metadata.annotations✅ 是的,annotations 也算spec.template.spec.restartPolicy...
改 spec.template 下任何字段都会触发 hash 变化 → 触发滚动更新。
「用 annotation 强制触发滚动更新」的实战妙用
痛点场景:
- ConfigMap / Secret 配置更新了
- 但 Pod template 里引用方式没变(
envFrom configMapRef name写死) - Deployment controller 算 hash 没变 → 不会自动重建 Pod → 老 Pod 跑老配置
解决思路:在 spec.template.metadata.annotations 里维护版本号字段,每次想"刷新"所有 Pod 时手动 bump:
metadata:
annotations:
version/config: "1.0" # ConfigMap 改了 bump
version/secret: "3" # Secret 改了 bump# ConfigMap 更新后手动触发:
kubectl patch deployment hotupdate-deploy \
--patch '{"spec":{"template":{"metadata":{"annotations":{"version/config":"1.1"}}}}}'即使 Deployment 自身配置一字不变,也照滚一遍,让所有 Pod 重新拉取最新 ConfigMap。
滚动更新完整流程
kubectl patch
↓
Deployment Controller 检测 spec.template hash 变化
↓
创建 newRS(初始 replicas=0),保留 oldRS
↓
按 maxSurge / maxUnavailable 节奏交替伸缩:
t1: oldRS=4, newRS=0
t2: oldRS=3, newRS=1 (maxSurge=1,每轮 newRS +1 / oldRS -1)
t3: oldRS=2, newRS=2
t4: oldRS=1, newRS=3
t5: oldRS=0, newRS=4 (完成)
↓
oldRS 缩到 0 但**不删除**,方便回滚(kubectl rollout undo)spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # 最多比期望多几副本(如 4 副本 → 允许 5)
maxUnavailable: 25% # 最多几副本不可用(4 副本 → 允许 1 挂)替换粒度:Pod 级(不是容器级,不是节点级)
Q:同一个 yaml 里跑的节点,改了一个容器配置,滚动更新能用到所有容器上吗?
A:80% 对,中间有两个修正点:
✅ 理解对的点:
- 改 yaml 后 apply,整个 Deployment 下所有 Pod 副本都会换成新版本
- 最终状态 = 全部一致的新 Pod,不是「老的留老的、新的换新的」
❎ 需要修正的点:
- 「节点」说法偏差:Deployment 管的是 Pod 副本(ReplicaSet 下的 N 个 Pod),不是节点。一个节点可以跨多个 Deployment 跑多个 Pod
- 「只改某个容器」被重新加载:不能只替换 Pod 里某一个容器。Pod 整体哈希变了 → 整个 Pod 重建,所有容器都跟着重启(哪怕没改那个容器)
| 概念 | 替换粒度 |
|---|---|
| Deployment 滚动更新 | Pod 级(一个 Pod 一个 Pod 换) |
| Pod 内多容器共享资源 | 不能只换单个容器 |
| 副本数滚动 | 按 spec.replicas 而不是节点数 |
⚠️ 实战例子(双容器 Deployment):
spec:
replicas: 3
template:
spec:
containers:
- name: nginx
image: myapp:v1.0 → v2.0 # 改了
- name: busybox # 没改
image: tools:busyboxapply 之后结果:
- ✅ 3 个 Pod 全部重建(nginx 升到 v2.0)
- ✅ busybox 也跟着重启了(Pod 是整体)
- ❎ 不是「只换 nginx、保留 busybox」
- ❎ 不是「只换第 1 个 Pod 保留其他 2 个」
三个改动 vs 替换范围
| 改的东西 | 触发滚动? | 结果 |
|---|---|---|
| image tag v1 → v2 | ✅ 是 | 3 个 Pod 都升到 v2 |
| 加个环境变量 | ✅ 是 | 3 个 Pod 都重启加载新 env |
| 改 annotations 版本号 | ✅ 是 | 3 个 Pod 都重启 |
| 改 ConfigMap 内容(但 Pod template 引用方式没变) | ❌ 否 | Pod 不会自动重建!老配置一直跑! |
最后那条是实战中特别容易踩的坑。
kubectl patch 的 3 种策略
| 类型 | 适用场景 | 备注 |
|---|---|---|
| strategic merge patch(默认) | 90% 场景 | 智能合并,保留未指定字段 |
| json merge patch(RFC 7396) | 简单合并 | 嵌套对象会被新值完全替换 |
| json patch(RFC 6902) | 数组级精细操作 | op: replace/add/remove,需数组 path |
实战几乎只用默认的 strategic merge patch。
易踩坑清单
- annotation key 含
/不报错:K8s annotation key 允许带/,用作命名空间分类(version/config是常见用法) kubernetes.io/前缀禁止用户使用:保留前缀,会拒绝创建- 同值改两次不触发:必须 v1.0 → v2.0 这种值变化才会触发 hash 变化
kubectl rollout status deployment/<name>比观察 Pod 状态更直观,是排查进度标准命令- 回滚用
kubectl rollout undo deployment/<name>:回到上一个 Revision,因为旧 ReplicaSet 没被删(K8s 默认保留历史 RS) - 滚动过程 Pod 数可能临时超出 replicas:因为有 maxSurge;批量调度也可能临时不足(maxUnavailable 允许少量挂)
- annotation 触发的"虚拟更新"会真的起停一遍 Pod:哪怕业务逻辑没变,所有 Pod 都会被替换一次(生产环境注意流量抖动)
15.16 Pod 调度策略:反亲和性 + Node 污点与 Toleration
A. Pod 反亲和性 podAntiAffinity
完整 YAML
apiVersion: v1
kind: Pod
metadata:
name: pod-podantiaffinity-required
spec:
containers:
- name: nginx
image: aaronxudocker/myapp:v1.0
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: podenv
operator: In
values: ["pro"]
topologyKey: kubernetes.io/hostname逐字段拆解
| 字段 | 含义 |
|---|---|
affinity | 亲和性总开关 |
podAntiAffinity | 类型:反亲和(vs podAffinity 是亲和) |
requiredDuringSchedulingIgnoredDuringExecution | 硬限制(必须满足,否则 Pending) |
labelSelector.matchExpressions | 要避开的 Pod 标签匹配条件 |
key / operator / values | 匹配表达式:label 筛选条件 |
topologyKey | 拓扑域(节点/AZ/region) |
一句话讲透
调度本 Pod 时,scheduler 去找全集群带 podenv=pro 标签的 Pod,本 Pod 不能调度到已经存在这种 Pod 的 node 上。
required vs preferred
| 写法 | 含义 | 失败结果 |
|---|---|---|
requiredDuringSchedulingIgnoredDuringExecution | 硬限制 | Pod Pending,永远调度不上 |
preferredDuringSchedulingIgnoredDuringExecution | 软限制 | 违反也调度,加权分扣 |
IgnoredDuringExecution 代表什么
- 「调度时考虑,运行时不强制」
- Pod 一旦调度上去,后续反亲和规则变化不会触发驱逐
- 后续调度才遵守新规则
topologyKey 决定颗粒度
| topologyKey | 颗粒度 |
|---|---|
kubernetes.io/hostname | 节点(同主机名不贴) |
topology.kubernetes.io/zone | 可用区 |
topology.kubernetes.io/region | 区域 |
调度可视图
集群 3 个 node,2 个 podenv=pro Pod:
┌───────────────────────┐
│ node1: [nginx-pro ] │
│ node2: [nginx-pro ] │
│ node3: [] │
│ │
│ 第 3 个 nginx-pro Pod 被强制调度到 node3
└───────────────────────┘反亲和 vs 亲和用途
| 类型 | 效果 | 经典场景 |
|---|---|---|
podAffinity | 跟带 label 的 Pod 同拓扑域 | DB cache 跟 DB 同 node |
podAntiAffinity | 跟带 label 的 Pod 不同拓扑域 | 主从不同 node / ingress 散布多个 node |
易踩坑
- 节点数不够会 Pending:3 个 podenv=pro 分散要 3 个 node,集群只有 2 个 → 第 3 个永远 Pending
- 诊断:
kubectl describe pod看0/3 nodes are available: 3 node(s) didn't match pod anti-affinity rules
- 诊断:
- 不命中的 Pod 不参与计算:label selector 命不中的 Pod scheduler 不看
- 影响的是同拓扑域的「别的 Pod」,不是自己:本 Pod 自己也带 podenv=pro 也行,约束的是避开别的同类 Pod
IgnoredDuringExecution不能省:跟 required 配对的全名就是这个,缩写不合法matchExpressions是数组:多条表达式是 AND 关系yamlmatchExpressions: - key: podenv operator: In values: ["pro"] - key: app operator: In values: ["nginx"] # 表示:app=nginx AND podenv=pro- Pod 反亲和是调度阶段控制:不会让已运行的 Pod 被驱逐
- 多 namespace 下的 Pod 也起作用:label selector 跨 namespace 查(指定 namespaces 除外)
B. Node 污点(Taints)+ Pod Toleration
4 种查看方法
# ① describe 直眼看 Taints: 行
kubectl describe node node01 | grep -A 5 "Taints:"
# ② YAML 完整信息
kubectl get node node01 -o yaml | grep -A 10 taints
# ③ JSONPath 直接抠字段
kubectl get node node01 -o jsonpath='{.spec.taints}' && echo
# ④ 全部节点 + 自定义列(最常用)
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints输出示例:
NAME TAINTS
master [map[effect:NoSchedule key:node-role.kubernetes.io/master timeAdded:...]]
node01 <none>
node02 <none>只要看到 <none> 或空数组 = 无污点,所有 Pod 都能调度。
Taint 字段格式
spec:
taints:
- key: dedicated
value: gpu
effect: NoSchedule
timeAdded: "2026-..." # 系统自动添加,系统 taint 才有格式:key=value:effect(key 可不带 value,如 node-role.kubernetes.io/master:NoSchedule)
三种 effect 对比
| effect | 效果 |
|---|---|
NoSchedule | 禁止新 Pod 调度,已跑 Pod 不动 |
PreferNoSchedule | 软限制,尽量不调度;没节点也会调 |
NoExecute | 禁止新调度 + 驱逐已在运行的 Pod(最狠) |
常见系统自带 taint
node-role.kubernetes.io/master:NoSchedule # master 节点默认
node.kubernetes.io/unreachable:NoExecute # node 失联时自动
node.kubernetes.io/not-ready:NoExecute # NotReady
node.kubernetes.io/memory-pressure:NoSchedule # 内存压力
node.kubernetes.io/disk-pressure:NoSchedule # 磁盘压力
node.kubernetes.io/pid-pressure:NoSchedule # PID 压力
node.kubernetes.io/network-unavailable:NoSchedule # 网络未就绪设置 / 去掉 taint
# 设置
kubectl taint nodes node01 dedicated=gpu:NoSchedule
# 去掉(加后缀 -)
kubectl taint nodes node01 dedicated=gpu:NoSchedule-Taint + Toleration 原理(单向门 + 钥匙)
Node 端(抹黄油、贴反门):
kubectl taint nodes node01 dedicated=gpu:NoSchedulePod 端(带钥匙 → 才让进):
spec:
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
# tolerationSeconds: 3600 # NoExecute 才用,最多忍多久驱逐调度逻辑:Node 的所有 taint 都被 Pod 的 tolerations 完全覆盖,Pod 才被允许调上去。
实战:GPU 节点打 gpu=true:NoSchedule,普通 Pod 没 toleration 自动跳过;只有 ML 任务 Pod 带 toleration 才能用。
易踩坑清单
- master 节点有隐性污点:
kubectl get nodes看 master 是<none>,其实系统默认加了node-role.kubernetes.io/master:NoSchedule,必须 describe 才看全 - NoExecute 立刻驱逐:不加 toleration 直接 NoExecute,在跑 Pod 立马被踢 → 服务中断
- tolerationSeconds 只对 NoExecute 有效:其他两种 effect 忽略这字段
- toleration 不能 Partial 匹配:写
key=A不等于容忍key=A,value=B;要么Equal+ value 写全,要么用Exists只匹配 key - toleration 是「允许」不是「调度」:带 toleration ≠ 一定被调上去,只是「不会因为这个 taint 拒绝」
effect不写会报错:系统识别不到这个 taint- NoSchedule 不驱逐老 Pod:只挡新 Pod;要驱逐用 NoExecute
- 可以同时加多种 taint:一个 Node 可多个 taint,调度时 Pod 需对每个 taint 都有对应 toleration