diff --git a/.env.server.example b/.env.server.example index 1666e79b..f104c82f 100644 --- a/.env.server.example +++ b/.env.server.example @@ -33,6 +33,19 @@ DATABASE_STATEMENT_TIMEOUT_MS=15000 DATABASE_SLOW_QUERY_THRESHOLD_MS=500 # PostgreSQL 原生慢查询日志阈值(毫秒);-1 关闭 POSTGRES_LOG_MIN_DURATION_STATEMENT_MS=500 +# PostgreSQL 容器资源与性能参数(按服务器内存调整;默认值按 2G 内存机器设置, +# 4G+ 机器可将 shared_buffers 提到 512MB、effective_cache_size 提到 2GB): +POSTGRES_MEM_LIMIT=1g +POSTGRES_CPUS=2 +POSTGRES_SHARED_BUFFERS=256MB +POSTGRES_EFFECTIVE_CACHE_SIZE=1GB +POSTGRES_WORK_MEM=8MB +POSTGRES_MAINTENANCE_WORK_MEM=128MB +# 后端与 Web 容器限额(内存紧张的小机器可相应收紧) +BACKEND_MEM_LIMIT=512m +BACKEND_CPUS=1 +WEB_MEM_LIMIT=256m +WEB_CPUS=0.5 # Logging LOG_LEVEL=info diff --git a/deploy/ubuntu-deploy.sh b/deploy/ubuntu-deploy.sh index 24c44691..fa15c044 100755 --- a/deploy/ubuntu-deploy.sh +++ b/deploy/ubuntu-deploy.sh @@ -197,6 +197,32 @@ check_compose_config() { docker compose config >/dev/null || fail "Docker Compose 配置校验失败。" } +# 部署前磁盘空间预检:备份目录所在文件系统至少要有首份备份 + 现有备份量 2 倍的余量。 +# 不足时直接失败,避免备份或迁移过程中磁盘写满导致数据库不可用。 +ensure_disk_space() { + local backup_root="${PROJECT_ROOT}/backups" + local floor_kib=1048576 # 1GiB + local existing_kib=0 require_kib free_kib + + mkdir -p "${backup_root}" 2>/dev/null || true + + if [[ -d "${backup_root}" ]]; then + existing_kib="$(du -sk "${backup_root}" 2>/dev/null | awk '{print $1}' || true)" + existing_kib="${existing_kib:-0}" + fi + + require_kib=$((existing_kib * 2)) + [[ ${require_kib} -lt ${floor_kib} ]] && require_kib=${floor_kib} + + free_kib="$(df -Pk "${PROJECT_ROOT}" 2>/dev/null | awk 'NR==2 {print $4}' || true)" + free_kib="${free_kib:-0}" + + log "磁盘空间预检:可用 ${free_kib} KiB,备份目录已占用 ${existing_kib} KiB,要求至少 ${require_kib} KiB" + if [[ ${free_kib} -lt ${require_kib} ]]; then + fail "磁盘可用空间不足(可用 ${free_kib} KiB,需 ≥ ${require_kib} KiB)。请清理磁盘或调小 DB_BACKUP_KEEP 后重试。" + fi +} + pull_base_images() { log "拉取基础镜像" docker compose pull --ignore-buildable @@ -482,6 +508,7 @@ main() { validate_env_file confirm_reset_database ensure_data_dir + ensure_disk_space deploy_stack wait_for_health print_applied_migrations diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d72f5915..84c890cb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,8 +16,20 @@ services: "pg_stat_statements.track=all", "-c", "log_min_duration_statement=${POSTGRES_LOG_MIN_DURATION_STATEMENT_MS:-500}", + "-c", + "shared_buffers=${POSTGRES_SHARED_BUFFERS:-256MB}", + "-c", + "effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB}", + "-c", + "work_mem=${POSTGRES_WORK_MEM:-8MB}", + "-c", + "maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-128MB}", ] restart: unless-stopped + mem_limit: ${POSTGRES_MEM_LIMIT:-1g} + cpus: ${POSTGRES_CPUS:-2} + shm_size: 512mb + stop_grace_period: 60s logging: *json-log-rotation environment: TZ: ${TZ:-Asia/Shanghai} diff --git a/docker-compose.yml b/docker-compose.yml index 21d4f820..61e2fb1c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: postgres: image: docker.m.daocloud.io/library/postgres:16-bookworm # 开启 pg_stat_statements,便于定位生产环境累计最耗时的 SQL。 + # 内存类参数按 2G 内存机器给出默认值,请按服务器内存调整(见 .env.server.example)。 command: [ "postgres", @@ -17,8 +18,21 @@ services: "pg_stat_statements.track=all", "-c", "log_min_duration_statement=${POSTGRES_LOG_MIN_DURATION_STATEMENT_MS:-500}", + "-c", + "shared_buffers=${POSTGRES_SHARED_BUFFERS:-256MB}", + "-c", + "effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB}", + "-c", + "work_mem=${POSTGRES_WORK_MEM:-8MB}", + "-c", + "maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-128MB}", ] restart: unless-stopped + # 让 PostgreSQL 优雅关闭有足够时间完成检查点;无限制时 OOM 可能拖垮整机。 + mem_limit: ${POSTGRES_MEM_LIMIT:-1g} + cpus: ${POSTGRES_CPUS:-2} + shm_size: 512mb + stop_grace_period: 60s logging: *json-log-rotation environment: TZ: ${TZ:-Asia/Shanghai} @@ -46,6 +60,10 @@ services: DEBIAN_MIRROR: ${DEBIAN_MIRROR:-mirrors.aliyun.com} NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmmirror.com} restart: unless-stopped + # 30s 让后端在 SIGTERM 后完成优雅关闭(HTTP 连接收尾 + 数据库连接池释放)。 + mem_limit: ${BACKEND_MEM_LIMIT:-512m} + cpus: ${BACKEND_CPUS:-1} + stop_grace_period: 30s env_file: - .env depends_on: @@ -101,6 +119,9 @@ services: ALPINE_MIRROR: ${ALPINE_MIRROR:-mirrors.aliyun.com} NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmmirror.com} restart: unless-stopped + mem_limit: ${WEB_MEM_LIMIT:-256m} + cpus: ${WEB_CPUS:-0.5} + stop_grace_period: 20s logging: *json-log-rotation depends_on: backend: diff --git a/docs/数据库SQL约束与运维.md b/docs/数据库SQL约束与运维.md index eef94d9f..b62d1f8c 100644 --- a/docs/数据库SQL约束与运维.md +++ b/docs/数据库SQL约束与运维.md @@ -38,3 +38,20 @@ ORDER BY query_start; - `DATABASE_MAX_CONNECTIONS` 按后端实例数总量评估;扩容后端实例时不能简单地每实例增加连接数。 - CPU 限额只能防止数据库拖垮整机,不能替代 SQL 优化。 - 数据量持续增长后,排行榜和历史统计应迁移到汇总表、缓存或只读副本,不应继续依赖在线全量聚合。 + +## 容器资源限制 + +Compose 对 postgres 与各服务已配置资源限额,默认值按 2G 内存机器设置,均可在 `.env` 覆盖(见 `.env.server.example` 的 `POSTGRES_*` / `BACKEND_MEM_LIMIT` / `WEB_MEM_LIMIT` 等): + +| 服务 | 限额 | 说明 | +| --- | --- | --- | +| postgres | `mem_limit` 1g、`cpus` 2、`shm_size` 512mb | `shm_size` 不足会导致并行查询/大排序报错;`stop_grace_period` 60s 保证检查点能完整落盘 | +| backend | `mem_limit` 512m、`cpus` 1 | `stop_grace_period` 30s 保证优雅关闭(HTTP 收尾 + 连接池释放)能跑完 | +| web | `mem_limit` 256m、`cpus` 0.5 | Caddy 静态服务,低占用 | +| dev postgres | 同生产默认 | 仅 postgres 受限;backend/frontend 开发容器不设限,避免 tsx watch / npm install 内存波动被误杀 | + +调整原则: + +- 内存限额不要低于 RSS 峰值的 1.3 倍,否则会触发 OOM 杀容器(`restart: unless-stopped` 会崩溃循环)。 +- 内存类 PG 参数遵循大致比例:`shared_buffers` ≈ 内存的 1/8~1/4,`effective_cache_size` ≈ 内存的 1/2~3/4,且两者之和不宜超过可用内存的 80%。 +- 修改限额后 `docker compose config` 会以明文展示最终值,可用它核对插值结果;变更仅对重建/重启的容器生效。