ops-manual.md 12 KB

支付平台运维手册

服务器信息

  • 公网 IP: 47.94.88.244(qcsj88888.com / api.qcsj88888.com / doc.qcsj88888.com 三条 A 记录均指向此)
  • hostname: iZ2zed18i1o6sivyqop2plZ
  • 内网 IP: 172.20.205.37(原手册所记,本次未复核)
  • 域名: qcsj88888.com、api.qcsj88888.com、doc.qcsj88888.com
  • 系统: Ubuntu (Alibaba Cloud ECS)

这台机器在别处没有任何记录。排查时容易和 antom(8.138.203.55)、国内 credit-pay (47.107.96.60)、美西(132.148.75.28)三台混淆——它是独立的第四台。

容器拓扑

容器 镜像(2026-09-24 实测) 端口映射 网络(未复核)
java-backend pay:latest ⚠️ 0.0.0.0:8001→8001 app-tier, deploy_app-tier
frontend xjz/frontend:2.5.4 0.0.0.0:5173→80 app-tier
payment-doc xjz/payment-doc-server:1.0.0 0.0.0.0:7878→7878 —
postgres postgres:18.3 0.0.0.0:5432→5432 ⚠️ 宿主机, deploy_app-tier
redis redis:8.6.2-alpine 0.0.0.0:6379→6379 ⚠️ deploy_app-tier
nginx-proxy-manager jc21/nginx-proxy-manager:latest 0.0.0.0:80-81, 443 deploy_app-tier
gogs gogs/gogs:latest 0.0.0.0:3000, 2222 deploy_app-tier

⚠️ 两处与旧版手册不符,上服务器前先核实:

  1. java-backend 实际跑的是 pay:latest,不是手册和仓库 compose 里写的 xjz/java-backend:1.0.0。照「升级部署」一节构建 xjz/java-backend:1.0.0 再 docker compose up -d java-backend,很可能不会替换掉正在运行的容器。服务器上的 compose 与仓库里的 docker-compose.yml 已经不同步,部署前先比对。
  2. postgres / redis 绑的是 0.0.0.0,不是 127.0.0.1。 目前靠阿里云安全组挡着 (2026-09-24 从公网实测 5432 / 6379 均不可达),但容器层面是无保护的, 安全组一旦放宽就直接暴露。建议改成 127.0.0.1:5432:5432 这类回环绑定。
qcsj88888.com (HTTPS)
    ↓ NPM
    ├── /api/v1/* → java-backend:8001
    └── 其余 → frontend:5173

项目路径

  • 代码: /root/payments/code/payment-platform
  • Compose: /root/payments/code/payment-platform/docker-compose.yml
  • NPM 数据: /root/payments/deploy/nginx-proxy-manager/data

常用操作

查看容器状态

cd /root/payments/code/payment-platform
docker compose ps
docker stats --no-stream

查看日志

docker compose logs java-backend --tail 50       # 最近50行
docker compose logs java-backend -f               # 实时跟踪
docker compose logs java-backend --since 5m       # 最近5分钟

重启后端

docker compose restart java-backend

停止/启动

docker compose stop java-backend
docker compose start java-backend

健康检查

curl http://localhost:8001/api/v1/payment/notify/health
# 预期返回: {"code":0,"data":{"status":"ok"}}

升级部署

# === 本地 ===
cd D:\project2\payment-platform\java
mvn clean package -DskipTests
scp target/payment-platform-1.0.0.jar root@172.20.205.37:/root/payments/code/payment-platform/java/target/

# === 服务器 ===
cd /root/payments/code/payment-platform/java
docker build -t xjz/java-backend:1.0.0 .
cd ..
docker compose up -d java-backend
docker compose logs java-backend --tail 20 -f

数据库迁移

新增 SQL 脚本放在 java/sql/,服务器执行:

docker exec -i postgres psql -U admin -d payment_platform_prod < /root/payments/code/payment-platform/java/sql/xxx.sql

回滚到 Python

docker stop java-backend
docker rm java-backend
cd /root/payments/code/payment-platform
git checkout master
docker compose up -d backend

配置说明

JVM 参数

JAVA_OPTS: "-Xms128m -Xmx256m -XX:+UseSerialGC -XX:MaxRAMPercentage=50 -XX:TieredStopAtLevel=1"
  • -Xms128m: 初始堆 128MB
  • -Xmx256m: 最大堆 256MB
  • -XX:+UseSerialGC: 单线程 GC(2C 服务器避免并发 GC 开销)
  • -XX:MaxRAMPercentage=50: JVM 最多用 50% 物理内存
  • -XX:TieredStopAtLevel=1: 减少 JIT 编译层级,启动更快

环境变量(数据库/Redis)

SPRING_DATASOURCE_DRUID_URL: jdbc:postgresql://postgres:5432/payment_platform_prod?currentSchema=public
SPRING_DATASOURCE_DRUID_USERNAME: admin
SPRING_DATASOURCE_DRUID_PASSWORD: 'xjz#123321'
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_DATABASE: 0
SPRING_DATA_REDIS_PASSWORD: '123456#xjz'

端口暴露(2026-09-24 公网实测)

从公网直连 47.94.88.244 的探测结果:

端口 服务 公网 说明
80 / 443 NPM ✅ 开放 对外服务,正常
81 NPM 管理面板 ❌ 不可达 安全组已挡,保持
3000 gogs ⚠️ 开放 见下
2222 gogs SSH ❌ 不可达
5432 postgres ❌ 不可达 靠安全组挡,容器绑的是 0.0.0.0
6379 redis ❌ 不可达 同上
8001 java-backend ❌ 不可达 经 NPM 转发
7878 payment-doc ❌ 不可达 经 NPM 转发

gogs 的 3000 是本机除 80/443 外唯一对公网开放的服务。 若是有意为之(外部拉取代码) 则无需处理;若是无意的,建议安全组关掉,改由 NPM 反代到域名下。


SSL 证书

本节 2026-09-24 新增。当天三个域名 HTTPS 中断三天,根因与处置见下。

机制

TLS 由 nginx-proxy-manager(NPM,容器名 nginx-proxy-manager)终结。宿主机上 只有它占 80/443。

域名 容器内代理配置 证书目录 证书名
qcsj88888.com /data/nginx/proxy_host/2.conf /etc/letsencrypt/live/npm-2/ npm-2
api.qcsj88888.com /data/nginx/proxy_host/3.conf /etc/letsencrypt/live/npm-3/ npm-3
doc.qcsj88888.com /data/nginx/proxy_host/4.conf /etc/letsencrypt/live/npm-4/ npm-4

三个域名各自一张独立证书(NPM 一个 Proxy Host 一张,SAN 不合并)。 live/ 下是符号链接,指向 archive/npm-<id>/ 里的实际文件。

宿主机数据目录:/root/payments/deploy/nginx-proxy-manager/data/ (证书不在这里面,在容器内 /etc/letsencrypt)。

校验方式:DNS-01(NPM 原生功能,非定制改造)

证书走 DNS challenge + dns-01,认证器是 certbot 的阿里云插件:

Requested authenticator dns-aliyun
Var pref_challs=['dns-01']
Var dns_aliyun_credentials=/etc/letsencrypt/credentials/credentials-*

这是 NPM 自带的功能,不要误判成定制改造。 可选 provider 列在 /app/certbot/dns-plugins.json(上游路径 backend/certbot/dns-plugins.json), provider 的显示名从该文件读取,所以日志里会出现 via Aliyun 这种拼接文案—— 这是正常的,不是补丁痕迹。

镜像来源确认(2026-09-24):

Created:     2026-02-17T05:43:40Z
RepoDigests: jc21/nginx-proxy-manager@sha256:2aa69b382a384b676c0d4f1d6f2eac40ecd478fcf7af1cfb3f9f1d3cd0c81e12

所以这台能直连 Let's Encrypt。 不要把 antom 那台"必须用 ZeroSSL(国际出口不通)" 的结论套到这台上——两台出网情况不同。

续期

NPM 每小时 :27:42 跑一次检查,把所有已进入「到期前 30 天」窗口的证书串行续掉。 三张分散在几个整点完成,是因为各自 expires_on 差了几小时、跨过窗口的时刻不同, 不是「每次只续一张」。

⚠️ 续期成功后 NPM 不会 reload nginx,根因见下。

docker logs nginx-proxy-manager 2>&1 | grep -i 'SSL'

验证:看实际发出的证书,不要只看文件

# 服务器本机(127.0.0.1 + SNI,绕开 DNS 缓存)
echo | openssl s_client -connect 127.0.0.1:443 -servername qcsj88888.com 2>/dev/null | openssl x509 -noout -dates
# 外网视角(在任一有国际出口的机器上跑)
curl -sS -o /dev/null -w 'verify=%{ssl_verify_result}\n' https://qcsj88888.com/

verify=0 表示客户端校验通过。

为什么必须这样验: 2026-09-24 那次,磁盘上证书完全正常(certbot certificates 报 VALID: 59 days),但 nginx 一直发 6/25 签发的旧证书。只扫证书文件的到期检查 会报"一切正常",而生产已经挂了。

装载故障的处置

症状:磁盘证书是新的,公网发出的却是旧的。此时不要重签(浪费 CA 配额且治不好)。

docker exec nginx-proxy-manager nginx -t

必须先输出 syntax is ok + test is successful 才能继续。证书与私钥不匹配时 nginx -t 会拦下——绝不让 nginx 加载坏配置(9-13 antom 那次就是栽在 "新证书配旧私钥"上,reload 后 nginx 起不来、域名全挂)。

docker exec nginx-proxy-manager nginx -s reload

根因(2026-09-24 查清):续期路径不 reload nginx

NPM 只在「签发新证书」时 reload nginx,定时续期这条路线上根本没有这一步。

全 /app 范围内 Nginx.reload() 的调用点(grep -rn "Nginx.reload()" /app/):

proxy-host.js:296,397          redirection-host.js:294,395
access-list.js:201,305         stream.js:243,344
dead-host.js:242,324           ip_ranges.js:113
setting.js:51,63               certificate.js:155,158,164,171,177,184
nginx.js:94(定义处)           migrations/2021...regenerate_default_host.js:22

全是 CRUD 流程。certificate.js 那 6 处在 insertAndFetch(data) 之后,属于 POST /certificates(签发),服务于「临时禁用 host → 签发 → 恢复 host」。

续期链路一次都没有:

processExpiringHosts  (:49)   定时任务,查 expires_on 进入 30 天窗口的证书
  → renew             (:893)  串行 forEach 调用
     → renewLetsEncryptSsl              (:929)  http-01
     → renewLetsEncryptSslWithDnsChallenge (:970)  dns-01
        → utils.execFile(certbotCommand, args, adds.opts)
        → logger.info(result) → return result

两个 renew 函数体里只有 execFile 和 logger.info,不 import nginx 模块,也没有 reload。

代码与现象互相印证: 8/25 续期日志显示 certbot 成功、数据库写入,全程无 reload 记录、 无报错 —— 不是「reload 步骤丢了」,是这条路径上本来就没有。

为什么只有这台暴露: reload 只在改配置时发生(动 NPM 界面会顺带把新证书带进来)。 这台三个月没人碰过 NPM 配置,database.sqlite 上一次写入就是 8/25 的续期本身, 所以 nginx 从 6/25 签发后再没重载过。

未核实上游是否已在更新版本中修掉。可确认的事实只有:这个 digest (sha256:2aa69b38…)的镜像,续期路径不 reload。

复发时间线与处置建议

NPM 在到期前 30 天续期,即约 10/23 再次静默续期。届时若不干预,nginx 仍发 8/24 那张, 11/22 三个域名再次过期。

建议处置(尚未实施):宿主机加定时自愈 —— 比对「实际发出的证书」与「磁盘当前证书」, 不一致就 nginx -t && nginx -s reload,并推钉钉告警。

不要改镜像。 /app/internal/certificate.js 是控制面代码,改错会让 nginx-proxy-manager 起不来,连带 80/443 全挂 —— 而 80/443 正是用来签发和续期的入口。


故障排查

后端起不来

docker compose logs java-backend --tail 100

常见原因:

  • Connection refused: postgres:5432 → postgres 没加入 deploy_app-tier,执行 docker network connect deploy_app-tier postgres
  • OutOfMemoryError → 调大 -Xmx 到 384m
  • 端口占用 → lsof -i:8001,停掉占用进程

页面访问 502

检查后端是否正常:

curl http://localhost:8001/api/v1/payment/notify/health

后端正常则查 NPM 是否在线:docker ps | grep nginx-proxy-manager

磁盘空间

docker system prune -a --filter "until=72h"  # 清理72小时前的旧镜像

备份

# 数据库
docker exec postgres pg_dump -U admin payment_platform_prod > /root/backup/payment_$(date +%Y%m%d).sql