MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

在 Kubernetes 上部署无状态应用的最佳实践

2022-10-014.6k 阅读

一、理解无状态应用

在深入探讨在 Kubernetes 上部署无状态应用的最佳实践之前,我们首先需要明确什么是无状态应用。

无状态应用是指那些不依赖于特定状态或持久化数据的应用程序。它们在每次运行时,都可以基于相同的初始条件进行启动和操作,不会因为之前的运行状态而影响当前的运行逻辑。例如,常见的 Web 服务器(如 Nginx、Apache)、RESTful API 服务器等都属于无状态应用。

与有状态应用不同,无状态应用不需要维护跨会话的状态信息。比如,一个处理 HTTP 请求的 API 服务器,每个请求的处理过程都是独立的,不依赖于之前请求的处理结果(除非特意在请求间传递数据,但这并非应用自身的固有状态)。这种特性使得无状态应用在分布式环境中更容易进行水平扩展,因为每个实例都可以独立地处理请求,而不需要额外处理状态同步等复杂问题。

二、Kubernetes 基础概念与无状态应用部署关联

  1. Pods Pods 是 Kubernetes 中最小的可部署和可管理的计算单元。一个 Pod 可以包含一个或多个紧密相关的容器,这些容器共享网络命名空间和存储卷。在部署无状态应用时,通常会将应用的单个实例包装在一个 Pod 中。例如,我们要部署一个简单的 Node.js Web 应用,就可以将该 Node.js 应用及其运行时环境打包成一个容器,然后放入一个 Pod 中。

以下是一个简单的 Pod 定义示例(YAML 格式):

apiVersion: v1
kind: Pod
metadata:
  name: my-nodejs-app
spec:
  containers:
  - name: nodejs-container
    image: my-nodejs-app-image:latest
    ports:
    - containerPort: 3000

在这个示例中,my-nodejs-app 是 Pod 的名称,my-nodejs-app-image:latest 是包含 Node.js 应用的 Docker 镜像,containerPort: 3000 表示容器内应用监听的端口。

  1. Deployments Deployments 是 Kubernetes 中用于管理 Pod 生命周期的高层次资源。它提供了声明式的更新策略,允许我们轻松地创建、更新和删除 Pod 实例。通过 Deployment,我们可以定义期望的 Pod 副本数量,Kubernetes 会自动确保实际运行的 Pod 数量与期望数量一致。

例如,我们希望运行 3 个 my-nodejs-app Pod 副本,可以这样定义 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:latest
        ports:
        - containerPort: 3000

这里 replicas: 3 定义了期望的 Pod 副本数为 3,selector 用于匹配带有 app: my-nodejs-app 标签的 Pod,template 部分则定义了 Pod 的具体配置,与前面单个 Pod 的定义类似。

  1. Services Services 在 Kubernetes 中扮演着服务发现和负载均衡的角色。对于无状态应用,Service 可以将一组具有相同功能的 Pod 暴露给集群内部或外部的客户端。有多种类型的 Service,常见的如 ClusterIP、NodePort 和 LoadBalancer。
  • ClusterIP:这是默认的 Service 类型,它为 Service 分配一个集群内部的虚拟 IP 地址,只能在集群内部访问。例如,我们可以创建一个 ClusterIP Service 来暴露 my-nodejs-app Deployment 中的 Pod:
apiVersion: v1
kind: Service
metadata:
  name: my-nodejs-app-service
spec:
  selector:
    app: my-nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: ClusterIP

这里 selector 选择带有 app: my-nodejs-app 标签的 Pod,port: 80 是 Service 对外暴露的端口,targetPort: 3000 是 Pod 内容器实际监听的端口。

  • NodePort:这种类型的 Service 在每个 Node 上开放一个指定的端口,通过 <NodeIP>:<NodePort> 可以从集群外部访问 Service。
apiVersion: v1
kind: Service
metadata:
  name: my-nodejs-app-service
spec:
  selector:
    app: my-nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
    nodePort: 30080
  type: NodePort

nodePort: 30080 定义了在每个 Node 上开放的端口号。

  • LoadBalancer:如果集群运行在支持负载均衡器的云提供商(如 AWS、GCP 等)上,LoadBalancer 类型的 Service 会为应用分配一个外部负载均衡器,使其可以从互联网上访问。

三、构建无状态应用容器镜像

  1. 选择基础镜像 选择合适的基础镜像是构建无状态应用容器镜像的第一步。基础镜像应该尽量轻量,同时包含应用运行所需的基本依赖。例如,对于基于 Node.js 的无状态应用,可以选择官方的 node:alpine 镜像作为基础镜像。alpine 是一个轻量级的 Linux 发行版,占用空间小,启动速度快。

  2. 编写 Dockerfile 以 Node.js 应用为例,假设我们的应用代码位于 app 目录下,并且依赖 package.json 文件进行安装。以下是一个简单的 Dockerfile:

FROM node:alpine

WORKDIR /app

COPY package.json.
RUN npm install

COPY.

EXPOSE 3000

CMD ["node", "app.js"]

FROM node:alpine 指定基础镜像为 node:alpineWORKDIR /app 设置工作目录为 /appCOPY package.json. 将本地的 package.json 文件复制到容器内的工作目录,并通过 RUN npm install 安装应用依赖。COPY. 将整个应用代码复制到容器内。EXPOSE 3000 声明容器内应用监听的端口为 3000。CMD ["node", "app.js"] 定义容器启动时执行的命令,即运行 app.js 文件。

  1. 构建和推送镜像 在包含 Dockerfile 的目录下,使用以下命令构建镜像:
docker build -t my-nodejs-app-image:latest.

-t 选项指定镜像的标签,这里为 my-nodejs-app-image:latest。最后的 . 表示构建上下文为当前目录。

构建完成后,如果需要将镜像推送到镜像仓库(如 Docker Hub、私有镜像仓库等),可以使用以下命令(假设已经登录到镜像仓库):

docker push my-nodejs-app-image:latest

四、在 Kubernetes 上部署无状态应用的步骤

  1. 创建 Namespace(可选但推荐) Namespace 可以将 Kubernetes 集群划分为多个虚拟集群,每个 Namespace 内的资源名称是唯一的,有助于资源的隔离和管理。例如,我们可以为无状态应用创建一个专门的 Namespace:
apiVersion: v1
kind: Namespace
metadata:
  name: stateless-apps

使用 kubectl apply -f namespace.yaml 命令创建该 Namespace。

  1. 部署 Deployment 如前面提到的,通过编写 Deployment YAML 文件来定义无状态应用的部署。假设我们的 my-nodejs-app-deployment.yaml 文件内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
  namespace: stateless-apps
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:latest
        ports:
        - containerPort: 3000

使用 kubectl apply -f my-nodejs-app-deployment.yaml 命令在 stateless-apps Namespace 中创建该 Deployment。Kubernetes 会根据定义启动 3 个 my-nodejs-app Pod 副本。

  1. 创建 Service 根据应用的访问需求,创建相应类型的 Service。如果希望在集群内部访问,可以创建 ClusterIP Service。假设 my-nodejs-app-service.yaml 文件如下:
apiVersion: v1
kind: Service
metadata:
  name: my-nodejs-app-service
  namespace: stateless-apps
spec:
  selector:
    app: my-nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: ClusterIP

使用 kubectl apply -f my-nodejs-app-service.yaml 命令创建 Service。这样,集群内其他 Pod 就可以通过 my-nodejs-app-service:80 访问到 my-nodejs-app 应用。

如果需要从集群外部访问,可以创建 NodePort 或 LoadBalancer Service。例如,创建 NodePort Service 的 YAML 文件如下:

apiVersion: v1
kind: Service
metadata:
  name: my-nodejs-app-service
  namespace: stateless-apps
spec:
  selector:
    app: my-nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
    nodePort: 30080
  type: NodePort

创建后,外部可以通过 <NodeIP>:30080 访问应用。

五、无状态应用的扩展与伸缩

  1. 水平扩展 无状态应用的一个重要优势就是易于水平扩展。在 Kubernetes 中,通过修改 Deployment 的 replicas 字段可以轻松实现水平扩展。例如,将 my-nodejs-app-deployment 的副本数从 3 增加到 5,可以使用以下命令:
kubectl scale deployment my-nodejs-app-deployment --replicas=5 -n stateless-apps

或者直接编辑 Deployment 的 YAML 文件,修改 replicas 字段后重新应用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
  namespace: stateless-apps
spec:
  replicas: 5
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:latest
        ports:
        - containerPort: 3000

然后执行 kubectl apply -f my-nodejs-app-deployment.yaml。Kubernetes 会自动创建额外的 Pod 副本,并且 Service 会自动将流量均衡到新的副本上。

  1. 自动伸缩 Kubernetes 提供了 Horizontal Pod Autoscaler(HPA)来实现自动伸缩。HPA 可以根据 CPU 使用率或其他自定义指标自动调整 Deployment 的副本数量。

首先,确保集群中的 Metrics Server 已经安装并运行,它用于提供 Pod 和 Node 的资源使用指标。

然后,创建一个 HPA 配置文件,例如 my-nodejs-app-hpa.yaml

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: my-nodejs-app-hpa
  namespace: stateless-apps
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-nodejs-app-deployment
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

scaleTargetRef 指向要进行自动伸缩的 Deployment,minReplicasmaxReplicas 分别定义了最小和最大副本数,metrics 部分指定了根据 CPU 使用率进行伸缩,当平均 CPU 使用率达到 50% 时,HPA 会自动调整副本数量。

使用 kubectl apply -f my-nodejs-app-hpa.yaml 命令创建 HPA。随着应用负载的变化,Kubernetes 会自动调整 my-nodejs-app 的 Pod 副本数量,以保持平均 CPU 使用率接近 50%。

六、无状态应用的更新策略

  1. 滚动更新 滚动更新是 Kubernetes Deployment 的默认更新策略。在更新应用时,它会逐步替换旧的 Pod 为新的 Pod,确保服务的连续性。例如,当我们要更新 my-nodejs-app 的镜像版本时,只需要修改 Deployment 的镜像标签,然后重新应用 YAML 文件:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
  namespace: stateless-apps
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:new-version
        ports:
        - containerPort: 3000

执行 kubectl apply -f my-nodejs-app-deployment.yaml 后,Kubernetes 会先停止一个旧的 Pod,然后启动一个新的 Pod,依次类推,直到所有 Pod 都更新为新的版本。在更新过程中,Service 会持续将流量导向可用的 Pod,保证应用的正常运行。

  1. 回滚 如果在更新过程中发现问题,Kubernetes 提供了简单的回滚机制。可以使用以下命令回滚到上一个版本:
kubectl rollout undo deployment my-nodejs-app-deployment -n stateless-apps

Kubernetes 会撤销最近的一次更新,将 Deployment 恢复到上一个稳定的版本。也可以通过指定版本号来回滚到特定的历史版本,例如:

kubectl rollout undo deployment my-nodejs-app-deployment --to-revision=2 -n stateless-apps

这里 --to-revision=2 表示回滚到版本号为 2 的历史版本。

七、存储管理

虽然无状态应用本身不依赖持久化状态,但在某些情况下可能需要访问共享存储,例如读取配置文件或存储临时数据。

  1. ConfigMaps ConfigMaps 用于存储应用的配置信息,如环境变量、配置文件等。可以将配置信息与应用镜像分离,方便在不同环境中进行配置调整。

例如,创建一个 ConfigMap 来存储 my-nodejs-app 的数据库连接字符串:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-nodejs-app-config
  namespace: stateless-apps
data:
  db_connection_string: mongodb://localhost:27017/mydb

在 Deployment 中,可以通过环境变量或挂载卷的方式使用 ConfigMap 中的配置信息:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
  namespace: stateless-apps
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:latest
        ports:
        - containerPort: 3000
        env:
        - name: DB_CONNECTION_STRING
          valueFrom:
            configMapKeyRef:
              name: my-nodejs-app-config
              key: db_connection_string

这里通过 env 部分将 ConfigMap 中的 db_connection_string 作为环境变量注入到容器中。

  1. EmptyDir 卷 EmptyDir 卷是一种临时存储卷,它在 Pod 生命周期内存在,用于在容器之间共享临时数据。例如,my-nodejs-app 可能需要在不同容器之间共享一些临时生成的文件:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
  namespace: stateless-apps
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:latest
        ports:
        - containerPort: 3000
        volumeMounts:
        - name: shared-data
          mountPath: /app/shared
      volumes:
      - name: shared-data
        emptyDir: {}

这里定义了一个 emptyDir 卷,并将其挂载到容器内的 /app/shared 目录,多个容器可以通过这个目录共享数据。

八、监控与日志管理

  1. 监控 为了确保无状态应用在 Kubernetes 上的健康运行,监控是必不可少的。可以使用 Prometheus 和 Grafana 来搭建监控系统。

首先,部署 Prometheus Operator,它可以简化 Prometheus 和相关组件的部署和管理。然后创建一个 ServiceMonitor 资源来定义如何采集 my-nodejs-app 的监控指标。例如:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-nodejs-app-monitor
  namespace: stateless-apps
spec:
  selector:
    matchLabels:
      app: my-nodejs-app
  endpoints:
  - port: http-metrics
    interval: 30s

这里假设 my-nodejs-app 容器内暴露了一个 /metrics 端点用于提供监控指标,通过 ServiceMonitor 定义每 30 秒采集一次指标。Prometheus 会根据 ServiceMonitor 的定义采集指标,并存储在其时间序列数据库中。

  1. 日志管理 对于无状态应用的日志,可以使用 Fluentd 或 Filebeat 等工具将容器内的日志收集起来,并发送到集中式日志管理系统,如 Elasticsearch 和 Kibana(ELK 栈)。

以 Fluentd 为例,首先部署 Fluentd 作为 DaemonSet,使其在每个 Node 上运行。Fluentd 会自动收集容器的标准输出和标准错误日志。然后配置 Fluentd 将日志发送到 Elasticsearch:

<source>
  @type tail
  path /var/log/containers/*.log
  pos_file /var/log/es-containers.log.pos
  tag kubernetes.*
  <parse>
    @type json
  </parse>
</source>

<match kubernetes.**>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
  logstash_prefix fluentd
  logstash_dateformat %Y%m%d
  type_name _doc
  include_tag_key true
  tag_key @log_name
  <buffer>
    @type file
    path /var/log/fluentd-buffers/kubernetes.system.buffer
    flush_mode interval
    retry_type exponential_backoff
    flush_thread_count 2
    flush_interval 5s
    retry_forever
    retry_max_interval 30
  </buffer>
</match>

通过这种方式,my-nodejs-app 的日志会被收集并发送到 Elasticsearch,然后可以在 Kibana 中进行查询、可视化和分析。

九、安全最佳实践

  1. 容器安全
  • 最小化镜像:如前面构建镜像时提到的,选择轻量的基础镜像,并尽量减少镜像中的不必要组件。避免在镜像中包含敏感信息,如密码、密钥等。
  • 定期更新镜像:及时更新基础镜像和应用依赖,以修复已知的安全漏洞。
  • 运行时安全:使用 seccomp 配置文件来限制容器内进程的系统调用,提高容器的安全性。例如,可以定义一个 seccomp.json 文件:
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64"
  ],
  "syscalls": [
    {
      "name": "accept",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "bind",
      "action": "SCMP_ACT_ALLOW"
    },
    // 其他允许的系统调用
  ]
}

在 Deployment 中应用这个 seccomp 配置文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nodejs-app-deployment
  namespace: stateless-apps
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nodejs-app
  template:
    metadata:
      labels:
        app: my-nodejs-app
    spec:
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: /path/to/seccomp.json
      containers:
      - name: nodejs-container
        image: my-nodejs-app-image:latest
        ports:
        - containerPort: 3000
  1. Kubernetes 集群安全
  • RBAC(Role - Based Access Control):使用 RBAC 来管理用户和服务账户对 Kubernetes 资源的访问权限。例如,为 my-nodejs-app 创建一个专门的 ServiceAccount,并为其分配有限的权限:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-nodejs-app-sa
  namespace: stateless-apps

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: my-nodejs-app-role
  namespace: stateless-apps
rules:
- apiGroups: [""]
  resources: ["pods", "services"]
  verbs: ["get", "list", "watch"]

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-nodejs-app-rolebinding
  namespace: stateless-apps
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: my-nodejs-app-role
subjects:
- kind: ServiceAccount
  name: my-nodejs-app-sa
  namespace: stateless-apps

这样,my-nodejs-app-sa 只能对 stateless-apps Namespace 内的 podsservices 资源进行 getlistwatch 操作。

  • 网络策略:使用网络策略来限制 Pod 之间的网络访问。例如,只允许特定的 Pod 访问 my-nodejs-app Service:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-nodejs-app-network-policy
  namespace: stateless-apps
spec:
  podSelector:
    matchLabels:
      app: my-nodejs-app
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: api - gateway
    ports:
    - protocol: TCP
      port: 80

这里定义了只有带有 role: api - gateway 标签的 Pod 可以通过 TCP 端口 80 访问 my-nodejs-app Pod。

通过以上全面的最佳实践,我们可以在 Kubernetes 上高效、安全地部署和管理无状态应用,充分发挥 Kubernetes 在容器编排方面的强大功能,满足现代应用的高可用性、可扩展性和安全性需求。