Three proposals addressing the 2026-02-25 trace pipeline OOM crash (114K CIDs): 1. trace-events-memory-triage: fetchmany iterator (read_sql_df_slow_iter), admission control (50K CID limit for non-MSD), cache skip for large queries, early memory release with gc.collect() 2. trace-async-job-queue: RQ-based async jobs for queries >20K CIDs, separate worker process with isolated memory, frontend polling via useTraceProgress composable, systemd service + deploy scripts 3. trace-streaming-response: chunked Redis storage (TRACE_STREAM_BATCH_SIZE=5000), NDJSON stream endpoint (GET /api/trace/job/<id>/stream), frontend ReadableStream consumer for progressive rendering, backward-compatible with legacy single-key storage All three proposals archived. 1101 tests pass, frontend builds clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
5.3 KiB
Markdown
150 lines
5.3 KiB
Markdown
## Context
|
||
|
||
提案 1(trace-events-memory-triage)解決了峰值記憶體問題並加入 admission control,
|
||
但 CID > 50K 的查詢被直接拒絕(HTTP 413)。
|
||
使用者仍有合理需求查詢大範圍資料(例如 TMTT 站 5 個月 = 114K CIDs)。
|
||
|
||
目前 codebase 完全沒有非同步任務基礎設施(無 Celery、RQ、Dramatiq)。
|
||
所有操作都是同步 request-response,受 gunicorn 360s timeout 硬限。
|
||
|
||
需要引入輕量級 job queue,讓大查詢在獨立 worker 進程中執行,
|
||
不佔 gunicorn thread、不受 360s timeout 限制、失敗可重試。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- CID > 閾值的 trace events 查詢改走非同步 job(API 回 202 + job_id)
|
||
- 獨立 worker 進程(systemd unit),不佔 gunicorn 資源
|
||
- Job 狀態可查詢(queued/running/completed/failed)
|
||
- 結果有 TTL 自動清理,不佔 Redis 長期記憶體
|
||
- 前端自動判斷同步/非同步路徑,顯示 job 進度
|
||
- 最小新依賴(利用既有 Redis)
|
||
|
||
**Non-Goals:**
|
||
- 不做通用 task queue(只處理 trace events)
|
||
- 不做 job 重試(大查詢重試消耗巨大,失敗後使用者手動重新觸發)
|
||
- 不做 job 取消(Oracle 查詢一旦發出難以取消)
|
||
- 不做 job 持久化到 DB(Redis TTL 足夠)
|
||
- 不修改 lineage 階段(仍然同步,通常 < 120s)
|
||
|
||
## Decisions
|
||
|
||
### D1: RQ(Redis Queue)而非 Celery/Dramatiq
|
||
|
||
**決策**:使用 RQ 作為 job queue。
|
||
|
||
**理由**:
|
||
- 專案已有 Redis,零額外基礎設施
|
||
- RQ 比 Celery 輕量 10 倍(無 broker 中間層、無 beat scheduler、無 flower)
|
||
- RQ worker 是獨立 Python 進程,記憶體隔離
|
||
- API 簡單:`queue.enqueue(func, args, job_timeout=600, result_ttl=3600)`
|
||
- 社群活躍,Flask 生態整合良好
|
||
|
||
**替代方案**:
|
||
- Celery:過重,專案不需要 beat、chord、chain 等功能 → rejected
|
||
- Dramatiq:更輕量但社群較小,Redis broker 整合不如 RQ 成熟 → rejected
|
||
- 自製 threading:前面討論已排除(worker 生命週期、記憶體競爭)→ rejected
|
||
|
||
### D2: 同步/非同步分界閾值
|
||
|
||
**決策**:
|
||
|
||
| CID 數量 | 行為 |
|
||
|-----------|------|
|
||
| ≤ 20,000 | 同步處理(現有 events endpoint) |
|
||
| 20,001 ~ 50,000 | 非同步 job(回 202 + job_id) |
|
||
| > 50,000 | 非同步 job(回 202 + job_id),worker 內部分段處理 |
|
||
|
||
**env var**:`TRACE_ASYNC_CID_THRESHOLD`(預設 20000)
|
||
|
||
**理由**:
|
||
- ≤ 20K CIDs 的 events 查詢通常在 60s 內完成,同步足夠
|
||
- 20K-50K 需要 2-5 分鐘,超出使用者耐心且佔住 gunicorn thread
|
||
- > 50K 是提案 1 的 admission control 上限,必須走非同步
|
||
|
||
**提案 1 的 HTTP 413 改為 HTTP 202**:
|
||
當提案 2 實作完成後,提案 1 的 `TRACE_EVENTS_CID_LIMIT` 檢查改為自動 fallback 到 async job,
|
||
不再拒絕請求。
|
||
|
||
### D3: Job 狀態與結果儲存
|
||
|
||
**決策**:使用 RQ 內建的 job 狀態追蹤(儲存在 Redis)。
|
||
|
||
```
|
||
Job lifecycle:
|
||
queued → started → finished / failed
|
||
|
||
Redis keys:
|
||
rq:job:{job_id} # RQ 內建 job metadata
|
||
trace:job:{job_id}:meta # 自訂 metadata(profile, cid_count, domains, progress)
|
||
trace:job:{job_id}:result # 完成後的結果(JSON,設 TTL)
|
||
```
|
||
|
||
**env vars**:
|
||
- `TRACE_JOB_TTL_SECONDS`:結果保留時間(預設 3600 = 1 小時)
|
||
- `TRACE_JOB_TIMEOUT_SECONDS`:單一 job 最大執行時間(預設 1800 = 30 分鐘)
|
||
|
||
### D4: API 設計
|
||
|
||
```
|
||
POST /api/trace/events ← 現有,CID ≤ 閾值時同步
|
||
POST /api/trace/events ← CID > 閾值時回 202 + job_id(同一 endpoint)
|
||
GET /api/trace/job/{job_id} ← 查詢 job 狀態
|
||
GET /api/trace/job/{job_id}/result ← 取得完整結果
|
||
GET /api/trace/job/{job_id}/result?domain=history&offset=0&limit=5000 ← 分頁取結果
|
||
```
|
||
|
||
**202 回應格式**:
|
||
```json
|
||
{
|
||
"stage": "events",
|
||
"async": true,
|
||
"job_id": "trace-evt-abc123",
|
||
"status_url": "/api/trace/job/trace-evt-abc123",
|
||
"estimated_seconds": 300
|
||
}
|
||
```
|
||
|
||
### D5: Worker 部署架構
|
||
|
||
```
|
||
systemd (mes-dashboard-trace-worker.service)
|
||
→ conda run -n mes-dashboard rq worker trace-events --with-scheduler
|
||
→ 獨立進程,獨立記憶體空間
|
||
→ MemoryMax=4G(cgroup 保護)
|
||
```
|
||
|
||
**env vars**:
|
||
- `TRACE_WORKER_COUNT`:worker 進程數(預設 1)
|
||
- `TRACE_WORKER_QUEUE`:queue 名稱(預設 `trace-events`)
|
||
|
||
### D6: 前端整合
|
||
|
||
`useTraceProgress.js` 修改:
|
||
|
||
```javascript
|
||
// events 階段
|
||
const eventsResp = await fetchStage('events', payload)
|
||
if (eventsResp.status === 202) {
|
||
// 非同步路徑
|
||
const { job_id, status_url } = eventsResp.data
|
||
return await pollJobUntilComplete(status_url, {
|
||
onProgress: (status) => updateProgress('events', status.progress),
|
||
pollInterval: 3000,
|
||
maxPollTime: 1800000, // 30 分鐘
|
||
})
|
||
}
|
||
// 同步路徑(現有)
|
||
return eventsResp.data
|
||
```
|
||
|
||
## Risks / Trade-offs
|
||
|
||
| 風險 | 緩解措施 |
|
||
|------|---------|
|
||
| RQ 新依賴增加維護成本 | RQ 穩定、API 簡單、只用核心功能 |
|
||
| Worker 進程增加記憶體使用 | 獨立 cgroup MemoryMax=4G;空閒時幾乎不佔記憶體 |
|
||
| Redis 儲存大結果影響效能 | 結果 TTL=1h 自動清理;配合提案 3 串流取代全量儲存 |
|
||
| Worker crash 丟失進行中 job | RQ 內建 failed job registry;使用者可手動重觸發 |
|
||
| 前端輪詢增加 API 負載 | pollInterval=3s,只有 active job 才輪詢 |
|