Утром 20 августа 2026 года один из production-серверов начал вести себя нестабильно: сайты то открывались, то отвечали 500 Internal Server Error. Одновременно начал сбоить phpMyAdmin. При такой картине первыми под подозрение естественно попадают PHP, MariaDB, нагрузка или недавнее изменение кода. Но в этот раз причина находилась в другом слое.
От первого сбоя до полного восстановления production прошло около 7,5 часа с учётом пауз и параллельной работы с поддержкой. За это время удалось исключить данные и приложения, воспроизвести ошибку без DNS и клиентской сети, проверить второй независимый сайт и связать аварию с изменением пакета Nginx, установленным автоматическим обновлением Ubuntu.
Что было известно в начале
Симптом не был привязан к одной странице. HTTP 500 возникал на разных URL, включая простые запросы вроде /favicon.ico. В журнале Nginx постоянно повторялся сигнал:
no buffer space in script copy
Эта строка явно указывала на Nginx, но сама по себе ещё не доказывала конкретную директиву, модуль или внутренний дефект. Поэтому вместо подгонки фактов под первую версию важно было определить границы сбоя.
| Время | Событие | Значение для расследования |
|---|---|---|
| До 06:06 | Сайты работали штатно | Есть базовая точка нормальной работы |
| 06:06 | unattended-upgrade обновил пакет Nginx с 1.18.0-6ubuntu14.18 до .19 | Зафиксировано изменение общего инфраструктурного слоя |
| После 07:18 | В журнале массово появились alerts, а запросы начали завершаться HTTP 500 | Временная близость усилила гипотезу о регрессии или несовместимости |
| Во время диагностики | Тот же симптом подтвердился на втором независимом сайте | Код одного проекта перестал быть общим знаменателем |
После обновления до .20 | Сайты, административные части и phpMyAdmin восстановились | Одна контролируемая смена устранила симптом во всём контуре |
Почему проверка базы данных была правильной
То, что причина оказалась не в MariaDB, не делает первоначальную проверку базы ошибкой. Одновременный сбой phpMyAdmin и сообщение панели управления о невозможности записать полученные данные могли указывать на хранилище, базу или промежуточный слой.
Гипотезу проверили фактами: сервер MariaDB отвечал, SQL напрямую через независимый клиент выполнялся, зависших запросов не было, а процессы базы не демонстрировали аварийного состояния. После этого базу данных перестали менять. Это важное правило: после достаточного опровержения слоя не нужно продолжать его «лечить».
Диагностика по слоям
Вместо хаотичной замены компонентов система проверялась снизу вверх. Каждый тест должен был не просто добавить ещё один лог, а убрать из дерева причин целый класс гипотез.
| Слой | Что проверили | Вывод |
|---|---|---|
| Ресурсы хоста | Свободное место, CPU, I/O, vmstat | Общее исчерпание ресурсов не подтвердилось |
| MariaDB | Доступность, прямой SQL, список процессов | База принимала и выполняла запросы |
| Клиентская сеть | Прямой запрос на IP сервера с корректными Host и SNI | DNS, браузер и внешний маршрут не были причиной |
| Маршрут приложения | Товарные страницы, поиск, sitemap, favicon | Сбой не зависел от одного контроллера OpenCart |
| Отдельный worker | Завершение одного процесса Nginx | Ошибка перешла на следующий worker |
| Второй сайт | Независимый проект с другой PHP-цепочкой | Общим остался фронтовый слой Nginx |
Два сайта дали решающий A/B-тест
Основной сайт работал по цепочке Nginx → PHP-FPM → PHP. Второй независимый проект использовал Nginx → Apache → PHP. У них были разные кодовые базы, настройки приложения и способ передачи запроса в PHP, но оба получали одинаковый HTTP 500 с одним и тем же alert Nginx.
Это резко сузило поиск. Если два независимых приложения с разными backend-цепочками падают одинаково, а база и хост стабильны, нужно исследовать самый нижний общий компонент. В этой схеме им был Nginx.
Контрольные эксперименты, отделившие симптом от причины
- Прямой запрос к серверу.
curl --resolveнаправил HTTPS-запрос прямо на нужный IP, сохранив домен для Host и SNI. Ошибка осталась — внешний DNS и браузер исключены. - Простой ресурс. HTTP 500 на
/favicon.icoпоказал, что проблема шире сложного бизнес-маршрута. - Режим обслуживания. Ожидаемый 503 в maintenance-режиме и быстрый 500 в обычном пути были разными сигналами, а не одним общим «сайт упал».
- Замена worker. После завершения одного worker alert появился в другом процессе. Причина не находилась в одном повреждённом worker.
- Независимый проект. Та же ошибка на втором сайте значительно ослабила любую версию, связанную только с OpenCart.
Строка в apt history, которая изменила направление
Когда Nginx был определён как общий слой, история пакетного менеджера показала точное изменение: в 06:06 Ubuntu автоматически обновила Nginx с ревизии .18 до .19. Одного совпадения по времени недостаточно для доказательства, но оно сформировало сильную гипотезу, согласованную со всеми уже собранными фактами.
df -h
vmstat 1
mysqladmin ping
nginx -V
dpkg-query -W 'nginx*' 'libnginx*'
grep -R "upgrade nginx" /var/log/apt/history.log*
curl --resolve example.com:443:SERVER_IP https://example.com/favicon.ico
В следующем инциденте проверку последних системных изменений стоит делать раньше — сразу после появления alert от конкретного инфраструктурного компонента. Это не заменяет диагностику, но быстрее формирует правильный список гипотез.
Почему не стоило сразу делать downgrade
На production-сервере с панелью управления, несколькими сайтами, TLS-конфигурациями, дополнительными модулями Nginx и разными backend-цепочками поспешный откат способен создать новую несовместимость. Пакет — это не один бинарный файл: важны зависимости, модули, конфигурация и активное поколение worker-процессов.
Поэтому сначала были собраны доказательства, а затем выполнено одно контролируемое обновление до доступной ревизии 1.18.0-6ubuntu14.20. После этого сразу восстановились оба сайта, административные части и phpMyAdmin, а массовые HTTP 500 исчезли. Корреляция «одна общая смена — одновременное восстановление всех контуров» существенно усиливает RCA.
Что RCA доказывает, а что оставляет открытым
| Подтверждено фактами | Не подтверждено отдельным воспроизведением |
|---|---|
| Сбой затронул два независимых сайта | Конкретная директива Nginx, запускавшая ошибку |
| MariaDB, диск, CPU и I/O не объясняли симптом | Точный внутренний путь кода или конкретный CVE |
| Nginx сам возвращал 500 в прямом тесте | Была ли причина чистой регрессией пакета или взаимодействием с конфигурацией/модулем |
| Сбой начался после автоматического изменения пакета | Универсальная воспроизводимость на любом сервере с этой ревизией |
| Следующая ревизия устранила симптом во всём контуре | Был бы откат безопасен с данным набором зависимостей |
Доказательства поддерживают аккуратный вывод: production-сбой был локализован в общем слое Nginx, появился после автоматического перехода на пакетную ревизию .19 и прекратился после контролируемого перехода на .20. Точный внутренний механизм требует отдельного лабораторного воспроизведения.
Короткий playbook на следующий инцидент
- Зафиксировать время первого сбоя, точный статус, URL, Host, PID и строку журнала.
- Разделить внешний маршрут и сервер: выполнить прямой запрос с корректными Host и SNI.
- Проверить один простой ресурс и один динамический маршрут.
- Быстро исключить диск, память, CPU, I/O, базу и сетевую доступность.
- Найти второй независимый сервис на том же общем слое.
- Просмотреть
apt history, время изменения пакетов и конфигураций. - До вмешательства сохранить активную конфигурацию, версии пакетов и контрольные ответы.
- Выполнить одну контролируемую смену и повторить тот же набор canary-запросов.
- После восстановления проверить главную, админку, статический ресурс, динамический маршрут и второй сайт.
Как уменьшить риск автоматических обновлений
- Отделить обычные security-обновления от неконтролируемой смены критичного data plane; для Nginx определить окно и canary.
- Хранить инвентарь версий Nginx, модулей и пакетных зависимостей вместе с последней рабочей конфигурацией.
- Иметь проверенные планы обновления вперёд и отката до того, как произойдёт авария.
- Запускать синтетические health-checks для статического ресурса, динамического маршрута, админки и второго независимого сайта.
- Оповещать не только о доле HTTP 500, но и о новых alerts в журнале общего proxy-слоя.
- После смены пакета подтверждать, что трафик обслуживает ожидаемое поколение worker-процессов.
Главный вывод
Во время аварии очень легко ремонтировать то, что находится прямо перед глазами: PHP-код, SQL или страницу, которая первой попала под проверку. Но production — это цепочка зависимостей, и одинаковый симптом у двух независимых приложений часто важнее сотен строк лога одного из них.
Хорошая диагностика — не угадывание виновника. Это последовательное исключение слоёв, сохранение контрольных результатов и одна смена, эффект которой можно измерить. В этом кейсе одна строка в apt history стала решающей только потому, что вокруг неё уже была построена доказательная схема.