線上 HBase Thrift2 服務出現**單線程 CPU 長期 99%+、請求超時率飆升**。經 top -H、jstack、jstat、jmap 多工具鏈定位,根因為 **ClientScanner 未正確關閉導致對象堆積、老年代佔用 99.99%、CMS GC 持續併發失敗觸發 Full GC 風暴**。本文完整復盤現象觀測、線程定位、堆內存分析、代碼級根因、三層治理方案(客戶端修復、服務端補丁、API 替代)與預防體系。
telnet 端口通top -H 顯示 4 線程 CPU 99%+ (PID 20727-20730),呈波動狀態TransportException: EAGAIN (timedout)# 1. 找出高 CPU 線程 ID (十進制) ps -mp 20693 -o THREAD,tid,time | sort -rn # 輸出示例: # 20730 18.8% (nid=0x50fa) # 20729 18.7% (nid=0x50f9) # 20728 18.7% (nid=0x50f8) # 20727 18.7% (nid=0x50f7) # 2. 十六進制 nid 對應 jstack # 0x50f7 = 20727, 0x50f8 = 20728 ... "Gang worker#0 (Parallel CMS Threads)" os_prio=0 tid=0x... nid=0x50f7 runnable "Gang worker#1 (Parallel CMS Threads)" os_prio=0 tid=0x... nid=0x50f8 runnable # 確認:高 CPU 線程均為 CMS 並發 GC 線程
# jmap -histo 20693 | head -30 num #instances #bytes class name 1: 7283521 22518411456 [B (byte[] 數據) 2: 4982714 1993085880 java.util.TreeMap$Entry 3: 1285599 3617087664 java.util.TreeMap 4: 4285217 445662568 org.apache.hadoop.hbase.client.ClientScanner 5: 4285222 377099536 org.apache.hadoop.hbase.client.Scan 6: 4284875 37069000 org.apache.hadoop.hbase.client.ScannerCallable 7: 4285528 342921344 [Ljava.util.HashMap$Node; 8: 4284880 308511360 org.apache.hadoop.hbase.client.ScannerCallableWithReplicas ... 11: 4285283 205693584 org.apache.hadoop.hbase.client.RpcRetryingCaller # 關鍵發現: # ClientScanner 實例 428 萬+,佔用 ~400MB # Scan、ScannerCallable、ScannerCallableWithReplicas 數量一致 # 推斷:大量 Scanner 未關閉,持有底層 RPC 連接、緩衝區、結果集
// 簡化版關鍵邏輯 private final MapscannerMap = new ConcurrentHashMap<>(); private final AtomicInteger nextScannerId = new AtomicInteger(0); @Override public int openScanner(ByteBuffer table, TScan scan) throws TIOError, TException { Table htable = getTable(table); ResultScanner resultScanner = null; try { resultScanner = htable.getScanner(scanFromThrift(scan)); } catch (IOException e) { throw getTIOError(e); } finally { closeTable(htable); // 僅關閉 Table,未關閉 Scanner! } // 將 Scanner 放入 Map,賴以追蹤 return addScanner(resultScanner); // 返回 scannerId } private int addScanner(ResultScanner scanner) { int id = nextScannerId.getAndIncrement(); scannerMap.put(id, scanner); // 關鍵:強引用持有,GC 無法回收 return id; } @Override public void closeScanner(int scannerId) throws TIOError, TIllegalArgument, TException { ResultScanner scanner = scannerMap.get(scannerId); if (scanner == null) throw new TIllegalArgument("Invalid scannerId"); scanner.close(); // 真正釋放資源 scannerMap.remove(scannerId); // 移除引用 }
openScanner → 服務端創建 ResultScanner → 放入 scannerMapcloseScanner**(異常退出、網絡中斷、代碼邏輯漏調)scannerMap 永久強引用 ResultScanner → 關聯 Scan、ClientScanner、RpcRetryingCaller、byte[] 緩衝區全部無法 GCHBASE-17182層級三:API 替代(架構規避)
改用
getScannerResults(一次性拉取所有結果) 替代openScanner/getNext/closeScanner流式 API,避免長連接 Scanner 狀態管理:#!/bin/bash # save-as: restart-thrift2-on-gc-pressure.sh # 監控 Old Gen 使用率 > 90% 且 Full GC 頻率 > 1/min,自動重啟 Thrift2 PID=$(jps -l | grep ThriftServer | awk '{print $1}') if [ -z "$PID" ]; then exit 0; fi O_UTIL=$(jstat -gcutil $PID 1 1 | tail -1 | awk '{print $4}') # O% FGC_RATE=$(jstat -gc $PID 60000 2 | tail -1 | awk '{print $9}') # FGC/min if (( $(echo "$O_UTIL > 90" | bc -l) )) && (( $(echo "$FGC_RATE > 1" | bc -l) )); then echo "$(date) GC pressure detected: O=${O_UTIL}%, FGC/min=${FGC_RATE}, restarting..." kill -TERM $PID sleep 10 # 假設有 supervisord/systemd 管理自動拉起 fi八、 預防體系建設
| 維度 | 措施 | 驗證方式 |
|---|---|---|
| 代碼規範 | 強制 Scanner 使用 try-with-resources,Checkstyle 規則檢查 | CI 門禁 |
| 配置治理 | 啟用 HBASE-17182 補丁,配置 TTL=1h、MaxScanners=10000 | 啟動檢查腳本 |
| 監控告警 | Thrift2 進程 Old Gen% > 80%、FGC/min > 0.5、ScannerMapSize > 5000 告警 | Prometheus + Grafana |
| 混沌演練 | 定期殺死客戶端進程,驗證服務端 Scanner 自動回收 | 演練報告 |
| 版本治理 | 升級 HBase 2.4+ (內建 Scanner TTL)、或回港至 1.4 穩定分支 | 升級驗收 |
HBase Thrift2 Scanner 泄漏是典型的「資源生命周期管理缺失」導致的 GC 風暴。通過「客戶端規範關閉、服務端 TTL 托底、架構層面 API 替代」三層防線,配合監控告警與混沌演練,可將此類故障從「深夜驚魂」轉化為「可控風險」。