限流算法(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
双向复制数据流,使用带宽受限的读取器和写入器达到限速效果。
在实际应用中,通常会将限流算法与带宽控制相结合,以确保请求频率和传输速率都符合系统预期,保障网络和资源的稳定性。