feat(tools): XHProf 数据清理任务与定时注册

This commit is contained in:
augushong
2026-08-15 05:47:34 +08:00
parent 7e73902325
commit b472df8c1d
4 changed files with 220 additions and 1 deletions

View File

@@ -73,4 +73,10 @@ STRICT_VIEW_JS=true
MAKE_VIEW_WHILE_MISSING=false
UPDATE_LEVEL=production
UPDATE_LEVEL=production
# XHProf 性能采样配置
[XHPROF]
# 采样数据保留天数run/detail/run_watch 三表按 create_time 清理;<= 0 时清理任务跳过,永久保留)
# 注意:采样采集开关 XHPROF_ENABLE 是容器环境变量Dockerfile/stack 注入),不进 .env
KEEP_DAYS = 30

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace app\tools\controller\timer;
use base\tools\controller\timer\XhprofRunCleanBase;
/**
* XHProf 采样数据清理定时任务App 入口类).
*
* 框架路由 /tools/timer.XhprofRunClean/do 与 /tools/timer.XhprofRunClean/execute
* 均通过本类入口实例化(依赖倒置:定时器框架/路由系统直接调本类,不调 Base
*
* 业务侧如需定制清理行为(如改批大小、加自定义过滤、改保留策略等),
* 重写本类对应方法即可拦截,无需改动 extend/base/。
*/
class XhprofRunClean extends XhprofRunCleanBase
{
}

View File

@@ -45,4 +45,20 @@ return [
'concurrency' => 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',
],
];

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace base\tools\controller\timer;
use app\common\controller\TimerController;
use think\facade\Db;
use think\facade\Log;
/**
* XHProf 采样数据清理定时任务Base 层).
*
* 清理范围(三表,均按入库时间 create_time 早于 N 天前):
* 1. ul_xhprof_run_watch函数监控关联表按 create_time 分批删除
* 2. ul_xhprof_run_detail采样明细LONGTEXT profile_data
* 该表是豁免表,无 create_time 约定字段——先收集过期 run id
* 再按主键 id INdetail.id = run.id1:1分批删除
* 3. ul_xhprof_run采样主表按 create_time 分批删除
*
* 永不清理:
* - ul_xhprof_log_position导入位置基准删了会全量重读 jsonl
* - ul_xhprof_watch函数监控配置用户手动维护的业务数据
* - xhgui_* 表XHGui 自建表,不属于本框架管理范围
*
* 控制参数(.env [XHPROF] KEEP_DAYS默认 30<= 0 = 永久跳过防误删全表):
* - xhprof.keep_days 采样保留天数
*
* 业务侧如需定制清理逻辑,重写 app/tools/controller/timer/XhprofRunClean.php 对应方法即可拦截。
*/
class XhprofRunCleanBase extends TimerController
{
/**
* 防刷间隔(秒).
* 仅做控制器侧防刷,不影响定时器侧 frequency 节流。
*/
protected $frequency = 600;
/**
* 并发上限 cap单分片id=0。纯 DB 清理任务无需并发分片。
*/
protected $concurrency = 1;
/**
* 分批删除的批量大小(避免单条 DELETE 锁表太久)。
*/
protected const RAW_BATCH_SIZE = 5000;
/**
* 每批 DELETE 之间的微秒休眠(避免主从延迟、降低 DB 压力)。
*/
protected const RAW_BATCH_USLEEP = 100000;
/**
* 执行清理.
*
* @param bool $dryRun true 时只统计将删数量不执行删除(用于验证保留策略)
* @return string 删除计数 json三表各自计数 + dryRun 标记keep_days <= 0 时返回 disabled 标记)
*/
public function do(bool $dryRun = false)
{
// 1. 读保留天数intval 强转:.env 值是字符串,默认值是 int
$keepDays = (int) env('xhprof.keep_days', 30);
// 2. keep_days <= 0 永久跳过(防误删全表),返回 disabled 标记
if ($keepDays <= 0) {
Log::debug('xhprof keep_days <= 0, skip clean');
return json_encode([
'disabled' => 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_timeT1 豁免表设计),无法按时间条件删;
* detail.id = run.id1: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;
}
}