微服务无感发布(二):微服务优雅的启动

haohaoxiangshou / 2024-10-12 / 原文

上一篇文章当中我们介绍了微服务在k8s环境下无感发布存在的问题和解决思路,今天来看一看代码的实现方式。

预热以及缓存加载

服务在容器启动之后,可能需要加载一些必要缓存,在这些缓存加载之后,才能够提供对外服务,注册到注册中心。在spring-cloud下,服务注册是通过监听WebServerInitializedEvent事件来触发的,所以只需要保证在该事件之前加载完缓存以及预热完成即可。

预热逻辑接口

首先,我们需要一个接口来定义预热方法:

/**
 * 预热 逻辑接口
 */
public interface BaseWarmupHandler {

    /**
     * 预热方法
     */
    void warmup();
}
http预热

接下来,我们实现一个预热HTTP请求的方法

@RestController
@RequestMapping
public class WarmupController {

    /**
     * 系统预热的接口
     * 参考项目: <link url="https://github.com/steinsag/warm-me-up"/>
     * @param request 请求参数
     * @return  返回
     */
    @PostMapping("/warmup")
    public Response<String> post(@RequestBody @Valid WarmupRequest request) {
        return Response.success(request.toString());
    }
}

在这个控制器中,我们定义了一个/warmup的POST接口,用于触发web容器相关预热逻辑。

预热逻辑实现
@Slf4j
@Service
public class ServletWarmupHandler implements BaseWarmupHandler{

    @Resource
    private Environment environment;

    @Resource
    private OkHttpClient okHttpClient;
    @Override
    public void warmup() {
        final String serverPort = environment.getProperty("local.server.port");
        final String baseUrl = "http://localhost:" + serverPort;
        final String warmUpEndpoint = baseUrl + "/warmup";

        log.debug("Sending REST request to warm up...");

        // 创建 MediaType
        MediaType mediaType = MediaType.get("application/json; charset=utf-8");
        // 创建 RequestBody
        RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(createSampleMessage()));
        Request request = new Request.Builder()
                .url(warmUpEndpoint)
                .post(body)
                .build();

        try (Response response = okHttpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                log.error("warm up request error:{}", response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private WarmupRequest createSampleMessage() {
        final WarmupRequest warmUpRequestDto = new WarmupRequest();
        warmUpRequestDto.setInputMessage("warm me up");
        warmUpRequestDto.setPatternString("paretern");
        warmUpRequestDto.setSomeNumber(BigDecimal.TEN);
        warmUpRequestDto.setEm(WarmupEnum.ME);

        return warmUpRequestDto;
    }
}
启动时自动预热

最后,我们需要在服务启动时自动触发预热逻辑。这可以通过监听ApplicationReadyEvent事件来实现:

@Service
public class WarmupService implements ApplicationListener<ApplicationReadyEvent> {

    @Resource
    private ApplicationContext applicationContext;

    @Resource
    private ApplicationEventPublisher eventPublisher;

    @Override
    public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
        log.debug("warm up start .........");

        Map<String, BaseWarmupHandler> warmupHandlerMap = applicationContext.getBeansOfType(BaseWarmupHandler.class);
        for (Map.Entry<String, BaseWarmupHandler> entry : warmupHandlerMap.entrySet()) {
            try {
                entry.getValue().warmup();
            } catch (Exception e) {
                log.warn("warm up error, handler:{}, error:", entry.getKey(), e);
            }
        }

        log.debug("warm up done!");
        eventPublisher.publishEvent(new WarmupEndEvent(this));
    }
}

这个事例仅仅提供了一个预热web容器http请求的思路,在实际项目中要根据自身需求编写@Valid,jdbc或者缓存相关的预加载代码。

微服务就绪接口与Kubernetes就绪探针配置

在微服务架构中,每个服务的就绪时机可能各不相同,有的可能需要等待缓存加载完成,有的则需要确保已在注册中心成功注册。为了准确反映服务的就绪状态,项目应提供一个专门的就绪接口。以下是一个简单的Java接口示例:

@RestController("/lifeCycle")
public class StartedController implements ApplicationListener<InstanceRegisteredEvent<?>> {
    
    /** 项目是否启动完成 */
    private boolean started = false;
    
    @GetMapping("/started")
    public boolean started() {
        if (started) {
            return true;
        }
        // 这里根据项目定义的异常类型返回
        throw new RuntimeException("服务尚未启动");
    }

    @Override
    public void onApplicationEvent(InstanceRegisteredEvent<?> event) {
        // InstanceRegisteredEvent 是spring-cloud 提供的注册中心模版完成注册时发送的事件。
        // 这个里可以根据项目调整项目成功的时机
        this.started = true;
    }
}

在Kubernetes环境中,就绪探针(Readiness Probe)用于判断服务是否已经准备好接收流量。以下是一个Kubernetes容器配置就绪探针的示例:

readinessProbe:
  httpGet:
      # host:连接使用的主机名,默认是 Pod 的 IP。也可以在 HTTP 头中设置 "Host" 来代替。
      # scheme:用于设置连接主机的方式(HTTP 还是 HTTPS)。默认是 "HTTP"。
      # path:访问 HTTP 服务的路径。默认值为 "/"。
      path: /lifeCycle/started
      # httpHeaders:请求中自定义的 HTTP 头。HTTP 头字段允许重复。
      httpHeaders:
        - name: Accept
          value: application/json
      # port:访问容器的端口号或者端口名。如果数字必须在 1~65535 之间。
      port: 10087
  initialDelaySeconds: 5
  # 执行探测的时间间隔(单位是秒)。默认是 10 秒。最小值是 1。 当容器未就绪时,ReadinessProbe 可能会在除配置的
  periodSeconds: 5

提示:就绪探针通过HTTP请求访问指定的路径和端口,如果接口返回的状态码大于或等于200且小于400,则表示探测成功,服务已就绪;否则,表示探测失败,服务尚未就绪。

引用

shelltea.warmup-spring-boot-starter
kubernetes文档