最近在帮客户把一个本地开发的扩展服务迁到 ABAP environment(SAP BTP 上的 ABAP 云运行时),我发现团队脑子里还是十几年前的经典 ABAP 习惯:代码里到处是 SELECT SINGLE、循环里调 RFC、性能问题等到压测才想起来查。这篇文章想把三件事放一起讲清楚:ABAP environment 的架构到底怎么影响你的扩展设计、Sizing 该怎么估算才算不拍脑袋、上线之后性能反模式怎么一条条治理掉。适合正要上云、或者已经在云里被性能问题追着跑的 ABAP 开发者和架构师。
1. ABAP environment 的架构全景:先搞清楚我们在哪
1.1 从经典 ABAP 到云 ABAP:底层模型的关键差异
很多老 ABAP 开发者第一次打开 ABAP environment 的 ADT 工程时,第一反应是“我原来的 TCode 怎么都没了”。这不是功能阉割,而是运行时模型变了。在经典 ABAP 应用服务器里,你面对的是一组 DIA、UPD、BTC 工作进程,数据库表直接可以读,RFC 可以直接建,dynpro 可以随便写。到了 ABAP environment,底层改成了基于 Cloud Foundry 的云运行时,应用进程由平台调度,你不再有自由选择工作进程类型和数量的权力,连一张表的物理存储都不归你直接管理。
这个变化带来的核心约束是:代码不再运行在“你自己的应用服务器”上,而是运行在一个被平台托管的 ABAP 会话里,数据库用的是独立的 HANA Cloud 实例。你通过 CDS entity、ABAP SQL、OData 服务、HTTP/RFC outbound 与外界打交道。开发模型全面转向 ABAP RESTful Application Programming Model(RAP)和 CDS 语义建模,传统 dynpro 和大部分经典事务码已经被云开发模型明确排除。
理解这一点非常重要。因为很多“为什么云上性能差”的争议,根源不是 SAP 做了手脚,而是我们把经典环境的优化思路带了进来:经典环境里一个 RFC 调用走的是本机或近距离的 RFC 通道,开销低;云环境里同样的调用可能要经历不同层的网络跳转、服务绑定、OAuth 令牌刷新,成本完全不是一个量级。架构上的差异直接决定了后面设计、Sizing、性能优化的一切前提。
1.2 在 BTP 中的定位:应用层、数据层与集成层的协同
ABAP environment 不是孤立的,它只是 BTP 里的一层。你在 BTP 子账户里创建 ABAP environment 实例时,同时会在背后绑定云 Foundry 环境、HANA Cloud 数据库,并通过 destination、communication arrangement 打通外部系统。换句话说,你写 ABAP 代码,但 ABAP 只是整个分布式系统的一小段,外面还挂着身份认证、API 网关、消息队列、对象存储。
我建议团队把它理解成“一个封装好的业务领域服务层”。对外,它暴露 OData 或 HTTP 服务;对内,它访问 HANA Cloud;横向,它通过 communication system 与 S/4HANA、其他 BTP 服务或本地系统集成。你不再需要关心工作进程怎么分配,但必须关心你的应用到底需要多少并发、多少内存、多少数据库吞吐,而这些正是 Sizing 要回答的问题。
有一个点容易被忽略:ABAP environment 不支持经典 RFC 服务器模式,也就是说外部系统不能直接调你的自定义 RFC 函数模块。外部统一的入口是 OData 或 HTTP 服务。这个限制看起来很“不方便”,但反过来看是一件好事——它强迫你把业务能力显式建模成 API,而不是把 RFC 函数当内部后门到处乱接。我见过太多经典系统里几百个 RFC 函数彼此调用,依赖关系根本理不清。云环境给了你一次重新划边界的理由。
1.3 扩展模式的取舍:In-App 与 Side-by-Side 到底选哪个
架构上你第一个要做的决定是:新功能到底放在 S/4HANA 里面做扩展,还是在 ABAP environment 里做 Side-by-Side 扩展。很多团队默认“既然我熟悉 ABAP,那就直接在 S/4 里加表加程序”,但这里有个隐性成本:S/4HANA 的核心租约(contractual)限制和升级兼容性要求越来越严格,往核心系统里塞大量自定义代码,每次升级都可能炸出一堆兼容性问题。
在 ABAP environment 里做扩展,好处是环境独立,升级由 SAP 托管,自定义代码造成的升级风险基本隔离。代价是你必须接受它那套约束:不能用经典的隐式增强、不能直接改核心表、所有数据模型要通过 CDS 自定义发布。我的建议很简单:如果你的功能是 S/4HANA 主流程的微小增强,且数据主体就在 S/4 里,那 In-App 可能是合理选择;如果是一个相对完整的业务能力、多个外部系统参与、有自己的生命周期,那就放到 ABAP environment 来。
这个选择直接影响后面所有工作。Say你选 Side-by-Side,Sizing 和性能设计就要从“我这个服务要撑多少外部调用”出发,而不是“我的表有多少行”。架构决策从来不是技术洁癖,而是后续所有容量规划的出发点。
2. 可扩展应用的设计思路:不是把旧代码搬上云就完事
2.1 以 RAP 业务对象为骨架:行为、数据、语义层一起建模
ABAP environment 里的标准应用开发模型是 RAP。RAP 的核心思想是让你把业务对象建模成一个完整的“数据 + 行为”单元:CDS 视图负责数据投影,行为定义(behavior definition)负责操作和校验,OData 服务负责暴露端点。为什么强调这一点?因为可扩展性首先来自清晰的边界——当每个业务对象都独立成服务时,你可以单独对它做性能测试、单独扩容、单独治理并发问题。
我在实际项目里见过最典型的反面例子:团队把迁移过来的代码堆在一个巨大的类里,一个方法两千行,直接拼 SQL、直接在 UPDATE 前写各种 IF 判断,没有 RAP 的行为层,也没有 CDS 的语义模型。结果就是每次需求改动都要动这个类,性能问题也集中在这一团乱麻里,根本没法按业务对象拆分治理。
正确的做法是先把业务能力拆成若干 RAP 业务对象。比如一个库存扩展服务,你可以拆成“库存预留”、“库存转移”、“库存盘点”三个业务对象。每个对象有自己的数据模型、行为实现、授权控制。这样一来,Sizing 时可以按对象估算负载,性能优化时可以按对象做针对性设计,扩展时也不会把一个对象的改动波及其他对象。
2.2 无状态优先与幂等设计:会话管理的两个硬原则
经典 ABAP 里,开发者习惯在 ABAP 内存、或者通过 IMPORT/EXPORT TO MEMORY、甚至通过 SPA/GPA 参数在会话之间传递状态。但在云运行时,会话和进程是由平台调度的,你无法保证同一个用户的下一次请求一定落在同一个会话里。说得直接一点:依赖 ABAP 会话状态的做法,在 ABAP environment 里就是定时炸弹。
所以可扩展应用的第一原则是尽量无状态:所有业务判断需要的上下文,要么从请求参数拿,要么从数据库读,要么从外部服务取,不要指望上一次调用留下的内存状态。如果确实需要跨请求的状态(比如多步骤向导),把它显式存在业务表里,并用一个请求 ID/会话 ID 做关联。
第二原则是幂等。尤其是在接收外部系统回调或消息事件时,外部系统可能因为超时重试,同一个事件会发两次。如果你的服务没有幂等处理,就会出现重复扣库存、重复建单这类低级但致命的问题。实现幂等不复杂:在一个业务表里记录处理过的消息 ID,处理前先查重,处理时加锁保证并发安全。这两条原则加上去之后,你的服务才能应对弹性伸缩——因为无论实例怎么扩缩、请求怎么漂移,结果都一致。
2.3 数据访问与批处理:把计算留在数据库,而不是拉回 ABAP
可扩展应用在数据访问层面最容易犯的错,是把“数据库该干的活”搬到应用层干。比如你需要几千行的汇总结果,正确做法应该是在 CDS 视图里用聚合函数算好,只把几百 KB 的结果返回;反模式是 SELECT 全表到 ABAP 内表,然后在 LOOP 里求和过滤。两者的资源消耗可能差一个数量级。
ABAP environment 的优势在于 HANA Cloud 的计算能力很强,你要学会“尽量把筛选、投影、连接、聚合下推给数据库”。这不仅是性能优化问题,也是可扩展性问题:数据库层面做一次集合并行计算,比 ABAP 会话里一个线程慢慢循环要可扩展得多。
另外,凡是涉及批量更新的场景,尽量使用批量操作而不是逐行操作。RAP 的行为实现天然支持批量导入(FOR ALL INSTANCES),你在 MODIFY 时直接传一个内表,让框架一次性写库,而不是在循环里单条 UPDATE。不要觉得“我把内表传进去就完事了”,关键是要保证你的行为方法里没有隐式的单行处理逻辑——很多性能问题恰恰藏在“看起来是批量、实际内部一条条处理”的实现里。
2.4 异步与事件驱动:什么时候走队列,什么时候同步返回
不是所有请求都必须同步处理。很多 ABAP 开发者习惯了函数调用的同步思维,遇到外部系统慢就跟着慢,导致整个服务被拖垮。在分布式架构里,正确的思路是把“需要立即返回结果的操作”和“可以稍后处理的操作”分开。
如果一个操作要调用外部系统、执行多步骤业务逻辑、且用户不需要即时确认最终结果,就别让它阻塞在 HTTP 请求里。ABAP environment 支持通过队列/异步机制处理这类场景,你可以先把任务落表,返回“已接收”的确认,再由后台任务或事件消费者去处理。这样你的服务吞吐就不再受外部系统延迟的直接影响。
但异步不是银弹,它带来了新的复杂性:你需要处理任务失败重试、需要保证事件顺序、需要监控积压量。我一般会这样判断:如果外部调用平均响应小于 200ms 且只是查询,同步没问题;如果涉及写多个系统或调用链超过三个,优先考虑异步。这个判断标准不是万能的,但能帮你避免把整个系统做成一条同步的串行链路,扩展性自然也就上来了。
3. Sizing:把资源估算从“拍脑袋”变成“算得清”
3.1 Sizing 到底在算什么:并发、吞吐、数据增长三件事
很多人在做资源规划时,上来就问“你能不能帮我预估一个 ABAP environment 实例够不够用”,但没人能只凭一个模糊的业务描述给出靠谱答案。Sizing 的本质是把业务负载翻译成平台资源的消耗。在 ABAP environment 里,资源通常按计算单元(compute unit)和相关内存、磁盘规格来购买,不同服务计划的规格不完全一样,但分析方法是一致的。
你需要从三个维度来建模。第一个是并发:同一时刻有多少用户在操作你的服务、有多少外部系统在调用你的 API。第二个是吞吐:高峰时段每小时要处理多少请求,每个请求大概要执行多少次数据库读、多少条写、多少外部调用。第三个是数据增长:业务表每个月新增多少行,历史数据保留多久,这个直接决定了 HANA 的内存和存储需求。
很多人把这三个维度混在一起谈,结果要么过度估算造成成本浪费,要么严重低估导致上线就崩。我建议分开建表估算,同时标出峰值和均值——平台资源按峰值预留,成本核算按均值评估,这样至少不会出现“平均负载都正常、一到月底报表就 OOM”的尴尬。
3.2 工作负载矩阵:一张表把业务量换算成资源消耗
我在实际项目里习惯先做一张工作负载矩阵。行是核心业务场景或 API,列是频率、单次数据库读次数、单次数据量、CPU 密集程度、内存峰值、外部依赖延迟。这个矩阵不需要精确到字节,但需要让团队能横向比较不同场景的资源消耗占比。
| 业务场景 | 每小时调用量 | 单次SQL读次数 | 单次返回数据量 | CPU密集度 | 外部依赖 |
|---|---|---|---|---|---|
| 库存预留查询 | 2000 | 50 | 30 KB | 低 | 无 |
| 库存转移创建 | 300 | 200 | 10 KB | 中 | S/4HANA |
| 批处理对账任务 | 10 | 50000 | 5 MB | 高 | 文件存储 |
| 外部系统回写 | 500 | 80 | 20 KB | 中 | 无 |
有了这张表,你就能做换算。比如“库存转移创建”每小时 300 次,每次 200 次 SQL 读,合计 6 万次 DB 操作,加上 S/4HANA 外部调用延迟,可以推算出这个场景需要的数据库吞吐和网络带宽。SAP 官方在计算时通常以事务类型和用户数为基础,但项目初期你很难拿到“标准用户数”这种数据,工作负载矩阵反而是最贴近实际情况的估算手段。
换算时需要一个参考基准。一般云平台的 compute unit 说明里会给出该规格适合的负载描述,你可以把矩阵里所有场景的总吞吐量加起来,除以单个计算单元的经验吞吐能力,得到初始实例数。具体数值以你实际购买的服务计划为准,我通常的做法是先按这个数字的 70% 利用率来预留,不是把计算单元用满——因为一旦 CPU 长时间超过 70%,响应时间就开始明显劣化,再往上叠加业务波动,性能雪崩是迟早的事。
3.3 上线后的验证与校准:让 Sizing 跟着监控走
Sizing 不是一次性的估算,它必须在系统上线后通过实际监控来校准。ABAP environment 部署到 BTP 后,你可以从平台层的指标看到应用容器的 CPU 使用率、内存使用率、HTTP 响应时间,以及数据库层的 HANA Cloud 资源指标。如果上线后 CPU 长期超过 80%,说明初始估算偏低了;如果一直不到 30%,说明成本可能花多了。
我更推荐在正式上线前做一次基线压测。不用特别复杂的工具,选一两个核心读场景、一两个核心写场景,按 Sizing 矩阵里的峰值吞吐量去压,观察响应时间和资源水位。压测结果如果与估算偏差超过 20%,就回头检查工作负载矩阵里哪一项估错了,是调用频次高了,还是单次 SQL 读次数比预想的多。
还有两个容易忽略的坑。第一个是高可用与冗余:如果你要求更高的可用性等级,通常需要部署多个实例或环境,Sizing 时必须把冗余系数算进去。第二个是数据增长带来的内存压力:HANA Cloud 是列式内存数据库,数据量的增长对内存的消耗往往比想象中快得多,尤其是明细表。我建议在 Sizing 的初始阶段就预留未来 12 个月的数据增长空间,否则你可能上线几个月后就要为加内存而重构数据模型,那就被动了。
4. 性能反模式治理:我把踩过的坑都列给你
4.1 高频反模式清单与代码级对照
性能反模式这个词听起来很学术,实际上就是在说“那些看起来正常、跑起来要命的代码写法”。在 ABAP environment 里,我梳理了几类最高频的,几乎每个迁移项目都能碰到。
最常见的是 N+1 查询,也就是在循环里逐条读取数据库。比如你在 LOOP 里对每一行做 SELECT SINGLE,本来一条查询能解决的问题变成了几百条。这在本地数据库时代尚且能忍,在云环境里网络延迟和数据传输成本被放大后,直接能把一个简单的查询拖成秒级响应。
" 反模式:循环内逐行查询 LOOP AT lt_orders INTO ls_order. SELECT SINGLE status FROM zorder_status WHERE order_id = @ls_order-order_id INTO @lv_status. ls_order-status = lv_status. MODIFY lt_orders FROM ls_order. ENDLOOP. " 修正:一次 IN 查询批量读取 SELECT order_id, status FROM zorder_status FOR ALL ENTRIES IN @lt_orders WHERE order_id = @lt_orders-order_id INTO @DATA(lt_status). SORT lt_status BY order_id. LOOP AT lt_orders INTO ls_order. READ TABLE lt_status INTO DATA(ls_status) WITH KEY order_id = ls_order-order_id. IF sy-subrc = 0. ls_order-status = ls_status-status. ENDIF. MODIFY lt_orders FROM ls_order. ENDLOOP.第二类是数据传输过度,典型表现是 SELECT * 或者把整张大表拉到应用层再过滤。要记住:ABAP environment 里的数据库是 HANA Cloud,它的强项是列式存储和并行计算。你完全可以在 CDS 视图里把过滤条件、关联关系、聚合计算都定义好,让数据库干活。我在代码评审时看到 SELECT * 基本是零容忍的,因为它不仅浪费内存和带宽,而且会让 Sizing 时对数据量规模的估算完全失真。
第三类是逐行写库。有些人写批处理时习惯在 LOOP 里一条条 MODIFY,但在云架构里,单条数据库事务的往返开销是最大的成本之一。你应该把数据一次性收集到一个内表,再用批量 MODIFY 一次提交,必要时结合行为定义的 FOR ALL INSTANCES 方法实现。逐行写库不仅慢,还会让数据库锁的持有时间变长,并发一高就容易锁等待。
第四类是循环内外部调用。在 LOOP 里一个接一个调用 HTTP destination 或 RFC 到 S/4HANA,每一个调用都是秒级网络延迟,几百个订单跑下来接口直接超时。正确做法是先确认外部系统是否支持批量接口,支持就传一批参数;如果不支持批量,就要考虑并发调用,但要控制并发度,避免把对方系统打爆。
第五类是有状态滥用。ABAP 会话里存了大量上下文,导致会话无法被平台有效地复用和回收,资源越堆越高。这种问题在云环境里比经典环境更难排查,因为你不能像以前那样查看某个工作进程上挂着哪个用户。我通常在架构设计阶段就明确:所有 OData 服务默认无状态,只在极少数明确需要时才启用状态管理,并设置严格的生命周期控制。
4.2 怎么识别反模式:从日志和指标里找证据
反模式靠代码评审只能抓一部分,运行时问题还得靠证据。ABAP environment 不像经典环境那样能随手开 ST05 和 SE30,但你仍然有四条路拿到性能数据。
第一,平台层的容器监控指标,包括 CPU、内存、HTTP 响应时间。如果某个服务的 CPU 使用率异常高,而业务量没有明显增长,大概率有低效代码在空转。第二,应用日志。ABAP environment 里可以用应用日志记录每个请求的处理时间和关键步骤耗时,我在写代码时习惯对核心服务做一个耗时埋点:请求进来记一个时间戳,数据库操作完记一个,外部调用完记一个,最后算出各部分占比。这比任何监控工具都直观。
第三,数据库层的指标。HANA Cloud 提供数据库会话数、当前 SQL 执行情况、临时内存使用等指标。如果一个服务的内存水位很高,很可能不是因为业务量大,而是因为某个查询把大量数据加载到了临时表。第四,压测时的错误日志。压测不仅是看响应时间,更要看超时和 500 错误集中发生在哪种请求上——连接池耗尽、锁冲突、外部依赖超时,表现完全不同。
拿到这些证据后,你要做的事情是给每个反模式标注“影响面”和“优先级”。比如循环内外部调用通常是 P0,因为它直接影响吞吐上限;SELECT * 如果只影响一个低频报表,那就是 P2。不要试图一口气修完所有问题,先修影响最大的几个,再回头处理细节。
4.3 治理流程:从反模式清单到持续改进闭环
性能反模式治理不是一次性行动,而是一套持续流程。我建议从三个层面来做。
第一个层面是把反模式清单写进开发规范。这是最省力的手段。在我的团队里,代码评审时有一张固定的对照表:有没有循环内 SQL、有没有循环内外呼、有没有 SELECT *、有没有逐行写库、有没有无状态约束被破坏。评审时直接对照打钩,比一遍遍口头强调“注意性能”有效得多。ABAP 环境里的检查工具也能帮你做静态扫描,但人工对照清单仍然是最后的兜底。
第二个层面是运行时监控和定期巡检。每个月看一次核心服务的响应时间趋势、CPU 峰值、数据库内存增长曲线。不是为了写汇报,而是为了发现那些“缓慢劣化”的问题——比如某张表数据量涨了十倍导致一个原本能接受的查询变慢,或者某个批处理任务随着数据量增长开始拖垮其他服务。
第三个层面是每次发布前的基线回归。凡是改动核心数据模型或关键服务逻辑,都要跑一遍性能回归,对比改动前后的响应时间和资源消耗。这个流程听起来重,但实际执行时可以很轻:跑几个核心压测场景,对比关键指标即可。我见过太多系统因为一次“看起来很小的改动”导致性能回退,上线后才发现,那时再定位成本就高了。
5. 常见问题与排查技巧实录
5.1 连接管理与会话泄漏:为什么接口跑着跑着就超时
在 ABAP environment 里做外部集成时,最常遇到的问题是 HTTP destination 连接没有被正确复用或释放。你每次调用 HTTP 服务如果都新建一个客户端对象,又不显式关闭,最后连接池会被耗尽,后续请求全部排队等待超时。这个现象在高并发压测时特别明显:刚开始响应很快,跑几分钟后开始大量超时,CPU 占用却不高。
我的排查思路分三步。先看平台的连接数指标,确认是否达到上限;再看代码里是否每个请求都新建了 HTTP client,且没有正确 close;最后检查 destination 的配置,确认是否启用了连接池并设置了合适的超时时间。修起来其实很简单,但很多人根本不会想到去查连接管理,因为经典 ABAP 里 HTTP 调用不频繁,这个问题被掩盖了。
另外,你自己的 ABAP 会话也可能泄漏。如果代码里创建了运行时资源、锁对象、数据库游标,却没有正确处理,累积起来也会拖垮整个实例。云环境里没有经典 SAP 那种后台进程管理工具,所以只能靠代码规范来预防:能用局部变量就不用全局的,该释放的锁和游标一定要释放,至于会话状态更是能不存就不存。
5.2 OData 服务的深层“地狱”:$expand 带来的连锁查询
OData 是 ABAP environment 最主要的对外接口方式,也是性能问题的重灾区。最典型的场景是前端一个列表页,为了省事在请求里加了一串 $expand,把主对象、明细、明细的明细、相关联的仓库信息一次性拉出来。数据库可能要执行几十上百条 SQL,临时表膨胀,内存飙高。
我在一个项目里遇到过真实案例:客户写了一个三层 $expand 的查询,跑一次要 8 秒,而且调用一多,数据库内存水位直接告警。后来我们把查询拆成两个接口,第一个只返回主对象和必要字段,第二个按需加载明细,再加上缓存,响应时间从 8 秒降到 1.2 秒,数据库压力降了一半多。
这不是逼着前端多调接口,而是让接口更符合业务真实使用场景。大多数列表页第一眼只需要主数据,明细是用户点了某一行才需要。与其把一个巨型 payload 一次塞给前端,不如拆成按需加载。还有一个小技巧是检查 CDS view 的关联定义,看看有没有把不需要在 OData 里暴露的关联也带上了——很多时候性能问题根本不是代码写错,而是数据模型暴露了太多不必要的数据传输路径。
5.3 批处理任务的峰值抖动:为什么月末总有人来投诉
最后说一个特别容易忽略的问题:定时批处理任务的资源竞争。你有没有想过,为什么月末对账跑批的时候,在线业务也跟着变慢?原因很简单——批处理任务默认和在线服务共享同一个环境的资源池,而批处理往往吃满 CPU 和数据库连接,把在线请求挤到队列里排队。
这个问题的解法有几个层次。最简单的层次是错峰:把重负载批处理安排到业务低峰时段,比如凌晨,而不是白天。第二个层次是限流或分区:把一个大任务拆成多个小任务,分时段批量执行,降低瞬间资源冲击。第三个层次是在架构上做隔离:如果预算和技术条件允许,把批处理和在线服务部署到不同的 ABAP environment 实例里,彻底隔离资源,这属于比较彻底的治理手段。
5.4 问题排查速查表
| 症状 | 可能原因 | 优先排查方向 |
|---|---|---|
| 请求响应缓慢但CPU不高 | 外部依赖延迟、连接池排队、数据库锁等待 | 查看外部调用耗时和数据库会话状态 |
| 并发一高就有大量超时 | 连接未释放、有状态会话占用、锁粒度过大 | 检查HTTP客户端释放、会话状态、锁边界 |
| 数据库内存持续上涨 | 查询返回数据量过大、临时表膨胀、数据增长过快 | 检查SQL返回行数、$expand层级、Sizing模型 |
| 特定接口每秒请求量低但很慢 | N+1查询、循环内外部调用、SELECT * | 代码Review对照反模式清单 |
| 月末批处理时在线业务变慢 | 批处理与在线共享资源池 | 错峰、拆分任务、环境隔离 |
我个人在实际操作中还有一个很深的体会:Sizing 不是算一次就完事,性能反模式治理也不是上线前冲刺。如果你把架构设计想清楚,Sizing 才有意义;如果 Sizing 不跟着监控持续校准,后面的反模式治理就只是在救火。每次新项目我都是先花半天把工作负载矩阵填完,再花一天做基线压测,之后把反模式清单挂在代码评审和发布流程里。这套流程不复杂,但它让我在两个项目里避免了“上线一时爽、运维火葬场”的结局。先想清楚要支撑什么业务,再谈扩展性;先守住性能底线,再谈新功能——这个顺序别搞反。