乐于分享
好东西不私藏

OnlyOffice 核心架构解析:一个文档服务器是如何运转的

OnlyOffice 核心架构解析:一个文档服务器是如何运转的

1. 整体架构鸟瞰

OnlyOffice Docs 的架构设计遵循一个核心原则:文档服务器本身无状态地处理文档逻辑,文件存储与用户管理完全交由外部系统负责。

这一设计使 OnlyOffice 能够以"插件"的方式嵌入几乎任何现有系统,而不要求对外部系统做侵入性改造。

在一次典型的集成部署中,涉及三个角色:

+--------------------------+        +--------------------------+
|                          |        |                          |
|   你的应用(集成方)      |        |   文件存储服务            |
|                          |        |   (你的系统负责)        |
|  - 用户认证              |        |                          |
|  - 文件管理 UI           |        |  可以是:                 |
|  - 权限控制              |        |  - 本地磁盘               |
|  - 生成编辑器配置        |        |  - S3 / OSS              |
|                          |        |  - Nextcloud             |
+-----------+--------------+        +------------+-------------+
            |                                    |
            |  1. 页面中嵌入编辑器 JS             |  3. 回调保存 / 下载文件
            |  2. 传入文档 URL + 配置             |
            v                                    |
+-----------+------------------------------------+-------------+
|                                                              |
|               ONLYOFFICE Document Server                     |
|                                                              |
|   Nginx  →  Node.js 服务  →  协同服务  →  转换服务           |
|                                                              |
+--------------------------------------------------------------+

这个三角关系贯穿了 OnlyOffice 的所有集成场景,理解它是读懂后续所有内容的前提。


2. Document Server 的组成模块

Document Server 并不是一个单一进程,而是由多个相互协作的服务组成。以 Docker 部署为例,一个完整的 Document Server 实例包含以下核心组件:

2.1 Nginx(反向代理与静态资源服务)

Nginx 是整个服务的入口,承担两类工作:

  • 将 HTTP/HTTPS 请求路由到对应的后端服务
  • 直接提供编辑器前端静态资源(JS、CSS、字体、图标等)的服务

编辑器的前端代码体积不小(压缩后约 10–20 MB),Nginx 对其做了 gzip 压缩与缓存策略优化。在生产部署中,也可以在 Nginx 前再加一层 CDN 来加速静态资源的分发。

2.2 Node.js 文档服务(docservice)

这是整个架构的核心,基于 Node.js 实现,主要职责包括:

  • 接收并验证来自集成方的文档打开请求(JWT 校验)
  • 管理文档的编辑会话(session)
  • 通过 WebSocket 与浏览器端编辑器保持长连接
  • 将用户操作转发给协同服务进行合并处理
  • 在文档关闭后触发保存回调

Node.js 的选择不是偶然的:WebSocket 的长连接管理、高并发 I/O 处理,恰好是 Node.js 的强项。

2.3 转换服务(converter)

转换服务负责文件格式的相互转换,是一个独立运行的进程。它处理两类工作:

  • 打开时转换:将非 OOXML 格式(如 .odt、.ods、.odp、.txt、.csv)转换为编辑器可处理的内部格式
  • 导出时转换:将编辑完成的文档转换为目标格式(如导出为 PDF)

转换服务基于原生代码实现(C++ 核心),能够以接近原生 Office 的精度处理格式转换。这个服务也通过 REST API 对外暴露,集成方可以独立调用文件转换功能,无需打开编辑器。

2.4 拼写检查服务(spellchecker)

独立运行的多语言拼写检查服务,基于 Hunspell 引擎。编辑器中的实时拼写检查(红色下划线)依赖此服务。支持 80 余种语言的词典,词典文件可以扩展。

2.5 RabbitMQ(消息队列)

各服务之间通过 RabbitMQ 进行异步消息传递,主要用于:

  • 协同操作的消息分发(将一个用户的操作广播给同一文档的其他用户)
  • 转换任务的队列管理(避免大文件转换阻塞主流程)
  • 服务间解耦(某一服务重启不影响整体)

在高并发场景下,RabbitMQ 是 Document Server 水平扩展能力的关键支撑。

2.6 Redis(缓存与会话存储)

Redis 在架构中承担两个角色:

  • 会话缓存:保存当前活跃文档的编辑状态、在线用户列表、操作历史
  • 分布式锁:在多实例部署时,保证同一文档同时只有一个协同服务在处理操作合并

2.7 PostgreSQL(持久化存储)

Document Server 使用 PostgreSQL 存储:

  • 文档的操作历史记录(用于版本恢复)
  • 文档缓存元数据
  • 系统配置信息

需要注意的是,PostgreSQL 存储的不是文档文件本身,文档文件始终由集成方的存储系统负责。PostgreSQL 里存的是"这个文档被编辑过哪些操作、当前编辑状态是什么"这类元数据。

组件关系总览

浏览器
  |
  |  HTTP (静态资源)
  |  WebSocket (实时操作)
  v
Nginx
  |
  +---> Node.js 文档服务 (docservice)
  |          |
  |          +---> RabbitMQ ---> 协同服务
  |          |
  |          +---> Redis (会话/锁)
  |          |
  |          +---> PostgreSQL (历史/元数据)
  |
  +---> 转换服务 (converter)
  |
  +---> 拼写检查服务 (spellchecker)

3. 文档生命周期:从打开到保存

以用户在浏览器中打开一个 .docx 文件为例,完整的生命周期如下:

阶段一:初始化(打开文档)

步骤 1:集成方生成编辑器配置

用户点击"在线编辑"后,集成方的后端生成一份 JSON 格式的编辑器配置,核心内容包括:

{
  "document": {
    "fileType": "docx",
    "key": "唯一文档标识符(决定是否复用缓存)",
    "title": "合同草稿.docx",
    "url": "https://your-storage.com/files/contract.docx"
  },
  "editorConfig": {
    "callbackUrl": "https://your-app.com/onlyoffice/callback",
    "user": {
      "id": "user-001",
      "name": "张三"
    },
    "mode": "edit"
  }
}

这份配置会被签入 JWT Token 后传给浏览器页面。

步骤 2:浏览器加载编辑器

浏览器加载 Document Server 提供的 api.js,该脚本初始化编辑器,向 Document Server 发送文档打开请求。

步骤 3:Document Server 获取文件

Document Server 收到请求后,通过配置中的 document.url 主动从集成方的存储服务下载原始文档文件。这是一个重要的设计细节:文件是 Document Server 主动拉取的,而非集成方推送的。这意味着 Document Server 必须能够访问到文件 URL(网络可达)。

步骤 4:文件解析与渲染

Document Server 的转换服务将 .docx 解析为编辑器内部格式,通过 WebSocket 将渲染数据推送给浏览器端编辑器。编辑器在浏览器中以 Canvas 渲染文档内容(不是 HTML DOM 渲染,这是 OnlyOffice 与 Google Docs 的一个重要架构差异,后文会详述)。

阶段二:编辑中

用户的每一次键盘输入、格式修改、光标移动,都会产生操作指令(Operation)。这些操作以极低延迟通过 WebSocket 发送到 Document Server,经过协同服务处理后:

  • 更新服务端维护的文档状态
  • 将操作广播给同一文档的所有其他在线用户
  • 持久化到操作历史(用于版本恢复)

阶段三:保存(关闭文档)

OnlyOffice 的保存机制与许多人的预期不同,值得单独解释。

不是实时写回,而是会话结束时回调。

当最后一个用户关闭文档后(或超过配置的空闲超时时间),Document Server 才会:

  1. 1
    将内存中的最终文档状态序列化为 .docx 文件
  2. 2
    向集成方配置的 callbackUrl 发送 HTTP POST 请求
  3. 3
    回调中携带文档下载链接(Document Server 临时生成的)
  4. 4
    集成方的后端从该链接下载文件,保存到自己的存储系统

这个"延迟回调"的设计是合理的——频繁写回会造成大量 I/O,集中在会话结束时保存效率更高。但它也带来一个需要注意的点:如果 Document Server 在用户编辑期间崩溃,尚未保存的操作可能丢失。生产环境需要通过持久化操作日志来规避这一风险。

**强制保存(Force Save)**是针对这一问题的解决方案,允许集成方主动触发中间保存,Document Server 会立即将当前状态回调给集成方,而不等待会话结束。


4. 协同编辑的实现原理

协同编辑是 OnlyOffice 的核心能力之一,理解其实现原理有助于在集成时做出正确的架构决策。

4.1 核心问题:并发冲突

假设文档中有一行文字"Hello World",用户 A 在第 6 个字符处插入",",与此同时用户 B 删除了第 1 到第 5 个字符。

如果两个操作独立到达服务端并依次应用:

初始状态:Hello World
A 的操作:在位置 6 插入"," → Hello ,World
B 的操作:删除位置 1-5     → ,World(A 的操作被应用到了错误位置)

这就是协同编辑的经典并发冲突问题。

4.2 操作变换(Operational Transformation,OT)

OnlyOffice 使用操作变换算法来解决这个问题。OT 的核心思路是:在应用一个操作之前,先根据已发生的并发操作对其进行变换,使其在新的文档状态下仍然表达用户的原始意图。

上述例子中,服务端收到 A 的插入操作后,需要将 B 的删除操作变换为"删除位置 1-5(考虑到 A 已插入一个字符,实际偏移不变)",保证最终两个用户的操作意图都得到正确体现。

OT 算法的正确实现极其复杂(尤其是多用户并发场景下的变换函数需要满足严格的数学性质),这是协同编辑领域长期的研究课题。OnlyOffice 选择 OT 而非更新的 CRDT(无冲突复制数据类型)算法,主要是因为 OT 在富文本编辑场景下的语义更直观,且更容易与现有的文档格式模型集成。

4.3 两种协作模式的技术含义

OnlyOffice 提供的两种协作模式在技术层面的差异:

快速模式(Fast Mode)

  • 操作实时通过 WebSocket 同步到所有用户
  • 服务端操作变换在毫秒级完成
  • 适合人数较少(通常不超过 10 人)的高频协作
  • 冲突解决对用户透明,偶发的"跳字"现象是 OT 变换的正常表现

严格模式(Strict Mode)

  • 用户编辑某一段落时,该段落对其他用户锁定(只读)
  • 提交后锁定解除,变更同步给所有人
  • 适合多人同时编辑同一文档但分工明确的场景
  • 牺牲了实时性,换取了零冲突的编辑体验

4.4 版本历史与操作回放

Document Server 将每次编辑会话的操作序列持久化到 PostgreSQL。这使得版本历史功能得以实现——通过回放操作序列,可以还原文档在任意历史时刻的状态。

版本恢复的粒度是"编辑会话"而非"每次按键",即每次文档被关闭并重新打开,会形成一个新的版本节点。这个粒度在大多数业务场景下是合理的。


5. 文件格式转换引擎

5.1 为什么 OnlyOffice 的格式兼容性更好

传统开源办公软件(如 LibreOffice)的实现路径是:将 .docx 解析后转换为自有内部格式(如 ODF),在内部格式上进行编辑,保存时再转换回 .docx。这个"解析 → 内部格式 → 重新生成"的过程会不可避免地损失 OOXML 规范中某些 LibreOffice 内部格式不支持的特性。

OnlyOffice 的做法不同:它的编辑器内部格式与 OOXML 高度对齐,避免了中间转换损失。用一个不精确但直观的类比:LibreOffice 是把英文书翻译成中文再翻译回英文,OnlyOffice 是直接用英文读写。

5.2 转换服务的技术实现

转换服务基于 C++ 实现,主要包含:

  • OOXML 解析器:完整实现 ECMA-376 规范(即 .docx/.xlsx/.pptx 的底层标准)
  • ODF 转换器:实现 OOXML 与 ODF 之间的双向映射
  • PDF 渲染引擎:将文档转换为 PDF,基于自研的版面渲染逻辑
  • 其他格式适配层:处理 .txt、.csv、.html、.epub 等格式

5.3 转换 API 的使用方式

转换服务通过 REST API 暴露,集成方可以独立调用,不需要打开编辑器界面:

POST https://your-document-server/ConvertService.ashx
请求体(JSON):
{
  "async": false,
  "filetype": "docx",
  "key": "唯一转换任务标识",
  "outputtype": "pdf",
  "title": "output.pdf",
  "url": "https://your-storage.com/files/contract.docx"
}

转换服务同样支持异步模式(async: true),适合处理大文件或批量转换场景,通过轮询接口获取转换结果。


6. JWT 安全通信机制

6.1 为什么需要 JWT

在 OnlyOffice 的三方架构中,Document Server 需要主动访问集成方提供的文件 URL 和回调 URL。如果没有签名机制,任何人都可以伪造请求,让 Document Server 去访问任意 URL,或者伪造回调数据篡改文件。

JWT(JSON Web Token)在这里解决两个方向的安全问题:

  • 集成方 → Document Server:配置信息需要签名,Document Server 验签后才处理请求
  • Document Server → 集成方:回调请求携带 JWT Header,集成方验签后才接受保存操作

6.2 签名流程

集成方持有一个与 Document Server 共享的密钥(Secret)。生成编辑器配置时,将配置 JSON 用该密钥签名为 JWT Token:

原始配置 JSON
      |
      v
使用 HMAC-SHA256 + Secret 签名
      |
      v
JWT Token(Header.Payload.Signature)
      |
      v
以 token 字段附加在配置 JSON 中,传给浏览器

Document Server 收到配置后,用相同的 Secret 验签,验证通过才继续处理。反向(Document Server 回调集成方)也是同样的机制。

6.3 没有启用 JWT 的风险

社区版默认可以不配置 JWT(方便开发调试),但生产环境务必启用。未启用 JWT 意味着:

  • 任何人只要能访问 Document Server,就可以构造请求让其下载任意 URL
  • 伪造的回调请求可以替换集成方存储系统中的文件内容

JWT 的配置方式会在第四篇部署文章中详细说明。


7. 三大编辑器的架构共性与差异

Document Editor、Spreadsheet Editor、Presentation Editor 共享同一套底层架构(渲染引擎、协同层、转换层),但在编辑模型上各有侧重。

7.1 共同的渲染方式:Canvas 渲染

OnlyOffice 的三大编辑器均采用 Canvas 渲染,而非将文档内容映射为 HTML DOM 树。

这个选择有其权衡:

Canvas 渲染的优势:

  • 像素级控制文档版面,与打印输出高度一致(WYSIWYG)
  • 不受 CSS 渲染模型限制,复杂排版(如文字绕图)实现更精确
  • 减少大型文档时 DOM 节点过多导致的性能问题

Canvas 渲染的代价:

  • 文档内容对屏幕阅读器(辅助技术)不友好,无障碍访问支持较弱
  • 选中文本、浏览器查找(Ctrl+F)等原生浏览器功能需要自行实现
  • 调试渲染问题比 HTML DOM 更困难

相比之下,Google Docs 采用 HTML 渲染(加大量 JS 控制),Notion 采用标准 contenteditable,各有取舍。

7.2 Document Editor(文字处理)

  • 排版引擎实现了完整的流式布局模型:文字流、段落、分节、分页
  • 光标与选区由自实现的 Hit-testing 逻辑处理(不依赖浏览器文本选区)
  • 修订(Track Changes)在操作层记录,渲染时叠加显示

7.3 Spreadsheet Editor(电子表格)

  • 采用虚拟化渲染:只渲染可视区域内的单元格,支持超大表格(百万行级别)
  • 公式引擎独立实现,兼容 Excel 函数库(约 450 个函数)
  • 图表引擎基于自研矢量渲染,支持实时数据联动

7.4 Presentation Editor(演示文稿)

  • 幻灯片采用固定尺寸画布(16:9 或 4:3),布局模型比 Document Editor 简单
  • 动画与过渡效果在编辑模式下以静态缩略图预览,播放模式下全效展示
  • 对 .pptx 中母版(Slide Master)和版式(Slide Layout)的支持是格式兼容性的关键点

8. 各版本功能对比

在第一篇中已经提到了开源版与商业版的大致区别,这里给出更完整的技术层面对比:

功能维度社区版(AGPL)企业版开发者版
三大编辑器完整支持完整支持完整支持
并发连接数限制无限制无限制无限制
文件格式转换 API支持支持支持
JWT 安全集成支持支持支持
插件系统支持支持支持
宏(JavaScript)支持支持支持
文档水印不支持支持支持
白标定制(去品牌)不支持支持支持
文档访问权限精细控制基础高级高级
LDAP / AD 集成不支持支持支持
SSO(SAML/OAuth)不支持支持支持
集群 / 高可用部署需自行实现官方方案官方方案
审计日志不支持支持支持
官方技术支持论坛SLA 支持SLA 支持
授权方式免费开源按年/按服务器按年/按服务器

开发者版与企业版的核心区别在于使用场景的授权:企业版面向内部部署,开发者版专门授权给将 OnlyOffice 集成进商业产品的 ISV(独立软件开发商),允许对外提供服务。


9. 小结

本文从工程视角拆解了 OnlyOffice Document Server 的内部结构,核心要点如下:

架构层面:

  • Document Server 是一个由 Nginx、Node.js、转换服务、RabbitMQ、Redis、PostgreSQL 组成的多进程系统
  • 三方协作模型(你的应用 + Document Server + 文件存储)是所有集成场景的基础
  • 文件由 Document Server 主动从集成方拉取,保存通过回调机制推送给集成方

协同编辑层面:

  • 基于操作变换(OT)算法处理并发冲突
  • 保存机制是"会话结束后回调",生产环境需关注强制保存配置

格式兼容层面:

  • 转换服务基于 C++ 实现,内部格式与 OOXML 高度对齐,这是格式保真度的来源
  • 三大编辑器均采用 Canvas 渲染,与 HTML DOM 渲染路线的系统有本质差异

安全层面:

  • JWT 是三方通信的安全基础,生产环境必须启用