В качестве подопытного для оптимизации был взят PHP API, размещённый на ~10 серверах. Все нижеперечисленные приёмы были опробованы и применены. Поэтому рекомендую присмотреться к списку, если у вас PHP API на нескольких (от 1 до ~10) серверах с обычным стеком (nginx, fpm, mysql/postgres, redis/memcahed, rabbitmq, …), который почему-то задыхается на казалось бы неплохом железе и к тому же не утилизирует весь выделенный CPU..
Не буду подробно расписывать способы, не связанные с PHP или уже практически везде повторённые сотни раз в каждой статье (как раз детали можно найти в тех статьях):
-
увеличить мощности сервера; заказать ещё серверов;
-
вынести на разные сервера приложение/БД/кэш;
-
оптимизировать настройки БД / кэша (выключение сброса на диск) / php-fpm (про pm=static); не выполнять одинаковые запросы, иметь индексы в БД;
-
не класть в кэш мегабайты данных (или хотя бы иметь кэш для мелких и больших данных с разным требованиям к времени доступа); не выполнять очень много долгих операций в однопоточных сервисах (redis, например).
-
не выполнять тяжёлые операции синхронно — перевести всё на очереди;
Известные рекомендации
Держим постоянные соединения до хранилищ
Довольно просто — делаем все соединения до БД/кэша persistent. Только аккуратнее если у вас целевой адрес динамический (внутри docker/k8s).
Используем реплики хранилищ
Делаем слейвы БД — ходим туда за данными. Или же делаем несколько кэшей/БД — шардируем данные между ними. Например, у нас сессии хранятся в одном редисе, а данные кэша — во втором, а кэш репозитория (см. дальше) — в третьем.
Ставим пулер запросов к базе (для postgres)
pgbouncer устанавливаем перед всеми БД. Некоторые связать через Unix domain socket вместо сети. Что это можно быть:
-
nginx -> php-fpm и обратно (например, для reverse-прокси) php-fpm -> nginx
-
php-fpm -> pgbouncer / любая БД, pgbouncer -> БД
Снижаем сетевую нагрузку, уменьшаем задержки. Без потока клиентского трафика разницы не будет, а с ним получиться убрать нагрузку на ядро ОС в установке и проведении по всему процессу сетевых соединений от одного до другого сервиса на одной и то же машине.
Используйте память сервера как более быстрый кэш
Нередка ситуация когда для API требуется много cpu (условно, 32 ядра и 16 гб озу или больше), поэтому на серверах приложений довольно часто можно найти и какое-то количество свободной памяти (не занимаемой php-fpm, nginx или что там у вас ещё крутится на этих же серверах?). Давайте её используем: выставляем лимит для контейнера shm_size, (напр, 512Мб) и подключаем каталог /dev/shm как каталог для кэша в виде файлов. В хост-машине с этим сложнее — придётся следить самостоятельно за очисткой кэша.
Далее подключаем этот кэш как отдельный компонент кэширования в фреймворке и кладём в него большие куски не особо критичных данных (каких-нибудь списков) на небольшой промежуток времени (исчисляемый минутами). Если протухнет или будет не самый актуальный (если на одном сервере кэш будет лежать до 14:00:00, а на втором до 14:00:02), то ничего страшного не произойдёт.
В итоге мы утилизировали память и получили очень быстрый кэш (т.к данные по сути лежат в памяти, а не на диске).
Переиспользуйте сетевые соединения
В статье хорошо описывается принцип reverse-прокси для сетевого межсервисного взаимодействия.
Особенным плюсом для умирающей после каждого запроса области памяти при запуске через php-fpm (вместе с curl-handler’ами) является то, что мы можем таким образом держать постоянные коннекты от reverse-прокси до внешних сервисов, а приложение будет ходить по дешёвому http к reverse-прокси (а можно же ещё и по UDS ходить, тогда задержки будут минимальны).
Переходим на другую модель работы интерпретатора
Стандартный php-fpm очищает память после каждого запроса, а перед обработкой нового заполняет много чего ещё — даже preload не помогает. Стоит ли упоминать в минусах этого подхода необходимость на каждый запрос:
-
Постоянно выбирать из кэша одни и те же редко (раз в день) изменяемые данные?
-
Инициализировать контейнер, компоненты?
-
Устанавливать соединения с БД/пулером/кэшем/внешними сервисами?
Другая парадигма запуска — один раз запустился, и обрабатывает в цикле все запросы внутри приложения. Если очень коротко, то она реализована в: асинхронных (требуют переписать довольно сильно логику; ReactPHP, AMPHP, Workerman, Swoole, и др.) и синхронных (требует не таких кардинальных изменений, но всё таки; Roadrunner) серверах приложений.
Стоит отметить что данный приём — очень трудозатратен. В случае с асинхронными серверами приложений придётся переписать почти всё взаимодействие с БД и др. хранилищами, в случае синхронных — провести ревью и убрать любое использование «глобального» (ну или очень вездесущего) «состояния».
Зато очевидный выигрыш: постоянные соединения до всех внешних источников данных, уменьшение утилизации CPU (можно ожидать как минимум в ~полтора раза), уменьшение среднего времени ответа.
ссылка на оригинал статьи https://habr.com/ru/articles/744202/
Добавить комментарий