海光 DCU 生产环境 munmap_chunk(): invalid pointer 排查实录
摘要:本文记录一个运行在海光 DCU + DTK 国产化环境上的声纹分析服务,因
librosa的audioread回退路径在进程内写坏堆,导致进程反复崩溃、任务不断中断的问题。从现象到根因,再到“用 ffmpeg 子进程隔离不可靠解码”的修复方案,完整复盘。
一、问题背景与现象
生产服务器配置:
- 国产化 海光 DCU 显卡,驱动栈 DTK 25.04.4
- Conda 环境
python3.11 - 声纹分析服务
voice1,底层是 Celery worker,由 systemd 托管,配置了Restart=always
运行一段时间后,进程频繁崩溃,日志里反复出现:
munmap_chunk(): invalid pointer
由于 Restart=always,进程被不断拉起,正在执行的“声纹分析”任务随之中断重来。更诡异的是:
- 三个配置几乎相同的 service(
voice1/voice/voice2)里,只有voice1崩; - 无论重启多少次,甚至复制一份成
voice3.service,必然复现; - 崩溃时间点不固定,有时在启动加载声纹时,有时在处理任务的“音转文”阶段。
二、排查过程
munmap_chunk(): invalid pointer 是 glibc 的堆破坏告警:某段 C/C++ 原生代码做了越界写或跨分配器释放,破坏了堆元数据,后续某次 free() 才被检测到。它不是 Python 异常,try/except 抓不到,进程直接被 abort。
我用一组只读、不碰生产队列的命令做对比:
# 对比三个服务触发“soundfile 读取失败”的次数
for s in voice1 voice voice2; do
echo "$s: PySoundFile失败=$(journalctl -u $s --since today | grep -c 'PySoundFile failed')"
done
# 抓取崩溃前上下文,判断崩在哪个阶段、哪个文件
journalctl -u voice1 --since today --no-pager | grep -B 100 -m1 munmap_chunk
journalctl -u voice1 --since today | grep -E '添加声纹成功|添加声纹失败|视频地址|PySoundFile|munmap_chunk'
对比结果一锤定音:
| 服务 | 今日 PySoundFile failed 次数 |
|---|---|
| voice1 | 7004 |
| voice | 0 |
| voice2 | 0 |
只有 voice1 在大量触发 PySoundFile failed,另外两个一次都没有——说明它们根本没领到那些“坏文件”,配置相同只是表象。
再翻崩溃现场日志,锁定到启动加载执法员声纹的那一刻:
[libmpg123] Giving up searching valid MPEG header after 65536 bytes of junk.
speaker_verification.py:167: UserWarning: PySoundFile failed. Trying audioread instead.
...
munmap_chunk(): invalid pointer
三、根因分析
三个因素叠加:
1. 声纹文件不太标准
一批执法员声纹是头部带 64KB “junk” 的非标准 .mp3(可能是异常 ID3 标签或导出时混入的非音频字节)。它们能播放,但不是严格标准 mp3。
2. librosa 的“静默回退”是隐患
librosa.load 默认先用 soundfile(libsndfile)读取;读不动时自动回退到 audioread。这一步是静默的,绕开了业务代码的异常处理。对比日志里两类文件的表现:
.m4a那批:soundfile 直接抛异常 → 被except接住 → 打印“添加声纹失败”(安全失败,没破坏堆);.mp3那批:soundfile 读不出但不报错 → audioread 硬解码成功 → 打印“添加声纹成功”,但就在解码瞬间把堆写坏了。
注意:“添加声纹成功”只代表这条声纹的特征算出来了、函数正常返回了,并不代表堆没被破坏。最危险的恰恰是这些“成功”的 mp3,而不是“失败”的 m4a。
3. 海光 / DTK 环境的库不兼容
audioread 在这台机器上走的是进程内原生解码(gstreamer / ffmpeg 的 C 库),与 dtk / conda 混载的 libstdc++ / libgomp 相处不好,在进程内写坏了堆。
关键认知:崩溃位置不等于破坏位置
堆被破坏的那一刻,进程不会立刻崩——破坏只是把堆的内部记账弄脏了。真正的 munmap_chunk 要在之后某次毫不相干的 free() 才被检测到。这就是“有时在启动崩、有时在任务崩”的原因。
完整时间线如下:
- 播种(因):每次启动,
voice_init加载坏 mp3 → audioread 在进程内写坏堆。此时函数仍能返回,打印“添加声纹成功”。 - 继续运行:堆已脏,但后续文件读取调用照样能完成,继续打印“成功”。
- 收割(果):之后某次普通的
free()(常常恰好在“音转文”阶段)踩到坏块 →munmap_chunk→ 进程 abort。 - 重启循环:
Restart=always拉起 → 重新播种 → 迟早再次收割。
为什么只有 voice1?因为它恰好领到了这批坏 mp3;voice / voice2 没领到,自然不崩。
四、解决方案
核心思路:彻底绕开 audioread 回退。遇到 soundfile 读不动的文件,改用 ffmpeg 子进程转成标准 wav 再读。ffmpeg 对 junk 头部极其鲁棒,且跑在独立进程里——即便解码有问题也只崩子进程,绝不会污染主进程堆。
新增安全加载函数
在 speaker_verification.py 中新增:
def _safe_load_audio(audio_path, target_sr=None):
"""禁止 librosa 的 audioread 回退:soundfile 读不动时,用 ffmpeg 转码兜底。"""
import subprocess, tempfile
data, sr = None, None
try:
data, sr = sf.read(audio_path, dtype="float32", always_2d=True)
except Exception:
data, sr = None, None
if data is None: # soundfile 读不动 → ffmpeg 子进程转码
tmp = None
try:
fd, tmp = tempfile.mkstemp(suffix=".wav")
os.close(fd)
cmd = ["ffmpeg", "-y", "-i", audio_path]
if target_sr is not None:
cmd += ["-ar", str(target_sr)]
cmd += ["-ac", "1", "-f", "wav", tmp]
subprocess.run(cmd, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, check=True, timeout=120)
data, sr = sf.read(tmp, dtype="float32", always_2d=True)
except Exception:
data, sr = None, None
finally:
if tmp and os.path.exists(tmp):
try: os.remove(tmp)
except OSError: pass
if data is None:
raise RuntimeError(f"无法解码音频: {audio_path}")
return np.ascontiguousarray(data.T), sr # (channels, samples),与 librosa 对齐
替换调用点
# speaker_verification.py - preprocess_audio()
# 改前: y, sr = librosa.load(audio_path, sr=None, mono=False)
# 改后:
y, sr = _safe_load_audio(audio_path)
# audio_transcribe.py - extract_audio_segment()(共 2 处)
# 改前: audio, sr = librosa.load(audio_file, sr=16000)
# 改后:
audio, sr = _safe_load_audio(audio_file)
if audio.shape[0] > 1:
audio = librosa.to_mono(audio)
if sr != 16000:
audio = librosa.resample(audio, orig_sr=sr, target_sr=16000)
sr = 16000
验证
journalctl -u voice1 -f | grep -E 'PySoundFile|munmap_chunk|添加声纹成功'
预期 PySoundFile failed 与 munmap_chunk 同时消失,声纹全部“添加声纹成功”。
五、经验总结
munmap_chunk类崩溃第一反应是“原生堆破坏”,不是 Python 逻辑 bug,优先怀疑 C 扩展与混载动态库。- 崩溃位置和根因位置可以相隔很远:用日志量化对比(如
PySoundFile failed计数)比盯着崩溃栈更有用。 librosa.load的audioread回退是隐患:在国产化 / 非标准库环境下,它会把不可靠的原生解码器加载进进程内。生产服务应显式控制音频加载路径,把不可靠解码隔离到子进程,或统一预转码为标准 wav。- “配置一样却不崩”往往是任务数据不同:用日志计数对比能快速定位。
- 临时止血可在 unit 里加
Environment="MALLOC_CHECK_=0"让 glibc 不主动 abort,但那只是挡一下,根因仍要靠修代码。
附:诊断命令速查
# 三服务异常对比
for s in voice1 voice voice2; do
echo "$s: PySoundFile=$(journalctl -u $s --since today | grep -c 'PySoundFile failed') \
munmap=$(journalctl -u $s --since today | grep -c munmap_chunk)"
done
# 崩溃上下文
journalctl -u voice1 --since today --no-pager | grep -B 100 -m1 munmap_chunk
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/MMMMMMNONG/article/details/169264315




