前言
众所周知,PHP是单线程同步堵塞模型,基于堵塞的模型,常常我们在开发中一个不小心就会把服务器搞崩,下面将跟大家一起分享常见的堵塞案例。(下图为一个标准的同步堵塞IO)
几个常见的场景:
- 1.为什么一段非常简单的代码却把服务器搞崩溃了?
- 2.为什么一个正常的不能再正常的Insert SQL却把MYSQL给卡死了?
- 3.为什么我一个页面卡死了,其它新页面也无法打开?
- 4.为什么消费者非常空闲,却无法消费?
常见的几个堵塞函数
TOP1:file_get_contents
最容易也最致命的函数TOP1
<?php
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.
<?php
$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.
<?php
$ch = curl_init('http://www.xxx.com/api.php');
curl_setopt_array($ch,
array(
CURLOPT_TIMEOUT=>3,
CURLOPT_CONNECTTIMEOUT=>5
)
);
echo curl_exec($ch);
TOP2:mysql_connect
数据库卡顿灾星TOP2
<?php
$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_connect更少一层的OO,所以稳定性上、扩展性上,效率上都比mysql_*更优秀。
<?php
$conn = new PDO($connect_string);
$conn->setAttribute(PDO::ATTR_TIMEOUT, 3);
//todo
或者使用fsockopen或者mysql_ping预访问数据库,防止进入堵塞死循环。
TOP3:session_start
文件锁引起的堵塞,体验非常差的函数TOP3
看一下例子,感受一下。
<?php
//1.php
@session_start();
sleep(10);
<?php
//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。
<?php
//1.php
@session_start();
session_write_close();
sleep(10);
其它引起堵塞的场景
TOP1:忘记关闭连接
在非连接池的情况下,很多人都习惯打开链接以后就不管了,反正会有gc,但事实下,gc并没有值得信赖。
<?php
$conn = mysql_connect(…);
//dosomething
一般观念认为,页面在执行完成以后,PHP就会自动对该页面的所有变量进行 自动的变量回收。所以,在打开一个连接以后,根本不需要调用disconnect方法。
可是,mysql对连接资源的回收受my.cnf中的配置影响,在一定的时间内,如果 大量的连接没有被释放,则会引起严重的mysql拒绝连接的情况。
规避办法
- 1.养成打开连接和关闭连接的习惯;
- 2.使用持久连接,用链接池;
- 3.为类增加析构函数__destruct,自动关闭连接;
- 4.注册register_shutdown_function事件,自动释放变量和资源。
TOP2:大量并发UPDATE +1
很多人以为我只是普通的UPDATE啊,为什么会把数据库卡死?
<?php
$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。
规避办法
- 1.要相信程序,盲目的自信往往容易给自己挖坑;
- 2.相似操作的SQL,建议进行合并操作(insert many,increment, decrement);
- 3.多使用队列和多线程来提高批量任务的执行速度。redis和gearman 都是很不错的软件。
TOP3:consumer执行复杂运算
在队列中,单个consumer执行复杂的逻辑时,往往会卡住其它本机consumer。多个conumser之间尽量少涉及相同的数据行操作,避免锁引起的等待。
规避办法
- 1.consumer尽量执行比较轻的运算
- 2.复杂的运算转入worker,如gearman,充分利用分布式
- 3.加快队列的消费速度