Files
live-hub-py/docs/迁移方案-live-hub-py独立部署.md
yml2213 98a9f0e0bc chore(merge): 账号事件迁移改号 0041,并去重排查报告
dev 与 main 从 294c6cd 分叉后,各自新增了一个 0040 迁移(都从 20261002_0039 起链):
- main:20261003_0040 douyu_tasks.account_assigned_to(生产已应用)
- dev: 20261002_0040 account_events(未上生产)

两者合并即两个 alembic head,upgrade head 会直接失败。因为账号事件迁移从未被生产库
应用过,改号是安全的:

1. account_events 迁移改名为 20261003_0041,down_revision 指向 20261003_0040;
2. tests/test_migrations.py 的 HEAD_REVISION 同步改 20261003_0041;
3. 迁移保持幂等,本机开发库(alembic_version 已是 20261003_0040、account_events 表已存在)
   实跑 upgrade 通过,仅推进版本号、数据未动;
4. 顺带修正规划/解耦设计里「下一个迁移编号」的过期取值(现为 20261003_0042)。

排查报告去重:两个会话各自写了一份《生产服务器高负载排查…》报告,内容重叠且互相矛盾
(一处写 8 核/16GB,另一处有 nproc 原始输出为 4 vCPU/7.1GiB)。合并为单一文档
docs/生产服务器高负载排查与优化报告.md,以带原始命令输出的事实为准,删除重复的那份,
并修正引用它的迁移方案文档。
2026-10-04 13:20:36 +08:00

19 KiB
Raw Permalink Blame History

live-hub-py 独立部署迁移方案(负载分流)

采集时间:2026-10-03 12:53 ~ 13:05 CST 源服务器:order+douyu_huya / 47.114.114.225(阿里云 cn-hangzhou,4 vCPU / 7.1 GiB / 49 GB 系统盘) 目标服务器:待购买(用户确认「还没有购买,有计划分开」) 迁移对象:live-hub-py(用户选定,理由见 §2) 本文所有负载数字均为本次实测,非估算;工时为含缓冲的区间估计。


0. 结论(TL;DR)

问题 结论
负载为什么忽高忽低 4 核机器上混跑 2 套业务 + 2 个数据库,实例级 CPU 在 46% ~ 380% 之间剧烈摆动(实测 24 个采样点),任一方突发就把 4 核打满,排队后 load average 长期挂在 10 左右
迁哪个 live-hub-py。它自洽(App + MySQL 一起走)、逻辑数据只有 1.1 GB、只牵动 live.khhao.com 一个域名;order_site 是四个域名的入口网关,搬迁代价高得多
目标机要什么规格 4 vCPU / 8 GB / 40~60 GB 系统盘 / 10 Mbps,必须买在阿里云 cn-hangzhou(否则 OSS 内网端点失效)
要做什么 11 项改动面,其中 3 项是真正的坑:.env 里的 APP_ENCRYPTION_KEY 必须原样照抄、MySQL 卷要改成新的本地卷、compose 里的 external: order-site-net 要摘掉
要多久 顺利半天(4~6 小时),其中停服窗口 20~40 分钟;含演练与回滚预案 1 个工作日
迁完能降到多少 主服峰值砍掉约 40%~60%,15 分钟负载从 10 降到 **46**;但 order_site 自己峰值还能到 3.2 核,SQL 重构/索引不做,下次还会打满
买机器之前 立刻能回收 约 8 GB 磁盘(binlog 3.6 G + 构建缓存 2 G + 无用镜像 2.2 G),磁盘水位从 84% 降到 ~68%

1. 现场实测:负载为什么忽高忽低

1.1 采样方法

在源服务器上每 5 秒采一次 loadavg + docker stats,连续 24 个点(12:54:04 ~ 12:57:04):

for i in $(seq 1 24); do
  echo "$(date +%T) load:$(cut -d' ' -f1-3 /proc/loadavg)"
  docker stats --no-stream --format "{{.Name}} {{.CPUPerc}}"
  sleep 5
done

1.2 实测曲线(节选,单位 %,4 核 = 400%)

时刻 load1 douyu-login mysql postgres backend web 合计/400% idle
12:55:22 7.31 0.41 4.54 8.45 29.65 2.58 46% 86.7%
12:54:44 10.04 4.28 1.16 107.52 20.52 4.82 138% 14.0%
12:54:20 11.24 27.38 84.40 138.22 54.83 4.92 310% 28.3%
12:54:12 11.09 38.97 66.31 222.80 51.11 12.98 392% 8.9%
12:55:38 7.03 0.77 57.64 222.32 94.40 4.66 380% 0.0%
12:56:18 7.58 79.33 138.41 127.95 55.01 6.05 407% 1.7%
12:56:49 8.92 124.48 45.71 42.25 31.83 9.81 254% 3.8%

怎么读这张表:合计在 46% → 407% 之间来回跳,idle 在 0% ~ 86.7% 之间跳。这不是「持续满载」,而是「反复打满—回落—再打满」,正是用户观感里的「忽高忽低」。

1.3 单实例摆动幅度(同一次采样内 min → max)

容器 归属 CPU 摆动 说明
order_site-postgres-1 order_site 2.43% → 222.80% 单实例最大摆动,parallel worker 打满约 2 核
live-hub-py-mysql-1 live-hub-py 1.16% → 138.41% 任务批次一到就跑满,批次结束立刻回落
live-hub-py-douyu-login-1 live-hub-py 0.41% → 124.48% 登录/充值批次并发,典型突发型
order_site-backend-1 order_site 13.89% → 145.92% 跟 Postgres 同涨同落

1.4 系统层证据

load average: 10.36, 9.38, 10.72      # 15 分钟负载稳定在 10.2~10.75
4 vCPU / Mem 7.1Gi used 4.1Gi / swap used 577Mi
%Cpu(s): ... 0.0 id ... 20.9 wa      # 采样中出现 20.9% 的 IO wait
/dev/vda3  49G  39G  7.7G  84% /     # 磁盘只剩 7.7G
live-hub-py-mysql-1   BLOCK I/O 396GB / 23.1GB   # 累计读 396GB

三条结论:

  1. 突发型负载来自 live-hub-py(登录批次 + MySQL 批次查询),这是「忽高忽低」的直接来源。
  2. 持续型高位来自 order_site(Postgres 常态 100%~220%,backend 30%~145%),它决定了「低」也低不到哪去。
  3. 两者峰值会叠加(12:56:18 合计 407%),互相抢核 → 延迟放大 → 队列堆积 → load 10。

1.5 历史结论的沿用与修正

docs/生产服务器高负载排查与优化报告.md 里「PostgreSQL JIT 导致 197% CPU」的判断成立,12:42 执行 jit = off 后 Postgres 从 197.75% 掉到 1.58%。但本次复测显示它又回到了 100%~222% —— 说明 jit = off 只是止血,getWorkerOrderOverview 的 CTE + 并行 worker 才是根,仍需按原报告做 SQL 重构(§9)。


2. 为什么先迁 live-hub-py(而不是 order_site)

维度 live-hub-py order_site
对外入口 仅 live.khhao.com 一个域名 四个域名的网关:ks.khhao.com、worker.khhao.com、live.khhao.com、mall.khhao.com 全在它的 Caddy(order_site-web-1)里终结 TLS
监听端口 容器内 8800,宿主 8000(新机空闲) 宿主 80 / 443,目标机若已有 Caddy 直接冲突
数据库 MySQL,逻辑数据 881 MB(douyu_tasks 一张表 759 MB) PostgreSQL,库 5236 MB(10 张大表,最大 admin_audit_logs 769 MB)
卷体积 卷名义 4.9 GB,其中 binlog 3.6 GB 不用搬 卷 5.3 GB,基本都要搬
跨项目依赖 接了 order_site_default 网络、读 order_site PG —— 但 ORDER_SITE_LOOKUP_ENABLED=false,当前是休眠的 反向无依赖(已确认 order_site 不访问 livehub 任何东西)
迁移改动面 11 项 20+ 项(还要拆网关、拆 DNS、合 Caddy)
工时 半天 1~1.5 天

一句话:live-hub-py 是一个能整体搬走的自治单元;order_site 是「搬它就得把整个入口层一起搬」的重量级单元。


3. 目标机规格建议

项 建议 依据
vCPU 4 核(下限 2 核) 实测 livehub 单体峰值:app 124% + mysql 138% ≈ 2.6 核同时突发;2 核会排队
内存 8 GB(下限 4 GB) 实测 app 611 MiB + mysql 897 MiB ≈ 1.5 GiB,加上 build cache 与页缓存,4 GB 会紧张
系统盘 40~60 GB ESSD 镜像 1.87 GB + MySQL 1.12 GB + DB 1.1 GB + 代码 1.2 GB + 首次构建缓存 5~8 GB
带宽 ≥ 5 Mbps,建议 10 Mbps 容器实测 NET I/O 2.95 GB 入 / 234 MB 出(约 2 小时),均值约 3.3 Mbps,突发更高
地域 阿里云 cn-hangzhou(关键) .env 里 OSS_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com —— 内网端点只在同地域可用。换地域或换云商必须改成公网 oss-cn-hangzhou.aliyuncs.com,并接受公网流量费与更高延迟
操作系统 Ubuntu 24.04 + Docker + compose plugin 与源机一致,源机当前 Docker Compose v5.x
安全组 22(限自己的 IP)、80/443(方案 A 自签 TLS 需要);8000 不要对公网开放 源机上 8000 是 0.0.0.0 裸暴露的,迁移时顺手收紧

同地域的额外好处:DB dump 可以经 OSS 内网中转(源机 put → 新机 get),不走公网、不计公网流量、速度是内网级;也可以直接走阿里云同地域公网,通常也比跨云快得多。


4. 迁移改动面清单(要做什么)

A. 代码与镜像

# 事项 说明
A1 从 gitea 拉代码 git clone https://gitea.hao858.com/yml/live-hub-py.git,切到生产提交 294c6cd(main,已确认与 origin/main 一致)
A2 构建镜像 docker compose build。多阶段构建:前端 npm build + yyb-worker node_modules + Python(uv) 运行时。首次全量构建 15~25 分钟
A3 镜像加速 compose 里已写死 docker.m.daocloud.io/library/mysql:8.4 等;构建参数 USE_CHINA_MIRRORS=true 已在 .env 中确认

B. 配置改写(3 个真坑)

# 事项 不做的后果
B1 .env 里 APP_ENCRYPTION_KEY、JWT_SECRET_KEY 必须一字不差照抄 换了 APP_ENCRYPTION_KEY → 账号密码、邮箱密码、Cookie、代理密钥等历史密文全部解不开,等于数据报废
B2 MYSQL_VOLUME_EXTERNAL 改为 false,MYSQL_VOLUME_NAME 用新名字(如 live-hub-py_mysql-data) 源机靠 external: true 复用旧卷 douyu_login_py_mysql-data。新机上若沿用 external,Docker 会自动建一个空卷 —— 2026-10-02 已经因此发生过一次「看起来数据全丢」
B3 docker-compose.yml 摘掉 networks: order-site-net(external: order_site_default)与服务的该网络 新机上不存在 order_site_default 网络,compose up 直接报错起不来
B4 ORDER_SITE_DATABASE_URL 处理 该功能当前是关的(ORDER_SITE_LOOKUP_ENABLED=false),建议保持关闭并在文档留痕。将来要开,需为跨机 PG 访问单独做 TLS 代理 + IP 白名单
B5 APP_PORT 新机上 8000 空闲,可沿用;若与既有服务冲突再改
B6 COOKIE_SECURE=true 保持 前置 TLS 必须继续有,否则 Cookie 下发异常

C. 数据搬运

# 事项 体量 做法
C1 MySQL 全量 逻辑 881 MB mysqldump --single-transaction --routines --triggers --events douyu_login,压缩后约 150~250 MB
C2 MySQL 增量(切换窗口) 停机期间写入量 停旧容器后重跑一次 dump,只这一次的差异
C3 虎牙设备指纹状态 data/huya_fp_states/ 2.4 MB + data/huya_device_profiles.json 308 KB 必须搬,这是虎牙设备指纹的连续性,丢了要重新养
C4 验证码截图 data/captcha_captures/ 208 MB 可搬可不搬(排查用样本);不搬能省 15 分钟
C5 旧 sqlite data/web.db 24 MB 不用搬(7 月 25 日之后未更新,历史遗留)
C6 日志 logs/ 590 MB 不用搬,留在旧机做追溯
C7 备份件 backups/login_tasks_20261001_182959.sql 32 MB 可选
C8 binlog 3.6 GB 不用搬(无从库,已确认 SHOW REPLICAS 为空)

D. 入口与 DNS

# 事项 说明
D1 方案 A(推荐):新机自建 Caddy,live.khhao.com 的 A 记录改指新机 新机 Caddy 自动签证书;需安全组开 80/443。目前 live.khhao.com → 47.114.114.225,证书由旧机 Caddy 签发(已确认 live.khhao.com 证书存在)。回滚只需把 DNS 改回去
D2 方案 B(备选):旧机 Caddy 保留 live.khhao.com,反代到新机 IP:8000 无 DNS 改动、无证书操作,新机安全组只放行旧机 IP。缺点:旧机仍是入口单点,且多一跳跨公网

E. 依赖外部服务(无需改动,但要确认可达)

服务 地址 说明
Roundcube 邮箱验证码 http://111.229.206.54:8000/(代码默认值,.env 未覆盖) 公网可达,与源机无关
OSS oss-cn-hangzhou-internal.aliyuncs.com / bucket live-hub-assets 依赖目标机在 cn-hangzhou
鱼翅直充供应商 FISH_FIN_RECHARGE_*(4 个密钥 + 回调 URL) 公网,回调地址若含旧机域名需一并改
应用宝 Worker 容器内回环 127.0.0.1:8810 同容器,自动跟随

F. 不需要迁移的东西(已确认)

  • 没有 crontab(crontab -l 为空),没有自定义 systemd timer(只有 sysstat/logrotate 等系统默认项)。
  • 定时任务全在应用内(AUTOMATION_ENABLED=true),随容器搬走;各作业开关默认关闭,在 /automation 页面单独控制。
  • yyb-worker 状态目录 data/yyb-worker-jobs/ 为空。

5. 分阶段执行计划与工时

阶段 内容 停服 工时
T0 准备(买机前就能做) 清理源机磁盘;确认代码已推 gitea;导出 .env;跑一次全量 mysqldump 并校验可导入 否 0.5 h
T1 新机初始化 安全组;装 Docker + compose;建目录;从 gitea clone;写 .env(B1);改 compose(B2/B3/B5) 否 0.5 h
T2 构建与预热 docker compose build(15~25 min);起 MySQL;导入 T0 的全量 dump;alembic 由 entrypoint 自动跑 否 0.5~1 h
T3 预跑验证 起 app,跑通 health + 管理员登录 + 只读页面;用小批量账号各跑 1 个斗鱼/虎牙任务验证换 IP 后的风控 否 0.5 h
T4 切换窗口 旧机 compose stop douyu-login(冻结写)→ 增量 dump → 导入 → 新机 compose up → 冒烟 → 改 DNS 是 20~40 min
T5 验证与收尾 WebSocket、OSS 上传、自动化作业、Cookie 检测/重登;旧机保留停止态容器与卷作为回滚 否 1 h
T6 观察期 观察 3~7 天:任务成功率、风控拦截、资源曲线;之后再删旧机数据 否 3~7 天旁观

合计:

  • 顺利路径:半天(4~6 小时),其中真正停服 20~40 分钟。
  • 稳妥路径:1 个工作日(多出一次演练 + 回滚验证)。
  • 若目标机选腾讯云:T2 数据中转改走公网(+20~40 min),且 OSS_ENDPOINT 必须改公网端点(改动面 +1,且有流量费)。

6. 停机窗口与回滚

切换窗口内的顺序(务必按序):

# 1) 冻结旧机写入
cd /root/live-hub-py && docker compose stop douyu-login

# 2) 旧机增量 dump(此时库是静止的)
docker exec live-hub-py-mysql-1 sh -c \
  'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers --events douyu_login' \
  | gzip > /tmp/livehub-final.sql.gz

# 3) 经 OSS 内网中转(同地域时免公网流量)
#    源机 put → 新机 get;或用 ossutil 直接 cp

# 4) 新机导入(新机 MySQL 必须已按 compose 的 --default-time-zone=+00:00 启动)
gunzip -c livehub-final.sql.gz | docker exec -i live-hub-py-mysql-1 \
  mysql -uroot -p"$MYSQL_ROOT_PASSWORD" douyu_login

# 5) 起服务 + 冒烟
docker compose up -d && curl -fsS http://127.0.0.1:8000/api/health

# 6) 改 DNS:live.khhao.com A 记录 → 新机 IP

回滚:live.khhao.com 的 A 记录改回 47.114.114.225,旧机 docker compose start douyu-login。旧机数据与镜像在观察期内一律不删。

时区坑(本次必须复核):全库时间列存 UTC 裸值,MySQL 靠 --default-time-zone=+00:00 对齐。导入前后不要手工改容器时区,也不要拿 SELECT NOW() 当北京时间比对(会被慢 8 小时误导 —— README 里已记录踩过这个坑)。mysqldump 默认 --tz-utc 在这一口径下是安全的。


7. 风险清单

风险 等级 缓解
换公网 IP 触发斗鱼/虎牙风控 高 T3 阶段用小批量账号先验证;项目本身有代理池与预检机制可兜底;异常时立刻回滚 DNS
APP_ENCRYPTION_KEY 抄错 高 迁移前用旧 .env 对一条已加密记录做解密自检,再切流量
MySQL 卷命名踩坑导致「假丢数据」 中 B2 强制改成新本地卷;导入后立即 SELECT COUNT(*) FROM douyu_tasks 比对 222679 行
OSS 内网端点在新地域不可达 中 机器买在 cn-hangzhou;若换地域,用 ossutil 做一次上传自检
跨项目订单查询功能失效 低 该功能当前是关闭的;保持关闭并在文档留痕
磁盘不足(构建缓存峰值) 低 T0 先回收 8 GB;构建完 docker builder prune
旧机 80/443 上 live.khhao.com 的 Caddy 块残留 低 观察期结束后再从旧 Caddyfile 里删掉该 site 块

8. 迁完的预期收益

立刻缓解:

  • 主服少掉 0.5 ~ 2.2 核的突发负载 + 约 1.5 GiB 内存。
  • 少掉 MySQL 的 396 GB 累计块读,IO wait(实测峰值 20.9%)显著下降。
  • 磁盘回收:MySQL 卷 4.9 GB 搬走后,源机 84% → 约 70%。
  • 波动源头被移走:主服 load 15 分钟均值预期从 10.5 降到 46。

仍然没解决的(必须说清):

  • order_site-postgres-1 单独峰值就能到 222.8%,加 backend 145.9% ≈ 3.2 核。迁走 livehub 后,order_site 独占 4 核仍然会在高峰期打满。
  • 所以迁 livehub 是必要条件、不是充分条件。后续二选一:
    • ① 按 docs/生产服务器高负载排查与优化报告.md 做 getWorkerOrderOverview 的 CTE 重构(UNION → UNION ALL)+ 打手端概览缓存,消除并行 worker 突刺;
    • ② 或把 order_site 留在 4 核机上但升配到 8 核。
  • 建议顺序:先迁 livehub(半天)→ 观察一周 → 再决定 order_site 是优化还是升配/搬迁。这样每次只动一个变量,出问题好定位。

9. 买机器之前就能做的立即降压(零成本)

以下都在源机上执行,不停服。合计可回收约 8 GB 磁盘,并把 MySQL 的慢查询日志 IO 降下来。

# 1) 先补一份全量备份再动 binlog(目前只有 10-01 的一份手工备份)
cd /root/live-hub-py && docker exec live-hub-py-mysql-1 sh -c \
  'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers douyu_login' \
  | gzip > /root/live-hub-py/backups/full-$(date +%Y%m%d).sql.gz

# 2) 清 binlog:已确认无从库(SHOW REPLICAS 为空),保留最近 3 天保 PITR 能力
docker exec live-hub-py-mysql-1 mysql -uroot -p"$MYSQL_ROOT_PASSWORD" \
  -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY);"
#    并把保留期从 30 天收紧到 7 天(改 compose 的 mysql command):
#    --binlog-expire-logs-seconds=604800
#    预期回收 ~3.6 GB

# 3) 清构建缓存(当前 6.29 GB,可回收 2.07 GB)
docker builder prune -f

# 4) 清已退出的旧容器与无用镜像(预期回收 ~2.2 GB)
docker rm douyu-login douyu-login-db
docker rmi douyu_login_py-douyu-login docker.m.daocloud.io/minio/minio pc-crmeb-pc

# 5) 慢查询日志目前 --long-query-time=1,阈值过低、写入太频繁
#    排查期可留,稳定后建议调到 3~5 秒或关闭

同时建议(不花钱):

  • /tmp/Caddyfile.fmt|orig|new 是旧编辑残留,确认无用后删掉。
  • 源机上 8000 端口 0.0.0.0 裸暴露且不经 Caddy(live.khhao.com 是走 Caddy 的),迁移时应顺带收紧为只监听内网。
  • 源机 /home/order_site_migrate(17 MB)看名字是上次迁移残留,确认后再删。