mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-30 07:33:45 +00:00
Update Docker.md
补充 Dockerfile 与构建缓存、多阶段构建、常用指令的区别、端口与数据卷与网络、容器问题排查、Pod 的创建过程、Service 与 Ingress、Pod 异常状态排查,增加配图和实测附录;“实现原理”移到“镜像与容器”之前。
This commit is contained in:
+678
-24
@@ -5,9 +5,10 @@
|
||||
* [二、与虚拟机的比较](#二与虚拟机的比较)
|
||||
* [三、优势](#三优势)
|
||||
* [四、使用场景](#四使用场景)
|
||||
* [五、镜像与容器](#五镜像与容器)
|
||||
* [六、实现原理](#六实现原理)
|
||||
* [五、实现原理](#五实现原理)
|
||||
* [六、镜像与容器](#六镜像与容器)
|
||||
* [七、Kubernetes](#七kubernetes)
|
||||
* [附录:实测](#附录实测)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -70,17 +71,7 @@ Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Doc
|
||||
|
||||
Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。
|
||||
|
||||
## 五、镜像与容器
|
||||
|
||||
镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。
|
||||
|
||||
镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
|
||||
|
||||
构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。
|
||||
|
||||
<div align="center"> <img src="pics/docker-layers.png" width="740px"/> </div><br>
|
||||
|
||||
## 六、实现原理
|
||||
## 五、实现原理
|
||||
|
||||
容器就是宿主机上的普通进程,Docker 用 Linux 内核的两个机制把它和其它进程隔开:
|
||||
|
||||
@@ -101,11 +92,178 @@ $ echo $$
|
||||
|
||||
因为所有容器共享宿主机的内核,所以容器比虚拟机轻量,但隔离性也更弱:内核漏洞可能让容器中的进程影响宿主机。
|
||||
|
||||
## 六、镜像与容器
|
||||
|
||||
镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。
|
||||
|
||||
镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
|
||||
|
||||
构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。
|
||||
|
||||
<div align="center"> <img src="pics/docker-layers.png" width="740px"/> </div><br>
|
||||
|
||||
分层由联合文件系统实现,Docker 默认使用 OverlayFS:镜像的各层是只读的下层目录(lowerdir),容器的可写层是上层目录(upperdir),两者合并后挂载为容器的根目录。
|
||||
|
||||
容器第一次修改镜像中的某个文件时,先把整个文件复制到上层再修改,这就是写时复制;删除文件时在上层放一个同名的白障(whiteout)文件把它遮住。镜像本身始终不变,所以同一个镜像可以同时启动多个容器。
|
||||
|
||||
例如在订单服务的容器中修改 /etc/alpine-release、删除 /etc/motd 后,docker diff 显示 C /etc/alpine-release 和 D /etc/motd,上层目录中多了 alpine-release 的完整副本和一个名为 motd 的白障文件(Docker 28.4 实测,见[附录 A3](#a3-overlayfs-的可写层))。
|
||||
|
||||
可写层随容器一起删除,需要长期保存的数据(例如 MySQL 的数据文件)要放在数据卷中,见下文。
|
||||
|
||||
### 用 Dockerfile 构建镜像
|
||||
|
||||
Dockerfile 是描述怎样构建镜像的文本文件,每条指令在上一步的结果上执行,修改文件系统的指令(RUN、COPY、ADD)各产生一层。
|
||||
|
||||
下面把一个 Spring Boot 3 订单服务打包成镜像,它提供 GET /orders/{id} 接口,返回订单的状态:
|
||||
|
||||
```dockerfile
|
||||
# 第一阶段:用 Maven 和 JDK 编译打包
|
||||
FROM maven:3.9-eclipse-temurin-21 AS build
|
||||
WORKDIR /src
|
||||
COPY pom.xml .
|
||||
RUN mvn -q dependency:go-offline
|
||||
COPY src ./src
|
||||
RUN mvn -q package -DskipTests
|
||||
|
||||
# 第二阶段:只把 jar 复制到 JRE 镜像中
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /src/target/order-service-1.0.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
```
|
||||
|
||||
其中用到的指令:
|
||||
|
||||
- FROM:指定基础镜像,AS build 给这个构建阶段命名;
|
||||
- WORKDIR:设置后续指令的工作目录,目录不存在时自动创建;
|
||||
- COPY:把构建上下文(docker build 指定的目录)中的文件复制到镜像中,--from=build 表示从前一个阶段复制;
|
||||
- RUN:构建镜像时执行命令,执行结果保存为新的一层;
|
||||
- EXPOSE:说明容器监听的端口,只起文档作用,运行时仍要用 -p 发布端口;
|
||||
- ENTRYPOINT:容器启动时执行的命令。
|
||||
|
||||
在 Dockerfile 所在目录执行 docker build -t order-service:1.0 .(最后的点表示把当前目录作为构建上下文),就得到了名为 order-service、标签为 1.0 的镜像。
|
||||
|
||||
#### 1. 构建缓存
|
||||
|
||||
构建时 Docker 逐条检查指令:指令本身和它用到的文件(COPY 按文件内容计算校验和)都与上次相同,就直接复用上次构建的层。某一层失效后,它之后的所有层都要重新构建。
|
||||
|
||||
所以变化少的步骤要放在前面。上面的 Dockerfile 先只复制 pom.xml 并下载依赖,再复制源码,只改源码时下载依赖的那一层仍然命中缓存。
|
||||
|
||||
首次构建用了 214.6 秒,其中下载依赖 209.6 秒;改一行源码后重新构建只用 6.4 秒。如果一开始就复制全部文件再执行 mvn package,每次改源码都要重新下载依赖,用了 107.5 秒(Docker 28.4 实测,见[附录 A1](#a1-构建缓存与多阶段构建))。
|
||||
|
||||
<div align="center"> <img src="pics/docker-build-cache.png" width="780px"/> </div><br>
|
||||
|
||||
构建上下文中不需要的文件(例如 target、.git 目录)写进 .dockerignore,既减少发送给 Docker 的数据,也避免它们的变化让缓存失效。
|
||||
|
||||
#### 2. 多阶段构建
|
||||
|
||||
编译需要 Maven 和 JDK,运行只需要 JRE 和 jar 包。多阶段构建(multi-stage build)在一个 Dockerfile 中写多个 FROM,每个 FROM 开始一个阶段,最终的镜像只包含最后一个阶段的内容,前面阶段中的 Maven、源码和下载的依赖都不会进入最终镜像。
|
||||
|
||||
上面的订单服务镜像解压后约 236 MB,主要是 JRE 162 MB 和 jar 包 24 MB;在 Maven 镜像中一个阶段完成构建和运行,镜像约 680 MB,多出了 JDK、Maven 和构建时下载的依赖(见[附录 A1](#a1-构建缓存与多阶段构建))。镜像越小拉取越快,包含的工具越少,可被利用的漏洞也越少。
|
||||
|
||||
减小镜像的其它做法:
|
||||
|
||||
- 选择更小的基础镜像,例如 -alpine、-slim 版本;
|
||||
- 把下载、安装和清理写在同一条 RUN 中,因为在后面的层中删除文件只是把它遮住,前面的层并没有变小。
|
||||
|
||||
#### 3. 常用指令的区别
|
||||
|
||||
- COPY 与 ADD:COPY 只复制本地文件;ADD 还能下载远程 URL 和 Git 仓库、自动解压本地的 tar 包。只是复制文件时用 COPY,行为明确;
|
||||
- RUN 与 CMD、ENTRYPOINT:RUN 在构建镜像时执行,结果写入镜像;CMD 和 ENTRYPOINT 只是记录容器启动时要执行的命令;
|
||||
- CMD 与 ENTRYPOINT:docker run 镜像名之后的参数会替换 CMD,而追加到 ENTRYPOINT 之后。例如 docker run order-service:1.0 --server.port=9090 启动后,容器中的 1 号进程是 java -jar app.jar --server.port=9090。通常 ENTRYPOINT 写固定的命令,CMD 写可以被覆盖的默认参数。
|
||||
|
||||
ENTRYPOINT 和 CMD 应写成 JSON 数组的 exec 形式。写成 shell 形式(例如 ENTRYPOINT java -jar app.jar)时,命令由 /bin/sh -c 启动,Shell 成为 1 号进程时不会把 docker stop 发出的 SIGTERM 转发给 Java,Docker 等待 10 秒后改用 SIGKILL 强制结束,应用来不及优雅停止。
|
||||
|
||||
例如 ENTRYPOINT java -jar app.jar; echo "order-service exited" 的容器停止用了 10.3 秒,退出码 137;exec 形式只用 0.7 秒,退出码 143,日志中有 Spring Boot 的 Graceful shutdown complete(见[附录 A2](#a2-exec-形式与-shell-形式))。需要注意的是,Alpine 的 sh 在命令只有一条时会直接用它替换自己,这时 Java 仍然是 1 号进程。
|
||||
|
||||
### 运行容器:端口、数据卷与网络
|
||||
|
||||
```sh
|
||||
docker run -d --name order -p 8080:8080 order-service:1.0
|
||||
```
|
||||
|
||||
-d 表示在后台运行,--name 给容器命名,-p 宿主机端口:容器端口 把宿主机 8080 端口收到的连接转发到容器的 8080 端口。
|
||||
|
||||
每个容器有自己的网络命名空间,两个容器都监听 8080 不会冲突,冲突的是宿主机端口。再用 -p 8080:8080 启动第二个容器时报错 Bind for 0.0.0.0:8080 failed: port is already allocated,改用 -p 8081:8080 即可(见[附录 A4](#a4-端口数据卷网络与-compose))。同样,宿主机上已有 MySQL 占用 3306 时,容器中的 MySQL 可以映射为 -p 3307:3306;端口被哪个进程占用可以用 ss -lntp 或 lsof -i :3306 查看。
|
||||
|
||||
#### 1. 数据卷
|
||||
|
||||
数据卷(volume)是由 Docker 管理、独立于容器生命周期的目录,用 -v 卷名:容器内路径 挂载。例如把 MySQL 的数据目录放在数据卷 mysql-data 中:
|
||||
|
||||
```sh
|
||||
docker run -d --name mysql -p 3316:3306 -v mysql-data:/var/lib/mysql \
|
||||
-e MYSQL_ROOT_PASSWORD=Root2026 mysql:8.0
|
||||
```
|
||||
|
||||
在其中建表并插入订单 1024,执行 docker rm -f mysql 删除容器,再用同样的命令创建新容器,订单 1024 仍然在(见[附录 A4](#a4-端口数据卷网络与-compose))。
|
||||
|
||||
-v 后面写宿主机的绝对路径时是绑定挂载(bind mount),例如 -v /data/mysql:/var/lib/mysql,宿主机上可以直接查看和备份这些文件,配置文件也常用这种方式挂载进容器。数据卷不依赖宿主机的目录结构,是官方推荐的持久化方式。
|
||||
|
||||
#### 2. 网络
|
||||
|
||||
Docker 默认使用 bridge 网络:每个容器得到一个私有 IP,通过宿主机上的虚拟网桥 docker0 互相通信,访问外部时做 NAT,-p 则是在宿主机上添加了一条转发规则。其它常见的网络模式:
|
||||
|
||||
- host:容器直接使用宿主机的网络命名空间,不需要端口映射,但没有网络隔离;
|
||||
- none:容器只有回环网卡;
|
||||
- overlay:把多台宿主机上的容器连成一个网络,用于 Docker Swarm 等跨主机的场景。
|
||||
|
||||
容器之间要按名字访问。默认 bridge 网络中的容器只能用 IP 互访,而 IP 在容器重建后会变;用 docker network create shop 创建的网络带有内置的 DNS,加入该网络的容器可以用容器名访问,例如 mysql 解析为 172.19.0.2,在默认网络中则解析失败(见[附录 A4](#a4-端口数据卷网络与-compose))。
|
||||
|
||||
需要注意的是,容器中的 localhost 指容器自己,访问宿主机上的服务要用宿主机的 IP(Docker Desktop 中可以用 host.docker.internal)。
|
||||
|
||||
#### 3. Docker Compose
|
||||
|
||||
多个容器一起部署时,用 Docker Compose 把上面的参数写进一个 YAML 文件,docker compose up -d 一次启动全部容器。Compose 自动创建一个网络,服务名就是容器的主机名:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: Root2026
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-pRoot2026"]
|
||||
interval: 3s
|
||||
retries: 20
|
||||
order:
|
||||
image: order-service:1.0
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
mysql-data:
|
||||
```
|
||||
|
||||
depends_on 默认只保证依赖的容器先启动,不等它能提供服务;加上 condition: service_healthy 后,等 MySQL 的健康检查通过才启动订单服务,实测订单服务比 MySQL 晚约 9.6 秒启动(见[附录 A4](#a4-端口数据卷网络与-compose))。Compose 只管理单台机器上的容器,跨机器的调度、自愈和扩缩容由 Kubernetes 负责。
|
||||
|
||||
### 容器的常见问题排查
|
||||
|
||||
常用命令:
|
||||
|
||||
- docker ps -a:列出包括已退出容器在内的所有容器,状态一栏带有退出码,例如 Exited (1);
|
||||
- docker logs -f --tail 100 order:查看容器的标准输出和标准错误,所以容器中的应用应把日志打印到标准输出;
|
||||
- docker inspect order:查看容器的完整配置和状态,例如退出码、是否因内存不足被杀(State.OOMKilled)、挂载的目录和 IP;
|
||||
- docker exec -it order sh:在运行中的容器里执行命令,例如查看配置文件、测试网络是否连通;
|
||||
- docker stats:实时查看每个容器的 CPU、内存、网络和磁盘 I/O,例如订单服务空闲时内存约 160 MiB;
|
||||
- docker system df:查看镜像、容器、数据卷和构建缓存占用的磁盘,docker image prune -a 删除没有被任何容器使用的镜像。
|
||||
|
||||
常见问题:
|
||||
|
||||
- 容器启动后立即退出:容器的生命周期跟随 1 号进程,主进程结束容器就退出。例如启动 MySQL 时没有设置 MYSQL_ROOT_PASSWORD,容器状态为 Exited (1),docker logs 中列出了需要设置的环境变量(见[附录 A4](#a4-端口数据卷网络与-compose));把服务放到后台运行的启动命令(例如 nginx 不加 daemon off)也会让主进程立即结束;
|
||||
- 退出码 137:进程被 SIGKILL 杀死(128 + 9),常见原因是超过内存限制被内核杀掉,或者 docker stop 等待超时;143 是收到 SIGTERM 后退出;
|
||||
- 宿主机上能运行,容器中不能:常见原因是容器中缺少依赖的库或命令(Alpine 使用 musl 而不是 glibc)、镜像的 CPU 架构与机器不一致(arm64 与 amd64)、程序连接的 localhost 指向了容器自己、文件权限不同;
|
||||
- Docker 守护进程重启:默认情况下守护进程退出时会停止所有容器,开启 live-restore 后容器在守护进程重启期间继续运行;需要自动恢复的容器用 --restart unless-stopped 启动。
|
||||
|
||||
## 七、Kubernetes
|
||||
|
||||
Docker 解决的是单台机器上怎样运行容器。服务有几十个、部署在几十台机器上时,还需要决定每个容器放在哪台机器上、挂了谁来重启、怎样扩容、怎样不停机升级,这就是 Kubernetes(K8s)做的事情。
|
||||
|
||||
K8s 的核心思想是声明期望的状态,由控制器不断把实际状态调整为期望状态。例如下面的配置声明 order-service 要运行 3 个副本,并通过一个 Service 对外提供访问:
|
||||
K8s 的核心思想是声明期望的状态,由控制器不断把实际状态调整为期望状态。例如下面的配置声明订单服务要运行 3 个副本,并通过一个 Service 提供统一的访问入口:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
@@ -124,11 +282,16 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: nginx:1.27-alpine
|
||||
image: order-service:1.0
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- containerPort: 8080
|
||||
resources:
|
||||
requests: { cpu: 250m, memory: 256Mi }
|
||||
limits: { cpu: "1", memory: 512Mi }
|
||||
readinessProbe:
|
||||
httpGet: { path: /, port: 80 }
|
||||
httpGet: { path: /actuator/health/readiness, port: 8080 }
|
||||
livenessProbe:
|
||||
httpGet: { path: /actuator/health/liveness, port: 8080 }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -139,23 +302,492 @@ spec:
|
||||
app: order-service
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
targetPort: 8080
|
||||
```
|
||||
|
||||
其中的几个概念:
|
||||
|
||||
- Pod:K8s 调度的最小单位,包含一个或几个共享网络和存储的容器,每个 Pod 有自己的 IP,重建后 IP 会变;
|
||||
- Pod:K8s 调度的最小单位,包含一个或几个容器,每个 Pod 有自己的 IP,重建后 IP 会变。同一个 Pod 中的容器共享网络、IPC 和 UTS 命名空间,可以用 localhost 互相访问、主机名相同,也可以挂载同一个存储卷,但默认不共享 PID 命名空间(见[附录 A6](#a6-pod-的创建资源限制与共享的命名空间));
|
||||
- Deployment:管理一组相同的 Pod,负责保持副本数、扩缩容和滚动升级;
|
||||
- Service:为一组 Pod 提供固定的虚拟 IP 和域名,把请求负载均衡到其中就绪的 Pod 上;
|
||||
- 控制平面:API Server 接收所有请求,etcd 保存集群状态,调度器决定 Pod 放在哪个节点,控制器管理器运行各种控制器;每个节点上的 kubelet 负责启动和检查本节点的容器。
|
||||
|
||||
在单节点 K8s(k3s v1.31)上实际操作的结果如下:
|
||||
在两个节点的 K8s 集群(k3s v1.31.4)上实际操作的结果如下(见[附录 A5](#a5-部署自愈与滚动升级)):
|
||||
|
||||
- kubectl apply 之后约 20 秒,3 个 Pod 全部就绪,Service 得到虚拟 IP 10.43.189.89,在集群内用 http://order-service 就能访问;
|
||||
- 删除一个 Pod,Deployment 创建的 ReplicaSet 发现副本数少于 3,立即创建新的 Pod,约 1.2 秒后恢复为 3/3,新 Pod 的 IP 与原来不同,Service 自动更新了后端列表;
|
||||
- kubectl scale 把副本数改为 5;kubectl set image 修改镜像版本后,Deployment 创建新的 ReplicaSet,逐个用新版本的 Pod 替换旧的,约 12 秒完成滚动升级,期间一直有就绪的 Pod 在提供服务,需要时可以用 kubectl rollout undo 回滚。
|
||||
- kubectl apply 之后约 21 秒,3 个 Pod 全部就绪,分布在两个节点上,Service 得到虚拟 IP 10.43.210.38,在集群内用 http://order-service 就能访问,请求被分到不同的 Pod;
|
||||
- 删除一个 Pod,Deployment 创建的 ReplicaSet 发现副本数少于 3,0.3 秒内创建了新的 Pod,约 11 秒后新 Pod 启动完成并通过就绪检查,恢复为 3/3,新 Pod 的 IP 与原来不同,Service 自动更新了后端列表;
|
||||
- kubectl scale 修改副本数;kubectl set image 把镜像改为 order-service:1.1 后,Deployment 创建新的 ReplicaSet,逐个用新版本的 Pod 替换旧的,约 33 秒完成滚动升级,期间一直有就绪的 Pod 在提供服务,需要时可以用 kubectl rollout undo 回滚。
|
||||
|
||||
readinessProbe 是就绪探针:Pod 启动后要通过检查才会加入 Service 的后端列表,避免把请求转发给还没启动完成的实例。
|
||||
readinessProbe 是就绪探针:Pod 启动后要通过检查才会加入 Service 的后端列表,避免把请求转发给还没启动完成的实例。livenessProbe 是存活探针:连续失败 3 次(默认值)时 kubelet 重启容器,用于发现进程还在、但已经不能工作的情况,例如死锁。Spring Boot 的 Actuator 在 K8s 中自动提供 /actuator/health/readiness 和 /actuator/health/liveness 两个端点。
|
||||
|
||||
删除 Pod(缩容、滚动升级)时,控制平面把它从 Service 的后端中摘除,同时 kubelet 向容器的 1 号进程发送 SIGTERM,30 秒(terminationGracePeriodSeconds 的默认值)后进程还没退出就发送 SIGKILL。应用收到 SIGTERM 后应停止接收新请求、处理完已有请求再退出,Spring Boot 3.4 起默认开启这种优雅停机。摘除后端需要一点时间才能传到每个节点,所以常用 preStop 钩子先等待几秒(例如 sleep 5),再让应用开始停止。
|
||||
|
||||
### Pod 的创建过程
|
||||
|
||||
K8s 的各组件不直接互相调用,而是都通过 API Server 协作:每个组件监听(watch)自己关心的对象,发现实际状态与期望不一致就采取行动,再把结果写回 API Server。以订单服务中被调度到节点 d1-agent 的一个 Pod 为例:
|
||||
|
||||
<div align="center"> <img src="pics/k8s-pod-create.png" width="860px"/> </div><br>
|
||||
|
||||
1. kubectl 把 Deployment 发给 API Server,API Server 完成认证、鉴权和准入检查后写入 etcd;
|
||||
2. 控制器管理器中的 Deployment 控制器发现新的 Deployment,创建 ReplicaSet;ReplicaSet 控制器发现副本数不足,创建 3 个 Pod 对象,这时的 Pod 还没有指定节点;
|
||||
3. 调度器发现未调度的 Pod,先过滤掉不满足条件的节点(资源不足、有不能容忍的污点等),再给剩下的节点打分,选出 d1-agent 写入 Pod 的 nodeName;
|
||||
4. d1-agent 上的 kubelet 发现分配给自己的 Pod,通过容器运行时接口(Container Runtime Interface, CRI)让 containerd 创建 Pod 沙箱:先启动一个 pause 容器持有网络命名空间,再调用 CNI 插件为它配置网卡、分配 IP 10.42.1.5;Pod 声明了存储卷时,还会通过 CSI 插件把卷挂载到节点上;
|
||||
5. kubelet 拉取镜像(这里镜像已在节点上),创建并启动容器;
|
||||
6. kubelet 执行探针,就绪探针通过后把 Pod 的状态更新为 Ready;
|
||||
7. EndpointSlice 控制器把 10.42.1.5:8080 加入 order-service 的后端,各节点的 kube-proxy 随之更新转发规则。
|
||||
|
||||
从提交到这个 Pod 就绪用了 21 秒:创建对象和调度约 1 秒,创建容器 11 秒,Spring Boot 启动和就绪探针 9 秒(见[附录 A6](#a6-pod-的创建资源限制与共享的命名空间))。创建容器较慢是因为实验中的节点运行在 Docker 容器里,containerd 只能用逐个复制文件的 native 快照准备根文件系统。kubectl describe pod 末尾的 Events 按顺序记录了 Scheduled、Pulled、Created、Started 等事件,Pod 起不来时先看这里。
|
||||
|
||||
YAML 中的 resources 声明了 Pod 需要的资源:
|
||||
|
||||
- requests 是调度的依据,调度器只把 Pod 放到剩余可分配资源不少于 requests 的节点上,CPU 紧张时也按 requests 的比例分配 CPU 时间;
|
||||
- limits 是运行时的上限,由 kubelet 写入容器的 cgroup。订单服务的 limits 是 1 个 CPU 和 512 MiB,容器中的 cpu.max 为 100000 100000,memory.max 为 536870912(见[附录 A6](#a6-pod-的创建资源限制与共享的命名空间))。
|
||||
|
||||
超过 CPU 上限的进程被限流,变慢但不会被杀;超过内存上限的进程被内核杀掉,状态为 OOMKilled。
|
||||
|
||||
### Service 与 Ingress
|
||||
|
||||
Pod 的 IP 会变,调用方不能直接写 Pod 的 IP。Service 用标签选出一组 Pod,提供一个固定的虚拟 IP(ClusterIP)和域名 order-service(完整域名 order-service.default.svc.cluster.local)。EndpointSlice 控制器维护其中就绪 Pod 的地址,每个节点上的 kube-proxy 把它们写成 iptables 或 IPVS 规则。
|
||||
|
||||
订单服务所在的节点上,与 order-service 相关的 iptables 规则是:发往 10.43.210.38:80 的包,以 1/3 的概率 DNAT 到 10.42.0.8:8080,剩下的以 1/2 的概率 DNAT 到 10.42.1.4:8080,其余 DNAT 到 10.42.1.5:8080,三个 Pod 被选中的概率相同(见[附录 A7](#a7-service-与-ingress))。虚拟 IP 没有绑定在任何网卡上,只存在于每个节点的转发规则中。
|
||||
|
||||
Service 按访问范围分为几种类型:
|
||||
|
||||
- ClusterIP:默认类型,只能在集群内访问;
|
||||
- NodePort:在 ClusterIP 的基础上,在每个节点上打开同一个端口(默认范围 30000~32767),访问任一节点的这个端口都会转发到后端的 Pod,例如订单服务被分配了 31887 端口;
|
||||
- LoadBalancer:在 NodePort 的基础上,由云厂商创建一个外部负载均衡器,把流量转发到各节点;
|
||||
- ExternalName:把服务名映射为一个外部域名(DNS 的 CNAME 记录),不做转发。
|
||||
|
||||
Service 工作在传输层,只认 IP 和端口。对外提供多个 HTTP 服务时,每个服务一个 LoadBalancer 成本高,也不能按域名和路径分发,这时用 Ingress。Ingress 是一组 HTTP 路由规则,例如把 shop.example.com/orders 转发到 order-service:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: shop
|
||||
spec:
|
||||
rules:
|
||||
- host: shop.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /orders
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: order-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
Ingress 只是规则,真正处理请求的是 Ingress 控制器,例如 Ingress-NGINX、Traefik(k3s 自带),它们本身作为 Pod 运行,通过 LoadBalancer 或 NodePort 类型的 Service 对外暴露。实测中带 Host: shop.example.com 访问 /orders/1024 返回订单数据,访问 /users/1 或换成其它域名都返回 404(见[附录 A7](#a7-service-与-ingress))。
|
||||
|
||||
<div align="center"> <img src="pics/k8s-traffic.png" width="800px"/> </div><br>
|
||||
|
||||
需要注意的是,Ingress 控制器通常不经过 Service 的虚拟 IP,而是从 EndpointSlice 取得 Pod 的地址后直接转发给 Pod(Traefik 的默认行为),Service 在这里只用来找到后端。Ingress API 已经冻结,不再增加新功能,新功能加在它的后继 Gateway API 中。
|
||||
|
||||
Pod 之间的通信由 CNI 插件实现。K8s 的网络模型要求每个 Pod 有集群内唯一的 IP,任意两个 Pod 不经过 NAT 就能直接通信。k3s 默认的 Flannel 给每个节点分配一个网段(实验中 d1-server 上的 Pod 是 10.42.0.x,d1-agent 上的是 10.42.1.x),跨节点的包用 VXLAN 封装后经节点之间的网络发送。
|
||||
|
||||
### Pod 异常状态的排查
|
||||
|
||||
Pod 的阶段(phase)只有 Pending、Running、Succeeded、Failed、Unknown 五种,kubectl get pods 的 STATUS 一栏显示得更细。常见的异常状态(k3s 实测,见[附录 A8](#a8-pod-的异常状态)):
|
||||
|
||||
- Pending:Pod 还没有调度到节点上。describe 的事件中有 FailedScheduling 时是调度失败,例如申请 20 个 CPU 时为 0/2 nodes are available: 2 Insufficient cpu;两个节点都有污点 dedicated=db:NoSchedule、Pod 没有对应的容忍时为 2 node(s) had untolerated taint;Pod 使用的 PVC 没有绑定到存储卷时也会停在这里;
|
||||
- ImagePullBackOff:拉取镜像失败,常见原因是镜像名或标签写错、私有仓库没有配置凭证(imagePullSecrets)、节点访问不了镜像仓库,kubelet 在 ErrImagePull 和 ImagePullBackOff 之间反复重试;
|
||||
- CrashLoopBackOff:容器启动后反复退出,kubelet 每次重启前的等待时间按 10 秒、20 秒、40 秒翻倍,最长 5 分钟。kubectl logs --previous 查看上一次退出前的日志,例如把 server.port 写成 abc 时,日志中是 APPLICATION FAILED TO START 和具体的配置错误,退出码 1;
|
||||
- OOMKilled:容器超过内存上限被内核杀掉,退出码 137,之后同样进入 CrashLoopBackOff。例如内存上限 256 MiB,JVM 堆却设为 300 MB 并在启动时全部占用,Java 应用的堆和堆外内存加起来要小于 limits;
|
||||
- Running 但 READY 为 0/1:进程在运行,但就绪探针不通过,Pod 不会收到 Service 转发的请求,例如探针路径写成不存在的 /ready 时,事件中是 Readiness probe failed: HTTP probe failed with statuscode: 404。
|
||||
|
||||
排查的顺序通常是:kubectl get pods -o wide 看状态、重启次数和所在节点;kubectl describe pod 看事件;kubectl logs 看应用日志,容器重启过的加 --previous;kubectl exec 进入容器检查配置和网络;kubectl top pod 看资源占用(需要 metrics-server)。
|
||||
|
||||
节点 NotReady 指控制平面一段时间没有收到节点上 kubelet 的心跳,这个时间由 node-monitor-grace-period 决定(v1.31 默认 40 秒,v1.32 起 50 秒)。停掉工作节点 d1-agent 后 32 秒,它变为 NotReady,Conditions 中是 Kubelet stopped posting node status,并被加上 node.kubernetes.io/unreachable 污点;上面的 Pod 同时被标记为未就绪,从 Service 的后端中摘除。Pod 默认容忍这个污点 300 秒,又过了约 5 分钟才被驱逐,Deployment 在 d1-server 上补足了副本(见[附录 A9](#a9-节点-notready))。
|
||||
|
||||
排查 NotReady 时先用 kubectl describe node 看 Conditions:
|
||||
|
||||
- Ready 为 Unknown:kubelet 没有上报状态,登录节点检查 kubelet 和容器运行时是否在运行(systemctl status kubelet、journalctl -u kubelet),以及节点到 API Server 的网络和证书;
|
||||
- Ready 为 False:kubelet 在上报,但节点不健康,例如 CNI 插件没有就绪,原因写在 Message 中;
|
||||
- MemoryPressure、DiskPressure、PIDPressure 为 True:节点的内存、磁盘或进程数不足,kubelet 会驱逐节点上的 Pod 来回收资源。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
以下实验的环境为 Apple M1 Pro(10 核)、macOS 上的 Docker Desktop,Docker Engine 28.4.0(使用 containerd 镜像存储),Linux 内核 6.10.14(aarch64)。订单服务基于 Spring Boot 3.5.16、JDK 21,只有一个 GET /orders/{id} 接口,返回订单号、状态和主机名,并引入了 spring-boot-starter-actuator 提供健康检查。
|
||||
|
||||
### A1 构建缓存与多阶段构建
|
||||
|
||||
正文中的 Dockerfile,冷缓存时首次构建(节选):
|
||||
|
||||
```text
|
||||
$ time docker build --progress=plain -t order-service:1.0 order-service
|
||||
#11 [build 4/6] RUN mvn -q dependency:go-offline
|
||||
#11 DONE 209.6s
|
||||
#13 [build 6/6] RUN mvn -q package -DskipTests
|
||||
#13 DONE 3.2s
|
||||
3:34.63 total(zsh 的 time 输出)
|
||||
```
|
||||
|
||||
把源码中的 "SHIPPED" 改为 "PAID" 后重新构建:
|
||||
|
||||
```text
|
||||
#8 [build 3/6] COPY pom.xml .
|
||||
#8 CACHED
|
||||
#10 [build 4/6] RUN mvn -q dependency:go-offline
|
||||
#10 CACHED
|
||||
#11 [build 5/6] COPY src ./src
|
||||
#11 DONE 0.0s
|
||||
#12 [build 6/6] RUN mvn -q package -DskipTests
|
||||
#12 DONE 4.8s
|
||||
6.411 total
|
||||
```
|
||||
|
||||
对照组只有一个阶段、先复制全部文件:
|
||||
|
||||
```dockerfile
|
||||
FROM maven:3.9-eclipse-temurin-21
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN mvn -q package -DskipTests
|
||||
ENTRYPOINT ["java", "-jar", "target/order-service-1.0.jar"]
|
||||
```
|
||||
|
||||
```text
|
||||
# 首次构建(--no-cache)
|
||||
#8 [4/4] RUN mvn -q package -DskipTests
|
||||
#8 DONE 96.2s
|
||||
real 1m40.139s
|
||||
# 改一行源码后重新构建
|
||||
#7 [3/4] COPY . .
|
||||
#7 DONE 0.1s
|
||||
#8 [4/4] RUN mvn -q package -DskipTests
|
||||
#8 DONE 102.4s
|
||||
real 1m47.494s
|
||||
```
|
||||
|
||||
镜像大小取 docker history 中各层大小之和(解压后):多阶段构建的镜像 235.7 MB,其中 Alpine 3.24 根文件系统 9.31 MB、字体和证书等系统包 40.6 MB、JRE 162 MB、jar 包 23.8 MB;单阶段的镜像 680.2 MB,其中 Ubuntu 根文件系统 110 MB、系统包 76.2 MB 与 90.5 MB、JDK 309 MB、Maven 11.1 MB、mvn package 这一层(依赖和编译结果)83.3 MB。
|
||||
|
||||
### A2 exec 形式与 shell 形式
|
||||
|
||||
在正文的镜像上分别用两种形式启动,6 秒后执行 docker stop:
|
||||
|
||||
```text
|
||||
# exec 形式:ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
stop took 0.68 s
|
||||
ExitCode=143
|
||||
... GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||
... GracefulShutdown : Graceful shutdown complete
|
||||
|
||||
# shell 形式:ENTRYPOINT java -jar app.jar; echo "order-service exited"
|
||||
$ docker exec order-shell ps -o pid,comm,args
|
||||
PID COMMAND COMMAND
|
||||
1 sh /bin/sh -c java -jar app.jar; echo "order-service exited"
|
||||
6 java java -jar app.jar
|
||||
stop took 10.34 s
|
||||
ExitCode=137
|
||||
(日志中没有 Graceful shutdown)
|
||||
```
|
||||
|
||||
shell 形式写成 ENTRYPOINT java -jar app.jar 或 ENTRYPOINT echo "starting" && java -jar app.jar 时,ps 显示 1 号进程是 java,停止耗时 0.43 s 和 0.24 s,退出码都是 143。
|
||||
|
||||
用 docker run order-service:1.0 --server.port=9090 启动:
|
||||
|
||||
```text
|
||||
$ docker exec order9090 ps -o pid,args
|
||||
PID COMMAND
|
||||
1 java -jar app.jar --server.port=9090
|
||||
... TomcatWebServer : Tomcat started on port 9090 (http) with context path '/'
|
||||
```
|
||||
|
||||
### A3 OverlayFS 的可写层
|
||||
|
||||
```text
|
||||
$ docker exec ovl head -1 /proc/mounts
|
||||
overlay / overlay rw,relatime,
|
||||
lowerdir=.../snapshots/426/fs:.../snapshots/354/fs:...(共 8 个目录),
|
||||
upperdir=.../snapshots/427/fs,
|
||||
workdir=.../snapshots/427/work 0 0
|
||||
$ docker exec ovl sh -c 'echo note >> /etc/alpine-release; rm /etc/motd'
|
||||
$ docker diff ovl
|
||||
C /etc
|
||||
C /etc/alpine-release
|
||||
D /etc/motd
|
||||
C /tmp
|
||||
A /tmp/hsperfdata_root/1
|
||||
A /tmp/tomcat.8080.7999032892732268743
|
||||
...
|
||||
# 在 Docker Desktop 的虚拟机中查看 upperdir/etc
|
||||
-rw-r--r-- 1 root root 12 Oct 28 10:19 alpine-release
|
||||
c--------- 2 root root 0, 0 Oct 28 10:19 motd
|
||||
```
|
||||
|
||||
白障文件是主次设备号为 0, 0 的字符设备。/tmp 下新增的文件是 JVM 和 Tomcat 运行时创建的。
|
||||
|
||||
### A4 端口、数据卷、网络与 Compose
|
||||
|
||||
```text
|
||||
$ docker run -d --name order -p 8080:8080 order-service:1.0
|
||||
$ curl localhost:8080/orders/1024
|
||||
{"host":"cb371bf03045","id":1024,"status":"SHIPPED"}
|
||||
$ docker run -d --name order2 -p 8080:8080 order-service:1.0
|
||||
docker: Error response from daemon: failed to set up container networking: driver failed programming
|
||||
external connectivity on endpoint order2 (...): Bind for 0.0.0.0:8080 failed: port is already allocated
|
||||
$ docker run -d --name order2 -p 8081:8080 order-service:1.0
|
||||
$ curl localhost:8081/orders/1024
|
||||
{"status":"SHIPPED","id":1024,"host":"3a76640c3641"}
|
||||
$ docker stats --no-stream
|
||||
NAME CPU % MEM USAGE / LIMIT NET I/O PIDS
|
||||
order 0.24% 162.2MiB / 7.654GiB 1.72kB / 725B 43
|
||||
order2 0.33% 153.3MiB / 7.654GiB 1.47kB / 683B 43
|
||||
```
|
||||
|
||||
MySQL 8.0:
|
||||
|
||||
```text
|
||||
$ docker run -d --name mysql-bad mysql:8.0
|
||||
$ docker ps -a --filter name=mysql-bad
|
||||
mysql-bad Exited (1) 2 seconds ago
|
||||
$ docker logs mysql-bad
|
||||
You need to specify one of the following as an environment variable:
|
||||
- MYSQL_ROOT_PASSWORD
|
||||
- MYSQL_ALLOW_EMPTY_PASSWORD
|
||||
- MYSQL_RANDOM_ROOT_PASSWORD
|
||||
|
||||
$ docker network create shop
|
||||
$ docker run -d --name mysql --network shop -p 3316:3306 -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=Root2026 mysql:8.0
|
||||
$ mysql -h127.0.0.1 -P3316 -uroot -p -e "create database shop; create table shop.orders(id bigint primary key, status varchar(16)); insert into shop.orders values (1024, 'SHIPPED')"
|
||||
$ docker rm -f mysql
|
||||
$ docker run ...(同上,重新创建容器)
|
||||
$ mysql -h127.0.0.1 -P3316 -uroot -p -e "select * from shop.orders"
|
||||
id status
|
||||
1024 SHIPPED
|
||||
|
||||
$ docker run --rm --network shop busybox nslookup mysql
|
||||
Name: mysql
|
||||
Address: 172.19.0.2
|
||||
$ docker run --rm busybox nslookup mysql
|
||||
** server can't find mysql: NXDOMAIN
|
||||
```
|
||||
|
||||
正文中的 compose.yaml:
|
||||
|
||||
```text
|
||||
$ docker compose up -d
|
||||
Container shop-mysql-1 Started
|
||||
Container shop-mysql-1 Waiting
|
||||
Container shop-mysql-1 Healthy
|
||||
Container shop-order-1 Starting
|
||||
Container shop-order-1 Started
|
||||
$ docker inspect -f '{{.Name}} started {{.State.StartedAt}}' shop-mysql-1 shop-order-1
|
||||
/shop-mysql-1 started 2026-10-28T10:18:50.539721208Z
|
||||
/shop-order-1 started 2026-10-28T10:19:00.181142879Z
|
||||
```
|
||||
|
||||
### A5 部署、自愈与滚动升级
|
||||
|
||||
两个节点的 k3s v1.31.4:server 节点 d1-server(控制平面兼工作节点)和 agent 节点 d1-agent 各运行在一个 Docker 特权容器中,容器内不能再用 OverlayFS,containerd 使用 native 快照(--snapshotter native)。订单服务的 1.0、1.1 两个镜像用 docker save 导出后导入两个节点的 containerd。
|
||||
|
||||
```text
|
||||
$ kubectl apply -f order.yaml
|
||||
all ready after 21.1 s
|
||||
$ kubectl get pods -o wide
|
||||
NAME READY STATUS RESTARTS AGE IP NODE
|
||||
order-service-5ccc589fd8-b76g2 1/1 Running 0 20s 10.42.1.5 d1-agent
|
||||
order-service-5ccc589fd8-vb2lx 1/1 Running 0 20s 10.42.1.4 d1-agent
|
||||
order-service-5ccc589fd8-vz9hj 1/1 Running 0 20s 10.42.0.8 d1-server
|
||||
$ kubectl get svc order-service
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
order-service ClusterIP 10.43.210.38 <none> 80/TCP 21s
|
||||
$ kubectl exec order-service-5ccc589fd8-b76g2 -- wget -qO- http://order-service/orders/1024(4 次)
|
||||
{"host":"order-service-5ccc589fd8-vz9hj","id":1024,"status":"SHIPPED"}
|
||||
{"host":"order-service-5ccc589fd8-vb2lx","id":1024,"status":"SHIPPED"}
|
||||
{"host":"order-service-5ccc589fd8-vz9hj","id":1024,"status":"SHIPPED"}
|
||||
{"status":"SHIPPED","id":1024,"host":"order-service-5ccc589fd8-b76g2"}
|
||||
```
|
||||
|
||||
删除一个 Pod 后每 0.1~0.2 秒查询一次 Pod 列表:
|
||||
|
||||
```text
|
||||
$ kubectl delete pod/order-service-5ccc589fd8-b76g2
|
||||
new Pod object created after 0.34 s
|
||||
3/3 ready again after 11.2 s
|
||||
NAME READY STATUS RESTARTS AGE IP NODE
|
||||
order-service-5ccc589fd8-494s9 1/1 Running 0 11s 10.42.0.9 d1-server
|
||||
...
|
||||
NAME ADDRESSTYPE PORTS ENDPOINTS
|
||||
order-service-m2pjv IPv4 8080 10.42.1.4,10.42.0.8,10.42.0.9
|
||||
$ kubectl set image deploy/order-service app=order-service:1.1
|
||||
rolling update finished after 33.2 s
|
||||
NAME DESIRED CURRENT READY
|
||||
order-service-5b6f68cdcf 3 3 3
|
||||
order-service-5ccc589fd8 0 0 0
|
||||
$ kubectl rollout undo deploy/order-service
|
||||
undo finished after 32.9 s
|
||||
```
|
||||
|
||||
### A6 Pod 的创建、资源限制与共享的命名空间
|
||||
|
||||
A5 中 Pod order-service-5ccc589fd8-b76g2 的事件和状态:
|
||||
|
||||
```text
|
||||
$ kubectl get events(Deployment 与 ReplicaSet 的事件,节选)
|
||||
2026-10-28T10:21:39Z Deployment ScalingReplicaSet deployment-controller Scaled up replica set order-service-5ccc589fd8 to 3
|
||||
2026-10-28T10:21:40Z ReplicaSet SuccessfulCreate replicaset-controller Created pod: order-service-5ccc589fd8-b76g2
|
||||
$ kubectl get events --field-selector involvedObject.name=order-service-5ccc589fd8-b76g2
|
||||
TIME REASON FROM MESSAGE
|
||||
<nil> Scheduled <none> Successfully assigned default/order-service-5ccc589fd8-b76g2 to d1-agent
|
||||
2026-10-28T10:21:40Z Pulled kubelet Container image "order-service:1.0" already present on machine
|
||||
2026-10-28T10:21:51Z Created kubelet Created container app
|
||||
2026-10-28T10:21:51Z Started kubelet Started container app
|
||||
2026-10-28T10:21:52Z Unhealthy kubelet Readiness probe failed: Get "http://10.42.1.5:8080/actuator/health/readiness": dial tcp 10.42.1.5:8080: connect: connection refused
|
||||
$ kubectl get pod order-service-5ccc589fd8-b76g2 -o jsonpath='{range .status.conditions[*]}...{end}'(节选)
|
||||
PodScheduled 2026-10-28T10:21:40Z
|
||||
Ready 2026-10-28T10:22:00Z
|
||||
$ kubectl exec order-service-5ccc589fd8-b76g2 -- cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max /sys/fs/cgroup/cpu.weight
|
||||
536870912
|
||||
100000 100000
|
||||
10
|
||||
```
|
||||
|
||||
Scheduled 事件的 firstTimestamp 为空,PodScheduled 条件的时间为 10:21:40。cpu.weight 10 由 requests 的 250m 换算而来。
|
||||
|
||||
一个 Pod 中放订单服务和一个 busybox 容器(sleep 3600),分别查看 1 号进程的命名空间:
|
||||
|
||||
```text
|
||||
$ kubectl exec sidecar-demo -c sidecar -- wget -qO- localhost:8080/orders/1024
|
||||
{"host":"sidecar-demo","id":1024,"status":"SHIPPED"}
|
||||
$ kubectl exec sidecar-demo -c sidecar -- ps
|
||||
PID USER TIME COMMAND
|
||||
1 root 0:00 sleep 3600
|
||||
[app] [sidecar]
|
||||
net net:[4026533666] net net:[4026533666]
|
||||
ipc ipc:[4026534201] ipc ipc:[4026534201]
|
||||
uts uts:[4026534200] uts uts:[4026534200]
|
||||
pid pid:[4026534204] pid pid:[4026534207]
|
||||
mnt mnt:[4026534203] mnt mnt:[4026534206]
|
||||
```
|
||||
|
||||
两个容器的 hostname 都是 sidecar-demo;节点上可以看到这个 Pod 的 /pause 进程。
|
||||
|
||||
### A7 Service 与 Ingress
|
||||
|
||||
在 d1-agent 上查看 kube-proxy 写入的规则(节选):
|
||||
|
||||
```text
|
||||
$ iptables-save -t nat | grep default/order-service
|
||||
-A KUBE-SERVICES -d 10.43.210.38/32 -p tcp -m comment --comment "default/order-service cluster IP" -m tcp --dport 80 -j KUBE-SVC-QWJNOBD5KHUXZAD6
|
||||
-A KUBE-SVC-QWJNOBD5KHUXZAD6 -m comment --comment "default/order-service -> 10.42.0.8:8080" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-CRY6GKLRKVOMXZX4
|
||||
-A KUBE-SVC-QWJNOBD5KHUXZAD6 -m comment --comment "default/order-service -> 10.42.1.4:8080" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-4ZNL5PABF7B3TQCL
|
||||
-A KUBE-SVC-QWJNOBD5KHUXZAD6 -m comment --comment "default/order-service -> 10.42.1.5:8080" -j KUBE-SEP-FZGSJQ7SZ4EWXRUD
|
||||
-A KUBE-SEP-CRY6GKLRKVOMXZX4 -p tcp -m comment --comment "default/order-service" -m tcp -j DNAT --to-destination 10.42.0.8:8080
|
||||
```
|
||||
|
||||
正文中的 Ingress,k3s 自带的 Traefik 2.11 处理,在 d1-server 上访问节点 IP 的 80 端口:
|
||||
|
||||
```text
|
||||
$ kubectl get ingress shop
|
||||
NAME CLASS HOSTS ADDRESS PORTS AGE
|
||||
shop traefik shop.example.com 172.19.0.2,172.19.0.3 80 5s
|
||||
$ wget -qO- --header "Host: shop.example.com" http://172.19.0.2/orders/1024
|
||||
{"host":"order-service-5ccc589fd8-vb2lx","id":1024,"status":"SHIPPED"}
|
||||
$ wget --header "Host: shop.example.com" http://172.19.0.2/users/1
|
||||
HTTP/1.1 404 Not Found
|
||||
$ wget --header "Host: other.example.com" http://172.19.0.2/orders/1024
|
||||
HTTP/1.1 404 Not Found
|
||||
```
|
||||
|
||||
把 order-service 改为 NodePort:
|
||||
|
||||
```text
|
||||
$ kubectl patch svc order-service -p '{"spec":{"type":"NodePort"}}'
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
order-service NodePort 10.43.210.38 <none> 80:31887/TCP 103s
|
||||
$ wget -qO- http://172.19.0.3:31887/orders/1024 (在 d1-server 上访问 d1-agent 的 NodePort)
|
||||
{"status":"SHIPPED","id":1024,"host":"order-service-5ccc589fd8-b76g2"}
|
||||
```
|
||||
|
||||
### A8 Pod 的异常状态
|
||||
|
||||
在命名空间 lab 中创建 5 个故意出错的 Pod:
|
||||
|
||||
```yaml
|
||||
# pending-demo:申请 20 个 CPU
|
||||
resources: { requests: { cpu: "20" } }
|
||||
# pull-demo:不存在的标签
|
||||
image: order-service:9.9
|
||||
# crash-demo:错误的启动参数
|
||||
args: ["--server.port=abc"]
|
||||
# oom-demo:堆比内存上限大,并在启动时全部占用
|
||||
env:
|
||||
- { name: JAVA_TOOL_OPTIONS, value: "-Xms300m -Xmx300m -XX:+AlwaysPreTouch" }
|
||||
resources: { limits: { memory: 256Mi } }
|
||||
# notready-demo:就绪探针的路径不存在
|
||||
readinessProbe:
|
||||
httpGet: { path: /ready, port: 8080 }
|
||||
```
|
||||
|
||||
```text
|
||||
$ kubectl get pods -n lab (约 2 分钟后)
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
crash-demo 0/1 CrashLoopBackOff 4 (35s ago) 2m8s
|
||||
notready-demo 0/1 Running 0 2m8s
|
||||
oom-demo 0/1 CrashLoopBackOff 4 (32s ago) 2m8s
|
||||
pending-demo 0/1 Pending 0 2m8s
|
||||
pull-demo 0/1 ImagePullBackOff 0 2m8s
|
||||
|
||||
pending-demo Warning FailedScheduling default-scheduler 0/2 nodes are available: 2 Insufficient cpu. preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.
|
||||
pull-demo Warning Failed kubelet Failed to pull image "order-service:9.9": failed to pull and unpack image "docker.io/library/order-service:9.9": failed to resolve reference ...
|
||||
Warning Failed kubelet Error: ErrImagePull
|
||||
Warning Failed kubelet Error: ImagePullBackOff
|
||||
crash-demo Warning BackOff kubelet Back-off restarting failed container app in pod crash-demo_lab(...)
|
||||
notready-demo Warning Unhealthy kubelet Readiness probe failed: HTTP probe failed with statuscode: 404
|
||||
|
||||
oom-demo lastState: OOMKilled exitCode=137
|
||||
crash-demo lastState: Error exitCode=1
|
||||
$ kubectl logs -n lab crash-demo --previous
|
||||
APPLICATION FAILED TO START
|
||||
Description:
|
||||
Invalid value 'abc' for configuration property 'server.port' (originating from '"server.port" from property source "commandLineArgs"'). Validation failed for the following reason:
|
||||
Failed to convert to type java.lang.Integer
|
||||
```
|
||||
|
||||
oom-demo 在第一次被杀之前 kubectl get pods 显示的状态是 OOMKilled。给两个节点都加上污点后创建 Pod:
|
||||
|
||||
```text
|
||||
$ kubectl taint nodes d1-server d1-agent dedicated=db:NoSchedule
|
||||
$ kubectl run -n lab taint-demo --image=order-service:1.0
|
||||
Warning FailedScheduling 5s default-scheduler 0/2 nodes are available: 2 node(s) had untolerated taint {dedicated: db}. ...
|
||||
$ kubectl taint nodes d1-server d1-agent dedicated=db:NoSchedule-
|
||||
(8 秒后 taint-demo 在 d1-server 上 Running)
|
||||
```
|
||||
|
||||
### A9 节点 NotReady
|
||||
|
||||
用 docker stop 停掉 d1-agent 所在的容器,相当于节点宕机:
|
||||
|
||||
```text
|
||||
10:28:55 $ docker stop d1-k3s-agent
|
||||
10:29:28 d1-agent NotReady after 32 s
|
||||
Type Status LastTransitionTime Reason Message
|
||||
MemoryPressure Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
|
||||
DiskPressure Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
|
||||
PIDPressure Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
|
||||
Ready Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
|
||||
Taints: node.kubernetes.io/unreachable:NoSchedule
|
||||
10:34:33 pods on d1-agent marked for deletion after 337 s
|
||||
NAME READY STATUS IP NODE
|
||||
order-service-5ccc589fd8-24sqr 1/1 Terminating 10.42.1.8 d1-agent
|
||||
order-service-5ccc589fd8-2f56l 1/1 Terminating 10.42.1.9 d1-agent
|
||||
order-service-5ccc589fd8-2vrx4 1/1 Running 10.42.0.11 d1-server
|
||||
order-service-5ccc589fd8-4gzps 1/1 Running 10.42.0.17 d1-server
|
||||
order-service-5ccc589fd8-gzsbv 1/1 Running 10.42.0.16 d1-server
|
||||
$ kubectl get pod -o jsonpath='{.items[0].spec.tolerations}'
|
||||
[{"effect":"NoExecute","key":"node.kubernetes.io/not-ready","operator":"Exists","tolerationSeconds":300},
|
||||
{"effect":"NoExecute","key":"node.kubernetes.io/unreachable","operator":"Exists","tolerationSeconds":300}]
|
||||
```
|
||||
|
||||
节点失联后,Pod 无法确认已经停止,一直显示 Terminating。恢复 d1-agent、重新部署后再停一次,每 4 秒查看 EndpointSlice 中各地址的 ready:
|
||||
|
||||
```text
|
||||
+33s node=Ready endpoints: 10.42.0.19=true 10.42.1.14=true 10.42.1.13=true
|
||||
+37s node=NotReady endpoints: 10.42.0.19=true 10.42.1.14=false 10.42.1.13=false
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
@@ -168,7 +800,29 @@ readinessProbe 是就绪探针:Pod 启动后要通过检查才会加入 Servic
|
||||
- [What is Docker](https://www.docker.com/what-docker)
|
||||
- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)
|
||||
- [Understanding the image layers | Docker Docs](https://docs.docker.com/get-started/docker-concepts/building-images/understanding-image-layers/)
|
||||
- [Dockerfile reference | Docker Docs](https://docs.docker.com/reference/dockerfile/)
|
||||
- [Building best practices | Docker Docs](https://docs.docker.com/build/building/best-practices/)
|
||||
- [Multi-stage builds | Docker Docs](https://docs.docker.com/build/building/multi-stage/)
|
||||
- [Docker build cache | Docker Docs](https://docs.docker.com/build/cache/)
|
||||
- [OverlayFS storage driver | Docker Docs](https://docs.docker.com/engine/storage/drivers/overlayfs-driver/)
|
||||
- [Volumes | Docker Docs](https://docs.docker.com/engine/storage/volumes/)
|
||||
- [Bridge network driver | Docker Docs](https://docs.docker.com/engine/network/drivers/bridge/)
|
||||
- [Control startup order | Docker Docs](https://docs.docker.com/compose/how-tos/startup-order/)
|
||||
- [Live restore | Docker Docs](https://docs.docker.com/engine/daemon/live-restore/)
|
||||
- [Kubernetes 文档:Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
|
||||
- [Kubernetes 文档:Service](https://kubernetes.io/docs/concepts/services-networking/service/)
|
||||
- [Kubernetes 文档:Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
|
||||
- [Kubernetes 文档:Pod Lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)
|
||||
- [Kubernetes 文档:Liveness, Readiness, and Startup Probes](https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/)
|
||||
- [Kubernetes 文档:Resource Management for Pods and Containers](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
|
||||
- [Kubernetes 文档:Kubernetes Scheduler](https://kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/)
|
||||
- [Kubernetes 文档:Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
|
||||
- [Kubernetes 文档:Node Status](https://kubernetes.io/docs/reference/node/node-status/)
|
||||
- [Kubernetes 文档:Services, Load Balancing, and Networking](https://kubernetes.io/docs/concepts/services-networking/)
|
||||
- [Kubernetes 文档:Debug Pods](https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/)
|
||||
- [Spring Boot 文档:Graceful Shutdown](https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html)
|
||||
- [Spring Boot 文档:Kubernetes Probes](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html#actuator.endpoints.kubernetes-probes)
|
||||
- [Traefik 文档:Kubernetes Ingress](https://doc.traefik.io/traefik/v2.11/routing/providers/kubernetes-ingress/)
|
||||
- [K3s 文档:Basic Network Options](https://docs.k3s.io/networking/basic-network-options)
|
||||
- [namespaces(7) — Linux manual page](https://man7.org/linux/man-pages/man7/namespaces.7.html)
|
||||
- [cgroups(7) — Linux manual page](https://man7.org/linux/man-pages/man7/cgroups.7.html)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
Reference in New Issue
Block a user