Spring Boot Admin使用及心跳檢測(cè)原理

介紹

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ì)信息。

Spring Boot Admin Server

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 Client 啟動(dòng)后

以上就可以使用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)景需要考慮:

  1. 客戶端先啟動(dòng),服務(wù)端后啟動(dòng)
  2. 服務(wù)端先啟動(dòng),客戶端后啟動(dòng)
  3. 服務(wù)端運(yùn)行中,客戶端下線
  4. 客戶端運(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)求:

客戶端定時(shí)POST

服務(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)之處,歡迎大家指正。

本文參考了:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容