feat(stack): XHProf 采集切换文件落盘并独立 xhgui 供应

This commit is contained in:
augushong
2026-08-14 21:58:29 +08:00
parent e0e2580646
commit da388a8c4f
6 changed files with 138 additions and 47 deletions

View File

@@ -3,11 +3,19 @@
* XHProf 性能分析采集器初始化脚本 * XHProf 性能分析采集器初始化脚本
* *
* 通过 PHP auto_prepend_file 机制自动加载,零侵入业务代码。 * 通过 PHP auto_prepend_file 机制自动加载,零侵入业务代码。
* 采集数据通过 HTTP Upload 发送到 XHGui 可视化服务。 *
* 采集数据落盘方式(环境变量 XHPROF_SAVER默认 file
* - file 本地 jsonl 文件落盘(默认;按天分文件,由后台导入定时任务增量入库)
* - upload HTTP Upload 推送到 XHGui 可视化服务(旧行为,兼容保留)
* - stack file + upload 双写
* *
* 环境变量控制: * 环境变量控制:
* - XHPROF_ENABLE=true 开启采集(默认关闭) * - 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 * - XHGUI_UPLOAD_TOKEN 上传令牌(默认 ulthon-admin-xhprof
* *
* Cookie 手动触发: * Cookie 手动触发:
@@ -30,7 +38,19 @@ require_once '/opt/xhprof/vendor/autoload.php';
use Xhgui\Profiler\Profiler; use Xhgui\Profiler\Profiler;
use Xhgui\Profiler\ProfilingFlags; 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 = [ $config = [
// 采集标志CPU + 内存 + 不记录内置函数 + 不记录 span // 采集标志CPU + 内存 + 不记录内置函数 + 不记录 span
'profiler.flags' => [ 'profiler.flags' => [
@@ -39,13 +59,6 @@ $config = [
ProfilingFlags::NO_BUILTINS, ProfilingFlags::NO_BUILTINS,
ProfilingFlags::NO_SPANS, 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% 随机采样 // 采集控制cookie 强制触发 或 1% 随机采样
'profiler.enable' => function () { 'profiler.enable' => function () {
// 浏览器带 cookie _profiler 时强制采集(手动调试) // 浏览器带 cookie _profiler 时强制采集(手动调试)
@@ -55,9 +68,53 @@ $config = [
// 默认 1% 采样率 // 默认 1% 采样率
return mt_rand(1, 100) === 1; return mt_rand(1, 100) === 1;
}, },
// URL 规范化:采集时原生计算 simple_url 写入 metapath 去 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) {
// uploadHTTP POST 到 XHGui旧行为兼容保留
$config['save.handler'] = Profiler::SAVER_UPLOAD;
} elseif ($saver === Profiler::SAVER_STACK) {
// stackfile + 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}.jsonlfilename 在每个请求求值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 { try {
$profiler = new Profiler($config); $profiler = new Profiler($config);
$profiler->start(); $profiler->start();

View File

@@ -49,38 +49,58 @@ cd source/stack/docker-dev-myproject && docker compose up -d
## XHProf 性能分析(可选) ## 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 ```bash
# 在模式目录中,加 --profile xhprof 启动
cd source/stack/docker-dev 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 ```bash
docker compose --profile xhprof up -d cd source/stack/docker-dev
XHPROF_ENABLE=true XHPROF_SAVER=stack XHGUI_UPLOAD_URL=<XHGui上传地址> 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` 覆盖
### 认证配置 XHGui 存储使用 PDO MySQL。docker-dev / docker-dev-sync 复用栈内 MySQLdocker-serve / full 没有内置 MySQL需配置 `XHGUI_PDO_DSN` 指向可用的 MySQL 实例:
通过环境变量覆盖默认密码(在 `.env` 或 docker-compose 命令行设置):
```env
XHGUI_AUTH_USER=myuser
XHGUI_AUTH_PASS=mypassword
```
### 存储配置
默认使用 PDO MySQL 存储(复用 docker-dev 的 MySQL。通过环境变量自定义
```env ```env
XHGUI_PDO_DSN=mysql:host=mysql;dbname=ulthon;charset=utf8mb4 XHGUI_PDO_DSN=mysql:host=mysql;dbname=ulthon;charset=utf8mb4
@@ -88,21 +108,27 @@ XHGUI_PDO_USER=root
XHGUI_PDO_PASS=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 目录规则(强约束) ## default 目录规则(强约束)

View File

@@ -18,7 +18,9 @@ services:
# Sync polling interval in seconds (default: 3) # Sync polling interval in seconds (default: 3)
- SYNC_INTERVAL=${SYNC_INTERVAL:-3} - SYNC_INTERVAL=${SYNC_INTERVAL:-3}
- XHPROF_ENABLE=${XHPROF_ENABLE:-false} - 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} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof}
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
@@ -64,7 +66,7 @@ services:
xhgui: xhgui:
image: xhgui/xhgui:0.24.x image: xhgui/xhgui:0.24.x
restart: unless-stopped restart: unless-stopped
profiles: ["xhprof"] profiles: ["xhgui"]
ports: ports:
- "8142:80" - "8142:80"
volumes: volumes:

View File

@@ -16,7 +16,9 @@ services:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
environment: environment:
- XHPROF_ENABLE=${XHPROF_ENABLE:-false} - 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} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof}
depends_on: depends_on:
mysql: mysql:
@@ -60,7 +62,7 @@ services:
xhgui: xhgui:
image: xhgui/xhgui:0.24.x image: xhgui/xhgui:0.24.x
restart: unless-stopped restart: unless-stopped
profiles: ["xhprof"] profiles: ["xhgui"]
ports: ports:
- "8142:80" - "8142:80"
volumes: volumes:

View File

@@ -20,7 +20,9 @@ services:
# - ./storage:/var/www/html/storage # - ./storage:/var/www/html/storage
environment: environment:
- XHPROF_ENABLE=${XHPROF_ENABLE:-false} - 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} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof}
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
@@ -28,7 +30,7 @@ services:
xhgui: xhgui:
image: xhgui/xhgui:0.24.x image: xhgui/xhgui:0.24.x
restart: always restart: always
profiles: ["xhprof"] profiles: ["xhgui"]
ports: ports:
- "8142:80" - "8142:80"
volumes: volumes:

View File

@@ -20,7 +20,9 @@ services:
# - ./storage:/var/www/html/storage # - ./storage:/var/www/html/storage
environment: environment:
- XHPROF_ENABLE=${XHPROF_ENABLE:-false} - 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} - XHGUI_UPLOAD_TOKEN=${XHGUI_UPLOAD_TOKEN:-ulthon-admin-xhprof}
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
@@ -28,7 +30,7 @@ services:
xhgui: xhgui:
image: xhgui/xhgui:0.24.x image: xhgui/xhgui:0.24.x
restart: always restart: always
profiles: ["xhprof"] profiles: ["xhgui"]
ports: ports:
- "8142:80" - "8142:80"
volumes: volumes: