1. 程式人生 > >Spring Cloud Netflix Eureka原始碼導讀與原理分析

Spring Cloud Netflix Eureka原始碼導讀與原理分析

Spring Cloud Netflix技術棧中,Eureka作為服務註冊中心對整個微服務架構起著最核心的整合作用,因此對Eureka還是有很大的必要進行深入研究。

本文主要分為四部分,一是對專案構建的簡要說明;二是對程式入口點的定位,幫助大家找到閱讀原始碼的起點;三是對Eureka實現機制的分析;四是與使用Zookeeper相比Eureka作為註冊服務的區別。

1. 原始碼

1.1 原始碼獲取、構建

在構建 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);
        }
    }

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 {
}

start() 方法中可以看到

eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);

的呼叫,也就是說,在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();
    }

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);

的呼叫,該程式碼所在的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的響應並不一定是即時的,大部分時候只是快取資訊。

其次,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();
        }
    }

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中獨立出來成為單獨可部署執行的服務。