Просмотр исходного кода

docs(s8): add database optimization plan

Document S8 scheduling and database write pressure mitigations after the login timeout incident.

Co-authored-by: Cursor <cursoragent@cursor.com>
skygu 2 дней назад
Родитель
Сommit
bcb0925c55
2 измененных файлов с 341 добавлено и 0 удалено
  1. 1 0
      doc/README.md
  2. 340 0
      doc/plan/S8/S8数据库处理优化方案.md

+ 1 - 0
doc/README.md

@@ -46,6 +46,7 @@
 | [审批流集成开发指南.md](./审批流集成开发指南.md) | `IFlowBizHandler`、`ApprovalPanel`、配置与 API |
 | [Windows后端WinSW守护重启方案.md](./Windows后端WinSW守护重启方案.md) | Windows 服务器上使用 WinSW 守护后端服务、自动重启与日志落盘 |
 | [plan/S1/S1-订单管理-审批流程实施方案.md](./plan/S1/S1-订单管理-审批流程实施方案.md) | S1 订单审批实施方案 |
+| [plan/S8/S8数据库处理优化方案.md](./plan/S8/S8数据库处理优化方案.md) | S8 调度、检测日志、通知扫描与数据库写入压力优化方案 |
 | [plan/数据库迁移/S1/S1-任务交接记忆.md](./plan/数据库迁移/S1/S1-任务交接记忆.md) | **S1 数据中台迁移**跨会话交接(当前进度、阻塞、验收 SQL) |
 | [plan/数据库迁移/S1/S1数据中台迁移实施计划.md](./plan/数据库迁移/S1/S1数据中台迁移实施计划.md) | S1 数据中台迁移主方案与分块步骤 |
 | [plan/数据中台模块扩展开发指南-S3范式.md](./plan/数据中台模块扩展开发指南-S3范式.md) | 数据中台扩展(仿 S3):四库逻辑、作业与 Cursor 协作 |

+ 340 - 0
doc/plan/S8/S8数据库处理优化方案.md

@@ -0,0 +1,340 @@
+# S8 数据库处理优化方案
+
+> 文档定位:S8 异常协同模块数据库写入、调度任务与运行稳定性优化方案
+> 创建日期:2026-05-28
+> 触发背景:超级管理员登录在 `CreateToken` 阶段超时,排查发现 MySQL 大量事务卡在 `waiting for handler commit`
+
+---
+
+## 一、问题背景
+
+2026-05-28 凌晨,超级管理员登录失败。后端日志显示账号密码校验已经通过,失败点发生在登录成功后的令牌创建阶段:
+
+- `SysAuthService.Login` 调用 `CreateToken`
+- `CreateToken` 更新 `SysUser.LastLoginTime`、`LastLoginIp` 等字段
+- MySQL 返回 `The Command Timeout expired before the operation completed`
+
+数据库侧同时出现大量 `waiting for handler commit`,涉及 `SysUser`、`SysJobTriggerRecord`、`SysJobDetail`、`ado_s8_watch_rule`、`ado_s8_notification_log`、`SysLogOp`、`SysLogEx` 等表。MySQL 还出现 `The table '/tmp/#sql...' is full`,说明数据库服务器临时目录、磁盘或提交刷盘链路已进入异常状态。
+
+本次故障不是超级管理员账号逻辑、审批流候选用户接口或前端登录逻辑导致;直接根因是数据库提交阶段阻塞,S8/审批/调度类任务的高频写入进一步放大了数据库压力。
+
+---
+
+## 二、现场证据
+
+### 2.1 登录失败点
+
+登录请求中,超级管理员账号已进入 `CreateToken`,异常栈落在更新用户登录信息:
+
+```text
+Admin.NET.Core.Service.SysAuthService.CreateToken(...)
+UPDATE `SysUser`
+SET `LastLoginIp` = ..., `LastLoginTime` = ...
+WHERE `Id` = 1300000000101
+
+MySqlConnector.MySqlException:
+The Command Timeout expired before the operation completed.
+```
+
+### 2.2 数据库提交堆积
+
+`SHOW FULL PROCESSLIST` 显示多个事务长时间停留在:
+
+```text
+State: waiting for handler commit
+```
+
+典型 SQL 包括:
+
+- `UPDATE SysUser SET LastLoginTime = ... WHERE Id = 1300000000101`
+- `INSERT INTO SysJobTriggerRecord (...)`
+- `UPDATE SysJobDetail SET ... WHERE JobId = ...`
+- `UPDATE ado_s8_watch_rule SET lock_token = ..., lock_until = ...`
+- `INSERT INTO ado_s8_notification_log (...)`
+- `INSERT INTO SysLogOp / SysLogEx (...)`
+
+### 2.3 多实例同时写同一库
+
+数据库进程中可见至少两个应用来源同时连接 `aidopdev`:
+
+- `49.65.129.55`
+- `39.105.125.212`
+
+这意味着多个后端实例可能同时注册并执行定时任务。`Concurrent = false` 只能限制单进程内并发,不能阻止多实例重复调度。
+
+### 2.4 MySQL 临时空间异常
+
+查询进程统计时 MySQL 返回:
+
+```text
+The table '/tmp/#sql...' is full
+```
+
+该现象通常指向数据库服务器 `/tmp`、MySQL 临时目录、根盘、数据盘或临时表空间不足,也可能与 IO 阻塞叠加。
+
+---
+
+## 三、S8 当前不合理点
+
+### 3.1 缺少环境级调度总开关
+
+S8 相关 Job 通过 Furion Schedule 注解注册:
+
+- `job_s8_watch_scheduler`
+- `job_s8_timeout_auto_escalation`
+- `job_s8_active_flow_stuck_scan`
+
+只要服务启动并启用调度,这些 Job 就可能运行。开发机、测试机、生产机如果连接同一个库,就会重复写调度表和业务表。
+
+### 3.2 主调度 1 分钟 tick 偏频繁
+
+`S8WatchSchedulerJob` 当前固定 1 分钟触发:
+
+```text
+trigger_s8_watch_scheduler: 60000 ms
+```
+
+虽然单条规则有 `poll_interval_seconds` 和 `next_run_at` 控制,但 Job 每分钟仍会发生:
+
+- Furion 调度触发记录写入
+- 过期 lease 清理查询/更新
+- 到期规则查询
+- 运行日志输出
+
+在多实例部署下,该成本会线性放大。
+
+### 3.3 Furion 调度记录写入偏高
+
+每次 Job 执行都会写入 `SysJobTriggerRecord`。S8 主调度 1 分钟一次,另外还有每 5 分钟执行的 S8 超时升级、ActiveFlow 卡死扫描、审批流超时任务。多实例同时运行时,`SysJobTriggerRecord` 写入频率明显升高。
+
+### 3.4 DetectionLog 记录粒度偏细
+
+S8 每条规则运行会写 `AdoS8DetectionLog`,包括:
+
+- `NO_HIT`
+- `REFRESHED`
+- `CREATED`
+- `RECOVERED`
+- `EVALUATE_FAILED`
+
+其中 `NO_HIT` 和持续 `REFRESHED` 在长期稳定状态下价值较低,但会持续写库、持续增长。
+
+### 3.5 ActiveFlow 孤儿扫描去重依赖 Payload
+
+`S8ActiveFlowWatchService` 在孤儿流程扫描中读取通知日志 `Payload` 并解析 `flowInstanceId` 做去重。`Payload` 是大 JSON 文本,难以利用索引,数据量增长后扫描成本会持续上升。
+
+### 3.6 规则 SQL 缺少统一执行保护
+
+规则 evaluator 通过数据源 SQL 拉取结果。如果规则 SQL 没有时间窗口、分页或限制行数,容易出现一次性全量扫描。当前需要补充统一超时、最大返回行数和规则 SQL 审核约束。
+
+---
+
+## 四、优化目标
+
+1. **恢复稳定性**:避免 S8/审批调度写入拖垮登录、菜单、用户信息等基础能力。
+2. **控制写入频率**:减少空跑、重复跑和低价值日志写入。
+3. **支持多环境隔离**:开发、测试、生产连接同库时,不默认重复执行调度。
+4. **增强可观测性**:保留关键运行证据,但避免日志表无限增长。
+5. **降低数据库依赖风险**:规则 SQL、通知扫描、调度记录均有明确上限与归档策略。
+
+---
+
+## 五、优化方案
+
+### 5.1 增加调度总开关
+
+新增配置建议:
+
+```json
+{
+  "Scheduler": {
+    "Enabled": false,
+    "OwnerInstance": "prod-scheduler-01"
+  },
+  "S8": {
+    "Scheduler": {
+      "Enabled": false
+    }
+  }
+}
+```
+
+建议策略:
+
+- 开发环境默认 `false`
+- 测试/生产只允许一个服务实例为 `true`
+- 普通 Web 实例只提供 API,不运行后台调度
+- 调度实例单独部署、单独监控
+
+### 5.2 S8 主调度频率配置化
+
+将 `S8WatchSchedulerJob.IntervalMs` 从固定常量调整为配置项。
+
+建议默认:
+
+- 开发环境:关闭
+- 测试环境:5 分钟
+- 生产环境:1 至 5 分钟,由规则规模决定
+
+规则本身已经有 `poll_interval_seconds` 和 `next_run_at`,因此 Job tick 不必长期固定为 1 分钟。
+
+### 5.3 降低空跑写入
+
+优化方向:
+
+- `picked = 0` 的 tick 不写业务侧检测日志。
+- Furion 触发记录保留系统默认行为,但降低 Job 频率。
+- `S8WatchSchedulerJob` 只在有 picked、failed、created、refreshed、leaseReleased 时输出详细日志。
+
+### 5.4 DetectionLog 分级记录
+
+建议调整:
+
+- `CREATED`、`RECOVERED`、`EVALUATE_FAILED`:必须保留明细。
+- `REFRESHED`:可按规则/对象限频记录,例如同一 dedupKey 10 分钟最多一条。
+- `NO_HIT`:不再每轮写明细,改为更新 `AdoS8RuleDetectionState` 或规则 last 状态。
+- 增加清理策略:保留 30 至 90 天明细,历史数据归档或汇总。
+
+### 5.5 ActiveFlow 告警去重结构化
+
+新增结构化字段或独立告警表,避免从 `Payload` 解析:
+
+- `channel`
+- `exception_id`
+- `flow_instance_id`
+- `biz_type`
+- `biz_id`
+- `created_at`
+
+建立索引:
+
+```sql
+CREATE INDEX idx_s8_notify_channel_flow_time
+ON ado_s8_notification_log(channel, flow_instance_id, created_at);
+```
+
+如果不改通知日志表,也可新增 `ado_s8_alert_dedup` 专用表承载去重键。
+
+### 5.6 规则 SQL 执行保护
+
+为所有 SQL evaluator 增加约束:
+
+- 设置 CommandTimeout,例如 10 至 30 秒。
+- 规则 SQL 必须包含时间窗口或业务状态过滤。
+- 对结果行数设置上限,例如 500 或 1000。
+- 超限时写 `EVALUATE_FAILED`,并累计失败次数触发自动暂停。
+- SQL 配置评审时检查是否命中索引。
+
+### 5.7 调度表清理与归档
+
+重点处理:
+
+- `SysJobTriggerRecord`
+- `AdoS8DetectionLog`
+- `ado_s8_notification_log`
+- `SysLogOp`
+- `SysLogEx`
+
+建议:
+
+- 调度记录保留 7 至 30 天。
+- 检测明细保留 30 至 90 天。
+- 系统操作日志按现有审计要求保留,超过期限归档。
+- 清理任务必须错峰运行,避免与 S8 主调度、MDP 同步同一时间段执行。
+
+---
+
+## 六、实施计划
+
+### 阶段一:止血与配置隔离
+
+目标:先避免再次拖垮基础登录能力。
+
+任务:
+
+- 停止多余后端实例的调度能力。
+- 增加调度总开关,开发环境默认关闭。
+- 明确只允许一个调度实例连接 `aidopdev` 执行 Job。
+- 将 S8 主调度默认频率调整为 5 分钟或配置化。
+
+验收:
+
+- `SHOW FULL PROCESSLIST` 中不再持续出现大量 S8/Job 相关 `waiting for handler commit`。
+- 超级管理员登录稳定在 3 秒以内。
+- `SysJobTriggerRecord` 写入速率明显下降。
+
+### 阶段二:降低 S8 写入量
+
+目标:减少低价值日志和空跑写入。
+
+任务:
+
+- 调整 `AdoS8DetectionLog` 记录策略。
+- `NO_HIT` 改为汇总状态,不每轮插入明细。
+- `REFRESHED` 增加限频或合并策略。
+- 增加日志清理/归档脚本。
+
+验收:
+
+- 无命中规则运行时,`AdoS8DetectionLog` 不再线性增长。
+- 长期重复命中的对象不会高频写刷新日志。
+- S8 功能看板仍能展示关键创建、恢复、失败证据。
+
+### 阶段三:扫描与 SQL 执行优化
+
+目标:降低单次规则或扫描拖垮数据库的风险。
+
+任务:
+
+- ActiveFlow 孤儿扫描去重字段结构化。
+- 规则 SQL 增加超时、行数上限和索引评审。
+- 对慢规则记录运行耗时并自动暂停。
+
+验收:
+
+- 单条规则运行超时可被捕获并记录,不拖垮整个 tick。
+- ActiveFlow 扫描在数据量增长后仍保持可控耗时。
+- 慢规则连续失败后自动暂停,避免持续冲击数据库。
+
+### 阶段四:数据库侧长期治理
+
+目标:避免数据库环境自身成为单点风险。
+
+任务:
+
+- 监控 `/tmp`、MySQL 数据盘、binlog、redo、undo 空间。
+- 监控 `Threads_running`、`waiting for handler commit`、行锁等待、慢 SQL。
+- 对开发库评估 `sync_binlog`、`innodb_flush_log_at_trx_commit` 配置;生产库需按可靠性要求谨慎调整。
+- 对高频日志表建立保留策略和归档策略。
+
+验收:
+
+- 磁盘使用率、临时目录使用率有告警。
+- 提交等待和连接堆积能在故障前被发现。
+- 调度类表增长有可预测上限。
+
+---
+
+## 七、应急处理流程
+
+当再次出现登录超时、菜单加载超时或大量 SQL 提交等待时,按以下顺序处理:
+
+1. 停止多余应用实例,保留一个调度实例。
+2. 检查数据库服务器磁盘、inode、`/tmp` 和 MySQL 数据目录。
+3. 查看 `SHOW FULL PROCESSLIST`,确认是否大量 `waiting for handler commit`。
+4. 优先重启 MySQL 或谨慎 kill 长时间卡住的应用连接。
+5. 登录超级管理员验证基础链路。
+6. 再逐步恢复 S8/审批/MDP 定时任务。
+
+---
+
+## 八、后续决策点
+
+需要负责人确认:
+
+- S8 调度实例是否单独部署。
+- 各环境默认是否禁用调度。
+- DetectionLog 保留期限。
+- `SysJobTriggerRecord` 清理策略。
+- 规则 SQL 最大返回行数和超时阈值。
+- 是否允许开发库调整 MySQL 刷盘参数以提升可用性。