Node 在有讚的實踐

有贊技術發表於2019-02-16

一、概述

4月21日,有贊舉辦了第一屆“有贊技術開發日”的活動,我作為分享講師,分享了有贊最近一年在 Node 這一塊的實踐經驗。但由於分享時間有限,我也只能把最重要的內容拿出來和大家分享,所以這個週末就花了幾個小時時間,結合那次的分享,並完善了其中的一些內容,寫了這篇文章,希望可以給大家帶來新的啟發。

二、Node 基礎框架的迭代與演進

1. 從 Koa 到 阿童木(Astroboy)

(1)Koa + 中介軟體

有贊最早的一個比較完整的 Node 專案是公司內部的一個管理系統,這個系統是用 Node 全棧開發的,主要包括一個給 HR 用的員工管理系統和給小夥伴用的 APP。就像大多數公司一樣,我們第一個 Node 專案也是直接用 Koa,然後整合一些開源的中介軟體,這樣就快速的把專案搭建起來了。

這個專案做了半年之後,我們把 Node 該踩的坑基本也都踩了一遍,所以我們就開始嘗試在對外產品上使用 Node了,我們第一個嘗試改造的專案是公司的官網,這是最簡單的一個專案,基本沒什麼大的風險。

(2)腳手架專案模板

第二個專案我們不可能再按照之前的方式,簡單用 Koa 加上一堆中介軟體的方式來搭建專案了,因為已經有了之前的經驗,所以我們就整理了下這一套方案,抽離出了一個專案模板,每個新專案只要把這個模板克隆下來,然後改一下配置,就可以快速搭建出一個新的專案來。

(3)阿童木 1.0

專案多了之後,這種方式弊端很快就顯現出來了,因為模板程式碼和業務程式碼是耦合在一起,如果要改模板生成的程式碼,只能每個專案手動更新,而隨著時間的推移,越來越難保持同步了,每個專案的目錄結構和程式碼風格可能也會變得非常不一樣,所以,解耦框架程式碼和業務程式碼就非常重要了。所以我們就在腳手架模板的基礎上抽離出了一個框架叫 Astroboy(阿童木),這個框架是在 Koa 的基礎上封裝的,這樣,每個專案都基於這個框架開發,如果框架更新了,專案也只需要更改下框架的版本號。

Node 在有讚的實踐

(4)阿童木 2.0

很多專案都開始用 Node 了,新的問題又出現了,因為每個產品的業務場景都不一樣,對框架的需求也都不一樣。例如某個中介軟體,產品 A 可能需要,而產品 B 可能根本不需要這個中介軟體,而這個時候的框架又不支援定製改造。所以對框架來說,又提出了新的挑戰,所以在今年年初,對框架做了一次大的重構。

這次重構在阿童木 1.0 的基礎上,加入了很多新特性,主要有以下幾點:

  • 基於 Koa2 開發,效能表現優異
  • 提供基於 Astroboy 定製上層框架的能力
  • 高度可擴充套件的外掛機制
  • 漸進式開發

首先提供基於 Astroboy 定製上層框架的能力,如下圖所示,Youzan Base Framework 是在阿童木的基礎上定製的一個有贊最基礎的 Node Web 框架,這一層主要整合了一些有贊最基礎的服務,像:

  • 天網系統接入,這是有贊內部的一個日誌及業務監控系統
  • 健康檢查,運維監控系統每隔5秒鐘,都會檢查系統服務可用性
  • 全鏈路監控,對於一次 HTTP 請求,一般都會呼叫多個後端介面,相應的後端介面也會再去呼叫其他介面,所以整個呼叫過程實際上是一棵樹狀的結構,如果碰到效能問題,找出其中效能瓶頸問題就非常重要了,全鏈路監控就是為了解決這個問題。
  • Dubbo 服務呼叫接入,關於這一點,檢視下面關於服務化的介紹。

有了 Youzan Base Framework 後,我們就需要在上面開發業務了,這個分兩種業務場景:對於一些簡單單一的業務,直接繼承 Youzan Base Framework 開發就可以了;而如果是一些複雜的業務,就可以先在 Youzan Base Framework 的基礎上,定製出一個業務框架,像我們有贊原先有一個超大的 PHP 專案(我們叫 Iron),那麼服務化拆分後,Node 就承擔了原先 PHP 的部分,所以我們新先定製了一個業務級的框架叫 Iron Base Framework,然後再按照業務模組(交易、店鋪、使用者、營銷)拆分成多個子專案。

Node 在有讚的實踐

其次是支援外掛化,關於這一點,可檢視下面關於外掛的說明。

2. 框架的幾個核心概念

以上介紹了有贊 Node 基礎框架迭代和演變的過程,下面主要介紹下阿童木2.0 框架的幾個核心概念

(1)應用 Application

應用 Application 的概念很好理解,在這裡應用就可以理解成一個專案,它是從框架繼承下來,並且例項化之後的一個例項,應用也是由一個一個外掛構成的。

(2)框架 Framework

Astroboy 框架是在 Koa2 的基礎上封裝的,關於框架的概念,這裡就不再做過多的介紹了。

(3)外掛 Plugin

外掛化是軟體設計中一個很重要的思想,很多軟體像 Eclipse 都支援這樣的特性,外掛化可以讓我們的系統解耦,每個模組做到獨立開發,而模組之間又不會相互影響,這樣的特性對於大型專案來說是非常重要的。

外掛化是 Astroboy 框架中最核心的一個實現,它是服務(Service)、中介軟體(Middleware)和工具函式庫(Lib)等的載體,它本質上還是 NPM 包,只不過是在 NPM 包的基礎上,做了更深層次的抽象。基於 Astroboy 的應用,就是由一個一個的 Plugin 組成的,Plugin 就是我們手中的積木,通過 Astroboy 的框架引擎把這些積木組織在一起,就形成了系統。

那麼外掛跟普通的 NPM 包有什麼區別呢?

外掛約定了目錄結構,這樣每個外掛看起來都是類似的,這對於團隊的協作是非常重要,如果每個模組看起來都不一樣,那麼團隊的協作成本就會很高。 應用啟動後,外掛的程式碼是自動注入到整個應用的,只需要在外掛的配置檔案裡面開啟這個外掛即可。

一個外掛可以包含哪些資訊?

  • 外掛後設資料,包括外掛名稱、版本、描述等;
  • 服務(Service)、中介軟體(Middleware)以及工具函式庫(Lib)等;
  • Koa 內建物件的擴充套件,包括 Context、Application、Request 以及 Response 等;

外掛的管理

  • 安裝外掛,通過npm install 命令即可,例如:npm install [<@scope>/]@
  • 啟用外掛,安裝外掛後還需要啟用外掛,外掛才會真正生效。啟用外掛也很簡單,只需要配置 plugin.default.js 即可,如果不同環境外掛配置不一樣,也只需修改相應* 環境的配置(plugin.${env}.js)即可,這裡 env 表示 Node 執行時的環境變數,例如:development、test、production 等。如下程式碼所示:
'astroboy-cookie': {
    enable: true,
    path: path.resolve(__dirname, '../plugins/astroboy-cookie')
}
複製程式碼

enable 設定成 true 就可以開啟這個外掛,path 表示外掛的絕對路徑,這種一般適合於還在快速迭代中的外掛,如果外掛已經很穩定了,你就可以把這個外掛打包釋出成一個 NPM 包,然後通過 package 宣告你的外掛即可,如下程式碼所示:

'astroboy-cookie': {
    enable: true,
    package: 'astroboy-cookie'
}
複製程式碼
  • 禁用外掛,禁用外掛就更加簡單了,只需將 enable 設定成 false 即可。

三、Node 接入有贊服務化體系的歷程

1. 為什麼要做服務化?

隨著公司業務的發展,網站應用的規模不斷擴大,垂直應用越來越多,應用之間互動不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提高業務複用及整合的分散式服務框架(RPC)是關鍵,所以在這個時候,分散式服務架構就勢在必行了。

2. 技術棧的選擇

在介紹技術棧選擇之前,先講一下公司的一些技術背景。

在公司成立初期,為了能夠快速開發,把產品快速做出來推出市場,所以我們選擇用 PHP 語言,我想這也是大多數創業公司的選擇。而隨著業務的發展,PHP 越來越難處理複雜的業務。

所以等到了一定時候,我們開始做服務化拆分,那麼首先考慮的就是底層技術的選擇,我們從下面幾點考慮:

  • 第一個是這門技術的生態是否足夠完善,也就是相關的開源軟體、工具是否成熟;
  • 第二個是否能夠快速招到你需要的人才。

3. 服務化拆分之後,每一層職責分別是什麼?

對於 Node 層,我們的定位是一層很薄的中間層,Node 這一層不會過多地處理業務邏輯,業務邏輯全部都交給 Java 來處理,它只負責下面三件事情:

  • 模板渲染:模板渲染說的就是 HTML 模板的渲染;
  • 業務編排:對於一個稍微複雜一點的頁面,通常需要聚合多個介面返回的資料才能顯示完整的頁面,所以在這種情況下,Node 就需要聚合多個介面的返回結果,然後將合併後的資料返回給前端。
  • 介面轉發:Java 的服務是不會直接暴露到公網提供給前端使用的,所以在這種情況下,Node 需要承擔介面轉發的角色。

而對於 Java 這一層,就需要承擔業務邏輯以及快取等複雜的操作,這裡就不做過多的介紹了。

4. Node 如何呼叫 Java 介面?

那麼服務化拆分之後,首先要解決的一個問題是:Node 如何呼叫 Java 提供的介面。首先,我們想到的就是 HTTP 的方式,這裡說明一下,我們公司採用的分散式服務化框架是阿里開源的 Dubbo 框架,而 Dubbo 框架本身是支援通過新增註解的方式生成 Restful API 的,所以在初期,我們就是採用這個現成的方案。

而隨著應用數目的增加,這種方式的弊端也逐漸顯現出來,主要有下面幾點:

  • 如果某個介面需要暴露給 Node 使用,就需要手動再去新增額外的註解。
  • 每增加一個應用,運維都需要針對每個應用配置域名,不同的環境又需要配置不同的域名,所以隨著應用數的增加,應用域名的管理越來越難維護。
  • 相應的,node 也需要維護一份很長的域名配置檔案。
  • 由於 Java 是直接提供 HTTP 介面,所以效能上相對 RPC 的方式會低一點。

所以,我們就調研了下,看其他公司在使用 Dubbo 框架時,Node 是如何呼叫 Java 的?如下圖所示:

Node 在有讚的實踐

首先,Java 應用服務啟動的時候,會往服務註冊中心註冊服務,這裡的服務註冊中心可能是 ETCD 或者 Zookeeper,然後,Node 應用在啟動的時候,會先從服務註冊中心拉取服務列表,接著 Node 會跟 Java 服務建立一條TCP長連結,除此之外,Node 還需要負責 Hession 協議解析以及負載均衡等。

不難發現,這種方式 Node 的職責就比較重,而且對 Node 開發的要求會很高。所以,我們對這種方式做了改進,如下圖所示:

Node 在有讚的實踐

我們在 Node 和 Java 之間新增了一層中間代理層 Tether,Tether 是用 Go 語言寫的一個本地代理,Tether 會對外暴露一個 HTTP 的服務,對 Node 來說,只需要通過 HTTP 方式呼叫本地的服務即可,其他服務化相關的服務發現、協議解析、負載均衡、長鏈建立維護都交由 Tether 來處理。這樣,Node 這一層就非常輕量了,那麼,最終實現出來,Node 是怎麼呼叫 Java 服務的呢?如下程式碼所示:

const Service = require('../base/BaseService');

class GoodsService extends Service {
  /**
   * 根據商品 alias 獲取商品詳情
   * @param  {String} alias 商品 alias
   */
  async getGoodsDetailByAlias(alias) {
    const result = this.invoke(
      'com.youzan.ic.service.GoodsService',
      'getGoodsDetailByAlias',
      [alias]
    );
    return result;
  }
}
module.exports = GoodsService;
複製程式碼

對 Node 來說,呼叫 Java 服務它只需要關注三個點:

  • 服務名:服務名是由 Java 的包名 + 類名組成,例如上面的 com.youzan.ic.service.GoodsService
  • 方法名:Java 類對外暴露的方法,例如上面程式碼所示的根據商品 alias 查詢商品詳情的一個方法 getGoodsDetailByAlias
  • 引數:引數就是傳遞給 Java 的引數列表

最後,總結下這種方式都有哪些優點:

  • 第一個是使用簡單,對前端開發非常友好,只需要通過 HTTP 方式呼叫本地的 Tether 服務即可;
  • 第二個是多語言接入成本低,後期如果有其他語言(Python、Ruby)也需要接入整個服務化體系,也像 Node 一樣,它們都只需要呼叫本地 Tether 暴露的 HTTP 服務即可,沒有額外的開發成本了。
  • 第三個是後期更方便做協議層的優化,因為這種方式 Tether 其實就是一個代理,後期如果需要做協議層效能上的優化,那隻需要優化 Tether 的效能就可以了。

那麼,看到這裡,有人可能又會想,這裡 Node 也是通過 HTTP 方式呼叫 Java 的,效能上是不是也存在問題呢?所以這裡我們就做了一些優化,如下程式碼所示:

const Agent = require('agentkeepalive');

module.exports = new Agent({
  maxSockets: 100,
  maxFreeSockets: 10,
  timeout: 60000,
  freeSocketKeepAliveTimeout: 30000,
});
複製程式碼

這裡,我們引用了一個 agentkeepalive 包,在 HTTP 早期,每個 HTTP 請求都要求開啟一個 TCP Socket 連線,並且使用一次之後就斷開這個 TCP 連線,使用 keep-alive 可以改善這種狀態,即在一次 TCP 連線中可以持續傳送多份資料而不會斷開連線。所以通過使用 keep-alive 機制,就可以減少 TCP 連線建立次數。

四、參考資料

https://github.com/apache/incubator-dubbo https://github.com/QianmiOpen/dubbo2.js https://github.com/QianmiOpen/dubbo-node-client https://github.com/p412726700/node-zookeeper-dubbo https://zh.wikipedia.org/wiki/HTTP%E6%8C%81%E4%B9%85%E8%BF%9E%E6%8E%A5

Node 在有讚的實踐

相關文章