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

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'

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;
}

为vSAN启用RDMA

交换机上启用LLDP、PFC和DCBx,PFC优先级为3(默认值),DCBx的标准为IEEE(默认值)。

dcb pfc
#
interface 25GE1/0/10
 description Host-01
 port link-type trunk
 undo port trunk allow-pass vlan 1
 port trunk allow-pass vlan 125
 stp edged-port enable
 lldp tlv-enable dcbx
 dcb pfc enable mode manual
#
interface 25GE1/0/12
 description Host-02
 port link-type trunk
 undo port trunk allow-pass vlan 1
 port trunk allow-pass vlan 125
 stp edged-port enable
 lldp tlv-enable dcbx
 dcb pfc enable mode manual
#
lldp enable
#

通过主机-配置-物理适配器确认用于vSAN网卡的设备位置和端口,以下示例中是0000:8a:00的端口2(vmnic3)和0000:8b:00的端口1(vmnic4),首先查看一下LLDP和DCBx的相关配置。需要先安装MFT才能使用相关命令。

[root@yaoge123:~] /opt/mellanox/bin/mlxconfig -d 0000:8a:00.1 query|grep -iE "dcb|lldp"
        LLDP_NB_DCBX_P1                             False(0)            
        LLDP_NB_RX_MODE_P1                          OFF(0)              
        LLDP_NB_TX_MODE_P1                          OFF(0)              
        LLDP_NB_DCBX_P2                             False(0)            
        LLDP_NB_RX_MODE_P2                          OFF(0)              
        LLDP_NB_TX_MODE_P2                          OFF(0)              
        DCBX_IEEE_P1                                True(1)             
        DCBX_CEE_P1                                 True(1)             
        DCBX_WILLING_P1                             True(1)             
        DCBX_IEEE_P2                                True(1)             
        DCBX_CEE_P2                                 True(1)             
        DCBX_WILLING_P2                             True(1)             

[root@yaoge123:~] /opt/mellanox/bin/mlxconfig -d 0000:8b:00.0 query|grep -iE "dcb|lldp"
        LLDP_NB_DCBX_P1                             False(0)            
        LLDP_NB_RX_MODE_P1                          OFF(0)              
        LLDP_NB_TX_MODE_P1                          OFF(0)              
        LLDP_NB_DCBX_P2                             False(0)            
        LLDP_NB_RX_MODE_P2                          OFF(0)              
        LLDP_NB_TX_MODE_P2                          OFF(0)              
        DCBX_IEEE_P1                                True(1)             
        DCBX_CEE_P1                                 True(1)             
        DCBX_WILLING_P1                             True(1)             
        DCBX_IEEE_P2                                True(1)             
        DCBX_CEE_P2                                 True(1)             
        DCBX_WILLING_P2                             True(1)       

发现LLDP没有启动,启用LLDP和DCBx,DCBx IEEE已启用。

[root@yaoge123:~] /opt/mellanox/bin/mlxconfig -d 0000:8a:00.1 set LLDP_NB_DCBX_P2=1 LLDP_NB_RX_MODE_P2=2 LLDP_NB_TX_MODE_P2=2 DCBX_WILLING_P2=1 DCBX_IEEE_P2=1 

Device #1:
----------

Device type:        ConnectX5           
Name:               MCX512A-ACU_Ax_Bx   
Description:        ConnectX-5 EN network interface card; 10/25GbE dual-port SFP28; PCIe3.0 x8; UEFI Enabled (x86/ARM)
Device:             0000:8a:00.1        

Configurations:                                          Next Boot       New
        LLDP_NB_DCBX_P2                             False(0)             True(1)             
        LLDP_NB_RX_MODE_P2                          OFF(0)               ALL(2)              
        LLDP_NB_TX_MODE_P2                          OFF(0)               ALL(2)              
        DCBX_WILLING_P2                             True(1)              True(1)             
        DCBX_IEEE_P2                                True(1)              True(1)             

 Apply new Configuration? (y/n) [n] : y
Applying... Done!
-I- Please reboot machine to load new configurations.

[root@yaoge123:~] /opt/mellanox/bin/mlxconfig -d 0000:8b:00.0 set LLDP_NB_DCBX_P1=1 LLDP_NB_RX_MODE_P1=2 LLDP_NB_TX_MODE_P1=2 DCBX_WILLING_P1=1 DCBX_IEEE_P1=1 

Device #1:
----------

Device type:        ConnectX5           
Name:               MCX512A-ACU_Ax_Bx   
Description:        ConnectX-5 EN network interface card; 10/25GbE dual-port SFP28; PCIe3.0 x8; UEFI Enabled (x86/ARM)
Device:             0000:8b:00.0        

Configurations:                                          Next Boot       New
        LLDP_NB_DCBX_P1                             False(0)             True(1)             
        LLDP_NB_RX_MODE_P1                          OFF(0)               ALL(2)              
        LLDP_NB_TX_MODE_P1                          OFF(0)               ALL(2)              
        DCBX_WILLING_P1                             True(1)              True(1)             
        DCBX_IEEE_P1                                True(1)              True(1)             

 Apply new Configuration? (y/n) [n] : y
Applying... Done!
-I- Please reboot machine to load new configurations.

重启后查看网卡当前配置,确认LLDP和DCBx均已启用

[root@yaoge123:~] /opt/mellanox/bin/mlxconfig -d 0000:8a:00.1 query|grep -iE "dcb|lldp"
        LLDP_NB_DCBX_P1                             False(0)            
        LLDP_NB_RX_MODE_P1                          OFF(0)              
        LLDP_NB_TX_MODE_P1                          OFF(0)              
        LLDP_NB_DCBX_P2                             True(1)             
        LLDP_NB_RX_MODE_P2                          ALL(2)              
        LLDP_NB_TX_MODE_P2                          ALL(2)              
        DCBX_IEEE_P1                                True(1)             
        DCBX_CEE_P1                                 True(1)             
        DCBX_WILLING_P1                             True(1)             
        DCBX_IEEE_P2                                True(1)             
        DCBX_CEE_P2                                 True(1)             
        DCBX_WILLING_P2                             True(1)             
[root@yaoge123:~] /opt/mellanox/bin/mlxconfig -d 0000:8b:00.0 query|grep -iE "dcb|lldp"
        LLDP_NB_DCBX_P1                             True(1)             
        LLDP_NB_RX_MODE_P1                          ALL(2)              
        LLDP_NB_TX_MODE_P1                          ALL(2)              
        LLDP_NB_DCBX_P2                             False(0)            
        LLDP_NB_RX_MODE_P2                          OFF(0)              
        LLDP_NB_TX_MODE_P2                          OFF(0)              
        DCBX_IEEE_P1                                True(1)             
        DCBX_CEE_P1                                 True(1)             
        DCBX_WILLING_P1                             True(1)             
        DCBX_IEEE_P2                                True(1)             
        DCBX_CEE_P2                                 True(1)             
        DCBX_WILLING_P2                             True(1)             

查看网卡的DCB状态,模式为IEEE,PFC已启用且优先级为3(0 0 0 1 0 0 0 0)

[root@2288Hv6-05:~] esxcli network nic dcb status get -n vmnic3
   Nic Name: vmnic3
   Mode: 3 - IEEE Mode
   Enabled: true
   Capabilities: 
         Priority Group: true
         Priority Flow Control: true
         PG Traffic Classes: 8
         PFC Traffic Classes: 8
   PFC Enabled: true
   PFC Configuration: 0 0 0 1 0 0 0 0
   IEEE ETS Configuration: 
         Willing Bit In ETS Config TLV: 1
         Supported Capacity: 8
         Credit Based Shaper ETS Algorithm Supported: 0x0
         TX Bandwidth Per TC: 13 13 13 13 12 12 12 12
         RX Bandwidth Per TC: 13 13 13 13 12 12 12 12
         TSA Assignment Table Per TC: 2 2 2 2 2 2 2 2
         Priority Assignment Per TC: 1 0 2 3 4 5 6 7
         Recommended TC Bandwidth Per TC: 13 13 13 13 12 12 12 12
         Recommended TSA Assignment Per TC: 2 2 2 2 2 2 2 2
         Recommended Priority Assignment Per TC: 1 0 2 3 4 5 6 7
   IEEE PFC Configuration: 
         Number Of Traffic Classes: 8
         PFC Configuration: 0 0 0 1 0 0 0 0
         Macsec Bypass Capability Is Enabled: 0
         Round Trip Propagation Delay Of Link: 0
         Sent PFC Frames: 0 0 0 0 0 0 0 0
         Received PFC Frames: 0 0 0 0 0 0 0 0
   DCB Apps: 
[root@2288Hv6-05:~] esxcli network nic dcb status get -n vmnic4
   Nic Name: vmnic4
   Mode: 3 - IEEE Mode
   Enabled: true
   Capabilities: 
         Priority Group: true
         Priority Flow Control: true
         PG Traffic Classes: 8
         PFC Traffic Classes: 8
   PFC Enabled: true
   PFC Configuration: 0 0 0 1 0 0 0 0
   IEEE ETS Configuration: 
         Willing Bit In ETS Config TLV: 1
         Supported Capacity: 8
         Credit Based Shaper ETS Algorithm Supported: 0x0
         TX Bandwidth Per TC: 13 13 13 13 12 12 12 12
         RX Bandwidth Per TC: 13 13 13 13 12 12 12 12
         TSA Assignment Table Per TC: 2 2 2 2 2 2 2 2
         Priority Assignment Per TC: 1 0 2 3 4 5 6 7
         Recommended TC Bandwidth Per TC: 13 13 13 13 12 12 12 12
         Recommended TSA Assignment Per TC: 2 2 2 2 2 2 2 2
         Recommended Priority Assignment Per TC: 1 0 2 3 4 5 6 7
   IEEE PFC Configuration: 
         Number Of Traffic Classes: 8
         PFC Configuration: 0 0 0 1 0 0 0 0
         Macsec Bypass Capability Is Enabled: 0
         Round Trip Propagation Delay Of Link: 0
         Sent PFC Frames: 0 0 0 0 0 0 0 0
         Received PFC Frames: 0 0 0 0 0 0 0 0
   DCB Apps: 

开源镜像站缓存加速

除了某些土豪站用全闪以外,大多数开源镜像站都是以机械盘为主,当带宽充裕的时候IO显然会成为瓶颈,那么采用少量的SSD来提升性能几乎是唯一的办法。加SSD的方法有很多种:

  1. lvmcache bcache bcachefs:因在用ZFS所以没有测试这类方案,看上去比较复杂;
  2. ZFS L2ARC:实际测试基本无效果,zpool iostat 看L2ARC读取很少;
  3. nginx proxy_cache:效果显著,但是必须用反代。

proxy_cache配置在前端nginx上,前端nginx访问SSD,前端nginx需要编译安装ngx_cache_purge以便主动清除缓存。前端nginx反代给后端nginx,后端nginx访问机械盘提供实际的数据。当然也可以一个nginx同时访问SSD和机械盘,这个nginx自己反代给自己,因为只有反代了才能缓存,前端nginx缓存配置如下:

http {
    ……
    proxy_cache_path /cache levels=1:2 use_temp_path=off keys_zone=mirror:64m inactive=24h max_size=2600g;
    ……
}
server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;
    listen *:443 quic;
    listen [::]:443 quic;
    server_name mirror.nju.edu.cn mirrors.nju.edu.cn;
    ……
    proxy_cache mirror;
    proxy_cache_key $request_uri;
    proxy_cache_valid 200 24h;
    proxy_cache_valid 301 302 1h;
    proxy_cache_valid any 1m;
    proxy_cache_lock on;
    proxy_cache_lock_age 3s;
    proxy_cache_lock_timeout 3s;
    proxy_cache_use_stale error timeout updating;
    proxy_cache_background_update on;
    proxy_cache_revalidate on;
    cache_purge_response_type text;
    proxy_cache_purge PURGE from 127.0.0.1 192.168.10.10;
    add_header X-Cache-Status $upstream_cache_status;
    ……
    location / {
        proxy_pass http://192.168.10.10:8000;

        ……
    }
    ……
}
  • proxy_cache_path:
    use_temp_path=off 临时文件直接放到 cache 目录中,防止文件在 temp_path 和 cache_path 之间移动。缓存文件会先放在 temp_path 中,下载完了再移动到 cache_path 下,设定成 off 比较简单安全。
    keys_zone=mirror:64m 内存缓存区名称是 mirror 容量是64MB,每MB内存可以缓存八千个key,可以估算一下key的数量设定缓存区大小。
    inactive=6h 超过6小时没有访问的缓存会被删除,无论是否 valid,防止缓存被长期未用的数据占用。
    max_size=1946g 要比缓存目录 /cache 容量稍小,如果缓存用尽 nginx 会报错 [crit] …… failed (2: No such file or directory) 而且会直接断开客户端链接。
  • proxy_cache mirror:匹配前面 proxy_cache_path 的内存缓存区名称。
  • proxy_cache_key $request_uri:原始请求的URL,用这个URL用作缓存的key,只要这个key(请求URL)一样则nginx用缓存中一样的数据回复。
  • proxy_cache_valid 200 24h; proxy_cache_valid 301 302 1h; proxy_cache_valid any 1m;:HTTP状态码200的缓存24小时、301/302的缓存1小时、其它的缓存1分钟,缓存达到这个时间就会被抛弃,防止数据被长期缓存不能得到更新。
  • proxy_cache_lock on:当有多个请求同一个文件时(同一个key),只有第一个请求从反代中获取,通过加锁让后续的请求等待第一个请求完成后从缓存中获取,防止并发从后端中请求同一个文件,加重后端负载。
  • proxy_cache_lock_age 3s:第一个请求锁定3秒,到期后就释放锁,下一个请求发送到后端。
  • proxy_cache_lock_timeout 3s:后续的请求如果等待3秒还未从缓存中获取,就直接从后端获取,但是不缓存。
  • proxy_cache_use_stale error timeout updating:当请求后端时出现错误(error)或超时(timeout)时使用过期缓存,如果因缓存过期正在更新中(updating)时使用过期缓存,过期的缓存总比没有好。
  • proxy_cache_background_update on:允许后台请求过期的缓存。
  • proxy_cache_revalidate on:更新缓存时使用 If-Modified-Since 和 If-None-Match 请求后端,后端可以返回304而非200减少传输。
  • cache_purge_response_type text:清除缓存后返回文本格式的结果。
  • proxy_cache_purge PURGE from 127.0.0.1 192.168.10.10:使用PURGE这个HTTP方法清除缓存,允许本地和192.168.10.10来清除。
  • add_header X-Cache-Status $upstream_cache_status:添加HTTP头X-Cache-Status以便客户端知道缓存情况。

后端nginx可能需要设定某些文件的缓存有效期,比如提供给mirrorz的相关文件需要根据不同的请求站点设置不同的 Access-Control-Allow-Origin 头以实现跨域,但是URL相同则前端缓存key相同,前端无法根据不同的请求站点回复不同的HTTP头,在缓存有效期内均回复第一次访问缓存下来的HTTP头,所以比较简单的方法就是直接禁用有关文件的缓存:

server {
    listen 8000;
    root /mirror;
    ……
    location ~ ^/mirrorz/ {
        expires -1;
        ……
    }
    ……
}
  • expires -1:负值禁用缓存,通过添加HTTP头 Cache-Control: no-cache 和 Expires,Expires被设定为当前时间之前,这样前端nginx就不会缓存 mirrorz 的URL了。

编辑 tunasync worker 的配置文件添加一个脚本,使得每个同步作业结束后均清除前端上这个镜像的所有缓存:

#tunasync worker的配置文件
[global]
……
exec_on_success = [ "/home/mirror/postexec.sh" ]
exec_on_failure = [ "/home/mirror/postexec.sh" ]
……

#上述的postexec.sh脚本内容
#!/bin/bash
MIRROR_DIR="/mirror/"
DIR="${TUNASYNC_WORKING_DIR/"$MIRROR_DIR"/}"
/usr/bin/curl --silent --output /dev/null --request PURGE "https://mirror.nju.edu.cn/$DIR/*"

镜像站某些状态文件会频繁的周期更新,每次更新后都用 curl --silent --output /dev/null --request PURGE "https://mirror.nju.edu.cn/status/*" 清除一下缓存。

NVMe over RoCE 网络规划

NVMe over RoCE 存储系统的网络应认真规划,下面以一个简单的系统为例说明:

  • 一台双控制器(A控、B控)NVMe存储,每个控制器上有四个NVMe over RoCE的网口(A控A1-A4、B控B1-B4)
  • 两台无损以太网交换机(交换机A、交换机B)
  • 两台服务器(服务器1、服务器2)
  • 每台服务器有两个RoCE网卡(服务器X网卡1、服务器X网卡2),每个网卡有两个网口(服务器X网卡1A、服务器X网卡1B、服务器X网卡2A、服务器X网卡2B)
  • VLAN 123-124 用于 NVMe over RoCE、VLAN 99 用于业务网

交换机A的VLAN 123(192.168.123.0/24)用于传输NVMe over RoCE、交换机B用VLAN 124(192.168.124.0/24)用于传输NVMe over RoCE。无论是控制器、交换机、网卡坏任意一个,甚至每类都任意坏一个,均不影响业务连续性

接口交换机链接VLANNVMe over RoCE IP
存储A控A1交换机A123192.168.123.101
存储A控A2交换机A123192.168.123.102
存储A控A3交换机B124192.168.124.101
存储A控A4交换机B124192.168.124.102
存储B控B1交换机A123192.168.123.103
存储B控B2交换机A123192.168.123.104
存储B控B3交换机B124192.168.124.103
存储B控B4交换机B124192.168.124.104
服务器1网卡1A交换机A123192.168.123.11
服务器1网卡1B交换机B99业务网
服务器1网卡2A交换机A99业务网
服务器1网卡1B交换机B124192.168.124.11
服务器2网卡1A交换机A123192.168.123.12
服务器2网卡1B交换机B99业务网
服务器2网卡2A交换机A99业务网
服务器2网卡2B交换机B124192.168.124.12

VMware ESXi 配置 NVMe over RoCE

NVMe over RoCE 的相关配置需要在 vCenter Server Client 下面操作,ESXi Host Client 下是没有相关选项的,至少图形界面是这样,用 esxicli 估计可以直接在 ESXi 下面配置。所以需要先用本地的盘部署一套VCSA,精简配置大约150GB就够了,等NVMe配置好再迁移到存储上。

  • 在配置-网络-RDMA 适配器下确认RDMA适配器(vmrdmaX)对应的物理网络适配器(vmnicX),可通过交换机 shutdown 等方法确认规划用于 NVMe 的端口对应的物理网络适配器名称。
    如 vmrdma0 对应 vmnic2、vmrdma3 对应 vmnic5。
  • 在配置-网络-虚拟交换机下添加网络,选择 VMkernel 网络适配器、选择新建标准交换机、输入MTU(Dorado 推荐5500),选择一个用于NVMe的物理网络适配器,VLAN ID 填写对应交换机的 NVMe over RoCE 用 VLAN,可用服务中仅勾选 NVMe over RDMA,配置静态IPv4地址。
  • 重复上述过程,为每个 NVMe 端口对应的物理网络适配器创建独立的虚拟交换机并配置独立的 VLAN、独立的 VMkernel 端口,实现每个 VMKernel 和物理网络适配器之间一一映射。
    如 vSwitch1 绑定物理网络适配器 vmnic2、VMkernel 端口 vmk1 192.168.123.11、VLAN ID  123;vSwitch2 绑定物理网络适配器 vmnic5、VMkernel 端口 vmk2 192.168.124.11、VLAN ID  124。
  • 在配置-存储-存储适配器-添加软件适配器,添加 NVMe over RDMA 适配器,选择对应的RDMA设备。重复该过程为每个 NVMe 的 RDMA适配器 添加 软件适配器。
    如为 vmrdma0 添加 vmhba64,为 vmrdma3 添加 vmhba65
  • 在配置-存储-存储适配器中,选中刚刚添加的vmhbaXX(VMware NVME over RDMA Storage Adapter),在控制器中添加控制器,填写子系统NQN(存储的NQN)、该网络对应VLAN下的存储IP,保持活动时间 Dorado 推荐5秒,重复此过程添加存储在该VLAN下的所有IP。
    如 vmhba64 添加有 192.168.123.101-104 四个控制器
  • 重复上述过程给剩下的 vmhbaXX 添加控制器。
    vmhba65 添加有 192.168.124.101-104 四个控制器
  • 复查所有访问路径 esxcli nvme fabrics connection list
  • 上述的每个控制器如果都可以访问同一个LUN,那么每一个都是一个多路径。7.0最多4条,8.0最多8条,8.0U1最多32条。
  • 最终结果如下表所示,8个路径 = 2个物理网络适配器 x 4个控制器
RDMA适配器VMkernel适配器VMkernel 适配器 IP物理适配器虚拟交换机VLAN存储适配器存储控制器 IP
vmrdma0vmk1192.168.123.11vmnic2vSwitch1123vmhba64192.168.123.101-104
vmrdma3vmk2192.168.124.11vmnic5vSwitch2124vmhba65192.168.124.101-104

NVMe over RoCE 网络配置

NVMe over RoCE 要求无损网络,因此交换机和主机网卡均需要一些配置来保证无损。

QoS标记采用PCP为例,PCP是在VLAN标签中标记优先级,所以流量必须在VLAN中,Dorado 不是所有的前端接口卡都支持DSCP。

  • 交换机启用PFC优先级3,1分钟发生死锁20次,关闭端口PFC
  • 交换机配置PFC死锁检测,检测时间1000ms,恢复时间1500ms
  • 交换机与存储和服务器链接用于NVMe over RoCE的所有端口配置PFC、VLAN和MTU
  • 两台交换机使用不同的VLAN,防止出现跨交换机的流量
  • 交换机在队列3启用AI-ECN并加载集中存储模型
  • 交换机启用LLDP(iNOF的基本要求)
  • 交换机启用简单的iNOF(默认域、无反射器)
dcb pfc nof
 priority 3 turn-off threshold 20
#
dcb pfc deadlock-detect timer 1000
dcb pfc deadlock-recovery timer 1500
#
interface 25GE1/0/1
 description Dorado-A
 port link-type trunk
 port trunk allow-pass vlan 123
 stp edged-port enable
 dcb pfc enable nof mode manual
 jumboframe enable 5500
#               
interface 25GE1/0/2
 description Dorado-A
 port link-type trunk
 port trunk allow-pass vlan 123
 stp edged-port enable
 dcb pfc enable nof mode manual
 jumboframe enable 5500
#
interface 25GE1/0/3
 description Dorado-B
 port link-type trunk
 port trunk allow-pass vlan 123
 stp edged-port enable
 dcb pfc enable nof mode manual
 jumboframe enable 5500
#               
interface 25GE1/0/4
 description Dorado-B
 port link-type trunk
 port trunk allow-pass vlan 123
 stp edged-port enable
 dcb pfc enable nof mode manual
 jumboframe enable 5500
#
interface 25GE1/0/9
 description Host-01
 port link-type trunk
 port trunk allow-pass vlan 123
 stp edged-port enable
 dcb pfc enable nof mode manual
 jumboframe enable 5500
#
interface 25GE1/0/11
 description Host-02
 port link-type trunk
 port trunk allow-pass vlan 123
 stp edged-port enable
 dcb pfc enable nof mode manual
 jumboframe enable 5500
#
lldp enable
#
ai-service
 #
 ai-ecn
  ai-ecn enable
  assign queue 3 model ai_ecn_centralizedstorage
 #
 inof
#
return
  • 为确保 RoCE 流量无损,在 ESXi 主机中将 PFC 优先级值配置为 3
[root@yaoge123:~] esxcli system module parameters set -m nmlx5_core -p "pfctx=0x08 pfcrx=0x08"
  • 重启后查看,pfctx和pfcrx值均为0x08
[root@yaoge123:~] esxcli system module parameters list -m nmlx5_core | grep pfc
pfcrx                int            0x08   Priority based Flow Control policy on RX.
   Notes: Must be equal to pfctx.
pfctx                int            0x08   Priority based Flow Control policy on TX.
   Notes: Must be equal to pfcrx.

GitLab CVE-2023-7028 临时应对措施

一个非常无语的高危漏洞,任何人都可以重置任意账号的密码然后发送到指定邮箱

对于容器部署来说,直接修改 /var/opt/gitlab/nginx/conf/gitlab-http.conf 文件,在其中添加

location /users/password { 
return 444;
}

然后重新加载nginx配置文件

gitlab-ctl hup nginx