李涛自留地
回到顶部
首页 > php学习 > php session机制是怎么跑起来的,我拆开看了一遍

php session机制是怎么跑起来的,我拆开看了一遍

文章目录

上周四晚上十点多,我还在改一个后台登录的 bug,用户登录成功不到两秒就被踢回登录页,刷新一次还是一样。第一反应是 cookie 没写进去,翻完前端代码没发现问题,索性登上服务器,把 php session机制 从读文件到回收整个翻了一遍,才找到根因。这篇把过程记下来,省得下次白跑。

一次登录失败,把我带到 session 文件落在哪个目录

我在测试机上复现,登录接口返回 200,响应头里的 cookie 也带着 PHPSESSID,可下一个请求就是读不到用户。php session机制 这块,当时我以为前端把 cookie 的 domain 写错了,结果发现是后端每次都在新建会话。

目录里成排的会话文件,其中一张被抽出,php session机制

判断依据很直接:同一个浏览器连着发两次请求,session id 都不一样。id 一旦换了,等于换了一个新会话,之前写的登录态全丢。

第一步,看 php 把会话存在哪儿。先用 php --ini 确认这次加载的是哪份 php.ini——php-fpm 和命令行走的不是同一份,CLI 里 /etc/php/8.x/cli/php.ini,fpm 在 /etc/php/8.x/fpm/php.ini,版本号按你机器上的来。确认完再过滤两个配置项,输出大概长这样,重点看 save_path 后面那两个值,本地值和主值要一致。别直接拿 CLI 的输出当成线上结论,我在这上面白跑过一次。

php -i | grep -E 'session.save_path|session.save_handler'

默认的 files 处理器就是一个目录,一个会话一个文件,文件名是 sess_ 加 session id。php session机制 的 id 由 php 生成,长度和字符集跟 session.sid_length、session.sid_bits_per_character 有关,不同版本默认值不同,以官方文档为准。

目录里文件堆多了也难受,save_path 可以写成带层级的形式,比如 2;/var/lib/php/sessions,php 会在下面按 0-9a-f 铺两层子目录。我第一次见到还以为是有人手动分的,其实那个数字是配置项在起作用。

第二步,进目录看文件本身。ls 一行就够,重点看属主和权限那两列。

ls -l /var/lib/php/sessions | head -n 5

我看到一堆 sess_ 开头的文件,其中有几个属主是 root、权限 600。php-fpm 跑在 www-data 下,读不了 root 的文件,session_start 拿不到内容,只能当新会话处理。原因是我之前用 root 跑过一次命令行脚本,顺手把文件建成了 root 的。

会话是怎么读写的:一个 id 换一个文件

请求进来,php 从 cookie 取 PHPSESSID,拼成 sess_xxx,打开文件读出来反序列化进 $_SESSION,脚本结束时再写回去。php session机制 的整个过程对你是透明的,文件在请求期间一直开着。

文件里存的是纯文本,一行一个键,格式是键名加竖线再加序列化的值。调试时直接 cat 一个 sess_ 文件比打印 $_SESSION 快,但别在生产上改,写坏了会话就废。

终端与进程树里排队等一把文件锁的请求,php session机制

老实说这里有个坑我吃过。files 处理器在 session_start 时会加锁,同一个 session id 的并发请求要排队。我当时以为列表页慢是 SQL 的问题,结果发现页面上四个 ajax 共用一个会话,全卡在锁上。

一段逻辑用完会话就不再动它,可以早点关掉,把锁让出去。

<?php
session_start();
$_SESSION['uid'] = 42;
// 数据写完就释放文件锁,后面的耗时逻辑不再阻塞
session_write_close();

注意关掉之后 $_SESSION 还能读,但改了不会落盘,要改得再 session_start 一次。这一步很容易漏,我吃过亏。

这也是 php session机制 里最反直觉的地方:慢的不是读写,是等锁。你排查接口耗时的时候,如果发现同一用户的请求串行执行,先怀疑它。

过期时间与 gc 触发,跟你想象的不太一样

session.gc_maxlifetime 默认 1440 秒,超过这个时间没被写过的会话算过期。判断依据是文件的 mtime,不是创建时间。我原以为是按创建时间算,结果手动 touch 一下,过期时间就往后推了。

清理不是定时跑,是概率触发。gc_probability 除以 gc_divisor,默认 1/100,也就是每次 session_start 有百分之一的机会顺手扫一遍目录,具体默认值以官方文档为准。

还有个常被搞混的点:session.cookie_lifetime 管的是浏览器那边 cookie 活多久,gc_maxlifetime 管的是服务器上文件留多久。在 php session机制 里,这两件事互不影响,cookie 设成关闭浏览器就失效,文件照样躺到过期为止。

最容易忽略的是:gc 只认自己那套 gc_maxlifetime,目录却是大家共用的。三个站点共用一个 save_path,各自配的过期时间不同,短的那个会把长的那边的会话删掉。php session机制 这块我踩过,用户莫名掉线就是这么来的。

堆满过期会话文件的缓存槽,通道被堵住,php session机制

所以要么每个站点分目录,要么把清理交给系统。Debian 系的包一般把 gc_probability 设成 0,改用 cron 或 systemd-tmpfiles 清 /var/lib/php/sessions,你改 php.ini 时记得确认这条还在。

想看一眼堆了多少过期文件,用 find 按修改时间筛,输出是一行行列出来的路径,重点看行数。

find /var/lib/php/sessions -type f -mmin +60 | wc -l

输出就是一个数字,一行而已,看不出是哪天堆的,但能判断清道夫有没有在干活。

这个数字一直涨,说明你在用的那套清理根本没跑起来,别急着删,先确认是谁负责清。php session机制 的目录一旦膨胀到几十万个小文件,ls 本身都会变慢。

几个我踩过的坑

容器里跑 php,会话文件写在容器自己的 /tmp 或 /var/lib/php/sessions,副本一多,用户在 A 副本登录、请求打到 B 副本就没会话了。要么挂共享卷,要么换处理器。

多机部署还有个更隐形的:负载均衡把同一用户的请求轮着分发,会话却没共享,表现就是有时能进有时进不去。看着像随机掉线,其实取决于这次命中哪台机器。

换处理器不难,前提是先装并启用 redis 扩展,pecl install redis 或发行版的 php-redis 包都行,用 php -m | grep redis 确认加载成功,再把 session.save_handler 设成 redis、save_path 写 tcp://127.0.0.1:6379,参数以官方文档为准。扩展没装就改这两项,session_start 会直接报找不到 save handler,整站登录一起挂。换完文件锁那套也没了,并发模型跟着变。

还有个开关值得开:session.use_strict_mode,打开后 php 不再接受自己没生成过的 id,能挡掉一部分会话固定攻击。默认是关的,我现在的项目都开着。

我现在上线前会做的两件事

看一眼 save_path 的属主,确认 php-fpm 的运行用户能读写;再看一眼 php session机制 的清理归谁管,是 php 自己按概率扫,还是系统定时清。

会话平时不出声,出问题就是登录态莫名丢失,排查起来很费劲。这套检查我写进了部署清单,你要是也遇到用户反复掉线,先去目录里 ls 一眼再说。

若非作者标注皆为李涛自留地原创文章,转载或复制请以超链接形式并注明出处。