聊一聊異構系統間資料一致性

從此啟程發表於2022-04-10

之前忙活過一個多方合作的專案,異構系統間維護資料一致性有時候是個頭疼但必須解決的問題,今天我從背景、問題、解決思路、技術方案等幾個方面淺談一下,拋磚引玉。

背景

異構系統

近兩年我司承接了某個持續性的會議專案,即每季度或月不定期舉行會議。本專案目前有三個主要供應方(面向使用者的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的信譽。
  • 今晚就不要睡覺了吧?大家多堅持一下。

就如現在的疫情封控一樣,做好了精準防控一片讚歌,失控了就好好居家、共渡難關。 網路和現實都會告訴你什麼就是人間。

總結

以上是關於定製化需求的一些解決方案,希望對未來類似產品或專案做個參考。本篇從問題著手,分析有利於解決/消除異構系統資料一致性辦法。當然資料一致性也依賴於自身系統的高可用,這裡未做過多描述,以後再說。

到此結束,謝謝觀看!

相關文章