真實(shí)線上DB存儲(chǔ)架構(gòu)升級(jí)實(shí)戰(zhàn)!
前言
交易系統(tǒng)1.0的存儲(chǔ)架構(gòu)采用了單庫單表,隨著業(yè)務(wù)快速發(fā)展,訂單量從日單量十萬級(jí)別快速增長到日單量百萬級(jí)別,預(yù)計(jì)在未來兩三個(gè)月就存儲(chǔ)就會(huì)出現(xiàn)瓶頸,DB存儲(chǔ)架構(gòu)升級(jí)迫在眉睫。
交易系統(tǒng)如何做到平滑遷移?
由于面對(duì)業(yè)務(wù)場景是交易系統(tǒng),即使是業(yè)務(wù)低峰期,業(yè)務(wù)也無法接受臨時(shí)停服,基于此背景下,設(shè)計(jì)了如下一套不停服下的數(shù)據(jù)平滑遷移工具。
平滑遷移流程設(shè)計(jì)
由于面對(duì)的業(yè)務(wù)場景是交易系統(tǒng),即使是業(yè)務(wù)低峰期,依舊有交易在不斷產(chǎn)生,所以對(duì)于線上DB停寫從業(yè)務(wù)角度是不能接受的,同時(shí)如何實(shí)現(xiàn)新舊數(shù)據(jù)源的靈活切換、切換前的充分驗(yàn)證及觀測(cè),以及切換后如果出現(xiàn)問題如何快速回滾,基于這些問題設(shè)計(jì)了一套雙寫雙讀流程。
切換前充分驗(yàn)證和可觀測(cè)性: 在整個(gè)切換過程中的關(guān)鍵環(huán)節(jié)都增加監(jiān)控?cái)?shù)據(jù)埋點(diǎn)上報(bào),從而實(shí)現(xiàn)整個(gè)操作過程的可觀測(cè),也保證了在每一個(gè)環(huán)境切換前的充分驗(yàn)證。
動(dòng)態(tài)配置下發(fā): 將切換過程中需要調(diào)整的參數(shù)全部寫入動(dòng)態(tài)配置中心(比如Nacos),并為每個(gè)動(dòng)態(tài)下發(fā)參數(shù)設(shè)計(jì)灰度放量的過程,比如基于用戶尾號(hào)放量、基于租戶維度放量等,保證在切換過程中的影響范圍做到可控。
具體操作流程如下:
a). 對(duì)業(yè)務(wù)代碼進(jìn)行改造上線,使得具備雙寫讀舊數(shù)據(jù)源的能力,通過Grafana監(jiān)控讀寫曲線及異常監(jiān)控等指標(biāo)。
b). 通過離線任務(wù)或者DBA提供的數(shù)據(jù)同步組件,將舊數(shù)據(jù)源的數(shù)據(jù)開始全量同步。
c). 待全量數(shù)據(jù)同步完成后,通過動(dòng)態(tài)配置中心開始逐步放量,將雙寫讀舊數(shù)據(jù)源切換到雙寫讀新數(shù)據(jù)源,同時(shí)通過Grafana監(jiān)控讀寫曲線及異常監(jiān)控等指標(biāo).
d). 灰度一段時(shí)間后,若未發(fā)現(xiàn)異常時(shí),在通過動(dòng)態(tài)配置中心逐步灰度切換到單寫讀新數(shù)據(jù)源,最后下線舊數(shù)據(jù)源。在第3、4步驟切換過程中一旦發(fā)現(xiàn)問題立刻對(duì)動(dòng)態(tài)配置進(jìn)行回滾。
圖片
切流組件實(shí)現(xiàn)
平滑切流組件的執(zhí)行流程圖如下圖所示:
圖片
對(duì)應(yīng)的核心執(zhí)行代碼如下:
public T call() {
try {
// 1. 灰度開關(guān)關(guān)閉,走老邏輯
if (Objects.isNull(shardConfig)) {
return oldCall.call();
}
Callable<T> mainCall = oldCall;
Callable<T> subCall = newCall;
// 2. 業(yè)務(wù)是否使用新數(shù)據(jù)源數(shù)據(jù)返回的結(jié)果
if (shardConfig.isUseNewResult()) {
mainCall = newCall;
subCall = oldCall;
}
// 未命中灰度,則直接返回主邏輯結(jié)果
if (!isGray()) {
return result;
}
// 是否開啟異步數(shù)據(jù)對(duì)比旁路邏輯
if (shardConfig.isAsync()) {
Callable<T> finalSubCall = subCall;
Thread thread = new Thread(() -> {
process(finalSubCall, result);
});
EXECUTOR_SERVICE.submit(thread);
} else {
// 同步鏈路數(shù)據(jù)對(duì)比
process(subCall, result);
}
return result;
} catch (Exception e) {
throw new RuntimeException("call error");
}
}
/**
* 對(duì)新邏輯返回的結(jié)果和舊邏輯返回的數(shù)據(jù)進(jìn)行對(duì)比
* @param subCall 子邏輯結(jié)果
* @param result 主邏輯結(jié)果
*/
private void process(Callable<T> subCall, T result){
// TODO 對(duì)新邏輯返回的結(jié)果和舊邏輯返回的數(shù)據(jù)進(jìn)行對(duì)比
try {
T subReuslt = subCall.call();
boolean isSame = shardComparator.compare(result, subReuslt);
if (!isSame) {
// TODO 上報(bào)對(duì)比異常監(jiān)控
PerfHelper.report("methodName", methodName);
}
} catch (Exception e) {
LOGGER.error("compare exception");
}
}
private boolean isGray() {
// TODO 業(yè)務(wù)灰度規(guī)則
return true;
}
交易系統(tǒng)平滑遷移過程中的數(shù)據(jù)一致性如何保證?
圖片
用戶下單后需要冗余三個(gè)視角維度的數(shù)據(jù),對(duì)于C端用戶視角的數(shù)據(jù)一致性,采用雙寫+binlog補(bǔ)償+定時(shí)任務(wù)兜底;對(duì)于B端商家視角的數(shù)據(jù)一致性,使用雙寫+binlog補(bǔ)償+定時(shí)任務(wù)兜底+數(shù)據(jù)回源四種手段保證數(shù)據(jù)一致性。最后通過數(shù)據(jù)流對(duì)賬+可視化監(jiān)控來實(shí)時(shí)觀測(cè)數(shù)據(jù)不一致的情況,并對(duì)異常數(shù)據(jù)進(jìn)行手動(dòng)修復(fù)。
遇到的一些坑
下單鏈路代碼不合理
從單庫單表切換到分庫分表后,通過監(jiān)控發(fā)現(xiàn)讀新數(shù)據(jù)源數(shù)據(jù)對(duì)比錯(cuò)誤量放大,原因是原來的下單鏈路存在:更新完DB后立馬查詢,由于主從數(shù)據(jù)同步還未完成導(dǎo)致無法查詢到最新數(shù)據(jù)。
解決思路: 調(diào)整業(yè)務(wù)邏輯
數(shù)據(jù)對(duì)比采用線程池進(jìn)行異步對(duì)比導(dǎo)致監(jiān)控異常
起初使用線程池進(jìn)行異步對(duì)比,是為了盡量不影響主鏈路耗時(shí),但由于線程池使用姿勢(shì)不太合理,導(dǎo)致實(shí)際發(fā)生比對(duì)時(shí)新舊數(shù)據(jù)源已經(jīng)不一致,其根本原因是由于異步延遲執(zhí)行導(dǎo)致。
解決思路: 1. 數(shù)據(jù)對(duì)比改為同步執(zhí)行,主鏈路耗時(shí)無明顯劣化,業(yè)務(wù)可接受;2. 動(dòng)態(tài)調(diào)整線程池參數(shù),使得異步任務(wù)執(zhí)行更快處理(該方法嘗試后,發(fā)現(xiàn)錯(cuò)誤量有所下降,但未完全消除。
后續(xù)
本文介紹了一個(gè)線上交易系統(tǒng)從單庫單表架構(gòu)升級(jí)為多庫多表過程中,如何實(shí)現(xiàn)不停服平滑遷移的方案,同時(shí)在整個(gè)遷移過程中實(shí)現(xiàn)了可觀測(cè)、可回滾。
隨著訂單量持續(xù)增長,后續(xù)B端商家側(cè)的數(shù)據(jù)逐漸出現(xiàn)了數(shù)據(jù)傾斜、大賬單等問題,后續(xù)有機(jī)會(huì)再介紹這些問題的解決思路和治理方案。