Это продолжение цикла статей про нашу СУБД на 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]
