SAP HANA负载管理这个词,很多人是在生产系统被一条失控SQL拖垮之后,才第一次认真研究的。内存数据库最大的卖点是快,但代价是所有会话都挤在同一个共享内存和计算资源池里,并没有天然的“分车道”概念。于是经常出现这种场面:报表在跑大聚合,批处理在做重算,某个接口突然发起全表扫描,交易屏上一条简单查询卡在原地转圈。这篇文章想把我这些年落地SAP HANA负载管理的经验从头到尾讲清楚,从workload class、workload mapping到statement level limit,再到上线前后的验证方法、排错思路和不少踩过的坑。无论你是刚接手HANA基架的运维,还是被业务连环追问“系统为什么变慢”的开发,都能从这套流程里找到可以直接照做的部分。
1. 负载管理到底在治什么病:混合负载下的资源争抢
1.1 混合负载的典型乱象
我之前配合过一个客户环境,一个实例上同时跑着交易系统的在线查询、报表系统的日结任务、以及若干外围接口的定时取数,典型的OLTP与OLAP混跑。按理说HANA内存数据库处理这些场景不会有太大压力,但问题恰恰出在“混跑”上。有一次某张事实表的统计任务在凌晨触发,单条SQL把CPU占用打到了90%以上,同时临时内存飙升,同一时间在线订单查询接口的响应时间从几十毫秒陡增到十几秒,运营侧直接来投诉。
这种问题并不是个例。HANA为了追求极致性能,会充分利用所有可用CPU核和内存做并行计算,一条设计不太好的SQL,比如多张列式大表做笛卡尔关联,就足以把整个实例的计算资源吃光。数据库本身不会主动去判断“这条语句是不是该让路”,因为默认情况下所有请求被平等对待。这就是负载管理的第一个价值:在数据库层面建立“交通规则”,不让个别语句和个别应用滥用资源。
1.2 为什么加内存加CPU解决不了
很多团队的第一反应是扩容。加内存、加CPU核,把机器规格拉高之后,短期确实能缓解,但本质问题并没有消失。我见过一台已经升过两次配置的机器,依然会因为某个月结批处理打满资源而出现大面积阻塞。原因很简单:硬件扩容只是把天花板抬高了,并没有限制任何一条语句可以冲到天花板之上的能力。
HANA的共享内存架构决定了请求之间没有物理隔离。不像传统应用服务,可以通过拆分实例来隔离故障域,HANA里所有会话都在同一个计算引擎上跑,一条语句吃掉的CPU时间、临时内存和网络带宽,都是其他会话失去的资源。如果不去限制“单条语句能消耗多少”,那么无论堆多少硬件,都可能被一条失控SQL瞬间打穿。负载管理在数据库层做的事情,本质上就是三个词:分类、配额、限时。给不同类型的业务划分不同的资源池和优先级,给单条语句设好可以消耗的资源上限,再给关键路径上的SQL设置执行时限。这比无限堆硬件靠谱得多,也便宜得多。
1.3 负载管理的目标和边界
负载管理解决的是“资源争抢时的优先次序”和“突发语句失控时的兜底”,而不是把所有慢查询都变快。它允许你制定规则:交易类查询永远优先,报表类批处理可以排队等资源,后台接口只能在空闲窗口使用较多CPU。同时,如果某条语句超过预设时间阈值,数据库可以直接终止它,避免它拖慢整个实例。这里是有一个清晰的边界的:如果业务本身SQL写得很差,比如缺过滤条件或索引失效,负载管理只能保护其他会话不被拖死,但那条坏SQL本身的性能不会因此变好。想清楚这一点,才不会对这项功能有不切实际的期待,上线之后也更容易和业务部门对齐预期。
2. 从组件到机制:workload class、mapping与语句限制怎么配合
2.1 workload class是资源策略的载体
SAP HANA的负载管理,核心第一步是创建workload class。可以把class理解成一张“资源策略标签”,它规定了进入这个标签的会话拥有什么优先级、最多能用多少CPU、最大并发数、内存限制以及语句级阈值。官方预置了几个class,比如SAP_DEFAULT、SAP_INTERNAL等,但真正要落地,需要按业务场景自定义。
class里几个关键字段值得仔细研究:
- WORKLOAD_PRIORITY:数字越小优先级越高。需要注意,HANA里1代表最高优先级,数字越大反而越“低人一等”。这一点和很多人直觉相反,我第一次配置时就差点搞反。
- CPU_NUMBER_OF_VIRTUAL_CPU / CPU_WEIGHT:限制该class内所有会话可用的虚拟CPU数量,或者通过CPU权重控制资源占比。
- MAX_CONCURRENCY:允许同时执行的语句数量,超过这个数后,额外请求会进入等待队列。
- MEMORY_LIMIT:会话级别的内存限制,超过阈值会触发指定动作。
- STATEMENT_LIMIT_TYPE / STATEMENT_LIMIT_THRESHOLD / STATEMENT_LIMIT_ACTION:语句超时限制以及触发后的动作。
给业务分类时,我习惯用一个类比:class就是车道。交易查询走快速车道,报表批处理走重载车道,运维人员走应急车道。每条车道都有自己的限速和车流量控制,谁也别想挤到别人的车道里去。
2.2 workload mapping是“谁进哪个车道”
光有class不生效,必须把会话映射到class上。这就是workload mapping的作用。映射条件可以基于会话用户、应用用户名、应用名、客户端主机、端口等属性。最常见的方式是按APPLICATION_NAME映射,因为应用名在连接串里容易统一管理。比如交易系统连接时设置APPLICATION_NAME=SAP_ORDER,报表系统设置APPLICATION_NAME=SAP_BW_REPORT,那么在mapping里就可以把对应应用名映射到不同的workload class。
mapping还有一个容易被忽略的点:多个映射规则同时命中时,HANA会按照映射的优先级和定义顺序选择最匹配的一条。这里要特别注意,SAP_DEFAULT是个兜底class,所有没被其他mapping匹配到的会话都会落到SAP_DEFAULT上。所以如果你在自定义class里把优先级配得很高,但mapping条件写得太严,比如要求精确匹配一个从来没出现过的APPLICATION_NAME,那么生产会话其实仍然全部跑在默认class里,等于什么都没做。这种情况我在客户环境里排查过不止一次。
2.3 statement level limit为什么是最后的底线
HANA默认对单条SQL的执行时间和资源消耗不设上限,这就像一辆没有刹车的车。负载管理中最好用也最容易被忽视的,就是statement level limit。它允许你把限制精确到“单条语句级别”,而不是整个会话级。可以按照执行时间、CPU时间、内存消耗或IO量来设阈值,比如“任何SQL执行超过120秒就记录日志并取消”,这就是很多生产环境急需的兜底能力。
触发后的动作有三种:LOG、CANCEL、KILL。LOG只记录不干预,适合灰度观察;CANCEL会主动取消语句;KILL则是强制终止整个会话。我一般强烈建议:第一次上线时先用LOG,观察一阵子,确认阈值合理后,再把高风险的业务切到CANCEL。一上来就KILL,很容易误伤正常的月结任务,而且如果会话被KILL,应用侧往往要等很久才能感知到连接已经断开,排障更麻烦。
2.4 静态参数与视图体系:另一条腿
动态的class、mapping体系可以随时调整,但HANA里还有一些静态参数也在负载管理中扮演角色,比如索引服务器配置文件里的max_concurrency、statement_memory_limit等,这些参数主要控制实例级的整体并发和单条语句内存上限。我通常把这两类手段分开理解:静态参数是“保险丝”,防止系统因为某种极端情况整体熔断;动态class是“红绿灯”,在平时就做好资源分配。两者配合使用,才能既保证日常稳定,又兜住极端场景。
此外要熟练使用系统视图M_WORKLOAD_CLASSES、M_WORKLOAD_MAPPINGS、M_ACTIVE_WORKLOAD_CLASSES、M_ACTIVE_STATEMENTS来观察实时状态。我排障时最常用M_WORKLOAD_CLASSES去确认当前活跃会话被分配到了哪个class,再用M_ACTIVE_STATEMENTS去看具体语句在等什么资源。没有这些视图的话,配置负载管理就像闭着眼睛开车,出了问题完全没法定位。
3. 实操落地:从现状分析到上线配置的完整路径
3.1 第一步:摸清家底,找到最痛的语句
任何负载管理项目,都不要跳过现状分析直接配参数。我一般先花半小时收集以下几类信息:
- 当前实例上有哪些应用在连接,分别使用什么用户名和应用名;
- 最近一段时间哪些语句执行时间最长、消耗CPU最多;
- 是否已经出现过资源争抢的事件,比如CPU打满、内存分配失败或锁等待;
- 业务侧对关键交易的响应时间预期是多少。
可以先用几条SQL快速看活跃语句和按时间排序的昂贵语句:
-- 查看当前活跃语句与所属会话 SELECT S.SESSION_ID, S.USER_NAME, S.APPLICATION_NAME, S.STATUS, S.CURRENT_STATEMENT FROM M_ACTIVE_STATEMENTS S ORDER BY S.START_TIME; -- 查看执行时间较长的语句(需要开启expensive statement tracing) SELECT STATEMENT_HASH, USER_NAME, APPLICATION_NAME, START_TIME, DURATION_MICROSECOND, STATEMENT_STRING FROM M_EXPENSIVE_STATEMENTS ORDER BY DURATION_MICROSECOND DESC LIMIT 50;如果M_EXPENSIVE_STATEMENTS查不到数据,需要先确认索引服务器配置中expensive statement的阈值是否开启以及阈值设得多大。这是个很常见的小坑,后面会专门讲。统计结果能直接告诉你:谁是大户,谁是偶尔超时的个例。把排名Top 5的语句捞出来后,负载管理策略基本就有方向了。
3.2 第二步:确定业务分类与优先级配额
拿上一节提到的客户环境举例,当时系统里有三类典型负载:在线交易查询、报表批处理、后台接口任务。我跟负责基架的同事一起把负载分成了三组,并给每组定了初始的配额策略。
| 业务分类 | workload class名 | 优先级 | 语句限制逻辑 | 初始动作 |
|---|---|---|---|---|
| 在线交易查询 | OLTP_CRITICAL | 1 | 超过60秒取消 | LOG |
| 报表批处理 | BW_REPORT | 7 | 超过600秒记录 | LOG |
| 后台接口任务 | INT_JOB | 5 | 超过180秒取消 | LOG |
优先级的数字不是随便拍的。在线交易直接影响用户体验,必须放在最高优先级;报表批处理一般可以容忍排队,但也不能让它彻底饿死,所以给了中等优先级;后台接口任务介于两者之间。语句限制的阈值我一开始都没敢设得太激进,统一用LOG观察一段时间再说。
这里想提醒一下,Max Concurrency等并发参数要谨慎设置。并发数设太小,可能造成业务请求排队时间过长;设太大,等于没限制。初始阶段可以先不动并发,只通过优先级和语句阈值把最明显的问题压住,等运行一个月有了真实数据,再回头调整并发配额。
3.3 第三步:创建workload class和mapping
分析和设计做完后,就开始动手创建。创建class的语法很直白,把需要的属性带上即可:
CREATE WORKLOAD CLASS "OLTP_CRITICAL" SET WORKLOAD_PRIORITY = 1, STATEMENT_LIMIT_TYPE = 'STATEMENT_LIMIT_THRESHOLD', STATEMENT_LIMIT_THRESHOLD = 60, STATEMENT_LIMIT_ACTION = 'LOG'; CREATE WORKLOAD CLASS "BW_REPORT" SET WORKLOAD_PRIORITY = 7, STATEMENT_LIMIT_TYPE = 'STATEMENT_LIMIT_THRESHOLD', STATEMENT_LIMIT_THRESHOLD = 600, STATEMENT_LIMIT_ACTION = 'LOG'; CREATE WORKLOAD CLASS "INT_JOB" SET WORKLOAD_PRIORITY = 5, STATEMENT_LIMIT_TYPE = 'STATEMENT_LIMIT_THRESHOLD', STATEMENT_LIMIT_THRESHOLD = 180, STATEMENT_LIMIT_ACTION = 'LOG';接着创建mapping。当时应用连接串里的Application属性值并不统一,得先和各个应用团队确认清楚他们设置的APPLICATION_NAME到底是什么。确认之后再建映射规则:
CREATE WORKLOAD MAPPING "MAP_OLTP_CRITICAL" APPL_NAME 'SAP_ORDER' TO WORKLOAD CLASS "OLTP_CRITICAL"; CREATE WORKLOAD MAPPING "MAP_BW_REPORT" APPL_NAME 'SAP_BW' TO WORKLOAD CLASS "BW_REPORT"; CREATE WORKLOAD MAPPING "MAP_INT_JOB" APPL_NAME 'SAP_INTERFACE' TO WORKLOAD CLASS "INT_JOB";这里有个很重要的细节:HANA在评估mapping时,不同的mapping规则如果同时命中,会根据优先顺序选择。所以建议把最具体的规则放前面,把兜底性质的规则放后面。当时还专门加了一条针对运维账号的规则,把管理员会话映射到高优先级class,免得管理员排查问题时反被自己的负载管理策略误伤。
3.4 第四步:配置statement level limit与验证策略
配置完class和mapping之后,有个很容易漏掉的操作:确认会话真的被映射到了预期class。不能想当然。验证方法很简单,查询M_WORKLOAD_CLASSES视图:
SELECT CLASS_NAME, ACTIVE_SESSION_COUNT, WORKLOAD_PRIORITY FROM M_WORKLOAD_CLASSES;如果发现OLTP_CRITICAL的ACTIVE_SESSION_COUNT始终是0,而大量交易会话还挂在SAP_DEFAULT下,那就是mapping没有生效,需要回头看连接串里APPLICATION_NAME值是否匹配。这个步骤一定要在上线前完成,否则后续所有“配置成功”的结论都是空中楼阁。
确认映射生效后,把语句限制的action从LOG切到CANCEL就需要按业务影响评估。我的建议是:先让LOG状态跑至少一个完整业务周期,比如一整个工作日加一次批量任务,观察被记录下来的语句里,有哪些是真正失控的坏查询,有哪些虽然是长查询但业务上必须跑完。确认阈值不会误伤必要任务后,再针对高风险class执行切换:
ALTER SYSTEM ALTER WORKLOAD CLASS "OLTP_CRITICAL" SET STATEMENT_LIMIT_TYPE = 'STATEMENT_LIMIT_THRESHOLD', STATEMENT_LIMIT_THRESHOLD = 60, STATEMENT_LIMIT_ACTION = 'CANCEL';我个人不太建议在切换CANCEL的同时把阈值也一起往下调。一次性改变两个变量,出了问题根本说不清是被误杀了,还是阈值收得太紧。每轮只动一个变量,这是运维的基本素养。
3.5 第五步:回滚预案
配置负载管理,回滚方案一定要提前想好。好在这套机制是动态的,回滚比静态参数要容易很多。需要临时恢复原状时,把class属性改回去,或者直接删掉mapping规则:
-- 暂时让某类会话不再走自定义class DROP WORKLOAD MAPPING "MAP_INT_JOB";生产上如果出现大量业务积压、误杀严重,最快的动作是把对应class的语句限制action改成LOG,而不是删掉class本身。因为DROP DDL操作本身也要占用系统资源,在高负载场景下还可能出现等待。所以我的建议是先调整行为,再慢慢清理配置。另外,所有class和mapping的创建SQL都建议留档保存,方便追溯和批量重建。
4. 常见问题与排错实录:那些文档里不会写的事
4.1 配了class但一个会话都没匹配上
这个问题出现的频率高得惊人。原因通常是连接串里设置的应用名和mapping里写的应用名不一致。比如应用团队在连接串里写的是SAP_ORDER_v1,mapping里写的是SAP_ORDER,结果完全匹配不上,会话全部落到SAP_DEFAULT。排查时不要只看连接串的默认值,最好在应用侧临时输出会话的SESSION_USER和APPLICATION_USER,或者直接查M_ACTIVE_STATEMENTS中该会话的属性对比。
另一个隐蔽原因是大小写敏感。HANA里mapping的对象值比匹配是区分大小写的,SAP_Order和SAP_ORDER是两个完全不同的值。遇到过几次这种情况,应用团队说“我明明设了Application属性”,但实际设的是小写开头,最后统一改为大写才正常。
4.2 设置了语句限制但长查询照样跑
配置了STATEMENT_LIMIT_THRESHOLD之后,发现某些超长语句依然没被取消,先别急着怀疑功能没用。优先检查三件事:该class的STATEMENT_LIMIT_ACTION是否还是LOG;语句是否以系统内部任务或后台任务方式运行,部分内部操作不受普通语句级限制约束;查询会话是否真的进入了对应class。其中第三点最容易被忽略,很多人只看mapping建了没建,不看会话实际落在哪个class。我每次排查都会先把M_ACTIVE_WORKLOAD_CLASSES和M_WORKLOAD_CLASSES两张视图对一遍,排除“规则没生效”之后再谈其他。
还有一点要注意,statement level limit在个别版本里对某些复杂内部并行子任务的判定不完全一致。如果升级了HANA版本,建议把该类参数的行为重新测试一轮,不要假设旧配置在新版本里依然有完全一样的效果。
4.3 限制过严导致业务投诉
把STATEMENT_LIMIT_ACTION直接设成CANCEL,阈值又设得比较紧,很容易接到业务电话:“我的报表为什么跑到一半断了?”这种情况十有八九是阈值的取值只看了单次采样的峰值,没有观察完整业务周期。比如某条批处理在月初数据量特别大时要跑15分钟,平时只要2分钟,如果你按平时2分钟的一半设了60秒限制,月初必然出麻烦。所以阈值设计要留足余量,或者按不同业务周期分时段动态调整。还要养成先LOG一周再CANCEL的习惯,这个习惯能帮你避开绝大多数误杀投诉。
一旦真出现误杀,处理方式也很简单直接:先把对应class的action改回LOG,再和业务确认实际需求,重新定阈值。不要为了“面子”硬扛,生产系统的第一原则是稳。
4.4 连接池和Application User的隐形问题
很多Java应用会使用连接池复用数据库会话。这时SESSION_USER通常是技术账号,而APPLICATION_USER可能长期保持为空,或者被统一设成连接池初始化时的一个值。如果你用APPLICATION_USER做mapping条件,很可能把原本不同类型的业务全部映射到同一个class。我遇到过连接池场景下application user被统一设置为某中间件服务账号,结果所有应用流量全部涌进一个class,直接把在线交易带上高延迟。后来改用APPLICATION_NAME映射,并让应用在连接串中显式传值,问题才解决。
所以我的建议是:mapping条件优先使用APPLICATION_NAME,它是应用连接串里最容易控制和区分的属性。如果确实要按用户维度分,务必确认应用侧每个业务线使用的数据库账号是否互不相同。
4.5 视图与参数的小坑:数据不刷新与权限不足
查询M_WORKLOAD_CLASSES、M_ACTIVE_STATEMENTS时,偶尔会遇到数据看起来不是最新的情况。Bloom filter或者统计信息延迟是一个原因,但更常见的是系统监控视图本身的刷新周期和你的查询时间点不一致。遇到类似情况,不要马上断言“系统没生效”,等一两分钟再查一次,或者直接用M_ACTIVE_STATEMENTS查单条语句的DURATION变化来交叉验证。
权限问题也值得一提。这部分内容在官方文档里很不起眼,但实际运维中非常关键:普通开发账号即使能连上数据库,也不一定有权限查看其他会话的SQL文本。排查负载管理问题时,建议用一个具备系统监控权限的专用账号,否则在M_ACTIVE_STATEMENTS里看到的语句内容全是空的,排查效率会大打折扣。
最后说点实在的
负载管理不是配完一次就结束的静态工程。我自己的习惯是:上线后第一周每天都看一次M_WORKLOAD_CLASSES和M_EXPENSIVE_STATEMENTS的增量变化,确认哪些语句被记录、哪些class出现了等待;一个月后对比业务反馈再做第二轮阈值调整。大多数团队一开始都只是想把“失控SQL拖垮系统”这个问题解决掉,但跑顺了之后会发现,负载管理更大的价值在于让不同类型业务之间有了清晰的资源边界,新应用上线时也能提前预估它对现有系统的影响。配置规则的时候宁可先松后紧、先LOG后CANCEL,哪怕多观察几天,也比一晚上把所有长查询都杀掉要稳妥得多。这套方法论不复杂,最难的是在每个决策节点上都忍住“尽快见效”的冲动,把每一步验证扎实。