Привет! Я уже писал вам о том, как продвигать инициативы в корпорации. Точнее, как (иногда) это удается, и какие сложности могут возникнуть: Ретроспектива граблей. Как самописное решение оказалось круче платного и Как мы выбирали систему кеширования. Часть 1.
Сегодня я хочу продолжить и рассказать про психологически наиболее напряженный момент в том проекте, про который первые две статьи – когда итог проекта определяли не столько технические навыки команды, сколько уверенность в своих расчетах и готовность идти до конца.
Скажу сразу – я считаю, что довести проект до такого напряженного момента – это ошибка намного большая, чем любой героизм по вытягиванию проекта из такой … проблемы.
Но, этот опыт я не скрываю и охотно делюсь им – потому, что считаю:
Сочетание этих пунктов – просто обязывает поделиться прекрасным опытом «как заработать переплет на ровном месте». Но, надо отметить, подобная ситуация – является исключительной в компании Спортмастер. То есть, исключено, что такая ситуация повторится – планирование и определение ответственности сейчас – совершенно на другом уровне.
Итак, кажется, достаточно вступления, если готовы – добро пожаловать под кат.
2017 год, июнь. Мы дорабатываем админку. Админка – это не только набор формочек и таблиц в web-интерфейсе – введенные значения нужно склеить с десятками других данных, которые получаем из сторонних систем. Плюс, каким-то образом преобразовать и, в итоге – отправить потребителям (главный из которых – ElasticSearch сайта Спортмастер).
Основная сложность как раз в том, чтобы преобразовать и отправить. А именно:
Если первые 2 пункта – чисто технические, и продиктованы самой задачей, то вот с 3-м пунктом, конечно же, надо разбираться организационно. Но, реальный мир далек от идеального, так что работаем с тем что есть.
А именно – разобрались с тем, как быстро клепать web-формы и их объекты на стороне сервера.
Один человек из команды назначался на роль профессионального «формо-шлёпа» и с помощью подготовленных web-компонент выкатывал демку для ui быстрее, чем аналитики правили рисунки этого ui.
А вот с тем, чтобы поменять схемы трансформаций – здесь возникала сложность.
Сначала мы пошли привычным путем – проводить трансформацию в sql-запросе к Oracle. В команде был специалист по DB. Он продержался до момента, когда запрос представлял из себя 2 страницы сплошного sql-текста. Мог бы продолжать и дальше, но когда приходили изменения от аналитиков – объективно, самое сложное было – найти то место, в которое внести правки.
Аналитики выражали правила на схемах, которые хоть и были нарисованы в чем-то отстраненном от программного кода (что-то из visio/draw.io/gliffy), но были так похожи на квадратики и стрелочки в ETL-системах (например, Pentaho Kettle, который в то время как раз использовали для поставки данных на сайт Спортмастер). Вот если бы у нас был не SQL-запрос, а ETL-схема! Тогда постановка и решение были бы топологически одинаково выражены, а значит – правка кода могла бы занимать времени столько же, что и правка постановки!
Но с ETL-системами есть другая сложность. Тот же Pentaho Kettle – отлично подходит, когда требуется создать новый индекс в ElasticSearch, в который записать все данные, склеенные из нескольких источников (remark: на самом деле, именно Pentaho Kettle – подходит не очень, т.к. в трансформациях использует javascript не связанный с java-классами, через которые к данным обращается потребитель – из-за этого можно записать то, что потом не получится превратить в нужные pojo-объекты. Но это отдельная тема, в стороне от основного хода статьи).
Но что делать, когда в админке пользователь поправил одно поле в одном документе? Для поставки этого изменения в ElasticSearch сайта Спортмастер – не создавать же новый индекс, в который залить все документы этого типа и, в том числе – обновленный!
Хотелось, чтобы, когда изменился один объект во входных данных – то в ElasticSearch сайта отправить обновление только для соответствующего выходного документа.
Ладно сам входной документ, но ведь он, по схеме трансформаций – мог через join быть прикреплен к документам другого типа! А значит, надо анализировать схему трансформаций и вычислять, какие именно выходные документы будут задеты изменением данных в источниках.
Поиск коробочных продуктов для решения такой задачи – не привел ни к чему. Не нашли.
А когда отчаялись найти – прикинули, а как это должно работать внутри, а как это можно сделать?
Идея возникла буквально сразу.
Если итоговую ETL можно разбить на составные части, каждая из которых имеет определенный тип из конечного набора (например, filter, join и т.д.), тогда, возможно – будет достаточно создать такой же конечный набор специальных узлов, которые соответствуют исходным, но с тем отличием, что работают не с самими данными, а с их изменением?
Очень подробно, с примерами и ключевыми моментами в реализации, наше решение – я хочу осветить в отдельной статье. Чтобы разобраться с опорными позициями – это потребует серьезного погружения, способности мыслить абстрактно и полагаться на то, что еще не проявлено. Действительно, это будет интересно именно с математической точки зрения и интересно только тем хабровчанам, кто интересуется техническими деталями.
Здесь скажу только, что мы создали математическую модель, в которой описали 7 типов узлов и показали, что эта система является полной – то есть, с помощью этих 7 типов узлов и соединений между ними – можно выразить любую схему трансформации данных. В основе реализации – активно используется получение и запись данных по ключу (именно по ключу, без дополнительных условий).
Таким образом, наше решение обладало сильной стороной в отношении всех вводных сложностей:
Из рискованных моментов, только один – решение пишем с нуля, самостоятельно.
Собственно, ловушки не заставили себя ждать.
Еще один сюрприз организационного характера состоял в том, что одновременно с нашей разработкой шел переход основного мастер-хранилища на новую версию, и при этом менялся формат, в котором данные это хранилище предоставляет. И хорошо было бы, чтоб наша система – сразу работала с новым хранилищем, а не со старым. Вот только, новое хранилище еще не готово. Но зато, структуры данных известны и нам могут выдать демо-стенд, на который зальют небольшое количество связанных данных. Идет?
Вот в продуктовом подходе, при работе с потоком поставки ценности, всем оптимистам однозначно вколачивается предупреждение: есть блокер -> задачу в работу не-бе-рем, точка.
Но тогда, такая зависимость даже подозрений не вызвала. Действительно, мы были в эйфории от успехов с прототипом Дельта-процессора – системы для обработки данных по дельтам (реализация математической модели, когда по схеме трансформации вычисляются изменения в выходных данных, как ответ на изменение данных на входе).
Среди всех схем трансформаций, одна была наиболее важной. Кроме того, что сама схема была самой большой и сложной, так еще к выполнению трансформации по этой схеме было жесткое требование – ограничение по времени выполнения на полном объеме данных.
Так, трансформация должна выполняться 15 минут и ни секундой дольше. Главные входные данные – таблица с 5,5 млн записей. На этапе разработки таблица еще не заполнена. Точнее, заполнена небольшим, тестовым набором данных в количестве 10 тысяч строк.
Что ж, приступаем. В первой реализации Дельта-процессор работал на HashMap в роли Key-Value хранилища (напомню, нам требуется очень много считывать и записывать объекты по ключам). Разумеется, что на продакшн-объемах, в памяти все промежуточные объекты не поместятся – поэтому вместо HashMap мы переходим на Hazelcast.
Почему именно Hazelcast – так потому, что этот продукт был знаком, использовался в backend к сайту Спортмастера. Плюс, это распределенная система и, как нам казалось – если друг что-то будет не так с производительностью – добавим еще инстансов на парочку машин и вопрос решен. В крайнем случае – на десяток машин. Горизонтальное масштабирование и все дела.
И вот, мы запускаем наш Дельта-процессор для целевой трансформации. Отрабатывает практически моментально. Это и понятно – данных то всего 10 тысяч вместо 5,5 млн. Поэтому измеренное время умножаем на 550, и получаем результат: что-то около 2 минут. Отлично! Фактически – победа!
Это было в самом начале работ по проекту – как раз, когда надо определиться с архитектурой, подтвердить гипотезы (провести тесты которые их подтверждают), интегрировать пилотное решение по вертикали.
Так-как тесты показали отличный результат — то есть, подтвердили все гипотезы, мы быстро провернули пилот — собрали вертикально интегрированный «скелет» для небольшого кусочка функционала. И приступили к основному кодированию – наполнению «скелета мясом».
Чем успешно и бодро занимались. До того прекрасного дня, когда в мастер-хранилище залили полный набор данных.
Запустили тест на этом наборе.
Через 2 минуты не отработал. Не отработал и через 5, 10, 15 минут. То есть, в нужные рамки не поместились. Но, с кем не бывает, надо будет подкрутить что-нибудь по мелочам и поместимся.
Но тест не отработал и через час. И даже через 2 часа оставалась надежда, что вот он отработает, и мы поищем, что надо подкрутить. Остатки надежды были даже через 5 часов. Но, через 10 часов, когда уходили домой, а тест все еще не отработал – надежды уже не было.
Беда была в том, что и на следующий день, когда пришли в офис – тест все еще старательно продолжал работать. В итоге прокрутился 30 часов, не стали дожидаться, выключили.
Катастрофа!
Проблему локализовали достаточно быстро.
Hazelcast – когда работал на небольшом объеме данных – на самом деле прокручивал все в памяти. А вот когда потребовалось скидывать данные на диск – производительность просела в тысячи раз.
Программирование было бы скучным и безвкусным занятием, если бы не начальство и обязательства сдавать готовый продукт. Так и нам, буквально через день, как получили полный набор данных – надо идти к начальству с отчетом, как прошел тест на продакшн-объемах.
Вот это очень серьезный и сложный выбор:
Чтобы понять, какие чувства при этом возникают – это можно только когда полностью вложиться в идею, пол-года воплощать задуманное, создавать продукт, который поможет коллегам решить огромный пласт проблем.
И вот так, отказаться от любимого творения – это очень сложно.
Это свойственно для всех людей – мы любим то, во что вложили много сил. Поэтому и тяжело слышать критику – надо осознанно прикладывать усилия, чтобы адекватно воспринимать обратную связь.
В общем, мы решили, что есть еще очень и очень много разных систем, которые можно использовать как Key-Value хранилище, и если Hazelcast не подошел, то уж что-нибудь точно подойдет. То есть, приняли решение рискнуть. К нашему оправданию можно сказать, что это был еще не «кровавый дедлайн» — в целом, еще оставался запас по времени, чтобы «съехать» на запасное решение.
На той встрече с начальством наш менеджер обозначил, что «тест показал, что на продакшн объемах система работает стабильно, не падает». Действительно, система работала стабильно.
До релиза 60 дней.
Чтобы найти замену для Hazelcast на роль Key-Value хранилища данных, мы составили список всех кандидатов – получился список из 31 продукта. Это все, что удалось нагуглить и узнать по знакомым. Дальше гугл выдавал какие-то совсем уж непристойные варианты, вроде курсовой работы какого-то студента.
Чтобы проверять кандидатов быстрее – подготовили небольшой тест, который за несколько минут запуска показывал производительность на нужных объемах. И работу распараллелили – каждый брал следующую систему из списка, настраивал, запускал тест, брал следующую.
Работали быстро, отщелкивали по несколько систем в день.
На 18-й системе стало понятно, что это бессмысленно. Под наш профиль нагрузки – ни одна из этих систем не заточена. В них много рюшек и реверансов, чтобы было удобно использовать, много красивых подходов к горизонтальному масштабированию – но нам это профита не дает никакого.
Нам нужна система, которая _быстро_ сохраняет по ключу объект на диск и быстро по ключу считывает.
Раз так – набрасываем алгоритм, как это можно реализовать. В целом, кажется достаточно реализуемым – если одновременно: а) принести в жертву объем, который будут данные занимать на диске, б) иметь приблизительно оценки по объему и характерным размерам данных в каждой таблице.
Что-то в стиле, выделять под объекты память (на диске) с запасом, кусками фиксированного максимального объема. Тогда с помощью таблиц указателей… и тд …
Повезло, что до этого не дошло.
Спасение пришло в виде RocksDB.
Это продукт от Facebook, который заточен под быстрое считывание и сохранение массива байт на диск. При этом, доступ к файлам предоставляет через интерфейс, который похож на Key-Value хранилище. Фактически, в качестве ключа – массив байт, в качестве значения – массив байт. Оптимизирован, чтобы эту работу делать быстро и надежно. Все. Если надо что-то более красивое и высоко-уровневое – прикручивайте сверху сами.
Ровно то, что нам надо!
RocksDB, прикрученный в роли Key-Value хранилища – вывел показатель целевого теста на уровень 5 часов. Это было далеко от 15 минут, но было сделано главное. Главное – было понимание, что происходит, понимание, что запись на диск идет максимально быстро, быстрее невозможно. На SSD, в рафинированных тестах, RocksDB выжимал 400Мб/сек, а этого было достаточно для нашей задачи. Задержки – где-то в нашем, в обвязочном коде.
В нашем коде, а это значит – справимся. Разберем на кусочки, но справимся.
У нас есть алгоритм и входные данные. Снимаем спектр входных данных, проводим подсчет: сколько каких действий система должна выполнить, как эти действия выражаются в run-time затратах JVM (присвоить значение переменной, войти в метод, создать объект, копировать массив байт и тд), плюс, сколько каких обращений к RocksDB следует провести.
По расчетам получается, что должны уложиться в 2 мин (примерно, как показывал тест для HashMap в самом начале, но это всего лишь совпадение – алгоритм с тех пор поменялся).
И все таки, тест работает 5 часов.
И вот, до релиза 30 дней.
Это особая дата – теперь свернуть будет нельзя – на запасной вариант перейти не успеем.
Конечно же, в этот день руководителя проекта вызывают к начальству. Вопрос тот же самый – успеваете, все в порядке?
Вот самый лучший способ описать эту ситуацию – расширенная титульная картинка к этой статье. То есть, начальству показана та часть картинки, которая вынесена в титул. А в реальности – вот так.
Хотя, в реальности, конечно же – нам было совсем не смешно. И сказать, что «Все классно!» — это возможно только для человека с очень сильным навыком самообладания.
Большое, огромное уважение к менеджеру, за то, что он поверил, доверился разработчикам.
Действительно, реально имеющийся код – показывает 5 часов. А теоретический расчет – показывает 2 минуты. Как такому можно поверить?
А вот возможно, если: модель сформулирована понятно, как считать – понятно, и какие значения подставлять – тоже понятно. То есть – то, что в реальности выполнение занимает больше времени – означает, что в реальности выполняется не совсем тот код, который мы рассчитываем там выполнять.
Центральная задача – найти в коде «балласт». То есть, какие-то действия выполняются в довесок к основному потоку создания итоговых данных.
Помчали. Юнит-тесты, функциональные композиции, дробление функций и локализация мест с непропорциональными затратами времени на выполнение. Много всего проделали.
Попутно сформулировали такие места, где можно серьезно подкрутить.
Например, сериализация. Сначала использовали стандартную java.io. Но если прикрутить Cryo, то в нашем кейсе получаем прирост в 2,5 раза по скорости сериализации и 3 раза сокращение объема сериализованных данных (а значит, в 3 раза меньше объем IO, который как раз и съедает основные ресурсы). Но, более подробно — это тема для отдельной, технической статьи.
А вот ключевой момент, или «где спрятался слон» — попробую описать одним абзацем.
Когда делаем get/set по ключу – в расчетах это проходило как 1 операция, затрагивает IO в объеме равном ключ + объект-значение (в сериализованном виде, разумеется).
Но что, если сам объект, на котором вызываем get/set – это Map, который тоже получаем по get/set с диска. Сколько в таком случае будет выполнено IO?
В наших расчетах эта особенность не учитывалась. То есть, считали, как 1 IO для ключ + объек-значение. А на деле?
Например, в Key-Value хранилище, по ключу key-1 находится объект obj-1 с типом Map, в котором под ключом key-2 надо сохранить некоторый объект obj-2. Вот здесь мы и считали, что операция потребует IO для key-2 + obj-2. Но в реальности, потребуется считать obj-1, провести с ним манипуляцию и отправить в IO: key-1 + obj-1. И если это Map в которой 1000 объектов, то расход IO будет примерно в 1000 раз больше. А если 10 000 объектов, то … Вот так и получили «балласт».
Когда проблема обозначена – как правило, решение очевидно.
В нашем случае это стала особая структура для манипуляций внутри вложенных Map. То есть, такая Key-Value, которая для get/set принимает сразу два ключа, которые следует применить последовательно: key-1, key-2 – то есть, для первого уровня и для вложенного. Как реализовать такую структуру – подробно расскажу с удовольствием, но опять же, в отдельной, технической статье.
Здесь, из этого эпизода я подчеркну и продвигаю такую особенность: предельно-детально сформулированная проблема – это и есть хорошее решение.
В этой статье я постарался показать организационные моменты и ловушки, которые могут возникнуть. Такие ловушки очень хорошо видны «сбоку» или по прошествии времени, но очень легко в них попасть, когда впервые оказываешься с ними рядом. Надеюсь, кому-то такое описание запомнится, и в нужный момент сработает напоминание «где-то про что-то такое уже слышал».
И, главное – теперь, когда рассказано все про процесс, про психологические моменты, про организационные. Теперь, когда дано представление, под какие задачи и в каких условиях система создавалась. Теперь – можно и следует рассказать о системе с технической стороны – что это за математическая модель такая, и на какие ухищрения в коде мы пошли, и до каких нестандартных решений додумались.
Об этом – в следующей статье.
А пока – Happy New Code!
Сегодня я хочу продолжить и рассказать про психологически наиболее напряженный момент в том проекте, про который первые две статьи – когда итог проекта определяли не столько технические навыки команды, сколько уверенность в своих расчетах и готовность идти до конца.
Скажу сразу – я считаю, что довести проект до такого напряженного момента – это ошибка намного большая, чем любой героизм по вытягиванию проекта из такой … проблемы.
Но, этот опыт я не скрываю и охотно делюсь им – потому, что считаю:
- именно проблемные места – это точки роста
- наибольшие проблемы «прилетают» именно оттуда, откуда не ждешь
Сочетание этих пунктов – просто обязывает поделиться прекрасным опытом «как заработать переплет на ровном месте». Но, надо отметить, подобная ситуация – является исключительной в компании Спортмастер. То есть, исключено, что такая ситуация повторится – планирование и определение ответственности сейчас – совершенно на другом уровне.
Итак, кажется, достаточно вступления, если готовы – добро пожаловать под кат.
2017 год, июнь. Мы дорабатываем админку. Админка – это не только набор формочек и таблиц в web-интерфейсе – введенные значения нужно склеить с десятками других данных, которые получаем из сторонних систем. Плюс, каким-то образом преобразовать и, в итоге – отправить потребителям (главный из которых – ElasticSearch сайта Спортмастер).
Основная сложность как раз в том, чтобы преобразовать и отправить. А именно:
- поставлять нужно данные в виде json, которые весят и по 100Кб, а отдельные выскакивают за 10Мб (развертка по наличию и критериям доставки товара по магазинам)
- встречаются json со структурой, которая имеет рекурсивные вложения любого уровня вложенности (например, меню внутри пункта меню, в котором опять пункты с меню и прочее)
- итоговая постановка не утверждена и постоянно меняется (например, работа с товарами по Моделям сменяется подходом, когда работаем по Цвето-Моделям). Постоянно – это несколько раз в неделю, с пиковым показателем 2 раза в день в течение недели.
Если первые 2 пункта – чисто технические, и продиктованы самой задачей, то вот с 3-м пунктом, конечно же, надо разбираться организационно. Но, реальный мир далек от идеального, так что работаем с тем что есть.
А именно – разобрались с тем, как быстро клепать web-формы и их объекты на стороне сервера.
Один человек из команды назначался на роль профессионального «формо-шлёпа» и с помощью подготовленных web-компонент выкатывал демку для ui быстрее, чем аналитики правили рисунки этого ui.
А вот с тем, чтобы поменять схемы трансформаций – здесь возникала сложность.
Сначала мы пошли привычным путем – проводить трансформацию в sql-запросе к Oracle. В команде был специалист по DB. Он продержался до момента, когда запрос представлял из себя 2 страницы сплошного sql-текста. Мог бы продолжать и дальше, но когда приходили изменения от аналитиков – объективно, самое сложное было – найти то место, в которое внести правки.
Аналитики выражали правила на схемах, которые хоть и были нарисованы в чем-то отстраненном от программного кода (что-то из visio/draw.io/gliffy), но были так похожи на квадратики и стрелочки в ETL-системах (например, Pentaho Kettle, который в то время как раз использовали для поставки данных на сайт Спортмастер). Вот если бы у нас был не SQL-запрос, а ETL-схема! Тогда постановка и решение были бы топологически одинаково выражены, а значит – правка кода могла бы занимать времени столько же, что и правка постановки!
Но с ETL-системами есть другая сложность. Тот же Pentaho Kettle – отлично подходит, когда требуется создать новый индекс в ElasticSearch, в который записать все данные, склеенные из нескольких источников (remark: на самом деле, именно Pentaho Kettle – подходит не очень, т.к. в трансформациях использует javascript не связанный с java-классами, через которые к данным обращается потребитель – из-за этого можно записать то, что потом не получится превратить в нужные pojo-объекты. Но это отдельная тема, в стороне от основного хода статьи).
Но что делать, когда в админке пользователь поправил одно поле в одном документе? Для поставки этого изменения в ElasticSearch сайта Спортмастер – не создавать же новый индекс, в который залить все документы этого типа и, в том числе – обновленный!
Хотелось, чтобы, когда изменился один объект во входных данных – то в ElasticSearch сайта отправить обновление только для соответствующего выходного документа.
Ладно сам входной документ, но ведь он, по схеме трансформаций – мог через join быть прикреплен к документам другого типа! А значит, надо анализировать схему трансформаций и вычислять, какие именно выходные документы будут задеты изменением данных в источниках.
Поиск коробочных продуктов для решения такой задачи – не привел ни к чему. Не нашли.
А когда отчаялись найти – прикинули, а как это должно работать внутри, а как это можно сделать?
Идея возникла буквально сразу.
Если итоговую ETL можно разбить на составные части, каждая из которых имеет определенный тип из конечного набора (например, filter, join и т.д.), тогда, возможно – будет достаточно создать такой же конечный набор специальных узлов, которые соответствуют исходным, но с тем отличием, что работают не с самими данными, а с их изменением?
Очень подробно, с примерами и ключевыми моментами в реализации, наше решение – я хочу осветить в отдельной статье. Чтобы разобраться с опорными позициями – это потребует серьезного погружения, способности мыслить абстрактно и полагаться на то, что еще не проявлено. Действительно, это будет интересно именно с математической точки зрения и интересно только тем хабровчанам, кто интересуется техническими деталями.
Здесь скажу только, что мы создали математическую модель, в которой описали 7 типов узлов и показали, что эта система является полной – то есть, с помощью этих 7 типов узлов и соединений между ними – можно выразить любую схему трансформации данных. В основе реализации – активно используется получение и запись данных по ключу (именно по ключу, без дополнительных условий).
Таким образом, наше решение обладало сильной стороной в отношении всех вводных сложностей:
- данные нужно поставлять в виде json –> мы работаем с pojo-объектами (plain old java object, если кто не застал времена, когда такое обозначение было в ходу), которые легко перегнать в json
- встречаются json со структурой, которая имеет рекурсивные вложения любого уровня вложенности –> опять же, pojo (главное, что нет циклов, а сколько уровней вложенности – не важно, тк легко обрабатываем в java через рекурсию)
- итоговая постановка постоянно меняется –> отлично, тк мы меняем схему трансформации быстрее, чем аналитики оформляют (в схемах) пожелания к экспериментам
Из рискованных моментов, только один – решение пишем с нуля, самостоятельно.
Собственно, ловушки не заставили себя ждать.
Особый момент N1. Ловушка. «Хорошо экстраполируем»
Еще один сюрприз организационного характера состоял в том, что одновременно с нашей разработкой шел переход основного мастер-хранилища на новую версию, и при этом менялся формат, в котором данные это хранилище предоставляет. И хорошо было бы, чтоб наша система – сразу работала с новым хранилищем, а не со старым. Вот только, новое хранилище еще не готово. Но зато, структуры данных известны и нам могут выдать демо-стенд, на который зальют небольшое количество связанных данных. Идет?
Вот в продуктовом подходе, при работе с потоком поставки ценности, всем оптимистам однозначно вколачивается предупреждение: есть блокер -> задачу в работу не-бе-рем, точка.
Но тогда, такая зависимость даже подозрений не вызвала. Действительно, мы были в эйфории от успехов с прототипом Дельта-процессора – системы для обработки данных по дельтам (реализация математической модели, когда по схеме трансформации вычисляются изменения в выходных данных, как ответ на изменение данных на входе).
Среди всех схем трансформаций, одна была наиболее важной. Кроме того, что сама схема была самой большой и сложной, так еще к выполнению трансформации по этой схеме было жесткое требование – ограничение по времени выполнения на полном объеме данных.
Так, трансформация должна выполняться 15 минут и ни секундой дольше. Главные входные данные – таблица с 5,5 млн записей. На этапе разработки таблица еще не заполнена. Точнее, заполнена небольшим, тестовым набором данных в количестве 10 тысяч строк.
Что ж, приступаем. В первой реализации Дельта-процессор работал на HashMap в роли Key-Value хранилища (напомню, нам требуется очень много считывать и записывать объекты по ключам). Разумеется, что на продакшн-объемах, в памяти все промежуточные объекты не поместятся – поэтому вместо HashMap мы переходим на Hazelcast.
Почему именно Hazelcast – так потому, что этот продукт был знаком, использовался в backend к сайту Спортмастера. Плюс, это распределенная система и, как нам казалось – если друг что-то будет не так с производительностью – добавим еще инстансов на парочку машин и вопрос решен. В крайнем случае – на десяток машин. Горизонтальное масштабирование и все дела.
И вот, мы запускаем наш Дельта-процессор для целевой трансформации. Отрабатывает практически моментально. Это и понятно – данных то всего 10 тысяч вместо 5,5 млн. Поэтому измеренное время умножаем на 550, и получаем результат: что-то около 2 минут. Отлично! Фактически – победа!
Это было в самом начале работ по проекту – как раз, когда надо определиться с архитектурой, подтвердить гипотезы (провести тесты которые их подтверждают), интегрировать пилотное решение по вертикали.
Так-как тесты показали отличный результат — то есть, подтвердили все гипотезы, мы быстро провернули пилот — собрали вертикально интегрированный «скелет» для небольшого кусочка функционала. И приступили к основному кодированию – наполнению «скелета мясом».
Чем успешно и бодро занимались. До того прекрасного дня, когда в мастер-хранилище залили полный набор данных.
Запустили тест на этом наборе.
Через 2 минуты не отработал. Не отработал и через 5, 10, 15 минут. То есть, в нужные рамки не поместились. Но, с кем не бывает, надо будет подкрутить что-нибудь по мелочам и поместимся.
Но тест не отработал и через час. И даже через 2 часа оставалась надежда, что вот он отработает, и мы поищем, что надо подкрутить. Остатки надежды были даже через 5 часов. Но, через 10 часов, когда уходили домой, а тест все еще не отработал – надежды уже не было.
Беда была в том, что и на следующий день, когда пришли в офис – тест все еще старательно продолжал работать. В итоге прокрутился 30 часов, не стали дожидаться, выключили.
Катастрофа!
Проблему локализовали достаточно быстро.
Hazelcast – когда работал на небольшом объеме данных – на самом деле прокручивал все в памяти. А вот когда потребовалось скидывать данные на диск – производительность просела в тысячи раз.
Программирование было бы скучным и безвкусным занятием, если бы не начальство и обязательства сдавать готовый продукт. Так и нам, буквально через день, как получили полный набор данных – надо идти к начальству с отчетом, как прошел тест на продакшн-объемах.
Вот это очень серьезный и сложный выбор:
- сказать «как есть» = отказаться от проекта
- сказать «как хотелось бы» = рисковать, тк, не известно, сможем ли проблему исправить
Чтобы понять, какие чувства при этом возникают – это можно только когда полностью вложиться в идею, пол-года воплощать задуманное, создавать продукт, который поможет коллегам решить огромный пласт проблем.
И вот так, отказаться от любимого творения – это очень сложно.
Это свойственно для всех людей – мы любим то, во что вложили много сил. Поэтому и тяжело слышать критику – надо осознанно прикладывать усилия, чтобы адекватно воспринимать обратную связь.
В общем, мы решили, что есть еще очень и очень много разных систем, которые можно использовать как Key-Value хранилище, и если Hazelcast не подошел, то уж что-нибудь точно подойдет. То есть, приняли решение рискнуть. К нашему оправданию можно сказать, что это был еще не «кровавый дедлайн» — в целом, еще оставался запас по времени, чтобы «съехать» на запасное решение.
На той встрече с начальством наш менеджер обозначил, что «тест показал, что на продакшн объемах система работает стабильно, не падает». Действительно, система работала стабильно.
До релиза 60 дней.
Особый момент N2. Не ловушка, но и не открытие. «Меньше – значит больше»
Чтобы найти замену для Hazelcast на роль Key-Value хранилища данных, мы составили список всех кандидатов – получился список из 31 продукта. Это все, что удалось нагуглить и узнать по знакомым. Дальше гугл выдавал какие-то совсем уж непристойные варианты, вроде курсовой работы какого-то студента.
Чтобы проверять кандидатов быстрее – подготовили небольшой тест, который за несколько минут запуска показывал производительность на нужных объемах. И работу распараллелили – каждый брал следующую систему из списка, настраивал, запускал тест, брал следующую.
Работали быстро, отщелкивали по несколько систем в день.
На 18-й системе стало понятно, что это бессмысленно. Под наш профиль нагрузки – ни одна из этих систем не заточена. В них много рюшек и реверансов, чтобы было удобно использовать, много красивых подходов к горизонтальному масштабированию – но нам это профита не дает никакого.
Нам нужна система, которая _быстро_ сохраняет по ключу объект на диск и быстро по ключу считывает.
Раз так – набрасываем алгоритм, как это можно реализовать. В целом, кажется достаточно реализуемым – если одновременно: а) принести в жертву объем, который будут данные занимать на диске, б) иметь приблизительно оценки по объему и характерным размерам данных в каждой таблице.
Что-то в стиле, выделять под объекты память (на диске) с запасом, кусками фиксированного максимального объема. Тогда с помощью таблиц указателей… и тд …
Повезло, что до этого не дошло.
Спасение пришло в виде RocksDB.
Это продукт от Facebook, который заточен под быстрое считывание и сохранение массива байт на диск. При этом, доступ к файлам предоставляет через интерфейс, который похож на Key-Value хранилище. Фактически, в качестве ключа – массив байт, в качестве значения – массив байт. Оптимизирован, чтобы эту работу делать быстро и надежно. Все. Если надо что-то более красивое и высоко-уровневое – прикручивайте сверху сами.
Ровно то, что нам надо!
RocksDB, прикрученный в роли Key-Value хранилища – вывел показатель целевого теста на уровень 5 часов. Это было далеко от 15 минут, но было сделано главное. Главное – было понимание, что происходит, понимание, что запись на диск идет максимально быстро, быстрее невозможно. На SSD, в рафинированных тестах, RocksDB выжимал 400Мб/сек, а этого было достаточно для нашей задачи. Задержки – где-то в нашем, в обвязочном коде.
В нашем коде, а это значит – справимся. Разберем на кусочки, но справимся.
Особый момент N3. Опора. «Теоретический расчет»
У нас есть алгоритм и входные данные. Снимаем спектр входных данных, проводим подсчет: сколько каких действий система должна выполнить, как эти действия выражаются в run-time затратах JVM (присвоить значение переменной, войти в метод, создать объект, копировать массив байт и тд), плюс, сколько каких обращений к RocksDB следует провести.
По расчетам получается, что должны уложиться в 2 мин (примерно, как показывал тест для HashMap в самом начале, но это всего лишь совпадение – алгоритм с тех пор поменялся).
И все таки, тест работает 5 часов.
И вот, до релиза 30 дней.
Это особая дата – теперь свернуть будет нельзя – на запасной вариант перейти не успеем.
Конечно же, в этот день руководителя проекта вызывают к начальству. Вопрос тот же самый – успеваете, все в порядке?
Вот самый лучший способ описать эту ситуацию – расширенная титульная картинка к этой статье. То есть, начальству показана та часть картинки, которая вынесена в титул. А в реальности – вот так.
Хотя, в реальности, конечно же – нам было совсем не смешно. И сказать, что «Все классно!» — это возможно только для человека с очень сильным навыком самообладания.
Большое, огромное уважение к менеджеру, за то, что он поверил, доверился разработчикам.
Действительно, реально имеющийся код – показывает 5 часов. А теоретический расчет – показывает 2 минуты. Как такому можно поверить?
А вот возможно, если: модель сформулирована понятно, как считать – понятно, и какие значения подставлять – тоже понятно. То есть – то, что в реальности выполнение занимает больше времени – означает, что в реальности выполняется не совсем тот код, который мы рассчитываем там выполнять.
Центральная задача – найти в коде «балласт». То есть, какие-то действия выполняются в довесок к основному потоку создания итоговых данных.
Помчали. Юнит-тесты, функциональные композиции, дробление функций и локализация мест с непропорциональными затратами времени на выполнение. Много всего проделали.
Попутно сформулировали такие места, где можно серьезно подкрутить.
Например, сериализация. Сначала использовали стандартную java.io. Но если прикрутить Cryo, то в нашем кейсе получаем прирост в 2,5 раза по скорости сериализации и 3 раза сокращение объема сериализованных данных (а значит, в 3 раза меньше объем IO, который как раз и съедает основные ресурсы). Но, более подробно — это тема для отдельной, технической статьи.
А вот ключевой момент, или «где спрятался слон» — попробую описать одним абзацем.
Особый момент 4. Прием для поиска решения. «Проблема = решение»
Когда делаем get/set по ключу – в расчетах это проходило как 1 операция, затрагивает IO в объеме равном ключ + объект-значение (в сериализованном виде, разумеется).
Но что, если сам объект, на котором вызываем get/set – это Map, который тоже получаем по get/set с диска. Сколько в таком случае будет выполнено IO?
В наших расчетах эта особенность не учитывалась. То есть, считали, как 1 IO для ключ + объек-значение. А на деле?
Например, в Key-Value хранилище, по ключу key-1 находится объект obj-1 с типом Map, в котором под ключом key-2 надо сохранить некоторый объект obj-2. Вот здесь мы и считали, что операция потребует IO для key-2 + obj-2. Но в реальности, потребуется считать obj-1, провести с ним манипуляцию и отправить в IO: key-1 + obj-1. И если это Map в которой 1000 объектов, то расход IO будет примерно в 1000 раз больше. А если 10 000 объектов, то … Вот так и получили «балласт».
Когда проблема обозначена – как правило, решение очевидно.
В нашем случае это стала особая структура для манипуляций внутри вложенных Map. То есть, такая Key-Value, которая для get/set принимает сразу два ключа, которые следует применить последовательно: key-1, key-2 – то есть, для первого уровня и для вложенного. Как реализовать такую структуру – подробно расскажу с удовольствием, но опять же, в отдельной, технической статье.
Здесь, из этого эпизода я подчеркну и продвигаю такую особенность: предельно-детально сформулированная проблема – это и есть хорошее решение.
Завершение
В этой статье я постарался показать организационные моменты и ловушки, которые могут возникнуть. Такие ловушки очень хорошо видны «сбоку» или по прошествии времени, но очень легко в них попасть, когда впервые оказываешься с ними рядом. Надеюсь, кому-то такое описание запомнится, и в нужный момент сработает напоминание «где-то про что-то такое уже слышал».
И, главное – теперь, когда рассказано все про процесс, про психологические моменты, про организационные. Теперь, когда дано представление, под какие задачи и в каких условиях система создавалась. Теперь – можно и следует рассказать о системе с технической стороны – что это за математическая модель такая, и на какие ухищрения в коде мы пошли, и до каких нестандартных решений додумались.
Об этом – в следующей статье.
А пока – Happy New Code!