84 lines
6.4 KiB
Markdown
84 lines
6.4 KiB
Markdown
## Why
|
||
|
||
目前各歷史報表服務(reject-history、hold-history、resource-history)、查詢工具(query-tool)、中段不良分析(mid-section-defect)和 Job 查詢(job-query)各自實作不同的批次查詢、快取和並行執行模式,缺乏統一編排與保護。主要問題:
|
||
|
||
1. **Oracle 超時**:長日期範圍(365+ 天)或大量 Container ID(工單展開後可達數千筆)的查詢可能超過 300 秒 call_timeout
|
||
2. **OOM 風險**:reject/hold dataset cache 以 `limit: 999999999` 取回全部資料,無記憶體上限守衛
|
||
3. **保護分散**:`EventFetcher` 已有 ID 分批 + 快取,但 reject/hold/resource dataset cache 仍各自維護查詢與快取策略
|
||
4. **重複程式碼**:3 個 dataset cache 各自複製相同的 parquet-in-Redis 序列化邏輯
|
||
5. **ID 展開膨脹**:工單 resolve 後 container ID 可能大量擴張,缺乏跨服務一致的分批/合併流程
|
||
6. **重查成本高**:延長查詢範圍(例如 1-6 月改 1-8 月)無法有效重用已查區段結果
|
||
7. **query-tool 超時風險高**:多數查詢仍走 `read_sql_df`(主 pool / 55s timeout),大查詢下容易超時
|
||
|
||
需要一個**可穩定複用的查詢引擎模組**,任何服務接入後自動獲得分解、快取、記憶體保護和超時保護。
|
||
|
||
## What Changes
|
||
|
||
- 新增 `BatchQueryEngine` 共用模組,提供:
|
||
- **時間範圍分解**:長日期 → ~31 天月份區間,每段獨立查詢
|
||
- **時間分解語意**:明確定義 chunk 邊界(閉區間)、跨月切割與最後一段不足月行為
|
||
- **ID 批次分解**:大量 ID(工單/Lot/GD Lot/流水批展開後)→ 1000 筆一批
|
||
- **query_hash 規格**:統一 canonicalization 與雜湊欄位,確保 chunk/cache key 穩定
|
||
- **記憶體守衛**:每個 chunk 結果檢查 `DataFrame.memory_usage()`,超過閾值時中止並警告
|
||
- **結果筆數限制**:可配置的最大結果筆數,超過時截斷並標記
|
||
- **受控並行執行**:預設循序、可選並行,嚴格遵守 slow query semaphore
|
||
- **Redis 分塊快取**:每個 chunk 獨立快取,支援部分命中(延長查詢範圍時複用已查過的區間)
|
||
- **快取層互動**:明確定義 chunk cache 與服務既有 L1/L2 dataset cache 的讀寫順序
|
||
- **進度追蹤**:Redis HSET 記錄進度,可供前端顯示
|
||
- 新增「**大結果落地層(Parquet spill)**」設計:
|
||
- 當長查詢結果超過記憶體/列數門檻時,將合併後結果以 Parquet 寫入本機持久目錄(例如 `tmp/query_spool/`)
|
||
- Redis 僅保存 metadata(query_id → parquet path / schema / rows / created_at / ttl)
|
||
- `/view` 與 `/export` 讀取流程優先走 Redis metadata + Parquet,避免整包 DataFrame 常駐 worker RAM
|
||
- 定時清理(TTL + 背景清理器)刪除過期 parquet,避免磁碟持續膨脹
|
||
- 新增 `redis_df_store` 共用模組,將 parquet-in-Redis 存取邏輯從 3 個 dataset cache 提取為共用工具
|
||
- 所有**引擎接管的 chunk 查詢**統一使用 slow 路徑(300 秒級 timeout)
|
||
- 使用既有「**獨立 SLOW POOL(小容量)**」做慢查詢連線重用
|
||
- 明確**不使用主查詢 pool** 承載慢查詢,避免拖垮一般 API
|
||
- 當 SLOW POOL 不可用時,降級為 slow direct connection(不影響主 pool)
|
||
|
||
## Capabilities
|
||
|
||
### New Capabilities
|
||
- `batch-query-engine`: 統一批次查詢引擎模組,涵蓋分解策略(時間/ID)、記憶體守衛、結果限制、受控執行、Redis 分塊快取、進度追蹤、結果合併
|
||
|
||
### Modified Capabilities
|
||
- `reject-history-api`: 主查詢改為透過引擎執行;date_range 模式自動時間分解,container 模式(工單/Lot/GD Lot 展開後)自動 ID 分批
|
||
- `hold-dataset-cache`: 主查詢改為透過引擎執行,長日期自動分解
|
||
- `resource-dataset-cache`: 主查詢改為透過引擎執行,長日期自動分解
|
||
- `event-fetcher-unified`: 保持既有最佳化(batch + streaming + cache),僅在需要統一監控/進度模型時再評估導入
|
||
|
||
## Impact
|
||
|
||
- **後端**:新增 2 個共用模組(`batch_query_engine.py`、`redis_df_store.py`),優先修改 3 個 dataset cache 主查詢路徑(reject/hold/resource)
|
||
- **受影響服務**(優先順序):
|
||
- P0:reject-history(最容易超時/OOM — 長日期 + 工單展開 + 目前 `limit=999999999`)
|
||
- P1:hold-history、resource-history(相同架構,直接套用)
|
||
- P2:mid-section-defect(4 階段管線,偵測查詢 + 上游歷史)、job-query(缺快取 + 日期分解)
|
||
- P3:query-tool(優先處理 `read_sql_df` 高風險路徑並導入慢查詢保護)、event-fetcher(保持可選)
|
||
- **資料庫**:不改 SQL,僅縮小每次查詢的 bind parameter 範圍
|
||
- **資料庫連線策略**:慢查詢與一般 pooled query 隔離,避免資源互相干擾
|
||
- **Redis**:新增 `batch:*` 前綴的分塊快取鍵
|
||
- **儲存層**:新增 Parquet 結果落地目錄與清理機制(Redis 轉為索引/metadata,不再承載全部大結果)
|
||
- **記憶體**:引擎強制單 chunk 記憶體上限(預設 256MB),超過時中止
|
||
- **可用性**:Redis 設定 `maxmemory` + eviction 後仍可透過 Parquet metadata 回復查詢結果(cache 不命中不等於資料遺失)
|
||
- **向下相容**:短查詢(< 60 天、< 1000 ID)走現有路徑,零額外開銷;既有 route/event 快取策略保持不變
|
||
- **前端**:可選性變更,長查詢可顯示進度條(非必要)
|
||
|
||
## Parquet 落地的預期效果與副作用
|
||
|
||
**預期效果:**
|
||
- 大幅降低 worker 在「merge + cache 回填」階段的峰值記憶體(避免單 worker 突增到 GB 級)
|
||
- Redis 記憶體由「存整包資料」轉為「存索引/熱資料」,降低 OOM 與 lock timeout 連鎖風險
|
||
- 服務重啟後,若 parquet 尚未過期,仍可恢復查詢結果(搭配 metadata)
|
||
|
||
**可能副作用(Side Effects):**
|
||
- 磁碟 I/O 增加:查詢高峰時會有 parquet 寫入/讀取尖峰
|
||
- 磁碟容量風險:清理策略失效時,spool 目錄可能持續膨脹
|
||
- 資料一致性風險:metadata 指向檔案若被外部刪除/損壞,會出現 stale pointer
|
||
- 安全與治理:落地檔案需納入權限控管、備份/清理與稽核策略
|
||
|
||
**緩解方向:**
|
||
- 強制 TTL + 定期掃描清理(以 metadata 與檔案 mtime 雙重判斷)
|
||
- 啟動時做 orphan/stale 檢查與自動修復(刪 metadata 或刪孤兒檔)
|
||
- 先以 reject-history 長查詢為 P0,逐步擴展到其他服務
|