✅ 目标:实现一个 /graceful-shutdown 接口

调用该接口后:

  1. 服务从 Nacos 主动注销
  2. 停止接收新请求(可选)
  3. 等待正在处理的请求完成
  4. 最终关闭应用(或让 K8s 处理终止)

✅ 一、接口设计思路

步骤 操作
1 提供 POST /graceful-shutdown 接口
2 接口内:从 Nacos 注销本实例
3 触发 Spring 优雅停机(或主动退出)
4 配合 preStop 或直接调用,实现平滑下线

✅ 二、Spring Boot 实现代码(Java)

1. 创建控制器:GracefulShutdownController.java

package com.example.demo.controller;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingFactory;
import com.alibaba.nacos.api.naming.NamingService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PreDestroy;
import java.net.InetAddress;
import java.net.UnknownHostException;

@RestController
public class GracefulShutdownController {

    @Value("${spring.application.name}")
    private String serviceName;

    @Value("${server.port}")
    private int port;

    @Value("${spring.cloud.nacos.discovery.server-addr}")
    private String nacosServerAddr;

    private volatile boolean isShuttingDown = false;

    @PostMapping("/graceful-shutdown")
    public String gracefulShutdown() {
        if (isShuttingDown) {
            return "Already shutting down";
        }
        isShuttingDown = true;

        new Thread(() -> {
            try {
                // 1. 获取本机 IP
                String ip = InetAddress.getLocalHost().getHostAddress();
                System.out.println("🛑 开始平滑下线: " + serviceName + " at " + ip + ":" + port);

                // 2. 从 Nacos 注销
                NamingService namingService = NamingFactory.createNamingService(nacosServerAddr);
                namingService.deregisterInstance(serviceName, ip, port);
                System.out.println("🗑️ 已从 Nacos 注销服务");

                // 3. 等待正在处理的请求完成(可配置)
                Thread.sleep(3000); // 等待 3s 传播 + 请求完成

                // 4. 触发应用关闭
                System.exit(0); // 或调用 /actuator/shutdown

            } catch (Exception e) {
                System.err.println("❌ 平滑下线失败: " + e.getMessage());
                e.printStackTrace();
                System.exit(1);
            }
        }).start();

        return "Graceful shutdown triggered";
    }
}

💡 你也可以不 System.exit(0),而是返回后由 K8s 的 preStop 或滚动更新来终止。


2. 启用 Actuator(可选,用于对比)

如果你更信任 /actuator/shutdown,也可以调用它:

management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: health,info,shutdown

然后接口里调用:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("🔥 应用正在关闭...");
}));
System.exit(0);

✅ 三、Deployment 配置(关键:暴露接口)

deployment.yaml(片段)

apiVersion: apps/v1
kind: Deployment
meta
  name: demo-service
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-service
  template:
    meta
      labels:
        app: demo-service
    spec:
      containers:
      - name: demo-service
        image: your-registry/demo-service:1.0.0
        ports:
        - containerPort: 8080
        # 健康检查
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
        # 关键:PreStop 调用平滑下线接口
        lifecycle:
          preStop:
            exec:
              command:
                - /bin/sh
                - -c
                - >
                  # 调用平滑下线接口
                  curl -X POST http://localhost:8080/graceful-shutdown ||
                  sleep 5
        resources:
          limits:
            memory: "1Gi"
            cpu: "500m"
      terminationGracePeriodSeconds: 30  # 给足时间注销
---
apiVersion: v1
kind: Service
meta
  name: demo-service
spec:
  selector:
    app: demo-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP

✅ 四、如何使用这个接口?

场景 1:K8s 滚动更新时自动触发(推荐)

通过 preStop 自动调用,无需人工干预。

场景 2:手动触发下线(运维操作)

# 获取 Pod IP
POD_IP=$(kubectl get pod demo-service-7d6f8c7b7b-abcde -o jsonpath='{.status.podIP}')

# 调用平滑下线接口
curl -X POST http://$POD_IP:8080/graceful-shutdown

⚠️ 注意:调用后 Pod 会退出,K8s 会创建新实例(如果是 Deployment)


场景 3:蓝绿/灰度发布时控制流量

你可以先调用 /graceful-shutdown 下线旧版本,再上线新版本。


✅ 五、安全加固(重要!)

⚠️ 不要让这个接口被随意调用!

方案 1:加 Token 验证

@PostMapping("/graceful-shutdown")
public String gracefulShutdown(@RequestHeader(value = "X-Auth-Token") String token) {
    if (!"your-secret-token".equals(token)) {
        return "Unauthorized";
    }
    // ... 执行下线
}

方案 2:只允许 localhost 调用

@PostMapping("/graceful-shutdown")
public String gracefulShutdown(HttpServletRequest request) {
    String remoteAddr = request.getRemoteAddr();
    if (!remoteAddr.equals("127.0.0.1") && !remoteAddr.equals("0:0:0:0:0:0:0:1")) {
        return "Forbidden";
    }
    // ... 执行下线
}

方案 3:NetworkPolicy 限制访问

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
meta
  name: allow-only-local
spec:
  podSelector:
    matchLabels:
      app: demo-service
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSecurityContext: {}  # 只允许本 Pod 自己调用(实际需更细粒度)

更合理的是:只允许 K8s 节点或特定服务调用


✅ 六、替代方案:不暴露接口,全靠 preStop

如果你不想暴露接口,可以直接在 preStop 中:

  1. curl 调用 Nacos REST API 注销
  2. 或执行 jcmd 发送 shutdown 命令
preStop:
  exec:
    command:
      - /bin/sh
      - -c
      - >
        IP=$(hostname -i) &&
        curl -X DELETE "http://nacos.middleware.svc.cluster.local:8848/nacos/v1/ns/instance?serviceName=demo-service&ip=$IP&port=8080" &&
        sleep 5

这样就不需要写 Java 接口了。


✅ 七、总结:两种方式对比

方式 优点 缺点 推荐场景
/graceful-shutdown 接口 灵活,可加逻辑、日志、监控 需开发,暴露接口有风险 需要复杂逻辑(如通知、审计)
preStop 直接调 REST API 简单,无需改代码 不够灵活,难调试 大多数场景推荐

✅ 最终建议

  • 大多数情况:使用 preStop + Nacos REST API,无需写接口
  • 需要复杂逻辑:写 /graceful-shutdown 接口,加 Token 验证
  • 必须配合terminationGracePeriodSecondsreadinessProbe

如果你需要我生成:

  • ✅ 完整的 GitHub 项目(含接口 + YAML)
  • ✅ 使用 JWT 鉴权的高级版本
  • ✅ 配合 Istio 实现流量零丢失的方案

欢迎继续提问!我可以为你定制企业级平滑发布方案。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐