diff --git a/source/docker/xhprof-bootstrap.php b/source/docker/xhprof-bootstrap.php index 35f39e6..bd31acf 100644 --- a/source/docker/xhprof-bootstrap.php +++ b/source/docker/xhprof-bootstrap.php @@ -3,11 +3,19 @@ * XHProf 性能分析采集器初始化脚本 * * 通过 PHP auto_prepend_file 机制自动加载,零侵入业务代码。 - * 采集数据通过 HTTP Upload 发送到 XHGui 可视化服务。 + * + * 采集数据落盘方式(环境变量 XHPROF_SAVER,默认 file): + * - file 本地 jsonl 文件落盘(默认;按天分文件,由后台导入定时任务增量入库) + * - upload HTTP Upload 推送到 XHGui 可视化服务(旧行为,兼容保留) + * - stack file + upload 双写 * * 环境变量控制: * - XHPROF_ENABLE=true 开启采集(默认关闭) - * - XHGUI_UPLOAD_URL XHGui 上传地址(默认 http://xhgui:80/run/import) + * - XHPROF_SAVER 落盘方式:file / upload / stack(默认 file) + * - XHPROF_FILE_DIR jsonl 落盘目录(默认 /var/www/html/runtime/xhprof; + * 采集端与导入任务均以 getenv 读取此变量,保持单一来源, + * 禁止与 ThinkPHP env() 混用,两处漂移会导致导入读不到文件) + * - XHGUI_UPLOAD_URL XHGui 上传地址(upload/stack 模式使用,默认 http://xhgui:80/run/import) * - XHGUI_UPLOAD_TOKEN 上传令牌(默认 ulthon-admin-xhprof) * * Cookie 手动触发: @@ -30,7 +38,19 @@ require_once '/opt/xhprof/vendor/autoload.php'; use Xhgui\Profiler\Profiler; use Xhgui\Profiler\ProfilingFlags; -// 4. 配置 Profiler +// 4. 落盘目录(单一来源:XHPROF_FILE_DIR,导入定时任务读同一变量) +$fileDir = getenv('XHPROF_FILE_DIR') ?: '/var/www/html/runtime/xhprof'; + +// 5. 落盘方式(默认 file) +$saver = getenv('XHPROF_SAVER') ?: Profiler::SAVER_FILE; + +// stack 依赖上传地址:XHGUI_UPLOAD_URL 为空时降级 file 单写,防止 upload 分支静默丢数据 +if ($saver === Profiler::SAVER_STACK && !getenv('XHGUI_UPLOAD_URL')) { + error_log('[xhprof] XHPROF_SAVER=stack 但 XHGUI_UPLOAD_URL 为空,降级为 file 单写'); + $saver = Profiler::SAVER_FILE; +} + +// 6. 配置 Profiler $config = [ // 采集标志:CPU + 内存 + 不记录内置函数 + 不记录 span 'profiler.flags' => [ @@ -39,13 +59,6 @@ $config = [ ProfilingFlags::NO_BUILTINS, ProfilingFlags::NO_SPANS, ], - // 数据上传方式:HTTP POST 到 XHGui - 'save.handler' => Profiler::SAVER_UPLOAD, - 'save.handler.upload' => [ - 'url' => getenv('XHGUI_UPLOAD_URL') ?: 'http://xhgui:80/run/import', - 'timeout' => 3, - 'token' => getenv('XHGUI_UPLOAD_TOKEN') ?: 'ulthon-admin-xhprof', - ], // 采集控制:cookie 强制触发 或 1% 随机采样 'profiler.enable' => function () { // 浏览器带 cookie _profiler 时强制采集(手动调试) @@ -55,9 +68,53 @@ $config = [ // 默认 1% 采样率 return mt_rand(1, 100) === 1; }, + // URL 规范化:采集时原生计算 simple_url 写入 meta(path 去 query、纯数字段替换 :id)。 + // 后台导入任务直接读 meta.simple_url,仅缺失时兜底计算,此为唯一实现点 + 'profiler.simple_url' => function ($url) { + $path = parse_url($url, PHP_URL_PATH); + $parts = explode('/', trim($path, '/')); + foreach ($parts as &$p) { + if ($p !== '' && ctype_digit($p)) { + $p = ':id'; + } + } + return implode('/', $parts); + }, ]; -// 5. 启动采集(异常不影响应用正常运行) +// 7. 落盘方式分派(未知值回退 file) +if ($saver === Profiler::SAVER_UPLOAD) { + // upload:HTTP POST 到 XHGui(旧行为,兼容保留) + $config['save.handler'] = Profiler::SAVER_UPLOAD; +} elseif ($saver === Profiler::SAVER_STACK) { + // stack:file + upload 双写(SaverFactory 递归创建 savers 数组中的每个 saver) + $config['save.handler'] = Profiler::SAVER_STACK; + $config['save.handler.stack'] = [ + 'savers' => [Profiler::SAVER_FILE, Profiler::SAVER_UPLOAD], + ]; +} else { + // file:本地 jsonl 文件落盘(默认) + $config['save.handler'] = Profiler::SAVER_FILE; +} + +// FileSaver 不自建目录,start 前确保目录存在 +if ($saver !== Profiler::SAVER_UPLOAD) { + @mkdir($fileDir, 0777, true); +} + +// 按天分文件(runs-{Ymd}.jsonl):filename 在每个请求求值(bootstrap 每请求执行), +// date() 跨天自然滚动;导入任务可安全删除已导完的整天文件 +$config['save.handler.file'] = [ + 'filename' => $fileDir . '/runs-' . date('Ymd') . '.jsonl', +]; + +$config['save.handler.upload'] = [ + 'url' => getenv('XHGUI_UPLOAD_URL') ?: 'http://xhgui:80/run/import', + 'timeout' => 3, + 'token' => getenv('XHGUI_UPLOAD_TOKEN') ?: 'ulthon-admin-xhprof', +]; + +// 8. 启动采集(异常不影响应用正常运行) try { $profiler = new Profiler($config); $profiler->start(); diff --git a/source/stack/README.md b/source/stack/README.md index 9b6b598..42df01e 100644 --- a/source/stack/README.md +++ b/source/stack/README.md @@ -49,38 +49,58 @@ cd source/stack/docker-dev-myproject && docker compose up -d ## XHProf 性能分析(可选) -docker-dev / docker-dev-sync / docker-serve / full 模式内置了 XHProf 性能分析能力,默认不启用,完全不影响现有行为。采集数据存储在 MySQL 中(PDO 方式),docker-dev / docker-dev-sync 开箱即用;docker-serve / full 需配置 XHGUI_PDO_DSN 环境变量指向可用的 MySQL。 +docker-dev / docker-dev-sync / docker-serve / full 模式内置了 XHProf 性能分析能力,默认不启用,完全不影响现有行为。 -### 启用方式 +### 推荐路径:本地文件落盘 + 后台查看(默认) + +采集数据以 jsonl 文件按天落盘到容器内目录,由后台导入定时任务增量读取写入数据库,最终在后台管理界面查看性能报告。无需启动任何额外服务。 + +``` +请求 + └─ auto_prepend_file(xhprof-bootstrap.php) + └─ php-profiler 采集(XHPROF_ENABLE 总闸 + cookie _profiler=1 强制 + 1% 采样) + └─ 落盘 {XHPROF_FILE_DIR}/runs-{Ymd}.jsonl(默认 /var/www/html/runtime/xhprof,按天分文件) + └─ 后台导入定时任务增量读取入库(读同一 XHPROF_FILE_DIR,导完的整天文件自动删除) + └─ 后台管理界面查看性能报告 +``` + +启用方式(默认即为 file 落盘,无需额外 profile): ```bash -# 在模式目录中,加 --profile xhprof 启动 cd source/stack/docker-dev -XHPROF_ENABLE=true docker compose --profile xhprof up -d +XHPROF_ENABLE=true docker compose up -d ``` -或者在 `.env` 中设置 `XHPROF_ENABLE=true`,然后: +相关环境变量: + +| 变量 | 默认 | 说明 | +|------|------|------| +| XHPROF_ENABLE | false | 采集总开关,true 开启 | +| XHPROF_SAVER | file | 落盘方式:file(文件落盘)/ upload(推送 XHGui)/ stack(双写) | +| XHPROF_FILE_DIR | 空 | jsonl 落盘目录,空时容器内 `/var/www/html/runtime/xhprof`;采集与导入共用此变量 | +| XHGUI_UPLOAD_URL | 空 | XHGui 上传地址,仅 upload / stack 模式使用 | +| XHGUI_UPLOAD_TOKEN | ulthon-admin-xhprof | 上传令牌,仅 upload / stack 模式使用 | + +### 手动触发采集 + +默认 1% 采样率。需要强制采集某个请求时,浏览器请求带 cookie `_profiler=1`。 + +### 可选:XHGui 独立可视化服务 + +XHGui 已降级为可选服务(独立 profile),只负责实时可视化,不参与默认采集链路。需要时以 `--profile xhgui` 启动,并推荐配合 stack 双写: ```bash -docker compose --profile xhprof up -d +cd source/stack/docker-dev +XHPROF_ENABLE=true XHPROF_SAVER=stack XHGUI_UPLOAD_URL= docker compose --profile xhgui up -d ``` -### 查看性能报告 +- `XHPROF_SAVER=stack`:文件落盘与 XHGui 推送同时写(SaverFactory 递归双写),落盘数据不丢,XHGui 实时可看 +- `XHGUI_UPLOAD_URL` 为空时不会推送;`XHPROF_SAVER=stack` 且该地址为空时,bootstrap 记录警告并自动降级为 file 单写(防静默丢数据) +- `XHPROF_SAVER=upload`(只推送 XHGui 不落盘)为旧行为,兼容保留,不推荐 -浏览器打开 `http://localhost:8142`,输入 Basic Auth 用户名密码(默认 `admin` / `xhgui123`)。 +浏览器打开 `http://localhost:8142` 查看,Basic Auth 默认 `admin` / `xhgui123`,通过 `XHGUI_AUTH_USER` / `XHGUI_AUTH_PASS` 覆盖。 -### 认证配置 - -通过环境变量覆盖默认密码(在 `.env` 或 docker-compose 命令行设置): - -```env -XHGUI_AUTH_USER=myuser -XHGUI_AUTH_PASS=mypassword -``` - -### 存储配置 - -默认使用 PDO MySQL 存储(复用 docker-dev 的 MySQL)。通过环境变量自定义: +XHGui 存储使用 PDO MySQL。docker-dev / docker-dev-sync 复用栈内 MySQL;docker-serve / full 没有内置 MySQL,需配置 `XHGUI_PDO_DSN` 指向可用的 MySQL 实例: ```env XHGUI_PDO_DSN=mysql:host=mysql;dbname=ulthon;charset=utf8mb4 @@ -88,21 +108,27 @@ XHGUI_PDO_USER=root XHGUI_PDO_PASS=root ``` -docker-serve / full 模式没有内置 MySQL,需要设置 `XHGUI_PDO_DSN` 指向可用的 MySQL 实例。 - -### 手动触发采集 - -默认 1% 采样率。需要强制采集某个请求时,浏览器请求带 cookie `_profiler=1`。 - ### 端口说明 | 端口 | 服务 | 说明 | |------|------|------| -| 8142 | XHGui | 性能分析报告界面 | +| 8142 | XHGui | 可选的性能分析报告界面(需 `--profile xhgui`) | + +### 迁移说明(profiles 更名,破坏性变更) + +XHGui 服务的 compose profile 已由 `xhprof` 更名为 `xhgui`,采集开关与 XHGui 界面彻底解耦: + +- 旧命令 `docker compose --profile xhprof up -d` 不再启动 XHGui,请改用 `--profile xhgui` +- 现在只需 `XHPROF_ENABLE=true` 即可采集(默认文件落盘),无需任何 profile +- 旧栈若依赖 upload 直推 XHGui,需显式设置 `XHPROF_SAVER=upload` 或改用推荐的 file / stack 模式 + +### 部署注记 + +bootstrap 的落盘方式切换与后台导入定时任务是配套变更,须同批上线:若采集端已切换为默认 file 落盘而后台导入任务未部署,落盘文件将无人消费、持续增长。 ### 默认行为 -不加 `--profile xhprof` 时,XHGui 服务不启动,行为与未集成 XHProf 完全一致。 +`XHPROF_ENABLE=false`(默认)时不采集,行为与未集成 XHProf 完全一致;不加 `--profile xhgui` 时 XHGui 服务不启动。 ## default 目录规则(强约束) diff --git a/source/stack/docker-dev-sync/docker-compose.yaml b/source/stack/docker-dev-sync/docker-compose.yaml index 5b0904c..91c0c4c 100644 --- a/source/stack/docker-dev-sync/docker-compose.yaml +++ b/source/stack/docker-dev-sync/docker-compose.yaml @@ -18,7 +18,9 @@ services: # Sync polling interval in seconds (default: 3) - SYNC_INTERVAL=${SYNC_INTERVAL:-3} - XHPROF_ENABLE=${XHPROF_ENABLE:-false} - - XHGUI_UPLOAD_URL=http://xhgui:80/run/import + - XHPROF_SAVER=${XHPROF_SAVER:-file} + - XHPROF_FILE_DIR=${XHPROF_FILE_DIR:-} + - XHGUI_UPLOAD_URL=${XHGUI_UPLOAD_URL:-} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof} extra_hosts: - "host.docker.internal:host-gateway" @@ -64,7 +66,7 @@ services: xhgui: image: xhgui/xhgui:0.24.x restart: unless-stopped - profiles: ["xhprof"] + profiles: ["xhgui"] ports: - "8142:80" volumes: diff --git a/source/stack/docker-dev/docker-compose.yaml b/source/stack/docker-dev/docker-compose.yaml index 9d75b44..64408cd 100644 --- a/source/stack/docker-dev/docker-compose.yaml +++ b/source/stack/docker-dev/docker-compose.yaml @@ -16,7 +16,9 @@ services: - "host.docker.internal:host-gateway" environment: - XHPROF_ENABLE=${XHPROF_ENABLE:-false} - - XHGUI_UPLOAD_URL=http://xhgui:80/run/import + - XHPROF_SAVER=${XHPROF_SAVER:-file} + - XHPROF_FILE_DIR=${XHPROF_FILE_DIR:-} + - XHGUI_UPLOAD_URL=${XHGUI_UPLOAD_URL:-} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof} depends_on: mysql: @@ -60,7 +62,7 @@ services: xhgui: image: xhgui/xhgui:0.24.x restart: unless-stopped - profiles: ["xhprof"] + profiles: ["xhgui"] ports: - "8142:80" volumes: diff --git a/source/stack/docker-serve/docker-compose.yaml b/source/stack/docker-serve/docker-compose.yaml index 15f6dfc..c2d2d71 100644 --- a/source/stack/docker-serve/docker-compose.yaml +++ b/source/stack/docker-serve/docker-compose.yaml @@ -20,7 +20,9 @@ services: # - ./storage:/var/www/html/storage environment: - XHPROF_ENABLE=${XHPROF_ENABLE:-false} - - XHGUI_UPLOAD_URL=http://xhgui:80/run/import + - XHPROF_SAVER=${XHPROF_SAVER:-file} + - XHPROF_FILE_DIR=${XHPROF_FILE_DIR:-} + - XHGUI_UPLOAD_URL=${XHGUI_UPLOAD_URL:-} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof} extra_hosts: - "host.docker.internal:host-gateway" @@ -28,7 +30,7 @@ services: xhgui: image: xhgui/xhgui:0.24.x restart: always - profiles: ["xhprof"] + profiles: ["xhgui"] ports: - "8142:80" volumes: diff --git a/source/stack/full/docker-compose.yaml b/source/stack/full/docker-compose.yaml index bbf3943..65e171b 100644 --- a/source/stack/full/docker-compose.yaml +++ b/source/stack/full/docker-compose.yaml @@ -20,7 +20,9 @@ services: # - ./storage:/var/www/html/storage environment: - XHPROF_ENABLE=${XHPROF_ENABLE:-false} - - XHGUI_UPLOAD_URL=http://xhgui:80/run/import + - XHPROF_SAVER=${XHPROF_SAVER:-file} + - XHPROF_FILE_DIR=${XHPROF_FILE_DIR:-} + - XHGUI_UPLOAD_URL=${XHGUI_UPLOAD_URL:-} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof} extra_hosts: - "host.docker.internal:host-gateway" @@ -28,7 +30,7 @@ services: xhgui: image: xhgui/xhgui:0.24.x restart: always - profiles: ["xhprof"] + profiles: ["xhgui"] ports: - "8142:80" volumes: