diff --git a/.gitignore b/.gitignore index 15f0366..63f1481 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ vendor/ .env.* !.env.example +# Runtime logs(容器 bind mount,目录用 .gitkeep 保留) +logs/* +!logs/.gitkeep + # OS .DS_Store Thumbs.db diff --git a/Dockerfile b/Dockerfile index 8fba1cd..257b28a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,15 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app \ && CGO_ENABLED=0 GOOS=linux go build -o /out/backfill ./cmd/backfill FROM alpine:3.20 -RUN apk add --no-cache ca-certificates tzdata +# wget 用于容器 healthcheck;ca-certificates 用于 TLS;tzdata 用于本地时区 +RUN apk add --no-cache ca-certificates tzdata wget WORKDIR /app COPY --from=builder /out/app /app/app COPY --from=builder /out/backfill /app/backfill EXPOSE 8080 + +# 容器级 healthcheck:拒绝把 unhealthy 容器纳入负载均衡 +HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \ + CMD wget --spider -q http://127.0.0.1:8080/v1/health || exit 1 + ENTRYPOINT ["/app/app"] diff --git a/README.md b/README.md index 4b47d1a..db6c1a2 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,93 @@ curl -s 'localhost:8080/v1/market/context?symbol=BTCUSDT' | jq 'keys' --- +## 一键部署(docker compose) + +适合生产或测试环境快速起服务。**3 步上线**: + +```bash +# 1. 准备环境变量 +cp .env.example .env +# 编辑 .env,至少修改 POSTGRES_PASSWORD(不修改 compose 直接拒绝启动) +# 国内机器还要填 HTTP_PROXY / HTTPS_PROXY + +# 2. 启动(自动建表) +docker compose up -d + +# 3. 验证 +curl -s http://localhost:8080/v1/health | jq . +docker compose ps # 应看到 3 行:postgres healthy / migrate exited(0) / app running +``` + +### 启动顺序 + +`docker compose up` 会按如下依赖链推进,**任何一环失败都会阻断**: + +``` +postgres 起 → pg_isready healthcheck 通过 + → migrate 容器跑 migrations/ up → 退出 0 + → app 起,连 DB,启 cron +``` + +migrate 是一次性 init container(image `migrate/migrate:v4.18.1`),跑完立即退出。app 用 `depends_on: condition: service_completed_successfully` 等它结束,保证 app 启动时表一定存在。 + +### 容器 healthcheck + +- **postgres**:`pg_isready`,5s 间隔 +- **app**:`wget --spider /v1/health`,15s 间隔,start_period 20s + +`docker compose ps` 会显示 `(healthy)` 或 `(unhealthy)`。k8s / 反向代理 / LB 可直接根据 healthcheck 状态做流量切换。 + +### 优雅停机 + +`docker compose stop` 默认 10s,本项目设 `stop_grace_period: 30s`: + +- app 收到 SIGTERM +- fiber `ShutdownWithContext(10s)` 等当前 HTTP 请求结束 +- cron Scheduler.Stop() 等当前 collector 任务结束 +- 30s 超时后 docker 才发 SIGKILL + +### 日志 + +容器 stdout 走 docker json-file driver: + +- 单文件最大 50 MB +- 保留 5 个滚动文件 +- 应用业务日志额外挂载到宿主机 `./logs/` + +查实时日志: + +```bash +docker compose logs -f app +docker compose logs -f postgres +``` + +### 回填历史 + +启动后 DB 是空的;调用 `/v1/market/context` 会 fallback 到 Binance(带 warnings)。要立刻获得满载历史,进容器跑 backfill: + +```bash +docker compose exec app /app/backfill --symbol BTCUSDT --interval 1h --limit 500 +docker compose exec app /app/backfill --symbol ETHUSDT --interval 1h --limit 500 +# 其它 interval 同理 +``` + +或等 cron 自然落库(15m K 线每 15 分钟一次,1h 每整点一次)。 + +### 生产 checklist + +合并前再过一遍: + +- [ ] `.env` 中 `POSTGRES_PASSWORD` 已改为强密码 +- [ ] 国内机器:`.env` 中 `HTTP_PROXY` / `HTTPS_PROXY` 已配 +- [ ] `docker compose config` 渲染后无 `${VAR:?}` 报错 +- [ ] `docker compose up -d` 后 `docker compose ps` 全部 healthy / exited(0) +- [ ] `curl /v1/health` 200,`curl /v1/market/context?symbol=BTCUSDT` 返回非空 `technical` +- [ ] 宿主机 `./logs/` 目录有 app 写入权限(默认 alpine root 用户写没问题) +- [ ] `make guard` 在仓库根目录全绿(即使是 deploy 修改,守卫规则也必须不破) + +--- + ## 国内网络 `fapi.binance.com` 国内直连不稳定,通过环境变量挂代理即可(Go 默认尊重 `HTTPS_PROXY`): diff --git a/docker-compose.yml b/docker-compose.yml index 5a72253..f4d10d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,12 @@ services: timeout: 3s retries: 10 start_period: 10s + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" # init container:跑完 migration 立即退出。 # 由 app 依赖它 service_completed_successfully,保证表存在再启动 app。 @@ -58,6 +64,16 @@ services: NO_PROXY: ${NO_PROXY:-} volumes: - ./config:/app/config:ro + - ./logs:/app/logs + # SIGTERM → 等 app 内置 10s graceful shutdown(fiber.ShutdownWithContext) + # + cron 任务结束的缓冲 = 30s。超时后 docker 发 SIGKILL。 + stop_grace_period: 30s + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" volumes: hermes_pg_data: diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29