Posts 归档放在 GitHub Release 上,先后撞上的两个上限
Post
Cancel

归档放在 GitHub Release 上,先后撞上的两个上限

  每天凌晨,导出脚本 export-daily.mjs 把前天的前一天从 PostgreSQL 导成 csv.gz / jsonl.gz,中间留一天修数据。每个文件都作为资产传到归档仓库的 GitHub Release(tag 是 daily-<日期>),然后把 manifest 和 CHECKSUM 提交进仓库,最后发布。交付走 Telegram bot:客户要哪段数据,bot 就按 manifest 从 Release 下载文件,逐个校验 sha256,打成 tar 传到 R2,再把预签名 URL 发出去。

  这套东西一直跑得挺好,直到 7 月先后撞上了 GitHub 的两个上限。

第一个:单个 Release 最多 1000 个资产

  7 月 1 号,归档后的巡检报红:manifest 读不到,累计 K 线表里也没有那一天。原因是导出 06-29 这天时,在上传第 1001 个资产那一步,GitHub 返回了 422 file_count limited to 1000 assets per release,脚本中途抛错,后面写 manifest、写 K 线那几步都没走到。

  这次是导出脚本出了问题。Predict.fun 的盘口当时按 category_slug 导出,每个市场一个文件。BTC/ETH/BNB 的 5 分钟和 15 分钟市场每天有几百个槽位,光盘口就有大约 819 个文件。再算上 Polymarket 文件、K 线和快照,一天超过 1180 个资产。ETH 和 BNB 原来只有日级市场,后来多了密集的 5m/15m 槽位。06-28 还是 672 个,06-29 第一次越过 1000。

  我改成按 <ASSET>-<INTERVAL> 聚合,BTC-5M、BTC-15M、BTC-DAILY 每个系列一个文件。每一行仍然带着 market_id 和 category_slug,信息没少。category_slug 为空的市场兜底放到 MARKET-<id>,不丢数据。聚合后,盘口从 819 个文件降到 11 个以内,一天总资产约 217 个。

  上传逻辑也改了两处:

  1. 列已有资产改成分页请求 /releases/{id}/assets?per_page=100&page=N,不再用 release 对象里内嵌的 assets 数组。内嵌的那个不保证完整,而后面有一步是「删掉不在清单里的旧资产」,拿一个残缺的清单去做删除是很危险的。非 200 的响应(包括 404)一律抛错。
  2. 在删除和上传之前先做预检:文件数超过 1000 或者 basename 有重复,直接抛错,不去碰一个已经发布的 release。

  导出本身是幂等的,重跑一次 node scripts/export-daily.mjs 2026-06-29:分页删掉那 1000 个旧资产,传上 217 个新的,提交 manifest,发布,巡检转绿。

历史数据还是旧格式

  这个修复只对之后的数据生效。06-12 到 06-28 这 17 天,每天还是 470 到 518 个盘口文件,一共 7965 个。7 月 14 号 Predict.fun 的数据单独开卖以后,这批旧格式的文件开始变成问题:客户拿到的 tar 里文件数爆炸,格式也和后面的不一致。

  7 月 18 号写了一个重聚合的迁移脚本 reaggregate-predict-orderbook.mjs。数据库里那段时间的原始数据早就按保留策略清掉了,只能从现有的 Release 文件重新合并。合并规则直接复用导出脚本里的 predictOrderbookSeries,行逐字搬运不做任何改写,按 recv_ms 排序,并且断言读入行数、旧文件行数、新文件行数三者相等,不等就抛。默认是 dry-run,加 --execute 才真写。

  真写时要改已经发布的 Release,我把流程写成可以从任意中间状态恢复:上传新文件,修改 manifest 和 CHECKSUM,提交推送,把 tag 指到新的 HEAD,最后才删掉 manifest 之外的旧盘口文件。任何一步崩了,重跑都会对账到目标状态。工作区不干净时,启动和删除前都会拒绝执行。这个脚本复审了五轮。

  跑完的结果:盘口文件 7965 变成 269,全量 predict 从 8462 个文件变成 766 个,少了 11 倍。每天行数守恒,重跑全部 skip。

第二个:每小时 5000 次请求

  同一天,我在处理另一个问题:全量 predict 的交付太慢了。stageArchivedFiles 原来是一个一个串行地下载、校验,06-12 到 07-16 一共大约 8462 个文件,要花 2 个小时左右。

  我先加了一个有界并发池 mapConcurrent(默认 8 路),配套三件事:

  • makePacer 做全局节流,所有并发 lane 共享,默认 75ms 一个请求,大约每分钟 800 个,压在 GitHub 的 secondary rate limit(大约每分钟 900)下面。
  • withRetry 只对 403、429、5xx 和网络错误重试,遵守 Retry-After(秒数和 HTTP-date 两种格式都要认)和 x-ratelimit-reset,最多等 60 秒。sha256 不匹配立即抛,不重试。
  • 并发数和节流间隔都可以用环境变量 DELIVERY_CONCURRENCY / GITHUB_MIN_INTERVAL_MS 调。

  按这个吞吐,全量 predict 从 2 小时降到了 11 分钟左右。可真跑全量的时候,请求很快被 403 了,响应头里 x-ratelimit-remaining=0。

  GitHub 给这个 token 的主限额是每小时 5000 个请求,全量 predict 有 8462 个资产,一个小时下不完。以前串行大约一秒一个,拖了两个小时,每个滚动窗口碰巧都没超过 5000。加并发后,很快用完了额度。

  最后还是得避免重复请求 GitHub。归档资产不可变,同一个 sha256 永远对应同样的字节,所以每个资产只从 GitHub 下载一次:

  • 本地缓存按内容寻址,路径是 <cache>/<sha256>。
  • 命中时仍然复验 sha256,文件坏了就当未命中重新下载,能自愈。未命中时下载、校验,再原子 rename 进缓存。
  • 交付先把文件分成命中和未命中两组,全部命中就是零 GitHub 请求、纯本地拷贝,秒级完成。
  • 碰到主限额(remaining 为 0)时 withRetry 立即抛,不在那里空等;secondary 限流照旧退避重试。
  • 开打之前先估算要发多少请求,超过剩余额度就直接失败,提示先预热。
  • 预热脚本 warm-asset-cache.mjs 在后台按每小时 5000 的节奏往缓存里灌,断点续传,幂等,撞到限额就等重置再继续。

  前面那个重聚合脚本能「读缓存零下载」,也是因为缓存已经先预热好了。

预签名链接太长

  R2 的预签名 URL 大约 600 个字符。我要把链接转发给客户,走短信、二维码或者表单,太长了根本发不出去。

  我用一个免费的 Cloudflare Worker 做了短链,代码在仓库的 shortlink/ 下面:POST /put 带 Bearer token 存进 KV、返回短码,GET /d/<code> 302 跳转过去。链接从大约 600 个字符缩到大约 40 个。几个设计点:

  • 短码是一个完整 uuid 的 128 bit,不可枚举。
  • 跳转目标必须以配置好的交付 URL 前缀开头(R2 的 origin、桶和 deliveries/ 前缀),没配置就一律拒绝。不这样做的话,一旦 token 泄露,这个 Worker 就是一个开放重定向。POST 存的时候和 GET 跳的时候各校验一次。
  • 过期时间最多 7 天。
  • 调用方是 fail-open 的:没配 Worker、或者 Worker 出任何错,shortenUrl 返回 null,bot 照样发原始链接,绝不因为短链挡住交付。

  一开始 bot 同时发短链和原始链接,因为 Workers KV 是最终一致的,最长可能 60 秒才能读到,我不想让付费客户点开是个 404。第二天改成默认只发短链,命令里带 raw / 直链 / 长链 才附上原始长链。

tmpfs 装不下全量包

  还有一个跟磁盘有关的。打包的临时目录默认在 /tmp,而采集机的 /tmp 是一个大约 7.8GiB 的 tmpfs,也就是内存盘。全量 Polymarket 包大约 15GB,根本放不下,磁盘预检 checkBundleFits 会直接拒绝。

  解决办法是给交付 bot 和订阅 API 的 pm2 配置加上 TMPDIR,指到 200G 的数据盘上。预检读的也是同一个 tmpdir(),所以两边自动一致。写在 .env 里不会生效,因为 config.mjs 用的是 dotenv.parse,只解析不回写 process.env,而 os.tmpdir() 读的是进程环境变量。

  1000 个资产的上限通过聚合文件解决,5000 次请求的上限则靠缓存减少重复下载。


outcometick 工程笔记(4/21)

上一篇:每日巡检的假红:三周里修掉的几处误判

下一篇:采集拆到两台机器,以及预打包那把复审了五轮的文件锁

This post is licensed under CC BY 4.0 by the author.

每日巡检的假红:三周里修掉的几处误判

采集拆到两台机器,以及预打包那把复审了五轮的文件锁