設想我們的應用成長很快,訪問量很大,爲了防止系統被大量請求打垮而不可用,我們需要做一些常規的保護措施。
先來了解幾個基本概念:
限流:後端服務有可能會面臨大量的請求,這可能是因爲用戶量確實很大,也可能是客戶端代碼中有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中的裝飾器模式的實現,我們的示例服務又強壯了一點。