Categories
程式開發

在優化Node.js服務時學到的六課經驗


在 Klarna,我們付出了很多努力來幫助我們的開發人員提供高質量和安全的服務。我們為開發人員提供的一項服務是一個運行 A/B 測試的平台。這個平台的一個關鍵組成部分是一組流程,用來針對每個傳入的請求做出決定:將請求暴露給哪種測試(A 或 B)。這一流程進而確定了按鈕用哪種顏色渲染、向用戶顯示哪種佈局,甚至是要使用哪種第三方後端,等等。這些決定會直接影響用戶體驗。

這組流程中每一步的性能都是非常重要的,因為它會在 Klarna 生態系統的關鍵決策路徑中同步使用。對這類流的一個典型要求是 99.9%的請求的處理延遲都要在個位數級別。為了確保我們能持續遵循這些要求,我們開發了一個性能測試管道來對該服務進行負載測試。

要點 1:性能測試可以為我們提供信心,讓我們知道每個新版本的性能都不會打折扣。

儘管在過去兩年時間裡,我們在平台的生產環境下幾乎沒有遇到過任何性能問題,但這些測試確實揭示了一些問題。測試開始幾分鐘後,在中等且穩定的請求速率下,請求持續時間從正常範圍激增至幾秒鐘的水平:

在優化Node.js服務時學到的六課經驗 1我們認為,雖然在生產環境中還沒有發生這種情況,但現實負載“追上”這種模擬負載的壓力也只是時間問題而已,因此這個問題是值得研究的。

要點 2:通過“增加”負載,我們可以在問題影響生產環境之前就將它們暴露出來。

要注意的另一件事是,問題大約需要兩到三分鐘才會出現。在第一個迭代中,我們僅運行了次兩分鐘測試。只有將測試持續時間延長到十分鐘之後,我們才發現了這個問題。

要點 3:長時間的負載測試可以暴露出各種問題。如果一切正常,請嘗試延長測試時間。

一般來說,我們使用以下指標來監控服務:每秒傳入請求的數量、傳入請求的持續時間以及錯誤率。這些指標可以很好地看出服務是否出現了問題。

但當服務出現異常時,這些指標不會提供任何見解。當問題浮現時,你需要知道瓶頸在哪裡。為此,你需要監控 Node.js 運行時使用的資源。顯而易見的指標是 CPU 和內存利用率,但是有時這些並不是實際的瓶頸所在。在我們的例子中,CPU 利用率很低,內存利用率也很低。

Node.js 使用的另一項資源是事件循環。就像我們需要知道進程正在使用多少兆字節的內存一樣,我們也需要知道事件循環需要處理多少“任務”。事件循環是在名為“libuv”的 C++ 庫中實現的。關於事件循環,這裡有一個 Kenneth Gibson 的出色演講

它為這些“任務”使用的術語叫做活動請求(Active Requests)。要追踪的另一個重要指標是活動句柄(Active Handles)的數量,也就是 Node.js 進程持有的打開文件句柄或套接字的數量。有關句柄類型的完整列表,請參見 libuv 文檔

因此,如果測試正在使用 30 個連接,那麼應該可以看到 30 個活動句柄。活動請求是這些句柄上待處理的操作數。哪些操作呢?完整列表可在 libuv 文檔中找到,舉例來說這些可以是讀 / 寫操作。

看一下服務報告的這些指標,就能發現有什麼不對勁。儘管活動句柄的數量符合我們的預期(在此測試中為 30 個左右),但活動請求的數量實在太多了,竟然有數以萬計:

在優化Node.js服務時學到的六課經驗 2不過我們仍然不知道隊列中有哪些類型的請求。按照活動請求的類型細分後,圖像就更加清晰了。在報告的指標中,有一種類型的請求非常突出:UV_GETADDRINFO。當 Node.js 嘗試解析一個 DNS 名稱時就會生成此類請求。

但是為什麼會生成這麼多 DNS 解析請求呢?原來我們正在使用的 StatsD 客戶端嘗試解析每個外發消息的主機名。公平地說,它確實提供了一種緩存 DNS 結果的選項,但是這一選項並不考慮該 DNS 記錄的 TTL,而是無限期地緩存結果。因此,如果該記錄在客戶端已解析之後被更新,則客戶端永遠都不會知道。由於 StatsD 負載均衡器可能已使用其他 IP 重新部署,並​​且我們不能強制重啟服務以更新 DNS 緩存,因此這種無限期緩存結果的方法對我們來說是不可行的。

我們想出的解決方案是在客戶端外部添加適當的 DNS 緩存。給“DNS”模塊打上猴子補丁並不難。結果要好得多:

在優化Node.js服務時學到的六課經驗 3

要點 4:在考慮傳出請求時不要忘記 DNS 解析。另外,不要忽略記錄的 TTL——它真的會毀掉你的應用。

解決這個問題後,我們在服務中重新啟用了更多功能並再次進行了測試。具體來說,我們啟用了一個邏輯,該邏輯為每個傳入請求生成一個到某個 Kafka 主題的消息。測試再次表明,在較長的一段時間內,響應時間(秒)出現了明顯的峰值:

在優化Node.js服務時學到的六課經驗 4查看服務中的指標後發現了一個明顯的問題,就是我們剛剛啟用的功能——向 Kafka 生成消息的延遲非常高:

在優化Node.js服務時學到的六課經驗 5我們決定試著做一次小改進——將傳出的消息排隊在內存中,然後每秒分批刷新一次。再次運行測試,我們發現服務的響應時間有了明顯的改善:

在優化Node.js服務時學到的六課經驗 6

要點 5:批量 I/O 操作!即使在異步情況下,I/O 也很昂貴。

最後的說明:如果沒有一種讓運行結果可重複且一致的測試方法,那麼上述測試也是做不到的。性能測試管道的第一個迭代的結果就是不一致的,所以也沒法為我們提供信心。努力打造出合適的測試管道後,我們就能夠嘗試各種事物,實驗補丁效果,最重要的是有信心證明我們正在尋求的數字不是偶然的產物。

要點 6:在嘗試任何改進之前,你應該先做一次結果可以信賴的測試。

編輯:我收到了一些問題,詢問在這裡使用哪些工具來執行測試。我們在這裡使用了下面這些工具:

負載由一個內部工俱生成,其簡化了以分佈式模式運行 Locust 的過程。基本上,我們只需要運行一個命令,該工具就會啟動負載生成器,為它們提供測試腳本,並將結果收集到 Grafana 的儀表板上,也就是上文中的黑色屏幕截圖。這是測試中(客戶端)的視角。

被測服務正在向 Datadog 報告指標,也就是上文中的白色屏幕截圖。

原文鏈接

6 Lessons Learned From Optimizing The Performance of a Node.js Service