跳转至

02 深入理解 Kubernetes 的编排对象

Kubernetes 系统是一套分布式容器应用编排系统。当用来承载业务负载时,主要使用的编排对象有 Deployment、ReplicaSet、StatefulSet、DaemonSet 等。读者可能会好奇:Kubernetes 管理的核心对象不是 Pod 吗?为什么要专门学习这些编排对象?

事实上,这些编排对象都是对 Pod 对象的扩展与更高层面的封装,并作为核心工作负载 API 固化在 Kubernetes 系统中。认真的回顾和理解这些编排对象,依据生产实践场景进行合理配置,能让 Kubernetes 系统更好地支持业务需求。本文将从实际应用发布的场景入手,分析和梳理具体场景中需要考虑的编排因素,并整理一套可落地的编排对象实践指南。

一、常规业务容器部署策略

策略一:强制运行不少于 2 个容器实例副本

在应对常规业务容器部署场景时,Kubernetes 提供了 Deployment 标准编排对象。Deployment 用于管理无状态的业务容器 Pod。

需要强调的是:容器组 Pod 被设计为短生命周期的实例,它无法像虚拟机那样持久保存进程状态。由于 Pod 实例具有易失性,生产环境中每次部署的实例数必须是多副本(默认最少 2 个副本)。

多副本 Deployment 配置示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

策略二:采用节点亲和与 Pod 亲和/反亲和实现高可用

Kubernetes 默认调度策略是选取资源最空闲的节点部署 Pod,并不主动考虑业务高可用的分布情况。若未限制亲和性,可能多个副本会被调度到同一台物理节点上,一旦该主机宕机就会导致服务中断。

为了实现业务容错与高可用,需要通过 Node 节点亲和性(Node Affinity)Pod 反亲和性(Pod Anti-Affinity) 进行约束。

1. 主机亲和性(Node Affinity)

主机亲和性分为两种类型:

  • requiredDuringSchedulingIgnoredDuringExecution(硬亲和):强制调度到满足条件的节点;
  • preferredDuringSchedulingIgnoredDuringExecution(软亲和):优先调度到满足条件的节点,无法满足时不强求。

主机亲和性配置示例:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: gcr.azk8s.cn/google-samples/node-hello:1.0

2. Pod 间反亲和性(Pod Anti-Affinity)

使用 Pod 反亲和性可以避免同一个应用的多个副本被调度到同一个宿主机节点上,规避单点故障:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: zk-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: zk
  template:
    metadata:
      labels:
        app: zk
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - zk
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: zk
        image: zookeeper:3.5

上述配置以主机的 kubernetes.io/hostname 为拓扑域,确保带有 app=zk 标签的 Pod 被分散部署在不同的物理节点上。

策略三:使用 preStop Hook 和 readinessProbe 保证平滑零中断更新

在发布更新应用时,必须确保滚动更新过程中业务请求零中断。

在容器配置中引入 readinessProbe(就绪检查)与 preStop 生命周期回调:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      component: nginx
  template:
    metadata:
      labels:
        component: nginx
    spec:
      containers:
      - name: nginx
        image: xds2000/nginx-hostname
        ports:
        - name: http
          hostPort: 80
          containerPort: 80
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /
            port: 80
            httpHeaders:
            - name: X-Custom-Header
              value: Awesome
          initialDelaySeconds: 15
          timeoutSeconds: 1
        lifecycle:
          preStop:
            exec:
              command: ["/bin/bash", "-c", "sleep 30"]

机制原理:

  1. readinessProbe 就绪检查:容器启动后,只有通过就绪检查探测,控制面才会将其加进 Service 的 Endpoint 列表中。转发规则更新完成后,流量才会切入新 Pod,避免了新 Pod 尚未就绪导致请求失败;
  2. preStop 优雅下线 Hook:在 Pod 被销毁前执行 sleep 30,为 Endpoint Controller 和 kube-proxy 留出足够的时间更新转发规则与摘除流量,使旧 Pod 处于 Terminating 状态时仍能继续处理残余请求。

策略四:通过泛域名转发南北向流量范式

当常规集群对外暴露统一公网 IP(Ingress 或 LoadBalancer)并配置泛域名(如 *.test.foo.io)时,希望根据请求中不同的 Host 自动转发到对应的后端 Service(例如 a.test.foo.io 转发到 my-svc-a)。

我们可以利用 OpenResty/Nginx 的 Lua 脚本来实现泛域名动态转发路由。

示例 proxy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: proxy
  labels:
    component: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      component: nginx
  template:
    metadata:
      labels:
        component: nginx
    spec:
      containers:
      - name: nginx
        image: "openresty/openresty:centos"
        ports:
        - name: http
          containerPort: 80
          protocol: TCP
        volumeMounts:
        - mountPath: /usr/local/openresty/nginx/conf/nginx.conf
          name: config
          subPath: nginx.conf
      - name: dnsmasq
        image: "janeczku/go-dnsmasq:release-1.0.7"
        args:
          - --listen
          - "127.0.0.1:53"
          - --default-resolver
          - --append-search-domains
          - --hostsfile=/etc/hosts
          - --verbose
      volumes:
      - name: config
        configMap:
          name: configmap-nginx
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-nginx
  labels:
    component: nginx
data:
  nginx.conf: |-
    worker_processes  1;
    error_log  /error.log;
    events {
        use epoll;
        worker_connections  1024;
    }
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        keepalive_timeout 120s;
        resolver 127.0.0.1:53 ipv6=off;
        server {
            listen 80;
            location / {
                set $service  '';
                rewrite_by_lua '
                    local host = ngx.var.host
                    local m = ngx.re.match(host, "(.+).test.foo.io")
                    if m then
                        ngx.var.service = "my-svc-" .. m[1]
                    end
                ';
                proxy_pass http://$service;
            }
        }
    }

配套 Service service.yaml 与 Ingress ingress.yaml

apiVersion: v1
kind: Service
metadata:
  name: service-nginx
  labels:
    component: nginx
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    component: nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-nginx
spec:
  rules:
  - host: "*.test.foo.io"
    http:
      paths:
      - path: /
        backend:
          serviceName: service-nginx
          servicePort: 80

二、有状态业务容器部署策略

StatefulSet 是专门针对有状态应用(如 MySQL、Redis、Zookeeper、Elasticsearch 等)设计的编排对象。

1. StatefulSet 基础配置示例

编写 web.yaml 部署 Headless Service 与 StatefulSet:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: gcr.azk8s.cn/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

查看创建的 Pod 列表:

$ kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          1m
web-1     1/1       Running   0          1m

StatefulSet 的核心特征在于:每个 Pod 拥有固定的顺序索引(web-0, web-1)与稳定的网络标识(DNS 域名)

执行 Hostname 检验:

$ for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1

在集群内部使用 nslookup 解析固定网络域名:

$ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
/ # nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.6

/ # nslookup web-1.nginx
Name:      web-1.nginx
Address 1: 10.244.2.6

策略五:灵活运用 StatefulSet 的 Pod 管理策略

默认情况下 StatefulSet 按顺序依次创建或终止 Pod(挂载顺序 0 -> 1 -> 2)。

对于某些微服务场景,如果只需要唯一的网络身份标识而不需要严格的顺序创建,可以通过设置 podManagementPolicy: "Parallel" 开启并行管理策略,加速 Pod 的并发创建与销毁:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  podManagementPolicy: "Parallel"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: gcr.azk8s.cn/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

三、业务运维与系统类容器部署策略

1. DaemonSet 部署节点守护进程

当需要为集群中的每一个工作节点部署系统级守护进程(如 CNI 网络插件 Calico、日志收集 Fluentd、Node Exporter 监控等)时,应采用 DaemonSet 对象。

查看 DaemonSet 的更新策略:

$ kubectl get ds/<daemonset-name> -o go-template='{{.spec.updateStrategy.type}}{{"
"}}'
RollingUpdate

更新镜像:

kubectl set image ds/<daemonset-name> <container-name>=<container-new-image>

查看更新进度:

$ kubectl rollout status ds/<daemonset-name>
daemonset "<daemonset-name>" successfully rolled out

2. CronJob 定时任务

对于需要定期执行的脚本或批处理任务,可以使用 CronJob 对象进行管理:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  successfulJobsHistoryLimit: 0
  failedJobsHistoryLimit: 0
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

总结

本文系统性梳理了 Kubernetes 中核心的生产级工作负载编排对象:

  • Deployment:适合无状态常规微服务,结合亲和性约束与零中断更新策略;
  • StatefulSet:适合需要稳定网络标识和顺序持久化的有状态数据服务;
  • DaemonSet:适合节点级守护进程;
  • CronJob:适合周期性批处理任务。

在生产实践中,应依据业务的具体运行模式合理选择与组合编排对象。对于更加复杂的定制化运维逻辑,还可以结合 CRD 与 Operator 编程框架自定义控制面逻辑。