构建高效的客户端 Rate Limit 框架:设计与应用场景

本文介绍了一个用于客户端的通用限流框架设计与实践。该框架通过多种限流策略(计数器、滑动窗口、令牌桶)解决了HTTP请求并发、reCAPTCHA验证、日志写入等场景的限流需求。框架采用模块化设计,支持灵活配置和扩展,并集成了开发环境实时告警和生产环境Telemetry监控功能,有效提升了系统稳定性和用户体验。

1. 问题背景与需求

在现代客户端开发中,资源管理和系统稳定性至关重要。然而,在实际业务中,我们发现了以下问题:

  1. HTTP 请求无约束:短时间内可能触发大量并发 HTTP 请求,缺乏全局限制,容易导致服务端压力激增。
  2. 不合理的 reCaptcha 错误处理:当服务负载较高时,频繁弹出 reCaptcha 提示框,极大地影响用户体验。
  3. 日志系统重复记录:大量重复 MemLog 日志可能掩盖关键问题,影响诊断效率。
  4. 重复 Telemetry 数据:Telemetry 系统被重复数据淹没,导致分析困难。
  5. 频繁文件操作:频繁打开文件可能导致系统资源耗尽。
  6. 频繁数据库读写:短时间内的高频数据库操作影响性能并降低系统响应速度。

解决目标
为了解决以上问题,我们设计了一个通用的 Rate Limit 框架,以实现以下目标:

  • 统一管理各类操作的限流规则(如 HTTP 请求、日志记录、文件操作等)。
  • 提供灵活可配置的限流策略以适应不同业务场景。
  • 优化用户体验,减少无效操作对用户的干扰。
  • 提供扩展性,适配未来可能的需求变化。

2. 框架设计思路

根据需求,我们将 Rate Limit 框架的设计分为以下几个关键模块:

  1. 统一管理模块:提供统一的限流机制,支持跨业务模块的限流需求。
  2. 策略定义与配置:允许开发者为不同场景灵活配置限流策略,包括阈值、时间窗口等。
  3. 数据采集与监控:实时采集操作数据并监控限流执行状态。
  4. 限流执行与反馈:根据策略结果进行限流操作,同时提供友好的反馈机制,如开发阶段的对话框提示和线上日志上报。

3. 框架结构设计

3.1 核心组件

  1. RateLimiter:限流核心模块,负责执行限流策略。
  2. RateLimitStrategy:策略配置模块,用于定义场景的限流规则和算法。可以为不同的业务场景(如HTTP请求、文件操作、数据库操作)设置不同的限流策略和配置。
  3. DataCollector:数据采集模块,负责实时记录操作数据。通过DataCollector接口,可以实现自定义的数据收集逻辑,适应不同的监控需求。
  4. RateLimitHandler:限流处理模块,根据策略执行反馈操作(如弹窗或日志上报)。

3.2 架构图

以下是框架的核心架构图:

image.png

3.3 核心类图

以下是核心类的 UML 类图:

image.png

4. 实现细节

4.1 核心模块代码

RateLimitTypes.h

定义限流策略的核心数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// RateLimitTypes.h
#ifndef RATE_LIMIT_TYPES_H
#define RATE_LIMIT_TYPES_H

#include <chrono>

namespace RateLimitFramework {

enum class RateLimitAlgorithm {
SIMPLE_COUNTER,
SLIDING_WINDOW,
TOKEN_BUCKET
};

enum class StrategyType {
Request,
Recapcha,
MEMLOG,
DB_IO
// Add more strategy types as needed
};

struct RateLimitStrategy {
StrategyType strategyType;
int threshold;
std::chrono::milliseconds timeWindow;
RateLimitAlgorithm algorithm;
};

} // namespace RateLimitFramework

#endif // RATE_LIMIT_TYPES_H

DataCollector

负责记录和查询操作时间戳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#ifndef DATA_COLLECTOR_H
#define DATA_COLLECTOR_H

#include "cmmlib/CmmBase.h"
#include "RateLimitTypes.h"
#include <string>
#include <chrono>
#include <map>
#include <deque>

using namespace std;
using namespace std::chrono;

namespace RateLimitFramework {

class CMM_API DataCollector {
protected:
std::map<std::string, std::deque<std::chrono::steady_clock::time_point>> timestamps;

public:
virtual void record(const std::string& key, const RateLimitStrategy& strategy);
virtual int getData(const std::string& key, const RateLimitStrategy& strategy) const;
virtual ~DataCollector() = default;
};

} // namespace RateLimitFramework

#endif // DATA_COLLECTOR_H

#include "cmmlib/ratelimit/DataCollector.h"
#include <algorithm>

using namespace std;
using namespace std::chrono;
using namespace RateLimitFramework;

void DataCollector::record(const std::string& key, const RateLimitStrategy& strategy) {
auto now = std::chrono::steady_clock::now();
auto& timePoints = timestamps[key];
timePoints.push_back(now);

auto startTime = now - strategy.timeWindow;
while (!timePoints.empty() && timePoints.front() < startTime) {
timePoints.pop_front();
}
}

int DataCollector::getData(const std::string& key, const RateLimitStrategy& strategy) const {
auto it = timestamps.find(key);
if (it == timestamps.end()) {
return 0;
}
auto now = std::chrono::steady_clock::now();
auto startTime = now - strategy.timeWindow;
return std::count_if(it->second.begin(), it->second.end(),
[startTime](const auto& tp) { return tp >= startTime; });
}

RateLimiter

根据不同的限流算法执行限流逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#ifndef RATE_LIMITER_H
#define RATE_LIMITER_H

#include "DataCollector.h"
#include "cmmlib/CmmBase.h"
#include <map>
#include <memory>
#include <chrono>

namespace RateLimitFramework {

class CMM_API RateLimitHandler {
public:
virtual void handle(const std::string& key, const std::map<std::string, std::string>& context) = 0;
virtual ~RateLimitHandler() = default;
};

class CMM_API RateLimiter {
private:
struct Strategy {
RateLimitStrategy config;
std::shared_ptr<DataCollector> dataCollector;
std::shared_ptr<RateLimitHandler> handler;
std::chrono::steady_clock::time_point lastExecutionTime;
double tokens;

Strategy(const RateLimitStrategy& cfg, std::shared_ptr<DataCollector> dc, std::shared_ptr<RateLimitHandler> rh)
: config(cfg), dataCollector(dc), handler(rh), lastExecutionTime(std::chrono::steady_clock::now()), tokens(cfg.threshold) {}

Strategy()
: config{}, dataCollector(nullptr), handler(nullptr), lastExecutionTime(std::chrono::steady_clock::now()), tokens(0.0) {}
};

std::map<StrategyType, Strategy> strategies;

public:
void addStrategy(const RateLimitStrategy& strategy, std::shared_ptr<DataCollector> dataCollector, std::shared_ptr<RateLimitHandler> handler);
bool checkLimit(StrategyType strategyType, const std::string& key);

private:
bool checkSimpleCounter(const Strategy& strategy, const std::string& key);
bool checkSlidingWindow(const Strategy& strategy, const std::string& key);
bool checkTokenBucket(Strategy& strategy, const std::string& key);
};

} // namespace RateLimitFramework

#endif // RATE_LIMITER_H

#include "cmmlib/ratelimit/RateLimit.h"
#include <map>
#include <algorithm>

using namespace std;
using namespace std::chrono;
using namespace RateLimitFramework;

namespace RateLimitFramework {

void RateLimiter::addStrategy(const RateLimitStrategy& strategy, shared_ptr<DataCollector> dataCollector, shared_ptr<RateLimitHandler> handler) {
strategies[strategy.strategyType] = Strategy(strategy, dataCollector, handler);
}

bool RateLimiter::checkLimit(StrategyType strategyType, const std::string& key) {
auto strategyIt = strategies.find(strategyType);
if (strategyIt == strategies.end()) {
return true; // If strategy doesn't exist, allow the request
}

auto& strategy = strategyIt->second;
strategy.dataCollector->record(key, strategy.config);

switch (strategy.config.algorithm) {
case RateLimitAlgorithm::SIMPLE_COUNTER:
return checkSimpleCounter(strategy, key);
case RateLimitAlgorithm::SLIDING_WINDOW:
return checkSlidingWindow(strategy, key);
case RateLimitAlgorithm::TOKEN_BUCKET:
return checkTokenBucket(strategy, key);
}
return true;
}

bool RateLimiter::checkSimpleCounter(const Strategy& strategy, const std::string& key) {
int count = strategy.dataCollector->getData(key, strategy.config);
if (count > strategy.config.threshold) {
std::map<std::string, std::string> context;
strategy.handler->handle(key, context);
return false;
}
return true;
}

bool RateLimiter::checkSlidingWindow(const Strategy& strategy, const std::string& key) {
int count = strategy.dataCollector->getData(key, strategy.config);
if (count > strategy.config.threshold) {
std::map<std::string, std::string> context;
strategy.handler->handle(key, context);
return false;
}
return true;
}

bool RateLimiter::checkTokenBucket(Strategy& strategy, const std::string& key) {
auto now = steady_clock::now();
auto timePassed = chrono::duration_cast<chrono::milliseconds>(now - strategy.lastExecutionTime).count();
strategy.tokens += timePassed * (strategy.config.threshold / static_cast<double>(strategy.config.timeWindow.count()));
strategy.tokens = (std::min)(strategy.tokens, static_cast<double>(strategy.config.threshold));
strategy.lastExecutionTime = now;

if (strategy.tokens < 1.0) {
std::map<std::string, std::string> context;
strategy.handler->handle(key, context);
return false;
}
else {
strategy.tokens -= 1.0;
return true;
}
}
}

4.2 测试程序

示例测试用例展示了简单计数限流算法的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include "cmmlib/ratelimit/RateLimit.h"
#include "cmmlib/ratelimit/DataCollector.h"
#include "ConsoleHandler.h"
#include "EmitRequestDataCollector.h"
#include <iostream>
#include <thread>

using namespace std;
using namespace std::chrono;
using namespace RateLimitFramework;

class RateLimitTest : public ::testing::Test {
protected:
RateLimiter rateLimit;
shared_ptr<DataCollector> collector;
shared_ptr<RateLimitHandler> handler;

void SetUp() override {
collector = make_shared<EmitRequestDataCollector>();
handler = make_shared<ConsoleHandler>();
}
};

TEST_F(RateLimitTest, SimpleCounterTest) {
RateLimitStrategy strategy = { StrategyType::Request, 5, milliseconds(1000), RateLimitAlgorithm::SIMPLE_COUNTER };
rateLimit.addStrategy(strategy, collector, handler);

string key = "example.com/api";

// Test within limit
for (int i = 0; i < 5; i++) {
EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Request, key));
}

// Test exceeding limit
EXPECT_FALSE(rateLimit.checkLimit(StrategyType::Request, key));

// Wait for the window to pass
this_thread::sleep_for(milliseconds(1100));

// Test reset after window
EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Request, key));
}

TEST_F(RateLimitTest, DifferentKeysTest) {
RateLimitStrategy strategy = { StrategyType::Request, 3, milliseconds(1000), RateLimitAlgorithm::SIMPLE_COUNTER };
rateLimit.addStrategy(strategy, collector, handler);

string key1 = "example.com/api1";
string key2 = "example.com/api2";

// Test key1
for (int i = 0; i < 3; i++) {
EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Request, key1));
}
EXPECT_FALSE(rateLimit.checkLimit(StrategyType::Request, key1));

// Test key2 (should not be affected by key1's limit)
for (int i = 0; i < 3; i++) {
EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Request, key2));
}
EXPECT_FALSE(rateLimit.checkLimit(StrategyType::Request, key2));
}

TEST_F(RateLimitTest, NonExistentStrategyTest) {
string key = "test";

// Should allow requests for non-existent strategies
EXPECT_TRUE(rateLimit.checkLimit(static_cast<StrategyType>(999), key));
}

TEST_F(RateLimitTest, MultipleStrategiesTest) {
RateLimitStrategy apiStrategy = { StrategyType::Request, 5, milliseconds(1000), RateLimitAlgorithm::SIMPLE_COUNTER };
RateLimitStrategy recapchaStrategy = { StrategyType::Recapcha, 3, milliseconds(2000), RateLimitAlgorithm::SLIDING_WINDOW };

rateLimit.addStrategy(apiStrategy, make_shared<EmitRequestDataCollector>(), handler);
rateLimit.addStrategy(recapchaStrategy, make_shared<EmitRequestDataCollector>(), handler);

string request_url = "example.com/api";
string recapcha_url = "user123";

// Test Request strategy
for (int i = 0; i < 5; i++) {
EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Request, request_url));
}
EXPECT_FALSE(rateLimit.checkLimit(StrategyType::Request, request_url));

// Test Recapcha strategy
for (int i = 0; i < 3; i++) {
EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Recapcha, recapcha_url));
}
EXPECT_FALSE(rateLimit.checkLimit(StrategyType::Recapcha, recapcha_url));

// Wait for API strategy to reset
this_thread::sleep_for(milliseconds(1100));

EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Request, request_url));
EXPECT_FALSE(rateLimit.checkLimit(StrategyType::Recapcha, recapcha_url)); // Login strategy should still be blocked

// Wait for Login strategy to reset
this_thread::sleep_for(milliseconds(1000));

EXPECT_TRUE(rateLimit.checkLimit(StrategyType::Recapcha, recapcha_url));
}

5. 实际应用场景

开发环境集成

在开发阶段,我们实现了实时告警机制:

  • 当检测到限流事件时,立即弹出对话框提醒开发者
  • 包含具体的限流原因和相关上下文信息
  • 帮助开发者及早发现和解决潜在问题

比如下面的截图就是某个 HTTP Request 触发了 Ratelimit 规则的 Warning 提示

image.png

生产环境监控

在生产环境中:

  • 通过Telemetry系统上报限流事件
  • 收集用户行为数据进行分析
  • 持续优化限流策略和阈值

下图是线上触发 Ratelimit 规则的数据情况

image.png

6. 总结

Rate Limit 框架的设计与实现为客户端提供了统一、灵活、可扩展的限流解决方案,不仅提升了系统稳定性,还优化了用户体验。