Мы часто пишем код, не вникая, сколько ресурсов уйдет на его выполнение. И это может быть ок. Но, во-первых, каждый нормальный разработчик старается становиться лучше и раскачивает свои скилы. Во-вторых, это легко может обернуться реальным багом с абсолютно не очевидными корнями, и вопрос-таки придется решать. В моем случае эта проблема проявилась, когда девопсы перевели нашу CRM систему в Kubernetes. В ней перестал формироваться Excel отчет по заявкам, если задать ему временную выборку хотя бы за месяц. Sidekiq в какой-то момент просто прекращал выполнение джобы, не возвращая ошибок (хотя код сервиса предусматривает и запись exception-ов, и изменение статуса экспорта). Модель экспорта у нас просто зависала в статусе ‘in_progress’, но Sidekiq прекращал выполнять работу, никого в известность не ставя.  Забегая вперед, скажу, что причина оказалась в новых настройках контейнеров, которым девопсы жестко ограничили допустимое использование памяти, и если процесс выходил за рамки, то он просто тихо убивался. Таким образом, слишком грузные экспорты зависали в CRM, якобы в прогрессе, а пользователи ничего не могли поделать.  Оптимизировать "наугад" — вообще плохая идея. Чтобы понимать, есть ли в наших действиях какой-то толк, сформируем метрику. Для измерения памяти воспользуемся гемом benchmark-memory, и напишем нехитрый метод: Обратите внимание на вызов Garbage Collector в конце. Не подчищенный «мусор» очень любит раздувать память. А эффективность работы GC сильно зависит от версии Ruby чем новее, тем лучше и быстрее он работает. Но нам, чтобы измерять проводимые оптимизации, нужен стабильный результат. Вот, например, как наш метод оценил бы экспорт 100 заявок без вызова GC: В общем, включаем в методе запуск Garbage Collector’а, и теперь мы сможем легко измерить потребление памяти для любого процесса: Здесь мы видим, что для простого вывода единицы в консоль нам требуется 464 байта. Результаты для исходной реализации нашего экспорта выглядели так: Для 100 заявок требуется полтора мегабайта, для 1000 заявок — почти в десять раз больше. Налицо линейная асимптотика — чем больше исходные данные, тем больше потребляемая память. Звучит логично, но это и есть, так называемое, раздувание памяти (ссылка на подробный материал об этом ниже). Получается, для корректной работы программы мы должны заранее учесть возможные ограничения на обращение к сервису экспорта, нас это не устраивает. Первое, что пришло в голову — это изменить формат отчета, и перейти с xlsx на csv. Спросил у «бизнеса», оказалось, им без разницы. В итоге, такой мизерной «оптимизации» оказалось достаточно, чтобы на 40% сократить потребление памяти: Весь процесс формирования отчета можно изобразить такой схемой:  В принципе, уже при взгляде на нее не сложно догадаться, где раздувается память. В ReportWorker’е мы целиком формируем пулл объектов и передаем его наш сервис. Но можно попробовать записывать данные в потоковом режиме. Если передавать в сервис только массив айдишников, мы можем использовать в нем такой метод для формирования коллекции: Но как сделать так, чтобы результаты запроса к базе не загружались в память целиком, а обрабатывались по очереди? Тут нам поможет метод ActiveRecord find_each: В итоге, следующий замер потребления памяти показал такие результаты: То есть, как мы и ожидали, память не раздувается, независимо от того, какое количество заявок мы хотим выгрузить в файл.  Итак, парочка достаточно простых оптимизаций, — и отчет, который раньше требовал 18,5 Гб, теперь укладывается менее чем в 200 Кб. Можно было и эту цифру сократить, например, выгружая из базы данных не весь объект целиком, а лишь те поля, которые нам нужны. Но в нашем случае пришлось бы сильно пожертвовать читабельностью кода, а заданный бюджет и так вполне устраивает, поэтому оптимизировать память дальше я не стал. А вот скорость оставляла желать лучшего. Но о том, как мы справились с этой проблемой, я расскажу вам в следующем посте. Web Developer