做者| 不四(死马)蚂蚁金服 语雀产品技术负责人html
语雀是什么?
语雀是一个专业的云端知识库,面向我的和团队,提供不同凡响的知识管理,打造轻松流畅的工做协同,它提供各类格式的在线文档(富文本、表格、设计稿等)编辑能力,支持实时在线多人协同编辑,数据云端保存不丢失。而语雀与其余文档工具最大的不一样是,它经过知识库来对文档进行组织,让知识创做者更好的管理知识。前端
语雀技术架构演进
原型阶段
语雀诞生于 2016 年,当时蚂蚁金融云须要一个工具来承载它的文档。当时负责的技术同窗利用业余时间,开始搭建这个文档工具。项目的初期,没有任何人员和资源支持,同时也为了快速验证原型,技术选型上选择了最低成本的方案。程序员
底层服务彻底基于体验技术部内部提供的 BaaS 服务和容器托管平台:web
- Object 服务:一个类 MongoDB 的数据存储服务;
- File 服务:阿里云 OSS 的基础上封装的一个文件存储服务;
- DockerLab:一个容器托管平台;
这些服务和平台都是基于 Node.js 实现,专门给内部创新型应用使用,也正是因为有这些下降创新成本的内部服务,才给工程师们提供了更好的创新环境。数据库
应用层服务端天然而然的选用了体验技术部开源的 Node.js Web 框架 Egg(蚂蚁内部的封装 Chair),经过一个单体 Web 应用实现服务端。应用层客户端也选用了 React 技术栈,结合内部的 antd,并采用 CodeMirror 实现了一个功能强大、体验优雅的 markdown 在线编辑器。编程
这时能够算做语雀的“原型阶段”,它仅仅是一个工程师的业余项目,采用内部专为创新应用提供的 BaaS 服务和一系列的开源技术解决方案,验证了在线文档工具这个产品原型。canvas
PS:当时我还不在语雀团队,可是巧的是我却在给语雀提供 Object、File 等 BaaS 服务和 Egg.js Web 框架的支持。浏览器
内部服务阶段
随着在线文档工具获得了团队内部的承认,语雀的目标已经不只仅是金融云的文档工具,而是志在替代 confluence 等竞品,成为阿里内部十万员工的知识管理平台。语雀要面向知识创做者,只提供 Markdown 编辑器确定没法让非技术人员更高效的使用语雀。尽管有很多真爱粉由于语雀开始学习甚至爱上了 Markdown,可是咱们仍然义无反顾的踏入了富文本编辑器领域的深坑。同时和 Word 等富文本编辑器不一样,咱们选择了更“Web”的路线,在富文本编辑器中加入了公式、文本绘图、思惟导图等特点功能。而随着语雀在知识管理领域的不断探索,知识管理的三层结构(团队、知识库、文档)开始成型。在此之上的协做、分享、搜索与消息动态等功能愈来愈复杂单纯的依靠 BaaS 服务已经没法知足语雀的业务需求了。缓存
为了应对业务发展带来的挑战,咱们主要从下面几个点进行改造:安全
- BaaS 服务虽然使用简单成本低,可是它们提供的功能不足以知足语雀业务的发展,同时稳定性上也有不足。因此咱们将底层服务由 BaaS 替换成了内部的 IaaS 服务(MySQL、OSS、缓存、搜索等服务)。
- Web 层仍然采用了 Node.js 与 Egg 框架,可是业务层借鉴 rails 社区的实践开始变成了一个大型单体应用,经过引入 ORM 构建数据模型层,让代码的分层更清晰;
- 前端编辑器从 codeMirror 迁移到 Slate。为了更好的实现语雀编辑器的功能,咱们内部 fork 了 Slate 进行深刻开发,同时也自定义了一个独立的内容存储格式,以提供更高效的数据处理和更好的兼容性。
在内部服务阶段,语雀已经成为了一个正式的产品,和蚂蚁的其余项目没有什么区别了,经过在阿里内部的磨炼,语雀的产品形态基本定型。
商业化阶段
随着语雀的内部影响力愈来愈大,一些离职出去创业的阿里校友们开始找到玉伯:“语雀挺好用的,有没有考虑商业化以后让外面的公司也可以用起来?” 通过小半年的酝酿和重构,18 年初,语雀开始正式对外提供服务,进行商业化。
当一个应用走出公司内到商业化环境中,面临的技术挑战一会儿就变大了。最核心的知识创做管理部分的功能愈来愈复杂,表格、思惟导图等新格式的加入,多人实时协同的需求对编辑器技术提出了更高的挑战。而为了更好的服务企业用户与我的用户, 语雀在企业服务、会员服务等方面也投入了很大精力。在业务快速发展的同时,服务商业化对质量、安全和稳定性也提出了更高的要求。
为了应对业务发展,语雀的架构也随之发生了演进:
咱们将底层的依赖彻底上云,所有迁移到了阿里云上,阿里云不只仅提供了基础的存储、计算能力,同时也提供了更丰富的高级服务,同时在稳定性上也有保障。
- 丰富的云计算基础服务,保障语雀的服务端能够选用最适合语雀业务的的存储、队列、搜索引擎等基础服务;
- 更多人工智能服务给语雀的产品带来了更多的可能性,包括 OCR 识图、智能翻译等服务,最终都直接转化成为了语雀的特点服务;
而在应用层,语雀的服务端依然仍是以一个基于 Egg 框架的大型的 Node.js web 应用为主。可是随着功能愈来愈多,也开始将一些相对比较独立的服务从主服务中拆出去,能够把这些服务分红几类:
- 微服务类:例如多人实时协同服务,因为它相对独立,且长链接服务不适合频繁发布,因此咱们将其拆成了一个独立的微服务,保持其稳定性;
- 任务服务类:像语雀提供的大量本地文件预览服务,会产生一些任务比较消耗资源、依赖复杂。咱们将其从主服务中剥离,能够避免不可控的依赖和资源消耗对主服务形成影响;
- 函数计算类:相似 plantuml 预览、mermaid 预览等任务,对响应时间的敏感度不高,且依赖能够打包到阿里云函数计算中,咱们会将其放到函数计算中运行,既省钱又安全;
随着编辑器愈来愈复杂,在 slate 的基础上进行开发遇到的问题愈来愈多。最终语雀仍是走上了自研编辑器的道路,基于浏览器的 contenteditable 实现了富文本编辑器,经过 canvas 实现了表格编辑器,经过 SVG 实现了思惟导图编辑器。
语雀富文本编辑器相关的介绍,能够看看 Lake Editor 之父隆昊的分享:富文本编辑器的技术演进。
语雀的这个阶段(也是如今所处的阶段)是商业化阶段,可是咱们仍然保持了一个很小的团队,经过 JavaScript 全栈进行研发。底层的服务全面上云,借力云服务打造语雀的特点功能。同时为企业级用户和我的知识工做者者提供知识创做和管理工具。
JavaScript 全栈
在社交网络上,你们好像对 JavaScript 全栈的见解都比较负面,“样样通,样样松”多是你们听到全栈工程师这个名词后的第一印象。那为何语雀选择了 JavaScript 全栈的方向呢?
JavaScript 全栈与产品工程师
在语雀,咱们并不将用 JavaScript 全栈进行开发的工程师定义为全栈工程师,而是“一专多能”型的产品工程师:
- 他们是产品的“技术合伙人”,他们对产品有 owner 感,和产品经理一块儿参与产品讨论设计,从技术的角度上对产品设计方案提出建议,独立的完成产品功能的全栈研发,并跟踪发布后的产品结果。
- 同时他们也是某一个技术领域的领域专家,例若有人多是服务端领域的专家、测试领域的专家、前端构建领域的专家、CSS 领域的专家。他们能够用本身的专业领域知识来优化团队研发工具链,提高产品研发效率。
在语雀,产品工程师们的产品研发流程是这样的:
- 在产品设计阶段,产品工程师就会参与进去进行讨论,最终会产出一份 final design 的产品设计稿。因为前期产品工程师参与充分讨论,通常此处定下的产品设计稿到后期的研发过程当中不会遇到技术上的问题;
- **随后会在语雀上进行文档化的系统分析设计。**会在语雀上发起异步的评审。一些大的技术方案会有其余的领域专家加入进来一块儿进行评审,确保将全部的技术难点都梳理清楚;
- 系统设计清晰后,进入研发阶段;
- 对全部的代码,都须要有自动化测试覆盖。对全部新增代码和修改的业务逻辑都须要有彻底覆盖的单元测试,对关键链路的功能同时也要提供端到端测试。编写完自动化测试是进入代码评审前的必备流程。
- 阶段性的功能研发完成、测试编写完善后会发起异步的代码评审。会邀请相关业务的负责人和对应的一些领域专家来进行代码评审。从业务逻辑的正确性,安全性,可维护性等多个角度来进行代码评审。
- 最终在发布上线时,必须遵循三板斧原则:可灰度、可应急、可监控。避免功能变动可能带来的 bug 影响到大量用户。
语雀是如何进行全栈 JavaScript 测试的呢?感兴趣的同窗能够看看语雀团队大前端自动化测试大牛达峰老师的分享:大前端测试的思考和在语雀的实践
经过 JavaScript 全栈,语雀团队能够更高效、高质量的的完成产品研发:
- 从代码层面上来讲,有大量的代码能够复用,以编辑器举例,它不只仅能够在 Web 端使用,也能够在桌面端使用。同时许多数据处理的能力还能够在服务端使用。
- 从产品研发效率上来讲,全栈研发减小了大量沟通成本,在语雀当前的阶段是很是高效的。而 JavaScript 全栈避免了开发者在不一样的语言中进行切换,不用考虑前端使用的 lodash / moment 等工具类在其余语言中应该用什么,大大提高全栈的研发效率。
- 最后从工程师角度来看,全栈研发让工程师有机会深度参与到产品研发的整个流程中,你们会自发的去思考产品有什么优化点,从技术上能帮助产品作什么。例如语雀最近新上的 OCR 搜图功能,就是语雀的全栈工程师自发从技术预研到产品落地完成整个产品优化的。
JavaScript 全栈与 Node.js
说到 JavaScript 全栈,有一个绕不过去的技术就是 Node.js。做为一个与前端结合紧密的服务端运行时,基本上就成为了全栈的代言人。那 Node.js 是否是真的是一个适合大型商业化项目的语言呢?你们对它都有颇多质疑:
其实随着 JS 语言的发展,许多问题已经获得了解决,例如 Async Function 的出现,可让开发者以同步的方式编写异步代码,理解起来更简单,异常处理也变简单了。同时随着社区的进一步完善,大量高质量的工具模块、框架涌现出来。语雀的服务端部分基于 Egg 框架,已经集成了大量 Web 开发须要的模块和服务,同时基于 Async Function 编程模型也更加简单。TypeScript 的出现也打消了许多人对 JavaScript 进行大型项目开发的疑虑。除此以外,语雀还有一些其余的方式来保障代码质量和可维护性(语雀甚至是一个纯 JavaScript 项目,没有一行 TypeScript 代码)。
语雀作的第一件事情就是肯定核心系统和外部系统的边界。经过六边形架构(也叫作端口适配器架构),咱们把语雀核心系统和外界系统和用户之间的交互固定下来。经过“端口”的形式,来肯定输入和输出。外部系统经过“适配器”来将系统对接到语雀暴露的端口之上,只须要按照“端口”定义来实现,外部系统能够自由替换。
在这个模型下,Controller 就是语雀暴露给用户接口的 HTTP 适配器。在 Controller 中,咱们对用户请求参数进行格式校验和转换,检查用户权限,并格式化输出。
咱们定义好语雀与第三方平台和服务之间的交互方式(通常是一系列方法),经过适配器,将不一样环境的不一样服务封装成统一的方法,并在调用时记录好调用日志。
数据模型层便是数据层的 Model,以 Doc 模型举例,它的 meta 信息数据被存储在了 MySQL 中,而文档正文数据被加密后存储在 OSS 中。对于语雀核心的业务逻辑来讲,彻底不感知底层的存储在哪里。更进一步来讲,只要语雀是使用 SQL 和数据库进行交互,底层数据能够无缝迁移到 OceanBase 等其余支持完整 SQL 语法的数据库中,即便有少许修改也能够在 Model 层封装掉。
最终以一次文档发布举例,用户经过调用 HTTP 接口与语雀进行交互,数据会经过 Model 层写入到存储中,包括 MySQL 和 OSS,更新文档缓存。同时出发异步消息给其余系统,触发钉钉的 WebHook,并将数据同步到搜索引擎中。这些和外界系统的交互经过适配器封装以后各司其职,参数转换、权限校验、日志记录,不只确保核心逻辑的精简,也让系统调用链路跟踪更加简单。
混合应用架构
当系统发展到必定程度后,究竟是应该继续在大单体应用上加功能,仍是拆分红微服务呢?这两种架构既然存在,确定有各自的优劣,具体选择那种架构形式,应该是与当前的业务规模和团队分布决定的。因此语雀的技术架构随着语雀的业务形态也变成了一个混合式的技术架构。
语雀的主服务是一个大型的 Node.js 服务,集中了全部的应用业务逻辑。而在主服务以外,还有一些不一样形态的其余服务。
- 微服务:一些独立而稳定的功能模块,或者有额外部署架构需求的服务,会经过微服务的形式独立部署,系统间暂时经过 HTTP 接口进行交互。例如实时协同服务,因为其自身比较独立稳定,并且是长链接服务,不能频繁发布重启,因此将其部署成了一个独立的微服务。
- 任务集群:一些 CPU 密集型的任务,或者依赖了一些复杂的第三方依赖的服务,会放到一个独立的任务集群中。例如各类文件预览服务,可能依赖到了其余服务,且须要消耗大量计算成本,放到任务集群经过队列消除并发后最为合适。
- 函数计算:一些对响应时间比较高且能够函数化的服务,咱们会尽可能迁移到阿里云的函数计算,例如plantuml、mermaid 等文本绘图服务。
以 mermaid 的渲染举例。用户输入一段 mermaid 代码调用语雀,语雀调用一个部署在阿里云函数计算的函数,在函数中运行 puppeteer 渲染成 svg 返回。
为何要特别把 Serverless 单独拿出来讲呢?还记得以前说 Node.js 是单线程,不适合 CPU 密集型任务么?因为 Serverless 的出现,咱们能够将这些存在安全风险的,消耗大量 CPU 计算的任务都迁移到函数计算上。它运行在沙箱环境中,不用担忧用户的恶意代码形成安全风险,同时将这些 CPU 密集型的任务从主服务中剥离,避免出现并发时阻塞主服务。按需付费的方式也能够大大节约成本,不须要为低频功能场景部署一个常驻服务。因此咱们会尽可能的把这类服务都迁移到 Serverless 上(如阿里云函数计算)。
语言以外的通用领域
除了语言以外,任何的商业化系统还有更多须要考虑的方面,其中最重要的两点可能就是安全性和稳定性了。
一个系统从前端、服务端到底层的依赖都存在着各类各样的安全风险:
- 前端安全风险:XSS、跳转钓鱼、跨站请求等
- 服务端安全风险:水平权限问题、未受权访问、敏感信息泄露、SSRF、SQL 注入等
- 云服务的安全风险:短信/邮件轰炸、数据泄露风险、内容安全等
这些安全问题想要解决基本都没有银弹,只能一个个单独处理,可是有一些基本的原则:
- 不要信任用户的任何输入
- 任何渲染富文本的地方都须要防范 XSS,内容也可能并非经过 IDE 输入的;
- 要在服务端执行用户的代码必定要放在沙箱中;
- 要从服务端请求用户传递的资源,必定要通过 SSRF 过滤;
- 沉淀标准的编码范式来处理安全风险,且须要在 Code Review 中重点关注
- 全部接口都必须有权限校验;
- 响应序列化方法过滤敏感信息;
- 不容许拼接 SQL;
语雀从商业化一开始就和安全团队通力协做,从内部的安全意识培训、内部安全团队测试,到内部的红蓝攻防、外部的白帽子渗透测试,安全是一场持久战。
为了保障语雀的稳定性,咱们从前端到服务端和云服务上都作了许多工做,和安全同样,稳定性也是一个从前到后的长期工程。语雀的稳定性保障主要在两个维度:
- 保障服务可用性:从架构设计上要杜绝单点,底层的数据都须要进行容灾和备份,服务须要多单元、可用区部署。同时避免引入没必要要的强依赖;
- 异常可监控和追溯:从前端的业务埋点日志、异常日志监控,到服务端的全链路日志跟踪和采集,系统性能监控和分析。最终咱们能够达到异常可及时感知和追溯,性能问题能够定位分析;
什么叫作避免引入没必要要的强依赖呢?以语雀的场景举例,MySQL 就是一个没法去除的强依赖,而缓存不该该是一个强依赖,可是最先语雀的 session 是存储在缓存(Redis)中的,一旦 Redis 集群出问题,用户资料没法获取就致使用户没法登陆。这就把缓存变成了一个强依赖。因此咱们将 session 存储放到了 MySQL 中,Redis 就变成了一个弱依赖,它挂了系统还能正常运行。另外一个例子,语雀前段时间上线了多人实时协同编辑的功能,而在这个功能上线以前,是经过文档加锁的方式避免多我的同时编辑同一篇文档的。然而多人实时协同引入了另外一个服务,一旦实时协同服务挂了,用户就没法编辑文档了,它又变成了语雀系统的一个强依赖,为了解决他,咱们在用户链接协同服务失败的时候,自动切换到老的锁模式。这样协同服务也变成了语雀的一个弱依赖。
语雀如何选择技术栈
语雀这几年一步步发展过来,背后的技术一直在演进,可是始终遵循了几条原则:
- 技术栈选型要匹配产品发展阶段。产品在不一样的阶段对技术提出的要求是不同的,越前期,对迭代效率的要求越高,商业化规模化以后,对稳定性、性能的要求就会变高。不须要一上来就用最早进的技术方案,而是须要和产品阶段一块儿考虑和权衡。
- 技术栈选型要结合团队成员的技术背景。语雀选择 JavaScript 全栈的缘由是孵化语雀的团队,大部分都是 JavaScript 背景的程序员,同时 Node.js 在蚂蚁也算是一等公民,配套的设施相对完善。
- 最重要的一点是,不论选择什么技术栈,安全、稳定、可维护(扩展)都是要考虑清楚的。用什么语言、用什么服务会变化,可是这些基础的安全意识、稳定性意识,如何编写可维护的代码,都是决定项目可否长期发展下去的重要因素。
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的技术圈。”
原文出处:https://www.cnblogs.com/alisystemsoftware/p/12193018.html