Привет, Хабр!

Эта маленькая заметка родилась в процессе обсуждения статьи «Распределенные монолиты...», а поскольку тема требует дальнейшего раздумья — я решил зафиксировать ее у себя в блоге. Автор статьи фактически описывает распределенную базу данных, доказывая, что единственнo правильной структурой хранения в ней является Журнал Событий. Аргументы приблизительно следующие:

  • Поскольку событие всегда локализовано в пространстве / времени, оно может хранить все данные в себе самом (иногда в виде ссылок на более ранние события), что делает событие сериализуемым, увеличивает локальность, уменьшает связность и т.д.
  • Событие, однажды случившись, больше не изменяется (любые уточнения оформляются другими событиями), что уменьшает репликационный трафик.
  • Формат хранения события можно более-ли-менее унифицировать, и отвязать от конкретной предметной области.
  • Журналы событий можно относительно безболезненно разделять / сливать, можно хранить разные типы событий на разных нодах, то есть по сути мы говорим о распределенной БД.
  • События упрорядочены по времени, и эта последовательность отражает также причинно-следственную связь (текущее событие не может ссылаться на более позднее).
  • При записи события не требуется транзакционно обновлять другие данные (на самом деле требуется, но в ограниченном числе случаев, например, баланс абонента в биллинговой системе должен быть мгновенно-актуальным, необходимо обновлять счетчики ссылок, и т.д.).
  • Отчетность может быть построена непосредственно по журналу событий, без необходимости преобразовывать данные в нормализованный вид.

Относительно последнего пункта — многочисленные тесты производительности (в том числе и в моем блоге) показывают, что на современном железе обработка миллиарда событий (фактов) однопроходным алгоритмом с кратковременной памятью занимает минуты, что вполне приемлемо для отчетности. А поскольку обработка событий разных типов, не связанных ссылками, легко параллелится — время построения отчетов можно свести к десяткам секунд, при этом не имея накладных расходов на нормализацию данных / индексирование / сбор статистики / отладку и хинтирование запросов — как это происходит в обычных РСУБД. Поэтому построение отчетности на основе таких данных меня не пугает. Однако, рассмотрим проблему шире.

Типичное бизнес-приложение можно представить в виде цепочки трансформации данных:
«входные данные => модель => выходные данные». Любая схема хранения — это компромисс между тремя крайностями:

  • Храним данные в формате выхода. Так работают разнообразные витрины и OLAP, где важно время отклика. Минусы известны — избыточная потребность в памяти и низкая скорость обновления (как правило, пакетного).
  • Храним данные в формате модели предметной области, упрощая таким образом алгоритмы расчетов. Недостатков куча — требуется двойная трансформация данных (от входа к модели, и от модели к выходу), возрастают транзакционные издержки, трудно обеспечить распределенное хранение, изменение схемы стоит дорого и т.д. Тем не менее, это самый популярный вариант.
  • Храним данные в формате входа (собственно то, что и предлагает автор статьи — журнал событий). В данном случае транзакционные издержки минимальны, данные легко разделяются и сливаются, простая, понятная и стабильная схема хранения. Профит! Правда, придется заново учиться программировать.

Применительно к области моих интересов (корпоративные информационные системы) событие — это первичный документ, а справочник можно рассматривать как более раннее событие. Структуру таблиц типовой западной ERP я уже описывал в статье «NoERP или новый взгляд...», где предложил упразднить избыточную нормализацию данных (за исключением регистров мгновенных остатков), а большинство расчетов / отчетов переписать на однопроходные циклы, в которых будут обрабатываться непосредственно первичные документы. Аргументы повторять не буду, все изложено в статье, но предложенная мной схема практически совпала с тезисом автора первой статьи, а именно, что журнал событий — это и есть данные. Приятно, когда в этом направлении думает кто-то еще.

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

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

Собственно, на уровне идеи это пока все, я по прежнему надеюсь на конкретный проект (например, на базе кассовых чеков или фактур), и, если такая возможность представится, отчитаюсь о результатах.

Комментарии (25)


  1. msin
    29.05.2019 16:36

    На мой взгляд слишком революционный подход…
    Тем более что полная история событий не заменит первичные данные (они же предметные).
    Простой пример: есть заказчик, есть его заказ и мы начинаем производство по какой-то позиции заказа. Для этого кладовщик выдал материал для выполнения операций.
    Выдача материала — это событие, без сомнения… Но это так же и расходная накладная, которая связана с производственным заданием и позицией заказа.
    Если стоит цель точного учета запасов — без проблем, будем считать события по приходу и расходу и получим текущий учетный запас… А, к примеру, при работе по ГосОборонЗаказу материал закупается отдельно (по предоплате по ГОЗу), и списывается строго под ГОЗ… тут событиями не обойтись, придется в каждом событии делать ссылки на разные первичные документы (заказчик, заказ, позиция заказа и т.д.).
    События, по сути, это создание нового первичного документа или изменения статуса существующего документа… А нужно ли иметь историю всех изменений статусов документов?
    Конечно, это может быть полезно — вести полную историю события в привязке к пользователям (чтобы найти того, кто накосячил), но для общего управления это не нужно.
    Вот есть позиция заказа, она в статусе частично отгружена, из 10 изделий уже отгружено 5… чтобы найти историю отгрузок, достаточно поднять накладные по этому заказу. Но для управления и планирования (ERP) история отгрузок не нужна, достаточно знать, что нужно отгрузить еще 5 штук и крайний строк отгрузки. Этого хватит для принятия всех управленческих решений…
    И в итоге от предметной области никуда не деться, просто мы добавляем сюда полную историю создания и изменения всех статусов…


    1. epishman Автор
      29.05.2019 18:16

      В случае с ГОЗ-ом — это просто отдельная аналитика в событииях «приход» и «расход», либо разные первичные документы (в цикле мы все-равно идем по всем типам в хронологическом порядке).

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

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


      1. msin
        29.05.2019 19:55

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

        Пост в хабах ERP-системы… возможно вы опрашиваете 10000 IoT устройств и тогда хватит одних событий. Но если нужно управлять ресурсами предприятия, то события это просто лог изменений в БД без ссылок на таблицы, в которых проходят эти изменения


        1. epishman Автор
          30.05.2019 04:29

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


  1. Yo1
    29.05.2019 17:21

    по сути идея предлагает сделать недоделанный мап-редюс. в мап-редюсе хотя бы в параллель все читается.
    подход уже успел взлететь и угаснуть. на практике отчет пойдет читать сообщения о транзакцияй их «внезапно» миллиард, в память не влезает и привед.
    сейчас модно в кафку такие вот сообщения писать и от туда уже чем-то типа спарк вычитывать, находу в памяти агрегируя.


    1. epishman Автор
      29.05.2019 18:07

      Именно map/reduce с параллелизмом!

      У меня миллиард записей на Rust на 4-х ядерном ноутбуке считает 10 минут. Просто надо уметь писать, но SQL тоже нужно уметь писать, я в свое время на Axapta таких алгоритмов насмотрелся, уж лучше цикл.
      PS
      Буду благодарен за информацию — где подход взлетал, и почему угас?


      1. Yo1
        29.05.2019 19:44
        +1

        просто надо опыт хоть немного иметь. и не на ноутбуке. может с лайками фейсбука это работает, но в серьезных бизнес задачах ентерпрайза такое работает хреново. во первых вся эта дребедень зависит от того что ты выберешь ключом. выбрал ты какой-нить номер клиента, все здорово. целый месяц. но на второй тебе говорят, слушай. у нас тут полно клиентов на одном адресе… ты еще не переписал все заново, а уже прилетает следующее маааленький вопрос от юристов: а что значит «Событие, однажды случившись, больше не изменяется»? вы может слышали, но GDPR требует как минимум анонимизировать всю историю…


        1. epishman Автор
          30.05.2019 04:30

          Почитайте первую мою статью, там больше конкретики, я отталкивался от Axapta, но в принципе структура таблиц у SAP и Oracle похожая.


          1. Yo1
            30.05.2019 10:29

            не увидел ничего там интересного. говорю, мы примерно такое налабали лет 5 назад. «сообщения» это были avro объекты в parquet файликах. теперь самоотверженно выпиливаем. не работает.
            представь что тебе пришло требование теперь группировать по компаниям, зарегистрированным на одном адресе. все, с твоей структурой в один проход теперь ты нихера не посчитаешь.
            +GDPR


            1. epishman Автор
              30.05.2019 12:28

              Про Регламент защиты ничего не скажу, некомпетентен, а задачи группировки (pivot table) решаются элементарно в один проход с минимальным расходом памяти и многопоточно (если конечно формулы агрегации аддитивные). Вы в другом правы — при каждом изменении нужно лопатить программный код, так как на SQL — заменил пару слов в запросе — точно не получится. Тут либо привыкать надо и свой фреймворк делать, либо платить за SQL и все прилагающееся — память, диски, админа.


              1. Yo1
                30.05.2019 13:48

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


  1. tonad
    29.05.2019 18:08

    Мой абстрактный урановый конь в вакууме.
    Допустим есть сервис, который мне дает утром текущую погоду, и потом шлет раз в минуту обновления, например +1 или -2 градуса.
    Я записываю событие — получил погоду 10 градусов.
    Далее события — изменение +2 градуса,
    далее — изменение -1 градус
    и т.д.
    В конце дня записываю в отчеты «погода на конец дня 11 градусов»
    Раз в неделю, я делаю верификацию, и возможны ситуации, что я пропустил событие, или оно было мной сохранено с ошибкой(возможно с ошибкой прислали).
    Как будет корректироваться событийный журнал погоды, что бы в итоге мой журнал совпадал с журналом вендора?


    1. epishman Автор
      29.05.2019 18:10

      Фулскан с определенного горизонта, на котором считаем что расхождений нет, непонятно в чем вопрос.


      1. tonad
        29.05.2019 19:03

        Вопрос в том, что рушится идеология только вставки новых событий, в том числе, для коррекции.


        1. epishman Автор
          30.05.2019 04:31

          Если Вы пропустили событие — рушится вообще любая идеология, вы попытаетесь продать некупленное, и т.д.


  1. Varim
    29.05.2019 19:19

    Отдельно хочется сказать про способ организации связей внутри журнала событий — она обязательно должна быть двусторонней, то есть каждое событие должно хранить счетчик ссылок на себя. Это необходимо для реализации однопроходных алгоритмов — мы идем от старых событий к новым, при этом кэшируем в памяти каждое событие с ненулевым количеством входящих ссылок, и удаляем его из кэша только после обработки всех ссылающихся.
    Не понял зачем вообще нужен счетчик ссылок и/или двух сторонняя связь, для оптимизации потребления ОЗУ? Нельзя фильтровать события по ID или еще каким то полям что бы не всё пихать в ОЗУ?


    1. epishman Автор
      29.05.2019 21:44

      Документ ссылается на документ, который в свою очередь ссылается на документ… Если мы начнем дергать ссылки отдельными запросами (хоть и по индексу) — проиграем в производительности промышленным СУБД. А если все обрабатывается в одном цикле (за линейное время) — для распутывания клубка связей нам придется кэшировать все события, на которые кто-то еще ссылается.


  1. msin
    29.05.2019 20:07

    Кстати, не понял, почему

    Храним данные в формате модели предметной области, упрощая таким образом алгоритмы расчетов. Недостатков куча — требуется двойная трансформация данных (от входа к модели, и от модели к выходу)

    Как раз ввод и хранение практически полностью совпадает с моделью (есть буквально несколько исключений — единицы закупки, хранения и расхода сырья и нормы времени на операцию).
    А вывод отличается только консолидацией по некоторым условиям, которая в подавляющем большинстве случаев делается на стороне БД.


    1. epishman Автор
      29.05.2019 21:46

      Да нифига, почитайте статью про NoSQL, там удвоение абстракций как минимум. В 1С-ERP то же самое — есть документы, есть регистры, есть проводки по счетам — еще сложнее цепочка.


  1. eefadeev
    30.05.2019 13:09

    Сама по себе идея «Храним только неизменную первичку, а всё остальное, при необходимости, считаем, прокручивая требуемые объёмы первички алгоритмами» — прекрасная идея. В том случае если вы являетесь счастливым обладателем неограниченных вычислительных мощностей безграничной производительности. К сожалению в реальной жизни это работает сначала хорошо, потом, терпимо, а потом практически перестаёт работать. Потому что данные накапливаются и с каждым днём (и даже часом) их становится всё больше и больше.

    И это только надводная часть айсберга…


    1. epishman Автор
      30.05.2019 14:12

      Как-то очень давно на оракле интернет-биллинг писал, там миллиарды записей в месяц, не так чтобы и много, но попытка использовать join накрылась — требовался серьезный тюнинг БД чтобы удержать расход памяти в приемлемых рамках. Пришлось делать через курсор одним проходом — из ресурсов кроме ядер ничего не нужно, память тратим только на буфер и кэш, линейное время и т.д. Вторая задача из недавнего — по дереву BOM посчитать плановую себестоимость продукции, зная цену сырья. Сначала мудрил с хитрым рекурсивным SQL, потом переделал на курсор + временную таблицу, получилось 2 полных прохода по дереву, опять же быстрее работало. Используя промышленную СУБД Вам нужно не меньше, а зачастую больше ресурсов. Хотя, согласен, использовать запросы вместо кодинга — это существенно проще, и программисты дешевле.


      1. eefadeev
        30.05.2019 14:21

        Вместо того, чтобы подумать над структурой и характером данных вы предлагаете писать тонны кода. Тоже, в общем вариант.
        Картинка с мужиком «Пишите код, б...!».jpg


        1. epishman Автор
          30.05.2019 14:37

          Я же понимаю, что серебряной пули не существует, речь о балансе между
          Cтруктурой и Алгоритмом. Во времена, когда писался SAP не было map/reduce, async/await, многопоточка писалась сложно, и естественно, пошли по пути засовывания всех промежуточных данных в модель. Сейчас средства программирования ушли далеко вперед, и мой мессидж всего лишь о том, что существенную часть данных сегодня дешевле посчитать на лету, чем хранить и обновлять распределенно-транзакционно.


  1. vazerdim
    30.05.2019 19:35

    Всю жизнь связан с системами ERP и также возникают мысли, то что сейчас у нас есть, это не совсем верное решение.
    Но оно работает в млн. компаний и разработано тоже не глупыми людьми.
    Я хорошо знаком с системой Dynamics Ax.

    Если взять и поразмышлять о будущем систем ERP, да и вообще о системах автоматизации производства, то я вижу это следующим образом. Все операции с системой — это своего рода процессы, нельзя просто создать строку в справочнике и т.п. документах, так как в любом изменении может потребоваться согласование или выстроена другая цепочка событий, которая может корректироваться в процессе жизни компании. Соответственно мы имеем множество процессов, большинство из которых очень короткие, грубо говоря блок сохранения в хранилище данных. Но все это отдельные workflow.

    Что касается управления всем этим, то мне видеться решением — бот который может понимать потребности сотрудника\администратора и предлагать нужный процесс для модификации, т.е. из системных и кастомных блоков собрать новый процесс или модифицировать существующий. Тем самым обслуживание становиться проще и есть возможность автоматически тестировать все процессы на предмет ошибок и выдавать эту информацию администратору.
    Само хранилище должно быть в виде key — value. Причем также хранить к каждому значению ссылку на метаданные этого значения, чтобы потом можно было запустить процесс по очистке тех же персональных данных т.е. ссылку мы оставляем, но само значение пустое. Тем самым у нас есть несколько хранилищ key-value.

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

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

    Опять же это только мысли в слух.


    1. epishman Автор
      30.05.2019 20:11

      Вы совершено правы — все изменения это процессы (то есть цепочки событий), и AI-боты уже на подходе (черт, сталкивался как-то, лучше бы их не было:)). Вопрос — как данные хранить. Сейчас у всех ERP центральная база, и вся распределенка — это полная репликация средствами СУБД, что стоит кучи денег, трафика, и усилий сисадмина. А жизнь требует реальной распределенки, когда часть решений могут приниматься на месте. Хранение событий прекрасно и легковесно распределяется. Я понятно не сектант, но даже уничтожение небольшой доли промежуточных данных (которые возможно вычислить на лету) — это уже прогресс. В том же дайнамиксе промежуточные остатки не храняться (а в 1С ежедневные срезы в базе лежат) — и никто в европе не умер. Про ненужность таблиц *trans я в первой статье уже писал, и так далее.
      PS
      Собственно, базы key/value и были ответом на потребность в распределенке, потому что в РСУБД с распределенкой не очень.