Это продолжение цикла статей про нашу СУБД на Rust. Предыдущие были про устройство ядра по подсистемам:
Мы знаем как готовить БД. Но индустрия изменилась: что бы я заложил в OLTP-БД с нуля
Как я проектирую OLTP-БД с нуля: принципы, trade-off’ы и архитектурные решения
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 ( |
аналитика по тем же данным, без второго хранилища |
production-preview |
In-memory ( |
горячие справочники и очереди; долговечность на выбор: none / logged / snapshotted |
production |
Temp ( |
сессионные промежуточные данные |
production |
Партиционирование ( |
большие таблицы, нарезанные по диапазону или списку значений |
production |
Append-only ( |
журналы аудита, событийность, тайм-серии, реестры: только INSERT, UPDATE и DELETE отклоняются; разблокирует оптимизации GC, локов и хранения |
production |
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)

vagon333
29.06.2026 07:46Движок базы данных - это фундамент любой системы.
Как Вы преодолеваете недоверие клиентов построить фундамент на песчаном грунте?Тоже с базами данных 30+ лет, и некоторые красные флаги, типа OLTP + OLAP в одной бочке, не могу принять на веру.

anishukserg Автор
29.06.2026 07:46Мы и не просим вставать на него прямо сейчас. Мы вышли в открытый доступ на этапе dev preview, чтобы можно было посмотреть и потрогать без обязательств. Предыдущие статьи про архитектуру — ровно для этого: не реклама, а устройство, которое можно разобрать и задать вопросы. Доверие к СУБД зарабатывается только годами эксплуатации — это мы понимаем очень хорошо, и именно поэтому не ждём, что кто-то примет что-либо на веру. Ни архитектурные решения, ни HTAP.
Про OLTP + OLAP — согласны для большинства того, что под этим продаётся: репликация с задержкой под другим именем. Конкретно у нас: аналитические сканы изолированы от OLTP-кэша через отдельный путь чтения, UNDO-модель даёт аналитике консистентный снапшот без давления на версионность.Пока: колоночная материализация ещё не подключена — это v0.7. Сейчас работает изоляция нагрузок и свежие данные без второго хранилища.
Ставьте, тестируйте, сообщайте, где не так.
openbpm_pm
Добрый день.
Quickstart (~ 60 seconds)
# 1. Download and unpack (Linux x86_64 / aarch64, glibc >= 2.28) # https://github.com/angarabase/angarabase/releases
... но на GitHub нет опубликованных релизов portable версии, когда ожидать?
Или подскажите пожалуйста по какой ссылке еще можно бужет скачать.
anishukserg Автор
Можно поставить из rpm репозитория инструкция прямо на angarabase.dev. На GitHub чуть позже появится.
openbpm_pm
На GitHub продублировали только rpm пакеты, а было бы удобнее, если бы там появилась обещанная portable сборка. Так что еще немного подождем.
anishukserg Автор
Добавили — portable-версия теперь на GitHub рядом с rpm. Спасибо, что написали, а то затянули бы.