主机「活着」但很慢:Prometheus + Grafana 画出资源曲线(2026)

Prometheus 负责按固定间隔抓取指标并存储时序数据,node_exporter 负责把 Linux 的 CPU、内存、磁盘、网络暴露成 HTTP 接口,Grafana 负责把查询结果画成图和告警。 三者用一份 docker-compose.yml 就能在一台 2 核 4G 的机器上跑起来,常驻内存合计约 300MB。关键配置只有三处:Prometheus 的 scrape_configs 指向 node_exporter、node_exporter 必须以只读方式挂载宿主根目录、Grafana 首次登录后立刻改默认密码。2026 年 10 月这次部署的目的,是回答一个 Uptime-Kuma 答不了的问题——「每天下午三点开始变慢,到底是 CPU、内存还是磁盘 IO」。

上面是结论。上一篇写 Uptime-Kuma 时,它解决的是「服务活着吗」;这篇补上「活着但难受」那一层。

监控数据流

为什么有了 Uptime-Kuma 还要上 Prometheus?

结论:两者监控的对象不同——Uptime-Kuma 探的是「结果」(HTTP 是否 200、端口是否通),Prometheus 采的是「过程」(CPU 曲线、内存趋势、磁盘增长速率)。

维度 Uptime-Kuma Prometheus + Grafana
数据模型 探测结果(up/down + 响应时间) 多维时序指标
典型问题 「站点挂了吗」 「为什么从 15:00 开始变慢」
存储 自带 SQLite 时序库,按保留期滚动删除
告警 内置,配置简单 规则表达式灵活,能算速率与同比
上手成本 十分钟 一小时

故障排查时最有用的是趋势。比如「内存缓慢上涨、三天后 OOM」,Uptime-Kuma 只会在 OOM 那一刻告诉你挂了,Prometheus 能让你提前两天看到斜率。

部署五步

第一步:写 compose,一次起三个容器

结论:三个服务放一个 compose 文件,用命名网络互通,Prometheus 通过服务名访问 node_exporter。

services:
  prometheus:
    image: prom/prometheus:v2.54.1
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./rules:/etc/prometheus/rules:ro
      - prom_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'
      - '--web.enable-lifecycle'
    ports:
      - "127.0.0.1:9090:9090"

  node-exporter:
    image: prom/node-exporter:v1.8.2
    container_name: node-exporter
    restart: unless-stopped
    pid: host
    volumes:
      - /:/host:ro,rslave
    command:
      - '--path.rootfs=/host'
    ports:
      - "127.0.0.1:9100:9100"

  grafana:
    image: grafana/grafana:11.2.0
    container_name: grafana
    restart: unless-stopped
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=换成你的强密码
      - GF_USERS_ALLOW_SIGN_UP=false
    ports:
      - "127.0.0.1:3000:3000"

volumes:
  prom_data:
  grafana_data:

三个端口全部绑 127.0.0.1,公网访问统一走 Nginx 反代。Grafana 千万别直接开在公网 3000 端口——它是整套系统里唯一有登录态的东西,暴露出去等于把监控数据送人。

步骤 1:编写 compose

第二步:确认 node_exporter 采到的是「宿主」而不是容器

结论:容器里的 node_exporter 默认只看得到容器自身的命名空间,必须挂载宿主根目录并指定 --path.rootfs,否则磁盘和内存指标全是错的。

这是本篇最重要的一个坑。我第一次部署时 compose 里只写了 image,没挂 /,结果 Grafana 上磁盘使用率死死停在 12%——那是容器可写层的使用率,不是宿主 40G 盘的 87%。

两个参数缺一不可:

    pid: host                    # 让采集器看到宿主的进程与网络命名空间
    volumes:
      - /:/host:ro,rslave        # 只读挂载宿主根,rslave 保证传播
    command:
      - '--path.rootfs=/host'    # 告诉 exporter 去哪里读 /proc 和 /sys

验证采集结果:

$ curl -s http://127.0.0.1:9100/metrics | grep 'node_filesystem_size_bytes{mountpoint="/"'
node_filesystem_size_bytes{device="/dev/vda1",fstype="ext4",mountpoint="/"} 4.2148e+10

4.2e10 约等于 40G,说明读到的是宿主盘。如果这个值是几 GB 甚至几 MB,就是挂载没生效。

pid: host 影响的是 node_processes_* 这类指标——不加的话你看到的是容器内的进程数,永远是 3 个左右,没有任何参考价值。

步骤 2:验证宿主指标

第三步:配置抓取目标与保留期

结论:prometheus.yml 里一个 job 指一个 exporter,抓取间隔 15 秒对单机足够,保留期按磁盘容量倒推。

prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - /etc/prometheus/rules/*.yml

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']
        labels:
          instance: 'vps-shanghai-01'

  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

instance 标签是自定义的——默认会是 node-exporter:9100 这种容器名,多台机器时完全分不清谁是谁。加上可读的名字,后面告警文案里直接用得上。

保留期在启动参数里:--storage.tsdb.retention.time=15d。粗算磁盘占用:单个指标每 15 秒一个点,约 1–2 字节压缩后;node_exporter 大概 800–1200 个活跃序列,15 天大约 200–400MB。40G 的机器留 15 天比较合适,512MB 内存的小机器建议降到 7 天。

改完配置热加载(因为开了 --web.enable-lifecycle):

$ curl -X POST http://127.0.0.1:9090/-/reload

配置语法参考 Prometheus 官方配置文档。

步骤 3:配置抓取

第四步:六条能直接用的 PromQL

结论:日常排障 90% 的问题用这六条查询就能定位,全部可以在 Prometheus 的 Graph 页面直接粘。

CPU 使用率:

100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

内存使用率(用 MemAvailable 而不是 MemFree,后者把缓存算成已用,会吓死人):

(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

根分区剩余百分比:

node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100

磁盘写入速率(MB/s):

rate(node_disk_written_bytes_total{device="vda"}[5m]) / 1024 / 1024

网络出向带宽(Mbps):

rate(node_network_transmit_bytes_total{device="eth0"}[5m]) * 8 / 1000000

系统负载(1 分钟):

node_load1

node_load1 单独看没意义,要和核数比。2 核机器负载持续大于 2 就是过载。我把它和 CPU 使用率放在同一张图里,一眼能看出是「真算不过来」还是「IO 阻塞导致的假高负载」——后者 CPU 不高但负载高,通常意味着磁盘或网络在拖。

步骤 4:Grafana 出图

第五步:配两条真正会响的告警

结论:告警宁少勿多,先配「磁盘将满」和「内存将满」两条,误报率低、价值高。

groups:
  - name: host-basic
    rules:
      - alert: DiskWillFillIn4Hours
        expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "根分区预计 4 小时内写满"
          description: "实例 {{ $labels.instance }} 当前可用 {{ $value | humanize }}B"

      - alert: MemoryHigh
        expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "内存使用率超过 90% 持续 15 分钟"

predict_linear 是这套系统里最有价值的一个函数——它按最近 6 小时的斜率推算 4 小时后还剩多少空间。「磁盘 87%」不会告警,「磁盘按当前速度 3 小时后满」才会。 前者是噪音,后者是要行动的信号。

告警接收端可以直接用 Grafana 的 Alerting 配飞书 Webhook,不必再单独部署 Alertmanager——单机场景多一个组件就多一处会坏的地方。具体做法是在 Grafana 里建一个 Contact point,类型选 Webhook,把飞书机器人的地址填进去,然后在 Alert rule 里引用它。飞书自定义机器人要求 POST 体是 {"msg_type":"text","content":{"text":"..."}},Grafana 默认的模板不是这个格式,需要在 Contact point 的 Message 里手写模板。这一步有点绕,但只要配通一次就能一直用。

不过也有代价:Grafana 的告警状态和图表在同一个进程里,Grafana 挂了告警也就哑了。如果告警本身是刚需(比如这台机器上跑着收款接口),还是老老实实上 Alertmanager,让它独立于可视化层。我的判断标准是「告警延迟十分钟会不会造成实际损失」——不会就用 Grafana 自带的,会就上 Alertmanager。

步骤 5:告警规则

踩过的四个坑

结论:容器化 node_exporter 的挂载、Grafana 默认密码、时间同步、保留期,这四个是最常见的翻车点。

坑一:node_exporter 采到容器指标。 见第二步,pid: host + --path.rootfs=/host + 挂载 / 三件套缺一不可。

坑二:Grafana 默认 admin/admin。 第一次登录会强制改密码,但如果你的 3000 端口开着公网,从启动到改密码这几分钟就是敞开的窗口。用 GF_SECURITY_ADMIN_PASSWORD 环境变量直接设。

坑三:容器时区和宿主不一致。 Grafana 图表按浏览器时区显示,但 Prometheus 存的是 UTC 时间戳,一般没问题;真正出错的是告警文案里的时间。给三个容器都加 TZ=Asia/Shanghai。

坑四:保留期设太长把盘写满。 默认 15d,但如果指标基数大(多实例、多磁盘、多网卡),实际占用会翻几倍。上线一周后 du -sh /var/lib/docker/volumes/*prom* 看一眼真实体积,再决定要不要缩短。

常见问题

Q:和 Netdata 比,Prometheus 是不是太重了?
A:Netdata 装完就有图,零配置,单机体验更好。Prometheus 的优势是多机聚合、查询语言灵活、能和现有告警体系对接。只有一台机器且不想折腾,Netdata 更划算。

Q:2 核 4G 跑这三个够吗?
A:够。实测常驻内存 Prometheus 约 150MB、Grafana 约 120MB、node_exporter 约 20MB,CPU 基本空闲。抓取间隔从 15s 调到 30s 还能再省一半。

Q:能不能监控容器级别的 CPU 和内存?
A:可以,加 cadvisor 容器(gcr.io/cadvisor/cadvisor),抓 8080 端口,然后用 container_cpu_usage_seconds_total 这类指标。但它本身占资源较多,小机器上按需加。

Q:数据能不能长期保留?
A:本地时序库不适合长期存。要长期留存就接 Thanos 或 VictoriaMetrics,或者定期把关键指标导出成 CSV。单机场景 15 天足够定位问题。

相关阅读

上一篇 让备份不再依赖 rsync 目录:MinIO 单机对象存储 + rclone 挂载实录(2026)