КДПВ
Картинка для привлечения внимания

Это история про архитектуру одного сервиса. Рассказанная до конца, а не только до того места, где обычно останавливаются статьи про архитектуру. Намеренно упрощена бизнес-логика, чтобы не размывать основной смысл.

Глава 1. В начале было просто

Сервис принимал запрос, писал строку в базу, отвечал 201. Это весь код:

app.MapPost("/direct", async (Message dto, AppDbContext db, CancellationToken ct) =>
{
    Message msg = new() { Content = dto.Content, CreatedAt = DateTime.UtcNow };
    db.Messages.Add(msg);
    await db.SaveChangesAsync(ct);
    return Results.Created($"/messages/{msg.Id}", msg);
});

Никто не пишет статей про этот код, потому что в нём нечего обсуждать. Но именно об него разбиваются все последующие решения — держите его в голове, он ещё пригодится в финале.

Глава 2. «Нам нужна очередь» — и вот почему это не глупость

Рано или поздно один сервис перестаёт быть одним сервисом. Появляется второй, которому важно узнать о том же событии — обсчитать аналитику, обновить поисковый индекс, отправить письмо. Возникает соблазн просто дёрнуть его по HTTP, и это первая ошибка, которую все совершают и все же исправляют: синхронный вызов делает вас настолько же надёжным, насколько надёжен самый хрупкий из ваших соседей.

Значит нужно асинхронно, через брокер. И почти всегда этим брокером оказывается Kafka. Это не карго-культ, а рациональный выбор:

  • 80%+ компаний из Fortune 100 её используют, клиентские библиотеки есть для всех языков (kafka.apache.org/powered-by)

  • Проверена в бою: выросла из LinkedIn, где гоняли миллиарды событий в день. Netflix, Uber, Goldman Sachs — все на ней (подробнее о том, как Kafka устроена внутри)

  • Масштабируется линейно: партиции + consumer groups, добавляй брокеров без изменения кода

  • Готовая модель доставки: pull, персистентный лог, репликация, настраиваемое хранение

  • В конце концов это модно.

В коде это выглядит так:

db.Messages.Add(msg);
await db.SaveChangesAsync(ct);                    // (1) записали в базу
await producer.ProduceAsync(KafkaConsumer.Topic, new()
{
    Timestamp = new(msg.CreatedAt),
    Key = msg.Id,
    Value = msg
}, ct);                                            // (2) отправили в Kafka

Спросите себя: что случится, если процесс упадёт между строкой (1) и строкой (2)? В базе — запись есть. В Kafka — ничего. Downstream-сервис никогда не узнает, что событие произошло.

Клиент → HTTP POST → [Сервис] → INSERT → Postgres → HTTP 201
схема 1

И это не экзотика на 0.001% инцидентов. CancellationToken по закрытию соединения клиента — таймаут, закрытие или обновление страницы, кроме того случается рестарт пода, деплой, OOM-killer, да просто исключение внутри ProduceAsync — любое из этого гарантированно создаёт дыру.

Это dual write problem: два независимых ресурса обновляются не атомарно. Нельзя обернуть INSERT в Postgres и ProduceAsync в Kafka в одну транзакцию. Они просто не знают друг о друге.

Делать запись и отправку в обратном порядке - получается еще хуже.

«Просто ретраить» не помогает:

  • Сервис падает до ретрая → событие потеряно

  • Ретрай проходит, но и оригинал прошёл → дубликат

  • Ретраим и базу тоже → дубликат заказа

Глава 3. Гарантия отправки

Может, распределённая транзакция? 2PC? К сожалению нет:

  • Kafka не поддерживает XA. RabbitMQ не поддерживает. SQS не поддерживает.

  • 2PC блокирующий: координатор падает → участники висят с локами бесконечно

  • 30–40% потери пропускной способности по сравнению с локальными транзакциями

  • Требует, чтобы все участники были доступны одновременно

Вот к чему мы на самом деле пришли: если вы пишете в Kafka напрямую из бизнес-транзакции — вы уже нарушаете гарантии. Спорить с этим бессмысленно — можно только либо принять outbox, либо жить с потерянными сообщениями.

На помощь приходит паттерн Transactional Outbox.

  • Его называют «каноническим решением» для надёжной публикации событий — формулировка из разбора у Chris Richardson, microservices.io.

  • AWS Prescriptive Guidance рекомендует именно его.

  • Confluent включает outbox как обязательный шаг в собственный курс по микросервисам.

Идея простая до гениальности: записать и результат работы, и намерение отправить сообщение в одной транзакции. Обе таблицы — в одной базе. Одна ACID-транзакция. Либо обе записи закоммичены, либо обе откачены.

Вам не нужно писать диспетчер outbox и таблицу руками. Оно уже давно реализовано в сотнях библиотек. Например ZeroAlloc.Outbox. Всё, что от вас требуется — это зарегистрировать сервисы в DI и написать крошечный класс-адаптер для отправки в Kafka.

Вот как выглядит настройка в Program.cs:

// 1. Регистрируем сам Outbox из библиотеки ZeroAlloc.Outbox
builder.Services.AddOutbox(options =>
{
    options.PollingInterval = TimeSpan.FromMilliseconds(100); // Для тестов
    options.BatchSize = 50;
    options.MaxAttempts = 3;
})
.WithEfCore<AppDbContext>()
.AddMessageOutbox();

// 2. Регистрируем наш адаптер, который просто дергает Kafka
builder.Services.AddTransient<IOutboxDispatcher<Message>, OutboxDispatcher>();

И сам адаптер — 5 строк кода, библиотека сама берет на себя фоновый опрос, батчинг, ретраи и транзакционность:

public class OutboxDispatcher(IProducer<int, Message> producer) : IOutboxDispatcher<Message>
{
    public async ValueTask DispatchAsync(Message message, CancellationToken ct)
    => await producer.ProduceAsync(KafkaConsumer.Topic, new()
    {
        Timestamp = new(message.CreatedAt),
        Key = message.Id,
        Value = message
    }, ct);
}

В коде эндпоинта мы просто инжектим IOutboxWriter<Message> и пишем в той же транзакции:

app.MapPost("/outbox", async (Message dto, AppDbContext db, IOutboxWriter<Message> outbox, ...) =>
{
    var id = await db.Database.CreateExecutionStrategy().ExecuteInTransactionAsync(async (ct) =>
    {
        Message msg = new() { Content = dto.Content, CreatedAt = DateTime.UtcNow };
        db.Messages.Add(msg);
        await db.SaveChangesAsync(ct);
        
        // ← Пишем в outbox в ТОЙ ЖЕ транзакции. ZeroAlloc.Outbox сам всё сохранит
        await outbox.WriteAsync(msg, ct: ct);      
        return msg.Id;
    }, ct => Task.FromResult(false), ct);
    ...
});
Postgres (WAL) → Debezium (Kafka Connect) → Kafka topic → консьюмер(ы)
схема 2

Проблема dual write решена. Но взамен мы получили новую.

Цена атомарности

Цена

Суть

Write amplification

INSERT + INSERT + UPDATE на каждое сообщение

Vacuum

Постоянно обновляемая outbox-таблица генерирует мёртвые кортежи

Фоновый диспетчер

Ещё один процесс, конкурирующий за соединения к Postgres

Задержка

Интервал опроса. Не миллисекунды. Сотни миллисекунд.

И главное — вся эта нагрузка живёт внутри того же процесса и той же базы, что обслуживает бизнес-логику. Частый поллинг создаёт постоянный фоновый I/O даже тогда, когда сообщений нет.

Глава 4. Debezium. Выносим боль за пределы сервиса

Всю эту нагрузку не обязательно держать в том же процессе и постоянно дергать базу. Postgres и так пишет каждое изменение в WAL (Write-Ahead Log). Debezium — это Kafka Connect коннектор, который читает WAL с помощью логической репликации и публикует результат в Kafka-топик. Приложение делает только INSERT. Outbox не нужен. Всю работу по надёжной доставке берёт на себя отдельный процесс.

Postgres (WAL) → Debezium (Kafka Connect) → Kafka topic → консьюмер(ы)
схема 3

Опрос сообщества Debezium 2026 года показал, что 91.3% респондентов уже активно используют его в проде (результаты опроса), а список пользователей включает организации разного масштаба. Выглядит так, что решению можно доверять.

Конфигурация коннектора:

{
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "table.include.list": "public.messages",
    "plugin.name": "pgoutput",
    "slot.name": "debezium_slot",
    "publication.name": "debezium_publication",
    "slot.drop.on.stop": "true",
    "snapshot.mode": "no_data",
    "poll.interval.ms": "100"
  }
}

Важные настройки:

Мы решили проблему нагрузки на приложение. Мы не решили проблему количества движущихся частей: теперь у нас Postgres, Kafka и отдельная JVM для Kafka Connect.

Глава 5. Стоп. А зачем Kafka в этой схеме?

Посмотрите ещё раз на диаграмму главы 4. Debezium читает WAL, который Postgres пишет в любом случае. Единственное, что реально делает Kafka в этой конкретной схеме — это ретранслирует то, что уже лежит в WAL, ещё через один сетевой хоп.

WAL — это последовательный, append-only лог с независимыми читателями, каждый из которых хранит свою позицию. Опишите Kafka человеку, который не знает, что это Kafka, — вы только что описали WAL.

Kafka

PostgreSQL

Topic

Таблица

Partition + фильтр

PUBLICATION

Consumer + offset

Replication slot

Broker

WAL + pgoutput

Produce

INSERT

Consume

Чтение потока репликации

Если оба потребителя события — ваш код, то Kafka в этой схеме — это плата за ретрансляцию того, что уже существует. INSERT уже попал в WAL. Отдельного «отправить в очередь» просто не требуется:

 ┌────────────────────────────────────┐ │  BEGIN TRANSACTION                 │ │    INSERT INTO messages (...);     │  ← это одновременно и запись, │  COMMIT                            │     и «отправка в очередь» └────────────────────────────────────┘ │ ▼  (автоматически, через WAL) Replication slot → подписчик получает InsertMessage
схема 4

Ноль write amplification. Ноль outbox-таблиц. Нет фоновых диспетчеров. Нет отдельной JVM для Kafka Connect.

Глава 6. Читаем логическую репликацию

Настройка на стороне БД, лучше прямо в миграции:

CREATE PUBLICATION rep_pub FOR TABLE messages;
SELECT * FROM pg_create_logical_replication_slot('rep_slot', 'pgoutput');

Для чтения используем Npgsql.Replication — часть штатного драйвера. Никаких сторонних библиотек:

await using var conn = new LogicalReplicationConnection(connectionString);
await conn.Open(stoppingToken);
var slot = new PgOutputReplicationSlot("rep_slot");
await foreach (var message in conn.StartReplication(
    slot, new PgOutputReplicationOptions("rep_pub", PgOutputProtocolVersion.V4, binary: true),
    stoppingToken))
{
    if (message is InsertMessage insertMessage)
    {
        Message msg = await ReadMessageAsync(insertMessage, stoppingToken);
        completions.Complete(msg.Id, msg);
    }
    conn.SetReplicationStatus(message.WalEnd);  // обязательно!
    await conn.SendStatusUpdate(stoppingToken);
}

Пример чтения логической репликации Postgres я уже приводил в статье Ваш кэш в Redis неэффективен, что с этим делать?

Код эндпонита:

app.MapPost("/replication", async (Message dto, AppDbContext db, ...) =>
{
    Message msg = new() { Content = dto.Content, CreatedAt = DateTime.UtcNow };
    db.Messages.Add(msg);
    await db.SaveChangesAsync(ct);
    return Results.Created($"/messages/{msg.Id}", msg);
});
 ┌────────────────────────────────────┐ │  BEGIN TRANSACTION                 │ │    INSERT INTO messages (...);     │  ← это одновременно и запись, │  COMMIT                            │     и «отправка в очередь» └────────────────────────────────────┘ │ ▼  (автоматически, через WAL) Replication slot → подписчик получает InsertMessage
схема 5

Глава 7. Бенчмарк

Приложение в Aspire с эндпонитами, как в статье. Продьюсер и консьюмер в одном процессе. Консьюмер просто сигналит вызывающему потоку, что сообщение обработано. Код по ссылке https://github.com/gandjustas/habr-aspnet-kafka.

Методика: k6, 250 виртуальных пользователей, каждый запрос ждёт реального подтверждения доставки. Железо — Intel i9-9900KF, 64 ГБ RAM, вся инфраструктура в контейнерах.

Результат:

Эндпоинт

Итер/сек

avg

p95

CPU приложения на сообщение

/replication

5 625

43.9 ms

65.1 ms

0.382 ms

/direct

5 133

48.0 ms

76.9 ms

0.353 ms

/naive (Kafka)

705

350.5 ms

410.4 ms

1.274 ms

/debezium (100мс)

477

507.8 ms

908.4 ms

1.647 ms

/outbox (100мс)

56.2

4 378.4 ms

4 804.5 ms

3.250 ms

/replication обгоняет /naive в 8 раз по throughput и по задержке. Причём в CPU-времени на сообщение разрыв ещё честнее: 0.382 мс против 1.274 мс — Kafka-путь в 3.3 раза дороже по факту потраченных тактов, при этом менее надежен.

Почему Debezium и outbox гораздо медленнее

  • Debezium: poll.interval.ms Kafka Connect (по умолчанию 500 мс). Уменьшение до 100 мс: 426 → 507 итер/сек. Узкое место — таймер опроса.

  • ZeroAlloc.Outbox: Задержки дает не интервал опроса, а последовательная отправка внутри батча (BatchSize = 50). Уменьшение интервала с 1с до 100мс: 28 → 56 итер/сек (×2, а не ×10). Дальнейшее уменьшение бессмысленно — нужно делать батч на клиенте, но для этого надо писать свой Outbox.

Глава 8. «А что если нагрузка вырастет?»

Практически любой разговор о нужности Кафки сводится к этому аргументу.

Но насколько реально можно поиметь проблемы:

Наш собственный замер: до 5 600 сообщений в секунду и 1,3 МБ/с через прямую WAL-репликацию — на одном CPU, без единой оптимизации.

Подавляющее большинство систем, которые сегодня платят за Kafka, физически не приближаются к границам, за которые логическая репликация не выходит.

Когда Kafka действительно нужна

  • Много разнородных downstream-потребителей не под вашим контролем

  • Десятки тысяч сообщений/сек и растёт

  • Бизнесу нужна долгая история с произвольной перемоткой \ пререпроигрыванием истории, и вы можете это сделать за разумное время

  • Много медленных консьюмеров и их число меняется (не совместимо с предыдущим)

Когда логической репликации достаточно

  • Вы владеете и продюсером, и консьюмером

  • Нагрузка до десятков тысяч сообщений/сек

  • Хотите удешевить инфраструктуру

  • Критична консистентность и задержки

Финал: мы вернулись туда, откуда начали

Мы сделали полный круг. Прямой вызов → Kafka, чтобы разнести сервисы → outbox (через ZeroAlloc.Outbox), чтобы сообщения не терялись → Debezium, чтобы outbox не грузил приложение → прямое чтение WAL, код эндпоинта /replication выглядит как код прямого вызова.

Мораль: Используйте Postgres, пока не столкнулись с проблемами масштабирования. Когда столкнётесь — у вас будет конкретная измеренная метрика, чтобы обосновать добавление Kafka или другого компонента. А не абстрактное «так принято в микросервисах».

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


  1. mosinnik
    14.09.2026 13:23

    чтобы Kafka не теряла сообщения

    С каких пор кафка теряет сообщения? Если вы не можете настроить rf=3, isr=2, ack=all и дождаться ack от кафки, то почему это вдруг кафка теряет сообщения, а не вы криво все настроили, не разобравшись в выбранном иснтрументе?


    1. gandjustas Автор
      14.09.2026 13:23

      Такое ощущение что вы прочитали только последний абзац, а не всю статью от начала до конца.


      1. mosinnik
        14.09.2026 13:23

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


        1. gandjustas Автор
          14.09.2026 13:23

          Глава 2 об этом. Я пожалуй в тексте поправлю, чтобы не было идеи что дело в кафке.


  1. Fedyaration
    14.09.2026 13:23

    Хорошо, что в финале есть оговорки про владельца продюсера и консьюмеров: именно граница ответственности обычно и определяет, когда WAL уже не заменяет Kafka. Бенчмарк с задержками тоже наглядно показывает цену «просто добавим брокер».


  1. MonkAlex
    14.09.2026 13:23

    Какую задачу решаете?

    Если задача "оповестить заинтересованных, что случилось событие1", то я не вижу этого в решении через БД. Где событие, как версионируется контракт, как подписываться на события потребителям в других сервисах?

    Как выглядит предложенный вариант? В wal попадает что - событие специальное для оповещения или какая рядовая информация вида "создан заказ 123 с полями ..."?

    А ещё очень подозрительно звучит идея дать репликацию эээ аутбоксов между разными БД. Вместо общей универсальной шины общения между N сервисами у вас будет каждое с каждым и комбинаторный взрыв?

    ПС: я понимаю что кафка может быть перебором. Я не уверен что предложенное решение хорошее.


    1. gandjustas Автор
      14.09.2026 13:23

      Все статье описано:

      Рано или поздно один сервис перестаёт быть одним сервисом. Появляется второй, которому важно узнать о том же событии — обсчитать аналитику, обновить поисковый индекс, отправить письмо. Возникает соблазн просто дёрнуть его по HTTP, и это первая ошибка, которую все совершают и все же исправляют: синхронный вызов делает вас настолько же надёжным, насколько надёжен самый хрупкий из ваших соседей.


    1. gandjustas Автор
      14.09.2026 13:23

      В WAL попадает все что записано в базу. Оно всегда попадает. Как прочитать WAL - на ваше усмотрение. Пример кода есть.


      1. MonkAlex
        14.09.2026 13:23

        Понятно что попадает всё. Что вы предлагаете туда складывать? Отдельная табличка "событий" (как аутбокс) или потребитель сам начинает шариться в вашей БД в поисках "ага, вот заказы, а вот клиенты, а вот ещё история изменений статусов"?


        1. gandjustas Автор
          14.09.2026 13:23

          Я предлагаю читать из самих таблиц, куда сохраняются данные пока это позволяет решить задачу.


          1. MonkAlex
            14.09.2026 13:23

            Это прямо максимально грустно звучит. Та самая проблема, от которой все годами уходят - потребители залезли в вашу БД (за отчетами) и любые ваши изменения в сервисе теперь ломают работу потребителей. Потому что для сервиса БД - его внутренности и он их хочет менять в любой момент как сможет. А для потребителя это контракт, как же так, вы сломали мне работу!

            Вы пишите в статье что кафка не нужна на маленьких объемах, а подскажите, какие у вас объемы, где используется чтение WAL и как давно вы с таким подходом работаете?


            1. gandjustas Автор
              14.09.2026 13:23

              В пике видел 2000 прочитанных сообщений\сек, каждое из которых вызывало нетривиальную работу. Это был потолок для читателей, а не для WAL. WAL при этом не рос больше чем обычно.

              Естественно отправители и получатели были сделаны одной командой, поэтому никаких проблем нарушения контракта не было. В статье я указал, что при очень разнородных получателях не стоит использовать прямое чтение WAL. Лучше Debenzium + Kafka тогда.


      1. mosinnik
        14.09.2026 13:23

        кстати а что там с фильтрацией чувствительных данных, которые легко могут попасть в WAL, но которые недопустимо светить другим сервисам?


        1. gandjustas Автор
          14.09.2026 13:23

          Если вы делаете оба сервиса, то откуда такая проблема?


          1. mosinnik
            14.09.2026 13:23

            обычно другой сервис это другая зона ответственности, даже если делает та же команда. В этой другой зоне ответственности могут быть другие требования эксплуатации и тем более ИБ, зачем в нем решать проблемы работы с персухой например и влетать в немилость к безопасникам на приемках лишь из-за того, что сервис читает WAL в котором эта персуха летит рядом с тем что нам реально надо?


  1. khomenkoev
    14.09.2026 13:23

    Статья наглядно показывает, что Postgres сейчас пытаются закрыть вообще все дыры — и хранилище, и транзакции, и очереди через WAL. Но у такой универсальности есть предел. Рано или поздно Postgres придется подвинуться, уступая специализированным распределенным решениям, а останутся связки под конкретные задачи.

    Как по мне, баланс сил в будущем будет выглядеть примерно так:

    • Apache Kafka: Никуда не уйдет. Она нужна не как очередь для одного сервиса, а как сквозная шина для интеграции десятков независимых команд.

    • Redis / Valkey: Сверхбыстрый In-Memory кеш, когда дисковая БД (даже Postgres) начинает захлебываться на чтении.

    • Apache Ignite: Распределенная In-Memory БД для тяжелых вычислений «на лету» над большими данными.

    • NewSQL (CockroachDB, TiDB): Замена классическому Postgres, когда проект вырастает из одного сервера и упирается в шардирование.

    • Vector DB (Qdrant, Milvus): Специфичная ниша для ИИ и работы с эмбеддингами.

    Так что Кафка останется на своем месте (в enterprise-интеграции), а вот монополия Postgres точно пошатнется в пользу распределенной памяти (Ignite/Redis) и NewSQL.


    1. gandjustas Автор
      14.09.2026 13:23

      Пока мы видим ровно обратную картину. Pg все больше подъедает workload других систем. Добавление jsonb и json индексов почти похоронило nosql решения. Если бы апдейт в постгресе не был таким дорогим, то наверное уже совсем похоронило бы.

      timescaleDb и pgvector прекрасно работают для подавляющего большинства задач.

      Таже самая репликация и синхронизированный кэш в приложении за счет нее, о котором я писал в прошлой статье, делает Redis избыточным во многих сценариях.

      Плюс железо постоянно двигает "предел универсальности".

      Только когда речь заходит об очень больших нагрузках, которые есть у малой части проектов, или интеграции очень разнородных систем там без кучи дополнительных сервисов никуда.


      1. khomenkoev
        14.09.2026 13:23

        Справедливо, «предел универсальности» благодаря железу и расширениям действительно отодвинулся, и для условных 80% средних проектов Postgres сегодня — это отличная «серебряная пуля».

        Но когда проект и организация растут, монополия Pg начинает мешать не потому, что он «не тянет» железно, а из-за смешения контекстов и нагрузок на уровне движка. Дьявол кроется в двух плоскостях: эксплуатационной и архитектурной.

        1. Эксплуатационные боли (Day-2 Operations):

        • Проблемы с MVCC и VACUUM: Использование Pg в качестве очереди/WAL-брокера порождает колоссальный объем UPDATE и DELETE. Автовакуум часто не успевает за очисткой мертвых строк (dead tuples), что приводит к раздуванию (bloat) таблиц, индексов и деградации дискового IO.

        • Конкуренция за Shared Buffers: Когда в одной БД живут OLTP, аналитика и Vector DB, они вымывают кэш друг друга. Один тяжелый запрос или построение векторного индекса (HNSW) гарантированно выкинут «горячие» транзакционные данные из shared_buffers.

        • Раздувание Blast Radius: Если из-за утечки памяти в условном расширении или кривого json-индекса упадет инстанс Pg, то «приляжет» вообще всё — и кор-транзакции, и очереди, и витрины. Специализированные решения изолируют эти риски.

        • Обслуживание: Делать бэкапы, репликацию и накатывать миграции на терабайтную «свалку всего» кратно сложнее, чем администрировать изолированные легковесные сервисы.

        2. Архитектурные ограничения:

        • Размытие контрактов и связанность (Tight Coupling): Кафка ценна не пропускной способностью, а тем, что она выступает в роли изолирующего слоя (Decoupled Layer) со своей экосистемой (Schema Registry). Чтение WAL напрямую или через Debezium завязывает другие команды на внутреннюю структуру ваших таблиц. Изменили схему ради оптимизации своего сервиса — сломали контракты потребителям.

        • Проблема распределенного состояния (In-Memory кэш): Идея заменить Redis синхронизированным кэшем внутри приложения за счет репликации хорошо звучит только для монолита. В микросервисах это масштабируется плохо: мы либо дублируем гигабайты кэша в памяти каждого пода, либо вынуждены городить свою распределенную систему инвалидации, заново изобретая Redis, но сбоку.

        • Утилизация соединений (Connection Pooling): Архитектура Postgres «один клиент = один процесс» делает его крайне чувствительным к количеству соединений. Когда десятки микросервисов идут в одну БД за кэшем, очередями и транзакцииями, менеджмент сессий начинает отъедать слишком много ресурсов.

        Так что тут вопрос скорее не в том, может ли Postgres выполнять все эти функции (может, и делает это круто), а в том, какую цену команда платит за эту универсальность на долгой дистанции.


        1. gandjustas Автор
          14.09.2026 13:23

          Я бы тоже мог в ИИ засунуть диалог и попросил сгенерировать ответ, но напишу сам.

          Использование Pg в качестве очереди/WAL-брокера порождает колоссальный объем UPDATE и DELETE

          Очевидно вы не скормили ИИ сам код из статьи, потому что там нет ни одного UPDATE и DELETE. Это как раз пробелма outbox, о которой я писал в главе 3, но и она при желании обходится. Об этом будет следующая статья.

          Конкуренция за Shared Buffers: Когда в одной БД живут OLTP, аналитика и Vector DB, они вымывают кэш друг друга. Один тяжелый запрос или построение векторного индекса (HNSW) гарантированно выкинут «горячие» транзакционные данные из shared_buffers

          Проблема только в том, то если вы разложите те же данные на разные серверы, то объем ресурсов для их обработки увеличится. Можно все эти ресурсы отдать для PG и точно хватит для тех же нагрузок.
          Суть всех OLAP и HNSW в том, что они позволяют делать специализированные запросы, не поднимая все данные с диска в память разом. Поэтому расход памяти на один запрос у них не катастрофический и один жирный PG с расширениями будет вывозить нагрузку которую вы поделите на 3 специализированные системы, как минимум до определенного предела. Который все время двигается.
          Но самое главное - никто не мешает поднять на одном сервере несколько инстансов PG, один для OLAP, один для OLTP, а еще один для pgvector и поделить ресурсы между ними. Чуть больше администрирования и все.

          сли из-за утечки памяти в условном расширении или кривого json-индекса упадет инстанс Pg, то «приляжет» вообще всё

          Если у вас упадет основная мастер-база, то у вас и так все приляжет. Страшна завязка не на основной компонент, как БД, а на второстепенный. Например если все прилегает если упал Кэш и вы переключились на readable репклику, в которую нельзя ничего записать, а новый мастер еще не выбран.

          Обслуживание: Делать бэкапы, репликацию и накатывать миграции на терабайтную «свалку всего» кратно сложнее, чем администрировать изолированные легковесные сервисы

          Как раз наоборот. Постгрес очень легко бекапится. По сути реплки и есть бекап. С репили можно сделать еще одну реплику. Или снять дамп если вам нужно подмножество данных. Восстановление прода на больших объемах это или миграция или катастрофа, в обычной жизни не встречается. А в малых объемах - просто накатите дамп.
          Это все сильно проще чем отдельный бекап-рестор разных систем.

          Еще раз повторю мысль: Postgres вам достаточно пока вы не упретесь в проблемы его производительности. Большинство проектов не упрется в них никогда. Они скорее упрутся в кривые запросы и не совсем прямые руки сисадминов.


          1. khomenkoev
            14.09.2026 13:23

            С вами приятно общаться. Как бы мы ни хотели, Postgres всё же медлительный. У него есть свои минусы с мертвыми данными, которые приходится чистить либо на автомате (когда система позволяет сделать это без блокировок, например, со стороны каких-нибудь отчетов), либо вручную, запуская вакуумирование или реиндексацию. Сценарий “кривых рук”, как вы написали. Я с таким сталкивался: архитектору или администратору БД проще закрывать на это глаза — не они же тратят по несколько часов в неделю на ручную рутину. Впрочем, это касается и других сфер ИТ. Цифровой век требует беспрецедентной скорости, мощности и современности. Для этого необходимы принципиально другие инструменты, способные работать молниеносно. Такие механизмы уже существуют, и сегодня проще внедрить новое технологическое решение с нуля, чем пытаться переделать устаревающее.
            Возвращаясь к Kafka: она останется актуальной и сегодня, и завтра. Пока нет смысла её убирать (разве что в вашей схеме) — со своими задачами она справляется лучше всех.
            p.s. без примеров ИИ


            1. gandjustas Автор
              14.09.2026 13:23

              Медлительный по сравнению с чем?

              Можете привести тест который покажет медлительность Postgres на end-to-end сценарии?


              1. khomenkoev
                14.09.2026 13:23

                Согласен, наиболее показательный был бы сквозной тест end-to-end - он выявляет узкие места в реальном сценарии. Времени и желания доказывать нет. Вопрос по существу понятен


  1. kmatveev
    14.09.2026 13:23

    Помимо неприятного LLM-ного стиля статьи, я осуждаю её основную мысль. Не надо использовать логическую wal-репликацию для Postgres, она плохо заменяет Kafka. Процесс walsender не очень хорошо масштабируется под большое количество соединений. Медленный получатель заставит wal расти. А если получатель отстанет достаточно сильно, то его подписку отменят, и он потеряет данные. Нет возможности распараллелить обработку данных. Debezium страдает от аналогичных проблем. Самый простой вариант: отослать в Kafka, чтобы сохранить в базу уже получателем, автор не рассмотрел.


    1. gandjustas Автор
      14.09.2026 13:23

      Можете подробнее рассказать почему логическая репликация не достаточно хорошо масштабируется и большое количество соединений это сколько?

      Я несколько раз слышал это аргумент, но не понял сколько это в цифрах. Сколько нужно подключений логической репликации чтобы стало катастрофически плохо работать? Я видел глазами десятки на один небольшой сервер и сервер этого фактически не замечал.

      Если у нас читатель в принципе не успевает обрабатывать все сообщения в единицу времени, то без разницы что будет транспортом Kafka или WAL, рано или поздно и тот и другой потеряют сообщения из-за того, что хранилище переполнится. В базе данные сохранятся в таблицах, они сами по себе никуда не потеряются, их можно будет прочитать и обработать в пакетном режиме. Kafka потеряет насовсем.

      Самый простой вариант: отослать в Kafka, чтобы сохранить в базу уже получателем, автор не рассмотрел.

      В статье пример использования Kafka для МСА, где каждый сервис выполняет свою полезную работу. Если Сервис просто пересылает сообщение в кафку, то никакой полезной работы он не выполняет. Можно клиентский запрос отправить сразу на целевой сервис.


      1. kmatveev
        14.09.2026 13:23

        Нет, мы не проводили исследований, в которых бы менялось количество подключений, количество трафика по каждому и прочее. У нас подключений немного (единицы), объём трафика - примерно 20-50 тысяч сообщений в секунду, по килобайту каждое, и хорошее железо. И мы сталкивались с проблемами репликационного трафика, и даже с осторожностью используем его непосредственно для репликации: какой-нибудь триггер в базе-получателе может всё тормознуть.

        Если у нас читатель в принципе не успевает обрабатывать все сообщения в единицу времени, то без разницы что будет транспортом Kafka или WAL, рано или поздно и тот и другой потеряют сообщения из-за того, что хранилище переполнится.

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

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

        Из Kafka тоже гораздо правильнее обрабатывать в пакетном режиме, вам брокер обеспечивает пачки. Как их обеспечить в протоколе репликации - я не проверял.

        В статье пример использования Kafka для МСА, где каждый сервис выполняет свою полезную работу. Если Сервис просто пересылает сообщение в кафку, то никакой полезной работы он не выполняет. Можно клиентский запрос отправить сразу на целевой сервис.

        так и нужно делать.


        1. gandjustas Автор
          14.09.2026 13:23

          У нас подключений немного (единицы), объём трафика - примерно 20-50 тысяч сообщений в секунду, по килобайту каждое, и хорошее железо.

          20-50мб трафика только на сообщения это выше того порога, про который идет речь в статье. Поэтому не вижу смысла спорить.

          По статистике от 50% до 80% имеют трафик меньше на порядок.


  1. gandjustas Автор
    14.09.2026 13:23

    [del] не туда


  1. northrop
    14.09.2026 13:23

    Не совсем понятно где на схеме 2 update. Insert 2 раза вижу.