最近在维护一个PHP后台守护进程时,频繁遇到"SQLSTATE[HY000]: General error: 2006 MySQL server has gone away"的错误。这个报错通常发生在长时间运行的PHP脚本中,特别是那些需要持续连接数据库的守护进程场景。
MySQL服务器默认会在8小时无活动后断开空闲连接(由wait_timeout参数控制)。当守护进程尝试复用这个已经断开的连接时,就会触发2006错误。这个问题在以下场景尤为常见:
MySQL服务端有两个关键参数控制连接生命周期:
wait_timeout:非交互式连接空闲超时(默认28800秒/8小时)interactive_timeout:交互式连接空闲超时(同默认8小时)当连接空闲时间超过这些阈值,服务端会主动断开连接,但客户端通常不会立即感知到这个变化。
PHP的PDO和mysqli都支持持久连接(通过PDO::ATTR_PERSISTENT),这种连接不会被脚本执行结束释放。对于守护进程来说:
最简单的处理方式是捕获异常后重建连接:
php复制try {
// 数据库操作
} catch (\PDOException $e) {
if ($e->getCode() == 'HY000' && strpos($e->getMessage(), 'server has gone away') !== false) {
$this->reconnect();
// 重试操作
} else {
throw $e;
}
}
注意:单纯的错误捕获不能预防问题,只是事后补救措施
更可靠的做法是定期执行简单查询保持连接活跃:
php复制class DbKeepAlive {
private $lastActiveTime = 0;
private $heartbeatInterval = 1800; // 30分钟
public function query($sql) {
$this->checkConnection();
// 执行实际查询
}
private function checkConnection() {
if (time() - $this->lastActiveTime > $this->heartbeatInterval) {
$this->ping();
$this->lastActiveTime = time();
}
}
private function ping() {
try {
$this->pdo->query('SELECT 1');
} catch (\PDOException $e) {
$this->reconnect();
}
}
}
对于高并发场景,建议使用专业的连接池管理:
php复制$config = [
'min_connections' => 2,
'max_connections' => 10,
'max_idle_time' => 3600,
];
$pool = new ConnectionPool(
function() {
return new PDO($dsn, $user, $pass);
},
$config
);
$connection = $pool->get();
try {
// 使用连接
} finally {
$pool->put($connection);
}
MySQL服务端配置建议:
ini复制[mysqld]
wait_timeout = 86400 # 调整为24小时
interactive_timeout = 86400
net_read_timeout = 120 # 重要:网络读取超时
net_write_timeout = 120 # 重要:网络写入超时
PHP连接配置建议:
php复制$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_TIMEOUT => 5, // 连接超时
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false // 守护进程建议禁用持久连接
]);
建议实现以下监控指标:
Prometheus监控示例:
php复制$metrics->inc('db_errors_total', [
'type' => 'gone_away'
]);
确认错误是否真的是2006:
bash复制grep "MySQL server has gone away" /var/log/php.log
检查实际超时时间:
sql复制SHOW VARIABLES LIKE '%timeout%';
检查连接使用情况:
sql复制SHOW STATUS LIKE 'Threads_connected';
SHOW PROCESSLIST;
启用MySQL完整日志:
ini复制[mysqld]
general_log = 1
general_log_file = /var/log/mysql/mysql-general.log
使用tcpdump抓包分析:
bash复制tcpdump -i any -s 0 -l -w /tmp/mysql.pcap port 3306
对于关键业务系统,建议考虑:
微服务架构下的连接管理示例:
php复制$circuitBreaker = new CircuitBreaker(
'database_service',
new RedisAdapter()
);
$result = $circuitBreaker->call(function() {
return $db->query('SELECT ...');
});
建议进行以下测试验证解决方案:
长时间空闲测试:
php复制sleep(3600 * 9); // 超过默认超时时间
$db->query('SELECT 1'); // 应该成功
压力测试脚本:
php复制for ($i = 0; $i < 1000; $i++) {
$db->query('SELECT SLEEP(1)');
if (0 === $i % 100) {
sleep(3600 * 2);
}
}
网络模拟测试:
bash复制# 模拟网络中断
iptables -A INPUT -p tcp --dport 3306 -j DROP
sleep 30
iptables -D INPUT -p tcp --dport 3306 -j DROP
修改数据库配置:
php复制// config/database.php
'mysql' => [
'options' => [
\PDO::ATTR_TIMEOUT => 5,
\PDO::ATTR_PERSISTENT => false,
],
'read' => [
'options' => [
\PDO::ATTR_EMULATE_PREPARES => true,
],
],
],
自定义重连逻辑:
php复制DB::connection()->setReconnector(function($connection) {
$connection->disconnect();
$connection->reconnect();
});
配置数据库参数:
php复制// config/database.php
return [
'break_reconnect' => true, // 自动重连
'break_match_str' => ['server has gone away', 'Lost connection'],
];
使用Doctrine连接监听器:
php复制class ConnectionListener implements EventSubscriber
{
public function getSubscribedEvents()
{
return [ConnectionEvents::POST_CONNECT];
}
public function postConnect(ConnectionEventArgs $args)
{
$args->getConnection()
->executeQuery('SET SESSION wait_timeout=28800');
}
}
在Docker/K8s环境中需额外注意:
K8s健康检查配置示例:
yaml复制livenessProbe:
exec:
command:
- mysqladmin
- ping
initialDelaySeconds: 30
periodSeconds: 10
虽然本文聚焦PHP,但其他语言的解决方案也值得参考:
Python示例(SQLAlchemy):
python复制from sqlalchemy import create_engine
from sqlalchemy import event
engine = create_engine("mysql://...", pool_recycle=3600)
@event.listens_for(engine, "engine_connect")
def ping_connection(connection, branch):
connection.scalar("SELECT 1")
Java示例(HikariCP):
java复制HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://...");
config.setConnectionTestQuery("SELECT 1");
config.setIdleTimeout(600000); // 10分钟