在本文中,我想检查与 PHP 和 MySQL 相关的各种超时设置,并描述 PHP 客户端处理超时的方法。
其缘由是在使用 Laravel(Lumen)进行批处理时,MySQL server has gone away
在几十秒左右发生了一次重度查询,无法正常执行查询。
当我与google老师核实时,我找到了一种方法来扩展wait_timeout的设置值作为解决方案。在我们的环境中,它是默认值,28800
因此不适用,我们决定寻找其他原因。
验证环境
- PHP : 7.3.25
- mysqlnd:5.0.12-dev
- mysql服务器:5.7.32
超时分类
这一次,我想将PHP端的检测超时和MySQL端的检测超时分开。
开头wait_timeout
是MySQL端的一个设置,所以是服务器端的超时。
MySQL服务器端超时设置
首先,我想检查一下 MySQL 的超时时间。
连接到 MySQL 并show global variables like '%timeout%';
mysql> show global variables like '%timeout%';
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| connect_timeout | 10 |
| delayed_insert_timeout | 300 |
| have_statement_timeout | YES |
| innodb_flush_log_at_timeout | 1 |
| innodb_lock_wait_timeout | 50 |
| innodb_rollback_on_timeout | OFF |
| interactive_timeout | 28800 |
| lock_wait_timeout | 31536000 |
| net_read_timeout | 30 |
| net_write_timeout | 60 |
| rpl_stop_slave_timeout | 31536000 |
| slave_net_timeout | 60 |
| wait_timeout | 28800 |
+-----------------------------+----------+
最常用的是:
connect_timeout
innodb_lock_wait_timeout
wait_timeout
interactive_timeout
详细请参照
MySQL :: MySQL 5.7 Reference Manual :: 5.1.7 Server System Variables
connect_timeout
是等待接受连接数据包的时间。
顾名思义,这是一个连接超时。容易理解!
innodb_lock_wait_timeout
是等待行锁获取的时间。for update
更新相同数据或
从多个连接获取锁时也是如此。
接下来 wait_timeout
。
“服务器在关闭非交互式连接之前等待其活动的秒数。”
简而言之,它是从诸如发出连接或查询之类的活动发生到诸如发出查询之类的下一个活动发生之间的延迟。
我将使用下面的代码进行尝试。
<?php
$mysqli = new mysqli('db', 'root', 'secret');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
sleep(10); // PHP側で10秒待つ
if ($result = $mysqli->query('SELECT table_name, table_type, engine FROM information_schema.tables;')) {
echo 'successful!!' . PHP_EOL;
} else {
echo 'failed!!' . PHP_EOL;
}
$mysqli->close();
此代码给出如下错误。
root@ff99dda9cf79:/var/www/html# php test_wait_timeout.php
Warning: mysqli::query(): MySQL server has gone away in /var/www/html/test_wait_timeout.php on line 11
Warning: mysqli::query(): Error reading result set's header in /var/www/html/test_wait_timeout.php on line 11
failed!!
*在尝试之前,将 wait_timeout 时间设置为 5 秒。
最后interactive_timeout
。
这很容易误解,但它是“服务器在关闭交互式连接之前等待交互式连接上的活动的秒数”。
在等待活动的时间方面与 wait_timeout 相同,具体取决于连接模式是交互式还是非交互式。
交互式示例是 mysql 客户端和 MySQL Workbench。
非交互类型包括通过重定向直接查询 PHP 应用程序和 mysql 客户端等模式。
您可以按如下方式检查连接(线程)的超时值。
mysql> set global wait_timeout=5;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM performance_schema.variables_by_thread WHERE variable_name='wait_timeout';
+-----------+---------------+----------------+
| THREAD_ID | VARIABLE_NAME | VARIABLE_VALUE |
+-----------+---------------+----------------+
| 29 | wait_timeout | 28800 |
+-----------+---------------+----------------+
1 row in set (0.00 sec)
mysql> set global wait_timeout=5;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM performance_schema.variables_by_thread WHERE variable_name='wait_timeout';
+-----------+---------------+----------------+
| THREAD_ID | VARIABLE_NAME | VARIABLE_VALUE |
+-----------+---------------+----------------+
| 29 | wait_timeout | 28800 |
+-----------+---------------+----------------+
1 row in set (0.00 sec)
mysql> show global variables like '%timeout%';
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| connect_timeout | 10 |
| delayed_insert_timeout | 300 |
| have_statement_timeout | YES |
| innodb_flush_log_at_timeout | 1 |
| innodb_lock_wait_timeout | 50 |
| innodb_rollback_on_timeout | OFF |
| interactive_timeout | 28800 |
| lock_wait_timeout | 31536000 |
| net_read_timeout | 30 |
| net_write_timeout | 60 |
| rpl_stop_slave_timeout | 31536000 |
| slave_net_timeout | 60 |
| wait_timeout | 5 |
+-----------------------------+----------+
13 rows in set (0.01 sec)
由于我这里是和mysql客户端交互连接,所以即使wati_timeout设置为5秒,interactive_timeout
设置值是有效variables_by_thread
的,wait_timeout
值是28800
。
另一方面,正如我在 wait_timeout 的解释中所确认的,我认为 5 秒的 wait_timeout 在 PHP 应用程序(非交互类型)中是有效的。
很高兴知道它既可以使用 mysql 客户端等工具手动设置,也可以在运行时使用应用程序设置!
PHP端超时
现在让我们谈谈PHP客户端的超时处理。
在此之前,我将说明可用于在 PHP 中连接 MySQL 的 API 和驱动程序之间的关系。
也就是说,似乎可以在API层和驱动层进行一些设置。
mysqlnd超时设置
作为驱动程序,请使用当前事实上的标准 mysqlnd。
可能的设置是mysqlnd.net_read_timeout
(>=PHP5.3.0)。
如果你看这里的手册,有很长的解释,但是似乎执行了很长的查询并且经过了设置的时间后,检测到超时并断开连接。
尝试运行下面的代码。
<?php
ini_set('mysqlnd.net_read_timeout', 5);
$mysqli = new mysqli('db', 'root', 'secret');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
// MySQLサーバー側で10秒待つ
if ($result = $mysqli->query('select sleep(10);')) {
echo 'successful!!' . PHP_EOL;
} else {
echo 'failed!!' . PHP_EOL;
}
$mysqli->close();
然后,没有等待 10 秒,我得到了以下结果。
ot@ff99dda9cf79:/var/www/html# php test.php
Warning: mysqli::query(): MySQL server has gone away in /var/www/html/test.php on line 11
Warning: mysqli::query(): Error reading result set's header in /var/www/html/test.php on line 11
failed!!
似乎我能够在 PHP 端设置 5 秒的超时。
顺便说一句,当我再次尝试提交查询时,查询立即失败。这是因为连接断开$mysqli->close();,报错后需要重新连接才能重新提交查询。
PDO 超时设置
现在让我们看看 API 方面。
在 Laravel (Lumen) 中,用于 MySQL 连接的标准 API 是 PDO。
在 PDO 中,可以设置以下超时选项。
PDO::ATTR_TIMEOUT
看手册,好像 不看驱动源码 就不能知道具体的东西。. 暂时想看看能不能用于查询超时。
指定超时秒数。并非所有驱动程序都支持该选项,而且根据驱动程序的不同,该选项的处理也不同。例如,sqlite将持续等待该秒数以确保可写入的锁定,但其他驱动程序也将该秒数作为连接时的超时或读取的超时来处理。传递整型。
运行下面的代码。
<?php
$options = [
PDO::ATTR_TIMEOUT => 5
];
try {
$pdo = new pdo('mysql:host=db', 'root', 'secret', $options);
} catch (PDOException $e) {
die('Connection failed: ' . $e->getMessage());
}
if ($pdo->query('select sleep(10);')) {
echo 'successful!!' . PHP_EOL;
} else {
echo 'failed!!' . PHP_EOL;
}
$pdo = null;
结果是……
ot@ff99dda9cf79:/var/www/html# php test_pdo.php
successful!!
我等了 10 秒钟,它工作正常。
显然它不能用于检测查询超时。
顺便说一句,似乎有必要分配 null 以显式关闭 PDO 连接。
https://www.php.net/manual/en/pdo.connections.php
那么这个设置在哪里发挥作用呢??
答案是……看起来连接超时了。
https://github.com/php/php-src/blob/2c0d56cc150ada2355319c418c0c6e8321ef7b0f/ext/pdo_mysql/mysql_driver.c#L630
MySQLi 超时设置
那么不能在API端控制查询超时吗?? 说起来,MySQLi从PHP7.2开始增加了一个叫MYSQLI_OPT_READ_TIMEOUT的选项,看来这里可以控制。
现在让我们运行下面的代码。
<?php
$mysqli = mysqli_init();
if (!$mysqli) {
die('mysqli_init failed');
}
if (!$mysqli->options(MYSQLI_OPT_READ_TIMEOUT, 5)) {
die('Setting MYSQLI_INIT_COMMAND failed');
}
if (!$mysqli->real_connect('db', 'root', 'secret')) {
die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
}
if ($result = $mysqli->query('select sleep(10);')) {
echo 'successful!!' . PHP_EOL;
} else {
echo 'failed!!' . PHP_EOL;
}
$mysqli->close();
然后,在大约 5 秒内发生错误,如下所示。
它似乎按预期工作。
ot@ff99dda9cf79:/var/www/html# php test_mysqli.php
Warning: mysqli::query(): MySQL server has gone away in /var/www/html/test_mysqli.php on line 16
Warning: mysqli::query(): Error reading result set's header in /var/www/html/test_mysqli.php on line 16
failed!!
警告点:
mysqli_options() 必须在 mysqli_init() 之后和 mysqli_real_connect() 之前调用。