關於Node.js後端架構的一點後知後覺

前言

上週有幸和淘寶前端團隊的七念老師作了一些NodeJS方面上的交流(實際狀況實際上是他電話面試了我╮(╯-╰)╭),咱們主要聊到了我參與維護的一個線上NodeJS服務,關於它的現狀和當下的不足。他向我提出的一些問題帶給了我很大啓發,儘管回答的不是很好。問題大意是,對於你意識到的這些不足,你將嘗試怎樣去改進它們?甚至,若是給你一個機會來從新設計這個系統服務,你將如何作?相比如今有什麼的改進?javascript

爲何說這些問題對我產生了啓發,是由於這些問題是我未曾考慮過的。或者說考慮過,但沒有這麼嚴肅的考慮過。這裏的「嚴肅」指的是具體到線上,細節,容災容錯等方面。而在電話以後我從新嘗試回答這些問題的過程當中又收穫了很多新的知識。html

這篇文章與以往的文章不一樣,並非闡述某一個問題的最佳解決方案,也不會落實到具體的代碼上。而是分享在探尋答案過程當中收穫的心得、留下的困惑還有一點我的的經驗。至於這些可否拿來回答最初的那些問題我沒有十足的把握,也許能,但確定不是最佳答案。由於後端架構實在一個頗有深度的話題,也是一個極其成熟的技術方向。即便有了理論方面的積累,面對變幻無窮的業務需求不免仍是靈活的對方案進行改進,而不管是理論仍是實踐經驗都是我欠缺的。前端

這段話原本應該是寫在結尾,感受順嘴也就掛在了開頭。java

最後,本文的部份內容和圖片參考自圖書Node.js design patterns的第七章內容Scalability and Architectural Patterns。其實書中該章中的大部份內容也並不是原創,可是它作了很好的彙總和遷移,具體我會在以後說明。因此若有雷同,不是巧合。node

正文

一個怎樣的後端服務才能算得上優秀?或者放低身段說合格?再把這個問題翻譯翻譯,優秀或者合格的標準是什麼?程序員

假設如今須要你用NodeJS搭建一個http服務,我猜想你會藉助express框架用不到10行的代碼完成這項工做。不能說這麼作是錯的,但這樣簡易的程序是脆弱的:一旦部署上線以後,可能瞬間就被大量涌入的請求擊垮,更不要提各類潛在的漏洞危險。退一步說,即便線上程序通過了這一關考驗,若是你要更新程序怎麼辦?不得不讓用戶中斷訪問一段時間?面試

在我看來,後端服務務必要知足兩條特性:算法

  • 能容錯(Fault tolerant)
  • 可擴展(Scalability)

固然還有一些其餘特性也很重要,好比程序要健壯,接口設計要友好,程序修改起來要靈活等等。但容錯性和拓展性纔是正常運行的基本保障,至少保證了你的服務是可用的,永遠是可用的。而不管實現服務的代碼如何優雅,它都是爲業務服務的,一旦用戶沒法訪問你的服務了,再優美的代碼也無濟於事。因此接下來的問題就是,咱們後端程序的架構如何的設計以保證知足這兩條特性呢?數據庫

首先咱們說說拓展性(Scalability)。express

按照書中的說法,拓展性劃分爲三類,以下圖所示:

  • X軸方向:純粹的對服務實例進行拓展,例如爲了響應更多的請求
  • y軸方向:爲服務添加新的功能,功能性拓展
  • z軸方向:按照業務數據對服務進行拓展(這裏沒搞懂,不知道這麼說是否準確)

而一般實際的拓展過程當中多維度是同時進行的,例如增添了新的功能也就意味着有跟多的流量進入,也就是意味着須要增長新的服務實例。

實例拓展

咱們先談第一類X軸拓展,增長服務的實例。增長服務實例也分爲兩類,橫向拓展(horizontal scaling)和縱向拓展(vertical scaling),橫向表示利用更多的機器,縱向表示在同一臺機器上挖掘它的潛力。但其實橫向和縱向二者解決問題的思路的差別並不大。

從小到大,先說縱向拓展。

咱們都知道NodeJS程序是以單進程形式運行,32位機器上最多也只有1GB內存的實用權限(在64位機器上最大的內存權限擴大到1.7GB)。而目前絕大部分線上服務器的CPU都是多核而且至少16GB起,如此以來Node程序便沒法充分發揮機器的潛力。同時NodeJS本身也意識到了這一點,因此它容許程序建立多個子進程用於運行多個實例。

具體技術細節涉及到Cluster模塊,詳情能夠查看NodeJS相關文檔: https://nodejs.org/api/cluster.html

下圖就是對以上所說多進程模式原理的圖解:

簡單來講,首先咱們有一個主進程master,但master主進程並不實際的處理業務邏輯,但除了業務邏輯之外事情它都作:它是manager,負責啓動子進程,管理子進程(若是子進程掛了要及時重啓),它也扮演router,也就是對該程序的訪問請求首先到達主進程,再由主進程分配請求給子進程worker。而子進程才負責處理業務邏輯。

在這個機制下有兩條細節須要咱們定奪如何處理。

如何把外界的請求平均的分配給不一樣的worker處理?這裏的平均不是指數量上的平均(由於單條請求處理的工做量可能不一樣),而是既不能讓某個子進程太閒,也不能讓某個子進程太忙,保證它們始終處於工做的狀態便可。這也是咱們常說的負載均衡(load-balancing)。 默認狀況下Cluster模塊採用的是round robin負載均衡算法,說白了就是依次按順序把請求派給列表上的子進程,派到結尾以後又重頭開始。

這個算法只能保證每一個子進程收到的請求個數是平均的,和隨機算法相似。但若是某個子進程遇到問題,處理變得遲緩了,然後續的請求又源源不斷的分配過來,那麼這個子進程的壓力就大了,這就略顯不公了。除此以外咱們還要考慮到超時,重作等機制的創建。因此主進程master做爲路由時不只僅是轉發請求,還要能智能的分配請求。

另外一個問題是狀態共享問題,假如某個用戶第一次訪問該服務時是分配給了線程A上的實例A處理,而且用戶在這個實例上進行了登錄,而沒有過幾秒鐘以後當用戶第二次訪問時分配給了線程B上的實例B處理,若是此時用戶在A上的登錄狀態沒有共享給其餘實例的話,那麼用戶不得不從新登錄一次,這樣的用戶體驗是沒法接受的。以下圖所示

這個問題的解決辦法是把狀態進行共享:

也能夠新增一個模塊用於記錄用戶第一次訪問的實例,並在以後當用戶訪問服務時始終訪問該實例

主進程-子進程的模式思路不只適用於縱向拓展,還適用於橫向拓展。當單臺機器已經沒法知足你需求的時候,你能夠把單實例子進程的概念拓展爲單臺機器:咱們將在多臺機器上部署多個進行實例,用戶的訪問請求也並不是直接到達它們,而是先到達前方的代理機器,它也是負責負載均衡的機器,負責將請求轉發給部署了應用實例的機器。這樣的模式咱們也一般稱爲反向代理模式:

咱們仍然能對這個模式持續改進,例如動態的啓動或者關閉機器上的實例用於節省資源,甚至想辦法移除負載平衡這一環節用於提升通信的效率。在這裏就不延伸開了去了,具體能夠參考Node.js design patterns這本書中的內容。

最後在這裏要說一件很重要的事情。上面說的負載平衡也好,反向代理也好,都不是新的技術。相反,都是很是很是成熟,有着至關多經驗積累的技術。然而爲何咱們接觸起來卻感受如此的新鮮和陌生?我想緣由大概是NodeJS程序員大可能是由前端工程師轉化而來,而你們此前都只專一於前端代碼而不多接觸後端知識。然而若是你從入行開始就是一個Java程序員或者運維工程師,相信你對這一切早就耳熟能詳而且手到擒來。

幾年前看到過一篇文章,(很惋惜如今找不到了,若是有哪位同窗知道篇文章的麻煩告知一下謝謝),記錄的是一位技術人員針對網站訪問量增大而作的一系列技術改進。文章的後半部分我記不得了,可是前半部分遇到的問題和改進的思路和咱們是如出一轍的:請求驟增,增長實例機器和解決session共享問題。我想說的是,雖然NodeJS是新技術,可是咱們解決問題的思路和方案能夠來自傳統軟件行業,而且它們在這方面比咱們有經驗的多。因此咱們在學習NodeJS,在尋找一些問題的解決方案時,不要侷限於NodeJS自己,而是應該開闊眼界,跨語言包容的去汲取知識。

功能拓展

你也許會問新增功能有什麼難點?每一個程序員的平常就是不斷的進行功能迭代。但在這裏咱們但願解決一個問題,就是既然咱們沒法保證功能不會出錯,那咱們有沒有辦法保證當一個功能出錯以後不會影響整個程序的正常運行?這也是咱們所說的容錯性。

道理都懂,咱們都明白程序須要容錯,因此try/catch是從編碼上解決這個問題。但問題是try/catch不是萬能的,萬無一失的程序也是不存在的,因此咱們要換個思路解決這個問題,咱們容許程序出錯,可是要及時把錯誤隔離,而且再也不影響程序的運行。這個就要從架構上解決這個問題。例如使用微服務(Microservices)架構。

在介紹微服務架構以前,咱們要了解其它架構爲何無法知足咱們的要求。例如咱們經常使用的單體(monolithic)架構。單體架構這個詞你可能不熟悉,但幾乎咱們天天都在和它打交道,大部分的後端服務都歸屬於單體架構,對它的解釋我翻譯Martin Fowler的描述:

企業級應用一般分爲三個部分:用戶界面(包含運行在用戶瀏覽器上的html頁面和javascript腳本),數據庫(一般是包含許多表的關係數據庫),和服務端應用。服務端應用將會處理http請求,執行業務邏輯,從數據庫中取得數據,生成html視圖返回給瀏覽器。這樣的服務端應用就被稱爲單體(monolith)——單個具備邏輯性的執行過程。任何針對系統的修改都會致使從新構建和部署一個新版本的服務端應用。

(注:以上這段描述摘自Martin Fowler的文章Microservices,我認爲這是對微架構描述最全面的文章,若是想對這一小節作更深刻的瞭解能夠把這篇文章細讀。 這也是我讀到的Martin Fowler所寫的文章中最通俗的文章。我的認爲Martin Fowler的文章讀起來比較晦澀,John Resig緊隨其後)

單體架構是一種很天然的搭建應用的方式,它符合咱們對業務處理流程的認知。但單體應用也存在問題:任何一處,不管大小的修改都會致使整個應用被從新構建和從新部署。隨着應用規模和複雜性的不斷增大,參與維護的人數增多,每一輪迭代修改的模塊增多,對上線來講是極大的考驗,對於內部單個模塊的拓展也是極爲不利的。例如當圖片壓縮請求劇增時,須要新增圖片壓縮模塊的實例,但實際上不得不擴展整個單體應用的實例。

微服務架構解決的就是這一系列問題。顧名思義,微服務架構下軟件是由多個獨立的服務組成。這些服務相互獨立互不干預。以拆分上面所說的單體應用爲例,咱們能夠把處理HTTP請求的模塊和負責數據庫讀寫的模塊分離出來成爲獨立的服務,這兩個模塊從功能上看是沒有任何交集。這樣的好處就是,咱們能夠獨立的部署,拓展,修改這些服務。例如應用須要添加新的接口時,咱們只須要修改處理HTTP請求的服務,只公開這部分代碼給修改者,只上線這部分服務,拓展時也只須要新添這部分服務的實例。

微服務和咱們一般編寫的模塊(以文件爲單位,以命名空間爲單位)相比更加獨立,更像是一個五臟俱全的「小應用」,若是你讀完了我以前推薦的Martin Fowler關於微服務的文章的話,你會對這點更深有感觸:微服務除了在運維上獨立之外,它還能夠擁有獨立的數據庫,還應該配備獨立的團隊維護。它甚至能夠容許使用其餘的語言進行開發,只要對外接口正常便可。

固然微服務也存在不足,例如如何將諸多的微服務在大型架構中組織起來,如何提升不一樣服務之間的通訊效率都是須要在實際工做中解決的問題。

微服務說到底仍是解耦思想的實踐。從這個意義上來講,React下的Flux架構某種意義上也屬於微服務。若是你瞭解Flux的起源的話,Flux架構其實來源於後端的CQRS,即Command Query Responsibility Segregation,命令與查詢職責分離,也就是將數據的讀操做和寫操做分離開。這麼設計的理由有不少,舉例說一點:在許多業務場景中,數據的讀和寫的次數是不平衡,可能上千次的讀操做纔對應一次寫操做,好比機票餘票信息的查詢和更新。因此把讀和寫操做分開可以有針對性的分別優化它們。例如提升程序的scalability,scalability意味着咱們可以在部署程序時,給讀操做和寫操做部署不一樣數量的線上實例來知足實際的需求。

若是你也有Unity編程經驗的話會對解耦更有感觸,在Unity中咱們已經不能稱之爲解耦,而是自治,這是Unity的設計模式。舉個例子,屏幕上少則可能有十幾個遊戲元素,例如玩家、敵人還有子彈。你必須爲它們編寫「死亡」的規則,「誕生」的規則,交互的規則。由於你根本沒法預料玩家在什麼時候何種位置發射出子彈,也沒法預料子彈什麼時候在什麼位置碰撞上什麼狀態敵人。因此你只能讓它們在規則下自由發揮。這和微服務有殊途同歸之妙:獨立,隔離,自治。

總結

實話實說,這篇文章裏沒有乾貨,全都是舶來品。但舶來品不是一個貶義詞,它是咱們學習知識和解決問題的第一手材料。我仍是想重申一遍,在後端領域來講Node.js是一個新人,咱們應該學習前輩的經驗。借用許多年前奔馳廣告的一句話:經典是對經典的繼承,經典是對經典的背叛。只有站在前人的肩膀上,咱們纔有可能創新,看的更遠。

相關文章
相關標籤/搜索