尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

达梦数据库内存管理

达梦数据库内存管理 达梦内存管理内存池、缓冲区与排序区核心论点达梦DM8并不把每一次小内存申请都交给操作系统的malloc/free而是用自管内存把「数据页缓存、共享内存池、会话运行时内存」拆开管理。BUFFER、SORT_BUF_SIZE、HJ_BUF_SIZE 设小了会把复杂查询赶出内存、打到临时表空间设大了会在多会话下把物理内存乘爆甚至拖垮操作系统。本文按「为什么自管 → 两类内存池差在哪 → 参数过大过小会怎样 → 排序区/哈希区在复杂 SQL 里怎么工作 → 完整上机」展开SQL 均可在单实例上复现。前言本文所要解决的问题有1共享内存池和运行时内存池谁在实例启动时向操作系统要内存谁在会话起来之后才出现2BUFFER、SORT_BUF_SIZE设得太小和太大分别会在哪一层出问题3一条带ORDER BY和等值JOIN的 SQL扫描、排序、哈希分别消耗哪一块内存1. 为什么达梦要自己管内存而不是每次调用操作系统数据库是典型的「高频申请、高频释放」软件解析 SQL、建执行计划、排序、哈希连接、缓存数据页每秒钟可能发生成千上万次小块内存进出。如果每一次都走操作系统要陷入内核发生系统调用和可能的线程切换堆上容易产生碎片长时间运行后「总量够、却申请不到连续块」数据库自己看不到「这块内存是哪个会话、哪条 SQL 拿走的」泄露和越界很难定位。达梦官方文档《DM 内存结构》中「数据库管理系统是一种对内存申请和释放操作频率很高的软件如果每次对内存的使用都使用操作系统函数来申请和释放效率会比较低加入自己的内存管理是 DBMS 系统所必须的。」——出处DM 内存结构达梦产品手册因此 DMServer 作为单进程、多线程的共享服务器启动时先向操作系统要几块「大池子」之后绝大多数小分配都在池内完成用完归还池而不是归还操作系统。操作系统看到的主要是dmserver进程的 RSS池内部谁占用、有没有溢出要靠V$MEM_POOL、V$BUFFERPOOL来看。关系结构操作系统物理内存 └── 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池本会话的解析、执行器私有结构会话建立时创建会话结束时销毁BUFFER不是内存池。它是按数据页切好的高速缓存用自由链 / LRU 链 / 脏链管理。内存池解决的是「小块分配不要每次找 OS」。把这两件事当成同一个旋钮是调参里最常见的误区。2. 共享内存池 与 运行时内存池2.1 共享内存池共享内存池由MEMORY_POOL指定初始大小。实例启动时向操作系统申请这一大片。之后需要小片内存从池里切用完还回池供其他模块复用池不够按MEMORY_EXTENT_SIZE扩展MEMORY_TARGET是扩展后的收缩水位超过该值后空闲时尽量缩回目标大小。高并发时把MEMORY_POOL适当调大是为了减少运行期再向操作系统伸手。新人培训课件《DM7 性能诊断与优化》里的建议逻辑是公共内存池过小会频繁向 OS 申请拉低效率。相关参数还可以看到MEMORY_N_POOLS/N_MEM_POOLS把公共池切成多片降低并发分配时的锁冲突。并发会话多时片数过少会出现「内存总量还够但分配排队」的假象。2.2 运行时内存池除共享池外功能模块还会持有自己的运行时池。对应用最有体感的是会话内存池SESSION客户端连上来、服务器为该会话准备私有环境时创建。事务控制块、非特殊操作符的临时结构多从这里出。虚拟机内存池VIRTUAL MACHINE / VM语句真正执行时创建。执行器、表达式计算等大量工作内存来自这里。它们的共同特点是随使用产生随会话或语句结束释放。这些运行时池会从操作系统申请一片作为本模块自己的池来用。还可以这样理解分工生命周期与整个会话绑定的会话池、VM 池→ 独立池会话之间互不抢同一把分配锁这正是连接池「会话要保持」的底层原因之一生命周期只覆盖某个操作符的排序块、哈希块→ 往往不再单独向 OS 再开一个池而是从共享池里切用完立刻还。所以「运行时内存池 会话动态申请」这句话是对的但要补半句动态申请的对象优先是达梦自己的池而不是每次直接malloc。只有池的初始值用尽、尚未顶到 target 时才会再向操作系统要超过 target 之后更倾向于回到共享池避免和 OS 频繁交互。2.3 一条 SQL 实际会碰几块池简化时间线客户端连接 └─ 创建 SESSION 运行时池 ← 会话级连接在就在 收到 SQL ├─ 解析 / 绑定 / 计划 │ └─ 字典缓冲、SQL 缓冲共享池侧 └─ 执行 ├─ 创建 / 使用 VM 池 ← 本语句执行期 ├─ 扫表数据页走 BUFFER ├─ ORDER BY / DISTINCT排序区短生命周期多从共享池切 └─ HASH JOIN / HASH GROUP哈希区同上超限则外存哈希 会话断开 └─ SESSION / VM 池归还用V$MEM_POOL时会同时看到名字像共享池的条目以及大量带会话线程号的运行时池。把CREATOR和V$SESSIONS.THRD_ID关联就能回答「哪条会话把内存抬起来了」。3. BUFFER、SORT_BUF_SIZE、哈希参数太小和太大分别怎样过小、过大的对照见本节各小节和第 3.4 表。是否真的外溢、是否真的在淘汰只能看第 5 节里V$BUFFERPOOL、RECYCLE、SET TIMING ON的上机截图。3.1 BUFFER数据页高速缓存启动时按BUFFER向 OS 申请连续内存按页大小切开挂到自由链。读数据先找缓冲没有再读盘并放入 LRU改数据页变脏进脏链由检查点/淘汰刷盘。设太小自由链很快耗尽LRU 不停淘汰V$BUFFERPOOL里FREE接近 0N_DISCARD64或淘汰计数持续上涨缓冲命中率低iostat的%util/await升高SQL 看起来「优化器没问题但就是慢」。设太大实例启动直接失败或启动后把 OS 可用内存吃光Linux 开始 Swapsi/so不为 0此时再快的缓冲也比不过被换到磁盘上的「假内存」挤压共享池、排序区、哈希区复杂查询反而更爱打临时表脏页总量变大检查点刷盘更猛出现周期性 IO 尖峰。经验范围不是公式单机 OLTP 常把BUFFER放到物理内存的 60%80%且MAX_BUFFER建议与BUFFER对齐避免无节制动态扩张。数据量比内存小可以按数据量来没必要为了「看起来大方」把缓冲开到远超数据文件。BUFFER_POOLS是缓冲区分片数并发高时用质数如 47、101降低缓冲内部闩锁冲突。它不增加总容量只改变并发结构。RECYCLE是临时表空间用的缓冲。排序外溢、哈希外溢、WITH物化、临时表都会打到这里。只调SORT_BUF_SIZE却把RECYCLE留在默认几十 MB外排序依然会很痛。3.2 SORT_BUF_SIZE单线程排序上限排序区不是启动时划死的一块「排序专用大数组」而是每次排序先申请、排完就释放。SORT_BUF_SIZE约束的是**单个排序操作常见口径单个线程**能用的内存上限。还有一组配套参数名称以你版本V$PARAMETER为准参数作用SORT_BUF_SIZE单次/单线程排序内存上限SORT_BUF_GLOBAL_SIZE实例内排序内存总和上限SORT_BLK_SIZE排序分片大小SORT_FLAG0 整片排序1 大内存分片排序一般不建议长期打开设太小内排序变外排序排不下的批次写入临时段再多路归并RECYCLE和临时表空间上涨磁盘排序比内存排序慢一个数量级很常见建索引、ORDER BY大结果集、GROUP BY/DISTINCT都会受影响。设太大它往往是会话级可改参数。100 个会话同时排序理论峰值接近100 × SORT_BUF_SIZE再和SORT_BUF_GLOBAL_SIZE去争共享池被排序块占满其他会话解析、哈希一起挨饿连接池场景下「测环境单会话很快、生产一高峰就 OOM」多半是这个乘法没算。手册对默认值的建议随版本有 2MB / 20MB 等差异以当前实例为准不要背死数字。建索引可以会话内临时调大作业跑完改回去。3.3 哈希区HJ_BUF_* 与 HAGR_BUF_*哈希连接消耗内存因为要选较小的一侧做 Build在内存里建哈希表再探测另一侧。达梦把哈希缓冲称作虚拟缓冲并不是预先划死一块「哈希专用物理内存」而是按数据量估算能放下就在内存池里做哈希放不下就走外存哈希分区写临时段再逐分区哈希。官方手册原文「DM8 提供了为哈希连接而设定的缓冲区不过该缓冲区是个虚拟缓冲区。……如果计算出的数据量大小超过了哈希缓冲区的大小则使用 DM8 创新的外存哈希方式如果没有超过哈希缓冲区的大小实际上还是使用内存池来进行哈希操作。」——出处同上《DM 内存结构》3.4 节参数含义HJ_BUF_SIZE单次哈希连接可用内存HJ_BUF_GLOBAL_SIZE实例内哈希连接内存总和HJ_BLK_SIZE哈希操作符每次分配块大小HAGR_BUF_SIZE/HAGR_BUF_GLOBAL_SIZE哈希分组、DISTINCT、集合运算、分析函数等HAGR_HASH_SIZE聚集时哈希桶个数HJ_BUF_SIZE 太小Build 表估不准或真实更大反复外存哈希等值连接没有索引时尤其明显。HJ_BUF_SIZE 太大单条 SQL 吃得很爽多条并发 HASH JOIN 会顶满HJ_BUF_GLOBAL_SIZE再和 BUFFER 抢物理内存。HJ_BUF_GLOBAL_SIZE 太大看起来「哈希很宽裕」实际是从整机内存里挖走一块BUFFER 命中率下降OLTP 与分析混跑时最容易中招。嵌套循环靠索引反复探测几乎不吃哈希区哈希连接吃内存、换的是「没索引也能做大结果等值连接」。调哈希参数之前先看执行计划里到底是NEST LOOP还是HASH JOIN否则会调错对象。3.4 一张表收口参数过小过大MEMORY_POOL频繁向 OS 扩展N_EXTEND_EXCLUSIVE长期 0启动占用高空闲也浪费可能挤 BUFFERBUFFER命中率低、淘汰多、物理 IO 高Swap、启动失败、检查点 IO 风暴、挤占排序/哈希SORT_BUF_SIZE外排序、临时表空间涨、ORDER BY/建索引慢会话数一乘就爆挤共享池HJ_BUF_SIZE外存哈希、等值大连接变慢单 SQL 吃内存并发哈希互相排队RECYCLE外排序/外哈希即使「算法对了」也被磁盘拖死临时场景不占那么多时浪费4. 复杂查询里排序区和哈希区到底在干什么ORDER BY / GROUP BY / DISTINCT │ ├─ 工作集 ≤ SORT_BUF_SIZE → 内存内排序 → 得到有序结果 └─ 工作集 SORT_BUF_SIZE → 写入临时段RECYCLE→ 多路归并 等值 HASH JOIN │ ├─ Build 侧 ≤ HJ_BUF_SIZE → 内存中建哈希表并探测 └─ Build 侧 HJ_BUF_SIZE → 分区写入临时段 → 逐分区再哈希上面只是机制你的库走的是哪一条以实验 5、实验 6 的EXPLAIN和耗时截图为准。4.1 排序区ORDER BY 只是入口会用到排序的远不止ORDER BYORDER BY/GROUP BY无合适索引时DISTINCT、UNION去重MERGE JOIN前的有序化创建索引时的键排序部分窗口函数内排序数据量 ≤SORT_BUF_SIZE再受全局上限约束全程内存完成结束即释放。外排序装不下就分批写临时段走临时表空间缓冲落在RECYCLE再归并。结果仍然正确代价是 IO 和 CPU 双涨。所以看到「这条 SQL 的计划并不差但一加 ORDER BY 就慢」优先看能不能用索引避免排序避免不了时单次排序量是否远大于SORT_BUF_SIZE临时表空间和RECYCLE是否已经成为第二瓶颈。4.2 哈希区等值连接的「用内存换随机 IO」哈希连接典型条件等值连接t1.c1 t2.c1非等值、BETWEEN通常不会选它连接列缺索引或优化器认为建哈希表比嵌套循环反复探索引更划算选较小的 row source 做 Build 表。内存足够Build 侧在内存建哈希表Probe 侧逐行探测CPU 密集、IO 少。内存不够两表按哈希函数分区写入临时存储再对每个分区做内存哈希——这就是外存哈希。分区次数越多越接近「自己实现了一遍磁盘 hash join」。HAGR_*覆盖的是另一类哈希GROUP BY哈希聚合、DISTINCT、集合运算等。一条 SQL 完全可能同时占用排序区和哈希区例如SELECTdept_id,COUNT(*),MAX(amount)FROMbig_fact fJOINdim_dept dONf.dept_idd.dept_id-- 可能 HASH JOIN → HJ_BUF_*GROUPBYdept_id-- 可能 HASH GROUP → HAGR_BUF_*ORDERBY2DESC;-- 排序区 → SORT_BUF_*扫描这些表的数据页仍然走BUFFER。因此「复杂查询慢」要拆成三问页缓命中够不够、哈希是否外溢、排序是否外溢。只加 BUFFER解决不了后两问。4.3 和达梦其他产品的关系不只单实例数据守护DataWatch备库同样有自己的BUFFER与内存池。备库内存往往小于主库若按主库原样拷贝dm.ini备库可能起不来或在重演 REDO 时把 OS 打满。读写分离 / DSC每个节点一份缓冲不存在「集群共享一块 BUFFER」。SQL 打到哪一节点排序/哈希就在那一节点的运行时内存里发生。DMETL / 数据迁移大批量装数、排序清洗时作业进程和数据库会话会叠加内存。ETL 主机和数据库主机的 BUFFER 规划要分开算不要默认「都在一台机器上BUFFER 开到 80%」。5. 上机实验第 1 步看清四个参数证明 BUFFER 和内存池不是一回事SELECTPARA_NAME,PARA_VALUEFROMV$DM_INIWHEREPARA_NAMEIN(BUFFER,MEMORY_POOL,SORT_BUF_SIZE,HJ_BUF_SIZE);第 2 步再连一次库证明运行时池跟着会话走在窗口执行下列sql语句SELECTSESS_ID,USER_NAME,STATE,THRD_IDFROMV$SESSIONSORDERBYSESS_ID;在新连接的窗口里再执行上面那句。用户多出一行SESS_ID。比内存池SELECTCOUNT(*)ASPOOL_CNTFROMV$MEM_POOL;SELECTNAME,COUNT(*)ASCNTFROMV$MEM_POOLGROUPBYNAMEORDERBYCNTDESC;第 3 步造表 对比排序内存CREATETABLEMEM_FACT(IDINTNOTNULL,DEPTINTNOTNULL,AMOUNTDECIMAL(18,2),PADVARCHAR(180));INSERTINTOMEM_FACTSELECTLEVEL,MOD(LEVEL,50)1,MOD(LEVEL,1000)*0.37,RPAD(X,180,Y)FROMDUALCONNECTBYLEVEL50000;COMMIT;SELECTCOUNT(*)FROMMEM_FACT;让服务器把行按ID排完再写入另一张表避免 Manager 把行画在屏幕上。先热身一次耗时丢掉只为把数据打进 BUFFERSELECTCOUNT(*),SUM(ID)FROMMEM_FACT;再准备结果表同一窗口继续CREATETABLEMEM_OUT(IDINT,PADVARCHAR(180));SP_SET_PARA_VALUE(1,SORT_BUF_SIZE,1);TRUNCATETABLEMEM_OUT;INSERTINTOMEM_OUTSELECTID,PADFROMMEM_FACTORDERBYIDDESC;COMMIT;看这条INSERT的耗时。同一窗口再SP_SET_PARA_VALUE(1,SORT_BUF_SIZE,32);TRUNCATETABLEMEM_OUT;INSERTINTOMEM_OUTSELECTID,PADFROMMEM_FACTORDERBYIDDESC;COMMIT;每档连跑两遍只记第二遍第一遍可能还在填缓冲。比较的是INSERT ... ORDER BY的耗时。最后把排序缓冲改回第 1 步的原值SP_SET_PARA_VALUE(1,SORT_BUF_SIZE,2);第 4 步对比哈希内存大表自己连自己维表很小的 JOIN 往往看不出差异所以用自连接把 Build 侧做大。SP_SET_PARA_VALUE(1,HJ_BUF_SIZE,2);SELECT/* USE_HASH(A, B) */COUNT(*)FROMMEM_FACT A,MEM_FACT BWHEREA.IDB.IDANDA.DEPT1;看耗时。同一窗口再SP_SET_PARA_VALUE(1,HJ_BUF_SIZE,64);SELECT/* USE_HASH(A, B) */COUNT(*)FROMMEM_FACT A,MEM_FACT BWHEREA.IDB.IDANDA.DEPT1;/* USE_HASH(A, B) */是提示优化器走哈希连接。两次差不多说明本机数据量未打满 HJ_BUF差异不明显。做完清理DROPTABLEMEM_FACT;6. 调参时的一组可执行原则先分清三种慢页缓命中不够BUFFER、排序外溢SORT_* RECYCLE、哈希外溢HJ_* / HAGR_*。用EXPLAINV$BUFFERPOOLV$MEM_POOL三件套不要凭感觉加内存。BUFFER 按数据热度和物理内存一起算不是按「参数表里谁默认大」谁就该最大。改完必须重启用淘汰计数而不是「感觉」验收。SORT_BUF_SIZE 按单条 SQL 的排序工作集估再乘并发。连接池里的每个活连接都可能同时排序。哈希参数看 Build 侧大小。小维表哈希吃不满HJ_BUF_SIZE大表无索引等值连接才需要认真调并给HJ_BUF_GLOBAL_SIZE留并发余量。共享池负责「少找 OS」。N_EXTEND_EXCLUSIVE长期大于 0再考虑加MEMORY_POOL/MEMORY_TARGET而不是先把 BUFFER 再加 20GB。守护、集群、ETL 节点各自算一份内存账。达梦产品线里每个进程都有自己的缓冲与池拷贝一份主库dm.ini到小规格备库是现场常见事故。7.总结达梦把内存管起来不是为了多几个参数名字而是为了在单进程多线程模型下把「数据页缓存」和「小块工作内存」从操作系统的通用堆里拆出来。共享内存池在启动时要一大块换的是后续少做系统调用运行时内存池跟着会话走换的是会话之间少抢同一把分配锁也换来连接池必须被正确关闭、否则池不释放。排序区和哈希区则是复杂查询里最容易被忽略的两条旁路它们在够用时几乎无感不够用时不会立刻报错而是改走临时表空间——于是故障表现常常是「磁盘很忙、SQL 计划却看不出明显错误」。参考引用已在正文用双引号标出达梦产品手册《DM 内存结构》https://eco.dameng.com/document/dm/zh-cn/pm/memory-structure.html达梦培训材料《DM7 性能诊断与优化》实例内存参数表MEMORY_POOL、BUFFER、SORT_BUF_SIZE、HJ_BUF_*动态视图V$PARAMETER/V$DM_INI、V$MEM_POOL、V$BUFFERPOOL、V$SESSIONS、V$SQL_STAT以所装版本系统视图说明为准
返回列表