排解 Java 用戶端程式庫的延遲問題

簡介

如果應用程式的延遲時間高於平常、輸送量不佳,或是 Java Datastore 用戶端發生逾時情形,問題可能源自於用戶端 gRPC 設定,而非 Firestore/Datastore 後端。本指南將協助您診斷及解決常見的用戶端節流問題、不當的通道集區設定,以及過多的通道流失。

診斷

使用 Java 用戶端時,啟用詳細的 gRPC 和 gax-java 記錄檔,對於監控通道集區動態、診斷節流、連線重複使用問題,或找出過多的通道流失,都至關重要。

為 Java 用戶端啟用記錄功能

如要啟用詳細記錄功能,請按照下列方式修改 logging.properties 檔案:

## This tracks the lifecycle events of each grpc channel 
io.grpc.ChannelLogger.level=FINEST
## Tracks channel pool events(resizing, shrinking) from GAX level
com.google.api.gax.grpc.ChannelPool.level=FINEST

此外,請更新 logging.properties 中的輸出記錄層級,擷取這些記錄:


# This could be changed to a file or other log output
handlers=java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level=FINEST
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter

套用設定

你可以透過下列任一方式套用 logging.properties 檔案:

1. 透過 JVM 系統屬性

啟動 Java 應用程式時,請新增這個引數:

-Djava.util.logging.config.file=/path/to/logging.properties

2. 以程式輔助方式載入

這個方法適用於整合測試或應用程式,因為記錄設定是在程式碼中管理。請確保這段程式碼在應用程式生命週期的早期執行。

LogManager logManager = LogManager.getLogManager();
  try (final InputStream is = Main.class.getResourceAsStream("/logging.properties")) {
logManager.readConfiguration(is);
}

記錄範例

啟用詳細記錄後,您會看到 com.google.api.gax.grpc.ChannelPoolio.grpc.ChannelLogger 的混合訊息:

頻道資源不足,觸發頻道集區擴充:

09:15:30.123 [pool-1-thread-1] DEBUG com.google.api.gax.grpc.ChannelPool - Detected throughput peak of 40, expanding channel pool size: 4 -> 6. 
09:15:30.124 [grpc-nio-worker-ELG-1-5] DEBUG io.grpc.ChannelLogger - [Channel<5>: (datastore.googleapis.com:443)] Entering IDLE state 
09:15:30.124 [grpc-nio-worker-ELG-1-5] DEBUG io.grpc.ChannelLogger - [Channel<6>: (datastore.googleapis.com:443)] Entering IDLE state 
09:15:30.125 [grpc-nio-worker-ELG-1-5] TRACE io.grpc.ChannelLogger - [Channel<5>: (datastore.googleapis.com:443)] newCall() called 
09:15:30.126 [grpc-nio-worker-ELG-1-5] DEBUG io.grpc.ChannelLogger - [Channel<5>: (datastore.googleapis.com:443)] Entering CONNECTING state 09:15:30.127 [grpc-nio-worker-ELG-1-5] DEBUG io.grpc.ChannelLogger - [Channel<5>: (datastore.googleapis.com:443)] Entering READY state with picker: Picker{result=PickResult{subchannel=Subchannel<7>: (datastore.googleapis.com:443), streamTracerFactory=null, status=Status{code=OK, description=null, cause=null}, drop=false, authority-override=null}} 
09:15:31.201 [grpc-nio-worker-ELG-1-6] TRACE io.grpc.ChannelLogger - [Channel<6>: (datastore.googleapis.com:443)] newCall() called 
09:15:31.202 [grpc-nio-worker-ELG-1-6] DEBUG io.grpc.ChannelLogger - [Channel<6>: (datastore.googleapis.com:443)] Entering CONNECTING state 09:15:31.203 [grpc-nio-worker-ELG-1-6] DEBUG io.grpc.ChannelLogger - [Channel<6>: (datastore.googleapis.com:443)] Entering READY state with picker: Picker{result=PickResult{subchannel=Subchannel<8>: (datastore.googleapis.com:443), streamTracerFactory=null, status=Status{code=OK, description=null, cause=null}, drop=false, authority-override=null}}

頻道過度佈建,導致頻道集區縮減:

09:13:59.609 [grpc-nio-worker-ELG-1-4] DEBUG io.grpc.ChannelLogger - [Channel<21>: (datastore.googleapis.com:443)] Entering READY state with picker: Picker{result=PickResult{subchannel=Subchannel<23>: (datastore.googleapis.com:443), streamTracerFactory=null, status=Status{code=OK, description=null, cause=null}, drop=false, authority-override=null}}
09:14:01.998 [pool-1-thread-1] DEBUG com.google.api.gax.grpc.ChannelPool - Detected throughput drop to 0, shrinking channel pool size: 8 -> 6.
09:14:01.999 [pool-1-thread-1] TRACE io.grpc.ChannelLogger - [Channel<13>: (datastore.googleapis.com:443)] shutdown() called
09:14:01.999 [pool-1-thread-1] DEBUG io.grpc.ChannelLogger - [Channel<13>: (datastore.googleapis.com:443)] Entering SHUTDOWN state
09:14:01.999 [pool-1-thread-1] DEBUG io.grpc.ChannelLogger - [Channel<13>: (datastore.googleapis.com:443)] Terminated
09:14:01.999 [pool-1-thread-1] TRACE io.grpc.ChannelLogger - [Channel<15>: (datastore.googleapis.com:443)] shutdown() called
09:14:01.999 [pool-1-thread-1] DEBUG io.grpc.ChannelLogger - [Channel<15>: (datastore.googleapis.com:443)] Entering SHUTDOWN state
09:14:01.999 [pool-1-thread-1] DEBUG io.grpc.ChannelLogger - [Channel<15>: (datastore.googleapis.com:443)] Terminated

這些記錄項目有下列作用:

  1. 監控頻道大小調整決策
  2. 辨識頻道重複使用與流失率
  3. 找出傳輸連線問題

解讀記錄檔以診斷症狀

如要排解高延遲或錯誤等效能問題,請先為 com.google.api.gax.grpc.ChannelPoolio.grpc.ChannelLogger 啟用記錄功能。然後,將您觀察到的症狀與本指南中的常見情境進行比對,解讀記錄並找出解決方法。

徵兆 1:啟動時或流量激增期間延遲時間較長

您可能會發現應用程式啟動後的前幾項要求速度緩慢,或是流量突然增加時,延遲時間會大幅增加。

可能原因

如果頻道集區的資源不足,通常就會發生這種延遲情形。每個 gRPC 管道可處理的並行要求數量有限 (Google 中介軟體限制為 100 個)。達到這個上限後,新的 RPC 會在用戶端排隊,等待可用時段。排隊是造成延遲的主要原因。

雖然通道集區的設計可因應負載變化,但其大小調整邏輯會定期執行 (預設為每分鐘),並逐步擴充 (預設為一次最多新增 2 個通道)。因此,從流量最初暴增到集區擴充之間,會有固有的延遲。在這段期間,新要求會等待飽和的管道清除,或是等待集區在下一個調整大小週期新增管道,因此會發生延遲。

應留意的記錄

尋找顯示通道集區正在擴充的記錄。這是主要證據,證明用戶端正在因應超出目前容量的流量高峰。

頻道集區擴充記錄:

[pool-1-thread-1] DEBUG com.google.api.gax.grpc.ChannelPool - Detected throughput peak of 40, expanding channel pool size: 4 -> 6.

解讀:集區偵測到流量暴增,因此正在開啟新連線來處理增加的負載。如果擴充作業頻繁發生,尤其是在啟動附近,這就表示初始設定不足以應付一般工作負載。

如何解決

目標是在流量湧入前準備好足夠的管道,避免客戶在壓力下建立管道。

增加 initialChannelCount (啟動時延遲時間較長):

如果發現啟動時延遲時間較長,最有效的解決方法是增加 initialChannelCount 中的 ChannelPoolSettings。根據預設,這個值會設為 10,對於在啟動時處理大量流量的應用程式來說,這個值可能太低。

如要找出應用程式的正確值,請按照下列步驟操作:

  1. com.google.api.gax.grpc.ChannelPool 啟用 FINEST 層級記錄。
  2. 在一般啟動工作負載或流量高峰下執行應用程式。
  3. 觀察管道集區擴充記錄,瞭解集區穩定時的大小。

舉例來說,如果看到類似以下的記錄:
[pool-1-thread-1] DEBUG com.google.api.gax.grpc.ChannelPool - Detected throughput peak of 80, expanding channel pool size: 4 -> 6.

一分鐘後,您會看到記錄:

[pool-1-thread-1] DEBUG com.google.api.gax.grpc.ChannelPool - Detected throughput peak of 95, expanding channel pool size: 6 -> 8.

如果發現擴充的管道集區大小穩定在 8 個管道,且找不到更多擴充記錄,表示應用程式需要 8 個管道才能處理工作負載。建議先將 initialChannelCount 設為 8,這可確保預先建立連線,減少或消除即時調整大小造成的延遲。

目標是確保集區有足夠的容量來處理尖峰負載,且不會將要求加入佇列。

增加 minChannelCount (流量暴增):

如果流量尖峰期開始時發生高延遲,表示流量增加速度比管道集區建立新連線的速度快。如果應用程式的流量會突然暴增,這項功能就非常實用。

在正常流量期間,集區會穩定使用少量管道,並在流量增加時主動新增管道,避免飽和。這個擴充程序需要時間,因此爆量湧入的初始要求會排入佇列,導致延遲時間較長。

增加 minChannelCount 會提高永久開放頻道的基準。這可確保系統已具備足夠的容量來處理初始爆量,進而消除擴充延遲和相關的延遲尖峰。

增加 maxChannelCount (流量暴增):

如果在流量尖峰期間發現延遲時間較長,且記錄檔顯示通道集區持續達到上限 (maxChannelCount),表示目前的上限可能太低,無法因應尖峰流量。根據預設,maxChannelCount 設為 200。如要判斷更合適的值,請參閱連線集區設定指南,根據應用程式的每秒查詢次數 (QPS) 峰值和平均要求延遲時間,計算出最佳連線數。

問題 2:間歇性逾時或遠端程序呼叫 (RPC) 失敗

有時應用程式大部分時間都能正常運作,但偶爾會發生間歇性逾時或 RPC 失敗的情況,即使在正常流量期間也是如此。

可能原因:網路不穩定

用戶端與 Datastore 服務之間的連線遭到捨棄。gRPC 用戶端會嘗試自動重新連線,但這個程序可能會導致暫時性故障和延遲。

應留意的記錄

診斷網路問題時,最重要的記錄是 TRANSIENT_FAILURE

  • 暫時性失敗記錄:
[grpc-nio-worker-ELG-1-7] DEBUG io.grpc.ChannelLogger - [Channel<9>: (datastore.googleapis.com:443)] Entering TRANSIENT_FAILURE state
  • 解讀:這項記錄是重大警訊,表示頻道已失去連線。單一的獨立失敗事件可能只是輕微的網路故障。不過,如果經常看到這些訊息,或是頻道卡在這個狀態,就表示有重大潛在問題。

如何解決

調查網路環境。檢查應用程式與 datastore.googleapis.com 之間的防火牆、Proxy、路由器或一般網路不穩定問題。

症狀 3:每個用戶端超過 20,000 個並行要求,導致延遲時間過長

這項特定症狀適用於以極大規模執行的應用程式,通常是單一用戶端執行個體必須處理超過 20,000 個並行要求時。應用程式在正常情況下運作良好,但當流量增加到每個用戶端超過 20,000 個並行要求時,您會發現效能急劇下降。延遲時間大幅增加,且在流量高峰期內維持高延遲狀態。這是因為用戶端的連線集區已達上限,無法進一步擴充。

可能原因

由於管道集區已達到設定的 maxChannelCount,因此用戶端出現管道飽和問題。根據預設,集區設定的管道上限為 200 個。由於每個 gRPC 管道最多可處理 100 個並行要求,因此只有在單一用戶端執行個體同時處理約 20,000 個要求時,才會達到這個限制。

集區達到上限後,就無法再建立連線。所有 200 個管道都會過載,且用戶端會將新要求加入佇列,導致延遲時間大幅增加。

下列記錄有助於確認已抵達 maxChannelCount

應留意的記錄

主要指標是觀察通道集區是否在達到設定上限時停止擴充,即使負載持續增加也是如此。

  • 記錄:您會看到集區展開,顯示先前的記錄。使用預設設定時,您在延遲尖峰前看到的最後一個擴充記錄,會是集區達到 200 個管道的記錄:
[pool-1-thread-1] DEBUG com.google.api.gax.grpc.ChannelPool - ... expanding channel pool size: 198 -> 200.
  • 指標:延遲時間過長時,您不會再看到「擴充管道集區大小」記錄。如果沒有這些記錄,加上延遲時間較長,就表示已達到 maxChannelCount 上限。

如何解決

目標是確保集區有足夠的容量來處理尖峰負載,且不會將要求加入佇列。

  • 增加 maxChannelCount:主要解決方案是將 maxChannelCount 設定增加至可支援應用程式尖峰流量的值。請參閱連線集區設定指南,根據應用程式的每秒查詢次數 (QPS) 峰值和平均要求延遲時間,計算最佳連線數量。

附錄

以下各節提供補充資訊,協助您排解問題。

瞭解頻道狀態

記錄中可能會顯示下列管道狀態,有助於瞭解連線行為:

狀態 說明
IDLE 通道已建立,但沒有任何有效連線或 RPC。正在等待流量。
連線中 管道正積極嘗試與 gRPC 伺服器建立新的網路傳輸 (連線)。
準備就緒 管道已建立並正常運作,隨時可以傳送 RPC。
TRANSIENT_FAILURE 管道發生可復原的故障 (例如網路中斷、伺服器暫時無法使用)。系統會自動嘗試重新連線。
SHUTDOWN 頻道已關閉,可能是手動關閉 (例如shutdown() 呼叫) 或因閒置逾時而中斷。無法啟動新的 RPC。

提示

  • 如果您使用 SLF4J 或 Logback 等結構化記錄架構,則必須在 logback.xml 或其他記錄器設定檔中設定對應的記錄等級。記錄外觀會對應 java.util.logging 層級。