開源中國有個年度開源軟件的活動,裏面有兩個 SOFA 相關的項目(SOFABoot & SOFARPC),你們幫忙點兩下一塊兒投個票:www.oschina.net/project/top…。同時也歡迎你們關注 SOFAStackjava
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
實現了 ApplicationContextInitializer
接口。app
ApplicationContextInitializer
是 Spring
框架原有的概念,這個類的主要目的就是在 ConfigurableApplicationContext
類型(或者子類型)的 ApplicationContext
作 refresh
以前,容許咱們 對 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!");
}
}
複製代碼
SofaBootHealthCheckInitializer
在 initialize
方法中主要作了兩件事:
environment
是不是 SpringCloud
的(3.0.0 開始支持 springCloud
,以前版本無此 check
)logging.level
這兩件事和健康檢查沒有什麼關係,可是既然放在這個模塊裏面仍是來看下。
首先就是爲何會有這個驗證。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
回調方法中設置進行的 )這裏是處理 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);
}
}
複製代碼
這個類是 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();
}
}
}
複製代碼
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
到底作了什麼?
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。
這個接口是 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;
}
複製代碼
按照上面的分析,咱們能夠本身來實現下這幾個擴展。
@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;
}
}
複製代碼
@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
:檢測應用程序是否正在運行