首页
/ Bitcoin Core 29.4 维护版深度解析:chainstate 数据库定期压缩与一批稳定性修复

Bitcoin Core 29.4 维护版深度解析:chainstate 数据库定期压缩与一批稳定性修复

2026-09-06 19:24:24作者:何举烈Damon

本文基于本仓库 doc/release-notes/release-notes-29.4.md 发布说明,结合 src/ 下实现源码,逐项拆解 Bitcoin Core 29.4 这一维护版本的核心变更:链状态(chainstate)数据库的“反复重写大量数据”缺陷如何被修复、LevelDB 的 seek compaction 为何被禁用、以及钱包、构建、CI 与文档层面的多项修正。读完你既能掌握 29.4 的升级路径与兼容性边界,也能从源码层面理解 UTXO 集落盘与压缩的真实工作机制。

版本总览:这是一个“修内功”的维护版

Bitcoin Core 29.4 属于 29.x 系列的功能性维护版本,官方发布说明(即仓库中的 doc/release-notes/release-notes-29.4.md)明确指出:本次发布包含各种缺陷修复与性能改进,以及更新的翻译。本仓库根目录的 README.md 表明其定位为 Bitcoin Core 的 integration/staging tree,29.4 的改动正对应同步进主线的这些合并请求。

从补丁分布看,29.4 的变更集中在:

  • Validation(验证):2 个补丁,其中就包含本次最大的看点——chainstate 定期压缩;
  • Leveldb:禁用 seek compaction(1 个上游仓库补丁);
  • Net / Wallet / Build / Test / Doc / CI / Misc:各 1~4 个小补丁。

整个版本没有引入新的共识规则或 RPC 行为变化,属于典型的“稳定性 + 工程质量”发布。以下按“能直接影响你节点磁盘 I/O 的核心变更 → 其余分项修复”顺序展开。

如何升级到 29.4

发布说明给出了标准的三平台升级流程,保持与既有维护版一致:

  1. 先彻底关停旧版本节点,并等待进程完全退出。由于需要把内存中的脏缓存刷盘、可能进行数据目录迁移,某些情况下关闭过程可能持续几分钟,切勿在中途强行结束进程。
  2. Windows:直接运行安装程序(installer)覆盖安装。
  3. macOS:将新的 /Applications/Bitcoin-Qt 覆盖到旧程序。
  4. Linux:覆盖 bitcoind / bitcoin-qt 可执行文件即可。

两点补充说明(与发布说明一致):

  • 支持从已到达 EOL(生命周期结束)的旧版本直接升级,但若数据目录需要迁移,首次启动可能耗时较长;
  • 旧钱包版本在一般情况下仍然受支持,即老钱包数据文件无需特殊转换即可被 29.4 加载。

兼容性:受支持与测试的操作系统

发布说明声明的支持边界为:

系统 版本要求
Linux Kernel 3.17+
macOS macOS 13+
Windows Windows 10+

绝大多数其他类 Unix 系统理论上可运行,但官方测试频率较低;官方明确不建议在不支持的系统上运行 Bitcoin Core。对于运行节点的生产环境,建议优先匹配上表。构建细节可参考本仓库 INSTALL.md 及各平台独立指南(如 doc/build-freebsd.md)。

核心看点:修复 chainstate 数据库“反复重写自身大块数据”

发布说明在 Notable changes 中给出了一句关键描述:

本版本修复了一个问题:chainstate 数据库在正常运行时反复重写自身的大块数据,导致过量的磁盘读写。

这句话直接点出了 29.4 最重要的性能修复。它由三个 PR 协同完成,下面从源码层面还原其来龙去脉。

为什么会出现“反复重写”?——LevelDB 的 seek compaction 机制

chainstate(UTXO 集)与区块、交易索引一样,底层由嵌入的 LevelDB 存储。LevelDB 的 compaction(压缩)分为两类:

  1. size compaction(大小压缩):当某层文件总大小超过阈值时触发,把低层小文件合并成大文件,属于写入驱动的常规维护;
  2. seek compaction(寻道压缩):当某层文件因随机读(seek)次数累积到一定阈值时,主动重写该文件,把热点数据提升到更易读的位置。

本仓库内嵌 LevelDB 的 src/leveldb/db/version_set.cc 保留了相关注释与逻辑:

// conservative and allow approximately one seek for every 16KB
// of data before triggering a compaction.
//
// Note: seek compactions are disabled. See Version::UpdateStats.
f->allowed_seeks = static_cast<int>((f->file_size / 16384U));
if (f->allowed_seeks < 100) f->allowed_seeks = 100;

注释中 "seek compactions are disabled. See Version::UpdateStats" 即对应发布说明中的 #61 (bitcoin-core/leveldb): Disable seek compaction。其风险在于:UTXO 集是典型的“大量随机读取 + 少量写入”负载,如果高频的随机查询反复命中同一批 SSTable 文件,seek compaction 会不断把这些文件重写到新的层级布局,造成发布说明所描述的“normal operation 期间过量磁盘读写”。

与之配套的两个回归测试也写明了行为契约:

  • src/leveldb/db/db_test.cc 中的 GetDoesNotTriggerSeekCompaction:seek compaction 在此 fork 中禁用,因此重复读取不得改变层级布局,而手动压缩仍须正常工作;
  • src/leveldb/db/autocompact_test.cc:连续读取 100 次后,被读区间的文件大小不得缩小——即读操作永远不会调度压缩。

也就是说:29.4 起,LevelDB 层的“读导致写”这一自我放大行为被彻底掐断。

配套的正向维护:chainstate 按概率定期压缩

仅仅关掉 seek compaction 还不够——UTXO 集中长期失效的数据仍需要一种受控的回收机制,这正是 PR #35465 coins: compact chainstate regularly 的职责:把“压缩时机”从 LevelDB 内部的隐式触发,改为 Bitcoin Core 验证层显式、有节制地调度

src/validation.cpp 中可以看到决定是否压缩的判定函数:

// Return whether the completed full flush should compact chainstate
static bool ShouldCompactChainstate(bool in_ibd)
{
    static constexpr uint32_t flush_ratio{320}; // Roughly every 2 weeks with hourly flushes
    return !in_ibd && FastRandomContext().randrange(flush_ratio) == 0;
}

关键实现事实:

  • 压缩只在一次完整的全量落盘(full flush)结束后才可能发生,避免与常规写入争抢;
  • 采用 1/320 的随机概率randrange(flush_ratio) == 0),若按约每小时一次的 flush 频率估算,平均大约每两周触发一次整库压缩;
  • 同步阶段(Initial Block Download,IBD)中不触发——刚同步完的 chainstate 本身布局较新,无需压缩,也避免拖慢同步。

调度点在 FlushStateToDisk 的全量 flush 分支中(src/validation.cpp):

if (full_flush_completed) {
    ...
    if (!m_chainman.m_interrupt && ShouldCompactChainstate(m_chainman.IsInitialBlockDownload())) {
        try {
            CoinsDB().CompactFullAsync();
        } catch (const std::exception& e) {
            LogWarning("Failed to start chainstate compaction (%s)", e.what());
        }
    }
}

后台异步压缩的实现:CompactFullAsync

真正执行压缩的是 CCoinsViewDB::CompactFullAsync()src/txdb.cpp)。它被要求在持有 cs_main 时调用,内部通过 std::async一次性后台线程中执行,避免阻塞主验证线程:

m_compaction = std::async(std::launch::async, [this] {
    try {
        util::ThreadRename("utxocompact");
        LOCK(m_db_mutex);
        LogDebug(BCLog::COINDB, "Starting chainstate compaction of %s", ...);
        m_db->CompactFull();
        ...
    } catch (const std::exception& e) {
        LogWarning("Failed chainstate compaction (%s)", e.what());
    }
}).share();

几个值得注意的工程细节:

  • 线程被命名为 utxocompact,方便运维通过线程名定位;
  • 内部加 m_db_mutexsrc/txdb.h 声明的缓存重分配互斥,防止压缩期间 ResizeCache() 替换底层 CDBWrapper 造成竞态;
  • 返回 std::shared_future<void>,多个调用方可共享等待。若上一次压缩尚未结束,直接复用同一个 future(m_compaction),不会叠加多个压缩任务;
  • 析构函数中若检测到后台压缩未完成,会打印 Waiting for background chainstate compaction of ... 并阻塞等待其结束,保证正常退出时数据一致(src/txdb.cpp)。

最底层调用 CompactFull()src/dbwrapper.cpp)等价于对整个键空间的 CompactRange(nullptr, nullptr)

void CDBWrapper::CompactFull() { DBContext().pdb->CompactRange(nullptr, nullptr); }

用测试“看见”压缩效果

仓库的单元测试 src/test/coins_tests.cppcoins_db_leveldb_layout)直观地验证了压缩的行为:它通过 LevelDB 属性 leveldb.num-files-at-level2 统计第二层文件数,先断言初始为 0,然后执行

WITH_LOCK(::cs_main, return base.CompactFullAsync()).wait();
BOOST_CHECK_EQUAL(level2_files(base), 1);

——一次完整的后台压缩后,第二层恰好收敛为 1 个文件,并且其中的 coin 与 best block 数据完好可读。这从测试侧印证了“定期整库压缩能把散落数据合并为紧凑布局”的效果。

如果你希望对节点施加一次即时压缩,可以关注 src/dbwrapper.cpp 中由 DBParams.options.force_compact 控制的路径——打开数据库时即执行一次全量压缩(对应相关启动选项/配置),属于运维侧的可选手段。

小结:29.4 对磁盘 I/O 的实际影响

把以上三点拼起来,29.4 修复 chainstate 异常磁盘写放大的完整链路是:

  1. LevelDB 层面禁用 seek compaction#61),杜绝“读热点 → 重写文件”的自我放大循环,这是消除“反复重写大块数据”的关键;
  2. 验证层引入 1/320 概率、非 IBD 期间触发的显式定期全量压缩#35465),在 flush 完成后的后台线程中执行,保证 UTXO 存储长期保持紧凑;
  3. 整套行为被测试锁定:读不改变层级(src/leveldb/db/autocompact_test.cc)、压缩收敛层级布局(src/test/coins_tests.cpp)。

从源码结构可以推断,受益面最大的是长期运行、持续处理随机 UTXO 查询的主网全节点:其运行期磁盘写入应明显下降,而存储布局仍会由周期压缩保持健康。

Validation:#35209 修正预计算交易数据的生命周期

第二个 Validation 补丁 #35209 validation: correct lifetime of precomputed tx data 属于内存安全/正确性修复,与本仓库 29.4 代码中 src/validation.cpp 的实现直接对应:

CBlockUndo blockundo;

// Precomputed transaction data pointers must not be invalidated
// until after `control` has run the script checks (potentially
// in multiple threads). Preallocate the vector size so a new allocation
// doesn't invalidate pointers into the vector, and keep txsdata in scope
// for as long as `control`.
std::vector<PrecomputedTransactionData> txsdata(block.vtx.size());
std::optional<CCheckQueueControl<CScriptCheck>> control;
if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && fScriptChecks) control.emplace(queue);

理解这个补丁需要先了解 PrecomputedTransactionData:它是签名/脚本校验的“预计算数据”(BIP341 taproot 所需的 hashPrevouts/hashAmounts 等单次或双重 SHA256 中间量,定义见 src/script/interpreter.h),在校验同一笔交易的多个输入时可复用,避免重复计算哈希。

修复的实质是生命周期(lifetime)问题

  • ConnectBlock 会把各笔交易的预计算数据指针交给 CScriptCheck(定义于 src/validation.h,持有 PrecomputedTransactionData* txdata 指针),脚本检查可能在 CCheckQueueControl 管理下的多线程工作队列中并发执行;
  • 若保存这些对象的容器在脚本检查尚未结束时就因扩容(reallocation)而移动了元素,指向旧位置的指针就会悬空——这是典型的使用已失效指针(use-after-move/use-after-free 类)隐患;
  • 修复方式是:按区块交易数预先分配好容量std::vector<PrecomputedTransactionData> txsdata(block.vtx.size());),确保后续写入元素时 vector 不再触发重新分配,同时让 txsdata 的生命周期覆盖 control 运行脚本检查的整个过程。

从代码层面看,这一改动消除了并发脚本校验路径上对容器地址稳定性的依赖,属于保障长稳运行的正确性加固。相关的脚本缓存校验行为可进一步参考 src/test/txvalidationcache_tests.cpp,其中同样以栈上 PrecomputedTransactionData 贯穿多轮 CheckInputScripts 调用。

Net:#34093 修复默认网关查询的编译告警

#34093 netif: fix compilation warning in QueryDefaultGatewayImpl() 对应 src/common/netif.cpp 中的默认网关探测实现。该文件按平台分别实现:

  • Linux(含 IPv4/IPv6,src/common/netif.cpp 起):通过 AF_NETLINKNETLINK_ROUTE socket 发起 RTM_GETROUTE 查询,注意代码中对 Linux 与 FreeBSD 的 NLM_F_DUMPRTM_F_PREFIX 标志做了条件编译区分;
  • Windowssrc/common/netif.cpp 起):使用 GetBestRoute2 查询到默认网关的最佳路由;
  • macOSsrc/common/netif.cpp 起):通过 sysctl 读取 PF_ROUTE 路由表并遍历 rt_msghdr 消息寻找 RTAX_GATEWAY
  • 其他平台则落到返回 std::nullopt 的 dummy 实现(src/common/netif.cpp)。

该函数服务于出站连接的默认出口探测(用于确定是否使用代理等场景),本补丁仅消除特定平台编译时的告警,不影响行为语义。

Wallet:#35228 估算输入体积时使用真实 outpoint

#35228 wallet: use outpoint when estimating input size 改善了钱包在构造/预估交易时的输入体积(进而影响手续费)估算精度。其代码落点在 src/wallet/spend.cpp

int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoint,
                                    const SigningProvider* provider, bool can_grind_r,
                                    const CCoinControl* coin_control)
{
    if (!provider) return -1;
    if (const auto desc = InferDescriptor(txout.scriptPubKey, *provider)) {
        if (const auto weight = MaxInputWeight(*desc, CTxIn{outpoint}, coin_control, true, can_grind_r)) {
            return static_cast<int>(GetVirtualTransactionSize(*weight, 0, 0));
        }
    }
    return -1;
}

核心变化是:把具体花费的 COutPoint 传入 MaxInputWeight,使“最大输入体积”的推导能结合该输入真实的 outpoint 信息(例如基于描述符推断出的输入构造),而不是仅凭脚本模板做泛化估计。旧路径在估算时未能利用 outpoint 细节。

在预选输入的处理路径上(src/wallet/spend.cpp),外部输入也会携带 outpoint 走带完整参数的估算分支:

if (input_bytes == -1) {
    input_bytes = CalculateMaximumSignedInputSize(txout, outpoint,
                            &coin_control.m_external_provider, can_grind_r, &coin_control);
}

从代码结构可以推断,这一改动提升了 fee bump / 找零计算前“预估输入字节数”的准确性,从而让手续费估算更贴近最终签名后交易的实际情况。若某些输入因无法求解而估算失败(返回 -1),上层会报出 Not solvable pre-selected input ... 之类的错误(src/wallet/spend.cpp),属于既有行为。

Build:两项构建系统修正

#34228:gen_id 中取消继承 SOURCE_DATE_EPOCH

依赖构建脚本 depends/gen_id 中,在输出工具链 ID 前显式清除了环境变量:

# Unset SOURCE_DATE_EPOCH to prevent it from leaking into tool
# outputs and to maximize reuse of the built package cache.
unset SOURCE_DATE_EPOCH

SOURCE_DATE_EPOCH 是 GNU 可复现构建标准中用于固定时间戳的环境变量;若其值渗入某些工具(例如 gcc 的 __DATE__/__TIME__ 或压缩工具时间戳)的输出,会导致同一工具链在不同时间生成的内容 ID 漂移。此改动通过 unset 它,一方面避免 ID 被无关的时间因素污染,另一方面最大化 depends 预构建包缓存的复用命中率,进而加速构建与 CI。

#34848:CMake 迁移废弃的 SQLite3 target

29.x 系列已全面转向 CMake 构建体系(见根目录 CMakeLists.txtCMakePresets.json)。本补丁在 CMake 侧迁离了新版 CMake 中已废弃的 SQLite3 target 用法,改为使用新的引用方式,以适配更新的 CMake 版本并消除 deprecation 告警,避免未来 CMake 升级导致构建失败。该改动不影响 -DWITH_SQLITE 之类的既有构建开关语义。

Test:#34918 清理 fuzz 中未使用的全局指针

#34918 fuzz: [refactor] Remove unused g_setup pointers 是一次纯重构:删除 fuzz 目标中不再使用的 g_setup 全局指针变量。fuzz 构建与运行入口可参考 ci/lint/requirements.txttest/fuzz/coins_view.cpp(该文件里 CCoinsViewDB::CompactFullAsync 也出现在 fuzz 操作的候选动作中),此类清理旨在减少全局状态与未使用变量,提升 fuzz 代码的可维护性与覆盖率工具的准确性,不产生行为变化。

Doc:四处文档修正

本版本打包了四个文档层面的修正,均不影响运行行为,但值得部署与开发人员留意:

PR 内容 影响
#34510 修复 bpftrace 安装文档链接失效 涉及 contrib/tracing/README.md 一带的跟踪工具指引可读性
#34561 钱包 RPC 手册页中补全缺失的 fee_rate 参数示例 修正 send*/bumpfee 等 RPC 文档示例的可执行性
#34671 更新 Debian/Ubuntu 下的 Guix 安装指引 对应 contrib/guix/INSTALL.md,涉及 contrib/guix 目录下的构建流程
#35283 在 BSD 构建指南中提示需 -DWITH_ZMQ=ON 已体现于 doc/build-freebsd.md:要启用 ZeroMQ 通知,需安装 libzmq4 pkgconf 并在配置时传入 -DWITH_ZMQ=ON

其中 #35283 对 BSD 用户有实操价值:ZeroMQ 通知不是默认编译进构建的,必须显式 -DWITH_ZMQ=ON。ZMQ 消息协议的整体用法可参见 doc/zmq.mdcontrib/zmq/zmq_sub.py

CI:三处流水线调整

  • #35202 ci: restore sockets in i686, no IPC job:在 i686 且关闭 IPC 的 CI job 中恢复 socket 相关测试覆盖;
  • #35378 ci: switch runners from cirrus to warpbuild:把 CI runner 从 Cirrus 迁移到 WarpBuild(基础设施调整);
  • #35408 ci: 35378 followups:上一迁移的后续收尾补丁。

这些 CI 脚本改动分布在 ci/test/(如 ci/test/00_setup_env_i686_no_ipc.sh)下,属于工程维护范畴,与运行时行为无关。

Misc:#35175 兼容 Boost ≥ 1.91 的编译修复

#35175 multi_index: fix compilation failure with boost >= 1.91 修复了 Boost 1.91 及以上版本中 boost::multi_index 相关 API 变动引起的编译失败。Bitcoin Core 大量依赖 Boost 容器(如交易池、addrman 等模块中的 multi_index 结构),当发行版升级 Boost 后此补丁保证 29.4 仍可正常编译。

Credits:贡献者

29.4 由以下直接贡献者合入(依发布说明):andrewtoth、Cory Fields、Daniel Pfeifer、darosior、fanquake、Hennadii Stepanov、jayvaliya、junbyjun1238、Lőrinc、MarcoFalke、SomberNight、ToRyVand、willcl-ark,同时感谢在 Transifex 上协助翻译的所有社区成员。

升级建议与总结

综合来看,Bitcoin Core 29.4 是一个“低成本、高确定性收益”的维护版:

  1. 建议升级:修复了 chainstate 在正常运行期的异常磁盘写放大,对长期运行的主网/测试网节点尤其有意义;无任何共识或 RPC 破坏性变化,升级风险低;
  2. 升级方式:按上文三平台流程,先完全关停旧进程再替换二进制,首次启动如遇数据迁移请耐心等待;
  3. 验证要点:升级后可在日志中观察 utxocompact 线程相关的 Starting chainstate compaction ... / Finished chainstate compaction ...(对应 src/txdb.cpp),确认周期压缩按约两周一次的节奏正常工作。

如需从源码深入后续版本差异,可在本仓库 doc/release-notes/ 目录下对照 29.0、30.0、31.0 等相邻版本发布说明,观察该项目在链状态存储与验证性能上的持续演进脉络。

登录后查看全文
热门项目推荐
相关项目推荐