跳到内容

元数据卡

  • 前置知识:第7章(Docker 与 K8s 基础)、第10章(IaC 与 GitOps)
  • 预计时间:45 分钟
  • 阅读模式:高度专注
  • 完成标志:理解 Ingress、NetworkPolicy、StatefulSet、PV/PVC 的生产级用法,能设计 Pod 资源配额和 HPA 策略,掌握多集群隔离和生产清单

你的进度

远征军的指挥系统在 K8s 上已经跑了一阵子。无状态服务跑得不错——Deployment 的滚动升级、HPA 的自动扩缩、Service 的负载均衡都稳定运行。但前线开始出现更复杂的场景:

情报分析服务需要挂载持久化存储——它要把每次侦查的原始数据存下来供后续分析。但 Pod 重启后,数据丢失了。新的需求也来了——不同哨塔之间的通信需要防火墙策略,外部用户需要通过域名访问指挥台的 API 而非 IP。你还需要为新战区隔离一个独立的 K8s 命名空间。

"Pod 重启后数据会丢"——这种 pod 跑无状态服务没问题,但情报分析的数据不能丢。DataDog 的 Agent 需要 nginx 的日志来采集——这些是伴生容器角色,不是业务代码能解决的。部署越来越复杂,你发现只掌握第 7 章的 Pod/Service/Deployment 远远不够。

你的任务

扩展你的 K8s 技能树到生产级别:管理有状态服务的 StatefulSet 和 PV/PVC,配置细粒度的网络策略和服务网格,设计合理的资源配额和自动扩缩策略,掌握多集群和命名空间隔离模式。最终整理一张 K8s 生产部署的 checklist。


破局 · 溯源


Ingress:应用层的流量入口

Service 是集群内部的负载均衡器。当外部流量要进入集群时——比如侦查系统的 API 需要对外网提供服务——你需要 Ingress 来统一管理入口。林将军不会让外部信使直接绕到每个帐篷门口,而是统一在要塞大门处登记、验信、分派:

外部请求 → 域名解析 → Ingress Controller (Nginx / Traefik)

                      根据 host 和 path 路由
                      /  →  frontline-service:80
                      /api → atlas-gateway:8080
yaml
# ingress.yaml
# 将外部域名路由到集群内部服务
# kubectl apply -f ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: atlas-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.atlas-frontline.com
    secretName: atlas-tls-secret
  rules:
  - host: api.atlas-frontline.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: atlas-gateway
            port:
              number: 8080
      - path: /intel
        pathType: Prefix
        backend:
          service:
            name: intel-service
            port:
              number: 8081

Ingress Controller 有很多选择——Nginx Ingress Controller 最普及,Traefik 在微服务和 Service Mesh 场景更灵活,HAProxy Ingress 在性能场景受青睐。

Ingress 可以附加高级注解:

yaml
annotations:
  # 限流
  nginx.ingress.kubernetes.io/limit-rps: "1000"
  nginx.ingress.kubernetes.io/limit-burst-multiplier: "3"

  # CORS
  nginx.ingress.kubernetes.io/enable-cors: "true"
  nginx.ingress.kubernetes.io/cors-allow-origin: "https://console.atlas-frontline.com"

  # 白名单
  nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8, 172.16.0.0/12"

NetworkPolicy:细粒度网络访问控制

默认情况下,K8s 集群中的所有 Pod 可以自由通信——这在生产环境中是严重的安全隐患。情报服务的数据库不应该能被前端服务直接访问。你需要定义明确的白名单,就像在阵地里规定哪些单位可以出入哪些区域:

yaml
# network-policy.yaml
# 只允许 atlas-gateway 访问 intel-service
# kubectl apply -f network-policy.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: intel-service-policy
  namespace: frontline
spec:
  podSelector:
    matchLabels:
      service: intel-service
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          service: atlas-gateway
    ports:
    - protocol: TCP
      port: 8081
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: database
    ports:
    - protocol: TCP
      port: 5432

规则说明:

  • podSelector:策略的目标 Pod(谁被保护)
  • ingress.from:允许访问目标 Pod 的来源
  • egress.to:目标 Pod 可以访问的目标
  • namespaceSelector:跨命名空间的规则

NetworkPolicy 具有累积性——如果一个 Pod 被多个 NetworkPolicy 匹配,允许的流量是这些规则的并集。没有 NetworkPolicy 的命名空间默认允许所有流量——这意味着你必须主动定义策略才有防护。

NetworkPolicy 需要一个支持它的 CNI 插件——Calico、Cilium、Weave Net 都支持。Flannel 不支持 NetworkPolicy。

Service Mesh 简介

当 NetworkPolicy 不够精细时(例如需要重试、熔断、流量分割、mTLS 加密),你进入 Service Mesh 的领域。

Service Mesh (Istio / Linkerd) 模式:

每个 Pod 旁边注入一个 sidecar proxy(Envoy)
所有进出本 Pod 的流量都经过这个 proxy

Pod A ──→ sidecar (Envoy) ──mTLS──→ sidecar (Envoy) ──→ Pod B

Service Mesh 能做的事情:

  • 流量管理:金丝雀发布、流量分割、超时和重试策略
  • 安全:自动 mTLS、服务间身份认证
  • 可观测性:HTTP 指标、调用拓扑图、访问日志

但 Service Mesh 有显著的成本:每个请求多一次代理跳转(增加 2-5ms 延迟),sidecar 的额外资源消耗,运维复杂度显著增加。对于中小团队,先用好 NetworkPolicy 和 K8s 内置的 Service 就足够。Service Mesh 在 100+ 服务的大型微服务网格中才真正体现价值。


StatefulSet 和 PV/PVC:有状态工作负载

Deployment 适合无状态服务——它们可以互换、可以随意扩缩、重启后不关心数据。但当你的服务需要持久化数据时——数据库、消息队列、文件存储——你需要 StatefulSet。

StatefulSet 和 Deployment 的区别:

Deployment:
  Pod 名称随机: atlas-postgres-6f9b7c8d9-x2y4z
  所有 Pod 完全等价
  扩缩顺序随机
  PVC 不固定,可以共享

StatefulSet:
  Pod 名称有序: atlas-postgres-0, atlas-postgres-1, atlas-postgres-2
  Pod 有稳定网络标识(通过 Headless Service)
  扩缩按顺序(0 → 1 → 2 创建,2 → 1 → 0 缩容)
  每个 Pod 绑定唯一的 PVC(Pod-0 删除重建后挂载同样的卷)

PersistentVolume 和 PersistentVolumeClaim

PV 是集群中的一块存储资源(由管理员预置或通过 StorageClass 动态供应)。PVC 是 Pod 对存储的请求。

管理员创建 StorageClass

StatefulSet 的 volumeClaimTemplates 自动生成 PVC

StorageClass 的 provisioner 自动创建 PV(动态供应)

PVC 绑定到 PV

Pod 挂载 PVC 到指定路径

情报分析服务需要持久化存储——每次侦查的原始数据不能因为 Pod 重启就丢掉。StatefulSet 和 PV/PVC 确保每个 Pod 挂载独立的、持久的存储卷。部署一个 Postgres 试试:

yaml
# statefulset-postgres.yaml
# 部署有状态的 PostgreSQL
# kubectl apply -f statefulset-postgres.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: atlas-postgres
spec:
  serviceName: atlas-postgres-headless
  replicas: 3
  selector:
    matchLabels:
      service: atlas-postgres
  template:
    metadata:
      labels:
        service: atlas-postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
          name: pg
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: atlas-db-secret
              key: password
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            cpu: "1"
            memory: "2Gi"
          limits:
            cpu: "2"
            memory: "4Gi"
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "fast-ssd"
      resources:
        requests:
          storage: 100Gi
yaml
# headless-service.yaml
# StatefulSet 需要 Headless Service 来提供稳定的网络标识
# kubectl apply -f headless-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: atlas-postgres-headless
spec:
  clusterIP: None    # Headless:不做负载均衡
  selector:
    service: atlas-postgres
  ports:
  - port: 5432
    name: pg

StatefulSet 的 Pod 可以通过 DNS 名称互访:atlas-postgres-0.atlas-postgres-headless.frontline.svc.cluster.local

PV/PVC 的生命周期:

PVC 创建(Pending)→ StorageClass 动态供应 PV
                    → PV 创建(Available)
                    → 绑定到 PVC(Bound)
                    → Pod 使用
                    → Pod 删除(PVC 默认不自动删除)
                    → 管理员回收 PV(Retain / Delete / Recycle)

persistentVolumeReclaimPolicy 决定 PVC 释放后 PV 的行为:

  • Retain:保留数据。管理员手动处理——适合需要审计或手动迁移的数据
  • Delete:自动删除 PV 和后端存储——适合临时数据或 CI/CD 环境
  • Recycle:清空数据后重新使用——已废弃,建议用动态供应的 Delete 策略

ConfigMap 和 Secrets——生产环境中的正确用法

第 7 章你已经见过基本用法。生产环境中有更精细的要求:

不可变 ConfigMap / Secret

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: atlas-config
immutable: true    # 创建后不可修改——只能删除重建
data:
  LOG_LEVEL: "INFO"
  CACHE_TTL: "300"

不可变的好处:减少 API Server 的负载(无需 watch 变更),避免 Pod 运行时配置不一致。只在确认配置不会频繁变动时使用。

External Secrets Operator

生产环境不把 Secret 明文放在 Git 中。External Secrets Operator(ESO)从外部密钥服务同步到 K8s Secret。就像军械库的钥匙不放在墙上挂着的钥匙板上,而是锁在保险柜里——需要通过特定流程才能取用:

yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: atlas-db-secret
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: atlas-db-secret
    creationPolicy: Owner
  data:
  - secretKey: password
    remoteRef:
      key: secret/atlas/frontline/database
      property: password

ESO 会定期从 Vault / AWS Secrets Manager / GCP Secret Manager 读取值,写入 K8s Secret。你只需要在 Git 中管理 ExternalSecret 这个 CRD 资源。


Pod 资源 requests/limits 和自动扩缩

requests 和 limits 的正确配置

这是一个常见的困扰。下面是原则:

requests(调度保证):
  Pod 调度时,K8s 根据 Node 上剩余的 requests 做分配
  多个 Pod 的 requests 总和不能超过 Node 的总容量
  requests 不是"固定分配"——Pod 实际可以用更多

limits(运行上限):
  Pod 不能超过 limits
  CPU 是 throttled(降频),内存是 OOMKilled(被杀)

部署时的经验法则:

生产服务:
  requests: limits 的 50-80%
  CPU requests: 根据过去一周的 P99 CPU 使用率的 80%
  内存 limits: 根据压力测试的最大内存使用量 + 20% buffer

关键批处理任务:
  requests = limits(确保它在调度时有确定的资源,而且不会主动超卖)

开发测试环境:
  可以只设 requests 不设 limits(允许 Burst)
  但设置一份"软上限"防止一个 Pod 拖垮整个 Node

配置示例:

yaml
resources:
  requests:
    cpu: "500m"        # 0.5 核
    memory: "512Mi"
  limits:
    cpu: "1000m"       # 最多用 1 核
    memory: "1Gi"      # 超过会被 OOM Kill

HPA 的自定义指标

除了 CPU/内存,HPA 可以根据自定义指标(如 QPS、队列长度)扩缩:

yaml
# hpa-custom-metrics.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: intel-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: intel-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: intel_queue_depth
      target:
        type: AverageValue
        averageValue: 100

需要 Prometheus Adapter 来提供自定义指标。

VPA(Vertical Pod Autoscaler)

HPA 水平扩缩(加副本),VPA 垂直扩缩(加资源)。VPA 分析历史使用模式,自动调整 Pod 的 requests 和 limits。

yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: intel-service-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: intel-service
  updatePolicy:
    updateMode: "Auto"
  resourcePolicy:
    containerPolicies:
    - containerName: '*'
      minAllowed:
        cpu: 100m
        memory: 50Mi
      maxAllowed:
        cpu: "4"
        memory: 10Gi

VPA 推荐与 HPA 同时使用,但要确保两者不冲突——VPA 调整 Pod 的资源规格,HPA 调整副本数。两者配合时,通常让 VPA 基于 CPU/内存调整 requests,HPA 基于自定义指标(如 QPS)调整副本数。


多集群和命名空间隔离

命名空间隔离

生产环境中,不同的应用、团队、环境用命名空间隔离:

命名空间规划:
  frontline-prod     → 生产环境
  frontline-staging  → 预发布环境
  frontline-dev      → 开发环境
  monitoring         → 监控基础设施
  logging            → 日志采集
  istio-system       → 服务网格控制面

通过 ResourceQuota 限制命名空间的资源上限:

yaml
# resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: frontline-quota
  namespace: frontline-dev
spec:
  hard:
    requests.cpu: "20"
    requests.memory: "40Gi"
    limits.cpu: "40"
    limits.memory: "80Gi"
    persistentvolumeclaims: "10"
    pods: "50"
    services: "10"

LimitRange 设置命名空间内的 Pod 和容器的默认资源范围:

yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: frontline-limits
  namespace: frontline-dev
spec:
  limits:
  - max:
      cpu: "4"
      memory: "8Gi"
    min:
      cpu: "100m"
      memory: "64Mi"
    default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "200m"
      memory: "256Mi"
    type: Container

多集群策略

当跨越地理区域或完全隔离环境运行时,你需要多集群方案。

常见多集群模式:

1. Hub-and-Spoke(中心辐射):
   中心集群运行管理面(Argo CD、监控)
   子集群运行工作负载
   子集群通过 Agent 向中心集群注册

2. 联邦(Federation):
   KubeFed(已归档)
   多个集群之间同步资源
   一个 Deployment YAML 可以推送到多个集群

3. 独立集群 + 统一 GitOps:
   每个集群部署独立的 Argo CD
   所有集群的配置都在同一个 Git 仓库的不同目录
   每个目录对应一个集群的配置

对于规模不大的团队,方案 3(独立集群 + 统一 GitOps)是最简单的多集群管理方式。


生产部署 Checklist

部署生产 K8s 集群前逐项确认:

集群层面:
  [✓] etcd 备份策略——每日备份到独立存储
  [✓] 控制平面高可用——至少 3 个控制节点
  [✓] CNI 插件全网通行并支持 NetworkPolicy
  [✓] 节点自动修复(Cluster Autoscaler)

应用层面:
  [✓] 所有 Pod 设置了 requests 和 limits
  [✓] 关键服务配置了 livenessProbe 和 readinessProbe
  [✓] 关键服务有 PDB(PodDisruptionBudget)
  [✓] 敏感信息使用 K8s Secret 或 External Secrets Operator
  [✓] 镜像使用不可变标签(semver),不用 latest

网络层面:
  [✓] 关键服务之间有 NetworkPolicy
  [✓] Ingress 配置了 TLS 终止
  [✓] 非必要端口不对外暴露

可观测性层面:
  [✓] 集群级别监控(Node Exporter + cAdvisor)
  [✓] 应用级别 Metrics(Prometheus 采集)
  [✓] 日志采集(Fluentd / Loki)
  [✓] 关键告警配置了通知路由

运维层面:
  [✓] HPA 配置了合理的 min/max replicas
  [✓] 有状态服务的 PVC 配置了回收策略
  [✓] 有灾备方案(跨 AZ / 跨区域)
  [✓] rolling update 的 maxUnavailable/maxSurge 已配置

常见陷阱

  1. Pod 的 requests > limits。 这不可能。K8s 不会允许。requests 必须 <= limits。不合理的配置会导致 Pod 调度失败或运行时行为异常。

  2. 集群自动扩缩(Cluster Autoscaler)和 HPA 之间震荡。 当 HPA 因为 CPU 升高而扩容,但 Node 资源不足触发 Cluster Autoscaler 加 Node——新 Node 启动后 CPU 下降,HPA 缩容,Cluster Autoscaler 回收 Node——可能造成反复震荡。设置 HPA 的冷却时间(--horizontal-pod-autoscaler-downscale-stabilization)来缓解。

  3. 显式写死 Pod IP 或使用 hostPort。 Pod IP 在节点迁移、Pod 重启、扩缩容后会变。应该通过 Service 的 DNS 名称访问。如果必须固定 IP(例如遗留数据库),使用 StatefulSet + Headless Service。

  4. Secret 的数据没有加密。 默认情况下 K8s Secret 只做 base64 编码,不是加密。任何有 etcd 访问权限的人都能读取。启用 etcd 加密(--encryption-provider-config)或使用 External Secrets Operator。

  5. 删除 PVC 导致数据丢失。 当 Pod 被删除后,PVC 默认保留。但如果你 kubectl delete pvchelm uninstall 删除了 PVC,PV 根据 reclaim policy 可能被删除。对有状态服务,始终确认 Retain 策略或明确的备份计划。


通关挑战

  • 热身:在本地 K8s 集群中创建一个 PV/PVC 和一个挂载该 PVC 的 Pod。写入文件,删除 Pod,再创建一个新 Pod 挂载同一个 PVC——验证数据持久化。

  • 挑战:部署一个 Postgres StatefulSet(3 副本)到 K8s 集群。配置 NetworkPolicy 只允许某几个服务访问 5432 端口。验证未授权的 Pod 无法连接。

bash
# 创建 NetworkPolicy 验证
# 启动一个没有授权的 Pod
kubectl run test-pod --image=alpine --rm -it -- sh
# 尝试连接 postgres-0 —— 应该超时
nc -zv atlas-postgres-0.atlas-postgres-headless 5432

# 启动一个有授权的 Pod(匹配 network policy 的标签)
kubectl run test-pod --labels="service=atlas-gateway" --image=alpine --rm -it -- sh
# 连接 postgres-0 —— 应该成功
nc -zv atlas-postgres-0.atlas-postgres-headless 5432
  • 观察:部署后打开 Prometheus,创建一个面板展示每个命名空间的总 CPU 和 内存 requests。观察 HPA 触发时 requests 总和的变化。配置一个 PDB(PodDisruptionBudget),模拟 Node 下线,观察 K8s 如何遵守最少可用 Pod 的约束。
yaml
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: intel-service-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      service: intel-service

验收标准

  • 能用 Ingress 实现外部流量的域名路由
  • 能用 NetworkPolicy 实现细粒度的网络隔离
  • 能用 StatefulSet + PV/PVC 部署有状态服务
  • 能配置合理的 requests/limits 和 HPA 策略
  • 能基于生产 checklist 确认集群部署质量

常见卡点

  1. HPA 不起作用? 检查 metrics-server 是否正在运行(kubectl get pods -n kube-system | grep metrics-server)。检查 Deployment 的 Pod 是否设置了资源 requests。HPA 不会在没有 requests 的情况下基于 CPU/内存做自动扩缩。

  2. StatefulSet 缩容时数据会丢失吗? 默认的 volumeClaimTemplates 创建 PVC 后不会自动删除。缩容时,对应 Pod 被删除但 PVC 保留。如果要安全缩容,需要手动处理 PVC。扩容时,新的 Pod 会绑定新的 PVC(从 volumeClaimTemplates 创建)。

  3. NetworkPolicy 创建后意外拦截了流量? NetworkPolicy 默认是白名单模式。一旦为命名空间设置了任何 NetworkPolicy,没有显式允许的流量都会被拒绝。确保你的 NetworkPolicy 覆盖了所有需要通信的服务。

  4. Ingress 404? 确认 Ingress Controller 在运行(kubectl get pods -n ingress-nginx)。检查 Ingress 资源中的 ingressClassName 是否正确。确认后端 Service 和 Pod 的端口匹配。


现在不需要理解

  • Operator 开发(用 Kubebuilder 写自己的 Operator)——高级话题,当有自定义需求时再学
  • K8s 网络插件的原理(CNI、Calico BGP、Cilium eBPF)——扩展知识,故障排查时深入
  • 多集群联邦的复杂配置——大多数团队长期停留在"多个独立集群+统一 GitOps"模式

旅人笔记

K8s 生产实践不是去学更多的 API 对象。Deployment 和 Pod 只占 K8s 能力的三分之一。真正的生产挑战来自你能用什么机制管理有状态数据、隔离网络流量、分配资源配额、建立跨集群治理。Ingress 让你有能力管理外部流量入口,NetworkPolicy 给你内部微隔离的能力,StatefulSet 让有状态服务不掉数据,PV/PVC 把存储从 Pod 生命周期解耦。这些都掌握了,你才真正理解了"容器编排"在生产中意味着什么。


下一站预告

可观测性的三支柱你已经了解了——但那只覆盖了"基础"。生产级别的可观测性面对的是高基数指标、告警疲劳、数十个 Grafana 面板的管理压力。下一章,我们从"能用"走向"好用"——深入 Metrics 的基数控制、告警去重、和可观测性即代码的实践。

Built with VitePress | Software Systems Atlas