bitcoin-cli stop 之后到进程退出之前:比特币核心的关停顺序 图 1
bitcoin-cli stop 之后到进程退出之前:比特币核心的关停顺序 · 图 1

bitcoin-cli stop 发出后,终端几秒内就返回,进程却常常还要转十几秒才真正退出。这段时间里比特币核心在按一份相当讲究的清单做告别:先把所有对外入口焊死,再拆网络,然后按依赖顺序把内存里的状态落盘。这份清单完整写在一个 Shutdown 函数里,按 v31.0 源码逐段读,能澄清很多关于”要不要等它""强杀会怎样”的疑问。

第一阶段:关门

顺序从外往里。先停 RPC 的 HTTP 接口、REST 接口、RPC 服务本体和 HTTP 服务器——这就是为什么 stop 之后再敲 bitcoin-cli 会连不上:门已经焊死,但内部还活着。随后断开所有链上客户端(比如通过 IPC 挂在节点上的比特币-Qt 壳),再停端口映射(UPnP/NAT-PMP 的撤销在这里),然后注销对等管理器的验证回调并停掉连接管理器:断所有对端、关监听套接字、停 Tor 控制通道。这个阶段结束后,节点在外界视角已经不存在了,剩下的全是善后。

bitcoin-cli stop 之后到进程退出之前:比特币核心的关停顺序 图 2
bitcoin-cli stop 之后到进程退出之前:比特币核心的关停顺序 · 图 2

第二阶段:等与停线程

接下来有一个容易被忽略的细节:等待后台初始化线程。如果节点还在加载区块索引或做启动期的重活,关停会等它收工再继续——这解释了刚启动就立刻停机时 Shutdown 反而更慢的现象。然后停调度器,此后任何模块都不许再排定时的活;对等管理、连接管理、封禁管理、地址管理等对象依次销毁,确保没有线程还在碰这些数据结构。

第三阶段:落盘三连

写盘按依赖排序,顺序本身就是在保数据:

  1. 内存池快照——若开着 persistmempool 且启动时读过它,就把当前内存池写回数据目录,下次重启钱包立刻看得到待确认交易;
  2. 费用估计器落盘,它维护着历史费率统计;
  3. 链状态强制刷盘。注意这一步出现两次:第一次生成链状态刷新回调,钱包正是靠这个回调把自身同步到链头,避免下次启动重扫区块;处理完回调后再刷一次并重置视图,确保没有回调遗漏的数据。索引组件(交易索引、区块过滤器索引等)在回调冲刷之后才停。

第四阶段:告别

最后断开所有 IPC 客户端、拆 ZMQ、销毁各大子系统对象、删除 pid 文件,日志写下 Shutdown done,进程返回零。整条流水线是幂等设计的:初始化半途失败的场景也走同一个函数,缺哪个模块就跳过哪步。

一个常见误解

有人以为 stop 之后日志里只有一行 Shutdown in progress,其实这行之后才是主体工作。反过来,如果停机卡在某一步不动,日志停在的位置就是线索:卡在连接管理器说明有对端线程没退出,卡在链状态刷盘通常是磁盘写入压力,卡在后台初始化线程等待则是启动任务还没跑完。生产环境建议给服务管理器配一个远大于常态停机耗时的超时(比如数分钟),让优雅关停走完后自然退出,超时兜底才升级到强杀;同时把”发生过非优雅停机”这件事记进运维台账,因为它会体现在下一次启动的日志里。

强杀的区别

对比之下,直接 kill -9 或拔电跳过的是全部第三阶段:链状态和钱包文件靠各自的日志与提交机制保证可恢复,下次启动会触发回放修复、钱包重扫,启动时间明显变长;persistmempool 快照缺失,未确认交易列表归零重建。数据一致性由持久化层兜底,但优雅停机省的是恢复成本。生产节点建议走 stop,并在服务管理器里给足超时再谈强杀。本文顺序对照 v31.0 源码 init.cpp,早期版本步骤有增删,核对旧版本请看对应源码。