- POST /app/watch/progress 只写 Redis 缓冲立即返回(p95 75ms→2ms), WatchProgressFlusher 每 2s 把 watch:pg:dirty 队列按身份合并批量 UPSERT, Redis 异常自动回退同步写库,接口契约不变 - 读接口用缓冲覆盖 DB 行保证读己之写;Caffeine 缓存剧集校验免去上报路径两条 DB 查询 - 落库按「新增进度/新增流水/更新行」分组攒批(MyBatis BATCH 只合并连续同形状语句), 配合 reWriteBatchedInserts 多值 INSERT,12.4 万积压 45 秒消化完, 1650 QPS 持续压测队列稳态归零
264 lines
9.6 KiB
YAML
264 lines
9.6 KiB
YAML
# Author: fruit.com.cn
|
||
#
|
||
# 办公室单机部署(docs/04-部署架构.md §三 精简方案 + §五 第一阶段用 Compose):
|
||
# app-api(8081) + admin-api(8080,只绑本机) + transcode-worker + PostgreSQL 18 + Redis
|
||
# 转码 worker 与在线服务**同机但独立进程/容器**(§8.5 隔离要求:不抢在线服务资源)。
|
||
#
|
||
# 用法(目标主机需要 Docker + Compose):
|
||
# cp .env.example .env # 填密钥与数据库密码
|
||
# docker compose up -d --build
|
||
# tools/smoke.sh
|
||
#
|
||
# 本文件在本机**未验证**(本机没有 Docker),见 DEPLOY.md「未在本机验证的部分」。
|
||
services:
|
||
|
||
# ─────────── 基础设施 ───────────
|
||
db:
|
||
image: postgres:18-alpine
|
||
container_name: fruitshorts-db
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_DB: ${DB_NAME:-fruitshorts}
|
||
POSTGRES_USER: ${DB_USER:-fruitshorts}
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD:?数据库密码必填,见 .env.example}
|
||
# 中文与印尼语内容需要 UTF-8(镜像默认已是 UTF8,这里显式一次)
|
||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
||
volumes:
|
||
# PostgreSQL 18 起镜像把数据放在 /var/lib/postgresql/<major>/,卷必须挂到父目录
|
||
- db-data:/var/lib/postgresql
|
||
# 首次启动自动建表 + 造种子数据;顺序即文件名顺序(脚本本身幂等,可重复执行)
|
||
- ./sql/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||
- ./sql/schema-phase1.sql:/docker-entrypoint-initdb.d/02-schema-phase1.sql:ro
|
||
- ./sql/partition-maintenance.sql:/docker-entrypoint-initdb.d/03-partition-maintenance.sql:ro
|
||
- ./sql/data.sql:/docker-entrypoint-initdb.d/04-data.sql:ro
|
||
- ./sql/data-phase1.sql:/docker-entrypoint-initdb.d/05-data-phase1.sql:ro
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-fruitshorts} -d ${DB_NAME:-fruitshorts}"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 20s
|
||
# 不映射端口:只有同网络的容器需要访问;本机要连库时再放开 5432
|
||
expose:
|
||
- "5432"
|
||
|
||
redis:
|
||
# 第一阶段还没有代码依赖 Redis(C2 才接缓存与限流),先把基础设施就位,
|
||
# 免得上线时再改编排。要持久化就留 volume,纯缓存场景可去掉。
|
||
image: redis:7-alpine
|
||
container_name: fruitshorts-redis
|
||
restart: unless-stopped
|
||
command: ["redis-server", "--appendonly", "yes"]
|
||
volumes:
|
||
- redis-data:/data
|
||
healthcheck:
|
||
test: ["CMD", "redis-cli", "ping"]
|
||
interval: 10s
|
||
timeout: 3s
|
||
retries: 10
|
||
expose:
|
||
- "6379"
|
||
|
||
# ─────────── 应用 ───────────
|
||
app-api:
|
||
build:
|
||
context: .
|
||
args:
|
||
MODULE: fruitshorts-app-api
|
||
image: fruitshorts/app-api:latest
|
||
container_name: fruitshorts-app-api
|
||
restart: unless-stopped
|
||
env_file: .env
|
||
environment:
|
||
SERVER_PORT: "8081"
|
||
# 默认连编排里的 db;要连外部库就在 .env 里覆盖 DB_URL。
|
||
# reWriteBatchedInserts=true:pgjdbc 把 JDBC 批内多条 INSERT 重写成多值
|
||
# INSERT,观看进度缓冲落库(watch:pg:dirty 消费任务)依赖它跑满吞吐。
|
||
DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}?reWriteBatchedInserts=true}
|
||
# 限流/缓存用 Redis;容器内 127.0.0.1 是容器自身,必须指向编排里的 redis
|
||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||
ports:
|
||
- "8081:8081"
|
||
volumes:
|
||
# 媒资落盘:三个服务共享同一个卷
|
||
- ./data/uploads:/data/uploads
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8081/healthz || exit 1"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 40s
|
||
retries: 3
|
||
|
||
admin-api:
|
||
build:
|
||
context: .
|
||
args:
|
||
MODULE: fruitshorts-admin-api
|
||
image: fruitshorts/admin-api:latest
|
||
container_name: fruitshorts-admin-api
|
||
restart: unless-stopped
|
||
env_file: .env
|
||
environment:
|
||
SERVER_PORT: "8080"
|
||
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: "2GB"
|
||
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: "2GB"
|
||
CORS_ALLOWED_ORIGINS: "http://localhost:3005,http://127.0.0.1:3005,http://39.162.50.99:3005,http://192.168.31.250:3005"
|
||
DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}}
|
||
# 管理端仅内网,页面上直接播放/下载裸媒资地址(不签播放签名),
|
||
# 因此这里不启用 /files/episode/** 的签名校验;公开的 app-api 仍保持开启。
|
||
FRUITSHORTS_MEDIA_PROTECTED_PREFIXES: ""
|
||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||
ports:
|
||
# docs/12-上线检查清单「管理端仅内网」:只对局域网开放;不在路由器做端口转发,公网访问不到
|
||
- "8080:8080"
|
||
volumes:
|
||
- ./data/uploads:/data/uploads
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/healthz || exit 1"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 40s
|
||
retries: 3
|
||
|
||
transcode-worker:
|
||
build:
|
||
context: .
|
||
args:
|
||
MODULE: fruitshorts-transcode-worker
|
||
# worker 镜像里装 ffmpeg,两个 API 镜像不装(省 80MB)
|
||
INSTALL_FFMPEG: "true"
|
||
image: fruitshorts/transcode-worker:latest
|
||
container_name: fruitshorts-transcode-worker
|
||
restart: unless-stopped
|
||
env_file: .env
|
||
environment:
|
||
DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}}
|
||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||
volumes:
|
||
# 与两个入口同一个媒资根,否则找不到源文件
|
||
- ./data/uploads:/data/uploads
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
# worker 不监听端口(server.port=0),用进程存活判断
|
||
test: ["CMD-SHELL", "pgrep -f app.jar >/dev/null || exit 1"]
|
||
interval: 60s
|
||
timeout: 5s
|
||
start_period: 40s
|
||
retries: 3
|
||
|
||
# ─────────── 监控 ───────────
|
||
# 编排统一在这里:prometheus(应用指标 + 容器指标 + 黑盒探活)+ grafana(自动加载看板)
|
||
prometheus:
|
||
image: prom/prometheus:latest
|
||
container_name: fruitshorts-prometheus
|
||
restart: unless-stopped
|
||
ports:
|
||
- "9090:9090"
|
||
volumes:
|
||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||
- prometheus-data:/prometheus
|
||
command:
|
||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||
# 15 天保留期,避免磁盘无限增长
|
||
- "--storage.tsdb.retention.time=15d"
|
||
- "--web.enable-lifecycle"
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9090/-/healthy >/dev/null || exit 1"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 20s
|
||
retries: 3
|
||
|
||
grafana:
|
||
image: grafana/grafana:latest
|
||
container_name: fruitshorts-grafana
|
||
restart: unless-stopped
|
||
ports:
|
||
- "3000:3000"
|
||
volumes:
|
||
- grafana-data:/var/lib/grafana
|
||
# 数据源与看板全部来自仓库,换机器不需要手工配
|
||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||
environment:
|
||
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
|
||
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?Grafana 管理员密码必填,见 .env.example}
|
||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||
# 新用户默认界面语言:简体中文
|
||
GF_USERS_LANGUAGE: "zh-Hans"
|
||
TZ: Asia/Shanghai
|
||
depends_on:
|
||
- prometheus
|
||
- loki
|
||
|
||
# ─────────── 日志(Loki + Promtail)───────────
|
||
# Loki:日志存储与查询引擎;Promtail:采集容器 stdout/stderr 推给 Loki。
|
||
# Grafana 里通过 Loki 数据源可直接搜索/过滤各容器的请求日志。
|
||
loki:
|
||
image: grafana/loki:latest
|
||
container_name: fruitshorts-loki
|
||
restart: unless-stopped
|
||
command: -config.file=/etc/loki/local-config.yaml
|
||
volumes:
|
||
- ./loki-config.yml:/etc/loki/local-config.yaml:ro
|
||
- loki-data:/loki
|
||
expose:
|
||
- "3100"
|
||
# 镜像为 distroless(无 shell/wget),无法在容器内做 HTTP 探活;
|
||
# 靠 restart: unless-stopped 自动拉起,Grafana 数据源状态可反映可用性。
|
||
|
||
promtail:
|
||
image: grafana/promtail:latest
|
||
container_name: fruitshorts-promtail
|
||
restart: unless-stopped
|
||
command: -config.file=/etc/promtail/config.yml
|
||
volumes:
|
||
- ./promtail-config.yml:/etc/promtail/config.yml:ro
|
||
# 读取容器日志需要 Docker socket
|
||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||
depends_on:
|
||
- loki
|
||
|
||
# 黑盒探活:从外部探测接口可用性与耗时
|
||
blackbox:
|
||
image: prom/blackbox-exporter:latest
|
||
container_name: fruitshorts-blackbox
|
||
restart: unless-stopped
|
||
volumes:
|
||
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
|
||
|
||
# 容器资源指标(CPU / 内存 / 网络 / 重启),供 Prometheus 抓取
|
||
cadvisor:
|
||
image: gcr.io/cadvisor/cadvisor:latest
|
||
container_name: fruitshorts-cadvisor
|
||
restart: unless-stopped
|
||
privileged: true
|
||
volumes:
|
||
- /:/rootfs:ro
|
||
- /var/run:/var/run:ro
|
||
- /sys:/sys:ro
|
||
- /var/lib/docker/:/var/lib/docker:ro
|
||
|
||
volumes:
|
||
# 命名卷:跟着 compose 项目走,`docker compose down -v` 才会删
|
||
db-data:
|
||
redis-data:
|
||
prometheus-data:
|
||
grafana-data:
|
||
loki-data:
|