debug
This commit is contained in:
@@ -1519,9 +1519,17 @@ class Site extends BaseController
|
|||||||
$intWindowHours,
|
$intWindowHours,
|
||||||
$strBotScope,
|
$strBotScope,
|
||||||
$mixSelectedBots
|
$mixSelectedBots
|
||||||
));
|
))->header([
|
||||||
|
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
'Expires' => '0',
|
||||||
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取蜘蛛抓取概览失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取蜘蛛抓取概览失败:' . $Throwable->getMessage())->header([
|
||||||
|
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
'Expires' => '0',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class AdminCors extends AllowCrossDomain
|
|||||||
protected $header = [
|
protected $header = [
|
||||||
'Access-Control-Allow-Credentials' => 'true',
|
'Access-Control-Allow-Credentials' => 'true',
|
||||||
'Access-Control-Max-Age' => 3600,
|
'Access-Control-Max-Age' => 3600,
|
||||||
'Access-Control-Allow-Methods' => '*',
|
'Access-Control-Allow-Methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
||||||
'Access-Control-Allow-Headers' => '*',
|
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With, admin-token',
|
||||||
|
'Vary' => 'Origin',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class DomainSpiderCrawlWorkbenchHelper
|
class DomainSpiderCrawlWorkbenchHelper
|
||||||
{
|
{
|
||||||
|
protected const MAX_AGGREGATE_RUNS = 12;
|
||||||
|
|
||||||
protected static function codeRootFromStorageRoot(string $storageRoot): string
|
protected static function codeRootFromStorageRoot(string $storageRoot): string
|
||||||
{
|
{
|
||||||
return rtrim(dirname(rtrim($storageRoot, '/')), '/');
|
return rtrim(dirname(rtrim($storageRoot, '/')), '/');
|
||||||
@@ -308,7 +310,7 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'summary' => $summary,
|
'summary_path' => (string)$summaryPath,
|
||||||
'normalized' => self::normalizeRunSummary($publicRoot, $summary, (string)$summaryPath),
|
'normalized' => self::normalizeRunSummary($publicRoot, $summary, (string)$summaryPath),
|
||||||
'generated_at_ts' => $generatedTimestamp,
|
'generated_at_ts' => $generatedTimestamp,
|
||||||
];
|
];
|
||||||
@@ -336,7 +338,14 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
$anomalyRecords = [];
|
$anomalyRecords = [];
|
||||||
$latestGeneratedAt = '';
|
$latestGeneratedAt = '';
|
||||||
foreach ($runEntries as $entry) {
|
foreach ($runEntries as $entry) {
|
||||||
$summary = is_array($entry['summary'] ?? null) ? (array)$entry['summary'] : [];
|
$summaryPath = trim((string)($entry['summary_path'] ?? ''));
|
||||||
|
if ($summaryPath === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$summary = self::readJsonFile($summaryPath);
|
||||||
|
if (empty($summary)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$generatedAt = trim((string)($summary['generated_at'] ?? ''));
|
$generatedAt = trim((string)($summary['generated_at'] ?? ''));
|
||||||
if ($generatedAt !== '' && ($latestGeneratedAt === '' || strcmp($generatedAt, $latestGeneratedAt) > 0)) {
|
if ($generatedAt !== '' && ($latestGeneratedAt === '' || strcmp($generatedAt, $latestGeneratedAt) > 0)) {
|
||||||
$latestGeneratedAt = $generatedAt;
|
$latestGeneratedAt = $generatedAt;
|
||||||
@@ -363,6 +372,15 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
return $aggregated;
|
return $aggregated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function limitAggregateRunEntries(array $runEntries): array
|
||||||
|
{
|
||||||
|
if (count($runEntries) <= self::MAX_AGGREGATE_RUNS) {
|
||||||
|
return $runEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice($runEntries, 0, self::MAX_AGGREGATE_RUNS);
|
||||||
|
}
|
||||||
|
|
||||||
protected static function buildRecentRuns(array $runEntries, int $limit): array
|
protected static function buildRecentRuns(array $runEntries, int $limit): array
|
||||||
{
|
{
|
||||||
$items = [];
|
$items = [];
|
||||||
@@ -993,7 +1011,7 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($windows as $hours) {
|
foreach ($windows as $hours) {
|
||||||
$runEntries = self::collectRunEntries($publicRoot, $runsRoot, $hours);
|
$runEntries = self::collectRunEntries($publicRoot, $runsRoot, $hours);
|
||||||
$summary = self::aggregateRunSummaries($runEntries);
|
$summary = self::aggregateRunSummaries(self::limitAggregateRunEntries($runEntries));
|
||||||
$summary = self::filterSummaryByBotScope($summary, $botScope);
|
$summary = self::filterSummaryByBotScope($summary, $botScope);
|
||||||
$summary = self::filterSummaryBySelectedBots($summary, $selectedBots);
|
$summary = self::filterSummaryBySelectedBots($summary, $selectedBots);
|
||||||
$metrics = self::summarizeWindow($summary, $host);
|
$metrics = self::summarizeWindow($summary, $host);
|
||||||
@@ -1241,7 +1259,7 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
$recentRuns = self::buildRecentRuns($runEntries, $runLimit);
|
$recentRuns = self::buildRecentRuns($runEntries, $runLimit);
|
||||||
|
|
||||||
if ($windowHours > 0 && !empty($runEntries)) {
|
if ($windowHours > 0 && !empty($runEntries)) {
|
||||||
$latestSummary = self::aggregateRunSummaries($runEntries);
|
$latestSummary = self::aggregateRunSummaries(self::limitAggregateRunEntries($runEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($latestSummary) && !empty($recentRuns)) {
|
if (empty($latestSummary) && !empty($recentRuns)) {
|
||||||
|
|||||||
286
docs/2026-04-16-admin2-spider-workbench-production-fix.md
Normal file
286
docs/2026-04-16-admin2-spider-workbench-production-fix.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# 2026-04-16 Admin2 蜘蛛预览正式服排障记录
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
本次排障发生在正式服管理端 `https://admin2.jpjdxs.info/`。
|
||||||
|
|
||||||
|
用户反馈的问题分为三个阶段:
|
||||||
|
|
||||||
|
1. 管理端请求蜘蛛工作台接口时报 CORS 错误。
|
||||||
|
2. 前端在处理临时产物和缓存后,首页出现运行时异常,页面加载不稳定。
|
||||||
|
3. 页面恢复后,点击“蜘蛛预览”接口返回 `500`。
|
||||||
|
|
||||||
|
本次工作定位为正式服辅助分析、止血、优化与修复,开发主线仍由主 Codex 继续维护。
|
||||||
|
|
||||||
|
## 最终结论
|
||||||
|
|
||||||
|
这次问题不是单点故障,而是三层问题叠加:
|
||||||
|
|
||||||
|
1. 前端生产环境接口地址指向了跨域域名,导致浏览器先出现 CORS 报错。
|
||||||
|
2. 排障过程中过的脏 `dist` 与浏览器 `Service Worker` 缓存叠加,导致首页运行时异常。
|
||||||
|
3. 蜘蛛工作台接口在 24 小时窗口下同步聚合过多抓取摘要文件,触发 PHP 128MB 内存上限,最终返回 `500`。
|
||||||
|
|
||||||
|
## 线上现象
|
||||||
|
|
||||||
|
### 1. CORS 阶段
|
||||||
|
|
||||||
|
浏览器最初报错:
|
||||||
|
|
||||||
|
- 管理端来源:`https://admin2.jpjdxs.info`
|
||||||
|
- 被请求接口:`https://adapi.ra12.shop/site/domain/spider-crawl/workbench/summary?...`
|
||||||
|
- 浏览器错误:`No 'Access-Control-Allow-Origin' header is present on the requested resource`
|
||||||
|
|
||||||
|
### 2. 前端运行时阶段
|
||||||
|
|
||||||
|
首页曾出现运行时异常:
|
||||||
|
|
||||||
|
- `TypeError: Cannot read properties of null (reading 'nextSibling')`
|
||||||
|
|
||||||
|
这个阶段和手工改动过的前端产物以及旧缓存有关,不是蜘蛛工作台业务逻辑本身的问题。
|
||||||
|
|
||||||
|
### 3. 蜘蛛预览 500 阶段
|
||||||
|
|
||||||
|
页面恢复后,点击蜘蛛预览再次报错:
|
||||||
|
|
||||||
|
- 请求地址:
|
||||||
|
- `/api/site/domain/spider-crawl/workbench/summary?run_limit=10&anomaly_limit=80&window_hours=24&bot_scope=all&selected_bots=baiduspider,sogou`
|
||||||
|
- 浏览器错误:
|
||||||
|
- `AxiosError: Request failed with status code 500`
|
||||||
|
|
||||||
|
## 根因定位
|
||||||
|
|
||||||
|
### 根因一:前端生产环境接口配置为跨域域名
|
||||||
|
|
||||||
|
正式服前端原先在生产环境里直接请求外部域名,浏览器因此走了跨域模式。
|
||||||
|
|
||||||
|
关联文件:
|
||||||
|
|
||||||
|
- `VideoAdminV2/.env.production`
|
||||||
|
|
||||||
|
修复思路:
|
||||||
|
|
||||||
|
- 将前端生产环境接口基址改为同源 `/api`
|
||||||
|
- 由管理端 nginx 反向代理到后端服务
|
||||||
|
|
||||||
|
### 根因二:旧 `Service Worker` 与临时修改产物污染
|
||||||
|
|
||||||
|
在排障期间,浏览器端同时受到以下因素影响:
|
||||||
|
|
||||||
|
- 旧 `Service Worker` 缓存
|
||||||
|
- 旧静态资源哈希
|
||||||
|
- 临时手工调整过的 `dist`
|
||||||
|
|
||||||
|
这会导致首页有机会出现和真实源码不一致的运行时行为。
|
||||||
|
|
||||||
|
修复思路:
|
||||||
|
|
||||||
|
- 用 `nvm` 环境重新构建干净前端产物
|
||||||
|
- 关闭 PWA,避免继续注册新的 `Service Worker`
|
||||||
|
- 首页增加旧缓存清理逻辑
|
||||||
|
- 首页增加 `localStorage` 解析容错
|
||||||
|
|
||||||
|
### 根因三:蜘蛛工作台同步聚合过多摘要文件导致 PHP 内存耗尽
|
||||||
|
|
||||||
|
这是点击“蜘蛛预览”时 `500` 的真实根因。
|
||||||
|
|
||||||
|
日志证据来自:
|
||||||
|
|
||||||
|
- `/www/wwwlogs/source.video2.com.error.log`
|
||||||
|
|
||||||
|
关键错误:
|
||||||
|
|
||||||
|
- `Allowed memory size of 134217728 bytes exhausted`
|
||||||
|
- 涉及文件:
|
||||||
|
- `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php`
|
||||||
|
- `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlLogHelper.php`
|
||||||
|
|
||||||
|
当时请求示例:
|
||||||
|
|
||||||
|
- `GET /site/domain/spider-crawl/workbench/summary?run_limit=10&anomaly_limit=80&window_hours=24&bot_scope=all&selected_bots=baiduspider,sogou`
|
||||||
|
|
||||||
|
触发原因:
|
||||||
|
|
||||||
|
- `window_hours=24` 时会扫描大量 `crawl-logs.summary.json`
|
||||||
|
- 旧实现会把多份 summary 读入内存后进行聚合
|
||||||
|
- 正式服数据量明显大于开发环境
|
||||||
|
|
||||||
|
排障时观察到的数据规模:
|
||||||
|
|
||||||
|
- `storage/domain-spider-crawl/runs` 目录约 `47M`
|
||||||
|
- `crawl-logs.summary.json` 数量约 `154` 份
|
||||||
|
- 单个摘要文件最大接近 `958507` bytes
|
||||||
|
|
||||||
|
## 已执行的修复
|
||||||
|
|
||||||
|
### 一、管理端同源代理与 CORS 调整
|
||||||
|
|
||||||
|
#### 1. 管理端 nginx 增加 `/api/` 反向代理
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- `/www/server/panel/vhost/nginx/admin2.jpjdxs.info.conf`
|
||||||
|
|
||||||
|
处理效果:
|
||||||
|
|
||||||
|
- 管理端改为访问同源 `/api/...`
|
||||||
|
- 管理端不再直接跨域请求 `adapi.ra12.shop`
|
||||||
|
|
||||||
|
#### 2. 后端 CORS 头调整
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoSource2/code/app/admin/middleware/AdminCors.php`
|
||||||
|
|
||||||
|
处理效果:
|
||||||
|
|
||||||
|
- 补齐允许方法
|
||||||
|
- 补齐允许头
|
||||||
|
- 增加 `Vary: Origin`
|
||||||
|
|
||||||
|
## 二、前端正式服止血
|
||||||
|
|
||||||
|
### 1. 前端生产环境改为同源接口
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoAdminV2/.env.production`
|
||||||
|
|
||||||
|
关键配置:
|
||||||
|
|
||||||
|
- `VITE_API_URL=/api`
|
||||||
|
- `VITE_PWA=false`
|
||||||
|
|
||||||
|
### 2. 首页增加容错和旧缓存清理
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoAdminV2/index.html`
|
||||||
|
|
||||||
|
处理内容:
|
||||||
|
|
||||||
|
- `IOMS-global` 的 `localStorage` 读取加 `try/catch`
|
||||||
|
- 启动时注销旧 `serviceWorker`
|
||||||
|
- 清理旧缓存
|
||||||
|
|
||||||
|
### 3. 清理不再需要的启动期缓存逻辑
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoAdminV2/src/main.ts`
|
||||||
|
|
||||||
|
处理内容:
|
||||||
|
|
||||||
|
- 去掉启动时全量清理缓存的临时逻辑,避免继续引入不确定性
|
||||||
|
|
||||||
|
### 4. 使用 `nvm` 环境重新构建前端
|
||||||
|
|
||||||
|
确认到正式服 `nvm` 环境为:
|
||||||
|
|
||||||
|
- Node: `v20.19.6`
|
||||||
|
- npm: `10.8.2`
|
||||||
|
|
||||||
|
实际使用命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export HOME=/home/www
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
. "$NVM_DIR/nvm.sh"
|
||||||
|
cd /www/wwwroot/VideoAdminV2
|
||||||
|
nvm use 20
|
||||||
|
npm run build:pro
|
||||||
|
```
|
||||||
|
|
||||||
|
构建后线上主包为:
|
||||||
|
|
||||||
|
- `assets/js/index-23f333c2.js`
|
||||||
|
|
||||||
|
## 三、蜘蛛工作台 500 修复
|
||||||
|
|
||||||
|
核心修复文件:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php`
|
||||||
|
|
||||||
|
处理内容:
|
||||||
|
|
||||||
|
1. 增加聚合 run 上限:
|
||||||
|
- `MAX_AGGREGATE_RUNS = 12`
|
||||||
|
2. `collectRunEntries()` 不再保存整份 summary 内容,只保存:
|
||||||
|
- `summary_path`
|
||||||
|
- `normalized`
|
||||||
|
- `generated_at_ts`
|
||||||
|
3. `aggregateRunSummaries()` 改为按需读取文件,而不是提前把所有 summary 常驻内存
|
||||||
|
4. `buildSummary()` 在窗口聚合前先裁剪最近 run
|
||||||
|
5. `buildWindowComparison()` 也在聚合前先裁剪最近 run
|
||||||
|
|
||||||
|
这次修复的目标是正式服止血,不是最终态架构优化。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
### 1. 前端接口基址
|
||||||
|
|
||||||
|
已确认前端主包中接口基址为:
|
||||||
|
|
||||||
|
- `baseURL:"/api"`
|
||||||
|
|
||||||
|
### 2. CORS 头
|
||||||
|
|
||||||
|
已确认响应头包含:
|
||||||
|
|
||||||
|
- `Access-Control-Allow-Origin: https://admin2.jpjdxs.info`
|
||||||
|
- `Vary: Origin`
|
||||||
|
|
||||||
|
### 3. helper 在 128MB 内存下可跑通
|
||||||
|
|
||||||
|
排障时在本机使用命令验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -d memory_limit=128M -r '... DomainSpiderCrawlWorkbenchHelper::buildSummary(...) ...'
|
||||||
|
```
|
||||||
|
|
||||||
|
修复后返回正常,说明核心内存问题已被压住。
|
||||||
|
|
||||||
|
### 4. PHP-FPM 已重载
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
service php-fpm-83 reload
|
||||||
|
```
|
||||||
|
|
||||||
|
用于确保线上 PHP 进程加载到修复后的 helper 代码。
|
||||||
|
|
||||||
|
### 5. 用户回归验证
|
||||||
|
|
||||||
|
用户最终反馈:
|
||||||
|
|
||||||
|
- “好了终于跑通了”
|
||||||
|
|
||||||
|
说明这次正式服问题已经完成闭环。
|
||||||
|
|
||||||
|
## 对主 Codex 的交接建议
|
||||||
|
|
||||||
|
这次修复属于正式服止血方案,主 Codex 后续建议继续做结构化优化:
|
||||||
|
|
||||||
|
1. 不要在在线接口里同步扫描过多 run 文件。
|
||||||
|
2. 将蜘蛛工作台改造成预聚合模型,而不是请求时全量聚合。
|
||||||
|
3. 将 `1h / 6h / 24h / 72h` 窗口统计结果提前固化为摘要文件或缓存。
|
||||||
|
4. 为大窗口查询增加更清晰的降级策略与上限说明。
|
||||||
|
5. 如果后续还要放大统计维度,应考虑异步任务或离线聚合,不要继续依赖同步 `json_decode(file_get_contents(...))` 全量加载。
|
||||||
|
|
||||||
|
## 本次涉及文件
|
||||||
|
|
||||||
|
前端相关:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoAdminV2/.env.production`
|
||||||
|
- `/www/wwwroot/VideoAdminV2/index.html`
|
||||||
|
- `/www/wwwroot/VideoAdminV2/src/main.ts`
|
||||||
|
- `/www/server/panel/vhost/nginx/admin2.jpjdxs.info.conf`
|
||||||
|
|
||||||
|
后端相关:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoSource2/code/app/admin/middleware/AdminCors.php`
|
||||||
|
- `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php`
|
||||||
|
|
||||||
|
文档落点:
|
||||||
|
|
||||||
|
- `/www/wwwroot/VideoSource2/docs/2026-04-16-admin2-spider-workbench-production-fix.md`
|
||||||
Reference in New Issue
Block a user