微服務-限流

一.介紹

       互聯網應用發展到今天,從單體應用架構到SOA以及今天的微服務,隨着微服務化的不斷升級進化,服務和服務之間的穩定性變得越來越重要,分佈式系統之所以複雜,主要原因是分佈式系統需要考慮到網絡的延時和不可靠,微服務很重要的一個特質就是需要保證服務冪等,保證冪等性很重要的前提需要分佈式鎖控制併發,同時緩存、降級和限流是保護微服務系統運行穩定性的三大利器。

       限流的目的是通過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或等待(比如秒殺、評論、下單)、降級(返回兜底數據或默認數據,如商品詳情頁庫存默認有貨)。

       一般開發高併發系統常見的限流有:限制總併發數(比如數據庫連接池、線程池)、限制瞬時併發數(如nginx的limit_conn模塊,用來限制瞬時併發連接數)、限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率);其他還有如限制遠程接口調用速率、限制MQ的消費速率。另外還可以根據網絡連接數、網絡流量、CPU或內存負載等來限流。

 

二.常見的限流算法

2.1 漏桶

       水(也就是請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大會直接溢出,然後就拒絕請求,可以看出漏桶算法能強行限制數據的傳輸速率。

2.2 令牌桶

       隨着時間流逝,系統會按恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶裏加入令牌(想象和漏洞漏水相反,有個水龍頭在不斷的加水),如果桶已經滿了就不再加了。新請求來臨時,會各自拿走一個令牌,如果沒有令牌可拿了就阻塞或者拒絕服務。

2.3 算法對比

  • 令牌桶是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求;漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕。
  • 漏桶限制的是常量流出速率(即流出速率是一個固定常量值不能修改),從而平滑突發流入速率;令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支持一次拿3個令牌,4個令牌),漏桶算法能夠強行限制數據的傳輸速率,令牌桶算法能夠在限制數據的平均傳輸速率的同時還允許某種程度的突發情況。令牌桶還有一個好處是可以方便的改變速度。一旦需要提高速率,則按需提高放入桶中的令牌的速率。所以,限流框架的核心算法還是以令牌桶算法爲主。
  • 兩個算法方向是相反的,對於相同的參數得到的限流效果是一樣的。

三.限流策略

       對於一個應用系統來說一定會有極限併發/請求數,即總有一個TPS/QPS閥值,如果超了閥值則系統就會不響應用戶請求或響應的非常慢,因此我們最好進行過載保護,防止大量請求湧入擊垮系統。

3.1 限制總資源數

       如果有的資源是稀缺資源(如數據庫連接、線程),而且可能有多個系統都會去使用它,那麼需要限制應用;可以使用池化技術來限制總資源數:連接池、線程池。比如分配給每個應用的數據庫連接是100,那麼本應用最多可以使用100個資源,超出了可以等待或者拋異常。

      

3.2 限流某個接口的總併發/請求數

       如果接口可能會有突發訪問情況,但又擔心訪問量太大造成崩潰,如搶購業務;這個時候就需要限制這個接口的總併發/請求數總請求數了;因爲粒度比較細,可以爲每個接口都設置相應的閥值。

package main

import (
    "sync"
    "net"
    "strconv"
    "fmt"
    "log"

)

const (
    MAX_CONCURRENCY = 10000 
    CHANNEL_CACHE = 200
)

var tmpChan = make(chan struct{}, MAX_CONCURRENCY)
var waitGroup sync.WaitGroup

func main(){
    concurrency()
    waitGroup.Wait()
}

//進行網絡io
func request(currentCount int){
    fmt.Println("request" + strconv.Itoa(currentCount) + "\r")
    tmpChan <- struct{}{}
    conn, err := net.Dial("tcp",":8080")
    <- tmpChan
    if err != nil { log.Fatal(err) }
    defer conn.Close()
    defer waitGroup.Done()
}

//併發
func concurrency(){
    for i := 0;i < MAX_CONCURRENCY;i++ {
        waitGroup.Add(1)
        go request(i)
    }
}

      適合對業務無損的服務或者需要過載保護的服務進行限流,如搶購業務,超出了大小要麼讓用戶排隊,要麼告訴用戶沒貨了,對用戶來說是可以接受的。而一些開放平臺也會限制用戶調用某個接口的試用請求量,也可以用這種計數器方式實現。這種方式也是簡單粗暴的限流,沒有平滑處理,需要根據實際情況選擇使用。

3.3 限制某個接口的時間窗請求數

       即一個時間窗口內的請求數,如想限制某個接口/服務每秒/每分鐘/每天的請求數/調用量。如一些基礎服務會被很多其他系統調用,比如商品詳情頁服務會調用基礎商品服務調用,但是怕因爲更新量比較大將基礎服務打掛,這時我們要對每秒/每分鐘的調用量進行限速。

3.4 平滑限流某個接口的請求數

       之前的限流方式都不能很好地應對突發請求,即瞬間請求可能都被允許從而導致一些問題;因此在一些場景中需要對突發請求進行整形,整形爲平均速率請求處理(比如5r/s,則每隔200毫秒處理一個請求,平滑了速率)。這個時候有兩種算法滿足我們的場景:令牌桶和漏桶算法。

package main

import (
  "net/http"
  "golang.org/x/time/rate"
)
  
var limiter = rate.NewLimiter(2, 5)
func limit(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if limiter.Allow() == false {
      http.Error(w, http.StatusText(429), http.StatusTooManyRequests)
      return
    }
    next.ServeHTTP(w, r)
  })
}
  
func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", okHandler)
  // Wrap the servemux with the limit middleware.
  http.ListenAndServe(":4000", limit(mux))
}
  
func okHandler(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

3.5 分佈式限流

     當應用爲單點應用時,只要應用進行了限流,那麼應用所依賴的各種服務也都得到了保護。 

       但線上業務出於各種原因考慮,多是分佈式系統,單節點的限流僅能保護自身節點,但無法保護應用依賴的各種服務,並且在進行節點擴容、縮容時也無法準確控制整個服務的請求限制。   

       而如果實現了分佈式限流,那麼就可以方便地控制整個服務集羣的請求限制,且由於整個集羣的請求數量得到了限制,因此服務依賴的各種資源也得到了限流的保護。

3.5.1 Redis方案

   實現原理其實很簡單。既然要達到分佈式全侷限流的效果,那自然需要一個第三方組件來記錄請求的次數。

   其中 Redis 就非常適合這樣的場景。

  • 每次請求時將方法名進行md5加密後作爲Key 寫入到 Redis 中,超時時間設置爲 2 秒,Redis 將該 Key 的值進行自增。
  • 當達到閾值時返回錯誤。
  • 寫入 Redis 的操作用 Lua 腳本來完成,利用 Redis 的單線程機制可以保證每個 Redis 請求的原子性。
local key = KEYS[1] --限流KEY(一秒一個)
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
   return 0
else  --請求數+1,並設置2秒過期
   redis.call("INCRBY", key,"1")
   redis.call("expire", key,"2")
   return 1
end

      方案的缺點顯而易見,每取一次令牌都會進行一次網絡開銷,而網絡開銷起碼是毫秒級,所以這種方案支持的併發量是非常有限的。

3.5.2 QPS分配

       舉個例子,我們有兩臺服務器實例,對應的是同一個應用程序(Application.name相同),程序中設置的QPS爲100,將應用程序與同一個控制檯程序進行連接,控制檯端依據應用的實例數量將QPS進行均分,動態設置每個實例的QPS爲50,若是遇到兩個服務器的配置並不相同,在負載均衡層的就已經根據服務器的優劣對流量進行分配,例如一臺分配70%流量,另一臺分配30%的流量。面對這種情況,控制檯也可以對其實行加權分配QPS的策略。

      客觀來說,這是一種集羣限流的實現方案,但依舊存在不小的問題。該模式的分配比例是建立在大數據流量下的趨勢進行分配,實際情況中可能並不是嚴格的五五分或三七分,誤差不可控,極容易出現用戶連續訪問某一臺服務器遇到請求駁回而另一臺服務器此刻空閒流量充足的尷尬情況。

3.5.3 發票服務器

       這種方案的思想是建立在Redis令牌桶方案的基礎之上的。如何解決每次取令牌都伴隨一次網絡開銷,該方案的解決方法是建立一層控制端,利用該控制端與Redis令牌桶進行交互,只有當客戶端的剩餘令牌數不足時,客戶端才向該控制層取令牌並且每次取一批。

       這種思想類似於Java集合框架的數組擴容,設置一個閾值,只有當超過該臨界值時,纔會觸發異步調用。其餘存取令牌的操作與本地限流無二。雖然該方案依舊存在誤差,但誤差最大也就一批次令牌數而已。

 

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