記得很多年前就有喜歡在面試的時候問這個問題:如何在高併發、大流量的時候,進行服務限流?
不同人能給出不同的解決辦法。
無外乎兩種處理:
-
- 在客戶端限流。
-
- 在服務端限流。
在客戶端限流,就是利用產品設計,讓單位時間內(可以是1秒,10秒,30秒,1分鐘等)只能發出一定請求數量。給用戶友好的交互提醒,讓他過一會兒再試。
當然如果遇到懂技術的用戶,通過一些手段繞過客戶端限流限制,那麼服務端又會承受這潑天的密集請求。
在服務端限流是一個比較好的選擇,更多的控制權放在服務端。一般考慮在2個地方去實現限流。第一個是利用API Gateway,在網關增加請求速率的限制,把大量的請求直接攔在網關處,從而減少服務器在一定時間內能處理的請求數量。
另一個方案是在API服務裏面增加限流邏輯,大體的實現思路是:
初始化一個容量固定(比如N)的bucket並裝滿N個token。每當一個請求過來,就消耗一個token。當bucket沒有token了,就無法處理請求了。
而我們在一個時間間隔之後快速refill滿bucket,繼續等待請求過來消耗token。
下面用一個js代碼來展示一個bucket是如何被消耗 token並且自動refill的:
class TokenBucket {
constructor(capacity, refillRate, refillInterval) {
this.capacity = capacity; // Maximum tokens in the bucket
this.tokens = capacity; // Initial number of tokens
this.refillRate = refillRate; // Number of tokens added per interval
this.refillInterval = refillInterval; // Interval for refilling tokens in milliseconds
// Start the refill process
setInterval(() => this.refill(), this.refillInterval);
}
// Refill tokens periodically
refill() {
this.tokens = Math.min(this.tokens + this.refillRate, this.capacity);
console.log(`Refilled. Current tokens: ${this.tokens}`);
}
// Attempt to consume tokens
consume(tokensRequired) {
if (this.tokens >= tokensRequired) {
this.tokens -= tokensRequired;
console.log(`Consumed ${tokensRequired} tokens. Remaining: ${this.tokens}`);
return true;
} else {
console.log(`Not enough tokens. Required: ${tokensRequired}, Available: ${this.tokens}`);
return false;
}
}
}
// Example usage
const bucket = new TokenBucket(10, 1, 1000); // Capacity of 10 tokens, refills 1 token every second
// Simulate sending packets
setInterval(() => {
const packetSize = 3; // Tokens required per packet
if (bucket.consume(packetSize)) {
console.log("Packet sent successfully");
} else {
console.log("Failed to send packet due to insufficient tokens");
}
}, 500); // Attempt to send a packet every 0.5 seconds
關於如何實現refill還有很多不同實現方法,比如固定時間窗口,滑動時間窗口等,我這個實現是最簡單粗暴的。後面找機會再聊一下滑動時間窗口的實現。
總結
令牌桶是一個很常見並且好用的算法,對於那些希望在一段時間內只處理有限請求的場景特別使用。畢竟這潑天的富貴,也要一口一口慢慢喫啊!