1. 程式人生 > >04. Spring Cloud--客戶端彈性模式

04. Spring Cloud--客戶端彈性模式

4.1 簡介

在分散式系統中,當服務崩潰時,很容易檢測到該服務已經不在了,因此應用程式可以繞過它。然而,當服務執行緩慢時,檢測到這個服務效能不佳並繞過它是非常困難的,這是因為以下幾個原因。

  1. 服務的降級可以以間歇性問題開始,並形成不可逆轉的勢頭 – 降級可能只發生在很小的爆發中。故障的第一個跡象可能是一小部分使用者抱怨某個問題,直到突然間應用程式容器耗盡了執行緒池並徹底崩潰。
  2. 對遠端服務的呼叫通常是同步的,並且不會縮短長時間執行的呼叫 – 服務的呼叫者沒有超時的概念來阻止服務呼叫的永久掛起。應用程式開發人員呼叫該服務來執行操作並等待服務返回。
  3. 應用程式經常被設計為處理遠端資源的徹底故障,而不是部分降級
    – 通常,只要服務沒有徹底失敗,應用程式將繼續呼叫這個服務,並且不會採取快速失敗措施。該應用程式將繼續呼叫表現不佳的服務。呼叫的應用程式或服務可能會優雅地降級,但更有可能因為資源耗盡而崩潰。資源耗盡是指有限的資源(如執行緒池或資料庫連線)消耗殆盡,而呼叫客戶端必須等待該資源變為可用。

效能不佳的遠端服務所導致的潛在問題是,它們不僅難以檢測,還會觸發連鎖效應,從而影響整個應用程式生態系統。如果沒有適當的保護措施,一個性能不佳的服務可以迅速拖垮多個應用程式。

什麼是客戶端彈性模式?
客戶端彈性軟體模式的重點是,在遠端服務發生錯誤或表現不佳時保護遠端資源(另一個微服務呼叫或資料庫查詢)的客戶端免於崩潰。這些模式的目標是讓客戶端“快速失敗”,而不消耗諸如資料庫連線和執行緒池之類的寶貴資源,並且可以防止遠端服務的問題向客戶端的消費者進行“上游”傳播。

有4 種客戶端彈性模式,它們分別是:

  1. 客戶端負載均衡(client load balance)模式
  2. 斷路器(circuit breaker)模式
  3. 後備(fallback)模式
  4. 艙壁(bulkhead)模式

4.2 客戶端負載均衡模式

在討論服務發現(01. Spring Cloud–服務發現)時,我們瞭解了客戶端負載均衡模式。客戶端負載均衡涉及讓客戶端從服務發現代理(如Netflix Eureka)查詢服務的所有例項,然後快取服務例項的物理位置。 每當服務消費者需要呼叫該服務例項時,客戶端負載均衡器將從它維護的服務位置池返回一個位置。

因為客戶端負載均衡器位於服務客戶端和服務消費者之間,所以負載均衡器可以檢測服務例項是否丟擲錯誤或表現不佳。如果客戶端負載均衡器檢測到問題,它可以從可用服務位置池中移除該服務例項,並防止將來的服務呼叫訪問該服務例項。

4.3 斷路器模式

斷路器模式是模仿電路斷路器的客戶端彈性模式。在電氣系統中,斷路器檢測是否有過多電流流過電線。如果斷路器檢測到問題,它將斷開與電氣系統的其餘部分的連線,並保護下游部件不被燒燬。

有了軟體斷路器,當遠端服務被呼叫時,斷路器將監視這個呼叫。如果呼叫時間太長,斷路器將會介入並中斷呼叫。此外,斷路器將監視所有對遠端資源的呼叫,如果對某一個遠端資源的呼叫失敗次數足夠多,那麼斷路器實現就會出現並採取快速失敗,阻止將來呼叫失敗的遠端資源。

4.3.1 在Spring Cloud中實現斷路器模式

首先,要在pom.xml檔案中新增依賴:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
  <groupId>com.netflix.hystrix</groupId>
  <artifactId>hystrix-javanica</artifactId>
</dependency>

然後,要修改Spring入口檔案,如下:

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker   //告訴SpringCloud將要為服務使用Hystrix
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

最後,使用的時候,需要在要監視的方法上添加註解如下:

@HystrixCommand
public User getUserById(String id) {
    // 訪問資料庫處理...
}

使用@HystrixCommand註解,在任何時候呼叫getUserById方法時,Hystrix斷路器都將包裝這個呼叫。每當呼叫時間超過1000ms時,斷路器將終端對getUserById方法的呼叫。

如果訪問超時,則會返回如下資訊:

{
    "timestamp": "2018-11-07T21:01:58.033+0000",
    "status": 504,
    "error": "Gateway Timeout",
    "message": "com.netflix.zuul.exception.ZuulException: Hystrix Readed time out"
}

4.3.2 定製斷路器的超時時間

Hystrix 允許通過commandProperties 屬性來定製斷路器的行為。commandProperties屬性接受一個HystrixProperty 物件陣列,它可以傳入自定義屬性來配置Hystrix 斷路器。在以下程式碼中,使用execution.isolation.thread.timeoutIn Milliseconds 屬性設定Hystrix 呼叫的最大超時時間為 5s。

@HystrixCommand(commandProperties = {
	@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
})
public User getUserById(String id) {
    // 訪問資料庫處理...
}

4.4 後備模式

要使用Hystrix 實現一個的後備策略,開發人員必須做兩件事情。第一件是,需要在@HystrixCommand 註解中新增一個名為fallbackMethod 的屬性。該屬性將包含一個方法的名稱,當Hystrix 因為呼叫耗費時間太長而不得不中斷該呼叫時,該方法將會被呼叫。

第二件是,需要定義一個待執行的後備方法。此後備方法必須與由@HystrixCommand 保護的原始方法位於同一個類中,並且必須具有與原始方法完全相同的方法簽名,因為傳遞給由@HystrixCommand 保護的原始方法的所有引數都將傳遞給後備方法。

程式碼如下:

@HystrixCommand(fallbackMethod = "fallbackUser")
public String getUserById(String id) {
    // 訪問資料庫處理...
}

public String fallbackUser(String id){
    return "fall back user";
}

getUserById方法在執行過程中丟擲異常時,fallbackUser方法會作為備用方法被呼叫,也就是說呼叫者會得到fallbackUser方法的執行結果。

4.5 艙壁模式

在基於微服務的應用程式中,開發人員通常需要呼叫多個微服務來完成特定的任務。在不使用艙壁模式的情況下,這些呼叫預設是使用同一批執行緒來執行呼叫的,這些執行緒是為了處理整個Java容器的請求而預留的。在存在大量請求的情況下,一個服務出現效能問題會導致Java容器的所有執行緒被刷爆並等待處理工作,同時堵塞新請求,最終導致Java容器崩潰。艙壁模式將遠端資源呼叫隔離在它們自己的執行緒池中,以便可以控制單個表現不佳的服務,而不會使該容器崩潰。

Hystrix使用執行緒池來委派所有對遠端服務的請求。在預設情況下,所有的Hystrix命令都將共享一個執行緒池來處理請求。這個執行緒池將有10個執行緒來處理遠端服務呼叫,而這些遠端服務呼叫可以是任何東西,包括REST服務呼叫、資料庫呼叫等。

在應用程式中訪問少量的遠端資源時,這種模型執行良好,並且各個服務的呼叫量分佈相對均勻。問題是,如果某些服務具有比其他服務高得多的請求量或更長的完成時間,那麼最終可能導致Hystrix執行緒池中的執行緒耗盡,因為一個服務最終會佔據預設執行緒池中的所有執行緒。

Hystrix提供了一種易於使用的機制,在不同的遠端資源呼叫之間建立艙壁。程式碼如下:

@HystrixCommand(
        fallbackMethod = "fallbackUser",
        threadPoolKey = "userThreadPool",
        threadPoolProperties = {
                @HystrixProperty(name = "coreSize", value = "30"),
                @HystrixProperty(name = "maxQueueSize", value = "10")
        }
)
public String getUserById(String id) {
    // 訪問資料庫處理
}

public String fallbackUser(String id){
    return "fall back user";
}

要注意的第一件事是, 我們在@HystrixCommand 註解中引入了一個新屬性, 即threadPoolKey。這向 Hystrix 發出訊號,我們想要建立一個新的執行緒池。如果線上程池中沒有設定任何進一步的值,Hystrix 會使用threadPoolKey 屬性中的名稱搭建一個執行緒池,並使用所有的預設值來對執行緒池進行配置。

要定製執行緒池,應該使用@HystrixCommand 上的threadPoolProperties 屬性。此屬性使用HystrixProperty 物件的陣列,這些HystrixProperty 物件用於控制執行緒池的行為。使用coreSize 屬性可以設定執行緒池的大小。

開發人員還可以線上程池前建立一個佇列,該佇列將控制線上程池中執行緒繁忙時允許堵塞的請求數。此佇列大小由maxQueueSize 屬性設定。一旦請求數超過佇列大小,對執行緒池的任何其他請求都將失敗,直到佇列中有空間。