-
Notifications
You must be signed in to change notification settings - Fork 47
Task8 Work Cases #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Task8 Work Cases #17
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| # Task 8 | ||
|
|
||
| ## Introduction | ||
|
|
||
| Я работаю в компании igooods - доставка продуктов на дом. У нас монолит на Ruby on Rails, который вот уже несколько месяцев потихоньку растаскиваем. У нас уже вынесена пользовательская фронтовая часть в отдельный проект на ReactJS. В монолите у нас осталась API (для реакта и мобильных приложений iOS & Android), админка сделаная в Rails-way стиле, а также есть еще пара Rails Engines с API сборщика и курьера. | ||
|
|
||
| Проект начал разрабатываться в 2015 году аутсорсинговой компанией, но с недавнего времени руководство решило набрать штат разработчиков. Я пришел в компанию в середине Марта этого года. Сейчас работаю в команде мобильной разработки (iOS & Android apps), отвечаю за бекенд часть. | ||
| В качестве системы мониторинга и сбора логов используем DataDog. И по моим наблюдениям производительность приложения неуклонно деградирует. Никто особо не следит за производительностью, нет алертов на долгие ответы, часть эндпоинтов увеличило среднее время ответа в несколько раз (x2 x3) за время моего совсем не пристального наблюдения. | ||
|
|
||
| В общем я думаю мне будет чем заняться как минимум до конца года. | ||
|
|
||
| ## Stack | ||
|
|
||
| - Ruby on Rails | ||
| - PostgreSQL | ||
| - Elasticsearch | ||
| - DataDog | ||
| - rspec + test-prof | ||
|
|
||
| ## Choose case | ||
|
|
||
| Для начала, я пошел к своему менеджеру, показал ему несколько проблемных эндпоинтов в датадоге, и сказал что готов их всех оптимизировать. Он выбрал наиболее критичную на его взгляд функциональность - это поиск в каталоге. | ||
|
|
||
| Для поиска в API предусмотрено два эндпоинта и оба они отвечали в среднем примерно за 350 ms (p50), (p99 > 3 sec). | ||
|
|
||
| Бюджетом метрики было выбрано время 200 мс. | ||
|
|
||
| ## Case 1 First search | ||
|
|
||
| Поиск в каталоге у нас происходит по следующему принципу: сначала пользователю выдается часть результатов с разбивкой по категориям (1 запрос), а потом он может выбрать конкретную (или все) и посмотреть все там (2 запрос). | ||
|
|
||
| Первым я решил оптимизировать первый запрос с разбивкой по категориям. | ||
|
|
||
| ### Step 1. Preparing | ||
|
|
||
| Первое что я сделал - это настроил `local_production` окружение и потестил скорость на своем ноутбуке, с помощью утилиты Apach Benchmark. Оказалось что время ответа у нас с продом примерно одинаковое. Perfect! | ||
|
|
||
| ``` | ||
| Document Length: 41400 bytes | ||
|
|
||
| Concurrency Level: 1 | ||
| Time taken for tests: 3.109 seconds | ||
| Complete requests: 10 | ||
| Failed requests: 0 | ||
| Total transferred: 418820 bytes | ||
| HTML transferred: 414000 bytes | ||
| Requests per second: 3.22 [#/sec] (mean) | ||
| Time per request: 310.853 [ms] (mean) | ||
| Time per request: 310.853 [ms] (mean, across all concurrent requests) | ||
| Transfer rate: 131.57 [Kbytes/sec] received | ||
|
|
||
| Connection Times (ms) | ||
| min mean[+/-sd] median max | ||
| Connect: 0 0 0.0 0 0 | ||
| Processing: 294 311 14.7 307 339 | ||
| Waiting: 294 311 14.7 307 339 | ||
| Total: 294 311 14.7 307 339 | ||
|
|
||
| Percentage of the requests served within a certain time (ms) | ||
| 50% 307 | ||
| 66% 307 | ||
| 75% 316 | ||
| 80% 334 | ||
| 90% 339 | ||
| 95% 339 | ||
| 98% 339 | ||
| 99% 339 | ||
| 100% 339 (longest request) | ||
| ``` | ||
|
|
||
| ### Step 3. Using Ruby-prof | ||
|
|
||
| Далее я начал профилировать и искать самый жирный по CPU участок кода. И нашел его практически сразу, это оказался не сам поиск, но подготовка ответа на клиент. | ||
| Посмотрев поближе, что происходит в самом жирном методе, я увидел, что после группировки записей на каждую группировку следует запрос количества в Elastic. т.е. здесь мы имели классический N+1, только в elastic. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Получается не совсем по CPU жирный участок, а по wall-time, во время которого CPU на самом деле ещё и простаивает
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Да, конечно же wall-time. Не то написал. |
||
|
|
||
| ### Step 4.1 Elasticsearch to DataDog | ||
|
|
||
| При просмотре логов локально, я видел запросы в эластик, но в датадоге их небыло. Поэтому следующим моим пулреквестом стало подключение эластика к датадогу. Картина в датадоге по этому эндпоинту стала более очевидной - запросы в эластик отжирали кучу времени. Плюс я увидел запросы, которые мне не показывались в development логе - `index_name/count`. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| ### Step 4.2 Do you realy need it? | ||
|
|
||
| У нас в компании не оказалось хороших специалистов по эластику, поэтому, вместо того чтобы думать об оптимизации самих запросов в эластик, я спросил у своей mobile команды, и команды frontend (web) используют ли они это количество из ответа. Оказалось, что кол-во товаров в каждой категории отображается после поиска в web, а ios и android этого не используют. Далее были не продолжительные переговоры с командой веба, и принято решение просто выпилить расчет количества из ответа, и за количеством они будут ходить по другому эндпоинту (который уже есть, но нуждается в небольшой доработке по входящим параметрам). | ||
|
|
||
| ### Step 5. Something else? | ||
|
|
||
| Задачи были поставлены, и взяты в следующий спринт на исполнение. Я пока выпилил не нужную кучу запросов, и замерил время ответа с помощью ab. Оно сильно уменьшилось, и стало в районе 190 - 220 мс. Взглянув еще раз в логи, я увидел там N+1 запрос в базу на получение главной категории (у нас используется gem `ancestry`). Причем это все происходит в одном из основных сериалайзеров, и если убрать N+1 из него - получим небольшой прирост производительности почти в половине приложения и перестанем нагружать БД ненужной работой. | ||
|
|
||
| ### Step 6. Cache it stupid | ||
|
|
||
| Ancestry не позволяет легко и просто предзагрузить данные, к тому же даже если сделать это, придется передавать их в сериалайзеры во множестве местах. Исходя из этого, я выбрал путь кеширования (хранения) нужной информации прямо в таблице БД. Тогда все решится обычным добавлением ассоциации в eager_load/includes | ||
| Для этого я сделал миграцию по добавлению поля, и отдельный rake-task для заполнения этого поля. Также внес необходимые изменения в код создания объекта. Выложил пулреквест. | ||
|
|
||
| ### Step 7. Finishing | ||
|
|
||
| Теперь оставалось только внести изменения в сериалайзер, и убрать N+1, что я и сделал. | ||
| Бенчмарки обрадовали. В бюджет уложился. | ||
|
|
||
| ``` | ||
| Document Length: 41298 bytes | ||
|
|
||
| Concurrency Level: 1 | ||
| Time taken for tests: 1.503 seconds | ||
| Complete requests: 10 | ||
| Failed requests: 0 | ||
| Total transferred: 417800 bytes | ||
| HTML transferred: 412980 bytes | ||
| Requests per second: 6.65 [#/sec] (mean) | ||
| Time per request: 150.269 [ms] (mean) | ||
| Time per request: 150.269 [ms] (mean, across all concurrent requests) | ||
| Transfer rate: 271.52 [Kbytes/sec] received | ||
|
|
||
| Connection Times (ms) | ||
| min mean[+/-sd] median max | ||
| Connect: 0 0 0.0 0 0 | ||
| Processing: 130 150 17.4 152 179 | ||
| Waiting: 130 150 17.5 151 179 | ||
| Total: 130 150 17.4 152 179 | ||
|
|
||
| Percentage of the requests served within a certain time (ms) | ||
| 50% 152 | ||
| 66% 154 | ||
| 75% 157 | ||
| 80% 178 | ||
| 90% 179 | ||
| 95% 179 | ||
| 98% 179 | ||
| 99% 179 | ||
| 100% 179 (longest request) | ||
| ``` | ||
|
|
||
| ### Step 8. Waiting for deploy | ||
|
|
||
| В данный момент команда web уже сдалала свое дело, и мой последний PR с оптимизацией уедет в продакшн в ближайшем релизе. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 very cool |
||
|
|
||
| ## Case 2. Second search | ||
|
|
||
| ### Step 1. DataDog | ||
|
|
||
| После подключения эластика к мониторингу я увидел что здесь в среднем делается 7! запросов в эластик. Это оказалось для меня неожиданностью. Поразбиравшись "а зачем так много ведь по идее достаточно одного?", я понял что одним тут не обойдешься (если опять таки не урезать ответ сервера), но тремя - вполне (это все с высоты моего поверхностного знания эластика, а так... может и можно в один уложиться). | ||
|
|
||
| ### Step 2. Reduce queries | ||
|
|
||
| Я потратил некоторое количество времени на изучение вопроса как получить в одном запросе данные, в нужном мне порядке, и нашел ответ. В данный момент переписываю код на использование результатов нового запроса. По моим прогнозам - как раз останется 3 (и в некоторых случаях 4) необходимых запроса. По бенчмаркам пока не могу сказать, на сколько уменьшится время. | ||
|
|
||
| ### Step 3. Bug... | ||
|
|
||
| При раскуривании всего этого дела, я обнаружил баг, что в некоторых случаях (применение доп. фильтров по поисковому запросу) вспомогательная информация, отправляемая в ответе по этому поиску - некорректна. Но при этом в приложениях этого бага не видно, т.е. имеем не всегда нужную работу на сервере. | ||
|
|
||
| В планах обговорить это с командой, и вынести куда нибудь еще получение этой информации, т.к. она далеко не всегда полчается им нужна. | ||
|
|
||
| ### Step4 To be continued... | ||
|
|
||
| ## Case 3. CI | ||
|
|
||
| ### Step 1. Description | ||
|
|
||
| У нас очень долго (от 7 до 10 минут) исполняется job по прогону тестов на CI. | ||
| В 7 домашке я пытался воткнуть нам parallel_tests и время было просто прекрасным - CI проходил за 4 минуты (и это без балансировки). Но к сожалению получили рандомно падающие тесты с ошибками нейминга. Команда бекендеров в целом оказалась против такого. | ||
|
|
||
| ### Step 2. Occasion | ||
|
|
||
| На CI (да и на локалке тоже) периодически падали тесты, когда эластик по каким-то непонятным причинам оказывался недоступен. Обидно, когда из-за независящих от тебя причин падает CI, и приходится перезапускать его (а иногда и несколько раз). | ||
| Обращался к нашим девопсам - обещали посмотреть и решить проблему, но так и ни фига не сделали пока. | ||
|
|
||
| И вот однажды после очередного падения, я решил залезть в тест и посмотреть, так ли нужен в нем эластик, т.к. судя по тесту было не понятно зачем он там. И в теле самого теста я никаких запросов в индекс не нашел. | ||
|
|
||
| ### Step 3. Holy shit!!! | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. )) |
||
|
|
||
| И внезапно оказалось что в spec_helper было установлено следующее `Chewy.strategy(:urgent)`. То есть мы дрючим эластик практически во всех тестах, где надо и где не надо. Каждое создание/обновление товара или даже позиции в корзине обновляло индекс в эластике. | ||
|
|
||
| Я изменил стратегию на bypass, которая обновляет индекс только по явному требованию, отредактировал пару мест в приложении, чтобы учитывалась текущая установленная стратегия. | ||
|
|
||
| ### Step 4. Catshooting | ||
|
|
||
| До кучи, я решил разобраться с проблемой котиков. Недавно у меня закончилось место на диске. При расследовании выяснилось что у меня очень много фотографий котиков в папке uploads (60Gb). Котик по умолчанию загружался в фабрике создания сотрудника, причем валидации на обязательность аватарки не было. Вынес в треит и после этого починил 2 теста, которые реально проверяли автарку. | ||
| Создал задачу на будущее об очистке артифактов после прогона тестов. | ||
|
|
||
| ### Step 5. Take Profit | ||
| Выложил пулреквест. Job с тестами теперь выполняется на CI за 4,5 - 5 минут. Нехилый профит как мне кажется. Заодно и уменьшили внезапные падения тестов из-за недоступности эластика. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ну и если всё-таки прикрутить parallel_test уже будет 2 минуты, это будет прям другое ощущение от работы |
||
|
|
||
| ## Результаты | ||
|
|
||
| Удалось применить полученные на курсе знания в своем проекте. | ||
| Ранее также занимался задачами оптимизации, но больше вслепую без использования профилировщиков. Одни раз использовал ruby-spy. Но в целом было все удачно. Но это скорее от безобразно написанного кода, и бездумных решений авторов (в том числе моих)). | ||
| Самый интересный кейс был с sidekiq воркером, который отжирал все 220 GB памяти сервера за 4 часа непрерывной и умирал, без результатов. | ||
| И мне удалось сделать так чтобы он на тех же данных работал 15 минут и жрал только 3,5 GB памяти (just memoization). | ||
|
|
||
| Здесь же, мне помогли профилировщики и мониторинг, и без них было совсем не очевидно по коду где именно главная точка роста. | ||
| В планах и дальше заниматься оптимизацией API и не только. Кайф от этого получал всегда :) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 👍 👍 |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Кажется монолит, распиливаемый на сервисы - сейчас наиболее распространённое состояние проектов )