一段把我逼疯的截图:数据库服务器慢得离谱,监控面板却一片绿
接手这套数据库环境的第一周,我就踩了一个印象深刻的坑。某天下午业务方反馈订单写入超时,我按常规流程打开Prometheus大盘,CPU、内存、磁盘、QPS全部正常,慢查询也没明显上升。排查了两三个小时,最后登录服务器用iftop一看,出方向带宽被打满,千兆网卡几乎跑满。而我的监控面板连一条带宽曲线都没有,自然一片“岁月静好”。
从那之后,我下定决心把数据库服务器的上传与下载带宽监控做成一套完整的、可复用的方案。图省事直接扔进Prometheus的查询框,rate(node_network_transmit_bytes_total[5m])这种写法谁都会,但真正落地时会遇到一堆小坑:网卡数量爆炸、字节和比特换算混乱、告警误报、Grafana单位显示对不上……这篇文章把我实际用的方案、踩过的坑、以及为什么这样做,完整拆出来讲。
1. 先从指标定义开始:receive和transmit千万别记反
1.1 上传、下载与服务器收发的对应关系
Prometheus生态里最常用的主机监控采集器是node_exporter,它通过读取/proc/net/dev暴露一组网络指标。最核心的就是这两个:
node_network_receive_bytes_total:网卡累计收到的字节数,也就是流量从外部进入服务器的方向。对数据库服务器而言,这就是“下载”,是客户端发起查询、数据导入、主节点接收同步请求消耗的带宽。node_network_transmit_bytes_total:网卡累计发送的字节数,也就是服务器向外发出的方向。这就是“上传”,数据库返回查询结果集、主备同步发送binlog、备份文件上传到对象存储,走的都是这个方向。
我见过太多新手把这两个指标搞反,写出来的图表和真实流量方向完全相反。这里分享一个我自己的记忆方法:想一次TCP连接就够了。客户端向数据库发SQL,对服务器来说数据是“进来”的,对应receive_bytes_total;数据库把结果集回给客户端,数据是“出去”的,对应transmit_bytes_total。
1.2 rate和increase的取舍,以及时间窗口带来的后果
这两个指标都是累计计数器(Counter),不能直接用node_network_receive_bytes_total画图,因为数值只增不减,重启后还会归零。必须通过rate()或increase()转换为速率后再展示。
# 常用写法:5分钟平均入方向速率,单位字节/秒 rate(node_network_receive_bytes_total{device="eth0"}[5m])rate()计算的是时间窗口内的平均速率,窗口越大曲线越平滑,但代价是响应变慢。我之前为了图表好看,把窗口设成了15分钟,结果有一次夜间备份任务把带宽打满,因为尖峰被15分钟平均抹平了,告警根本没触发。
后续我统一了策略:
- 告警规则用
[1m]窗口,保证能够捕捉到短时突发流量; - 仪表盘展示用
[5m]窗口,兼顾平滑度和敏感度; - 事后分析用
[30m]甚至[1h]窗口,观察整体趋势。
另一个容易踩的坑是用increase()判断一段时间内的流量绝对值,比如“过去5分钟上传了多少MB”。这个场景下increase()是对的,但注意它返回的是窗口首尾差值,不是瞬时速率。把increase()和rate()混在一起画同一张图,单位一个是字节、一个是字节/秒,语义完全不同。
1.3 字节到比特:网络厂商用的是1000进制
这是整个带宽监控里最容易出问题的地方。node_network_*_bytes_total的原始单位是字节(Byte),而带宽的通用单位是比特(bit),两者相差8倍。更麻烦的是,网络设备的带宽规格按1000进制计算,不是存储领域的1024进制。
- 1 Byte = 8 bit
- 1 Kbps = 1000 bps
- 1 Mbps = 1000 Kbps
- 1 Gbps = 1000 Mbps
所以要把字节速率换算成Mbps,正确公式是:
sum(rate(node_network_receive_bytes_total{device=~"eth.*|enp.*"}[5m])) by (instance) * 8 / 1000 / 1000*8从字节换算成比特,/1000得到Kbps,再/1000得到Mbps。如果直接用/1024,你会得到“MiB/s”那一套,和云厂商控制台、交换机端口的带宽数值对不上,自检的时候很容易怀疑监控数据是不是坏了。
我看到过有人在Grafana里把单位选成bytes/sec但PromQL里已经乘了8,结果数值放大8倍显示;也有人PromQL没乘8,只靠Grafana面板选bits/sec强行转换,面板层面是能转,但告警规则里没处理,阈值和实际带宽完全不是一个量纲。
2. 多网卡下的选择困难:为什么数据库服务器上会冒出一堆网卡
2.1 虚拟网卡、桥接网卡、回环网卡:不处理就是在自欺欺人
如果你只是在一台纯净虚机上测试,device="eth0"写死就行。但生产环境的数据库服务器,尤其装了Docker、Kubernetes组件或者使用云平台镜像的机器,ip link列出来可能有几十个网络接口:
1: lo: 2: eth0: 3: docker0: 4: veth2a3b91c@if9: 5: veth4f1e8ab@if9: 6: br-1f2a3b4c: 7: flannel.1: 8: cni0:这些接口里,lo是回环不产生真实网络流量,docker0、veth*、br-*、flannel.*、cni0属于容器虚拟网络。如果不加过滤直接sum(node_network_receive_bytes_total),这些虚拟网卡的流量会计入总和,而且它们之间存在转发关系,同一份报文可能被物理网卡、docker0、veth各计一次,导致最终带宽数值严重虚高。
我遇到过一个真实案例:某台数据库宿主机上跑着十几个容器,按所有网卡汇总后的入方向带宽显示接近3Gbps,但物理网卡明明只有千兆。问题就出在容器跨节点通信的报文在物理网卡和veth上重复计数。
2.2 用ip命令快速梳理网卡矩阵
在确定过滤规则之前,我习惯先登录服务器看一眼网卡现状:
# 列出所有网卡 ip -o link show # 查看路由,确认哪块网卡是真正承载业务流量的 ip route get 1.1.1.1 # 看网卡速率和链路状态 ethtool eth0ip route get会告诉你发往公网/业务网段时走的是哪块网卡,这块就是需要监控的主体。常见命名规律如下:
| 网卡名特征 | 来源 | 监控建议 |
|---|---|---|
| eth0, eth1 | 传统物理/虚机网卡 | 监控 |
| ens33, enp0s3, eno1 | 现代Linux统一命名 | 监控 |
| bond0 | 多网卡绑定聚合 | 监控bond,不监控成员网卡 |
| lo | 回环 | 忽略 |
| docker0, veth*, br-* | Docker网络 | 忽略 |
| flannel., cni0, tunl | 容器网络插件 | 忽略 |
| dummy0, tap*, kube-ipvs0 | 系统虚拟接口 | 忽略 |
2.3 node_exporter侧过滤还是PromQL侧过滤
清理虚拟网卡流量有两个层面,可以配合使用。
第一个层面是采集器侧直接只采集需要的网卡。node_exporter支持通过启动参数指定网卡白名单。注意不同版本的参数名有差异,早期版本是--collector.network.interface.whitelist,较新版本改成了--collector.network.device.whitelist,建议以node_exporter --help的输出为准。
[Service] ExecStart=/usr/local/bin/node_exporter \ --collector.network.device.whitelist="eth0,ens192" Restart=always如果数据库服务器上只有两块真实业务网卡,这种方案最干净,采集到的指标集变小,对Prometheus的存储压力也小。
第二个层面是PromQL侧正则过滤。即使采集器采集了所有网卡,查询时也能用正则排除虚拟接口:
# 推荐:只匹配常见的物理网卡命名 sum(rate(node_network_receive_bytes_total{device=~"eth.*|enp.*|ens.*|eno.*|bond.*"}[5m])) by (instance) * 8 / 1000 / 1000 # 或者反向排除虚拟网卡 sum(rate(node_network_receive_bytes_total{device!~"lo|docker.*|veth.*|br-.*|flannel.*|cni.*|dummy.*|tap.*|tunl.*|kube-.*"}[5m])) by (instance) * 8 / 1000 / 1000两条查询在多数情况下等效。根据我的经验,建议PromQL里始终保留过滤条件,因为node_exporter白名单只能管采集,如果你的集群里还有其他用途的node_exporter、或者后加网卡时忘记更新服务配置,PromQL正则兜底至少能保证监控面板不错乱。
2.4 团队成员经常忽略的:bond网卡重复计数
数据库服务器常用主备网卡做bond,比如bond0由eth0和eth1组成。监控时必须只监控bond0,绝不能同时把eth0、eth1和bond0一起sum。bond的两个成员网卡承载的是同一份流量,合在一起会把带宽统计成真实流量的两倍。
如果服务器上已经存在bond配置,用ip -d link show bond0能看到成员关系。在PromQL推荐使用device=~"eth.*|enp.*|ens.*|eno.*|bond.*"这种正则时,理论上会把eth0和eth1也包含进去,所以真要面对bond环境时,最稳妥的方式是把白名单改成只监控bond0。
3. 落到实处的查询模板与告警规则:阈值、窗口、预测
3.1 标准查询模板:入方向、出方向、汇总口径
我整理了一套经过生产环境考验的PromQL模板,直接复制就能用。假设job名称为db-node,监控目标是数据库服务器节点。
# 入方向带宽(下载),单位Mbps,按节点聚合并取5分钟平均 sum( rate(node_network_receive_bytes_total{ job="db-node", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }[5m]) ) by (instance) * 8 / 1000 / 1000# 出方向带宽(上传),单位Mbps,按节点聚合并取5分钟平均 sum( rate(node_network_transmit_bytes_total{ job="db-node", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }[5m]) ) by (instance) * 8 / 1000 / 1000如果想看某个数据库节点某块网卡的具体曲线,把sum() by (instance)去掉,保留device标签:
rate(node_network_transmit_bytes_total{ job="db-node", instance="db-node-01:9100", device=~"eth.*|enp.*" }[5m]) * 8 / 1000 / 1000这样就能看到单卡级别的上传带宽。排障时非常有用,尤其是服务器有管理网卡和业务网卡分离的情况下,可以快速确认打满的是哪一块。
3.2 利用node_network_speed_bytes计算端口利用率
告警阈值写成固定值有局限:千兆网卡打满900Mbps才危险,万兆网卡同样数值还处于低水位。更好的办法是利用node_network_speed_bytes计算端口利用率。
该指标表示网卡协商速率,如果存在的话:
# 当前入方向带宽利用率,百分比 sum( rate(node_network_receive_bytes_total{ job="db-node", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }[1m]) ) by (instance) * 8 / sum(node_network_speed_bytes{ job="db-node", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }) by (instance) * 100注意一个常见问题:很多云主机的node_network_speed_bytes字段不存在或者返回0,因为虚拟网卡没有通过ethtool上报速率。此时这个表达式会出现除零错误。稳妥的做法是为该指标设置一个默认值,或者干脆在查询里做一个fallback:
sum( rate(node_network_receive_bytes_total{ job="db-node", device=~"eth.*|enp.*" }[1m]) ) by (instance) * 8 / on(instance) group_left node_network_max_speed_bytes * 100如果指标实在缺失,不如直接回到固定阈值方案,按网卡规格分成不同的告警组。
3.3 告警规则示例:为什么是1m窗口加for 5m
我的告警规则最终长这样:
groups: - name: db-network-alerts rules: - alert: DatabaseInboundBandwidthHigh expr: | sum( rate(node_network_receive_bytes_total{ job="db-node", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }[1m]) ) by (instance) * 8 / 1000 / 1000 > 900 for: 5m labels: severity: warning annotations: summary: "数据库节点入方向带宽持续偏高" description: "{{ $labels.instance }} 入方向带宽超过 900Mbps,请检查是否有大批量导入或客户端全表扫描。" - alert: DatabaseOutboundBandwidthHigh expr: | sum( rate(node_network_transmit_bytes_total{ job="db-node", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }[1m]) ) by (instance) * 8 / 1000 / 1000 > 900 for: 5m labels: severity: warning annotations: summary: "数据库节点出方向带宽持续偏高" description: "{{ $labels.instance }} 出方向带宽超过 900Mbps,请检查备份任务、数据同步或超大结果集查询。"使用[1m]窗口是为了敏锐捕捉突发流量,配合for: 5m规避偶然的瞬时尖峰误报。一个典型的异常是备份任务持续10分钟占用60%带宽,如果用[5m]窗口,它的平均值也会比较明显;但如果是[15m]窗口,原本15分钟的占满会变成“5分钟平均值不高”,很容易瞒过告警。
告警阈值900这个数字是我按千兆网卡写的,实际应该根据每台数据库服务器的网卡规格动态设置,或通过node_network_speed_bytes算成百分比告警。
3.4 预测类告警:不要把counter当速率用
有些文章会推荐用predict_linear做带宽预测,比如预测6小时后磁盘满、预测带宽达到阈值。这个函数的原理是对一个历史区间做线性回归,预测未来某个时刻的值。
问题是predict_linear作用于Counter原始值时,预测的是“字节数”,不是“速率”,而且网络流量本身的突发性和周期性很强,线性回归在大多数情况下并不好用。我试过一段时间,发现它要么告警太早,要么在周期性任务(比如每小时整点备份)面前完全失效。
所以我的结论是:带宽监控场景下,老老实实用rate()加for窗口判断持续状态,预测功能留给容量规划分析,别直接接告警。如果一定要做预测型告警,可以结合时间序列的周期特征做分位数告警,但因为复杂度和维护成本太高,在一般团队里不划算。
4. Grafana展示的这些细节,决定你的面板是工具还是摆设
4.1 Stat面板显示实时带宽:最常用也最容易出错
监控带宽最常用的面板是Stat(统计)和Time Series(时间序列)。Stat面板适合放在数据库总览页顶部,一行显示“当前入方向带宽”和“当前出方向带宽”。
具体配置方法:
- 新增Panel,选择Stat类型。
- Query写入前面提到的PromQL,注意当前时间点的值就是Stat显示的数值。
- Transform中可以设置两行查询分别命名为“入方向”和“出方向”。
- Standard options里设置Unit,如果PromQL已经换算成Mbps,单位选择
data rate下面的Mbps;如果PromQL没有换算,则选择bytes/sec,两者不能混。 - Thresholds设三段:绿色<80%、黄色80%-90%、红色>=90%。
这里最大的坑就是我前面反复强调的单位。有个同事照着我的配置做了一遍,但他的PromQL把* 8漏掉了,面板单位却选成了Mbps,看起来数值合理,实际上是错的,方向上也没有意义。验证面板正确性的最快方法:在服务器上手动发起一个大文件下载或传输,看面板数字和iftop显示的速率是否一致。
4.2 用变量切换instance和device,别把网卡写死在查询里
一个数据库集群可能有多个节点,每台节点的网卡名还可能不同。我的做法是在Grafana Dashboard设置里定义两个变量:
instance:查询label_values(node_network_receive_bytes_total{job="db-node"}, instance),用来切换节点。device:查询label_values(node_network_receive_bytes_total{job="db-node", instance="$instance"}, device),用来在该节点上选择网卡。
注意device变量的取值需要排除虚拟网卡,否则下拉框里全是veth*、docker0这些无意义选项。可以在变量查询时直接加正则:
label_values(node_network_receive_bytes_total{ job="db-node", instance="$instance", device=~"eth.*|enp.*|ens.*|eno.*|bond.*" }, device)这样仪表盘交互性会好很多,排障时切换到具体节点和网卡非常顺滑。
4.3 时间序列面板推荐叠加的辅助线:端口速率上限
在Time Series面板里,我习惯把带宽曲线和网卡速率上限叠加在同一张图上。最简单的方式是使用node_network_speed_bytes构建一个常量序列:
node_network_speed_bytes{ job="db-node", instance=~"$instance", device=~"eth.*|enp.*" } * 8 / 1000 / 1000这条线的数值是固定的,但会随实例和网卡变量切换而变化。配合带宽曲线,一眼就能看出利用率是30%还是95%,比单独记一个阈值数字直观得多。有些面板把这条辅助线做成虚线并固定为1000或10000,如果网卡规格不统一,就不够实用。
4.4 总览页和实例页分离:别让sum by把问题藏起来
我见过一种设计:所有数据库节点合在一起用一个sum()展示总带宽,页面很简洁,但一旦某个节点带宽异常,总览页根本看不出是哪台。更好的做法是分层:
- 总览页:展示所有节点的带宽汇总,用来宏观判断整个数据库集群是否处于高流量状态;
- 实例详情页:通过变量选择具体节点,展示该节点的入方向、出方向、单网卡曲线、端口利用率;
- 告警看板:通过告警表达式直接指向具体instance,不经过总览页。
如果总览页和实例页都用同一套PromQL,只是变量不同,维护成本很低。切忌在同一张图里堆几十个时间序列还不加图例,最终看起来像一团乱麻。
5. 监控上线之后的实战价值:从带宽告警反推数据库问题
5.1 周期性尖峰:从带宽面板关联到数据库慢查询
带宽监控上线后第一个收获,是发现一套业务库每15分钟出现一次出方向带宽尖峰,峰值能干到600Mbps,持续约1分钟,然后又掉回50Mbps以内。这个节奏非常规律,明显是某种定时任务。
顺着时间轴叠加数据库慢查询日志,发现每个尖峰出现前30秒,都会有若干条大结果集查询出现在慢日志里。定位到具体业务后,发现是某数据同步服务每隔15分钟全量拉取当天数据,再在内存里过滤。问题不在数据库性能,而在同步任务设计不合理。
后续方案:把同步任务改成增量拉取,只在必要时段进行全量同步,尖峰下降到100Mbps左右,夜间备份任务和同步任务也不再争抢带宽。
5.2 备份任务和同步任务的特征识别
用Prometheus看带宽曲线一段时间后,你可以总结出不同任务的“流量指纹”:
| 任务类型 | 流量特征 | 常见方向 | 关联指标 |
|---|---|---|---|
| 逻辑备份 | 稳定持续的出方向流量 | transmit | 磁盘读速率上升,CPU中等 |
| 物理备份 | 出方向大流量,占用率高 | transmit | 磁盘读速率极高 |
| 主从同步 | 持续的出方向流量,随事务量波动 | transmit | binlog日志大小上升 |
| 数据导入 | 入方向大流量 | receive | 写入QPS上升,磁盘IO升高 |
| 应用全表扫描 | 入方向请求小,出方向结果集大 | transmit | 慢查询数量上升,临时表空间增长 |
这个表格是我在监控排障中沉淀下来的。只看带宽曲线无法定位所有问题,但结合数据库自身的连接数、慢查询、磁盘读写指标,能很快把问题圈到具体任务类别。
5.3 连接风暴与结果集过大:区分两类出方向流量异常
出方向带宽告警时,我的排查路径是先看数据库连接数和活跃会话数。两类场景很典型:
第一类是连接数突然飙升。数据库建立连接的过程需要交换握手包,即使真正执行的SQL不多,连接数一多也会消耗带宽,同时伴随网络连接数指标暴涨。我遇到过一次应用侧配置错误导致连接池疯狂重建连接,短时间几千个连接涌入,出方向带宽和CPU同时飙升,Prometheus告警一小时内连发十几条。
第二类是单个或少数会话返回超大结果集。特征恰好相反,连接数没有明显变化,但某个会话的执行时间和发送数据量暴增。此时我会去查performance_schema或者等价视图中的会话状态,找到Sent_bytes最大的会话,再到慢查询日志里定位对应SQL。
区分这两类场景的意义在于处理方式完全不同:连接风暴往往要在应用侧限制连接池或启动重试机制;而超大结果集需要优化SQL逻辑,必要时在数据库网关做结果集大小限制。如果只盯着带宽数字,很容易把力气花在扩容网卡上,解决不了根本问题。
最后再分享一个运维习惯
从那次被业务方催着排查的下午之后,我有一个固定的习惯:任何一台数据库服务器纳入Prometheus监控时,第一件事不是看CPU和内存,而是先写一条带宽查询语句,确认ip route get的主网卡名、node_exporter采集到的网卡列表、以及输出数值和iftop是一致的。这一步全部正常,才继续配置其他指标和告警。
带宽是数据库所有对外服务的第一道门,大多数时候它很平静,但一旦出问题往往非常剧烈。把这道门纳入Prometheus监控,不只是多几张面板的事,它会在你排查那些“看起来一切都正常”的诡异故障时,直接指向答案。希望这篇文章能让你少走一点我当初走过的弯路,尤其是那些单位换算和网卡过滤的细节,值得花时间一次搞定。