Doris 4.1.1 + CentOS 7(glibc 2.17):查询 DLF 外表导致 BE 稳定宕机

Viewed 4

环境:3 FE + 5 BE,Doris 4.1.1,CentOS 7.9(glibc 2.17),JDK 17.0.2。
现象:

  • 同一批库表挂了两个 catalog:
    • 云上:Aliyun DLF(hive.metastore.type=dlf)+ OSS-HDFS;
    • 云下:本地 Hive Metastore + HDFS。
  • 同一句 SQL:走云上 catalog 稳定让 BE 宕机(5 台 BE 几乎同时重启);走云下 catalog 完全正常。
  • 规避手段:SET max_file_scanners_concurrency = 1; 可以不崩(但这是把线程销毁次数压下去,属于掩盖)。
  • 升级 JindoSDK 6.10.8 → 6.10.9 无效。
  • 调大 JVM -Xmx 无效(崩在原生分配器,不在 Java 堆)。

以下内容是结合ai排查结论,ai认为问题在于cnetos7.9的glibc版本过低,与doris有兼容性问题(在本文第三章节第3小结)

二、崩溃栈

生产 core(gdb 7.6 因 DWARF5 只能到函数名级别):

#0  pthread_sigmask                       libpthread.so.0
#1  PosixSignals::chained_handler         libjvm.so
#2  JVM_handle_linux_signal               libjvm.so
#3  <signal handler called>
#4  atomic_load_p (a=0x1a0000)            jemalloc atomic.h:83
#5  rtree_leaf_elm_bits_read              jemalloc rtree.h:182
#7  rtree_metadata_read                   jemalloc rtree.h:446
#8  emap_alloc_ctx_lookup (ptr=0x7fb1b40008c0)  jemalloc emap.h:238
#9  ifree (tcache=0x0, slow_path=true)    jemalloc jemalloc.c:2877
#10 je_free_default                       jemalloc jemalloc.c:3014
#11 _dl_deallocate_tls                    ld-linux-x86-64.so.2
#12 __free_stacks / #13 __deallocate_stack / #14 start_thread

在干净的 4.1.1 单机上用最小复现跑出来的栈(不需要 DLF/OSS):

*** SIGSEGV address not mapped to object (@0x49500) ***
 0# doris::signal::FailureSignalHandler    signal_handler.h:420
 1# PosixSignals::chained_handler          libjvm.so
 2# JVM_handle_linux_signal                libjvm.so
 4# rtree_metadata_read                    jemalloc rtree.h:446
 5# je_free_default                        jemalloc jemalloc.c:3014
 6# __pthread_create_2_1                   libpthread.so.0
 7# os::create_thread                      libjvm.so
 8# JVM_StartThread                        libjvm.so

这条栈与历史 issue #36641(2.0.10)、以及 #45210 正文里的栈基本同族。
关键细节:崩溃地址 0x1a0000 恰好等于 ((ptr >> 12) & 0x3FFFF) * 8 —— 也就是 jemalloc 的 rtree leaf 指针是 NULL,
release 构建里 NULL 检查被裁掉,直接读 NULL + subkey*8 才会崩。这说明被 free 的指针根本不是 jemalloc 分配的。

三、根因(ai结合源码分析排查)

1. Doris 只接管了"公开"的 malloc 家族

be/src/runtime/memory/jemalloc_hook.cpp 用 __attribute__((alias)) 覆盖了 11 个公开符号:
malloc / free / calloc / realloc / memalign / aligned_alloc / valloc / pvalloc / posix_memalign / cfree / malloc_usable_size。
doris_be 又是 ENABLE_EXPORTS 1(-rdynamic)链接的,所以这些符号对进程内所有 .so 生效(包括 libjvm、glibc 的 free 调用)。

2. 但 jemalloc 自带的 __libc_* 拦截器没有被编译

jemalloc 只有不带 public prefix 构建时才会生成 __libc_malloc/free/calloc/realloc/memalign/valloc 这些 glibc 内部别名
(configure.ac:if test "x${JEMALLOC_PREFIX}" = "x";src/jemalloc.c:3252/3281-3298)。
而 Doris 的构建参数是 --with-jemalloc-prefix=je(thirdparty/build-thirdparty.sh),所以这些符号一个都没有。
在真 4.1.1 二进制上可以验证:

$ nm -D --defined-only lib/doris_be | grep -E ' (malloc|free|calloc|memalign)$'
T malloc / T free / T calloc / T memalign ...     # 7 个,有
$ nm -D --defined-only lib/doris_be | grep __libc_memalign
(空)                                             # 没有 ← 问题在这

3. glibc ≤ 2.27 的 TLS 分配/释放是不对称的

  • glibc 2.17 elf/dl-tls.c:529:给"运行期 dlopen 的模块"分配每线程动态 TLS 块用 __libc_memalign(→ glibc 自己的堆);
  • 释放却在 dl-tls.c:482 用公开 free(→ 被 Doris 接管到 jemalloc);
  • glibc 2.28 起改成用 malloc 分配(2.28 :346、2.34 :436)→ 分配/释放都进 jemalloc,问题自然消失。

于是:glibc 堆分配的指针,被 jemalloc 去释放 → jemalloc 的 extent map 里没有它 → rtree leaf=NULL → SIGSEGV。

为什么"云上崩、云下不崩":BE 的 C++ 读取代码两边完全一样;差别只在于云上 catalog 会让 BE 内 JVM 运行期 dlopen JindoSDK 的 /tmp/libjindosdk-*.so(带 TLS),而本地 HDFS 路径不加载任何"运行期 dlopen 的带 TLS 原生库"。再叠加云上 I/O 慢导致的扫描线程高频创建/销毁,就把概率放大到"稳定复现"。

为什么和 JindoSDK 版本无关:触发条件只与"它是不是一个运行期 dlopen 的带 TLS 原生库"有关,与它内部修了什么无关。

四、证据(欢迎复核)

4.1 同一份二进制、同一份负载,两个 glibc 版本结果完全不同

环境 现象 gdb 计数
CentOS 7.9 / glibc 2.17 第 1 次运行就 SIGSEGV _dl_allocate_tls=64,__libc_memalign=102
Rocky 9.7 / glibc 2.34 10/10 不崩 _dl_allocate_tls=484,__libc_memalign=0

(同一个测试程序:运行时 dlopen 一个带 __thread 的 .so,8 轮 × 64 线程,每线程 touch 该模块 TLS 后退出。)

4.2 真 Doris 4.1.1 单机复现(不需要 DLF)

单机部署 1 FE + 1 BE(同一版本串),自建 2000 行表,然后:

  1. 自建一个带 __thread 的 native 库 libtlsmod.so;
  2. 用一个 Java UDF 在静态初始化里 System.load() 它(等价于 JindoSDK 运行期 dlopen),每次 evaluate() 起几个线程调它的 native 函数后退出;
  3. SELECT COUNT(tls_udf(CAST(k AS STRING))) FROM t; → 第 1 条查询就让 BE 崩(栈见上面第二节)。

4.3 修复验证(LD_PRELOAD,无需重编译 Doris)

只用 LD_PRELOAD 补一个符号:

void *__libc_memalign(size_t alignment, size_t size) { return memalign(alignment, size); }
修复前 修复后
首条查询 SIGSEGV 正常返回 2000
BE 进程 崩溃/重启 pid 全程不变
压力 — 26 轮查询 × 2000 行 × 4 线程 ≈ 20.8 万次线程创建/销毁,零崩溃

shim 确实在被调用(be.out 里有标记),且 nm -D libmemalign_shim.so 只有 T __libc_memalign + U memalign。

五、修复建议(结合ai排查给出的方案,正在测试环境尝试方案2是否有效)

方案 1(推荐):在 jemalloc_hook.cpp 里补齐 6 个 __libc_* 别名

#ifdef __GLIBC__
// glibc <= 2.27 用 __libc_memalign 分配每线程动态 TLS 块,却用公开 free 释放;
// 只接管 free 会导致"glibc 堆分配、jemalloc 释放",在线程创建/退出时 SIGSEGV。
void* __libc_malloc(size_t n)             __THROW { return jemalloc(n); }
void  __libc_free(void* p)                __THROW { jefree(p); }
void* __libc_calloc(size_t n, size_t s)   __THROW { return jecalloc(n, s); }
void* __libc_realloc(void* p, size_t n)   __THROW { return jerealloc(p, n); }
void* __libc_memalign(size_t a, size_t s) __THROW { return jealigned_alloc(a, s); }
void* __libc_valloc(size_t s)             __THROW { return jevalloc(s); }
#endif

doris_be 已开 ENABLE_EXPORTS,加完这些符号会自动导出;与上面的 LD_PRELOAD 方案语义完全等价(已实测)。

方案 2(不改代码,临时止血):LD_PRELOAD 一个 shim

把上面的单符号 shim 编成 .so,放到 be/lib/,用 wrapper 在启动 BE 前 export LD_PRELOAD。
不需要重编译 Doris、可随时回退(ai演练过 apply → rollback → re-apply,每一步查询都正常)。

方案 3(长期):升级到 glibc ≥ 2.28 的操作系统

Rocky/Alma 8+、Ubuntu 20.04+ 等。实测同一份问题程序在这些系统上 10/10 不崩。

方案 4(仅规避,不修复)

固定扫描线程池(让线程只创建不销毁)> 改用原生 S3(不走 Jindo/OSS-HDFS)> max_file_scanners_concurrency=1。

0 Answers