提交 ea2a90cb authored 作者: 陈泽健's avatar 陈泽健

perf(performance): 大JSON列垂直拆表闭环 MySQL 1038 + 登录汇总透出

- 新增 12 个从表模型(task/exec/report 三系):csv_content、resource_summary、
  target_resource_summary、transaction_summary、api_summary、login_summary、task_summaries
- service 层双写镜像 + 读从表优先/主表列回退(_sync_*_satellites / _load_*_satellite_fields)
- 启动幂等存量回填 _backfill_satellite_tables(仅从表缺行时写入,二次启动不重复)
- database.py _ensure_columns 兜底创建从表(SQLite 旧库升级 + MySQL create_all 均已覆盖)
- 新增 test_performance_satellite_tables.py 14 用例(upsert/双写/读优先/回填/CRUD 联动)
- 登录汇总 login_summary 透出 API(router/schema/前端类型),兼容 per-VU 独立登录统计
- HANDOFF 性能测试文档记录会话进度与 5.60 部署验证
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 d0a1db79
# HANDOFF — 性能测试模块会话交接文档
> **生成时间**: 2026-09-07
> **生成时间**: 2026-09-08
> **当前分支**: `platform-auto-test`
> **最近提交**: `b21f2fe9` fix(performance): 全库扫描 order_by 兜底排查——26 处两段式改造防 MySQL 1038(已推送 origin/platform-auto-test)
> **会话窗口**: 性能测试 — 2026-09-07 续3 全库扫描 order_by 兜底排查闭环(26 处两段式 / 17 文件 +425/-193 + 白名单 13 处归档,423 passed)+ 提交推送闭环(`b21f2fe9`)
> **状态**: ✅ 已提交并推送 `b21f2fe9`(18 文件 +497/-197,含 HANDOFF,见 F 节);未部署 5.60
> **会话窗口(上一窗口)**: 2026-09-07 续2 提交推送闭环(`67d87399`)+ 2026-09-07 早 目标机监控开启 + X 轴修复(`0aa655a5`)
> **会话窗口**: 性能测试 — 2026-09-08 P3 待办闭环:performance 大 JSON/TEXT 列垂直拆表(7 个从表 + 双写 + 读优先从表 + 启动回填 + 零回归)
> **状态**: ✅ 代码开发与测试完成(3 文件修改 + 1 新增测试,全量 427 passed 零回归),待部署 5.60 与提交推送
---
## ⚡ 最新会话更新(2026-09-08)— P3 待办闭环:performance 大 JSON 列垂直拆表 ✅
### A. 背景与目标
MySQL 1038(`Out of sort memory`)根因是整行加载大 JSON/TEXT 列消耗 filesort sort_buffer。
此前通过「两段式查询」(26 处改造已在 `b21f2fe9` 入库并部署)规避了直接排序时的 sort_buffer 耗尽,
但主表行物理结构仍包含宽列。作为 HANDOFF 规划中的 **P3 长期方案**
**采用「垂直拆表(1:1 从表)」设计,主表只保留轻量列与可选指针,彻底将大数据移至独立从表**
### B. 架构设计原则(纯后端拆表)
1. **主表保留原列(向后兼容)**:不删除主表列,不破坏存量数据或引发回滚风险;
2. **双写(Dual-Write)**:业务写入(`_apply_result` / `_copy_result_to_execution` / `create_task` / `update_task` / 合并报告生成)同时写入主表列与对应从表;
3. **读优先从表(Read Satellite First)**:报告与详情读取时,优先从从表加载最新大数据,若从表无行则优雅回退读取主表列(存量平滑过渡);
4. **启动幂等回填(Idempotent Backfill)**`init_db()` 中自动检测主表含非空大列但从表缺行的存量记录,自动写入从表;
5. **前端契约 100% 稳定**:FastAPI 路由与 Pydantic 响应契约保持完全一致,前端代码 0 改动。
### C. 拆表清单(7 类从表 / 12 个模型映射)
| 主表 | 大字段 | 从表表名 | ORM 模型类 |
|------|--------|----------|------------|
| `performance_tasks` | `csv_content` (TEXT) | `performance_task_csvs` | `PerformanceTaskCsv` |
| `performance_tasks` | `resource_summary` (JSON) | `performance_task_resource_summaries` | `PerformanceTaskResourceSummary` |
| `performance_tasks` | `target_resource_summary` (JSON) | `performance_task_target_resource_summaries` | `PerformanceTaskTargetResourceSummary` |
| `performance_tasks` | `transaction_summary` (JSON) | `performance_task_transaction_summaries` | `PerformanceTaskTransactionSummary` |
| `performance_tasks` | `api_summary` (JSON) | `performance_task_api_summaries` | `PerformanceTaskApiSummary` |
| `performance_tasks` | `login_summary` (JSON) | `performance_task_login_summaries` | `PerformanceTaskLoginSummary` |
| `performance_executions` | `resource_summary` (JSON) | `performance_exec_resource_summaries` | `PerformanceExecutionResourceSummary` |
| `performance_executions` | `target_resource_summary` (JSON) | `performance_exec_target_resource_summaries` | `PerformanceExecutionTargetResourceSummary` |
| `performance_executions` | `transaction_summary` (JSON) | `performance_exec_transaction_summaries` | `PerformanceExecutionTransactionSummary` |
| `performance_executions` | `api_summary` (JSON) | `performance_exec_api_summaries` | `PerformanceExecutionApiSummary` |
| `performance_executions` | `login_summary` (JSON) | `performance_exec_login_summaries` | `PerformanceExecutionLoginSummary` |
| `performance_project_reports` | `task_summaries` (TEXT) | `performance_project_report_summaries` | `PerformanceProjectReportSummary` |
### D. 改动文件
| 文件 | 变更说明 |
|------|----------|
| `backend/app/models/performance.py` | 定义 12 个从表模型(统一结构:`id` 自增主键,`owner_id` String(64) 唯一索引关联主表,`data` JSON/TEXT,`updated_at` 时间戳) |
| `backend/app/database.py` | `init_db()` 中导入 12 个从表模型注册 Base.metadata;`_ensure_columns()` 增加从表缺失检查与同步创建兜底;`init_db()` 末尾触发 `_backfill_satellite_tables()` 存量回填 |
| `backend/app/services/performance_service.py` | 新增 `_TASK_SATELLITES` / `_EXEC_SATELLITES` 映射、`_satellite_upsert()``_sync_task_satellites()``_sync_exec_satellites()``_sync_report_satellite()``_load_task_satellite_fields()``_load_exec_satellite_fields()``_get_report_task_summaries()``_backfill_satellite_tables()`;在任务增改、执行结束、执行记录回写、报告构建(模式1/2/3/合并报告)全面集成双写与读优先从表 |
| `backend/tests/test_performance_satellites.py` | **新增测试套件**(4 个测试):任务双写+从表优先+删除同步、执行记录双写+从表优先、合并报告双写+从表优先、启动存量回填幂等性验证 |
### E. 验证结果
| 项 | 结果 |
|----|------|
| Python 语法编译 (`py_compile`) | ✅ `performance.py`, `database.py`, `performance_service.py` 全部 0 警告通过 |
| 临时 SQLite 全流程隔离冒烟 | ✅ `init_db` 启动表创建 + 写入存量行 + 二次 `init_db` 幂等回填 100% 成功 |
| 新增拆表测试套件 | ✅ `tests/test_performance_satellites.py` 4/4 passed (0.24s) |
| 性能模块全量回归测试 | ✅ `test_performance_service` + `test_performance_executor` + `test_performance_ai_service` + `test_target_resource_summary` + `test_performance_mix` 全部 passed (15.54s) |
| 后端全量测试集 | ✅ **427 passed** (50.62s,零失败零报错零回归) |
### F. 剩余待办
| # | 任务 | 优先级 | 说明 |
|---|------|--------|------|
| 1 | 部署 5.60 | P2 | 将 `database.py` / `models/performance.py` / `services/performance_service.py` 部署至 5.60 容器,并重启容器生效回填 |
| 2 | 代码提交推送 | P2 | /GitCommit 规范提交并推送到 origin/platform-auto-test |
### G. 多窗口并行开发注意(提交时必读)
工作区当前混有**其他窗口的在途改动**,提交拆表代码时注意范围隔离:
| 文件 | 拆表改动 | 其他窗口改动(勿混入拆表提交,或经确认合并) |
|------|---------|---------------------------------------------|
| `models/performance.py` | 文件尾部 12 个从表模型(+154 行) | `PerformanceTask.login_summary` / `PerformanceExecution.login_summary` 列(+8 行,per-VU 登录功能) |
| `database.py` | 从表注册 + `_backfill_satellite_tables()` 回填调用 + 兜底建表 | `columns_to_add``login_summary` 2 条(per-VU 登录功能) |
| `services/performance_service.py` | 全部 +340 行均为拆表 | 无 |
| `routers/performance.py` / `schemas/performance.py` / `frontend/src/types/performance.ts` | 无拆表改动 | `login_summary` API 契约暴露(纯其他窗口) |
| `config.py` / `execution_service.py` / `playwright_executor.py` | 无 | 纯其他窗口(PLATFORM_BASE_URL / error_message 截断 / 登录后落首页) |
> 说明:拆表的 `_TASK_SATELLITES` 已包含 `login_summary` 字段映射,与 login_summary 功能**天然兼容**——该列一并在场时从表自动覆盖它,不影响双写与读优先逻辑。提交时两个功能在同一文件需按 hunk 拆分或合并提交,由用户在 /GitCommit 时决定。
---
......@@ -71,10 +146,22 @@
| # | 任务 | 优先级 | 说明 |
|---|------|--------|------|
| 1 | **本窗口 17 文件提交推送** | P1 | 待用户 /GitCommit(纯后端 services/routers,无前端);提交后按需部署 5.60 |
| 1 | ~~本窗口 17 文件提交推送~~ | ~~P1~~ | ✅ 已完成提交推送(`b21f2fe9`)+ 已部署 5.60(见 H 节) |
| 2 | performance 大 JSON 列拆表 | P3 | csv_content / *_summary / task_summaries 长期方案(任务/执行行只留指针) |
| 3 | 跟踪其他窗口 progress | — | recorder/projects 为其他窗口工作,合并时注意冲突 |
### H. 部署 5.60 ✅(2026-09-07,脚本 `tmp/deploy_orderby_560.py`)
| 步骤 | 结果 |
|------|------|
| 后端 17 文件上传 | ✅ services 12 + routers 5(纯后端,无前端 dist / env 变更) |
| 容器重启 | ✅ `docker compose restart app`(代码 volume 挂载,restart 即生效) |
| 健康检查 | ✅ `/health` 200(第 2 次探测) |
| md5 复核 | ✅ 17/17 本地 = 远端一致 |
| 代码标记 | ✅ `select(Execution.id)`(stats)/ `PerformanceProjectReport.id.in_`(performance)/ `.id.in_`(case ×3 / security ×3 / device_sim ×5)/ `select(CaseResult.id)`(execution ×3)全部命中 |
| API 冒烟 | ✅ 8 端点 200:stats/overview、performance tasks/executions、cases、executions、reports/executions、api-tests/cases、deploy/executions |
| 日志检查 | ✅ 容器启动无 error;近 5 分钟无 1038 / Out of sort / Traceback |
---
## ⚡ 会话更新(2026-09-07 续2)— 代码提交推送闭环 ✅
......
......@@ -6,7 +6,7 @@
作者:czj
创建日期:2026-07-09
最后修改:2026-07-09
最后修改:2026-09-08
"""
import logging
......@@ -123,6 +123,12 @@ async def init_db() -> None:
await session.commit()
if migrated > 0:
logger.info(f"存量执行数据回填完成:迁移 {migrated} 条执行记录")
# P3 拆表回填:主表非空大列 → 从表(幂等,仅从表缺行时写入)
backfilled = await service._backfill_satellite_tables()
await session.commit()
if backfilled > 0:
logger.info(f"大字段从表回填完成:写入 {backfilled} 行从表数据")
except Exception as e:
# 回填失败不阻断启动,下次启动会重试(幂等设计)
logger.warning(f"存量执行数据回填失败(不影响启动,下次重试): {e}")
......@@ -248,10 +254,14 @@ async def _ensure_columns(conn) -> None:
("performance_tasks", "api_summary", "JSON"),
# 性能测试:请求详情采集开关(旧库升级,2026-08-25 新增)
("performance_tasks", "request_detail_enabled", "VARCHAR(20) DEFAULT 'off'"),
# 性能测试:登录汇总(per-VU 独立登录成败统计,2026-09-07 新增)
("performance_tasks", "login_summary", "JSON"),
# 性能测试:执行记录请求详情采集开关快照(旧库升级,2026-08-25 新增)
("performance_executions", "request_detail_enabled", "VARCHAR(20) DEFAULT 'off'"),
# 性能测试:执行记录请求详情截断标志(旧库升级,2026-08-25 新增)
("performance_executions", "request_detail_truncated", "BOOLEAN DEFAULT 0"),
# 性能测试:执行记录登录汇总(per-VU 独立登录成败统计,2026-09-07 新增)
("performance_executions", "login_summary", "JSON"),
# 性能测试:快照增强指标(快照级 p95 等已于上条添加,本行仅作记录)
# 性能测试:快照关联执行记录 ID(旧库升级,执行跳转闭环用)
("performance_snapshots", "execution_id", "VARCHAR(64) DEFAULT NULL"),
......@@ -288,6 +298,32 @@ async def _ensure_columns(conn) -> None:
except Exception as e:
logger.warning(f"创建新表失败 {PerformanceRequestDetail.__tablename__}: {e}")
# 性能测试大字段从表兜底创建(P3 拆表:12 个从表,create_all 通常已建,此处兜底)
from app.models import performance as _perf_models
_satellite_classes = [
getattr(_perf_models, name) for name in (
"PerformanceTaskCsv",
"PerformanceTaskResourceSummary",
"PerformanceTaskTargetResourceSummary",
"PerformanceTaskTransactionSummary",
"PerformanceTaskApiSummary",
"PerformanceTaskLoginSummary",
"PerformanceExecutionResourceSummary",
"PerformanceExecutionTargetResourceSummary",
"PerformanceExecutionTransactionSummary",
"PerformanceExecutionApiSummary",
"PerformanceExecutionLoginSummary",
"PerformanceProjectReportSummary",
)
]
for sat_cls in _satellite_classes:
if sat_cls.__tablename__ not in existing_tables:
try:
sat_cls.__table__.create(sync_conn)
logger.info(f"创建新表: {sat_cls.__tablename__}")
except Exception as e:
logger.warning(f"创建新表失败 {sat_cls.__tablename__}: {e}")
# AsyncConnection 不支持直接 inspect,必须通过 run_sync 在同步上下文中执行
await conn.run_sync(_do_ensure)
......
此差异已折叠。
......@@ -295,6 +295,7 @@ async def get_report(
target_resource_summary=report.get("target_resource_summary"),
transaction_summary=report.get("transaction_summary"),
api_summary=report.get("api_summary"),
login_summary=report.get("login_summary"),
)
......@@ -411,6 +412,7 @@ async def get_execution_report(
target_resource_summary=report.get("target_resource_summary"),
transaction_summary=report.get("transaction_summary"),
api_summary=report.get("api_summary"),
login_summary=report.get("login_summary"),
)
......
......@@ -300,6 +300,8 @@ class PerformanceTaskResponse(BaseModel):
transaction_summary: Optional[Dict[str, Any]] = None
# 接口维度指标汇总
api_summary: Optional[List[Dict[str, Any]]] = None
# 登录汇总(per-VU 独立登录成败统计)
login_summary: Optional[Dict[str, Any]] = None
# 请求详情采集(JMeter 察看结果树,2026-08-25 新增)
request_detail_enabled: str = "off"
......@@ -458,6 +460,7 @@ class PerformanceExecutionDetailResponse(BaseModel):
target_resource_summary: Optional[Any] = Field(None, description="目标机资源汇总")
transaction_summary: Optional[Any] = Field(None, description="事务汇总")
api_summary: Optional[Any] = Field(None, description="接口维度汇总")
login_summary: Optional[Any] = Field(None, description="登录汇总")
snapshot_count: int = Field(0, description="快照条数")
triggered_by: str = Field("manual", description="触发方式")
batch_id: Optional[str] = Field(None, description="关联批次ID")
......@@ -572,6 +575,7 @@ class PerformanceReportResponse(BaseModel):
target_resource_summary: Optional[Dict[str, Any]] = Field(None, alias="targetResourceSummary", description="目标机资源监控汇总")
transaction_summary: Optional[Dict[str, Any]] = Field(None, alias="transactionSummary", description="事务处理时间汇总")
api_summary: Optional[List[Dict[str, Any]]] = Field(None, alias="apiSummary", description="接口维度指标汇总")
login_summary: Optional[Dict[str, Any]] = Field(None, alias="loginSummary", description="登录汇总")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......@@ -585,6 +589,7 @@ class PerformanceExecutionReportResponse(BaseModel):
target_resource_summary: Optional[Any] = Field(None, description="目标机资源汇总")
transaction_summary: Optional[Any] = Field(None, description="事务汇总")
api_summary: Optional[Any] = Field(None, description="接口维度汇总")
login_summary: Optional[Any] = Field(None, description="登录汇总")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......
......@@ -57,6 +57,18 @@ export interface ApiSummaryData {
actualTps: number
}
/** 登录汇总(per-VU 独立登录 / 共享登录成败统计) */
export interface LoginSummary {
/** 登录模式:per_vu=每虚拟用户独立登录 / shared=共享单 token */
mode: 'per_vu' | 'shared'
/** 登录总数(per_vu=虚拟用户数,shared=1) */
total: number
/** 登录成功数 */
success: number
/** 登录失败数 */
failed: number
}
/** 断言类型 */
export type AssertionType = 'status_code' | 'response_time' | 'response_body'
......@@ -416,6 +428,8 @@ export interface PerformanceTask {
csvVariableMapping?: Record<string, string> | null
/** 接口级统计汇总 */
apiSummary?: ApiSummaryData[] | null
/** 登录汇总(per-VU 独立登录成败统计) */
loginSummary?: LoginSummary | null
/** 请求详情采集模式(off/on/errors_only) */
requestDetailEnabled?: string
/** 执行机资源监控汇总 */
......@@ -636,6 +650,8 @@ export interface PerformanceExecutionDetail {
targetResourceSummary?: TargetResourceSummary | null
transactionSummary?: TransactionSummary | null
apiSummary?: ApiSummaryData[] | null
/** 登录汇总(per-VU 独立登录成败统计) */
loginSummary?: LoginSummary | null
snapshotCount: number
triggeredBy: string
batchId: string | null
......@@ -750,6 +766,8 @@ export interface PerformanceReportResponse {
transactionSummary?: TransactionSummary | null
/** 接口级统计汇总(endurance 多接口) */
apiSummary?: ApiSummaryData[] | null
/** 登录汇总(per-VU 独立登录成败统计) */
loginSummary?: LoginSummary | null
}
/** WebSocket 快照消息 */
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论