PHP 阻塞类型


PHP 阻塞类型,常见PHP堵塞案例


前言

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

下图为一个标准的同步堵塞IO:

几个常见的场景:

  1. 为什么一段非常简单的代码却把服务器搞崩溃了?
  2. 为什么一个正常的不能再正常的Insert SQL却把MYSQL给卡死了?
  3. 为什么我一个页面卡死了,其它新页面也无法打开?
  4. 为什么消费者非常空闲,却无法消费?

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拒绝连接的情况。

规避办法

  1. 养成打开连接和关闭连接的习惯;
  2. 使用持久连接,用链接池;
  3. 为类增加析构函数__destruct,自动关闭连接;
  4. 注册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。

规避办法

  1. 要相信程序,盲目的自信往往容易给自己挖坑;
  2. 相似操作的SQL,建议进行合并操作(insert many,increment, decrement);
  3. 多使用队列和多线程来提高批量任务的执行速度。redis和gearman 都是很不错的软件。

consumer执行复杂运算

在队列中,单个consumer执行复杂的逻辑时,往往会卡住其它本机consumer。 多个conumser之间尽量少涉及相同的数据行操作,避免锁引起的等待。

规避办法

  1. consumer尽量执行比较轻的运算
  2. 复杂的运算转入worker,如gearman,充分利用分布式
  3. 加快队列的消费速度

参考资料

php阻塞类型,常见PHP堵塞案例 https://blog.csdn.net/weixin_42360846/article/details/116416930


返回