Posts 从全量重算到增量 PnL:按天事实、晚到的映射和被限流的 CPU
Post
Cancel

从全量重算到增量 PnL:按天事实、晚到的映射和被限流的 CPU

  到九月底,每轮榜单都用 compute-pnl 重读 [coverageStart, b1] 的流水,30 天一轮约 1.75 小时。几个大步骤占 11–14 GiB,ClickHouse 上限是 23.28 GiB。09-28 先将账本保留范围设为 60 天,09-29 实现增量引擎并开始影子运行,10-02 切换到生产,10-05 又因为主机 CPU 限流调整调度。

公式为什么能拆

  PnL 的核心公式是 pnl = cashFlow(B0,B1] + closing(B1) − opening(B0)。把窗口切成一天一段:

  • cashFlow 是事件之和。每个事件的现金只依赖事件本身、同一笔交易里的其他事件(NegRisk 归因、mint/burn 配对都要求同 tx,一笔 tx 不会跨天)、事件时点之前的数据,以及注册表。所以窗口的现金流等于各天现金流之和,每天单独算,算完不再变,除非这些输入被改写。
  • 所有排除标记取 max,计数取和,同样可以按天拼。
  • 端点余额是创世以来转账的整数和,可以累加维护,bal(B1) = checkpoint + Σ delta,bal(B0) = bal(B1) − Σ_{窗口内} delta。估值还要保存跨天的成交序列结转。

  按天计算现金流和标记,拼窗口时再算两个端点。还要处理跨天的最后成交价、2000 块标价窗口,以及非整天的首尾段,不能只把日期范围换成一天。pnl.ts 的共享 SQL 抽取前后,19 个生成函数的输出逐字节一致;引擎等价性另外用跨天规格测试和生产 parity 核对。

  10-01 将核算创世块 G 固定为 91,973,715(08-14),余额、partialHistory 和估值历史都从 G 起算。设计文档估计,约 10-13 起全量管线的 coverageStart 会随 60 天上限前移;此前可用相同范围对比,此后需区分核算起点差异。

影子期和撞上的前缀

  影子表放在独立库 pmwallets_inc。最早 query_id 用 pmw-stats-137-inc- 前缀,与正式统计查询混在一起。09-29 到 09-30,map-loop 因为持续看到这个前缀,20 小时没有映射,未映射持仓从 $761 涨到约 $60.6 万。正式 run 清理影子查询后,影子又启动下一条语句,还触发过「残留杀不掉」的拒绝开跑。

  前缀改成 pmw-inc-<chain>-,和 pmw-stats-% 没有交集,优先级改由影子自己保证:每步开始前等正式管线空闲满 300 秒,运行中每 5 秒检查一次,出现正式语句就取消自己的。影子单条语句在服务端限额 10 GiB / 600 秒,另配一个每 10 秒跑一次的 cron,影子进程不在时取消所有 pmw-inc 语句。

映射搬进 names 进程

  旧 map-loop 每轮扫描全窗口发现条件,实测 108 秒、11.27 GiB,只在没有统计重查询时运行。09-30 把它改成跟随新块增量发现。

  新的 ConditionMapper(src/ledger/condition-mapper.ts)并进了实时补市场名的 pmwallets-names 进程,用 Postgres 游标 condition-map:137 只读新块:7 张带 conditionId 的表,加上新块成交和转账里的 token 经注册表取 condition,和闸门同源,测试钉住。实测一轮 0.26–1.3 秒、内存小于 50MB。代价是游标看不到写在它之前的行,账本回填或新增抵押品之后,要手跑一次 deploy/map-all.sh(约 11 GiB,持映射锁)。

晚到的映射

  映射变成持续增量之后,立刻碰到下一个问题。每天的事实登记时带指纹,原来的规则是指纹任一项变了,就撤回该天及之后所有天、按顺序重建。某天一个 condition 晚映射,触发了从 09-07 起 25 天的重放,约 3.5 小时。

  后面的天从前面的天拿的只有三样:checkpoint(只看转账)、最后成交价的结转(只看成交)、按天的余额 delta(只看转账),都和映射无关。所以指纹分成账本和映射两类:只有 map= 变了,就只重建那一天(inc_days.valid = 2),checkpoint 从旧 build 原样复制;账本变了照旧撤回之后所有天。拼窗口开头先单独重建映射变了的天,前后快照只比账本部分,因为影子拿不到映射锁,而映射每分钟都在写。

  parity 对晚映射单独归因:新行必须存在,成交 5 列和 splitOut / mergeIn / redeemIn 三列必须等于旧 condition 行加上搬来的 unmapped 行,通过后才豁免估值相关列。文档也注明,这种豁免不能证明所有估值差异都由映射造成;同映射输入下的估值等价性由集成测试覆盖。

对比和切换

  对比跑了 3 轮 7d、2 轮 30d,每轮 4,400–4,600 万行,行集合一致。按 parity 的容差与归因规则,wallet_stats、entity_stats 没有未解释差异;这不等于浮点列逐位相同。已解释项包括 NegRisk 转换的 2–5 µUSDC 取整差异、强弱标价边界的浮点翻转,以及晚映射。

  增量引擎的首轮 30d run:421,811 个钱包,75.5 分钟(全量 118 分钟),管线原因排除 0.76%,各步峰值不超过 7.96 GiB。切换时发现旧 worker 收到 SIGINT 会把当前这一轮跑完,只能 kill -9,新进程启动时先 killLeftoverQueries,杀掉了 6.64 GiB 的残留。最后清掉影子结果表 27.6G、全量的 stats_* 19.9G。回滚方式是把 STATS_ENGINE 改回 full 重启 stats;账本自创世有洞,或者结构上不可服务(冻结日没有有效 build、读到已删除的账本)时,该轮自动回退全量并告警。

preflight 成了最重的一步

  切换之后,最重的一步变成了发布前的 preflight,18.3 GiB。原来它的第一条语句是 9 个分支的 UNION,ClickHouse 会同时执行各分支,峰值约等于各分支之和:held 4.4、traded 3.1、7 张表 4.0、mapped 2.3 GiB。每个 *_canonical 视图都要 INNER JOIN 一次 ingest_blocks FINAL,所以开销在引用次数上,跟聚合键关系不大。

  中间试过「每 token 签名 Σhash(wallet)·delta」的方案,但转账表要读 8 次,撞上 12 GiB,撤回了。哈希签名也不能作为无碰撞的精确证明。最后逐分支写临时表再计数,15 条语句最高 7.86 GiB。traded 和「held 但不在 traded 里」有排除关系,拆开后可能因为中途新增成交而两边都漏,最终合成同一条 PF_TOKENS_SQL。10-03 首轮 7d 发布了 422,164 个钱包。

CPU 被限流

  10-03(UTC),推送 p50 从约 0.3 秒涨到 1.5 秒,7d 发布从约 67 分钟拖到 3 小时以上。vmstat 的 steal 是 54–74%,平时为 1–5%。当时主机控制面板显示 CPU 限流,手动解除后恢复。

  当天加了过载保护:开跑前检查推送延迟和 CPU steal,过载时暂缓发布、跳过曲线补算,每轮之后冷却。复审补上失败计数、探针超时和连接池 error 处理;共享冷却下,被挡住的窗口下一轮优先,避免固定顺序让另一个窗口长期排不上。

  10-04 第二次限流,过载探针没挡住。30d 跑完一轮后空闲时 steal 已经是 16–21%,7d 一开跑就到 70%。限流状态下 steal 随需求上涨,空闲时测不出来。停 stats 时要先 KILL pmw-stats-* 语句,pm2 stop 会等 kill_timeout 10 分钟。

  10-05 改成每个窗口每天跑一次,放在推送低谷:STATS_PERIODS=30d@1,7d@4,从对应的 UTC 整点起跑,失败或暂缓继续重试。stats 每天重载时间从约 8 小时降到 2.5–3 小时,页面和 llms.txt 改成「每天重算一次」。最初按 completedAt 判断当天是否完成;10-07 才改为检查 slot 对应目标日是否已发布,避免跨日重试占掉当天。


pmwallets 工程笔记(6/7)

上一篇:pmwallets 的成交推送:延迟锚点、LISTEN/NOTIFY 和一条钉住 xmin 的等锁语句

下一篇:改口径之后重建历史:标记价加权和 Polymarket Protocol V2 接入

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

LPSignal 的机会信号从 finalized 归档挪到链头

归档从 T+2 提前到次日:出货编排与导出水位