它的本质是:一种基于随机抽样 (Random Sampling)的惰性垃圾回收策略。由于 PHP 是同步阻塞模型,GC 操作(遍历目录、stat 文件、unlink)是昂贵的 I/O 操作。为了避免每次请求都执行 GC 导致性能抖动,PHP 采用“掷骰子”的方式,以极低的概率在普通请求中“夹带”GC 任务,从而将清理成本分摊到大量请求中,实现最终一致性 (Eventual Consistency)的会话清理。
如果把 Session GC 比作商场保洁:
- 理想情况:每秒钟都有专职保洁员打扫所有垃圾桶(实时 GC)。->成本极高,影响顾客体验(性能损耗)。
- PHP 的策略:没有专职保洁员。每当有 100 个顾客(请求)进门时,随机抽取 1 个顾客,让他顺便把过期的垃圾扔了(概率触发 GC)。
gc_probability = 1gc_divisor = 100- 结果:平均每个请求承担 1% 的 GC 开销。虽然某个特定时刻可能有少量过期 Session 残留,但长期来看,垃圾会被清理干净。
- 核心逻辑:用“即时性”换取“平均性能”。牺牲少量的实时清理精度,换取系统整体的高吞吐和低延迟。
一、概率算法:gc_probability/gc_divisor
1. 公式解析
if(rand(1,gc_divisor)<=gc_probability){run_gc();}gc_probability(默认 1):分子。表示触发 GC 的权重。gc_divisor(默认 100):分母。表示采样基数。- 触发概率:P = 1 100 = 1 % P = \frac{1}{100} = 1\%P=1001=1%。
2. 为什么是概率?
- 避免惊群效应:如果每次请求都检查 GC,高并发下所有进程同时遍历目录,会导致磁盘 I/O 飙升,CPU 等待,响应时间剧烈波动。
- 平滑负载:将 GC 的计算和 I/O 成本均匀分散到时间轴上。
- 简单高效:
rand()函数开销极小,判断逻辑几乎为零。
💡 核心洞察:这不是真正的“定时任务”,而是一种“统计意义上的清理”。它不保证过期 Session 立即消失,只保证它们最终会消失。
二、执行流程:GC 内部发生了什么?
当概率命中时,PHP 执行以下步骤:
1. 打开 Session 保存路径
- 读取
session.save_path配置的目录(如/var/lib/php/session)。
2. 遍历目录 (Directory Iteration)
- 使用
opendir()和readdir()逐个读取文件名。 - 瓶颈:如果目录下有几十万甚至上百万个 Session 文件,这一步非常慢。
3. 检查过期 (Stat & Compare)
- 对每个文件执行
stat()系统调用,获取最后修改时间 (mtime)。 - 计算年龄:
current_time - mtime。 - 判断:如果
年龄 > session.gc_maxlifetime(默认 1440 秒),标记为过期。
4. 删除文件 (Unlink)
- 执行
unlink()删除过期文件。 - 注意:
unlink也是 I/O 操作,频繁删除小文件会导致文件系统碎片化。
5. 完成
- GC 结束,继续处理当前的 Session 读写请求。
三、性能陷阱:文件存储的阿喀琉斯之踵
1. 目录膨胀问题
- 场景:高并发网站,每天产生 100 万 Session。
- 问题:单个目录下文件过多。
readdir变慢。stat变慢。- 文件系统 inode 耗尽。
- 后果:一旦触发 GC,该请求可能耗时数秒甚至超时,导致用户感知卡顿。
2. 概率失效问题
- 场景:低流量网站,每天只有 10 个请求。
- 问题:
1/100的概率意味着平均每 100 次请求才触发一次 GC。如果一天只有 10 次请求,可能连续 10 天都不触发 GC。 - 后果:过期 Session 堆积如山,占用磁盘空间,且存在安全风险(会话劫持窗口期变长)。
3. 竞争条件 (Race Condition)
- 场景:多个 PHP-FPM 进程同时命中概率,同时执行 GC。
- 后果:多个进程同时遍历和删除同一批文件,造成不必要的 I/O 争用和 CPU 浪费。
四、现代替代方案:超越文件 GC
鉴于文件 Session GC 的局限性,现代架构通常采用以下策略:
1. 禁用 PHP 内置 GC,使用外部清理
- 配置:
(设置 probability 为 0 即可完全禁用)session.gc_probability = 0 session.gc_divisor = 1 - 替代方案:
- Cron Job:编写 Shell 脚本,每天凌晨低峰期执行
find /var/lib/php/session -type f -mmin +1440 -delete。 - 优势:可控时间,避免高峰期内耗。
- Cron Job:编写 Shell 脚本,每天凌晨低峰期执行
2. 迁移到 Redis/Memcached (推荐)
- 原理:利用内存数据库的原生 TTL (Time-To-Live) 机制。
- 优势:
- 自动过期:Redis 内部有高效的惰性删除和定期删除算法,无需应用层干预。
- 无 I/O 瓶颈:内存操作,微秒级。
- 无目录膨胀:Key-Value 结构,不受文件数量限制。
- 配置:
session.save_handler = redis session.save_path = "tcp://127.0.0.1:6379"
3. 使用 Systemd Timer
- 原理:利用 Linux systemd 的定时器单元,定期触发清理脚本。
- 优势:比 Cron 更精确,有日志记录,依赖管理更好。
🚀 总结:原子化“Session GC”全景图
| 维度 | PHP 内置文件 GC | Redis TTL | Cron/Systemd 清理 |
|---|---|---|---|
| 触发机制 | 概率随机 (Probabilistic) | 原生自动 (Native) | 定时确定性 (Deterministic) |
| 性能影响 | 高 (I/O 密集,不可控) | 极低 (内存操作) | 低 (离线执行) |
| 实时性 | 差 (可能延迟很久) | 好 (近似实时) | 中 (取决于间隔) |
| 扩展性 | 差 (单目录瓶颈) | 极好 (集群支持) | 中 (需处理分布式锁) |
| 适用场景 | 小型单机应用 | 中大型/分布式应用 | 无法使用 Redis 的场景 |
| 隐喻 | 随机抽查的保洁 | 自毁装置的垃圾 | 夜间集中清运 |
终极心法:
PHP Session GC 的本质,是“低成本妥协”。
它在单机、低并发下工作良好,但在高并发下是性能杀手。
别依赖概率来保证清洁,要用确定的机制(TTL 或 Cron)来管理生命周期。
于随机中见权衡,于确定中见可靠;以机制为尺,解清理之牛,于架构演进中,求高效之真。
行动指令:
- 检查配置:
php -i | grep session.gc_查看当前概率和最大生存时间。 - 评估现状:如果你的 Session 存储在文件中且流量较大,立即计划迁移到 Redis。
- 禁用内置 GC:如果必须用文件存储,设置
session.gc_probability = 0。 - 设置外部清理:添加一个 Cron 任务,每天清理一次过期 Session 文件。
- 思维升级:记住,概率适合用于负载均衡和缓存淘汰,但不适合用于需要强一致性的资源清理。