背景
在hbase集羣故障時,hbase client無法連接region server的時候,因爲重試參數配置問題,程序並不會直接拋出異常,而是會一直重試,導致異常報警沒有觸發。此篇文章講述client的重試機制及參數配置。
代碼解析
RpcRetryingCall.java
中 callWithRetries
函數是Rpc請求重試機制的實現, 可以參考以下源碼(hbase版本爲1.2.1)
/**
* Retries if invocation fails.
* @param callTimeout Timeout for this call
* @param callable The {@link RetryingCallable} to run.
* @return an object of type T
* @throws IOException if a remote or network exception occurs
* @throws RuntimeException other unspecified error
*/
public T callWithRetries(RetryingCallable<T> callable, int callTimeout)
throws IOException, RuntimeException {
List<RetriesExhaustedException.ThrowableWithExtraContext> exceptions =
new ArrayList<RetriesExhaustedException.ThrowableWithExtraContext>();
this.globalStartTime = EnvironmentEdgeManager.currentTime();
context.clear();
for (int tries = 0;; tries++) {
long expectedSleep;
try {
callable.prepare(tries != 0); // if called with false, check table status on ZK
interceptor.intercept(context.prepare(callable, tries));
return callable.call(getRemainingTime(callTimeout));
} catch (PreemptiveFastFailException e) {
throw e;
} catch (Throwable t) {
ExceptionUtil.rethrowIfInterrupt(t);
if (tries > startLogErrorsCnt) {
LOG.info("Call exception, tries=" + tries + ", retries=" + retries + ", started=" +
(EnvironmentEdgeManager.currentTime() - this.globalStartTime) + " ms ago, "
+ "cancelled=" + cancelled.get() + ", msg="
+ callable.getExceptionMessageAdditionalDetail());
}
// translateException throws exception when should not retry: i.e. when request is bad.
interceptor.handleFailure(context, t);
t = translateException(t);
callable.throwable(t, retries != 1);
RetriesExhaustedException.ThrowableWithExtraContext qt =
new RetriesExhaustedException.ThrowableWithExtraContext(t,
EnvironmentEdgeManager.currentTime(), toString());
exceptions.add(qt);
if (tries >= retries - 1) {
throw new RetriesExhaustedException(tries, exceptions);
}
// If the server is dead, we need to wait a little before retrying, to give
// a chance to the regions to be
// tries hasn't been bumped up yet so we use "tries + 1" to get right pause time
expectedSleep = callable.sleep(pause, tries + 1);
// If, after the planned sleep, there won't be enough time left, we stop now.
long duration = singleCallDuration(expectedSleep);
if (duration > callTimeout) {
String msg = "callTimeout=" + callTimeout + ", callDuration=" + duration +
": " + callable.getExceptionMessageAdditionalDetail();
throw (SocketTimeoutException)(new SocketTimeoutException(msg).initCause(t));
}
} finally {
interceptor.updateFailureInfo(context);
}
try {
if (expectedSleep > 0) {
synchronized (cancelled) {
if (cancelled.get()) return null;
cancelled.wait(expectedSleep);
}
}
if (cancelled.get()) return null;
} catch (InterruptedException e) {
throw new InterruptedIOException("Interrupted after " + tries + " tries on " + retries);
}
}
}
HBase客戶端請求在那個時間段網絡有異常導致rpc請求失敗,會進入重試邏輯
根據HBase的重試機制(退避機制),每兩次重試機制之間會休眠一段時間,即cancelled.wait(expectedSleep)
,這個休眠時間太長導致這個線程一直處於TIME_WAITING狀態。
休眠時間由expectedSleep = callable.sleep(pause,tries + 1)
決定,根據hbase算法,默認最大的expectedSleep爲20s,整個重試時間會持續8min,這也就是說全局鎖會被持有8min。
重要參數設置
hbase.client.pause
失敗重試時等待時間,隨着重試次數越多,重試等待時間越長,計算方式如下所示:
public static int RETRY_BACKOFF[] = { 1, 2, 3, 5, 10, 20, 40, 100, 100, 100, 100, 200, 200 }; long normalPause = pause * HConstants.RETRY_BACKOFF[ntries];long jitter = (long)(normalPause * RANDOM.nextFloat() * 0.01f);
所以如果重試10次,hbase.client.pause=50ms,則每次重試等待時間爲{50,100,150,250,500,1000,2000,5000,5000,5000}。
屬性默認值爲100ms,可以設置爲50ms,甚至更小。
hbase.client.retries.number
失敗時重試次數,默認爲31次。可以根據自己應用的需求將該值調整的比較小。比如整個提供應用的超時時間爲3s,則根據上面重試時間計算方法,可以將重試次數調整爲3次。
hbase.rpc.timeout
該參數表示一次RPC請求的超時時間。如果某次RPC時間超過該值,客戶端就會主動關閉socket。
默認該值爲1min,應用爲在線服務時,可以根據應用的超時時間,設置該值.如果應用總共超時爲3s,則該值也應該爲3s或者更小.
hbase.client.operation.timeout
該參數表示HBase客戶端發起一次數據操作直至得到響應之間總的超時時間,數據操作類型包括get、append、increment、delete、put等。該值與hbase.rpc.timeout的區別爲,hbase.rpc.timeout爲一次rpc調用的超時時間。而hbase.client.operation.timeout爲一次操作總的時間(從開始調用到重試n次之後失敗的總時間)。
舉個例子說明,比如一次Put請求,客戶端首先會將請求封裝爲一個caller對象,該對象發送RPC請求到服務器,假如此時因爲服務器端正好發生了嚴重的Full GC,導致這次RPC時間超時引起SocketTimeoutException,對應的就是hbase.rpc.timeout。那假如caller對象發送RPC請求之後剛好發生網絡抖動,進而拋出網絡異常,HBase客戶端就會進行重試,重試多次之後如果總操作時間超時引起SocketTimeoutException,對應的就是hbase.client.operation.timeout。
hbase.client.scanner.timeout.period
該參數是表示HBase客戶端發起一次scan操作的rpc調用至得到響應之間總的超時時間。一次scan操作是指發起一次regionserver rpc調用的操作,hbase會根據scan查詢條件的cacheing、batch設置將scan操作會分成多次rpc操作。比如滿足scan條件的rowkey數量爲10000個,scan查詢的cacheing=200,則查詢所有的結果需要執行的rpc調用次數爲50個。而該值是指50個rpc調用的單個相應時間的最大值。
參數推薦
在網絡出現抖動的異常情況下,默認最差情況下一個線程會存在8min左右的重試時間,從而會導致其他線程都阻塞在regionLockObject這把全局鎖上。爲了構建一個更穩定、低延遲的HBase系統,除過需要對服務器端參數做各種調整外,客戶端參數也需要做相應的調整:
-
hbase.client.pause
:默認爲100,可以減少爲50 -
hbase.client.retries.number
:默認爲31,可以減少爲21
修改後,通過上面算法可以計算出每次連接集羣重試之間的暫停時間將依次爲:
[50,100,150,250,500,1000,2000,5000,5000,5000,5000,10000,10000,…,10000]
客戶端將會在2min內重試20次,然後放棄連接到集羣,進而會再將全局鎖交給其他線程,執行其他請求。