|
| 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