Skip to content

Commit 0970570

Browse files
author
ShellMonster
committed
feat: 新增域名级QPS限流功能
新增功能: - 支持按域名配置独立的QPS限制 - 支持精确匹配和通配符匹配(*.example.com) - 支持单机模式(LocalTokenBucket)和分布式模式(RedisTokenBucket) - 令牌桶算法实现精确的QPS控制 架构设计: - QPSScheduler:单线程调度器,管理DelayHeap和ReadyQueue - DomainRateLimiter:域名级限流管理器,自动路由到对应令牌桶 - 零侵入性:QPS关闭时代码流程与原始完全一致 支持的爬虫类型: - AirSpider(单机内存队列) - Spider/BatchSpider/TaskSpider(分布式Redis队列) 配置项: - DOMAIN_RATE_LIMIT_ENABLE:是否启用 - DOMAIN_RATE_LIMIT_RULES:域名QPS规则 - DOMAIN_RATE_LIMIT_DEFAULT:默认QPS - DOMAIN_RATE_LIMIT_STORAGE:存储模式(local/redis) 测试验证: - 单机QPS精度测试:误差 < 2% - 分布式多进程共享QPS测试:2进程共享配额,误差 1.7% 文档: - 新增 docs/source_code/域名级QPS限流.md Author: ShellMonster
1 parent 100cde4 commit 0970570

21 files changed

Lines changed: 3838 additions & 7 deletions

PR_DESCRIPTION.md

Lines changed: 434 additions & 0 deletions
Large diffs are not rendered by default.

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* 使用进阶
1919
* [请求-Request](source_code/Request.md)
2020
* [响应-Response](source_code/Response.md)
21+
* [域名级QPS限流](source_code/域名级QPS限流.md)
2122
* [代理使用说明](source_code/proxy.md)
2223
* [用户池说明](source_code/UserPool.md)
2324
* [浏览器渲染-Selenium](source_code/浏览器渲染-Selenium.md)
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# 域名级QPS限流
2+
3+
域名级QPS限流功能可以针对不同域名配置独立的请求频率限制,防止因请求过快被目标网站封禁,同时支持单机和分布式两种模式。
4+
5+
## 1. 功能特点
6+
7+
- **域名级别控制**:可为不同域名配置不同的QPS限制
8+
- **精确控制**:基于令牌桶算法,QPS控制误差 < 2%
9+
- **通配符支持**:支持 `*.example.com` 匹配所有子域名
10+
- **分布式支持**:多进程/多机器可共享同一QPS配额
11+
- **零侵入性**:关闭时不影响原有流程和性能
12+
13+
## 2. 配置说明
14+
15+
`setting.py``__custom_setting__` 中配置:
16+
17+
```python
18+
# 域名级QPS限流配置
19+
DOMAIN_RATE_LIMIT_ENABLE = False # 是否启用,默认关闭
20+
DOMAIN_RATE_LIMIT_DEFAULT = 0 # 默认QPS限制,0表示不限制
21+
DOMAIN_RATE_LIMIT_RULES = {} # 域名QPS规则
22+
DOMAIN_RATE_LIMIT_MAX_PREFETCH = 100 # 最大预取请求数
23+
DOMAIN_RATE_LIMIT_STORAGE = "local" # 存储模式:local/redis
24+
```
25+
26+
### 配置项详解
27+
28+
| 配置项 | 类型 | 默认值 | 说明 |
29+
|-------|------|-------|------|
30+
| `DOMAIN_RATE_LIMIT_ENABLE` | bool | False | 是否启用QPS限流 |
31+
| `DOMAIN_RATE_LIMIT_DEFAULT` | int | 0 | 默认QPS,0表示不限制 |
32+
| `DOMAIN_RATE_LIMIT_RULES` | dict | {} | 域名QPS规则字典 |
33+
| `DOMAIN_RATE_LIMIT_MAX_PREFETCH` | int | 100 | 最大预取数,防止内存溢出 |
34+
| `DOMAIN_RATE_LIMIT_STORAGE` | str | "local" | 存储模式,local或redis |
35+
36+
### 存储模式
37+
38+
| 模式 | 说明 | 适用场景 |
39+
|-----|------|---------|
40+
| `local` | 本地内存存储 | AirSpider、单进程爬虫 |
41+
| `redis` | Redis分布式存储 | 多进程/多机器部署,需共享QPS配额 |
42+
43+
## 3. 使用示例
44+
45+
### AirSpider 使用
46+
47+
```python
48+
import feapder
49+
50+
51+
class MySpider(feapder.AirSpider):
52+
__custom_setting__ = dict(
53+
DOMAIN_RATE_LIMIT_ENABLE=True,
54+
DOMAIN_RATE_LIMIT_RULES={
55+
"www.baidu.com": 2, # 百度限制 2 QPS
56+
"*.taobao.com": 5, # 淘宝全域名限制 5 QPS
57+
},
58+
DOMAIN_RATE_LIMIT_DEFAULT=10, # 其他域名默认 10 QPS
59+
)
60+
61+
def start_requests(self):
62+
yield feapder.Request("https://www.baidu.com/s?wd=test1")
63+
yield feapder.Request("https://www.baidu.com/s?wd=test2")
64+
yield feapder.Request("https://item.taobao.com/item1")
65+
yield feapder.Request("https://detail.taobao.com/item2")
66+
67+
def parse(self, request, response):
68+
print(f"处理: {request.url}")
69+
70+
71+
if __name__ == "__main__":
72+
MySpider(thread_count=10).start()
73+
```
74+
75+
上述代码中:
76+
- `www.baidu.com` 的请求频率被限制为每秒2次
77+
- `*.taobao.com` 匹配 `item.taobao.com``detail.taobao.com` 等,限制为每秒5次
78+
- 其他未匹配的域名,使用默认限制每秒10次
79+
80+
### Spider 分布式使用
81+
82+
```python
83+
import feapder
84+
85+
86+
class MyDistributedSpider(feapder.Spider):
87+
__custom_setting__ = dict(
88+
REDISDB_IP_PORTS="localhost:6379",
89+
REDISDB_USER_PASS="",
90+
REDISDB_DB=0,
91+
92+
# QPS限流配置
93+
DOMAIN_RATE_LIMIT_ENABLE=True,
94+
DOMAIN_RATE_LIMIT_STORAGE="redis", # 使用Redis,多进程共享配额
95+
DOMAIN_RATE_LIMIT_RULES={
96+
"api.example.com": 10, # API限制 10 QPS
97+
},
98+
DOMAIN_RATE_LIMIT_DEFAULT=20,
99+
)
100+
101+
def start_requests(self):
102+
for i in range(100):
103+
yield feapder.Request(f"https://api.example.com/data/{i}")
104+
105+
def parse(self, request, response):
106+
print(f"处理: {request.url}")
107+
108+
109+
if __name__ == "__main__":
110+
MyDistributedSpider(redis_key="test:spider").start()
111+
```
112+
113+
分布式模式下,多个进程共享同一个Redis令牌桶,确保所有进程合计的QPS不超过配置值。
114+
115+
### 混合限制示例
116+
117+
```python
118+
__custom_setting__ = dict(
119+
DOMAIN_RATE_LIMIT_ENABLE=True,
120+
DOMAIN_RATE_LIMIT_RULES={
121+
"www.baidu.com": 2, # 精确匹配
122+
"*.taobao.com": 5, # 通配符匹配
123+
"api.jd.com": 10, # 精确匹配
124+
},
125+
DOMAIN_RATE_LIMIT_DEFAULT=0, # 其他域名不限制
126+
)
127+
```
128+
129+
匹配优先级:
130+
1. **精确匹配**:先检查域名是否完全匹配
131+
2. **通配符匹配**:检查是否匹配 `*.xxx.com` 模式
132+
3. **默认值**:使用 `DOMAIN_RATE_LIMIT_DEFAULT`
133+
134+
## 4. 工作原理
135+
136+
### 架构图
137+
138+
```
139+
┌──────────────────────────────────────┐
140+
│ QPSScheduler │
141+
│ │
142+
Request ───────▶ │ ┌────────────────────────────────┐ │
143+
│ │ DomainRateLimiter │ │
144+
│ │ ┌──────────┐ ┌──────────────┐ │ │
145+
│ │ │ 令牌桶1 │ │ 令牌桶2 │ │ │
146+
│ │ │(baidu) │ │ (*.taobao) │ │ │
147+
│ │ └──────────┘ └──────────────┘ │ │
148+
│ └────────────────────────────────┘ │
149+
│ │ │
150+
│ ▼ │
151+
│ ┌─────────────┐ ┌─────────────┐ │
152+
│ │ DelayHeap │ │ ReadyQueue │ │
153+
│ │ (等待队列) │ │ (就绪队列) │ │
154+
│ └─────────────┘ └─────────────┘ │
155+
│ │ │
156+
└──────────────────────────│──────────┘
157+
158+
ParserControl
159+
(消费处理)
160+
```
161+
162+
### 令牌桶算法
163+
164+
采用令牌桶算法实现精确的QPS控制:
165+
166+
1. **令牌生成**:按配置的QPS速率持续生成令牌
167+
2. **令牌消费**:每个请求消费一个令牌
168+
3. **预扣机制**:请求到达时立即预扣令牌,返回等待时间
169+
4. **排队等待**:令牌不足时,请求进入延迟队列等待
170+
171+
### 分布式模式
172+
173+
分布式模式使用Redis + Lua脚本实现:
174+
175+
```
176+
进程A ──┐
177+
├──▶ Redis令牌桶 ──▶ 统一QPS配额
178+
进程B ──┘
179+
180+
Lua脚本保证操作原子性,避免竞争条件
181+
```
182+
183+
## 5. 支持的爬虫类型
184+
185+
| 爬虫类型 | 支持 | 推荐存储模式 |
186+
|---------|-----|-------------|
187+
| AirSpider || local |
188+
| Spider || redis(多进程时) |
189+
| BatchSpider || redis(多进程时) |
190+
| TaskSpider || redis(多进程时) |
191+
192+
## 6. 注意事项
193+
194+
1. **QPS=0 表示不限制**:配置为0的域名或默认值为0时,对应请求不受QPS限制
195+
196+
2. **多进程必须用Redis模式**`local` 模式下每个进程有独立的令牌桶,无法共享配额
197+
198+
3. **预取数量**`DOMAIN_RATE_LIMIT_MAX_PREFETCH` 控制调度器预取的请求数,过大会占用内存,过小可能影响性能
199+
200+
4. **性能影响**:QPS关闭时(`DOMAIN_RATE_LIMIT_ENABLE=False`),代码流程与原始完全一致,无任何性能损耗
201+
202+
5. **通配符匹配**`*.example.com` 可匹配 `a.example.com``b.c.example.com` 等,但不匹配 `example.com` 本身
203+
204+
## 7. 完整代码示例
205+
206+
### 示例1:基础使用
207+
208+
```python
209+
import feapder
210+
211+
212+
class BasicQPSSpider(feapder.AirSpider):
213+
__custom_setting__ = dict(
214+
DOMAIN_RATE_LIMIT_ENABLE=True,
215+
DOMAIN_RATE_LIMIT_RULES={
216+
"httpbin.org": 2, # 限制 2 QPS
217+
},
218+
)
219+
220+
def start_requests(self):
221+
for i in range(10):
222+
yield feapder.Request(f"https://httpbin.org/get?id={i}")
223+
224+
def parse(self, request, response):
225+
print(f"状态码: {response.status_code}, URL: {request.url}")
226+
227+
228+
if __name__ == "__main__":
229+
BasicQPSSpider(thread_count=10).start()
230+
```
231+
232+
### 示例2:分布式多进程
233+
234+
```python
235+
import feapder
236+
237+
238+
class DistributedQPSSpider(feapder.Spider):
239+
__custom_setting__ = dict(
240+
REDISDB_IP_PORTS="localhost:6379",
241+
REDISDB_USER_PASS="",
242+
REDISDB_DB=0,
243+
244+
DOMAIN_RATE_LIMIT_ENABLE=True,
245+
DOMAIN_RATE_LIMIT_STORAGE="redis",
246+
DOMAIN_RATE_LIMIT_RULES={
247+
"api.example.com": 5, # 多进程合计 5 QPS
248+
},
249+
)
250+
251+
def start_requests(self):
252+
for i in range(50):
253+
yield feapder.Request(f"https://api.example.com/item/{i}")
254+
255+
def parse(self, request, response):
256+
print(f"处理: {request.url}")
257+
258+
259+
if __name__ == "__main__":
260+
# 可启动多个进程,共享 5 QPS 配额
261+
DistributedQPSSpider(redis_key="qps:spider").start()
262+
```
263+
264+
## 8. 常见问题
265+
266+
### Q: QPS设置了但没生效?
267+
268+
A: 检查以下几点:
269+
1. `DOMAIN_RATE_LIMIT_ENABLE` 是否为 `True`
270+
2. 域名规则是否正确匹配(注意 `www.baidu.com``baidu.com` 是不同的)
271+
3. 分布式模式下是否配置了 `DOMAIN_RATE_LIMIT_STORAGE="redis"`
272+
273+
### Q: 多进程QPS不准确?
274+
275+
A: 确保使用 `redis` 存储模式,`local` 模式下各进程独立计算,无法共享配额。
276+
277+
### Q: 如何关闭某个域名的限制?
278+
279+
A: 将该域名的QPS设置为0,或不在规则中配置该域名且 `DOMAIN_RATE_LIMIT_DEFAULT=0`

0 commit comments

Comments
 (0)