最近遷移數據時需要執行大Scan,HBase集群經常碰到以下日志:
Exception in thread "main" org.apache.hadoop.hbase.DoNotRetryIOException: Failed after retry of OutOfOrderScannerNextException: was there a rpc timeout?
出現上述日志后這次Scan就掛了,HBase Client不能自動恢復了。下面分析一下相關代碼。
客戶端Scan示例如下:
Scan scan = new Scan();
scan.setStartRow(...);
scan.setStopRow(...);
scan.setCaching(20);
Result result;
try (ResultScanner rs = table.getScanner(scan)) {
while ((result = rs.next()) != null) {
// deal with result
}
}
-
table.getScanner(scan)做了什么:
初始化一個ScannerCallable對象,調用call(),這個call()會發送一個特殊的ScanRequest rpc請求給
數據所在的RS(定位RS的過程不在本文討論范圍),RS收到請求后發現request中沒有scanner id,認為這是一個全新的Scan請求,RS會分配一個該RS全局唯一的scanner id,這個id會返回給客戶端供這個Scan后續的ScanRequest使用。同時會為這個scan分配一個數據迭代器RegionScannerImpl(具體取數據邏輯看HBase Scan流程分析),將RegionScannerImpl和對應的元數據HRegion包裝到RegionScannerHolder中,放入以下map,如下所示:
protected long addScanner(RegionScanner s, HRegion r) throws LeaseStillHeldException {
long scannerId = this.scannerIdGen.incrementAndGet();
String scannerName = String.valueOf(scannerId);
RegionScannerHolder existing =
scanners.putIfAbsent(scannerName, new RegionScannerHolder(s, r));
assert existing == null : "scannerId must be unique within regionserver's whole lifecycle!";
this.leases.createLease(scannerName, this.scannerLeaseTimeoutPeriod,
new ScannerListener(scannerName));
return scannerId;
}
最后,RS會為這個Scanner創建一個Lease,有效期60s,這個以后再說,用於控制大Scan無限的占用RS資源。可以看出,這里第一次調ScannerCallable的call(),實際上沒有從RS獲取到實際數據,而是做了一些初始化工作,例如獲取到scanner id.
2. next()做了什么
第一次next()從RegionServer取回20條記錄緩存在client本地,后續19次next直接從本地取,不需要訪問RS,第21次繼續向RS取20條,如果向RS取數據時,客戶端超時了,那么client不會自動從scan成功的最后一個rowkey的下一個rowkey開始取數據。而是拋給上層應用解決。
每次向RS獲取數據都調用ScannerCallable的call(),由於上面已經獲取到了scanner id,這里構造ScanRequest都會帶着這個scanner id,並且每次都會返回實際的數據。由於client的一個Scan可能需要多次向RS取數據,為了保證客戶端順序的得到所有數據不漏,Client和RS都維護一個nextCallSeq字段,客戶端每次得到RS的一批數據后,將nextCallSeq加1供后續ScanRequest使用。RS端同樣,每次接受到ScanRequest都將對應的nextCallSeq加1,如果客戶端在每次獲取數據超時了,那么client的nextCallSeq沒有加1,后續RS收到ScanRequest發現nextCallSeq匹配不上,RS會拋出OutOfOrderScannerNextException,客戶端看到這種異常不進行retry,直接拋出next().
碰到這種異常,一個規避的方法就是scan.setCaching()設置小點。另外,一個就是在應用中重試,每次將最后一次Scan得到的最后一個rowkey記下來,一旦出現這種問題,就重新起一個Scan,設置startkey,但是這樣的問題是重試后得到的數據是不是一致的:RS端為了維護一次Scan的數據是一致的,在getScanner()里初始化迭代器RegionScannerImpl時將當前的mvcc read point保存了下來,所以如果重啟一個新的Scan,read point很可能不一樣。
####參考文獻
[HBase-0.98.9](https://github.com/apache/hbase/tree/0.98)