SOFABoot 健康檢查能力分析

glmapper發表於2018-11-16

開源中國有個年度開源軟體的活動,裡面有兩個 SOFA 相關的專案(SOFABoot & SOFARPC),大家幫忙點兩下一起投個票:www.oschina.net/project/top…。同時也歡迎大家關注 SOFAStack

Liveness Check & Readiness Check

Spring Boot 提供了一個基礎的健康檢查的能力,中介軟體和應用都可以擴充套件來實現自己的健康檢查邏輯。但是 Spring Boot 的健康檢查只有 Liveness Check 的能力,缺少 Readiness Check 的能力,這樣會有比較致命的問題。當一個微服務應用啟動的時候,必須要先保證啟動後應用是健康的,才可以將上游的流量放進來(來自於 RPC,閘道器,定時任務等等流量),否則就可能會導致一定時間內大量的錯誤發生。

針對 Spring Boot 缺少 Readiness Check 能力的情況,SOFABoot 增加了 Spring Boot 現有的健康檢查的能力,提供了 Readiness Check 的能力。利用 Readiness Check 的能力,SOFA 中介軟體中的各個元件只有在 Readiness Check 通過之後,才將流量引入到應用的例項中,比如 RPC,只有在 Readiness Check 通過之後,才會向服務註冊中心註冊,後面來自上游應用的流量才會進入。

除了中介軟體可以利用 Readiness Check 的事件來控制流量的進入之外,PAAS 系統也可以通過訪問 http://localhost:8080/actuator/readiness 來獲取應用的 Readiness Check 的狀況,用來控制例如負載均衡裝置等等流量的進入。

使用方式

SOFABoot 的健康檢查能力需要引入:

<dependency>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>healthcheck-sofa-boot-starter</artifactId>
</dependency>
複製程式碼

區別於SpringBoot的:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
複製程式碼

詳細工程科參考:sofa-boot

健康檢查啟動日誌

SOFABoot 健康檢查能力分析

程式碼分析

既然是個Starter,那麼就先從 spring.factories 檔案來看:

org.springframework.context.ApplicationContextInitializer=\
com.alipay.sofa.healthcheck.initializer.SofaBootHealthCheckInitializer

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alipay.sofa.healthcheck.configuration.SofaBootHealthCheckAutoConfiguration
複製程式碼

SofaBootHealthCheckInitializer

SofaBootHealthCheckInitializer 實現了 ApplicationContextInitializer 介面。

ApplicationContextInitializerSpring 框架原有的概念,這個類的主要目的就是在 ConfigurableApplicationContext 型別(或者子型別)的 ApplicationContextrefresh 之前,允許我們 對 ConfigurableApplicationContext 的例項做進一步的設定或者處理。

public class SofaBootHealthCheckInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        Environment environment = applicationContext.getEnvironment();
        if (SOFABootEnvUtils.isSpringCloudBootstrapEnvironment(environment)) {
            return;
        }
        // init logging.level.com.alipay.sofa.runtime argument
        String healthCheckLogLevelKey = Constants.LOG_LEVEL_PREFIX
                                        + HealthCheckConstants.SOFABOOT_HEALTH_LOG_SPACE;
        SofaBootLogSpaceIsolationInit.initSofaBootLogger(environment, healthCheckLogLevelKey);
      SofaBootHealthCheckLoggerFactory.getLogger(SofaBootHealthCheckInitializer.class).info(
            "SOFABoot HealthCheck Starting!");
    }
}
複製程式碼

SofaBootHealthCheckInitializerinitialize 方法中主要做了兩件事:

  • 驗證當前 environment 是否是 SpringCloud 的(3.0.0 開始支援 springCloud,之前版本無此 check
  • 初始化 logging.level

這兩件事和健康檢查沒有什麼關係,但是既然放在這個模組裡面還是來看下。

1、springCloud 環境驗證

首先就是為什麼會有這個驗證。SOFABoot 在支援 SpringcLoud 時遇到一個問題,就是當在 classpath 中新增spring-cloud-context 依賴關係時,org.springframework.context.ApplicationContextInitializer會被呼叫兩次。具體背景可參考 # issue1151 && # issue 232

private final static String SPRING_CLOUD_MARK_NAME = "org.springframework.cloud.bootstrap.BootstrapConfiguration";

public static boolean isSpringCloudBootstrapEnvironment(Environment environment) {
    if (environment instanceof ConfigurableEnvironment) {
        return !((ConfigurableEnvironment) environment).getPropertySources().contains(
            SofaBootInfraConstants.SOFA_BOOTSTRAP)
               && isSpringCloud();
    }
    return false;
}

public static boolean isSpringCloud() {
    return ClassUtils.isPresent(SPRING_CLOUD_MARK_NAME, null);
}
複製程式碼

上面這段程式碼是 SOFABoot 提供的一個用於區分 引導上下文 和 應用上下文 的方法:

  • 檢驗是否有"org.springframework.cloud.bootstrap.BootstrapConfiguration"這個類來判斷當前是否引入了spingCloud的引導配置類
  • environment 中獲取 MutablePropertySources 例項,驗證 MutablePropertySources 中是否包括 sofaBootstrap ( 如果當前環境是 SOFA bootstrap environment,則包含 sofaBootstrap;這個是在 SofaBootstrapRunListener 回撥方法中設定進行的 )

2、初始化 logging.level

這裡是處理 SOFABoot 日誌空間隔離的。

public static void initSofaBootLogger(Environment environment, String runtimeLogLevelKey) {
    // 初始化 logging.path 引數
    String loggingPath = environment.getProperty(Constants.LOG_PATH);
    if (!StringUtils.isEmpty(loggingPath)) {
        System.setProperty(Constants.LOG_PATH, environment.getProperty(Constants.LOG_PATH));
        ReportUtil.report("Actual " + Constants.LOG_PATH + " is [ " + loggingPath + " ]");
    }

    //for example : init logging.level.com.alipay.sofa.runtime argument
    String runtimeLogLevelValue = environment.getProperty(runtimeLogLevelKey);
    if (runtimeLogLevelValue != null) {
        System.setProperty(runtimeLogLevelKey, runtimeLogLevelValue);
    }

    // init file.encoding
    String fileEncoding = environment.getProperty(Constants.LOG_ENCODING_PROP_KEY);
    if (!StringUtils.isEmpty(fileEncoding)) {
        System.setProperty(Constants.LOG_ENCODING_PROP_KEY, fileEncoding);
    }
}
複製程式碼

SofaBootHealthCheckAutoConfiguration

這個類是 SOFABoot 健康檢查機制的自動化配置實現。

@Configuration
public class SofaBootHealthCheckAutoConfiguration {
    /** ReadinessCheckListener: 容器重新整理之後回撥 */
    @Bean
    public ReadinessCheckListener readinessCheckListener() {
        return new ReadinessCheckListener();
    }
    /** HealthCheckerProcessor: HealthChecker處理器 */
    @Bean
    public HealthCheckerProcessor healthCheckerProcessor() {
        return new HealthCheckerProcessor();
    }
    /** HealthCheckerProcessor: HealthIndicator處理器 */
    @Bean
    public HealthIndicatorProcessor healthIndicatorProcessor() {
        return new HealthIndicatorProcessor();
    }
    /** AfterReadinessCheckCallbackProcessor: ReadinessCheck之後的回撥處理器 */
    @Bean
    public AfterReadinessCheckCallbackProcessor afterReadinessCheckCallbackProcessor() {
        return new AfterReadinessCheckCallbackProcessor();
    }
    /** 返回 SofaBoot健康檢查指標類 例項*/
    @Bean
    public SofaBootHealthIndicator sofaBootHealthIndicator() {
        return new SofaBootHealthIndicator();
    }

    @ConditionalOnClass(Endpoint.class)
    public static class ConditionReadinessEndpointConfiguration {
        @Bean
        @ConditionalOnEnabledEndpoint
        public SofaBootReadinessCheckEndpoint sofaBootReadinessCheckEndpoint() {
            return new SofaBootReadinessCheckEndpoint();
        }
    }

    @ConditionalOnClass(Endpoint.class)
    public static class ReadinessCheckExtensionConfiguration {
        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnEnabledEndpoint
        public ReadinessEndpointWebExtension readinessEndpointWebExtension() {
            return new ReadinessEndpointWebExtension();
        }
    }
}
複製程式碼

ReadinessCheckListener

public class ReadinessCheckListener implements PriorityOrdered,
                                   ApplicationListener<ContextRefreshedEvent> 
複製程式碼

從程式碼來看,ReadinessCheckListener 實現了 ApplicationListener 監聽器介面,其所監聽的事件物件是ContextRefreshedEvent,即當容器上下文重新整理完成之後回撥。 SOFABoot 中通過這個監聽器來完成 readniess check 的處理。

onApplicationEvent 回撥方法:

public void onApplicationEvent(ContextRefreshedEvent event) {
    // healthCheckerProcessor init
    healthCheckerProcessor.init();
    // healthIndicatorProcessor init
    healthIndicatorProcessor.init();
    // afterReadinessCheckCallbackProcessor init
    afterReadinessCheckCallbackProcessor.init();
    // readiness health check execute
    readinessHealthCheck();
}
複製程式碼
  • 初始化 healthCheckerProcessor,這個裡面就是將當前所有的HealthChecker型別的bean找出來,然後放在一個map中,等待後面的 readiness check
public void init() {
    // 是否已經初始化了
    if (isInitiated.compareAndSet(false, true)) {
        // applicationContext 應用上下文不能為null
        Assert.notNull(applicationContext, () -> "Application must not be null");
        // 獲取所有型別是 HealthChecker 的bean
        Map<String, HealthChecker> beansOfType = applicationContext
                .getBeansOfType(HealthChecker.class);
        // 排序
        healthCheckers = HealthCheckUtils.sortMapAccordingToValue(beansOfType,
                applicationContext.getAutowireCapableBeanFactory());
        // 構建日誌資訊,對應在健康檢查日誌裡面列印出來的是:
        // ./logs/health-check/common-default.log:Found 0 HealthChecker implementation
        StringBuilder healthCheckInfo = new StringBuilder(512).append("Found ")
                .append(healthCheckers.size()).append(" HealthChecker implementation:")
                .append(String.join(",", healthCheckers.keySet()));
        logger.info(healthCheckInfo.toString());
    }
}
複製程式碼
  • 初始化 healthIndicatorProcessor,將所有的healthIndicator 型別的bean 找出來,然後放在一個map中等待readiness check。如果想要在 SOFABootReadiness Check 裡面增加一個檢查項,那麼可以直接擴充套件 Spring BootHealthIndicator這個介面。
public void init() {
    // 是否已經初始化
    if (isInitiated.compareAndSet(false, true)) {
        // applicationContext 驗證
        Assert.notNull(applicationContext, () -> "Application must not be null");
        // 獲取所有HealthIndicator型別的bean
        Map<String, HealthIndicator> beansOfType = applicationContext
                .getBeansOfType(HealthIndicator.class);
        // 支援 Reactive 方式
        if (ClassUtils.isPresent(REACTOR_CLASS, null)) {
            applicationContext.getBeansOfType(ReactiveHealthIndicator.class).forEach(
                    (name, indicator) -> beansOfType.put(name, () -> indicator.health().block()));
        }
        // 排序
        healthIndicators = HealthCheckUtils.sortMapAccordingToValue(beansOfType,
                applicationContext.getAutowireCapableBeanFactory());
        // 構建日誌資訊
        // Found 2 HealthIndicator implementation:
        // sofaBootHealthIndicator, diskSpaceHealthIndicator
        StringBuilder healthIndicatorInfo = new StringBuilder(512).append("Found ")
                .append(healthIndicators.size()).append(" HealthIndicator implementation:")
                .append(String.join(",", healthIndicators.keySet()));
        logger.info(healthIndicatorInfo.toString());
    }
}
複製程式碼
  • 初始化 afterReadinessCheckCallbackProcessor。如果想要在 Readiness Check 之後做一些事情,那麼可以擴充套件 SOFABoot 的這個介面
public void init() {
    //  是否已經初始化
    if (isInitiated.compareAndSet(false, true)) {
        // applicationContext 驗證
        Assert.notNull(applicationContext, () -> "Application must not be null");
        // 找到所有 ReadinessCheckCallback 型別的 bean 
        Map<String, ReadinessCheckCallback> beansOfType = applicationContext
                .getBeansOfType(ReadinessCheckCallback.class);
        // 排序
        readinessCheckCallbacks = HealthCheckUtils.sortMapAccordingToValue(beansOfType,
                applicationContext.getAutowireCapableBeanFactory());
        // 構建日誌
        StringBuilder applicationCallbackInfo = new StringBuilder(512).append("Found ")
                .append(readinessCheckCallbacks.size())
                .append(" ReadinessCheckCallback implementation: ")
                .append(String.join(",", beansOfType.keySet()));
        logger.info(applicationCallbackInfo.toString());
    }
}
複製程式碼
  • readinessHealthCheck,前面的幾個init方法中均是為readinessHealthCheck做準備的,到這裡SOFABoot已經拿到了當前多有的HealthCheckerHealthIndicatorReadinessCheckCallback 型別的 bean 資訊。

    // readiness health check
    public void readinessHealthCheck() {
        // 是否跳過所有check,可以通過 com.alipay.sofa.healthcheck.skip.all 配置項配置決定
        if (skipAllCheck()) {
            logger.warn("Skip all readiness health check.");
        } else {
            // 是否跳過所有 HealthChecker 型別bean的 readinessHealthCheck,
            // 可以通過com.alipay.sofa.healthcheck.skip.component配置項配置
            if (skipComponent()) {
                logger.warn("Skip HealthChecker health check.");
            } else {
                //HealthChecker 的 readiness check
                healthCheckerStatus = healthCheckerProcessor
                    .readinessHealthCheck(healthCheckerDetails);
            }
            // 是否跳過所有HealthIndicator 型別bean的readinessHealthCheck
            // 可以通過 com.alipay.sofa.healthcheck.skip.indicator配置項配置
            if (skipIndicator()) {
                logger.warn("Skip HealthIndicator health check.");
            } else {
                //HealthIndicator 的 readiness check
                healthIndicatorStatus = healthIndicatorProcessor
                    .readinessHealthCheck(healthIndicatorDetails);
            }
        }
        // ReadinessCheck 之後的回撥函式,做一些後置處理
        healthCallbackStatus = afterReadinessCheckCallbackProcessor
            .afterReadinessCheckCallback(healthCallbackDetails);
        if (healthCheckerStatus && healthIndicatorStatus && healthCallbackStatus) {
            logger.info("Readiness check result: success");
        } else {
            logger.error("Readiness check result: fail");
        }
    }
    複製程式碼

Readiness Check 做了什麼

前面是 SOFABoot 健康檢查元件處理健康檢查邏輯的一個大體流程,瞭解到了 Readiness 包括檢查 HealthChecker 型別的beanHealthIndicator 型別的 bean。其中HealthIndicatorSpringBoot自己的介面 ,而 HealthCheckerSOFABoot 提供的介面。下面繼續通過 XXXProcess 來看下 Readiness Check 到底做了什麼?

HealthCheckerProcessor

HealthChecker 的健康檢查處理器,readinessHealthCheck 方法

public boolean readinessHealthCheck(Map<String, Health> healthMap) {
    Assert.notNull(healthCheckers, "HealthCheckers must not be null.");
    logger.info("Begin SOFABoot HealthChecker readiness check.");
    boolean result = healthCheckers.entrySet().stream()
            .map(entry -> doHealthCheck(entry.getKey(), entry.getValue(), true, healthMap, true))
            .reduce(true, BinaryOperators.andBoolean());
    if (result) {
        logger.info("SOFABoot HealthChecker readiness check result: success.");
    } else {
        logger.error("SOFABoot HealthChecker readiness check result: failed.");
    }
    return result;
}
複製程式碼

這裡每個HealthChecker又委託給doHealthCheck來檢查

private boolean doHealthCheck(String beanId, HealthChecker healthChecker, boolean isRetry,
                              Map<String, Health> healthMap, boolean isReadiness) {
    Assert.notNull(healthMap, "HealthMap must not be null");
    Health health;
    boolean result;
    int retryCount = 0;
    // check 型別  readiness ? liveness
    String checkType = isReadiness ? "readiness" : "liveness";
    do {
        // 獲取 Health 物件
        health = healthChecker.isHealthy();
        // 獲取 健康檢查狀態結果
        result = health.getStatus().equals(Status.UP);
        if (result) {
            logger.info("HealthChecker[{}] {} check success with {} retry.", beanId, checkType,retryCount);
            break;
        } else {
            logger.info("HealthChecker[{}] {} check fail with {} retry.", beanId, checkType,retryCount);
        }
        // 重試 && 等待
        if (isRetry && retryCount < healthChecker.getRetryCount()) {
            try {
                retryCount += 1;
                TimeUnit.MILLISECONDS.sleep(healthChecker.getRetryTimeInterval());
            } catch (InterruptedException e) {
                logger
                    .error(
                        String
                            .format(
                                "Exception occurred while sleeping of %d retry HealthChecker[%s] %s check.",
                                retryCount, beanId, checkType), e);
            }
        }
    } while (isRetry && retryCount < healthChecker.getRetryCount());
    // 將當前 例項 bean 的健康檢查結果存到結果集healthMap中
    healthMap.put(beanId, health);
    try {
        if (!result) {
            logger
                .error(
                    "HealthChecker[{}] {} check fail with {} retry; fail details:{}; strict mode:{}",
                    beanId, checkType, retryCount,
                    objectMapper.writeValueAsString(health.getDetails()),
                    healthChecker.isStrictCheck());
        }
    } catch (JsonProcessingException ex) {
        logger.error(
            String.format("Error occurred while doing HealthChecker %s check.", checkType), ex);
    }
    // 返回健康檢查結果
    return !healthChecker.isStrictCheck() || result;
}
複製程式碼

這裡的 doHealthCheck 結果需要依賴具體 HealthChecker 實現類的處理。通過這樣一種方式可以SOFABoot可以很友好的實現對所以 HealthChecker 的健康檢查。HealthIndicatorProcessorreadinessHealthCheckHealthChecker的基本差不多;有興趣的可以自行閱讀原始碼 Alipay-SOFABoot

AfterReadinessCheckCallbackProcessor

這個介面是 SOFABoot 提供的一個擴充套件介面, 用於在 Readiness Check 之後做一些事情。其實現思路和前面的XXXXProcessor 是一樣的,對之前初始化時得到的所有的ReadinessCheckCallbacks例項bean逐一進行回撥處理。

public boolean afterReadinessCheckCallback(Map<String, Health> healthMap) {
    logger.info("Begin ReadinessCheckCallback readiness check");
    Assert.notNull(readinessCheckCallbacks, "ReadinessCheckCallbacks must not be null.");

    boolean result = readinessCheckCallbacks.entrySet().stream()
            .map(entry -> doHealthCheckCallback(entry.getKey(), entry.getValue(), healthMap))
            .reduce(true, BinaryOperators.andBoolean());

    if (result) {
        logger.info("ReadinessCheckCallback readiness check result: success.");
    } else {
        logger.error("ReadinessCheckCallback readiness check result: failed.");
    }
    return result;
}
複製程式碼

同樣也是委託給了doHealthCheckCallback來處理

private boolean doHealthCheckCallback(String beanId,
                                      ReadinessCheckCallback readinessCheckCallback,
                                      Map<String, Health> healthMap) {
    Assert.notNull(healthMap, () -> "HealthMap must not be null");
    boolean result = false;
    Health health = null;
    try {
        health = readinessCheckCallback.onHealthy(applicationContext);
        result = health.getStatus().equals(Status.UP);
        // print log 省略
    } catch (Throwable t) {
        // 異常處理
    } finally {
        // 存入 healthMap
        healthMap.put(beanId, health);
    }
    return result;
}
複製程式碼

擴充套件 Readiness Check 能力

按照上面的分析,我們可以自己來實現下這幾個擴充套件。

實現 HealthChecker 介面

@Component
public class GlmapperHealthChecker implements HealthChecker {

    @Override
    public Health isHealthy() {
        // 可以檢測資料庫連線是否成功
        // 可以檢測zookeeper是否啟動成功
        // 可以檢測redis客戶端是否啟動成功
        // everything you want ...
        if(OK){
            return Health.up().build();
        }
        return Health.down().build();
    }

    @Override
    public String getComponentName() {
        // 元件名
        return "GlmapperComponent";
    }
    
    @Override
    public int getRetryCount() {
        // 重試次數
        return 1;
    }

    @Override
    public long getRetryTimeInterval() {
        // 重試間隔
        return 0;
    }

    @Override
    public boolean isStrictCheck() {
        return false;
    }
}
複製程式碼

實現 ReadinessCheckCallback 介面

@Component
public class GlmapperReadinessCheckCallback implements ReadinessCheckCallback {

    @Override
    public Health onHealthy(ApplicationContext applicationContext) {
        Object glmapperHealthChecker = applicationContext.getBean("glmapperHealthChecker");
        if (glmapperHealthChecker instanceof GlmapperHealthChecker){
            return Health.up().build();
        }
        return Health.down().build();
    }
}
複製程式碼

再來看下健康檢查日誌:

SOFABoot 健康檢查能力分析

可以看到我們自己定義的檢查型別ready了。

從日誌看到有一個 sofaBootHealthIndicator,實現了HealthIndicator 介面。

public class SofaBootHealthIndicator implements HealthIndicator {
    private static final String    CHECK_RESULT_PREFIX = "Middleware";
    @Autowired
    private HealthCheckerProcessor healthCheckerProcessor;

    @Override
    public Health health() {
        Map<String, Health> healths = new HashMap<>();
        // 呼叫了 healthCheckerProcessor 的 livenessHealthCheck
        boolean checkSuccessful = healthCheckerProcessor.livenessHealthCheck(healths);
        if (checkSuccessful) {
            return Health.up().withDetail(CHECK_RESULT_PREFIX, healths).build();
        } else {
            return Health.down().withDetail(CHECK_RESULT_PREFIX, healths).build();
        }
    }
}
複製程式碼

livenessHealthCheckreadinessHealthCheck 兩個方法都是交給 doHealthCheck 來處理的,沒有看出來有什麼區別。

小結

本文基於 SOFABoot 3.0.0 版本,與之前版本有一些區別。詳細變更見:SOFABoot upgrade_3_x。本篇文章簡單介紹了 SOFABootSpringBoot 健康檢查能力擴充套件的具體實現細節。

最後再來補充下 livenessreadiness,從字面意思來理解,liveness就是是否是活的,readiness 就是意思是否可訪問的。

  • readiness:應用即便已經正在執行了,它仍然需要一定時間才能 提供 服務,這段時間可能用來載入資料,可能用來構建快取,可能用來註冊服務,可能用來選舉 Leader等等。總之 Readiness 檢查通過前是不會有流量發給應用的。目前 SOFARPC 就是在 readiness check 之後才會將所有的服務註冊到註冊中心去。
  • liveness:檢測應用程式是否正在執行

相關文章