
Это история про архитектуру одного сервиса. Рассказанная до конца, а не только до того места, где обычно останавливаются статьи про архитектуру. Намеренно упрощена бизнес-логика, чтобы не размывать основной смысл.
Глава 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-сервис никогда не узнает, что событие произошло.
И это не экзотика на 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); ... });
Проблема dual write решена. Но взамен мы получили новую.
Цена атомарности
Цена |
Суть |
|---|---|
Write amplification |
|
Vacuum |
Постоянно обновляемая outbox-таблица генерирует мёртвые кортежи |
Фоновый диспетчер |
Ещё один процесс, конкурирующий за соединения к Postgres |
Задержка |
Интервал опроса. Не миллисекунды. Сотни миллисекунд. |
И главное — вся эта нагрузка живёт внутри того же процесса и той же базы, что обслуживает бизнес-логику. Частый поллинг создаёт постоянный фоновый I/O даже тогда, когда сообщений нет.
Глава 4. Debezium. Выносим боль за пределы сервиса
Всю эту нагрузку не обязательно держать в том же процессе и постоянно дергать базу. Postgres и так пишет каждое изменение в WAL (Write-Ahead Log). Debezium — это Kafka Connect коннектор, который читает WAL с помощью логической репликации и публикует результат в Kafka-топик. Приложение делает только INSERT. Outbox не нужен. Всю работу по надёжной доставке берёт на себя отдельный процесс.
Опрос сообщества 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" } }
Важные настройки:
slot.drop.on.stop: true— без этого при удалении коннектора слот останется висеть и будет копить WAL, пока не кончится диск. Подробнее о том, как настраивать репликацию и администрировать слоты.poll.interval.ms: 100— по умолчанию 500 мс. Нужно для тестов.
Мы решили проблему нагрузки на приложение. Мы не решили проблему количества движущихся частей: теперь у нас 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. Отдельного «отправить в очередь» просто не требуется:
Ноль 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); });
Глава 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.msKafka Connect (по умолчанию 500 мс). Уменьшение до 100 мс: 426 → 507 итер/сек. Узкое место — таймер опроса.ZeroAlloc.Outbox: Задержки дает не интервал опроса, а последовательная отправка внутри батча (
BatchSize = 50). Уменьшение интервала с 1с до 100мс: 28 → 56 итер/сек (×2, а не ×10). Дальнейшее уменьшение бессмысленно — нужно делать батч на клиенте, но для этого надо писать свой Outbox.
Глава 8. «А что если нагрузка вырастет?»
Практически любой разговор о нужности Кафки сводится к этому аргументу.
Но насколько реально можно поиметь проблемы:
Вряд ли у вас будет так много данных https://topicpartition.io/definitions/small-data, ваш бизнес и кодовая база не растут так быстро.
По данным aiven.io 80% Kafka кластеров не превышают 1 МБ/с
Отчет RedPanda за 2023-2024 год показывает, что у 56% компаний трафик данных ≤1 МБ/с
Postgres достаточно быстр чтобы на современных дисках держать огромные нагрузки
Наш собственный замер: до 5 600 сообщений в секунду и 1,3 МБ/с через прямую WAL-репликацию — на одном CPU, без единой оптимизации.
Подавляющее большинство систем, которые сегодня платят за Kafka, физически не приближаются к границам, за которые логическая репликация не выходит.
Когда Kafka действительно нужна
Много разнородных downstream-потребителей не под вашим контролем
Десятки тысяч сообщений/сек и растёт
Бизнесу нужна долгая история с произвольной перемоткой \ пререпроигрыванием истории, и вы можете это сделать за разумное время
Много медленных консьюмеров и их число меняется (не совместимо с предыдущим)
Когда логической репликации достаточно
Вы владеете и продюсером, и консьюмером
Нагрузка до десятков тысяч сообщений/сек
Хотите удешевить инфраструктуру
Критична консистентность и задержки
Финал: мы вернулись туда, откуда начали
Мы сделали полный круг. Прямой вызов → Kafka, чтобы разнести сервисы → outbox (через ZeroAlloc.Outbox), чтобы сообщения не терялись → Debezium, чтобы outbox не грузил приложение → прямое чтение WAL, код эндпоинта /replication выглядит как код прямого вызова.
Мораль: Используйте Postgres, пока не столкнулись с проблемами масштабирования. Когда столкнётесь — у вас будет конкретная измеренная метрика, чтобы обосновать добавление Kafka или другого компонента. А не абстрактное «так принято в микросервисах».
Комментарии (28)

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

MonkAlex
14.09.2026 13:23Какую задачу решаете?
Если задача "оповестить заинтересованных, что случилось событие1", то я не вижу этого в решении через БД. Где событие, как версионируется контракт, как подписываться на события потребителям в других сервисах?
Как выглядит предложенный вариант? В wal попадает что - событие специальное для оповещения или какая рядовая информация вида "создан заказ 123 с полями ..."?
А ещё очень подозрительно звучит идея дать репликацию эээ аутбоксов между разными БД. Вместо общей универсальной шины общения между N сервисами у вас будет каждое с каждым и комбинаторный взрыв?
ПС: я понимаю что кафка может быть перебором. Я не уверен что предложенное решение хорошее.

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

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

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

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

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

gandjustas Автор
14.09.2026 13:23В пике видел 2000 прочитанных сообщений\сек, каждое из которых вызывало нетривиальную работу. Это был потолок для читателей, а не для WAL. WAL при этом не рос больше чем обычно.
Естественно отправители и получатели были сделаны одной командой, поэтому никаких проблем нарушения контракта не было. В статье я указал, что при очень разнородных получателях не стоит использовать прямое чтение WAL. Лучше Debenzium + Kafka тогда.

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

gandjustas Автор
14.09.2026 13:23Если вы делаете оба сервиса, то откуда такая проблема?

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

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.

gandjustas Автор
14.09.2026 13:23Пока мы видим ровно обратную картину. Pg все больше подъедает workload других систем. Добавление jsonb и json индексов почти похоронило nosql решения. Если бы апдейт в постгресе не был таким дорогим, то наверное уже совсем похоронило бы.
timescaleDb и pgvector прекрасно работают для подавляющего большинства задач.
Таже самая репликация и синхронизированный кэш в приложении за счет нее, о котором я писал в прошлой статье, делает Redis избыточным во многих сценариях.
Плюс железо постоянно двигает "предел универсальности".
Только когда речь заходит об очень больших нагрузках, которые есть у малой части проектов, или интеграции очень разнородных систем там без кучи дополнительных сервисов никуда.
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 выполнять все эти функции (может, и делает это круто), а в том, какую цену команда платит за эту универсальность на долгой дистанции.

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 вам достаточно пока вы не упретесь в проблемы его производительности. Большинство проектов не упрется в них никогда. Они скорее упрутся в кривые запросы и не совсем прямые руки сисадминов.

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

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

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

gandjustas Автор
14.09.2026 13:23Можете подробнее рассказать почему логическая репликация не достаточно хорошо масштабируется и большое количество соединений это сколько?
Я несколько раз слышал это аргумент, но не понял сколько это в цифрах. Сколько нужно подключений логической репликации чтобы стало катастрофически плохо работать? Я видел глазами десятки на один небольшой сервер и сервер этого фактически не замечал.
Если у нас читатель в принципе не успевает обрабатывать все сообщения в единицу времени, то без разницы что будет транспортом Kafka или WAL, рано или поздно и тот и другой потеряют сообщения из-за того, что хранилище переполнится. В базе данные сохранятся в таблицах, они сами по себе никуда не потеряются, их можно будет прочитать и обработать в пакетном режиме. Kafka потеряет насовсем.
Самый простой вариант: отослать в Kafka, чтобы сохранить в базу уже получателем, автор не рассмотрел.
В статье пример использования Kafka для МСА, где каждый сервис выполняет свою полезную работу. Если Сервис просто пересылает сообщение в кафку, то никакой полезной работы он не выполняет. Можно клиентский запрос отправить сразу на целевой сервис.

kmatveev
14.09.2026 13:23Нет, мы не проводили исследований, в которых бы менялось количество подключений, количество трафика по каждому и прочее. У нас подключений немного (единицы), объём трафика - примерно 20-50 тысяч сообщений в секунду, по килобайту каждое, и хорошее железо. И мы сталкивались с проблемами репликационного трафика, и даже с осторожностью используем его непосредственно для репликации: какой-нибудь триггер в базе-получателе может всё тормознуть.
Если у нас читатель в принципе не успевает обрабатывать все сообщения в единицу времени, то без разницы что будет транспортом Kafka или WAL, рано или поздно и тот и другой потеряют сообщения из-за того, что хранилище переполнится.
Это правда. Но у kafka есть масштабирование и при отправке, и при обработке: можно запустить несколько обработчиков параллельно, развязав их по разным партициям.
В базе данные сохранятся в таблицах, они сами по себе никуда не потеряются, их можно будет прочитать и обработать в пакетном режиме.
Из Kafka тоже гораздо правильнее обрабатывать в пакетном режиме, вам брокер обеспечивает пачки. Как их обеспечить в протоколе репликации - я не проверял.
В статье пример использования Kafka для МСА, где каждый сервис выполняет свою полезную работу. Если Сервис просто пересылает сообщение в кафку, то никакой полезной работы он не выполняет. Можно клиентский запрос отправить сразу на целевой сервис.
так и нужно делать.

gandjustas Автор
14.09.2026 13:23У нас подключений немного (единицы), объём трафика - примерно 20-50 тысяч сообщений в секунду, по килобайту каждое, и хорошее железо.
20-50мб трафика только на сообщения это выше того порога, про который идет речь в статье. Поэтому не вижу смысла спорить.
По статистике от 50% до 80% имеют трафик меньше на порядок.
mosinnik
С каких пор кафка теряет сообщения? Если вы не можете настроить rf=3, isr=2, ack=all и дождаться ack от кафки, то почему это вдруг кафка теряет сообщения, а не вы криво все настроили, не разобравшись в выбранном иснтрументе?
gandjustas Автор
Такое ощущение что вы прочитали только последний абзац, а не всю статью от начала до конца.
mosinnik
прочитал всю и даже перечитал, чтобы найти где расписано что именно кафка теряет сообщения, а не кривой код. Потрудитесь привести что ли доказательства, либо уж поправьте текст на то что вы хотели донести, чтобы не вводить в заблуждение о надежности самой кафки
gandjustas Автор
Глава 2 об этом. Я пожалуй в тексте поправлю, чтобы не было идеи что дело в кафке.