Files
OJ2/docker/compose.school.yml
yuetsh fb22b7d49f docs(阶段5): 用生产快照跑完整切换演练
演练用 compose.debian.yml **本身**在本机 Docker 里跑,不是简化版。
数据是 2026-08-07 的 pg_dumpall 快照:1710 用户 / 956 题 / 123140 提交。

## 出口标准达成

停旧栈 11s,起新栈 34s(镜像预先构建好),全链路验证约 2 分钟 ——
**停机不到 1 分钟**,远在 30 分钟内。真正的时间风险在构建镜像(首次约 5 分钟),
所以手册里第一条就是「镜像必须在停机窗口之前构建好」。

验证到位的:首页、站点配置、题目列表、标签、公告、登录(argon2 新哈希和
Django pbkdf2 旧哈希都支持)、个人页、排行榜、后台四个接口、判题机自动注册,
以及**完整判题**(提交 Python A+B → AC,1.2 秒,两个测试点全过)。

## 两件原以为要做、实测不用做的事

- **不需要任何 DDL**:生产 dump 和新后端在用的库逐列对比,两边都是 278 列,
  零差异。新后端直接跑在现有结构上。
- **不需要重置序列**:我在 phase3-coverage.md 里记的那条「切换必做:重置序列」
  **是错的**,来自我手工按显式 id 导入、又没补 setval 的本地库。真实的
  pg_dumpall 带 30 条 setval,且把快照里所有序列和 max(id) 逐个对过,错位 0 个。
  已在原文档上标注更正,没有删掉原文 —— 错误结论本身也是信息。

## 回滚保证已实测

新栈跑完登录、提交、判题之后,再和生产 dump 比一次结构:逐列一致,零差异。
加上数据目录布局照抄旧后端,回滚 = 停新栈 + 起旧栈,约 20 秒,不动任何数据。
(未实测的部分也写明了:本机没构建旧 Django 镜像,「起旧栈」这一步没跑过。)

## 演练抓到的真问题

**pg_dumpall 备份会覆盖数据库口令。** 恢复完快照,新后端立刻报
`password authentication failed` —— 因为 dump 里带
`ALTER ROLE onlinejudge ... PASSWORD 'md5…'`,把角色口令覆盖成了备份时生产的那个。
正常切换不受影响(根本不恢复备份),但灾难恢复时这一条不写下来,
现场会被一个看起来毫不相干的报错卡住。

**恢复备份前必须先停应用**,否则 dump 里的 DROP DATABASE 失败。演练时因为
目标库是空的,数据照样进去了 —— 那是运气,目标库有数据就是满屏主键冲突。

## 镜像体积没达标,写明了原因

api 镜像 487MB,设计文档写的是「数十 MB」。一半以上(269MB)是 clang-format
拖进来的 LLVM,光 libLLVM.so 就 124MB。旧 Python 镜像同样装了 clang-format,
所以新镜像仍明显更小,但当初估「数十 MB」时没把它算进去。
瘦身路径也记了(换静态 clang-format 可砍 265MB),暂不做。

## 清理

演练在 data/postgres 留下了一份完整的生产数据副本,含 1710 名学生的
raw_password 明文列,已删除。手册里留了提醒 —— 那不是测试数据。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:13:59 -06:00

112 lines
3.3 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 机房。**这里没有数据库** —— 连的是服务器那台的 5445见 compose.debian.yml
#
# docker compose -f docker/compose.school.yml --env-file docker/.env.school up -d --build
#
# 两个站点共用一个库,但各有各的 Redis、判题沙箱和后端。这意味着
#
# - 判题队列是每站独立的BullMQ 在本地 Redis 上),学生在哪边提交就在哪边判,
# 和旧后端的 Dramatiq 拓扑一致;
# - WebSocket 推送也走本地 Redis pub/sub所以只推得到连在**本站**的学生。
# 旧后端的 Channels 也是这样,不是回归;
# - **切换那天两边都要切。** 只切一边的话,另一边的旧后端仍在读写同一个库,
# 而库结构已经按新后端迁过了。
services:
oj-redis:
image: redis:7-alpine
container_name: oj-redis
restart: always
volumes:
- ../data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
oj-judge:
image: registry.cn-hongkong.aliyuncs.com/oj-image/judge:1.6.1
container_name: oj-judge
restart: always
read_only: true
cap_drop:
- SETPCAP
- MKNOD
- NET_BIND_SERVICE
- SYS_CHROOT
- SETFCAP
- FSETID
tmpfs:
- /tmp
volumes:
- ../data/backend/test_case:/test_case:ro
- ../data/judge_server/log:/log
- ../data/judge_server/run:/judger
environment:
SERVICE_URL: http://oj-judge:8080
BACKEND_URL: http://oj-api:3000/api/judge-server/heartbeat
TOKEN: ${OJ2_JUDGE_TOKEN:?请在 env 文件里设置 OJ2_JUDGE_TOKEN}
# 机房机器内存宽裕,判题给足
mem_limit: 2g
oj-api:
build: &build
context: ..
dockerfile: docker/Dockerfile
target: api
image: oj2-api:latest
container_name: oj-api
restart: always
depends_on:
oj-redis:
condition: service_healthy
volumes:
- ../data/backend:/data
environment: &api-env
# 库在服务器上走公网。DB_HOST 默认值就是服务器地址,换机器改 env 文件
DATABASE_URL: postgres://onlinejudge:${POSTGRES_PASSWORD:?}@${DB_HOST:-150.158.29.156}:${DB_PORT:-5445}/onlinejudge
REDIS_URL: redis://oj-redis:6379
JUDGE_SERVER_URL: http://oj-judge:8080
JUDGE_SERVER_TOKEN: ${OJ2_JUDGE_TOKEN:?}
JUDGE_CONCURRENCY: ${JUDGE_CONCURRENCY:-4}
AI_KEY: ${AI_KEY:-}
# 机房走 http 直连 IP没有 TLS。带 Secure 的 Cookie 浏览器不会回传,
# 学生会「登录成功但立刻又是未登录」。这里必须是 false。
COOKIE_SECURE: ${COOKIE_SECURE:-false}
healthcheck:
test: ["CMD", "oj2-api", "healthcheck"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
mem_limit: 4g
oj-worker:
build: *build
image: oj2-api:latest
container_name: oj-worker
restart: always
depends_on:
- oj-api
volumes:
- ../data/backend:/data
environment: *api-env
command: ["oj2-api", "worker"]
mem_limit: 2g
oj-web:
build:
context: ..
dockerfile: docker/Dockerfile
target: web
image: oj2-web:latest
container_name: oj-web
restart: always
depends_on:
- oj-api
volumes:
- ../data/backend/log:/data/log
ports:
- "81:8000"
mem_limit: 256m