From b472df8c1daaa84a2eba2be55508bd1f1de487ff Mon Sep 17 00:00:00 2001 From: augushong Date: Sat, 15 Aug 2026 05:47:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20XHProf=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=B8=85=E7=90=86=E4=BB=BB=E5=8A=A1=E4=B8=8E=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .example.env | 8 +- app/tools/controller/timer/XhprofRunClean.php | 20 ++ extend/base/common/command/timer/config.php | 16 ++ .../controller/timer/XhprofRunCleanBase.php | 177 ++++++++++++++++++ 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 app/tools/controller/timer/XhprofRunClean.php create mode 100644 extend/base/tools/controller/timer/XhprofRunCleanBase.php diff --git a/.example.env b/.example.env index e8c2dde..ed862f9 100644 --- a/.example.env +++ b/.example.env @@ -73,4 +73,10 @@ STRICT_VIEW_JS=true MAKE_VIEW_WHILE_MISSING=false -UPDATE_LEVEL=production \ No newline at end of file +UPDATE_LEVEL=production + +# XHProf 性能采样配置 +[XHPROF] +# 采样数据保留天数(run/detail/run_watch 三表按 create_time 清理;<= 0 时清理任务跳过,永久保留) +# 注意:采样采集开关 XHPROF_ENABLE 是容器环境变量(Dockerfile/stack 注入),不进 .env +KEEP_DAYS = 30 \ No newline at end of file diff --git a/app/tools/controller/timer/XhprofRunClean.php b/app/tools/controller/timer/XhprofRunClean.php new file mode 100644 index 0000000..20bf57e --- /dev/null +++ b/app/tools/controller/timer/XhprofRunClean.php @@ -0,0 +1,20 @@ + 1, 'run_type' => 'auto', ], + [ + 'name' => 'xhprof_import', + 'type' => 'site', + 'target' => '/tools/timer.XhprofLogImport/do', + 'frequency' => 60, + 'concurrency' => 1, + 'run_type' => 'all', + ], + [ + 'name' => 'xhprof_clean', + 'type' => 'site', + 'target' => '/tools/timer.XhprofRunClean/do', + 'frequency' => 86400, + 'concurrency' => 1, + 'run_type' => 'auto', + ], ]; diff --git a/extend/base/tools/controller/timer/XhprofRunCleanBase.php b/extend/base/tools/controller/timer/XhprofRunCleanBase.php new file mode 100644 index 0000000..beea97a --- /dev/null +++ b/extend/base/tools/controller/timer/XhprofRunCleanBase.php @@ -0,0 +1,177 @@ + true, + 'keep_days' => $keepDays, + 'dry_run' => $dryRun, + ], JSON_UNESCAPED_UNICODE); + } + + $threshold = time() - ($keepDays * 86400); + + Log::info("xhprof_clean: 阈值 create_time<" . date('Y-m-d H:i:s', $threshold) . " ({$keepDays} 天)" . ($dryRun ? ' [dry-run]' : '')); + + // 3. 先收集过期 run id(必须在删 run 之前;detail 豁免表无 create_time,只能经主键关联删) + $expiredRunIds = Db::name('xhprof_run') + ->where('create_time', '<', $threshold) + ->column('id'); + + // 4. 按依赖顺序清理:run_watch → detail → run + $runWatchDeleted = $this->cleanByCreateTime('xhprof_run_watch', $threshold, $dryRun); + $detailDeleted = $this->cleanDetailByRunIds($expiredRunIds, $dryRun); + $runDeleted = $dryRun ? count($expiredRunIds) : $this->cleanByCreateTime('xhprof_run', $threshold, false); + + Log::info("xhprof_clean: run_watch {$runWatchDeleted} 行, detail {$detailDeleted} 行, run {$runDeleted} 行" . ($dryRun ? ' [dry-run]' : '')); + + return json_encode([ + 'run_watch_deleted' => $runWatchDeleted, + 'detail_deleted' => $detailDeleted, + 'run_deleted' => $runDeleted, + 'dry_run' => $dryRun, + ], JSON_UNESCAPED_UNICODE); + } + + /** + * 按 create_time 分批清理有约定字段的表(run_watch / run). + * + * 每批 5000 行 + usleep 100ms,避免长事务锁表(照抄 nginx 清理模板的批量模式)。 + * + * @param string $table 表名(不含前缀,由 Db::name 自动补) + * @param int $threshold 过期阈值(create_time 早于该时间戳删除) + * @param bool $dryRun true 时只统计将删数量不执行删除 + * @return int 累计删除行数(dryRun 为将删行数) + */ + protected function cleanByCreateTime(string $table, int $threshold, bool $dryRun): int + { + $query = Db::name($table)->where('create_time', '<', $threshold); + + if ($dryRun) { + return (int) $query->count(); + } + + $total = 0; + + // 循环分批 DELETE:每批 limit 5000,直到该阈值区间内无残留 + while (true) { + $deleted = Db::name($table) + ->where('create_time', '<', $threshold) + ->limit(self::RAW_BATCH_SIZE) + ->delete(); + + if ($deleted <= 0) { + break; + } + + $total += $deleted; + + if ($deleted === self::RAW_BATCH_SIZE) { + // 批次间休眠,降低 DB 压力(避免主从延迟) + usleep(self::RAW_BATCH_USLEEP); + } else { + // 末批(< 5000)说明已清完该阈值区间 + break; + } + } + + return $total; + } + + /** + * 清理 detail 豁免表(按 run id 主键关联删). + * + * ul_xhprof_run_detail 无 create_time(T1 豁免表设计),无法按时间条件删; + * detail.id = run.id(1:1 非自增主键),把过期 run id 分批 IN 删除。 + * + * @param array $expiredRunIds 过期的 run 主键列表(删 run 之前收集) + * @param bool $dryRun true 时只统计将删数量不执行删除 + * @return int 累计删除行数(dryRun 为将删行数) + */ + protected function cleanDetailByRunIds(array $expiredRunIds, bool $dryRun): int + { + if (empty($expiredRunIds)) { + return 0; + } + + $total = 0; + + // 按 RAW_BATCH_SIZE 分块 IN 删除,避免单条 DELETE ... IN (...) 过大 + foreach (array_chunk($expiredRunIds, self::RAW_BATCH_SIZE) as $chunk) { + $query = Db::name('xhprof_run_detail')->whereIn('id', $chunk); + + if ($dryRun) { + $total += (int) $query->count(); + continue; + } + + $total += $query->delete(); + usleep(self::RAW_BATCH_USLEEP); + } + + return $total; + } +}