达梦数据库服务器(DMServer)在运行过程中需要频繁申请和释放小块内存。
若每次都调用操作系统的 malloc/free,会引发以下问题:
1)每次内存操作都要陷入操作系统内核,产生系统调用开销;
2)长期频繁分配释放容易在堆上形成内存碎片,导致总量充足却无法分配连续空间;
3)数据库无法追踪每块内存被哪个会话或哪条SQL占用,不利于问题定位。
因此,达梦在进程启动时预先向操作系统申请若干大块内存,由数据库自身负责内部划分和回收。绝大多数小块内存分配都在这些预申请的空间内完成,不再与操作系统交互。操作系统看到的仅是 dmserver 进程占用的常驻内存(RSS)。
关系结构如下:
操作系统物理内存 └── dmserver 进程 ├── 数据缓冲区 BUFFER / KEEP / RECYCLE / FAST │ (数据页;自由链 / LRU 链 / 脏链) ├── 共享内存池 MEMORY_POOL (实例启动时向 OS 申请) │ └── 排序块、哈希块等短生命周期小片内存 └── 运行时内存池 ├── SESSION 池 (会话建立时创建,断开时释放) └── VM 池 (语句执行期)
| 区域 | 典型参数 | 存什么 | 生命周期 |
|---|---|---|---|
| 数据缓冲区 | BUFFER / KEEP / RECYCLE / FAST_POOL_PAGES |
数据页、索引页 | 实例级,启动时格式化成分页 |
| 共享内存池 | MEMORY_POOL / MEMORY_TARGET / MEMORY_EXTENT_SIZE |
字典、SQL 计划缓存、日志缓冲、排序/哈希等短生命周期块 | 实例启动时向 OS 申请,可扩展/收缩 |
| 运行时内存池 | 会话池、虚拟机(VM)池 | 本会话的解析、执行器私有结构 | 会话建立时创建,会话结束时销毁 |
数据缓冲区存放从磁盘读入的数据页和索引页。查询读取数据时优先在此查找,未命中则从磁盘读取并缓存。修改数据时页标记为脏页,由检查点或淘汰机制刷回磁盘。
数据缓冲区的生命周期为实例级,随数据库启动分配,持续至实例关闭。管理方式是按固定页大小划分,通过自由链、LRU链、脏链进行管理。典型参数包括 BUFFER、KEEP、RECYCLE、FAST_POOL_PAGES。
共享内存池存储数据字典、SQL执行计划缓存、日志缓冲区,以及排序和哈希等操作的工作内存。其生命周期为实例启动时向操作系统申请初始空间,运行中可按需扩展或收缩。
典型参数包括 MEMORY_POOL(初始大小)、MEMORY_TARGET(收缩上限)、MEMORY_EXTENT_SIZE(单次扩展步长)。
共享内存池是供所有会话共享的工作区。当需要小块内存(如排序或哈希操作)时,从该池中切分。使用完毕后归还池中,供其他模块复用。池中空间不足时,按 MEMORY_EXTENT_SIZE 逐次扩展。当占用超过 MEMORY_TARGET 时,空闲部分会缩回目标值。
运行时内存池存放会话私有数据结构、执行器工作区、表达式计算临时空间。其生命周期为会话建立时创建,会话断开时释放。典型参数包括会话池和虚拟机池,由数据库自动管理。
运行时内存池属于会话私有资源。每个客户端连接建立后,数据库为该会话创建独立的运行时池,用于存放事务控制块、执行计划上下文等。SQL语句执行期间还会创建虚拟机池,语句结束后即释放。
数据缓冲区在实例启动时一次性向操作系统申请,实例关闭时归还。共享内存池在实例启动时申请初始块,运行中按需扩展,达到上限后空闲部分可收缩,但极少归还操作系统。运行时内存池在会话建立时向操作系统申请,会话断开后归还。
需要注意:BUFFER 不属于内存池,它是按固定页大小组织的数据缓存,与处理小块分配的共享内存池是两条独立的存储路径。
客户端连接 └─ 创建 SESSION 运行时池 ← 会话级,连接在就在 收到 SQL ├─ 解析 / 绑定 / 计划 │ └─ 字典缓冲、SQL 缓冲(共享池侧) └─ 执行 ├─ 创建 / 使用 VM 池 ← 本语句执行期 ├─ 扫表:数据页走 BUFFER ├─ ORDER BY / DISTINCT:排序区(短生命周期,多从共享池切) └─ HASH JOIN / HASH GROUP:哈希区(同上,超限则外存哈希) 会话断开 └─ SESSION / VM 池归还
排序区和哈希区是复杂查询中最常消耗内存的两个操作区域,两者均从共享内存池中获取工作内存,使用完毕后立即释放。
排序操作不仅出现在 ORDER BY 子句中,还出现在 GROUP BY 分组(无可用索引时)、DISTINCT 去重、UNION 合并去重、MERGE JOIN 前的有序化、创建索引时的键排序以及部分窗口函数等场景。
排序区的工作方式如下。当待排序数据量不超过 SORT_BUF_SIZE 时,在内存中完成排序,结束后释放内存。当待排序数据量超过 SORT_BUF_SIZE 时,分批写入临时表空间(走 RECYCLE 缓冲),最后进行多路归并,产生磁盘I/O。
关键参数包括 SORT_BUF_SIZE(单次排序操作可用的内存上限)、SORT_BUF_GLOBAL_SIZE(整个实例内所有排序操作可占用的内存总和上限)、SORT_BLK_SIZE(排序分片大小)以及 RECYCLE(临时表空间使用的缓冲区大小,影响外排序性能)。
哈希连接用于等值连接场景,如 t1.c1 = t2.c1,当连接列缺少索引或优化器判断哈希比嵌套循环更优时选用。
哈希区的工作方式如下。数据库选择较小的一侧作为Build表,在内存中构建哈希表,再扫描另一侧(Probe表)逐行探测匹配。若Build表数据量不超过 HJ_BUF_SIZE,全程在内存完成。若Build表数据量超过 HJ_BUF_SIZE,两表按哈希函数分区写入临时存储,再逐分区执行内存哈希,称为外存哈希,产生磁盘I/O。
关键参数包括 HJ_BUF_SIZE(单次哈希连接可用的内存上限)、HJ_BUF_GLOBAL_SIZE(实例内所有哈希连接可占用的内存总和上限)、HJ_BLK_SIZE(哈希操作每次分配的块大小),以及 HAGR_BUF_SIZE(哈希分组、DISTINCT等聚合操作的内存上限)和 HAGR_BUF_GLOBAL_SIZE(实例内所有哈希聚合操作的内存总和上限)。
哈希分组(HAGR_)与哈希连接(HJ_)使用不同参数。一条SQL可能同时触发排序和哈希,例如同时包含 GROUP BY 和 ORDER BY 的查询。
ORDER BY / GROUP BY / DISTINCT │ ├─ 工作集 ≤ SORT_BUF_SIZE → 内存内排序 → 得到有序结果 └─ 工作集 > SORT_BUF_SIZE → 写入临时段(RECYCLE)→ 多路归并 等值 HASH JOIN │ ├─ Build 侧 ≤ HJ_BUF_SIZE → 内存中建哈希表并探测 └─ Build 侧 > HJ_BUF_SIZE → 分区写入临时段 → 逐分区再哈希
MEMORY_POOL 设置过小会导致运行中频繁向操作系统扩展,增加系统调用开销,N_EXTEND_EXCLUSIVE 计数持续增长。设置过大会导致实例启动占用过高,空闲空间浪费,并挤压其他内存区域。
BUFFER 设置过小会导致命中率低,频繁淘汰数据页,物理读增加,FREE 接近0,淘汰计数持续上升。设置过大会导致实例启动失败或触发操作系统Swap,脏页增多,检查点刷盘造成IO尖峰,同时挤压排序和哈希可用空间。
SORT_BUF_SIZE 设置过小会导致内排序转外排序,临时表空间膨胀,ORDER BY 和建索引操作显著变慢。设置过大时,多个会话并发,总消耗约为会话数乘以 SORT_BUF_SIZE,容易耗尽内存。
HJ_BUF_SIZE 设置过小会导致哈希连接转为外存哈希,等值大连接变慢。设置过大时,单条SQL占用大量内存,多并发时受全局上限限制,且与 BUFFER 争抢物理内存。
RECYCLE 设置过小会导致外排序或外哈希即使算法正确,仍受限于磁盘速度,整体缓慢。设置过大时,临时场景不占那么多空间造成浪费。
需要说明:SORT_BUF_SIZE 和 HJ_BUF_SIZE 是会话级参数,单会话设置过大尚可,但需乘以并发会话数估算实际内存消耗。SORT_BUF_GLOBAL_SIZE 和 HJ_BUF_GLOBAL_SIZE 是实例级上限,用于防止所有会话的总消耗失控。
SELECT PARA_NAME, PARA_VALUE
FROM V$DM_INI
WHERE PARA_NAME IN (
'BUFFER',
'MEMORY_POOL',
'SORT_BUF_SIZE',
'HJ_BUF_SIZE'
);
在窗口执行下列sql语句
SELECT SESS_ID, USER_NAME, STATE, THRD_ID
FROM V$SESSIONS
ORDER BY SESS_ID;
在新连接的窗口里再执行上面那句。用户多出一行 SESS_ID。
比内存池:
SELECT COUNT(*) AS POOL_CNT FROM V$MEM_POOL;
SELECT NAME, COUNT(*) AS CNT
FROM V$MEM_POOL
GROUP BY NAME
ORDER BY CNT DESC;
CREATE TABLE MEM_FACT (
ID INT NOT NULL,
DEPT INT NOT NULL,
AMOUNT DECIMAL(18,2),
PAD VARCHAR(180)
);
INSERT INTO MEM_FACT
SELECT LEVEL,
MOD(LEVEL, 50) + 1,
MOD(LEVEL, 1000) * 0.37,
RPAD('X', 180, 'Y')
FROM DUAL
CONNECT BY LEVEL <= 50000;
COMMIT;
SELECT COUNT(*) FROM MEM_FACT;
让服务器把行按 ID 排完再写入另一张表,避免 Manager 把行画在屏幕上。
先热身一次(耗时丢掉,只为把数据打进 BUFFER):
SELECT COUNT(*), SUM(ID) FROM MEM_FACT;
再准备结果表,同一窗口继续:
CREATE TABLE MEM_OUT (ID INT, PAD VARCHAR(180));
SP_SET_PARA_VALUE(1, 'SORT_BUF_SIZE', 1);
TRUNCATE TABLE MEM_OUT;
INSERT INTO MEM_OUT SELECT ID, PAD FROM MEM_FACT ORDER BY ID DESC;
COMMIT;
看这条 INSERT 的耗时。同一窗口再:
SP_SET_PARA_VALUE(1, 'SORT_BUF_SIZE', 32);
TRUNCATE TABLE MEM_OUT;
INSERT INTO MEM_OUT SELECT ID, PAD FROM MEM_FACT ORDER BY ID DESC;
COMMIT;
每档连跑两遍,只记第二遍(第一遍可能还在填缓冲)。比较的是 INSERT ... ORDER BY 的耗时。
最后把排序缓冲改回第 1 步的原值:
SP_SET_PARA_VALUE(1, 'SORT_BUF_SIZE', 2);
维表很小的 JOIN 往往看不出差异,所以用自连接把 Build 侧做大。
SP_SET_PARA_VALUE(1, 'HJ_BUF_SIZE', 2);
SELECT /*+ USE_HASH(A, B) */ COUNT(*)
FROM MEM_FACT A, MEM_FACT B
WHERE A.ID = B.ID
AND A.DEPT = 1;
看耗时。同一窗口再:
SP_SET_PARA_VALUE(1, 'HJ_BUF_SIZE', 64);
SELECT /*+ USE_HASH(A, B) */ COUNT(*)
FROM MEM_FACT A, MEM_FACT B
WHERE A.ID = B.ID
AND A.DEPT = 1;
/*+ USE_HASH(A, B) */ 是提示优化器走哈希连接。两次差不多,说明本机数据量未打满 HJ_BUF,差异不明显。
做完清理:
DROP TABLE MEM_FACT;
1)先定位瓶颈类型:使用V$BUFFERPOOL查看缓冲区命中率与淘汰计数;使用V$MEM_POOL查看内存池扩展次数;使用EXPLAIN查看执行计划中的排序和哈希操作。确定问题是缓冲区不足、排序外溢还是哈希外溢,再调整对应参数。
2)数据缓冲区(BUFFER):按数据热度和物理内存总量规划,一般设为物理内存的60%~80%。若数据文件总大小小于此比例,按实际数据量设置即可。修改后需重启实例生效,用命中率验证效果。
3)排序内存(SORT_BUF_SIZE):按单条SQL的排序数据量估算,乘以最大并发会话数,再受SORT_BUF_GLOBAL_SIZE约束。建索引等操作可临时调大,完成后恢复。
4)哈希内存(HJ_BUF_SIZE):按Build表大小估算,乘以最大并发哈希连接数,再受HJ_BUF_GLOBAL_SIZE约束。仅对等值大连接有效,嵌套循环连接不受此参数影响。
5)共享内存池(MEMORY_POOL):通过N_EXTEND_EXCLUSIVE观察扩展频率,若长期大于0再考虑增大初始值。同时设置MEMORY_TARGET作为上限,防止池无限扩张。
文章
阅读量
获赞
