被问性能后,我封装了这个 PHP 错误上报工具

最近我把自己常用的一套错误上报逻辑封装成了一个 Composer 包,叫 hejunjie/lazylog

功能很简单也很实用:安全地写本地日志 + 把异常信息上报到远端(支持同步/异步) 。本文讲讲为什么我要做这个库、实现思路、在不同运行环境下如何选择(以及我推荐的优化方案)。


起因:为啥要做这个工具?

先讲个背景。之前我写了一个 Go 项目 ------ oh-shit-logger,目标是把不同语言、不同项目里的错误集中收集到一个地方。Go 做服务天然快、部署也简单: GitHub Actions自动打包,我只要把包丢主机上一键启动就好了。

但上线后有朋友问:

"PHP 上报错误会不会太耗性能?网络 I/O 会不会成为瓶颈?"

这是个很合理的问题。网络 I/O 的确有成本,但异常本身在多数系统里不是那种持续不断、高频率的事件(如果异常多到经常并发,那系统可能已经在出问题了)。

与其空谈"会不会慢",我更愿意把常用做法封装一下,直接给出一个实战好用的方案------于是 hejunjie/lazylog 诞生了。


思路概览:伪异步 + 可回退的同步

lazylog 的核心思路很简单:

  • 本地写日志:线程安全、支持按行数/大小自动切分,长期运行不会把单个日志文件撑爆。
  • 远程上报 :提供两种方式:
    • 异步上报(伪异步) :通过 proc_open()exec() fork 出一个 PHP CLI 子进程来发送 HTTP POST,不阻塞主进程。适用于 PHP-FPM、一次性 CLI 脚本等短生命周期环境。
    • 同步上报:直接在当前进程做一个带超时的 HTTP POST,适合常驻内存框架(Webman、Swoole、RoadRunner 等)或需要保证上报结果的场景。

我把这些行为都封装在一个很小的包里:composer require hejunjie/lazylog,在任何项目里都能快速复用。


异步实现细节:为什么是"伪异步"?

PHP 没有内置线程(除非用扩展),但我们可以通过子进程实现"非阻塞式"的上报:

  • proc_open():启动子进程并可拿到 stdin/stdout/stderr,控制能力强;但会创建管道资源,需要注意关闭管道以免资源泄露。
  • exec():简单粗暴,把命令交给 shell 去做 fork,父进程可立即返回(命令后面加 &)。语义上更轻量,但控制能力弱。

两者的本质都是 fork 一个新进程去跑 PHP CLI,然后子进程读取临时文件(或者接收传参)、发 POST、删临时文件、退出。主进程不会等子进程走完就返回给用户,所以对用户体验几乎零影响。

优点 :实现简单、跨平台、即插即用;适合错误信息本身不高频的场景。
缺点:在"极高并发"场景下(比如每秒上千条错误)会比较吃资源,子进程启动和网络请求仍然有成本。


常驻内存框架(Webman/Swoole)该怎么办?

这是个重要的实践问题:在常驻内存框架中,我更推荐用同步上报或队列,而不是频繁 fork 子进程。

原因很直观:

  • 常驻框架的 Worker 是长期存在的,fork 子进程会带来额外的资源管理问题(僵尸进程、内存增长、文件描述符等)。
  • 同步上报虽然会阻塞当前 Worker,但只影响当前 Worker,不会像在传统短生命周期中影响整个请求模型。对于大多数低频异常而言,这个阻塞代价是可以接受的。
  • 更稳妥的做法是:把异常先格式化成数组,投递到队列,由专门的队列 worker 来异步上报。这样既避免了直接 fork,又能在不影响主流程的情况下批量/可靠地上报。

我在包里同时提供了 reportSync()(同步上报)和 reportAsync()(伪异步上报),并提供 Logger::formatThrowable() 帮你把异常转成纯数据结构,方便推队列或序列化。


实际使用示例

这里只放伪代码以示意,实际代码见仓库。

本地写日志

php 复制代码
Logger::write('/var/logs', 'error/app.log', 'Task Failed', ['msg' => 'something wrong']);

短生命周期场景(异步上报)

php 复制代码
try {
  // ...
} catch (Throwable $e) {
  Logger::reportAsync($e, 'https://your-collector/collect', 'my-project');
}

常驻框架(推荐同步或队列)

php 复制代码
try {
  // ...
} catch (Throwable $e) {
  // 同步上报(简单、直接)
  Logger::reportSync($e, 'https://your-collector/collect', 'my-project');

  // 或者:转成数组,投递队列,由 Worker 负责上报(推荐)
  $payload = Logger::formatThrowable($e, 'my-project');
  Queue::push('error_report', $payload);
}

性能那些事儿

有人担心"网络 I/O 会把 PHP 卡死"。我的观点是:

  • 错误本身通常是低频事件。如果你的系统错误频率高到持续占用大量带宽/请求,那说明系统正常运行已经有更严重的问题了。
  • 对于多数业务,一次 fork 一个子进程并做一次 HTTP POST 的开销在可接受范围,用户体验影响极小。
  • 在对性能要求极苛刻或错误量非常大的场景,正确做法是把上报变成队列 + 批量发送或将上报移动到专门的后端处理链路,而不是在业务路径里频繁 fork。

总之:衡量利弊后选择适合你业务的方式lazylog 提供了两端(sync/async)以及格式化功能,方便你按需设计。


最后

我把它做成 composer 包的原因很直接:我希望 快速把 PHP 项目的错误上报到我自己的 Go 服务(oh-shit-logger) ,而不是每个项目都重复造轮子。把常用逻辑抽出来,项目里 composer require hejunjie/lazylog 就能统一上报方式------既省事又稳妥。

如果你想快速了解这个项目:Zread 解析文档

  • 如果你是在 PHP-FPM / CLI 的短生命周期环境:reportAsync() 很方便,能保证主流程不被阻塞。
  • 如果你是在 Webman/Swoole 等常驻内存框架 :优先考虑 reportSync() 或推队列再上报。
  • 如果你面临的是极高并发的错误量:把上报放队列,批量发送,或交由专门的采集基础设施处理。
相关推荐
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082855 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe5 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5