memstore 明明分了 50%,为什么只用了 0.09G?——一次 OB 租户内存认知纠偏
「勇敢DBA不怕困难」 · 数据库爱好者,顺手折腾 AI 和工具。踩过的坑写出来,希望你能少踩几个。
那天下午例行巡检,一条 SQL 下去,屏幕上的数字让我愣了三秒:
KVSTORE_CACHE_ID 8.4G
MEMSTORE_CTX_ID 0.09G
一个 12G 的租户,KVCache 吃了 8 个多 G。
而文档里白纸黑字写着:memstore 占租户内存的 50%。
我心里咯噔一下:6G 的 memstore 怎么只剩 0.09G?KVCache 凭什么越过 50% 的线吃到 8.4G?是配错了,还是集群出了什么 bug?
如果你看到这组数字的第一反应跟我一样——恭喜,你踩进了同一个坑。今天把这个坑从头到尾挖开。
—— 01 翻文档,越翻越糊涂 ——
第一反应翻官方文档,找到这段话(原文截图,取自 V4.4.1 文档中心):
不可动态伸缩的内存管理
目前与不可动态伸缩内存相关的配置只有 memstore_limit_percentage 和 ob_sql_work_area_percentage,前者表示租户的 MemStore 部分最多占租户总内存上限的百分比,后者表示 SQL 阻塞算子工作区内存占用上限。
租户的写入或者更新会增加 MemStore 的内存使用,当租户的 MemStore 部分内存到达上限以后,后续的写入或者更新操作将会被拒绝。
OceanBase 数据库会根据 MemStore 的内存使用比例决定何时进行转储或者合并释放 MemStore 的内存,该比例由配置项 freeze_trigger_percentage 控制,表示当 MemStore 内存占用到达其上限的百分比后就进行冻结(转储的前置动作),默认值为租户 MemStore 内存上限的 20%。

memstore_limit_percentage 表示租户的 MemStore 部分最多占租户总内存上限的百分比。当 MemStore 部分内存到达上限以后,后续的写入或者更新操作将会被拒绝。
字都认识,连起来就是解释不了眼前的现象。
按这段话的理解,12G 租户的内存应该是这样切的:
12G = memstore 6G + 其他 6G
那 KVCache 无论如何不该超过 6G——可它实实在在吃了 8.4G。
文档和现实,总有一个在说谎。
—— 02 先把账本摊开算 ——
光吵没用,先把账算清楚。把那个租户三个节点的内存全部拉出来:
KVSTORE_CACHE_ID 8.4G ← 大头
DEFAULT_CTX_ID 0.97G
PLAN_CACHE_CTX_ID 0.37G
CO_STACK 0.12G
MEMSTORE_CTX_ID 0.09G ← 就这?
其他零零碎碎 ~0.1G
───────────────────────────
合计 ≈ 10.1G
有意思的来了:所有模块加起来 10.1G,没超 12G;但 KVCache 一家 8.4G,远超"6G 以内"的预期。
也就是说,集群没超卖、没爆内存、一切健康——"50% 被侵犯"这个前提本身,可能就是错的。
那 50% 到底是什么意思?
—— 03 源码捅破窗户纸:50% 是墙,不是切片 ——
带着问题去啃 observer 源码,在租户内存管理那段找到了关键代码(ob_resource_mgr.cpp 的 update_hold):
if (!limiter_.acquire(size)) {
return false; // 第一关:租户总内存
} else if (label != OB_KVSTORE_CACHE_MB) {
if (!update_ctx_hold(ctx_id, size)) return false; // 第二关:各模块自己的额度
} else {
update_cache_hold(size); // KVCache:只记账,不查额度!
}
两行代码,捅破了窗户纸:
memstore 的 50% 是"使用上限",不是"预分配的切片"。
这两者的区别,用一个仓库的比喻就明白了:
切分制(我以为的):仓库一开工就把 50% 的地圈起来挂上"memstore 专用"的牌子,哪怕不堆货,别人也不能进;
额度制(实际机制):只是规定 memstore 最多允许堆到 50% 的位置——墙砌在那里,但今天只进了 0.09G 的货。空着的地,门口的热门货架(KVCache)就摊开用了。
OB 的租户内存走的是额度制(overcommit):memstore 写到多少占多少。我这个租户写入涓涓细流,连零头都没用到,它那 6G 的"地盘"基本空着,KVCache 看到满院空闲,自然吃成了 8.4G。
一句话:墙在 50%,货在 0.09G,货架在吃墙内的空地——谁都没违规。
—— 04 KVCache 岂不是要上天?海绵与石头 ——
马上有下一个问题:KVCache 没有 50% 的额度,那它岂不是能把整个租户吃光?写请求来了怎么办?
继续翻源码,发现这个设计里最妙的一环——洗(wash)机制。
KVCache 的内存是"任何人都能来抢"的:任何模块分配内存吃紧,都会触发 sync_wash,从 KVCache 里把最冷的数据洗出去腾地。这个接口挂在每个模块的分配器上,是通用的。
所以这套内存的江湖规矩是:
先到先吃,后到先抢,KVCache 永远让。
我把它叫做"海绵与石头":memstore 是石头(有墙、只进不出、不能被挤),KVCache 是海绵(无额度、多多益善、随时可被挤干)。
内存世界的挤压永远是单向的——海绵让着石头。
哪天写入洪峰来了,剧情自动反转:memstore 从 0.09G 涨向 6G,KVCache 一路被洗到 4G、3G……全程无人值守,账本永远不破。
顺带解开一个小悬案:你可能注意到 GV$OB_MEMORY 里 KVCache 的 USED 恒等于 0——不是统计坏了,是它的内存压根不走普通模块的记账路径,单独走 cache_hold 计账。看 HOLD 就行。
—— 05 三条水位线,文档只写了一条 ——
再看 memstore 的完整生命周期,其实是三条线:
20% ─── 冻结线(freeze_trigger_percentage 默认 20)
到这就冻结+转储,日常在这附近波动
60% ─── 限速线(writing_throttling_trigger_percentage 默认 60)
写入变慢但不停,边写边等
100% ── 拒绝线(memstore_limit)
这才报 4013 拒绝写入
12G 租户:冻结线 1.2G、限速线 3.6G、拒绝线 6G。
注意这里有个三层嵌套的百分比:"20%" 乘在 memstore 上限 6G 上,不是乘在租户总内存 12G 上——冻结线 1.2G,相当于总内存的 10%。
文档一句"默认值为租户 MemStore 内存上限的 20%",把参数值和线的位置揉在一起,读的人十个有八个在这里晕。
更妙的是,啃源码还会发现冻结线是动态的:
memstore_freeze_trigger = MIN(memstore_limit, 当前实际还能拿到的内存) × 20%
如果 KVCache 把租户内存吃得所剩无几,冻结线会比 20% 更早触发——系统会提前让 memstore 收敛。这是文档完全没写的自平衡设计。
—— 06 回头提了个工单 ——
搞明白之后再看那段文档,终于看清它的两个问题:
一是漏了关键一句。文档只说"最多占 50%",从没说过"没用到的额度可以被其他模块临时使用,memstore 涨上来时它们会让出"。少了这半句,谁都会把"最多占"读成"分走了"——字面没错,信息不完整,歧义就是这么来的。
二是从 20% 直接跳到"拒绝写入",中间整整一条 60% 限速缓冲带被吞了。
于是我提了个工单,把实测数据和两句改写建议一起发了过去。
官方同学的回复是:"文档写了最多占,没有问题。"——技术上他没错,但"读者会误解"这件事本身,就是文档的 bug。
我也没争,把矛盾数据(8.4G > 6G)和具体改写文本递上去:给文档团队提需求,比争论"有没有歧义"有用得多。
至于改不改,随缘。文章你先看到,就不亏。
—— 07 写在最后 ——
回头看这次折腾,最有价值的不是弄懂了 50%,是那个思维转折:
当观测数据和认知矛盾时,先别急着怀疑集群——先怀疑自己的理解模型。
12G、8.4G、0.09G 这三个数字从头到尾都是对的,错的是我脑子里那张"内存要切块分"的图。换成"额度+弹性",所有矛盾瞬间自洽。
文档没写的半句,源码里都有。
下次你看到 KVCache 吃掉租户 70% 内存,可以安心喝茶了。
—— 附:这次用到的三条 SQL,拿走不谢 ——
-- ① 按模块看租户内存账本
SELECT CTX_NAME, ROUND(SUM(HOLD)/1024/1024/1024,2) AS GB
FROM OCEANBASE.GV$OB_MEMORY
WHERE TENANT_ID = <你的租户>
GROUP BY CTX_NAME ORDER BY GB DESC;
-- ② memstore 三条水位线(注意视图名:GV$OB_MEMSTORE,不带 INFO;带 INFO 的是 memtable 明细视图)
SELECT svr_ip,
ROUND(ACTIVE_SPAN/1024/1024/1024,2) AS active_gb,
ROUND(MEMSTORE_USED/1024/1024/1024,2) AS total_gb,
ROUND(FREEZE_TRIGGER/1024/1024/1024,2) AS freeze_line_gb,
ROUND(MEMSTORE_LIMIT/1024/1024/1024,2) AS limit_gb
FROM oceanbase.GV$OB_MEMSTORE
WHERE tenant_id = <你的租户>;
-- ③ KVCache 里面谁在吃(顺带看命中率)
SELECT CACHE_NAME, PRIORITY,
ROUND(SUM(CACHE_SIZE)/1024/1024/1024,2) AS GB,
ROUND(AVG(HIT_RATIO),1) AS hit_ratio
FROM OCEANBASE.GV$OB_KVCACHE
WHERE TENANT_ID = <你的租户>
GROUP BY CACHE_NAME, PRIORITY ORDER BY GB DESC;
你的库里,KVCache 最大吃到过多少?评论区聊聊。
—— 参考资料 ——
文中引用的官方文档原文(截图取自 V4.4.1 文档中心,官方后续可能修订):
https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000003977299
《租户内部内存管理 · 不可动态伸缩的内存管理》一节
有帮助的话,点个赞👍
我是胖虎,关注我,持续分享数据库和 AI 这条路上的坑与解。
想聊的话,后台留言或加我微信都行。

浙公网安备 33010602011771号