English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

guest
1 / ?
back to lessons

形式 1:狀態修復。形式 2:浪費資源的報告。

一顆定時的心臟依照時鐘跳動,而不是依照需求或變化,而是依照計時器。

兩種形式,一個根本原因:以排程工作取代正確的設計。

形式 1:狀態修復

狀態轉換無法以原子方式完成。沒有修復轉換本身,而是讓背景工作在延遲後執行並進行調和。使用者會在調和期間看到損壞的狀態。

GitHub 範例(2026-04-08): 某個 pull request 的上游 repository 變成私有。GitHub 嘗試執行狀態轉換:關閉 PR、更新分支狀態、清除 merge 狀態。但該轉換並未以原子方式完成。PR 狀態同時顯示「branch-forced-closed」與「Merge status cannot be loaded」。數分鐘後,一個 Sidekiq 背景工作才完成修復。觀察者在這段期間看到不一致的狀態。

The Metered Heart:Sidekiq 工作是依照排程執行。它並非因為 GitHub 偵測到狀態損壞才啟動,而是因為計時器到期。使用者即時觀看 PR 時,會看到自相矛盾的 PR,直到下一次工作執行才恢復正常。

Form 2: Wasteful Report

一份報告或聚合(aggregation)在固定時間間隔從頭重新計算。沒有快取檢查、沒有冪等性保護、沒有增量更新。每次執行都是完整掃描。

範例:一個每夜的 cron 工作,會從頭掃描所有訂單來重新計算每位使用者的總購買金額;一個每日分析工作,從原始事件日誌重新產生儀表板;一個每週摘要郵件,會查詢 activity 資料表中的每一列。

無論自上次執行以來資料是否有變更,這些工作都會啟動。即使只有最近 24 小時有新資料,它們仍會掃描完整歷史。它們用排程重複取代了增量設計。

The Shared Root

Metered Heart 無法正確反映自身狀態。它只知道時鐘。Form 1:狀態修復工作在 T+5 分鐘執行,無論 T+0 時狀態是否已損壞。Form 2:報告工作在凌晨 2 點執行,無論昨日以來資料是否有變更。

時鐘本身不帶有「需要執行什麼」的資訊。事件才會攜帶這些資訊:例如「某狀態轉換剛剛失敗」、「新訂單剛剛抵達」。Metered Heart 會把這些資訊丟棄,並以排程取而代之。

資本耗損

Metered Heart 會耗損活資本:工程師必須隨時待命處理破損狀態事件。它也會侵蝕社會信任:使用者看到不一致的資料,並回報那些會自行恢復的缺陷。它會放大其他 MOAD:一個掃描所有紀錄以修復破損狀態的修復工作,通常包含 MOAD-0001(O(N²) 掃描)。一個重新計算冷資料的報表工作可能觸發 MOAD-0005(快取雪崩)。MOAD-0009 會加劇其他缺陷。

共享根源

Form 1 與 Form 2 在表面上看似不同:一個修復狀態,一個重新計算資料。但它們的根本原因彼此相連。

Form 1 與 Form 2 共享同一個根本原因。請用一句話描述它,然後從你使用過的軟體中各舉一個例子。

因改變而觸發,而非因時鐘

事件驅動設計會在狀態改變時觸發。狀態改變即為事件,而事件就是觸發器。

形式 1:原子性轉換取代修復工作。

如果某個狀態轉換會讓系統處於損壞的中間狀態,則缺陷存在於轉換本身,而非因為缺少修復工作。請修正轉換,使其能以原子方式(或交易方式)完成。當轉換以原子方式完成時,損壞的狀態就不會存在,修復工作也就無需執行。

# 缺陷:非原子性轉換會留下損壞的狀態
def close_pr_on_repo_private(pr_id):
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed'   # 步驟 1:部分狀態
pr.save()                             # 立即對使用者可見
# ... 其他步驟可能失敗 ...
pr.merge_status = 'not_applicable'
pr.save()                             # 步驟 2:現在已一致
# Sidekiq job 會在步驟 2 失敗時進行調解
# FIX: 原子轉換;不顯示中間狀態
def close_pr_on_repo_private(pr_id):
with db.transaction():
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed'
pr.merge_status = 'not_applicable'
pr.save()   # 兩個欄位會以原子方式提交;永遠不會寫入一半

形式 2:增量更新取代完整重新計算。

一份從頭重新計算的報表之所以觸發,是因為「舊資料 + 新資料 = 新結果」。但「舊結果 + 增量 = 相同的新結果」,這是透過增量方式計算而得。事件:新資料到達。觸發條件:僅針對新資料更新彙總值。

# 缺陷:依排程進行完整重新計算
def nightly_totals_job():
for user in all_users():
total = sum(o.amount for o in user.orders)  # 掃描所有時間
user.total_purchases = total
user.save()

# 修正:事件驅動增量更新
def on_order_placed(order):
order.user.total_purchases += order.amount   # 僅增量
order.user.save()

增量更新在訂單到達時觸發,而非在凌晨 2 點執行。它只更新受影響的使用者,只讀取新訂單,而非讀取所有歷史訂單。夜間任務因此消失。

為什麼 Form 1 揭示了不完整的狀態轉換

Form 1 的 Metered Heart 顯示某個狀態轉換未完成。修復任務之所以存在,是因為工程師發現狀態損壞後,選擇加入調解機制而非修復轉換本身。修復任務只是對錯誤架構決策的補丁。

MOAD-0009 作為放大器

MOAD-0009 會放大其他 MOAD。一個掃描所有記錄以找出損壞狀態的修復任務:會造成 MOAD-0001(每次執行時進行 O(N) 或 O(N²) 掃描)。一個從頭重新計算所有資料的報表任務:會造成 MOAD-0005(當任務啟動並命中溫熱的上游時引發快取風暴)。MOAD-0009 不僅本身造成傷害,還會定期引發其他 MOAD。

診斷與重新設計

團隊在凌晨 2 點執行夜間 cron 任務。該任務會掃描所有使用者的所有訂單,並從頭重新計算每位使用者的總購買金額。任務需執行 4 小時,到早上 6 點儀表板才顯示最新總額。在凌晨 2 點至 6 點之間,儀表板顯示的是前一天的總額。

這是 MOAD-0009 的哪一種形式?應該由什麼事件觸發重新計算?哪種中間資料結構能讓更新變成增量式?