Overleaf编译链路拆解:从.tex到PDF的6个关键节点
【免费下载链接】overleafA web-based collaborative LaTeX editor项目地址: https://gitcode.com/GitHub_Trending/ov/overleaf
点击Recompile后几秒钟,右侧面板刷新出一页排版好的PDF,左侧LaTeX源码纹丝未动。Overleaf的PDF编译就是这条链路的产物——它不在浏览器里发生,而是跨越了Web服务、CLSI编译服务和Filestore存储服务之间的一连串调用。下面把这条链路从后端到前端逐段拆开。
一次编译经过了谁
参与角色一共六个,调用方向是单向的:浏览器把编译请求发给Web服务,Web服务转给CLSI;CLSI先从Filestore拉取项目文件,再驱动一个临时的TeX Live容器执行编译,产物落在本地输出目录;最后浏览器拿着CLSI返回的URL把PDF拉回前端渲染。
上图从左到右就是一次完整编译的生命周期。
编译器如何被选中 🖥️
CLSI负责什么?一句话:它是编译请求的唯一入口,把"跑一次LaTeX"封装成一个RESTful调用。实现上,它监听三个端口,默认值定义在services/clsi/config/settings.defaults.cjs里:
| 端口 | 用途 |
|---|---|
| TCP/3013 | RESTful编译接口 |
| TCP/3048 | 负载信息上报(供负载均衡器摘除忙碌节点) |
| TCP/3049 | 服务控制接口 |
选哪个编译器由请求体里的compiler字段决定,可选latex、pdflatex、xelatex、lualatex,单次进程超时由timeout指定(默认60秒)。这里有个细节:CLSI用doCompileWithLock做项目级锁,同一项目并发编译会直接返回423而不是排队,避免了重复消耗CPU。把编译从Web进程里拆出来独立成服务,意图很明显——编译是最重的负载,独立部署才能单独扩容。
PDF文件最终存到了哪里 📦
产物不落Filestore,而是留在CLSI自己的输出目录里。这一点容易想反:CLSI编译完后在响应里返回一组outputFiles,每个条目带一个URL(形如/project/<id>/output/output.pdf),由浏览器直接向CLSI拉取。真正和Filestore打交道的是编译前的下行方向——CLSI通过apis.filestore.url(默认http://127.0.0.1:3009)把项目资源下载进编译工作区,并发数由FILESTORE_PARALLEL_FILE_DOWNLOADS控制(默认1)。
// settings.defaults.cjs 中的关键默认值 compileSizeLimit: process.env.COMPILE_SIZE_LIMIT || '7mb', processLifespanLimitMs: parseInt(process.env.PROCESS_LIFE_SPAN_LIMIT_MS) || 60 * 60 * 24 * 1000 * 2, compileConcurrencyLimit: isSpotInstance ? 32 : 64,换句话说,Filestore管"源文件在哪",CLSI管"产物从哪读",两边职责不重叠。Filestore自身支持多种后端,持久化策略由services/filestore/中的PersistorManager按配置选择本地磁盘或对象存储。这种"存储与计算彻底分离"的拆法,让Filestore可以只被Filestore一类服务读写,CLSI崩溃重启不会动到任何用户数据。
沙箱编译为什么用兄弟容器
负责什么:隔离。LaTeX是C程序,恶意或写坏的文档可能越权写文件,所以CLSI默认不开沙箱、而是把SANDBOXED_COMPILES=true交给Server Pro——开启后,每次编译都在一个临时的"兄弟容器"里跑,镜像由TEXLIVE_IMAGE指定(社区版默认quay.io/sharelatex/texlive-full:2017.1)。CLSI容器通过挂载宿主机的/var/run/docker.sock来拉启这些兄弟容器,容器以TEXLIVE_IMAGE_USER(默认tex)用户运行,并叠加seccomp/AppArmor配置文件。沙箱模式下还强制要求设置SANDBOXED_COMPILES_HOST_DIR_COMPILES,否则进程直接退出,防止工作目录悄悄落在意料之外的位置。为什么用容器而不是chroot之类更轻的方案?因为TeX Live体积大、依赖杂,容器镜像天然解决版本一致性,且生命周期跟一次编译绑定,用完即弃。
什么情况会卡住,往哪里调
这套设计有三处需要你动手,都是提前可知的事:
- 大项目编译被拒:请求体超过7mb时body-parser直接拒绝,这是
COMPILE_SIZE_LIMIT的默认值。调整路径:在部署环境变量里调大COMPILE_SIZE_LIMIT,或在fork中改services/clsi/config/settings.defaults.cjs的默认值。 - 编译超时:状态返回
timedout是timeout字段到点杀进程,默认60秒。参考文献宏包反复迭代的长文档,把请求里的timeout调大即可,不用动服务。 - 高并发下503:非抢占实例的并发上限是
compileConcurrencyLimit(spot实例32、常规64),EPIPE错误会翻译成503让上游重试。扩容的正确姿势是加CLSI副本——3048端口的负载上报就是给负载均衡器摘节点的。
所以那几秒钟是怎么花掉的
回看链路:几秒里,Web转发了请求,CLSI下载了项目文件、拉起TeX容器、跑完主文件编译、清理了产物索引。真正的耗时大头永远是容器内那两三次LaTeX进程,服务之间的调用只占零头。想再往下钻,建议直接读services/clsi/app/js/CompileController.js的请求处理流和services/clsi/README.md里的API示例,那里有从请求到响应的完整契约。
【免费下载链接】overleafA web-based collaborative LaTeX editor项目地址: https://gitcode.com/GitHub_Trending/ov/overleaf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考