Spring Boot Admin使用及心跳檢測原理

ldzsl發表於2021-09-09

介紹

Spring Boot Admin是一個Github上的一個開源專案,它在Spring Boot Actuator的基礎上提供簡潔的視覺化WEB UI,是用來管理 Spring Boot 應用程式的一個簡單的介面,提供如下功能:

  • 顯示 name/id 和版本號

  • 顯示線上狀態

  • Logging日誌級別管理

  • JMX beans管理

  • Threads會話和執行緒管理

  • Trace應用請求跟蹤

  • 應用執行引數資訊,如:

    • Java 系統屬性

    • Java 環境變數屬性

    • 記憶體資訊

    • Spring 環境屬性

Spring Boot Admin 包含服務端和客戶端,按照以下配置可讓Spring Boot Admin執行起來。

本文示例程式碼:

使用

Server端

1、pom檔案引入相關的jar包
新建一個admin-server的Spring Boot專案,在pom檔案中引入server相關的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本身能夠發現自己(自己也是客戶端)。

2、 application.yml配置

在application.yml配置如下,除了server.port=8083的配置是server 對外公佈的服務埠外,其他配置是server本身作為客戶端的配置,包括指明指向服務端的地址和當前應用的基本資訊,使用@@可以讀取pom.xml的相關配置。

在下面Client配置的講解中,可以看到下面類似的配置。

server:
  port: 8083spring:
  boot:
    admin:
      url: 
  name: server
  description: @project.description@
  version: @project.version@

3、配置日誌級別

在application.yml的同級目錄,新增檔案logback.xml,用以配置日誌的級別,包含的內容如下:

<?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,這樣可以透過控制檯日誌檢視server端和client端的互動情況。

4、新增入口方法註解

在入口方法上新增@EnableAdminServer註解。

@Configuration@EnableAutoConfiguration@EnableAdminServerpublic class ServerApplication {    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
}

5、啟動專案

啟動admin-server專案後,可以看到當前註冊的客戶端,點選明細,還可以檢視其他明細資訊。

圖片描述

Spring Boot Admin Server

Client端

在上述的Server端配置中,server本身也作為一個客戶端註冊到自己,所以client配置同server端配置起來,比較見到如下。

建立一個admin-client專案,在pom.xml新增相關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配置註冊中心地址等資訊:

spring:
  boot:
    admin:
      url: 
  name: client
  description: @project.description@
  version: @project.version@
endpoints:
  trace:
    enabled: true
    sensitive: false

3、配置日誌檔案

在application.yml的同級目錄,新增檔案logback.xml,用以配置日誌的級別,包含的內容如下:

<?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的級別,可以輸出和伺服器的通訊資訊,以便我們在後續心跳檢測,瞭解Spring Boot Admin的實現方式。

4、啟動Admin-Client應用

啟動客戶端專案,在服務端監聽了客戶端的啟動,並在頁面給出了訊息提示,啟動後,服務端的介面顯示如下:(兩個客戶端都為UP狀態)

圖片描述

Spring Boot Admin Client 啟動後

以上就可以使用Spring Boot Admin的各種監控服務了,下面談一談客戶端和服務端怎麼樣做心跳檢測的。

心跳檢測/健康檢測原理

原理

在Spring Boot Admin中,Server端作為註冊中心,它要監控所有的客戶端當前的狀態。要知道當前客戶端是否當機,剛釋出的客戶端也能夠主動註冊到服務端。

服務端和客戶端之間透過特定的介面通訊(/health介面)通訊,來監聽客戶端的狀態。因為客戶端和服務端不能保證釋出順序。

有如下的場景需要考慮:

  1. 客戶端先啟動,服務端後啟動

  2. 服務端先啟動,客戶端後啟動

  3. 服務端執行中,客戶端下線

  4. 客戶端執行中,服務端下線

所以為了解決以上問題,需要客戶端和服務端都設定一個任務監聽器,定時監聽對方的心跳,並在伺服器及時更新客戶端狀態。

上文的配置使用了客戶端主動註冊的方法。

除錯準備

為了理解Spring Boot Admin的實現方式,可透過DEBUG   和檢視日誌的方式理解伺服器和客戶端的通訊(心跳檢測)

  • 在pom.xml右鍵spring-boot-admin-server和spring-boot-admin-starter-client,Maven->
    DownLoad Sources and Documentation

  • 在logback.xml中設定日誌級別為DEBUG

客戶端發起POST請求

客戶端相關類

  • RegistrationApplicationListener

  • ApplicationRegistrator

在客戶端啟動的時候呼叫RegistrationApplicationListener的startRegisterTask,該方法每隔 registerPeriod = 10_000L,(10秒:預設)向服務端POST一次請求,告訴伺服器自身當前是有心跳的。

  • 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()方法中,向服務端POST了Restful請求,請求的地址為/api/applications
並把自身資訊帶了過去,服務端接受請求後,透過sha-1演算法計算客戶單的唯一ID,查詢hazelcast快取資料庫,如第一次則寫入,否則更新。

服務端接收處理請求相關類

  • 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  (快取資料庫)

在上述更新狀態使用了publisher.publishEvent事件訂閱的方式,接受者接收到該事件,做應用的業務處理,在這塊使用這種方式個人理解是為了程式碼的複用性,因為服務端定時輪詢客戶端也要更新客戶端在伺服器的狀態。

pulishEvent設計到的類有:

  • StatusUpdateApplicationListener->onClientApplicationRegistered

  • StatusUpdater-->updateStatus

這裡不詳細展開,下文還會提到,透過日誌,可以檢視到客戶端定時傳送的POST請求:

圖片描述

客戶端定時POST

服務端定時輪詢

在伺服器當機的時候,伺服器接收不到請求,此時伺服器不知道客戶端是什麼狀態,(當然可以說伺服器在一定的時間裡沒有收到客戶端的資訊,就認為客戶端掛了,這也是一種處理方式),在Spring Boot Admin中,服務端透過定時輪詢客戶端的/health介面來對客戶端進行心態檢測。

這裡設計到主要的類為:

  • 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));
        }
    }



作者:billJiang
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2325/viewspace-2820478/,如需轉載,請註明出處,否則將追究法律責任。

相關文章