快速实现静态站点的自动化更新

在现代前后端分离或静态站点,将网站代码安全快速的推送到 Web 服务器提高效率的必由之路。

架构

开发者将网站代码 Push 到 GitLab 的主分支。GitLab CI 启动一个临时的轻量级 Runner,通过 SSH 触发 Web Server 主动拉取 Git 代码。专用的 pushgit 容器与 Nginx 容器分离,只有静态资源/代码所在的目录是共享挂载的。CI 只与 pushgit 容器通信,没有任何 Web Server 的其它权限。

Web Server 端配置与搭建

pushgit 容器

这个容器用于接受 GitLab CI 的SSH访问,触发拉取 Git 更新网站文件。

  • 生成容器中 SSHd 的主机密钥存放在 host_key 目录下,保持容器重建后的主机密钥不变
  • 生成一对用户密钥存放在 id_key 目录下,以后容器就是用这个私钥到 GitLab 上拉取文件
  • authorized_keys 存放的是 GitLab CI 的 SSH 公钥,CI 用这个公钥来触发更新

docker-compose.yml:

  pushgit:
    build: ./pushgit
    image: pushgit
    container_name: pushgit
    restart: unless-stopped
    ports:
      - 2222:22
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./yaoge1:/var/www/yaoge1:rw
      - ./yaoge2:/var/www/yaoge2:rw

Dockerfile:

FROM alpine:latest

RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.cernet.edu.cn/alpine#g' /etc/apk/repositories

RUN apk update --no-cache \
 && apk upgrade --no-cache \
 && apk add --no-cache tzdata ca-certificates openssh-server openssh-client git \
 && update-ca-certificates \
 && rm -rf /var/cache/apk/*

COPY --chmod=0600 host_key/ssh_host_*_key /etc/ssh/
COPY --chmod=0644 host_key/ssh_host_*_key.pub /etc/ssh/
RUN echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/disable_password.conf \
 && echo "StrictHostKeyChecking accept-new" > /etc/ssh/ssh_config.d/accept_hostkey.conf

RUN adduser -D -u 1000 -h /home/pushgit -s /home/pushgit/pushgit.sh pushgit && passwd -u pushgit
COPY --chown=pushgit:pushgit --chmod=0700 pushgit.sh /home/pushgit/
RUN install -d -m 0700 -o pushgit -g pushgit /home/pushgit/.ssh
COPY --chown=pushgit:pushgit --chmod=0600 id_key/id_* /home/pushgit/.ssh/
COPY --chown=pushgit:pushgit --chmod=0600 authorized_keys /home/pushgit/.ssh/
RUN ssh-keyscan git.yaoge123.com >> /home/pushgit/.ssh/known_hosts \
 && chown pushgit:pushgit /home/pushgit/.ssh/known_hosts \
 && chmod 0644 /home/pushgit/.ssh/known_hosts

CMD ["/usr/sbin/sshd", "-De"]

pushgit.sh:

#!/bin/sh
set -e
echo "$2: "
cd "/var/www/$2" || { echo "Directory not found!"; exit 1; }
git config --global safe.directory "/var/www/$2"
git fetch --all
git reset --hard @{upstream}
git clean -fd

authorized_keys:

restrict,command="yaoge1" ssh-ed25519 AxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxZ gitlab-yaoge1
restrict,command="yaoge2" ssh-ed25519 AxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxU gitlab-yaoge2

Nginx 配置

server {
    listen 80;
    listen [::]:80;
    server_name yaoge1.yaoge123.com;
    server_tokens off;
    return 301 https://$server_name$request_uri;
}

server {
    listen *:443 ssl;
    listen [::]:443 ssl;
    server_name yaoge1.yaoge123.com;
    server_tokens off;
    
    include ssl/acme-challenge.conf;
    include ssl/yaoge123_com.conf;

    location ~ /\.git {
        return 404;
    }

    root /var/www/yaoge1;
}

GitLab 端配置

创建一个用户(比如 WebServerPush),把上述 id_key/id_*.pub 加入用户的 SSH Keys 下,把这个用户加入到项目成员中,给 Reporter 角色。这个用户是公用的,所有类似的项目都添加这个用户即可。

生成一对网站项目专用的 key,其中公钥放到上述 authorized_keys 中,私钥经BASE64编码后存到CI/CD变量中。

变量键值 (Key)说明
SSH_PRIVATE_KEY_BASE64SSH 私钥的Base64编码,选择 Masked and hidden 和 Protect variable
SERVER_IPWeb Server 的 IP
SERVER_PORTpushgit 容器暴露的 SSH 端口(如上文示例的 2222)
SSH_USERpushgit 容器内的操作账户(如上文示例的 pushgit)

.gitlab-ci.yml

deploy_to_server:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client coreutils
    - echo "$SSH_PRIVATE_KEY_BASE64" | base64 -d > ./ssh_key
    - chmod 400 ./ssh_key
  script:
    - ssh -o StrictHostKeyChecking=accept-new -i ./ssh_key -T -p $SERVER_PORT $SSH_USER@$SERVER_IP

下面只要在 Web Server 上用 git clone 初始化一下即可,后续只要 git 推送了新的文件,就会自动触发更新。

使用 nginx 反代 rsync

用于前端的 nginx 将 rsync/rsync-ssl 反代给后端真实的 rsync 服务,通过 proxy protocol 传递客户端真实IP。

stream {

    server {
        listen 873;
        proxy_pass 192.168.10.10:873;
        proxy_protocol on;
        proxy_timeout 1m;
        proxy_connect_timeout 5s;
    }

    server {
        listen 874 ssl;
        listen [::]:874 ssl;
   
	ssl_certificate ssl/yaoge123_com-ecc.crt;
        ssl_certificate_key ssl/yaoge123_com-ecc.key;
        ssl_certificate ssl/yaoge123_com-rsa.crt;
        ssl_certificate_key ssl/yaoge123_com-rsa.key;

	ssl_session_timeout 1d;
        ssl_session_cache shared:rsyncSSL:10m;

	ssl_dhparam ssl/dhparam.pem;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ecdh_curve X25519:prime256v1:secp384r1;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
        ssl_prefer_server_ciphers off;

        ssl_stapling on;
        ssl_stapling_verify on;

        proxy_pass 192.168.10.10:873;
        proxy_protocol on;
        proxy_timeout 1m;
        proxy_connect_timeout 5s;
    }
}

Seafile 13 缩略图服务器优化实践

Seafile 13 版本将缩略图服务从主服务中拆分出来成为独立的 thumbnail-server 容器,这个架构改变在生产环境中遇到了内存泄漏和高并发下的性能问题。经过和AI几天的折腾,总结出以下优化方案。

问题现象

生产环境高并发请求,nginx 大面积499/504,用户看到的缩略图都裂开了。thumbnail-server 运行后可见内存快速增长,直到 OOM Killer 介入,重启,继续涨,继续崩,Grafana 上看内存曲线就是一个又一个锯齿。

问题分析

内存泄漏

通过 tracemalloc 分析发现主要有三处内存泄漏:

  1. SeaFile._content 缓存泄漏:seafobj 库中 SeaFile 对象的 _content 属性会缓存整个文件内容,处理完成后没有清理,导致大文件内容一直驻留在内存中。
  2. 缩略图生成函数无显式清理:PIL 图像对象、临时文件等资源在函数结束后没有显式释放,依赖 Python 的垃圾回收机制,但在高并发场景下 GC 跟不上分配速度。
  3. task_results_map 无限增长:任务结果存储在字典中,当客户端超时断开连接时,结果永远不会被取走,字典持续增长。

C 扩展层面的碎片化

Python 层面修复后,内存仍然缓慢增长。用 /proc/PID/smaps 分析发现:

总 RSS: 3020 MB
├── 匿名内存 (malloc): 2995 MB (99%)
│   ├── libavif (Pillow): 743 MB
│   ├── _imagingmath (Pillow): 282 MB
│   └── 碎片化块: 20+ 个 50-64MB 块
└── Python (tracemalloc): 8 MB

Pillow 处理图片时 glibc malloc 分配的内存无法归还系统,造成严重碎片化。

大文件占用内存

大文件(视频/PDF)会全部加载到内存,占用大量内存容量。

性能瓶颈

  1. 异步阻塞:http_response.py 中使用 time.sleep() 而不是 await asyncio.sleep(),阻塞了事件循环,影响并发处理能力。
  2. 队列竞争:PDF/PSD 等慢任务与普通图片在同一队列中处理,慢任务阻塞快任务。
  3. Worker 数量固定:默认只有 3 个 worker,无法充分利用多核服务器。

Nginx 配置问题

容器内 nginx 配置 proxy_pass http://localhost:8088,当启用IPv6时 localhost 解析到 IPv6 地址,而后端只监听 IPv4,导致连接失败。

优化方案

1. 内存泄漏修复

在 utils.py 的 get_file_content_by_obj_id() 函数中添加 finally 块清理 SeaFile 对象:

def get_file_content_by_obj_id(repo_id, file_id):
    f = fs_mgr.load_seafile(repo_id, 1, file_id)
    try:
        return f.get_content()
    finally:
        # 清理缓存的文件内容,防止内存泄漏
        if hasattr(f, '_content'):
            f._content = None
        if hasattr(f, 'blocks'):
            f.blocks = None

在 thumbnail.py 的各个缩略图生成函数中添加显式的垃圾回收:

def create_image_thumbnail(repo_id, file_id, thumbnail_file, size):
    try:
        # 原有逻辑
        ...
    finally:
        gc.collect()

在 thumbnail_task_manager.py 中添加任务结果过期清理机制:

RESULT_EXPIRE_TIME = 120  # 秒

def _cleanup_expired_results(self):
    """清理超过 120 秒的任务结果"""
    now = time.time()
    expired_keys = []
    with self._results_lock:
        for task_id, (result, timestamp) in self.task_results_map.items():
            if now - timestamp > RESULT_EXPIRE_TIME:
                expired_keys.append(task_id)
        for key in expired_keys:
            del self.task_results_map[key]

2. 使用 jemalloc 替换 glibc malloc

jemalloc 内存管理更好,能主动归还内存给系统。

Dockerfile 添加:

RUN apt-get install -y libjemalloc2

启动脚本添加:

export JEMALLOC_LIB=${JEMALLOC_LIB:-/usr/lib/x86_64-linux-gnu/libjemalloc.so.2}
if [ -z "$LD_PRELOAD" ] && [ -f "$JEMALLOC_LIB" ]; then
    export LD_PRELOAD="$JEMALLOC_LIB"
fi

3. 大文件流式处理

文件大于阈值时不再全部加载到内存,而是流式写入临时文件:

def stream_file_to_path(seafile_obj, dest_path, chunk_size=8*1024*1024):
    with open(dest_path, 'wb') as f:
        for chunk in iter(lambda: seafile_obj.read(chunk_size), b''):
            f.write(chunk)

4. 异步阻塞修复

将 http_response.py 中的 time.sleep() 改为 await asyncio.sleep()

# 修改前
time.sleep(0.2)

# 修改后
await asyncio.sleep(0.2)

5. 队列分离

新增 pdf_queue 处理慢任务(PDF、PSD),让快任务(图片、SVG、XMIND)不被阻塞:

def __init__(self):
    self.image_queue = queue.Queue(32)      # 快任务
    self.pdf_queue = queue.Queue(32)        # 慢任务
    self.video_queue = queue.Queue(32)      # 视频

队列分配:

  • image_queue:图片、SVG、XMIND(快)
  • pdf_queue:PDF、PSD(慢)
  • video_queue:视频(很慢)

6. 环境变量支持

添加 TASK_WORKERS 和 THUMBNAIL_IMAGE_SIZE_LIMIT 环境变量支持,方便根据服务器配置调整:

# settings.py
TASK_WORKERS = int(os.environ.get('THUMBNAIL_TASK_WORKERS', TASK_WORKERS))
THUMBNAIL_IMAGE_SIZE_LIMIT = int(os.environ.get('THUMBNAIL_IMAGE_SIZE_LIMIT', 30))

7. Nginx IPv6 修复

将 localhost 改为 127.0.0.1 强制使用 IPv4:

proxy_pass http://127.0.0.1:8088;

同时启用日志便于排查问题:

access_log /opt/seafile/logs/thumbnail-server.access.log seafileformat;
error_log /opt/seafile/logs/thumbnail-server.error.log;

8. 内存限制自动重启

万一还有漏网之鱼,超过内存限制就自动退出让容器重启:

MEMORY_LIMIT_MB = int(os.environ.get('THUMBNAIL_MEMORY_LIMIT', 4096))

def _check_memory_and_exit():
    with open('/proc/self/statm') as f:
        rss_pages = int(f.read().split()[1])
    rss_mb = rss_pages * 4096 / 1024 / 1024
    if rss_mb > MEMORY_LIMIT_MB:
        logger.warning(f'Memory {rss_mb:.0f}MB exceeds limit, exiting')
        os._exit(1)

生产环境配置

优化后服务器配置缩减到 8 核 16 GB,完全够用。

环境变量配置(/nfs/thumbnail.env):

THUMBNAIL_TASK_WORKERS=32
THUMBNAIL_IMAGE_SIZE_LIMIT=100
THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT=1024
变量默认值说明
THUMBNAIL_TASK_WORKERS324每种队列的 worker 线程数
THUMBNAIL_IMAGE_SIZE_LIMIT100 MB30 MB原始文件大小限制
THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT1024 MB256 MB图片解码后内存限制

PR 提交

以上优化已提交 PR 到官方仓库 haiwen/seafile-thumbnail-server 的 13.0 分支:

PR内容
#23Python 内存泄漏修复
#24asyncio.sleep 修复
#25THUMBNAIL_TASK_WORKERS 环境变量
#26nginx IPv6 修复
#27THUMBNAIL_IMAGE_SIZE_LIMIT 环境变量
#28队列分离优化
#29智能流式处理
#30内存限制自动重启
#32jemalloc 支持

总结

缩略图服务是典型的 I/O 密集型应用,瓶颈主要在 S3 存储的网络延迟而非 CPU。优化的重点是:

  1. 及时释放内存:处理完文件后立即清理缓存
  2. 避免阻塞:异步代码不要用同步 sleep
  3. 队列隔离:快慢任务分开处理
  4. 可配置性:通过环境变量调整参数

优化后内存稳定在 <2GB 左右,高峰期也能正常处理请求。

用 Anubis 保护网站免受恶意访问,以 GitLab 为例

git.nju.edu.cn 从2025年11月开始得到了 AI bot 的热爱,CPU 根本扛不住,临时限制了校外的网络带宽,但这导致校外慢的几乎无法使用。所以趁着元旦,用 Anubis 对网站进行保护,把 bot 拒之门外。

反向代理模式

Anubis 默认以反向代理的模式运行,所有的流量都经过 Anubis ,Anubis 再将流量反代给后端服务。这种情况下前面还要有一个 HTTPd 如 nginx 实现 TLS 终结再将流量反向代理给 Anubis。在这种情况下怎么将客户端的真实IP传递给最后的应用尤为关键。

当正常访问者的IP有可能为 RFC 1918 私网IP,如校园网环境,校园网内用户使用私网IP访问校园网内服务器是非常正常的行为。但是当这种情况下,Anubis 的处理就有点不尽如人意了。

Anubis 使用 X-Real-IP 和 X-Forwarded-For 处理客户端真实IP,与之相关的环境变量有两个 CUSTOM_REAL_IP_HEADER 和 XFF_STRIP_PRIVATE。

Anubis 会对 X-Forwarded-For 进行处理

  • 当 XFF_STRIP_PRIVATE=true (默认值)时从右向左扫描将第一个公网IP配置为新的 X-Forwarded-For ,如果所有的客户端都是公网 IP 这没有问题,但是如果客户端是公网和私网 IP 混合的模式就是麻烦了。对于私网 IP 的客户端,Anubis 的日志中 X-Forwarded-For 就变空了,如果后端服务使用 X-Forwarded-For 识别客户端真实IP( GitLab 中配置 nginx[‘real_ip_header’] = ‘X-Forwarded-For’ ),结果后端记录的都是前端 nginx 的 IP,这里的 nginx IP 也是私网IP。
  • 当 XFF_STRIP_PRIVATE=false 时从右向左扫描将第一个公网或私网IP配置为新的 X-Forwarded-For ,结果 Anubis 的日志中 X-Forwarded-For 均为前端 nginx 的IP,后端记录的也都是前端 nginx 的 IP。

Anubis 要求前端反向代理将客户端真实IP放在 X-Real-IP 中,如果请求头名字不是这个可以用 CUSTOM_REAL_IP_HEADER 来配置,但是必须有否则会报错。如果后端服务使用 X-Real-IP 识别客户端真实IP( GitLab 中配置 nginx[‘real_ip_header’] = ‘X-Real-IP’ ),结果后端记录的都是 Anubis 的 IP,估计是反代给后端的时候就没有 X-Real-IP ,但在 Anubis 的日志中是正常的,具体我没有探究,在群友的提醒下马上改用 sidecar 模式了。

当然也不是没有解决办法,可以过两次 nginx 。第一次 nginx 把 X-Real-IP 和 X-Forwarded-For 用其它名字的请求头再保存一次,然后将流量反代给 Anubis 。Anubis处理完后将流量反代给 nginx,第二次 nginx 用第一次保存的其它名字做恢复,然后将流量反代给后端服务。配置看起来像下面这样,还是赶快用边车模式吧。

#第一层nginx
  listen 443 ssl;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Real-IP-Frontend $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-For-Frontend $proxy_add_x_forwarded_for;
  proxy_pass http://anubis:8923;
#anubis
  BIND=":8923"
  TARGET=: "http://nginx:3923"
#第二层nginx
  listen 3923;
  proxy_set_header X-Real-IP $http_x_real_ip_frontend;
  proxy_set_header X-Forwarded-For $http_x_forwarded_for_frontend;
  proxy_pass http://gitlab:80;

不太理解 Anubis 为什么要去这样修改 X-Real-IP 和 X-Forwarded-For,正常的反向代理应该从网络层获取客户端IP然后添加到 X-Forwarded-For 后面。

sidecar 模式

对所有的请求,nginx 都会内部发起子请求 auth_request 到 /.within.website/x/cmd/anubis/api/check ,如果认证服务(anubis)返回 200,则请求继续反代给 gitlab 。如果认证服务(anubis)返回 401,则返回客户端 307 重定向到 /.within.website/?redir=……,引导客户端通过 anubis 验证,成功后再跳回原来的链接。

nginx 配置片段

  • /.within.websit 是需要反代给 Anubis 的内容,不包括请求体,只包括请求头,通过请求头传递客户端IP。需要注意的是虽然 proxy_pass_request_body off;,但 nginx 的 body 大小检查发生在处理 proxy_pass 之前,所以还需要添加 client_max_body_size 0;。
  • @redirectToAnubis 是给客户端的重定向,让客户端通过 Anubis 验证。注意这里需要 nginx 有 ndk 和 set-misc 模块
  • / 是主入口,auth_request 把所有的请求先给 /.within.website/x/cmd/anubis/api/check ,如果返回200则继续,如果返回401则转给上面的@redirectToAnubis
    location /.within.website/ {
        proxy_pass http://anubis:8923;
	proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass_request_body off;
        proxy_set_header content-length "";
        auth_request off;
        client_max_body_size 0;
    }
    location @redirectToAnubis {
        set_escape_uri $encoded_redir $scheme://$host$request_uri;
        return 307 /.within.website/?redir=$encoded_redir;
        auth_request off;
    }
    location / {
        auth_request /.within.website/x/cmd/anubis/api/check;
        error_page 401 = @redirectToAnubis;
        include conf.d/proxy_git;
    }

其中 proxy_git 的内容

        proxy_connect_timeout 600;
        proxy_send_timeout 600;
        proxy_read_timeout 600;
        proxy_buffering off;
        proxy_request_buffering off;
        client_max_body_size 0;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
	proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://gitlab:80;

anubis 策略配置片段

  • 为了能够让 nginx 区别被拦截的请求,需要对 anubis 的拒绝策略更改 HTTP 状态码为403
  • 对于生产环境建议持久化数据,而不是使用内存
status_codes:
  CHALLENGE: 200
  DENY: 403
store:
  backend: bbolt
  parameters:
    path: /data/bbolt/anubis.bdb

anubis 容器配置片段

  • 挂载配置文件和持久化目录,注意持久化目录对于1000:1000要可写
  • BIND 为接受请求的端口,在 sidecar 模式下 TARGET 为空格
  • 持久化存储需要固定一个64个字符的16进制私钥(可用 openssl rand -hex 32 生成)
  • Anubis 输出的日志是标准的 ndjson
    volumes:
      - ./anubis/policy.yaml:/data/cfg/botPolicy.yaml:ro
      - ./anubis/bbolt:/data/bbolt:rw
    environment:
      BIND: ":8923"
      TARGET: " "
      COOKIE_DOMAIN: "git.nju.edu.cn"
      CUSTOM_REAL_IP_HEADER: "X-Real-IP"
      ED25519_PRIVATE_KEY_HEX: "123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"
      POLICY_FNAME: "/data/cfg/botPolicy.yaml"
    labels:
      - co.elastic.logs/json.target=""
      - co.elastic.logs/json.add_error_key=true

以上的 nginx 和 GitLab 均在容器中运行,anubis 容器镜像为 ghcr.io/techarohq/anubis:v1.24.0

齐治堡垒机监控

Node exporter

ExecStart=/usr/local/sbin/node_exporter

NGINX Prometheus Exporter

ExecStart=/usr/local/sbin/nginx-prometheus-exporter --nginx.scrape-uri=http://localhost:9090/stub_status
/etc/nginx/conf.d/stub_status.conf 
server {
	listen 9090;
	listen [::]:9090;
	server_name localhost;
	
	location /stub_status {
		stub_status on;
		access_log off;
	}
}

Prometheus Valkey & Redis Metrics Exporter

ExecStart=/usr/local/sbin/redis_exporter -redis.addr rediss://localhost:6379 -skip-tls-verification -tls-ca-cert-file /var/lib/redis/ca.crt -tls-client-cert-file /var/lib/redis/client.crt -tls-client-key-file /var/lib/redis/client.key

PostgreSQL Server Exporter

Environment="DATA_SOURCE_NAME=host=localhost port=5432 user=shterm dbname=shterm sslmode=require sslcert=/var/lib/pgsql/client.crt sslkey=/var/lib/pgsql/client.key"
ExecStart=/usr/local/sbin/postgres_exporter

注意:PostgreSQL Server Exporter 的某个版本只支持部分版本的的 PostgreSQL ,需要查看 PostgreSQL Server Exporter 对应 Tags README.md 中的 CI Tested PostgreSQL versions

比如 PostgreSQL 9.6 最后一个支持的 PostgreSQL Server Exporter 版本是 0.12.1,但是存在报错,没有报错的版本是 0.11.1

OpenWrt/ImmortalWrt 配置NJU BRAS校外拨入

OpenWrt/ImmortalWrt 配置南京大学的BRAS校外拨入(L2TP),添加策略路由仅让访问校内IP走VPN。

用 opkg 安装 xl2tpd 及其依赖包:先更新列表,再过滤xl2tpd,安装时会自动依赖的包。

在 /etc/config/network 中添加如下配置,其中将***替换为统一认证的用户名密码。完成后在网络-接口中可见名为nju的接口,开机会自动链接。

config interface 'nju'
	option proto 'l2tp'
	option server '202.119.36.101'
	option username '***'
	option password '***'
	option ipv6 '0'
	option mtu '1452'
	option defaultroute '0'
	option delegate '0'
	option ip4table 'main'

config route
	option interface 'nju'
	option target '114.212.0.0/16'
	option mtu '1452'

config route
	option interface 'nju'
	option target '180.209.0.0/20'
	option mtu '1452'

config route
	option interface 'nju'
	option target '202.119.32.0/19'
	option mtu '1452'

config route
	option interface 'nju'
	option target '202.127.247.0/24'
	option mtu '1452'

config route
	option interface 'nju'
	option target '202.38.126.160/28'
	option mtu '1452'

config route
	option interface 'nju'
	option target '202.38.2.0/23'
	option mtu '1452'

config route
	option interface 'nju'
	option target '210.28.128.0/20'
	option mtu '1452'

config route
	option interface 'nju'
	option target '210.29.240.0/20'
	option mtu '1452'

config route
	option interface 'nju'
	option target '219.219.112.0/20'
	option mtu '1452'

config route
	option interface 'nju'
	option target '58.192.32.0/20'
	option mtu '1452'

config route
	option interface 'nju'
	option target '58.192.48.0/21'
	option mtu '1452'

config route
	option interface 'nju'
	option target '58.193.224.0/19'
	option mtu '1452'

config route
	option interface 'nju'
	option target '58.195.80.0/20'
	option mtu '1452'

东芝 X20 与 X10 区别

美的早就把东芝的白色家电收购了,东芝的售后由美的来负责,海鲜市场上有非常便宜的美的洗衣机延保卡,香啊。

  • X20为了能够做到全嵌入,三围尺寸均比X10小多了,没有X10的臃肿感,滤网和洗涤剂也都是从正面维护。
  • X20多了一个底座,可以当作储物空间,其实最重要的是抬升X20主机,否则机器太低弯腰取衣服太累了。
  • X10是斜筒拿取比较舒服,X20就是完全的平没有一点倾角,感觉上X10的筒更小了,没有倾角的筒即使底座垫高了拿取还是感觉别扭,而且衣服很容易掉出来,应该是为了尺寸妥协了。
  • X20只有自动投放,没有像X10那样还有手工投放的槽位,洗衣液还可以直接扔筒里面,但是柔顺剂就没有办法了,应该是空间问题放不下。
  • X10的自动投放可以设定精确的比例,即可以分别设定洗涤剂和柔顺剂30L水对应的投放量3ml-30ml,但是X20去掉了这个精确的设置,只有普通和浓缩两档,这算是反向升级了。不过话说回来,能买到的大多数洗涤剂和柔顺剂很少有精确的标注。
  • X10的排水过滤是很密的梳子,可以留下很多毛絮需要经常清理,X20的排水过滤只能拦住大东西了,这个可能不能说是升级,但是的确省事了。
  • X10有专门降低噪音的烘干和脱水模式或设置,X20没有,但是X20比X10的还安静。
  • X20有专门的单脱水和漂洗脱水程序,这个比X10那种需要变更程序把前面的洗涤漂洗关掉的奇葩操作人性化多了。X20总算加上了中途添衣功能,可以不中断预约。
  • 烘干的风向X10是后送风前回风,这会导致很多毛絮积攒在门封附近,X10的门封有非常多的褶皱和角落,想要清洁干净是不可能的,拿上镊子掏上一个小时才可能清洁的差不多,很多角落极深极难清洁。X20是前送风后回风,而且门封做了改进少了很多角落,烘干完成基本上不用清洁门封。这个应该是X20最有用最大的改进了,X10清理毛絮简直是噩梦。
  • X10烘干过滤网进风前的拐弯处会积攒非常多毛絮,因为角度的原因非常难取出这些毛絮,X20则好很多基本不会积攒。

X20的升级还是值得的,至少从清洁的角度方便了很多,虽然感觉洗涤容量少了,不过可以天天洗嘛,反正买了延保卡,使劲用~~

GitLab 限制域名

GitLab 的 Web 服务应该限制只有制定域名可以访问,防止其它非备案的解析,对于独立运行的 nginx 这非常简单。虽然 GitLab 的 Web 服务由 nginx 提供,但其配置文件由 gitlab-ctl reconfigure 自动生产,不应该手工更改 nginx 的配置文件,因为会被 reconfigure 覆盖,应该通过修改 gitlab.rb 来控制 nginx 配置。又因在 gitlab 生成的 nginx 配置中使用了 default_server ,所以在额外的配置中只能使用正则负向先行断言。

通过修改 gitlab.rb 或者环境变量插入一个 nginx 配置文件

nginx['custom_nginx_config'] = "include /etc/gitlab/nginx-default.conf;"

nginx-default.conf 配置文件内容如下

server {
    listen       80;
    listen  [::]:80;
    listen      443 ssl;
    listen [::]:443 ssl;

    http2 on;

    server_name ~^(?!git\.yaoge123\.com$).*$;
    server_tokens off;
    ssl_reject_handshake on;
    return 444;
}

Lenovo DSS-G 监控

Lenovo DSS-G 是联想将GPFS集成的软硬件一体化设备,作为一套存储监控非常重要,等到丢数据的时候就追悔莫及了。

组件数据库

DSS-G 使用组件数据库(compDB )来描述硬件组件,如机柜、DSS-G的服务器和JBOD。使用 DSS-G 不需要compDB,但是健康监控需要。

第一步:生成组件数据库

始终使用dry run来生成组件数据库(compDB),其中dssg01是DSS-G两台服务器的node class(dssgmkstorage时创建的)

[root@dss01 ~]# dssgmkcompdb --racktype RACK42U --verbose --dryrun -N dssg01
DSS-G 5.0c
Parsing options: --racktype RACK42U --verbose --dryrun -N dssg01 --
Entering verbose mode
Entering dry run mode

Using provided nodeclass or node list
dss01
dss02
Checking whether all nodes belong to the same cluster...

Using rack type "RACK42U" of height 42U

Checking server model...

…………

# DSS-G210-1

Setting componentDB...
Note: all commands below support the --dry-run option
DRYRUN: mmaddcomp -F /root/yaoge123gpfs.io01-comp.2025-03-15.233334.851103.stanza --replace
DRYRUN: mmchcomploc -F /root/yaoge123gpfs.io01-comploc.2025-03-15.233334.851103.stanza
DRYRUN: /root/yaoge123gpfs.io01-dispid.2025-03-15.233334.851103.sh

All done

第二步:检查两个stanza文件内容,其中组件安装在机柜中的具体位置在 -comploc..stanza 文件中

第三步:导入组件数据库,照抄dssgmkcompdb最后面三行DRYRUN后面的命令运行,然后用mmlscomp和mmlscomploc检查结果。

[root@dss01 ~]# mmlscomp

    Rack Components

Comp ID  Part Number  Serial Number  Name
-------  -----------  -------------  ----
      6  RACK42U      *******        A1

    Server Components

Comp ID  Part Number  Serial Number  Name              Node Number
-------  -----------  -------------  ----------------  -----------
      9  7D9ECTOLWW   ********       SR655V3-********          101
     10  7D9ECTOLWW   ********       SR655V3-********          102

    Storage Enclosure Components

Comp ID  Part Number  Serial Number  Name            Display ID
-------  -----------  -------------  --------------  ----------
      8  7DAHCT0LWW   ********       D4390-********

    Storage Server Components

Comp ID  Part Number  Serial Number  Name
-------  -----------  -------------  ----------
      7  DSS-G210                    DSS-G210-1
[root@dss01 ~]# mmlscomploc

Rack  Location  Component
----  --------  ----------------
A1    U35-36    SR655V3-********
A1    U33-34    SR655V3-********
A1    U01-04    D4390-********

Storage Server  Index  Component
--------------  -----  ----------------
DSS-G210-1          3  SR655V3-********
DSS-G210-1          2  SR655V3-********
DSS-G210-1          1  D4390-********

设置DSS-G监控

因为DSS-G本身的冗余性(节点冗余、链路冗余等),出现故障不一定会导致整个文件系统失效,这往往会导致故障没有被立刻发现,但是这些故障会破坏冗余性或导致性能下降,管理员应该知晓这些故障并积极修复。dssghealthmon 它会周期性(默认1小时)检查整个DSS-G的健康状态,如有故障会自动发送邮件通知。dssghealthmon 可以通过confluent节点带外访问XCC(服务器的BMC)检查硬件健康状态,所以需要DSS-G服务器到Confluent的免密码SSH。

第一步:编辑配置文件 /etc/dssg/dssghealthmon.conf

  • contactEmail: 故障通知的Email,必填项。
  • 其它不想填就都注释了

第二步:先查看一下DSS-G的nodeclass名称,然后启动监控

[root@dss01 ~]# mmlsnodeclass 
Node Class Name       Members
--------------------- -----------------------------------------------------------
dssg01                dss01,dss02

[root@dss01 ~]# dssghealthmon_startup dssg01 confluent
Obtaining Confluent version from management server confluent
3.11.1

Processing nodeclass...
Node Class Name       Members
--------------------- -----------------------------------------------------------
dssg01                dss01,dss02

Parsing configuration file...

Copying configuration file...

Creating tuple file...
dss01:dss01:7D9ECTOLWW:********
dss02:dss02:7D9ECTOLWW:********

Copying tuple file...

Setting or replacing the dssghealthmon_erflist cronjob...
Warning: dssghealthmon_erflist cronjob has NOT been specified

Creating and copying daemon environment file...

Starting dssghealthmond...
The dssghealthmon system has been successfully started

第三步:在两个服务器上检查监控状态和配置文件

[root@dss01 ~]# dssghealthmon_status dssg01
Processing nodeclass...
Node Class Name       Members
--------------------- -----------------------------------------------------------
dssg01                dss01,dss02

Obtaining status of the DSS-G health monitor...
dss01: active
dss02: active
[root@dss01 ~]# systemctl status dssghealthmond.service
● dssghealthmond.service - DSS-G Health Monitor
     Loaded: loaded (/etc/systemd/system/dssghealthmond.service; disabled; preset: disabled)
     Active: active (running) since Sat 2025-04-05 18:52:43 CST; 1min 57s ago
    Process: 1361859 ExecStart=/opt/lenovo/dss/dssghealthmon/dssghealthmond $NODECLASS $MGMT (code=exited, status=0/SUCCESS)
   Main PID: 1361946 (dssghealthmond)
      Tasks: 2 (limit: 2468168)
     Memory: 2.3M
        CPU: 34.410s
     CGroup: /system.slice/dssghealthmond.service
             ├─1361946 /bin/bash /opt/lenovo/dss/dssghealthmon/dssghealthmond dssg01 confluent 1361859
             └─1362172 sleep 3600

Apr 05 18:52:43 dss01 systemd[1]: Starting DSS-G Health Monitor...
Apr 05 18:52:43 dss01 systemd[1]: Started DSS-G Health Monitor.
[root@dss01 ~]# cat /etc/dssg/dssghealthmon.env 
NODECLASS=dssg01
MGMT=confluent
[root@dss01 ~]# cat /etc/dssg/dssghealthmon.hosts 
dss01:dss01:7D9ECTOLWW:********
dss02:dss02:7D9ECTOLWW:********

[root@dss02 ~]# systemctl status dssghealthmond.service
● dssghealthmond.service - DSS-G Health Monitor
     Loaded: loaded (/etc/systemd/system/dssghealthmond.service; disabled; preset: disabled)
     Active: active (running) since Sat 2025-04-05 18:52:43 CST; 4min 52s ago
    Process: 87519 ExecStart=/opt/lenovo/dss/dssghealthmon/dssghealthmond $NODECLASS $MGMT (code=exited, status=0/SUCCESS)
   Main PID: 87601 (dssghealthmond)
      Tasks: 2 (limit: 2468168)
     Memory: 2.2M
        CPU: 1.344s
     CGroup: /system.slice/dssghealthmond.service
             ├─87601 /bin/bash /opt/lenovo/dss/dssghealthmon/dssghealthmond dssg01 confluent 87519
             └─87823 sleep 3600

Apr 05 18:52:43 dss02 systemd[1]: Starting DSS-G Health Monitor...
Apr 05 18:52:43 dss02 systemd[1]: Started DSS-G Health Monitor.
[root@dss02 ~]# cat /etc/dssg/dssghealthmon.env 
NODECLASS=dssg01
MGMT=confluent
[root@dss02 ~]# cat /etc/dssg/dssghealthmon.hosts 
dss01:dss01:7D9ECTOLWW:********
dss02:dss02:7D9ECTOLWW:********

第四步:两台服务器均配置和测试发邮件,以QQ企业邮箱为例,注意需要urlencode

[root@dss01 ~]# cat ~/.mailrc 
set v15-compat
set mta=smtps://yaoge%40yaoge123.com:************@smtp.exmail.qq.com:465 smtp-auth=login
set from=cicam@nju.edu.cn
[root@dss01 ~]# echo "$HOSTNAME" | mailx -s "TEST" yaoge@yaoge123.com

[root@dss01 ~]# scp ~/.mailrc dss02:/root/
.mailrc

[root@dss02 ~]# echo "$HOSTNAME" | mailx -s "TEST" yaoge@yaoge123.com

第五步:模拟故障测试

[root@dss01 ~]# mmvdisk rg list
[root@dss01 ~]# mmvdisk pdisk list --rg dss02
# 找到一个pdisk模拟失效
[root@dss01 ~]# mmvdisk pdisk change --rg dss02 --pdisk e1s44 --simulate-dead
[root@dss01 ~]# mmvdisk pdisk list --rg dss02 --not-ok

                              declustered                                                
recovery group  pdisk            array     paths  capacity  free space  FRU (type)       state
--------------  ------------  -----------  -----  --------  ----------  ---------------  -----
dss02           e1s44         DA1              0    20 TiB    1024 GiB  03LC215          simulatedDead/draining/replace

# 可见已经开始rebuilding
[root@dss01 ~]# mmvdisk rg list --rg dss02 --all

                                                                        needs    user 
recovery group  node class  active   current or master server          service  vdisks  remarks
--------------  ----------  -------  --------------------------------  -------  ------  -------
dss02           dssg01      yes      dss02                             yes           2  

……

declustered   needs                         vdisks       pdisks           capacity     
   array     service  type    BER    trim  user log  total spare rt  total raw free raw  background task
-----------  -------  ----  -------  ----  ---- ---  ----- ----- --  --------- --------  ---------------
NVR          no       NVR   enable   -        0   1      2     0  1          -        -  scrub 14d (66%)
SSD          no       SSD   enable   -        0   1      1     0  1          -        -  scrub 14d (20%)
DA1          yes      HDD   enable   no       2   1     44     2  2    834 TiB  144 GiB  rebuild-1r (8%)

……

vdisk               RAID code        disk group fault tolerance         remarks
------------------  ---------------  ---------------------------------  -------
RG002LOGHOME        4WayReplication  -                                  rebuilding
RG002LOGTIP         2WayReplication  1 pdisk                            
RG002LOGTIPBACKUP   Unreplicated     0 pdisk                            
RG002VS001          3WayReplication  -                                  rebuilding
RG002VS002          8+2p             -                                  rebuilding

第六步:等收到报警邮件后,恢复正常

[root@dss01 ~]# mmvdisk pdisk change --rg dss02 --pdisk e1s44 --revive
[root@dss01 ~]# mmvdisk pdisk list --rg dss02 --not-ok
mmvdisk: All pdisks of recovery group 'dss02' are ok.

第七步:检查日志

  • /var/log/dssg/dssghealthmond.log.<timestamp> 是周期性状态检查的日志,时间戳是健康检查服务启动的时刻,每次周期检查的日志会附加在文件末尾。查看两个服务器日志可见只有一个服务器实际进行了健康检查的动作,另一个服务器知道它不是leader就不干了。
  • /var/log/dssg/dssghealthmon.erf_* 是故障日志,文件名中包含故障类型、部件标识和时间戳,报警邮件正文就是这个文件的内容。
  • 一旦解决故障后,应该在DSS的所有节点上删除ERF文件。

Grafana+Prometheus 实现监控可视化

Grafana 是数据可视化展示平台,可以连接包括 Prometheus 在内的各种数据源。Prometheus 是一套监控报警和时序数据库的组合,Prometheus 主动发起TCP连接访问监控对象上面 Exporter 的端口获取信息,存储在自己的时序数据库中,Grafana 实时从 Prometheus 查询数据展示出来。

监控中数据获取有 Pull 和 Push 两大流派:

  • Pull(拉)是监控系统主动获取数据,监控系统需要服务发现机制,需要被监控对象开放端口能够被访问,一般来说监控对象配置简单、监控系统配置复杂。好处是容易发现监控对象离线,监控系统控制拉取顺序和频率,防止监控系统过载;坏处是每个监控对象都要开放端口,防火墙等安全配置复杂;麻烦是监控系统需要维护监控对象信息。
  • Push(推)是监控对象主动推送数据,监控系统被动获取,只有监控系统需要开放端口,监控对象配置稍复杂(设置推送目标)。好处是监控系统不需要维护监控对象列表,有且仅有监控系统需要开放端口,监控对象只要能网络可达监控系统即可,哪怕在多个防火墙后进行了多次NAT;坏处是监控系统可能过载,不容易发现监控对象离线;麻烦是每个监控对象都需要配置监控系统信息。

监控对象(客户端)

Node Exporter 是 Prometheus 官方提供的物理机监控客户端,注意更改版本号为最新的。

wget -q https://mirror.nju.edu.cn/github-release/prometheus/node_exporter/LatestRelease/node_exporter-1.9.0.linux-amd64.tar.gz
tar xf node_exporter-*
sudo mv node_exporter-*/node_exporter /usr/local/sbin/
sudo chown root:root /usr/local/sbin/node_exporter
rm -rf ./node_exporter-*
sudo cat > /etc/systemd/system/node_exporter.service << EOF
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
Restart=always
ExecStart=/usr/local/sbin/node_exporter
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=20s
SendSIGKILL=no

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable --now node_exporter.service

DCGM-Exporter 是 NVIDIA 官方提供的GPU监控客户端,它基于DCGM。

安装 NVIDIA Data Center GPU Manager (DCGM),注意更改操作系统版本、架构、key包版本。

wget https://developer.download.nvidia.cn/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
rm -f cuda-keyring_1.1-1_all.deb
sudo add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /"
sudo apt-get update && sudo apt-get install datacenter-gpu-manager-4-cuda-all
systemctl enable --now nvidia-dcgm

安装 Go 然后编译 DCGM-Exporter,注意更改Go的版本

wget https://mirror.nju.edu.cn/golang/go1.24.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go*.tar.gz
rm -f go*.tar.gz
export PATH=$PATH:/usr/local/go/bin
go env -w GO111MODULE=on 
go env -w GOPROXY="https://repo.nju.edu.cn/go/,direct"
git clone https://github.com/NVIDIA/dcgm-exporter
cd dcgm-exporter
make binary
cp cmd/dcgm-exporter/dcgm-exporter /usr/local/sbin/
mkdir /usr/local/etc/dcgm-exporter
cp etc/* /usr/local/etc/dcgm-exporter/
cd ..
rm -rf dcgm-exporter
cat > /etc/systemd/system/dcgm-exporter.service <<EOF
[Unit]
Description=Prometheus DCGM exporter
Wants=network-online.target nvidia-dcgm.service
After=network-online.target nvidia-dcgm.service

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/sbin/dcgm-exporter --collectors /usr/local/etc/dcgm-exporter/default-counters.csv

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now dcgm-exporter.service

注意 DCGM 和 DCGM-Exporter 的版本要匹配,DCGM-Exporter 的版本格式为 DCGM版本-Exporter版本。如下列 4.1.1-4.0.4 意思是 DCGM-Exporter 本身的版本是4.0.4,对 DCGM 的版本要求是 4.1.1,经查看是匹配的。

/usr/local/sbin/dcgm-exporter --version
2025/03/23 10:46:13 maxprocs: Leaving GOMAXPROCS=112: CPU quota undefined
DCGM Exporter version 4.1.1-4.0.4
(base) root@ubuntu:~# dcgmi --version

dcgmi  version: 4.1.1

监控系统(服务端)

先把 Docker 和 Docker Compose V2 装好,然后用容器部署 Grafana 和 Prometheus。简单起见不考虑前面套反向代理、Prometheus加认证、consul 实现自动服务发现注册。

先建好目录并更改所有者/组,否则启动的时候会报没有权限

mkdir grafana
mkdir prometheus
mkdir prometheus-conf
sudo chown 472:0 grafana/
sudo chown 65534:65534 prometheus

编辑 docker-compose.yml

services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    restart: always
    volumes:
      - ./prometheus:/prometheus
      - ./prometheus-conf:/etc/prometheus/conf
    command:
      - --config.file=/etc/prometheus/conf/prometheus.yml
      - --web.console.libraries=/usr/share/prometheus/console_libraries
      - --web.console.templates=/usr/share/prometheus/consoles
      - --web.listen-address=0.0.0.0:9090
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.size=100GB  #数据库存储容量限制,超过后会自动删除最老的数据
      - --storage.tsdb.wal-compression
  grafana:
    image: grafana/grafana
    container_name: grafana
    restart: always
    ports:
      - 3000:3000
    volumes:
      - ./grafana:/var/lib/grafana
    environment:
      - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-piechart-panel
      - GF_SECURITY_ADMIN_PASSWORD=yaoge123   #admin的初始密码
      - GF_SERVER_ENABLE_GZIP=true
      - GF_SERVER_DOMAIN=192.168.1.10   #如果前面有反代要改
      - GF_SERVER_ROOT_URL=http://192.168.1.10   #如果前面有反代要改
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_NAME=yaoge123
      #- GF_SERVER_SERVE_FROM_SUB_PATH=true   #反代后有子路径
      #- GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana   #反代后有子路径
      #- GF_SECURITY_COOKIE_SECURE=true   #反代上有HTTPS
    depends_on:
     - prometheus

导入默认配置 prometheus.yml

cd prometheus-conf
wget https://raw.githubusercontent.com/prometheus/prometheus/refs/heads/main/documentation/examples/prometheus.yml

prometheus.yml 中主要编辑的是监控目标

# my global config
global:
  scrape_interval: 30s # Set the scrape interval to every 15 seconds. Default is every 1 minute.采样间隔
  evaluation_interval: 30s # Evaluate rules every 15 seconds. The default is every 1 minute.
  scrape_timeout: 30s #采样超时,有一些exporter读取很慢,需要放宽超时。
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:9090"]
       # The label name is added as a label `label_name=<label_value>` to any timeseries scraped from this config.
        labels:
          app: "prometheus"

  - job_name: node_exporter
    static_configs:
    - targets: 
      - '192.168.1.101:9100'
      - '192.168.1.102:9100'
  - job_name: dcgm-exporter
    static_configs:
    - targets:
      - '192.168.1.101:9400'
      - '192.168.1.102:9400'

把容器启起来 cd .. && docker compose up -d

下面就是配置 Grafana

至此一个简单的监控平台就已经搭建好了,在真实的生产环境中使用 consul 实现被监控对象的管理是必不可少的,Prometheus 最好加上认证,Grafana 前面最好套一个 NGINX 做反代实现 HTTPS。