go-kit微服務:限流

rayson發表於2019-02-21

由於業務應用系統的負載能力有限,為了防止非預期的請求對系統壓力過大而拖垮業務應用系統,每個API介面都是有訪問上限的。API介面的流量控制策略:分流、降級、限流等。本文討論限流策略,雖然降低了服務介面的訪問頻率和併發量,卻換取服務介面和業務應用系統的高可用。

限流的目的是通過對併發訪問/請求進行限速,或者對一個時間視窗內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理。

限流演算法

常用的限流演算法有兩種:漏桶演算法和令牌桶演算法。

漏桶演算法

漏桶演算法(Leaky Bucket)是網路世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)時經常使用的一種演算法,它的主要目的是控制資料注入到網路的速率,平滑網路上的突發流量。漏桶演算法提供了一種機制,通過它,突發流量可以被整形以便為網路提供一個穩定的流量。

漏桶演算法思路很簡單,水(請求)先進入到漏桶裡,漏桶以一定的速度出水(介面有響應速率),當水流入速度過大會直接溢位(訪問頻率超過介面響應速率),然後就拒絕請求,可以看出漏桶演算法能強行限制資料的傳輸速率。示意圖如下:

go-kit微服務:限流

因為漏桶的漏出速率是固定的引數,所以即使網路中不存在資源衝突(沒有發生擁塞),漏桶演算法也不能使流突發(burst)到埠速率。因此,漏桶演算法對於存在突發特性的流量來說缺乏效率。

令牌桶演算法

令牌桶演算法是網路流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種演算法。典型情況下,令牌桶演算法用來控制傳送到網路上的資料的數目,並允許突發資料的傳送。

令牌桶演算法的原理是系統會以一個恆定的速度往桶裡放入令牌,而如果請求需要被處理,則需要先從桶裡獲取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。從原理上看,令牌桶演算法和漏桶演算法是相反的,一個“進水”,一個是“漏水”。

go-kit微服務:限流

令牌桶的另外一個好處是可以方便的改變速度。 一旦需要提高速率,則按需提高放入桶中的令牌的速率。 一般會定時(比如100毫秒)往桶中增加一定數量的令牌,有些變種演算法則實時的計算應該增加的令牌的數量。

go-kit微服務限流實現

結合以上分析我將基於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方案

本文示例將繼續在上篇文章程式碼基礎上進行完善(地址附文末),前兩篇忘記放地址。

Step-1:建立限流器

首先,使用如下命令安裝最新版本的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)
		}
	}
}
複製程式碼

Step-2:修改main

下來就是使用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)
}
複製程式碼

Step-3:編譯&執行

在控制檯編譯並執行應用程式,然後通過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!,即限流器把令牌發完了將請求中斷,服務不可用;接下來繼續訪問時,服務恢復,即限流器恢復填滿令牌桶。

gokit內建實現方案

Step-1:建立限流器

首先下載依賴的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)
		}
	}
}
複製程式碼

Step-2:修改main

將限流方法封裝改為如下實現:

//add ratelimit,refill every second,set capacity 3
ratebucket := rate.NewLimiter(rate.Every(time.Second*1), 3)
endpoint = NewTokenBucketLimitterWithBuildIn(ratebucket)(endpoint)
複製程式碼

Step-3:編譯&執行

在控制檯編譯並執行應用程式,然後通過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內建庫)實現服務限流。

服務開發過程中我們需要充分考慮服務的可用性,尤其是那些比較消耗系統資源的服務,為其增加限流機制,確保服務穩定可靠執行。

圖片來自網際網路。

本文首發於本人微信公眾號【兮一昂吧】,歡迎掃碼關注!

go-kit微服務:限流

相關文章