100 lines
3.6 KiB
Markdown
100 lines
3.6 KiB
Markdown
# DataSources 列表接口性能优化
|
||
|
||
## 背景
|
||
|
||
`GET /api/v1/datasources` 是数据源管理页面的核心接口,响应慢会直接阻塞页面渲染。
|
||
|
||
## 优化前的查询链路
|
||
|
||
`_load_datasource_list_context` 按顺序执行以下查询:
|
||
|
||
| 序号 | 函数 | 查询内容 | 瓶颈 |
|
||
|------|------|---------|------|
|
||
| 1 | `_load_latest_running_tasks` | collection_tasks 窗口函数,stale check 依赖此结果 | 必须串行 |
|
||
| 2 | `_load_latest_completed_tasks` | collection_tasks 窗口函数(最近完成任务) | 串行等待 |
|
||
| 3 | `_load_datasource_data_counts` | `COUNT(*) GROUP BY source` on collected_data | **最慢,全表扫描** |
|
||
| 4 | `_load_datasource_endpoint_overrides` | datasource_configs 简单 SELECT | 串行等待 |
|
||
|
||
## 第一阶段:并行化
|
||
|
||
将 2/3/4 三个互不依赖的查询改为 `asyncio.gather` + 独立 session 并行执行:
|
||
|
||
```python
|
||
async def _fetch_completed():
|
||
async with async_session_factory() as s:
|
||
return await _load_latest_completed_tasks(s, datasource_ids)
|
||
|
||
async def _fetch_counts():
|
||
async with async_session_factory() as s:
|
||
return await _load_datasource_data_counts(s, sources)
|
||
|
||
async def _fetch_overrides():
|
||
async with async_session_factory() as s:
|
||
return await _load_datasource_endpoint_overrides(s, sources)
|
||
|
||
completed_tasks, data_counts, endpoint_overrides = await asyncio.gather(
|
||
_fetch_completed(), _fetch_counts(), _fetch_overrides(),
|
||
)
|
||
```
|
||
|
||
> **注意**:SQLAlchemy `AsyncSession` 不支持在同一 session 上并发,每个协程必须独立开 session。
|
||
|
||
## 第二阶段:删除重量级查询
|
||
|
||
### 删除 `_load_datasource_data_counts`
|
||
|
||
`data_count` 字段仅用于前端在"最近采集"列显示 `(0条)` 的边缘提示,不值得为此维持一次 `COUNT(*) GROUP BY` 全表扫描。
|
||
|
||
- 前端同步移除 `(0条)` 显示逻辑
|
||
- 移除 `BuiltInDataSource` 接口中的 `data_count` 字段
|
||
|
||
### 删除 `_load_latest_completed_tasks`
|
||
|
||
`last_status` 和 `last_run_at` 已由 collector 在任务完成时直接更新到 `DataSource` 模型字段,不需要再 JOIN collection_tasks 获取:
|
||
|
||
```python
|
||
# 优化前:需要查 completed_tasks
|
||
last_run_at = datasource.last_run_at or (last_task.completed_at if last_task else None)
|
||
last_status = datasource.last_status or (last_task.status if last_task else None)
|
||
|
||
# 优化后:直接读模型字段
|
||
last_run_at = datasource.last_run_at
|
||
last_status = datasource.last_status
|
||
```
|
||
|
||
同步移除 `last_records_processed` 字段(来源是 completed_tasks,列表不显示此字段)。
|
||
|
||
## 优化后的查询链路
|
||
|
||
```
|
||
datasources SELECT → 主数据,必须
|
||
_load_latest_running_tasks → 必须(进行中状态 + stale check)
|
||
_load_datasource_endpoint_overrides → 必须(endpoint 覆盖,列表详情和采集器设置需要显示当前有效地址)
|
||
```
|
||
|
||
3 个查询(原来 5 个),后两个顺序执行(running tasks 先完成用于 stale check,endpoint overrides 轻量)。
|
||
|
||
## 前端 triggerDatasource 双调修复
|
||
|
||
`triggerDatasource` 中存在双重 `fetchData()` 调用:
|
||
|
||
```typescript
|
||
// 修复前
|
||
} else {
|
||
window.setTimeout(() => { fetchData() }, 800) // 无 task_id 时延迟刷
|
||
}
|
||
fetchData() // 总是立即刷 → 与上面的延迟刷重叠
|
||
|
||
// 修复后(二者互斥)
|
||
if (res.data.task_id) {
|
||
fetchData() // 有 task_id:立即刷一次
|
||
} else {
|
||
window.setTimeout(fetchData, 800) // 无 task_id:等 800ms 再刷一次
|
||
}
|
||
```
|
||
|
||
## 相关文件
|
||
|
||
- `backend/app/api/v1/datasources.py` — `_load_datasource_list_context`、`list_datasources`
|
||
- `frontend/src/admin/pages/PlainResourcePages.tsx` — `BuiltInDataSource` interface、`triggerDatasource`
|