背景
最近由于业务需要,需要一个web-excel平台给用户提供数据分析能力,经过一番评估,确定使用onlyoffice提供的web-excel能力,它基于GPL-V3协议,其开源的代码可以直接进行本地化部署。本文记录了在调研期间学习到的onlyoffice整体架构设计理念
整体设计
先简单说明下webExcel的核心能力,这些能力决定了整体架构:
- 支持多人协同编辑能力:不管是webExcel,还是其他doc、ppt产品线,都支持多人同时编辑同一份文档。前后端依靠协同编辑协议来保证编辑时数据的正确性,协议的核心在服务端,它决定了多人在编辑同一个文档内同一个sheet和单元格范围时,只有一个协同者的操作是有效的。
- 支持原生的xlsx文件,这点也是产品成功的核心要素,因为大部分用户从桌面迁移到在线平台时最大的诉求之一是要能打开原先的xlsx文件,并且做到打开和保存时都是同一份格式。onlyoffice为了支持这个功能,内部自定义一套前端页面能识别的序列化协议,依赖服务端的转换服务将xlsx文件格式转换为该协议内容。
- 在线服务化能力:支持第三方服务方便使用webExcel服务快速打开xlsx文件,目前业界已有wopi协议约定了服务间的接口,webExcel实现了该协议的服务端逻辑,这样三方服务只需要实现基本的文件打开、保存接口即可。
所以webExcel需要服务端来支撑上述能力,客户端支持多个终端,包括web、desktop、H5。由于业务以web为主,所以接下来以Web场景为例介绍具体实现思路。前后端整体架构图如下:
整个架构图核心围绕三个服务之间交互,配合中间件作为通信桥梁;这三个服务的具体功能如下:
文档管理服务,具备:
- 文档管理能力,包括上传、下载功能、文档保存功能;
- 提供文档打开的入口页面,页面内包含打开文档所需的参数,包括
- 文档信息,包括文档ID、获取文档内容的接口、保存文档的接口地址等
- 用户认证信息和权限信息
这里有必要介绍下文档ID(docId)的概念;我们知道文档可以有多版本,每个版本的内容不一样,docId就是某次打开/编辑某一个版本文件时的ID,不同版本的docId是不一样的。版本的内容在文件保存时更新并生成下一个版本。
该服务属于三方服务自定义范畴,不过onlyoffice官方提供多语言的example工程作为参考,具体见这里
协同服务,实现了:
- 配合客户端,实现了协同编辑协议,核心是支持锁设计,这样锁住单元格range后其他client就不能编辑
- 认证:决定一个用户是否有权打开这个文档;核心是通过JWT协议实现
- 文档生命周期管理,包括实现文档打开、编辑、保存、关闭等基本的交互操作,这块逻辑和用户操作息息相关
- 依赖中间件和其他服务交互:通过队列实现和其他服务的交互,包括文件转换、保存、下载、打印等功能
- GC管理,定时清理过期缓存文件和会话,缓存文件是什么?下面会提到文档打开时会调用转换服务将xlsx文件转换为Editor.bin,这个文件存放在本地/远程存储空间。文档编辑完成并保存成功后,这个文件的功能就是失效了,所以为了避免这个文件占据存储空间,有必要定时清理这些失效的文件
转换服务
- 负责将不同Excel格式文档转换为自定义序列化格式,该格式类似XLSB格式,新文件名称叫做Editor.bin。由于转换对性能要求高,所以转换逻辑通过C++实现,转换时通过创建独立的进程来运行转换逻辑
- 实现文件内容转pdf、导入导出功能
- 与协同服务交互:接受协同服务发送过来的转换格式请求,执行完后再发送回消息到队列来响应请求
服务对外依赖的中间件包括缓存、数据库、队列、定时任务调度等:
- 缓存:存储用户在线连接数、文档协同相关状态信息等
- 数据库:保存打开的文档信息、文档协同编辑时存储的op记录
- 队列:在服务间建立异步通信通道
- 定时任务调度:目前依赖操作系统默认的crontab执行定时清理历史文档任务
设计感想
协议驱动设计
前后端通过协同编辑协议保证多人编辑的正确性;底层定义了每个op操作序列化协议,它是前后端交互的基础。前端浏览器通过op实时save操作信息和通过回放支持多人协同,服务端通过原始xlsx+op操作列表生成新的xlsx文件,所以如何保证op的实时存储和响应性能是需要考虑的。
onlyoffice在编辑时通过websocket协议实时把op传输到协同服务并存储在DB中,并且op本身不会更新,从DB角度看op只涉及到insert和delete操作:文档协同编辑时涉及到insert操作,文档保存成功后调用delete操作。这样的模型保证了只要程序内部不出错,那么最终转换的xlsx内容是正确的;这个设计理念比较可靠;并且从文档操作频率和用户并发角度看op操作对DB的整体性能是可以接受的。
不过从文档保存到最后调用delete删除op记录,这个操作流程不满足事务特性,中间涉及到多个服务间的异步协同,需要有个状态记录操作过程,所以接下来从文档状态流转来理解如何在多个服务间保证文档操作完整性
文档内部状态机
在不影响整体理解情况下,这里只列出核心的状态流转图,图中解释了每个状态改变前后的操作,每个状态都写入数据库里进行持久化存储,重点介绍文件打开和关闭的状态流转。
打开文件
因为打开的xlsx文件需要经过转换服务转换生成Editor.bin,这是一个异步操作,协同服务和转换服务中间通过消息队列传递任务消息,所以为了判断转换是否成功,引入WaitQueue状态,处于该状态表示文件正在转换。
正常情况下如果xlsx文件转换成功后,就会更新状态为OK,然后协同服务异步通知前端页面可以打开Editor.bin文件了。
如果客户端因为网络原因和服务端重连或者重刷页面,此时如果服务端根据状态判断文件处于WaitQueue,那么直接通知客户端进入等待状态。
如果二次打开同一个版本的xlsx文件,那么服务端根据OK状态判断文件已经打开过了,这样避免对文件重新进入转换的流程。
关闭文件
如果服务端收到websocket的close事件后,就进入关闭流程;不过与传统的本地文件立刻保存不一样,webExcel在立刻保存之前等待5s(时间可调),这个时间内的状态称为Save Version;如果5s内收到重新打开文档的请求,那么在这个请求内就将状态重新reset为OK;
如果超过5s会触发真正的save逻辑,此时也是通过Queue通过转换服务转换任务:将当前的op序列化列表作用在Editor.bin文件上并生成新的xlsx文件,生成成功后通知 文档管理服务 下载新版本的文件,同时更新当前docId的状态为Update Version,表示上个文档的版本已更新了。
服务间耦合重影响部署
目前官方提供Docker镜像封装了上述三个服务和中间件,属于典型的单体式架构,这可以在开发环境上快速使用。但是部署在生产环境上,就需要考虑服务的稳定性和合理性了;至少中间件直接部署在容器里就不符合现阶段微服务的要求;并且中间件生成的结果(文件、消息任务等)直接放在本地,导致没法进行横向扩容。所以这点需要根据业务情况去定制的。
最后做个简单总结:整体来说,webExcel的整体架构比较清晰,前后端服务职责明确,不过工程毕竟是10年前开发,所以源码带有一定历史味道,代码质量不太符合现代化规范,并且官方几乎没有什么技术细节的文档,特别涉及到协同协议和op序列化理解上;所以本文简单梳理服务端架构,理解为什么要这么设计以及过程中需要注意的技术细节,期望给基于onlyoffice进行二次开发的开发者带来帮助。