CLOUD GIDO
工程笔记
场记2026-09-08GIDO

Day 16|Doris BE 周期性重启:不是内存不够,是 RScan 把 PID 打满了

系列:《Cloud GIDO 三产品特性深讲》加餐 · 场记第 1 篇
标签:Doris、存算分离、线程泄漏、K8s、pids.max、排障


开篇:组件齐了,BE 照样「周期性消失」

Cloud GIDO 跑在 Kafka / Flink / Doris / K8s 之上。运行时再强,底座也会出「看起来像内存、其实不是」的故障。

很常见的现场:

  1. Compute(CG)节点隔一两天重启一次
  2. 第一反应:加内存(8G → 16G)
  3. 第二反应:怪 Routine Load / 大查询
  4. FE 开始报 UnknownHostException、offset 拉失败——其实是 BE 已经挂了

真正要命的日志往往只有一行:

Could not create thread. (error 11) Resource temporarily unavailable
Thread pool routine_load failed to create thread

这篇按真实排查顺序写:痛点 → 证据 → 纠偏 → 根因 → 分层治理
环境参考:Doris BE 3.1.4(Cloud / Storage Vault / S3)、K8s、pids.max ≈ 37776
配图已脱敏:数据源 / 集群 / namespace / pod 标签均已打码或裁掉,勿使用未打码原图发文。

夜莺:近 7 天 container_threads 锯齿,爬到约 38k 后重启


现象:重启曲线是「锯齿」,不是偶发尖峰

时间线通常是:

  1. 线程数线性爬升(监控呈锯齿)
  2. 顶到容器 pids.max
  3. 某次再 pthread_create 失败(日志常落在 Routine Load)
  4. BE abort 重启 → 线程归零 → 再爬

图上要点:

  • 斜率近似直线 → 更像「创建不回收」,不像随负载抖动
  • 每条线顶在 ~38k,与 pids.max≈37776 吻合
  • 多条线错开爬升:重启会换 container id,图例会变多

结论先行:不是 OOM,加内存挡不住。


error 11:先查 PID,不是先加内存

pthread_create 返回 EAGAIN(error 11),常见上限:

限制典型来源怎么看
容器 PID 配额K8s podPidsLimit / cgroup pids.maxcat /sys/fs/cgroup/pids.max
用户线程上限ulimit -u / nproculimit -u
节点内核kernel.pid_max / threads-maxsysctl

重启后进容器,常见是「掉下来之后」的稳态:

cat /sys/fs/cgroup/pids.max      # ≈ 37776
cat /sys/fs/cgroup/pids.current  # 几千~一万多
ulimit -u                        # unlimited
sysctl kernel.pid_max            # 很大,不是瓶颈

崩溃前是否贴顶,必须看监控历史(夜莺 / Grafana / Prometheus),不能只看重启后的 pids.current


用监控钉死「直接死因」

PromQL(脱敏示例):

container_threads{
  namespace="<ns>",
  pod=~"doris-be-.*",
  container="compute"
}

只认 container="compute"、镜像为 doris-be 的那条。

观察数值含义
峰值37774~37776贴满 pids.max≈37776
重启后约 1.1万~1.7万abort 后线程掉下来

因果链:

线程打满 pids.max → 再建线程 EAGAIN → abort 重启

24 小时窗口同样是锯齿:

24 小时窗口下的线程锯齿(脱敏)


坑:日志写 Routine Load ≠ RL 是主因

崩溃栈落在 RL,容易误判:「增量也不大,怎么打挂?」

拆两层:

角色含义
触发者撞顶时正好 RL 再开线程 → 日志写 RL
水位贡献者真正堆到 3.7 万的,未必是 RL

要回答「谁占线程」,必须对还活着、正在上涨的 BE 做线程名采样,不能只看 abort 栈。


关键一招:按线程名聚合

PID=$(pidof doris_be)

echo "PID=$PID total=$(ls /proc/$PID/task | wc -l) time=$(date -u +%H:%M:%S)"

for t in /proc/$PID/task/*/comm; do cat "$t"; done 2>/dev/null \
  | sort | uniq -c | sort -rn | head -40

早期采样示例:

线程名数量说明
RScan_normal5545远端/云存储扫描,已是大头
brpc_*数百RPC
routine_load24很少
total~7300

水位更高时(total≈25377):

总线程25377
RScan_normal23620(约 93%)
routine_load24,且 RL task_count≈0

再对照 BE metrics:

curl -s http://127.0.0.1:8040/metrics \
  | grep -E 'doris_be_thread_pool_active_threads|doris_be_routine_load_task_count' \
  | grep -E 'RScan|routine_load'

对比很扎心:

  • 指标里 RScan_normalmax=512,active=0
  • 系统里却有 两万多个 同名线程

不是扫描真的需要两万线程,而是泄漏:创建了不回收。
RL 只是贴顶后再 pthread_create 失败的那一棒。


根因(可写进故障报告)

维度结论
直接原因线程打满 pids.max≈37776,EAGAIN → abort
主要贡献RScan_normal 远端扫描线程异常堆积
业务相关扫云表、INSERT ... SELECT、Storage Vault/S3 读路径
易误解abort 栈常在 RL;RL ≠ 水位主因
无效手段只加内存(如提到 16G)

一句话:

RScan_normal 线程泄漏 → 贴满 pids.max → EAGAIN → BE abort。


处理分层:止血 / 告警 / 治本

止血

  • PAUSE 重复 / 非必要 Routine Load
  • 降低大宽表 INSERT...SELECT 频率(现场曾把 ADS 宽表从 1 分钟改为 10 分钟,爬升斜率明显变缓)
  • 必要时滚动重启泄漏严重的 BE(治标)
  • 评估提高 podPidsLimit / pids.max(如 65536)

宽表调度降频后,斜率变缓(仍可能最终贴顶)

降频能「减速」,说明宽表/扫云会加速泄漏;曲线仍可能爬到 ~38k → 治本还得解决 RScan 不回收

告警

pids.max≈37776 时,日常可能就在 1.7万~2.5万。建议:

级别阈值持续
Warning30000(约 80%)≥10 分钟
Critical35000(约 93%)≥1~2 分钟
max by (pod) (
  container_threads{
    namespace="<ns>",
    pod=~"doris-be-.*",
    container="compute"
  }
)

以后若提高 pids.max,按 66% / 85% / 93% 同比调整。

治本

  • 收敛并发扫云表 / 宽表加工
  • 核对 remote scan / scanner 相关上限
  • 对照 Doris 版本是否有 Remote Scan 线程泄漏补丁
  • RScan_normal 线程采样与 container_threads 叠图

社区公开 Issue(本来就在公网,可直接引用):

  • apache/doris#66997
    [Bug][Cloud] RScan_normal ThreadPool idle workers never shrink; OS threads grow far beyond max_threads=512

Apache Doris 社区 Issue 标题

脱敏原则:内部集群名 / 公司环境 / 私有监控数据源要打码;Apache Doris 等开源仓库与 Issue可以放开,反而增强可信度。


排障清单(可收藏)

容器内

cat /sys/fs/cgroup/pids.max
cat /sys/fs/cgroup/pids.current
ulimit -u

PID=$(pidof doris_be)
ls /proc/$PID/task | wc -l
for t in /proc/$PID/task/*/comm; do cat "$t"; done 2>/dev/null \
  | sort | uniq -c | sort -rn | head -40

curl -s http://127.0.0.1:8040/metrics \
  | grep 'doris_be_thread_pool_active_threads' \
  | grep -v '} 0$' | sort -t' ' -k2 -nr | head -20

监控

container_threads{namespace="<ns>",pod=~"doris-be-.*",container="compute"}
container_memory_working_set_bytes{namespace="<ns>",pod=~"doris-be-.*",container="compute"}

FE

SHOW BACKENDS\G
SHOW ROUTINE LOAD\G
SHOW PROCESSLIST;

经验教训

  1. abort 栈 ≠ 根因水位——日志写谁,只说明谁最后踩空。
  2. error 11 优先查 PID / 线程,不要先扩内存。
  3. 重启后现场会「变干净」——依赖监控历史 + 上涨中采样。
  4. 池配置要对齐 OS 线程数——max=512、active=0 却有两万同名线程 → 高度可疑泄漏。
  5. 告警阈值贴日常水位——太低只会吵到麻木。

排查叙事可记成:

表象(重启)→ 误判(内存 / RL)→ 证据(pids.max + 夜莺锯齿)→ 纠偏(线程名采样)→ 根因(RScan 泄漏)→ 分层治理


今日小结

底座组件「齐了」不等于不会踩坑。Doris Cloud / 存算分离场景下,RScan_normal 这类远端扫描路径一旦泄漏,会先打满容器 PID,再以「很像 OOM / 很像 RL」的样子把 BE 打挂。

对跑 GIDO 批加工、宽表 INSERT...SELECT、扫云表的团队:监控里请把 container_threads 和线程名采样当成一等公民,别只盯内存曲线。

讨论:

  1. 你们 Doris Compute 的 pids.max / 线程告警阈值是怎么定的?
  2. 撞过 error 11 时,最后是内存、RL,还是某个内部线程池泄漏?

延伸

  • 系列总览:https://cloud-gido.com
  • Apache Doris 文档 / Issue(Remote Scan / Cloud)
  • 发 CSDN:本地上传 images/fig-doris-rscan-*.png(平台不认相对路径)