搞数据库运维的朋友都知道,数据库跑得慢,很多时候不是硬件不行,而是它在“等”。等磁盘把数据读上来,等一把锁被释放,等网络把消息送回来。这些“等”在达梦数据库里,就是等待事件。把等待事件盯住了,性能问题基本就能找到线头。这篇文章不讲大而全的监控平台,就聊怎么用一个小脚本,聚焦I/O瓶颈和锁竞争这两类最常见的等待,做到轻量级告警和快速定位异常。
一、为什么盯着等待事件?
等待事件不是一堆冷冰冰的数字,它更像是数据库自己的“行车记录仪”。每一次会话只要在等什么东西,都会记录下来:等了多久、等的什么、谁在等。你不需要去猜数据库哪里慢,直接看等待事件就知道它卡在哪。
1.1 数据库也会“堵车”
想象一下,你开车上班,最怕的不一定是车坏,而是堵在路上。数据库也一样,它的CPU、内存、磁盘都够快,但一旦大量任务同时去抢一个资源,大家就都堵住了。等待事件就是告诉你“堵点在哪个路口”。比如I/O等待,就好比所有车都在等一条很窄的收费通道,过一辆要半天。锁等待也好理解,就像几个人抢一个厕所,谁先进去,其他人只能在外面等。
1.2 两类最常见的坑
在生产环境里,最常见的等待事件就两拨:一拨是I/O相关的,比如“db file sequential read”,意思是单块读很慢,通常是磁盘性能不行,或索引设计有问题;另一拨是锁相关的,比如会话A更新了一行数据,还没提交,会话B想去更新同一行,就只能干等着。这两类问题如果不监控,往往要等到业务方投诉“系统卡死”才发现,那时候已经很被动了。
二、监控前的准备工作
要搭建监控,不用上来就搞一大堆服务器和中间件,只要达梦数据库本身提供了动态视图,再加一个小脚本,就能跑起来。
2.1 达梦数据库的动态视图
达梦数据库提供了一些“体检表”,按名字大概能猜到作用:
V$SYSTEM_EVENT:系统级别的等待事件汇总,比如某个等待事件一共发生了多少次、累计等待了多少秒。V$SESSION_WAIT:当前正在发生的等待,能看到每个会话正在等什么、已经等了多少秒。V$LOCK:锁信息,帮助分析锁竞争。
这些视图就像汽车的仪表盘,平时不亮灯,一旦有问题,数值就会往上跳。
2.2 需要一个只读账号
监控脚本用普通账号连库就行,千万别拿 SYSDBA 去跑定时任务。权限给最小化,能查这几个视图就够了。由于这里我们统一用 Python 示例,创建账号的 SQL 就不单独列了,你可以让 DBA 帮忙开一个只读账号,或者用安全管理工具配置。
三、写一个轻量级监控脚本
3.1 技术栈:Python + dmPython
我们这次全部用 Python 3 和达梦官方驱动 dmPython,不需要额外装什么监控平台。脚本就做三件事:连库、查等待事件、超阈值就告警。
3.2 连接数据库
先建立连接。注意达梦默认端口是 5236,用户密码改成你自己的。
# 技术栈:Python 3 + dmPython(达梦官方驱动)
# 安装方式:pip install dmPython
import time
import dmPython
# 数据库连接配置,按实际环境修改
DB_CONFIG = {
"user": "monitor", # 只读监控账号
"password": "monitor123", # 密码
"server": "127.0.0.1", # 数据库IP
"port": 5236 # 达梦默认端口
}
def connect():
"""建立到达梦数据库的连接"""
try:
conn = dmPython.connect(**DB_CONFIG)
print("连接成功")
return conn
except Exception as e:
print(f"连接失败: {e}")
return None
3.3 采集I/O等待事件
I/O 等待事件通常带有 file 或者 log file 关键字。我们写一个函数,把系统里 I/O 类等待按累计时间排个序,方便看哪个最严重。
def get_io_wait(conn):
"""查询系统I/O类等待事件,按累计等待时间降序返回"""
sql = """
SELECT event_name, -- 等待事件名称
total_waits, -- 总等待次数
time_waited, -- 累计等待时间(秒)
average_wait -- 平均每次等待时间(秒)
FROM v$system_event
WHERE event_name LIKE 'db file%' -- 数据文件I/O
OR event_name LIKE 'log file%' -- 日志文件I/O
ORDER BY time_waited DESC
"""
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
cursor.close()
return rows
3.4 采集锁等待事件
锁竞争的特点是会话在 v$session_wait 里等待的事件名带 lock 或 enq。我们统计正在等待锁的会话数,以及最长的等待秒数。
def get_lock_wait_sessions(conn):
"""统计当前锁等待的会话数,以及最大等待秒数"""
sql = """
SELECT COUNT(*) AS lock_wait_count, -- 等待锁的会话数量
NVL(MAX(seconds_in_wait), 0) AS max_wait_seconds -- 最长的等待秒数
FROM v$session_wait
WHERE event LIKE 'lock%' -- 锁等待事件
OR event LIKE 'enq%' -- 队列类锁等待
"""
cursor = conn.cursor()
cursor.execute(sql)
row = cursor.fetchone()
cursor.close()
# 返回 (等待会话数, 最大等待秒数)
return row[0], row[1]
3.5 告警判断与主循环
阈值怎么定?这里给个示例:单个 I/O 等待事件累计超过 100 秒就告警;锁等待会话超过 5 个,或者最大等待超过 60 秒就告警。实际环境要根据业务和硬件调整。
def send_alert(message):
"""发送告警,这里用打印演示,实际可以接钉钉/企业微信/邮件"""
print(f"[ALARM] {message}")
def monitor_once(conn):
"""执行一次监控检查,发现异常就调用send_alert"""
# 1. 检查I/O等待
io_rows = get_io_wait(conn)
for row in io_rows:
event_name, total_waits, time_waited, average_wait = row
# 累计等待超过100秒,说明I/O大概率存在问题
if time_waited > 100:
send_alert(
f"I/O等待严重: 事件={event_name}, "
f"总等待={time_waited}秒, 平均等待={average_wait}秒"
)
# 2. 检查锁等待
lock_count, max_wait = get_lock_wait_sessions(conn)
if lock_count > 5:
send_alert(f"锁竞争线程多: 当前等待锁的会话={lock_count}个")
if max_wait > 60:
send_alert(f"锁等待超时: 最大等待={max_wait}秒")
def main():
conn = connect()
if conn is None:
return
# 每隔30秒检查一次,也可以改成5分钟
while True:
monitor_once(conn)
time.sleep(30)
if __name__ == "__main__":
main()
3.6 完整示例脚本
把上面的函数拼在一起,就是一个能直接跑的轻量级监控脚本。脚本会每 30 秒采集一次,并把异常信息打印出来。生产环境可以再加个钉钉机器人,把 send_alert 里的 print 换成 HTTP 推送就行。
# 技术栈:Python 3 + dmPython
# 功能:采集达梦数据库I/O等待和锁等待,超阈值告警
import time
import dmPython
DB_CONFIG = {
"user": "monitor",
"password": "monitor123",
"server": "127.0.0.1",
"port": 5236
}
def connect():
"""连接达梦数据库"""
try:
conn = dmPython.connect(**DB_CONFIG)
return conn
except Exception as e:
print(f"连接失败: {e}")
return None
def get_io_wait(conn):
"""查询I/O等待事件"""
sql = """
SELECT event_name, total_waits, time_waited, average_wait
FROM v$system_event
WHERE event_name LIKE 'db file%' OR event_name LIKE 'log file%'
ORDER BY time_waited DESC
"""
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
cursor.close()
return rows
def get_lock_wait_sessions(conn):
"""统计锁等待会话数和最大等待秒数"""
sql = """
SELECT COUNT(*), NVL(MAX(seconds_in_wait), 0)
FROM v$session_wait
WHERE event LIKE 'lock%' OR event LIKE 'enq%'
"""
cursor = conn.cursor()
cursor.execute(sql)
cnt, max_sec = cursor.fetchone()
cursor.close()
return cnt, max_sec
def send_alert(message):
"""告警输出,实际可替换为钉钉/邮件推送"""
print(f"[ALARM] {message}")
def check(conn):
"""执行一轮检查"""
for ev, waits, waited, avg in get_io_wait(conn):
if waited > 100:
send_alert(f"I/O等待: {ev}, 累计{waited}秒, 平均{avg}秒")
cnt, max_sec = get_lock_wait_sessions(conn)
if cnt > 5:
send_alert(f"锁等待会话数={cnt}")
if max_sec > 60:
send_alert(f"最长锁等待={max_sec}秒")
def main():
conn = connect()
if not conn:
return
# 生产环境建议定时调度,比如用crontab代替死循环
while True:
check(conn)
time.sleep(30)
if __name__ == "__main__":
main()
四、快速定位异常的套路
告警只是提醒你“出事了”,更关键的是快速查清谁在搞鬼。脚本已经知道是哪类等待,接下来继续用 Python 连接数据库,把具体会话捞出来。
4.1 当前谁在等I/O?
当 I/O 告警触发时,你可以执行下面的查询,找出正在等待磁盘的会话,以及它们对应的会话 ID。然后去查这个会话正在执行什么 SQL。
def find_io_waiting_sessions(conn):
"""找出正在等待I/O的会话ID和等待时间"""
sql = """
SELECT session_id, event, seconds_in_wait
FROM v$session_wait
WHERE event LIKE 'db file%'
ORDER BY seconds_in_wait DESC
"""
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
cursor.close()
for sid, event, secs in rows:
print(f"会话{sid} 正等待[{event}], 已等{secs}秒")
return rows
拿到会话 ID 后,可以再通过会话视图关联当前执行的 SQL。达梦的 v$session 里有 sql_text 或可以通过 v$sql 查询,这里不展开,但方向是对的。
4.2 当前谁在等锁?
锁竞争通常有一个“持锁者”和很多“等待者”。你可以在 v$lock 视图里按对象聚合,找到被争抢的那一行数据。下面用 Python 打印当前锁等待的会话和锁类型。
def find_lock_waiters(conn):
"""列出正在等待锁的会话ID、锁模式、等待时长"""
sql = """
SELECT w.session_id,
l.lmode, -- 锁模式,如6表示排他锁
w.seconds_in_wait
FROM v$session_wait w
JOIN v$lock l ON w.session_id = l.sess_id
WHERE w.event LIKE 'lock%'
ORDER BY w.seconds_in_wait DESC
"""
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
cursor.close()
for sid, lmode, secs in rows:
print(f"会话{sid} 申请锁模式{lmode}, 已等待{secs}秒")
return rows
实际处理时,你先找到一个正在等锁的会话,然后去 v$lock 里查谁阻塞了它,把那条阻塞事务回滚或者提交,等待自然会消失。
五、关联技术:等待事件分类与阈值设定
等待事件不仅仅是 I/O 和锁,还有 CPU 调度、网络延迟、日志刷盘等。但不管哪一类,监控的思路都一样:先看累计等待时间,再看实时等待会话。
阈值怎么定比较合理?不要拍脑袋,最好观察一周的正常数据,然后取平均值加两到三倍标准差。比如连续 7 天锁等待最大秒数都在 10 秒以下,那 60 秒显然就是异常信号。下面给一个小示例,用 Python 计算最近 N 次采集的平均值和标准差,帮助动态调整阈值。
# 技术栈:Python 3
# 功能:根据历史监控数据计算动态阈值(简单示例)
# 假设 history 是每次采集到的锁等待最大秒数列表
history = [5, 6, 5, 7, 8, 6, 9, 5, 6, 7]
def compute_threshold(values, multiplier=3):
avg = sum(values) / len(values) # 平均值
diff = sum((v - avg) ** 2 for v in values) # 平方差和
std = (diff / len(values)) ** 0.5 # 标准差
return avg + multiplier * std # 动态阈值
dynamic_threshold = compute_threshold(history)
print(f"动态锁等待阈值建议设为: {dynamic_threshold:.1f}秒")
这个思路可以用在你的监控脚本里,让告警阈值跟着业务周期走,而不是一成不变。
六、应用场景
这套轻量级方案特别适合以下场景:
一是没有专职 DBA 的小团队,不想上昂贵的监控平台,用一个小脚本就能把达梦的关键病症看住。二是已有云监控但粒度不够细的场景,比如云监控只看 CPU 和内存,数据库内部等待事件看不到,这时候脚本能补上盲区。三是做数据库性能巡检,每天跑一次,输出异常事件清单,让运维有据可查。
七、技术优缺点
先说优点。轻量,一个 Python 脚本就能跑,不引入额外的中间件;聚焦,直接看等待事件,能快速定位到 I/O 或锁问题;直观,告警信息带着具体事件名和数字,不用翻指标曲线猜半天;成本低,只读账号加定时任务就行。
再说缺点。它只能看到“症状”,不能自动给出解决方案,比如索引建议、SQL改写还是得人工分析。另外,采样频率和阈值需要不断调整,设置不好容易漏报或误报。脚本本身也不覆盖所有等待事件,比如网络类、内存类没包含,需要后期扩展。
八、注意事项
第一,给足权限但别给太多。监控账号只需要查询动态视图的权限,不要授予 DDL 或 DML 权限。第二,监控脚本不要跑太频繁,30 秒一次足够,否则查询动态视图本身也消耗数据库资源。第三,告警方式要靠谱,打印到控制台肯定容易漏,建议接入钉钉/企业微信/邮件,并且设置告警合并,避免半夜被轰炸。第四,重点关注那些“持续增长”的等待,而不是瞬时值。比如锁等待从 10 秒涨到 200 秒,肯定是事务没有及时提交,需要马上处理。第五,一定要保留历史数据,哪怕写到一个文本文件里,方便对比趋势和做复盘。
九、文章总结
达梦数据库的性能监控不需要搞得很复杂,抓住等待事件这个核心,就能小成本解决大问题。我们聚焦 I/O 瓶颈和锁竞争,用 Python 和 dmPython 写一个轻量级脚本,定时采集 V$SYSTEM_EVENT 和 V$SESSION_WAIT 的数据,超过阈值就告警。再配合查询当前会话的等待情况,就能快速定位是谁在拖慢数据库。
这种方案的灵魂在于“少而精”,不贪多,先把最影响业务的 I/O 和锁看住,后续再慢慢扩展其他等待事件。等你跑一段时间,优化了几个慢 SQL,解决了几次死锁,你会觉得:原来数据库监控可以这样简单。
评论
围绕“搭建达梦数据库性能监控体系时,聚焦输入输出瓶颈与锁竞争等关键等待事件,实现轻量级告警和快速定位异常。”参与讨论