
一、概述
4月21日,有贊舉辦了第一屆“有贊技術(shù)開發(fā)日”的活動,我作為分享講師,分享了有贊最近一年在 Node 這一塊的實踐經(jīng)驗。但由于分享時間有限,我也只能把最重要的內(nèi)容拿出來和大家分享,所以這個周末就花了幾個小時時間,結(jié)合那次的分享,并完善了其中的一些內(nèi)容,寫了這篇文章,希望可以給大家?guī)硇碌膯l(fā)。
二、Node 基礎(chǔ)框架的迭代與演進
1. 從 Koa 到 阿童木(Astroboy)
(1)Koa + 中間件
有贊最早的一個比較完整的 Node 項目是公司內(nèi)部的一個管理系統(tǒng),這個系統(tǒng)是用 Node 全棧開發(fā)的,主要包括一個給 HR 用的員工管理系統(tǒng)和給小伙伴用的 APP。就像大多數(shù)公司一樣,我們第一個 Node 項目也是直接用 Koa,然后整合一些開源的中間件,這樣就快速的把項目搭建起來了。
這個項目做了半年之后,我們把 Node 該踩的坑基本也都踩了一遍,所以我們就開始嘗試在對外產(chǎn)品上使用 Node了,我們第一個嘗試改造的項目是公司的官網(wǎng),這是最簡單的一個項目,基本沒什么大的風(fēng)險。
(2)腳手架項目模板
第二個項目我們不可能再按照之前的方式,簡單用 Koa 加上一堆中間件的方式來搭建項目了,因為已經(jīng)有了之前的經(jīng)驗,所以我們就整理了下這一套方案,抽離出了一個項目模板,每個新項目只要把這個模板克隆下來,然后改一下配置,就可以快速搭建出一個新的項目來。
(3)阿童木 1.0
項目多了之后,這種方式弊端很快就顯現(xiàn)出來了,因為模板代碼和業(yè)務(wù)代碼是耦合在一起,如果要改模板生成的代碼,只能每個項目手動更新,而隨著時間的推移,越來越難保持同步了,每個項目的目錄結(jié)構(gòu)和代碼風(fēng)格可能也會變得非常不一樣,所以,解耦框架代碼和業(yè)務(wù)代碼就非常重要了。所以我們就在腳手架模板的基礎(chǔ)上抽離出了一個框架叫 Astroboy(阿童木),這個框架是在 Koa 的基礎(chǔ)上封裝的,這樣,每個項目都基于這個框架開發(fā),如果框架更新了,項目也只需要更改下框架的版本號。

(4)阿童木 2.0
很多項目都開始用 Node 了,新的問題又出現(xiàn)了,因為每個產(chǎn)品的業(yè)務(wù)場景都不一樣,對框架的需求也都不一樣。例如某個中間件,產(chǎn)品 A 可能需要,而產(chǎn)品 B 可能根本不需要這個中間件,而這個時候的框架又不支持定制改造。所以對框架來說,又提出了新的挑戰(zhàn),所以在今年年初,對框架做了一次大的重構(gòu)。
這次重構(gòu)在阿童木 1.0 的基礎(chǔ)上,加入了很多新特性,主要有以下幾點:
- 基于 Koa2 開發(fā),性能表現(xiàn)優(yōu)異
- 提供基于 Astroboy 定制上層框架的能力
- 高度可擴展的插件機制
- 漸進式開發(fā)
首先提供基于 Astroboy 定制上層框架的能力,如下圖所示,Youzan Base Framework 是在阿童木的基礎(chǔ)上定制的一個有贊最基礎(chǔ)的 Node Web 框架,這一層主要集成了一些有贊最基礎(chǔ)的服務(wù),像:
- 天網(wǎng)系統(tǒng)接入,這是有贊內(nèi)部的一個日志及業(yè)務(wù)監(jiān)控系統(tǒng)
- 健康檢查,運維監(jiān)控系統(tǒng)每隔5秒鐘,都會檢查系統(tǒng)服務(wù)可用性
- 全鏈路監(jiān)控,對于一次 HTTP 請求,一般都會調(diào)用多個后端接口,相應(yīng)的后端接口也會再去調(diào)用其他接口,所以整個調(diào)用過程實際上是一棵樹狀的結(jié)構(gòu),如果碰到性能問題,找出其中性能瓶頸問題就非常重要了,全鏈路監(jiān)控就是為了解決這個問題。
- Dubbo 服務(wù)調(diào)用接入,關(guān)于這一點,查看下面關(guān)于服務(wù)化的介紹。
有了 Youzan Base Framework 后,我們就需要在上面開發(fā)業(yè)務(wù)了,這個分兩種業(yè)務(wù)場景:對于一些簡單單一的業(yè)務(wù),直接繼承 Youzan Base Framework 開發(fā)就可以了;而如果是一些復(fù)雜的業(yè)務(wù),就可以先在 Youzan Base Framework 的基礎(chǔ)上,定制出一個業(yè)務(wù)框架,像我們有贊原先有一個超大的 PHP 項目(我們叫 Iron),那么服務(wù)化拆分后,Node 就承擔(dān)了原先 PHP 的部分,所以我們新先定制了一個業(yè)務(wù)級的框架叫 Iron Base Framework,然后再按照業(yè)務(wù)模塊(交易、店鋪、用戶、營銷)拆分成多個子項目。

其次是支持插件化,關(guān)于這一點,可查看下面關(guān)于插件的說明。
2. 框架的幾個核心概念
以上介紹了有贊 Node 基礎(chǔ)框架迭代和演變的過程,下面主要介紹下阿童木2.0 框架的幾個核心概念
(1)應(yīng)用 Application
應(yīng)用 Application 的概念很好理解,在這里應(yīng)用就可以理解成一個項目,它是從框架繼承下來,并且實例化之后的一個實例,應(yīng)用也是由一個一個插件構(gòu)成的。
(2)框架 Framework
Astroboy 框架是在 Koa2 的基礎(chǔ)上封裝的,關(guān)于框架的概念,這里就不再做過多的介紹了。
(3)插件 Plugin
插件化是軟件設(shè)計中一個很重要的思想,很多軟件像 Eclipse 都支持這樣的特性,插件化可以讓我們的系統(tǒng)解耦,每個模塊做到獨立開發(fā),而模塊之間又不會相互影響,這樣的特性對于大型項目來說是非常重要的。
插件化是 Astroboy 框架中最核心的一個實現(xiàn),它是服務(wù)(Service)、中間件(Middleware)和工具函數(shù)庫(Lib)等的載體,它本質(zhì)上還是 NPM 包,只不過是在 NPM 包的基礎(chǔ)上,做了更深層次的抽象?;?Astroboy 的應(yīng)用,就是由一個一個的 Plugin 組成的,Plugin 就是我們手中的積木,通過 Astroboy 的框架引擎把這些積木組織在一起,就形成了系統(tǒng)。
那么插件跟普通的 NPM 包有什么區(qū)別呢?
插件約定了目錄結(jié)構(gòu),這樣每個插件看起來都是類似的,這對于團隊的協(xié)作是非常重要,如果每個模塊看起來都不一樣,那么團隊的協(xié)作成本就會很高。 應(yīng)用啟動后,插件的代碼是自動注入到整個應(yīng)用的,只需要在插件的配置文件里面開啟這個插件即可。
一個插件可以包含哪些信息?
- 插件元數(shù)據(jù),包括插件名稱、版本、描述等;
- 服務(wù)(Service)、中間件(Middleware)以及工具函數(shù)庫(Lib)等;
- Koa 內(nèi)置對象的擴展,包括 Context、Application、Request 以及 Response 等;
插件的管理
- 安裝插件,通過npm install 命令即可,例如:npm install [<@scope>/]<name>@</name>
- 啟用插件,安裝插件后還需要啟用插件,插件才會真正生效。啟用插件也很簡單,只需要配置 plugin.default.js 即可,如果不同環(huán)境插件配置不一樣,也只需修改相應(yīng)* 環(huán)境的配置(plugin.${env}.js)即可,這里 env 表示 Node 運行時的環(huán)境變量,例如:development、test、production 等。如下代碼所示:
'astroboy-cookie': {
enable: true,
path: path.resolve(__dirname, '../plugins/astroboy-cookie')
}
enable 設(shè)置成 true 就可以開啟這個插件,path 表示插件的絕對路徑,這種一般適合于還在快速迭代中的插件,如果插件已經(jīng)很穩(wěn)定了,你就可以把這個插件打包發(fā)布成一個 NPM 包,然后通過 package 聲明你的插件即可,如下代碼所示:
'astroboy-cookie': {
enable: true,
package: 'astroboy-cookie'
}
- 禁用插件,禁用插件就更加簡單了,只需將 enable 設(shè)置成 false 即可。
三、Node 接入有贊服務(wù)化體系的歷程
1. 為什么要做服務(wù)化?
隨著公司業(yè)務(wù)的發(fā)展,網(wǎng)站應(yīng)用的規(guī)模不斷擴大,垂直應(yīng)用越來越多,應(yīng)用之間交互不可避免,將核心業(yè)務(wù)抽取出來,作為獨立的服務(wù),逐漸形成穩(wěn)定的服務(wù)中心,使前端應(yīng)用能更快速的響應(yīng)多變的市場需求。此時,用于提高業(yè)務(wù)復(fù)用及整合的分布式服務(wù)框架(RPC)是關(guān)鍵,所以在這個時候,分布式服務(wù)架構(gòu)就勢在必行了。
2. 技術(shù)棧的選擇
在介紹技術(shù)棧選擇之前,先講一下公司的一些技術(shù)背景。
在公司成立初期,為了能夠快速開發(fā),把產(chǎn)品快速做出來推出市場,所以我們選擇用 PHP 語言,我想這也是大多數(shù)創(chuàng)業(yè)公司的選擇。而隨著業(yè)務(wù)的發(fā)展,PHP 越來越難處理復(fù)雜的業(yè)務(wù)。
所以等到了一定時候,我們開始做服務(wù)化拆分,那么首先考慮的就是底層技術(shù)的選擇,我們從下面幾點考慮:
- 第一個是這門技術(shù)的生態(tài)是否足夠完善,也就是相關(guān)的開源軟件、工具是否成熟;
- 第二個是否能夠快速招到你需要的人才。
3. 服務(wù)化拆分之后,每一層職責(zé)分別是什么?
對于 Node 層,我們的定位是一層很薄的中間層,Node 這一層不會過多地處理業(yè)務(wù)邏輯,業(yè)務(wù)邏輯全部都交給 Java 來處理,它只負責(zé)下面三件事情:
- 模板渲染:模板渲染說的就是 HTML 模板的渲染;
- 業(yè)務(wù)編排:對于一個稍微復(fù)雜一點的頁面,通常需要聚合多個接口返回的數(shù)據(jù)才能顯示完整的頁面,所以在這種情況下,Node 就需要聚合多個接口的返回結(jié)果,然后將合并后的數(shù)據(jù)返回給前端。
- 接口轉(zhuǎn)發(fā):Java 的服務(wù)是不會直接暴露到公網(wǎng)提供給前端使用的,所以在這種情況下,Node 需要承擔(dān)接口轉(zhuǎn)發(fā)的角色。
而對于 Java 這一層,就需要承擔(dān)業(yè)務(wù)邏輯以及緩存等復(fù)雜的操作,這里就不做過多的介紹了。
4. Node 如何調(diào)用 Java 接口?
那么服務(wù)化拆分之后,首先要解決的一個問題是:Node 如何調(diào)用 Java 提供的接口。首先,我們想到的就是 HTTP 的方式,這里說明一下,我們公司采用的分布式服務(wù)化框架是阿里開源的 Dubbo 框架,而 Dubbo 框架本身是支持通過添加注解的方式生成 Restful API 的,所以在初期,我們就是采用這個現(xiàn)成的方案。
而隨著應(yīng)用數(shù)目的增加,這種方式的弊端也逐漸顯現(xiàn)出來,主要有下面幾點:
- 如果某個接口需要暴露給 Node 使用,就需要手動再去添加額外的注解。
- 每增加一個應(yīng)用,運維都需要針對每個應(yīng)用配置域名,不同的環(huán)境又需要配置不同的域名,所以隨著應(yīng)用數(shù)的增加,應(yīng)用域名的管理越來越難維護。
- 相應(yīng)的,node 也需要維護一份很長的域名配置文件。
- 由于 Java 是直接提供 HTTP 接口,所以性能上相對 RPC 的方式會低一點。
所以,我們就調(diào)研了下,看其他公司在使用 Dubbo 框架時,Node 是如何調(diào)用 Java 的?如下圖所示:

首先,Java 應(yīng)用服務(wù)啟動的時候,會往服務(wù)注冊中心注冊服務(wù),這里的服務(wù)注冊中心可能是 ETCD 或者 Zookeeper,然后,Node 應(yīng)用在啟動的時候,會先從服務(wù)注冊中心拉取服務(wù)列表,接著 Node 會跟 Java 服務(wù)建立一條TCP長鏈接,除此之外,Node 還需要負責(zé) Hession 協(xié)議解析以及負載均衡等。
不難發(fā)現(xiàn),這種方式 Node 的職責(zé)就比較重,而且對 Node 開發(fā)的要求會很高。所以,我們對這種方式做了改進,如下圖所示:

我們在 Node 和 Java 之間添加了一層中間代理層 Tether,Tether 是用 Go 語言寫的一個本地代理,Tether 會對外暴露一個 HTTP 的服務(wù),對 Node 來說,只需要通過 HTTP 方式調(diào)用本地的服務(wù)即可,其他服務(wù)化相關(guān)的服務(wù)發(fā)現(xiàn)、協(xié)議解析、負載均衡、長鏈建立維護都交由 Tether 來處理。這樣,Node 這一層就非常輕量了,那么,最終實現(xiàn)出來,Node 是怎么調(diào)用 Java 服務(wù)的呢?如下代碼所示:
const Service = require('../base/BaseService');
class GoodsService extends Service {
/**
* 根據(jù)商品 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 來說,調(diào)用 Java 服務(wù)它只需要關(guān)注三個點:
- 服務(wù)名:服務(wù)名是由 Java 的包名 + 類名組成,例如上面的 com.youzan.ic.service.GoodsService
- 方法名:Java 類對外暴露的方法,例如上面代碼所示的根據(jù)商品 alias 查詢商品詳情的一個方法 getGoodsDetailByAlias
- 參數(shù):參數(shù)就是傳遞給 Java 的參數(shù)列表
最后,總結(jié)下這種方式都有哪些優(yōu)點:
- 第一個是使用簡單,對前端開發(fā)非常友好,只需要通過 HTTP 方式調(diào)用本地的 Tether 服務(wù)即可;
- 第二個是多語言接入成本低,后期如果有其他語言(Python、Ruby)也需要接入整個服務(wù)化體系,也像 Node 一樣,它們都只需要調(diào)用本地 Tether 暴露的 HTTP 服務(wù)即可,沒有額外的開發(fā)成本了。
- 第三個是后期更方便做協(xié)議層的優(yōu)化,因為這種方式 Tether 其實就是一個代理,后期如果需要做協(xié)議層性能上的優(yōu)化,那只需要優(yōu)化 Tether 的性能就可以了。
那么,看到這里,有人可能又會想,這里 Node 也是通過 HTTP 方式調(diào)用 Java 的,性能上是不是也存在問題呢?所以這里我們就做了一些優(yōu)化,如下代碼所示:
const Agent = require('agentkeepalive');
module.exports = new Agent({
maxSockets: 100,
maxFreeSockets: 10,
timeout: 60000,
freeSocketKeepAliveTimeout: 30000,
});
這里,我們引用了一個 agentkeepalive 包,在 HTTP 早期,每個 HTTP 請求都要求打開一個 TCP Socket 連接,并且使用一次之后就斷開這個 TCP 連接,使用 keep-alive 可以改善這種狀態(tài),即在一次 TCP 連接中可以持續(xù)發(fā)送多份數(shù)據(jù)而不會斷開連接。所以通過使用 keep-alive 機制,就可以減少 TCP 連接建立次數(shù)。
四、參考資料
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