在数据量快速增长的HTAP场景中,IMCI列存索引的元数据内存会随行数和列数线性增长,可能导致集群内存不足甚至OOM。本文介绍PolarDB MySQL版的IMCI(In-Memory Column Index)内存占用特征、元数据详解、内存查询方法以及参数调优方式,帮助您合理管理列存索引内存并降低内存使用。
适用范围
本文的PolarDB MySQL版集群版本为:
MySQL 8.0.1,内核小版本为8.0.1.1.50及以上。
MySQL 8.0.2,内核小版本为8.0.2.2.30及以上。
背景介绍
IMCI(In-Memory Column Index)与InnoDB的Buffer Pool在内存管理上存在本质差异。InnoDB的Buffer Pool是纯LRU缓存,可以通过参数灵活调整大小。而IMCI的内存由两部分组成:一部分是随数据量线性增长的常驻元数据(无法通过缓存参数缩减),另一部分是LRU可控的缓存数据。了解这些差异有助于您合理规划内存和进行参数调优。
对比维度 | InnoDB Buffer Pool | IMCI内存 |
缓存模式 | 纯LRU缓存,可通过 | 元数据常驻内存 + 部分数据LRU缓存。 |
可控性 | 调整参数即可缩放。 | 元数据随数据量增长而增长,不可通过缓存参数缩减。 |
扩容需求 | 数据量增长时缓存命中率下降但不影响启动。 | 数据量增长可能导致需要更大规格才能启动。 |
IMCI的元数据(PackMeta、Mask、ExtentInfo等)会常驻内存,其大小取决于表数量、列数量和行数量。本文中列出的参数默认值均为新版本默认值。实际业务环境通常会通过集群模板对部分参数进行预配置,实际生效值可能与代码默认值不同。请始终以SHOW GLOBAL VARIABLES LIKE 'imci_%'查询到的实际值为准。
IMCI存储模型术语说明
在深入理解内存占用前,需先了解IMCI的核心存储概念。以下术语在后续章节中反复出现。
术语 | 含义 |
Pack | IMCI的基本存储单元,类似InnoDB的Page。每个Pack存储固定行数的某一列数据。 |
PackMeta | 每个Pack的元数据对象,记录行数、空值数、数据大小、MinMax统计值等信息。每列每Pack一个,约350字节,常驻内存。 |
Mask | 每个Pack维护的行级可见性数组(Insert Mask + Delete Mask),本质是per-row的uint64事务版本号数组。每Pack一个(所有列共享),按需分配。 |
NCI | Non-Clustered Index,非聚簇索引。IMCI中的二级索引,使用LSM-tree + B-Tree结构存储。 |
Pruner | 数据裁剪器,利用MinMax统计值或Bloom Filter在查询时快速跳过不相关的Pack,减少扫描量。 |
RowBuf | Row Buffer,行缓冲区。用于行存到列存转换过程中的中间结果缓存。 |
IMCI内存分类
IMCI内存分为两大类:常驻元数据(不可缓存淘汰,随数据量线性增长)和LRU可控数据(可通过参数动态调整上限)。
常驻元数据(不可淘汰)
常驻元数据与行数、列数线性相关,不可通过缓存参数缩减。以下为各ModId及其内存特征:
PACKMETA_OBJ:约350B x Pack数 x 列数,行数x列数较大时为最大项。NCI_BP:NCI BufferPool结构,表多且nci_lsm_bp_shard大时可成为最大项。MASK_GROUP:默认LSM-mask模式下,per-pack x pack_size x 8B。MASK_LSM:Mask LSM Memtable层内存,受imci_mask_memtable_size控制。PACK_EXT_META:MinMax统计值,约32B x Pack数 x 列数。EXTENT_INFO:Extent磁盘位置信息,约24B x Pack数 x 列数。OBJ_STRUCT、NCI_STRUCT/NCI_BTREE、COLINFO_OBJ、PACKMETA_VEC。
LRU可控数据
LRU可控数据可通过对应的*lru_cache_capacity参数动态调整上限。未使用的功能(如未创建全文索引时FTS_DICT)不会实际占用缓存空间。
LRU_PACK:对应参数imci_lru_cache_capacity。NCI_BP_PAGE:对应参数imci_nci_lru_cache_capacity,写入压力大时占比较多。BLOOM_FILTER/PRUNER:对应参数imci_pruner_lru_cache_capacity。FTS_DICT:对应参数imci_fts_lru_cache_capacity。
关于Mask模式(
enable_lsm_mask参数,默认true,READ_ONLY):默认模式下PACK_MASK的ALLOC为0;如果在IMCI_MEM_ALLOC中看到PACK_MASK有值,说明该集群使用的是传统Array Mask模式。LRU内存会根据负载自适应调整,该部分内存只作介绍,不建议自行调整。
数据量增长与元数据内存增长速率
以一张1亿行、100列的表为例(pack_shift=16,pack_size=65536,Pack数量约1,526),各类常驻元数据随数据量增长的估算量级如下:
元数据类型 | 增长维度 | 单位开销 | 1亿行x100列估算 | 实际情况 |
PackMeta( | per-Pack x per-Column | 约350B/pack/列 | 约51 MB | 常驻元数据中通常是最大项(行数x列数决定)。 |
PACK_EXT_META(MinMax统计) | per-Pack x per-Column | 约32B/pack/列 | 约4.7 MB | 数值列为主;字符串列另算 |
EXTENT_INFO(磁盘位置) | per-Pack x per-Column | 约24B/pack/列 | 约3.5 MB | 仅已持久化的file-pack。 |
PACKMETA_VEC(指针向量) | per-Pack x per-Column | 16B/pack/列 | 约2.3 MB | 存储PackMeta指针数组。 |
Mask类( | per-Pack(与列数无关) | pack_size x 8B/列 | 理论上限约763 MB | 默认LSM-mask模式下通常非主要开销。 |
NCI结构( | per-NCI索引行数 | 与B+Tree节点数相关 | 视索引数量而定 | 通常占用较小,无NCI索引时为0。 |
NCI_BP(常驻结构) | 表数 x nci_lsm_bp_shard | BufferPool结构 + 分片对象 | 视表数和分片数而定 | 表多且shard大时可成为常驻大头。 |
占内存较大的元数据详解
以下逐一分析IMCI中占内存较大的7类元数据,每类包含内存计算公式和示例说明。
PackMeta(PACKMETA_OBJ) — 每Pack每列约350字节
IMCI以Pack为基本存储单位。pack_shift是Pack行数的指数,pack_size = 2^pack_shift(代码默认pack_shift=16即每Pack存65,536行)。每个Pack的每一列都有一个PackMeta对象,记录行数、空值数、数据大小、MinMax统计值等信息。
内存占用计算公式:
PackMeta总内存 ≈ SUM(NUM_PACKS x NUM_COLS) x 350字节通过以下SQL可直接获取Pack x 列的总数(推荐,无需手动估算):
-- 查询当前集群所有列存索引的 Pack×列 总数,直接乘以 350 即可得到 PackMeta 估算内存
SELECT SUM(NUM_PACKS * NUM_COLS) AS total_pack_cols,
ROUND(SUM(NUM_PACKS * NUM_COLS) * 350 / 1024 / 1024, 2) AS packmeta_est_mb
FROM information_schema.IMCI_INDEXES;示例:一张1亿行、100列的表(pack_shift=16,即pack_size=65536):
NUM_PACKS≈ 1,526,NUM_COLS= 100。PackMeta内存 ≈ 1,526 x 100 x 350B ≈ 51 MB。
Mask内存 — 两种互斥模式(LSM vs Array)
PackMeta是per-Pack-per-column的,而Mask是per-Pack的(所有列共享同一份Mask)。估算Mask内存时不需要乘以列数。
每个Pack维护Insert Mask和Delete Mask两个掩码,用于MVCC行级可见性判断。每个Mask本质上是一个per-row的uint64事务版本号数组,因此每个Mask缓冲区大小为pack_size x 8字节。
Mask实现有两种互斥模式,由enable_lsm_mask参数控制(默认true,READ_ONLY,切换需重启):
模式 | 参数值 | 使用的ModId | 内存特征 |
LSM Mask(默认) |
|
|
|
传统Array Mask |
|
| 每Pack最大 |
默认模式(LSM Mask)的内存占用计算:
MASK_GROUP内存 ≈ pack_count x pack_size x 8字节(per-pack的seq_v向量)
+ pack_count x 额外的GroupMaskBitmap开销
MASK_LSM内存 ≈ 活跃Memtable大小(可通过imci_mask_memtable_size控制)
MASK_CACHE内存 ≈ 读缓存,部分可控传统模式的内存占用计算:
PACK_MASK每Pack最大 = pack_size x 8 x 2(INS + DEL)
1亿行表(pack_shift=16): 1,526 packs x 65536 x 8 x 2 ≈ 1.49 GBMask是按需分配的。只读场景下,未被修改的Pack不会分配Mask,实际Mask内存会远低于理论上限值。
PACK_EXT_META — MinMax统计值(约32B x Pack数 x 列数)
每个file-pack(已持久化的Pack)的每列存储MinMax统计值(最小值、最大值),用于查询时的Pruning(跳过不相关Pack)。数值类型的MinMax约32字节,字符串类型(PACKSTR_MIN_MAX)为变长存储。
内存占用计算公式:
PACK_EXT_META总内存 ≈(总行数 / pack_size) x 列数 x 32字节ExtentInfo — Extent位置信息(约24B x Pack数 x 列数)
每个file-pack的每列记录数据在磁盘上的Extent位置(ImciCompactedExtentInfo,变长结构约24B)。
内存占用计算公式:
EXTENT_INFO总内存 ≈(总行数 / pack_size) x 列数 x 24字节NCI结构(NCI_STRUCT / NCI_BTREE) — 通常占用较小
NCI(Non-Clustered Index)的B-Tree叶子节点(NCI_STRUCT)和内部节点(NCI_BTREE)。如果未创建任何NCI索引,则NCI相关内存为0。
NCI_STRUCT:B+Tree叶子节点,与索引行数线性相关。NCI_BTREE:B+Tree内部节点结构。nci_lsm_bp_shard:Buffer Pool分片数,每个分片有独立元数据,产生固定开销。可通过SHOW GLOBAL VARIABLES LIKE 'imci_nci_lsm_bp_shard'查看当前值。
实际情况:NCI_STRUCT / NCI_BTREE的常驻内存通常占用较小,不是内存瓶颈。
NCI_BP — 表多且shard大时可成为大头
NCI_BP是NCI BufferPool及其分片结构的常驻内存。每个NCI索引会创建一个BufferPool,内部包含nci_lsm_bp_shard个分片,每个分片包含页表、刷脏队列等管理结构。
当表/索引数量众多(如几万张表)且nci_lsm_bp_shard配置较大时,NCI_BP的常驻结构内存会显著增长,可成为常驻内存中的大头。
注意区分NCI_BP和NCI_BP_PAGE:NCI_BP是BufferPool管理结构,属于常驻内存;NCI_BP_PAGE是BufferPool中的数据页,属于LRU可控数据,受imci_nci_lru_cache_capacity控制。
查询当前集群元数据内存占用
方法一:查询各模块内存分配(推荐)
查看IMCI所有内存模块的分配情况,重点关注ALLOC值较大的模块。
-- 查看 IMCI 所有内存模块的分配情况
SELECT MOD_TYPE,
ROUND(ALLOC / 1024 / 1024, 2) AS alloc_mb,
ROUND(USED / 1024 / 1024, 2) AS used_mb,
HOLD_CNT
FROM information_schema.IMCI_MEM_ALLOC
WHERE ALLOC > 0
ORDER BY ALLOC DESC;常驻元数据(不可通过LRU淘汰):这类元数据常驻内存,其大小通常与数据量(行数、列数)线性相关,无法通过LRU策略淘汰。
元数据类型(MOD_TYPE) | 含义 | 增长维度 | 单位估算 |
| Mask seq_v 向量(默认模式,per-Pack) | 行数 |
|
| Mask LSM Memtable(默认模式) | 写入活跃度 | 受 |
| Insert/Delete 掩码(仅当 | 行数 |
|
| Pack 元数据对象 | 行数 × 列数 | 约 |
| MinMax 统计值 | 行数 × 列数 | 约 |
| Extent 磁盘位置 | 行数 × 列数 | 约 |
| 字符串列的 MinMax | 行数 × 字符串列数 | 变长 |
| 表/列/行组对象结构 | 表数 × 列数 | per表/列 |
| NCI B+Tree 叶子节点(无NCI时为0) | NCI 索引行数 | per-NCI索引 |
| NCI B+Tree 内部节点(无NCI时为0) | NCI 索引行数 | per-NCI索引 |
| 列信息对象 | 列数 | per列 |
| PackMeta指针向量 | 列数 × Pack数 |
|
方法二:PackMeta估算
通过查询IMCI_INDEXES系统表获取Pack x 列总数,直接乘以350即可估算PackMeta内存。
SELECT SUM(NUM_PACKS * NUM_COLS) AS total_pack_cols,
ROUND(SUM(NUM_PACKS * NUM_COLS) * 350 / 1024 / 1024, 2) AS packmeta_est_mb
FROM information_schema.IMCI_INDEXES;方法三:表级列存索引内存
查看各列的内存大小,定位内存占用较高的表和列。
SELECT SCHEMA_NAME, TABLE_NAME, COLUMN_NAME,
ROUND(MEM_SIZE / 1024 / 1024, 2) AS mem_mb
FROM information_schema.IMCI_COLUMNS
WHERE MEM_SIZE > 0
ORDER BY MEM_SIZE DESC;方法四:元数据与可控数据汇总
通过以下SQL将所有ModId按常驻元数据和LRU可控数据两类汇总,快速了解内存构成比例。
SELECT 'metadata_resident' AS category,
ROUND(SUM(ALLOC) / 1024 / 1024, 2) AS total_mb
FROM information_schema.IMCI_MEM_ALLOC
WHERE MOD_TYPE IN(
'PACKMETA_OBJ', 'PACKMETA_ENTRY_OBJ', 'PACKMETA_VEC',
'PACK_MASK', 'PACK_MASK_META_VEC', 'MASK_LSM', 'MASK_GROUP',
'EXTENT_INFO', 'EXTENT_ID_VEC', 'EXTENT_ID_SET',
'OBJ_STRUCT', 'COLINFO_OBJ', 'NCI_STRUCT', 'NCI_BTREE', 'NCI_BP',
'PACK_EXT_META', 'PACKSTR_MIN_MAX', 'ROW_GROUP_META_VEC', 'GROUP_MASK_BITMAP_VEC'
)
UNION ALL
SELECT 'lru_controllable' AS category,
ROUND(SUM(ALLOC) / 1024 / 1024, 2) AS total_mb
FROM information_schema.IMCI_MEM_ALLOC
WHERE MOD_TYPE IN(
'LRU_PACK', 'LRU_STRUCT', 'LRU_RB_BLOCK',
'NCI_BP_PAGE', 'NCI_BP_PAGE_TMP', 'NCI_LRU_HD',
'BLOOM_FILTER', 'PRUNER', 'LRU_PRUNER',
'FTS_DICT', 'MASK_CACHE',
'RB_CACHE', 'CACHE_HANDLE_POOL', 'SINDEX'
);参数调优
调整pack_shift(降低Pack数量)
pack_shift决定每个Pack的行数(pack_size = 2^pack_shift)。增大pack_shift值可显著减少Pack数量,从而等比例降低所有per-pack元数据(PackMeta、PACK_EXT_META、EXTENT_INFO等)的内存占用。
pack_shift | pack_size | 1亿行的Pack数 | PackMeta(100列) | 相对默认值 |
14 | 16,384 | 6,104 | 约207.2 MB | 4x默认 |
16(默认) | 65,536 | 1,526 | 约51 MB | 基准 |
17 | 131,072 | 763 | 约25.9 MB | 0.5x默认 |
18 | 262,144 | 382 | 约13.0 MB | 0.25x默认 |
配置方式:
-- 查看当前默认值
SHOW GLOBAL VARIABLES LIKE 'imci_default_pack_shift';
-- 设置新的全局默认(对新建索引生效)
SET GLOBAL imci_default_pack_shift = 17;建表时通过COMMENT指定:
ALTER TABLE t1 ADD COLUMNAR INDEX idx1(col1, col2) COMMENT 'pack_shift=17';对已有索引生效:修改imci_default_pack_shift仅对新建索引生效,已有的列存索引仍使用原pack_shift。如需对存量数据也生效,需配合imci_dynamic_pack_shift_policy参数:
策略值 | 含义 |
| 不对已有索引应用新 |
| Recover时强制使用新的 |
| 仅从行存全量恢复时使用新 |
| 仅对非分区表生效。 |
增大pack_shift会减少元数据,但可能影响查询裁剪粒度(MinMax pruning粒度变粗,可能扫描更多无关数据)。建议先查询当前实际pack_shift,再逐步调大到17或18进行测试。取值范围:6(最小,64行/Pack)~ 18(最大,262,144行/Pack)。
调整nci_lsm_bp_shard
每个NCI Buffer Pool按nci_lsm_bp_shard分片管理,每个分片有独立的元数据结构。减少分片数可降低固定开销,但会降低并发性能。
参数 | 代码默认值 | 推荐调整 | 说明 |
| 800 | 100~400 | 集群模板可能已调整,请先查询当前值。 |
该参数为READ_ONLY,仅在启动参数(my.cnf)中设置,不支持SET GLOBAL动态修改。修改后需重启生效。
其他可调参数
参数 | 作用 | 代码默认值 | 动态生效 | 调优建议 |
| 所有Memtable上限。 | 集群内存 x 10% | 是 | 设置总量限制。 |
| pack_shift动态策略。 | DISABLED | 需重启 |
常见问题排查
场景一:集群内存持续增长
当发现集群内存持续增长且怀疑是IMCI元数据导致时,按以下步骤排查:
步骤1:查看IMCI总体内存分布。
步骤2:判断内存类型。如果占比高的MOD_TYPE是
PACKMETA_OBJ、NCI_BP、MASK_GROUP等,属于常驻元数据,进入步骤3。步骤3(常驻元数据占比高):调整
pack_shift增大到17或18,减少Pack数量。需重建索引生效。步骤4(验证效果):再次执行步骤1的SQL,确认目标MOD_TYPE的内存已下降。
步骤5(仍不足):如调整后内存仍然不足,需要升级集群规格。
场景二:集群因元数据内存不足OOM
诊断方法:
检查监控的内存使用率字段,如果内存使用率监控接近100%后,监控曲线割断后骤跌,大概率是OOM。
如果日志中出现IMCI相关的内存分配失败,说明当前规格的内存不足以加载所有元数据。
恢复方法:
方法A:升级到更大内存规格。
方法B:删除不必要的列存索引。
方法C:通过启动参数增大
pack_shift并设置imci_dynamic_pack_shift_policy=ENABLED,减少元数据量后重启。
场景三:预估IMCI元数据内存需求
方法一:直接查询当前常驻内存占用(推荐,最准确)
执行以下SQL排除LRU可控数据,查询当前集群IMCI常驻内存总量:
SELECT ROUND(SUM(ALLOC) / 1024 / 1024, 2) AS resident_meta_mb
FROM information_schema.IMCI_MEM_ALLOC
WHERE MOD_TYPE NOT IN
('NCI_BP_PAGE', 'LRU_PACK', 'PRUNER', 'BLOOM_FILTER', 'LRU_RB_BLOCK', 'FTS_DICT');方法二:通过Pack x 列总数估算per-pack-per-column元数据
-- 查询Pack x 列总数,乘以约406字节即可估算per-pack-per-column元数据
SELECT SUM(NUM_PACKS * NUM_COLS) AS total_pack_cols,
ROUND(SUM(NUM_PACKS * NUM_COLS) * 406 / 1024 / 1024, 2) AS per_pack_col_meta_mb
FROM information_schema.IMCI_INDEXES;手动估算公式参考(适用于数据尚未导入、需要提前规划的场景):
1. per-pack-per-column元数据 ≈ SUM(NUM_PACKS x NUM_COLS) x 406字节
2. Mask内存(极端上限):
默认LSM-mask模式 ≈ 每张表的总行数 x 8字节(单份seq_v向量)
传统PACK_MASK模式 ≈ 每张表的总行数 x 16字节(INS + DEL两份)
3. 总元数据 ≈ 1 + 2(实际Mask远低于极端值)Mask内存是极端上限(所有Pack全部分配了Mask),实际占用取决于写入活跃度。只读场景下,未被修改的Pack不会分配Mask,实际Mask内存会远低于此值。上述公式仅覆盖主要元数据,未计入PACKSTR_MIN_MAX(字符串列MinMax,变长)、NCI_BP(表多时常驻开销)等。推荐使用方法一的SQL查询实际值。
场景四:根据内存占用获取规格建议
以下SQL可根据当前集群IMCI常驻内存的实际占用,推算建议的集群内存规格(按2的幂次取整,预留约65%给IMCI外的系统开销,最低8 GB,最高512 GB):
-- 根据当前 IMCI 常驻内存,推算建议的实例内存规格(GB)
SELECT LEAST(
GREATEST(POWER(2, CEIL(LOG2(SUM(ALLOC) / 0.35 / 1024 / 1024 / 1024))),2),
512
) AS recommended_mem_gb
FROM information_schema.IMCI_MEM_ALLOC
WHERE MOD_TYPE NOT IN ('NCI_BP_PAGE', 'LRU_PACK', 'PRUNER', 'BLOOM_FILTER', 'LRU_RB_BLOCK', 'FTS_DICT');SQL说明:先减去5 GB基础系统开销,再除以0.35(即保留约65%给非IMCI元数据用途),再向上取2的幂次,最终限定在8~512 GB范围内。
升级与降配注意事项
操作 | 注意事项 |
降配(内存缩小) | 高风险。降配前必须通过估算公式验证元数据内存能否容纳在新规格的内存中。如果元数据常驻内存超过新规格的可用内存,降配后集群可能无法启动。建议先删除不必要的列存索引后再降配。 |
升级(内存扩大) | 安全操作。 |
数据量大幅增长 | 新导入大量数据后,元数据内存会同步增长。建议在数据导入前通过估算公式预判增长量,必要时提前升级规格。 |
删除列存索引 | 删除列存索引后,对应的PackMeta、Mask、ExtentInfo等元数据会被释放。如果某些表不需要IMCI加速,删除索引是降低内存最直接的方式。 |
降配前必须确认列存常驻元数据不超过新规格可用内存的40%,否则集群可能无法启动。可使用场景四:根据内存占用获取规格建议中的SQL获取规格建议。
参数生效方式速查
参数 | 生效方式 | 说明 |
| 仅对新建索引生效 | 需重建索引才能对存量生效。 |
| 需重启生效 | 控制recover时是否应用新pack_shift。 |
| READ_ONLY,仅启动时 | 修改后需重启。 |
总结
集群内存大头速查
关注优先级 | 模块 | 何时成为大头 | 降低手段 |
最高 |
| 行数 x 列数大(宽表 + 大数据量)。 | 增大 |
最高 |
| 表/NCI索引多且 | 降低 |
中 |
| 随PackMeta同步增长。 | 同上:增大 |
低 |
| 默认LSM-mask模式下通常非大头。 | 若传统模式 |
核心建议
定期监控:通过
IMCI_MEM_ALLOC查看各模块内存分布,重点关注PACKMETA_OBJ和NCI_BP。容量规划:
PACKMETA_OBJ与行数 x 列数成正比,NCI_BP与NCI索引数 xnci_lsm_bp_shard成正比,需据此预估并选择足够的集群规格。LRU缓存一般无需调整:通过
SHOW GLOBAL VARIABLES确认当前值后再决定是否调整,不建议随意缩减。pack_shift是降低常驻元数据的核心手段:增大后可等比例减少所有per-pack开销,但需重建索引。
降配前验证:必须确认列存常驻元数据不超过新规格可用内存的40%,否则集群可能无法启动。可使用场景四:根据内存占用获取规格建议中的SQL获取规格建议。