Spring Cloud Netflix技術棧中,Eureka作為服務注冊中心對整個微服務架構起着最核心的整合作用,因此對Eureka還是有很大的必要進行深入研究。
本文主要分為四部分,一是對項目構建的簡要說明;二是對程序入口點的定位,幫助大家找到閱讀源碼的起點;三是對Eureka實現機制的分析;四是與使用Zookeeper相比Eureka作為注冊服務的區別。
1. 源碼
1.1 源碼獲取、構建
我們需要分別下載 Eureka 官方源碼和 Spring Cloud Netflix 適配 Eureka 的代碼。可以在 https://github.com/Netflix/eureka 下載到原生 Eureka 代碼,在 https://github.com/spring-cloud/spring-cloud-netflix/tree/v1.2.2.RELEASE 下載Spring Cloud針對於Eureka的Spring Cloud適配。
在構建 Eureka 官方源碼時一定要使用項目里自帶的gradlew
而不要自行下載gradle(首先要科學上網), 因為gradle早已更新到3.X版本,而Eureka用的是2.1.0版本構建的項目,新版本構建時會報錯。Spring Cloud Netflix構建起來很簡單,執行 mvn clean package
,耐心等待即可。(我機器上是12分鍾)
1.2 程序構成
Eureka:
1. 是純正的 servlet 應用,需構建成war包部署
2. 使用了 Jersey 框架實現自身的 RESTful HTTP接口
3. peer之間的同步與服務的注冊全部通過 HTTP 協議實現
4. 定時任務(發送心跳、定時清理過期服務、節點同步等)通過 JDK 自帶的 Timer
實現
5. 內存緩存使用Google的guava包實現
1.3 代碼結構
模塊概覽:
eureka-core 模塊包含了功能的核心實現:
1. com.netflix.eureka.cluster - 與peer節點復制(replication)相關的功能
2. com.netflix.eureka.lease - 即”租約”, 用來控制注冊信息的生命周期(添加、清除、續約)
3. com.netflix.eureka.registry - 存儲、查詢服務注冊信息
4. com.netflix.eureka.resources - RESTful風格中的”R”, 即資源。相當於SpringMVC中的Controller
5. com.netflix.eureka.transport - 發送HTTP請求的客戶端,如發送心跳
6. com.netflix.eureka.aws - 與amazon AWS服務相關的類
eureka-client模塊:
Eureka客戶端,微服務通過該客戶端與Eureka進行通訊,屏蔽了通訊細節
eureka-server模塊:
包含了 servlet 應用的基本配置,如 web.xml。構建成功后在該模塊下會生成可部署的war包。
2. 代碼入口
2.1 作為純Servlet應用的入口
由於是Servlet應用,所以Eureka需要通過servlet的相關監聽器 ServletContextListener
嵌入到 Servlet 的生命周期中。EurekaBootStrap
類實現了該接口,在servlet標准的contextInitialized()
方法中完成了初始化工作:
@Override
public void contextInitialized(ServletContextEvent event) { try { // 讀取配置信息 initEurekaEnvironment(); // 初始化Eureka Client(用來與其它節點進行同步) // 初始化server initEurekaServerContext(); ServletContext sc = event.getServletContext(); sc.setAttribute(EurekaServerContext.class.getName(), serverContext); } catch (Throwable e) { logger.error("Cannot bootstrap eureka server :", e); throw new RuntimeException("Cannot bootstrap eureka server :", e); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
2.2 與Spring Cloud結合的膠水代碼
Eureka是一個純正的Servlet應用,而Spring Boot使用的是嵌入式Tomcat, 因此就需要一定的膠水代碼讓Eureka跑在Embedded Tomcat中。這部分工作是在 EurekaServerBootstrap
中完成的。與上面提到的EurekaBootStrap
相比,它的代碼幾乎是直接將原生代碼copy過來的,雖然它並沒有繼承 ServletContextListener
, 但是相應的生命周期方法都還在,然后添加了@Configuration
注解使之能被Spring容器感知:
原生的 EurekaBootStrap
類實現了標准的ServletContextListener
接口
Spring Cloud的EurekaServerBootstrap
類沒有實現servlet接口,但是保留了接口方法的完整實現
我們可以推測,框架一定是在某處調用了這些方法,然后才是執行原生Eureka的啟動邏輯。EurekaServerInitializerConfiguration
類證實了我們的推測。該類實現了 ServletContextAware
(拿到了tomcat的ServletContext對象)、SmartLifecycle
(Spring容器初始化該bean時會調用相應生命周期方法):
@Configuration @CommonsLog public class EurekaServerInitializerConfiguration implements ServletContextAware, SmartLifecycle, Ordered { }
- 1
- 2
- 3
- 4
- 5
在 start()
方法中可以看到
eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
- 1
的調用,也就是說,在Spring容器初始化該組件時,Spring調用其生命周期方法start()
從而觸發了Eureka的啟動。
@Override
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
try { eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext); // 調用 servlet 接口方法手工觸發啟動 log.info("Started Eureka Server"); // ... ... } catch (Exception ex) { // Help! log.error("Could not initialize Eureka servlet context", ex); } } }).start(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
2.3 其它幾個重要的代碼入口
了解以上入口信息后,我們就可以根據自己的需要自行研讀相關的代碼了。這里再提示幾個代碼入口:
1. com.netflix.appinfo.InstanceInfo
類封裝了服務注冊所需的全部信息
2. Eureka Client探測本機IP是通過org.springframework.cloud.commons.util.InetUtils
工具類實現的
3. com.netflix.eureka.resources.ApplicationResource
類相當於Spring MVC中的控制器,是服務的注冊、查詢功能的代碼入口點
3. 可能會被坑的幾處原理
3.1 Eureka的幾處緩存
Eureka的wiki上有一句話,大意是一個服務啟動后最長可能需要2分鍾時間才能被其它服務感知到,但是文檔並沒有解釋為什么會有這2分鍾。其實這是由三處緩存 + 一處延遲造成的。
首先,Eureka對HTTP響應做了緩存。在Eureka的”控制器”類ApplicationResource
的109行可以看到有一行
String payLoad = responseCache.get(cacheKey);
- 1
的調用,該代碼所在的getApplication()
方法的功能是響應客戶端查詢某個服務信息的HTTP請求:
String payLoad = responseCache.get(cacheKey); // 從cache中拿響應數據 if (payLoad != null) { logger.debug("Found: {}", appName); return Response.ok(payLoad).build(); } else { logger.debug("Not Found: {}", appName); return Response.status(Status.NOT_FOUND).build(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
上面的代碼中,responseCache
引用的是ResponseCache
類型,該類型是一個接口,其get()
方法首先會去緩存中查詢數據,如果沒有則生成數據返回(即真正去查詢注冊列表),且緩存的有效時間為30s。也就是說,客戶端拿到Eureka的響應並不一定是即時的,大部分時候只是緩存信息。
其次,Eureka Client對已經獲取到的注冊信息也做了30s緩存。即服務通過eureka客戶端第一次查詢到可用服務地址后會將結果緩存,下次再調用時就不會真正向Eureka發起HTTP請求了。
**再次, 負載均衡組件Ribbon也有30s緩存。**Ribbon會從上面提到的Eureka Client獲取服務列表,然后將結果緩存30s。
最后,如果你並不是在Spring Cloud環境下使用這些組件(Eureka, Ribbon),你的服務啟動后並不會馬上向Eureka注冊,而是需要等到第一次發送心跳請求時才會注冊。心跳請求的發送間隔也是30s。(Spring Cloud對此做了修改,服務啟動后會馬上注冊)
以上這四個30秒正是官方wiki上寫服務注冊最長需要2分鍾的原因。
3.2 服務注冊信息不會被二次傳播
如果Eureka A的peer指向了B, B的peer指向了C,那么當服務向A注冊時,B中會有該服務的注冊信息,但是C中沒有。也就是說,如果你希望只要向一台Eureka注冊其它所有實例都能得到注冊信息,那么就必須把其它所有節點都配置到當前Eureka的peer
屬性中。這一邏輯是在PeerAwareInstanceRegistryImpl#replicateToPeers()
方法中實現的:
private void replicateToPeers(Action action, String appName, String id, InstanceInfo info /* optional */, InstanceStatus newStatus /* optional */, boolean isReplication) { Stopwatch tracer = action.getTimer().start(); try { if (isReplication) { numberOfReplicationsLastMin.increment(); } // 如果這條注冊信息是其它Eureka同步過的則不會再繼續傳播給自己的peer節點 if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) { return; } for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { // 不要向自己發同步請求 if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) { continue; } replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node); } } finally { tracer.stop(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
3.3 多網卡環境下的IP選擇問題
如果服務部署的機器上安裝了多塊網卡,它們分別對應IP地址A, B, C,此時:
Eureka會選擇IP合法(標准ipv4地址)、索引值最小(eth0, eth1中eth0優先)且不在忽略列表中(可在application.properites
中配置忽略哪些網卡)的網卡地址作為服務IP。
這個坑的詳細分析見:http://blog.csdn.net/neosmith/article/details/53126924
4. 作為服務注冊中心,Eureka比Zookeeper好在哪里
著名的CAP理論指出,一個分布式系統不可能同時滿足C(一致性)、A(可用性)和P(分區容錯性)。由於分區容錯性在是分布式系統中必須要保證的,因此我們只能在A和C之間進行權衡。在此Zookeeper保證的是CP, 而Eureka則是AP。
4.1 Zookeeper保證CP
當向注冊中心查詢服務列表時,我們可以容忍注冊中心返回的是幾分鍾以前的注冊信息,但不能接受服務直接down掉不可用。也就是說,服務注冊功能對可用性的要求要高於一致性。但是zk會出現這樣一種情況,當master節點因為網絡故障與其他節點失去聯系時,剩余節點會重新進行leader選舉。問題在於,選舉leader的時間太長,30 ~ 120s, 且選舉期間整個zk集群都是不可用的,這就導致在選舉期間注冊服務癱瘓。在雲部署的環境下,因網絡問題使得zk集群失去master節點是較大概率會發生的事,雖然服務能夠最終恢復,但是漫長的選舉時間導致的注冊長期不可用是不能容忍的。
4.2 Eureka保證AP
Eureka看明白了這一點,因此在設計時就優先保證可用性。Eureka各個節點都是平等的,幾個節點掛掉不會影響正常節點的工作,剩余的節點依然可以提供注冊和查詢服務。而Eureka的客戶端在向某個Eureka注冊或時如果發現連接失敗,則會自動切換至其它節點,只要有一台Eureka還在,就能保證注冊服務可用(保證可用性),只不過查到的信息可能不是最新的(不保證強一致性)。除此之外,Eureka還有一種自我保護機制,如果在15分鍾內超過85%的節點都沒有正常的心跳,那么Eureka就認為客戶端與注冊中心出現了網絡故障,此時會出現以下幾種情況:
1. Eureka不再從注冊列表中移除因為長時間沒收到心跳而應該過期的服務
2. Eureka仍然能夠接受新服務的注冊和查詢請求,但是不會被同步到其它節點上(即保證當前節點依然可用)
3. 當網絡穩定時,當前實例新的注冊信息會被同步到其它節點中
因此, Eureka可以很好的應對因網絡故障導致部分節點失去聯系的情況,而不會像zookeeper那樣使整個注冊服務癱瘓。
5. 總結
Eureka作為單純的服務注冊中心來說要比zookeeper更加“專業”,因為注冊服務更重要的是可用性,我們可以接受短期內達不到一致性的狀況。不過Eureka目前1.X版本的實現是基於servlet的java web應用,它的極限性能肯定會受到影響。期待正在開發之中的2.X版本能夠從servlet中獨立出來成為單獨可部署執行的服務。