☸️ Kubernetes 学习总结与避坑指南
- 约 33 分钟读完 - 全文 13084 字☸️ Kubernetes 学习总结与避坑指南
参考书籍:龚正 等《Kubernetes 权威指南:从 Docker 到 Kubernetes 实践全接触》、Marko Lukša《Kubernetes in Action》 目标读者:会一点 Docker、刚接触 K8s,或者「YAML 会抄但一出问题就懵」的同学。
Kubernetes(简称 K8s,因为 K 和 s 之间有 8 个字母)是 Google 开源的容器编排系统,解决了「成百上千个容器跑在成百上千台机器上,谁来调度、谁来扩容、挂了谁来拉起」的问题。它不是一门「背完概念就会」的技术——新人最常见的状态是:每个名词都听过,但它们之间怎么串起来的说不清楚,Pod 一挂就不知道该看哪里。
这篇文章帮你建立主干知识框架:
- K8s 集群整体长什么样?Master 和 Node 各自干什么?
- Pod、Node、Service、Ingress、ConfigMap 这些核心对象到底是什么关系?
- Istio 服务网格在解决什么问题?
- 共享存储(PV/PVC/NAS)怎么用,有哪些坑?
- 排查 K8s 问题需要补哪些 Unix 网络编程基础知识?
- 生产环境如何做内外网访问隔离(私网端点 / 内网 LB / NetworkPolicy)?
- 在阿里云 ACK(以及 AWS EKS)上落地时要注意什么?
- Jenkins 如何与 K8s 打通,实现自动化部署、验证与失败回滚?
- 遇到 CrashLoopBackOff、OOMKilled、502、DNS 慢这些经典问题,该按什么思路排查?
一、K8s 整体架构:一个「中心化大脑 + 一群干活的节点」
K8s 集群在逻辑上分为两大部分:
- 控制平面(Control Plane,俗称 Master):集群的大脑,负责「决策」——调度、扩缩容、故障自愈。
- 工作节点(Node):真正跑容器的机器,负责「执行」。
graph TB
subgraph 控制平面["控制平面 Control Plane(阿里云 ACK 托管,用户不可见)"]
APIServer[kube-apiserver<br/>集群唯一入口,所有操作都经过它]
ETCD[(etcd<br/>分布式 KV 存储<br/>集群所有状态都在这里)]
Scheduler[kube-scheduler<br/>决定 Pod 调度到哪个 Node]
CM[kube-controller-manager<br/>各种控制器:副本数维持、节点监控等]
CCM[cloud-controller-manager<br/>对接云厂商:SLB/EBS/路由]
end
subgraph 节点1["Worker Node 1"]
K1[kubelet<br/>节点上的「包工头」]
P1[kube-proxy<br/>维护 Service 网络规则]
C1[容器运行时<br/>containerd]
Pod1[Pod A] ~~~ Pod2[Pod B]
end
subgraph 节点2["Worker Node 2"]
K2[kubelet]
P2[kube-proxy]
C2[containerd]
Pod3[Pod C]
end
APIServer --> ETCD
Scheduler --> APIServer
CM --> APIServer
CCM --> APIServer
K1 --> APIServer
K2 --> APIServer
User[kubectl / CI/CD] --> APIServer
记住这条主线:所有组件只跟 apiserver 说话,apiserver 把状态写进 etcd。你敲的每一条 kubectl 命令、每一个控制器的决策,都是「读/写 apiserver」这个动作。理解了这一点,后面所有概念都好串了。
K8s 的核心设计思想是声明式 API + 控制器循环:
graph LR
A[你提交 YAML<br/>期望状态:3 个副本] --> B[apiserver 存进 etcd]
B --> C{控制器不断对比<br/>期望状态 vs 实际状态}
C -->|实际只有 2 个| D[再创建 1 个 Pod]
D --> C
C -->|一致| E[什么都不做]
这就是「自愈」的本质:不是有人盯着修,而是控制器永远在循环纠偏。所以 K8s 里的对象是「期望状态」的描述,不是一条条命令。
二、Node:干活的机器
2.1 Node 上有什么
每个 Node 上必装三个组件:
- kubelet:节点代理。接收 apiserver 下发的 Pod 定义(PodSpec),调用容器运行时把容器拉起来,并持续上报节点和 Pod 状态。Pod 挂了重启、探针检测,都是 kubelet 干的。
- 容器运行时:真正拉镜像、跑容器的软件。Docker 已在 K8s 1.24 后被移除,现在主流是 containerd。
- kube-proxy:维护节点上的网络规则(iptables/IPVS),让 Service 的虚拟 IP 能转发到真实 Pod。
2.2 节点相关的避坑点
- 节点 NotReady:最常见的原因是 kubelet 挂了、节点磁盘满(DiskPressure)、内存不足(MemoryPressure)或网络不通。排查顺序:先
kubectl describe node <名字>看 Conditions,再登节点看systemctl status kubelet和日志journalctl -u kubelet。 - 磁盘压力:容器日志、镜像、emptyDir 都在吃节点磁盘。务必给应用日志配轮转(logrotate 或运行时自带的日志上限
containerLogMaxSize),否则磁盘写满会把节点上所有 Pod 驱逐(Evicted)。 - 不要随意登节点手工改东西:K8s 期望节点是「牛」不是「宠物」——可替换、不特殊。手工装的 Agent、改的 iptables 规则,节点一重建全没了。需要节点级能力请用 DaemonSet。
三、Pod:K8s 的最小调度单元
3.1 为什么不是直接调度容器
Pod 是一组共享网络和存储的容器集合。同一个 Pod 里的容器:
- 共享同一个 IP 和端口空间(通过
localhost直接互通) - 可以共享 Volume
graph TB
subgraph Pod["Pod(10.0.1.15)"]
direction LR
C1[主容器<br/>app:8080]
C2[边车容器<br/>日志采集]
V[(共享 Volume<br/>日志目录)]
C1 --> V
C2 --> V
end
C1 -.->|localhost:8080| C2
这个设计的经典用途就是 Sidecar(边车)模式:日志采集、流量代理(Istio 的 Envoy)都以边车容器形式注入 Pod,主容器无感知。
新人直觉「一个 Pod 跑一个容器」,这对 90% 的场景是对的。只有容器之间必须同生共死、共享 localhost 或共享文件时,才放进同一个 Pod。
3.2 Pod 生命周期与探针
stateDiagram-v2
[*] --> Pending: 创建,等调度/拉镜像
Pending --> Running: 调度成功,容器启动
Running --> Succeeded: 正常退出(Job 类)
Running --> Failed: 异常退出
Running --> Running: 探针失败→kubelet 重启容器
三个探针,新人人手必配:
| 探针 | 作用 | 配错的后果 |
|---|---|---|
livenessProbe |
判断容器是否活着,失败则重启容器 | 太灵敏 → 无限重启循环 |
readinessProbe |
判断是否能接流量,失败则从 Service 摘除 | 不配 → 启动期请求打到没 ready 的 Pod,报 502 |
startupProbe |
启动探针,成功前禁用另外两个 | 慢启动应用(如老 Java 服务)不配 → 被 liveness 误杀 |
3.3 资源 requests/limits 与 QoS:最容易被忽视的重灾区
resources:
requests: # 调度依据:保证分到这么多
cpu: "250m" # 0.25 核
memory: "512Mi"
limits: # 使用上限
cpu: "500m"
memory: "1Gi"- requests 是调度的唯一依据:节点剩余可分配资源 ≥ Pod 的 requests 总和,Pod 才调度得上去。不配 requests,调度就是瞎排,节点超载。
- CPU 超 limit → 被限流(throttle),容器变慢但不死;内存超 limit → OOMKilled,直接杀。这是两种完全不同的失败模式,排查时先分清。
- QoS 等级:requests=limits 为
Guaranteed(最后被驱逐);都不配为BestEffort(节点资源紧张时最先被杀)。生产核心服务至少配Burstable。
四、工作负载:不要直接创建裸 Pod
实际工作中你几乎不会手写 Pod,而是使用「管理 Pod 的控制器」:
| 对象 | 适用场景 | 一句话 |
|---|---|---|
| Deployment | 无状态服务(Web/API) | 副本数 + 滚动更新 + 回滚 |
| StatefulSet | 有状态服务(MySQL/Kafka) | 稳定的名字和网络标识、按序启停 |
| DaemonSet | 节点级 Agent(日志/监控) | 每个 Node 跑一个 |
| Job / CronJob | 一次性/定时任务 | 跑完即退 |
Deployment 滚动更新过程:
sequenceDiagram
participant U as 用户
participant D as Deployment
participant RS1 as ReplicaSet v1
participant RS2 as ReplicaSet v2
U->>D: kubectl set image 更新镜像
D->>RS2: 创建新 RS,先拉起 1 个新 Pod
RS2-->>D: 新 Pod ready
D->>RS1: 缩掉 1 个旧 Pod
Note over D,RS2: 循环:新 +1 → 旧 -1
D->>RS1: 缩到 0(旧 RS 保留,可回滚)
U->>D: kubectl rollout undo(随时回滚)
避坑点:
- maxSurge / maxUnavailable 控制滚动节奏。默认 25%/25%,意味着更新中可能瞬间少 25% 的可用副本——容量紧张的服务要调小 maxUnavailable。
- 回滚靠旧 ReplicaSet 保留的 Pod 模板,
revisionHistoryLimit默认 10,别设成 0 否则无法回滚。 - 镜像 tag 不要用
latest:无法回滚、无法审计,且imagePullPolicy行为反直觉。用 git commit 号或语义化版本。
五、K8s 网络模型与必备的 Unix 网络编程知识
K8s 的网络问题占了线上故障的一半以上。而很多「K8s 网络问题」本质上是 Unix 网络编程/TCP 协议问题,只是发生在容器环境里。这一节把两边打通。
5.1 K8s 网络的三条铁律
K8s 对网络模型只有三个基本要求(具体实现交给 CNI 插件,如 Flannel、Calico、阿里云 Terway):
- 每个 Pod 有自己的独立 IP
- 任意两个 Pod 之间可以直接通信(不经过 NAT)
- Node 和 Pod 之间可以直接通信
graph LR
subgraph 节点A["Node A"]
PA["Pod A<br/>10.244.1.5"]
end
subgraph 节点B["Node B"]
PB["Pod B<br/>10.244.2.8"]
end
PA <-->|"Pod 网络(CNI:Terway/Flannel/Calico)<br/>直连,无 NAT"| PB
5.2 Service:一组 Pod 的稳定入口
Pod 会漂移(重建后 IP 就变了),所以不能在代码里写死 Pod IP。Service 提供一个固定虚拟 IP(ClusterIP)+ DNS 名字,负载均衡到后端 Pod。
graph LR
Client[客户端 Pod] -->|"访问 my-svc:80<br/>DNS 解析为 10.96.0.10"| SVC["Service: my-svc<br/>ClusterIP 10.96.0.10"]
SVC -->|kube-proxy 的 iptables/IPVS 规则转发| P1[Pod 1]
SVC --> P2[Pod 2]
SVC --> P3[Pod 3]
三种 Service 类型:
- ClusterIP(默认):仅集群内可达
- NodePort:在每个节点开一个高端口(30000-32767),可从集群外访问
- LoadBalancer:对接云厂商负载均衡器(阿里云 CLB/SLB、AWS NLB/ALB),对外暴露的标准姿势
5.3 为什么排查 K8s 网络要懂 Unix 网络编程
下面这几组知识,是在 K8s 环境里排查连接问题的「元技能」:
(1)TCP 连接状态与 TIME_WAIT
TCP 挥手后主动关闭方会进入 TIME_WAIT 状态,默认保持 2MSL(Linux 上约 60s)。在高并发短连接场景(比如服务间大量 HTTP/1.0 式短调用),客户端端口会被 TIME_WAIT 大量占用,最终 Cannot assign requested address。
stateDiagram-v2
ESTABLISHED --> FIN_WAIT1: 主动方 close()
FIN_WAIT1 --> FIN_WAIT2: 收到对端 ACK
FIN_WAIT2 --> TIME_WAIT: 收到对端 FIN
TIME_WAIT --> CLOSED: 2MSL 超时(约60s)
ESTABLISHED --> CLOSE_WAIT: 对端先 close()
CLOSE_WAIT --> LAST_ACK: 我方 close()
LAST_ACK --> CLOSED
K8s 里的对应症状与解法:
- 症状:服务间调用偶发超时,
ss -s看到 timewait 几万。 - 正解:应用层用长连接 + 连接池(HTTP keep-alive、gRPC),别依赖
net.ipv4.tcp_tw_reuse这类内核参数硬扛。 - 另一个常见坑是 CLOSE_WAIT 堆积:对端关了连接但你的代码没 close()(连接泄漏),
ss -ant | grep CLOSE_WAIT一查便知。
(2)socket、backlog 与「连接被 Reset」
服务端 listen(fd, backlog) 的 backlog 是「已完成三次握手但应用还没 accept」的队列长度。K8s 场景下突发流量打到刚重启的 Pod,如果应用 backlog 太小(或迟迟不 accept),内核会直接 RST,客户端看到的是 Connection reset by peer。这类问题在 Pod 扩容瞬间、探针刚通过时高发——readinessProbe 通过 ≠ 应用真的能扛流量。
(3)epoll 与 I/O 多路复用
一个进程通过 epoll 可以管理数万连接(C10K 问题的解法)。Nginx Ingress、Envoy 都是 epoll 模型。要懂的点:
- 进程能开的连接数受 文件描述符上限限制:
ulimit -n。容器里同样生效,高并发服务记得确认 limit(cat /proc/<pid>/limits)。 - 每个 TCP 连接 = 一个四元组(源IP:源端口 → 目标IP:目标端口)。客户端对单一目标的并发连接数理论上受源端口数(约 2.8 万个可用)限制——这又回到「必须用连接池复用连接」。
(4)conntrack:K8s 特有的网络坑
kube-proxy 的 iptables 模式依赖内核 conntrack(连接跟踪表)。节点上连接数巨大时 conntrack 表被打满(默认 nf_conntrack_max 往往不够),表现为新建连接随机丢包、无日志,极难排查。解法:调大 net.netfilter.nf_conntrack_max,或干脆把 kube-proxy 切到 IPVS 模式。阿里云 ACK 的 Terway 模式在这方面压力小很多。
(5)DNS:K8s 里的「隐形依赖」
集群内所有服务名解析都靠 CoreDNS。两个经典坑:
- glibc 的 DNS 解析是串行的,且默认
ndots:5导致集群内短域名先查 5 次后缀。症状:应用偶发解析慢 5s(一次超时重传)。解法:FQDN 写法my-svc.my-ns.svc.cluster.local.(结尾加点)或调整 Pod 的dnsConfig。 - Java 应用默认永久缓存 DNS 结果(
networkaddress.cache.ttl=-1),Service 后端 IP 变了 JVM 不知道。容器里务必显式设置 DNS 缓存 TTL(如 30s)。
5.4 内外网访问隔离:哪些服务能暴露,哪些必须藏住
生产集群的基本安全原则:默认所有东西都不出集群,只开必要的口子。K8s 默认网络是「全通」的(任何 Pod 可以访问任何 Pod,LoadBalancer 一不小心就开到公网),隔离要靠自己显式配置。隔离分三个层面,从外到内:
graph TB
Internet[公网用户] -->|只放行 443| WAF[WAF / 安全组<br/>云平台层]
WAF --> PubLB[公网 ALB/CLB<br/>仅 Web 入口服务]
PubLB --> PubIng[Ingress Controller<br/>公网命名空间]
PubIng --> WebSvc[Web Service<br/>ClusterIP]
Office[办公网/运维] -->|VPN/跳板机| IntLB[内网 LoadBalancer<br/>intranet 类型]
IntLB --> AdminSvc[管理后台 / 监控<br/>Grafana / Jenkins]
subgraph 集群内["集群内部(东西向流量)"]
WebSvc -->|NetworkPolicy 允许| AppSvc[业务 Service]
AppSvc -->|NetworkPolicy 允许| DBSvc[数据库 Proxy Service]
AppSvc -.->|NetworkPolicy 拒绝| Other[其他 namespace]
end
第一层:云网络边界(VPC / 安全组)
- API Server 用私网端点:阿里云 ACK 创建集群时选择「专有网络(私网)」API Server 访问,或只绑定内网 EIP,运维通过 VPN/跳板机访问;公网端点即便有证书认证也是攻击面。AWS EKS 对应 endpoint private access。
- 节点安全组收紧入方向:只放行 NodePort 范围给内网、放行 LB 健康检查来源,其余一律拒绝。ACK 的节点安全组默认规则要审一遍。
- 数据库、Redis 这类云产品走 VPC 内网地址,安全组只对集群节点网段开放。
第二层:Service 暴露方式——内外网分流的关键
LoadBalancer 类型 Service 默认创建的是公网 LB,这是最常见的「误暴露」事故。面向内部的服务要用注解显式声明内网 LB:
# 阿里云:内网 CLB(VPC 内可达,公网不可达)
apiVersion: v1
kind: Service
metadata:
name: internal-admin
annotations:
service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type: "intranet"
spec:
type: LoadBalancer
ports:
- port: 80AWS EKS 对应注解:service.beta.kubernetes.io/aws-load-balancer-internal: "true"(NLB)或 aws-load-balancer-scheme: internal(ALB Controller)。
经验法则:
- 对外服务:公网 ALB/CLB + Ingress,前面最好再挂 WAF。
- 对内服务(管理后台、监控、Jenkins):内网 LB 或纯 ClusterIP,配合 VPN/跳板机访问。
- 默认用 ClusterIP,需要出集群时再显式升级类型——把「暴露」当成一个需要评审的动作。
- Ingress 上还可以用注解做来源 IP 白名单(如
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8),给内部接口再加一道闸。
第三层:NetworkPolicy——集群内部的东西向隔离
即使流量不出集群,「测试 namespace 的 Pod 误连生产数据库」这类事故也常见。NetworkPolicy 是 Pod 级的防火墙(需要 CNI 支持,ACK 的 Terway、Calico 都支持,Flannel 不支持):
# 第一步:namespace 默认全拒(白名单模式的起点)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: prod
spec:
podSelector: {} # 选中 namespace 内所有 Pod
policyTypes: ["Ingress", "Egress"]
---
# 第二步:只放行需要的流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-app
namespace: prod
spec:
podSelector:
matchLabels: {app: order-svc}
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels: {app: web-frontend}
ports:
- port: 8080避坑点:
- NetworkPolicy 是白名单累加模型:任何一条 policy 选中某 Pod,该 Pod 未被放行的方向就被默认拒绝。写漏了 DNS(Egress 到 kube-dns 的 53 端口)会导致全 namespace 解析失败——默认拒绝后记得显式放行 DNS。
- 策略只对选中后新建立的连接生效,且 Flannel 下写了也不生效(静默失效,最坑的一种)。
- 云安全组管的是「节点/LB 维度」,NetworkPolicy 管的是「Pod 维度」,两者互补不替代。
- 上了 Istio 的话,还可以用 AuthorizationPolicy 做带身份(mTLS 服务身份)的七层鉴权,比纯 IP/端口的 NetworkPolicy 更细。
六、Ingress:HTTP 流量的统一入口
Service 解决的是四层(TCP)负载均衡;Ingress 解决的是七层(HTTP/HTTPS)路由:按域名、按路径把外部流量分发到不同 Service。
graph TB
User[外部用户] -->|HTTPS| LB[云负载均衡<br/>阿里云 ALB/CLB]
LB --> IC[Ingress Controller<br/>Nginx / ALB Ingress]
IC -->|"a.example.com/api/*"| S1[Service: api]
IC -->|"a.example.com/web/*"| S2[Service: web]
IC -->|"b.example.com/*"| S3[Service: admin]
S1 --> P1[api Pod]
S2 --> P2[web Pod]
S3 --> P3[admin Pod]
注意分清两层:
- Ingress 资源(YAML 对象):只是路由规则的声明,本身不做任何事。
- Ingress Controller(真正干活的组件):如 ingress-nginx、阿里云 ALB Ingress Controller。它 watch Ingress 资源,把规则翻译成 Nginx 配置并热加载。
避坑点:
pathType: Prefix和Exact的区别,/api配 Prefix 会把/apiserver也匹配进去。- 后端重写(
rewrite-target)是最容易写错的注解,先想清楚「客户端路径 → 后端真实路径」的映射。 - HTTPS 证书放在 Secret 里,过期忘续是经典事故——建议上 cert-manager 自动签发续期。
- 502 的第一排查动作:看 Ingress Controller 日志里 upstream 是「连接拒绝」(Pod 没 ready)还是「超时」(应用卡死)。
七、ConfigMap 与 Secret:配置和代码分离
十二要素应用的第一条就是「配置存环境」。K8s 里对应两个对象:
- ConfigMap:普通配置(应用参数、配置文件、特性开关)
- Secret:敏感配置(密码、密钥、证书),etcd 中 Base64 编码(注意:Base64 不是加密,生产环境要开 etcd 静态加密或对接 KMS)
两种注入方式:
# 方式一:环境变量
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db_host
# 方式二:挂载为文件(推荐,支持热更新)
volumeMounts:
- name: config
mountPath: /etc/app
volumes:
- name: config
configMap:
name: app-config两个必踩的坑,提前告诉你:
- 环境变量注入的配置,改了 ConfigMap 不会生效,必须重启 Pod。文件挂载方式会自动更新(kubelet 定期同步,有分钟级延迟),但应用要支持 reload。
subPath挂载的 ConfigMap 不会自动更新。这是用了 subPath 挂单个配置文件的同学必踩的坑——改完 ConfigMap 发现容器里还是旧的。解法:挂载整个目录,或接受重启 Pod 的事实。- ConfigMap 有 1MB 大小限制,别把大文件塞进去。
八、共享存储:PV / PVC / StorageClass
容器是临时的,数据要持久化就必须落到外部存储。K8s 的存储抽象分三层:
graph TB
subgraph 管理员/云平台侧
SC[StorageClass<br/>定义「用什么盘」<br/>如 alicloud-disk-essd / nas]
PV[PersistentVolume<br/>一块真实的存储<br/>云盘/NAS 文件系统]
end
subgraph 应用侧
PVC[PersistentVolumeClaim<br/>「我要 100Gi 存储」的申请单]
Pod[Pod 挂载 PVC]
end
PVC -->|绑定| PV
SC -->|动态供给:PVC 一创建<br/>自动开出对应 PV| PV
Pod --> PVC
现代用法几乎全是动态供给:你只需要写 PVC 声明要多少空间,StorageClass 自动调用云厂商 API 创建云盘/NAS,不用手工建 PV。
三种访问模式(Access Modes)是高频考点:
| 模式 | 含义 | 典型存储 |
|---|---|---|
| ReadWriteOnce (RWO) | 只能挂到一个节点 | 云盘(阿里云 ESSD、AWS EBS) |
| ReadWriteMany (RWX) | 多节点共享读写 | 文件存储(阿里云 NAS、AWS EFS) |
| ReadOnlyMany (ROX) | 多节点只读 | 对象存储挂载、只读数据卷 |
共享存储的避坑清单:
- 云盘(RWO)不能跨可用区挂载。PV 创建在可用区 A,Pod 被调度到可用区 B 就永远 Pending。解法:用
WaitForFirstConsumer模式的 StorageClass(等 Pod 定好节点再开盘),并用拓扑约束调度。 - 要多副本共享文件(如多个 Web Pod 共享上传目录),必须用 NAS/EFS 这类 RWX 存储,云盘做不到。
- NAS 挂载注意权限问题:容器内进程 UID 和 NAS 目录属主不匹配会 Permission denied,用
securityContext.fsGroup或挂载参数解决。 - 删除 PVC 会不会丢数据,取决于 PV 的
persistentVolumeReclaimPolicy(Retain 保留 / Delete 连盘一起删)。生产数据盘务必确认是 Retain。 - StatefulSet 用
volumeClaimTemplates给每个副本独立开盘,扩缩容时数据卷跟着 Pod 名字走,这是有状态服务的标准姿势。
九、Istio 服务网格:把「网络能力」从业务代码里剥出来
微服务多了之后,每个服务都要自己实现超时重试、熔断、灰度、流量监控——这些和业务无关。Istio 的思路:给每个 Pod 注入一个 Envoy 边车代理,接管所有进出流量,上述能力全部在代理层实现,业务代码零侵入。
graph TB
subgraph 控制平面["Istio 控制平面(istiod)"]
Pilot[配置下发<br/>流量规则]
Citadel[证书管理<br/>mTLS]
end
subgraph 数据平面
subgraph PodA["Pod A"]
App1[业务容器] --- E1[Envoy 边车]
end
subgraph PodB["Pod B"]
App2[业务容器] --- E2[Envoy 边车]
end
end
Pilot -.->|xDS 配置下发| E1
Pilot -.->|xDS 配置下发| E2
E1 <-->|"mTLS 加密流量<br/>(应用无感知)"| E2
核心能力速览:
- 流量管理:VirtualService + DestinationRule 实现按权重灰度(v1 90% / v2 10%)、按 Header 路由、超时重试、熔断
- 安全:服务间自动 mTLS 加密 + 身份认证(AuthorizationPolicy)
- 可观测:不用改代码就拿到全链路指标、调用拓扑、访问日志
- 入口:Istio Gateway 替代/补充原生 Ingress,作为网格的南北向入口
避坑点(Istio 坑不少,挑最重要的):
- 边车有成本:每个 Pod 多一份 Envoy 的 CPU/内存(默认 requests 约 100m/128Mi),上千 Pod 规模要算这笔账,必要时调小。
- Sidecar 启动顺序:业务容器可能比 Envoy 先起来导致启动瞬间网络不通(现在
holdApplicationUntilProxyStarts可解);Job 类任务跑完 Envoy 不退导致 Job 永不结束,需要显式退出或关掉注入。 - 协议识别:Service 端口名要带协议前缀(
name: http-xxx),否则 Istio 按 TCP 处理,七层能力全部失效。这是网格内路由不生效的第一嫌疑。 - 排障三板斧:
istioctl analyze(静态检查配置)、istioctl proxy-config(看 Envoy 实际生效配置)、Envoy 的:15000/stats和:15000/clusters。 - 新人别一上来就上全量 Istio。先在非核心 namespace 开注入练手,理解 VirtualService 再说。
十、云上落地:阿里云 ACK 为主,AWS EKS 对照
自己从零搭一套生产级 K8s(etcd 高可用、证书、网络插件)成本极高,生产环境几乎都用云厂商托管服务。
10.1 阿里云 ACK 关键点
graph TB
subgraph 阿里云托管["ACK 托管层(免费/Pro)"]
MA[控制平面:apiserver/etcd/scheduler<br/>阿里云托管,SLA 保障]
end
subgraph 你的账号["你的 VPC 内"]
N1[ECS 节点池 1<br/>按量/抢占式]
N2[ECS 节点池 2<br/>弹性伸缩 ESS]
end
MA --> N1
MA --> N2
N1 -.对接.-> CLB[CLB/ALB<br/>负载均衡]
N1 -.对接.-> NAS[云盘 ESSD / NAS<br/>持久存储]
N1 -.对接.-> SLS[SLS 日志 / ARMS 监控<br/>可观测]
实践经验:
- 网络插件选 Terway:Pod 直接用 VPC 弹性网卡 IP,无 overlay 开销、conntrack 压力小,还能配 NetworkPolicy;Flannel 适合小规模简单场景。Terway 模式下规划 VPC 网段要留够,每个 Pod 占一个真实 IP。
- 节点池 + 弹性伸缩:按业务波峰波谷自动扩缩节点;抢占式实例(Spot)跑无状态任务能省 70% 成本,但要容忍随时被回收。
- Ingress 用 ALB Ingress Controller:比自建 Nginx Ingress 省心,ALB 本身按量弹性,且和 ACK 深度集成。
- 日志和监控开箱即用:日志服务 SLS(采集容器 stdout 和文件日志)、ARMS Prometheus 监控,别再自己搭 ELK 了。
- 镜像仓库 ACR:用 ACR EE(企业版)+ ACK 免密拉取插件,避免在 YAML 里配 imagePullSecret。
- 存储选 CNFS/云盘 ESSD(有状态单副本)+ NAS(共享文件)+ OSS(静态资源、备份) 的组合。
10.2 AWS EKS 速查对照
| 需求 | 阿里云 ACK | AWS EKS |
|---|---|---|
| 网络插件 | Terway(VPC 网卡直连) | aws-vpc-cni(同理) |
| 七层入口 | ALB Ingress | AWS Load Balancer Controller (ALB/NLB) |
| 块存储 | 云盘 ESSD | EBS (gp3) |
| 共享文件存储 | NAS | EFS |
| 日志 | SLS | CloudWatch |
| 监控 | ARMS Prometheus | Managed Prometheus / Grafana |
| 镜像仓库 | ACR | ECR |
| 无服务器节点 | 虚拟节点(ECI) | Fargate |
概念完全互通,换云只是换 Controller 和注解。
十一、典型问题排查手册
11.1 通用排查流程
graph TD
A[发现问题] --> B{Pod 状态?<br/>kubectl get pods}
B -->|Pending| C[kubectl describe pod<br/>看 Events]
C --> C1{原因}
C1 -->|资源不足| C2[加节点/降 requests<br/>或开弹性伸缩]
C1 -->|存储挂载失败| C3[检查 PVC 绑定/可用区]
C1 -->|镜像拉取失败| C4[ImagePullBackOff:查镜像名/凭证/网络]
B -->|CrashLoopBackOff| D[kubectl logs --previous<br/>看上一个容器的临终日志]
D --> D1{原因}
D1 -->|应用自身报错| D2[修代码/配置]
D1 -->|Exit 137| D3[OOMKilled:调大 memory limit<br/>或查内存泄漏]
D1 -->|Exit 143| D4[被优雅终止:查探针/滚动更新]
B -->|Running 但访问异常| E[分层验证]
E --> E1[1. 进 Pod 内 curl localhost 通吗?]
E1 --> E2[2. curl Pod IP 通吗?]
E2 --> E3[3. curl Service ClusterIP 通吗?<br/>查 Endpoints 是否有后端]
E3 --> E4[4. Ingress 域名通吗?<br/>查 Controller 日志]
11.2 高频问题速查表
| 症状 | 最常见原因 | 第一动作 |
|---|---|---|
| ImagePullBackOff | 镜像名/tag 错、私有仓库无凭证、节点无公网 | describe pod 看 Events 报错原文 |
| CrashLoopBackOff | 配置错、依赖没起来、探针误杀 | kubectl logs --previous |
| OOMKilled (137) | 内存 limit 太小或泄漏 | 看监控曲线,调 limit;Java 服务注意堆外内存 |
| Pod 一直 Pending | 资源不够、PVC 未绑定、污点/亲和性 | describe pod 看调度失败原因 |
| 服务间偶发超时 | conntrack 满、DNS 慢、TIME_WAIT 耗尽 | ss -s、CoreDNS 日志、查连接池配置 |
| 滚动更新后 502 | 没配 readinessProbe / 没处理 SIGTERM 优雅退出 | 配探针 + preStop + 应用优雅关闭 |
| ConfigMap 改了不生效 | 环境变量注入 / subPath 挂载 | 重启 Pod 或改为目录挂载 |
| 跨可用区挂载失败 | 云盘和节点不在同一 AZ | WaitForFirstConsumer + 拓扑约束 |
11.3 优雅退出:滚动更新不断流的完整姿势
Pod 被删除时的标准流程(很多人只知道一半):
sequenceDiagram
participant K as kubelet
participant P as Pod
participant EP as Endpoints
K->>EP: 把 Pod 从 Service 摘除(不再接新流量)
K->>P: 执行 preStop 钩子(如 sleep 5,等摘流量生效)
K->>P: 发送 SIGTERM
P->>P: 应用应:停止接新请求,处理完存量请求后退出
Note over K,P: 超过 terminationGracePeriodSeconds(默认30s)
K->>P: SIGKILL 强杀
三个缺一不可:readinessProbe 保证新 Pod ready 才切流量;preStop/优雅关闭保证老 Pod 处理完存量请求;terminationGracePeriodSeconds 给够时间。Java/Go 服务请确认框架注册了 SIGTERM 处理器(Spring Boot 的 server.shutdown=graceful、Go 的 signal.Notify)。
11.4 kubectl 排障速查
kubectl get pods -o wide -n <ns> # 看状态和所在节点
kubectl describe pod <pod> -n <ns> # 看 Events(排障第一步)
kubectl logs <pod> -c <container> --previous # 崩溃前的日志
kubectl get endpoints <svc> -n <ns> # Service 后面到底有没有 Pod
kubectl exec -it <pod> -- sh # 进容器内验证网络
kubectl run debug --rm -it --image=busybox:1.36 -- sh # 临时网络调试 Pod
kubectl top pod / kubectl top node # 资源占用
kubectl get events --sort-by=.lastTimestamp -n <ns> # 按时间看事件十二、CI/CD:Jenkins 与 K8s 打通,实现自动化部署与验证
手工 kubectl apply 只适合学习。生产环境的标准做法是把「构建 → 推镜像 → 部署 → 验证 → 失败回滚」串成流水线,代码合并后自动完成发布。
12.1 整体流水线长什么样
graph LR
Dev[开发者 git push] --> Hook[Webhook 触发 Jenkins]
Hook --> Build[阶段1: 构建<br/>单测 + Docker 镜像]
Build --> Push[阶段2: 推送镜像<br/>到 ACR/ECR<br/>tag = git commit]
Push --> Deploy[阶段3: 部署<br/>kubectl set image<br/>更新 K8s]
Deploy --> Verify[阶段4: 验证<br/>rollout status + 冒烟测试]
Verify -->|成功| Done[发布完成<br/>钉钉/邮件通知]
Verify -->|失败| RB[自动回滚<br/>rollout undo] --> Alarm[告警通知]
关键认知:K8s 侧不需要装任何 Jenkins 插件或 Agent 也能被部署——Jenkins 只要有目标集群的 kubeconfig(访问凭证),用 kubectl 就能驱动 apiserver 完成一切。打通的本质就两件事:凭证怎么管、流水线脚本怎么写。
12.2 Jenkins 的两种部署形态
| 形态 | 说明 | 适用 |
|---|---|---|
| Jenkins 在集群外(独立服务器/另一集群) | 通过 kubeconfig 远程操作业务集群 | 一套 Jenkins 管多个集群,最常见 |
| Jenkins 跑在 K8s 集群内 | 配合 Kubernetes 插件,每次构建动态拉起一个 Pod 作为构建 agent,用完即销毁 | 构建任务多、想弹性利用集群资源 |
graph TB
subgraph Jenkins侧["Jenkins(集群外或集群内)"]
J[Jenkins Master]
C1[凭据1: kubeconfig<br/>Secret file 类型]
C2[凭据2: ACR 账号<br/>Username/Password 类型]
J --> C1
J --> C2
end
subgraph 阿里云["阿里云"]
J -->|docker push| ACR[ACR 镜像仓库]
J -->|"kubectl(走内网 API Server 端点)"| API[ACK apiserver]
API --> Pod[滚动更新业务 Pod]
Pod -->|免密拉取| ACR
end
推荐做法:Jenkins 与 ACK 在同一 VPC 内,kubectl 直接走 API Server 的内网端点(安全且免公网流量);镜像仓库用 ACR 企业版 + ACK 免密拉取,业务侧零凭证。
12.3 最小权限的部署凭证:别把 admin kubeconfig 给 Jenkins
这是 CI/CD 安全的第一课。给 Jenkins 一个只能操作特定 namespace、只能改 Deployment 的 ServiceAccount:
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins-deployer
namespace: prod
---
# Role 只在 prod namespace 内生效
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: prod
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins-deployer
namespace: prod
subjects:
- kind: ServiceAccount
name: jenkins-deployer
namespace: prod
roleRef:
kind: Role
name: deployer
apiGroup: rbac.authorization.k8s.io然后为这个 SA 生成 token,做成只含该 token 的 kubeconfig,存进 Jenkins 的 Secret file 凭据。好处:凭证泄露也只影响一个 namespace 的 Deployment,且可以随时 kubectl delete rolebinding 吊销。
12.4 完整 Jenkinsfile 示例
pipeline {
agent any
environment {
IMAGE = "registry.cn-hangzhou.aliyuncs.com/mycompany/order-svc"
TAG = "${env.GIT_COMMIT.take(8)}" // 用 git commit 做 tag,可回滚可追溯
KUBECONFIG = credentials('ack-prod-kubeconfig') // Secret file 凭据
}
stages {
stage('构建') {
steps {
sh 'mvn -q test' // 先跑单测,挂了直接终止
sh "docker build -t ${IMAGE}:${TAG} ."
}
}
stage('推送镜像') {
steps {
withCredentials([usernamePassword(
credentialsId: 'acr-account',
usernameVariable: 'U', passwordVariable: 'P')]) {
sh 'echo $P | docker login registry.cn-hangzhou.aliyuncs.com -u $U --password-stdin'
sh "docker push ${IMAGE}:${TAG}"
}
}
}
stage('部署到 ACK') {
steps {
sh "kubectl --kubeconfig=$KUBECONFIG -n prod \
set image deployment/order-svc order-svc=${IMAGE}:${TAG}"
}
}
stage('验证') {
steps {
// 阻塞等待滚动更新完成;超时/失败会非零退出
sh "kubectl --kubeconfig=$KUBECONFIG -n prod \
rollout status deployment/order-svc --timeout=300s"
// 冒烟测试:走内网 SLB 打健康检查和关键接口
sh "curl -fsS --retry 5 --retry-delay 3 \
http://order-svc-internal.prod.svc.cluster.local/healthz || exit 1"
}
}
}
post {
failure {
// 部署验证失败自动回滚到上一个版本
sh "kubectl --kubeconfig=$KUBECONFIG -n prod \
rollout undo deployment/order-svc || true"
}
}
}12.5 「验证」环节的正确姿势
很多流水线只做到 kubectl apply 就算「发布成功」,这是自欺欺人。apply 返回成功只代表 apiserver 收下了 YAML,Pod 起不起得来是另一回事。可靠的验证链:
- rollout status 阻塞等待:确认所有新副本 ready(背后靠的就是 readinessProbe——再次体现探针的重要性)。
- 冒烟测试:对真实入口打
/healthz和一两个核心业务接口,确认「能起来」且「能干活」。 - 失败自动回滚:
rollout undo依赖旧 ReplicaSet 还在(revisionHistoryLimit别设 0)且镜像 tag 可追溯——这就是前面说「不要用 latest」的根本原因。 - 更严谨的做法是灰度验证:先用 Istio 把 5% 流量切到新版本观察指标,确认无误再全量(见第九章)。
12.6 CI/CD 避坑清单
- 镜像 tag 必须唯一可追溯:git commit 短哈希或构建号,杜绝
latest,否则回滚时根本不知道该回滚到哪个镜像。 - 凭据最小化 + 分环境:dev/staging/prod 用不同的 SA 和 Jenkins 凭据,prod 的部署动作加人工审批(Jenkins 的
input步骤)。 - 声明式优先:服务变多后,把
kubectl set image换成 Helm/Kustomize(YAML 也进 Git 仓库,可审计可 diff);再进一步上 GitOps(ArgoCD/Flux)——Jenkins 只负责构建推镜像,部署由集群内的 ArgoCD 从 Git 仓库自动同步,Jenkins 不再需要生产集群的任何凭证,安全性大幅提升。 - Jenkins 到集群的网络:kubectl 走内网 API Server 端点;如果 Jenkins 在另一个 VPC,用云企业网/对等连接打通,不要把 API Server 暴露公网给 Jenkins 用。
- 构建产物(镜像)与部署配置分离管理,避免「同一个 tag 被覆盖推送」导致回滚失效——ACR 里给 prod 仓库开镜像 tag 不可变(immutable)开关。
十三、学习路线建议
- 第一周:本地装 minikube 或 kind,把 Deployment/Service/ConfigMap 各玩一遍,亲手制造一次 CrashLoopBackOff 并修好它。
- 第二周:搞懂网络——Pod 网络、Service 转发(iptables)、Ingress、NetworkPolicy 内外网隔离,配合
tcpdump/ss在容器里抓包看真实流量。 - 第三周:上云。开一套阿里云 ACK(按量付费,玩完就删),跑通「ACR 推镜像 → ACK 部署 → ALB Ingress 暴露 → NAS 挂共享盘 → SLS 看日志」全流程。
- 第四周:打通 CI/CD。用 Jenkins + 最小权限 SA 实现「推代码 → 自动构建 → 自动部署 → rollout 验证 → 失败回滚」完整流水线。
- 进阶:Helm 包管理、HPA 自动扩缩容、Istio 灰度发布、GitOps(ArgoCD)。
最后送一句《Kubernetes 权威指南》反复强调的心法:K8s 的一切都是声明式的——你描述「我要什么」,控制器负责「做到」。排查问题时永远先问:期望状态(YAML/Spec)是什么?实际状态(Status/Events)是什么?两者的差距就是答案所在。
参考资料:
- 《Kubernetes 权威指南:从 Docker 到 Kubernetes 实践全接触(第 4 版)》龚正 等
- 《Kubernetes in Action》Marko Lukša
- 《Unix 网络编程 卷1》W. Richard Stevens(网络排障内功)
- Kubernetes 官方文档
- 阿里云 ACK 文档