开篇:数据源不是"永远在线"的
在构建实时行情系统的第一天,你满怀信心地申请了一个免费数据源的 API Key,写了一个简单的 Python 脚本开始拉取数据。前 10 分钟一切顺利,你甚至开始规划下一步的存储方案。但很快,日志里开始出现 429 Too Many Requests,随后是 Connection refused,最后干脆彻底断连。你懵了:明明数据源承诺"免费版支持每分钟 60 次调用",为什么还是被限流了?更糟糕的是,断流之后系统直接停摆,直到你手动重启。
这不是虚构的场景,而是每一个实时数据系统开发者都会遇到的第一道槛:数据源的限流与断流。与传统的内部服务不同,外部数据源是不可控的。你无法要求对方扩容,也无法提前预知它们的限流策略何时收紧。你唯一能做的,就是让自己的客户端足够"聪明":既能优雅地遵守限流规则,又能在断流时快速恢复。
本文将从限流策略的识别与适配、自适应请求调度、断线重连与多源冗余三个层面,深入剖析如何应对数据源的"限流"与"断流",并提供可复用的代码实现。最后,我们将对比几种主流数据源的限流特性,帮助你在选型时做出更明智的决策。
一、限流策略的识别与适配
限流是数据源最常用的保护手段。不同类型的限流策略,对客户端的适配要求完全不同。
1.1 常见的限流维度
- QPS(每秒请求数):最常见,如"每秒最多 5 次请求"。
- 日调用量:按自然日或滚动 24 小时统计,超出后直接拒绝。
- 并发连接数:针对 WebSocket 长连接,限制同时打开的连接数。
- 配额重置周期:分钟、小时、天,重置时可能"突增"。
数据源通常会在 HTTP 响应头中返回限流信息。例如:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1640995200
客户端必须解析这些头部,动态调整请求频率。
1.2 适配不同限流策略的客户端设计
一个健壮的客户端应包含以下组件:
python
import time
import requests
from threading import Lock
class RateLimiter:
"""通用限流器,支持 QPS 和剩余配额自适应"""
def __init__(self, qps=None):
self.qps = qps
self.last_request = 0
self.lock = Lock()
self.remaining = None
self.reset_time = None
def acquire(self):
with self.lock:
# 基于 QPS 限制
if self.qps:
now = time.time()
interval = 1.0 / self.qps
if now - self.last_request < interval:
time.sleep(interval - (now - self.last_request))
self.last_request = time.time()
# 基于剩余配额限制(如果数据源返回了头部)
if self.remaining is not None and self.remaining <= 0:
wait = self.reset_time - time.time()
if wait > 0:
time.sleep(wait)
def update_from_response(self, headers):
"""从响应头更新限流状态"""
if 'X-RateLimit-Remaining' in headers:
self.remaining = int(headers['X-RateLimit-Remaining'])
if 'X-RateLimit-Reset' in headers:
self.reset_time = int(headers['X-RateLimit-Reset'])
1.3 处理"突增"与"冷却"
有些数据源的限流是"令牌桶"模型:允许短时间内突发请求,但平均速率受限。此时,我们可以在客户端维护一个简单的令牌桶,既允许突发,又保证长期不超限。
python
class TokenBucket:
def __init__(self, capacity, rate):
self.capacity = capacity
self.rate = rate
self.tokens = capacity
self.last_refill = time.time()
def acquire(self, tokens=1):
while True:
now = time.time()
self.tokens = min(self.capacity, self.tokens + (now - self.last_refill) * self.rate)
self.last_refill = now
if self.tokens >= tokens:
self.tokens -= tokens
return
time.sleep((tokens - self.tokens) / self.rate)
二、自适应请求调度:在合规前提下最大化吞吐
仅仅遵守限流规则还不够,我们还要在限流范围内尽可能多地获取数据。这就需要自适应调度。
2.1 动态调整批量大小
如果数据源支持批量请求(如一次请求获取 100 只股票的数据),那么我们可以动态调整批量大小:当剩余配额充裕时,用小批量快速拉取;当配额紧张时,用大批量减少请求次数。
python
class AdaptiveBatcher:
def __init__(self, min_batch=1, max_batch=100):
self.min_batch = min_batch
self.max_batch = max_batch
self.current_batch = min_batch
def adjust(self, remaining_quota):
"""根据剩余配额调整批量大小"""
if remaining_quota > 50:
self.current_batch = min(self.max_batch, self.current_batch + 10)
elif remaining_quota < 10:
self.current_batch = max(self.min_batch, self.current_batch - 5)
2.2 请求优先级队列
当系统同时需要拉取历史数据和实时数据时,可以设置优先级:实时数据优先,历史数据在配额充足时再补。
python
import heapq
class PriorityRequestQueue:
def __init__(self):
self.queue = [] # (priority, timestamp, request)
def push(self, request, priority=5):
heapq.heappush(self.queue, (priority, time.time(), request))
def pop(self):
if self.queue:
return heapq.heappop(self.queue)[2]
三、断线重连与多源冗余:让系统"永不掉线"
限流是"主动限制",断流则是"被动中断"。网络抖动、服务端重启、负载均衡切换都可能导致连接断开。应对断流,我们需要重连策略,更高级的做法是多源冗余。
3.1 智能重连策略
指数退避 + 抖动是标配,但还需考虑:
- 区分临时性故障与永久性故障 :如果返回
401 Unauthorized(API Key 无效),不应重试。 - 重试上限:连续重试 10 次仍失败,应切换到备用数据源。
python
async def request_with_retry(url, max_retries=10, base_delay=1):
for attempt in range(max_retries):
try:
response = await http.get(url)
if response.status == 429:
# 限流,等待后重试
wait = int(response.headers.get('Retry-After', base_delay * (2 ** attempt)))
await asyncio.sleep(wait)
continue
if response.status >= 500:
# 服务端错误,重试
await asyncio.sleep(base_delay * (2 ** attempt))
continue
return response
except Exception as e:
if attempt == max_retries - 1:
raise
await asyncio.sleep(base_delay * (2 ** attempt))
3.2 多源冗余与自动切换
生产级系统通常同时接入多个数据源,形成主备或负载均衡。当主数据源不可用时,自动切换到备用。
python
class DataSourceManager:
def __init__(self, sources):
self.sources = sources # [{'name':'sourceA', 'client':...}, ...]
self.primary = 0
self.fail_count = [0] * len(sources)
async def fetch(self, *args, **kwargs):
for i in range(len(self.sources)):
idx = (self.primary + i) % len(self.sources)
try:
result = await self.sources[idx]['client'].fetch(*args, **kwargs)
self.fail_count[idx] = 0
self.primary = idx # 提升成功的源为主源
return result
except Exception as e:
self.fail_count[idx] += 1
if self.fail_count[idx] >= 3:
# 临时标记为不可用,可后续异步检测恢复
pass
continue
raise Exception("All data sources failed")
四、数据源限流特性对比
不同数据源的限流策略差异巨大,直接影响客户端的设计复杂度。以下对比三家代表性数据源:
| 数据源 | 限流维度 | 配额重置 | 客户端友好特性 | 适用场景 |
|---|---|---|---|---|
| Finnhub | QPS(免费版 60/min) | 每分钟 | 响应头包含剩余配额,支持 WebSocket 订阅减少 REST 调用 | 个人研究、中等频率 |
| TickDB | 订阅制套餐,根据套餐等级提供相应配额 | 订阅周期 | 支持 WebSocket 推送,无需频繁轮询;提供用量仪表盘 | 生产级实时系统 |
| Polygon.io | 按套餐限流(基础版 5 req/s) | 每秒 | 响应头完整,支持批量化请求 | 美股高频策略 |
从表中可以看出,选择数据源时,除了考虑价格和覆盖范围,还应评估其限流策略是否与自己的访问模式匹配。例如,如果主要通过 WebSocket 获取实时推送,TickDB 的订阅制模式可以简化配额管理;如果需要灵活的历史查询,Polygon.io 的批量接口则更具优势。
五、完整实现:一个能"抗揍"的数据采集器
整合上述所有要素,下面是一个简化的数据采集器核心类,支持限流自适应、重试、多源切换:
python
import asyncio
import aiohttp
import random
import time
class RobustDataFetcher:
def __init__(self, sources, rate_limiter):
self.sources = sources
self.rate_limiter = rate_limiter
self.current_source = 0
async def fetch(self, url, **kwargs):
for _ in range(len(self.sources)):
source = self.sources[self.current_source]
try:
# 限流器控制
self.rate_limiter.acquire()
async with aiohttp.ClientSession() as session:
async with session.get(url, **source.get('headers', {})) as resp:
if resp.status == 429:
# 限流,更新限流器状态
self.rate_limiter.update_from_response(resp.headers)
# 根据 Retry-After 等待
retry_after = int(resp.headers.get('Retry-After', 1))
await asyncio.sleep(retry_after)
continue
if resp.status >= 500:
# 服务端错误,尝试下一个源
self._switch_source()
continue
return await resp.json()
except Exception as e:
print(f"Source {source['name']} failed: {e}")
self._switch_source()
raise Exception("All sources exhausted")
def _switch_source(self):
self.current_source = (self.current_source + 1) % len(self.sources)
print(f"Switched to {self.sources[self.current_source]['name']}")
总结与延伸
应对数据源的限流与断流,本质上是在不确定性中构建确定性。本文从三个层面给出了解决方案:
- 限流适配:解析响应头,动态调整请求频率,令牌桶算法应对突发。
- 自适应调度:批量大小动态调整,优先级队列保证实时数据优先。
- 断流恢复:指数退避重试,多源冗余自动切换。
在实际项目中,数据源的选择也至关重要。如果希望简化客户端复杂度,可以考虑像 TickDB 这样提供统一 WebSocket 推送、套餐模式清晰的数据源,让开发者更专注于业务逻辑。当然,你也可以在 ClawHub 上搜索 "real-time market data" 探索更多开源或商业方案,选择最适合自己场景的工具。
本文仅作为技术实践分享,所展示的数据来源于公开的行情 API,不构成任何投资建议。市场有风险,投资需谨慎。