作者:溫昂展
導語:海量服務之道其一,有損服務; TAF特性其一,容錯; Less is more
上一節簡單提到了用戶端在選取Invoker節點時,會對Invoker清單執行死活檢查,屏蔽掉一定時間内異常的節點,進而達到容災的目的。下面對TAF容錯機制做具體探讨。
一、容錯性
從機率學的角度,随着分布式系統規模不斷擴大,即使是小機率的系統錯誤依然不可忽略不計,容錯設計必不可少。
在分布式計算領域有一個公理即:CAP理論,分布式系統必然需要滿足“P” 項,在遇到某個節點或網絡分區故障時,仍然能對外提供滿足一緻性和可用性的服務,而一緻性和可用性須有一方取舍,通常我們會選擇系統高可用。(在這次實習做的項目中也有所體會,為保證系統高可用我容忍了一定判重狀态的不一緻,實際上很多業界優秀的NoSQL方案也是這麼做的)。
說得再直白一點? 一句話: 任何一台服務節點down掉,都不影響業務的通路;
怎麼保護? 簡單點: 先屏蔽掉該異常節點,請求先發送到别的節點去,隔一段時間再試試異常的。
注意這裡所說的容錯性是站在系統層面上的,而業務上的容錯是交給業務方自行根據需要做定制和實作的,如:根據服務端錯誤傳回、捕獲調用異常資訊或是在錯誤回調中做相應重試處理。
二、容錯保護
系統要做到容錯,首先需要思考:系統會有哪些錯誤? 系統如何能發現這些錯誤?
而所謂的容錯保護就是在發現這些錯誤節點後采取特定的容錯政策來保證系統的可用性,最簡單的方式就是将這些錯誤節點移除屏蔽掉,然後定期重試,若發現錯誤節點恢複正常則取消屏蔽。
1. 錯誤類型
根據前面對用戶端向服務端發起請求過程的分析,為保證系統的高可用性,若出現建立連接配接失敗,或是處理請求時出現大量逾時(參考:過載保護),我們應将該節點判定為異常節點。
具體分析連接配接失敗或處理逾時的原因是比較複雜的,可能是網絡線路中斷引起,亦有可能是節點系統異常,或是服務節點當機等等。既然異常情況可能性較多,我們則不去具體細化探讨到各種情況的異同,而是概括性地抽象出系統錯誤出現的表征,以此作為依據發現錯誤卻是比較容易實作的。
2. 如何發現
針對這個問題,必然要從兩個角度出發考慮:
- 在服務端做監控
- 用戶端主動發現
對于節點連接配接失敗,一方面可以讓服務端保持心跳上報,告知目前服務正常運作;另一方面可以使用戶端建立連接配接失敗時傳回錯誤資訊,以此判定;
對于節點過載,一方面可以監控服務端的服務隊列處理情況; 另一方面可以在用戶端統計請求的逾時響應情況,以此判定。
三、TAF實作
分析清楚問題,再考慮如何實作就比較簡單了,TAF的實作同樣是從以上兩個角度做考慮的。回想前面在整體架構介紹中提到的,petsvr服務會定期上報心跳到node服務,由node服務統一将心跳上報registry,以此我們可以在registry端設計名字服務排除政策,移除故障節點;而對于節點過載情況,考慮到在Invoker上直接統計更為精确,直接更新可用節點清單更為及時,同時沒有服務端Obj 複用問題,是以我們可以設計用戶端主動屏蔽政策。
1. 名字服務排除政策:
業務服務 svr 主動上報心跳給名字服務,使名字服務知道服務部署的節點存活情況,當服務的某節點故障時,名字服務不再傳回故障節點的位址給Client,達到排除故障節點的目标。名字服務排除故障需要通過服務心跳和Client位址清單拉取兩個過程,預設故障排除時間在1分鐘。
2. Client主動屏蔽政策:
為了更及時的屏蔽故障節點,Client根據調用被調服務的異常情況來判斷是否有故障來更快進行故障屏蔽。具體政策是,當client調用某個svr出現調用連續逾時,或者調用的逾時比率超過一定百分比,client會對此svr進行屏蔽,讓流量分發到正常的節點上去。對屏蔽的svr節點,每隔一定時間(預設30秒)進行重連,如果正常,則進行正常的流量分發。
代碼實作放在ServantnvokerAliveChecker工具類中,每個服務URL會對應一個死活統計狀态ServantInvokerAliveStat,每次Invoker執行請求結束後會檢查更新該活性,
代碼邏輯很簡單,以下情況則屏蔽該服務節點:
- 周期内逾時次數超過MinTimeoutInvoke,且逾時比率大于總數的frequenceFailRadio
- 連續調用逾時次數超過frequnceFailInvoke(5秒内)
- 連接配接失敗connectionTimeout錯誤
如下:
public synchronized void onCallFinished(int ret, ServantProxyConfig config) {
if (ret == Constants.INVOKE_STATUS_SUCC) {
frequenceFailInvoke = 0;
frequenceFailInvoke_startTime = 0;
lastCallSucess.set(true);
netConnectTimeout = false;
succCount++;
} else if (ret == Constants.INVOKE_STATUS_TIMEOUT) {
if (!lastCallSucess.get()) {
frequenceFailInvoke++;
} else {
lastCallSucess.set(false);
frequenceFailInvoke = 1;
frequenceFailInvoke_startTime = System.currentTimeMillis();
}
netConnectTimeout = false;
timeoutCount++;
} else if (ret == Constants.INVOKE_STATUS_EXEC) {
if (!lastCallSucess.get()) {
frequenceFailInvoke++;
} else {
lastCallSucess.set(false);
frequenceFailInvoke = 1;
frequenceFailInvoke_startTime = System.currentTimeMillis();
}
netConnectTimeout = false;
failedCount++;
} else if (ret == Constants.INVOKE_STATUS_NETCONNECTTIMEOUT) {
netConnectTimeout = true;
}
//周期重置
if ((timeout_startTime + config.getCheckInterval()) < System.currentTimeMillis()) {
timeoutCount = 0;
failedCount = 0;
succCount = 0;
timeout_startTime = System.currentTimeMillis();
}
if (alive) {
// 周期内逾時次數超過MinTimeoutInvoke,且逾時比率大于總數的frequenceFailRadio
long totalCount = timeoutCount + failedCount + succCount;
if (timeoutCount >= config.getMinTimeoutInvoke()) {
double radio = div(timeoutCount, totalCount, 2);
if (radio > config.getFrequenceFailRadio()) {
alive = false;
ClientLogger.getLogger().info(identity + "|alive=false|radio=" + radio + "|" + toString());
}
}
if (alive) {
// 5秒内連續失敗n次
if (frequenceFailInvoke >= config.getFrequenceFailInvoke() && (frequenceFailInvoke_startTime + 5000) > System.currentTimeMillis()) {
alive = false;
ClientLogger.getLogger().info(identity + "|alive=false|frequenceFailInvoke=" + frequenceFailInvoke + "|" + toString());
}
}
if (alive) {
//連接配接失敗
if (netConnectTimeout) {
alive = false;
ClientLogger.getLogger().info(identity + "|alive=false|netConnectTimeout" + "|" + toString());
}
}
} else {
if (ret == Constants.INVOKE_STATUS_SUCC) {
alive = true;
}
}
}
複制
感謝閱讀,有錯誤之處還請不吝賜教。