ADR-011: Cache 記憶體預算改以 offheap 承擔 byte 封頂(supersede ADR-006 的 byte-heap)
ADR 編號: 011 狀態: 已接受 (Accepted) 決策日期: 2026-06-28 決策者: Development Team(框架維護者) 取代: ADR-006 的「byte-sized heap」設計(其餘兩層管制骨架沿用)
摘要
把 cache 記憶體預算管制的 byte 封頂從 on-heap(ADR-006 的 heapMemory(MB) + Ehcache sizeof 引擎)移到 offheap。heap tier 一律改回筆數計(heap(entries),不觸發 sizeof);byte 預算只治理 offheap(序列化大小精確、不經 sizeof)。ADR-006 的「manager 層加總檢查 + per-cache runtime 驅逐 + safe-by-default + offheap 四段 fallback」骨架完整保留——本案只抽掉「on-heap byte 計量」這一塊。
背景 (Context)
問題陳述
ADR-006 為了「heap 終於可以 byte 封頂、與 -Xmx 對齊」,引入 byte-sized heap(CacheBuilder.heapMemory(MB)),並在 governance 下強制 heap tier 用 byte 計、REJECT 筆數 heap。但純 in-process 的 on-heap byte 封頂,唯一手段是 Ehcache 的 sizeof 引擎走訪物件圖估算 retained size。該引擎在平台目標 JDK 25 上已不可靠:
- 最準的 Instrumentation 路預設失效:sizeof 的
AgentSizeOf靠動態 self-attach 載入內嵌 agent 取得Instrumentation.getObjectSize(),但 JDK 9 起jdk.attach.allowAttachSelf預設false、JDK 21+(JEP 451)對動態載入 agent 發警告 → self-attach 預設走不通。 - 退化到 deprecated 的 Unsafe:fallback
UnsafeSizeOf用sun.misc.Unsafe,在 JDK 25 已 terminally deprecated(JEP 471/498)、排程移除;可跑但近似、且壽命有限。 - 最後的反射路在強封裝下不可靠:
ReflectionSizeOf需對 JDK 內部類別setAccessible,JDK 16+ 強封裝會丟InaccessibleObjectException。
ADR-006 自己也已記載 sizeof「非逐位元組精確,須留安全邊際」。換言之,當初換來的「heap byte cap」本就是近似又脆的保證,而現在還疊上 JDK 的不可靠性。
關鍵觀察
sizeof 的麻煩只存在於 on-heap byte 計量這一個組合。Ehcache 的 offheap 與 disk tier 不用 sizeof——它們存序列化後的 bytes,大小本來就精確已知。框架自身的管制碼(enforceBudget / liveOffheapTotalMB)也從不呼叫 sizeof,只是把各 cache「配置的池大小」當數字加總。因此只要把 byte 封頂的責任放在 offheap,sizeof 就整個退場。
假設前提
- 既有 byte-heap 呼叫端極少且全在框架樹內(almanac 三個 client),且 byte-heap 從未發版(仍在
4.0.0-SNAPSHOT的[Unreleased]),故無下游破壞成本。 - 沿用 ADR-006 假設:快取為支援性結構,不應是 heap 的主要消費者;大量、變動大小的負載本就適合放 offheap。
- 平台目標 JDK 為 25。
考量的方案 (Options Considered)
方案 A: byte 預算掛 offheap,heap 退成筆數快取層 — 本案
說明:移除 heapMemory(MB) / fastHeapMemory(MB) 與 TierConfiguration.heapSizeMB;heap 只剩 heap(entries)。byte 預算只治理 offheap(精確、sizeof-free)。要真實 byte 封頂就用 offheap,heap 作為其上的小筆數快取層。
優點:
- ✅ 保住 ADR-006 的核心目標(防爆行程記憶體的真實 byte cap),但放在 bytes 真正可精確量測的 offheap 上。
- ✅ sizeof 引擎完全不再被觸發,免疫 JDK 25 的 Instrumentation/Unsafe 不可靠。
- ✅ 寫入零 sizeof 開銷;ADR-006 的兩層骨架、offheap 四段 fallback 原樣保留。
缺點:
- ❌ heap-only cache 只能筆數封頂(無 byte cap);要 byte 安全須用 offheap。
- ❌ offheap 要求 key/value 可序列化、且有序列化 CPU/延遲成本。
評分: 5
方案 B: heap 改筆數、另加 heap 筆數預算維度(「記憶體大小 + 物件數量」)
說明:offheap 用 MB 預算、heap 另立筆數預算,兩種單位並存。
優點:✅ 最簡單、零 sizeof。 缺點:❌ 預算裂成兩種單位、無法加總成單一「行程總量」上限;heap 仍無真實 byte cap,未達「防爆」目標。
評分: 3
方案 C: nominal weigher(heap byte 預算 = 筆數 × 宣告每筆大小)
說明:保留 MB 介面,但以呼叫端宣告的每筆估值換算筆數驅逐,runtime 不跑 sizeof。
優點:✅ 保住統一 MB 介面、零 sizeof。 缺點:❌ 準確度靠呼叫端估值;仍是「估」而非真實量測,未比 offheap 精確。
評分: 3
方案 D: 維持 ADR-006 byte-heap,調 sizeof / 加 JVM 參數
說明:保留 byte-heap,靠 -Djdk.attach.allowAttachSelf=true 或 --add-opens 讓 sizeof 較可靠。
缺點:❌ 把 JDK 脆性轉嫁成部署負擔;Unsafe 仍在移除排程,治標不治本。
評分: 1
決策 (Decision)
選擇方案: A
核心理由:
- 管制的首要目的是防爆行程記憶體;真實 byte cap 應放在 bytes 可精確量測的 offheap,而非靠 JDK 25 已不可靠的 on-heap sizeof。
- sizeof 的麻煩只在 on-heap byte 計量;移到 offheap 即根除,且 ADR-006 的其餘骨架(加總檢查、runtime 驅逐、safe-by-default、offheap 四段 fallback)原封不動。
- byte-heap 從未發版、呼叫端全在樹內,可乾淨移除、零下游成本。
本案取代 ADR-006 的 byte-heap 子決策,不推翻 ADR-006 的記憶體管制整體方向;ADR-006 狀態改為「Superseded(byte-heap 部分)」並指向本 ADR。
權衡分析 (Trade-offs)
我們獲得什麼 (Gains)
- ✅ 真實 byte 封頂仍在(落在 offheap),且不依賴 JDK 25 已不可靠的 sizeof。
- ✅ 寫入零 sizeof 開銷;管制邏輯與心智模型更單純(byte 預算 = offheap 一種單位)。
我們放棄什麼 (Losses)
- ❌ heap tier 不再能以真實 bytes 封頂(改筆數計)。
- ❌ 要 byte 安全的負載須走 offheap,承擔序列化成本與
Serializable要求。
風險與緩解措施 (Risks & Mitigations)
| 風險 | 嚴重性 | 機率 | 緩解措施 |
|---|---|---|---|
| heap-only cache 無 byte cap、放大物件撐爆 heap | 中 | 低 | 文件明示「heap-only = 筆數封頂;要 byte 防爆用 offheap」;快取定位為支援性小結構 |
| 既有筆數 heap + offheap 多層 cache 行為改變 | 低 | 低 | heap 筆數計本就是 Ehcache 標準快取層用法;offheap 仍精確封頂 |
| 誤以為 heap 筆數 = 記憶體上限 | 低 | 中 | Javadoc / 設計指南標明筆數計不封 bytes |
影響 (Consequences)
正面影響
- ➕ 移除對 Ehcache sizeof 引擎的依賴,免疫其在新 JDK 的退化。
- ➕ 管制單位收斂為 offheap 一種,API 與推導更簡單。
負面影響
- ➖ heap 失去 byte 封頂能力;需要 byte 安全者改用 offheap。
中性影響
- 🔸 ADR-006 既有的 offheap 管制、safe-by-default、四段 fallback、observe-not-block 全數不變。
實作指南 (Implementation Guidelines)
必須遵守的規則
- 移除 byte-heap API surface:
CacheBuilder.heapMemory、DualCacheBuilder.fastHeapMemory、TierConfiguration.heapSizeMB一律刪除;heap 只剩heap(entries)。 - EhcacheConfigMapper:heap 永遠走
ResourcePoolsBuilder.heap(entries),不再有heap(MB, MemoryUnit)路徑(sizeof 不被觸發)。 - byte 預算只治理 offheap:
MemoryBudget只留offheapBudgetMB/onExceedOffheap/offheapBudgetSource;MemoryBudgetResolver.resolve(offheapBudgetMB, onExceed)只推 offheap(四段 fallback 不變)。 - enforceBudget:不再
REJECT筆數 heap;只做 offheap 加總檢查(讀 live config 重算、含外部 cache),超額依OnExceed處置。 - 遷移樹內呼叫端:almanac
LocationService/AddressService/CalendarService的heapMemory(1)還原為各自原本的筆數(heap(16)/heap(64)/heap(8),皆 heap + disk 多層)。
檢查清單
- 移除
heapMemory/fastHeapMemory/TierConfiguration.heapSizeMB與互斥驗證 -
EhcacheConfigMapperheap 永遠筆數計 -
MemoryBudget/MemoryBudgetResolver/CacheManagerBuilder移除 heap byte 預算,保留 offheap 四段 fallback -
EhcacheCacheManager.enforceBudget改為 offheap-only、移除 uncounted-entries-heap 警告 - almanac 三個 client 還原筆數 heap
- 測試:移除無效的
governed_byteHeap_isAllowed(從不put),補 offheap put/get round-trip + governed entries-heap 合法 + offheap 加總 REJECT -
CHANGELOG.md[Unreleased]修訂(byte-heap 未發版,原地調整非疊加 Breaking Changes)
相關文檔 (References)
內部文檔
- ADR-006: CacheManager 層記憶體預算管制(被本案取代 byte-heap 部分)
- cache 模組原始碼:
appfuse-serverio.leandev.appfuse.cache.{api,core,config,builder,adapter} - HTTP/Cache/CSV/Content 工具指南:
../guides/core/cache.md
相關規範
03-versioning.md(框架層 SemVer)、m-changelog-format.md(CHANGELOG 與 Breaking Changes 格式)、30-public-api.md(公開 API 範圍觸發 CHANGELOG)
外部資源
- Ehcache 3 Tiering — heap/offheap/disk、byte-sized heap、sizeof
- JEP 451: Prepare to Disallow the Dynamic Loading of Agents
- JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
變更歷史 (Change Log)
| 日期 | 變更內容 | 變更者 |
|---|---|---|
| 2026-06-28 | 初版、狀態 → 已接受:byte 封頂從 on-heap(ADR-006 sizeof)移到 offheap;移除 heapMemory / fastHeapMemory / TierConfiguration.heapSizeMB 與 CacheManagerBuilder.heapBudgetMB,MemoryBudget/Resolver/enforceBudget 收斂為 offheap-only;almanac 三 client 還原筆數 heap;測試補 offheap put/get round-trip;CHANGELOG [Unreleased] 原地修訂(byte-heap 未發版)。appfuse-server 全測試通過 | Development Team + AI |
文檔維護者: Development Team + AI Assistant 最後審閱: 2026-06-28