mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-31 05:05:33 +08:00
178 lines
6.5 KiB
PHP
178 lines
6.5 KiB
PHP
<?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 IN(detail.id = run.id,1: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_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;
|
||
}
|
||
}
|