ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

kkFileView Docker部署与附件在线预览实践

kkFileView Docker部署与附件在线预览实践

1. 先想清楚:这套预览服务到底解决哪一段问题

业务方想要的效果往往很简单:用户在浏览器里点一下附件,文件就能直接打开查看,不用先下载、不用装任何客户端、手机上看也不变形。真正落地的时候,这个朴素需求会牵扯出一大堆细节,格式兼容、字体、并发、缓存、安全、授权链接的时效性,每一项都能让人返工。我这几年做过的几个后台系统里,附件预览都是同一个套路:用 Docker 部署 kkFileView,把 Office、PDF、图片、压缩包、音视频等格式的判断和渲染统一交给它,业务系统只负责拼一条预览链接丢给前端 iframe。这篇就按我实际落地的顺序,把部署、配置、对接到排障完整过一遍,从没碰过这个工具的人照着做能跑通,已经跑通的人也能从里面捡到几个少踩坑的细节。

kkFileView 是国产开源项目,Java 写的,本质是一个"格式转译中间层":它接收一个文件 URL,根据扩展名决定走哪条渲染链路——Office 系列交给内置的 LibreOffice 转成图片或者 PDF,PDF 走 pdf.js 之类的渲染,文本、代码、图片直接展示,压缩包列目录,音视频用播放器。对业务系统来说,它就是一组 HTTP 接口,最核心的那个叫onlinePreview。用 Docker 部署的好处是,它依赖的 LibreOffice、字体、Java 运行时全被打进镜像了,本机上不用折腾那套"装了 LibreOffice 却发现中文字体全变方框"的经典事故。

1.1 业务侧的真实诉求长什么样

先把需求拆开看,因为不同诉求会直接决定你选哪种方案。第一类是"内部系统附件预览",用户在工单、合同、报销单里上传的文件,需要同部门的人点开看一眼,这类场景对样式还原度要求中等,对响应速度要求高,因为附件的平均大小往往只有几百 KB。第二类是"对外文档展示",比如产品手册、报告、公示材料,这类对排版还原和移动端适配要求极高,一个表格错位就可能被投诉。第三类是"批量浏览与检索",比如档案系统要在列表里给每一行都提供缩略图或首屏预览,这种场景的性能压力跟单文件预览完全不是一个量级。

这三种诉求对应到 kkFileView 上的配置策略是不一样的。第一类适合 Office 转图片、低分辨率、强缓存;第二类适合转 PDF 加高清渲染,甚至考虑前端直接嵌 PDF;第三类必须做缓存预热和并发控制,否则用户一翻页,后端 LibreOffice 进程就被打满。我在项目启动阶段一般会先问清楚"平均文件大小、峰值并发、是否对外、是否要求水印"这四个问题,答案基本就决定了后面的参数表。

1.2 三条技术路线的取舍

在选 kkFileView 之前,业内常见的做法无非三条,我把它们的实际体验列成表格,方便你对照自己的场景判断。

方案代表做法优点实际踩到的坑
纯前端渲染前端用各类 JS 库直接解析 docx、xlsx、pdf不占服务端资源,部署简单复杂排版还原差,老版 doc、ppt 基本无解,字体依赖客户端
服务端转译kkFileView、Office 转 PDF 服务、自建 LibreOffice 调用格式覆盖广,样式还原稳定,客户端零负担吃内存,转换有冷启动开销,需要缓存和队列
客户端/插件要求用户装阅读器或用本地 Office 打开还原度最高移动端和外部用户直接不可用,运维成本高

绝大多数 To B 后台最后都会落到第二条路上,原因很现实:你没法要求外部用户装东西,也没法保证他们的浏览器能解析十年前的老 doc。而在"服务端转译"这一类里,自己拿 LibreOffice 裸调是最容易翻车的——进程管理、超时、僵尸进程、并发隔离,每一样都要写不少代码。kkFileView 把这些脏活包了一层,虽然它自己也有坑,但起点高得多。

1.3 能力清单和边界

我得把边界说清楚,免得你上线后被业务方追问。kkFileView 的强项是办公文档和常见文本格式,Office 三件套、PDF、OFD、图片、纯文本、代码文件、压缩包目录、音视频,覆盖得相当全。它的弱项主要在两块:一是高度复杂的排版,比如带复杂公式的 docx、多层嵌套图表、特殊艺术字,转出来可能有细微偏差,这是 LibreOffice 渲染引擎本身的限制,不是配置能解决的;二是交互式内容,宏、表单控件、动画,转成图片或 PDF 之后就只剩静态画面了。

还有一个容易被忽略的点:它做的是"预览",不是"编辑"。有些业务方会问能不能在页面上改两笔再保存,答案是另请高明,这类需求要走在线编辑方案,跟预览是两条完全不同的技术栈。我一般会在需求确认阶段就把这句话说死,省得后期扯皮。

2. 部署方案设计:单容器还是拆开

确定要用之后,下一步是设计部署形态。kkFileView 官方镜像是一个"全家桶",Java 应用加 LibreOffice 加转换进程管理全在一个容器里,这是它最省事也最推荐的用法。我见过有人想把它拆成"应用容器 + 独立的 Office 转换服务",理论上可行,因为它的转换层本质是 JODConverter 去连本地或远程的 Office 进程端口,但拆开之后版本匹配、字体同步、连接池管理全要自己维护,收益很小。除非你有非常明确的隔离诉求,否则别拆。

2.1 镜像里到底装了什么

理解镜像内容对排障极其重要。这个镜像大致包含四层东西:Java 运行时和应用 jar 包、内置的 LibreOffice(负责 Office 转换)、应用配置文件目录、以及启动脚本。启动脚本会先把 Office 的监听进程拉起来,再启动 Java 应用,所以你在docker logs里会先看到 Office 相关的输出,然后才是 Spring Boot 的启动横幅。如果日志里只有 Java 启动而没有 Office 进程,那 Office 转换必然是失败的,这是排查"Office 文件转不出来"的第一现场。

配置文件在容器内的固定路径下,通常形如/opt/kkFileView-x.x.x/config/application.properties。这个路径里的版本号会随镜像版本变化,所以别硬编码记,第一次跑起来之后docker exec进去看一眼目录结构最稳妥。与之相邻的还有日志目录,后面排障全靠它。

2.2 资源估算:别拿 1C1G 的机器硬上

这是新手最常低估的地方。Java 应用本身占 300 到 500 MB,LibreOffice 每个转换进程在打开一个中等复杂度文档时会瞬时吃到 200 到 400 MB,官方镜像默认会起两个 Office 监听端口做并行转换。所以一个能稳定跑起来的容器,内存基线我给到 2 GB,推荐 4 GB;CPU 两核起步,因为文档转换是 CPU 密集型操作,单核机器上一个 30 页的 ppt 转图能让你等半分钟。

磁盘也要留够。转换产物默认会落盘做缓存,一个 20 页的 docx 转成图片可能是几十 MB,缓存保留期如果设成一个月,日活几百的系统轻松堆到几十 GB。我一般给缓存目录单独挂一块盘或者单独一个卷,配合定时清理策略,把磁盘占用的上限锁死。

2.3 目录与挂载规划

我习惯在宿主机上建一个统一目录,把所有需要持久化的东西都放在里面,容器随时可以删了重建。典型结构是这样:

/data/kkfileview/ ├── config/application.properties # 自定义配置 ├── cache/ # 转换产物缓存 ├── logs/ # 应用日志 └── fonts/ # 追加字体(可选)

这里有个必须提醒的坑:只挂载单个配置文件,不要挂载整个 config 目录。因为镜像里的 config 目录除了主配置,可能还有日志配置、模板文件等一堆东西,整目录挂载会用一个空目录把它们全遮住,启动直接报错,而且报错信息往往指向一个跟配置毫无关系的地方,非常迷惑。我第一次踩这个坑的时候,对着"找不到某个模板"的报错查了快两个小时。

2.4 网络与端口

对外只需要暴露应用端口,默认 8012。容器内部还有 Office 转换进程监听的端口(一般是 2001、2002 这类),这些不需要也不应该映射到宿主机,它们只在容器内部通信。我见过把内部端口也映射出去的做法,除了增加攻击面没有任何好处。

如果预览服务要接在业务域名下面,建议走统一网关或者 Nginx 反代,让业务系统和预览服务同源,这样前端 iframe 嵌入时不会遇到跨域和 X-Frame-Options 的麻烦。具体配置我在第 5 节展开。

3. 上手部署:从拉到跑通第一条预览链接

前面铺垫够了,直接开干。我下面用的版本是 4.4.0,不同小版本的配置键名会有出入,凡是涉及具体键名的地方,都以你镜像里那份application.properties的实际内容为准,这一点务必记住。

3.1 拉镜像与首次启动

先拉镜像,然后跑一个最简容器,什么配置都不改,目的只有一个:确认这套东西在你机器上能起来。

docker pull keking/kkfileview:4.4.0 docker run -d \ --name kkfileview \ -p 8012:8012 \ --restart unless-stopped \ keking/kkfileview:4.4.0

起来之后看日志:

docker logs -f --tail 200 kkfileview

等到日志里出现应用启动完成的字样,浏览器打开http://服务器IP:8012/,会看到一个自带的测试页面,可以直接上传一个 docx 或 pdf 试试。这个页面是官方提供的最快验证手段,能省掉你自己拼链接的时间。如果这里就能正常预览,说明基础链路(Java、LibreOffice、字体、渲染)全通了,剩下的都是配置和对接问题。

3.2 Compose 版本:我更推荐这个

命令行跑一次没问题,但生产环境我更愿意用 compose 管起来,配置和挂载都写在文件里,换机器时复制一份就能重建。这是我实际在用的模板:

services: kkfileview: image: keking/kkfileview:4.4.0 container_name: kkfileview restart: unless-stopped ports: - "8012:8012" volumes: - ./config/application.properties:/opt/kkFileView-4.4.0/config/application.properties - ./cache:/opt/kkFileView-4.4.0/file - ./logs:/opt/kkFileView-4.4.0/log environment: - TZ=Asia/Shanghai - LANG=zh_CN.UTF-8 deploy: resources: limits: memory: 4g healthcheck: test: ["CMD-SHELL", "curl -fs http://localhost:8012/ > /dev/null || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 120s

几个细节值得说。TZ和LANG一定要设,不设的话日志时间对不上,中文文件名在极端情况下也可能出问题。start_period给到 120 秒,因为 LibreOffice 拉起和 Java 初始化都需要时间,健康检查设得太急会导致容器反复被判定不健康。内存限制我按 4G 设,这既是保护容器,也是保护宿主机——万一某个文档把 LibreOffice 的内存吃爆,至少不会把整台机器拖死。

挂载缓存目录那行,路径要跟你容器内实际情况对齐。如果你不确定,先跑 3.1 那个裸容器,docker exec -it kkfileview ls /opt看一眼真实目录名,再回来改,别照抄。

3.3 验证:三类文件各测一遍

只测一个 docx 是不够的。我上线前的验证清单固定是这个顺序:

  1. 一个简单 docx,验证 Office 转译链路。
  2. 一个带中文、带表格的 xlsx,验证字体和排版。
  3. 一个几十页的 pdf,验证大文件和分页渲染。
  4. 一个文件名带空格和中文的图片,验证 URL 编码。

第 4 条特别关键,因为预览 URL 里的文件地址要经过编码,空格、+、&、中文这些字符处理不当就会 404 或者预览空白。这个坑我在第 5 节会专门给代码。

3.4 关于"配置不生效"这件事

改完配置要重启容器,这一点大家都知道。但有个更隐蔽的问题:配置文件挂载成功了,内容也改了,重启后却没生效。原因通常是格式问题——properties 文件里键值之间的等号两侧别随便加空格,不要用 Tab,注释用#,中文值在某些版本里需要转义或者干脆避免。另一个常见原因是有些配置项在启动时被硬编码覆盖了,这种情况你改配置没用,得通过环境变量或者换镜像版本解决。所以我的习惯是改完配置,先docker exec进去cat一遍,确认容器里看到的就是我改的那份,再重启验证。

4. 配置项拆解:把默认值改成能上生产的值

默认配置是"能跑起来"级别的,直接上生产会出问题。这一节挑我必改的几组配置说,每组都说清楚为什么改、改成什么、改了会带来什么副作用。

4.1 缓存与清理:不配这个迟早磁盘满

默认会缓存转换结果,这是好事,第二次打开同一个文件几乎秒开。但默认的缓存保留期往往偏长,日积月累就是磁盘告警。我一般把保留期设成 7 到 15 天,具体看业务:如果同一批文件会被反复查看,就设长一点;如果是"上传就看一次"的场景,三天足够。

同时必须打开定时清理。清理任务建议放在凌晨低峰期,Cron 表达式类似0 0 3 * * ?,每天三点跑一次。这里有个实测经验:清理任务跑的时候会扫目录,如果缓存目录里文件数量到了百万级,扫描本身就会拖慢磁盘 IO,所以缓存目录一定要和其他服务分开,别把日志、数据库文件放在同一块盘上。

注意:清缓存只能治标。如果你的业务存在大量"一次性文件"(预览完就再也不会看),更彻底的做法是业务侧在上传时就生成预览并把结果保存下来,或者干脆用对象存储的生命周期策略去删源文件,从源头减少缓存量。

4.2 Office 转图还是转 PDF

这是影响体验最大的一个开关。转图片的意思是每页渲染成一张图,前端翻页看;转 PDF 是整篇合成一个 PDF,交给 PDF 渲染器展示。

转图片的优势是移动端体验稳定,缩放、双指放大都是原生图片行为,不容易出兼容问题;缺点是页数多的时候请求数多,且文字不可选中、不可搜索。转 PDF 的优势是文件小、可搜索、可复制文字,适合文档类内容;缺点是对渲染器依赖更强,超大 PDF 在低端手机上可能卡顿。

我的选择逻辑是:内部系统、附件为主、移动端访问多,选转图片;对外文档、报告类、需要复制文字,选 PDF。如果业务两种都有,可以按文件类型分流,或者干脆两个都试一遍让业务方拍板——这个决定权最好别自己扛。

4.3 字体和水印:中文乱码的根因

中文显示成方框,几乎是每个用 LibreOffice 做转换的人都遇到过的。原因是容器里没装中文字体,LibreOffice 找不到字形就画方块。镜像一般会带一部分字体,但不会覆盖所有中文字体,尤其是某些公文常用的宋体、仿宋、楷体。

解决办法是把宿主机上合法的字体文件复制进容器的字体目录,然后重建字体缓存、重启容器。我这边的做法是准备一个字体目录,把需要的字体文件放进去,用挂载的方式映射到容器内对应的字体路径,再执行一次字体缓存刷新。这里必须提醒一句:字体是有版权的,商用字体要确认授权,别为了排版好看随手把一堆来源不明的字体扔进生产环境,这是实打实的法律风险。

水印是另一个常被要求的配置。默认无水印。一旦打开,预览页面会铺上一层文字,用于内部资料防外传。水印的内容、字号、颜色、透明度、倾斜角度一般都能配。我的经验是透明度别调太高,否则会盖住正文影响阅读;也别太低,太低截图之后看不见,等于没加。

4.4 并发、超时和 JVM 参数

并发这块要理解它的模型:转换是串行排队到 Office 进程上去执行的,进程数决定了并行度。默认起两个进程,意味着同一时刻最多两个文档在转换,第三个请求要么排队要么失败,取决于配置。小团队够用,日活上千的系统建议加到四个,但每加一个进程,内存基线就要多留 300 到 500 MB,这笔账要算清楚。

超时时间建议按业务的最大文档来定。默认值对付普通文档没问题,但一个 50 页带大量图片的 ppt,转换可能要二三十秒,超时设短了就会看到"预览失败"但日志里其实转换还在跑,用户重试一次,又起一个转换任务,进程越堆越多。这个问题我在第 6 节会讲怎么善后。

JVM 参数一般不用大动,但如果你的容器内存限制设得比较紧,建议显式指定堆大小,比如限制 2G 内存的容器给 JVM 分 1G 堆,剩下的留给 LibreOffice 和系统缓存。不指定的话 JVM 可能按宿主机总内存去推算,容器一超限就被 OOM 干掉,日志里连个像样的错误都没有,非常难查。

4.5 安全项:这几个开关必须拧紧

预览服务本质上是"你给它一个 URL,它去下载并解析",这在安全上天然是个 SSRF 风险点。如果你的服务部署在内网,攻击者构造一个指向内网管理后台或者元数据地址的 url 参数,就能借你的服务去探测内网。所以:

  • 用域名或主机白名单限制可预览的文件来源,只允许业务系统的文件服务域名,其他一律拒绝。较新版本提供了类似trust.host的白名单配置,如果你的版本没有这个键,那就在 Nginx 或者网关层做来源校验,绝不能裸奔。
  • 用扩展名黑名单挡掉危险类型。html、htm、svg 这类能执行脚本的格式,默认就不该让它在预览域名下渲染,否则等于开了个 XSS 通道。
  • 预览服务不要和后台管理系统同域。最稳妥的是放在独立子域,即便出问题也不会牵连主站 Cookie。
  • 对外网开放时,在网关层加签名鉴权,让 url 参数带时效签名,避免别人拿你的服务当免费的文档转译接口刷。

注意:白名单配置改完一定要用内网地址实测一次,确认真的被拦住了。我见过配了白名单但因为格式写错(比如协议头大小写、带端口没带端口)导致规则没生效的情况,配置看着对,实际没拦住。

5. 反向代理与业务系统对接

服务跑通、配置调好,接下来是把链接拼出来给业务用。这一步的坑比部署还多,因为涉及编码、域名、跨域。

5.1 Nginx 反代配置

我通常让预览服务挂在一个独立子域下,Nginx 做反代。核心配置大概是这个样子:

server { listen 443 ssl; server_name preview.example.internal; client_max_body_size 200m; location / { proxy_pass http://127.0.0.1:8012; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_send_timeout 300s; proxy_read_timeout 300s; proxy_buffering off; } }

两个参数值得单独说明。proxy_read_timeout必须放大,默认 60 秒,大文档转换超过这个时间 Nginx 会先断开,用户看到的是 504,但后端其实还在转,白白浪费资源。proxy_buffering off是为了让流式返回的响应不被缓冲,对于 PDF 这种边生成边返回的内容,关掉缓冲体验更顺。

如果你要把预览服务放在业务域名的子路径下(比如/preview/),那就要同时改应用上下文路径和 Nginx 的转发规则,两边必须一致,否则静态资源路径会错。这一块最容易出问题,我的建议是能用独立子域就别用子路径,省一半的事。

5.2 URL 拼装:编码错一个字就 404

这是对接环节最高频的故障点。预览接口的文件地址参数需要做编码处理,具体规则各版本略有差异,但通用的做法是:先对文件完整 URL 做一次 Base64,再做一次 URL 编码。

Java 版本:

import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; public String buildPreviewUrl(String fileUrl, String previewHost) throws Exception { String base64 = Base64.getEncoder() .encodeToString(fileUrl.getBytes(StandardCharsets.UTF_8)); // URLEncoder 会把空格编成 +,必须替换回 %20 String encoded = URLEncoder.encode(base64, "UTF-8").replace("+", "%20"); return previewHost + "/onlinePreview?url=" + encoded; }

前端 JavaScript 版本:

function buildPreviewUrl(fileUrl, previewHost) { // 注意:要正确处理中文,先把字符串转成 UTF-8 字节 const bytes = new TextEncoder().encode(fileUrl); let binary = ''; bytes.forEach(b => binary += String.fromCharCode(b)); const base64 = btoa(binary); return previewHost + '/onlinePreview?url=' + encodeURIComponent(base64); }

Python 版本:

import base64 from urllib.parse import quote def build_preview_url(file_url: str, preview_host: str) -> str: b64 = base64.b64encode(file_url.encode('utf-8')).decode('ascii') return f"{preview_host}/onlinePreview?url={quote(b64, safe='')}"

三个版本都要注意同一件事:Base64 之后再做 URL 编码,顺序不能反。反过来做,Base64 里的+和/会被当成特殊字符,服务端解出来就是乱码,表现就是"预览一片空白"或者"文件不存在"。另外前端用btoa时如果直接传中文字符串会抛异常,必须先转字节,这是很多人卡住的地方。

还有一个参数值得配上:如果文件 URL 里没有扩展名(比如对象存储的 key 是一串哈希),kkFileView 就判断不出格式,需要额外传一个文件名参数,让它知道该走哪条渲染链路。这个参数在对接对象存储时几乎必用。

5.3 嵌入方式与移动端

前端嵌入最省事的是 iframe,给一个固定宽高就行。但有两个细节要注意:一是预览服务返回的响应头里如果有X-Frame-Options: DENY,iframe 会直接白屏,需要在 Nginx 里放开或者干脆不设;二是移动端 iframe 的高度要用视口单位自适应,写死像素高度在小屏上会只露半页。

如果业务要求"点开全屏看",更稳的做法是新开一个页面直达预览链接,而不是在弹窗里塞 iframe,尤其是 iOS 上,弹窗内的 iframe 滚动行为经常很别扭。这个取舍我一般是两个都做,列表页给缩略预览,详情页给全屏跳转。

6. 排障实录:那些日志里不会直接告诉你的答案

这一节是我这几年攒下来的问题清单,按"现象"查最快。

6.1 常见问题速查表

现象大概率原因处理方向
页面空白,控制台无报错url 参数编码错误,Base64 与 URL 编码顺序反了用工具手动解一次,看能否还原出正确地址
提示文件不存在预览服务访问不到文件地址,网络不通或无权限进容器 curl 一下文件 URL
中文变方框容器缺中文字体挂载字体并刷新缓存后重启
Office 转不出来,其他格式正常容器内 Office 进程没起来或已崩溃看日志开头的 Office 启动输出
大文件转半天然后失败超时时间太短调大反代和应用超时
偶发 502转换把内存打满,容器被 OOM看 docker inspect 的重启次数和退出码
同一个文件第二次打开还是很慢缓存没生效或缓存目录没持久化确认缓存目录挂载正确、有写权限
部分文件预览样式错乱字体缺失或 LibreOffice 版本渲染差异补字体,极端情况下换镜像版本试

6.2 中文变方框的完整处理路径

这个问题的处理流程我整理成了固定动作。第一步,进容器执行字体列表命令,看有没有中文字体,没有就说明是缺字体。第二步,把字体文件放进容器的字体目录,注意权限要可读。第三步,刷新字体缓存,这一步很多人会漏,不刷新的话新放进去的字体不生效。第四步,重启容器,因为 LibreOffice 进程在启动时加载字体,不重启看不到变化。第五步,拿一个字体最全的 docx 复测。

还有一个隐蔽情况:字体装了,但文档里用的是某个特定字重的变体(比如细体、粗体),而容器里只有常规体,LibreOffice 会用常规体替代,看起来"能显示但不像原稿"。这种情况只能把对应字重也补上,或者接受替代效果。

6.3 转换卡死和进程堆积

这是最恶心的一类问题。表现是:某几个文件一直转不出来,CPU 占用高但不下降,重启容器后短暂恢复,过一阵又出现。根因通常是某个畸形文档让 LibreOffice 进程卡在了死循环或者弹窗等待状态(比如文档里有需要确认的宏提示)。

排查手段是进容器看进程列表,数一下 Office 进程有几个,正常情况下应该等于配置的进程数,多出来的就是僵尸。短期处理是重启容器,长期处理有三个方向:一是给转换加硬超时,超时直接杀进程重建;二是把这类问题文件单独识别出来,在业务侧拒绝上传或者标记为"不支持预览";三是升级镜像版本,LibreOffice 的健壮性在各版本之间差异挺大,我遇到过某个版本对特定老 doc 处理有 bug,换个版本就好了。

6.4 大文件和并发打满

一个 100 MB 的 ppt 能让整个服务卡住十几秒,这期间其他请求全在排队。缓解办法有几个层次:业务侧限制上传大小和页数,这是最有效的;预览侧提高并发进程数,但受内存限制;再加一层就是给预览请求做队列和限流,超过阈值直接返回"正在处理,请稍后",而不是让用户干等然后超时。

我实际做的组合是:业务侧限制单个附件 50 MB 以内、页数 200 页以内;预览侧起三个转换进程;网关层对预览接口做并发限流。这套组合下来,日常使用基本感知不到排队。

6.5 日志在哪看,看什么

应用日志和转换日志是分开的。应用日志告诉你请求进来了、参数是什么、走了哪条链路;转换日志告诉你 LibreOffice 那边发生了什么。排障顺序应该是先看应用日志确认参数正确,再看转换日志确认渲染阶段有没有异常。我见过不少情况是参数就错了,却一直在转换日志里找原因,方向完全跑偏。

日志级别默认是信息级,排查时可以临时调到调试级,能看到更细的中间过程,但记得事后调回来,调试日志量很大,磁盘吃不消。

7. 上线之后:从"能用"到"扛得住"

跑通只是开始,真正考验人的是上线之后。这一节说几个我在长期运行中总结出来的东西。

7.1 缓存命中率比什么都重要

预览服务的性能瓶颈几乎全在"第一次转换"。如果你的业务里同一个文件会被多人反复查看(这在工单、审批类系统里非常普遍),那缓存命中率就是核心指标。观察方法很简单,看日志里同一个文件短时间内有没有重复触发转换。

提高命中率的做法:确保缓存目录持久化且容量足够;缓存保留期按业务访问规律设定,别设太短;如果业务侧能在文件上传时就触发一次"预转换"(比如上传成功后异步请求一次预览接口),用户第一次点开就是秒开,体验提升非常明显。这个预热动作成本很低,收益很高,我基本每个项目都会加。

7.2 多副本部署的坑

单机扛不住要扩副本的时候,有个问题必须先想清楚:缓存是本地磁盘的。两个副本各自缓存各自的,用户第一次请求打到 A,第二次打到 B,还是要重新转一遍,缓存等于白做。

解决办法有两条:一是让缓存落到共享存储上,但共享存储的 IO 延迟会让转换变慢,需要实测;二是用支持集中式缓存的配置(较新版本提供了对接 Redis 一类的选项,具体以你镜像支持的配置为准),或者干脆在网关层做会话粘性,让同一个文件地址的请求固定落到同一个副本。我倾向于后者,实现简单,副作用小。

另外,多副本时转换进程数要按副本数重新算内存账,别一个副本 4G 内存 × 4 个副本把宿主机撑爆。

7.3 该监控哪些指标

不用搞得很复杂,四个指标够用:容器内存使用率(防止 OOM)、转换平均耗时(感知性能退化)、预览请求错误率(感知故障)、缓存目录占用(防止磁盘满)。前三个用常规的容器监控就能拿到,第四个可以写个简单的定时脚本上报。

我还会额外盯一个"Office 进程数量",超过预期值就告警,这能提前发现进程堆积,比等到用户投诉要早得多。

7.4 一个容易被忽略的稳定性细节

容器重启策略一定要设成unless-stopped,而不是always。区别在于,你手动停机做维护的时候,always会跟你对着干,把容器又拉起来;unless-stopped则尊重你的手动操作。这个细节在半夜做紧急维护的时候特别重要,能省不少骂。

另外一个实践是给容器设日志轮转,Docker 默认的 json-file 日志驱动不做限制的话,一个日志文件能涨到几十 GB,直接把磁盘吃满,而且这种故障表现得非常诡异——服务本身好好的,但突然所有写入都失败。在 compose 里加上日志大小和文件数限制,两行配置的事。

最后分享一个我在实际项目里反复验证过的小技巧:把预览服务和文件存储放在同一个内网网段。原因很实际,预览服务每次转换都要去拉一遍源文件,如果文件存储在外网或者跨机房,光下载就占掉大半时间,而且大文件下载超时会导致转换直接失败。同网段之后,这部分延迟基本可以忽略,实测下来转换耗时的波动小了很多。至于怎么让业务侧的文件地址在内网可达、外网又不可达,那属于网络规划的事,提前跟运维对一次比事后改架构省事得多。

踩过几次坑之后我的感受是,这类"看起来只是部署一个开源工具"的活儿,真正花时间的从来不是部署本身,而是编码细节、资源估算和故障兜底。工具帮你把最脏的那部分封装好了,剩下的判断题还得自己做。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进