链数据列存:ClickHouse 建模、Reorg 与 Lakehouse 分层¶
30 秒版(开场)¶
链数据平台不能按
block_number覆盖,因为 reorg 后同一高度可出现不同 hash。我的分层是:对象存储/lakehouse 保留可重放 raw evidence,canonical control plane 按 parent lineage 提交主链映射,ClickHouse 承担高吞吐分析和热查询。MergeTree 的ORDER BY决定数据排序与稀疏主索引,主键不保证唯一;ReplacingMergeTree只在后台 merge 时做最终去重,查询正确性不能赌 merge 已发生。容量要用真实压缩率、parts/merge headroom、复制倍数、扫描字节和回填峰值测量,不能只报“每天多少行”。
3 分钟版(精讲深度)¶
- 事实分层:raw observation、canonical assignment、finalized watermark、derived serving 是不同事实,不能揉成一张“最新状态表”。
- 身份键:日志至少保留
(chain_id, block_hash, tx_hash, log_index);block 以(chain_id, block_hash)标识,高度只是排序/定位维度。 - ClickHouse 物理设计:
PARTITION BY主要用于生命周期和 part 管理,ORDER BY才决定 part 内排序和稀疏索引;过细 partition 会制造大量 parts。 - 去重边界:ClickHouse primary key 不唯一;ReplacingMergeTree 是 eventual cleanup。
关键查询用显式版本聚合、经过验证的
FINAL或独立 compacted serving table。 - 冷/热分层:lakehouse 保存长期 raw、schema/snapshot 与可重放文件;ClickHouse 保存热点 规范化列和聚合;PostgreSQL/共识 KV 可保存小而关键的扫块水位、lease 和 canonical commit。
10 分钟版(端到端数据模型)¶
flowchart LR
N["RPC / node / archive"] --> R["raw immutable evidence<br/>object storage + Iceberg"]
R --> D["versioned decoder"]
N --> D
D --> V["lineage validator"]
V --> C["canonical control plane<br/>head / finalized / epoch"]
D --> H["ClickHouse hot facts"]
C --> H
H --> S["serving views / API / BI"]
R --> B["backfill / replay"]
B --> D
核心不变量:
- raw 证据不因 reorg 删除,任何派生结果都能追溯到 decoder version 和 source provenance。
- canonical head 只能沿已验证 parent lineage 原子推进;finalized 以下冲突 fail closed。
- backfill 与 realtime 在重叠 hash 区间交接,不用
MAX(block_number)猜已经接上。 - 聚合结果要能按 canonical version 撤销/重算;finalized 只减少重算频率,不替代 raw。
一张 raw event 表的起点¶
CREATE TABLE chain_event_raw
(
chain_id LowCardinality(String),
block_number UInt64,
block_hash String,
parent_hash String,
tx_hash String,
event_index UInt32,
block_time DateTime64(3, 'UTC'),
observed_at DateTime64(3, 'UTC'),
decoder_id LowCardinality(String),
payload String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(block_time)
ORDER BY
(
chain_id,
block_number,
block_hash,
tx_hash,
event_index
);
这只是候选起点,不是通用最优答案:
- 高频查询若以 address/contract 为第一过滤维度,可能需要独立投影或 serving table;不要把
address无脑塞到最前面,否则按高度扫描会变差。 block_time是链数据,不等于 ingest time。若存在异常时间戳、超长 backfill 或按到达时间 管理生命周期,要另外保留并评估observed_at分区策略。- 多链吞吐、查询和保留策略差异很大时,可以按链族拆表;统一 schema 不应抹平 UTXO、 account/object、trace 与 event 的不同基数。
- String 便于示意;生产可在链内固定长度明确时选固定二进制表示,并统一编码,避免 hex 大小写 或前缀造成身份不一致。
Partition 不是二级索引¶
ClickHouse 官方把 partition 首先定位为数据管理能力:TTL、整分区移动/删除和独立维护。
查询真正高频的前缀应反映在 ORDER BY。过细 partition 会产生更多独立 parts,而 parts 只在
同一 partition 内 merge。
讲解时应先问:
- 主要查询是按链+高度、地址+时间、交易 hash,还是全链聚合?
- 数据保留和回填以月、天还是高度区间管理?
- 单批写入多少行,峰值每秒产生多少 parts?
- reorg 更正是否落在相同 partition?
- 哪些查询必须精确,哪些允许近实时 eventual view?
ReplacingMergeTree 不能当唯一约束¶
可用版本行表达 canonical assignment:
-- 示意:同一个 chain_id + block_number 的 assignment 不断追加版本
ENGINE = ReplacingMergeTree(version)
ORDER BY (chain_id, block_number)
但必须知道:
- 去重发生在后台 merge,时间未知;未 merge 的重复行仍会被普通查询读到。
ORDER BY值决定“同一逻辑键”,不是单独声明的PRIMARY KEY。OPTIMIZE ... FINAL读写量很大,不能当每次写后的同步提交协议。- 查询时
FINAL可求正确版本,但要对数据量、分区裁剪和并发做真实 benchmark。 - 更常见的精确查询是
argMax(value, version)、维护 compacted table,或由 canonical control plane 生成稳定批次。version必须单调且同键不歧义;若可能并列,应把 source sequence 等确定性 tie-breaker 纳入版本元组。选型必须用查询计划和 SLI 证明。
如果同一逻辑键可能被写进不同 partition,后台 merge 永远不会跨 partition 帮你去重。因此 partition 表达式也属于正确性审查的一部分。
Reorg 提交方式¶
observe candidate block/hash
-> persist raw block/tx/event
-> validate parent and decoder coverage
-> find common ancestor on divergence
-> append canonical=false for removed lineage
-> append canonical=true for adopted lineage
-> atomically publish new canonical version/head
-> recompute affected aggregates
-> advance finalized watermark only from chain-specific evidence
链上原始事实与业务事实分开:同一链上 observation 在 reorg 后可以失去 canonical 身份,但 业务充值 intent 不应因此被复制成一笔新订单。账本侧用冲正/待确认状态机,不直接依赖 ClickHouse 后台 merge 的时机。
Materialized View 与回填¶
ClickHouse materialized view 通常在新 block 插入时处理该批数据;对历史 source part 的 mutation、 删除或已有数据,并不会神奇地重放全部派生逻辑。大规模 decoder 升级应:
- 固定 raw snapshot 与 decoder version;
- 写新目标表或新版本列;
- 分区/高度 checkpoint 回填;
- 做 count、hash、金额守恒、抽样和 canonical 差异校验;
- shadow query 后原子切换 serving alias/view;
- 保留旧版本到回滚窗口结束。
Lakehouse 为什么不是“更便宜的 ClickHouse”¶
Iceberg 以 metadata、manifest 和 snapshot 管理对象存储中的数据文件,支持 schema/partition evolution 和 time travel。它适合长期 raw、跨引擎回放和批量重算,但:
- table snapshot 是数据文件集合的一致视图,不是区块链 finality;
- 每次写会形成新 snapshot/metadata,必须 expire snapshot、compact 小文件、清理 orphan file;
- schema field ID 和 partition evolution 降低演进风险,仍要给 decoder 语义单独版本化;
- 不同计算引擎对格式版本和特性的支持可能不同,升级前要做兼容矩阵。
一种常见分层:
| 层 | 适合保存 | 主要 SLO |
|---|---|---|
| Object storage + Iceberg | raw block/trace/event、decoder 输出快照 | 可重放、长期保留、成本 |
| ClickHouse | 热明细、地址/协议查询、聚合 | ingest lag、查询 P95/P99、扫描字节 |
| OLTP/control plane | 扫块水位、lease、canonical head、job 状态 | 事务正确性、低延迟写入 |
容量与成本验证¶
不要用固定“ClickHouse 能压缩十倍”作答案。先测:
daily_raw_bytes = daily_rows × sampled_raw_bytes_per_row
daily_compressed = daily_rows × measured_compressed_bytes_per_row
steady_storage = daily_compressed × retention_days × replica_factor
required_headroom = merges + mutations/backfill + replica repair + temporary parts
query_cost = read_rows + read_bytes + CPU + peak_memory + concurrency
recovery_time = bytes_to_restore / measured_effective_throughput + replay/validation
至少用代表性的链、trace 深度、payload 分布、排序键和 codec 做样本。分别压测 realtime 稳态、 历史 backfill、深 reorg 重算、节点补副本和大查询并发;任何 QPS/压缩率都必须附工作负载和版本。
生产指标¶
| 域 | 指标 |
|---|---|
| ingest | source/canonical height lag、rows/s、batch size、reject/retry、decoder coverage |
| parts/merge | active parts、small parts、merge queue、merge bytes/s、mutation backlog |
| correctness | duplicate logical keys、canonical diff、parent gap、finalized conflict、replay checksum |
| query | read_rows、read_bytes、latency、peak memory、spill、failed query |
| replication | replica queue、absolute/relative delay、lost part、read-only replica |
| lakehouse | snapshot/manifest 数、小文件、orphan、compaction lag、restore rehearsal |
| cost | hot/cold bytes、replica factor、扫描 TB、egress、backfill compute |
深挖问答¶
- ClickHouse primary key 会拒绝重复吗?
不会。它是稀疏索引/排序语义,不是 OLTP unique constraint。 - ReplacingMergeTree 是否保证读到唯一最新行?
后台 merge 只有 eventual cleanup;精确读需FINAL、版本聚合或 compacted serving 方案。 - 为什么不能按高度 UPSERT?
reorg 后同高度可有多个 block hash;覆盖会丢失证据,也无法安全找共同祖先。 - Partition 越细查询越快吗?
不一定。Partition 主要服务数据管理,过细会制造 parts 和 merge 开销;查询前缀主要看排序键。 - Iceberg snapshot 是否等于 finalized snapshot?
不等于。前者是表文件元数据视图,后者是链共识/业务风险状态。 - 如何证明列存方案比 PostgreSQL 好?
用同一工作负载比较 ingest、扫描字节、P95/P99、回填时间、恢复时间和总成本;不凭产品标签。
反模式与错误表达¶
- “ClickHouse 主键天然唯一。”
- “ReplacingMergeTree 写入后立刻只剩一行。”
- “每次查询加
FINAL就没有性能问题。” - “按
block_number覆盖就能处理 reorg。” - “Partition 按地址/交易 hash 越细越快。”
- “Materialized View 会自动重算历史 mutation。”
- “Iceberg time travel 就是链上 finality。”
- “列存压缩率固定,所以可以直接估成本。”