Node 在有讚的實踐

1、概述

4月21日,有贊舉辦了第一屆「有贊技術開發日」的活動,我做爲分享講師,分享了有贊最近一年在 Node 這一塊的實踐經驗。但因爲分享時間有限,我也只能把最重要的內容拿出來和你們分享,因此這個週末就花了幾個小時時間,結合那次的分享,並完善了其中的一些內容,寫了這篇文章,但願能夠給你們帶來新的啓發。javascript

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

1. 從 Koa 到 阿童木(Astroboy)

(1)Koa + 中間件

有贊最先的一個比較完整的 Node 項目是公司內部的一個管理系統,這個系統是用 Node 全棧開發的,主要包括一個給 HR 用的員工管理系統和給小夥伴用的 APP。就像大多數公司同樣,咱們第一個 Node 項目也是直接用 Koa,而後整合一些開源的中間件,這樣就快速的把項目搭建起來了。前端

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

(2)腳手架項目模板

第二個項目咱們不可能再按照以前的方式,簡單用 Koa 加上一堆中間件的方式來搭建項目了,由於已經有了以前的經驗,因此咱們就整理了下這一套方案,抽離出了一個項目模板,每一個新項目只要把這個模板克隆下來,而後改一下配置,就能夠快速搭建出一個新的項目來。node

(3)阿童木 1.0

項目多了以後,這種方式弊端很快就顯現出來了,由於模板代碼和業務代碼是耦合在一塊兒,若是要改模板生成的代碼,只能每一個項目手動更新,而隨着時間的推移,愈來愈難保持同步了,每一個項目的目錄結構和代碼風格可能也會變得很是不同,因此,解耦框架代碼和業務代碼就很是重要了。因此咱們就在腳手架模板的基礎上抽離出了一個框架叫 Astroboy(阿童木),這個框架是在 Koa 的基礎上封裝的,這樣,每一個項目都基於這個框架開發,若是框架更新了,項目也只須要更改下框架的版本號。git

(4)阿童木 2.0

不少項目都開始用 Node 了,新的問題又出現了,由於每一個產品的業務場景都不同,對框架的需求也都不同。例如某個中間件,產品 A 可能須要,而產品 B 可能根本不須要這個中間件,而這個時候的框架又不支持定製改造。因此對框架來講,又提出了新的挑戰,因此在今年年初,對框架作了一次大的重構。github

此次重構在阿童木 1.0 的基礎上,加入了不少新特性,主要有如下幾點:apache

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

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

  • 天網系統接入,這是有贊內部的一個日誌及業務監控系統
  • 健康檢查,運維監控系統每隔5秒鐘,都會檢查系統服務可用性
  • 全鏈路監控,對於一次 HTTP 請求,通常都會調用多個後端接口,相應的後端接口也會再去調用其餘接口,因此整個調用過程其實是一棵樹狀的結構,若是碰到性能問題,找出其中性能瓶頸問題就很是重要了,全鏈路監控就是爲了解決這個問題。
  • Dubbo 服務調用接入,關於這一點,查看下面關於服務化的介紹。

有了 Youzan Base Framework 後,咱們就須要在上面開發業務了,這個分兩種業務場景:對於一些簡單單一的業務,直接繼承 Youzan Base Framework 開發就能夠了;而若是是一些複雜的業務,就能夠先在 Youzan Base Framework 的基礎上,定製出一個業務框架,像咱們有贊原先有一個超大的 PHP 項目(咱們叫 Iron),那麼服務化拆分後,Node 就承擔了原先 PHP 的部分,因此咱們新先定製了一個業務級的框架叫 Iron Base Framework,而後再按照業務模塊(交易、店鋪、用戶、營銷)拆分紅多個子項目。後端

其次是支持插件化,關於這一點,可查看下面關於插件的說明。緩存

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 便可。

3、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 的?以下圖所示:

首先,Java 應用服務啓動的時候,會往服務註冊中心註冊服務,這裏的服務註冊中心多是 ETCD 或者 Zookeeper,而後,Node 應用在啓動的時候,會先從服務註冊中心拉取服務列表,接着 Node 會跟 Java 服務創建一條TCP長連接,除此以外,Node 還須要負責 Hession 協議解析以及負載均衡等。

不難發現,這種方式 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 鏈接創建次數。

4、參考資料

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

相關文章
相關標籤/搜索