實現微服務預熱呼叫之後再開始服務(下)

乾貨滿滿張雜湊發表於2022-01-02

繼續分析其他接入點。

其他需要初始化的接入點分析

我們有時候還需要做一些自定義的初始化操作,但是如何在註冊到註冊中心狀態為 UP 也就是開始處理請求之前做這些操作呢?

為了更加與雲環境相容,Spring Boot 從 2.3.0 版本之後引入了一些雲上部署相關的概念:

  • LivenessState(存活狀態):就應用程式而言,存活狀態是指應用程式的狀態是否正常。如果存活狀態不正常,則意味著應用程式本身已損壞,無法恢復。在 k8s 中,如果存活檢測失敗,則 kubelet 將殺死 Container,並且根據其重新啟動策略進行重啟:
    • 在 spring boot 中對應的介面是 /actuator/health/liveness
    • 對應的列舉類是 org.springframework.boot.availability.LivenessState,包括下面兩個狀態:
      • CORRECT:存活狀態正常
      • BROKEN:存活狀態不正常
  • Readiness(就緒狀態):指的是應用程式是否已準備好接受並處理客戶端請求。出於任何原因,如果應用程式尚未準備好處理服務請求,則應將其宣告為繁忙,直到能夠正常響應請求為止。如果 Readiness 狀態尚未就緒,則不應將流量路由到該例項。在 k8s 中,如果就緒檢測失敗,則 Endpoints 控制器將從 Endpoints 中刪除這個 Pod 的 IP 地址,如果你沒有使用 k8s 的服務發現的話,就不用太關心這個:
    • 在 spring boot 中對應的介面是 /actuator/health/readiness
    • 對應的列舉類是 org.springframework.boot.availability.ReadinessState,包括下面兩個狀態:
      • ACCEPTING_TRAFFIC:準備好接受請求
      • REFUSING_TRAFFIC:目前不能接受請求了

預設情況下,Spring Boot 在初始化過程中會修改這些狀態,對應原始碼(我們只關心 listeners 相關,這些標誌著 Spring Boot 生命週期變化):

SpringApplication.java


public ConfigurableApplicationContext run(String... args) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	DefaultBootstrapContext bootstrapContext = createBootstrapContext();
	ConfigurableApplicationContext context = null;
	configureHeadlessProperty();
	SpringApplicationRunListeners listeners = getRunListeners(args);
	//告訴所有 Listener 啟動中
	listeners.starting(bootstrapContext, this.mainApplicationClass);
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
		configureIgnoreBeanInfo(environment);
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		context.setApplicationStartup(this.applicationStartup);
		prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		//告訴所有 Listener 啟動完成
		listeners.started(context);
		//呼叫各種 SpringRunners + CommandRunners
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, listeners);
		throw new IllegalStateException(ex);
	}

	try {
	    //通知所有 Listener 執行中
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

其中,listeners.startedlisteners.running 裡面做的事情是:

@Override
public void started(ConfigurableApplicationContext context) {
    //釋出 ApplicationStartedEvent 事件
	context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
	//設定 LivenessState 為 CORRECT
	AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}

@Override
public void running(ConfigurableApplicationContext context) {
    //釋出 ApplicationReadyEvent 事件
	context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
	//設定 ReadinessState 為 ACCEPTING_TRAFFIC
	AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}

由於 ApplicationContext 釋出事件與訂閱該事件的處理是同步進行的,所以如果我們想在 LivenessState 為 CORRECT 之前做點操作,可以監聽 ApplicationStartedEvent 事件。同理,想在 ReadinessState 為 ACCEPTING_TRAFFIC 之前做點操作就監聽 ApplicationStartedEvent 事件。如何將 LivenessState 還有 ReadinessState 與註冊例項到註冊中心的狀態聯絡起來呢

我們用的註冊中心是 Eureka,註冊中心的例項是有狀態的。我們的 Eureka Client 的配置是:

eureka:
  client:
    # eureka client 重新整理本地快取時間
    # 預設30s
    # 對於普通屬性,用駝峰或者橫槓名稱配置都可以,這裡用的駝峰名稱,下面配置用的橫槓名稱
    registryFetchIntervalSeconds: 5
    healthcheck:
      # 啟用健康檢查
      enabled: true
    # 定時檢查例項資訊以及更新本地例項狀態的任務的間隔
    instance-info-replication-interval-seconds: 10
    # 初始定時檢查例項資訊以及更新本地例項狀態的任務延遲
    initial-instance-info-replication-interval-seconds: 5

我們啟用了 Eureka 的健康檢查,其實就是通過呼叫本地的 /actuator/health 介面的相同服務進行健康檢查。這個健康檢查,會在定時檢查例項資訊以及更新本地例項狀態的任務中呼叫。這個任務的初始延遲我們設定為了 10s,之後檢查間隔設定為了 5s。健康檢查包括存活狀態檢查還有就緒狀態檢查,存活狀態為 CORRECT 的時候 Status 才為 UP,就緒狀態為 ACCEPTING_TRAFFIC 的時候 status 才為 UP。對應的 HealthIndicator 是:

public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {

	public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
		super(availability, ReadinessState.class, (statusMappings) -> {
		    //存活狀態為 CORRECT 的時候 Status 才為 UP
			statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
			statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
		});
	}
}
public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {

	public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
		super(availability, ReadinessState.class, (statusMappings) -> {
		    //就緒狀態為 ACCEPTING_TRAFFIC 的時候 status 才為 UP
			statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
			statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
		});
	}
}

定時檢查例項資訊以及例項狀態並同步到 Eureka Server 的流程如下

image

我們可以使用這個機制,讓初始註冊到 Eureka 的狀態不為 UP,等待存活狀態為 CORRECT 的時候並且就緒狀態為 ACCEPTING_TRAFFIC 的時候,才會通過上面的定時檢查任務將例項狀態設定為 UP 同步到 Eureka Server。

可以加上配置,指定初始註冊狀態:

eureka:
  instance:
    # 初始例項狀態
    initial-status: starting

這樣我們可以監聽 ApplicationStartedEvent 事件實現微服務初始化操作,操作完成後才開始服務。同時還要考慮只執行一次的問題,因為你的 ApplicationContext 不止一個,例如 Spring Cloud 啟用 BootStrap Context 之後,就多了一個 BootStrap Context,我們要保證只執行一次的話,可以像下面這麼寫程式碼,繼承下面這個抽象類集合:

import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;

import java.util.concurrent.atomic.AtomicBoolean;

import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME;


public abstract class AbstractMicronServiceInitializer implements ApplicationListener<ApplicationStartedEvent> {
    private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        if (isBootstrapContext(event)) {
            return;
        }
        //由於spring-cloud的org.springframework.cloud.context.restart.RestartListener導致同一個context觸發多次
        //我個人感覺 org.springframework.cloud.context.restart.RestartListener 這個在spring-boot2.0.0之後的spring-cloud版本是沒有必要存在的
        //但是官方並沒有正面回應,以防之後官方還拿這個做點事情,這裡我們做個適配,參考我問的這個issue:https://github.com/spring-cloud/spring-cloud-commons/issues/693
        synchronized (INITIALIZED) {
            if (INITIALIZED.get()) {
                return;
            }
            //每個spring-cloud應用只能初始化一次
            init();
            INITIALIZED.set(true);
        }
    }

    protected abstract void init();

    static boolean isBootstrapContext(ApplicationStartedEvent applicationEvent) {
        return applicationEvent.getApplicationContext().getEnvironment().getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME);
    }
}

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章