ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Fli 并发与限流设计揭秘:Google Flights 搜索库的令牌桶、线程池与日期扫描熔断机制完整剖析

Fli 并发与限流设计揭秘:Google Flights 搜索库的令牌桶、线程池与日期扫描熔断机制完整剖析 Fli 并发与限流设计揭秘Google Flights 搜索库的令牌桶、线程池与日期扫描熔断机制完整剖析【免费下载链接】fliGoogle Flights MCP, CLI and Python Library项目地址: https://gitcode.com/gh_mirrors/fli2/fliFli 是一个 Google Flights 机票搜索工具链提供 Python 库、命令行工具CLI、MCP 服务器和 TypeScript 移植版支持机票搜索与跨日期价格扫描。它背后藏着一套精巧的并发与限流设计——令牌桶、全局共享线程池、日期扫描熔断机制。本文带你用源码视角看懂这三道关卡为什么一个爬虫式的搜索库要做到又快又不被拦为什么 Google Flights 搜索库必须做并发控制先搞清楚 Fli 面对的现实约束速率天花板Google 的公开限流约10 次请求/秒超了就容易被拦日期扫描很贵Google 搜索页没有日历网格接口扫描一个日期范围时每一天都要单独抓一整页约 2 MB一次扫描上限 93 天意味着近 200 MB 的流量调用方是并发的CLI、MCP 服务器、Python/TS 库的用户都可能在同时触发并行搜索。不控速 被 Google 拦无脑并发 线程爆炸。Fli 的解法分三层限流 → 线程池 → 熔断。第一道关卡令牌桶限流器 TokenBucketRateLimiter限流是整套设计的核心实现位于 fli/search/_concurrency.pylimiter TokenBucketRateLimiter(calls10, period1.0) limiter.acquire() # 阻塞到拿到一个令牌再发请求它的行为很直白桶初始是满的10 个令牌并按容量/周期的速率10 个/秒持续补充补充算法基于单调时钟令牌 经过秒数 × 补充速率见 _concurrency.py#L83-L89 的_refill每次 HTTP 请求前先acquire()一个令牌拿不到就按到达顺序排队等待等待时长精确计算为缺额 ÷ 补充速率还能带超时为什么用threading.Condition而不是信号量因为信号量只会扣数而令牌桶需要随时间补水——这个细节写在模块注释里_concurrency.py#L21-L34。TypeScript 版fli-js/src/search/concurrency.ts#L65-L140是 1:1 移植用单线程事件循环替代了锁一条 FIFO 的 Promise 队列保证等待者按顺序醒来拿令牌。新手划重点令牌桶的价值在于全局一份预算。Fli 的 HTTP 客户端是进程级单例见 client.py#L324-L341 的get_client()双重检查锁所以所有线程、所有调用方共享同一个限流器——10 req/s 的天花板对整个进程只有一份不会被嵌套搜索悄悄放大。第二道关卡全局共享线程池与 parallel_map限流管住了速度线程池管住并发宽度。fli/search/_concurrency.py 提供三个部件get_executor()模块级、懒加载的单例ThreadPoolExecutor默认 10 个 worker——刻意对齐 10 req/s 的限流上限限流器反正只会放行这么多再多线程也白等源码注释原话configure_concurrency(n)按需调整 worker 上限调大时直接丢弃旧池、重建新池parallel_map(fn, items)唯一的对外并发入口把fn并行应用到每个元素结果按输入顺序返回。parallel_map有两个值得学习的设计快速路径任务数 ≤ 1 或 worker 数为 1 时直接同步循环执行避免为琐碎输入付出提交/等待的调度开销parallel_map 实现错误契约任一 worker 抛错先等其余在途任务完成再重抛第一个异常——不取消兄弟任务因为它们手里握着限流令牌让它跑完能干净释放。⚠️ 还有一个防死锁细节日期扫描刻意用一层扁平的parallel_map处理全部日期。源码注释解释得很清楚dates.py#L330-L350如果在每个分块的 worker 里再嵌套第二层parallel_map两层共用同一个有界线程池外层任务可能占满所有 worker 后阻塞等待内层任务——而内层任务永远排不上队直接死锁。TS 版的 parallelMap 用worker 数量受限的 Promise.all实现同样的语义min(workers, n)个协程轮询消费共享队列结果同样按输入顺序输出。第三道关卡日期扫描熔断机制 _SweepHealth这是 Fli 最有意思的设计位于 fli/search/dates.py。问题场景日期扫描中一个日期失败可能代价巨大——客户端重试 3 次 × 页面抓取重试 3 次 单个日期最多烧掉 9 个 HTTP 请求。如果客户端正在被 Google 拦截典型如欧盟 IP 遇到同意页那么 93 个日期会以完全相同的方式失败——花 93 倍代价去学一个事实是最亏本的买卖。Fli 的答案是一个轻量熔断器_SweepHealthdates.py#L95-L141规则只有三条规则说明只统计确定性失败只有页面 200 但拿不到ds:1载荷同意页/拦截页计入次数超时、断连不计——那只是网络抖动推不出后面的日期也会失败只在首次成功前生效连续 5 次SWEEP_FAILURE_THRESHOLD 5空页面且零成功 → 熔断跳闸一旦任何一个日期加载成功熔断器永久解除武装后续抖动照常享受完整重试预算跳闸即止损排队中的日期在发请求前先查熔断器_price_one_date 检查点跳闸则直接跳过、零开销配套的还有硬性上限MAX_DATES_PER_SEARCH 93dates.py#L64-L72防止2026-01-01 到 12-31这种请求悄悄发出 365 个请求。最后_collectdates.py#L410-L561把结局分成清晰的几类全部失败→抛错、熔断跳闸→抛错或价格真实但不完整警告、一半以上没加载→抛错不能由少数加载页得出没航班的结论、部分失败→只打一行警告。绝不把传输挂了伪装成这条航线没航班。限流与重试如何配合限流负责匀速重试负责扛住抖动两者在客户端层交汇Python 版fli/search/client.pyget/post方法先rate_limiter.acquire()再发请求外层套 tenacity 重试装饰器——最多 3 次、指数退避而证书错误明确不重试确定性失败重试只是浪费时间TypeScript 版fli-js/src/search/client.ts手写重试循环退避时长为backoffMs × 2^attempt且退避睡眠可被 AbortSignal 打断——用户取消后不会傻等 4 秒退避会话按线程隔离curl_cffi的 Session 底层是 libcurl 句柄不可跨线程共享所以每个 worker 线程用自己的 Sessionthreading.local但限流预算仍是全局共享的那一份。 一句话总结这套组合拳令牌桶定上限、线程池定宽度、重试扛抖动、熔断止损血。新手能带走的 5 个设计要点限流器全局唯一预算挂在进程级单例客户端上任何嵌套/并行调用都不会放大总量worker 数对齐限流上限默认 10 个线程对应 10 req/s不养只会干等的线程熔断只统计确定性失败网络抖动不触发熔断被拦截才触发熔断器只在首次成功前武装健康的扫描永远保留完整重试预算快路径零开销单个任务直接同步执行并发基建不为琐碎场景付出代价。延伸阅读想动手验证相关测试是最好的活文档限流器与线程池tests/search/test_concurrency.py日期扫描与熔断器含跳闸后跳过的场景tests/search/test_date_sweep.py客户端单例与错误分类tests/search/test_client.py快速上手搜索docs/python/quickstart.md完整示例集examples/python/ 与 examples/typescript/【免费下载链接】fliGoogle Flights MCP, CLI and Python Library项目地址: https://gitcode.com/gh_mirrors/fli2/fli创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表