Eureka高可用架構
https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance
上圖中主要的名稱說明:
- Register:EurekaClient注冊(Http請求)到EurekaServer,EurekaClient會發送自己元數據(ip,port,主頁等),EurekaServer會將其添加到服務注冊列表ConcurrentHashMap里
- Renew:EurekaClient默認每30秒發送心跳(timer定時任務,發送Http請求)到EurekaServer續約,向EurekaServer證明其活性,EurekaServer將EurekaClient心跳中的時間戳參數與已有服務列表中對應的該服務的時間戳進行比較,不相等就更新對應的服務列表;如果EurekaServer 90秒都沒收到某個EurekaClient的續約,並且沒有進入保護模式,就會將該服務從服務列表將其剔除(Eviction)
- Get Registry:EurekaClient默認每30秒從EurekaServer獲取服務注冊列表,並且會與自身本地已經緩存過的服務列表進行比較合並,有點像本地倉庫從git倉庫進行git pull
- Cancel:EurekaClient服務的下線
- Make Remote Call:EurekaClient服務間進行遠程調用,比如通過RestTemplate+Ribbon或Fegin
- us-east-1c:美國東海岸EurekaServer
- Replicate:EurekaServer集群節點之間數據(主要是服務注冊列表信息)同步
Eureka源碼
1. 是純正的 servlet 應用,需構建成war包部署
2. 使用了 Jersey 框架(如注解@POST、@Consumers)實現自身的 RESTful HTTP接口
3. peer之間的同步與服務的注冊全部通過 HTTP 協議實現
4. 定時任務(發送心跳續約、定時清理過期服務Eviction、節點同步等)通過 JDK 自帶的 Timer 實現
5. 內存緩存使用Google的guava包實現
EurekaServer初始化
由於是Servlet應用,所以Eureka需要通過servlet的相關監聽器 ServletContextListener 嵌入到 Servlet 的生命周期中。EurekaBootStrap 類實現了該接口,在servlet標准的contextInitialized()方法中完成了EurekaServer初始化工作:
/** * Initializes Eureka, including syncing up with other Eureka peers and publishing the registry. * * @see * javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent) */ @Override public void contextInitialized(ServletContextEvent event) { try { initEurekaEnvironment(); 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); } }
EurekaBootStrap實現了ServletContextListener接口,重寫了contextInitialized方法;
initEurekaEnvironment主要做的是讀取配置信息,設置eureka的數據中心datacenter和環境environment;
initEurekaServerContext主要是初始化servlet代碼如下:
protected void initEurekaServerContext() throws Exception { ...//省略代碼 PeerAwareInstanceRegistry registry; if (isAws(applicationInfoManager.getInfo())) { ...//省略代碼,如果是AWS的代碼 } else { registry = new PeerAwareInstanceRegistryImpl( eurekaServerConfig, eurekaClient.getEurekaClientConfig(), serverCodecs, eurekaClient ); } PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes( registry, eurekaServerConfig, eurekaClient.getEurekaClientConfig(), serverCodecs, applicationInfoManager ); }
與Spring Cloud結合的膠水代碼
Eureka是一個純正的Servlet應用,而Spring Boot使用的是嵌入式Tomcat, 因此就需要一定的膠水代碼讓Eureka跑在Embedded Tomcat中。
這部分工作是在 EurekaServerBootstrap 中完成的。
與上面提到的EurekaBootStrap相比,它的代碼幾乎是直接將原生代碼copy過來的,雖然它並沒有繼承 ServletContextListener, 但是相應的生命周期方法都還在,然后添加了@Configuration注解使之能被Spring容器感知:
原生的 EurekaBootStrap
類實現了標准的ServletContextListener
接口,Spring Cloud的EurekaServerBootstrap
類沒有實現servlet接口,但是保留了接口方法的完整實現。
在類EurekaServerInitializerConfiguration中實現了 ServletContextAware
(拿到了tomcat的ServletContext對象)、SmartLifecycle
(Spring容器初始化該bean時會調用相應生命周期方法):
@Configuration @CommonsLog public class EurekaServerInitializerConfiguration implements ServletContextAware, SmartLifecycle, Ordered { }
在 start()
方法中:
eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
在SpringCloud中,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. com.netflix.appinfo.InstanceInfo類封裝了服務注冊所需的全部信息
2. Eureka Client探測本機IP是通過org.springframework.cloud.commons.util.InetUtils工具類實現的
3. com.netflix.eureka.resources.ApplicationResource類相當於Spring MVC中的控制器,是服務的注冊、查詢功能的代碼入口點
一個服務啟動后最長可能需要2分鍾時間才能被其它服務感知到
當一個服務A啟動后,首先需要注冊到某個EurekaServer集群節點上,已經注冊到EurekaServer上的服務B此時想要Make Remote Call服務A,最長可能要等兩分鍾,這是因為三處緩存和一處延遲:
1. Eureka Client對已經獲取到的本地的注冊信息也做了30s緩存。
即服務通過eureka客戶端第一次查詢服務注冊信息會請求EurekaServer,然后會將請求返回的結果緩存,下次再調用時就不會真正向Eureka發起HTTP請求了,直接查詢本地的服務注冊信息
2. 負載均衡組件Ribbon也有30s緩存
Ribbon會從Eureka Client本地獲取服務列表,然后將結果緩存30s
3. EurekaServer對HTTP響應做了緩存。在EurekaServer的”控制器”類 ApplicationResource的109行可以看到有一行
String payLoad = responseCache.get(cacheKey);
的調用,該代碼所在的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(); }
responseCache引用的是ResponseCache類型,該類型是一個接口,其get()方法首先會去緩存中查詢數據,如果沒有則生成數據返回(即真正去查詢注冊列表),且緩存的有效時間為30s。
也就是說,客戶端拿到Eureka的響應並不一定是即時的,大部分時候只是緩存信息。
4. 如果你並不是在Spring Cloud環境下使用這些組件(Eureka, Ribbon),你的服務一啟動(不是啟動完成)並不會馬上向Eureka注冊,而是需要等到第一次發送心跳請求時才會注冊
心跳請求的發送間隔也是30s。(Spring Cloud對此做了修改,服務啟動后會馬上注冊)
Eureka保護機制
https://www.cnblogs.com/theRhyme/p/10058988.html
作為注冊中心,Eureka比Zookeeper好在哪里
Zookeeper保證CP
注冊中心需求分析及關鍵設計考量: https://www.cnblogs.com/theRhyme/p/10130714.html
當向注冊中心查詢服務列表時,我們可以容忍注冊中心返回的是幾分鍾以前的注冊信息,但不能接受服務直接down掉不可用。
也就是說,服務注冊功能對可用性的要求要高於一致性。
但是zk會出現這樣一種情況,當master節點因為網絡故障與其他節點失去聯系時,剩余節點會重新進行leader選舉。問題在於,選舉leader的時間太長,30 ~ 120s, 且選舉期間整個zk集群都是不可用的,這就導致在選舉期間注冊服務癱瘓。
在雲部署的環境下,因網絡問題使得zk集群失去master節點是較大概率會發生的事,雖然服務能夠最終恢復,但是漫長的選舉時間導致的注冊長期不可用是不能容忍的。
Eureka保證AP
Eureka優先保證可用性。Eureka各個節點都是平等的,幾個節點掛掉不會影響正常節點的工作,剩余的節點依然可以提供注冊和查詢服務。
而Eureka的客戶端在向某個Eureka注冊或時如果發現連接失敗,則會自動切換至其它節點,只要有一台Eureka還在,就能保證注冊服務可用(保證可用性),只不過查到的信息可能不是最新的(不保證強一致性)。
除此之外,Eureka還有一種自我保護機制,如果在15分鍾內超過85%的節點都沒有正常的心跳,那么Eureka就認為客戶端與注冊中心出現了網絡故障,此時會出現以下幾種情況:
1. Eureka不再從注冊列表中移除因為長時間沒收到心跳而應該過期的服務
2. Eureka仍然能夠接受新服務的注冊和查詢請求,但是不會被同步到其它節點上(即保證當前節點依然可用)
3. 當網絡穩定時,當前實例新的注冊信息會被同步到其它節點中
因此, Eureka可以很好的應對因網絡故障導致部分節點失去聯系的情況,而不會像zookeeper那樣使整個注冊服務癱瘓。
就算Eureka節點一個都不可用,還有緩存的服務列表,如EurekaClient本地的服務列表。
Service Health Check
使用 ZooKeeper 作為服務注冊中心時,服務的健康檢測常利用 ZooKeeper 的 Session 活性 Track機制 以及結合 Ephemeral ZNode的機制,簡單而言,就是將服務的健康監測綁定在了 ZooKeeper 對於 Session 的健康監測上,或者說綁定在TCP長鏈接活性探測上了。
這在很多時候也會造成致命的問題,ZK 與服務提供者機器之間的TCP長鏈接活性探測正常的時候,該服務就是健康的么?答案當然是否定的!注冊中心應該提供更豐富的健康監測方案,服務的健康與否的邏輯應該開放給服務提供方自己定義,而不是一刀切搞成了 TCP 活性檢測!
健康檢測的一大基本設計原則就是盡可能真實的反饋服務本身的真實健康狀態,否則一個不敢被服務調用者相信的健康狀態判定結果還不如沒有健康檢測。
A服務是如何訪問到B服務的?
首先A和B是注冊到EurekaServer上的,每隔30秒會從Eureka注冊中心拉取一次服務列表進行比對合並,注冊時服務會將自己的元數據比如ip,port,服務名稱注冊到EurekaServer;
所以A訪問B服務是通過服務列表提供的ip,port,服務名訪問的。
這里是EurekaClient服務自身通過每隔30s從注冊中心拉取服務列表;
而Dubbo的模式是消費者向注冊中心訂閱服務列表,如果訂閱的服務列表變動,注冊中心將基於長連接推送變更數據給消費者
參考來源:
https://blog.csdn.net/forezp/article/details/73017664
https://blog.csdn.net/neosmith/article/details/53131023
https://yq.aliyun.com/articles/601745?spm=a2c4e.11153940.blogcont604028.19.6daf2a38OLvUBo