K8s故障排查 & 控制器与Service实战

14516 字
73 分钟
K8s故障排查 & 控制器与Service实战
K8s故障排查 & 控制器与Service实战

K8s故障排查 & 控制器与Service实战#

[TOC]


故障排查三板斧#

Important

故障排查三板斧是 K8s 排障的三大核心命令:

  • kubectl describe :查看资源详细信息与事件(Events)
  • kubectl logs :查看容器日志
  • kubectl exec 配合 command & args:修改启动命令进容器排查

三板斧各有分工:describe 看”状态对不对”,logs 看”日志报什么”,exec 看”容器里什么情况”

kubectl describe 故障排查#

作用:查看资源的详细信息、运行状态,根据状态及事件信息确定问题原因

实战案例:镜像名称写错导致拉取失败

Terminal window
1)编写资源清单(镜像名称故意写错,多了很多个1)
root@Master ~# vim /k8s/pods/03-pods-troubleshooting-describe.yaml
apiVersion: v1
kind: Pod
metadata:
name: troubleshooting-describe
spec:
containers:
- name: c1
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v111
root@Master ~# kubectl apply -f /k8s/pods/03-pods-troubleshooting-describe.yaml
pod/troubleshooting-describe created
2)查看Pod状态(先是ErrImagePull,随后变成ImagePullBackOff)
root@Master pods# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
troubleshooting-describe 0/1 ErrImagePull 0 15s 10.100.1.13 worker01
# ErrImagePull = 镜像拉取失败
'稍等片刻,会变成 ImagePullBackOff,表示镜像拉取失败后进入退避重试'
# 我这次没拉成功,我休息一会儿(退避)再拉
3)用 describe 查看错误信息(重点看 Events 段)
root@Master pods# kubectl describe pod troubleshooting-describe
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 12m default-scheduler Successfully assigned default/troubleshooting-describe to worker01
Normal Pulling 9m22s (x5 over 12m) kubelet spec.containers{c1}: Pulling image "registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v111"
Warning Failed 9m22s (x5 over 12m) kubelet spec.containers{c1}: Failed to pull image "registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v111": rpc error: code = NotFound desc = failed to pull and unpack image "registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v111": failed to resolve image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v111: not found
# ⬆️ Events 里能看到 Failed to pull image + not found,一眼定位是镜像拉取失败
Note

错误分析:通过 Events 的 Failed to pull image ... not found 可知是镜像问题

解决思路:

  • ① 用户镜像名称写错 → 检查镜像名
  • ② 用户没有权限拉取镜像 → 检查是否需要登录私有仓库

彩蛋:查看集群所有事件 kubectl get events ✅,比 describe 看得更全

kubectl logs 故障排查#

作用:查看 Pod 指定容器的日志,一般用来查看服务日志进行故障排查

Tip

💡 describe -f 是“从哪读”,logs -f 是“一直跟”

  • kubectl describe -f deployment.yaml:-f = —filename,指定文件/目录/URL,和 apply/get/create/delete -f 的那个 -f 一样
  • kubectl logs -f pod/xxx:-f = —follow,持续跟踪输出,类似 tail -f,持续输出监控

实战案例:同一 Pod 两个 nginx 抢 80 端口

Terminal window
1)编写资源清单(c1、c2 都是 nginx 镜像,都监听80端口)
root@Master pods# vim /k8s/pods/04-troubleshooting-logs.yaml
apiVersion: v1
kind: Pod
metadata:
name: troubleshooting-logs
spec:
containers:
- name: c1
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1
- name: c2
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2
root@Master pods# kubectl apply -f 04-troubleshooting-logs.yaml
pod/troubleshooting-logs created
2)查看状态(1/2 Error,c2 容器挂了)
root@Master pods# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
troubleshooting-logs 1/2 Error 1 (16s ago) 20s 10.100.2.8 worker02
3)describe 查看哪个容器异常
root@Master pods# kubectl describe pod troubleshooting-logs
Containers:
c1:
State: Running # c1 正常
Ready: True
c2:
State: Terminated # c2 挂了
Reason: Error
Exit Code: 1
Last State: Terminated
Reason: Error
Exit Code: 1
......restarting failed container c2
# describe 只能看到 c2 在重启,但不知道具体原因
`有个容器一直在重启`
root@Master ~# kubectl get pods troubleshooting-logs
NAME READY STATUS RESTARTS AGE
troubleshooting-logs 1/2 CrashLoopBackOff 🔥6 (4m54s ago) 11m
4)用 logs 查看指定容器日志(-c 指定容器名)
root@Master pods# kubectl logs -c c1 troubleshooting-logs
# c1 日志正常:nginx 正常启动
2026/09/03 01:51:40 [notice] 1#1: start worker processes
root@Master pods# kubectl logs -c c2 troubleshooting-logs
2026/09/03 01:52:00 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)
# ⬆️ 真相大白!c2 报 bind() 80 端口被占用
Note

错误分析:c1 日志正常,c2 报 bind() 80 端口失败

原因:同一 Pod 内所有容器共享网络名称空间,80 端口只能被一个进程占用,c1 先占了,c2 就起不来

修改容器启动命令故障排查#

作用:实际工作中容器可能无法启动,可以先通过修改启动命令让容器起来,再进容器排查

Warning

使用指定命令时要注意容器内是否有该工具,否则照样无法启动

Important

Command &&Args 本质:

  • command → 覆盖 Dockerfile 的 ENTRYPOINT
  • args → 覆盖 Dockerfile 的 CMD

两者都写,容器启动时执行的就是 command 里的内容 + args 参数

Terminal window
root@Master ~# kubectl delete -f /k8s/pods/04-troubleshooting-logs.yaml
pod "troubleshooting-logs" deleted from default namespace
# 移除重新编辑
root@Master pods# vim /k8s/pods/04-troubleshooting-logs.yaml
apiVersion: v1
kind: Pod
metadata:
name: troubleshooting-logs
spec:
containers:
- name: c1
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1
- name: c2
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2
command: # 相当于替换Dockerfile的ENTRYPOINT(容器中得有这个工具)
- tail
args: # 相当于替换Dockerfile的CMD
- -f
- /etc/hosts
⚠️ 缩进教训:`command`/`args` 必须和 `name` 平级(4空格),多一个空格就报 `did not find expected key`
root@Master ~# kubectl apply -f /k8s/pods/04-troubleshooting-logs.yaml
pod/troubleshooting-logs created
2)查看状态(2/2 Running,两个容器都起来了)
root@Master ~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
troubleshooting-logs 2/2 Running 0 2m26s 10.100.2.9 worker02
3)查看两个容器的启动命令
root@Master pods# kubectl exec -c c1 troubleshooting-logs -- ps -ef
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off; # c1 是 nginx
32 nginx 0:00 nginx: worker process
root@Master pods# kubectl exec -c c2 troubleshooting-logs -- ps -ef
PID USER TIME COMMAND
1 root 0:00 tail -f /etc/hosts # c2 是 tail
7 root 0:00 ps -ef
4)进 c2 容器手动启动 nginx(复现端口冲突)
root@Master ~# kubectl exec -it -c c2 troubleshooting-logs -- sh
/ # nginx
2026/09/07 08:53:08 [emerg] 19#19: bind() to 0.0.0.0:80 failed (98: Address in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)
# ⬆️ 手动启动 nginx 才发现是 80 端口被 c1 占了!
5)修改端口 80 → 81,再启动 nginx
/ # sed -i '/listen/s#80#81#g' /etc/nginx/conf.d/default.conf
/ # nginx
2026/09/07 08:54:09 [notice] 21#21: nginx/1.20.1
/ # 2026/09/07 08:54:09 [notice] 22#22: start worker processes
# ✅ 改端口后 nginx 启动成功!
6)访问验证(同 Pod 共享IP,c1 在80,c2 在81)
root@Master ~# curl -s 10.100.2.9:80 | grep h1
<h1 style="color: green">凡人修仙传 v1 </h1> # c1
root@Master ~# curl -s 10.100.2.9:81 | grep h1
<h1 style="color: red">凡人修仙传 v2 </h1> # c2 ✅
'同一个 Pod IP,两个端口,各跑各的'
7)清理
root@Master ~# kubectl delete -f /k8s/pods/04-troubleshooting-logs.yaml
pod "troubleshooting-logs" deleted from default namespace

容器状态与基础架构容器#

容器的三种状态#

容器有三个状态:Waiting、Running、Terminated

状态含义
Running容器正常运行
Terminated容器终止(一般删除时出现)
Waiting既非 Running 也非 Terminated,等待中(如拉镜像、无法启动)
Caution

当 Pod 状态出现 CrashLoopBackOff 关键字时,说明该 Pod 内有容器一直在重启

基础架构容器 pause 提供网络名称空间#

Important

Pod 与容器的关系:Pod 是 K8s 最小调度单元,一个 Pod 内除了业务容器,还有一个基础架构容器 pause

  • pause 为 Pod 提供名称空间(ipc、net、time、user)
  • 业务容器共享 pause 的名称空间

验证过程:

Terminal window
1)确认 Pod 调度到哪台节点
root@Master ~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
troubleshooting-logs 2/2 Running 2 (63m ago) 15h 10.100.2.11 worker02
root@Master ~# curl -s 10.100.2.11 | grep h1
<h1 style="color: green">凡人修仙传 v1 </h1>
root@Master ~# curl -s 10.100.2.11:81 | grep h1
<h1 style="color: red">凡人修仙传 v2 </h1>
Terminal window
'去 worker02 节点(10.0.0.15)用 crictl 查看 Pod 容器关系'
2)列出Pod 沙箱(sandbox)
# 在 containerd 架构里,一个 Pod 对应一个 sandbox 容器(pause 容器),它先被创建、持有网络和 IPC 命名空间,业务容器再加入进去
# crictl pods 只显示沙箱本身,不显示里面的业务容器(那要用 crictl ps 看)
root@Worker02 ~# crictl pods --latest
--latest, -l # 查看最新创建的
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
cea825bae4001 2 hour ago Ready troubleshooting-logs default 1 (default)
'STATE 沙箱状态,Ready 表示沙箱就绪,可以往里放容器(业务容器挂了沙箱也仍是 Ready)'
'NAMESPACE 所属命名空间,default'
'ATTEMPT 第几次尝试创建该沙箱'
=============================================================
在 Kubernetes 的抽象里,Pod 是一组共享网络/IPC 命名空间的容器
落到 containerd 这个运行时的实现上,每个 Pod 先创建一个沙箱(sandbox),沙箱在底层就是靠一个 pause 容器实现的;pause 进程几乎什么都不做,只负责“占住”网络命名空间、IPC 命名空间这些资,然后 Pod 里的业务容器都以 join 的方式加入这个 pause 容器的命名空间
3)列出指定沙箱中的业务容器(不含 pause / init 容器)
root@Worker02 ~# crictl ps --pod cea825bae4001
'接按沙箱 ID 过滤'
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
b1164d69ab22c f28fd43be4ad4 2 hours ago Running c1 1 cea825bae4001 troubleshooting-logs default
56b1ec3d54dbf d65adc8a2f327 2 hours ago Running c2 1 cea825bae4001 troubleshooting-logs default
# ⬆️ 一个沙箱(cea825bae4001) 挂两个业务容器 c1、c2
4)查看对应的进程ID和命名空间
root@Worker02 ~# crictl inspect
inspect inspecti inspectp
`分别对应容器,镜像和沙盒的详细信息`
root@Worker02 ~# crictl inspectp cea825bae4001 | jq '.info.pid'
1460 # 这里是沙盒的进程ID
# 它的 ipc、uts、pid、mount 都没有 path——没有 path 的意思是“新建一个独立命名空间”
# pause 是 Pod 里第一个起来的,这些命名空间就是它创建的
'下面分别产看两个容器的进程命名空间'
root@Worker02 ~# crictl inspect b1164d69ab22c | jq '{pid: .info.pid, ns: .info.runtimeSpec.linux.namespaces}'
{
"pid": 1486,
"ns": [
{
"type": "pid"
},
{
"path": "/proc/1460/ns/ipc",
"type": "ipc"
},
{
"path": "/proc/1460/ns/uts",
"type": "uts"
},
{
"type": "mount"
},
{
"path": "/proc/1460/ns/net",
"type": "network"
},
{
"type": "cgroup"
}
]
}
root@Worker02 ~# crictl inspect 56b1ec3d54dbf | jq '{pid: .info.pid, ns: .info.runtimeSpec.linux.namespaces}'
{
"pid": 1525,
"ns": [
{
"type": "pid"
},
{
"path": "/proc/1460/ns/ipc",
"type": "ipc"
},
{
"path": "/proc/1460/ns/uts",
"type": "uts"
},
{
"type": "mount"
},
{
"path": "/proc/1460/ns/net",
"type": "network"
},
{
"type": "cgroup"
}
]
}
# 我们找到了三个进程的 PID
pause 进程 --> 1460
c1 进程 --> 1486
c2 进程 --> 1525
root@Worker02 ~# pstree -p
containerd-shim(1436)─┬─nginx(1486)─┬─nginx(1548)
│ └─nginx(1549)
├─pause(1460)
├─tail(1525)───nginx(11077)─┬─nginx(11078)
│ └─nginx(11079)
`我们第二个容器的进程是 tail -f 启动的!`
5)验证三个进程共享网络名称空间
root@Worker02 ~# ls -l /proc/1460/ns/net # pause
lrwxrwxrwx 1 xxx 0 Sep 8 08:10 /proc/1460/ns/net -> 'net:[4026532425]'
root@Worker02 ~# ls -l /proc/1486/ns/net # c1 nginx
lrwxrwxrwx 1 xxx 0 Sep 8 11:13 /proc/1486/ns/net -> 'net:[4026532425]'
root@Worker02 ~# ls -l /proc/1525/ns/net # c2 tail
lrwxrwxrwx 1 xxx 0 Sep 8 08:34 /proc/1525/ns/net -> 'net:[4026532425]'
# ⬆️ 三个进程的 net 指向同一个 ns(4026532413)→ 共享网络名称空间!
'同理 ipc/time/user 也一致,这就是为什么同Pod容器共享IP和端口'

pause架构
pause架构

Note

各自独立的 cgroup 视图、各自独立的文件系统(互相看不到对方挂载的内容)、各自独立的进程视图(互相看不到对方的进程)

Terminal window
6)实验A:删除业务容器 → 沙箱不动 → 重启后 IP 不变
root@Worker02 ~# crictl rm -f b1164d69ab22c && crictl rm -f 56b1ec3d54dbf && sleep 15
root@Worker02 ~# crictl ps --pod cea825bae4001 # 指定沙箱ID
CONTAINER IMAGE CREATED STATE NAME
a3e62380de657 d65adc8a2f327 41 seconds ago Running c2
aaf8547f22137 f28fd43be4ad4 41 seconds ago Running c1
'容器的id已经发生变化,被 kubelet 自动拉起'
root@Worker02 ~# kubectl get pods troubleshooting-logs -o wide
NAME READY STATUS RESTARTS AGE IP
troubleshooting-logs 2/2 Running 4 (10ms ago) 20h 10.100.2.11
`IP 不变(pause 没动,Pod 网络不变)RESTARTS 重启次数增加两次`
root@Worker02 ~# kubectl exec -c c2 troubleshooting-logs -- sh -c 'sed -i '/listen/s#80#81#g' /etc/nginx/conf.d/default.conf'
root@Worker02 ~# kubectl exec -c c2 troubleshooting-logs -- sh -c 'nginx'
# 服务依然可用
root@Worker02 ~# curl -s 10.100.2.11:80 | grep h1
<h1 style="color: green">凡人修仙传 v1 </h1>
root@Worker02 ~# curl -s 10.100.2.11:81 | grep h1
<h1 style="color: red">凡人修仙传 v2 </h1>
7)实验B(对照组):删除整个沙箱 → kubelet 重建沙箱 → 观察 IP 是否变化
root@Worker02 ~# crictl rmp -f cea825bae4001 && sleep 20
root@Worker02 ~# crictl pods -l
POD ID CREATED STATE NAME
b504f64136d6f 49 seconds ago Ready troubleshooting-logs
root@Worker02 ~# kubectl get pods troubleshooting-logs -o wide
NAME READY STATUS RESTARTS AGE IP NODE
troubleshooting-logs 2/2 Running 4 (53m ago) 20h 10.100.2.12 worker02
# kubectl 显示的 RESTARTS 计数器和“53m ago”这个时间戳,记录的是kubelet 亲自统计的“容器死亡→重启”事件:kubelet 每次看到某容器退出、然后按重启策略把它拉起,计数 +1、时间戳更新
# 而我们这次的 crictl rmp -f 走的是另一条路:kubelet 轮询时发现的情况不是“容器退出了”,而是“整个沙箱连同容器凭空不存在了”,对 kubelet 来说这属于“实际状态偏离期望状态”,处理方式是做一次全新的创建(新沙箱 + 新容器),而不是把它记为一次“重启”
# 新容器创建时,restartCount 沿用了旧容器档案里的数值(4),且没有发生“退出事件”,所以:计数不加 → 还是 4、时间戳不更新 → 还是上一次真实重启的 53m ago
Important

结论:

  • 删除业务容器 → kubelet 自动重建,Pod IP 不变(pause 沙箱还在)
  • 删除pause 基础架构容器 → 整个 Pod 重建,Pod IP 会变

pause 是 Pod 的”根”,业务容器都是它派生出来的

crictl常用命令#

Terminal window
# 删除已退出的旧业务容器
crictl rm $(crictl ps -aq --state Exited)
# 删除 NotReady 的残留沙箱(先查残留的沙箱)
crictl pods --state NotReady
crictl rmp $(crictl pods -q --state NotReady)
# 业务容器的启动命令(最常用):
`这就是最终生效的启动命令`
root@Worker02 ~# crictl inspect b1164d69ab22c | jq '.info.runtimeSpec.process.args'
[
"/docker-entrypoint.sh",
"nginx",
"-g",
"daemon off;"
]
root@Worker02 ~# crictl inspect 56b1ec3d54dbf | jq '.info.runtimeSpec.process.args'
[
"tail",
"-f",
"/etc/hosts"
]
✅ `process.args 是 runtime 真正 exec 的那一份,镜像的 ENTRYPOINT/CMD 和 Kubernetes 里写的 command/args 都已经合并进去了:`
'它只告诉你“最终是什么”,不告诉你“哪来的”'
# kubectl 侧对照(如果 Pod 是 k8s 管的,这是看“用户意图”的地方——镜像之外追加的 command/args)
root@Master ~# kubectl get pod troubleshooting-logs -o json | jq '.spec.containers[] | {name, command, args}'
{
"name": "c1",
"command": null,
"args": null
}
{
"name": "c2",
"command": [
"tail"
],
"args": [
"-f",
"/etc/hosts"
]
}

部署 WordPress 实战#

部署 MySQL 数据库与环境变量#

作用:K8s 通过 env 给容器传环境变量,MySQL 镜像读取变量初始化库和账号

Terminal window
# 环境准备
jiuzhao@Ubuntu ~$ docker save mysql:8.0.36 wordpress:7.1.0-php8.5 | gzip > wp-mysql.tar.gz
jiuzhao@Ubuntu ~$ scp wp-mysql.tar.gz Worker01:/tmp
wp-mysql.tar.gz 100% 436MB 256.2MB/s 00:01
root@Worker01 ~# ctr -n k8s.io images import /tmp/wp-mysql.tar.gz
`只需要worker01导入即可!指定node节点`
docker.io/library/mysql:8.0.36 saved
docker.io/library/wordpress:7.1.0 php8.5 saved
application/vnd.oci.image.index.v1+json sha256:a532724022429812ec797c285c1b540a644c15e248579c6bfdf12a8fbaab4964
application/vnd.oci.image.index.v1+json sha256:397daa8a8816347e724362c2122129601eb0811fbaff0ce9b2b22c7b057a745d
Importing elapsed: 17.3s total: 0.0 B (0.0 B/s)
1)编写资源清单(env 传环境变量,args 指定 MySQL 参数)
root@Master pods# vim /k8s/pods/05-pods-env-mysql.yaml
apiVersion: v1
kind: Pod
metadata:
name: db-mysql
spec:
# 指定分配至worker01
nodeName: worker01
containers:
- name: c1
image: docker.io/library/mysql:8.0.36
# 向容器传递环境变量
env:
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "yes"
- name: MYSQL_DATABASE
value: "wordpress"
- name: MYSQL_USER
value: "kpyun"
- name: MYSQL_PASSWORD
value: "kpyun123"
- name: "TZ"
value: "Asia/Shanghai"
args:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_bin
- --default-authentication-plugin=mysql_native_password
root@Master pods# kubectl apply -f /k8s/pods/05-pods-env-mysql.yaml
pod/db-mysql created
2)等待启动并查看
root@Master ~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
db-mysql 1/1 Running 0 8s 10.100.1.18 worker01
3)查看容器里的环境变量
root@Master ~# kubectl exec db-mysql -- env | grep MYSQL
root@Master ~# kubectl exec -it db-mysql -- env | grep MYSQL
MYSQL_MAJOR=8.0
MYSQL_VERSION=8.0.36-1.el8
MYSQL_SHELL_VERSION=8.0.36-1.el8
MYSQL_ALLOW_EMPTY_PASSWORD=yes
MYSQL_DATABASE=wordpress
MYSQL_USER=kpyun
MYSQL_PASSWORD=kpyun123
# ✅ 我们传的变量生效了
4)连接测试(wordpress 库已自动创建)
root@Master pods# kubectl exec -it db-mysql -- mysql wordpress
# 在容器内执行的命令:连接 wordpress 数据库
Welcome to the MySQL monitor.
Server version: 8.0.36 MySQL Community Server - GPL
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| wordpress |
+--------------------+
mysql> SHOW TABLES;
Empty set (0.01 sec)
mysql> SELECT user,host,plugin FROM mysql.user;
+------------------+-----------+-----------------------+
| user | host | plugin |
+------------------+-----------+-----------------------+
| kpyun | % | mysql_native_password |
| root | % | mysql_native_password |
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session | localhost | caching_sha2_password |
| mysql.sys | localhost | caching_sha2_password |
| root | localhost | mysql_native_password |
+------------------+-----------+-----------------------+
# ✅ kpyun 用户自动创建,用兼容插件
Note

MySQL 8.0 创建用户时 --default-authentication-plugin=mysql_native_password 可让 wordpress 老版本 PHP 兼容连接

MySQL 8.4+ 才用 caching_sha2_password

部署 WordPress(hostNetwork 模式)#

Terminal window
1)编写资源清单(hostNetwork 让 Pod 直接用宿主机网络)
root@Master ~# MYSQLIP=$(kubectl get pod db-mysql -o jsonpath='{.status.podIP}')
root@Master ~# echo "MySQL IP: $MYSQLIP"
MySQL IP: 10.100.1.18
root@Master ~# vim /k8s/pods/05-pods-wordpress-hostNetwork.yaml
apiVersion: v1
kind: Pod
metadata:
name: blog-wp
spec:
nodeName: worker01
hostNetwork: true # 直接使用宿主机网络,不用Pod网段
containers:
- image: docker.io/library/wordpress:7.1.0-php8.5
name: c1
env:
- name: WORDPRESS_DB_HOST
value: 10.100.1.18:3306 # MySQL Pod 的 IP
- name: WORDPRESS_DB_USER
value: kpyun
- name: WORDPRESS_DB_PASSWORD
value: kpyun123
- name: WORDPRESS_DB_NAME
value: "wordpress"
root@Master pods# kubectl apply -f /k8s/pods/05-pods-wordpress-hostNetwork.yaml
pod/blog-wp created
2)查看状态(hostNetwork 的 Pod IP = 宿主机 IP)
root@Master ~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
blog-wp 1/1 Running 0 4m45s 10.0.0.14 worker01
db-mysql 1/1 Running 0 46m 10.100.1.18 worker01
# ⬆️ blog-wp 的 IP 是 10.0.0.14(worker01 宿主机IP),不是 Pod 网段!
3)访问验证(进入安装页说明连上MySQL了)
root@Master ~# curl -s -I http://10.0.0.14/
HTTP/1.1 302 Found
Date: Tue, 08 Sep 2026 08:16:04 GMT
Server: Apache/2.4.68 (Debian)
X-Powered-By: PHP/8.5.10
root@Master ~# curl -s -L http://10.0.0.14/ | grep -oE "<title>[^<]*</title>"
<title>WordPress &rsaquo; Installation</title> # ✅ 进入安装页
Important

hostNetwork 模式:Pod 不使用 Pod 网段 IP,直接用宿主机 IP + 端口,类似”容器和宿主机共享网络”,适合对网络性能要求高或需要固定访问地址的场景


Deployment 副本控制器(含滚动更新与回滚)#

什么是 Deployment#

Note

Deployment 是新一代“副本控制器”(底层由 ReplicaSet 驱动),作用是控制指定 Pod 副本数量始终存活 + 支持滚动更新与回滚

只要 Pod 挂了或被删,立刻自动重建,副本数永远等于 replicas

层级关系:Deployment 管 ReplicaSet,ReplicaSet 管 Pod——Deployment 负责”版本”,RS 负责”副本数”

apiVersion 与 API 组#

为什么 Deployment 写 apps/v1,Pod 只写 v1?

Note

写资源清单时 apiVersion 的取值跟着资源走:Pod/Service/Namespace 写 v1,Deployment 写 apps/v1——不是随意的,背后是 k8s 的 API 分组设计

k8s 把所有资源按功能分进不同的 API 组:最基础的一批(Pod、Service…)住在”核心组”,后来扩展的高级资源(Deployment、Job…)按功能各立门户(apps、batch…)

用 kubectl explain 对比两个字段,差异就藏在开头几行:

Terminal window
root@Master ~# kubectl explain pods.apiVersion
KIND: Pod
VERSION: v1
FIELD: apiVersion <string>
......
root@Master ~# kubectl explain deployments.apiVersion
GROUP: apps
KIND: Deployment
VERSION: v1
FIELD: apiVersion <string>
......
# ⬆️ 唯一差异:Deployment 多了 GROUP: apps 这行;Pod 没有 GROUP 行 → 它属于”核心API组”
对比项核心 API 组命名 API 组
apiVersion 写法v1(没斜杠)apps/v1(组名/版本)
API 路径/api/v1/apis/apps/v1
典型成员Pod、Service、Namespace、ConfigMap、SecretDeployment、ReplicaSet、StatefulSet、DaemonSet、Job、CronJob
定位k8s 诞生就有,最基础的一批后来扩展的”高级”资源,按功能分组管理
Note

/api/v1(老地址,无 s)= 核心组;/apis/<组>/<版本>(新地址,有 s)= 承载所有命名组——历史遗留,别改错

集群到底支持哪些组?一条命令全列出:

Terminal window
root@Master ~# kubectl api-versions
apps/v1
autoscaling/v1
autoscaling/v2
batch/v1
networking.k8s.io/v1
storage.k8s.io/v1
......
v1
# ⬆️ 每行一个”组/版本”(本集群共23个);最后那个孤零零没名字的 v1 = 核心 API 组
# 它特殊到”连组名都没有”——这就是写 Pod/Service 清单时只写 v1 的原因

版本后缀是稳定度阶梯:v1(稳定,生产可用)> v1beta1(测试版,字段可能变)> v1alpha1(实验版,别碰)

Important
  • 记法:没斜杠(v1)= 核心组;有斜杠(apps/v1)= 命名组
  • apiVersion 告诉 API Server 用哪套 schema 解析你的 yaml,写错直接报错拒收
  • 忘了该写啥?kubectl api-versions 查组,kubectl explain <资源>.apiVersion 看 GROUP 行

实战案例:

命令选项说明
kubectl get—show-labels额外显示 LABELS 列(观察 pod-template-hash 必备)
kubectl get-o wide多显示 IP、NODE 等列;-o yaml/-o json 看完整配置
kubectl getdeploy,rs,po逗号连写多种资源,一次看全三层
kubectl delete pods-l apps=xiuxian按标签批量删(—all = 删当前命名空间全部)
kubectl scale—replicas=5直接改副本数;也可改 yaml 后重新 apply
kubectl set imagedeployment/<名> <容器名>=<镜像>容器名必须和 yaml 里的 name: c1 对上
kubectl rollout status—实时盯滚动进度,Ctrl+C 退出不影响更新本身
kubectl rollout history—列出 REVISION 版本号;CHANGE-CAUSE 默认空,`kubectl annotate deployment/xxx kubernetes.io/change-cause=“v1→v2” 可补记录
kubectl rollout undo—to-revision=1指定回滚到哪个版本;不加则默认回上一个版本
kubectl delete deploy—cascade=orphan只删 Deployment,留下 Pod 成”孤儿”继续跑(默认是级联全删)
Terminal window
1)编写资源清单
root@Master ~# vim /k8s/pods/06-deploy-xiuxian.yaml
apiVersion: apps/v1 # ⭐ 首见!Deployment 属 apps 命名API组(Pod 属核心组只写 v1),详见上一节
kind: Deployment
metadata:
name: deploy-xiuxian
spec:
replicas: 3 # 副本数量3
selector:
matchLabels: # 注意:Deployment 用 matchLabels(rc 用 selector)
apps: xiuxian # 关联Pod的标签(必须能匹配 template 里的标签)
template:
metadata:
labels:
apps: xiuxian
version: v1
spec:
containers:
- name: c1 # 后面 set image 要用这个名字定位容器
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1
root@Master ~# kubectl apply -f /k8s/pods/06-deploy-xiuxian.yaml
deployment.apps/deploy-xiuxian created
2)查看 Deployment、ReplicaSet 和 Pod
root@Master ~# kubectl get deploy,rs,po --show-labels -o wide
NAME READY UP-TO-DATE AVAILABLE AGE ... SELECTOR LABELS
deployment.apps/deploy-xiuxian 3/3 3 3 81s ... apps=xiuxian
......
replicaset.apps/deploy-xiuxian-54c5c88868 3 3 3 81s ... apps=xiuxian,pod-template-hash=54c5c88868
# ⬆️ RS 自动多出 pod-template-hash=54c5c88868 标签——模板内容的指纹
# 这个 hash 就是 Deployment 区分"版本"的钥匙:一个版本的模板 = 一个 RS
pod/deploy-xiuxian-54c5c88868-tnkcg 1/1 Running 0 81s 10.100.1.19 worker01 apps=xiuxian,pod-template-hash=54c5c88868,version=v1
pod/deploy-xiuxian-54c5c88868-tszks 1/1 Running 0 81s 10.100.1.20 worker01 ...(第3个同理,略)
# ⬆️ 层级:Deployment → RS → Pod;三个副本落在 worker01×2 + worker02×1
3)删除 Pod 观察自动重建
root@Master ~# kubectl delete pods -l apps=xiuxian
pod "deploy-xiuxian-54c5c88868-tnkcg" deleted from default namespace
...(3个全删,输出略)
# 稍等片刻...
root@Master ~# kubectl get deploy,po
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/deploy-xiuxian 3/3 3 3 5m55s
pod/deploy-xiuxian-54c5c88868-fjgzw 1/1 Running 0 12s # ⬅️ 新名字
pod/deploy-xiuxian-54c5c88868-sntht 1/1 Running 0 12s
pod/deploy-xiuxian-54c5c88868-v6rdt 1/1 Running 0 12s
# ⬆️ Pod 名字全变了,但前缀 RS hash(54c5c88868)没变——补副本是 RS 的活,版本没变
4)扩缩容(一行命令搞定)
root@Master ~# kubectl scale deployment deploy-xiuxian --replicas=5
deployment.apps/deploy-xiuxian scaled
root@Master ~# kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
deploy-xiuxian 5/5 5 5 6m40s
5)滚动更新:v1 → v2
root@Master ~# kubectl set image deployment deploy-xiuxian c1=registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2
deployment.apps/deploy-xiuxian image updated
root@Master ~# kubectl rollout status deployment deploy-xiuxian
deployment "deploy-xiuxian" successfully rolled out
root@Master ~# kubectl get rs,po --show-labels
NAME DESIRED CURRENT READY AGE LABELS
replicaset.apps/deploy-xiuxian-54c5c88868 0 0 0 7m26s apps=xiuxian,pod-template-hash=54c5c88868,version=v1
replicaset.apps/deploy-xiuxian-864bdf9885 5 5 5 29s apps=xiuxian,pod-template-hash=864bdf9885,version=v1
# ⬆️ 关键现象:旧 RS(54c5c88868)DESIRED 归 0 但【没有删除】,新 RS(864bdf9885)接管全部 5 个副本
NAME READY STATUS RESTARTS AGE LABELS
pod/deploy-xiuxian-864bdf9885-4nljh 1/1 Running 0 29s apps=xiuxian,pod-template-hash=864bdf9885,version=v1
pod/deploy-xiuxian-864bdf9885-kwpvr 1/1 Running 0 28s apps=xiuxian,pod-template-hash=864bdf9885,version=v1
......
# ⬆️ 新 Pod 名字前缀全部变成 864bdf9885——旧 RS 缩到 0 却留着 = "后悔药",这就是秒级回滚的原因
6)查看版本历史
root@Master ~# kubectl rollout history deployment deploy-xiuxian
deployment.apps/deploy-xiuxian
REVISION CHANGE-CAUSE
1 <none>
2 <none>
# ⬆️ REVISION 1 = v1 镜像(旧 RS 54c5c88868),REVISION 2 = v2 镜像(当前 RS 864bdf9885)
7)回滚:v2 → v1(后悔药)
root@Master ~# kubectl rollout undo deployment deploy-xiuxian --to-revision=1
Warning: resource deployments/deploy-xiuxian was previously managed with 'kubectl apply'.
Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation,
which may cause unexpected behavior on future 'kubectl apply' operations.
Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/deploy-xiuxian rolled back
# ⬆️ 警告解释:你的 Deployment 是 apply 创建的,k8s 在注解里存了"上次 apply 的完整配置"
# rollout undo 只改运行时状态、不更新这个注解——如果之后 yaml 里还是 v1 而注解记着别的,可能行为错乱
# ⬆️ 正确做法:回滚后立刻把 yaml 改回 apps:v1,再 kubectl apply -f 一次刷新注解,警告即消
root@Master ~# kubectl get deploy,rs
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/deploy-xiuxian 5/5 5 5 10m
......
replicaset.apps/deploy-xiuxian-54c5c88868 5 5 5 10m # ⬅️ 旧RS原地满血复活!
replicaset.apps/deploy-xiuxian-864bdf9885 0 0 0 3m7s # ⬅️ v2的RS缩到0
# ⬆️ 回滚 = 两个 RS 的期望值互换,镜像早就在节点上,不用重新拉,所以飞快
8)删除 Deployment → 拔掉插线板,RS 和 Pod 一起清场
root@Master ~# kubectl delete -f /k8s/pods/06-deploy-xiuxian.yaml
deployment.apps "deploy-xiuxian" deleted from default namespace
root@Master ~# kubectl get deploy,rs,po
No resources found in default namespace.
# ⬆️ 删的是 Deployment,RS 和 Pod 一并消失
# 只想删 Deployment 留下 Pod:kubectl delete deploy deploy-xiuxian --cascade=orphan
Important

Deployment 核心结论:

  • 删 Pod → RS 自动补新的,副本数永远=replicas(和 rc 完全一样)
  • 删 Deployment → 名下 RS 和 Pod 一起被带走(--cascade=orphan 可只删上层留下 Pod)
  • 滚动更新的本质 = 换一个新 RS:新 RS 逐个扩容、旧 RS 逐个缩容,全程副本数不跌、服务不断
  • 旧 RS 只缩到 0 不删除——这就是“后悔药”:rollout undo 时新旧 RS 期望值一互换,回滚秒级完成
  • 三板斧记牢:kubectl rollout status / history / undo
  • rc 已过时,新环境一律用 Deployment

Service 管理实战#

什么是 svc#

Note

svc 是 Service 的简称,基于标签选择器关联后端 Pod,为 Pod 代理请求

svc 有三大功能:

  • ① 为 Pod 提供统一访问入口(固定 IP,Pod 换了它不变)
  • ② 实现 Pod 的负载均衡(请求轮询分发到后端)
  • ③ 实现 Pod 的服务发现(后端 Pod 变了,Endpoints 自动更新)

ClusterIP 类型 Service 功能验证#

Terminal window
1)环境准备:部署 Deployment(3个Pod)+ 给每个Pod写入不同首页
root@Master ~# kubectl apply -f /k8s/pods/06-deploy-xiuxian.yaml
deployment.apps/deploy-xiuxian created
root@Master ~# kubectl get pods -o wide -l apps=xiuxian
NAME READY STATUS RESTARTS AGE IP NODE
deploy-xiuxian-54c5c88868-277nk 1/1 Running 0 5s 10.100.2.43 worker02
deploy-xiuxian-54c5c88868-fkt99 1/1 Running 0 5s 10.100.1.46 worker01
deploy-xiuxian-54c5c88868-qr7lq 1/1 Running 0 5s 10.100.1.47 worker01
# 给3个Pod写入不同首页(写各自Pod名),便于观察负载均衡
root@Master ~# kubectl exec deploy-xiuxian-54c5c88868-277nk -- sh -c "echo 111 > /usr/share/nginx/html/index.html"
root@Master ~# kubectl exec deploy-xiuxian-54c5c88868-fkt99 -- sh -c "echo 222 > /usr/share/nginx/html/index.html"
root@Master ~# kubectl exec deploy-xiuxian-54c5c88868-qr7lq -- sh -c "echo 333 > /usr/share/nginx/html/index.html"
2)创建 ClusterIP Service
root@Master ~# vim /k8s/pods/07-svc-xiuxian.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-xiuxian
labels:
school: kpyun
spec:
selector: # 标签选择器:筛选出带这些标签的 Pod 作为后端
apps: xiuxian
version: v1
ports:
- port: 90 # svc 的端口(客户端访问的端口)
targetPort: 80 # 容器端口(流量最终转发到这里)
root@Master ~# kubectl apply -f /k8s/pods/07-svc-xiuxian.yaml
service/svc-xiuxian created
3)查看 svc(详情里的 Endpoints 就是后端 Pod 清单)
root@Master ~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.200.0.1 <none> 443/TCP 35h
svc-xiuxian ClusterIP 10.200.227.87 <none> 90/TCP 0s
# 看 selector、ClusterIP、端口映射、Endpoints 全貌
root@Master ~# kubectl describe svc svc-xiuxian
Selector: apps=xiuxian,version=v1 # ⬅️ 靠它找后端
Type: ClusterIP
IP: 10.200.227.87
Port: <unset> 90/TCP
TargetPort: 80/TCP
Endpoints: 10.100.2.43:80,10.100.1.46:80,10.100.1.47:80
# ⬆️ 三个 Endpoints 正好对应第1步三个Pod的IP:容器端口
4)测试负载均衡(循环访问10次,看轮流分发)
root@Master ~# for i in $(seq 10); do curl -s 10.200.227.87:90; done
222
222
111
222
333
222
...
# 同一个 svc IP:90,10次请求分给了3个后端 → 负载均衡✅
# 注意:是"轮询"不是"严格交替",短时间连接复用可能连到同一后端,次数多了才均匀
5)验证服务发现(删除Pod后 Endpoints 自动更新,客户端无感知)
root@Master ~# kubectl delete pods -l apps=xiuxian
root@Master ~# kubectl get pods -l apps=xiuxian -o wide
deploy-xiuxian-54c5c88868-m2hsx 1/1 Running 0 11s 10.100.1.48 worker01
deploy-xiuxian-54c5c88868-s9lc6 1/1 Running 0 11s 10.100.2.45 worker02
deploy-xiuxian-54c5c88868-t6w7b 1/1 Running 0 11s 10.100.2.44 worker02
# 新Pod新IP
root@Master ~# kubectl get endpoints svc-xiuxian
NAME ENDPOINTS AGE
svc-xiuxian 10.100.1.48:80,10.100.2.44:80,10.100.2.45:80 18s
# ⬆️ Pod 全换了、IP 全变了,Endpoints 自动跟上 → 服务发现✅(客户端无感知)
# ⬆️ 客户端继续 curl 10.200.227.87:90 依然正常(新Pod是默认首页,因为我们只给旧Pod写过自定义内容)
`集群内还可以直接用 svc 名字访问(DNS 服务发现,CoreDNS 提供)`
root@Master ~# kubectl run -it dns-test --image=registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1 --rm -- sh -c "curl -s http://svc-xiuxian:90 | grep h1"
'临时测试 Pod 用完即删'
<h1 style="color: green">凡人修仙传 v1 </h1>
# ⬆️ 在集群内任意 Pod 里,svc-xiuxian 这个名字会被 DNS 解析成 ClusterIP → 服务发现的另一半
6)清理实验环境
root@Master ~# kubectl delete -f /k8s/pods/07-svc-xiuxian.yaml
service "svc-xiuxian" deleted
root@Master ~# kubectl get svc
# default 命名空间恢复干净(仅剩默认的 kubernetes svc)
Important

服务发现的价值:后端 Pod 挂了/换了,svc 的 Endpoints 自动更新,客户端只要记住 svc 的名字/IP,永远不用管后端是谁

svc 的四种类型#

Terminal window
# 查看svc的TYPE类型
root@Master ~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.200.0.1 <none> 443/TCP 6d23h
svc-xiuxian ClusterIP 10.200.217.133 <none> 90/TCP 111m
# kubectl explain:查看 Kubernetes 资源
`svc.spec.type:查看 Service 规格中 type 字段的详细说明`
root@Master ~# kubectl explain svc.spec.type
KIND: Service
VERSION: v1
FIELD: type <string>
ENUM:
ClusterIP
ExternalName
LoadBalancer
NodePort

Service类型
Service类型

Terminal window
1)ClusterIP——默认类型,“内部食堂”
图上最左边:Client 在 k8s 内部,通过 `clusterIP:81` 访问,svc 按 `selector: apps=v1` 转发到后端三个 Pod 的 `targetPort:80`
2)NodePort——“在每个工作节点上开一扇门”
第二张图多画了两个东西:下面一排“K8S集群'所有'工作节点”+ `nodePort: 30000~32767`,以及一个从集群外部伸进来的 Client
3)LoadBalancer——“请个专业门卫”
第三张图在 NodePort 外面又包了一层粉色的 MetalLB(10.0.0.150~10.0.0.180),外部 Client 访问的是这段地址
4)ExternalName——“它不代理 Pod,只是个 DNS 别名”
最后一张图完全不同:没有 Pod、没有 selector,Service 下面直接挂着 Client,右边通过 `CNAME(DNS)` 指向集群外的 MySQL
类型访问入口(全部可用)典型场景备注
ClusterIP① 集群内 → ClusterIP内部服务互访(前后端、连DB)默认类型,不指定就是它
NodePort① 集群内 → ClusterIP
② 集群外 → 任意节IP
测试环境对外、没有 LB 时的临时方案包含 ClusterIP 能力
nodePort 范围 30000~32767;①② 同时有效
LoadBalancer① 集群内 → ClusterIP
② 集群外 → 节点IP
③ 集群外 → LB的VIP
外部 LB VIP → 节点nodePort → Pod
生产环境对外暴露(公有云 SLB、自建 MetalLB/OpenELB)包含 NodePort 能力,最贵也最常用
①②③ 同时有效,③最终也流经②的 nodePort
ExternalName集群内 → svc名(DNS 直接 CNAME 到外部域名,流量不经过 svc)集群内用固定名字访问外部服务(如外部 MySQL)无 selector、无 ClusterIP、无端口,是“假的 Service”
Important

一句话记忆:

  • 前三种是层层套娃:LoadBalancer ⊃ NodePort ⊃ ClusterIP,每层多解决一个“谁在访问”的问题
  • 判断口诀:谁访问? 集群内自己人 → ClusterIP;集群外但图省事 → NodePort;集群外且是生产 → LoadBalancer;根本不是访问 Pod 而是给外部服务起别名 → ExternalName
  • 我们实验过的 svc-xiuxian 就是默认的 ClusterIP——所以只有集群内的 Pod/节点能 curl 通,你笔记本上是访问不到的

NodePort 类型暴露 Pod 服务#

Terminal window
1)编写资源清单
root@Master ~# vim /k8s/pods/08-svc-xiuxian-NodePort.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-xiuxian-nodeport
spec:
type: NodePort # ★ 与 ClusterIP 唯一的区别:多这一行
# 在 ClusterIP 能力之上,给"所有节点"各开一个 nodePort
selector: # 标签选择器:筛出带这些标签的 Pod 作为后端
apps: xiuxian
version: v1
ports:
- port: 90 # svc 端口:集群内部走 ClusterIP:90(ClusterIP 能力依然保留)
targetPort: 80 # 容器端口:流量最终转发到 Pod 的 80
# nodePort: 30090 # 节点端口:不写则系统自动从 30000~32767 随机分配
# 生产建议不写(避免端口冲突),测试要固定时才指定
root@Master ~# kubectl apply -f /k8s/pods/08-svc-xiuxian-NodePort.yaml
service/svc-xiuxian-nodeport created
2)查看(NodePort 端口 32099 是自动分配的)
root@Master ~# kubectl get svc svc-xiuxian-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc-xiuxian-nodeport NodePort 10.200.62.199💡 <none> 💡90:30794/TCP 19s
# ClusterIP 照样分配 90:30794 表示 ClusterIP:90 + 每个节点:30794 都能访问
3)路径① 集群内(老入口,ClusterIP 能力)
root@Master ~# for i in $(seq 5); do curl -s 10.200.62.199:90; done
222
333
111
222
222
4)路径② 集群外(新入口,任意节点IP:nodePort)
root@Master ~# NPORT=$(kubectl get svc svc-xiuxian-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
root@Master ~# for ip in 10.0.0.13 10.0.0.14 10.0.0.15; do \
echo "$ip:$NPORT => HTTP $(curl -s -o /dev/null -w '%{http_code}' $ip:$NPORT)"; done
10.0.0.13:30794 => HTTP 200
10.0.0.14:30794 => HTTP 200
10.0.0.15:30794 => HTTP 200
# ⬆️ 三个节点都能通过 NodePort 访问并转发到后端 Pod!这就是集群外用户访问的方式
Important

NodePort 本质:在 ClusterIP 基础上,给每个节点的指定端口都开了转发,集群外用户访问 任意节点IP:NodePort 就能进到 svc,再由 svc 负载均衡到后端 Pod

Service 的底层实现:kube-proxy + iptables#

Terminal window
# nodePort 30794 上根本没有进程在监听!
root@Master ~# ss -lntp | grep 30794 | wc -l
0
# ⬆️ 那 30794 是谁接的流量?答案是内核 netfilter
# kube-proxy 只是"规则的编写者",不转发任何流量,它只负责把 svc/pod 的变化翻译成 iptables 规则
`读规则:一条请求的旅程(沿着链名追)`
root@Master ~# iptables-save | grep 30794
-A KUBE-NODEPORTS -p tcp ... --dport 30794 -j KUBE-EXT-HDYZGZXZ7A2KN5KK
# ⬆️ 第1跳:凡访问"任意节点IP:30794"的 tcp 包,跳转到 KUBE-EXT-xxx 链
# (注意没有 -d 限制 → 所以三个节点 IP 都通,你 curl 3 个都返回 200)
root@Master ~# iptables-save | grep KUBE-EXT-HDYZGZXZ7A2KN5KK
-A KUBE-EXT-HDYZGZXZ7A2KN5KK ... -j KUBE-MARK-MASQ # 外部进来的包先做 SNAT 标记 ✅
-A KUBE-EXT-HDYZGZXZ7A2KN5KK -j KUBE-SVC-HDYZGZXZ7A2KN5KK
# ⬆️ 第2跳:KUBE-EXT 链 = 打标记(伪装) + 进入 KUBE-SVC 链
root@Master ~# iptables-save | grep KUBE-SVC-HDYZGZXZ7A2KN5KK
-A KUBE-SERVICES -d 10.200.62.199/32 --dport 90 -j KUBE-SVC-HDYZGZXZ7A2KN5KK
# ⬆️ 惊喜在这:ClusterIP:90 的规则也跳到同一个 KUBE-SVC-xxx 链!
# 证明"入口叠加不关闭"——nodePort 和 ClusterIP 两条路在此汇合,后面完全共用
-A KUBE-SVC-... --probability 0.33333333349 -j KUBE-SEP-UU3FPI5DYYJF3YVO # → 10.100.1.54:80
-A KUBE-SVC-... --probability 0.50000000000 -j KUBE-SEP-DR6WXWV734UWRDCJ # → 10.100.2.50:80
-A KUBE-SVC-... -j KUBE-SEP-FNYYL37A75CRRGKG # → 10.100.2.51:80
# ⬆️ 第3跳:负载均衡的真相——三条规则按概率随机分流(--mode random)
# 第1条 1/3 概率命中;没中到第2条,剩2个目标,概率写 1/2;最后一条兜底必然命中
# (1/3, 1/2, 兜底) → 三个后端各约 1/3 → 这就是"随机负载均衡"的数学实现

nat 表 POSTROUTING(包出节点前的最后一站,SNAT区)

nat 表 PREROUTING(包进节点的第一站,DNAT区)

任意节点IP:30794

ClusterIP:90

源是Pod网段→不盖章

概率 1/3

概率 1/2

兜底必中

Pod在本机

Pod在其他节点

有章 → MASQUERADE

源IP改为节点IP(SNAT)

无章 → RETURN

保留 Pod 源IP

外部客户端

集群内 Pod 客户端

KUBE-NODEPORTS

匹配 --dport 30794

KUBE-SERVICES

匹配 目的IP=ClusterIP

KUBE-EXT-xxx

无条件盖章 0x4000

(MASQ 标记,还不做NAT)

KUBE-SVC-xxx

随机概率分流

KUBE-SEP-1

仅 DNAT → Pod1:80

KUBE-SEP-2

仅 DNAT → Pod2:80

KUBE-SEP-3

仅 DNAT → Pod3:80

路由决策

(目的地址已改成PodIP)

走 veth/cni0 送到本机Pod

FORWARD + flannel 隧道

KUBE-POSTROUTING

检查 0x4000 印章

外部路径:回包能原路返回

内部路径:后端看到真实客户端IP

nat 表 POSTROUTING(包出节点前的最后一站,SNAT区)

nat 表 PREROUTING(包进节点的第一站,DNAT区)

任意节点IP:30794

ClusterIP:90

源是Pod网段→不盖章

概率 1/3

概率 1/2

兜底必中

Pod在本机

Pod在其他节点

有章 → MASQUERADE

源IP改为节点IP(SNAT)

无章 → RETURN

保留 Pod 源IP

外部客户端

集群内 Pod 客户端

KUBE-NODEPORTS

匹配 --dport 30794

KUBE-SERVICES

匹配 目的IP=ClusterIP

KUBE-EXT-xxx

无条件盖章 0x4000

(MASQ 标记,还不做NAT)

KUBE-SVC-xxx

随机概率分流

KUBE-SEP-1

仅 DNAT → Pod1:80

KUBE-SEP-2

仅 DNAT → Pod2:80

KUBE-SEP-3

仅 DNAT → Pod3:80

路由决策

(目的地址已改成PodIP)

走 veth/cni0 送到本机Pod

FORWARD + flannel 隧道

KUBE-POSTROUTING

检查 0x4000 印章

外部路径:回包能原路返回

内部路径:后端看到真实客户端IP

链名字含义干什么
KUBE-NODEPORTS节点端口入口匹配 nodePort,认领外部流量
KUBE-SERVICES服务入口匹配 ClusterIP,认领内部流量
KUBE-EXT-xxx外部扩展外部流量多做一步 MASQ 标记,再转交 SVC
KUBE-SVC-xxx服务分发负载均衡发生地:按概率随机选一个后端
KUBE-SEP-xxx单个后端 (endpoint)执行 DNAT:把目的地址改成 PodIP<80>
Important

底层结论:

  • svc 是假的,iptables 是真的——get svc 里那个 ClusterIP 从未被任何设备“持有”,ping 不通是正常的(没有规则响应 icmp),curl 通才说明规则在工作
  • kube-proxy 的三种模式:iptables(默认、最常见)→ IPVS(大规模集群,哈希表查找比遍历规则快)→ ebpf(如 Cilium,彻底绕开 kube-proxy)
  • 概率分流 (1/3, 1/2, 兜底) 是 iptables 实现随机负载均衡的经典手法
  • 为什么外部流量要 MASQ/SNAT:Pod 回包如果不经过 SNAT,会直接回给客户端源地址导致连接错乱——所以 KUBE-EXT 先标记再转发

顺带一提:这也解释了实验课上一个高频疑问——“为什么 ping ClusterIP 不通但服务正常?” 内核里没有 IP 属于这台机器,icmp 包撞不上任何规则,自然无人应答;而 tcp 包一进来就被 DNAT 走了


名称空间#

什么是名称空间#

Note

Namespace 是 k8s 里的“资源分组/隔离单位”——同一集群内划分出多个逻辑上的“独立空间”,不同团队/项目/环境各用各的,互不干扰

注意:它只是资源的逻辑分组,不是网络隔离、也不是资源配额(那些要靠 NetworkPolicy、ResourceQuota 实现)

Terminal window
root@Master ~# kubectl api-resources
NAME SHORTNAMES APIVERSION NAMESPACED KIND
bindings v1 true Binding
componentstatuses cs v1 false ComponentStatus
configmaps cm v1 true ConfigMap
endpoints ep v1 true Endpoints
events ev v1 true Event
limitranges limits v1 true LimitRange
namespaces ns v1 false Namespace
nodes no v1 false Node
# 不是所有资源都支持名称空间,看 NAMESPACED 列(true=支持,false=全局)⬆️

响应式管理名称空间#

Terminal window
1)查看名称空间
root@Master ~# kubectl get ns
NAME STATUS AGE
default Active 35h # 不指定 -n 时资源默认落这里
kube-flannel Active 24h # 装 flannel 时带来的,不是内置的 🔥
kube-node-lease Active 35h # 内置:kubelet 心跳租约
kube-public Active 35h # 内置:公共资源
kube-system Active 35h # 内置:集群系统组件
# 内置只有4个 ✅(default + 后面三个);kube-flannel 是装了 flannel 才出现的
# 换别的网络插件(如 calico)就会出现 kube-calico 之类的,名字跟着插件走
2)创建名称空间
root@Master ~# kubectl create ns kpyun
namespace/kpyun created
# 也可以用 yaml(声明式)kind: Namespace + metadata.name,apply 进去
3)在指定名称空间创建资源(-n 指定)
root@Master ~# kubectl run xixi --image=registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1 -n kpyun
pod/xixi created
root@Master ~# kubectl run haha --image=registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2 -n kpyun
pod/haha created
# 不加 -n 就落到 default —— 名称空间必须在"创建那一刻"指定,资源建好后不能搬家(删了重建是唯一的"搬家"方式)
4)查看指定名称空间资源
root@Master ~# kubectl get pods -n kpyun -o wide
NAME READY STATUS RESTARTS AGE IP NODE
haha 1/1 Running 0 7s 10.100.1.58 worker01
xixi 1/1 Running 0 11s 10.100.1.57 worker01
root@Master ~# kubectl get pods -o wide
No resources found in default namespace.
# ⬆️ 不加 -n 默认查 default → kpyun 里的看不到(这就是"隔离"最直观的体现)
# 注意 IP:两个 Pod 都是 10.100.1.x —— 名称空间不隔离网络!跨 ns 的 Pod 照常互通
5)查看所有名称空间资源
root@Master ~# kubectl get pods -A
# -A (--all-namespaces):一眼看全集群所有 Pod,排障时最常用
NAMESPACE NAME READY STATUS RESTARTS AGE
kpyun haha 1/1 Running 0 15s
kpyun xixi 1/1 Running 0 15s
kube-flannel kube-flannel-ds-b5dtr 1/1 Running 4 24h
kube-system coredns-558f5766d5-x52j5 1/1 Running 4 35h
...
6)删除名称空间(级联删除里面所有资源,生产慎用!)
root@Master ~# kubectl delete ns kpyun
namespace "kpyun" deleted
root@Master ~# kubectl get pods -n kpyun
No resources found in kpyun namespace.
# 和删 Deployment 连带 RS/Pod 一个道理:删 ns = 拔掉整个插线板
# 里面的 haha、xixi 一个不剩全部带走,没有"孤儿"选项
Caution

删除名称空间 = 级联删除该 ns 下所有资源,生产环境慎用!!!建议重要 ns 开启保护,或用 RBAC 限制 delete ns 权限

Important

名称空间核心结论:

  • 内置 4 个,网络插件等组件会自带自己的 ns——看 ns 列表能反推出集群装过什么
  • -n 是 k8s 的“作用域开关”:不加就是 default,-A 就是全局视角
  • 名称空间隔离的是资源视图(kubectl 层面看不见彼此),不隔离网络(跨 ns 的 Pod IP 互通,DNS 也能跨 ns 解析)
  • 删 ns = 级联删除内部所有资源,生产环境慎之又慎

声明式管理名称空间#

Terminal window
1)编写资源清单(一个文件多个资源,用 --- 分隔)
root@Master ~# vim /k8s/pods/09-ns-svc-pods.yaml
apiVersion: v1
kind: Namespace # 文档1:声明式创建名称空间(等价 kubectl create ns kpyun)
metadata:
name: kpyun
--- # ⭐ 多文档分隔符:一个 yaml 文件里塞多个资源,apply 一次性提交
apiVersion: v1
kind: Pod
metadata:
name: xixi
labels:
apps: xiuxian # 故意和 haha 打同一个标签!测试 svc 能否跨 ns 选中它
namespace: kpyun # ⭐ 资源写进哪个 ns,就在 metadata.namespace 指定
spec:
containers:
- name: c1
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1
---
apiVersion: v1
kind: Pod
metadata:
name: haha
labels:
apps: xiuxian
namespace: default # default 可省略,写出来为了对比醒目
spec:
containers:
- name: c1
image: registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2
---
apiVersion: v1
kind: Service
metadata:
name: svc-xiuxian # ⭐ 没写 namespace → 落在 default(伏笔:它只能选中 default 里的Pod)
spec:
type: ClusterIP # 默认类型也是它
selector:
apps: xiuxian # 标签 xixi/haha 都匹配,但 selector 只在"自己所在的ns"内找!
ports:
- port: 90
targetPort: 80
root@Master ~# kubectl apply -f /k8s/pods/09-ns-svc-pods.yaml
namespace/kpyun created
pod/xixi created
pod/haha created
service/svc-xiuxian created
# ⬆️ 一次 apply 建了4种资源——这就是声明式:命令行敲4条 → 一个文件搞定,可重复apply、可进git
2)查看资源(xixi 在 kpyun,haha 在 default)
root@Master ~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
haha 1/1 Running 0 6s 10.100.2.54 worker02 # default 视角只见 haha
root@Master ~# kubectl get pods -n kpyun -o wide
NAME READY STATUS RESTARTS AGE IP NODE
xixi 1/1 Running 0 6s 10.100.1.59 worker01 # kpyun 视角只见 xixi
3)关键验证:svc(default里)到底选中了谁?
root@Master ~# kubectl describe svc svc-xiuxian | grep -E "Namespace|IP|Port|Endpoints"
Namespace: default
Type: ClusterIP
IP: 10.200.45.202
IPs: 10.200.45.202
Port: <unset> 90/TCP
Endpoints: 10.100.2.54:80
# ⬆️ 只有 haha 的IP!xixi(10.100.1.59) 标签明明匹配却没进 Endpoints
# 因为 svc 的 selector 只在自己所在的 ns 里筛标签
root@Master ~# for i in $(seq 6); do curl -s 10.200.45.202:90 | grep h1; done
<h1 style="color: red">凡人修仙传 v2 </h1> ×6
# ⬆️ 访问6次全是 v2(haha) → 流量永远到不了 kpyun 里的 xixi
4)但是!直接 curl 跨 ns 的 Pod IP —— 通!
root@Master ~# curl -s 10.100.1.59 | grep h1
凡人修仙传 v1
# ⬆️ xixi 在 kpyun 里,从 default 的视角直接访问它的 Pod IP,照样通!
# → 网络数据面完全不隔离,"隔离"的只是 svc selector 的匹配范围(控制面)
5)对照组:在 kpyun 名称空间中"再建一个"同名 svc(注意:不是搬家,是新建!)
root@Master ~# vim /k8s/pods/10-svc-in-kpyun.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-xiuxian
namespace: kpyun # ⭐ 唯一改动:指定名称空间(svc-xiuxian同名不冲突:不同 ns 就是不同资源)
spec:
type: ClusterIP
selector:
apps: xiuxian
ports:
- port: 90
targetPort: 80
root@Master ~# kubectl apply -f /k8s/pods/10-svc-in-kpyun.yaml
service/svc-xiuxian created
`⭐ 同一条命令、同一个 svc 名字,只有 -n 不同 → 看到的是两个不同的 svc!`
root@Master ~# kubectl describe svc svc-xiuxian -n default | grep -E "Namespace|IP:|Port|TargetPort|Endpoints"
Namespace: default
IP: 10.200.45.202
Port: <unset> 90/TCP
TargetPort: 80/TCP
Endpoints: 10.100.2.54:80
root@Master ~# kubectl describe svc svc-xiuxian -n kpyun | grep -E "Namespace|IP:|Port|TargetPort|Endpoints"
Namespace: kpyun
IP: 10.200.74.188
Port: <unset> 90/TCP
TargetPort: 80/TCP
Endpoints: 10.100.1.59:80
# ⬆️ 名字相同、selector 相同、端口相同——但 ns 不同 → 两个独立资源:
# ① 各自分配了不同的 ClusterIP(10.200.45.202 vs 10.200.74.188)
# ② 各自的 selector 只在自己 ns 里筛 → default 的选中 haha,kpyun 的选中 xixi
# 流量验证:各访问各的,井水不犯河水
root@Master ~# for i in $(seq 4); do curl -s 10.200.74.188:90 | grep h1; done
<h1 style="color: green">凡人修仙传 v1 </h1> ×4 # ⬅️ kpyun 的 svc → 永远只有 xixi(v1)
# (对照 default 的 10.200.45.202:90 → 永远只有 haha(v2),第3步已验证)
6)删除资源(删 ns 连带里面全部资源)
root@Master ~# kubectl delete -f /k8s/pods/09-ns-svc-pods.yaml
namespace "kpyun" deleted # ⬅️ 拆的是这栋楼
pod "xixi" deleted from kpyun namespace # ⬅️ 文件里点名的
pod "haha" deleted from default namespace # ⬅️ 文件里点名的
service "svc-xiuxian" deleted from default namespace # ⬅️ 文件里点名的
# kpyun 里的 svc-xiuxian:无声无息,跟着楼一起没了
Important

ns 是资源分组,不是网络边界:管得住“kubectl 看得见什么、selector 选得中什么”,管不住数据包——跨 ns 直连 Pod IP、DNS 解析全通

svc 只选同 ns 的 Pod:标签对却没流量?先 get svc -A 看它住在哪

yaml 只是出生证明:k8s 只认资源住在哪个 ns,不认它来自哪个文件——删 ns 就是全楼清场

记法:管视野(ns)、隔网络(NetworkPolicy)、隔权限(RBAC),各管各的


附加组件 CoreDNS 验证#

什么是 CoreDNS#

Note

CoreDNS 是 K8s 集群官方标配的 DNS 解析组件,负责将 svc 的名称解析为 ClusterIP

主要作用:

  • ① 将 svc 名称解析为 ClusterIP
  • ② 为 headless svc 的 Pod 提供负载均衡
  • ③ 可配置内网自定义域名解析

DNS A 记录格式:<Service_NAME>.<NAMESPACE>.svc.<域名>

域名取决于初始化时指定的 --service-dns-domain,本集群是 kpyun.com(默认是 cluster.local)

查看 CoreDNS 相关信息#

Terminal window
1)查看 CoreDNS Pod
root@Master ~# kubectl get pod -n kube-system -l k8s-app=kube-dns -o wide
NAME READY STATUS RESTARTS AGE IP NODE
coredns-558f5766d5-x52j5 1/1 Running 9 (3h34m ago) 7d22h 10.100.0.21 master
coredns-558f5766d5-xfdtf 1/1 Running 9 (3h34m ago) 7d22h 10.100.0.20 master
# 过滤这两个coredns
2)查看 kube-dns svc(ClusterIP 10.200.0.10)
root@Master ~# kubectl get -n kube-system svc kube-dns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.200.0.10 <none> 53/UDP,53/TCP,9153/TCP 7d22h
root@Master ~# kubectl describe -n kube-system svc kube-dns | egrep -i 'Type|IP:|Port|Endpoints'
Annotations: prometheus.io/port: 9153
Type: ClusterIP
IP: 10.200.0.10
Port: dns 53/UDP
TargetPort: 53/UDP
Endpoints: 10.100.0.20:53,10.100.0.21:53
Port: dns-tcp 53/TCP
TargetPort: 53/TCP
Endpoints: 10.100.0.20:53,10.100.0.21:53
Port: metrics 9153/TCP
TargetPort: 9153/TCP
Endpoints: 10.100.0.20:9153,10.100.0.21:9153
# Endpoints里面的 IP 都是上面两个 CoreDNS Pod的 IP地址
'在kube-system这个名称空间中,有个svc关联两个CoreDNS Pod'
3)进入 Pod 查看 DNS 配置(resolv.conf)——随便哪个 Pod 都可以
root@Master ~# kubectl run dns-test --image=registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1
root@Master ~# kubectl exec dns-test -n default -- cat /etc/resolv.conf
search default.svc.kpyun.com svc.kpyun.com kpyun.com # 搜索域
nameserver 10.200.0.10 # DNS 指向 CoreDNS
options ndots:5 # 点数阈值(后面细讲)
# 对比:另一个名称空间(kpyun)里 Pod 的 resolv.conf
root@Master ~# kubectl create ns kpyun
namespace/kpyun created
root@Master ~# kubectl get ns kpyun
NAME STATUS AGE
kpyun Active 12s
root@Master ~# kubectl run dns-test --image=registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1 -n kpyun
pod/dns-test created
`虽然pod名相同,但ns不同`
root@Master ~# kubectl exec -n kpyun dns-test -- cat /etc/resolv.conf
search kpyun.svc.kpyun.com svc.kpyun.com kpyun.com
# ⬆️ 三行里只有 search(搜索域)第一条不一样——它随【Pod所在ns】变化!
# search 列表 = [<Pod的ns>.svc.<域名>, svc.<域名>, <域名>]
# 结论:所有 Pod 的 nameserver 都指向 kube-dns 这个 svc,间接指向那两个 CoreDNS Pod
# 为什么用两个 CoreDNS Pod?冗余(一个挂了另一个顶上)+ 负载分担
4)Pod 内 ping svc 短名(ping 走系统 resolver,会自动按 search 拼接)
root@Master ~# kubectl label pod dns-test apps=xiuxian
# 为dns-test都打上标签(后面svc好过滤)
root@Master ~# kubectl label pod dns-test apps=xiuxian -n kpyun
`一个是default,一个是kpyun`
root@Master ~# kubectl get pods -A -o wide --show-labels | grep dns-test
default dns-test 1/1 Running 10.100.2.61 worker02 apps=xiuxian,run=dns-test
kpyun dns-test 1/1 Running 10.100.1.70 worker01 apps=xiuxian,run=dns-test
root@Master ~# vim /k8s/pods/11-svc-dns-xiuxian.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-xiuxian
namespace: default
spec:
selector: # 标签选择器:筛选带这些标签的 Pod 作为后端
apps: xiuxian
ports:
- port: 90
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: svc-xiuxian
namespace: kpyun # 只有名称空间不同,其他都相同
spec:
selector:
apps: xiuxian
ports:
- port: 90
targetPort: 80
root@Master ~# kubectl apply -f /k8s/pods/11-svc-dns-xiuxian.yaml
service/svc-xiuxian created
service/svc-xiuxian created
root@Master ~# kubectl describe svc svc-xiuxian -n default | egrep 'Namespace|IP:|Endpoints'
Namespace: default
IP: 10.200.240.34
Endpoints: 10.100.2.61:80
root@Master ~# kubectl describe svc svc-xiuxian -n kpyun | egrep 'Namespace|IP:|Endpoints'
Namespace: kpyun
IP: 10.200.114.224
Endpoints: 10.100.1.70:80
root@Master ~# kubectl exec dns-test -n default -- ping -c1 svc-xiuxian # default里的Pod
PING svc-xiuxian (10.200.240.34): 56 data bytes
`ping不通,底层不走ICMP包,域名能解析出来即可!` `<Service_NAME>.<NAMESPACE>.svc.<域名>`
# 短名被 search 第一条补全成 svc-xiuxian.default.svc.kpyun.com → 本ns的ClusterIP
root@Master ~# kubectl exec dns-test -n kpyun -- ping -c1 svc-xiuxian # kpyun里的Pod
PING svc-xiuxian (10.200.114.224): 56 data bytes
# ⬆️ 同一个短名,kpyun 的 Pod 拼出 svc-xiuxian.kpyun.svc.kpyun.com → kpyun里同名svc的ClusterIP
# "同名不冲突"的又一次体现:名字相同,解析结果随Pod所在ns而变
5)更常用的是直接 curl(应用层服务发现:代码里只写 svc名:端口)
root@Master ~# kubectl exec dns-test -- curl -s http://svc-xiuxian:90 | grep h1
<h1 style="color: green">凡人修仙传 v1 </h1>
# ⬆️ 拼接+解析+转发全自动,应用完全不感知后端 Pod 是谁 → 服务发现闭环

搜索域拼接对比实验#

Terminal window
1)跨ns简写"svc名.名称空间"——能通!
`search 列表 = [<Pod的ns>.svc.<域名>, svc.<域名>, <域名>]`
root@Master ~# kubectl exec dns-test -- ping -c1 svc-xiuxian.kpyun
PING svc-xiuxian.kpyun (10.200.114.224): 56 data bytes
# ⬆️ 拼1: svc-xiuxian.kpyun.default.svc.kpyun.com ✗
# 拼2: svc-xiuxian.kpyun.svc.kpyun.com ✓ 正好凑出完整A记录格式!
# "svc名.ns"跨ns简写能用的原理 = 命中第2条搜索域
2)缺.com的半截域名——失败
root@Master ~# kubectl exec dns-test -- ping -c1 svc-xiuxian.default.svc.kpyun
ping: bad address 'svc-xiuxian.default.svc.kpyun'
# ⬆️ 3个点<5 → 先拼接:+default.svc.kpyun.com / +svc.kpyun.com / +kpyun.com 全✗
# 再按绝对名字查:缺.com → NXDOMAIN ✗(补办法见 dig 节的 FQDN 尾点)
3)不存在的svc——干净失败
root@Master ~# kubectl exec dns-test -- ping -c1 nosuchsvc
ping: bad address 'nosuchsvc'
# ⬆️ 所有拼接 + 绝对查询全部 NXDOMAIN → 解析失败
4)ping vs curl:解析成功 ≠ ping 得通!
root@Master ~# kubectl exec dns-test -- ping -c2 svc-xiuxian | tail -1
2 packets transmitted, 0 packets received, 100% packet loss
root@Master ~# kubectl exec dns-test -- curl -s -o /dev/null -w "%{http_code}\n" http://svc-xiuxian:90
200
# ⬆️ DNS解析(UDP 53)成功拿到IP,但 ping(ICMP) 到 ClusterIP 无人应答:
# kube-proxy 写的 iptables 规则只处理 TCP/UDP,ICMP 包不匹配任何规则 → 石沉大海
# 所以"svc ping不通"不代表服务故障,curl/telnet 通了就行!
Note

options ndots:5 最后说清楚

分两个角色就不乱了:

  • 数数的人(resolver):每次查询自动数”你写的名字”里的点数和 5 比,决定先拼接还是先直查——你不用算
  • 写名字的人(你):只有一个决策——简称还是全名?
场景怎么写尾点
平时 curl/ping 集群内服务短名 svc-xiuxian不带,让系统拼
跨 nssvc名.ns 或全名不带也行
脚本 / 监控 / dig 排障全名带,1 次查询、结果不被污染
访问外部 API api.example.com原样不带(点数<5 会先白拼几次)

尾点本质是一句声明:“这名字是完整的,别帮我拼”——它不参与数点,直接终结比较流程

dig 验证 DNS 组件正常工作#

Terminal window
`环境准备`
# 我们用的 dnsutils(网络诊断镜像)新增 telnet、curl、mysql-client 工具,一站式网络诊断:
# docker.io 直连超时,本地走代理拉取 → 导入集群三步走
jiuzhao@Ubuntu ~$ docker pull docker.xuanyuan.run/jsha/dnsutils:latest # ①走代理拉取
jiuzhao@Ubuntu ~$ docker tag docker.xuanyuan.run/jsha/dnsutils:latest jsha/dnsutils:v1
# ★必须改tag::latest 默认 imagePullPolicy=Always 会去外网拉→超时;改成v1走IfNotPresent用本地
jiuzhao@Ubuntu ~$ docker save jsha/dnsutils:v1 -o dnsutils-v1.tar # ②导出并传入各节点
root@Master ~# ctr -n k8s.io images import /tmp/dnsutils-v1.tar # ③导入containerd
# ★必须 -n k8s.io:kubelet 只看这个命名空间里的镜像
root@Master ~# crictl images | grep dnsutils
docker.io/jsha/dnsutils v1 ...
# ⬆️ 导入后名字自动规范化,补全了 docker.io/ 前缀(Docker Hub是默认仓库)
# 三个节点都要导入(或者只导目标节点 + spec.nodeName 固定调度)
Terminal window
1)查看 svc 列表
root@Master ~# kubectl get svc -A
NAMESPACE NAME TYPE CLUSTER-IP PORT(S) AGE
default kubernetes ClusterIP 10.200.0.1 443/TCP 8d
default svc-xiuxian ClusterIP 10.200.240.34 90/TCP 57m
kpyun svc-xiuxian ClusterIP 10.200.114.224 90/TCP 57m
kube-system kube-dns ClusterIP 10.200.0.10 53/UDP,53/TCP,9153/TCP 8d
2)Master 宿主机自带 dig,直接指定 CoreDNS 解析
# `@` 指定问谁
root@Master ~# dig @10.200.0.10 kubernetes.default.svc.kpyun.com +short
10.200.0.1
root@Master ~# dig @10.200.0.10 svc-xiuxian.default.svc.kpyun.com +short
10.200.240.34
root@Master ~# dig @10.200.0.10 svc-xiuxian.kpyun.svc.kpyun.com +short
10.200.114.224
root@Master ~# dig @10.200.0.10 kube-dns.kube-system.svc.kpyun.com +short
10.200.0.10
# ✅ 四个 svc 全部正确解析为各自的 ClusterIP!
3)看拼接过程:+showsearch 把每次尝试的 QUESTION SECTION 打出来(用上一节的两个ns实验环境)
root@Master ~# kubectl run dnsutils --image=docker.io/jsha/dnsutils:v1 -n kpyun
pod/dnsutils created
root@Master ~# kubectl get pods -nkpyun -o wide
NAME READY STATUS RESTARTS AGE IP NODE
dns-test 1/1 Running 0 42m 10.100.1.70 worker01
dnsutils 1/1 Running 0 41s 10.100.1.72 worker01
# `+search` 模拟 Pod 内拼接行为、`+showsearch` 把每一次拼接尝试打印出来(过程层)
root@Master ~# kubectl exec dnsutils -n kpyun -- dig +search +showsearch svc-xiuxian.kpyun | grep -E "^;svc"
;svc-xiuxian.kpyun.kpyun.svc.kpyun.com. IN A # ← 拼1 NXDOMAIN
;svc-xiuxian.kpyun.svc.kpyun.com. IN A # ← 拼2 命中 → 10.200.114.224
root@Master ~# kubectl exec dnsutils -n kpyun -- dig +search +showsearch svc-xiuxian | grep -E "^;svc"
;svc-xiuxian.kpyun.svc.kpyun.com. IN A # ← 拼1即命中,后面不再试
root@Master ~# kubectl exec dnsutils -n kpyun -- dig +search +showsearch svc-xiuxian.kpyun.svc.kpyun | grep -E "^;svc"
;svc-xiuxian.kpyun.svc.kpyun.kpyun.svc.kpyun.com. IN A
;svc-xiuxian.kpyun.svc.kpyun.svc.kpyun.com. IN A
;svc-xiuxian.kpyun.svc.kpyun.kpyun.com. IN A
;svc-xiuxian.kpyun.svc.kpyun. IN A
# ⬆️ 对比一目了然:短名1次尝试就停;跨ns简写要2次;缺com的要4次全失败
# Pod里DNS查询量大的元凶就是这些"多余的拼接尝试"(ndots:5 + search列表)
4)FQDN 结尾带点 = 绝对域名,跳过一切拼接(排障时最快最准的姿势)
root@Master ~# dig @10.200.0.10 svc-xiuxian.default.svc.kpyun.com. +short
10.200.240.34
# ⬆️ 只发1次查询、绝不会被公网域名劫走答案,写监控/脚本时推荐这种写法
Important

dig 验证成功的意义:svc 的 DNS 名称能被 CoreDNS 解析成 ClusterIP,说明 CoreDNS 工作正常 这也解释了为什么 wordpress 能用 svc-db 这个名字连上 MySQL(CoreDNS 把 svc-db 解析成 ClusterIP)

Terminal window
1)nslookup 验证(注意:查完整FQDN没问题;但busybox的nslookup查短名不走search拼接,结果和容器真实行为不一致——短名测试请用ping/getent)
root@Master ~# kubectl exec dnsutils -n kpyun -- nslookup svc-xiuxian.default.svc.kpyun.com
Server: 10.200.0.10
Address: 10.200.0.10#53
Name: svc-xiuxian.default.svc.kpyun.com
Address: 10.200.240.34
2)curl 访问 svc
root@Master ~# kubectl exec dnsutils -n kpyun -- curl -s http://svc-xiuxian:90 | grep h1
<h1 style="color: green">凡人修仙传 v1 </h1>
3)telnet 测试端口连通性
root@Master ~# kubectl exec dnsutils -n kpyun -- sh -c 'echo hi | telnet svc-xiuxian 90 | head -3'
Connection closed by foreign host.
Trying 10.200.114.224...
Connected to svc-xiuxian.kpyun.svc.kpyun.com.
Escape character is '^]'.
4)mysql-client 工具
root@Master ~# kubectl exec dnsutils -n kpyun -- which mysql
/usr/bin/mysql
Tip

dnsutils 集成了 dig/nslookup/telnet/curl/mysql 五个工具,排障神器:

  • dig/nslookup → 查 DNS 解析
  • telnet → 测端口通不通
  • curl → 测 HTTP 服务
  • mysql → 测数据库连接

网络问题用它一个 Pod 全搞定

实战案例:deploy + svc 部署 WordPress#

使用 deploy 部署 MySQL#

Terminal window
1)编写资源清单(deploy 托管 MySQL,env 初始化)
root@Master ~# vim deploy-mysql.yaml
# rc 已过时,新环境一律用 deploy
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-mysqld-server
spec:
replicas: 1 # 副本数量1
selector:
matchLabels: # Deployment 用 matchLabels
apps: mysqld # 关联Pod的标签(必须能匹配 template 里的标签)
template:
metadata:
labels:
apps: mysqld
spec:
# 指定分配至worker01
nodeName: worker01
containers:
- name: c1
image: docker.io/library/mysql:8.0.36
# 向容器传递环境变量
env:
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "yes"
- name: MYSQL_DATABASE
value: "wordpress"
- name: MYSQL_USER
value: "kpyun"
- name: MYSQL_PASSWORD
value: "kpyun123"
- name: "TZ"
value: "Asia/Shanghai"
args:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_bin
- --default-authentication-plugin=mysql_native_password
root@Master ~# kubectl apply -f deploy-mysql.yaml
deployment.apps/deploy-mysqld-server created
2)查看状态
root@Master ~# kubectl get deploy,rs,pods -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/deploy-mysqld-server 1/1 1 1 22s c1 docker.io/library/mysql:8.0.36 apps=mysqld
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/deploy-mysqld-server-77589f5fcf 1 1 1 22s c1 docker.io/library/mysql:8.0.36 apps=mysqld,pod-template-hash=77589f5fcf
NAME READY STATUS RESTARTS AGE IP NODE
pod/deploy-mysqld-server-77589f5fcf-pdbxg 1/1 Running 0 22s 10.100.1.75 worker01
3)验证数据库和用户
root@Master ~# kubectl exec -it deploy-mysqld-server-77589f5fcf-pdbxg -- mysql wordpress
Welcome to the MySQL monitor
Server version: 8.0.36 MySQL
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| wordpress | # ✅ wordpress 库自动创建
+--------------------+
mysql> SELECT user,host,plugin FROM mysql.user;
+------------------+-----------+-----------------------+
| user | host | plugin |
+------------------+-----------+-----------------------+
| kpyun | % | mysql_native_password |
# ✅ kpyun 用户就绪

基于 deploy 部署 WordPress#

Terminal window
1)编写资源清单
`svc-db 关联 MySQL,wordpress 通过 svc-db 连库,svc-wp 用 NodePort 暴露`
root@Master ~# vim deploy-wordpress.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-db # 给 MySQL 做服务发现
spec:
selector:
apps: mysqld # 基于这个标签进行过滤
ports:
- port: 3306 # svc 端口:wordpress 连的就是它
targetPort: 3306 # 容器端口:最终转发给 MySQL 的 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-wordpress-server
spec:
replicas: 1 # 副本数量1
selector:
matchLabels: # Deployment 用 matchLabels
apps: wp # 关联Pod的标签(必须能匹配 template 里的标签)
template:
metadata:
labels:
apps: wp
spec:
# 指定分配至worker01(有mysql和wordpress,其他节点还没有上传镜像)
nodeName: worker01
containers:
- name: c1
image: docker.io/library/wordpress:7.1.0-php8.5
env:
- name: WORDPRESS_DB_HOST
value: svc-db:3306 # 指向 svc 名称(CoreDNS 解析)
- name: WORDPRESS_DB_USER
value: kpyun
- name: WORDPRESS_DB_PASSWORD
value: kpyun123
- name: WORDPRESS_DB_NAME
value: "wordpress"
---
apiVersion: v1
kind: Service
metadata:
name: svc-wp # NodePort 暴露给外部
spec:
type: NodePort # 集群外访问入口;nodePort(节点端口) 不写自动分配(本次实验分到 31752)
selector:
apps: wp
ports:
- port: 80 # svc 端口(ClusterIP:80 的内部入口依然保留)
targetPort: 80 # 容器端口:wordpress 的 Apache 监听 80
root@Master ~# kubectl apply -f deploy-wordpress.yaml
service/svc-db created
deployment.apps/deploy-wordpress-server created
service/svc-wp created
# ⬆️ 三个资源一次创建
2)查看资源
root@Worker02 ~# kubectl get svc,pods -o wide
NAME TYPE CLUSTER-IP PORT(S) AGE SELECTOR
service/svc-db ClusterIP 10.200.193.21 3306/TCP 5m5s apps=mysqld
service/svc-wp NodePort 10.200.253.11 80:31752/TCP 5m5s apps=wp
NAME READY STATUS RESTARTS AGE IP NODE GATES
pod/deploy-mysqld-server-77589f5fcf-pdbxg 1/1 Running 0 24m 10.100.1.75 worker01
pod/deploy-wordpress-server-64b8fc994c-c6hsz 1/1 Running 0 115s 10.100.1.76 worker01
3)验证svc资源
root@Worker02 ~# kubectl describe svc svc-db | egrep 'Type|IP:|Endpoint'
Type: ClusterIP
IP: 10.200.193.21
Endpoints: 10.100.1.75:3306
`svc-db 关联 MySQL(Endpoints)`
root@Worker02 ~# kubectl describe svc svc-wp | egrep 'Type|IP:|Endpoint'
Type: NodePort
IP: 10.200.253.11
Endpoints: 10.100.1.76:80
4)浏览器/NodePort 访问
root@Master ~# NPORT=$(kubectl get svc svc-wp -o jsonpath='{.spec.ports[0].nodePort}')
root@Master ~# curl -s -L http://10.0.0.13:$NPORT/ | grep -oE "<title>[^<]*</title>"
<title>WordPress &rsaquo; Installation</title> # 进入安装页

WordPress整套部署架构
WordPress整套部署架构

Important

整套架构(nodePort 由系统自动分配,本次实验分到 31752):

  • svc-wp(NodePort)→ 外部入口:集群外访问任意节点IP<31752>,负载均衡到 wordpress Pod
  • wordpress 凭 WORDPRESS_DB_HOST=svc-db:3306 先问 CoreDNS 拿到 svc-db 的 ClusterIP,再连库(图中①②③)
  • svc-db(ClusterIP)→ 负载均衡到 MySQL Pod

服务名连接 vs IP 连接:用 svc 名称连库,MySQL 换了 Pod 地址也无需改配置(Endpoints 自动跟上),这就是服务发现的威力

挑战:把 MySQL 放在 kpyun 名称空间,wordpress 放 default,实现跨名称空间连接

Terminal window
1)核心:跨 ns 的 svc 域名格式
# 同一个 ns 内:svc-db
# 跨 ns 访问:svc-db.<namespace>
# 完整 FQDN:svc-db.kpyun.svc.kpyun.com
Tip

烧脑思路:把 mysqld 的 deploy、svc-db 都加 namespace: kpyun,wordpress 的 WORDPRESS_DB_HOST 改成 svc-db.kpyun,其余不变,试试吧!(完整参考答案在下面)

动手前先想清楚两个易错点:

  • svc-db 不跟着搬家 ❌→ selector 只在 svc 自己所在的 ns 筛 Pod(09-ns-svc-pods.yaml 验证过的铁律),MySQL 搬走了,留在 default 的 svc-db 一个后端都选不中,Endpoints 直接为空
  • DB_HOST 还写短名 svc-db❌ → wordpress 在 default,短名会被解析到 default 自己的 svc-db(已删)→ 连库失败,页面报 Error establishing a database connection

烧脑参考答案:完整 YAML 如下,3 处改动全部用 ⭐ 标出,注释即讲解

Terminal window
2)清场 + 编写跨 ns 版资源清单
# 先删掉基础版(svc-db 相当于要"搬家":ns 只能在出生时指定,删了在目标 ns 重建)
root@Master ~# kubectl delete -f deploy-wordpress.yaml
service "svc-db" deleted
deployment.apps "deploy-wordpress-server" deleted
service "svc-wp" deleted
root@Master ~# kubectl delete -f deploy-mysql.yaml
deployment.apps "deploy-mysqld-server" deleted
root@Master ~# vim deploy-wordpress-crossns.yaml
# —— 文档1:Namespace —— 前面实验建过 kpyun;apply 幂等,已存在自动跳过
apiVersion: v1
kind: Namespace
metadata:
name: kpyun
---
# —— 文档2:MySQL Deployment —— ⭐改动1:加 namespace: kpyun
# ns 只能在"出生那一刻"指定,没有搬家,只能删了在目标 ns 重建
# 名称空间不隔离网络:default 的 wordpress 照样能连它的 ClusterIP
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-mysqld-server
namespace: kpyun # ⭐ 改动1:MySQL Pod 出生在 kpyun
spec:
replicas: 1 # 数据库单副本(实验环境;生产应上 StatefulSet + 持久化存储)
selector:
matchLabels:
apps: mysqld
template:
metadata:
labels:
apps: mysqld # 标签保持不变,svc-db 靠它在 kpyun 内认后端
spec:
nodeName: worker01 # mysql/wordpress 镜像只导入过 worker01
containers:
- name: c1
image: docker.io/library/mysql:8.0.36
env: # 和基础版完全一致,env 不受 ns 影响
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "yes"
- name: MYSQL_DATABASE
value: "wordpress"
- name: MYSQL_USER
value: "kpyun"
- name: MYSQL_PASSWORD
value: "kpyun123" # 生产环境应改用 Secret,实验从简
- name: "TZ"
value: "Asia/Shanghai"
args:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_bin
- --default-authentication-plugin=mysql_native_password
---
# —— 文档3:svc-db —— ⭐改动2:Service 必须跟着 MySQL 一起搬进 kpyun
# 铁律:svc 的 selector 只在【自己所在的 ns】筛 Pod
# MySQL Pod 在 kpyun,svc-db 留在 default → Endpoints 为空,连了也白连
apiVersion: v1
kind: Service
metadata:
name: svc-db
namespace: kpyun # ⭐ 改动2:和 MySQL Pod 同 ns,selector 才生效
spec:
type: ClusterIP # 只给集群内部连库用
selector:
apps: mysqld # 在 kpyun 内选中上面的 MySQL Pod
ports:
- port: 3306 # svc 端口:wordpress 连的就是它
targetPort: 3306 # 容器端口:MySQL 实际监听的端口
---
# —— 文档4:wordpress Deployment —— 不写 namespace,留守 default
# ⭐改动3 只有一行:DB_HOST 从 svc-db 改成跨 ns 简写 svc-db.kpyun
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-wordpress-server # 没写 namespace → 落在 default
spec:
replicas: 1
selector:
matchLabels:
apps: wp
template:
metadata:
labels:
apps: wp
spec:
nodeName: worker01
containers:
- name: c1
image: docker.io/library/wordpress:7.1.0-php8.5
env:
- name: WORDPRESS_DB_HOST
# ⭐ 改动3:跨 ns 简写(等价全称 svc-db.kpyun.svc.kpyun.com)
# wordpress Pod 在 default,resolver 拿着 search 列表自动拼接:
# 拼1: svc-db.kpyun.default.svc.kpyun.com ✗ NXDOMAIN
# 拼2: svc-db.kpyun.svc.kpyun.com ✓ 命中 → kpyun 里 svc-db 的 ClusterIP
value: svc-db.kpyun:3306
- name: WORDPRESS_DB_USER
value: kpyun # 账号密码不用动:kpyun 用户的 host='%',跨 ns 照样能连
- name: WORDPRESS_DB_PASSWORD
value: kpyun123
- name: WORDPRESS_DB_NAME
value: "wordpress"
---
# —— 文档5:svc-wp —— 留在 default,外部入口跟着 wordpress 走
# selector 同样只筛同 ns → 选中的是 default 的 wordpress Pod
apiVersion: v1
kind: Service
metadata:
name: svc-wp
namespace: default # default 本可省略,写出来说明"它故意留下"
spec:
type: NodePort
selector:
apps: wp
ports:
- port: 80
targetPort: 80
# nodePort 不写:自动分配(30000~32767)
root@Master ~# kubectl apply -f deploy-wordpress-crossns.yaml
namespace/kpyun unchanged
deployment.apps/deploy-mysqld-server created
service/svc-db created
deployment.apps/deploy-wordpress-server created
service/svc-wp created
# ⬆️ 一次 apply 5 个文档,各回各的 ns(模板没变,RS hash 沿用基础版)

跨 ns 版架构一图流(名称空间边界一目了然):

WordPress跨名称空间架构
WordPress跨名称空间架构

Important

烧脑复盘:跨 ns 连库三部曲

  • Pod 搬家:Deployment 加 namespace: kpyun(ns 只能出生时指定,“搬家”= 删了在目标 ns 重建)
  • svc 跟着搬:svc-db 同样加 namespace: kpyun —— selector 只在同 ns 筛 Pod,svc 不搬 Endpoints 就是空
  • 客户端改名字:WORDPRESS_DB_HOST=svc-db.kpyun,靠 Pod 的 search 域拼接完成跨 ns 解析

全程没动任何网络配置 —— 名称空间不隔离网络,改的只是”名字”,这就是 CoreDNS 服务发现的威力

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
久棹
不是先学好了再干,而是先干起来再学习,干中学!
分类
站点统计
文章
115
分类
15
标签
272
总字数
306,562
运行时长
0 天
最后活动
0 天前
文章目录