前言
众所周知,PHP是单线程同步堵塞模型,基于堵塞的模型,常常我们在开发中一个不小心就会把服务器搞崩, 下面将跟大家一起分享常见的堵塞案例。
下图为一个标准的同步堵塞IO:
几个常见的场景:
- 为什么一段非常简单的代码却把服务器搞崩溃了?
- 为什么一个正常的不能再正常的Insert SQL却把MYSQL给卡死了?
- 为什么我一个页面卡死了,其它新页面也无法打开?
- 为什么消费者非常空闲,却无法消费?
file_get_contents
最容易也最致命的函数
if (file_get_contents('http://www.xxx.com/api.php')) {
//dosomething
}else{
//dosomething
}
file_get_contents 不受 set_time_limit 的影响,也就是说,就算页面已经超时,而file_get_contents进行依然会被挂起。
所以,如果一个远程地址卡死的话,所有调用file_get_contents的地方都会全部挂起,从而引起服务器的502。
规避办法
从PHP5.0开始,file_get_contents可以支持context参数了, 所以如果一定要使用file_get_contents的话, 一定要为其提供 context参数,设定timeout:
$param = array(
'http'=>array('timeout'=>3,'method'=>'GET')
);
echo file_get_contents('http://www.xxx.com/api.php',false, stream_context_create($param));
也可以使用curl替代file_get_contents.
$ch = curl_init('http://www.xxx.com/api.php');
curl_setopt_array($ch,
array(
CURLOPT_TIMEOUT=>3,
CURLOPT_CONNECTTIMEOUT=>5
)
);
echo curl_exec($ch);
mysql_connect
数据库卡顿灾星
$conn = mysql_connect('ip:3306', 'root', 'root');
if (!$conn) {
die('mysql connect error');
}
由于mysql_connect
的 timeout 是受php.ini
中的 connect_timeout 影响的,一般都默认是30s。所以如果数据库连接不上,
而在这30s中大量的进程都卡在连接数据库上,那服务器出现502是必然的了。
规避办法
使用PDO的连接方式替代mysql_connect
,PDO有着更加完善的属性配置,
所以稳定性上、扩展性上,效率上都比mysql_*
更优秀。
$conn = new PDO($connect_string);
$conn->setAttribute(PDO::ATTR_TIMEOUT, 3);
//todo
或者使用 fsockopen 或者 mysql_ping 预访问数据库,防止进入堵塞死循环。
session_start
文件锁引起的堵塞,体验非常差的函数
看一下例子,感受一下。
1.php
文件内容:
@session_start();
sleep(10);
2.php
文件内容:
@session_start();
echo 'a';
同一个用户,先访问1.php
,再访问2.php
,会发现只有等1.php
中的sleep完成 以后,2.php
才会显示出来a。
这是因为session_start时,session文件会被锁死,只有被释放以后,其它程序才能继续。
期间,其它程序一律会被堵塞。
规避办法:
避免使用session,而且就算使用session,也要设置session_handler,避免文件死锁。
session.save_handler = memcache
session.save_path = "tcp://127.0.0.1:11211"
就算一定要使用session,也要做session_write_close。
1.php
文件内容:
@session_start();
session_write_close();
sleep(10);
忘记关闭连接
在非连接池的情况下,很多人都习惯打开链接以后就不管了,反正会有gc,但事实下,gc并没有值得信赖。
$conn = mysql_connect(…);
//dosomething
一般观念认为,页面在执行完成以后,PHP就会自动对该页面的所有变量进行 自动的变量回收。 所以,在打开一个连接以后,根本不需要调用disconnect方法。
可是,mysql对连接资源的回收受my.cnf中的配置影响,在一定的时间内, 如果大量的连接没有被释放,则会引起严重的mysql拒绝连接的情况。
规避办法
- 养成打开连接和关闭连接的习惯;
- 使用持久连接,用链接池;
- 为类增加析构函数
__destruct
,自动关闭连接; - 注册
register_shutdown_function
事件,自动释放变量和资源。
大量并发UPDATE +1
很多人以为我只是普通的UPDATE啊,为什么会把数据库卡死?
$sql = 'UPDATE `table` SET column=column+1 WHERE id=1';
某统计模块,在对每一次请求的时候都会对某表的某字段进行自加1操作。 这个表非常简单,就两个字段,id和column, ID有索引且是主键。column是 int类型。 某开发者非常相信自己,说这个SQL绝对不会引起问题。
可结果是,某天,运维哥哥发现MYSQL大量的这个SQL处于wait状态。 上W条这个SQL一直阻塞着MYSQL,引起MYSQL的卡死。
原来后端某统计模块在对该表进行复杂的统计,造成了TABLE LOCK。从而阻塞了所有的UPDATE。
规避办法
- 要相信程序,盲目的自信往往容易给自己挖坑;
- 相似操作的SQL,建议进行合并操作(insert many,increment, decrement);
- 多使用队列和多线程来提高批量任务的执行速度。redis和gearman 都是很不错的软件。
consumer执行复杂运算
在队列中,单个consumer执行复杂的逻辑时,往往会卡住其它本机consumer。 多个conumser之间尽量少涉及相同的数据行操作,避免锁引起的等待。
规避办法
- consumer尽量执行比较轻的运算
- 复杂的运算转入worker,如gearman,充分利用分布式
- 加快队列的消费速度
参考资料
php阻塞类型,常见PHP堵塞案例 https://blog.csdn.net/weixin_42360846/article/details/116416930