javascript
Spring Cloud部分源码分析Eureka,Ribbon,Feign,Zuul
Eureka
- SpringCloud Eureka使用NetFlix Eureka來實現的,它包括了服務端組件和客戶端組件,并且都是用java 編寫的。 Eureka服務端就是服務注冊中心,
 - Eureka客戶端主要處理服務的注冊發現,通過注解和參數配置的方式,客戶端向注冊中心注冊,并且周期性的發送心跳維護一個可用列表。
 - 服務注冊中心,用戶服務注冊發現功能。
 
- SpringCloud的功能都是依賴NetFlixEureka中的方法完成的,下面就依次介紹對于的所有功能:
 
- 如上圖所式,左邊的DiscoveryClient是一個接口,是SPringCloud定義的一個服務發現所需要用到的常用抽象方法,其中EurekaDiscoveryClient是其中的一個實現類,該類關聯了NetFlix Eurkea中的一個EurekaClient類,通過這個類來實現服務發現功能,
 - 在netFlixEreka中有幾個用來獲取服務列表的方法:
 
- 這三個方法都是獲取當前服務列表的方法:取其中一個getServiceUrlsFromConfig分析:
 
- 它先通過配置信息中獲取對于的Region和Zone信息,然后通過對應的Zone和一定的算法來確認加載位于哪個Zone的配置ServiceUrl,由此處可以看出來Eureka是優先返回本服務所在Zone的Url列表信息。具體的算法是在EurekaClientConfigBean類中有一個getEurekaServiceServiceUrls方法完成。之后會通過一個簡單的算法,將其他Zone中的url信息加入到URLs列表中,最后返回對應的列表信息,如果遍歷所有zone之后都沒有獲取到對應的url則會拋出IllegalArguementException
 
- Eureka的注冊也是在NetFlix的這個類中完成的,其中有一個很明顯的方法叫做register方法,當時并不是直接調用這個方法進行服務的注冊,是通過另外一種途徑,其中在DiscoveryClient類中有一個構造方法,這個構造方法調用本類中的一個InitScheduleTasks()方法。該方法中有一個這樣的判斷
 - 依據方法命名可以推斷出,這個方法用來判斷是否需要注冊到eurka中,中個方法中創建了NetFlix 的InstanceInfoReplicator的一個實例,這個類繼承了Runnable接口是一個線程,該線程中定義類一個線程池,實用的是ScheduleExecutorService,她會執行一個定時任務,這個定時任務的作用就是定時去執行本線程,
 - 具體作用可以看到它的run()方法中來得知,其中有一個DiscoveryClient.register()方法,其中Registe方法中是通過發送rest的請求的方式進行注冊操作。
 - 因此整個注冊過程是先通過定時任務定時去執行這個線程,然后通過判斷現有狀態來決定是否需要注冊服務。
 
- 服務續約是和服務注冊也是在這個方法中的另外兩個定時任務執行的。其中TimeedSuPerVisorTask是和服務注冊在同一個判斷分支中,但是同服務獲取的參數不同的是其中的配置信息,還有第一個參數,服務續約功能的第一個參數“hearbeat”從這個參數可以看出,是通過這個定時任務來定時發送心跳,來達到服務續約的目的,最終實也是通過rest請求的方式來達到續約的目的。而定時任務中其中另外一個參數:timeOut是從配置中讀取過來的作為發送心跳的時間也就是這個定任務執行的時間,這個配置如下:
 
- 服務獲取還是在剛才的InitScheduledTasks方法中還有另外一個判斷分支:
 
- 可以看到其中也有一個定時任務代碼如下: 
- 定時任務還是利用ScheduleExecutorService,來作為定時任務執行器,定義了一個線程池TimeedSuPerVisorTask, 定時執行的實踐是從配置文件中獲取得來的renewaLintervalInSecs,最后的參數是定時任務執行時間單位。 這個定時任務的具體任務是做了服務獲取的工作,服務獲取之所以放到另外一個if條件中是因為這個功能有一個參數配置來限制此IF條件是否執行:
 
 
- 其實這個參數是默認True,所以一般不配置
 - 有上面三個可以得出,Eureka的服務發現,注冊,獲取,續約都是通過rest請求的方式來獲取的。
 
-  
SpringCloud體系都是通過rest請求的方式交互,服務注冊中心就是處理這些rest請求的作用,都在com.netflix.eureka.resources中
4.1.服務注冊請求:
 -  
在ApplicationResource類中有一個addInstance方法,首先會判斷配置是否正確,之后會調用InstanceRegistry類中的register方法來進行服務注冊。它線會調用一個publishEvent函數,之后調用父類AbstractInstanceRegistry中的register方法進注冊實現從源碼中可以看到,服務列表是存儲在一個雙層Map中
 
- 第一層Map中的Key存儲的AppName引用名稱:
 
- 第二層Map中的可以存儲的InstanceInfo中的id
 
4.2. 服務獲取請求
- 從InstanceResource類中可以找到getInstanceInfo這個方法可以可以看到他是通過appName以及infoid來回去對于的節點列表信息:
 
?
- 最終是調用的AbstractInstanceResource類中的getApplicationByAppAndId,從雙層數組中獲取對應的信息:
 
- 服務提供者:(也可能是消費者)
 - 服務注冊:通過rest請求并且帶上元數據信息到server
 - 服務同步:不同eureka在同一個集群中會相互復制可用的節點信息。
 - 服務續約:定時任務發送心跳給eureka server
 
5.1 服務消費者(也可能是提供者):
- 獲取服務:啟動時候通過rest請求該注冊中心獲取服務清單,清單30跟新一次,可配置
 - 服務調用:通過ribbon輪訓配置中節點實現負載。
 - 服務下線:服務正常關閉會發送一個下線的rest請求給service 通過并且狀態變為DOWN
 
5.2服務注冊中心:
- 失效剔除:沒收到下線請求但是沒有收到續約定時任務發出的請求回復,認定下線,時間可配
 - 自我保護:當15分鐘內心跳失敗比利低于85% 那么就觸發自我保護,也就是將現有注冊列表中的信息保護起來不做30更新,這樣就會有節點下線而不被剔除的情況而調用失敗。測試環境可以加上eureka.server.enable-self-preservation=false
 
SpringCloud Ribbon
-  
一般說的負載均衡都是服務器端的負載均衡,其實負載均衡分為硬件負載(F5)和軟件負載(nginx),不管是那哪種,都需要維護一個可用的服務列表,然后請求過來之后用一定的算法去列表中選一臺服務做轉發。
 -  
而客戶端的負載均衡和服務器負載均衡最大的不同是列表是維護在客戶端這邊。而這個清單來自于一個注冊中心,比如Eureka就是一個服務注冊中心,客戶端負載均衡和服務器端類似也是通過心跳去維護服務清單的健康,只不是這個步驟需要和Eureka配合完成。
 
- 下面通過Ribbon的源碼分析,Ribbon如何實現客戶端負載均衡, 在使用Ribbon的時候會用一個@LoadBalanced,通過這個可以找到具體loadBalanced所在的包位置,之后查看對于包下類,可以看到有一個LoadBalancedClient接口,這個接口定義類負載均衡應該具備的一些功能:
 - 方法Choose:根據傳入的serviceId參數選擇服務,兩個execute使用從choose負載均衡器中選出來的服務執行請求內容,方法reconstructUrl:通過ServiceId以及對于的服務列表中的信息以及配置信息,將具體的ServiceInstance轉為Host:port形式。
 - 對主要的類進行整理:
 
通過整理對應包中的類, 得到以上類圖,從中可以看到LoadBalancerAutoConfiguration按類名稱來猜測應該是和自動化配置相關,看其源碼可以知道,Robbin實現負載均衡的先決條件:
創建LoadBalancerInterceptor Bean用來做為請求的攔截器
創建一個RestTemplateCustomizer Bean用來給RestTemplate類型的請求加上攔截器
- 以上攔截器的作用在于當一個被@LoadBalanced注解修飾的RestTemplate對象向外發起Http請求的時候會被該類中的intercept函數攔截,因為我門使用RestTemplate時候都是用服務名字作為Host所有可以直接從URI中得到,之后調用executor。
 - Executor:
 
- 最終調用的executor方法首先通過getServer方法獲取到對象實例server, 此處的getServer的實現并不是調用的LoacBalancerClient中的choose方法,而是調用NetFlix Ribbon自生的ILoadBalancer接口中定義的ChooseServer函數,這個接口有幾個實現類,此處Ribbon調用的是ZoneAwareLoadBalancer類中的實現,在獲取到server之后,會利用serverId以及server對象來生成RibbonServer對象,接著生成ServerInstance(服務實例的抽象,此處等同RibbonServer)類調用最終的apply方法
 - 此方法實現向一個具體服務實例發起請求,從而實現一開始以服務名為Host的URI到Host:port形式的實際訪問地址的轉換。
 
- 負載均衡的最終實現方式: 
- Ribbon的核心接口還是在ILoadBalancer接口,上面以及說過其中定義的接口信息。一下將分析其所有實現,來解析他如何做到客戶端負載均衡:
 - AbstractLoadBalancer他是以上接口的一個抽象類,其中定義了一個枚舉類型定義了服務的三種狀態(All:所有, STATUS_UP存活的實例, STATUS_NOT_UP:停止服務的實例),實現了一個ChooseServer()方法,通過調用接口中的ChooseServer方法實現的
 - BaseLoadBalancer繼承了以上抽象類,可以明顯看到該類中定義并且維護了兩個服務列表,可見一個是所有服務,一個是可用服務。
 - 定義了各種和服務相關的內容,比如IPing檢查服務是否正常,定義負載均衡的處理規則IRule對象,其中有一個關鍵方法chooseServer()方法,底層實現是交給IRule來完成對象的選擇。如下代碼
 
 
- 并且在初始化Ping任務的時候,該類會創建一個定時任務,來檢查Server是否可用,10秒執行一次。用的Timer定時器
 
- DynamicServerListLoadBalancer中的服務篩選攔截器
 
- 類中有一個屬性ServerListFilter其中ServerListFilter是用作篩選的攔截器,有多個實現,如下
 
-  
blackOutServerPerCentage: 故障實例百分比(斷路器斷開數量/實例數量)>=0.8
 -  
activeReqerStsPerServer: 實例平均負載 >=0.6
 -  
availableServers: 可用的實例數量(實例總數-斷路器斷開數量)< 2
 -  
ServerListSubsetFilter:完全繼承ZoneAffinityServerLIstFilter,適合大型集群服務的情況,他是在zone的方式篩選下在一層的篩選,通過固定的方式來做健康檢查,檢查之后的服務列表是最后的列表信息
 -  
ZonePreferenceServerListFilter:通過配置的區域Zone或者Eureka實例元數據所屬的區域來篩選指定服務列表,先通過ZoneAffinityServerListFilter攔截器來過濾得到區域感知的結果,然后在更具配置來篩選
 -  
ZoneAwareLoadBalancer是對DynamixServerListLoadBalancer的擴展可以從ChooseServer源碼可以看到,當Zone的可用個數小于1 的時候,直接調用父類BaseLoadBalancer中的chooseServer方法,此方法逐個輪詢每一個server,當zone個數大于1 的時候,執行選擇策略,會調用ZoneAvoidanceRule中靜態方法getAvailableZones來獲取到可用的Zone區域的集合,當這個zone結合不為空的時隨機選育個zone區域,在確定某一個zone的時候在獲取對應zone的服務均衡器,并且調用IRule接口chooseServer來選擇具體的服務實例,具體實現是在ZoneAvoidanceRule
 -  
以上的負載均衡策略都是針對Zone級別的,也就是先選擇Zone,之后在選擇具體zone中的server,Ribbon實現的服務選擇策略源頭都在IRule 接口,
 
以下分別介紹每個負載均衡規則:
- 可以看到接口中有一個Choose接口
 
- 實際上委托給了另外一個choose方法,其中有一個getLoadBalancer負載均衡對象的參數,次,可以通過這個對象得到可用實例列表,已經所有實例列表,之后通過rand.nextInt(ServerCount)方法從可用列表中選取一個實例
 
- 這個策略的實現和之前的實現類似,只是隨機的時候曾加了一個算法,首先設置了一個線程安全的AtomicInteger,初始化為0 ,之后循環去獲取,如果獲取十次都沒有得到擇提示bug,其中循環算法也有不同如下:
 
- 可以看到,并不是通過所有列表數量來隨機,
 
- 這個策略是實現了一個具有重試機制的選擇功能,具體的選擇是交給了RundRobinRule來完成的,如果得到的具體實例就返回,否則,判斷是否在重試許可范圍內,如果在,的話就再次輪訓調用choose方法。
 
- 這個策略是繼承了RoundRobinRule,但是曾加了一個權重列表的判斷,當權重列表中最后一個小于0.001的時候,直接使用父類中的類篩選策略,如果權重大于0.001會通過權重來篩選,如下:
 
- 權重維護:
 - 這個策略在初始化的時候會調用一個Init方法,這個方法的作用是設置了一個定時器,每30s執行一次,具體執行的內容在內部類ServerWeight類中:他會根據loadBalancerStatus中來獲取實例的響應時間來計算權重信息。
 
- 直接定義了一個RoundRobinRule,調用了choose方法,用來給其他高級策略做最后的備份。
 
- 這個策略注入了負載均衡統計對象LoadBalancerStatus,他通過遍歷負載均衡中維護的所有服務,首先過濾掉故障的實例,然后找出并發請求數量最小的一個,也就是選出最空閑的一個實例,如果負載均衡對象為空或者,過濾之后實例為空,擇默認用父類中的choose方法。
 
- 利用google Guava Collection 中的apply方法對服務進行過濾,過濾apply是一個抽象類,由各個子類實現。之后在對過濾之后的服務列表進行輪詢
 
- 繼承以上的類,但是過濾邏輯不同主要判斷兩個內容:是否故障,也就是斷路器是否打開,實例并發數量是否大于閥值2^32-1,可以通過配置..ActiveConnectionsLimit來修改
 - 參數配置:rebbon可以全局配置也可以正對某個實例配置
 - 和Eureka結合之后:會觸發Eureka 中實現的對Ribbon的自動化配置,ServerList和Iping(服務檢測)都會交給Eureka中的服務治理框架來維護。這時候的配置將更簡單。因為Eureka幫我們維護來服務列表,而且Ribbon默認實現區域親和策略,所有在Eureka上做分區的集群,指定eureka的zone就可以實現闊區域的容錯。
 
CAP
C: 一致性 A:可用性 P:可靠性
Eureka是AP
Zookeeper是CP
SpringCloud Feign
- Feign幫我們整合來SpringCloud Ribbon和SpringCloud Hystrix,我們不用每一個需要調用的接口都封裝一個REstTemplate的配置,只需要定義一個接口并且加上Feign的配置,Feign會自動幫我們完成這些配置的。
 - 優點:在開發當中,我們調用哪個實例的接口,只需要封裝一個Feign修飾的接口,但是如果每個項目都自己定義Feign的調用,十個項目可能就定義十次,因此我門實際開發中為了減少編碼量,我門都是服務提供方提供服務的同時也定義好一個額外的項目API接口,這樣所有服務消費者都可以用引入這個jar的形式來得到對于的API信息。
 - 缺點:接口提供者修改來API,調用者可能項目都會報錯,所有開發要遵循開筆原則。
 
SpringClud Zuul
- Zuul在默認情況下是會過濾請求頭中的敏感信息,比如Cookie,Authorization,但是如果我們需要Zuul和Shiro或者SpringSecurity一起使用的話需要配置:
 - zuul.sensitiveHeaders= 設置全局參數為空來覆蓋默認
 - zuul.routes..customSensitiveHeaders=true
 - 指定路由開啟自定義敏感頭zuul.routes..sensitiveHeaders=將指定路由的敏感頭設置為空。
 
推薦后兩者
Zuul包含來對Ribbon和Hystroy的依賴,三個時間配置:
Hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds //-----斷路器斷開超時配置 Ribbon.ConnectionTimeout//---ribbon連接超時配置可重試 Ribbon.ReadTimeout//—ribbon請求超時配置可重試- Zuul過濾器詳解:
 
- ZuulFilter是一個抽象類,其中有4個抽象方法,我們可以在Zuul的包目錄下看到很大Zuul自定義的一些過濾器:org.springframework.cloud.netflix.zuul.filters
 - ServerDetectionFilter:是最先執行的,執行順序是-3,可以看到沒有更小的類,這個過濾器的作用是用來檢測請求是通過Spring的DispatcherServlet還是ZuulServlet來處理運行。(一般通過網關的外部請求都會被Spring的處理,除了通過zuul/* 路徑訪問的請求會繞過Spring的dispatcherServlet的處理,大文件上傳,可以通過zuul.servletPath參數修改)
 - 還有其他pre包下的一些過濾器都會依次執行 
- Route過濾器:
 - RibbonRoutingFilter 它是執行在route階段,執行順序是10,作用是通過使用Ribbon和hystrix來向服務發起請求。但是只在請求上下文中存在serverId的請求生效。
 - SimpltHostRoutingFilter:route第二階段執行,但是只有在上下文中有RouteHost才執行,也就是配置具體服務路徑的形式,作用是直接向routeHost發起http并沒有用刀hystrix
 - SendForwardFilter: route第三階段對請求上下文中有forward.to參數的請求生效,執行本地跳轉
 - Post過濾器:
 - SendResponseFilter:
 - SendErrorFilter: 當發上面幾個攔截器中發生異常的時候會被他捕獲,并且封裝成異常的格式返回。
 
 - Zuul動態加載: 
- 可以通過配置完成讓zuul可以在不重啟的情況下動態加載配置信息。這個需要SpringCloud config配置中心配合使用,需要在ZuulProperties的bean對象上面加上@RefreshScope標簽,并且將配置文件寫到git中。
 
 - SpringCloud zuul源碼:
 
- 要使用zuul我們用到了一個標簽,@EnableZuulProxy,可以從這個標簽中看到ZuulProxyConfigurateion這個類的注入,從這個類的源碼可以看到:該類中加載了一系列bean對象,首先如果容器中沒有RouteLocator bean的情況下會加載DiscoveryClientRouteLocator,roteLocator是一個接口,其中定義了三個方法:
 
- 可以從具體的實現中可以看出,改接口定義是為了獲取具體的配置路由,其中getRoutes是獲取所有路由信息,getMatchIngRoute是通過請求中的path信息來獲取對應的配置中的路由信息,這個方法可以自定義實現,只需要實現RouteLocator接口就可以實現自定義的路由獲取,以及具體路徑路由的尋找方案。
 - 接下來注入了一系列的Filter,PreDecorationFilter, RibbonRoutingFilter, SimpleHostRoutingFilter,這三個攔截器都是在pre階段生效的。在來看看本類(ZuulProxyConfigurateion)的父類ZuulConfiguration:
 - 注入了Pre階段和一些Post階段的 的攔截器:
 - 其中最重要的是如果在容器中沒有ZuulServlet bean類的話他會注入ZuulServlet類,這個是Zuul的核心類,
 - ZuulServlet:是最核心的類,其中有一個service方法,調用inite方法中可以看到,他為每一個請求生成了一個RequestContext,requestContext繼承了ConcurrentHashMap
 
- 之后調用preroute來調用對應的filter其中的之中實現是
 
- 程序設定首先執行pre類型的攔截器,以上方法是用來獲取這一類的所有攔截器,更具之后的實現可以知道,他最終執行的是ZuulFilter攔截器
 
- 其他的兩個route和postRoute的實現方式也是同樣的道理,在所有Filter的執行過程中他們可以共享同一個RequestContext,就是在Init方法中為每個請求生產的哪個對象,該對象的生命周期貫穿與整個請求,執行一個過濾器之后,他會把結果放到這個對象中,提供給后續的filter使用,以達到后續filter可以更具前面filter的結果來決定是否執行。最后Errorfilter是除了postFilter之外的所有filter異常的時候都會執行。同時也初始化了 ZuulFilterInitializer bean類:
 - 該類的中定義了一個Zuufilter的Map類型的數據,會將所有的filter向filterRegistry注冊,FilterRegistry定義了一個ConcurrentHashMap用來存儲所有的過濾器。并且在這個bean類中定義了一些針對filter的CURD操作SpringCloudZuul中自己定義了一部分Filter去做一些信息的處理,ZuulServer處理請求默認是用的”IOS-8859-1”所以在文件上傳的時候會有亂碼問題。
 
- 當請求過來,ZuulFilter會先執行Pre 類型的攔截器,其中最后一個SpringCloud Zuul定義的Filter是 PreDecorationFilter,這個攔截器會去判斷請求上下文中是否存在forward.to和ServiceId如果都不存在他在回去執行具體過濾器的操作,當然在判斷之前他必須拿到所有的路由規則,他是通過調用routeLocation中的getMatchingRoute方法來獲取當前的路由規則,并且初始化到系統properties中
 
總結
以上是生活随笔為你收集整理的Spring Cloud部分源码分析Eureka,Ribbon,Feign,Zuul的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 怎样利用经期快速减肥
 - 下一篇: 调理脾胃会减肥么