<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>zero deps — инженерные статьи</title>
    <link>https://zerodeps.tech/texts</link>
    <description>V8, Node.js, производительность, архитектура и инженерная практика.</description>
    <language>ru</language>
    <lastBuildDate>Fri, 17 Jul 2026 06:00:00 GMT</lastBuildDate>
    <atom:link xmlns:atom="http://www.w3.org/2005/Atom" href="https://zerodeps.tech/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Смена типа тоже не бесплатна</title>
      <link>https://zerodeps.tech/texts/changing-a-type-is-not-free</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/changing-a-type-is-not-free</guid>
      <pubDate>Fri, 17 Jul 2026 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Одинаковый hidden class не гарантирует стабильность оптимизированного кода: V8 специализируется ещё и на representation поля. Когда числовое поле впервые получает строку, representation расширяется, зависимый машинный код может деоптимизироваться, а дальнейшая цена зависит от функции, версии V8 и нагрузки.</description>
      <content:encoded><![CDATA[<p><strong>Смена типа тоже не бесплатна</strong></p>
<p>Привет, читатель. Давненько из меня не выходило контента. Пора исправляться. Принёс для тебя дополнение к прошлой статье про объекты.</p>
<p>Я разбирал hidden classes, inline cache и форму объектов. Главный практический совет был простой: если поле опциональное, создай его сразу и положи <code>null</code>, чтобы все объекты сохранили одинаковую форму.</p>
<p>Совет рабочий, но неполный. Форма объекта у V8 может остаться прежней, а контракт поля всё равно изменится. И тогда горячая функция тоже поедет на деоптимизацию.</p>
<p>Если через один путь проходят десятки тысяч однотипных объектов, и ты уже полез смотреть за их формой, стоит понимать и вторую половину механики.</p>
<p><strong>Map знает не только форму</strong></p>
<p>У каждого объекта в V8 есть Map, он же hidden class. Map описывает набор полей, их порядок и смещения. Но в дескрипторе поля хранится ещё и его внутренняя representation, то есть способ, которым V8 ожидает хранить значение.</p>
<p>Упрощённо интересующая нас цепочка выглядит так:</p>
<pre data-language="text"><code class="language-text">Smi        маленькое целое, закодированное прямо в tagged-слоте
Double     числовое значение с плавающей точкой
HeapObject ссылка на объект в куче: строку, объект, null, undefined и так далее
Tagged     широкое представление, которое принимает и Smi, и ссылки</code></pre>
<p>Это внутренний контракт хранения. В самом V8 отдельно существует ещё и field type, но для перехода из <code>number</code> в <code>string</code> нам достаточно representation.</p>
<p>Когда V8 много раз видит <code>{ x: 42 }</code>, поле <code>x</code> может получить representation <code>Smi</code>.</p>
<p>Оптимизатор по известному Map читает поле как маленькое целое и строит код под этот контракт.</p>
<p>Потом кто-то делает так:</p>
<pre data-language="js"><code class="language-js">obj.x <span class="syntax-operator">=</span> <span class="syntax-string">&#39;boom&#39;</span></code></pre>
<p>Строка уже не помещается в <code>Smi</code>. V8 расширяет representation до <code>Tagged</code>, потому что теперь поле должно принимать и целые числа, и ссылки на объекты в куче.</p>
<p>Ключевой момент: <strong>это не обязательно создаёт новый Map.</strong> Для некоторых расширений V8 обновляет дескриптор на месте, и адрес Map остаётся тем же. Но оптимизированный код зависел не только от адреса Map, а ещё и от representation. Контракт изменился, зависимый код больше нельзя считать безопасным для использования.</p>
<p><strong>Вот сам деопт</strong></p>
<p>Минимальный пример на Node 24.17.0 с V8 13.6:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">function</span> <span class="syntax-function">readX</span>(object) {
  <span class="syntax-keyword">return</span> object.x
}

<span class="syntax-keyword">const</span> a <span class="syntax-operator">=</span> { x<span class="syntax-operator">:</span> <span class="syntax-number">1</span> }
<span class="syntax-keyword">const</span> b <span class="syntax-operator">=</span> { x<span class="syntax-operator">:</span> <span class="syntax-number">2</span> }

<span class="syntax-comment">// Убираем отдельную зависимость от constness поля.</span>
a.x <span class="syntax-operator">=</span> <span class="syntax-number">3</span>
b.x <span class="syntax-operator">=</span> <span class="syntax-number">4</span>

<span class="syntax-keyword">for</span> (<span class="syntax-keyword">let</span> i <span class="syntax-operator">=</span> <span class="syntax-number">0</span>; i <span class="syntax-operator">&lt;</span> <span class="syntax-number">100_000</span>; i<span class="syntax-operator">++</span>) {
  <span class="syntax-function">readX</span>(i <span class="syntax-operator">&amp;</span> <span class="syntax-number">1</span> <span class="syntax-operator">?</span> a <span class="syntax-operator">:</span> b)
}

<span class="syntax-comment">// --allow-natives-syntax</span>
<span class="syntax-operator">%</span><span class="syntax-function">OptimizeFunctionOnNextCall</span>(readX)
<span class="syntax-function">readX</span>(a)

a.x <span class="syntax-operator">=</span> <span class="syntax-string">&#39;boom&#39;</span></code></pre>
<p>Запускаем:</p>
<pre data-language="bash"><code class="language-bash">node --allow-natives-syntax --trace-deopt demo.js</code></pre>
<p>И получаем:</p>
<pre data-language="text"><code class="language-text">[marking dependent code ... (readX) for deoptimization,
 reason: dependent field representation changed]</code></pre>
<p>То есть одна запись строки действительно инвалидировала уже оптимизированную <code>readX</code>.</p>
<p>В этой версии V8 <code>%HaveSameMap(a, b)</code> остаётся истинным даже после присваивания строки. Форма та же, Map тот же, а деоптимизация всё равно случилась.</p>
<p><code>%OptimizeFunctionOnNextCall</code> и <code>%HaveSameMap</code> являются внутренними средствами диагностики V8. В прод их, разумеется, тащить не стоит.</p>
<p><strong>Где здесь подвох с <code>null</code></strong></p>
<p>Вернёмся к совету «создай поле заранее и положи <code>null</code>».</p>
<p>Для формы объекта он по-прежнему полезен:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">const</span> user <span class="syntax-operator">=</span> { id, name, role<span class="syntax-operator">:</span> <span class="syntax-literal">null</span> }

<span class="syntax-keyword">if</span> (isAdmin) {
  user.role <span class="syntax-operator">=</span> <span class="syntax-string">&#39;admin&#39;</span>
}</code></pre>
<p>Поле <code>role</code> есть у всех объектов с самого начала. Кроме того, и <code>null</code>, и строка относятся к <code>HeapObject</code>, поэтому именно representation при такой записи расширять не требуется.</p>
<p>С числом история другая:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">const</span> stats <span class="syntax-operator">=</span> { score<span class="syntax-operator">:</span> <span class="syntax-literal">null</span> } <span class="syntax-comment">// HeapObject</span>
stats.score <span class="syntax-operator">=</span> <span class="syntax-number">42</span>              <span class="syntax-comment">// нужно принять ещё и Smi, получаем Tagged</span></code></pre>
<p>Если код успел оптимизироваться между этими двумя состояниями, расширение поля может инвалидировать зависимую оптимизацию.</p>
<p>Для целочисленного поля стоит сразу дать целочисленное значение:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">const</span> stats <span class="syntax-operator">=</span> { score<span class="syntax-operator">:</span> <span class="syntax-number">0</span> }
stats.score <span class="syntax-operator">=</span> <span class="syntax-number">42</span></code></pre>
<p>С <code>undefined</code> та же проблема, что с <code>null</code>: для V8 это объект в куче, а не числовая заглушка.</p>
<p>Но не стоит превращать это в очередной карго-культ. Если <code>null</code> является честным состоянием бизнес-модели, оставь <code>null</code>. Семантика программы важнее одной потенциальной деоптимизации на прогреве.</p>
<p>А если поле хранит дробные числа, одного <code>0</code> тоже недостаточно для идеальной стабильности: переход от <code>Smi</code> к <code>Double</code> способен потребовать ещё одно расширение.</p>
<p>Идеальный вариант для горячей структуры: присвоить реальное значение до того, как код станет горячим.</p>
<p><strong>Расширение происходит не на каждом присваивании</strong></p>
<p>Здесь легко сделать неправильный вывод: будто каждое переключение <code>42 → &#39;boom&#39; → 42</code> снова деоптимизирует функцию из-за representation.</p>
<p>Нет. Representation расширяется до <code>Tagged</code> один раз и самостоятельно обратно до <code>Smi</code> уже не сужается. После этого и число, и строка помещаются в установленный контракт. Повторной генерализации поля на каждом переключении нет.</p>
<p>Деопты всё ещё возможны, если горячий код специализировался на фактических значениях. Например, выражение <code>object.x + 1</code> сначала выполняет числовое сложение, а со строкой начинает конкатенацию.</p>
<p>Это уже другая причина: обратная связь по типам самой операции плюс возможные аллокации строк. Сваливать всё это в одну корзину с field representation удобно только до первого человека, который полезет в <code>--trace-deopt</code>.</p>
<p>Поэтому я не буду продавать здесь красивое «переключение типов замедляет цикл ровно в два раза». Такой микробенч легко измеряет одновременно чтение поля, смену representation, арифметическую специализацию, конкатенацию и повторную оптимизацию. Цифра получится эффектной, но инженерного смысла в ней будет примерно нисколько.</p>
<p>Защищаемый вывод уже есть в трейсе: первое расширение representation способно инвалидировать весь оптимизированный код, который от неё зависит. Цена конкретного деопта зависит от функции, нагрузки, версии V8 и того, сколько зависимого кода придётся пересобрать.</p>
<p><strong>Что делать-то?</strong></p>
<p>Да, в целом ничего :)</p>
<p>Это просто интересные факты про Node.js, так же, как и про Map в прошлой статье.</p>
<p>Но если ты оптимизациофил, то:</p>
<ul><li>Создавай горячие объекты с полным и стабильным набором полей.</li><li>Для целочисленного поля используй числовое начальное значение, а не <code>null</code> или <code>undefined</code>.</li><li>Присваивай реальное значение до прогрева, особенно если поле может хранить дробные числа.</li><li>Не смешивай число и строку в одном поле без причины.</li><li>Проверяй подозрения через <code>--trace-deopt</code>.</li></ul>
<p><strong>Что в итоге</strong></p>
<p>Одинаковая форма объекта ещё не гарантирует, что оптимизированный код останется валидным. V8 учитывает representation полей и может встроить это знание в машинный код.</p>
<p>Переход <code>number → string</code> дорог не потому, что строка сама по себе медленная. Он опасен в момент, когда ломается контракт уже скомпилированной функции. V8 расширяет поле, помечает зависимый код на деоптимизацию и затем работает с новым, более широким представлением.</p>
<p>А <code>null</code> ни хороший, ни плохой. Для ссылочного поля это нормальная заглушка. Для числового поля это уже другой внутренний контракт.</p>
<p>JS разрешает нам менять типы как угодно. V8 тоже не против. Но лучше не менять.</p>
<p>Кушайте кашу, читайте книги и не меняйте типы без необходимости.</p>
<p><strong>Что почитать</strong></p>
<p>• <a href="https://v8.dev/blog/fast-properties" target="_blank" rel="noopener noreferrer">Fast properties in V8</a>, официальный разбор Maps, дескрипторов и быстрых свойств • <a href="https://chromium.googlesource.com/v8/v8/+/refs/heads/main/src/objects/property-details.h" target="_blank" rel="noopener noreferrer">Representation в исходниках V8</a>, актуальная внутренняя модель <code>Smi</code>, <code>Double</code>, <code>HeapObject</code> и <code>Tagged</code> • <a href="https://chromium.googlesource.com/v8/v8/+/refs/heads/main/src/objects/map-updater.cc" target="_blank" rel="noopener noreferrer">MapUpdater в исходниках V8</a>, генерализация полей и обновление зависимого кода • <a href="https://mathiasbynens.be/notes/shapes-ics" target="_blank" rel="noopener noreferrer">V8 internals: hidden classes &amp; inline caches</a>, подробный разбор Mathias Bynens</p>]]></content:encoded>
    </item>
    <item>
      <title>Объекты в JS не бесплатные</title>
      <link>https://zerodeps.tech/texts/js-objects-are-not-free</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/js-objects-are-not-free</guid>
      <pubDate>Tue, 21 Apr 2026 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Стабильная форма JavaScript-объекта позволяет V8 читать поля через hidden classes и inline cache. Разный порядок полей, условное добавление свойств и особенно delete размывают этот контракт; на горячем пути это переводит доступ от monomorphic к megamorphic или dictionary mode и заметно увеличивает стоимость операции.</description>
      <content:encoded><![CDATA[<p><strong>Объекты в JS не бесплатные</strong></p>
<p>_Hidden classes, inline cache и почему shape объекта важнее, чем кажется._</p>
<p>Продолжу про микро-оптимизаций. После <code>Date.now()</code> и <code>parseInt/parseFloat</code> — про, казалось бы, самую безобидную вещь: обычный <code>{}</code>.</p>
<p>Сразу оговорка: для 99% систем это не имеет значения. А вот если у тебя hot path, через который проходят десятки тысяч объектов в секунду, или ты пишешь библиотечный код, стоит знать, что V8 на самом деле делает с твоими объектами.</p>
<p><strong>И чего он там с ними делает?</strong></p>
<p>Когда ты создаёшь объект, V8 не выделяет «мешок ключей». Он строит для него <strong>hidden class</strong> (_map_ внутри V8) структуру, которая описывает layout: какие у объекта поля, в каком порядке они лежат, какого типа, по каким офсетам читать. Сам объект это компактный массив значений, как struct в C. Hidden class хранится отдельно, и каждый объект держит на него ссылку.</p>
<p>Hidden classes связаны между собой через <strong>transitions</strong> и вместе образуют <strong>transition tree</strong>. Когда ты добавляешь поле к объекту, V8 идёт по исходящему переходу из текущего hidden class: если переход с таким полем уже есть переключается на существующий дочерний, если нет создаёт новый hidden class и новую ветку дерева.</p>
<p>Если несколько объектов идут по одним и тем же переходам в одинаковом порядке они <strong>разделяют один hidden class</strong>. Это даёт V8 базу для <strong>inline cache</strong>: «по этому офсету всегда лежит string, читаем напрямую, без проверок».</p>
<p>Когда hidden classes начинают расходиться (по порядку, по типам, по добавлению/удалению), inline cache переходит: – <strong>monomorphic</strong> (один shape — fast path), – <strong>polymorphic</strong> (до 4 shapes — V8 проверяет каждый, всё ещё быстро), – <strong>megamorphic</strong> (4+ — V8 сдаётся, идёт через generic lookup).</p>
<p>Стоимость растёт на каждом шаге.</p>
<p><strong>Где ты теряешь?</strong></p>
<p>Классический пример постепенная сборка vs литерал:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">const</span> a <span class="syntax-operator">=</span> {}
a.id <span class="syntax-operator">=</span> <span class="syntax-number">1</span>
a.name <span class="syntax-operator">=</span> <span class="syntax-string">&#39;jopa&#39;</span>

<span class="syntax-keyword">const</span> b <span class="syntax-operator">=</span> { id<span class="syntax-operator">:</span> <span class="syntax-number">1</span>, name<span class="syntax-operator">:</span> <span class="syntax-string">&#39;jopa&#39;</span> }</code></pre>
<p>Если порядок добавления полей совпадает, оба варианта в итоге попадают на один hidden class. У литерала есть бонус V8 запоминает финальный shape как <strong>boilerplate</strong> и при повторных вызовах аллоцирует объект сразу с готовым layout, без прохода по transition tree. Но в hot loop разница в установившемся режиме копеечная.</p>
<p>Хуже, если порядок полей в разных фабриках не совпадает:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">function</span> <span class="syntax-function">makeA</span>(id, name) { <span class="syntax-keyword">return</span> { id, name } }
<span class="syntax-keyword">function</span> <span class="syntax-function">makeB</span>(name, id) { <span class="syntax-keyword">return</span> { name, id } }</code></pre>
<p>Это уже <strong>два разных hidden class</strong>. Любой код, который читает <code>obj.id</code> через эти фабрики, видит два shape на одном call site и IC становится polymorphic. Само по себе ещё не больно, но если таких разных объектов не два, а шесть, то добро пожаловать в megamorphic.</p>
<p>Ещё кейс условное добавление полей:</p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">const</span> user <span class="syntax-operator">=</span> { id, name }

<span class="syntax-keyword">if</span> (isAdmin) {
  user.role <span class="syntax-operator">=</span> <span class="syntax-string">&#39;admin&#39;</span>
}</code></pre>
<p>Часть объектов имеет shape <code>{id, name}</code>, часть — <code>{id, name, role}</code>. Уже polymorphic. Лучше класть <code>role: null</code> сразу и потом присвоить значение при необходимости.</p>
<p><strong>delete — отдельная история</strong></p>
<pre data-language="js"><code class="language-js"><span class="syntax-keyword">delete</span> user.name</code></pre>
<p>Вот тут V8 уже не прощает. <code>delete</code> переводит объект в <strong>dictionary mode</strong> (он же slow mode, normalized properties). Это hashmap вместо фиксированного layout. Ни inline cache, ни офсетов, каждое чтение поля идёт через hash lookup в C++ runtime. И обратно V8 объект <strong>уже не вернёт</strong>, даже если форма стабилизировалась.</p>
<p>Вроде как память освободил. А по факту превратил объект в <code>Map</code> без типизации.</p>
<p>Если поле больше не нужно лучше присвой <code>null</code> или <code>undefined</code>. Shape сохранится, движок не потеряет в оптимизации.</p>
<p><strong>А что по цифрам?</strong></p>
<p>Стенд: Node 24, миллион объектов с тремя полями, в hot loop читаем <code>obj.id</code>, меряем ns/op. По три прогона, чтобы JIT успел устаканиться.</p>
<p>— literal (same shape): <strong>3.99 нс</strong> — stepwise (same order): <strong>4.08 нс</strong> — poly IC (2 shape): <strong>4.65 нс</strong> — megamorphic (6 shapes): <strong>8.51 нс</strong> — dict mode (after <code>delete</code>): <strong>56.12 нс</strong></p>
<p>Что видно: – <strong>literal vs stepwise</strong> — разницы практически нет. V8 одинаково хорошо справляется с обоими. – <strong>polymorphic IC (2 shape)</strong> — ~15% оверхед. V8 спокойно тянет до 4 shape на одном call site. – <strong>megamorphic (6 shape)</strong> — <strong>x2+ к чтению</strong>. Уже серьёзно. – <strong>delete (dictionary mode)</strong> — <strong>x14 медленнее</strong>. Больно :)</p>
<p>На 100k RPS × 50 чтений полей на запрос с dict-mode объектами вместо нормальных — потеря ~260 мс CPU/с.</p>
<p><strong>А что было на Node 22?</strong></p>
<p>Для наглядности — те же сценарии на Node 22:</p>
<p>— literal: <strong>11.0 → 4.0 нс</strong> (x2.8) — stepwise: <strong>11.1 → 4.1 нс</strong> (x2.7) — poly (2 shape): <strong>10.3 → 4.7 нс</strong> (x2.2) — megamorphic: <strong>14.1 → 8.5 нс</strong> (x1.7) — dict mode: <strong>56.8 → 56.1 нс</strong> (=)</p>
<p>Что интересно:</p>
<p>– <strong>Fast path между релизами стал ~x2.5 быстрее.</strong> TurboFan и Maglev продолжают тюниться, монохромное чтение поля на Node 24 — 4 нс против 11 на Node 22. – <strong>Dict mode — константа.</strong> 56 нс на обеих версиях. Это C++ hashmap lookup, JIT там не играет, оптимизировать нечего. – <strong>Относительная разница fast/slow растёт.</strong> На Node 22 <code>delete</code> был x5 медленнее literal&#39;а, на Node 24 — уже <strong>x14</strong>. Fast path уходит вперёд, slow path стоит.</p>
<p>Вывод: с каждым новым V8 сидеть в dictionary mode или megamorphic IC становится всё дороже _относительно_ правильного кода.</p>
<p><strong>А что делать то?</strong></p>
<p>– В hot path — <strong>литералы</strong>, со всеми полями сразу. Не знаешь значение полож <code>null</code>. – Поля во всех фабриках — <strong>в одном порядке</strong>. Делаешь <code>{ id, name }</code>, делай везде <code>{ id, name }</code>. – <code>delete</code><strong> — не нужон</strong>. Только <code>obj.field = null</code> (или <code>undefined</code>). – Если объект сложный и часто создаётся — <strong>класс</strong> или <strong>фабрика</strong>. Один shape гарантированно. – Не стоит превращать один call site в универсальную точку на все случаи жизни: четыре shape потолок V8, дальше generic.</p>
<p><strong>Что в итоге?</strong></p>
<p>Объект в JS не «просто словарь». Это контракт с V8: ты обещаешь стабильный shape, V8 обещает быструю работу через hidden classes и inline cache. Нарушаешь — съезжаешь в polymorphic, потом megamorphic, потом dictionary mode. С каждым шагом дороже.</p>
<p>В обычном CRUD это не заметно. На hot path — это десятки процентов CPU и заметные хвосты по p99.</p>
<p><code>Date.now()</code> тратил CPU на syscall, <code>parseFloat</code> — на парсинг, плохой shape — на cache miss. А потом говорят: &quot;ой да ну какой Node, он же медленный, давайте напишем на Go и пойдем за ванильным лате на растительном&quot;</p>
<p>Node быстрый. Просто не мешай ему.</p>
<p><strong>Что</strong> еще<strong> почитать?</strong> – <a href="https://mathiasbynens.be/notes/shapes-ics" target="_blank" rel="noopener noreferrer">V8 internals: hidden classes &amp; inline caches</a> — Mathias Bynens, must-read – <a href="https://v8.dev/blog/fast-properties" target="_blank" rel="noopener noreferrer">Fast properties in V8</a> — официальный блог V8 – <a href="https://v8.dev/blog/elements-kinds" target="_blank" rel="noopener noreferrer">Elements kinds in V8 —</a> про массивы и их shapes</p>]]></content:encoded>
    </item>
    <item>
      <title>System design 6: у архитектуры нет конца</title>
      <link>https://zerodeps.tech/texts/system-design-never-ends</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/system-design-never-ends</guid>
      <pubDate>Mon, 19 Jan 2026 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Production не завершает system design, а превращает гипотезы архитектуры в наблюдаемые факты. Проектировщик возвращается к требованиям после метрик и инцидентов, обновляет карту рисков, механизмы деградации и принципы эволюции, передавая команде модель мышления, а не только схему.</description>
      <content:encoded><![CDATA[<p><strong>06. System design. А где конец то?</strong></p>
<p>Всё хорошее и плохое имеет свойство заканчиваться, и я спешу тебя порадовать: мы с тобой добрались до финальной части серии про System Design. Путь был сложный, много буков было написано. Я последовательно попытался донести ключевые моменты о процессе проектирования архитектуры.</p>
<p>Надеюсь, сейчас ты уже понимаешь, что архитектура — это далеко не просто красивая схема. Это длительный и сложный итеративный процесс сбора требований. Это глубокий анализ и понимание потоков данных. Это кропотливое планирование обратной связи. И, наконец, это трезвый выбор инструментов без погони за «стандартом индустрии». Всё это вместе помогает создать архитектуру, способную выжить в проде.</p>
<p>Примерно на этом этапе принципиальная схема с детализацией для разных команд и специалистов готова, ТЗ написано, спецификации составлены, и таргеты определены. Команды окончательно определились с инструментами и подходами к реализации, начинают писать код и собирать инфру.</p>
<p>Казалось бы, всё, конец. Можно спокойно выдохнуть, дождаться альфы, посмотреть, как это всё будет дышать в приближённой к реальности среде.</p>
<p>Но нет. К сожалению, а может, и счастью, это далеко не конец. А почему? С какого?</p>
<p><strong>У самурая есть только путь</strong></p>
<p>И у архитектуры тоже. Тот момент, когда код написан, а инфраструктура поднята, далеко не финишная черта. Это лишь первый серьёзный контрольный пункт на бесконечной трассе. Потому что живая система не может быть статичной. Она дышит, меняется, сталкивается с реальностью.</p>
<p>Ты помнишь три столпа: надёжность, масштабируемость и сопровождаемость? Так вот, прод — это полигон, где они вступают в жестокую схватку. Твоя архитектурная схема превращается в реальность, и эта реальность начинает ставить эксперименты:</p>
<p>Теория потоков данных сталкивается с практикой. Ты разделил read-path, write-path и обработку. Отлично. Но теперь метрики показывают, что твой &quot;изолированный&quot; контур обработки в фоне неожиданно конкурирует за дисковый IO с репликацией базы в пиковое время. Поток данных оказался хитрее твоей схемы. Значит, надо корректировать: менять расписание, добавлять лимиты, пересматривать приоритеты.</p>
<p>Обратная связь проходит боевое крещение. Ты настроил Circuit Breaker и backpressure. А они срабатывают слишком часто или, наоборот, молчат, когда уже всё прилегло. Приходится калибровать пороги по живому трафику. Твои &quot;управляемая деградация&quot; и &quot;предсказуемый отказ&quot; из красивых слов превращаются в конкретные скрипты, алерты и рутину эксплуатации.</p>
<p>Выбор инструментов проверяется на прочность. Ты не поддался и взял «скучный» PostgreSQL. Он отлично держит. Но оказалось, что одна конкретная аналитическая выборка, о которой забыли спросить на старте, выполняется 20 секунд. Придётся думать: то ли денормализовать, то ли параллелить, то ли подружить его с тем самым ClickHouse, против которого ты выступал.</p>
<p>Компромиссы перестают быть абстрактными. Ты решил пожертвовать строгой консистентностью ради скорости. И вот первый пользователь пишет в саппорт: &quot;Я только что лайкнул пост, а счётчик не обновился!&quot;. Бизнес задаёт вопрос: &quot;Это баг или фича?&quot;. Тебе приходится объяснять, что это не баг, а осознанный выбор, и договариваться, как сделать эту &quot;фичу&quot; менее болезненной для пользователя.</p>
<p>Это и есть тот самый путь. Архитектура — это не проект, который можно сдать. Это состояние системы и процесса вокруг неё.</p>
<p><strong>Финал? Нет, передача эстафеты</strong></p>
<p>Когда первая версия системы уезжает в прод, твоя роль как проектировщика не заканчивается. Она трансформируется. Теперь ты не столько архитектор-созидатель, сколько архитектор-исследователь и наставник.</p>
<p>Ты передаёшь эстафету командам эксплуатации (SRE/DevOps) и разработки, снабжая их не просто схемой, а моделью мышления:</p>
<p>Карта рисков и слабых мест — показываешь, где система может хрустнуть, и как это заметить по метрикам.</p>
<p>План действий при пожаре — что масштабировать в первую очередь, какие фичи можно отключить, как интерпретировать алерты.</p>
<p>Принципы эволюции — объясняешь, почему нельзя &quot;быстро прикрутить&quot; новую фичу, нарушающую разделение потоков данных, и как это сделать правильно.</p>
<p>А сам возвращаешься к началу цикла. К анализу метрик, к новым требованиям бизнеса, к проектированию следующих итераций. Потому что система, которая не эволюционирует, — мёртвая система.</p>
<p>Так что же, всё напрасно? Вечный цикл «нарисовали-сломали-перерисовали»? Никакого конца?</p>
<p>Как раз наоборот! Вся проделанная работа — сбор требований, осознание компромиссов, проектирование потоков и обратной связи, трезвый выбор инструментов — это и есть тот самый фундамент, карта и компас. Ты не построил неприступную крепость на века. Ты построил живой, адаптивный организм и дал команде инструменты для его развития.</p>
<p>Фундамент — потому что без этой работы система рухнет при первом же столкновении с реальностью. Карта — потому что теперь ты видишь не просто квадратики, а ландшафт, где текут данные и как система на них реагирует. Компас — потому что когда появляются новые требования или всё идёт не по плану, у тебя есть принципы (те самые три столпа) и понимание потоков, чтобы принимать решения, а не тыкать пальцем в небо.</p>
<p><strong>Что в итоге?</strong></p>
<p>Ты прошёл полный цикл взрослого System Design:</p>
<ol><li>Перестал <a href="https://t.me/zero_deps/56" target="_blank" rel="noopener noreferrer">рисовать квадратики</a> и понял, что дизайн — это борьба с реальностью.</li><li>Научился <a href="https://t.me/zero_deps/60" target="_blank" rel="noopener noreferrer">задавать неудобные вопросы</a>, чтобы превращать влажные фантазии бизнеса в измеримые требования.</li><li>Осознал три столпа: <a href="https://t.me/zero_deps/66" target="_blank" rel="noopener noreferrer">надёжность, масштабируемость, сопровождаемость</a> — и начал жонглировать их противоречиями.</li><li>Увидел систему как <a href="https://t.me/zero_deps/72" target="_blank" rel="noopener noreferrer">потоки данных</a> и разделил их на контуры записи, чтения и обработки.</li><li>Научил систему реагировать на мир с помощью <a href="https://t.me/zero_deps/82" target="_blank" rel="noopener noreferrer">обратной связи</a>: backpressure, circuit breakers и управляемой деградации.</li><li>Осознанно выбрал инструменты, не поддавшись искушению <a href="https://t.me/zero_deps/91" target="_blank" rel="noopener noreferrer">забивать гвозди микроскопом</a>.</li></ol>
<p>И вот теперь ты здесь. Понимаешь, что конечной точки нет. Есть только путь постоянной адаптации, рефакторинга и осознанных компромиссов. И в этом — вся красота и сложность.</p>
<p>Главное — не бояться начинать, мыслить структурно и помнить, что лучшая архитектура та, что позволяет системе и команде меняться не ломаясь. А путь, как известно, и есть главная цель.</p>
<p>Спасибо, что прошёл этот путь со мной. Удачи в проектировании систем, которые не просто работают, а живут и растут.</p>
<p>Я не прощаюсь, совсем скоро вернусь и принесу почитать что-нибудь интересное.</p>
<p>Что ещё почитать для пути:</p>
<p>· Книга «Designing Data-Intensive Applications» Мартина Клеппмана — библия по теме. · Блог <a href="https://highscalability.com/" target="_blank" rel="noopener noreferrer">High Scalability</a> — разборы архитектур реальных компаний. · Книга «Release It!» Майкла Найгарда — про то, как проектировать системы, которые не падают в проде. · <a href="https://www.thoughtworks.com/en-gb/radar" target="_blank" rel="noopener noreferrer">Technology Radar от ThoughtWorks</a> — чтобы держать руку на пульсе, но не гнаться за каждой волной.</p>]]></content:encoded>
    </item>
    <item>
      <title>System design 5: не забивай гвозди микроскопом</title>
      <link>https://zerodeps.tech/texts/choosing-technology-stack</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/choosing-technology-stack</guid>
      <pubDate>Tue, 13 Jan 2026 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Выбор стека начинается с принципиального решения — какой контракт хранения, доставки или вычисления нужен системе. Затем классы инструментов проверяются против нагрузки, модели отказов, экспертизы команды и стоимости эксплуатации; название технологии появляется в самом конце.</description>
      <content:encoded><![CDATA[<p><strong>5. System design. Забиваем гвозди микроскопом</strong></p>
<p>Надеюсь ты, читатель, выжил и с пользой провёл этот новогодний фриз. Тонны салатов, вкусной еды и тотального разложения продуктивности не смогли сломить твою тягу к знаниям и развитию.</p>
<p>С учётом пройденного пути кажется, что всё готово. Требования выбиты, потоки данных распутаны, компромиссы между надёжностью, масштабируемостью и сопровождаемостью найдены, обратная связь продумана. В голове — стройная архитектурная концепция. Пора бы уже и код писать.</p>
<p>Вот он, тот самый момент, которого все мы так ждали. Выбор инструментов. Тех самых квадратиков, которые ты будешь рисовать на схеме.</p>
<p>Рука так и тянется к модному, к «стандартам индустрии». Хочется взять Kafka, потому что «все большие так делают». Запустить десяток микросервисов на Go. Запихнуть всё в Kubernetes и поставить сверху service mesh. А базу, конечно, ту самую очередную Distributed SQL, про которую читал на Хабре.</p>
<p>Астанавись. Выбор инструментов это не награда за проделанную работу. Это не «финальный босс», которого нужно победить эпической убервафлей. Это ещё одна серия компромиссов, где твоим главным врагом становится хайп, а союзником скучный, нудный документ с требованиями.</p>
<p><strong>Почему «просто взять лучшее» не работает?</strong></p>
<p>Потому что у каждого инструмента есть своя цена. И речь не только о лицензии.</p>
<p>_Цена обучения._ Команда знает PostgreSQL как свои пять пальцев, но ты хочешь CockroachDB для глобального шардинга. Готовы ли всё на месяцы замедлиться, изучая новые паттерны, отладку и тонкости?</p>
<p>_Цена эксплуатации._ Этот блестящий распределённый кеш самовосстанавливаться при отказе двух узлов? Здорово. А кто будет его мониторить, настраивать и разбираться, почему он вдруг начал терять 1% записей? Это твои SRE? Они уже согласны?</p>
<p>_Цена связности._ Каждый новый «квадратик» не просто функция. Это новая точка отказа, новый протокол, новый клиент в коде, новый драйвер, новый источник задержек в сетевых вызовах. Микросервис на Rust может быть быстрым, но если для связи с ним всем остальным сервисам нужна кастомная бинарная сериализация, ты добавляешь сложность во всю систему.</p>
<p>_Цена моды._ Инструмент, о котором все кричат сегодня, завтра может оказаться «legacy-технологией, от которой все уходят». Выбирая его, ты берёшь на себя обязательство по его миграции через N лет.</p>
<p><strong>Так, как же выбирать то? </strong></p>
<p>Отталкивайся не от хайпа, а от следствия.</p>
<p>Твоя отправная точка не список «крутых технологий», смотри на:</p>
<p>_Требования из твоего документа._ Нужна строгая консистентность? Забудь про eventual consistency stores для записи ядра. Нужны джойны сложных агрегатов? Привет, реляционные базы или тщательно спроектированные документные.</p>
<p>_Характеристики потоков данных._ Данные пишутся редко, а читаются всегда? Кеш, или read-реплики. Нужна гарантированная доставка событий между контурами? Пора смотреть на брокеры (прости господи). Но не «воткнём Кафку», а «нам нужен персистентный лог с консьюмер-группами».</p>
<p>_Выбранные компромиссы._ Пожертвовали скоростью записи ради надёжности? Инструмент должен давать сильные гарантии durability. Пожертвовали консистентностью ради масштабируемости? Ищите базу с tuneable consistency.</p>
<p><strong>Пример: Выбираем хранилище для «ленты как у Threads».</strong></p>
<p>Из требований помнишь: скорость чтения критична, актуальность лайков/комментариев может отставать на 5 секунд, рост медленный.</p>
<p>_Искушение:_ Взять Cassandra или ScyllaDB. Горизонтальное масштабирование, высокая доступность, запись быстрая. Вайбово, надо брать!</p>
<p><em>Реальность:  </em> Модель данных: лента для каждого пользователя, которая часто пересчитывается. Это запросы по ключу (user_id) с range scan по времени. Cassandra неэффективна для range scans в рамках одного партишна. А денормализовать ленту для каждого подписчика это гигантский объём данных при малом росте.</p>
<p><em>Скучное решение:  </em> PostgreSQL с таблицей feeds (user_id, post_id, timestamp), правильными индексами и репликой для чтения. На старте выдержит легко. Команда знает. При росте сначала тюнинг, потом партициирование, и только потом, возможно, шардинг. Инструмент соответствует реальным, а не гипотетическим потокам данных.</p>
<p><strong>Критерии выбора</strong>: чек-лист перед тем, как «воткнуть»</p>
<ol><li>Решает ли он КОНКРЕТНУЮ проблему из моего дизайна? Не «как бы нам использовать Redis», а «нам нужно хранить сессию пользователя с TTL 15 минут».</li><li>Соответствует ли он «бюджету боли» по сопровождению? Есть ли в команде экспертиза? Есть ли готовые Terraform-модули, Helm-чарты, дашборды в Grafana?</li><li>Что происходит при его отказе? Это SPOF? Как он интегрируется в нашу обратную связь (circuit breakers, fallbacks)?</li><li>Как он себя ведёт в наших паттернах доступа? Под нагрузкой, в наших хвостах распределения (p99, p999)?</li><li>Насколько он нас загоняет в угол? Легко ли будет заменить его через год, если требования изменятся кардинально?</li></ol>
<p><strong>Что в итоге? </strong></p>
<p>_Сначала_: принципиальное решение (например, «нам нужен персистентный лог событий»). _Потом_: варианты (Kafka, NATS JetStream, Pulsar, RabbitMQ с quorum queues). _Затем_: проверка по чек-листу против ТВОИХ требований и контекста (нагрузка, экспертиза, инфраструктура). _В финале_: конкретный инструмент.</p>
<p>Правильно выбранный инструмент тот, что тихо растворяется в архитектуре, позволяя системе работать так, как ты задумал. Не тот, что требует к себе постоянного внимания и героических усилий.</p>
<p>Архитектура рождается из требований и компромиссов, а не из списка технологий. Инструмент это всего лишь наиболее точная реализация твоего решения на данном этапе. Лучше взять простое и знакомое решение. Это лучше, чем выбирать модное без понимания, зачем.</p>
<p>А что дальше? Дальше код, деплой, метрики и... встреча с суровой реальностью. А почему это ещё не конец, я расскажу в следующий раз.</p>
<p>Читайте книги, пишите код, кушайте кашу.</p>
<p><strong>Что ещё почитать:</strong></p>
<ul><li><a href="https://dbdb.io/" target="_blank" rel="noopener noreferrer">Database of Databases</a> — поможет с выбором бд</li><li><a href="https://www.thoughtworks.com/radar" target="_blank" rel="noopener noreferrer">Martin Fowler — Technology Radar </a>— хороший источник для размышлений о зрелости технологий.</li><li><a href="https://t.me/zero_deps/82" target="_blank" rel="noopener noreferrer">4. System design. Обратная связь. Если бы мы знали, что это такое, но мы не знаем, что это такое</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>System design 4: обратная связь</title>
      <link>https://zerodeps.tech/texts/system-design-feedback-loops</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/system-design-feedback-loops</guid>
      <pubDate>Thu, 25 Dec 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Обратная связь позволяет системе замечать отклонение и менять поведение до полного отказа: ограничивать вход, снижать качество необязательной обработки, изолировать медленные зависимости и отдавать приоритет критичному. Один rate limit не решает задачу — реакции нужны на записи, чтении, обработке и внешних вызовах.</description>
      <content:encoded><![CDATA[<p><strong>4. System design. Обратная связь. Если бы мы знали, что это такое, но мы не знаем, что это такое</strong></p>
<p>В прошлый раз я говорил про потоки данных. Про то, как данные движутся по системе, про контуры записи, чтения и обработки. Про то, что архитектура должна строиться вокруг движения данных, а не вокруг технологий, трендовых инструментов и “стандартов индустрии”.</p>
<p>Так вот, есть один момент, который я тогда только упомянул вскользь, а он на самом деле очень важный. Поток данных может выйти из-под контроля. Может прийти больше, чем система способна переварить. Может появиться всплеск нагрузки, который сломает всё к чертям. Может упасть внешняя зависимость, и система начнёт помирать, пытаясь дождаться ответа, которого никогда не будет.</p>
<p>В такие моменты система должна как-то реагировать. Непросто падать с ошибкой 503 и отправлять всех в jopa. А делать что-то умное. Что-то, что позволит ей выжить, пусть и в урезанном виде.</p>
<p>На помощь нам придёт “обратная связь”. Если бы мы знали, что это такое, но мы не знаем, что это такое. Шутка, конечно. На самом деле знаем, но не помним. Штука очень полезная, про неё забывают на этапе проектирования и потом напихивают в разные места, затыкая дыры. Не надо так.</p>
<p><strong>Что же такое обратная связь? </strong></p>
<p>Обратная связь это когда система реагирует на изменения в окружающей среде и корректирует своё поведение. Звучит просто. И на практике просто, если знать, что делать. Если не знать - сложно. Такая простая штука может значительно влиять на жизнеспособность системы. Без неё система работает ровно до первого неожиданного события, а потом падает и всё: стресс, убытки, всё лежит, все грустят.</p>
<p>Представь термостат. Он измеряет температуру и включает или выключает обогрев, чтобы поддерживать заданную температуру. Это классический пример обратной связи: система реагирует на изменения и корректирует своё поведение. Без этого механизма температура либо постоянно росла бы, либо падала, пока всё не сломалось. В программных системах то же самое. Только вместо температуры у нас нагрузка, задержки, ошибки, доступность ресурсов. И система должна уметь реагировать на эти изменения.</p>
<p>Без обратной связи система становится хрупкой. Она работает ровно до тех пор, пока всё идёт по плану. Как только появляется что-то неожиданное пик нагрузки, сбой компонента, изменение требований система ложится. Потому что у неё нет механизма, который бы сказал: “Оу-оу, полегче, я такое не умею, я такое не могу и вообще мне тяжко”. По-хорошему нужно систему научить на такое реагировать. Для этого нужно как минимум понимать потоки данных в системе.</p>
<p><strong>Есть несколько типов обратной связи: </strong></p>
<p>Отрицательная обратная связь это когда система компенсирует отклонения и возвращает состояние к норме. Это стабилизация. Rate limit, который замедляет запросы при перегрузке. Circuit breaker, который отключает неработающий сервис, чтобы не тратить ресурсы впустую. Кеш, который отдаёт устаревшие данные, когда свежие недоступны. Всё это примеры отрицательной обратной связи: система реагирует на проблему и пытается вернуть всё в рабочее состояние. Это то, что тебе нужно в 99% случаев.</p>
<p>Положительная обратная связь это когда система усиливает отклонения. Звучит опасненько. Каскадные падения сервисов, когда один упал, нагрузка перераспределилась, другие не выдержали и тоже упали. Runaway-процессы, которые жрут всё больше ресурсов, пока система не умрёт. Но иногда положительная обратная связь нужна: например, когда система должна быстро масштабироваться при росте нагрузки. Главное контролировать этот процесс.</p>
<p>Backpressure это сигнал “стоп, хватит”. Когда вход растёт быстрее, чем система может переварить, она должна как-то сообщить источнику: “Полегче, я не успеваю”. Это может быть явный отказ в обработке, замедление ответов или очередь, прости господи, которая переполняется и начинает отклонять новые задачи.</p>
<p>Управляемая деградация это когда система жертвует частью функциональности, но остаётся работоспособной. Вместо того чтобы упасть целиком, система отключает несущественные фичи, упрощает обработку, отдаёт менее свежие данные. Ядро системы продолжает работать и предоставлять основной функционал. Это не идеальное решение, но лучше, чем полный отказ. Пользователи могут не увидеть свежие рекомендации или точную статистику, но смогут прочитать ленту и создать пост, например.</p>
<p>Все эти механизмы работают вместе, дополняя друг друга, создавая систему, которая может адаптироваться к изменениям. Backpressure защищает от перегрузки, управляемая деградация позволяет продолжать работать при проблемах, отрицательная обратная связь стабилизирует систему, положительная (когда нужна) позволяет быстро масштабироваться.</p>
<p><strong>Где обратная связь нужна в архитектуре? </strong></p>
<p>Обратная связь должна быть на всех уровнях системы. Не только в одном месте, но и везде, где система взаимодействует с внешним миром или внутренними компонентами. Классическая ошибка: добавили rate limit на входе и радуемся, что всё готово. А потом система падает из-за проблем с чтением или фоновыми задачами. “Как так? Я же защитил вход!”, ага, да, но не учёл весь поток данных.</p>
<p>На уровне контура записи обратная связь защищает систему от перегрузки. Rate limit ограничивает количество запросов в единицу времени. Throttling замедляет обработку, когда система перегружена. Валидация отклоняет некорректные данные до того, как они попадут в систему и начнут создавать проблемы. Без этого контур записи может стать точкой отказа: слишком много данных, система не справляется, все грустят. Классическая история: кто-то решил загрузить миллион записей через API, система пытается всё обработать, база задыхается, всё падает, DDoS. А могло бы быть просто: 429 “Слишком много запросов, попробуйте позже”.</p>
<p>На уровне контура чтения обратная связь обеспечивает доступность данных даже при проблемах. Кеширование позволяет отдавать данные быстро, даже если источник медленный. Fallback на устаревшие данные даёт возможность показать что-то пользователю, когда свежие данные недоступны. Read replicas распределяют нагрузку чтения, не перегружая основную базу. Без этого каждое чтение становится зависимостью от работоспособности всех компонентов, и любая проблема превращается в полный отказ. Например, система, где каждое чтение идёт в основную базу: когда база тормозит, всё падает периодически, и юзер видит йух. А могло бы быть: читаем из реплики, если реплика недоступна из кеша, если кеш пустой из основной базы, но с таймаутом. И система продолжает работать. Да, кода раза в два больше, зато надёжненько.</p>
<p>На уровне контура обработки обратная связь управляет приоритетами и ресурсами. Приоритизация задач позволяет обрабатывать важное в первую очередь, откладывая менее критичное. Отмена долгих операций освобождает ресурсы, когда система перегружена. Изоляция фоновых задач не даёт им мешать пользовательским запросам. Без контроля тяжёлые операции могут заблокировать всю систему, и пользователи перестанут получать ответы. Классика: фоновый джоб по пересчёту статистики запустился в пик нагрузки, сожрал CPU и память, юзеры получают отказ в обслуживании, система прилегла. А могло бы быть так: фоновые задачи останавливаются при высокой нагрузке, возобновляются, когда нагрузка спадает.</p>
<p>На уровне инфраструктуры обратная связь обеспечивает масштабирование и отказоустойчивость. Автоскейлинг добавляет ресурсы при росте нагрузки и убирает при снижении. Circuit breakers отключают неработающие зависимости, чтобы они не тянули систему вниз. Health checks обнаруживают проблемы до того, как они станут критичными. Это позволяет системе адаптироваться, реагировать на сбои и нагрузку. Например, система курильщика, где при падении одного сервиса все остальные продолжают пытаться его вызвать, накапливают таймауты, система деградирует. А в системе здорового человека: circuit breaker глушит упавший сервис, система продолжает работать без этого сервиса, периодически проверяет, не восстановился ли он.</p>
<p>Важно понимать: обратная связь на одном уровне не решает проблему полностью. Нужна система обратных связей, которая работает на всех уровнях одновременно. Иначе получится ситуация, когда ты защитил вход, но система падает из-за проблем с чтением. Или наоборот. Rate limit на входе бесполезен, если чтение из базы блокирует всю систему. Кеширование не поможет, если фоновые задачи жрут все ресурсы. Circuit breaker не спасёт, если проблема внутри, а не во внешних зависимостях.</p>
<p>Поэтому проектировать обратную связь нужно системно. Не “добавим rate limit и всё”, а “посмотрим, где система может сломаться, как идут потоки данных, выявим уязвимые места и добавим обратную связь там, где нужно”. Это требует понимания потоков данных, о которых мы говорили в прошлый раз. Ты можешь добавить обратную связь везде, но это будет оверинжиниринг. Или можешь добавить только в одном месте, но этого будет недостаточно. Нужно найти баланс.</p>
<p><strong>Пример </strong></p>
<p>Давай посмотрим, как это работает на практике. Представь бота, который торгует на нескольких криптобиржах одновременно: Binance, OKX, Bybit. Нормальное состояние: WebSocket-потоки идут стабильно, ордера исполняются, задержки в пределах нормы. Но вот происходит что-то: новость про регуляцию, Трамп что-то сказал, или Хейс чихнул, или все вдруг решили поторговать. Классическая ситуация: всё было хорошо и вдруг всё плохо. Наверное, ты даже где-то такое видел?</p>
<p>Без обратной связи система пытается обработать всё. Каждое обновление из WS обрабатывается, каждый тик анализируется, каждый ордер отправляется на биржу. Потоки начинают душить систему, очередь сообщений растёт, задержки увеличиваются с миллисекунд до секунд. Система начинает принимать плохие решения, потому что цены меняются быстрее, чем система успевает на них реагировать. А потом система вообще ложится. Какой-то сервис положил, допустим, базу или распределённый кеш, откуда читает контур принятия решений. В тг к тебе уже стучатся: “Бот лёг? Там движение! Ордеров нет!” А ты в душе не представляешь, почему оно там лежит и что вообще происходит.</p>
<p><em>Теперь добавим обратную связь на разных уровнях. </em></p>
<p>На контуре записи (WS-потоки) ставим backpressure. Когда система не успевает обрабатывать обновления из WebSocket, она сигнализирует: “Падажди, я не успеваю”. Это может быть явный отказ принимать новые сообщения или замедление обработки. Но важно не переборщить: слишком агрессивный backpressure заставит тебя пропустить важные обновления цены, слишком мягкий не поможет при реальном всплеске. Нужно найти золотую середину. Стоит начать с консервативных значений и вручную, по метрикам, подгонять при необходимости.</p>
<p>На контуре чтения (обработка котировок) включаем приоритизацию. Критичные пары BTC/USDT, ETH/USDT обрабатываются в первую очередь, менее ликвидные потом. Если система перегружена, она может временно отключить обработку некоторых пар, сосредоточившись на самых важных. Это лучше, чем пытаться обработать всё и упасть.</p>
<p>На контуре обработки (торговая логика) приоритизируем ордера. Критичные ордера те, что должны исполниться немедленно, например, обрабатываются в первую очередь. Менее критичные ордера откладываются, когда система перегружена. Если нагрузка критическая, система может временно отключить некоторые стратегии, освобождая ресурсы для самых важных.</p>
<p>Для внешних зависимостей бирж, API-провайдеров, ораклов организуем circuit breakers. Если биржа не отвечает или отвечает слишком медленно, circuit breaker “разрывает цепь” и перестаёт отправлять ордера на эту биржу на некоторое время. Система продолжает торговать на других биржах, но не падает из-за проблем с одной биржей. Пример ошибки: бот отправляет ордер на Binance, тот не отвечает (rate limit или просто проблемы с сетью), бот ждёт таймаут, накапливает ордера, падает. А могло бы быть: circuit breaker разрывает цепь, бот продолжает работать на других биржах, периодически проверяет, не восстановился ли Binance.</p>
<p>Всё это вместе создаёт систему, которая может деградировать управляемо. При всплеске волатильности она не падает, а упрощается: обрабатывает только критичные пары, отключает менее важные стратегии, перестаёт торговать на проблемных биржах. Система может заработать меньше, но не потеряет всё из-за полного отказа.</p>
<p>Попробую на цифрах показать. Допустим, обычная нагрузка: 10 тысяч обновлений в секунду от всех бирж. Система справляется, все обновления обрабатываются за 10–50 миллисекунд. Внезапно приходит 50 тысяч обновлений в секунду: дамп, все начинают торговать одновременно, биржи заливают обновлениями. Без обратной связи очередь сообщений растёт, задержки увеличиваются до секунд, система начинает терять деньги, потому что цены меняются быстрее, чем система успевает на них реагировать. А потом система вообще падает.</p>
<p>С обратной связью картина другая. Backpressure на WS-потоках пропускает только 20 тысяч обновлений в секунду, остальные отбрасываются. Из этих 20 тысяч система обрабатывает только критичные пары: BTC/USDT, ETH/USDT; остальные временно игнорируются. Circuit breaker отслеживает задержки на биржах: если задержка превышает допустимый порог (допустим, 500 миллисекунд), он перестаёт отправлять ордера на эту биржу. Менее критичные стратегии останавливаются, освобождая CPU и память для самых важных.</p>
<p>Результат: система продолжает работать. Часть пар не обрабатывается, часть стратегий отключена, но ядро системы стабильно. Система может заработать меньше, но не потеряет всё из-за полного отказа. Когда волатильность спадает, всё возвращается в норму: circuit breaker возвращает в работу биржи, стратегии возобновляются, обработка всех пар восстанавливается.</p>
<p>Важно понимать: это неидеальное решение. Идеального не бывает. Но это управляемая деградация вместо полного отказа. С точки зрения пользователя падение прибыли значительно лучше, чем полный слив.</p>
<p><strong>Приведу несколько типичных ошибок. </strong></p>
<p>Типичная ошибка, игнорирование обратной связи вообще. “У нас же монга с кафкой, они всё выдержат”. Нет, не выдержат. Рано или поздно нагрузка превысит возможности, и система упадёт. Обратная связь не опциональная фича для highload-систем. Это необходимость для любой системы, которая хочет выжить.</p>
<p>Другая ошибка, неправильная настройка. Rate limit на 1000 запросов в секунду, когда система реально может обработать только 100. Или circuit breaker, который срабатывает после одной ошибки и блокирует сервис на час. Или кеш с TTL в секунду, который не даёт никакого эффекта. Обратная связь должна быть настроена под реальные возможности системы и требования бизнеса. Иначе она либо не поможет, либо навредит.</p>
<p>Третья ошибка, отсутствие мониторинга. Если ты не видишь, когда и как срабатывает обратная связь, ты не можешь понять, работает ли она правильно. Сколько запросов отклоняется rate limiterом? Как часто circuit breaker банит ресурс? Насколько часто пользователи видят устаревшие данные из кеша? Какой вообще hit rate по кешу? Без метрик обратная связь становится чёрным ящиком, и ты не знаешь, помогает она или мешает.</p>
<p>Четвёртая ошибка, обратная связь только на одном уровне. Защитил вход rate limiterом, но забыл про чтение. Или настроил кеширование, но не подумал про фоновые задачи. Система всё равно падает, просто в другом месте. Нужна комплексная система обратных связей, которая покрывает все уровни архитектуры.</p>
<p><strong>Что в итоге? </strong></p>
<p>Обратная связь очень полезная и нужная штука. Это практический механизм, без которого система не может адаптироваться к изменениям и выживать.</p>
<p>Без обратной связи система работает ровно до первого неожиданного события: пик нагрузки, сбой компонента, изменение требований и всё ломается. С обратной связью система может реагировать, адаптироваться, деградировать управляемо, но продолжать работать.</p>
<p>Мы уже прошли путь от сбора требований через понимание компромиссов и потоков данных к обратной связи. Это логичная последовательность: сначала понимаешь, что нужно системе, потом выбираешь компромиссы, потом видишь, как данные движутся, и, наконец, настраиваешь реакцию системы на изменения.</p>
<p>Обратная связь связывает всё вместе. Она превращает статичную схему в живую систему, которая может реагировать на реальность. Без неё даже самая красивая архитектура останется просто набором компонентов, которые красиво выглядят на диаграмме, но не работают в проде.</p>
<p>Поэтому когда проектируешь систему, думай не только про компоненты и связи между ними. Думай про то, как система будет реагировать, когда что-то пойдёт не так. Потому что что-то обязательно пойдёт не так. И лучше быть к этому готовым.</p>
<p>Простая система с правильной обратной связью лучше сложной системы без неё. Потому что первая продолжит работать, когда вторая упадёт.</p>
<p>И в этом вся суть system design: не нарисовать красивую схему, а построить систему, которая выживет.</p>
<p>Увидимся в Новом году. Кушайте салатики и читайте книги. Хейтеры, люблю вас!</p>
<p><strong>Что ещё почитать:</strong></p>
<ul><li><a href="https://t.me/zero_deps/56" target="_blank" rel="noopener noreferrer">0. System design – это тебе не квадратики рисовать</a></li><li><a href="https://t.me/zero_deps/72" target="_blank" rel="noopener noreferrer">3. System design. Потоки …, гхм, данных </a>- предыдущая статья про потоки</li><li><a href="https://www.oreilly.com/library/view/release-it/9781680500264/" target="_blank" rel="noopener noreferrer">Release It!</a> Майкл Найгард про проектирование production-ready систем</li><li><a href="https://martinfowler.com/bliki/CircuitBreaker.html" target="_blank" rel="noopener noreferrer">The Circuit Breaker Pattern</a> Фаулер про circuit breakers</li></ul>]]></content:encoded>
    </item>
    <item>
      <title>System design 3: проектируй потоки данных</title>
      <link>https://zerodeps.tech/texts/system-design-data-flows</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/system-design-data-flows</guid>
      <pubDate>Sun, 23 Nov 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Система работает не набором компонентов, а движением данных через контуры записи, чтения и обработки. Если не описать скорость входа, точки накопления, границы истины, backpressure и конкуренцию за ресурсы, красивая схема не покажет будущие задержки и каскадную деградацию.</description>
      <content:encoded><![CDATA[<p><strong>3. System design. Потоки …, гхм, данных</strong></p>
<p>_Штош. Тут должен быть мем с котом, но его не будет. Немного выбило меня из ритма повествования. Можно меня поздравить, я теперь совсем безработный, буду чилить и возможно даже больше писать &quot;умных мюслей&quot;. А может, и не буду, а может, завтра уже буду работать в новом месте, а может, и нет. Посмотримс_</p>
<p>Ну ладно, минутка офтопа закончилась. Вернёмся к system design.</p>
<p>В прошлый раз я закончил статью анонсом большого практического материала. Хотел на практике показать, как получается архитектура на основании собранных требований. Материал написал, получилось много, сложно и как-то скомкано. Не получилось впихнуть невпихуемое в рамки одной статьи. Поэтому переобуваюсь в прыжке и предлагаю тебе продолжить знакомится с важными элементами system design.</p>
<p>На этот раз расскажу тебе про &quot;Потоки данных&quot;, зачем стоит заранее подумать, куда и как текут данные, почему это знание значительно увеличит шансы на выживание проекта, и как не получить _потоки говны_ вместо данных.</p>
<p><strong>Что за потоки и откуда и куда текут?</strong></p>
<p>Есть одна особенность, которую я замечаю у разработчиков, у тех, кто хочет попробовать в архитектуру. Они все думают о системе как о наборе компонентов. Сервис А общается с Сервисом Б. Коллекция users связана с коллекцией balances. Тут мы очередь вкрячим, тут кеш бросим, и мониторинг сверху прикрутим, и чтоб графики красивые. Получается схема, которую можно показать коллегам и сказать: &quot;_Смотрите, у меня современная, красивая архитектура!_&quot;</p>
<p>Проблема в том, что такая схема не отражает жизнь системы. Она статична. Её можно повесить на стену, но она ничего не скажет о том, почему система начинает деградировать, где начинается нагрузка и что именно может сломаться.</p>
<p>Все забывают, что единственная настоящая работа системы, это преобразование данных из одного состояния в другое. Нет данных, нет и системы. И данные, в отличие от компонента на диаграмме, не стоят на месте. Их может быть много или мало, но в любом случае они как-то попадают в систему, движутся внутри неё, где-то оседают и в какой-то момент исчезают.</p>
<p>Это непрерывный поток. Можно сравнить с рекой или ручьём. В одном месте тихое спокойное течение, в другом месте пороги, стремительный бурный поток и водовороты, где-то разливы, а иногда это всё может превратиться в говнотечь, если вовремя не подумать о берегах.</p>
<p>Поэтому, когда я говорю &quot;потоки данных&quot;, я вообще не говорю про Kafka streams, SSE, event sourcing или очередной модный паттерн. Я говорю про базовую механику системы. О том, что данные всегда в движении. И что именно это движение диктует архитектуру куда сильнее, чем выбранная база, фреймворк или микросервисная топология.</p>
<p>По опыту, большинство архитектурных ошибок появляются не из-за того, что выбрали «не ту БД» или «не тот язык». Ошибки появляются из-за того, что никто не задался вопросом: куда эти данные идут и что с ними происходит по пути.</p>
<p>Вот делаешь ты API, которое принимает пост. А дальше что? Он превращается в одну запись? В несколько? Нужно ли рассылать дальше? А подтверждение нужно? Кешировать до преобразования или после? Кто читает? Как часто? Как долго данные остаются актуальными? Как они валидируются?</p>
<p>На большую часть вопросов отвечают требования, для этого ты их и собирал. Важно формализовать поток. Но если ты не можешь ответить на эти вопросы, дальше нет смысла рисовать красивые куберы, очереди (прости господи), базы и реплики. Сколько ни старайся, данные всё равно потекут так, как им положено по природе процесса, а не по твоей диаграмме.</p>
<p>Важно понимать, что данные это не сущности. Данные это движение сущностей.</p>
<p>Пост, пользователь, лайк это не просто строка в таблице или документ в коллекции. Это срез конкретного состояния. Поток данных это не про срез. Это про историю изменений. Не «что лежит», а «что происходило».</p>
<p>А вот понять &quot;что же там происходило&quot;, всегда сложнее, чем кажется.</p>
<p>Когда в систему попадает новый пост, это не просто &quot;запись в бд&quot;, а момент, когда поток данных смещает нагрузку и меняет контекст.</p>
<p>Чтение поста тоже не про обычный findOne или SELECT. Это ожидание того, что кто-то подготовил данные для чтения, желательно быстрого. Если нет подготовки, значит, система может отдавать данные с задержкой и тормозить поток.</p>
<p>Удаление поста требует, чтобы все связанные данные были стёрты. Пропустил и вот данные уже не консистентны, могут быть ошибки.</p>
<p>Лайк поста это про контроль хаоса, два пользователя пытаются изменить одно и то же. Если не учесть движение потока, гонки могут стать нормой.</p>
<p>Лента вообще живёт, только благодаря предварительной сборке. Кеш, заранее рассчитанный срез, и у системы есть шансы уложиться в SLA.</p>
<p>Всё это не просто функции, это узлы движения данных. Сервис это место, где поток проходит. База это место, где поток оседает. Очередь это место, где поток задерживается.</p>
<p>На этапе проектирования важно видеть всю траекторию движения данных, чтобы не превратить архитектуру в набор случайных, но красивых решений. Даже при выборе правильных инструментов, база, кеш, протокол, если, не учтён поток данных, система начнёт деградировать со старта. Задержки будут расти, хвосты копится, воркеры будут душить CPU, p99 расти.</p>
<p><strong>А что делать-то?</strong></p>
<p>Когда начинаешь смотреть на систему как на средство управления потоком, а не как на набор красивых квадратиков, внезапно становится ясно: Архитектура то, не про сервисы. Она про движение. Сервисы, базы, очереди всего лишь инструменты управления потоком, через которые поток проходит, оседает, задерживается. Важная для понимания механика. Без этого знания будет сложно построить жизнеспособную систему.</p>
<p>Первое, что приходится принять: <strong>Поток всегда в движении</strong>. Независимо от того, понимаешь ты его или нет. Можешь хотеть идеальный write-API, предсказуемый кеш или волшебный микросервис, который решит все проблемы. Но данные будут идти по своему маршруту, диктуемому задачей, временем, нагрузкой и законами природы. Система будет вести себя так, как движутся данные, а не так, как ты её нарисовал.</p>
<p>Чтобы совладать с потоком, его нужно разложить на три составляющие части: <strong>контур записи, контур чтения и контур обработки данных</strong>. Каждая часть со своей динамикой, рисками и болевыми точками.</p>
<p><strong>Часть первая: Контур записи</strong></p>
<p>Это write-path. Здесь важно всё: порядок, момент фиксации истины, нагрузка, валидность, скорость реакции. Любая ошибка здесь, как трещина в фундаменте. Ты можешь её сразу не увидеть, но она обязательно проявится позже. Клеппман в DDIA хорошо пишет про то, что write-path это та зона, где принимаются необратимые решения. Если вход нестабилен, никакие кеши и шарды дальше этого не исправят. Вход обязан быть предсказуемым, иначе поток начинает рвать систему в самых неожиданных местах.</p>
<p><strong>Часть вторая: Контур чтения </strong></p>
<p>Вроде как всё просто: достал данные и отдал. Но по опыту, всё далеко не так. В реальности чтение это доминирующий поток. Пользователи читают в десятки раз чаще, чем пишут. И этот поток агрессивный: ему нужна скорость, консистентность в рамках контракта и минимальная логика. Если читать «как есть», напрямую из структуры записи, ничего не получится, дорого, долго, тяжело. Read-path требует данных, подготовленных заранее. Денормализация, кеш, заранее собранные агрегаты, это не оптимизация, это само условие того, что система вообще будет отвечать. Фаулер много писал про этот конфликт: чтение нельзя обслуживать теми же структурами, что запись, если ты претендуешь на SLA, а не на стартап-демку.</p>
<p><strong>Часть третья: Контур обработки данных</strong></p>
<p>Тут вообще обычно проходят мимо, забывают, что, данные в системе преобразуются, обрабатываются, меняются. А потом, ой, кто-то где-то задушил CPU и IO. И начинают поиск обычно с входа, контура записи. А там всё красиво и гладко, не падает, не тротлит. А ведь именно тут происходят самые тяжёлые операции: пересборка агрегатов, обновление статистик, пересчёт связей, материализация, очистка, TTL, миграция, индексация, репликация и много чего ещё. Это тот самый тихий поток, который незаметно работает сутками, пока однажды не попадает в неудачное окно и не начинает давить всю систему. Иногда достаточно одного крупного пересчёта, который внезапно совпал по времени с пользовательским пиком, чтобы p99 ушёл в стратосферу, а в логах появилось много нового и непечатного.</p>
<p>Поток данных порождает сложность, а система, в свою очередь, должна иметь внутренние механизмы, способные эту сложность компенсировать. Ага, да, мы опять пришли к закону Эшби про необходимое разнообразие. База она такая.</p>
<p>Если write-path, read-path и фон неразделены, если у них нет изоляции, буферов, независимых темпов, система становится хрупкой. Это и есть тот момент, когда малейший всплеск нагрузки превращается в неконтролируемую деградацию, а простая операция начинает складывать половину инфраструктуры.</p>
<p>Когда разделение потоков становится очевидным, архитектура начинает выстраиваться сама. Нужна быстрая лента, формируем представление данных заранее. Нужна консистентная запись, уделяем внимание входу, а не тому, как красиво выглядит микросервисная схема. Нужны тяжёлые отчёты, их место в фоне, в собственной вселенной, а не рядом с пользовательскими запросами. Нужны жёсткие SLA, чтение и запись никогда не должны жить в одной структуре данных.</p>
<p>И вот мысль, которую я хотел донести: Многие архитектурные решения не про &quot;инструменты&quot;. Они про управление потоком данных. Меняешь форму данных, меняешь поток. Меняешь порядок событий, меняется поток. Убираешь в фон тяжёлые операции, подальше от hot path, меняешь поток. Всё сводится к управлению потоком. Архитектура вырастает из движения, а не наоборот.</p>
<p>Поэтому отвечая на вопрос «_что делать-то?_», ответ простой: <strong>Сначала понять свой поток данных, потом разделить его, затем разобраться, как система должна на этот поток реагировать. И только после этого, выбирать инструменты и рисовать стрелочки с квадратиками.</strong></p>
<p>Только в таком порядке. Иначе получится красивая схема и поток говны по ней.</p>
<p><strong>Пример. Своя CDN на монге и немного боли</strong></p>
<p>Представь, что ты делаешь свою «бедную» CDN. Ничего космического, просто раздача статики: картинки, js, css. Пара edge-нод, один origin, всё это крутится рядом с основным приложением. Хочется контролировать версии, уметь быстро откатываться, иногда включать разные варианты для разных клиентов. И вот тут кто-то говорит знакомое: «_давайте всё положим в монгу, сто раз так делали_».</p>
<p>Схема рождается быстро. Файлы лежат где-то на диске или в s3, а в Mongo хранятся метаданные: путь, версия, флаги, список разрешённых клиентов, настройки кеша. Приходит запрос на статику, CDN-нода идёт в Mongo, достаёт документ, понимает, какую версию отдавать, смотрит, не выключен ли ресурс, и уже потом лезет в хранилище. Красиво, гибко, всё динамически настраивается.</p>
<p>На бумаге это выглядит даже красиво. На практике, ну такое.</p>
<p>Контур записи ещё терпимый. Разработчик выкатывает новую версию фронта, сервис релизов пишет пачку документов в Mongo, отмечает новую версию как активную, старую как «archived». Поток данных на входе относительно небольшой, немного всплесков, но монга это переваривает. Все довольны, все ходят и рассказывают, что у них «динамическая CDN с конфигом в базе».</p>
<p>Проблемы вылезают в контуре чтения. Любой запрос на статический ресурс начинает с чтения из Mongo. Даже если у тебя есть локальный кеш файлов на edge-ноде, ты всё равно сначала лезешь в базу: вдруг версию поменяли, вдруг ресурс выключили, вдруг флаг изменился. Пока аудитория маленькая, всё живёт. Как только прилетает бОльшая нагрузка, чтение превращается в поток запросов к Mongo, который, вообще-то, никто не проектировал под роль горячего конфига для CDN.</p>
<p>Монга начинает задыхаться. Планировщик запросов крутит лишние индексы, дисковые операции растут, p95 вылазит за пределы того, что фронт таскает без истерики. Ты добавляешь кэширование на уровне приложения, начинаешь держать конфиг в памяти, придумываешь инвалидацию. Поток чтения уже не выглядит простым: часть идёт в монгу, часть в локальный кеш, часть в соседний узел. И всё это держится на соплях и надежде, что где-то не забыли про invalidate.</p>
<p>А теперь подключается контур обработки.</p>
<p>Бизнес хочет удалять старые версии, чистить неиспользуемую статику, пересобирать манифесты. Пишешь фоновые задачи, которые ночами пробегают по Mongo, ищут старые документы, пересчитывают ссылки, сносят мусор. В какой-то момент этих задач становится много, они начинают работать не только ночью, но и «по расписанию» и «по кнопке». И вот уже поток фоновой обработки лезет в те же коллекции, что и горячее чтение CDN.</p>
<p>Получается красивая каша. Вход пишет новые версии. Чтение при каждом запросе лезет в Mongo, чтобы понять, что отдавать. Фон пересобирает всё это дело, чистит и переиндексирует. Контуры неразделены, темпы у всех разные, буферов почти нет. Достаточно одного неудачного совпадения: деплой, пара «ручных» скриптов по миграции метаданных и пик трафика. Итог предсказуем. Edge-ноды начинают встать на блокировках к монге, CDN «внезапно» перестаёт быть CDN и превращается в дорогой прокси к базе.</p>
<p>Если на это смотреть через призму потоков, картина другая.</p>
<p>На записи тебе вообще не нужна Mongo на hot-path. Нужно один раз зафиксировать факт: «появилась новая версия набора файлов с таким-то идентификатором». Этого достаточно. Подробный конфиг можно материализовать отдельно. В обработке ты спокойно пересобираешь манифесты, генерируешь простой, плоский формат настроек для каждой edge-ноды, кладёшь его в отдельную коллекцию или вообще в файловый снапшот. А контур чтения должен жить своей жизнью: брать уже готовый, прогретый конфиг целиком в память и не ходить в mongo на каждый чих.</p>
<p>В таком варианте поток записи остаётся тонким: немного изменений метаданных. Поток обработки тяжёлый, но изолированный: он пересобирает конфиги, не трогая горячие запросы. Поток чтения максимально простой: один раз в несколько секунд или минут нода подхватывает свежий снапшот конфига, а все пользовательские запросы работают по нему в памяти, не трогая ни Mongo, ни файловое хранилище лишний раз.</p>
<p>Снаружи два решения могут выглядеть похоже: «у нас CDN, конфиг в монге, всё динамическое». Но одно построено как «каждый запрос это маленькое приключение в базу», а второе уже как нормальная работа с потоком: истина пишется отдельно, конфиг готовится отдельно, чтение живёт на готовых срезах.</p>
<p>Формально те же компоненты, та же монга, те же edge-ноды. А по факту в одном случае у тебя постоянная война с пиками, а в другом система, которая хотя бы не стреляет себе в ногу каждый раз, когда кто-то открыл главную страницу.</p>
<p><strong>Что в итоге?</strong></p>
<p>Если смотреть на систему честно, становится видно: она держится не на компонентах, а на движении данных. Пока ты рассматриваешь архитектуру как набор сервисов, все решения остаются косметикой. Поток данных идёт как шёл, и именно он определяет, где появится задержка, где начнёт жрать CPU, где система начнёт деградировать.</p>
<p>Когда разбиваешь поток на контуры: запись, чтение, обработку. Все проблемы приобретают форму. Становится ясно, почему запись должна быть предсказуемой, чтение быстрым, а тяжёлая логика жить отдельно. Пример с CDN на монге это хорошо показывает: одинаковые технологии снаружи дают два совершенно разных поведения в зависимости от того, управляешь ты потоком или таскаешься за ним.</p>
<p>Все архитектурные решения на самом деле про одно: про управление движением данных. Меняешь поток, меняется система. Не меняешь, она меняет тебя.</p>
<p>Поэтому порядок такой же простой, как и беспощадный: <strong>увидеть поток, разделить поток, понять реакцию системы </strong>–<strong> и только потом выбирать инструменты.</strong></p>
<p>В обратном порядке работает только красивая схема и поток говны поверх неё.</p>
<p>Следующая часть как раз будет про реакцию. Про то, что система делает, когда поток выходит из-под контроля. Про обратную связь. Про то, что это, и почему она так важна.</p>
<p>А пока совершенствуйся как специалист и никогда не останавливайся в обучении. Никогда не знаешь, где и когда, какие знания могут пригодиться.</p>
<p><strong>Что еще почитать?</strong></p>
<ul><li><a href="https://t.me/zero_deps/66" target="_blank" rel="noopener noreferrer">2. System design держится на трёх столпах</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>System design 2: три столпа архитектуры</title>
      <link>https://zerodeps.tech/texts/three-pillars-of-system-design</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/three-pillars-of-system-design</guid>
      <pubDate>Fri, 17 Oct 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Архитектура держится на надёжности, масштабируемости и сопровождаемости, но максимизировать все три одновременно нельзя. Требования задают бюджет сложности: инженер осознанно решает, где платить инфраструктурой, где принимать ограничение масштаба, а где допускать деградацию.</description>
      <content:encoded><![CDATA[<p><strong>2. System design держится на трёх столпах</strong></p>
<p>Допустим, ты успешно собрал требования, был посланы куда-нибудь пару раз, выбил из бизнеса конкретику и понял, что система должна делать и как себя вести. Теперь у тебя есть &quot;документ&quot; с цифрами, SLA, ограничениями, функциональными и нефункциональными требованиями.</p>
<p>Отлично. И вот ты смотришь на этот список, список длинный, много буков и цифров. Что дальше? Как из этого &quot;документа&quot; получить архитектуру?</p>
<p>Для начала нужно налить себе чего-нибудь, например, кофе. Закрыться у себя в кабинете, чтобы тебе никто не мог помешать. А дальше придётся вспомнить много теории и начать думать. Знаю, звучит сложно, особенно про думать, но придётся с этим как-то жить. По-другому никак.</p>
<p>А вспоминать лучше всего начать с трёх столпов любой архитектуры: надёжности, масштабируемости и сопровождаемости. И все три столпа это компромиссы. Каждый из них конфликтует с остальными. Улучшаешь один и ты убиваешь другие.</p>
<p>Не понимая этой базы, любая проектируемая система будет строиться заведомо с меньшими шансами на выживание в реальном мире. С каждым шагом ты будешь желать всё сжечь и уволиться всё больше и больше.</p>
<p>Поэтому предлагаю тебе устроиться поудобнее и почитать про базу system design.</p>
<p><strong>Что там за столпы такие?</strong></p>
<p>Начну сразу с ремарки: надёжность, масштабируемость и сопровождаемость это не красивые слова из ролика на ютубе или презентации. Это конкретные инженерные свойства системы, их можно измерить, потрогать, пощупать.</p>
<p><strong>Надёжность</strong> — это способность системы работать в условиях, когда вот-вот всё упадёт и когда уже часть упала. Не &quot;не падать никогда&quot;, а &quot;падать предсказуемо и восстанавливаться быстро&quot;. Предсказуемая деградация функциональности. Какой таргет по доступности? Что происходит при отказе компонента? Как быстро система возвращается к нормальной работе? Можно ли и какие данные терять при сбое?</p>
<p><strong>Масштабируемость</strong> — это способность системы справляться с ростом нагрузки без деградации качества. Не &quot;держать любую нагрузку&quot;, а &quot;держать запланированную нагрузку в рамках SLA&quot;. Как система ведёт себя при 2x, 10x, 100x росте трафика? Где появляются узкие места? Можно ли масштабировать по частям или только целиком?</p>
<p><strong>Сопровождаемость</strong> — это способность системы изменяться без катастрофических последствий. Не &quot;код должен быть красивым&quot;, а &quot;изменения должны быть предсказуемыми и безопасными&quot;. Сколько времени уходит на добавление фичи? Как часто изменения ломают что-то ещё? Можно ли откатить изменения? Сколько людей нужно для поддержки системы?</p>
<p>Вот эти три свойства и определяют архитектуру. Все три свойства противоречат друг другу. Постоянная борьба за баланс и компромисс.</p>
<p><strong>Почему же они конфликтуют?</strong></p>
<p>Потому что мир конечен. У тебя ограниченный бюджет, ограниченное время, ограниченные ресурсы. И каждое решение, которое улучшает одно свойство, ухудшает другое.</p>
<p>Хочешь надёжность? Добавляешь реплики, валидацию, проверки, мониторинг. Пишешь код для контролируемой деградации, отлаживаешь управляемую остановку сервисов. Добавляешь код для работы с граничными случаями. Система становится сложнее, медленнее, дороже. Появляется больше абстракций. Сопровождаемость падает. Больше кода, больше конфигов, больше точек отказа.</p>
<p>Хочешь масштабируемость? Разносишь по серверам сервисы, добавляй кеши, настраиваешь шардинг, появляется желание завозить очереди, прости господи. Пишешь больше кода для обеспечения согласованности. Думаешь в сторону микросервисов и распределенки. Система становится сложнее, появляются новые типы сбоев, консистентность данных страдает. Надёжность падает. Больше компонентов, больше сетевых вызовов, больше вероятность упасть.</p>
<p>Хочешь сопровождаемость? Упрощаешь архитектуру, минимизируешь зависимости, пишешь простой код. Но система становится менее гибкой, хуже масштабируется, сложнее оптимизируется. Думаешь в сторону монолита. Масштабируемость падает. Масштабирование по частям становится практически невозможным, нельзя использовать некоторые инструменты.</p>
<p>К сожалению, таковы законы природы. Нельзя получить всё, везде и сразу. Постоянный торг, постоянный поиск компромиссов.</p>
<p><strong>Пример</strong></p>
<p>Давай покажу примеры, как эти компромиссы работают на практике.</p>
<p><strong>Кеш vs консистентность</strong></p>
<p>Начнём с классики. Хочешь скорость? Добавляй кеш. Данные читаются из памяти, ответы прилетают быстрее. Масштабируемость растёт, нагрузка на базу падает.</p>
<p>Но что происходит с консистентностью? Данные в кеше могут быть устаревшими. Пользователь видит старую информацию. Если кеш упал, время ответа кратно возрастает. Если кеш переполнился, часть данных вытесняется.</p>
<p>Можешь добавить инвалидацию кеша, но тогда кеш становится сложнее. Можешь использовать write-through, но тогда записи становятся медленнее. Можешь использовать eventual consistency, но тогда пользователи видят разные данные в момент времени.</p>
<p>Выбор зависит от требований. Для ленты новостей eventual consistency это нормально. Для банковского баланса совершенно неприемлемо.</p>
<p><strong>Микросервисы vs простота</strong></p>
<p>Хочешь независимые релизы и масштабирование по частям? Завози микросервисы. Каждый сервис можно разрабатывать, тестировать и деплоить отдельно. Команды не мешают друг другу.</p>
<p>Но что происходит со сложностью? Появляются сетевые вызовы, нужно думать про таймауты и ретраи. Руки тянутся к очередям, сложность x10. Данные размазываются по сервисам, консистентность становится проблемой. Отладка усложняется, практически неминуемая большая связность, один запрос проходит через десяток сервисов.</p>
<p>Можешь добавить service mesh, но тогда появляется ещё один слой абстракции. Можешь использовать event sourcing, но тогда система становится ещё сложнее. Можешь минимизировать количество сервисов, но тогда теряешь преимущества микросервисов.</p>
<p>Примерно тут ты вспоминаешь про саги. Способ склеить распределённые транзакции без 2PC. Каждая операция локальна, но цепочка координируется сообщениями. Если шаг падает, запускается компенсирующее действие. В теории красиво, на практике превращается в ад отложенных событий и &quot;ручного&quot; восстановления состояния. Любая ошибка в оркестрации, и данные начинают расходиться.</p>
<p>Выбор зависит от размера команды и сложности предметной области. Для стартапа из трёх человек микросервисы это оверинжиниринг. Для команды из ста человек монолит будет якорем.</p>
<p><strong>Синхронность vs асинхронность</strong></p>
<p>Хочешь простоту и предсказуемость? Делай всё синхронно. Запрос пришёл, обработался, ответ ушёл. Легко отлаживать, легко тестировать, легко понимать.</p>
<p>А что с производительностью? Медленная операция блокирует весь запрос. Если один компонент тормозит, тормозит вся система. Масштабировать можно только вертикально.</p>
<p>Можешь добавить очереди, прости господи, но тогда появляется eventual consistency. Можешь использовать неблокирующие вызовы, но тогда код становится сложнее. Можешь разбить операцию на этапы, но тогда нужно думать про компенсацию, привет саги.</p>
<p>Выбор зависит от требований к задержкам и пропускной способности. Для систем реального времени синхронность может быть критична. Для batch-обработки асинхронность это необходимость.</p>
<p><strong>И как выбирать эти ваши компромиссы?</strong></p>
<p>Ага, примерно тут, ты достаёшь из широких штанин, толщиною с консервную банку, хм, документ. Тот, что ты собирал с боем в прошлый раз. Требования. Каждое архитектурное решение должно быть обосновано конкретной строкой или абзацем в этом документе.</p>
<p>Если бизнес кричал «нужна скорость», теперь ты смотришь какой ценой. Где они согласились терпеть боль. Медленную запись? Потерю консистентности? Сложность кода?</p>
<p>Если требовали «надёжность», ты открываешь SLA и смотришь, что на самом деле под этим имелось в виду. Сколько минут даунтайма они готовы пережить, сколько данных потерять, какие ошибки простить.</p>
<p>Если просили «простоту» ищешь, для кого именно. Для разработчиков, чтобы быстрее пилить фичи? Для пользователей, чтобы не путались в интерфейсе? Или для эксплуатации, чтобы не вылавливать зомби-процессы ночью?</p>
<p>Тут появляется концепция &quot;бюджета боли&quot;. У тебя есть ограниченный бюджет на сложность, на производительность, на надёжность. Ты не можешь получить всё сразу. Ты можешь только осознанно распределить этот бюджет.</p>
<p><strong>Что за бюджет такой?</strong></p>
<p>Представь, что у тебя есть сто деняк. Ты можешь потратить их на надёжность, на масштабируемость или на сопровождаемость. Но на всё сразу у тебя не хватает.</p>
<p>Если потратишь всё на надёжность, получишь систему, которая не падает, но тормозит и которую никто не понимает. Если потратишь всё на масштабируемость, получишь систему, которая держит нагрузку, но падает от любого чиха. Если потратишь всё на сопровождаемость, получишь систему, которую легко поддерживать, но которая не масштабируется.</p>
<p>Правильный подход потратить бюджет осознанно. Понять, что важнее для твоего конкретного случая, и вложиться в это. А в остальном принять компромиссы.</p>
<p><strong>Пример: система уведомлений</strong></p>
<p>Допустим, тебе нужно построить систему уведомлений. Бизнес говорит: &quot;Нужно отправлять уведомления пользователям. Быстро, надёжно, много.&quot;</p>
<p>Ты собираешь требования и понимаешь:</p>
<ul><li>100k пользователей, 10k уведомлений в день</li><li>Время доставки не критично, можно в течение минуты</li><li>Потеря 1% уведомлений допустима</li><li>Команда из пяти человек, бюджет ограничен</li></ul>
<p>Теперь ты можешь осознанно выбирать компромиссы:</p>
<p><strong>Надёжность vs простота</strong>: Можно сделать простую систему, один процесс, одна база, прямой вызов. Но если процесс упадёт, часть задач потеряется. Можно добавить репликацию и мониторинг, но система станет сложнее.</p>
<p><strong>Масштабируемость vs консистентность</strong>: Можно гарантировать, что каждое событие будет обработано ровно один раз, но тогда придётся решать вопросы дубликатов и идемпотентности. Можно выбрать eventual consistency, но тогда часть данных может устаревать или теряться.</p>
<p><strong>Сопровождаемость vs производительность</strong>: Можно написать простой и прозрачный код, который легко читать и менять. Но он не выдержит пиков. Можно добавить кэширование, пайплайны и оптимизации, но код станет запутаннее и более хрупким.</p>
<p>Исходя из требований, ты выбираешь: простота важнее надёжности (1% потерь допустимы), eventual consistency важнее строгой консистентности (задержка не критична), простота важнее производительности (нагрузка невысокая).</p>
<p>Получается простая система: прямые вызовы, базовая обработка ошибок, минимальный контроль состояния. Не идеально, но соответствует заявленным требованиям.</p>
<p><strong>Что в итоге</strong></p>
<p>System design это не про то, чтобы найти &quot;правильную&quot; архитектуру, тем более не про &quot;чистую&quot;. Правильной архитектуры не существует. Есть только архитектура, которая подходит под твои требования и ограничения, и даёт шанс на выживание проекта.</p>
<p>Три столпа: надёжность, масштабируемость и сопровождаемость. Три, казалось бы, простых таргета, которые есть в каждой системе, причиняют огромное количество боли. Особенно, если наступить не в тот компромисс или войти не в ту дверь.</p>
<p>Искусство настоящего архитектора - умело жонглировать этой болью, принимать решения, жертвовать, ради достижения поставленной цели.</p>
<p>Рождение архитектуры, в котором помогает system design, интересный, тяжёлый и завораживающий итеративный процесс, поучаствовать в котором я всем желаю.</p>
<p>В следующий раз попробую показать на примере, как последовательно от требований, от документа, толщиною с консервную банку, перейти к архитектурному решению с шансами на жизнь.</p>
<p>А пока читайте книги, всем добра.</p>
<p><strong>Что еще почитать?</strong></p>
<ul><li><a href="https://t.me/zero_deps/60" target="_blank" rel="noopener noreferrer">1. System design начинается с вопросов</a></li><li>[High Scalability — Real World Architectures](http://highscalability.com/) - много интересных примеров</li><li><a href="https://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener noreferrer">Martin Fowler — Microservices Trade-Offs</a> - про компромисы в микросервисах</li></ul>]]></content:encoded>
    </item>
    <item>
      <title>System design 1: начинается с вопросов</title>
      <link>https://zerodeps.tech/texts/system-design-starts-with-questions</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/system-design-starts-with-questions</guid>
      <pubDate>Sun, 12 Oct 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>System design начинается не со схемы, а с перевода бизнес-желаний в измеримые ограничения: нагрузку, задержку, доступность, допустимую потерю данных и поведение при сбое. Хорошие вопросы вскрывают противоречия заранее и дают каждому архитектурному компоненту конкретное обоснование.</description>
      <content:encoded><![CDATA[<p><strong>1. System design начинается с вопросов</strong></p>
<p>Обычное утро понедельника. Ничего не предвещало беды. Ты наливаешь кофе, открываешь бук, пуллишь проект и начинаешь читать ночные коммиты от коллег. Тишина, спокойствие, идиллия.</p>
<p>И вот он. Мерзкий, пронзающий сознание звук уведомления, от которого сводит зубы. Тебе пишет PM. Сворачиваешь проект с опаской и дурным предчувствием, открываешь диалог. А там две строчки.</p>
<p>“_Бизнес хочет ленту как у Threads. Какие сроки?_”</p>
<p>И вот тогда это происходит. Момент, когда дыхание замедляется. Ты переживаешь весь спектр эмоций. Сонм мыслей: &quot;_Какая вам… лента? Мы же сайт для учёта бобров_&quot;, &quot;_А Твиттер вам не написать?_&quot;. Мимо пролетают стадии от гнева до принятия.</p>
<p>Вдох, выдох. В такие моменты кажется, что согласиться и разбираться по ходу дела приемлемая стратегия. Но если принять такие правила игры, ошибки могут быть очень дорогими.</p>
<p>За красивой формулировкой про &quot;ленту&quot; нет конкретики. Ничего измеримого. Буквально ты не знаешь ничего.</p>
<p>Если сейчас не докопаться до сути, не собрать конкретные ожидания и измеримые показатели, дальше будет только хуже. Вместо простого проекта получится &quot;челмедведосвин&quot;. Ты не сможешь его поддерживать. Вносить изменения будет сложно, развитие станет пыткой.</p>
<p>А что ты можешь сделать? Для начала пишешь PM: “_Мне нужно собрать требования. Организуй встречу_”.</p>
<p>И начинаешь подготовку к сражению. Твоим оружием будут сотни неудобных, повторяющихся и дотошных вопросов. Твоей победой станет метаморфоза влажной фантазии бизнеса в конкретные требования к проекту.</p>
<p>Вот почему system design начинается с вопросов, а не со схем. Вопросы это первая ступень к архитектуре. Архитектуре, которая будет работать в реальном мире, а не на бумаге.</p>
<p>Какие вопросы задавать и зачем, я сейчас расскажу.</p>
<p><strong>Почему вопросы это не просто формальность</strong></p>
<p>Вся эта история с вопросами кажется банальной. Ну спросил, ну ответили, ну записал. Что тут сложного? А сложного тут всё.</p>
<p>Бизнес говорит на языке результатов. &quot;_Хотим ленту_&quot;, &quot;_нужна аналитика_&quot;, &quot;_сделайте как у конкурентов_&quot;, &quot;_сделай чтоб работало_&quot;. Это нормально, они не обязаны знать, что такое eventual consistency или почему Redis не подходит для персистентного хранения.</p>
<p>Твоя задача перевести эти хотелки в технические требования. И вот тут начинается самое интересное.</p>
<p>Каждое &quot;хочу&quot; скрывает десятки неявных предположений. Бизнес не думает о том, что лента должна работать при 10k RPS, что данные могут быть неконсистентными первые 5 секунд, что при падении сервиса пользователи должны видеть кеш, а не белую страницу.</p>
<p>Они просто хотят, чтобы &quot;_всё работало_&quot;. А что значит &quot;_работало_&quot; это уже твоя головная боль.</p>
<p>Вопросы не придирки и не попытка усложнить простую задачу. Это способ вытащить на свет все те скрытые требования, о которых бизнес даже не подозревает.</p>
<p>Без этого ты будешь строить систему вслепую. Сделаешь что-то, что технически работает, но не решает реальную проблему. Или решает, но с такими компромиссами, что через полгода придётся всё сжечь и написать заново.</p>
<p><strong>Какие вопросы задавать</strong></p>
<p>Вопросы должны вытаскивать не просто пожелания, а фундаментальные ограничения системы. Архитектура это компромиссы между противоречивыми требованиями. Твоя задача найти эти противоречия до того, как они сломают систему в проде.</p>
<p>Начни с формальных требований, которые можно измерить и потрогать. Сколько пользователей, какие запросы, как долго система может отвечать. Это основа для всех дальнейших решений. Без цифр и строгих таргетов у тебя не проект, а путь самопознания. Если не повезёт, путь закончится не продом, а дуркой.</p>
<p>Но формальные требования это только верхушка айсберга. Под ними лежат неформальные требования, те, что бизнес даже не осознаёт. Что происходит, когда система падает? Пользователи ждут или уходят? Можно ли показать устаревшие данные? Что важнее, скорость или точность?</p>
<p>Эти неформальные требования часто противоречат друг другу. Хочешь скорость, жертвуешь консистентностью. Хочешь надёжность, усложняешь систему. Хочешь простоту, ограничиваешь функциональность. Это неизбежные компромиссы. Их нельзя избежать, можно только осознанно выбирать.</p>
<p>Не существует серебряной пули. Каждое простое решение скрывает за собой сложность. Когда бизнес просит &quot;ленту как у Threads&quot;, он просит именно такую пулю, магическое решение, которое решит все проблемы без компромиссов. Твоя задача показать, что такой пули не существует, и помочь выбрать правильные компромиссы.</p>
<p>Здесь вступает в игру кибернетика. Есть закон необходимого разнообразия. Система может справиться с разнообразием внешней среды, только если у неё есть достаточное внутреннее разнообразие реакций. Проще говоря, если мир вокруг сложный, твоя система тоже должна быть сложной.</p>
<p>Но сложность это не хаос. Это структурированная сложность, где каждый элемент имеет роль и ограничения. Твои вопросы должны выявить не только что система должна делать, но и как она должна реагировать на изменения, сбои, пики нагрузки.</p>
<p>Каждое техническое противоречие можно решить, если правильно его сформулировать. Не &quot;_нужна и скорость, и надёжность_&quot;, а &quot;_скорость там, где важно не тормозить, надёжность там, где нельзя упасть_&quot;. Не &quot;_нужна и простота, и функциональность_&quot;, а &quot;_простота для тех, кто просто пользуется, функциональность для тех, кому это нужно_&quot;.</p>
<p>Твои вопросы должны вытаскивать эти противоречия на поверхность. Что важнее: время разработки или время отклика? Стоимость инфраструктуры или доступность? Простота поддержки или богатство функций?</p>
<p>Каждый ответ на твой вопрос должен приводить к архитектурному решению. Если бизнес говорит &quot;_нужна скорость_&quot;, ты спрашиваешь &quot;_Чем можно пожертвовать ради скорости?_&quot;. Если говорят &quot;_нужна надёжность_&quot;, ты спрашиваешь &quot;_Что считается сбоем?_&quot;. Если говорят &quot;_нужна простота_&quot;, ты спрашиваешь &quot;_Простота для кого? Для разработчиков? Для пользователей? Для бизнеса?_&quot;.</p>
<p>Без этих вопросов ты будешь строить систему, которая решает не ту проблему. Или решает правильную проблему, но с неправильными компромиссами. Или с правильными компромиссами, но в неправильном контексте.</p>
<p><strong>Как задавать вопросы</strong></p>
<p>Сбор требований это не одноразовая встреча. Это итеративный процесс, где каждый цикл углубляет понимание и выявляет новые противоречия. Это архитектурное мышление, способность видеть систему как набор взаимосвязанных компромиссов.</p>
<p>Есть два типа сложности: сущностная и случайная. Сущностная это то, что нельзя убрать, не изменив саму задачу. Случайная это то, что мы добавляем сами, неправильно понимая требования. Твои вопросы должны выявить сущностную и минимизировать случайную.</p>
<p>Начни с широкого контекста. Собери всех заинтересованных лиц и задавай открытые вопросы. Как сейчас работает процесс? Что больше всего бесит в текущем решении? Что будет, если ничего не менять? Цель понять общую картину, не вдаваясь в детали.</p>
<p>Записывай всё. Даже то, что кажется неважным. Потом разберёшься. Часто самые важные ограничения скрываются в мелочах, которые кажутся очевидными.</p>
<p>Теперь иди к каждому стейкхолдеру отдельно. У них разные интересы и приоритеты. Продукт думает про пользовательский опыт, бизнес про деньги, эксплуатация про стабильность. Каждый видит систему под своим углом, и твоя задача собрать эти углы в единую картину.</p>
<p>Не принимай ответы типа &quot;_ну, как обычно_&quot; или &quot;_сделайте нормально_&quot;. Докапывайся до цифр. Если говорят &quot;_быстро_&quot;, спрашивай &quot;_Что для вас значит быстро? Сколько миллисекунд, секунд?_&quot;. Если говорят &quot;_надёжно_&quot;, спрашивай &quot;_Что считать сбоем? Сколько девяток доступности? Что видит пользователь при сбое?_&quot;.</p>
<p>К этому моменту у тебя будет список требований длиной в километр. Большинство из них будут противоречить друг другу. Это нормально. Система живёт в мире ограниченных ресурсов, где нельзя получить всё сразу.</p>
<p>Время для взрослых разговоров. Что важнее: скорость или надёжность? Простота или функциональность? Бюджет или качество? Не пытайся угодить всем. Лучше честно сказать &quot;_это требование увеличит сроки в два раза_&quot; и дать бизнесу принять решение.</p>
<p>Оформи всё в документ. Не в виде списка пожеланий, а в виде конкретных, измеримых требований. &quot;_Система должна поддерживать тысячу одновременных пользователей_&quot; вместо &quot;_должна быть надёжной_&quot;. &quot;_Время ответа API не должно превышать 200 миллисекунд в 95 процентах случаев_&quot; вместо &quot;_должна работать быстро_&quot;.</p>
<p>Этот документ станет основой для всех дальнейших решений. Каждое архитектурное решение должно быть обосновано конкретным требованием из этого документа. Если не можешь объяснить, зачем нужен тот или иной компонент, значит, этот компонент лишний.</p>
<p>Посмотрим как это работает на примере.</p>
<p><strong>Cценарий 1: Сам решил</strong></p>
<p>PM приходит с задачей. &quot;_Бизнес хочет ленту новостей. Сделайте как у Threads_&quot;.</p>
<p>Ты спрашиваешь: &quot;_А что именно имеется в виду?_&quot;</p>
<p>PM отвечает: _&quot;Ну, лента. Где посты идут в хронологическом порядке, с лайками и комментариями&quot;_.</p>
<p>Вроде понятно. Ты киваешь, но в голове уже крутится: &quot;_А что, если пользователей будет много? А если нагрузка вырастет? А если понадобится поиск?_&quot;.</p>
<p>Хоронишь эти вопросы в себе и накидываешь схему. Таблицы posts, likes, comments. API для получения ленты. И вроде красиво. Микросервисы. Кафка, прости господи, чтобы общались. Valkey за кэш. Поиск на эластике. Ну и докер, кубер, графана, прометеус. Всё как у взрослых.</p>
<p>Через месяц выкатываешь на прод. Сотня пользователей заходит одновременно, и система падает.</p>
<p>Бизнес задаёт неудобные вопросы: &quot;Почему ничего не работает? Почему так медленно? У Threads же быстро работает!&quot;</p>
<p>Ты начинаешь оптимизировать. Добавляешь кеши, тюнишь Kafka, добавляешь реплики. Каждая правка ломает что-то ещё. Через полгода у тебя зоопарк микросервисов, devops-шапито и система, которую никто не понимает, и все боятся трогать. Поздравляю, ты создал legacy.</p>
<p><strong>Cценарий 2: Наорал </strong></p>
<p>Ты не киваешь. Ты хватаешь PM, привязываешь его к стулу и начинается, хм, диалог.</p>
<p>Ты задаёшь вопросы. Сначала PM. Кто пользователи? Сколько постов в день? Кто пишет? Что пишут? Как часто заходят? А сколько пользователей через пол года, год? Что важнее, скорость или актуальность? Что считается сбоем? Можно ли показывать устаревшие данные?  А нужна аналитика? А архивировать будем? Поиск нужен? Реагируем на лайки сразу? Комментарии сразу видно? А подписки? А уведомления? И так далее.</p>
<p>Потом идешь вместе с PM пытать бизнес и эксплуатацию.</p>
<p>Через несколько дней у тебя полная картина. 5к активных пользователей. Сто постов в день. Пик нагрузки в обед и вечером. Главное — скорость. Можно жертвовать актуальностью лайков и комментариев. Простои до получаса допустимы. Архивируем через пол года. Поиск простой. Рост медленный. И тд. и тп.</p>
<p>Ты делаешь схему под это. Одна таблица posts с денормализованными данными. Один Valkey для кеша с TTL пять минут. Никакой плеяды микросервисов. Никаких очередей. Одна база, один сервис.</p>
<p>Лента открывается быстро. Система держит нагрузку. Код простой, понятный, надёжный. Бизнес доволен. Тебе не задают неудобные вопросы.</p>
<p>Всё то модное, что ты впихнул в первом сценарии, оказалось ненужным.</p>
<p>Во втором случае ты потратил часы на вопросы, но сэкономил месяцы на разработке и поддержке. Вопросы помогли увидеть реальную задачу и отбросить лишнее.</p>
<p><strong>Что в итоге</strong></p>
<p>System design это не про схемы. Это умение задавать правильные вопросы и вытаскивать из бизнеса реальные требования, а не слепо следовать его влажным фантазиям.</p>
<p>Без вопросов ты строишь систему вслепую. С вопросами решаешь конкретную задачу конкретными средствами.</p>
<p>Но вопросы это только начало. Дальше нужно понять, как переводить требования в архитектурные решения, выбирать правильные компромиссы и не поддаваться на соблазн &quot;_сделать как у больших дядек_&quot;.</p>
<p>В следующих частях расскажу, почему system design держится на трех принципах, как из требований получается архитектура, как выбирать правильные компромиссы и почему простое решение часто лучше сложного.</p>
<p>А пока перестань кивать на хотелки бизнеса и начни задавать неудобные вопросы. Скажешь себе потом спасибо.</p>
<p><strong>Что еще почитать?</strong></p>
<ul><li><a href="https://t.me/zero_deps/56" target="_blank" rel="noopener noreferrer">0. System design – это тебе не квадратики рисовать</a></li><li><a href="https://arxiv.org/pdf/1508.01954" target="_blank" rel="noopener noreferrer">Ordering Interrogative Questions for Effective Requirements Engineering: The W6H Pattern</a> - немного академки</li><li><a href="https://habr.com/ru/articles/924570/" target="_blank" rel="noopener noreferrer">System Design: Чек-лист по сбору и фиксации требований на все случае жизни</a> - больше конкретики</li><li><a href="https://www.youtube.com/watch?v=KWQ4UWmOyEI&amp;list=PL4_hYwCyhAva6-f-YxobKju-6ltmn-jNC" target="_blank" rel="noopener noreferrer">Бунин О.В. Highload 1. Сбор требований. Фронтенд-бэкенд</a> - оч советую посмотреть этот курс целиком</li></ul>]]></content:encoded>
    </item>
    <item>
      <title>System design 0: это не квадратики</title>
      <link>https://zerodeps.tech/texts/system-design-is-not-boxes</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/system-design-is-not-boxes</guid>
      <pubDate>Fri, 26 Sep 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>System design — это не схема сервисов, а управляемая работа с требованиями, потоками данных, отказами и компромиссами. Хорошая архитектура следует из измеримых ограничений, умеет деградировать под давлением и меняет форму после обратной связи из production без капитального ремонта.</description>
      <content:encoded><![CDATA[<p><strong>0. System design – это тебе не квадратики рисовать</strong></p>
<p>В начале карьеры все архитектурные вопросы сводятся к проектированию малых частей системы. Все в пределах одного сервиса. Немного думаешь про базу, немного про, прости господи, очереди, чуть-чуть про оптимизацию.</p>
<p>Обо всем остальном думают умные дядьки. Они рассказывают, зачем ты пишешь этот сервис и какие требования к нему. Скажут, какую бд взять, какой язык использовать и как сделать так, чтобы держало нагрузку. Остальное дело техники: пара паттернов, охапка костылей – сервис готов. Можно перекладывать json’ы.</p>
<p>Проходит несколько лет, и твой умный дядька исчезает. PM приходит уже к тебе. И вдруг выясняется: теперь именно ты отвечаешь на неудобные вопросы и должен объяснить, как строить сервис.</p>
<p>Последний год ты много слышал про system design, но не обращал внимания. Думал, это для больших и умных. Слышал, что он про архитектуру. Его хотят на собесах. Там рисуют квадратики и стрелочки: квадратики становятся сервисами, базами, кешами и (прости господи) очередями , стрелочки - каналами связи.</p>
<p>Посмотрел ютуб, посмотрел прошлые сервисы, подумал и нарисовал. Рассказал команде. Они постарались и сделали. Сервис выкатили на прод.</p>
<p>Сервис крутится, бизнес мутится, растет нагрузка. Растут задержки. Сервис начинает задыхаться и падать, downtime растет. Всё чаще тебе приходится отвечать на вопросы, на которые раньше отвечали другие.</p>
<p>Ты идешь советоваться к умными дядьками из других команд и компаний. Они почему-то в разрез с ютубом мало говорят про квадратики и стрелочки. Они говорят про какого-то Клепмана, про Эшби и кибернетику, про Альтшуллера и ТРИЗ.</p>
<p>Тебе говорят, что system design это про компромиссы: надежность, масштабируемость и сопровождаемость. Говорят, что мало один раз нарисовать — нужно возвращаться, пересматривать, менять архитектуру под новые боли и требования. Что нужно собирать эти требования. Что схем должно быть много: для бизнеса — своя, для девопсов — своя, для разработки — своя. Разные уровни абстракции, разные нюансы.</p>
<p>И все это – теперь хотят от тебя. Мрак. Ужас.</p>
<p>Поэтому, пока не поздно, я предлагаю тебе погрузиться со мной в знакомство с system design. Я не буду рассказывать «как пройти собес». Я расскажу тебе: почему system design не про стрелочки и квадратики, зачем тебе он нужен и как он может облегчить твои страдания.</p>
<p><strong>Что же такое «System design»?</strong></p>
<p>Чем System design точно не является: рисованием и инструментом для прохождения интервью. Так его блоггеры на ютубе продают. Так проще. Но реальность гораздо сложнее.</p>
<p>System design - это спор с реальностью, борьба с ограничениями, поиск компромиссов. Это мир, в котором у каждой идеи есть цена, у каждого ограничения есть причина. Это мир, в котором &quot;правильная архитектура&quot; сейчас становится &quot;неправильной&quot; через пару месяцев.</p>
<p>Любая система живет в изменчивой среде, и твоя задача – сделать так, чтобы она не была хрупкой, выгибалась и деформировалась, но не ломалась с треском. Это свойство системы: гибкость. Это способность пережить пик нагрузки, переварить новый источник данных, выдержать новый SLA, предоставить новый контракт. При этом не сломать команду, которая разрабатывает систему.</p>
<p>С осознанием этого, ты скорее всего придешь к первой взрослой мысли: «<strong>нужно собрать требования</strong>». Это на первый взгляд просто. Спросил, чего хотят, сделал что-то похожее. Но нет.</p>
<p>Собирать нужно с умом. Формулировка: «хотим ленту как у Threads” не подойдет. Нужно выбить из бизнеса и аналитиков конкретику: сколько пользователей планируется на старте, сколько через год, сколько времени у тебя на p99, какой сбой считается нормой, чем можно пожертвовать при пиках, важнее истина или скорость, чем бизнес готов жертвовать взамен на экономию.</p>
<p>Если всего этого не узнать в начале, ты рисуешь картинку — сферический сервис в вакууме, который красиво выглядит на схеме, но не имеет ничего общего с реальностью. Подробнее поговорим отдельно. Сбор требований важная большая тема.</p>
<p>После сбора требований логически появится следующая мысль: &quot;<strong>нужно определить технологический стек</strong>&quot;. Это не про знать все языки и инструменты, это не про держать в голове энциклопедию, это не про культ «возьмем модный брокер». Это простой расчет.</p>
<p>Требования дают представление: «такой профиль данных, здесь читаем, тут пишем, такие хвосты распределения, такие пики». Отсюда вытекают формат данных, модель консистентности, понимание, что журнала хватит и очередь не нужна, что вот тут может быть узко, а тут проблем быть не должно. Ты уже прикидываешь, нужен ли hot path в памяти, и чем придётся платить за восстановление.</p>
<p>Стек складывается естественно: какие составные части, на чём они будут написаны, какие инструменты лягут в основу. Эти мысли уже можно переносить в ТЗ и схемы.</p>
<p>Что-то уже есть на бумаге и технологический стек первично определен. Начинаешь погружаться глубже. Теперь нужно работать со связями. Мысль три: «<strong>нужно определить поток данных и обратную связь системы</strong>».</p>
<p>На этом этапе зачастую красивые презентации заканчивается. Поток данных это не просто стрелочки от сервиса к сервису. Нужно описать ритм, как система будет жить. Где данные зарождаются, как они движутся по системе, где данные накапливаются, где могут упереться в ботлнек, где задерживаются.</p>
<p>И главное - как система реагирует, когда на нее давят. Не существует «пассивной» системы: если разнообразие воздействий мира на систему больше спектра ее реакций, система уходит в отказ.</p>
<p>Если по-простому: поток данных должен быть сопряжён с обратной связью. Когда вход растёт быстрее, чем ты можешь переварить, ты либо сигнализируешь «стоп, хватит» (backpressure), либо начинаешь управляемо деградировать. Не «всё для всех, но “сдохли”», а «меньше, но вовремя».</p>
<p>И вот здесь начинает появляться настоящая архитектура: система, которая умеет не геройствовать до смерти, а вовремя сказать «нет» и остаться живой.</p>
<p>Если обобщить: System design всегда живёт на четырёх слоях: технологическом, организационном, фундаментальном и изобретательском. Игнорируешь хоть один и система падает не там, где ждёшь, а там, где больнее всего.</p>
<p><strong>Посмотрим на примере</strong></p>
<p>У тебя небольшой сервис в HFT-экосистеме. Клиенты сами парсят разные внешние источники и приводят данные к формату, нужному системе.</p>
<p>Сервис принимает данные, собирает в пачки и отправляет дальше, в контур принятия решений. Вроде ничего сложного.</p>
<p>Но реальность бьет по твоей картинке цифрами. Бюджет задержки жесткий. Данные устаревают буквально спустя пару сотен миллисекунд, а еще нужно принять решение и это решение обработать. Бизнесу важнее своевременность, чем богатая обвязка вокруг каждого события. И да, падать система может, но подниматься обязана быстро, без долгих реплеев и нескольких минут на холодный старт.</p>
<p>С этого места любая «идеальная» схема превращается в торги с реальностью. Сначала ты, под влиянием общественности, кладёшь между нормализацией и обработкой брокер сообщений. Красиво: есть буфер, есть независимость потребителей, есть история. Но под всплесками у тебя начинается разброд в хвостах: холодные консьюмеры приходят не вовремя, очереди пухнут, рваные задержки делают p99 некрасивым. Да, это отчасти лечится, но чудес не бывает, ты платишь за удобство бэклога лишними миллисекундами и нестабильностью хвостов.</p>
<p>Ок, пробуешь подойти с другой стороны. Выносишь горячий путь целиком в память. Кольцевой буфер, предсказуемость, минимум аллокаций, никакой тяжелой сериализации. Красота, пока всё живо. Как только система падает, выясняется, что воспроизводимость историй тебе нужна не назавтра, а сейчас. Иначе ты либо принимаешь решения «вслепую», либо тормозишь прод ради восстановления. Местами это допустимо, но обычно — нет.</p>
<p>В какой-то момент ты перестаёшь играть в чёрно-белое и делаешь так, как делают взрослые: разделяешь истину и скорость.</p>
<p>Горячий путь идёт по памяти и держит SLA, а рядом ты пишешь минимальный, но надёжный журнал, из которого можно подняться, догнать и перепроверить. Не «всё и сразу», а ровно столько, чтобы после падения не начинать жизнь заново. Это дороже по ресурсам и по мозгам: двойная запись, идемпотентность, продуманная консистентность. Зато вместо религиозного спора «брокер или память» у тебя работает система, объединившая оба подхода.</p>
<p>Дальше больше, завозишь дисциплину. Ты привязываешь поведение к метрикам: когда хвосты начинают расти и превышают допустимые значения, обедняешь обработку, сохраняешь ядро.</p>
<p>Ты не глотаешь бесконечный вход, ты просишь столько, сколько способен обработать, а остальное честно отклоняешь. Выбираешь обратимость действий, чтобы не держаться за «идеальную транзакцию» в распределённой грязи.</p>
<p>Оформляешь эти решения словами, которые команда понимает: здесь у нас компенсирующая логика, тут мы разносим запись и чтение в разные модели, тут у нас предохранитель, тут выбрасываем «дорогое» обогащение при перегреве.</p>
<p>А далее, ты регулярно возвращаешься к схеме, не чтобы «дорисовать рамку», а чтобы проверить, что текущее поведение соответствует изменившимся требованиям.</p>
<p>С этого места становится видно главное. System design не про то, чтобы однажды красиво объяснить «как мы всё построим». Это про способность системы и команды менять форму, не теряя сути. Про честные компромиссы между скоростью и истиной, между простотой и гибкостью, между идеалом и тем, что реально держит прод. И да, про изобретательность по ТРИЗ: не «выбрать единственно верный инструмент», а развернуть противоречие так, чтобы выиграть временем и уменьшить боль.</p>
<p><strong>Вывод</strong></p>
<p>System design это широко, сложно и очень интересно. Это не про квадратики и стрелочки. Это про то, как разговаривать с реальностью на языке требований, обратной связи и компромиссов. Если твоя схема выдерживает новый источник, новый SLA и новый сбой без капитального ремонта, значит, ты всё сделал правильно. Всё остальное — декор.</p>
<p><strong>Что еще почитать?</strong></p>
<ul><li><a href="https://www.seangoedecke.com/good-system-design" target="_blank" rel="noopener noreferrer">Sean goedecke. Everything I know about good system design</a></li><li><a href="https://www.intercom.com/blog/six-principles-of-system-design/" target="_blank" rel="noopener noreferrer">Intercom. Six principles of system design</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>Очереди всё ещё не нужны: проверяем цифрами</title>
      <link>https://zerodeps.tech/texts/queues-are-still-not-needed</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/queues-are-still-not-needed</guid>
      <pubDate>Wed, 27 Aug 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>В конкретном стенде для приёма задач с zero-loss и SLA p99 до 50 мс локальный WAL и отдельная ingest-база оказались ровнее и проще Valkey Streams и RabbitMQ. Результат не доказывает, что брокеры бесполезны: он показывает, что очередь оправдана маршрутизацией и независимыми потребителями, а не самим фактом асинхронной записи.</description>
      <content:encoded><![CDATA[<p><strong>Очереди все еще не нужны.</strong></p>
<p>Каждый  разработчик думает об архитектуре нового проекта,  и часто приходит к такому заключению: «_Завезу микросервисы, и чтоб красиво кафку туда, или реббит, чтоб общались. Редис для кешей, и все это в кубере. Заживу!_».</p>
<p>Обоснование тому такое: «_Задел на будущее, кучу систем так построили, нормально работает, даже не больно, практически. И распределенка, и отказы с пиками держим. Красота!_». Я мог бы согласится, но не буду. Если начать копать глубже, всплывает много нюансов. Большую часть того, что всплывает, я <a href="https://t.me/zero_deps/23" target="_blank" rel="noopener noreferrer">описал в прошлый раз</a>.</p>
<p>Тогда же, в <a href="https://t.me/c/2874265781/75" target="_blank" rel="noopener noreferrer">комментариях</a>, чтобы не быть голословным и ради академического интереса, решил собрать стенд и уже на цифрах тебе показать, что очереди в классическом понимании все еще не нужны.</p>
<p>Расскажу что собирал, как и что измерял и какие результаты получил.</p>
<p><strong>Часть первая: Преамбула</strong></p>
<p>Начну издалека, с теоретического обоснования, как учили еще в университетах.</p>
<p>Есть у тебя сервис А. Базу свою этот сервис и в хвост и в гриву, поэтому запись там медленная — допустим, 60 мс. Сервис А принимает на эндпоинт <code>/task</code> задачу, а бизнес вместе с SRE говорит тебе как архитектору: «_Ждать ажно 60мс никак не можем, ответить нужно за 50 и точка. Zero loss, идемпотентность, at-least-once. Ещё давай, чтобы graceful degradation было, throughput не падал, latency стабильно в SLA. Ordering guarantees можно не жёсткие, но durability кровь из носа. И чтоб backpressure обязательно. Завтра релиз_».</p>
<p>Отдельное спасибо бизнесу, что порядок записи не так уж и важен, и этим ты можем пренебречь, то есть писать можно параллельно пачками и быстрее разбирать хвост.</p>
<p>Формализуем требования:</p>
<ul><li><strong>SLA входа:</strong> p99(t_api) ≤ 50 мс при <strong>500</strong> RPS (без аварий).</li><li><strong>At-least-once</strong> доставка <strong>без дублей</strong> в целевой БД (идемпотентность по <code>id</code>).</li><li><strong>Durability</strong> на приёме: после ответа клиенту запись <strong>потеряться не должна</strong> при падении сервиса/воркера/базы.</li></ul>
<p>Остальное как бы не услышал, оно тебе не важно.</p>
<p>Уяснив требования бизнеса и хотелки SRE, наливаешь кофу и начинаешь думать, какие варианты у тебя есть и чего эти варианты тебе будут стоить.</p>
<p><strong>Часть вторая: Кейсы</strong></p>
<p>Возьмем сферический проект в вакууме, представим, что у тебя нет ничего кроме как api и базы. Api напрямую пишет задачу в бд и беспощадно нарушает SLA.</p>
<p>&quot;_Чтож делать? Чтож делать?_&quot;</p>
<p><strong>Кейс 1. WAL (Write-Ahead Logging)</strong></p>
<p>Старый “советский” метод. Api пишет не в базу, а в локальный журнал. Формат тупой как гвоздь: длина + payload. Один fsync на батч или по таймауту. Ротация — по размеру и времени. Старый сегмент закрыли, новый открыли. Воркеры читают завершённые сегменты и докатывают в базу пачками, хоть по 100, хоть по 1000 штук.</p>
<p><strong>Плюсы:</strong> – Latency +-1 мс на входе (fsync по батчу/таймеру). – Durability честная после fsync. – At‑least‑once через идемпотентность в БД. – Failover тривиальный (перечитываем сегмент). – Естественный backpressure по росту сегментов.</p>
<p><strong>Минусы:</strong> – Дисковая нагрузка (особенно fsync=always). – Своя реализация и поддержка (скорее всего реализации готовой не будет или будет написана индусом). – Нужен мониторинг лага/ротации, контроль места на диске.</p>
<p>WAL – это &quot;скучный&quot; вариант, но и предсказуемый, сам сломал –сам чинишь. Нет брокеров, нет «кластеров», нет магии. Файл и логика: как писать, как читать, как ротировать. Стоит упомянуть, что все последующие кейсы в той или иной степени под капотом имеют схожий механизм обеспечения сохранности данных – fsync.</p>
<p><strong>Кейс 2. Ingest DB</strong></p>
<p>Тут уже будет нужен DevOps. Вместо того, чтобы писать в журнал, пишем сразу в базу — но не в боевую, а в отдельный инстанс (ingest), который не тормозит, заточен на быструю запись и физически крутится на другом сервере.  Уже из него воркер параллельно пачками переливает в нужную базу.</p>
<p><strong>Плюсы:</strong> – Durability на стороне СУБД. – Дубликаты гасит ON CONFLICT. – Масштабирование воркерами, размер пачки. – Лаг прозрачен (ingest → main).</p>
<p><strong>Минусы:</strong> – Индексы на ingest могут тормозить вставку. – Вакуум/блоат и размер пачки нужно тюнить. – UNLOGGED нельзя при zero‑loss. – Возможна гонка за ресурс на горячих партициях. – Медленная/общая БД = боттлнек.</p>
<p>Получается, что IngestDB — это в теории решение между чистым WAL и брокером. Простая вставка на входе, батчи на выходе, минимум инфраструктуры. Теоретически хорошее решение, завез отдельную базу, затюнил и решил проблему.</p>
<p><strong>Кейс 3: Valkey (Streams с AOF)</strong></p>
<p>Вот ты уже практически прям рядом с очередью. Еще не жирный брокер, но уже очередь на минималках. Берём Valkey (форк Redis), включаем streams и appendfsync=always. Тут как и с базой нужОн DevOps, лучше два, поднимаем новый отдельный сервис.</p>
<p>А работает кейс так: – api пишет событие в XADD stream * payload. Операция быстрая, но не бесплатная, каждый fsync блокирует запись. – Клиент получает ACK сразу после попадания в AOF, значит durability честная. – Воркер читает через XREADGROUP, обрабатывает пачку и помечает offset.</p>
<p><strong>Плюсы:</strong> – Низкая теоретическая базовая задержка на XADD. – AOF даёт честную durability. – At‑least‑once встроен (PEL/ACK) (с оговоркой, только на доставку, дубли не контролируются). – Привычный стек (в теории).</p>
<p><strong>Минусы:</strong> – p99 скачет из‑за fsync на нагрузке. – Backpressure кривой: растёт лаг, сервис ест память. – Failover: потеря хвоста без WAIT/min‑replicas‑to‑write. – Доп. сервис и зоопарк, рост RAM до краша.</p>
<p>Valkey хорош для UX-шных событий, коротких очередей, быстрых стримов, где пропускная способность важнее стабильной задержки. Но в задаче «zero loss + SLA по latency» valkey начинает подтекать: да, не потеряет, но предсказуемость страдает. В оправдание запишем тот факт, что скорее всего, valkey или redis уже будет на проекте как cache.</p>
<p><strong>Кейс 4: Rabbit</strong></p>
<p>Ну вот и добрались до Очереди. Да, именно с большой буквы. Настоящая, взрослая, жирненькая очередь, как у больших дядек в фаанге. &quot;Для очередей придумана!&quot; - это про нее. Все архитекторы дружно кивают головами: durable queue, ack, retry, всё как завещали тебе Фаулер, Хоппе (Хохпе?) и Клишин.</p>
<p>Как работает: – api пишет задачу в durable queue через AMQP. Это не запись в файл напрямую, а запись через брокер, его буферы, его собственный журнал, еще и по сети. – ACK клиенту прилетает сразу после попадания в очередь (и тут уже вопрос: в память или на диск). – Воркер подписан на очередь, получает сообщения, подтверждает через ack, или говорит, что не смог через nack.</p>
<p><strong>Плюсы:</strong> – Ack/Nack/retry/маршрутизация из коробки. – At‑least‑once встроен (с оговоркой: гарантируется сохранность сообщения, но не отсутствие дубликатов) – Fanout/headers/топики для сложной топологии.</p>
<p><strong>Минусы:</strong> – Latency выше и пила по p99. – Durability условная без durable+persistent+publisher confirm. – Failover через Raft: пауза на выбор лидера, redelivery, без confirm — потеря. – Сложный зоопарк (кластер, HA‑политики, мониторинг), непредсказуемый throughput при пиках.</p>
<p>Несмотря на то, что RabbitMQ выглядит «серьёзным», &quot;большим&quot;, &quot;взрослым&quot; и очень нужным,  по факту добавляет: дополнительный hop в pipeline по сети, непредсказуемые задержки и вероятность потерять данные при кривой конфигурации.</p>
<p>Для задач «принять таску и сохранить без потерь» — rabbit не лучше, чем WAL или Ingest. Для задач «маршрутизировать в десять разных консьюмеров с fanout/headers-routing» — норм. Но если тебе это не надо, RabbitMQ — это, уже на теоретической части, оверинжиниринг.</p>
<p>Примерно такой поток мыслей будет в голове у архитектора, который с кофой сидит и думает как будет закрывать SLA. Давай теперь расскажу как и на чем тесты проводились.</p>
<p><strong>Часть третья: Стенд. Что? Где? Когда?</strong></p>
<p>Собрал значит стенд, один <a href="https://github.com/zerodeps-tech/anti-queue" target="_blank" rel="noopener noreferrer">репозиторий,</a> 4 сценария. Поднимаем через docker-compose, немного bash скриптов, make.</p>
<p>Окружение у стенда получилось следующее: - <strong>Node.js ≥ 22</strong>:  api, воркеры, утилиты, клиенты для бд и очередей, самописный WAL - <strong>PostgreSQL 16</strong>:  основная медленная СУБД и ingest в отдельном контейнере для второго кейса - <strong>RabbitMQ 4.1</strong>: для четвертого кейса - Генератор нагрузки: <strong>autocannon</strong>. - Метрики: логи в json для таймингов. - ОС-метрики: <strong>telegraf 1.35</strong></p>
<p>Для каждого кейса отдельный api и worker (см. apps/*). – WAL: запись в файл, ротация и воркер, который докатывает сегменты. – Ingest: отдельная БД для приема и воркер, который батчами с ограниченной параллельностью переливает из ingest в main. – Valkey: XADD в stream, чтение через consumer group. – Rabbit: durable очередь по AMQP.</p>
<p>Для Ingest, Valkey, Rabbit использовал готовые клиенты из npm. WAL пришлось написать самому, в npm не оказалось нормальной реализации, есть пара пакетов, один древний, второй сомнительный. Ну и zerodeps как никак. 700 строк и +- рабочий WAL готов, с ротациями, с таймерами, локами и блекджеком.</p>
<p>Долгую запись в основную бд эмулирую через триггер со sleep на запись каждой строки. Нагрузка идёт отдельным сервисом <code>loadgen</code> (autocannon под капотом).</p>
<p>Сценарий нагрузки: RPS: 500. Каждый прогон: 30 с прогрев (1/2 RPS) → 5 мин стабильно RPS → 30 спад (1/2 RPS). Сбои (на 3-й минуте):  Kill воркера на 30с, затем рестарт.</p>
<p>Можно придумать другие сценарии, но не стал, одного считаю достаточным для сравнения в рамках данного изыскания.</p>
<p>По метриками тоже не стал изобретать. Telegraf для os, там все из коробки. По бизнес метрикам json логи в файл. Просто читать и анализировать.</p>
<p><strong>Что собирал:</strong></p>
<ul><li><code>durable</code> — время надёжной фиксации в соответствующем стораже:</li></ul>
<p>- WAL: до записи в журнал. - Ingest: до коммита транзакции в ingest-БД. - Rabbit: до получения publisher confirm. - Valkey: задержка <code>XADD</code>; только при <code>appendfsync=always</code> это «сразу durable».</p>
<ul><li><code>committed</code> — кол-во строк и время коммита в основную бд</li><li><code>db_count</code> – кол-во записей в основной бд в момент времени</li><li><code>backlog</code> – размер хвоста в момент времени</li><li><code>api_latency</code> – время, за которое ответил api и каким кодом</li><li>OS метрики cpu, mem, diskio, net для контейнеров, участвующих в прогоне (api/worker/сервис).</li></ul>
<p>Для сравнения по каждому кейсу будем считать набор стабильных метрик:</p>
<ul><li>latency (api, durable, committed) — чтобы видеть, где именно в цепочке появляется задержка; </li><li>backlog — динамику роста и слива очереди, пик и площадь под кривой, то есть сколько «долга» система накапливает; </li><li>пропускную способность (committed rps), стабильность и хвостовые индексы (tail index) — насколько тяжёлые редкие задержки по сравнению с медианой; </li><li>итоговое количество записей (rows_total) — для нормализации этих значений между прогонами разного масштаба. </li></ul>
<p>В результате получаем сопоставимые профили: видно, как быстро система набирает очередь, с какой скоростью от неё избавляется, какие задержки видны пользователю на API-уровне и насколько стабильны внутренние транзакции.</p>
<p>Пример отчета можно посмотреть тут: <a href="https://github.com/zerodeps-tech/anti-queue/blob/master/docs/compare.md" target="_blank" rel="noopener noreferrer">comare.md</a></p>
<p>Так же оценим когнитивную нагрузку для каждого кейса.</p>
<p>Критерии оценки:</p>
<ul><li>Код: строки собственного кода (API+воркер+реплей/ретраи/метрики).</li><li>Конфиг: число обязательных настроек.</li><li>Failure modes: список типов отказов, о которых нужно помнить.</li><li>Наблюдаемость: минимально достаточный набор метрик/алертов.</li><li>Ранбуки: шаги запуска/восстановления для «упал X».</li></ul>
<p>Каждый пункт от 0 до 5, суммируем и получаем «индекс когнитивки»; чем ниже — тем проще.</p>
<p>В целом никакого рокет сайенс, можно самому при желании потрогать. Перейдем к конкретике.</p>
<p><strong>Часть четвертая: Когнитивная нагрузка</strong></p>
<p><strong>WAL</strong> • Код: 5/5. (+- 50 api и worker + 600 реализация). • Конфиг: 1/5 (Путь до рабочей директории, когда и как fsync, когда и как rorate, размер батча). 0 devops • Failure: 3/5 (диск/ротация/fsync). • Наблюдаемость: 3/5 (лаг сегментов, место на диске, скорость докатки). • Ранбуки: 2/5 (перезапуск воркера, зачистка сегментов, квоты на диск).</p>
<p>Сумма: 14/25 - реализация подводит, код, который нужно будет поддерживать, не выкинуть. DevOps: 0 - не нужен.</p>
<p><strong>Ingest</strong> • Код:  2/5, ( +-100 api + worker). • Конфиг: 2/5 (Конфиги БД, размер батча, lease_ms) . 1 devops • Failure: 4/5 (вакуум/блоат, гонки за ресурс, ретраи транзакций, тайминги lease). • Наблюдаемость: 3/5 (лаг ingest→main, deadlocks/retries, длительность батчей). • Ранбуки: 3/5 (восстановить сервис, чистки/автовакуум, реплей).</p>
<p>Сумма: 14/25 — средняя  нагрузка, «можно держать в голове». DevOps: 1 - вторая бд на другом сервере</p>
<p><strong>Valkey (Streams + AOF)</strong> • Код: 2/5 ( +- 100 api + worker). • Конфиг: 3/5. (Конфигурация Valkey, stream, group, blockMs  claimIdleMs) 2 devops • Failure modes: 5/5 (RAM-утечка на lag, AOF/репликация, PEL/claim, trim-политики). • Наблюдаемость: 4/5 (lag/PEL/maxlen/latency скачет от fsync). • Ранбуки: 4/5 (перевыбор лидера, WAIT/min-replicas-to-write, ручные trim).</p>
<p>Сумма: 18 — высокий индекс, «держать в голове труднее». DevOps: 2  - отдельный &quot;новый&quot; сервис, возможны трудности.</p>
<p><strong>RabbitMQ (Quorum)</strong> • Код: 1/5 (+-80 строк api + worker). • Конфиг: 5/5 (users, permissions, exchanges, queues, bindings, policies и тд) 4 DevOps • Failure modes: 5/5 (quorum/confirm, flow control, DLX, poison msg, переизбрания). • Наблюдаемость: 5/5 (queues/channels/consumers/lag/DLQ/flow/memory/disk alarms). • Ранбуки: 5/5 (кластер, политики, шардирование, восстановление, дрейф конфигов).</p>
<p>Сумма: 21 — высокая когнитивка, зоопарк, труднее удержать в голове DevOps: 4  - кластер, конфиги и риски потерь высокие.</p>
<p>В итоге по когнитивке:</p>
<ul><li><strong>WAL (14/25)</strong> — простая эксплуатация, но платишь поддержкой собственного кода.</li><li><strong>Ingest (14/25)</strong> — баланс: чуть DevOps, остальное — дисциплина СУБД.</li><li><strong>Valkey (18/25)</strong> — «быстро, но дорого в голове»: много тюнинга и аварийных сценариев.</li><li><strong>Rabbit (21/25)</strong> — минимум кода у тебя, максимум сложности в инфраструктуре.</li></ul>
<p>Оценка субъективна, попытка переложить на цифры виденье сложности каждого решения. Перейдем к тому, ради чего мы тут все собрались.</p>
<p><strong>Часть пятая: Цифры</strong></p>
<p><strong>SLA (p99 API ≤ 50 мс)</strong></p>
<p>Все четыре кейса выдержали заявленный SLA по входным запросам. Разница в том, насколько уверенно они это делают:</p>
<ul><li><strong>WAL</strong> — минимальные задержки, <strong>p99 = 14 мс</strong>. Ответы приходят практически мгновенно, так как запрос завершается сразу после записи в локальный журнал.</li><li><strong>RabbitMQ</strong> — <strong>23 мс</strong>. Здесь есть сетевой hop и подтверждение от брокера, но запас прочности остаётся высоким.</li><li><strong>Ingest</strong> — <strong>44 мс</strong>, вплотную к верхней границе SLA. Для сценария «50 мс» этого хватает, но зазора на будущее почти нет.</li><li><strong>Valkey</strong> — <strong>46 мс</strong>, прямо на краю допустимого. Любые пики нагрузки могут выбить его за SLA.</li></ul>
<p>Все четыре решения «держат SLA», но WAL и RabbitMQ имеют запас, Ingest и Valkey балансируют на границе.</p>
<p><strong>Durable latency</strong></p>
<p>Это время до того момента, когда запись гарантированно не потеряется (fsync или confirm).</p>
<ul><li><strong>WAL</strong> — <strong>14 мс</strong>, фактически совпадает с API latency. Минимум прослоек = минимум задержки.</li><li><strong>RabbitMQ</strong> — <strong>22 мс</strong>. Брокер добавляет задержку из-за своей внутренней журнализации и подтверждений.</li><li><strong>Ingest</strong> — <strong>44 мс</strong>. Всё упирается в транзакцию Postgres, на каждое подтверждение уходит десятки миллисекунд.</li><li><strong>Valkey</strong> — <strong>45 мс</strong>, почти то же самое: fsync на AOF дороже, чем кажется, и скачет вместе с нагрузкой.</li></ul>
<p>Видно, что «каждая новая прослойка» добавляет десятки миллисекунд к фиксации. WAL выигрывает именно простотой: запись в файл и всё.</p>
<p><strong>Коммиты в основную БД</strong></p>
<p>Тут все варианты упираются в одно «бутылочную горлышко»: медленная основная база, ожидаемо. p99 коммита ~ <strong>12.3 секунд</strong>, средний throughput около <strong>16 rps</strong>.</p>
<ul><li><strong>WAL</strong>: среднее время ~ <strong>9.8 с</strong>. Профиль ровный, вариативность низкая — хорошая предсказуемость.</li><li><strong>Ingest</strong>: ~ <strong>11.7 с</strong>. Немного хуже WAL, но тоже стабильно.</li><li><strong>Valkey</strong>: ~ <strong>11.3 с</strong>. Схож с Ingest, чуть быстрее.</li><li><strong>RabbitMQ</strong>: выбивается — среднее всего <strong>5.5 с</strong>, но при этом хвост тяжёлый, доходит до <strong>12.4 с</strong>. Коэффициент вариации самый высокий (<strong>0.035</strong>), то есть профиль «рваный»: часть задач пролетает быстро, часть задерживается надолго.</li></ul>
<p>Никакая очередь сама по себе не лечит узкое место в базе: throughput одинаковый, разница только в том, насколько предсказуемо сообщение добирается до базы.</p>
<p><strong>Backlog и дренаж</strong></p>
<p>Здесь видно, насколько быстро система накапливает очередь и как справляется с хвостом после сбоя:</p>
<ul><li><strong>WAL</strong>: пик ~ <strong>47k</strong> задач, скорость дренажа <strong>353/с</strong>, хвост уходит за ~ <strong>133 с</strong>. Рабочий, предсказуемый профиль.</li><li><strong>Ingest</strong>: чуть больше очередь — <strong>50k</strong>, дренаж <strong>362/с</strong>, очистка за ~ <strong>138 с</strong>. По сути те же цифры, что у WAL.</li><li><strong>Valkey</strong>: также <strong>50k</strong> в пике, но дренирует медленнее — <strong>327/с</strong>, до нуля ~ <strong>152 с</strong>. Отставание небольшое, но заметное.</li><li><strong>RabbitMQ</strong>: резко выбивается. Очередь раздувается до <strong>88k</strong>, скорость дренажа всего <strong>250/с</strong>, на полное очищение уходит ~ <strong>353 с</strong>.</li></ul>
<p>RabbitMQ тратит больше ресурсов на собственные механизмы и за счёт этого копит самый большой хвост и дренирует его хуже всех. WAL и Ingest — наоборот, разгребают быстрее всего.</p>
<p><strong>Стабильность RPS</strong></p>
<p>Коэффициент вариации показывает, насколько «ровно» система держит нагрузку.</p>
<ul><li><strong>WAL</strong>: CV = <strong>0.004</strong> — почти идеально прямая линия.</li><li><strong>Ingest</strong>: CV = <strong>0.003</strong> — ещё ровнее.</li><li><strong>Valkey</strong>: CV = <strong>0.026</strong>, заметные колебания.</li><li><strong>RabbitMQ</strong>: CV = <strong>0.035</strong>, самая «пилообразная» динамика.</li></ul>
<p>Чем выше CV, тем чаще будут провалы и скачки нагрузки. Для бизнеса важнее ровный поток — тут выигрывают WAL и Ingest.</p>
<p><strong>Нагрузка на железо</strong></p>
<p>Смотрим на ресурсы контейнеров:</p>
<ul><li><strong>WAL</strong>: worker грузит CPU до <strong>68%</strong>, в среднем ~<strong>16%</strong>. Для single-process нагрузки это нормально.</li><li><strong>Ingest</strong>: Postgres-ingest забирает ~<strong>9% CPU</strong>, пиками до ~<strong>32%</strong>. Очень умеренно.</li><li><strong>Valkey</strong>: CPU потребление невысокое, память в пределах пары гигабайт.</li><li><strong>RabbitMQ</strong>: сам брокер в среднем ~<strong>19% CPU</strong>, но с пиками до <strong>100%</strong>. При этом база тоже нагружена.</li></ul>
<p>RabbitMQ на ровном месте создаёт дополнительную нагрузку на CPU, WAL и Ingest в этом плане экономичнее.</p>
<p>Все собранные цифры можно посмотреть <a href="https://github.com/zerodeps-tech/anti-queue/blob/master/docs/compare.md" target="_blank" rel="noopener noreferrer">тут</a> или самому собрать.</p>
<p><strong>Что в итоге?</strong></p>
<p>По цифрам картинка складывается однозначная:</p>
<ul><li><strong>WAL и Ingest</strong> закрывают SLA и zero-loss с минимальными накладными расходами. Первый вариант даёт мгновенный ответ и предсказуемое поведение ценой поддержки собственного кода. Второй требует отдельной базы и DevOps-дисциплины, но зато использует &quot;привычные&quot; инструменты (бд). В обоих случаях задержка минимальна, профиль ровный, дренаж очереди быстрый.</li></ul>
<ul><li><strong>Valkey и RabbitMQ</strong> тоже справляются с задачей, но делают это хуже: добавляют десятки миллисекунд на ровном месте, сильнее накапливают хвост и дают нестабильный профиль. RabbitMQ особенно выделяется большим backlog и скачущим RPS, а вместе с этим тащит в проект новые точки отказа, дполонительные сложности эксплуатации и накладные расходы.</li></ul>
<p>Из этого следует простой вывод: <strong>очередь — не фундамент архитектуры, а специализированный инструмент</strong>.</p>
<p>Нужен фан-аут на десятки консьюмеров, маршрутизация по ключам или хитрые сценарии доставки? Тогда Rabbit/Kafka оправданы: они решают задачи иснтументами, которых нет у простого WAL или Ingest.</p>
<p>Нужно просто принять событие и гарантированно не потерять? Файловый журнал или ingest-база справляются проще, быстрее и надёжнее.</p>
<p>Тащить очередь «по умолчанию» — это всё равно что начинать проект сразу с микросервисов в Kubernetes: звучит солидно, но на деле означает больше кода, DevOps шапито, лишняя инфраструктура, больше мониторинга и больше точек отказа.</p>
<p><strong>Очереди нужны не «по умолчанию», а когда без них никак.</strong> В остальных случаях — это фетиш, технический долг и лишний костыль, который сам себе суёшь в ...</p>
<p><strong>Что еще почитать?</strong></p>
<ul><li><a href="https://t.me/zero_deps/23" target="_blank" rel="noopener noreferrer">Прошлый пост про очереди</a></li><li><a href="https://github.com/zerodeps-tech/anti-queue" target="_blank" rel="noopener noreferrer">Репо со стендом</a></li><li><a href="https://medium.com/@2003priyanshusingh/rabbitmq-vs-valkey-stream-which-message-broker-is-right-for-your-microservices-c91b7e9135c2" target="_blank" rel="noopener noreferrer">RabbitMQ vs Reddis</a></li><li><a href="https://jack-vanlightly.com/blog/2023/10/2/the-advantages-of-queues-on-logs" target="_blank" rel="noopener noreferrer">The advantages of queues on logs</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>Зачем ты везде тащишь очереди?</title>
      <link>https://zerodeps.tech/texts/queues-are-not-default</link>
      <guid isPermaLink="true">https://zerodeps.tech/texts/queues-are-not-default</guid>
      <pubDate>Mon, 11 Aug 2025 06:00:00 GMT</pubDate>
      <dc:creator>Василий Каменюк</dc:creator>
      <description>Очередь не должна быть архитектурным дефолтом: она добавляет доставку at-least-once, дубли, порядок, эволюцию схем, мониторинг и новые точки отказа. Для асинхронной работы внутри одного домена транзакционный outbox и идемпотентный воркер часто дают тот же полезный контракт без отдельного брокера.</description>
      <content:encoded><![CDATA[<p><strong>Зачем ты везде тащишь очереди?</strong></p>
<p>Вот ты проектируешь свою систему, базы, микросервисы, кэш - все красиво. И вот встает вопрос: как это все связать, как они будет общаться?</p>
<p>И тут со всех утюгов кричат:</p>
<ul><li>_У тебя микросервисы - тебе нужны очереди!_</li><li>_Очереди - это модно._</li><li>_Очереди - это полезно._</li><li>_Очереди - это масштаб_ и надежность<em>.</em></li><li>_Очереди &quot;корпоративный стандарт&quot;._ (как и TS, ага да)</li></ul>
<p>Ты берешь, допустим RabitMQ, поднимаешь брокер (+1 сервис и глобальная точка отказа), и получаешь большую асинхронную боль сквозь всю систему, рост когнитивной нагрузки и эксплуатационные издержки: лаг, дубликаты, краш консумера, ядовитые сообщения и дрейф схем. Да есть и полезное: буферизация, распределение по времени, изоляция сбоев и фан-аут. Но какой ценой?</p>
<p>Если у тебя CRUD между двумя сервисами и одинаковые требования по времени ответа и доступности - очередь только навредит.</p>
<p>Синхронный вызов честно говорит: «_либо сделал, либо нет_». С очередью ты говоришь: «_когда-нибудь, возможно, кто-то где-то это обработает_».</p>
<p>Отсюда вылезают взрослые темы, которые почему-то игнорируют, когда «прикручивают Kafka/Rabbit ради моды»:</p>
<ul><li><strong>Доставка</strong>: at-least-once - дубликаты - <strong>идемпотентность</strong> на приёмнике</li><li><strong>Порядок</strong>: забудь «строгий порядок», у тебя <strong>head-of-line blocking</strong> или вообще все перемешано.</li><li><strong>Нагрузка</strong>: «очередь поможет удержать пик нагрузки» — да, но хвост <strong>растёт</strong>. Хвост — это твой скрытый долг по времени отклика.</li><li><strong>Мониторинг</strong>: трассировка через брокер — это чёрный ящик и пляски с кореляторами.</li><li><strong>Контракты</strong>: cобытие — это публичное API. Версионирование, эволюция схем, миграции потребителей — отдельная боль.</li></ul>
<p>Очередь приносит тебе <strong>асинхронность как обязательство</strong>. А это обязательство принуждает тебя проектировать идемпотентные операции, хранить входящие/исходящие события, жанглировать ретраями и дедупликацией, думать про повторный прогон истории (пересчёт из журнала). Если ты к этому не готов - будет больно, очередь сломает прод, или тебя.</p>
<p><strong>Пример</strong></p>
<p>Сценарий: Создать задачу и отправить ее на исполнение в другой сервис.</p>
<p>Вариант А — MongoDB + outbox (без внешней очереди):</p>
<p>Идея простая: Одной транзакцией пишем задачу и событие. Сервис исполнитель читает события напрямую, делает работу, фиксирует факт обработки. Никакой магии, никаких скрытых механизов доставки, честная атомарность.</p>
<pre data-language="js"><code class="language-js"><span class="syntax-comment">// Сервис &quot;Планирователь&quot;</span>
<span class="syntax-comment">/**
 * @param {import(&#39;mongodb&#39;).MongoClient} client
 * @param {import(&#39;mongodb&#39;).Db} db
 * @param {Task} task
 * @returns {{status: boolean, taskId: string}}
 */</span>
<span class="syntax-keyword">export</span> <span class="syntax-keyword">async</span> <span class="syntax-keyword">function</span> <span class="syntax-function">createTask</span> (client, db, task) {
  <span class="syntax-keyword">const</span> session <span class="syntax-operator">=</span> client.<span class="syntax-function">startSession</span>()

  <span class="syntax-keyword">try</span> {
    <span class="syntax-keyword">const</span> eventId <span class="syntax-operator">=</span> <span class="syntax-builtin">crypto</span>.<span class="syntax-function">randomUUID</span>()

    session.<span class="syntax-function">startTransaction</span>()

    <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;tasks&#39;</span>).<span class="syntax-function">insertOne</span>(
      {
        _id<span class="syntax-operator">:</span> task.id,
        type<span class="syntax-operator">:</span> task.type,
        payload<span class="syntax-operator">:</span> task.payload,
        status<span class="syntax-operator">:</span> <span class="syntax-string">&#39;pending&#39;</span>,
        createdAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>()
      },
      { session }
    )

    <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>).<span class="syntax-function">insertOne</span>(
      {
        _id<span class="syntax-operator">:</span> eventId,
        type<span class="syntax-operator">:</span> <span class="syntax-string">&#39;task.created&#39;</span>,
        payload<span class="syntax-operator">:</span> { taskId<span class="syntax-operator">:</span> task.id },
        createdAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>(),
        pickedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span>,
        doneAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span>
      },
      { session }
    )

    <span class="syntax-keyword">await</span> session.<span class="syntax-function">commitTransaction</span>()

    <span class="syntax-keyword">return</span> { status<span class="syntax-operator">:</span> <span class="syntax-literal">true</span>, eventId }
  } <span class="syntax-keyword">catch</span> (error) {
    <span class="syntax-builtin">console</span>.<span class="syntax-function">error</span>(<span class="syntax-string">`[createTask] ${task.id}:`</span>, error)
    <span class="syntax-keyword">await</span> session.<span class="syntax-function">abortTransaction</span>()
  } <span class="syntax-keyword">finally</span> {
    <span class="syntax-keyword">await</span> session.<span class="syntax-function">endSession</span>()
  }
}

<span class="syntax-comment">// Сервис &quot;Делатель&quot;</span>
<span class="syntax-comment">/**
 * @param {import(&#39;mongodb&#39;).MongoClient} client
 * @param {import(&#39;mongodb&#39;).Db} db
 * @returns {Promise&lt;void&gt;}
 */</span>
<span class="syntax-keyword">export</span> <span class="syntax-keyword">async</span> <span class="syntax-keyword">function</span> <span class="syntax-function">executor</span> (client, db) {
  <span class="syntax-comment">// атомарно «забронировали» событие</span>
  <span class="syntax-keyword">const</span> event <span class="syntax-operator">=</span> <span class="syntax-keyword">await</span> db
    .<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>)
    .<span class="syntax-function">findOneAndUpdate</span>(
      { type<span class="syntax-operator">:</span> <span class="syntax-string">&#39;task.created&#39;</span>, pickedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span> },
      { $set<span class="syntax-operator">:</span> { pickedAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>() } },
      { sort<span class="syntax-operator">:</span> { createdAt<span class="syntax-operator">:</span> <span class="syntax-number">1</span> }, returnDocument<span class="syntax-operator">:</span> <span class="syntax-string">&#39;after&#39;</span> }
    )

  <span class="syntax-keyword">if</span> (<span class="syntax-operator">!</span>event) {
    <span class="syntax-comment">//нет событий</span>
    <span class="syntax-function">delay</span>(<span class="syntax-number">100</span>)
    <span class="syntax-keyword">return</span>
  }

  <span class="syntax-comment">// идемпотентность на уровне обработчика</span>
  <span class="syntax-keyword">const</span> processedId <span class="syntax-operator">=</span> <span class="syntax-string">`${event._id}:executor`</span>
  <span class="syntax-keyword">const</span> processed <span class="syntax-operator">=</span> <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;processed&#39;</span>).<span class="syntax-function">findOne</span>({ _id<span class="syntax-operator">:</span> processedId })

  <span class="syntax-keyword">if</span> (<span class="syntax-operator">!</span>processed) {
    <span class="syntax-comment">// уже делали — пропускаем</span>
    <span class="syntax-keyword">return</span>
  }

  <span class="syntax-keyword">const</span> task <span class="syntax-operator">=</span> <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;tasks&#39;</span>).<span class="syntax-function">findOne</span>({ _id<span class="syntax-operator">:</span> event.payload.taskId, status<span class="syntax-operator">:</span> { $ne<span class="syntax-operator">:</span> <span class="syntax-string">&#39;done&#39;</span> } })

  <span class="syntax-keyword">if</span> (<span class="syntax-operator">!</span>task) {
   <span class="syntax-comment">// таск уже кто-то сделал</span>
    <span class="syntax-keyword">return</span>
  }

  <span class="syntax-keyword">if</span> (task) {
    <span class="syntax-keyword">try</span> {
      <span class="syntax-comment">// делаем таску, помним про идемпотентность</span>
      <span class="syntax-keyword">await</span> <span class="syntax-function">doWork</span>(task)
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;processed&#39;</span>).<span class="syntax-function">insertOne</span>({ _id<span class="syntax-operator">:</span> processedId, at<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>() })
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;tasks&#39;</span>).<span class="syntax-function">updateOne</span>({ _id<span class="syntax-operator">:</span> task._id }, { $set<span class="syntax-operator">:</span> { status<span class="syntax-operator">:</span> <span class="syntax-string">&#39;done&#39;</span> } })
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>).<span class="syntax-function">updateOne</span>({ _id<span class="syntax-operator">:</span> event._id }, { $set<span class="syntax-operator">:</span> { doneAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>() } })
    } <span class="syntax-keyword">catch</span> (error) {
      <span class="syntax-comment">// ретраи с backoff, лимит → DLQ</span>
      <span class="syntax-builtin">console</span>.<span class="syntax-function">error</span>(<span class="syntax-string">`[executor] ${task.id}:`</span>, error)
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>).<span class="syntax-function">updateOne</span>({ _id<span class="syntax-operator">:</span> event._id }, { $set<span class="syntax-operator">:</span> { pickedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span> } })
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;processed&#39;</span>).<span class="syntax-function">deleteOne</span>({ _id<span class="syntax-operator">:</span> processedId })
      <span class="syntax-keyword">await</span> <span class="syntax-function">delay</span>(<span class="syntax-number">100</span>)
    }
  }
}
<span class="syntax-comment">// executor запускается setInterval или setTimeout выше по архитектуре</span></code></pre>
<p>В примере важное: Задача и событие родились <strong>в одном коммите</strong> — значит, не потеряются. Исполнитель читает из той же БД — меньше инфраструктуры, меньше точек отказа. Цена — ты сам хозяин ретраев и DLQ, но это как раз то, что нужно контролировать.</p>
<p>Уже слышу крики: &quot;А как же изоляция, границы, связность, какой &quot;ужос&quot; два сервиса работают с одной бд. Как же Чистая архитектура?!&quot; Нормально, отвечу я, когда два сервиса читают одну БД: один владеет записью и инвариантами, остальные читают только стабильную проекцию/реплику по контракту — вот и вся изоляция. Ну это мы отвлеклись. Продолжим.</p>
<p>Вариант Б — RabbitMQ (внешняя очередь)</p>
<p>Наивно делать «insert в БД и затем publish в Rabbit» нельзя: между этими шагами всегда есть окна потерь/дублей. Значит… сюрприз… <strong>всё равно нужен outbox</strong>. Просто вместо прямого чтения его, будет «ретранслятор в Rabbit».</p>
<pre data-language="js"><code class="language-js"><span class="syntax-comment">// Сервис &quot;Планирователь&quot;</span>
<span class="syntax-keyword">export</span> <span class="syntax-keyword">async</span> <span class="syntax-keyword">function</span> <span class="syntax-function">createTask</span> (client, db, task) {
<span class="syntax-comment">//createTask такой же как и в Варианте А, только меняем немного запись</span>
<span class="syntax-comment">//...</span>
  <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>).<span class="syntax-function">insertOne</span>(
    {
      _id<span class="syntax-operator">:</span> eventId,
      type<span class="syntax-operator">:</span> <span class="syntax-string">&#39;task.created&#39;</span>,
      payload<span class="syntax-operator">:</span> { taskId<span class="syntax-operator">:</span> task.id },
      createdAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>(),
      pickedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span>,
      publishedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span>
    },
    { session }
  )
<span class="syntax-comment">//...</span>
}

<span class="syntax-comment">// но тут нам нужен еще relay - штука по доставке события из бд в очередь</span>

<span class="syntax-comment">/**
 * @param {import(&#39;mongodb&#39;).Db} db
 * @param {import(&#39;amqplib&#39;).Channel} rmqChannel
 * @param {string} exchange
 * @returns {Promise&lt;void&gt;}
 */</span>
<span class="syntax-keyword">export</span> <span class="syntax-keyword">async</span> <span class="syntax-keyword">function</span> <span class="syntax-function">relay</span> (db, rmqChannel, exchange) {
  <span class="syntax-keyword">const</span> event <span class="syntax-operator">=</span> <span class="syntax-keyword">await</span> db
    .<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>)
    .<span class="syntax-function">findOneAndUpdate</span>(
      { type<span class="syntax-operator">:</span> <span class="syntax-string">&#39;task.created&#39;</span>, pickedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span> },
      { $set<span class="syntax-operator">:</span> { pickedAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>() } },
      { sort<span class="syntax-operator">:</span> { createdAt<span class="syntax-operator">:</span> <span class="syntax-number">1</span> }, returnDocument<span class="syntax-operator">:</span> <span class="syntax-string">&#39;after&#39;</span> }
    )

  <span class="syntax-keyword">if</span> (<span class="syntax-operator">!</span>event) {
    <span class="syntax-keyword">await</span> <span class="syntax-function">delay</span>(<span class="syntax-number">50</span>)
    <span class="syntax-keyword">return</span>
  }

  <span class="syntax-keyword">try</span> {
    <span class="syntax-keyword">await</span> <span class="syntax-function">publishConfirm</span>(rmqChannel, exchange, <span class="syntax-string">&#39;task.created&#39;</span>, <span class="syntax-builtin">Buffer</span>.<span class="syntax-keyword">from</span>(<span class="syntax-builtin">JSON</span>.<span class="syntax-function">stringify</span>(event)))
    <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>).<span class="syntax-function">updateOne</span>({ _id<span class="syntax-operator">:</span> event._id }, { $set<span class="syntax-operator">:</span> { publishedAt<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>() } })
  } <span class="syntax-keyword">catch</span> (e) {
    <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;outbox&#39;</span>).<span class="syntax-function">updateOne</span>({ _id<span class="syntax-operator">:</span> event._id }, { $set<span class="syntax-operator">:</span> { pickedAt<span class="syntax-operator">:</span> <span class="syntax-literal">null</span> } })
    <span class="syntax-keyword">await</span> <span class="syntax-function">sleep</span>(<span class="syntax-number">100</span>)
  }
}

<span class="syntax-comment">// Сервис &quot;Делатель&quot;</span>
<span class="syntax-comment">/**
 * @param {import(&#39;mongodb&#39;).Db} db
 * @param {Object} msg
 */</span>
<span class="syntax-keyword">export</span> <span class="syntax-keyword">async</span> <span class="syntax-keyword">function</span> <span class="syntax-function">consumeAndExecute</span> (db, msg) {
  <span class="syntax-keyword">const</span> event <span class="syntax-operator">=</span> <span class="syntax-builtin">JSON</span>.<span class="syntax-function">parse</span>(msg.content.<span class="syntax-function">toString</span>())

  <span class="syntax-comment">// идемпотентность на уровне обработчика</span>
  <span class="syntax-keyword">const</span> processedId <span class="syntax-operator">=</span> <span class="syntax-string">`${event._id}:executor`</span>
  <span class="syntax-keyword">const</span> processed <span class="syntax-operator">=</span> <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;processed&#39;</span>).<span class="syntax-function">findOne</span>({ _id<span class="syntax-operator">:</span> processedId })

  <span class="syntax-keyword">if</span> (<span class="syntax-operator">!</span>processed) {
    <span class="syntax-comment">//дубль, уже исполнили</span>
    <span class="syntax-keyword">return</span> <span class="syntax-string">&#39;ack&#39;</span>
  }

  <span class="syntax-keyword">const</span> task <span class="syntax-operator">=</span> <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;tasks&#39;</span>).<span class="syntax-function">findOne</span>({ _id<span class="syntax-operator">:</span> event.payload.taskId, status<span class="syntax-operator">:</span> { $ne<span class="syntax-operator">:</span> <span class="syntax-string">&#39;done&#39;</span> } })

  <span class="syntax-keyword">if</span> (<span class="syntax-operator">!</span>task) {
    <span class="syntax-keyword">return</span> <span class="syntax-string">&#39;ack&#39;</span>
  }

  <span class="syntax-keyword">if</span> (task) {
    <span class="syntax-keyword">try</span> {
      <span class="syntax-comment">// делаем таску, помним про идемпотентность</span>
      <span class="syntax-keyword">await</span> <span class="syntax-function">doWork</span>(task)
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;processed&#39;</span>).<span class="syntax-function">insertOne</span>({ _id<span class="syntax-operator">:</span> processedId, at<span class="syntax-operator">:</span> <span class="syntax-builtin">Date</span>.<span class="syntax-function">now</span>() })
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;tasks&#39;</span>).<span class="syntax-function">updateOne</span>({ _id<span class="syntax-operator">:</span> task._id }, { $set<span class="syntax-operator">:</span> { status<span class="syntax-operator">:</span> <span class="syntax-string">&#39;done&#39;</span> } })
      <span class="syntax-keyword">return</span> <span class="syntax-string">&#39;ack&#39;</span>
    } <span class="syntax-keyword">catch</span> (error) {
      <span class="syntax-builtin">console</span>.<span class="syntax-function">error</span>(<span class="syntax-string">`[consumeAndExecute] ${task.id}:`</span>, error)
      <span class="syntax-comment">// ретраи с backoff, DLX/TTL, лимит → DLQ, можно ответить &#39;nack&#39; получим еще раз</span>
      <span class="syntax-keyword">await</span> db.<span class="syntax-function">collection</span>(<span class="syntax-string">&#39;processed&#39;</span>).<span class="syntax-function">deleteOne</span>({ _id<span class="syntax-operator">:</span> processedId })
      <span class="syntax-keyword">return</span> <span class="syntax-string">&#39;ack&#39;</span>
    }
  }
}</code></pre>
<p>А где минусы спросишь ты. Так вот же, в полтора раза больше кода, больше мест, где, что-то может пойти не так. Дополнительный сервис с брокером. Транспорт между брокером и сервисами. Если не делаешь relay — потеряешь события при падениях между БД и брокером. Если не делаешь inbox и идемпотентность — словишь дубль при redelivery/повторной публикации. Rabbit сам по себе не решает твою бизнес-консистентность, он только перевозит байты.</p>
<p>Внутрисистемный асинхрон: Mongo + outbox закрывает 90% кейсов без зоопарка, и даже фан-аут тоже можно сделать. Но если хочешь фан-аут на внешних потребителей из коробки, независимые группы, интеграции или просто очень хочется очередь — бери Rabbit (или другой брокер), <strong>но</strong> не забывай: outbox/inbox, idempotency, confirm-публикации, DLQ. Очередь без этих штук — просто дорогая труба с иллюзией надёжности.</p>
<p><strong>Что в итоге?</strong></p>
<p>Очередь — не дефолт. Это тяжёлая зависимость, которая тащит за собой протоколы, идемпотентность, эволюцию схем, эксплуатацию и новые точки отказа.</p>
<p>Нужен буфер? Сделай backpressure и честные 429. Нужна обработка «не в запросе»? Outbox и воркер. Нужен событийный продукт между доменами? И тут все еще можно обойтись без брокера, но уже можно задуматься про него, выбрать, закатать рукава и решиться нырнуть в асинхронный персональный ад.</p>
<p>Не тащи очереди «потому что так делают все». Тащи их, когда без них <strong>не сходится модель отказов и нагрузок</strong>. Во всех остальных случаях это просто ещё один слой боли.</p>
<p>Хочешь поспорить — покажи, где у тебя backpressure, где идемпотентность и как ты репроцессишь хвост. Если ответ «никак» — очередь тебе не помогла, она просто спрятала проблему в тень.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
