有一个 php 项目遇到了 session blocking 的问题, 在本地进行复现, 两个请求打开同一个 session 文件:
timing | request a | request b |
---|---|---|
1 | session_start() | - |
2 | sleep(10) | - |
3 | session_start() | |
4 | end | … |
5 | end |
当第二个请求调用 session_start()
时发生了阻塞. 因为看不懂 php 源码, 直接看下文件的使用情况:
$ fuser /var/tmp/52domsuodlujgj8tbisut17180
/var/tmp/52domsuodlujgj8tbisut17180: 1679 8177
$ lsof /var/tmp/52domsuodlujgj8tbisut17180
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
php-fpm5. 1679 vagrant 16uW REG 252,0 23941 1314854 /var/tmp/52domsuodlujgj8tbisut17180
php-fpm5. 8177 vagrant 9u REG 252,0 23941 1314854 /var/tmp/52domsuodlujgj8tbisut17180
当 request a
完成后, 再执行 lsof
:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
php-fpm5. 8177 vagrant 12uW REG 252,0 23941 1314854 /var/tmp/52domsuodlujgj8tbisut17180
可以看到, 1679 的进程文件描述符为 16uW
, 而另外一个是 9u
查看手册:
u:for a read and write lock of any length;
W:for a write lock on the entire file;
因此可以判断 request a
被分配到了 PID 1679 的进程, 先拿到了 session 文件的写锁, 并在请求结束时才释放. 而另外一个请求在获取锁的时候发生了阻塞.
文件描述符前面的数字不知道干嘛用的, 有趣的是当前面一个进程完成后, 下面那个进程拿到锁之后那个数字总是会 + 3
那么要避免阻塞, 有两种解决办法: 打开 session 文件不上写锁, 或是手动 session_commit()
/ session_write_close()
.
两种各有优劣:
然而目前我能想到的正常的业务流程和业务场景里, 不上写锁带来的问题貌似都不影响, 问题不大.
在 php 7 里, php 提供了这样的功能, 在 session_start()
的 options
传递 read_and_close
为 true
, 让 php 在读完 session 文件后关闭.
而在 php 7 之前的版本就只能手动实现了. 简单地写一下改为上共享锁的实现:
function get_session_save_path()
{
if ('' === $path = session_save_path()) {
$path = sys_get_temp_dir();
}
return rtrim($path, '/');
}
function readSession($id)
{
$file = get_session_save_path() . "/{$id}";
if (!is_file($file) || (filemtime($path) + $this->getTimeout() < time())) {
return '';
}
$data = '';
if ($handle = fopen($file, 'rb')) {
try {
if (flock($handle, LOCK_SH)) {
clearstatcache(true, $handle);
$data = fread($handle, filesize($file) ?: 0);
flock($handle, LOCK_UN);
}
} finally {
fclose($handle);
}
}
return $data;
}
看着是不是很熟悉啊这段代码, 没错, 参考 laravel 的, 哈哈. 其实是个很基础的知识点了, 一直在写 api 差点把这个忘光了.