Это продолжение цикла статей про нашу СУБД на Rust. Предыдущие были про устройство ядра по подсистемам:

  1. Мы знаем как готовить БД. Но индустрия изменилась: что бы я заложил в OLTP-БД с нуля

  2. Контракт вместо настроек: чего я жду от OLTP-БД

  3. Как я проектирую OLTP-БД с нуля: принципы, trade-off’ы и архитектурные решения

  4. API-контракты между слоями ядра

  5. Buffer Pool с Clock-sweep и изоляцией сканов

  6. Векторизованный исполнитель с многоверсионными индексами

  7. MVCC без VACUUM: что нам дал UNDO-лог и какую цену мы заплатили (предыдущая статья цикла)

Мы назвали наш проект AngaraBase.

Документация уже открыта на angarabase.dev; оттуда же можно поставить текущую версию и своими руками потрогать всё, о чём шёл разговор все эти месяцы.

Весь цикл мы писали, не называя проект по имени. С этой статьи все называем своими именами. Анонимность всё это время была не осторожностью, а осознанным выбором, и причин на то было несколько:

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

Вторая: сначала архитектура. Мы хотели, чтобы проект пришёл к читателю не очередным «вот ещё одна база данных», а с уже разобранными контрактами, инвариантами и компромиссами, чтобы было видно, что и зачем устроено именно так.

Третья: имя обязывает. Назвать продукт публично значит начать отвечать за обратную совместимость: за имена, форматы, поведение. Пока имени нет, руки развязаны: что видели корявым, переписывали, от легаси уходили везде, где могли. Получилось не везде (у нас, например, ещё жив синхронный I/O (про это писали в API-контракты между слоями ядра), хотя и здесь мы методично двигаемся к async), но назови мы всё это раньше, пришлось бы тащить за собой каждое промежуточное решение.

Теперь о цифрах. Полный сравнительный сьют, с воспроизводимым стендом, прозрачной методикой и опубликованными артефактами, мы готовим отдельной статьёй (она уже почти готова): мешать манифест с простынёй замеров не на пользу ни тому, ни другому, а одной цифрой такую СУБД всё равно не описать. Здесь приведём только два ориентира, которые уже не стыдно назвать. Первый: точечное чтение по тёплому кэшу на одной ноде отвечает примерно за 0.46–0.48 мс, против около 1 мс, которую мы видим на том же сценарии у PostgreSQL 18.4. Второй ниже, в разделе про HTAP. И сразу про обратную сторону, как есть: на тяжёлой полнотабличной агрегации и на транзакционном throughput под высокой конкуренцией мы пока заметно медленнее зрелых движков. Важно, что мы понимаем причину и это не баг и не потолок архитектуры: и колоночный путь, и единый снапшот заложены ровно под эти сценарии. Отстаёт пока реализация, а не замысел: материализация в колонки ещё не подключена, часть путей I/O всё ещё синхронные. Мы как раз начинаем следующий мажорный релиз, где по планам эти куски и доделываются, и именно они должны изменить картину. Так что это фронт работ с понятной причиной и целями, а не цифры, которые мы прячем за округлениями.


Статус на момент публикации

Сразу про лицензирование, чтобы не было недомолвок. Мы любим open source и многим ему обязаны, но не считаем, что для тяжёлой enterprise-СУБД чистая пермиссивная модель это устойчивый путь развития. Дело не в деньгах: всё, что есть, мы сделали на своём времени. Дело в долгой игре. Чистый пермиссивный код слишком легко взять и запустить как чужой сервис, ничего не возвращая в разработку; при этом с теми, кто готов строить и развивать продукт вместе, мы наоборот хотим работать в открытую, на оговоренных условиях. И нам важно вести AngaraBase как один целостный движок, а не повторить историю, где сильное ядро обрастает зоопарком форков и расширений разного качества, за сборку которых в надёжную систему никто не отвечает. Поэтому исходники открыты, но лицензия не на 100% пермиссивна: будет бесплатная Community-версия и коммерческая, и мы склоняемся к модели, где код со временем сам переходит под полностью открытую лицензию. Точные границы редакций и сроки пока не фиксируем, сейчас важнее довести продукт; когда модель устаканится, расскажем отдельно, без мелкого шрифта.

Что реализовано на момент публикации:

  • HTAP: row-store для OLTP и колоночное хранилище (production-preview) под одним SQL, векторизованный исполнитель (SIMD-батчи, Hash Join, агрегации) для аналитики по свежим данным.

  • Метрики и health: HTTP-эндпоинт /metrics в формате Prometheus (несколько сотен метрик), плюс liveness/readiness/startup пробы.

  • USDT-пробы по горячим путям, читаемые bpftrace/bcc/perf без рестарта.

  • Системные представления: angara_stat_activity и angara_stat_statements в духе pg_stat_*, с wait-events.

  • EXPLAIN с ANALYZE, VERBOSE, FORMAT JSON и DIAGNOSTIC.

  • Онлайн-операции: бэкап без остановки базы и ALTER TABLE через ghost-table.

  • Бэкапы FULL/DIFF/LOG с обязательным VERIFY и PITR на обычном ext4.

  • Ошибки: fail-closed контракт через явные SQLSTATE.

  • Машина времени: AS OF SNAPSHOT/TIMESTAMP для разбора инцидентов.

Что ещё не готово:

  • Колоночный слой пока в статусе production-preview, единый HTAP-роутер ещё дозревает.

  • Async I/O: полный переход не завершён, часть путей пока синхронные.

  • Единый GC-координатор (сейчас несколько независимых механизмов).

  • Структурированные спаны по всем горячим путям (OTLP опционален и по умолчанию выключен).

  • S3/MinIO как удалённый sink для бэкапов.

  • Репликация, HA, шардинг: фокус по-прежнему single-node, остальное по roadmap.


Тридцать лет полировки не догнать спринтом

За PostgreSQL, Oracle и MS SQL стоят десятилетия инженерной шлифовки: оптимизатор, который видел миллионы планов, расширения на любой случай, экосистема инструментов, тонна документации и людей, которые всё это знают. Делать вид, что мы за пару лет догоним этот объём вылизанности, было бы лукавством, прежде всего перед собой.

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

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


Направление 1: один движок для транзакций и аналитики (HTAP)

Главная причина смотреть на AngaraBase не в отдельной фиче, а в том, как устроено ядро: транзакции и аналитика живут в одном движке.

Сам термин HTAP не новый: его ввёл Gartner ещё в 2014 году для систем, которые тянут и транзакции, и аналитику без переноса данных между ними. На практике под этим почти всегда прячут репликацию из OLTP в аналитическую базу, и вот здесь наш подход расходится с привычным.

Классическая раскладка выглядит так: OLTP-база отдельно, аналитическое хранилище отдельно, между ними ETL или репликация. Платят за это дважды: сложностью контура и тем, что аналитика всегда отстаёт от реальности на интервал перекачки. «Свежий» отчёт здесь означает «вчерашний».

Мы пошли иначе. Под одним SQL и одним транзакционным снапшотом у нас сосуществуют row-store для OLTP и колоночное хранилище для аналитики (пока в статусе production-preview). Поверх работает векторизованный исполнитель: батчи по 1024 строки, std::simd, SelectionVector для фильтров без копирования; Hash Join, хэш-агрегации и GROUP BY идут по векторному конвейеру. Связку row→column держит RowToColumnBridge, не протаскивая MVCC в векторный слой. По замерам из статьи про векторизованный исполнитель, на простых полнотабличных агрегатах это даёт около ×1.2–1.4, а на GROUP BY, где батчевая обработка ключей выигрывает сильнее всего, доходит до ×2.7 относительно построчного пути.

Но ценность этого пути не в самих SIMD-батчах, а в двух следствиях для эксплуатации:

  • аналитика по свежим данным, без второго хранилища. Отчёт строится по тем же строкам, которые только что записала транзакция, без ETL-лага и без отдельной системы, которую нужно синхронизировать и обслуживать;

  • аналитика не мешает транзакциям, и наоборот. Полные сканы изолированы от горячего OLTP working set отдельным маршрутом чтения (BufferRing, об этом была статья про Buffer Pool), а из-за UNDO-модели аналитический скан читает O(живых строк) независимо от интенсивности обновлений (об этом была статья про MVCC). То есть count(*) поверх часто обновляемой таблицы не дорожает от того, что её активно пишут. И это не только теория: тот самый второй ориентир из бенчмарков, под конкурентным длинным аналитическим сканом OLTP-throughput на нашем стенде проседает меньше чем на 20%. Ровно то разделение, ради которого обычно заводят две отдельные системы.

Иными словами, не нужно выбирать между «быстрыми транзакциями» и «свежей аналитикой» и держать ради этого две системы. И да, сам выбор маршрута тоже наблюдаем: на дашборде есть панель «SQL routing decisions», по которой видно, когда запрос ушёл по векторному или колоночному пути. Как это устроено внутри, разбирала статья про векторизованный исполнитель; здесь важно, зачем он нужен.

Назовём и то, чего по этому пути пока ждать не стоит. Свежесть и изоляция аналитики у нас уже есть, а вот по сырой скорости тяжёлых полнотабличных агрегатов мы сейчас кратно отстаём от зрелых движков. Причина конкретная: вектор-исполнитель (ему была посвящена та самая статья) уже работает, но материализация данных в колоночные сегменты (тот самый HTAP-sync) ещё не подключена, поэтому большие агрегаты всё ещё идут построчным путём, и колоночное хранилище их пока не разгоняет.

Это осознанный порядок работ, а не сюрприз. Мы с самого начала вкладывались в корректность и архитектуру, а не в красивую цифру на синтетике: гнаться за throughput, пока не устаканились контракты и инварианты, значит оптимизировать то, что ещё может перемениться. И узкое место мы не угадываем, а видим: ровно та наблюдаемость, про которую вся эта статья, по фазам показывает, где уходит время. Переработка этого пути, материализация в колонки и векторизация горячих агрегатов, заложена в v0.7.

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

Тип хранения

Под какую задачу

Статус

Row-store (по умолчанию)

OLTP: точечные чтения, запись, транзакции

production

Колоночное / HTAP (USING COLUMNAR)

аналитика по тем же данным, без второго хранилища

production-preview

In-memory (WITH (storage='memory'))

горячие справочники и очереди; долговечность на выбор: none / logged / snapshotted

production

Temp (CREATE TEMP TABLE)

сессионные промежуточные данные

production

Партиционирование (PARTITION BY RANGE / LIST)

большие таблицы, нарезанные по диапазону или списку значений

production

Append-only (WITH (append_only=true))

журналы аудита, событийность, тайм-серии, реестры: только INSERT, UPDATE и DELETE отклоняются; разблокирует оптимизации GC, локов и хранения

production

No-delete (WITH (mutation_policy='no_delete'))

удаление (DELETE/TRUNCATE) запрещено, обычные правки разрешены: защита от случайного или несанкционированного DELETE

production

production: стабильная семантика; production-preview: API устоялся, нагрузочное тестирование продолжается.

Две последние строки это не отдельный движок, а политика мутаций поверх обычной таблицы: задаётся при создании или через ALTER TABLE.

Чтобы не было переобещаний: materialized views пока живут только в каталоге (исполнения ещё нет), а UNLOGGED- и foreign-таблиц нет вовсе. Это план, а не текущий набор.

Раскладку можно довести до конкретного устройства и до времени. Tablespaces: таблицу или индекс кладём в отдельный tablespace на своём пути или устройстве (CREATE TABLESPACE ... LOCATION, затем CREATE TABLE ... TABLESPACE), с опциональным шифрованием на уровне tablespace. Автопартиционирование: для секционированных по диапазону таблиц (типичный time-series) движок сам нарезает новые партиции по интервалу и сам убирает старые по политике хранения (auto_partition_interval и auto_partition_retain), так что отдельный partition manager в кроне не нужен. Сразу про рамки: авто-нарезка пока только для RANGE, а per-tablespace размер страницы это план, не текущая фича.

Чем это отличается от других HTAP, и куда мы идём

Идея «транзакции и аналитика в одном движке» не нова, так что назовём соседей по ландшафту прямо. Почти все известные HTAP-системы выбрали путь распределённого кластера: TiDB (PingCAP), OceanBase (Ant Group) и GaussDB (Huawei), а рядом распределённые SQL-движки вроде CockroachDB и YugabyteDB. HTAP там получается за счёт горизонтального масштабирования: данные размазаны по узлам, между ними консенсус, поверх обычно Kubernetes и оператор кластера. Это даёт масштаб, которого одна нода не даст, но и платить за него приходится постоянно, эксплуатацией распределённой системы, даже когда нагрузка спокойно живёт на одном сервере. С другого края стоят аналитические движки вроде ClickHouse: они очень быстры на агрегатах, но полноценных транзакций не дают, и свежие OLTP-данные попадают в них отдельной загрузкой.

Наша ставка сознательно между этими полюсами: HTAP на одной ноде. Транзакции и аналитика под одним SQL и одним снапшотом, без шардинга, консенсуса и кластерного оператора, и при этом с настоящими транзакциями, а не «почти». Тезис простой: очень большая доля реальных нагрузок целиком помещается на один современный сервер, и для них распределённый кластер не преимущество, а лишний слой, который надо обслуживать. Если данные на один узел не влезают, сегодня это не ваш выбор: репликации, HA и шардинга у нас пока нет (см. раздел «Чего ещё нет»).

Куда мы с этим идём. Ближайший шаг не «стать распределёнными», а дожать сам single-node HTAP: подключить материализацию в колоночные сегменты (тот самый HTAP-sync), чтобы тяжёлые агрегаты поехали по колоночному пути, а не построчному (это v0.7, об этом выше). Распределённый контур на горизонте есть, но мы его пока не обещаем и в заголовок не выносим: сначала доводим до зрелости то, на что поставили.


Направление 2: метрики через endpoint, а не через костыли

Начнём с метрик. У нас это HTTP-эндпоинт /metrics в текстовом формате Prometheus (version=0.0.4), который поднимается рядом с сервером и по умолчанию слушает 127.0.0.1:9898 (адрес задаётся ops.metrics_addr или переменной окружения). Никакого внешнего exporter’а ставить не надо: реестр живёт прямо в процессе на атомарных счётчиках, без отдельной зависимости от prometheus-библиотеки.

Важнее адреса другое: покрытие и то, как оно подано. Метрик уже несколько сотен, но держать их в голове оператору не нужно: поверх эндпоинта идёт готовый дашборд (Grafana, лежит прямо в поставке), и собран он не по слоям движка, а по вопросам, которые в три часа ночи задаёт дежурный.

Схематичный вид панели «At a Glance»; числа иллюстративные.

Дальше дашборд разложен на секции, и заголовок каждой сформулирован как вопрос, а не как имя подсистемы:

  • Query Performance: запросы стали медленнее?

  • Transactions: есть ли борьба за строки?

  • Locks: что именно блокирует запросы?

  • WAL & Durability: данные точно в безопасности?

  • Buffer Pool & Memory: хватает ли RAM?

  • GC & MVCC: нужна ли сборке мусора помощь?

  • Storage I/O & Index Health: диск стал узким местом?

  • Recovery & Replay: последний рестарт был чистым?

  • Plan Cache & Optimizer: кэш планов окупается?

А что стоит за конкретными цифрами (за «возрастом старейшего снапшота» или за тем самым «зоопарком из пяти механизмов GC»), мы подробно разбирали в статье про MVCC. Здесь важна не лекция, а то, что всё это уже сведено в одну панель из коробки, а не собирается руками под каждый новый инцидент.

Плюс три health-эндпоинта под Kubernetes: /health/live, /health/startup, /health/ready. Каждый отвечает JSON и недвусмысленным HTTP-кодом (503 с причиной, если сервер ещё не дочитал startup-последовательность), а не «процесс жив, значит всё хорошо».

Принцип, которого мы держимся: метрика существует до того, как понадобилась, а не дописывается после инцидента. Если внутри движка есть решение, которое может пойти не так, у него есть счётчик.


Направление 3: USDT-пробы для взгляда внутрь без остановки сервера

Метрики отвечают на «сколько» и «как часто». Они плохо отвечают на «что именно тормозило вот этот конкретный запрос прямо сейчас». Для этого у нас по горячим путям расставлены USDT-пробы (userland statically defined tracing): те самые статические точки трассировки, которые умеют читать bpftrace, bcc и perf.

Ключевых свойств два.

Во-первых, нулевая цена, когда никто не смотрит. В скомпилированном бинаре проба представляет собой NOP-инструкцию и запись в ELF-секции; пока к ней не прицепился внешний инструмент, она ничего не стоит. Пробы включены в сборку по умолчанию, так что пересобирать debug-вариант ради трассировки продакшена не нужно.

Во-вторых, подключение без рестарта. Прод тормозит прямо сейчас? Вы цепляетесь к живому процессу и снимаете данные, не роняя сессии:

# какие пробы вообще есть
bpftrace -l 'usdt:./angarabased:angarabase:*'

# гистограмма ожиданий на блокировках
bpftrace -e 'usdt:./angarabased:angarabase:lock_wait_end { @ = hist(arg1); }'

# запросы, у которых I/O дольше 1 мс
bpftrace -e 'usdt:./angarabased:angarabase:io_end /arg1 > 1000/ { @slow[arg0] = count(); }'

Пробы покрывают то, что реально хочется видеть под нагрузкой: старт/финиш запроса, фазы парсинга-планирования-исполнения, ожидания на блокировках, I/O и fsync, work группового коммита, векторные батчи и их fallback, очереди QoS-планировщика. Таксономия проб образует append-only контракт (новые значения только добавляются, старые не переезжают), и его соблюдение проверяется отдельным lint’ом в CI, чтобы ваши bpftrace-скрипты не сломались от релиза к релизу.

Отдельно подчеркну: сам движок не несёт в себе eBPF-машину. Пробы остаются статическими точками, к которым внешние ядерные инструменты цепляются по требованию. Это сознательный выбор в пользу «нулевой накладной по умолчанию», а не «ещё одна подсистема внутри СУБД».


Направление 4: спросить базу о ней самой по SQL

Не у всех под рукой bpftrace, и не всё стоит гонять через ядро. Поэтому часть наблюдаемости вынесена туда, где DBA живёт привычно: в системные представления, читаемые обычным SELECT по PostgreSQL-протоколу.

  • angara_stat_activity, аналог pg_stat_activity: живые сессии, их состояние, нормализованный отпечаток запроса и, главное, текущий wait-event, то есть на чём именно сессия стоит прямо сейчас (блокировка строки, чтение страницы, fsync, ожидание клиента). Внутри это RAII-учёт: каждая блокирующая операция входит под guard, фиксирует длительность и одновременно зажигает соответствующую USDT-пробу. То есть «что висит» видно и по SQL, и через eBPF, из одного источника.

  • angara_stat_statements, аналог pg_stat_statements, только встроенный: отдельное расширение ставить и загружать не нужно, оно есть всегда. Запросы с литералами, схлопнутыми в $N, со счётчиками вызовов и временем (total/min/max/mean); похожие запросы с разными значениями сходятся в один queryid.

Но главный рабочий инструмент DBA это EXPLAIN, и в него мы вложились заметно плотнее остального. Поддержаны ANALYZE (фактические числа, а не только оценки оптимизатора), VERBOSE, FORMAT JSON для машинной обработки и режим DIAGNOSTIC, который раскрывает, что именно и почему решил планировщик.

Своя особенность нашего EXPLAIN ANALYZE в том, что время разложено по фазам, parse / plan / execute / commit, с микросекундной точностью. Это сразу разводит три разных «медленно», которые в привычном выводе сливаются в одно: запрос тормозит на планировании, запрос тормозит на чтении с диска, или запись тормозит на коммите (flush WAL). Выглядит это так:

EXPLAIN (ANALYZE, DIAGNOSTIC)
SELECT id, tenant_id, value, status FROM wave_bench WHERE id = 42000;

VectorProject cost=0.00..10.00 rows=10 stats=live
  VectorIndexScan index_name=wave_bench_pkey index_col=id
                  key_range==42000 index_rows_fetched=<runtime>
                  scan_strategy_reason="index scan: high selectivity (0.0000)"
                  cost=0.00..10.00 rows=10 stats=live

Actual Rows: 1
Actual Time: 631 ms

--- Per-Phase Timing (RFC-2026-340) ---
Parse Time:   13 us
Plan Time:     0 us
Exec Time:     0 us
Commit Time:   0 us
Total Time:  631447 us
Overhead:    631434 us

--- Optimizer Diagnostics ---
workload_class = select
replan_reason  = none
cache_status   = hit
reason_codes   = stats_default_fallback

Реальный вывод, снятый под волновой нагрузкой (40 одновременных соединений). Parse Time + Exec Time = 13 мкс; Overhead 631 мс — время ожидания в очереди исполнителя при пике волны.

Что здесь ценно для эксплуатации, помимо фаз:

  • планировщик объясняет свой выбор, а не только показывает дерево. У узла видно, какой индекс взят (index=...) и по какому диапазону ключа, а scan_strategy_reason подсказывает, почему скан выбран именно такой (например, no_index_available, если вы ждали индекс, а его нет). Рядом флаг stats=live: он говорит, опирается оценка на собранную статистику или это дефолтная прикидка. Тут же признаём слабое место: оценка кардинальности у нас пока эвристическая (в примере планировщик ждал 120 строк, по факту вернулось 37), и stats=live стоит ровно для того, чтобы догадка не выдавалась за факт;

  • виден векторный путь. Если запрос ушёл по векторному исполнителю, в плане это прямо помечено (VectorSeqScan, VectorFilter и так далее), так что «а почему этот запрос не векторизовался» не нужно угадывать;

  • DIAGNOSTIC показывает решения оптимизатора и состояние кэша планов. cache_status (план достали из кэша или перепланировали, и почему: stats_drift, schema_changed), reason_codes (index_only_eligible, bitmap_candidate_rejected, hash_join_fits_work_mem), а под нагрузкой ещё и runtime_facts: сколько ушло в спил на диск, сколько ждали flush WAL, сколько запросов отклонено по бюджету. FORMAT JSON отдаёт всё это машинно, без вычистки регулярками.

Стоит знать и про пределы: времени по каждому отдельному оператору мы пока не даём, в EXPLAIN ANALYZE это суммарные числа по запросу плюс разбивка на четыре фазы, а не на каждый узел дерева. А EXPLAIN ANALYZE для DML выполняется как dry-run в откатываемой транзакции: дорогой UPDATE можно оценить безопасно, но эффектов реальной конкуренции на нём не увидеть.

Этого достаточно, чтобы пройти типовой путь диагностики «что сейчас медленно», не выходя из psql: посмотреть активные сессии и их wait-events, найти тяжёлый запрос в статистике, разобрать его план по фазам и только при необходимости спускаться к пробам.


Направление 5: бэкап как операция, а не файл рядом с базой

Резервное копирование мы строили как first-class подсистему ядра, а не как pg_dump в кроне или снапшот файловой системы. С точки зрения функциональности уже есть:

  • FULL / DIFF / LOG: полный (онлайн, без остановки базы, через fuzzy copy + WAL + UNDO replay), дифференциальный по changed-map и архив WAL по диапазонам LSN для низкого RPO;

  • VERIFY как обязательная операция: проверка SHA-256 каждого файла из манифеста и непрерывности LSN-цепочки; результат включает max_restorable_lsn, то есть точку, до которой восстановление гарантировано, даже если часть цепочки повреждена;

  • PITR: восстановление до конкретного LSN или метки времени, через тот же recovery-код, что работает при каждом рестарте после падения;

  • бюджет на I/O: онлайн-копирование идёт несколькими воркерами (по умолчанию 4, до 16) чанками по 4 МБ, но с потолком по throughput (≈500 МБ/с) и по IOPS (10 000), так что бэкап не отъедает диск у пользовательской нагрузки в самый неудачный момент;

  • архив: готовый backupset переносится в файловое хранилище или на NAS командами push / pull / list / prune; публикация атомарная (через *.part + rename), существующий артефакт не перезаписывается, что защищает от случайной порчи архива;

  • всё это на обычном ext4, без требования btrfs/ZFS.

Главная идея здесь та же, что и во всём остальном: «бэкап существует» означает не «файл создан», а «доказано, что из него можно восстановиться». Поэтому если end_lsn не может быть гарантирован, backupset помечается NOT_RESTORABLE сразу при создании, а не выясняется в ночь инцидента. Подробный разбор внутренней механики оставим отдельной статье; здесь важно, что это операционная функциональность, а не обещание.

Чего пока нет, скажем прямо: обязательного шифрования (сейчас опциональное) и S3/MinIO как удалённого sink. Пока только локальный путь или NAS как смонтированная директория.


Направление 6: ошибки, которым можно верить

К наблюдаемости относится и то, как база сообщает, что что-то пошло не так. Мы выбрали fail-closed: вместо тихой выдачи неправильного результата база отдаёт явный SQLSTATE, по которому можно написать алерт и runbook. Несколько кодов, которые мы сознательно выдаём:

  • 72000 (snapshot too old): запрошена версия, которую GC уже отрезал (тот же паттерн, что ORA-01555);

  • 40001 (serialization_failure): проигран оптимистичный CAS или конфликт сериализуемости, приложению остаётся откат и повтор;

  • 54023 (configuration_limit_exceeded): превышен бюджет (например, write-set транзакции), DML отклоняется, а не уводит сервер в OOM;

  • 53100 (disk_full): исчерпание места под UNDO/WAL.

За этим стоит контракт, который мы стараемся выдерживать для каждого ресурса: ресурс → метрика → SQLSTATE → runbook. То есть у ограничения есть и цифра, по которой видно приближение к границе, и явный код на момент срабатывания, и описанная реакция. Конфигурация при этом остаётся обозримой поверхностью параметров с разумными дефолтами, а неизвестные ключи не проглатываются молча (для этого есть отдельный счётчик).


Направление 7: машина времени для разбора инцидентов

Раз история строк и так живёт в UNDO ради MVCC, мы дали к ней прямой SQL-доступ: SELECT ... AS OF SNAPSHOT <n> и AS OF TIMESTAMP '<ts>'. Для эксплуатации это конкретный инструмент:

  • «что видел клиент в 14:03?»: прямой запрос к состоянию базы на момент, без audit-таблиц и триггеров;

  • ошибочный DELETE: вернуть точечно несколько строк из прошлого, не поднимая PITR всего инстанса.

Фича opt-in (retention по умолчанию выключен: это осознанный компромисс по размеру UNDO) и за пределами окна тоже fail-closed: 72000 с подсказкой, а не молчаливо неверные данные. Механику мы разбирали в статье про MVCC на UNDO-логе.


Один инстанс, много баз с изолированными доменами

В одном инстансе AngaraBase живёт много баз, и это не просто неймспейс поверх общего хранилища. У каждой базы свой WAL, свой checkpoint и свой crash/recovery-домен. Практическое следствие для эксплуатации: операции и сбои изолированы по базе, а не размазаны по всему инстансу. Тяжёлый checkpoint или долгая транзакция в одной базе не блокируют остальные (per-DB checkpoint идёт с таймаут-изоляцией), а восстановление после сбоя поднимает базы независимо. И положить базу можно на свой путь или устройство (CREATE DATABASE ... LOCATION).

Это отличается от привычной модели, где WAL и контрольные точки общие на весь кластер и активность одной базы видна всем по журналу и по I/O. У нас единица изоляции и обслуживания это база, а не инстанс. Но есть и границы у этого: согласованный снапшот сразу по нескольким базам (cross-DB) пока не поддерживается, это в планах.


Какой SQL работает, а что вернёт ошибку

AngaraBase совместима с PostgreSQL по протоколу (pgwire), но это не значит «весь Postgres». У нас явно зафиксированный поддерживаемый subset, и работающего в нём уже достаточно для реальных приложений: DML с INSERT ... ON CONFLICT (upsert) и RETURNING, JOIN (INNER/LEFT, цепочки), GROUP BY с агрегатами, оконные функции (например ROW_NUMBER), нерекурсивные CTE, CASE/COALESCE/LIKE, подзапросы в WHERE, секционирование, основные типы (int/text/bool/float/NUMERIC/timestamp/UUID), расширенный pgwire-протокол (Parse/Bind/Execute, text и binary).

Но важнее не длина списка, а контракт на его краях. Поддержанное мы держим с корректной семантикой и стабильными кодами ошибок. А всё, что вне subset, обязано вернуть явный 0A000 (feature_not_supported) с предсказуемым сообщением. Это тот же fail-closed, что и в направлении про ошибки выше: отказ, на который можно написать обработку, лучше молчаливого сюрприза. Совместимость мы меряем не процентом, а прогоном на реальных формах запросов от psql, DBeaver и Django.

Про границы: серверную логику в базу не тащим, поэтому хранимых процедур, pl/pgSQL и триггеров нет (и в планы пока не входит); внешние расширения (PostGIS, pg_partman, TimescaleDB и прочие) не реализуем; внешние ключи пока metadata-only (NOT ENFORCED); часть продвинутого SQL (DISTINCT ON, NULLS FIRST/LAST) сознательно отдаёт 0A000. Полная матрица «что Supported, что Stubbed, что Not supported» лежит в документации на angarabase.dev.


Сквозная цель: обслуживание без даунтайма

Наблюдаемость отвечает на вопрос «что происходит». Второй вопрос оператора звучит так: «можно ли это починить, не останавливая базу». И здесь у нас есть и сделанное, и ясное направление движения.

Уже работает онлайн: FULL-бэкап снимается без остановки; ALTER TABLE ADD/DROP COLUMN на больших таблицах идёт через ghost-table: фоновая копия с атомарным cutover’ом, где эксклюзивный лок держится миллисекунды, а не всю операцию; точечное восстановление нескольких строк через AS OF не требует поднимать PITR всего инстанса. Сборку мусора мы сознательно держим фоновой и порционной, без всяких аналогов VACUUM FULL, которые берут эксклюзивный лок и переписывают таблицу, пока приложение ждёт.

Цель, к которой мы идём, простая: инстанс должен жить месяцами без maintenance window и без ручной «гигиены» от DBA. Если для здоровья хранилища нужен рестарт или окно простоя, мы считаем это багом дизайна, а не нормой эксплуатации. Дойти до конца этого пути ещё предстоит, но каждый кирпич (онлайн-бэкап, онлайн-DDL, фоновый GC) кладётся именно в эту стену.


Чего ещё нет, без прикрас

  • Единый GC-координатор. Сейчас очистку выполняют несколько независимых механизмов со своими watermark’ами; оператору приходится смотреть на пачку метрик вместо одного индикатора здоровья. Сводим к координатору поэтапно.

  • Статистика и CBO. Оценка кардинальности пока эвристическая, не на собранной статистике таблиц. stats=live в EXPLAIN явно сигналит, когда оценка дефолтная (подробнее — в разделе про EXPLAIN). Сбор статистики и полноценный CBO — следующий крупный шаг планировщика.

  • Обязательные структурированные спаны по всем горячим путям. tracing и OTLP-экспорт есть, но опциональны и по умолчанию выключены; пробы и метрики покрывают больше, чем спаны.

  • Удалённый sink бэкапов (S3/MinIO) и обязательное шифрование: в плане ближайших версий.

  • Репликация, HA и шардинг. Фокус сегодня single-node. Распределённый кластер в планах есть, но не сразу.


Вместо заключения

Эта статья не про новую фичу, а про осознанную ставку. Точнее, про три. Мы не делаем вид, что догнали по зрелости enterprise-системы, но с самого начала вкладываемся в то, что считаем важнее ещё одного процента на синтетике. В архитектуру, где транзакции и аналитика живут в одном движке (HTAP), и аналитике не нужно второе хранилище. В наблюдаемость, чтобы база была видна насквозь: метрики до инцидента, пробы без рестарта, представления по SQL, бэкап с доказательством восстановимости, ошибки, которым можно верить. И в эксплуатацию без остановки: онлайн-бэкап, онлайн-DDL, фоновый GC без stop-the-world. Когда что-то пойдёт не так (а оно пойдёт), вы должны и понять причину сами, и починить, не останавливая продакшен.

Пишите в комментариях, в том числе если считаете, что мы расставили приоритеты не туда.

Раньше этот раздел заканчивался приглашением в закрытую бету. Теперь документация открыта, текущую версию можно поставить с angarabase.dev и составить собственное мнение, не спрашивая разрешения. Если попробуете, расскажите, что увидели: где удобно, где жмёт, чего не хватило именно оператору. Мы и строим эту базу в расчёте на то, что её будут читать насквозь, и взгляд тех, кто смотрит на неё глазами эксплуатации, для нас сейчас ценнее всего.

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

Куда дальше:

  • Документация и установка текущей версии: angarabase.dev

  • Обратная связь и баги: GitHub Issues и Telegram @angarabase

  • Вопросы по теме статьи: в комментарии ниже

  • Технологическое партнёрство и кейсы для ядра: в личку, [@angarabase_bot]

Комментарии (6)


  1. openbpm_pm
    29.06.2026 07:46

    Добрый день.

    Quickstart (~ 60 seconds)

    # 1. Download and unpack (Linux x86_64 / aarch64, glibc >= 2.28) # https://github.com/angarabase/angarabase/releases

    ... но на GitHub нет опубликованных релизов portable версии, когда ожидать?

    Или подскажите пожалуйста по какой ссылке еще можно бужет скачать.


    1. anishukserg Автор
      29.06.2026 07:46

      Можно поставить из rpm репозитория инструкция прямо на angarabase.dev. На GitHub чуть позже появится.


      1. openbpm_pm
        29.06.2026 07:46

        На GitHub продублировали только rpm пакеты, а было бы удобнее, если бы там появилась обещанная portable сборка. Так что еще немного подождем.


        1. anishukserg Автор
          29.06.2026 07:46

          Добавили — portable-версия теперь на GitHub рядом с rpm. Спасибо, что написали, а то затянули бы.


  1. vagon333
    29.06.2026 07:46

    Движок базы данных - это фундамент любой системы.
    Как Вы преодолеваете недоверие клиентов построить фундамент на песчаном грунте?

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


    1. anishukserg Автор
      29.06.2026 07:46

      Мы и не просим вставать на него прямо сейчас. Мы вышли в открытый доступ на этапе dev preview, чтобы можно было посмотреть и потрогать без обязательств. Предыдущие статьи про архитектуру — ровно для этого: не реклама, а устройство, которое можно разобрать и задать вопросы. Доверие к СУБД зарабатывается только годами эксплуатации — это мы понимаем очень хорошо, и именно поэтому не ждём, что кто-то примет что-либо на веру. Ни архитектурные решения, ни HTAP.

      Про OLTP + OLAP — согласны для большинства того, что под этим продаётся: репликация с задержкой под другим именем. Конкретно у нас: аналитические сканы изолированы от OLTP-кэша через отдельный путь чтения, UNDO-модель даёт аналитике консистентный снапшот без давления на версионность.Пока: колоночная материализация ещё не подключена — это v0.7. Сейчас работает изоляция нагрузок и свежие данные без второго хранилища.

      Ставьте, тестируйте, сообщайте, где не так.