Skip to content

☁️ 云计算 ​

本章收录云计算相关知识,包括:容器(Docker/Podman/Kubernetes)、虚拟机(JVM/KVM)、虚拟化、云原生等。

来源:从 linux.md(容器管理)、rhcsa.md(容器题)、rhce.md(ansible-navigator)、redhat.md(容器考试变化)、network.md(OpenVPN Docker)汇总整理,原文件内容保留不变。


📑 目录索引 ​


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 提供更好的安全性对安全性要求较高的容器化应用
PodmanRed Hat 开发的下一代容器运行时,RHEL 8/9 默认容器工具,RHCSA/RHCE 2026 官方指定无守护进程(Daemonless);Rootless 原生支持;完全兼容 Docker CLI;原生支持 Pod 概念;podman generate systemd 一键集成 systemdRHEL/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 ​

特性DockerPodman
架构C/S 架构(需要 daemon 守护进程)无守护进程(直接通过 runc)
Rootless需要额外配置原生支持(推荐)
CLI 兼容性自己的命令完全兼容 Docker 命令(别名 alias docker=podman)
RHEL 推荐❌✅(RHCSA/RHCE 考试用 podman)
配置文件/etc/docker/daemon.json/etc/containers/registries.conf
镜像构建docker buildpodman build(用 Containerfile 或 Dockerfile)
镜像命名docker.io/xxxlocalhost/xxx(默认)

考试场景:RHCSA/RHCE 2026 改用 Podman,所有容器操作都用 podman 命令。


1.4 镜像构建隔离性(Build once, run anywhere) ​

构建时:pip 包装到镜像里,与物理机隔离 ​

问题: pip install -r requirements.txt 装的包是下载到镜像里吗?和物理机隔离吗?

答案: 都对 ✅

构建过程:

  1. docker build 启动临时容器
  2. 在临时容器内执行 RUN pip install
  3. 包安装到容器的 /usr/local/lib/python3.x/site-packages/
  4. 临时容器销毁,结果冻结成一层固化到镜像

两个核心点:

  1. 下载到镜像里:装的包是镜像文件系统的一部分,可 docker save/load 分发
  2. 与物理机隔离:物理机没装 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 考试用) ​

toml
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 用) ​

toml
# /etc/containers/registries.conf
[registries.search]
registries = ['utility.lab.example.com']

[registries.insecure]
registries = ['utility.lab.example.com']

2.2 构建镜像 ​

bash
# 完整流程(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 images

podman build 关键参数:

参数含义
-t NAME:TAG指定镜像名和标签
.构建上下文(当前目录),漏掉会报错
-f Containerfile路径指定 Containerfile 路径(默认当前目录)

2.3 镜像导入/导出/标签/推送 ​

bash
# 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,需要删除

bash
# 方法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)镜像
bash
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 的源路径都相对这个上下文根:

dockerfile
COPY . /app           # 把上下文根下所有文件 → 容器 /app
COPY ./src /app/src   # 上下文根/src → 容器 /app/src

坑:如果当前目录有 .git、node_modules、.env,构建会:

  1. 慢(大文件全发给 daemon)
  2. 泄露(.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一次打多个标签

完整流程示例:

bash
# 目录结构
.
├── 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:v1

2.7 镜像与容器元数据(docker inspect) ​

2.7.1 docker inspect 基础 ​

用途:查任何 docker 对象的详细元数据,返回 JSON。默认刷屏,配合 -f 用 Go template 取单字段才是日常姿势。

支持 inspect 的对象:

bash
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.Statusrunning/exited/paused/restarting/dead
State.Pid容器 PID 1 在宿主机的进程号(debug 用)
State.ExitCode上次退出码(exited 时看)
State.OOMKilled是否被 OOM 杀掉
Mounts所有挂载点(type/source/destination)
NetworkSettingsIP、Gateway、Ports(端口映射)
HostConfig启动时的资源/策略(内存/CPU/重启策略/日志驱动)
LogPath日志文件路径
Env环境变量

核心:-f Go template 用法(重点记)

写法含义
&#123;&#123;.Id&#125;&#125;取顶层字段
&#123;&#123;.State.Pid&#125;&#125;嵌套字段(点路径)
&#123;&#123;json .Mounts&#125;&#125;转 JSON(接 jq 美滋滋)
&#123;&#123;range .X&#125;&#125;&#123;&#123;.Y&#125;&#125;&#123;&#123;end&#125;&#125;遍历数组/切片
&#123;&#123;range .X&#125;&#125;&#123;&#123;println .&#125;&#125;&#123;&#123;end&#125;&#125;每行打一项
&#123;&#123;index .X "key"&#125;&#125;map 取值

容器常用 -f 例子:

bash
# 查容器 IP(最常用)
docker inspect -f '&#123;&#123;range .NetworkSettings.Networks}}&#123;&#123;.IPAddress}}&#123;&#123;end}}' web

# 查主进程 PID
docker inspect -f '&#123;&#123;.State.Pid}}' web

# 查所有挂载点
docker inspect -f '&#123;&#123;json .Mounts}}' web | jq .

# 查退出码(排查为什么起不来)
docker inspect -f '&#123;&#123;.State.ExitCode}}' web

# 查 OOM
docker inspect -f '&#123;&#123;.State.OOMKilled}}' web

# 查端口映射
docker inspect -f '&#123;&#123;json .NetworkSettings.Ports}}' web | jq .

镜像常用 -f 例子:

bash
# 完整 SHA256
docker inspect -f '&#123;&#123;.Id}}' nginx:latest

# 默认 CMD
docker inspect -f '&#123;&#123;json .Config.Cmd}}' centos:7

# ENTRYPOINT
docker inspect -f '&#123;&#123;json .Config.Entrypoint}}' nginx:1.27

# 所有环境变量
docker inspect -f '&#123;&#123;range .Config.Env}}&#123;&#123;println .}}&#123;&#123;end}}' nginx:1.27

# 所有 tag
docker inspect -f '&#123;&#123;json .RepoTags}}' nginx:1.27

# 所有层
docker inspect -f '&#123;&#123;range .RootFS.Layers}}&#123;&#123;println .}}&#123;&#123;end}}' nginx:1.27

实战排查三连(背下来):

bash
# 容器起不来三连:
docker ps -a | grep web                                # 1. 看在不在
docker logs --tail 50 web                              # 2. 看日志
docker inspect -f '状态=&#123;&#123;.State.Status}} 退出码=&#123;&#123;.State.ExitCode}} OOM=&#123;&#123;.State.OOMKilled}}' web  # 3. 看状态

# 端口映射没生效?
docker inspect -f '&#123;&#123;json .NetworkSettings.Ports}}' web | jq .

# 环境变量问题排查
docker inspect -f '&#123;&#123;range .Config.Env}}&#123;&#123;println .}}&#123;&#123;end}}' web

2.7.2 GraphDriver.Data 消失问题(Docker 版本演进) ​

现象:老教程里 docker inspect centos:7 输出长这样:

json
"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:

json
"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镜像所有只读层的合并视图
MergedDirLower + Upper 联合挂载出来的虚拟合并盘
WorkDiroverlay2 内部工作目录(atomic rename 用)

版本差异对比:

项老版 overlay2新版 snapshotter
路径/var/lib/docker/overlay2/<id>/snapshotter 统一管理,不公开
inspectGraphDriver.Data 有四件套只有 RootFS.Layers
进 MergedDir 翻文件✅ 可以❌ 普通方法不行
启动速度一般略快(lazy pull、按需加载)

验证自己用的是哪个版本:

bash
docker version                                                    # client & server 版本
docker info | grep -i "storage driver"                            # 看 storage driver 关键!
ls /var/lib/docker/                                               # 新版可能没 overlay2/ 目录

这影响什么?

  1. 直接进 MergedDir 翻文件这条老技巧死了

  2. 想看镜像内文件的新办法:

    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
  3. 别再按老教程查 /var/lib/docker/overlay2/——查不到


3. 容器操作 ​

3.1 运行容器(podman run) ​

bash
# 创建挂载目录
mkdir /home/sashat/syslog

# 运行容器(带特权模式和卷挂载)
podman run -d --privileged \
  -v /home/sashat/syslog:/var/log:Z \
  --name logserver \
  registry.lab.example.com/library/rsyslog:latest

podman 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 的命令才好用)。

bash
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.5CPU 核数限制
⚙️ 资源-m 512m内存限制
⚙️ 资源--memory-swap -1swap 限制(-1 = 不限)
⚙️ 资源--cpuset-cpus 0,1绑死特定 CPU
🔐 安全--privileged特权模式(慎用,几乎能调所有内核接口)
🔐 安全--cap-add NET_ADMIN增加 capability
🔐 安全--cap-drop ALL去掉所有 capability(最小权限)
🔐 安全--security-opt seccomp=unconfinedseccomp/apparmor
🔐 安全--user跑非 root 用户(首选安全措施)
🪵 日志--log-driver json-file|syslog|fluentd日志驱动
🪵 日志--log-opt max-size=10m日志驱动参数(用于切割)
🪵 日志--entrypoint /bin/sh覆盖镜像默认入口

实战模板:

bash
# 起后台的交互式开发环境
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 myapp

3.1.2 docker run 最后的 /usr/sbin/nginx 是啥? ​

完整语法:docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

镜像名之后的所有位置参数,都是给容器执行的进程(不是挂载、不是参数)。

bash
docker run -d -p 80:80 --name my_nginx rocky_nginx:v1 /usr/sbin/nginx
#                       ^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^
#                       IMAGE             COMMAND

/usr/sbin/nginx 就是说容器起来后执行这个二进制,作为容器主进程(PID 1)。

为什么不写也能跑? 镜像 Dockerfile 通常定义了默认启动命令:

dockerfile
ENTRYPOINT ["/usr/sbin/nginx"]   # ENTRYPOINT 形式
CMD ["/usr/sbin/nginx"]          # CMD 形式

什么时候要写命令行版本?

  1. 带自定义参数启动:docker run rocky_nginx:v1 /usr/sbin/nginx -g "daemon off;"
  2. 覆盖默认进程(调试):docker run -it --rm rocky_nginx:v1 /bin/bash
  3. 镜像没设 ENTRYPOINT/CMD 时,必须命令行给命令,否则起不来

ENTRYPOINT vs CMD vs 命令行:

  • ENTRYPOINT:命令行参数作为它的追加参数(不容易覆盖)
  • CMD:命令行参数整体替换它
  • 都没设:必须命令行给命令

跟挂载点 -v 的区别:

bash
# -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 进入容器执行命令 ​

bash
# 进入容器交互式 shell
podman exec -it logserver /bin/bash

# 在容器内执行 logger
logger "This is a rhcsa exam"

# 退出容器
exit

验证:

bash
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(瘦身镜像坑) ​

报错:

bash
$ 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❌ 只有 ashdocker exec -it <容器> sh
gcr.io/distroless/*❌ 完全没 shell🚫 exec 进去不了
scratch❌ 空🚫 exec 进去不了

快速判断镜像类型:

bash
# 看镜像基础
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(最快)

bash
docker exec -it pv-ac-sim sh

② alpine 装 bash

bash
docker exec -it pv-ac-sim apk add bash
docker exec -it pv-ac-sim bash

⚠️ 装 bash 不会写回镜像,容器重启就丢。

③ 改 Dockerfile 用完整镜像(推荐永久)

dockerfile
# 从
FROM node:alpine
# 改成
FROM node:20-bookworm   # debian 完整版

distroless 镜像怎么 debug:

  1. 看应用日志:docker logs <容器> / kubectl logs
  2. sidecar + shareProcessNamespace:
    yaml
    shareProcessNamespace: true
    exec 进带 shell 的 sidecar 看主容器进程
  3. 临时换镜像:本地调试用 node:20-bookworm 替代 distroless
  4. 远程调试:Node.js 加 --inspect=0.0.0.0:9229,Chrome DevTools 连接

一句话速记: 瘦身镜像(alpine/distroless)没 bash,先用 docker exec -it <容器> sh 试一下;distroless 连 sh 都没有,exec 完全失效,只能靠日志和 sidecar。


3.3 端口映射 -p 主机端口:容器端口 ​

命令示例:

bash
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,映射到主机不同端口)。

常用写法:

bash
-p 8080:80                  # 指定映射(最常用)
-p 127.0.0.1:8080:80        # 只允许本机访问(绑定指定 IP,更安全)
-P                          # 大写 P:随机映射端口(自动选空闲端口)

查看实际映射:

bash
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 ✅ 显式指定服务端口

建议写法: 显式写出端口号更清晰:

bash
python3 -m http.server 8000

虽然默认就是 8000,但显式写不容易出错。

一句话总结: 不加端口号能跑是因为 http.server 默认 8000,和 -p 前面的 8000 没关系。三个端口要一致:主机端口 → 容器端口 → 服务监听端口。


3.4 Docker 临时容器(--rm) ​

场景:用 Docker 运行一次性容器执行某个操作,容器自动删除。

bash
# 生成 UUID(以 sing-box 代理工具为例)
docker run --rm ghcr.io/sagernet/sing-box:latest generate uuid
参数含义
--rm容器执行完后自动删除,不留垃圾
ghcr.io/sagernet/sing-box:latestDocker 镜像地址(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,覆盖机器上所有容器。

典型用途(这个命令几乎都是配合管道/命令替换用):

bash
# 停止所有容器
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 过滤:

bash
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:latestimport 回来(变镜像一层)

3.6.6 日志 & 资源 ​

  • docker logs <id> / -f:看 / 跟踪
  • docker logs --tail 100 / --since 10m:最近 100 行 / 最近 10 分钟
  • docker logs -t:带时间戳

3.6.7 批量操作(重点) ​

方案 1:直接传多个 ID

bash
docker stop $(docker ps -qa)
docker rm -f $(docker ps -qa)

方案 2:先过滤再操作(精准)

bash
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:输入为空就不执行
bash
docker ps -qa | xargs -r docker stop
docker ps -qa | xargs -r docker rm -f
docker ps -qa | xargs -I{} docker inspect -f '&#123;&#123;.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 实战速查(背下来) ​

bash
# 清场:全部停 + 全部删
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 -af

3.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 bashexec 进入,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) ​

内存限制(硬限制) ​

bash
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 限制对比示例:

bash
# 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 配置后无法解析域名

解决:

bash
# 修改容器 /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 Start

4.2 容器 NAT(MASQUERADE) ​

问题: VPN 客户端连接后能访问部分网站,但部分网站访问不了。

原因: VPN 子网 172.27.0.0/16 没做 NAT,包出去了回不来。

解决:

bash
# 容器内添加 iptables NAT
iptables -t nat -A POSTROUTING -s 172.27.0.0/16 -o eth0 -j MASQUERADE

# 持久化:修改 entrypoint 脚本,使重启后规则还在

⚠️ 容器重启后 iptables 规则会丢失,必须修改 entrypoint 让规则持久化。


格式: --link 源容器:别名

bash
docker run ... --link web01:web01 --link web02:web02 --link web03:web03
位置含义是否必填
左 web01被链接的源容器名(已存在的容器)必填
右 web01在新容器里用什么名字访问它可省,省略则等于左边

省略右边时,默认就用源容器名。

bash
docker run ... --link web01:web01
位置含义是不是一定要写
左 web01被链接的源容器名(已存在的容器)必填
右 web01在新容器里用什么名字访问它可省,省略就等于左边

省略右边等价:

bash
--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 发起方能连源容器)
  1. 只能在 default bridge 网络用:其他网络(host / none / 自定义)直接失效
  2. Docker 官方已废弃:未来版本会移除
  3. 启动顺序敏感:lb01 启动时,web01/02/03 必须已经存在

第 1 步:建网络 ​

bash
docker network create lnmp-net

任意名字都可。这是一个虚拟网桥,凡挂上来的容器互通。

bash
docker network ls
# 能看到:
# NETWORK ID     NAME       DRIVER    SCOPE
# abc123...      lnmp-net   bridge    local

第 2 步:所有容器接进来 ​

bash
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03

connect 可以把运行中的容器插到网络上,不需要重启。

第 3 步:启动 lb01,挂网络 ​

bash
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.11

lb01 nginx 访问 web01 时:

  1. 查 /etc/resolv.conf → 127.0.0.11
  2. Docker DNS 返回 web01 在 lnmp-net 上的 IP(如 172.18.0.3)
  3. 请求发过去

验证:

bash
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 推荐?❌ 已废弃✅ 推荐
bash
# 给 web01 多挂一个别名
docker network connect --alias api.local lnmp-net web01

# 启动时挂别名
docker run --network lnmp-net --network-alias api.local nginx

bash
# 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 就再也不会出现。



一句话本质: --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新容器内部

两个动作:

  1. 外侧:找到源容器(源容器必须先存在)
  2. 内侧:写 /etc/hosts

左右可以不一样:

bash
--link web01:phpbackend
# 进 lb01 后:cat /etc/hosts 会看到 "172.17.0.2  phpbackend"(没有 web01 这一行)

这台架构每个容器内的 /etc/hosts 长啥样 ​

lb02 启动后 ​

bash
docker exec lb02 cat /etc/hosts
127.0.0.1   localhost
172.17.0.5  php-fpm01
172.17.0.6  php-fpm02

web01 启动后(web02/03 一样) ​

bash
docker exec web01 cat /etc/hosts
127.0.0.1   localhost
172.17.0.4  lb02

lb01 启动后 ​

bash
docker exec lb01 cat /etc/hosts
127.0.0.1   localhost
172.17.0.2  web01
172.17.0.3  web02
172.17.0.4  web03

为啥这台架构所有东西都写主机名而不是 IP ​

容器 IP 是动态的:

bash
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。


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。


两边写不一样会变成啥样 ​

bash
# 例:web01 启动时改成 --link lb02:phpapp
docker run ... --link lb02:phpapp --name web01 ...

那 web01 内部 /etc/hosts:

172.17.0.4  phpapp      ← 改名了

web01 的 nginx 配置也得跟着改:

nginx
proxy_pass http://phpapp;   # 不再是 lb02

应用层(nginx 配置)用的名字 = link 时写的右半边。内外要一致。

你这套架构全用同名写法(外叫啥内也叫啥),本质是"省心"。


一句话总结 ​

--link 容器外:容器里 整个链路逻辑:

"去外侧把 源容器 找到,把它当前的 IP 拿到,把它和 容器内这个名字 绑成一条解析,塞进 新容器 的 /etc/hosts 里。"

"都是主机名" = 容器名当 hostname 用,省得 IP 写死,nginx 配置就写容器名,运行时 OS 查 /etc/hosts 拿当下 IP。


nginx 配置对应写法的猜测(你这套架构) ​

lb01 里的 nginx 配置(应该是 http 块或 stream 块) ​

nginx
upstream web_cluster {
    server web01:80;
    server web02:80;
    server web03:80;
}
server {
    listen 80;
    proxy_pass web_cluster;
}

web01/02/03 里的 nginx 配置(每个容器都一样的) ​

nginx
location / {
    proxy_pass http://lb02;
}

lb02 里的 nginx 配置 ​

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 的那个名字。



⚠️ --link 在默认 bridge 网络上已弃用(会有 WARNING),必须用自定义网络替代。

第 1 步:建网络 ​

bash
docker network create lnmp-net

任意名字都可。这是一个虚拟网桥,凡挂上来的容器互通。

bash
docker network ls
# 能看到:
# NETWORK ID     NAME       DRIVER    SCOPE
# abc123...      lnmp-net   bridge    local

第 2 步:所有容器接进来 ​

bash
docker network connect lnmp-net web01
docker network connect lnmp-net web02
docker network connect lnmp-net web03

connect 可以把运行中的容器插到网络上,不需要重启。

第 3 步:启动 lb01,挂网络 ​

bash
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.11

lb01 nginx 访问 web01 时:

  1. 查 /etc/resolv.conf → 127.0.0.11
  2. Docker DNS 返回 web01 在 lnmp-net 上的 IP(如 172.18.0.3)
  3. 请求发过去

验证:

bash
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 推荐?❌ 已废弃✅ 推荐
bash
# 给 web01 多挂一个别名
docker network connect --alias api.local lnmp-net web01

# 启动时挂别名
docker run --network lnmp-net --network-alias api.local nginx

完整的迁移步骤:

bash
# 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 就再也不会出现。



整体架构 ​

                    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
bash
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 配置(推测):

nginx
upstream php_backend {
    server php-fpm01:9000;
    server php-fpm02:9000;
}
location ~ \.php$ {
    proxy_pass http://php_backend;
}
bash
docker run -d --name web01 ... 
  --link lb02:lb02
  • 作用:让 web01-03 解析 lb02
  • 性质:web01-03 内的 nginx 把请求再反代给 lb02 处理 PHP
  • 依赖:lb02 必须先启动
bash
docker run -d --name lb01 ... 
  --link web01:web01 
  --link web02:web02 
  --link web03:web03
  • 作用:让 lb01 解析三台 web
  • 性质:正向——总入口转轮询到后端
  • 依赖:web01/02/03 必须先启动
容器link 目标必要?说明
lb02php-fpm01/02✅ 必须反代目标,靠 DNS 解析
web01/02/03lb02⚠️ 看你架构如果 web 直接处理 PHP 就可省
lb01web01/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 一次"

这套脚本很像练手用——把三层负载全部分离,每层独立容器,能看清每层干嘛。 生产里叫"过度架构",实验场景直观,生产去掉中间层。

php-fpm01/02 ──先起──┐
                     ↓ 必须存在
                   lb02 ──先起──┐
                                  ↓ 必须存在
                              web01/02/03 ──先起──┐
                                                  ↓ 必须存在
                                              lb01

整个 run.sh 严格串行,靠 sleep 3 凑合缓冲。某一台启动慢,后面全挂。

自定义网络方案:

bash
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 ...

启动顺序完全无关。

bash
docker rm -f web01          # ← link 关系一起销毁
docker run -d --link lb02:lb02 --name web01 ...

容器本身不持久化 link,每条 run 命令都要带。

正经做法:写 docker-compose.yml:

yaml
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-net

depends_on 表达依赖顺序,声明式而不是命令式。 docker compose up -d 一键起。


维度--link自定义网络
实现原理改 /etc/hosts(硬塞 IP 解析)Docker 内置 DNS(动态解析)
容器必须先存在?是(启动顺序敏感)否
能跨自定义网桥?否,只能 default bridge是
Docker 推荐?❌ 已废弃✅ 推荐

完整迁移命令:

bash
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(最实用) ​

bash
docker inspect --format='&#123;&#123;range .NetworkSettings.Networks}}&#123;&#123;.IPAddress}}&#123;&#123;end}}' web01
# 输出:172.17.0.2

--format 是 Go template,把字段抠出来。

多个容器一起查:

bash
docker inspect \
  --format='&#123;&#123;.Name}} -> &#123;&#123;range .NetworkSettings.Networks}}&#123;&#123;.IPAddress}}&#123;&#123;end}}' \
  web01 web02 web03

# /web01 -> 172.17.0.2
# /web02 -> 172.17.0.3
# /web03 -> 172.17.0.4

2. 完整 docker inspect(信息巨多) ​

bash
docker inspect web01
# 一坨 JSON:IP、挂载、端口、网络、环境变量……

太长的话 grep:

bash
docker inspect web01 | grep -i ipaddress

3. 自定义网络的 IP(多网络容器必看) ​

容器挂多个网络时,每个网络一个 IP,必须带网络名过滤:

bash
# 容器同时挂 default bridge 和 lnmp-net
docker inspect --format='&#123;&#123;.NetworkSettings.Networks.lnmp_net.IPAddress}}' web01

⚠️ 坑: lnmp-net 在 Go template 里点 . 写不了,自动替换成下划线(lnmp-net → lnmp_net)。

4. docker network inspect(从网络视角看所有容器) ​

bash
docker network inspect bridge          # default bridge
docker network inspect lnmp-net        # 自定义网络

输出 JSON 里有 Containers 字段,每个容器对应一个 IP。批量看最合适。

5. 一行列出所有容器的 IP + 状态 ​

bash
docker inspect \
  --format='&#123;&#123;.Name}} | &#123;&#123;range .NetworkSettings.Networks}}&#123;&#123;.IPAddress}}&#123;&#123;end}} | &#123;&#123;.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 | running

docker ps -aq 取所有容器 ID(包括停的)。

bash
docker exec lb01 cat /etc/hosts
# 这就是 --link 注入到容器内的解析表
# 能看到 Docker 把 link 源容器的 IP 写进去

总结:常用的就这 3 个 ​

命令用途
docker inspect --format '...' <容器>最常用,精确抠字段
docker network inspect <网络>看某网络上所有容器的 IP
docker exec <容器> cat /etc/hosts进容器看 --link 注入表

万能模板(多网络、要看状态都行):

bash
docker inspect --format='&#123;&#123;.Name}}: &#123;&#123;range .NetworkSettings.Networks}}&#123;&#123;.IPAddress}} &#123;&#123;end}}-&#123;&#123;.State.Status}}' <容器>


5. 容器持久化与开机自启 ​

5.1 为什么需要先运行临时容器? ​

podman generate systemd 需要读取已存在的容器配置来生成 .service 文件。

正确流程:

run 临时容器(配好所有参数) → generate systemd 生成服务文件 → 删除临时容器 → systemd 接管

5.2 完整操作流程(rootless 容器) ​

bash
# 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(持久化):

bash
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,包出去了回不来

完整解决步骤:

bash
# 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 测试容器(一行启动) ​

bash
# 启动一个临时 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_test

6.3 LNMP 容器化项目:挂载点的关键逻辑 ​

核心思想 ​

让容器本身"无状态",把配置、代码、数据全部从容器内部拎出来放到宿主机。

6.3.1 挂载点对照表 ​

挂载点对照表 ​

容器宿主机路径容器内路径挂的是啥
redis./redis/config/usr/local/etc/redisredis.conf 配置
redis./redis/data/dataRDB/AOF 持久化数据
mysql./mysql/config/etc/mysql/conf.d覆盖式配置(追加生效)
mysql./mysql/data/var/lib/mysql库、表、binlog
php-fpm01/02./blog/www/usr/share/nginx/htmlPHP 应用代码
php-fpm01/02./blog/php/usr/local/etcphp.ini + pool 配置
web01/02/03./blog/nginx/etc/nginxweb 容器的 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 ​

bash
redis-server --save 60 1 --loglevel warning
#   ├ 每 60 秒 ├ 至少 1 个 key 变更 → 触发 RDB
#   └ warning 日志级别

也可以写进 redis.conf 挂进去。命令行更直观,两条路都行。

3. ./blog/www 同时被 5 个容器挂载 ​

bash
./blog/www → /usr/share/nginx/html
# php-fpm01、php-fpm02、web01、web02、web03

意义:

  • 改一份代码,5 个容器全部生效
  • 不需要 re-build 镜像
  • 生产里这目录应该是 git pull + rsync,容器只是消费者

6.3.2 容器化部署的核心思想 ​

① 容器是"无状态"的 ​

容器随时可删,"会变的东西"必须留在容器外。

② 配置和数据生命周期不同,分开存放 ​

bash
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 ​

yaml
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 全起。

前面讲过,自定义网络替代 --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-playbookansible-navigator
执行环境宿主机本地容器内(Podman)
输出直接打到终端默认进交互界面,加 -m stdout 才打到终端
环境隔离❌✅
配置文件ansible.cfgansible-navigator.yml(指定容器镜像)
PATH 环境直接加载需要完整 PATH(su - devops 才能加载)

考试关键: 配置文件 ansible-navigator.yml 指定容器镜像地址,考试镜像在 registry.lab.example.com。

yaml
# ansible-navigator.yml
---
ansible-navigator:
  execution-environment:
    image: registry.lab.example.com/rhel9-ee:latest
  playbook-artifact:
    enable: true

常用命令:

bash
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 完整考试操作流程 ​

bash
# 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 ps

11.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>
生成 systemdpodman 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 指令详解 ​

语法:

dockerfile
ENV <key>=<value> ...

两个阶段都可用:

  • 构建时:RUN / CMD / ENTRYPOINT 里用 $VAR 引用
  • 运行时:作为环境变量传给容器内进程

ENV vs ARG 对比:

指令构建时运行时
ENV✅ 可用✅ 可用
ARG✅ 可用❌ 不可见

实战 Dockerfile 解析(ubuntu:26.04 sshd 镜像):

dockerfile
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 实际用法区分:

  1. DEBIAN_FRONTEND=noninteractive — apt 工具自动读取(约定俗成的隐性引用)
  2. TZ=Asia/Shanghai — Dockerfile 里显式 $TZ 设置时区 + 运行时透传给应用
  3. APP_VERSION=v1.0 — Dockerfile 里不引用,只在运行时给应用程序读

运行时验证:

bash
docker exec -it <容器> env                # 查看所有环境变量
docker exec -it <容器> echo $APP_VERSION  # 输出 v1.0

12.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 没有):

  1. 自动解压 tar 文件:ADD archive.tar.gz /app/
  2. URL 下载:ADD https://example.com/file /tmp/(不推荐,建议用 RUN curl)

12.3 ADD vs COPY ​

维度ADDCOPY
基础复制✅✅
自动解压 tar✅❌
URL 下载✅❌
官方推荐度需要解压/下载才用能 COPY 就 COPY

官方建议:能 COPY 就 COPY,需要解压 tar 或下载时才用 ADD。

12.4 EXPOSE 真相 ​

只是文档声明,告诉用户端口用途,不会真发布端口,必须 docker run -p 映射才生效。

dockerfile
EXPOSE 22 8000    # 仅文档作用,实际发布靠 -p

12.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/

/var/lib/docker/ 子目录:

overlay2/   ← 镜像和容器层都在这(最大头)
image/      ← 镜像元数据
volumes/    ← 匿名卷
containers/ ← 容器元数据
network/    ← 网络配置
tmp/

查看空间占用 ​

bash
# 一键看 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 占空间的四类 ​

  1. Images - 拉下来的镜像
  2. Containers - 创建过的容器层(rm 后还在)
  3. Local Volumes - 容器卷
  4. Build Cache - build 中间层(不用 build 几乎不占)

清理命令(按风险从小到大) ​

1. 清 dangling 镜像(标签 <none>) ​

bash
# dangling = 无主镜像,build 中间产物
docker images
# REPOSITORY   TAG       IMAGE ID
# <none>       <none>    abc123...  ← 这种

docker image prune      # 清 dangling
docker image prune -a   # 清 dangling + 所有无用的已命名镜像

2. 激进清(全清) ​

bash
docker system prune         # 清停止的容器/无用网络/dangling/构建缓存
docker system prune -a      # 上面的 + 所有未被容器用的镜像
# ⚠️ -a 威力大,会删 php:7.4-fpm 等没起容器的镜像

3. 按时间条件 ​

bash
docker image prune -a --filter "until=24h"    # 24h 前
docker image prune -a --filter "until=168h"   # 7 天前
docker builder prune --filter "until=24h"     # 构建缓存

4. 单独清某种资源 ​

bash
docker container prune   # 停止的容器
docker volume prune      # 无主卷   ⚠️ 会丢数据
docker network prune     # 无用网络

这台 LNMP 架构主要吃的空间 ​

资源估算处理建议
php:7.4-fpm~400MBimage prune -a
nginx:latest~180MBimage prune -a
mysql:5.7~500MB镜像大但常驻,清了还得拉
redis:7.4~130MB同上
容器停止后的层几百 MBsystem prune 一次清掉
dangling 镜像看 build 频率image prune 必清

没用 Dockerfile build,构建缓存基本不占。

直接 docker system prune -a 通常能回收 2-3GB。


搬 docker 到大盘(操作前要老大同意) ​

bash
# 1. 停 docker
systemctl stop docker

# 2. 同步到新盘(不是 mv,保留权限)
rsync -aP /var/lib/docker/ /mnt/newdisk/docker/

# 3. 改配置
vim /etc/docker/daemon.json
json
{
  "data-root": "/mnt/newdisk/docker"
}
bash
# 4. 起回去
systemctl start docker

# 5. 确认
docker info | grep "Docker Root Dir"

⚠️ 改 docker 根目录属于"系统/服务改动",按 MEMORY 铁律必须先经老大同意。



14. docker system df 输出解读 ​

不是定死的 ​

docker system df 的 SIZE 是当下实际占硬盘多少字节,每次执行现算的。

字段含义 ​

bash
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实际占磁盘字节数(动态算的)
RECLAIMABLEprune 后能省下多少

RECLAIMABLE 怎么算 ​

类型啥算"可回收"
Images没被任意容器引用(running/stopped 都不算占用)
Containers状态是 exited/dead
Volumes没被任意容器挂载
Build Cache永远可回收(下次 build 重生成)

⚠️ 坑:Container stopped 状态占的层不会自动释放,要 docker container prune 或 docker rm 才释放。

SIZE 何时变化 ​

操作SIZE 变化
docker pull xxxImages ↑
docker run xxx 容器内写文件Containers ↑
docker stop 容器Container SIZE 不变
docker rm -fContainer SIZE 可能不立即变小(layer 在 overlay)
docker compose up -d --buildBuild Cache ↑
docker image prune -aImages ↓
docker system prune各类 ↓

"用过的就还占着",prune 才释放。


ACTIVE 全 0 的解读 ​

bash
# 输出里 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)可能产生。

bash
docker volume ls
docker volume inspect <卷名> | grep Mountpoint
ls /var/lib/docker/volumes/<卷名>/_data/
# 看到 mysql/ redis 文件夹 → ⚠️ 是数据库数据,不能清

这个场景的清理顺序 ​

ACTIVE 全 0 → 全清风险低:

bash
# 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 ​

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 等价于:

bash
/bin/sh -c "touch /tmp/live; sleep 60; rm -rf /tmp/live; sleep 3600"
  1. 创建存活标记 /tmp/live
  2. 撑 60 秒
  3. 删除标记
  4. 长睡(进程还在,但标记没了)

livenessProbe 行为 ​

  • exec.command 跑 test -e /tmp/live:文件存在返回 0(活),不存在返回非 0(死)
  • initialDelaySeconds: 1:容器启动 1 秒后才开始探测
  • periodSeconds: 3:每 3 秒探测一次

完整循环:

  • 0s:容器启动 → 创建标记 → 探针通过
  • 1s 起:每 3s 探测一次
  • 60s 后:标记被删 → 探针失败累计到 failureThreshold(默认 3)次 → K8s 重启容器
  • 重启后脚本重新跑,标记又出现,循环往复

这是典型的 livenessProbe 演示用例:故意制造"假死",看 K8s 探针失败触发重启的效果。

探针参数速查 ​

字段默认值作用
initialDelaySeconds0容器启动后多久开始第一次探测
periodSeconds10探测间隔
timeoutSeconds1单次探测超时
failureThreshold3连续失败多少次判死
successThreshold1连续成功多少次判活(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 ​

yaml
readinessProbe:
  httpGet:
    port: 80
    path: /index2.html
    initialDelaySeconds: 1
    periodSeconds: 3
startupProbe:
  httpGet:
    path: /index1.html
    port: 80
    periodSeconds: 10
    failureThreshold: 30

nginx: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 ​

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 ​

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: 80

Pod 模板(template)怎么理解 ​

template = Pod 的样板/模具,告诉控制器"以后要补 Pod 就按这个规格创建"。

类比:模具(template) vs 产品(Pod);class(template) vs instance(Pod)。

关键细节:

  1. template.metadata.labels 必须包含 selector 的全部 label,否则 K8s 拒绝创建(防止孤儿 Pod)
  2. template.metadata.name 写不写无所谓:K8s 创建 Pod 时会强制清空,然后生成 rc-demo-xxxxx(带随机后缀)。最佳实践是不写
  3. 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 的隐藏陷阱 ​

  1. image: aaronxudocker/myapp:v1.0 — 私人镜像需要 imagePullSecrets,不然 ImagePullBackOff
  2. selector 只有 1 个 label — 生产里通常多个 label 区分版本/环境
  3. RC 已被淘汰 — K8s 1.18 后官方建议用 Deployment(自带滚动更新、回滚),但学 RC 是为了理解 Deployment 的底层

15.5 Service 删除命令详解 ​

最常用 ​

bash
kubectl delete svc <service-name>

svc = service 简写,完全等价。

批量/选择删除 ​

bash
# 一次删多个
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 文件删 ​

bash
kubectl delete -f service.yaml

需要强制删除吗? ​

不需要。Service 是集群内逻辑对象(不是进程,没有"清理"动作),删除立即生效,没有 grace period 概念。Pod 那种 -f --grace-period=0 --force 套路对 Service 不适用。

删除前的"看一眼" ​

bash
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:

yaml
selector:
  app: nginx

Service 会自动维护 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 ​

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 对象本身"贴的标签,用于:

  1. 给人查:kubectl get deploy -l app=myapp-deploy
  2. 给工具引用:Service Mesh(Istio/Linkerd)、监控、CI/CD 识别资源
  3. 跨 controller 关联:某些 operator 按 label 找 Deployment

绝对不会参与 Deployment → RS 的匹配。这是新手最大误区。

YAML 的隐藏陷阱 + 最佳实践 ​

yaml
metadata:
  labels:
    app: myapp-deploy     # ⚠️ 跟 selector 不一致,容易误导

虽然功能没问题,但非常容易把人搞糊涂。最佳实践:

yaml
# ✅ 推荐 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"来源 ​

实验现象 ​

bash
$ 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

详细步骤:

  1. Endpoints Controller(kube-controller-manager 组件)持续 watch Service selector 和 Pod labels
  2. 把匹配的 Pod IP 写入 Endpoints(v1.21+ 默认 EndpointSlice)
  3. kube-proxy(每 Node 跑一个)监听 Endpoints 变化
  4. 生成 iptables 规则(轮询负载均衡)
  5. 访问 ClusterIP 时 iptables DNAT 到 Pod IP
bash
$ 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 应用代码大概类似:

python
@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.0Docker Hub 仓库名镜像作者
Service 名 myappK8s 资源名你
Pod label app=myappK8s label 值你
返回字符串 "Myapp Version 1.0"应用代码硬编码镜像作者

K8s 永远不会把 Service 名、label 塞进 HTTP 响应 body。4 个"Myapp"只是恰好同名让你产生错觉。

⑤ 这套机制的"自愈"特性 ​

Pod IP 是动态的,Service 自动跟上:

Pod 重启 → IP 变了 → Endpoints Controller 检测 → 更新 Endpoints
  → kube-proxy 重新生成 iptables → 客户端无感知继续访问

这就是 Service 的核心价值:入口稳定,后端可换。

⑥ 验证方法 ​

bash
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 关键字段 ​

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 补新 Pod

Job 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 Failed6
backoff period失败后等多久再创建新 Pod(指数退避)初始 10s 翻倍

数法:每个退出码非 0 的 Pod 直接 +1 到 .status.failed,所以 N 个 Error Pod = .status.failed = N。 判定:.status.failed >= backoffLimit → Job 标 Failed,不再创建新 Pod。

"卡住"的真实原因(两个机制叠加):

  1. 失败后 backoff period 指数增长(10s → 20s → 40s → 80s...),看起来像卡住
  2. backoffLimit 累计达到 → Job 判 Failed → 停止创建 Pod,COMPLETIONS 永远卡在 <10

验证 backoffLimit 是否触发 ​

bash
# 看 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 更早反映真实状态

改进模板 ​

yaml
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 → 查 default namespace
  • -n kube-system → 查 kube-system namespace
  • -A = --all-namespaces → 查全部

4 个内置 namespace ​

Namespace作用
default默认,用户资源默认落这
kube-systemK8s 系统组件(calico、kube-proxy、coredns...)
kube-public公开可读,放集群公共信息
kube-node-lease节点心跳(1.14+),节点健康检测

常用 namespace 命令 ​

bash
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 ​

yaml
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 ​

维度普通 ServiceHeadless Service
clusterIP有(自动分配 VIP)None
DNS 解析单条 A 记录 → ClusterIP多条 A 记录 → 每个 Pod IP
负载均衡K8s 自动(iptables/IPVS)客户端自己做
配套 StatefulSet❌✅(标准用法)

核心区别:Headless 不要 VIP,直接把后端 Pod IP 全部暴露,客户端自己挑 Pod。

三种典型用途 ​

  1. StatefulSet 配套:每个 Pod 要稳定 DNS(如 mysql-0.mysql, mysql-1.mysql)
  2. 客户端自定义负载:要看到所有后端,自己选(轮询/最少连接/权重)
  3. 服务发现:拿所有 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 自动补全 ​

bash
nameserver 10.0.0.10                            # CoreDNS
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
  • search 列表:Pod 里 ping service-headliness(短域名)时自动按顺序补全
  • ndots:5:域名中点数 < 5 才走 search 补全

实际场景:StatefulSet 配 Headless Service ​

yaml
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: 3

DNS 解析能力:

  • mysql.default.svc.cluster.local → 3 个 Pod IP
  • mysql-0.mysql.default.svc.cluster.local → 单个 Pod 的稳定 IP(StatefulSet 核心)

验证 Headless ​

bash
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)

三个生活场景:

  1. 外卖平台挑厨师:顾客想自己选评价最高的 → Headless(系统告诉 3 个厨师,自己选)
  2. StatefulSet 稳定卡号:Pod 挂了重建名字不能变 → Headless(mysql-0 永远是 mysql-0)
  3. 监控系统数机器:要看所有后端 IP → Headless(一眼看到所有 Pod)

一句话速记: Headless = 关掉自动总机,给你员工名单自己打电话。


15.11 K8s Service 四种类型 DNS 对照 + ExternalName ​

K8s Service 4 种类型 DNS 对照 ​

类型dig 返回后端
ClusterIPA <ClusterIP> (1 条)集群内 Pod,K8s 代理
HeadlessA <PodIP1> A <PodIP2> A <PodIP3> (N 条)集群内 Pod,无代理
ExternalNameCNAME <外部域名> (链式解析)集群外服务,无代理
NodePort/LoadBalancerA <ClusterIP> (1 条)集群内 Pod,K8s 代理 + 端口暴露

"无 ClusterIP" 的 3 种 Service ​

类型ClusterIPselectorDNS 记录
HeadlessNone有多 A → Pod IP
ExternalName无无CNAME → 外部域名
无 selector Service有/无无A → 手动建 Endpoints

Headless vs ExternalName 对比 ​

维度HeadlessExternalName
目的暴露集群内 Pod暴露集群外外部服务
DNS 记录A 记录(Pod IP 列表)CNAME(别名到外部域名)
后端来源selector 选中的 PodexternalName 指定的域名
典型场景StatefulSet、稳定标识集群内访问外部 DB

共性:都是"无代理" Service

  • 都没 ClusterIP(<none>)
  • 都不走 iptables/IPVS 代理
  • 都是纯 DNS 层面的服务发现

ExternalName 详解 ​

yaml
spec:
  type: ExternalName
  externalName: www.baidu.com

dig 返回 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.188

ExternalName 特点:

  • 不分配 ClusterIP(EXTERNAL-IP 列显示外部域名)
  • 不需要 selector(没有 Pod 关联)
  • DNS 返回 CNAME 记录(域名别名)
  • 不转发流量,纯"域名翻译"

ExternalName 实战场景 ​

场景 1:集群内访问外部数据库(最常用)

yaml
apiVersion: v1
kind: Service
metadata:
  name: external-mysql
  namespace: prod
spec:
  type: ExternalName
  externalName: db.prod.example.com

Pod 里 mysql -h external-mysql.prod.svc.cluster.local → 连 db.prod.example.com好处:代码写死集群内域名,外部地址变了只改 Service。

场景 2:环境切换(测试/生产)

yaml
# 测试环境
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) │                         │           │          │
│ └───────┘                          └───────────┘          │
└────────────────────────────────────────────────────────────┘

推论:

  1. nginx 默认监听 0.0.0.0:80 → 自动监听在 Pod IP(eth0):80
  2. busybox 因共享 netns,curl localhost:80 就能访问 nginx
  3. Pod 内所有容器看到的 eth0 = localhost = 路由表 = iptables = Pod 级"网络面板"

containerPort 本质 ​

yaml
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 种方案 ​

yaml
# 方案 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普通虚拟机
IPPod 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(边车)模式 ​

完整双容器示例 ​

yaml
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: v1core 组不带 /v1xxx 前缀
kind: Pod资源类型最小调度单位
metadata.namePod 名字同 namespace 内唯一
spec.containers[]容器数组本 Pod 含 2 个容器
spec.containers[].ports端口声明containerPort 是声明性
spec.containers[].volumeMounts容器内挂载点挂哪个卷、挂哪里
spec.volumesPod 级卷定义整个 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 → 采集日志推 ELK
  • promtail → 采集日志推 Loki
  • 自定义 agent → 上报监控埋点
  • istio-proxy → Service Mesh 边车代理流量

EmptyDir 易踩坑清单 ​

  1. 生命周期 = Pod 生命周期:Pod 一删数据清空;持久化必须用 hostPath / PV / PVC
  2. 挂载会"遮蔽"容器原有目录:nginx 把卷挂到 /var/log/nginx 后,原有文件被藏(不是删,umount 后可见)
  3. 多容器共享 = 要考虑读写冲突:两边同时操作同份文件可能锁竞争/读写不一致
  4. 不跨节点:和 hostPath 不同,emptyDir 严格在 Pod 内共享;跨 Pod 共享得用 PV
  5. 默认存在节点本地磁盘:节点挂了数据丢;可指定 medium: Memory 走 tmpfs(飞快但吃内存,不算入容器内存 limit)
  6. 必须先在 volumes 里定义,容器才能挂:先有卷声明再有容器挂载
  7. 挂载路径建议用绝对路径:相对路径在不同镜像下行为不一致

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. 大小约 1~2 MB,只保留最常用的 Unix 工具
  2. 默认 shell 是 ash,不是 bash,/bin/sh 才是入口
  3. 不包含 bash、glibc、GNU coreutils,很多 Debian/Ubuntu 脚本直接搬过去会报缺命令
  4. alpine:xxx 镜像也用 ash(基于 busybox + musl libc),同一套坑

解决方案(三种写法任选) ​

yaml
# ✅ 写法 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 选型对照 ​

镜像家族默认 shellPod spec 里应该写
busyboxash/bin/sh
alpineash/bin/sh
debian / ubuntubash/bin/bash
centos / rhelbash/bin/bash

排查标准动作 ​

  1. 看 events:kubectl describe pod <name> → 找 Failed 行的 exec: "xxx" 信息
  2. 验证镜像里到底有没有这二进制:
    bash
    docker run --rm --entrypoint ls aaronxudocker/tools:busybox /bin/
    # 或
    docker run --rm aaronxudocker/tools:busybox which sh
  3. 临时进入容器测试:用 kubectl run --rm -it --image=busybox test -- sh 跑一下
  4. 改 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 触发 + 替换粒度 ​

命令本身 ​

bash
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):

yaml
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 / volumeMounts
  • spec.template.metadata.labels
  • spec.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:

yaml
metadata:
  annotations:
    version/config: "1.0"   # ConfigMap 改了 bump
    version/secret: "3"     # Secret 改了 bump
bash
# 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)
yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%         # 最多比期望多几副本(如 4 副本 → 允许 5)
      maxUnavailable: 25%   # 最多几副本不可用(4 副本 → 允许 1 挂)

替换粒度:Pod 级(不是容器级,不是节点级) ​

Q:同一个 yaml 里跑的节点,改了一个容器配置,滚动更新能用到所有容器上吗?

A:80% 对,中间有两个修正点:

✅ 理解对的点:

  • 改 yaml 后 apply,整个 Deployment 下所有 Pod 副本都会换成新版本
  • 最终状态 = 全部一致的新 Pod,不是「老的留老的、新的换新的」

❎ 需要修正的点:

  1. 「节点」说法偏差:Deployment 管的是 Pod 副本(ReplicaSet 下的 N 个 Pod),不是节点。一个节点可以跨多个 Deployment 跑多个 Pod
  2. 「只改某个容器」被重新加载:不能只替换 Pod 里某一个容器。Pod 整体哈希变了 → 整个 Pod 重建,所有容器都跟着重启(哪怕没改那个容器)
概念替换粒度
Deployment 滚动更新Pod 级(一个 Pod 一个 Pod 换)
Pod 内多容器共享资源不能只换单个容器
副本数滚动按 spec.replicas 而不是节点数

⚠️ 实战例子(双容器 Deployment):

yaml
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: nginx
        image: myapp:v1.0 → v2.0    # 改了
      - name: busybox                # 没改
        image: tools:busybox

apply 之后结果:

  • ✅ 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。

易踩坑清单 ​

  1. annotation key 含 / 不报错:K8s annotation key 允许带 /,用作命名空间分类(version/config 是常见用法)
  2. kubernetes.io/ 前缀禁止用户使用:保留前缀,会拒绝创建
  3. 同值改两次不触发:必须 v1.0 → v2.0 这种值变化才会触发 hash 变化
  4. kubectl rollout status deployment/<name> 比观察 Pod 状态更直观,是排查进度标准命令
  5. 回滚用 kubectl rollout undo deployment/<name>:回到上一个 Revision,因为旧 ReplicaSet 没被删(K8s 默认保留历史 RS)
  6. 滚动过程 Pod 数可能临时超出 replicas:因为有 maxSurge;批量调度也可能临时不足(maxUnavailable 允许少量挂)
  7. annotation 触发的"虚拟更新"会真的起停一遍 Pod:哪怕业务逻辑没变,所有 Pod 都会被替换一次(生产环境注意流量抖动)

15.16 Pod 调度策略:反亲和性 + Node 污点与 Toleration ​

A. Pod 反亲和性 podAntiAffinity ​

完整 YAML ​
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
易踩坑 ​
  1. 节点数不够会 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
  2. 不命中的 Pod 不参与计算:label selector 命不中的 Pod scheduler 不看
  3. 影响的是同拓扑域的「别的 Pod」,不是自己:本 Pod 自己也带 podenv=pro 也行,约束的是避开别的同类 Pod
  4. IgnoredDuringExecution 不能省:跟 required 配对的全名就是这个,缩写不合法
  5. matchExpressions 是数组:多条表达式是 AND 关系
    yaml
    matchExpressions:
    - key: podenv
      operator: In
      values: ["pro"]
    - key: app
      operator: In
      values: ["nginx"]
    # 表示:app=nginx AND podenv=pro
  6. Pod 反亲和是调度阶段控制:不会让已运行的 Pod 被驱逐
  7. 多 namespace 下的 Pod 也起作用:label selector 跨 namespace 查(指定 namespaces 除外)

B. Node 污点(Taints)+ Pod Toleration ​

4 种查看方法 ​
bash
# ① 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 字段格式 ​
yaml
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 ​
bash
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 ​
bash
# 设置
kubectl taint nodes node01 dedicated=gpu:NoSchedule
# 去掉(加后缀 -)
kubectl taint nodes node01 dedicated=gpu:NoSchedule-
Taint + Toleration 原理(单向门 + 钥匙) ​

Node 端(抹黄油、贴反门):

bash
kubectl taint nodes node01 dedicated=gpu:NoSchedule

Pod 端(带钥匙 → 才让进):

yaml
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 才能用。

易踩坑清单 ​
  1. master 节点有隐性污点:kubectl get nodes 看 master 是 <none>,其实系统默认加了 node-role.kubernetes.io/master:NoSchedule,必须 describe 才看全
  2. NoExecute 立刻驱逐:不加 toleration 直接 NoExecute,在跑 Pod 立马被踢 → 服务中断
  3. tolerationSeconds 只对 NoExecute 有效:其他两种 effect 忽略这字段
  4. toleration 不能 Partial 匹配:写 key=A 不等于容忍 key=A,value=B;要么 Equal + value 写全,要么用 Exists 只匹配 key
  5. toleration 是「允许」不是「调度」:带 toleration ≠ 一定被调上去,只是「不会因为这个 taint 拒绝」
  6. effect 不写会报错:系统识别不到这个 taint
  7. NoSchedule 不驱逐老 Pod:只挡新 Pod;要驱逐用 NoExecute
  8. 可以同时加多种 taint:一个 Node 可多个 taint,调度时 Pod 需对每个 taint 都有对应 toleration

小弟二号打造的知识体系