Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Переклади 09 листопада 2026

Laravel без таймауту підключення до MySQL: чотири спроби, дві хвилини

Переклад статті Івана Михавка Laravel's Missing DB Connect Timeout: Four Attempts, Two Minutes.

Наш сервер MySQL був цілком здоровий, а сайт усе одно не працював 8 хвилин.

Ні розгортання, ні сплеску трафіку, ні повільного запиту до бази. Сервер застосунку просто якийсь час не міг дістатися до бази даних, а Laravel чекав. І чекав. Виявилося, що підключення Laravel до MySQL не має власного таймауту підключення, а коли підключитися не вдається, Laravel пробує ще раз, і ще три рази.

Що сталося

Проєкт, над яким я працюю, розміщено в Hetzner. Сервер застосунку спілкується з MySQL через приватну мережу Hetzner. Одного дня ця внутрішня мережа відмовила в одному датацентрі. MySQL працював. Сервер застосунку працював. Вони просто не бачили одне одного.

Лог Laravel заповнився ось таким:

SQLSTATE[HY000] [2002] Connection timed out

Помилка в лозі під час мережевої проблеми - це нормально. Але перестав відповідати весь сайт, включно з маршрутами, які взагалі не звертаються до бази.

На сервері один пул PHP-FPM на 5 воркерів, і його ділять два Laravel-застосунки: API вітрини магазину та API, через яке наша ERP-система обмінюється даними. Кожен запит, якому була потрібна база, займав воркер і зависав на підключенні. За хвилину всі 5 воркерів застрягли, і більше ніхто не міг пробитися. Короткий мережевий збій перетворився на повну недоступність.

Логи nginx за той день (сервер застосунку обслуговує обидва API, вітрина стоїть перед ним):

Де Помилок під час збою До збою
Сервер застосунку 260 x 5xx 0
Вітрина 7 x 504, 43 x 499 (клієнт не дочекався) 0

Чому одна спроба підключення триває 30 секунд

Подивіться на опції підключення mysql у config/database.php (у нас він зі старішого скелета; у Laravel 13 така сама структура):

'options' => extension_loaded('pdo_mysql') ? array_filter([
  PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],

Жодного PDO::ATTR_TIMEOUT. У скелеті Laravel 13 його теж немає.

Примітка перекладача. У скелеті Laravel 13 константа записана через клас драйвера: Mysql::ATTR_SSL_CA (тобто Pdo\Mysql), а не PDO::MYSQL_ATTR_SSL_CA. Суть не змінюється: крім SSL-сертифіката, в опціях нічого немає.

То хто вирішує, скільки чекає підключення? Не php.ini, і це мене здивувало. У pdo_mysql є власне значення за замовчуванням: коли ви передаєте PDO будь-який масив опцій і не вказуєте в ньому ATTR_TIMEOUT, він чекає 30 секунд. Laravel передає опції завжди (у Connector є набір за замовчуванням: режим помилок, регістр тощо), тож у Laravel-застосунку це завжди 30.

Примітка перекладача. У документації PHP до PDO::ATTR_TIMEOUT число 30 не назване, але вихідний код розширення підтверджує слова автора: у ext/pdo_mysql/mysql_driver.c таймаут береться як pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30), і лише тоді, коли масив опцій передано.

Я перевірив це на PDO з адресою, до якої немає маршруту, і з default_socket_timeout, виставленим у 3, щоб довести, що він тут ні до чого:

$start = microtime(true);

try {
  new PDO('mysql:host=10.255.255.1;dbname=x', 'x', 'x', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  ]);
} catch (PDOException $e) {
  echo round(microtime(true) - $start, 2) . 's ' . $e->getMessage();
}
$ php -d default_socket_timeout=3 probe.php
29.84s SQLSTATE[HY000] [2002] Connection timed out

Зверніть увагу: приберіть масив опцій, і те саме підключення впаде через 3 секунди, бо голий new PDO() без опцій якраз зважає на default_socket_timeout. Тому швидкий тест на чистому PHP може показати не те число, яке бачить ваш застосунок.

30 секунд на запит - це вже погано. Спойлер: Laravel чекає чотири рази.

Laravel пробує чотири рази

Connection timed out є в списку помилок «втраченого з'єднання» Laravel, у LostConnectionDetector:

'SQLSTATE[HY000] [2002] Connection timed out',

Для помилки з цього списку Laravel повторює спробу у двох місцях.

Перше - конектор. Якщо створити PDO не вдалося через помилку втраченого з'єднання, Connector::createConnection() одразу пробує ще раз.

Друге - запит до бази. Laravel підключається ліниво, тож підключення відбувається всередині першого запиту. Коли цей запит падає з помилкою втраченого з'єднання, Illuminate\Database\Connection скидає підключення і виконує запит ще раз (тут показано Laravel 12; у 13 додано захист для запитів, що запускаються під час самого перепідключення, але основний запит однаково виконується повторно):

protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback)
{
  if ($this->causedByLostConnection($e->getPrevious())) {
    $this->reconnect();

    return $this->runQueryCallback($query, $bindings, $callback);
  }

  throw $e;
}

Виконати запит ще раз - означає ще раз підключитися, через той самий конектор, з його власним повтором. Отже, 2 спроби x 2 = 4 спроби підключення на один невдалий запит до бази.

Один невдалий запит до бази дає чотири спроби підключення: дві в конекторі, потім перепідключення і ще дві

Я виміряв це на Laravel 12.43 і на свіжому 13.40, з підключенням до хоста, до якого немає маршруту. Числа на обох однакові:

config(['database.connections.probe' => [
  'driver' => 'mysql',
  'host' => '10.255.255.1',
  'database' => 'x',
  'username' => 'x',
  'password' => 'x',
  'options' => [PDO::ATTR_TIMEOUT => 2],
]]);

$start = microtime(true);

try {
  DB::connection('probe')->select('select 1');
} catch (Throwable $e) {
  echo round(microtime(true) - $start, 2) . 's';
}
PDO::ATTR_TIMEOUT Один невдалий select 1
1 4.01 с
2 8.01 с
не задано 4 x 30 с, близько 2 хвилин

Останній рядок наша команда виміряла локально ще до виправлення: близько 30 секунд на спробу. Зі стандартним для nginx fastcgi_read_timeout у 60 секунд nginx здається на півдорозі, але воркер FPM за ним продовжує чекати.

А транзакції? Усередині відкритої транзакції повтор на рівні запиту до бази пропускається. Але beginTransaction() має власне перепідключення при втраченому з'єднанні, тож DB::transaction() однаково вийшов приблизно на 4x (3.8 с з таймаутом в 1 секунду).

Виправлення

Одна опція в кожному підключенні до MySQL, яке використовує застосунок:

'options' => extension_loaded('pdo_mysql') ? array_filter([
  PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
  // Секунди на одну спробу підключення; Laravel повторює спроби, тож невдалий запит чекає ~4x від цього значення.
  PDO::ATTR_TIMEOUT => (int) env('DB_CONNECT_TIMEOUT', 2),
]) : [],

Плюс DB_CONNECT_TIMEOUT=2 в .env.example з тією самою приміткою, що це значення на одну спробу.

Чотири деталі:

  • Не використовуйте 0 як «без обмеження». У нашому конфігу і в скелеті Laravel 13 array_filter() його відкидає, і підключення повертається до 30 секунд за замовчуванням. Скелет 12.x відфільтровує лише null, тож там 0 доходить до PDO, який тоді бере default_socket_timeout (за замовчуванням 60 секунд). «Без обмеження» не виходить у жодному випадку.
  • Це таймаут підключення, а не таймаут запиту до бази. На staging select sleep(5) проходив і з таймаутом 2.
  • Чому 2? У приватній мережі здорове підключення займає значно менше мілісекунди. Беріть значення в кілька разів більше за ваше найгірше здорове підключення: з TLS або з керованою базою, що стоїть далі, може знадобитися більше.
  • Обидва застосунки на спільному пулі отримали ту саму зміну. Якби виправили лише один, другий так само міг би забити весь пул.

Щоб це довести, я запустив наведену вище перевірку на копії справжнього конфігу mysql з підміненим хостом. Локально: 8.0 с. На staging: 8.0 с, після config:cache і перезавантаження FPM (opcache там не перевіряє зміни файлів).

Чого це не виправляє

Скажу чесно: це не тримає сайт живим, коли база недоступна. Запити, яким потрібна база, однаково падають. Змінюється те, як вони падають: помилка через 8 секунд замість воркера, що застряг на 2 хвилини. Воркери лишаються вільними, тож маршрути, які віддаються з кешу, і health check однаково можуть відповідати.

Чому не звичні підозрювані?

  • max_execution_time не допоміг би. У Linux він не враховує час, проведений у потокових операціях і системних викликах, а завислий connect - це саме воно.
  • request_terminate_timeout у FPM вбиває застряглий воркер, але кожен новий запит однаково впирається в те саме 30-секундне підключення.
  • Більший пул лише відкладає недоступність. Дві хвилини очікування на кожен запит заб'ють і 50 воркерів, просто пізніше. Збільшення pm.max_children - окреме рішення.
  • Повтор спроби в Laravel я залишив. При справжньому секундному збої саме повтор рятує запит, а з коротким таймаутом він коштує секунди, а не хвилини.
  • Circuit breaker або проксі на кшталт ProxySQL можуть падати ще швидше. Таймаут під ними однаково потрібен.

Ця стаття про MySQL. Redis і вихідні HTTP-виклики теж потребують власних таймаутів, а як тут поводиться PostgreSQL, я не перевіряв.

Коротко

  • Підключення Laravel до MySQL без PDO::ATTR_TIMEOUT чекає 30 секунд pdo_mysql на кожну спробу, а не значення з вашого php.ini.
  • Підключення, що впало з таймаутом, вважається «втраченим з'єднанням», тож Laravel робить 4 спроби на один запит до бази: близько 2 хвилин.
  • Під час мережевого збою це тримає всі воркери FPM, тож лягають і маршрути, яким база не потрібна.
  • Задайте PDO::ATTR_TIMEOUT і пам'ятайте, що справжнє очікування - 4x від цього значення.

Спробуйте на своєму застосунку: спрямуйте копію свого підключення mysql на 10.255.255.1 і заміряйте select 1.

Джерела

Переклад оригіналу: dev.to
0

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Переклади 09 листопада 2026

Як створити CLI-інструмент на PHP для AI-агентів і запускати його будь-де через CPX

Покроковий гайд від Tighten: пишемо на PHP консольний інструмент зі стабільним JSON і кодами виходу, публікуємо його на Packagist і запускаємо з будь-якого каталогу через CPX 2.0 - без composer global require.

7
Переклади 04 листопада 2026

Спільна мова для PHP: навіщо стандартній бібліотеці SortDirection і Time\Duration

Переказ колонки Тіма Дюстерхуса в блозі PHP Foundation: чому крихітні типи SortDirection і Time\Duration у PHP 8.6 важать більше за свій розмір, чому від'ємні тривалості заклали з першої версії і що дадуть Time\Instant та Time\Clock.

9

Вакансії за темою

TerraForce Нова
Сьогодні

Full-Stack Developer (Laravel, React/Angular)

Розробник full-stack для розробки нових функцій і підтримки існуючого функціоналу в командах проєктів. Основний стек: Laravel і PHP (від 2 років), React або Angular (від 2 років комерційного досвіду), MySQL, REST API, JavaScript, HTML/CSS. Потрібні добре знання ООП, патернів проєктування, орієнтація на якість коду; бажаний досвід розподілених систем або високонавантажених проєктів.

Full Stack Developer (PHP / React) Middle / Middle+

Full Stack розробник розвиватиме та підтримуватиме власний аналітичний продукт для перевірки контрагентів і оцінки ризиків, працюючи зі складною бізнес-логікою та великими обсягами даних. Основний стек: PHP 8.x, Laravel або Symfony, MySQL, React; також потрібні проєктування API, оптимізація SQL-запитів, Docker, CI/CD. Вимоги: комерційний досвід від 3 років, досвід підтримки legacy-коду, англійська B1+ для читання документації.

Starlight Media Нова
3 дні тому

Senior Full-Stack Developer (PHP / Go / Vue.js)

Senior Full-Stack розробник матиме розвивати й підтримувати існуючі веб-платформи, писати модулі, сервіси та API, а також розробляти клієнтську частину на Vue.js і Vanilla JS. Основний стек: PHP 8.x з Yii2 та/або Laravel, Go для сервісів, MySQL/MariaDB, REST API і Webhooks. Вимоги: досвід від 3 років, знання ООП і шаблонів проєктування, ES6+, Vue.js, SPA, Git, Docker і Composer; англійська B1-B2.

Пакети за темою

Kalimera

welldigit/kalimera

Interactive installer that scaffolds Laravel applications the Well Digit way.

v1.0.0 16

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,335 v2.5.1 13 28