服務註冊伺服器


還記得〈@RepositoryRestResource〉中建立了個 REST 服務嗎?在〈HAL 與 RestTemplate〉也基於該 REST 服務建立了個網站可以查看訊息,若後來也有其他服務或網站,紛紛圍繞著該 REST 服務而建立,沒想到它會這麼熱門,只不過熱門到只使用單個伺服器有點不堪負荷了。

那麼啟動兩個以上的服務實例好了,然後呢?通知既有圍繞著該服務的其他消費者這些服務實例的位址,請他們可以的話,分別銜接不同的服務實例以減輕個別實例的負擔,問題是你怎麼通知?若是個內部服務,或許還好各部門通告一下,若是個外部服務,你根本無法掌握有哪些消費者怎麼辦?就算你有辦法通知好了,怎麼分配他們各自銜接哪個實例?如果未來必須因應不同時期需求,啟動不同數量的服務實例,那又該怎麼辦?總不能每次都這樣個別通知吧!

以上問題的基本解決方式,是建立一個服務註冊網站,啟動的實例向該網站註冊名稱、實體位址等,不活動的實例就註銷掉,而想要消費實例的客戶端,向該網站查詢可用的服務實例,根據查詢到的實體位址來進行負載平衡。

如果使用 Spring Cloud,可以結合 Netflix Eureka 服務發現引擎來實現,若是使用 Spring Boot,在建立專案時,可以選擇 Cloud Discovery 中的 Eureka Server,專案的 build.gradle 會包含以下內容:

implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-server')

(Netflix?那間線上影音公司?是的,在服務註冊、發現、平衡等基礎建設上,Netflix 有著卓越的貢獻,玩這類東西不可能沒聽過 Netflix,Spring Cloud 也整合了許多 Netflix 開放原始碼的方案。)

Spring 5 基於 Java SE 8,然而,由於 Eureka Server 這個 Starter 相依在 JAXB API,在 Java SE 9 支援模組化之後,JAXB API 並不在 java.se 模組之中,而是被劃分到 java.se.ee,預設模組圖中是不會有這個模組的,因此若是基於 Java SE 9/10 來運行這個專案,就會發生找不到相關類別的錯誤,一個簡單解決此問題的方式是在執行時,加上 --add-modules java.se.ee 引數。

然而,Java SE 11 時,java.se.ee 模組從 JDK 移除了,因此在 Java SE 11 之後,--add-modules java.se.ee 會發生找不到該模組的錯誤,這時只好自行在 build.gradle 中加上必要的相依了:

implementation('javax.xml.bind:jaxb-api:2.2.11') 
implementation('com.sun.xml.bind:jaxb-core:2.2.11')     
implementation('com.sun.xml.bind:jaxb-impl:2.2.11') 
implementation('javax.activation:activation:1.1.1')     

接著,必須在啟動的類別上標註 @EnableEurekaServer

package cc.openhome;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class SevdissvrApplication {
    public static void main(String[] args) {
        SpringApplication.run(SevdissvrApplication.class, args);
    }
}

然後,要來定義 Eureka 嵌入式伺服器的相關資訊,這可以定義在 bootstrap.properties 中:

server.port=8761
eureka.instance.hostname=eurka-server
eureka.client.registerWithEureka=false
eureka.client.fetchRegistry=false
eureka.server.waitTimeInMsWhenSyncEmpty=0

因為本身是提供註冊服務的伺服器,不用向自身註冊,eureka.client.registerWithEureka 設為 false(預設是 true),也不用取得服務實例的註冊表進行快取,因此 eureka.client.fetchRegistry 也設為 false(預設是 true)。

之後文件會談到服務發現的客戶端,為了減輕伺服器的負擔,客戶端會快取服務實例的註冊表,需要服務實例實體位址等資訊時,會從快取的註冊表中查找,而客戶端則定時向註冊伺服器更新註冊表(預設 30 秒)。

在 Eureka 伺服器啟動之後,預設會等待 5 分鐘,讓相關的服務實例在這段時間向其完成註冊,之後才會提供服務查找,等待的時間可以透過 eureka.server.waitTimeInMsWhenSyncEmpty 來設定,單位是毫秒,在開發階段可以將這個值設為更小或者是 0,而不用每次都枯等 5 分鐘。

服務實例向 Eureka 伺服器註冊時,必須提供三次的心跳(heartbeat),也就是向伺服器 PING 三次,每次間隔 10 秒,因此服務註冊會需要 30 秒的時間,在這之後,服務實例每 30 秒進行一次心跳,Eureka 伺服器,會計算心跳失敗比例在 15 分鐘內是否低於 85%,如果是的話就會在 Eureka 的資訊頁面上出現警訊:

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

這表示 Eureka 伺服器令目前的註冊表進入了保護模式,在保護模式下,伺服器維護的註冊表不會刪除註冊訊息,因此可能發生查找到的服務實際上並不存在的問題,因此才會顯示以上的訊息。

85% 這個比例是來自 eureka.server.renewalPercentThreshold 的預設值 0.85,在單機測試時,若 eureka.server.waitTimeInMsWhenSyncEmpty 被設為 0,Eureka 伺服器啟動後,在還沒有任何服務實例註冊時,也會顯示這個警訊,基本上是不用在意,之後有服務實例註冊後,警訊就會消失了。

如果真的在意這個警訊,可以藉由設定 eureka.server.renewalPercentThreshold 為更小的值,或者乾脆將 eureka.server.enableSelfPreservation 設為 false 來關掉保護模式。

接下來啟動專案,可以連線 http://localhost:8761/,就會看到 Eureka 的資訊頁面:

服務註冊伺服器

在圖中下方的 Application,會列出註冊的服務實例,目前還沒有任何實例,服務實例的註冊方式會在下篇文件中說明。

你可以在 EurekaServer 中找到以上的範例專案。