Анализ возможности проверки работоспособности SOFABoot

задняя часть Spring Alipay Открытый исходный код

Open Source China ежегодно проводит мероприятие по программному обеспечению с открытым исходным кодом, которое включает два проекта, связанных с SOFA (SOFABoot и SOFARPC). Пожалуйста, нажмите и проголосуйте вместе:Ооо, ооо .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

Журнал запуска проверки работоспособности

анализ кода

Поскольку это 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интерфейс.

ApplicationContextInitializerдаSpringПервоначальная концепция фреймворка, основной целью этого класса являетсяConfigurableApplicationContextтип (или подтип)ApplicationContextДелатьrefreshПрежде позвольте намConfigurableApplicationContextinstance для дальнейших настроек или обработки.

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!");
    }
}

SofaBootHealthCheckInitializerсуществуетinitializeМетод в основном делает две вещи:

  • проверить текущий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. если вы хотитеSOFABootизReadiness CheckДобавьте в него пункт проверки, тогда вы сможете напрямую развернутьSpring BootизHealthIndicatorэтот интерфейс.
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У меня есть то, что у меня есть сейчасHealthChecker,HealthIndicatorа такжеReadinessCheckCallbackТип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");
        }
    }
    

Что делает проверка готовности

спередиSOFABootОбщий процесс обработки логики проверки работоспособности компонента проверки работоспособности, изученныйReadinessвключая осмотрHealthCheckerТипbeanа такжеHealthIndicatorТипbean. вHealthIndicatorдаSpringBootсобственный интерфейс, аHealthCheckerдаSOFABoot提供的接口。 Давай продолжим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проверка здоровья.HealthIndicatorProcessorизreadinessHealthCheckа такжеHealthCheckerОсновы практически одинаковые, кому интересно, можно прочитать исходный код самостоятельно.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;
}

Расширенные возможности проверки готовности

Согласно приведенному выше анализу, мы можем реализовать эти расширения самостоятельно.

Реализовать интерфейс 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();
        }
    }
}

livenessHealthCheckа такжеreadinessHealthCheckОба метода даныdoHealthCheckразобрался, разницы не увидел.

резюме

Эта статья основана наSOFABoot 3.0.0версия, есть некоторые отличия от предыдущей версии. Подробные изменения см.SOFABoot upgrade_3_x. Эта статья кратко знакомитSOFABootправильноSpringBootКонкретные сведения о реализации расширения возможности проверки работоспособности.

Наконец, добавьтеlivenessа такжеreadiness, понимается буквально,livenessжив ли он,readinessЭто означает, доступен ли он.

  • readiness: Даже если приложение уже запущено, ему все равно требуется определенное время для предоставления услуг.Это время может быть использовано для загрузки данных, может использоваться для создания кешей, может использоваться для регистрации услуг, может использоваться для выбораLeaderи т.п. во всяком случаеReadinessНи один трафик не будет отправлен на приложение, пока проверка не будет передана. В настоящее времяSOFARPCтолько что вreadiness checkТолько тогда все сервисы будут зарегистрированы в реестре.
  • liveness: определяет, запущено ли приложение