上游服務不可用了,下游服務如何應對?

2023-06-06 09:00:49

1. 引言

在系統中,上游服務和下游服務是兩個關鍵概念。上游服務通常指的是提供某種功能或資料的伺服器端,它接收來自下游服務的請求,並根據請求進行處理和響應。下游服務通常指的是發起請求並依賴上游服務的使用者端,它們通過傳送請求向上遊服務請求資料或執行某些操作。

上游服務和下游服務之間的共同作業是系統中實現整體功能的關鍵。上游服務提供了核心的業務邏輯和資料,下游服務則依賴於上游服務來完成其特定任務。

下游服務的穩定性和可用性直接依賴於上游服務的可靠性和效能。如果上游服務不可用或出現故障,同時下游服務沒有采取任何應對措施,此時將可能出現以下問題:

  1. 無法正常執行任務:下游服務可能無法執行其功能,因為它依賴於上游服務的資料或資源。這將導致下游服務無法正常工作。
  2. 延遲和效能下降:如果下游服務不對上游服務的不可用進行適當處理,而是無限期等待或不斷嘗試請求,系統的響應時間會增加,導致效能下降。
  3. 級聯故障:如果下游服務繼續發起大量的請求到不可用的上游服務,它可能會導致下游服務資源被全部佔用,無法為其他請求提供服務。這可能導致級聯故障,使整個系統不可用。

因此,下游服務應該採取適當的應對措施來處理上游服務不可用的情況,以確保系統的穩定性和可用性。

2. 情況分類

對於上游服務的不可用,此時可以區分為短暫不可用和長時間不可用兩種情況,從而採用不同的方式來進行處理。

短暫不可用,是指上游服務在一段時間內暫時無法提供正常的服務,通常是由於網路波動或負載過高等原因導致的。這種情況下,上游服務可能會在很短的時間內自行恢復,並重新可用。短暫不可用通常持續時間較短,可以通過重試機制來處理。下游服務可以在請求失敗後進行重試,直到上游服務恢復正常。

長時間不可用,是指上游服務在較長的時間內無法提供正常的服務,無法自行恢復或恢復時間較長。這種情況可能是由於嚴重的故障、系統崩潰,或其他長期性問題導致的。在這種情況下,簡單地進行無限制的重試可能會導致系統被該請求全部佔用,甚至引發級聯故障。

短暫不可用和長時間不可用是上游服務不可用的兩種常見情況,它們需要不同的應對策略。瞭解並區分這兩種情況對於確保系統的穩定性和可用性至關重要。

3. 短暫不可用

3.1 處理方式

短暫的不可用可能只是臨時性的,可能是網路擁塞或其他暫時性問題導致的。此時可以通過重試機制,下游服務可以嘗試重新傳送請求,增加請求成功的機會,避免由於系統的短暫不可用導致請求的失敗。

3.2 程式碼範例

重試機制在許多使用者端庫中已經成為標準功能。這些使用者端庫提供了一些設定選項,以控制重試行為。下面是一個使用gRPC的範例程式碼,展示瞭如何設定和使用重試機制,然後基於此避免由於系統的短暫不可用導致請求的失敗。

首先展示伺服器端程式碼,用於向外提供服務:

package main

import (
        "context"
        "log"
        "net"

        pb "path/to/your/protobuf"
        "google.golang.org/grpc"
)

type server struct {
        pb.UnimplementedYourServiceServer
}

func (s *server) YourRPCMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
        // 處理請求邏輯
        return &pb.Response{Message: "Hello, world!"}, nil
}

func main() {
        listener, err := net.Listen("tcp", ":50051")
        if err != nil {
                log.Fatalf("Failed to listen: %v", err)
        }

        srv := grpc.NewServer()
        pb.RegisterYourServiceServer(srv, &server{})

        log.Println("Server started")
        if err := srv.Serve(listener); err != nil {
                log.Fatalf("Failed to serve: %v", err)
        }
}

接下來展示使用者端程式碼,其在伺服器端出現錯誤時,會進行重試,避免由於服務的短暫不可用,導致請求的失敗,保證系統的可用性:

package main

import (
        "context"
        "log"
        "time"
        pb "path/to/your/protobuf" 
        "google.golang.org/grpc"
)

func main() {
        conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
        if err != nil {
                log.Fatalf("Failed to connect: %v", err)
        }
        defer conn.Close()

        client := pb.NewYourServiceClient(conn)

        // 設定重試次數和超時時間
        retryOptions := []grpc.CallOption{
                grpc.WithRetryMax(3),           // 最大重試次數
                grpc.WithTimeout(5 * time.Second), // 超時時間
        }
        // 發起 gRPC 請求(帶重試和超時時間)
        ctx := context.Background()
        response, err := client.YourRPCMethod(ctx, &pb.Request{}, retryOptions...)
        if err != nil {
                log.Fatalf("Failed to make request: %v", err)
        }

        log.Println("Response:", response.Message)
}

在使用者端程式碼中,我們通過 grpc.WithRetryMax 設定最大重試次數,通過 grpc.WithTimeout 設定超時時間。

這樣,當遇到伺服器端短暫不可用時,使用者端將進行最多 3 次的重試,並在每次重試時設定 5 秒的超時時間。能夠讓上游服務在短暫不可用情況下,仍然能夠最大限度得保證下游服務請求的成功。

3.3 最佳實踐

但是並非當請求失敗時,便不斷進行重試,直到請求成功,需要設定適當的重試策略和超時機制。避免由於大量的重試導致下游服務壓力變大,從而導致服務從短暫不可用轉變成了長時間不可用。

首先確定最大重試次數,根據系統需求和上游服務的特性,確定最大重試次數。避免無限制地進行重試,因為過多的重試可能會給上游服務造成過大的負擔。通常,3-5次重試是一個常見的值,但具體數位應根據實際情況進行調整。

其次設定合理的超時時間,為了避免下游服務長時間等待不可用的上游服務,設定合理的請求超時時間是很重要的。超時時間應根據上游服務的預期響應時間和網路延遲進行調整。一般來說,超時時間應該在幾秒鐘的範圍內,以確保及時地放棄不可用的請求並進行後續處理。

4. 長時間不可用

4.1 會導致的問題

長時間不可用的情況可能導致嚴重的問題,其中包括系統資源被全部佔用和級聯故障的風險。

首先是上游服務可能直接崩潰。當上遊服務面臨大量請求失敗時,如果下游服務仍然持續地發起請求並無腦重試,此時隨著請求數的增加,系統的資源(例如執行緒、記憶體、連線等)將被耗盡,導致系統無法處理其他的請求。結果是整個系統變得不可響應,甚至崩潰。

其次是級聯故障的風險。長時間不可用的上游服務可能引發級聯故障。在分散式系統中,不同的服務通常相互依賴和互動,上游服務的不可用性可能會影響到下游服務。如果下游服務在遇到上游服務不可用時不具備合適的應對機制,它們可能會繼續嘗試傳送請求,造成資源浪費和堆積。這樣的情況可能導致下游服務也不可用,甚至影響到更多的依賴系統,最終導致整個系統的級聯故障。

所以,我們在設計系統時,下游服務在一些關鍵節點應該採取適當的應對措施來處理上游服務不可用的情況,以確保系統的穩定性和可用性。

4.2 處理方式

當上遊服務長時間不可用時,下游服務需要採取一些措施來處理這個問題,以避免系統資源被全部佔用和防止級聯故障的發生。有兩種常見的處理方式:引入熔斷器和資料降級。

首先可以引入熔斷器:熔斷器是一種用於監控上游服務可用性並在需要時進行熔斷的機制。當上遊服務長時間不可用時,熔斷器會直接熔斷下游服務對上游服務的請求,而不是無腦地繼續傳送請求。這樣可以避免下游服務持續佔用系統資源,並且防止級聯故障的發生。

熔斷器通常有三個狀態:關閉狀態、開啟狀態和半開狀態。在關閉狀態下,請求會正常通過;當錯誤率達到一定閾值時,熔斷器會開啟,所有請求都會被熔斷;在一段時間後,熔斷器會進入半開狀態,允許部分請求通過,以檢查上游服務是否已經恢復正常。通過熔斷器的狀態轉換,下游服務可以更好地適應上游服務的不可用情況。

其次是進行資料快取與降級,如果上游服務提供的資料不是實時性要求很高,下游服務可以考慮在上游服務可用時快取一部分資料。當上遊服務不可用時,下游服務可以使用已經快取的資料作為降級策略,確保系統的正常執行。這樣可以減少對上游服務的依賴,避免系統資源被全部佔用。

兩種常見的方式,可以幫助下游服務在上游服務長時間不可用的情況下保持穩定性,並避免系統資源被全部佔用和級聯故障的發生。

4.3 程式碼範例

下面展示一個範例程式碼,展示下游服務向上遊服務傳送請求,並實現熔斷機制和快取降級操作,從而在上游服務不可用時,保證下游服務的穩定性:

package main

import (
        "fmt"
        "sync"
        "time"

        "github.com/sony/gobreaker"
)

var (
        circuitBreaker *gobreaker.CircuitBreaker
        cache          map[string]string
        cacheMutex     sync.RWMutex
)

func main() {
        // 初始化熔斷器
        circuitBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
                Name:        "MyCircuitBreaker",
                MaxRequests: 3,                  // 最大請求數
                Interval:    5 * time.Second,    // 統計時間視窗
                Timeout:     1 * time.Second,    // 超時時間
                ReadyToTrip: customTripFunc,     // 自定義熔斷判斷函數
        })

        // 初始化快取
        cache = make(map[string]string)

        // 模擬下游服務傳送請求
        for i := 0; i < 10; i++ {
                response, err := makeRequest("https://baidu.com")
                if err != nil {
                        fmt.Println("Request failed:", err)
                } else {
                        fmt.Println("Response:", response)
                }
                time.Sleep(1 * time.Second) // 間隔一段時間再次傳送請求
        }
}

// 傳送請求的函數
func makeRequest(url string) (string, error) {
        // 使用熔斷器包裝請求函數
        response, err := circuitBreaker.Execute(func() (interface{}, error) {
                // 假設向上遊服務傳送 HTTP 請求
                // ...
                // 返回響應資料或錯誤
                return "Response from  service", nil
        })

        if err != nil {
                // 請求失敗,使用快取降級
                cacheMutex.RLock()
                cachedResponse, ok := cache[url]
                cacheMutex.RUnlock()

                if ok {
                     return cachedResponse, nil
                }

                return "", err
        }
        // 請求成功,更新快取
        cacheMutex.Lock()
        cache[url] = response.(string)
        cacheMutex.Unlock()

        return response.(string), nil
}

// 自定義熔斷判斷函數,根據實際情況進行判斷
func customTripFunc(counts gobreaker.Counts) bool {
        // 根據失敗次數或其他指標來判斷是否觸發熔斷
        return counts.ConsecutiveFailures > 3
}

上述程式碼範例中,使用了gobreaker開源專案實現了熔斷機制,並使用本地快取實現了快取降級操作。

在傳送請求的函數makeRequest中,使用熔斷器包裝請求函數,在請求函數內部實際傳送請求給上游服務。如果請求成功,則更新快取,並返回響應。

如果請求失敗,則再次嘗試從快取中獲取資料,如果快取中存在,則返回快取的響應。如果快取中也不存在資料,則返回錯誤。

這樣,下游服務可以在上游服務不可用的情況下,通過快取提供一定的降級功能,避免對上游服務進行頻繁無效的請求。而熔斷機制則可以在下游服務可用性不高時,直接熔斷請求,減少對上游服務的負載,提高整體系統的穩定性。

5. 總結

本文主要介紹了上游服務不可用時,下游服務的應對措施。主要分為短暫不可用和長時間不可用兩種情況。

短暫不可用通常由網路波動或其他暫時性問題導致。在這種情況下,可以採用重試機制來成功請求上游服務,確保資源的可用性。重試機制是一種簡單而有效的方法,通過多次重試請求,以應對短暫的不可用情況,避免下游服務受到影響。

長時間不可用可能導致嚴重的問題,如上游服務崩潰或級聯故障。為了應對這種情況,可以引入熔斷器保護機制來確保下游服務的穩定性。熔斷器能夠快速切斷對不可用的上游服務的請求,避免系統資源被全部佔用。此外,根據具體服務的特性,可以考慮使用快取降級來處理長時間不可用的情況。通過快取上游服務的響應資料,即使上游服務不可用,下游服務仍可以從快取中獲取資料,保持服務的可用性。

綜上所述,重試機制和熔斷器是應對上游服務不可用的關鍵措施。重試機制適用於短暫不可用情況,而熔斷器則用於長時間不可用的情況下保護下游服務。此外,根據具體情況,採用快取降級等策略可以進一步提升服務的穩定性和可用性。