深度解析spring cloud分散式微服務的實現

AIGeorge發表於2020-08-26

分散式系統

微服務就是原來臃腫的專案拆分為多個模組互不關聯。如:按照子服務拆分、資料庫、介面,依次往下就更加細粒度,當然運維也就越來越難受了。

分散式則是偏向與機器將諾大的系統劃分為多個模組部署在不同伺服器上。

微服務和分散式就是作用的“目標不一樣”。

微服務與Cloud

微服務是一種概念,spring-cloud是微服務的實現。

微服務也不一定必須使用cloud來實現,只是微服務中有許多問題,如:負載均衡、服務註冊與發現、路由等等。

而cloud則是將這些處理問題的技術整合了。

Spring-Cloud 元件

Eureka

Eureka是Netifix的子模組之一,Eureka有2個元件,一個EurekaServer 實現中間層伺服器的負載均衡和故障轉移,一個EurekaClient它使得與server互動變得簡單。

Spring-Cloud封裝了Netifix公司開發的Eureka模組來實現服務註冊和發現。

透過Eureka的客戶端 Eureka Server維持心跳連線,維護可以更方便監控各個微服務的執行。

角色關係圖



Eureka使用

客戶端

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server:
port: 4001
eureka:
client:
serviceUrl:
defaultZone: 服務端提供的註冊地址 參考服務端配置的這個路徑
instance:
instance-id: admin-1 #此例項註冊到eureka服務端的唯一的例項ID
prefer-ip-address: true #是否顯示IP地址
leaseRenewalIntervalInSeconds: 10 #eureka客戶需要多長時間傳送心跳給eureka伺服器,表明它仍然活著,預設為30 秒 (與下面配置的單位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka伺服器在接收到例項的最後一次發出的心跳後,需要等待多久才可以將此例項刪除,預設為90秒
spring:
application:
name: server-admin #此例項註冊到eureka服務端的name



服務端

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-
netflix-eureka-server</artifactId>
</dependency>
yml檔案宣告 
server:
port: 3000
eureka:
server:
enable-self-preservation: false #關閉自我保護機制
eviction-interval-timer-in-ms: 4000 #設定清理間隔
(單位:毫秒 預設是60*1000)
instance:
hostname: localhost 
client:
registerWithEureka: false #不把自己作為一個客戶端註冊到自己身上
fetchRegistry: false #不需要從服務端獲取註冊資訊
(因為在這裡自己就是服務端,而且已經禁用自己註冊了)
serviceUrl:
defaultZone: {eureka.instance.hostname}:${server.port}/eureka
在SpringBoot 啟動專案中加入註解:@EnableEurekaServer 
就可以啟動專案了,訪問對應地址就可以看到介面。



Eureka 叢集

服務啟動後Eureka Server會向其他服務server 同步,當消費者要呼叫服務提供者,則向服務註冊中心獲取服務提供者的地址,然後將提供者的地址快取到本地,下次呼叫時候直接從本地快取中獲取

yml 服務端

server:
port: 3000
eureka:
server:
enable-self-preservation: false #關閉自我保護機制
eviction-interval-timer-in-ms: 4000 #設定清理間隔
(單位:毫秒 預設是60*1000)
instance:
hostname: eureka3000.com 
client:
registerWithEureka: false #不把自己作為一個客戶端
註冊到自己身上
fetchRegistry: false #不需要從服務端獲取註冊資訊
(因為在這裡自己就是服務端,而且已經禁用自己註冊了)
serviceUrl:
defaultZone: 

(這裡不註冊自己,註冊到其他服務上面以為會同步。)



yml 客戶端

server:
port: 4001
eureka:
client:
serviceUrl:
defaultZone:
eureka3001.com:3001/eureka,
/eureka #eureka服務端提供的註冊地址 參考服務端配置的這個路徑
instance:
instance-id: admin-1 #此例項註冊到eureka服務端的唯一的例項ID
prefer-ip-address: true #是否顯示IP地址
leaseRenewalIntervalInSeconds: 10 #eureka客戶需要多長時間發
送心跳給eureka伺服器,表明它仍然活著,預設為30 秒 (與下面配置的單位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka伺服器在
接收到例項的最後一次發出的心跳後,需要等待多久才可以將此例項刪除,預設為90秒
spring:
application:
name: server-admin #此例項註冊到eureka服務端的name



CAP定理

C:Consistency 一致性
A:Availability 可用性
P:Partition tolerance 分割槽容錯性
這三個指標不能同時達到



Partition tolerance

分割槽容錯性,大多數分散式系統都部署在多個子網路。每一個網路是一個區。區間的通訊是可能失敗的如一個在本地,一個在外地,他們之間是無法通訊的。分散式系統在設計的時候必須要考慮這種情況。

Consistency

一致性,寫操作後的讀取,必須返回該值。如:伺服器A1和伺服器A2,現在發起操作將A1中V0改為V1,使用者去讀取的時候讀到伺服器A1得到V1,如果讀到A2伺服器但是伺服器

還是V0,讀到的資料就不對,這就不滿足一致性。

所以讓A2返回的資料也對,的讓A1給A2傳送一條訊息,把A2的V0變為V1,這時候不管從哪裡讀取都是修改後的資料。

Availability

可用性就是使用者只要給出請求就必須回應,不管是本地伺服器還是外地伺服器只要接收到就必須做出回應,不管資料是否是最新必須做出回應,負責就不是可用性。

C與A矛盾

一致性和可用性不能同時成立,存在分割槽容錯性,通訊可能失敗。

如果保證一致性,A1在寫操作時,A2的讀寫只能被鎖定,只有等資料同步了才能讀寫,在鎖定期間是不能讀寫的就不符合可用性。

如果保持可用性,那麼A2就不會被鎖定,所以一致性就不能成立。

綜上 無法做到一致性和可用性,所以系統在設計的時候就只能選其一。

Eureka與Zookeeper

Zookeeper遵循的是CP原則保持了一致性,所以在master節點因為網路故障與剩餘“跟隨者”接點失去聯絡時會重新選舉“領導者”,選取“領導者”大概會持續30-120s的時間,且選舉的時候整個zookeeper是不可用的。導致在選舉的時候註冊服務癱瘓。

Eureka在設計的時候遵循AP可用性。Eureka各個接點是公平的,沒有主從之分,down掉幾個幾點也沒問題,其他接點依然可以支援註冊,只要有一臺Eureka在,註冊就可以用,只不過查詢到的資料可能不是最新的。Eureka有自我保護機制,如果15分鐘之內超過85%接點都沒有正常心跳,那麼Eureka認為客戶端與註冊中心出現故障,此時情況可能是

Eureka不在從註冊列表移除因為長時間沒有瘦到心跳而過期的服務。

Eureka仍然能夠接收註冊和查詢,但不會同步到其他接點。

當網路穩定後,當前的 例項註冊資訊會更新到其他接點。

Ribbon

rebbon主要提供客戶端的負載均衡,提供了一套完善的客戶端的配置。Rebbin會自動幫助你基於某種規則(如:簡單的輪詢,隨機連結等)。

服務端的負載均衡是一個url透過一個代理伺服器,然後透過代理伺服器(策略:輪詢,隨機 ,權重等等),反向代理到你的伺服器。

客戶端負載均衡是透過一個請求在客戶端已經宣告瞭要呼叫那個服務,然後透過具體演算法來完成負載均衡。

Ribbon使用

引入依賴,Eureka以及把Ribbon整合在裡面。

使用Ribbon只有在RestTemplate上面加入@LoadBalanced註解。

Feign負載均衡

feign是一個宣告式的webService客戶端,使用feign會讓編寫webService更簡單,就是定義一個介面加上註解。

feign是為了編寫java http客戶端更加簡單,在Ribbon+RestTemplate此基礎上進一步封裝,簡化了使用Spring Cloud Ribbon時,自動封裝服務呼叫客戶端的開發量。

Feign使用

引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在啟動類上加@EnableFeignClients
然後在介面上加@FeignClient("SERVER-POWER")註解其中引數就是服務的名字。



Feign整合Ribbon

利用Ribbon維護服務列表資訊,融合了Ribbon的負載均衡配置,與Ribbon不同的是Feign只需要定義服務繫結介面以宣告的方式,實現簡答的服務呼叫。

hystrix斷路器

是一種用於處理分散式系統延遲和容錯的開源庫。在分散式系統中許多依賴不可避免的會呼叫失敗,比如超時、異常等,斷路器保證出錯不會導致整體服務失敗,避免級聯故障。

斷路器其實就是一種開關設定,類似保險絲,像呼叫方返回一個符合預期的、可處理的備選響應,而不是長時間等待或者丟擲無法處理的異常,保證服務呼叫方執行緒不會被長時間 不必要佔用,從而避免了在分散式系統中蔓延,乃至雪崩。

微服務中 client->微服務A->微服務B->微服務C->微服務D,其中微服務B異常了,所有請求微服務A的請求都會卡在B這裡,就會導致執行緒一直累積在這裡,那麼其他微服務就沒有可用執行緒,導致整個伺服器雪崩。

針對這方案有 服務限流、超時監控、服務熔斷、服務降級

降級 超時

降級就是服務響應過長 ,或者不可用了,就是服務呼叫不了了,我們不能把錯誤資訊返回出來,或者長時間卡在哪裡,所以要準備一個策略當發生這種問題我們直接呼叫這個方法快速返回這個請求,不讓他一直卡在那。

要在呼叫方做降級(要不然那個微服務都down掉了在做降級就沒有意義)。

引入hystrix依賴

<dependency> 
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId> 
</dependency>
在啟動類上加入@EnableHystrix 或者@EnableCircuitBreaker。
@RequestMapping("/feignPower.do") 
@HystrixCommand(fallbackMethod = "fallbackMethod") 
public Object feignPower(String name){ 
return powerServiceClient.power(); 
} 
fallbackMethod:
public Object fallbackMethod(String name){ 
System.out.println(name); 
return R.error("降級資訊"); 
}
這裡的降級資訊具體內容根據業務需求來,比如返回一個預設的查詢資訊等等。
hystrix有超時監聽,當你請求超過1秒 就會超時,這個是可以配置的



這裡的降級資訊具體內容根據業務需求來,比如返回一個預設的查詢資訊等等。

hystrix有超時監聽,當你請求超過1秒 就會超時,這個是可以配置的

降級什麼用

第一他可以監聽服務有沒有超時。第二報錯了他這裡直接截斷了沒有讓請求一直卡在這個。

其實降級,當你係統迎來高併發的時候,這時候發現系統馬上承載不了這個大的併發 ,可以先關閉一些不重要 的微服務(就是在降級方法返回一個比較友好的資訊)把資源讓出來給主服務,其實就是整體資源不夠用了,忍痛關閉某些服務,待過渡後再開啟。

熔斷限流

熔斷就像生活中的跳閘,比如電路故障了,為了防止事故擴大,這裡切斷你的電源以免意外發生。當一個微服務呼叫多次,hystrix就會採取熔斷 機制,不在繼續呼叫你的方法,會預設短路,5秒後試探性的先關閉熔斷機制,如果在這時候失敗一次會直接呼叫降級方法,一定程度避免雪崩,

限流,限制某個微服務使用量,如果執行緒佔用超過了,超過的就會直接降級該次呼叫。

Feign整合hystrix

feign預設支援hystrix,需要在yml配置中開啟。
feign: 
hystrix: 
enabled: true
降級方法
@FeignClient(value = "SERVER-POWER", fallback = PowerServiceFallBack.class)
public interface PowerServiceClient {
@RequestMapping("/power.do")
public Object power(@RequestParam("name") String name);
}
在feign客戶端的註解上 有個屬性叫fallback 然後指向一個類 PowerServiceClient 
@Component
public class PowerServiceFallBack implements PowerServiceClient {
@Override
public Object power(String name) {
return R.error("測試降級");
}
}



Zuul 閘道器

zuul包含了對請求的路由和過濾兩個主要功能

路由是將外部請求轉發到具體的微服務例項上。是實現統一入口基礎而過濾器功能負責對請求的處理過程干預,是實現請求校驗等功能。

Zuul與Eureka進行整合,將zuul註冊在Eureka服務治理下,同時從Eureka獲取其他服務資訊。(zuul分服務最終還是註冊在Eureka上)

路由

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
最後要註冊在Eureka上所以需要引入eureka依賴
YML
server:
port: 9000
eureka:
client:
serviceUrl:
defaultZone: 服務端提供的註冊地址 參考服務端配置的這個路徑
instance:
instance-id: zuul-0 #此例項註冊到eureka服務端的唯一的例項ID
prefer-ip-address: true #是否顯示IP地址
leaseRenewalIntervalInSeconds: 10 #eureka客戶需要多長時間傳送心跳給eureka伺服器,表明它仍然活著,預設為30 秒 (與下面配置的單位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka伺服器在接收到例項的最後一次發出的心跳後,需要等待多久才可以將此例項刪除,預設為90秒
spring:
application:
name: zuul #此例項註冊到eureka服務端的name 
啟動類 @EnableZuulProxy
在實際開發當中我們肯定不會/server-power這樣透過微服務呼叫,
可能只要一個/power就好了 
zuul: 
routes:
mypower: 
serviceId: server-power 
path: /power/** 
myorder: 
serviceId: server-order 
path: /order/**
注意/**代表是所有層級 /* 是代表一層。
一般我們會禁用服務名呼叫
ignored-services:server-order 這樣就不能透過此服務名呼叫,
不過這個配置如果一個一個通微服務名字設定太複雜
一般禁用服務名 ignored-services:“*”
有時候要考慮到介面呼叫需要一定的規範,比如呼叫微服務URL需要字首/api,可以加上一個prefix
prefix:/api 在加上strip-prefix: false /api字首是不會出現在路由中
zuul:
prefix: /api
ignored-services: "*"
stripPrefix: false
routes:
product:
serviceId: server-product
path: /product/**
order:
serviceId: server-order
path: /order/**



過濾器

過濾器(filter)是zuul的核心元件,zuul大部分功能是透過過濾器實現的,zuul中定義了4種標準過濾器型別,這些過濾器型別對應與請求的生命週期,

PRE:這種過濾器在請求路由前被呼叫,可利用過濾器進行身份驗證,記錄請求微服務的除錯資訊等。

ROUTING:這種過濾器將請求路由到微服務,這種過濾器用於構建傳送給微服務請求,並使用 Apache HttpClient或Netfix Ribbon請求微服務。

POST:這種過濾器在路由微服務後執行,可用來相應新增標準的HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端。

ERROR:在其他階段傳送錯誤時執行過濾器

繼承ZuulFilter

@Component
public class LogFilter extends ZuulFilter { 
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER+1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
//被代理到的微服務
String proxy = (String)ctx.get("proxy");
//請求的地址
String requestURI = (String)ctx.get("requestURI");
//zuul路由後的url
System.out.println(proxy+"/"+requestURI);
HttpServletRequest request = ctx.getRequest();
String loginCookie = CookieUtil.getLoginCookie(request);
ctx.addZuulRequestHeader("login_key",loginCookie);
return null;
}
}



由此可知道自定義zuul Filter要實現以下幾個方法。

filterType:返回過濾器型別,有pre、route、post、erro等幾種取值

filterOrder:返回一個int值指定過濾器的順序,不同過濾器允許返回相同數字。

shouldFilter:返回一個boolean判斷過濾器是否執行,true執行,false不執行。

run:過濾器的具體實現。

Spting-Cloud預設為zuul編寫並開啟一些過濾器。如果要禁用部分過濾器,只需在application.yml裡設定zuul…disable=true,例如zuul.LogFilter.pre.disable=true

zuul也整合了了hystrix和ribbon的, 提供降級回退,繼承FallbackProvider 類 然後重寫裡面的方法。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31478593/viewspace-2714909/,如需轉載,請註明出處,否則將追究法律責任。

相關文章