Вопрос производительности PHP-кода для Badoo один из самых важных. От качества PHP-бэкенда напрямую зависят количество ресурсов, которые мы тратим на разработку и эксплуатацию, скорость работы сервиса и впечатление, которое он производит на пользователей.
Поэтому темой третьей встречи сообщества PHP-разработчиков в нашем офисе мы сделали производительность бэкенда и пригласили к обсуждению коллег из «Авито» и «Мамбы».
Читайте под катом расшифровку дискуссии, в которой мне повезло быть модератором: как устроена инфраструктура трёх компаний, как мы измеряем производительность и на какие метрики ориентируемся, какие инструменты используем, как делаем выбор между железом и оптимизацией.
А 15 февраля приходите на следующий Badoo PHP Meetup: обсудим легаси.
Мы расшифровали только часть дискуссии, которая показалась нам наиболее интересной. Полная версия доступна на видео.
Эксперты:
У меня есть притча.
Примерно раз в полгода мы смотрим на метрики и ищем, что тормозит, плохо работает, что надо оптимизировать. Однажды обратили внимание на наш контейнер зависимостей от Symfony, который разросся до 52 000 строк. Решили, что это он, подлец, во всём виноват: 20 мс оверхеда на каждый запрос. Мы его распиливали. Мы его уменьшали. Мы как-то пытались его разделить, но ничего не помогало.
А потом оказалось, что у нас есть антиспам, которому нужно сходить в 20 баз, чтобы выполнить все необходимые запросы.
Решения, которые находятся первыми, не всегда правильные. Смотрите лучше в трейсы, логи и бенчмарки своих запросов, а не ломитесь напрямую. Вот такая история.
У нас достаточно большая экосистема на PHP, поэтому мы периодически занимаемся оптимизацией. Растём, достигаем какой-то планки по CPU, понимаем, что нужно либо докупать железо, либо оптимизировать. Взвешиваем аргументы за и против каждого варианта и решаем. Чаще всего — в пользу оптимизации, потому что железа надо много.
В один из таких моментов мы выделили целую группу, которая занималась тем, что искала разные неоптимальные штуки в PHP-скриптах и оптимизировала. Это происходило буквально «по капле»: здесь нашли процент, там нашли процент — несколько человек в течение месяца находили по проценту. В какой-то момент рядом оказался наш сишник einstein_man. Он решил посмотреть, что можно сделать: зашёл вечером, запустил Perf, нашёл пару проблем в экстеншенах PHP — и ускорил всё за пару вечеров на 13%!
У меня есть две истории. Одна о фейле, другая — про суперразработчика.
О фейле. У нас много микросервисов, в том числе на PHP. Все работают в Kubernetes. Я работал над одним из этих микросервисов. Там была значительная утилизация CPU: потратили неделю на оптимизацию и поиск проблем. Оказалось, что один из разработчиков для подсчета code coverage тестов в своём (другом) сервисе добавил в базовые образы Xdebug, после чего все микросервисы в продакшене целый квартал работали с включённым Xdebug! Через квартал мы это обнаружили, исправили базовые образы — и начали ловить неожиданные подарки: я перевыкатил свой сервис, и он стал работать быстрее. При каждом деплое у нас образ сервиса пересобирается, и теперь без Xdebug.
История успеха. У нас много микросервисов, и их становилось всё больше и больше. В этой ситуации количество RPC-вызовов превращается в проблему. Например, на карточке объявления — а это одна из самых частотных страниц в «Авито» — для отрисовки страницы задействовано порядка 30 микросервисов. Причём это всё сделано не очень явно: вроде как вызываешь какую-то абстракцию, а под ней последовательно выполняются пять RPC-вызовов в другие сервисы.
С годами карточка объявления сильно деградировала. Один сильный разработчик целый квартал бился, оптимизировал, выводил все RPC-вызовы наружу. Когда он смог это сделать, он их запараллелил через Guzzle multi request — и получил вместо 30 последовательных синхронных запросов те же самые 30 запросов, но параллельных, что очень сильно ускорило работу. После этого рефакторинга время ответа карточки равняется максимальному времени ответа любого из сервисов. Но ему понадобился целый квартал, чтобы оптимизировать/переписать код отображения карточки объявления.
У нас порядка 15 000 RPS. Кластер из 80 FPM-серверов, закупленных несколько лет назад. На каждом кластере в статику запущен FPM (максимально в 50 чайлдов). Там загружено около десяти в пик, в прайм-тайм. Среднее время ответа — 100 мс, и мы стараемся его удерживать (когда оно переваливает за 100 мс, мы начинаем поиск тормозящих штук).
У нас есть собственная система мониторинга производительности. Мы раскидали в коде очень много счётчиков, порядка 120 на запрос. Мониторим очень много ивентов, которые происходят внутри кода на PHP.
У нас всё стандартно: nginx, PHP-FPM. Примерно 600 серверов с FPM. Если вообще про PHP говорить, то, наверное, есть ещё около 300 серверов разного назначения типа скриптовых, бэк-офисных и прочих.
Из особенностей конфигурации есть две. Во-первых, у нас есть BMA-прокси — это прокси для мобил. То есть перед тем как запрос прилетает в nginx, он попадает на специальный прокси, который держит persistent connection и отправляет запросы в nginx. Вторая особенность — иногда нужно выключать CLI opcache (у нас он включён на скриптовых машинах). Когда-то мы его не выключали и теряли 30% CPU на этом. После того как осознали свою ошибку, удивились, как много можно сэкономить одной настройкой.
У нас есть патчи PHP, но они почти не связаны с производительностью.
Есть момент с конкурентным локом APCu — когда нужно много записывать в один и тот же ключ. Внутренняя архитектура APCu построена таким образом, что есть глобальный лок на ключ, и при интенсивной записи начинаются тормоза. Поэтому у нас там есть кастомное расширение, которое решает эту проблему. Это только отчасти относится к производительности, поскольку влияет на время ответа, но не на потребление CPU.
У нас около 2 млн реквестов в минуту (~33 kRPS). Монолитное приложение, написано на PHP, пишется уже более 11 лет. Оно в фазе быстрого роста. Когда компания стартовала, было 65 LXC на 65 физических серверах. На каждом контейнере с приложением запущены PHP-FPM, nginx и вспомогательный софт для метрик, логирования. Ничего особенного.
За эти годы мы ни разу не прибавляли железо для монолита. У нас растут посещаемость, количество объявлений, количество пользовательских транзакций, и постоянно мы оптимизируем, улучшаем код, оптимизируем софт. Потребление CPU и памяти в течение последних лет падает: 65 контейнеров для монолита нам сейчас хватает за глаза.
У нас есть система сбора логов. Она логирует два показателя — время от старта FPM до shutdown function и до конца выполнения скрипта. Вторая метрика нужна, чтобы видеть, что происходит после shutdown function.
Мы меряем JS. Это, на самом деле, так себе метрика, очень часто нарушаются сетевые каналы. В результате загрузка где-нибудь в российской глубинке начинает тупить. Поэтому мы так смотрим: «О, прыгнуло — значит, где-то что-то отвалилось». Плюс сторонняя реклама очень сильно искажает метрику. И, главное, приходят спамеры, и это вообще какой-то рандом.
Кстати, мы раньше очень активно использовали Pinba от Badoo. Мне она и сейчас нравится. Большинство метрик собирали ею, но потом перешли на StatsD-протокол. Сейчас мы делаем замеры с разных точек: с фронта, с серверов перед приложением, с nginx и с самого PHP-приложения. У нас есть выделенная команда перформанса. Она начинала с перформанса фронта, но потом переключилась и на бэк. С фронта она не только JS, CSS и прочую статику собирает, но заодно и время ответа сервера. В первую очередь мы ориентируемся на время ответа приложения.
У нас всё похоже на то, что рассказали ребята. При помощи классической Pinba для PHP мы измеряем время работы PHP-скрипта с точки зрения PHP. Но также у нас есть, например, Pinba для nginx, которая измеряет время ответа с точки зрения nginx. На клиенте тоже собираем метрики.
На что мы смотрим? С одной стороны, на время ответа. Оно не связано с планированием ресурсов. Если оно плохое, его надо улучшать, потому что это, по сути, качество сервиса. Другое дело — что надо как-то планировать железо. У нас команды ITOps и мониторинга мониторят всё железо. Выставлены какие-то планки по сети, по диску. Есть какие-то значения, после которых происходит alert — и мы что-то делаем. Как показала практика, обычно мы оптимизируем по CPU: упираемся именно в него.
У нас PHP-приложение само себя меряет и в register_shutdown_function() выбрасывает метрики. На каждом LXC стоит StatsD-сервер, который собирает эти метрики и отправляет дальше через коллекторы в Graphite cluster (в том числе ClickHouse), где хранятся данные. Это самодиагностика.
Также на каждом контейнере стоит nginx, то есть nginx + PHP-FPM. С nginx мы собираем внешние метрики, относящиеся ко времени работы PHP-приложения. Ещё перед ними стоит на отдельных серверах (мы их называем avi-http) nginx, выполняющий базовый роутинг, где также собираются более высокоуровневые метрики, такие как время ответа, количество 500-х кодов ответов и другие.
Мы написали свою собственную тулзу. Когда Pinba только вышла — в 2012 году, очень давно — это был модуль к MySQL, который по UDP принимал что-то такое. Сложно было вынимать графики, он был не очень оптимизирован по производительности. И мы не придумали ничего лучше, чем написать свою собственную штуку, которую назвали Better Than Pinba. Это просто сервер счётчиков, который принимает их с PHP-клиента.
Мы раскидали в коде очень много таймеров: каждый раз, когда мы хотим что-то измерить, в коде выставляем старт и стоп таймера. Модуль сам посчитает время выполнения счётчика, агрегирует накопленные счётчики в пакет, отправит их в демон. Интерфейс сам извлечёт всё необходимое и построит связные графики по нужному счётчику.
Одна из проблем Pinba заключалась в отсутствии собственного интерфейса — нужно было перекладывать данные в RRD (тогда ещё был такой мрак). Поэтому мы написали свой собственный интерфейс. Каждый раз, когда мы видим, что скакнуло, мы можем установить скрипт. В скрипте затегированы все агрегированные счётчики, которые у нас отправляются. Мы можем посмотреть, где какой счётчик вырос, либо увеличилось время ответа у счётчика, либо увеличилось количество счётчиков.
Видно, когда происходит падение производительности. Мы начинаем копать в эту сторону. До PHP 7 мы использовали XHProf, потом он перестал у нас собираться. Поэтому мы перешли на Xdebug. Xdebug мы тыкаем только тогда, когда видна проблема.
Это распространённое мнение — что XHProf на PHP 7 не собирается. Это правда, но лишь отчасти. Если взять XHProf из мастера, он действительно не соберётся. Но если на GitHub переключиться на ветку, которая называется Experimental (или что-то вроде того), то там всё работает для PHP 7 нормально, production ready. Проверено.
Нет, я переключался. У меня не заработало.
Хочу добавить про Pinba. Вы в какой-то мере стали пророками. Нам тоже в определённый момент перестало хватать производительности. Мы сделали Pinba2, которая очень быстрая. Можно её использовать как замену Pinba.
У нас всё скромно. Мы только взяли направление перформанса в работу: собираем метрики, вроде времени ответа. Используем StatsD. Мы пока не используем никакие профилировщики на регулярной основе, но я точно знаю, что некоторые команды используют их в своих микросервисах, написанных на PHP. По-моему, даже кто-то New Relic юзает. Но в контексте основного монолитного приложения пока мы только подходим к этому.
Железячная история у нас мониторится в Grafana, Zabbix. Что касается конкретно PHP-части, то у нас есть Pinba, у нас есть куча таймеров; строим по ним удобные графики. Используем XHProf, на продакшене гоняем его для части запросов. У нас всегда доступны свежие профайлы XHProf. У нас есть liveprof: это наш инструмент, можете почитать о нём в том числе в моей статье. Это всё происходит автоматически, надо просто смотреть. Используем phpspy. Он у нас не запущен постоянно: когда кто-то хочет что-то посмотреть, он заходит на машину, снимает профиль. В принципе, как и в случае с Perf.
С XHProf та же история. Мы когда-то давно его использовали: это была личная инициатива пары разработчиков, и, по сути, не взлетело. Он перестал собираться. Мы собираем кучу метрик с вызовов роутеров, контроллеров, различных моделей. Порядка 60–70% внутренней сети дата-центра занято UDP-пакетами с метриками. На данный момент нам этого хватает. Сейчас будем искать новые места для оптимизации.
Монолитное приложение минимум пять лет работает на 65 LXC. Оптимизируем код, улучшаем его: ему ресурсов хватает. У нас основной capacity planning уходит в Kubernetes, где порядка 400 более или менее живущих микросервисов написаны на PHP/Go. Мы потихоньку кусочки от монолита отрезаем, но он всё равно растёт. Мы его не можем остановить.
Вообще PHP — крутой язык. На нём быстро реализуется бизнес-логика.
В первую очередь команды ITOps и мониторинга следят за тем, чтобы ресурсов хватало. Если мы начинаем приближаться к пороговому значению, коллеги это замечают. Наверное, они в первую очередь ответственны за глобальный capacity planning. У PHP-части основной ресурс CPU, поэтому мы за ним сами следим.
Мы себе поставили такую планку: мы не должны «съедать» более 60% кластера. 60%, а не 95%, потому что у нас гипертрединг, который дополнительно выжимает из процессора больше, чем можно выжать без него. За это мы платим тем, что у нас после 50% потребление CPU может расти непредсказуемым образом, потому что гипертрединг-ядра не совсем честные.
Мы делаем деплой и видим, что у нас что-то провалилось, — вот такой capacity planning. На глазок! У нас есть некий запас по производительности, который нам позволяет так делать. Мы его стараемся придерживаться.
Кроме того, мы делаем оптимизацию постфактум. Когда мы видим, что что-то отвалилось, мы откатываемся, если совсем всё плохо. Но такого практически не бывает.
Либо мы просто видим, что «вот здесь неоптимально, сейчас мы быстро всё исправим и всё будет работать как надо».
Мы особо не заморачиваемся: это очень сложно, а выхлоп будет не очень большой.
Для отправки событий между сервисами используется Kafka, для обеспечения взаимодействия между сервисами — JSON-RPC, но не full, а его упрощённая версия, от которой мы не можем избавиться. Есть более быстрые реализации: те же protobuf, gRPC. Это есть в наших планах, но точно не в приоритете. Имея более 400 микросервисов, трудно их все портировать на новый протокол. Есть куча других мест для оптимизации. Нам сейчас точно не до этого.
У нас как таковых микросервисов нет. Есть сервисы, тоже есть Kafka, собственный протокол поверх Google protobuf. Наверное, мы бы использовали gRPC, потому что он классный, у него есть поддержка всех языков, возможность очень легко связывать разные куски. Но когда нам это понадобилось, gRPC ещё не было. Зато был protobuf, и мы взяли его. Поверх него добавили разные штуки, чтобы это была не просто сериализация, а полноценный протокол.
У нас тоже нет микросервисов. Есть сервисы, в основном написанные на C. Мы используем JSON-RPC, потому что это удобно. Ты просто открыл сокет, когда свой сишный код отлаживаешь, и быстро написал, что хотел. Тебе что-то вернулось. С protobuf сложнее, потому что нужно какие-то дополнительные инструменты использовать. Небольшой оверхед есть, но мы считаем, что за удобство нужно платить, а это не большая цена.
У нас есть большие таблицы, монолитные. Есть шард. Шард альтерится довольно быстро, потому что идут сразу много параллельных alter. Большая таблица с профилями альтерится порядка трёх часов. Мы используем перконовские тулзы, которые не лочат её на чтение и на запись. Кроме того, мы перед alter деплоимся, чтобы код поддерживал и то, и другое состояние. После alter тоже деплоимся: деплоиться — быстрее, чем применять какие-то схемы.
У нас самое большое хранилище (мы его называем «споты») — это огромная шардированная база данных. Если мы берём таблицу «User», то у неё очень много шардов. Я навскидку не скажу, сколько конкретно таблиц будет на одном сервере: идея в том, что их много мелких. Когда мы изменяем схему, по сути, мы просто делаем alter. На каждой мелкой таблице он происходит быстро. Есть другие хранилища — там уже другие подходы. Если одна огромная база, там есть перконовские тулзы.
В общем, мы используем разные штуки по потребностям. Самое частое изменение — это изменение этой огромной шардированной базы спотов, в которой у нас уже выстроен процесс. Это всё работает очень просто.
То самое монолитное приложение, которое сервит большую часть трафика, деплоится пять-шесть раз в день. Практически каждые два часа.
В плане работы с базой данных это отдельный вопрос. Есть миграции, они накатываются автоматически. Это ревьювят DBA. Есть опция скипнуть миграцию и вручную накатить. Миграция будет автоматически накатываться в staging при тестировании кода. Но на продакшене, если там какая-то сомнительная миграция, которая утилизирует кучу ресурсов, то DBA запустит её вручную.
Код должен быть такой, чтобы он работал со старой и новой структурами базы данных. Мы часто делаем многоходовки для деплоя фичи. За две-три выкатки удаётся получить желаемое состояние. Точно так же есть огромные базы данных, шардированные базы данных. Если считать по всем микросервисам, то в день 100–150 деплоев точно есть.
Нет. Зависит от endpoint. Смотрим в отдельности на всё. Пытаемся понять, насколько это критично. Какие-то endpoints вообще в фоне запрашиваются. Это на пользователя никак не влияет, даже если время ответа будет 20 с. Это случится в фоне, никакой разницы нет. Главное — чтобы какие-то важные штуки делались быстро. Возможно, 200 мс ещё окей, но небольшой рост уже имеет значение.
Мы с рендера HTML переходим на API-запросы. Тут на самом деле API гораздо быстрее отвечает, чем идёт большой, тяжёлый HTML-ответ. Поэтому сложно выделить, например, значение в 100 мс. Мы ориентировались на 200 мс. Потом случился PHP 7 — и мы начали ориентироваться на 100 мс. Это «в среднем по больнице». Это очень расплывчатая метрика, которая говорит о том, что пора хотя бы посмотреть туда. А так мы скорее ориентируемся на деплой и подскочившее время ответа после него.
У нас делался ресёрч одной из команд перформанса. Коллеги измерили, насколько больше компания зарабатывает при ускорении загрузки страницы при различных сценариях. Посчитали, насколько больше происходит байеров, транзакций, звонков, переходов и так далее. По этим данным можно понять, что в какой-то момент ускорение перестаёт иметь смысл. Например, если время ответа одной из страниц с 90 мс ускорили до 70 мс, это дало +2% байеров. А если ускорять с 70 мс до 60 мс, то там уже плюс 0,1% байеров, что вообще входит в погрешность. Точно так же, как у ребят, всё сильно зависит от страницы, с которой работаем. В целом по «Авито» 75-й персентиль — около 75 мс. На мой взгляд, это медленно. Мы сейчас ищем места для оптимизации. У нас до перехода на микросервисы всё происходило намного быстрее, и мы пытаемся оптимизировать производительность.
Моё мнение: настоящий программист — это оптимизация. Мне кажется, в больших компаниях типа Badoo, моей, «Яндекса» много разработчиков разного уровня. Есть как джуниор-девелоперы/стажёры, так и ведущие разработчики. Мне кажется, там всегда прибавляется количество мест для оптимизации/пересмотра. Железо — это последний шаг. Для монолита на 65 LXC мы уже очень давно не добавляли железо. Утилизация CPU — 20%. Мы уже думаем перекидывать их в Kubernetes-кластер.
Мне очень нравится позиция Семёна. Но у меня полностью противоположная точка зрения. В первую очередь я бы смотрел на железо. Можно ли докинуть железа и будет ли это дешевле? Если да, то проще решить проблему железом. Разработчик может в это время заняться чем-то другим, полезным. Время разработчика стоит денег. Железо тоже стоит денег, поэтому нужно сравнивать.
Что из этого важнее — непонятно. Если и то, и другое будет стоить одинаково, то железо выигрывает, потому что разработчик в это время сможет что-то делать. Конкретно у нас в части PHP-бэкенда не получается так делать. Оптимизация нам обходится значительно дешевле, чем покупка железа.
Про остановиться. С точки зрения планирования у нас есть какая-то планка. Если мы снижаем потребление CPU ниже неё, то мы останавливаемся. С другой стороны, есть ещё и качество сервиса. Если мы видим, что время ответа нас где-то не устраивает, то надо оптимизировать.
Мне кажется, что всё зависит от размеров команды и проекта. Чем меньше проект, тем проще докупить железо, потому что зарплата разработчика — величина постоянная, а код, который работает, пользователи, которые с ним работают, — очень разные. Если пользователей мало, то проще докупить железо и поручить разработчику работу по развитию проекта. Если пользователей очень много, то один разработчик может компенсировать большую часть стоимости серверов.
Реально всё зависит от масштаба. Если у вас тысяча серверов и оптимизация приводит к тому, что вам не нужна ещё одна тысяча серверов, то явно лучше оптимизировать. Если у вас один сервер, то можно спокойно купить ещё два-три сервера и забить на оптимизацию. Если команда маленькая, то пускай серверы закупаются. Если команда большая и у вас два-три дата-центра, то купить шесть дата-центров — уже не так дёшево, не так быстро.
Вообще PHP — очень хороший язык. Он реально позволяет решать бизнес-задачи. Он достаточно быстрый. Все проблемы производительности — это проблемы ошибок, багов, легаси-кода (какой-то недовыпиленный код, неоптимальные вещи, которые точно так же выстрелили бы на другом языке). Портируя их в сыром виде на Golang, Java, Python, ты получишь ту же самую производительность. Вся проблема именно в том, что там много легаси.
Вводить новый язык, на мой взгляд, имеет смысл для того, чтобы расширять стек и возможности для найма. Сейчас достаточно трудно найти хороших PHP-разработчиков. Если мы вводим в техрадар Golang, то мы можем нанимать гоферов. На рынке мало PHP-шников и мало гоферов, а вместе их уже становится много. Например, у нас был один эксперимент. Мы брали C#-разработчиков, которые готовы осваивать новые языки, — просто расширяли стек найма. Если говорить людям, что мы научим их писать на PHP, они говорят, что лучше не надо. А если мы им предлагаем научиться писать на PHP, Go и ещё обещаем возможность пописать на Python, то люди охотнее откликаются. Для меня это расширение возможностей для найма. В других языках есть какие-то вещи, которых реально не хватает в PHP. Но в целом PHP супердостаточно для решения бизнес-задач и реализации крупных проектов.
Я, наверное, полностью соглашусь с Семёном. Переписывать просто так нет смысла. PHP — достаточно производительный язык. Если его сравнивать с другими скриптовыми некомпилируемыми языками, то он, наверное, будет чуть ли не самым быстрым. Переписывать на какие-то языки типа Go и прочих? Это другие языки, у них другие проблемы. Писать на них всё-таки сложнее: не так быстро и много нюансов.
Но тем не менее есть такие штуки, которые на PHP писать либо сложно, либо неудобно. Какие-то многопроцессные, многопоточные вещи лучше писать на другом языке. Пример задачи, где оправданно неприменение PHP, — в принципе, любые хранилки. Если это сервис, который много хранит в памяти, то, наверное, PHP не лучший язык, потому что у него очень большой оверхед по памяти за счёт динамической типизации. Получается, что ты сохраняешь 4-хбайтовый int, а расходуются 50 байт. Я, конечно, утрирую, но всё равно этот оверхед очень большой. Если у вас какая-то хранилка, то её лучше написать на другом компилируемом языке со статической типизацией. Так же, как какие-то штуки, которые требуют многопоточного выполнения.
Почему PHP считается не очень быстрым? Потому что он динамический. Перевод на Go — это решение проблемы переводом кода с динамической типизации на статическую. За счёт этого всё быстрее происходит. Конкретно для Go в моём плане имеются задачи конкретного потока данных. Например, если есть какой-то поток данных, который нужно конвертировать в более удобные форматы, — это идеальная штука. Поднял демон, он получает на вход один поток данных, выдаёт другой. Памяти жрёт мало. PHP жрёт много памяти в этом случае: надо тщательно следить за тем, чтобы она чистилась.
Перевод на Go — это перевод на микросервисы, потому что целиком ты не выпилишь код. Не возьмёшь и не перепишешь его целиком. Перевод на микросервисы — это более глубокая задача, чем перевод с одного языка на другой. Нужно решить сначала её, а потом уже думать о том, на каком языке писать. Самое сложное — это научиться в микросервисы.
Необязательно использовать Go в микросервисах. Многие сервисы написаны на PHP и имеют приемлемое время ответа. Команда, которая их саппортит, решила для себя, что ей нужно очень быстро писать бизнес-логику, вводить фичи. Они реально иногда делают быстрее, чем гоферы.
У нас есть тенденция к найму гоферов, переводу на Go. Но у нас всё равно большая часть кода написана на PHP, и так будет ещё как минимум несколько лет. Тут нет реальной гонки, мы не решили, что Go лучше или хуже. В день в монолит выкатывается шесть релизов. В нашем чате «Avito деплой» есть список задач, которые деплоятся. В день в каждом релизе по 20 задач как минимум выкатывается: пять-шесть релизов в день, примерно 80 тасков, что люди сделали по этим задачам. Всё это сделано на PHP.
Это очень сложно. Есть психологический момент: запустил новую фичу — ты молодец. Запустил гигантскую новую фичу — ты супермолодец! Когда менеджер или разработчик говорит, что удалил фичу (вывели из эксплуатации), он не получает такого признания. Его работа не видна. Например, команда может получить премию за успешную реализацию новых фич, но я не видел, чтобы награждали за спил мёртвой или экспериментальной функциональности. А результат от этого можно получить реально колоссальный.
Куча легаси-фич, которые уже никто не использует, реально тормозят разработку. Есть сотни случаев, когда новый человек приходит в компанию и занимается рефакторингом мёртвого кода, который никогда не вызывается, так как он не знает, что это мёртвый код.
Мы пытаемся как-то договариваться с менеджерами, выяснять по golog, что это была за фича, с аналитиками считаем, сколько денег она приносит, кому она была нужна, и только после пытаемся её спилить. Это сложный процесс.
Мы выпиливаем мёртвый код, когда до него добраемся. И далеко не всегда его можно быстро выпилить. Сначала ты пишешь один код, потом другой, сверху третий, а потом под этим всем оказывается, что эта фича не нужна. Она тянет за собой дикое количество зависимостей, которые тоже нужно поправить. Это не всегда возможно, и менеджерам нужно сообщать об этом.
Менеджер ставит задачу, ты её оцениваешь. Говоришь: «Знаешь, парень, я это буду делать полгода. Ты готов потерпеть полгода?». Он говорит: «Нет, давай думать, что нужно выпилить, что можно оставить. Что принципиально нужно, а что нужно, чтобы поиграться». Вот так всё и происходит.
Когда разработчик получает фичу, он оценивает, насколько она сложная с точки зрения разработки, насколько она тяжёлая с точки зрения перформанса. Если он видит, что либо одно, либо другое, то уточняет у продакт-менеджера, действительно ли это нужно, можем ли мы где-то что-то убрать. Иногда бывает, что изменения не критичны, и менеджеры быстро идут на уступки, когда узнают, что эта штука сложная или жрёт ресурсы. Бывает просто: говоришь «Давайте её уберём» — и всё.
Бывает так, что фича уезжает, и после этого мы замечаем, что она перформит не так хорошо, как хотелось бы. И тогда, опять же, можно поговорить с менеджером. Часто всё заканчивается успешно.
По удалению фич какого-то выстроенного процесса нет. Это делается эпизодически. Мы видим, что есть какая-то фича, подходим и предлагаем её выключить. Выключаем, смотрим, что она даёт, и выпиливаем. Другое дело — мёртвый код. У нас есть специальный экстеншен, даже целая инфраструктура, для детекта мёртвого кода. Мы видим этот мёртвый код. Мы стараемся потихоньку его спиливать. Другой вопрос — если он действительно мёртвый и в него никогда не заходит реквест, то он никак не влияет на производительность. Он влияет на поддерживаемость. Люди его постоянно читают, хотя могли бы не читать. Тут никакой особой связи с производительностью нет.
Поэтому темой третьей встречи сообщества PHP-разработчиков в нашем офисе мы сделали производительность бэкенда и пригласили к обсуждению коллег из «Авито» и «Мамбы».
Читайте под катом расшифровку дискуссии, в которой мне повезло быть модератором: как устроена инфраструктура трёх компаний, как мы измеряем производительность и на какие метрики ориентируемся, какие инструменты используем, как делаем выбор между железом и оптимизацией.
А 15 февраля приходите на следующий Badoo PHP Meetup: обсудим легаси.
Мы расшифровали только часть дискуссии, которая показалась нам наиболее интересной. Полная версия доступна на видео.
Эксперты:
- Семён Катаев, руководитель группы разработки в юните Core Services «Авито»
- Павел Мурзаков pmurzakov, PHP-тимлид в Badoo
- Михаил Буйлов mipxtx, IT-директор компании «Мамба»
Расскажите историю про оптимизацию из вашей практики: грандиозный успех или грандиозный провал — то, чем интересно поделиться.
Михаил Буйлов, «Мамба»
У меня есть притча.
Примерно раз в полгода мы смотрим на метрики и ищем, что тормозит, плохо работает, что надо оптимизировать. Однажды обратили внимание на наш контейнер зависимостей от Symfony, который разросся до 52 000 строк. Решили, что это он, подлец, во всём виноват: 20 мс оверхеда на каждый запрос. Мы его распиливали. Мы его уменьшали. Мы как-то пытались его разделить, но ничего не помогало.
А потом оказалось, что у нас есть антиспам, которому нужно сходить в 20 баз, чтобы выполнить все необходимые запросы.
Решения, которые находятся первыми, не всегда правильные. Смотрите лучше в трейсы, логи и бенчмарки своих запросов, а не ломитесь напрямую. Вот такая история.
Павел Мурзаков, Badoo
У нас достаточно большая экосистема на PHP, поэтому мы периодически занимаемся оптимизацией. Растём, достигаем какой-то планки по CPU, понимаем, что нужно либо докупать железо, либо оптимизировать. Взвешиваем аргументы за и против каждого варианта и решаем. Чаще всего — в пользу оптимизации, потому что железа надо много.
В один из таких моментов мы выделили целую группу, которая занималась тем, что искала разные неоптимальные штуки в PHP-скриптах и оптимизировала. Это происходило буквально «по капле»: здесь нашли процент, там нашли процент — несколько человек в течение месяца находили по проценту. В какой-то момент рядом оказался наш сишник einstein_man. Он решил посмотреть, что можно сделать: зашёл вечером, запустил Perf, нашёл пару проблем в экстеншенах PHP — и ускорил всё за пару вечеров на 13%!
Семён Катаев, «Авито»
У меня есть две истории. Одна о фейле, другая — про суперразработчика.
О фейле. У нас много микросервисов, в том числе на PHP. Все работают в Kubernetes. Я работал над одним из этих микросервисов. Там была значительная утилизация CPU: потратили неделю на оптимизацию и поиск проблем. Оказалось, что один из разработчиков для подсчета code coverage тестов в своём (другом) сервисе добавил в базовые образы Xdebug, после чего все микросервисы в продакшене целый квартал работали с включённым Xdebug! Через квартал мы это обнаружили, исправили базовые образы — и начали ловить неожиданные подарки: я перевыкатил свой сервис, и он стал работать быстрее. При каждом деплое у нас образ сервиса пересобирается, и теперь без Xdebug.
История успеха. У нас много микросервисов, и их становилось всё больше и больше. В этой ситуации количество RPC-вызовов превращается в проблему. Например, на карточке объявления — а это одна из самых частотных страниц в «Авито» — для отрисовки страницы задействовано порядка 30 микросервисов. Причём это всё сделано не очень явно: вроде как вызываешь какую-то абстракцию, а под ней последовательно выполняются пять RPC-вызовов в другие сервисы.
С годами карточка объявления сильно деградировала. Один сильный разработчик целый квартал бился, оптимизировал, выводил все RPC-вызовы наружу. Когда он смог это сделать, он их запараллелил через Guzzle multi request — и получил вместо 30 последовательных синхронных запросов те же самые 30 запросов, но параллельных, что очень сильно ускорило работу. После этого рефакторинга время ответа карточки равняется максимальному времени ответа любого из сервисов. Но ему понадобился целый квартал, чтобы оптимизировать/переписать код отображения карточки объявления.
Расскажите, какой у вас размер кластера PHP, как он сконфигурирован — как минимум, PHP-FPM или, может быть, где-то Apache затесался?
Михаил Буйлов, «Мамба»
У нас порядка 15 000 RPS. Кластер из 80 FPM-серверов, закупленных несколько лет назад. На каждом кластере в статику запущен FPM (максимально в 50 чайлдов). Там загружено около десяти в пик, в прайм-тайм. Среднее время ответа — 100 мс, и мы стараемся его удерживать (когда оно переваливает за 100 мс, мы начинаем поиск тормозящих штук).
У нас есть собственная система мониторинга производительности. Мы раскидали в коде очень много счётчиков, порядка 120 на запрос. Мониторим очень много ивентов, которые происходят внутри кода на PHP.
Павел Мурзаков, Badoo
У нас всё стандартно: nginx, PHP-FPM. Примерно 600 серверов с FPM. Если вообще про PHP говорить, то, наверное, есть ещё около 300 серверов разного назначения типа скриптовых, бэк-офисных и прочих.
Из особенностей конфигурации есть две. Во-первых, у нас есть BMA-прокси — это прокси для мобил. То есть перед тем как запрос прилетает в nginx, он попадает на специальный прокси, который держит persistent connection и отправляет запросы в nginx. Вторая особенность — иногда нужно выключать CLI opcache (у нас он включён на скриптовых машинах). Когда-то мы его не выключали и теряли 30% CPU на этом. После того как осознали свою ошибку, удивились, как много можно сэкономить одной настройкой.
У нас есть патчи PHP, но они почти не связаны с производительностью.
Есть момент с конкурентным локом APCu — когда нужно много записывать в один и тот же ключ. Внутренняя архитектура APCu построена таким образом, что есть глобальный лок на ключ, и при интенсивной записи начинаются тормоза. Поэтому у нас там есть кастомное расширение, которое решает эту проблему. Это только отчасти относится к производительности, поскольку влияет на время ответа, но не на потребление CPU.
Семён Катаев, «Авито»
У нас около 2 млн реквестов в минуту (~33 kRPS). Монолитное приложение, написано на PHP, пишется уже более 11 лет. Оно в фазе быстрого роста. Когда компания стартовала, было 65 LXC на 65 физических серверах. На каждом контейнере с приложением запущены PHP-FPM, nginx и вспомогательный софт для метрик, логирования. Ничего особенного.
За эти годы мы ни разу не прибавляли железо для монолита. У нас растут посещаемость, количество объявлений, количество пользовательских транзакций, и постоянно мы оптимизируем, улучшаем код, оптимизируем софт. Потребление CPU и памяти в течение последних лет падает: 65 контейнеров для монолита нам сейчас хватает за глаза.
Как вы измеряете производительность? Каким инструментом меряете время ответа клиента?
Михаил Буйлов, «Мамба»
У нас есть система сбора логов. Она логирует два показателя — время от старта FPM до shutdown function и до конца выполнения скрипта. Вторая метрика нужна, чтобы видеть, что происходит после shutdown function.
Мы меряем JS. Это, на самом деле, так себе метрика, очень часто нарушаются сетевые каналы. В результате загрузка где-нибудь в российской глубинке начинает тупить. Поэтому мы так смотрим: «О, прыгнуло — значит, где-то что-то отвалилось». Плюс сторонняя реклама очень сильно искажает метрику. И, главное, приходят спамеры, и это вообще какой-то рандом.
Семён Катаев, «Авито»
Кстати, мы раньше очень активно использовали Pinba от Badoo. Мне она и сейчас нравится. Большинство метрик собирали ею, но потом перешли на StatsD-протокол. Сейчас мы делаем замеры с разных точек: с фронта, с серверов перед приложением, с nginx и с самого PHP-приложения. У нас есть выделенная команда перформанса. Она начинала с перформанса фронта, но потом переключилась и на бэк. С фронта она не только JS, CSS и прочую статику собирает, но заодно и время ответа сервера. В первую очередь мы ориентируемся на время ответа приложения.
Павел Мурзаков, Badoo
У нас всё похоже на то, что рассказали ребята. При помощи классической Pinba для PHP мы измеряем время работы PHP-скрипта с точки зрения PHP. Но также у нас есть, например, Pinba для nginx, которая измеряет время ответа с точки зрения nginx. На клиенте тоже собираем метрики.
На что мы смотрим? С одной стороны, на время ответа. Оно не связано с планированием ресурсов. Если оно плохое, его надо улучшать, потому что это, по сути, качество сервиса. Другое дело — что надо как-то планировать железо. У нас команды ITOps и мониторинга мониторят всё железо. Выставлены какие-то планки по сети, по диску. Есть какие-то значения, после которых происходит alert — и мы что-то делаем. Как показала практика, обычно мы оптимизируем по CPU: упираемся именно в него.
Семён Катаев, «Авито»
У нас PHP-приложение само себя меряет и в register_shutdown_function() выбрасывает метрики. На каждом LXC стоит StatsD-сервер, который собирает эти метрики и отправляет дальше через коллекторы в Graphite cluster (в том числе ClickHouse), где хранятся данные. Это самодиагностика.
Также на каждом контейнере стоит nginx, то есть nginx + PHP-FPM. С nginx мы собираем внешние метрики, относящиеся ко времени работы PHP-приложения. Ещё перед ними стоит на отдельных серверах (мы их называем avi-http) nginx, выполняющий базовый роутинг, где также собираются более высокоуровневые метрики, такие как время ответа, количество 500-х кодов ответов и другие.
Какие инструменты у вас есть для трека перформанса? Что применяете наиболее часто?
Михаил Буйлов, «Мамба»
Мы написали свою собственную тулзу. Когда Pinba только вышла — в 2012 году, очень давно — это был модуль к MySQL, который по UDP принимал что-то такое. Сложно было вынимать графики, он был не очень оптимизирован по производительности. И мы не придумали ничего лучше, чем написать свою собственную штуку, которую назвали Better Than Pinba. Это просто сервер счётчиков, который принимает их с PHP-клиента.
Мы раскидали в коде очень много таймеров: каждый раз, когда мы хотим что-то измерить, в коде выставляем старт и стоп таймера. Модуль сам посчитает время выполнения счётчика, агрегирует накопленные счётчики в пакет, отправит их в демон. Интерфейс сам извлечёт всё необходимое и построит связные графики по нужному счётчику.
Одна из проблем Pinba заключалась в отсутствии собственного интерфейса — нужно было перекладывать данные в RRD (тогда ещё был такой мрак). Поэтому мы написали свой собственный интерфейс. Каждый раз, когда мы видим, что скакнуло, мы можем установить скрипт. В скрипте затегированы все агрегированные счётчики, которые у нас отправляются. Мы можем посмотреть, где какой счётчик вырос, либо увеличилось время ответа у счётчика, либо увеличилось количество счётчиков.
Видно, когда происходит падение производительности. Мы начинаем копать в эту сторону. До PHP 7 мы использовали XHProf, потом он перестал у нас собираться. Поэтому мы перешли на Xdebug. Xdebug мы тыкаем только тогда, когда видна проблема.
Павел Мурзаков, Badoo
Это распространённое мнение — что XHProf на PHP 7 не собирается. Это правда, но лишь отчасти. Если взять XHProf из мастера, он действительно не соберётся. Но если на GitHub переключиться на ветку, которая называется Experimental (или что-то вроде того), то там всё работает для PHP 7 нормально, production ready. Проверено.
Михаил Буйлов, «Мамба»
Нет, я переключался. У меня не заработало.
Павел Мурзаков, Badoo
Хочу добавить про Pinba. Вы в какой-то мере стали пророками. Нам тоже в определённый момент перестало хватать производительности. Мы сделали Pinba2, которая очень быстрая. Можно её использовать как замену Pinba.
Семён Катаев, «Авито»
У нас всё скромно. Мы только взяли направление перформанса в работу: собираем метрики, вроде времени ответа. Используем StatsD. Мы пока не используем никакие профилировщики на регулярной основе, но я точно знаю, что некоторые команды используют их в своих микросервисах, написанных на PHP. По-моему, даже кто-то New Relic юзает. Но в контексте основного монолитного приложения пока мы только подходим к этому.
Павел Мурзаков, Badoo
Железячная история у нас мониторится в Grafana, Zabbix. Что касается конкретно PHP-части, то у нас есть Pinba, у нас есть куча таймеров; строим по ним удобные графики. Используем XHProf, на продакшене гоняем его для части запросов. У нас всегда доступны свежие профайлы XHProf. У нас есть liveprof: это наш инструмент, можете почитать о нём в том числе в моей статье. Это всё происходит автоматически, надо просто смотреть. Используем phpspy. Он у нас не запущен постоянно: когда кто-то хочет что-то посмотреть, он заходит на машину, снимает профиль. В принципе, как и в случае с Perf.
Семён Катаев, «Авито»
С XHProf та же история. Мы когда-то давно его использовали: это была личная инициатива пары разработчиков, и, по сути, не взлетело. Он перестал собираться. Мы собираем кучу метрик с вызовов роутеров, контроллеров, различных моделей. Порядка 60–70% внутренней сети дата-центра занято UDP-пакетами с метриками. На данный момент нам этого хватает. Сейчас будем искать новые места для оптимизации.
Раз уж мы дошли до железа: в вашей компании кто-то системно занимается capacity planning? Как этот процесс построен?
Семён Катаев, «Авито»
Монолитное приложение минимум пять лет работает на 65 LXC. Оптимизируем код, улучшаем его: ему ресурсов хватает. У нас основной capacity planning уходит в Kubernetes, где порядка 400 более или менее живущих микросервисов написаны на PHP/Go. Мы потихоньку кусочки от монолита отрезаем, но он всё равно растёт. Мы его не можем остановить.
Вообще PHP — крутой язык. На нём быстро реализуется бизнес-логика.
Павел Мурзаков, Badoo
В первую очередь команды ITOps и мониторинга следят за тем, чтобы ресурсов хватало. Если мы начинаем приближаться к пороговому значению, коллеги это замечают. Наверное, они в первую очередь ответственны за глобальный capacity planning. У PHP-части основной ресурс CPU, поэтому мы за ним сами следим.
Мы себе поставили такую планку: мы не должны «съедать» более 60% кластера. 60%, а не 95%, потому что у нас гипертрединг, который дополнительно выжимает из процессора больше, чем можно выжать без него. За это мы платим тем, что у нас после 50% потребление CPU может расти непредсказуемым образом, потому что гипертрединг-ядра не совсем честные.
Михаил Буйлов, «Мамба»
Мы делаем деплой и видим, что у нас что-то провалилось, — вот такой capacity planning. На глазок! У нас есть некий запас по производительности, который нам позволяет так делать. Мы его стараемся придерживаться.
Кроме того, мы делаем оптимизацию постфактум. Когда мы видим, что что-то отвалилось, мы откатываемся, если совсем всё плохо. Но такого практически не бывает.
Либо мы просто видим, что «вот здесь неоптимально, сейчас мы быстро всё исправим и всё будет работать как надо».
Мы особо не заморачиваемся: это очень сложно, а выхлоп будет не очень большой.
Вы говорите о микросервисах. Как они взаимодействуют? Это REST или вы используете бинарные протоколы?
Семён Катаев, «Авито»
Для отправки событий между сервисами используется Kafka, для обеспечения взаимодействия между сервисами — JSON-RPC, но не full, а его упрощённая версия, от которой мы не можем избавиться. Есть более быстрые реализации: те же protobuf, gRPC. Это есть в наших планах, но точно не в приоритете. Имея более 400 микросервисов, трудно их все портировать на новый протокол. Есть куча других мест для оптимизации. Нам сейчас точно не до этого.
Павел Мурзаков, Badoo
У нас как таковых микросервисов нет. Есть сервисы, тоже есть Kafka, собственный протокол поверх Google protobuf. Наверное, мы бы использовали gRPC, потому что он классный, у него есть поддержка всех языков, возможность очень легко связывать разные куски. Но когда нам это понадобилось, gRPC ещё не было. Зато был protobuf, и мы взяли его. Поверх него добавили разные штуки, чтобы это была не просто сериализация, а полноценный протокол.
Михаил Буйлов, «Мамба»
У нас тоже нет микросервисов. Есть сервисы, в основном написанные на C. Мы используем JSON-RPC, потому что это удобно. Ты просто открыл сокет, когда свой сишный код отлаживаешь, и быстро написал, что хотел. Тебе что-то вернулось. С protobuf сложнее, потому что нужно какие-то дополнительные инструменты использовать. Небольшой оверхед есть, но мы считаем, что за удобство нужно платить, а это не большая цена.
У вас огромные базы данных. Когда вам нужно поменять схему в одной из них, как вы это делаете? Какими-то миграциями? Если эти миграции занимают несколько дней, как это влияет на производительность?
Михаил Буйлов, «Мамба»
У нас есть большие таблицы, монолитные. Есть шард. Шард альтерится довольно быстро, потому что идут сразу много параллельных alter. Большая таблица с профилями альтерится порядка трёх часов. Мы используем перконовские тулзы, которые не лочат её на чтение и на запись. Кроме того, мы перед alter деплоимся, чтобы код поддерживал и то, и другое состояние. После alter тоже деплоимся: деплоиться — быстрее, чем применять какие-то схемы.
Павел Мурзаков, Badoo
У нас самое большое хранилище (мы его называем «споты») — это огромная шардированная база данных. Если мы берём таблицу «User», то у неё очень много шардов. Я навскидку не скажу, сколько конкретно таблиц будет на одном сервере: идея в том, что их много мелких. Когда мы изменяем схему, по сути, мы просто делаем alter. На каждой мелкой таблице он происходит быстро. Есть другие хранилища — там уже другие подходы. Если одна огромная база, там есть перконовские тулзы.
В общем, мы используем разные штуки по потребностям. Самое частое изменение — это изменение этой огромной шардированной базы спотов, в которой у нас уже выстроен процесс. Это всё работает очень просто.
Семён Катаев, «Авито»
То самое монолитное приложение, которое сервит большую часть трафика, деплоится пять-шесть раз в день. Практически каждые два часа.
В плане работы с базой данных это отдельный вопрос. Есть миграции, они накатываются автоматически. Это ревьювят DBA. Есть опция скипнуть миграцию и вручную накатить. Миграция будет автоматически накатываться в staging при тестировании кода. Но на продакшене, если там какая-то сомнительная миграция, которая утилизирует кучу ресурсов, то DBA запустит её вручную.
Код должен быть такой, чтобы он работал со старой и новой структурами базы данных. Мы часто делаем многоходовки для деплоя фичи. За две-три выкатки удаётся получить желаемое состояние. Точно так же есть огромные базы данных, шардированные базы данных. Если считать по всем микросервисам, то в день 100–150 деплоев точно есть.
Хотелось бы узнать, какое для вас эталонное время ответа бэкенда, на которое вы равняетесь? Когда вы понимаете, что нужно дальше оптимизировать, или что пора закончить? «Среднее по больнице» значение есть?
Павел Мурзаков, Badoo
Нет. Зависит от endpoint. Смотрим в отдельности на всё. Пытаемся понять, насколько это критично. Какие-то endpoints вообще в фоне запрашиваются. Это на пользователя никак не влияет, даже если время ответа будет 20 с. Это случится в фоне, никакой разницы нет. Главное — чтобы какие-то важные штуки делались быстро. Возможно, 200 мс ещё окей, но небольшой рост уже имеет значение.
Михаил Буйлов, «Мамба»
Мы с рендера HTML переходим на API-запросы. Тут на самом деле API гораздо быстрее отвечает, чем идёт большой, тяжёлый HTML-ответ. Поэтому сложно выделить, например, значение в 100 мс. Мы ориентировались на 200 мс. Потом случился PHP 7 — и мы начали ориентироваться на 100 мс. Это «в среднем по больнице». Это очень расплывчатая метрика, которая говорит о том, что пора хотя бы посмотреть туда. А так мы скорее ориентируемся на деплой и подскочившее время ответа после него.
Семён Катаев, «Авито»
У нас делался ресёрч одной из команд перформанса. Коллеги измерили, насколько больше компания зарабатывает при ускорении загрузки страницы при различных сценариях. Посчитали, насколько больше происходит байеров, транзакций, звонков, переходов и так далее. По этим данным можно понять, что в какой-то момент ускорение перестаёт иметь смысл. Например, если время ответа одной из страниц с 90 мс ускорили до 70 мс, это дало +2% байеров. А если ускорять с 70 мс до 60 мс, то там уже плюс 0,1% байеров, что вообще входит в погрешность. Точно так же, как у ребят, всё сильно зависит от страницы, с которой работаем. В целом по «Авито» 75-й персентиль — около 75 мс. На мой взгляд, это медленно. Мы сейчас ищем места для оптимизации. У нас до перехода на микросервисы всё происходило намного быстрее, и мы пытаемся оптимизировать производительность.
И вечный вопрос: железо или оптимизация? Как понять, стоит купить железо или лучше вложиться в оптимизацию? Где граница?
Семён Катаев, «Авито»
Моё мнение: настоящий программист — это оптимизация. Мне кажется, в больших компаниях типа Badoo, моей, «Яндекса» много разработчиков разного уровня. Есть как джуниор-девелоперы/стажёры, так и ведущие разработчики. Мне кажется, там всегда прибавляется количество мест для оптимизации/пересмотра. Железо — это последний шаг. Для монолита на 65 LXC мы уже очень давно не добавляли железо. Утилизация CPU — 20%. Мы уже думаем перекидывать их в Kubernetes-кластер.
Павел Мурзаков, Badoo
Мне очень нравится позиция Семёна. Но у меня полностью противоположная точка зрения. В первую очередь я бы смотрел на железо. Можно ли докинуть железа и будет ли это дешевле? Если да, то проще решить проблему железом. Разработчик может в это время заняться чем-то другим, полезным. Время разработчика стоит денег. Железо тоже стоит денег, поэтому нужно сравнивать.
Что из этого важнее — непонятно. Если и то, и другое будет стоить одинаково, то железо выигрывает, потому что разработчик в это время сможет что-то делать. Конкретно у нас в части PHP-бэкенда не получается так делать. Оптимизация нам обходится значительно дешевле, чем покупка железа.
Про остановиться. С точки зрения планирования у нас есть какая-то планка. Если мы снижаем потребление CPU ниже неё, то мы останавливаемся. С другой стороны, есть ещё и качество сервиса. Если мы видим, что время ответа нас где-то не устраивает, то надо оптимизировать.
Михаил Буйлов, «Мамба»
Мне кажется, что всё зависит от размеров команды и проекта. Чем меньше проект, тем проще докупить железо, потому что зарплата разработчика — величина постоянная, а код, который работает, пользователи, которые с ним работают, — очень разные. Если пользователей мало, то проще докупить железо и поручить разработчику работу по развитию проекта. Если пользователей очень много, то один разработчик может компенсировать большую часть стоимости серверов.
Семён Катаев, «Авито»
Реально всё зависит от масштаба. Если у вас тысяча серверов и оптимизация приводит к тому, что вам не нужна ещё одна тысяча серверов, то явно лучше оптимизировать. Если у вас один сервер, то можно спокойно купить ещё два-три сервера и забить на оптимизацию. Если команда маленькая, то пускай серверы закупаются. Если команда большая и у вас два-три дата-центра, то купить шесть дата-центров — уже не так дёшево, не так быстро.
Раз уж у нас PHP-митап, то должна прозвучать эта фраза: зачем нам PHP, раз у нас всё время такие проблемы? Давайте перепишем на Go, C, С#, Rust, Node.js!
Как вы думаете, оправдан ли в целом подход переписывания? Есть ли какие-то задачи, для решения которых этим стоит заниматься и вкладывать в это время?
Семён Катаев, «Авито»
Вообще PHP — очень хороший язык. Он реально позволяет решать бизнес-задачи. Он достаточно быстрый. Все проблемы производительности — это проблемы ошибок, багов, легаси-кода (какой-то недовыпиленный код, неоптимальные вещи, которые точно так же выстрелили бы на другом языке). Портируя их в сыром виде на Golang, Java, Python, ты получишь ту же самую производительность. Вся проблема именно в том, что там много легаси.
Вводить новый язык, на мой взгляд, имеет смысл для того, чтобы расширять стек и возможности для найма. Сейчас достаточно трудно найти хороших PHP-разработчиков. Если мы вводим в техрадар Golang, то мы можем нанимать гоферов. На рынке мало PHP-шников и мало гоферов, а вместе их уже становится много. Например, у нас был один эксперимент. Мы брали C#-разработчиков, которые готовы осваивать новые языки, — просто расширяли стек найма. Если говорить людям, что мы научим их писать на PHP, они говорят, что лучше не надо. А если мы им предлагаем научиться писать на PHP, Go и ещё обещаем возможность пописать на Python, то люди охотнее откликаются. Для меня это расширение возможностей для найма. В других языках есть какие-то вещи, которых реально не хватает в PHP. Но в целом PHP супердостаточно для решения бизнес-задач и реализации крупных проектов.
Павел Мурзаков, Badoo
Я, наверное, полностью соглашусь с Семёном. Переписывать просто так нет смысла. PHP — достаточно производительный язык. Если его сравнивать с другими скриптовыми некомпилируемыми языками, то он, наверное, будет чуть ли не самым быстрым. Переписывать на какие-то языки типа Go и прочих? Это другие языки, у них другие проблемы. Писать на них всё-таки сложнее: не так быстро и много нюансов.
Но тем не менее есть такие штуки, которые на PHP писать либо сложно, либо неудобно. Какие-то многопроцессные, многопоточные вещи лучше писать на другом языке. Пример задачи, где оправданно неприменение PHP, — в принципе, любые хранилки. Если это сервис, который много хранит в памяти, то, наверное, PHP не лучший язык, потому что у него очень большой оверхед по памяти за счёт динамической типизации. Получается, что ты сохраняешь 4-хбайтовый int, а расходуются 50 байт. Я, конечно, утрирую, но всё равно этот оверхед очень большой. Если у вас какая-то хранилка, то её лучше написать на другом компилируемом языке со статической типизацией. Так же, как какие-то штуки, которые требуют многопоточного выполнения.
Михаил Буйлов, «Мамба»
Почему PHP считается не очень быстрым? Потому что он динамический. Перевод на Go — это решение проблемы переводом кода с динамической типизации на статическую. За счёт этого всё быстрее происходит. Конкретно для Go в моём плане имеются задачи конкретного потока данных. Например, если есть какой-то поток данных, который нужно конвертировать в более удобные форматы, — это идеальная штука. Поднял демон, он получает на вход один поток данных, выдаёт другой. Памяти жрёт мало. PHP жрёт много памяти в этом случае: надо тщательно следить за тем, чтобы она чистилась.
Перевод на Go — это перевод на микросервисы, потому что целиком ты не выпилишь код. Не возьмёшь и не перепишешь его целиком. Перевод на микросервисы — это более глубокая задача, чем перевод с одного языка на другой. Нужно решить сначала её, а потом уже думать о том, на каком языке писать. Самое сложное — это научиться в микросервисы.
Семён Катаев, «Авито»
Необязательно использовать Go в микросервисах. Многие сервисы написаны на PHP и имеют приемлемое время ответа. Команда, которая их саппортит, решила для себя, что ей нужно очень быстро писать бизнес-логику, вводить фичи. Они реально иногда делают быстрее, чем гоферы.
У нас есть тенденция к найму гоферов, переводу на Go. Но у нас всё равно большая часть кода написана на PHP, и так будет ещё как минимум несколько лет. Тут нет реальной гонки, мы не решили, что Go лучше или хуже. В день в монолит выкатывается шесть релизов. В нашем чате «Avito деплой» есть список задач, которые деплоятся. В день в каждом релизе по 20 задач как минимум выкатывается: пять-шесть релизов в день, примерно 80 тасков, что люди сделали по этим задачам. Всё это сделано на PHP.
Оптимизируете ли вы за счёт сокращения функциональности? И как следите за накопившимися старыми легаси-фичами, которые никому не нужны?
Семён Катаев, «Авито»
Это очень сложно. Есть психологический момент: запустил новую фичу — ты молодец. Запустил гигантскую новую фичу — ты супермолодец! Когда менеджер или разработчик говорит, что удалил фичу (вывели из эксплуатации), он не получает такого признания. Его работа не видна. Например, команда может получить премию за успешную реализацию новых фич, но я не видел, чтобы награждали за спил мёртвой или экспериментальной функциональности. А результат от этого можно получить реально колоссальный.
Куча легаси-фич, которые уже никто не использует, реально тормозят разработку. Есть сотни случаев, когда новый человек приходит в компанию и занимается рефакторингом мёртвого кода, который никогда не вызывается, так как он не знает, что это мёртвый код.
Мы пытаемся как-то договариваться с менеджерами, выяснять по golog, что это была за фича, с аналитиками считаем, сколько денег она приносит, кому она была нужна, и только после пытаемся её спилить. Это сложный процесс.
Михаил Буйлов, «Мамба»
Мы выпиливаем мёртвый код, когда до него добраемся. И далеко не всегда его можно быстро выпилить. Сначала ты пишешь один код, потом другой, сверху третий, а потом под этим всем оказывается, что эта фича не нужна. Она тянет за собой дикое количество зависимостей, которые тоже нужно поправить. Это не всегда возможно, и менеджерам нужно сообщать об этом.
Менеджер ставит задачу, ты её оцениваешь. Говоришь: «Знаешь, парень, я это буду делать полгода. Ты готов потерпеть полгода?». Он говорит: «Нет, давай думать, что нужно выпилить, что можно оставить. Что принципиально нужно, а что нужно, чтобы поиграться». Вот так всё и происходит.
Павел Мурзаков, Badoo
Когда разработчик получает фичу, он оценивает, насколько она сложная с точки зрения разработки, насколько она тяжёлая с точки зрения перформанса. Если он видит, что либо одно, либо другое, то уточняет у продакт-менеджера, действительно ли это нужно, можем ли мы где-то что-то убрать. Иногда бывает, что изменения не критичны, и менеджеры быстро идут на уступки, когда узнают, что эта штука сложная или жрёт ресурсы. Бывает просто: говоришь «Давайте её уберём» — и всё.
Бывает так, что фича уезжает, и после этого мы замечаем, что она перформит не так хорошо, как хотелось бы. И тогда, опять же, можно поговорить с менеджером. Часто всё заканчивается успешно.
По удалению фич какого-то выстроенного процесса нет. Это делается эпизодически. Мы видим, что есть какая-то фича, подходим и предлагаем её выключить. Выключаем, смотрим, что она даёт, и выпиливаем. Другое дело — мёртвый код. У нас есть специальный экстеншен, даже целая инфраструктура, для детекта мёртвого кода. Мы видим этот мёртвый код. Мы стараемся потихоньку его спиливать. Другой вопрос — если он действительно мёртвый и в него никогда не заходит реквест, то он никак не влияет на производительность. Он влияет на поддерживаемость. Люди его постоянно читают, хотя могли бы не читать. Тут никакой особой связи с производительностью нет.
15 февраля мы проведём четвёртую встречу Badoo PHP Meetup. Обсудим, как разные компании работают с легаси: SuperJob, ManyChat, «Авито», Badoo и FunCorp расскажут про свой опыт.
Регистрируйтесь, чтобы участвовать вживую, или присоединяйтесь к трансляции на нашем YouTube-канале. Надеемся, что этот разговор о легаси упростит кому-то жизнь.
И присоединяйтесь к чату митапа, там регулярно бывают интересные обсуждения.