Doris 2.1.11 BE 多节点异常掉线:AutoReleaseClosure callback reuse 数据竞争与 std::bad_alloc 双

Doris 2.1.11 BE 多节点异常掉线:AutoReleaseClosure callback reuse 数据竞争与 std::bad_alloc 双故障归因

关键词:Doris BE 掉线、Doris BE 崩溃、SIGSEGV、std::bad_alloc、AutoReleaseClosure、callback reuse、PR #61782、ExchangeSendCallback、brpc、Doris 2.1.11、core dump 分析

目录

  • 一、问题现象
  • 二、环境信息
  • 三、排查过程:从"一个故障"到"两个故障"
  • [四、第一类故障:AutoReleaseClosure callback reuse](#四、第一类故障:AutoReleaseClosure callback reuse "#%E5%9B%9B%E7%AC%AC%E4%B8%80%E7%B1%BB%E6%95%85%E9%9A%9Cautoreleaseclosure-callback-reuse")
  • [五、第二类故障:std::bad_alloc → SIGABRT](#五、第二类故障:std::bad_alloc → SIGABRT "#%E4%BA%94%E7%AC%AC%E4%BA%8C%E7%B1%BB%E6%95%85%E9%9A%9Cstdbad_alloc--sigabrt")
  • [六、Host is down / CANCELLED 的正确解读](#六、Host is down / CANCELLED 的正确解读 "#%E5%85%ADhost-is-down--cancelled-%E7%9A%84%E6%AD%A3%E7%A1%AE%E8%A7%A3%E8%AF%BB")
  • 七、处置方案
  • 八、总结

一、问题现象

1.1 告警表现

某 Doris 2.1.11 生产集群(3 FE + 12 BE)连续两个晚间出现多台 BE 异常掉线。

阶段 告警内容
第一晚 BE 不存活节点总数 > 0,当前值 2(104、108)
第二晚 同上,当前值 1(101)

监控层面看,是同一个现象:多个 BE 在短时间窗口内发生进程级异常退出,被 FE 判定为 Dead。

1.2 初步判断(事后证明是错的)

第一反应是"三个 BE 掉线 = 同一个 bug"。

这个判断看起来很合理:同一集群、同一版本、同一时间窗口、同样的告警文案。

但它差点让我们在生产上换错补丁。


二、环境信息

组件 版本/配置
Doris 2.1.11
集群规模 3 FE + 12 BE
部署方式 BE 由 SelectDB Manager 启动(非 systemd 托管)
业务负载 夜间批量 INSERT INTO ... SELECT / Stream Load
数据格式 Duplicate / Unique 模型混合

三、排查过程:从"一个故障"到"两个故障"

3.1 第一步:拿到每台的信号类型

不要先看业务日志,先看 core 的信号。

BE 崩溃信号 核心特征
101 SIGSEGV Status::create → AutoReleaseClosure → ExchangeSendCallback → brpc::EndRPC
108 SIGSEGV 与 101 相同的调用链
104 SIGABRT std::bad_alloc → terminate → abort → DeltaWriterV2

信号不同,根因必然不同。

SIGSEGV 是"访问了非法内存",SIGABRT 是"程序主动中止"。这两个信号在 C++ 里对应完全不同的故障类型:

  • SIGSEGV → 空指针、野指针、数据竞争导致的对象被回收
  • SIGABRT → 未捕获异常(如 std::bad_alloc)、断言失败

3.2 第二步:确认 core 栈

101 / 108 的栈(关键片段):

ruby 复制代码
SIGSEGV
  ↓ doris::Status::create()
  ↓ doris::AutoReleaseClosure<
       PTransmitDataParams,
       doris::pipeline::ExchangeSendCallback<PTransmitDataResult>
     >::_process_status()
  ↓ doris::AutoReleaseClosure<...>::Run()
  ↓ brpc::Controller::EndRPC()
  ↓ brpc::policy::ProcessRpcResponse()
  ↓ bthread::TaskGroup::task_runner()

104 的栈:

c 复制代码
std::bad_alloc
  ↓ std::terminate()
  ↓ abort()
  ↓ SIGABRT
​
RuntimeState::cancel_reason()
DeltaWriterV2::write()
vectorized::VTabletWriterV2::write_memtable()
vectorized::VTabletWriterV2::write()
PlanFragmentExecutor

3.3 修正后的故障模型

c 复制代码
BE 多节点异常掉线
    │
    ├── 101 / 108
    │     └── SIGSEGV → AutoReleaseClosure callback reuse race
    │
    └── 104
          └── SIGABRT → std::bad_alloc → 内存分配失败

这张表是本次复盘最重要的修正。

如果把 104 也归到 AutoReleaseClosure,会与 core 证据直接冲突------104 的栈里根本没有 AutoReleaseClosure。


四、第一类故障:AutoReleaseClosure callback reuse

4.1 调用栈为什么重要

101/108 的栈有一整条完整的链路,指向一个具体类:

swift 复制代码
AutoReleaseClosure<PTransmitDataParams, ExchangeSendCallback<PTransmitDataResult>>

ExchangeSendCallback 说明这条路径是 BE 之间的 Exchange 数据传输 (PTransmitData 是 Doris 内部 BE-to-BE 的 RPC)。

换句话说:这不是普通查询,是分布式执行计划里的数据交换阶段。

4.2 溯源:这个问题上游已经修了

拿着这段栈去搜 Apache Doris 的 issue/PR,能找到:

makefile 复制代码
PR #61782
Title: [fix](rpc) Fix AutoReleaseClosure data race with callback reuse
Commit: 0a1d842
合并时间: 2026-04-16(master)

这个 PR 的崩溃栈与我们的 core 高度一致。

4.3 官方对机制的描述

PR 的核心说明是:

callback 的 call() 可能复用 callback 对象,例如 vdata_stream_sender.h 中的 get_send_callback();新的 RPC 会修改 response_ 和 cntl_。 如果 AutoReleaseClosure::Run() 在执行 call() 后才检查 cntl_->Failed() 或 response_->status(),它读取到的就可能已经是新 RPC 的状态,而不是原 RPC 的结果。

4.4 问题代码

2.1.11 的 be/src/util/ref_count_closure.h:

scss 复制代码
void Run() override {
    Defer defer {[&]() { delete this; }};
​
    if (auto tmp = callback_.lock()) {
        tmp->call();          // ← 可能触发 callback 复用,发起新的 RPC
    }
​
    if (cntl_->Failed()) {    // ← 此时 cntl_ 可能已被新 RPC 改写
        _process_if_rpc_failed();
    } else {
        _process_status<ResponseType>(response_.get());  // ← response_ 同理
    }
}

问题在于顺序:

scss 复制代码
callback_->call()
        ↓
这次调用可能发起新 RPC,复用同一个 callback
        ↓
新 RPC 改写 cntl_ / response_
        ↓
旧 Run() 继续读 cntl_ / response_
        ↓
读到的是别人的状态

4.5 官方不是"理论推断",是抓到了实证

PR 里有一段调试日志,是在生产/测试环境真实捕获的:

csharp 复制代码
RACE DETECTED:
AutoReleaseClosure response_ was reused by a new RPC
while still in Run().
The old closure is about to read response_->status()
but the new RPC may be concurrently writing to the same
response_ object.

这一点很关键:不是"可能有问题",是"确实捕获到了"。

4.6 修复方式

修复版本引入了新的 be/src/util/brpc_closure.h,把 Run() 改成只调用 callback:

scss 复制代码
void Run() override {
    Defer defer {[&]() { delete this; }};
    if (auto tmp = callback_.lock()) {
        tmp->call();
    }
}

同时新增 HandleErrorBrpcCallback,把"错误检查"的职责移进 callback 内部:

sql 复制代码
旧版本:
AutoReleaseClosure::Run()
    ├── callback->call()
    ├── RPC error check
    └── response status check
​
修复版本:
AutoReleaseClosure::Run()
    └── callback->call()
             ├── RPC error check
             ├── response status check
             └── Query cancel

4.7 单元测试证据

PR 新增了 be/test/exec/exchange/exchange_sink_test.cpp,专门测"callback 复用后状态是否被污染":

阶段 结果
修复前 7 个测试,4 通过、3 失败
修复后 7 个测试,全部通过

失败的三个用例名非常说明问题:

复制代码
test_closure_call_must_not_corrupt_status_check
test_closure_call_must_not_hide_error_status
test_closure_call_must_not_hide_rpc_failure

4.8 完整故障链

css 复制代码
业务 SQL
    ↓
Distributed Plan
    ↓
Pipeline Exchange
    ↓
BE ↔ BE 之间 PTransmitData BRPC
    ↓
AutoReleaseClosure
    ↓
RPC 完成 → AutoReleaseClosure::Run()
    ↓
callback->call()
    ↓
Exchange callback 被复用,发起下一次 RPC
    ↓
新 RPC 修改 cntl_ / response_
    ↓
旧 Run() 继续读取 → 数据竞争
    ↓
Status::create() 访问非法内存
    ↓
SIGSEGV → BE 进程退出

五、第二类故障:std::bad_alloc → SIGABRT

5.1 能确认的三件事

第一,进程因 SIGABRT 退出(不是 SIGSEGV)。

第二,直接触发是 std::bad_alloc:

c 复制代码
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc

含义:某个动态内存分配操作失败(new / malloc / vector 扩容 / Block 分配等)。

第三,崩溃发生在 Tablet Writer 路径:

arduino 复制代码
DeltaWriterV2::write()
VTabletWriterV2::write_memtable()
VTabletWriterV2::write()

说明崩溃时 BE 正在处理写入,不是读取。

5.2 不能直接下的结论

不能仅凭 std::bad_alloc 就说"机器内存耗尽"。

bad_alloc 至少有六种可能来源:

可能 说明
A. Linux OOM 系统级内存耗尽,malloc 失败
B. Doris Memory Tracker 限制 Doris 自身内存水位限制触发分配失败
C. allocator 碎片 内存没耗尽但无法分配连续大块
D. 单次超大对象申请 例如一次性申请过大的 Block
E. MemTable / Buffer 瞬时暴涨 写入峰值导致
F. 写入 + Compaction 叠加 多任务并发造成内存压力

本次没有拿到足够证据区分这六种。

所以 104 的结论只能写成:

已确认"内存分配失败导致进程 abort",但具体是系统 OOM、Doris 内存限制还是单次异常大分配,仍需进一步证据。

5.3 下一步需要采集什么

perl 复制代码
# 1) 内核 OOM
dmesg -T | grep -Ei "oom|out of memory|killed process"
journalctl -k | grep -Ei "oom|out of memory|killed process"
​
# 2) BE 内存日志
grep -Ei "bad_alloc|memory limit|memory exceeded|MemTracker|MemTable|DeltaWriter" \
  be/log/be.INFO* be/log/be.WARNING*
​
# 3) 崩溃前 5~10 分钟的 Load / Insert 时间线
# 4) BE Memory Tracker 的 Query / Load / MemTable / Compaction 分项

六、Host is down / CANCELLED 的正确解读

FE Audit Log 里大量出现:

csharp 复制代码
[CANCELLED] failed to send brpc when exchange
Host is down

这条不能单独当作 BE 掉线的根因。

正确的因果顺序是:

swift 复制代码
BE 异常退出
    ↓
目标 BE 不再响应 BRPC
    ↓
其他 BE / Coordinator 发 Exchange RPC
    ↓
Host is down / connection reset
    ↓
Fragment cancel
    ↓
Query CANCELLED

Host is down 和 CANCELLED 都是 BE 进程退出后的二次传播结果。

同理,Compaction 日志里那些:

arduino 复制代码
estimate batch size for vertical compaction
succeed to do cumulative compaction

只能说明"当时 Compaction 在跑",不能证明它是 SIGSEGV 的原因------101/108 的栈里根本没有 Compaction 相关符号。


七、处置方案

7.1 P0:先分类,再处置

必须形成完整的归因表:

sql 复制代码
BE → 崩溃时间 → 信号 → 栈顶 → Query ID → SQL → 根因

没有分类,就没有正确的修复。

7.2 P1:101/108 的 backport 要整套做

不建议 直接把 master 的 brpc_closure.h 覆盖到 2.1.11 的 ref_count_closure.h。

原因:这个 PR 涉及多个调用方:

bash 复制代码
be/src/util/brpc_closure.h
be/src/exec/exchange/vdata_stream_sender.h
be/src/exec/operator/exchange_sink_buffer.cpp
be/src/exec/runtime_filter/runtime_filter.cpp
be/src/exec/runtime_filter/runtime_filter_mgr.cpp
be/src/exec/runtime_filter/runtime_filter_producer.cpp
be/src/exec/sink/writer/vtablet_writer.cpp
be/test/exec/exchange/exchange_sink_test.cpp

正确做法:

css 复制代码
2.1.11 源码树
    ↓ 建专门 backport 分支
逐文件迁移
    ↓ 解决 2.1 分支代码差异
完整编译
    ↓ Exchange UT + Runtime Filter UT
测试环境长时间运行
    ↓ 灰度 BE
生产滚动升级

7.3 P1:修复 BE 进程托管

本次事故的放大因素 :BE 由 SelectDB Manager 启动,systemctl 管不到真实进程。

ini 复制代码
BE crash
    ↓
systemd 未托管
    ↓
Restart=on-failure 无法按预期拉起
    ↓
BE 长时间 Dead
    ↓
业务持续受影响

检查方法:

bash 复制代码
systemctl status doris-be
systemctl cat doris-be
ps -ef | grep doris_be

确认 MainPID 是否等于当前 BE 的 PID。如果不是,说明托管是假的。

7.4 P1:104 单独做内存专项

104 与 PR #61782 无关,必须走另一条路:

arduino 复制代码
DeltaWriterV2::write()
    ↓
VTabletWriterV2::write_memtable()
    ↓
MemTable / Block / Buffer / Arena
    ↓
到底是哪一次 allocation 抛出了 bad_alloc?

八、总结

8.1 四个判断

① 同一个告警文案,可能是完全不同的故障

三个 BE 同时掉线,监控上是同一行字。但信号不同(SIGSEGV / SIGABRT),根因就不同。

排查第一步永远是看信号和栈,不是看告警。

② "静默的二次传播"会骗人

Host is down、CANCELLED 这些错误在 FE 日志里刷屏,看起来像是根因,实际是结果。

判断方法:看它发生的时间点,是在进程退出之前还是之后。

③ 上游补丁不等于能直接拿来用

找到了 PR 不等于问题解决。这个 PR 跨了 8 个文件、改了 callback 生命周期模型、还依赖 UT 验证。

"能不能 backport"和"找到了什么 bug"是两个不同难度的问题。

④ 不能归因的地方要明说

104 至今没有完整根因------只能说"bad_alloc 导致 abort",说不出是哪一次分配。

在报告里明确写出"未确认",比编一个合理的解释更专业。

8.2 排查 Checklist

检查项 方法
每台 BE 的崩溃信号 gdb -c core / eu-stack,看 SIGSEGV / SIGABRT / SIGBUS
栈顶符号 提取前 10 帧,重点看 Doris 命名空间
是否命中已知上游问题 拿栈顶符号 + 版本号搜 Apache Doris issues
FE 侧错误的时间线 确认 Host is down 是否晚于 BE 退出时间
进程托管是否有效 MainPID 与真实 PID 对比
内存类崩溃的证据 dmesg + Memory Tracker + 崩溃前 10 分钟日志

参考

  • Apache Doris PR #61782:[fix](rpc) Fix AutoReleaseClosure data race with callback reuse
  • Apache Doris commit 0a1d842
  • Apache Doris 源码:be/src/util/ref_count_closure.h、be/src/exec/exchange/vdata_stream_sender.h
  • Apache Doris 官方邮件归档:commits@doris.apache.org
相关推荐
小蒋观天下1 小时前
网络抖动、乱序、丢包:UDP/RTSP/TCP对AI抓拍、轨迹跟踪的不同影响
大数据·人工智能·深度学习
super_Omni1 小时前
Chrome 插件入驻实战:从 Manifest V3 到过审上架的完整路径
大数据·前端·chrome
悟天特斯2 小时前
安防一体化:从“各管各的“到“全局联动“
java·大数据·网络·人工智能·物联网
geovindu3 小时前
SQL Data Sampling and Approximation Techniques using postgreSQL 18
大数据·数据库·数据仓库·sql·postgresql·数据库开发·数据库架构
Leo.yuan3 小时前
集团司库系统之外还需要什么?2026年资金分析与司库决策平台建设指南
大数据·人工智能·区块链
2601_970703834 小时前
工业除湿机选型与安全使用研究报告:把铭牌数字放回工况里
大数据·人工智能
starzy19904 小时前
Kafka网络层第一道门:Acceptor线程是怎么启动的
大数据·kafka
银河麒麟操作系统4 小时前
两案例入选全球计算联盟年度产业应用案例汇编
大数据·汇编·人工智能