среда, 17 декабря 2014 г.

[prog.memories] Помнится, обрабатывал когда-то большие объемы данных на Ruby...

Вот эта запись в FB навеяла воспоминания об одной из задачек, которую мы решали в "Интервэйле" сначала с помощью Ruby, а затем C++.

Суть была в том, что один из программных комплексов писал на диск множество "протокольных файлов", т.е. файлов, в которых фиксировались следы обработки транзакций. Вроде такого: начали фазу #1, получили результат фазы #1, начали фазу #2, получили результат фазы #2 и т.д. По мере накопления информации в протокольных файлах ее нужно было извлекать и записывать в большую историческую БД. А уже по содержимому БД затем строилась всяческая статистика и отчеты.

Где-то в 2005-м или 2006-м для решения задачи импорта протокольных файлов в историческую БД был использован язык Ruby. Что казалось разумным. Код короткий, не сложный, основное время тратится на парсинг текста из файла и операции с БД. Т.е. в основном должны были работать низкоуровневые процедуры, написанные на C (либо в дебрях работы с регулярными выражениями, либо внутри ODBC-драйверов), так что общая тормознутость Ruby препятствием не должна была быть.

Поначалу все работало как задумывалось, но и объемы данных были небольшими, всего несколько миллионов записей за сутки. Но время шло, росли размеры протокольных файлов и где-то к концу 2009-го, когда нужно было перелопачивать уже десятки, а то и сотни миллионов записей в сутки, стало очевидно, что недалек тот день, когда Ruby-скрипту для импорта в БД информации, собранной за 24 часа работы программного комплекса, потребуется больше 24-х часов работы.

Нужно сказать, что сам скрипт импорта работал по крайне примитивному алгоритму. И можно было бы получить заметный выигрыш за счет смены алгоритма. Но предварительные разбирательства с узкими местами показали, что огромное количество времени тратится внутри Ruby-библиотеки для работы с СУБД. Причем тормозит именно Ruby-новый код, который формирует обращения к ODBC-слою. Т.е. здесь тормоза Ruby показали себя во всей красе.

Соответственно, ребром стал совсем не праздный вопрос: а имеет ли смысл реализовывать на тормозном Ruby более изощренный алгоритм импорта, если часть выигрыша все равно будет съедена неповоротливостью Ruby-нового рантайма?

К этому добавилась еще одна проблемка -- динамическая типизация. Динамическая типизация очень хороша в маленьких скриптах, написанных на выброс. Но когда приложение живет и развивается годами, когда разные его куски переписывают/дописывают разные люди, когда в течении месяцев в работающий код никто вообще не заглядывает, то динамическая типизация начинает играть против разработчика. Слишком много времени уходит на то, чтобы восстановить детали работы того или иного куска кода. Особенно, когда там задействуются более-менее сложные структуры данных. В статически-типизированном языке довольно просто -- сразу видно, что вот здесь список из объектов, внутри которых словари, элементами которых являются мультимножества, ключом в которых вот это, а значением -- вектор вот таких структур. Тогда как в Ruby-коде такие вещи нужно как-то по-другом раскапывать. Даже при наличии хорошего кода и комментариев в нем.

В общем, году в 2010-м мы все это дело переписали на C++. Заодно и применив более хитрый алгоритм, в котором часть данных предварительно накапливалась и обрабатывалась в ОП, тем самым, существенно уменьшалось количество последующих запросов к СУБД. Здесь было довольно много работы в ОП, и мне даже сложно представить, насколько все это было бы медленнее в Ruby. В итоге, новая C++ная версия импорта оказалась, как минимум, на порядок быстрее предыдущей или даже более быстрой, но этого я уже не помню, склероз... :) Может бывшие коллеги меня поправят. (Upd. Очевидцы подсказали, что время обработки было снижено с двух десятков часов до десятка минут.)

Эта история стала для меня еще одним подтверждением того, что скриптовые языки и серьезные задачи -- это плохо совместимые вещи. Огромное достоинство программ на скриптовых языках для меня -- это возможность поправить что-то прямо на целевой машине. Со скомпилированным в бинарник C++ кодом так не получится, а вот в Ruby-коде учесть какую-то особенность целевой платформы прямо на месте -- запросто. Однако, как только появляется намек на то, что программа должна работать быстро и в течении длительного времени, то от скриптов и динамики лучше сразу уйти. Тем более, что современные мейнстримовые компилируемые языки (C++, Java, C#) уже обзаводятся конструкциями, позволяющими писать меньше кода, но не терять ни в читабельности, ни в скорости работы.

Комментариев нет: