語雀的技術(shù)架構(gòu)演進(jìn)之路
每個技術(shù)人心中或多或少都有一個「產(chǎn)品夢」,好的技術(shù)需要搭配好的產(chǎn)品,才能讓用戶愛不釋手,尤其是做一款知識服務(wù)型產(chǎn)品。
作者何翊宇(花名:不四)是螞蟻金服體驗技術(shù)部高級前端技術(shù)專家,語雀產(chǎn)品技術(shù)負(fù)責(zé)人。本文從技術(shù)架構(gòu)的視角,回顧了語雀的原型、內(nèi)部服務(wù)和對外商業(yè)化的全過程,并對函數(shù)計算在語雀架構(gòu)演進(jìn)過程中所扮演的角色做了詳細(xì)的介紹。
語雀是一個專業(yè)的云端知識庫,用于團(tuán)隊的文檔協(xié)作。現(xiàn)在已是阿里員工進(jìn)行文檔編寫和知識沉淀的標(biāo)配,并于 2018 年開始對外提供服務(wù)。
1
原型階段
| 回到故事的開始。
2016 年,語雀孵化自螞蟻科技,當(dāng)時,螞蟻金融云需要一個工具來承載它的文檔,負(fù)責(zé)的技術(shù)同學(xué)利用業(yè)余時間,搭建了這個文檔工具。項目的初期,沒有任何人員和資源支持,同時也是為了快速驗證原型,技術(shù)選型上選擇了最低成本的方案。
底層服務(wù)完全基于體驗技術(shù)部內(nèi)部提供的 BaaS 服務(wù)和容器托管平臺:
Object 服務(wù):一個類 MongoDB 的數(shù)據(jù)存儲服務(wù);
File 服務(wù):阿里云 OSS 的基礎(chǔ)上封裝的一個文件存儲服務(wù);
DockerLab:一個容器托管平臺;

這些服務(wù)和平臺都是基于 Node.js 實現(xiàn)的,專門給內(nèi)部創(chuàng)新型應(yīng)用使用,也正是由于有這些降低創(chuàng)新成本的內(nèi)部服務(wù),才給工程師們提供了更好的創(chuàng)新環(huán)境。
語雀的應(yīng)用層服務(wù)端,自然而然的選用了螞蟻體驗技術(shù)部開源的 Node.js Web 框架 Egg(螞蟻內(nèi)部的封裝 Chair),通過一個單體 Web 應(yīng)用實現(xiàn)服務(wù)端。應(yīng)用層客戶端也選用了 React 技術(shù)棧,結(jié)合內(nèi)部的 antd,并采用 CodeMirror 實現(xiàn)了一個功能強大、體驗優(yōu)雅的 markdown 在線編輯器。
當(dāng)時僅僅是一個工程師的業(yè)余項目,采用內(nèi)部專為創(chuàng)新應(yīng)用提供的 BaaS 服務(wù)和一系列的開源技術(shù),驗證了在線文檔工具這個產(chǎn)品原型。
2
內(nèi)部服務(wù)階段
2017年,隨著語雀得到團(tuán)隊內(nèi)部的認(rèn)可,他的目標(biāo)已經(jīng)不僅僅是金融云的文檔工具,而是成為阿里所有員工的知識管理平臺。不僅面向技術(shù)人員 Markdown 編輯器,還向非技術(shù)知識創(chuàng)作者,提供了富文本編輯器,并選擇了更“Web”的路線,在富文本編輯器中加入了公式、文本繪圖、思維導(dǎo)圖等特色功能。而隨著語雀在知識管理領(lǐng)域的不斷探索,知識管理的三層結(jié)構(gòu)(團(tuán)隊、知識庫、文檔)開始成型。
| 在此之上的協(xié)作、分享、搜索與消息動態(tài)等功能越來越復(fù)雜單純的依靠 BaaS 服務(wù)已經(jīng)無法滿足語雀的業(yè)務(wù)需求了。
為了應(yīng)對業(yè)務(wù)發(fā)展帶來的挑戰(zhàn),我們主要從下面幾個點進(jìn)行改造:
BaaS 服務(wù)雖然使用簡單成本低,但是它們提供的功能不足以滿足語雀業(yè)務(wù)的發(fā)展,同時穩(wěn)定性上也有不足。所以我們將底層服務(wù)由 BaaS 替換成了阿里云的 IaaS 服務(wù)(MySQL、OSS、緩存、搜索等服務(wù))。
Web 層仍然采用了 Node.js 與 Egg 框架,但是業(yè)務(wù)層借鑒 rails 社區(qū)的實踐開始變成了一個大型單體應(yīng)用,通過引入 ORM 構(gòu)建數(shù)據(jù)模型層,讓代碼的分層更清晰。
前端編輯器從 codeMirror 遷移到 Slate。為了更好的實現(xiàn)語雀編輯器的功能,我們內(nèi)部 fork 了 Slate 進(jìn)行深入開發(fā),同時也自定義了一個獨立的內(nèi)容存儲格式,以提供更高效的數(shù)據(jù)處理和更好的兼容性。

在內(nèi)部服務(wù)階段,語雀已經(jīng)成為了一個正式的產(chǎn)品,通過在阿里內(nèi)部的磨煉,語雀的產(chǎn)品形態(tài)基本定型。
3
對外商業(yè)化階段
隨著語雀在內(nèi)部的影響力越來越大,一些離職出去創(chuàng)業(yè)的阿里校友們開始找到玉伯(螞蟻體驗技術(shù)部研究員):“語雀挺好用的,有沒有考慮商業(yè)化之后讓外面的公司也能夠用起來?”?
| 經(jīng)過小半年的醞釀和重構(gòu),2018 年初,語雀開始正式對外提供服務(wù),進(jìn)行商業(yè)化。
當(dāng)一個應(yīng)用走出公司內(nèi)到商業(yè)化環(huán)境中,面臨的技術(shù)挑戰(zhàn)一下子就變大了。最核心的知識創(chuàng)作管理部分的功能越來越復(fù)雜,表格、思維導(dǎo)圖等新格式的加入,多人實時協(xié)同的需求對編輯器技術(shù)提出了更高的挑戰(zhàn)。而為了更好的服務(wù)企業(yè)用戶與個人用戶,語雀在企業(yè)服務(wù)、會員服務(wù)等方面也投入了很大精力。在業(yè)務(wù)快速發(fā)展的同時,服務(wù)商業(yè)化對質(zhì)量、安全和穩(wěn)定性也提出了更高的要求。
為了應(yīng)對業(yè)務(wù)發(fā)展,語雀的架構(gòu)也隨之發(fā)生了演進(jìn):
我們將底層的依賴完全上云,全部遷移到了阿里云上,阿里云不僅僅提供了基礎(chǔ)的存儲、計算能力,同時也提供了更豐富的高級服務(wù),同時在穩(wěn)定性上也有保障。
豐富的云計算基礎(chǔ)服務(wù),保障語雀的服務(wù)端可以選用最適合語雀業(yè)務(wù)的的存儲、隊列、搜索引擎等基礎(chǔ)服務(wù);
更多人工智能服務(wù)給語雀的產(chǎn)品帶來了更多的可能性,包括 OCR 識圖、智能翻譯等服務(wù),最終都直接轉(zhuǎn)化成為了語雀的特色服務(wù);
而在應(yīng)用層,語雀的服務(wù)端依然還是以一個基于 Egg 框架的大型的 Node.js Web 應(yīng)用為主。但是隨著功能越來越多,也開始將一些相對比較獨立的服務(wù)從主服務(wù)中拆出去,可以把這些服務(wù)分成幾類:
微服務(wù)類:例如多人實時協(xié)同服務(wù),由于它相對獨立,且長連接服務(wù)不適合頻繁發(fā)布,所以我們將其拆成了一個獨立的微服務(wù),保持其穩(wěn)定性。
任務(wù)服務(wù)類:像語雀提供的大量本地文件預(yù)覽服務(wù),會產(chǎn)生一些任務(wù)比較消耗資源、依賴復(fù)雜。我們將其從主服務(wù)中剝離,可以避免不可控的依賴和資源消耗對主服務(wù)造成影響。
函數(shù)計算類:類似 Plantuml 預(yù)覽、Mermaid 預(yù)覽等任務(wù),對響應(yīng)時間的敏感度不高,且依賴可以打包到阿里云函數(shù)計算中,我們會將其放到函數(shù)計算中運行,既省錢又安全。
隨著編輯器越來越復(fù)雜,在 slate 的基礎(chǔ)上進(jìn)行開發(fā)遇到的問題越來越多。最終語雀還是走上了自研編輯器的道路,基于瀏覽器的 Contenteditable 實現(xiàn)了富文本編輯器,通過 Canvas 實現(xiàn)了表格編輯器,通過 SVG 實現(xiàn)了思維導(dǎo)圖編輯器。

語雀的這個階段(也是現(xiàn)在所處的階段)是商業(yè)化階段,但是我們?nèi)匀槐3至艘粋€很小的團(tuán)隊,通過 JavaScript 全棧進(jìn)行研發(fā)。底層的服務(wù)全面上云,借力云服務(wù)打造語雀的特色功能。同時為企業(yè)級用戶和個人知識工作者者提供知識創(chuàng)作和管理工具。
4
和函數(shù)計算的不解之緣
語雀是一個復(fù)雜的 Web 應(yīng)用,也是一個典型的數(shù)據(jù)密集型應(yīng)用(Data-Intensive Application),背后依賴了大量的數(shù)據(jù)庫等云服務(wù)。語雀服務(wù)端是 Node.js 技術(shù)棧。當(dāng)提到 node 的時候,可能立刻就會有幾個詞浮現(xiàn)在我們腦海之中:單線程(single-threaded)、非阻塞(non-blocking)、異步(asynchronously programming),這些特性一方面非常的適合于構(gòu)建可擴展的網(wǎng)絡(luò)應(yīng)用,用來實現(xiàn) Web 服務(wù)這類 I/O 密集型的應(yīng)用,另一方面它也是大家一直對 node 詬病的地方,對 CPU 密集型的場景不夠友好,一旦有任何阻塞進(jìn)程的方法被執(zhí)行,整個進(jìn)程就被阻塞。
阿里云函數(shù)計算是事件驅(qū)動的全托管計算服務(wù)。通過函數(shù)計算,您無需管理服務(wù)器等基礎(chǔ)設(shè)施,只需編寫代碼并上傳,只需要為代碼實際運行所消耗的資源付費,代碼未運行則不產(chǎn)生費用。
把函數(shù)計算引入之后,我們可以將那些 CPU 密集型、存在不穩(wěn)定因素的操作統(tǒng)統(tǒng)放到函數(shù)計算服務(wù)中去執(zhí)行,而我們的主服務(wù)再次回歸到了 I/O 密集型應(yīng)用模型,又可以愉快的享受 node 給我們帶來的高效研發(fā)福利了!
以語雀中遇到的一個實際場景來舉例,用戶傳入了一些 HTML 或者 Markdown 格式的文檔內(nèi)容,我們需要將其轉(zhuǎn)換成為語雀自己的文檔格式。在絕大部分情況下,解析用戶輸入的內(nèi)容都很快,然而依然存在某些無法預(yù)料到的場景會觸發(fā)解析器的 bug 而導(dǎo)致死循環(huán)的出現(xiàn),甚至我們不太敢升級 Markdown 解析庫和相關(guān)插件以免引入更多的問題。但是隨著函數(shù)計算的引入,我們將這個消耗 CPU 的轉(zhuǎn)換邏輯放到函數(shù)計算上,語雀的主服務(wù)穩(wěn)定性不會再被影響。

| 除了幫助 Web 系統(tǒng)分擔(dān)一些 CPU 密集型操作以外,函數(shù)計算還能做什么呢?
在語雀上我們支持各種代碼形式來繪圖,包括 Plantuml、公式、Mermaid,還有一些將文檔導(dǎo)出成 PDF、圖片等功能。這些場景有兩個特點:
他們依賴于一些復(fù)雜的應(yīng)用軟件,例如 Puppeteer、Graphviz 等;
可能需要執(zhí)行用戶輸入的內(nèi)容;
支持這類場景看似簡單,通過 process.exec 子進(jìn)程調(diào)用一下就搞定了。但是當(dāng)我們想把它做成一個穩(wěn)定的對外服務(wù)時,問題就出現(xiàn)了。這些復(fù)雜的應(yīng)用軟件可能從設(shè)計上并沒有考慮要長期運行,長期運行時的內(nèi)存占用、穩(wěn)定性可能會有一些問題,同時在被大并發(fā)調(diào)用時,對 CPU 的壓力非常大。再加上有些場景需要運行用戶輸入的代碼,攻擊者通過構(gòu)建惡意輸入,可以在服務(wù)器上運行攻擊代碼,非常危險。
在沒有引入函數(shù)計算之前,語雀為了支持這些功能,盡管單獨分配了一個任務(wù)集群,在上面運行這些三方服務(wù),接受主服務(wù)的請求來避免影響主服務(wù)的穩(wěn)定性。但是為了解決上面提到的一系列問題還需要付出很大的成本:
需要維持一個不小的任務(wù)集群,盡管可能大部分時間都用不上那么多資源。
需要定時對三方應(yīng)用軟件進(jìn)行重啟,避免長時間運行帶來的內(nèi)存泄露,即便如此有些特殊請求也會造成第三方軟件的不穩(wěn)定。
對用戶的輸入進(jìn)行檢測和過濾,防止黑客惡意攻擊,而黑客的攻擊代碼很難完全防住,安全風(fēng)險依舊很大。

最后語雀將所有的第三方服務(wù)都分別打包在函數(shù)中,將這個任務(wù)集群上的功能都拆分成了一系列的函數(shù)放到了函數(shù)計算上。通過函數(shù)計算的特點一下解決了上面的所有問題:
函數(shù)計算的計費模式是按照代碼實際運行的 CPU 時間計費,不需要長期維護(hù)一個任務(wù)集群了。
函數(shù)計算上的函數(shù)運行時盡管會有一些常駐函數(shù)的優(yōu)化,但是基本不用考慮長期運行帶來的一系列問題,且每次調(diào)用之間都相互獨立,不會互相影響。
用戶的輸入代碼是運行在一個沙箱容器中,即便不對用戶輸入做任何過濾,惡意攻擊者也拿不到任何敏感信息,同時也無法進(jìn)入內(nèi)部網(wǎng)絡(luò)執(zhí)行代碼,更加安全。

| 除了上面提到的這些功能之外,語雀最近還使用 OSS + 函數(shù)計算替換了之前使用的阿里云視頻點播服務(wù)來進(jìn)行視頻和音頻的轉(zhuǎn)碼。

從語雀的實踐來看,語雀并沒有像 SFF 一樣將 Web 服務(wù)遷移到函數(shù)計算之上(SFF 模式并不是現(xiàn)在的函數(shù)計算架構(gòu)所擅長的),但是函數(shù)計算在語雀整體的架構(gòu)中對穩(wěn)定性、安全性和成本控制起到了非常重要的作用。總結(jié)下來函數(shù)計算非常適合下面幾種場景:
對于時效性要求不算非常高的 CPU 密集型操作,分擔(dān)主服務(wù) CPU 壓力。
當(dāng)做沙箱環(huán)境執(zhí)行用戶提交的代碼。
運行不穩(wěn)定的三方應(yīng)用軟件服務(wù)。
需要很強動態(tài)伸縮能力的服務(wù)。
在引入函數(shù)計算之后,語雀現(xiàn)階段的架構(gòu)變成了以一個 Monolith Application 為核心,并將一些獨立的功能模塊根據(jù)使用場景和對能力的要求分別拆分成了 Microservices 和 Serverless 架構(gòu)。應(yīng)用架構(gòu)與團(tuán)隊成員組成、業(yè)務(wù)形態(tài)息息相關(guān),但是隨著各種云服務(wù)與基礎(chǔ)設(shè)施的完善,我們可以更自如的選擇更合適的架構(gòu)。

為什么要特別把 Serverless 單獨拿出來說呢?還記得之前說 Node.js 是單線程,不適合 CPU 密集型任務(wù)么?
由于 Serverless 的出現(xiàn),我們可以將這些存在安全風(fēng)險的,消耗大量 CPU 計算的任務(wù)都遷移到函數(shù)計算上。它運行在沙箱環(huán)境中,不用擔(dān)心用戶的惡意代碼造成安全風(fēng)險,同時將這些 CPU 密集型的任務(wù)從主服務(wù)中剝離,避免出現(xiàn)并發(fā)時阻塞主服務(wù)。按需付費的方式也可以大大節(jié)約成本,不需要為低頻功能場景部署一個常駐服務(wù)。所以我們會盡量的把這類服務(wù)都遷移到 Serverless 上(如阿里云函數(shù)計算)。
5
結(jié)語 | 語雀的技術(shù)棧選擇
語雀這幾年一步步發(fā)展過來,背后的技術(shù)一直在演進(jìn),但是始終遵循了幾條原則:
技術(shù)棧選型要匹配產(chǎn)品發(fā)展階段。產(chǎn)品在不同的階段對技術(shù)提出的要求是不一樣的,越前期,對迭代效率的要求越高,商業(yè)化規(guī)模化之后,對穩(wěn)定性、性能的要求就會變高。不需要一上來就用最先進(jìn)的技術(shù)方案,而是需要和產(chǎn)品階段一起考慮和權(quán)衡。
技術(shù)棧選型要結(jié)合團(tuán)隊成員的技術(shù)背景。語雀選擇 JavaScript 全棧的原因是孵化語雀的團(tuán)隊,大部分都是 JavaScript 背景的程序員,同時 Node.js 在螞蟻也算是一等公民,配套的設(shè)施相對完善。
最重要的一點是,不論選擇什么技術(shù)棧,安全、穩(wěn)定、可維護(hù)(擴展)都是要考慮清楚的。用什么語言、用什么服務(wù)會變化,但是這些基礎(chǔ)的安全意識、穩(wěn)定性意識,如何編寫可維護(hù)的代碼,都是決定項目能否長期發(fā)展下去的重要因素。
本文作者:
何翊宇,花名不四,高級前端技術(shù)專家,現(xiàn)就職于螞蟻金服體驗技術(shù)部,語雀產(chǎn)品技術(shù)負(fù)責(zé)人。2011 年開始專注在 Node.js 與 Web 研發(fā)領(lǐng)域,負(fù)責(zé)過內(nèi)部的 Node.js 的模塊管理系統(tǒng)和中間件服務(wù)等基礎(chǔ)設(shè)施,也做過 Node.js Web 框架的研發(fā)和開源。同時持續(xù)在使用 Node.js 進(jìn)行產(chǎn)品研發(fā),先后負(fù)責(zé)過淘寶時光機、天貓搭建渲染服務(wù)以及語雀等產(chǎn)品。開源愛好者,Koa.js 和 Egg.js 核心開發(fā)者,cnpm 中國鏡像維護(hù)者。
往期推薦
后臺回復(fù)?學(xué)習(xí)資料?領(lǐng)取學(xué)習(xí)視頻
如有收獲,點個在看,誠摯感謝
