#48: Реплицируем RDBMS с ко-фаундером Neon
Александр Пахомов и Стас Кельвич (сооснователь Neon — serverless Postgres as a service, где он сделал storage layer) с высоты птичьего полёта осматривают весь ландшафт баз данных: почему `SQL` пережил полвека и все попытки его заменить, что осталось от волны `NoSQL`, зачем Neon отдаёт Postgres по `HTTP` для edge-воркеров на V8, и где заканчивается один сервер. По пути — «константы скорости света» для пропускной способности (30–40k транзакций в секунду в одно соединение), инженерия шардирования и shuffle join, а также распределённые транзакции: двухфазный и трёхфазный коммит, Paxos Commit и подходы к изоляции. Это первая, обзорная часть; технические кишки Neon — в следующем выпуске.
Главное
- `SQL` — декларативный язык: ты описываешь желаемый результат, а система сама придумывает, как его получить; с рекурсивными запросами (`WITH RECURSIVE`) он формально Turing-полный.
- Базу данных итерировать труднее, чем компилятор или язык: люди не прощают потерю данных, поэтому рынок ценит в СУБД в первую очередь надёжность и годы безотказной работы — это тормозит эксперименты и делает индустрию консервативной.
- Волна `NoSQL` начала 2010-х в итоге эволюционировала обратно к классике: у той же MongoDB появились write-ahead log, B-деревья и buffer manager, и она стала похожа на обычную базу, просто с другим языком запросов.
- В edge-средах (Cloudflare Workers, V8 isolates) нельзя открыть произвольный TCP-сокет, а долгоживущие соединения не переиспользуются между запросами — поэтому Neon отдаёт Postgres по `HTTP`: stateless-запросы V8 агрессивно переиспользует, экономя лишние round-trip'ы.
- «Константа скорости света» для честного пинг-понга «запрос-ответ» в одно соединение — 30–40 тысяч транзакций в секунду в любой базе (Postgres, Redis, свой кэш на C++); упирается в CPU и планировщик ОС, а не в саму базу. Без сети и диска, батчами — миллионы (уровень Java `ConcurrentHashMap`).
- Распределяют данные ради двух вещей: пропускной способности (масштабировать за предел одного сервера) и отказоустойчивости (реплики переживают сбой ноды) — но 10 машин отказывают чаще одной, выигрыш даёт только система, умеющая восстанавливаться от отказов.
- Распределённый `JOIN` больших таблиц чаще всего сводится к shuffle join: перешардировать данные по ключу через сеть, локально сджойнить и смёржить; узкое место — не CPU, а бэкплейн сетевого свитча.
- Атомарность распределённой транзакции обеспечивают двухфазным коммитом (блокирующий: нужна доступность всех участников), а корректный неблокирующий вариант — Paxos Commit; изоляцию делают либо кросс-нодовым snapshot isolation (Spanner, CockroachDB, TiDB), либо детерминированным реордерингом неинтерактивных транзакций (FaunaDB, YDB).
В выпуске
- Стас Кельвич — Сооснователь Neon (serverless Postgres as a service), где сделал storage layer и WAL-стриминг; ранее хакал кластерный Postgres в Postgres Professional и работал в команде баз данных Яндекса. GitHub ↗
Ссылки
Расшифровка
[00:00] Александр: Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Вы слушаете подкаст, в котором разработчик современной базы данных изучает, как они работают, и делится знаниями со слушателями. Сегодня у меня в гостях Стас Кельвич, ко-фаундер базы данных as a service Neon. Мы поговорим про историю языка запросов SQL, разберём феномен его популярности, обсудим NoSQL-базы, распределённые транзакции, шардирование и много-много всего полезного и интересного из мира баз данных. Поехали!
[00:39] Александр: Ты сказал, что этим летом языку запросов SQL исполнилось 50 лет.
[00:46] Стас: Да, но там сложно, с какого момента конкретно считать. По-моему, этим летом вышла статья ACM про то, что SQL уже 50 лет, и они отсчитывали время от статьи Кодда про реляционную алгебру. Хотя, если это 73-й год, то сама статья вышла раньше. Наверное, они считали не от неё, а от System R — это уже была имплементация. Надо перечитать, но получается где-то 74-й.
[01:20] Александр: SQL сложно называть языком программирования, но если мы с некоторыми условностями всё-таки его так назовём — это ведь query language.
[01:27] Стас: Можно назвать его языком декларативного программирования. Он необычен прежде всего тем, что не императивный: ты не говоришь, как это сделать. Тут я и функциональные, и нефункциональные языки в одну категорию отношу — не так важно, какие там идеи, но алгоритм всё равно пишешь ты. А с декларативным программированием ты описываешь конечный результат, который хочешь получить, и система сама придумывает и выдаёт тебе его. Насчёт «язык программирования» — checkbox тут скорее стоит, особенно с WITH RECURSIVE: когда у тебя появляется возможность делать рекуррентные запросы, он формально Turing-полный, ты можешь написать на нём что хочешь. Не обязательно самым удобным способом, но можешь. Я не буду очень религиозен по этому поводу: можно говорить «язык запросов», можно «декларативное программирование». Кто-нибудь наверняка поспорит, но я за это сильно топить не буду.
[02:31] Александр: Интересно, что 50 лет для нашей индустрии — это почти вся индустрия, порядок такой. Как будто это самый устоявшийся и самый старый язык. Он, конечно, прокачивался, стандарты выходили, но в целом это самый старый декларативный язык, который прижился и до сих пор обладает огромной актуальностью. То есть знать SQL — это прям must-have для разработчика: если ты не можешь написать запрос средней сложности, грубо говоря с HAVING, — это странно. Мне вот интересно, за счёт чего так вышло, что этот язык до сих пор настолько актуален?
[03:04] Стас: Сложно сказать почему. Это ведь не какой-то бесконечно удачный язык. С точки зрения синтаксиса он появлялся в те времена, когда было сильно влияние COBOL-like идеи: давайте сделаем язык программирования, похожий на естественный. Считалось, что это упростит использование. Кажется, от этой идеи все отказались, особенно в обычных языках программирования. Есть, условно, два наследника той эпохи — SQL и COBOL. COBOL, можно сказать, уже нет (опять же, кто-нибудь поспорит), а SQL есть, и им пользуются.
[03:52] Стас: Что на это влияет? Кажется, базу данных сделать сложнее, чем минимально применимый компилятор или интерпретатор языка. Проблема в том, что люди не любят, когда их данные теряются. Если у тебя компилятор начал крэшиться при компиляции — окей, неприятно, но с этим можно что-то сделать: от компилятора мы ожидаем, что баги проявятся хотя бы на этапе компиляции, и это то, что ты как разработчик своего нового языка должен гарантировать. С базой данных сложнее: очень много всего нужно учесть, чтобы не терять данные. И сразу становится важным, сколько лет твоему проекту. Гарантия «вот у Васи оно три года работает и не потеряло данные» весит гораздо больше, чем когда ты пишешь язык программирования. Это одна из причин, которая делает итерации в базах данных сложнее.
[05:03] Александр: То есть мысль такая: написать новый язык программирования, принести в него какие-то новые идеи, сделать эксперимент, понять, работает он или нет, — это, во-первых, легче. И, во-вторых, добиться доверия людей, внедрить хотя бы в полупродакшн — с языком тоже легче.
[05:28] Стас: Я думаю, скорее второе. И то, и то — сложные инженерные задачи; с языками программирования они порой даже сложнее, чем с базами, и написать сложнее. Но то, как рынок тебя оценивает, отличается: у языков программирования safety-фичи «оно уже есть, работает много лет» гораздо реже в приоритете, и это делает итерацию проще. А с базой данных наоборот: большинству, особенно если мы говорим про OLTP, в первую очередь нужно, чтобы данные сохранялись и не терялись. Это консервативность, и она протекает в фичи.
[06:16] Стас: Не знаю, насколько это применимо к самому языку, потому что имплементации всего с нуля были. Но тут тоже, наверное, критическая масса людей. Мне кажется, декларативность важна — это важная фича. Люди пытались от неё отказаться, таких попыток было много, и каждый раз рынок после такой попытки всё равно приходил к тому, что для декларативных языков есть ниша. Дальше вопрос — какие именно декларативные языки. Есть сам SQL с достаточно посредственным синтаксисом. Его можно улучшать, но вопрос: если ты не меняешь сильно семантику, перевешивают ли бенефиты от улучшения синтаксиса то, что теперь кому-то нужно это выучить? Например, были ребята — у них стек Telegraf, InfluxDB — они в какой-то момент сделали свой синтаксис, который, мне кажется, чуть приятнее использовать в несложных запросах, чем SQL. Пожили с ним года три-четыре и откатились обратно к SQL, потому что это то, чего люди хотят. Тут уже просто инерция, и рынок определяет.
[07:34] Стас: Могу немного сайд-историю про это рассказать. Меня всегда интересовало, почему многопроцессорность пошла по модели, где мы эмулируем одну картинку поверх мульти-CPU архитектуры. То есть все сильно парятся по поводу гарантий ордеринга обращений к памяти, всех этих memory-моделей. А есть альтернативная модель: ты условно ничего не гарантируешь про ордеринг обращений к общей памяти, а делаешь message passing прямо на уровне инструкций CPU — между ядрами. Получается немного другая модель программирования. У меня в круге общения обычно нет людей с хорошим опытом в дизайне железа, но на одной конференции я пересёкся с человеком, который когда-то, довольно давно, участвовал в разработке CPU с нуля. Это был старый проект — из тех времён, когда люди пытались сделать JVM в железе: процессор, который мог исполнять JVM-инструкции. Там, по-моему, сделали одну из первых имплементаций транзакционной памяти. Проект сожрал космическое количество денег и не выстрелил. В следующий раз транзакционную память делал уже Intel где-то с 2010-го: они лет десять пытались её включить, включали, откатывали из-за багов, снова включали, снова откатывали — каждые два года новая итерация этой истории.
[09:24] Стас: И вот я этого человека спросил — помню, что использовал словосочетание fundamental reasons: есть ли какие-то fundamental technical reasons, чтобы не сделать многоядерную архитектуру через message passing? Он, видно, тоже про это думал, и сразу ответил: «Fundamental technical reasons? There are fundamental marketing reasons». Весь софт написан для одноядерных процессоров. Если отмотать назад, к эпохе, когда почти все системы были одноядерными, и ты начинаешь продавать свой многоядерный процессор — весь софт написан под одно ядро, ты хочешь его запускать и хочешь использовать преимущество хардварного параллелизма. И тут начинается backward compatibility, и она становится важнее всего. Естественно, приходишь к тому multi-CPU, который у нас сейчас есть, — просто ни у кого не будет бюджета сделать иначе. И это не то что кто-то денег не нашёл: их точно не будет, потому что это деньги на ветер — пытаться переучить весь мир, при том что и так, и так будет работать. А если бы всё изначально строилось из того, что ядер много, возможно, оно пошло бы по другой траектории. Но нет.
[10:40] Стас: Долгое отступление, но про маркетинговые причины и про то, как люди думают о чём-то, тоже важно. И не стоит относиться к этому как «ну вот, они все неправы». Может, и неправы, но так устроен мир.
[10:54] Александр: То есть, если вернуться к SQL, к тому феномену или просто факту, что он настолько прижился, — его просто уже никто особо не хочет менять, хотя попытки будут.
[11:07] Стас: Все пытаются. Недавно Google выпустил статью: давайте чуть-чуть модернизируем синтаксис, — и раскатили это у себя. Я думаю, что-то из этого приживётся.
[11:15] Александр: Именно — не поменять в корне парадигму. Например, когда был бум NoSQL-баз, все про это тоже говорили. Как раз мой карьерный рост попал на те времена, когда нужно было понимать, чем NoSQL отличается от SQL. Это был прям стандартный вопрос для джуна — как будто ему надо было понимать это фундаментально. Сейчас, если мне такой вопрос зададут, я телегу раскачу на целый выпуск подкаста. А тогда казалось: всё, SQL — это SQL, а NoSQL — это Not Only SQL. Но убрать SQL не очень-то получилось: они как-то сейчас вместе живут, есть key-value, есть другие диалекты, но в целом SQL живёт. И вот этот процесс постепенной плавной модернизации, о котором ты сказал на примере Google, как будто даёт больше надежд: давайте не переворачиваться с ног на голову, а плавно подтачивать существующую парадигму.
[12:19] Стас: Ну да. Мы начали сравнивать базы данных с компиляторами — можно усложнить проблему и взять операционки. Операционки не идеальны, но количество ресурсов, которое нужно, чтобы сделать операционку, огромно. Многие пытались, а у нас есть три-четыре, может пять, основных операционных систем, и всё. Кто-то попытался — не получилось. У Google даже не получилось с Fuchsia, если ты следил: они писали свою операционку с нуля, много чего там правильно и хорошо сделали. Не знаю, полностью ли свернут проект, но команду Fuchsia заметно сократили. Сложно: много кода, большая инерция, очень сложный проект.
[13:07] Стас: Про NoSQL сложно говорить, потому что там очень много всего разного. Это плеяда баз, которая появилась тоже в начале 2010-х и челленджила основные дизайн-решения существующих баз, и они были очень разные. Часть — про «давайте сделаем другой язык запросов». Можно сгруппировать так: был пласт идей про то, что мы поспорим с какими-то дизайн-решениями текущих реляционных баз — время прошло, они неверные. Это очень правильная вещь: всегда нужно долбить устоявшееся. Время поменялось, появилось другое железо, другие наборы проблем, какое-то легаси устарело — имеет смысл попробовать сделать заново, с первых принципов. Это один кластер идей.
[13:57] Стас: А другой кластер — и это было видно во многих стартапах — что люди не до конца понимали проблемы, которые уже решены в существующих на тот момент базах. Инженеры, начинавшие стартапы про базы данных, не всегда полностью понимали, почему оно так сделано. И это уже с надзнаками минус: спорить с устоявшимся положением вещей круто и полезно, но если ты при этом плохо в нём ориентируешься, у тебя не вся информация, тебе сложнее. Ты её выучишь постепенно — как человек и как компания, — но лучше бы знал с самого начала: тогда больше шансов на успех. Многие позакрывались по разным причинам. А те, кто не позакрылись — если брать именно базы, которые будут для тебя system of record, где приложение хранит основные данные, — постепенно эволюционировали и стали очень похожи на обычные базы, возможно, с другим языком запросов. Та же Mongo начинала гораздо радикальнее, чем есть сейчас: со временем появился и write-ahead logging, и правильные деревья, и buffer manager — она стала гораздо больше похожа на базы начала семидесятых, на которые похожи все остальные, включая Oracle, MySQL, Postgres. В конкуренции с ними они просто стали очень похожи. Там, кстати, не SQL, но после того, как ты распарсил язык, сделал первый семантический анализ, дальше у тебя оптимизатор — и поехало, всё примерно так же устроено. Оно не соответствует стандарту; есть плюсы и минусы. Большой плюс — можешь делать то, что твои кастомеры хотят, и не нужно соответствовать чьему-то легаси. Проблема — людям придётся это учить.
[15:47] Александр: Да, с этим, кстати, у меня отлично справляется ChatGPT — писать запросы в Mongo. Идеально.
[15:58] Александр: Ещё одна ветка NoSQL-баз — это движение в сторону упрощения и вообще отказа от какого-либо языка. Вам предоставляют, грубо говоря, key-value. По сути-то все базы данных под капотом и есть key-value. Зачем тебе как инженеру прослойка в виде языка, если задача подходит и голова позволяет? Ты можешь сделать просто key-value хранилище, которое будет работать максимально эффективно, и не надо запросы писать. Вопрос, конечно, с транзакциями — как хранить, — но если их отложить, то все индексы и все деревья так или иначе от key-value. И что из этого более-менее прижилось? Какие-то большие key-value. Redis, наверное.
[16:45] Александр: Redis — это больше как кэш, а я хочу именно то, что хранит на диске персистентно.
[16:52] Стас: Redis можно сконфигурировать.
[16:53] Александр: Можно, но изначально он всё-таки позиционировался как быстрый кэш над базой. Может, ты что-то вспомнишь ещё?
[17:01] Стас: Есть Tarantool, были всякие подходы, CouchDB, CouchBase — с ними я меньше сталкивался. Был ещё такой Redis-like, куда неплохо вставили CRDT — там был conflict resolution. По-моему, одна из первых систем, где это сделали; название сейчас не вспомню. Пойнт в том, что если ты делаешь условный SET в Redis, то на структурах данных достаточно органично определяется merge-операция — в зависимости от того, какие у тебя запросы на чтение. Самый простой пример: если у тебя есть табличка, в которую ты просто всегда дописываешь данные — какой-то сенсор пишет, — это хорошая ситуация для distributed multi-master. У тебя есть две копии, в каждую можно писать, они в две стороны реплицируются. Если одна часть базы полчаса не получала INSERT‘ы из-за отсутствия связности, а потом связность появилась, и у тебя в каждом INSERT‘е были UID, ты приходишь к синхронному состоянию: было временно несинхронное — синхронизировался. В SQL это сделать сложно, потому что каждая таблица предполагает, что ты можешь делать с ней всё что угодно. А когда операции над данными более явные — когда ты создаёшь SET, multi-set, очередь, append-only set, — гораздо лучше можно задефайнить вот такие merge-операции.
[18:55] Александр: Когда мы вводим больше constraint’ов, больше ограничений на то, что может происходить со структурой данных, тем больше свободы с точки зрения технической реализации — чтобы сделать это эффективнее. Нам не нужно учитывать все возможные сценарии.
[19:41] Стас: С обычными базами сложно, потому что тебе придётся большую часть этого сделать самому. А если у тебя есть база, которая ограничивает тебя набором структур данных — вот у тебя есть SET, — то думать про это сильно проще. Ты начинаешь думать: могу ли я это приложение уместить в эти структуры данных? И какие фичи мне не следует делать, потому что их не покрыть этими структурами? Интересный кейс — оффлайн: приложение какое-то время работает без интернета, но потом eventually интернет появляется, и это дополнительное условие. В каком-то смысле это почти во всех мобильных приложениях: даже если это игрушка, люди про это задумываются. Статистика, может, устаревшая, но несколько лет назад на половине приложений стоял SQLite. Когда появляется интернет, ты синхронизируешься с сервером. Обычно всё происходит внутри твоего user ID, поэтому конфликтов мало, но если у тебя есть браузер и девайс, то появляются и конфликты между ними.
[20:41] Стас: Да, про мобилки. Была такая база — Realm. Она вроде из iOS, но была подо всё. У них тоже, по-моему, не делали SQL-интерфейса — они делали очень прикольные драйверы для основных языков: такой красиво написанный ORM, и очень приятно с точки зрения потребления. Но с бизнесом у них не очень сложилось — их выкупила Mongo. По сумме инвестиций сделка была скорее спасением, чем большим выходом. По-моему, они и сейчас есть, но не то чтобы сильно популярны. Они прикольную планку задали с точки зрения developer experience. Я никогда не пользовался продуктом — читал в основном документацию, гонял примеры, — но то, как устроена документация, как они свой ORM для каждой платформы выписали, со мной очень срезонировало.
[21:35] Александр: Я такими продуктами пользовался — где предоставляется некоторый клиент, SDK, что-то, с помощью чего ты просто в языке программирования пользуешься им как библиотекой. Основные методы у сущностей есть, и их хватает: определить таблицу, сущность, сохранить, считать. И когда этот код уже написан — причём качественно, продуманно, — он написан лучше, чем ты сам написал бы то же самое для своего приложения. Тебе ведь поверх хранилища по-любому нужны какие-то абстракции: ты не будешь SQL из REST-контроллера сразу писать, ты будешь через ORM или репозиторий — кто как. А тут оно у тебя уже есть — хоба, заиспользовал, круто. Но как будто эта идея всё равно не выстрелила: всем хочется какой-то унификации и, возможно, допом уже иметь SDK. Если у тебя нет SQL, то этим SDK как будто никого не впечатлишь — сложно продавать.
[22:46] Стас: Ну да, но тут ещё разные рынки. Штука с SDK именно для мобильных девайсов чуть проще, потому что набор языков и платформ более фиксированный. А если ты берёшь обобщённую бэкенд-разработку, там гораздо больше всего. Но в вебе что-то подобное тоже происходит — не SDK, а скорее свой query language: я про GraphQL. Немножко в другую сторону, но решение плюс-минус тех же задач, если грубыми мазками. Люди привыкают делать просто HTTP-запрос в другой сервис вместо запроса в базу. Ведь очень много кода пишется просто вокруг сущностей в таблице, где бизнес-логики почти нет — есть сервисы, которые действительно просто HTTP-прослойка над таблицей.
[23:45] Стас: Ещё вопрос, что потом происходит с таким сервисом. Ты можешь его сделать, но потом тебе становится интересно, сколько у тебя юзеров в день регистрируется. И если у тебя просто CRUD на нескольких табличках — тебе надо написать программу, скачать все данные, написать программу, чтобы получить этот график. А если у тебя есть SQL или подобный способ общения со всей базой, то при правильном сетапе ты сделаешь это за минуту: сходишь по табличкам, посмотришь, сколько за последние недели было юзеров каждый день. В противном случае пришлось бы писать программку. Когда ты пишешь сервис, это для тебя не проблема, но потом чаще всего об этом задумываешься — и вот тот момент, когда ты или делаешь какую-то репликацию в обычную базу, или ещё что-то. Я не то чтобы топлю за то, чтобы пользоваться базами данных во всём — интересно, какие есть новые идеи. Вот ты упомянул GraphQL.
[24:55] Александр: Насколько я понимаю, в Facebook много сервисов, которые видны пользователям, действительно написаны без бэкенда как такового. Ты декларативно определяешь набор аутентификационных правил, потом GraphQL-схему — и всё, дальше пишешь фронтенд-код. Бэкенд-кода у тебя нет, его заменяет такая обобщённая штука. Но у тебя потом есть отдельный, не-GraphQL, аналитический доступ к этим данным — с дашбордами и всем остальным.
[25:28] Стас: Хороший пример. Действительно, много приложений можно так написать.
[25:36] Александр: Мы уже посмотрели на стандартный SQL, с него начали. Поговорили про какие-то NoSQL-решения, MongoDB со своим языком запросов, про key-value-вариации, про Realm и мобилки, про GraphQL в вебе. И вот сейчас я вспомнил — есть такая прикольная книжка, «Семь баз данных за семь недель». Она, конечно, уже подустарела, мягко говоря. И там была база данных, которая предоставляла REST-интерфейс поверх данных.
[26:15] Стас: Firebase?
[26:17] Александр: Firebase я знаю, но это не из той книжки. Я сейчас загуглю. Я бы, кстати, не сильно советовал её сейчас читать — она действительно подустарела. Ты не читал?
[26:31] Стас: Нет, даже не слышал.
[26:35] Александр: Там интересно — про каждую базу данных отдельный подход. По-моему, это Riak.
[26:41] Стас: Riak, ага. Мы оба забыли её название — я как раз Riak пытался вспомнить про CRDT. В Riak, по-моему, можно было определять правила, как у тебя мержатся данные. И там как раз был HTTP-интерфейс.
[26:59] Стас: Нет, HTTP, мне кажется, это сейчас table stakes. Даже если ты делаешь SQL, настолько всё проще с HTTP, что постепенно народ туда идёт. Могу, кстати, историю из Neon про HTTP и Postgres-протокол рассказать.
[27:16] Александр: Давай — HTTP мне как раз интересно развить. Давай на примере Neon погрузимся в историю.
[27:22] Стас: Neon — это Postgres as a service. Мы сделали свой storage layer для Postgres — можем отдельно поговорить, что это меняет, — но для end-user это просто Postgres, который умеет масштабироваться. То есть автоматически находить размер твоей «компьютерной дыры»: не обязательно заранее выбирать, сколько у тебя будет CPU и памяти, он может тюнить это по ходу, а ты можешь задать границы. И storage позволяет делать такие штуки, как branch: ты можешь легко форкнуть свою базу данных, и она быстро форкнется вне зависимости от размера. Проведи параллель с EFS-снапшотами: ты делаешь copy-on-write снимок данных. Есть терабайтная база — ты сделал новый branch, стартанул базу со снапшота. Мы называем это branching, происходит моментально: под капотом мы никаких данных не копируем, кроме метаданных. А потом, когда ты начинаешь менять данные, эти бранчи живут независимо.
[28:23] Стас: Возвращаясь к HTTP. Это Postgres. Снаружи мы видны как обычный Postgres TCP-порт, и люди подключаются. Дальше — проблемы. Есть люди, которые пишут, например, Cloudflare Worker или Vercel Edge Functions, которые тоже под капотом Cloudflare Worker. И это такой environment — это V8. V8 isolates: в V8 есть security-контекст, который называется isolate, и тогда ты в один Unix-процесс с этим V8 можешь напихать сотни разных приложений. Получается очень эффективно по памяти: ты пишешь свой JavaScript-код, деплоишь. И в случае Cloudflare Worker можешь это ещё разложить по всем дата-центрам мира, где у них есть presence, чтобы сделать геолокальным — они сделают anycast, когда ты по BGP находишь ближайший к тебе сервер и именно с ним говоришь. Всё очень эффективно в плане денег: ты просто задеплоил JavaScript в существующий процесс, и у тебя аллоцированы десятки килобайт памяти, когда ты не работаешь.
[29:36] Стас: Но в V8 нельзя открыть TCP-соединение. Точнее, HTTP-соединение, которое тоже является TCP-соединением, ты открыть можешь, а вот именно API, чтобы открыть произвольное TCP-соединение, у тебя нет. У нас какое-то количество таких пользователей, и это то, куда мы как компания за пользователями хотим идти. И первое, что мы сделали, — туннель через веб-сокеты: ты открываешь веб-сокет к базе, а внутри у тебя обычный Postgres-протокол. Плюс в том, что можешь взять обычный JavaScript-драйвер для Postgres — мы сделали такой раппер для pg. То есть можешь использовать pg, подключить ещё наш NPM-пакет, и что этот пакет сделает? Он просто перехватит TCP.open, TCP.write, TCP.read, под капотом откроет веб-сокет и будет эти write и read через веб-сокет перенаправлять. А дальше, если у тебя уже есть существующий код с драйвером, он весь будет работать — все странные фичи Postgres, типа LISTEN/NOTIFY и всё остальное: у тебя просто поменялся транспорт, а весь драйвер строится поверх этого TCP-сокета.
[30:52] Стас: Но дальше начинается следующая штука. Этот код запускается на HTTP-запрос, и ты не можешь переиспользовать открытое веб-сокет-соединение между запросами. Нет никакого shared-контекста — это одна из причин, почему это дёшево и удобно запускать, — а значит, ты не можешь пошарить веб-сокет и в следующий раз будешь открывать его заново. Это handshake TCP, потом TLS, потом ещё несколько auth-пакетов Postgres. Если у тебя latency до базы, скажем, один хоп, 3 миллисекунды, то суммарно набегает десяток таких хопов, вот эти 30 миллисекунд ты просто выкидываешь на каждое соединение. Это плохо.
[31:42] Стас: Соответственно, следующая итерация — давайте сделаем это на HTTP. С HTTP другая история: если ты не запромоутил соединение в веб-сокет, а остался на уровне обычного HTTP — GET-запроса или POST-запроса, — то у тебя есть гарантия, что соединение stateless. А V8 очень агрессивно переиспользует открытые TCP-соединения, даже между разными изолятами. За счёт того, что в базовом HTTP нет состояния, это очень важная гарантия — ты можешь переиспользовать соединение между условными Васей и Петей, хотя у тебя между ними security-барьер, данные так не утекают. Соответственно, если у тебя есть набор Cloudflare-серверов и ты ходишь на какой-то домен, api.neon.tech, то постепенно, если сколько-то людей пользуется, у тебя в среднем получается, что на этом сервере всегда есть открытое соединение к api.neon.tech. Cloudflare — очень много серверов, но в целом их не то чтобы прям много: если пара тысяч людей пользуются и хотя бы десяток популярных сайтов среди них есть, то за счёт round-robin у тебя везде есть открытые HTTP-соединения. И тогда у тебя снова один round-trip: не 30 миллисекунд, а снова 3 миллисекунды, и всё хорошо.
[33:08] Стас: Это опять же одна из вещей, которые уже есть в мире. Можно спорить, какой протокол лучше, но есть V8, есть куча деплойментов этого V8 — браузеры, не браузеры, — мир так устроен. Есть хорошие причины избегать shared state в таких compute-backend-историях: это проще масштабировать. И это давит на то, что тебе нужно давать HTTP-интерфейсы. В нашем случае — да и много кто так сделал — это просто пост-запросик: ты SQL-ник шлёшь и получаешь ответ в каком-то формате. Это уже не полностью совместимая история: часть функциональности не будет работать. За счёт того, что к базам есть долгоживущие соединения, много фичей требуют именно сессионной семантики — какие-то переменные, значения писать, временные таблицы, которые видны только в этой сессии. А история про request-response, где нет контекста сессии, это меняет. И это важно в контексте масштабирования: все истории про сессионный контекст сложнее масштабируются, оно плохо дружит с cloud load balancer’ами — нужны sticky-сессии, чтобы этот контекст где-то хранить. Это есть в протоколах Postgres, MS SQL, Oracle и всех остальных, и это для них проблема: не 100% функциональности можно предоставить в аккуратном request-response. Но ничего не мешает сделать урезанный набор: у кого помещается через HTTP — идут через HTTP, у кого нет — открывают долгоживущие соединения.
[35:01] Александр: А внутри этих воркеров просто взять Postgres, нативный драйвер, не получится, потому что он по TCP соединяется?
[35:11] Стас: По TCP — да, он тебе просто пожалуется, что нет TCP.open, будет JavaScript-ошибка, там нет такого окружения. И получается, что люди, которые это пишут и хотели бы использовать Postgres, не могут. Но они могут задеплоить какой-нибудь general-purpose слой — есть PostGraphile, много чего, — который возьмёт Postgres и переоткроет его как REST или GraphQL, и им проще пользоваться. Но если можешь напрямую, и тебе не нужен именно GraphQL, то можно напрямую кидать запросы, и мы видим много таких юзеров.
[35:55] Александр: Интересно про Neon для слушателей, которым интересно, как он внутренне устроен. Мы планируем со Стасом следующий выпуск записать, состоящий почти только из подобных технических штук про Neon, так что оставайтесь подписанными — будет большой, хороший, интересный выпуск.
[36:29] Александр: Давай немножко назад — про ландшафт, про рынок. Мы очень много проговорили про то, как приложения взаимодействуют с базами, про протоколы. Есть ли у тебя что-то ещё в голове про уровень языка запросов, про что мы, возможно, не поговорили?
[36:57] Стас: Ты про то, как коммуникация устроена, или про сам язык запросов?
[36:59] Александр: Больше про язык.
[37:01] Стас: Вообще есть много Prolog-like попыток, язычок Datalog. Есть такой проект Datomic — интересный подход: не отказываться от декларативности, но сделать хорошо задизайненный, с более понятной семантикой, чем SQL, язык декларативного программирования. Люди этим занимаются, и довольно давно — та же история про 50+ лет. Сейчас вокруг Datomic и в основном Clojure-комьюнити эта движуха идёт. Таких попыток было много. Stonebraker — один из важных людей в базах данных: он много разных баз сделал, в том числе напрямую относится к ранней версии Postgres, к его группе в Berkeley, а также Vertica, VoltDB, SciDB. Много коммерчески успешных проектов. Есть его фраза, где-то из конца 70-х — начала 80-х: количество бизнес-приложений, которым нужны рекурсивные запросы, равно примерно нулю. А «которым нужны рекурсивные запросы» — это как раз был камень в огород Prolog-like языков запросов для баз данных. Мол, чуваки, вы делаете ресёрч, а идите пообщайтесь с людьми — людям это не надо. Опять же, это мнение Stonebraker’а достаточно давнее. Кажется, есть поле, где можно нанести пользу таким подходом, но пока в массы оно не пошло — какого-то широкого адопшена за пределами Clojure-комьюнити, за пределами DataScript, который один наш знакомый эксперт в редакторах кода тоже поддерживает, я не вижу.
[38:48] Александр: Да. Datomic — интересный кейс функционального, полудекларативного стиля.
[38:57] Стас: Скорее функциональный. Я, правда, ничего на нём не писал, могу ошибаться. Ну и векторные базы данных — векторные запросы. Это свежий бранч. Основной вопрос: есть ли здесь достаточно дифференцирующих фичей, чтобы сделать отдельную базу вокруг векторного поиска, или это просто фича в существующей базе. Для Postgres есть pgvector, и я слышу и вижу по индустрии, что тебе часто проще сделать новый индекс в условном Postgres — и это всё eventually поддержат, — чем сетапить новую базу и перекладывать данные из одного места в другое. Пример, где этого не произошло, — полнотекстовый поиск. Почти во всех базах есть поддержка полнотекстового поиска, но это достаточно глубокая область, если ты уходишь во все локали, правила стемминга языков. Стемминг — это про то, как слова меняются в разных формах, временах, падежах, и это тебе нужно учитывать в полнотекстовом поиске. Потом ещё надо как-то делать ранжирование результатов. И почти во всех классических базах это сделали, но эти фичи не получали достаточно любви, чтобы объять все языки, все правила стемминга. И здесь решения снаружи баз, типа Elastic, популярнее встроенных. Тут уже продуктовое: Elastic фокусируется ровно на full-text search, и эта часть продукта получает гораздо больше внимания, чем получала бы в базе данных, где у тебя пять человек на B-дерево, пять на full-text search — и всё.
[40:53] Стас: Векторный поиск может пойти либо по пути full-text search, либо по пути географического поиска. Когда ты ищешь что-то по координатам — найти ближайшую кафешку, — тоже есть проприетарное решение, отдельная база данных, обычно надстройка над Oracle. Лидер индустрии, платные решения дорого стоят. Но есть также штуки типа PostGIS, есть географические индексы даже без экстеншенов, и почти все пользуются встроенными. В той же Mongo есть географические индексы. PostGIS — платное решение, но если ты не гео-специалист, который с теодолитом уровень на дороге отмечает, то чаще всего ты просто делаешь CREATE COLUMN TYPE coordinates или географический POINT, создаёшь индекс — и всё работает. Кажется, что векторный поиск будет именно таким. Хотя инвестиций в него много. Есть, например, фонды, которые принципиально не инвестируют в vector search, потому что не верят, что там достаточно дифференциации.
[41:16] Александр: Для слушателей, кому интересно, как устроен vector search и последние новости из этой области, делаю ссылочку на выпуск подкаста «Тысяча фичей» про Qdrant — мы там с CTO Qdrant очень подробно про это говорили. И да, там, конечно, некоторая дифференциация с позицией рандомного индекса есть — если бы её не было, сложно было бы вообще существовать как отдельная база. Но в целом я с тобой согласен: прикрутить плюс один индекс — это де-факто стандарт, который поддержали все базы. Я в том числе это тоже делал.
[42:54] Стас: У меня со своей колокольни тоже не факт, что всё видно про индустрию. Мой комментарий скорее про размер рынка. Всегда есть какая-то часть — с той же географией: сегмент профессионалов, которые работают full day с географическими данными, большой. Можно сделать много компаний, которые будут зарабатывать деньги. С векторами, возможно, тоже есть такие ниши. Я скорее про то, что каждый разработчик будет юзать, когда ему понадобится vector search. Тут моя ставка в основном на то, что это будет просто ещё один индекс, и для вендоров баз данных этого достаточно — взяли и сделали. А когда у тебя очень много векторов, очень большая, нестандартная размерность — начинаются нюансики, возможно, нужны кастомные решения.
[43:47] Стас: Ещё есть аналогия с full-text search. Он сильно помогает, например, генетикам: ты можешь секвенировать ДНК, и у тебя просто текст. Основная проблема — либо поиск фрагментов, зачастую с опечатками. Дистанция — неважно, Левенштейн или не Левенштейн, — но какая-то близость тебе нужна, и люди этим пользуются: берут движки от full-text search. Это работает, но до каких-то пределов, потому что в этом языке мало «слов» и очень длинные «предложения», такие вещи отличаются. И тут народ пользуется в том числе, например, суффиксными деревьями. Но они не от хорошей жизни: если у тебя есть деньги, ты купишь что-то специализированное.
[44:44] Александр: Мне кажется, мы хорошо подошли к вопросу, который может помочь ребятам, кто слушает, начинающим, и в целом систем-дизайн пройти. Есть популярный ответ на большинстве собеседований и технических задач: какую базу использовать — бери Postgres, и всё. В большинстве случаев этот ответ, наверное, правильный. Мы тут коснулись нескольких аспектов, когда Postgres может не хватать — например, очень специализированные full-text-серчи или специализированные геоиндексы. Но если для инженера, который смотрит на задачи именно инженерно, а не с точки зрения бизнеса, — когда становится понятно, что брать Postgres не лучшее решение?
[46:10] Стас: Мы про бэкенд-разработку говорим?
[46:12] Александр: В целом да.
[46:12] Стас: У меня много бэкграунда в Rails. Я, если честно, всегда начинаю со SQLite — опять же, я бородатый ретроград, это было давно, сейчас другие популярные тулзы. Я бы задеплоил это изначально со SQLite: очень удобно этот файлик кидать, с точки зрения developer experience. Как только появляется какой-то трафик — да, тогда надо переключиться на нормальную базу, потому что у этого файлика никаких бэкапов не настроено, а если и настроено, всё равно теряешь чуть-чуть данных. Твой вопрос был про другое — где это не подходит. Мне кажется, надо какой-то back-of-the-envelope calculation, расчёт на салфетке. Прикидываешь, сколько у тебя пользователей. Вот вся Viza — по-моему, у них 30 тысяч транзакций в секунду. То, что твой лэптоп — неважно, Redis, Postgres, SQLite, — да что там, твой телефон это выдержит. Много всего другого, но с другой стороны, есть очень простое финансовое приложение, где тебе часто нужно маленькое время отклика, чем меньше, тем лучше. Если какой-то высокочастотный трейдинг — появляется такое требование: кто первый задетектил, что по какому-то набору инструментов, по какому-то ребру этого графа можно что-то заработать на текущем тик-тайме, кто первый поставил ставку, тот и выиграл деньги. И там начинается другое — это почти всегда кастомный C++, традиционно. Народ ещё играется с FPGA на сетевых картах: FPGA-код отсылаешь в железо, там можешь принять какие-то решения. Хотя, по-моему, про это больше говорили, чем реально в продакшене использовали: используется, но не так широко, как надеялись. Всё равно начинается — сложнее менять этот код, сложнее дебажить, если ты используешь FPGA, поэтому там только самые простые стратегии. Проще что-то поменять и отдебажить на условных плюсах, и low latency сетевухи с обычным интерфейсом программирования всё равно рулят.
[48:51] Александр: Знаешь, какая мысль меня посетила, когда ты сказал про тысячи транзакций в секунду. Вопрос такой: это много или мало? Если у тебя не было много опыта с перформансом, то, с одной стороны, до хрена, с другой — нормально. Вот я в голове держу: Postgres 30 тысяч транзакций выдерживает.
[49:13] Стас: Postgres, 30 тысяч — это в одно соединение, в один поток, на лэптопе, если это чтение, и ты правильно затюнишь, правильно всё сделаешь с точки зрения prepared statement, если хорошо понимаешь, что делаешь. Есть такой профессор — Энди Павло, очень хороший учитель, нестандартный подход к преподаванию. Он часто использует термин «лучший латвийский эксперт по оптимизации SQL» — это референс к конкретному человеку. Если у тебя есть такой эксперт и ты всё правильно сделаешь, то с обычного ноута — даже не с армянских маков — можно 200 тысяч. А на армянских маках, если прям задаться целью, ближе к миллиону на ридах. С записями — сотни тысяч. И это ещё вопрос метода доступа: если ты батчами фигачишь, там можно и миллиард. Я говорю про отдельные транзакции.
[50:11] Александр: Транзакции — это всё-таки транзакционный доступ, да? То есть OLTP versus аналитика. Обработать миллиард строк аналитики, посчитать по ним COUNT, можно за секунду, потому что ты одним запросом считал, векторными инструкциями сложил — и это миллиарды в секунду. А если OLTP — это десятки тысяч. Такие цифры — нормальная нагрузка. А когда некоторые инженеры говорят: «У нас полторы тысячи RPS, это капец, нам нужна распределённая база»…
[50:46] Стас: Ну, RPS разные бывают. Полторы тысячи RPS тоже может быть много.
[50:49] Александр: Я в плане, что надо всегда смотреть, что за этими полутора тысячами. Если это десять отдельных транзакционных запросов Postgres, то больше ты и не сделаешь.
[51:01] Стас: Про 30 тысяч — тут основное время у тебя будет проводиться не столько в базе, сколько в операционной системе: ожидание данных из сетевой карты и потом пробуждение твоего потока или процесса. Эта константа — 30–40 тысяч запросов в секунду в одно соединение, если ты делаешь честный пинг-понг без батчей, request-response, — она будет одинаковой во всех базах. Возьмёшь Redis, выключишь pipeline — будет такой же пинг-понг. Напишешь на C или C++ кэш-сервер и будешь к нему делать пинг-понг — те же 30–40 тысяч. Это больше связано с частотой процессора и современными операционками, чем с тем, что ты делаешь.
[51:50] Стас: Следующая константа, которую полезно держать в голове, — Java-вская ConcurrentHashMap. Это миллионы запросов в секунду. Если у тебя multicore-система — миллионы, и, наверное — давно я не делал этот эксперимент, — ближе к десятку миллионов на каком-нибудь современном хорошем ARM.
[52:11] Александр: То есть смотри: были в разрезе сеть, процессор, чуть-чуть оперативки, возможно, в какой-то момент диск, вот этот end-to-end пинг — 30 тысяч, разумный предел. В один поток без батча; можешь делать больше потоков, у тебя больше ядер. Иногда можешь батчевать, иногда нет — миллионы запросов в секунду можно выжать при правильной архитектуре. Мы говорим про какую-то константу, как скорость света: сколько фотонов пустишь в пучке — твоё дело, столько полезной нагрузки и сделаешь, но быстрее скорости света не разгонишь. Если есть предел пинг-понга одной транзакции без оптимизаций — это 30–40 тысяч. Потом убираем сеть, убираем диск, оставляем оперативную память процессора, кэши, тот же паттерн доступа, но путь сильно короче — тут уже миллионы, десятки миллионов.
[53:18] Стас: Да, и это будет та же скорость, если ты, например, можешь батчевать. Всё, что про эти 30 тысяч в секунду, — это очень быстрая сетка, когда ты условно в одной стойке с правильными свитчами. Если ты будешь то же самое делать из Европы в Штаты через океан, то, естественно, не будет 30 тысяч на такой request-response. Это 30 тысяч про то, что внутри одной стойки или между VM-ками, даже на одной физической машине. Если можешь батчевать и не трогаешь диск, то улетает в скорость Java ConcurrentHashMap. Я делаю референс к ней, потому что не так много именно multi-threaded хэш-мап: почти во всех стандартных библиотеках — в тех же плюсах — хэш-мапы обычно single-threaded, в Rust тоже в крейтах надо искать concurrent hash map. Это единственное, по-моему, из стандартных библиотек, где оно есть; можно легко бенчмарку написать. Если ты батчуешь транзакции, то правило про 30–40 тысяч раз в секунду в одно соединение остаётся, но за один round-trip можешь сделать достаточно много, в зависимости от транзакции. И это всё было про маленькие транзакции.
[54:44] Стас: Можно поговорить про обратное — какая это скорость. Если у тебя миллион запросов в секунду, другой вопрос, должен ли ты направлять его в один сервер. Можешь, но дальше начинается вся история про радиус поражения. Может, тебе лучше размазать по кластеру, и тут уже больше вопрос про семантику. Если система такая, что при поломке одной физической машины всё встало, — тогда лучше на одной машине: вероятность поломки одной машины меньше, чем вероятность поломки одной из десяти.
[55:26] Александр: Да, но это если для тебя поломка — что-то, из чего ты незаметно восстановиться не можешь, не можешь сделать graceful degradation. Если у тебя на domain-lang 10% кастомеров не могут пользоваться сервисом — это условно лучше, чем если она умерла и все не могут.
[55:45] Александр: Если мы говорим про распределение — одна машина и много машин, — тут есть ряд ключевых вещей, некоторые ты назвал. Зачем в целом распределять данные? Первое — да, может быть какая-то нагрузка, предел одной машины, для которой мы уже сделали всё возможное, чтобы не было препятствий на пути этой «скорости света»: и сбатчевали, и запайплайнили — пришли к теоретическому пределу. Давайте теперь сделаем вторую машину, такой дизайн, и X2 сможем держать больше нагрузки.
[56:28] Александр: Дальше, естественно, если мы не говорим про транзакции, джойны, блокировки — а это чаще всего именно то, что влияет на плохой перформанс большинства приложений: транзакция взяла lock на всю таблицу, и все остальные ждут, будет у тебя 100 RPS.
[56:43] Стас: Ну да, важная оговорка, что ты про какие-то точечные запросы. Как только ты начинаешь делать что-то более серьёзное, что анализирует много строчек, блокирующий доступ, — естественно, все цифры другие.
[56:57] Александр: Да, мы в разумном кейсе это рассматриваем. Распределять можно для повышения пропускной способности — чтобы светить не одним лучом, а двумя, освещать в два раза больше пространства. Для чего ещё distributed можно сделать? Чтобы повысить отказоустойчивость системы, если она задизайнена так, чтобы восстанавливать отказы. Если у тебя на одном сервере задеплоен Postgres, ты сам его поднял, и это вся система — вероятность того, что она откажет, допустим, 10% в час. Если мы делаем 10 машин, вероятность отказа считается по-другому и будет больше 10%: больше частей, которые могут отказать. Вопрос «какова вероятность, что в ближайший час одна из машин откажет» даёт вероятность сильно выше, чем когда машина одна. Но если система умеет восстанавливаться от отказов — есть репликация и так далее, — то задай вопрос на уровне системы: какова вероятность, что откажет вся реплицируемая система? Она сильно меньше, чем у одной ноды. То есть система становится отказоустойчивой.
[58:20] Александр: Такая система будет поддерживать реплики, распределённые транзакции, распределённые коммиты. Отказоустойчивость — второй фактор, который говорит за то, чтобы распределять. Если мы не можем себе позволить на одном сервере хранить данные, нам нужно 100% хранить их распределённо, чтобы на нескольких дисках bit rot нам данные не испортил — надо реплицировать.
[58:49] Стас: Ну, RAID-диски тоже в эту сторону. Хотя одну систему можно и реплицировать, и RAID-диски использовать. Но бывают большие датасеты, много транзакций, разные датасеты: какие-то очень легко шардировать, какие-то нет. Классический сценарий — если у тебя социальная игрушка или соцсеть, у тебя есть user ID, это часто очень хорошее шардирование, можно дизайнить систему вокруг этого. Другой вопрос, откуда вообще прилетают все эти требования про количество данных. Иногда ты работаешь в Google и делаешь сервис, который мы знаем, что запустим, нальём трафиком, и можем прикинуть, сколько там. Особенно когда есть предыдущий сервис, с ним что-то не так, и принято решение переписать заново — ты прям знаешь трафик, знаешь, сколько CPU живёт. Тогда можно так дизайнить. А часто мы начинаем проект с нуля, и тут начинается продуктовая история. Люди часто думают: «Каждый в мире будет использовать мой сервис, там 8 миллиардов людей» — и из этого считают нагрузку.
[1:00:04] Стас: С какой-то вероятностью 8 миллиардов людей не будут использовать твой сервис. Если про советы — мне кажется, надо выбрать базу данных, которую ты хорошо знаешь. Неважно, распределённая или нет: экономь время, используй то, что знаешь. Не знаешь никакой — используй файлы или ещё что-нибудь. Шипни продукт, а там посмотрим. Обычно нераспределённое программирование просто проще. Я обычно с такой точки зрения про это думаю: начни с чего-то простого, потом сможешь это сэволюционировать. С другой стороны, не надо слепо использовать этот аргумент: иногда ты действительно знаешь, какая будет нагрузка. Особенно если была предыдущая система, которую ты переписываешь, или у тебя есть маркетинговый бюджет, который точно приведёт к какому-то трафику, — ты можешь его прикинуть, поговорить с людьми.
[1:01:03] Стас: Есть статья про то, как Google написал Spanner. Есть человек по имени Джефф Дин в Google, который присутствовал при написании почти всех их основных систем, — очень легендарный чувак, один из авторов Spanner. И где-то в предисловии он говорит — это немного другой угол, не столько про нагрузку, сколько про то, какие гарантии тебе база даёт. Мысль такая: мы написали MapReduce и начали использовать его во многих системах, и заметили со временем, что в MapReduce нет никаких транзакций. Не говоря уже о том, что такое транзакция и что конкретно тебе из набора гарантий нужно — это отдельный вопрос, но транзакций там нет ни в каком виде. Окей, ты начинаешь писать сервис, у тебя очень понятная задача, они тебе не нужны. Но постепенно сервис эволюционирует, и они становятся нужны. И они заметили, что потом эти 50 сервисов, написанных так, постепенно обросли самодельными наколеночными транзакциями, которые люди делают в приложении — у этих подходов есть даже свои названия.
[1:02:22] Стас: И поинт, который Джефф Дин делает: вот почему мы написали Spanner. Кажется, со временем лучше, чтобы система была изначально чуть более медленной, чуть менее эффективной по железу, но давала тебе такие гарантии, что потом не нужно будет переносить сложность из базы в код. Эта сложность — имплементация каких-то околотранзакционных вещей — будет одна, хорошо протестированная, в базе, по сравнению с тем, что она будет в десяти разных приложениях, где люди будут тратить ресурсы на то, чтобы дебажить эти имплементации. Давайте унесём это в базу — распределённую, но в которой мы тестируем этот набор компонентов. И даже если вам в первый день это не нужно, то с некоторой вероятностью потом будет нужно, и лучше делать так. Так что могу сослаться на авторитета.
[1:03:21] Александр: Spanner — вообще хорошая система, интересная.
[1:03:24] Стас: Да, он не очень коммерчески успешный за пределами Google, но в Google им много пользуются. Мне кажется, разработчики, которые им пользуются, обычно скорее были недовольны: user experience страдал, много чего нельзя. Но тут вопрос, насколько это фундаментальное ограничение — какие-то вещи в распределённой системе ты не можешь делать так же, как в обычной, — а где просто не сделали. Есть бэнд людей, которые пишут Spanner, им нужно какое-то время, чтобы догнать по всем фичам базы, которые очень давно писались.
[1:04:02] Александр: Да, это тоже фактор, который мы часто забываем: технологии не сами по себе в вакууме появились, а пишутся людьми. Иногда полезно взглянуть на свой проект — на работе или личный: он сразу был весь фичастый, или сначала было что-то одно, потом добавили, потом убрали? Это же развивающаяся система, такая же, как все остальные. В моменте говорить «а вот здесь нет поддержки хранения JSON’ов» — стандартная фича, которую в целом всегда можно реализовать потом, — это аргумент в текущем моменте, но не вообще. А то, что нет распределённых джойнов, каких-то эффективных, — вот это уже может быть фундаментальным ограничением, просто потому что данные могут так лежать, архитектура хранилища может быть так выстроена. Такое, по-моему, было в какой-то момент в ClickHouse — сейчас вроде пофиксили, но эффективных джойнов, по-моему, до сих пор нет, просто потому что дизайн системы другой. И вот это уже может быть аргументом при выборе. Или транзакции уровня serializable, эффективный snapshot isolation, как в Postgres, — в ClickHouse, может, и появится, но маловероятно. То есть есть фундаментальные ограничения системы, а есть просто ресурсные. Понимать это тоже важно.
[1:05:36] Стас: ClickHouse и Postgres — ты затронул ещё одно измерение, не про distributed или non-distributed, а про то, пытаешься ли ты покрыть все кейсы работы с данными или нет. Базы вроде Postgres или Oracle пытаются: ты можешь делать всё что хочешь, и продуктовая поверхность гигантская. И это значит, что какие-то вещи ты делаешь неоптимально. Ты можешь сделать их оптимально, но за счёт того, что такая большая система и backward compatible — надо всё делать и не делать регрессий в существующих юзкейсах, — тебе просто сложнее и медленнее это сделать. Приводя в пример того же Stonebraker’а: он когда-то сделал презентацию, что вот Postgres и похожие системы — такие all-in-one, где ты в одной базе делаешь и аналитику, и транзакции, и научные расчёты, — сильно сдерживают инновации в каждом из юзкейсов. One size fits all — ты не можешь сильно заточиться под юзкейс. И он нарисовал такой треугольник: нужно на самом деле сделать три базы, — и сделал три компании. Это VoltDB для транзакций, Vertica для аналитики и ещё SciDB. Честно говоря, я никогда SciDB не пользовался, но разговаривал с людьми, которые над ним работали: там, по-моему, гражданами первого класса были матрицы, много вычислений с матрицами. Из этих трёх, по-моему, Vertica оказалась достаточно коммерчески успешной, VoltDB и SciDB — не очень. Они пострадали от того, что Snowflake появился, но в какой-то момент чувствовали себя хорошо.
[1:07:44] Стас: И ClickHouse тоже — если делать этот треугольник, он где-то у вершины: очень специфичный вид аналитики, где у тебя SQL, sequential scan, редкие джойны с небольшими табличками. То есть ты пытаешься научиться делать одну вещь очень хорошо, а не быть general-purpose аналитической системой. Я не знаю, какие у них планы. У них сейчас очень много ресурсов, они очень доступны — их легко поспрашивать в Telegram-каналах. Возможно, они как раз будут пытаться играть в более обобщённую аналитическую систему с обобщёнными джойнами и тем самым конкурировать со Snowflake. Кстати, Snowflake тоже очень острую конкуренцию с Databricks ощущает. Сейчас кажется, что производная роста у Databricks получше, а Snowflake сильно потеряли в оценке — были проблемы именно с бизнесом. Они как раз поменяли серьёзную часть руководства, много людей поменялось — это было признание того, что что-то не работает. Не про продукт, скорее про go-to-market компании: как они продают свой продукт.
[1:09:00] Александр: Прям отлично рассказываешь. Действительно, когда я про ClickHouse говорил про эти джойны, я сам понимаю, что они же могут их добавить — у них есть ресурс. Ты правильно говоришь: с точки зрения архитектуры они специально сделали так, что у них всё под этот use case, они не делали сложного оптимизатора.
[1:09:22] Стас: Любой проект может добавить что угодно, но это архитектурное изменение — долго для них. Мне их продукт как бы говорит: «нет и не собираемся». Можем, конечно, всё можем, можем и операционку из него сделать, но не собираемся. Так же и с обобщёнными джойнами и оптимизатором. А Snowflake, наоборот, начиналась как компания-оптимизатор: первые люди, которые приходили, — очень хорошие специалисты по оптимизации запросов. И это очень математически ёмкая область.
[1:09:57] Александр: Это такая и математика, и алгоритмическая, и computer science в целом.
[1:10:04] Стас: Да, нам нужно уметь читать всякие формулки, хорошо понимать статистику, и оно всё равно не работает в конце — поэтому ты расстраиваешься. Есть статья года, наверное, 2018-го — ретроспектива про то, насколько хороши наши оптимизаторы. Это очень хорошая область для computer-science-ресёрча: всегда есть что написать в статье, всегда что-то поломано, нужно какой-то тип запросов предсказывать лучше, по нему кардинальности. Много статей написано — это кормит ресёрчера. Народ сделал интересное сравнение: давайте посмотрим, насколько все сейчас оптимайзеры хороши. Взяли набор запросов по какой-то методологии, и там условно на четверти запросов ты ошибёшься на два порядка. Столько человеко-часов в это зарыто, и всё равно можно ошибиться в 100 раз. Задача в целом не то чтобы сильно решаемая.
[1:11:06] Стас: Ты ведёшь статистику по распределению значений в колонках, а потом пытаешься предсказать что-то про джойны. А джойн — это двумерное распределение: ты потеряешь всю информацию о том, что в отделе А сидят мальчики, а в отделе Б девочки. Как оптимизатор ты будешь предполагать, а информации в виде двумерной гистограммы у тебя не будет — будут две одномерные: сколько мальчиков и девочек в компании и сколько людей в каждом отделе. А то, что один отдел — бухгалтерия, другой — инжиниринг, и в первый попадают одни, во второй другие, — про это оптимизатор пропустит, будет предполагать. Адресовать это можно по-разному: можно заставлять людей создавать двумерные гистограммы, много чего делать, но задача такая ill-defined.
[1:11:59] Стас: Кстати, у Databricks большая аджента — давайте засунем сюда много ML. Тут есть два утверждения. Одно: мы выполнили запрос, он ошибся в сто раз в предсказании, сколько строчек будет, мы выкинули эти результаты. Но в самом этом предсказании — в том, что он ошибся в сто раз, — зашита информация, что данные распределены неравномерно. Это очень полезная информация, которую мы выкидываем. Если ты на ней постепенно учишься — неважно, какую модель туда засунешь, — постепенно выучишь все эти многомерные распределения, которые появляются из джойнов. Другое направление: если ты берёшь сервис типа Databricks или Snowflake — тут аккуратный вопрос про то, с чьими данными и какие гарантии переиспользования для обучения, — но в целом, когда у тебя multi-tenant архитектура, оптимизатор может посмотреть и на названия колонок, научиться не только на твоих данных, но и на других, а потом ты его будешь переиспользовать. Такая ML-система: чем больше данных для обучения, тем лучше работает. И с оптимизаторами, которые не всегда называли ML, но по сути это было тем же самым — сложная статистическая модель, где ты пытаешься предсказать кардинальное количество строчек, чтобы выбрать оптимальный план, — ML сильно помогает. И то, что это сервисы, хранящие данные разных пользователей, тоже помогает. Хотя барьер: можешь ли ты данными условного BMW обучить свою модель? Скорее всего, BMW будет против.
[1:13:59] Александр: А есть у тебя примеры уже рабочих систем, где действительно извлекают ценность из этих миссов в предсказании и обучаются, и в следующий раз эффективность выше? Это теоретическая гипотеза или уже работающая система?
[1:14:19] Стас: Есть условные экстеншены для Postgres. Не знаю, есть ли хоть одна база, которая выпустила это в продакшен. Оно нормально работает, другой вопрос, что всё равно постепенно прогревается. И часто система, которая предсказуемо плохо работает, лучше, чем та, что иногда плохо, иногда хорошо. Мне кажется, это основной барьер: да, ты изначально медленно работаешь, потом научишься быстрее, но это до тех пор, пока не рестартанёшь, не потеряешь статистику, не появится перекос в данных. Но, кажется, плюсов всё равно больше, чем минусов, это eventually появится. Тут тоже, наверное, нет фундаментальной причины, почему не сложилось. Не так много людей этим занимается — было несколько ресёрч-групп. В Postgres встраиваться сложно. С точки зрения ML это скорее инженерная задача.
[1:15:24] Стас: Да, она очень простая. В тех случаях, которые я знаю, это был вообще KNN — поиск ближайших соседей. Работает лучше, чем что-то более сложное.
[1:15:36] Александр: Интересно. Мы сейчас немножко копнули в оптимизаторы. У меня есть несколько выпусков про оптимизаторы — ко второму сезону вас отсылаю, если кому-то хочется действительно понять, как это работает, или было сложновато воспринимать сейчас. Мне кажется, мы так сошли с распределения данных в оптимизацию запросов. Давай сделаем такой мыслительный эксперимент — или не мыслительный, а в историю посмотрим. С точки зрения инженера: мы много говорим про бизнес-кейсы, и я понимаю, что это, наверное, даже важнее в большинстве случаев, но большинство слушателей подкаста — инженеры. И какие-то инженерные решения, именно как распределить данные, будут сильно интересны. Давай возьмём базу данных, одну, Postgres-like, которая стоит на одном сервере и прекрасно работает с точки зрения OLTP. Давай сейчас ограничим, что мы не делаем аналитику, и наш бизнес — интернет-магазин. Мы сделали свой стартап, знаем Postgres, используем его, начинаем расти. Продуктовая гипотеза сработала, нам дали инвестиции, мы вложились в маркетинг, повалили пользователи. Happy case. Было на стадии тестирования и MVP сотни взаимодействий в час, а теперь растёт: люди начинают чаще пользоваться корзиной, откладывать, совершать покупки — OLTP-сценарий. И мы понимаем, что скоро система захлебнётся. Какие у нас есть варианты с точки зрения инженерной, куда посмотреть? Есть Snowflake, но это аналитика. Есть Spanner, есть CockroachDB. Как нам мыслить?
[1:17:43] Стас: Окей. В том сетапе, который ты описал: начинает расти трафик, растёт CPU на базе, и мы видим, что через какое-то время максимальный размер этого CPU перевалим. Тут надо, с одной стороны, покупать себе время — можно ещё начать смотреть свои запросы, оптимизации тоже один порядок себе выиграть. Трафик растёт, мы знаем, что перерастём одну систему. Надо сначала покупать время: увеличивать размер инстанса, оптимизировать общение с базой прямо в моменте, потому что если хочешь что-то серьёзно переписать — это тоже время. Такое вертикальное масштабирование.
[1:18:26] Стас: Дальше это сильно специфично. Наверное, я немного не про инжиниринг хранилищ, сколько про developer experience. Идеально было бы так: ты нажал кнопку, и все те же запросы теперь работают, система как-то распределяет данные, ты об этом ничего не знаешь, и всё работает. Такого сейчас нет. Я думаю, здесь тоже нет фундаментальных причин, почему это невозможно — мне кажется, на каком-то масштабе десятилетия оно появится. Там дорого и долго делать, потом по фичам догонять. Но если вопрос про сейчас — да, надо шардировать. Если у тебя есть какой-то хороший ключ шардирования — если он есть, надо его использовать. Можно шардировать из приложения: если кросс-шардовых взаимодействий мало, у тебя есть ключ типа пользователя или тенанта — самый прямолинейный подход. Так работает Facebook: там миллионы SQL‘ей под сайтом, шардированные по user ID.
[1:19:36] Александр: Про шардирование давай, кстати. По-моему, я в подкасте подробно механизм шардирования не разбирал. Он интуитивно довольно простой, но когда перед тобой реально стоит задача сделать шардирование, чтобы оно работало, начинаешь глубже погружаться. Как это сделать с точки зрения того же Postgres? Насколько я знаю, шардирование происходит довольно топорно: под каждый диапазон делаешь таблички и физически их разносишь.
[1:20:04] Стас: Ну, Postgres просто не шардированная база. Это один инстанс. Там есть то, что называется партиционирование: ты можешь сделать одну логическую табличку, но физически будут разные таблички. Один из юзкейсов — когда у тебя time series данные: если бы ты в одну табличку положил, тебе больно было бы удалять. Хранишь данные за три месяца, нужно последний месяц удалить — придётся перелопатить табличку, будет импакт. А если у тебя каждый месяц — своя табличка, ты просто файл удалил. Это уже очень специфичная проблема — workaround вокруг существующих проблем баз данных. В идеале ты не должен об этом заботиться и просто удалять данные, где created_at меньше какого-то отрезка времени.
[1:21:06] Стас: Про шардинг я говорил про application-level шардинг. Очень открытый вопрос — как расшардировать абстрактное приложение, но один из паттернов, который часто встречается: люди шардируют прямо из приложения. Ты знаешь user ID в приложении, у тебя есть 10 баз — берёшь остаток от деления user ID на 10 и идёшь в определённую базу. При этом сама база ничего не знает про другие базы. Но начинаются проблемы: миграции делать сложнее, какие-то вещи ты не можешь сделать. Остаётся открытым вопрос: что сделать, когда можно поставить 11-й сервер, и надо как-то перебалансировать. С этой проблемой обычный подход — ты заводишь логических шардов сильно больше, чем физических, и где-то поддерживаешь маппинг. Почти все системы, реализующие шардирование, так делают: где-то это vnodes называется, где-то ещё как-то. Логических шардов у тебя условно 10 тысяч, и ты наливаешь эти логические шарды на физические. 10 инстансов базы, 1000 логических шардов — 100 логических шардов на каждую базу. Когда появится новая база, ты этот маппинг-уровень просто перестроишь, данные отреплицируешь, чтобы они уехали на 11-й сервер. Этот промежуточный уровень метаданных чуть более гибкий, чем просто остаток от деления. Остаток от деления жестокий к тебе: если ты поменял с 10 на 11, у тебя все данные поехали, очень много всего нужно переделать.
[1:22:50] Стас: Есть ещё подходы. Альтернативная вселенная — consistent hashing. Не буду рассказывать, как устроено, но идея в том, что это замена остатка на 10 на чуть более сложную формулу, которая позволяет менять количество шардов. Тебе нужно будет переместить данные, но локализованные — перевести из одной части системы в другую. Про consistent hashing часто говорят в шардировании, но это чуть менее гибкий подход. Если у тебя есть уровень метаданных, где ты явно мапишь шарды, он более гибкий: например, у тебя появится очень важный юзер. Ты Instagram, у тебя есть все юзеры, а есть Джастин Бибер; ты Twitter, есть все юзеры, а есть Илон Маск. Если у тебя есть метаданные, где ты можешь явно шард с этим юзером подвинуть на какой-то сервер, где будут вообще другие правила жизни, этим проще оперировать, чем если у тебя формула. Опять же, обычно делают и consistent hashing, и промежуточный слой. Но если просто формула говорит, какие данные куда едут, тогда у тебя немного связаны руки. Это скорее история про торрент-like сценарий, когда никакого оператора нет: тогда тебе нужно, чтобы система сама куда-то возила данные, процент сбоев сильно выше, тогда хороша полностью автоматизированная система, которая живёт по своим правилам и человек не нужен.
[1:24:35] Александр: Хорошо, мы поговорили довольно подробно про шардирование, сделанное больше силами и мозгами разработчиков: система вообще не знает, что такое шардирование, она просто представляет таблички на разных серверах, где все юзеры с остатком от деления на 10, равным 1, — вот здесь, равным 2 — там, а для базы это просто айдишник и айдишник.
[1:25:06] Стас: Да, и с этим будет много проблем. Есть инженерные задачи, с которыми сталкиваешься: ты не сджойнишь данные из разных баз средствами баз данных, тебе нужно делать это на стороне сервера приложения, а это уже довольно сложная задача, и проще её вообще заскипать — «мы не умеем, отстаньте, давайте переливать в другую базу».
[1:25:28] Александр: Если мы делаем следующий шаг: сами мы не хотим, у нас недостаточно ресурсов разработчиков, чтобы этим заниматься, — рассматриваем альтернативу расширить наш Postgres на одном инстансе. Что у нас ещё есть из решений? Postgres-like — что есть?
[1:25:49] Стас: Есть всякие, как CockroachDB, есть штуки типа TiDB, есть в Amazon… Redshift?
[1:25:59] Александр: Нет, Redshift — это аналитика.
[1:26:03] Стас: А есть в SQL такая штука… Не DynamoDB?
[1:26:06] Александр: Dynamo — да, это не SQL.
[1:26:08] Стас: Там такой язычок запросов, понятные гарантии, понятные ограничения. Dynamo очень много используется в самом Amazon для метаданных. Хорошая система, хорошо масштабируется, Amazon очень хорошо умеет её оперировать, достаточно стабильна. Что ещё? Для MySQL есть PlanetScale — в каком-то смысле конкурент Neon, но на MySQL-рынке, а мы на Postgres-рынке. Ребята из GitHub сделали — GitHub был на MySQL, там движок под капотом, я забыл, как называется, но это, скажем так, MySQL с плюшками. Базы типа 100+ терабайт у них есть, но тоже много ограничений. Есть всякие роутеры над Postgres, которые часть логики уносят в свой роутинг, но ничего такого, что я бы прям рекомендовал. Из того, что прямо шардируется, — и если шардирование вам по каким-то причинам нужно, потому что тоже вопрос, сколько шардов система переживает, — из действительно большого, какие-то сотни и тысячи нод, это Dynamo и Spanner, наверное. Mongo — это, наверное, всё-таки про десятки нод. Я не знаю их максимальный кластер, наверное, он сильно больше, но кажется, что во всех прям больших кластерах какое-то количество человек участвует в том, чтобы его затюнить, не то чтобы оно прям задизайнено для этого. Не знаю, у тебя, может, больше опыта?
[1:27:41] Александр: Нет, с точки зрения существующих кластерных продуктов не могу назвать какой-то, который прям рекомендую, — возможно, потому что не сталкивался. У меня не было такой задачи: есть Postgres, нужно отмасштабировать, переехать — и я это прошёл с положительным подкреплением. Я чаще либо такие системы разрабатывал, либо приходил, когда всё уже переехало и никаких проблем нет.
[1:28:11] Александр: Я в плане, что мы до этого смотрели инженерное решение на стороне разработчиков, а теперь переезжаем на решение, где говорим: давайте это решать не будем, за нас решает база данных. Нам проще в плане костов на разработку, скорее всего, база будет платная, часть ресурсов туда отдадим, но нам ок. И мы перечисляли решения — DynamoDB, Spanner, CockroachDB, — которые в целом работают паттерном как Postgres, возможно, где-то нет SQL, но это OLTP-like мир, и для интернет-магазина подходит. Вопрос больше денег и каких-то тонких фич. Но какие на уровне базы данных есть интересные инженерные решения? Например, Postgres лопается по количеству данных и по CPU. Какие инженерные решения приняты в этих трёх базах, чтобы это ограничение переступить, и какими костами — какие новые ограничения добавляются?
[1:29:43] Стас: С точки зрения разработчика баз данных мало что меняется в том плане, что вся эта история с distributed-запросами и транзакциями — по большой части надстройка сверху. В конце концов, тебе прилетит какая-то теперь уже часть запроса, которую нужно выполнить по тем же самым правилам. Иногда планировщик глобальный, иногда локальный — у тебя появляются новые проблемы, но чтобы прям что-то переписывать, такого мало. Иногда бывают тактические решения в коде, которые стоят на пути и будут проще, если мы перепишем A на B, но это не принципиально. Если делать примеры из Postgres: там достаточно экстравагантный метод трекинга снапшотов, который позволяет избежать записи на диск ещё одной мапы, а другие базы так делают. Это сильно стоит на пути многого, но это надо одну структуру данных на другую заменить — и так, и так можно, просто в новых условиях с другой структурой чуть проще жить. А так — это надстройка сверху.
[1:30:56] Стас: Тебе нужен какой-то distributed execution, и он очень похож на application-level sharding: если запросы локальные, тебе нужно понять, идеально на каком-то координаторе, куда какой шард отсылать, если запрос касается одного шарда, — и дальше вся история точно такая же. Если распределённый запрос, тебе нужен ещё какой-то распределённый execution, чаще всего это сведётся к так называемому shuffle join. Пример — Spark SQL, который не OLTP-база, а аналитическая, и там с первых дней всё делали distributed. По большому счёту, всё, что касается distributed joins, — самый хороший перформанс ты чаще всего получишь через shuffle join: например, распределить большую табличку по всем нодам, у каждой будет свой range, и поджойнить её. Если ты джойнишь две большие таблицы, каждая из которых не помещается на ноду, вариантов не так много: тебе нужно временно для этого запроса перешардировать свои таблицы, локально поджойнить и всё смёржить. Тут можно отослать слушателей к 100TB Sorting Benchmark, где люди соревнуются, как быстрее всего отсортировать 100TB на каком-то количестве нод. Почти всегда сейчас алгоритм — просканировать правильно, раскидать по нодам, и дальше сортировку локальную сделать везде. Есть и другие подходы, но на текущем железе и сетках это хорошо с точки зрения сетей: каждая нода посылает результаты всем.
[1:32:59] Стас: Условно, если у тебя идеально перемешанные данные, 10 нод, есть табличка с какими-то именами, они хорошо перемешаны, и ты хочешь сделать этот shuffle join — по сути нужно отсортировать по какому-то ключу. У тебя есть 1TB от этой большой таблички, у тебя будут данные, которые нужны каждому шарду, если ты хочешь, чтобы сквозь шарды оно было отсортировано. 9 соседей есть, и ты бьёшь свои данные на 10 кусочков, понимаешь, что один кусочек — мой, первый кусочек — тому серверу.
[1:33:39] Александр: Объясни поподробнее. По какому-то критерию, вот у нас 10 нод, нам нужно взять свои локальные куски и отправить всем девяти, но понять, кому что отправлять, — как это происходит?
[1:33:54] Стас: Джойним две таблицы A и B по ключу X, и они обе не отсортированы по ключу X. Всё, что нам нужно, — обе отсортировать по ключу X и сделать merge join. Ни A, ни B не помещается на одной ноде, поэтому мы можем делать сквозную сортировку: нода D1 отвечает — как телефонная книжка или словарь — за диапазон от А до В, следующая от В до Е, и так далее, последняя от Э до Я. И каждая нода, когда смотрит на свои данные, если они не были отсортированы по алфавиту в этом примере, у неё будет какой-то кусочек данных от А до В и так далее. И когда ты для запроса это распределяешь по нодам, ты пошлёшь один кусочек туда, другой туда. Это важно, потому что ты используешь N-квадрат соединений между нодами, и свитчи часто в такое умеют. Я примку там гигабит, 10 гигабит, 100 гигабит. Но бэкплейн свитча часто умеет больше — чтобы эти 100 гигабит между какими-то парами работали. Он редко даёт тебе полный квадрат соединений: не все смогут одновременно писать в свитч 100 гигабит друг к другу, даже непересекающимися парами. Условно, у тебя 50 нод, каждая первая с 51-й, и так далее, — свитч всё равно раньше остановится, и это очень дорогие свитчи, которые могут дать full throughput. Для distributed-тренинга это важно, там чуть-чуть другие свитчи, но это нечастая проблема.
[1:35:36] Стас: Важно, что ты, например, не шлёшь одни и те же данные всем — тогда ты делаешь не очень эффективно. Другой пример: когда у тебя есть маленькая табличка и большая, можно разослать маленькую всем, чтобы все могли локально с ней сджойниться. Но на практике оказывается, что проще расшардировать большую, хотя это больше CPU, ты перенесёшь больше данных, но они будут параллельно с точки зрения сети — разные рёбра в этом графе связанности нод. А если ты одну маленькую рассылаешь, ты либо делаешь совсем прямолинейно — в цикле шлёшь табличку всем, — либо можешь делать постепенно: отослать одной, потом вы вдвоём шлёте следующей, потом уже четверо, и дальше. Такой алгоритм много где применяется, но я не видел, чтобы его в базах применяли — обычно в цикле шлют, а по факту просто отказываются от этого плана в пользу shuffle join, где большую расшардируешь, и в итоге один план, он быстрее работает.
[1:36:50] Стас: С точки зрения distributed execution — я очень упрощаю, — какой-то блок, которого тебе не хватает: нужны соединения всех со всеми, нужны метаданные в базе, чтобы знать, куда рассылать. После этого distributed execution начинает работать: есть какая-то нода сверху, либо каждая нода может принимать запросы, и у всех есть метаданные, и она знает, как юзерский запрос разбить на подзапросы, посылает всем, где есть стадия локального исполнения. И дальше таких стадий очень много, план может быть развесистый — сотни стадий. Каждый узел этого графа исполнения запроса ты можешь выполнить либо на координаторе, у которого есть запрос от клиента, либо на самом шарде, либо где-то между, потому что либо шард может быть бутылочным горлышком, либо координатор. Промежуточные шаги плана тоже можно запланировать на другие ноды. Это большая задача планирования, ещё одно измерение: тебе не только данные, но и ресурсы планировать. Мало какие базы этим занимаются, но некоторые — SQL Server, Oracle — занимаются, с точки зрения того, каким юзерам какие ресурсы и сколько они могут использовать. А когда у тебя ещё и кластер, начинается Kubernetes-like планирование.
[1:38:31] Александр: Мы поговорили про application-side шардирование, про шардирование, которое базы данных умеют, про shuffle join хорошо рассказали. Тут, наверное, стоит подсветить челлендж shuffle join: данные не обязательно идеально распределены, есть статистические перекосы — не знаю, Александров среди имён будет выше, — и это распределение может сделать так, что 50% данных окажется просто на одной ноде.
[1:39:04] Стас: Да, но если ты под запрос делаешь shuffle, то тебя это не сильно касается. Естественно, тебе нужно сначала локально просканировать данные, если это можно делать в несколько шагов. Первый шаг обычно называют radix: всё, что нужно понять, — сколько префиксов вообще везде. Ты можешь сделать шаг координации так, чтобы, не сильно жертвуя временем исполнения, получить хорошие шарды на выходе того размера, который хочешь. То есть с каждого шарда берёшь гистограммки, собрал их в одном месте, понял глобальное распределение — и теперь точно знаешь, какие куски данных куда послать, чтобы в конце шарды получились одинаковые.
[1:39:46] Александр: Это интересная задача, которая помогает понять, как механизмы работают в распределённых базах и какие там ограничения — например, почему это иногда долго, в том числе потому, что происходит этот шаффлинг. Как это оптимизировать — ты хорошо про свитчи сказал: сеть, скорее всего, может быть узким местом.
[1:40:11] Стас: В тренинге, где нужно очень много пересылать данные, в построении поискового индекса — там тоже такая же задача, как в shuffle join’ах. Это сильно будет constrained — именно сетевая карта или свитч часто.
[1:40:27] Александр: Мы распределили базу данных, тут стоит ещё завершающую тему неглубоко рассмотреть — а что с транзакциями? Джойны джойнами, данные мы сканируем, но транзакции мы как-то ещё… Кусок полностью пропустили — про транзакционный контроль.
[1:40:44] Стас: Есть какое-то количество подходов. Тех, которые сейчас по индустрии применяются, наверное, два основных. Что такое транзакции? Есть классическое определение ACID: атомарность, консистентность, изоляция и durability. Durability — это отказоустойчивость: то, что ты на рестарт не потеряешь. Для распределённых транзакций она ничего особо нового не добавляет: каждый шард либо не должен терять данные, либо иметь распределённую семантику — я могу потерять данные, но у меня есть реплика, на которой они будут. Durability — понятное свойство.
[1:41:31] Стас: Консистентность — это буква, которая была добавлена туда скорее для того, чтобы сокращение хорошо звучало.
[1:41:38] Александр: Ну да, «AID» как-то не очень.
[1:41:42] Стас: Да. Но консистентность в оригинальных статьях объяснена как консистентность с констрейнтами приложения — это больше про foreign key и все эти штуки. Уникальность, NOT NULL какой-нибудь. Это немного другая часть истории. А остаётся атомарность и изоляция — вот эти две важные с точки зрения семантики. Там есть разные уровни изоляции, протоколы. Допустим, у тебя есть два шарда. Атомарность — это что данные или полностью не видны, или полностью видны одновременно. Твои, которые ты сделал, — INSERT. Если он попадает на два шарда, не должно быть ситуации, когда половина транзакции видна читающим транзакциям, а вторая нет. Если ты делаешь это просто с уровня приложения, пошлёшь две независимые транзакции.
[1:42:33] Стас: Простой пример: у тебя база — банковский аккаунт, есть шардирование по user ID и состояние баланса. Мы не говорим ни про какую двойную запись — как хорошо дизайнить схему этой базы, надо подрумяниться. Если у тебя есть просто ID и сколько у тебя денег, и есть кошелёк сверху, который от юзера A переводит деньги юзеру B, и параллельно есть транзакция, которая считает количество денег в системе. Допустим, банковское приложение такое, что нельзя вводить новые деньги и нельзя выводить: всегда одинаковое количество денег, юзеры просто отсылают их друг другу. Если ты делаешь перевод двумя независимыми транзакциями — снял у юзера A, положил юзеру B, — то читающая транзакция, в зависимости от того, первая транзакция сначала завершилась или вторая, увидит либо что денег меньше в системе, либо что больше. Этого хочется избегать. Это скорее про изоляцию.
[1:43:38] Стас: Атомарность — про то, что не будет ситуации, что на ноде A транзакция закоммитилась, а на ноде B нет. Такого не должно быть, потому что базе данных, в зависимости от того, как ты с ней работаешь, вполне законно сказать: я не могу закоммитить эту транзакцию. Например, deadlock detected. Или уровень изоляции такой, что базе законно сказать «не могу сериализовать, попробуй ещё раз» — такой оптимистичный concurrency control. И будет плохо, если ты на одну ноду подослал транзакцию, она закоммитилась, а здесь тебе сказали deadlock detected. Если ты ничего не сделаешь, у тебя данные не на месте.
[1:44:23] Стас: Для атомарности чаще всего самое популярное — двухфазный коммит: ты сначала попытаешься взять с базы данных обещание, что я могу эту транзакцию закоммитить, и это обещание должно быть записано на диске. И ты можешь стартануть либо коммит этой транзакции, либо аборт. У этого протокола есть свои проблемы. Если одна из нод ушла, ты не можешь принять решение. Условно, были три ноды, ты на все три послал транзакцию, из двух получил обещание, что могу закоммититься, а что было на третьей — не знаешь. Пока она не вернётся, ты никогда этого не узнаешь. И тебе нельзя не заабортить, потому что там она могла быть закоммичена, и люди уже увидят результаты. Так называется блокирующий протокол.
[1:45:18] Александр: Нам нужна доступность всех участников коммита.
[1:45:22] Стас: Да, и тут дизайн-решение. Ты можешь сделать протокол коммита неблокирующим: добавить промежуточную стадию, которая изначально называлась трёхфазный коммит. Есть такой протокол, он тоже корявый, в нём есть проблемы.
[1:45:41] Александр: Там, по-моему, timestamp вносится как дополнительная сущность — время, за которое если не ответил, то считается что-то. Ещё одна фаза общения появляется.
[1:45:54] Стас: Да. Но в нём тоже есть проблема — он не даёт того, что обещает, легко найти в нём косяк. Корректный неблокирующий протокол называется Paxos Commit. Он тоже, как 3PC, что на идеальном пути коммита, когда никто не фейлит, тебе всё равно нужно сделать три шага: prepare, pre-commit и commit, или pre-abort и abort. Но он добавляет — вот про что ты говорил — не совсем timestamp, а логические часы, можно сказать Lamport clocks. Его Лампорт написал когда-то, Paxos Commit. Но фаза с pre-commit и pre-abort в теории может продолжаться бесконечно. На практике быстро сходится, но это одна из теоретических вещей: такой trade-off, что нельзя сделать алгоритм, который достигнет консенсуса за ограниченное число шагов. В этих алгоритмах существует расписание, которое бесконечно длится — вы бесконечно спорите друг с другом. На практике это вообще не проблема, тебе нужно прям специально моргать этими нодами.
[1:47:05] Стас: Это очень интересное доказательство того, что нельзя сделать алгоритм коммита с конечным количеством сообщений. По-моему, единственное доказательство, которое я видел, использующее приёмы из того, что называется алгебраическая топология. Они строят достаточно абстрактные геометрические фигуры — не трёхмерные, но в целом это такие политопы, делят их на кусочки, красят рёбра, и, применяя теорему из этой математической области, можно доказать, что не существует такого алгоритма. Это как раз вещи, которые обычно не получается доказать: очень сложно доказать несуществование чего-то. Бывает лучший известный алгоритм такой-то, а сказать «не существует алгоритм» очень сложно. Это одна из областей, где получилось.
[1:47:56] Александр: Прикольно. Это ещё в сторону проблемы двух генералов.
[1:48:00] Стас: Да, это то же самое.
[1:48:04] Александр: Я думаю сделать целый сезон про распределённые системы, как это всё работает, в деталях с визуальными штуками. Так что в деталях слушателям стоит подождать сезона, не буду сильно технически заострять. Но если мы говорим про способы реализации распределённых транзакций и коммита в частности — есть ещё какие-то подходы?
[1:48:34] Стас: Да. Как у разработчика распределённой системы у тебя есть атомарность — не говорю ещё про изоляцию, это отдельная тема. С атомарностью ты либо идёшь в сторону Paxos Commit, либо просто используешь 2PC и говоришь, что у тебя шарды атомарные в плане, что у тебя есть какое-то количество бэкапов, реплик — прямая аналогия с обычной базой. Если primary поломался, ты промоутишься на secondary, и оно работает. Тот же Spanner идёт именно по такому пути: у них обычный 2PC, и шарды промоутятся. Другой вопрос, как ты реплицируешь шарды: там Paxos будет на уровне репликации шардов или Raft, но это уже похожий набор идей, но для другого. Если Paxos Commit решает задачу коммита, чтобы N акторов пришли к одному решению, задача репликации чуть другая, чуть больше — в литературе обычно называется reliable broadcast: свой write-ahead log или какие-то записи можешь всем разослать, и как только они на majority есть, они оттуда не пропадут. Или может быть просто обычная репликация с обычными гарантиями, с каким-то внешним, не знаю, ZooKeeper’ом или внешним демоном, который промоутит primary в secondary. В целом это одно и то же: Paxos просто позволяет не закладываться на тайм-аут и не иметь внешнюю сущность, которая следит и промоутит тебя в primary.
[1:50:16] Александр: Ну давай, мы начали про атомарность и изоляцию — можно про изоляцию сказать.
[1:50:22] Стас: Есть разные возможности сделать изоляцию. Можно делать либо какой-то централизованный сервис, который раздаёт всем снапшоты, и снапшот — это набор правил, по которому ты какие-то транзакции игнорируешь, какие-то нет. Идея в том, что с одним и тем же снапшотом ты можешь читать данные и видеть одни и те же данные вне зависимости от того, кто их как меняет: новые данные ты уже не увидишь. Тут разные подходы, и, наверное, большой класс алгоритмов или систем пытается сделать такой cross-node snapshot isolation — или через центральный сервис, или распределённо. Здесь Spanner, CockroachDB будут все в этой части, и TiDB — как CockroachDB, они последователи Spanner. SAP HANA — есть такая база от SAP, у них тоже есть распределённые решения, которые делают распределённый snapshot isolation другими способами, это другой набор алгоритмов.
[1:51:34] Стас: Есть какая-то абсолютно ортогональная вселенная баз данных, которые пытаются делать transaction ordering. Вместо того чтобы делать распределённые снапшоты, ты говоришь, что у меня транзакции всегда неинтерактивные: ты не можешь сделать один стейтмент транзакции, посмотреть на результаты и сделать следующий стейтмент. Ты всегда транзакцию полностью отсылаешь, и если система может определить read set и write set транзакции, то она может их переупорядочить и послать для execution на шарды. Получается такая виртуальная последовательность: всё выполняется последовательно, ты избегаешь проблемы целиком, с необходимостью иметь снапшоты, но появляются серьёзные констрейнты на то, какие запросы ты можешь исполнять и как. Тут тоже начинается вопрос: проще писать базу данных — пользователям сложнее пользоваться.
[1:52:37] Александр: Ну, с этим реордерингом получается, что snapshot isolation — это multiversion concurrency control?
[1:52:45] Стас: Да. С реордерингом тебе не обязательно иметь MVCC. Его чаще всего будут иметь всё равно, потому что тебе, наверное, нужен будет отдельный способ делать аналитическую транзакцию: с реордерингом важно, что они краткоживущие, каждая транзакция тебе заблокирует pipeline выполнения. Поэтому тебе нужно будет часто отдельный снапшот, но его можно сделать проще, потому что обычно тебе снапшот тогда нужен только read-only, не read-write, и ты можешь просто видеть какую-то неизменяющуюся картину данных. Это, наверное, FaunaDB и YDB — есть такой ещё один проект, из баз данных Яндекса. Вот это подход. Причём они, по-моему, достаточно независимы друг от друга: примерно в одно и то же время появились, и кажется, друг на друга не смотрели. В книжках это давно-давно было описано, но, наверное, это единственные две имплементации. Я даже не знаю, были ли исторические имплементации этого подхода, но есть Fauna и есть YDB.
[1:53:46] Александр: Интересно, может быть, с ребятами из YDB пообщаться, какие-то детали узнать. Запишу на карандашик. Ну вот, про это поговорили. Окей, наверное, всё. Мы написали распределённую базу данных, можно деплоить в продакшн. Шип это.
[1:54:01] Стас: Да-да.
[1:54:03] Александр: Следующий выпуск, я очень надеюсь, состоится. У нас не менее интересный, а даже более интересный, потому что сегодня мы поговорили больше про ландшафт — про разные базы, разные подходы, разные юзкейсы, в целом огляделись вокруг. А вокруг чего мы оглядывались? Вокруг Neon Database. И в детали, тонкости, интересные инженерные решения мы обязательно зароемся в следующем выпуске. Так что, ребят, подписывайтесь, кто ещё не. Будет точно интересно. А вдохновляющий спич-вопрос в конце у меня всегда есть. Но если у тебя конкретно нет чего-то, что бы ты хотел сказать, я бы отослал слушателей к специальному выпуску, который эксклюзивно в Telegram-канале выходит: там мы довольно хорошо про мотивацию поговорили, про общее настроение разработчика — в общем, всё, что не про базы данных, но довольно мотивирующее и вдохновляющее. Но предоставляю тебе слово в конце. Если что-то хочешь сказать слушателям — welcome.
[1:55:12] Стас: Пишите код, придумывайте проекты, шипите проекты. От этого можно получать много удовольствия, когда твоими проектами пользуются. У разных людей по-разному устроено то, от чего они получают больше всего удовольствия. Мы много говорили про то, как сделать какую-то идеальную систему. Делайте неидеальные системы, мне кажется, и проверяйте их на реальных пользователях. Это хороший подход. Не везде и не универсально, но как основной способ взаимодействия с миром, мне кажется, подходит.
[1:55:46] Александр: Кайф, кайф. Спасибо большое.
[1:55:59] Стас: Спасибо.