在現(xiàn)代 PHP 應(yīng)用開(kāi)發(fā)中,將耗時(shí)任務(wù)(如郵件發(fā)送、報(bào)表生成、數(shù)據(jù)同步)從主請(qǐng)求流程中剝離出來(lái)進(jìn)行異步處理,是提升用戶(hù)體驗(yàn)和系統(tǒng)吞吐量的關(guān)鍵。雖然 RabbitMQ、Kafka 等專(zhuān)業(yè)消息隊(duì)列是標(biāo)準(zhǔn)解決方案,但對(duì)于中小型項(xiàng)目或追求輕量級(jí)架構(gòu)的場(chǎng)景,我們往往希望找到一個(gè)更“接地氣”的方案。
本文記錄了一次完整的技術(shù)探索之旅:從一個(gè)簡(jiǎn)單的想法開(kāi)始,如何利用 PHP 生態(tài)中已有的工具,構(gòu)建一個(gè)能夠應(yīng)對(duì)“大量寫(xiě)入、少量消費(fèi)”場(chǎng)景的高性能隊(duì)列。
需求起點(diǎn):一個(gè)輕量級(jí)的任務(wù)隊(duì)列
我們的核心需求很簡(jiǎn)單:
1. 輕量級(jí):不希望引入新的、重型的服務(wù)依賴(lài)(如 Java/Erlang 系的消息中間件)。
2. 可靠:任務(wù)不能輕易丟失。
3. 高性能:能夠承受 Web 應(yīng)用在高并發(fā)下的大量任務(wù)寫(xiě)入。
很自然地,我們的第一個(gè)想法落在了 SQLite 身上。它無(wú)需額外服務(wù)、零配置、就是一個(gè)文件,完美符合“輕量級(jí)”的要求。
還有一個(gè)需求,這個(gè)針對(duì)的是單機(jī)內(nèi)部的隊(duì)列,比如日志記錄,日志先記錄到隊(duì)列中,再通過(guò)消費(fèi)者統(tǒng)一記錄到集中機(jī)器上。
思路一:SQLite 作為隊(duì)列的初步嘗試與瓶頸
使用 SQLite 做隊(duì)列的思路非常直接:生產(chǎn)者 (PHP-FPM) 在 Web 請(qǐng)求中向 tasks 表 INSERT 一條記錄;消費(fèi)者 (CLI) 則是一個(gè)后臺(tái)腳本,循環(huán) SELECT 任務(wù)來(lái)處理。
然而,當(dāng)面臨“大量寫(xiě)入、少量消費(fèi)”的場(chǎng)景時(shí),瓶頸很快出現(xiàn):寫(xiě)入并發(fā)。SQLite 在默認(rèn)模式下,任何時(shí)刻只允許一個(gè)寫(xiě)入者。當(dāng)成百上千個(gè) PHP-FPM 進(jìn)程同時(shí)嘗試插入任務(wù)時(shí),會(huì)激烈地爭(zhēng)奪數(shù)據(jù)庫(kù)的寫(xiě)鎖,導(dǎo)致大量請(qǐng)求失敗或超時(shí)。
基礎(chǔ)優(yōu)化
為了解決并發(fā)問(wèn)題,一些基礎(chǔ)優(yōu)化是必須的:
1. 開(kāi)啟 WAL 模式:PRAGMA journal_mode=WAL;,允許一個(gè)寫(xiě)入者和多個(gè)讀取者并發(fā),極大緩解了鎖爭(zhēng)用。
2. 設(shè)置超時(shí):PRAGMA busy_timeout = 5000;,讓寫(xiě)入失敗的進(jìn)程等待一段時(shí)間而不是立即報(bào)錯(cuò)。
3. 使用高速磁盤(pán) (SSD):從物理層面提升 I/O 性能。
這些優(yōu)化能解決大部分問(wèn)題,但如果寫(xiě)入壓力達(dá)到極限,我們還能做什么?
思路二:探索純內(nèi)存方案
為了徹底消除磁盤(pán) I/O 瓶頸,我們自然想到了內(nèi)存。
方案 A:APCu - 一個(gè)美麗的陷阱
APCu 是 PHP 的一個(gè)共享內(nèi)存緩存。我們可以用一個(gè)共享數(shù)組來(lái)當(dāng)隊(duì)列。但這很快暴露了新問(wèn)題:
CPU 爭(zhēng)用:為了保證并發(fā)安全,我們必須使用 apcu_cas (Compare-And-Swap) 循環(huán)來(lái)寫(xiě)入。在高并發(fā)下,大量進(jìn)程會(huì)在此處“忙等待” (Busy-Waiting),瘋狂空轉(zhuǎn) CPU,導(dǎo)致系統(tǒng)負(fù)載飆升。
數(shù)據(jù)易失:服務(wù)重啟,內(nèi)存中的所有任務(wù)全部丟失。
結(jié)論:APCu 方案在高并發(fā)寫(xiě)入下性能不佳且有數(shù)據(jù)丟失風(fēng)險(xiǎn),不推薦。
方案 B:Redis - 專(zhuān)業(yè)選手,但增加了依賴(lài)
Redis 是內(nèi)存方案的工業(yè)標(biāo)準(zhǔn)。它使用高效的事件循環(huán)模型,提供原子的列表操作 (LPUSH/BRPOP),性能卓越且功能豐富。它也支持?jǐn)?shù)據(jù)持久化。唯一的“缺點(diǎn)”是,它違背了我們最初“不引入新服務(wù)”的原則。
思路三:兩全其美?SQLite on tmpfs
有沒(méi)有一種方案,既能擁有內(nèi)存的極致速度,又能利用 SQLite 成熟的并發(fā)模型,還無(wú)需修改代碼?答案是:將 SQLite 數(shù)據(jù)庫(kù)文件放在內(nèi)存文件系統(tǒng) (tmpfs) 中。
在 Linux 中,tmpfs 是一個(gè)基于內(nèi)存的文件系統(tǒng)(/dev/shm 就是一個(gè)現(xiàn)成的例子)。我們可以將 SQLite 數(shù)據(jù)庫(kù)文件創(chuàng)建在這里。
優(yōu)點(diǎn)
- 極致性能:所有數(shù)據(jù)庫(kù)操作,包括加鎖、讀寫(xiě),都變成了內(nèi)存操作,速度極快。
- 健壯的并發(fā)模型:我們依然享受著 SQLite 優(yōu)秀的“阻塞等待”并發(fā)模型,沒(méi)有 APCu 的 CPU 空轉(zhuǎn)問(wèn)題。
- 零代碼修改:PHP 代碼無(wú)需任何改動(dòng),只需改變數(shù)據(jù)庫(kù)文件的路徑。
缺點(diǎn)
- 數(shù)據(jù)完全易失:和所有內(nèi)存方案一樣,服務(wù)器重啟,數(shù)據(jù)灰飛煙滅。這個(gè)方案非常適合任務(wù)可再生的場(chǎng)景(如生成縮略圖、更新緩存),但不適用于關(guān)鍵業(yè)務(wù)(如訂單處理)。
最終方案:在 Docker 中優(yōu)雅地實(shí)現(xiàn) "SQLite on tmpfs"
在現(xiàn)代化的 Docker 開(kāi)發(fā)環(huán)境中,實(shí)現(xiàn)這個(gè)方案變得異常簡(jiǎn)單和優(yōu)雅。我們無(wú)需手動(dòng)在服務(wù)器上執(zhí)行 mount 命令,只需在 docker-compose.yml 中聲明即可。
# File: docker-compose.yml
version: '3.8'
services:
php-fpm:
build: ./php
volumes:
- ./src:/app
# 關(guān)鍵配置:聲明一個(gè) tmpfs 掛載
tmpfs:
# 推薦方案:精確設(shè)置所有者
- /app/ramdisk:size=256M,uid=33,gid=33
# ... 其他服務(wù)
權(quán)限設(shè)置:專(zhuān)業(yè)做法 vs. 粗暴捷徑
由于 PHP-FPM 進(jìn)程通常以低權(quán)限用戶(hù)(如 www-data)運(yùn)行,而 tmpfs 默認(rèn)由 root 創(chuàng)建,直接寫(xiě)入會(huì)因權(quán)限不足而失敗。
專(zhuān)業(yè)做法(推薦):通過(guò) uid=33,gid=33(www-data 的典型 ID),我們精確地將內(nèi)存目錄的所有權(quán)交給了 PHP 進(jìn)程,這遵循了安全的最小權(quán)限原則。
粗暴捷徑(不推薦):使用 mode=0777。這會(huì)將目錄權(quán)限設(shè)為 rwxrwxrwx,允許容器內(nèi)的任何用戶(hù)寫(xiě)入。雖然能解決問(wèn)題,但它過(guò)度授權(quán),留下了安全隱患。
總結(jié)
由此來(lái)看,如果我們不通過(guò)Redis或其他專(zhuān)業(yè)的消息隊(duì)列服務(wù),同時(shí)要處理的任務(wù)是在單機(jī)內(nèi)的,不需要遠(yuǎn)程互聯(lián),那么我們完全可以通過(guò)sqlite實(shí)現(xiàn),再配合linux開(kāi)啟內(nèi)存文件系統(tǒng),避免觸發(fā)磁盤(pán)IO。雖然無(wú)論從寫(xiě)入、更新、刪除、讀取等任何方面,Redis都更專(zhuān)業(yè),性能更好,但我們的方案是更簡(jiǎn)單的,同時(shí)可以利用sql表結(jié)構(gòu),很省事的實(shí)現(xiàn)隊(duì)列的需求。
原文標(biāo)題: PHP 高性能隊(duì)列探索:從 SQLite 到內(nèi)存,我們?cè)撊绾芜x擇?
原文地址: https://phpreturn.com/index/a68d9e185d81e3.html
原文平臺(tái): PHP武器庫(kù)
版權(quán)聲明: 本文由phpreturn.com(PHP武器庫(kù)官網(wǎng))原創(chuàng)和首發(fā),所有權(quán)利歸phpreturn(PHP武器庫(kù))所有,本站允許任何形式的轉(zhuǎn)載/引用文章,但必須同時(shí)注明出處。