#49: Serverless Postgres: как работает Neon Database
Александр Пахомов и Стас Кельвич (сооснователь Neon, где он сделал storage layer) разбирают внутреннее устройство serverless-Postgres. Как Neon отделяет compute от storage: `compute` — это чуть подхаченный Postgres, у которого чтение с диска заменено на чтение по сети, а `storage` — свои сервисы на Rust (Safekeeper — консенсусный `WAL` на модифицированном Raft/DiskPaxos, и PageServer — партиционированное LSM-дерево поверх S3). По пути — процессы Postgres-комьюнити (commit-fest'ы, пятилетние патчи, `TLA+`), branching через copy-on-write и запросы к истории по `LSN`, live-миграция виртуалок ради вертикального скейлинга без разрыва соединений, авторизация по `HTTP` для V8-isolate'ов, а также будущее многопоточного Postgres. Это вторая, глубоко техническая часть; обзорная — в выпуске 48.
Главное
- Neon разделяет `compute` и `storage`: compute — это Postgres, у которого чтение страниц с диска заменено чтением по сети, а запись `WAL` идёт в свой storage; storage выглядит для Postgres как реплика на запись и как диск на чтение.
- Storage-слой multi-tenant и stateless-по-compute: `HA` и защита от split-brain живут в storage, поэтому compute-ноды (`stateless` Postgres) можно агрессивно перезапускать в Kubernetes — это резко упрощает эксплуатацию миллионов баз силами 10 человек.
- Durability строится двумя тирами: Safekeeper коммитит `WAL` в 2 из 3 availability-зон (аналог quorum commit), затем PageServer материализует журнал в 8-килобайтные страницы и как можно быстрее сбрасывает их в S3 — «горячую картошку» отдают S3 как фундаменту durability.
- Safekeeper реализует консенсус модифицированным Raft (по сути DiskPaxos): роли разделены — Postgres-compute только пропоузер/лидер, Safekeeper только фолловер; корректность части протокола проверяли на `TLA+` (это model checker, не доказательство — как property-based testing).
- PageServer — партиционированное LSM-дерево, но append-only ради дешёвой выгрузки в S3 (S3 не даёт менять префикс); без garbage collection хранится вся история, поэтому запрос страницы на произвольном `LSN` дёшев — отсюда time-travel, дешёвые исторические запросы и восстановление до точного `LSN`.
- Branching — это copy-on-write: бранч терабайтной базы создаётся мгновенно, данные копируются лишь при записи; это раскрывает preview-окружения на реальных данных, тест миграций против прода и (в разработке) анонимизацию/маскирование данных на бранче.
- Вертикальный автоскейлинг делают live-миграцией виртуалки (QEMU/KVM, pre-copy/post-copy), утаскивая за собой IP через `VXLAN`: `TCP`-соединения, транзакции и контекст сессии переживают переезд с одной Kubernetes-ноды на другую — обычный HTTP-style rescheduling для Postgres не годится из-за долгоживущих запросов и сессионного состояния.
- Форк Postgres у Neon крошечный (10–20k строк, адаптация новой версии — меньше месяца), потому что почти всё вынесено в расширения; будущее — многопоточный Postgres (thread-local вместо static-переменных), над которым команда работает в апстриме, а не в своём форке.
В выпуске
- Стас Кельвич — Сооснователь Neon (serverless Postgres as a service), где сделал storage layer и WAL-стриминг; ранее хакал кластерный Postgres в Postgres Professional и работал в команде баз данных Яндекса. GitHub ↗
Ссылки
- Neon — serverless Postgres as a service
- Socrates: The New SQL Server in the Cloud (Microsoft Research, SIGMOD 2019)
- TLA+ — язык и model checker Лесли Лэмпорта
- pgrx — фреймворк для расширений Postgres на Rust
- Amazon Aurora
- Supabase — Postgres-платформа для приложений
- Выпуск 48: обзор ландшафта баз данных с ко-фаундером Neon
Расшифровка
[00:00] Александр: Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Вы слушаете 49-й выпуск подкаста «Тысяча фичей». В предыдущем выпуске мы вместе со Стасом Кельвичем обсуждали ландшафт рынка баз данных и разбирались в основных подходах к масштабированию этих систем. Если 48-й выпуск прошёл мимо вас, то смело ставьте плеер на паузу и включайте его. Сегодня мы погружаемся вглубь Neon Database: подробно рассматриваем процесс разработки Postgres — ведь Neon построен поверх Postgres, — обсуждаем протоколы консенсуса, долговечную запись на диск, да и просто хорошо проводим время за уютным диалогом двух увлечённых инженеров. В конце Стас ответит на вопросы слушателей из телеграм-канала подкаста. Если вы тоже хотите задать вопрос Стасу и быть в курсе того, что происходит с подкастом, — подписывайтесь на канал, ссылка в описании к выпуску. Вступление сделано, самое время погрузиться в диалог. Приятного прослушивания. Поехали!
[01:10] Александр: Стас, есть интересная статья — по-моему, одна из первых вообще публичных статей Neon, где продукт заявляет о себе, — «Select Hello World». Всем, кому интересно, я советую, ссылку оставлю в описании. Но начать мне бы хотелось примерно с того же, с чего начинается эта статья. Что такое Neon? Привет, мир! Расскажи, пожалуйста, про себя как про продукт.
[01:33] Стас: В той статье мы немного уходим в технические детали. А глобальная идея была такая: есть сообщество Postgres-разработчиков, которые пишут Postgres уже достаточно долго, — примерно одни и те же люди последние лет тридцать. Новые тоже постоянно приходят, и это хорошо, но у них сложился свой консенсус, свои истории. Долгое время основная модель, как эти разработчики зарабатывали деньги, была такая: ты делаешь компанию, которая оказывает консультационные услуги на разном масштабе. Это может быть просто консалтинг — у кого-то установлен Postgres, ты решаешь их проблемы и продаёшь часы. На это можно жить и писать дальше свой Postgres. Либо ты делаешь форк Postgres и продаёшь его уже в более энтерпрайзном виде — как коробочный софт, как продаются MS SQL, Oracle: добавляешь каких-то фич. Причём часто это даже не только фичи. У корпоративного клиента внутренние процессы так устроены, что у него нет опытных DBA, ему важно, чтобы кто-то эти проблемы решал; у него зачастую стоит какая-то база, и он не может съехать на Postgres просто потому, что придётся нанимать людей. Так это и существовало: Enterprise DB, Second Quadrant, Crunchy — у всех чуть разная модель. Crunchy, например, сертифицировали Postgres для компаний, где сложнее регуляция и нужна сертификация — во Франции такого много, в Германии, в Британии; Second Quadrant вообще британская. Postgres Pro в России шёл примерно по такой же модели. И это всё работало.
[03:24] Стас: Но появляется всё больше облаков, и в облаках есть Amazon, у которого это как инсталляция Postgres. При этом люди, которые Postgres разрабатывают, к этому не имеют никакого отношения. Получается, есть разработчики, а есть другая компания, которая это перепродаёт и с разработчиками никак не связана. История не новая: много баз и продуктов поменяли лицензию, чтобы Amazon не мог просто взять твой опенсорсный продукт, захостить и перепродавать. По-моему, Mongo одни из первых. У Postgres лицензия — Postgres License, она даже не BSD, она ещё пермиссивнее: делай что хочешь, можешь закрывать исходный код, можешь открывать, мы отвечаем за продукт. И получается, что есть Amazon и другие облака, которые на этом зарабатывают, и есть разработчики, которые делают всю работу, но немного оторваны. Плюс Amazon лет пять-семь особо не участвовал в разработке: они много чего фиксят, но это оставалось в их приватном форке. Не то чтобы они не хотели делиться — скорее хотели, потому что ребейс на следующую версию так проще, — но процессы были не так устроены. Это, кстати, поменялось: сейчас они гораздо активнее участвуют. Основная идея была — давайте сделаем database-сервис, в котором работают разработчики из core-команды Postgres, это будет только Postgres, и попробуем конкурировать с RDS от Amazon. С одной стороны, странная затея — конкурировать с Amazon в таких сервисах. С другой — есть хорошие примеры. Та же Kafka: есть Confluent, который делает продукт, и есть точно такой же сервис от Amazon, который хостит эту же Kafka. У Confluent получилось выигрывать у нативного сервиса: новые фичи у них появляются раньше, экспертизы больше, — в среднесрочной перспективе это начинает работать.
[05:31] Стас: И мы будем контрибьютить наши изменения обратно в Postgres. Ещё важный момент про стейкхолдеров. У основных компаний, которые разрабатывают Postgres, клиенты энтерпрайзные, цикл продаж коробочный — год и так далее. Из-за этого в самом Postgres приоритизируются фичи, нужные для такого сценария, а то, что нужно для облака, туда не попадает. То есть проблема двойная: во-первых, Amazon зарабатывает на софте, а во-вторых, все, кто Postgres разрабатывают, работают по другой модели — поэтому Postgres не получает фич, нужных облаку. В мире коробочного софта динамически переконфигурировать Postgres иногда надо, но это не критично. А одна из самых больших проблем в облаке — блин, даже не то, что какой-то SQL-фичи нет, а то, что нельзя поменять настройки без рестарта базы. Так что идей две: во-первых, пусть этот сервис делают Postgres-разработчики — это создаёт правильную экономику и повышает exposure проблем облачных сетапов самим разработчикам. Много лет назад большим пользователем Postgres стал Heroku: там были хорошие инженеры, которые все проблемы своего огромного флота Postgres’ов репортили в комьюнити, и Postgres сильно много чего поправил. Надеемся, постепенно с нами будет так же. И всё, что про Postgres, — история долгая, long-term. Проект движется с очень большим throughput, но и с гигантской latency: какие-то фичи, даже не очень большие, спокойно могут занять пять-шесть-семь лет, чтобы их закоммитить. Вот такой обобщённый нетехнический ответ на идею Neon: давайте сделаем хороший Postgres as a service.
[07:43] Александр: Мне кажется, идеальное начало для вводного момента. Как разработчик, я сразу смотрю: ага, бранчик, прикольно, — какое-то Git-like бранчевание, наверное, где-то на CI можно запустить. А когда объясняешь человеку, который больше про бизнес, про саму идею — зачем это делать, — ты говоришь: смотрите, есть core-разработчики с огромной экспертизой Postgres, которые напрямую не участвуют в зарабатывании денег, например, на инсталляциях, которые сейчас on-prem делают другие компании. Модель всем понятная, но работает не так, как могла бы, — где-то есть пробелы. И вот давайте сделаем так, чтобы Postgres-разработчики, которые пишут код десятилетиями, принимали прямое участие в зарабатывании на этом. Очень понятная, простая и прозрачная идея. И хочется в неё чуть-чуть погрузиться, потому что, мне кажется, не все имеют представление, что такое Postgres как комьюнити, как продукт, как там устроены процессы, что такое Postgres Hackers, почему есть люди, которые пишут код, но напрямую денег не зарабатывают. Мне, например, 27 лет, я пришёл в разработку семь лет назад — для меня Postgres уже что-то состоявшееся, опенсорсная программа, которую можно запустить в докере или в клауде. А как он развивается, какая у него история, почему там патчи по пять лет — для меня, такого фрешмена, это странно: в мире, где ты можешь отправить пул-реквест, и его тут же смёржат, если CI прошёл, — какие пять лет? Давай про это поговорим.
[10:00] Стас: В какой-то момент появилась одна из первых таких баз — System R от IBM. Даты я беру из головы, так что перепроверяйте: по-моему, начало 70-х, где-то 72-й год, она начала набирать популярность. Это как раз была реляционная модель, но, по-моему, ещё не SQL — другой синтаксис запросов. Народ начал задумываться, что рынок будет расти, надо делать похожий продукт. И вентчурли, fast-forward лет на десять, появляется Oracle, а чуть позже — в институте Berkeley — база Ingres, её делает Стоунбрейкер. Очень рекомендую: у него есть большое недавнее интервью. Он такой взрослый дядька, лет восемьдесят, и его подробно расспросили, как начинался Ingres, какая была идея. Если коротко: он работает в институте, они со студентами в лаборатории пытаются сделать проект, и его нужно продавать наружу — так тогда работали институты в Штатах. Ты должен приводить соинвесторов с рынка, показывать состоятельность идеи. Стоунбрейкер там много интересного говорит — и про состояние страны, и почему он вообще решил этим заниматься, какие у него были жизненные выборы. Ссылку на интервью найдём в описании.
[11:38] Стас: Ingres, наверное, не был сильно коммерчески популярным. Потом они написали новую систему — Postgres, «вторая система, давайте сделаем лучше по инженерии». Опять же, писали её в основном PhD-студенты, экономической составляющей было немного, потом её, по-моему, продали, а Postgres остался жить своей жизнью. Насколько я помню, вначале он был написан на Lisp — где-то до конца 80-х. Потом его героическим усилием переписали на C, причём не то чтобы очень идиоматически: сделали много механической замены. По-моему, ещё где-то до 2017 года почти весь планировщик и executor были написаны «на листах»: ты делаешь механическую замену — есть список в C, есть все эти лисповые функции, car, cons и остальное, — и всё имплементируешь в C. С точки зрения менеджмента памяти и эффективности по CPU это не самый лучший способ, но в низкоуровневом языке так достаточно удобно работать с высокоуровневыми концепциями; особенно планировщик — логическая задачка, где в итоге получается низкоуровневый код. Так вот, где-то в начале 90-х Postgres переписали на C, назывался он тогда просто Postgres, SQL ещё не было — был язык под названием QUEL, в целом довольно похожий. Если посмотреть на его синтаксис, он даже поприятнее SQL, но SQL уже начал в начале 90-х сильно побеждать: он был в Oracle, и это то, чего хотели пользователи. В какой-то момент парсер запросов переписали на SQL, и тогда, в 95-м, подумали, что Postgres надо переименовать. Было большое долгое обсуждение, разработчики со всех мест спорили, и в итоге выбрали название PostgreSQL. По-моему, лет через пятнадцать все сошлись, что это была самая большая ошибка проекта — ощущайте шутку, но никто не знает, как это произносить: все говорят то «Postgres», то ещё как-то. А главное, на долгом периоде все остальные языки запросов к базам постепенно перестали существовать, так что «SQL» в названии уже мало что сообщает — можно было остаться просто Postgres. Это больше шутка, чем реальная проблема.
[14:23] Стас: Есть неплохие слайды разработчика из Heroku — Питера Ван Хардевейка (я не уверен в произношении), по-моему, он оригинально океанолог, до того как ушёл в IT. Он занимался Postgres’ами в Heroku и сделал очень хорошую презентацию — слайдов сто — про то, как происходило развитие Postgres. Пишут Postgres достаточно разные люди: приходят по-разному, присылают первые патчи, потом кто-то начинает заниматься этим фултайм. Большая часть так или иначе работает в экосистеме Postgres и зарабатывает на этом. Кто-то просто работает над ним фултайм — что-то вроде ретайрмента. Хотя один из самых активных разработчиков, Том Лейн, — это мужик, который, по-моему, когда-то написал основную имплементацию JPEG. Если помнишь: включаешь старый Photoshop или PDF-ридер, выскакивает окошко-лоадер, где перечислены патенты, — во многих из них фигурировал Том Лейн. В какой-то момент он начал заниматься Postgres; тоже мужик в возрасте, но пишет очень много кода. Потом пришёл и бигтех: Microsoft в какой-то момент купил компанию Citus, и сейчас много людей из Microsoft пишут в Postgres. То, что я говорил, — что бигтех не участвует в жизни Postgres — было справедливо, когда мы начинали делать Neon. А вот эта идея — что чуваки как-то странно отстранены, Postgres набирает популярность, а никто из его разработчиков сами сервисы не пишет — многие её увидели, и такие отделы появились и в Microsoft, и в Amazon, чтобы нужные фичи продвигать в сам Postgres. Сейчас ситуация гораздо более сбалансированная. Кто ещё писал? Ассанж когда-то посылал патчи в Postgres и, по-моему, активно участвовал в обсуждении переименования из Postgres в PostgreSQL — это как раз 95–96-й год. Он тогда, по-моему, был студентом то ли в Австралии, то ли в Новой Зеландии — ранняя интернет-тусовка.
[16:45] Стас: Так, про что я рассказывал. Откуда Postgres, кто пишет: SQL, бэкапы, write-ahead log, MVCC — все признаки нормальной базы появились. Он всегда принадлежал комьюнити, и работает оно примерно по тем же правилам, что и BSD-комьюнити: народ старается двигаться консенсусом. Если кто-то из основных разработчиков сильно против — скорее не случится: можно продолжать спорить и убеждать, но пара десятков людей, у кого есть права закоммитить, стараются не коммитить, если есть серьёзное разногласие по патчу или подходу. При этом все понимают, что лучше не быть против чего-то, если ты прямо не уверен, что это плохо, — никто не любит годами делать фичи, в которых все не уверены; тогда лучше просто не делать. Но иногда, да, штуки тянутся по пять-шесть лет. Тут важно понимать релиз-цикл.
[17:52] Стас: Новая версия релизится примерно в сентябре. Активная разработка идёт с сентября до марта: там три или четыре так называемых commit-fest’а. Условно, один месяц пишем код, второй месяц пытаемся его закоммитить. И самые большие фичи, которые за релиз-цикл должны что-то серьёзно поменять, лучше пытаться закоммитить в самом начале цикла. То есть что-то большое, что в теории может много всего поломать, ты коммитишь в первые один-два commit-fest’а. Чем дальше — тем скорее скажут: чувак, мы просто не будем коммитить твои 50 тысяч строк диффа, это опасно, особенно с точки зрения данных, давай в следующий год. И это накапливается: через год люди, которые этим занимались, уже заняты чем-то другим — вот из-за этого часто и сложно что-то закоммитить. Это обобщённый гайдлайн, не то чтобы им прямо строго пользуются. У народа есть самописный веб-трекер: GitLab, GitHub и всем таким особо не пользуются. Основной способ обсуждения — e-mail. И база этих писем доступна с очень бородатого года, точно до 95-го, её можно скачать и по ней искать. На долгом сроке это интересно, потому что все сервисы закрываются и переоткрываются, а срок жизни проекта сильно дольше, чем срок жизни многих сервисов вроде Gerrit или не-Gerrit. По этим архивам ты можешь найти, почему то или иное решение приняли много лет назад, — и это очень часто важный контекст.
[19:46] Стас: Итак, проходят четыре commit-fest’а, потом делают feature freeze, выпускают бетки, рассылают пользователям. Это важный процесс, потому что стабилизировать бывает сложно. Postgres популярный, много кто ставит себе беты и тестирует их на некритичном проде — это даёт много информации, так находят много багов. Часто фичи откатывают: не можем это постабилизировать — давайте полностью выкинем. Так, например, было с SQL/JSON. Поддержка JSON — JSONPath, тип данных — это одно. Потом стандарт SQL сам сделал поддержку JSON, всякие функции типа JSON_TABLE, чтобы превратить JSON-объект в табличку. Там очень много нюансов: как парсишь, какое поведение при невалидных данных, что с датой и временем. И была конкретная проблема: в PostgreSQL, если при парсинге типа данных кидаешь ошибку, в целом надо откатывать всю транзакцию. Это не фундаментальное ограничение, а следствие того, как он написан: от ошибки сложно зарекавериться и продолжить работу. Почти всё структурировано так, что если кинул ошибку — можешь сделать что-то вроде try/catch, но в catch только освободить ресурсы и память, а ошибку всё равно придётся кинуть дальше. А поймать ошибку и продолжить работу — сложно. Это, например, одна из причин, почему при COPY IN — импорте данных — было бы очень удобно просто пропускать невалидные строчки, но это долго обсуждалось и требовало сильной перестройки кодовой базы. С JSON_TABLE сделали что-то более минималистичное и в итоге закоммитили, но вот так оно кочевало из года в год.
[22:04] Александр: А по тестированию — пока ты рассказываешь, у меня крутится мысль: это же очень сложный процесс — стабилизировать фичи в такой долгоживущей большой системе, особенно написанной на C, где могут вылетать приколы, которые непонятно как дебажить и воспроизводить. И вроде как чёткого pipeline нет, оно как-то естественным образом происходит. Как это всё выглядит?
[22:40] Стас: То, что сейчас называется green trunk: тесты на main не должны быть поломаны. Если кто-то закоммитил и тесты упали — во-первых, люди стараются так не делать, это то, за что волнуешься: закоммитил — лучше бы потом оно не оказалось красненьким. Естественно, так происходит достаточно часто, и сразу понятно, кто за это отвечает: тот, кто закоммитил. Если за два дня не получается стабилизировать — обычно откатывают: разберёмся и попробуем ещё раз. Набор тестов, этот green trunk, лежит прямо в исходниках Postgres. Меряется coverage, всякое такое. Я бы не сказал, что там прям упоролись по тестированию всего, но оно на хорошем уровне. Обычно приводят в пример SQLite: вот SQLite прям сильно упоролся, у них в какие-то моменты было прямо state-of-the-art тестирование базы. Postgres более обычный, с хорошим coverage, где-то есть косяки, что-то не очень хорошо покрыто. Но основной контроль качества — это всё-таки то, насколько скрупулёзно вычитываются патчи и насколько документирован код. Почти во всех частях Postgres, особенно написанных за последние десять лет, ты можешь просто выкинуть исходный код и читать комментарии. Подход проекта такой: ты ещё раз объясняешь текстом всё, что происходит. С низкоуровневым кодом чуть больше ментальных усилий надо, чтобы понять intention: когда ты переставляешь четыре указателя, чтобы что-то удалить в списке, — если ты этот шаблон читал пятьдесят раз, то сразу видишь, но ещё проще прочитать в комментах и что происходит, и зачем. И народ требует, чтобы все патчи были так же прокомментированы, там прямо line-by-line review — что может пойти не так. Если что-то попало в Postgres и закомментировано — значит, автор прямо сильно подумал над каждой строчкой.
[25:16] Стас: На CI, когда пишешь код, всегда есть процесс дебага: до того как ты всё прокомментировал, ты написал код, зачастую запустил — и он крашит. Открываешь дебагер, смотришь, что не так: ожидал, что API-функция ведёт себя иначе. Ещё есть тесты, которые комьюнити поддерживает. Например, есть штука вроде SQLsmith, которая генерирует случайные синтаксические деревья SQL — валидные синтаксически, но, может, невалидные семантически, — и можно много их посылать в базу. Ты никогда не знаешь, какой у этого рандомного запроса должен быть ответ: чтобы проверить, тебе нужно написать ещё один Postgres, но корректный. Так что проверить ответ ты не можешь, но такие штуки часто находят краши — тест не в том, валиден результат или нет, а в том, что от такого запроса база не упала. Это, наверное, самое property-based testing, или фаззинг. Ещё есть тесты на MVCC: мини-фреймворки для конкурентного поведения, где ты определяешь несколько сессий, каждая что-то делает, потом сессия 1 должна дождаться чего-то от сессии 2, и так далее. Проекту всегда нужны ревьюеры патчей и люди, которые потестируют. А когда появилась бета — там уже берут количеством инсталляций.
[27:00] Александр: Интересно. Получается, язык — с современной точки зрения C не самый развитый, компилятор не так много констрейнтов накладывает, — и недостатки компилятора и всей этой системы покрываются человеческими ресурсами: давайте лучше ревьюить, давайте удлиним цикл разработки до года, временем плюс людьми закроем дыры, которые в современных языках могут быть закрыты из коробки. И в этом основная ценность языков вроде Rust — надеюсь, мы про него ещё поговорим сегодня. Undefined behavior, на детект которого закладывается несколько месяцев бета-тестирования, может просто не быть в Rust: хоп — и ты сложил этот кост, его просто нет. Есть небольшая плата, что дольше пишешь код. Плюс ревью таких проектов: когда понимаешь, что констрейнты, существующие в коде, гарантируются компилятором, тебе не нужно так много объяснять и так явно декларировать свои intention — часть уже задекларирована в дизайне компилятора. Кажется, тут интересная штука: быстро на Rust писать серьёзный большой код не получится. Может, у тебя есть контрпример — что на Rust написано и быстро фигачит в продакшн?
[28:46] Стас: Ну, ClickHouse, наверное, один из таких примеров — он, правда, на плюсах, но релизные циклы, процесс ревью и вливания кода там довольно быстрые. На плюсах много чего пишется. Большая часть сервисов во всём FAANG до сих пор на плюсах, и деплоят их быстро: написал код, фиче-флаги, выкатил, поломалось — не поломалось, соответственно, включаешь или выключаешь флаг. C — да, медленнее писать: по клавиатуре стучать нужно чуть больше. Я бы даже не сказал, что фатально медленнее, но обычно чуть-чуть. С другой стороны, C — очень простой язык с небольшим количеством концепций. В плюсах можно быть умным: написать код в стиле, который тебе нравится, а другие с трудом прочитают, — широкое поле для обсуждений, как что написать. В C быть умным гораздо сложнее: берёшь и пишешь. Есть какое-то небольшое количество стилей, с разным количеством макросов, но это очень утилитарный язык, и его почти всегда просто читать. Это минималистичный язык, в стандарте нет большого количества конструкций. Если умеешь читать JavaScript, то умеешь читать C — первое время надо разобраться с управлением памятью, а после этого берёшь и читаешь. Конкретно Postgres неплохо закомментирован — его очень просто и быстро читать. Иногда это вообще работает так: пытаюсь разобраться с репликацией — просто возьми три дня и прочитай весь код, попытайся его понять, плюс в дебагере потыкай. И это работает. А какие-то кодовые базы на плюсах мне давались медленнее: комбинация. Иногда потому, что никто ничего не комментировал: легко понять, что делает код, а зачем — сложнее. Что хотел сказать автор. А иногда народ упарывается по темплейтам — тут уже зависит от того, насколько ты привык, от количества знаний: ну, что векторов нет, у хешмапа .at. Такие будничные вещи.
[31:39] Стас: Будьте осторожны, компилятор поработает — Rust. Чем меньше у тебя лет опыта, тем больше времени ты проведёшь в дебагере. Начиная с какого-то момента ты будешь просто писать корректно и сам. Но, во-первых, много времени должно пройти, а во-вторых, всё равно не 100%.
[31:32] Александр: Слушай, есть ещё вопрос про концепцию — почему и как Neon пошёл тем путём, которым пошёл. Мы сейчас хорошо обсудили, как устроено Postgres-сообщество: и разработку, и людей, и процессы. Допустим, существует такое комьюнити. Появилась идея — сделать из него какой-то serverless-Postgres, дать разработчикам возможность строить сервис. Как с точки зрения софт-скиллов всё это провернуть? Надо знать этих людей, надо ими быть, войти в комьюнити. Они же не сами такие взяли и решили пойти компанию делать. Расскажи про организацию всего этого.
[33:22] Стас: Надо собраться, найти деньги, найти людей и начать делать. Конкретно про Neon: всю кашу начал заваривать Никита Шамгунов. Он когда-то сделал компанию, оригинально называлась MemSQL, потом Single Store. Потом начал заниматься инвестициями в фонде Khosla Ventures. У них исторически часть проектов делалась по классической модели — когда к тебе приходят люди с идеей, — а часть в обратную сторону: мы видим нишу на рынке, давайте сделаем; кто-то из партнёров находит команду, и дальше либо остаётся работать, либо они сами это делают. Никита ещё со своих MemSQL-дней — когда продавал Single Store — знал, что почти во всех компаниях, кому они продают базу, уже стоит Postgres и есть много потребностей в нём. И он начал искать команду, начал с коммитеров Postgres, — но у большинства уже есть работа, проекты. Постепенно по цепочке рекомендаций он вышел на меня. Я при этом не коммитер — у меня нет ключей от гит-репы, но я активно участвовал в какие-то периоды. И я тоже хотел делать компанию, связанную с Postgres. Вышел он на меня через общего знакомого. Созвонились, я такой: да, пошли, я тогда в Яндексе работал, давай делать. Также он вышел на Хейки, коммитера из Финляндии: Хейки как раз заканчивал большой проект, связанный с Postgres, в Greenplum, лет пять им занимался, и стоял выбор — оставаться, браться за что-то ещё или куда-то идти; он тоже решил присоединиться. Так мы начали, подняли первые деньги. А дальше начинается обычное: абстрактно мы понимаем, что делать. Ты начал спрашивать про branching, про разделение compute и storage — это не то чтобы оригинальная идея, скорее результат абстрактного «давайте сделаем хороший сервис для Postgres, а как — ну, давайте разделим compute и storage», потому что есть валидация того, что Amazon сделал с Aurora, есть статьи, мы более-менее знали, что последние лет пять работает и не работает. А дальше начинается обычная история: надо шипать быстрее, надо делать качественный код быстро. Рецепт успеха понятный, просто это довольно сложно делать, и в конце концов это надо начать продавать.
[36:24] Александр: Слушай, мне очень понравился заход, который у нас получился, — мне кажется, мы прям готовы начать обсуждать сам Neon, сам продукт.
[36:37] Стас: Мы долго готовились, запрягали.
[36:40] Александр: И вот ты говоришь: есть концептуальная идея — сделать хороший сервер с Postgres в клауде, для этого можно сделать A, B, C. Давай про эти A, B, C поговорим: что было первым? Собрались, форкнули Postgres или не форкнули, начали что-то делать — вот что это было? Интересны первые шаги именно с точки зрения кода, архитектуры, фич.
[37:08] Стас: Мы почти сразу решили делать то, что Amazon сделал с Aurora. Можно было и не только это, но начинали мы в 2021-м. Если бы мы начали года на два-три раньше, наверное, делали бы какой-нибудь шардированный Postgres. Но постепенно это стало отходить на второй план: люди стали меньше париться, шардированное что-то или нет, — дайте мне API, которое работает, а внутри крутитесь как хотите. Эта идея просервировалась: это ваша зона ответственности, это наша. Шардинг — штука полезная, но часто его хотели компании, которым он на самом деле ещё не нужен или, возможно, никогда не будет; это искусственный констрейнт: ты делаешь его не потому, что нужен, а потому что люди хотят, — тоже нормально. Но это постепенно начало отваливаться со всей серверлесс-движухой: я посылаю запросы, вы присылаете ответы, я плачу — и желательно не за размер коробки, а за количество сделанной работы.
[38:18] Стас: Amazon Aurora — это был такой подход от AWS: давайте не будем ломать совместимость с самой базой. Они сделали оригинальную Aurora для MySQL и для Postgres. Если приложение написано для Postgres, оно будет работать и с Aurora Postgres. Но часть проблематичных вещей мы унесём в наш storage layer, который напишем с нуля и сделаем мультитенантным. Это примерно то же, что сделали и мы. Идея в том, что ты отрываешь от базы storage: остаётся compute — сервер, который занимается процессингом данных, — и остаётся storage layer на других серверах. И на storage-слое ты можешь сделать high availability именно на storage: выкинуть все Postgres’овые тулзы, которые занимаются промоутом реплики в primary, хартбитами, — это всё очень сложно делать на масштабе, оно достаточно плохо сделано, и кажется, что сложно сделать хорошо. Если пользуешься обычным Postgres, у тебя много single-tenant систем. Следишь за мыслью? Допустим, ты хочешь сделать managed Postgres в облаке, берёшь обычный Postgres. Что сделать первым, если люди хотят HA? Начать деплоить несколько реплик. Потом сделать штуку, которая шлёт хартбиты, проверяет, что живо, что нет, промоутит реплику в primary, если primary недоступен. При этом ещё нужны какие-то лизы по времени, чтобы что-то сделать со split-brain: если тебе кажется, что primary недоступен, но к нему есть подключённые клиенты, и они какие-то данные видят. Это делается на масштабе одной базы, а когда их сотни тысяч и для каждого нужно два кластера, — становится очень неприятной проблемой.
[40:35] Александр: А вот этот код, который делает промоушены, оркестрации, — это не часть Postgres?
[40:40] Стас: Это внешние тулзы, кем-то написанные, они разные. Все облака обычно пишут своё, ближе к их инфраструктуре и сервисам. И очень хорошо, если это можно унести на storage-уровень и сделать не Postgres’овыми средствами, а сам storage сделать multi-tenant: тогда есть один storage-сервер, к которому можно приконнектить много баз. Если ты делаешь database as a service, большинство баз на сервисе будут примерно пустые: денег на них зарабатываешь мало, но их всё равно достаточно много, и с этим нужно считаться. Что я пытаюсь сказать: сделать high availability на уровне storage сильно упрощает оперирование системой. Когда у тебя есть просто компьютер, который можно запустить в Kubernetes-поде, в нём нет никакого стейта, — ты можешь его прибить, поднять заново и не сильно переживать, что два компьютера одновременно запущены. В протоколе общения со storage можно выписать Raft или ещё что-то, что защитит тебя от split-brain. То есть этим компьютером можно гораздо агрессивнее управлять, и операционных проблем сильно меньше. Это один бенефит. Другой — что при разделении compute и storage compute гораздо легче масштабировать: у тебя нет какой-нибудь 5-терабайтной базы, которую нужно за собой таскать. Если у тебя обычный Postgres, хранящий всю базу на локальном диске, и ты хочешь сервер побольше, надо как-то перекачать эти терабайты. Можно использовать удалённый storage, но тут начинается latency и так далее. Удалённый storage — это тоже вариант разделения compute и storage.
[42:38] Александр: Есть, по-моему, другое название подобного подхода — shared-nothing и shared-disk. Когда у тебя compute и диск разделены — это shared-everything?
[42:48] Стас: Это ближе к shared-everything, хотя не совсем диск, я могу про это рассказать. Исторически shared-everything и shared-nothing обозначали немного другое. Shared-everything — это классический Oracle RAC: у тебя один большой диск, подключённый к кучке серверов, и API там даже не совсем дисковый, оно чуть умнее написано — серверы понимают, кто владеет страницей. На этот Oracle RAC-диск протекает информация о том, кто сейчас владеет страницей, — сильно интегрированная система. Shared-nothing — это идея: давайте возьмём сто одинаковых коробок в клауде и размажем по ним базу. Никто не запрещает назначать этим коробкам роли — тут storage, тут не storage. Так что всё зависит от того, как ты интерпретируешь термины. Один из вариантов интерпретации — commodity-железо или не commodity: если у тебя полка с дисками, это shared-everything с кастомным железом, а если обычные коробки — shared-nothing. В такой трактовке то, что делает Neon, — скорее shared-nothing. Так что с терминами надо аккуратно, смотря кто как трактует.
[44:29] Александр: Хорошо, что проговорили, потому что у меня был такой вопрос. Мысль, которую я для себя вывел: в клауде есть другой паттерн взаимодействия с системой, где нужно уметь что-то быстро заскейлить и быстро задаунскейлить. Когда нагрузка возрастает, система должна её переварить. Скейлинг в этом случае — это, грубо говоря, поднятие новых процессов, которые начнут обрабатывать входящие запросы. Обработка запросов — это тот самый compute: распарсить SQL, построить физический план, пойти его экзекьютить. И эта работа может быть заскейлена независимо от того, сколько у тебя данных и где они лежат. Потому что когда мы скейлим обычную программу — например, берём IntelliJ IDEA у меня на компьютере и хотим её заскейлить, — надо скопировать директорию с состоянием, настройки, и это время. Память, данные на диске — чтобы это скопировать, нужно время, которого мы не всегда можем себе позволить. Поэтому хочется по щелчку поднимать compute независимо от того, есть данные или нет. Разделение compute и storage позволяет скейлить compute. Где в этой картине находится код Postgres — в compute, в storage или там и там?
[46:22] Стас: По сути, compute — это Postgres, которому в тех местах, где он читает диск, вставили чтение из сети, а в тех местах, где он занимается репликацией, вставили запись по сети — просто в чуть другое место. Наш storage на запись выглядит как реплика — с точки зрения Postgres реплика, — и получает весь журнал операций, write-ahead log. А на чтение он выглядит как диск. Один из способов думать про это — удалённая файловая система с не очень обычным интерфейсом записи: ты в неё никогда не пишешь страницу, у неё нет API «записать страницу», а есть API «получить журнал операций», который она воспроизводит. Если бы ты просто подключил удалённую файловую систему к Postgres, ты бы записывал данные два раза, потому что есть журнал. Это просто инвариант в базах: если ты меняешь что-то в странице, сначала должен это зажурналировать. Так работает durability: сначала пишешь intention что-то сделать, потом делаешь. Если ты поломался, не доделав, то можешь накатить intention. А intention должен точно попасть на диск. Факт того, применилась транзакция или нет, с точки зрения durability — это записали ли мы точно на диск операцию в журнал. После записи журнала можно начинать всё остальное; даже если поломаемся в процессе, после рестарта доделаем. Так устроено в базах, файловых системах, много где, — иногда и без журнала можно, про это отдельно поговорим. Без журнала с SSD становится постепенно популярнее: например, новая файловая система Apple — APFS — без журнала. Про это тоже можно отдельно. Так вот, наш storage — это такой fancy-диск, на самом деле несколько сервисов, всё написано на Rust. В некоторых местах storage мы тоже зовём Postgres’овые функции, которые проще позвать из Postgres, чем переписывать на Rust: их достаточно много и нетривиально. Хотя некоторые переписывали. У одной крупной компании был похожий подход к разделению storage и compute, который они никогда не пустили в продакшн, но, например, переписали релевантный для storage Postgres’овый код, тестировали. Насколько я понимаю, это было слишком перемешано с их внутренними системами, и много времени нужно, чтобы отделить, а на это никто время не тратил, а потом стало вообще неактуально. Итак: compute — это чуть-чуть изменённый Postgres, storage — кастомные сервисы на Rust.
[49:14] Стас: Могу ещё повторить про операционную простоту. Что такое сервис? Это десять людей, которые поддерживают миллионы баз данных. Если ты в компании и у тебя пять баз, то количество баз на одного DBA — единицы, десятки, редко сотни. Если ты сервис, то у тебя два-три-пять-десять людей, а баз очень-очень много, и тебе важно, чтобы оно масштабировалось: не нужно было на каждые сто тысяч баз нанимать ещё по пять человек. Operations должен быть автоматизирован и простой. И вот разница: когда у тебя есть мастеры, несколько реплик и промоушены — там гораздо больше чего может пойти не так, и это сложно сделать. А легко управляя своим storage, который занимается HA и который ты можешь хорошо тестировать при основной его работе, — в конце концов тебе проще управлять очень большой инсталляцией. Причём это полезно не только потому, что можно заимплементировать автоскейлинг compute (не таская за собой storage) — это можно было бы сделать и с обычным удалённым сетевым диском, — а именно HA на большом масштабе делать сложнее. В среднем оно будет работать, но когда не работает — гораздо больше режимов, по которым оно ломается, и это сложнее. В итоге понадобится очень много людей, кто будет за этим следить.
[50:57] Александр: Понятно. То есть основной плюс того, что так разделили, — тебе как компании, которая предоставляет serverless-Postgres, не нужно под каждого нового клиента, грубо говоря, выделять 10% своего DBA. У тебя очень много клиентов, очень много баз данных, целый зоопарк, и надо уметь обеспечивать доступность всех этих баз клиентам, которые тебе платят. В чём камень преткновения, если compute и storage не разделены? Почему в этом случае HA сильно сложнее?
[51:40] Стас: Конкретной причины нет, но разница в том, что у тебя либо какое-то количество мультитенантных систем, либо, если ты сетапишь независимые Postgres, — условно, миллион инсталляций single-tenant Postgres. А если у тебя мультитенантный storage, то получается тот же миллион инсталляций Postgres, но stateless, и одна (или одна на регион) инсталляция storage. Этот storage — один сервис, в который входит много баз, ты его обновляешь — взял, оно всё обновилось, выкатил. И обновление storage не приводит к рестарту compute. А если у тебя миллион Postgres — ты можешь их обновить раз в месяц, потому что это рестарт для клиента, начинаешь делать по расписанию. Итерироваться сложнее, фичи делать сложнее. Мультитенантной системой проще оперировать: N машин, на которых запущены N процессов, и в каждый процесс присоединены десятки тысяч пользователей, — этим проще управлять, чем миллионом коробок, где каждая принадлежит одному пользователю.
[53:03] Александр: Ну этот миллион коробок — он же, наверное, у нас тоже есть?
[53:08] Стас: У нас тоже есть миллион коробок, но они stateless, и это сильно проще.
[53:11] Александр: То есть эти инсталляции compute в виде Postgres без диска — они всё ещё у каждого пользователя свои?
[53:21] Стас: У каждого пользователя своя, но ты не паришься за storage, не паришься с борьбой со split-brain — это решается на другом уровне.
[53:33] Александр: Окей, прикольно. Про разделение хорошо поговорили — это один из первых технических моментов, про которые Neon заявляет у себя на сайте, и довольно интересная штука. Ну окей, давай так: разделили, написали некоторое количество адаптеров, чуть-чуть подхачили код, который работает с диском и с репликой в Postgres, научились запускать его быстро и чтобы он не падал, когда под ним нет диска с данными, а весь storage вынесли в отдельную систему на Rust — про неё поговорим. Дальше — какие ещё есть архитектурные разделения этих сервисов, кто за что отвечает? Давай такой system design этой системы приведём: что ещё есть на верхнеуровневых квадратиках?
[54:28] Стас: Мы начинали с того, как сделала Aurora. Aurora за несколько лет до нас начала использовать S3 нативно, и это оказалось прямо очень хорошей идеей. Какие-то данные надо будет точно загружать на S3 — самое очевидное, это бэкапы, а ещё нечасто используемые куски базы. С S3 достаточно большая latency — сотни миллисекунд, — но очень большой throughput: если правильно сделать, он будет больше, чем даже у локального диска. Сразу казалось, что имеет смысл дизайнить так, чтобы выжимать максимум из S3, потому что это очень хорошая система с очень хорошими гарантиями durability и достаточно хорошими гарантиями availability. С availability иногда бывают инциденты, а вот чтобы S3 терял данные — я даже не помню, было ли последний раз. Помню, у Dropbox когда-то были минорные инциденты, которые они хорошо расследовали и трейсили к драйверам/фирмваре дисков: оказалось, что одни и те же данные лежали на одинаковых моделях дисков, и они потом поправили алгоритмы скедулинга. Но S3 теряет данные редко. Это одна из вещей, которую мы сразу решили использовать: наш storage написан так, что ты записал транзакцию, зареплицировал её в несколько availability-зон, она уже на диске, и мы даже ничего с ней не делаем — как только записали в большинстве availability-зон. Сейчас мы в трёх зонах: два из трёх нужно записать, чтобы транзакция закоммитилась. С точки зрения latency это примерно то же, что отправить на два из трёх реплик — quorum commit.
[56:12] Стас: На этом же уровне мы сделали reliable broadcast — такой Paxos-протокол: чтобы при коннекте к storage сделать раунд голосования. Берёшь последний generation, предлагаешь себя как кандидата на то, чтобы быть лидером в текущем терме, storage тебе подтверждает или не подтверждает, если есть конкуренты, и потом каждая запись штампуется этим термом. Как только появляется другой кандидат, storage уже не разрешает нам ничего коммитить. Достаточно понятный алгоритм с хорошими гарантиями, который позволяет избежать всех тайм-лизов и внешних демонов, следящих, кто живой. Первая система коммитит на наши SSD; как только транзакция попала на диск, она считается успешной, и после этого мы отправляем этот write-ahead log на следующую систему. Следующая система уже реплеит журнал — её мы называем PageServer: она умеет понимать журнал и материализовывать его в страницы. Postgres оперирует всегда 8-килобайтными страницами — неважно, таблица это или индекс. Storage умеет переваривать журнал в страницы и сразу же отправлять на S3. С сохранностью данных вариант такой: либо данные на нашем первом тире хранения (который хранит их несколько раз в разных AZ), либо они на S3. Получается pipeline: сначала закоммитил на три наших сервера, потом мы переправили это на один сервер, который хранит уже всю базу, а не только последний кусочек, и отправили сразу же на S3. Как только новые данные оказались на S3, основное хранилище отправляет фидбэк первому тиру: окей, эти данные попали на S3, можешь их удалить. Получается, что мы как сервис — если доверяем S3 — ответственны только за последние сколько-то мегабайт записанных данных. Условно, если записал и больше ничего не пишешь, через несколько секунд оно осядет на S3, и за durability теперь отвечает S3. Мы пытаемся от данных, за сохранность которых отвечаем, избавиться как от горячей картошки — как можно быстрее спихнуть на S3. Тогда, если у нас всё упало, мы всё равно можем это скачать с S3: это уже availability-проблема — тоже плохо, но гораздо менее рискованно. Сервис даун — или сервис даун, потому что данные потеряли, — это разные вещи. И то и то плохо, но одно сильно хуже.
[59:11] Александр: То есть S3 — это фундамент, такое строение.
[59:16] Стас: Отвечаем мы за данные, только либо очень свежие. А если игнорировать самые свежие данные, которые ты только что записал, то то, что на storage, — это кэш над S3. Можно так воспринимать.
[59:27] Александр: Ну да, одним словом — кэш над S3, но в нём, я так понимаю, мы сейчас обсудили две системы внутри этого кэша.
[59:35] Стас: Да, мы их называем… Первое — это Safekeeper, второе — PageServer, это наша внутренняя терминология. В Aurora так не сделано, они их совмещают. Из литературы так предложили сделать в аналоге от Microsoft — статья называлась Socrates, — они тоже предложили эти два тира. Они сделали это по другим причинам: для MS SQL, а в MS SQL формат журнала не такой, как в Postgres. Журнал ты не можешь проиграть сам по себе, если у тебя нет доступа к каталогу базы данных: он не self-contained. А в Postgres есть прикольное свойство: если у тебя есть предыдущая версия страницы и запись, которая эту страницу меняет, — тебе хватает. Наследие Lisp?
[1:00:23] Александр: Ну, вряд ли.
[1:00:27] Стас: Есть нюансики, но у тебя есть страница и модификация страницы — и всё работает. В MS SQL у тебя есть страница, есть запись, которая меняет эти страницы, и ещё нужна сама база — чтобы проиграть, надо смотреть в саму базу. Поэтому они сделали подготавливающий тир и тир хранения. По другим причинам, но нам это показалось очень хорошей идеей. Разные диски должны быть на таких нодах, разные требования к машинкам. Первый тир, который мы называем Safekeeper’ами, должен очень быстро создаваться — SSD с достаточно маленьким объёмом, там много данных не бывает. Количество данных пропорционально скорости записи плюс скорости, с которой ты избавляешься от этих данных, — такой буфер. CPU там много не нужно. Если у тебя 10-терабайтная база, количество данных на Safekeeper пропорционально просто скорости записи — это какие-то гигабайты. Следующий тир, PageServer’ы, — уже основное хранение: туда поехали все твои терабайты. В теории мы всё равно используем SSD, но там можно пытаться сделать дешевле, оно будет нормально работать: не нужна такая скорость доступа. И за этим всем есть S3. Гарантия durability такая: либо оно на Safekeeper’е, либо через PageServer на S3. Так это балансируется.
[1:02:03] Александр: Мне очень понравилась идея, что WAL — системе, которая предназначена быстро записывать в конец, — и PageServer’у, где в целом random access плюс иногда sequential scan, — вообще говоря, нужен разный хардвер: разные диски эти задачи выполняют эффективнее. И я думал: а если я просто ставлю себе на сервак Postgres — могу ли я как-то намаунтить диски так, чтобы здесь лежал WAL, а здесь основное?
[1:02:40] Стас: Да, и получается, что этот подход на размере клауда тоже сохраняется: мы можем на более высоком масштабе так же оптимизировать. Это прикольно, потому что не всегда и не все базы позволяют так затюнить. Разные директории ты обычно всё равно можешь замаунтить.
[1:03:11] Александр: Ну да, директории — если ты DBA, то, наверное, можешь. А если обычный разработчик, то тебе не до этого. Но круто. Итак, Safekeeper — отдельная система, которая отвечает за то, чтобы записывать WAL, уметь его проигрывать и реплицировать в object store.
[1:03:23] Стас: Она не проигрывает, она просто записывает. Две основные функции. Первая — защищаться от split-brain, не разрешать в одну и ту же базу писать с разных compute. Вторая — получить WAL, сохранить на диске и отдавать всем, кто спросит. А спросит — это следующий тир.
[1:03:42] Александр: И эта система Safekeeper, такого консенсусного WAL внутри клауда, использует, я так понимаю, какой-то Paxos-like протокол. Как там достигается консенсус?
[1:03:56] Стас: Можно начать с того, что Paxos и Raft — это примерно один и тот же протокол. Raft — это подмножество Paxos, чуть более аккуратно выписанное, с меньшим количеством степеней свободы, чтобы проще было воспроизвести на практике. Терминология Raft — лидер и фолловер. В Raft все ноды одноранговые: есть N нод, скажем, 3, каждая может себя предложить лидером в каком-то терме, а другие за неё голосуют. Начинается репликация: лидер рассылает запись, фолловер подтверждает, всё аккуратно провязано номерами. И когда лидер предлагает себя, у него ещё есть random timeout — это тоже не обсуждается в статье про Paxos, но важно на практике: как избежать, чтобы несколько лидеров одновременно дрались. Нам это не очень подходит, потому что у наших нод есть роли: наш compute может предлагать WAL, может быть лидером, но не может быть фолловером — он в целом не может принимать WAL, ему негде его хранить и нечем проигрывать. Safekeeper может быть только фолловером. В итоге сетап такой: фиксированное количество Safekeeper’ов и потенциально бесконечное количество compute. И все дерутся из этих compute — на практике их два-три. Если ты случайно накосячил, у тебя теперь есть две базы, которые пытаются стартануть и записать что-то в один и тот же сет Safekeeper’ов. Или, пользуясь терминологией Paxos, это пропоузеры и… как называется? Пропоузер и акцептор, по-моему. Есть те, кто в Raft-аналогии потребляют, и те, кто производят; и те, кто производят, должны выиграть выборы. Такое разделение на две роли достаточно стандартно в терминологии Paxos, а Raft специально более жёсткий, более простой — там этого нет. То, как мы это написали, — почти стандартный Raft, но с разделением на эти роли. Такое обсуждается в литературе под названием DiskPaxos: у тебя Paxos, но часть нод — это просто фиксированное количество дисков без CPU, и N процессов, соревнующихся за эти диски. Достаточно исследованная в литературе проблема, ещё с конца 90-х — начала 2000-х. Raft появился сильно позже, когда был большой интерес к распределённым системам, но это современный язык того, как про это говорят. Нам нужна была достойная вещь, поэтому, используя терминологию и подходы Raft, мы добавили степень свободы: пропоузеры (Postgres’ы) могли предлагать свою кандидатуру, а Safekeeper’ы за них голосовать.
[1:07:01] Стас: И доказательство корректности. Количество TLA+ мы писали отчасти просто потому, что могли: знали, умели, и хотели поменять какие-то вещи и проверить, что они корректны — по сравнению с тем, как в статьях написано. Но мы пишем не с нуля: это Postgres’овый write-ahead log, и чем меньше поменяешь формат этого лога, тем лучше, — оттуда возникали идеи что-то сбоку хранить, которое хотелось проверить. То, что мы проверили, — это model check, не extensive: это не доказательство, оно на разумных настройках проходит TLA-чек. Потом, по-моему, через полгода мы это всё выпилили и сделали, как в статье должно было быть, потому что компакшены оказались сложнее — они всё равно были нужны по другим причинам, а все оптимизации, которые мы сделали, уже выкинули. Долгосрочно так было проще. Кому интересно — этот код лежит в репозитории. Чуть позже мы сделали то, что называется change of membership. В сетапе может быть потенциально бесконечно пропоузеров и фиксированный сет Safekeeper’ов. Возникает вопрос: что происходит, если один упал и ты хочешь добавить новый, — в Raft это называется membership change. Это мы сделали сильно позже, поначалу была ручная замена. Ну, опять же, это мультитенантная система: одной заменой поменял 10 тысяч баз. Для membership change написали спеки. Потом уже вошли во вкус: по-моему, ещё для рестарта VM писали спеки. Это уже скорее просто: вот стоит машина, в ней нужно что-то проверить. Можно было бы написать на Python программку, но всё стоит комбинаторно — а если ты знаешь TLA, то на TLA проще: ты пишешь только Definition, где стоит машина, а model checker проверит из неё все выходы.
[1:09:14] Александр: Может, стоит пару слов сказать, что такое TLA — не все слушатели вообще сталкивались с такой системой.
[1:09:20] Стас: Есть такой чувак — Лесли Лэмпорт. Известен своими хотейками, мне кажется, в основном. Метки Лэмпорта, да. Бодрый дядька, который много важного сделал в распределённых системах. Логические часы — это его терминология, тоже его алгоритм. Первое решение задачи о том, как… Протокол консенсуса — это протокол, который позволяет N системам договориться о каком-то значении. Это то, что называется single-decree Paxos, когда договариваешься об одном значении. А если хочешь договориться об одном значении, потом передоговориться о его следующем значении — это уже похоже на задачи репликации, или reliable broadcast, или multi-Paxos. Это отдельный результат: reliable broadcast и консенсус на нескольких значениях — одна и та же проблема. Люди искали решение задачи консенсуса какое-то время; Лэмпорт придумал, как это сделать, и у него достаточно хорошее доказательство — во-первых, почему оно работает, а во-вторых, почему только так и можно. Это довольно редкая штука — показать, что, например, с меньшим количеством round-trip’ов нельзя. И он это написал в виде сказки про древнегреческий остров Паксос, на котором люди голосуют за новые декреты. Кода нет — есть такая история. Статью не приняли, она, по-моему, лет десять пролежала у него в столе; про неё уже знали — он ни от кого не скрывал, — но опубликовали лет через десять. При этом было ещё несколько работ от Барбары Лисков — view stamp replication, — они сделали примерно то же самое, чуть с другой терминологией, примерно в то же время. То есть несколько групп решили задачу, но чувство юмора Лэмпорта — сделать это древнегреческой сказкой с кусочками греческого языка; а не понравилось ревьюерам — ну, это ваши проблемы, ревьюеры. Это Лэмпорт.
[1:11:34] Стас: Потом Лэмпорт ушёл в формальное доказательство программ, и у него всегда очень практический подход. Вся область формального доказательства большая, много подходов, они математически ёмкие. Лэмпорт попытался сделать что-то более простое. Обычно, если занимаешься формальной верификацией, ты оперируешь теорией типов — кто знаком с функциональными языками и системами типизации, там много всего происходит, нужно знать какое-то количество математики и алгебры. Лэмпорт сделал систему, где всё в обычной теории множеств — более-менее привычная математика: A принадлежит B и так далее. Это просто язычок, на котором ты описываешь стейт-машину. Программа — это всегда просто стейт-машина, которая вычисляется, и потом ты можешь её сам к себе ещё раз применить. Условно, если пишешь алгоритм сортировки, то стейт-машина — это один прогон цикла. Если рекурсивно написать сортировку, то описание того, что делает один прогон, нужно переложить на TLA, а дальше TLA может запустить её на всех входах, посмотреть, что будет, и на каждом проверить инварианты — условно, асерты, которые ты напишешь. Ещё Лэмпорт написал первые версии TLA+ — это какая-то мешанина из TeX и всего, одновременно IDE, TeX и model checker, всё сложно запустить, — по-моему, это не сильно добавило популярности подходу. Потом другие люди этим занялись, хотя бы вытащили интерпретатор в отдельный Java-файл. Сейчас есть неплохие плагины, например, для VS Code — интерактивно что-то написать с подсказками.
[1:13:36] Александр: Если простыми словами, то TLA+ — это штука, которая доказывает корректность логики, корректность алгоритма, какие-то инварианты, которые мы как авторы декларируем: например, консенсус достигнут при таком-то условии; а вдруг есть набор входных данных, при котором будет ошибка. И эта программка — если мы правильно описываем нашу логику в тех языковых конструкциях, которые она предоставляет, — со стопроцентной вероятностью может сказать, правильно ли это с точки зрения формальной верификации. Но то, как мы заимплементируем эту логику на нашем языке программирования, проверить уже не можем — это другое.
[1:14:25] Стас: Ко всему прочему, она ещё не доказана — это model checker. Ты просто запускаешь его, проверяешь все варианты на каком-то ограниченном количестве возможностей. Про тот же Raft: обычно N от 2 до 5 участников, обмен сообщениями — не более семи туда-сюда, — и он проверит все возможные комбинации. Чем больше числа, тем дольше. Это не формальная доказательность, это больше как фаззинг или property-based testing: вот этот набор тестов на таком наборе данных не нашёл ошибки в алгоритме. Но это не значит, что ошибки нет.
[1:15:05] Александр: Да, но это, наверное, занимает какую-то нишу. Если ты написал свою программу и запускаешь фаззинг, то условно можешь проверить тысячи комбинаций в секунду. А если написал именно TLA-модельку и не запускаешь процессы, то, во-первых… я, кстати, не помню, какой там рейт — скажем, миллионы в секунду можешь проверить.
[1:15:27] Стас: Плюс там начинается много математики. Обычно появляются симметрии; есть способы прунинга search space, потому что в этой постановке стоит машина — неважно, как ты сюда пришёл, но если ты уже приходил и искал выход отсюда, то дальше можешь не искать. Прунинг очень сильно работает.
[1:15:54] Александр: Обход графа, может быть, даже backtracking с оптимизацией.
[1:16:01] Стас: Типа того, да. Если материализуешь пути как граф, то, во-первых, «сюда уже пришёл», во-вторых, появляются симметрии. Короче, он сильно быстрее проверяет. Но проверяет не твой код, а написание того кода. У Лэмпорта есть видеокурс про это на его страничке. Первая задача, которую он решает, — из «Крепкого орешка 3». Помнишь? Я, кстати, не смотрел. Первые два смотрел.
[1:16:31] Стас: Там была проблема: нужно было пятилитровым и трёхлитровым ведром налить воду, а то что-то взорвётся. Классические школьные олимпиадные задачки — три литра, пять литров. И это тот класс комбинаторных задач, которые очень хорошо решаются с помощью TLA. У тебя получаются два экшена: вытащить из одной переменной тройку и добавить в другую, потом минус пять сюда, плюс пять сюда. И инвариант — какой результат должен быть, ты его поставишь. Точнее, «не должно быть»: она ищет нарушение инвариантов. После этого она идёт проверять все трассы, и когда инвариант зафейлился — это и есть трасса, которая приводит к результату.
[1:17:23] Александр: Окей, хорошо. Мы далеко уехали. Итак, вы писали набор этих model checker’ов для своей реализации Paxos-like алгоритма Safekeeper’а. Вы прям писали на Rust с нуля реализацию, или использовали какие-то библиотеки для достижения консенсуса?
[1:17:41] Стас: На Rust с нуля. Но, опять же, консенсус с точки зрения кода — это обработать два типа сообщений и один ивент. Single-decree Paxos: предложить переменную, спросить у всех число, выбрать максимум, и во втором этапе разослать все значения с этим максимумом. И очень важно на проверяющей стороне проверить, чтобы рассылали то же самое число, которое ты на первом шаге отдал. И ещё важно его не потерять — ни в коем случае. Есть статья от Google — не помню, как называлась их система, одна из первых реализаций, ещё прямо Paxos-Paxos, до того как появился Raft. По-моему, «Paxos Made Live». Они обсуждали, как теряли данные: записывали это generation, эту чиселку, на получающих системах на диск, не делали чек-сумму, потом диск покорраптился, оно не смогло прочитать — и вместо того чтобы выйти с ошибкой, инициализировало всё дефолтными значениями. Это баг, но важно эту чиселку не потерять.
[1:18:53] Александр: Прикольно. Так, Safekeeper. Давай чуть-чуть зум-аут. Получается, рядом система, которую вы тоже написали сами, — это PageServer. Саму роль в общей архитектуре мы обсудили, но давай тоже внутрь немножечко копнём: как он работает, какая у него задача? Это что-то типа буфера, page buffer в классическом понимании?
[1:19:19] Стас: Page buffer обычно в памяти. Мы про PageServer думаем просто как про реплику. Только реплику, которая не умеет обрабатывать запросы, а умеет только странички. Задача — получить журнал и разложить это по файлам, чтобы потом, когда у тебя спросят десятую страницу вот этого, ты мог её проиграть. В этом плане задача очень простая: хранить файлы и менять их через интерфейс записи, который журнал. При этом если ты хочешь прозрачно синхронизироваться с S3, появляется ограничение: форматы файлов или структур данных лучше бы были append-only. S3 не позволяет менять что-то в префиксе — можно только записывать новые данные, но не менять старые. Условно, если в гигабайтном файле хочешь поменять страничку на S3, надо переписать весь гигабайт. Из этого получается ещё один constraint. Ты не обязательно мог бы иметь разные форматы на S3 и на локальном диске, но это не идеально. Есть LSM-деревья, которые в каком-то смысле сами по себе этому удовлетворяют. Условно, для PageServer’а какие варианты? Можно либо написать своё хранилище — B-дерево, LSM-дерево, — либо взять то, как Postgres это делает: он раскладывает файл. Но Postgres написан для жёстких дисков — или в последнее время. У Postgres есть операция «пойди в файл, обнови страничку». Поэтому мы выписываем. Даже полный LSM мы не выписывали — LSM с фиксированным количеством слоёв, потому что на storage мы шардируем базы, если они превышают какой-то размер. Поэтому у одного LSM есть верхний барьер по количеству данных: 100 гигабайт, больше данных не будет, поэтому мы не выписывали большого количества слоёв.
[1:21:29] Александр: Я хотел спросить про LSM. Это, наверное, часть in-memory, часть LSM, PageServer’а в каком-то плане?
[1:21:38] Стас: Он на диске — на рестарт не теряет данные.
[1:21:44] Александр: Получается, что в картине с S3 — если мы представим себе LSM (а слушатели, у кого не представляется LSM перед глазами, — отправляю вас в выпуск про LSM из предыдущего сезона про базы данных, там я прикреплял целую картинку, которую сам рисовал, можете открыть в телеграм-канале и представить), — вот этот холодный уровень, который уже на диске, персистентный, — в S3, скорее всего, располагается, а горячий уровень, где дерево и оно может допроигрывать состояние страниц из WAL, реализован в PageServer’е по большей части. А WAL — это Safekeeper’ы.
[1:22:32] Стас: Но это трюки, потому что на самых глубоких уровнях могут быть важные данные. У LSM нет свойства, что данные «всплывают». Если кто-то записал какую-то табличку, что-то важное, и никогда её не менял, то эта старая запись будет как раз на самых глубоких уровнях, но она тебе всё равно нужна для чтения. Нет свойства, что внешние слои — это что-то ненужное. Там что-то старое, но старое может быть нужно. Поэтому у нас чуть другая система: мы эти слои ещё бьём на кусочки, и дальше простые эвристики — условно, 24 часа или больше не было доступа — мы их выгружаем.
[1:23:25] Александр: Чтение определяет, будут эти данные находиться на PageServer’е на диске или на S3. Если в классическом LSM только запись определяет расположение данных, то в вашей реализации ещё и чтение может их как бы протянуть.
[1:23:35] Стас: Ну да, с точки зрения структуры LSM всё так же, как должно быть. Просто на одном слое, который тоже побит, — в литературе про LSM это называется партиционированный LSM, — эти кусочки просто уедут на S3, а если надо будет прочитать, мы их скачаем с S3 и положим на диск. Тут ещё важно, что мы храним всю историю: если мы не делаем garbage collection, то храним не только последний срез данных, но и всю историю. Это другое свойство того, что мы не переписываем данные. Оно append-only: формально ты берёшь и добавляешь вечно, потом запускаешь garbage collection, который подрезает что-то старше семи дней. Само по себе это не происходит, как у обычных данных. Условно, у тебя есть архив WAL, представляющий историю, и есть последний снимок данных. У нас как такого последнего снимка нет — есть какой-то снимок последней истории. Это сделано, потому что нужен был неперезаписывающий формат, чтобы просто выгружать на S3.
[1:24:45] Стас: Но в этом есть очень прикольный бенефит: исторический запрос становится дешёвым. Ты можешь спрашивать последнюю версию данных, а можешь спросить то, что было час назад, не делая специальных усилий. Интерфейс чтения — это «дай мне страничку с таким номером на таком LSN». LSN — это Log Sequence Number, это база данных. Можешь спросить последнюю, можешь всегда спросить какую-то предпоследнюю, вчерашнюю. Что удобно, во-первых, для реплик, которые зафиксированы на каком-то периоде, которые не фолловят мастер: ты запустил read-only реплику, которая работает только на этом снимке данных. Либо ты можешь что-то искать в истории — такой бисект по истории. В обычной базе, если хочешь откатиться на вчера, тебе нужно найти последний бэкап, накатить на него WAL вперёд, потому что бэкап тоже не каждую секунду снимается: дейли-бэкапы и журнал между этими днями. Берёшь бэкап, накатываешь на него данные, и, в зависимости от интенсивности записи, где-то через час твоя база будет готова. Иногда быстрее, иногда медленнее. И это проблема для компании побольше, которой важно время восстановления, RTO. Если ты пишешь очень много данных, тебе медленно проигрывать этот write-ahead log. И ты не можешь бесконечно часто делать снапшоты, потому что снапшоты — тоже бэкапы, они отъедают перформанс — либо на базе, либо на блок-девайсе. А здесь, за счёт того, что мы не вытираем данные (вытираем сами, отдельным действием), это становится сильно проще. Посылать запросы к историческим данным легко. Можешь, например, достаточно быстро найти конкретный LSN, когда я удалил этого юзера или когда дропнул этот col, — проверяя, есть эта таблица или нет. Мы даже UI для этого сделали: выбираешь интервал времени, col, и мы там делаем бинарный поиск и находим конкретный LSN, на который можешь восстановиться.
[1:26:54] Александр: Интересно. То есть если я случайно дропнул какого-то важного пользователя и надо это восстановить, я могу прямо в UI найти, какая версия базы этого пользователя ещё имеет, и откатиться с минимальными потерями. Прикольно. И, наверное, исходя из этой особенности — что всё хранится и не удаляется по дефолту — идея branching, одна из фич, которую тоже можно найти на главном сайте: базу можно бранчевать. Вообще говоря, я такого термина «бранчевание» до Neon не встречал. Думаю, наверное, где-то эта идея есть, по-другому называется — кто-то, может, это time-travel докрученный с ветками. Давай про это поговорим: что такое бранчевание вообще и как его использовать обычному рядовому разработчику.
[1:27:55] Стас: Да. Если пишешь свой storage для базы, то это одна из вещей, которые не очень сложно сделать. А у определённого класса пользователей есть потребности. Основной пользователь — люди, которые настраивают себе preview-окружения. Ты сделал pull request, и есть достаточно много ботов, которых можно поставить прямо в GitHub: он придёт и скажет, что задеплоил твой pull request на таком-то временном домене в Vercel. Если заводишь аккаунт на Vercel, сразу привязываешь GitHub, и Vercel тебе на каждый pull request даёт временный домен — тыкаешь и смотришь на свой pull request, ничего настраивать не надо. Неплохо работает с сайтиками, где данные перемешаны с кодом: условно, лендинг, и контент в Markdown там же в репозитории лежит. А если приложение использует базу, то начинает работать хуже: pull request прогонит по этой базе, или тебе каждый раз новую базу нужно сетапить, но тогда она пустая. Сиды можно настроить, но тоже надо настраивать, и они разъезжаются. Начинаются плюсы-минусы. А чтобы сделать автоматически, можно сделать бранчевание базы: на каждый pull request создаёшь бранч. Либо, зависит от потребностей компании, можешь просто бранчевать production — для небольших проектов это нормально, и тогда ты проверишь свои миграции на реальных данных, что какие-то констрейнты не разваливаются. Можно придумать достаточно много миграций, которые пройдут на staging и поломаются на production: начинаешь валидировать какой-то констрейнт, а у тебя уже есть данные на production, кто-то из юзеров написал что-то по-другому в поле для e-mail; или начинаешь uniqueness где-то инфорсить. Либо люди заводят отдельный staging — это уже компании побольше, которые больше парятся про PII и доступ разработчиков: production должен быть меньше, патрулирован, — и тогда бранчуют staging на каждый pull request.
[1:30:17] Стас: Branching делается подходом, который называется copy-on-write. Когда ты создаёшь бранч, никаких данных не копируется. Если у тебя терабайтная база, всё равно мгновенно создаётся бранч. Как только начнёшь писать в эту базу, вот тогда эти данные поедут в отдельную директорию, и чтение будет сначала смотреть в этой директории, если какие-то оверрайды отдельно, а потом уже в parent. Их достаточно дёшево создавать и дёшево удалять. Люди, кто сетапит себе preview environment, пользуются достаточно активно. Мы сделали интеграцию с Vercel и ещё несколькими компаниями, которые делают современные аналоги Heroku. Кто-то пользовался Heroku: идея в том, что у тебя есть репозиторий с кодом, ты делаешь push, и дальше ничего не настраиваешь в инфраструктуре — инфраструктура провайдера понимает фреймворк, как его раздеплоить и так далее; ты только видишь UI и приятные штуки в виде preview deployment. Есть ещё люди, которых сильно меньше, кто в бизнес-логике пользуется бранчингом. У нас есть клиент, который занимается моделированием в довольно традиционной отрасли: у них достаточно сложная схема данных. Ты можешь запустить моделирование с некоторыми изменениями, но эти изменения отражаются в изменении схем. Словно у тебя есть бранч основной базы, ты сделал несколько бранчей, в них несколько небольших изменений схемы, запустил моделирование на каждом, потом свою функцию оптимизировал, остался один бранч — и ты это перенёс в изменения на main. Таких мало. Я, наверное, сначала даже не очень понял, зачем это им. Ты можешь такое тоже сделать в обычном SQL с чуть более обобщённой схемой, но так просто сложнее — можешь делать какой-то attribute-value вместо SQL, и тогда как хочешь менять схему в пределах одной базы. Но это сложнее, особенно если схема нетривиальна: сделать N клонов базы и в них делать ALTER и прочее.
[1:32:33] Александр: Слушай, вот эта идея с миграциями прямо на продакшн-базу в отдельном бранче — это, вообще говоря, довольно свежая идея. Обычно берут миграции, накатывают на staging, смотрят, что ничего не поломалось, потом начинают накатывать на проде одной транзакцией: если что-то пошло не так, транзакция просто не коммитится и деплоймент неуспешен. А тут можно оптимистично накатить в отдельную ветку, посмотреть, что оно работает реально на продакшене, и уже потом либо второй волной накатить на последнюю версию, которая станет основной веткой, либо как-нибудь ещё. Я к чему? Подход к деплойменту приложений, которые активно используют базу и активно её меняют, может в положительную сторону поменяться с использованием такой модели бранчевания. То есть мы теперь не только код бранчуем, но и данные в какой-то момент. Очень интересная идея, которая пока, мне кажется, не сильно распространена, но у неё огромный потенциал.
[1:33:46] Стас: Народ пользуется, и это сильно другой тип разработки, чем с базой данных. С базой обычно понятно, что надо: всем нужно, чтобы было быстрее и стоило меньше, — очень чёткая цель, просто туда сложно дойти. А когда делаешь branching — этим мало пользовались. До нас были другие компании, например, ребята из PlanetScale, они для MySQL сделали, но у них немного другая модель — в бранчах нет данных, у этого свои плюсы-минусы. С бранчеванием это открытая проблема: скорее сделали эксперимент и смотрим, как этим люди пользуются, а как не пользуются. Немного другой тип технического бизнеса.
[1:34:41] Александр: Ещё знаешь, что интересно? Возможность вообще эту фичу попробовать получилась из-за того, что вы начали свой storage layer писать. И, как ты сказал, когда пишешь этот storage layer и понимаешь, что у тебя там MVCC и так далее, оно предрасполагает к тому, чтобы эту фичу запровадить довольно малой кровью разработки. Ты как разработчик, который понимает, как система работает, понимаешь стоимость разработки этой фичи, и в то же время, когда выступаешь как бизнес, ты такой: ага, вот эту фичу можно попробовать, и это будет стоить мало. Симбиоз, когда разработчик и фаундер, и код может писать, и понимает, что можно это попробовать, — и оно выстреливает. Потому что когда это два разных человека, два разных отдела: бизнес говорит, что надо сделать, а ты как разработчик понимаешь, что тебе надо новую базу написать или как минимум новый индекс имплементировать, — и такой: блин, давай что-нибудь другое сделаем. А тут всё сошлось. Уникальный, прикольный кейс.
[1:36:03] Стас: Ну да, но даже если это компания и разное дело, всё равно надо разговаривать. Чем лучше ты понимаешь, какие констрейнты есть у других команд, тем лучше оно будет. Сейчас мы занимаемся анонимизацией данных на бранчах. Это одна из вещей, которые люди сильно хотят: у тебя есть production, ты сделал бранч production, и видишь там не оригинальные данные, а весь твой parent анонимизирован — имена заменены на другие имена. Много всего из этого можно сделать автоматически, ну и дать пользователям оверрайды: не хочу автоматически анонимизировать вот эту колонку — напишу правила. Тут уже есть инфраструктура расширений и утилит, которые это делают. Но начинается много всяких подходов. Есть динамическая маскировка данных, когда в бранче на уровне хранения оригинальные данные есть, но ты их маскируешь на уровне Postgres — у тебя есть роли, и ты можешь многое десятчить в трансформации данных. Условно, если будет какой-нибудь эксплойт к Postgres, то ты данные возьмёшь и прочитаешь. Либо можно делать статическую маскировку, когда ты действительно их перетрёшь: если поломаешь Postgres, данные уже перетёрты. Тут очень много вариантов, как это можно сделать, и нужно, чтобы люди разговаривали. С существующими решениями на рынке есть ограничения, но не все из них фундаментальные: много чего можно менять достаточно небольшими усилиями, если залезешь во внутренности Postgres.
[1:37:40] Александр: Да, это получается такое следующее развитие идеи бранчевания: давайте будем бранчевать плюс маскировать какие-то данные, и тогда мы заанлочим ещё один до этого закрытый юзкейс — когда я хочу протестировать свой код на продакшн-базе.
[1:38:00] Стас: Да, кстати, эта штука с маскировкой выглядит прикольно, когда видишь это end-to-end в тех же preview-окружениях. Ты написал приложение, у тебя есть production, открываешь pull request, открываешь ссылочку — и видишь, что в этом приложении, в том же UI, теперь совершенно другие данные. И чаще всего там даже дизайн едет. Не дело, но смотришь в свой же UI, и в нём другие данные, он немного чужой, и это произошло автоматом. Когда первый раз увидел это — ну ты сделал маскировку, знаешь, что бранчи замаскированы. Но следующий шаг, что прямо в твоём приложении теперь другие данные появляются, и оно работает само собой, — достаточно прикольно выглядит.
[1:38:58] Александр: Да, круто. А ещё можно дальше пойти, и я думаю, так и будет: часть функциональности фиче-флагов просто убрать. Насколько я понимаю, есть два основных юзкейса у фиче-флагов. Первый — когда ты закрываешь под фиче-флагом какую-то фичу и делаешь A/B-тестирование на части реальных пользователей: смотришь, увеличивается ли конверсия или количество кликов на кнопку в зависимости от её цвета. Это один юзкейс. А второй — ты разрабатываешь фичу в мобильном приложении или в вебе и не уверен, что стейкхолдерам, бизнесу эта фича вообще зайдёт и они захотят раскатить её на всех. У тебя есть специальный сабсет юзеров, для которых ты фичу открываешь, — их там пять человек. Они смотрят у себя в отделах: не, не нравится мне эта кнопка, не будем раскатывать. И чтобы показать им на реальном продакшене эту штуку, ты почти систему пишешь свою. А получается, что ты мог бы просто её задеплоить и вот так же им скинуть ссылочку: посмотрите. Проблема решается одним кликом. Тоже очень крутой юзкейс.
[1:40:22] Стас: Ну это как раз юзкейс, где хочется не маскировать данные, а показать именно продакшн.
[1:40:28] Александр: Круто. Про бранчинг мне прям понравилось. И я так понимаю, что если мы говорим про маскирование, то один из путей, как это можно решить, — приделать row-level security. Когда мы на каждую строчку в базе делаем проверки, которые говорят: а может ли это видеть этот человек, может ли он это модифицировать? Обычно, когда говорим про role-based access control, имеем в виду доступ к таблицам, базам, схемам, иногда колонкам — но это редко. А вот конкретным строчкам в таблице обычно такого нет. Получается, вы в каком-то виде имплементируете row-level security, и возможно, это может выйти не только за маскирование данных, но и как отдельная фича.
[1:41:24] Стас: Да, но row-level security обычно просто говорит, можешь ли ты видеть эту строчку или нет. Это мы, кстати, сделали такую штуку сейчас с авторизацией. Наш прокси, как мы разрешаем коннекции к базам, — по Postgres-протоколу, но кроме этого ещё и по HTTP. Что там важно во всяких окружениях: Vercel Edge Functions — продукт называется — или Cloudflare Workers, более знакомые, — это такие JavaScript-рантаймы, где ты деплоишь свой код сразу в кучу дата-центров, и тебе дают endpoint, прикрытый anycast: твой запрос уйдёт в ближайший к тебе дата-центр, там кусочек JavaScript-кода исполнится. И этот кусочек исполняется в V8, в таком security-контексте, который называется V8 isolate. В этом isolate у тебя нет окружения Node.js — это больше похоже на JavaScript-окружение во вкладке браузера: ты можешь открывать HTTP-соединение, но не можешь открыть произвольный TCP-connect. И много людей пользуются Cloudflare Workers, и это всегда проблема, если нужно сходить в базу, которая только по TCP разговаривает. Мы экспозим ещё на HTTP, и недавно тоже прикрутили авторизацию: этот HTTP-endpoint умеет понимать токены — условно, Clerk или других auth-провайдеров. Сначала идёшь на аутентификацию, получаешь токены, потом в базу идёшь с этим токеном, а в токене написан условно user ID, и можно настроить RLS-правило, что с таким запросом можно видеть, а что нельзя. По сути такой SQL с фронтенда, который можно сделать, только если у тебя есть токены, и в этом токене будет отрезано то, что нельзя видеть.
[1:43:36] Александр: Ну такой гибридный, уникальный подход.
[1:43:41] Стас: Гибридный, это мы недавно только зарелизили, и это ещё не полный продукт: есть только auth, там ещё много всего делать, чтобы можно было нормально писать приложение. Но я упомянул RLS. Этим тоже занимаемся, но немного с другой стороны. С точки зрения анонимизации мы вообще взяли за основу расширение: был уже такой PostgreSQL Anonymizer, их несколько, но над этим ещё работаем. Делать ли в PostgreSQL специальный саппорт, чтобы оно быстро работало, или расширение менять — пока у нас в процессе.
[1:44:17] Александр: Прям интересно: чем больше говорим, тем яснее картина становится, и тем больше вопросов. Но, говоря про вопросы, я бы напомнил, что у меня есть телеграм-канал, в котором я запостил и попросил читателей задать вопросы Стасу про Neon, на которые он бы хотел ответить. Но прежде чем задавать эти вопросы, у меня к тебе есть свой: хотел бы ли ты в рамках сегодняшнего выпуска проговорить ещё что-то про Neon, что мы не затронули?
[1:44:52] Стас: Наверное, про половину систем проговорили — много чего можно ещё. Из того, что интересно, — то, как работает автоскейлинг compute: лайв-миграция VM, их плат-девайсов. Область, где мы пришли к таким решениям, что, если бы я услышал это без обоснования, мне бы показалось, что решение странное и не очень интересное, но там есть причины, почему оно именно так. И Amazon, кстати, тоже в конце концов пришёл к таким же подходам, и не с первой итерации. У Aurora был serverless V1, сделанный по-другому, и они его в какой-то момент полностью закрыли и переписали — то, что называют serverless V2, а serverless V2 — это миграция виртуалки. Обычный QEMU. Почему так делаем? Во-первых, виртуалка всё равно нужна: тебе не хватает пода по причине безопасности. Мы не рассчитываем, что Postgres сам по себе — хороший security barrier. Мы всегда рассчитываем, что пользователь может выбраться из своего Postgres и получить доступ. Мы не даём рута на Postgres, но условно считаем, что пользователь всегда его может получить. Postgres — очень большая база, очень большая поверхность атаки, и такие CVE проходят, случаются. Поэтому хорошо закладываться, что люди могут выбраться из своих Postgres, и тебе хотя бы один барьер виртуализации нужен. Есть другие подходы, кроме виртуалки: можно Kubernetes вместо подов на виртуалках запускать, для этого есть решения; во-вторых, есть штуки типа gVisor от Google, где ты перехватываешь syscall’ы операционки, и они оказываются в специальной пользовательской программе вместо операционки, которая проверяет безопасность. Короче, варианты есть. Это одна часть — что нужны виртуалки, нужна хорошая изоляция.
[1:46:36] Стас: А вторая штука — люди не любят дисконнекты. Ты не можешь сделать обычное вертикальное масштабирование, которое делается для HTTP-сервиса. В HTTP у тебя очень короткоживущие, ну или относительно короткоживущие запросы. Можно сделать так, что есть какой-то прокси, который придерживает новые запросы, под капотом запускает контейнер побольше или виртуалку побольше и перенаправляет трафик туда. С базами данных типа Postgres это работает плохо. Aurora Serverless V1 была как раз так написана. Оказывается, в базах важно, что у тебя долгоживущий запрос: во-первых, можешь делать большую аналитическую транзакцию, во-вторых, у тебя может быть контекст сессии — нельзя просто так перекинуть запрос с одного контейнера на другой. Ты можешь создать временную таблицу, которая на дисконнект потеряется, — тебе нужно всё это отслеживать; в самой сессии базы очень много всего. Из-за таких вещей оказывается проще перекинуть. Ты хочешь отмасштабировать базу вверх — тебе проще заняться миграцией VM. У тебя есть какой-то хост-нода, это всё Kubernetes, но внутри пода мы запускаем виртуалку. И если нам нужно докинуть ресурсов этой виртуалке, а на текущей Kubernetes-ноде их нет свободных, мы можем сделать live-миграцию и не порвать TCP-соединение. Тащим за собой IP-адрес, и у тебя TCP-соединение — там есть какой-то хикап, секунда, когда всё залипает, — но потом остаются те же TCP-соединения, те же транзакции, тот же контекст. И ты уже на новом месте можешь докинуть ресурсы. Тоже забавная техническая проблема.
[1:48:54] Александр: А можно ещё разок? Я в какой-то момент немножко потерялся — как раз сохраняется контекст. За счёт чего он сохраняется?
[1:49:00] Стас: Виртуалка смигрировалась. У тебя вообще всё, что было, — ты утащил весь стейт памяти, процессор, всё, что там происходило, оно уехало на новое место. Стейт TCP-соединения сохранился, который живёт в ядре.
[1:49:19] Александр: То есть вы скейлите виртуалки, а не поды?
[1:49:23] Стас: Ну да. В каком смысле всё вместе. В кубере есть понятие request и limit, и их нельзя менять динамически. Ну, на самом деле нельзя было — начиная, по-моему, с 1.27 или 1.28 версии кубера это можно, но это экспериментальная фича, возможно, мы этим воспользуемся когда-нибудь. По факту мы не пользуемся кубернетисовскими request’ами и limit’ами. Мы ставим свои лейблы на поды, написали плагин к Kubernetes-планировщику, скедулеру, — это стандартная возможность кубера писать плагины к скедулеру. Этот плагин смотрит на наши аннотации подов, сколько места есть на ноде, можно кого-то туда ещё положить или нет. Важно, чтобы это проходило через основной планировщик кубера, потому что у тебя есть рейс между созданием пода и миграцией существующего пода куда-то: ты мог начать миграцию — там ещё было место, но если планировщик кубера об этом не знал, то к моменту, когда ты туда доедешь, эти ресурсы уже могли кому-то отдать.
[1:50:44] Александр: Идея понятна: получается, вы делаете скейлинг как stateful TCP-соединение.
[1:50:59] Стас: Да. Если никогда не сталкивался, это может казаться странным, что оно работает, но это достаточно commodity-штука. VMware и все остальные умели это делать давным-давно. Мы пользуемся QEMU — это тоже open source, — и куча других гипервизоров умеет. Есть Linux и KVM. Что такое виртуалка: если сам пишешь гипервизор, у тебя есть KVM, который настроит mapping памяти, и появится API — можешь создать виртуалку, подложить ей образ, внутри начнётся своя жизнь, загрузится операционка, что-то начнёт происходить. И в зависимости от того, как настроишь интерфейс общения с этой виртуалкой, — какое-то количество socket-like, vsock, vnet, — у тебя есть API чтения и записи из/в, ты можешь эмулировать драйверы. Нужен основной сетевой плюс API контроля питанием процессора: можешь её просто затормозить, после этого всю память прочитать — это массив байт, всю память читаешь, сериализуешь в сетку в новом месте, потом отдельно копируешь стейт процессоров, и всё, поехал. Внутри виртуалки никто об этом знать не будет — там начнутся спецэффекты со временем и всем остальным, поэтому надо уделять внимание, но с точки зрения гипервизора это просто массив. Если писал когда-нибудь код для WASM — вот то же самое: что такое код на WASM с точки зрения JavaScript — есть просто массив байт, есть какая-то execution-штука. Написать live-миграцию для WASM было бы даже проще: это массив в JavaScript, много способов есть этот массив отправить куда-нибудь и запустить в новом месте. Там надо ещё сдампить регистры execution-девайса правильно — я не помню, есть ли у WASM такой интерфейс, чтобы это сделать; предположим, что есть, тогда это просто.
[1:53:15] Стас: С live-миграцией есть несколько основных подходов. Ты можешь остановить source, скопировать память, потом скопировать state процессора и запустить на новом месте — это достаточно медленно, потому что если у тебя было 50 гигабайт памяти, то не нулевое количество даунтайма. Поэтому делают чуть хитрее — есть две основные стратегии: pre-copy и post-copy. Ты можешь либо начать миграцию до того, как останавливаешь виртуалку на source, но в бэкграунде детектить, какую память ты пачкаешь: послал байты на исходной виртуалке, отмечаешь битмапик, какую память пачкаешь; отправил оригинальный снапшот, потом дельту, ещё дельту, — и оно может либо сходиться, либо нет: вообще говоря, ты можешь пачкать память быстрее, чем работает сетка, потому что память фундаментально throughput больше, — будет зависеть от ворклоуда. Либо можно в обратную сторону: не переносить никакую память сначала, а перенести стейт процессора, и как только на приёмнике пытаешься что-то прочитать, чего там нет, делаешь page fault обратно и читаешь из источника, — параллельно ещё копируешь балком всю память, чтобы у тебя был один процесс, который переносит всё, плюс когда вне очереди нужна какая-то страничка, которая ещё не приехала, ты её спрашиваешь. Опять же, и то и другое есть в QEMU, и в приватных гипервизорах. Эта штука оказалась проще, чем я ожидал.
[1:55:00] Александр: Да, я тоже удивлён, что такая штука довольно прямолинейная, но решает задачу.
[1:55:08] Стас: Больше вопрос на нетворке — чтобы у тебя сессии не порвались. Там много подходов есть, самый простой, детский, — просто тащить IP за собой. Условно, играешь в контру в сетке, в одной физической LAN, и если кто-то у тебя назначает себе чужой IP-адрес, он его получает — то есть вы просто деретесь, нет никакого контроля на уровне интернета. Опять же, зависит: есть port security, есть всякие навороты, но в базовом варианте ты к себе какой хочешь IP-адрес, такой назначаешь, а все остальные умеют понимать изменения маппинга IP в ARP — физические адреса сетевых. Это самое простое: смигрировал с одного компьютера на другой, IP-адрес за собой потащил — и оно работает. Это только мы сделали, возможно, придётся переделать, потому что есть вопросы к тому, насколько один сегмент сети может быть большой физически. Это будет работать в одном физическом сегменте сети, ну или в двух уровнях. В кубере на разных нодах разные диапазоны IP-адресов — кубер тебе обычно такого не позволяет, поэтому пришлось немного помудрить: в каждый под вставляем ещё один сетевой интерфейс, который мы полностью контролируем и тащим за собой, поверх этого накручена штука VXLAN. Если встречался — она просто делает то, что сейчас называется software-defined networking, но это самый простой вариант: просто VLAN-девайс, ещё одна виртуальная сетка.
[1:56:49] Александр: Слушай, пока мы про это разговариваем, у меня в голове крутится небольшой диссонанс, который я наконец сформулировал. Обычно, когда говорим про базы данных, мы говорим про максимальное выжимание ресурсов из железа: оптимальность должна быть супер, базы данных — из самых заоптимизированных вообще софтверов. И одним из препятствий в оптимизациях для разработчиков баз данных является работа самой операционной системы. Такие приколы, как флаг O_DIRECT при записи, на который Linux ругался: разработчики баз данных хотят наиболее эффективно, в обход page cache операционной системы, писать, — и в целом операционная система базам данных строит барьер, который приходится обходить. А тут мы накручиваем этих барьеров на порядок больше: всё в клауд засовываем, в кубер, ещё виртуалка. Как это отражается на перформансе и, возможно, на тех предположениях, которые разработчики Postgres делали, что они бегут на Linux, а теперь в таком окружении? Есть какие-то инсайты — что на самом деле не особо, или наоборот, есть места, где сильно проигрываем?
[1:58:27] Стас: Кубер — это оркестрация, он на перформанс не влияет. Вопрос про что ты паришься — про нетворкинг и как он сделан, но там всегда есть возможность использовать железо. Для простоты давай не про нетворкинг, а про CPU и память. Виртуализация — если это не вложенная виртуализация, то сейчас она быстрая: в железе есть поддержка ещё одного уровня индирекции доступа к памяти. Во-первых, защищённый режим самого CPU — процессор знает о процессах, у каждого своя виртуальная TLB, чтобы отражать виртуальную память на физическую; и есть такая же поддержка ещё одного уровня косвенности с виртуалкой. Тут прямо сложно что-то намерить в плане перформанса. Другое дело, что ты сильно зависишь от того, насколько твой провайдер overcommitted по ресурсам, — и все это делают. У тебя может быть шумный сосед, поэтому виртуалки в целом шумнее — просто потому что все overcommitted. Если важна стабильность, то или железные надо, или размер виртуалки, равный ноде, — туда больше не засунешь. CPU легко oversubscribe-ить, с памятью гораздо сложнее. Условно, если ты смог зааллоцировать и что-то записать в эти, скажем, 256 гигабайт, то точно значит, что они твои, там никого другого нет. Если не используешь — чуть хитрее: последние годы научились и закоммитили в Linux всякие штуки, чтобы виртуалка могла отдавать неиспользуемую память. Не знаю, используют ли это в EC2, но по крайней мере сейчас это уже чуть более реалистично.
[2:00:41] Стас: Тут ещё важно сказать, что есть большая разница между OLTP-базами и OLAP-базами. Для OLTP-базы вот эта оптимизация перформанса, raw speed, гораздо меньше роли играет, чем для аналитических. OLTP-база большую часть своего CPU-времени проводит в борьбе с самой собой: блокировки, MVCC и всё остальное; это обычная row-oriented база, не column-oriented. По оценке Стоунбрейкера, две трети CPU-времени уходит на такую борьбу с самой собой. В аналитике, наоборот: если у тебя 100% ресурсов, они более-менее на то, что почитать, — это твой агрегат или join. Зависит от нагрузки. Так что я бы сказал: да, много уровней абстракции, но они скорее про оперирование. В итоге ты будешь выполняться на CPU с виртуализацией — это ещё пара парабитиков стоит, и ещё одно значение к адресу памяти нужно прибавить. Если ты по горячей памяти ходишь, то это почти нельзя почувствовать, а если по холодной — вообще без разницы: если у тебя оперативка далёкая, не в кэшах процессора, то виртуализацию ещё сложнее почувствовать.
[2:02:15] Александр: Классно, хороший инсайт. Я думал, что кастов довольно много, но вы аккуратно ко всему подходите, гранулярно, очень точно разрезаете там, где надо, — и система в общем работает довольно неплохо. А по поводу перформанса — есть ли у вас какие-то свои фреймворки, свой подход к тестированию именно перформанса поверх того, что уже есть в Postgres?
[2:02:51] Стас: Да, есть какое-то количество, в нескольких ипостасях. Мы стараемся максимально всё тестировать на самом сервисе: у нас есть staging нашего сервиса, и большинство перф-тестов просто используют наш API — стартануть и так далее, — потому что часто регрессия не в том, что ты в коде что-то поменял, в Postgres или Storage, а в том, что что-то в Kubernetes настроил по-другому, другой тип ноды, — и результат тот же, что оно медленнее работает. У нас достаточное количество тестов, написанных на питончике, они периодически запускаются, результаты складываются тоже в Postgres, и поверх этого Postgres строятся графики в Grafana. Перформанс в Postgres — очень широкая тема: есть и транзакции в секунду, и скорость вставки; это пространство, в котором сложно найти базис. Как-то нашли косяк: были типы вставки, на которых мы очень медленно процессили write-ahead log. Комбинация факторов и ошибка в коде, не критическая, но приводившая к тому, что на некоторых шардах тратили очень много CPU. Проверка размера таблицы использовалась как признак того, существует ли эта таблица. И такая проверка была на шардах, но на шарде факт того, что размер локальной таблички ноль, не значит, что её не существует, — потому что она может быть на других шардах. Это приводило к тому, что определённые кэши не срабатывают, хотя должны были. И пока клиент в это не уперся, никакие наши тесты не нашли, потому что это специфическая нагрузка.
[2:04:36] Александр: Ну да, нужен пустой шард.
[2:04:38] Стас: Не, шард пустой, я ещё с точки зрения Postgres: там должна быть таблица, в ней колонка с длинными значениями, — у тебя есть штука в Postgres под названием TOAST, это когда данные хранятся не в самой таблице, а сбоку от неё, когда у тебя длинные строчки. По ней должен пройти автовакуум, — и вот тогда всё сходится, и у тебя очень медленная вставка идёт. Хорошего ответа, как лучше находить, мы внутри не придумали. Да, надо больше тестировать и разнообразных штук. Всё, что придумали, — давайте сделаем попроще добавление каких-то штук, и вентчурли это приведёт к тому, что тестов много и они будут чуть более репрезентативны.
[2:05:19] Александр: Окей, а эти тесты уже не являются частью какого-то open-source-решения, это внутренняя разработка?
[2:05:27] Стас: По-моему, всё в Neon есть — просто там директория с тестами, они замаркированы как долгоиграющие: в обычном локальном запуске не запустятся. Не помню, оно в open source или нет; наверное, тоже в open source — там есть CI-джоба для GitHub-раннера, для GitHub CI, которая их запускает. Скорее всего, тоже в open source, в точку-GitHub лежит.
[2:06:03] Александр: Понял. Слушай, про open source раз заговорили — тема тоже, с одной стороны, простая и очевидная, с другой — не всегда. Решение делать Neon open source — оно было изначально принципиальным, и других вариантов не было, или это больше какая-то другая причина? Потому что, например, Snowflake, очень похожая по концепции и духу разработка, она closed-source.
[2:06:30] Стас: Как я говорил, оригинально была идея, что Postgres-комьюнити ближе, чтобы был сервис, написанный членами Postgres-комьюнити, — поэтому имело смысл делать open source. Плюс просто анализ рисков: кто может поднять такой же сервис, используя наш код? У всех больших облаков, кроме Microsoft, есть конкурирующий продукт, который делает то же самое. Тут прямо малые-малые риски, что они возьмут и выкинут своё, чтобы взять наше. У Microsoft по другим причинам этого нет, но у них есть такой же конкурирующий сервис такой же архитектуры для MS SQL. Для Postgres у них нет — судя по всему, из-за их внутренней конкуренции: Microsoft использует MS SQL, все хорошие печеньки идут туда, хотя, возможно, они уже так не считают. Поэтому вряд ли кто-то из больших игроков возьмёт. Если кто-то из мелких игроков — ну тоже: нам проще этот код, мы его написали, нам проще его запускать, у нас есть конкурентное преимущество. Реально рисков очень мало. Если ты пишешь условный Redis, у тебя рисков чуть больше. Ну или Mongo — самому Microsoft. Но таких прямо очевидных нет. А бенефитов от того, чтобы быть open source, много: во-первых, людям видно, что ты делаешь — с хайрингом чуть попроще, инженерам тоже приятно, что их работа видна другим. Плюсы перевешивают минусы для нас. Во-вторых, да, это началось с того, что давайте всё open source по Postgres-лицензии.
[2:08:52] Александр: Интересно. Я не до конца вглубь смотрел репозиторий. Сам Postgres, который вы используете, — он форкнутый и модифицированный, является частью этого репозитория или это отдельная штука?
[2:09:00] Стас: Open source, он тоже открыт. Является ли частью репозитория — зависит от того, считаешь ли ты git-сабмодуль частью git-репозитория. Он туда подключён как git-сабмодуль.
[2:09:10] Александр: Окей. И вот с этим форком вопрос, возможно, наболевший: какая-то часть кода там модифицирована, и её нужно up-to-date поддерживать с upstream’ом Postgres. Насколько это большая часть сил уходит на поддержание такого up-to-date состояния форкнутого Postgres?
[2:09:43] Стас: У нас не очень много. Этот патчик к Postgres достаточно мелкий — что-то порядка от 10 до 20 тысяч строк кода в диффе. Это мало. Сейчас у нас три версии Postgres, но это меньше месяца работы одному человеку — заадоптить новую версию. Основная работа там потому, что что-то меняется во внутренностях Postgres. Сам механический ребейс достаточно быстрый — какие-то дни, — а месяц, я имею в виду, чтобы открыть это уже каким-то пользователям в тесте и дальше. Ещё начинается история с расширениями: Postgres релизит новую версию, и через какое-то время расширения релизят поддержку этой версии. Тебе нужно открыть её с расширениями как провайдеру, или пометить какой-то бетой: ты открыл новую версию Postgres, но там ещё не все расширения доступны, потому что их авторы не поддержали эту версию. Смотря с чем сравнивать, но это не очень большой эффорт для нас. Не нулевой, явно. Компании, которые поддерживают свои большие форки Postgres, — тот же Postgres Pro, Enterprise DB, — там была большая эпопея: все люди садились на несколько месяцев каждый год и переносили свои фичи с предыдущей версии на новую. Потом проходят тесты, где ты тестируешь индивидуальные фичи, а потом начинается эпопея с комбинациями фич: встроенный пуллер вместе со встроенным сжатием одновременно протестировать. Там квадратная матрица, много интересного вылезает.
[2:11:27] Стас: Чтобы тестировать тройки фич — до такого никто никогда не доходил.
[2:11:31] Александр: Да, это жесть, конечно, матрица тестирования. Это уже не матрица, а куб тестирования будет.
[2:11:37] Стас: Куб тестирования, да. На двух измерениях все останавливались.
[2:11:41] Александр: Да, блин. Получается, у вас довольно аккуратный миниатюрный форк. Я так понимаю, эта история плюс-минус навсегда? То есть не планируется как-то…
[2:11:53] Стас: Слушай, мы, кстати, по-разному думали. Мы сначала хотели такой абсолютизм делать и стараться все наши изменения загомить. Они в целом все достаточно разумные и так или иначе имеют смысл в Postgres. Одна из штук, которые нам нужны… Важная оговорка: большая часть нашего кода вообще в расширениях. Postgres очень экстенсибл. В самом Postgres мы делаем модификации только там, где либо совсем нельзя в расширении, либо не хватает какого-то API: мы просто API в Postgres добавляем, а код всё равно в нашем расширении живёт. И один из таких API — это Storage Manager API. Это доступ к файлам на диске такой постраничный, простой API, в нём семь функций — условно, прочитать 8-килобайтную страничку из файла, записать, получить размер файла, ещё всякие. В Postgres был такой API, который, по-моему, году в 18-м удалили. Там было две имплементации: одна называлась md, Magnetic Disk, вторая — Sony Jukebox. Sony Jukebox — это кто-то когда-то приделывал к Postgres хранение на магнитной ленте. В 2017-м лет двадцать этим никто не занимался, ну и решили, что неплохо бы удалить. Но это API можно снова вернуть в Postgres. У Postgres обычно такая политика: API должно быть полезно для самого Postgres, — и оно могло бы быть полезно, если через него можно сделать сжатие или шифрование. Но чтобы закоммитить это, нужно сделать достаточно хороший плагин для основного функционала Postgres, за который потом другие люди будут отвечать, — ребейзить его, если что-то в Postgres меняешь. Не нулевое количество работы, которая нам не сильно нужна. Мы этим занимались, потом решили подзабить: нам не слишком важно иметь это закоммиченным, а во-вторых, эти усилия можно потратить на штуки, которые полезнее и комьюнити, и нам, чем это API.
[2:14:11] Стас: Последнее время, на что мы смотрим, — это поддержка тредов в Postgres. Это никак не уменьшает наш патч в Postgres, но количество потенциальных бенефитов от того, что Postgres не мультипроцессный, а мультитредовый, для нас и для всех остальных сильно больше. Это огромная история, и кажется, что ей полезнее заниматься, чем иметь такой гол, чтобы у нас был нулевой патч к Postgres. А вот делать свой форк мультитредовым точно не хочется: ты переписываешь, у тебя будут изменения чуть не в каждом файле, — больше механические, но это значит, что ты уже автоматически ничего не перебейзишь, или это будет очень сложно. Ты улетаешь в историю, где очень большая часть кодовой базы поменялась. Это лучше делать через основной Postgres. Это тоже история на много лет, много чего нужно переделывать, много про что договариваться. Расширения будут несовместимы, потому что расширения не поддерживают мультитрединг, — тебе нужен какой-то API, чтобы расширение объявляло, что поддерживает мультитрединг. Долгое время Python был блокером: сам интерпретатор Python не поддерживал запуск нескольких интерпретаторов в разных процессах, потому что в кодовой базе Python тоже используются статические переменные. И там не про Global Interpreter Lock, а про инстанциацию нескольких независимых интерпретаторов в разных тредах. А Postgres поддерживает Python как один из языков, на которых можно писать хранимки. Сейчас это не проблема, и, кажется, со всеми языками для хранимок это не проблема. Это тоже одна из причин, почему мы этим начали заниматься.
[2:16:10] Александр: Да, было бы прикольно потом как-нибудь, по достижении каких-то больших штук по этому направлению, — многопоточность в Postgres, — записать отдельный выпуск, потому что история животрепещущая, и, наверное, все, кто пользуется Postgres, так или иначе получат от этого бенефит. Огромный контрибьюшн в комьюнити.
[2:16:34] Стас: Ну да, это много работы, и непонятно, в обозримом будущем сойдётся или нет. Но постепенно, особенно Хейки, активно этим занимаются. Пока так: переделывать мелкие вещи, которые создают проблемы именно в мультитрединг-сетапе. Только там сигналы от процесса к процессу отправляются. Но это небольшие рефакторинги. Есть страничка на Postgres-вики, где этот roadmap выписан, но пока коммитят туда рефакторинги, которые в целом полезны. Если мультитрединг не случится, это всё равно полезные рефакторинги. Это с точки зрения того, как работают с комьюнити: давайте начнём с вещей, которые в любом случае будут полезны. Потом будет большая работа, и, наверное, самое большое изменение — просто замаркировать все статические переменные как thread-local. Это очень механическая работа, большое изменение, и там можно по-разному: либо статические переменные унести на какие-то контексты, либо написать макрос. Начинаются вопросы, как детектить, если кто-то добавит статическую переменную, которая не окружена этим макросом. В Clang, в LLVM много тулинга для этого, который, вообще говоря, работает не очень хорошо. Я думал, он лучше работает, чем на самом деле. Но в общем можно сделать автоматические тесты на то, что у тебя неразмаркированных статических переменных нет. Возможно, придётся патчить и Clang в LLVM тоже — не то чтобы там что-то глобальное, но хочется написать скрипт, который идёт по кодовой базе и скажет, есть ли у тебя статические переменные без thread-local, или выпишет их все. В LLVM есть, всякими атрибутами пользуешься. Короче, там есть нюансики — гораздо больше, чем я думал. Это API, которыми обычно пользуются всякие language server’ы, но там не нужна прямо 100% точность: если у тебя 99% точности, это норм — language server’а никто не заметит.
[2:18:52] Александр: Прикольно. Фух, ну да, надеюсь, что будет прикольно. Слушай, вот такой стратегический вопрос, возможно, не имеет особого смысла спрашивать его у тебя, потому что ты бизнес делаешь на Postgres, и ответ будет очевидный. Но за несколько лет работы над Neon — как ты думаешь, Postgres дальше будет жить и развиваться, и вот эта ставка на то, что основная кодовая база compute будет Postgres, — она в долгосроке сыграет? Или есть предпосылки, что, возможно, когда-то придётся всё переписать?
[2:19:37] Стас: Большой вопрос. Если хочешь сделать сильно масштабируемую базу, у тебя два варианта: либо менять какую-то существующую open-source базу, либо всё с нуля написать. Базу с нуля писать — условно, лет десять. И тут вопрос: у тебя хватит этого момента, импульса, чтобы сделать всё правильно с нуля, как у компании? При этом Postgres тоже не стоит на месте — будет медленнее развиваться, но будет. Всякие примеры — TiDB, CockroachDB, — казалось, они тоже правильно делают, и начали лет шесть-восемь назад. У CockroachDB хватило денег и ресурса прямо писать код лет пять. Потом возникает давление, что это надо продавать. И тут ты, когда замораживаешь текущее состояние со всеми проблемами, тебе его сложнее сильно менять. В текущем окружении того, как люди думают про проблемы, как проекты финансируются, сколько люди сидят в одной компании, — кажется, ты можешь heads-down, прямо в стол, писать года три. Потом надо это или продавать, или закрываться. И кажется, что три года — мало, чтобы написать хорошую базу с нуля. Есть подтверждения с рынка. Если тут будет какой-то disruption — это кто-то, кто сможет получить много денег, а потом убедить очень много людей писать это с каким-то виженом долго. Я не думаю, что это что-то, что можно написать за несколько лет; это нужно писать долго, и это сложно. Таких примеров давно не видел. Есть в ресёрче, но там всё равно чуть другое. Fuchsia, операционка, которую Google пишет, тоже: они достаточно долго писали её почти без кастомеров — в Nest-термостаты ставили, но это просто чтобы хотя бы… В Nest, честно говоря, неважно, что ставить. Но, по-моему, тоже сворачивают лавку — туда ещё кто-то коммитит, но ресурсы сократили. Особенно когда появился AI, они стали скептичнее относиться к таким проектам, где очень много сильных людей работает, а выхлопа, именно не-ресёрч, нет.
[2:22:06] Стас: С Postgres сложно: даже если хочешь побороться, побороться сложно. Сделать лучше можно. Моё впечатление — мнимая сложность: сделать Postgres со всей его скоростью ледника или сделать это отдельной компанией с нуля — и то и другое очень сложно.
[2:22:26] Александр: Да, я понимаю основной посыл. Действительно, база данных — это штука, которую надо пять-семь лет просто сидеть писать немалыми ресурсами: там должны быть квалифицированные разработчики, вижен, — недешёвое удовольствие. А выхлопа за эти пять-семь лет будет ноль.
[2:22:52] Стас: Это можно было бы сделать, если бы был гигантский рынок или гигантская проблема. По-хорошему, OLTP-базы — это довольно решённый вопрос. Обычный Postgres в RDS, в Neon и ещё где-то работает для очень большого количества кейсов. Количество кейсов, где нужно что-то фундаментально другое, маленькое, и размер opportunity тоже не то чтобы гигантский. Он большой — миллиарды долларов; весь database-рынок — это сотни миллиардов долларов, с большой константой впереди. Но большая часть этого рынка, в плане денег, в аналитике: там больше денег. Тот же Snowflake написал почти с нуля — там были люди, которые сразу знали, как и что делать, написали хороший планировщик. В аналитике в целом чуть меньше барьер, чтобы сделать минимально рабочий продукт, потому что он должен быть быстрый, а high reliability не так важна. Это более эластичный рынок: кто в аналитике быстрее работает, туда кастомеры идут. В OLTP у тебя есть скорость, а есть ещё просто доверие к системе, потому что это System of Record, там хранятся данные, — вполне разумно, что много кастомеров будут идти туда очень медленно. Если сделаешь идеальную OLTP-базу, размер рынка большой, но не гигантский. Я к тому, что если бы количество денег, которое ты в конце получишь, было в 10 или 100 раз больше, — думаю, кто-то бы разобрался, как это сделать. Это всё равно не выглядит как что-то невозможное. А так кажется проще инкрементально.
[2:24:48] Александр: Да-да. Вообще интересно, мне понравилось, к чему мы пришли: как прошлись по Neon и Postgres. Информации очень много полезной намайнили сегодня. Что, может, на вопросы ответим?
[2:25:07] Стас: Можно быстро. Были вопросы, я даже помню какие-то про Rust.
[2:25:11] Александр: Да, я прям сверху вниз буду зачитывать. Напомню, что это вопросы из телеграм-канала «Тысяча фичей». Первый вопрос от Александра Ф., звучит так: как работают read-only реплики при удалённом хранилище в Neon?
[2:25:30] Стас: Читают данные с удалённого хранилища. Реплика подписывается — мы говорили, что есть эти два тира хранения, Safekeeper и PageServer, — и реплика подписывается на write-ahead log Safekeeper’а и его проигрывает. Если у реплики какой-то странички нет, то она читает её с PageServer’а. Прямого соединения от primary к secondary нет — реплика забирает это не по прямой, а через Safekeeper. Primary пишет WAL на Safekeeper, реплика читает WAL с Safekeeper’а, но ей ещё нужна сама база, к чему это применять, — и она читает это так же, как и primary, со storage. Дальше есть дизайн-решение: делается со страницей, которая у тебя на реплике есть в буфере, закэшированная. Потому что приходит в write-ahead log — и ты можешь локально либо ей redo сделать, либо выкинуть и просто прочитать со storage, потому что storage уже redo сделал. Мы и так, и так пробовали. Я, кстати, не помню, какой из двух вариантов пошёл в продакшн, если честно, про реплики — надо посмотреть в код. По-моему, сначала мы выкидывали страничку — просто потому что так проще с точки зрения конкурентного доступа. Но это чуть медленнее: тебе нужно эту страничку прочитать. Не помню, вышло ли это изменение в прод. По-моему, сейчас мы делаем redo на самой реплике локально — странички, которые на реплике есть, реплеим. WAL, который меняет страничку, которой у тебя нет, точно не имеет смысла читать и реплеить — мы просто WAL игнорируем. При этом с репликой уже такие Postgres’овые особенности: там много нюансов с тем, как её включать в режиме, где нет всех данных. В Postgres реплику просто стартануть с чекпоинта и не очень просто стартануть не с чекпоинта: если стартуешь реплику с произвольного LSN, она на самом деле подождёт следующего чекпоинта и не будет стартовать. Для нас это плохо, в это, кстати, много сил было зарыто. И здесь тоже проще поменять в Postgres, потому что это небольшая проблема для Postgres, вот этот старт реплики, а большая для нас. Но менять по-хорошему надо и там, и там. Тут штуки вроде commit sequence number могли бы помочь — тоже отдельная история, что это и зачем.
[2:28:21] Александр: Слушай, мне кажется, ответ исчерпывающий, спасибо Александру. Остались два типа вопросов. Первый — про Rust, про него я бы в конце поговорил, как завершающую закрывашечку. А ещё один тип — про, я так понимаю, конкурента. Дима В. спрашивает: чем Neon отличается от Nile — the Nile, thenile.dev?
[2:28:45] Стас: Есть такой проект под названием Supabase. У них обычный Postgres, который они сетапят, но вокруг него много всяких полезных штук сделали: авторизация. Короче, это Postgres-based framework для разработки приложений. Мы таковым себя особо не считали, скорее оригинально были просто Postgres. Но постепенно тоже движемся в эту сторону — через призму того, как нас воспринимает рынок. Люди воспринимают нас как конкурента Supabase. Часть вещей сделана по-разному, но за счёт конкуренции фичесет выравнивается, и Supabase тоже начинает больше внимания уделять бранчам и удалённому storage, больше вкладываться в сам Postgres. Это, наверное, основной конкурент, и они начали чуть раньше нас, по-моему, на год или полтора. Есть PlanetScale — хорошая база, но это MySQL-мир. Его делают ребята из GitHub, бывшая database-команда GitHub — GitHub был на MySQL (не знаю, сейчас всё ещё ли, вряд ли). Оригинально там была большая инсталляция MySQL, и вот те же люди это делают. У них очень хорошо задизайнены интерфейсы, консольки, — туда ещё часть дизайнеров из GitHub пошла.
[2:30:13] Стас: Кто ещё есть? Самые близкие в плане того, что делаем… Nile делает немного другую штуку — multitenant-базу. У них есть смарт-роутер, который смотрит на user ID, и в каких-то случаях ты можешь попасть в одну и ту же базу, в один и тот же Postgres, а для каких-то своих пользователей можешь отселить на отдельный Postgres. Условно, у тебя есть B2B SaaS или социальная игра, и есть все пользователи, но есть, скажем, один очень популярный. Например, блогинг-платформа: у тебя есть все блоги, средний блог никто не читает, но удалять их всё равно нельзя, и есть какие-то очень популярные, — и для очень популярного можно его отселить на отдельную базу. У них такая проблема: им приходится на своём прокси-роутере парсить SQL, и мне кажется, это очень сложная задача. Нельзя сделать прямо очень reliable для всех типов стейтментов. Тут сложно найти баланс: какую функциональность ты пользователям не разрешаешь, а какую разрешаешь, чтобы делать этот роутинг прямо надёжно, а где-то можешь сделать ложноположительный/ложноотрицательный, и что ты делаешь с качеством в каждой из этих ситуаций. В целом идея хорошая, полезная деятельность, они делают это в multi-tenant-сетапе.
[2:31:56] Стас: Есть ещё примерно такой же технологический, в плане того, какой код нужно писать, но немного другой юзкейс: можно per-statement, например, если ты можешь детектить, это read-only или read-write стейтмент, — read-only роутить на реплике. В идеале ещё накинуть географически близкую реплику: у тебя есть одна read-write база где-то в одном месте, реплики по всему миру, и если можешь задать, что стейтмент read-only, — отправишь его на реплику. Это тоже задача сложнее, чем кажется. Условно, любая функция в таргет-листе SELECT: SELECT x FROM y — from table с колонкой y, — ты не знаешь, что делаешь в функции x. В стейтменте нет тела этой функции, а функция может что-то куда-то писать. Тебе нужно как-то определять семантику того, как ложноположительные и ложноотрицательные работают. И как будто эта задача не для SQL-парсинга, а чуть поглубже, где уже есть понимание read-only / не read-only — когда ты планируешь. Если ты в Европе и хочешь зароутить это, у тебя должны быть задеплоены роутеры по миру, и роутер должен понять, read-only это или нет, куда его отправлять. Этим роутером на самом деле может быть реплика: если реплика во время экзекьюшена понимает, что это не read-only, а read-write, то ты выкидываешь ошибку, разматываешь стек и отправляешь на read-write. Это сработает с single-statement транзакциями. А если у тебя интерактивная транзакция, и ты на пятом стейтменте это понял, — теперь уже приложение должно быть умным и понимать, что нужно транзакцию заново запустить, возможно, с каким-то флагом. Вот это был вопрос про the Nile — да, Nile этим занимается.
[2:34:00] Стас: Prisma недавно запустила свой Postgres-сервис. Prisma — это такой JavaScript ORM. Они долго делали Prisma Cloud, где был такой роутер, похожий на то, про что я говорил, и решили сделать свой Postgres. У них есть бетка. Они тоже играются с виртуалками. Там интересно: ты не можешь приконнектиться к этому Postgres. Я думаю, это by design — там request-response модель, request-response только через их приватный API. Ты написал код на этом ORM и запускаешь его к их серверу, и в программе оно работает, но приконнектиться к этому Postgres ты не можешь. Это будет интересный сигнал, если у них сработает — если они смогут из этого сделать бизнес-модель. Потому что кажется, очень много людей будут иметь проблемы: ты не можешь данные смигрировать, загрузить, репликацию настроить. Это такая минималистичная база. Если они найдут свой рынок — прикольно, это валидация того, что в минималистичном сетапе оно работает и можно вкладываться в такие фичи больше.
[2:35:19] Александр: Прикольно. Думаю, Дмитрий удовлетворён ответом про Nile. Есть вторая группа вопросов — два про Rust. Я, на самом деле, удивлён, что мы говорили про продукт, часть которого написана на Rust, и слово «Rust» прозвучало раза три за весь выпуск — это вообще нонсенс. Поэтому надо закрыть дыру. Вопрос от Николая: были ли моменты, где Rust ограничивал и пришлось использовать другой язык, например, C, C++ или что-то иное?
[2:35:55] Стас: Нет, таких моментов не было. Вообще про Rust: на Rust не писали до того, как мы начали Neon. У всех было много опыта с C, C++. С C, C++ вообще нормально жить когда-то внутри большой компании, которая пишет на C, C++, — самописка, хорошие библиотеки есть, много чего есть. В open source с этим всем сложнее. Есть всякие библиотеки от Facebook, от Google, немножко билд-тулзов, но когда живёшь вне монорепы, в которой живут большие игроки, всё становится сложнее. Мне хотелось этим заниматься, мы решили попробовать: давай напишем на Rust. Там хотя бы пакетный менеджер хороший — это мы знали, потому что когда-то писали на Ruby, примерно одни и те же люди писали пакетный менеджер в Ruby и в Rust; хотя бы эта часть нормально сделана. Ну и как-то попробовали, и норм пошло. Мы начинали с идеей: давайте попробуем, если начнём упираться в краши компилятора, то возьмём и перепишем всё на плюсах. Такой риск был в голове, и начинали мы ещё в 21-м, когда, например, async ещё не был стабилизирован в Rust. Но нет, всё нормально. За выходные прочитать и нормально код писать ты можешь. Ещё не быстро, но нормально. От пары месяцев до полугода — и ты уже начинаешь понимать более сложные концепции в Rust, какие-то workaround’ы появляются. Каких-то больших проблем с Rust у нас не было. Там всякие cancellation safety — были вопросики, которые, я думаю, были бы уже в любом языке: нужно придумать правила, как ты пишешь код конкретно в своей компании под этой задачей, что-то ты пытаешься не использовать. С точки зрения использования нам всё нравится, никогда не хотели на что-то другое переписать. При этом, например, расширение для Postgres мы пишем на C. Можно было бы и на Rust, но проще было на C, и все знали, как это делать. Это, кстати, поменялось за последние несколько лет: появился проект pgrx, который сделал большой набор врапперов для Postgres API на Rust. Сейчас новые расширения мы пишем на Rust — просто потому что кто-то, кто не мы, сделал большую работу, чтобы это было удобно. Расширение на Rust сейчас вообще прям хорошо, и их удобнее писать, чем на C.
[2:38:59] Стас: Хайринг — внезапно, это сильно помогает. Одна из вещей, которую мы случайно сделали правильно: мы не думали про хайринг, когда выбирали Rust, но это прям сильно помогает. Много хороших людей на рынке, для которых это плюс. Иногда это люди, которые уже с Rust-комьюнити взаимодействовали и хотят продолжать, чтобы основной язык был Rust, — одна категория. Другая — кто хотел бы пописать на Rust, и для них это просто плюс один конкретно этому работодателю. Мы как-то не сильно фокусируемся во время интервью на Rust. Чем дальше, тем больше это вопрос специализации: специалист или дженералист, в какое количество мест в коде ты будешь ходить. Если в конкретной команде, которая пишет на Rust сервис, — чуть больше внимания на то, что человек знал про Rust, но опять же не определяюще. Если кто-то писал на современных плюсах и знает, что такое move-семантика, мы более-менее ожидаем, что человек быстро въедет в Rust и с первого дня сможет писать код — может, не с той же скоростью, но сможет. Помогает, что люди такие: о, окей, давно хотел поработать на Rust, но не хотел идти заниматься криптой.
[2:40:36] Александр: Круто. Как раз это был завершающий вопрос, который ты опередил. Про хайринг всегда довольно интересно. И тогда вопрос: а есть ли у вас какие-то позиции на Rust? Может, слушателей кого-то заинтересуют, и может постучаться.
[2:40:52] Стас: Да, у нас вообще перенаём. Сейчас есть позиции на Rust в Storage, и периодически новые появляются. Заходите на сайт, нажимаете jobs, там можно даже, по-моему, подписаться. Новые вакансии появляются — присылайте CV, можете писать мне напрямую куда-нибудь: в Telegram, в LinkedIn.
[2:41:11] Александр: Круто. Я думаю, что кого-то это точно заинтересует. Стас, спасибо тебе большое за такой большой, огромный выпуск, полный огромного количества интересных технических и нетехнических аспектов Neon и не только. Если тебе есть что напоследок сказать, пожелать — пожалуйста, твоё время.
[2:41:34] Стас: Спасибо, Саш, что позвал. Опять я не подготовился к этому твоему вопросу.
[2:41:40] Александр: А я его максимально открыто задал, так что…
[2:41:43] Стас: Хороших выходных — мы записываем это в воскресенье. Мы вообще много про open source поговорили. Open source — это хороший повод попробовать делать какие-то вещи, которые кажутся сложными и интересными. И это ещё способ поучаствовать в глобальном комьюнити, и больше, чем обычно ожидают, увеличивает количество пользователей и людей, кто читает и будет этим софтом когда-то пользоваться. Любой патч, закоммиченный в достаточно большой проект, привлекает какое-то внимание и потом надолго с вами остаётся. Я помню, по-моему, один из первых патчей, которые я в Postgres закоммитил давным-давно, был про поиск ближайших соседей. В Postgres есть поиск ближайших соседей по многомерным векторам — есть такое расширение cube по многомерным векторам. Мне по каким-то причинам надо было — он медленно работал, и я часть вещей поправил. Мне, по-моему, до сих пор какие-то письма про это приходят, где люди ставят какой-то сетап: находят эти мейл-листы, где ты это сделал, потому что кейворды не то чтобы самые часто встречающиеся.
[2:43:04] Александр: Не, ну сейчас-то на хайпе вообще.
[2:43:07] Стас: Сейчас это ещё одна жизнь — с pgvector, RAG и всем остальным. Мой посыл: пробуйте себя в open source. Это очень хороший способ расширить свой кругозор, особенно если что-то кажется сложным. Ты спрашивал про C — я когда-то, помню, ввязался во что-то и просто купил книжку у метро. C очень простой язык, можно за несколько дней начать нормальный код писать. Open source, сложные задачи — помогает всем чувствовать себя лучше.
[2:43:43] Александр: Это точно. Особенно когда есть какие-то сомнения по поводу профессионального развития — синдром самозванца. Иногда говорят: а могу ли я реально? Вот если большой проект, сделал pull request, его приняли и решил проблему — то точно можешь. Я думаю, это помогает и увереннее себя чувствовать, и на работе, и по жизни тоже. Стас, спасибо тебе огромное. Услышимся!