# 技术设计:部署自初始化 ## Context ### 当前状态 1. **目录创建分散** - `pkg/storage/s3.go` 在初始化时创建临时目录 - 日志目录依赖 Dockerfile 预创建 - 没有统一的初始化入口 2. **配置管理复杂** - 4 个外部配置文件:`config.yaml`、`config.dev.yaml`、`config.staging.yaml`、`config.prod.yaml` - `pkg/config/watcher.go` 实现热重载(开发阶段不需要) - 必须手动拷贝配置文件才能启动 3. **部署流程繁琐** - 需要手动创建目录结构 - 需要手动拷贝配置文件 - Docker Compose 挂载 configs 目录 ### 约束 - 使用 Go 1.16+ 的 `go:embed` 特性 - 保持 Viper 作为配置解析库 - 容器内以非 root 用户 (appuser:1000) 运行 ## Goals / Non-Goals **Goals:** - 应用启动时自动创建所有必需目录 - 配置嵌入二进制,无需外部配置文件 - 环境变量作为配置覆盖机制 - 部署流程简化到 1 步 **Non-Goals:** - 配置热重载(开发阶段移除) - 多配置文件支持(统一用嵌入默认值 + 环境变量) - 向后兼容旧的配置方式 ## Decisions ### Decision 1: 目录初始化放在应用层 **选择**: 在 `main.go` 启动时调用集中化的目录初始化函数 **备选方案**: | 方案 | 优点 | 缺点 | |------|------|------| | Dockerfile RUN mkdir | 镜像固定 | 不支持运行时配置路径 | | Entrypoint 脚本 | 运行时动态 | Shell 脚本维护成本高 | | **应用代码初始化** | 跨环境通用、错误处理清晰 | 无 | **理由**: 应用代码可以读取配置中的路径,提供 Go 级别的错误处理,并在所有部署环境(Docker、K8s、裸机)通用。 ### Decision 2: 配置嵌入 + 环境变量覆盖 **选择**: 使用 `go:embed` 嵌入默认配置,环境变量覆盖 **配置优先级**: ``` 环境变量 (JUNHONG_*) > 嵌入默认值 ``` **备选方案**: | 方案 | 优点 | 缺点 | |------|------|------| | 纯外部文件 | 运行时可改 | 部署复杂 | | 纯环境变量 | 12-Factor | 复杂配置难表达 | | **嵌入 + 环境变量** | 开箱即用 + 灵活覆盖 | 改默认值需重编译 | **理由**: 嵌入配置保证"开箱即用",环境变量覆盖满足生产环境定制需求。开发阶段不需要频繁改默认值。 ### Decision 3: 环境变量命名规范 **选择**: `JUNHONG_` 前缀 + 下划线分隔 **格式**: `JUNHONG_{SECTION}_{KEY}` **示例**: ```bash JUNHONG_DATABASE_HOST=localhost JUNHONG_DATABASE_PORT=5432 JUNHONG_REDIS_ADDRESS=localhost:6379 JUNHONG_SERVER_ADDRESS=:3000 ``` **理由**: - 前缀避免与系统环境变量冲突 - 下划线分隔便于 Viper 的 `SetEnvKeyReplacer` 处理 - 符合业界惯例(类似 `POSTGRES_`、`REDIS_` 等) ### Decision 4: 删除配置热重载 **选择**: 删除 `pkg/config/watcher.go` 及相关逻辑 **理由**: - 开发阶段,配置变更频率低 - 重启容器即可应用新配置 - 减少代码复杂度 ### Decision 5: 目录降级策略 **选择**: 权限不足时使用系统临时目录作为 fallback ```go if err := os.MkdirAll(dir, 0755); err != nil { if os.IsPermission(err) { fallback := filepath.Join(os.TempDir(), "junhong", filepath.Base(dir)) os.MkdirAll(fallback, 0755) return fallback, nil } return "", err } ``` **理由**: 提高容错性,即使权限配置不当也能启动(降级运行)。 ## Architecture ### 启动流程 ``` main.go │ ├── 1. config.Load() // 加载嵌入配置 + 环境变量覆盖 │ ├── 读取 go:embed 的 defaults/config.yaml │ ├── 应用 JUNHONG_* 环境变量覆盖 │ └── 验证必填配置 │ ├── 2. bootstrap.EnsureDirectories(cfg) // 创建所有必需目录 │ ├── 临时文件目录 │ ├── 日志目录 │ └── 其他运行时目录 │ ├── 3. logger.Init(cfg) // 初始化日志 │ ├── 4. database.Init(cfg) // 初始化数据库 │ └── 5. ... 其他组件初始化 ``` ### 目录结构 ``` pkg/ ├── bootstrap/ │ └── directories.go # 新增:目录初始化 ├── config/ │ ├── config.go # 保留:配置结构定义 │ ├── loader.go # 重写:嵌入配置加载 │ ├── embedded.go # 新增:go:embed 逻辑 │ ├── defaults/ │ │ └── config.yaml # 新增:嵌入的默认配置 │ └── watcher.go # 删除 ``` ### 嵌入配置内容 ```yaml # pkg/config/defaults/config.yaml server: address: ":3000" read_timeout: 30s write_timeout: 30s shutdown_timeout: 30s prefork: false database: host: "" # 必须通过 JUNHONG_DATABASE_HOST 设置 port: 5432 user: "" # 必须通过 JUNHONG_DATABASE_USER 设置 password: "" # 必须通过 JUNHONG_DATABASE_PASSWORD 设置 dbname: "" # 必须通过 JUNHONG_DATABASE_DBNAME 设置 sslmode: "disable" max_open_conns: 25 max_idle_conns: 10 conn_max_lifetime: 5m redis: address: "" # 必须通过 JUNHONG_REDIS_ADDRESS 设置 port: 6379 password: "" db: 0 pool_size: 10 min_idle_conns: 5 dial_timeout: 5s read_timeout: 3s write_timeout: 3s storage: provider: "s3" temp_dir: "/tmp/junhong-storage" s3: endpoint: "" region: "" bucket: "" access_key_id: "" secret_access_key: "" use_ssl: false path_style: true presign: upload_expires: 15m download_expires: 24h logging: level: "info" development: false app_log: filename: "/app/logs/app.log" max_size: 100 max_backups: 3 max_age: 7 compress: true access_log: filename: "/app/logs/access.log" max_size: 100 max_backups: 3 max_age: 7 compress: true queue: concurrency: 10 retry_max: 5 timeout: 10m jwt: secret_key: "" # 必须通过 JUNHONG_JWT_SECRET_KEY 设置 token_duration: 24h access_token_ttl: 24h refresh_token_ttl: 168h middleware: enable_rate_limiter: false rate_limiter: max: 100 expiration: 1m storage: "memory" ``` ## Risks / Trade-offs | 风险 | 影响 | 缓解措施 | |------|------|----------| | 嵌入配置修改需重新编译 | 低(开发阶段) | 敏感配置通过环境变量,嵌入的只是默认值 | | 环境变量泄露 | 中 | 使用 K8s Secrets 或 Docker Secrets 管理 | | 目录降级后行为不一致 | 低 | 降级时打印 WARN 日志,明确告知 | | 删除热重载后调试不便 | 低 | 开发时直接重启进程,生产用 rolling update | ## Migration Plan ### 实施步骤 1. **新增目录初始化模块** - 创建 `pkg/bootstrap/directories.go` - 在 main.go 中调用 2. **新增配置嵌入模块** - 创建 `pkg/config/defaults/config.yaml` - 创建 `pkg/config/embedded.go` - 重写 `pkg/config/loader.go` 3. **清理旧配置逻辑** - 删除 `pkg/config/watcher.go` - 删除 `configs/*.yaml` 4. **更新 Docker 配置** - 更新 `Dockerfile.api` 和 `Dockerfile.worker` - 重写 `docker-compose.prod.yml` 5. **更新文档** - 更新 README.md 部署说明 - 更新环境变量文档 ### 回滚策略 如需回滚,恢复 git 提交即可(开发阶段无生产数据风险)。 ## Open Questions 无。设计已明确,可直接实施。