Kubernetes 开发随笔

常用命令

创建命令

  • 创建 Pod
1
# kubectl run <pod-name> --image=<image-name>
  • 创建 Deployment
1
# kubectl create deployment <deployment-name> --image=<image-name>
  • 创建 Service(暴露服务)
1
2
# 为指定的Deployment暴露服务
# kubectl expose deployment <deployment-name> --port=80 --type=NodePort --target-port=80

查看命令

  • 查看所有 Service
1
# kubectl get svc
  • 查看所有 Deployment
1
# kubectl get deployments
  • 查看所有 Secret
1
# kubectl get secrets
  • 查看所有 ConfigMap
1
# kubectl get configmaps

  • 查看所有 Pode 的运行状态
1
2
# 查看所有Pode的运行状态
# kubectl get pods -o wide
  • 查看指定 Pod 的日志信息
1
2
# 查看指定Pod的日志信息
# kubectl logs <pod-name>
  • 查看指定 Pod 的状态、事件、镜像拉取、容器日志、调度信息等详细运行情况
1
2
# 查看指定Pod的详细运行情况
# kubectl describe pod <pod-name>

连接命令

  • 连接进入指定 Pod 内部的容器
1
2
# 连接进入指定 Pod 内部的容器
# kubectl exec -it <pod-name> -- /bin/bash
1
2
# 如果Pod内部有多个容器(一个Pod可以有多个容器),可以使用 -c 参数指定具体的容器名称,容器名称可以使用 kubectl describe pod <pod-name> 命令获取得到
# kubectl exec -it <pod-name> -c <container-name> -- /bin/bash

Pod 的管理

使用命令创建 Pod

  • 在 Kubernetes 集群的 Master 节点中执行以下命令,通过 Deployment 创建一个 Nginx 的 Pod,并使用 Service 对外暴露服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建Nginx
# kubectl create deployment nginx --image=nginx

# 暴露Nginx的端口
# kubectl expose deployment nginx --port=80 --type=NodePort --target-port=80

# 查看Service列表
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 6h42m
nginx NodePort 10.0.0.38 <none> 80:31603/TCP 64s

# 查看所有Pode的运行状态
# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-6799fc88d8-w8g9b 1/1 Running 0 64s 10.244.2.2 k8s-node3 <none> <none>
  • 在 Kubernetes 集群的外部,通过浏览器访问 http://<node-ip>:31603,其中 IP 可以是任意集群节点的 IP 地址(比如 192.168.1.5),端口号可以通过 kubectl get svc 命令获取得到。若 Ngninx 容器在 Kubernetes 集群中创建并启动成功,则浏览器可以正常访问 Nginx 的首页(如下图所示)。

  • 若希望删除刚才在 Kubernetes 集群中创建的 Pod,可以在 Master 节点上执行以下命令:
1
2
3
4
5
# 删除Service
# kubectl delete service nginx

# 删除Deployment
# kubectl delete deployment nginx

特别注意

  • 在 Kubernetes 集群中,除了可以使用上述的方式通过 Deployment 来间接创建 Pod 外,还可以使用命令直接创建 Pod,比如 kubectl run mypod --image=nginx --restart=Never,删除 Pod 可以使用命令 kubectl delete pod mypod
  • 在较新的 Kubernetes 版本(v1.19 及以上)中,kubectl run 命令无论是否指定 --restart=Never,都会直接创建一个 Pod,而不会再创建 Deployment、ReplicaSet 或 Job 等控制器。
  • 在 Kubernetes 早期版本(v1.18 及之前)中,kubectl run 默认会创建 Deployment(即 --restart=Always 时),此行为在后续新版本中已被废弃。
  • 因此,在现代 Kubernetes 中,kubectl run 命令仅用于快速创建测试用的 Pod。

最佳实践

平滑重启 Java 应用

术语说明

  • Kubernetes 命令 kubectl rollout restart 会触发一次 Deployment 滚动更新(Rolling Update),从而达到滚动重启的效果。
  • 在社区里,也常将该命令称为 "滚动重启",这属于口语化叫法;因为它在效果上达到了不停服、一个一个平滑替换 Pod,看起来像 "重启",所以滚动重启是现象,滚动更新是机制。

Java 应用平滑重启的概念

  • 在 Kubernetes 中实现 Java 应用程序的平滑重启,本质上是通过 Deployment 滚动更新(Rolling Update)的方式实现,也就是逐个重建 Pod,而不是将全部 Pod 停掉再重建,这样可以保证服务在重启过程中尽量不丢失请求。
  • 所谓 Java 应用平滑重启(Graceful Restart)有两个关键点:
    • (1) 不中断现有请求:Java 应用在收到 SIGTERM 信号时,应该先停止接收新请求,但允许正在处理的请求完成,再退出。
    • (2) 保证服务可用性:Kubernetes 通过 Deployment 的滚动更新或 Pod 生命周期钩子来保证至少有一部分实例在运行,避免服务全部不可用。

Kubernetes 滚动更新的机制

  • Kubernetes 可以使用命令 kubectl rollout restart deployment <deployment-name> 实现 Deployment 滚动更新(Rolling Update)
  • Kubernetes 执行 Deployment 滚动更新(Rolling Update)后,实际上会:
    • 更新 Deployment 的一个字段(通常是 spec.template.metadata.annotations,加上一个时间戳);
    • 由于模板变更,Deployment Controller 认为需要 “滚动更新”;
    • 逐个创建新 Pod,删除旧 Pod,且遵守 spec.strategy.rollingUpdate 策略,比如:
      1
      2
      3
      4
      5
      6
      7
      apiVersion: apps/v1
      kind: Deployment
      strategy:
      type: RollingUpdate # 使用滚动更新方式(默认),逐个替换旧 Pod,保证服务不中断
      rollingUpdate:
      maxUnavailable: 0 # 滚动更新期间不可用的 Pod 数量最多允许 0 个,保证全部 Pod 始终保持可用
      maxSurge: 1 # 滚动更新期间最多只允许额外创建 1 个新 Pod,保证有新 Pod 启动后才关闭旧 Pod
      • maxSurge = 1 表示每次更新期间最多只允许额外创建一个新 Pod;
      • maxUnavailable = 0 表示保证全部 Pod 始终保持可用;
      • maxUnavailablemaxSurge 的默认值如下,如果希望 Deployment 滚动更新期间 Pod 保持 100% 可用,可以设置 maxUnavailable = 0maxSurge = 1
        字段默认值含义
        maxUnavailable25%更新时最多允许不可用的 Pod 数量(百分比或整数)。默认是 25%,即如果有 4 个副本,最多可以有 1 个 Pod 不可用。
        maxSurge25%更新时允许额外创建的 Pod 数量(百分比或整数)。默认是 25%,即如果有 4 个副本,最多可以额外启动 1 个新 Pod。

Java 应用平滑重启的实现

  • 在以下条件下(缺一不可),Kubernetes 可以做到几乎无感重启 Java 应用(不中断现有请求):
    • (1) Deployment 的滚动更新策略配置合理,比如:
      1
      2
      3
      4
      5
      6
      7
      apiVersion: apps/v1
      kind: Deployment
      strategy:
      type: RollingUpdate # 使用滚动更新方式(默认),逐个替换旧 Pod,保证服务不中断
      rollingUpdate:
      maxUnavailable: 0 # 更新期间不可用的 Pod 数量最多允许 0 个,确保 Pod 始终保持满负载可用
      maxSurge: 1 # 更新时额外创建 1 个新 Pod,保证有新 Pod 启动后才关闭旧 Pod
    • (2) Pod 的 readinessProbe 就绪探针配置正确,比如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      apiVersion: apps/v1
      kind: Deployment
      spec:
      template:
      spec:
      containers:
      - name: my-java-app
      image: myrepo/my-java-app:latest
      ports:
      - containerPort: 8080
      readinessProbe:
      httpGet:
      path: /actuator/health # 发送 HTTP 请求进行检测
      port: 8080
      initialDelaySeconds: 10 # 容器启动后等待 10 秒开始检查
      periodSeconds: 3 # 每 3 秒检查一次
      successThreshold: 1 # 成功 1 次即标记为就绪
      • 就绪探针未通过的 Pod 不会被分配到服务流量,保证请求不会被直接丢弃;
      • 只有当 Java 应用真正启动并能响应健康检查时,Kubernetes 才会把流量分配过去;
      • 这可以避免 Java 应用刚启动就到接收请求的问题。
    • (3) 通过 terminationGracePeriodSeconds 指定 Pod 在接收到 SIGTERM 信号后允许的优雅终止等待时间,比如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      apiVersion: apps/v1
      kind: Deployment
      spec:
      template:
      spec:
      terminationGracePeriodSeconds: 60 # Pod 优雅终止的最大等待时间(秒),K8s 会先发送 SIGTERM 信号,给容器最多 60 秒时间处理完当前所有请求并退出,超时后发送 SIGKILL 信号强制终止 Pod
      containers:
      - name: my-java-app
      image: myrepo/my-java-app:latest
      ports:
      - containerPort: 8080
      • 针对 Java 应用,Pod 的 terminationGracePeriodSeconds 通常设置为 30 ~ 60 秒,给 Java 应用足够时间处理完当前所有请求,再将其对应的容器杀掉;
      • 在此期间,容器可以自行处理收尾工作。如果超时仍未退出,Kubernetes 会发送 SIGKILL 信号强制终止 Pod。
    • (4) Java 应用支持优雅关机(Graceful Shutdown),比如:
      • 如果是 Java 普通应用,开发者可以自行监听 SIGTERM 信号,处理收尾工作
        1
        2
        3
        4
        5
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        System.out.println("Graceful shutdown initiated...");
        // 停止接收新请求
        // 等待正在处理请求完成
        }));
      • 如果是 SpringBoot(2.3 及以上版本)Web 应用,可以开启优雅停机,开发者通常无需自己监听 SIGTERM 信号
        1
        2
        3
        4
        5
        server:
        shutdown: graceful # 开启优雅停机(Graceful Shutdown),让应用在关闭前有时间处理完当前所有请求
        spring:
        lifecycle:
        timeout-per-shutdown-phase: 60s # 优雅停机的最长等待时间,超时后强制停止(默认 30 秒,可根据实际需要调整)

Java 应用平滑重启的注意事项

  • 纯 SpringBoot Web 应用:

    • 只需要配置 server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase,开启优雅关机;
    • SpringBoot 会自动在接收到 SIGTERM 时:
      • 停止接收新的 HTTP 请求(Tomcat / Netty 等 Web 服务器)。
      • 等待正在处理的请求完成,最长等待时间由 spring.lifecycle.timeout-per-shutdown-phase 决定。
      • 关闭 Spring 上下文及 Bean。
    • 整个流程已经自动实现了 “优雅停机”,开发者不需要额外写 Shutdown Hook。
  • 如果在 SpringBoot Web 应用内有额外的线程 / 资源:

    • 需要开发者自己实现资源关闭逻辑,可用 @PreDestroy 注解或 Shutdown Hook 实现。
  • 开发者什么时候需要自己监听 SIGTERM

    • 如果 Java 应用中有非 Spring 管理的异步线程或任务,比如:
      • 自己创建的 Thread(线程) 或 ExecutorService(线程池);
      • 消息队列消费者(Kafka / RabbitMQ);
      • 这类资源需要手动在 Shutdown Hook 或者 Spring Bean 的 @PreDestroy 中关闭,避免被强制 Kill 掉,比如:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        @PreDestroy
        public void cleanup() {
        if (threadPool != null && !threadPool.isShutdown()) {
        // 优雅关闭线程池
        threadPool.shutdown();
        try {
        // 阻塞当前线程 50 秒,让线程池中等待执行的任务和正在执行的任务执行完成
        if (!threadPool.awaitTermination(50, TimeUnit.SECONDS)) {
        // 等待超时后,立刻关闭线程池
        threadPool.shutdownNow();

        // 再次阻塞当前线程 5 秒,然后检查线程池是否已经关闭
        if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
        System.out.println("Thread pool did not terminate");
        }
        }
        } catch (Exception e) {
        // 捕获到异常后,立刻关闭线程池
        threadPool.shutdownNow();
        // 捕获到异常后,中断当前线程
        Thread.currentThread().interrupt();
        }
        }
        }