前言

众所周知,PHP是单线程同步堵塞模型,基于堵塞的模型,常常我们在开发中一个不小心就会把服务器搞崩,下面将跟大家一起分享常见的堵塞案例。(下图为一个标准的同步堵塞IO)

同步堵塞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.加快队列的消费速度