跳至主要内容

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-heapADR-006heapMemory(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 上已不可靠:

  1. 最準的 Instrumentation 路預設失效:sizeof 的 AgentSizeOf 靠動態 self-attach 載入內嵌 agent 取得 Instrumentation.getObjectSize(),但 JDK 9 起 jdk.attach.allowAttachSelf 預設 false、JDK 21+(JEP 451)對動態載入 agent 發警告 → self-attach 預設走不通。
  2. 退化到 deprecated 的 Unsafe:fallback UnsafeSizeOfsun.misc.Unsafe,在 JDK 25 已 terminally deprecated(JEP 471/498)、排程移除;可跑但近似、且壽命有限。
  3. 最後的反射路在強封裝下不可靠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

核心理由:

  1. 管制的首要目的是防爆行程記憶體;真實 byte cap 應放在 bytes 可精確量測的 offheap,而非靠 JDK 25 已不可靠的 on-heap sizeof。
  2. sizeof 的麻煩只在 on-heap byte 計量;移到 offheap 即根除,且 ADR-006 的其餘骨架(加總檢查、runtime 驅逐、safe-by-default、offheap 四段 fallback)原封不動。
  3. 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)

必須遵守的規則

  1. 移除 byte-heap API surfaceCacheBuilder.heapMemoryDualCacheBuilder.fastHeapMemoryTierConfiguration.heapSizeMB 一律刪除;heap 只剩 heap(entries)
  2. EhcacheConfigMapper:heap 永遠走 ResourcePoolsBuilder.heap(entries),不再有 heap(MB, MemoryUnit) 路徑(sizeof 不被觸發)。
  3. byte 預算只治理 offheapMemoryBudget 只留 offheapBudgetMB / onExceedOffheap / offheapBudgetSourceMemoryBudgetResolver.resolve(offheapBudgetMB, onExceed) 只推 offheap(四段 fallback 不變)。
  4. enforceBudget:不再 REJECT 筆數 heap;只做 offheap 加總檢查(讀 live config 重算、含外部 cache),超額依 OnExceed 處置。
  5. 遷移樹內呼叫端:almanac LocationService / AddressService / CalendarServiceheapMemory(1) 還原為各自原本的筆數(heap(16) / heap(64) / heap(8),皆 heap + disk 多層)。

檢查清單

  • 移除 heapMemory / fastHeapMemory / TierConfiguration.heapSizeMB 與互斥驗證
  • EhcacheConfigMapper heap 永遠筆數計
  • 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-server io.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)

外部資源


變更歷史 (Change Log)

日期變更內容變更者
2026-06-28初版、狀態 → 已接受:byte 封頂從 on-heap(ADR-006 sizeof)移到 offheap;移除 heapMemory / fastHeapMemory / TierConfiguration.heapSizeMBCacheManagerBuilder.heapBudgetMBMemoryBudget/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