上一篇使用了Eureka與Ribbon組件做了最簡單的的服務註冊與發現,我們知道Eureka是實現服務治理中心的組件,但是上一篇Eureka沒有實現集群,這樣沒有保證到Eureka Server的高可用。 理論上來講,因為服務消費者本地緩存了服務提供者的地址,即使Eureka Server宕機,也不 ...
上一篇使用了Eureka與Ribbon組件做了最簡單的的服務註冊與發現,我們知道Eureka是實現服務治理中心的組件,但是上一篇Eureka沒有實現集群,這樣沒有保證到Eureka Server的高可用。
理論上來講,因為服務消費者本地緩存了服務提供者的地址,即使Eureka Server宕機,也不會影響服務之間的調用,但是一旦新服務上線,已經在緩存在本地的服務提供者不可用了,服務消費者也無法知道,所以保證Eureka Server的高可用還是很有必要的。
那我們先來搭建一個Eureka的集群
一、Eureka集群配置
要做集群,我們想到肯定是增加一臺伺服器,那怎麼讓伺服器之間產生關係,先讓他們相互註冊,在修改之前,我們為了區分伺服器名稱,先修改下hosts文件,增加下麵一段:
127.0.0.1 eureka1.com
127.0.0.1 eureka2.com
然後修改下之前spring-cloud-learn-eureka 項目的配置文件,主要修改的是註冊的地址:
spring:
application:
name: spring-cloud-learn-eureka
server:
port: 8761
eureka:
instance:
hostname: eureka1.com
client:
#表示是否將自己註冊到Eureka Server,預設為true。
registerWithEureka: false
#表示是否從Eureka Server獲取註冊信息,預設為true。
fetchRegistry: false
serviceUrl:
defaultZone: http://eureka2.com:8762/eureka/
這個時候我們啟動此項目,並同時啟動服務提供者spring-cloud-learn-provider-dept,這裡其實跟之前都沒什麼區別,服務順利的註冊進去:
這個時候我們修改spring-cloud-learn-eureka的配置,即再啟動一個eureka server :
spring:
application:
name: spring-cloud-learn-eureka
server:
port: 8762
eureka:
instance:
hostname: eureka2.com
client:
#表示是否將自己註冊到Eureka Server,預設為true。
registerWithEureka: false
#表示是否從Eureka Server獲取註冊信息,預設為true。
fetchRegistry: false
serviceUrl:
defaultZone: http://eureka1.com:8761/eureka/
這個時候我們發現,第二台服務也註冊進來了,是因為這兩個Eureka伺服器互相同步信息,這樣就已經完成集群了。
接著實現,這個時候我們停掉eureka1服務,那eureka2中依然有服務,這說明掛掉一臺伺服器註冊服務仍然可用,那我們在想想,如果我們重啟服務提供者spring-cloud-learn-provider-dept會發生什麼呢?
啟動的時候,我們可以看到控制台是會報錯誤的,因為在spring-cloud-learn-provider-dept我們只向eureka1進行了註冊,那此時eureka1停掉之後找不到註冊地址,就會報錯,但是只是刷新http://eureka2.com:8762/,我們發現此時服務提供者依然存在,這個是因為的自我保護機制。
如果我們重啟eureka2服務,那這個時候就會發現此時spring-cloud-learn-provider-dept 就沒有了,這個也很好理解,在啟動時兩個服務會互相同步消息,而這個時候服務提供者未註冊到eureka1,那eureka2啟動也是沒有效果的,為瞭解決這個問題,那就讓服務提供者分別向兩台eureka伺服器進行註冊:
spring:
application:
name: spring-cloud-learn-provider-dept
server:
port: 8763
eureka:
client:
serviceUrl:
defaultZone: http://eureka1.com:8761/eureka/,http://eureka2.com:8762/eureka/
Eureka與ZooKeeper的主要區別在CAP原則的選擇上,CAP原則是指的是在一個分散式系統中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區容錯性),三者不可兼得(我們常說的魚和熊掌不可兼得)。CAP 原則也是 NoSQL 資料庫的基石。
-
一致性(Consistency,C):在分散式系統中的所有數據備份,在同一時刻是否同樣的值。(等同於所有節點訪問同一份最新的數據副本)。
-
可用性(Availability,A):在一個分散式系統的集群中一部分節點故障後,該集群是否還能夠正常響應客戶端的讀寫請求。(對數據更新具備高可用性)。
-
分區容錯性(Partition tolerance,P):大多數的分散式系統都分佈在多個子網路中,而每個子網路就叫做一個區(partition)。分區容錯的意思是,區間通信可能失敗。在一個分散式系統中一般分區容錯是無法避免的,因此可以認為 CAP 中的 P 總是成立的。CAP 理論告訴我們,在 C 和 A 之間是無法同時做到。
這個時候Eureka選擇了 AP,在剛剛的集群配置中,Eureka Server 採用的是Peer to Peer 對等通信。這是一種去中心化的架構(參看:微服務與微服務架構思想與原則),無 master/slave 之分,每一個 Peer 都是對等的。在這種架構風格中,節點通過彼此互相註冊來提高可用性,每個節點需要添加一個或多個有效的 serviceUrl 指向其他節點。每個節點都可被視為其他節點的副本。
在集群環境中如果某台 Eureka Server 宕機,Eureka Client 的請求會自動切換到新的 Eureka Server 節點上,當宕機的伺服器重新恢復後,Eureka 會再次將其納入到伺服器集群管理之中。當節點開始接受客戶端請求時,所有的操作都會在節點間進行複製(replicate To Peer)操作,將請求複製到該 Eureka Server 當前所知的其它所有節點中。
當一個新的 Eureka Server 節點啟動後,會首先嘗試從鄰近節點獲取所有註冊列表信息,並完成初始化。Eureka Server 通過 getEurekaServiceUrls() 方法獲取所有的節點,並且會通過心跳契約的方式定期更新。預設情況下,如果 Eureka Server 在一定時間內沒有接收到某個服務實例的心跳(預設周期為30秒),Eureka Server 將會註銷該實例(預設為90秒,如果某個 eureka.instance.lease-expiration-duration-in-seconds 進行自定義配置)。當 Eureka Server 節點在短時間內丟失過多的心跳時,那麼這個節點就會進入自我保護模式。
與 Eureka 有所不同,Zookeeper 在設計時就緊遵CP原則,即任何時候對 Zookeeper 的訪問請求能得到一致的數據結果,同時系統對網路分割具備容錯性,但是 Zookeeper 不能保證每次服務請求都是可達的。從 Zookeeper 的實際應用情況來看,在使用 Zookeeper 獲取服務列表時,如果此時的 Zookeeper 集群中的 Leader 宕機了,該集群就要進行 Leader 的選舉,又或者 Zookeeper 集群中半數以上伺服器節點不可用(例如有三個節點,如果節點一檢測到節點三掛了 ,節點二也檢測到節點三掛了,那這個節點才算是真的掛了),那麼將無法處理該請求。所以說,Zookeeper 不能保證服務可用性。
當然,在大多數分散式環境中,尤其是涉及到數據存儲的場景,數據一致性應該是首先被保證的,這也是 Zookeeper 設計緊遵CP原則的另一個原因。但是對於服務發現來說,情況就不太一樣了,針對同一個服務,即使註冊中心的不同節點保存的服務提供者信息不盡相同,也並不會造成災難性的後果。因為對於服務消費者來說,能消費才是最重要的,消費者雖然拿到可能不正確的服務實例信息後嘗試消費一下,也要勝過因為無法獲取實例信息而不去消費,導致系統異常要好。
三、Ribbon介紹
Ribbon是負責客戶端負載均衡的工具,與Nginx的作用類似,負載均衡應該大部分開發都是知道的,不清楚的百度學習一波。
簡單的說,Ribbon是Netflix發佈的開源項目,主要功能是提供客戶端的軟體負載均衡演算法,將Netflix的中間層服務連接在一起。Ribbon客戶端組件提供一系列完善的配置項如連接超時,重試等。簡單的說,就是在配置文件中列出Load Balancer(簡稱LB)後面所有的機器,Ribbon會自動的幫助你基於某種規則(如簡單輪詢,隨機連接等)去連接這些機器。我們也很容易使用Ribbon實現自定義的負載均衡演算法。
Ribbon的配置在上一篇中已經給出,實現也是非常的簡單,主要看幾種負載均衡演算法:
策略描述 | 實現說明 | |
---|---|---|
BestAvailableRule | 選擇一個最小的併發請求的server | 逐個考察Server,如果Server被tripped了,則忽略,在選擇其中ActiveRequestsCount最小的server |
AvailabilityFilteringRule | 過濾掉那些因為一直連接失敗的被標記為circuit tripped的後端server,並過濾掉那些高併發的的後端server(active connections 超過配置的閾值) | 使用一個AvailabilityPredicate來包含過濾server的邏輯,其實就就是檢查status里記錄的各個server的運行狀態 |
WeightedResponseTimeRule | 根據相應時間分配一個weight,相應時間越長,weight越小,被選中的可能性越低。 | 一個後臺線程定期的從status裡面讀取評價響應時間,為每個server計算一個weight。Weight的計算也比較簡單responsetime 減去每個server自己平均的responsetime是server的權重。當剛開始運行,沒有形成statas時,使用roubine策略選擇server。 |
RetryRule | 對選定的負載均衡策略機上重試機制。 | 在一個配置時間段內當選擇server不成功,則一直嘗試使用subRule的方式選擇一個可用的server |
RoundRobinRule | roundRobin方式輪詢選擇server | 輪詢index,選擇index對應位置的server |
RandomRule | 隨機選擇一個server | 在index上隨機,選擇index對應位置的server |
ZoneAvoidanceRule | 複合判斷server所在區域的性能和server的可用性選擇server | 使用ZoneAvoidancePredicate和AvailabilityPredicate來判斷是否選擇某個server,前一個判斷判定一個zone的運行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用於過濾掉連接數過多的Server。 |
當想要修改負載均衡的策略時,直接返回IRule實現即可,例:
@Bean public IRule myRule(){ return new RandomRule(); }
Feign 是一個聲明式的偽 Http 客戶端,它使得寫 Http 客戶端變得更簡單。使用 Feign,只需要創建一個介面並註解。它具有可插拔的註解特性,可使用 Feign 註解和 JAX-RS 註解。Feign 支持可插拔的編碼器和解碼器。Feign 預設集成了 Ribbon,並和 Eureka 結合,預設實現了負載均衡的效果。
Feign旨在使編寫Java Http客戶端變得更容易。前面在使用Ribbon+RestTemplate時,利用RestTemplate對http請求的封裝處理,形成了一套模版化的調用方法。但是在實際開發中,由於對服務依賴的調用可能不止一處,往往一個介面會被多處調用,所以通常都會針對每個微服務自行封裝一些客戶端類來包裝這些依賴服務的調用。所以,Feign在此基礎上做了進一步封裝,由他來幫助我們定義和實現依賴服務介面的定義。在Feign的實現下,我們只需創建一個介面並使用註解的方式來配置它(以前是Dao介面上面標註Mapper註解,現在是一個微服務介面上面標註一個Feign註解即可),即可完成對服務提供方的介面綁定,簡化了使用Spring cloud Ribbon時,自動封裝服務調用客戶端的開發量。
來實踐感受下:跟之前一樣再創建一個項目spring-cloud-learn-consumer-dept-feign,並新建pom.xml文件,手動添加到maven中:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.yuanqinnan</groupId> <artifactId>spring-cloud-learn-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>spring-cloud-learn-consumer-dept-feign</artifactId> <packaging>jar</packaging> <dependencies> <!-- Spring Boot Begin --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Boot End --> <!-- Spring Cloud Begin --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- Spring Cloud End --> </dependencies> </project>
創建啟動類:
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class ConsumerDeptFeignApplication { public static void main(String[] args) { SpringApplication.run(ConsumerDeptFeignApplication.class, args); } }
application.yml:
spring:
application:
name: spring-cloud-learn-consumer-dept-feign
server:
port: 8765
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
現在就是調用服務的操作,添加一個部門服務:
@FeignClient(value = "spring-cloud-learn-provider-dept") public interface DeptService { @RequestMapping(value = "hi", method = RequestMethod.GET) String sayHi(@RequestParam(value = "message") String message); }
這裡添加的@FeignClient的註解裡面的value屬性就是調用服務的名稱,而裡面的方法的寫法與WebMVC的寫法很類似,相當於調用服務的方法
然後再創建一個controller,,然後創建方法調用
@RestController public class DeptController { @Autowired private DeptService deptService; @RequestMapping(value = "hi", method = RequestMethod.GET) public String sayHi(@RequestParam String message) { return deptService.sayHi(message); } }