作者 | 不四
來源 | 阿里巴巴中間件(ID:Aliware_2018)
每個技術人心中或多或少都有一個「產(chǎn)品夢」,好的技術需要搭配好的產(chǎn)品,才能讓用戶愛不釋手,尤其是做一款知識服務型產(chǎn)品。
本文從技術架構的視角,回顧了語雀的原型、內部服務和對外商業(yè)化的全過程,并對函數(shù)計算在語雀架構演進過程中所扮演的角色做了詳細的介紹。
語雀是一個專業(yè)的云端知識庫,用于團隊的文檔協(xié)作。現(xiàn)在已是阿里員工進行文檔編寫和知識沉淀的標配,并于 2018 年開始對外提供服務。
原型階段
| 回到故事的開始。
2016 年,語雀孵化自螞蟻科技,當時,螞蟻金融云需要一個工具來承載它的文檔,負責的技術同學利用業(yè)余時間,搭建了這個文檔工具。項目的初期,沒有任何人員和資源支持,同時也是為了快速驗證原型,技術選型上選擇了最低成本的方案。
底層服務完全基于體驗技術部內部提供的 BaaS 服務和容器托管平臺:
-
Object 服務:一個類 MongoDB 的數(shù)據(jù)存儲服務;
-
File 服務:阿里云 OSS 的基礎上封裝的一個文件存儲服務;
-
DockerLab:一個容器托管平臺;
這些服務和平臺都是基于 Node.js 實現(xiàn)的,專門給內部創(chuàng)新型應用使用,也正是由于有這些降低創(chuàng)新成本的內部服務,才給工程師們提供了更好的創(chuàng)新環(huán)境。
語雀的應用層服務端,自然而然的選用了螞蟻體驗技術部開源的 Node.js Web 框架 Egg(螞蟻內部的封裝 Chair),通過一個單體 Web 應用實現(xiàn)服務端。
應用層客戶端也選用了 React 技術棧,結合內部的 antd,并采用 CodeMirror 實現(xiàn)了一個功能強大、體驗優(yōu)雅的 markdown 在線編輯器。
當時僅僅是一個工程師的業(yè)余項目,采用內部專為創(chuàng)新應用提供的 BaaS 服務和一系列的開源技術,驗證了在線文檔工具這個產(chǎn)品原型。
內部服務階段
2017年,隨著語雀得到團隊內部的認可,他的目標已經(jīng)不僅僅是金融云的文檔工具,而是成為阿里所有員工的知識管理平臺。
不僅面向技術人員 Markdown 編輯器,還向非技術知識創(chuàng)作者,提供了富文本編輯器,并選擇了更“Web”的路線,在富文本編輯器中加入了公式、文本繪圖、思維導圖等特色功能。
而隨著語雀在知識管理領域的不斷探索,知識管理的三層結構(團隊、知識庫、文檔)開始成型。
| 在此之上的協(xié)作、分享、搜索與消息動態(tài)等功能越來越復雜單純的依靠 BaaS 服務已經(jīng)無法滿足語雀的業(yè)務需求了。
為了應對業(yè)務發(fā)展帶來的挑戰(zhàn),我們主要從下面幾個點進行改造:
-
BaaS 服務雖然使用簡單成本低,但是它們提供的功能不足以滿足語雀業(yè)務的發(fā)展,同時穩(wěn)定性上也有不足。所以我們將底層服務由 BaaS 替換成了阿里云的 IaaS 服務(MySQL、OSS、緩存、搜索等服務)。
-
Web 層仍然采用了 Node.js 與 Egg 框架,但是業(yè)務層借鑒 rails 社區(qū)的實踐開始變成了一個大型單體應用,通過引入 ORM 構建數(shù)據(jù)模型層,讓代碼的分層更清晰。
-
前端編輯器從 codeMirror 遷移到 Slate。為了更好的實現(xiàn)語雀編輯器的功能,我們內部 fork 了 Slate 進行深入開發(fā),同時也自定義了一個獨立的內容存儲格式,以提供更高效的數(shù)據(jù)處理和更好的兼容性。
在內部服務階段,語雀已經(jīng)成為了一個正式的產(chǎn)品,通過在阿里內部的磨煉,語雀的產(chǎn)品形態(tài)基本定型。
對外商業(yè)化階段
隨著語雀在內部的影響力越來越大,一些離職出去創(chuàng)業(yè)的阿里校友們開始找到玉伯(螞蟻體驗技術部研究員):“語雀挺好用的,有沒有考慮商業(yè)化之后讓外面的公司也能夠用起來?”
| 經(jīng)過小半年的醞釀和重構,2018 年初,語雀開始正式對外提供服務,進行商業(yè)化。
當一個應用走出公司內到商業(yè)化環(huán)境中,面臨的技術挑戰(zhàn)一下子就變大了。最核心的知識創(chuàng)作管理部分的功能越來越復雜,表格、思維導圖等新格式的加入,多人實時協(xié)同的需求對編輯器技術提出了更高的挑戰(zhàn)。
而為了更好的服務企業(yè)用戶與個人用戶,語雀在企業(yè)服務、會員服務等方面也投入了很大精力。在業(yè)務快速發(fā)展的同時,服務商業(yè)化對質量、安全和穩(wěn)定性也提出了更高的要求。
為了應對業(yè)務發(fā)展,語雀的架構也隨之發(fā)生了演進:
我們將底層的依賴完全上云,全部遷移到了阿里云上,阿里云不僅僅提供了基礎的存儲、計算能力,同時也提供了更豐富的高級服務,同時在穩(wěn)定性上也有保障。
-
豐富的云計算基礎服務,保障語雀的服務端可以選用最適合語雀業(yè)務的的存儲、隊列、搜索引擎等基礎服務;
-
更多人工智能服務給語雀的產(chǎn)品帶來了更多的可能性,包括 OCR 識圖、智能翻譯等服務,最終都直接轉化成為了語雀的特色服務;
而在應用層,語雀的服務端依然還是以一個基于 Egg 框架的大型的 Node.js Web 應用為主。但是隨著功能越來越多,也開始將一些相對比較獨立的服務從主服務中拆出去,可以把這些服務分成幾類:
-
微服務類:例如多人實時協(xié)同服務,由于它相對獨立,且長連接服務不適合頻繁發(fā)布,所以我們將其拆成了一個獨立的微服務,保持其穩(wěn)定性。
-
任務服務類:像語雀提供的大量本地文件預覽服務,會產(chǎn)生一些任務比較消耗資源、依賴復雜。我們將其從主服務中剝離,可以避免不可控的依賴和資源消耗對主服務造成影響。
-
函數(shù)計算類:類似 Plantuml 預覽、Mermaid 預覽等任務,對響應時間的敏感度不高,且依賴可以打包到阿里云函數(shù)計算中,我們會將其放到函數(shù)計算中運行,既省錢又安全。
隨著編輯器越來越復雜,在 slate 的基礎上進行開發(fā)遇到的問題越來越多。最終語雀還是走上了自研編輯器的道路,基于瀏覽器的 Contenteditable 實現(xiàn)了富文本編輯器,通過 Canvas 實現(xiàn)了表格編輯器,通過 SVG 實現(xiàn)了思維導圖編輯器。
語雀的這個階段(也是現(xiàn)在所處的階段)是商業(yè)化階段,但是我們仍然保持了一個很小的團隊,通過 JAVAScript 全棧進行研發(fā)。底層的服務全面上云,借力云服務打造語雀的特色功能。同時為企業(yè)級用戶和個人知識工作者者提供知識創(chuàng)作和管理工具。
和函數(shù)計算的不解之緣
語雀是一個復雜的 Web 應用,也是一個典型的數(shù)據(jù)密集型應用(Data-Intensive Application),背后依賴了大量的數(shù)據(jù)庫等云服務。語雀服務端是 Node.js 技術棧。
當提到 node 的時候,可能立刻就會有幾個詞浮現(xiàn)在我們腦海之中:單線程(single-threaded)、非阻塞(non-blocking)、異步(asynchronously programming),這些特性一方面非常的適合于構建可擴展的網(wǎng)絡應用,用來實現(xiàn) Web 服務這類 I/O 密集型的應用,另一方面它也是大家一直對 node 詬病的地方,對 CPU 密集型的場景不夠友好,一旦有任何阻塞進程的方法被執(zhí)行,整個進程就被阻塞。
像語雀這樣用 node 實現(xiàn)整個服務端邏輯的應用,很難保證不會出現(xiàn)一些場景可能會消耗大量 CPU 甚至是死循環(huán)阻塞進程的,例如以 markdown 轉換舉例,由于用戶的輸入無法窮舉,總有各種可能讓轉換代碼進入到一個低效甚至是死循環(huán)的場景之中。
在 node 剛出世的年代,很難給這些問題找到完美的解決辦法,而即便是 Java 等基于線程并發(fā)模型的語言,在遇到這樣的場景也很頭痛,畢竟 CPU 對于 web 應用來說都是非常重要的資源。而隨著基礎設置越來越完善,當函數(shù)計算出現(xiàn)時,node 最大的短板看起來有了一個比較完美的解決方案。
阿里云函數(shù)計算是事件驅動的全托管計算服務。通過函數(shù)計算,您無需管理服務器等基礎設施,只需編寫代碼并上傳,只需要為代碼實際運行所消耗的資源付費,代碼未運行則不產(chǎn)生費用。
把函數(shù)計算引入之后,我們可以將那些 CPU 密集型、存在不穩(wěn)定因素的操作統(tǒng)統(tǒng)放到函數(shù)計算服務中去執(zhí)行,而我們的主服務再次回歸到了 I/O 密集型應用模型,又可以愉快的享受 node 給我們帶來的高效研發(fā)福利了!
以語雀中遇到的一個實際場景來舉例,用戶傳入了一些 html 或者 Markdown 格式的文檔內容,我們需要將其轉換成為語雀自己的文檔格式。
在絕大部分情況下,解析用戶輸入的內容都很快,然而依然存在某些無法預料到的場景會觸發(fā)解析器的 bug 而導致死循環(huán)的出現(xiàn),甚至我們不太敢升級 Markdown 解析庫和相關插件以免引入更多的問題。
但是隨著函數(shù)計算的引入,我們將這個消耗 CPU 的轉換邏輯放到函數(shù)計算上,語雀的主服務穩(wěn)定性不會再被影響。
| 除了幫助 Web 系統(tǒng)分擔一些 CPU 密集型操作以外,函數(shù)計算還能做什么呢?
在語雀上我們支持各種代碼形式來繪圖,包括 Plantuml、公式、Mermaid,還有一些將文檔導出成 PDF、圖片等功能。這些場景有兩個特點:
-
他們依賴于一些復雜的應用軟件,例如 Puppeteer、Graphviz 等;
-
可能需要執(zhí)行用戶輸入的內容;
支持這類場景看似簡單,通過 process.exec
子進程調用一下就搞定了。但是當我們想把它做成一個穩(wěn)定的對外服務時,問題就出現(xiàn)了。這些復雜的應用軟件可能從設計上并沒有考慮要長期運行,長期運行時的內存占用、穩(wěn)定性可能會有一些問題,同時在被大并發(fā)調用時,對 CPU 的壓力非常大。
再加上有些場景需要運行用戶輸入的代碼,攻擊者通過構建惡意輸入,可以在服務器上運行攻擊代碼,非常危險。
在沒有引入函數(shù)計算之前,語雀為了支持這些功能,盡管單獨分配了一個任務集群,在上面運行這些三方服務,接受主服務的請求來避免影響主服務的穩(wěn)定性。但是為了解決上面提到的一系列問題還需要付出很大的成本:
-
需要維持一個不小的任務集群,盡管可能大部分時間都用不上那么多資源。
-
需要定時對三方應用軟件進行重啟,避免長時間運行帶來的內存泄露,即便如此有些特殊請求也會造成第三方軟件的不穩(wěn)定。
-
對用戶的輸入進行檢測和過濾,防止黑客惡意攻擊,而黑客的攻擊代碼很難完全防住,安全風險依舊很大。
最后語雀將所有的第三方服務都分別打包在函數(shù)中,將這個任務集群上的功能都拆分成了一系列的函數(shù)放到了函數(shù)計算上。通過函數(shù)計算的特點一下解決了上面的所有問題:
-
函數(shù)計算的計費模式是按照代碼實際運行的 CPU 時間計費,不需要長期維護一個任務集群了。
-
函數(shù)計算上的函數(shù)運行時盡管會有一些常駐函數(shù)的優(yōu)化,但是基本不用考慮長期運行帶來的一系列問題,且每次調用之間都相互獨立,不會互相影響。
-
用戶的輸入代碼是運行在一個沙箱容器中,即便不對用戶輸入做任何過濾,惡意攻擊者也拿不到任何敏感信息,同時也無法進入內部網(wǎng)絡執(zhí)行代碼,更加安全。
| 除了上面提到的這些功能之外,語雀最近還使用 OSS + 函數(shù)計算替換了之前使用的阿里云視頻點播服務來進行視頻和音頻的轉碼。
由于瀏覽器可以直接支持播放的音視頻格式并不多,大量用戶上傳的視頻想要能夠直接在語雀上進行播放需要對它們進行轉碼,業(yè)界一般都是通過 FFmpeg 來對音視頻進行轉碼的。
轉碼服務也是一個典型的 CPU 密集型場景,如果要自己搭建視頻轉碼集群會面臨大量的資源浪費,而使用阿里云視頻點播服務,成本也比較高,而且能夠控制的東西也不夠多。
函數(shù)計算直接集成了 FFmpeg 提供音視頻處理能力,并集成到應用中心,配合 SLS 完善了監(jiān)控和數(shù)據(jù)分析。語雀將音視頻處理從視頻點播服務遷移到函數(shù)計算之后,通過優(yōu)化壓縮率、減少不必要的轉碼等優(yōu)化,將費用降低至之前的 1/5。
從語雀的實踐來看,語雀并沒有像 SFF 一樣將 Web 服務遷移到函數(shù)計算之上(SFF 模式并不是現(xiàn)在的函數(shù)計算架構所擅長的),但是函數(shù)計算在語雀整體的架構中對穩(wěn)定性、安全性和成本控制起到了非常重要的作用。總結下來函數(shù)計算非常適合下面幾種場景:
-
對于時效性要求不算非常高的 CPU 密集型操作,分擔主服務 CPU 壓力。
-
當做沙箱環(huán)境執(zhí)行用戶提交的代碼。
-
運行不穩(wěn)定的三方應用軟件服務。
-
需要很強動態(tài)伸縮能力的服務。
在引入函數(shù)計算之后,語雀現(xiàn)階段的架構變成了以一個 Monolith Application 為核心,并將一些獨立的功能模塊根據(jù)使用場景和對能力的要求分別拆分成了 Microservices 和 Serverless 架構。
應用架構與團隊成員組成、業(yè)務形態(tài)息息相關,但是隨著各種云服務與基礎設施的完善,我們可以更自如的選擇更合適的架構。
為什么要特別把 Serverless 單獨拿出來說呢?還記得之前說 Node.js 是單線程,不適合 CPU 密集型任務么?
由于 Serverless 的出現(xiàn),我們可以將這些存在安全風險的,消耗大量 CPU 計算的任務都遷移到函數(shù)計算上。它運行在沙箱環(huán)境中,不用擔心用戶的惡意代碼造成安全風險,同時將這些 CPU 密集型的任務從主服務中剝離,避免出現(xiàn)并發(fā)時阻塞主服務。
按需付費的方式也可以大大節(jié)約成本,不需要為低頻功能場景部署一個常駐服務。所以我們會盡量的把這類服務都遷移到 Serverless 上(如阿里云函數(shù)計算)。
結語 | 語雀的技術棧選擇
語雀這幾年一步步發(fā)展過來,背后的技術一直在演進,但是始終遵循了幾條原則:
-
技術棧選型要匹配產(chǎn)品發(fā)展階段。產(chǎn)品在不同的階段對技術提出的要求是不一樣的,越前期,對迭代效率的要求越高,商業(yè)化規(guī)模化之后,對穩(wěn)定性、性能的要求就會變高。不需要一上來就用最先進的技術方案,而是需要和產(chǎn)品階段一起考慮和權衡。
-
技術棧選型要結合團隊成員的技術背景。語雀選擇 JavaScript 全棧的原因是孵化語雀的團隊,大部分都是 JavaScript 背景的程序員,同時 Node.js 在螞蟻也算是一等公民,配套的設施相對完善。
-
最重要的一點是,不論選擇什么技術棧,安全、穩(wěn)定、可維護(擴展)都是要考慮清楚的。用什么語言、用什么服務會變化,但是這些基礎的安全意識、穩(wěn)定性意識,如何編寫可維護的代碼,都是決定項目能否長期發(fā)展下去的重要因素。
本文作者:
何翊宇,花名不四,高級前端技術專家,現(xiàn)就職于螞蟻金服體驗技術部,語雀產(chǎn)品技術負責人。2011 年開始專注在 Node.js 與 Web 研發(fā)領域,負責過內部的 Node.js 的模塊管理系統(tǒng)和中間件服務等基礎設施,也做過 Node.js Web 框架的研發(fā)和開源。同時持續(xù)在使用 Node.js 進行產(chǎn)品研發(fā),先后負責過淘寶時光機、天貓搭建渲染服務以及語雀等產(chǎn)品。開源愛好者,Koa.js 和 Egg.js 核心開發(fā)者,cnpm 中國鏡像維護者。