使用Flask+Librosa转换音频采样率时内存占用过高问题求助
问题描述
我基于Flask + Nginx + Gunicorn开发了一个支持文件上传并转换音频采样率的Web服务。处理约40MB的MP3或WAV文件时,内存使用率急剧飙升,即使是AWS Lightsail(2GB内存)的配置,瞬时内存占用也会超出限制。
日志信息
- Nginx error.log:
upstream prematurely closed connection while reading response header from upstream - Sys.log:触发OOM(内存不足)并终止进程
目前仅能靠虚拟内存勉强处理单个文件,但需要支持多用户并发,因此必须解决内存占用问题。
内存使用记录(自定义memory_usage函数统计)
- [#1] memory usage: 153.62500 MB
- [#2] memory usage: 488.25391 MB
- [#3] memory usage: 219.86719 MB
- [#4] memory usage: 220.30078 MB
问题1:内存占用远超文件本身大小的原因
从内存记录的峰值(#2到#3阶段)来看,核心原因集中在音频处理的加载与转码环节:
- 压缩音频解码后的体积膨胀
MP3是有损压缩格式,40MB的MP3解码为未压缩PCM数据后,体积会数倍增长。比如44.1kHz、16位立体声的音频,每分钟约占10MB内存,40MB的MP3对应的原始音频时长可能达到几十分钟,解码后内存占用自然会突破几百MB。 - librosa的默认数据格式额外占用内存
librosa.load默认将音频加载为32位浮点型数组(dtype=np.float32),相比原始WAV的16位整数格式,数据量直接翻倍,进一步推高内存占用。 - 中间环节的重复内存加载
- 处理MP3时,pydub会先将整个音频加载到内存完成转码,转码过程中同时存在压缩版和未压缩版的音频数据。
librosa.get_samplerate和librosa.load两次读取文件,底层可能会重复加载部分文件内容。
- 内存回收不及时
代码中仅删除了磁盘文件,但内存中的对象(如audSeg)未显式释放,Python垃圾回收机制的延迟会导致内存占用持续偏高。
问题2:解决方案
一、优化音频处理流程,减少内存占用
用ffmpeg替代pydub转码,避免全量加载
直接调用ffmpeg命令行完成转码与采样率转换,无需将整个音频加载到内存:ffmpeg -i input.mp3 -acodec pcm_s16le -ar {目标采样率} output.wav可以用Python的
subprocess模块执行该命令,转码过程由ffmpeg高效处理,内存占用极低。分块加载处理音频
如果必须使用librosa,改用librosa.stream分块加载音频,避免一次性加载整个文件到内存:import numpy as np # 分块读取并处理 with soundfile.SoundFile(save_location, mode='w', samplerate=target_sr, channels=1) as sf: for y, sr in librosa.stream(src, block_length=2048, hop_length=512): # 处理单块音频数据 processed_block = y.astype(np.int16) sf.write(processed_block)降低音频数据精度
调用librosa.load时指定dtype=np.int16,与原始WAV格式一致,直接减少一半内存占用:audio_data, sr = librosa.load(src, sr=sr//6, dtype=np.int16)强制释放内存
处理完临时对象后显式清空并触发垃圾回收:audSeg = AudioSegment.from_mp3(src) audSeg.export(src, format="wav") audSeg = None # 释放对象引用 import gc gc.collect() # 强制回收内存
二、优化Web服务配置
限制Gunicorn工作进程数
2GB内存服务器建议设置--workers=2,同时开启进程重启机制防止内存泄漏:gunicorn --workers=2 --max-requests=100 --max-requests-jitter=20 app:create_app()调整Nginx参数适配大文件
增大上传文件限制与超时时间,避免因处理超时导致连接中断:http { client_max_body_size 60M; # 适配40MB文件上传 server { proxy_read_timeout 300s; # 延长处理超时时间 proxy_connect_timeout 300s; } }强制上传文件写入磁盘
配置Flask的MAX_CONTENT_LENGTH,确保大文件直接写入磁盘而非内存缓存:def create_app(): app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 60 * 1024 * 1024 # 限制最大上传60MB # ... 其他配置
三、异步化任务处理
将音频转码任务剥离到后台,用Celery+Redis/RabbitMQ实现异步处理:
- 用户上传文件后,返回任务ID,无需等待处理完成
- 后台Worker完成转码后,通过邮件或前端通知用户下载
- 这种方式能让Web进程快速响应请求,避免长时间占用内存,大幅提升并发支持能力
原始代码
import psutil from flask import Flask, render_template, request, send_file import librosa, soundfile import librosa.display from werkzeug.utils import secure_filename from datetime import datetime import os import io from pydub import AudioSegment #memory def memory_usage(message: str = 'debug'): # current process RAM usage p = psutil.Process() rss = p.memory_info().rss / 2 ** 20 # Bytes to MB print(f"[{message}] memory usage: {rss: 10.5f} MB") file = open(f"C:\\Users\\kimkunyu\\Desktop\\mmmmm.txt","a") file.write(f"[{message}] memory usage: {rss: 10.5f} MB") file.close() def create_app(): app = Flask(__name__) @app.route('/') def hello_pybo(): return render_template('main.html') @app.route('/upload', methods=['POST', 'GET']) def upload(): if request.method == 'POST': audio_data = request.files["lc"] memory_usage('#1') if audio_data: filename = secure_filename(audio_data.filename) src = f"pybo/sound/{filename}" audio_data.save(src) stem, fileExtension = os.path.splitext(filename) if fileExtension == '.mp3': audSeg = AudioSegment.from_mp3(src) ## os.remove(src) src = f"pybo/sound/{stem}" + '.wav' audSeg.export(src, format="wav") sr = librosa.get_samplerate(src) memory_usage('#2') audio_data, sr = librosa.load(src, sr=sr // 6) # print(sr) # print(audio_data.shape) # print(audio_data.dtype) # print(len(audio_data)) memory_usage('#3') new_filename = f'{filename.split(".")[0]}_{str(datetime.now())}.wav' new_filename = new_filename.replace(':', "_") os.remove(src) save_location = f'pybo/output/{new_filename}' soundfile.write(save_location, audio_data, sr, format='WAV') memory_usage('#4') return render_template("download.html", save_location=save_location) @app.route('/download', methods=['POST', 'GET']) def download(): save_location = request.form["save_location"] with open(save_location, 'rb') as fo: return_data = io.BytesIO() memory_usage('#5') return_data.write(fo.read()) memory_usage('#6') # (after writing, cursor will be at last byte, so move it to start) return_data.seek(0) os.remove(save_location) return send_file(return_data, as_attachment=True, download_name='converted_file.wav') return app
内容的提问来源于stack exchange,提问作者bbaro

