雲計算

語雀的技術架構演進之路

作者 | 不四

每個技術人心中或多或少都有一個「產品夢」,好的技術需要搭配好的產品,才能讓用戶愛不釋手,尤其是做一款知識服務型產品。

作者何翊宇(花名:不四,微信:dead_horse)是螞蟻金服體驗技術部高級前端技術專家,語雀產品技術負責人。本文從技術架構的視角,回顧了語雀的原型、內部服務和對外商業化的全過程,並對函數計算在語雀架構演進過程中所扮演的角色做了詳細的介紹。

語雀是一個專業的雲端知識庫,用於團隊的文檔協作。現在已是阿里員工進行文檔編寫和知識沉澱的標配,並於 2018 年開始對外提供服務。

原型階段

回到故事的開始。

2016 年,語雀孵化自螞蟻科技,當時,螞蟻金融雲需要一個工具來承載它的文檔,負責的技術同學利用業餘時間,搭建了這個文檔工具。項目的初期,沒有任何人員和資源支持,同時也是為了快速驗證原型,技術選型上選擇了最低成本的方案。

底層服務完全基於體驗技術部內部提供的 BaaS 服務和容器託管平臺:

  • Object 服務:一個類 MongoDB 的數據存儲服務;
  • File 服務:阿里雲 OSS 的基礎上封裝的一個文件存儲服務;
  • DockerLab:一個容器託管平臺;

image.png
這些服務和平臺都是基於 Node.js 實現的,專門給內部創新型應用使用,也正是由於有這些降低創新成本的內部服務,才給工程師們提供了更好的創新環境。

語雀的應用層服務端,自然而然的選用了螞蟻體驗技術部開源的 Node.js Web 框架 Egg(螞蟻內部的封裝 Chair),通過一個單體 Web 應用實現服務端。應用層客戶端也選用了 React 技術棧,結合內部的 antd,並採用 CodeMirror 實現了一個功能強大、體驗優雅的 markdown 在線編輯器。

當時僅僅是一個工程師的業餘項目,採用內部專為創新應用提供的 BaaS 服務和一系列的開源技術,驗證了在線文檔工具這個產品原型。

內部服務階段

2017年,隨著語雀得到團隊內部的認可,他的目標已經不僅僅是金融雲的文檔工具,而是成為阿里所有員工的知識管理平臺。不僅面向技術人員 Markdown 編輯器,還向非技術知識創作者,提供了富文本編輯器,並選擇了更“Web”的路線,在富文本編輯器中加入了公式、文本繪圖、思維導圖等特色功能。而隨著語雀在知識管理領域的不斷探索,知識管理的三層結構(團隊、知識庫、文檔)開始成型。

在此之上的協作、分享、搜索與消息動態等功能越來越複雜單純的依靠 BaaS 服務已經無法滿足語雀的業務需求了。

為了應對業務發展帶來的挑戰,我們主要從下面幾個點進行改造:

  • BaaS 服務雖然使用簡單成本低,但是它們提供的功能不足以滿足語雀業務的發展,同時穩定性上也有不足。所以我們將底層服務由 BaaS 替換成了阿里雲的 IaaS 服務(MySQL、OSS、緩存、搜索等服務)。
  • Web 層仍然採用了 Node.js 與 Egg 框架,但是業務層借鑑 rails 社區的實踐開始變成了一個大型單體應用,通過引入 ORM 構建數據模型層,讓代碼的分層更清晰。
  • 前端編輯器從 codeMirror 遷移到 Slate。為了更好的實現語雀編輯器的功能,我們內部 fork 了 Slate 進行深入開發,同時也自定義了一個獨立的內容存儲格式,以提供更高效的數據處理和更好的兼容性。

image.png
在內部服務階段,語雀已經成為了一個正式的產品,通過在阿里內部的磨鍊,語雀的產品形態基本定型。

對外商業化階段

隨著語雀在內部的影響力越來越大,一些離職出去創業的阿里校友們開始找到玉伯(螞蟻體驗技術部研究員):“語雀挺好用的,有沒有考慮商業化之後讓外面的公司也能夠用起來?”

經過小半年的醞釀和重構,2018 年初,語雀開始正式對外提供服務,進行商業化。

當一個應用走出公司內到商業化環境中,面臨的技術挑戰一下子就變大了。最核心的知識創作管理部分的功能越來越複雜,表格、思維導圖等新格式的加入,多人實時協同的需求對編輯器技術提出了更高的挑戰。而為了更好的服務企業用戶與個人用戶,語雀在企業服務、會員服務等方面也投入了很大精力。在業務快速發展的同時,服務商業化對質量、安全和穩定性也提出了更高的要求。

為了應對業務發展,語雀的架構也隨之發生了演進:

我們將底層的依賴完全上雲,全部遷移到了阿里雲上,阿里雲不僅僅提供了基礎的存儲、計算能力,同時也提供了更豐富的高級服務,同時在穩定性上也有保障。

  • 豐富的雲計算基礎服務,保障語雀的服務端可以選用最適合語雀業務的的存儲、隊列、搜索引擎等基礎服務;
  • 更多人工智能服務給語雀的產品帶來了更多的可能性,包括 OCR 識圖、智能翻譯等服務,最終都直接轉化成為了語雀的特色服務;

而在應用層,語雀的服務端依然還是以一個基於 Egg 框架的大型的 Node.js Web 應用為主。但是隨著功能越來越多,也開始將一些相對比較獨立的服務從主服務中拆出去,可以把這些服務分成幾類:

  • 微服務類:例如多人實時協同服務,由於它相對獨立,且長連接服務不適合頻繁發佈,所以我們將其拆成了一個獨立的微服務,保持其穩定性。
  • 任務服務類:像語雀提供的大量本地文件預覽服務,會產生一些任務比較消耗資源、依賴複雜。我們將其從主服務中剝離,可以避免不可控的依賴和資源消耗對主服務造成影響。
  • 函數計算類:類似 Plantuml 預覽、Mermaid 預覽等任務,對響應時間的敏感度不高,且依賴可以打包到阿里雲函數計算中,我們會將其放到函數計算中運行,既省錢又安全。

隨著編輯器越來越複雜,在 slate 的基礎上進行開發遇到的問題越來越多。最終語雀還是走上了自研編輯器的道路,基於瀏覽器的 Contenteditable 實現了富文本編輯器,通過 Canvas 實現了表格編輯器,通過 SVG 實現了思維導圖編輯器。
image.png
語雀的這個階段(也是現在所處的階段)是商業化階段,但是我們仍然保持了一個很小的團隊,通過 JavaScript 全棧進行研發。底層的服務全面上雲,借力雲服務打造語雀的特色功能。同時為企業級用戶和個人知識工作者者提供知識創作和管理工具。

和函數計算的不解之緣

語雀是一個複雜的 Web 應用,也是一個典型的數據密集型應用(Data-Intensive Application),背後依賴了大量的數據庫等雲服務。語雀服務端是 Node.js 技術棧。當提到 node 的時候,可能立刻就會有幾個詞浮現在我們腦海之中:單線程(single-threaded)、非阻塞(non-blocking)、異步(asynchronously programming),這些特性一方面非常的適合於構建可擴展的網絡應用,用來實現 Web 服務這類 I/O 密集型的應用,另一方面它也是大家一直對 node 詬病的地方,對 CPU 密集型的場景不夠友好,一旦有任何阻塞進程的方法被執行,整個進程就被阻塞。

像語雀這樣用 node 實現整個服務端邏輯的應用,很難保證不會出現一些場景可能會消耗大量 CPU 甚至是死循環阻塞進程的,例如以 markdown 轉換舉例,由於用戶的輸入無法窮舉,總有各種可能讓轉換代碼進入到一個低效甚至是死循環的場景之中。在 node 剛出世的年代,很難給這些問題找到完美的解決辦法,而即便是 Java 等基於線程併發模型的語言,在遇到這樣的場景也很頭痛,畢竟 CPU 對於 web 應用來說都是非常重要的資源。而隨著基礎設置越來越完善,當函數計算出現時,node 最大的短板看起來有了一個比較完美的解決方案。

阿里雲函數計算是事件驅動的全託管計算服務。通過函數計算,您無需管理服務器等基礎設施,只需編寫代碼並上傳,只需要為代碼實際運行所消耗的資源付費,代碼未運行則不產生費用。

把函數計算引入之後,我們可以將那些 CPU 密集型、存在不穩定因素的操作統統放到函數計算服務中去執行,而我們的主服務再次迴歸到了 I/O 密集型應用模型,又可以愉快的享受 node 給我們帶來的高效研發福利了!

以語雀中遇到的一個實際場景來舉例,用戶傳入了一些 HTML 或者 Markdown 格式的文檔內容,我們需要將其轉換成為語雀自己的文檔格式。在絕大部分情況下,解析用戶輸入的內容都很快,然而依然存在某些無法預料到的場景會觸發解析器的 bug 而導致死循環的出現,甚至我們不太敢升級 Markdown 解析庫和相關插件以免引入更多的問題。但是隨著函數計算的引入,我們將這個消耗 CPU 的轉換邏輯放到函數計算上,語雀的主服務穩定性不會再被影響。
image.png

除了幫助 Web 系統分擔一些 CPU 密集型操作以外,函數計算還能做什麼呢?

在語雀上我們支持各種代碼形式來繪圖,包括 Plantuml、公式、Mermaid,還有一些將文檔導出成 PDF、圖片等功能。這些場景有兩個特點:

  • 他們依賴於一些複雜的應用軟件,例如 Puppeteer、Graphviz 等;
  • 可能需要執行用戶輸入的內容;

支持這類場景看似簡單,通過 process.exec 子進程調用一下就搞定了。但是當我們想把它做成一個穩定的對外服務時,問題就出現了。這些複雜的應用軟件可能從設計上並沒有考慮要長期運行,長期運行時的內存佔用、穩定性可能會有一些問題,同時在被大併發調用時,對 CPU 的壓力非常大。再加上有些場景需要運行用戶輸入的代碼,攻擊者通過構建惡意輸入,可以在服務器上運行攻擊代碼,非常危險。

在沒有引入函數計算之前,語雀為了支持這些功能,儘管單獨分配了一個任務集群,在上面運行這些三方服務,接受主服務的請求來避免影響主服務的穩定性。但是為了解決上面提到的一系列問題還需要付出很大的成本:

  • 需要維持一個不小的任務集群,儘管可能大部分時間都用不上那麼多資源。
  • 需要定時對三方應用軟件進行重啟,避免長時間運行帶來的內存洩露,即便如此有些特殊請求也會造成第三方軟件的不穩定。
  • 對用戶的輸入進行檢測和過濾,防止黑客惡意攻擊,而黑客的攻擊代碼很難完全防住,安全風險依舊很大。

image.png
最後語雀將所有的第三方服務都分別打包在函數中,將這個任務集群上的功能都拆分成了一系列的函數放到了函數計算上。通過函數計算的特點一下解決了上面的所有問題:

  • 函數計算的計費模式是按照代碼實際運行的 CPU 時間計費,不需要長期維護一個任務集群了。
  • 函數計算上的函數運行時儘管會有一些常駐函數的優化,但是基本不用考慮長期運行帶來的一系列問題,且每次調用之間都相互獨立,不會互相影響。
  • 用戶的輸入代碼是運行在一個沙箱容器中,即便不對用戶輸入做任何過濾,惡意攻擊者也拿不到任何敏感信息,同時也無法進入內部網絡執行代碼,更加安全。

image.png

除了上面提到的這些功能之外,語雀最近還使用 OSS + 函數計算替換了之前使用的阿里雲視頻點播服務來進行視頻和音頻的轉碼。

由於瀏覽器可以直接支持播放的音視頻格式並不多,大量用戶上傳的視頻想要能夠直接在語雀上進行播放需要對它們進行轉碼,業界一般都是通過 FFmpeg 來對音視頻進行轉碼的。轉碼服務也是一個典型的 CPU 密集型場景,如果要自己搭建視頻轉碼集群會面臨大量的資源浪費,而使用阿里雲視頻點播服務,成本也比較高,而且能夠控制的東西也不夠多。函數計算直接集成了 FFmpeg 提供音視頻處理能力,並集成到應用中心,配合 SLS 完善了監控和數據分析。語雀將音視頻處理從視頻點播服務遷移到函數計算之後,通過優化壓縮率、減少不必要的轉碼等優化,將費用降低至之前的 1/5。
image.png
從語雀的實踐來看,語雀並沒有像 SFF 一樣將 Web 服務遷移到函數計算之上(SFF 模式並不是現在的函數計算架構所擅長的),但是函數計算在語雀整體的架構中對穩定性、安全性和成本控制起到了非常重要的作用。總結下來函數計算非常適合下面幾種場景:

  • 對於時效性要求不算非常高的 CPU 密集型操作,分擔主服務 CPU 壓力。
  • 當做沙箱環境執行用戶提交的代碼。
  • 運行不穩定的三方應用軟件服務。
  • 需要很強動態伸縮能力的服務。

在引入函數計算之後,語雀現階段的架構變成了以一個 Monolith Application 為核心,並將一些獨立的功能模塊根據使用場景和對能力的要求分別拆分成了 Microservices 和 Serverless 架構。應用架構與團隊成員組成、業務形態息息相關,但是隨著各種雲服務與基礎設施的完善,我們可以更自如的選擇更合適的架構。
image.png
為什麼要特別把 Serverless 單獨拿出來說呢?還記得之前說 Node.js 是單線程,不適合 CPU 密集型任務麼?

由於 Serverless 的出現,我們可以將這些存在安全風險的,消耗大量 CPU 計算的任務都遷移到函數計算上。它運行在沙箱環境中,不用擔心用戶的惡意代碼造成安全風險,同時將這些 CPU 密集型的任務從主服務中剝離,避免出現併發時阻塞主服務。按需付費的方式也可以大大節約成本,不需要為低頻功能場景部署一個常駐服務。所以我們會盡量的把這類服務都遷移到 Serverless 上(如阿里雲函數計算)。

結語 | 語雀的技術棧選擇

語雀這幾年一步步發展過來,背後的技術一直在演進,但是始終遵循了幾條原則:

  • 技術棧選型要匹配產品發展階段。產品在不同的階段對技術提出的要求是不一樣的,越前期,對迭代效率的要求越高,商業化規模化之後,對穩定性、性能的要求就會變高。不需要一上來就用最先進的技術方案,而是需要和產品階段一起考慮和權衡。
  • 技術棧選型要結合團隊成員的技術背景。語雀選擇 JavaScript 全棧的原因是孵化語雀的團隊,大部分都是 JavaScript 背景的程序員,同時 Node.js 在螞蟻也算是一等公民,配套的設施相對完善。
  • 最重要的一點是,不論選擇什麼技術棧,安全、穩定、可維護(擴展)都是要考慮清楚的。用什麼語言、用什麼服務會變化,但是這些基礎的安全意識、穩定性意識,如何編寫可維護的代碼,都是決定項目能否長期發展下去的重要因素。

image.png
關注「Alibaba F2E」
把握阿里巴巴前端新動向

Leave a Reply

Your email address will not be published. Required fields are marked *