因爲業務應用系統的負載能力有限,爲了防止非預期的請求對系統壓力過大而拖垮業務應用系統,每一個API接口都是有訪問上限的。API接口的流量控制策略:分流、降級、限流等。本文討論限流策略,雖然下降了服務接口的訪問頻率和併發量,卻換取服務接口和業務應用系統的高可用。git
限流的目的是經過對併發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則能夠拒絕服務、排隊或等待、降級等處理。github
經常使用的限流算法有兩種:漏桶算法和令牌桶算法。golang
漏桶算法(Leaky Bucket)是網絡世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)時常用的一種算法,它的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,經過它,突發流量能夠被整形以便爲網絡提供一個穩定的流量。算法
漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以必定的速度出水(接口有響應速率),當水流入速度過大會直接溢出(訪問頻率超過接口響應速率),而後就拒絕請求,能夠看出漏桶算法能強行限制數據的傳輸速率。示意圖以下:api
由於漏桶的漏出速率是固定的參數,因此即便網絡中不存在資源衝突(沒有發生擁塞),漏桶算法也不能使流突發(burst)到端口速率。所以,漏桶算法對於存在突發特性的流量來講缺少效率。bash
令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型狀況下,令牌桶算法用來控制發送到網絡上的數據的數目,並容許突發數據的發送。微信
令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。從原理上看,令牌桶算法和漏桶算法是相反的,一個「進水」,一個是「漏水」。網絡
令牌桶的另一個好處是能夠方便的改變速度。 一旦須要提升速率,則按需提升放入桶中的令牌的速率。 通常會定時(好比100毫秒)往桶中增長必定數量的令牌,有些變種算法則實時的計算應該增長的令牌的數量。併發
結合以上分析我將基於go-kit實現微服務的限流功能。經過查閱gokit/kit/ratelimit
源碼,發現gokit基於go包golang.org/x/time/rate
內置了一種實現;另外,在此以前gokit默認使用的juju/ratelimit
實現方案(目前官方已經移除),我將基於兩種方式分別進行實現。微服務
與以前兩篇文章不一樣,本次實現將基於gokit內建的類型endpoint.Middleware
,該類型其實是一個function,使用裝飾者模式實現對Endpoint的封裝。定義以下:
# Go-kit Middleware Endpoint
type Middleware func(Endpoint) Endpoint
複製代碼
本文示例將繼續在上篇文章代碼基礎上進行完善(地址附文末),前兩篇忘記放地址。
首先,使用以下命令安裝最新版本的juju/ratelimit
庫:
go get github.com/juju/ratelimit
複製代碼
而後,新建go文件命名爲instrument.go
,實現限流方法:參數爲令牌桶(bkt)返回endpoint.Middleware
。使用令牌桶的TakeAvaiable
方法獲取令牌,若獲取成功則繼續執行,若獲取失敗則返回異常(即限流)。代碼以下:
var ErrLimitExceed = errors.New("Rate limit exceed!")
// NewTokenBucketLimitterWithJuju 使用juju/ratelimit建立限流中間件
func NewTokenBucketLimitterWithJuju(bkt *ratelimit.Bucket) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
if bkt.TakeAvailable(1) == 0 {
return nil, ErrLimitExceed
}
return next(ctx, request)
}
}
}
複製代碼
下來就是使用juju/ratelimit
建立令牌桶(每秒刷新一次,容量爲3),而後調用Step-1
實現限流方法對Endpoint進行裝飾。在main方法中增長以下代碼。
// add ratelimit,refill every second,set capacity 3
ratebucket := ratelimit.NewBucket(time.Second*1, 3)
endpoint = NewTokenBucketLimitterWithJuju(ratebucket)(endpoint)
複製代碼
修改後,完整代碼以下:
func main() {
ctx := context.Background()
errChan := make(chan error)
var logger log.Logger
{
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.With(logger, "caller", log.DefaultCaller)
}
var svc Service
svc = ArithmeticService{}
// add logging middleware
svc = LoggingMiddleware(logger)(svc)
endpoint := MakeArithmeticEndpoint(svc)
// add ratelimit,refill every second,set capacity 3
ratebucket := ratelimit.NewBucket(time.Second*1, 3)
endpoint = NewTokenBucketLimitterWithJuju(ratebucket)(endpoint)
r := MakeHttpHandler(ctx, endpoint, logger)
go func() {
fmt.Println("Http Server start at port:9000")
handler := r
errChan <- http.ListenAndServe(":9000", handler)
}()
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errChan <- fmt.Errorf("%s", <-c)
}()
fmt.Println(<-errChan)
}
複製代碼
在控制檯編譯並運行應用程序,而後經過Postman請求接口進行測試,便可看到輸出的日誌信息:
ts=2019-02-19T03:20:13.1908613Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T03:20:13.7144627Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T03:20:14.2276079Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T03:20:14.7414288Z caller=server.go:112 err="Rate limit exceed!"
ts=2019-02-19T03:20:15.2091773Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T03:20:16.0261559Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T03:20:16.6406654Z caller=server.go:112 err="Rate limit exceed!"
ts=2019-02-19T03:20:17.1912533Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T03:20:17.7828906Z caller=server.go:112 err="Rate limit exceed!"
複製代碼
從日誌中能夠看到,請求中出現了Rate limit exceed!
,即限流器把令牌發完了將請求中斷,服務不可用;接下來繼續訪問時,服務恢復,即限流器恢復填滿令牌桶。
首先下載依賴的go/time/rate
包,安裝方式以下(沒法直接使用go get指令):
git clone https://github.com/golang/time.git [Your GOPATH]/src/golang.org/x
複製代碼
而後在instrument.go
中添加方法NewTokenBucketLimitterWithBuildIn
,在其中使用x/time/rate
實現限流方法:
// NewTokenBucketLimitterWithBuildIn 使用x/time/rate建立限流中間件
func NewTokenBucketLimitterWithBuildIn(bkt *rate.Limiter) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
if !bkt.Allow() {
return nil, ErrLimitExceed
}
return next(ctx, request)
}
}
}
複製代碼
將限流方法封裝改成以下實現:
//add ratelimit,refill every second,set capacity 3
ratebucket := rate.NewLimiter(rate.Every(time.Second*1), 3)
endpoint = NewTokenBucketLimitterWithBuildIn(ratebucket)(endpoint)
複製代碼
在控制檯編譯並運行應用程序,而後經過Postman請求接口進行測試,便可看到輸出的日誌信息:
ts=2019-02-19T06:03:26.8650217Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T06:03:27.5747177Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T06:03:28.1274404Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T06:03:28.5892068Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T06:03:29.1327522Z caller=server.go:112 err="Rate limit exceed!"
ts=2019-02-19T06:03:29.59453Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T06:03:30.2138805Z caller=server.go:112 err="Rate limit exceed!"
ts=2019-02-19T06:03:30.6257682Z caller=logging.go:41 function=Subtract a=10 b=1 result=9 took=0s
ts=2019-02-19T06:03:31.2772011Z caller=server.go:112 err="Rate limit exceed!"
複製代碼
由日誌能夠看出效果與juju/ratelimit
方案同樣。
本文首先介紹了兩種經常使用的限流算法漏桶算法和令牌桶算法,而後經過兩種方案(juju/ratelimit
和gokit內置庫)實現服務限流。
服務開發過程當中咱們須要充分考慮服務的可用性,尤爲是那些比較消耗系統資源的服務,爲其增長限流機制,確保服務穩定可靠運行。
圖片來自互聯網。
本文首發於本人微信公衆號【兮一昂吧】,歡迎掃碼關注!