k8s优雅停机部署探讨

260 阅读2分钟
  1. k8s配置就绪探针,readinessProbe
      readinessProbe:
        httpGet:
           path: /actuator/health
           port: 3900
           scheme: HTTP
#             initialDelaySeconds: 20 ## 指定的这个秒以后才执行探测
        timeoutSeconds: 3
        periodSeconds: 5
        successThreshold: 1
        failureThreshold: 3

  1. k8s配置滚动更新策略、优雅关闭策略

      terminationGracePeriodSeconds: 90
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  minReadySeconds: 3
  revisionHistoryLimit: 10
  progressDeadlineSeconds: 600
  1. 开启健康检查、开启Nacos手动注销微服务端点开关
management:
  health:
    mail:
      enabled: false
    elasticsearch:
      enabled: false      
  endpoints:
    #    jmx:
    #      exposure:
    #        include: health,info
    web:
      exposure:
        include: "*"
        exclude: env,beans
  endpoint:
    serviceregistry:
      enabled: true
    health:
      enabled: true
      show-details: always
  1. k8s配置生命周期钩子
          lifecycle:
            preStop:
              httpGet:
                path: /actuator/xdownRegister
                port: 3900
                scheme: HTTP
#               exec:
#                 command: ["sleep", "60"]

/actuator/xdownRegister为自定义端点,配合Nacos内置端点serviceregistry使用

  1. 微服务通用项,配置自定义端点xdownRegister,主要包含三个文件:GracefulShutdownEndPoint、GracefulShutdownListener、GracefulShutdownProcessor。字如其名
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
@Endpoint(id = "xdownRegister")
@RequiredArgsConstructor
public class GracefulShutdownEndPoint {
    private final GracefulShutdownProcessor gracefulShutdownProcessor;

    /**
     * 访问路径:GET http://容器组IP:端口号/actuator/xdownRegister
     */
    @ReadOperation
    public Map<String, Object> getInfo() throws InterruptedException {
        Map<String, Object> info = new HashMap<>();

        gracefulShutdownProcessor.deregister(false);

        info.put("message", "service shutdown ok!");
        return info;
    }
}
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
    private final Environment env;
    private final GracefulShutdownProcessor gracefulShutdownProcessor;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        if (!"prod".equals(env.getActiveProfiles()[0])) {
            return;
        }

        try {
            gracefulShutdownProcessor.deregister(true);
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
        }
    }

}
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
@RequiredArgsConstructor
public class GracefulShutdownProcessor {

    private final RestTemplate restTemplate;
    public static final String INSTANCE_DOWN_X_URL = "http://%s:%s/actuator/serviceregistry?status=DOWN";
    @Value("${spring.application.name}")
    private String applicationName;

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

    public void deregister(boolean handleOthers) throws InterruptedException {
        InetAddress ipAddr = null;

        try {
            ipAddr = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            log.error(e.getMessage(), e);
        }

        if (Objects.isNull(ipAddr)) {
            return;
        }

        log.info(String.format("开始注销nacos实例 (%s - %s:%d)", applicationName, ipAddr.getHostAddress(), port));

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("Content-Type", "application/vnd.spring-boot.actuator.v2+json;charset=UTF-8");

        post(String.format(INSTANCE_DOWN_X_URL, ipAddr.getHostAddress(), port), new HashMap<>(), httpHeaders);

        log.info("注销nacos实例success.");

        if (handleOthers) {
            TimeUnit.SECONDS.sleep(45);
        }
    }

    private void post(String url, Object data, HttpHeaders requestHeaders) {
        HttpEntity<Object> requestEntity = new HttpEntity<>(data, requestHeaders);
        restTemplate.postForObject(url, requestEntity, String.class);
    }
}

总结

  • 整体实现的逻辑:
    • K8S Pod接收到关闭指令,进入生命周期钩子,立即调用/actuator/xdownRegister,将当前微服务从Nacos注册中心手动取消注册,防止再有其他流量打到当前服务;
    • 之后,系统监听ContextClosedEvent事件,进入优雅停机GracefulShutdownProcessor处理器,内部休眠45秒,使服务延迟关闭,可继续处理未完成的任务