Обновить
28
Евгений Иванов@eivanov

Разработчик YDB

18
Подписчики
Отправить сообщение

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

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

Проблема в том, что Вася видит лишь часть изменений, что и создаёт неконсистентность. Если бы он видел только старые данные или только новые, то всё было бы хорошо.

Спасибо за интересное дополнение. Наверное, мы не очень чётко отразили это в тексте, но весь посыл статьи следующий:

  1. При простом шардировании теряются гарантии изоляции.

  2. Распределенные СУБД (сюда я бы отнёс любой форк постгреса, у которого serializable многошардовые транзакции) не имеют такой проблемы, но у них есть накладные расходы на реализацию распределенных транзакций и распределенного снепшота.

  3. Накладные расходы не так велики, когда датацентры находятся близко друг к другу.

Мы выбрали Citus в силу его популярности. Конечно, есть и другие решения. Но в первую очередь нам интересны решения из OSS и то, что реализовано, а не предложено, но застряло по каким-то причинам. Из мира распределенных СУБД взяли YDB по понятным причинам. Но выбор конкретной распределенной СУБД в данном случае никак не влияет на основную идею.

В посте была ссылка на описание наших распределенных транзакций. Там же рядом описание MVCC. Вот выжимка, которая отвечает на Ваш вопрос:

Реализация распределенных транзакций в YDB основана на идеях Calvin, в котором распределение детерминированных транзакций между несколькими участниками осуществляется с использованием предопределенного порядка выполнения транзакций.

...

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

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

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

Вот здесь в коммите есть полное объяснение: цель была отпускать физический поток под vthread (семафор позволяет это делать), а не блокировать в synchronized-блоке внутри c3p0 (от которого, правда, мы потом вообще отказались). Проблема была не при работе с самой СУБД, а в том, как реализован пул сессий к БД внутри c3p0.

Мне кажется, что это полностью аналогично обсуждению Citus: в итоге, получается СУБД на базе Postgres с другими характеристиками и гарантиями. Т.е. нельзя взять произвольное приложение, работающее с Postgres, и использовать его с этой фичей.

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

Полностью с вами согласен, что serializable по умолчанию - безопаснее. Аппелировал к сильному утверждению, что нет доказательств, что производительность "serializable" ощутимо хуже. С оговоркой "в большинстве случаев" было бы нормально. Ещё лучше было бы сделать оговорку, что при невысокой concurrency. Т.к. при высокой конкуренции, даже без блокировок, множественные ретраи могут эту производительность убить. Другие уровни изоляции именно для решения данной проблемы и были оставлены.

Справедливо, надо было именно так написать и глубже развить эту тему.

А может и будет. В примере выше нередко бывает так, что сериализация это дедлок и отстрел транзакции-жертвы секунд через 30. На три порядка выше целевого по p99, упс.

Хорошее замечание. В Postgres дефолтный таймаут для дедловок 1 секунда и это минимальное рекомендуемое значение. Я привык к тому, что в YDB оптимистичные блокировки и дедлоков нет: кто первый, тот и закомитился - остальные сразу получили ошибку "transaction locks invalidated". Поэтому в большинстве случаев останется достаточно времени на ретрай.

Если у вас сериализация на клиенте, то зачем вам транзакции в базе?

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

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

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

  1. Если надо учесть constraint, включающий в себя подобный широкий диапазон строк, то обычно не так важно, где произойдет сериализация: в базе или на стороне клиента (особенно когда ping небольшой).

  2. Как часто транзакции, которые требуют сериализацию, пересекаются по диапазону ключей, на который берется лок. Если у приложения требование, скажем, 50 ms на p99, а транзакции выполняются за миллисекунду + сеть, то сериализация небольшого числа таких транзакций не будет проблемой.

Весь посыл поста в том, что при использование "serializable" снижается вероятность багов, но при этом в большинстве случаев не страдает производительность. И мы ни в коем случае не призываем не использовать слабые уровни, если это необходимо. Просто мы считаем, что по умолчанию должен быть уровень "serializable", а более слабые уровни должны указываться разработчиками приложений явно. Такие СУБД, как YDB и CockroachDB, вполне доказали, что это хорошо работает.

Рад, что статья оказалась полезной для Вас.

Когда проекту 10+ лет, то скорее всего уже всё отлажено. Конечно, баги могут всплывать, но вероятность к этому времени уже гораздо ниже.

К сожалению, проблему дедлоков до конца не решили. По факту, если нет контеншена (т.е. запросы к базе "размазаны"), то отлично работают оптимистичные блокировки (по факту, это и не совсем локи). Но если контеншн прям очень большой (безумно большой), то всё утонет в ретраях и тут, возможно, лучше пессимистичные (если дедлоки не станут часто случаться).

В голой базе нет вообще ничего

Как разработчик СУБД, я бы поспорил с этим утверждением :)

любую бизнес задачу решает база + КОД

Безусловно. Но не надо делать в коде то, что делает база. А если очень нравится писать код СУБД, то лучше его для СУБД и писать :)

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

Тут каждый решает сам для себя: так и репликацию можно самому делать. Тогда базе останется только буква A и половинка D из ACID :)

Ну вы же почему-то ничего не написали про ограничения и нюансы режимов изоляции. отличных от read committed :)

Стоило об этом написать, согласен. Просто фокус был на другом.

С многопоточностью так не работает.

Это базовое требование, которое должно быть ОСМЫСЛЕННО реализовано. И проблема именно в осмысленности. serializable сам по себе ничего не решает от слова совсем. Ну выполнились транзакции последовательно, или ... не выполнилась одна, что стало более возможно. А что хотели?

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

Я с Вами в этом никак не соглашусь. И мне кажется, что пример с многопоточностью в джаве в данном случае не совсем уместен. Когда у меня многопоточное приложение, это моя ответственность за то, чтобы оно работало правильно. Мы же говорим с Вами о транзакциях и concurrency control (строго говоря речь не о многопоточности, а о конкурентном выполнении). Приложение может быть однопоточным и отправлять асинхронные запросы (транзакции, шаги транзакций) в базу. Конкурентное/параллельное (и в следствие многопоточное) выполнение - всё это происходит внутри СУБД. Моя позиция заключается в том, что concurrency control - часть СУБД, и это ответственность СУБД сделать так, чтобы транзакции были ACID. Но никак не пользователь должен управлять блокировками, concurrency и т.п. Я считаю, что стандарт SQL это отражает и именно поэтому там по умолчанию уровень изоляции "serializable".

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

Я не понимаю, почему в контексте нашего обсуждения "serializable" - это костыль, а "select for update" нет. Я бы сказал, что наоборот :)

Потому что он ничего не решает в вашем примере :) Но давайте вы ответите на простой вопрос - какие локи будут установлены в вашем примере, который вы привели :)

Решает: работало неправильно - стало работать правильно. А о локах должны думать разработчики СУБД.

Т.е. мы будем получать кучу ошибок и начинать сначала

Могу привести в пример TPC-C, где обычно от базы требуется repeatable read / serializable (если не делать это на стороне клиента). На 8000 транзакций в секунду (это соответствует 16K warehouses с efficiency около 100% и это очень много) приходится всего 300-400 ошибок/с сериализации. Т.е. примерно 4%. Безусловно, есть приложения, где это гораздо более критично: вот тогда можно подумать о более слабом уровне изоляции и использовать контроль со стороны приложения. И всё, что я пытался донести, это то, что надо двигаться от serializable вниз при необходимости, а не наоборот.

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

Вы абсолютно правильно уловили суть поста. Но только мы считаем, что использование "Serializable" соответствует KISS и не означает лень. Найденные нами результаты исследований показывают, что ошибки из-за использования слабых уровней изоляций встречаются достаточно часто. И это неудивительно: я не знаю ни одного программиста, который бы не делал ошибок (в т.ч. в СУБД, ОС, компиляторах и т.п.). При этом выявлять concurrency баги в приложениях БД очень трудно.

А с таким подходом не спасет ничего :(

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

Ну и код понятно кривой от слова совсем, так как еще "древние" писали, что если хотите обновлять поле, на котором есть бизнес условие, либо прикрутите констрейнт, либо делайте явно select for update.

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

create table A (id int, depId int, empId int, stratDate time)
где храним всех ответственных. В одном рекорде храним факт того, что сотрудник Х сидит на смене в департаменте начиная с определенного времени. Куда можно внести длину смены (4 часа) и даже ограничить возможные точки начала смены, но мне лениво
В чем проблема read committed?

В Вашем примере не хватает деталей, к сожалению. Предположу, что ради простоты и наглядности pk является (id, depId, empId, time), т.е. в одно и то же время в одном департаменте может быть несколько дежурных (и это не один и тот же человек). Если мы хотим обеспечить ограничение, что всегда есть хотя бы 1 дежурный, то нам потребуется select + delete. И тут надо либо serializable, либо select for update, как Вы и указали (как и в первоначальном примере). Но почему просто не использовать serializable?

Мы как раз видим проблему в перекладывании (по умолчанию) ответственности за согласованность с СУБД на разработчиков приложений. В полноценных ACID-транзакциях такая ситуация невозможна, т.к. изоляция подразумевает сериализацию транзакций. А в примере, который мы привели, проблема вызвана отсутствием сериализации (по умолчанию).

В YDB требуется, чтобы PK был уникален, поэтому я немного изменил таблицу, но это никак не влияет на план запроса:

CREATE TABLE `/ru/home/eivanov89/mydb/window_fun_example` (
    id Uint64 NOT NULL,
    unique_nounce Uint64 NOT NULL,
    val_a int NOT NULL,
    val_b int NOT NULL,
    PRIMARY KEY (id, unique_nounce)
);

SELECT id, SUM(val_a) OVER (ORDER BY val_b DESC) AS running_total
FROM `/ru/home/eivanov89/mydb/window_fun_example`
ORDER BY id
LIMIT 1000;

Explain вполне ожидаемый:

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

Как в YDB решается эта проблема?

Извините, я не понял, какую проблему Вы имеете в виду. И ещё раз повторю, что шардирование - деталь реализации СУБД, и это на 99.9% прозрачно для пользователей.

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

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

В распределенной СУБД сбои в пределах модели отказов никогда не влияют на работу СУБД и клиентов. Отказал сервер (или целый ДЦ) - все продолжает работать абсолютно прозрачно для всех пользовательских приложений.

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

Достоинством распределенной СУБД является то, что не требуется десятков тысяч монолитных СУБД. Можно иметь одну большую СУБД и внутри нее тысячи изолированных друг от друга баз. Тогда потребуется один администратор вместо двух десятков. Кроме того, один администратор вполне справится с несколькими распределенными СУБД, а нескольких распределенных СУБД хватит 99.999% пользователей. Но я согласен, что администраторов распределенных СУБД на рынке мало и их тяжелее нанять.

Не преждевременная. Изначально большинство приложений пишется в расчёте на монолитную БД, и 95% за её пределы никогда и не вылезают. Работа с шардами начинается не раньше, чем будет очевидно, что в одну базу вся нагрузка не помещается.

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

Информация

В рейтинге
Не участвует
Откуда
Санкт-Петербург, Санкт-Петербург и область, Россия
Работает в
Дата рождения
Зарегистрирован
Активность

Специализация

Бэкенд разработчик, Разработчик баз данных
Старший
Git
C++
Многопоточность
Проектирование баз данных
Алгоритмы и структуры данных
Оптимизация кода
Системное программирование
Python
Bash
Английский язык