AI API 网关迁移与流量优化实战
背景
最近我将一个自建的 AI API 网关从旧服务器迁移到新服务器,期间遇到了一些典型的运维问题,包括:
- 数据库从 3.3GB 压缩到 30MB
- 日均流量从 20GB 降到 10GB 以下
- 反向代理从 Nginx 切换到 Caddy
- 数据库异地自动备份
这篇文章记录了我遇到的问题和解决方案,供有类似需求的朋友参考。
一、数据库迁移:只带业务数据,不带日志
初始数据库有 3.3GB ,但其中 90% 以上是无价值的日志数据。通过 pg_dump 排除日志表,核心业务数据只有 几十 MB。
排除大日志表
sql
-- 找出哪些表占空间最大
SELECT table_name,
pg_size_pretty(pg_total_relation_size(table_name::text)) as total_size
FROM (SELECT tablename FROM pg_tables WHERE schemaname = 'public') t
ORDER BY pg_total_relation_size(table_name::text) DESC
LIMIT 10;
结果发现前 5 张日志表占了绝大部分空间:
| 表 | 大小 | 说明 |
|---|---|---|
| ops_system_logs | 2.2 GB | 系统操作日志 |
| usage_logs | 425 MB | API 调用记录 |
| usage_billing_dedup | 273 MB | 计费去重 |
| ops_error_logs | 260 MB | 错误日志 |
| scheduler_outbox | 63 MB | 调度队列 |
pg_dump 排除特定表
bash
pg_dump -U user -d database \
-T ops_system_logs \
-T ops_error_logs \
-T usage_logs \
-T scheduler_outbox \
--no-owner \
--no-acl \
-Fc \
-f backup.dump
注意 :如果应用代码里引用了这些表的索引或外键(比如 billing_usage_entries 引用了 usage_logs.id),需要在目标库手动补上空表结构,只建表不插数据:
bash
# 从源库导出这些表的 schema(不含数据)
pg_dump -U user -d database \
-t ops_system_logs \
--schema-only > missing_tables.sql
# 在目标库执行
psql -U user -d database < missing_tables.sql
效果
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 数据库大小 | 3.3 GB | 31 MB |
| 用户数 | 3,234 | 3,234(一致 ✅) |
| 迁移耗时 | --- | 15 秒 |
二、Nginx → Caddy 切换
原服务器使用 Nginx 做反向代理,切换到 Caddy 后获得了几个好处:
为什么换 Caddy
- 自动 HTTPS --- 无需手动申请和续期 Let's Encrypt
- 内置 zstd 压缩 --- 比 gzip 压缩率更高
- 更简洁的配置 --- 优雅的 Caddyfile 语法
- 原生 HTTP/2 和 HTTP/3 支持
Caddyfile 配置
caddy
api.example.com {
# 反向代理到后端
reverse_proxy localhost:8080 {
health_uri /health
health_interval 30s
health_timeout 10s
health_status 200
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
transport http {
keepalive 120s
keepalive_idle_conns 256
read_buffer 16KB
write_buffer 16KB
}
fail_duration 30s
max_fails 3
unhealthy_status 500 502 503 504
}
# 压缩配置(zstd + gzip 双支持)
encode {
zstd
gzip 6
minimum_length 256
match {
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type text/*
}
}
# 请求体限制
request_body {
max_size 50MB
}
}
压缩效果对比
以 JSON API 响应(35KB)为例:
| 压缩方式 | 大小 | 压缩比 |
|---|---|---|
| 无压缩 | 35.7 KB | --- |
| gzip | 15.7 KB | 56% |
| zstd | 15.9 KB | 55% |
实际测试对文本类 API 响应,压缩率在 50-70%。
三、流量分析:找出真正的消耗来源
问题场景
服务器流量消耗异常快,怀疑是被攻击或有异常请求。
排查步骤
1. 检查 nginx/Caddy 日志中的实际流量
bash
# 统计每日流量
sudo awk '{date=substr($4,2,11); bytes[date]+=$10}
END{for(d in bytes) printf "%s %.2f GB\n", d, bytes[d]/1024/1024/1024}'
/var/log/caddy/api.log | sort
2. 按 URL 路径分析流量分布
bash
sudo awk '{urls[$7]+=$10; count[$7]++}
END{for(u in urls) printf "%.2f GB (%d reqs) %s\n", urls[u]/1024/1024/1024, count[u], u}'
/var/log/caddy/api.log | sort -rn | head -10
3. 找出单次响应异常的请求
bash
sudo awk '$7 == "/responses" {if($10>max){max=$10; line=$0}}
END{printf "最大响应: %.1f MB\n%s\n", max/1024/1024, line}' access.log
发现
/responses(AI 聊天 API)占流量的 80% 以上- 有人单次请求生成了 289.7 MB 的响应(疑似图片生成)
- 大部分用户平均响应只有 220KB
- 请求体限制之前配置为 256MB,过于宽松
优化措施
| 措施 | 效果 |
|---|---|
| 开启 zstd/gzip 压缩 | API 响应缩小 50-60% |
| 请求体限制 256MB → 50MB | 防止单次异常消耗 |
| 给高消耗用户加 RPM 限速 | 控制总请求量 |
四、自动备份脚本
每天凌晨自动备份数据库,排除日志表,保留 7 天。备份同时传输到远程服务器做异地容灾。
bash
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/data/backups"
REMOTE_SERVER="user@backup.example.com"
RETENTION_DAYS=7
# 排除的日志表
EXCLUDE_TABLES=(
ops_system_logs
ops_error_logs
usage_logs
scheduler_outbox
usage_billing_dedup
)
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="db_backup_$(date +%Y%m%d_%H%M%S).dump"
# 构建排除参数
EXCLUDE_ARGS=""
for table in "${EXCLUDE_TABLES[@]}"; do
EXCLUDE_ARGS="$EXCLUDE_ARGS -T $table"
done
# 备份
docker exec postgres pg_dump -U user -d database \
$EXCLUDE_ARGS --no-owner --no-acl -Fc > "$BACKUP_PATH"
# 验证备份完整性
docker run --rm postgres:18-alpine pg_restore -l "$BACKUP_FILE" || exit 1
# 传输到远程
scp "$BACKUP_PATH" "${REMOTE_SERVER}:${BACKUP_DIR}/"
# 清理过期备份
find "$BACKUP_DIR" -name "*.dump" -type f -mtime +$RETENTION_DAYS -delete
设置定时任务:
bash
echo "0 5 * * * root /opt/scripts/daily_backup.sh" > /etc/cron.d/db-backup
五、常见问题排查
503 Service Unavailable
切换到 Caddy 后可能遇到 503:
原因 :Caddy 的健康检查发现后端不可用后,会将后端标记为不可用一段时间(fail_duration 30s)。
解决:
bash
# 重启后端后需要重载 Caddy
caddy reload --config /etc/caddy/Caddyfile
或者修改健康检查配置,降低判定阈值:
caddy
reverse_proxy localhost:8080 {
health_uri /health
health_interval 10s
health_timeout 5s
fail_duration 10s
max_fails 1
}
413 Payload Too Large
请求体超出限制时返回 413。
排查:
bash
# 查看 nginx/Caddy 错误日志
grep "413" /var/log/caddy/*.log
# 确认当前限制值
grep max_size /etc/caddy/Caddyfile
如果通过 Cloudflare,还需要注意 Cloudflare 免费版限制了 100MB 的最大请求体,超过会被 Cloudflare 直接拦截。
总结
这次迁移总结了几点经验:
- 数据库日志要定期清理 --- 设计好保留策略,不然日志会占满磁盘
- 反向代理优先选 Caddy --- 配置简单,自动 HTTPS,自带 zstd 压缩
- 流量分析要找源头 --- 不要盲目扩带宽,先看流量花在哪
- 备份要验证 --- 用
pg_restore -l检查备份完整性,否则等于没备份 - 异地备份 --- 主备两台服务器互相备份,防止单点故障
代码和配置示例仅供参考,实际部署需要根据具体环境调整。