Categories
程式開發

Node.js在攜程的落地和最佳實踐


本文主要介紹在攜程,Node.js技術棧是如何從0到1進行技術落地的,以及在不斷磨合的過程中,總結出來的最佳實踐。

Node.js在攜程的落地和最佳實踐 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多個中間件,主要的中間件如下:

Node.js在攜程的落地和最佳實踐 2

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

Node.js在攜程的落地和最佳實踐 3

圖2. 三種維度的監控 圖片來源於網絡:https://zhuanlan.zhihu.com/p/28075841

Tracing提供的是整個請求過程中的數據,例如請求信息(頭部、地址)、響應信息(狀態碼,響應體)、請求耗時、調用鍊等信息。

Logging提供的是在請求處理過程中,每一個具體的事件埋點,這些埋點相對是分散的。可以是記錄普通的日誌,也可以是記錄拋出的錯誤。

Metric提供的是聚合數據。最大的特徵是可聚合的,它展現的是一個時間跨度中的某個維度的指標。一般用來記錄量化的指標,例如訪問量、性能等數據。

應用排障

一般我們排查問題的時候,會先通過Metric的聚合指標發掘出異常,然後追踪到某一批有異常的Tracing,可以查看到調用鏈、耗時等具體情況,也可以跟踪到某一個請求,查看裡面的事件埋點。也有其他方式的排障,例如下圖中展示,可以在線直接通過一個特殊的地址訪問到的一張火焰圖,可以非常快速地去排障。當有用戶說這個頁面出現問題,打開這個頁面排障,可以定位到底那個對應的地方出現問題。

Node.js在攜程的落地和最佳實踐 4

圖3. 火焰圖

二、Node.js最佳實踐

2.1 部署模型

Node.js在攜程的落地和最佳實踐 5

圖4. 部署模型

Node.js應用部署在Docker上,採用Nginx+PM2的模式。

2.2 問題一:多進程通信

多進程通信主要用於數據交換,最常見的有2種場景:

  1. 提供SequenceId:在單台機器需要提供唯一的並且按時間序列排列的ID。

  2. 提供遠端配置信息:當獲取遠端配置信息時,需要考慮多進程的共享分發。

Node.js在攜程的落地和最佳實踐 6

圖5. 多進程通信V1.0

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

基於這些問題,我們重新設計了第二個版本。

Node.js在攜程的落地和最佳實踐 7

圖6. 多進程通信V2.0

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

2.3 問題二:監控什麼內容

Node.js在攜程的落地和最佳實踐 8

圖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 問題三:全鏈路監控

全鏈路監控指的是端到端的監控,監控的是一系列的調用鏈情況。

Node.js在攜程的落地和最佳實踐 9

圖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可以追踪每個函數體。

Node.js在攜程的落地和最佳實踐 10

圖9. 頁面請求模型

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

三、總結

Node.js在攜程的落地和最佳實踐 11

  • Node.js工程化需要結合業務,反复磨合;

  • 設計好運維指標,做好Tracing/Logging/Metric的結合;

  • 密切關注上線之後的監控指標,防止內存洩漏;

  • 發掘出Node.js技術棧的差異,有針對性的解決問題;

  • 不要盲目相信同一個技術棧,合適才是最好的。

作者介紹:

潘斐斐,Trip.com高級研發經理。 2008年加入攜程,目前工作內容為Node.js框架平台整體構建、產品性能優化和創新型項目研發。

本文來自在2019攜程技術峰會上的分享。