PHP会话怪癖
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
你好,同行开发者!
您是否知道 PHP 会话在单个服务器实例上是阻塞的,但在多服务器架构上容易受到竞态条件漏洞的影响?
以下是关于 PHP 中会话工作原理你应该了解的一些重要知识。
首先,你需要了解会话的存储方式。
默认的会话保存处理程序名为“files”,它会将所有会话数据保存到一个文件中。该文件的名称与 PHPSESSID 的值完全相同,服务器正是通过 PHPSESSID 来确定会话数据的位置、会话是否存在以及如何检索会话数据。
其次需要了解的是,“files”会话处理程序的设计是阻塞式的,而且无法禁用此限制。这意味着每次服务器尝试打开您的会话文件时,它都会锁定该文件(使用flock),从而阻止任何其他进程打开该文件——直到锁定被解除,而这通常会在 PHP 脚本/请求完成后自动发生。这实际上是防止竞态条件的一种绝佳技巧。您可以想象一下以下代码片段。
<?php
if ($_SESSION['received_payment'] === false) {
$_SESSION['received_payment] = true;
sendMoney();
}
并行运行这段代码且不使用锁可能会导致 `sendMoney()` 函数被多次调用!这是一个竞态条件,可以通过使用锁来解决。请记住,虽然 PHP 是单线程的,但您可以通过并行运行多个进程来实现并发,Apache 或 Nginx 就是这么做的。pm2 也使用了同样的技巧来并行化 Node 进程。
所以没问题吧?错啦🙂
问题在于,这种模式在并行处理所有请求的总时间方面扩展性很差。请求本身是并行接收的,但由于锁定机制,它们是按顺序执行的。这意味着,如果您有 10 个并行的 Ajax 调用需要处理,假设每个调用需要 500 毫秒,那么您总共需要等待 5 秒钟才能完成所有 Ajax 请求。更糟糕的是,如果第一个调用需要 4 秒钟才能完成,而其余 9 个调用每个需要 100 毫秒。您最终仍然需要等待 5 秒钟,但您至少要等待 4 秒钟才能看到任何结果!
这里有一个很棒的演示,您可以亲自体验一下。
我也做了一个实验,这是使用“慢速”会话的情况:https://github.com/krukru/php-session-quirks/blob/master/example_1/screenshots/with-sharding/sharding.gif
这是会话一打开就关闭的情况:https://github.com/krukru/php-session-quirks/blob/master/example_0/screenshots/with-sharding-max-workers.png
还有一些其他因素需要考虑,例如浏览器连接数限制和 Web 服务器并发设置——但这些超出了本文的讨论范围。
那么该如何缓解这个问题呢?
有两种可行的解决方案。
第一种方法是在读取完会话数据后立即关闭会话。会话通常仅用于判断用户是已登录用户还是访客。之后(大多数情况下)就不再需要会话了,如果提前关闭会话,就可以同时处理下一个请求。
第二种解决方案是使用只读会话标志,即仅对会话执行“读取”操作。一个很好的例子是检查用户是访客还是已登录用户。这里您只从会话中读取数据,不写入任何内容——这样做的好处是不会出现竞态条件(因为数据没有被修改),也不需要加锁!但这种方法也有
其缺点。只读会话仅从 PHP 7 开始受支持,这对于希望支持 PHP 5 的框架来说是个问题(比如Yii2)。另一个问题是,像 Zend 和 Symfony 这样的主流框架对只读会话的支持速度较慢,请参阅https://github.com/zendframework/zend-session/issues/39和https://github.com/symfony/symfony/issues/24875。
所以最好的办法就是早点关门,尽量避免参加交易时段🙂
请记住,这仅适用于来自同一用户(同一 PHPSESSID)的 ajax 调用,并且仅当使用会话时(在脚本生命周期中的任何位置调用 session_start())!
好的,但是多服务器架构呢?嗯,现在你不能再使用“文件”作为会话处理程序了,因为一个会话可能存在于一个服务器实例上,但不存在于另一个服务器实例上。
解决此问题的方法是使用一些共享内存空间来管理会话,redis 和 memcached 是最适合这项工作的候选者。
Redis 会话处理程序完全不支持锁,而 memcached 已经开始支持锁,但成功程度不一(存在一些错误https://github.com/php-memcached-dev/php-memcached/issues/310)。
这意味着你无法获得使用“files”会话处理程序时那种极佳的竞态条件安全性。使用“received_payment”会话的简单代码片段要正确实现起来也变得非常困难。
很遗憾,这种情况的解决方案是改变你的代码逻辑,使用数据库进行锁定,或者使用某种特定的锁定机制(例如https://symfony.com/doc/current/components/lock.html),并尽可能避免使用会话。
您是如何进行会话管理的?在 Node 后端环境中,会话是如何工作的?请在下方评论区分享您的想法和经验!:)
祝您开发顺利!
相关链接和资源:
- 最初引发我思考这个问题的帖子是:https://pasztor.at/blog/stop-using-php-sessions/
- 我尝试重现各种实验https://github.com/krukru/php-session-quirks