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 種場景:

  1. 提供 SequenceId:在單台機器需要提供唯一且按時間序列排列的 ID。
  2. 提供遠端配置信息:當獲取遠端配置信息時,需要考慮多進程的共享分發。

圖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 信息。

性能情況

一般來說,中間件會處理應用常規的性能日誌記錄。包括:

  1. 每一個響應的請求耗時(服務端邏輯處理耗時,不包括網絡耗時)。
  2. 每一個 Transaction 的耗時。一個 Transaction 可以簡單理解為一個有功能意義的代碼片段。
  3. 跨應用調用的請求耗時。

錯誤 / 告警信息

錯誤告警信息是應用中需要重點關注的,包括:

  1. 應用邏輯出錯,例如處理 JSON 數據出錯等。
  2. HTTP 請求出錯,會記錄狀態碼、請求地址、返回內容。
  3. 應用中使用了不同版本的同一個包,會報一條告警信息通知開發工程師。

詳細數據日誌

詳細數據日誌一般有開發工程師針對應用的邏輯埋點,而非中間件統一處理。這些日誌會包括返回數據的記錄,具體運行在哪一段 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 攜程技術峰會上的分享。