ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SAP HANA内存溢出告警排查与SQL优化实战指南

SAP HANA内存溢出告警排查与SQL优化实战指南 凌晨两点多手机连着震了好几下告警邮件标题写着HANA Out of Memory Warning。说实话看到这个标题心里咯噔一下。打开系统一看HANA服务的内存使用率已经飙到93%几个关键会话被挂起业务方反馈某个报表一直转圈出不来。查了一圈罪魁祸首是一条通过SAP系统下发的SQL语句单条查询就把HANA的statement memory limit给顶爆了。这个场景在SAP S/4HANA和BW on HANA的环境里越来越常见。很多人一听到HANA内存溢出就觉得是硬件资源不够实际上大部分时候问题出在SQL语句本身或者出在从ABAP层生成的数据库查询上。这篇文章不聊虚的把我定位问题、临时救火、彻底优化的完整过程拆开揉碎讲清楚包括HANA内存机制的几个关键概念、什么样语句最容易踩雷、怎么从SAP侧抓到肇事SQL、以及后期怎么通过参数和监控手段防止再犯。不管是BASIS、ABAP开发还是HANA数据库管理员应该都能从中找到自己能用的东西。1. HANA为什么会因为一条SQL报内存溢出——先搞清楚它和传统数据库的差异1.1 列式存储和分析型场景的绝配与隐患HANA和传统的SQL Server或者Oracle在架构上的最大区别就是它把数据主要放在内存里而且默认采用列式存储。列式存储对分析型查询有天然优势只需要从内存里读取查询涉及的那些列不用像行式存储那样把整行数据都扫一遍。比如一张表有80个字段、5亿行数据你只查其中3个字段列式存储可能只需要读150MB的数据块而行式存储可能要读几十个GB。这个设计带来了一个很直接的结果HANA的查询速度上限很高但内存消耗也远高于传统数据库。因为数据常驻内存执行计划生成的中间结果集也尽量放内存连临时表都优先落内存。传统数据库可以把排序、hash join的溢数据写到磁盘临时文件HANA虽然也有磁盘卸载机制但在高并发、复杂查询的场景下内存压力会快速累积。一条SQL导致内存溢出警告本质上是这条SQL的执行过程超出了HANA为该会话或整个服务分配的内存上限。HANA在OOM之前会先触发警告这个警告阈值不是内存条物理用满而是配置参数里的allocation limit接近了或者是单个statement的内存消耗超过了statement_memory_limit。1.2 一条SQL的内存消耗路径执行计划、中间结果集与内存表很多人有一个误解觉得SQL消耗内存主要就是把表加载进内存。其实在HANA里表数据如果已经常驻内存这部分开销在查询前就已经存在。一条SQL真正新增的内存消耗主要来自三个地方第一是执行计划本身的构建和优化空间。HANA的优化器在做cost-based optimization时会为每个算子估算内存开销当遇到复杂关联或子查询时优化器可能生成多个候选计划这部分计算资源本身就占用内存。第二是中间结果集这是最大的内存消耗源。两个大表做hash joinHANA会在内存中为其中一个表构建hash表如果ON条件选择性很差这个hash表可能非常巨大。排序操作无论是ORDER BY还是DISTINCT都需要在内存里维护排序结构。GROUP BY的分组聚合、窗口函数的partition缓冲全都是内存大户。第三是内存表Row Store的临时对象。SQL里如果涉及会话级临时表或者中间表落地这些存储也是优先走内存的。我见过一个典型例子一条SQL把两张各2亿行的表做FULL OUTER JOIN既没有过滤条件关联键还有大量空值。优化器估算内存需要60GB但会话的statement_memory_limit只有15GB于是查询直接被终止持续产生OOM警告。1.3 内存的三个层次物理内存、Services内存与Statement内存要理解HANA的内存告警必须分清楚三个层次**物理内存Physical Memory**是指服务器实际配置的内存条容量。HANA要求物理内存的一半以上留给自身使用还要加上文件系统缓存等开销。如果物理内存本身不足光靠SQL优化是救不了的。Services内存是指HANA各个服务进程IndexServer、NameServer等从操作系统申请的内存总和。每个服务都有自己独立的分配上限IndexServer是最大头。SAP建议在HANA的global.ini里设置allocationlimit比如allocationlimit 85表示IndexServer最多使用物理内存的85%超过这个比例就会触发内存警告或OOM。Statement内存是单条SQL语句执行过程中可以消耗的内存上限。HANA在indexserver.ini里有参数statement_memory_limit控制单条语句的峰值内存默认单位是MB。比如设置为15000表示单条SQL最多消耗约15GB内存。除此之外还有max_statement_memory_limt参数作为硬上限防止某条语句突破软限制后失控。这三个层次叠加起来就是你看到的内存告警的不同形态。物理内存不足告警会出现在操作系统级别Services内存超限IndexServer会开始卸载列存数据或者拒绝新查询Statement内存超了单条SQL直接被kill同时会话里收到Out of memory at statement错误。所以遇到HANA内存溢出警告第一步不是盲目加内存而是先定位到底是哪个层次出了问题。如果每次告警都伴随着某条固定SQL那八成是Statement层次的问题优化语句比扩容更有效。2. 实时抓现行从告警到定位肇事SQL的完整排查链路2.1 告警邮件的解读与初步判断SAP HANA的默认内存告警通过HANA Cockpit或Solution Manager发出。邮件标题通常是Memory Allocation Failed或者Out of Memory Warning正文会包含主机名、服务名、当前内存使用率、分配限制等信息。收到告警后第一件事是先登录HANA Studio或DBA Cockpit查看Landscape页面里IndexServer的内存曲线确认是持续高位还是瞬时尖峰。如果是瞬时尖峰大概率是某条SQL或某个ABAP报表触发的如果是持续高位可能是有内存泄漏或者配置不合理。然后再看Memory Allocation页面把Used Memory和Allocated Memory分开观察。HANA有一个特点IndexServer一旦从操作系统申请了内存不会立即归还而是保留在自己的内存池里复用。所以即使SQL执行完了allocated memory可能仍然很高但这不代表当前业务还在消耗内存。真正需要担心的是Used Memory持续增长且不回落。2.2 用系统视图锁定活跃SQL抓到肇事SQL最快的方法是查询HANA的实时监控视图。最常用的两个是M_ACTIVE_STATEMENTS和M_ACTIVE_SESSION。M_ACTIVE_STATEMENTS能列出所有正在执行的SQL语句包含以下关键字段STATEMENT_ID语句的唯一标识HOST、PORT指向IndexServer的位置CONNECTION_ID会话连接USER_NAME执行用户SAP应用用户通常为SAPSR3或SYSTEMSTATEMENT_STRING具体的SQL文本MEMORY_SIZE该语句当前消耗的内存量DURATION已执行时长THREAD_COUNT、BLOCKED_BY是否被阻塞执行以下SQL就能实时抓取内存消耗最高的活跃语句SELECT TOP 20 STATEMENT_ID, USER_NAME, MEMORY_SIZE/1024/1024 AS memory_mb, DURATION/1000000 AS duration_sec, LEFT(STATEMENT_STRING, 500) AS sql_text FROM M_ACTIVE_STATEMENTS ORDER BY MEMORY_SIZE DESC;如果告警时这条语句还在执行看到的就是它。如果已经因为超限被kill了M_ACTIVE_STATEMENTS里就查不到了。这时候需要去HANA的trace文件里捞indexserver_alert_host.trc里会记录OOM事件的语句摘要或者在SAP层面用ST22查ABAP dump里面会关联到具体的数据库SQL。2.3 从SAP应用层往回反查ST05与事务码追踪很多时候HANA管理员能抓到一条巨长的SQL文本但看不出它对应用户的哪个操作。几百行的SQLFROM子句里全是一串/BIC/AZSD_CUBE或者CDPOS这种数据库表名业务人员只知道自己点了一个报表根本说不清底层逻辑。这时候需要借助SAP的SQL追踪工具。事务码ST05配合STAD事务码STAD查看已执行的ABAP统计可以做到反向定位。在ST05里选择SQL Trace打开追踪后让用户重新执行一次报表操作关闭追踪后就能看到ABAP层面发往数据库的全部SQL列表。与HANA监控视图里记录的SQL文本做比对找出对应的时间窗口、对应的SQL语句文本再通过SE30ABAP运行时分析进一步定位是哪个程序、哪个Form/Function Module发出的这条SQL。有个经验值得分享SAP系统发起的SQL大多不是手写的原生SQL而是Open SQL通过数据库适配层转换生成的。你在HANA里看到的SQL往往比ABAP代码里写的复杂得多带了很多隐式转换、UNION ALL、子查询等。所以ABAP开发看到HANA报出的SQL不要觉得陌生这就是ABAP Open SQL经过优化器重写后的样子。我在实际项目中遇到过一条SQLABAP代码里只是一个简单的SELECT * FROM ZTABLE WHERE MATNR lv_matnr但HANA执行计划里却出现了两次全表扫描。原因就是ZTABLE没有统计信息更新优化器估算行数严重失真选择了错误的执行计划。2.4 日志证据在HANA Trace和SAP Dump中寻找OOM细节如果问题反复出现建议直接查HANA的diagnostic files。目录通常在/usr/sap/SID/HDBinstance/trace下重点看indexserver_alert_host.trc。这里会记录OOM前的内存视图包含OOM发生时间点触发OOM的具体服务进程当时的Used Memory和Allocation Limit消耗内存最多的SQL语句通常是SQL文本的摘要内存消耗排名前十的session ID如果SQL文本被截断可以结合trace里的Statement ID去查M_ACTIVE_STATEMENTS的历史采样。前提是你有部署负载分析工具或者人工定期采样否则只能事后听用户复现。SAP应用层的ST22 dump同样重要。ABAP短转储里的Database error部分会带上HANA返回的错误码和消息文本。常见的错误消息是SQL Error 416: Out of memory at statement下面的参数会显示达到的限制值和请求的内存值比如Requested memory: 16800 MB; Limit: 15000 MB。这个数字直接说明问题语句想用16.8GB但限制是15GB超了。2.5 真实案例复盘一条报表SQL拖垮IndexServer内存这里分享一个今年处理过的真实案例过程比较典型。客户环境是S/4HANA 2020HANA版本2.0 SPS05内存总量512GBIndexServer的allocation limit设为85%。凌晨业务跑月结固定资产折旧和成本结算报表集中执行。1点47分告警邮件到达HANA IndexServer内存使用率已从72%在3分钟内飙升到91%接近警告线。我先用M_ACTIVE_STATEMENTS抓到两条内存消耗超过8GB的语句一条来自COEP成本对象行项目表的查询一条来自ACDOCA通用分录表的关联查询。它们的共同点是都把全表数据拉进内存做聚合运算过滤条件几乎没起效果。进一步看执行计划发现这两条SQL都有一个非常差劲的join顺序优化器选择把2.5亿行的ACDOCA作为驱动表再去嵌套循环关联其他表。按照字段分布来说完全应该用小表驱动大表但因为统计信息过旧HANA的优化器没有意识到某个过滤字段的cardinality其实很高。处理方式分两步走第一步临时救火直接在HANA层面给这几条SQL执行ALTER SYSTEM CANCEL SESSION断开被挂起的查询内存曲线在10分钟内回落到78%。第二步是永久修复要求ABAP团队针对这两个报表的Open SQL做了优化把ABAP代码里的SELECT *改成只取必要字段把原本在ABAP层做的行循环逻辑改成一条带JOIN和GROUP BY的完整SQL同时在HANA侧更新了ACDOCA的统计信息执行UPDATE STATISTICS。此后两周观察内存使用率峰值稳定在79%以内没有再触发警告。3. 最容易触发内存溢出的四类SQL写法逐条拆解3.1 无谓的全表扫描与SELECT * 滥用有相当一部分HANA内存溢出源头就是SELECT *配合巨型表。SAP标准表里动辄上亿行的并不少见比如BSEG会计凭证行项目、MSEG物料凭证、CDHDR/CDPOS变更文档这些表在传统数据库上执行SELECT *可能只是慢但在HANA上还会消耗大量内存存放中间结果集。比如SELECT * FROM MSEG WHERE MBLNR 4900000010这句如果MBLNR上有索引返回的行数不多问题不大。可是如果查询条件是SELECT * FROM MSEG WHERE WERKS IN (1000,2000,3000)且没有其他约束一旦WERKS的过滤因子很低返回行数可能达到几百万甚至上千万HANA需要把这部分列数据读进内存再返回给应用。更隐蔽的是ABAP代码里的SELECT *配合FOR ALL ENTRIES。ABAP的FOR ALL ENTRIES会展开成多个单值IN列表如果内表有3万行生成的IN列表就有3万项SQL文本巨大且执行计划复杂很容易触达内存和CPU上限。正确做法的优先级排序是永远只查所需字段能用SELECT SINGLE就不开游标查询条件要带上高选择性的过滤字段至少要有一个等值条件不要在一张大表上同时做多个范围过滤3.2 JOIN爆炸笛卡尔积、低基数关联与多表乱连HANA对JOIN的优化能力很强但对无脑JOIN也没有办法。最典型的炸弹是隐式笛卡尔积也就是两表JOIN的ON条件里关联键根本不唯一或者关联键存在大量NULL值。举个例子两张表各5000万行以COUNTRY字段做JOIN而COUNTRY字段只有5个不同的值。这种情况HANA的优化器可能选择nested loop或hash join无论选哪个中间结果集都会膨胀到25亿行左右内存必然被打爆。这种低基数关联在SAP数据模型里很常见。比如把LIKP交货单抬头和LIPS交货单行项目按VBELN关联如果不加LIPS上的行项目条件一对多膨胀到行项目级别还能接受但如果再关联一张按交货类型维护的表而这张表又按VSTEL发货点关联多个一对多叠加起来就是指数爆炸。低基数JOIN会同时带来两个问题内存膨胀和结果集语义错误。优化手段就是把大JOIN拆成小查询或者把低基数的维度表先做一次聚合再去关联事实表。3.3 ORDER BY / DISTINCT / GROUP BY在列式存储上的真实代价列式存储对于ORDER BY和DISTINCT并不像很多人想的那么友好。列存储在压缩状态下扫描很快但排序和去重需要把结果集在内存中物化成一个可排序的结构这个过程消耗的是会话内存。DISTINCT操作的本质是把结果集去重放到hash set里如果去重的字段基数很高比如订单号、凭证编号这类几乎唯一的字段hash表需要维护的行数非常庞大。我见过一个Query先JOIN出800万行然后DISTINCT去掉重复但去重字段本身就是主键级别的唯一字段DISTINCT完全多余白白消耗了十几GB内存。GROUP BY的问题类似如果分组字段组合后的 группировка数量级接近数据行数内存消耗会非常明显。窗口函数ROW_NUMBER()、RANK()、SUM() OVER(PARTITION BY ...)更是内存杀手因为每个partition的结果都要保留在内存里参与排序和计算。建议把这类操作尽量下推到应用层或者使用HANA的LIMIT OFFSET分页查询避免一次性把全量结果集排序返回给用户。SAP的ALV报表经常有这个问题用户点了个排序系统就把整表数据拉下来排序。3.4 子查询与UNION ALL的隐蔽陷阱子查询在HANA的优化器里未必会被扁平化处理。有些IN (SELECT ...)子查询会先物化出一个中间结果集再和外部查询做semi-join。如果子查询本身没有优化好返回的行数远超预期外部查询也会跟着遭殃。UNION ALL则是把多个查询结果直接拼接每个分支的中间结果都会驻留内存直到整个UNION完成。SAP的一些标准报表会在ABAP层拼接多个SELECT的结果然后一次性传给ALV显示这在HANA上特别容易触发大内存。我处理过一个BW报表问题一条SQL里有7个UNION ALL分支每个分支单独执行只要2秒合在一起需要消耗14GB内存执行时长超过3分钟。原因是每个分支都有一张大表的子查询HANA无法共享公共子表达式。优化方式是改写SQL把公共子查询先落地成本地临时表再UNION。或者改用UNION去重版本让优化器有机会做进一步压缩。如果业务允许尽量用UNION代替UNION ALL虽然多了一次去重开销但去重后结果集变小整体内存反而降低不少。4. 救火与治本SQL层面的调整和HANA参数设置4.1 即时止损会话kill、暂停负载与临时参数调整告警发生的第一时间先判断业务是否还能忍。如果报表还可以等待优先尝试降低并发让高负载SQL逐个执行。如果已经出现大量挂起会话立刻找出占用内存最多的会话用ALTER SYSTEM CANCEL SESSION session_id逐个取消。对于阻塞严重、取消也取消不掉的会话可以ALTER SYSTEM DISCONNECT SESSION session_id强制断开。止损之后临时调低新会话来SQL的内存上限防止新的查询进入后又爆掉ALTER SYSTEM ALTER CONFIGURATION (indexserver.ini, SYSTEM) SET (session, statement_memory_limit) 8000 WITH RECONFIGURE;这句话把所有会话的单条SQL内存限制调到8GB。注意单位是MB而且这是会话级别的软限制单条语句超过会被终止。生产环境需要结合业务峰值合理设置不要一刀切调太低否则大量报表会直接报Out of memory at statement错误业务投诉更多。4.2 从执行计划HANA的角度反推SQL改写方向处理过一次内存告警后一定要做一次完整的执行计划分析找到真正需要优化的点。HANA Studio的PlanViz是最直观的工具能展示每个算子的内存消耗和行数估算。重点看三处第一Column Search的结果。查看是否有列被误判为SEARCH QUERY类型没有用到列式索引。_SYS_STATISTICS表可以提供列的基础信息辅助判断。第二Join的类型和顺序。HANA如果选择了Nested Loop Join且外表行数是几百万级这时候应该通过改写SQL增加条件或者加hint调整join顺序。第三GroupBy节点的输入行数和输出行数。如果输入1亿行输出1亿行说明这个GROUP BY没有起到应有的压缩作用可能分组过于精细应该考虑更粗粒度的分组或在更高层聚合。基于执行计划回推SQL改写的优先级是先确认SQL语义有没有问题能否先做数据裁剪再看能否把大结果集延迟物化先用主键或索引字段定位行集再回表取数最后再考虑hint强制优化器走某条计划ABAP层面尽量在Open SQL里用CDS View的Join条件替代老式的SELECTFOR ALL ENTRIES。4.3 参数层面的精准限制global.ini与indexserver.ini推荐在HANA上开启statement级内存限制并配置一个比物理内存上限更保守的值。有人说设置statement_memory_limit会误伤正常的大查询实际上设置得当反而是保护。参数示例[indexserver] statement_memory_limit 15000 max_statement_memory_limit 20000statement_memory_limit是软限制max_statement_memory_limit是硬限制。单条语句超过软限制先告警超过硬限制直接终止。这两个值根据系统的物理内存总量和常见报表内存消耗统计来决定一般建议设为物理内存的3%到5%左右。512GB内存的机器单条语句软限制可以给10GB到20GB具体看业务峰值。global.ini里的allocationlimit也很重要[memorymanager] allocationlimit 85这个85指的是IndexServer最多使用物理内存的85%。如果物理内存512GBIndexServer最多用到435GB左右。剩下15%留给操作系统和文件系统缓存。这个值设太高会导致操作系统内存不足进入swap反而拖垮整体性能设太低业务高峰期频繁触发OOM。4.4 长期治理执行计划监控、统计信息更新与代码评审内存溢出告警只能靠监控发现但根本治理要靠在开发侧的控制。统计信息更新是最容易被忽略的一环。HANA的优化器非常依赖表的统计信息ACDOCA、BSEG、CDPOS这类超大表必须定期跑UPDATE STATISTICS否则优化器对行数的估算误差可能差出几个数量级。SAP针对HANA提供了默认的统计信息更新作业但很多客户关掉了或者没配好导致关键表几个月没更新过统计信息。SQL代码评审也是必需的。我在SAP项目的ABAP代码评审清单里固定包含几条是否存在SELECT *是否只查询字段清单是否对主要查询条件有索引支持是否存在FOR ALL ENTRIES内表过大的风险报表是否一次拉取全量数据到ALV是否可以分页是否在ABAP层做了大数据量的嵌套循环处理这些习惯养成后内存告警会少很多不只是避免溢出整个系统响应速度都会提升。5. 监控预警与日常巡检把内存爆雷扼杀在萌芽里5.1 配置告警阈值和采样频率没有告警就没有追赶优先级。HANA Cockpit里可以配置告警规则推荐覆盖以下三类指标IndexServer内存使用率告警阈值设为80%严重阈值设为90%单条Statement内存消耗超过软限制的次数关键表的统计信息年龄超过7天告警提醒采样频率建议每分钟一次告警邮件按15分钟聚合发送避免夜里被邮件轰炸。5.2 建立SQL性能基线每次排查完问题把SQL文本、用户、事务代码、内存消耗、执行时长记录到一份基线表里。不需要专门的系统一个Excel或Confluence页面就够。之后每周对比一次看有没有新的SQL突破基线值。这套做法成本极低但效果很明显。两三个月下来系统里哪些SQL是内存大户就一目了然了。后续做硬件扩容或者HANA版本升级时这些基线数据还能直接用来评估容量需求。5.3 检查清单每月做一次HANA内存健康体检推荐每个月做一轮固定巡检不需要很复杂但要有规律。我的巡检清单如下检查项方法预警条件IndexServer内存使用率HANA Cockpit /M_SYSTEM_MEMORY_USAGE平均使用率超过80%峰值内存消耗Top20 SQL查询历史负载分析表单条SQL内存消耗超过阈值的50%统计信息年龄SELECT * FROM SYS.TABLE_STATISTICS关键表超过7天未更新会话数量与连接池状态M_CONNECTIONS活跃会话数持续高于基线的2倍列存压缩率检查M_CS_TABLES的MEMORY_SIZE_IN_TOTAL压缩率低于1:3检查项不用每天都跑但每月至少跑一次。特别是SAP系统做了大的版本升级或者引入了新的业务功能后一定要重新评估内存基线和统计信息状态。写在最后的几条实战提醒处理HANA内存问题大多数情况下不是数据库管理员单方面能解决的需要和ABAP开发、业务模块顾问一起协作。我个人的体会是光在HANA层面限制语句内存只是治标真正的钥匙还是在SQL的写法上尤其是SAP标准报表和增强开发里的Open SQL。再补充一个容易被忽略的细节M_ACTIVE_STATEMENTS只能看到当前正在执行的语句如果告警发生在凌晨而你早上才看到SQL早就被终止了。这种情况下一定要依赖HANA的trace文件和ST22的dump记录所以trace的保留策略和日志轮转要提前配好否则出了事连证据都找不到。每次解决完这类问题我都会建议客户做一次复盘从SQL优化、参数调优、监控覆盖三个层面分别记录行动项下次再遇到类似告警处理速度会快很多。内存告警不可怕可怕的是每次都临时救火却找不到根因。
返回列表