前言
如果還不瞭解Eureka,那麼應該先從上一篇開始 SpringCloud入坑記-初識Eureka。搭建一個Eureka Server已經註冊一個實例,纔算有一個初步的認知。
自我保護
在實踐過程中,很可能會在頁面遇到下面一段紅色文字:
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 server的一種自我保護機制。
Eureka Server與Client之間是通過心跳確認對方是否存活。而現實環境中,網絡的抖動或者閃斷可能導致心跳未能如約而至,而服務本身是健康的,如果此時Eureka server將實例去除,就會導致正常的服務無法被調用到,要是網絡抖動比較頻繁會使得程序很不穩定。極端一點,發送心跳時網絡掉線導致註冊的服務信息被刪除,所有服務都失效無法訪問了。
Eureka引入了自我保護老解決這個問題,當最近一分鐘接收到的心跳次數小於一個值的時候,就禁止服務被刪除,保護註冊信息,同時顯示一段文字提示。
不過也有幾個問題:服務停掉了,還可以在Eureka Server上看到;服務上線了,Eureka Server上還不能及時獲取到。
在項目開發時,這個功能會干擾我們的集成,可以通過下面的參數關掉:
eureka.server.enableSelfPreservation=false
Eureka Server HA 高可用
在分佈式系統中,服務註冊中心是最重要的基礎部分
單個Eureka Server
上一節已經實現的Eureka Server與Client可以通過下面這個圖表示:
可以看到,Client註冊到Server上。
如果多個服務的話,可以都註冊到Server上,就可以用下面這個圖表示:
很容易就發現,Eureka Server
是很重要的一環,服務ClientA要與服務ClientB通信,不直接找對方,而是通過中間橋樑 Eureka Server
。要是Eureka Server
宕機,服務Client A、B、C…就無法註冊,於是需要做高可用的Eureka Server
兩個Eureka Server
Eureka Server支持一種P2P模式,可以在ServerA中註冊ServerB,它們就會相互協同工作。
複製之前的項目ms-a1-eureka-server
,命名爲ms-b1-eureka-server
,僅僅修改其中的配置文件application.yml
:
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8762/eureka
可以看到,對於ServerA(端口8761)註冊ServerB的信息(端口8762)。
再次複製之前的項目ms-a1-eureka-server
,命名爲ms-b2-eureka-server
,僅僅修改其中的配置文件application.yml
:
server:
port: 8762
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
ServerB(端口8762)註冊ServerA的信息(端口8761)。
分別啓動兩個項目,在Eureka Server的控制檯可以看到如下信息:
和之前的控制面板顯示的信息不一樣。
對於客戶端,也需要相應的配置來適應Eureka Server的高可用部署。
複製之前的項目ms-a1-product-plain
,命名爲ms-b1-product-plain
,僅僅修改其中的配置文件application.yml
:
server:
port: 8080
spring:
application:
name: product-plain
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka
將所有的Eureka Server列表都寫在了配置裏,用逗號隔開。啓動該項目,可以在兩個Eureke Server的頁面看到註冊的信息。停掉任意一個Eureka Server,對於商品服務"product-plain"沒有影響。
如果有多個服務的話,可以都註冊到Server上
三個Eureka Server
在實際的生產中,2臺Eureka Server是不夠的,一般需要3臺或者更多。
對於三臺Eureka Server,需要Eureka Server兩兩之間相互配合。
這時,對於ServerA就需要如下配置application.yml
:
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8762/eureka,http://localhost:8763/eureka
在Client端,需要將所有的Server配置好:
server:
port: 8080
spring:
application:
name: product-plain
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka
Eureka Server故障分析
上面講過Eureka Server
可以做高可用,但是這就能保證萬無一失了嗎?
有一個理論叫墨菲定律,它的一個觀點可以解釋爲:如果你擔心某種情況會發生,那麼它極有可能會發生,久而久之一定會發生。
Eureka Server 全部宕機
最壞的情況就是所有的Eureka Server
全部宕機,如果此時Client還沒有啓動,那麼服務可以啓動,但是會一直打印如下錯誤信息:
com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect
com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
這樣該服務無法被其他服務感知到,不能調用其他服務,也不會被其他服務調用到。屬於一個孤立的服務,一般屬於無用服務。
要是服務在成功啓動,並且註冊到了Eureka Server
以後,全部的Eureka Server
才宕機,此時客戶端依然會報錯,但是行爲略有不同。客戶端本地會維護一個所有服務的註冊列表信息,在源代碼DiscoveryClient
中可以看到如下聲明
private final AtomicReference<Applications> localRegionApps;
它保存了一份其他服務的註冊信息,通過CacheRefreshThread
來定時刷新,時間間隔爲renewalIntervalInSecs
,默認爲30S。如果刷新時,所有的Eureka Server不可用,那麼就不刷新列表。服務間可以正常通信,但是無法感知變化,要是有服務下線,其他服務無法獲知。
Eureka Server 部分宕機
Eureka Server
之間是Peer to Peer
的同步,它比Master-Slave
模式更爲穩定一些。Master-Slave
中Slave宕機不影響系統,要是Master宕機,需要重新選舉Master機器,Zookeeper便是這種機制。P2P模式不涉及到選舉,只是Eureka Server
之間的同步會報錯,不影響正常使用。
客戶端這邊,在配置文件中存有所有Eureka Server
的地址:
defaultZone: http://server1/eureka,http://server2/eureka,http://server3/eureka
在拉取註冊列表信息的時候,客戶端會嘗試依次從這些服務器中獲取信息,獲取不到就去下一個服務器中找。目前Eureka Server
部分宕機,所以肯定可以獲取到。其次,在客戶端還維護了一個不可達的Eureka Server
列表,之後就不從這些服務器拉取信息。要是該列表中的Eureka Server
數目達到一定的值,就將其清空。
還有一點,客戶端在拉取註冊信息的時候,不是每次都從第一個開始,這樣所有的請求壓力都集中在第一臺服務器上,而其他的Eureka Server
不幹活。實際上,這個請求是隨機的,保證所有的Eureka Server
能均勻地處理客戶端的拉取請求。
Eureka Server的REST API
Eureka Server
暴露了一些API可以直接調用,大部分時候,我們用不到這些API,但是在某些場景下,可以定製化一些東西。
先列舉幾個接口:
操作 | http method | http request |
---|---|---|
註冊新服務 | POST | /eureka/apps/{appId} |
註銷服務 | DELETE | /eureka/apps/{appId}/{instanceId} |
心跳 | PUT | /eureka/apps/{appId}/{instanceId} |
查詢所有實例 | GET | /eureka/apps |
查詢單個實例 | GET | /eureka/apps/{appId} |
還有很多接口可以調用,在Eureka的GitHub上有詳細的API列表。
有什麼用呢?試想一個場景,我們要定製化Eureka Server的頁面,將其嵌入到我們現有的監控平臺,直接把Eureka Server現有的頁面加入其中顯得風格不搭,就可以通過上面提供的API來自定義界面,還可以添加更多的功能。
例如,我訪問http://localhost:8761/eureka/apps/PRODUCT-PLAIN
,就可以拿到如下信息:
<application>
<name>PRODUCT-PLAIN</name>
<instance>
<instanceId>192.168.2.200:product-plain:8080</instanceId>
<hostName>192.168.2.200</hostName>
<app>PRODUCT-PLAIN</app>
<ipAddr>192.168.2.200</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">8080</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>30</renewalIntervalInSecs>
<durationInSecs>90</durationInSecs>
<registrationTimestamp>1555842871524</registrationTimestamp>
<lastRenewalTimestamp>1555848452274</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1555842870859</serviceUpTimestamp>
</leaseInfo>
<metadata>
<management.port>8080</management.port>
<jmx.port>52858</jmx.port>
</metadata>
<homePageUrl>http://192.168.2.200:8080/</homePageUrl>
<statusPageUrl>http://192.168.2.200:8080/actuator/info</statusPageUrl>
<healthCheckUrl>http://192.168.2.200:8080/actuator/health</healthCheckUrl>
<vipAddress>product-plain</vipAddress>
<secureVipAddress>product-plain</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1555842871524</lastUpdatedTimestamp>
<lastDirtyTimestamp>1555842870720</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
</application>
未完待續
Eureka還有很多東西可以討論,需要在實際運用中慢慢體會。在下一節中,我將引入Ribbon這一組件。
項目代碼託管於Github。