文章 569
评论 5
浏览 217477
文件预览服务并发瓶颈:LibreOffice 进程卡死?Docker 隔离池+超时强杀+队列调度

文件预览服务并发瓶颈:LibreOffice 进程卡死?Docker 隔离池+超时强杀+队列调度

公司用 LibreOffice 做 Office 文件的在线预览——把 Word/Excel/PPT 转成 PDF,前端用 PDF.js 展示。平时用着没问题,但每个月财务批量导出报表的时候,几十个人同时点预览。LibreOffice 进程 fork 了十几个,其中一个转一个 50MB 的 Excel 文件卡了 10 分钟。这个卡住的进程占着 CPU 不干活,后面排队的请求全堵住了。运维 ssh 上去 kill -9 才恢复。

LibreOffice 的 --headless 模式是为单次使用设计的,不是为高并发设计的。今天聊聊怎么用 Docker 容器隔离每个转换任务、超时强杀、再加一个队列缓冲——让文件预览服务从"一卡俱卡"变成"各管各的"。


问题:共享的 LibreOffice 进程没有隔离

最简单的预览方案是在服务器上直接调 LibreOffice:

soffice --headless --convert-to pdf input.docx

第一次调会 fork 一个 LibreOffice 后台进程。后续调用如果这个进程还在,就复用。如果这个进程卡死了——比如遇到了一个损坏的 docx 文件——后续所有转换请求都被卡住。

更糟的是:被 fork 出来的 LibreOffice 进程是共享同一份用户目录的。一个任务的临时文件可能被另一个任务读到。隔离性为零。


方案:Docker 容器池,每个任务独立容器

每个转换任务在独立的 Docker 容器里跑,转换完就销毁。容器的生命周期就是一次转换请求的生命周期。

public byte[] convertToPdf(byte[] input, String format) throws Exception {
    String containerId = UUID.randomUUID().toString().substring(0, 8);

    // 1. 把输入文件写入临时目录
    Path workDir = Files.createTempDirectory("convert-" + containerId);
    Files.write(workDir.resolve("input." + format), input);

    // 2. 启动 Docker 容器执行转换
    Process docker = new ProcessBuilder(
            "docker", "run", "--rm",
            "--name", "convert-" + containerId,
            "--memory=512m",          // 内存限制
            "--cpus=1",               // CPU 限制
            "-v", workDir + ":/data",
            "libreoffice-convert:latest",
            "soffice", "--headless", "--convert-to", "pdf",
            "/data/input." + format
    ).start();

    // 3. 等待完成,最多 60 秒超时
    boolean finished = docker.waitFor(60, TimeUnit.SECONDS);
    if (!finished) {
        docker.destroyForcibly();  // 超时 → 强杀容器
        // docker kill convert-xxx
        throw new TimeoutException("转换超时");
    }

    // 4. 读取输出
    byte[] pdf = Files.readAllBytes(workDir.resolve("input.pdf"));
    // 5. 清理临时文件
    Files.walk(workDir).sorted(Comparator.reverseOrder())
            .forEach(p -> p.toFile().delete());
    return pdf;
}

关键点:--rm 保证容器退出后自动清理,--memory=512m 限制单个容器的内存防止 OOM,waitFor(60, TimeUnit.SECONDS) 如果超时就 destroyForcibly() 强杀。

一个请求卡了,只影响它自己的容器。其他请求在各自的容器里正常运行。隔离性做到了进程级别(Docker 比进程更彻底)。


队列调度:用信号量控制并发数

Docker 容器也有开销——启动一个容器要几百毫秒。如果 100 个人同时点预览,100 个容器同时启动,Docker daemon 本身也会卡。

用信号量限制同时运行的容器数:

@Component
public class ConversionService {
    // 最多同时运行 5 个转换容器
    private final Semaphore semaphore = new Semaphore(5);

    public byte[] convert(byte[] input, String format) throws Exception {
        boolean acquired = semaphore.tryAcquire(30, TimeUnit.SECONDS);
        if (!acquired) {
            throw new BusyException("预览服务繁忙,请稍后重试");
        }
        try {
            return doConvert(input, format);
        } finally {
            semaphore.release();
        }
    }
}

如果 5 个容器都在跑,第 6 个请求最多等 30 秒。超过了就返回"预览服务繁忙"。这比让用户无限等要好——用户看到"繁忙"会稍后重试,而不会一直盯着 loading 转圈。


容器预热池:消除启动延迟

Docker 容器每次启动都需要几百毫秒,对于频繁的预览请求来说太慢了。可以维护一个"预热容器池"——提前启动 5 个容器,请求来了直接分配:

预热池(5 个容器提前跑好,idle 状态)
  → 请求来了 → 分配容器 → 传文件 → 转换
  → 转换完 → 容器回池(或销毁重建)

预热池把容器启动的几百毫秒延迟平摊到了空闲时间。用户请求的响应时间从 800ms 降到 200ms。


总结

进程级卡死+并发无隔离+无超时→文件预览一卡俱卡。

三条防线:

  • Docker 隔离 —— 每个任务独立容器,--rm 自动清理 + --memory 限制内存 + waitFor 超时强杀
  • 信号量并发控制 —— tryAcquire(30s),超过 5 个并发就排队等待
  • 预热容器池 —— 提前建好容器,消除启动延迟

装配之后,100 个人同时预览,5 个并发跑、剩下的排队 30 秒或提示"繁忙"。卡住的不影响别人,超时的自动强杀。



标题:文件预览服务并发瓶颈:LibreOffice 进程卡死?Docker 隔离池+超时强杀+队列调度
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/07/1783150047365.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消