Aurora DSQL — PostgreSQL-совместимая распределённая база, которая до недавнего времени не поддерживала внешние ключи. Отсутствие FKвнешний ключ — ограничение, требующее, чтобы значение в столбце ссылалось на существующую строку в другой таблице называли самым большим разрывом между DSQL и «обычным» Postgres, и это блокировало многие миграции. Теперь ограничения поддерживаются, но механика их проверки в распределённой системе принципиально отличается от привычной: вместо блокировок работают снимки и обнаружение конфликтов на коммите. Разберём, как это устроено и что из этого следует.
Почему отсутствие внешних ключей было блокером
В обсуждении «Amazon wasted their time building DSQL» нехватка FK упоминалась как один из главных блокеров внедрения PostgreSQL-совместимой распределённой базы. Пользователь SteveTabernacle2 утверждал, что без внешних ключей нельзя по-настоящему иметь «relational» данные, и называл «Postgres-compatible» вводящим в заблуждение.
Схема TPC-Cотраслевой эталонный тест транзакционной нагрузки, моделирующий заказы и склады включает все стандартные связи, и проверки ссылочной целостности добавляют чтения и могут вызывать повторяемые конфликты сериализации при конкурентных записях — эту стоимость нужно учитывать при сравнении результатов бенчмарка.
Архитектура: журналы, Crossbar и разделение путей чтения и записи
Журнальный слой (Journals) — это высокодоступный репликационный лог, который надёжно сохраняет транзакции между зонами или регионами. Crossbar — слой маршрутизации, который читает обновления из журналов и отправляет изменения нужным узлам хранения.
Разделение путей чтения и записи решает проблему, возникшую после того, как весь коммит стали писать в один журнал. Такое решение упростило путь записи и обеспечило атомарность и долговечность ACID, но усложнило путь чтения: чтобы узнать последнее значение строки, приходилось проверять все журналы, потому что изменение могла внести любая из них. При добавлении журналов для роста числа транзакций в секунду неизбежно упирались в ограничения пропускной способности сети.
Crossbar разделяет масштабирование пути чтения и пути записи: он предоставляет хранилищу API подписки, позволяя узлам хранилища подписываться на ключи в определённом диапазоне, и при поступлении транзакций маршрутизирует обновления подписанным узлам. Каждый журнал упорядочен по времени транзакции, и Crossbar должен следовать за каждым журналом, чтобы построить общий порядокединая последовательность, в которой все узлы видят транзакции в одном и том же порядке.
Однако при масштабировании эта архитектура упирается в хвостовые задержки. Симуляция показала, что при 40 хостах вместо ожидаемого миллиона TPS достигалось лишь около 6 000 TPS, а хвостовая задержка выросла с приемлемой 1 секунды до катастрофических 10 секунд. Причина в том, что каждая транзакция должна читать с нескольких хостов, и вероятность столкнуться хотя бы с одной паузой GCсборка мусора — приостановка процесса для освобождения памяти приближается к 100%.
Консистентность DSQL обеспечивает иначе: система использует MVCCмноговерсионность — читатели видят снимок прошлого состояния данных с precision timestamps для координационно-свободных чтений и оптимистичный контроль конкурентностиконфликты выявляются не блокировкой заранее, а проверкой при коммите для записей, откладывая координацию до момента коммита через распределённые adjudicatorsкомпоненты, которые на этапе коммита проверяют транзакции на конфликты и выносят решение о фиксации и систему репликации Journal. Это минимизирует межрегиональную задержку, требуя координации только при коммитах, а не для отдельных операторов. DSQL даёт строгую согласованность (linearizability) и ACID-транзакции с непрерывной доступностью при отказах зоны доступности или региона.
Для транзакций, охватывающих несколько Adjudicators, применяется «Linearized 2PC»: adjudicators голосуют, но только лидер записывает финальный коммит в свой единственный Journal — так удаётся избежать координации нескольких логов. Multi-Region peered clustersпара кластеров в двух регионах, которая для приложений выглядит как одна логическая база данных предоставляют единую логическую базу данных с двумя региональными эндпоинтами, поддерживают одновременные чтения и записи и обеспечивают строгую согласованность данных.
Control plane: где принимаются решения о масштабируемости
Control planeчасть системы, которая управляет кластером: масштабированием, топологией и операциями без ручного вмешательства DSQL выполняет больше, чем CRUD-операции: это «мозг» hands-free операций и масштабирования, который обнаруживает перегрев кластеров и оркестрирует изменения топологии. Именно там принимаются решения о масштабируемости, безопасности и устойчивости, и они приоритизируются даже тогда, когда их трудно реализовать.
Изначально control plane был написан на Kotlin, потому что сервисы вроде Aurora и RDS доказали пригодность JVM-языков для control plane, а преимущества Rust (пропускная способность, задержка, безопасность памяти) не были настолько критичны. Также требовались внутренние библиотеки, ещё недоступные в Rust, и были инженеры, уже продуктивные в Kotlin.
Однако после интеграции с data planeчасть системы, которая непосредственно исполняет запросы и хранит данные возникли проблемы: control plane должен разделять часть логики с data plane, но из-за разных языков нельзя было создать общую библиотеку, и версии кода на Kotlin и Rust иногда слегка различались. В итоге было решено переписать control plane на Rust. Этому способствовали изменения за год: Rust 2021 edition устранил многие болевые точки, внутренняя библиотечная поддержка расширилась (в некоторых случаях, например в AWS Authentication Runtime client, Rust-реализации обгоняли Java-аналоги), а часть интеграционных задач была перенесена в API Gateway и Lambda.
Как работает OCC и почему это важно для FK
В Aurora DSQL конфликты проверяются на этапе коммита. Отказ от пессимистичных блокировок означает, что читатели никогда не блокируют писателей, а писатели — читателей, поэтому нет проблемы «медленного держателя блокировки»: разработчик, ушедший на обед с открытой транзакцией, не может обрушить базу.
Компромисс — рост числа абортов. Из-за длительного интервала read-modify-commit транзакция может столкнуться с write-write конфликтами на горячих ключах, и под высокой конкуренцией транзакции будут абортироваться, тогда как традиционная база могла бы поставить их в очередь. Если два региона пишут в один ключ, OCCоптимистичный контроль конкурентности: конфликты выявляются не блокировкой заранее, а проверкой в момент коммита абортирует одну из транзакций, причём конфликт обнаруживается только на коммите — вы платите задержку WAN до того, как узнаете о необходимости повтора.
Клиент при конфликте получает не ожидание в очереди, а аборт транзакции, после которого нужно повторить попытку. Поскольку конфликт обнаруживается лишь на коммите, клиент сначала платит штраф за сетевую задержку WAN, и только потом узнаёт, что транзакцию надо повторить.
Механика транзакции
Запросы исполняются query processors в Firecracker MicroVMs без локального состояния. Чтения выполняются без координации: QP назначает локальную метку времени, а хранилище само обрабатывает остальное, поэтому чтение требует нулевой координации с лидером. Записи используют OCC вместе с MVCC при Snapshot Isolationрежим изоляции, при котором транзакция видит согласованный снимок данных на момент своего начала, а координация происходит один раз при коммите и стоит примерно 1 RTT:
Whether you write one row or a hundred, coordination only happens once at commit time, costing just 1 RTT (or slightly more for a multi-shard commit).Для транзакций, затрагивающих несколько Adjudicators, применяется «linearized 2PC»: Adjudicators голосуют, но только лидер пишет финальный коммит в свой единственный Journal.
Как проверяются внешние ключи
Aurora DSQL обеспечивает ссылочную целостность через проверку снимка во время транзакций и обнаружение конфликтов в момент коммита. Проверка FK-связей идёт против согласованного снимка транзакции без блокировки таблиц, что позволяет параллельные операции, а на коммите используются неявные проверки KEY SHAREрежим блокировки строки в PostgreSQL, который не мешает другим транзакциям читать и обновлять те же строки, но сигнализирует о возможном конфликте при изменении ключа для выявления конфликтующих изменений. Транзакции, которые нарушили бы ограничение, отклоняются с ошибкой сериализации.
Механизм опирается на Adjudicator и KEY SHARE в PostgreSQL, чтобы обнаруживать изменения соответствующих строк в момент коммита, не блокируя параллельные чтения. Заявленные свойства: отсутствие блокировок, отсутствие изменения масштабирования для чтений без FK, читатели FK никогда не заставляют других читателей абортироваться, и хорошее масштабирование FK для хорошо распределённых нагрузок.
Поскольку конфликты приводят к сбоям транзакций, а не к ожиданию, приложения должны реализовывать логику повторных попыток. AWS также отмечает, что все DML-операции над ссылающимися или ссылочными таблицами вызывают дополнительные чтения для поддержания ссылочной целостности, и рекомендует бенчмаркать нагрузку перед добавлением FK-ограничения.
Какие типы FK поддерживаются
Aurora DSQL поддерживает внешние ключи с действиями CASCADE, SET NULL и другими referential actions, позволяя приложениям обеспечивать ссылочную целостность прямо в базе. Целостность обеспечивается через snapshot verification во время транзакций и conflict detection в момент commit. Проверки выполняются без блокировки таблиц, с использованием implicit KEY SHARE checks при commit, а нарушающие ограничение транзакции отклоняются с serialization error.
Дополнительные распределённые механизмы требуются в виде Adjudicator и механизма KEY SHARE из PostgreSQL. Все DML-операции над связанными таблицами вызывают дополнительные чтения для поддержания referential integrity.
Создание FK и асинхронные индексы
Aurora DSQL поддерживает нативные ограничения внешних ключей, и схема TPC-C включает все стандартные связи. Отдельно указано, что Aurora DSQL поддерживает только асинхронное создание индексов, и репозиторий учитывает это требование, чтобы избежать блокирующих операций.
Что происходит при нарушении FK
При нарушении ограничения транзакция отклоняется с ошибкой сериализации, а не блокируется и не откладывает валидацию. Приложениям следует реализовать логику повторных попыток, поскольку параллельные конфликты приводят к сбоям транзакций, а не к ожиданию. Для обнаружения изменений в соответствующих строках в момент коммита без блокировки параллельных чтений используются Adjudicator и механизм KEY SHARE из PostgreSQL.
Пограничные случаи
При сетевом разрыве между регионами проверка внешних ключей опирается на тот же механизм, что и в обычном режиме: ссылочная целостность подтверждается по согласованному снимку транзакции, а конфликты выявляются в момент коммита. Транзакция, которая нарушила бы ограничение, отклоняется с ошибкой сериализации — она не блокируется и её валидация не откладывается.
Весь коммит транзакции записывается в один журнал независимо от числа изменяемых строк, что обеспечивает атомарность и долговечность ACID. Чтобы получить последнее значение строки, нужно проверить все журналы, так как изменение могло прийти из любого из них. Crossbar предоставляет storage API подписки на ключи в заданном диапазоне и маршрутизирует обновления подписанным узлам, а каждый журнал упорядочен по времени транзакции, и Crossbar следует за каждым журналом, чтобы создать общий порядок.
Что видит клиент
При конфликте транзакций клиент получает не ожидание в очереди, а прерывание транзакции, после которого нужно повторить попытку. Поскольку конфликт обнаруживается только в момент коммита, клиент сначала платит штраф за сетевую задержку WAN и лишь затем узнаёт о необходимости повтора. При сильной конкуренции транзакции прерываются, тогда как традиционная база могла бы поставить их в очередь. Чтобы снизить частоту таких прерываний, клиентам следует использовать FOR UPDATE и проектировать схемы так, чтобы write-write конфликты возникали для нарушений бизнес-логики.
Multi-Region кластеры дают два региональных эндпоинта, по одному в каждом регионе пары, оба представляют одну логическую базу и поддерживают одновременные чтение и запись с сильной согласованностью, а третий регион служит только журнальным свидетелемузлом, который хранит журнал транзакций для подтверждения коммитов, но не обслуживает клиентские подключения.
Компромиссы выбора OCC
DSQL применяет OCC вместе с MVCC при Snapshot Isolation, чтобы читатели не конфликтовали с писателями: поскольку читатели смотрят на снимок прошлого, read-write конфликты невозможны. Это устраняет проблему «медленного держателя блокировки»: читатели не блокируют писателей и наоборот.
Компромисс — высокая вероятность write-write конфликтов на горячих ключах, что ограничивает число последовательных операций над одной строкой. При записи в один ключ из двух регионов OCC абортит одну из транзакций, и конфликт обнаруживается только на коммите, что означает оплату задержки WAN до повтора. В разделе Lessons Learned упоминается трение в отношении Foreign Key Constraints и последовательностей с высокой локальностью. AWS отмечает, что все DML-операции над ссылающимися или ссылочными таблицами несут дополнительные чтения для поддержания ссылочной целостности, и рекомендует бенчмаркать нагрузку перед добавлением FK.
Почему расширения написаны на Rust
Расширения DSQL решили писать на Rust, потому что каждая новая строка C-кода была шансом добавить ошибку безопасности памяти, такую как use-after-free или переполнение буфера. Команда осознала это на код-ревью, найдя несколько проблем безопасности памяти в «казалось бы, простой» реализации структуры данных, и поняла, что на Rust можно было бы просто взять проверенную безопасную реализацию с Crates.io.
Исследование команды Android подтвердило их мышление: подавляющее большинство новых багов приходит из нового кода, поэтому, выбрав безопасный по памяти язык, можно предотвратить такие баги. Хотя Rust-код тесно взаимодействует с API Postgres, команда смогла создать абстракции, которые принудительно обеспечивают безопасные шаблоны доступа к памяти. Например, в C часто есть два поля, которые нужно безопасно использовать вместе, вроде char* и поля len, и приходится полагаться на соглашения или комментарии; в Rust это оборачивается в единый тип String, который инкапсулирует безопасность. Своими Rust-абстракциями команда смогла закодировать эти правила в систему типов, делая невозможным нарушение инвариантов.
Как сравнивали и что получилось
Стенд и методология
Для максимальной производительности нагрузку распределяют по нескольким EC2-инстансам в разных зонах доступности: каждый инстанс-загрузчик и исполнитель запускается в отдельной зоне доступности. Регион задаётся параметром --region, который обязателен для подключения к Aurora DSQL, но сам регион не назван.
Замер воспроизводится в три фазы.
Фаза 1 (schema/item init) выполняется один раз: создаёт таблицы и индексы и загружает общую таблицу item на 100000 позиций, но не грузит данные складов. Используются --skipMainDataLoad true, --create true, --load true, --execute false.
Фаза 2 (distributed warehouse loading) запускает несколько параллельных инстансов, каждый из которых грузит свой поднабор складов через --startWarehouseIndex и --endWarehouseIndex. Например, loader 1 берёт склады 1,4,7,...,199 (67 складов), с --loaderThreads 70, --skipItemLoad true, --create false, --load true, --execute false, --clear false.
Фаза 3 (distributed benchmark execution) запускает несколько инстансов-исполнителей, каждый на своём поднаборе складов, с теми же --startWarehouseIndex, --endWarehouseIndex, --region, а также --execute true, --create false, --load false, --clear false.
Параметры нагрузки: общее число складов в базе должно быть одинаковым на всех инстансах; распределение между инстансами задаётся шагом между складами (например, шаг 3 для трёхчастного разделения); число одновременных терминалов на исполнителе должно соответствовать числу складов, обрабатываемых этим инстансом. Пример распределения — 200 складов на 3 инстанса в разных зонах доступности.
Агрегация результатов
Каждый executor-инстанс генерирует собственный файл результатов с метриками производительности для назначенного ему подмножества складов. Файлы сохраняются в текущем каталоге с метками времени в формате results_<timestamp>.csv или results_<timestamp>.json, и каждый инстанс создаёт независимые файлы.
Для оценки общей производительности Aurora DSQL нужно объединить результаты со всех executor-инстансов. Ручная агрегация: собрать все файлы результатов, сложить значения TPS со всех инстансов для получения общей пропускной способности кластера, вычислить взвешенное по объёмам транзакций среднее значение задержки и сложить количество успешных и неуспешных транзакций по всем инстансам.
Пример агрегации: для трёх инстансов с результатами 1 200 TPS / 67 складов, 1 180 TPS / 67 складов и 1 150 TPS / 66 складов итоговая производительность кластера составляет 3 530 TPS на 200 складах.
Оговорки
Распределённый подход даёт более реалистичные результаты за счёт распределения нагрузки по нескольким EC2-инстансам в разных зонах доступности.
Что из этого следует на практике
- FK теперь есть, но ценой дополнительных чтений. Все DML-операции над ссылающимися или ссылочными таблицами вызывают дополнительные чтения для поддержания ссылочной целостности, поэтому перед добавлением FK-ограничения нагрузку нужно измерить.
- Конфликты не ждут, а абортят. При нарушении ограничения или конкурентном изменении транзакция отклоняется с ошибкой сериализации, а не ставится в очередь. Приложения обязаны реализовывать логику повторных попыток.
- Цена конфликта в multi-Region — задержка WAN. Если два региона пишут в один ключ, OCC абортирует одну из транзакций, и конфликт обнаруживается только на коммите: задержку WAN вы платите до того, как узнаете о необходимости повтора.
- Горячие ключи ограничивают число последовательных операций над строкой. Длительный интервал read-modify-commit делает write-write конфликты на горячих ключах вероятными.
- Проверка FK не блокирует таблицы. Она идёт против согласованного снимка транзакции, а на коммите используются неявные проверки KEY SHARE. Читатели FK не заставляют других читателей абортироваться.
- Схему стоит проектировать под конфликты. При необходимости клиентам следует использовать FOR UPDATE и проектировать схемы так, чтобы write-write конфликты возникали для нарушений бизнес-логики.
Где смотреть в коде
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: Parameters (Phases 1 & 2))
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: Multi-Instance Distributed Benchmarking across AZs
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: Instance Results
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: Parameters (Phase 3)
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: Aggregation Method
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: 1: Schema and Item Initialization
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: **Foreign Key Constraint Compatibility**
- amazon-contributing/aurora-dsql-benchbase-benchmarking/README.md: 2: Distributed Warehouse Loading
Источники
- github.com/…/README.md
- www.infoq.com/news/2026/09/aurora-dsql-foreign-keys/
- www.allthingsdistributed.com/…/just-make-it-scale-an-aurora-dsql-story.html
- muratbuffalo.blogspot.com/…/aurora-dsql-scalable-multi-region-oltp.html
- arxiv.org/…/2607.13276
- aws.amazon.com/blogs/aws/amazon-aurora-dsql-is-now-generally-available/
- docs.aws.amazon.com/…/what-is-aurora-dsql.html