SOFABoot 健康檢查能力分析

開源中國有個年度開源軟件的活動,裏面有兩個 SOFA 相關的項目(SOFABoot & SOFARPC),你們幫忙點兩下一塊兒投個票:www.oschina.net/project/top…。同時也歡迎你們關注 SOFAStackjava

Liveness Check & Readiness Check

Spring Boot 提供了一個基礎的健康檢查的能力,中間件和應用均可以擴展來實現本身的健康檢查邏輯。可是 Spring Boot 的健康檢查只有 Liveness Check 的能力,缺乏 Readiness Check 的能力,這樣會有比較致命的問題。當一個微服務應用啓動的時候,必需要先保證啓動後應用是健康的,才能夠將上游的流量放進來(來自於 RPC,網關,定時任務等等流量),不然就可能會致使必定時間內大量的錯誤發生。git

針對 Spring Boot 缺乏 Readiness Check 能力的狀況,SOFABoot 增長了 Spring Boot 現有的健康檢查的能力,提供了 Readiness Check 的能力。利用 Readiness Check 的能力,SOFA 中間件中的各個組件只有在 Readiness Check 經過以後,纔將流量引入到應用的實例中,好比 RPC,只有在 Readiness Check 經過以後,纔會向服務註冊中心註冊,後面來自上游應用的流量纔會進入。github

除了中間件能夠利用 Readiness Check 的事件來控制流量的進入以外,PAAS 系統也能夠經過訪問 http://localhost:8080/actuator/readiness 來獲取應用的 Readiness Check 的情況,用來控制例如負載均衡設備等等流量的進入。redis

使用方式

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

<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-bootbootstrap

健康檢查啓動日誌

代碼分析

既然是個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 接口。app

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

這兩件事和健康檢查沒有什麼關係,可是既然放在這個模塊裏面仍是來看下。

一、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 回調方法中設置進行的 )

二、初始化 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();
    }
}
複製代碼

再來看下健康檢查日誌:

能夠看到咱們本身定義的檢查類型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:檢測應用程序是否正在運行
相關文章
相關標籤/搜索