diff --git a/case-study.md b/case-study.md new file mode 100644 index 0000000..f88c6e9 --- /dev/null +++ b/case-study.md @@ -0,0 +1,58 @@ +# Задание № 8 + +### О проекте +* Дейтинг-сервис для мобильного оператора, нагруженность небольшая. +* Проект стартовал в начале 16-го года. +* Используем Sentry для ошибок; DBA следят за постгресом, при случае указывают на долгие запросы; сисадмины следят за нагрузкой прода в графане. +* На первый взгляд в проекте выглядело все хорошо: актуальные версии ruby и rails, было уже несколько индексов в базе, долгие процессы вынесены в асинхронные воркеры. +* Пришел в компанию Funbox полтора года назад на позицию бэкенд разработчика, в основном занимаюсь доработкой API в нескольких проектах. +До этого уже были проблемы с памятью в другом проекте, я толком не смог ничего улучшить, инициативу перехватил мой тимлид и + от него я узнал про jemalloc. Дальше я заинтересовался темой перформанса ruby и таким образом записался на этот курс =) + +### Проблема +Сисадмины завели задачу, что приложение с каждым днем потребляет все больше оперативной памяти, им приходится вручную +рестартить puma раньше времени (уже была ежедневная автоперезагрузка для логротатора). + +### Оптимизации +Первым делом попробовал самые простые инструменты: +- прогнал основные эндпойнты вместе с pghero, он только указал что есть 1 duplicate index, составной уже его покрывал, убрал индекс. +- запуск тестов с включенным bullet подсказал что можно добавить один прелоуд. Что интересно, после его добавления стало +еще на один запрос больше, а N+1 не исчезли. Дело оказалось в том, что такую связь с сортировкой запрелоудить нельзя: + +```has_one :last_message, -> { order(created_at: :desc) }, class_name: 'Message', inverse_of: :chat```. +Возможно в будущем это поле придется кэшировать. +- прогнал leaky-gems, результат показал что есть пару нестабильных гемов, обновил их версии. +- далее было решено перейти к серьезным мерам: подключил логгирование oink, запатчил его, чтобы еще логировался request_id запроса +и можно было посмотреть запрос в основных рельсовых логах. +- через день собрал логи, прогнал их через oink, он показал что в топе по использованию activerecord два эндпойнта. +Навскидку общего между ними было только использование единого сериалайзера. Глянув рельсовые логи, увидел что при SQL запросе + юзеров не было limit, оттуда и грузилось очень много записей. Далее в сериалайзере я нашел проблему: + `elsif scope.matches.map(&:id).include?(object.id)` + заменил на: + `elsif scope.matches.where(id: object.id).exists?` +- Пока проверял изменение, также заметил что мемоизация в сериалайзере не работала: + ``` + def superlike + @superlike ||= object.find_super_like(scope.id) + end + ``` + Стал разбираться, и оказалось что функция в некоторых случаях возвращает nil и такой вариант мемоизации не работает, + заменил на: + ``` + def superlike + return @superlike if defined? @superlike + + @superlike ||= object.find_super_like(scope.id) + end + ``` +- Сохранил изменения, выкатил на прод, на следующий день логи показывали значительное улучшение ситуации, + однако записей грузилось еще достаточно много. Глянув самые тяжелые запросы в логах обнаружил, что от некоторых + клиентов (возможно старые версии мобильного приложения) приходят запросы без параметра limit, тогда на бэке я добавил + дефолтный limit и валидацию на максимально допустимый (чтобы нельзя было специально сделать тяжелые + запросы с `?limit=500` и тд). + +### Результат: +- С помощью проделанной оптимизации, удалось сократить потребление памяти проекта с 3.6 ГБ до 3 Гб. Причиной оказалось +отсутствие лимитов в SQL запросе и в валидации параметров в целом, а на стейджинге и локально это не было заметно, так как не было +достаточно много записей в базе. В будущем планируем переход с active_model_serializer на другой сериалайзер для ускорения + времени ответа сервера.