Go語言微服務實戰之限流與熔斷保護

設想我們的應用成長很快,訪問量很大,爲了防止系統被大量請求打垮而不可用,我們需要做一些常規的保護措施。

先來了解幾個基本概念:

限流:後端服務有可能會面臨大量的請求,這可能是因爲用戶量確實很大,也可能是客戶端代碼中有bug(例如出現遞歸之類的問題),還有可能是不法分子惡意攻擊。大量的請求最終有可能導致服務不可用,如果是核心服務造成的影響會更嚴重,這時候就需要服務端根據QPS的情況做限流,一旦請求量超出閾值,則採取某種措施(等待或者直接拒絕處理)。

熔斷:如果服務因爲某種原因而頻繁的出現請求超時的情況,此時需要對後續的請求進行短路處理,也就是不實際調用後臺服務,而是返回給調用方一個mock的值,等到服務恢復以後,用戶可以繼續正常訪問服務。

接下來我們繼續擴展之前的示例代碼,我們先加限流,下一篇文章在加熔斷。

1、給服務做限流保護

我們已經瞭解了限流的概念,下面來看看現實當中用的比較多的限流算法:令牌桶算法。

令牌桶算法實際上是個類似於生產者和消費者的一種算法:有一個令牌桶,桶子的初始容量爲N個令牌,當有請求過來的時候,先得從令牌桶裏拿一個令牌,如果沒有令牌就要等待。還有一個生產者,每隔一定時間就往令牌裏放一個令牌。可以通過配置相關參數來達到限流的目的。

 go語言已經有令牌桶算法的實現供我們使用,我們就來用用看(當然想自己手動擼一個也是可以的):

首先在go module中導入依賴包:

github.com/juju/ratelimit v1.0.1

這個是github上標星比較多的一個令牌桶算法的實現,另外github上也有uber實現的漏桶算法的實現,可以自行選擇。

1.1 理解micro的裝飾器

在加限流功能前我們先認識一下micro的裝飾器模式,在micro中很多地方都使用了裝飾器來讓用戶對其進行自定義擴展,在客戶端進行調用時,可以通過選項來改變調用行爲,我們看看之前示例代碼中接口的定義:

// Client API for Greeter service

type GreeterService interface {
	Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error)
}

注意最後的opts這個參數,這個東東就是我們提供一些調用選項來對調用進行控制的地方。那有哪些選項可供我們設置呢,看下面的定義就知道了:

type CallOptions struct {
	SelectOptions []selector.SelectOption

	// Address of remote hosts
	Address []string
	// Backoff func
	Backoff BackoffFunc
	// Check if retriable func
	Retry RetryFunc
	// Transport Dial Timeout
	DialTimeout time.Duration
	// Number of Call attempts
	Retries int
	// Request/Response timeout
	RequestTimeout time.Duration
	// Stream timeout for the stream
	StreamTimeout time.Duration
	// Use the services own auth token
	ServiceToken bool

	// Middleware for low level call func
	CallWrappers []CallWrapper

	// Other options for implementations of the interface
	// can be stored in a context
	Context context.Context
}

請求的超時值,重試的次數,採用什麼負載均衡策略來選擇節點等等都是可以設置的,現在我們重點關注下面這個選項:

CallWrappers

這是一個CallWrapper類型的切片,從字面意思上來看就是對原始調用請求進行包裝,我們看看CallWrapper的定義:

// CallFunc represents the individual call func
type CallFunc func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error

// CallWrapper is a low level wrapper for the CallFunc
type CallWrapper func(CallFunc) CallFunc

這是一個函數類型,參數是原始CallFunc,返回一個新的CallFunc,你可以在這個新的CallFunc裏上下其手。

這是針對某個特定的接口做限流,如果你想對整個服務做限流,方式也是類似的,可以用micro.WrapClient對整個客戶端做包裝即可。

1.2 增加限流代碼

現在我們其實已經大概知道了要如何擴展代碼增加限流功能了,開始動手。新建一個文件,內容非常簡單:

/**
 * 帶上限流功能,針對整個服務的所有接口,用一個令牌桶控制整個服務的訪問量
 * 參數:
 * fillIntervalMs 向令牌桶添加令牌的週期,以毫秒爲單位
 * bucketCapicty  令牌桶中的容量
 * quantumAdd     每次添加多少令牌到桶裏
 * wait           當令牌耗盡時是否等待
 */
func WithRateLimit(fillIntervalMs int, bucketCapicty, quantumAdd int64, wait bool) micro.Option {
	return micro.WrapClient(newRateLimitWrapper(fillIntervalMs, bucketCapicty, quantumAdd, wait))
}

/**
 * 帶上限流功能,針對服務的某個接口,每個接口用一個令牌桶來控制訪問量
 * 參數:
 * bucket 控制接口訪問的令牌桶
 * wait   當令牌耗盡時是否等待
 */
func WithRateLimitCall(bucket *ratelimit.Bucket, wait bool) client.CallOption {
	wrapper := func(f client.CallFunc) client.CallFunc {
		if wait {
			// 一直等待到令牌桶中有令牌爲止
			time.Sleep(bucket.Take(1))
		} else if bucket.TakeAvailable(1) == 0 {
			// 沒有拿到令牌,又不等待,直接返回錯誤給客戶端
			return func(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
				return apperror.TooManyRequest
			}
		}
		return f
	}
	return client.WithCallWrapper(wrapper)
}

我們提供了兩個工具函數,一個用於包裝整個客戶端,一個用於包裝接口。

現在我們修改一下上一篇文章中的客戶端代碼:

var (
	//控制接口Hello訪問的令牌桶
	helloBucket = ratelimit.NewBucketWithQuantum(time.Millisecond * time.Duration(10), 50, 1)
)

func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
	//通過rpc調用服務端
	response, e := g.Client.Hello(ctx, &pb.Request{Name: "Hello Micro"}, tool.WithRateLimitCall(helloBucket, false))
	if e != nil {
		return e
	}
	rsp.Msg = response.Msg
	return nil
}

代碼基本一樣,增加了一個用於控制Hello調用的令牌桶,這個令牌桶容量爲50,每10ms向令牌中添加1個令牌,然後在RPC的時候增加了限流選項:tool.WithRateLimitCall。

現在,我們的Greeter.Hello接口已經具備限流的能力了,下面我們來測試一下。

1.3 測試限流功能

我們把代碼跑起來,跟上一篇文章一樣,先啓動網關,在啓動服務端,最後啓動客戶端。

然後我們來用wrk壓測工具來測試一下:

起5個線程,建100個連接模擬100個客戶,測試結果如圖,我們發現有大量的請求沒有成功(Non-2xx or 3xx responses),這是因爲被我們的限流算法把請求給拒絕掉了。

做爲對比,我們去掉限流代碼在測試一次:

可以看到,去掉限流之後不會有失敗的請求了。

2、小結

這篇文章我們繼續擴展微服務示例代碼,給接口加上了限流功能,順便跟着這個功能瞭解了micro中的裝飾器模式的實現,我們的示例服務又強壯了一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章