Клиент решил сэкономить на переезде: вместо того чтобы заказать миграцию у стороннего специалиста, перенёс пять серверов на новый хостинг силами техподдержки самого хостинга - это обычно делают бесплатно или почти бесплатно, в отличие от вдумчивого переезда с проверкой конфигурации. Через несколько дней после переезда сайты начали сыпаться один за другим, и клиент обратился уже ко мне - разгребать то, что перенос оставил после себя. В итоге заплатил за это заметно больше, чем стоила бы нормальная миграция с самого начала. Классическое «скупой платит дважды»: бесплатный перенос гарантирует, что данные окажутся на новом месте, но ничего не гарантирует про то, в каком состоянии там окажется конфигурация.
Инфраструктура - пять серверов на ISPmanager 6, root по SSH, у каждого свой пароль. Сайты - WordPress/WooCommerce, часть из них - «сетки» из десятков доменов-алиасов на одном движке: один флагманский домен обслуживает несколько десятков городских дублей-поддоменов с одинаковым контентом под разные регионы. Ниже - хроника одной рабочей недели: первый день после переезда, три отдельных рецидива на одном и том же сайте, аудит безопасности всего флота и разбор бага с раздувшейся на 5 миллионов строк таблицей. Идентифицирующие детали изменены.
День 1: первый день после переезда - три независимых пожара
Обход всех серверов
Задача звучала просто: «проверь их все, на некоторых уже перестали работать сайты». Вместо того чтобы гадать - обошёл все домены на всех серверах и сверил реальный ответ с DNS:
# ISPmanager не кладёт vhost'ы в дефолтные места nginx - сначала находим реальную раскладку
find /etc/nginx/vhosts -maxdepth 2 -type d
# затем на каждый домен: реальный статус + текущий A-record
curl -s -o /dev/null -w "%{http_code}" -H "Host: $domain" http://127.0.0.1/
dig +short $domain
Нашёл: два сайта реально сломаны на одном сервере (203.0.113.81), один ложный 403 (антибот-правило в .htaccess резало curl без User-Agent), и несколько доменов, для которых просто ещё не переключён DNS - это уже не проблема сервера.
fence-voronezh.example + fence-hub.example: диск 100%, WP_DEBUG и pm.max_children = 5
Один из городских доменов сети отдавал таймауты, флагманский домен сети - 502. Диагностика:
df -h / # 154G/154G, 100%
du -xh --max-depth=1 /var/www/httpd-logs # error.log одного из доменов = 63G
Причина роста лога - WP_DEBUG = true в проде: WordPress писал лавину deprecated-предупреждений на каждый запрос, а logrotate крутится раз в сутки и просто не успевал. Обрезал лог (truncate, не rm - процесс продолжает писать в тот же файловый дескриптор), выключил WP_DEBUG. Диск вернулся к 60% - но сайты всё ещё не открылись.
Вторая, более глубокая причина: pm.max_children = 5 у обоих пулов, притом что флагманский домен держит трафик по ~60 доменам-алиасам сразу. Поднял до 20:
sed -i 's/pm.max_children = 5/pm.max_children = 20/' \
/opt/php84/etc/php-fpm.d/pool.d/fence-hub.example.conf \
/opt/php84/etc/php-fpm.d/pool.d/fenceum.example.conf
systemctl reload php-fpm84
Load average упал с 7.78 до 4.05, оба сайта - стабильный 200. Этот же паттерн («маленький пул воркеров плюс что-то, что держит воркер дольше обычного») вернётся ещё дважды за неделю.
enclose-city.example: 502 в редакторе Elementor
Разработчик прислал скриншот 502 при открытии редактора Elementor. Причина - не PHP-лимиты (256M/300s, всё в порядке), а nginx:
upstream sent too big header while reading response header from upstream
У Elementor-редактора раздутые HTTP-заголовки (много cookie/nonce), не влезающие в дефолтный fastcgi_buffer_size. На соседнем сайте того же клиента на том же сервере это уже было исправлено раньше - просто забыли перенести настройку при миграции. Добавил в оба блока location @php (HTTP и HTTPS):
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
Системная защита от повторного разрастания логов
Раз WP_DEBUG оказался включён на одном сайте, стоило проверить остальные - и тот же баг нашёлся почти на всех сайтах клиента (шаблон при миграции просто не подчищали). На одном из доменов вдобавок WP_DEBUG_DISPLAY = true - ошибки PHP показывались прямо посетителям. Починил на 6 сайтах разом.
Но logrotate раз в сутки - недостаточная страховка сама по себе. Развернул независимый cron-вотчдог на всех 5 серверах:
#!/bin/bash
# diskguard.sh - обрезает любой лог в httpd-logs при превышении порога,
# независимо от logrotate/ISPmanager. Раз в 10 минут.
for f in /var/www/httpd-logs/*.log; do
size=$(stat -c%s "$f" 2>/dev/null) || continue
if [ "$size" -gt 1073741824 ]; then # 1 ГБ
: > "$f"
echo "$(date) truncated $f (${size} bytes)" >> /var/log/diskguard.log
fi
done
Сработал сам в течение того же часа - поймал забытый 3-ГБ лог на pavehub.example (203.0.113.82), который иначе разросся бы так же.
Три сайта с 500 на главной
Три связанных сайта отдавали 500 ещё до всех сегодняшних правок. WordPress глотал текст ошибки - вызвал front-controller напрямую через PHP CLI, чтобы увидеть настоящую причину:
php -r "require '/var/www/.../wp-load.php'; var_dump(error_get_last());"
Настоящей fatal-ошибки не было - значит где-то в коде осознанно вызывается wp_die(). Нашёл: на одном сайте забытый wp_die(); стоял перед рабочим wp_redirect(wp_login_url()), а не вместо него - типичный оставленный отладочный код. На двух других сайтах, работающих на общей кастомной CRM-теме, для одного пути анонимный посетитель корректно редиректился на логин, а для пустого пути (главная) та же функция в конце просто падала в безусловный wp_die(); exit();.
Убрал лишний wp_die() на первом сайте - но вылез вторичный баг: wp_login_url() уходил в цикл редиректов, потому что на сайте отдельно стоит защита wp-login.php через плагин All In One WP Security с секретным URL входа. Поправил редирект на реальный секретный путь.
На двух других сайтах при первой попытке скопировал цель редиректа с уже сломанной соседней ветки того же файла - это была уже моя собственная ошибка, обнаруженная только через час, когда клиент заново проверил ссылку и она всё ещё вела в 404. Разобрался до конца: правильная схема использовалась в 16 местах рабочего кода и в самом зарегистрированном правиле переписывания URL, а неверная - была единичной опечаткой в одном файле. Поправил не только цель редиректа, но и условия-исключения. Итог прозрачно указал в отчёте клиенту - и саму ошибку, и её исправление.
Вечер: перегрузка от краулинга ботов
Отдельно вечером того же дня (load average 15+ на 4 ядрах) один из сайтов перестал открываться. Причина - не FPM и не давешний фикс:
mysqld - 189% CPU, десятки одновременных SQL_CALC_FOUND_ROWS-запросов
Разбор трафика показал: массовый краулинг ссылок вида ?add-to-cart=XXXXX на каждой карточке товара - у WooCommerce на каждый такой URL кэш не срабатывает, и каждый клик бота гоняет тяжёлый запрос по каталогу. Из 20 000 последних запросов половина отдала 499 (клиент не дождался ответа), значительная часть - от краулера, который сканирует ссылки для превью в соцсетях и по документации игнорирует robots.txt, поэтому обычный запрет в файле не помогает.
Развернул fail2ban-джейл под конкретные User-Agent’ы, сначала протестировав фильтр на реальном логе (61% строк совпало - подтверждение масштаба, не ложное срабатывание):
fail2ban-regex /var/www/httpd-logs/pavehub.example.access.log /etc/fail2ban/filter.d/nginx-badbots.conf
За несколько минут забанили 83+ адресов, load average упал с 15-18 до 0.70. Раскатал тот же джейл на весь флот на случай похожего трафика в будущем.
День 2: тот же сайт, но на этот раз не шум, а атака
Тот же флагманский домен сети, симптомы похожие (сайт «виснет»), но причина другая. Пул PHP-FPM, поднятый три дня назад, снова забит полностью. На этот раз это не боты-краулеры, а сканер уязвимостей (42 IP, целится в бэкап-файлы конфигурации) и брутфорс через POST //xmlrpc.php со статичным поддельным User-Agent (12 IP).
awk '{print $12}' access.log | sort | uniq -c | sort -rn | head
grep -c 'POST //xmlrpc.php' access.log
45% всего трафика сайта оказалось этим самым XML-RPC-флудом. При таком масштабе точечные баны по IP - догонялки, атака росла быстрее, чем fail2ban успевал банить, load average дошёл до 16.63. Проверил, что легитимных потребителей xmlrpc.php на сайте нет (плагинов вроде Jetpack нет), и закрыл эндпоинт полностью на уровне nginx:
location = /xmlrpc.php { return 403; }
(добавлено перед location / в обоих блоках - HTTP и HTTPS; nginx схлопывает //xmlrpc.php в /xmlrpc.php, так что второй вариант блокируется тем же правилом). Плюс два новых fail2ban-джейла на всех 5 серверах: сутки бана за повторные POST на xmlrpc и неделя бана за один же запрос к бэкап-файлам - по такому пути легитимный посетитель не пойдёт никогда.
Load average упал с 16-18 до ~2, пул PHP-FPM отпустило.
На пике инцидента спросили: не поднять ли лимит воркеров ещё больше? Ответ был - нет:
На каждый воркер уходит около 192 МБ RAM. 20 воркеров - это уже почти 4 ГБ. Поднять до 40 - около 7.7 ГБ только под этот пул, а на сервере всего 11 ГБ, из них ещё MySQL и два других сайта. Своп уже частично использовался именно во время атаки при текущих 20 воркерах. Упёрся не в лимит, а в реальную атаку, которая теперь заблокирована - поднимать лимит вместо устранения причины означает при следующей атаке уйти не в контролируемые 502, а в своп и OOM-killer.
Это осознанное решение важно держать в голове рядом с Днём 5, где тот же самый рычаг поднимал уже на других серверах - но с явной проверкой памяти, а не по умолчанию.
День 3: тот же сайт, третий раз - и снова другая причина
«Сайт снова не работает» - но по логам не то же самое, что в первые два раза. На этот раз - MySQL:
df -h / # снова под 100%
du -sh /var/lib/mysql/*/ # binlog-файлы: 54 ГБ, 523 файла
Время хранения бинарных логов было выставлено на 7 дней - при активной записи это накопило полсотни гигабайт логов репликации, которая тут даже не используется. Штатная команда очистки зависла - MySQL не мог завершить операцию, потому что для записи собственного индекса покупки логов ему самому не хватало свободного места на диске (классический deadlock «нужен диск, чтобы освободить диск»). Пришлось руками:
# Ручное удаление binlog-файлов на уровне ФС, оставив последние 20 + активный
cd /var/lib/mysql && rm binlog.000001 ... binlog.000570
mysql -e "PURGE BINARY LOGS TO 'binlog.000571';" # синхронизирует индекс постфактум
Освободило 51 ГБ (100% → 67%). Снизил retention фактически, а не только по конфигу:
[mysqld]
binlog_expire_logs_seconds = 86400 # было 604800
SET GLOBAL binlog_expire_logs_seconds = 86400; -- применить без рестарта
Распространил тот же retention на весь флот, кроме серверов, где бинлог вообще выключен.
Даже после этого фикса каталог сайта весил заметно больше ожидаемого. Причина - плагин кэширования: его файловый кэш страниц сам по себе никогда не удаляет физически устаревшие файлы с диска, только перестаёт их использовать по истечении срока. В сочетании с тем, что флагманский домен сети отвечает на любой Host-заголовок (сервер настроен как default_server для всей сети городских доменов), это создало больше 300 000 файлов кэша, часть - многолетней давности. Очистка каталога вручную освободила 48 ГБ; тот же паттерн нашёлся ещё на 4 сайтах на других серверах, в сумме ещё около 57 ГБ.
Развернул постоянный предохранитель - cacheguard.sh, cron раз в сутки на всех 5 серверах, который трёт файлы кэша старше 3 дней в кэш-директории любого сайта на сервере:
MAX_AGE_DAYS=3
for dir in /var/www/*/data/www/*/wp-content/cache; do
[ -d "$dir" ] || continue
find "$dir" -type f -mtime +$MAX_AGE_DAYS -delete
find "$dir" -mindepth 1 -type d -empty -delete
done
День 4: проактивный аудит атак и баг с 5-миллионной таблицей
Поиск того, что защита ещё не покрывает
По собственной инициативе прогнал логи всех 5 серверов на предмет паттернов атак, для которых ещё нет банов. Нашёл и подтвердил действующего атакующего (один и тот же IP на двух разных серверах клиента одновременно): time-based blind SQL-инъекцию через REST API с ротацией User-Agent на каждый запрос, энумерацию пользователей WordPress через штатный REST-эндпоинт, пробы веб-шеллов через обфусцированное кодирование путей, и попытку эксплуатации известной публичной уязвимости через характерный User-Agent (проверил - не сработало, движок не уязвим).
Добавил новые fail2ban-джейлы на весь флот. Отдельно проверил и осознанно не забанил второй подозрительный по объёму запросов IP - по паттерну обращений (правки контента сразу на двух сайтах клиента, реальный fingerprint браузера) это оказался собственный админ клиента, а не атакующий.
Таблица опций на 5,3 миллиона строк
Ранее уже был известен риск («нагрузка на БД, требует отдельного разбора» из отчёта первого дня) - на этот раз докопался до конкретной причины:
SELECT COUNT(*) FROM wp7k2_options; -- 5 300 000+ (норма - тысячи)
SELECT option_name, COUNT(*) FROM wp7k2_options
WHERE option_name LIKE '_transient_%'
GROUP BY option_name ORDER BY 2 DESC LIMIT 5;
Корень - в дочерней теме сайта: ключ кэша строился с использованием полного URI запроса. Поскольку URI запроса уникален почти для каждого визита (особенно с учётом ботового трафика по фильтрованным ссылкам каталога), каждый визит создавал новую, никогда не переиспользуемую и не истекающую запись transient. Что интересно - сам вызов создания transient в этом месте кода уже был закомментирован предыдущим разработчиком, а рядом даже нашёлся его же забытый скрипт-костыль, вручную удаляющий такие записи батчами - то есть проблема была известна раньше, но не вылечена системно.
Безопасная батч-очистка (важно - не блокирующий DELETE сразу по 5 млн строк, а пакетами, с проверкой свободного места на каждом шаге):
while true; do
QUEUED=$(mysql -N -B "$DB" -e "
TRUNCATE cleanup_ids;
INSERT INTO cleanup_ids SELECT option_id FROM wp7k2_options
WHERE option_name LIKE '_transient_timeout_%' AND option_value < UNIX_TIMESTAMP()
LIMIT 20000;
SELECT COUNT(*) FROM cleanup_ids;")
[ "$QUEUED" -eq 0 ] && break
mysql "$DB" -e "
DELETE v FROM wp7k2_options v
JOIN wp7k2_options t ON v.option_name = REPLACE(t.option_name,'_transient_timeout_','_transient_')
JOIN cleanup_ids c ON t.option_id = c.option_id;
DELETE t FROM wp7k2_options t JOIN cleanup_ids c ON t.option_id = c.option_id;"
AVAIL=$(df --output=avail -B1 / | tail -1)
[ "$AVAIL" -lt 104857600 ] && { echo "SAFETY ABORT: <100MB"; break; }
done
Удалило 2 373 262 строки (5,3 млн → 30 620). Но DELETE не сжимает файл таблицы на диске при пофайловом хранении InnoDB - освобождает место только внутри самого файла. Чтобы реально вернуть диск, таблицу пересобрал (дешевле, чем полная оптимизация - не требует двойного места таблицы во временном хранилище):
mysqldump acc48210_pavehub wp7k2_options > wp7k2_options_backup.sql
mysql -e "RENAME TABLE wp7k2_options TO wp7k2_options_old;
CREATE TABLE wp7k2_options LIKE wp7k2_options_old;"
mysql acc48210_pavehub < wp7k2_options_backup.sql
# проверка: строки на месте, критичные опции (siteurl, active_plugins, rewrite_rules) целы
mysql -e "DROP TABLE wp7k2_options_old;"
Освободило около 90 ГБ.
Отдельно объяснился и сам факт, почему баг возвращался, хотя код давно исправлен: вызов создания transient в проблемном месте уже был закомментирован в коде, но строки продолжали плодиться. Дело оказалось не в коде, а в устаревшем opcache: правка на диске была, но воркеры PHP-FPM, годами живущие в памяти, продолжали исполнять старый скомпилированный байткод. Один systemctl reload php-fpm84 - без единой правки кода - немедленно остановил рост (проверил эмпирически: плоский счётчик строк 90+ секунд после релоада против тысяч новых строк в час до него).
Отдельным вопросом всплыло, почему в отчёте не было сказано про открытую проблему с нагрузкой на БД - это был тот самый пункт из отчёта первого дня, который провисел нерешённым несколько дней. План из двух частей: увеличил буферный пул InnoDB с дефолтных 128 МБ до 2.5 ГБ онлайн, без рестарта:
SET GLOBAL innodb_buffer_pool_size = 2684354560;
[mysqld]
innodb_buffer_pool_size = 2684354560
Плюс включил файловый кэш страниц у уже установленного, но выключенного плагина кэширования - не руками (риск ошибиться в структуре кэша), а через официальный API плагина:
$cache = WPO_Page_Cache::instance();
$cache->config->update(['enable_page_caching' => true]);
$cache->enable(true); // сам создаёт advanced-cache.php и включает WP_CACHE в wp-config.php
Проверка сразу после показала false у статуса включённости - не баг, просто константа кэша начинает действовать только со следующей загрузки wp-config.php, а не в уже запущенном CLI-процессе. Подтвердил реальным трафиком после reload - файлы кэша начали появляться, load average упал с 1-3 до ~0.9. Отдельно проверил по прямому вопросу клиента - не забьёт ли новый кэш диск так же, как раньше: нет, предохранитель cacheguard.sh уже покрывает и этот путь, маска у него достаточно общая.
Скрытая проблема, найденная по отдельному запросу
Проверка "не было ли проблем с доступностью за последние 30 минут" на ещё одном сайте флота нашла 31 настоящую ошибку 500, не покрытую ни одним существующим джейлом - все по одному паттерну, эксплуатирующему баг ядра WordPress в одном из batch-эндпоинтов REST API. Добавил отдельный джейл на все 5 серверов - легитимного использования такого эндпоинта на этих сайтах нет.
День 5: резонанс двух настроек - и аудит всего флота
Пятиминутный простой
Плановая проверка ("не было ли проблем сегодня") на паре сайтов одного сервера. На одном всё в порядке - только уже известный сканер, забаненный джейлом с Дня 4. На другом - кластер из 10 ошибок 500/504 с одного и того же клиента (легитимный админ, опознанный ранее), но на разных URL - главная, админка, AJAX, форма обратной связи, редактор, даже favicon - в пятиминутном окне.
Разброс URL при одном источнике - верный признак не бага конкретной страницы, а исчерпания общего ресурса. В error-логе - прямое подтверждение:
upstream timed out (110: Connection timed out) ... upstream: "fastcgi://unix:/var/www/php-fpm/4.sock"
Конфиг пула (найден не с первой попытки - на этой инфраструктуре реальные пулы лежат в /opt/phpXX/etc/php-fpm.d/pool.d/, а не в стандартном месте):
pm = ondemand
pm.max_children = 5
и в подключаемом конфиге сайта:
php_value[max_execution_time] = 300
Тот же самый паттерн, что и в самый первый день - маленький пул плюс завышенный лимит времени выполнения. Один подвисший запрос (форма обратной связи или вызов редактора) занял воркер на срок вплоть до max_execution_time (окно инцидента почти ровно 300 секунд), параллельный фоновый трафик редактора (автосохранение, heartbeat) съел оставшиеся воркеры - и любой новый запрос вставал в очередь к nginx, получая 504.
sed -i 's/^pm.max_children = 5/pm.max_children = 12/' \
/opt/php84/etc/php-fpm.d/pool.d/enclose-city.example.conf
/etc/init.d/php-fpm84 configtest # php-fpm84 не в PATH - проверка синтаксиса только так
systemctl reload php-fpm84
От разового фикса - к аудиту всего флота
Логичный следующий шаг: та же ли беда на остальных серверах? Прогнал по всем пяти серверам поиск всех конфигов пулов под всеми версиями PHP. Картина: на двух серверах лимит уже подняли раньше в рамках этой же недели, но ещё на трёх сайтах стоял дефолт в 5 воркеров - в том числе на сайте с 120 000+ запросов в день, в 8 с лишним раз больше трафика, чем у сайта, где как раз и случился сегодняшний инцидент. Тот же риск, но с гораздо более высокой вероятностью реализации.
Отдельно проверил сайты, которые вообще не используют PHP-FPM, а идут через связку nginx → Apache/mod_php:
grep -E "proxy_pass|fastcgi_pass" /etc/nginx/vhosts/.../site.conf
# proxy_pass http://127.0.0.1:8082;
grep MaxRequestWorkers /etc/apache2-isp-php84/mods-available/mpm_prefork.conf
# MaxRequestWorkers 150 ← общий дефолт ISPmanager, запас большой, не проблема
Фикс - но с проверкой памяти, в отличие от Дня 2
Прежде чем поднимать лимит фронтально до 20 везде, вспомнил собственное же решение с Дня 2 - там отказал именно из-за нехватки RAM. Проверил доступную память на всех затронутых серверах перед фиксом:
free -h
# total used free available
# сервер A: 7.8Gi 1.4Gi 1.1Gi 6.3Gi
# сервер B: 7.8Gi 2.4Gi 676Mi 5.3Gi
# сервер C: 7.8Gi 1.7Gi 1.4Gi 6.0Gi
5-6 ГБ свободно на каждом - контекст принципиально другой, чем на перегруженном сервере в День 2, где ещё и активная атака шла одновременно. Поднял до 20 везде, где стоял дефолт 5:
sed -i 's/^pm.max_children = 5/pm.max_children = 20/' "$conf"
systemctl reload php-fpm84
Итог по флоту: минимальный лимит воркеров на любом php-fpm-сайте - 12, у всех остальных - 20.
Итог
За неделю закрыл три независимых инцидента подряд на одном и том же домене, довёл до конца ранее незакрытую проблему с нагрузкой на БД, нашёл и обезвредил реального атакующего (не спутав его с админом клиента), развернул три постоянных предохранителя (защита от разрастания логов, защита от разрастания кэша, набор fail2ban-джейлов) на весь флот из пяти серверов, а не только на тот сайт, где проблема впервые проявилась. Всё, что перенос "бесплатными руками" хостинга оставил не перенесённым или не проверенным, - настройки PHP-FPM, nginx, забытые дебаг-флаги - было найдено и закрыто одно за другим.
Выводы
- pm.max_children = 5 (дефолт ISPmanager) - тикающая бомба, а не безопасное значение по умолчанию. За неделю один и тот же паттерн - маленький пул плюс что-то, что удерживает воркер дольше обычного - воспроизвёлся трижды на разных сайтах разных серверов.
- Одинаковый симптом не значит одинаковая причина. Три инцидента на одном и том же домене подряд ("сайт не работает") оказались тремя независимыми причинами: нехватка воркеров, XML-RPC-атака, переполнение диска бинлогами MySQL. Диагностику нужно проводить заново каждый раз, не подставляя вчерашний ответ.
- Поднимать лимит воркеров - не универсальный фикс. На перегруженном сервере под активной атакой это может увести систему в своп и OOM-killer вместо контролируемых 502. Перед поднятием - обязательно проверять доступную память и то, не является ли текущая перегрузка симптомом атаки, которую надо сначала устранить, а не заливать лимитами.
- Кластер ошибок с разными URL от одного источника в узком временном окне - почти всегда признак исчерпания общего ресурса (пул воркеров, соединения к БД), а не бага конкретной страницы.
- На ISPmanager с несколькими версиями PHP реальные конфиги php-fpm-пулов лежат не там, где их ищут по умолчанию, а бинарник интерпретатора может не быть в PATH - об этом стоит помнить при аудите лимитов конкурентности, и проверять не только php-fpm, но и Apache MPM для сайтов, идущих через связку nginx → Apache.
- Кэш-плагины сами по себе не убирают логически устаревшие файлы с диска - только перестают их использовать. Без внешнего предохранителя это тихо копит десятки гигабайт годами.
- DELETE не возвращает место на диск при пофайловом хранении InnoDB - только освобождает пространство внутри файла таблицы. Для реального возврата диска после массовой чистки - пересборка таблицы, дешевле по временному месту, чем полная оптимизация.
- Правка кода без перезагрузки PHP-FPM может не сработать вообще - механизм автоматического обнаружения изменений штатно должен подхватывать правки за секунды, но на практике долгоживущие воркеры иногда продолжают исполнять старый байткод. Если фикс "должен был сработать, но не работает" - первое, что стоит попробовать, это перезагрузка PHP-FPM.
- Бесплатный перенос от техподдержки хостинга переносит данные, а не конфигурацию. Ни один из описанных выше багов не был новым - все они существовали ещё на старом хостинге в каком-то виде, но всплыли или обострились именно после переезда, потому что никто не сверил лимиты, дебаг-флаги и nginx-конфиги построчно между старой и новой площадкой.
Похожая ситуация после переезда, или просто хочется, чтобы кто-то прошёлся по флоту серверов до того, как он начнёт сыпаться сам, - пишите, разберём.