介紹
Spring Boot Admin是一個(gè)Github上的一個(gè)開源項(xiàng)目,它在Spring Boot Actuator的基礎(chǔ)上提供簡潔的可視化WEB UI,是用來管理 Spring Boot 應(yīng)用程序的一個(gè)簡單的界面,提供如下功能:
- 顯示 name/id 和版本號(hào)
- 顯示在線狀態(tài)
- Logging日志級(jí)別管理
- JMX beans管理
- Threads會(huì)話和線程管理
- Trace應(yīng)用請(qǐng)求跟蹤
- 應(yīng)用運(yùn)行參數(shù)信息,如:
- Java 系統(tǒng)屬性
- Java 環(huán)境變量屬性
- 內(nèi)存信息
- Spring 環(huán)境屬性
Spring Boot Admin 包含服務(wù)端和客戶端,按照以下配置可讓Spring Boot Admin運(yùn)行起來。
本文示例代碼:boot-admin-demo
使用
Server端
1、pom文件引入相關(guān)的jar包
新建一個(gè)admin-server的Spring Boot項(xiàng)目,在pom文件中引入server相關(guān)的jar包
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>1.5.3</version>
</dependency>
其中spring-boot-admin-starter-client
的引入是讓server本身能夠發(fā)現(xiàn)自己(自己也是客戶端)。
2、 application.yml配置
在application.yml配置如下,除了server.port=8083
的配置是server 對(duì)外公布的服務(wù)端口外,其他配置是server本身作為客戶端的配置,包括指明指向服務(wù)端的地址和當(dāng)前應(yīng)用的基本信息,使用@@可以讀取pom.xml
的相關(guān)配置。
在下面Client配置的講解中,可以看到下面類似的配置。
server:
port: 8083
spring:
boot:
admin:
url: http://localhost:8083
info:
name: server
description: @project.description@
version: @project.version@
3、配置日志級(jí)別
在application.yml的同級(jí)目錄,添加文件logback.xml
,用以配置日志的級(jí)別,包含的內(nèi)容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.web" level="DEBUG"/>
<jmxConfigurator/>
</configuration>
在此處配置成了DEBUG
,這樣可以通過控制臺(tái)日志查看server端和client端的交互情況。
4、添加入口方法注解
在入口方法上添加@EnableAdminServer
注解。
@Configuration
@EnableAutoConfiguration
@EnableAdminServer
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
5、啟動(dòng)項(xiàng)目
啟動(dòng)admin-server項(xiàng)目后,可以看到當(dāng)前注冊(cè)的客戶端,點(diǎn)擊明細(xì),還可以查看其他明細(xì)信息。
Client端
在上述的Server端配置中,server本身也作為一個(gè)客戶端注冊(cè)到自己,所以client配置同server端配置起來,比較見到如下。
創(chuàng)建一個(gè)admin-client項(xiàng)目,在pom.xml
添加相關(guān)client依賴包。
1、pom.xml添加client依賴
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>1.5.3</version>
</dependency>
2、application.yml配置
在application.yml配置注冊(cè)中心地址等信息:
spring:
boot:
admin:
url: http://localhost:8083
info:
name: client
description: @project.description@
version: @project.version@
endpoints:
trace:
enabled: true
sensitive: false
3、配置日志文件
在application.yml的同級(jí)目錄,添加文件logback.xml
,用以配置日志的級(jí)別,包含的內(nèi)容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.web" level="DEBUG"/>
<jmxConfigurator/>
</configuration>
配置為DEBUG
的級(jí)別,可以輸出和服務(wù)器的通信信息,以便我們?cè)诤罄m(xù)心跳檢測(cè),了解Spring Boot Admin的實(shí)現(xiàn)方式。
4、啟動(dòng)Admin-Client應(yīng)用
啟動(dòng)客戶端項(xiàng)目,在服務(wù)端監(jiān)聽了客戶端的啟動(dòng),并在頁面給出了消息提示,啟動(dòng)后,服務(wù)端的界面顯示如下:(兩個(gè)客戶端都為UP
狀態(tài))
以上就可以使用Spring Boot Admin的各種監(jiān)控服務(wù)了,下面談一談客戶端和服務(wù)端怎么樣做心跳檢測(cè)的。
心跳檢測(cè)/健康檢測(cè)原理
原理
在Spring Boot Admin中,Server端作為注冊(cè)中心,它要監(jiān)控所有的客戶端當(dāng)前的狀態(tài)。要知道當(dāng)前客戶端是否宕機(jī),剛發(fā)布的客戶端也能夠主動(dòng)注冊(cè)到服務(wù)端。
服務(wù)端和客戶端之間通過特定的接口通信(/health接口)通信,來監(jiān)聽客戶端的狀態(tài)。因?yàn)榭蛻舳撕头?wù)端不能保證發(fā)布順序。
有如下的場(chǎng)景需要考慮:
- 客戶端先啟動(dòng),服務(wù)端后啟動(dòng)
- 服務(wù)端先啟動(dòng),客戶端后啟動(dòng)
- 服務(wù)端運(yùn)行中,客戶端下線
- 客戶端運(yùn)行中,服務(wù)端下線
所以為了解決以上問題,需要客戶端和服務(wù)端都設(shè)置一個(gè)任務(wù)監(jiān)聽器,定時(shí)監(jiān)聽對(duì)方的心跳,并在服務(wù)器及時(shí)更新客戶端狀態(tài)。
上文的配置使用了客戶端主動(dòng)注冊(cè)的方法。
調(diào)試準(zhǔn)備
為了理解Spring Boot Admin的實(shí)現(xiàn)方式,可通過DEBUG
和查看日志的方式理解服務(wù)器和客戶端的通信(心跳檢測(cè))
在pom.xml右鍵spring-boot-admin-server和spring-boot-admin-starter-client,Maven->
DownLoad Sources and Documentation在logback.xml中設(shè)置日志級(jí)別為DEBUG
客戶端發(fā)起POST請(qǐng)求
客戶端相關(guān)類
- RegistrationApplicationListener
- ApplicationRegistrator
在客戶端啟動(dòng)的時(shí)候調(diào)用RegistrationApplicationListener的startRegisterTask,該方法每隔 registerPeriod = 10_000L,(10秒:默認(rèn))向服務(wù)端POST一次請(qǐng)求,告訴服務(wù)器自身當(dāng)前是有心跳
的。
- RegistrationApplicationListener
@EventListener
@Order(Ordered.LOWEST_PRECEDENCE)
public void onApplicationReady(ApplicationReadyEvent event) {
if (event.getApplicationContext() instanceof WebApplicationContext && autoRegister) {
startRegisterTask();
}
}
public void startRegisterTask() {
if (scheduledTask != null && !scheduledTask.isDone()) {
return;
}
scheduledTask = taskScheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
registrator.register();
}
}, registerPeriod);
LOGGER.debug("Scheduled registration task for every {}ms", registerPeriod);
}
- ApplicationRegistrator
public boolean register() {
boolean isRegistrationSuccessful = false;
Application self = createApplication();
for (String adminUrl : admin.getAdminUrl()) {
try {
@SuppressWarnings("rawtypes") ResponseEntity<Map> response = template.postForEntity(adminUrl,
new HttpEntity<>(self, HTTP_HEADERS), Map.class);
if (response.getStatusCode().equals(HttpStatus.CREATED)) {
if (registeredId.compareAndSet(null, response.getBody().get("id").toString())) {
LOGGER.info("Application registered itself as {}", response.getBody());
} else {
LOGGER.debug("Application refreshed itself as {}", response.getBody());
}
isRegistrationSuccessful = true;
if (admin.isRegisterOnce()) {
break;
}
} else {
if (unsuccessfulAttempts.get() == 0) {
LOGGER.warn(
"Application failed to registered itself as {}. Response: {}. Further attempts are logged on DEBUG level",
self, response.toString());
} else {
LOGGER.debug("Application failed to registered itself as {}. Response: {}", self,
response.toString());
}
}
} catch (Exception ex) {
if (unsuccessfulAttempts.get() == 0) {
LOGGER.warn(
"Failed to register application as {} at spring-boot-admin ({}): {}. Further attempts are logged on DEBUG level",
self, admin.getAdminUrl(), ex.getMessage());
} else {
LOGGER.debug("Failed to register application as {} at spring-boot-admin ({}): {}", self,
admin.getAdminUrl(), ex.getMessage());
}
}
}
if (!isRegistrationSuccessful) {
unsuccessfulAttempts.incrementAndGet();
} else {
unsuccessfulAttempts.set(0);
}
return isRegistrationSuccessful;
}
在主要的register()方法中,向服務(wù)端POST
了Restful請(qǐng)求,請(qǐng)求的地址為/api/applications
并把自身信息帶了過去,服務(wù)端接受請(qǐng)求后,通過sha-1算法計(jì)算客戶單的唯一ID,查詢hazelcast緩存數(shù)據(jù)庫,如第一次則寫入,否則更新。
服務(wù)端接收處理請(qǐng)求相關(guān)類
- RegistryController
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<Application> register(@RequestBody Application application) {
Application applicationWithSource = Application.copyOf(application).withSource("http-api")
.build();
LOGGER.debug("Register application {}", applicationWithSource.toString());
Application registeredApp = registry.register(applicationWithSource);
return ResponseEntity.status(HttpStatus.CREATED).body(registeredApp);
}
- ApplicationRegistry
public Application register(Application application) {
Assert.notNull(application, "Application must not be null");
Assert.hasText(application.getName(), "Name must not be null");
Assert.hasText(application.getHealthUrl(), "Health-URL must not be null");
Assert.isTrue(checkUrl(application.getHealthUrl()), "Health-URL is not valid");
Assert.isTrue(
StringUtils.isEmpty(application.getManagementUrl())
|| checkUrl(application.getManagementUrl()), "URL is not valid");
Assert.isTrue(
StringUtils.isEmpty(application.getServiceUrl())
|| checkUrl(application.getServiceUrl()), "URL is not valid");
String applicationId = generator.generateId(application);
Assert.notNull(applicationId, "ID must not be null");
Application.Builder builder = Application.copyOf(application).withId(applicationId);
Application existing = getApplication(applicationId);
if (existing != null) {
// Copy Status and Info from existing registration.
builder.withStatusInfo(existing.getStatusInfo()).withInfo(existing.getInfo());
}
Application registering = builder.build();
Application replaced = store.save(registering);
if (replaced == null) {
LOGGER.info("New Application {} registered ", registering);
publisher.publishEvent(new ClientApplicationRegisteredEvent(registering));
} else {
if (registering.getId().equals(replaced.getId())) {
LOGGER.debug("Application {} refreshed", registering);
} else {
LOGGER.warn("Application {} replaced by Application {}", registering, replaced);
}
}
return registering;
}
- HazelcastApplicationStore (緩存數(shù)據(jù)庫)
在上述更新狀態(tài)使用了publisher.publishEvent事件訂閱的方式,接受者接收到該事件,做應(yīng)用的業(yè)務(wù)處理,在這塊使用這種方式個(gè)人理解是為了代碼的復(fù)用性,因?yàn)榉?wù)端定時(shí)輪詢客戶端也要更新客戶端在服務(wù)器的狀態(tài)。
pulishEvent設(shè)計(jì)到的類有:
- StatusUpdateApplicationListener->onClientApplicationRegistered
- StatusUpdater-->updateStatus
這里不詳細(xì)展開,下文還會(huì)提到,通過日志,可以查看到客戶端定時(shí)發(fā)送的POST請(qǐng)求:
服務(wù)端定時(shí)輪詢
在服務(wù)器宕機(jī)的時(shí)候,服務(wù)器接收不到請(qǐng)求,此時(shí)服務(wù)器不知道客戶端是什么狀態(tài),(當(dāng)然可以說服務(wù)器在一定的時(shí)間里沒有收到客戶端的信息,就認(rèn)為客戶端掛了
,這也是一種處理方式),在Spring Boot Admin中,服務(wù)端通過定時(shí)輪詢客戶端的/health接口來對(duì)客戶端進(jìn)行心態(tài)檢測(cè)。
這里設(shè)計(jì)到主要的類為:
- StatusUpdateApplicationListene
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if (event.getApplicationContext() instanceof WebApplicationContext) {
startStatusUpdate();
}
}
public void startStatusUpdate() {
if (scheduledTask != null && !scheduledTask.isDone()) {
return;
}
scheduledTask = taskScheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
statusUpdater.updateStatusForAllApplications();
}
}, updatePeriod);
LOGGER.debug("Scheduled status-updater task for every {}ms", updatePeriod);
}
- StatusUpdater
public void updateStatusForAllApplications() {
long now = System.currentTimeMillis();
for (Application application : store.findAll()) {
if (now - statusLifetime > application.getStatusInfo().getTimestamp()) {
updateStatus(application);
}
}
}
public void updateStatus(Application application) {
StatusInfo oldStatus = application.getStatusInfo();
StatusInfo newStatus = queryStatus(application);
boolean statusChanged = !newStatus.equals(oldStatus);
Application.Builder builder = Application.copyOf(application).withStatusInfo(newStatus);
if (statusChanged && !newStatus.isOffline() && !newStatus.isUnknown()) {
builder.withInfo(queryInfo(application));
}
Application newState = builder.build();
store.save(newState);
if (statusChanged) {
publisher.publishEvent(
new ClientApplicationStatusChangedEvent(newState, oldStatus, newStatus));
}
}
服務(wù)端日志:

這里就不詳細(xì)展開,如有不當(dāng)之處,歡迎大家指正。
本文參考了: