1. 数据库新增迁移 20261003_0040,在 douyu_tasks 表增加 account_assigned_to 字段,并创建 (account_assigned_to, handbook_scope, id) 与 (account_assigned_to, id) 复合覆盖索引; 2. 迁移脚本自动回填历史 24 万条任务的所属客服 ID; 3. 任务服务与账号路由联动:创建任务时自动继承 account_assigned_to,修改账号分配时同步更新关联任务归属; 4. 斗鱼任务查询路由 _visible_tasks_query 改为通过 account_assigned_to 复合索引过滤,消除与 accounts 表的跨表关联与临时表全量文件排序; 5. 新增 test_douyu_task_assigned_optimization 单元测试并通过整链迁移与回归测试。
19 KiB
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 分钟负载从 |
| 买机器之前 | 立刻能回收 约 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
三条结论:
- 突发型负载来自 live-hub-py(登录批次 + MySQL 批次查询),这是「忽高忽低」的直接来源。
- 持续型高位来自 order_site(Postgres 常态 100%~220%,backend 30%~145%),它决定了「低」也低不到哪去。
- 两者峰值会叠加(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)看名字是上次迁移残留,确认后再删。