作者
不四
来源
阿里巴巴中间件(ID:Aliware_)
每个技术人心中或多或少都有一个「产品梦」,好的技术需要搭配好的产品,才能让用户爱不释手,尤其是做一款知识服务型产品。
本文从技术架构的视角,回顾了语雀的原型、内部服务和对外商业化的全过程,并对函数计算在语雀架构演进过程中所扮演的角色做了详细的介绍。
语雀是一个专业的云端知识库,用于团队的文档协作。现在已是阿里员工进行文档编写和知识沉淀的标配,并于年开始对外提供服务。
原型阶段
回到故事的开始。
年,语雀孵化自蚂蚁科技,当时,蚂蚁金融云需要一个工具来承载它的文档,负责的技术同学利用业余时间,搭建了这个文档工具。项目的初期,没有任何人员和资源支持,同时也是为了快速验证原型,技术选型上选择了最低成本的方案。
底层服务完全基于体验技术部内部提供的BaaS服务和容器托管平台:
Object服务:一个类MongoDB的数据存储服务;File服务:阿里云OSS的基础上封装的一个文件存储服务;DockerLab:一个容器托管平台;
这些服务和平台都是基于Node.js实现的,专门给内部创新型应用使用,也正是由于有这些降低创新成本的内部服务,才给工程师们提供了更好的创新环境。
语雀的应用层服务端,自然而然的选用了蚂蚁体验技术部开源的Node.jsWeb框架Egg(蚂蚁内部的封装Chair),通过一个单体Web应用实现服务端。
应用层客户端也选用了React技术栈,结合内部的antd,并采用CodeMirror实现了一个功能强大、体验优雅的markdown在线编辑器。
当时仅仅是一个工程师的业余项目,采用内部专为创新应用提供的BaaS服务和一系列的开源技术,验证了在线文档工具这个产品原型。
内部服务阶段
年,随着语雀得到团队内部的认可,他的目标已经不仅仅是金融云的文档工具,而是成为阿里所有员工的知识管理平台。
不仅面向技术人员Markdown编辑器,还向非技术知识创作者,提供了富文本编辑器,并选择了更“Web”的路线,在富文本编辑器中加入了公式、文本绘图、思维导图等特色功能。
而随着语雀在知识管理领域的不断探索,知识管理的三层结构(团队、知识库、文档)开始成型。
在此之上的协作、分享、搜索与消息动态等功能越来越复杂单纯的依靠BaaS服务已经无法满足语雀的业务需求了。
为了应对业务发展带来的挑战,我们主要从下面几个点进行改造:
BaaS服务虽然使用简单成本低,但是它们提供的功能不足以满足语雀业务的发展,同时稳定性上也有不足。所以我们将底层服务由BaaS替换成了阿里云的IaaS服务(MySQL、OSS、缓存、搜索等服务)。Web层仍然采用了Node.js与Egg框架,但是业务层借鉴rails社区的实践开始变成了一个大型单体应用,通过引入ORM构建数据模型层,让代码的分层更清晰。前端编辑器从codeMirror迁移到Slate。为了更好的实现语雀编辑器的功能,我们内部fork了Slate进行深入开发,同时也自定义了一个独立的内容存储格式,以提供更高效的数据处理和更好的兼容性。
在内部服务阶段,语雀已经成为了一个正式的产品,通过在阿里内部的磨炼,语雀的产品形态基本定型。
对外商业化阶段
随着语雀在内部的影响力越来越大,一些离职出去创业的阿里校友们开始找到玉伯(蚂蚁体验技术部研究员):“语雀挺好用的,有没有考虑商业化之后让外面的公司也能够用起来?”
经过小半年的酝酿和重构,年初,语雀开始正式对外提供服务,进行商业化。
当一个应用走出公司内到商业化环境中,面临的技术挑战一下子就变大了。最核心的知识创作管理部分的功能越来越复杂,表格、思维导图等新格式的加入,多人实时协同的需求对编辑器技术提出了更高的挑战。
而为了更好的服务企业用户与个人用户,语雀在企业服务、会员服务等方面也投入了很大精力。在业务快速发展的同时,服务商业化对质量、安全和稳定性也提出了更高的要求。
为了应对业务发展,语雀的架构也随之发生了演进:
我们将底层的依赖完全上云,全部迁移到了阿里云上,阿里云不仅仅提供了基础的存储、计算能力,同时也提供了更丰富的高级服务,同时在稳定性上也有保障。
丰富的云计算基础服务,保障语雀的服务端可以选用最适合语雀业务的的存储、队列、搜索引擎等基础服务;更多人工智能服务给语雀的产品带来了更多的可能性,包括OCR识图、智能翻译等服务,最终都直接转化成为了语雀的特色服务;而在应用层,语雀的服务端依然还是以一个基于Egg框架的大型的Node.jsWeb应用为主。但是随着功能越来越多,也开始将一些相对比较独立的服务从主服务中拆出去,可以把这些服务分成几类:
微服务类:例如多人实时协同服务,由于它相对独立,且长连接服务不适合频繁发布,所以我们将其拆成了一个独立的微服务,保持其稳定性。任务服务类:像语雀提供的大量本地文件预览服务,会产生一些任务比较消耗资源、依赖复杂。我们将其从主服务中剥离,可以避免不可控的依赖和资源消耗对主服务造成影响。函数计算类:类似Plantuml预览、Mermaid预览等任务,对响应时间的敏感度不高,且依赖可以打包到阿里云函数计算中,我们会将其放到函数计算中运行,既省钱又安全。随着编辑器越来越复杂,在slate的基础上进行开发遇到的问题越来越多。最终语雀还是走上了自研编辑器的道路,基于浏览器的Contenteditable实现了富文本编辑器,通过Canvas实现了表格编辑器,通过SVG实现了思维导图编辑器。
语雀的这个阶段(也是现在所处的阶段)是商业化阶段,但是我们仍然保持了一个很小的团队,通过JavaScript全栈进行研发。底层的服务全面上云,借力云服务打造语雀的特色功能。同时为企业级用户和个人知识工作者者提供知识创作和管理工具。
和函数计算的不解之缘
语雀是一个复杂的Web应用,也是一个典型的数据密集型应用(Data-IntensiveApplication),背后依赖了大量的数据库等云服务。语雀服务端是Node.js技术栈。
当提到node的时候,可能立刻就会有几个词浮现在我们脑海之中:单线程(single-threaded)、非阻塞(non-blocking)、异步(asynchronouslyprogramming),这些特性一方面非常的适合于构建可扩展的网络应用,用来实现Web服务这类I/O密集型的应用,另一方面它也是大家一直对node诟病的地方,对CPU密集型的场景不够友好,一旦有任何阻塞进程的方法被执行,整个进程就被阻塞。
像语雀这样用node实现整个服务端逻辑的应用,很难保证不会出现一些场景可能会消耗大量CPU甚至是死循环阻塞进程的,例如以markdown转换举例,由于用户的输入无法穷举,总有各种可能让转换代码进入到一个低效甚至是死循环的场景之中。
在node刚出世的年代,很难给这些问题找到完美的解决办法,而即便是Java等基于线程并发模型的语言,在遇到这样的场景也很头痛,毕竟CPU对于web应用来说都是非常重要的资源。而随着基础设置越来越完善,当函数计算出现时,node最大的短板看起来有了一个比较完美的解决方案。
阿里云函数计算是事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传,只需要为代码实际运行所消耗的资源付费,代码未运行则不产生费用。
把函数计算引入之后,我们可以将那些CPU密集型、存在不稳定因素的操作统统放到函数计算服务中去执行,而我们的主服务再次回归到了I/O密集型应用模型,又可以愉快的享受node给我们带来的高效研发福利了!
以语雀中遇到的一个实际场景来举例,用户传入了一些HTML或者Markdown格式的文档内容,我们需要将其转换成为语雀自己的文档格式。
在绝大部分情况下,解析用户输入的内容都很快,然而依然存在某些无法预料到的场景会触发解析器的bug而导致死循环的出现,甚至我们不太敢升级Markdown解析库和相关插件以免引入更多的问题。
但是随着函数计算的引入,我们将这个消耗CPU的转换逻辑放到函数计算上,语雀的主服务稳定性不会再被影响。
除了帮助Web系统分担一些CPU密集型操作以外,函数计算还能做什么呢?
在语雀上我们支持各种代码形式来绘图,包括Plantuml、公式、Mermaid,还有一些将文档导出成PDF、图片等功能。这些场景有两个特点:
他们依赖于一些复杂的应用软件,例如Puppeteer、Graphviz等;可能需要执行用户输入的内容;支持这类场景看似简单,通过process.exec子进程调用一下就搞定了。但是当我们想把它做成一个稳定的对外服务时,问题就出现了。这些复杂的应用软件可能从设计上并没有考虑要长期运行,长期运行时的内存占用、稳定性可能会有一些问题,同时在被大并发调用时,对CPU的压力非常大。
再加上有些场景需要运行用户输入的代码,攻击者通过构建恶意输入,可以在服务器上运行攻击代码,非常危险。
在没有引入函数计算之前,语雀为了支持这些功能,尽管单独分配了一个任务集群,在上面运行这些三方服务,接受主服务的请求来避免影响主服务的稳定性。但是为了解决上面提到的一系列问题还需要付出很大的成本:
需要维持一个不小的任务集群,尽管可能大部分时间都用不上那么多资源。需要定时对三方应用软件进行重启,避免长时间运行带来的内存泄露,即便如此有些特殊请求也会造成第三方软件的不稳定。对用户的输入进行检测和过滤,防止黑客恶意攻击,而黑客的攻击代码很难完全防住,安全风险依旧很大。
最后语雀将所有的第三方服务都分别打包在函数中,将这个任务集群上的功能都拆分成了一系列的函数放到了函数计算上。通过函数计算的特点一下解决了上面的所有问题:
函数计算的计费模式是按照代码实际运行的CPU时间计费,不需要长期维护一个任务集群了。函数计算上的函数运行时尽管会有一些常驻函数的优化,但是基本不用考虑长期运行带来的一系列问题,且每次调用之间都相互独立,不会互相影响。用户的输入代码是运行在一个沙箱容器中,即便不对用户输入做任何过滤,恶意攻击者也拿不到任何敏感信息,同时也无法进入内部网络执行代码,更加安全。
除了上面提到的这些功能之外,语雀最近还使用OSS+函数计算替换了之前使用的阿里云视频点播服务来进行视频和音频的转码。
由于浏览器可以直接支持播放的音视频格式并不多,大量用户上传的视频想要能够直接在语雀上进行播放需要对它们进行转码,业界一般都是通过FFmpeg来对音视频进行转码的。
转码服务也是一个典型的CPU密集型场景,如果要自己搭建视频转码集群会面临大量的资源浪费,而使用阿里云视频点播服务,成本也比较高,而且能够控制的东西也不够多。
函数计算直接集成了FFmpeg提供音视频处理能力,并集成到应用中心,配合SLS完善了监控和数据分析。语雀将音视频处理从视频点播服务迁移到函数计算之后,通过优化压缩率、减少不必要的转码等优化,将费用降低至之前的1/5。
从语雀的实践来看,语雀并没有像SFF一样将Web服务迁移到函数计算之上(SFF模式并不是现在的函数计算架构所擅长的),但是函数计算在语雀整体的架构中对稳定性、安全性和成本控制起到了非常重要的作用。总结下来函数计算非常适合下面几种场景:
对于时效性要求不算非常高的CPU密集型操作,分担主服务CPU压力。当做沙箱环境执行用户提交的代码。运行不稳定的三方应用软件服务。需要很强动态伸缩能力的服务。在引入函数计算之后,语雀现阶段的架构变成了以一个MonolithApplication为核心,并将一些独立的功能模块根据使用场景和对能力的要求分别拆分成了Microservices和Serverless架构。
应用架构与团队成员组成、业务形态息息相关,但是随着各种云服务与基础设施的完善,我们可以更自如的选择更合适的架构。
为什么要特别把Serverless单独拿出来说呢?还记得之前说Node.js是单线程,不适合CPU密集型任务么?
由于Serverless的出现,我们可以将这些存在安全风险的,消耗大量CPU计算的任务都迁移到函数计算上。它运行在沙箱环境中,不用担心用户的恶意代码造成安全风险,同时将这些CPU密集型的任务从主服务中剥离,避免出现并发时阻塞主服务。
按需付费的方式也可以大大节约成本,不需要为低频功能场景部署一个常驻服务。所以我们会尽量的把这类服务都迁移到Serverless上(如阿里云函数计算)。
结语
语雀的技术栈选择
语雀这几年一步步发展过来,背后的技术一直在演进,但是始终遵循了几条原则:
技术栈选型要匹配产品发展阶段。产品在不同的阶段对技术提出的要求是不一样的,越前期,对迭代效率的要求越高,商业化规模化之后,对稳定性、性能的要求就会变高。不需要一上来就用最先进的技术方案,而是需要和产品阶段一起考虑和权衡。技术栈选型要结合团队成员的技术背景。语雀选择JavaScript全栈的原因是孵化语雀的团队,大部分都是JavaScript背景的程序员,同时Node.js在蚂蚁也算是一等公民,配套的设施相对完善。最重要的一点是,不论选择什么技术栈,安全、稳定、可维护(扩展)都是要考虑清楚的。用什么语言、用什么服务会变化,但是这些基础的安全意识、稳定性意识,如何编写可维护的代码,都是决定项目能否长期发展下去的重要因素。本文作者:
何翊宇,花名不四,高级前端技术专家,现就职于蚂蚁金服体验技术部,语雀产品技术负责人。年开始专注在Node.js与Web研发领域,负责过内部的Node.js的模块管理系统和中间件服务等基础设施,也做过Node.jsWeb框架的研发和开源。同时持续在使用Node.js进行产品研发,先后负责过淘宝时光机、天猫搭建渲染服务以及语雀等产品。开源爱好者,Koa.js和Egg.js核心开发者,cnpm中国镜像维护者。