前事不忘,后事之师,不忘国耻!

 找回密码
 用户注册
搜索
查看: 3|回复: 0

[开发应用] Sybase ASE 锁升级与隔离级别治理实战:从 1204 锁耗尽到分批提交与升级阈值双线收敛

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
Sybase ASE 锁升级与隔离级别治理实战:从 1204 锁耗尽到分批提交与升级阈值双线收敛


一、具体的问题

某套生产库,ASE 15.7(16.0 上逻辑完全一样),页大小 4K,明细表 item_detail 一千二百万行,建表时用的是 datarows 锁方案。夜间对账批处理跑完之后,第二天上午十点往后,业务端开始零星报错:
  1. Msg 1204, Level 20, State 1:
  2. ASE has run out of LOCKS. Re-run your command when there are fewer
  3. active users, or contact a user with System Administrator (SA) role
  4. to reconfigure ASE with more LOCKS.
复制代码

比这个更难受的是新连接根本连不上,客户端直接抛:
  1. Process with spid 214 could not connect to the ASE which has
  2. temporarily run out of locks.
复制代码

也就是说,锁一耗尽,不是某个慢语句失败,而是整个实例暂时拒绝提供服务。

运维的处理路径非常典型,三步走:先把 number of locks 从 10000 加到 200000,安静了一周;再提到 1000000,安静了两周;最后加到 2000000,锁内存吃到三百多兆,第二天照旧报 1204。而且这一次连 dbcc 都卡住不返回,只能找窗口重启。

真正的问题从来不在锁的数量。它藏在两个地方:批处理的事务边界,以及这张表的行锁升级阈值。

二、核心原理

1. 行锁表上的锁是怎么长起来的

datarows 表的每一行被读写都是独立的锁结构,ASE 内核里每个锁占 184 字节。这不是一个可以忽略的数字:配到 200 万锁,光锁结构就是 200万 × 184 字节 ≈ 351MB 内存。这也是为什么加锁数必须同步看 max memory,否则直接报 5861:
  1. The current 'max memory' value '1792000', is not sufficient to change
  2. the parameter 'number of locks' to '3500000'.
复制代码

把锁数当成一个可以随便拧大的旋钮,是这个故障反复复发的起点。

2. 锁升级只有两级,判定有三个阈值

ASE 的锁升级(lock promotion)规则很干脆:行锁升级成表锁,没有行锁升页锁这一步。判定用三个参数,LWM(低水位)、HWM(高水位)、PCT(百分比):


  • 当前扫描会话在表上的锁数 小于 LWM:不尝试升级;
  • 大于 HWM:立刻尝试升级为表锁;
  • 处于两者之间:锁数超过「表行数 × PCT%」时尝试升级。


出厂默认:row lock promotion LWM = 200、HWM = 200、PCT = 100。注意 LWM 最小值为 2,PCT 取值范围 1 到 100。

3. 三个致命的细节

第一,计数是「每次扫描会话」重新开始的。一个事务里写六十条 DELETE,每条语句各自计数;单条最多锁住一百九十行,低于 HWM 200,永远达不到升级条件,而整个事务维度的锁数已经累到一万一千多个。锁就这么一条一条堆上去的,跟表的行数没关系,跟语句条数有关系。

第二,升级尝试失败不会重试。白天有其他会话持着冲突锁,升级必然失败,ASE 不排队不等待,继续老老实实拿行锁。这解释了为什么「夜里批处理没事、白天报错」——夜里升级能成功,白天升级失败后行锁继续累积。

第三,调大锁数是纯缓解:任务形态没变,锁峰值没变,只是把水位抬高,顺带把内存顶到墙根。

4. 隔离级别这笔账

ASE 会话默认隔离级别 1(read committed),读的时候加短时共享锁,语句结束就放。级别 0 是脏读,读不加锁;级别 2(repeatable read)把共享锁持到事务结束;级别 3(serializable)还要封住扫描位置防幻影插入,锁量基本翻倍。

只想让报表查询别挡住批处理,at isolation 0 或 readpast 就够了。全局把隔离级别开到 2 或 3,等于用并发换心理安慰。

三、实例参考(动手步骤)

步骤 0:确认现状基线
  1. -- 锁资源水位:Pct_act 顶到 100、Num_free 为 0 就是耗尽
  2. sp_monitorconfig "number of locks"
  3. go
  4. -- 谁在吃锁:找 LocksHeld 的个位数大户
  5. select SPID, LocksHeld, BatchCount, CPUTime, LineNumber
  6. from   monProcessActivity
  7. where  LocksHeld > 1000
  8. order  by LocksHeld desc
  9. go
  10. -- 这张表到底是什么锁方案
  11. select lockscheme("item_detail")
  12. go
  13. -- 服务器级升级阈值
  14. sp_configure "row lock"
  15. go
复制代码

判据:LocksHeld 集中在个别 SPID 上是单会话问题,不是全局并发问题;lockscheme 返回 datarows 且是批处理主力表,就重点查升级阈值。

步骤 1:抓现行,证明是「事务内累积」而不是「单语句过多」
  1. -- 在监控会话里跑,每 10 秒采一次批处理会话的持锁数
  2. create table lockwatch(spid int, locks_held int, sampled_at datetime)
  3. go
  4. while 1 = 1
  5. begin
  6.     insert lockwatch
  7.     select SPID, LocksHeld, getdate()
  8.     from   monProcessActivity
  9.     where  SPID = 178
  10.     waitfor delay "00:00:10"
  11. end
  12. go
复制代码

采五分钟就能看出形状:如果 locks_held 是一条一条的阶梯往上爬,每爬一截正好对应一条 DELETE 执行完,说明锁是事务内累积的;如果单条语句就把锁数顶到几万,才需要去看执行计划。

步骤 2:改事务边界,这是治本的第一条

把「一个事务里六十条 DELETE」改成「每批八百行提交一次」:
  1. set rowcount 800
  2. go
  3. while 1 = 1
  4. begin
  5.     begin tran
  6.     delete item_detail
  7.     where  batch_no = @batch
  8.     and    flag = 0
  9.     if @@rowcount = 0
  10.     begin
  11.         commit tran
  12.         break
  13.     end
  14.     commit tran
  15. end
  16. go
复制代码

提交一次,事务级行锁全部释放,锁峰值由「几十条语句累积」变成「单批八百行」。这一步通常能砍掉九成的锁占用,而且不需要重启、不需要停机,是最该先做的一步。

步骤 3:下调这张表的升级阈值
  1. -- 在目标库内执行;阈值按 表 > 数据库 > 服务器 的优先级覆盖
  2. sp_setrowlockpromote "table", item_detail, 50, 100, 30
  3. go
复制代码

含义是:锁数到 50 就开始判,超过表行数的 30% 或者超过 100 个就直接尝试升级为表锁。对批处理来说,拿到一次表锁比拿一万个行锁便宜得多——锁结构省了,升级判断也不再被「单语句锁数上不去」卡住。

反例是把它设成 0、0、0 想「永不升级」,那是把偶发的 1204 变成常态。

步骤 4:读侧降到隔离级别 0,或者用 readpast
  1. -- 报表、巡检这类脏读可接受的查询
  2. select count(*) from item_detail at isolation 0
  3. go
  4. -- 队列式取数:跳过被锁住的行,不排队
  5. select top 50 * from task_queue readpast
  6. go
复制代码

注意 at isolation 只能加在单条查询或 declare cursor 上,不能用在子查询、insert ... select、create view、带 into 的语句里;也不能和 holdlock、noholdlock 混用,混用时 ASE 会告警并忽略 at isolation。

步骤 5:把锁数调回合理值,并加兜底护栏
  1. -- 先看 max memory 够不够,不够就先加大,再改锁数
  2. sp_configure "max memory"
  3. go
  4. sp_configure "number of locks", 500000
  5. go
  6. -- 16.0 SP02 及以上:限制单会话锁数,超限中止事务而不是拖垮整库
  7. sp_configure "allow resource limits", 1
  8. go
  9. -- 参数顺序以官方文档为准,典型写法:
  10. sp_add_resource_limit null, "at all times", "lock_count", 200000, 2, 3, null
  11. go
复制代码

最后把监控挂上:每五分钟跑一次 sp_monitorconfig "number of locks" 落表,Pct_act 超过 80% 就告警;每周跑一次 sp_sysmon 00:10:00,看 Lock Management 那一节的 Lock Promotions 行,确认升级是真的在发生。

治理前后对比

指标治理前治理后
number of locks 配置2,000,000500,000
锁结构内存占用351MB88MB
锁峰值 Max_Used顶格 2,000,00046,000
Pct_act100%4.6%
1204 报错日均 26 次0
连接被拒日均 9 次0
批处理事务持锁11,400 个行锁1 个表锁
批处理耗时3 小时 18 分1 小时 52 分
max memory 余量800MB(已顶墙)2.6GB


批处理反而变快了,这个结果一开始团队不信。原因是行锁本身有维护成本,一万多个锁结构的申请、释放、哈希查找,比一次表锁贵得多。

四、实操检查清单


  • [ ] 用 sp_monitorconfig "number of locks" 确认当前 Pct_act 与 Max_Used,留一张基线表
  • [ ] 对高频批处理表执行 select lockscheme("表名"),确认是 datarows 还是 allpages
  • [ ] 用 monProcessActivity.LocksHeld 排序列出锁大户,确认是单会话还是全局并发
  • [ ] 统计批处理事务里的语句条数,凡是「一个事务几十条 DML」一律视为高风险
  • [ ] 所有批量 DELETE/UPDATE 改成 set rowcount N 或 top N 分批,N 取 500 到 1000
  • [ ] 分批循环里确认 @@rowcount = 0 才 break,避免死循环空转
  • [ ] 对批处理主力表执行 sp_setrowlockpromote "table" 下调阈值,参数组合写进变更单
  • [ ] 报表与巡检类查询统一加 at isolation 0 或 readpast,并在代码评审里卡住
  • [ ] 检查 max memory 与 total logical memory,保证前者大于后者,别踩 5861
  • [ ] 加锁数时按「每并发 20 个锁」起步估算,并预留并行查询的三分之一余量
  • [ ] 配置采样任务,Pct_act 超 80% 告警;不要把 1204 交给用户来发现
  • [ ] 变更后观察三个工作日,覆盖「白天高峰 + 夜间批处理」两个时段再收尾


几个容易踩的坑

一、以为「200 行以上就升级」。默认 LWM 和 HWM 都是 200,但计数是每条语句各自扫描会话重新计。一个事务六十条语句,单条最多 190 行,永远不升级。这个坑几乎每次都有人踩。

二、表级阈值覆盖数据库级,数据库级覆盖服务器级。更要留意的是存储位置:数据库级阈值存在 master..sysattributes,表级存在用户库的 sysattributes。dump/load 到新服务器后,表级阈值跟着库走,数据库级阈值不会跟过去。迁移完发现「阈值莫名失效」,多半是这个原因。

三、PCT 的分母是表行数,不是页数。行锁表的 PCT 是按行数算的,照抄页锁表的经验去算,会得到完全错误的预期。

四、升级失败不排队重试。白天有冲突锁,升级尝试直接失败,ASE 继续发行锁。所以「白天报错、夜里正常」并不是随机抖动,而是升级成功率在两个时段不同。

五、1204 之后不一定是报错,可能是挂住。锁耗尽后某些命令会 hang 在那儿不返回,连 dbcc 都挂(SAP Note 1966154 记录的正是这个现象)。这时候再怎么等也没用,只能定位并 kill 会话。

六、加锁数不看 max memory。锁数改大会同步增加逻辑内存需求,max memory 不够就报 5861,改不动。顺序必须是先加 max memory、再加锁数。

七、千万不要为了「省事」全局开隔离级别 2 或 3。级别 2 的共享锁持有到事务结束,级别 3 还要封扫描位置,锁量翻倍、并发塌方。只对确实需要的那几条语句用 holdlock 或 at isolation serializable。

八、改锁方案(alter table ... lock allpages)必须配 reorg rebuild,大表上是重量级操作。小热表值得改,大表改之前先算好停机窗口,别在业务高峰动手。

九、分批提交后忘了处理批与批之间的可见性。中途失败会留下半完成状态,需要水位字段做断点续跑,或者整批幂等重跑。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-11-14 08:03 , Processed in 0.045174 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表