HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Теги: программирование на php, оптимизация производительности, фреймворк laravel, оптимизация laravel-приложений</p>
1 <p>Теги: программирование на php, оптимизация производительности, фреймворк laravel, оптимизация laravel-приложений</p>
2 <p>Хочу поделиться небольшим кейсом оптимизации<strong>Laravel-приложения</strong>. Этот кейс служит хорошей иллюстрацией алгоритма оптимизации в целом, в процессе пришлось столкнуться с типичными проблемами, плюс он содержит несколько решений, относящихся именно к Laravel.</p>
2 <p>Хочу поделиться небольшим кейсом оптимизации<strong>Laravel-приложения</strong>. Этот кейс служит хорошей иллюстрацией алгоритма оптимизации в целом, в процессе пришлось столкнуться с типичными проблемами, плюс он содержит несколько решений, относящихся именно к Laravel.</p>
3 <p>Исходно имеем микросервис авторизации, владеющий информацией о правах пользователей, реализованный на Laravel. Правами пользователи наделяются через роли (<strong>RBAC</strong>), система прав выглядит следующим образом: - трёхуровневое дерево; - на верхнем уровне - сервисы; - посередине - страницы; - на нижнем уровне - элементы страниц.</p>
3 <p>Исходно имеем микросервис авторизации, владеющий информацией о правах пользователей, реализованный на Laravel. Правами пользователи наделяются через роли (<strong>RBAC</strong>), система прав выглядит следующим образом: - трёхуровневое дерево; - на верхнем уровне - сервисы; - посередине - страницы; - на нижнем уровне - элементы страниц.</p>
4 <p>Для страниц и элементов страниц есть набор действий, которые с ними можно осуществлять. В тот момент времени, когда было решено заняться оптимизацией, сервис тратил на ответ о правах пользователя в рамках сервиса<strong>~140 мс</strong>, что позволяло выдерживать только стандартную нагрузку и<strong>совсем не позволяло пиковую</strong>. Для удержания пиковой нагрузки без апгрейда сервера необходимо было снизить время ответа хотя бы до 50 мс. Целевым показателем было выбрано<strong>20 мс</strong>, чтобы иметь запас по производительности.</p>
4 <p>Для страниц и элементов страниц есть набор действий, которые с ними можно осуществлять. В тот момент времени, когда было решено заняться оптимизацией, сервис тратил на ответ о правах пользователя в рамках сервиса<strong>~140 мс</strong>, что позволяло выдерживать только стандартную нагрузку и<strong>совсем не позволяло пиковую</strong>. Для удержания пиковой нагрузки без апгрейда сервера необходимо было снизить время ответа хотя бы до 50 мс. Целевым показателем было выбрано<strong>20 мс</strong>, чтобы иметь запас по производительности.</p>
5 <h2>Выполненные шаги по оптимизации</h2>
5 <h2>Выполненные шаги по оптимизации</h2>
6 <ol><li><strong>Анализ запросов к БД (отказ от Eloquent и доменной модели прав)</strong>. Поскольку данные о правах представлены иерархической структурой, но хранятся в реляционной БД (PostgreSQL), то первое предположение было о том, что основные "тормоза" происходят на этапе работы с БД. Анализ показал, что Eloquent выполнял O(N) запросов, где N - количество сервисов. Для получения набора прав было решено отказаться от Eloquent и доменной модели прав. Запросы переписаны на "сырой" SQL, в результате их количество сокращено до 3 (по одному для каждого уровня), плюс проводится дополнительная обработка результатов в коде. В результате время подготовки ответа удалось сократить до<strong>~100 мс</strong>.</li>
6 <ol><li><strong>Анализ запросов к БД (отказ от Eloquent и доменной модели прав)</strong>. Поскольку данные о правах представлены иерархической структурой, но хранятся в реляционной БД (PostgreSQL), то первое предположение было о том, что основные "тормоза" происходят на этапе работы с БД. Анализ показал, что Eloquent выполнял O(N) запросов, где N - количество сервисов. Для получения набора прав было решено отказаться от Eloquent и доменной модели прав. Запросы переписаны на "сырой" SQL, в результате их количество сокращено до 3 (по одному для каждого уровня), плюс проводится дополнительная обработка результатов в коде. В результате время подготовки ответа удалось сократить до<strong>~100 мс</strong>.</li>
7 <li><strong>Денормализация данных</strong>. Принято решение денормализовать данные и хранить в БД уже подготовленный ответ с полным набором прав пользователя по всем сервисам уже в JSON-формате, что позволит не тратить время на обработку и сократит время выборки. Как результат, удалось сократить ещё 2 запроса к БД и время подготовки ответа стало<strong>~70 мс</strong>.</li>
7 <li><strong>Денормализация данных</strong>. Принято решение денормализовать данные и хранить в БД уже подготовленный ответ с полным набором прав пользователя по всем сервисам уже в JSON-формате, что позволит не тратить время на обработку и сократит время выборки. Как результат, удалось сократить ещё 2 запроса к БД и время подготовки ответа стало<strong>~70 мс</strong>.</li>
8 <li><strong>Проблема обновления данных</strong>. После денормализации было установлено, что существует юзеркейс, в котором пользователям несколько раз меняется набор прав, после чего они тут же запрашиваются. Денормализация привела к тому, что время обновления прав существенно возросло, и в этом кейсе время подготовки ответа пользователю вернулось почти к исходному показателю (стало ~130 мс). Кейс не был очень существенным, однако подвёл к мысли, что решение пока неоптимально. В качестве дополнительной оптимизации изменили алгоритм денормализации и стали хранить данные по правам на каждый сервис в отдельном поле и отдавать информацию о правах на каждый сервис по отдельности (бизнес-логику это не нарушило, т. к. каждому сервису интересны права только про его страницы и элементы). Это позволило обновлять данные частично и немного уменьшило трафик, в итоге время подготовки типичного ответа стало<strong>~60 мс</strong>, а в рассмотренном юзеркейсе<strong>~90 мс</strong>.</li>
8 <li><strong>Проблема обновления данных</strong>. После денормализации было установлено, что существует юзеркейс, в котором пользователям несколько раз меняется набор прав, после чего они тут же запрашиваются. Денормализация привела к тому, что время обновления прав существенно возросло, и в этом кейсе время подготовки ответа пользователю вернулось почти к исходному показателю (стало ~130 мс). Кейс не был очень существенным, однако подвёл к мысли, что решение пока неоптимально. В качестве дополнительной оптимизации изменили алгоритм денормализации и стали хранить данные по правам на каждый сервис в отдельном поле и отдавать информацию о правах на каждый сервис по отдельности (бизнес-логику это не нарушило, т. к. каждому сервису интересны права только про его страницы и элементы). Это позволило обновлять данные частично и немного уменьшило трафик, в итоге время подготовки типичного ответа стало<strong>~60 мс</strong>, а в рассмотренном юзеркейсе<strong>~90 мс</strong>.</li>
9 <li><strong>Выносим данные в кэш</strong>. Денормализованные данные стало возможно вынести в кэш, т. к. теперь обновление прав затрагивало конкретные кэшированные данные, которые можно было легко инвалидировать. Это позволило сократить время подготовки типичного ответа до<strong>~50 мс</strong>. Пиковую нагрузку уже стало можно выдерживать, но без какого-либо запаса прочности.</li>
9 <li><strong>Выносим данные в кэш</strong>. Денормализованные данные стало возможно вынести в кэш, т. к. теперь обновление прав затрагивало конкретные кэшированные данные, которые можно было легко инвалидировать. Это позволило сократить время подготовки типичного ответа до<strong>~50 мс</strong>. Пиковую нагрузку уже стало можно выдерживать, но без какого-либо запаса прочности.</li>
10 <li><strong>Оптимизируем работу IoC-контейнера (отказ от автоматического DI)</strong>. Дальнейшая оптимизация со стороны источников данных уже не могла дать значительного прироста производительности, поэтому начали оптимизировать уже работу кода. В процессе анализа выяснилось, что автоматический подбор классов, реализующих требуемые интерфейсы в IoC-контейнере, достаточно медленный. Было решено отказаться от этого механизма и подсказывать контейнеру явно конкретные классы без привязки к интерфейсам. Это позволило сократить время до<strong>~45 мс</strong>.</li>
10 <li><strong>Оптимизируем работу IoC-контейнера (отказ от автоматического DI)</strong>. Дальнейшая оптимизация со стороны источников данных уже не могла дать значительного прироста производительности, поэтому начали оптимизировать уже работу кода. В процессе анализа выяснилось, что автоматический подбор классов, реализующих требуемые интерфейсы в IoC-контейнере, достаточно медленный. Было решено отказаться от этого механизма и подсказывать контейнеру явно конкретные классы без привязки к интерфейсам. Это позволило сократить время до<strong>~45 мс</strong>.</li>
11 <li><strong>Делаем часть работы IoC-контейнера вручную</strong>. Следующим шагом стал полный отказ от использования контейнера для инстанцирования отдельных сущностей, которые использовались для подготовки ответа с набором прав пользователя. Т. е. в провайдере служб эти сущности собирались вручную с lazy-инициализацией. Смогли выиграть ещё немного и получили результат<strong>~37 мс</strong>.</li>
11 <li><strong>Делаем часть работы IoC-контейнера вручную</strong>. Следующим шагом стал полный отказ от использования контейнера для инстанцирования отдельных сущностей, которые использовались для подготовки ответа с набором прав пользователя. Т. е. в провайдере служб эти сущности собирались вручную с lazy-инициализацией. Смогли выиграть ещё немного и получили результат<strong>~37 мс</strong>.</li>
12 <li><strong>Оптимизация роутинга</strong>. Сервис поддерживал порядка 60 различных запросов, что приводило к довольно медленному роутингу. Часть запросов была удалена (в них возвращалась частично информация о правах, теперь стал использоваться полный вариант), часть объединена с переходом на дополнительные параметры в теле запроса. В результате удалось сократить количество запросов до ~20. Эта оптимизация плюс включение кэширования в провайдере роутинга позволило сократить время до<strong>~30 мс</strong>(включение кэширования без оптимизации давало результат ~33 мс).</li>
12 <li><strong>Оптимизация роутинга</strong>. Сервис поддерживал порядка 60 различных запросов, что приводило к довольно медленному роутингу. Часть запросов была удалена (в них возвращалась частично информация о правах, теперь стал использоваться полный вариант), часть объединена с переходом на дополнительные параметры в теле запроса. В результате удалось сократить количество запросов до ~20. Эта оптимизация плюс включение кэширования в провайдере роутинга позволило сократить время до<strong>~30 мс</strong>(включение кэширования без оптимизации давало результат ~33 мс).</li>
13 </ol><p>На этом этапе было принято решение остановиться, т. к. дальнейшая оптимизация уже требовала значительной переработки архитектуры. В итоге<strong>удалось сократить время на подготовку запроса почти в 4.7 раза</strong>и добиться некоторого запаса производительности даже на период пиковых нагрузок. Laravel-специфичная оптимизация на последних этапах позволила получить примерно<strong>треть итогового прироста производительности</strong>.</p>
13 </ol><p>На этом этапе было принято решение остановиться, т. к. дальнейшая оптимизация уже требовала значительной переработки архитектуры. В итоге<strong>удалось сократить время на подготовку запроса почти в 4.7 раза</strong>и добиться некоторого запаса производительности даже на период пиковых нагрузок. Laravel-специфичная оптимизация на последних этапах позволила получить примерно<strong>треть итогового прироста производительности</strong>.</p>
14  
14