原文鏈接:OceanBase分布式數據庫-海量數據 筆筆算數
本文介紹 KVcache 相關問題的排查方法。
KVCache 相關概念
在進行排查前,需要了解幾個概念。
-
pin
一個 cache 塊 ( memblock ) 被 pin 住,表示它正在被引用。
cache 的由多個定長的塊組成,每個塊稱為一個 memblock 。每個 memblock 中存放了多個 KV ,使用者通過 KV 指針來讀取 KV 。容易理解,在使用指針的過程中,需要保證指針的安全,也即需要 pin 住存放 KV 的 memblock ,保證它不被釋放。cache 內部通過引用計數來實現,cache 每向外吐出一個 KV 指針,都要對 KV 所在的 memblock 的引用計數加 1 ( pin 住這個 memblock ) ,當不再使用 KV 指針時,引用計數減 1 。也就是說,當引用計數大于 0 時,說明有人正在讀其中的內存,那這個 memblock 就不能被釋放,反之如果等于 0 ,則釋放是安全的。
簡而言之,如果一個 memblock 被 pin 住,那么它不能被釋放。
-
sync wash
sync wash 指 cache 騰出自身內存給租戶使用的過程。 從 1 可知,如果一個 memblock 沒有被 pin ,那么它是可以被釋放的。sync wash 就是 cache 找到沒有被 pin 的 memblock ,釋放內存,騰給租戶使用。
-
cache 大小無上限
cache 的大小在一個租戶內是無上限的。 為了最大化利用租戶內存,在租戶內不限制 cache 的大小,理論上 cache 最大可以占滿一個租戶的內存。但是 cache 的內存比較特殊,在租戶需要內存時,會觸發 cache 的 sync wash ,騰出內存給租戶使用。
常見問題
ret=-4273 can not find enough memory block to wash
首先 4273 不是 cache 的問題,4273 報錯表示在 sync wash 過程中發現 cache 中所有的 memblock 都被 pin 了,沒有內存可以釋放。 導致 4273 的原因可能有:
-
cache 本身已經被榨干了,沒有內存可以 wash ,所以就找不到不被 pin 的 memblock。
-
cache 本身還有內存,但是都被 pin 住了,無法 wash 出來;這個原因又分為兩種情況:
- 確實有需要較多 cache 的 SQL 在執行。
- cache handle 引用計數有泄漏。
排查的思路就是逐步確認是哪個原因導致的 4273 ,步驟如下:
-
查看當時 cache 的大小。
* 通過 MEMORY 日志,memory 日志可以看到租戶的 cache_hold ,這個字段記錄了 cache 的總大小。 * 通過 CACHE 日志,可以看到當時各個 cache 的大小。 * 通過虛擬表 `__all_virtual_kvcache_info`,各版本都有,但是是實時數據,需要在案發時查詢。
如果此時 cache 大小很少,說明此時 cache 本身已經被榨干,wash 不出內存符合預期,應該查看當時租戶的內存分布,看看其他 mod 是否符合預期,否則進入第二步。
-
判斷是否存在泄漏。
這一步是通過查看 cache 的大小能否降下來,來區分是 cache handle 引用計數有泄漏還是確實有需要較多 cache 的 SQL 在執行。
對于 OceanBase 數據庫 V4.0 及以后版本,可以停止所有查詢后嘗試手動 flush cache ,如果 cache 能降下來,則說明沒有泄漏,是確實有需要較多 cache 的 SQL 在執行導致。
手動 flush 需要直連到 OBServer 節點上執行,如果可以 flush 干凈 ( 可能需要手動 flush 多次 ) ,則表示沒有泄漏,對比 flush 前后?
__all_virtual_kvcache_info
?表中 size 的變化來判斷。也可以通過查詢?__all_virtual_kvcache_store_memblock
?虛擬表來直接查看所有 memblock 引用計數,看是否有異常。select * from __all_virtual_kvcache_store_memblock where ... order by ref_count desc limit 10;
對于 OceanBase 數據庫 V4.0 之前版本,只能通過日志查看在 4273 報錯后,cache 的大小是否降下去過,如果能降下去,則說明一定沒有泄漏,如果沒降下去過,則無法判斷。( 因為之前版本手動 flush 并不會立即釋放 cache 的內存,而是通過降低其訪問熱度,通過后臺 wash 線程慢慢刷出去。 )
如果是較多 cache 的 SQL 在執行導致,規避方案是,將 cache 用量較大的操作分散在租戶內存壓力較小的時候執行,如果仍有報錯,嘗試對租戶內存進行擴容,增大內存。 如果是 cache handle 引用計數有泄漏導致,則需要復現問題,排查泄漏的路徑。
cache 占用內存較高
如前文所述,cache 在租戶內是無上限的,所以理論上無論 cache 占多大內存,只要能被 sync wash ,就是符合預期的。 判斷能否 sync wash 出來需要參考問題 1 中判斷 cache 大小能否降下來的方法,能降下來就是能 sync wash 出來。
cache 預熱
OceanBase 數據庫 V4.0 及以后的版本支持 cache 預熱功能,之前版本沒有此功能。 為緩解 compaction 后的性能抖動,在 compaction 時會將新生成的微塊放入 cache 中,進行預熱。 預熱并不會將所有新生成的微塊都預熱進 cache ,而是根據租戶的內存情況進行預熱。現有策略下,data block cache 使用租戶空閑內存的 5% ,index block 使用租戶空閑內存的 2% 。 data block 和 index block 按照不同的優先級被預熱進 cache ,按照中間層索引樹的等級分配不同的優先級,越接近根節點的 block 優先級越高。
wash
cache 騰出自身內存的過程稱為 wash ,cache 的 wash 行為分為同步 wash 和異步 wash 兩種。 同步 wash 就是上文提到的 sync wash,同步騰出內存,這里不再贅述。 異步 wash 由一個后臺 wash 線程完成,wash 線程會定期地根據每個租戶的內存壓力 ( 包括租戶大小、當前 cache 大小、租戶當前空閑內存等 ) 計算出租戶應該 wash 出的 cache size ,然后再根據每個 memblock 的訪問熱度從低到高 wash 。如果壓力不大,計算結果可能是 0,不做 wash ,可以根據如下日志來判斷,如果有則表示 wash 線程異步 wash 了 memblock。
COMMON_LOG(INFO, "Wash memory, ","tenant_id", wash_iter->first,"cache_size", tenant_wash_info->cache_size_,"lower_mem_limit", tenant_wash_info->lower_limit_,"upper_mem_limit", tenant_wash_info->upper_limit_,"min_wash_size", tenant_wash_info->min_wash_size_,"max_wash_size", tenant_wash_info->max_wash_size_,"mem_usage", lib::get_tenant_memory_hold(wash_iter->first),"reserve_mem", static_cast<int64_t>((static_cast<double>(tenant_wash_info->upper_limit_)) * tenant_reserve_mem_ratio_),"wash_size", tenant_wash_info->wash_size_);
wash 線程的異步 wash 實際上是減去 memblock 原始的引用計數,等待引用計數減為 0 時執行釋放,因此:
- 異步 wash 并不能立即釋放 memblock ,需要等待不被 pin 。
- 如果有引用計數泄漏泄漏,wash 線程一樣不能 wash memblock。
手動 flush
手動 flush cache 表示手動清理指定 cache , 命令如下,目前只能在 sys 租戶下執行,需要直連要 flush 的 OBServer 節點。
alter system flush kvcache [tenant tenant_name [cache 'cache_name']];
cache_name 可以在?__all_virtual_kvcache_info
?中查到,常用的 cache_name 有 :user_block_cache、index_block_cache、user_row_cache、fuse_row_cache、bf_cache。
OceanBase 數據庫 V4.0 及以后版本,flush 包含了立即清空的功能,預期情況下,flush 之后 cache 應該是立即被清空(若內存占用太多,flush 只清理一部分內存)。如果發生 4274 的報錯,是 cache 較多導致的超時問題,再次 flush 即可。
OceanBase 數據庫 V4.0 之前版本,手動 flush 只會清除指定 cache 中 KV 的索引,索引刪除后,對應的 KV 就無法再被訪問到,其所在的 memblock 的熱度就會持續降低,等待 wash 線程將其 wash 出去。
判斷手動 flush 是否生效
無論新老版本,手動 flush 一定會清理掉全部的 kv_cnt ,可以觀察 flush 前后 kv_cnt 是否清零過來判斷手動 flush 是否生效。
select * from __all_virtual_kvcache_info where cache_name = '<cache_name>';
監控 cache handle ,排查引用計數泄漏
OceanBase 數據庫 V4.0 及之后版本,如果懷疑或確認 cache 引用計數有泄漏,可以通過如下方法診斷。
-
binary 需要啟用 ENABLE_DEBUG_LOG 編譯選項。
-
打開監控,指定監控的 cache name 。
alter system set leak_mod_to_check = 'cache_name';
-
查看泄漏 backtrace 。
select * from __all_virtual_kvcache_handle_leak_info where tenant_id = tenant_id order by hold_count desc limit 10;
在 OceanBase 數據庫 V4.3 及以后版本虛擬表更名為?
__all_virtual_storage_leak_info
,并且新增配置項?_storage_leak_check_mod
?用于指定泄漏監控的內容。這里簡單介紹一下監控的實現方法。cache 對外吐出的引用計數都包含在 cache_handle 中,一個 cache_handle hold 住 1 個引用計數,所以在 cache 對外吐出 cache_handle 時,記錄一條 backtrace ,在 cache_handle reset 時,消除記錄。所以最后遺留下來未釋放的 backtrace 很大可能就是泄漏的 backtrace 。
根據記錄方式可以知道,只要當前 cache 外部有 cache_handle ,那就會被記錄 backtrace ,所以任何路徑在持有 cache_handle 期間都會被記錄,需要抓到持有時間過長或不符合預期的堆棧才是泄漏的堆棧。
如果事先沒有開啟 cache handle 監控,可以通過?
__all_virtual_kvcache_store_memblock
?虛擬表簡要確認一下問題,這張表會輸出當前 server 上所有 memblock 的信息。 (OceanBase 數據庫 V 4.3 及以后版本使用新的虛擬表)select * from __all_virtual_kvcache_store_memblock where ... order by ref_count desc limit 10;
關注?
ref_count
?列,這一列表示 memblock 的當前的引用計數,目前當 memblock 沒有被引用時,這里拿到的引用計數為 2 ( 初始引用計數為 1 ,查 memblock 信息時需要先加引用計數做保護,因此為 2 ) 。在確保某 memblock 當前不會被 pin 的前提下,ref_count 大于 2 的 memblock 都可以認為有引用計數泄漏。
泄漏檢測擴展 (OceanBase 數據庫 V4.3 及以后版本)
OceanBase 數據庫 V4.3 及以后版本,新增存儲層的泄漏檢測功能,目前支持的檢測內容有 cache handle、io handle、storage iter ,同時,新增配置項?_storage_leak_check_mod
?用于配置泄漏檢測的內容,有效值分別為?cache_name / all_cache / io_handle / storage_iter
?,默認為空串,設定成空串或其他值時,關閉監控。
同時,__all_virtual_kvcache_handle_leak_info
?虛擬表更名為?__all_virtual_storage_leak_info
。
檢測步驟使用新的配置和虛擬表即可:
-
binary 需要啟用 ENABLE_DEBUG_LOG 編譯選項。
-
打開監控,指定監控的內容。
alter system set _storage_leak_check_mod = 'cache_name' | 'all_cache' | 'io_handle' | 'storage_iter' ;
-
查看泄漏 backtrace。
select * from __all_virtual_storage_leak_info where ... order by hold_count desc limit 10;