604 lines
14 KiB
Markdown
604 lines
14 KiB
Markdown
# 基于 FTP 中转的配置双向同步工具设计方案
|
||
|
||
## 1. 文档目的
|
||
|
||
本文档用于说明一套基于 FTP 中转的配置同步工具设计方案,满足以下目标:
|
||
|
||
- 开发环境定时从 Git 拉取新配置,并通过生产环境 `push` 接口推送到生产
|
||
- 生产环境定时从 `pull` 接口拉取新配置,并同步回开发环境 Git
|
||
- 在开发环境与生产环境不能直接互通时,通过 FTP 服务作为中转通道完成双向同步
|
||
|
||
## 2. 已知约束
|
||
|
||
### 2.1 技术约束
|
||
|
||
- JDK:`1.8`
|
||
- Spring Boot:`2.7.18`
|
||
- 轻量数据库:`H2` 或同类开源可商用数据库
|
||
- 其他依赖必须为开源可商用组件
|
||
|
||
### 2.2 网络与部署约束
|
||
|
||
- 无法登录 FTP 所在服务器主机
|
||
- 只能访问 FTP 服务:`IP + 端口 + 用户名/密码`
|
||
- 网络拓扑如下:
|
||
|
||
```text
|
||
开发环境 <----> FTP A <----> 生产环境
|
||
```
|
||
|
||
说明:
|
||
|
||
- 开发环境可以访问 FTP A
|
||
- 生产环境可以访问 FTP A
|
||
- 开发与生产不假设可以直接互通
|
||
|
||
## 3. 设计原则
|
||
|
||
- 同一套程序,按不同 `profile` 部署在开发和生产两端
|
||
- 通过 FTP 传递标准化同步包,避免环境间直接通信依赖
|
||
- 使用本地状态库记录同步任务、检查点、应答信息,保证可追踪、可恢复
|
||
- 同步流程必须具备幂等控制,避免重复推送、重复提交
|
||
- 开发到生产、生产回开发必须隔离处理,避免双向同步形成死循环
|
||
|
||
## 4. 总体方案
|
||
|
||
推荐采用“**双端代理 + FTP 中转 + 本地状态库**”架构:
|
||
|
||
- `Sync-Agent-Dev`:部署在开发环境
|
||
- `Sync-Agent-Prod`:部署在生产环境
|
||
- `FTP A`:作为唯一中转通道
|
||
- `H2`:记录同步状态、任务、检查点、重试信息
|
||
|
||
整体结构如下:
|
||
|
||
```text
|
||
开发环境
|
||
Sync-Agent-Dev
|
||
|- 拉取 Git
|
||
|- 上传/下载 FTP A
|
||
|- 写入 Git
|
||
|
||
生产环境
|
||
Sync-Agent-Prod
|
||
|- 调用生产 pull 接口
|
||
|- 调用生产 push 接口
|
||
|- 上传/下载 FTP A
|
||
|
||
中转
|
||
FTP A
|
||
|- dev-to-prod/
|
||
|- prod-to-dev/
|
||
|- ack/
|
||
|- failed/
|
||
```
|
||
|
||
## 5. 部署模式
|
||
|
||
建议只维护一套代码,通过 Spring Profile 控制角色:
|
||
|
||
- `dev-agent`:启用开发侧能力
|
||
- `prod-agent`:启用生产侧能力
|
||
|
||
### 5.1 开发侧职责
|
||
|
||
- 定时拉取 Git 指定分支的新配置
|
||
- 判断是否存在新的有效版本
|
||
- 打包配置并上传到 FTP
|
||
- 下载生产侧回传的同步包
|
||
- 将生产侧回传配置写入 Git
|
||
- 提交并推送到远端仓库
|
||
|
||
### 5.2 生产侧职责
|
||
|
||
- 轮询 FTP,获取开发侧上传的配置包
|
||
- 校验后调用生产 `push` 接口导入配置
|
||
- 定时调用生产 `pull` 接口拉取最新配置
|
||
- 打包并上传回 FTP,供开发侧消费
|
||
|
||
## 6. 技术选型
|
||
|
||
| 分类 | 选型 | 说明 |
|
||
| --- | --- | --- |
|
||
| 运行时 | JDK 1.8 | 满足约束 |
|
||
| 框架 | Spring Boot 2.7.18 | 主体框架 |
|
||
| 调度 | Spring Scheduling | 实现定时任务 |
|
||
| 重试 | Spring Retry | 失败重试 |
|
||
| 数据库 | H2 File Mode | 轻量、嵌入式、可持久化 |
|
||
| Git 操作 | JGit | 纯 Java 实现 |
|
||
| FTP 操作 | Apache Commons Net | 主流 FTP 客户端 |
|
||
| JSON | Jackson | 标准序列化组件 |
|
||
| 日志 | SLF4J + Logback | 默认日志能力 |
|
||
|
||
### 6.1 数据库模式建议
|
||
|
||
虽然需求提到“类似 H2 的轻量化内存数据库”,但本场景不建议纯内存模式,原因如下:
|
||
|
||
- 服务重启后需要保留同步检查点
|
||
- 失败任务需要支持补偿和人工追踪
|
||
- 需要记录包处理状态,避免重复消费
|
||
|
||
因此建议使用:
|
||
|
||
- `H2 File Mode`
|
||
|
||
即本地文件数据库,仍然轻量,但支持状态持久化。
|
||
|
||
## 7. 核心业务流程
|
||
|
||
系统包含两条主链路。
|
||
|
||
### 7.1 链路一:开发 Git -> 生产 push 接口
|
||
|
||
用途:将开发环境 Git 中的新配置推送到生产环境。
|
||
|
||
流程如下:
|
||
|
||
1. `dev-agent` 定时拉取 Git 指定分支
|
||
2. 判断 Git 最新提交是否为新的有效配置版本
|
||
3. 将配置目录打包为标准同步包
|
||
4. 上传至 FTP 路径 `dev-to-prod/out/`
|
||
5. `prod-agent` 轮询 FTP,发现新包后下载
|
||
6. 校验包完整性、幂等键和来源信息
|
||
7. 调用生产环境 `push` 接口导入配置
|
||
8. 成功后生成 `ack` 文件上传到 FTP
|
||
9. `dev-agent` 读取 `ack`,将任务状态更新为成功
|
||
|
||
建议时序图如下:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant G as Git(开发)
|
||
participant D as Sync-Agent-Dev
|
||
participant F as FTP A
|
||
participant P as Sync-Agent-Prod
|
||
participant API as 生产Push接口
|
||
|
||
D->>G: 定时 pull 配置
|
||
D->>D: 检查是否有新版本
|
||
D->>D: 打包 zip + manifest
|
||
D->>F: 上传 dev-to-prod/out/
|
||
P->>F: 轮询并下载新包
|
||
P->>P: 校验 hash/traceId
|
||
P->>API: 调用 push 接口
|
||
API-->>P: 返回处理结果
|
||
P->>F: 上传 ack
|
||
D->>F: 读取 ack
|
||
D->>D: 更新状态为成功
|
||
```
|
||
|
||
### 7.2 链路二:生产 pull 接口 -> 开发 Git
|
||
|
||
用途:将生产环境当前配置回传到开发环境,形成配置镜像或审计记录。
|
||
|
||
流程如下:
|
||
|
||
1. `prod-agent` 定时调用生产 `pull` 接口
|
||
2. 将返回配置标准化后计算版本标识或内容哈希
|
||
3. 如果与上次同步结果不同,则打包上传到 FTP `prod-to-dev/out/`
|
||
4. `dev-agent` 轮询 FTP 并下载新包
|
||
5. 解包后写入本地 Git 工作目录
|
||
6. 提交 commit 并推送到远端 Git
|
||
7. 成功后写回 `ack`
|
||
|
||
建议时序图如下:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant API as 生产Pull接口
|
||
participant P as Sync-Agent-Prod
|
||
participant F as FTP A
|
||
participant D as Sync-Agent-Dev
|
||
participant G as Git(开发)
|
||
|
||
P->>API: 定时调用 pull 接口
|
||
API-->>P: 返回当前配置
|
||
P->>P: 标准化并计算 hash
|
||
P->>F: 上传 prod-to-dev/out/
|
||
D->>F: 轮询并下载新包
|
||
D->>D: 解包并写入工作区
|
||
D->>G: commit + push
|
||
D->>F: 上传 ack
|
||
```
|
||
|
||
## 8. 标准同步包设计
|
||
|
||
为保证跨环境处理一致,建议所有同步内容封装为统一格式的压缩包。
|
||
|
||
### 8.1 包结构
|
||
|
||
```text
|
||
package.zip
|
||
|- manifest.json
|
||
|- config/
|
||
|- sha256.txt
|
||
```
|
||
|
||
### 8.2 manifest 字段建议
|
||
|
||
```json
|
||
{
|
||
"traceId": "uuid",
|
||
"direction": "DEV_TO_PROD",
|
||
"sourceEnv": "DEV",
|
||
"sourceVersion": "gitCommitId",
|
||
"contentHash": "sha256",
|
||
"createdAt": "2026-04-15T10:00:00+08:00"
|
||
}
|
||
```
|
||
|
||
### 8.3 字段说明
|
||
|
||
- `traceId`:本次同步唯一流水号
|
||
- `direction`:同步方向,例如 `DEV_TO_PROD`、`PROD_TO_DEV`
|
||
- `sourceEnv`:来源环境
|
||
- `sourceVersion`:来源版本号,开发侧通常为 Git Commit ID
|
||
- `contentHash`:配置内容哈希,便于判断重复包
|
||
- `createdAt`:包生成时间
|
||
|
||
## 9. FTP 目录规划
|
||
|
||
建议在 FTP A 上使用如下目录结构:
|
||
|
||
```text
|
||
/dev-to-prod/out/
|
||
/dev-to-prod/ack/
|
||
/prod-to-dev/out/
|
||
/prod-to-dev/ack/
|
||
/failed/
|
||
```
|
||
|
||
目录说明:
|
||
|
||
- `/dev-to-prod/out/`:开发侧发往生产侧的同步包
|
||
- `/dev-to-prod/ack/`:生产侧返回的处理应答
|
||
- `/prod-to-dev/out/`:生产侧发往开发侧的同步包
|
||
- `/prod-to-dev/ack/`:开发侧返回的处理应答
|
||
- `/failed/`:失败包归档目录
|
||
|
||
### 9.1 上传规范
|
||
|
||
为避免消费端读取到半截文件,建议采用临时文件上传策略:
|
||
|
||
1. 先上传为 `.tmp`
|
||
2. 上传完成后重命名为正式 `.zip`
|
||
3. 消费端只处理 `.zip` 文件
|
||
|
||
## 10. Git 分支策略
|
||
|
||
这是方案中的关键设计点。
|
||
|
||
不建议将“开发配置推生产”和“生产配置回传开发”写到同一个 Git 分支,否则极易形成循环同步。
|
||
|
||
建议拆分为两个分支:
|
||
|
||
- `config-dev-main`:开发主配置分支
|
||
- `config-prod-snapshot`:生产配置镜像分支
|
||
|
||
同步规则:
|
||
|
||
- `DEV -> PROD` 只消费 `config-dev-main`
|
||
- `PROD -> DEV` 只写入 `config-prod-snapshot`
|
||
|
||
### 10.1 这样设计的好处
|
||
|
||
- 避免双向同步形成闭环
|
||
- 生产回传配置不会覆盖开发主线
|
||
- 便于审计“生产当前实际配置”
|
||
|
||
### 10.2 机器人提交标记
|
||
|
||
建议同步工具在 commit message 中增加固定前缀,例如:
|
||
|
||
```text
|
||
sync(prod->git): traceId=xxx version=xxx
|
||
```
|
||
|
||
开发侧扫描 Git 时应忽略同步机器人生成的提交,进一步降低环路风险。
|
||
|
||
## 11. 本地状态库设计
|
||
|
||
建议至少建立以下 3 张表。
|
||
|
||
### 11.1 `sync_checkpoint`
|
||
|
||
用于记录各方向的最后一次成功检查点。
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| id | bigint | 主键 |
|
||
| direction | varchar | 同步方向 |
|
||
| last_success_version | varchar | 最后成功版本 |
|
||
| last_success_hash | varchar | 最后成功内容哈希 |
|
||
| updated_at | timestamp | 更新时间 |
|
||
|
||
### 11.2 `sync_task`
|
||
|
||
用于记录每次同步任务生命周期。
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| id | bigint | 主键 |
|
||
| trace_id | varchar | 流水号 |
|
||
| direction | varchar | 同步方向 |
|
||
| source_version | varchar | 来源版本 |
|
||
| package_name | varchar | 包文件名 |
|
||
| status | varchar | 状态 |
|
||
| retry_count | int | 重试次数 |
|
||
| error_msg | clob | 错误信息 |
|
||
| created_at | timestamp | 创建时间 |
|
||
| updated_at | timestamp | 更新时间 |
|
||
|
||
### 11.3 `sync_ack`
|
||
|
||
用于记录应答信息。
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| id | bigint | 主键 |
|
||
| trace_id | varchar | 流水号 |
|
||
| ack_side | varchar | 应答方 |
|
||
| ack_status | varchar | 应答状态 |
|
||
| ack_time | timestamp | 应答时间 |
|
||
| remark | varchar | 备注 |
|
||
|
||
## 12. 幂等与一致性设计
|
||
|
||
### 12.1 幂等键建议
|
||
|
||
建议以如下组合作为幂等键:
|
||
|
||
```text
|
||
direction + sourceVersion + contentHash
|
||
```
|
||
|
||
约束效果:
|
||
|
||
- 已经处理过的包不能重复推送
|
||
- 已经提交过的生产快照不能重复写 Git
|
||
|
||
### 12.2 一致性策略
|
||
|
||
本方案属于跨系统、跨网络的异步同步,不适合做强一致事务。
|
||
|
||
建议采用:
|
||
|
||
- “本地落库 + 外部调用 + 最终一致”模式
|
||
- 每一步记录状态
|
||
- 失败后允许自动重试或人工补偿
|
||
|
||
## 13. 失败处理与补偿机制
|
||
|
||
### 13.1 自动重试
|
||
|
||
以下场景建议自动重试:
|
||
|
||
- FTP 上传失败
|
||
- FTP 下载失败
|
||
- 生产 `push` 接口调用失败
|
||
- 生产 `pull` 接口调用失败
|
||
- Git push 失败
|
||
|
||
建议策略:
|
||
|
||
- 最大重试次数:`3 ~ 5`
|
||
- 重试间隔:指数退避,例如 `30s / 60s / 120s`
|
||
|
||
### 13.2 失败归档
|
||
|
||
连续失败后建议:
|
||
|
||
- 将包移动到 FTP 的 `/failed/`
|
||
- 将任务状态置为 `FAILED`
|
||
- 记录完整错误信息
|
||
- 触发告警
|
||
|
||
### 13.3 人工补偿
|
||
|
||
后续可以增加一个管理接口,支持:
|
||
|
||
- 按 `traceId` 重新执行
|
||
- 重置任务状态
|
||
- 查看失败原因
|
||
|
||
## 14. 安全设计
|
||
|
||
### 14.1 传输安全
|
||
|
||
优先级建议如下:
|
||
|
||
1. 优先使用 `FTPS`
|
||
2. 如果只能使用普通 FTP,建议对同步包内容做 AES 加密
|
||
|
||
### 14.2 凭据管理
|
||
|
||
以下信息不得写死在代码中:
|
||
|
||
- FTP 地址、端口、用户名、密码
|
||
- Git 用户名、密码或 Token
|
||
- 生产接口认证信息
|
||
|
||
建议通过以下方式外置:
|
||
|
||
- `application-*.properties`
|
||
- 环境变量
|
||
- 启动参数
|
||
|
||
### 14.3 审计日志
|
||
|
||
建议记录:
|
||
|
||
- 谁发起了同步
|
||
- 同步方向
|
||
- 来源版本
|
||
- 包名
|
||
- 接口调用结果
|
||
- 异常原因
|
||
|
||
## 15. 项目结构建议
|
||
|
||
有两种实现方式。
|
||
|
||
### 15.1 方案 A:单工程 + Profile 切换
|
||
|
||
适用于项目规模较小、交付快的场景。
|
||
|
||
```text
|
||
sync-tool
|
||
|- src/main/java
|
||
| |- config
|
||
| |- ftp
|
||
| |- git
|
||
| |- job
|
||
| |- package
|
||
| |- repository
|
||
| |- service
|
||
| |- web
|
||
|- src/main/resources
|
||
| |- application.properties
|
||
| |- application-dev-agent.properties
|
||
| |- application-prod-agent.properties
|
||
```
|
||
|
||
### 15.2 方案 B:多模块拆分
|
||
|
||
适用于后续可能演化较多、职责更清晰的场景。
|
||
|
||
```text
|
||
sync-tool
|
||
|- common
|
||
|- dev-agent
|
||
|- prod-agent
|
||
```
|
||
|
||
当前建议优先采用:
|
||
|
||
- `方案 A:单工程 + Profile`
|
||
|
||
理由:
|
||
|
||
- 实现成本低
|
||
- 运维简单
|
||
- 早期更适合快速打通链路
|
||
|
||
## 16. 核心模块划分
|
||
|
||
建议按职责拆分以下模块:
|
||
|
||
- `GitService`
|
||
- 拉取仓库
|
||
- 检查最新提交
|
||
- 提交并推送生产回传配置
|
||
- `FtpService`
|
||
- 上传、下载、重命名、目录扫描
|
||
- `PackageService`
|
||
- 生成 zip
|
||
- 生成 manifest
|
||
- 校验 hash
|
||
- `SyncTaskService`
|
||
- 任务创建
|
||
- 状态变更
|
||
- 检查点维护
|
||
- `ProdPushService`
|
||
- 调用生产 `push` 接口
|
||
- `ProdPullService`
|
||
- 调用生产 `pull` 接口
|
||
- `AckService`
|
||
- 生成和消费 ack 文件
|
||
- `JobScheduler`
|
||
- 各类定时任务调度
|
||
|
||
## 17. 定时任务建议
|
||
|
||
### 17.1 开发侧任务
|
||
|
||
- `GitPullJob`
|
||
- 周期拉取 Git 并检查是否有新配置
|
||
- `UploadDevPackageJob`
|
||
- 将待同步配置上传到 FTP
|
||
- `ConsumeProdPackageJob`
|
||
- 下载生产回传包并写入 Git
|
||
- `AckScanJob`
|
||
- 扫描生产侧 ack 并更新任务状态
|
||
|
||
### 17.2 生产侧任务
|
||
|
||
- `ConsumeDevPackageJob`
|
||
- 下载开发侧同步包并调用生产 `push`
|
||
- `PullProdConfigJob`
|
||
- 定时调用生产 `pull` 接口
|
||
- `UploadProdPackageJob`
|
||
- 将拉取结果上传到 FTP
|
||
- `AckScanJob`
|
||
- 扫描开发侧 ack 并更新任务状态
|
||
|
||
## 18. 一期 MVP 建议
|
||
|
||
建议按最小可交付版本分阶段实施。
|
||
|
||
### 阶段 1:打通主链路
|
||
|
||
- 建立 Spring Boot 工程
|
||
- 集成 H2、JGit、FTP
|
||
- 实现开发到生产的全量包同步
|
||
- 实现生产 `push` 接口调用
|
||
|
||
### 阶段 2:打通回传链路
|
||
|
||
- 接入生产 `pull` 接口
|
||
- 实现生产到开发的 FTP 回传
|
||
- 实现开发侧写入 Git 并推送
|
||
|
||
### 阶段 3:增强稳定性
|
||
|
||
- 增加重试
|
||
- 增加 ack 机制
|
||
- 增加失败归档
|
||
- 增加告警与审计日志
|
||
|
||
## 19. 风险与注意事项
|
||
|
||
### 19.1 最大风险:双向同步闭环
|
||
|
||
如果生产回传配置写入开发主分支,再被开发侧识别为“新配置”,会再次推送到生产,形成无限循环。
|
||
|
||
规避措施:
|
||
|
||
- 使用独立镜像分支
|
||
- 识别机器人提交
|
||
- 使用幂等键
|
||
|
||
### 19.2 配置冲突风险
|
||
|
||
如果开发和生产都会修改同一份配置,且要求双向合并,则不能简单用文件覆盖方式处理。
|
||
|
||
当前建议:
|
||
|
||
- 将生产回传定义为“镜像/审计”
|
||
- 不直接回写开发主配置分支
|
||
|
||
### 19.3 FTP 能力限制
|
||
|
||
如果 FTP 不支持原子重命名、目录权限受限或稳定性较差,需要额外做兼容与重试。
|
||
|
||
## 20. 结论
|
||
|
||
在当前网络条件下,推荐采用“**开发代理 + 生产代理 + FTP 中转 + H2 状态库**”的双端部署方案。
|
||
|
||
该方案具备以下特点:
|
||
|
||
- 不依赖开发与生产直接互通
|
||
- 满足开发到生产、生产到开发的双向同步需求
|
||
- 支持状态记录、失败重试、幂等控制和审计追踪
|
||
- 适合使用 `Java 1.8 + Spring Boot 2.7.18` 快速落地
|
||
|
||
## 21. 后续可继续细化内容
|
||
|
||
后续可以基于本方案继续输出:
|
||
|
||
- `application.properties` 配置项设计
|
||
- H2 建表 SQL
|
||
- 核心类图与接口设计
|
||
- 各定时任务的时序与状态流转
|
||
- Spring Boot 工程骨架
|