限流算法(Rate Limiting)是一種控制資源使用速率的策略,廣泛應用于服務器、代理、網關等場景。它可以有效地防止資源過載,保障服務的穩定性。常見的限流算法有:
- 計數器算法(Fixed Window Counter)
- 滑動窗口算法(Sliding Window Log)
- 漏桶算法(Leaky Bucket)
- 令牌桶算法(Token Bucket)
一、業界主流的限流算法
1. 計數器算法(Fixed Window Counter)
計數器算法是一種最簡單的限流算法,通過記錄請求的數量來實現限流。它將時間劃分為固定窗口(例如每秒或每分鐘),每個窗口內有固定的請求上限。
原理圖示:
時間軸:|-----Window 1-----|-----Window 2-----|-----Window 3-----|
請求數:| 5 | 10 | 7 | (窗口1) -> 閾值未超 (窗口2) -> 閾值超
優點:實現簡單,適用于短時間限流。
缺點:時間窗口邊界處存在“臨界突發”現象,導致短時間內允許大量請求通過。
2. 滑動窗口算法(Sliding Window Log)
滑動窗口算法將時間劃分為多個小的窗口,記錄每個小窗口的請求數。與固定窗口不同,滑動窗口會在請求到達時按需調整當前窗口的位置,以平滑突發請求。
原理圖示:
時間軸:|------|------|------|------|------|------| (按小窗口平滑計數)
窗口1 窗口2 窗口3 窗口4
優點:可避免“臨界突發”現象,平滑請求數。
缺點:需要存儲每個窗口的請求數,內存開銷較大。
3. 漏桶算法(Leaky Bucket)
漏桶算法通過將請求放入一個漏桶中,漏桶會以恒定速率排出請求。當請求到達速率大于桶的漏出速率時,多余的請求被丟棄。可以有效避免突發流量。
原理圖示:
| Requests
|---->漏桶-----> Constant Outflow
優點:能夠平滑流量、限制請求速率,適合需要恒定流速的場景。
缺點:突發請求有較高丟棄率。
4. 令牌桶算法(Token Bucket)
令牌桶算法在固定的時間間隔內向桶中加入令牌,每次請求需要消耗一個令牌。桶的容量是有限的,當令牌用完后,新的請求只能等待或被拒絕。該算法適合支持一定程度突發流量的場景。
原理圖示:
| 每秒生成 10 個令牌
|---->令牌桶(容量為 20)-----> 請求
優點:支持一定程度的流量突發,更加靈活。
缺點:突發流量過多時,可能仍需等待。
二、結合代理服務器的限流:實現上行和下行帶寬限制
在代理服務器上,限流通常用來控制上行(客戶端到代理)和下行(代理到客戶端)的帶寬。假設目標是限制上下行的帶寬,例如:上行帶寬限制為1MB/s,下行帶寬限制為500KB/s。
1. 實現原理
- 上行限流:在客戶端請求到達代理服務器時,讀取數據的速率受限。
- 下行限流:在代理服務器響應數據時,將響應內容按限定速率發送給客戶端。
通過這種方式,可以確保服務器和客戶端之間的數據流速在允許范圍內,不會因為突發流量而影響整體帶寬。
2. 核心 Golang 代碼示例
以下是一個帶寬限制的 Golang 示例代碼。我們使用 令牌桶來限制速率,通過定時器控制讀取和寫入速率。
package main
import (
"fmt"
"io"
"net"
"net/http"
"time"
)
// LimitReader 根據限制速率讀取數據
type LimitReader struct {
r io.Reader
bandwidth int
}
func (l *LimitReader) Read(p []byte) (n int, err error) {
// 計算需要讀取的數據量
chunkSize := l.bandwidth / 8
if len(p) > chunkSize {
p = p[:chunkSize]
}
// 讀取數據
n, err = l.r.Read(p)
time.Sleep(time.Second / 8) // 按帶寬限制速率讀取
return
}
// LimitWriter 根據限制速率寫入數據
type LimitWriter struct {
w io.Writer
bandwidth int
}
func (l *LimitWriter) Write(p []byte) (n int, err error) {
// 計算需要寫入的數據量
chunkSize := l.bandwidth / 8
for len(p) > 0 {
if len(p) < chunkSize {
chunkSize = len(p)
}
n, err := l.w.Write(p[:chunkSize])
if err != nil {
return n, err
}
time.Sleep(time.Second / 8) // 按帶寬限制速率寫入
p = p[chunkSize:]
}
return
}
func handleProxy(w http.ResponseWriter, req *http.Request) {
// 連接目標服務器
destConn, err := net.Dial("tcp", req.Host)
if err != nil {
http.Error(w, "無法連接目標服務器", http.StatusServiceUnavailable)
return
}
// 劫持客戶端連接
clientConn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
http.Error(w, "無法劫持連接", http.StatusInternalServerError)
return
}
// 設置上下行限速
upstream := &LimitReader{r: clientConn, bandwidth: 1024 * 1024} // 上行限速:1MB/s
downstream := &LimitWriter{w: clientConn, bandwidth: 512 * 1024} // 下行限速:500KB/s
// 雙向復制數據流
go io.Copy(destConn, upstream)
go io.Copy(downstream, destConn)
}
func main() {
fmt.Println("Starting Proxy server with rate limiting on :8080")
http.HandleFunc("/", handleProxy)
http.ListenAndServe(":8080", nil)
}
3. 代碼解析
- LimitReader 和 LimitWriter:分別對上行和下行的數據進行限速,控制數據的讀取和寫入速率。
- 雙向限速代理:通過
io.Copy雙向復制數據流,使用帶寬受限的讀取器和寫入器達到限速效果。
在實際應用中,通常會將限流算法與帶寬控制相結合,以確保請求頻率和傳輸速率都符合系統預期,保障網絡和資源的穩定性。