Kubernetes运维折腾手记
今天就从实际踩坑经验出发,聊聊 Kubernetes 运维的那些事儿。刚开始接触 Kubernetes 的时候,我也犯过"能跑就行"的错误。
部署:能跑就行吗?
刚开始接触 Kubernetes 的时候,我也犯过"能跑就行"的错误。
记得第一次在阿里云上部署 Kubernetes 集群,用了 kubeadm 搭了个三节点的集群。看着 kubectl get nodes 返回的 Ready 状态,我满心欢喜地把服务迁了上去。
结果第二天就出事了。
# 当时的节点状态
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
master-node Ready master 2d v1.24.0
worker-node-1 Ready <none> 2d v1.24.0
worker-node-2 Ready <none> 2d v1.24.0
# 但仔细看,有问题
$ kubectl describe node worker-node-1 | grep -A 5 "Allocated resources"
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 2900m (72%) 4000m (100%)
memory 6Gi (75%) 8Gi (100%)
ephemeral-storage 0 (0%) 0 (0%)
hugepages-2Mi 0 (0%) 0 (0%)
资源请求看起来还好,但 limits 已经打满了。我随手跑了个容器上去,结果 pod 一直处在 Pending 状态:
$ kubectl get pods my-app-xxx
NAME READY STATUS RESTARTS AGE
my-app-xxx 0/1 Pending 0 5m
$ kubectl describe pod my-app-xxx | tail -20
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling <unknown> default-scheduler 0/3 nodes are available: 3 Insufficient cpu.
教训:部署前先做容量规划。
我后来养成了个习惯,部署前先算一笔账:
# resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-resources
namespace: production
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "6"
limits.memory: 12Gi
这样至少能避免把节点撑爆的情况。
镜像:拉不到的痛
镜像问题绝对是我踩过最多的坑。
最经典的一次是这样的:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: my-registry.io/my-project/my-app:v1.0.0
imagePullPolicy: Always
上线后,pod 一直在 ImagePullBackOff:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-7f8b9c5d6-xk4pw 0/1 ImagePullBackOff 0 2m
my-app-7f8b9c5d6-ypm9l 0/1 ImagePullBackOff 0 2m
my-app-7f8b9c5d6-zqw7m 0/1 ImagePullBackOff 0 2m
查了一下事件:
$ kubectl describe pod my-app-7f8b9c5d6-xk4pw | grep -A 3 Events
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 57s kubelet Pulling image "my-registry.io/my-project/my-app:v1.0.0"
Warning Failed 57s kubelet Failed to pull image "my-registry.io/my-project/my-app:v1.0.0": rpc error: code = Unknown desc = Error response from daemon: pull access denied for my-registry.io/my-project/my-app, repository does not exist or may require 'docker login'
问题出在哪?镜像地址写错了,应该是 my-registry.com 而不是 my-registry.io。
这只是个小失误,但更麻烦的是私有仓库的认证问题。我后来是这样解决的:
# image-pull-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
namespace: production
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJteS1yZWdpc3RyeS5jb20iOnsidXNlcm5hbWUiOiJ1c2VyIiwicGFzc3dvcmQiOiJwYXNzd29yZCIsImF1dGgiOiJkGh0cHA6Ly91c2VyOnBhc3N3b3JkQG15LXJlZ2lzdHJ5LmNvbSJ9fX0=
然后在 deployment 里引用:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
imagePullSecrets:
- name: registry-credentials
containers:
- name: app
image: my-registry.com/my-project/my-app:v1.0.0
经验:imagePullPolicy 不要总设成 Always。
我发现很多人喜欢把 imagePullPolicy 设成 Always,觉得这样能确保用最新镜像。但这有个问题——每次都要拉镜像,网络不好的时候会很慢,而且如果你的 CI/CD 流程里镜像版本号都变了,这个设置就没什么意义了。
现在我的做法是:
- 开发环境:
imagePullPolicy: Always - 测试环境:
imagePullPolicy: IfNotPresent - 生产环境:
imagePullPolicy: IfNotPresent,而且镜像版本号要明确
存储持久化的坑
有状态应用在 Kubernetes 上跑起来容易,但要持久化数据就麻烦了。
我遇到过一次,部署了个 PostgreSQL,数据没持久化,结果 pod 重启后数据全丢了。
# 错误的配置
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
template:
spec:
containers:
- name: postgres
image: postgres:14
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
这个配置看起来是对的,但问题出在哪里?
StorageClass 没指定!默认用的是缺省的 StorageClass,而那个类用的是 local 存储而不是持久化存储。
# 修正后的配置
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
template:
spec:
containers:
- name: postgres
image: postgres:14
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: csi-ssd # 指定具体的 StorageClass
resources:
requests:
storage: 10Gi
最佳实践:部署前先检查 StorageClass。
# 查看可用的 StorageClass
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 30d
csi-ssd csi.qingcloud.com Delete Immediate true 30d
# 查看默认的 StorageClass
$ kubectl get storageclass -o jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}'
local-path
故障排查:从现象到本质
说回开头那个 CrashLoopBackOff 的 case。
凌晨两点,我开始了排查流程。
第一步:看 pod 状态
$ kubectl get pods -n production
NAME READY STATUS RESTARTS AGE
my-app-7f8b9c5d6-xk4pw 0/1 CrashLoopBackOff 5 10m
CrashLoopBackOff,重启了 5 次,说明启动后很快就挂了。
第二步:看 pod 日志
$ kubectl logs my-app-7f8b9c5d6-xk4pw -n production
Error: Could not find or load main class com.example.MyApp
Caused by: java.lang.ClassNotFoundException: com.example.MyApp
Java 找不到主类,这通常是打包问题。
第三步:看 pod 详情
$ kubectl describe pod my-app-7f8b9c5d6-xk4pw -n production | grep -A 10 "Containers:"
Containers:
app:
Container ID: docker://abc123...
Image: my-registry.com/my-project/my-app:v1.0.0
Image ID: docker-pullable://my-registry.com/my-project/my-app@sha256:xyz...
Port: 8080/TCP
Host Port: 0/TCP
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Wed, 16 Jul 2026 02:15:23 +0800
Finished: Wed, 16 Jul 2026 02:15:24 +0800
启动只用了 1 秒钟就挂了,exit code 是 1,符合日志里的 ClassNotFoundException。
第四步:检查镜像
我登到 CI/CD 服务器上,重新打了个镜像,这次特意检查了一下:
# 本地测试镜像
$ docker run --rm my-registry.com/my-project/my-app:v1.0.0
Error: Could not find or load main class com.example.MyApp
果然是镜像本身的问题。
查了一下构建日志,发现问题了:
# 错误的 Dockerfile
FROM openjdk:11-jre-slim
COPY target/my-app.jar /app/
WORKDIR /app
CMD ["java", "-jar", "my-app.jar"]
构建时没有生成 JAR 文件,或者 JAR 文件的位置不对。修正后:
# 修正后的 Dockerfile
FROM openjdk:11-jre-slim
COPY target/my-app-1.0.0.jar /app/my-app.jar
WORKDIR /app
CMD ["java", "-jar", "my-app.jar"]
重新部署后,服务正常了。
排查思路总结:
- 先看 pod 状态(kubectl get pods)
- 再看 pod 日志(kubectl logs)
- 然后看 pod 详情(kubectl describe pod)
- 检查镜像本身(docker run)
- 检查配置文件和构建过程
监控:不要等出事再救
监控系统的重要性,我就不多说了。关键是监控什么。
我推荐先从这几个核心指标开始:
# prometheus-rules.yaml
groups:
- name: kubernetes-app
rules:
# Pod 重启过多
- alert: PodRestartingTooMuch
expr: increase(kube_pod_container_status_restarts_total[1h]) > 5
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} is restarting too much"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} has restarted {{ $value }} times in the last hour"
# Pod 长时间 Pending
- alert: PodPendingTooLong
expr: kube_pod_status_phase{phase="Pending"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} has been pending for more than 10 minutes"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is in Pending state"
# 节点资源不足
- alert: NodeNotReady
expr: kube_node_status_ready{condition="true"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Node {{ $labels.node }} is not ready"
description: "Node {{ $labels.node }} has been not ready for more than 5 minutes"
除了这些基础告警,我还加了业务层面的监控:
# 业务指标监控
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value | humanizePercentage }} for {{ $labels.service }}"
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High latency detected"
description: "P95 latency is {{ $value }}s for {{ $labels.service }}"
性能优化:从能跑到跑得好
当 Kubernetes 集群上的应用越来越多,性能优化就变得重要了。
我遇到过一个 case,某个 Java 应用在 Kubernetes 上跑得比在虚拟机上慢。
先看了一下资源使用情况:
$ kubectl top pod -n production my-app-7f8b9c5d6-xk4pw
NAME CPU(cores) MEMORY(bytes)
my-app-7f8b9c5d6-xk4pw 500m 1Gi
CPU 和内存看起来都不高,但响应时间却很慢。
查了一下容器配置:
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
问题出在哪里?
Java 的垃圾回收(GC)在容器环境下需要特别配置。默认情况下,JVM 会根据宿主机的内存来决定堆大小,而不是容器的 limits。
解决方案:
spec:
containers:
- name: app
image: my-registry.com/my-project/my-app:v1.0.0
env:
- name: JAVA_OPTS
value: "-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
配置后,应用的响应时间明显提升了。
其他常见的性能优化点:
设置合理的 requests 和 limits
- requests:保证应用的最小资源
- limits:防止应用消耗过多资源
- 通常 requests 和 limits 的比例在 1:2 左右比较合适
使用节点亲和性
affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-type operator: In values: - high-memory使用 Horizontal Pod Autoscaler
apiVersion: autoscaling/v2 kind: HorizontalPodAutcaler metadata: name: my-app-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: my-app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70
网络问题:最常见的坑之一
Kubernetes 的网络问题也经常让人头疼。
有一次,两个服务之间一直不通,但服务本身都正常。
# 服务 A 能访问外网
$ kubectl exec -it service-a-xxx -- curl https://www.google.com
OK
# 但服务 A 访问服务 B 不通
$ kubectl exec -it service-a-xxx -- curl http://service-b:8080
curl: (7) Failed to connect to service-b port 8080: Connection refused
排查过程:
先检查服务 B 是否正常运行
$ kubectl get pods -n production | grep service-b service-b-7f8b9c5d6-abcde 1/1 Running 0 10m检查服务 B 的 Service
$ kubectl get svc service-b -n production NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service-b ClusterIP 10.96.123.45 <none> 8080/TCP 10m检查服务 B 的 endpoints
$ kubectl get endpoints service-b -n production NAME ENDPOINTS AGE service-b 10.244.1.5:8080 10m检查网络策略
$ kubectl get networkpolicies -n production No resources found in production namespace.直接用 IP 访问
$ kubectl exec -it service-a-xxx -- curl http://10.244.1.5:8080 OK
用 IP 能访问,说明网络本身没问题,问题出在 DNS 解析上。
检查一下 CoreDNS:
$ kubectl get pods -n kube-system | grep coredns
coredns-5d78c9869d-abcde 1/1 Running 0 30d
coredns-5d78c9869d-fghij 1/1 Running 0 30d
$ kubectl logs -n kube-system coredns-5d78c9869d-abcde | tail -20
[INFO] plugin/errors: 2 www.google.com. A: 142.250.188.46
[INFO] plugin/errors: 2 service-b.production.svc.cluster.local. A: no such host
CoreDNS 日志显示 service-b.production.svc.cluster.local 找不到。
问题找到了——service B 的 Service 定义里 namespace 写错了:
# 错误的 Service 定义
apiVersion: v1
kind: Service
metadata:
name: service-b
namespace: staging # 应该是 production
spec:
selector:
app: service-b
ports:
- port: 8080
targetPort: 8080
修正后,网络就通了。
网络排查思路:
- 先用 IP 测试,排除 DNS 问题
- 检查 Service 和 Endpoints 是否正常
- 检查网络策略(NetworkPolicy)
- 检查 CoreDNS 日志
- 检查 CNI 插件配置(Calico、Flannel 等)
运维工具:不是越多越好
Kubernetes 的生态很丰富,有很多运维工具。但不是工具越多越好,适合自己团队的才是最好的。
我现在用的工具集:
kubectl:基础命令行工具,必备
k9s:终端 UI 工具,比 kubectl 直观
$ brew install k9s $ k9skube-bench:安全检查工具
$ kubectl run --rm -i --tty kube-bench --image=aquasec/kube-bench --restart=Never --version=1.24kube-hunter:安全漏洞扫描
$ docker run --rm --network=host aquasec/kube-hunterveltig:管理多个 Kubernetes 集群
# veltig-config.yaml clusters: - name: production context: production-context - name: staging context: staging-context
总结
Kubernetes 运维是个持续学习和踩坑的过程。从最初的各种坑到现在能从容应对问题,我总结了几点经验:
- 容量规划很重要:不要等到节点资源不足了才扩容
- 镜像管理要规范:版本号要明确,拉取策略要合理
- 存储配置要小心:StorageClass 和持久化策略要搞清楚
- 监控要提前建设:不要等出事了再装监控
- 排查要有思路:从现象到本质,一步步缩小范围
- 工具要精简:适合自己团队的才是最好的
- 文档要写清楚:特别是网络拓扑和资源配额
Kubernetes 是个好东西,但它不是万能的。合理使用,加上良好的运维实践,才能发挥它的最大价值。
凌晨两点的那个故障后来很快就解决了,但那次经历让我更加重视日常的运维工作。毕竟,凌晨两点被叫起来修bug,谁都不想多经历几次。
版权声明: 本文首发于 指尖魔法屋-Kubernetes运维折腾手记(https://blog.thinkmoon.cn/post/131_kubernetes_ops_deployment_troubleshooting/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。