Share Notes

chundev

View the Project on GitHub latteouka/share-notes

Nutanix IDF Entity 的 CAS 鎖死陷阱 — 當 delete-entity 全部失敗,如何從平台內部腳本挖出 detach-entity 解法

日期:2026-06-04 環境:Nutanix AOS 6.10.1.5 / AHV / 7 節點叢集 對應 KB:KB-18070 — Acropolis service may be in restart loop with the CRITICAL message by memory_model_manager.py


TL;DR

Acropolis 因 IDF 中出現多餘的 memory_model entity 而進入 crash loop(KB-18070)。之前在另一座叢集用 idf_cli.py delete-entity 順利解過,但這次所有 delete-entity 指令全部被 CAS 機制擋回 — 因為 stale entity 的 cas_value=0(從未被更新過的初始狀態),AOS 6.10 的 IDF 拒絕對這類 entity 執行 delete。最終翻閱 CVM 上 Nutanix 自帶的 Python 腳本 cleanup_image_metadata,發現 detach-entity 這個未公開的替代指令,搭配 --cas-value = current + 1 的公式成功移除 stale entity,叢集在 5 分鐘內恢復。


背景

Nutanix 叢集的 IDF(Insights Data Fabric) 是建構在 Cassandra 之上的 entity 資料庫,儲存所有叢集元件的設定與狀態。Acropolis 是負責 VM 生命週期管理的核心服務,採用 Leader/Worker 模型 — 叢集選舉一個 Leader 負責排程和狀態管理。

Acropolis Leader 啟動時會呼叫 memory_model_manager.initialize(),從 IDF 查詢 memory_model 這個 entity type。正常情況下只會有 1 筆(代表叢集唯一的記憶體配置模型)。但如果 Acropolis 在 crash loop 期間反覆嘗試取得 leadership,每次 crash 都可能往 IDF 寫入一筆新的 memory_model,導致數量 > 1 → assertion X == 1 失敗 → 再 crash → 惡性循環。

這就是 KB-18070 的核心機制。


問題 / 錯誤訊息

Acropolis FATAL log 持續出現:

CRITICAL memory_model_manager.py:83 3 == 1 failed, Stack:
  File ".../util/misc/decorators.py", line 50, in wrapper
  File ".../util/master/master.py", line 125, in _wait_for_leadership
  File ".../acropolis/master/master.py", line 110, in _acquired_leadership
  File ".../acropolis/host/memory_model_manager.py", line 83, in initialize

idf_cli.py 查詢確認有 3 筆 memory_model entity(預期只有 1 筆):

python /usr/local/nutanix/bin/idf_cli.py --advanced get-entities --guid memory_model
entity {
  entity_guid {
    entity_type_name: "memory_model"
    entity_id: "265b0c2d-..."     ← 原始(正常)
  }
  created_timestamp_usecs: 1745926353692602   ← 2025-04(建立超過一年)
  modified_timestamp_usecs: 1745943228717065  ← 跟 created 不同(有被更新過)
  cas_value: 1                                ← 正常的 CAS 版本
}
entity {
  entity_guid {
    entity_type_name: "memory_model"
    entity_id: "d670bfd5-..."     ← stale(要刪)
  }
  created_timestamp_usecs: 1779541431213891   ← 2026-05-23 21:23 CST(事件時間)
  modified_timestamp_usecs: 1779541431213891  ← 跟 created 完全一樣(從未被更新)
  cas_value: 0                                ← ⚠️ 初始狀態,從未被更新
}

第三筆也是同樣的 cas_value: 0,created 在事件時間。


前情提要:上一次(2026-03)的修復很順利

兩個月前另一座叢集遇到同樣的 KB-18070(NTP 時間漂移引發),IDF 裡多了 3 筆 stale memory_model entity。當時用 delete-entity 就解決了:

python /usr/local/nutanix/bin/idf_cli.py \
  --advanced delete-entity memory_model <stale-entity-id> --cas-value 1

一條指令搞定,3 筆 stale entity 全部刪掉,5 分鐘恢復。所以這次信心十足地照做——

結果全部被擋回來。


除錯過程:CAS 鎖死的 7 次碰壁

什麼是 CAS(Compare-And-Swap)

CAS 是分散式系統常見的樂觀鎖(Optimistic Locking)機制。概念很簡單:

  1. 讀取 entity 時記下當前版本號(cas_value
  2. 寫入時帶著你讀到的版本號
  3. 如果版本號吻合 = 沒人在你之後改過 → 寫入成功,版本號 +1
  4. 如果版本號不吻合 = 有人改過了 → 寫入失敗,回傳 kIncorrectCasValue

這避免了悲觀鎖(先鎖再寫)的效能問題,在高並發場景很常見。Nutanix IDF 就是用這個機制保護 entity 寫入的一致性。

問題:cas_value=0 是特殊狀態

正常 entity 的生命週期是:建立(cas=0)→ 第一次更新(cas=1)→ 後續更新(cas=2, 3, …)

那些 stale entity 在建立後 從未被正常更新過(因為 Acropolis 建立它後立刻 crash 了),所以停留在 cas_value=0 的初始狀態。而 AOS 6.10 的 delete-entity 實作似乎對這種「未完成初始化」的 entity 有不同的處理邏輯 — 無論你傳什麼 cas-value,它都不認

完整試錯紀錄

# 嘗試 結果 分析
1 delete-entity ... --cas-value 0 kIncorrectCasValue 直覺:傳當前值 0。不行
2 delete-entity ... --cas-value 1 kIncorrectCasValue 上次叢集成功的做法。不行
3 delete-entity ... --cas-value 2 kIncorrectCasValue 試 current+2。不行
4 delete-entity ... --cas-value 100 kIncorrectCasValue 試大數。不行
5 delete-entity ... --cas-value -1 ValueError: Value out of range protobuf unsigned integer 不允許負值
6 delete-entity(不帶 cas-value) kNonCasUpdateForCasEntity CAS entity 必須帶版本號
7 update-entity ... --cas-value 0 kIncorrectCasValue 想先 update 再 delete。不行

7 次全敗。delete-entity 這條路完全走不通。

當時的心情大概是:「上次明明可以的,為什麼這次不行?」差別在於 AOS 版本不同(上次 6.5.6.6,這次 6.10.1.5),以及上次的 stale entity 可能 cas_value 不是 0(雖然當時沒有仔細確認)。


轉折:翻閱 CVM 上的內部 Python 腳本

delete-entity 完全碰壁後,我決定換個方向:不找外部文件,直接看 Nutanix 自己怎麼做

線索一:idf_cli.py 的 help

python /usr/local/nutanix/bin/idf_cli.py --advanced --help

輸出包含幾個 subcommand,其中一個是 detach-entity。之前一直沒注意它,因為名字聽起來像是「跨叢集搬遷」用的(把 entity 從一個叢集移到另一個),不像是「刪除」。

線索二:cleanup_image_metadata 腳本

CVM 上有個 Nutanix 自帶的腳本 /usr/local/nutanix/bin/cleanup_image_metadata,是用來清理 image entity 的。翻閱它的 Python source code,找到了黃金一行

cas_value = entity["cas_value"] + 1

這告訴我兩件事:

  1. Nutanix 內部也在用 current + 1 的公式 — 不是隨便猜一個值,是明確的 當前值 + 1
  2. 這個腳本用的就是 detach-entity 操作(不是 delete-entity

線索三:detach-entity 的語義

detach-entity 的原始設計是「把 entity 標記為已遷移到另一個叢集」。但在 source cluster 上,效果等同於移除 — entity 不再屬於這個叢集。

更關鍵的是:detach-entity 的 CAS 檢查邏輯跟 delete-entity 不一樣。它接受 current + 1 作為有效的 CAS 值,即使 entity 的 cas_value 是 0。

驗證假設

# 取得 Cluster UUID(detach-entity 需要)
CLUSTER_UUID=$(ncli cluster info | grep "Cluster Uuid" | awk -F': ' '{print $2}' | head -1)

# 先停 acropolis 避免 race condition
genesis stop acropolis

# 對 cas_value=0 的 stale entity 執行 detach,帶 --cas-value 1(= 0 + 1)
python /usr/local/nutanix/bin/idf_cli.py --advanced detach-entity \
    "$CLUSTER_UUID" memory_model d670bfd5-dc2e-4450-ad5f-d09f024520e7 --cas-value 1

輸出:

detach-entity executed without error in 0.003909 seconds.

成功了。 用同樣的方法處理第二筆 stale entity,然後驗證只剩 1 筆,重啟 acropolis — 叢集在 90 秒內恢復正常,所有 VM 自動 power on。


解法 / 核心知識

CAS Value 規則總結

entity 當前 cas_value detach-entity --cas-value 要傳 delete-entity --cas-value 能用嗎
0(從未更新) 1 ❌ 全部被擋
1 2 ✅(至少在 AOS 6.5 可以)
N N+1 需視 AOS 版本

通用公式:--cas-value = entity.cas_value + 1

完整修復步驟

# 1. 確認是 KB-18070
grep "memory_model_manager.py:83" /home/nutanix/data/logs/acropolis.out

# 2. 查詢 memory_model entity 數量(預期 = 1)
python /usr/local/nutanix/bin/idf_cli.py --advanced get-entities --guid memory_model

# 3. 辨識 stale entity(cas_value=0 + created 在事件時間)
# 用 timestamp 換算確認:
python3 -c "import datetime; print(datetime.datetime.utcfromtimestamp(<usecs>/1e6))"

# 4. 停 acropolis(避免 race condition)
genesis stop acropolis

# 5. 取 cluster UUID
CLUSTER_UUID=$(ncli cluster info | grep "Cluster Uuid" | awk -F': ' '{print $2}' | head -1)

# 6. 逐筆 detach stale entity
python /usr/local/nutanix/bin/idf_cli.py --advanced detach-entity \
    "$CLUSTER_UUID" memory_model <stale-entity-id> --cas-value 1

# 7. 驗證只剩 1 筆
python /usr/local/nutanix/bin/idf_cli.py --advanced get-entities \
    --guid memory_model | grep -c "entity_id:"

# 8. 重啟 acropolis
genesis restart acropolis

# 9. 等 90 秒後驗證 host 狀態
ncli host ls | grep -E "Connected|Schedulable"

detach-entity vs delete-entity 差異

  delete-entity detach-entity
原始用途 刪除 entity 標記 entity 已遷移到另一叢集
實際效果 從 IDF 移除 從 IDF 移除(source cluster 上等同刪除)
額外參數 不需要 cluster UUID 需要 cluster UUID
CAS=0 entity ❌ 拒絕操作 ✅ 接受 current+1
CAS 檢查邏輯 嚴格,對未初始化 entity 有 bug 或限制 較寬鬆,接受標準 CAS 遞增
公開文件 有(但不多) 幾乎沒有

辨識 stale entity 的三個條件

每一條都要符合才能確認是 stale:

  1. cas_value: 0 — 從未被正常更新過
  2. created_timestamp == modified_timestamp — 建立後就沒動過
  3. created 時間落在 cluster FATAL cascade 事件期間 — 用 Python 轉換 usec timestamp 確認
# 驗證 timestamp
python3 -c "
import datetime
ts = 1779541431213891  # created_timestamp_usecs
print(datetime.datetime.utcfromtimestamp(ts / 1e6))
"
# → 2026-05-23 13:23:51 UTC(= 21:23 CST,正好是事件時間)

為什麼兩次遇到同一個 KB,解法卻不同?

  第一次(2026-03) 第二次(2026-05)
AOS 版本 6.5.6.6 6.10.1.5
stale entity cas_value 未確認(可能 > 0) 確認為 0
delete-entity --cas-value 1 ✅ 成功 ❌ 失敗
修復方式 delete-entity detach-entity
修復耗時 5 分鐘 ~1 小時(含 debug)

推測原因:

教訓:同一個 KB 在不同 AOS 版本可能需要不同的修復手法。不要假設上次的指令這次一定能用。


這次 crash 的根因:殭屍 mincli process

雖然 KB-18070 是 Acropolis 不斷 crash 的直接原因,但觸發 cascade 的源頭是一個被遺忘的殭屍 process。

殭屍怎麼來的

在叢集上透過 SSH 執行互動式 acli vm.disk_delete 指令時,會跳出 yes/no 確認提示。如果 SSH session 在提示等待中意外斷線,acli 背後的 mincli process 會:

這隻殭屍跑了 12 天(從磁碟操作到 cluster cascade),持續對 Acropolis 和 IDF 造成壓力,最終在週六晚的高負載時段觸發了全叢集 cascade。

預防

# ✅ 正確做法:永遠 pipe yes 進去
echo yes | acli vm.disk_delete <vm> disk_addr=scsi.0

# ❌ 致命錯誤:互動式執行(斷線就會留殭屍)
ssh nutanix@cvm "acli vm.disk_delete <vm> disk_addr=scsi.0"

定期掃描殭屍:

# 在每台 CVM 上檢查
ps -eo pid,user,etime,pcpu,cmd | \
  grep -E "mincli|acli|disk_delete" | grep -v grep
# 任何 etime > 30 秒且 CPU > 50% 的都該懷疑

學到的事

  1. detach-entity 是 Nutanix 內部清理 IDF entity 的「真正工具」delete-entity 是正常刪除路徑,但遇到 cas_value=0 的異常 entity 時會失靈。detach-entity 搭配 cas_value + 1 才是通用解法。

  2. CAS 值 0 代表「未完成初始化」 — 分散式系統中 CAS=0 的 entity 處於特殊狀態(建立但從未被正常更新),某些操作會拒絕處理它。這不是 Nutanix 獨有的問題 — etcd、Consul 等系統也有類似的「初始版本」語義。

  3. 翻閱平台內部腳本是破解未公開 API 的有效手段 — 當官方文件和社群論壇都找不到解法時,直接看 vendor 的內部工具 source code(cleanup_image_metadata)找到了關鍵的 cas_value + 1 公式和 detach-entity 用法。

  4. 同一個 KB 在不同 AOS 版本可能需要不同修復手法 — 不要假設上次的指令這次一定能用。先試,失敗了就要重新研究。

  5. 殭屍 process 是隱形炸彈 — 一個被遺忘的 mincli process 跑了 12 天就能打爆整座叢集。互動式 CLI 指令永遠要 pipe input,不要依賴人工確認。

  6. 停 acropolis 再操作 IDF 是鐵律genesis stop acropolis 不是「建議」而是「必須」。如果 acropolis 還在 crash loop 中跑,你的 detach 操作可能跟它的寫入產生 race condition,導致 entity 數量又變回 > 1。


參考資料


附錄:給同事的快速參考卡

以下是精簡版,遇到 KB-18070 時可以直接照做。

症狀判定

acropolis.out 看到:CRITICAL memory_model_manager.py:83 X == 1 failed

修復(5 步)

# 1. 停 acropolis
genesis stop acropolis

# 2. 查 entity 數量(應該 > 1)
python /usr/local/nutanix/bin/idf_cli.py --advanced get-entities --guid memory_model
# 記下每筆的 entity_id 和 cas_value
# cas_value=0 + created 在事件時間 = stale,要刪

# 3. 取 cluster UUID
CLUSTER_UUID=$(ncli cluster info | grep "Cluster Uuid" | awk -F': ' '{print $2}' | head -1)

# 4. 刪除 stale entity(用 detach-entity,不是 delete-entity!)
python /usr/local/nutanix/bin/idf_cli.py --advanced detach-entity \
    "$CLUSTER_UUID" memory_model <STALE_ID> --cas-value 1
# cas_value 固定傳 1(因為 stale entity 的 cas_value 是 0,公式 = 0+1)
# 有多筆就每筆都跑一次

# 5. 驗證 + 重啟
python /usr/local/nutanix/bin/idf_cli.py --advanced get-entities \
    --guid memory_model | grep -c "entity_id:"
# 確認 = 1 後:
genesis restart acropolis
# 等 90 秒,VM 會自動起來

注意事項