Node.js在攜程的落地和最佳實踐
- 2019 年 12 月 6 日
- 筆記

作者|潘斐斐
本文主要介紹在攜程,Node.js 技術棧是如何從 0 到 1 進行技術落地的,以及在不斷磨合的過程中,總結出來的最佳實踐。
在攜程 Node.js 應用根據用戶群,主要分兩個方向:
DA(數據聚合服務)和 SSR(服務端渲染)是服務於外部用戶的,目標是提升用戶體驗。當然,DA 和 SSR 同時也提升了開發效率,例如前端開發人員可以更加靈活地整合數據,同構給開發人員省去了大量重複的開發工作量;
公司桌面工具(例如內部 IM 等)是服務於內部員工的,一般使用 Electron,開發維護成本低,產品迭代快。
一、Node.js 工程化
基於上述三個場景, 目前攜程有一套 Node.js 的工程化方案。工程化的方案並不是一成不變的, 在任何階段遇到了實際問題, 都會更新甚至推翻一些步驟,為的就是更好地服務於整個應用開發的生命周期。
工程化涵蓋五大部分:開發、構建、測試、發佈和運維。
1.1 開發
腳手架
有三個類型的腳手架:Web Application、DA Service 和 Desktop Tools。這三種類型的腳手架會服務於上述提到的三種場景。
這三種腳手架有共同點:標準化的 Docker 日誌、預置統一的中間件。但同時他們也是有差異的,例如 Desktop Tools 和 Web Application 的應用模型不一樣, Desktop 有 UI 層,那麼 UI 層和應用層上的應用日誌和用戶行為如何關聯,方便後續的排障;DA Service 需要將應用的健康狀況周期性上報給治理中心、熔斷機制等等,這些框架層面的差異,腳手架會集成進去,做業務開發同學可以不用關心這些基礎設施的接入。
核心中間件
核心中間件主要是做基礎設施的建設。目前有 20 多個中間件,主要的中間件如下:

圖1. 核心中間件介紹
- 存儲服務 主要應用於長期的固化存儲,例如靜態資源。主要提供的是 Ceph 客戶端。
- 業務服務 主要應用於 DA 場景,提供 SOA Client 和 SOA Service。SOA Client 用來獲取數據,需要重點關注的是讀取性能和容錯處理;SOA Service 用來提供對外的聚合服務,需要重點關注的是穩定性和響應性能。
- 監控服務 涵蓋所有的應用,提供三個維度的監控:Tracing、Metrics 和 Logging。具體介紹請參看下方」運維」部分。
- 公共服務 主要包括配置中心、ABTest 的客戶端、數據訪問層等。
- 緩存服務 主要用於配置信息的緩存、應用數據的緩存。提供 Redis 客戶端和共享內存兩個中間件。
1.2 構建
Docker 鏡像
Node.js 的版本更新頻率很快,每 6 個月會發佈一個大版本的升級,期間會陸續出很多小版本。如果為每個版本都做一個鏡像,會帶來極高的開發和運維成本。基於更新頻率,我們目前選取 2 個固定版本,在 Node.js 版本更替的時候,可以保證一個穩定的鏡像。
安裝依賴包
為了提升開發效率,在構建時安裝依賴包需要保證速度快。如果中間件中用到一部分 C++ 模塊,那麼在安裝時會做實時編譯,這樣會導致耗時長,甚至會因為環境問題編譯失敗。所以我們會將用到 C++ 模塊的中間件做一下預編譯,為 windows、linux 和 mac 這三個平台分別編譯出 2 個固定版本的預編譯包。
依賴包掃描
掃描的目的主要解決幾個問題:
- 應用中不同的包如果引用了同一個子包,但是子包的版本不一致,就會導致應用中裝了多個版本同一個包,會引發 bug;
- 中間件缺乏治理能力。通過掃描依賴包,能夠做到中間件統一收口。一旦要升級,可以很快通知到開發做快速升級。例如第三方依賴包有安全問題,可以在構建環節就提醒開發人員升級版本。
1.3 測試
目前測試環節包括單元測試、集成測試、壓力測試和自動化測試。自動化測試主要針對 Service 和 UI 兩方面測試。UI 自動化測試使用的是 Puppteer。每次代碼更新,會走一遍自動化測試流程,保證代碼質量。
1.4 發佈
攜程雲和公有雲
每個雲的部署環境、網絡、位置等差異,會帶來應用訪問差異,例如訪問異常,網絡延遲等。這些差異需要在基礎設施層面抹平,避免放在應用邏輯層面處理。
應用一體化發佈
一體化發佈也可以理解為一鍵發佈。一條發佈指令包含了應用核心框架、靜態資源、配置的同時發佈,而不需要開發人員思考什麼步驟需要發什麼資源。這樣不僅可以提升效率,還能有效地控制發佈回滾。
私有 npm 包發佈
私有包的發佈和 GIT 做高度集成。原因是:第一,可以通過 git 做快速的發佈;第二,有歷史可查,方便查看到每個版本發佈的時間、人員;第三,有權限控制,避免發生生產級別故障。
1.5 運維
運維是整個環節中最重要也是最容易被忽略的環節。一個應用上線只是開始,真正要關注的一定是運維指標。
日誌監控
三種維度的監控:tracing、logging 和 metric。

圖2. 三種維度的監控 圖片來源於網絡:https://zhuanlan.zhihu.com/p/28075841
Tracing 提供的是整個請求過程中的數據,例如請求信息(頭部、地址)、響應信息(狀態碼,響應體)、請求耗時、調用鏈等信息。
Logging 提供的是在請求處理過程中,每一個具體的事件埋點,這些埋點相對是分散的。可以是記錄普通的日誌,也可以是記錄拋出的錯誤。
Metric 提供的是聚合數據。最大的特徵是可聚合的,它展現的是一個時間跨度中的某個維度的指標。一般用來記錄量化的指標,例如訪問量、性能等數據。
應用排障
一般我們排查問題的時候,會先通過 Metric 的聚合指標發掘出異常,然後追蹤到某一批有異常的 Tracing,可以查看到調用鏈、耗時等具體情況,也可以跟蹤到某一個請求,查看裏面的事件埋點。也有其他方式的排障,例如下圖中展示,可以在線直接通過一個特殊的地址訪問到的一張火焰圖,非常迅速地排障。當有用戶說這個頁面出現問題,打開這個頁面排障,可以定位到底哪個對應的地方出現問題。

圖3. 火焰圖
二、Node.js 最佳實踐
2.1 部署模型

圖4. 部署模型
Node.js 應用部署在 Docker 上,採用 Nginx+PM2 的模式。
2.2 問題一:多進程通信
多進程通信主要用於數據交換,最常見的有 2 種場景:
- 提供 SequenceId:在單台機器需要提供唯一且按時間序列排列的 ID。
- 提供遠端配置信息:當獲取遠端配置信息時,需要考慮多進程的共享分發。

圖5. 多進程通信 V1.0
在第一版本設計中,我們採用的是 IPC 機制進行多進程的通信。Master 作為一個中轉站,當 Slave 有消息分發時,通知給 Master,再由 Master 分發給各 Slave,從而達到進程之間通信的效果。但是上線之後發現,這樣的機制會遇到幾個問題:數據量必須控制好體積;數據的同步會有延遲;Master 必須時刻在線,一旦 Master 進程掛掉,就需要等待重啟再重連。
基於這些問題,我們重新設計了第二個版本。

圖6. 多進程通信 V2.0
在第二個版本的設計中,我們使用了共享內存(shared memory)。舉一個場景為例,當需要獲取某個配置的時候,先將這塊內存鎖定,嘗試從內存中獲取數據。如果判斷數據存在且在有效期內,那麼解鎖並從內存中讀取數據返回,否則從服務端獲取數據,當服務端有數據返回時,將數據和有效期更新到內存中,解鎖並從內存中讀取數據返回。通過共享內存的機制,可以非常輕量級且高效地實現多進程之間的數據共享。
2.3 問題二:監控什麼內容

圖7. 監控指標
Nginx 會監控整個 Docker 上所有應用的情況:
- CPU util:CPU 總的使用率。
- CPU throttle count&time:CPU 被限制的次數和 CPU 使用率被限制的總時間。這兩個指標的上升一般表示應用有 CPU 密集型操作,需要檢查一下是否有大量的計算等操作。
- Mem RSS used:這個指標上升一般顯示應用內存泄漏的問題。
- HTTP imcoming&outgoging:http request 的數量變化趨勢。如果有錯誤響應或者超過了告警的閾值,則會在趨勢圖中顯示。
- Connection reset:這個指標如果上升,表示應用出現了大量的拒絕請求,例如服務器的並發數超過了原本的承載量等原因。
Nginx 中監控的是整個 Docker 的情況,但是我們更需要的是監控應用的指標。應用一般採用 PM2 cluster –i max 模式啟動,最大化利用 CPU。
Heartbeat(心跳信息)
每個 slave 一分鐘發送一次 Heartbeat(心跳信息)給到 CAT 數據中心。一般來說,如果 Heartbeat 告警的話,需要立刻查看一下錯誤日誌,是不是有異常錯誤導致進程已經退出了。
Heartbeat 主要包括 CPU、Memory、網絡信息等。這些信息和上述提到的 Nginx 信息不是一個維度的。它更細節地關注了應用的情況,而不是整個 Docker 的情況。如果需要分析應用細節的問題,需要查看這裡的 Heartbeat 信息。
性能情況
一般來說,中間件會處理應用常規的性能日誌記錄。包括:
- 每一個響應的請求耗時(服務端邏輯處理耗時,不包括網絡耗時)。
- 每一個 Transaction 的耗時。一個 Transaction 可以簡單理解為一個有功能意義的代碼片段。
- 跨應用調用的請求耗時。
錯誤 / 告警信息
錯誤告警信息是應用中需要重點關注的,包括:
- 應用邏輯出錯,例如處理 JSON 數據出錯等。
- HTTP 請求出錯,會記錄狀態碼、請求地址、返回內容。
- 應用中使用了不同版本的同一個包,會報一條告警信息通知開發工程師。
詳細數據日誌
詳細數據日誌一般有開發工程師針對應用的邏輯埋點,而非中間件統一處理。這些日誌會包括返回數據的記錄,具體運行在哪一段 transaction 中。這些日誌一般是故障發生時,用來複盤時的輔助手段。
2.4 問題三:全鏈路監控
全鏈路監控指的是端到端的監控,監控的是一系列的調用鏈情況。

圖8. Tracing 模型
在介紹全鏈路模型之前,首先介紹 Tracing 模型(圖 8)。Tracing 模型是一個樹狀結構的模型。以一個場景為例,當用戶發起一個請求,這個請求的處理中有三段邏輯(authentication、soa request 和 data aggregation)。在整個請求體外層會有一個 Transaction#1,記錄請求響應等信息。每一個邏輯段會對應一個 Transaction#2,Transaction#2 的父節點是 Transaction#1。Transaction#2 中可以有多個 Logging 信息,根據類型可以分為 Event/Error/Log,也可以包含 Metric 信息。這些 Logging 和 Metric 都有父節點,是 Transacation#2。按照這樣的結構可以將一整個 request 的過程的監控信息記錄下來。
要做全鏈路監控,就是需要將每個 request 和調用鏈做關聯。
在過程中遇到的最核心的問題是,如何將上下文進行關聯。第一個版本使用的是 domain 的模塊,使用 domain 的 add api 將上下文信息記錄下來,使用 run api 運行邏輯代碼塊。第二個正在測試中的版本是使用 async_hook 的模塊,引入了生命周期的概念,通過 executionAsyncId 和 ttriggerAsyncId 可以追蹤每個函數體。

圖9. 頁面請求模型
通過上圖的頁面請求模型可以將每個請求做關聯,從而達到全鏈路監控的效果。
三、總結

- Node.js 工程化需要結合業務,反覆磨合;
- 設計好運維指標,做好 Tracing/Logging/Metric 的結合;
- 密切關註上線之後的監控指標,防止內存泄漏;
- 發掘出 Node.js 技術棧的差異,有針對性地解決問題;
- 不要盲目相信同一個技術棧,合適才是最好的。
作者介紹
潘斐斐,Trip.com 高級研發經理。2008 年加入攜程,目前工作內容為 Node.js 框架平台整體構建、產品性能優化和創新型項目研發。
本文來自在 2019 攜程技術峰會上的分享。