Pull to refresh
59
29
Стас Выщепан @gandjustas

Умею оптимизировать программы

Send message

Без технических деталей читать не интересно. Как Dev Rel могли бы это заранее понять.

если вы продали товара больше чем есть на складе, то вы можете известить юзера о том, что сроки доставки увеличатся.

Такой бред мог написать только тот, кто никогда не занимался интернет-магазинами.

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

Какие преимущества должна нести архитектура, чтобы оправдать подобные косяки?

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

Независимо от архитектуры программы товаровед может выставить товар на полку до того как его забьют в программу. Так что этот вопрос к архитектуре не относится никак.

А вот если у вас интернет-магазин, который торгует со склада, то вам нужно вести учет в реальном времени и эвентсорсинг поможет вам продать больше товара чем есть на складе. Закон о розничной торговле применим также в этом случае.

Нет, вы именно путаете.

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

Выделенное означает строгую согласованность. Потому что предыдущие остальные операции должны быть учтены на момент выполнения.

 В торговле сначала делаются движения, потом анализируются остатки и если они отрицательные, то транзакция откатывается.

Выделенное означает строгую согласованность. Потому что предыдущие остальные операции должны быть учтены на момент выполнения.

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

А как оно внутри устроено - через сохранение значения или "виртуальные" таблицы - дело десятое. Материализованные представления даже границу этих различий стирают.

Вы бред пишите.

1с всегда использовала строгую консистентность. Пересчет регистров всегда был транзакции при проведении документов.

Вы все ещё путаете лог изменений и эвенсорсинг.

В реальности строгая консистентность описана в законе. Например розничная торговля: при оплате покупателем строго консистентно возникает обязанность передать товар.

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

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

Почему же нет? При строгой консистентность Я могу явно делать -1 и откатывать если результат отрицательный.

Но это неважно. Важно как не уйти в минус. Что эвентсорсинг предлагает для решения проблемы?

Что за бред вы несете?

Лог операций это не эвентсорсинг. Лог операций при строгой консистентности изменений применяли за много (тысяч?) лет до изобретения эвентсорсинга.

Точно также, но есть нюанс, который вы как обычно игнорируете.

Пользователь видит не логи, а суммую. Сумма получается через неопредлеленное время после записи в лог.

У вам обязательно случится ситуация, когда вы записали в лог -1, но сумма не успела обновиться, а в это время другой пользователь сделал действие (оплатил товар) и в лог еще раз записали -1. Сумма пересчиталась и стала отрицательной, а такого случаться не должно.

Вы осознаете что такая проблема существует? Я что-то уже начал сомневаться в этом.

Когда вы используете строгую консистентность у вас сумма перечситывается сразу и в минус она никогда не уйдет.

Пример с твиттером это пример с данными которые можно потерять. Лайкам и репостам не требуется вообще никакая консистентность. Если ваш лайк никто не увидит, то вы об этом не узнаете.

Да вы что?

Списывать (резервировать) единицу товара надо в момент оплаты. Иначе до физического выбытия вы её ещё раз продадите.

Как это можно сделать в es?

Вы подменяете понятия.

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

Ключевая особенность эвентсорсинга это безусловная "отложенная согласованность" она же "согласованность в конечном счете". Как я уже приводил многократно примеры эта "отложенная согласованность" с большим успехом позволяет терять данные и приводит к ошибкам.

Вы сами говорите что всегда надо знать сколько товара на складе. Теперь объясните как гарантированно не продать больше чем есть на складе при отложенной согласованности.

Тут как анекдоте про нюанс. Он всегда есть.

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

Если у вас "консистентность в конечном счете", то не можете. Вы должна подождать пока некоторый фоновый процесс сделает вам счет. Сколько ждать - в общем случае неизвестно. Если за время ожидания поступят еще звонки, то полученный в любой момент счет будет неактуальным.

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

Вы думаете в 1с какая-то магия происходит? Там такие же запросы в базу и тоже часто встречается кривой код конфигурации, который может данные потерять.

у вас не будет транзакций концептуально

В реальном мире транзакции существуют. Любая сделка это транзакция. Обязательства возникают одновременно у множества контрагентов.

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

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

Один из таких алгоритмов, который применяется практически во всех СУБД, это Write Ahead Log (WAL). Мы сначала атомарной операцией записи сохраняем то, что мы хотим поменять, а потом уже меняем и если все ок, то помечаем данные меткой этой транзакции, а если нет, то транзакцию откатываем - просто не сохраняем изменения.

Я под этим подразумеваю возможность обновления множества сущностей в атомарно, допустим запись эвентов и обновление проекций в одной транзакции. 

У вас уже есть WAL. Даже если нет и вы нашли систему без WAL, вы его изобретаете в виде eventstore.

код обновления проекций, или там взаимодействия с другими сервисами, по-задумке, находится в другом месте и работает с задержкой

Как можно оплату с задержкой принять? Пользователь сформировал заказ, нажал оплатить, дальше что?

Я посмотрел ваш код, у вас там тест пинает многократно апи, пока заказ не станет в нужном состоянии. Но в реальности это не бездушная машина делает, а пользователь нажимает кнопки на сайте. Если он увидит, что после добавления товара в корзину количество не поменялось он уйдет с вашего сайта и никогда не вернется.

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

Не понимаю откуда взялся тут ACID, ACID он как бы про другое

ACID и сохраняемые транзакции это одно и то же. Не бывает транзакций без ACID, и не бывает ACID без транзакций. Это определяющие свойства.

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

Что значит "становится хрупким" ? Почему имеет тенденцию разрастаться? И почему на это должны влиять проекции? Можете привести пример кода?

Мой пример без эвенсорсинга:

// Этот эндпоинт вызывает робокасса или другой платежный сервис
// если он вернул ошибку, то деньги не списываются
public void Authorize(int orderId)
{
    var changed = db.Order
      .Where(o => o.Id == orderId && o.Status < OrderStatus.Paid )
      .ExecuteUpdate(s => s.SetProperty(o => o.Status, OrderStatus.Paid));
    if(changed != 1) throw new Exception();
}

// Этот эндпоинт вызывает клиент
// order.Status всегда содержит актуальный статус заказа
// клиент не сможет перейти к оплате 
// даже с помощью хаков не сможет оплатить еще раз
public Order Get(int orderId)
{
    return db.Order.First(o => o.Id == orderId);
}

Даже если вы можете работать с EF7 или не приучены писать ExecuteUpdate, то можно написать такой код:

public void Authorize(int orderId)
{
    var order = db.Order.First(o => o.Id == orderId && o.Status < OrderStatus.Paid);
    order.Status = OrderStatus.Paid;
    db.SaveChanges();
}

Если кто-то межу строками 3 и 5 поменяет статус заказа, то saveChanges отвалится из-за оптимистичной конкуренции и оплата не пройдет.

Что в этом примере "становится хрупким" ? Что будет разрастаться?

Как вы решите проблему двойной оплаты в своем коде?

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

Я вижу проблемы в вашем коде. Я вижу что если перевести код на простые чтение и запись базы, то проблем станет меньше. Поэтому и прошу привести более жизнеспособный пример, чем заказы и оплаты.

PS. Открыл ваше резюме и увидел, что вы занимались payment gateway в Додо. Там все так плохо как у вас в статье?

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

Пока не доказано обратное.

У вас два неявных предположения:

  • Вы предполагаете что проблемы в отсутствии транзакционности будут возникать редко.

  • Вы предполагаете что поддержание ACID гарантий сильно замедляет скорость разработки.

Оба предположения неверные.

В моей практике был один интересный случай. Я был тогда еще очень молод, только закончил универ, где на предпоследнем курсе мы изучали реляционные СУБД. К нам в контору устроился весьма опытный программист, много чего написал в своей жизни и умел реально быстро делать.

И как-то раз мы с товарищем, с которым учились вместе, посмотрели его код. Там было так: запрос "Получить значение А", небольшие вычисления со значением А, которые занимали едины миллисекунд, запрос "Записать в А новое значение". Запросы были не в транзакции.

Мы с товарищем сразу почувствовали неладное. Но опытный программист сказал нам: "там задержка между запросами минимальна, не успеет другой запрос туда вклиниться, я всегда так делал. Если написать транзакцию, то будут дедлоки, а так проблем никогда не было".

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

В итоге в код все равно были добавлены транзакции и updlock. Но если бы это было сделано сразу, то не было бы ущерба репутации и не было бы потрачено несколько часов работы саппорта.

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

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

Вопросы межсервисного взаимодействия к эвент-сорсингу не относятся

Платежный сервис он не ваш, это robocassa или что-то в этом роде. Он работает просто: вы передаете запрос, он показывает пользователю страницу, берет денные карты клиента, списывает деньги, делает веб-запрос (вполне себе идемпотентно) по указанному вами адресу, а клиента перенаправляет назад в ваш магазин.

Но даже если представить что сервис ваш, то это никак не помешает вам дважды принять оплату в эвент-сорсинг архитектуре.

Условно, все те же проблемы нужно решать как и без эвент-сорсинга — делать пессиместичные и дюрабельные блокировки, вызовы идемпотентными и прочее.

Чтобы делать пессимистичные и дюрабельные блокировки вам нужна согласованность write и read моделей (внезапно!)

Как идемпотентность вызовов поможет вам не потерять данные об оплате клиента в случае eventual consistency?

Вопрос не понял, уточните?

Ваш пример в статье такой, что если переписать его без эвенсорсинга на обычную РСУБД, то он станет работать не просто надежнее, но и быстрее. А аудит можно прикрутить отдельно.

Есть у вас пример, который не получится переписать без эвенсорсинга без потери ключевых характеристик?

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

Дочитал до пункта "когда не применять"

Когда необходима строгая согласованность между read и write моделями данных. В таком случае скорее всего не обойтись без транзакций и эвент-сорсинг теряет свои полезные свойства.

Когда у вас появляются деньги вам нужна строгая согласованность.

В вашем примере вы легко можете взять с клиента оплату за заказ дважды:

  1. Клиент жмет "оплатить" на странице заказа, в момент проведения транзакции связь ломается, клиент не получает ответа от платежного сервиса, но информация об оплате попадает в эвентстор.

  2. Далее клиент перезагружает страницу заказа, видит что статус заказа все еще "ожидает оплаты", так как трансформер не отработал, и оплачивает еще раз.

  3. Вы потом долго разбираетесь в эвентсторе откуда два раза изменение статуса с "не оплачен" на "оплачен" (кстати что делать если в эвентсторе оказались несовместимые события, как разруливается конкурентный доступ?)

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

Может быть у вас есть пример эвентсорсинга, который несет только полезные свойства, а не tradeoff с отрицательной суммой?

Information

Rating
269-th
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity

Specialization

Software Architect, Delivery Manager
Lead
C#
.NET Core
Entity Framework
ASP.Net
Database
High-loaded systems
Designing application architecture
Git
PostgreSQL
Docker