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 从2015年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 验证
  • / 是主入口,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 {
        return 307 /.within.website/?redir=$scheme://$host$request_uri;
        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

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。

DNSmasq 为HPC集群外容器提供集群内主机名解析

在HPC集群中通常有DNS和本地hosts提供解析服务,以便节点间通过主机名互相通信,而不是直接使用IP地址。但是如果在集群外有一个独立服务器中的容器需要与集群内的节点通过主机名通讯,就需要通过DNS来给容器提供解析服务。

通过自动化脚本将集群的hosts拷贝到独立服务器的一个目录下,如 /home/hpc/dns/hosts

自己做一个dnsmasq的容器:

[yaoge123]$ cat dnsmasq/Dockerfile 
FROM alpine:latest
RUN apk update \
 && apk upgrade \
 && apk add --no-cache \
            dnsmasq \
 && rm -rf /var/cache/apk/*

编写docker-compose.yml:

  1. dnsmasq提供了DNS服务,需要指定ip地址,以便在下面其它容器配置中指定dns ip
  2. /home/hpc/dns 是存储hosts的本机目录
  3. 生产环境用 –keep-in-foreground,调试时用–no-daemon和–log-queries
  4. –domain-needed 一定要加,防止dnsmasq将没有域的主机名(没有.的)转发给上游DNS
  5. –cache-size= 改的比hosts文件行数多一些
  6. abc是要解析集群内主机名的容器,添加的dns就是为了用dnsmasq来提供解析服务
  7. 不要解析的就不要加dns
services:
  dnsmasq:
    build: ./dnsmasq
    image: dnsmasq
    container_name: dnsmasq
    networks:
      default:
        ipv4_address: 192.168.100.200
    volumes:
      - /home/hpc/dns:/etc/dns:ro
    command:
      - dnsmasq
      - --keep-in-foreground
        #- --no-daemon
        #- --log-queries
      - --domain-needed
      - --no-hosts
      - --cache-size=3000
      - --hostsdir=/etc/dns
  abc:
    image: abc
    container_name: abc
    dns:
      - 192.168.100.200
  …………
networks:
  default:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.100.0/24

测试解析和查看 dnsmasq 缓存情况,evictions为0最好

[yaoge123]# run --rm -it --network=docker_default --dns=192.168.100.200 alpine sh
/ # apk add bind-tools
/ # dig +short node_name
/ # for i in "cachesize.bind insertions.bind evictions.bind misses.bind hits.bind auth.bind servers.bind";do dig +short chaos txt $i;done

使用 Singularity 容器升级固件

构建一个安装了SST的CentOS8容器

singularity build –sandbox sst-build docker://centos:8.4.2105

cp sst-*.x86_64.rpm sst-build/home

singularity shell -w sst-build

rpm -ivh /home/sst-*.x86_64.rpm

rm /home/sst-*.x86_64.rpm

exit

singularity build sst.sif sst-build

到节点上升级

singularity shell –writable-tmpfs sst.sif

sst show -ssd

sst load -ssd 0

sst load -ssd 1

……

exit

reboot

 

Prometheus + Grafana 监控 NVIDIA GPU

1.首先安装 NVIDIA Data Center GPU Manager (DCGM),从 https://developer.nvidia.com/dcgm 下载安装

nv-hostengine -t
yum erase -y datacenter-gpu-manager
rpm -ivh datacenter-gpu-manager*
systemctl enable --now dcgm.service

2. 安装 NVIDIA DCGM exporter for Prometheus,从 https://github.com/NVIDIA/gpu-monitoring-tools/tree/master/exporters/prometheus-dcgm 下载手工安装

wget -q -O /usr/local/bin/dcgm-exporter https://raw.githubusercontent.com/NVIDIA/gpu-monitoring-tools/master/exporters/prometheus-dcgm/dcgm-exporter/dcgm-exporter
chmod +x /usr/local/bin/dcgm-exporter
mkdir /run/prometheus 
wget -q -O /etc/systemd/system/prometheus-dcgm.service https://raw.githubusercontent.com/NVIDIA/gpu-monitoring-tools/master/exporters/prometheus-dcgm/bare-metal/prometheus-dcgm.service
systemctl daemon-reload
systemctl enable --now prometheus-dcgm.service

3. 从 https://prometheus.io/download/#node_exporter 下载 node_exporter,手工安装为服务并添加 dcgm-exporter 资料

tar xf node_exporter*.tar.gz
mv node_exporter-*/node_exporter /usr/local/bin/
chown root:root /usr/local/bin/node_exporter
chmod +x /usr/local/bin/node_exporter

cat > /etc/systemd/system/node_exporter.service <<EOF
[Unit]
Description=Prometheus Node Exporter
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target
EOF

sed -i '/ExecStart=\/usr\/local\/bin\/node_exporter/c\ExecStart=\/usr\/local\/bin\/node_exporter --collector.textfile.directory=\/run\/prometheus' /etc/systemd/system/node_exporter.service

systemctl daemon-reload
systemctl enable --now node_exporter.service

4. Grafana 添加这个Dashboard
https://grafana.com/grafana/dashboards/11752

Docker 自动更新 Let’s Encrypt

在nginx的 docker run 中添加webroot和配置文件挂载

-v $PWD/nginx/letsencrypt/:/var/www/letsencrypt:ro \
-v $PWD/letsencrypt/etc/:/etc/nginx/letsencrypt/:ro \

在nginx中将wwwroot发布出去

location ^~ /.well-known/ {
    root /var/www/letsencrypt/;
}

在nginx中配置证书文件

ssl_certificate letsencrypt/live/www.yaoge123.com/fullchain.pem;
ssl_certificate_key letsencrypt/live/www.yaoge123.com/privkey.pem;

创建 certbot 的docker run脚本,以后只要周期性运行这个脚本就可以自动更新证书了

#!/bin/sh
cd $(dirname $0)
pwd

docker run -it --rm \
	-v $PWD/letsencrypt/etc:/etc/letsencrypt \
	-v $PWD/letsencrypt/lib:/var/lib/letsencrypt \
	-v $PWD/letsencrypt/log:/var/log/letsencrypt \
	-v $PWD/nginx/letsencrypt:/var/www \
	certbot/certbot \
	certonly --webroot \
	--email yaoge123@example.com --agree-tos --no-eff-email \
	--webroot-path=/var/www/ \
	-n \
	--domains www.yaoge123.com
docker kill --signal=HUP nginx

CentOS 7 YUM 安装 Cacti

先添加EPEL再用yum安装cacti和中文字体

yum install cacti cacti-spine mariadb-server google-noto-sans-simplified-chinese-fonts

编辑 /etc/httpd/conf.d/cacti.conf ,在 Directory /usr/share/cacti/ 中添加可访问的浏览器客户端

编辑 /etc/cron.d/cacti ,去掉注释

编辑 /etc/spine.conf ,注释RDB_*

创建数据库

[root@yaoge123]# mysqladmin --user=root create cacti

创建数据库用户

[root@yaoge123]# mysql --user=root mysql
MariaDB [mysql]> GRANT ALL ON cacti.* TO cactiuser@localhost IDENTIFIED BY 'cactiuser';
MariaDB [mysql]> flush privileges;

数据库用户增加 timezone 权限

[root@yaoge123]# mysql -u root
MariaDB [(none)]> GRANT SELECT ON mysql.time_zone_name TO cactiuser@localhost IDENTIFIED BY 'cactiuser';
MariaDB [(none)]> flush privileges;

数据库增加 timezone

[root@yaoge123]# mysql_tzinfo_to_sql /usr/share/zoneinfo/ | mysql -u root mysql

新建一个文件 /etc/my.cnf.d/cacti.cnf ,内容供参考根据实际情况修改

[mysqld]
character-set-client = utf8mb4
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_additional_mem_pool_size = 80M
innodb_buffer_pool_size = 1024M
innodb_doublewrite = ON
innodb_file_format = Barracuda
innodb_file_per_table = ON
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_large_prefix = ON
join_buffer_size = 748M
max_allowed_packet = 16777216
max_heap_table_size = 374M
tmp_table_size = 374M

重启相关服务,设置开机自动启动

systemctl restart mariadb
systemctl enable mariadb
systemctl restart httpd
systemctl enable httpd

导入数据库

[root@yaoge123]# mysql cacti < /usr/share/doc/cacti-*/cacti.sql

浏览器打开 http://<server>/cacti/ ,默认用户名密码为 admin/admin