博客 / 詳情

返回

被問性能後,我封裝了這個 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() 幫你把異常轉成純數據結構,方便推隊列或序列化。


實際使用示例

這裏只放偽代碼以示意,實際代碼見倉庫。

本地寫日誌

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

短生命週期場景(異步上報)

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

常駐框架(推薦同步或隊列)

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() 或推隊列再上報。
  • 如果你面臨的是極高併發的錯誤量:把上報放隊列,批量發送,或交由專門的採集基礎設施處理。
user avatar jidongdehai_co4lxh 頭像 user_ze46ouik 頭像 cipchk 頭像 opentiny 頭像 youngcoding 頭像
5 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.