На связи Миша Иглицкий, бэкенд-разработчик в платформе Яндекс Еды. У нас есть PHP-монолит, в который пишут мало нового кода, поэтому он спокойно работает и не привлекает к себе лишнего внимания. Так оно бы и продолжалось, если бы не случился период повышенной нагрузки и понадобилось зарезервировать побольше мощностей для беспроблемной обработки подобного спроса.
Монолит отлично справился. Но после этой ситуации я случайно обратил внимание, что RPS в пиковые вечерние часы подозрительно совпадает с количеством ядер CPU, выделенных на весь монолит. Нехитрая арифметика показала: 100% загрузки одного ядра приходятся на 2,5 RPS. Я решил разобраться, что к чему.
Спойлер: дело оказалось не только в PHP.
❇️ Perforator vs PHP-монолит
У нас есть Perforator, который установлен почти на все хост-машины в дата-центрах, и с его помощью можно посмотреть, на что уходит процессорное время на подах. Проблема в том, что он видит почти всю работу PHP как вызов
zend_execute_ex.Хорошая новость: коллеги как раз допиливали поддержку PHP в Perforator, но пока не вмержили её в основную ветку. Однако нам залили на одну из хост-машин, где крутился инстанс PHP-монолита, специально собранную версию с поддержкой PHP. И я сразу же увидел пожирателя CPU.
Им оказалась функция
getRoutePattern для атрибута http.route. Изначально никто не заметил, что getRouteCollection не возвращает готовый закешированный список роутов, а каждый раз собирает его заново: проходит по всем YAML-файлам и читает аннотации в PHP-файлах.🦾 Исправление оказалось элементарным: теперь роуты читаются и сохраняются один раз на этапе сборки контейнера, а в рантайме берутся из кеша.
➖ Результат: потребление CPU в 99-м процентиле снизилось почти на 30%.
❇️ Инфраструктурные полтергейсты
На стороне PHP очевидных тормозов больше не было. Но я обратил внимание, что теперь топ потребления CPU выглядит так:
🟢 PHP: 20%
🟢 HAProxy: 19%
🟢 psql: 17%
🔍 Внутри psql мы увидели странный call stack. Оказалось, что в Debian psql по историческим причинам запускается через Perl-обёртку. Она выбирает нужную версию клиента, потому что в системе может быть установлено сразу несколько версий PostgreSQL (этой возможности уже больше 20 лет).
Также монолит на PHP не использует postgres-специфичные connection string, чтобы подключаться к primary-ноде БД — это решение вынесено на уровень HAProxy. А он не умеет делать это нативно, только проверять tcp connectivity. Так что для проверки были написаны специальные скрипты, которые и вызывали psql. В итоге один только запуск Perl-обёртки начал заметно потреблять ресурсы.
🦾 Проблему решили довольно просто: добавили agent-check в HAProxy. Передаём проверку состояния бэкенда отдельному агенту, который может делать то, чего HAProxy не умеет.
➖ Результат: строки с описанием бэкенда теперь длиннее, но зато работа стала быстрее и стабильнее. И после выкатки этого изменения на тестинге Perl пропал в профилях.
🔶 Читайте больше подробностей в статье на Хабре. Там я рассказал, почему HAProxy продолжал расходовать много CPU даже после наших изменений. А ещё поделился, как мы увеличили выделенную под APCu память и реализовали динамическую балансировку.
Подписывайтесь:
💬 @Yandex4Backend
📹 @YandexforBackend
