開源中國有個年度開源軟體的活動,裡面有兩個 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
健康檢查啟動日誌
程式碼分析
既然是個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
之前,允許我們 對 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
這兩件事和健康檢查沒有什麼關係,但是既然放在這個模組裡面還是來看下。
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"); } } 複製程式碼
Readiness Check 做了什麼
前面是 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;
}
複製程式碼
擴充套件 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();
}
}
}
複製程式碼
livenessHealthCheck
和 readinessHealthCheck
兩個方法都是交給 doHealthCheck
來處理的,沒有看出來有什麼區別。
小結
本文基於 SOFABoot 3.0.0
版本,與之前版本有一些區別。詳細變更見:SOFABoot upgrade_3_x。本篇文章簡單介紹了 SOFABoot
對 SpringBoot
健康檢查能力擴充套件的具體實現細節。
最後再來補充下 liveness
和 readiness
,從字面意思來理解,liveness
就是是否是活的,readiness
就是意思是否可訪問的。
readiness
:應用即便已經正在執行了,它仍然需要一定時間才能 提供 服務,這段時間可能用來載入資料,可能用來構建快取,可能用來註冊服務,可能用來選舉Leader
等等。總之Readiness
檢查通過前是不會有流量發給應用的。目前SOFARPC
就是在readiness check
之後才會將所有的服務註冊到註冊中心去。liveness
:檢測應用程式是否正在執行