之前忙活過一個多方合作的專案,異構系統間維護資料一致性有時候是個頭疼但必須解決的問題,今天我從背景、問題、解決思路、技術方案等幾個方面淺談一下,拋磚引玉。
背景
異構系統
近兩年我司承接了某個持續性的會議專案,即每季度或月不定期舉行會議。本專案目前有三個主要供應方(面向使用者的A方,資料中間B方,會議資料同步C方【我司】)。 為了方便演示問題,以下流程和職責都做了裁剪。
簡化流程如下: 簡化職責如下:
A方職責: 使用者通過官網/小程式進行報名,A方呼叫B方的標準介面,不儲存資料 B方職責:作為ISP,提供標準查詢、新增、修改等相關介面,幾乎不提供定製。基於表單和表單資料,完成資料儲存與流轉。 C方職責:提供匯入/更新/稽核/登出等入口,新資料會通知到B方,B方資料新增/更新也會通知到C方。
從圖例來看,B方/C方資料儲存方面是冗餘的。但B方只儲存了核心資料,提供不了太多業務行為,C方具有業務需要的全套流程,但在此專案中作為後方支援及後續現場支援,三方形成了一種生態和諧。本篇部落格主旨在討論多方異構系統之間如何保證資料的一致性。
產品/專案
從標準Sass系統來講,這樣的多方互動,不利於系統穩定性,有諸多不可控因素。但從專案角度,這是各方考慮/鬥爭/談判/費用等綜合因素下友好協商的結果。 當然這是一個私有部署專案,所以會有很多堅持和妥協。
大領導提到一個說法:專案是要交付的,功能完美是產品考慮的。在功能不完善的情況下,如何去交付?
最後的兜底
哎,一言難盡。是通宵了幾次核對/修復資料的,這是最後的辦法了。為了苦逼不再重現,今年要對整個線動一動手術。(說好的.net 不996呢?)(拿著白菜價操著賣白粉的心)。
問題
請求無序
C方 需要所有子會報名前,主會必須報名。 B方 各會之間的報名資料是無序到達的。
迴圈更新
B方 任意報名資料更新或新增都會推送到C方,C方收到更新也會更新B方。這裡有一些措施進行了攔截中斷,但仍會頻繁迴圈更新問題。這是目前現狀(為什麼會出現?太趕工?)
排錯困難
無開發環境,需盲寫程式碼,發到測試環境進行聯調測試。 呼叫鏈太長,日誌過多,排錯時需要根據呼叫各服務介面來判斷走到了哪步,出現了一個問題。呼叫鏈能查到一些問題,但不容批量定位問題。單個查太難。
bug
高併發下,redis元件出現各種問題(timeout等) token問題 資料丟失 更新失效 資料重複 佇列積壓 介面請求時間超長 其他問題...
資料很大,也很小
大部分資料能對上,偶爾幾十個或斷斷續續產生新問題的資料需要及時人工修復。功能有缺陷,人工也是一種交付辦法,但不可持續,太他媽的累了。資料不一致,也是導致通宵核對/修復資料的一大原因。如果資料全一致,就不會那麼辛苦了。
解決思路
管理層
明確專案是要繼續做的 目標產品化/更方便維護方向發展。一團隊養一專案。 有改進想法提出來,拉會推進 缺人,招人(遙遙無期...)
技術層
針對請求無序問題,引入延時佇列,先處理主會、子會延遲幾秒鐘在處理。 針對迴圈更新問題,記錄B方資料來源,非必要情況下,不回更B方。必須終止掉。【冤冤相報何時了】 針對排錯困難問題,引入mysql記錄新增報名的請求以及處理結果,可以更快查詢處理結果。 針對bug,測試根據各測試場景進行復測,按10/100/1000/3000/萬級規模壓測。提前發下問題。 推進客戶方一起做必要去重邏輯。
其他因素
無論是標準產品還是交付專案,做任何改動都要評估。
多溝通,大家都是站在一條線的。有利於事情解決的方案認同度會更高。 預估花多少時間,有多少資源。 能擠出來的空窗期有多久,客戶方/產品方對於需求的急迫性有多強。 基於場景測試,把缺陷優先順序先列出來,根據空窗期先修復緊急缺陷。
把緊急且影響範圍廣的問題解決了,風險就小了很多了。80%的問題是由20%的因素造成的。 這也正符合程式優化中的時間/空間區域性性。
“程式執行時,在一段時間裡,程式的執行往往呈現高度的區域性性, 包括時間區域性性和空間區域性性。
”
時間區域性性是一旦一個指令被執行了, 則在不久的將來,它可能再被執行。
空間區域性性是一旦一個指令一個儲存單元被訪問,那麼它附近的單元也將很快被訪問.
技術方案
mysql實現延遲佇列
優先處理主會,子會延時處理 由於隱私問題,這裡只列部分欄位
資料庫輪詢獲取未處理資料 這裡如何提高消費速度,可以參考《計算機系統結構》中標量處理機的流水線的一些知識。
首先要無相關,即按AccountId分組,分組內的資料是無衝突/相關的,可以分批進行。記錄各任務狀態,最後統一提交資料庫狀態,然後1s後繼續輪詢。這種類似靜態流水線。動態流水線較為複雜,這裡暫不做實現。
do
{
var groupTemps = groupDatas.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
var currentRecords = new List<QidianNotifydelayData>();
foreach (var item in groupTemps)
{
currentRecords.AddRange(item.ToList());
}
var temp = taskFunc(currentRecords);
taskList.Add(temp);
pageIndex++;
}
while ((pageIndex - 1) * pageSize <= groupCount);
//等待全部執行
await Task.WhenAll(taskList.ToArray());
await _dbContext.CommitAsync();
Thread.Sleep(1);
如果1s輪詢覺得太浪費,後續可以根據請求傳送標記位(下次輪詢時間),有資料時,可以快速輪詢,無資料時放寬時間。極端處理方式,當主會請求過來處理完成後,直接發起子會處理,但要考慮資料庫是否能承受的住這種併發壓力。
如果考慮請求會重複執行,可以在執行內加redis鎖。慎用for update,併發一大就over.
/// <summary>
/// 鎖定執行。
/// </summary>
/// <param name="key"></param>
/// <param name="func"></param>
/// <param name="timeSpan"></param>
/// <returns></returns>
public async Task<BizResult<T>> LockExcute<T>(string key, Func<Task<BizResult<T>>> func, int timeSpan)
{
var db = (this._cacheClient as RedisClient).Db;
var mutexKey = string.Format("mutex:", key);
if (await db.StringSetAsync(mutexKey, "1", TimeSpan.FromSeconds(timeSpan), When.NotExists))
{
try
{
var item = await func.Invoke();
return item;
}
catch (Exception ex)
{
_logger.LogError("LockExcute:Exception:" + ex.Message);
return BizResult.BusinessFailed<T>(-1, $"執行失敗,Message:{ex.Message}");
}
finally
{
await db.KeyDeleteAsync(mutexKey);
}
}
else
{
_logger.LogWarning($"LockExcute:Key:{key},正在處理中,請稍候");
return BizResult.BusinessFailed<T>(-1, "正在處理中,請稍候");
}
}
redis實現延遲佇列
由於業務中一個Account同時只能處理一個主會,如果在處理子會的時候,主會請求突然過來了,就會有問題,這裡就需要加鎖主會。引入了Redis延遲佇列 基於Redis ZSet有序集合實現。 思路:當前時間戳和延時時間相加,也就是到期時間,存入Redis中,然後不斷輪詢,找到到期的,拿到再刪除即可。 目前實現缺點:不利於監控,未發起http請求處理業務,導致呼叫鏈有缺。
/// <summary>
/// 3.入佇列
/// </summary>
/// <param name="redisModel"></param>
/// <returns></returns>
public async Task EnqueueZset(DataToModel redisModel)
{
redisModel.UpdateTime = redisModel.UpdateTime.AddSeconds(5);// 最後更新時間 + 5秒
var redisDb = _redisConnectionService.GetRedisConnectionMultiplexer().GetDatabase(0);//預設DB0
if (redisDb != null)
{
IsoDateTimeConverter timeFormat = new IsoDateTimeConverter();
timeFormat.DateTimeFormat = "yyyy-MM-dd HH:mm:ss.fff";
await redisDb.SortedSetAddAsync(ZSet_Queue, JsonConvert.SerializeObject(redisModel, Formatting.Indented, timeFormat), redisModel.UpdateTime.ToTimeStamp());//得分 --放入redis
_logger.LogInformation($"資料排隊--入佇列!redisModel:{JsonConvert.SerializeObject(redisModel)}");
}
}
rabbmit實現延遲佇列
死信佇列過期--》重推信佇列?暫未實現。
資料更新方案
核心原則:先查詢對比,有變更再更新。從B方資料過來的,儘量不再更新回去。減小併發量,控制複雜度。
資料核對方案
待補充。未實現自動化。後期可以獲取雙方系統資料,彙總對比。
部署/壓測/監控
Jmeter(來自於測試同學提供的指令碼)
這裡只做簡單截圖
配置預定義引數 必要情況下配置後置處理程式
配置好thread group,http request後,執行呼叫觀察介面
查詢請求執行是否成功 檢視聚合報告
kubernetes
kubectl get nodes 獲取所有節點 kubectl get pod -A 檢視所有服務,觀察status和age kubectl logs [-f] [-p] POD [-c CONTAINER] 檢視日誌資訊。
“-c, --container="": 容器名
”
-f, --follow[=false]: 指定是否持續輸出日誌
--interactive[=true]: 如果為true,當需要時提示使用者進行輸入。預設為true
--limit-bytes=0: 輸出日誌的最大位元組數。預設無限制
-p, --previous[=false]: 如果為true,輸出pod中曾經執行過,但目前已終止的容器的日誌
--since=0: 僅返回相對時間範圍,如5s、2m或3h,之內的日誌。預設返回所有日誌。只能同時使用since和since-time中的一種
--since-time="": 僅返回指定時間(RFC3339格式)之後的日誌。預設返回所有日誌。只能同時使用since和since-time中的一種
--tail=-1: 要顯示的最新的日誌條數。預設為-1,顯示所有的日誌
--timestamps[=false]: 在日誌中包含時間戳
mysql監控(來自於運維同學的反饋)
這裡只截圖簡單資訊
通過雲監控檢視mysql狀態[最大連線數/cpu/記憶體/慢查詢/索引建議/鎖]
呼叫鏈/日誌
此處暫不截圖。
失控
一期方案 二期方案
三期方案 當然那是進展順利的情況下,不順利的情況下就變成了這樣
某些時候也會聽到如下言論:
一定要保證xx的信譽。 今晚就不要睡覺了吧?大家多堅持一下。
就如現在的疫情封控一樣,做好了精準防控一片讚歌,失控了就好好居家、共渡難關。 網路和現實都會告訴你什麼就是人間。
總結
以上是關於定製化需求的一些解決方案,希望對未來類似產品或專案做個參考。本篇從問題著手,分析有利於解決/消除異構系統資料一致性辦法。當然資料一致性也依賴於自身系統的高可用,這裡未做過多描述,以後再說。
到此結束,謝謝觀看!