Skip to content
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

Evgeny Varnakov -- Week 8 #22

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

geneux
Copy link

@geneux geneux commented Jul 20, 2021

Задание 8

О проекте, вкратце )

Как я писал ранее, много рассказать не получится. Да, проект большой, страшно интересный и рассказать про него можно очень много, но... Проект плотно закрыт NDA, насколько это вообще возможно и вся разработка сильно закрыта и изолирована от внешнего мира. Могу только сказать, что это федеральная информационная система, и ей уже не первый год пользуется большинство граждан нашей страны.

Технически, это набор из многих связанных сервисов, часть из них, как уже можно было понять, на Ruby и Ruby on Rails. Сама разработка ведется разными командами и не первый год, со всеми вытекающими последствиями, в том числе, периодически возникают какие-то кейсы, влияющие на потребление ресурсов и производительность. При этом, код проекта оптимизировался не раз, и сделать определенные процессы быстрее или менее прожорливыми бывает достаточно не тривиально.

Ради того, чтобы освежить инструментарий для решения таких задач, я и пошел на этот курс. В данном случае, меня интересовало чисто практическое применение в рабочих задачах, без какой-то рекламной составляющей, написания статей на Хабре, или "накачки" личного бренда. Я не пришел на курс "с нуля" и далеко не первый раз столкнулся с оптимизациями, но мне было очень интересно посмотреть взгляд на этот процесс под другим углом, и узнать что-нибудь новое. И, надо сказать, это получилось!

О применении всего на проекте

Но, нет, применять все сразу, "на хайпе" я тоже не стал. Да, это можно бы было сделать на многих интернет-проектах, которые открыты, имеют простой цикл разработки и сами по себе поощряют любые изменения, направленные на оптимизации. А здесь все выходит не так:

  1. Мы занимаемся только разработкой. Эксплуатацией продакшен-инфраструктуры занимается заказчик, мы не имеем ни доступа к боевой инфраструктуре, ни к системам мониторинга. Если случаются проблемы, они формируются в виде отдельных инцидентов, и вот тут уже надо быть наготове! Но это достаточно частные случаи, к тому же, обычно достаточная оптимизация достигается за счет простого рефакторинга, подкрепляясь замерами.
  2. Наша инфраструктура для разработки и тестирования несколько отличается от продуктива, да и не имеет реальной нагрузки. Это, кстати, вносит нам в повседневной жизни некоторые проблемы с тем, чтобы повторять реальные кейсы с прода.
  3. По производительности все уже много раз оптимизировалось, красивого варианта с "тут поправил, все стало вдвое быстрее" не получится. Можно говорить о том, чтобы найти бутылочное горлышко и выжать несколько процентов, но и это уже непросто.
  4. И организационно -- есть четко регламентированный процесс согласования изменений и бюджетов на определенные задачи, в который спонтанные оптимизации совсем не вписываются.

Но, вполне предсказуемо, пара интересных кейсов за время прохождения все же возникло, о них и расскажу

Кейс 1. Деградация производительности на 2.7.

Да, мы поймали случай хорошей такой деградации выполнения при бампе версии Руби. Расскажу эту историю поподробнее.

Есть у нас один Руби-сервис, в виде отдельного проекта, представляющий, по сути своей, некое API для организации и работы с большими данными. Он реализует всякие map-reduce концепции и все такое. Что тут главное -- структуры данных в памяти там перелопачиваются действительно очень большие. В силу своей обособленности и хорошей стабильной работы, этот сервис долгое время пробыл без существенных изменений и "застрял" на старой версии Рубей. В определенный момент мы решили его обновить на 2.7.последний, и... Получили ощутимую такую деградацию по времени работы.

Проект был хорошо покрыт тестами, имеющими метрики по времени выполнения, поэтому мы смогли практически сразу определить проблемное место. А вот дальше начались интересные поиски.

Понятно, что по коду совершенно ничего подозрений не вызывало. Там были относительно простые манипуляции с длинными числовыми массивами. Так как использовалось относительно много ФП-конструкций, вызвали подозрение сначала именно они.

RbSpy особенно не смог указать на какое-то место, в основном из-за роста в "c function". Но, пройдя по проблемным местам ruby-prof-ом, мы, с удивлением, обнаружили, что притормаживает вызов Array#include?!

Когда удалось убедиться в том, что это действительно этот вызов, и ФП было реабилитировано, объяснение нашлось простое, но совершенно нелогичное: в версии 2.5 был переработан метод rb_equal, вот в этом PR: https://github.com/ruby/ruby/pull/1552/files

Видно, что ранее для сравнения элементов использовался метод rb_equal_opt, и был переделан на вызов rb_equal. Последний более универсален, позволил убрать лишнюю логику в интерпретаторе, но... медленнее по факту для длинных массивов с относительно простыми типами данных, такими как числа. Что и наблюдалось в нашем случае.

Так что, это история наглядно демонстрирует, что не все при повышении версии строго улучшается, бывает и наоборот.

Кейс 2. Оптимизация невоспроизводимого.

Интересный случай возник также у нас совсем недавно, и был связан, скорее, с построением цикла оптимизации, чем непосредственно с ней. Мы просто не могли воспользоваться профайлером.

Ситуация была относительно простая: был сервис импорта данных в систему, он был расчитан на определенный размер этих данных. Но из-за изменившегося законодательства, вдруг стали появляться данные, размером в 50-100 раз превышающие те, на которые закладывались при разработке.

Система показала себя очень достойно, и даже смогла обработать часть из них. Но, понятно, что какой-то критический момент не мог не настать, и к сервису импорта все-таки стал захаживать OOM killer.

Все было бы просто: запустить memory_profiler и посмотреть, куда ушла память, и оптимизировать этот процесс. Однако, даже на самой мощной разработческой машине памяти было в 8 раз меньше, чем на серверах, где возникала проблема (32 против 256 Гб). Доткнуть памяти также не представлялось реальным, материнка на десктопах все равно не позволяла держать больше 64, про ноуты вообще молчу. Понятно, что надо было действовать иначе.

Мы пошли двумя путями: стали строить тестовые структуры и пытаться уменьшить размер входных данных. И и в том, и в другом случае получили практически один и тот же результат: Память или не кончалась, демонстрируя некий крайне небольшой логарифмический рост, ближе к константе, или вдруг начинала лавинообразно потребляться (что видно тем же Valgrind-ом) и это вызывало аварийное завершение процесса. Собственно, из-за того, что поймать рост, не получив попутно OOM не получалось, мы не могли применить профилировщик.

Ок, что делать? Окей, мы не можем определить, в какой момент происходит переполнение точно. И мы можем наблюдать со стороны системы что есть сильный рост. Поэтому, было решено каким-то образом поймать момент этого роста, и запрофилировать. Для этого мы запустили параллельно основному процессу еще один тред, и в нем стали измерять потребление через промежутки времени. При достижении порогового значения, тред останавливал выполнение основной логики и собирался отчет memory_profiler'а. Подобрав затем точное значение порога, мы смогли запрофилировать момент лавинообразного роста...

Не буду совсем закапываться в детали, но причиной его оказался даже не размер данных, не их количество, а определенная структура. При ее возникновении в коде, внутри ФП-конструкций, возникала сильно вложенная рекурсия, в которую передавался весьма внушительный контекст. Случай был сильно частный, поэтому ни моделирование данных, ни из укорачивание и не дали никакого результата, роста не было.

Проанализировав существующее решщение, мы поняли, что оно не было особенно оправдано, хотя код выглядел весьма изящно. И, принеся в жертву изящность и сделав обработку в этом конкретном случае более линейной, мы, наконец, смогли избавиться от переполнений и закрыть проблему заказчика.

Это было как бы подверждение того, что ценой за красивые синтаксические конструкции может быть деградация производительности, либо потребления ресурсов. И красота кода не равна его эффективности.

Заключение

Эти истории, пожалуй, были наиболее интересными из всего, что возникало за последние пару месяцев. Да, в рамках такого, достаточно сухого изложения, тем более, когда уже известно относительно простое решение, все выглядит не слишком захватывающе. Но для работающей над проблемами команды это было в канве "Доктору Хаузу такое и не снилось". Было проведено много мозговых штурмов, опробовано тупиковых путей, и проведено много часов в ожидании, что наконец-то сейчас все выполнится как надо. И, в итоге, обладая правильным инструментарием, и понимая, как надо анализировать получаемую информацию, в итоге, все получилось.

Так что, грамотный системный подход к оптимизации -- правильный путь к ее успешному выполнению. И только так можно разобраться с, казалось бы, загадочным и необъяснимым.

@geneux geneux changed the title Optimization stories Evgeny Varnakov -- Week 8 Jul 20, 2021
Copy link
Collaborator

@spajic spajic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪


## Кейс 1. Деградация производительности на 2.7.

Да, мы поймали случай хорошей такой деградации выполнения при бампе версии Руби. Расскажу эту историю поподробнее.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

оу шит 😬


Видно, что ранее для сравнения элементов использовался метод `rb_equal_opt`, и был переделан на вызов `rb_equal`. Последний более универсален, позволил убрать лишнюю логику в интрепретаторе, но... медленнее по факту для длинных массивов с относительно простыми типами данных, такими как числа. Что и наблюдалось в нашем случае.

Так что, это история наглядно демонстрирует, что не все при повышении версии строго улучшается, бывает и наоборот.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Решили смириться, или заюзали какую-нибудь более быструю функцию на C?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Или откатились обратно?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Спасибо за ревью! Наконец добрался до того, чтобы ответить, на работе сплошные релизы, дома -- переезд )))

Здесь решение получилось уже не такое эпичное, как найденная проблема.)) Остались на старой версии, на 2.7 еще и памяти отъедалось больше -- похоже GC не очень успевал проявлять разум, а его таблицы стали обширнее. Сильно разбираться не стали, понятно было, что, по совокупности факторов, тут надо будет хорошенько зарыться, чтобы хотя бы в мониторинге не было видно деградации. Решили оставить сейчас как есть и запланировали переделку сервиса на C++, он достаточно простой, и переписать давно уже было желание.


Мы пошли двумя путями: стали строить тестовые структуры и пытаться уменьшить размер входных данных. И и в том, и в другом случае получили практически один и тот же результат: Память или не кончалась, демонстрируя некий крайне небольшой логарифмический рост, ближе к константе, или вдруг начинала лавинообразно потребляться (что видно тем же Valgrind-ом) и это вызывало аварийное завершение процесса. Собственно, из-за того, что поймать рост, не получив попутно OOM не получалось, мы не могли применить профилировщик.

Ок, что делать? Окей, мы не можем определить, в какой момент происходит переполнение точно. И мы можем наблюдать со стороны системы что есть сильный рост. Поэтому, было решено каким-то образом поймать момент этого роста, и запрофилировать. Для этого мы запустили параллельно основному процессу еще один тред, и в нем стали измерять потребление через промежутки времени. При достижении порогового значения, тред останавливал выполнение основной логики и собирался отчет `memory_profiler`'а. Подобрав затем точное значение порога, мы смогли запрофилировать момент лавинообразного роста...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Крутяк, у нас на курсе один раз парень сделал по такой схеме 2е ДЗ с оптимизацией памяти.
Запустил 2й поток и мониторил потребление памяти 1го.
А при превышении лимита сразу завершал работу.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, там тоже можно было так сделать... Но там процесс один идет, фактически, ограничится длина входных данных. А, вот, тут как раз несколько последовательных процессов было, и надо было понять, на каком все разносит, и такая реализация оказалась очень удобной и информативной.


Эти истории, пожалуй, были наиболее интересными из всего, что возникало за последние пару месяцев. Да, в рамках такого, достаточно сухого изложения, тем более, когда уже известно относительно простое решение, все выглядит не слишком захватывающе. Но для работающей над проблемами команды это было в канве "Доктору Хаузу такое и не снилось". Было проведено много мозговых штурмов, опробовано тупиковых путей, и проведено много часов в ожидании, что наконец-то сейчас все выполнится как надо. И, в итоге, обладая правильным инструментарием, и понимая, как надо анализировать получаемую информацию, в итоге, все получилось.

Так что, грамотный системный подход к оптимизации -- правильный путь к ее успешному выполнению. И только так можно разобраться с, казалось бы, загадочным и необъяснимым.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪💪💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants