PHP sessions——性能和扩展性

仅仅是理解PHP sessions的概念还不够,我们还要深入了解PHP sessions运行机制。默认情况下,每个session都是系统中以session ID为文件名的文件。在php函数session_start()被调用的时候php进程会立即读取对应的session文件,并在调用session_close()函数或者脚本结束时把文件保存到磁盘上。在代码执行的过程中,session文件会被锁起以防止冲突。session文件的存储机制和加锁行为在性能和扩展性上都有优化空间。

一 存储机制
前面说过每个session都是以独立文件的形式存储在系统中。PHP的垃圾回收进程会定期清除过期session文件释放磁盘空间(相关配置项有session.gc_probability, session.gc_divisor和session.gc_maxlifetime)

大流量网站垃圾回收可能会导致很大开销。虽然文件系统有扩展性,但是问题还是比较大。尽管文件对于单服务器应用非常有用,但是如果把应用扩展到多台服务器上得话文件就会出问题。即使你用简单会话保持的负载均衡策略强制用户访问同一台服务器,这也并不是一个理想的解决办法。如果其中一台服务器出了问题,比如,奔溃了,用户请求就会被转发到新服务器上,他们的session文件会被抹掉。

扩展的理想状态是,可以按照需要添加或者删除服务器,web服务器必须是无状态的机器——任何一台服务器都可以响应任何用户的请求。这就要求php session信息能集中存储以被所有web服务器共用。目前已经一些网络共享文件的方案存在比如,NFS、GlusterFS等,但是这些方案配置起来很复杂,生环境下的维护难度也很大。我们需要更好的解决办法。

Redis和Memcached就是上面说的更好的方法。两者都是效率高的键值对数据库。大致来说,我们可以把session数据存到中央数据库而不是文件里。它的高扩展性能极大提高大流量网站的性能,虽然网络延迟会有轻微的性能损耗.选Redis还是Memcached呢?网上有很多两者VS的文章,让人无所适从。Twitter和GitHub用的是Redis;Facebook和Netflix用的是Memcached.两种数据库在生成环境中表现都很稳定而且效率很高,两者都是正确的选择。

需要用数据库来存储session数据的情况是:
1,你有多台服务器。
2,你可能会对服务器进行更改。
3,对服务器进行更改的时候不能丢失session数据。
如果不满足这三个条件,你继续使用默认的文件存储机制也很安全。

PHP提供了很多能修改session存储机制的扩展.要用哪一个可以通过配置文件的配置项来修改,比如,session.save_handler = redis,但是这样修改并不能解决session文件加锁的问题。为了最大限度的利用sessions,你可能需要用SessionHandlerInterface创建自己的session处理类并定义一些简单的方法。

SessionHandler 类的基本结构如下:
class MyHandler implements SessionHandlerInterface {...}
$handler = new MyHandler();
session_set_save_handler($handler, true);
// Must be called after `session_set_save_handler`
session_start();

二 序列化
暂且不谈序列化。PHP里的$_SESSION变量是个关联数组。数据结构要序列化为字符串才能存到文件或者数据库中。PHP序列化sessions的时候使用的是特殊的算法,这个算法更快内存占用更少。其他的类库比如MessagePack 或者 igbinary,可能比内置函数更快返回的字符串更小,但是比较复杂不值得。但是你两台不同环境的服务器之间要进行session共享的话,比如php和NodeJS,用MessagePack或者JSON替换PHP的序列化格式就很有必要。

如果$_SESSION里只是写基本数据类型,比如,整型,字符串,后者数组,你直接序列化就可以了。如果里面存的是对象类型的数据事情就变得复杂了。序列化对象和重构对象的过程中可能会出现问题。代码写的不好的话,操作对象时_sleep和_wakup这两个魔术方法可能会抛异常。另外,如果你把对象存在session里,对象所属的类就必须被引用到整个项目。如果你用composer自动加载类文件,就不会出现找不到类文件的问题,但是这也意味着任何使用session的地方你都要加载composer定义的全部类文件。session里保持对象的另一个缺陷是,静态属性在序列化的时候会丢失。如果你的对象依赖静态属性的话,重构对象就会失败。如果一个类的对象被保存在session里那么要更新这类就麻烦了。如果你成session里重构出一个老版本的对象,对象会被新版类的签名重新实例化。如果两个版本之间的变化不大这个过程能正常进行,反之就会出现问题而且问题很难调试。

由于有这么多潜在风险,我强烈建议你别把对象放到session里。如果你希望保持用户登陆状态,把用户ID存在session里然后从数据库或者cache中取数据构造用户信息,而不是把User类的对象存在session里。这样做可能稍微有点麻烦,但是不序列化类,你的程序会更稳定更敏捷。

目前,会话锁是PHP性能的最大威胁者之一。PHP session在请求开始的时候从文件中读取,请求结束的时候写入,这种情况特别容易出现不同进程间的资源争夺跟数据冲突。假如你有个为session设置首选项的API接口。


初始化数据如下:
[ "theme" => "blue", "volume" = >100]

用户几乎同时发起两个访问请求:请求A把theme值设为"red",请求B把volume值设为50.如果请求B在请求A调用session_close()之前调用了session_start();请求B都到的theme值应该是blue.当请求B调用session_close()时,它会把["theme"=>"blue", "volume"=>50]写到session里,请求A的设置被覆盖了。这样的竞态条件会让你的程序看上去很混乱而且可能导致严重问题(比如用户已经退出登陆,但是有个接口覆盖了退出登陆的数据,用户有处在登陆状态了)

PHP开发人员用会话锁来解决这个问题。当session_start()被调用的时候,PH会请求到一个session文件的独占锁。如果独占锁在其他请求哪里,PHP就等它释放之后获取。独占锁在数据写到session文件里的时候才会释放,也就是程序结束或者session_close()函数执行完之后。如果一个用户同时只发起一个请求程序还可以运行。

会话锁是不是解决了所有问题?假如上面的API运行时间为1秒。关闭会话锁,因为是并发请求所以上面的程序运行时间为1秒。但是,开启会话锁,就要2秒了。请求少的情况下,这并没什么。假如有个单页应用通过AJAX向你的服务器发送了成千上万的请求。如果每个请求都要等前面的程序执行完,那么你的程序就会极慢。关闭会话锁之后Education.com页面加载时间从180毫秒降到了100毫秒。


会话锁和页面加载时间

我可以通过关闭会话锁来加快网站响应速度,但这是把双刃剑。你必须在数据完整但是响应时间长跟快速响应但可能出现数据错误之间做出抉择。如果的网站只有满足其中一种情况的话那就完美了。


三 自动合并

当我第一次面对这个选择的时候,我立马想到了Git.两个人能同时从服务器上取文件,并操作同一文件完了提交。同样的竞态情况也会出现。Git本可以忽略冲突让第二个开发者的修改覆盖前一个的,或者可以采取PHP的方式加锁,捡取是锁上这个分支,不允许其他任何人取文件直到你修改完成提交之后。不管哪种方式都是非常恐怖的。幸运的是,Git开发者使用了更由创造性的方式:智能自动合并。如果两个人编辑的地方不一样,Git就自动合并两个版本。这样既避免了冲突又不影响响应速度。如果编辑的是同一个地方呢?Git就不合并交给程序员手动处理。

Git自动合并的方式稍微调整一下之后就可以用到PHP session文件的管理上来。策略如下:
1,禁用会话锁;
2,调用session_start(),记录session的初始化状态;
3,调用session_close()时,记录请求过程中的差异操作;
4,重新从数据库中读取最新的session数据;
5,把差异数据整合到最新的session数据中;
6,把session数据写到数据库;

有几个地方需要注意。
第一,第四不和第六不还是会有竞争。从第四步到第六步已经足够快了,竞争情况出现的概率很低,如果真的有,用会话锁对页面加载速度也不会有太大影响。

第二,如果在第五不有冲突也不太可能要开发者手动干预。要想好自动处理流程。假如你在session里保存了用户访问网页的ID($_SESSION['history'][] = $pageid).你的代码应该可以找到差异数据,找到新的ID追加到session数组里。

这个处理逻辑很难标准化。业务不同处理逻辑也不一样。

现有存储机制并没有内置函数可以改变会话锁本身,你需要自己的创建一个SessionHandler类。

作者写的SessionHandler类: https://github.com/edu-com/php-session-automerge.



英语原文地址:https://www.phparch.com/2018/01/php-sessions-in-depth/


评论2条