适用场景
CLIProxyAPI 的 usage 统计默认在内存中,进程重启后会丢失。本方案基于
/v0/management/usage/export与/v0/management/usage/import实现可恢复持久化。
1. 方案原因
- usage 统计是内存态,服务重启会清空。
- 官方有 management API 的 export/import,适合做外部持久化。
- 仅靠停机导出仍有风险(断电、
kill -9不触发ExecStop),因此增加周期导出。
2. 方案说明
2.1 设计目标
- 启动后自动导入历史 usage。
- 停止前自动导出最新 usage。
- 运行中每 5 分钟自动导出一次,降低异常中断损失。
2.2 文件职责
/etc/systemd/system/cli-proxy-api.service
- 主服务 unit(通过
ExecStartPost与ExecStop提供启停钩子)。
/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh
- 实际执行 export/import。
- 从
/opt/cli-proxy-api/config/.management-key读取管理密钥。 - 备份文件写入
/opt/cli-proxy-api/export/usage-stats-backup.json。
/etc/systemd/system/cli-proxy-usage-export.service
- oneshot 导出服务。
/etc/systemd/system/cli-proxy-usage-export.timer
- 每 5 分钟触发一次导出服务。
3. 部署配置(完整原文)
3.1 主服务 unit(服务启停钩子)
/etc/systemd/system/cli-proxy-api.service
[Unit]
Description=CLI Proxy API - OpenAI/Gemini/Claude Compatible Proxy Server
After=network-online.target
Wants=network-online.target
[Service]
Environment="ALL_PROXY=http://192.168.114.9:7890"
Environment="HTTP_PROXY=http://192.168.114.9:7890"
Environment="HTTPS_PROXY=http://192.168.114.9:7890"
Type=simple
ExecStart=/opt/cli-proxy-api/bin/cli-proxy-api -config /opt/cli-proxy-api/config/config.yaml
ExecStartPost=-/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh import
ExecStop=-/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh export
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/cli-proxy-api/config /opt/cli-proxy-api/export
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
3.2 启停同步脚本
/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh
#!/usr/bin/env bash
set -u
MODE="${1:-}"
BASE_URL="https://127.0.0.1:8317"
KEY_FILE="/opt/cli-proxy-api/config/.management-key"
BACKUP_DIR="/opt/cli-proxy-api/export"
BACKUP_FILE="${BACKUP_DIR}/usage-stats-backup.json"
TMP_FILE="${BACKUP_FILE}.tmp"
log() {
logger -t cli-proxy-usage-sync -- "$*"
echo "[cli-proxy-usage-sync] $*" >&2
}
read_key() {
if [! -r "$KEY_FILE"](/posts/!--r-"$key_file"/); then
log "key file not found: $KEY_FILE"
return 1
fi
KEY="$(tr -d '\r\n' < "$KEY_FILE")"
if [-z "$KEY"](/posts/-z-"$key"/); then
log "key is empty"
return 1
fi
return 0
}
do_export() {
mkdir -p "$BACKUP_DIR"
local code
code="$(curl -k -sS --connect-timeout 2 --max-time 5 \
-H "X-Management-Key: $KEY" \
-o "$TMP_FILE" -w "%{http_code}" \
"$BASE_URL/v0/management/usage/export" || echo 000)"
if ["$code" == "200"](/posts/"$code"-==-"200"/); then
mv -f "$TMP_FILE" "$BACKUP_FILE"
chmod 600 "$BACKUP_FILE" || true
log "export ok -> $BACKUP_FILE"
else
rm -f "$TMP_FILE"
log "export failed, http=$code"
fi
}
do_import() {
if [! -s "$BACKUP_FILE"](/posts/!--s-"$backup_file"/); then
log "backup not found, skip import"
return 0
fi
local i code
for i in $(seq 1 20); do
code="$(curl -k -sS --connect-timeout 2 --max-time 5 \
-X POST \
-H "X-Management-Key: $KEY" \
-H "Content-Type: application/json" \
--data-binary "@$BACKUP_FILE" \
-o /dev/null -w "%{http_code}" \
"$BASE_URL/v0/management/usage/import" || echo 000)"
if ["$code" == "200"](/posts/"$code"-==-"200"/); then
log "import ok from $BACKUP_FILE"
return 0
fi
sleep 1
done
log "import failed, last_http=$code"
}
if ! read_key; then
exit 0
fi
case "$MODE" in
export) do_export ;;
import) do_import ;;
*) log "unknown mode: $MODE" ;;
esac
exit 0
3.2.1 export:从本地 management API 下载 usage 快照到文件
do_export() 的核心目标,是把当前内存中的 usage 统计,通过 HTTPS 接口导出成一个本地 JSON 备份文件。
执行流程如下:
mkdir -p "$BACKUP_DIR"- 先确保备份目录存在,避免
curl -o写文件时报错。
- 先确保备份目录存在,避免
调用
curl请求导出接口:
code="$(curl -k -sS --connect-timeout 2 --max-time 5 \
-H "X-Management-Key: $KEY" \
-o "$TMP_FILE" -w "%{http_code}" \
"$BASE_URL/v0/management/usage/export" || echo 000)"
这里的含义可以拆开看:
请求地址:
$BASE_URL/v0/management/usage/export- 也就是
https://127.0.0.1:8317/v0/management/usage/export。 - 这是本机上的 management API,不是从公网下载文件,而是从本地服务导出 usage 数据。
- 也就是
-k- 允许
curl在 TLS 场景下跳过服务端证书校验。 - 当前脚本用于本机回环地址调用自签证书接口,因此使用
-k保证导入导出流程稳定执行。
- 允许
没有显式写
-X GET- 因为
curl在没有上传数据时默认就是GET请求。 - 所以这里本质上是:向 export 接口发起 GET,请服务返回 usage JSON。
- 因为
-o "$TMP_FILE"- 把服务返回的响应体直接写到临时文件。
- 也就是说,导出的 JSON 内容不会打印到终端,而是先落盘到
${BACKUP_FILE}.tmp。
-w "%{http_code}"- 让
curl在请求结束后,额外把 HTTP 状态码输出到标准输出。 - 这样 shell 可以把它捕获到变量
code中,例如200、401、500。
- 让
|| echo 000- 如果
curl本身执行失败(例如连接不上、超时、被拒绝),就不会拿到正常 HTTP 状态码。 - 这时补一个
000,用于表示“不是 HTTP 业务失败,而是传输层/连接层失败”。
- 如果
- 根据返回码判断是否覆盖正式备份:
code == 200- 说明导出成功。
- 执行
mv -f "$TMP_FILE" "$BACKUP_FILE",把临时文件原子替换成正式备份文件。 - 这样做比直接写正式文件更稳妥,能避免导出过程中断时留下半截文件。
其他状态码
- 说明导出失败。
- 执行
rm -f "$TMP_FILE"删除临时文件,避免误把失败响应当成备份。
chmod 600 "$BACKUP_FILE"- 把备份文件权限收紧为仅所有者可读写。
- 因为 usage 数据通常属于管理面信息,建议不要对其他用户开放。
3.2.2 import:把本地 JSON 备份文件上传回服务
do_import() 的核心目标,是在服务启动后,把上一次保存的 usage 统计重新导入到内存中。
执行流程如下:
- 先检查备份文件是否存在且非空:
if [! -s "$BACKUP_FILE"](/posts/!--s-"$backup_file"/); then
log "backup not found, skip import"
return 0
fi
-s表示文件存在且大小大于 0。- 如果没有可用备份,就直接跳过导入,避免空请求。
- 启动重试循环:
for i in $(seq 1 20); do
...
sleep 1
done
- 这里最多重试 20 次,每次间隔 1 秒。
- 这样设计的原因是:
ExecStartPost触发时,主进程虽然已经启动,但 management API 端口未必立刻可用。 - 因此需要给服务一点“预热时间”。
- 调用
curl请求导入接口:
code="$(curl -k -sS --connect-timeout 2 --max-time 5 \
-X POST \
-H "X-Management-Key: $KEY" \
-H "Content-Type: application/json" \
--data-binary "@$BACKUP_FILE" \
-o /dev/null -w "%{http_code}" \
"$BASE_URL/v0/management/usage/import" || echo 000)"
这个请求与 export 的区别在于:
export是“从接口取数据到文件”。import是“把文件内容作为请求体发回接口”。
具体含义如下:
-X POST- 明确指定使用
POST。 - 因为这里是把数据提交给服务,语义上属于导入/写入操作。
- 明确指定使用
-H "Content-Type: application/json"- 告诉服务端:这次请求体里发送的是 JSON 数据。
- 服务端通常会按这个类型解析导入内容。
--data-binary "@$BACKUP_FILE"- 把本地文件内容原样读取出来,作为 HTTP 请求体发送。
@文件名是curl的文件上传语法。- 这里使用
--data-binary而不是普通-d,是为了尽量按原始字节发送文件内容,避免对换行等内容做不必要处理,更适合发 JSON 备份文件。
-o /dev/null- 丢弃响应体。
- 因为这里真正关心的是“导入是否成功”,而不是接口返回正文内容。
-w "%{http_code}"- 同样把状态码单独输出出来,供脚本判断是否成功。
- 成功后立即结束,失败则继续重试:
如果返回
200- 说明服务已成功导入备份数据。
- 记录日志后直接
return 0。
如果不是
200- 继续等待 1 秒,再重试下一轮。
20 次后仍失败
- 记录
import failed, last_http=$code。 - 但脚本本身仍然
exit 0,避免因为 usage 导入失败而阻塞主服务启动。
- 记录
3.2.3 本脚本里用到的 curl 参数说明
| 参数 | 出现位置 | 作用 | 这里为什么要用 |
|---|---|---|---|
-k | export / import | 跳过 TLS 证书校验 | 本机访问 https://127.0.0.1:8317 且证书为自签场景,避免证书校验失败导致同步中断 |
-sS | export / import | -s 开启 silent 模式,不显示进度条;-S 在 silent 模式下若出错仍显示错误信息 | 适合 systemd/脚本场景,日志更干净,但异常时仍能看到错误 |
--connect-timeout 2 | export / import | 连接建立超时时间设为 2 秒 | 避免本地接口异常时长时间卡住 |
--max-time 5 | export / import | 整个请求最多执行 5 秒 | 本机回环调用无需长等待,快速失败可以缩短 systemd 钩子阻塞时间 |
-H "X-Management-Key: $KEY" | export / import | 添加自定义 HTTP 请求头 | 给 management API 传递鉴权密钥 |
-o "$TMP_FILE" | export | 把响应体写入文件 | 将导出的 usage JSON 直接保存到临时文件 |
-o /dev/null | import | 丢弃响应体 | 导入时只关心返回码,不关心正文 |
-w "%{http_code}" | export / import | 在请求完成后输出 HTTP 状态码 | 便于 shell 脚本做成功/失败判断 |
-X POST | import | 指定请求方法为 POST | 导入属于提交数据,不再使用默认 GET |
-H "Content-Type: application/json" | import | 声明请求体类型是 JSON | 帮助服务端按 JSON 解析导入文件 |
--data-binary "@$BACKUP_FILE" | import | 把文件内容作为请求体原样发送 | 适合把 JSON 备份文件完整上传到 import 接口 |
3.2.4 手工调试时可直接使用的 curl 示例
如果你想绕过脚本、直接在命令行验证接口,可以使用下面两条命令。
- 手工导出 usage 到文件:
curl -k -sS \
-H "X-Management-Key: <MANAGEMENT_KEY>" \
-o usage-stats-backup.json \
https://127.0.0.1:8317/v0/management/usage/export
- 手工把备份文件导回服务:
curl -k -sS -X POST \
-H "X-Management-Key: <MANAGEMENT_KEY>" \
-H "Content-Type: application/json" \
--data-binary "@usage-stats-backup.json" \
https://127.0.0.1:8317/v0/management/usage/import
> 上面两条命令更适合人工排障或联调;而脚本版本额外加入了超时、临时文件、状态码判断、失败清理和重试逻辑,更适合 systemd 自动化场景。
3.3 周期导出 service
/etc/systemd/system/cli-proxy-usage-export.service
[Unit]
Description=CLIProxyAPI usage statistics periodic export
After=network-online.target cli-proxy-api.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh export
3.4 周期导出 timer
/etc/systemd/system/cli-proxy-usage-export.timer
[Unit]
Description=Run CLIProxyAPI usage export every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true
Unit=cli-proxy-usage-export.service
[Install]
WantedBy=timers.target
4. 从零部署命令(可复用)
请替换
<MANAGEMENT_KEY>为你的 management 明文密钥
# 1) 写主服务 unit(配置启停钩子)
cat >/etc/systemd/system/cli-proxy-api.service <<'UNIT'
[Unit]
Description=CLI Proxy API - OpenAI/Gemini/Claude Compatible Proxy Server
After=network-online.target
Wants=network-online.target
[Service]
Environment="ALL_PROXY=http://192.168.114.9:7890"
Environment="HTTP_PROXY=http://192.168.114.9:7890"
Environment="HTTPS_PROXY=http://192.168.114.9:7890"
Type=simple
ExecStart=/opt/cli-proxy-api/bin/cli-proxy-api -config /opt/cli-proxy-api/config/config.yaml
ExecStartPost=-/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh import
ExecStop=-/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh export
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/cli-proxy-api/config /opt/cli-proxy-api/export
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
UNIT
# 2) 写脚本
install -m 755 /tmp/cli-proxy-usage-sync.sh /opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh
# 3) 写 timer 单元
install -m 644 /tmp/cli-proxy-usage-export.service /etc/systemd/system/cli-proxy-usage-export.service
install -m 644 /tmp/cli-proxy-usage-export.timer /etc/systemd/system/cli-proxy-usage-export.timer
# 4) 保存 management key(脚本读取)
printf '%s' '<MANAGEMENT_KEY>' >/opt/cli-proxy-api/config/.management-key
chmod 600 /opt/cli-proxy-api/config/.management-key
chown root:root /opt/cli-proxy-api/config/.management-key
# 5) 如果曾用 drop-in,删除旧配置
rm -f /etc/systemd/system/cli-proxy-api.service.d/usage-sync.conf
# 6) 生效并启用
systemctl daemon-reload
systemctl enable --now cli-proxy-usage-export.timer
systemctl restart cli-proxy-api.service
5. 验证命令
# 主服务是否加载启停钩子配置
systemctl status cli-proxy-api.service --no-pager -l
cat /etc/systemd/system/cli-proxy-api.service
# timer 是否生效
systemctl status cli-proxy-usage-export.timer --no-pager -l
systemctl list-timers cli-proxy-usage-export.timer --no-pager
# 手动触发一次导出并查看日志
systemctl start cli-proxy-usage-export.service
journalctl -t cli-proxy-usage-sync -n 50 --no-pager
# 检查备份文件
ls -lh /opt/cli-proxy-api/export/usage-stats-backup.json
6. 回滚方案
# 1) 关闭周期导出
systemctl disable --now cli-proxy-usage-export.timer
rm -f /etc/systemd/system/cli-proxy-usage-export.timer
rm -f /etc/systemd/system/cli-proxy-usage-export.service
# 2) 从服务配置移除启停钩子(恢复不含启停钩子的版本)
# 删除这两行:
# ExecStartPost=-/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh import
# ExecStop=-/opt/cli-proxy-api/bin/cli-proxy-usage-sync.sh export
# 3) 生效
systemctl daemon-reload
systemctl restart cli-proxy-api.service
7. 注意事项
ExecStop仅覆盖正常 stop/restart,不覆盖断电或kill -9。- 周期导出越频繁,损失窗口越小,但管理 API 调用更频繁。
.management-key建议严格600权限。- 如果端口或路径变更,需同步更新脚本中的
BASE_URL、KEY_FILE、BACKUP_FILE。
8. 总结
本方案通过“服务启停钩子 + timer 周期导出”实现 usage 统计可恢复:
- 启动自动恢复历史统计。
- 停止自动导出最新统计。
- 运行期间持续快照,减少异常中断数据损失。
适合长期运行、频繁重启、需要保留 usage 趋势数据的 CLIProxyAPI 部署。