AWS Lambda容器部署LibreOffice进程线程耗尽问题求助
问题描述
基于public.ecr.aws/lambda/nodejs:16-x86_64镜像的AWS Lambda函数,负责将DOCX文件转换为PDF并上传至S3存储桶。初始运行正常,但经过约200次热调用后,进程和线程数持续增长(每次调用新增约6个),最终达到Lambda的1024进程/线程限制,函数执行失败并报以下错误:
Error: Command failed: libreoffice7.2 -env:UserInstallation=file:///tmp/lo-profile-... --headless --convert-to pdf --outdir /tmp/ /tmp/test1.docx
/usr/bin/libreoffice7.2: fork: retry: Resource temporarily unavailable
/usr/bin/libreoffice7.2: fork: retry: No child processes
javaldx: Could not find a Java Runtime Environment!
Warning: failed to read path from javaldx
fork: Resource temporarily unavailable
terminate called after throwing an instance of 'std::runtime_error'
what(): osl::Thread::create failed
当前执行LibreOffice的代码如下:
import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); const fileId = fileName + FileExtension.DOCX; const binaryPath = 'libreoffice7.2'; const argumentsString = '--headless --invisible --nodefault --view --nolockcheck --nologo --norestore --convert-to pdf --outdir /tmp /tmp/'; const command = `${binaryPath} ${argumentsString}${fileId}`; await execAsync(command);
解决方法
1. 彻底终止LibreOffice关联进程组
exec无法直接追踪LibreOffice启动的所有子进程,改用spawn并终止整个进程组,确保所有关联进程被清理:
import { spawn } from 'child_process'; import { promisify } from 'util'; const waitForEvent = promisify((emitter, event) => new Promise(resolve => emitter.once(event, resolve))); async function convertDocxToPdf(filePath) { const args = [ '--headless', '--invisible', '--nodefault', '--nolockcheck', '--nologo', '--norestore', '--convert-to', 'pdf', '--outdir', '/tmp', filePath ]; // 启用detached模式,方便终止整个进程组 const child = spawn('libreoffice7.2', args, { detached: true }); try { await Promise.all([ waitForEvent(child.stdout, 'end'), waitForEvent(child.stderr, 'end'), ]); if (child.exitCode !== 0) { throw new Error(`LibreOffice转换失败,退出码:${child.exitCode}`); } } finally { // 终止整个进程组,清理所有关联子进程 try { process.kill(-child.pid, 'SIGTERM'); } catch (err) { // 忽略进程已终止的错误 if (err.code !== 'ESRCH') throw err; } } }
2. 使用独立的临时配置目录
每次调用生成唯一的用户配置目录,避免进程间共享状态导致泄漏,转换完成后删除该目录:
import { spawn } from 'child_process'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; import fs from 'fs/promises'; const waitForEvent = promisify((emitter, event) => new Promise(resolve => emitter.once(event, resolve))); async function convertDocxToPdf(filePath) { // 生成唯一配置目录 const profileDir = `/tmp/lo-profile-${randomUUID()}`; const args = [ `-env:UserInstallation=file://${profileDir}`, '--headless', '--invisible', '--nodefault', '--nolockcheck', '--nologo', '--norestore', '--convert-to', 'pdf', '--outdir', '/tmp', filePath ]; const child = spawn('libreoffice7.2', args, { detached: true }); try { await Promise.all([ waitForEvent(child.stdout, 'end'), waitForEvent(child.stderr, 'end'), ]); if (child.exitCode !== 0) { throw new Error(`LibreOffice转换失败,退出码:${child.exitCode}`); } } finally { // 终止进程组 try { process.kill(-child.pid, 'SIGTERM'); } catch (err) { if (err.code !== 'ESRCH') throw err; } // 删除临时配置目录 await fs.rm(profileDir, { recursive: true, force: true }); } }
3. 禁用Java依赖
错误中提示找不到JRE,DOCX转PDF无需Java支持,可通过以下方式禁用相关逻辑:
- 在启动参数中添加:
--nocrashreport --nofirststartwizard - 设置环境变量:
process.env.JAVA_HOME = '';
4. 辅助优化
- 降低Lambda函数的并发数,减少热容器的复用压力
- 在Docker镜像构建时预安装轻量版本的LibreOffice,避免冗余组件
内容的提问来源于stack exchange,提问作者emblaszkiewicz

