Skip to content
0

多进程fork死锁导致的容器OOM排查

一、问题现象

Prometheus OpenStack Exporter 使用 ForkingMixIn 处理 HTTP 请求。集群规模为 4 控制 + 3 云产品 + 20 网络 + 1224 计算节点,容器内存限制 1GiB。

3 个 openstack-metrics 容器内存占用均超过 90%(900MiB ~ 1024MiB)。拉取 24 天内存数据,从重启后的 41MiB 单调增长至 900MiB+,无回落。

进入容器查看进程:

[root@node-1 etc]# ps -eo pid,ppid,rss,etime,stat,wchan:50,cmd
    PID    PPID   RSS     ELAPSED STAT WCHAN                    CMD
      1       0    64  29-12:18:05 Ss   arm64_sys_rt_sigtimedwait  /usr/local/bin/dumb-init /bin/bash /tmp/openstack-exporter.sh start
      7       1 58304  29-12:18:04 Ssl  do_select                  python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
    113       7 57536  29-11:46:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
   2429       7 59328   1-18:18:06 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
   2590       7 57024   1-17:29:02 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
   3916       7 57408   1-11:05:02 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  10895       7     0       00:06 Z    -                           [python] <defunct>
  10896       7     0       00:02 Z    -                           [python] <defunct>
  10897       0  6528       00:00 Rs+  -                           ps -eo pid,ppid,rss,etime,stat,wchan:50,cmd
  13550       7 61120  26-16:07:02 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  13783       7 61376  26-15:01:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  18127       7 59200  12-02:16:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  18889       7 61696  25-13:18:02 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  19537       7 57792  11-19:11:36 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  19862       7 61376  11-17:34:36 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  20232       7 59008  25-06:31:36 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  28047       7 60480  10-00:07:36 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  29618       7 59200   9-16:15:36 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  36832       7 59776   8-04:15:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  37647       7 59328   8-00:14:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  39463       7 57536   7-15:03:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  52180       7 61632  18-14:23:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  53416       7 61824   4-17:41:32 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml
  55857       7 61632  17-20:03:36 S    futex_wait_queue_me        python /usr/local/bin/exporter/main.py --config-file=/etc/openstack-exporter/config.yaml

PPID=7 的 S 状态子进程共 19 个,WCHAN 全部为 futex_wait_queue_me(等锁,不消耗 CPU),ELAPSED 从 4 天到 29 天不等,说明持续累积。RSS 在 57MiB ~ 62MiB 之间,累计约 1100MiB,与容器内存占用吻合。可以确认是进程泄漏,而非常规内存泄漏。

二、根因分析

1. OSCache 刷新周期

OSCache 每个周期请求 Nova /os-hypervisors/detail 接口均失败(超时 20s + 1 次重试 = 40s),其余接口合计约 5s,单次执行总耗时约 45s。加上默认 sleep 30s,完整周期约 75s。而 Prometheus scrape 间隔为 30s,两者存在周期性交叉。

2. 稳定复现

在 OSCache 线程中永久持有 logging handler 锁,模拟 fork() 瞬间锁被持有的场景:

python
def run(self):
    if self.type == 'service':
        sleep(3)
        root = logging.getLogger()
        h = root.handlers[0]
        h.lock.acquire()  # 永久持锁
        while True:
            sleep(3600)

部署后 curl /metrics100% 必现死锁,移除后恢复正常。

3. 死锁机制

ForkingMixIn 流程为 accept() → fork()。父进程中有 OSCache 线程周期性刷新缓存并写日志。fork() 仅复制调用线程(主线程),OSCache 线程在子进程中直接消失。

父进程
├── 主线程: serve_forever() → accept() → fork()
├── OSCache(service) 线程: 周期刷新,调用 logger.info/debug
└── OSCache(resource) 线程: 同上

fork() 时若 OSCache 线程正持有 handler.lock:
  → 锁状态原样复制到子进程
  → 子进程中该锁永久无法释放 → 死锁

日志调用链路:

logger.info → Logger._log → Logger.callHandlers
  → handler.handle → handler.acquire(获取 handler.lock)
  → handler.emit → handler.release

fork() 落在 acquire 和 release 之间 → 子进程继承 locked 状态

4. 偶发条件

死锁需两个条件同时成立:

  1. fork() 瞬间 OSCache 线程正持有 logger 的 handler.lock
  2. 子进程中 collector 数据为空(接口失败),触发 except 块的 logger.warning

时序上,OSCache 周期 75s 与 scrape 间隔 30s 周期性交叉,加上 /os-hypervisors/detail 持续失败 40s 拉长了持锁窗口,大幅提高了碰撞概率。

三、修复

子进程 fork 后不再调用 logger,从根源消除条件 2。

diff
- except Exception as e:
+ except Exception:
      traceback.print_exc()
-     logger.warning("Failed to retrieve stats for collector "
-                    "with key: {}, error: {}".format(
-                        collector.get_cache_key(), str(e)))
logger.warning(...)traceback.print_exc()
输出路径logging → handler.lock → stderr直接写 stderr fd
经过 handler.lock ← 死锁来源
信息量异常类型 + 消息完整调用栈 + 行号

traceback.print_exc() 直接写 fd,不经过锁,且输出完整调用栈,K8s 环境下 stderr 同样由 kubectl logs 采集。部署后子进程不再死锁,容器内存回落至正常水平。

四、总结

  1. ForkingMixIn + 线程 + 锁 = 高危组合fork() 后子进程只继承调用线程,但会复制整个进程地址空间,包括锁状态。当 scrape 请求触发 fork() 的时刻恰好落在 OSCache 刷新线程持有锁的窗口内,子进程将继承一个不可释放的锁(全局 logging 锁),从而死锁。子进程中任何等锁操作都将永久阻塞。

  2. traceback.print_exc() 是 fork-safe 的。直接写文件描述符,不经过锁机制,子进程中可安全使用。

  3. 内存增长先区分泄漏类型ps -eo 查看 WCHAN 和进程数量,避免在代码层面排查内存泄漏走弯路。

最近更新