1. 一个让系统停滞的内存转储
做SAP这么多年,ST22几乎是每个运维和开发都不愿多看几眼的界面,因为它出现往往意味着系统里有东西坏了。但在所有ST22转储类型里,MEMORY_NO_MORE_PAGING是我认为最需要认真对待的一个。它不像是某个程序逻辑写错导致的异常,而更像是整台应用服务器的内存体系被挤到了极限,最后在“Paging(分页)”这一步彻底崩了。
这个错误出现的时候,业务用户的第一感知通常很明确:操作到一半屏幕卡住,几秒钟后看到“短转储”的红色系统消息提示,再往后可能连登录都变慢,因为应用服务器的可用资源被大量挤占。对于SAP的运维顾问、ABAP开发人员以及负责系统性能的团队来说,掌握这个错误的来龙去脉,不只是为了“消转储”,更是为了避免生产系统在业务高峰期出现大面积不可用。
这篇文章里,我会从SAP Paging机制讲起,结合我实际排查过的一些案例场景,把MEMORY_NO_MORE_PAGING的成因、排查路径、调优思路和代码层面的规避方法完整拆一遍。不管是刚接触SAP Basis的新手,还是写了很多年ABAP却对内存管理了解不深的开发,这篇文章都值得认真看完。
1.1 MEMORY_NO_MORE_PAGING到底是什么
从SAP的错误分类来看,MEMORY_NO_MORE_PAGING属于“内存空间不足”类错误,触发点是ABAP运行时环境在尝试把内存中的数据换出到Paging区域时,发现Paging空间已经耗尽,或者无法继续扩展,于是直接终止当前工作进程。这里的“Paging”不是操作系统级别的虚拟内存换页,而是SAP应用服务器内部专门用于保存溢出数据的一块独立区域。
这个错误比较棘手的地方在于:它表面上指向“Paging空间不够”,但实际根源往往在扩展内存(Extended Memory)的配置、程序对内存的消耗方式,甚至并发用户的整体资源占用上。很多时候,你打开ST22看到这个错误,再往下翻错误分析,会发现触发点只不过是一条SELECT语句或者一次内表操作,而真正的问题藏在更前面的内存申请链条里。
1.2 这个错误的影响范围有多大
单次MEMORY_NO_MORE_PAGING转储意味着一个工作进程被终止,正在跑的事务或报表直接中断。如果只是个别用户在跑一个异常程序,影响还算可控;最怕的是刚好在月末结账、物料账运行、MRP批量跑批这种时段出现,多个后台作业和在线用户同时把内存顶到上限,短时间内会出现连续多个进程被终止,整个应用服务器进入半瘫痪状态。
我在一次现场支持时遇到过这样的情况:某生产系统从上午十点开始,半小时内连续产生了几十条MEMORY_NO_MORE_PAGING转储,涉及两三个常用报表和某些后台作业。业务部门的反馈是“系统越来越慢,什么操作都转圈”。打开ST22一看,内存类转储占了绝大多数,应用服务器的负载并没有异常高,但内存换页的压力已经让所有工作进程都处于濒临崩溃的边界。
1.3 谁最该深入了解这个机制
如果你只负责SAP业务模块的配置,可能一辈子都不用打开ST22;但如果你是ABAP开发者,写报表时动不动用无条件SELECT把整表数据装进内表,那这个错误会在你最意想不到的时候找上门。如果你是Basis或系统管理员,内存参数的调整、Paging空间的规划、并发压力的控制,都直接关系到系统能否平稳运行。
所以这篇文章的受众,我认为主要是ABAP开发人员和管理SAP系统的技术人员。运维人员需要知道怎么排查和调参,开发人员需要知道怎么写代码才能避免把系统内存吃垮。两边共同把一个机制理解透了,生产环境的这套内存体系才能稳定。
2. SAP Paging机制与内存体系拆解
很多ABAP开发对SAP内存体系的理解,其实停留在“内存表放在内存里,数据库表放在数据库里”这个层面,这并不能解释MEMORY_NO_MORE_PAGING为什么发生。要理解这个错误,首先要把SAP应用服务器的内存分区搞清楚。
2.1 ABAP运行时内存体系的整体框架
SAP应用服务器进程的内存大致可以分成以下几个区域:
- 扩展内存(Extended Memory):ABAP程序运行时的“主战场”,内表、变量、对象实例的数据基本都放在这里。系统按照
em/initial_size_MB和em/max_size_MB这两个参数控制扩展内存的分配策略。它是一块相对独立的共享内存区域,各个工作进程按需从这里申请空间。 - 私有内存(Private Memory / Heap Memory):每个工作进程还有自己独立的堆内存区域,主要用于存放一些不属于ABAP上下文的数据。ABAP程序申请大块内存时,尤其是超过扩展内存配置限制后,会转向私有堆内存。
- Roll缓冲(Roll Area):当工作进程处理完一个请求、被切换去处理另一个请求时,当前ABAP上下文的数据不能直接丢,需要临时保存到Roll区域。它相当于工作进程之间的“临时寄存处”。
- Paging(分页区域):这是存储那些从内存中被“换出”的数据的空间。当扩展内存和Roll区域压力增大,系统会把一些暂时不用的数据块写到Paging区域,腾出空间给正在活跃的程序片段使用。
这四个区域相互配合,构成了ABAP程序运行时的完整内存环境。其中,Paging和Roll在物理上都对应着应用服务器上的文件(如Linux系统下安装目录中的分页文件),而扩展内存和私有内存则对应物理内存中的映射区。理解这一点很关键:Paging虽然是“内存体系”的一部分,但它本质上是有磁盘IO参与的数据交换区,速度远比不上真正的物理内存。
2.2 Paging在其中的位置与触发路径
我用一个生活里的场景来解释Paging的位置。你在一张办公桌上处理文件,桌面就是扩展内存,正常情况下的工作都在桌上完成。桌面上堆不下的时候,你会把一部分不那么急用的文件放进身后的文件柜,文件柜就是Roll区域。但文件柜容量也有限,再放不下,就只能搬到更远处的仓库去,这个仓库就是Paging。等你要用仓库里的文件时,再跑一趟搬回来。
SAP的工作进程在处理用户请求时,也是这个流程。程序申请的内存超过了扩展内存的可用额度时,系统会尝试把一些数据挪到Paging区;一旦Paging空间也满了,申请新空间的请求就会失败,于是触发MEMORY_NO_MORE_PAGING终止进程。
这里有一个容易被忽略的点:Paging区域的默认配置通常不是无限大的,它受ipc/paging_blocks等参数控制。很多系统的Paging空间从安装时就按当时的业务规模设定,数据量增长、程序复杂度上升之后,这个空间没有同步扩容,结果就是平时勉强够用,遇到高峰或者个别内存大户程序时段就直接崩盘。
2.3 几个关键参数和调整方向
SAP Basis团队最常打交道的几个内存参数,和这个错误直接相关:
em/initial_size_MB:扩展内存的初始分配值。这个值太小会导致进程启动时频繁向操作系统申请内存,增加开销。em/max_size_MB:扩展内存的最大上限。这是单个工作进程能够占用的扩展内存天花板。遇到单个程序内存消耗特别大的情况,这个参数决定它能不能继续长胖。abap/heap_area_total:所有ABAP工作进程的堆内存总量上限。这个参数全局控制私有堆内存的使用,防止所有进程加起来把物理内存吃光。abap/heap_area_dia和abap/heap_area_nondia:分别限制Dialog和后台工作进程的堆内存分配量。ipc/paging_blocks:Paging文件的总块数。这个参数直接决定了Paging区域的大小,调整后需要重启应用服务器生效。rdisp/roll_maxfs:Roll缓冲文件的最大大小。
这些参数不是独立存在的,它们相互牵制。我经常遇到的一个情况是:客户觉得内存不够,就把em/max_size_MB调得很大,但忽略了abap/heap_area_total的总量限制,导致单个进程虽然能申请很多扩展内存,但所有进程共享的堆内存总量很快见底,依然会触发内存类转储。调整内存参数时,一定要按“单进程上限”和“全局总量”两个维度一起看,缺一不可。
2.4 为什么报错的是Paging而不是扩展内存
很多第一次接触这个错误的开发会疑惑:程序用的内存明明是内表,是在扩展内存里的,为什么报错却扯到Paging上?原因在于SAP的内存管理机制里,扩展内存不是一个“只进不出”的区域。系统会定期或者按需把扩展内存中的某些数据块标记为“不活跃”,然后移到Paging区。这个动作本身是正常的内存管理行为,目的就是给活跃进程腾出足够空间。
问题在于,当多个进程同时处于高内存消耗状态,或者某个进程的内表数据量极大,系统需要向Paging区搬运大量的数据块。如果Paging区剩余空间不足,搬不出去,新的内存申请就无法满足,系统只能终止进程。所以MEMORY_NO_MORE_PAGING的本质,是“内存压力已经传导到了整个存储链条的最末端”,而不是单纯的某个区域空间不足。
3. 最常见的三类诱因
排查这个错误的时候,我总结过一套经验:所有MEMORY_NO_MORE_PAGING案例,几乎都可以归结到三个方向——程序本身内存消耗失控、系统配置跟不上数据量增长、并发压力集中释放。很多时候还是两个甚至三个因素叠加。
3.1 大结果集一次性加载
这一类是最容易定位的,也是开发阶段最不应该出现的。
比如说,一个报表程序为了显示全量数据,写了一句最简单粗暴的代码:
SELECT * FROM zflight INTO TABLE @DATA(lt_flight).如果zflight表只有几千行,问题不大;但业务运行几年后,这张表可能有上百万行。单次SELECT把所有字段全部拉进内表,整个内表在扩展内存中的占用可能达到几百兆甚至上GB。一条用户请求就把一个工作进程的扩展内存吃掉了大半,再来两个并发用户,系统就处在悬崖边缘。
我在帮客户做健康检查时,不止一次在自定义开发程序里看到这类没有WHERE条件、没有分页、没有数量限制的全量查询。写的时候很爽,上线之后就是定时炸弹。数据量小的时候爆炸不了,等表长大到一定程度,每个月的批处理时间一到,就会集中爆发。
3.2 代码层面的内存失控
比全表SELECT更隐蔽的是代码逻辑导致的内存失控。比较典型的是递归调用没有终止条件,或者递归层次过深。
ABAP里写递归函数不是不行,但要非常克制。我见过一个用来展开BOM结构的程序,设计的时候是循环调用函数自身,但物料主数据里存在循环嵌套的异常场景时,递归没有出口,一层套一层,每层都往内表里追加数据,几秒钟内就能把扩展内存和Paging空间全部顶满。ST22报出来的错误里,这种场景的调用栈会特别长,一拉鼠标滑轮都滑不到底。
还有一种隐蔽的内存失控是字符串拼接。在循环里反复做字符串拼接,每次拼接都生成一个新的字符串对象,旧对象还没来得及释放,新的又来了。内表行数本身不大,但每一行的CLOB字段都在膨胀,整个程序的内存占用就会呈几何级数增长。在没有GC压力的情况下,这类程序跑得越久死得越快。
3.3 并发高峰的放大器效应
单个程序的内存消耗如果在正常范围,但同一时段有大量用户同时执行高消耗操作,就会把系统的整体内存预算瞬间打穿。这种情况在每个月末、季度末尤其明显:财务做月结、物料做账、销售做统计报表,所有高消耗任务挤在同一个时间窗口。
我遇到过一个很典型的场景:某个查询报表单次执行只需要150MB的扩展内存,本身不算离谱。但月末上午,20多个业务用户几乎同时打开了这个报表,加上后台还有三四个批处理作业在跑,瞬间需要的总内存就超额了。系统不得不频繁地把数据往Paging区搬运,Paging区IO压力激增,最终多个工作进程同时报MEMORY_NO_MORE_PAGING。
并发这个因素经常被忽略,因为单看任何一个程序都觉得没有问题。但系统的内存预算不是按“单个程序”来分配的,而是按“所有并发进程的总和”来分配的。一个平时正常的程序,在错误的时间和其他程序叠加,就可能成为压垮系统的最后一根稻草。
4. ST22排查流程与现场处理
遇到MEMORY_NO_MORE_PAGING,不要慌,按照一套固定的排查流程走,基本都能把问题缩小到一个很具体的范围。这套流程花不了多长时间,但能帮你避免在错误的方向上浪费时间。
4.1 读懂ST22转储里的关键字段
进入ST22之后,你会看到系统里的转储列表。按时间排序,找到MEMORY_NO_MORE_PAGING类型的记录,双击进入详情页。这里有几个信息是排查看重中之后的:
首先是“What happened?”区域,系统会用一段文字描述到底发生了什么。通常会出现类似“The current process was terminated due to a memory shortage”的描述。这里告诉你的是直接原因,而不是根本原因,所以不要在这句话上纠结太久。
其次是“错误分析”(Error Analysis),这里会给出更具体的信息,比如是哪个进程、在哪个内存区域申请空间时失败。再往下,“终止位置”会列出程序名、包含对象名、行号,这是你定位代码的关键线索。
最后是“活动类型”(Activity type),它会告诉你是Dialog请求、RFC调用还是后台作业。这个信息对于判断影响范围很有用——如果是后台作业,直接找到作业所属的计划任务,重点关注批处理时间窗口;如果是Dialog请求,需要关注前端用户的操作路径和并发情况。
4.2 定位触发程序的思路
拿到程序名和行号之后,用SE80或SE93打开对应程序源码,找到出问题的那一行。绝大多数情况下,这一行附近会有一句SELECT语句、一个内表操作,或者一个函数调用。不要只看这一行,要把前后至少二三十行代码都看清楚,理解这段代码在执行什么业务逻辑,内表承载了多少数据量。
如果转储信息里给出的程序名是一个很通用的报表,并且涉及多个用户同时使用,那么问题可能不只是代码本身,还需要结合并发情况判断。如果是一个只在批处理中运行的函数,那么就要重点分析这个函数是否在某个数据维度上做了全量处理。
我一般还会顺手查一下数据库表的记录数。直接用SE11查看表的“附加信息”或者用事务代码SE14统计一下表的当前记录量。如果程序查询的主表已经有几百万行,而程序没有分页处理,基本上可以断定根因了。
4.3 用ST02/AL08/ST06组合确认内存状态
定位到具体程序之后,还需要确认系统当前的内存状态,以判断是偶发还是趋势性问题。
ST02显示的是SAP内存区域的使用情况。重点看Paging和Extended Memory两块的当前使用率。如果Paging的使用率已经接近90%以上,说明系统的Paging空间本身就吃紧;如果Extended Memory的使用率经常冲到高位,说明扩展内存配置可能偏小。
AL08可以查看当前登录用户和工作进程的内存占用。如果发现某几个用户对应的进程内存占用明显偏高,可以用它锁定嫌疑用户。ST06则是看操作系统层面的物理内存使用情况,避免只盯着SAP内部参数而忽略了物理服务器本身的内存是否已经耗尽。
这三个工具配合使用,基本可以拼出完整的画面:程序吃内存、进程占资源、系统Paging空间紧张,三者重叠在一起。
4.4 现场止血的几种办法
生产系统出了问题,第一要务是恢复业务运行。在确定根因之前,可以先做以下临时处置:
如果转储集中在个别高消耗程序上,最直接的办法是临时限制该程序的可执行用户范围,或者通知业务部门暂停使用该事务。如果是后台作业导致,先锁定或暂停相关作业,等业务低峰期再执行。
如果系统整体内存压力非常大,可以考虑扩展Paging空间的参数并重启应用服务器。但是要注意,这个操作会中断所有在线用户连接,必须在跟业务确认过的情况下再执行。如果只是个别程序问题,重启服务器来解决不是最优方案,因为你要么回到原样,要么在没有解决问题的情况下去调大参数,属于治标不治本。
4.5 常见问题速查
| 现象 | 可能原因 | 优先排查 |
|---|---|---|
| 单个报表执行到一半就转储 | 程序一次性加载了大结果集 | ST22定位程序行号、检查SQL和表数据量 |
| 特定后台作业批量运行时报错 | 批处理程序内存消耗失控 | 查看作业对应的程序代码、监控批处理时段内存占用 |
| 多个用户同时操作时并发转储 | 并发高峰放大了内存压力 | AL08看占用、错峰安排、限制并发 |
| 系统刚启动时正常,运行几天后频繁转储 | Paging空间被碎片化占满 | 检查ST02的Paging使用率、考虑重启释放 |
| 调整了 em/max_size_MB 仍然报错 | 未考虑 abap/heap_area_total 全局限制 | 检查所有堆内存参数、重新规划内存预算 |
这个速查表是我在平时工作中沉淀下来的,覆盖面不一定全,但用来做第一轮筛选基本够用。
5. 根治方案:参数调优与代码规范
找到问题只是第一步,最重要的是把问题解决掉,并且防止它再次发生。根治措施需要从参数和代码两个层面同时进行,只做任何一个方面都可能留下隐患。
5.1 参数调整的边界与原则
先说明一点:调参这件事不是越大越好。内存参数配高了,单个用户的内存占用上限变大,但如果物理机总内存不够,反而会导致操作系统级别的交换,系统整体性能下降,甚至引发Linux的OOM Killer。所以调参前必须知道服务器的物理内存总量,并且给操作系统和其他非SAP应用留出足够余量。
我个人在做内存参数规划时,会先算一笔账。假设一台物理机有64GB内存,操作系统和其他服务大约占用10GB,那么SAP可用的物理内存预算大约在50GB左右。这个50GB要分配给所有SAP工作进程、缓冲区、共享内存和Paging/ Roll文件。理论上,所有进程的最大内存占用之和,不应该超过这个预算。
具体到参数上,我会按以下顺序逐项核对:
- 确认
em/max_size_MB的值,确保单个Dialog进程的扩展内存上限足够支撑正常业务程序。 - 检查
abap/heap_area_dia和abap/heap_area_nondia的分配,确保Dialog进程和后台进程之间的内存额度不要失衡。 - 核对
abap/heap_area_total的值,让它在所有进程堆内存之和的合理范围内。 - 重点查看
ipc/paging_blocks的当前配置,通过操作系统上Paging文件的当前大小来判断是否有扩展空间。
每一项参数修改都建议在开发或测试环境验证后再上生产。动态参数可以通过RZ11直接改,静态参数改完需要重启实例才能生效。很多系统管理员在调整em/max_size_MB后没有重启,以为已经生效,结果过了几天又报同样的错误,这种坑我也踩过几次。
5.2 代码层面的通用优化清单
ABAP程序的内存管理,很大程度取决于编写者的习惯。我归纳了一些在实际工作中验证过有效的优化方向,照着做基本能避免掉进内存陷阱。
第一条,尽量避免全表无条件SELECT。如果一张表的行数已经超过几十万,程序的逻辑应该是分批获取数据,而不是一口气全部加载。举个典型的例子:
DATA: lt_batch TYPE TABLE OF zflight, lv_last_id TYPE zflight-id VALUE 0, lv_more TYPE abap_bool VALUE abap_true. WHILE lv_more = abap_true. SELECT * FROM zflight WHERE id > @lv_last_id ORDER BY id UP TO 1000 ROWS INTO TABLE @lt_batch. IF lines( lt_batch ) < 1000. lv_more = abap_false. ENDIF. LOOP AT lt_batch INTO DATA(ls_flight). lv_last_id = ls_flight-id. " 业务处理 ENDLOOP. CLEAR lt_batch. ENDWHILE.这样每一批只占用一小段内存,处理完一批释放一批,程序的峰值内存不会随表数据量线性上涨。
第二条,能在数据库层完成的计算就交给数据库。数据库通常有强大的聚合和过滤能力,把几百行结果通过网络传到应用层和把几万行原始数据拉到应用层再计算,内存差的不是一个数量级。使用SUM、COUNT、MAX、MIN等聚合函数就是一个很好的选择。
第三条,小心使用FOR ALL ENTRIES。这个语法在SAP开发中很常用,但有一个经典陷阱:如果FOR ALL ENTRIES后面的驱动内表为空,生成的SQL会变成无WHERE条件的全表扫描,而且是“没有任何过滤条件”的那种。所以使用前务必判空,否则一张千万级表会被毫无防备地完整读一遍。
第四条,动态数据不要放内表里反复处理。有些业务逻辑需要多次遍历数据,把数据放在数据库表还是内表要权衡清楚。少量数据用内表没问题,大数据量还反复在应用层循环扫描,内存和CPU都扛不住。
第五条,避免在循环里做字符串拼接或频繁的内表操作。字符串拼接会反复生成临时对象,APPEND到内表本身不贵,但每轮循环都触发内表扩容就会造成不必要的内存拷贝。这些都是ABAP开发中比较隐蔽的内存床点。
5.3 运维侧的分流与监控
代码调优和参数调整都完成后,运维侧还需要建立长效机制。
对高消耗程序,建议在业务侧做并发控制。可以通过调整SAP的作业调度策略,把多个重负载的批处理作业错开执行时间,避免同时涌入。在线用户方面,如果同一个报表在固定时段出现并发峰值,也可以考虑在应用层做用户数限制或排队机制。
监控方面,我建议定期记录ST02里Paging和扩展内存的使用率趋势,至少每周一次。如果发现使用率在逐步爬升,说明业务数据量或程序的负载在增长,就要提前评估是否需要调参或优化代码,而不是等它报警。
ST22的转储也应该有定期的复盘机制。不要只是点进去看一眼就关掉,至少要统计一下每个转储类型的数量,特别是内存类的转储是否有上升趋势。如果MEMORY_NO_MORE_PAGING从一周一次变成一天一次,系统的内存健康状况就是在恶化,必须提前介入。
5.4 避免走入“无脑调参”的误区
最后想特别提醒一句:不要一看到MEMORY_NO_MORE_PAGING就把em/max_size_MB翻倍。这种做法治标不治本,而且可能带来新的问题。
举例来说,如果程序本身的写法就是把一张百万行大表一次读进内表,你把em/max_size_MB从2GB调到4GB,它确实不会再立刻报Paging不足了,但这只是把程序曾经产生过大内存消耗的事实掩盖了。到了高峰期,多个用户同时执行这种程序,物理内存很快就会被吃满,届时出现的问题可能不只是SAP的短转储,而是整个服务器的操作系统内存崩溃。
正确的顺序是先看代码、再看并发、最后才调参数。代码优化是根本,并发控制是辅助,参数调整是兜底。这个顺序反了,问题只会越拖越重。
最后说点我的实际体会
这些年处理过不少系统内存相关的故障,我越来越觉得,大多数MEMORY_NO_MORE_PAGING的根源都不是“系统配置出错”,而是“系统在诞生之初没想过会有这么大的数据量”。一个当初写着方便的全表查询,随着业务发展变成了内存杀手;一个当初从别的程序拷贝过来的处理逻辑,因为数据增长而成了系统的定时炸弹。
所以每次排查完一个内存问题,我都会把整个处理过程记录下来,包括当时的ST22截图、内存参数、程序代码、优化前后的对比。累积下来的这些案例就是最好的知识库。以后再遇到类似问题,翻一下之前的处理记录,能省去大量重复排查的时间。
另外,如果你是在开发阶段就看到了这篇文章,那我要多说一句:写代码的时候,永远假设你操作的表会变成当前行数的100倍。用这个标准去审视每一句SQL和每一次内表操作,很多内存问题根本不会发生。系统的稳定性不只是在运维阶段维护出来的,更是开发阶段设计出来的。