404 监控的四个坑:计数翻倍、键名推导、点路径取值、与「自己看不到数据」

404 监控的四个坑:计数翻倍、键名推导、点路径取值、与「自己看不到数据」-知集
404 监控的四个坑:计数翻倍、键名推导、点路径取值、与「自己看不到数据」
此内容为付费阅读,请付费后查看
¥1
立即购买
您当前未登录!建议登陆后购买,可保存购买订单
付费阅读
阅读约 48 分钟14,342 字

摘要:给站点做了一套 404 访问监控:自定义表去重计数、后台明细与趋势、每日自动清理。功能不复杂,但从「写完」到「敢信它的数字」之间,踩了四个坑——一次请求被记了两次、后台开关开的和代码读的不是同一个键、配置里声明了定时任务却从未注册、以及管理员自己怎么测都看不到数据。其中第一个坑的根因完全不在 PHP 里,而在 nginx 配置,并且它会影响任何挂在 404 上的逻辑。本文逐一还原,附单请求复验法与一份零依赖交付代码。
适合阅读:做过埋点 / 统计 / 日志类功能,或正在排查「计数不对」的开发者。

一、先说清楚要什么

404 是「内容腐化」的最早信号:一篇文章改了 slug、一张图挪了目录、一次改版删了页面,搜索引擎和被引用的旧链接不会主动告诉你,但 404 表会。所以这个功能的目标很朴素:知道有哪些 URL 在被访问却不存在,然后决定是做 301 还是补内容。

要求落地方式
按 URL 去重计数表里 url 存原串、url_key 存 md5(url) 并建唯一键
记录上下文首次 / 最后时间、来源页(referer)、IP
不污染数据过滤 favicon / robots / sitemap / 静态资源与 AJAX
不被扫描器打爆当日唯一 URL 数量上限
不无限增长每日 cron 清理 N 天前的记录
能看到后台「工具 → 404监控」:今日明细 + 近 7 天趋势

功能本身没有难度。难点全部集中在「怎么确认它的数字是对的」——下面是四个真实的坑。

二、坑①:一次请求,被记了两次

功能上线后做最基础的验证:清掉某 URL 的旧记录,用 curl 发一次 404 请求,再去看计数。

curl -s -o /dev/null http://127.0.0.1/zhiji-m404t-a?x=1   # 只发一次
# 然后查表 → count = 2

一次请求记两次。第一反应当然怀疑自己的代码——是不是钩子挂了两次?是不是 template_redirect 被触发了两遍?查了一遍,没有。

根因:nginx 与 WordPress 的一次「接力」

把服务器的 nginx 配置拿出来看,两行配置叠在一起就出事:

# 全局 http 段
fastcgi_intercept_errors on;      # nginx 拦截 PHP 返回的错误状态码

# 站点 server 段
error_page 404 /404.html;          # 拦截后,去取这个静态页

关键在第三件事:/404.html 这个文件并不存在。

于是这个请求走了这样一条路:

步骤发生了什么
1请求 `/zhiji-m404t-a` → nginx 交给 PHP → WordPress 判定 404 → 返回 404
2全局开着的 `fastcgi_intercept_errors` 让 nginx 拦下这个 404
3按 `error_page 404 /404.html` 去取静态页 → 文件不存在
4回落到 `try_files` 的最后一个参数(`index.php`)→ WordPress 又跑了一整遍
5第二遍同样走到 `template_redirect` → 打点钩子再执行一次 → 计数 +2

所以不是「钩子挂了两次」,而是WordPress 真的被完整执行了两遍。

确认与修法

# 确认:清掉该 URL 记录后只发一次请求,看 count
curl -s -o /dev/null http://127.0.0.1/some-404-url
# count=2 → 命中本坑;count=1 → 与本坑无关

# 修法(二选一,改完必须复验)
# A. 注释掉站点配置里的 error_page 404 /404.html;(全局 intercept_errors 可保留)
# B. 或放一个真实的 /404.html,让 nginx 无需回落到 PHP
nginx -t && nginx -s reload

修完再跑一次同样的单请求复验 → count=1。

这个坑真正值得记住的地方:它不属于 404 监控。任何挂在 404 上的逻辑——埋点、广告统计、限流、自动跳转、外链追踪——在这台服务器上都会被执行两遍。遇到「某个统计总是翻倍」,先数一数请求真正被处理了几次,再去怀疑业务代码。

三、坑②:后台开关开的,和代码读的,不是同一个键

第二个坑更隐蔽:后台把「启用404监控」打开了,前台却完全不记录,不报错、不留日志、后台明明显示已开启。

根因:一个「智能到出错」的默认推导

项目的模块框架会为每个模块自动推导一个选项键,规则是「模块名 + _enabled」。对大多数模块没问题,但这个模块叫 Monitor404——名字里带数字:

模块 Monitor404  →  默认推导  monitor404_enabled
                    实际使用的键  monitor_404_enabled
                                          ↑ 数字前多一个下划线

两个键都是合法字符串,谁也不会报错。后台表单写的是 monitor_404_enabled,代码读的是推导出来的 monitor404_enabled——一个写、一个读,永远对不上。

修法:键名不许推导,必须显式声明

// 模块元信息里显式声明选项键,不走推导
// (后台字段的 id 一个字符都不能改——已有站点的数据在它名下)
'option' => 'monitor_404_enabled',

教训:凡是「按名字推导」产生的标识符,都要能显式覆盖。命名规则一旦遇到例外(数字、缩写、历史遗留键),推导就会静默跑偏。而且这类 bug php -l、各种 lint 全都查不出来——两个键都是合法字符串。只有「后台开一下、前台看有没有生效」这种端到端动作能发现它。

四、坑③:配置里声明了定时任务,但它从未被注册

这个模块有一个每日清理任务(删除 30 天前的记录)。声明方式是在模块契约里写一行:

'contract' => array(
    'schedules' => array(
        array('hook' => 'zhiji_404_log_cleanup', 'interval' => 'daily'),
    ),
),

按理说,框架会读这份契约,把它同步成真正的 WP-Cron 任务(模块关掉时幽灵任务自动消失)。但它从来没有被注册过。

根因:框架自己一个「取值函数」的缺陷

框架读嵌套配置用的是一个路径取值工具,调用长这样:

$schedules = zhiji_arr_get($def, 'contract.schedules');   // 期望拿到那个数组

而那个工具不支持点路径——它把 'contract.schedules' 当成一个完整的字面键名去数组里找,当然找不到,恒返回空。于是「声明了定时任务」这件事在框架眼里等于没写,cron 同步、依赖校验两条链路一起静默失效。

修法是逐层取值:

// ✗ 不支持点路径:恒空
$schedules = zhiji_arr_get($def, 'contract.schedules');

// ✓ 逐层取
$contract  = isset($def['contract']) && is_array($def['contract']) ? $def['contract'] : array();
$schedules = isset($contract['schedules']) ? $contract['schedules'] : array();

这个坑的启示:一个「通用的取值小工具」如果语义和自己名字暗示的不一样,它就会成为整个项目里最隐蔽的 bug 源——因为所有调用方都以为自己是对的。发现它之后,我把项目里所有用点路径的调用点都过了一遍(这个模块的 schedules 与 uses 两处首当其冲)。

另外,这个坑是被 404 模块暴露出来的,但受害者不止它——任何在契约里声明了 schedules / uses 的模块,当时都一起失效了。所以修完框架,要把「谁用了这个入口」全部回归一遍。

五、坑④:管理员自己怎么测,都看不到数据

第四个坑不算 bug,但它是最容易被误判成 bug 的一个:功能上线后自己去访问一个不存在的地址,刷新后台 → 空的。

三个叠加的「善意设计」

机制本意后果
登录用户默认不统计避免自己调试时污染数据管理员就是登录用户 → 自己测永远没数据
静态资源白名单过滤 favicon / robots / 图片的常见误报随手测了个 /a.png 也不会记录
AJAX 不记录后台 AJAX 的 404 是插件在试探,不是内容腐化走了 ajax 路径的测试也看不到

三个都是对的,叠起来就是「你怎么测都看不到东西」。正确做法是:先说清楚这三个前提,再给测试留一条明确的路——把「统计登录用户」做成独立开关(默认关,测的时候打开),并且用匿名请求(curl 不带 Cookie)来验证基本链路。

教训:默认值为「静默地不做事」时,一定要给验证留一条后门(一个开关、或一句「用匿名 curl 测」的说明)。否则下一个接手的人会花半小时怀疑代码,而问题只是「你自己的账号被豁免了」。

六、这类功能该怎么验证

统计类功能的验证有个特点:「页面能打开」完全不能说明数字是对的。这次用的是一组分层断言,按能发现的问题从粗到细:

断言做法能发现
单请求计数清旧记录 → curl 一次 → 查 count重复执行 / 钩子挂两次(坑①)
白名单连打 4 个静态资源 URL → 查表行数应为 0过滤规则失效
登录豁免带 Cookie 请求 → 应不记录;开开关后 → 应记录豁免开关接反(坑④)
上限保护灌到当日上限 → 应停止记录防爆逻辑失效
清理造一条 31 天前的行 + 一条今天的行 → 跑 cron → 只删旧的时间段边界写错
后台页真登录抓 tools.php?page=… → 200 且含明细表权限 / 注册问题

其中「单请求计数」这条最值得先做。它用一个动作同时排除了「钩子重复」和「请求被处理两遍」两类完全不同的原因——如果不是 1,再往下查是前者还是后者。

附:同系列文章

本文属于「zibll 子主题开发实录」系列,同系列其它篇目见文末导航。


📚 zibll 子主题开发实录(共 22 篇) · 系列总目录与阅读路径 →

① 前端性能与加载

② 排版与样式系统

③ 功能实现与边界

④ 系统与运维

⑤ 成就与活动系统

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容