Контракт вместо настроек: чего я жду от OLTP-БД в 2026
После первой статьи в комментариях несколько раз прозвучало примерно одно и то же:
"Всё правильно, но это же про любую зрелую СУБД — что с этим делать?"
Я думал над этим вопросом несколько недель. И в итоге решил не искать ответ в виде
"возьмите правильный инструмент X" — а попробовать честно сформулировать:
какими свойствами OLTP-БД должна обладать сама по себе, независимо от того,
насколько хорош ваш оператор, консультант или runbook.
Здесь две части:
Что я называю "контрактом" БД — и почему это важнее списка фич.
Сценарии из реальной эксплуатации — и какие механизмы их предотвращают.
А в конце — самое интересное, к чему это все меня привело...
Что такое "контракт" — и почему это не маркетинг
Попробую объяснить не через определение, а через ощущение.
Когда вы покупаете автомобиль, вы не читаете инструкцию к тормозам каждое утро.
Вы просто знаете: нажал педаль — машина тормозит. Это контракт. Он не зависит от того,
правильно ли вы настроили тормозную жидкость этим утром или не забыли включить
"режим торможения" в меню.
С базами данных часто иначе. Многие критичные свойства — предсказуемая латентность,
изоляция данных, аудит действий, поведение под нагрузкой — работают, только если:
правильно настроено,
правильно задеплоено,
и команда не забыла включить нужную опцию.
Это не контракт. Это договорённость, которая легко рассыпается при смене команды,
росте нагрузки или очередном обновлении.
Контракт БД — это проверяемые гарантии, которые либо выполняются всегда,
либо система явно сообщает об отказе. Без тихих исключений, без
"ну мы же предупреждали в документации".
Чтобы контракт был настоящим, а не декларативным, он должен быть:
формализован — можно проверить программно, не только "на глаз";
наблюдаем — есть метрики/события на каждое нарушение;
fail-closed по умолчанию — если условие не выполнено, система отказывает предсказуемо;
одинаково понятен разработчику, DBA и SRE — не только автору конфига.
10 свойств, с которых должен начинаться продакшен
Это не "идеальный мир будущего". Это минимум, ниже которого начинаются
повторяющиеся инциденты и "магия настройки". Я разбил их на четыре группы —
так проще думать, какие из них у вас уже есть, а какие держатся на runbook'ах.
Безопасность: нельзя "забыть включить"
1. Нет контекста — нет запроса. Если для выполнения DML/DDL нужен security context
(например, tenant_id), а он не задан — запрос просто не исполняется.
Это звучит жёстко, но альтернатива хуже: "невинный" admin-скрипт без контекста
превращается в источник утечки между клиентами. Да, это поднимает планку
для ad-hoc запросов и миграций — но это сознательный компромисс.
2. Аудит нельзя выключить незаметно. Аудит — не флаг, который включают по настроению.
Это часть контракта исполнения: попытка обойти или отключить фиксируется и алертит.
Без этого после инцидента вы гарантированно услышите "мы не можем восстановить картину".
Нужна политика ретенции и фильтрации, иначе аудит превратится в гору логов —
но это проблема операционная, а не архитектурная.
Предсказуемость под нагрузкой: "не должно быть сюрпризов"
3. Фоновые процессы не едят latency-бюджет бесконтрольно. Каждый, кто эксплуатировал
PostgreSQL на большом объёме, знает ощущение: "вакуум опять всё сожрал".
Maintenance (cleanup, compaction, freeze-подобные задачи) должен быть ограничен
по влиянию на интерактивный трафик. Фоновая уборка может быть медленнее — зато p99
не "срывается" в самый неподходящий момент.
4. Память предсказуема по классам нагрузки. 50 параллельных сортировок не должны
молча удвоить RSS и убить соседей. Тяжёлые запросы могут получить throttle
или явный отказ — но это лучше, чем OOM посреди пика.
5. Наплыв соединений — политика, а не "как получится". Queue, reject, backpressure —
что угодно, но явно описанное. Не "иногда повезло подключиться, иногда нет".
Часть клиентов получит предсказуемый отказ вместо зависания на 30 секунд — и это нормально.
6. Длинные транзакции и горячие конфликты под контролем ядра. Лимиты на длительность
и размер транзакции по классам workload. Раннее обнаружение lock-конфликтов.
Контролируемый abort с диагностикой, а не "всё зависло и непонятно почему".
Некоторые "длинные бизнес-операции" придётся разбивать — но это рефакторинг,
а не катастрофа.
7. Деградация планов — не сюрприз. Каждый DBA хотя бы раз видел:
"вчера быстро, сегодня медленно — ничего не менялось". На деле менялась статистика
или параметры, и план тихо "уехал". Должны быть управляемые рамки:
что может поменяться автоматически, а что — только по явному действию.
Часть агрессивных оптимизаций может быть отключена ради стабильности — и это ок.
Multi-tenant: "чужой шум — не моя проблема"
8. Изоляция арендаторов — примитив ядра. Если один "шумный" арендатор
может просадить latency у всех остальных, у вас нет SLA — у вас есть надежда.
Изоляция ресурсов (CPU/memory/IO/locks) должна быть встроена,
а не достраиваться оркестратором. Лимиты жёстче, квоты нужно явно выдавать —
но зато "noisy neighbor" перестаёт быть системным риском.
9. Режимы деградации описаны заранее. Что происходит на пике?
Что отбрасывается первым? Что приоритизируется? Если у системы нет ответа —
каждый постмортем будет начинаться с нуля. Бизнес должен принять,
что некоторые операции в критичной ситуации будут отвергнуты — но лучше заранее
договориться "что жертвуем", чем каждый раз "гадать, что упало".
Наблюдаемость: "мы думали, что включено"
10. Каждая гарантия наблюдаема. Это замыкающее свойство. Если у гарантий 1–9
нет метрик/событий, которые можно проверить до инцидента, — они существуют только
на бумаге. "Мы думали, что включено" — это не баг эксплуатации,
это архитектурный долг. Да, телеметрия стоит ресурсов и требует дисциплины
по SLO/алертам — но без неё вы летите вслепую.
7 историй о том, как p99 "срывается"
Теперь — практика. Ниже не абстрактные "failure modes", а узнаваемые ситуации,
которые повторяются в разных стеках. Если вы узнали хотя бы две — значит,
список гарантий выше не теоретический.
"Транзакция, которая не отпускает"
Пятница, вечер. Мониторинг показывает: p99 растёт волнами, таблицы медленно
раздуваются, фоновая уборка отстаёт. Никто ничего не деплоил. Через час
находите: забытая транзакция в idle-in-transaction, открыта 4 часа назад из
скрипта миграции, который давно закончился. Она держит snapshot, а значит —
ни vacuum, ни cleanup не могут убрать старые версии.
Что должно быть в ядре: жёсткие лимиты на длительность транзакции по классам
workload. Если транзакция приближается к лимиту — контролируемый abort
с понятной диагностикой. Остальной трафик продолжает работать.
"Релиз, который всё сломал (но CPU в норме)"
Понедельник после релиза. Часть API уходит в таймауты. Первое, что смотрят — CPU.
Нормально. Второе — диск. Нормально. Третье — "а что поменялось?"
Оказывается, новый код добавил UPDATE по горячей таблице в другом порядке,
и зона конфликта блокировок расширилась. Очередь ожиданий растёт,
а прямого "bottleneck" нет — есть только цепочка ожиданий.
Что должно быть в ядре: lock budget per query/txn. Раннее обнаружение
конфликтных графов. Политика: "если конфликт затянулся — abort наименее
приоритетной операции, критичный путь остаётся живым."
"Вакуум (или что-то похожее) опять всё съел"
Каждый день в 14:00 p99 на 5 минут уходит в небо. Потом "само" возвращается.
DBA говорит: "это вакуум". Ops говорит: "надо просто пережить".
Бизнес говорит: "а почему мы это обнаруживаем каждый раз случайно?"
Что должно быть в ядре: scheduler для фоновых задач, привязанный
к SLO интерактивного трафика. Если онлайн-нагрузка растёт — фоновая работа
автоматически дросселируется. Даже если это удлиняет обслуживание — предсказуемость
важнее скорости уборки.
"Все подключились одновременно"
Деплой микросервисов. 200 подов стартуют, каждый открывает по 10 коннектов.
2000 соединений за 30 секунд. БД не успевает — начинается каскад таймаутов.
Приложение ретраит — становится хуже. Через 5 минут всё лежит, хотя сама БД
"технически жива".
Что должно быть в ядре: admission control на входе. Очередь с TTL.
Явная политика: "новые низкоприоритетные подключения — reject,
существующие критичные потоки не деградируют".
"Один клиент положил всех"
Multi-tenant SaaS. У "тихих" арендаторов растёт latency.
Ничего не менялось — кроме того, что один крупный клиент запустил
массовый импорт. Его нагрузка забрала IO-бюджет, и "соседи" просели.
Что должно быть в ядре: tenant-aware планировщик. Лимиты на CPU/memory/IO
по арендаторам. Наблюдаемость: "кто у кого отнял бюджет".
"Шумный" арендатор получает throttle — а не деградацию всей популяции.
"Временное исключение стало постоянным"
После инцидента безопасности: "кто это сделал?" — "не можем установить,
аудит был отключён для этой схемы... временно... три месяца назад."
Что должно быть в ядре: security context и аудит — неотключаемая часть
исполнения. Операция без требуемого контекста не выполняется. Точка.
Не "можно обойти, если знаешь как".
"Вчера быстро, сегодня медленно — ничего не менялось"
Тот же endpoint, похожие данные, но время ответа выросло в 5 раз.
"А что изменилось?" — "Ничего." На самом деле поменялась статистика
(или параметры), и оптимизатор выбрал другой план. Воспроизвести сложно,
откатить — непонятно куда.
Что должно быть в ядре: контролируемые режимы стабильности планов.
Обнаружение опасных регрессий до массового эффекта. Откат к стабильной стратегии,
даже если она не самая быстрая "в среднем" — потому что "в среднем" никого
не волнует, когда горит p99.
Граница между БД и платформой
Kubernetes-операторы, service mesh, внешние политики — всё это важно.
Но есть вещи, которые опасно оставлять снаружи:
контроль security context на уровне запроса;
инварианты аудита;
защита latency-бюджета от внутренних фоновых процессов;
изоляция арендаторов в shared deployment.
Если эти свойства не являются контрактом БД, платформа в лучшем случае
маскирует проблему, а не решает её.
Простой способ проверить себя: пройдитесь по списку из 10 свойств выше
и честно ответьте на один вопрос по каждому пункту — это свойство ядра
или это runbook. Не нужно считать баллы. Обычно достаточно двух-трёх ответов
"это runbook", чтобы понять, где именно у вас хрупкость.
Как я пришёл к мысли о проектировании БД с нуля
Оговорюсь: эти мысли я формулировал не параллельно с написанием статьи.
Всё описанное ниже — это ретроспектива примерно полугодового пути,
который привёл к конкретному решению. Статья — просто попытка выстроить
это в логику, понятную без контекста.
Параллельно с размышлениями о БД в свободное время от работы я несколько месяцев изучал Rust — не потому что "модно",
а потому что язык меня зацепил именно философией: он делает невозможными целые классы ошибок,
которые в других языках просто "иногда случаются".
Никаких скрытых аллокаций в неподходящий момент.
Никакого GC, который сам решает, когда ему удобно почистить память.
Явные инварианты прямо в системе типов, гарантии которые дает язык.
Отправной точкой для погружения в Rust стала статья https://habr.com/ru/companies/bitrix/articles/878912/.
И в какой-то момент два потока мыслей про проблемы СУБД и Rust сошлись: Rust на этапе компиляции запрещает написать код, который может упасть в рантайме из-за памяти. Он навязывает контракт разработчику. Я понял, что хочу того же от базы данных: чтобы она навязывала контракт приложению, запрещая запросы, которые могут положить БД.
Я описывал требования к "правильной БД" — предсказуемое поведение под нагрузкой,
контракты вместо настроек, никаких фоновых сюрпризов — и это звучало
как описание того, для чего Rust и создавался.
Язык, который не позволяет "случайно забыть" важное.
Именно это я хотел от базы данных.
Я стал смотреть: а есть ли что-то живое на Rust в мире OLTP?
Нашёл несколько экспериментов, пару заброшенных репозиториев,
один академический проект. Ничего, что выглядело бы как
"это решает проблему, которую я описал выше".
Следующий очевидный вопрос: "А почему не CockroachDB или YugabyteDB?"
Потому что они решают другую задачу — распределённость.
Мне нужна предсказуемая одноузловая OLTP с правильными примитивами в ядре.
"А почему не форк Postgres?" — потому что это значит бороться с
тридцатью годами обратной совместимости и C-кодовой базы.
Это не критика — это просто другой вектор.
Вот тогда и появилась мысль: а что если попробовать самому?
Не "написать базу данных" как самоцель —
а начать с честного списка архитектурных решений:
что должно быть в ядре обязательно, что сознательно остаётся за рамками v1,
и почему. К чему это все привело — расскажу уже в следующей статье.
В следующей статье я разберу именно это: как контрактные гарантии из этого списка
превращаются в конкретные архитектурные примитивы — и что это значит
на практике, когда начинаешь с чистого листа.
P.S. Под p99 я в этой статье имею в виду 99-й персентиль latency, но важна оговорка: сам по себе p99 ничего не значит без ответа на вопрос “чего именно?”. Для OLTP меня интересует не средняя температура по больнице, а хвост деградации: время запроса, commit latency, ожидание блокировок, всплески на fsync/checkpoint. Иными словами, речь о предсказуемости под нагрузкой, а не об одной красивой цифре.
