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"]

重新部署后,服务正常了。

排查思路总结:

  1. 先看 pod 状态(kubectl get pods)
  2. 再看 pod 日志(kubectl logs)
  3. 然后看 pod 详情(kubectl describe pod)
  4. 检查镜像本身(docker run)
  5. 检查配置文件和构建过程

监控:不要等出事再救

监控系统的重要性,我就不多说了。关键是监控什么。

我推荐先从这几个核心指标开始:

# 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

配置后,应用的响应时间明显提升了。

其他常见的性能优化点:

  1. 设置合理的 requests 和 limits

    • requests:保证应用的最小资源
    • limits:防止应用消耗过多资源
    • 通常 requests 和 limits 的比例在 1:2 左右比较合适
  2. 使用节点亲和性

    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: node-type
              operator: In
              values:
              - high-memory
    
  3. 使用 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

排查过程:

  1. 先检查服务 B 是否正常运行

    $ kubectl get pods -n production | grep service-b
    service-b-7f8b9c5d6-abcde   1/1     Running   0          10m
    
  2. 检查服务 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
    
  3. 检查服务 B 的 endpoints

    $ kubectl get endpoints service-b -n production
    NAME        ENDPOINTS          AGE
    service-b   10.244.1.5:8080    10m
    
  4. 检查网络策略

    $ kubectl get networkpolicies -n production
    No resources found in production namespace.
    
  5. 直接用 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

修正后,网络就通了。

网络排查思路:

  1. 先用 IP 测试,排除 DNS 问题
  2. 检查 Service 和 Endpoints 是否正常
  3. 检查网络策略(NetworkPolicy)
  4. 检查 CoreDNS 日志
  5. 检查 CNI 插件配置(Calico、Flannel 等)

运维工具:不是越多越好

Kubernetes 的生态很丰富,有很多运维工具。但不是工具越多越好,适合自己团队的才是最好的。

我现在用的工具集:

  1. kubectl:基础命令行工具,必备

  2. k9s:终端 UI 工具,比 kubectl 直观

    $ brew install k9s
    $ k9s
    
  3. kube-bench:安全检查工具

    $ kubectl run --rm -i --tty kube-bench --image=aquasec/kube-bench --restart=Never --version=1.24
    
  4. kube-hunter:安全漏洞扫描

    $ docker run --rm --network=host aquasec/kube-hunter
    
  5. veltig:管理多个 Kubernetes 集群

    # veltig-config.yaml
    clusters:
      - name: production
        context: production-context
      - name: staging
        context: staging-context
    

总结

Kubernetes 运维是个持续学习和踩坑的过程。从最初的各种坑到现在能从容应对问题,我总结了几点经验:

  1. 容量规划很重要:不要等到节点资源不足了才扩容
  2. 镜像管理要规范:版本号要明确,拉取策略要合理
  3. 存储配置要小心:StorageClass 和持久化策略要搞清楚
  4. 监控要提前建设:不要等出事了再装监控
  5. 排查要有思路:从现象到本质,一步步缩小范围
  6. 工具要精简:适合自己团队的才是最好的
  7. 文档要写清楚:特别是网络拓扑和资源配额

Kubernetes 是个好东西,但它不是万能的。合理使用,加上良好的运维实践,才能发挥它的最大价值。

凌晨两点的那个故障后来很快就解决了,但那次经历让我更加重视日常的运维工作。毕竟,凌晨两点被叫起来修bug,谁都不想多经历几次。

版权声明: 本文首发于 指尖魔法屋-Kubernetes运维折腾手记https://blog.thinkmoon.cn/post/131_kubernetes_ops_deployment_troubleshooting/) 转载或引用必须申明原指尖魔法屋来源及源地址!