最近IT168资讯在监测自己的站点的时候发现,页面偶尔会打开很慢,读取数据卡顿,找到网站日志,发现一堆错误。经检查是无法从Memcached读写Session数据失败导致的。为什么会出现这类问题呢?IT168资讯找了…
最近it168资讯在监测自己的站点的时候发现,页面偶尔会打开很慢,读取数据卡顿,找到网站日志,发现一堆错误。经检查是无法从Memcached读写Session数据失败导致的。为什么会出现这类问题呢?it168资讯找了很久的解决办法,最终解决成功,现在分享一下Memcached读写Session数据失败的原因分析及解决办法。
一、错误提示内容
因为it168资讯是将PHP的Session数据保存路径是设置为Memcached保存的,这样可以让网站响应速度更快,但是同时也会带来一些其他问题。比如经常会出现Memcached读写Session数据失败。
注意:PHP的Session数据可以保存在file文件中,也可以保存在Memcached、Redis甚至数据库里,本文的问题只限于Memcached,如果是Redis貌似不会出现这类问题。
一般Memcached读写Session数据失败后,日志里将有大量提示如下:
session_start(): Failed to read session data: memcached (path: 127.0.0.1:11211) Unknown: Failed to write session data (memcached). Please verify that the current setting of session.save_path is correct.
监测结果如下图
有大量的Memcached读写Session数据失败的错误提示如下
比如很多时候不少小伙伴在WordPress的后台站点健康里会出现提示:
PHP 会话是由 session_start() 函数调用创建的。这会干扰 REST API 和环回请求。在发出任何 HTTP 请求之前,会话应该由 session_write_close() 关闭。
其实用本文的方法也是可以解决的。
二、问题分析
为什么会无法读取呢,it168资讯经过了大量的了解及实践后,发现是因为PHP的Session会话锁导致的。因为其中问题的代码中为session_start()相关设置。即受影响的代码为
if (!session_id()) session_start();
什么是PHP的Session会话锁?
在PHP中,当同一个客户端执行两个包含session_start()的页面,如果先执行的脚步执行时间较长,就会导致session文件阻塞。具体表现为:一个PHP页面的执行时间比较长,而只要这个页面没有执行完毕,其他的页面访问都是长时间加载状态,只有那个页面执行完毕了,剩下的页面才能打开。
原因:PHP 默认是用文件格式存储session,对于每一个请求执行包含有session_start()的页面后,就会默认创建一个包含session_id的文件名,且会对文件进行锁定,如果用户在这个客户端又访问了一个包含了session_start()的页面,由于session_id一样,这个页面也要读取该用户存放的session文件,如果第一个页面没有执行完成,这个文件就一直被锁定,第二个页面就无法获取,只能一直处于等待状态。
可能造成的后果:如果网站有大量的用户访问,就好导致session读取文件一直堵塞等待,用户浏览器一直保持和服务器的连接,从而会消耗掉大量的服务器资源。而且,随着web服务器活跃连接数的增大,可能会耗费完连接资源,造成出现拒绝服务现象。
当你调用 session_start() 时,都发生了什么
我们使用一个基本的 PHP 配置为例:当你开始一次 PHP 会话时,PHP会在 session.save_path 路径下创建一个普通的文件,默认路径为 /var/lib/php/session 。所有的 session 数据都保存在这个地方。
如果你的用户还没有一个 session cookie ,那么 PHP 将产生一个新的 ID,并设置到用户机器的 cookie 中。如果是一个已访问过的用户,那么他会将 cookie 发送给你的 web 服务器,PHP 则会解析它,并且从 session.save_path 路径下加载到相应的 session 数据。
简而言之,这就是 session_start() 的所做的工作。
会话锁与并发
接下来我们举一个稍微完整一点的例子,来我们说明PHP初始化session后,各个场景下所发生的事情。
Timing | PHP Code | Linux/Server |
---|---|---|
0ms | session_start(); |
创建文件锁:/var/lib/php/session/sess_$identifier |
15ms | SQL查询,for循环,第三方API调用 | 持有session文件锁 |
350ms | PHP脚本执行结束 | session文件锁被移除 |
当你调用session_start()(或者PHP的session.auto_start被设置为true时,该方法会被自动调用),操作系统会锁住session文件。大多数文件锁的实现都是flock
,在Linux上,它也用于防止定时任务的重复执行或者其它文件锁定工作。
在Linux机器上,一个session文件锁看起来就像这样子。
该session的文件锁会保持到脚本执行结束或者被主动移除(后面会讲到)。这是一个读写锁:任何对session读取都必须等到锁被释放之后。
锁本身并不是问题。它保护session文件中的数据,防止多个同时写入损毁数据或者覆盖之前的数据。
但是当第二个并发的PHP执行想要获取同一个PHP会话的时候,就会造成问题了。
Timing | script 1 | Linux/Server | script 2 |
---|---|---|---|
0ms | session_start(); |
script1锁定(flock )文件/var/lib/php/session/sess_$identifier |
session_start(); 被调用,但是被锁阻塞。PHP等待锁被移除。 |
15ms | SQL查询,for循环,第三方API调用 | 文件锁保持不变。 | 脚本仍然在等待,啥都不做。 |
350ms | script1执行结束。 | script1持有的文件锁被移除。 | script2仍然在等待。 |
360ms | script2得到新的文件锁。 | script2现在可以执行它的SQL查询,for循环… | |
700ms | script2持有的文件锁被移除。 | script2执行结束。 |
解释一下上面的表格:
- 当2个PHP文件同时想要开始一个会话时,只有一个能赢且获得锁。另一个则需要等待。
- 当它等待的时候,不会做任何事情:
session_start()
阻塞了之后动作的执行。 - 一旦第一个脚本的锁被移除,第二个脚本在获得锁的同时就可以向后继续执行了。
在绝大多数场景下,这都使得PHP对于同一个用户来说,表现得像是一系列同步脚本:一个执行完成后执行下一个,没有平行的请求。即使你使用AJAX调用这些PHP脚本也无济于事。
所以,刚才两个脚本没能同时在350ms左右的时间执行完毕,第一个脚本350ms执行完毕,而第一个脚本则消耗两倍的时长执行了700ms,因为它得等第一个脚本先执行完。
有的小伙伴说,哪有这么麻烦,如果是Memcached保存session的话,直接将memcached.sess_locking
设置为“off”,来避免session锁就行了,其实然并卵。这个方法it168资讯就试过,没啥用。
PHP session锁有可能出现的问题
锁的存在也它好的一面。想象以下没有“session锁”的场景,当两个脚本同时处理同一个session数据时,可能引发错误:
Timing | script 1 | script 2 |
---|---|---|
0ms | session_start(); session数据被读入到$_SESSION变量中 |
session_start(); session数据被读入到$_SESSION变量中 |
15ms | 脚本1写入session数据:$_SESSION['payment_id'] = 1; |
脚本2写入session数据:$_SESSION['payment_id'] = 5; |
350ms | sleep(1); |
脚本结束,保存session数据 |
450ms | 脚本结束,保存session数据 |
session中的数据值应该是多少?
应当是脚本1的所保存的值。因为脚本2所保存的值被脚本1最后所保存的值覆盖了。
这是一个非常尴尬,而且又很难排查的并发问题。session锁可以防止这种情况发生。
绝大多数情况下,这是写session数据时才会碰到的问题。如果你有一个PHP脚本只是读取session数据(大多数ajax请求都是),你可以安全地对数据进行多次读取。另一方面,如果你有一个长时间运行的脚本,它读取了session数据并且还会修改session数据,而另一个脚本开始执行并且读取到了旧的过时数据 — 这也可能使你的应用出错。
三、解决办法
以上给大家普及了一下PHP session锁的原理知识,接下来就介绍解决办法。
PHP中有一个方法叫做session_write_close()
。它的功能如其名:写入session数据,关闭session文件,从而解除了session锁。你在PHP代码中,可以这样使用。
因为it168资讯用的是PHP 7.4,因此这里的解决办法是直接修改受影响的文件代码,如下,原文件中的受影响代码如下
if (!session_id()) session_start();
修改后为
if (!session_id()) session_start(['read_and_close' => true]
);
保存后重启PHP,问题解决。
解决后的网络波动也停止了,如下图。
原创文章,作者:admin,如若转载,请注明出处:https://www.it168.online/webtech/4094/