跳转至

连接池与缓存

这个项目相比同类实现快 约 2 倍,来源就是这两块。

缓存

两级结构

内存 LRU  ──miss──▶  磁盘 SQLite (WAL)  ──miss──▶  回源网络
   ↑                        ↑
  快,进程内              跨进程 / 跨重启持久
  • 内存层:进程内 LRU,命中就是字典查找
  • 磁盘层:单文件 SQLite,开 WAL 模式,可多进程共享
  • 未传 cache_dir 时只有内存层(进程退出即失效)

TTL 分级

不同数据的「新鲜度要求」差了几个数量级,所以 TTL 是分级的:

数据类型 TTL 理由
五档报价 5s 变化最快
分钟 K 线 60s 边界处才会新增一根
日 K 线 当天 当天数据在盘中会变
复权 K 线 1 天 复权因子一天最多变一次
历史逐笔 永久 已经成交的不会变
证券列表 1 天 只有新股上市才变

缓存的是「已解析记录」

缓存里存的是解析好的 dataclass 对象(codec/payload.py 做 pickle + zlib), 不是原始响应字节。所以缓存命中时是 0 网络 + 0 解析,只有一次反序列化。

这也是为什么重复请求能从 0.31s 降到 0.016s。

single-flight

同一时刻对同一个缓存键的并发请求会自动合并:第一个去回源,其余的等待并共享结果。

# 100 个协程同时请求同一只股票的同一段 K 线
# → 只发 1 次网络请求
results = await asyncio.gather(*[
    td.get_bars("sh600000", KlinePeriod.DAY, count=800)
    for _ in range(100)
])

上限由 max_inflight(默认 64)控制。

缓存键会做规范化——get_quotes 的键是排序去重后的股票集合, 所以 [A, B] 和 [B, A] 共享缓存。

连接池

动态扩缩容

参数 默认 含义
pool_min 3 保底连接数
pool_max 12 上限
mac_pool_cap 4 单台 MAC 服务器连接上限
timeout 3.0 连接超时(秒)

按等待队列长度与空闲时长动态扩缩容,不是启动时就拉满。

健康分与选路

每台服务器维护一个健康分,取连接时按健康分 + EWMA 延迟综合选路:

事件 健康分变化
请求失败 × 0.5
请求成功 + 0.2
连续失败 进入 120s 冷却,冷却期内不再被选中

失败会跨服务器故障转移——重试用的是另一台,而不是同一台重连。

EWMA(指数加权移动平均)延迟让选路对「最近变慢」敏感,而不是被历史均值拖住。

MAC 通道的单台连接上限

mac_pool_cap 默认 4,与标准池的 std_pool_cap 一致——这是对端单台连接限制, 不是本项目的选择。两个通道的可用台数见 通道与数据源: 实测可达台数会随服务器上下线变化,以当日测速缓存为准。

并发分页

大请求会被拆成多页,每页带显式 start 偏移——无状态,所以可以并行。 多连接并行拉取 → 按序拼接 → 某页失败就换连接重试该页。

count=2400  →  3 页  →  3 个连接并行  →  按序拼接

两种例外:

场景 策略 原因
KlinePeriod.MIN_1 单连接串行 各服务器历史深度不一致,并发会导致非确定性空洞
MAC 通道复权 K 线 单连接串行(_fetch_mac_kline_serial) 同上

调参建议

concurrency 超过 pool_max 之后只会排队。实践值 8~16。 MAC 类请求(逐笔 / 分时)并发能力受 mac_pool_cap 限制,调大 concurrency 无用。

下一步