chundev
日期: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
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 在事件時間。
兩個月前另一座叢集遇到同樣的 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 是分散式系統常見的樂觀鎖(Optimistic Locking)機制。概念很簡單:
cas_value)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(雖然當時沒有仔細確認)。
在 delete-entity 完全碰壁後,我決定換個方向:不找外部文件,直接看 Nutanix 自己怎麼做。
python /usr/local/nutanix/bin/idf_cli.py --advanced --help
輸出包含幾個 subcommand,其中一個是 detach-entity。之前一直沒注意它,因為名字聽起來像是「跨叢集搬遷」用的(把 entity 從一個叢集移到另一個),不像是「刪除」。
CVM 上有個 Nutanix 自帶的腳本 /usr/local/nutanix/bin/cleanup_image_metadata,是用來清理 image entity 的。翻閱它的 Python source code,找到了黃金一行:
cas_value = entity["cas_value"] + 1
這告訴我兩件事:
current + 1 的公式 — 不是隨便猜一個值,是明確的 當前值 + 1detach-entity 操作(不是 delete-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。
| 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"
delete-entity |
detach-entity |
|
|---|---|---|
| 原始用途 | 刪除 entity | 標記 entity 已遷移到另一叢集 |
| 實際效果 | 從 IDF 移除 | 從 IDF 移除(source cluster 上等同刪除) |
| 額外參數 | 不需要 cluster UUID | 需要 cluster UUID |
| CAS=0 entity | ❌ 拒絕操作 | ✅ 接受 current+1 |
| CAS 檢查邏輯 | 嚴格,對未初始化 entity 有 bug 或限制 | 較寬鬆,接受標準 CAS 遞增 |
| 公開文件 | 有(但不多) | 幾乎沒有 |
每一條都要符合才能確認是 stale:
cas_value: 0 — 從未被正常更新過created_timestamp == modified_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,正好是事件時間)
| 第一次(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 版本可能需要不同的修復手法。不要假設上次的指令這次一定能用。
雖然 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% 的都該懷疑
detach-entity 是 Nutanix 內部清理 IDF entity 的「真正工具」 — delete-entity 是正常刪除路徑,但遇到 cas_value=0 的異常 entity 時會失靈。detach-entity 搭配 cas_value + 1 才是通用解法。
CAS 值 0 代表「未完成初始化」 — 分散式系統中 CAS=0 的 entity 處於特殊狀態(建立但從未被正常更新),某些操作會拒絕處理它。這不是 Nutanix 獨有的問題 — etcd、Consul 等系統也有類似的「初始版本」語義。
翻閱平台內部腳本是破解未公開 API 的有效手段 — 當官方文件和社群論壇都找不到解法時,直接看 vendor 的內部工具 source code(cleanup_image_metadata)找到了關鍵的 cas_value + 1 公式和 detach-entity 用法。
同一個 KB 在不同 AOS 版本可能需要不同修復手法 — 不要假設上次的指令這次一定能用。先試,失敗了就要重新研究。
殭屍 process 是隱形炸彈 — 一個被遺忘的 mincli process 跑了 12 天就能打爆整座叢集。互動式 CLI 指令永遠要 pipe input,不要依賴人工確認。
停 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
# 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 會自動起來
detach-entity 不是 delete-entity(delete 對 cas_value=0 的 entity 會失敗)--cas-value 永遠傳 entity 當前 cas_value + 1