依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
146 lines
5.8 KiB
Markdown
146 lines
5.8 KiB
Markdown
# ADR-004:模型儲存採用 S3-Compatible 介面
|
||
|
||
## 狀態
|
||
Accepted — 2026-04-21
|
||
|
||
## 背景 (Context)
|
||
|
||
visionA Cloud 需要儲存:
|
||
|
||
1. **使用者上傳的模型檔案**(`.nef`、`.onnx` 等,幾 MB ~ 幾百 MB)
|
||
2. **模型 metadata**(JSON,幾 KB)
|
||
3. **(未來)轉檔產物**:kneron_model_converter 產生的 `.nef`
|
||
4. **(未來)推論紀錄、snapshots、截圖**
|
||
|
||
POC(edge-ai-platform)用本地檔案系統 + in-memory registry。local-tool 也一樣。這在單機環境 OK,雲端不行:
|
||
|
||
| 問題 | 本地檔案系統 | S3-compatible |
|
||
|------|------------|--------------|
|
||
| 多節點共用 | ❌ 各節點看不到彼此 | ✅ 共享 bucket |
|
||
| 持久性 | 單機磁碟壞 = 資料沒 | ✅ 11 個 9 的設計 |
|
||
| 容量彈性 | 單機磁碟有上限 | ✅ 幾乎無上限 |
|
||
| 成本效益 | 計算節點帶容量 = 貴 | 冷熱分層、便宜 |
|
||
| 直接給前端下載 | 要透過應用伺服器 | ✅ Presigned URL 直給 |
|
||
| Multipart upload | 自己實作 | ✅ 標準 |
|
||
|
||
同時,雛形階段不想立刻接真實 S3 / MinIO(需架設、需 credentials、增加 setup 成本)。
|
||
|
||
## 決策 (Decision)
|
||
|
||
在 `internal/storage` 定義 **`Store` interface**,以 S3 的語義為基礎:
|
||
|
||
```go
|
||
package storage
|
||
|
||
import (
|
||
"context"
|
||
"io"
|
||
"time"
|
||
)
|
||
|
||
// Store 是模型 / 檔案儲存的抽象層。語義對齊 S3。
|
||
type Store interface {
|
||
// Put 上傳一個物件。size <0 表示未知大小(streaming)。
|
||
Put(ctx context.Context, key string, r io.Reader, size int64, meta map[string]string) error
|
||
|
||
// Get 下載一個物件,回傳 reader(caller 要負責 Close)。
|
||
Get(ctx context.Context, key string) (io.ReadCloser, *ObjectInfo, error)
|
||
|
||
// Stat 取得物件資訊但不下載 body。
|
||
Stat(ctx context.Context, key string) (*ObjectInfo, error)
|
||
|
||
// Delete 刪除一個物件。
|
||
Delete(ctx context.Context, key string) error
|
||
|
||
// List 列出 prefix 下的物件。
|
||
List(ctx context.Context, prefix string) ([]*ObjectInfo, error)
|
||
|
||
// PresignedGetURL 產生讓使用者直接下載的簽名 URL。
|
||
PresignedGetURL(ctx context.Context, key string, ttl time.Duration) (string, error)
|
||
|
||
// PresignedPutURL 產生讓使用者直接上傳的簽名 URL。
|
||
PresignedPutURL(ctx context.Context, key string, ttl time.Duration) (string, error)
|
||
}
|
||
|
||
type ObjectInfo struct {
|
||
Key string
|
||
Size int64
|
||
ContentType string
|
||
LastModified time.Time
|
||
ETag string
|
||
Metadata map[string]string
|
||
}
|
||
```
|
||
|
||
### 雛形提供兩個實作
|
||
|
||
1. **`LocalFSStore`**:把物件存在本地 `data/storage/` 下
|
||
- Key 對應檔案路徑(`models/user-123/yolov5.nef` → `data/storage/models/user-123/yolov5.nef`)
|
||
- Metadata 存在同名 `.meta.json`
|
||
- `PresignedGetURL` / `PresignedPutURL` 回 `http://localhost:PORT/storage/...` 這種由 api-server 代理的 URL(雛形的「假簽名」)
|
||
- 啟動最快,開發者零成本
|
||
|
||
2. **`S3Store`**(可選,雛形後期加):用 `aws-sdk-go-v2`
|
||
- 連真實 AWS S3、MinIO、Cloudflare R2、Backblaze B2 等
|
||
- `PresignedGetURL` / `PresignedPutURL` 是真的 S3 presigned URL
|
||
- 需要 `S3_ENDPOINT`、`S3_BUCKET`、`S3_REGION`、`S3_ACCESS_KEY`、`S3_SECRET_KEY` env
|
||
|
||
### 選擇由 config 決定
|
||
|
||
```yaml
|
||
storage:
|
||
backend: localfs # or "s3"
|
||
localfs:
|
||
root: ./data/storage
|
||
base_url: http://localhost:3001/storage # api-server 代理路徑
|
||
s3:
|
||
endpoint: "" # 空字串用 AWS 原生;填 MinIO / R2 等用自訂
|
||
bucket: visiona-models
|
||
region: us-east-1
|
||
access_key: ${S3_ACCESS_KEY}
|
||
secret_key: ${S3_SECRET_KEY}
|
||
```
|
||
|
||
## 考慮過的替代方案
|
||
|
||
| 方案 | 優點 | 缺點 | 排除原因 |
|
||
|------|------|------|---------|
|
||
| **直接用 AWS SDK 呼叫 S3** | 簡單 | 綁定 AWS;MinIO / R2 要 workaround | 綁定單一雲 |
|
||
| **用 MinIO SDK** | 輕量 | MinIO SDK 寫的 code 去接 AWS S3 要微調;綁定特定 SDK | 換雲成本 |
|
||
| **自己做儲存服務** | 完全掌控 | 重新發明輪子;費時;達不到 S3 等級的持久性 | 不值得 |
|
||
| **資料庫存 blob** | 交易一致性 | DB 不適合儲存大檔;昂貴 | 技術錯配 |
|
||
| **只用 LocalFS 不抽象** | 簡單 | 未來替換 = 重寫所有 model handler | 違反抽象原則 |
|
||
|
||
## 後果 (Consequences)
|
||
|
||
### 正面影響
|
||
|
||
- **雛形零 setup**:`docker-compose up` 直接跑,不需 S3 / MinIO
|
||
- **切雲端零改動**:換 config 的 `backend: s3` 即可
|
||
- **支援多家雲**:endpoint 可指向 AWS / Cloudflare R2 / Backblaze B2 / MinIO / 自建 Ceph 等
|
||
- **介面 S3-native**:未來加 multipart upload、server-side encryption 等 S3 進階特性無需變介面
|
||
|
||
### 負面影響(接受的取捨)
|
||
|
||
- **`PresignedGetURL` 在 LocalFS 是假的**:需 api-server 提供 `/storage/...` 代理端點。這段代理程式碼要寫,但不複雜
|
||
- **LocalFS 不支援多節點**:雛形階段只有單節點,可接受;Phase 1 遷到 S3 後自動解決
|
||
- **Interface 比純 SDK 包裝更抽象**:略多一層呼叫,效能無感(本地 disk IO 遠大於 interface method dispatch)
|
||
|
||
### 風險
|
||
|
||
- **LocalFS 的 metadata 靠 sidecar `.meta.json`**:不是原子的;雛形忽略;Phase 1 改 S3 自然原子
|
||
- **PresignedPutURL 在 LocalFS 實作要處理 CSRF / auth**:雛形可暫時接受裸上傳;Phase 1 上 S3 後由 S3 presigned signature 保護
|
||
- **大檔上傳 OOM**:`Put` 使用 `io.Reader` 已是 streaming,LocalFS 實作用 `io.Copy` 避免 buffer 整個 body;需要 code review 把關
|
||
|
||
## 合規性
|
||
|
||
- [x] Architect 確認
|
||
- [ ] 雛形測試:確保 `LocalFSStore` 通過 interface conformance tests
|
||
- [ ] Phase 1:確保 `S3Store` 通過相同 tests
|
||
- [ ] 加入 `.gitignore`:`data/storage/` 不進 Git
|
||
|
||
## 相關文件
|
||
- Design Doc §3.3(外部依賴)
|
||
- TDD §8(儲存層介面)
|
||
- 相關 ADR:ADR-005(雛形不接 DB / Auth)
|