從 Node.js 看看服務端框架的一些感想

爲什麼寫這篇文章?因爲早上在思考一個問題「想獲取一家公司的數據(內容型公司),反爬措施做的比較好(VIP會員制度,訪問次數太多會鎖掉賬號)。有幾個方向想去嘗試:1.App 逆向破解看看網絡請求部分的參數是如何生成的;2.Charles 抓包破解參數部分看看能否模擬;3.查看小程序是否有漏洞。最後想來想去還是算了,因爲反爬蟲措施即使破解了,但是當請求的次數較多的時候還是會封鎖 VIP 賬號。最後想的是找出封鎖賬號的請求次數的臨界值,然後用爬蟲手段去獲取數據,但是不能超過臨界值就可以。因爲 VIP 賬號價格實在太高就放棄了。過了幾天我想到了用瀏覽器插件的方式去做這個事情。也就是在對方的網站裏面注入我們的 JS 腳本,腳本會在網站上面添加一個按鈕,點擊按鈕就可以將數據同步到我們自己的數據中心。廢話說了一大堆,因爲 JS 在瀏覽器環境裏面不具備服務端的能力所以想到的是通過接口將數據讓一個 Node.js 的服務去處理,將數據入庫等操作。問題正式進入主題,要開發微服務的過程中選擇用 Node 的 express 還是 Koa 還是 eggjs 等問題困擾了我一會兒。這篇文章不針對這些具體的庫進行討論,而是對於服務端的一些思考」

優秀的後端框架?

一個什麼樣的框架算得上是優秀或者合格?有個需求讓你寫一個 HTTP 服務,藉助於 express 你可能初始化項目、安裝依賴、寫完代碼都用不了6分鐘?覺得似乎很簡單,哥們兒你想想這是一個服務,而不是說讓你能跑就行了。計算機學生的平時作業差不多滿足了。但是你說你這個東西能打嗎?可以說“戰五渣”。一旦部署到線上環境,可能瞬間就被大量湧入的請求擊垮,更何況有些人要攻擊你。換個角度思考問題。加入你的線上程序需要升級,你該怎麼辦?停止當前的服務讓用戶等待一段時間嗎?

所以一個後端服務必須滿足2個特性:

  • 容錯性強(Fault tolerate)
  • 可拓展性高(Scalability)

其他的特性也很重要,比如程序的健壯性、接口設計友好、代碼修改起來靈活等等特性。但是容錯性、可拓展性是服務正常運行的基本保障。至少得向用戶保證服務是可用的。無論代碼寫的多優雅它都是爲業務所服務的。

拓展性(Scalability)

拓展性

  • X軸:純粹對服務的實例進行拓展。爲了響應更多的請求
  • Y軸:未服務添加新的給你。功能性拓展
  • Z軸:按照業務數據對服務進行拓展

實例拓展


增加服務實例包括兩類:橫向拓展、縱向拓展。橫向拓展表示利用更多的及其。縱向拓展表示在一臺及其上挖掘它的潛力。

NodeJS 程序是單進程運行的。32位機器上最多隻有 1GB 內存的實用權限(在 64GB 機器上的最大內存權限擴大到 1.7GB)。目前絕大部分線上服務器 CPU 都是多核並且至少 16GB。如此 Node 便無法發揮機器的最大能力。Node 早就意識到這一點,它允許創建多個子進程運行多個實例。

多進程模式

有一個主進程 master,但是 master 進程並不實際處理業務邏輯,但是除了業務邏輯之外的事情它都負責。它是 manager,負責啓動子進程、管理子進程(如果進程掛到了則需要重啓)。同時扮演 Router 的角色,也就是對程序的所有訪問請求都先到達主進程,主進程分配請求給子進程 worker(子進程負責處理業務邏輯)

這個機制下有兩條細節需要處理。

  1. 如何把外界的任務平均分配給不同的 worker 處理?這裏的平均並不是指數量上的平均(因爲每個請求的工作量可能不同)。不能讓某個子進程太閒,也不能讓某個子進程太忙,而是始終處於工作的狀態。也就是「負載均衡(load-balancing)」。默認情況下 Clust 模塊採用的是 round robin 負載均衡算法,說白了就是依次按照順序把請求指派給列表上的子進程,到結尾之後重頭開始。

這個算法只能保證每個子進程收到的請求個數是是平均的。但如果某個進程本來的任務很複雜,後來又由於不斷的收到被平均指配的任務,那麼這個子進程的壓力就很大了。除此之外我們需要考慮超時、重做機制,所以主進程 master 作爲路由時不僅僅需要轉發請求,還需要智能的分配請求

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

用戶信息不共享

解決方案1:將狀態共享

解決方案1

解決方案2:新增一個模塊專門用於記錄用戶第一次訪問的實例。並在之後當用戶訪問服務時始終指派訪問該實例

解決方案2

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

負載均衡模式

這個模式仍然可以繼續改進:動態的啓動或者關閉機器上的若干實例用於節省資源、移除負載均衡這一環節用於提高通訊的效率。

對於所有的開發來說,很多道理都是通用的。比如設計模塊、解耦思想。上面說的負載均衡、反向代理等等不只是 Java、Node、PHP、.Net 等都存在。所以只要是服務端的概念,Node 裏面一樣存在。(有的 Node 工程師是從前端開發轉過來的,所以在此強調。)雖然 Node.js 較新,但是解決思路或者方案可以借鑑傳統的服務端方案。跳出語言的限制去看待問題、解決問題、尋找思路和方案

功能拓展

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

道理都懂,我們都明白程序需要容錯,所以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 作爲服務端的新人,應該學習前輩的經驗。借用奔馳廣告的一句話:經典是對經典的繼承、經典是對經典的背叛。只有站在前人的肩膀上,我們纔有可能創新,看的更遠

(以上文章部分參考自網絡,因爲本人看到後相見恨晚,和我思想觀念一致,所以搬運總結於此,望共勉)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章