凌晨两点多被电话叫起来处理一套交易库的性能问题,是很多做数据库运维的人共同的记忆。到了现场你会发现,数据库层面看起来一切正常:AWR 里的 Top SQL 排第一的语句执行时间也就几十毫秒,等待事件里 db file sequential read 占比不算离谱,连接数也在正常范围。但业务方咬死说系统慢,页面转圈转了十几秒。这种时候,问题往往不在数据库里面,而在它脚下那台机器上——CPU 被别的进程抢了、内存开始换页、磁盘队列排到了几十米开外。OSWatcher(全称 OSWatcher Black Box,下面统一简称 OSW)就是专门用来回答“机器当时到底在干什么”这个问题的一套工具。它本身不复杂,几个 shell 脚本加一堆 .dat 文本文件,但它解决的是数据库自带诊断能力覆盖不到的那一层。这篇内容面向已经会写 SQL、会用 AWR 报表,但对操作系统层指标还没形成体系认知的同行,也适合需要给性能问题留下可复盘证据的团队参考。
1. 数据库变慢时,为什么我第一个动作是去看操作系统
数据库的自己诊断能力是有盲区的,这一点很多人是在踩过坑之后才彻底接受的。AWR 告诉你“这个 SQL 花了 3 秒”,ASH 告诉你“会话在等 db file sequential read”,但这两个结论都在默认一个前提:操作系统层面的资源是充足且公平分配的。这个前提一旦不成立,AWR 里所有的数字都会变形,而且变形的方式很隐蔽——它会让你以为是数据库内部慢了,实际上是整个进程在排队。
1.1 AWR 报表现象和操作系统真实状态之间的偏差
举个具体的偏差。某套系统在业务高峰期出现大量 latch free 和 cursor: pin S wait on X 等待,从 AWR 看像是共享池配置有问题,于是有人去调 _shared_pool_size,调完之后更慢了。后来翻操作系统数据才发现,那段时间 CPU 的 run queue 排到了 30 多,机器只有 8 个物理核。真实的因果是 CPU 严重不足导致持有 latch 的会话得不到调度,等 latch 的会话自然越积越多。你看,AWR 记录的是结果,操作系统记录的是原因,两者的因果方向经常被搞反。
还有一种更常见的偏差来自内存。数据库进程的 RSS 在缓慢增长,AWR 里的 buffer cache 命中率一切正常,但机器已经开始用 swap 了。这时候数据库每一次读文件都会触发一次换页,响应时间从 0.5 毫秒跳到 20 毫秒,而 AWR 只会告诉你“IO 变慢了”,它不知道是物理磁盘慢还是内存换页导致的额外开销。这类问题如果只看数据库内部的报表,能查一周也查不出所以然。
我自己的习惯是:任何超过 30% 的性能回退,先不打开 AWR,先去翻 OSW 的归档目录,把问题时间段内 CPU、内存、IO、网络四类数据扫一遍。这个过程通常十几分钟就能完成,而它能帮你排除掉一大半“伪数据库问题”。顺序反过来做,代价是可能在错误的方向上花掉几个小时。
提示:OSW 的价值不在于它有多少指标,而在于它在你需要的时候“刚好有”。它的采集是常驻的,不依赖问题发生时才去抓现场——这一点决定了它和事后用 top、vmstat 手动抓数据完全是两回事。
1.2 OSW 到底采集了什么:一份清单和它的取舍
OSW 的原理说破了很简单:它用一层 shell 循环,按固定间隔调用操作系统上已有的性能命令,把输出按时间戳写成文本文件。它不是监控系统,没有数据库,没有前端,没有告警,就是一堆带时间的文本。正因为简单,它几乎没有资源开销,可以在生产环境长期跑着。
它默认采集的项目大致分这么几类,我把用途也一并写上,方便你在排查时知道该翻哪个目录:
| 采集目录 | 底层命令 | 主要用途 |
|---|---|---|
| oswvmstat | vmstat | CPU 队列、上下文切换、换页、IO 等待 |
| oswtop | top | 单进程级别的 CPU 和内存占用排序 |
| oswiostat | iostat -x | 每块磁盘的利用率、响应时间、队列深度 |
| oswmpstat | mpstat | 每个 CPU 核的使用分布,看是否单核打满 |
| oswmeminfo | /proc/meminfo | 内存构成,区分 cache 和真实占用 |
| oswps | ps | 进程快照,看进程数是否异常膨胀 |
| oswslabinfo | /proc/slabinfo | 内核对象缓存,排查内核态内存泄漏 |
| oswnetstat | netstat | 连接数、重传、网络队列 |
| oswprvtnet | /proc/net | 私网相关的网络统计 |
这份清单有个很明显的特点:它关心的都是“进程外部”的资源。数据库自己有多少 session、执行了多少次逻辑读,这些它一概不管,那是 AWR 的活。所以两者天然互补,谁也不替代谁。
需要说明的是,OSW 的采集项是可以在启动时通过采集器目录做裁剪的。有些人为了“采得全”,把能开的都开了,结果 top 和 ps 这两个采集器在进程数上万的主机上单个周期要跑一两秒,采集频率一高就形成了自己拖慢自己的局面。我的建议是默认项全开,但采集间隔不要低于 30 秒,进程数特别多的高并发主机可以考虑关掉 oswps,需要的时候再单独抓。
2. 部署看起来只是解压加启动,真正的难点在参数和空间
OSW 的安装过程用一句话就能说完:解压压缩包、改环境变量、跑启动脚本。听起来五分钟的事,但我在实际环境中见过因为目录规划不当,跑到第三天后磁盘写满把整个主机带崩的情况。所以这一段重点讲参数,而不是讲命令。
2.1 采集间隔和保留天数怎么折算成磁盘占用
这两件事是强关联的,必须先算清楚再启动。OSW 有两个核心参数:-i是采集间隔(秒),-f是归档保留时长(小时)。默认值通常是 30 秒和 48 小时。
磁盘占用的估算方法很朴素:单个采集周期的文件体积基本固定,采集频率决定了每天写多少个周期,两者相乘就是日增量。以一套中等规模的主机为例,全部采集器开启、间隔 30 秒的情况下,日增量大概在几百兆这个量级;如果主机上有几百个磁盘设备,iostat 的文件会显著变大,日增量翻倍也很正常。
所以我的做法是启动前先做一次小规模试跑:用 30 秒间隔跑 10 分钟,然后du -sh看一下归档目录体积,乘以 144 得到每日增量,再乘以想保留的天数,就是需要的空间。这个动作耗时十分钟,但能避免后面所有麻烦。
保留时长怎么定,取决于你的问题发现周期。如果团队的问题通常在业务方投诉后才被发现,那保留 48 小时往往不够——周末出的问题周一才发现,数据早就被滚动覆盖了。这种场景我一般会配到 96 小时甚至 168 小时,前提是磁盘空间算得过来。反过来,如果已经有一套实时监控能在分钟级发现问题,48 小时完全够用,因为数据只是用来复盘那一段的细节。
2.2 启动脚本的参数与开机自启的实际做法
启动脚本的参数不多,但每个都值得说清楚:
-i采集间隔,单位秒,不建议低于 15 秒,低于这个值采集本身的开销就不可忽略了-f归档保留小时数,按上面算出来的值填-l日志目录,建议和归档目录分开,避免日志把归档区写满-t归档目录,这个参数最容易被忽略,也最容易出事,如果不指定,默认会写到脚本所在目录下,很多人解压到家目录就启动了,结果系统盘被写满-a采集器目录,用于自定义采集项
关于开机自启,我的经验是不要图省事直接塞进 rc.local。原因是 rc.local 在很多发行版上默认不启用,而且执行时机在部分服务启动之前,OSW 依赖的一些命令可能还没就绪,会导致采集文件里出现大量空输出。更稳妥的做法是写一个独立的 systemd 单元或者用 crontab 的@reboot,前者更好管理状态,后者更简单。
用 crontab 的做法大概是这样,前提是脚本里的相对路径都改成了绝对路径:
@reboot /opt/oswbb/startOSWbb.sh 30 96 /opt/oswbb/log /opt/oswbb/archive # 每十分钟检查一次,进程不在就拉起来 */10 * * * * if [ ! -f /opt/oswbb/oswbb.pid ]; then /opt/oswbb/startOSWbb.sh 30 96 /opt/oswbb/log /opt/oswbb/archive; fi注意:检查进程是否存活要用 pid 文件而不是
ps | grep。用过 grep 的人都知道,grep 自己那一行经常会匹配到关键字,导致判断永远为真,进程死了也拉不起来。这个坑我看过太多次了。
2.3 装在数据库主机上,还是单独一台监控机
这是个需要认真权衡的决策,两条路各有代价。
装在数据库主机上的好处是数据最真实,任何资源争抢都能直接反映出来,而且不依赖网络。坏处有两个:一是主机彻底挂掉的时候,最后那几秒的数据也一起没了,而恰恰是那几秒最有价值;二是它和数据库抢同一份资源,虽然开销极小,但在资源已经极度紧张的场景下,任何额外负担都会被放大。
装在一台独立监控机上,通过远程方式采集,好处是主机宕机时数据仍然保留,且零侵入。代价是需要额外的网络和认证配置,采集到的是“从外面看到的”指标,某些内核态细节拿不到。
我个人的取舍是:核心生产库用独立监控机,普通业务库直接装在本地。理由很简单,核心库最怕的就是“出事时没有现场”,而普通库的问题多半是资源争抢型,本地采最准。
3. 归档目录里那些 dat 文件怎么读出门道
很多人拿到 OSW 的数据之后第一反应是找工具看图,其实在图上看到之前,用几个命令扫一遍文本文件往往更快。文本文件的好处是你能看到原始值,不会被图表的采样聚合和缩放误导。
3.1 文件命名规则与时间段对齐的方法
OSW 的文件命名是固定格式:<采集器名>_<年>.<月>.<日>.<时>.<分>.dat,比如oswvmstat_24.05.12.14.30.dat,表示这个文件从 24 年 5 月 12 日 14 点 30 分开始写。注意是开始时间,不是文件名里那个时间段的数据。一个文件里通常包含很多个采集周期,具体多少取决于文件大小上限。
文件内部,每个采集周期之间用一行固定的分隔标记隔开,形如zzz ***Sun May 12 14:30:05 CST 2024。这个分隔行是整个 OSW 数据里最重要的一个东西,因为它是你定位时间点的唯一抓手。想找某个具体时间点的数据,直接:
grep -n "zzz \*\*\*Sun May 12 14:35" oswvmstat_24.05.12.14.30.dat拿到行号之后用 sed 取那一段,就能得到那个时刻的完整快照。这个技巧比打开图表拖时间轴精确得多,尤其是在排查持续几秒钟的瞬时抖动时。
对齐时间这件事本身有个坑:数据库服务器和你的工作站可能不在同一时区,OSW 写的时间戳用的是主机本地时间,而数据库 AWR 里的时间可能是数据库时区。两者相差 8 小时的情况下,你会对着完全错位的数据分析半天。我的习惯是每次分析前先用date命令确认一次主机时间,并且在笔记里记录时区偏移。
3.2 三类高频信号在文本里的典型形态
不用图表也能看出来的问题,主要有这么三类,它们的特征形态很有辨识度。
第一类是 CPU 饱和。打开 vmstat 文件,关注r列(运行队列长度)和us+sy两列之和。健康状态下r应该长期小于 CPU 核数,如果持续出现 r 值超过核数 2 倍以上,同时 us+sy 接近 100,那就是明确的 CPU 饥饿。这时候再去看 oswtop,就能找到是哪个进程在吃 CPU——注意不一定是数据库进程。
第二类是 IO 瓶颈。iostat 输出里重点看两列:%util和await。%util 接近 100 说明磁盘设备一直忙,await 是这个设备上请求的平均响应时间(毫秒)。这两个要一起看:%util 高但 await 低,说明磁盘忙但响应正常,问题不大;%util 不高但 await 很高,说明请求在排队但队列不深,往往是单块盘性能下降或者存储侧的问题。这种“两个指标打架”的情况,正是需要 OSW 原始数据而不是聚合图表的地方。
第三类是内存压力。看 vmstat 的si和so两列(每秒换入换出页数)。只要这两列长期不为零,就说明系统在真的换页,这时候数据库的性能一定受影响,不管 AWR 怎么说。同时看 /proc/meminfo 的 available 字段,而不是 free 字段——Linux 上 free 把 cache 也算成已用,看起来永远紧张,实际可用的是 available。
3.3 用分析器出图的完整流程和常见卡点
OSW 配套有一个分析器(OSWbba),是一个 Java 程序,输入归档目录,输出图表和 HTML 报告。流程大概是:
# 1. 确认 Java 可用,版本不能太低 java -version # 2. 进入分析器目录,指定归档目录启动 java -jar oswbba.jar -i /opt/oswbb/archive # 3. 交互式输入起止时间,格式是 MM/dd/yy HH:mm # 4. 选择要生成的内容,一般选生成图表和数据文件常见的卡点有三个。一是 Java 版本,分析器对 JDK 版本有要求,太新的 JDK 可能跑不起来,太旧的也不行,遇到报错先看是不是这个原因。二是内存,图表生成阶段对堆内存有要求,归档目录特别大的时候需要加大-Xmx,否则会抛内存溢出。三是时间格式,这个输入框非常严格,少一位数或者用错分隔符都会失败,而且报错信息不友好,容易让人以为是数据损坏。
还有一点值得说:分析器生成图片时如果用了无图形界面的模式,某些配置下会报找不到显示设备的错误,这个和 Java 的图形库有关,加-Djava.awt.headless=true通常能绕过。
4. 三个现象的完整排查链路还原
理论讲完了,这一段用三个模拟场景把排查过程串起来,重点不是结论,而是每一步为什么这么走。
4.1 CPU 长期高位但业务方说速度正常
现象是监控平台对某套系统报 CPU 使用率超过 85%,持续了两小时,但业务方反馈一切正常。这时候最容易犯的错误是直接让开发去优化 SQL。
第一步应该看 mpstat,判断是全核均匀还是单核打满。如果所有核的 us 都接近 90%,那是真的算力不足;如果只有一两个核接近 100,其他核闲置,那要怀疑是不是某个单线程进程在做密集计算,或者中断集中在某个核上。这一步花不到一分钟,但方向完全不同。
第二步看 oswtop,找到占用最高的进程。如果头部不是数据库进程,而是某个备份程序、日志采集程序或者定时任务,那答案就很清楚了。实际环境中“数据库 CPU 高”这个说法十有八九是误判,数据库往往只是一堆进程中占得比较多的那个,但绝对值并不高。
第三步才是回到数据库,看这个时间段内的活跃会话数。如果会话数很低,说明数据库其实很闲,CPU 消耗来自后台进程(比如并行查询、归档、统计信息收集)。到这一步结论已经很明确了。
这条链路的关键在于:先用操作系统数据把“谁的 CPU”这个问题回答清楚,再决定要不要进数据库内部。跳过第一步直接进数据库,是绝大多数误判的起点。
4.2 数据库报 IO 等待,磁盘却显示很闲置
这个现象我第一次遇到的时候困惑了很久。AWR 里 db file sequential read 的平均等待时间到了 20 毫秒以上,而 iostat 显示那块盘的 %util 只有 20% 出头。
第一个要排除的是 iostat 的统计对象对不对。数据库的数据文件在哪个挂载点、底层对应哪些物理设备,这个映射关系经常被搞错。用 lsblk 或者多路径工具确认一遍,很可能你看着闲的那块盘根本不是你该看的那块。这个错误比想象中普遍,尤其在存储做过卷管理、多路径、条带化的环境里。
第二个要排除的是缓存层的干扰。存储侧如果有一层很大的写缓存,iostat 看到的响应时间是设备驱动层的,不代表请求真正落盘的耗时。这种情况下磁盘看起来闲,但数据库的等待时间是真的长,问题在存储侧而不是主机侧。
第三个才是考虑单块盘性能退化。这种情况下 %util 不会很高,但 await 明显偏高,同时队列深度上不去。判断方法是看这块盘的历史基线,如果它比一个月前的响应时间翻了一倍,那就是硬件层面在变差。
这条链路的经验是:iostat 的数字必须建立在“设备映射正确”这个前提上,否则所有的分析都是对着别人家的数据做推理。
4.3 可用内存持续下降,但进程本身没占多少
现象是 free 出来的 available 值在几天内从 60% 掉到 10%,但 top 排序看下来,没有任何进程的 RSS 明显增长。这种情况很容易被当成内存泄漏上报,然后进入漫长的排查。
第一步应该看 /proc/meminfo 的构成。如果下降的部分是被 Slab 或者 PageTables 拿走的,那就和用户态进程无关,是内核层面的对象缓存。oswslabinfo 就是为这个准备的,看哪个 slab 对象在持续增长,通常能定位到具体的内核子系统。
第二步看是不是文件缓存没有被正确统计。有些环境里大量文件读写会把 page cache 撑得很大,available 会相应变小,但这些缓存是可回收的,不代表内存紧张。判断标准是看有没有换页,si/so 为零就不算真紧张。
第三步才考虑数据库自己的内存管理。数据库的共享内存通常在启动时就分配固定大小,不会持续增长;但 PGA 这类按需分配的部分会随着并发上升而涨。这个可以从 oswps 里的进程 RSS 变化趋势看出来。
这条链路的重点是:“内存不够”这个结论必须由换页行为来支撑,而不是由 free 的数字来支撑。只看 free 就下结论,是新手最常见的误判。
5. 数据收集齐了之后,怎么让第二个人也能看懂
性能排查很少有一个人从头干到尾的情况,数据往往要在运维、DBA、开发、厂商之间流转。这时候数据的可读性和上下文完整性,比数据本身的精度更重要。
5.1 抓取时间段的选择比打包工具重要
我见过很多次把整个归档目录原样打包发出去的情况,几十个 G 的包,对方下载要半天,打开之后发现 99% 的数据和问题无关。正确做法是先做时间窗口裁剪,原则是“问题开始前 15 分钟,到问题结束后 15 分钟”。
为什么要有前置窗口?因为问题的根因往往发生在现象出现之前。比如内存泄漏是在凌晨三点开始积累的,业务投诉是早上九点,你只抓九点那段,看到的只是一个已经满了的结果,看不到增长过程。前置窗口给的就是这个增长曲线的起点。
为什么要有后置窗口?为了确认恢复动作的效果。你调整了参数或者重启了服务,恢复的那一瞬间各指标怎么变化的,这个信息对判断根因极有价值。很多人抓完问题点就收工,等别人问“你改完之后好没好”的时候答不上来。
裁剪可以用脚本按文件名批量筛,因为文件名本身就带起始时间,按时间排序取区间内的文件就行。这个过程要小心的是跨零点的文件,需要按文件名时间有序合并之后再切。
5.2 交付之前必须补的三样东西
纯粹的数据包是没有分析价值的,交付时至少补齐这三样:
一是时间范围的明确说明,包括时区。写成“5 月 12 日 14:00 到 16:00(主机本地时间,UTC+8)”,不要只写“下午两点到四点”。
二是指标的对应关系。主机上有几块盘、数据库的数据文件在哪个挂载点、哪个进程是数据库主进程,这些信息在原始数据里是看不出来的,但对方分析时必须知道。我一般会附一个简单的说明表。
三是已知的业务背景。那个时间段有没有做大促、有没有跑批量任务、有没有做过发布,这些外部事件对解读数据至关重要。少写这一句,对方可能会花几个小时去研究一个由批量任务引起的 CPU 峰值。
提示:如果数据要交给外部支持团队,主机名、IP、路径里的敏感信息需要提前处理。别等着对方问你要,提前做好比事后补更省事。
6. 长期跑着不管,早晚会出的几个问题
OSW 因为太简单,很容易被装完之后遗忘。但它在生产环境里是要长期跑的,放着不管会积累出一堆毛病。
6.1 归档被覆盖、脚本被误删、时区被改
归档被覆盖是所有问题的头号来源,而且往往在你最需要数据的时候才发现。表现是打开目录一看,最早的文件是昨天下午的,而你要查的是前天晚上的事。防范办法很简单,就是把保留时长的实际占用算出来,并且在监控里加一条对归档目录大小的监控。空间不够的时候宁可拉长采集间隔,也不要缩短保留时长——间隔拉长只是精度下降,保留时长缩短是直接把数据弄丢了。
脚本被误删的问题,多半发生在系统清理或者目录迁移的时候。OSW 的脚本都是明文 shell,改过参数之后如果没备份,重新部署一遍还得重新配。我的做法是把整个目录做一次 baseline 打包,放在另一个路径下,出问题直接解压回来。
时区被改的影响比较隐蔽。如果主机时区调整过,OSW 之前写的数据用的是旧时区,之后的是新时区,同一个目录里的文件时间轴会错位。排查时如果不注意这一点,会得出“某时刻数据缺失”的错误结论。所以每次分析前,先确认那段时间内主机有没有做过时间同步或者时区调整。
6.2 用一个简单的健康检查脚本兜住长期运行
与其依赖人记得检查,不如让系统自己检查。除了前面提到的用 pid 文件做进程拉起之外,我还建议加两个检查:一是每天检查一次最新文件的时间戳,如果超过采集间隔的 3 倍还没更新,说明采集卡住了;二是每周检查一次归档目录的体积增长趋势,如果某天突然暴涨,多半是采集器或者设备数量发生了变化。
#!/bin/bash ARCHIVE=/opt/oswbb/archive LATEST=$(find $ARCHIVE -name "*.dat" -printf '%T@\n' | sort -n | tail -1 | cut -d. -f1) NOW=$(date +%s) GAP=$(( (NOW - LATEST) / 60 )) if [ $GAP -gt 5 ]; then echo "OSW 采集可能已停止,最后更新在 ${GAP} 分钟前" # 这里接告警通知 fi这个脚本不复杂,但它能把“数据悄悄没了”这种最难发现的问题变成一条告警。我在实际运维里的体会是,监控工具本身也需要被监控,尤其是那种“装完之后就不用管”的工具,恰恰是最容易失效的。
最后分享一个小习惯:每次做完一次没有找到根因的排查,我都会在归档目录旁边留一个文本文件,写上这次排查的时间范围、已经排除了哪些方向、还剩下哪些怀疑点。下一次问题复发的时候,这份笔记比任何图表都值钱,因为它省掉的是重复劳动。