4月21日,有贊舉辦了第一屆「有贊技術開發日」的活動,我做爲分享講師,分享了有贊最近一年在 Node 這一塊的實踐經驗。但因爲分享時間有限,我也只能把最重要的內容拿出來和你們分享,因此這個週末就花了幾個小時時間,結合那次的分享,並完善了其中的一些內容,寫了這篇文章,但願能夠給你們帶來新的啓發。javascript
有贊最先的一個比較完整的 Node 項目是公司內部的一個管理系統,這個系統是用 Node 全棧開發的,主要包括一個給 HR 用的員工管理系統和給小夥伴用的 APP。就像大多數公司同樣,咱們第一個 Node 項目也是直接用 Koa,而後整合一些開源的中間件,這樣就快速的把項目搭建起來了。前端
這個項目作了半年以後,咱們把 Node 該踩的坑基本也都踩了一遍,因此咱們就開始嘗試在對外產品上使用 Node了,咱們第一個嘗試改造的項目是公司的官網,這是最簡單的一個項目,基本沒什麼大的風險。java
第二個項目咱們不可能再按照以前的方式,簡單用 Koa 加上一堆中間件的方式來搭建項目了,由於已經有了以前的經驗,因此咱們就整理了下這一套方案,抽離出了一個項目模板,每一個新項目只要把這個模板克隆下來,而後改一下配置,就能夠快速搭建出一個新的項目來。node
項目多了以後,這種方式弊端很快就顯現出來了,由於模板代碼和業務代碼是耦合在一塊兒,若是要改模板生成的代碼,只能每一個項目手動更新,而隨着時間的推移,愈來愈難保持同步了,每一個項目的目錄結構和代碼風格可能也會變得很是不同,因此,解耦框架代碼和業務代碼就很是重要了。因此咱們就在腳手架模板的基礎上抽離出了一個框架叫 Astroboy(阿童木),這個框架是在 Koa 的基礎上封裝的,這樣,每一個項目都基於這個框架開發,若是框架更新了,項目也只須要更改下框架的版本號。git
不少項目都開始用 Node 了,新的問題又出現了,由於每一個產品的業務場景都不同,對框架的需求也都不同。例如某個中間件,產品 A 可能須要,而產品 B 可能根本不須要這個中間件,而這個時候的框架又不支持定製改造。因此對框架來講,又提出了新的挑戰,因此在今年年初,對框架作了一次大的重構。github
此次重構在阿童木 1.0 的基礎上,加入了不少新特性,主要有如下幾點:apache
首先提供基於 Astroboy 定製上層框架的能力,以下圖所示,Youzan Base Framework 是在阿童木的基礎上定製的一個有贊最基礎的 Node Web 框架,這一層主要集成了一些有贊最基礎的服務,像:npm
有了 Youzan Base Framework 後,咱們就須要在上面開發業務了,這個分兩種業務場景:對於一些簡單單一的業務,直接繼承 Youzan Base Framework 開發就能夠了;而若是是一些複雜的業務,就能夠先在 Youzan Base Framework 的基礎上,定製出一個業務框架,像咱們有贊原先有一個超大的 PHP 項目(咱們叫 Iron),那麼服務化拆分後,Node 就承擔了原先 PHP 的部分,因此咱們新先定製了一個業務級的框架叫 Iron Base Framework,而後再按照業務模塊(交易、店鋪、用戶、營銷)拆分紅多個子項目。後端
其次是支持插件化,關於這一點,可查看下面關於插件的說明。緩存
以上介紹了有贊 Node 基礎框架迭代和演變的過程,下面主要介紹下阿童木2.0 框架的幾個核心概念
應用 Application 的概念很好理解,在這裏應用就能夠理解成一個項目,它是從框架繼承下來,而且實例化以後的一個實例,應用也是由一個一個插件構成的。
Astroboy 框架是在 Koa2 的基礎上封裝的,關於框架的概念,這裏就再也不作過多的介紹了。
插件化是軟件設計中一個很重要的思想,不少軟件像 Eclipse 都支持這樣的特性,插件化可讓咱們的系統解耦,每一個模塊作到獨立開發,而模塊之間又不會相互影響,這樣的特性對於大型項目來講是很是重要的。
插件化是 Astroboy 框架中最核心的一個實現,它是服務(Service)、中間件(Middleware)和工具函數庫(Lib)等的載體,它本質上仍是 NPM 包,只不過是在 NPM 包的基礎上,作了更深層次的抽象。基於 Astroboy 的應用,就是由一個一個的 Plugin 組成的,Plugin 就是咱們手中的積木,經過 Astroboy 的框架引擎把這些積木組織在一塊兒,就造成了系統。
那麼插件跟普通的 NPM 包有什麼區別呢?
插件約定了目錄結構,這樣每一個插件看起來都是相似的,這對於團隊的協做是很是重要,若是每一個模塊看起來都不同,那麼團隊的協做成本就會很高。 應用啓動後,插件的代碼是自動注入到整個應用的,只須要在插件的配置文件裏面開啓這個插件便可。
一個插件能夠包含哪些信息?
插件的管理
'astroboy-cookie': {
enable: true,
path: path.resolve(__dirname, '../plugins/astroboy-cookie')
}
複製代碼
enable 設置成 true 就能夠開啓這個插件,path 表示插件的絕對路徑,這種通常適合於還在快速迭代中的插件,若是插件已經很穩定了,你就能夠把這個插件打包發佈成一個 NPM 包,而後經過 package 聲明你的插件便可,以下代碼所示:
'astroboy-cookie': {
enable: true,
package: 'astroboy-cookie'
}
複製代碼
隨着公司業務的發展,網站應用的規模不斷擴大,垂直應用愈來愈多,應用之間交互不可避免,將核心業務抽取出來,做爲獨立的服務,逐漸造成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提升業務複用及整合的分佈式服務框架(RPC)是關鍵,因此在這個時候,分佈式服務架構就勢在必行了。
在介紹技術棧選擇以前,先講一下公司的一些技術背景。
在公司成立初期,爲了可以快速開發,把產品快速作出來推出市場,因此咱們選擇用 PHP 語言,我想這也是大多數創業公司的選擇。而隨着業務的發展,PHP 愈來愈難處理複雜的業務。
因此等到了必定時候,咱們開始作服務化拆分,那麼首先考慮的就是底層技術的選擇,咱們從下面幾點考慮:
對於 Node 層,咱們的定位是一層很薄的中間層,Node 這一層不會過多地處理業務邏輯,業務邏輯所有都交給 Java 來處理,它只負責下面三件事情:
而對於 Java 這一層,就須要承擔業務邏輯以及緩存等複雜的操做,這裏就不作過多的介紹了。
那麼服務化拆分以後,首先要解決的一個問題是:Node 如何調用 Java 提供的接口。首先,咱們想到的就是 HTTP 的方式,這裏說明一下,咱們公司採用的分佈式服務化框架是阿里開源的 Dubbo 框架,而 Dubbo 框架自己是支持經過添加註解的方式生成 Restful API 的,因此在初期,咱們就是採用這個現成的方案。
而隨着應用數目的增長,這種方式的弊端也逐漸顯現出來,主要有下面幾點:
因此,咱們就調研了下,看其餘公司在使用 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 服務它只須要關注三個點:
最後,總結下這種方式都有哪些優勢:
那麼,看到這裏,有人可能又會想,這裏 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