migration-process
Migration Process 架構設計
一間商店的定期購資料包含商品、會員、訂閱等不同類型。搬移時,要把舊 MySQL 的資料轉到新 PostgreSQL;每一類資料搬完後還要驗證,才能接著處理下一類。
整輪搬移涵蓋商品、會員、訂閱等不同資料。它們各有搬移與驗證規則,卻都要經過「搬移一批、保存進度、核對一批」的節奏。把每一類資料切成一個 Stage,才能讓共通流程與各類資料的規則有清楚的界線。
Stage 由對應的 Stage Handler 決定資料怎麼搬、怎麼驗證。Copied 只代表搬完;達到 Validated,才代表這個階段完成。migration_process 以 MigrationId、ShopId、Stage 記錄各階段的進度與狀態。因此,Product 搬完時還不能直接進入 Member。兩個 Stage 之間,必須先確認 Product 已驗證通過。
Transfer Job 知道怎麼搬目前這一批,成功後接續下一批;Validate Job 知道怎麼核對目前這一批,通過後接續下一批。兩者處理的是 Stage 內的工作,還需要有人判斷何時驗證、何時進入下一個 Stage。
以 Product 接到 Member 為例,Product 搬完後仍須驗證。只有狀態達到 Validated,才能安排 Member 搬移。
這個調度責任即使沒有 Manage Job,仍然需要有人承擔。若由 Transfer Job、Validate Job 各自判斷下一階段,再由 RD 拼出恢復流程,階段越多,越容易漏掉驗證、用錯游標,或提前進入下一階段。
Product 必須到 Validated,才能開始 Member。新增 Stage 時,也有明確的地方調整順序。Manage 任務,由保存的進度找出階段與游標,不必自行推算該叫哪個 Job。Manage Job 會區分一般恢復、重新驗證與支援的修復操作。Manage Job 因此負責跨階段調度、續跑與人工處理的入口。這些規則不必散落在每個批次 Job 裡。
Manage Job 在階段交接或恢復時讀進度、決定工作;一個階段內的批次接續,仍由 Transfer Job 與 Validate Job 自己完成。資料怎麼搬、怎麼驗證,則交給對應的 Stage Handler。
| 元件 | 角色 | 負責的事 |
|---|---|---|
Manage Job | 調度 | 讀取整間商店的進度,決定要搬移、驗證,或等待人工處理 |
Transfer Job | 搬移 | 讀取一批舊資料,轉換後寫入新資料庫,並接續下一批 |
Validate Job | 驗證 | 重新讀取一批來源資料,核對新資料庫,並接續下一批 |
Stage Handler | 資料類型規則 | 決定商品、會員、訂閱等資料各自的搬移與驗證方式 |
Repository | 資料存取 | 執行來源查詢、目標寫入與比對,以及進度保存 |
migration_process | 進度紀錄 | 保存每個階段的搬移與驗證進度、狀態及失敗資訊 |
NMQ | 任務派送 | 將 Manage、Transfer、Validate 任務交給 Worker 執行 |
migration_process 保存狀態與進度,供三種 Job 在處理時查詢或更新。Transfer Job 與 Validate Job 是依目前狀態選擇的工作。停下來後,怎麼從正確的位置接續?
Manage Job 以保存的階段、狀態與游標作為恢復依據;遇到驗證失敗時,還要區分重新核對與重寫資料。
假設 Order 搬到一半失敗,RD 以相同的 MigrationId 與 ShopId 重新建立 Manage 任務。Manage Job 會依已保存的狀態處理:
| 階段 | 已保存狀態 | 下一步 |
|---|---|---|
Shop、Product、Member | Validated | 略過 |
Order | Failed | 從保存的搬移游標續跑 |
| 後續階段 | Pending | 等待 Order 完成 |
失敗批次不推進搬移進度。RD 不必自行推算階段、游標或要派送的 Job;恢復工作的依據是 migration_process 保存的進度。
搬移失敗可以從保存位置續跑;驗證失敗可能代表資料有差異,不能直接自動重寫。Manage Job 依狀態與 RD 指定的操作做出不同決定:
| 目前狀態與操作 | Manage Job 的決定 |
|---|---|
Pending 或 Failed | 派送 Transfer Job |
Copied | 派送 Validate Job |
Running 或 Validating | 等待,不另派工作 |
ValidationFailed,一般 Resume | 停住,等待人工判斷 |
ValidationFailed,RetryValidation | 派送 Validate Job,從驗證游標重新核對 |
ValidationFailed,RepairValidation | 派送 Transfer Job 執行該階段支援的修復模式 |
Resume 會停住,待 RD 確認原因後再選擇重驗或支援的修復操作。一次 Migration 怎麼跑?
先指定這一輪要搬的商店與每批筆數。Manage Job 建立 14 個 Stage 的進度後,從第一個尚未驗證完成的階段開始,依序安排搬移與驗證。
MigrationIdShopIdBatchSize500。Manage Job 初始化 14 個 Stage 的進度,然後找出第一個尚未達到 Validated 的階段。即使某個階段已搬完,只要驗證未通過,就不會進入後面的階段。
| 順序 | 資料類型 |
|---|---|
| 1~4 | Shop → Product → Member → Order |
| 5~10 | UpdateHistory → CreateRegularLog → MemberLoginLog → ShopManagementLog → ApiAccessLog → ApiRequestLog |
| 11~14 | NotifyQueueArchive → OrderCreateQueueArchive → NotifyQueue → OrderCreateQueue |
每個 Stage 都先由 Transfer Job 一批批搬移。查到來源尾端後,狀態改為 Copied,再由 Manage Job 安排 Validate Job 一批批核對。
Transfer JobCopied,通知 Manage Job 安排驗證。Failed;失敗批次不推進進度。Validate JobValidated,通知 Manage Job 進入下一個 Stage。ValidationFailed,等待人工判斷。Copied 只代表搬完;Validated 才代表階段完成。即使最後一批只有 22 筆,系統仍會再查一次;真正查到 0 筆,才確認已到資料尾端。怎麼記住搬到哪裡?
同一個 Stage 可能已搬完多批資料,卻只驗證到較早的一批。migration_process 因此分開保存搬移與驗證進度,讓兩種工作都能從各自成功的位置接續。
migration_process 以一輪搬移、一間商店、一個 Stage 的組合識別每列進度:
MigrationIdShopIdStage每套進度都記住「上次成功處理到的來源主鍵」與成功累計。下一批在同一商店的來源資料中,從 PK 大於各自游標的位置繼續讀取。
last_pkprocessed_countPK > last_pkvalidation_last_pkvalidated_countPK > validation_last_pk「搬移成功累計 1,200 筆」和「驗證通過 500 筆」描述的是兩件不同的事。若驗證停在某一批,搬移已完成的位置仍要保留,重驗才能從驗證游標接續,不會把搬移進度蓋掉。
失敗後,資料與進度怎麼接回來?
重新派送工作只是恢復入口。真正需要確認的是:同一批資料重跑會不會重複寫入、進度會不會重複累計,以及驗證失敗是否需要改動目標資料。
目標寫入採用 Upsert:資料不存在就新增,存在則依規則更新。因此同一批再次執行時,可以重新處理已寫入的資料;進度更新另會檢查原游標,避免重複累計。
目前尚未有鎖、租約或心跳判斷任務是否仍存活。若程序突然中止,狀態仍留在 Running 或 Validating,Manage Job 會等待,不另派工作,需要人工確認後恢復。
ValidationFailed 可能是資料真的不同,也可能是驗證過程發生錯誤。先看 migration_process.last_error 和差異 log,確認原因;若來源或轉換程式有問題,應先修正原因,再決定後續操作。
| 操作 | 實際做的事 | 是否修改目標業務資料 |
|---|---|---|
RetryValidation | 再檢查一次,只讀取與比對 | 不會 |
RepairValidation | 依目前來源重寫失敗批次,再核對 | 會,執行 Upsert |
Product Stage 支援 RepairValidation;其他 Stage 驗證失敗時不能直接套用。| 查到的原因 | RD 要確認什麼 | 適合的處理 |
|---|---|---|
SCM timeout、DB timeout、網路錯誤 | 服務是否恢復?有沒有證據顯示目標資料錯誤? | 恢復後執行 RetryValidation |
| 目標缺資料或欄位不符 | 目前來源與轉換結果是否正確?目標是否應該跟著更新? | Product 可選 RepairValidation |
| 資料已由 RD 或其他程序修正 | 修正結果是否符合預期? | 以 RetryValidation 驗收 |
| 來源異常或轉換程式有 bug | 哪一份內容才是正確的? | 先修原因,再決定重驗或重寫 |
| 目標已有新版正常操作產生的更新 | 重寫是否會蓋掉應保留的資料? | 先確認資料交接規則,暫不修復 |
補同步為什麼要開新的一輪?
第一輪完成後,舊系統的資料仍可能改變。建立新的 MigrationId,沿用相同的 ShopId,就能從頭再搬移並驗證一次,補到原本游標之前的修改與新增關聯資料。
上一輪的游標已走到來源尾端。若舊系統後來修改了較早的資料,從原游標繼續讀取就不會再讀到它。新的一輪有自己的進度,會從來源重新讀取,再以 Upsert 寫入目標。
相同 PK 的資料走 Upsert,不會再新增一筆相同主鍵的資料。以下是第一輪完成後,來源再次變動的例子:
| 來源變動 | 第二輪從頭跑的結果 |
|---|---|
訂閱 PK 300 修改收件地址 | 更新目標既有的 PK 300 |
訂閱 PK 500 新增一期明細 | 重讀主檔及全部關聯資料,新增缺少的明細 |
新增訂閱 PK 1001 | 新增目標 PK 1001 |
商品 PK 100 完全沒變 | 不會新增另一筆 PK 100 |
第二輪只讀得到來源仍存在的資料。來源已刪除的資料不會因 Upsert 自動從目標刪掉。
若新版已開始處理 Queue,重跑可能把目標的執行狀態更新成舊來源狀態。沒有重複資料列,不代表工作不會再次執行。
舊資料刪除了,目標為什麼還在?
目前的補搬只會讀取來源仍存在的資料,再對目標執行 Upsert。來源刪掉的資料不會被讀到,因此重新搬移也不會自動清掉目標那一筆。
第一輪完成時,舊 MySQL 與新 PostgreSQL 都有 PK 100、PK 200、PK 300。之後舊系統刪除 PK 200;第二輪只能讀到 PK 100 和 PK 300,目標的 PK 200 仍會留下。
PK 100PK 300PK 100PK 200,目標獨有PK 300目前流程沒有找出「目標有、來源沒有」的資料。若要把刪除納入完整補同步,還需要增加以下比對與處置步驟:
同樣是目標獨有的資料,原因與用途可能不同。找出差異後,還需要按資料類型判斷:
| 資料類型 | 需要判斷的事 |
|---|---|
| 商品、會員、訂閱 | 是否確實被來源刪除?應刪除還是停用? |
訂閱明細、mapping | 主檔還在,但部分關聯資料是否被刪除?必須各自比對。 |
Queue | 是否已執行並移到 Archive?不能只因來源 Queue 沒有就判定刪除。 |
Log、Archive | 是否因保存期限被清除?目標是否仍應保留? |
目前支援的是新增與更新的補搬。完整補同步還缺刪除差異偵測與處置規則;可先產出「目標多出哪些資料」的清單,確認每類資料的意義後,再決定如何處理。
搬移完成,資料就能在新架構運作嗎?
搬移目標是讓舊資料能被新系統正確使用。主鍵存在、欄位原值相同,只能證明資料寫進去了;資料格式、時間區間與狀態仍可能需要轉換。
NotifyQueue:字串相同,Worker 卻讀不懂文件中的待發通知是 StockNotify。這筆資料仍可能被新版 Worker 消費,關鍵不只在於搬移後字串是否相同,還在於 JSON 是否符合新版格式。
| 來源欄位 | 案例值 |
|---|---|
nq_pk | 12001 |
nq_type | StockNotify |
nq_ref_shop | 2 |
nq_send_status | 0,待發 |
nq_sended_count | 0 |
舊版 GameBall 的 stock_estimate_housekeeper.php 直接儲存商品陣列;新版 RegularOrderStockNotifyJob 要求外層有 SchemaVersion、ProductItems、GiftItems 的 Version 2 envelope:
[
{
"Cycle": 1,
"SalePageId": 456,
"Qty": 2
}
]
{
"SchemaVersion": 2,
"ProductItems": [
{
"Cycle": 1,
"SalePageId": 456,
"Qty": 2
}
],
"GiftItems": []
}
目前程式把舊 JSON 原樣搬過去,再以來源原字串比對目標。這讓 Validate 可以通過,但新版 Worker 讀到舊陣列時仍會解析失敗;文件指出此問題目前尚未修正。
notify_queue_bak 是封存資料,不由待發通知 Worker 消費;相容性問題針對仍可能執行的 notify_queue。Product:銷售期間與狀態要一起轉換Product Handler 以 regular_product.rpd_page_id 向 SCM 查商品銷售期間,並以台灣時間(UTC+8)的 Now 比較。查到有效的 SCM 商品時,銷售期間使用 SCM 最新值;來源狀態為啟用時,還要依當下是否位於銷售區間內決定搬移後狀態。
| SCM 商品銷售時間 | 新架構欄位 |
|---|---|
SellingStartDateTime | rpd_sale_date_begin、rpd_plan_date_begin |
SellingEndDateTime | rpd_sale_date_end、rpd_plan_date_end |
舊架構沒有 rpd_plan_date_begin、rpd_plan_date_end,Migration 必須用 SCM 銷售區間補上。V2 用方案生效時間判斷方案是否適用;若 plan 區間沒有對齊銷售區間,即使原有銷售欄位正確,新架構仍可能在錯誤時間選到或排除方案。
程式測試使用固定的 Now。下表保留測試涵蓋的區間邊界、原狀態及 SCM 查無商品的結果:
| 商品原狀態 | SCM 銷售期間相對 Now | 搬移後狀態 |
|---|---|---|
1,啟用 | Now 前 1 小時至 Now 後 1 小時 | 1,仍在銷售 |
1,啟用 | Now 至 Now 後 1 小時 | 1,開始時間剛好等於現在 |
1,啟用 | Now 前 1 小時至 Now | 1,結束時間剛好等於現在 |
1,啟用 | Now 後 1 小時至 Now 後 2 小時 | 9,尚未開賣,停用 |
1,啟用 | Now 前 2 小時至 Now 前 1 小時 | 9,已停售,停用 |
9 或 8 | 期間在銷售中 | 保留原狀態 |
1,但 SCM 查無商品 | 沒有目前銷售期間 | 保留來源狀態與期間;HasCurrentSales = false |
Product 驗證會重新查 SCM 最新期間,沒有使用跨系統快照。若 SCM 在搬移與驗證之間改變銷售期間,兩次可能得到不同結果;目前沒有固定整輪搬移的時間基準。


