<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:media="http://search.yahoo.com/mrss/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Мишка на сервере</title>
    <link>https://jtprog.ru/</link>
    <description>Блог Михаила aka jtprogru про SRE, DevOps и системное администрирование: инженерные заметки, разборы инцидентов и практики эксплуатации.</description>
    <generator>Hugo</generator>
    <language>ru</language>
    <copyright>jtprogru</copyright>
    <lastBuildDate>Mon, 12 Oct 2026 16:24:00 +0300</lastBuildDate>
    <atom:link href="https://jtprog.ru/" rel="self" type="application/rss+xml" /><item>
      <title>Твой IaC тоже зависимость: supply chain инфраструктурных инструментов</title>
      <link>https://jtprog.ru/posts/iac-tooling-supply-chain/</link>
      <pubDate>Mon, 12 Oct 2026 16:24:00 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/iac-tooling-supply-chain/</guid>
      <category>Security</category>
      <category>DevOps</category>
      <category>CI/CD</category>
      <category>Terraform</category>
      <category>Ansible</category>
      <category>Supply Chain</category>
      <category domain="category">DevOps</category>
      <enclosure url="https://jtprog.ru/cover.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! 2026-03-19 в 17:43 UTC в репозитории Trivy появился тег <code>v0.69.4</code>. Релизный пайплайн отработал штатно, бинарники разъехались по GitHub Releases, Docker Hub, GHCR, ECR и deb/rpm-репозиториям. В тот же день почти все теги <code>aquasecurity/trivy-action</code> переехали на чужие коммиты. Сканер уязвимостей, который ставят в пайплайн ради безопасности, начал читать память раннера, собирать SSH-ключи, облачные учётки и токены Kubernetes и отправлять их на <code>scan.aquasecurtiy[.]org</code>. Перечитай имя домена: в нём переставлены две буквы.</p>
<p>Через пять дней в PyPI вышли заражённые релизы LiteLLM. Ломать саму библиотеку не пришлось: её CI ставил Trivy из apt-репозитория без фиксации версии — свежий сканер приехал сам и, по версии самой LiteLLM, унёс токен публикации.</p>
<p>Вход атакующим дала история трёхнедельной давности: в конце февраля через ошибку в GitHub Actions того же репозитория украли токен сервисного аккаунта. Секреты после этого перевыпустили, но не одномоментно, и доступ у атакующих сохранился.</p>
<p>В контурах, с которыми я работал в разных компаниях, провайдеры Terraform приезжали тремя способами: напрямую из публичного реестра, через чужое зеркало и через своё. Про образы приложений такой разнобой уже трудно представить: registry со сканером давно стал нормой. А здесь он никого не смущал. Меня тоже.</p>
<p>Тезис статьи: инструмент, которым ты строишь и проверяешь инфраструктуру, входит в твою цепочку поставки. Прав у него больше, чем у любого сервиса в проде, а проверок по дороге меньше, чем у npm-пакета во фронтенде. За последние пять лет по этому месту били больше десяти раз. Ниже — хроника этих ударов, разбор того, что каждый инструмент проверяет сам, и настройки, которые закрывают большую часть сценариев.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Есть своя история?</p>
    <div class="callout__content">Про инструмент, который оказался дырой в цепочке поставки, расскажи в <a href="/go/chat/?src=iac-tooling-supply-chain">чате</a>. Там же можно поспорить с выводами. Анонсы новых постов выходят в <a href="/go/tg/?src=iac-tooling-supply-chain">канале</a>.</div>
  </div>
</aside>

<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<p>Статья длинная (~61 000 знаков). Сразу — что заберёт каждая роль:</p>
<ul>
<li><strong>SRE, DevOps, платформенная команда.</strong> Таблицу «что инструмент проверяет сам, а что нет» для Terraform, Ansible, Puppet, Salt, Helm и CI. Плюс рабочие настройки зеркала, пина по хэшу и карантина для GitLab CI и GitHub Actions.</li>
<li><strong>Security и AppSec.</strong> Аргументы, почему инфраструктурный тулинг обязан попасть в scope, с датами и ссылками на первичные разборы инцидентов.</li>
<li><strong>Тимлид и руководитель.</strong> Семь шагов по возрастанию цены и <a href="#%d1%87%d1%82%d0%be-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d0%b2-%d0%bf%d0%be%d0%bd%d0%b5%d0%b4%d0%b5%d0%bb%d1%8c%d0%bd%d0%b8%d0%ba">план</a>, где они разложены по срокам: что делается за час, за день, за неделю и за квартал. И раздел о том, где эти меры не помогают.</li>
</ul>
<h2 id="tldr-если-читать-дальше-уже-некогда">TL;DR (если читать дальше уже некогда)</h2>
<ul>
<li>Провайдер Terraform, коллекция Ansible, модуль Puppet, экшен в CI и линтер в pre-commit исполняют чужой код с правами пайплайна или инженера. Это зависимости, причём самые привилегированные из всех.</li>
<li>С 2021 по 2026 год их ломали через угнанные токены мейнтейнеров, перезаписанные теги, тайпсквоттинг и инфраструктуру реестров. В 2026-м атаки стали прицельными: Trivy, KICS, реестр Coder, поддельные провайдеры.</li>
<li>Хэш зависимости между запусками из коробки запоминает только Terraform, и только для провайдеров. Модули, роли, экшены по тегу и хуки pre-commit не сверяются ни с чем.</li>
<li>У российских команд есть вторая ветка той же проблемы: после блокировок 2022–2024 годов многие ходят за провайдерами и образами через чужие зеркала.</li>
<li>Лечится это тем же, чем давно лечат зависимости приложений: своё зеркало, карантин новых версий, пин по хэшу, раздельные раннеры. Придумывать новое не нужно.</li>
</ul>
<h2 id="привилегированный-слепой-угол">Привилегированный слепой угол</h2>
<h3 id="что-я-называю-инфраструктурным-тулингом">Что я называю инфраструктурным тулингом</h3>
<p>Всё, что исполняется, пока ты строишь, меняешь или проверяешь инфраструктуру, и что написал не ты. Список длиннее, чем хотелось бы.</p>
<table>
	<thead>
			<tr>
					<th>Класс</th>
					<th>Примеры</th>
					<th>Откуда приезжает</th>
					<th>Где и с какими правами исполняется</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Провайдеры и модули IaC</td>
					<td>провайдеры Terraform и OpenTofu, модули из реестра и из git</td>
					<td><code>registry.terraform.io</code>, <code>registry.opentofu.org</code>, GitHub</td>
					<td>раннер или ноутбук, ключи облака из окружения</td>
			</tr>
			<tr>
					<td>Содержимое систем управления конфигурацией</td>
					<td>коллекции и роли Ansible, модули Puppet, формулы и расширения Salt</td>
					<td>Galaxy, Forge, PyPI, GitHub</td>
					<td>control node или master, root на каждом управляемом хосте</td>
			</tr>
			<tr>
					<td>Обвязка Kubernetes</td>
					<td>Helm-чарты и плагины, плагины <code>kubectl</code> из krew</td>
					<td>OCI-реестры, GitHub Releases</td>
					<td>контекст инженера или деплой-раннера, часто <code>cluster-admin</code></td>
			</tr>
			<tr>
					<td>Кирпичи CI</td>
					<td>GitHub Actions, компоненты и <code>include</code> в GitLab CI, образы для джобов</td>
					<td>Marketplace, CI/CD Catalog, Docker Hub</td>
					<td>раннер, все секреты и токен джобы</td>
			</tr>
			<tr>
					<td>Линтеры и сканеры</td>
					<td>Trivy, KICS, Checkov, <code>tflint</code>, <code>ansible-lint</code>, хуки pre-commit</td>
					<td>GitHub Releases, apt, pip, образы</td>
					<td>раннер или ноутбук, читают весь репозиторий и окружение</td>
			</tr>
			<tr>
					<td>Установщики</td>
					<td><code>curl ... | sh</code>, bootstrap-скрипты, apt- и yum-репозитории вендоров</td>
					<td>сайт вендора</td>
					<td>root на хосте в момент установки</td>
			</tr>
	</tbody>
</table>
<p>Последняя строка выглядит мелочью, пока не вспомнишь, что LiteLLM заразили именно через неё.</p>
<h3 id="чем-это-отличается-от-зависимости-приложения">Чем это отличается от зависимости приложения</h3>
<p>Зависимость приложения получает ровно те права, которые есть у сервиса: скомпрометированный пакет в сервисе корзины дотянется до базы корзины и её секретов. Неприятно, но у ущерба есть граница — и рисовал её ты сам.</p>
<p>У инфраструктурного инструмента такой границы нет и быть не может: он существует, чтобы менять всё — создавать сети, выдавать роли, раскатывать конфигурацию на весь парк. Отсюда и права:</p>
<ul>
<li>Провайдер Terraform — отдельный бинарник, который Terraform запускает дочерним процессом. Он наследует окружение, а в окружении лежат ключи облака.</li>
<li>Плагины из коллекции Ansible исполняются на control node, а модули уезжают на хосты и работают там под <code>become</code>.</li>
<li>Модуль Puppet приносит Ruby-код, который выполняется на сервере при компиляции каталога, и факты, которые исполняются от root на каждом агенте.</li>
<li>Расширение Salt — pip-пакет, который ставится на master и миньоны. На миньоне он работает от root, на master — с правами процесса, который раздаёт команды всему парку.</li>
<li>Экшен в CI исполняется в том же раннере, что и твои шаги, и может прочитать память процесса со всеми секретами джобы. Именно так работала нагрузка в атаках на <code>tj-actions</code> и Trivy.</li>
<li>Хук pre-commit запускается на ноутбуке инженера, где лежат kubeconfig, SSH-ключи и живая сессия облачного CLI.</li>
</ul>
<h3 id="terraform-plan-не-read-only"><code>terraform plan</code> не read-only</h3>
<p>На одном заблуждении держится половина пайплайнов: <code>plan</code> безопасен, он же ничего не меняет, только показывает разницу. Поэтому plan запускают автоматически на любой merge request, а строгие проверки вешают на apply.</p>
<p>В ноябре 2024 года Эллиот Уорд из Snyk Labs <a href="https://labs.snyk.io/resources/gitflops-dangers-of-terraform-automation-platforms" rel="noopener noreferrer" target="_blank">проверил пять платформ автоматизации Terraform</a>: Terraform Cloud, Atlantis, Digger, Env0 и Terrateam. Все пять в конфигурации по умолчанию позволяли исполнить произвольный код на этапе plan. Способов два. Первый — data-источник <code>external</code>, который по определению запускает локальную программу. Второй — собственный провайдер: Terraform скачает его на <code>init</code> и запустит на <code>plan</code>. В эксперименте с Terraform Cloud исследователь получил шелл в контейнере и увидел там AWS-ключи, которыми разворачивается настоящая инфраструктура.</p>
<p>Проверить первое можно за минуту на своём ноутбуке:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;external&#34; &#34;innocent&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  program</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sh&#34;, &#34;-c&#34;, &#34;id &gt; plan-was-here.txt; echo &#39;{}&#39;&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform init <span class="o">&amp;&amp;</span> terraform plan
</span></span><span class="line"><span class="cl">cat plan-was-here.txt</span></span></code></pre></div>
</div>
<p>На Terraform 1.15 план отвечает <code>No changes. Your infrastructure matches the configuration</code>, а файл с выводом <code>id</code> уже лежит рядом. Вместо <code>id</code> там может стоять что угодно.</p>
<p>Блока такого вида в чужом merge request достаточно. Его же 2026-08-31 нашли в модулях, которые 14 часов отдавались <a href="https://github.com/coder/coder/security/advisories/GHSA-vx42-ghc9-gw65" rel="noopener noreferrer" target="_blank">с адреса реестра Coder</a>: через доступ к Cloudflare атакующие добавили в пул свои серверы, и часть запросов уходила к ним.</p>
<h3 id="что-обычно-проверяют">Что обычно проверяют</h3>
<p>Сначала точка отсчёта. В компании со средней зрелостью безопасность цепочки поставки, она же шестой столп <a href="/posts/digital-immune-system/#%d1%88%d0%b5%d1%81%d1%82%d1%8c-%d1%81%d1%82%d0%be%d0%bb%d0%bf%d0%be%d0%b2-dis">Digital Immune System</a>, означает примерно такой набор:</p>
<ul>
<li><strong>Lock-файлы и SCA.</strong> Зависимости приложения зафиксированы, сканер сверяет их с базами уязвимостей на каждом merge request.</li>
<li><strong>SAST и DAST.</strong> Первый смотрит свой код, второй — собранное приложение.</li>
<li><strong>Зеркало пакетов.</strong> npm, PyPI, Maven и NuGet идут через Nexus или Artifactory, прямой выход в публичные реестры из CI закрыт.</li>
<li><strong>Карантин.</strong> Новая версия пакета становится доступна сборкам через несколько дней или недель, когда на неё уже наступил кто-то другой.</li>
<li><strong>Свой registry образов</strong> со сканером на входе.</li>
<li><strong>Подписи и SBOM.</strong> Cosign для образов, CycloneDX или SPDX для состава, admission-политика в кластере, которая не пускает неподписанное.</li>
</ul>
<p>Набор рабочий, спорить с ним я не собираюсь. Но все шесть пунктов отвечают на вопрос «что мы выпускаем», а вопрос «чем мы это собираем, раскатываем и проверяем» в scope не входит вовсе. Граница проходит по коду приложения и его зависимостям — всё по другую сторону пайплайна считается средой.</p>
<h3 id="перевёрнутая-пирамида">Перевёрнутая пирамида</h3>
<p>Наложи этот набор на права — и пирамида переворачивается: чем больше прав у артефакта, тем меньше из перечисленного до него доходит.</p>
<table>
	<thead>
			<tr>
					<th>Что приезжает</th>
					<th>Права при исполнении</th>
					<th>Что обычно проверяют</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>npm-пакет фронтенда</td>
					<td>сборочный контейнер</td>
					<td>lock-файл, SCA, карантин в зеркале</td>
			</tr>
			<tr>
					<td>Образ сервиса</td>
					<td>namespace сервиса</td>
					<td>свой registry, сканер, подпись, admission-политика</td>
			</tr>
			<tr>
					<td>Helm-чарт инфраструктурного компонента</td>
					<td>кластер</td>
					<td>иногда зеркало</td>
			</tr>
			<tr>
					<td>Провайдер Terraform</td>
					<td>облачный аккаунт целиком</td>
					<td>lock-файл, если его не забыли закоммитить</td>
			</tr>
			<tr>
					<td>Коллекция Ansible, модуль Puppet, формула Salt</td>
					<td>root на всём парке</td>
					<td>версия в requirements, если повезло</td>
			</tr>
			<tr>
					<td>Экшен или <code>include</code> в CI</td>
					<td>все секреты джобы</td>
					<td>тег</td>
			</tr>
			<tr>
					<td>Линтер в pre-commit</td>
					<td>ноутбук инженера</td>
					<td>тег</td>
			</tr>
	</tbody>
</table>
<p>Security-команда смотрит на верх таблицы: там код, который пишет компания, и привычные сканеры. Низ таблицы числится «инструментами», а инструменту вопрос «откуда ты это взял» задают редко. Про популярный npm-пакет так перестали думать после <a href="https://github.com/dominictarr/event-stream/issues/116" rel="noopener noreferrer" target="_blank">event-stream</a> и <a href="https://www.bleepingcomputer.com/news/security/big-sabotage-famous-npm-package-deletes-files-to-protest-ukraine-war/" rel="noopener noreferrer" target="_blank">node-ipc</a>. Про популярный провайдер думают до сих пор.</p>
<h2 id="хроника-20212026">Хроника 2021–2026</h2>
<p>В таблицу попало только то, что било по инструментам сборки, доставки и проверки инфраструктуры. Обычные заражённые пакеты из npm, PyPI и других реестров я не считал: <a href="https://www.sonatype.com/press-releases/sonatypes-10th-annual-state-of-the-software-supply-chain-report" rel="noopener noreferrer" target="_blank">по подсчёту Sonatype</a>, с 2019 года по осень 2024-го их набралось больше 700 тысяч. Последняя колонка забегает вперёд: в ней мера из практической части, которая закрыла бы сценарий на твоей стороне.</p>
<table>
	<thead>
			<tr>
					<th>Когда</th>
					<th>Что случилось</th>
					<th>Вектор</th>
					<th>Чем закрывается</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2021-01 … 04</td>
					<td>Bash Uploader у Codecov два месяца периодически подменяли, изменённая версия отправляла наружу окружение CI. У HashiCorp утёк GPG-ключ подписи релизов</td>
					<td>скрипт по <code>curl | bash</code> без сверки хэша</td>
					<td>пин и сверка контрольной суммы</td>
			</tr>
			<tr>
					<td>2021-07</td>
					<td>Через Kaseya VSA шифровальщик REvil разошёлся по клиентам нескольких десятков MSP</td>
					<td>уязвимость в сервере управления</td>
					<td>сегментация, права агента</td>
			</tr>
			<tr>
					<td>2023-01</td>
					<td>CircleCI: украдена сессия инженера, из платформы вынесли секреты клиентов</td>
					<td>CI-платформа как хранилище секретов</td>
					<td>короткоживущие учётки вместо статических</td>
			</tr>
			<tr>
					<td>2023-06</td>
					<td>BoostSecurity нашла в Terraform Registry несколько сотен уязвимых модулей, у многих можно перехватить уже опубликованные версии</td>
					<td>подвижные теги, <code>pull_request_target</code></td>
					<td>пин модулей по коммиту</td>
			</tr>
			<tr>
					<td>2024-11</td>
					<td>Snyk показала исполнение кода на <code>terraform plan</code> в пяти платформах</td>
					<td>plan на недоверенном коде</td>
					<td>allowlist провайдеров, отдельный раннер</td>
			</tr>
			<tr>
					<td>2025-03</td>
					<td>Перезаписаны тег <code>v1</code> у <code>reviewdog/action-setup</code> и все теги <code>tj-actions/changed-files</code>, секреты уходили в логи сборок</td>
					<td>подвижный тег экшена</td>
					<td>пин по SHA коммита</td>
			</tr>
			<tr>
					<td>2025-08</td>
					<td>Заражённые версии Nx искали секреты с помощью локальных AI-ассистентов</td>
					<td>токен публикации, украденный через CI</td>
					<td>карантин новых версий</td>
			</tr>
			<tr>
					<td>2026-02 … 03</td>
					<td>Бот hackerbot-claw за десять дней атаковал как минимум семь репозиториев, у Trivy украли токен и снесли релизы</td>
					<td><code>pull_request_target</code> в CI самого инструмента</td>
					<td>зеркало бинарников</td>
			</tr>
			<tr>
					<td>2026-03-19</td>
					<td>Trivy: перезаписаны теги экшенов, выпущен заражённый <code>v0.69.4</code></td>
					<td>неполная ротация учёток после прошлого инцидента</td>
					<td>пин по SHA, карантин, egress-фильтр</td>
			</tr>
			<tr>
					<td>2026-03-23</td>
					<td>KICS: 35 тегов экшена на четыре часа подменены стилером</td>
					<td>судя по всему, угнанный сервисный аккаунт</td>
					<td>пин по SHA</td>
			</tr>
			<tr>
					<td>2026-03-24</td>
					<td>LiteLLM: заражённые релизы в PyPI</td>
					<td>сканер из apt без фиксации версии</td>
					<td>пин версии инструмента</td>
			</tr>
			<tr>
					<td>2026-06 … 07</td>
					<td>CVE-2026-11332 и CVE-2026-16493 в <code>ansible-galaxy</code>: установка роли исполняет команды, для коллекций описана та же ошибка</td>
					<td>подстановка аргументов в <code>git clone</code></td>
					<td>свой Galaxy с проверенным содержимым</td>
			</tr>
			<tr>
					<td>2026-07</td>
					<td>Найден троянизированный провайдер <code>terraform-provider-awsbeta</code></td>
					<td>поддельный провайдер</td>
					<td>allowlist пространств имён, зеркало</td>
			</tr>
			<tr>
					<td>2026-08-31</td>
					<td>Реестр Coder 14 часов раздавал Terraform-модули со стилером</td>
					<td>угнанный Cloudflare-ключ, чужие серверы за доменом реестра</td>
					<td>своё зеркало модулей, сверка содержимого</td>
			</tr>
			<tr>
					<td>2026-09</td>
					<td>Провайдер <code>kreuzwenker/docker</code>, тайпсквот <code>kreuzwerker/docker</code></td>
					<td>имя с опечаткой</td>
					<td>allowlist, зеркало</td>
			</tr>
	</tbody>
</table>
<p>Теперь по периодам, с деталями, которые в таблицу не влезли.</p>
<h3 id="2021-скрипт-из-интернета-и-сервер-управления">2021: скрипт из интернета и сервер управления</h3>
<p>С 2021-01-31 по 2021-04-01 Bash Uploader, который Codecov предлагал запускать в CI одной строкой, периодически подменяли: изменённая версия отправляла на сторонний сервер переменные окружения сборки. <a href="https://about.codecov.io/security-update/" rel="noopener noreferrer" target="_blank">Заметили подмену не сканеры</a>: клиент сверил контрольную сумму скачанного скрипта с опубликованной, и они не сошлись. До этого два месяца о расхождении никто не сообщал.</p>
<p>Среди пострадавших — HashiCorp. В её CI лежал приватный GPG-ключ, которым подписаны файлы <code>SHA256SUMS</code> всех релизов, и по нему же <code>terraform init</code> проверяет официальные провайдеры HashiCorp. <a href="https://discuss.hashicorp.com/t/hcsec-2021-12-codecov-security-event-and-hashicorp-gpg-key-exposure/23512" rel="noopener noreferrer" target="_blank">Ключ отозвали</a>, следов злоупотребления не нашли, но <a href="https://discuss.hashicorp.com/t/terraform-updates-for-hcsec-2021-12/23570" rel="noopener noreferrer" target="_blank">Terraform пришлось обновлять</a>: в старые версии зашит прежний ключ, и релизы провайдеров, подписанные новым, они проверить не могут. Это первый случай в хронике, когда удар по вспомогательному инструменту CI дотянулся до корня доверия всей IaC-экосистемы.</p>
<p>Вторая история того же года формально про другое. 2021-07-02 группа REvil воспользовалась уязвимостью в серверах <a href="https://en.wikipedia.org/wiki/Kaseya_VSA_ransomware_attack" rel="noopener noreferrer" target="_blank">Kaseya VSA</a>. VSA — система удалённого управления: агент стоит на каждой машине, сервер умеет исполнить на них что угодно. По <a href="https://www.kaseya.com/press-release/kaseya-responds-swiftly-to-sophisticated-cyberattack-mitigating-global-disruption-to-customers/" rel="noopener noreferrer" target="_blank">оценке самой Kaseya</a>, напрямую пострадали около 50 клиентов. Многие клиенты Kaseya — MSP, то есть подрядчики, которые администрируют чужую инфраструктуру, и через них пострадали ещё от 800 до 1 500 компаний. Шведская сеть Coop в первый же день <a href="https://www.bbc.com/news/technology-57707530" rel="noopener noreferrer" target="_blank">закрыла больше половины</a> из своих 800 магазинов. Подрядчик в роли входной двери — сюжет знакомый: через него же <a href="/posts/hacking-aeroflot/#%d0%bf%d0%be%d0%b4%d1%80%d1%8f%d0%b4%d1%87%d0%b8%d0%ba-%d0%ba%d0%b0%d0%ba-%d0%b2%d1%85%d0%be%d0%b4%d0%bd%d0%b0%d1%8f-%d0%b4%d0%b2%d0%b5%d1%80%d1%8c">зашли в «Аэрофлот»</a>.</p>
<p>VSA — не Ansible и не Salt, но архитектура та же: центральный узел, которому root на всём парке положен по должности. С Salt ровно это случилось годом раньше: через <a href="https://nvd.nist.gov/vuln/detail/CVE-2020-11651" rel="noopener noreferrer" target="_blank">CVE-2020-11651</a> в открытые в интернет мастера заходили без аутентификации и <a href="https://www.tenable.com/blog/cve-2020-11651-cve-2020-11652-critical-salt-framework-vulnerabilities-exploited-in-the-wild" rel="noopener noreferrer" target="_blank">раскатывали майнеры</a> по всем миньонам.</p>
<h3 id="20232024-ci-оказался-сейфом-а-к-реестрам-появились-вопросы">2023–2024: CI оказался сейфом, а к реестрам появились вопросы</h3>
<p>2023-01-04 CircleCI попросила всех клиентов немедленно сменить все секреты, которые хранились в платформе. Из <a href="https://circleci.com/blog/jan-4-2023-incident-report" rel="noopener noreferrer" target="_blank">отчёта</a>: 2022-12-16 малварь на ноутбуке инженера украла сессию с уже пройденной двухфакторной аутентификацией, 2022-12-22 через неё вынесли переменные окружения, токены и ключи клиентов. Данные были зашифрованы, но ключи шифрования атакующий достал из работающего процесса.</p>
<p>Вывод неприятный. CI хранит ключи от всего, что умеет твоя автоматизация, а значит, статический ключ облака в переменной CI рано или поздно утечёт вместе с CI.</p>
<p>В июне 2023 года Франсуа Пру из BoostSecurity <a href="https://labs.boostsecurity.io/articles/erosion-of-trust-unmasking-supply-chain-vulnerabilities-in-the-terraform-registry" rel="noopener noreferrer" target="_blank">посмотрел на Terraform Registry</a> глазами атакующего. Главное наблюдение: модули, в отличие от провайдеров, lock-файлом не покрыты. Версия модуля — это git-тег, а тег можно передвинуть, поэтому план, который ты ревьюил в merge request, и план, который применится через час, могут не совпасть. Из 13 с лишним тысяч модулей реестра несколько сотен оказались уязвимы, и во многих случаях это позволяло перехватить уже опубликованные версии. В январе 2024 года HashiCorp <a href="https://discuss.hashicorp.com/t/hcsec-2024-04-terraform-registry-module-supply-chain-security-improvements/62815" rel="noopener noreferrer" target="_blank">закрыла в публичном реестре две дыры</a>: подвижные теги и перехват переименованного репозитория. Версия модуля теперь привязана к SHA коммита, сохранённому при публикации, а сама публикация — к неизменяемому ID репозитория. Уязвимые workflow в репозиториях модулей остались заботой их авторов, и модуль, подключённый напрямую из git по тегу, эта защита тоже не покрывает.</p>
<p>2024-03-29 Андрес Фройнд публично рассказал о бэкдоре в xz-utils 5.6.0 и 5.6.1. В таблице его нет, потому что xz не инструмент инфраструктуры, но <a href="https://research.swtch.com/xz-timeline" rel="noopener noreferrer" target="_blank">эта история</a> изменила сам разговор. Человек под именем Jia Tan почти полтора года присылал нормальные патчи, прежде чем выпустил свой первый релиз, и ещё почти год вёл проект до бэкдора. После xz довод «этим пользуются все» перестал работать как доказательство безопасности.</p>
<h3 id="2025-теги-оказались-переменными">2025: теги оказались переменными</h3>
<p>2025-03-11 между 18:42 и 20:31 UTC тег <code>v1</code> экшена <code>reviewdog/action-setup</code> указывал на коммит, который выгружал секреты раннера в лог сборки. Этим экшеном пользовался CI проекта <code>tj-actions</code>, и через него, скорее всего, утёк токен бота с правом записи. 2025-03-14 атакующий передвинул все теги <code>tj-actions/changed-files</code> на свой коммит. <a href="https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction" rel="noopener noreferrer" target="_blank">CISA</a> просит проверить все проекты, где экшен запускался с 2025-03-12 по 2025-03-15, а найденные в логах секреты считать скомпрометированными.</p>
<p>Экшен стоял в 23 000 репозиториев. <a href="https://www.endorlabs.com/learn/blast-radius-of-the-tj-actions-changed-files-supply-chain-attack" rel="noopener noreferrer" target="_blank">Endor Labs</a> нашла 5 416 репозиториев со ссылкой на него: за сутки атаки workflow запускался в 614, а секреты попали в лог в 218, и в основном это короткоживущие <code>GITHUB_TOKEN</code>. По данным <a href="https://unit42.paloaltonetworks.com/github-actions-supply-chain-attack/" rel="noopener noreferrer" target="_blank">Unit 42</a>, изначально целились в проект <code>coinbase/agentkit</code>, а все теги атакующий перезаписал через двадцать минут после того, как там убрали уязвимый workflow. Почему он пошёл на шумную массовую атаку, в Unit 42 не знают.</p>
<p>Одну деталь этой истории запомни, она вернётся в разделе про ограничения. Кто закрепил <code>tj-actions/changed-files</code> по SHA коммита, не пострадал — если только в эти дни не передвинул пин на заражённый коммит. С экшенами reviewdog пин спасал не всегда: составные экшены внутри себя ссылались на <code>reviewdog/action-setup@v1</code> по тегу.</p>
<p>2025-08-26 в npm вышли заражённые версии Nx, системы сборки монорепозиториев. Токен публикации, по <a href="https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c" rel="noopener noreferrer" target="_blank">advisory проекта</a>, украли через workflow на <code>pull_request_target</code>, который подставлял заголовок pull request в шелл-скрипт. По <a href="https://www.wiz.io/blog/s1ngularitys-aftermath" rel="noopener noreferrer" target="_blank">разбору Wiz</a>, postinstall-скрипт искал на машине ключи и токены, а для разведки вызывал установленные AI-ассистенты: Claude Code, Gemini CLI и Amazon Q с флагами, отключающими подтверждения. Найденное складывалось в публичный репозиторий <code>s1ngularity-repository</code> в аккаунте самой жертвы. Утёкших секретов в Wiz насчитали больше двух тысяч. Для сверки по номерам: инциденту присвоена <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-10894" rel="noopener noreferrer" target="_blank">CVE-2025-10894</a>, и она опубликована. Запись GHSA-cxm3-wv7p-598c в <a href="https://github.com/advisories/GHSA-cxm3-wv7p-598c" rel="noopener noreferrer" target="_blank">общей базе GitHub</a> с 2026-07-28 помечена отозванной, причина не названа. Advisory в репозитории проекта при этом на месте.</p>
<h3 id="2026-по-инструментам-бьют-прицельно">2026: по инструментам бьют прицельно</h3>
<p>С 2026-02-21 по 2026-03-02 аккаунт hackerbot-claw, который представлялся «автономным агентом для исследований безопасности», <a href="https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation" rel="noopener noreferrer" target="_blank">атаковал как минимум семь репозиториев</a> через ошибки в GitHub Actions. В списке два инструмента проверки инфраструктуры: Trivy и <code>DataDog/datadog-iac-scanner</code>. Токен у Trivy украли 2026-02-27 через workflow на <code>pull_request_target</code>. Тот pull request, по <a href="https://labs.boostsecurity.io/articles/megagame10418-the-user-behind-hackerbot-claw" rel="noopener noreferrer" target="_blank">разбору BoostSecurity</a>, открыл другой аккаунт, которого на GitHub уже нет, а hackerbot-claw пришёл в тот же workflow сутки спустя. Мейнтейнеры Trivy считают, что за обоими стоит один атакующий. Дальше с этим токеном репозиторий переименовали и закрыли, релизы с 0.27.0 по 0.69.1 удалили, а в расширение для VS Code на Open VSX залили подозрительный артефакт. Для потребителей первый удар выглядел как отказ: пайплайны, которые скачивали бинарник Trivy с GitHub Releases, перестали его находить.</p>
<p>Aqua сменила учётные данные, но не разом: по её же <a href="https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23" rel="noopener noreferrer" target="_blank">advisory</a>, ротация была не одномоментной, и атакующий мог успеть подсмотреть уже новые секреты. 2026-03-19 этим воспользовалась группа, которая называет себя TeamPCP. По <a href="https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack" rel="noopener noreferrer" target="_blank">данным Wiz</a>, перезаписаны 75 из 76 тегов <code>trivy-action</code> (в advisory Aqua — 76 из 77) и семь тегов <code>setup-trivy</code>. В 17:43 UTC атакующий запушил тег <code>v0.69.4</code>, и к 18:22 автоматика выложила релиз с тем же стилером. 2026-03-20 по npm пошёл самораспространяющийся червь CanisterWorm: в <a href="https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise" rel="noopener noreferrer" target="_blank">Aikido</a> считают его прямым продолжением атаки на Trivy и работой той же группы. 2026-03-22 на Docker Hub появились ещё два заражённых образа, <code>0.69.5</code> и <code>0.69.6</code>.</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/342dd828c33262b51ed8223cd3bc05f00b26fb2de146fb8e5925092981cbb1a7.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-342dd828c332-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 936px; background-color: transparent;" viewBox="4 4 936 291.25" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-342dd828c332-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-342dd828c332-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-342dd828c332-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-342dd828c332-light .error-icon{fill:#dce0e8;}#m-342dd828c332-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-342dd828c332-light .edge-thickness-normal{stroke-width:1px;}#m-342dd828c332-light .edge-thickness-thick{stroke-width:3.5px;}#m-342dd828c332-light .edge-pattern-solid{stroke-dasharray:0;}#m-342dd828c332-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-342dd828c332-light .edge-pattern-dashed{stroke-dasharray:3;}#m-342dd828c332-light .edge-pattern-dotted{stroke-dasharray:2;}#m-342dd828c332-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-342dd828c332-light .marker.cross{stroke:#5c5f77;}#m-342dd828c332-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-342dd828c332-light p{margin:0;}#m-342dd828c332-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-342dd828c332-light .cluster-label text{fill:#4c4f69;}#m-342dd828c332-light .cluster-label span{color:#4c4f69;}#m-342dd828c332-light .cluster-label span p{background-color:transparent;}#m-342dd828c332-light .label text,#m-342dd828c332-light span{fill:#4c4f69;color:#4c4f69;}#m-342dd828c332-light .node rect,#m-342dd828c332-light .node circle,#m-342dd828c332-light .node ellipse,#m-342dd828c332-light .node polygon,#m-342dd828c332-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-342dd828c332-light .rough-node .label text,#m-342dd828c332-light .node .label text,#m-342dd828c332-light .image-shape .label,#m-342dd828c332-light .icon-shape .label{text-anchor:middle;}#m-342dd828c332-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-342dd828c332-light .rough-node .label,#m-342dd828c332-light .node .label,#m-342dd828c332-light .image-shape .label,#m-342dd828c332-light .icon-shape .label{text-align:center;}#m-342dd828c332-light .node.clickable{cursor:pointer;}#m-342dd828c332-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-342dd828c332-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-342dd828c332-light .edgePaths .path{stroke:#5c5f77;stroke-width:1px;}#m-342dd828c332-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-342dd828c332-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-342dd828c332-light .edgeLabel p{background-color:#eff1f5;}#m-342dd828c332-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-342dd828c332-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-342dd828c332-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-342dd828c332-light .cluster text{fill:#4c4f69;}#m-342dd828c332-light .cluster span{color:#4c4f69;}#m-342dd828c332-light .node .collapsed-indicator{fill:#ccd0da;stroke:none;opacity:0.6;}#m-342dd828c332-light .node .collapsed-separator{stroke:#ccd0da;stroke-width:0.75px;}#m-342dd828c332-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-342dd828c332-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-342dd828c332-light rect.text{fill:none;stroke-width:0;}#m-342dd828c332-light .icon-shape,#m-342dd828c332-light .image-shape{background-color:#eff1f5;text-align:center;}#m-342dd828c332-light .icon-shape p,#m-342dd828c332-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-342dd828c332-light .icon-shape .label rect,#m-342dd828c332-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-342dd828c332-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-342dd828c332-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-342dd828c332-light .node .neo-node{stroke:#bcc0cc;}#m-342dd828c332-light [data-look="neo"].node rect,#m-342dd828c332-light [data-look="neo"].cluster rect,#m-342dd828c332-light [data-look="neo"].node polygon{stroke:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].swimlane.cluster rect{filter:none;}#m-342dd828c332-light [data-look="neo"].node path{stroke:#bcc0cc;stroke-width:1px;}#m-342dd828c332-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-342dd828c332-light [data-look="neo"].node circle{stroke:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-342dd828c332-light [data-look="neo"].icon-shape .icon{fill:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].icon-shape .icon-neo path{stroke:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-342dd828c332-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edges edgePaths"><path d="M164,155L200,155" id="m-342dd828c332-light-L_A_B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_A_B_0" data-points="W3sieCI6MTY0LCJ5IjoxNTV9LHsieCI6MjA0LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M356,155L392,155" id="m-342dd828c332-light-L_B_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_B_C_0" data-points="W3sieCI6MzU2LCJ5IjoxNTV9LHsieCI6Mzk2LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M548,137.125L560.9289321881345,137.125Q568,137.125 568,130.05393218813452L568,76.07106781186548Q568,69 575.0710678118655,69L776,69" id="m-342dd828c332-light-L_C_D_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_D_0" data-points="W3sieCI6NTQ4LCJ5IjoxMzcuMTI1fSx7IngiOjU2OCwieSI6MTM3LjEyNX0seyJ4Ijo1NjgsInkiOjY5fSx7IngiOjc4MCwieSI6Njl9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M548,155L584,155" id="m-342dd828c332-light-L_C_E_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_E_0" data-points="W3sieCI6NTQ4LCJ5IjoxNTV9LHsieCI6NTg4LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M548,172.875L560.9289321881345,172.875Q568,172.875 568,179.94606781186548L568,234.42893218813452Q568,241.5 575.0710678118655,241.5L776,241.5" id="m-342dd828c332-light-L_C_F_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_F_0" data-points="W3sieCI6NTQ4LCJ5IjoxNzIuODc1fSx7IngiOjU2OCwieSI6MTcyLjg3NX0seyJ4Ijo1NjgsInkiOjI0MS41fSx7IngiOjc4MCwieSI6MjQxLjV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(664, 68.25)"><g class="label" data-id="L_C_D_0" transform="translate(-33.3671875, -11.25)"><foreignObject width="66.734375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>вероятно</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(664, 240.75)"><g class="label" data-id="L_C_F_0" transform="translate(-46.1171875, -11.25)"><foreignObject width="92.234375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>та же группа</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-342dd828c332-light-flowchart-A-0" data-look="neo" transform="translate(88, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>02-27: через PR<br />украден токен Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-B-1" data-look="neo" transform="translate(280, 155)"><rect class="basic label-container" style="" x="-76" y="-68.25" width="152" height="136.5"/><g class="label" style="" transform="translate(-60, -56.25)"><rect/><foreignObject width="120" height="112.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-01: раскрытие и ротация,<br />доступ сохранился</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-C-3" data-look="neo" transform="translate(472, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-19: теги trivy-action<br />и релиз v0.69.4</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-D-5" data-look="neo" transform="translate(856, 69)"><rect class="basic label-container" style="" x="-76" y="-57" width="152" height="114"/><g class="label" style="" transform="translate(-60, -45)"><rect/><foreignObject width="120" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-20: червь в npm<br />на токенах из Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-E-7" data-look="neo" transform="translate(664, 155)"><rect class="basic label-container" style="" x="-76" y="-34.5" width="152" height="69"/><g class="label" style="" transform="translate(-60, -22.5)"><rect/><foreignObject width="120" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: nowrap; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-24: LiteLLM<br />в PyPI</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-F-9" data-look="neo" transform="translate(856, 241.5)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-23: теги<br />kics-github-action</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-342dd828c332-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-342dd828c332-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-342dd828c332-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 936px; background-color: transparent;" viewBox="4 4 936 291.25" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-342dd828c332-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-342dd828c332-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-342dd828c332-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-342dd828c332-dark .error-icon{fill:#181926;}#m-342dd828c332-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-342dd828c332-dark .edge-thickness-normal{stroke-width:1px;}#m-342dd828c332-dark .edge-thickness-thick{stroke-width:3.5px;}#m-342dd828c332-dark .edge-pattern-solid{stroke-dasharray:0;}#m-342dd828c332-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-342dd828c332-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-342dd828c332-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-342dd828c332-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-342dd828c332-dark .marker.cross{stroke:#a5adcb;}#m-342dd828c332-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-342dd828c332-dark p{margin:0;}#m-342dd828c332-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-342dd828c332-dark .cluster-label text{fill:#cad3f5;}#m-342dd828c332-dark .cluster-label span{color:#cad3f5;}#m-342dd828c332-dark .cluster-label span p{background-color:transparent;}#m-342dd828c332-dark .label text,#m-342dd828c332-dark span{fill:#cad3f5;color:#cad3f5;}#m-342dd828c332-dark .node rect,#m-342dd828c332-dark .node circle,#m-342dd828c332-dark .node ellipse,#m-342dd828c332-dark .node polygon,#m-342dd828c332-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-342dd828c332-dark .rough-node .label text,#m-342dd828c332-dark .node .label text,#m-342dd828c332-dark .image-shape .label,#m-342dd828c332-dark .icon-shape .label{text-anchor:middle;}#m-342dd828c332-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-342dd828c332-dark .rough-node .label,#m-342dd828c332-dark .node .label,#m-342dd828c332-dark .image-shape .label,#m-342dd828c332-dark .icon-shape .label{text-align:center;}#m-342dd828c332-dark .node.clickable{cursor:pointer;}#m-342dd828c332-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-342dd828c332-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-342dd828c332-dark .edgePaths .path{stroke:#a5adcb;stroke-width:1px;}#m-342dd828c332-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-342dd828c332-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-342dd828c332-dark .edgeLabel p{background-color:#24273a;}#m-342dd828c332-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-342dd828c332-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-342dd828c332-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-342dd828c332-dark .cluster text{fill:#cad3f5;}#m-342dd828c332-dark .cluster span{color:#cad3f5;}#m-342dd828c332-dark .node .collapsed-indicator{fill:#363a4f;stroke:none;opacity:0.6;}#m-342dd828c332-dark .node .collapsed-separator{stroke:#363a4f;stroke-width:0.75px;}#m-342dd828c332-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-342dd828c332-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-342dd828c332-dark rect.text{fill:none;stroke-width:0;}#m-342dd828c332-dark .icon-shape,#m-342dd828c332-dark .image-shape{background-color:#24273a;text-align:center;}#m-342dd828c332-dark .icon-shape p,#m-342dd828c332-dark .image-shape p{background-color:#24273a;padding:2px;}#m-342dd828c332-dark .icon-shape .label rect,#m-342dd828c332-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-342dd828c332-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-342dd828c332-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-342dd828c332-dark .node .neo-node{stroke:#494d64;}#m-342dd828c332-dark [data-look="neo"].node rect,#m-342dd828c332-dark [data-look="neo"].cluster rect,#m-342dd828c332-dark [data-look="neo"].node polygon{stroke:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].swimlane.cluster rect{filter:none;}#m-342dd828c332-dark [data-look="neo"].node path{stroke:#494d64;stroke-width:1px;}#m-342dd828c332-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-342dd828c332-dark [data-look="neo"].node circle{stroke:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-342dd828c332-dark [data-look="neo"].icon-shape .icon{fill:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].icon-shape .icon-neo path{stroke:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-342dd828c332-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edges edgePaths"><path d="M164,155L200,155" id="m-342dd828c332-dark-L_A_B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_A_B_0" data-points="W3sieCI6MTY0LCJ5IjoxNTV9LHsieCI6MjA0LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M356,155L392,155" id="m-342dd828c332-dark-L_B_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_B_C_0" data-points="W3sieCI6MzU2LCJ5IjoxNTV9LHsieCI6Mzk2LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M548,137.125L560.9289321881345,137.125Q568,137.125 568,130.05393218813452L568,76.07106781186548Q568,69 575.0710678118655,69L776,69" id="m-342dd828c332-dark-L_C_D_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_D_0" data-points="W3sieCI6NTQ4LCJ5IjoxMzcuMTI1fSx7IngiOjU2OCwieSI6MTM3LjEyNX0seyJ4Ijo1NjgsInkiOjY5fSx7IngiOjc4MCwieSI6Njl9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M548,155L584,155" id="m-342dd828c332-dark-L_C_E_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_E_0" data-points="W3sieCI6NTQ4LCJ5IjoxNTV9LHsieCI6NTg4LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M548,172.875L560.9289321881345,172.875Q568,172.875 568,179.94606781186548L568,234.42893218813452Q568,241.5 575.0710678118655,241.5L776,241.5" id="m-342dd828c332-dark-L_C_F_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_F_0" data-points="W3sieCI6NTQ4LCJ5IjoxNzIuODc1fSx7IngiOjU2OCwieSI6MTcyLjg3NX0seyJ4Ijo1NjgsInkiOjI0MS41fSx7IngiOjc4MCwieSI6MjQxLjV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(664, 68.25)"><g class="label" data-id="L_C_D_0" transform="translate(-33.3671875, -11.25)"><foreignObject width="66.734375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>вероятно</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(664, 240.75)"><g class="label" data-id="L_C_F_0" transform="translate(-46.1171875, -11.25)"><foreignObject width="92.234375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>та же группа</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-342dd828c332-dark-flowchart-A-0" data-look="neo" transform="translate(88, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>02-27: через PR<br />украден токен Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-B-1" data-look="neo" transform="translate(280, 155)"><rect class="basic label-container" style="" x="-76" y="-68.25" width="152" height="136.5"/><g class="label" style="" transform="translate(-60, -56.25)"><rect/><foreignObject width="120" height="112.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-01: раскрытие и ротация,<br />доступ сохранился</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-C-3" data-look="neo" transform="translate(472, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-19: теги trivy-action<br />и релиз v0.69.4</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-D-5" data-look="neo" transform="translate(856, 69)"><rect class="basic label-container" style="" x="-76" y="-57" width="152" height="114"/><g class="label" style="" transform="translate(-60, -45)"><rect/><foreignObject width="120" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-20: червь в npm<br />на токенах из Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-E-7" data-look="neo" transform="translate(664, 155)"><rect class="basic label-container" style="" x="-76" y="-34.5" width="152" height="69"/><g class="label" style="" transform="translate(-60, -22.5)"><rect/><foreignObject width="120" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: nowrap; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-24: LiteLLM<br />в PyPI</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-F-9" data-look="neo" transform="translate(856, 241.5)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-23: теги<br />kics-github-action</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-342dd828c332-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-342dd828c332-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs></svg>
</div>
</figure><p>2026-03-23 с 12:58 до 16:50 UTC то же произошло с <a href="https://www.wiz.io/blog/teampcp-attack-kics-github-action" rel="noopener noreferrer" target="_blank"><code>kics-github-action</code></a>: 35 тегов указывали на стилер. KICS — сканер IaC от Checkmarx. На следующий день в PyPI вышли <a href="https://research.jfrog.com/post/litellm-compromised-teampcp/" rel="noopener noreferrer" target="_blank">LiteLLM 1.82.7 и 1.82.8</a>. В 1.82.8 лежал <code>.pth</code>-файл, который запускал сбор учётных данных при каждом старте интерпретатора Python. По <a href="https://docs.litellm.ai/blog/security-update-march-2026" rel="noopener noreferrer" target="_blank">версии самой LiteLLM</a>, токен публикации утёк через Trivy в её CI. JFrog в том же разборе показывает как: скрипт проверок ставил сканер из apt-репозитория, всегда последнюю версию.</p>
<p>2026-06-05 раскрыта <a href="https://github.com/advisories/GHSA-w8p5-mx5w-cpqj" rel="noopener noreferrer" target="_blank">CVE-2026-11332</a>. Команда <code>ansible-galaxy role install</code> передавала поля <code>src</code> и <code>name</code> из <code>meta/requirements.yml</code> роли в <code>git clone</code> без разделителя <code>--</code>. Автор вредоносной роли мог так составить запись о зависимости, чтобы подставить опции git и выполнить команду на машине того, кто роль ставит. Для ролей это <a href="https://github.com/ansible/ansible/pull/87070" rel="noopener noreferrer" target="_blank">исправили</a> в ansible-core 2.21.1 и бэкпортировали в 2.20.7, 2.19.11, 2.18.18 и 2.16.19. Исправление оказалось неполным: <a href="https://github.com/advisories/GHSA-m37p-hmh2-w6j2" rel="noopener noreferrer" target="_blank">CVE-2026-16493</a> от 2026-07-21 описывает ту же ошибку в <code>ansible-galaxy collection install</code>. В upstream на 2026-10-12 она не закрыта: <a href="https://github.com/ansible/ansible/pull/87177" rel="noopener noreferrer" target="_blank">патч</a> висит с конца июня, мейнтейнеры считают его перестраховкой и сомневаются, что через коллекции до уязвимого вызова можно добраться. Red Hat при этом оценила уязвимость как Important и <a href="https://access.redhat.com/errata/RHSA-2026:63386" rel="noopener noreferrer" target="_blank">числит её исправленной</a> только в Satellite. Сведений об эксплуатации я не нашёл. Важен сам класс: код исполняется на этапе установки, до первого запуска плейбука.</p>
<p>Летом и осенью дошла очередь до Terraform, причём тремя разными способами.</p>
<p>В июле Zscaler <a href="https://www.zscaler.com/blogs/security-research/suspected-tradertraitor-group-uses-trojanized-terraform-provider-deliver" rel="noopener noreferrer" target="_blank">нашла <code>terraform-provider-awsbeta</code></a>. Провайдер ведёт себя как настоящий: внутри рабочий каркас с примерами ресурса и data-источника, Terraform получает от него штатные ответы. При запуске он заодно скачивает загрузчик с <code>hashicorp-terraform[.]io</code> и ставит бэкдор FLATROOF. Исследователи осторожно связывают кампанию с группой TraderTraitor и не скрывают, что не знают, как именно этот провайдер попал к жертве. Вероятный путь описан в смежном <a href="https://www.sentinelone.com/labs/dont-call-us-well-call-your-apis-tradertraitor-backdoors-resurface-on-victim-with-no-crypto-ties/" rel="noopener noreferrer" target="_blank">разборе SentinelLabs</a>: фальшивое собеседование и тестовое задание, в репозитории которого лежит Terraform-проект с подложенным <code>.terraform.lock.hcl</code> и провайдером из реестра на домене-двойнике.</p>
<p>2026-08-31 с 07:35 до 21:45 UTC часть запросов к <code>registry.coder.com</code> уходила на серверы атакующего. Сам реестр не взламывали: украли Cloudflare API-ключ и добавили свои адреса в пул, который отвечает за домен. Модули приезжали настоящие, но с лишним блоком <code>data &quot;external&quot;</code>. Он отправлял на <code>coder-infra[.]com</code> переменные окружения и секреты провижинера, а при сборке workspace ещё и OIDC-токен пользователя, его SSH-ключ и токены внешней аутентификации. В <a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-coder-registry-terraform-supply-chain-2026/" rel="noopener noreferrer" target="_blank">разборе CSA</a> есть фраза, которую стоит запомнить: одной фиксации версии модуля мало, чтобы такую подмену обнаружить.</p>
<p>В начале сентября были опубликованы провайдеры <code>gocommunity-io/dockerd</code> и <code>kreuzwenker/docker</code>. Второй отличается от популярного <code>kreuzwerker/docker</code> одной буквой. Вредоносный код просыпался, только если SHA-256 от двух переменных конфигурации совпадал с зашитым значением, то есть ждал конкретную жертву. <a href="https://www.aikido.dev/blog/graphalgo-terraform-go-modules" rel="noopener noreferrer" target="_blank">Aikido</a> описала находку 2026-09-22.</p>
<h3 id="что-видно-из-хроники">Что видно из хроники</h3>
<p><strong>Вспомогательный инструмент перестал быть побочной жертвой.</strong> В 2021-м Codecov ломали ради секретов его клиентов. В 2026-м ломают сам сканер, потому что он стоит в тысячах пайплайнов и ему разрешено читать всё.</p>
<p><strong>Сканеры и линтеры стали самой выгодной целью.</strong> Codecov, reviewdog, Trivy, KICS, <code>datadog-iac-scanner</code> — пять инструментов проверки, по которым били за пять лет. Их ставят ради безопасности, им выдают доступ ко всему репозиторию и окружению, а сами они не проходят ни одной проверки.</p>
<p><strong>Векторов всего четыре, и они повторяются:</strong></p>
<ul>
<li>подвижная ссылка: тег, <code>latest</code>, скрипт по URL;</li>
<li>CI самого инструмента, чаще всего <code>pull_request_target</code>;</li>
<li>инфраструктура доставки: реестр, DNS, бакет;</li>
<li>имя, похожее на настоящее.</li>
</ul>
<p><strong>Окно обнаружения сжалось.</strong> Codecov жил с подменой два месяца, <code>tj-actions</code> — меньше суток, Coder — 14 часов, KICS — четыре часа. Для карантина новых версий это хорошая новость: теперь хватает дней, а не месяцев. Но треть строк в таблице описывает подмену уже существующей версии, а против неё выдержка бессильна — помогает только хэш.</p>
<p><strong>В четырёх историях 2026 года есть домен-двойник.</strong> У Trivy украденное уходило на <code>aquasecurtiy</code>, у Coder — на <code>coder-infra</code>, у KICS — на домен, похожий на Checkmarx, а у поддельного провайдера двойник <code>hashicorp-terraform[.]io</code> раздавал загрузчик. Раннер, которому разрешено ходить только на известные адреса, поднял бы тревогу в первых трёх историях. Совсем закрыть утечку такой фильтр не обещает: у стилера Trivy был запасной канал через репозиторий в аккаунте самой жертвы.</p>
<h2 id="вторая-ветка-тебя-не-взломали-тебе-закрыли-дверь">Вторая ветка: тебя не взломали, тебе закрыли дверь</h2>
<p>Цепочка поставки рвётся и без атакующего. За те же пять лет поставщики инфраструктурных инструментов несколько раз меняли правила, и каждый раз кто-то узнавал об этом по упавшему пайплайну.</p>
<table>
	<thead>
			<tr>
					<th>Когда</th>
					<th>Что изменилось</th>
					<th>Как это выглядело у потребителя</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2022-03</td>
					<td>HashiCorp закрыла <code>registry.terraform.io</code> и <code>releases.hashicorp.com</code> для российских IP</td>
					<td><code>Failed to query available provider packages</code> на <code>terraform init</code></td>
			</tr>
			<tr>
					<td>2023-08</td>
					<td>Terraform сменил лицензию с MPL на BSL, появился форк OpenTofu</td>
					<td>ничего не упало, но выбор инструмента стал юридическим вопросом</td>
			</tr>
			<tr>
					<td>2024-05-30</td>
					<td>Docker Hub начал отвечать 403 российским IP, через несколько дней доступ вернулся</td>
					<td><code>docker pull</code> падает на базовых образах</td>
			</tr>
			<tr>
					<td>2024-08</td>
					<td>OpenTofu закрыл реестр для российских IP и удалил провайдеры Yandex Cloud, Cloud.ru (провайдер <code>sbercloud</code>) и Rustack</td>
					<td>провайдер твоего же облака пропал из реестра</td>
			</tr>
			<tr>
					<td>2024-10 … 2025-01</td>
					<td>Репозитории Salt переехали на инфраструктуру Broadcom, старые адреса и bootstrap-поддомены отключены</td>
					<td>bootstrap-скрипт не находит пакеты</td>
			</tr>
			<tr>
					<td>2024-11 … 2025-01</td>
					<td>Perforce объявила, что с начала 2025 года новые пакеты Puppet уходят в закрытый репозиторий, сообщество ответило форком OpenVox</td>
					<td>в публичном репозитории нет новых версий</td>
			</tr>
			<tr>
					<td>2025-08 … 09</td>
					<td>Bitnami перенесла версионированные образы в <code>bitnamilegacy</code> без обновлений</td>
					<td><code>ErrImagePull</code> у чартов с закреплённым тегом</td>
			</tr>
			<tr>
					<td>2026-05</td>
					<td>В Salt 3008 из поставки убрали сотни модулей (в <a href="https://saltproject.io/blog/the-great-salt-module-migration/" rel="noopener noreferrer" target="_blank">анонсе 2024 года</a> называли около 750), часть из них стала отдельными расширениями</td>
					<td>стейт ссылается на модуль, которого больше нет</td>
			</tr>
	</tbody>
</table>
<p>Ни одна строка не про взлом, а последствия те же: в какой-то день твой пайплайн зависит от решения, которое принял не ты. Лекарство тоже совпадает: копия того, от чего ты зависишь, должна лежать у тебя.</p>
<p>По первой строке: отдельного анонса я не нашёл, но 2022-03-01 сотрудник HashiCorp подтвердил запрет в <a href="https://github.com/hashicorp/terraform/issues/30591" rel="noopener noreferrer" target="_blank">issue про <code>releases.hashicorp.com</code></a>. Про реестр то же самое как факт пишет <a href="https://yandex.cloud/ru/docs/troubleshooting/terraform/known-issues/failed-to-quety-available-provider-packages" rel="noopener noreferrer" target="_blank">документация Yandex Cloud</a>, правда, без даты. Остальное подтверждается открытыми источниками: <a href="https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license" rel="noopener noreferrer" target="_blank">смена лицензии Terraform</a>, <a href="https://habr.com/ru/articles/818565/" rel="noopener noreferrer" target="_blank">блокировка Docker Hub</a>, <a href="https://github.com/opentofu/opentofu/blob/ea558d9d4b5ef4919356aabc8f3bc3d23af1d853/TSC_SUMMARY.md" rel="noopener noreferrer" target="_blank">решение о блокировке</a> и <a href="https://github.com/opentofu/registry/pull/817" rel="noopener noreferrer" target="_blank">удаление провайдеров</a> в OpenTofu, <a href="https://saltproject.io/blog/salt-project-package-repo-migration-and-guidance/" rel="noopener noreferrer" target="_blank">переезд репозиториев Salt</a>, <a href="https://saltproject.io/blog/salt-bootstrap-breakage/" rel="noopener noreferrer" target="_blank">поломка bootstrap</a> и <a href="https://docs.saltproject.io/en/latest/topics/releases/3008.0.html" rel="noopener noreferrer" target="_blank">release notes 3008</a>, <a href="https://www.puppet.com/blog/open-source-puppet-updates-2025" rel="noopener noreferrer" target="_blank">объявление Perforce</a> и <a href="https://voxpupuli.org/blog/2025/01/21/openvox-release/" rel="noopener noreferrer" target="_blank">первый релиз OpenVox</a>, <a href="https://github.com/bitnami/charts/issues/35164" rel="noopener noreferrer" target="_blank">объявление Bitnami</a>.</p>
<p>Для российских команд у этой ветки есть продолжение, о котором редко говорят вслух. После марта 2022-го зеркало для провайдеров появилось почти у всех, кто работает из России. Только чаще всего чужое. Вот блок из <a href="https://yandex.cloud/ru/docs/tutorials/infrastructure-management/terraform-quickstart#configure-provider" rel="noopener noreferrer" target="_blank">документации Yandex Cloud</a>, который с тех пор живёт в <code>~/.terraformrc</code> у многих:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">provider_installation</span> {
</span></span><span class="line"><span class="cl">  <span class="k">network_mirror</span> {
</span></span><span class="line"><span class="cl"><span class="n">    url</span>     <span class="o">=</span> <span class="s2">&#34;https://terraform-mirror.yandexcloud.net/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    include</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;registry.terraform.io/*/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">  <span class="k">direct</span> {
</span></span><span class="line"><span class="cl"><span class="n">    exclude</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;registry.terraform.io/*/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>К самому зеркалу у меня претензий нет, задачу доступности оно решает честно. Но с этого момента в цепочке поставки появилось новое звено, и доверие к нему нигде не записано. Кто его наполняет? Как быстро туда попадает отозванная версия? Что будет, если по этому адресу начнёт отвечать кто-то другой? История Coder показывает, что последний вопрос не теоретический.</p>
<p>С образами после 2024-05-30 вышло ещё проще. Списки «рабочих зеркал Docker Hub» разошлись по чатам за сутки — вперемешку зеркала крупных облаков и сервисы, поднятые неизвестно кем за вечер. Строка в <code>registry-mirrors</code> означает, что оператор зеркала может отдать тебе любой образ под любым тегом.</p>
<p>Обе ветки сходятся в одной точке: и от атаки на поставщика, и от его решения спасает одно и то же — своя копия и доверие, привязанное к содержимому, а не к имени и адресу. Прежде чем строить это у себя, полезно понять, что каждый инструмент проверяет сам.</p>
<h2 id="анатомия-доверия-что-инструмент-проверяет-сам">Анатомия доверия: что инструмент проверяет сам</h2>
<p>У менеджера зависимостей есть три уровня проверки: целостность доставки (приехало то, что отдал сервер), неизменность (сегодня приехало то же, что вчера) и происхождение (это выпустил тот, за кого оно себя выдаёт). Ниже — разбор по инструментам: какой уровень работает из коробки, какой включается руками, а какого нет вовсе. Вывод команд я снимал на Terraform 1.15, ansible-core 2.21, Helm 4.3 и pre-commit 4.6.</p>
<h3 id="провайдеры-terraform-и-opentofu">Провайдеры Terraform и OpenTofu</h3>
<p>Лучший случай во всём разделе, и у него всё равно три оговорки.</p>
<p>При установке из исходного реестра Terraform сверяет пакет с файлом <code>SHA256SUMS</code> и проверяет подпись этого файла GPG-ключом издателя. В выводе <code>terraform init</code> это одна строка:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installed hashicorp/external v2.4.2 (signed by HashiCorp)</span></span></code></pre></div>
</div>
<p>После этого в <code>.terraform.lock.hcl</code> ложатся хэши пакета: один <code>h1:</code> по содержимому и по одному <code>zh:</code> на zip каждой платформы (у меня получилось тринадцать). При следующем <code>init</code> несовпадение даёт ошибку <code>doesn't match any of the checksums previously recorded in the dependency lock file</code>. Документация честно называет эту модель trust on first use.</p>
<p>OpenTofu здесь слабее. Вывод <code>tofu</code> я не снимал, но в <a href="https://opentofu.org/docs/cli/plugins/signing/" rel="noopener noreferrer" target="_blank">документации</a> сказано прямо: из своего основного реестра он по умолчанию ставит и провайдеры без GPG-ключа, пропуская проверку подписи. Строгий режим включается переменной <code>OPENTOFU_ENFORCE_GPG_VALIDATION=true</code>.</p>
<p>Оговорка первая: подпись говорит только, что пакет выпустил владелец пространства имён. Вот настоящий, не поддельный провайдер Docker:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installed kreuzwerker/docker v4.6.0 (self-signed, key ID 0DCE698927DAF8EC)</span></span></code></pre></div>
</div>
<p>У тайпсквота <code>kreuzwenker/docker</code> строка была бы такой же, с другим ID ключа. Наизусть этот ID не помнит никто.</p>
<p>Оговорка вторая: через зеркало подпись не проверяется совсем. Тот же <code>hashicorp/external</code> из локального зеркала:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installed hashicorp/external v2.4.2 (unauthenticated)
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Warning: Incomplete lock file information for providers</span></span></code></pre></div>
</div>
<p>В lock-файл при этом попадает один хэш вместо четырнадцати, и это хэш того, что отдало зеркало. Собрал lock-файл через зеркало — поверил зеркалу на слово. При настроенном зеркале подписанные суммы издателя приносит только <code>terraform providers lock</code>: он игнорирует <code>provider_installation</code> и идёт в исходный реестр. По <a href="https://developer.hashicorp.com/terraform/cli/commands/providers/lock" rel="noopener noreferrer" target="_blank">документации</a>, официальные контрольные суммы под подписью разработчика отдаёт только исходный реестр.</p>
<p>Оговорка третья: вся конструкция работает, пока lock-файл лежит в git. Проверь <code>.gitignore</code> в инфраструктурных репозиториях: <code>.terraform.lock.hcl</code> иногда попадает туда по ошибке вместе с каталогом <code>.terraform</code>.</p>
<h3 id="модули-terraform">Модули Terraform</h3>
<p>Здесь проверять нечего, и в <a href="https://developer.hashicorp.com/terraform/language/files/dependency-lock" rel="noopener noreferrer" target="_blank">документации</a> это написано прямо: lock-файл отслеживает только провайдеры, выбор версий удалённых модулей Terraform не запоминает.</p>
<p>Модуль из реестра приезжает по версии, модуль из git — по <code>ref</code>. Если <code>ref</code> указывает на тег или ветку, содержимое может измениться между двумя <code>terraform init</code> (например, между джобами plan и apply с чистым каталогом). Встроенная защита одна: <code>ref</code> в виде SHA коммита для git-источников. У модулей из реестра нет и её. Остаётся верить реестру, а цену этой веры показала история Coder.</p>
<h3 id="ansible-роли-и-коллекции">Ansible: роли и коллекции</h3>
<p><a href="/posts/ansible-roles/">Роли</a> и коллекции устроены по-разному, и путать их опасно.</p>
<p>Роль — это git-репозиторий или tarball с GitHub. Контрольных сумм и подписей нет. Версия в <code>requirements.yml</code> задаётся тегом, веткой или коммитом, и неизменяем из них только коммит.</p>
<p>Коллекция — tarball с Galaxy-сервера. Сервер отдаёт SHA-256 артефакта, клиент сверяет скачанное. Внутри лежат <code>MANIFEST.json</code> и <code>FILES.json</code> с суммами каждого файла, поэтому <code>ansible-galaxy collection verify</code> умеет сравнить установленное с тем, что лежит на сервере. Это целостность доставки. А вот неизменности нет: lock-файла у Ansible не существует, и транзитивные зависимости коллекции разрешаются заново при каждой установке.</p>
<p>Происхождение закрывают GPG-подписи <code>MANIFEST.json</code>. Клиент их поддерживает, для этого есть ключи <code>--keyring</code> и <code>--required-valid-signature-count</code>. Только подписи должен отдать сервер. Я спросил публичный Galaxy про <code>community.general</code> 13.5.0, одну из самых популярных коллекций:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/index/community/general/versions/13.5.0/&#34;</span> <span class="p">|</span> jq .signatures
</span></span><span class="line"><span class="cl"><span class="c1"># []</span></span></span></code></pre></div>
</div>
<p>Подписей нет. Работают они там, где сервер сам подписывает содержимое: в Automation Hub или в собственном Galaxy NG. То есть как раз в своём зеркале.</p>
<p>Остаётся сама установка. В 2026 году в <code>ansible-galaxy install</code> закрыли подстановку аргументов git через зависимости роли: это CVE-2026-11332, закрытая в ansible-core 2.21.1 и в бэкпортах для старых веток. Для установки коллекции из git заведена парная CVE-2026-16493, и в upstream на 2026-10-12 она не исправлена: в ansible-core 2.21.5 разделителя <code>--</code> перед адресом по-прежнему нет. Мейнтейнеры, правда, сомневаются, что до этого вызова можно добраться с вредоносным адресом. Плагины коллекции в любом случае исполняются на control node при первом же запуске плейбука.</p>
<h3 id="puppet-и-salt">Puppet и Salt</h3>
<p>У Puppet модули приезжают с Forge или из git. Подписей нет. В 2021 году Puppet <a href="https://www.puppet.com/blog/malware-scanning" rel="noopener noreferrer" target="_blank">объявил сканирование релизов на малварь</a>, начав с Supported-модулей. По API Forge результата скана сегодня нет ни у community-модулей, ни у релизов 2026 года, так что рассчитывать на него не стоит. В <code>Puppetfile</code> версия модуля с Forge фиксируется точно, git-модуль закрепляется через <code>:commit</code>. У r10k есть полезное побочное свойство: он не разрешает зависимости сам, каждый модуль приходится прописать явно — и неожиданных транзитивных гостей не бывает.</p>
<p>У Salt источников три, и модель доверия у каждого своя.</p>
<p>Формулы — git-репозитории, которые подключают через <code>gitfs_remotes</code>. Мастер сам ходит в репозиторий по расписанию (по умолчанию раз в минуту) и раздаёт миньонам то, что там сейчас лежит. Это подвижная ссылка в чистом виде, и <a href="https://docs.saltproject.io/en/latest/topics/development/conventions/formulas.html" rel="noopener noreferrer" target="_blank">документация Salt</a> прямо просит так не делать: настоятельно рекомендует форкнуть репозиторий формулы к себе, чтобы избежать неожиданных изменений в инфраструктуре.</p>
<p>Расширения — pip-пакеты <code>saltext.*</code> со всеми свойствами pip-пакетов. В Salt 3008, который вышел в мае 2026 года, из поставки убрали сотни модулей, и часть из них теперь ставится расширениями.</p>
<p>Bootstrap-скрипт теперь скачивается с GitHub Releases. Рядом лежит файл с SHA-256, и сверять его нужно руками.</p>
<h3 id="helm-чарты-и-плагины">Helm: чарты и плагины</h3>
<p>Чарт можно подписать: рядом с пакетом кладётся файл <code>.prov</code>, и <code>helm install --verify</code> откажется ставить чарт без него — но по умолчанию флаг выключен, и чарт без подписи ставится молча. У классических HTTP-репозиториев клиент не сверяет скачанный архив даже с <code>digest</code> из <code>index.yaml</code>: на Helm 4.3 <code>helm pull</code> проходит и с подменённым tgz.</p>
<p>У OCI-реестров есть вариант надёжнее: ссылка на чарт по digest вместо тега. Digest неизменяем по построению.</p>
<p>Отдельная ловушка — <code>Chart.lock</code>. Поле <code>digest</code> в нём выглядит как контрольная сумма зависимостей, но это хэш их списка: он отвечает на вопрос «актуален ли lock», а не «то ли содержимое приехало».</p>
<p>Плагины Helm — исполняемый код с хуками установки. В Helm 4 подпись плагина проверяется по умолчанию, но, судя по справке <code>helm plugin install</code>, только при установке из tarball. Плагины <code>kubectl</code> из krew выглядят лучше: манифест в индексе содержит SHA-256 каждого архива, и клиент его сверяет.</p>
<h3 id="кирпичи-ci">Кирпичи CI</h3>
<p><strong>GitHub Actions.</strong> <code>uses: org/action@v4</code> ссылается на тег, а тег подвижен. <code>uses: org/action@</code> с сорока символами SHA ссылается на коммит, и неизменяема только такая ссылка. С августа 2025 года <a href="https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/" rel="noopener noreferrer" target="_blank">политика организации</a> умеет требовать полный SHA и запрещать конкретные экшены. Ограничение то же, что в истории с reviewdog: закреплённый экшен может внутри ссылаться на другой по тегу, а reusable workflows политика тоже не покрывает — их по-прежнему можно подключать по тегу.</p>
<p><strong>GitLab CI.</strong> <code>include: project</code> принимает <code>ref</code>, и туда можно поставить SHA коммита. Компонент из каталога подключается как <code>gitlab.example.internal/group/project/component@версия</code>, вместо версии тоже годится SHA. Для <code>include: remote</code> с GitLab 17.9 есть ключ <a href="https://docs.gitlab.com/ci/yaml/" rel="noopener noreferrer" target="_blank"><code>integrity</code></a> с SHA-256 включаемого файла: не совпало, и пайплайн падает. Образ в <code>image:</code> по тегу подвижен так же, как тег экшена. Неизменяема только форма <code>image: name@sha256:...</code>.</p>
<h3 id="сканеры-линтеры-и-pre-commit">Сканеры, линтеры и pre-commit</h3>
<p>Самая слабая зона. Инструмент попадает в пайплайн одним из четырёх способов: скрипт установки по URL, бинарник с GitHub Releases, пакет из apt-репозитория вендора или готовый образ. У большинства рядом лежат контрольные суммы, у многих есть подписи cosign, но сверка во всех четырёх случаях ручная: пока ты сам не написал эту строку в пайплайне, её нет.</p>
<p>Подписанный apt-репозиторий здесь не спасает. Подпись репозитория защищает от подмены по дороге, а заражённый Trivy <code>v0.69.4</code> собрал и выпустил штатный релизный пайплайн.</p>
<p>В pre-commit поле <code>rev</code> принимает тег, и обычно там тег и стоит. Команда <code>pre-commit autoupdate --freeze</code> пишет вместо тега SHA коммита и оставляет версию в комментарии. Заодно она обновляет каждый хук до последнего тега, об этом ниже.</p>
<h3 id="сводная-таблица">Сводная таблица</h3>
<table>
	<thead>
			<tr>
					<th>Артефакт</th>
					<th>Целостность доставки</th>
					<th>Неизменность между запусками</th>
					<th>Происхождение</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Провайдер Terraform из реестра</td>
					<td>да</td>
					<td>да, если lock-файл в git</td>
					<td>подпись владельца пространства имён</td>
			</tr>
			<tr>
					<td>Провайдер Terraform из зеркала</td>
					<td>да</td>
					<td>да, но хэш выдан зеркалом</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Модуль Terraform</td>
					<td>нет</td>
					<td>только git-<code>ref</code> по SHA</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Роль Ansible</td>
					<td>нет</td>
					<td>только версия по SHA</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Коллекция Ansible</td>
					<td>да</td>
					<td>нет</td>
					<td>подписи, если их отдаёт сервер</td>
			</tr>
			<tr>
					<td>Модуль Puppet</td>
					<td>да</td>
					<td>точная версия в <code>Puppetfile</code></td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Формула Salt через gitfs</td>
					<td>нет</td>
					<td>нет, пока не форкнешь</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Расширение Salt</td>
					<td>как у любого pip-пакета</td>
					<td>как у любого pip-пакета</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Helm-чарт</td>
					<td>только в OCI-реестрах</td>
					<td>digest в OCI</td>
					<td><code>.prov</code>, если включить <code>--verify</code></td>
			</tr>
			<tr>
					<td>Экшен или <code>include</code> в CI</td>
					<td>нет</td>
					<td>SHA коммита, <code>integrity</code></td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Сканер, линтер, хук</td>
					<td>руками</td>
					<td>руками</td>
					<td>руками</td>
			</tr>
	</tbody>
</table>
<p>Средняя колонка объясняет, почему хроника выглядит именно так. Почти в каждой её ячейке стоит «если», «только» или «руками».</p>
<h2 id="свой-контур-где-я-сам-стоял-в-этой-таблице">Свой контур: где я сам стоял в этой таблице</h2>
<p>Теперь про себя. В контурах, с которыми я работал в разных компаниях, картина повторялась с небольшими вариациями, поэтому опишу её собирательно.</p>
<p>Лучше всего было с чартами и образами. В одном из контуров внешний Helm-чарт проходил конвейер из шести шагов:</p>
<table>
	<thead>
			<tr>
					<th>Шаг</th>
					<th>Что делает</th>
					<th>Какой риск закрывает</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>declare</td>
					<td>чарт объявлен строкой <code>repo|chart|version</code> в общем списке</td>
					<td>никто не помнит, откуда это взялось</td>
			</tr>
			<tr>
					<td>validate</td>
					<td>один merge request меняет один чарт</td>
					<td>размытое ревью</td>
			</tr>
			<tr>
					<td>lint</td>
					<td><code>ct lint</code> и yamllint</td>
					<td>кривые values</td>
			</tr>
			<tr>
					<td>test</td>
					<td>установка в эфемерный kind-кластер</td>
					<td>чарт проходит dry-run, но падает при настоящей установке</td>
			</tr>
			<tr>
					<td>release</td>
					<td><code>helm package</code> и <code>helm push</code> в приватный OCI-реестр</td>
					<td>прод тянет чарт из публичного источника</td>
			</tr>
			<tr>
					<td>mirror images</td>
					<td>скрипт вытаскивает образы из <code>values.yaml</code> и кладёт их в свой registry</td>
					<td>образы идут в обход реестра</td>
			</tr>
	</tbody>
</table>
<p>Правило у конвейера одно: прод ничего не тянет из публичного интернета напрямую. Каждый внешний артефакт проходит через одну точку, и эта точка даёт сразу контроль, кэш и след для аудита.</p>
<p>Второе, что работало, — правило previous minor из регламента по инфраструктурному коду. Берём не последнюю вышедшую minor-версию, а предыдущую: свежая успевает отлежаться, а регрессии и сюрпризы ловит кто-то другой. Карантин на голой дисциплине, без единого инструмента. В том же регламенте были <code>CODEOWNERS</code>, защищённые ветки и закоммиченный lock-файл.</p>
<p>А вот про источник провайдеров и модулей там не было ни слова. Провайдеры Terraform, повторюсь, приезжали тремя способами: напрямую из реестра, через чужое зеркало и через своё. Про коллекции Ansible, модули Puppet и формулы Salt вопрос «откуда» звучал ещё реже.</p>
<p>Разложи это по сводной таблице из прошлого раздела, и вылезет перекос: для Helm-чарта построен конвейер из шести шагов, а для провайдера Terraform, у которого в руках облачный аккаунт, не было даже единого источника.</p>
<p>Убирается этот перекос тем же принципом, что у конвейера чартов, только применённым ко всем остальным классам: объявить, закрепить, зеркалировать, выдержать, сверить.</p>
<h2 id="практика-тот-же-конвейер-для-всего-остального">Практика: тот же конвейер для всего остального</h2>
<p>Шагов семь, с нулевого по шестой, и идут они по возрастанию цены. Первые два не требуют ни нового железа, ни согласований. Как всё раскладывается по срокам, от часа до квартала, собрано в конце, в разделе <a href="#%d1%87%d1%82%d0%be-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d0%b2-%d0%bf%d0%be%d0%bd%d0%b5%d0%b4%d0%b5%d0%bb%d1%8c%d0%bd%d0%b8%d0%ba">«Что сделать в понедельник»</a>. Команды для Terraform и Ansible я прогнал на тех же версиях, что и в прошлом разделе.</p>
<h3 id="шаг-0-инвентаризация-что-у-тебя-ходит-наружу">Шаг 0. Инвентаризация: что у тебя ходит наружу</h3>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Terraform: провайдеры и источники модулей</span>
</span></span><span class="line"><span class="cl">terraform get   <span class="c1"># без установленных модулей terraform providers падает</span>
</span></span><span class="line"><span class="cl">terraform providers
</span></span><span class="line"><span class="cl">grep -rhoE <span class="s1">&#39;source[[:space:]]*=[[:space:]]*&#34;[^&#34;]+&#34;&#39;</span> --include<span class="o">=</span><span class="s1">&#39;*.tf&#39;</span> --exclude-dir<span class="o">=</span>.terraform . <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GitHub Actions: всё, что не закреплено по SHA</span>
</span></span><span class="line"><span class="cl">grep -rhoE <span class="s1">&#39;uses:[[:space:]]*[^[:space:]]+&#39;</span> .github/workflows <span class="p">|</span> grep -vE <span class="s1">&#39;@[0-9a-f]{40}&#39;</span> <span class="p">|</span> sort <span class="p">|</span> uniq -c
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GitLab CI: внешние include и образы</span>
</span></span><span class="line"><span class="cl">grep -nE <span class="s1">&#39;^[[:space:]]*-?[[:space:]]*(remote|component|project|image):&#39;</span> .gitlab-ci.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ansible, Puppet, Salt</span>
</span></span><span class="line"><span class="cl">ansible-galaxy collection list
</span></span><span class="line"><span class="cl">ansible-galaxy role list
</span></span><span class="line"><span class="cl">puppet module list --tree
</span></span><span class="line"><span class="cl">salt-pip list <span class="p">|</span> grep -i saltext
</span></span><span class="line"><span class="cl">grep -A10 <span class="s1">&#39;^gitfs_remotes&#39;</span> /etc/salt/master
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># pre-commit и плагины</span>
</span></span><span class="line"><span class="cl">grep -nE <span class="s1">&#39;^[[:space:]]*(- repo|rev):&#39;</span> .pre-commit-config.yaml
</span></span><span class="line"><span class="cl">helm plugin list
</span></span><span class="line"><span class="cl">kubectl krew list</span></span></code></pre></div>
</div>
<p>Самый полезный источник при этом лежит не в репозитории. Возьми лог исходящих соединений раннера за неделю и посмотри, на какие домены он ходит на самом деле. Список всегда длиннее, чем следует из кода.</p>
<h3 id="шаг-1-закрепи-то-что-уже-есть">Шаг 1. Закрепи то, что уже есть</h3>
<p>Строить ничего не нужно — только заменить подвижные ссылки неизменяемыми.</p>
<p><strong>Terraform.</strong> Lock-файл собирается из исходного реестра, с подписями издателя, сразу под все платформы команды и CI:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform providers lock -platform<span class="o">=</span>linux_amd64 -platform<span class="o">=</span>darwin_arm64
</span></span><span class="line"><span class="cl">git add .terraform.lock.hcl</span></span></code></pre></div>
</div>
<p>В CI <code>init</code> запускается так, чтобы lock-файл нельзя было тихо изменить:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform init -input<span class="o">=</span><span class="nb">false</span> -lockfile<span class="o">=</span>readonly</span></span></code></pre></div>
</div>
<p>Модули из git закрепляются по коммиту:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">module</span> <span class="s2">&#34;vpc&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  source</span> <span class="o">=</span><span class="n"> &#34;git::https://git.example.internal/infra/terraform-vpc.git?ref</span><span class="o">=</span><span class="m">4</span><span class="err">f</span><span class="m">2</span><span class="err">a</span><span class="m">9</span><span class="err">c</span><span class="m">1</span><span class="err">e</span><span class="m">8</span><span class="err">b</span><span class="m">7</span><span class="err">d</span><span class="m">6</span><span class="err">a</span><span class="m">5</span><span class="err">f</span><span class="m">4</span><span class="err">e</span><span class="m">3</span><span class="err">d</span><span class="m">2</span><span class="err">c</span><span class="m">1</span><span class="err">b</span><span class="m">0</span><span class="err">a</span><span class="m">9</span><span class="err">f</span><span class="m">8</span><span class="err">e</span><span class="m">7</span><span class="err">d</span><span class="m">6</span><span class="err">c</span><span class="m">5</span><span class="err">b</span><span class="m">4</span><span class="err">a</span><span class="m">39</span><span class="err">&#34;</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p><strong>GitHub Actions.</strong> Тег остаётся в комментарии, чтобы человек понимал, что за версия:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 </span><span class="w"> </span><span class="c"># v4.2.2</span></span></span></code></pre></div>
</div>
<p>Руками это делать не нужно: пресет Renovate <code>helpers:pinGitHubActionDigests</code> расставит SHA и будет обновлять их вместе с комментарием. После этого включи в политике организации требование полного SHA, иначе через месяц кто-нибудь добавит новый экшен по тегу.</p>
<p><strong>GitLab CI.</strong> То же самое для include и образов:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">project</span><span class="p">:</span><span class="w"> </span><span class="l">platform/ci-templates</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">9b1d4c7e2a6f8035c1e4d7a9b2f6c8e0d3a5b7c1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">file</span><span class="p">:</span><span class="w"> </span><span class="l">/terraform/plan.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">remote</span><span class="p">:</span><span class="w"> </span><span class="l">https://templates.example.internal/lint.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">integrity</span><span class="p">:</span><span class="w"> </span><span class="l">sha256-&lt;base64&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.example.internal/tools/terraform@sha256:&lt;digest&gt;</span></span></span></code></pre></div>
</div>
<p>Значение для <code>integrity</code> считается так:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">openssl dgst -sha256 -binary lint.yml <span class="p">|</span> openssl base64 -A</span></span></code></pre></div>
</div>
<p><strong>pre-commit.</strong> Есть команда, которая расставляет SHA сама:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pre-commit autoupdate --freeze</span></span></code></pre></div>
</div>
<p>У неё есть подвох: она сначала обновляет каждый хук до последнего тега и только потом замораживает. Для шага «закрепи то, что есть» это обновление в обход карантина. Закрепить текущую версию без обновления можно руками: SHA тега показывает <code>git ls-remote &lt;repo&gt; 'refs/tags/&lt;тег&gt;*'</code>, для аннотированного тега нужна строка с <code>^{}</code>.</p>
<p><strong>Ansible.</strong> Точные версии коллекций, роли из своего git по коммиту:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">collections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">community.general</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">13.5.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">roles</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">src</span><span class="p">:</span><span class="w"> </span><span class="l">https://git.example.internal/mirrors/ansible-role-nginx.git</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">scm</span><span class="p">:</span><span class="w"> </span><span class="l">git</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l">7c3e1a9f5b2d8e4c6a0f1b3d5e7c9a2b4d6f8e01</span></span></span></code></pre></div>
</div>
<p><strong>Puppet.</strong> Точная версия для Forge, коммит для git:</p>
<div class="codeblock codeblock--ruby" data-lang="ruby"><span class="codeblock__lang" aria-hidden="true">ruby</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">mod</span> <span class="s1">&#39;puppetlabs-stdlib&#39;</span><span class="p">,</span> <span class="s1">&#39;9.6.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">mod</span> <span class="s1">&#39;nginx&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">git</span><span class="p">:</span>    <span class="s1">&#39;https://git.example.internal/mirrors/puppet-nginx.git&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">commit</span><span class="p">:</span> <span class="s1">&#39;1e5a9c3d7b2f6e0a4c8d1b5f9e3a7c2d6b0f4e8a&#39;</span></span></span></code></pre></div>
</div>
<p><strong>Salt.</strong> Формулы форкаются в свой git, и ветка, на которую смотрит <code>gitfs_remotes</code>, обновляется только через merge request. Расширения ставятся по файлу с хэшами:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">salt-pip install --require-hashes -r saltext-requirements.txt</span></span></code></pre></div>
</div>
<p>Хэши понадобятся для всего дерева зависимостей, а не только для самих расширений.</p>
<p><strong>Сканеры и прочие бинарники.</strong> Версия и контрольная сумма записаны в репозитории, сверка идёт до запуска:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSLO <span class="s2">&#34;https://tools.example.internal/trivy/0.69.3/trivy_0.69.3_Linux-64bit.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;&lt;sha256&gt;  trivy_0.69.3_Linux-64bit.tar.gz&#34;</span> <span class="p">|</span> sha256sum -c -</span></span></code></pre></div>
</div>
<p>Строка <code>apt-get install trivy</code> без версии в CI — ровно то, через что, по разбору JFrog, утёк токен публикации LiteLLM.</p>
<h3 id="шаг-2-своё-зеркало-оно-же-allowlist">Шаг 2. Своё зеркало, оно же allowlist</h3>
<p><strong>Terraform.</strong> Зеркало провайдеров — это каталог со статикой. Наполняется одной командой с машины, у которой есть доступ к исходному реестру:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform providers mirror -platform<span class="o">=</span>linux_amd64 -platform<span class="o">=</span>darwin_arm64 /srv/tf-mirror</span></span></code></pre></div>
</div>
<p>Команда зеркалит провайдеры текущей конфигурации, поэтому запускают её в каталоге со сводным <code>required_providers</code>. Дальше каталог отдаёт любой веб-сервер по HTTPS, а в <code>~/.terraformrc</code> раннеров и инженеров пишется:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">provider_installation</span> {
</span></span><span class="line"><span class="cl">  <span class="k">network_mirror</span> {
</span></span><span class="line"><span class="cl"><span class="n">    url</span> <span class="o">=</span> <span class="s2">&#34;https://tf-mirror.example.internal/&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Блока <code>direct</code> здесь нет, и это главное. Terraform перестаёт ходить в исходный реестр, а провайдер, которого нет в зеркале, просто не ставится:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Failed to query available provider packages
</span></span><span class="line"><span class="cl">Could not retrieve the list of available versions for provider
</span></span><span class="line"><span class="cl">kreuzwerker/docker: provider registry.terraform.io/kreuzwerker/docker was not
</span></span><span class="line"><span class="cl">found in any of the search locations</span></span></code></pre></div>
</div>
<p>Зеркало без <code>direct</code> само работает как allowlist. Тайпсквот не установится: его туда никто не клал. Провайдер, собранный атакующим под merge request, тоже. А если в зеркале нет <code>hashicorp/external</code>, блок <code>data &quot;external&quot;</code> из чужого merge request упадёт ещё на <code>init</code>.</p>
<p>Подпись издателя при этом не теряется. Lock-файл из шага 1 собран из исходного реестра, и <code>init -lockfile=readonly</code> через зеркало пройдёт, только если пакет из зеркала совпал с подписанным хэшем. Зеркало отвечает за доступность и за список разрешённого, lock-файл — за содержимое.</p>
<p><strong>Ansible.</strong> В <code>ansible.cfg</code> остаётся один сервер, свой, и включается обязательная подпись:</p>
<div class="codeblock codeblock--ini" data-lang="ini"><span class="codeblock__lang" aria-hidden="true">ini</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[galaxy]</span>
</span></span><span class="line"><span class="cl"><span class="na">server_list</span> <span class="o">=</span> <span class="s">internal</span>
</span></span><span class="line"><span class="cl"><span class="na">gpg_keyring</span> <span class="o">=</span> <span class="s">/etc/ansible/galaxy.kbx</span>
</span></span><span class="line"><span class="cl"><span class="na">required_valid_signature_count</span> <span class="o">=</span> <span class="s">+1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[galaxy_server.internal]</span>
</span></span><span class="line"><span class="cl"><span class="na">url</span> <span class="o">=</span> <span class="s">https://galaxy.example.internal/api/galaxy/</span></span></span></code></pre></div>
</div>
<p>Точный путь в <code>url</code> зависит от сервера. Оба свойства я проверил. С одним сервером в <code>server_list</code> клиент не откатывается на публичный Galaxy: при недоступном внутреннем установка падает. А знак плюс в <code>required_valid_signature_count</code> превращает отсутствие подписей в ошибку <code>Signature verification failed for 'community.general': no successful signatures</code>. Без плюса неподписанная коллекция ставится молча.</p>
<p><strong>Puppet.</strong> Адрес своего Forge задаётся в <code>r10k.yaml</code>:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">forge</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">baseurl</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;https://forge.example.internal&#39;</span></span></span></code></pre></div>
</div>
<p><strong>Salt.</strong> Форки формул уже лежат в своём git после шага 1. Для расширений добавляется свой индекс: <code>salt-pip install --index-url https://pypi.example.internal/simple/ ...</code>.</p>
<p><strong>CI.</strong> В GitHub политика разрешённых экшенов ограничивается своей организацией и явным списком, критичные экшены форкаются к себе. В GitLab шаблоны и компоненты живут в своей группе, а образы с Docker Hub идут через Dependency Proxy.</p>
<p>Остаётся вопрос, кто наполняет зеркала. Ответ тот же, что у конвейера чартов: декларативный список в репозитории и джоба по расписанию. Добавить провайдер, коллекцию или модуль — значит открыть merge request в этот список. Вот и точка ревью, которой раньше не было.</p>
<h3 id="шаг-3-карантин">Шаг 3. Карантин</h3>
<p>Правило previous minor хорошо работает как политика, но держать его на памяти людей не нужно. Renovate умеет ждать сам:</p>
<div class="codeblock codeblock--json" data-lang="json"><span class="codeblock__lang" aria-hidden="true">json</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;extends&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;config:recommended&#34;</span><span class="p">,</span> <span class="s2">&#34;helpers:pinGitHubActionDigests&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;minimumReleaseAge&#34;</span><span class="p">:</span> <span class="s2">&#34;14 days&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Без оговорок не обходится. Security-обновления Renovate выпускает в обход выдержки, а для всего остального срочного нужен записанный обходной путь. Начиная с Renovate 42 обновление без времени релиза проверку не проходит никогда, а у образов из своего registry этого времени нет — им нужен <code>minimumReleaseAgeBehaviour: timestamp-optional</code>. Главное же следует из хроники: выдержка защищает от заражённой новой версии и ничего не делает с подменой старой. От подмены защищает шаг 1.</p>
<h3 id="шаг-4-plan-на-недоверенном-коде">Шаг 4. Plan на недоверенном коде</h3>
<ul>
<li>Plan для merge request идёт на отдельном раннере с облачной ролью только на чтение. Apply запускается с защищённой ветки и с защищёнными переменными.</li>
<li>Статических ключей облака в переменных CI нет. Джоба получает OIDC-токен и меняет его на короткоживущую роль: <code>id_tokens</code> в GitLab, <code>id-token: write</code> в GitHub.</li>
<li>Зеркало без <code>direct</code> из шага 2 закрывает оба способа исполнить код на plan, пока в него не положили <code>hashicorp/external</code>. Если этот провайдер нужен команде, блоки <code>data &quot;external&quot;</code> придётся ловить на ревью.</li>
<li>Изменения в <code>required_providers</code>, в <code>source</code> модулей и в конфигурации CI требуют апрува владельца через <code>CODEOWNERS</code>.</li>
</ul>
<h3 id="шаг-5-сеть-раннера">Шаг 5. Сеть раннера</h3>
<p>Раннеру для работы нужны твой VCS, твоё зеркало и API облака. Всё остальное можно закрыть. Только не сразу: начни с режима аудита, неделю собирай список доменов и лишь потом блокируй. Для GitHub-hosted раннеров это умеет Harden-Runner с режимами <code>audit</code> и <code>block</code> (бесплатно он работает только на публичных репозиториях, а режим <code>block</code> есть только на Linux), а для своих раннеров в Kubernetes подойдут сетевые политики с правилами по FQDN.</p>
<h3 id="шаг-6-инвентарь-и-учения">Шаг 6. Инвентарь и учения</h3>
<p>Образ CI, в который зашиты Terraform, Ansible и сканеры, заслуживает SBOM не меньше, чем образ сервиса. Когда выйдет следующая новость про заражённый инструмент, вопрос «а у нас он есть и какой версии» должен закрываться запросом, а не опросом в чате.</p>
<p>Раз в квартал полезно пройтись по декларативным спискам: что устарело, что сменило владельца, что лежит в зеркале и никем не используется.</p>
<p>И последнее, уже из практики инцидентов. Сценарий «скомпрометирован инструмент из пайплайна» стоит прогнать на game day заранее. Действий в нём немного: найти все запуски за окно компрометации, считать утёкшим всё, до чего дотягивался раннер, и сменить это всё. Смена в день настоящего инцидента занимает часы, если список известен, и недели, если его составляют на ходу.</p>
<h2 id="где-это-не-работает">Где это не работает</h2>
<p>Раздел про меры был бы пропагандой без раздела про их границы.</p>
<p><strong>Карантин проигрывает длинному окну обнаружения.</strong> Бэкдор в xz прожил в релизах пять недель, подмену у Codecov два месяца никто не замечал. Выдержка в 14 дней пропускает обе истории.</p>
<p><strong>Пин замораживает и уязвимости.</strong> Закреплённая по SHA версия сама не обновится. Без Renovate или его аналога через год ты сидишь на идеально неизменяемом дырявом наборе.</p>
<p><strong>Транзитивные ссылки остаются подвижными.</strong> Закреплённый экшен может вызывать другой по тегу, как было у reviewdog. Коллекция Ansible тянет зависимости по диапазону версий. Образ джобы, закреплённый по digest, мог быть собран из <code>FROM ...:latest</code>.</p>
<p><strong>Зеркало не проверяет содержимое.</strong> Оно копирует то, что upstream отдавал в момент синхронизации, и если та попала в окно компрометации, у тебя лежит аккуратная копия малвари с правильным хэшем. Поэтому зеркало без карантина и без сверки с подписью издателя решает только задачу доступности.</p>
<p><strong>Машины разработчиков остаются снаружи.</strong> Поддельный провайдер <code>awsbeta</code>, судя по набору краденого, целил в ноутбук разработчика, а не в CI. Раздельные раннеры и сетевые фильтры туда не дотягиваются. Помогают только <code>~/.terraformrc</code> с зеркалом без <code>direct</code> на каждом ноутбуке и закреплённые хуки pre-commit.</p>
<p><strong>Проверяющий инструмент сам остаётся зависимостью.</strong> Сканер, которым ты проверяешь зеркало, приехал откуда-то и с какими-то правами. Рекурсия не разрывается, её можно только сделать короче: закрепить сканер так же, как всё остальное, и не давать ему секретов сверх необходимого.</p>
<p><strong>Цена.</strong> Команда из трёх человек не будет поддерживать Galaxy NG, зеркало Forge и собственный PyPI. Для неё реалистичный минимум такой: шаг 1 целиком, статическое зеркало провайдеров и форки формул, ролей и модулей в свой git. Это закрывает подмену существующих версий и тайпсквот, то есть большую часть таблицы из хроники.</p>
<h2 id="что-сделать-в-понедельник">Что сделать в понедельник</h2>
<p>Те же семь шагов, только разложенные не по цене, а по срокам. В понедельник делается первое, остальное идёт в план.</p>
<ul>
<li><strong>За час.</strong> Начало шага 1: <code>terraform providers lock</code> под все платформы, lock-файл в git, <code>-lockfile=readonly</code> в CI.</li>
<li><strong>За день.</strong> Дальше по шагу 1: закрепить по SHA экшены, include, образы джобов и хуки pre-commit, включить в политике организации требование SHA. И шаг 0: инвентаризация и лог исходящих соединений раннера за неделю.</li>
<li><strong>За неделю.</strong> Статическое зеркало провайдеров без <code>direct</code> из шага 2 и форки формул, ролей и модулей в свой git. Сюда же шаг 3: выдержка новых версий в Renovate.</li>
<li><strong>За квартал.</strong> Остальное: свой Galaxy с подписями из шага 2, раздельные раннеры для plan и apply и OIDC вместо статических ключей из шага 4, сетевой фильтр из шага 5, инвентарь и учения из шага 6.</li>
</ul>
<h2 id="заключение">Заключение</h2>
<p>Конвейер из шести шагов для Helm-чарта и три случайных способа доставки для провайдера Terraform уживались в одном контуре годами. Чарт при этом управляет одним кластером, а провайдер держит ключи от облака.</p>
<p>Чтобы этот перекос исправить, ничего нового изобретать не нужно. Зеркало, карантин, пин по хэшу и раздельные права давно работают для зависимостей приложений. Осталось перестать называть Terraform, Ansible и сканеры «инструментами» и начать называть их зависимостями.</p>
<h2 id="что-почитать">Что почитать</h2>
<ul>
<li><a href="https://labs.snyk.io/resources/gitflops-dangers-of-terraform-automation-platforms" rel="noopener noreferrer" target="_blank">GitFlops: The Dangers of Terraform Automation Platforms</a> — почему plan исполняет код.</li>
<li><a href="https://labs.boostsecurity.io/articles/erosion-of-trust-unmasking-supply-chain-vulnerabilities-in-the-terraform-registry" rel="noopener noreferrer" target="_blank">Erosion of Trust: Unmasking Supply Chain Vulnerabilities in the Terraform Registry</a> — модули и подвижные теги.</li>
<li><a href="https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack" rel="noopener noreferrer" target="_blank">Trivy Compromised: Everything You Need to Know</a> — хронология и индикаторы.</li>
<li><a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-coder-registry-terraform-supply-chain-2026/" rel="noopener noreferrer" target="_blank">Coder Registry Compromise</a> — подмена модулей через инфраструктуру доставки.</li>
<li><a href="https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation" rel="noopener noreferrer" target="_blank">hackerbot-claw</a> — пять техник атаки на GitHub Actions в одном разборе.</li>
<li><a href="https://developer.hashicorp.com/terraform/language/files/dependency-lock" rel="noopener noreferrer" target="_blank">Dependency Lock File</a> — что именно фиксирует Terraform и чего не фиксирует.</li>
<li><a href="https://slsa.dev" rel="noopener noreferrer" target="_blank">SLSA</a> — рамка, в которую всё это укладывается.</li>
</ul>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! 2026-03-19 в 17:43 UTC в репозитории Trivy появился тег <code>v0.69.4</code>. Релизный пайплайн отработал штатно, бинарники разъехались по GitHub Releases, Docker Hub, GHCR, ECR и deb/rpm-репозиториям. В тот же день почти все теги <code>aquasecurity/trivy-action</code> переехали на чужие коммиты. Сканер уязвимостей, который ставят в пайплайн ради безопасности, начал читать память раннера, собирать SSH-ключи, облачные учётки и токены Kubernetes и отправлять их на <code>scan.aquasecurtiy[.]org</code>. Перечитай имя домена: в нём переставлены две буквы.</p>
<p>Через пять дней в PyPI вышли заражённые релизы LiteLLM. Ломать саму библиотеку не пришлось: её CI ставил Trivy из apt-репозитория без фиксации версии — свежий сканер приехал сам и, по версии самой LiteLLM, унёс токен публикации.</p>
<p>Вход атакующим дала история трёхнедельной давности: в конце февраля через ошибку в GitHub Actions того же репозитория украли токен сервисного аккаунта. Секреты после этого перевыпустили, но не одномоментно, и доступ у атакующих сохранился.</p>
<p>В контурах, с которыми я работал в разных компаниях, провайдеры Terraform приезжали тремя способами: напрямую из публичного реестра, через чужое зеркало и через своё. Про образы приложений такой разнобой уже трудно представить: registry со сканером давно стал нормой. А здесь он никого не смущал. Меня тоже.</p>
<p>Тезис статьи: инструмент, которым ты строишь и проверяешь инфраструктуру, входит в твою цепочку поставки. Прав у него больше, чем у любого сервиса в проде, а проверок по дороге меньше, чем у npm-пакета во фронтенде. За последние пять лет по этому месту били больше десяти раз. Ниже — хроника этих ударов, разбор того, что каждый инструмент проверяет сам, и настройки, которые закрывают большую часть сценариев.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Есть своя история?</p>
    <div class="callout__content">Про инструмент, который оказался дырой в цепочке поставки, расскажи в <a href="/go/chat/?src=iac-tooling-supply-chain">чате</a>. Там же можно поспорить с выводами. Анонсы новых постов выходят в <a href="/go/tg/?src=iac-tooling-supply-chain">канале</a>.</div>
  </div>
</aside>

<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<p>Статья длинная (~61 000 знаков). Сразу — что заберёт каждая роль:</p>
<ul>
<li><strong>SRE, DevOps, платформенная команда.</strong> Таблицу «что инструмент проверяет сам, а что нет» для Terraform, Ansible, Puppet, Salt, Helm и CI. Плюс рабочие настройки зеркала, пина по хэшу и карантина для GitLab CI и GitHub Actions.</li>
<li><strong>Security и AppSec.</strong> Аргументы, почему инфраструктурный тулинг обязан попасть в scope, с датами и ссылками на первичные разборы инцидентов.</li>
<li><strong>Тимлид и руководитель.</strong> Семь шагов по возрастанию цены и <a href="#%d1%87%d1%82%d0%be-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d0%b2-%d0%bf%d0%be%d0%bd%d0%b5%d0%b4%d0%b5%d0%bb%d1%8c%d0%bd%d0%b8%d0%ba">план</a>, где они разложены по срокам: что делается за час, за день, за неделю и за квартал. И раздел о том, где эти меры не помогают.</li>
</ul>
<h2 id="tldr-если-читать-дальше-уже-некогда">TL;DR (если читать дальше уже некогда)</h2>
<ul>
<li>Провайдер Terraform, коллекция Ansible, модуль Puppet, экшен в CI и линтер в pre-commit исполняют чужой код с правами пайплайна или инженера. Это зависимости, причём самые привилегированные из всех.</li>
<li>С 2021 по 2026 год их ломали через угнанные токены мейнтейнеров, перезаписанные теги, тайпсквоттинг и инфраструктуру реестров. В 2026-м атаки стали прицельными: Trivy, KICS, реестр Coder, поддельные провайдеры.</li>
<li>Хэш зависимости между запусками из коробки запоминает только Terraform, и только для провайдеров. Модули, роли, экшены по тегу и хуки pre-commit не сверяются ни с чем.</li>
<li>У российских команд есть вторая ветка той же проблемы: после блокировок 2022–2024 годов многие ходят за провайдерами и образами через чужие зеркала.</li>
<li>Лечится это тем же, чем давно лечат зависимости приложений: своё зеркало, карантин новых версий, пин по хэшу, раздельные раннеры. Придумывать новое не нужно.</li>
</ul>
<h2 id="привилегированный-слепой-угол">Привилегированный слепой угол</h2>
<h3 id="что-я-называю-инфраструктурным-тулингом">Что я называю инфраструктурным тулингом</h3>
<p>Всё, что исполняется, пока ты строишь, меняешь или проверяешь инфраструктуру, и что написал не ты. Список длиннее, чем хотелось бы.</p>
<table>
	<thead>
			<tr>
					<th>Класс</th>
					<th>Примеры</th>
					<th>Откуда приезжает</th>
					<th>Где и с какими правами исполняется</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Провайдеры и модули IaC</td>
					<td>провайдеры Terraform и OpenTofu, модули из реестра и из git</td>
					<td><code>registry.terraform.io</code>, <code>registry.opentofu.org</code>, GitHub</td>
					<td>раннер или ноутбук, ключи облака из окружения</td>
			</tr>
			<tr>
					<td>Содержимое систем управления конфигурацией</td>
					<td>коллекции и роли Ansible, модули Puppet, формулы и расширения Salt</td>
					<td>Galaxy, Forge, PyPI, GitHub</td>
					<td>control node или master, root на каждом управляемом хосте</td>
			</tr>
			<tr>
					<td>Обвязка Kubernetes</td>
					<td>Helm-чарты и плагины, плагины <code>kubectl</code> из krew</td>
					<td>OCI-реестры, GitHub Releases</td>
					<td>контекст инженера или деплой-раннера, часто <code>cluster-admin</code></td>
			</tr>
			<tr>
					<td>Кирпичи CI</td>
					<td>GitHub Actions, компоненты и <code>include</code> в GitLab CI, образы для джобов</td>
					<td>Marketplace, CI/CD Catalog, Docker Hub</td>
					<td>раннер, все секреты и токен джобы</td>
			</tr>
			<tr>
					<td>Линтеры и сканеры</td>
					<td>Trivy, KICS, Checkov, <code>tflint</code>, <code>ansible-lint</code>, хуки pre-commit</td>
					<td>GitHub Releases, apt, pip, образы</td>
					<td>раннер или ноутбук, читают весь репозиторий и окружение</td>
			</tr>
			<tr>
					<td>Установщики</td>
					<td><code>curl ... | sh</code>, bootstrap-скрипты, apt- и yum-репозитории вендоров</td>
					<td>сайт вендора</td>
					<td>root на хосте в момент установки</td>
			</tr>
	</tbody>
</table>
<p>Последняя строка выглядит мелочью, пока не вспомнишь, что LiteLLM заразили именно через неё.</p>
<h3 id="чем-это-отличается-от-зависимости-приложения">Чем это отличается от зависимости приложения</h3>
<p>Зависимость приложения получает ровно те права, которые есть у сервиса: скомпрометированный пакет в сервисе корзины дотянется до базы корзины и её секретов. Неприятно, но у ущерба есть граница — и рисовал её ты сам.</p>
<p>У инфраструктурного инструмента такой границы нет и быть не может: он существует, чтобы менять всё — создавать сети, выдавать роли, раскатывать конфигурацию на весь парк. Отсюда и права:</p>
<ul>
<li>Провайдер Terraform — отдельный бинарник, который Terraform запускает дочерним процессом. Он наследует окружение, а в окружении лежат ключи облака.</li>
<li>Плагины из коллекции Ansible исполняются на control node, а модули уезжают на хосты и работают там под <code>become</code>.</li>
<li>Модуль Puppet приносит Ruby-код, который выполняется на сервере при компиляции каталога, и факты, которые исполняются от root на каждом агенте.</li>
<li>Расширение Salt — pip-пакет, который ставится на master и миньоны. На миньоне он работает от root, на master — с правами процесса, который раздаёт команды всему парку.</li>
<li>Экшен в CI исполняется в том же раннере, что и твои шаги, и может прочитать память процесса со всеми секретами джобы. Именно так работала нагрузка в атаках на <code>tj-actions</code> и Trivy.</li>
<li>Хук pre-commit запускается на ноутбуке инженера, где лежат kubeconfig, SSH-ключи и живая сессия облачного CLI.</li>
</ul>
<h3 id="terraform-plan-не-read-only"><code>terraform plan</code> не read-only</h3>
<p>На одном заблуждении держится половина пайплайнов: <code>plan</code> безопасен, он же ничего не меняет, только показывает разницу. Поэтому plan запускают автоматически на любой merge request, а строгие проверки вешают на apply.</p>
<p>В ноябре 2024 года Эллиот Уорд из Snyk Labs <a href="https://labs.snyk.io/resources/gitflops-dangers-of-terraform-automation-platforms" rel="noopener noreferrer" target="_blank">проверил пять платформ автоматизации Terraform</a>: Terraform Cloud, Atlantis, Digger, Env0 и Terrateam. Все пять в конфигурации по умолчанию позволяли исполнить произвольный код на этапе plan. Способов два. Первый — data-источник <code>external</code>, который по определению запускает локальную программу. Второй — собственный провайдер: Terraform скачает его на <code>init</code> и запустит на <code>plan</code>. В эксперименте с Terraform Cloud исследователь получил шелл в контейнере и увидел там AWS-ключи, которыми разворачивается настоящая инфраструктура.</p>
<p>Проверить первое можно за минуту на своём ноутбуке:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;external&#34; &#34;innocent&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  program</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sh&#34;, &#34;-c&#34;, &#34;id &gt; plan-was-here.txt; echo &#39;{}&#39;&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform init <span class="o">&amp;&amp;</span> terraform plan
</span></span><span class="line"><span class="cl">cat plan-was-here.txt</span></span></code></pre></div>
</div>
<p>На Terraform 1.15 план отвечает <code>No changes. Your infrastructure matches the configuration</code>, а файл с выводом <code>id</code> уже лежит рядом. Вместо <code>id</code> там может стоять что угодно.</p>
<p>Блока такого вида в чужом merge request достаточно. Его же 2026-08-31 нашли в модулях, которые 14 часов отдавались <a href="https://github.com/coder/coder/security/advisories/GHSA-vx42-ghc9-gw65" rel="noopener noreferrer" target="_blank">с адреса реестра Coder</a>: через доступ к Cloudflare атакующие добавили в пул свои серверы, и часть запросов уходила к ним.</p>
<h3 id="что-обычно-проверяют">Что обычно проверяют</h3>
<p>Сначала точка отсчёта. В компании со средней зрелостью безопасность цепочки поставки, она же шестой столп <a href="/posts/digital-immune-system/#%d1%88%d0%b5%d1%81%d1%82%d1%8c-%d1%81%d1%82%d0%be%d0%bb%d0%bf%d0%be%d0%b2-dis">Digital Immune System</a>, означает примерно такой набор:</p>
<ul>
<li><strong>Lock-файлы и SCA.</strong> Зависимости приложения зафиксированы, сканер сверяет их с базами уязвимостей на каждом merge request.</li>
<li><strong>SAST и DAST.</strong> Первый смотрит свой код, второй — собранное приложение.</li>
<li><strong>Зеркало пакетов.</strong> npm, PyPI, Maven и NuGet идут через Nexus или Artifactory, прямой выход в публичные реестры из CI закрыт.</li>
<li><strong>Карантин.</strong> Новая версия пакета становится доступна сборкам через несколько дней или недель, когда на неё уже наступил кто-то другой.</li>
<li><strong>Свой registry образов</strong> со сканером на входе.</li>
<li><strong>Подписи и SBOM.</strong> Cosign для образов, CycloneDX или SPDX для состава, admission-политика в кластере, которая не пускает неподписанное.</li>
</ul>
<p>Набор рабочий, спорить с ним я не собираюсь. Но все шесть пунктов отвечают на вопрос «что мы выпускаем», а вопрос «чем мы это собираем, раскатываем и проверяем» в scope не входит вовсе. Граница проходит по коду приложения и его зависимостям — всё по другую сторону пайплайна считается средой.</p>
<h3 id="перевёрнутая-пирамида">Перевёрнутая пирамида</h3>
<p>Наложи этот набор на права — и пирамида переворачивается: чем больше прав у артефакта, тем меньше из перечисленного до него доходит.</p>
<table>
	<thead>
			<tr>
					<th>Что приезжает</th>
					<th>Права при исполнении</th>
					<th>Что обычно проверяют</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>npm-пакет фронтенда</td>
					<td>сборочный контейнер</td>
					<td>lock-файл, SCA, карантин в зеркале</td>
			</tr>
			<tr>
					<td>Образ сервиса</td>
					<td>namespace сервиса</td>
					<td>свой registry, сканер, подпись, admission-политика</td>
			</tr>
			<tr>
					<td>Helm-чарт инфраструктурного компонента</td>
					<td>кластер</td>
					<td>иногда зеркало</td>
			</tr>
			<tr>
					<td>Провайдер Terraform</td>
					<td>облачный аккаунт целиком</td>
					<td>lock-файл, если его не забыли закоммитить</td>
			</tr>
			<tr>
					<td>Коллекция Ansible, модуль Puppet, формула Salt</td>
					<td>root на всём парке</td>
					<td>версия в requirements, если повезло</td>
			</tr>
			<tr>
					<td>Экшен или <code>include</code> в CI</td>
					<td>все секреты джобы</td>
					<td>тег</td>
			</tr>
			<tr>
					<td>Линтер в pre-commit</td>
					<td>ноутбук инженера</td>
					<td>тег</td>
			</tr>
	</tbody>
</table>
<p>Security-команда смотрит на верх таблицы: там код, который пишет компания, и привычные сканеры. Низ таблицы числится «инструментами», а инструменту вопрос «откуда ты это взял» задают редко. Про популярный npm-пакет так перестали думать после <a href="https://github.com/dominictarr/event-stream/issues/116" rel="noopener noreferrer" target="_blank">event-stream</a> и <a href="https://www.bleepingcomputer.com/news/security/big-sabotage-famous-npm-package-deletes-files-to-protest-ukraine-war/" rel="noopener noreferrer" target="_blank">node-ipc</a>. Про популярный провайдер думают до сих пор.</p>
<h2 id="хроника-20212026">Хроника 2021–2026</h2>
<p>В таблицу попало только то, что било по инструментам сборки, доставки и проверки инфраструктуры. Обычные заражённые пакеты из npm, PyPI и других реестров я не считал: <a href="https://www.sonatype.com/press-releases/sonatypes-10th-annual-state-of-the-software-supply-chain-report" rel="noopener noreferrer" target="_blank">по подсчёту Sonatype</a>, с 2019 года по осень 2024-го их набралось больше 700 тысяч. Последняя колонка забегает вперёд: в ней мера из практической части, которая закрыла бы сценарий на твоей стороне.</p>
<table>
	<thead>
			<tr>
					<th>Когда</th>
					<th>Что случилось</th>
					<th>Вектор</th>
					<th>Чем закрывается</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2021-01 … 04</td>
					<td>Bash Uploader у Codecov два месяца периодически подменяли, изменённая версия отправляла наружу окружение CI. У HashiCorp утёк GPG-ключ подписи релизов</td>
					<td>скрипт по <code>curl | bash</code> без сверки хэша</td>
					<td>пин и сверка контрольной суммы</td>
			</tr>
			<tr>
					<td>2021-07</td>
					<td>Через Kaseya VSA шифровальщик REvil разошёлся по клиентам нескольких десятков MSP</td>
					<td>уязвимость в сервере управления</td>
					<td>сегментация, права агента</td>
			</tr>
			<tr>
					<td>2023-01</td>
					<td>CircleCI: украдена сессия инженера, из платформы вынесли секреты клиентов</td>
					<td>CI-платформа как хранилище секретов</td>
					<td>короткоживущие учётки вместо статических</td>
			</tr>
			<tr>
					<td>2023-06</td>
					<td>BoostSecurity нашла в Terraform Registry несколько сотен уязвимых модулей, у многих можно перехватить уже опубликованные версии</td>
					<td>подвижные теги, <code>pull_request_target</code></td>
					<td>пин модулей по коммиту</td>
			</tr>
			<tr>
					<td>2024-11</td>
					<td>Snyk показала исполнение кода на <code>terraform plan</code> в пяти платформах</td>
					<td>plan на недоверенном коде</td>
					<td>allowlist провайдеров, отдельный раннер</td>
			</tr>
			<tr>
					<td>2025-03</td>
					<td>Перезаписаны тег <code>v1</code> у <code>reviewdog/action-setup</code> и все теги <code>tj-actions/changed-files</code>, секреты уходили в логи сборок</td>
					<td>подвижный тег экшена</td>
					<td>пин по SHA коммита</td>
			</tr>
			<tr>
					<td>2025-08</td>
					<td>Заражённые версии Nx искали секреты с помощью локальных AI-ассистентов</td>
					<td>токен публикации, украденный через CI</td>
					<td>карантин новых версий</td>
			</tr>
			<tr>
					<td>2026-02 … 03</td>
					<td>Бот hackerbot-claw за десять дней атаковал как минимум семь репозиториев, у Trivy украли токен и снесли релизы</td>
					<td><code>pull_request_target</code> в CI самого инструмента</td>
					<td>зеркало бинарников</td>
			</tr>
			<tr>
					<td>2026-03-19</td>
					<td>Trivy: перезаписаны теги экшенов, выпущен заражённый <code>v0.69.4</code></td>
					<td>неполная ротация учёток после прошлого инцидента</td>
					<td>пин по SHA, карантин, egress-фильтр</td>
			</tr>
			<tr>
					<td>2026-03-23</td>
					<td>KICS: 35 тегов экшена на четыре часа подменены стилером</td>
					<td>судя по всему, угнанный сервисный аккаунт</td>
					<td>пин по SHA</td>
			</tr>
			<tr>
					<td>2026-03-24</td>
					<td>LiteLLM: заражённые релизы в PyPI</td>
					<td>сканер из apt без фиксации версии</td>
					<td>пин версии инструмента</td>
			</tr>
			<tr>
					<td>2026-06 … 07</td>
					<td>CVE-2026-11332 и CVE-2026-16493 в <code>ansible-galaxy</code>: установка роли исполняет команды, для коллекций описана та же ошибка</td>
					<td>подстановка аргументов в <code>git clone</code></td>
					<td>свой Galaxy с проверенным содержимым</td>
			</tr>
			<tr>
					<td>2026-07</td>
					<td>Найден троянизированный провайдер <code>terraform-provider-awsbeta</code></td>
					<td>поддельный провайдер</td>
					<td>allowlist пространств имён, зеркало</td>
			</tr>
			<tr>
					<td>2026-08-31</td>
					<td>Реестр Coder 14 часов раздавал Terraform-модули со стилером</td>
					<td>угнанный Cloudflare-ключ, чужие серверы за доменом реестра</td>
					<td>своё зеркало модулей, сверка содержимого</td>
			</tr>
			<tr>
					<td>2026-09</td>
					<td>Провайдер <code>kreuzwenker/docker</code>, тайпсквот <code>kreuzwerker/docker</code></td>
					<td>имя с опечаткой</td>
					<td>allowlist, зеркало</td>
			</tr>
	</tbody>
</table>
<p>Теперь по периодам, с деталями, которые в таблицу не влезли.</p>
<h3 id="2021-скрипт-из-интернета-и-сервер-управления">2021: скрипт из интернета и сервер управления</h3>
<p>С 2021-01-31 по 2021-04-01 Bash Uploader, который Codecov предлагал запускать в CI одной строкой, периодически подменяли: изменённая версия отправляла на сторонний сервер переменные окружения сборки. <a href="https://about.codecov.io/security-update/" rel="noopener noreferrer" target="_blank">Заметили подмену не сканеры</a>: клиент сверил контрольную сумму скачанного скрипта с опубликованной, и они не сошлись. До этого два месяца о расхождении никто не сообщал.</p>
<p>Среди пострадавших — HashiCorp. В её CI лежал приватный GPG-ключ, которым подписаны файлы <code>SHA256SUMS</code> всех релизов, и по нему же <code>terraform init</code> проверяет официальные провайдеры HashiCorp. <a href="https://discuss.hashicorp.com/t/hcsec-2021-12-codecov-security-event-and-hashicorp-gpg-key-exposure/23512" rel="noopener noreferrer" target="_blank">Ключ отозвали</a>, следов злоупотребления не нашли, но <a href="https://discuss.hashicorp.com/t/terraform-updates-for-hcsec-2021-12/23570" rel="noopener noreferrer" target="_blank">Terraform пришлось обновлять</a>: в старые версии зашит прежний ключ, и релизы провайдеров, подписанные новым, они проверить не могут. Это первый случай в хронике, когда удар по вспомогательному инструменту CI дотянулся до корня доверия всей IaC-экосистемы.</p>
<p>Вторая история того же года формально про другое. 2021-07-02 группа REvil воспользовалась уязвимостью в серверах <a href="https://en.wikipedia.org/wiki/Kaseya_VSA_ransomware_attack" rel="noopener noreferrer" target="_blank">Kaseya VSA</a>. VSA — система удалённого управления: агент стоит на каждой машине, сервер умеет исполнить на них что угодно. По <a href="https://www.kaseya.com/press-release/kaseya-responds-swiftly-to-sophisticated-cyberattack-mitigating-global-disruption-to-customers/" rel="noopener noreferrer" target="_blank">оценке самой Kaseya</a>, напрямую пострадали около 50 клиентов. Многие клиенты Kaseya — MSP, то есть подрядчики, которые администрируют чужую инфраструктуру, и через них пострадали ещё от 800 до 1 500 компаний. Шведская сеть Coop в первый же день <a href="https://www.bbc.com/news/technology-57707530" rel="noopener noreferrer" target="_blank">закрыла больше половины</a> из своих 800 магазинов. Подрядчик в роли входной двери — сюжет знакомый: через него же <a href="/posts/hacking-aeroflot/#%d0%bf%d0%be%d0%b4%d1%80%d1%8f%d0%b4%d1%87%d0%b8%d0%ba-%d0%ba%d0%b0%d0%ba-%d0%b2%d1%85%d0%be%d0%b4%d0%bd%d0%b0%d1%8f-%d0%b4%d0%b2%d0%b5%d1%80%d1%8c">зашли в «Аэрофлот»</a>.</p>
<p>VSA — не Ansible и не Salt, но архитектура та же: центральный узел, которому root на всём парке положен по должности. С Salt ровно это случилось годом раньше: через <a href="https://nvd.nist.gov/vuln/detail/CVE-2020-11651" rel="noopener noreferrer" target="_blank">CVE-2020-11651</a> в открытые в интернет мастера заходили без аутентификации и <a href="https://www.tenable.com/blog/cve-2020-11651-cve-2020-11652-critical-salt-framework-vulnerabilities-exploited-in-the-wild" rel="noopener noreferrer" target="_blank">раскатывали майнеры</a> по всем миньонам.</p>
<h3 id="20232024-ci-оказался-сейфом-а-к-реестрам-появились-вопросы">2023–2024: CI оказался сейфом, а к реестрам появились вопросы</h3>
<p>2023-01-04 CircleCI попросила всех клиентов немедленно сменить все секреты, которые хранились в платформе. Из <a href="https://circleci.com/blog/jan-4-2023-incident-report" rel="noopener noreferrer" target="_blank">отчёта</a>: 2022-12-16 малварь на ноутбуке инженера украла сессию с уже пройденной двухфакторной аутентификацией, 2022-12-22 через неё вынесли переменные окружения, токены и ключи клиентов. Данные были зашифрованы, но ключи шифрования атакующий достал из работающего процесса.</p>
<p>Вывод неприятный. CI хранит ключи от всего, что умеет твоя автоматизация, а значит, статический ключ облака в переменной CI рано или поздно утечёт вместе с CI.</p>
<p>В июне 2023 года Франсуа Пру из BoostSecurity <a href="https://labs.boostsecurity.io/articles/erosion-of-trust-unmasking-supply-chain-vulnerabilities-in-the-terraform-registry" rel="noopener noreferrer" target="_blank">посмотрел на Terraform Registry</a> глазами атакующего. Главное наблюдение: модули, в отличие от провайдеров, lock-файлом не покрыты. Версия модуля — это git-тег, а тег можно передвинуть, поэтому план, который ты ревьюил в merge request, и план, который применится через час, могут не совпасть. Из 13 с лишним тысяч модулей реестра несколько сотен оказались уязвимы, и во многих случаях это позволяло перехватить уже опубликованные версии. В январе 2024 года HashiCorp <a href="https://discuss.hashicorp.com/t/hcsec-2024-04-terraform-registry-module-supply-chain-security-improvements/62815" rel="noopener noreferrer" target="_blank">закрыла в публичном реестре две дыры</a>: подвижные теги и перехват переименованного репозитория. Версия модуля теперь привязана к SHA коммита, сохранённому при публикации, а сама публикация — к неизменяемому ID репозитория. Уязвимые workflow в репозиториях модулей остались заботой их авторов, и модуль, подключённый напрямую из git по тегу, эта защита тоже не покрывает.</p>
<p>2024-03-29 Андрес Фройнд публично рассказал о бэкдоре в xz-utils 5.6.0 и 5.6.1. В таблице его нет, потому что xz не инструмент инфраструктуры, но <a href="https://research.swtch.com/xz-timeline" rel="noopener noreferrer" target="_blank">эта история</a> изменила сам разговор. Человек под именем Jia Tan почти полтора года присылал нормальные патчи, прежде чем выпустил свой первый релиз, и ещё почти год вёл проект до бэкдора. После xz довод «этим пользуются все» перестал работать как доказательство безопасности.</p>
<h3 id="2025-теги-оказались-переменными">2025: теги оказались переменными</h3>
<p>2025-03-11 между 18:42 и 20:31 UTC тег <code>v1</code> экшена <code>reviewdog/action-setup</code> указывал на коммит, который выгружал секреты раннера в лог сборки. Этим экшеном пользовался CI проекта <code>tj-actions</code>, и через него, скорее всего, утёк токен бота с правом записи. 2025-03-14 атакующий передвинул все теги <code>tj-actions/changed-files</code> на свой коммит. <a href="https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction" rel="noopener noreferrer" target="_blank">CISA</a> просит проверить все проекты, где экшен запускался с 2025-03-12 по 2025-03-15, а найденные в логах секреты считать скомпрометированными.</p>
<p>Экшен стоял в 23 000 репозиториев. <a href="https://www.endorlabs.com/learn/blast-radius-of-the-tj-actions-changed-files-supply-chain-attack" rel="noopener noreferrer" target="_blank">Endor Labs</a> нашла 5 416 репозиториев со ссылкой на него: за сутки атаки workflow запускался в 614, а секреты попали в лог в 218, и в основном это короткоживущие <code>GITHUB_TOKEN</code>. По данным <a href="https://unit42.paloaltonetworks.com/github-actions-supply-chain-attack/" rel="noopener noreferrer" target="_blank">Unit 42</a>, изначально целились в проект <code>coinbase/agentkit</code>, а все теги атакующий перезаписал через двадцать минут после того, как там убрали уязвимый workflow. Почему он пошёл на шумную массовую атаку, в Unit 42 не знают.</p>
<p>Одну деталь этой истории запомни, она вернётся в разделе про ограничения. Кто закрепил <code>tj-actions/changed-files</code> по SHA коммита, не пострадал — если только в эти дни не передвинул пин на заражённый коммит. С экшенами reviewdog пин спасал не всегда: составные экшены внутри себя ссылались на <code>reviewdog/action-setup@v1</code> по тегу.</p>
<p>2025-08-26 в npm вышли заражённые версии Nx, системы сборки монорепозиториев. Токен публикации, по <a href="https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c" rel="noopener noreferrer" target="_blank">advisory проекта</a>, украли через workflow на <code>pull_request_target</code>, который подставлял заголовок pull request в шелл-скрипт. По <a href="https://www.wiz.io/blog/s1ngularitys-aftermath" rel="noopener noreferrer" target="_blank">разбору Wiz</a>, postinstall-скрипт искал на машине ключи и токены, а для разведки вызывал установленные AI-ассистенты: Claude Code, Gemini CLI и Amazon Q с флагами, отключающими подтверждения. Найденное складывалось в публичный репозиторий <code>s1ngularity-repository</code> в аккаунте самой жертвы. Утёкших секретов в Wiz насчитали больше двух тысяч. Для сверки по номерам: инциденту присвоена <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-10894" rel="noopener noreferrer" target="_blank">CVE-2025-10894</a>, и она опубликована. Запись GHSA-cxm3-wv7p-598c в <a href="https://github.com/advisories/GHSA-cxm3-wv7p-598c" rel="noopener noreferrer" target="_blank">общей базе GitHub</a> с 2026-07-28 помечена отозванной, причина не названа. Advisory в репозитории проекта при этом на месте.</p>
<h3 id="2026-по-инструментам-бьют-прицельно">2026: по инструментам бьют прицельно</h3>
<p>С 2026-02-21 по 2026-03-02 аккаунт hackerbot-claw, который представлялся «автономным агентом для исследований безопасности», <a href="https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation" rel="noopener noreferrer" target="_blank">атаковал как минимум семь репозиториев</a> через ошибки в GitHub Actions. В списке два инструмента проверки инфраструктуры: Trivy и <code>DataDog/datadog-iac-scanner</code>. Токен у Trivy украли 2026-02-27 через workflow на <code>pull_request_target</code>. Тот pull request, по <a href="https://labs.boostsecurity.io/articles/megagame10418-the-user-behind-hackerbot-claw" rel="noopener noreferrer" target="_blank">разбору BoostSecurity</a>, открыл другой аккаунт, которого на GitHub уже нет, а hackerbot-claw пришёл в тот же workflow сутки спустя. Мейнтейнеры Trivy считают, что за обоими стоит один атакующий. Дальше с этим токеном репозиторий переименовали и закрыли, релизы с 0.27.0 по 0.69.1 удалили, а в расширение для VS Code на Open VSX залили подозрительный артефакт. Для потребителей первый удар выглядел как отказ: пайплайны, которые скачивали бинарник Trivy с GitHub Releases, перестали его находить.</p>
<p>Aqua сменила учётные данные, но не разом: по её же <a href="https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23" rel="noopener noreferrer" target="_blank">advisory</a>, ротация была не одномоментной, и атакующий мог успеть подсмотреть уже новые секреты. 2026-03-19 этим воспользовалась группа, которая называет себя TeamPCP. По <a href="https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack" rel="noopener noreferrer" target="_blank">данным Wiz</a>, перезаписаны 75 из 76 тегов <code>trivy-action</code> (в advisory Aqua — 76 из 77) и семь тегов <code>setup-trivy</code>. В 17:43 UTC атакующий запушил тег <code>v0.69.4</code>, и к 18:22 автоматика выложила релиз с тем же стилером. 2026-03-20 по npm пошёл самораспространяющийся червь CanisterWorm: в <a href="https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise" rel="noopener noreferrer" target="_blank">Aikido</a> считают его прямым продолжением атаки на Trivy и работой той же группы. 2026-03-22 на Docker Hub появились ещё два заражённых образа, <code>0.69.5</code> и <code>0.69.6</code>.</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/342dd828c33262b51ed8223cd3bc05f00b26fb2de146fb8e5925092981cbb1a7.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-342dd828c332-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 936px; background-color: transparent;" viewBox="4 4 936 291.25" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-342dd828c332-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-342dd828c332-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-342dd828c332-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-342dd828c332-light .error-icon{fill:#dce0e8;}#m-342dd828c332-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-342dd828c332-light .edge-thickness-normal{stroke-width:1px;}#m-342dd828c332-light .edge-thickness-thick{stroke-width:3.5px;}#m-342dd828c332-light .edge-pattern-solid{stroke-dasharray:0;}#m-342dd828c332-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-342dd828c332-light .edge-pattern-dashed{stroke-dasharray:3;}#m-342dd828c332-light .edge-pattern-dotted{stroke-dasharray:2;}#m-342dd828c332-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-342dd828c332-light .marker.cross{stroke:#5c5f77;}#m-342dd828c332-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-342dd828c332-light p{margin:0;}#m-342dd828c332-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-342dd828c332-light .cluster-label text{fill:#4c4f69;}#m-342dd828c332-light .cluster-label span{color:#4c4f69;}#m-342dd828c332-light .cluster-label span p{background-color:transparent;}#m-342dd828c332-light .label text,#m-342dd828c332-light span{fill:#4c4f69;color:#4c4f69;}#m-342dd828c332-light .node rect,#m-342dd828c332-light .node circle,#m-342dd828c332-light .node ellipse,#m-342dd828c332-light .node polygon,#m-342dd828c332-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-342dd828c332-light .rough-node .label text,#m-342dd828c332-light .node .label text,#m-342dd828c332-light .image-shape .label,#m-342dd828c332-light .icon-shape .label{text-anchor:middle;}#m-342dd828c332-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-342dd828c332-light .rough-node .label,#m-342dd828c332-light .node .label,#m-342dd828c332-light .image-shape .label,#m-342dd828c332-light .icon-shape .label{text-align:center;}#m-342dd828c332-light .node.clickable{cursor:pointer;}#m-342dd828c332-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-342dd828c332-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-342dd828c332-light .edgePaths .path{stroke:#5c5f77;stroke-width:1px;}#m-342dd828c332-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-342dd828c332-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-342dd828c332-light .edgeLabel p{background-color:#eff1f5;}#m-342dd828c332-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-342dd828c332-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-342dd828c332-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-342dd828c332-light .cluster text{fill:#4c4f69;}#m-342dd828c332-light .cluster span{color:#4c4f69;}#m-342dd828c332-light .node .collapsed-indicator{fill:#ccd0da;stroke:none;opacity:0.6;}#m-342dd828c332-light .node .collapsed-separator{stroke:#ccd0da;stroke-width:0.75px;}#m-342dd828c332-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-342dd828c332-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-342dd828c332-light rect.text{fill:none;stroke-width:0;}#m-342dd828c332-light .icon-shape,#m-342dd828c332-light .image-shape{background-color:#eff1f5;text-align:center;}#m-342dd828c332-light .icon-shape p,#m-342dd828c332-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-342dd828c332-light .icon-shape .label rect,#m-342dd828c332-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-342dd828c332-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-342dd828c332-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-342dd828c332-light .node .neo-node{stroke:#bcc0cc;}#m-342dd828c332-light [data-look="neo"].node rect,#m-342dd828c332-light [data-look="neo"].cluster rect,#m-342dd828c332-light [data-look="neo"].node polygon{stroke:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].swimlane.cluster rect{filter:none;}#m-342dd828c332-light [data-look="neo"].node path{stroke:#bcc0cc;stroke-width:1px;}#m-342dd828c332-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-342dd828c332-light [data-look="neo"].node circle{stroke:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-342dd828c332-light [data-look="neo"].icon-shape .icon{fill:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light [data-look="neo"].icon-shape .icon-neo path{stroke:#bcc0cc;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-342dd828c332-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-342dd828c332-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edges edgePaths"><path d="M164,155L200,155" id="m-342dd828c332-light-L_A_B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_A_B_0" data-points="W3sieCI6MTY0LCJ5IjoxNTV9LHsieCI6MjA0LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M356,155L392,155" id="m-342dd828c332-light-L_B_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_B_C_0" data-points="W3sieCI6MzU2LCJ5IjoxNTV9LHsieCI6Mzk2LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M548,137.125L560.9289321881345,137.125Q568,137.125 568,130.05393218813452L568,76.07106781186548Q568,69 575.0710678118655,69L776,69" id="m-342dd828c332-light-L_C_D_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_D_0" data-points="W3sieCI6NTQ4LCJ5IjoxMzcuMTI1fSx7IngiOjU2OCwieSI6MTM3LjEyNX0seyJ4Ijo1NjgsInkiOjY5fSx7IngiOjc4MCwieSI6Njl9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M548,155L584,155" id="m-342dd828c332-light-L_C_E_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_E_0" data-points="W3sieCI6NTQ4LCJ5IjoxNTV9LHsieCI6NTg4LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/><path d="M548,172.875L560.9289321881345,172.875Q568,172.875 568,179.94606781186548L568,234.42893218813452Q568,241.5 575.0710678118655,241.5L776,241.5" id="m-342dd828c332-light-L_C_F_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_F_0" data-points="W3sieCI6NTQ4LCJ5IjoxNzIuODc1fSx7IngiOjU2OCwieSI6MTcyLjg3NX0seyJ4Ijo1NjgsInkiOjI0MS41fSx7IngiOjc4MCwieSI6MjQxLjV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-light_flowchart-v2-pointEnd-margin)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(664, 68.25)"><g class="label" data-id="L_C_D_0" transform="translate(-33.3671875, -11.25)"><foreignObject width="66.734375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>вероятно</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(664, 240.75)"><g class="label" data-id="L_C_F_0" transform="translate(-46.1171875, -11.25)"><foreignObject width="92.234375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>та же группа</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-342dd828c332-light-flowchart-A-0" data-look="neo" transform="translate(88, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>02-27: через PR<br />украден токен Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-B-1" data-look="neo" transform="translate(280, 155)"><rect class="basic label-container" style="" x="-76" y="-68.25" width="152" height="136.5"/><g class="label" style="" transform="translate(-60, -56.25)"><rect/><foreignObject width="120" height="112.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-01: раскрытие и ротация,<br />доступ сохранился</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-C-3" data-look="neo" transform="translate(472, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-19: теги trivy-action<br />и релиз v0.69.4</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-D-5" data-look="neo" transform="translate(856, 69)"><rect class="basic label-container" style="" x="-76" y="-57" width="152" height="114"/><g class="label" style="" transform="translate(-60, -45)"><rect/><foreignObject width="120" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-20: червь в npm<br />на токенах из Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-E-7" data-look="neo" transform="translate(664, 155)"><rect class="basic label-container" style="" x="-76" y="-34.5" width="152" height="69"/><g class="label" style="" transform="translate(-60, -22.5)"><rect/><foreignObject width="120" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: nowrap; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-24: LiteLLM<br />в PyPI</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-light-flowchart-F-9" data-look="neo" transform="translate(856, 241.5)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-23: теги<br />kics-github-action</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-342dd828c332-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-342dd828c332-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-342dd828c332-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 936px; background-color: transparent;" viewBox="4 4 936 291.25" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-342dd828c332-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-342dd828c332-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-342dd828c332-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-342dd828c332-dark .error-icon{fill:#181926;}#m-342dd828c332-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-342dd828c332-dark .edge-thickness-normal{stroke-width:1px;}#m-342dd828c332-dark .edge-thickness-thick{stroke-width:3.5px;}#m-342dd828c332-dark .edge-pattern-solid{stroke-dasharray:0;}#m-342dd828c332-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-342dd828c332-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-342dd828c332-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-342dd828c332-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-342dd828c332-dark .marker.cross{stroke:#a5adcb;}#m-342dd828c332-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-342dd828c332-dark p{margin:0;}#m-342dd828c332-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-342dd828c332-dark .cluster-label text{fill:#cad3f5;}#m-342dd828c332-dark .cluster-label span{color:#cad3f5;}#m-342dd828c332-dark .cluster-label span p{background-color:transparent;}#m-342dd828c332-dark .label text,#m-342dd828c332-dark span{fill:#cad3f5;color:#cad3f5;}#m-342dd828c332-dark .node rect,#m-342dd828c332-dark .node circle,#m-342dd828c332-dark .node ellipse,#m-342dd828c332-dark .node polygon,#m-342dd828c332-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-342dd828c332-dark .rough-node .label text,#m-342dd828c332-dark .node .label text,#m-342dd828c332-dark .image-shape .label,#m-342dd828c332-dark .icon-shape .label{text-anchor:middle;}#m-342dd828c332-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-342dd828c332-dark .rough-node .label,#m-342dd828c332-dark .node .label,#m-342dd828c332-dark .image-shape .label,#m-342dd828c332-dark .icon-shape .label{text-align:center;}#m-342dd828c332-dark .node.clickable{cursor:pointer;}#m-342dd828c332-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-342dd828c332-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-342dd828c332-dark .edgePaths .path{stroke:#a5adcb;stroke-width:1px;}#m-342dd828c332-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-342dd828c332-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-342dd828c332-dark .edgeLabel p{background-color:#24273a;}#m-342dd828c332-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-342dd828c332-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-342dd828c332-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-342dd828c332-dark .cluster text{fill:#cad3f5;}#m-342dd828c332-dark .cluster span{color:#cad3f5;}#m-342dd828c332-dark .node .collapsed-indicator{fill:#363a4f;stroke:none;opacity:0.6;}#m-342dd828c332-dark .node .collapsed-separator{stroke:#363a4f;stroke-width:0.75px;}#m-342dd828c332-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-342dd828c332-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-342dd828c332-dark rect.text{fill:none;stroke-width:0;}#m-342dd828c332-dark .icon-shape,#m-342dd828c332-dark .image-shape{background-color:#24273a;text-align:center;}#m-342dd828c332-dark .icon-shape p,#m-342dd828c332-dark .image-shape p{background-color:#24273a;padding:2px;}#m-342dd828c332-dark .icon-shape .label rect,#m-342dd828c332-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-342dd828c332-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-342dd828c332-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-342dd828c332-dark .node .neo-node{stroke:#494d64;}#m-342dd828c332-dark [data-look="neo"].node rect,#m-342dd828c332-dark [data-look="neo"].cluster rect,#m-342dd828c332-dark [data-look="neo"].node polygon{stroke:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].swimlane.cluster rect{filter:none;}#m-342dd828c332-dark [data-look="neo"].node path{stroke:#494d64;stroke-width:1px;}#m-342dd828c332-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-342dd828c332-dark [data-look="neo"].node circle{stroke:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-342dd828c332-dark [data-look="neo"].icon-shape .icon{fill:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark [data-look="neo"].icon-shape .icon-neo path{stroke:#494d64;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-342dd828c332-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-342dd828c332-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-342dd828c332-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edges edgePaths"><path d="M164,155L200,155" id="m-342dd828c332-dark-L_A_B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_A_B_0" data-points="W3sieCI6MTY0LCJ5IjoxNTV9LHsieCI6MjA0LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M356,155L392,155" id="m-342dd828c332-dark-L_B_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_B_C_0" data-points="W3sieCI6MzU2LCJ5IjoxNTV9LHsieCI6Mzk2LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M548,137.125L560.9289321881345,137.125Q568,137.125 568,130.05393218813452L568,76.07106781186548Q568,69 575.0710678118655,69L776,69" id="m-342dd828c332-dark-L_C_D_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_D_0" data-points="W3sieCI6NTQ4LCJ5IjoxMzcuMTI1fSx7IngiOjU2OCwieSI6MTM3LjEyNX0seyJ4Ijo1NjgsInkiOjY5fSx7IngiOjc4MCwieSI6Njl9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M548,155L584,155" id="m-342dd828c332-dark-L_C_E_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 32 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_E_0" data-points="W3sieCI6NTQ4LCJ5IjoxNTV9LHsieCI6NTg4LCJ5IjoxNTV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/><path d="M548,172.875L560.9289321881345,172.875Q568,172.875 568,179.94606781186548L568,234.42893218813452Q568,241.5 575.0710678118655,241.5L776,241.5" id="m-342dd828c332-dark-L_C_F_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke-dasharray: 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 4; stroke-dashoffset: 0;;" data-edge="true" data-et="edge" data-id="L_C_F_0" data-points="W3sieCI6NTQ4LCJ5IjoxNzIuODc1fSx7IngiOjU2OCwieSI6MTcyLjg3NX0seyJ4Ijo1NjgsInkiOjI0MS41fSx7IngiOjc4MCwieSI6MjQxLjV9XQ==" data-look="neo" marker-end="url(#m-342dd828c332-dark_flowchart-v2-pointEnd-margin)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(664, 68.25)"><g class="label" data-id="L_C_D_0" transform="translate(-33.3671875, -11.25)"><foreignObject width="66.734375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>вероятно</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(664, 240.75)"><g class="label" data-id="L_C_F_0" transform="translate(-46.1171875, -11.25)"><foreignObject width="92.234375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>та же группа</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-342dd828c332-dark-flowchart-A-0" data-look="neo" transform="translate(88, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>02-27: через PR<br />украден токен Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-B-1" data-look="neo" transform="translate(280, 155)"><rect class="basic label-container" style="" x="-76" y="-68.25" width="152" height="136.5"/><g class="label" style="" transform="translate(-60, -56.25)"><rect/><foreignObject width="120" height="112.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-01: раскрытие и ротация,<br />доступ сохранился</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-C-3" data-look="neo" transform="translate(472, 155)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-19: теги trivy-action<br />и релиз v0.69.4</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-D-5" data-look="neo" transform="translate(856, 69)"><rect class="basic label-container" style="" x="-76" y="-57" width="152" height="114"/><g class="label" style="" transform="translate(-60, -45)"><rect/><foreignObject width="120" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-20: червь в npm<br />на токенах из Trivy</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-E-7" data-look="neo" transform="translate(664, 155)"><rect class="basic label-container" style="" x="-76" y="-34.5" width="152" height="69"/><g class="label" style="" transform="translate(-60, -22.5)"><rect/><foreignObject width="120" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: nowrap; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-24: LiteLLM<br />в PyPI</p></span></div></foreignObject></g></g><g class="node default" id="m-342dd828c332-dark-flowchart-F-9" data-look="neo" transform="translate(856, 241.5)"><rect class="basic label-container" style="" x="-76" y="-45.75" width="152" height="91.5"/><g class="label" style="" transform="translate(-60, -33.75)"><rect/><foreignObject width="120" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 120px; text-align: center; width: 120px;"><span class="nodeLabel"><p>03-23: теги<br />kics-github-action</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-342dd828c332-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-342dd828c332-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs></svg>
</div>
</figure><p>2026-03-23 с 12:58 до 16:50 UTC то же произошло с <a href="https://www.wiz.io/blog/teampcp-attack-kics-github-action" rel="noopener noreferrer" target="_blank"><code>kics-github-action</code></a>: 35 тегов указывали на стилер. KICS — сканер IaC от Checkmarx. На следующий день в PyPI вышли <a href="https://research.jfrog.com/post/litellm-compromised-teampcp/" rel="noopener noreferrer" target="_blank">LiteLLM 1.82.7 и 1.82.8</a>. В 1.82.8 лежал <code>.pth</code>-файл, который запускал сбор учётных данных при каждом старте интерпретатора Python. По <a href="https://docs.litellm.ai/blog/security-update-march-2026" rel="noopener noreferrer" target="_blank">версии самой LiteLLM</a>, токен публикации утёк через Trivy в её CI. JFrog в том же разборе показывает как: скрипт проверок ставил сканер из apt-репозитория, всегда последнюю версию.</p>
<p>2026-06-05 раскрыта <a href="https://github.com/advisories/GHSA-w8p5-mx5w-cpqj" rel="noopener noreferrer" target="_blank">CVE-2026-11332</a>. Команда <code>ansible-galaxy role install</code> передавала поля <code>src</code> и <code>name</code> из <code>meta/requirements.yml</code> роли в <code>git clone</code> без разделителя <code>--</code>. Автор вредоносной роли мог так составить запись о зависимости, чтобы подставить опции git и выполнить команду на машине того, кто роль ставит. Для ролей это <a href="https://github.com/ansible/ansible/pull/87070" rel="noopener noreferrer" target="_blank">исправили</a> в ansible-core 2.21.1 и бэкпортировали в 2.20.7, 2.19.11, 2.18.18 и 2.16.19. Исправление оказалось неполным: <a href="https://github.com/advisories/GHSA-m37p-hmh2-w6j2" rel="noopener noreferrer" target="_blank">CVE-2026-16493</a> от 2026-07-21 описывает ту же ошибку в <code>ansible-galaxy collection install</code>. В upstream на 2026-10-12 она не закрыта: <a href="https://github.com/ansible/ansible/pull/87177" rel="noopener noreferrer" target="_blank">патч</a> висит с конца июня, мейнтейнеры считают его перестраховкой и сомневаются, что через коллекции до уязвимого вызова можно добраться. Red Hat при этом оценила уязвимость как Important и <a href="https://access.redhat.com/errata/RHSA-2026:63386" rel="noopener noreferrer" target="_blank">числит её исправленной</a> только в Satellite. Сведений об эксплуатации я не нашёл. Важен сам класс: код исполняется на этапе установки, до первого запуска плейбука.</p>
<p>Летом и осенью дошла очередь до Terraform, причём тремя разными способами.</p>
<p>В июле Zscaler <a href="https://www.zscaler.com/blogs/security-research/suspected-tradertraitor-group-uses-trojanized-terraform-provider-deliver" rel="noopener noreferrer" target="_blank">нашла <code>terraform-provider-awsbeta</code></a>. Провайдер ведёт себя как настоящий: внутри рабочий каркас с примерами ресурса и data-источника, Terraform получает от него штатные ответы. При запуске он заодно скачивает загрузчик с <code>hashicorp-terraform[.]io</code> и ставит бэкдор FLATROOF. Исследователи осторожно связывают кампанию с группой TraderTraitor и не скрывают, что не знают, как именно этот провайдер попал к жертве. Вероятный путь описан в смежном <a href="https://www.sentinelone.com/labs/dont-call-us-well-call-your-apis-tradertraitor-backdoors-resurface-on-victim-with-no-crypto-ties/" rel="noopener noreferrer" target="_blank">разборе SentinelLabs</a>: фальшивое собеседование и тестовое задание, в репозитории которого лежит Terraform-проект с подложенным <code>.terraform.lock.hcl</code> и провайдером из реестра на домене-двойнике.</p>
<p>2026-08-31 с 07:35 до 21:45 UTC часть запросов к <code>registry.coder.com</code> уходила на серверы атакующего. Сам реестр не взламывали: украли Cloudflare API-ключ и добавили свои адреса в пул, который отвечает за домен. Модули приезжали настоящие, но с лишним блоком <code>data &quot;external&quot;</code>. Он отправлял на <code>coder-infra[.]com</code> переменные окружения и секреты провижинера, а при сборке workspace ещё и OIDC-токен пользователя, его SSH-ключ и токены внешней аутентификации. В <a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-coder-registry-terraform-supply-chain-2026/" rel="noopener noreferrer" target="_blank">разборе CSA</a> есть фраза, которую стоит запомнить: одной фиксации версии модуля мало, чтобы такую подмену обнаружить.</p>
<p>В начале сентября были опубликованы провайдеры <code>gocommunity-io/dockerd</code> и <code>kreuzwenker/docker</code>. Второй отличается от популярного <code>kreuzwerker/docker</code> одной буквой. Вредоносный код просыпался, только если SHA-256 от двух переменных конфигурации совпадал с зашитым значением, то есть ждал конкретную жертву. <a href="https://www.aikido.dev/blog/graphalgo-terraform-go-modules" rel="noopener noreferrer" target="_blank">Aikido</a> описала находку 2026-09-22.</p>
<h3 id="что-видно-из-хроники">Что видно из хроники</h3>
<p><strong>Вспомогательный инструмент перестал быть побочной жертвой.</strong> В 2021-м Codecov ломали ради секретов его клиентов. В 2026-м ломают сам сканер, потому что он стоит в тысячах пайплайнов и ему разрешено читать всё.</p>
<p><strong>Сканеры и линтеры стали самой выгодной целью.</strong> Codecov, reviewdog, Trivy, KICS, <code>datadog-iac-scanner</code> — пять инструментов проверки, по которым били за пять лет. Их ставят ради безопасности, им выдают доступ ко всему репозиторию и окружению, а сами они не проходят ни одной проверки.</p>
<p><strong>Векторов всего четыре, и они повторяются:</strong></p>
<ul>
<li>подвижная ссылка: тег, <code>latest</code>, скрипт по URL;</li>
<li>CI самого инструмента, чаще всего <code>pull_request_target</code>;</li>
<li>инфраструктура доставки: реестр, DNS, бакет;</li>
<li>имя, похожее на настоящее.</li>
</ul>
<p><strong>Окно обнаружения сжалось.</strong> Codecov жил с подменой два месяца, <code>tj-actions</code> — меньше суток, Coder — 14 часов, KICS — четыре часа. Для карантина новых версий это хорошая новость: теперь хватает дней, а не месяцев. Но треть строк в таблице описывает подмену уже существующей версии, а против неё выдержка бессильна — помогает только хэш.</p>
<p><strong>В четырёх историях 2026 года есть домен-двойник.</strong> У Trivy украденное уходило на <code>aquasecurtiy</code>, у Coder — на <code>coder-infra</code>, у KICS — на домен, похожий на Checkmarx, а у поддельного провайдера двойник <code>hashicorp-terraform[.]io</code> раздавал загрузчик. Раннер, которому разрешено ходить только на известные адреса, поднял бы тревогу в первых трёх историях. Совсем закрыть утечку такой фильтр не обещает: у стилера Trivy был запасной канал через репозиторий в аккаунте самой жертвы.</p>
<h2 id="вторая-ветка-тебя-не-взломали-тебе-закрыли-дверь">Вторая ветка: тебя не взломали, тебе закрыли дверь</h2>
<p>Цепочка поставки рвётся и без атакующего. За те же пять лет поставщики инфраструктурных инструментов несколько раз меняли правила, и каждый раз кто-то узнавал об этом по упавшему пайплайну.</p>
<table>
	<thead>
			<tr>
					<th>Когда</th>
					<th>Что изменилось</th>
					<th>Как это выглядело у потребителя</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2022-03</td>
					<td>HashiCorp закрыла <code>registry.terraform.io</code> и <code>releases.hashicorp.com</code> для российских IP</td>
					<td><code>Failed to query available provider packages</code> на <code>terraform init</code></td>
			</tr>
			<tr>
					<td>2023-08</td>
					<td>Terraform сменил лицензию с MPL на BSL, появился форк OpenTofu</td>
					<td>ничего не упало, но выбор инструмента стал юридическим вопросом</td>
			</tr>
			<tr>
					<td>2024-05-30</td>
					<td>Docker Hub начал отвечать 403 российским IP, через несколько дней доступ вернулся</td>
					<td><code>docker pull</code> падает на базовых образах</td>
			</tr>
			<tr>
					<td>2024-08</td>
					<td>OpenTofu закрыл реестр для российских IP и удалил провайдеры Yandex Cloud, Cloud.ru (провайдер <code>sbercloud</code>) и Rustack</td>
					<td>провайдер твоего же облака пропал из реестра</td>
			</tr>
			<tr>
					<td>2024-10 … 2025-01</td>
					<td>Репозитории Salt переехали на инфраструктуру Broadcom, старые адреса и bootstrap-поддомены отключены</td>
					<td>bootstrap-скрипт не находит пакеты</td>
			</tr>
			<tr>
					<td>2024-11 … 2025-01</td>
					<td>Perforce объявила, что с начала 2025 года новые пакеты Puppet уходят в закрытый репозиторий, сообщество ответило форком OpenVox</td>
					<td>в публичном репозитории нет новых версий</td>
			</tr>
			<tr>
					<td>2025-08 … 09</td>
					<td>Bitnami перенесла версионированные образы в <code>bitnamilegacy</code> без обновлений</td>
					<td><code>ErrImagePull</code> у чартов с закреплённым тегом</td>
			</tr>
			<tr>
					<td>2026-05</td>
					<td>В Salt 3008 из поставки убрали сотни модулей (в <a href="https://saltproject.io/blog/the-great-salt-module-migration/" rel="noopener noreferrer" target="_blank">анонсе 2024 года</a> называли около 750), часть из них стала отдельными расширениями</td>
					<td>стейт ссылается на модуль, которого больше нет</td>
			</tr>
	</tbody>
</table>
<p>Ни одна строка не про взлом, а последствия те же: в какой-то день твой пайплайн зависит от решения, которое принял не ты. Лекарство тоже совпадает: копия того, от чего ты зависишь, должна лежать у тебя.</p>
<p>По первой строке: отдельного анонса я не нашёл, но 2022-03-01 сотрудник HashiCorp подтвердил запрет в <a href="https://github.com/hashicorp/terraform/issues/30591" rel="noopener noreferrer" target="_blank">issue про <code>releases.hashicorp.com</code></a>. Про реестр то же самое как факт пишет <a href="https://yandex.cloud/ru/docs/troubleshooting/terraform/known-issues/failed-to-quety-available-provider-packages" rel="noopener noreferrer" target="_blank">документация Yandex Cloud</a>, правда, без даты. Остальное подтверждается открытыми источниками: <a href="https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license" rel="noopener noreferrer" target="_blank">смена лицензии Terraform</a>, <a href="https://habr.com/ru/articles/818565/" rel="noopener noreferrer" target="_blank">блокировка Docker Hub</a>, <a href="https://github.com/opentofu/opentofu/blob/ea558d9d4b5ef4919356aabc8f3bc3d23af1d853/TSC_SUMMARY.md" rel="noopener noreferrer" target="_blank">решение о блокировке</a> и <a href="https://github.com/opentofu/registry/pull/817" rel="noopener noreferrer" target="_blank">удаление провайдеров</a> в OpenTofu, <a href="https://saltproject.io/blog/salt-project-package-repo-migration-and-guidance/" rel="noopener noreferrer" target="_blank">переезд репозиториев Salt</a>, <a href="https://saltproject.io/blog/salt-bootstrap-breakage/" rel="noopener noreferrer" target="_blank">поломка bootstrap</a> и <a href="https://docs.saltproject.io/en/latest/topics/releases/3008.0.html" rel="noopener noreferrer" target="_blank">release notes 3008</a>, <a href="https://www.puppet.com/blog/open-source-puppet-updates-2025" rel="noopener noreferrer" target="_blank">объявление Perforce</a> и <a href="https://voxpupuli.org/blog/2025/01/21/openvox-release/" rel="noopener noreferrer" target="_blank">первый релиз OpenVox</a>, <a href="https://github.com/bitnami/charts/issues/35164" rel="noopener noreferrer" target="_blank">объявление Bitnami</a>.</p>
<p>Для российских команд у этой ветки есть продолжение, о котором редко говорят вслух. После марта 2022-го зеркало для провайдеров появилось почти у всех, кто работает из России. Только чаще всего чужое. Вот блок из <a href="https://yandex.cloud/ru/docs/tutorials/infrastructure-management/terraform-quickstart#configure-provider" rel="noopener noreferrer" target="_blank">документации Yandex Cloud</a>, который с тех пор живёт в <code>~/.terraformrc</code> у многих:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">provider_installation</span> {
</span></span><span class="line"><span class="cl">  <span class="k">network_mirror</span> {
</span></span><span class="line"><span class="cl"><span class="n">    url</span>     <span class="o">=</span> <span class="s2">&#34;https://terraform-mirror.yandexcloud.net/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    include</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;registry.terraform.io/*/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">  <span class="k">direct</span> {
</span></span><span class="line"><span class="cl"><span class="n">    exclude</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;registry.terraform.io/*/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>К самому зеркалу у меня претензий нет, задачу доступности оно решает честно. Но с этого момента в цепочке поставки появилось новое звено, и доверие к нему нигде не записано. Кто его наполняет? Как быстро туда попадает отозванная версия? Что будет, если по этому адресу начнёт отвечать кто-то другой? История Coder показывает, что последний вопрос не теоретический.</p>
<p>С образами после 2024-05-30 вышло ещё проще. Списки «рабочих зеркал Docker Hub» разошлись по чатам за сутки — вперемешку зеркала крупных облаков и сервисы, поднятые неизвестно кем за вечер. Строка в <code>registry-mirrors</code> означает, что оператор зеркала может отдать тебе любой образ под любым тегом.</p>
<p>Обе ветки сходятся в одной точке: и от атаки на поставщика, и от его решения спасает одно и то же — своя копия и доверие, привязанное к содержимому, а не к имени и адресу. Прежде чем строить это у себя, полезно понять, что каждый инструмент проверяет сам.</p>
<h2 id="анатомия-доверия-что-инструмент-проверяет-сам">Анатомия доверия: что инструмент проверяет сам</h2>
<p>У менеджера зависимостей есть три уровня проверки: целостность доставки (приехало то, что отдал сервер), неизменность (сегодня приехало то же, что вчера) и происхождение (это выпустил тот, за кого оно себя выдаёт). Ниже — разбор по инструментам: какой уровень работает из коробки, какой включается руками, а какого нет вовсе. Вывод команд я снимал на Terraform 1.15, ansible-core 2.21, Helm 4.3 и pre-commit 4.6.</p>
<h3 id="провайдеры-terraform-и-opentofu">Провайдеры Terraform и OpenTofu</h3>
<p>Лучший случай во всём разделе, и у него всё равно три оговорки.</p>
<p>При установке из исходного реестра Terraform сверяет пакет с файлом <code>SHA256SUMS</code> и проверяет подпись этого файла GPG-ключом издателя. В выводе <code>terraform init</code> это одна строка:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installed hashicorp/external v2.4.2 (signed by HashiCorp)</span></span></code></pre></div>
</div>
<p>После этого в <code>.terraform.lock.hcl</code> ложатся хэши пакета: один <code>h1:</code> по содержимому и по одному <code>zh:</code> на zip каждой платформы (у меня получилось тринадцать). При следующем <code>init</code> несовпадение даёт ошибку <code>doesn't match any of the checksums previously recorded in the dependency lock file</code>. Документация честно называет эту модель trust on first use.</p>
<p>OpenTofu здесь слабее. Вывод <code>tofu</code> я не снимал, но в <a href="https://opentofu.org/docs/cli/plugins/signing/" rel="noopener noreferrer" target="_blank">документации</a> сказано прямо: из своего основного реестра он по умолчанию ставит и провайдеры без GPG-ключа, пропуская проверку подписи. Строгий режим включается переменной <code>OPENTOFU_ENFORCE_GPG_VALIDATION=true</code>.</p>
<p>Оговорка первая: подпись говорит только, что пакет выпустил владелец пространства имён. Вот настоящий, не поддельный провайдер Docker:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installed kreuzwerker/docker v4.6.0 (self-signed, key ID 0DCE698927DAF8EC)</span></span></code></pre></div>
</div>
<p>У тайпсквота <code>kreuzwenker/docker</code> строка была бы такой же, с другим ID ключа. Наизусть этот ID не помнит никто.</p>
<p>Оговорка вторая: через зеркало подпись не проверяется совсем. Тот же <code>hashicorp/external</code> из локального зеркала:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installed hashicorp/external v2.4.2 (unauthenticated)
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Warning: Incomplete lock file information for providers</span></span></code></pre></div>
</div>
<p>В lock-файл при этом попадает один хэш вместо четырнадцати, и это хэш того, что отдало зеркало. Собрал lock-файл через зеркало — поверил зеркалу на слово. При настроенном зеркале подписанные суммы издателя приносит только <code>terraform providers lock</code>: он игнорирует <code>provider_installation</code> и идёт в исходный реестр. По <a href="https://developer.hashicorp.com/terraform/cli/commands/providers/lock" rel="noopener noreferrer" target="_blank">документации</a>, официальные контрольные суммы под подписью разработчика отдаёт только исходный реестр.</p>
<p>Оговорка третья: вся конструкция работает, пока lock-файл лежит в git. Проверь <code>.gitignore</code> в инфраструктурных репозиториях: <code>.terraform.lock.hcl</code> иногда попадает туда по ошибке вместе с каталогом <code>.terraform</code>.</p>
<h3 id="модули-terraform">Модули Terraform</h3>
<p>Здесь проверять нечего, и в <a href="https://developer.hashicorp.com/terraform/language/files/dependency-lock" rel="noopener noreferrer" target="_blank">документации</a> это написано прямо: lock-файл отслеживает только провайдеры, выбор версий удалённых модулей Terraform не запоминает.</p>
<p>Модуль из реестра приезжает по версии, модуль из git — по <code>ref</code>. Если <code>ref</code> указывает на тег или ветку, содержимое может измениться между двумя <code>terraform init</code> (например, между джобами plan и apply с чистым каталогом). Встроенная защита одна: <code>ref</code> в виде SHA коммита для git-источников. У модулей из реестра нет и её. Остаётся верить реестру, а цену этой веры показала история Coder.</p>
<h3 id="ansible-роли-и-коллекции">Ansible: роли и коллекции</h3>
<p><a href="/posts/ansible-roles/">Роли</a> и коллекции устроены по-разному, и путать их опасно.</p>
<p>Роль — это git-репозиторий или tarball с GitHub. Контрольных сумм и подписей нет. Версия в <code>requirements.yml</code> задаётся тегом, веткой или коммитом, и неизменяем из них только коммит.</p>
<p>Коллекция — tarball с Galaxy-сервера. Сервер отдаёт SHA-256 артефакта, клиент сверяет скачанное. Внутри лежат <code>MANIFEST.json</code> и <code>FILES.json</code> с суммами каждого файла, поэтому <code>ansible-galaxy collection verify</code> умеет сравнить установленное с тем, что лежит на сервере. Это целостность доставки. А вот неизменности нет: lock-файла у Ansible не существует, и транзитивные зависимости коллекции разрешаются заново при каждой установке.</p>
<p>Происхождение закрывают GPG-подписи <code>MANIFEST.json</code>. Клиент их поддерживает, для этого есть ключи <code>--keyring</code> и <code>--required-valid-signature-count</code>. Только подписи должен отдать сервер. Я спросил публичный Galaxy про <code>community.general</code> 13.5.0, одну из самых популярных коллекций:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s <span class="s2">&#34;https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/index/community/general/versions/13.5.0/&#34;</span> <span class="p">|</span> jq .signatures
</span></span><span class="line"><span class="cl"><span class="c1"># []</span></span></span></code></pre></div>
</div>
<p>Подписей нет. Работают они там, где сервер сам подписывает содержимое: в Automation Hub или в собственном Galaxy NG. То есть как раз в своём зеркале.</p>
<p>Остаётся сама установка. В 2026 году в <code>ansible-galaxy install</code> закрыли подстановку аргументов git через зависимости роли: это CVE-2026-11332, закрытая в ansible-core 2.21.1 и в бэкпортах для старых веток. Для установки коллекции из git заведена парная CVE-2026-16493, и в upstream на 2026-10-12 она не исправлена: в ansible-core 2.21.5 разделителя <code>--</code> перед адресом по-прежнему нет. Мейнтейнеры, правда, сомневаются, что до этого вызова можно добраться с вредоносным адресом. Плагины коллекции в любом случае исполняются на control node при первом же запуске плейбука.</p>
<h3 id="puppet-и-salt">Puppet и Salt</h3>
<p>У Puppet модули приезжают с Forge или из git. Подписей нет. В 2021 году Puppet <a href="https://www.puppet.com/blog/malware-scanning" rel="noopener noreferrer" target="_blank">объявил сканирование релизов на малварь</a>, начав с Supported-модулей. По API Forge результата скана сегодня нет ни у community-модулей, ни у релизов 2026 года, так что рассчитывать на него не стоит. В <code>Puppetfile</code> версия модуля с Forge фиксируется точно, git-модуль закрепляется через <code>:commit</code>. У r10k есть полезное побочное свойство: он не разрешает зависимости сам, каждый модуль приходится прописать явно — и неожиданных транзитивных гостей не бывает.</p>
<p>У Salt источников три, и модель доверия у каждого своя.</p>
<p>Формулы — git-репозитории, которые подключают через <code>gitfs_remotes</code>. Мастер сам ходит в репозиторий по расписанию (по умолчанию раз в минуту) и раздаёт миньонам то, что там сейчас лежит. Это подвижная ссылка в чистом виде, и <a href="https://docs.saltproject.io/en/latest/topics/development/conventions/formulas.html" rel="noopener noreferrer" target="_blank">документация Salt</a> прямо просит так не делать: настоятельно рекомендует форкнуть репозиторий формулы к себе, чтобы избежать неожиданных изменений в инфраструктуре.</p>
<p>Расширения — pip-пакеты <code>saltext.*</code> со всеми свойствами pip-пакетов. В Salt 3008, который вышел в мае 2026 года, из поставки убрали сотни модулей, и часть из них теперь ставится расширениями.</p>
<p>Bootstrap-скрипт теперь скачивается с GitHub Releases. Рядом лежит файл с SHA-256, и сверять его нужно руками.</p>
<h3 id="helm-чарты-и-плагины">Helm: чарты и плагины</h3>
<p>Чарт можно подписать: рядом с пакетом кладётся файл <code>.prov</code>, и <code>helm install --verify</code> откажется ставить чарт без него — но по умолчанию флаг выключен, и чарт без подписи ставится молча. У классических HTTP-репозиториев клиент не сверяет скачанный архив даже с <code>digest</code> из <code>index.yaml</code>: на Helm 4.3 <code>helm pull</code> проходит и с подменённым tgz.</p>
<p>У OCI-реестров есть вариант надёжнее: ссылка на чарт по digest вместо тега. Digest неизменяем по построению.</p>
<p>Отдельная ловушка — <code>Chart.lock</code>. Поле <code>digest</code> в нём выглядит как контрольная сумма зависимостей, но это хэш их списка: он отвечает на вопрос «актуален ли lock», а не «то ли содержимое приехало».</p>
<p>Плагины Helm — исполняемый код с хуками установки. В Helm 4 подпись плагина проверяется по умолчанию, но, судя по справке <code>helm plugin install</code>, только при установке из tarball. Плагины <code>kubectl</code> из krew выглядят лучше: манифест в индексе содержит SHA-256 каждого архива, и клиент его сверяет.</p>
<h3 id="кирпичи-ci">Кирпичи CI</h3>
<p><strong>GitHub Actions.</strong> <code>uses: org/action@v4</code> ссылается на тег, а тег подвижен. <code>uses: org/action@</code> с сорока символами SHA ссылается на коммит, и неизменяема только такая ссылка. С августа 2025 года <a href="https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/" rel="noopener noreferrer" target="_blank">политика организации</a> умеет требовать полный SHA и запрещать конкретные экшены. Ограничение то же, что в истории с reviewdog: закреплённый экшен может внутри ссылаться на другой по тегу, а reusable workflows политика тоже не покрывает — их по-прежнему можно подключать по тегу.</p>
<p><strong>GitLab CI.</strong> <code>include: project</code> принимает <code>ref</code>, и туда можно поставить SHA коммита. Компонент из каталога подключается как <code>gitlab.example.internal/group/project/component@версия</code>, вместо версии тоже годится SHA. Для <code>include: remote</code> с GitLab 17.9 есть ключ <a href="https://docs.gitlab.com/ci/yaml/" rel="noopener noreferrer" target="_blank"><code>integrity</code></a> с SHA-256 включаемого файла: не совпало, и пайплайн падает. Образ в <code>image:</code> по тегу подвижен так же, как тег экшена. Неизменяема только форма <code>image: name@sha256:...</code>.</p>
<h3 id="сканеры-линтеры-и-pre-commit">Сканеры, линтеры и pre-commit</h3>
<p>Самая слабая зона. Инструмент попадает в пайплайн одним из четырёх способов: скрипт установки по URL, бинарник с GitHub Releases, пакет из apt-репозитория вендора или готовый образ. У большинства рядом лежат контрольные суммы, у многих есть подписи cosign, но сверка во всех четырёх случаях ручная: пока ты сам не написал эту строку в пайплайне, её нет.</p>
<p>Подписанный apt-репозиторий здесь не спасает. Подпись репозитория защищает от подмены по дороге, а заражённый Trivy <code>v0.69.4</code> собрал и выпустил штатный релизный пайплайн.</p>
<p>В pre-commit поле <code>rev</code> принимает тег, и обычно там тег и стоит. Команда <code>pre-commit autoupdate --freeze</code> пишет вместо тега SHA коммита и оставляет версию в комментарии. Заодно она обновляет каждый хук до последнего тега, об этом ниже.</p>
<h3 id="сводная-таблица">Сводная таблица</h3>
<table>
	<thead>
			<tr>
					<th>Артефакт</th>
					<th>Целостность доставки</th>
					<th>Неизменность между запусками</th>
					<th>Происхождение</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Провайдер Terraform из реестра</td>
					<td>да</td>
					<td>да, если lock-файл в git</td>
					<td>подпись владельца пространства имён</td>
			</tr>
			<tr>
					<td>Провайдер Terraform из зеркала</td>
					<td>да</td>
					<td>да, но хэш выдан зеркалом</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Модуль Terraform</td>
					<td>нет</td>
					<td>только git-<code>ref</code> по SHA</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Роль Ansible</td>
					<td>нет</td>
					<td>только версия по SHA</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Коллекция Ansible</td>
					<td>да</td>
					<td>нет</td>
					<td>подписи, если их отдаёт сервер</td>
			</tr>
			<tr>
					<td>Модуль Puppet</td>
					<td>да</td>
					<td>точная версия в <code>Puppetfile</code></td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Формула Salt через gitfs</td>
					<td>нет</td>
					<td>нет, пока не форкнешь</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Расширение Salt</td>
					<td>как у любого pip-пакета</td>
					<td>как у любого pip-пакета</td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Helm-чарт</td>
					<td>только в OCI-реестрах</td>
					<td>digest в OCI</td>
					<td><code>.prov</code>, если включить <code>--verify</code></td>
			</tr>
			<tr>
					<td>Экшен или <code>include</code> в CI</td>
					<td>нет</td>
					<td>SHA коммита, <code>integrity</code></td>
					<td>нет</td>
			</tr>
			<tr>
					<td>Сканер, линтер, хук</td>
					<td>руками</td>
					<td>руками</td>
					<td>руками</td>
			</tr>
	</tbody>
</table>
<p>Средняя колонка объясняет, почему хроника выглядит именно так. Почти в каждой её ячейке стоит «если», «только» или «руками».</p>
<h2 id="свой-контур-где-я-сам-стоял-в-этой-таблице">Свой контур: где я сам стоял в этой таблице</h2>
<p>Теперь про себя. В контурах, с которыми я работал в разных компаниях, картина повторялась с небольшими вариациями, поэтому опишу её собирательно.</p>
<p>Лучше всего было с чартами и образами. В одном из контуров внешний Helm-чарт проходил конвейер из шести шагов:</p>
<table>
	<thead>
			<tr>
					<th>Шаг</th>
					<th>Что делает</th>
					<th>Какой риск закрывает</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>declare</td>
					<td>чарт объявлен строкой <code>repo|chart|version</code> в общем списке</td>
					<td>никто не помнит, откуда это взялось</td>
			</tr>
			<tr>
					<td>validate</td>
					<td>один merge request меняет один чарт</td>
					<td>размытое ревью</td>
			</tr>
			<tr>
					<td>lint</td>
					<td><code>ct lint</code> и yamllint</td>
					<td>кривые values</td>
			</tr>
			<tr>
					<td>test</td>
					<td>установка в эфемерный kind-кластер</td>
					<td>чарт проходит dry-run, но падает при настоящей установке</td>
			</tr>
			<tr>
					<td>release</td>
					<td><code>helm package</code> и <code>helm push</code> в приватный OCI-реестр</td>
					<td>прод тянет чарт из публичного источника</td>
			</tr>
			<tr>
					<td>mirror images</td>
					<td>скрипт вытаскивает образы из <code>values.yaml</code> и кладёт их в свой registry</td>
					<td>образы идут в обход реестра</td>
			</tr>
	</tbody>
</table>
<p>Правило у конвейера одно: прод ничего не тянет из публичного интернета напрямую. Каждый внешний артефакт проходит через одну точку, и эта точка даёт сразу контроль, кэш и след для аудита.</p>
<p>Второе, что работало, — правило previous minor из регламента по инфраструктурному коду. Берём не последнюю вышедшую minor-версию, а предыдущую: свежая успевает отлежаться, а регрессии и сюрпризы ловит кто-то другой. Карантин на голой дисциплине, без единого инструмента. В том же регламенте были <code>CODEOWNERS</code>, защищённые ветки и закоммиченный lock-файл.</p>
<p>А вот про источник провайдеров и модулей там не было ни слова. Провайдеры Terraform, повторюсь, приезжали тремя способами: напрямую из реестра, через чужое зеркало и через своё. Про коллекции Ansible, модули Puppet и формулы Salt вопрос «откуда» звучал ещё реже.</p>
<p>Разложи это по сводной таблице из прошлого раздела, и вылезет перекос: для Helm-чарта построен конвейер из шести шагов, а для провайдера Terraform, у которого в руках облачный аккаунт, не было даже единого источника.</p>
<p>Убирается этот перекос тем же принципом, что у конвейера чартов, только применённым ко всем остальным классам: объявить, закрепить, зеркалировать, выдержать, сверить.</p>
<h2 id="практика-тот-же-конвейер-для-всего-остального">Практика: тот же конвейер для всего остального</h2>
<p>Шагов семь, с нулевого по шестой, и идут они по возрастанию цены. Первые два не требуют ни нового железа, ни согласований. Как всё раскладывается по срокам, от часа до квартала, собрано в конце, в разделе <a href="#%d1%87%d1%82%d0%be-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d0%b2-%d0%bf%d0%be%d0%bd%d0%b5%d0%b4%d0%b5%d0%bb%d1%8c%d0%bd%d0%b8%d0%ba">«Что сделать в понедельник»</a>. Команды для Terraform и Ansible я прогнал на тех же версиях, что и в прошлом разделе.</p>
<h3 id="шаг-0-инвентаризация-что-у-тебя-ходит-наружу">Шаг 0. Инвентаризация: что у тебя ходит наружу</h3>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Terraform: провайдеры и источники модулей</span>
</span></span><span class="line"><span class="cl">terraform get   <span class="c1"># без установленных модулей terraform providers падает</span>
</span></span><span class="line"><span class="cl">terraform providers
</span></span><span class="line"><span class="cl">grep -rhoE <span class="s1">&#39;source[[:space:]]*=[[:space:]]*&#34;[^&#34;]+&#34;&#39;</span> --include<span class="o">=</span><span class="s1">&#39;*.tf&#39;</span> --exclude-dir<span class="o">=</span>.terraform . <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GitHub Actions: всё, что не закреплено по SHA</span>
</span></span><span class="line"><span class="cl">grep -rhoE <span class="s1">&#39;uses:[[:space:]]*[^[:space:]]+&#39;</span> .github/workflows <span class="p">|</span> grep -vE <span class="s1">&#39;@[0-9a-f]{40}&#39;</span> <span class="p">|</span> sort <span class="p">|</span> uniq -c
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GitLab CI: внешние include и образы</span>
</span></span><span class="line"><span class="cl">grep -nE <span class="s1">&#39;^[[:space:]]*-?[[:space:]]*(remote|component|project|image):&#39;</span> .gitlab-ci.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ansible, Puppet, Salt</span>
</span></span><span class="line"><span class="cl">ansible-galaxy collection list
</span></span><span class="line"><span class="cl">ansible-galaxy role list
</span></span><span class="line"><span class="cl">puppet module list --tree
</span></span><span class="line"><span class="cl">salt-pip list <span class="p">|</span> grep -i saltext
</span></span><span class="line"><span class="cl">grep -A10 <span class="s1">&#39;^gitfs_remotes&#39;</span> /etc/salt/master
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># pre-commit и плагины</span>
</span></span><span class="line"><span class="cl">grep -nE <span class="s1">&#39;^[[:space:]]*(- repo|rev):&#39;</span> .pre-commit-config.yaml
</span></span><span class="line"><span class="cl">helm plugin list
</span></span><span class="line"><span class="cl">kubectl krew list</span></span></code></pre></div>
</div>
<p>Самый полезный источник при этом лежит не в репозитории. Возьми лог исходящих соединений раннера за неделю и посмотри, на какие домены он ходит на самом деле. Список всегда длиннее, чем следует из кода.</p>
<h3 id="шаг-1-закрепи-то-что-уже-есть">Шаг 1. Закрепи то, что уже есть</h3>
<p>Строить ничего не нужно — только заменить подвижные ссылки неизменяемыми.</p>
<p><strong>Terraform.</strong> Lock-файл собирается из исходного реестра, с подписями издателя, сразу под все платформы команды и CI:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform providers lock -platform<span class="o">=</span>linux_amd64 -platform<span class="o">=</span>darwin_arm64
</span></span><span class="line"><span class="cl">git add .terraform.lock.hcl</span></span></code></pre></div>
</div>
<p>В CI <code>init</code> запускается так, чтобы lock-файл нельзя было тихо изменить:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform init -input<span class="o">=</span><span class="nb">false</span> -lockfile<span class="o">=</span>readonly</span></span></code></pre></div>
</div>
<p>Модули из git закрепляются по коммиту:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">module</span> <span class="s2">&#34;vpc&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  source</span> <span class="o">=</span><span class="n"> &#34;git::https://git.example.internal/infra/terraform-vpc.git?ref</span><span class="o">=</span><span class="m">4</span><span class="err">f</span><span class="m">2</span><span class="err">a</span><span class="m">9</span><span class="err">c</span><span class="m">1</span><span class="err">e</span><span class="m">8</span><span class="err">b</span><span class="m">7</span><span class="err">d</span><span class="m">6</span><span class="err">a</span><span class="m">5</span><span class="err">f</span><span class="m">4</span><span class="err">e</span><span class="m">3</span><span class="err">d</span><span class="m">2</span><span class="err">c</span><span class="m">1</span><span class="err">b</span><span class="m">0</span><span class="err">a</span><span class="m">9</span><span class="err">f</span><span class="m">8</span><span class="err">e</span><span class="m">7</span><span class="err">d</span><span class="m">6</span><span class="err">c</span><span class="m">5</span><span class="err">b</span><span class="m">4</span><span class="err">a</span><span class="m">39</span><span class="err">&#34;</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p><strong>GitHub Actions.</strong> Тег остаётся в комментарии, чтобы человек понимал, что за версия:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 </span><span class="w"> </span><span class="c"># v4.2.2</span></span></span></code></pre></div>
</div>
<p>Руками это делать не нужно: пресет Renovate <code>helpers:pinGitHubActionDigests</code> расставит SHA и будет обновлять их вместе с комментарием. После этого включи в политике организации требование полного SHA, иначе через месяц кто-нибудь добавит новый экшен по тегу.</p>
<p><strong>GitLab CI.</strong> То же самое для include и образов:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">project</span><span class="p">:</span><span class="w"> </span><span class="l">platform/ci-templates</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">9b1d4c7e2a6f8035c1e4d7a9b2f6c8e0d3a5b7c1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">file</span><span class="p">:</span><span class="w"> </span><span class="l">/terraform/plan.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">remote</span><span class="p">:</span><span class="w"> </span><span class="l">https://templates.example.internal/lint.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">integrity</span><span class="p">:</span><span class="w"> </span><span class="l">sha256-&lt;base64&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.example.internal/tools/terraform@sha256:&lt;digest&gt;</span></span></span></code></pre></div>
</div>
<p>Значение для <code>integrity</code> считается так:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">openssl dgst -sha256 -binary lint.yml <span class="p">|</span> openssl base64 -A</span></span></code></pre></div>
</div>
<p><strong>pre-commit.</strong> Есть команда, которая расставляет SHA сама:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pre-commit autoupdate --freeze</span></span></code></pre></div>
</div>
<p>У неё есть подвох: она сначала обновляет каждый хук до последнего тега и только потом замораживает. Для шага «закрепи то, что есть» это обновление в обход карантина. Закрепить текущую версию без обновления можно руками: SHA тега показывает <code>git ls-remote &lt;repo&gt; 'refs/tags/&lt;тег&gt;*'</code>, для аннотированного тега нужна строка с <code>^{}</code>.</p>
<p><strong>Ansible.</strong> Точные версии коллекций, роли из своего git по коммиту:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">collections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">community.general</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">13.5.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">roles</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">src</span><span class="p">:</span><span class="w"> </span><span class="l">https://git.example.internal/mirrors/ansible-role-nginx.git</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">scm</span><span class="p">:</span><span class="w"> </span><span class="l">git</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l">7c3e1a9f5b2d8e4c6a0f1b3d5e7c9a2b4d6f8e01</span></span></span></code></pre></div>
</div>
<p><strong>Puppet.</strong> Точная версия для Forge, коммит для git:</p>
<div class="codeblock codeblock--ruby" data-lang="ruby"><span class="codeblock__lang" aria-hidden="true">ruby</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">mod</span> <span class="s1">&#39;puppetlabs-stdlib&#39;</span><span class="p">,</span> <span class="s1">&#39;9.6.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">mod</span> <span class="s1">&#39;nginx&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">git</span><span class="p">:</span>    <span class="s1">&#39;https://git.example.internal/mirrors/puppet-nginx.git&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">commit</span><span class="p">:</span> <span class="s1">&#39;1e5a9c3d7b2f6e0a4c8d1b5f9e3a7c2d6b0f4e8a&#39;</span></span></span></code></pre></div>
</div>
<p><strong>Salt.</strong> Формулы форкаются в свой git, и ветка, на которую смотрит <code>gitfs_remotes</code>, обновляется только через merge request. Расширения ставятся по файлу с хэшами:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">salt-pip install --require-hashes -r saltext-requirements.txt</span></span></code></pre></div>
</div>
<p>Хэши понадобятся для всего дерева зависимостей, а не только для самих расширений.</p>
<p><strong>Сканеры и прочие бинарники.</strong> Версия и контрольная сумма записаны в репозитории, сверка идёт до запуска:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSLO <span class="s2">&#34;https://tools.example.internal/trivy/0.69.3/trivy_0.69.3_Linux-64bit.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;&lt;sha256&gt;  trivy_0.69.3_Linux-64bit.tar.gz&#34;</span> <span class="p">|</span> sha256sum -c -</span></span></code></pre></div>
</div>
<p>Строка <code>apt-get install trivy</code> без версии в CI — ровно то, через что, по разбору JFrog, утёк токен публикации LiteLLM.</p>
<h3 id="шаг-2-своё-зеркало-оно-же-allowlist">Шаг 2. Своё зеркало, оно же allowlist</h3>
<p><strong>Terraform.</strong> Зеркало провайдеров — это каталог со статикой. Наполняется одной командой с машины, у которой есть доступ к исходному реестру:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform providers mirror -platform<span class="o">=</span>linux_amd64 -platform<span class="o">=</span>darwin_arm64 /srv/tf-mirror</span></span></code></pre></div>
</div>
<p>Команда зеркалит провайдеры текущей конфигурации, поэтому запускают её в каталоге со сводным <code>required_providers</code>. Дальше каталог отдаёт любой веб-сервер по HTTPS, а в <code>~/.terraformrc</code> раннеров и инженеров пишется:</p>
<div class="codeblock codeblock--hcl" data-lang="hcl"><span class="codeblock__lang" aria-hidden="true">hcl</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">provider_installation</span> {
</span></span><span class="line"><span class="cl">  <span class="k">network_mirror</span> {
</span></span><span class="line"><span class="cl"><span class="n">    url</span> <span class="o">=</span> <span class="s2">&#34;https://tf-mirror.example.internal/&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Блока <code>direct</code> здесь нет, и это главное. Terraform перестаёт ходить в исходный реестр, а провайдер, которого нет в зеркале, просто не ставится:</p>
<div class="codeblock codeblock--text" data-lang="text"><span class="codeblock__lang" aria-hidden="true">text</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Failed to query available provider packages
</span></span><span class="line"><span class="cl">Could not retrieve the list of available versions for provider
</span></span><span class="line"><span class="cl">kreuzwerker/docker: provider registry.terraform.io/kreuzwerker/docker was not
</span></span><span class="line"><span class="cl">found in any of the search locations</span></span></code></pre></div>
</div>
<p>Зеркало без <code>direct</code> само работает как allowlist. Тайпсквот не установится: его туда никто не клал. Провайдер, собранный атакующим под merge request, тоже. А если в зеркале нет <code>hashicorp/external</code>, блок <code>data &quot;external&quot;</code> из чужого merge request упадёт ещё на <code>init</code>.</p>
<p>Подпись издателя при этом не теряется. Lock-файл из шага 1 собран из исходного реестра, и <code>init -lockfile=readonly</code> через зеркало пройдёт, только если пакет из зеркала совпал с подписанным хэшем. Зеркало отвечает за доступность и за список разрешённого, lock-файл — за содержимое.</p>
<p><strong>Ansible.</strong> В <code>ansible.cfg</code> остаётся один сервер, свой, и включается обязательная подпись:</p>
<div class="codeblock codeblock--ini" data-lang="ini"><span class="codeblock__lang" aria-hidden="true">ini</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[galaxy]</span>
</span></span><span class="line"><span class="cl"><span class="na">server_list</span> <span class="o">=</span> <span class="s">internal</span>
</span></span><span class="line"><span class="cl"><span class="na">gpg_keyring</span> <span class="o">=</span> <span class="s">/etc/ansible/galaxy.kbx</span>
</span></span><span class="line"><span class="cl"><span class="na">required_valid_signature_count</span> <span class="o">=</span> <span class="s">+1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[galaxy_server.internal]</span>
</span></span><span class="line"><span class="cl"><span class="na">url</span> <span class="o">=</span> <span class="s">https://galaxy.example.internal/api/galaxy/</span></span></span></code></pre></div>
</div>
<p>Точный путь в <code>url</code> зависит от сервера. Оба свойства я проверил. С одним сервером в <code>server_list</code> клиент не откатывается на публичный Galaxy: при недоступном внутреннем установка падает. А знак плюс в <code>required_valid_signature_count</code> превращает отсутствие подписей в ошибку <code>Signature verification failed for 'community.general': no successful signatures</code>. Без плюса неподписанная коллекция ставится молча.</p>
<p><strong>Puppet.</strong> Адрес своего Forge задаётся в <code>r10k.yaml</code>:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">forge</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">baseurl</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;https://forge.example.internal&#39;</span></span></span></code></pre></div>
</div>
<p><strong>Salt.</strong> Форки формул уже лежат в своём git после шага 1. Для расширений добавляется свой индекс: <code>salt-pip install --index-url https://pypi.example.internal/simple/ ...</code>.</p>
<p><strong>CI.</strong> В GitHub политика разрешённых экшенов ограничивается своей организацией и явным списком, критичные экшены форкаются к себе. В GitLab шаблоны и компоненты живут в своей группе, а образы с Docker Hub идут через Dependency Proxy.</p>
<p>Остаётся вопрос, кто наполняет зеркала. Ответ тот же, что у конвейера чартов: декларативный список в репозитории и джоба по расписанию. Добавить провайдер, коллекцию или модуль — значит открыть merge request в этот список. Вот и точка ревью, которой раньше не было.</p>
<h3 id="шаг-3-карантин">Шаг 3. Карантин</h3>
<p>Правило previous minor хорошо работает как политика, но держать его на памяти людей не нужно. Renovate умеет ждать сам:</p>
<div class="codeblock codeblock--json" data-lang="json"><span class="codeblock__lang" aria-hidden="true">json</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;extends&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;config:recommended&#34;</span><span class="p">,</span> <span class="s2">&#34;helpers:pinGitHubActionDigests&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;minimumReleaseAge&#34;</span><span class="p">:</span> <span class="s2">&#34;14 days&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Без оговорок не обходится. Security-обновления Renovate выпускает в обход выдержки, а для всего остального срочного нужен записанный обходной путь. Начиная с Renovate 42 обновление без времени релиза проверку не проходит никогда, а у образов из своего registry этого времени нет — им нужен <code>minimumReleaseAgeBehaviour: timestamp-optional</code>. Главное же следует из хроники: выдержка защищает от заражённой новой версии и ничего не делает с подменой старой. От подмены защищает шаг 1.</p>
<h3 id="шаг-4-plan-на-недоверенном-коде">Шаг 4. Plan на недоверенном коде</h3>
<ul>
<li>Plan для merge request идёт на отдельном раннере с облачной ролью только на чтение. Apply запускается с защищённой ветки и с защищёнными переменными.</li>
<li>Статических ключей облака в переменных CI нет. Джоба получает OIDC-токен и меняет его на короткоживущую роль: <code>id_tokens</code> в GitLab, <code>id-token: write</code> в GitHub.</li>
<li>Зеркало без <code>direct</code> из шага 2 закрывает оба способа исполнить код на plan, пока в него не положили <code>hashicorp/external</code>. Если этот провайдер нужен команде, блоки <code>data &quot;external&quot;</code> придётся ловить на ревью.</li>
<li>Изменения в <code>required_providers</code>, в <code>source</code> модулей и в конфигурации CI требуют апрува владельца через <code>CODEOWNERS</code>.</li>
</ul>
<h3 id="шаг-5-сеть-раннера">Шаг 5. Сеть раннера</h3>
<p>Раннеру для работы нужны твой VCS, твоё зеркало и API облака. Всё остальное можно закрыть. Только не сразу: начни с режима аудита, неделю собирай список доменов и лишь потом блокируй. Для GitHub-hosted раннеров это умеет Harden-Runner с режимами <code>audit</code> и <code>block</code> (бесплатно он работает только на публичных репозиториях, а режим <code>block</code> есть только на Linux), а для своих раннеров в Kubernetes подойдут сетевые политики с правилами по FQDN.</p>
<h3 id="шаг-6-инвентарь-и-учения">Шаг 6. Инвентарь и учения</h3>
<p>Образ CI, в который зашиты Terraform, Ansible и сканеры, заслуживает SBOM не меньше, чем образ сервиса. Когда выйдет следующая новость про заражённый инструмент, вопрос «а у нас он есть и какой версии» должен закрываться запросом, а не опросом в чате.</p>
<p>Раз в квартал полезно пройтись по декларативным спискам: что устарело, что сменило владельца, что лежит в зеркале и никем не используется.</p>
<p>И последнее, уже из практики инцидентов. Сценарий «скомпрометирован инструмент из пайплайна» стоит прогнать на game day заранее. Действий в нём немного: найти все запуски за окно компрометации, считать утёкшим всё, до чего дотягивался раннер, и сменить это всё. Смена в день настоящего инцидента занимает часы, если список известен, и недели, если его составляют на ходу.</p>
<h2 id="где-это-не-работает">Где это не работает</h2>
<p>Раздел про меры был бы пропагандой без раздела про их границы.</p>
<p><strong>Карантин проигрывает длинному окну обнаружения.</strong> Бэкдор в xz прожил в релизах пять недель, подмену у Codecov два месяца никто не замечал. Выдержка в 14 дней пропускает обе истории.</p>
<p><strong>Пин замораживает и уязвимости.</strong> Закреплённая по SHA версия сама не обновится. Без Renovate или его аналога через год ты сидишь на идеально неизменяемом дырявом наборе.</p>
<p><strong>Транзитивные ссылки остаются подвижными.</strong> Закреплённый экшен может вызывать другой по тегу, как было у reviewdog. Коллекция Ansible тянет зависимости по диапазону версий. Образ джобы, закреплённый по digest, мог быть собран из <code>FROM ...:latest</code>.</p>
<p><strong>Зеркало не проверяет содержимое.</strong> Оно копирует то, что upstream отдавал в момент синхронизации, и если та попала в окно компрометации, у тебя лежит аккуратная копия малвари с правильным хэшем. Поэтому зеркало без карантина и без сверки с подписью издателя решает только задачу доступности.</p>
<p><strong>Машины разработчиков остаются снаружи.</strong> Поддельный провайдер <code>awsbeta</code>, судя по набору краденого, целил в ноутбук разработчика, а не в CI. Раздельные раннеры и сетевые фильтры туда не дотягиваются. Помогают только <code>~/.terraformrc</code> с зеркалом без <code>direct</code> на каждом ноутбуке и закреплённые хуки pre-commit.</p>
<p><strong>Проверяющий инструмент сам остаётся зависимостью.</strong> Сканер, которым ты проверяешь зеркало, приехал откуда-то и с какими-то правами. Рекурсия не разрывается, её можно только сделать короче: закрепить сканер так же, как всё остальное, и не давать ему секретов сверх необходимого.</p>
<p><strong>Цена.</strong> Команда из трёх человек не будет поддерживать Galaxy NG, зеркало Forge и собственный PyPI. Для неё реалистичный минимум такой: шаг 1 целиком, статическое зеркало провайдеров и форки формул, ролей и модулей в свой git. Это закрывает подмену существующих версий и тайпсквот, то есть большую часть таблицы из хроники.</p>
<h2 id="что-сделать-в-понедельник">Что сделать в понедельник</h2>
<p>Те же семь шагов, только разложенные не по цене, а по срокам. В понедельник делается первое, остальное идёт в план.</p>
<ul>
<li><strong>За час.</strong> Начало шага 1: <code>terraform providers lock</code> под все платформы, lock-файл в git, <code>-lockfile=readonly</code> в CI.</li>
<li><strong>За день.</strong> Дальше по шагу 1: закрепить по SHA экшены, include, образы джобов и хуки pre-commit, включить в политике организации требование SHA. И шаг 0: инвентаризация и лог исходящих соединений раннера за неделю.</li>
<li><strong>За неделю.</strong> Статическое зеркало провайдеров без <code>direct</code> из шага 2 и форки формул, ролей и модулей в свой git. Сюда же шаг 3: выдержка новых версий в Renovate.</li>
<li><strong>За квартал.</strong> Остальное: свой Galaxy с подписями из шага 2, раздельные раннеры для plan и apply и OIDC вместо статических ключей из шага 4, сетевой фильтр из шага 5, инвентарь и учения из шага 6.</li>
</ul>
<h2 id="заключение">Заключение</h2>
<p>Конвейер из шести шагов для Helm-чарта и три случайных способа доставки для провайдера Terraform уживались в одном контуре годами. Чарт при этом управляет одним кластером, а провайдер держит ключи от облака.</p>
<p>Чтобы этот перекос исправить, ничего нового изобретать не нужно. Зеркало, карантин, пин по хэшу и раздельные права давно работают для зависимостей приложений. Осталось перестать называть Terraform, Ansible и сканеры «инструментами» и начать называть их зависимостями.</p>
<h2 id="что-почитать">Что почитать</h2>
<ul>
<li><a href="https://labs.snyk.io/resources/gitflops-dangers-of-terraform-automation-platforms" rel="noopener noreferrer" target="_blank">GitFlops: The Dangers of Terraform Automation Platforms</a> — почему plan исполняет код.</li>
<li><a href="https://labs.boostsecurity.io/articles/erosion-of-trust-unmasking-supply-chain-vulnerabilities-in-the-terraform-registry" rel="noopener noreferrer" target="_blank">Erosion of Trust: Unmasking Supply Chain Vulnerabilities in the Terraform Registry</a> — модули и подвижные теги.</li>
<li><a href="https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack" rel="noopener noreferrer" target="_blank">Trivy Compromised: Everything You Need to Know</a> — хронология и индикаторы.</li>
<li><a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-coder-registry-terraform-supply-chain-2026/" rel="noopener noreferrer" target="_blank">Coder Registry Compromise</a> — подмена модулей через инфраструктуру доставки.</li>
<li><a href="https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation" rel="noopener noreferrer" target="_blank">hackerbot-claw</a> — пять техник атаки на GitHub Actions в одном разборе.</li>
<li><a href="https://developer.hashicorp.com/terraform/language/files/dependency-lock" rel="noopener noreferrer" target="_blank">Dependency Lock File</a> — что именно фиксирует Terraform и чего не фиксирует.</li>
<li><a href="https://slsa.dev" rel="noopener noreferrer" target="_blank">SLSA</a> — рамка, в которую всё это укладывается.</li>
</ul>
</div>]]></content:encoded>
    </item><item>
      <title>Slidev: презентация как код, от пустой папки до PDF и GitHub Pages</title>
      <link>https://jtprog.ru/posts/slidev/</link>
      <pubDate>Mon, 05 Oct 2026 19:00:00 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/slidev/</guid>
      <category>Slidev</category>
      <category>Markdown</category>
      <category>GitHub Actions</category>
      <category>CLI</category>
      <category>Howto</category>
      <category domain="category">Howto</category>
      <enclosure url="https://jtprog.ru/cover.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Слайды я собираю там же, где пишу посты и правлю конфиги: в редакторе, в Markdown и с git под боком. Мышкой блоки не двигаю, шрифты от слайда к слайду не выравниваю, а вместо «финал_v3_точно_финал.pptx» у меня <code>git log</code>.</p>
<p>Делается это на <a href="https://sli.dev" rel="noopener noreferrer" target="_blank">Slidev</a>. Написать про него я собирался ещё в декабре 2023-го, а руки дошли только сейчас: к Стачке собрал на нём доклад про SRE MCP, и репозиторий получился таким, что его удобно разобрать по косточкам.</p>
<p>После этого поста у тебя будет своя презентация: по файлу на слайд, с заметками спикера рядом с текстом, с PDF одной командой и сайтом, который выкатывается на GitHub Pages по <code>git push</code>. Все примеры взяты из живого репозитория <a href="https://github.com/jtprogru/nastachku-2026" rel="noopener noreferrer" target="_blank">nastachku-2026</a>, результат можно <a href="https://jtprogru.github.io/nastachku-2026/" rel="noopener noreferrer" target="_blank">полистать в браузере</a>.</p>
<aside class="refresh-banner" role="note">
  <span class="refresh-banner__dot" aria-hidden="true"></span>
  <span class="refresh-banner__label"><time datetime="2026-10-13">13 октября 2026</time> — обновлено
  </span><span class="refresh-banner__text">В шаге 8 поправил строку про <code>--range</code>: в Slidev 53.0.0 он режет PDF только в паре с <code>--per-slide</code>. В <a href="#%d1%82%d0%be%d0%bd%d0%ba%d0%b8%d0%b5-%d0%bc%d0%b5%d1%81%d1%82%d0%b0-%d0%bd%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d0%bb%d0%b5%d0%b3%d0%ba%d0%be-%d1%81%d0%bf%d0%be%d1%82%d0%ba%d0%bd%d1%83%d1%82%d1%8c%d1%81%d1%8f">тонкие места</a> добавил номер слайда, который повторяется при экспорте с <code>--with-clicks</code>.</span></aside>

<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">По соседству</p>
    <div class="callout__content"><ul>
<li><a href="/posts/github-actions/">Github Actions для блогера</a> — как по пушу собирается и выкатывается этот блог;</li>
<li><a href="/posts/taskfile/">Taskfile</a> — про <code>make</code> и его более молодого сменщика, пригодится для обёрток вокруг сборки;</li>
<li><a href="/posts/how-read-qr-code/">Как декодировать QR-код руками</a> — на случай, если на финальном слайде у тебя будет QR.</li>
</ul>
</div>
  </div>
</aside>

<h2 id="зачем-слайдам-git">Зачем слайдам git</h2>
<p>Презентацию переписывают много раз. У моего доклада 46 коммитов за девять дней, и почти каждый из них — правка формулировки, перестановка слайдов или попытка влезть в слот. В pptx такая история выглядит как папка с копиями одного файла. Здесь это обычный <code>git diff</code>, по которому видно, что поменялось и зачем.</p>
<p>Оформление при этом живёт отдельно от содержания. Тема ставится зависимостью, и следующий доклад начинается со строчки <code>theme: bear</code>. Копировать прошлую презентацию и вычищать из неё чужие слайды больше не надо.</p>
<p>И из одних исходников получаются сразу сайт и PDF, оба в светлой и тёмной теме.</p>
<p>Плата за всё это — Node.js в зависимостях и вёрстка руками там, где не хватило готовой раскладки. Кому такой подход не подойдёт, скажу в конце.</p>
<h2 id="что-понадобится">Что понадобится</h2>
<ul>
<li>Node.js 22.12 или новее: это требование <code>@slidev/cli</code> 53;</li>
<li>npm, он приезжает вместе с Node.js;</li>
<li>git и аккаунт на GitHub, если нужен деплой;</li>
<li><code>make</code> — по желанию, для обёрток;</li>
<li>Markdown на уровне README. Vue знать не обязательно, пока хватает готовых компонентов.</li>
</ul>
<p>Команды ниже проверены на Slidev 53.0.0 и Node.js 26.</p>
<h2 id="шаг-1-заведи-проект">Шаг 1. Заведи проект</h2>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm init slidev@latest</span></span></code></pre></div>
</div>
<p>Стартер спросит имя проекта и предложит сразу поставить зависимости и запустить dev-сервер. В созданной папке важны две вещи: <code>slides.md</code> с самими слайдами и <code>package.json</code> с тремя скриптами — <code>dev</code>, <code>build</code> и <code>export</code>. Каталоги <code>components/</code>, <code>pages/</code> и <code>snippets/</code> лежат там как примеры, их можно сносить.</p>
<p>Если от автозапуска отказался, подними сервер сам. Проект я назвал <code>my-talk</code>, у тебя будет то имя, которое ты ввёл стартеру:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> my-talk
</span></span><span class="line"><span class="cl">npm install
</span></span><span class="line"><span class="cl">npm run dev</span></span></code></pre></div>
</div>
<p>Проверка: браузер открыл <code>http://localhost:3030</code> с титульным слайдом. Поправь заголовок в <code>slides.md</code> и сохрани файл — слайд обновится без перезагрузки страницы.</p>
<p>Что стоит запомнить сразу:</p>
<table>
	<thead>
			<tr>
					<th>Адрес или клавиша</th>
					<th>Что делает</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/presenter</code></td>
					<td>режим докладчика: текущий слайд, следующий, заметки и таймер</td>
			</tr>
			<tr>
					<td><code>/overview</code></td>
					<td>все слайды списком вместе с заметками</td>
			</tr>
			<tr>
					<td><code>o</code></td>
					<td>сетка слайдов для быстрого перехода</td>
			</tr>
			<tr>
					<td><code>g</code></td>
					<td>перейти к нужному слайду</td>
			</tr>
			<tr>
					<td><code>d</code></td>
					<td>переключить светлую и тёмную тему</td>
			</tr>
	</tbody>
</table>
<h2 id="шаг-2-напиши-слайд">Шаг 2. Напиши слайд</h2>
<p>Презентация в Slidev — это Markdown-файл, в котором слайды разделены строкой <code>---</code>. У слайда три части: frontmatter с настройками, тело и заметки спикера в HTML-комментарии в самом конце.</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">layout</span><span class="p">:</span><span class="w"> </span><span class="l">statement</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Дежурный не чинит.&lt;br&gt;Дежурный ищет.
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ночь, тикет, четырнадцать дашбордов.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">&lt;!--
</span></span></span><span class="line"><span class="cl"><span class="cm">Крючок, 40 секунд. Сцену знает каждый дежурный, из неё вырастает весь доклад.
</span></span></span><span class="line"><span class="cl"><span class="cm">--&gt;</span></span></span></code></pre></div>
</div>
<p><code>layout</code> выбирает раскладку: <code>statement</code> ставит одну мысль по центру экрана, <code>two-cols-header</code> даёт заголовок и две колонки, <code>section</code> рисует разделитель блока. Без этого поля слайд получает раскладку <code>default</code>.</p>
<p>Самый первый frontmatter в файле работает за двоих. Он настраивает титульный слайд и заодно всю презентацию, поэтому называется headmatter:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">theme</span><span class="p">:</span><span class="w"> </span><span class="l">bear</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">layout</span><span class="p">:</span><span class="w"> </span><span class="l">cover</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;SRE MCP&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="l">Михаил Савин</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">transition</span><span class="p">:</span><span class="w"> </span><span class="l">slide-left</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">drawings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">persist</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span></span></span></code></pre></div>
</div>
<p>Теперь слайд посложнее, с графиком и врезкой:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">&lt;Kicker&gt;01 · арифметика&lt;/Kicker&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Куда уходит время инцидента
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;div class=&#34;grid grid-cols-[1.6fr_1fr] gap-12 items-center mt-6&#34;&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;BarChart unit=&#34; мин&#34; :items=&#34;[{ label: &#39;Обнаружение&#39;, value: 923, highlight: true }, { label: &#39;Диагноз&#39;, value: 23 }, { label: &#39;Починка&#39;, value: 25 }]&#34; /&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;div&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;Callout type=&#34;important&#34; title=&#34;Главный вывод&#34;&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Дольше всего отказ живёт до первого сигнала: в 28 случаях из 38 его первым замечает клиент. Диагноз и починка занимают минуты, и только диагноз можно отдать ИИ без права на запись.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/Callout&gt;
</span></span><span class="line"><span class="cl"><span class="k">
</span></span></span><span class="line"><span class="cl"><span class="k">&gt; </span><span class="ge">*Медианы по 48 нашим постмортемам, февраль–сентябрь 2026.*
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/div&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/div&gt;</span></span></code></pre></div>
</div>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/slide-light_hu_ff7a384e7f163f7.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/slide-light_hu_ac41bba85660febc.png"
             alt="Слайд «Куда уходит время инцидента»: слева горизонтальный график из трёх столбцов, справа врезка с главным выводом"
             width="1200" height="676" title="Тот же слайд после сборки">
      </picture><figcaption>Тот же слайд после сборки</figcaption></figure><p>В этом куске три вещи, на которых держится вся вёрстка в Slidev.</p>
<p>Теги с заглавной буквы — Vue-компоненты. <code>Kicker</code>, <code>BarChart</code> и <code>Callout</code> приехали из темы, импортировать их не надо. Данные для графика лежат прямо в слайде, и когда поменяется цифра, картинку перерисовывать не придётся.</p>
<p>Классы у <code>&lt;div&gt;</code> — утилиты UnoCSS, по синтаксису как в Tailwind. <code>grid grid-cols-[1.6fr_1fr] gap-12</code> раскладывает слайд на две колонки без единой строчки CSS.</p>
<p>Пустые строки вокруг Markdown внутри HTML-тегов обязательны. Без них парсер примет содержимое за сырой HTML, и вместо жирного текста ты увидишь звёздочки.</p>
<h2 id="шаг-3-разложи-слайды-по-файлам">Шаг 3. Разложи слайды по файлам</h2>
<p>Стартер предлагает писать всё в один <code>slides.md</code>. На десяти слайдах это удобно. На сорока, да ещё с заметками спикера, у меня вышло бы больше полутора тысяч строк в одном файле, и любой <code>diff</code> превратился бы в кашу.</p>
<p>Slidev умеет подключать слайд из другого файла через <code>src:</code>. Тогда <code>slides.md</code> остаётся оглавлением:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># ── 01 · Арифметика MTTR ──</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">src</span><span class="p">:</span><span class="w"> </span><span class="l">./pages/01-mttr/00-section.md</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">src: ./pages/01-mttr/01-mttr.md
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">src: ./pages/01-mttr/02-thesis.md
</span></span><span class="line"><span class="cl">---</span></span></code></pre></div>
</div>
<p>А сами слайды лежат по одному в файле:</p>
<div class="codeblock codeblock--txt" data-lang="txt"><span class="codeblock__lang" aria-hidden="true">txt</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── slides.md              # headmatter, титул и порядок слайдов
</span></span><span class="line"><span class="cl">├── pages/
</span></span><span class="line"><span class="cl">│   ├── 00-intro/
</span></span><span class="line"><span class="cl">│   │   ├── 01-hook.md
</span></span><span class="line"><span class="cl">│   │   └── 02-speaker.md
</span></span><span class="line"><span class="cl">│   ├── 01-mttr/
</span></span><span class="line"><span class="cl">│   │   ├── 00-section.md  # разделитель блока
</span></span><span class="line"><span class="cl">│   │   ├── 01-mttr.md
</span></span><span class="line"><span class="cl">│   │   └── 02-thesis.md
</span></span><span class="line"><span class="cl">│   └── backup/            # бэкап-слайды «если спросят»
</span></span><span class="line"><span class="cl">├── components/            # свои Vue-компоненты
</span></span><span class="line"><span class="cl">├── public/                # картинки и QR-коды
</span></span><span class="line"><span class="cl">├── setup/routes.ts        # правка маршрутов, см. шаг 4
</span></span><span class="line"><span class="cl">└── style.css              # глобальные правки поверх темы</span></span></code></pre></div>
</div>
<p>Правила, которые у меня прижились:</p>
<ul>
<li>папка — блок доклада, <code>NN</code> в её имени совпадает с номером блока на слайдах;</li>
<li>файл называется <code>MM-слаг.md</code>, где <code>MM</code> — позиция слайда внутри блока. Вставил слайд в середину — перенумеруй соседей, иначе порядок файлов в папке разойдётся с порядком в презентации;</li>
<li>frontmatter слайда живёт в файле страницы, в <code>slides.md</code> остаются только <code>src:</code> и <code>hide:</code>;</li>
<li>YAML-комментарий над первым <code>src:</code> блока работает как заголовок раздела в оглавлении.</li>
</ul>
<p>После этого <code>slides.md</code> на всю презентацию занимает 240 строк, переставить слайд — значит перенести три строки, а правка текста трогает ровно один файл.</p>
<h2 id="шаг-4-дай-слайдам-имена">Шаг 4. Дай слайдам имена</h2>
<p>По умолчанию адрес слайда — его номер: <code>/7</code>, <code>/21</code>. Стоит вставить один слайд в начало, и все ссылки внутри презентации вместе с сохранёнными закладками начинают вести не туда.</p>
<p>Лечится это полем <code>routeAlias</code> во frontmatter слайда:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">routeAlias: gates
</span></span><span class="line"><span class="cl">---</span></span></code></pre></div>
</div>
<p>Теперь слайд открывается по адресу <code>/gates</code> и останется там, куда бы ты его ни переставил. Алиас у меня совпадает со слагом файла без номера. К повторяющимся слагам добавляю префикс блока: <code>mttr-section</code>, <code>permissions-thesis</code>.</p>
<p>На алиасы опираются и переходы по клику. Итоговый слайд с карточками у меня работает как меню: на вопросах из зала по карточке можно прыгнуть к нужной теме.</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">&lt;Card to=&#34;gate-1&#34; kicker=&#34;gate 01&#34; title=&#34;Read-only&#34;&gt;Токен не может писать&lt;/Card&gt;
</span></span><span class="line"><span class="cl">&lt;Card to=&#34;gate-4&#34; kicker=&#34;gate 04&#34; title=&#34;Аудит на сервере&#34;&gt;Журнал пишет не агент&lt;/Card&gt;</span></span></code></pre></div>
</div>
<p><code>Card</code> — компонент темы. Без неё то же самое делает встроенный <code>&lt;Link to=&quot;gates&quot;&gt;текст ссылки&lt;/Link&gt;</code>.</p>
<p>Остаётся одна шероховатость: корень сайта и <code>/presenter</code> Slidev жёстко ведёт на <code>/1</code>. Чтобы номер не всплывал даже при первом открытии, маршруты можно поправить в <code>setup/routes.ts</code>:</p>
<div class="codeblock codeblock--ts" data-lang="ts"><span class="codeblock__lang" aria-hidden="true">ts</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineRoutesSetup</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@slidev/types&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineRoutesSetup</span><span class="p">((</span><span class="nx">routes</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">route</span> <span class="k">of</span> <span class="nx">routes</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">route</span><span class="p">.</span><span class="nx">path</span> <span class="o">===</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nx">route</span><span class="p">.</span><span class="nx">redirect</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">&#39;/cover&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">route</span><span class="p">.</span><span class="nx">path</span> <span class="o">===</span> <span class="s1">&#39;/presenter&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nx">route</span><span class="p">.</span><span class="nx">redirect</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">&#39;/presenter/cover&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">routes</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div>
</div>
<p>Здесь <code>cover</code> — алиас титульного слайда. Он задаётся в headmatter той же строкой <code>routeAlias: cover</code>.</p>
<p>Проверка: открой <code>http://localhost:3030/</code>. В адресной строке должно оказаться <code>/cover</code>, а не <code>/1</code>.</p>
<h2 id="шаг-5-заметки-спикера-и-клики">Шаг 5. Заметки спикера и клики</h2>
<p>Последний HTML-комментарий на слайде — заметка спикера. В зале её не видно, а в режиме докладчика она стоит рядом со слайдом.</p>
<p>Список, который должен появляться по пунктам, оборачивается в <code>&lt;v-clicks&gt;</code>. Маркер <code>[click]</code> в заметке привязывает абзац к клику, и в режиме докладчика подсвечивается та фраза, до которой ты дошёл:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">&lt;v-clicks&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Запустил WAFaaS и собрал команду SRE
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Запустил SRE-комьюнити и подкаст про SRE
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/v-clicks&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">&lt;!--
</span></span></span><span class="line"><span class="cl"><span class="cm">0:50–1:15 · 25 с. Без биографии: по фразе на клик.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">[click] Запускал WAFaaS и собирал команду SRE.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">[click] Запустил SRE-комьюнити и подкаст.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Переход: «Об этом и доклад».
</span></span></span><span class="line"><span class="cl"><span class="cm">--&gt;</span></span></span></code></pre></div>
</div>
<p>Заметку я пишу по одному шаблону. Первая строка — тайминг и главная мысль слайда. Дальше текст, который произношу. Потом переход к следующему слайду. В конце справка: цифры и детали, которые нужны на вопросах, но вслух не идут.</p>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/presenter_hu_4e4c189586e2aafe.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/presenter_hu_b60f9a973241904.png"
             alt="Режим докладчика в Slidev: слева текущий слайд, справа следующий слайд и заметка с таймингом"
             width="1200" height="675" title="Режим докладчика: текущий слайд, следующий и заметка">
      </picture><figcaption>Режим докладчика: текущий слайд, следующий и заметка</figcaption></figure><p>На <code>/overview</code> те же заметки лежат списком рядом с миниатюрами, а в углу стоит счётчик. У моего доклада там 37 слайдов, 67 кликов и 4 284 слова. По этим цифрам удобно прикидывать хронометраж ещё до первого прогона.</p>
<h2 id="шаг-6-спрячь-а-не-удаляй">Шаг 6. Спрячь, а не удаляй</h2>
<p>Первый план доклада был расписан на 25:25. На живых прогонах он занял 37 минут при слоте в 30 вместе с вопросами. Пришлось ужиматься до 18:20, и шесть слайдов из показа ушли.</p>
<p>Удалять я их не стал. В <code>slides.md</code> у каждого появилась одна строка:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">src: ./pages/01-mttr/04-research.md
</span></span><span class="line"><span class="cl">hide: true
</span></span><span class="line"><span class="cl">---</span></span></code></pre></div>
</div>
<p>Слайд остаётся на своём месте в оглавлении, со своим номером файла и заметкой. В заметке к каждому скрытому слайду у меня записано, что поправить на соседях, если его вернуть. Чтобы вернуть, достаточно убрать <code>hide: true</code>.</p>
<h2 id="шаг-7-тема-компоненты-и-картинки">Шаг 7. Тема, компоненты и картинки</h2>
<p>Стартер приезжает с темой <code>seriph</code>. Другую можно выбрать в <a href="https://sli.dev/resources/theme-gallery" rel="noopener noreferrer" target="_blank">галерее</a>: пишешь её имя в <code>theme:</code>, и dev-сервер сам предложит поставить пакет.</p>
<p>Я пользуюсь своей, <a href="https://github.com/jtprogru/slidev-theme-bear" rel="noopener noreferrer" target="_blank">slidev-theme-bear</a>. В npm её нет, она ставится прямо из GitHub:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm i -D github:jtprogru/slidev-theme-bear</span></span></code></pre></div>
</div>
<p>В ней семнадцать своих раскладок, компоненты для врезок, карточек, графиков и схем, шрифты с кириллицей в зависимостях и проверка контраста по WCAG AA. Код темы открыт для некоммерческого использования, а вот медведь мой: в форке бренд придётся заменить на свой.</p>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/slide-cover_hu_2dc6f624a1165029.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/slide-cover_hu_467715e78dd754b3.png"
             alt="Титульный слайд доклада: логотип конференции слева вверху, название по центру, маскот-медведь в правом нижнем углу"
             width="1200" height="676" title="Титул: логотип и маскот вставлены компонентами">
      </picture><figcaption>Титул: логотип и маскот вставлены компонентами</figcaption></figure><p>Всё, чего нет в теме, лежит в самом проекте:</p>
<ul>
<li><code>components/</code> — свои Vue-компоненты, подключаются автоматически по имени файла. У меня их два: схема архитектуры и логотип конференции. В шаблоне организаторов логотип был только в PNG, поэтому он обведён в вектор и вставлен инлайном: так он перекрашивается вместе с темой и не ломается в PDF;</li>
<li><code>public/</code> — статика. Файл <code>public/avatar.jpg</code> доступен на слайде как <code>/avatar.jpg</code>;</li>
<li><code>style.css</code> — глобальные правки поверх темы. У меня там одна CSS-переменная, которая делает маскота монохромным;</li>
<li><code>&lt;style&gt;</code> в конце файла слайда — стили только для него. Выручает на плотных слайдах: основной текст уменьшается, а заголовки остаются в размере темы и не прыгают от слайда к слайду;</li>
<li>иконки — через Iconify. Ставишь набор командой <code>npm i -D @iconify-json/simple-icons</code> и пишешь <code>&lt;simple-icons-go /&gt;</code>. Иконки наследуют цвет текста и перекрашиваются вместе с темой.</li>
</ul>
<h2 id="шаг-8-собери-pdf">Шаг 8. Собери PDF</h2>
<p>Экспорт идёт через headless-браузер, поэтому сначала нужен Playwright:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm i -D playwright-chromium
</span></span><span class="line"><span class="cl">npx slidev export</span></span></code></pre></div>
</div>
<p>Проверка: рядом со <code>slides.md</code> появился <code>slides-export.pdf</code>, и страниц в нём столько же, сколько слайдов в презентации без скрытых.</p>
<p>Полезные флаги:</p>
<table>
	<thead>
			<tr>
					<th>Флаг</th>
					<th>Что делает</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--dark</code></td>
					<td>экспорт в тёмной теме</td>
			</tr>
			<tr>
					<td><code>--with-clicks</code></td>
					<td>отдельная страница на каждый клик</td>
			</tr>
			<tr>
					<td><code>--range 1,4-5</code></td>
					<td>только выбранные слайды; в PDF работает в паре с <code>--per-slide</code></td>
			</tr>
			<tr>
					<td><code>--per-slide</code></td>
					<td>рендерит слайды по одному и склеивает из них PDF</td>
			</tr>
			<tr>
					<td><code>--format png</code></td>
					<td>по картинке на слайд</td>
			</tr>
			<tr>
					<td><code>--format pptx</code></td>
					<td>pptx, в котором каждый слайд лежит картинкой</td>
			</tr>
			<tr>
					<td><code>--output &lt;файл&gt;</code></td>
					<td>своё имя файла вместо <code>slides-export</code></td>
			</tr>
	</tbody>
</table>
<p>У <code>--range</code> и <code>--with-clicks</code> есть по сюрпризу, оба разобраны в <a href="#%d1%82%d0%be%d0%bd%d0%ba%d0%b8%d0%b5-%d0%bc%d0%b5%d1%81%d1%82%d0%b0-%d0%bd%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d0%bb%d0%b5%d0%b3%d0%ba%d0%be-%d1%81%d0%bf%d0%be%d1%82%d0%ba%d0%bd%d1%83%d1%82%d1%8c%d1%81%d1%8f">тонких местах</a>.</p>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/slide-dark_hu_752d2b16b86f512e.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/slide-dark_hu_b33b2322c9871994.png"
             alt="Слайд с графиком в тёмной теме: те же данные на тёмном фоне"
             width="1200" height="676" title="Тот же слайд с флагом --dark">
      </picture><figcaption>Тот же слайд с флагом --dark</figcaption></figure><p>Добавь <code>slides-export*.pdf</code> в <code>.gitignore</code>: стартер создаёт его без этой строки.</p>
<p>Организаторы обычно просят файл, и PDF тут надёжнее всего. А pptx из Slidev годится разве что как запасной вариант: слайды в нём лежат картинками, текст не выделяется и не правится.</p>
<h2 id="шаг-9-выложи-слайды-на-github-pages">Шаг 9. Выложи слайды на GitHub Pages</h2>
<p><code>slidev build</code> собирает статический сайт в <code>dist/</code>. На GitHub Pages сайт живёт в подкаталоге с именем репозитория, и об этом ему надо сказать флагом <code>--base</code>:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx slidev build --base /nastachku-2026/</span></span></code></pre></div>
</div>
<p>Дальше workflow <code>.github/workflows/build.yaml</code>, который делает то же самое на каждый push в <code>main</code>:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">&#39;on&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">github-pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">${{ steps.deployment.outputs.page_url }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-node@v7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">node-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;lts/*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install dependencies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm install</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run build -- --base /nastachku-2026/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/configure-pages@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-pages-artifact@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dist</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/deploy-pages@v5</span></span></span></code></pre></div>
</div>
<p>В своём репозитории поменяй <code>/nastachku-2026/</code> на его имя и один раз включи Pages: Settings → Pages → Source → GitHub Actions.</p>
<p>Проверка: после зелёного прогона слайды открываются по адресу <code>https://&lt;логин&gt;.github.io/&lt;репозиторий&gt;/</code>.</p>
<p>Заодно я включил Dependabot: раз в неделю он приносит обновления npm-зависимостей и версий actions.</p>
<h2 id="makefile-чтобы-не-помнить-флаги">Makefile, чтобы не помнить флаги</h2>
<div class="codeblock codeblock--make" data-lang="make"><span class="codeblock__lang" aria-hidden="true">make</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nf">node_modules</span><span class="o">:</span> <span class="n">package</span>.<span class="n">json</span> <span class="n">package</span>-<span class="n">lock</span>.<span class="n">json</span>
</span></span><span class="line"><span class="cl">	npm ci
</span></span><span class="line"><span class="cl">	@touch node_modules
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">dev</span>
</span></span><span class="line"><span class="cl"><span class="nf">dev</span><span class="o">:</span> <span class="n">node_modules</span>
</span></span><span class="line"><span class="cl">	npm run dev
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span> <span class="n">node_modules</span>
</span></span><span class="line"><span class="cl">	npm run build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">THEME</span> <span class="o">?=</span> light
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">export</span>
</span></span><span class="line"><span class="cl"><span class="nf">export</span><span class="o">:</span> <span class="n">node_modules</span>
</span></span><span class="line"><span class="cl">	npm run <span class="nb">export</span> -- <span class="k">$(if</span> <span class="k">$(</span>filter dark,<span class="k">$(</span>THEME<span class="k">))</span>,--dark --output slides-export-dark.pdf<span class="k">)</span>
</span></span></code></pre></div>
</div>
<p>Весь трюк в первой цели. <code>node_modules</code> объявлен обычной файловой целью, которая зависит от манифестов. Зависимости ставятся, только если каталога нет или <code>package.json</code> с локом новее него. Поэтому <code>make dev</code> на свежем клоне работает сразу, а второй <code>make build</code> подряд ничего не переустанавливает.</p>
<p><code>make export THEME=dark</code> кладёт тёмный PDF в отдельный файл, светлый при этом не перезаписывается.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong><code>hide: true</code> выкидывает слайд целиком.</strong> Из показа, из сборки и из PDF, и ссылка на него перестаёт работать. Скрытых слайдов «на всякий случай», до которых можно дойти кнопкой со слайда «Вопросы», так не сделать. Мои бэкапы поэтому лежат в конце презентации под <code>hide: true</code>, а ответы на эти вопросы записаны в заметках.</li>
<li><strong>Заметки спикера едут в публичную сборку.</strong> На выложенном сайте работает <code>/presenter</code>, и твой текст вместе с ответами на острые вопросы может прочитать кто угодно. Если это в планы не входит, собирай с <code>--without-notes</code>.</li>
<li><strong>Прямая ссылка на слайд на GitHub Pages отдаёт код 404.</strong> Браузер при этом покажет нужный слайд: Slidev кладёт копию <code>index.html</code> в <code>404.html</code>, и роутер разбирается сам. А вот <code>curl</code>, проверка ссылок и превью в мессенджерах увидят честный 404. Если прямые ссылки важны, смотри в сторону <code>--router-mode hash</code>.</li>
<li><strong>Первый Markdown-заголовок слайда становится его названием.</strong> Поэтому надзаголовок перед <code>#</code> нельзя написать как <code>###</code>: названием слайда станет он. В теме для этого отдельный компонент <code>Kicker</code>.</li>
<li><strong>SVG через <code>&lt;img&gt;</code> не перекрашивается.</strong> <code>currentColor</code> внутри <code>&lt;img&gt;</code> превращается в чёрный, и на тёмной теме логотип пропадает. Всё, что должно менять цвет вместе с темой, вставляй инлайном через компонент.</li>
<li><strong>Шрифты из сети.</strong> Slidev умеет подтягивать шрифты из Google Fonts, и на площадке без интернета это неприятный сюрприз. В моей теме шрифты лежат в зависимостях, и слайды показываются офлайн.</li>
<li><strong>Номер слайда при <code>--with-clicks</code> повторяется.</strong> Сам Slidev номеров не рисует. Их ставит тема, и считает она слайды. Слайд с четырьмя кликами превращается в пять страниц подряд с одним и тем же <code>3 / 37</code>. Убрать номер из PDF можно одним правилом в <code>style.css</code>, потому что при экспорте Slidev вешает на <code>&lt;html&gt;</code> класс <code>print</code>: <code>html.print .bear-footer .tabular-nums { display: none; }</code>. Селектор здесь от моей темы, свой подсмотри в devtools. С <code>--per-slide</code> правило не сработает: там класса <code>print</code> нет. В своей теме я это починил: с <code>--with-clicks</code> номер из футера пропадает сам, а в обычном PDF остаётся.</li>
<li><strong><code>--range</code> в обычном PDF ничего не отрезает.</strong> В Slidev 53.0.0 команда <code>npx slidev export --range 3-4</code> отдала мне все 37 страниц вместо двух. Диапазон начинает работать с <code>--per-slide</code> или с <code>--format png</code>. Только не добавляй к <code>--per-slide</code> ещё и <code>--with-clicks</code>: клики не раскрываются, и каждый слайд попадает в PDF в состоянии до первого клика. Кусок доклада по кликам проще забрать картинками: <code>--format png --with-clicks --range 3-4</code> отдаёт по файлу на клик.</li>
<li><strong>Dependabot и тема из GitHub.</strong> Зависимость вида <code>github:jtprogru/slidev-theme-bear#v0.3.1</code> он предлагал «обновить» до v0.2.0. Тему пришлось добавить в <code>ignore</code> и бампать руками.</li>
<li><strong>Новому слайду алиас нужен сразу.</strong> Без <code>routeAlias</code> он откроется по номеру, и первая же перестановка сломает ссылку на него.</li>
</ul>
<h2 id="кому-slidev-не-подойдёт">Кому Slidev не подойдёт</h2>
<ul>
<li>Организаторы требуют pptx по своему шаблону и собираются править его сами. Картинки внутри pptx им не помогут.</li>
<li>Над слайдами работает несколько человек, и не все из них дружат с git.</li>
<li>Нужна презентация на пять слайдов к завтрашнему синку. Поднимать ради неё Node.js-проект дольше, чем нарисовать слайды руками.</li>
</ul>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<ol>
<li>Запусти <code>npm init slidev@latest</code>, выбери тему и убедись, что dev-сервер поднялся.</li>
<li>Сразу разложи слайды по <code>pages/NN-блок/MM-слаг.md</code> и раздай им <code>routeAlias</code>.</li>
<li>Пиши заметку спикера вместе со слайдом, а не накануне: тайминг, текст, переход, справка.</li>
<li>Прогони доклад вслух с таймером и спрячь лишнее через <code>hide: true</code>.</li>
<li>Сделай <code>npx slidev export</code> и пролистай PDF глазами: переносы, плотные слайды, тёмная тема.</li>
<li>Включи деплой на Pages и реши, едут ли заметки в публичную сборку.</li>
</ol>
<p>Как всё это выглядит в сборе, можно подсмотреть в <a href="https://github.com/jtprogru/nastachku-2026" rel="noopener noreferrer" target="_blank">nastachku-2026</a>: там же лежит README с правилами, по которым устроена презентация.</p>
<p>На этом всё! Profit!</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Слайды я собираю там же, где пишу посты и правлю конфиги: в редакторе, в Markdown и с git под боком. Мышкой блоки не двигаю, шрифты от слайда к слайду не выравниваю, а вместо «финал_v3_точно_финал.pptx» у меня <code>git log</code>.</p>
<p>Делается это на <a href="https://sli.dev" rel="noopener noreferrer" target="_blank">Slidev</a>. Написать про него я собирался ещё в декабре 2023-го, а руки дошли только сейчас: к Стачке собрал на нём доклад про SRE MCP, и репозиторий получился таким, что его удобно разобрать по косточкам.</p>
<p>После этого поста у тебя будет своя презентация: по файлу на слайд, с заметками спикера рядом с текстом, с PDF одной командой и сайтом, который выкатывается на GitHub Pages по <code>git push</code>. Все примеры взяты из живого репозитория <a href="https://github.com/jtprogru/nastachku-2026" rel="noopener noreferrer" target="_blank">nastachku-2026</a>, результат можно <a href="https://jtprogru.github.io/nastachku-2026/" rel="noopener noreferrer" target="_blank">полистать в браузере</a>.</p>
<aside class="refresh-banner" role="note">
  <span class="refresh-banner__dot" aria-hidden="true"></span>
  <span class="refresh-banner__label"><time datetime="2026-10-13">13 октября 2026</time> — обновлено
  </span><span class="refresh-banner__text">В шаге 8 поправил строку про <code>--range</code>: в Slidev 53.0.0 он режет PDF только в паре с <code>--per-slide</code>. В <a href="#%d1%82%d0%be%d0%bd%d0%ba%d0%b8%d0%b5-%d0%bc%d0%b5%d1%81%d1%82%d0%b0-%d0%bd%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d0%bb%d0%b5%d0%b3%d0%ba%d0%be-%d1%81%d0%bf%d0%be%d1%82%d0%ba%d0%bd%d1%83%d1%82%d1%8c%d1%81%d1%8f">тонкие места</a> добавил номер слайда, который повторяется при экспорте с <code>--with-clicks</code>.</span></aside>

<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">По соседству</p>
    <div class="callout__content"><ul>
<li><a href="/posts/github-actions/">Github Actions для блогера</a> — как по пушу собирается и выкатывается этот блог;</li>
<li><a href="/posts/taskfile/">Taskfile</a> — про <code>make</code> и его более молодого сменщика, пригодится для обёрток вокруг сборки;</li>
<li><a href="/posts/how-read-qr-code/">Как декодировать QR-код руками</a> — на случай, если на финальном слайде у тебя будет QR.</li>
</ul>
</div>
  </div>
</aside>

<h2 id="зачем-слайдам-git">Зачем слайдам git</h2>
<p>Презентацию переписывают много раз. У моего доклада 46 коммитов за девять дней, и почти каждый из них — правка формулировки, перестановка слайдов или попытка влезть в слот. В pptx такая история выглядит как папка с копиями одного файла. Здесь это обычный <code>git diff</code>, по которому видно, что поменялось и зачем.</p>
<p>Оформление при этом живёт отдельно от содержания. Тема ставится зависимостью, и следующий доклад начинается со строчки <code>theme: bear</code>. Копировать прошлую презентацию и вычищать из неё чужие слайды больше не надо.</p>
<p>И из одних исходников получаются сразу сайт и PDF, оба в светлой и тёмной теме.</p>
<p>Плата за всё это — Node.js в зависимостях и вёрстка руками там, где не хватило готовой раскладки. Кому такой подход не подойдёт, скажу в конце.</p>
<h2 id="что-понадобится">Что понадобится</h2>
<ul>
<li>Node.js 22.12 или новее: это требование <code>@slidev/cli</code> 53;</li>
<li>npm, он приезжает вместе с Node.js;</li>
<li>git и аккаунт на GitHub, если нужен деплой;</li>
<li><code>make</code> — по желанию, для обёрток;</li>
<li>Markdown на уровне README. Vue знать не обязательно, пока хватает готовых компонентов.</li>
</ul>
<p>Команды ниже проверены на Slidev 53.0.0 и Node.js 26.</p>
<h2 id="шаг-1-заведи-проект">Шаг 1. Заведи проект</h2>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm init slidev@latest</span></span></code></pre></div>
</div>
<p>Стартер спросит имя проекта и предложит сразу поставить зависимости и запустить dev-сервер. В созданной папке важны две вещи: <code>slides.md</code> с самими слайдами и <code>package.json</code> с тремя скриптами — <code>dev</code>, <code>build</code> и <code>export</code>. Каталоги <code>components/</code>, <code>pages/</code> и <code>snippets/</code> лежат там как примеры, их можно сносить.</p>
<p>Если от автозапуска отказался, подними сервер сам. Проект я назвал <code>my-talk</code>, у тебя будет то имя, которое ты ввёл стартеру:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> my-talk
</span></span><span class="line"><span class="cl">npm install
</span></span><span class="line"><span class="cl">npm run dev</span></span></code></pre></div>
</div>
<p>Проверка: браузер открыл <code>http://localhost:3030</code> с титульным слайдом. Поправь заголовок в <code>slides.md</code> и сохрани файл — слайд обновится без перезагрузки страницы.</p>
<p>Что стоит запомнить сразу:</p>
<table>
	<thead>
			<tr>
					<th>Адрес или клавиша</th>
					<th>Что делает</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/presenter</code></td>
					<td>режим докладчика: текущий слайд, следующий, заметки и таймер</td>
			</tr>
			<tr>
					<td><code>/overview</code></td>
					<td>все слайды списком вместе с заметками</td>
			</tr>
			<tr>
					<td><code>o</code></td>
					<td>сетка слайдов для быстрого перехода</td>
			</tr>
			<tr>
					<td><code>g</code></td>
					<td>перейти к нужному слайду</td>
			</tr>
			<tr>
					<td><code>d</code></td>
					<td>переключить светлую и тёмную тему</td>
			</tr>
	</tbody>
</table>
<h2 id="шаг-2-напиши-слайд">Шаг 2. Напиши слайд</h2>
<p>Презентация в Slidev — это Markdown-файл, в котором слайды разделены строкой <code>---</code>. У слайда три части: frontmatter с настройками, тело и заметки спикера в HTML-комментарии в самом конце.</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">layout</span><span class="p">:</span><span class="w"> </span><span class="l">statement</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Дежурный не чинит.&lt;br&gt;Дежурный ищет.
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ночь, тикет, четырнадцать дашбордов.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">&lt;!--
</span></span></span><span class="line"><span class="cl"><span class="cm">Крючок, 40 секунд. Сцену знает каждый дежурный, из неё вырастает весь доклад.
</span></span></span><span class="line"><span class="cl"><span class="cm">--&gt;</span></span></span></code></pre></div>
</div>
<p><code>layout</code> выбирает раскладку: <code>statement</code> ставит одну мысль по центру экрана, <code>two-cols-header</code> даёт заголовок и две колонки, <code>section</code> рисует разделитель блока. Без этого поля слайд получает раскладку <code>default</code>.</p>
<p>Самый первый frontmatter в файле работает за двоих. Он настраивает титульный слайд и заодно всю презентацию, поэтому называется headmatter:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">theme</span><span class="p">:</span><span class="w"> </span><span class="l">bear</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">layout</span><span class="p">:</span><span class="w"> </span><span class="l">cover</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;SRE MCP&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="l">Михаил Савин</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">transition</span><span class="p">:</span><span class="w"> </span><span class="l">slide-left</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">drawings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">persist</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span></span></span></code></pre></div>
</div>
<p>Теперь слайд посложнее, с графиком и врезкой:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">&lt;Kicker&gt;01 · арифметика&lt;/Kicker&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Куда уходит время инцидента
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;div class=&#34;grid grid-cols-[1.6fr_1fr] gap-12 items-center mt-6&#34;&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;BarChart unit=&#34; мин&#34; :items=&#34;[{ label: &#39;Обнаружение&#39;, value: 923, highlight: true }, { label: &#39;Диагноз&#39;, value: 23 }, { label: &#39;Починка&#39;, value: 25 }]&#34; /&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;div&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;Callout type=&#34;important&#34; title=&#34;Главный вывод&#34;&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Дольше всего отказ живёт до первого сигнала: в 28 случаях из 38 его первым замечает клиент. Диагноз и починка занимают минуты, и только диагноз можно отдать ИИ без права на запись.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/Callout&gt;
</span></span><span class="line"><span class="cl"><span class="k">
</span></span></span><span class="line"><span class="cl"><span class="k">&gt; </span><span class="ge">*Медианы по 48 нашим постмортемам, февраль–сентябрь 2026.*
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/div&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/div&gt;</span></span></code></pre></div>
</div>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/slide-light_hu_ff7a384e7f163f7.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/slide-light_hu_ac41bba85660febc.png"
             alt="Слайд «Куда уходит время инцидента»: слева горизонтальный график из трёх столбцов, справа врезка с главным выводом"
             width="1200" height="676" title="Тот же слайд после сборки">
      </picture><figcaption>Тот же слайд после сборки</figcaption></figure><p>В этом куске три вещи, на которых держится вся вёрстка в Slidev.</p>
<p>Теги с заглавной буквы — Vue-компоненты. <code>Kicker</code>, <code>BarChart</code> и <code>Callout</code> приехали из темы, импортировать их не надо. Данные для графика лежат прямо в слайде, и когда поменяется цифра, картинку перерисовывать не придётся.</p>
<p>Классы у <code>&lt;div&gt;</code> — утилиты UnoCSS, по синтаксису как в Tailwind. <code>grid grid-cols-[1.6fr_1fr] gap-12</code> раскладывает слайд на две колонки без единой строчки CSS.</p>
<p>Пустые строки вокруг Markdown внутри HTML-тегов обязательны. Без них парсер примет содержимое за сырой HTML, и вместо жирного текста ты увидишь звёздочки.</p>
<h2 id="шаг-3-разложи-слайды-по-файлам">Шаг 3. Разложи слайды по файлам</h2>
<p>Стартер предлагает писать всё в один <code>slides.md</code>. На десяти слайдах это удобно. На сорока, да ещё с заметками спикера, у меня вышло бы больше полутора тысяч строк в одном файле, и любой <code>diff</code> превратился бы в кашу.</p>
<p>Slidev умеет подключать слайд из другого файла через <code>src:</code>. Тогда <code>slides.md</code> остаётся оглавлением:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># ── 01 · Арифметика MTTR ──</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">src</span><span class="p">:</span><span class="w"> </span><span class="l">./pages/01-mttr/00-section.md</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">src: ./pages/01-mttr/01-mttr.md
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">src: ./pages/01-mttr/02-thesis.md
</span></span><span class="line"><span class="cl">---</span></span></code></pre></div>
</div>
<p>А сами слайды лежат по одному в файле:</p>
<div class="codeblock codeblock--txt" data-lang="txt"><span class="codeblock__lang" aria-hidden="true">txt</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── slides.md              # headmatter, титул и порядок слайдов
</span></span><span class="line"><span class="cl">├── pages/
</span></span><span class="line"><span class="cl">│   ├── 00-intro/
</span></span><span class="line"><span class="cl">│   │   ├── 01-hook.md
</span></span><span class="line"><span class="cl">│   │   └── 02-speaker.md
</span></span><span class="line"><span class="cl">│   ├── 01-mttr/
</span></span><span class="line"><span class="cl">│   │   ├── 00-section.md  # разделитель блока
</span></span><span class="line"><span class="cl">│   │   ├── 01-mttr.md
</span></span><span class="line"><span class="cl">│   │   └── 02-thesis.md
</span></span><span class="line"><span class="cl">│   └── backup/            # бэкап-слайды «если спросят»
</span></span><span class="line"><span class="cl">├── components/            # свои Vue-компоненты
</span></span><span class="line"><span class="cl">├── public/                # картинки и QR-коды
</span></span><span class="line"><span class="cl">├── setup/routes.ts        # правка маршрутов, см. шаг 4
</span></span><span class="line"><span class="cl">└── style.css              # глобальные правки поверх темы</span></span></code></pre></div>
</div>
<p>Правила, которые у меня прижились:</p>
<ul>
<li>папка — блок доклада, <code>NN</code> в её имени совпадает с номером блока на слайдах;</li>
<li>файл называется <code>MM-слаг.md</code>, где <code>MM</code> — позиция слайда внутри блока. Вставил слайд в середину — перенумеруй соседей, иначе порядок файлов в папке разойдётся с порядком в презентации;</li>
<li>frontmatter слайда живёт в файле страницы, в <code>slides.md</code> остаются только <code>src:</code> и <code>hide:</code>;</li>
<li>YAML-комментарий над первым <code>src:</code> блока работает как заголовок раздела в оглавлении.</li>
</ul>
<p>После этого <code>slides.md</code> на всю презентацию занимает 240 строк, переставить слайд — значит перенести три строки, а правка текста трогает ровно один файл.</p>
<h2 id="шаг-4-дай-слайдам-имена">Шаг 4. Дай слайдам имена</h2>
<p>По умолчанию адрес слайда — его номер: <code>/7</code>, <code>/21</code>. Стоит вставить один слайд в начало, и все ссылки внутри презентации вместе с сохранёнными закладками начинают вести не туда.</p>
<p>Лечится это полем <code>routeAlias</code> во frontmatter слайда:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">routeAlias: gates
</span></span><span class="line"><span class="cl">---</span></span></code></pre></div>
</div>
<p>Теперь слайд открывается по адресу <code>/gates</code> и останется там, куда бы ты его ни переставил. Алиас у меня совпадает со слагом файла без номера. К повторяющимся слагам добавляю префикс блока: <code>mttr-section</code>, <code>permissions-thesis</code>.</p>
<p>На алиасы опираются и переходы по клику. Итоговый слайд с карточками у меня работает как меню: на вопросах из зала по карточке можно прыгнуть к нужной теме.</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">&lt;Card to=&#34;gate-1&#34; kicker=&#34;gate 01&#34; title=&#34;Read-only&#34;&gt;Токен не может писать&lt;/Card&gt;
</span></span><span class="line"><span class="cl">&lt;Card to=&#34;gate-4&#34; kicker=&#34;gate 04&#34; title=&#34;Аудит на сервере&#34;&gt;Журнал пишет не агент&lt;/Card&gt;</span></span></code></pre></div>
</div>
<p><code>Card</code> — компонент темы. Без неё то же самое делает встроенный <code>&lt;Link to=&quot;gates&quot;&gt;текст ссылки&lt;/Link&gt;</code>.</p>
<p>Остаётся одна шероховатость: корень сайта и <code>/presenter</code> Slidev жёстко ведёт на <code>/1</code>. Чтобы номер не всплывал даже при первом открытии, маршруты можно поправить в <code>setup/routes.ts</code>:</p>
<div class="codeblock codeblock--ts" data-lang="ts"><span class="codeblock__lang" aria-hidden="true">ts</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineRoutesSetup</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@slidev/types&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineRoutesSetup</span><span class="p">((</span><span class="nx">routes</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">route</span> <span class="k">of</span> <span class="nx">routes</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">route</span><span class="p">.</span><span class="nx">path</span> <span class="o">===</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nx">route</span><span class="p">.</span><span class="nx">redirect</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">&#39;/cover&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">route</span><span class="p">.</span><span class="nx">path</span> <span class="o">===</span> <span class="s1">&#39;/presenter&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nx">route</span><span class="p">.</span><span class="nx">redirect</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">&#39;/presenter/cover&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">routes</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div>
</div>
<p>Здесь <code>cover</code> — алиас титульного слайда. Он задаётся в headmatter той же строкой <code>routeAlias: cover</code>.</p>
<p>Проверка: открой <code>http://localhost:3030/</code>. В адресной строке должно оказаться <code>/cover</code>, а не <code>/1</code>.</p>
<h2 id="шаг-5-заметки-спикера-и-клики">Шаг 5. Заметки спикера и клики</h2>
<p>Последний HTML-комментарий на слайде — заметка спикера. В зале её не видно, а в режиме докладчика она стоит рядом со слайдом.</p>
<p>Список, который должен появляться по пунктам, оборачивается в <code>&lt;v-clicks&gt;</code>. Маркер <code>[click]</code> в заметке привязывает абзац к клику, и в режиме докладчика подсвечивается та фраза, до которой ты дошёл:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">&lt;v-clicks&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Запустил WAFaaS и собрал команду SRE
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Запустил SRE-комьюнити и подкаст про SRE
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;/v-clicks&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">&lt;!--
</span></span></span><span class="line"><span class="cl"><span class="cm">0:50–1:15 · 25 с. Без биографии: по фразе на клик.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">[click] Запускал WAFaaS и собирал команду SRE.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">[click] Запустил SRE-комьюнити и подкаст.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Переход: «Об этом и доклад».
</span></span></span><span class="line"><span class="cl"><span class="cm">--&gt;</span></span></span></code></pre></div>
</div>
<p>Заметку я пишу по одному шаблону. Первая строка — тайминг и главная мысль слайда. Дальше текст, который произношу. Потом переход к следующему слайду. В конце справка: цифры и детали, которые нужны на вопросах, но вслух не идут.</p>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/presenter_hu_4e4c189586e2aafe.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/presenter_hu_b60f9a973241904.png"
             alt="Режим докладчика в Slidev: слева текущий слайд, справа следующий слайд и заметка с таймингом"
             width="1200" height="675" title="Режим докладчика: текущий слайд, следующий и заметка">
      </picture><figcaption>Режим докладчика: текущий слайд, следующий и заметка</figcaption></figure><p>На <code>/overview</code> те же заметки лежат списком рядом с миниатюрами, а в углу стоит счётчик. У моего доклада там 37 слайдов, 67 кликов и 4 284 слова. По этим цифрам удобно прикидывать хронометраж ещё до первого прогона.</p>
<h2 id="шаг-6-спрячь-а-не-удаляй">Шаг 6. Спрячь, а не удаляй</h2>
<p>Первый план доклада был расписан на 25:25. На живых прогонах он занял 37 минут при слоте в 30 вместе с вопросами. Пришлось ужиматься до 18:20, и шесть слайдов из показа ушли.</p>
<p>Удалять я их не стал. В <code>slides.md</code> у каждого появилась одна строка:</p>
<div class="codeblock codeblock--md" data-lang="md"><span class="codeblock__lang" aria-hidden="true">md</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">src: ./pages/01-mttr/04-research.md
</span></span><span class="line"><span class="cl">hide: true
</span></span><span class="line"><span class="cl">---</span></span></code></pre></div>
</div>
<p>Слайд остаётся на своём месте в оглавлении, со своим номером файла и заметкой. В заметке к каждому скрытому слайду у меня записано, что поправить на соседях, если его вернуть. Чтобы вернуть, достаточно убрать <code>hide: true</code>.</p>
<h2 id="шаг-7-тема-компоненты-и-картинки">Шаг 7. Тема, компоненты и картинки</h2>
<p>Стартер приезжает с темой <code>seriph</code>. Другую можно выбрать в <a href="https://sli.dev/resources/theme-gallery" rel="noopener noreferrer" target="_blank">галерее</a>: пишешь её имя в <code>theme:</code>, и dev-сервер сам предложит поставить пакет.</p>
<p>Я пользуюсь своей, <a href="https://github.com/jtprogru/slidev-theme-bear" rel="noopener noreferrer" target="_blank">slidev-theme-bear</a>. В npm её нет, она ставится прямо из GitHub:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm i -D github:jtprogru/slidev-theme-bear</span></span></code></pre></div>
</div>
<p>В ней семнадцать своих раскладок, компоненты для врезок, карточек, графиков и схем, шрифты с кириллицей в зависимостях и проверка контраста по WCAG AA. Код темы открыт для некоммерческого использования, а вот медведь мой: в форке бренд придётся заменить на свой.</p>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/slide-cover_hu_2dc6f624a1165029.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/slide-cover_hu_467715e78dd754b3.png"
             alt="Титульный слайд доклада: логотип конференции слева вверху, название по центру, маскот-медведь в правом нижнем углу"
             width="1200" height="676" title="Титул: логотип и маскот вставлены компонентами">
      </picture><figcaption>Титул: логотип и маскот вставлены компонентами</figcaption></figure><p>Всё, чего нет в теме, лежит в самом проекте:</p>
<ul>
<li><code>components/</code> — свои Vue-компоненты, подключаются автоматически по имени файла. У меня их два: схема архитектуры и логотип конференции. В шаблоне организаторов логотип был только в PNG, поэтому он обведён в вектор и вставлен инлайном: так он перекрашивается вместе с темой и не ломается в PDF;</li>
<li><code>public/</code> — статика. Файл <code>public/avatar.jpg</code> доступен на слайде как <code>/avatar.jpg</code>;</li>
<li><code>style.css</code> — глобальные правки поверх темы. У меня там одна CSS-переменная, которая делает маскота монохромным;</li>
<li><code>&lt;style&gt;</code> в конце файла слайда — стили только для него. Выручает на плотных слайдах: основной текст уменьшается, а заголовки остаются в размере темы и не прыгают от слайда к слайду;</li>
<li>иконки — через Iconify. Ставишь набор командой <code>npm i -D @iconify-json/simple-icons</code> и пишешь <code>&lt;simple-icons-go /&gt;</code>. Иконки наследуют цвет текста и перекрашиваются вместе с темой.</li>
</ul>
<h2 id="шаг-8-собери-pdf">Шаг 8. Собери PDF</h2>
<p>Экспорт идёт через headless-браузер, поэтому сначала нужен Playwright:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm i -D playwright-chromium
</span></span><span class="line"><span class="cl">npx slidev export</span></span></code></pre></div>
</div>
<p>Проверка: рядом со <code>slides.md</code> появился <code>slides-export.pdf</code>, и страниц в нём столько же, сколько слайдов в презентации без скрытых.</p>
<p>Полезные флаги:</p>
<table>
	<thead>
			<tr>
					<th>Флаг</th>
					<th>Что делает</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--dark</code></td>
					<td>экспорт в тёмной теме</td>
			</tr>
			<tr>
					<td><code>--with-clicks</code></td>
					<td>отдельная страница на каждый клик</td>
			</tr>
			<tr>
					<td><code>--range 1,4-5</code></td>
					<td>только выбранные слайды; в PDF работает в паре с <code>--per-slide</code></td>
			</tr>
			<tr>
					<td><code>--per-slide</code></td>
					<td>рендерит слайды по одному и склеивает из них PDF</td>
			</tr>
			<tr>
					<td><code>--format png</code></td>
					<td>по картинке на слайд</td>
			</tr>
			<tr>
					<td><code>--format pptx</code></td>
					<td>pptx, в котором каждый слайд лежит картинкой</td>
			</tr>
			<tr>
					<td><code>--output &lt;файл&gt;</code></td>
					<td>своё имя файла вместо <code>slides-export</code></td>
			</tr>
	</tbody>
</table>
<p>У <code>--range</code> и <code>--with-clicks</code> есть по сюрпризу, оба разобраны в <a href="#%d1%82%d0%be%d0%bd%d0%ba%d0%b8%d0%b5-%d0%bc%d0%b5%d1%81%d1%82%d0%b0-%d0%bd%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d0%bb%d0%b5%d0%b3%d0%ba%d0%be-%d1%81%d0%bf%d0%be%d1%82%d0%ba%d0%bd%d1%83%d1%82%d1%8c%d1%81%d1%8f">тонких местах</a>.</p>
<figure class="post-figure">
      <picture>
        <source srcset="/posts/slidev/slide-dark_hu_752d2b16b86f512e.webp" type="image/webp">
        <img loading="lazy" decoding="async"
             src="/posts/slidev/slide-dark_hu_b33b2322c9871994.png"
             alt="Слайд с графиком в тёмной теме: те же данные на тёмном фоне"
             width="1200" height="676" title="Тот же слайд с флагом --dark">
      </picture><figcaption>Тот же слайд с флагом --dark</figcaption></figure><p>Добавь <code>slides-export*.pdf</code> в <code>.gitignore</code>: стартер создаёт его без этой строки.</p>
<p>Организаторы обычно просят файл, и PDF тут надёжнее всего. А pptx из Slidev годится разве что как запасной вариант: слайды в нём лежат картинками, текст не выделяется и не правится.</p>
<h2 id="шаг-9-выложи-слайды-на-github-pages">Шаг 9. Выложи слайды на GitHub Pages</h2>
<p><code>slidev build</code> собирает статический сайт в <code>dist/</code>. На GitHub Pages сайт живёт в подкаталоге с именем репозитория, и об этом ему надо сказать флагом <code>--base</code>:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx slidev build --base /nastachku-2026/</span></span></code></pre></div>
</div>
<p>Дальше workflow <code>.github/workflows/build.yaml</code>, который делает то же самое на каждый push в <code>main</code>:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">&#39;on&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">github-pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">${{ steps.deployment.outputs.page_url }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-node@v7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">node-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;lts/*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install dependencies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm install</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run build -- --base /nastachku-2026/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/configure-pages@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-pages-artifact@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dist</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/deploy-pages@v5</span></span></span></code></pre></div>
</div>
<p>В своём репозитории поменяй <code>/nastachku-2026/</code> на его имя и один раз включи Pages: Settings → Pages → Source → GitHub Actions.</p>
<p>Проверка: после зелёного прогона слайды открываются по адресу <code>https://&lt;логин&gt;.github.io/&lt;репозиторий&gt;/</code>.</p>
<p>Заодно я включил Dependabot: раз в неделю он приносит обновления npm-зависимостей и версий actions.</p>
<h2 id="makefile-чтобы-не-помнить-флаги">Makefile, чтобы не помнить флаги</h2>
<div class="codeblock codeblock--make" data-lang="make"><span class="codeblock__lang" aria-hidden="true">make</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nf">node_modules</span><span class="o">:</span> <span class="n">package</span>.<span class="n">json</span> <span class="n">package</span>-<span class="n">lock</span>.<span class="n">json</span>
</span></span><span class="line"><span class="cl">	npm ci
</span></span><span class="line"><span class="cl">	@touch node_modules
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">dev</span>
</span></span><span class="line"><span class="cl"><span class="nf">dev</span><span class="o">:</span> <span class="n">node_modules</span>
</span></span><span class="line"><span class="cl">	npm run dev
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span> <span class="n">node_modules</span>
</span></span><span class="line"><span class="cl">	npm run build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">THEME</span> <span class="o">?=</span> light
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">export</span>
</span></span><span class="line"><span class="cl"><span class="nf">export</span><span class="o">:</span> <span class="n">node_modules</span>
</span></span><span class="line"><span class="cl">	npm run <span class="nb">export</span> -- <span class="k">$(if</span> <span class="k">$(</span>filter dark,<span class="k">$(</span>THEME<span class="k">))</span>,--dark --output slides-export-dark.pdf<span class="k">)</span>
</span></span></code></pre></div>
</div>
<p>Весь трюк в первой цели. <code>node_modules</code> объявлен обычной файловой целью, которая зависит от манифестов. Зависимости ставятся, только если каталога нет или <code>package.json</code> с локом новее него. Поэтому <code>make dev</code> на свежем клоне работает сразу, а второй <code>make build</code> подряд ничего не переустанавливает.</p>
<p><code>make export THEME=dark</code> кладёт тёмный PDF в отдельный файл, светлый при этом не перезаписывается.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong><code>hide: true</code> выкидывает слайд целиком.</strong> Из показа, из сборки и из PDF, и ссылка на него перестаёт работать. Скрытых слайдов «на всякий случай», до которых можно дойти кнопкой со слайда «Вопросы», так не сделать. Мои бэкапы поэтому лежат в конце презентации под <code>hide: true</code>, а ответы на эти вопросы записаны в заметках.</li>
<li><strong>Заметки спикера едут в публичную сборку.</strong> На выложенном сайте работает <code>/presenter</code>, и твой текст вместе с ответами на острые вопросы может прочитать кто угодно. Если это в планы не входит, собирай с <code>--without-notes</code>.</li>
<li><strong>Прямая ссылка на слайд на GitHub Pages отдаёт код 404.</strong> Браузер при этом покажет нужный слайд: Slidev кладёт копию <code>index.html</code> в <code>404.html</code>, и роутер разбирается сам. А вот <code>curl</code>, проверка ссылок и превью в мессенджерах увидят честный 404. Если прямые ссылки важны, смотри в сторону <code>--router-mode hash</code>.</li>
<li><strong>Первый Markdown-заголовок слайда становится его названием.</strong> Поэтому надзаголовок перед <code>#</code> нельзя написать как <code>###</code>: названием слайда станет он. В теме для этого отдельный компонент <code>Kicker</code>.</li>
<li><strong>SVG через <code>&lt;img&gt;</code> не перекрашивается.</strong> <code>currentColor</code> внутри <code>&lt;img&gt;</code> превращается в чёрный, и на тёмной теме логотип пропадает. Всё, что должно менять цвет вместе с темой, вставляй инлайном через компонент.</li>
<li><strong>Шрифты из сети.</strong> Slidev умеет подтягивать шрифты из Google Fonts, и на площадке без интернета это неприятный сюрприз. В моей теме шрифты лежат в зависимостях, и слайды показываются офлайн.</li>
<li><strong>Номер слайда при <code>--with-clicks</code> повторяется.</strong> Сам Slidev номеров не рисует. Их ставит тема, и считает она слайды. Слайд с четырьмя кликами превращается в пять страниц подряд с одним и тем же <code>3 / 37</code>. Убрать номер из PDF можно одним правилом в <code>style.css</code>, потому что при экспорте Slidev вешает на <code>&lt;html&gt;</code> класс <code>print</code>: <code>html.print .bear-footer .tabular-nums { display: none; }</code>. Селектор здесь от моей темы, свой подсмотри в devtools. С <code>--per-slide</code> правило не сработает: там класса <code>print</code> нет. В своей теме я это починил: с <code>--with-clicks</code> номер из футера пропадает сам, а в обычном PDF остаётся.</li>
<li><strong><code>--range</code> в обычном PDF ничего не отрезает.</strong> В Slidev 53.0.0 команда <code>npx slidev export --range 3-4</code> отдала мне все 37 страниц вместо двух. Диапазон начинает работать с <code>--per-slide</code> или с <code>--format png</code>. Только не добавляй к <code>--per-slide</code> ещё и <code>--with-clicks</code>: клики не раскрываются, и каждый слайд попадает в PDF в состоянии до первого клика. Кусок доклада по кликам проще забрать картинками: <code>--format png --with-clicks --range 3-4</code> отдаёт по файлу на клик.</li>
<li><strong>Dependabot и тема из GitHub.</strong> Зависимость вида <code>github:jtprogru/slidev-theme-bear#v0.3.1</code> он предлагал «обновить» до v0.2.0. Тему пришлось добавить в <code>ignore</code> и бампать руками.</li>
<li><strong>Новому слайду алиас нужен сразу.</strong> Без <code>routeAlias</code> он откроется по номеру, и первая же перестановка сломает ссылку на него.</li>
</ul>
<h2 id="кому-slidev-не-подойдёт">Кому Slidev не подойдёт</h2>
<ul>
<li>Организаторы требуют pptx по своему шаблону и собираются править его сами. Картинки внутри pptx им не помогут.</li>
<li>Над слайдами работает несколько человек, и не все из них дружат с git.</li>
<li>Нужна презентация на пять слайдов к завтрашнему синку. Поднимать ради неё Node.js-проект дольше, чем нарисовать слайды руками.</li>
</ul>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<ol>
<li>Запусти <code>npm init slidev@latest</code>, выбери тему и убедись, что dev-сервер поднялся.</li>
<li>Сразу разложи слайды по <code>pages/NN-блок/MM-слаг.md</code> и раздай им <code>routeAlias</code>.</li>
<li>Пиши заметку спикера вместе со слайдом, а не накануне: тайминг, текст, переход, справка.</li>
<li>Прогони доклад вслух с таймером и спрячь лишнее через <code>hide: true</code>.</li>
<li>Сделай <code>npx slidev export</code> и пролистай PDF глазами: переносы, плотные слайды, тёмная тема.</li>
<li>Включи деплой на Pages и реши, едут ли заметки в публичную сборку.</li>
</ol>
<p>Как всё это выглядит в сборе, можно подсмотреть в <a href="https://github.com/jtprogru/nastachku-2026" rel="noopener noreferrer" target="_blank">nastachku-2026</a>: там же лежит README с правилами, по которым устроена презентация.</p>
<p>На этом всё! Profit!</p>
</div>]]></content:encoded>
    </item><item>
      <title>Digital Immune System: инженерия устойчивости как продукт</title>
      <link>https://jtprog.ru/posts/digital-immune-system-maturity/</link>
      <pubDate>Fri, 07 Aug 2026 12:00:00 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/digital-immune-system-maturity/</guid>
      <category>SRE</category>
      <category>DIS</category>
      <category>SLO</category>
      <category>Auto-Remediation</category>
      <category>Reliability</category>
      <category>DevOps</category>
      <category domain="category">Opinions</category>
      <enclosure url="https://jtprog.ru/cover.svg" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.svg" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! В <a href="/posts/digital-immune-system/">февральской заметке про DIS</a> я разбирал концепцию через шесть столпов Gartner — observability, AI-augmented testing, chaos engineering, auto remediation, SRE, supply chain security. Это была карта поверхности: что входит, зачем оно нужно, с чего начать. С тех пор накопилось мыслей, которые в эту карту уже не влезают.</p>
<p><strong>DIS держится на связях.</strong> Архитектура, SLO, релизная дисциплина, наблюдаемость и экономика надёжности замкнуты в одну петлю обратной связи — вот что отличает иммунную систему от склада инструментов. Шесть столпов отвечают на вопрос «из чего собрано». Интереснее другое: как эти шесть друг друга питают, ограничивают и обучают.</p>
<p>Статичная защита ловит известное. Иммунная система учится на отклонениях — этим и отличается. Инциденты не меняют политики, gate-ы и лимиты автодействий? Значит, иммунитета нет, сколько бы компонент ты ни развернул. Проверяется это одним вопросом: замыкается ли у тебя цикл «инцидент → разбор → изменение правил».</p>
<p>Дальше разбираю, что связность означает в инженерных решениях, где проходит граница безопасной автоматики, как проектируется сознательное право на отказ и почему про DIS в какой-то момент приходится говорить с финансистами.</p>
<h2 id="операциональное-определение">Операциональное определение</h2>
<p>Цифровая иммунная система — это совокупность архитектурных ограничений, инженерных практик и оргритуалов, которая:</p>
<ul>
<li>минимизирует blast radius и вероятность каскадных отказов;</li>
<li>переводит редкие ручные героизмы в воспроизводимую автоматику с понятными границами;</li>
<li>использует наблюдаемость и SLO как язык контрактов между командами;</li>
<li>учится на каждом отклонении (включая «почти инциденты») и обновляет политику изменений;</li>
<li>считает деньги наравне с аптаймом: стоимость девяточек, цену шума, цену эскалаций.</li>
</ul>
<p>Определение операциональное: каждый пункт можно проверить у себя за полчаса. Либо есть, либо нет.</p>
<h2 id="связность-вместо-перечисления">Связность вместо перечисления</h2>
<p>DIS обычно описывают через <a href="/posts/digital-immune-system/#%d1%88%d0%b5%d1%81%d1%82%d1%8c-%d1%81%d1%82%d0%be%d0%bb%d0%bf%d0%be%d0%b2-dis">шесть практик</a> — в таком виде концепцию упаковал Gartner в <a href="https://www.gartner.com/en/articles/gartner-top-10-strategic-technology-trends-for-2023" rel="noopener noreferrer" target="_blank">Top Strategic Technology Trends for 2023</a>, анонсированных в октябре 2022 года. Каждая закрывает часть проблем, но шесть квадратов в презентации ещё не делают систему иммунной. Иммунитет рождается из связей между ними.</p>
<p>У самого термина, кстати, судьба показательная. Из топ-трендов Gartner DIS выпал сразу после 2023 года — в списках 2024, 2025 и <a href="https://www.gartner.com/en/newsroom/press-releases/2025-10-20-gartner-identifies-the-top-strategic-technology-trends-for-2026" rel="noopener noreferrer" target="_blank">2026</a> его уже нет, а сама концепция переехала в <a href="https://www.gartner.com/en/newsroom/press-releases/2024-08-21-gartner-2024-hype-cycle-for-emerging-technologies-highlights-developer-productivity-total-experience-ai-and-security" rel="noopener noreferrer" target="_blank">Hype Cycle for Emerging Technologies 2024</a>, причём в security-компанию к AI TRiSM и cybersecurity mesh. Знаменитый прогноз «инвестировавшие в digital immunity к 2025 году сократят downtime на 80%» Gartner публично так и не проверил. Инженерные лидеры вывеску тоже не подхватили: AWS называет то же самое continuous resilience, Google и Meta<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> — reliability engineering, Microsoft — reliability maturity. Термином пользуются в основном аутсорсеры вроде <a href="https://www.infosys.com/iki/techcompass/digital-immune-system.html" rel="noopener noreferrer" target="_blank">Infosys TechCompass</a> и HCLTech. Хоронить концепцию из-за этого не стоит: все шесть практик живее всех живых и развиваются активнее, чем в 2022-м. Просто бренд не прижился, а содержание разошлось по индустрии под другими именами — и дальше я говорю «DIS» именно про содержание.</p>
<p>Связь первая — <strong>SLO как gate в CI/CD</strong>. Observability и release safety встречаются в одной точке: метрики ошибок и латентности превращаются в условие, при котором релиз продолжается или останавливается. Если <a href="/posts/slo-as-architecture-blueprint/">SLO живут отдельно от пайплайна</a>, ты получил красивый отчёт для руководства и ничего больше.</p>
<p>Связь вторая — <strong>постмортем как вход в runbook-as-code</strong>. Chaos engineering и auto remediation замыкаются через анализ инцидентов: каждое ручное действие, попавшее в разбор, либо превращается в безопасную автоматику, либо становится поводом изменить архитектуру, чтобы это действие не требовалось.</p>
<p>Связь третья — <strong>policy-as-code как единый центр правил</strong>. OPA, Sentinel или своя самописка — неважно; важно, что проверки supply chain, требования к release safety и архитектурные ограничения (blast radius, freeze windows, обязательные canary) лежат в одном месте. Один центр правил — один способ их обновить после инцидента.</p>
<p>Связь четвёртая — <strong>финансы как читатель SLO</strong>. SRE и FinOps говорят на одном языке: стоимость одной девяточки, стоимость минуты простоя для конкретного домена, стоимость флапающего алерта. Без этой связи надёжность остаётся «инженерной заботой» и проигрывает фичам в борьбе за время команды. Каждый раз.</p>
<p>Схематически петля выглядит так:</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/ef991189416f6978343e90695eb0eeae1e5ccd593ba834ee0625948b10975e89.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-ef991189416f-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 485.117px; background-color: transparent;" viewBox="0 0 485.1171875 786" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-ef991189416f-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-ef991189416f-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-ef991189416f-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-ef991189416f-light .error-icon{fill:#dce0e8;}#m-ef991189416f-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-ef991189416f-light .edge-thickness-normal{stroke-width:1px;}#m-ef991189416f-light .edge-thickness-thick{stroke-width:3.5px;}#m-ef991189416f-light .edge-pattern-solid{stroke-dasharray:0;}#m-ef991189416f-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-ef991189416f-light .edge-pattern-dashed{stroke-dasharray:3;}#m-ef991189416f-light .edge-pattern-dotted{stroke-dasharray:2;}#m-ef991189416f-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-ef991189416f-light .marker.cross{stroke:#5c5f77;}#m-ef991189416f-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-ef991189416f-light p{margin:0;}#m-ef991189416f-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-ef991189416f-light .cluster-label text{fill:#4c4f69;}#m-ef991189416f-light .cluster-label span{color:#4c4f69;}#m-ef991189416f-light .cluster-label span p{background-color:transparent;}#m-ef991189416f-light .label text,#m-ef991189416f-light span{fill:#4c4f69;color:#4c4f69;}#m-ef991189416f-light .node rect,#m-ef991189416f-light .node circle,#m-ef991189416f-light .node ellipse,#m-ef991189416f-light .node polygon,#m-ef991189416f-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-ef991189416f-light .rough-node .label text,#m-ef991189416f-light .node .label text,#m-ef991189416f-light .image-shape .label,#m-ef991189416f-light .icon-shape .label{text-anchor:middle;}#m-ef991189416f-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-ef991189416f-light .rough-node .label,#m-ef991189416f-light .node .label,#m-ef991189416f-light .image-shape .label,#m-ef991189416f-light .icon-shape .label{text-align:center;}#m-ef991189416f-light .node.clickable{cursor:pointer;}#m-ef991189416f-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-ef991189416f-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-ef991189416f-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-ef991189416f-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-ef991189416f-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-ef991189416f-light .edgeLabel p{background-color:#eff1f5;}#m-ef991189416f-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-ef991189416f-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-ef991189416f-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-ef991189416f-light .cluster text{fill:#4c4f69;}#m-ef991189416f-light .cluster span{color:#4c4f69;}#m-ef991189416f-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-ef991189416f-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-ef991189416f-light rect.text{fill:none;stroke-width:0;}#m-ef991189416f-light .icon-shape,#m-ef991189416f-light .image-shape{background-color:#eff1f5;text-align:center;}#m-ef991189416f-light .icon-shape p,#m-ef991189416f-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-ef991189416f-light .icon-shape .label rect,#m-ef991189416f-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-ef991189416f-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-ef991189416f-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-ef991189416f-light .node .neo-node{stroke:#bcc0cc;}#m-ef991189416f-light [data-look="neo"].node rect,#m-ef991189416f-light [data-look="neo"].cluster rect,#m-ef991189416f-light [data-look="neo"].node polygon{stroke:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].node path{stroke:url(#m-ef991189416f-light-gradient);stroke-width:1px;}#m-ef991189416f-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-ef991189416f-light [data-look="neo"].node circle{stroke:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-ef991189416f-light [data-look="neo"].icon-shape .icon{fill:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-ef991189416f-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M292.32,52.458L264.374,57.965C236.427,63.472,180.534,74.486,152.587,83.493C124.641,92.5,124.641,99.5,124.641,103L124.641,106.5" id="m-ef991189416f-light-L_SLO_Gate_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SLO_Gate_0" data-points="W3sieCI6MjkyLjMyMDMxMjUsInkiOjUyLjQ1NzY4MjQ4NzIzMzR9LHsieCI6MTI0LjY0MDYyNSwieSI6ODUuNX0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoxMTAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,163L124.641,167.167C124.641,171.333,124.641,179.667,124.641,187.333C124.641,195,124.641,202,124.641,205.5L124.641,209" id="m-ef991189416f-light-L_Gate_Release_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Gate_Release_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MTYzfSx7IngiOjEyNC42NDA2MjUsInkiOjE4OH0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoyMTN9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,265.5L124.641,269.667C124.641,273.833,124.641,282.167,124.641,289.833C124.641,297.5,124.641,304.5,124.641,308L124.641,311.5" id="m-ef991189416f-light-L_Release_Obs_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Release_Obs_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MjY1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MjkwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MzE1LjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,368L124.641,372.167C124.641,376.333,124.641,384.667,124.641,392.333C124.641,400,124.641,407,124.641,410.5L124.641,414" id="m-ef991189416f-light-L_Obs_Inc_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Obs_Inc_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MzY4fSx7IngiOjEyNC42NDA2MjUsInkiOjM5M30seyJ4IjoxMjQuNjQwNjI1LCJ5Ijo0MTh9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,470.5L124.641,474.667C124.641,478.833,124.641,487.167,124.641,494.833C124.641,502.5,124.641,509.5,124.641,513L124.641,516.5" id="m-ef991189416f-light-L_Inc_PM_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Inc_PM_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6NDcwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NDk1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NTIwLjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M119.519,573L118.706,577.167C117.893,581.333,116.267,589.667,115.454,597.333C114.641,605,114.641,612,114.641,615.5L114.641,619" id="m-ef991189416f-light-L_PM_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_Policy_0" data-points="W3sieCI6MTE5LjUxODY3Mzc4MDQ4NzgsInkiOjU3M30seyJ4IjoxMTQuNjQwNjI1LCJ5Ijo1OTh9LHsieCI6MTE0LjY0MDYyNSwieSI6NjIzfV0=" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M180.744,573L189.649,577.167C198.555,581.333,216.365,589.667,232.77,597.695C249.175,605.723,264.174,613.446,271.674,617.307L279.173,621.169" id="m-ef991189416f-light-L_PM_FinOps_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_FinOps_0" data-points="W3sieCI6MTgwLjc0Mzk5NzcxMzQxNDY0LCJ5Ijo1NzN9LHsieCI6MjM0LjE3NTc4MTI1LCJ5Ijo1OTh9LHsieCI6MjgyLjcyOTUxNjAwNjA5NzYsInkiOjYyM31d" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M114.641,675.5L114.641,679.667C114.641,683.833,114.641,692.167,127.867,700.308C141.094,708.45,167.548,716.399,180.775,720.374L194.002,724.349" id="m-ef991189416f-light-L_Policy_Runbook_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Runbook_0" data-points="W3sieCI6MTE0LjY0MDYyNSwieSI6Njc1LjV9LHsieCI6MTE0LjY0MDYyNSwieSI6NzAwLjV9LHsieCI6MTk3LjgzMjMxNzA3MzE3MDcsInkiOjcyNS41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M372.535,725.5L386.4,721.333C400.265,717.167,427.996,708.833,441.861,696.125C455.727,683.417,455.727,666.333,455.727,649.25C455.727,632.167,455.727,615.083,455.727,598C455.727,580.917,455.727,563.833,455.727,546.75C455.727,529.667,455.727,512.583,455.727,495.5C455.727,478.417,455.727,461.333,455.727,444.25C455.727,427.167,455.727,410.083,455.727,393C455.727,375.917,455.727,358.833,455.727,341.75C455.727,324.667,455.727,307.583,455.727,290.5C455.727,273.417,455.727,256.333,455.727,239.25C455.727,222.167,455.727,205.083,455.727,188C455.727,170.917,455.727,153.833,455.727,136.75C455.727,119.667,455.727,102.583,450.494,90.265C445.262,77.947,434.797,70.394,429.564,66.617L424.332,62.841" id="m-ef991189416f-light-L_Runbook_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Runbook_SLO_0" data-points="W3sieCI6MzcyLjUzNDg3MDQyNjgyOTMsInkiOjcyNS41fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo3MDAuNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NjQ5LjI1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo1OTh9LHsieCI6NDU1LjcyNjU2MjUsInkiOjU0Ni43NX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NDk1LjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjQ0NC4yNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MzkzfSx7IngiOjQ1NS43MjY1NjI1LCJ5IjozNDEuNzV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjI5MC41fSx7IngiOjQ1NS43MjY1NjI1LCJ5IjoyMzkuMjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjE4OH0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MTM2Ljc1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo4NS41fSx7IngiOjQyMS4wODg2MDUxODI5MjY4LCJ5Ijo2MC41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M359.837,623L363.984,618.833C368.131,614.667,376.425,606.333,380.572,593.625C384.719,580.917,384.719,563.833,384.719,546.75C384.719,529.667,384.719,512.583,384.719,495.5C384.719,478.417,384.719,461.333,384.719,444.25C384.719,427.167,384.719,410.083,384.719,393C384.719,375.917,384.719,358.833,384.719,341.75C384.719,324.667,384.719,307.583,384.719,290.5C384.719,273.417,384.719,256.333,384.719,239.25C384.719,222.167,384.719,205.083,384.719,188C384.719,170.917,384.719,153.833,384.719,136.75C384.719,119.667,384.719,102.583,384.719,90.542C384.719,78.5,384.719,71.5,384.719,68L384.719,64.5" id="m-ef991189416f-light-L_FinOps_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_FinOps_SLO_0" data-points="W3sieCI6MzU5LjgzNjg5MDI0MzkwMjQsInkiOjYyM30seyJ4IjozODQuNzE4NzUsInkiOjU5OH0seyJ4IjozODQuNzE4NzUsInkiOjU0Ni43NX0seyJ4IjozODQuNzE4NzUsInkiOjQ5NS41fSx7IngiOjM4NC43MTg3NSwieSI6NDQ0LjI1fSx7IngiOjM4NC43MTg3NSwieSI6MzkzfSx7IngiOjM4NC43MTg3NSwieSI6MzQxLjc1fSx7IngiOjM4NC43MTg3NSwieSI6MjkwLjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoyMzkuMjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoxODh9LHsieCI6Mzg0LjcxODc1LCJ5IjoxMzYuNzV9LHsieCI6Mzg0LjcxODc1LCJ5Ijo4NS41fSx7IngiOjM4NC43MTg3NSwieSI6NjAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_SLO_Gate_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Gate_Release_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Release_Obs_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Obs_Inc_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Inc_PM_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_FinOps_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Runbook_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Runbook_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_FinOps_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-ef991189416f-light-flowchart-SLO-0" data-look="classic" transform="translate(384.71875, 34.25)"><rect class="basic label-container" style="" x="-92.3984375" y="-26.25" width="184.796875" height="52.5"/><g class="label" style="" transform="translate(-62.3984375, -11.25)"><rect/><foreignObject width="124.796875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>SLO как контракт</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Gate-1" data-look="classic" transform="translate(124.640625, 136.75)"><rect class="basic label-container" style="" x="-74.09375" y="-26.25" width="148.1875" height="52.5"/><g class="label" style="" transform="translate(-44.09375, -11.25)"><rect/><foreignObject width="88.1875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Gate в CI/CD</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Release-3" data-look="classic" transform="translate(124.640625, 239.25)"><rect class="basic label-container" style="" x="-85.7578125" y="-26.25" width="171.515625" height="52.5"/><g class="label" style="" transform="translate(-55.7578125, -11.25)"><rect/><foreignObject width="111.515625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Релиз и трафик</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Obs-5" data-look="classic" transform="translate(124.640625, 341.75)"><rect class="basic label-container" style="" x="-75.0390625" y="-26.25" width="150.078125" height="52.5"/><g class="label" style="" transform="translate(-45.0390625, -11.25)"><rect/><foreignObject width="90.078125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Inc-7" data-look="classic" transform="translate(124.640625, 444.25)"><rect class="basic label-container" style="" x="-116.640625" y="-26.25" width="233.28125" height="52.5"/><g class="label" style="" transform="translate(-86.640625, -11.25)"><rect/><foreignObject width="173.28125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Инцидент или near-miss</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-PM-9" data-look="classic" transform="translate(124.640625, 546.75)"><rect class="basic label-container" style="" x="-74.9453125" y="-26.25" width="149.890625" height="52.5"/><g class="label" style="" transform="translate(-44.9453125, -11.25)"><rect/><foreignObject width="89.890625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Постмортем</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Policy-11" data-look="classic" transform="translate(114.640625, 649.25)"><rect class="basic label-container" style="" x="-82.0546875" y="-26.25" width="164.109375" height="52.5"/><g class="label" style="" transform="translate(-52.0546875, -11.25)"><rect/><foreignObject width="104.109375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Policy-as-code</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-FinOps-13" data-look="classic" transform="translate(333.7109375, 649.25)"><rect class="basic label-container" style="" x="-87.015625" y="-26.25" width="174.03125" height="52.5"/><g class="label" style="" transform="translate(-57.015625, -11.25)"><rect/><foreignObject width="114.03125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>FinOps-витрина</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Runbook-15" data-look="classic" transform="translate(285.18359375, 751.75)"><rect class="basic label-container" style="" x="-91.765625" y="-26.25" width="183.53125" height="52.5"/><g class="label" style="" transform="translate(-61.765625, -11.25)"><rect/><foreignObject width="123.53125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Runbook-as-code</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-ef991189416f-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-ef991189416f-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-ef991189416f-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-ef991189416f-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 485.117px; background-color: transparent;" viewBox="0 0 485.1171875 786" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-ef991189416f-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-ef991189416f-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-ef991189416f-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-ef991189416f-dark .error-icon{fill:#181926;}#m-ef991189416f-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-ef991189416f-dark .edge-thickness-normal{stroke-width:1px;}#m-ef991189416f-dark .edge-thickness-thick{stroke-width:3.5px;}#m-ef991189416f-dark .edge-pattern-solid{stroke-dasharray:0;}#m-ef991189416f-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-ef991189416f-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-ef991189416f-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-ef991189416f-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-ef991189416f-dark .marker.cross{stroke:#a5adcb;}#m-ef991189416f-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-ef991189416f-dark p{margin:0;}#m-ef991189416f-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-ef991189416f-dark .cluster-label text{fill:#cad3f5;}#m-ef991189416f-dark .cluster-label span{color:#cad3f5;}#m-ef991189416f-dark .cluster-label span p{background-color:transparent;}#m-ef991189416f-dark .label text,#m-ef991189416f-dark span{fill:#cad3f5;color:#cad3f5;}#m-ef991189416f-dark .node rect,#m-ef991189416f-dark .node circle,#m-ef991189416f-dark .node ellipse,#m-ef991189416f-dark .node polygon,#m-ef991189416f-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-ef991189416f-dark .rough-node .label text,#m-ef991189416f-dark .node .label text,#m-ef991189416f-dark .image-shape .label,#m-ef991189416f-dark .icon-shape .label{text-anchor:middle;}#m-ef991189416f-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-ef991189416f-dark .rough-node .label,#m-ef991189416f-dark .node .label,#m-ef991189416f-dark .image-shape .label,#m-ef991189416f-dark .icon-shape .label{text-align:center;}#m-ef991189416f-dark .node.clickable{cursor:pointer;}#m-ef991189416f-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-ef991189416f-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-ef991189416f-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-ef991189416f-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-ef991189416f-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-ef991189416f-dark .edgeLabel p{background-color:#24273a;}#m-ef991189416f-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-ef991189416f-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-ef991189416f-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-ef991189416f-dark .cluster text{fill:#cad3f5;}#m-ef991189416f-dark .cluster span{color:#cad3f5;}#m-ef991189416f-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-ef991189416f-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-ef991189416f-dark rect.text{fill:none;stroke-width:0;}#m-ef991189416f-dark .icon-shape,#m-ef991189416f-dark .image-shape{background-color:#24273a;text-align:center;}#m-ef991189416f-dark .icon-shape p,#m-ef991189416f-dark .image-shape p{background-color:#24273a;padding:2px;}#m-ef991189416f-dark .icon-shape .label rect,#m-ef991189416f-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-ef991189416f-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-ef991189416f-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-ef991189416f-dark .node .neo-node{stroke:#494d64;}#m-ef991189416f-dark [data-look="neo"].node rect,#m-ef991189416f-dark [data-look="neo"].cluster rect,#m-ef991189416f-dark [data-look="neo"].node polygon{stroke:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].node path{stroke:url(#m-ef991189416f-dark-gradient);stroke-width:1px;}#m-ef991189416f-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-ef991189416f-dark [data-look="neo"].node circle{stroke:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-ef991189416f-dark [data-look="neo"].icon-shape .icon{fill:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-ef991189416f-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M292.32,52.458L264.374,57.965C236.427,63.472,180.534,74.486,152.587,83.493C124.641,92.5,124.641,99.5,124.641,103L124.641,106.5" id="m-ef991189416f-dark-L_SLO_Gate_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SLO_Gate_0" data-points="W3sieCI6MjkyLjMyMDMxMjUsInkiOjUyLjQ1NzY4MjQ4NzIzMzR9LHsieCI6MTI0LjY0MDYyNSwieSI6ODUuNX0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoxMTAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,163L124.641,167.167C124.641,171.333,124.641,179.667,124.641,187.333C124.641,195,124.641,202,124.641,205.5L124.641,209" id="m-ef991189416f-dark-L_Gate_Release_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Gate_Release_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MTYzfSx7IngiOjEyNC42NDA2MjUsInkiOjE4OH0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoyMTN9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,265.5L124.641,269.667C124.641,273.833,124.641,282.167,124.641,289.833C124.641,297.5,124.641,304.5,124.641,308L124.641,311.5" id="m-ef991189416f-dark-L_Release_Obs_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Release_Obs_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MjY1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MjkwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MzE1LjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,368L124.641,372.167C124.641,376.333,124.641,384.667,124.641,392.333C124.641,400,124.641,407,124.641,410.5L124.641,414" id="m-ef991189416f-dark-L_Obs_Inc_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Obs_Inc_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MzY4fSx7IngiOjEyNC42NDA2MjUsInkiOjM5M30seyJ4IjoxMjQuNjQwNjI1LCJ5Ijo0MTh9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,470.5L124.641,474.667C124.641,478.833,124.641,487.167,124.641,494.833C124.641,502.5,124.641,509.5,124.641,513L124.641,516.5" id="m-ef991189416f-dark-L_Inc_PM_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Inc_PM_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6NDcwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NDk1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NTIwLjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M119.519,573L118.706,577.167C117.893,581.333,116.267,589.667,115.454,597.333C114.641,605,114.641,612,114.641,615.5L114.641,619" id="m-ef991189416f-dark-L_PM_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_Policy_0" data-points="W3sieCI6MTE5LjUxODY3Mzc4MDQ4NzgsInkiOjU3M30seyJ4IjoxMTQuNjQwNjI1LCJ5Ijo1OTh9LHsieCI6MTE0LjY0MDYyNSwieSI6NjIzfV0=" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M180.744,573L189.649,577.167C198.555,581.333,216.365,589.667,232.77,597.695C249.175,605.723,264.174,613.446,271.674,617.307L279.173,621.169" id="m-ef991189416f-dark-L_PM_FinOps_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_FinOps_0" data-points="W3sieCI6MTgwLjc0Mzk5NzcxMzQxNDY0LCJ5Ijo1NzN9LHsieCI6MjM0LjE3NTc4MTI1LCJ5Ijo1OTh9LHsieCI6MjgyLjcyOTUxNjAwNjA5NzYsInkiOjYyM31d" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M114.641,675.5L114.641,679.667C114.641,683.833,114.641,692.167,127.867,700.308C141.094,708.45,167.548,716.399,180.775,720.374L194.002,724.349" id="m-ef991189416f-dark-L_Policy_Runbook_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Runbook_0" data-points="W3sieCI6MTE0LjY0MDYyNSwieSI6Njc1LjV9LHsieCI6MTE0LjY0MDYyNSwieSI6NzAwLjV9LHsieCI6MTk3LjgzMjMxNzA3MzE3MDcsInkiOjcyNS41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M372.535,725.5L386.4,721.333C400.265,717.167,427.996,708.833,441.861,696.125C455.727,683.417,455.727,666.333,455.727,649.25C455.727,632.167,455.727,615.083,455.727,598C455.727,580.917,455.727,563.833,455.727,546.75C455.727,529.667,455.727,512.583,455.727,495.5C455.727,478.417,455.727,461.333,455.727,444.25C455.727,427.167,455.727,410.083,455.727,393C455.727,375.917,455.727,358.833,455.727,341.75C455.727,324.667,455.727,307.583,455.727,290.5C455.727,273.417,455.727,256.333,455.727,239.25C455.727,222.167,455.727,205.083,455.727,188C455.727,170.917,455.727,153.833,455.727,136.75C455.727,119.667,455.727,102.583,450.494,90.265C445.262,77.947,434.797,70.394,429.564,66.617L424.332,62.841" id="m-ef991189416f-dark-L_Runbook_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Runbook_SLO_0" data-points="W3sieCI6MzcyLjUzNDg3MDQyNjgyOTMsInkiOjcyNS41fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo3MDAuNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NjQ5LjI1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo1OTh9LHsieCI6NDU1LjcyNjU2MjUsInkiOjU0Ni43NX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NDk1LjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjQ0NC4yNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MzkzfSx7IngiOjQ1NS43MjY1NjI1LCJ5IjozNDEuNzV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjI5MC41fSx7IngiOjQ1NS43MjY1NjI1LCJ5IjoyMzkuMjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjE4OH0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MTM2Ljc1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo4NS41fSx7IngiOjQyMS4wODg2MDUxODI5MjY4LCJ5Ijo2MC41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M359.837,623L363.984,618.833C368.131,614.667,376.425,606.333,380.572,593.625C384.719,580.917,384.719,563.833,384.719,546.75C384.719,529.667,384.719,512.583,384.719,495.5C384.719,478.417,384.719,461.333,384.719,444.25C384.719,427.167,384.719,410.083,384.719,393C384.719,375.917,384.719,358.833,384.719,341.75C384.719,324.667,384.719,307.583,384.719,290.5C384.719,273.417,384.719,256.333,384.719,239.25C384.719,222.167,384.719,205.083,384.719,188C384.719,170.917,384.719,153.833,384.719,136.75C384.719,119.667,384.719,102.583,384.719,90.542C384.719,78.5,384.719,71.5,384.719,68L384.719,64.5" id="m-ef991189416f-dark-L_FinOps_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_FinOps_SLO_0" data-points="W3sieCI6MzU5LjgzNjg5MDI0MzkwMjQsInkiOjYyM30seyJ4IjozODQuNzE4NzUsInkiOjU5OH0seyJ4IjozODQuNzE4NzUsInkiOjU0Ni43NX0seyJ4IjozODQuNzE4NzUsInkiOjQ5NS41fSx7IngiOjM4NC43MTg3NSwieSI6NDQ0LjI1fSx7IngiOjM4NC43MTg3NSwieSI6MzkzfSx7IngiOjM4NC43MTg3NSwieSI6MzQxLjc1fSx7IngiOjM4NC43MTg3NSwieSI6MjkwLjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoyMzkuMjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoxODh9LHsieCI6Mzg0LjcxODc1LCJ5IjoxMzYuNzV9LHsieCI6Mzg0LjcxODc1LCJ5Ijo4NS41fSx7IngiOjM4NC43MTg3NSwieSI6NjAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_SLO_Gate_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Gate_Release_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Release_Obs_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Obs_Inc_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Inc_PM_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_FinOps_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Runbook_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Runbook_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_FinOps_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-ef991189416f-dark-flowchart-SLO-0" data-look="classic" transform="translate(384.71875, 34.25)"><rect class="basic label-container" style="" x="-92.3984375" y="-26.25" width="184.796875" height="52.5"/><g class="label" style="" transform="translate(-62.3984375, -11.25)"><rect/><foreignObject width="124.796875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>SLO как контракт</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Gate-1" data-look="classic" transform="translate(124.640625, 136.75)"><rect class="basic label-container" style="" x="-74.09375" y="-26.25" width="148.1875" height="52.5"/><g class="label" style="" transform="translate(-44.09375, -11.25)"><rect/><foreignObject width="88.1875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Gate в CI/CD</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Release-3" data-look="classic" transform="translate(124.640625, 239.25)"><rect class="basic label-container" style="" x="-85.7578125" y="-26.25" width="171.515625" height="52.5"/><g class="label" style="" transform="translate(-55.7578125, -11.25)"><rect/><foreignObject width="111.515625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Релиз и трафик</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Obs-5" data-look="classic" transform="translate(124.640625, 341.75)"><rect class="basic label-container" style="" x="-75.0390625" y="-26.25" width="150.078125" height="52.5"/><g class="label" style="" transform="translate(-45.0390625, -11.25)"><rect/><foreignObject width="90.078125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Inc-7" data-look="classic" transform="translate(124.640625, 444.25)"><rect class="basic label-container" style="" x="-116.640625" y="-26.25" width="233.28125" height="52.5"/><g class="label" style="" transform="translate(-86.640625, -11.25)"><rect/><foreignObject width="173.28125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Инцидент или near-miss</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-PM-9" data-look="classic" transform="translate(124.640625, 546.75)"><rect class="basic label-container" style="" x="-74.9453125" y="-26.25" width="149.890625" height="52.5"/><g class="label" style="" transform="translate(-44.9453125, -11.25)"><rect/><foreignObject width="89.890625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Постмортем</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Policy-11" data-look="classic" transform="translate(114.640625, 649.25)"><rect class="basic label-container" style="" x="-82.0546875" y="-26.25" width="164.109375" height="52.5"/><g class="label" style="" transform="translate(-52.0546875, -11.25)"><rect/><foreignObject width="104.109375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Policy-as-code</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-FinOps-13" data-look="classic" transform="translate(333.7109375, 649.25)"><rect class="basic label-container" style="" x="-87.015625" y="-26.25" width="174.03125" height="52.5"/><g class="label" style="" transform="translate(-57.015625, -11.25)"><rect/><foreignObject width="114.03125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>FinOps-витрина</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Runbook-15" data-look="classic" transform="translate(285.18359375, 751.75)"><rect class="basic label-container" style="" x="-91.765625" y="-26.25" width="183.53125" height="52.5"/><g class="label" style="" transform="translate(-61.765625, -11.25)"><rect/><foreignObject width="123.53125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Runbook-as-code</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-ef991189416f-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-ef991189416f-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-ef991189416f-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p>Каждая стрелка — правило, которое команда поддерживает сознательно. Выпадает одна — петля размыкается, DIS возвращается к набору практик, которые работают локально, но не складываются в систему обучения.</p>
<h2 id="право-на-отказ-и-организованная-деградация">Право на отказ и организованная деградация</h2>
<p>Когда я слышу разговоры о надёжности, рамка почти всегда одна — «всё должно работать». Зрелый подход добавляет к ней сознательно спроектированный отказ: что и в каком порядке выключается, когда поддерживать полный функционал уже невозможно.</p>
<p>Право на отказ — <strong>архитектурная сущность</strong>: контур деградации проектируешь так же, как нормальный путь запроса. И отвечаешь на четыре вопроса: какие функции выключаются первыми, какие последними, кто жмёт кнопку и какие сигналы триггерят автоматическую деградацию без человека.</p>
<p>Канонический публичный пример — <a href="https://github.blog/news-insights/company-news/oct21-post-incident-analysis/" rel="noopener noreferrer" target="_blank">инцидент GitHub 21 октября 2018</a>. После 43-секундного разрыва между восточными ДЦ orchestrator автоматически промоутнул реплики MySQL, и в данных образовалось расхождение. Команда сознательно остановила доставку 5 миллионов webhook-событий и 80 тысяч Pages-сборок на время восстановления целостности БД — формулировка из постмортема: «explicit choice to partially degrade site usability […] instead of jeopardizing data we had already received from users». Это и есть приоритет «целостность данных &gt; доступность фичи», превращённый в решение под давлением.</p>
<p>Другой угол: деградацию можно продавать. Cloudflare продаёт <a href="https://developers.cloudflare.com/cache/how-to/always-online/" rel="noopener noreferrer" target="_blank">Always Online</a> как явный контракт: если origin недоступен, edge отдаст статическую копию из кеша или версию из Internet Archive, динамика честно режется до статики. Клиент покупает «худший, но живой» ответ заранее. Netflix формализовал ту же идею в Hystrix (проект давно в maintenance mode, но паттерн пережил инструмент) и архитектуре рекомендательных пайплайнов: онлайн-расчёт не уложился в SLA — сервис отдаёт precomputed fallback, неперсонализированную подборку вместо пятисотки. Свежее продолжение той же линии — <a href="https://netflixtechblog.com/enhancing-netflix-reliability-with-service-level-prioritized-load-shedding-e735e6ce8f7d" rel="noopener noreferrer" target="_blank">service-level prioritized load shedding</a>: приоритизированный сброс нагрузки переехал с edge внутрь отдельных сервисов, где команда сама решает, какими запросами жертвовать первыми. Проверку боем устроил инфраструктурный сбой: на восстановлении Netflix поймал двенадцатикратный всплеск pre-fetch-запросов с Android-устройств, и шеддинг это съел. Показательно, куда сместился центр тяжести Netflix за десять лет: от «сломай прод и посмотри» к деградации, встроенной в каждый сервис как штатный механизм.</p>
<p>Отсюда три практических следствия.</p>
<p><strong>Домены отказа определяют, какой кусок продукта можно уронить целиком.</strong> Независимые ingress, балансировщики, DNS-резолверы и хранилища секретов на вертикаль. Падает один домен — продукт теряет одну способность и остаётся живым. Без изоляции пользователь получает белую страницу. Стоимость такой изоляции — выше; стоимость отказа от неё — каскад при первом серьёзном инциденте.</p>
<p><strong>Self-bootstrap, когда control-зависимости отвалились.</strong> Кэшированные сертификаты и токены, emergency-конфиги, offline-режим для критичных путей. Система поднимается сама. Или хотя бы дотягивает уже идущий трафик, пока её control plane лежит и поднять его некому. Для облака это значит: приватный кластер переживает падение IAM или API провайдера.</p>
<p><strong>Региональную репликацию решаешь per-domain.</strong> Для платёжной вертикали приоритет — согласованность; для каталога — доступность. Глобальное «давайте всё реплицировать активно-активно» приводит к тому, что часть доменов плохо ложится в выбранную модель и компенсирует это сложными ad-hoc обходами.</p>
<h2 id="граница-безопасной-автоматики">Граница безопасной автоматики</h2>
<p>Auto remediation на бумаге звучит как «система сама перезапускает сервисы и масштабирует ресурсы». На практике у автоматики всегда есть граница, и она важнее самой автоматики.</p>
<p><strong>Control plane и data plane автоматизируешь по-разному.</strong> Data plane может рестартиться, переезжать между узлами, масштабироваться самостоятельно — здесь автоматика работает в зоне с предсказуемыми последствиями. Control plane — нет, или с дополнительными гардрейлами: автоматические изменения IAM, политик доступа, маршрутизации трафика между регионами — это операции с длинным blast radius и долгим rollback.</p>
<p><a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/" rel="noopener noreferrer" target="_blank">Глобальный отказ Facebook 4 октября 2021</a><sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> — учебник по тому, что происходит, когда control plane завязан сам на себя. Рутинная команда оценки доступности backbone обвалила все межДЦ-линки; audit-инструмент, который должен был такую команду остановить, не сработал из-за бага. DNS-серверы автоматически сняли BGP-анонсы дата-центров — корректная политика «unhealthy = снимаем» превратилась в самоблокировку, когда «unhealthy» стало «всё одновременно». Инженеры на полдня лишились внутренних инструментов диагностики и физического доступа к части помещений. Урок: у автоматики control plane должен быть канал управления, не завязанный на то, что она контролирует.</p>
<p>Зеркальный кейс — <a href="https://habr.com/ru/companies/yandex_cloud_and_infra/articles/1004584/" rel="noopener noreferrer" target="_blank">платформа Yandex Infrastructure</a>, где тот же урок заложили заранее, до аварии. Компоненты основного облака выкатывает отдельный редко обновляемый админский контур: сервис не деплоит сам себя, и кольцо «чтобы поднять облако, нужно облако» разорвано архитектурно. Система прав живёт внутри облака, но у дежурного есть аварийный обход — кнопка выдаёт абсолютный доступ мимо IDM, чтобы разбор инцидента не ждал восстановления той самой системы, которая лежит. SSH-сервер написан с нулём онлайновых зависимостей, аутентификация переведена с ключей на сертификаты, и раскатка прав по хостам перестала занимать 15 минут и ушла с критического пути. За шесть лет — три серьёзных инцидента на ~150 000 хостов и ни одного полного краха. За красивой фразой «канал управления, не зависящий от контролируемого» стоит вот такой список конкретных решений, и каждое кто-то оплатил временем команды.</p>
<p><strong>Любое рискованное автодействие — через kill-switch, голосование и bounds.</strong> Bounds по времени — не больше трёх автодействий за пять минут, дальше стоп и эскалация на человека. По объёму — не больше 10% парка за один проход. По типу — никогда для production-control-plane. Конкретные цифры у каждой команды свои, важно другое: лимиты записаны в конфиге автоматики, а не в голове дежурного. Голосование — для решений без однозначно безопасного исхода. Автоматический rollback на предыдущую работающую версию допустим. Автоматическое поднятие шкалы трафика после отказа — нет.</p>
<p>Хрестоматийный кейс отсутствующих bounds — <a href="https://www.sec.gov/files/litigation/admin/2013/34-70694.pdf" rel="noopener noreferrer" target="_blank">Knight Capital, 1 августа 2012</a>. Новый код раскатили на 7 из 8 SMARS-серверов, а на восьмом остался мёртвый код «Power Peg», от которого Knight отказался ещё в 2003-м — и флаг, который новый код переиспользовал, на этом сервере активировал именно его. Хуже того, в 2005-м из Power Peg вынесли счётчик исполненных акций и после этого не перетестировали: останавливаться после заполнения родительского ордера код разучился. Никакой проверки «сколько ордеров в секунду — слишком много», никакого rate-cap на исходящий поток не было — за ~45 минут система отправила миллионы дочерних ордеров, набила ~4 миллиона исполнений и сожгла более $460M по оценке SEC, фактически уничтожив компанию. Автоматика без bounds масштабировала ошибку человека быстрее, чем кто-либо успел вмешаться.</p>
<p><strong>Auto-healing допустим только там, где есть наблюдаемость, reversible-действия и понятный rollback-план.</strong> Если автоматику нельзя откатить одним действием — она работает только с человеком в петле. Это отсеивает большинство «умных» сценариев самовосстановления, которые красиво звучат в обзорах, но в проде разрывают петлю обратной связи.</p>
<p>Та же платформа Яндекса — сильный аргумент в пользу такой строгости. На масштабе ~50 000 приложений там сделали ставку на скорость доступа человека: дежурный дотягивается до системы за секунды, вместо того чтобы ждать, пока она починит себя сама. Философия прямо обратная модному auto-healing, и результат по надёжности от этого не пострадал. Полезная поправка к ощущению, что зрелость измеряется долей автоматических действий: измеряется она тем, у кого остаётся возможность вмешаться, когда автоматика ошиблась.</p>
<p>Meta зашла с третьей стороны и автоматизировала диагноз. Платформа <a href="https://engineering.fb.com/2025/12/19/data-infrastructure/drp-metas-root-cause-analysis-platform-at-scale/" rel="noopener noreferrer" target="_blank">DrP</a> — это root cause analysis как сервис: 300+ команд, 2000 анализаторов, 50 тысяч автоматических расследований в день и снижение MTTR на 20–80%. Автоматика действий там тоже есть, но центр тяжести смещён в диагноз: платформа приносит дежурному ранжированную гипотезу и оставляет решение ему. Ещё раньше туда же <a href="https://engineering.fb.com/2024/06/24/data-infrastructure/leveraging-ai-for-efficient-incident-response/" rel="noopener noreferrer" target="_blank">подключили LLM</a> — ранжировать подозрительные изменения при расследовании инцидента. Логика та же, что у Яндекса с break-glass: автоматический диагноз — безопасная зона. Ошибётся анализатор — дежурный потеряет минуты на проверку гипотезы, а не прод.</p>
<p><strong>Runbook-as-code: ручное действие живёт один раз.</strong> Попало в постмортем — либо исчезает (через архитектурное изменение, после которого действие не нужно), либо становится безопасной автоматикой с лимитами. Накопление повторяющихся ручных шагов — главный симптом разорванной петли.</p>
<p><strong>Release safety как код.</strong> Канареечный релиз с автоматическим rollback по порогам ошибок и латентности, freeze windows на пиковые периоды (новогодние праздники, чёрная пятница, день платежей), обязательные проверки политик. Договорённость с командой можно «в этот раз пропустить». Код, который останавливает пайплайн, — нельзя.</p>
<p>Главное правило: автоматика делает только то, за что инженер готов отвечать ночью. Всё остальное — под kill-switch и по шагам.</p>
<h2 id="экономика-надёжности">Экономика надёжности</h2>
<p>Самый сильный признак зрелого DIS — споры переезжают с веры на экономику. До этого <a href="/posts/reliability-is-a-conversation/">разговор о надёжности</a> сводится к «нам нужна ещё одна девятка» или «у нас слишком много шума в алертах» — без цифр.</p>
<p><strong>Стоимость одной девяточки</strong> — конкретная сумма, привязанная к домену отказа. Девятка для платёжного домена и девятка для рекомендательной выдачи стоят разных денег и приносят разную ценность. Глобальный SLO «99.95% по всему сервису» — худший компромисс между этими крайностями.</p>
<p><strong>Цена шума</strong> — стоимость флапающего алерта. Измеряется в человеко-часах ночной работы, в том, какие решения принимаются на следующее утро, в проценте пропущенных P1 на фоне P3-шума. Шумовой бюджет — управляемая метрика: команда видит свой flap rate, владеет качеством сигналов и платит за избыточное оповещение тем же бюджетом, что и за реальные инциденты.</p>
<p><strong>Цена эскалаций.</strong> Путь от P3 до P0 имеет свою стоимость: каждое повышение уровня — это человек, которого подняли среди ночи. Срежешь этот путь через policy-as-code — автоматическая первичная диагностика, маршрутизация к нужному owner-у, обогащение алерта контекстом — и это сразу деньги.</p>
<p><strong>FinOps-витрина «стоимость простоя»</strong> висит рядом с MTTR-дашбордом. Один отвечает на «как быстро мы поднимаем», другой — на «во сколько нам обошлась минута». Без второй цифры приоритеты надёжности расставляют «по ощущениям».</p>
<p><strong>Управляемая девяточность.</strong> Целишься в оптимум, а максимум оставляешь тем, кому его оплатят. Лишняя девятка стоит дорого: переход с 99.9% на 99.99% кратно увеличивает вложения в инфраструктуру и команду, а прирост пользовательской ценности с этими вложениями несопоставим. Зрелая команда умеет сказать руководству «нам не нужна следующая девятка» и объяснить, почему.</p>
<p>Сколько у тебя стоит минута простоя платёжного пути? Если ответа нет, весь дальнейший разговор про девятки — вкусовщина, и спорить в нём будет тот, кто громче.</p>
<p>Когда про DIS говорят на языке денег, руководство принимает решения о надёжности так же, как о фичах. Пока этого языка нет, надёжность живёт только на инженерных совещаниях, а бюджет уходит туда, где ценность показана в деньгах.</p>
<h2 id="чужие-лестницы-зрелости">Чужие лестницы зрелости</h2>
<p>Прежде чем предлагать свою диагностику, честный вопрос: может, готовая линейка уже есть? Я пошёл проверять и обнаружил показательную вещь. <strong>Сводной модели зрелости DIS не существует.</strong> Gartner её не публиковал: ни <a href="https://www.gartner.com/en/articles/what-is-a-digital-immune-system-and-why-does-it-matter" rel="noopener noreferrer" target="_blank">статья-первоисточник</a>, ни <a href="https://www.gartner.com/en/information-technology/glossary/digital-immune-system" rel="noopener noreferrer" target="_blank">глоссарий</a> уровней не предлагают, а в названиях платных Hype Cycle модель зрелости DIS не значится. Зато в 2023–2025 стадийные модели появились у тех, кто термин не употребляет вовсе — и по ним хорошо видно, куда индустрия смотрит на самом деле.</p>
<p>Самая близкая к «лестнице зрелости DIS» вещь на рынке — <a href="https://learn.microsoft.com/en-us/azure/well-architected/reliability/maturity-model" rel="noopener noreferrer" target="_blank">Reliability Maturity Model</a> из Azure Well-Architected Framework, появившаяся в 2025 году. Пять уровней: Get resilient → Self-preservation → Recovery readiness → Maintain stability → Stay resilient. SRE как роль появляется на третьем уровне, выделенная SRE-функция и safe deployment practices — на четвёртом, а пятый честно объявлен бесконечным: устойчивость к рискам, которые ещё не случались. Ирония в том, что «гартнеровскую» модель зрелости в итоге написал Microsoft.</p>
<p>AWS пошёл другим путём, и для этого поста он показательнее. <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/resilience-lifecycle-framework/introduction.html" rel="noopener noreferrer" target="_blank">Resilience Lifecycle Framework</a> — пять стадий, сознательно замкнутых в круг: Set Objectives → Design and Implement → Evaluate and Test → Operate → Respond and Learn. Последняя стадия возвращает тебя к первой: выучил урок инцидента — пересмотрел цели. AWS независимо пришёл к тому же выводу, вокруг которого построен весь этот пост: зрелость — это замкнутая петля, а не этаж, на который поднялся и остался. Инструментальная обвязка тоже живая: <a href="https://aws.amazon.com/about-aws/whats-new/2024/12/aws-resilience-hub-fault-injection-service-recommendations/" rel="noopener noreferrer" target="_blank">Resilience Hub</a> с конца 2024 года сам рекомендует эксперименты Fault Injection Service под конкретную архитектуру — chaos engineering из ручной дисциплины превращается в подсказку сервиса.</p>
<p>У CNCF есть <a href="https://maturitymodel.cncf.io/" rel="noopener noreferrer" target="_blank">Cloud Native Maturity Model</a> от рабочей группы Cartografos: пять уровней по четырём измерениям — People, Process, Policy, Technology. Для нашей темы интересны верхние ступени: chaos engineering как штатная операция появляется на четвёртом уровне, автоматическое восстановление в known good state — на пятом. То есть автоматика самовосстановления и здесь оказалась вершиной лестницы. Входным билетом её не считает никто.</p>
<p>По отдельным столпам DIS лестницы существуют давно, но живут независимо друг от друга. Chaos engineering — классическая Chaos Maturity Model из книги Netflix/O&rsquo;Reilly с осями sophistication и adoption, плюс практические руководства по внедрению у <a href="https://www.gremlin.com/community/tutorials/chaos-engineering-adoption-guide" rel="noopener noreferrer" target="_blank">Gremlin</a> и <a href="https://www.harness.io/resources/the-chaos-engineering-maturity-model" rel="noopener noreferrer" target="_blank">eBook Harness</a>. Observability — <a href="https://grafana.com/blog/2024/01/29/how-to-improve-your-observability-strategy-introducing-the-observability-journey-maturity-model/" rel="noopener noreferrer" target="_blank">Grafana Observability Journey</a> (Reactive → Proactive → Systematic) и <a href="https://docs.newrelic.com/docs/new-relic-solutions/observability-maturity/introduction/" rel="noopener noreferrer" target="_blank">модель New Relic</a>. Reliability целиком — <a href="https://www.usenix.org/publications/loginonline/reliability-maturity-model" rel="noopener noreferrer" target="_blank">Reliability Maturity Model</a> в журнале <a href="https://www.usenix.org/publications/loginonline" rel="noopener noreferrer" target="_blank">;login:</a> от USENIX (absent → reactive → proactive → strategic → visionary) и опросник <a href="https://sre.google/static/pdf/cloud-cre-production-maturity-assessment.pdf" rel="noopener noreferrer" target="_blank">Google CRE Production Maturity Assessment</a>. Обе — гугловые, и мерят они разное: первая описывает зрелость организации и её отношение к надёжности, вторая опрашивает про готовность конкретного прода. Supply chain — <a href="https://slsa.dev/spec/latest/levels" rel="noopener noreferrer" target="_blank">SLSA</a> с уровнями L0–L3, единственная по-настоящему формализованная лестница из всех шести столпов. А для auto remediation индустриального стандарта нет вовсе — только вендорские блоги, каждый со своей нумерацией.</p>
<p>Лестницы, которая охватывала бы устойчивость целиком, ни Google, ни Meta не предлагают — только компонентные модели из абзаца выше. Вместо общей шкалы у обеих эволюция метода. Google в статье <a href="https://www.usenix.org/publications/loginonline/evolution-sre-google" rel="noopener noreferrer" target="_blank">«The Evolution of SRE at Google»</a> (журнал <a href="https://www.usenix.org/publications/loginonline" rel="noopener noreferrer" target="_blank">;login:</a> от USENIX, авторы Tim Falzone и Ben Treynor Sloss, основатель Google SRE) признал, что классическая модель error budgets перестала покрывать системы, где допустимый ущерб равен нулю, и переходит на <a href="https://sre.google/resources/practices-and-processes/stpa/" rel="noopener noreferrer" target="_blank">STAMP/STPA</a> — системный анализ опасностей вместо подсчёта бюджета ошибок. Meta вкладывается в автоматизацию диагноза, о которой я писал выше. Обе стратегии сводятся к «поменяй способ думать об отказах». Для лестниц как жанра это плохая новость.</p>
<p>Академия к теме только подбирается. Единственная известная мне работа, которая целенаправленно строит именно DIS Maturity Model — <a href="https://link.springer.com/chapter/10.1007/978-3-031-59465-6_6" rel="noopener noreferrer" target="_blank">«A Conceptual Model of Digital Immune System to Increase the Resilience of Technology Ecosystems»</a> Беате Краузе и Яниса Грабиса (RCIS 2024, Springer): концептуальная модель устойчивости технологической экосистемы в контексте европейской регуляторики NIS2 и DORA. Судя по <a href="https://link.springer.com/chapter/10.1007/978-3-031-92471-2_13" rel="noopener noreferrer" target="_blank">продолжению 2025 года</a>, до опубликованной шкалы уровней дело пока не дошло. А в ноябре 2025 вышла первая целая книга про DIS — <a href="https://onlinelibrary.wiley.com/doi/book/10.1002/9781394383788" rel="noopener noreferrer" target="_blank">«Digital Immune System: Principles and Practices»</a> (Wiley-Scrivener): уклон в security и AI, но отдельная глава про экономику внедрения там есть. Приятно: не мне одному кажется, что разговор про DIS — это в том числе разговор экономистов.</p>
<p>Итог ревизии: компонентные лестницы есть, цельной — нет. Ни одна из существующих моделей не проверяет главного — замыкается ли петля между компонентами. Поэтому дальше — пять вопросов на связность вместо шестой лестницы.</p>
<h2 id="маркеры-зрелого-dis">Маркеры зрелого DIS</h2>
<p>Как отличить «у нас DIS» от «у нас отдельные практики»? Пять наблюдаемых маркеров. Это диагностика, не предписание: набрал три из пяти — основа для иммунной системы у тебя есть.</p>
<ol>
<li><strong>SLO двигают релизы.</strong> SLO, который не закрывает gate в CI/CD, остаётся отчётом для руководства.</li>
<li><strong>Класс проблем не повторяется.</strong> Каждый крупный инцидент уменьшает вероятность повторения класса проблем. В правилах и политиках видно, что система «училась»: после инцидента появился новый gate, изменился лимит автоматики, добавилась проверка в policy-as-code.</li>
<li><strong>Автоматика делает только обоснованное.</strong> Каждое автодействие имеет владельца, лимиты и kill-switch. Всё остальное живёт под человеком в петле.</li>
<li><strong>Команды спорят на языке экономики.</strong> Приоритеты надёжности обсуждают в рублях: стоимость девяточек, шума, эскалаций. Фразу «у нас плохо с алертами» на таком совещании перебивают вопросом «сколько это стоит».</li>
<li><strong>Руководство видит стоимость надёжности</strong> и принимает решения на этом языке. На совещании уровня CTO рядом с дашбордом MTTR висит дашборд FinOps.</li>
</ol>
<p>Ноль маркеров — DIS нет, какие бы инструменты ни были развёрнуты. Все пять — фокус смещается с построения иммунной системы на её эксплуатацию.</p>
<h2 id="что-ломает-dis-на-масштабе">Что ломает DIS на масштабе</h2>
<p>Разваливается всё тихо: кто-то пропускает один шаг в петле, и дальше система живёт по инерции. Архитектура при этом обычно нормальная. Ниже — типичные failure modes.</p>
<p><strong>SLO без gate.</strong> Есть метрика, есть SLO, есть <a href="/posts/burn-rate-is-not-speed/">error budget</a>, но релиз не блокируется, когда бюджет исчерпан. Цикл разомкнут: измеряем, но не действуем. Лечение: сделать SLO условием пайплайна.</p>
<p><strong>Автоматика, которой никто не поставил границ.</strong> Auto-healing пытается «починить» инфраструктурный инцидент — рестартит поды, перекидывает трафик, масштабирует кластер — и тем самым расширяет blast radius. Каждому автодействию нужны bounds: сколько раз за период, какой процент парка, при каких условиях остановиться и позвать человека.</p>
<p><strong>Постмортем без обучения.</strong> Разбор сделан, документ есть, action items записаны — runbook не обновлён, политики не изменены, lessons learned остались на странице в Confluence, которую с тех пор никто не открывал. Петля «инцидент → разбор → изменение правил» обрывается на втором шаге.</p>
<p><strong>За шумом никто не следит.</strong> Flap rate растёт, доверие к алертам падает, инженеры выключают нотификации канала, P1 пропускается на фоне P3-шума. Шум — управляемая метрика. Брошенный без присмотра, он деградирует наблюдаемость тише и сильнее, чем кажется.</p>
<p><strong>Про деньги молчат.</strong> Надёжность обсуждают только инженеры, бизнес решает вслепую. Любое сокращение бюджета в таком раскладе первым делом бьёт по надёжности: её ценность просто не переведена в те же единицы, что и всё остальное.</p>
<p>Любое из этих узких мест разрывает петлю обратной связи и возвращает DIS к набору практик.</p>
<h2 id="итого">Итого</h2>
<p>DIS — это про связность и дисциплину. Архитектура режет blast radius на домены отказа, SLO превращает обещания в измеримый контракт. Нагрузка и хаос по регламенту проверяют гипотезы надёжности раньше, чем это за тебя сделает продакшен. Автоматика работает под kill-switch и bounds. У онколла есть лимит на шум алертов и право остановить поставку. А экономика в конце считает, во сколько всё это обошлось. По отдельности ни один элемент иммунитета не даёт: он появляется там, где они замкнулись в петлю обучения.</p>
<p>Про российский контекст и <strong>кибериммунитет</strong> я уже писал в <a href="/posts/digital-immune-system/#%d1%81%d0%b2%d1%8f%d0%b7%d1%8c-%d1%81-%d0%b4%d1%80%d1%83%d0%b3%d0%b8%d0%bc%d0%b8-%d0%ba%d0%be%d0%bd%d1%86%d0%b5%d0%bf%d1%86%d0%b8%d1%8f%d0%bc%d0%b8">февральской заметке</a>. Здесь добавлю только одно: DIS дополняет этот подход петлёй обучения через инциденты и явной экономикой надёжности.</p>
<p>Порядок, в котором эти элементы вообще имеет смысл строить, — отдельный разговор: пока нет мониторинга, обсуждать автоматическое восстановление рано. Я развернул его в <a href="https://jtprogru.github.io/The-Way-of-SRE/reliability-hierarchy/" rel="noopener noreferrer" target="_blank">иерархию надёжности</a>, а проверку контролов безопасности тем же методом хаос-экспериментов — в <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/security-chaos-engineering/" rel="noopener noreferrer" target="_blank">Security Chaos Engineering</a>.</p>
<p>Хочешь понять, где стоишь — пройдись по пяти маркерам и посчитай, сколько выполняется на самом деле. Картина получится честнее, чем от аудита инструментов или примерки чужих лестниц из предыдущей секции: инструменты и уровни идут следом за связностью, а не наоборот.</p>
<p>PS: эта статья — то, что я не разобрал в <a href="/posts/digital-immune-system/">февральской заметке про DIS</a>. Если первая отвечала на «что это такое и из чего состоит», эта — на «что отличает зрелый DIS от витрины из шести квадратов».</p>
<h2 id="публичные-кейсы">Публичные кейсы</h2>
<p>Подборка постмортемов, инженерных описаний и моделей зрелости, на которых строится бо́льшая часть тезисов выше. Полезна как карта дальнейшего чтения.</p>
<p><strong>Право на отказ и организованная деградация:</strong></p>
<ul>
<li><a href="https://github.blog/news-insights/company-news/oct21-post-incident-analysis/" rel="noopener noreferrer" target="_blank">GitHub, 21.10.2018</a> — сознательная пауза webhooks и Pages ради целостности БД.</li>
<li><a href="https://blog.cloudflare.com/always-online-v2/" rel="noopener noreferrer" target="_blank">Cloudflare Always Online</a> и <a href="https://blog.cloudflare.com/cloudflares-always-online-and-the-internet-archive-team-up-to-fight-origin-errors/" rel="noopener noreferrer" target="_blank">интеграция с Internet Archive</a> — деградация как продуктовый контракт.</li>
<li><a href="https://github.com/Netflix/Hystrix/wiki/How-it-Works" rel="noopener noreferrer" target="_blank">Netflix Hystrix</a> и <a href="https://netflixtechblog.com/system-architectures-for-personalization-and-recommendation-e081aa94b5d8" rel="noopener noreferrer" target="_blank">архитектура персонализации</a> — fallback на precomputed-результат, встроенный в пайплайн.</li>
<li><a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/" rel="noopener noreferrer" target="_blank">Cloudflare, 2.07.2019</a> — global WAF terminate как заранее построенный kill-switch уровня домена.</li>
<li><a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer" target="_blank">Cloudflare, 18.11.2025</a> — bypass на prior-version proxy для Workers KV и Access как реактивная деградация.</li>
</ul>
<p><strong>Граница безопасной автоматики:</strong></p>
<ul>
<li><a href="https://www.sec.gov/files/litigation/admin/2013/34-70694.pdf" rel="noopener noreferrer" target="_blank">Knight Capital, 1.08.2012</a> — переиспользованный флаг + неполный деплой + автоматика без rate-cap, $460M+ за ~45 минут.</li>
<li><a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/" rel="noopener noreferrer" target="_blank">Facebook BGP, 4.10.2021</a> — баг audit-инструмента + автоснятие DNS-анонсов = control plane выпиливает сам себя.</li>
<li><a href="https://aws.amazon.com/message/41926/" rel="noopener noreferrer" target="_blank">AWS S3, 28.02.2017</a> — playbook-инструмент без safety-check на минимальную ёмкость масштабировал опечатку оператора.</li>
<li><a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/" rel="noopener noreferrer" target="_blank">Cloudflare WAF, 2.07.2019</a> — regex-правило раскатилось глобально без canary, потому что SOP для конфигов отличался от SOP для бинарей.</li>
<li><a href="https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/" rel="noopener noreferrer" target="_blank">GitLab, 31.01.2017</a> — оператор стёр primary; параллельно <a href="https://about.gitlab.com/blog/2017/02/01/gitlab-dot-com-database-incident/" rel="noopener noreferrer" target="_blank">все пять механизмов бэкапа</a> тихо не работали (silent failure).</li>
<li><a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer" target="_blank">Cloudflare, 18.11.2025</a> — изменение прав ClickHouse → дубль строк → превышен hard limit → <code>Result::unwrap()</code> panic в Rust-проксе → автогенерация плохого файла каждые 5 минут.</li>
<li><a href="https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage" rel="noopener noreferrer" target="_blank">Discord, 25.03.2026</a> — при вертикальном K8s-rescale termination grace period истёк раньше, чем начался handoff сессий; волна автоматических reconnect&rsquo;ов добавила thundering herd.</li>
<li><a href="https://habr.com/ru/companies/yandex_cloud_and_infra/articles/1004584/" rel="noopener noreferrer" target="_blank">Yandex Infrastructure, 10.03.2026</a> — не постмортем, а инженерное описание: админский контур, break-glass мимо IDM и SSH с нулём онлайновых зависимостей как заранее построенный ответ на урок Facebook.</li>
</ul>
<p><strong>Обратная сторона — где автоматика спасла:</strong></p>
<ul>
<li><a href="https://status.cloud.google.com/incident/cloud-networking/19009" rel="noopener noreferrer" target="_blank">Google Cloud Networking, 2.06.2019</a> — первый пункт follow-up в постмортеме — «immediately halted the datacenter automation software».</li>
<li><a href="https://github.com/Netflix/chaosmonkey" rel="noopener noreferrer" target="_blank">Netflix Chaos Monkey</a> и <a href="https://netflixtechblog.com/the-netflix-simian-army-16e57fbab116" rel="noopener noreferrer" target="_blank">Simian Army</a> — автоматика как тренажёр: регулярная поломка прода посреди рабочего дня приучила архитектуру к фоллбекам.</li>
</ul>
<p><strong>Модели зрелости и свежие публикации (2024–2026):</strong></p>
<ul>
<li><a href="https://learn.microsoft.com/en-us/azure/well-architected/reliability/maturity-model" rel="noopener noreferrer" target="_blank">Azure Well-Architected Reliability Maturity Model</a> — пять уровней от Get resilient до Stay resilient; ближайший на рынке аналог «лестницы зрелости DIS».</li>
<li><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/resilience-lifecycle-framework/introduction.html" rel="noopener noreferrer" target="_blank">AWS Resilience Lifecycle Framework</a> — пять стадий циклом, а не лестницей; финальная стадия Respond and Learn возвращает к пересмотру целей.</li>
<li><a href="https://maturitymodel.cncf.io/" rel="noopener noreferrer" target="_blank">CNCF Cloud Native Maturity Model</a> — chaos engineering на четвёртом уровне, автоматическое восстановление на пятом.</li>
<li><a href="https://www.usenix.org/publications/loginonline/evolution-sre-google" rel="noopener noreferrer" target="_blank">The Evolution of SRE at Google</a> (журнал <a href="https://www.usenix.org/publications/loginonline" rel="noopener noreferrer" target="_blank">;login:</a> от USENIX) и <a href="https://sre.google/resources/practices-and-processes/stpa/" rel="noopener noreferrer" target="_blank">STPA на sre.google</a> — отказ от error budgets как универсального инструмента в пользу системного анализа опасностей.</li>
<li><a href="https://engineering.fb.com/2025/12/19/data-infrastructure/drp-metas-root-cause-analysis-platform-at-scale/" rel="noopener noreferrer" target="_blank">DrP: Meta&rsquo;s Root Cause Analysis Platform at Scale</a> и <a href="https://engineering.fb.com/2024/06/24/data-infrastructure/leveraging-ai-for-efficient-incident-response/" rel="noopener noreferrer" target="_blank">Leveraging AI for efficient incident response</a> — автоматизация диагноза вместо автоматизации действия.</li>
<li><a href="https://netflixtechblog.com/enhancing-netflix-reliability-with-service-level-prioritized-load-shedding-e735e6ce8f7d" rel="noopener noreferrer" target="_blank">Netflix: service-level prioritized load shedding</a> — деградация как штатный механизм каждого сервиса.</li>
<li><a href="https://dora.dev/" rel="noopener noreferrer" target="_blank">DORA Research</a> — отчёты 2024–2025: метрики throughput/instability и reliability как операционная характеристика.</li>
<li><a href="https://link.springer.com/chapter/10.1007/978-3-031-59465-6_6" rel="noopener noreferrer" target="_blank">Krauze, Grabis — A Conceptual Model of Digital Immune System…</a> (RCIS 2024, Springer, полный текст платный) и <a href="https://link.springer.com/chapter/10.1007/978-3-031-92471-2_13" rel="noopener noreferrer" target="_blank">продолжение 2025 года</a> — единственная академическая попытка построить именно DIS Maturity Model.</li>
<li><a href="https://onlinelibrary.wiley.com/doi/book/10.1002/9781394383788" rel="noopener noreferrer" target="_blank">Digital Immune System: Principles and Practices</a> (Wiley-Scrivener, ноябрь 2025, платная) — первая книга целиком про DIS, включая главу об экономике внедрения.</li>
<li>Gartner: <a href="https://www.gartner.com/en/articles/what-is-a-digital-immune-system-and-why-does-it-matter" rel="noopener noreferrer" target="_blank">What Is a Digital Immune System and Why Does It Matter</a>, <a href="https://www.gartner.com/en/information-technology/glossary/digital-immune-system" rel="noopener noreferrer" target="_blank">глоссарий</a>, <a href="https://www.gartner.com/en/newsroom/press-releases/2024-08-21-gartner-2024-hype-cycle-for-emerging-technologies-highlights-developer-productivity-total-experience-ai-and-security" rel="noopener noreferrer" target="_blank">Hype Cycle for Emerging Technologies 2024</a> — первоисточники; часть страниц Gartner прячет за регистрацию.</li>
<li><a href="https://www.infosys.com/iki/techcompass/digital-immune-system.html" rel="noopener noreferrer" target="_blank">Infosys TechCompass: Digital Immune System</a> — редкий пример живого использования термина у сервисных компаний.</li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Meta Platforms Inc. (владелец Facebook и Instagram) признана экстремистской организацией, её деятельность запрещена на территории РФ. Упоминается здесь исключительно в инженерном контексте.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! В <a href="/posts/digital-immune-system/">февральской заметке про DIS</a> я разбирал концепцию через шесть столпов Gartner — observability, AI-augmented testing, chaos engineering, auto remediation, SRE, supply chain security. Это была карта поверхности: что входит, зачем оно нужно, с чего начать. С тех пор накопилось мыслей, которые в эту карту уже не влезают.</p>
<p><strong>DIS держится на связях.</strong> Архитектура, SLO, релизная дисциплина, наблюдаемость и экономика надёжности замкнуты в одну петлю обратной связи — вот что отличает иммунную систему от склада инструментов. Шесть столпов отвечают на вопрос «из чего собрано». Интереснее другое: как эти шесть друг друга питают, ограничивают и обучают.</p>
<p>Статичная защита ловит известное. Иммунная система учится на отклонениях — этим и отличается. Инциденты не меняют политики, gate-ы и лимиты автодействий? Значит, иммунитета нет, сколько бы компонент ты ни развернул. Проверяется это одним вопросом: замыкается ли у тебя цикл «инцидент → разбор → изменение правил».</p>
<p>Дальше разбираю, что связность означает в инженерных решениях, где проходит граница безопасной автоматики, как проектируется сознательное право на отказ и почему про DIS в какой-то момент приходится говорить с финансистами.</p>
<h2 id="операциональное-определение">Операциональное определение</h2>
<p>Цифровая иммунная система — это совокупность архитектурных ограничений, инженерных практик и оргритуалов, которая:</p>
<ul>
<li>минимизирует blast radius и вероятность каскадных отказов;</li>
<li>переводит редкие ручные героизмы в воспроизводимую автоматику с понятными границами;</li>
<li>использует наблюдаемость и SLO как язык контрактов между командами;</li>
<li>учится на каждом отклонении (включая «почти инциденты») и обновляет политику изменений;</li>
<li>считает деньги наравне с аптаймом: стоимость девяточек, цену шума, цену эскалаций.</li>
</ul>
<p>Определение операциональное: каждый пункт можно проверить у себя за полчаса. Либо есть, либо нет.</p>
<h2 id="связность-вместо-перечисления">Связность вместо перечисления</h2>
<p>DIS обычно описывают через <a href="/posts/digital-immune-system/#%d1%88%d0%b5%d1%81%d1%82%d1%8c-%d1%81%d1%82%d0%be%d0%bb%d0%bf%d0%be%d0%b2-dis">шесть практик</a> — в таком виде концепцию упаковал Gartner в <a href="https://www.gartner.com/en/articles/gartner-top-10-strategic-technology-trends-for-2023" rel="noopener noreferrer" target="_blank">Top Strategic Technology Trends for 2023</a>, анонсированных в октябре 2022 года. Каждая закрывает часть проблем, но шесть квадратов в презентации ещё не делают систему иммунной. Иммунитет рождается из связей между ними.</p>
<p>У самого термина, кстати, судьба показательная. Из топ-трендов Gartner DIS выпал сразу после 2023 года — в списках 2024, 2025 и <a href="https://www.gartner.com/en/newsroom/press-releases/2025-10-20-gartner-identifies-the-top-strategic-technology-trends-for-2026" rel="noopener noreferrer" target="_blank">2026</a> его уже нет, а сама концепция переехала в <a href="https://www.gartner.com/en/newsroom/press-releases/2024-08-21-gartner-2024-hype-cycle-for-emerging-technologies-highlights-developer-productivity-total-experience-ai-and-security" rel="noopener noreferrer" target="_blank">Hype Cycle for Emerging Technologies 2024</a>, причём в security-компанию к AI TRiSM и cybersecurity mesh. Знаменитый прогноз «инвестировавшие в digital immunity к 2025 году сократят downtime на 80%» Gartner публично так и не проверил. Инженерные лидеры вывеску тоже не подхватили: AWS называет то же самое continuous resilience, Google и Meta<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> — reliability engineering, Microsoft — reliability maturity. Термином пользуются в основном аутсорсеры вроде <a href="https://www.infosys.com/iki/techcompass/digital-immune-system.html" rel="noopener noreferrer" target="_blank">Infosys TechCompass</a> и HCLTech. Хоронить концепцию из-за этого не стоит: все шесть практик живее всех живых и развиваются активнее, чем в 2022-м. Просто бренд не прижился, а содержание разошлось по индустрии под другими именами — и дальше я говорю «DIS» именно про содержание.</p>
<p>Связь первая — <strong>SLO как gate в CI/CD</strong>. Observability и release safety встречаются в одной точке: метрики ошибок и латентности превращаются в условие, при котором релиз продолжается или останавливается. Если <a href="/posts/slo-as-architecture-blueprint/">SLO живут отдельно от пайплайна</a>, ты получил красивый отчёт для руководства и ничего больше.</p>
<p>Связь вторая — <strong>постмортем как вход в runbook-as-code</strong>. Chaos engineering и auto remediation замыкаются через анализ инцидентов: каждое ручное действие, попавшее в разбор, либо превращается в безопасную автоматику, либо становится поводом изменить архитектуру, чтобы это действие не требовалось.</p>
<p>Связь третья — <strong>policy-as-code как единый центр правил</strong>. OPA, Sentinel или своя самописка — неважно; важно, что проверки supply chain, требования к release safety и архитектурные ограничения (blast radius, freeze windows, обязательные canary) лежат в одном месте. Один центр правил — один способ их обновить после инцидента.</p>
<p>Связь четвёртая — <strong>финансы как читатель SLO</strong>. SRE и FinOps говорят на одном языке: стоимость одной девяточки, стоимость минуты простоя для конкретного домена, стоимость флапающего алерта. Без этой связи надёжность остаётся «инженерной заботой» и проигрывает фичам в борьбе за время команды. Каждый раз.</p>
<p>Схематически петля выглядит так:</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/ef991189416f6978343e90695eb0eeae1e5ccd593ba834ee0625948b10975e89.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-ef991189416f-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 485.117px; background-color: transparent;" viewBox="0 0 485.1171875 786" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-ef991189416f-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-ef991189416f-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-ef991189416f-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-ef991189416f-light .error-icon{fill:#dce0e8;}#m-ef991189416f-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-ef991189416f-light .edge-thickness-normal{stroke-width:1px;}#m-ef991189416f-light .edge-thickness-thick{stroke-width:3.5px;}#m-ef991189416f-light .edge-pattern-solid{stroke-dasharray:0;}#m-ef991189416f-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-ef991189416f-light .edge-pattern-dashed{stroke-dasharray:3;}#m-ef991189416f-light .edge-pattern-dotted{stroke-dasharray:2;}#m-ef991189416f-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-ef991189416f-light .marker.cross{stroke:#5c5f77;}#m-ef991189416f-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-ef991189416f-light p{margin:0;}#m-ef991189416f-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-ef991189416f-light .cluster-label text{fill:#4c4f69;}#m-ef991189416f-light .cluster-label span{color:#4c4f69;}#m-ef991189416f-light .cluster-label span p{background-color:transparent;}#m-ef991189416f-light .label text,#m-ef991189416f-light span{fill:#4c4f69;color:#4c4f69;}#m-ef991189416f-light .node rect,#m-ef991189416f-light .node circle,#m-ef991189416f-light .node ellipse,#m-ef991189416f-light .node polygon,#m-ef991189416f-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-ef991189416f-light .rough-node .label text,#m-ef991189416f-light .node .label text,#m-ef991189416f-light .image-shape .label,#m-ef991189416f-light .icon-shape .label{text-anchor:middle;}#m-ef991189416f-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-ef991189416f-light .rough-node .label,#m-ef991189416f-light .node .label,#m-ef991189416f-light .image-shape .label,#m-ef991189416f-light .icon-shape .label{text-align:center;}#m-ef991189416f-light .node.clickable{cursor:pointer;}#m-ef991189416f-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-ef991189416f-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-ef991189416f-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-ef991189416f-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-ef991189416f-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-ef991189416f-light .edgeLabel p{background-color:#eff1f5;}#m-ef991189416f-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-ef991189416f-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-ef991189416f-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-ef991189416f-light .cluster text{fill:#4c4f69;}#m-ef991189416f-light .cluster span{color:#4c4f69;}#m-ef991189416f-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-ef991189416f-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-ef991189416f-light rect.text{fill:none;stroke-width:0;}#m-ef991189416f-light .icon-shape,#m-ef991189416f-light .image-shape{background-color:#eff1f5;text-align:center;}#m-ef991189416f-light .icon-shape p,#m-ef991189416f-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-ef991189416f-light .icon-shape .label rect,#m-ef991189416f-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-ef991189416f-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-ef991189416f-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-ef991189416f-light .node .neo-node{stroke:#bcc0cc;}#m-ef991189416f-light [data-look="neo"].node rect,#m-ef991189416f-light [data-look="neo"].cluster rect,#m-ef991189416f-light [data-look="neo"].node polygon{stroke:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].node path{stroke:url(#m-ef991189416f-light-gradient);stroke-width:1px;}#m-ef991189416f-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-ef991189416f-light [data-look="neo"].node circle{stroke:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-ef991189416f-light [data-look="neo"].icon-shape .icon{fill:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-ef991189416f-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-ef991189416f-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-ef991189416f-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M292.32,52.458L264.374,57.965C236.427,63.472,180.534,74.486,152.587,83.493C124.641,92.5,124.641,99.5,124.641,103L124.641,106.5" id="m-ef991189416f-light-L_SLO_Gate_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SLO_Gate_0" data-points="W3sieCI6MjkyLjMyMDMxMjUsInkiOjUyLjQ1NzY4MjQ4NzIzMzR9LHsieCI6MTI0LjY0MDYyNSwieSI6ODUuNX0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoxMTAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,163L124.641,167.167C124.641,171.333,124.641,179.667,124.641,187.333C124.641,195,124.641,202,124.641,205.5L124.641,209" id="m-ef991189416f-light-L_Gate_Release_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Gate_Release_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MTYzfSx7IngiOjEyNC42NDA2MjUsInkiOjE4OH0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoyMTN9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,265.5L124.641,269.667C124.641,273.833,124.641,282.167,124.641,289.833C124.641,297.5,124.641,304.5,124.641,308L124.641,311.5" id="m-ef991189416f-light-L_Release_Obs_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Release_Obs_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MjY1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MjkwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MzE1LjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,368L124.641,372.167C124.641,376.333,124.641,384.667,124.641,392.333C124.641,400,124.641,407,124.641,410.5L124.641,414" id="m-ef991189416f-light-L_Obs_Inc_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Obs_Inc_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MzY4fSx7IngiOjEyNC42NDA2MjUsInkiOjM5M30seyJ4IjoxMjQuNjQwNjI1LCJ5Ijo0MTh9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M124.641,470.5L124.641,474.667C124.641,478.833,124.641,487.167,124.641,494.833C124.641,502.5,124.641,509.5,124.641,513L124.641,516.5" id="m-ef991189416f-light-L_Inc_PM_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Inc_PM_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6NDcwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NDk1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NTIwLjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M119.519,573L118.706,577.167C117.893,581.333,116.267,589.667,115.454,597.333C114.641,605,114.641,612,114.641,615.5L114.641,619" id="m-ef991189416f-light-L_PM_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_Policy_0" data-points="W3sieCI6MTE5LjUxODY3Mzc4MDQ4NzgsInkiOjU3M30seyJ4IjoxMTQuNjQwNjI1LCJ5Ijo1OTh9LHsieCI6MTE0LjY0MDYyNSwieSI6NjIzfV0=" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M180.744,573L189.649,577.167C198.555,581.333,216.365,589.667,232.77,597.695C249.175,605.723,264.174,613.446,271.674,617.307L279.173,621.169" id="m-ef991189416f-light-L_PM_FinOps_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_FinOps_0" data-points="W3sieCI6MTgwLjc0Mzk5NzcxMzQxNDY0LCJ5Ijo1NzN9LHsieCI6MjM0LjE3NTc4MTI1LCJ5Ijo1OTh9LHsieCI6MjgyLjcyOTUxNjAwNjA5NzYsInkiOjYyM31d" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M114.641,675.5L114.641,679.667C114.641,683.833,114.641,692.167,127.867,700.308C141.094,708.45,167.548,716.399,180.775,720.374L194.002,724.349" id="m-ef991189416f-light-L_Policy_Runbook_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Runbook_0" data-points="W3sieCI6MTE0LjY0MDYyNSwieSI6Njc1LjV9LHsieCI6MTE0LjY0MDYyNSwieSI6NzAwLjV9LHsieCI6MTk3LjgzMjMxNzA3MzE3MDcsInkiOjcyNS41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M372.535,725.5L386.4,721.333C400.265,717.167,427.996,708.833,441.861,696.125C455.727,683.417,455.727,666.333,455.727,649.25C455.727,632.167,455.727,615.083,455.727,598C455.727,580.917,455.727,563.833,455.727,546.75C455.727,529.667,455.727,512.583,455.727,495.5C455.727,478.417,455.727,461.333,455.727,444.25C455.727,427.167,455.727,410.083,455.727,393C455.727,375.917,455.727,358.833,455.727,341.75C455.727,324.667,455.727,307.583,455.727,290.5C455.727,273.417,455.727,256.333,455.727,239.25C455.727,222.167,455.727,205.083,455.727,188C455.727,170.917,455.727,153.833,455.727,136.75C455.727,119.667,455.727,102.583,450.494,90.265C445.262,77.947,434.797,70.394,429.564,66.617L424.332,62.841" id="m-ef991189416f-light-L_Runbook_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Runbook_SLO_0" data-points="W3sieCI6MzcyLjUzNDg3MDQyNjgyOTMsInkiOjcyNS41fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo3MDAuNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NjQ5LjI1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo1OTh9LHsieCI6NDU1LjcyNjU2MjUsInkiOjU0Ni43NX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NDk1LjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjQ0NC4yNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MzkzfSx7IngiOjQ1NS43MjY1NjI1LCJ5IjozNDEuNzV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjI5MC41fSx7IngiOjQ1NS43MjY1NjI1LCJ5IjoyMzkuMjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjE4OH0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MTM2Ljc1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo4NS41fSx7IngiOjQyMS4wODg2MDUxODI5MjY4LCJ5Ijo2MC41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/><path d="M359.837,623L363.984,618.833C368.131,614.667,376.425,606.333,380.572,593.625C384.719,580.917,384.719,563.833,384.719,546.75C384.719,529.667,384.719,512.583,384.719,495.5C384.719,478.417,384.719,461.333,384.719,444.25C384.719,427.167,384.719,410.083,384.719,393C384.719,375.917,384.719,358.833,384.719,341.75C384.719,324.667,384.719,307.583,384.719,290.5C384.719,273.417,384.719,256.333,384.719,239.25C384.719,222.167,384.719,205.083,384.719,188C384.719,170.917,384.719,153.833,384.719,136.75C384.719,119.667,384.719,102.583,384.719,90.542C384.719,78.5,384.719,71.5,384.719,68L384.719,64.5" id="m-ef991189416f-light-L_FinOps_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_FinOps_SLO_0" data-points="W3sieCI6MzU5LjgzNjg5MDI0MzkwMjQsInkiOjYyM30seyJ4IjozODQuNzE4NzUsInkiOjU5OH0seyJ4IjozODQuNzE4NzUsInkiOjU0Ni43NX0seyJ4IjozODQuNzE4NzUsInkiOjQ5NS41fSx7IngiOjM4NC43MTg3NSwieSI6NDQ0LjI1fSx7IngiOjM4NC43MTg3NSwieSI6MzkzfSx7IngiOjM4NC43MTg3NSwieSI6MzQxLjc1fSx7IngiOjM4NC43MTg3NSwieSI6MjkwLjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoyMzkuMjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoxODh9LHsieCI6Mzg0LjcxODc1LCJ5IjoxMzYuNzV9LHsieCI6Mzg0LjcxODc1LCJ5Ijo4NS41fSx7IngiOjM4NC43MTg3NSwieSI6NjAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_SLO_Gate_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Gate_Release_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Release_Obs_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Obs_Inc_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Inc_PM_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_FinOps_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Runbook_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Runbook_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_FinOps_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-ef991189416f-light-flowchart-SLO-0" data-look="classic" transform="translate(384.71875, 34.25)"><rect class="basic label-container" style="" x="-92.3984375" y="-26.25" width="184.796875" height="52.5"/><g class="label" style="" transform="translate(-62.3984375, -11.25)"><rect/><foreignObject width="124.796875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>SLO как контракт</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Gate-1" data-look="classic" transform="translate(124.640625, 136.75)"><rect class="basic label-container" style="" x="-74.09375" y="-26.25" width="148.1875" height="52.5"/><g class="label" style="" transform="translate(-44.09375, -11.25)"><rect/><foreignObject width="88.1875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Gate в CI/CD</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Release-3" data-look="classic" transform="translate(124.640625, 239.25)"><rect class="basic label-container" style="" x="-85.7578125" y="-26.25" width="171.515625" height="52.5"/><g class="label" style="" transform="translate(-55.7578125, -11.25)"><rect/><foreignObject width="111.515625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Релиз и трафик</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Obs-5" data-look="classic" transform="translate(124.640625, 341.75)"><rect class="basic label-container" style="" x="-75.0390625" y="-26.25" width="150.078125" height="52.5"/><g class="label" style="" transform="translate(-45.0390625, -11.25)"><rect/><foreignObject width="90.078125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Inc-7" data-look="classic" transform="translate(124.640625, 444.25)"><rect class="basic label-container" style="" x="-116.640625" y="-26.25" width="233.28125" height="52.5"/><g class="label" style="" transform="translate(-86.640625, -11.25)"><rect/><foreignObject width="173.28125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Инцидент или near-miss</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-PM-9" data-look="classic" transform="translate(124.640625, 546.75)"><rect class="basic label-container" style="" x="-74.9453125" y="-26.25" width="149.890625" height="52.5"/><g class="label" style="" transform="translate(-44.9453125, -11.25)"><rect/><foreignObject width="89.890625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Постмортем</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Policy-11" data-look="classic" transform="translate(114.640625, 649.25)"><rect class="basic label-container" style="" x="-82.0546875" y="-26.25" width="164.109375" height="52.5"/><g class="label" style="" transform="translate(-52.0546875, -11.25)"><rect/><foreignObject width="104.109375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Policy-as-code</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-FinOps-13" data-look="classic" transform="translate(333.7109375, 649.25)"><rect class="basic label-container" style="" x="-87.015625" y="-26.25" width="174.03125" height="52.5"/><g class="label" style="" transform="translate(-57.015625, -11.25)"><rect/><foreignObject width="114.03125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>FinOps-витрина</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-light-flowchart-Runbook-15" data-look="classic" transform="translate(285.18359375, 751.75)"><rect class="basic label-container" style="" x="-91.765625" y="-26.25" width="183.53125" height="52.5"/><g class="label" style="" transform="translate(-61.765625, -11.25)"><rect/><foreignObject width="123.53125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Runbook-as-code</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-ef991189416f-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-ef991189416f-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-ef991189416f-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-ef991189416f-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 485.117px; background-color: transparent;" viewBox="0 0 485.1171875 786" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-ef991189416f-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-ef991189416f-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-ef991189416f-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-ef991189416f-dark .error-icon{fill:#181926;}#m-ef991189416f-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-ef991189416f-dark .edge-thickness-normal{stroke-width:1px;}#m-ef991189416f-dark .edge-thickness-thick{stroke-width:3.5px;}#m-ef991189416f-dark .edge-pattern-solid{stroke-dasharray:0;}#m-ef991189416f-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-ef991189416f-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-ef991189416f-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-ef991189416f-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-ef991189416f-dark .marker.cross{stroke:#a5adcb;}#m-ef991189416f-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-ef991189416f-dark p{margin:0;}#m-ef991189416f-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-ef991189416f-dark .cluster-label text{fill:#cad3f5;}#m-ef991189416f-dark .cluster-label span{color:#cad3f5;}#m-ef991189416f-dark .cluster-label span p{background-color:transparent;}#m-ef991189416f-dark .label text,#m-ef991189416f-dark span{fill:#cad3f5;color:#cad3f5;}#m-ef991189416f-dark .node rect,#m-ef991189416f-dark .node circle,#m-ef991189416f-dark .node ellipse,#m-ef991189416f-dark .node polygon,#m-ef991189416f-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-ef991189416f-dark .rough-node .label text,#m-ef991189416f-dark .node .label text,#m-ef991189416f-dark .image-shape .label,#m-ef991189416f-dark .icon-shape .label{text-anchor:middle;}#m-ef991189416f-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-ef991189416f-dark .rough-node .label,#m-ef991189416f-dark .node .label,#m-ef991189416f-dark .image-shape .label,#m-ef991189416f-dark .icon-shape .label{text-align:center;}#m-ef991189416f-dark .node.clickable{cursor:pointer;}#m-ef991189416f-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-ef991189416f-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-ef991189416f-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-ef991189416f-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-ef991189416f-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-ef991189416f-dark .edgeLabel p{background-color:#24273a;}#m-ef991189416f-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-ef991189416f-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-ef991189416f-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-ef991189416f-dark .cluster text{fill:#cad3f5;}#m-ef991189416f-dark .cluster span{color:#cad3f5;}#m-ef991189416f-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-ef991189416f-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-ef991189416f-dark rect.text{fill:none;stroke-width:0;}#m-ef991189416f-dark .icon-shape,#m-ef991189416f-dark .image-shape{background-color:#24273a;text-align:center;}#m-ef991189416f-dark .icon-shape p,#m-ef991189416f-dark .image-shape p{background-color:#24273a;padding:2px;}#m-ef991189416f-dark .icon-shape .label rect,#m-ef991189416f-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-ef991189416f-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-ef991189416f-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-ef991189416f-dark .node .neo-node{stroke:#494d64;}#m-ef991189416f-dark [data-look="neo"].node rect,#m-ef991189416f-dark [data-look="neo"].cluster rect,#m-ef991189416f-dark [data-look="neo"].node polygon{stroke:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].node path{stroke:url(#m-ef991189416f-dark-gradient);stroke-width:1px;}#m-ef991189416f-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-ef991189416f-dark [data-look="neo"].node circle{stroke:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-ef991189416f-dark [data-look="neo"].icon-shape .icon{fill:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-ef991189416f-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-ef991189416f-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-ef991189416f-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-ef991189416f-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M292.32,52.458L264.374,57.965C236.427,63.472,180.534,74.486,152.587,83.493C124.641,92.5,124.641,99.5,124.641,103L124.641,106.5" id="m-ef991189416f-dark-L_SLO_Gate_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SLO_Gate_0" data-points="W3sieCI6MjkyLjMyMDMxMjUsInkiOjUyLjQ1NzY4MjQ4NzIzMzR9LHsieCI6MTI0LjY0MDYyNSwieSI6ODUuNX0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoxMTAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,163L124.641,167.167C124.641,171.333,124.641,179.667,124.641,187.333C124.641,195,124.641,202,124.641,205.5L124.641,209" id="m-ef991189416f-dark-L_Gate_Release_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Gate_Release_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MTYzfSx7IngiOjEyNC42NDA2MjUsInkiOjE4OH0seyJ4IjoxMjQuNjQwNjI1LCJ5IjoyMTN9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,265.5L124.641,269.667C124.641,273.833,124.641,282.167,124.641,289.833C124.641,297.5,124.641,304.5,124.641,308L124.641,311.5" id="m-ef991189416f-dark-L_Release_Obs_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Release_Obs_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MjY1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MjkwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6MzE1LjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,368L124.641,372.167C124.641,376.333,124.641,384.667,124.641,392.333C124.641,400,124.641,407,124.641,410.5L124.641,414" id="m-ef991189416f-dark-L_Obs_Inc_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Obs_Inc_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6MzY4fSx7IngiOjEyNC42NDA2MjUsInkiOjM5M30seyJ4IjoxMjQuNjQwNjI1LCJ5Ijo0MTh9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M124.641,470.5L124.641,474.667C124.641,478.833,124.641,487.167,124.641,494.833C124.641,502.5,124.641,509.5,124.641,513L124.641,516.5" id="m-ef991189416f-dark-L_Inc_PM_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Inc_PM_0" data-points="W3sieCI6MTI0LjY0MDYyNSwieSI6NDcwLjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NDk1LjV9LHsieCI6MTI0LjY0MDYyNSwieSI6NTIwLjV9XQ==" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M119.519,573L118.706,577.167C117.893,581.333,116.267,589.667,115.454,597.333C114.641,605,114.641,612,114.641,615.5L114.641,619" id="m-ef991189416f-dark-L_PM_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_Policy_0" data-points="W3sieCI6MTE5LjUxODY3Mzc4MDQ4NzgsInkiOjU3M30seyJ4IjoxMTQuNjQwNjI1LCJ5Ijo1OTh9LHsieCI6MTE0LjY0MDYyNSwieSI6NjIzfV0=" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M180.744,573L189.649,577.167C198.555,581.333,216.365,589.667,232.77,597.695C249.175,605.723,264.174,613.446,271.674,617.307L279.173,621.169" id="m-ef991189416f-dark-L_PM_FinOps_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PM_FinOps_0" data-points="W3sieCI6MTgwLjc0Mzk5NzcxMzQxNDY0LCJ5Ijo1NzN9LHsieCI6MjM0LjE3NTc4MTI1LCJ5Ijo1OTh9LHsieCI6MjgyLjcyOTUxNjAwNjA5NzYsInkiOjYyM31d" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M114.641,675.5L114.641,679.667C114.641,683.833,114.641,692.167,127.867,700.308C141.094,708.45,167.548,716.399,180.775,720.374L194.002,724.349" id="m-ef991189416f-dark-L_Policy_Runbook_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Runbook_0" data-points="W3sieCI6MTE0LjY0MDYyNSwieSI6Njc1LjV9LHsieCI6MTE0LjY0MDYyNSwieSI6NzAwLjV9LHsieCI6MTk3LjgzMjMxNzA3MzE3MDcsInkiOjcyNS41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M372.535,725.5L386.4,721.333C400.265,717.167,427.996,708.833,441.861,696.125C455.727,683.417,455.727,666.333,455.727,649.25C455.727,632.167,455.727,615.083,455.727,598C455.727,580.917,455.727,563.833,455.727,546.75C455.727,529.667,455.727,512.583,455.727,495.5C455.727,478.417,455.727,461.333,455.727,444.25C455.727,427.167,455.727,410.083,455.727,393C455.727,375.917,455.727,358.833,455.727,341.75C455.727,324.667,455.727,307.583,455.727,290.5C455.727,273.417,455.727,256.333,455.727,239.25C455.727,222.167,455.727,205.083,455.727,188C455.727,170.917,455.727,153.833,455.727,136.75C455.727,119.667,455.727,102.583,450.494,90.265C445.262,77.947,434.797,70.394,429.564,66.617L424.332,62.841" id="m-ef991189416f-dark-L_Runbook_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Runbook_SLO_0" data-points="W3sieCI6MzcyLjUzNDg3MDQyNjgyOTMsInkiOjcyNS41fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo3MDAuNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NjQ5LjI1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo1OTh9LHsieCI6NDU1LjcyNjU2MjUsInkiOjU0Ni43NX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6NDk1LjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjQ0NC4yNX0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MzkzfSx7IngiOjQ1NS43MjY1NjI1LCJ5IjozNDEuNzV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjI5MC41fSx7IngiOjQ1NS43MjY1NjI1LCJ5IjoyMzkuMjV9LHsieCI6NDU1LjcyNjU2MjUsInkiOjE4OH0seyJ4Ijo0NTUuNzI2NTYyNSwieSI6MTM2Ljc1fSx7IngiOjQ1NS43MjY1NjI1LCJ5Ijo4NS41fSx7IngiOjQyMS4wODg2MDUxODI5MjY4LCJ5Ijo2MC41fV0=" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/><path d="M359.837,623L363.984,618.833C368.131,614.667,376.425,606.333,380.572,593.625C384.719,580.917,384.719,563.833,384.719,546.75C384.719,529.667,384.719,512.583,384.719,495.5C384.719,478.417,384.719,461.333,384.719,444.25C384.719,427.167,384.719,410.083,384.719,393C384.719,375.917,384.719,358.833,384.719,341.75C384.719,324.667,384.719,307.583,384.719,290.5C384.719,273.417,384.719,256.333,384.719,239.25C384.719,222.167,384.719,205.083,384.719,188C384.719,170.917,384.719,153.833,384.719,136.75C384.719,119.667,384.719,102.583,384.719,90.542C384.719,78.5,384.719,71.5,384.719,68L384.719,64.5" id="m-ef991189416f-dark-L_FinOps_SLO_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_FinOps_SLO_0" data-points="W3sieCI6MzU5LjgzNjg5MDI0MzkwMjQsInkiOjYyM30seyJ4IjozODQuNzE4NzUsInkiOjU5OH0seyJ4IjozODQuNzE4NzUsInkiOjU0Ni43NX0seyJ4IjozODQuNzE4NzUsInkiOjQ5NS41fSx7IngiOjM4NC43MTg3NSwieSI6NDQ0LjI1fSx7IngiOjM4NC43MTg3NSwieSI6MzkzfSx7IngiOjM4NC43MTg3NSwieSI6MzQxLjc1fSx7IngiOjM4NC43MTg3NSwieSI6MjkwLjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoyMzkuMjV9LHsieCI6Mzg0LjcxODc1LCJ5IjoxODh9LHsieCI6Mzg0LjcxODc1LCJ5IjoxMzYuNzV9LHsieCI6Mzg0LjcxODc1LCJ5Ijo4NS41fSx7IngiOjM4NC43MTg3NSwieSI6NjAuNX1d" data-look="classic" marker-end="url(#m-ef991189416f-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_SLO_Gate_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Gate_Release_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Release_Obs_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Obs_Inc_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Inc_PM_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PM_FinOps_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Runbook_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Runbook_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_FinOps_SLO_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-ef991189416f-dark-flowchart-SLO-0" data-look="classic" transform="translate(384.71875, 34.25)"><rect class="basic label-container" style="" x="-92.3984375" y="-26.25" width="184.796875" height="52.5"/><g class="label" style="" transform="translate(-62.3984375, -11.25)"><rect/><foreignObject width="124.796875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>SLO как контракт</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Gate-1" data-look="classic" transform="translate(124.640625, 136.75)"><rect class="basic label-container" style="" x="-74.09375" y="-26.25" width="148.1875" height="52.5"/><g class="label" style="" transform="translate(-44.09375, -11.25)"><rect/><foreignObject width="88.1875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Gate в CI/CD</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Release-3" data-look="classic" transform="translate(124.640625, 239.25)"><rect class="basic label-container" style="" x="-85.7578125" y="-26.25" width="171.515625" height="52.5"/><g class="label" style="" transform="translate(-55.7578125, -11.25)"><rect/><foreignObject width="111.515625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Релиз и трафик</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Obs-5" data-look="classic" transform="translate(124.640625, 341.75)"><rect class="basic label-container" style="" x="-75.0390625" y="-26.25" width="150.078125" height="52.5"/><g class="label" style="" transform="translate(-45.0390625, -11.25)"><rect/><foreignObject width="90.078125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Inc-7" data-look="classic" transform="translate(124.640625, 444.25)"><rect class="basic label-container" style="" x="-116.640625" y="-26.25" width="233.28125" height="52.5"/><g class="label" style="" transform="translate(-86.640625, -11.25)"><rect/><foreignObject width="173.28125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Инцидент или near-miss</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-PM-9" data-look="classic" transform="translate(124.640625, 546.75)"><rect class="basic label-container" style="" x="-74.9453125" y="-26.25" width="149.890625" height="52.5"/><g class="label" style="" transform="translate(-44.9453125, -11.25)"><rect/><foreignObject width="89.890625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Постмортем</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Policy-11" data-look="classic" transform="translate(114.640625, 649.25)"><rect class="basic label-container" style="" x="-82.0546875" y="-26.25" width="164.109375" height="52.5"/><g class="label" style="" transform="translate(-52.0546875, -11.25)"><rect/><foreignObject width="104.109375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Policy-as-code</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-FinOps-13" data-look="classic" transform="translate(333.7109375, 649.25)"><rect class="basic label-container" style="" x="-87.015625" y="-26.25" width="174.03125" height="52.5"/><g class="label" style="" transform="translate(-57.015625, -11.25)"><rect/><foreignObject width="114.03125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>FinOps-витрина</p></span></div></foreignObject></g></g><g class="node default" id="m-ef991189416f-dark-flowchart-Runbook-15" data-look="classic" transform="translate(285.18359375, 751.75)"><rect class="basic label-container" style="" x="-91.765625" y="-26.25" width="183.53125" height="52.5"/><g class="label" style="" transform="translate(-61.765625, -11.25)"><rect/><foreignObject width="123.53125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Runbook-as-code</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-ef991189416f-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-ef991189416f-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-ef991189416f-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p>Каждая стрелка — правило, которое команда поддерживает сознательно. Выпадает одна — петля размыкается, DIS возвращается к набору практик, которые работают локально, но не складываются в систему обучения.</p>
<h2 id="право-на-отказ-и-организованная-деградация">Право на отказ и организованная деградация</h2>
<p>Когда я слышу разговоры о надёжности, рамка почти всегда одна — «всё должно работать». Зрелый подход добавляет к ней сознательно спроектированный отказ: что и в каком порядке выключается, когда поддерживать полный функционал уже невозможно.</p>
<p>Право на отказ — <strong>архитектурная сущность</strong>: контур деградации проектируешь так же, как нормальный путь запроса. И отвечаешь на четыре вопроса: какие функции выключаются первыми, какие последними, кто жмёт кнопку и какие сигналы триггерят автоматическую деградацию без человека.</p>
<p>Канонический публичный пример — <a href="https://github.blog/news-insights/company-news/oct21-post-incident-analysis/" rel="noopener noreferrer" target="_blank">инцидент GitHub 21 октября 2018</a>. После 43-секундного разрыва между восточными ДЦ orchestrator автоматически промоутнул реплики MySQL, и в данных образовалось расхождение. Команда сознательно остановила доставку 5 миллионов webhook-событий и 80 тысяч Pages-сборок на время восстановления целостности БД — формулировка из постмортема: «explicit choice to partially degrade site usability […] instead of jeopardizing data we had already received from users». Это и есть приоритет «целостность данных &gt; доступность фичи», превращённый в решение под давлением.</p>
<p>Другой угол: деградацию можно продавать. Cloudflare продаёт <a href="https://developers.cloudflare.com/cache/how-to/always-online/" rel="noopener noreferrer" target="_blank">Always Online</a> как явный контракт: если origin недоступен, edge отдаст статическую копию из кеша или версию из Internet Archive, динамика честно режется до статики. Клиент покупает «худший, но живой» ответ заранее. Netflix формализовал ту же идею в Hystrix (проект давно в maintenance mode, но паттерн пережил инструмент) и архитектуре рекомендательных пайплайнов: онлайн-расчёт не уложился в SLA — сервис отдаёт precomputed fallback, неперсонализированную подборку вместо пятисотки. Свежее продолжение той же линии — <a href="https://netflixtechblog.com/enhancing-netflix-reliability-with-service-level-prioritized-load-shedding-e735e6ce8f7d" rel="noopener noreferrer" target="_blank">service-level prioritized load shedding</a>: приоритизированный сброс нагрузки переехал с edge внутрь отдельных сервисов, где команда сама решает, какими запросами жертвовать первыми. Проверку боем устроил инфраструктурный сбой: на восстановлении Netflix поймал двенадцатикратный всплеск pre-fetch-запросов с Android-устройств, и шеддинг это съел. Показательно, куда сместился центр тяжести Netflix за десять лет: от «сломай прод и посмотри» к деградации, встроенной в каждый сервис как штатный механизм.</p>
<p>Отсюда три практических следствия.</p>
<p><strong>Домены отказа определяют, какой кусок продукта можно уронить целиком.</strong> Независимые ingress, балансировщики, DNS-резолверы и хранилища секретов на вертикаль. Падает один домен — продукт теряет одну способность и остаётся живым. Без изоляции пользователь получает белую страницу. Стоимость такой изоляции — выше; стоимость отказа от неё — каскад при первом серьёзном инциденте.</p>
<p><strong>Self-bootstrap, когда control-зависимости отвалились.</strong> Кэшированные сертификаты и токены, emergency-конфиги, offline-режим для критичных путей. Система поднимается сама. Или хотя бы дотягивает уже идущий трафик, пока её control plane лежит и поднять его некому. Для облака это значит: приватный кластер переживает падение IAM или API провайдера.</p>
<p><strong>Региональную репликацию решаешь per-domain.</strong> Для платёжной вертикали приоритет — согласованность; для каталога — доступность. Глобальное «давайте всё реплицировать активно-активно» приводит к тому, что часть доменов плохо ложится в выбранную модель и компенсирует это сложными ad-hoc обходами.</p>
<h2 id="граница-безопасной-автоматики">Граница безопасной автоматики</h2>
<p>Auto remediation на бумаге звучит как «система сама перезапускает сервисы и масштабирует ресурсы». На практике у автоматики всегда есть граница, и она важнее самой автоматики.</p>
<p><strong>Control plane и data plane автоматизируешь по-разному.</strong> Data plane может рестартиться, переезжать между узлами, масштабироваться самостоятельно — здесь автоматика работает в зоне с предсказуемыми последствиями. Control plane — нет, или с дополнительными гардрейлами: автоматические изменения IAM, политик доступа, маршрутизации трафика между регионами — это операции с длинным blast radius и долгим rollback.</p>
<p><a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/" rel="noopener noreferrer" target="_blank">Глобальный отказ Facebook 4 октября 2021</a><sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> — учебник по тому, что происходит, когда control plane завязан сам на себя. Рутинная команда оценки доступности backbone обвалила все межДЦ-линки; audit-инструмент, который должен был такую команду остановить, не сработал из-за бага. DNS-серверы автоматически сняли BGP-анонсы дата-центров — корректная политика «unhealthy = снимаем» превратилась в самоблокировку, когда «unhealthy» стало «всё одновременно». Инженеры на полдня лишились внутренних инструментов диагностики и физического доступа к части помещений. Урок: у автоматики control plane должен быть канал управления, не завязанный на то, что она контролирует.</p>
<p>Зеркальный кейс — <a href="https://habr.com/ru/companies/yandex_cloud_and_infra/articles/1004584/" rel="noopener noreferrer" target="_blank">платформа Yandex Infrastructure</a>, где тот же урок заложили заранее, до аварии. Компоненты основного облака выкатывает отдельный редко обновляемый админский контур: сервис не деплоит сам себя, и кольцо «чтобы поднять облако, нужно облако» разорвано архитектурно. Система прав живёт внутри облака, но у дежурного есть аварийный обход — кнопка выдаёт абсолютный доступ мимо IDM, чтобы разбор инцидента не ждал восстановления той самой системы, которая лежит. SSH-сервер написан с нулём онлайновых зависимостей, аутентификация переведена с ключей на сертификаты, и раскатка прав по хостам перестала занимать 15 минут и ушла с критического пути. За шесть лет — три серьёзных инцидента на ~150 000 хостов и ни одного полного краха. За красивой фразой «канал управления, не зависящий от контролируемого» стоит вот такой список конкретных решений, и каждое кто-то оплатил временем команды.</p>
<p><strong>Любое рискованное автодействие — через kill-switch, голосование и bounds.</strong> Bounds по времени — не больше трёх автодействий за пять минут, дальше стоп и эскалация на человека. По объёму — не больше 10% парка за один проход. По типу — никогда для production-control-plane. Конкретные цифры у каждой команды свои, важно другое: лимиты записаны в конфиге автоматики, а не в голове дежурного. Голосование — для решений без однозначно безопасного исхода. Автоматический rollback на предыдущую работающую версию допустим. Автоматическое поднятие шкалы трафика после отказа — нет.</p>
<p>Хрестоматийный кейс отсутствующих bounds — <a href="https://www.sec.gov/files/litigation/admin/2013/34-70694.pdf" rel="noopener noreferrer" target="_blank">Knight Capital, 1 августа 2012</a>. Новый код раскатили на 7 из 8 SMARS-серверов, а на восьмом остался мёртвый код «Power Peg», от которого Knight отказался ещё в 2003-м — и флаг, который новый код переиспользовал, на этом сервере активировал именно его. Хуже того, в 2005-м из Power Peg вынесли счётчик исполненных акций и после этого не перетестировали: останавливаться после заполнения родительского ордера код разучился. Никакой проверки «сколько ордеров в секунду — слишком много», никакого rate-cap на исходящий поток не было — за ~45 минут система отправила миллионы дочерних ордеров, набила ~4 миллиона исполнений и сожгла более $460M по оценке SEC, фактически уничтожив компанию. Автоматика без bounds масштабировала ошибку человека быстрее, чем кто-либо успел вмешаться.</p>
<p><strong>Auto-healing допустим только там, где есть наблюдаемость, reversible-действия и понятный rollback-план.</strong> Если автоматику нельзя откатить одним действием — она работает только с человеком в петле. Это отсеивает большинство «умных» сценариев самовосстановления, которые красиво звучат в обзорах, но в проде разрывают петлю обратной связи.</p>
<p>Та же платформа Яндекса — сильный аргумент в пользу такой строгости. На масштабе ~50 000 приложений там сделали ставку на скорость доступа человека: дежурный дотягивается до системы за секунды, вместо того чтобы ждать, пока она починит себя сама. Философия прямо обратная модному auto-healing, и результат по надёжности от этого не пострадал. Полезная поправка к ощущению, что зрелость измеряется долей автоматических действий: измеряется она тем, у кого остаётся возможность вмешаться, когда автоматика ошиблась.</p>
<p>Meta зашла с третьей стороны и автоматизировала диагноз. Платформа <a href="https://engineering.fb.com/2025/12/19/data-infrastructure/drp-metas-root-cause-analysis-platform-at-scale/" rel="noopener noreferrer" target="_blank">DrP</a> — это root cause analysis как сервис: 300+ команд, 2000 анализаторов, 50 тысяч автоматических расследований в день и снижение MTTR на 20–80%. Автоматика действий там тоже есть, но центр тяжести смещён в диагноз: платформа приносит дежурному ранжированную гипотезу и оставляет решение ему. Ещё раньше туда же <a href="https://engineering.fb.com/2024/06/24/data-infrastructure/leveraging-ai-for-efficient-incident-response/" rel="noopener noreferrer" target="_blank">подключили LLM</a> — ранжировать подозрительные изменения при расследовании инцидента. Логика та же, что у Яндекса с break-glass: автоматический диагноз — безопасная зона. Ошибётся анализатор — дежурный потеряет минуты на проверку гипотезы, а не прод.</p>
<p><strong>Runbook-as-code: ручное действие живёт один раз.</strong> Попало в постмортем — либо исчезает (через архитектурное изменение, после которого действие не нужно), либо становится безопасной автоматикой с лимитами. Накопление повторяющихся ручных шагов — главный симптом разорванной петли.</p>
<p><strong>Release safety как код.</strong> Канареечный релиз с автоматическим rollback по порогам ошибок и латентности, freeze windows на пиковые периоды (новогодние праздники, чёрная пятница, день платежей), обязательные проверки политик. Договорённость с командой можно «в этот раз пропустить». Код, который останавливает пайплайн, — нельзя.</p>
<p>Главное правило: автоматика делает только то, за что инженер готов отвечать ночью. Всё остальное — под kill-switch и по шагам.</p>
<h2 id="экономика-надёжности">Экономика надёжности</h2>
<p>Самый сильный признак зрелого DIS — споры переезжают с веры на экономику. До этого <a href="/posts/reliability-is-a-conversation/">разговор о надёжности</a> сводится к «нам нужна ещё одна девятка» или «у нас слишком много шума в алертах» — без цифр.</p>
<p><strong>Стоимость одной девяточки</strong> — конкретная сумма, привязанная к домену отказа. Девятка для платёжного домена и девятка для рекомендательной выдачи стоят разных денег и приносят разную ценность. Глобальный SLO «99.95% по всему сервису» — худший компромисс между этими крайностями.</p>
<p><strong>Цена шума</strong> — стоимость флапающего алерта. Измеряется в человеко-часах ночной работы, в том, какие решения принимаются на следующее утро, в проценте пропущенных P1 на фоне P3-шума. Шумовой бюджет — управляемая метрика: команда видит свой flap rate, владеет качеством сигналов и платит за избыточное оповещение тем же бюджетом, что и за реальные инциденты.</p>
<p><strong>Цена эскалаций.</strong> Путь от P3 до P0 имеет свою стоимость: каждое повышение уровня — это человек, которого подняли среди ночи. Срежешь этот путь через policy-as-code — автоматическая первичная диагностика, маршрутизация к нужному owner-у, обогащение алерта контекстом — и это сразу деньги.</p>
<p><strong>FinOps-витрина «стоимость простоя»</strong> висит рядом с MTTR-дашбордом. Один отвечает на «как быстро мы поднимаем», другой — на «во сколько нам обошлась минута». Без второй цифры приоритеты надёжности расставляют «по ощущениям».</p>
<p><strong>Управляемая девяточность.</strong> Целишься в оптимум, а максимум оставляешь тем, кому его оплатят. Лишняя девятка стоит дорого: переход с 99.9% на 99.99% кратно увеличивает вложения в инфраструктуру и команду, а прирост пользовательской ценности с этими вложениями несопоставим. Зрелая команда умеет сказать руководству «нам не нужна следующая девятка» и объяснить, почему.</p>
<p>Сколько у тебя стоит минута простоя платёжного пути? Если ответа нет, весь дальнейший разговор про девятки — вкусовщина, и спорить в нём будет тот, кто громче.</p>
<p>Когда про DIS говорят на языке денег, руководство принимает решения о надёжности так же, как о фичах. Пока этого языка нет, надёжность живёт только на инженерных совещаниях, а бюджет уходит туда, где ценность показана в деньгах.</p>
<h2 id="чужие-лестницы-зрелости">Чужие лестницы зрелости</h2>
<p>Прежде чем предлагать свою диагностику, честный вопрос: может, готовая линейка уже есть? Я пошёл проверять и обнаружил показательную вещь. <strong>Сводной модели зрелости DIS не существует.</strong> Gartner её не публиковал: ни <a href="https://www.gartner.com/en/articles/what-is-a-digital-immune-system-and-why-does-it-matter" rel="noopener noreferrer" target="_blank">статья-первоисточник</a>, ни <a href="https://www.gartner.com/en/information-technology/glossary/digital-immune-system" rel="noopener noreferrer" target="_blank">глоссарий</a> уровней не предлагают, а в названиях платных Hype Cycle модель зрелости DIS не значится. Зато в 2023–2025 стадийные модели появились у тех, кто термин не употребляет вовсе — и по ним хорошо видно, куда индустрия смотрит на самом деле.</p>
<p>Самая близкая к «лестнице зрелости DIS» вещь на рынке — <a href="https://learn.microsoft.com/en-us/azure/well-architected/reliability/maturity-model" rel="noopener noreferrer" target="_blank">Reliability Maturity Model</a> из Azure Well-Architected Framework, появившаяся в 2025 году. Пять уровней: Get resilient → Self-preservation → Recovery readiness → Maintain stability → Stay resilient. SRE как роль появляется на третьем уровне, выделенная SRE-функция и safe deployment practices — на четвёртом, а пятый честно объявлен бесконечным: устойчивость к рискам, которые ещё не случались. Ирония в том, что «гартнеровскую» модель зрелости в итоге написал Microsoft.</p>
<p>AWS пошёл другим путём, и для этого поста он показательнее. <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/resilience-lifecycle-framework/introduction.html" rel="noopener noreferrer" target="_blank">Resilience Lifecycle Framework</a> — пять стадий, сознательно замкнутых в круг: Set Objectives → Design and Implement → Evaluate and Test → Operate → Respond and Learn. Последняя стадия возвращает тебя к первой: выучил урок инцидента — пересмотрел цели. AWS независимо пришёл к тому же выводу, вокруг которого построен весь этот пост: зрелость — это замкнутая петля, а не этаж, на который поднялся и остался. Инструментальная обвязка тоже живая: <a href="https://aws.amazon.com/about-aws/whats-new/2024/12/aws-resilience-hub-fault-injection-service-recommendations/" rel="noopener noreferrer" target="_blank">Resilience Hub</a> с конца 2024 года сам рекомендует эксперименты Fault Injection Service под конкретную архитектуру — chaos engineering из ручной дисциплины превращается в подсказку сервиса.</p>
<p>У CNCF есть <a href="https://maturitymodel.cncf.io/" rel="noopener noreferrer" target="_blank">Cloud Native Maturity Model</a> от рабочей группы Cartografos: пять уровней по четырём измерениям — People, Process, Policy, Technology. Для нашей темы интересны верхние ступени: chaos engineering как штатная операция появляется на четвёртом уровне, автоматическое восстановление в known good state — на пятом. То есть автоматика самовосстановления и здесь оказалась вершиной лестницы. Входным билетом её не считает никто.</p>
<p>По отдельным столпам DIS лестницы существуют давно, но живут независимо друг от друга. Chaos engineering — классическая Chaos Maturity Model из книги Netflix/O&rsquo;Reilly с осями sophistication и adoption, плюс практические руководства по внедрению у <a href="https://www.gremlin.com/community/tutorials/chaos-engineering-adoption-guide" rel="noopener noreferrer" target="_blank">Gremlin</a> и <a href="https://www.harness.io/resources/the-chaos-engineering-maturity-model" rel="noopener noreferrer" target="_blank">eBook Harness</a>. Observability — <a href="https://grafana.com/blog/2024/01/29/how-to-improve-your-observability-strategy-introducing-the-observability-journey-maturity-model/" rel="noopener noreferrer" target="_blank">Grafana Observability Journey</a> (Reactive → Proactive → Systematic) и <a href="https://docs.newrelic.com/docs/new-relic-solutions/observability-maturity/introduction/" rel="noopener noreferrer" target="_blank">модель New Relic</a>. Reliability целиком — <a href="https://www.usenix.org/publications/loginonline/reliability-maturity-model" rel="noopener noreferrer" target="_blank">Reliability Maturity Model</a> в журнале <a href="https://www.usenix.org/publications/loginonline" rel="noopener noreferrer" target="_blank">;login:</a> от USENIX (absent → reactive → proactive → strategic → visionary) и опросник <a href="https://sre.google/static/pdf/cloud-cre-production-maturity-assessment.pdf" rel="noopener noreferrer" target="_blank">Google CRE Production Maturity Assessment</a>. Обе — гугловые, и мерят они разное: первая описывает зрелость организации и её отношение к надёжности, вторая опрашивает про готовность конкретного прода. Supply chain — <a href="https://slsa.dev/spec/latest/levels" rel="noopener noreferrer" target="_blank">SLSA</a> с уровнями L0–L3, единственная по-настоящему формализованная лестница из всех шести столпов. А для auto remediation индустриального стандарта нет вовсе — только вендорские блоги, каждый со своей нумерацией.</p>
<p>Лестницы, которая охватывала бы устойчивость целиком, ни Google, ни Meta не предлагают — только компонентные модели из абзаца выше. Вместо общей шкалы у обеих эволюция метода. Google в статье <a href="https://www.usenix.org/publications/loginonline/evolution-sre-google" rel="noopener noreferrer" target="_blank">«The Evolution of SRE at Google»</a> (журнал <a href="https://www.usenix.org/publications/loginonline" rel="noopener noreferrer" target="_blank">;login:</a> от USENIX, авторы Tim Falzone и Ben Treynor Sloss, основатель Google SRE) признал, что классическая модель error budgets перестала покрывать системы, где допустимый ущерб равен нулю, и переходит на <a href="https://sre.google/resources/practices-and-processes/stpa/" rel="noopener noreferrer" target="_blank">STAMP/STPA</a> — системный анализ опасностей вместо подсчёта бюджета ошибок. Meta вкладывается в автоматизацию диагноза, о которой я писал выше. Обе стратегии сводятся к «поменяй способ думать об отказах». Для лестниц как жанра это плохая новость.</p>
<p>Академия к теме только подбирается. Единственная известная мне работа, которая целенаправленно строит именно DIS Maturity Model — <a href="https://link.springer.com/chapter/10.1007/978-3-031-59465-6_6" rel="noopener noreferrer" target="_blank">«A Conceptual Model of Digital Immune System to Increase the Resilience of Technology Ecosystems»</a> Беате Краузе и Яниса Грабиса (RCIS 2024, Springer): концептуальная модель устойчивости технологической экосистемы в контексте европейской регуляторики NIS2 и DORA. Судя по <a href="https://link.springer.com/chapter/10.1007/978-3-031-92471-2_13" rel="noopener noreferrer" target="_blank">продолжению 2025 года</a>, до опубликованной шкалы уровней дело пока не дошло. А в ноябре 2025 вышла первая целая книга про DIS — <a href="https://onlinelibrary.wiley.com/doi/book/10.1002/9781394383788" rel="noopener noreferrer" target="_blank">«Digital Immune System: Principles and Practices»</a> (Wiley-Scrivener): уклон в security и AI, но отдельная глава про экономику внедрения там есть. Приятно: не мне одному кажется, что разговор про DIS — это в том числе разговор экономистов.</p>
<p>Итог ревизии: компонентные лестницы есть, цельной — нет. Ни одна из существующих моделей не проверяет главного — замыкается ли петля между компонентами. Поэтому дальше — пять вопросов на связность вместо шестой лестницы.</p>
<h2 id="маркеры-зрелого-dis">Маркеры зрелого DIS</h2>
<p>Как отличить «у нас DIS» от «у нас отдельные практики»? Пять наблюдаемых маркеров. Это диагностика, не предписание: набрал три из пяти — основа для иммунной системы у тебя есть.</p>
<ol>
<li><strong>SLO двигают релизы.</strong> SLO, который не закрывает gate в CI/CD, остаётся отчётом для руководства.</li>
<li><strong>Класс проблем не повторяется.</strong> Каждый крупный инцидент уменьшает вероятность повторения класса проблем. В правилах и политиках видно, что система «училась»: после инцидента появился новый gate, изменился лимит автоматики, добавилась проверка в policy-as-code.</li>
<li><strong>Автоматика делает только обоснованное.</strong> Каждое автодействие имеет владельца, лимиты и kill-switch. Всё остальное живёт под человеком в петле.</li>
<li><strong>Команды спорят на языке экономики.</strong> Приоритеты надёжности обсуждают в рублях: стоимость девяточек, шума, эскалаций. Фразу «у нас плохо с алертами» на таком совещании перебивают вопросом «сколько это стоит».</li>
<li><strong>Руководство видит стоимость надёжности</strong> и принимает решения на этом языке. На совещании уровня CTO рядом с дашбордом MTTR висит дашборд FinOps.</li>
</ol>
<p>Ноль маркеров — DIS нет, какие бы инструменты ни были развёрнуты. Все пять — фокус смещается с построения иммунной системы на её эксплуатацию.</p>
<h2 id="что-ломает-dis-на-масштабе">Что ломает DIS на масштабе</h2>
<p>Разваливается всё тихо: кто-то пропускает один шаг в петле, и дальше система живёт по инерции. Архитектура при этом обычно нормальная. Ниже — типичные failure modes.</p>
<p><strong>SLO без gate.</strong> Есть метрика, есть SLO, есть <a href="/posts/burn-rate-is-not-speed/">error budget</a>, но релиз не блокируется, когда бюджет исчерпан. Цикл разомкнут: измеряем, но не действуем. Лечение: сделать SLO условием пайплайна.</p>
<p><strong>Автоматика, которой никто не поставил границ.</strong> Auto-healing пытается «починить» инфраструктурный инцидент — рестартит поды, перекидывает трафик, масштабирует кластер — и тем самым расширяет blast radius. Каждому автодействию нужны bounds: сколько раз за период, какой процент парка, при каких условиях остановиться и позвать человека.</p>
<p><strong>Постмортем без обучения.</strong> Разбор сделан, документ есть, action items записаны — runbook не обновлён, политики не изменены, lessons learned остались на странице в Confluence, которую с тех пор никто не открывал. Петля «инцидент → разбор → изменение правил» обрывается на втором шаге.</p>
<p><strong>За шумом никто не следит.</strong> Flap rate растёт, доверие к алертам падает, инженеры выключают нотификации канала, P1 пропускается на фоне P3-шума. Шум — управляемая метрика. Брошенный без присмотра, он деградирует наблюдаемость тише и сильнее, чем кажется.</p>
<p><strong>Про деньги молчат.</strong> Надёжность обсуждают только инженеры, бизнес решает вслепую. Любое сокращение бюджета в таком раскладе первым делом бьёт по надёжности: её ценность просто не переведена в те же единицы, что и всё остальное.</p>
<p>Любое из этих узких мест разрывает петлю обратной связи и возвращает DIS к набору практик.</p>
<h2 id="итого">Итого</h2>
<p>DIS — это про связность и дисциплину. Архитектура режет blast radius на домены отказа, SLO превращает обещания в измеримый контракт. Нагрузка и хаос по регламенту проверяют гипотезы надёжности раньше, чем это за тебя сделает продакшен. Автоматика работает под kill-switch и bounds. У онколла есть лимит на шум алертов и право остановить поставку. А экономика в конце считает, во сколько всё это обошлось. По отдельности ни один элемент иммунитета не даёт: он появляется там, где они замкнулись в петлю обучения.</p>
<p>Про российский контекст и <strong>кибериммунитет</strong> я уже писал в <a href="/posts/digital-immune-system/#%d1%81%d0%b2%d1%8f%d0%b7%d1%8c-%d1%81-%d0%b4%d1%80%d1%83%d0%b3%d0%b8%d0%bc%d0%b8-%d0%ba%d0%be%d0%bd%d1%86%d0%b5%d0%bf%d1%86%d0%b8%d1%8f%d0%bc%d0%b8">февральской заметке</a>. Здесь добавлю только одно: DIS дополняет этот подход петлёй обучения через инциденты и явной экономикой надёжности.</p>
<p>Порядок, в котором эти элементы вообще имеет смысл строить, — отдельный разговор: пока нет мониторинга, обсуждать автоматическое восстановление рано. Я развернул его в <a href="https://jtprogru.github.io/The-Way-of-SRE/reliability-hierarchy/" rel="noopener noreferrer" target="_blank">иерархию надёжности</a>, а проверку контролов безопасности тем же методом хаос-экспериментов — в <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/security-chaos-engineering/" rel="noopener noreferrer" target="_blank">Security Chaos Engineering</a>.</p>
<p>Хочешь понять, где стоишь — пройдись по пяти маркерам и посчитай, сколько выполняется на самом деле. Картина получится честнее, чем от аудита инструментов или примерки чужих лестниц из предыдущей секции: инструменты и уровни идут следом за связностью, а не наоборот.</p>
<p>PS: эта статья — то, что я не разобрал в <a href="/posts/digital-immune-system/">февральской заметке про DIS</a>. Если первая отвечала на «что это такое и из чего состоит», эта — на «что отличает зрелый DIS от витрины из шести квадратов».</p>
<h2 id="публичные-кейсы">Публичные кейсы</h2>
<p>Подборка постмортемов, инженерных описаний и моделей зрелости, на которых строится бо́льшая часть тезисов выше. Полезна как карта дальнейшего чтения.</p>
<p><strong>Право на отказ и организованная деградация:</strong></p>
<ul>
<li><a href="https://github.blog/news-insights/company-news/oct21-post-incident-analysis/" rel="noopener noreferrer" target="_blank">GitHub, 21.10.2018</a> — сознательная пауза webhooks и Pages ради целостности БД.</li>
<li><a href="https://blog.cloudflare.com/always-online-v2/" rel="noopener noreferrer" target="_blank">Cloudflare Always Online</a> и <a href="https://blog.cloudflare.com/cloudflares-always-online-and-the-internet-archive-team-up-to-fight-origin-errors/" rel="noopener noreferrer" target="_blank">интеграция с Internet Archive</a> — деградация как продуктовый контракт.</li>
<li><a href="https://github.com/Netflix/Hystrix/wiki/How-it-Works" rel="noopener noreferrer" target="_blank">Netflix Hystrix</a> и <a href="https://netflixtechblog.com/system-architectures-for-personalization-and-recommendation-e081aa94b5d8" rel="noopener noreferrer" target="_blank">архитектура персонализации</a> — fallback на precomputed-результат, встроенный в пайплайн.</li>
<li><a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/" rel="noopener noreferrer" target="_blank">Cloudflare, 2.07.2019</a> — global WAF terminate как заранее построенный kill-switch уровня домена.</li>
<li><a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer" target="_blank">Cloudflare, 18.11.2025</a> — bypass на prior-version proxy для Workers KV и Access как реактивная деградация.</li>
</ul>
<p><strong>Граница безопасной автоматики:</strong></p>
<ul>
<li><a href="https://www.sec.gov/files/litigation/admin/2013/34-70694.pdf" rel="noopener noreferrer" target="_blank">Knight Capital, 1.08.2012</a> — переиспользованный флаг + неполный деплой + автоматика без rate-cap, $460M+ за ~45 минут.</li>
<li><a href="https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/" rel="noopener noreferrer" target="_blank">Facebook BGP, 4.10.2021</a> — баг audit-инструмента + автоснятие DNS-анонсов = control plane выпиливает сам себя.</li>
<li><a href="https://aws.amazon.com/message/41926/" rel="noopener noreferrer" target="_blank">AWS S3, 28.02.2017</a> — playbook-инструмент без safety-check на минимальную ёмкость масштабировал опечатку оператора.</li>
<li><a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/" rel="noopener noreferrer" target="_blank">Cloudflare WAF, 2.07.2019</a> — regex-правило раскатилось глобально без canary, потому что SOP для конфигов отличался от SOP для бинарей.</li>
<li><a href="https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/" rel="noopener noreferrer" target="_blank">GitLab, 31.01.2017</a> — оператор стёр primary; параллельно <a href="https://about.gitlab.com/blog/2017/02/01/gitlab-dot-com-database-incident/" rel="noopener noreferrer" target="_blank">все пять механизмов бэкапа</a> тихо не работали (silent failure).</li>
<li><a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer" target="_blank">Cloudflare, 18.11.2025</a> — изменение прав ClickHouse → дубль строк → превышен hard limit → <code>Result::unwrap()</code> panic в Rust-проксе → автогенерация плохого файла каждые 5 минут.</li>
<li><a href="https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage" rel="noopener noreferrer" target="_blank">Discord, 25.03.2026</a> — при вертикальном K8s-rescale termination grace period истёк раньше, чем начался handoff сессий; волна автоматических reconnect&rsquo;ов добавила thundering herd.</li>
<li><a href="https://habr.com/ru/companies/yandex_cloud_and_infra/articles/1004584/" rel="noopener noreferrer" target="_blank">Yandex Infrastructure, 10.03.2026</a> — не постмортем, а инженерное описание: админский контур, break-glass мимо IDM и SSH с нулём онлайновых зависимостей как заранее построенный ответ на урок Facebook.</li>
</ul>
<p><strong>Обратная сторона — где автоматика спасла:</strong></p>
<ul>
<li><a href="https://status.cloud.google.com/incident/cloud-networking/19009" rel="noopener noreferrer" target="_blank">Google Cloud Networking, 2.06.2019</a> — первый пункт follow-up в постмортеме — «immediately halted the datacenter automation software».</li>
<li><a href="https://github.com/Netflix/chaosmonkey" rel="noopener noreferrer" target="_blank">Netflix Chaos Monkey</a> и <a href="https://netflixtechblog.com/the-netflix-simian-army-16e57fbab116" rel="noopener noreferrer" target="_blank">Simian Army</a> — автоматика как тренажёр: регулярная поломка прода посреди рабочего дня приучила архитектуру к фоллбекам.</li>
</ul>
<p><strong>Модели зрелости и свежие публикации (2024–2026):</strong></p>
<ul>
<li><a href="https://learn.microsoft.com/en-us/azure/well-architected/reliability/maturity-model" rel="noopener noreferrer" target="_blank">Azure Well-Architected Reliability Maturity Model</a> — пять уровней от Get resilient до Stay resilient; ближайший на рынке аналог «лестницы зрелости DIS».</li>
<li><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/resilience-lifecycle-framework/introduction.html" rel="noopener noreferrer" target="_blank">AWS Resilience Lifecycle Framework</a> — пять стадий циклом, а не лестницей; финальная стадия Respond and Learn возвращает к пересмотру целей.</li>
<li><a href="https://maturitymodel.cncf.io/" rel="noopener noreferrer" target="_blank">CNCF Cloud Native Maturity Model</a> — chaos engineering на четвёртом уровне, автоматическое восстановление на пятом.</li>
<li><a href="https://www.usenix.org/publications/loginonline/evolution-sre-google" rel="noopener noreferrer" target="_blank">The Evolution of SRE at Google</a> (журнал <a href="https://www.usenix.org/publications/loginonline" rel="noopener noreferrer" target="_blank">;login:</a> от USENIX) и <a href="https://sre.google/resources/practices-and-processes/stpa/" rel="noopener noreferrer" target="_blank">STPA на sre.google</a> — отказ от error budgets как универсального инструмента в пользу системного анализа опасностей.</li>
<li><a href="https://engineering.fb.com/2025/12/19/data-infrastructure/drp-metas-root-cause-analysis-platform-at-scale/" rel="noopener noreferrer" target="_blank">DrP: Meta&rsquo;s Root Cause Analysis Platform at Scale</a> и <a href="https://engineering.fb.com/2024/06/24/data-infrastructure/leveraging-ai-for-efficient-incident-response/" rel="noopener noreferrer" target="_blank">Leveraging AI for efficient incident response</a> — автоматизация диагноза вместо автоматизации действия.</li>
<li><a href="https://netflixtechblog.com/enhancing-netflix-reliability-with-service-level-prioritized-load-shedding-e735e6ce8f7d" rel="noopener noreferrer" target="_blank">Netflix: service-level prioritized load shedding</a> — деградация как штатный механизм каждого сервиса.</li>
<li><a href="https://dora.dev/" rel="noopener noreferrer" target="_blank">DORA Research</a> — отчёты 2024–2025: метрики throughput/instability и reliability как операционная характеристика.</li>
<li><a href="https://link.springer.com/chapter/10.1007/978-3-031-59465-6_6" rel="noopener noreferrer" target="_blank">Krauze, Grabis — A Conceptual Model of Digital Immune System…</a> (RCIS 2024, Springer, полный текст платный) и <a href="https://link.springer.com/chapter/10.1007/978-3-031-92471-2_13" rel="noopener noreferrer" target="_blank">продолжение 2025 года</a> — единственная академическая попытка построить именно DIS Maturity Model.</li>
<li><a href="https://onlinelibrary.wiley.com/doi/book/10.1002/9781394383788" rel="noopener noreferrer" target="_blank">Digital Immune System: Principles and Practices</a> (Wiley-Scrivener, ноябрь 2025, платная) — первая книга целиком про DIS, включая главу об экономике внедрения.</li>
<li>Gartner: <a href="https://www.gartner.com/en/articles/what-is-a-digital-immune-system-and-why-does-it-matter" rel="noopener noreferrer" target="_blank">What Is a Digital Immune System and Why Does It Matter</a>, <a href="https://www.gartner.com/en/information-technology/glossary/digital-immune-system" rel="noopener noreferrer" target="_blank">глоссарий</a>, <a href="https://www.gartner.com/en/newsroom/press-releases/2024-08-21-gartner-2024-hype-cycle-for-emerging-technologies-highlights-developer-productivity-total-experience-ai-and-security" rel="noopener noreferrer" target="_blank">Hype Cycle for Emerging Technologies 2024</a> — первоисточники; часть страниц Gartner прячет за регистрацию.</li>
<li><a href="https://www.infosys.com/iki/techcompass/digital-immune-system.html" rel="noopener noreferrer" target="_blank">Infosys TechCompass: Digital Immune System</a> — редкий пример живого использования термина у сервисных компаний.</li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Meta Platforms Inc. (владелец Facebook и Instagram) признана экстремистской организацией, её деятельность запрещена на территории РФ. Упоминается здесь исключительно в инженерном контексте.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
</div>]]></content:encoded>
    </item><item>
      <title>Что происходит, когда ты открываешь сайт</title>
      <link>https://jtprog.ru/posts/what-happens-when-you-open-website/</link>
      <pubDate>Mon, 20 Jul 2026 12:00:00 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/what-happens-when-you-open-website/</guid>
      <category>Networking</category>
      <category>Linux</category>
      <category>Dns</category>
      <category>Ssl</category>
      <category>Basics</category>
      <category domain="category">Basics</category>
      <enclosure url="https://jtprog.ru/cover.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Ты вводишь адрес, жмёшь Enter — и через мгновение видишь страницу. Выглядит как одно действие. Под капотом же — <a href="https://aws.amazon.com/blogs/mobile/what-happens-when-you-type-a-url-into-your-browser/" rel="noopener noreferrer" target="_blank">каскад процессов</a>, прошивающий все уровни системы: от физических прерываний процессора до алгоритмов рендеринга браузера. Разберём <a href="https://habr.com/ru/companies/gnivc/articles/861432/" rel="noopener noreferrer" target="_blank">весь путь</a> — локальный и глобальный — от нажатия клавиши до отрисовки последнего пикселя на экране.</p>
<p>Спойлер: этот вопрос не зря любят на собеседованиях — по тому, где кандидат останавливается, видно всю его карту знаний. Про то, что там спрашивают ещё, я писал <a href="/posts/after-50-interview/">после полусотни собесов</a>. И полезен он не ради галочки: когда знаешь, из каких этажей состоит путь, «сайт тормозит» перестаёт быть магией и превращается в конкретный этаж, на котором надо копать.</p>
<h2 id="локальный-компьютер-до-первого-пакета">Локальный компьютер: до первого пакета</h2>
<h3 id="ввод-url-и-обработка-клавиатурных-событий">Ввод URL и обработка клавиатурных событий</h3>
<p>Всё начинается с физического нажатия клавиши. Каждое нажатие генерирует <strong>hardware interrupt</strong> (аппаратное прерывание), которое передаётся процессору. Клавиатура, как периферийное устройство, отправляет сигнал контроллеру прерываний, тот приостанавливает текущее выполнение процессора и вызывает <strong>interrupt service routine (ISR)</strong> — специальную процедуру обработки прерывания.</p>
<p>Вход в обработчик прерывания — это переключение в kernel mode и сохранение части состояния прерванного контекста (регистры, program counter, stack pointer). Но само по себе это ещё не <a href="https://www.lenovo.com/us/en/glossary/context-switch/" rel="noopener noreferrer" target="_blank"><strong>context switch</strong></a> в смысле планировщика: процессор часто возвращается в тот же поток, а переключение на другой процесс — отдельное событие, которое может и не произойти.</p>
<p>Дальше — важная тонкость, на которой легко ошибиться. Ввод в адресную строку обрабатывает <strong>UI-поток самого браузера (browser process)</strong>, а не JavaScript открытой веб-страницы: renderer-процесс страницы <code>keydown</code> из омнибокса вообще не видит. Браузер сам решает, что ты ввёл — URL или поисковый запрос, — и инициирует навигацию. (Те самые события <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent" rel="noopener noreferrer" target="_blank"><code>keydown</code></a>, JavaScript-<a href="https://javascript.plainenglish.io/understand-javascripts-event-loop-36c021f850f7" rel="noopener noreferrer" target="_blank"><strong>event loop</strong></a> и синхронизация с <strong>rendering pipeline</strong> — это уже про обработку ввода внутри самой страницы, а не про адресную строку.)</p>
<p>После ввода полного URL и нажатия Enter браузер парсит адрес, разделяя его на компоненты: протокол (<code>https://</code>), доменное имя (<code>example.com</code>), путь (<code>/page</code>) и query-параметры. По умолчанию браузеры используют HTTPS (порт 443).</p>
<h3 id="проверка-кэшей-браузера-до-сетевого-запроса">Проверка кэшей браузера до сетевого запроса</h3>
<p>Прежде чем инициировать DNS resolution, браузер проходит цепочку локальных кэшей — и во многих случаях сетевой запрос вообще не понадобится. Порядок такой: сначала <strong>HTTP-кэш браузера</strong> (memory cache для текущей сессии и disk cache между запусками) — если ресурс ещё свежий по <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching" rel="noopener noreferrer" target="_blank"><code>Cache-Control</code>/<code>Expires</code></a>, он отдаётся со статусом <code>from memory cache</code> / <code>from disk cache</code>, без единого пакета в сеть. Если ресурс формально устарел, но у него есть валидатор (<code>ETag</code> или <code>Last-Modified</code>), браузер отправляет <a href="https://datatracker.ietf.org/doc/html/rfc9111" rel="noopener noreferrer" target="_blank"><strong>conditional request</strong></a> (<code>If-None-Match</code> / <code>If-Modified-Since</code>) и может получить <code>304 Not Modified</code> — заголовки приходят, тело не передаётся.</p>
<p>Выше HTTP-кэша стоит <strong>Service Worker</strong>: если на origin зарегистрирован SW, он перехватывает <code>fetch</code> и способен отдать ответ из <strong>Cache Storage</strong> полностью офлайн, вообще минуя сеть. Отдельно существует <strong>bfcache (back/forward cache)</strong> — снимок полностью живой страницы (с DOM и состоянием JS), который позволяет мгновенно вернуться по кнопкам «назад/вперёд» без повторного парсинга и рендеринга.</p>
<p>Только промах во всей этой цепочке приводит к реальному сетевому пути, описанному ниже. Именно поэтому повторное открытие сайта визуально мгновенно: львиная доля ресурсов не доходит даже до стадии DNS.</p>
<h3 id="dns-resolution-преобразование-имени-в-ip-адрес">DNS Resolution: преобразование имени в IP-адрес</h3>
<p>Прежде чем браузер сможет установить соединение с сервером, ему надо преобразовать человекочитаемое доменное имя в IP-адрес — процесс, называемый <a href="https://cycle.io/learn/dns-resolution-process" rel="noopener noreferrer" target="_blank"><strong>DNS resolution</strong></a>. Процесс многоступенчатый, с несколькими уровнями кэширования.</p>
<p>Сначала браузер проверяет свой <strong>локальный DNS-кэш</strong>. Если записи нет, запрос уходит операционной системе, которая проверяет системный кэш (на современных дистрибутивах за это отвечает <code>systemd-resolved</code> — про его настройку у меня есть <a href="/posts/systemd-resolved/">отдельный пост</a>). При промахе в обоих кэшах запрос отправляется к <strong>DNS resolver</strong> — обычно это DNS-сервер провайдера или публичный DNS (Google 8.8.8.8, Cloudflare 1.1.1.1).</p>
<p>Resolver выполняет <strong>рекурсивный запрос</strong> от имени клиента. Если у него нет кэшированной записи, он обращается к <a href="https://www.geeksforgeeks.org/computer-networks/address-resolution-in-dns-domain-name-server/" rel="noopener noreferrer" target="_blank"><strong>Root DNS-серверам</strong></a> — 13 логических серверов (<code>a</code>–<code>m.root-servers.net</code>), каждый из которых на деле Anycast-кластер из сотен нод по всему миру. Root DNS не знает конкретного IP-адреса, но направляет запрос к <strong>TLD-серверам</strong>, отвечающим за зоны верхнего уровня (<code>.com</code>, <code>.org</code>, <code>.ru</code>).</p>
<p>TLD-сервер направляет запрос к <strong>Authoritative DNS-серверу</strong> конкретного домена, который и возвращает финальный IP. Весь процесс обычно занимает от 20 до 120 миллисекунд в зависимости от удалённости серверов и состояния кэшей.</p>
<p>Только это не строго линейная цепочка «один запрос → один IP → connect». Браузер обычно запрашивает записи <code>A</code> (IPv4) и <code>AAAA</code> (IPv6) параллельно, может учитывать <code>HTTPS</code>/<code>SVCB</code>-записи и ходить через DoH/DoT, а в ответ приходит <strong>несколько адресов</strong>. Дальше в дело вступает <a href="https://datatracker.ietf.org/doc/html/rfc8305" rel="noopener noreferrer" target="_blank"><strong>Happy Eyeballs</strong></a>: клиент запускает конкурентные попытки подключения по IPv6 и IPv4 со слегка разнесённым стартом, чтобы одна неработающая ветка не тормозила навигацию, — а иногда новое соединение вообще не нужно, потому что переиспользуется уже открытое. То есть реальная картина ближе к графу конкурентных операций, чем к последовательному списку.</p>
<p><a href="https://dev.to/wallacefreitas/understanding-domain-name-system-dns-recursive-iterative-queries-with-root-level-domains-581n" rel="noopener noreferrer" target="_blank">Рекурсивный и итеративный запросы</a> — не одно и то же: при рекурсивном resolver берёт на себя всю работу по обходу иерархии, при итеративном клиент сам последовательно обращается к каждому уровню.</p>
<h3 id="cdn-anycast-и-edge-куда-на-самом-деле-резолвится-имя">CDN, Anycast и edge: куда на самом деле резолвится имя</h3>
<p>Описанная выше цепочка для крупного сайта почти никогда не заканчивается на origin-сервере. Authoritative DNS для домена вроде <code>cnn.com</code> обычно делегирован <strong>CDN</strong> (Cloudflare, Akamai, Fastly, CloudFront), и он возвращает не фиксированный IP конкретного дата-центра, а адрес ближайшей <strong>edge-точки</strong>.</p>
<p>Работают два механизма приближения:</p>
<ul>
<li><strong>GeoDNS / EDNS Client Subnet</strong> — authoritative-сервер смотрит на подсеть резолвера (или клиента) и отдаёт разный ответ в зависимости от географии.</li>
<li><a href="https://www.cloudflare.com/learning/cdn/glossary/anycast-network/" rel="noopener noreferrer" target="_blank"><strong>Anycast</strong></a> — один и тот же IP-адрес анонсируется по BGP из десятков локаций мира, так что пакет естественным образом доходит до топологически ближайшей ноды.</li>
</ul>
<p>В результате TCP- и TLS-handshake происходят не с origin в другом полушарии, а с edge-нодой в десятках километров: <strong>RTT</strong> падает с ~100–150 мс до единиц миллисекунд. А это ключевой множитель для TCP slow start и числа TLS round-trips. На edge отдаётся закэшированный статический контент, а к origin запрос уходит только при cache miss или за динамикой — часто по уже прогретому keep-alive соединению. Попутно CDN обычно терминирует TLS на edge и первым в цепочке поддерживает свежие протоколы (HTTP/3, TLS 1.3), даже если origin о них не знает.</p>
<h3 id="установление-tcp-соединения-трёхстороннее-рукопожатие">Установление TCP-соединения: трёхстороннее рукопожатие</h3>
<p>Получив IP-адрес, браузер готов установить транспортное соединение. Для HTTP/1.1 и HTTP/2 используется <strong>TCP</strong> — надёжный протокол с установлением соединения (HTTP/3 работает иначе, поверх QUIC/UDP — о нём в разделе про версии протокола), а само установление TCP идёт через <a href="https://networkwalks.com/tcp-3-way-handshake-process/" rel="noopener noreferrer" target="_blank"><strong>three-way handshake</strong></a>.</p>
<p>Клиент отправляет <a href="https://www.geeksforgeeks.org/computer-networks/tcp-3-way-handshake-process/" rel="noopener noreferrer" target="_blank"><strong>SYN-пакет</strong></a> с начальным sequence number — случайным числом для синхронизации и защиты от подделки. Сервер отвечает <strong>SYN-ACK</strong>, который одновременно подтверждает SYN клиента (увеличивая sequence number на 1) и несёт собственный SYN со своим sequence number. Клиент завершает рукопожатие <strong>ACK-пакетом</strong>.</p>
<p>В процессе ОС выполняет <a href="https://www.gta.ufrj.br/ensino/eel878/sockets/syscalls.html" rel="noopener noreferrer" target="_blank">системные вызовы</a> <code>socket()</code> и <code>connect()</code> для создания сокета и инициации соединения. Сокет — это конечная точка двунаправленной связи между процессами. Клиент использует эфемерный порт (IANA-диапазон 49152–65535; Linux по умолчанию берёт из 32768–60999, см. <code>net.ipv4.ip_local_port_range</code>), а сервер слушает на стандартном порту (80 для HTTP, 443 для HTTPS). Кто хочет увидеть, как эфемерные порты ломают жизнь на практике, — почитай про <a href="/posts/ftp-passive-vs-active/">активный и пассивный режимы FTP</a>.</p>
<h3 id="управление-потоком-и-перегрузкой-mss-mtu-congestion-control">Управление потоком и перегрузкой: MSS, MTU, congestion control</h3>
<p>Three-way handshake — это не только синхронизация sequence numbers. В SYN-пакетах стороны согласуют опции, критичные для производительности:</p>
<ul>
<li><strong>MSS (Maximum Segment Size)</strong> — максимальный размер полезной нагрузки TCP-сегмента, выводимый из <strong>MTU</strong> канала (обычно 1500 байт для Ethernet → MSS ≈ 1460).</li>
<li><strong>Window Scale</strong> — множитель, позволяющий окну превышать 64 КБ.</li>
<li><strong>SACK (Selective Acknowledgment)</strong> и timestamps.</li>
</ul>
<p>Если по пути встречается канал с меньшим MTU, срабатывает фрагментация или (при выставленном бите DF) <a href="https://datatracker.ietf.org/doc/html/rfc1191" rel="noopener noreferrer" target="_blank"><strong>Path MTU Discovery</strong></a> через ICMP. А вот если ICMP заботливо прибит на файрволе — получаешь «PMTU black hole»: соединение устанавливается, мелкие пакеты ходят, а крупные молча теряются и передача виснет. Классика жанра, на которую убивают часы, прежде чем догадаться посмотреть на MTU.</p>
<p>После установки соединения объём данных «в полёте» ограничен сразу двумя окнами: <strong>receive window</strong> (flow control — сколько готов принять получатель) и <strong>congestion window / cwnd</strong> (congestion control — сколько сеть готова пропустить без потерь). TCP стартует с малого cwnd (<strong>initial window</strong>, обычно 10 сегментов) и в фазе <a href="https://datatracker.ietf.org/doc/html/rfc5681" rel="noopener noreferrer" target="_blank"><strong>slow start</strong></a> наращивает его экспоненциально каждый RTT, затем переходит в линейный рост (<strong>congestion avoidance</strong>); при потере (таймаут или три dup ACK) окно резко сокращается.</p>
<p>Именно slow start объясняет ценность первых ~14 КБ ответа (см. видео <a href="https://youtu.be/fmiqceS8ZNg" rel="noopener noreferrer" target="_blank">«Почему ваш сайт должен весить 14 КБ»</a>) — они умещаются в initial window и приходят за один round-trip, тогда как всё сверх того требует новых RTT на «раскачку». Современные стеки используют <strong>CUBIC</strong> (дефолт Linux, реагирует на потери) и <a href="https://queue.acm.org/detail.cfm?id=3022184" rel="noopener noreferrer" target="_blank"><strong>BBR</strong></a> (моделирует пропускную способность и RTT, а не ждёт потерь).</p>
<h3 id="tls-handshake-установление-защищённого-канала">TLS Handshake: установление защищённого канала</h3>
<p>Для HTTPS после установления TCP следует ещё один этап — <a href="https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/" rel="noopener noreferrer" target="_blank"><strong>TLS handshake</strong></a>. Он устанавливает зашифрованный канал и подтверждает, что сервер — тот, за кого себя выдаёт.</p>
<p>Клиент отправляет <strong>ClientHello</strong>: поддерживаемые версии (<code>supported_versions</code>, обычно <a href="https://datatracker.ietf.org/doc/html/rfc8446" rel="noopener noreferrer" target="_blank">TLS 1.3</a>), список <a href="https://en.wikipedia.org/wiki/Cipher_suite" rel="noopener noreferrer" target="_blank"><strong>cipher suites</strong></a>, 32 байта случайных данных и расширения — SNI (Server Name Indication), ALPN, <code>signature_algorithms</code> и, что принципиально для 1.3, <code>key_share</code> с уже сгенерированным эфемерным (EC)DHE-ключом. Cipher suites в TLS 1.3 используют только AEAD-алгоритмы вроде <a href="https://ciphersuite.info/cs/TLS_AES_256_GCM_SHA384/" rel="noopener noreferrer" target="_blank"><code>TLS_AES_256_GCM_SHA384</code></a> — и, в отличие от TLS 1.2, в само имя сьюта уже не входят алгоритмы обмена ключами и аутентификации (<code>ECDHE</code>, <code>RSA</code>), они вынесены в расширения. В обычном браузерном handshake с сертификатом ключи выводятся через эфемерный (EC)DHE — это и даёт forward secrecy (стандарт допускает ещё два режима: чистый PSK и PSK вместе с (EC)DHE, так что «всегда ECDHE» — упрощение).</p>
<p>Сервер отвечает <a href="https://www.ibm.com/docs/en/ibm-mq/9.3.x?topic=tls-overview-ssltls-handshake" rel="noopener noreferrer" target="_blank"><strong>ServerHello</strong></a> со своим <code>key_share</code> и выбранным cipher suite. С этого момента обе стороны уже могут вычислить общий (EC)DHE-секрет — и <strong>всё дальнейшее шифруется</strong>. Дальше сервер шлёт <strong>EncryptedExtensions</strong>, <strong>Certificate</strong> и <strong>CertificateVerify</strong> — подпись всего предыдущего handshake приватным ключом, доказывающую владение сертификатом. Клиент проверяет сам сертификат (публичный ключ, цепочку до доверенного Certificate Authority) и эту подпись.</p>
<p><strong>Отдельного обмена ключами в стиле «клиент шифрует секрет публичным ключом сервера» в TLS 1.3 нет.</strong> Общий (EC)DHE-секрет уже получен из двух <code>key_share</code>, и из него через <strong>HKDF</strong> выводятся все симметричные ключи. Никакого <a href="https://wiki.osdev.org/TLS_Handshake" rel="noopener noreferrer" target="_blank">pre-master secret</a> в <strong>ClientKeyExchange</strong> здесь не пересылается — это схема <strong>RSA key exchange из TLS 1.2 и более старых версий</strong>, которую 1.3 выкинул вместе с прочими небезопасными механизмами.</p>
<p>Финальный этап — обе стороны обмениваются сообщениями <strong>Finished</strong>, подтверждая целостность handshake. Отдельный <strong>ChangeCipherSpec</strong> в TLS 1.3 из настоящего handshake <a href="https://www.encryptionconsulting.com/tls-1-2-and-tls-1-3/" rel="noopener noreferrer" target="_blank">исключён</a> и оставлен лишь как пустой совместимый сигнал для старых middlebox. Полный handshake TLS 1.3 укладывается в <strong>один RTT</strong> (против двух в 1.2). С заранее полученным PSK возможен <strong>0-RTT</strong>: клиент отправляет ранние данные ещё до завершения нового handshake — но у них слабее гарантии безопасности и их можно воспроизвести повторно (replay), поэтому сервер не должен принимать через 0-RTT необратимые операции. То есть 0-RTT — это resumption с ранними данными, а не «handshake за ноль RTT».</p>
<h3 id="формирование-и-отправка-http-запроса">Формирование и отправка HTTP-запроса</h3>
<p>После установления защищённого соединения браузер готов отправить HTTP-запрос. <a href="https://www.linode.com/docs/guides/http-get-request/" rel="noopener noreferrer" target="_blank">Структура запроса</a> состоит из <strong>request line</strong> (метод, путь, версия HTTP), <strong>заголовков</strong> и опционального <strong>тела</strong>.</p>
<p>Request line для главной страницы обычно выглядит как <code>GET / HTTP/1.1</code>. За ним следуют <a href="https://beeceptor.com/docs/concepts/http-headers/" rel="noopener noreferrer" target="_blank">заголовки</a>, каждый на отдельной строке: <code>Host: example.com</code> (обязательный), <code>User-Agent: Mozilla/5.0...</code>, <code>Accept: text/html,application/xhtml+xml...</code>, <code>Accept-Language: ru,en</code>, <code>Accept-Encoding: gzip, deflate, br</code>, <code>Connection: keep-alive</code>, <code>Cache-Control: max-age=0</code>.</p>
<p>Сформированный запрос передаётся через системный вызов <code>send()</code> или <code>write()</code>, который копирует данные из user space в kernel space в <a href="https://www.linuxjournal.com/article/6345" rel="noopener noreferrer" target="_blank">socket buffer</a>. Дальше ядро обрабатывает их через network stack, готовя к передаче по сети.</p>
<h3 id="httphttps-редиректы-и-hsts">HTTP→HTTPS, редиректы и HSTS</h3>
<p>Реальный первый запрос редко сразу попадает в целевой ресурс. Если пользователь ввёл голый домен или <code>http</code>-URL, срабатывает несколько механизмов.</p>
<p>Первый — <a href="https://datatracker.ietf.org/doc/html/rfc6797" rel="noopener noreferrer" target="_blank"><strong>HSTS (HTTP Strict Transport Security)</strong></a>: если домен есть в HSTS <strong>preload list</strong>, вшитом в браузер, или ранее прислал заголовок <code>Strict-Transport-Security</code>, браузер ещё до выхода в сеть сам заменяет <code>http://</code> на <code>https://</code> (internal redirect 307), исключая небезопасный первый хоп и возможность SSL-stripping.</p>
<p>Второй — <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections" rel="noopener noreferrer" target="_blank"><strong>серверные редиректы</strong></a>: сервер отвечает статусами <code>301</code>/<code>302</code>/<code>307</code>/<code>308</code> с заголовком <code>Location</code>, и браузер повторяет путь для нового URL — иногда с новым DNS/TCP/TLS, если сменился хост.</p>
<p>Типичная цепочка выглядит как <code>http://cnn.com</code> → <code>https://cnn.com</code> → <code>https://www.cnn.com</code>. Каждый лишний редирект — дополнительный RTT (а при смене хоста ещё и новый TLS-handshake), поэтому их число минимизируют. Различается и семантика кэширования, хотя и не так жёстко, как принято думать: <code>301</code> и <code>308</code> (permanent) браузер <strong>может эвристически закэшировать</strong> даже без явного срока — но это не значит «навсегда». А <code>302</code> и <code>307</code> (temporary) тоже кэшируются, если в ответе есть подходящие директивы вроде <code>Cache-Control: max-age=...</code>. Фактическое повторное использование редиректа определяется <a href="https://datatracker.ietf.org/doc/html/rfc9110" rel="noopener noreferrer" target="_blank">freshness, revalidation и cache-control-политикой</a>, а не только кодом статуса.</p>
<h3 id="версии-протокола-http11-http2-http3-quic">Версии протокола: HTTP/1.1, HTTP/2, HTTP/3 (QUIC)</h3>
<p>Показанный выше <code>GET / HTTP/1.1</code> — упрощение: версия прикладного протокола согласуется ещё в TLS через <strong>ALPN (Application-Layer Protocol Negotiation)</strong> внутри ClientHello.</p>
<p><a href="https://httpwg.org/specs/rfc9112.html" rel="noopener noreferrer" target="_blank"><strong>HTTP/1.1</strong></a> — текстовый протокол. Формально в нём был <strong>pipelining</strong>: клиент мог отправить несколько запросов, не дожидаясь ответов. Но ответы всё равно обязаны приходить строго по порядку, что давало <strong>head-of-line (HoL) blocking</strong>; из-за этого браузеры от широкого pipelining практически отказались. Так что «один запрос за раз» — упрощение: точнее, на одном HTTP/1.1-соединении браузеры не делают полноценного независимого мультиплексирования, поэтому параллелизм исторически набирали открытием 6+ TCP-соединений на домен.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc9113" rel="noopener noreferrer" target="_blank"><strong>HTTP/2</strong></a> — бинарный: вместо текстовой request line используются псевдозаголовки (<code>:method</code>, <code>:scheme</code>, <code>:authority</code>, <code>:path</code>, <code>:status</code>) и бинарное фреймирование. Он мультиплексирует множество <strong>streams</strong> в одном TCP-соединении, добавляет сжатие заголовков (<strong>HPACK</strong>) и server push (ныне депрекейтед); заголовка <code>Connection: keep-alive</code> и механизма <code>Transfer-Encoding: chunked</code> в нём уже нет — их роль берёт на себя фреймовый уровень. Но HTTP/2 остаётся уязвим к <strong>TCP-level HoL blocking</strong> — потеря одного пакета тормозит все streams, потому что упорядочивание идёт на уровне TCP.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc9114" rel="noopener noreferrer" target="_blank"><strong>HTTP/3</strong></a> работает поверх <a href="https://datatracker.ietf.org/doc/html/rfc9000" rel="noopener noreferrer" target="_blank"><strong>QUIC</strong></a> — транспорта на UDP вместо TCP. QUIC объединяет транспортный и криптографический handshake (TLS 1.3 встроен внутрь), устанавливая соединение за 1-RTT или даже 0-RTT, а мультиплексирование делает на своём уровне — потеря пакета в одном stream не блокирует остальные, то есть transport-level HoL blocking устранён. Дополнительно QUIC переживает смену сети через <strong>connection migration</strong> по Connection ID: соединение не рвётся при переключении Wi-Fi ↔ LTE.</p>
<p>Для сайта за CDN клиент почти наверняка говорит по HTTP/2 или HTTP/3, а не по «учебному» HTTP/1.1.</p>
<h2 id="сетевой-путь-от-пакета-к-кадру">Сетевой путь: от пакета к кадру</h2>
<h3 id="прохождение-через-сетевой-стек-инкапсуляция">Прохождение через сетевой стек: инкапсуляция</h3>
<p>Данные HTTP-запроса проходят вниз по <a href="https://blog.nicolasmesa.co/posts/2018/08/what-happens-when-you-type-a-url-in-your-browser-and-press-enter/" rel="noopener noreferrer" target="_blank">сетевому стеку</a> через <strong>инкапсуляцию</strong> — последовательное добавление заголовков на каждом уровне.</p>
<p>На <strong>транспортном уровне (L4)</strong> данные разбиваются на <strong>TCP-сегменты</strong>. К каждому добавляется TCP-заголовок: source port (эфемерный порт клиента), destination port (443 для HTTPS), sequence number, acknowledgment number, flags (ACK, PSH и др.), window size, checksum.</p>
<p>На <strong>сетевом уровне (L3)</strong> сегмент инкапсулируется в <a href="https://textbook.cs168.io/routing/ip-header.html" rel="noopener noreferrer" target="_blank"><strong>IP-пакет</strong></a>. IP-заголовок включает: version (4 для IPv4), header length, total length, identification (для сборки фрагментов), flags и fragment offset, <a href="https://networklessons.com/ip-routing/ipv4-packet-header" rel="noopener noreferrer" target="_blank"><strong>TTL (Time To Live)</strong></a> — счётчик, уменьшающийся на каждом роутере, <strong>protocol field</strong> (6 для TCP, 17 для UDP), header checksum, source IP, destination IP.</p>
<p>Каждый роутер на пути декрементирует TTL на 1, и если значение достигает 0, пакет отбрасывается, а отправителю уходит ICMP Time Exceeded. Это предотвращает routing loops — и заодно на этом механизме работает <code>traceroute</code>.</p>
<p>На <a href="https://chessman7.substack.com/p/layer-2-network-operations-how-ethernet" rel="noopener noreferrer" target="_blank"><strong>канальном уровне (L2)</strong></a> IP-пакет инкапсулируется в <a href="https://www.networkacademy.io/ccna/network-fundamentals/data-link-layer" rel="noopener noreferrer" target="_blank"><strong>Ethernet frame</strong></a>. Заголовок содержит destination MAC (6 байт), source MAC (6 байт), EtherType (2 байта, <code>0x0800</code> для IPv4, <code>0x86DD</code> для IPv6). В конце кадра добавляется FCS (Frame Check Sequence) — контрольная сумма для обнаружения ошибок передачи.</p>
<p>Оговорка на весь этот раздел: дальше описан <strong>IPv4 поверх Ethernet</strong> — один распространённый вариант, а не универсальный «низ». В IPv6 структура заголовка другая, нет header checksum, вместо TTL — <strong>Hop Limit</strong>, а маршрутизаторы IPv6 пакеты не фрагментируют (это при необходимости делает источник). Да и физически соединение клиента может идти через Wi-Fi, cellular stack, VPN-туннель, виртуальный интерфейс или вообще loopback — ARP и Ethernet-кадр тогда выглядят иначе или отсутствуют вовсе.</p>
<h3 id="arp-resolution-преобразование-ip-в-mac-адрес">ARP Resolution: преобразование IP в MAC-адрес</h3>
<p>Прежде чем отправить кадр, система должна определить MAC-адрес следующего узла в локальной сети — обычно это адрес default gateway. Для этого используется <a href="https://en.wikipedia.org/wiki/Address_Resolution_Protocol" rel="noopener noreferrer" target="_blank"><strong>ARP (Address Resolution Protocol)</strong></a>.</p>
<p>Система сначала проверяет <a href="https://blogs.oracle.com/linux/arp-internals" rel="noopener noreferrer" target="_blank"><strong>ARP cache</strong></a> — таблицу соответствий IP → MAC в оперативной памяти. Если соответствия нет, отправляется <strong>ARP Request</strong> — широковещательный пакет с destination MAC <code>FF:FF:FF:FF:FF:FF</code>, который получают все устройства в локальной сети. Смысл запроса: «Кто имеет IP 192.168.1.1? Сообщите на MAC AA:BB:CC:DD:EE:FF».</p>
<p>Устройство с этим IP отвечает <a href="https://www.fortinet.com/resources/cyberglossary/what-is-arp" rel="noopener noreferrer" target="_blank"><strong>ARP Reply</strong></a> — unicast-пакетом со своим MAC. Система обновляет ARP cache и отправляет кадр. В Linux запись держится в состоянии reachable около 30 секунд (<code>base_reachable_time</code> с джиттером), потом переходит в stale и перед следующим использованием перепроверяется.</p>
<h3 id="маршрутизация-путь-через-роутеры">Маршрутизация: путь через роутеры</h3>
<p>После определения MAC-адреса кадр передаётся сетевому адаптеру (NIC) через <a href="https://en.wikipedia.org/wiki/Direct_memory_access" rel="noopener noreferrer" target="_blank"><strong>DMA (Direct Memory Access)</strong></a> — NIC напрямую копирует данные из системной памяти без участия CPU.</p>
<p>Кадр попадает в <a href="https://stackoverflow.com/questions/47450231/what-is-the-relationship-of-dma-ring-buffer-and-tx-rx-ring-for-a-network-card" rel="noopener noreferrer" target="_blank"><strong>transmit ring buffer</strong></a> — кольцевой буфер дескрипторов пакетов в оперативной памяти, разделяемый между драйвером и NIC. NIC читает дескрипторы, получает адреса пакетов в памяти и передаёт данные на физический уровень.</p>
<p>Пакет достигает первого роутера (обычно домашнего шлюза), который выполняет <strong>de-encapsulation и re-encapsulation</strong>: снимает Ethernet-заголовок, анализирует IP-заголовок, проверяет <a href="https://en.wikipedia.org/wiki/Routing_table" rel="noopener noreferrer" target="_blank"><strong>routing table</strong></a> для определения next hop, декрементирует TTL, пересчитывает IP checksum и создаёт новый Ethernet-кадр с новыми MAC-адресами.</p>
<p>Процесс повторяется на каждом роутере. Ключевое: <strong>IP-пакет (L3) остаётся неизменным от источника до получателя</strong> (за исключением TTL и checksum), тогда как <strong>Ethernet-кадр (L2) пересоздаётся на каждом hop</strong>. IP-адреса определяют конечную точку назначения, MAC-адреса используются только для доставки кадра на следующий hop.</p>
<p>Routing table содержит записи в формате: network prefix, netmask, next hop, interface. При получении пакета роутер выполняет <a href="https://jumpcloud.com/it-index/what-is-a-forwarding-table" rel="noopener noreferrer" target="_blank"><strong>longest prefix match</strong></a> — находит наиболее специфичное правило, соответствующее destination IP. Если соответствия нет, пакет отправляется через default route (<code>0.0.0.0/0</code>).</p>
<h3 id="bgp-маршрутизация-между-автономными-системами">BGP: маршрутизация между автономными системами</h3>
<p>Longest prefix match объясняет, как роутер выбирает next hop среди известных ему маршрутов, но не откуда берётся сама глобальная карта интернета. Домашний роутер знает лишь default route «всё незнакомое — провайдеру». Дальше пакет попадает в сеть ISP и движется между <strong>автономными системами (AS)</strong> — независимо управляемыми сетями (провайдеры, дата-центры, CDN), каждой из которых присвоен номер <strong>ASN</strong>. Маршруты между AS распространяет <a href="https://datatracker.ietf.org/doc/html/rfc4271" rel="noopener noreferrer" target="_blank"><strong>BGP (Border Gateway Protocol)</strong></a> — протокол, на котором держится весь междоменный роутинг.</p>
<p>BGP-роутеры на границах AS обмениваются анонсами достижимости префиксов (например, «сеть <code>151.101.0.0/16</code> доступна через <code>AS54113</code>») и выбирают путь не по кратчайшей метрике, а по <strong>политикам</strong>: длина AS-path, local preference, коммерческие отношения (peering vs transit). BGP-анонсы и делают возможным Anycast из раздела про CDN: один префикс анонсируется из многих точек, и каждый узел выбирает ближайшую по своим политикам.</p>
<p>Уязвимость модели — доверие к анонсам: ошибочный или злонамеренный анонс чужого префикса (<strong>BGP hijack</strong>) уводит трафик не туда. Классика жанра — 2008 год, когда Pakistan Telecom, пытаясь заблокировать YouTube внутри страны, анонсировал его префикс на весь мир и уронил сервис глобально. Частичная защита — <strong>RPKI</strong> и фильтрация маршрутов. Внутри одной AS работают уже IGP-протоколы (<strong>OSPF</strong>, <strong>IS-IS</strong>), а BGP отвечает именно за стык между AS.</p>
<h2 id="сервер-от-прерывания-до-приложения">Сервер: от прерывания до приложения</h2>
<h3 id="приём-пакета-hardware-interrupt-и-dma">Приём пакета: hardware interrupt и DMA</h3>
<p>Когда пакет достигает NIC сервера, начинается <a href="https://metebalci.com/blog/how-a-nic-rx-works/" rel="noopener noreferrer" target="_blank">обратный процесс</a>. NIC получает электрические сигналы, декодирует их в биты, проверяет FCS и сравнивает destination MAC с собственным адресом.</p>
<p>При успешной проверке NIC через <strong>DMA копирует пакет</strong> из своей внутренней памяти в системную RAM, помещая его в <a href="https://maxnilz.com/docs/004-network/005-linux-rx/" rel="noopener noreferrer" target="_blank"><strong>receive ring buffer</strong></a> — кольцевой массив дескрипторов в kernel space, предварительно выделенный драйвером.</p>
<p>После завершения DMA-передачи NIC генерирует <a href="https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-receiving-data/" rel="noopener noreferrer" target="_blank"><strong>hardware interrupt (IRQ)</strong></a>, сигнализируя CPU, что пакет готов к обработке. Прерывания сетевых карт бывают трёх типов: <strong>MSI-X</strong>, <strong>MSI</strong> и <strong>legacy interrupts</strong>. MSI-X предпочтительнее — позволяет назначить отдельное прерывание каждой receive queue, что улучшает параллелизм на многоядерных системах.</p>
<h3 id="обработка-прерывания-isr-и-переключение-в-ядро">Обработка прерывания: ISR и переключение в ядро</h3>
<p>При получении hardware interrupt процессор переключается в <strong>kernel mode</strong> и сохраняет состояние прерванного контекста — это смена уровня привилегий и сохранение регистров, а не обязательно полноценный context switch планировщика (тот меняет процесс и происходит отдельно). Процессоры поддерживают несколько <a href="https://www.geeksforgeeks.org/operating-systems/difference-between-user-mode-and-kernel-mode/" rel="noopener noreferrer" target="_blank"><strong>privilege levels</strong></a>: <strong>Ring 0 (kernel mode)</strong> — полный доступ ко всем системным ресурсам, и <strong>Ring 3 (user mode)</strong> — доступ только к адресному пространству процесса.</p>
<p>При входе в ядро CPU сохраняет текущее состояние (program counter, stack pointer, регистры, flags). Затем загружается адрес <strong>interrupt handler (ISR)</strong> из interrupt vector table и начинается его выполнение.</p>
<p>Для сетевых карт ISR выполняет минимум работы: подтверждает прерывание, <strong>отключает дальнейшие прерывания от NIC</strong> и планирует обработку пакета на уровне <strong>softirq</strong>. Зачем отключать: если бы NIC продолжал генерировать hardware interrupt на каждый пакет при высоком трафике, система тратила бы всё CPU-время на обработку прерываний, не успевая фактически обрабатывать пакеты — явление, известное как <a href="https://lwn.net/Articles/1008399/" rel="noopener noreferrer" target="_blank"><strong>interrupt storm</strong> или <strong>livelock</strong></a>.</p>
<h3 id="napi-и-softirq-эффективная-batch-обработка">NAPI и SoftIRQ: эффективная batch-обработка</h3>
<p>Для решения проблемы interrupt overload в Linux используется механизм <a href="https://docs.kernel.org/networking/napi.html" rel="noopener noreferrer" target="_blank"><strong>NAPI (New API)</strong></a>. NAPI комбинирует <a href="https://wiki.linuxfoundation.org/networking/napi" rel="noopener noreferrer" target="_blank">interrupt-driven подход с polling</a>: первый пакет генерирует прерывание, после чего драйвер переключается в режим polling для batch-обработки последующих.</p>
<p>ISR вызывает <code>napi_schedule()</code>, которая добавляет NAPI-структуру драйвера в список запланированных softirq и поднимает флаг <strong>NET_RX_SOFTIRQ</strong>. <a href="https://mirror.xyz/fanyadan.eth/OVMiK-NGKk_nYgX5AY75TcGJDZozfL2mYBdcslTnyi8" rel="noopener noreferrer" target="_blank">Softirq</a> — механизм отложенной обработки в ядре, выполняющийся с приоритетом ниже hardware interrupts, но выше обычных процессов.</p>
<p><strong>Softirq processing</strong> происходит в контексте ядра сразу после обработки hardware interrupts или при выходе из системного вызова. Softirq может выполняться <strong>параллельно на нескольких CPU</strong> — на этом держится масштабирование сетевой обработки. Для каждого CPU ядро поддерживает отдельную очередь softirq.</p>
<p>При выполнении NET_RX_SOFTIRQ вызывается <code>net_rx_action()</code>, которая обходит список NAPI-структур и вызывает <code>poll()</code> каждого драйвера. Poll-функция <a href="https://blog.packagecloud.io/illustrated-guide-monitoring-tuning-linux-networking-stack-receiving-data/" rel="noopener noreferrer" target="_blank">извлекает пакеты из receive ring buffer</a> (до определённого budget — обычно 64 пакета), обрабатывает их и передаёт выше по стеку. Дальше — развилка, и её легко описать наоборот. Если драйвер разгрёб <strong>всё доступное до исчерпания budget</strong>, он вызывает <code>napi_complete_done()</code> и снова включает hardware interrupts от NIC. Если же budget исчерпан, а пакеты ещё остались, poll-функция возвращает ровно значение budget — NAPI <strong>не</strong> завершается и планируется повторно, оставаясь в режиме polling. Возвращаться к чисто interrupt-driven обработке в этот момент как раз не нужно: иначе под нагрузкой поток пакетов не разгрести.</p>
<p>Если сервер держит серьёзную нагрузку, тюнинг всего этого хозяйства (размеры очередей, backlog, TCP-параметры) делается через <code>sysctl</code> — по этому поводу есть <a href="/posts/sysctl-hl/">отдельная шпаргалка</a>.</p>
<h3 id="rpsrfs-распределение-нагрузки-по-cpu">RPS/RFS: распределение нагрузки по CPU</h3>
<p>Для масштабирования на многоядерных системах Linux использует <a href="https://docs.kernel.org/networking/scaling.html" rel="noopener noreferrer" target="_blank"><strong>RPS (Receive Packet Steering)</strong> и <strong>RFS (Receive Flow Steering)</strong></a>. RPS — программная реализация RSS (Receive Side Scaling), распределяющая обработку пакетов по разным CPU.</p>
<p>RPS вычисляет хэш от заголовков пакета (source/destination IP и port) и по нему выбирает целевой CPU из настроенного списка. Пакет помещается в <strong>per-CPU backlog queue</strong> этого CPU, и генерируется <strong>IPI (Inter-Processor Interrupt)</strong>, чтобы разбудить обработку на удалённом CPU. Так все пакеты одного потока обрабатываются одним CPU — лучше cache locality.</p>
<p>RFS идёт дальше, пытаясь направить обработку пакета на тот же CPU, где работает приложение, принимающее эти данные. Данные с большой вероятностью уже в CPU cache — latency ниже.</p>
<h3 id="прохождение-через-network-stack-от-ethernet-к-tcp">Прохождение через network stack: от Ethernet к TCP</h3>
<p>Извлечённый из ring buffer пакет представлен в ядре структурой <a href="https://thinkpalm.com/blogs/how-linux-kernel-handles-network-packets/" rel="noopener noreferrer" target="_blank"><strong>sk_buff (socket buffer)</strong></a> — универсальной структурой для сетевых пакетов в Linux. Она содержит указатели на заголовки разных уровней и на сами данные, что позволяет передавать пакет между уровнями стека без копирования.</p>
<p>На <strong>канальном уровне (L2)</strong> ядро проверяет Ethernet-заголовок: сравнивает destination MAC с адресами локальных интерфейсов, проверяет FCS, извлекает EtherType.</p>
<p>На <strong>сетевом уровне (L3)</strong> вызывается <code>ip_rcv()</code>: проверяет версию, валидирует header length и total length, проверяет IP checksum и выполняет routing decision через lookup в routing table. Дальше развилка: если пакет предназначен локальной машине, он идёт в <code>ip_local_deliver()</code> и передаётся на транспортный уровень <strong>без уменьшения TTL</strong>. TTL/Hop Limit декрементируется не потому, что сервер принял пакет для локального сокета, а только на forwarding-пути (<code>ip_forward()</code>), когда узел пересылает пакет дальше — тогда и происходит уменьшение TTL (при TTL=0 пакет отбрасывается с ICMP Time Exceeded), пересчёт checksum и re-encapsulation. Здесь легко механически перенести операцию из forwarding path в общий receive path — но на приёме «для себя» её нет.</p>
<p>Перед передачей на транспортный уровень пакет проходит через <strong>Netfilter hooks</strong> — точки перехвата для firewall-правил (iptables/nftables), NAT и connection tracking. Именно здесь пакет может тихо умереть по правилу, которое кто-то накатил полгода назад — <a href="/posts/iptables-manual/">шпаргалка по iptables</a> в помощь.</p>
<p>На <strong>транспортном уровне (L4)</strong> функция протокола (для TCP — <code>tcp_v4_rcv()</code>) проверяет TCP checksum, выполняет <strong>socket demultiplexing</strong> — поиск соответствующего сокета по 4-tuple (source IP, source port, destination IP, destination port), обновляет состояние TCP-соединения, обрабатывает TCP flags и помещает данные в <strong>socket receive buffer</strong> — очередь, из которой приложение будет читать.</p>
<p>Для TLS-соединений данные всё ещё зашифрованы на этом этапе и будут расшифрованы уже в user space приложением или TLS-библиотекой.</p>
<h3 id="load-balancer-распределение-перед-web-server">Load balancer: распределение перед web server</h3>
<p>У крупного сайта пакет, дошедший до дата-центра, почти никогда не попадает напрямую на процесс web server — сначала он проходит через <a href="https://www.cloudflare.com/learning/performance/what-is-load-balancing/" rel="noopener noreferrer" target="_blank"><strong>балансировщик нагрузки</strong></a>. Различают два уровня.</p>
<p><strong>L4-балансировщик</strong> (транспортный) распределяет по IP и порту, не заглядывая в HTTP, и работает в двух режимах:</p>
<ul>
<li><strong>proxy / in-line</strong> — весь трафик, туда и обратно, идёт через балансировщик, который терминирует TCP;</li>
<li><strong>DSR (Direct Server Return)</strong> — запрос приходит через LB, но ответ backend отправляет клиенту напрямую, минуя балансировщик, что снимает с него обратный (обычно куда более объёмный) трафик.</li>
</ul>
<p><strong>L7-балансировщик / reverse proxy</strong> (прикладной) терминирует TLS, читает HTTP и маршрутизирует по <code>Host</code>/path/cookie, обеспечивает sticky sessions, health checks, retry и circuit breaking (Nginx, HAProxy, Envoy, облачные ALB). Про то, как это настраивается в nginx, у меня есть пост про <a href="/posts/nginx-lb/">балансировку бэкендов</a>, а про архитектуру в целом — <a href="/posts/ha-clustering/">про кластеризацию и доступность</a>.</p>
<p>Алгоритмы распределения — round-robin, least connections, hash по IP или URL, weighted. За балансировщиком стоит пул backend-серверов, и уже выбранный экземпляр проходит путь «сокет → приложение». На практике перед origin выстраивается целая цепочка: <strong>CDN edge → L4 LB → L7 reverse proxy → web server → application server</strong>, и на каждом стыке возможны свои keep-alive пулы соединений и собственный слой кэширования.</p>
<h3 id="web-server-от-сокета-к-приложению">Web Server: от сокета к приложению</h3>
<p>Web server (Nginx, Apache) узнаёт о доступности данных через механизмы <strong>event notification</strong>: <code>epoll</code> (Linux), <code>kqueue</code> (BSD) или <code>select</code>/<code>poll</code>. Они позволяют мониторить тысячи сокетов одновременно и получать уведомления, только когда данные доступны для чтения или записи.</p>
<p>При получении события сервер вызывает <a href="https://man7.org/linux/man-pages/man2/recv.2.html" rel="noopener noreferrer" target="_blank"><code>recv()</code></a> или <code>recvmsg()</code>, который копирует данные из kernel socket buffer в user space buffer приложения. Копирование пересекает границу kernel-user space, что стоит денег. Для минимизации существуют техники zero-copy (<code>sendfile</code>, <code>splice</code>).</p>
<p>Получив запрос, web server <strong>парсит его структуру</strong>: request line (метод, URI, версия HTTP), заголовки, опциональное тело.</p>
<p>Nginx использует <a href="https://nginx.org/en/docs/http/request_processing.html" rel="noopener noreferrer" target="_blank">асинхронную event-driven архитектуру</a> с небольшим числом worker-процессов (обычно по одному на CPU core). Каждый worker обрабатывает множество одновременных соединений в одном потоке через non-blocking I/O и event loop. Так Nginx держит десятки тысяч соединений с минимумом памяти. Apache традиционно использовал <strong>prefork MPM</strong>, где каждый запрос обрабатывается отдельным процессом, или <strong>worker MPM</strong> с thread pool; современные версии поддерживают и <strong>event MPM</strong>, аналогичный nginx.</p>
<p>Дальше web server выполняет <strong>routing</strong> на основе конфигурации: для статических файлов читает файл с диска и отправляет клиенту, для динамического контента перенаправляет запрос к application server (PHP-FPM, Python WSGI, Node.js) через FastCGI, HTTP proxy или другие протоколы. Какой именно <a href="/posts/virt-host-nginx/">server block</a> обработает запрос — определяется по заголовку <code>Host</code>, и это, кстати, частый источник сюрпризов, когда запрос улетает в дефолтный виртуалхост.</p>
<h3 id="обработка-приложением-и-формирование-ответа">Обработка приложением и формирование ответа</h3>
<p>Application server выполняет бизнес-логику: парсинг query-параметров и request body, валидацию и аутентификацию, обращения к базе, вызовы внешних API, рендеринг шаблонов, формирование ответа.</p>
<p>После обработки формируется <strong>HTTP response</strong>: status line (<code>HTTP/1.1 200 OK</code>), заголовки (Content-Type, Content-Length, Cache-Control, Set-Cookie), пустая строка-разделитель, тело ответа.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer" target="_blank"><strong>Cache-Control заголовки</strong></a> управляют <a href="https://web.dev/articles/http-cache" rel="noopener noreferrer" target="_blank">кэшированием на стороне клиента и промежуточных proxy</a>:</p>
<ul>
<li><code>Cache-Control: max-age=31536000, immutable</code> — для статики с версионированием (CSS, JS с hash в имени);</li>
<li><code>Cache-Control: no-cache</code> — для ресурсов, требующих revalidation перед использованием;</li>
<li><code>Cache-Control: no-store</code> — для приватных данных, которые кэшировать нельзя вообще.</li>
</ul>
<p>Response передаётся обратно через <code>send()</code> или <code>sendfile()</code> (для статических файлов), данные копируются в kernel space, TCP-стек сегментирует их, IP-стек инкапсулирует в пакеты, Ethernet-стек создаёт кадры. Пакеты помещаются в <strong>transmit ring buffer</strong>, NIC через DMA читает их и передаёт на физический уровень. Response идёт обратным путём через интернет к клиенту.</p>
<h2 id="браузер-рендеринг-и-отображение">Браузер: рендеринг и отображение</h2>
<h3 id="приём-и-парсинг-http-response">Приём и парсинг HTTP response</h3>
<p>Сервер ответил — мяч снова на стороне клиента. Response-пакеты проходят тот же путь: hardware interrupt, DMA в ring buffer, softirq, network stack, TCP reassembly — и попадают в socket buffer.</p>
<p>Браузерный network thread вызывает <code>recv()</code> для чтения данных. <a href="https://dev.to/bennyxguo/learn-how-the-browser-works-in-practice-http-request-and-parsing-4fdg" rel="noopener noreferrer" target="_blank">Response parser</a> обрабатывает структуру: status line (200, 301, 404, 500), response headers (Content-Type определяет тип контента и способ парсинга, Content-Length — для прогресса загрузки, <code>Transfer-Encoding: chunked</code> — для streaming), тело — собственно HTML, CSS, JavaScript, изображения.</p>
<h3 id="html-parsing-и-построение-dom">HTML Parsing и построение DOM</h3>
<p>Получив HTML, браузер начинает <a href="https://dev.to/ayush_maurya_/how-browsers-parse-and-render-html-from-request-to-paint-1ogp" rel="noopener noreferrer" target="_blank"><strong>parsing</strong></a> — преобразование текстового HTML в DOM (Document Object Model).</p>
<p>Процесс начинается с <strong>tokenization</strong>: парсер читает байты, декодирует их в символы согласно charset (обычно UTF-8), сканирует символы и выделяет токены — start tags (<code>&lt;div&gt;</code>), end tags (<code>&lt;/div&gt;</code>), attributes (<code>class=&quot;container&quot;</code>), text content, comments.</p>
<p>Из токенов строится <strong>DOM tree</strong>: открывающий тег создаёт новый node и становится текущим родителем, текстовое содержимое создаёт text node как дочерний, закрывающий тег возвращает контекст к родителю. DOM tree — иерархическая структура, где каждый HTML-элемент представлен объектом со свойствами, методами и связями parent-child-sibling.</p>
<p>«Стек тегов» — хорошая первая метафора, но реальный <a href="https://html.spec.whatwg.org/multipage/parsing.html" rel="noopener noreferrer" target="_blank">HTML-парсинг</a> сложнее: это токенизатор с большим числом состояний, tree-construction algorithm с <strong>insertion modes</strong>, автоматическое исправление некорректной вложенности, foster parenting для таблиц, adoption agency algorithm для форматирующих тегов и особая обработка parser-inserted скриптов. Именно поэтому браузер способен превратить формально «кривой» HTML в стабильный DOM — иногда совсем не такой, как наивно ожидаешь по разметке.</p>
<p>Во время парсинга браузер обнаруживает ссылки на внешние ресурсы (CSS, JavaScript, images, fonts) и начинает их загрузку параллельно. Для этого используется <strong>preload scanner</strong> — лёгковесный парсер, который сканирует HTML на предмет ресурсов, не дожидаясь основного парсера.</p>
<h3 id="css-parsing-и-cssom">CSS Parsing и CSSOM</h3>
<p>Когда браузер встречает <code>&lt;link rel=&quot;stylesheet&quot;&gt;</code> или <code>&lt;style&gt;</code>, он загружает CSS и начинает парсинг в <a href="https://leapcell.io/blog/how-css-affects-parsing-and-rendering" rel="noopener noreferrer" target="_blank"><strong>CSSOM (CSS Object Model)</strong></a>. CSS-парсер похож на HTML-парсер: tokenization, парсинг селекторов и правил, построение CSSOM tree, где каждый node представляет CSS rule с селектором и объявлениями свойств.</p>
<p><strong>CSS блокирует рендеринг — но не любой.</strong> Браузер не будет отрисовывать страницу, пока не загрузит и не распарсит render-blocking стили из <code>&lt;head&gt;</code>. При этом стиль с неподходящим <code>media</code>-условием (скажем, <code>media=&quot;print&quot;</code>) или помеченный как некритичный первый render не блокирует. В целом это предотвращает FOUC (Flash of Unstyled Content), но замедляет initial render. Поэтому критический CSS инлайнят в <code>&lt;head&gt;</code>, а некритический загружают асинхронно.</p>
<h3 id="javascript-выполнение-и-блокировка">JavaScript: выполнение и блокировка</h3>
<p>JavaScript имеет особое влияние на парсинг и рендеринг — правда, не всякий скрипт одинаково. Классический <strong>parser-inserted</strong> <code>&lt;script&gt;</code> без <code>async</code>/<code>defer</code> действительно <strong>останавливает парсинг HTML</strong>: скрипт может манипулировать DOM (<code>document.write()</code>, создание/удаление элементов), и браузер не знает заранее, что он сделает, поэтому обязан дождаться выполнения. А вот <code>async</code>, <code>defer</code>, module-скрипты и динамически добавленные элементы ведут себя иначе — так что «каждый <code>&lt;script&gt;</code> тормозит парсер» тоже упрощение.</p>
<p>Процесс для блокирующего скрипта: браузер загружает его (если src внешний), компилирует JavaScript в байт-код или machine code (JIT в V8, SpiderMonkey, JavaScriptCore), выполняет, и только после этого возобновляется HTML-парсинг.</p>
<p>Для оптимизации используются атрибуты:</p>
<ul>
<li><code>async</code> — скрипт загружается параллельно и выполняется, как только загрузится, не блокируя парсинг загрузкой (но само выполнение всё равно приостанавливает парсер и не сохраняет порядок относительно соседних async-скриптов);</li>
<li><code>defer</code> — скрипт загружается параллельно, но выполняется только после полного парсинга HTML, перед событием DOMContentLoaded;</li>
<li>module-скрипты по умолчанию ведут себя близко к <code>defer</code>.</li>
</ul>
<h3 id="render-tree-и-layout">Render Tree и Layout</h3>
<p>После построения DOM и CSSOM браузер объединяет их в <a href="https://bytebytego.com/guides/how-does-the-browser-render-a-web-page/" rel="noopener noreferrer" target="_blank"><strong>Render Tree</strong></a> — структуру, содержащую только видимые элементы с их вычисленными стилями. Render Tree не включает элементы с <code>display: none</code>, <code>&lt;head&gt;</code>, <code>&lt;script&gt;</code> и другие невидимые nodes.</p>
<p>Следующий этап — <strong>Layout (reflow)</strong>, где браузер вычисляет точное положение и размеры каждого элемента: расчёт box model (content, padding, border, margin), применение positioning (static, relative, absolute, fixed, sticky), вычисление flexbox и grid, обработку text flow и word wrapping.</p>
<p>Layout — дорогая операция, особенно на сложных страницах. Изменения, влияющие на layout (resize, изменение font-size, добавление/удаление элементов), вызывают reflow всего дерева или его части.</p>
<h3 id="painting-и-compositing">Painting и Compositing</h3>
<p>После Layout следует <strong>Paint</strong> — но, вопреки распространённому упрощению, это ещё не превращение элементов в пиксели. На стадии Paint браузер лишь <strong>строит список команд рисования</strong> (<strong>paint records</strong> / display list): «залить фон», «нарисовать границу», «вывести текст», «наложить тень», применить эффекты (box-shadow, border-radius). Сами пиксели тут ещё не заполняются — Chromium это прямо подчёркивает.</p>
<p>Отдельная стадия — <strong>rasterization (растеризация)</strong>: она исполняет эти команды по тайлам и превращает их в пиксели. Происходить она может позже и на других потоках, включая <strong>GPU raster threads</strong>. Именно поэтому paint и rasterization — разные этапы, а не одно и то же.</p>
<p>Дальше — <strong>compositing</strong>: разделение страницы на слои (layers) и отдельный рендеринг каждого. Элементы с <code>transform</code>, <code>opacity</code>, <code>position: fixed</code>, <code>will-change</code> часто создают отдельные слои. Compositing слоёв делает GPU — отсюда плавные анимации и скроллинг.</p>
<p>Финальный этап — <strong>presentation (display)</strong>: скомпозированный кадр отправляется в графический буфер, который выводится на экран. Условно вся цепочка выглядит так: <code>style calculation → layout → paint / display lists → rasterization → compositing → presentation</code>, причём последующие изменения могут инвалидировать лишь часть стадий, а не пересчитывать всю страницу.</p>
<h3 id="event-loop-и-rendering-pipeline">Event Loop и Rendering Pipeline</h3>
<p>Браузерный <a href="https://webperf.tips/tip/event-loop/" rel="noopener noreferrer" target="_blank"><strong>Event Loop</strong></a> координирует выполнение JavaScript и рендеринг. Цикл работает так: из <strong>task queue</strong> (macrotask queue) берётся одна задача (timer callbacks, I/O, events), задача выполняется до завершения, затем полностью обрабатывается <a href="https://adityaprabhat.hashnode.dev/event-loop-and-asynchrony-in-javascript" rel="noopener noreferrer" target="_blank"><strong>microtask queue</strong></a> (Promise callbacks, MutationObserver), после чего проверяется, нужно ли рендерить.</p>
<p>Важно, что момент обновления экрана (<strong>rendering opportunity</strong>) возникает <strong>отдельно</strong> от обработки задач и не привязан к жёсткому периоду. 16,67 мс — это длительность кадра при 60 Гц, а не фиксированный «такт» event loop: возможность обновить картинку зависит от частоты экрана, видимости вкладки, производительности страницы, throttling и наличия визуальных изменений. Стандарт HTML вообще не требует конкретного алгоритма выбора момента рендера — браузер вправе пропускать обновления или перейти, например, к 30 Гц. Если момент рендера подошёл: выполняются <strong>requestAnimationFrame callbacks</strong>, при необходимости пересчитываются Layout, Paint, rasterization и Compositing.</p>
<p>Если JavaScript-задача выполняется слишком долго (long task &gt; 50 мс), она блокирует Event Loop — рендеринг не происходит, страница перестаёт отвечать. Поэтому тяжёлые операции разбивают на chunks, а фоновые вычисления уносят в Web Workers.</p>
<h2 id="ключевые-прерывания-и-системные-вызовы">Ключевые прерывания и системные вызовы</h2>
<p>Путь пройден целиком — теперь шпаргалка: соберём в одном месте всё, что по дороге дёргало ядро.</p>
<h3 id="hardware-interrupts">Hardware Interrupts</h3>
<p>За время открытия сайта <strong>hardware interrupts</strong> случаются постоянно: <strong>keyboard interrupts</strong> при вводе URL, <strong>network interrupts (IRQ)</strong> при приёме/отправке пакетов, <strong>timer interrupts</strong> — для preemptive multitasking.</p>
<p>Каждое прерывание вызывает вход в обработчик с сохранением и восстановлением состояния CPU и выполнением ISR — но не обязательно полноценный context switch планировщика (смена процесса — отдельное событие). Частые прерывания всё равно создают overhead, поэтому применяют interrupt coalescing (группировка нескольких событий в одно прерывание) и NAPI polling для batch-обработки.</p>
<h3 id="software-interrupts-softirq">Software Interrupts (SoftIRQ)</h3>
<p><strong>Softirq</strong> — механизм отложенной обработки в ядре. Основные типы для сети: <strong>NET_RX_SOFTIRQ</strong> для приёма пакетов (обработка receive ring buffer, передача пакетов вверх по стеку) и <strong>NET_TX_SOFTIRQ</strong> для отправки (завершение передачи, освобождение буферов).</p>
<p>Softirq выполняются в kernel context на высоком приоритете, но ниже hardware interrupts. Они могут прерывать user processes, но не могут прерывать hardware interrupt handlers.</p>
<h3 id="system-calls">System Calls</h3>
<p><strong>System calls</strong> — интерфейс между user space и ядром:</p>
<table>
	<thead>
			<tr>
					<th>Группа</th>
					<th>Вызовы</th>
					<th>Что делают</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Socket operations</td>
					<td><code>socket()</code>, <code>bind()</code>, <code>listen()</code>, <code>accept()</code>, <code>connect()</code></td>
					<td>создание сокета, привязка к адресу, приём и инициация соединений</td>
			</tr>
			<tr>
					<td>Data transfer</td>
					<td><code>send()</code>, <code>sendto()</code>, <code>sendmsg()</code>, <code>recv()</code>, <code>recvfrom()</code>, <code>recvmsg()</code></td>
					<td>отправка и приём данных</td>
			</tr>
			<tr>
					<td>File operations</td>
					<td><code>read()</code>, <code>write()</code>, <code>open()</code>, <code>close()</code></td>
					<td>чтение/запись (работают и с сокетами), работа с дескрипторами</td>
			</tr>
			<tr>
					<td>I/O multiplexing</td>
					<td><code>select()</code>, <code>poll()</code>, <code>epoll()</code></td>
					<td>мониторинг множества файловых дескрипторов</td>
			</tr>
	</tbody>
</table>
<p>Каждый system call вызывает переключение из user mode в kernel mode: процессор переходит в <a href="https://unix-learn.moabukar.co.uk/kernel/kernel-privilege-levels/" rel="noopener noreferrer" target="_blank">Ring 0</a>, выполняет операцию в kernel space, копирует результаты обратно в user space и возвращается в Ring 3. Это <strong>не</strong> полноценный context switch планировщика и не обязательно полный TLB flush: на x86 с включённым Page Table Isolation вход в ядро может сопровождаться сменой таблиц страниц и лишней работой с TLB, но зависит это от архитектуры, поддержки PCID и включённых защит от уязвимостей — универсального правила «syscall → полный TLB flush» нет. Но и «дешёвыми» эти переключения не назвать, поэтому оптимизация часто сводится к минимизации числа system calls. Посмотреть, что именно процесс дёргает у ядра, можно через <a href="/posts/strace/"><code>strace</code></a> — очень отрезвляющее упражнение.</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<p>Главная ценность этой карты не в том, чтобы пересказать её на собеседовании, а в том, чтобы по симптому сразу понимать, на каком этаже искать. Вот примерное соответствие:</p>
<table>
	<thead>
			<tr>
					<th>Симптом</th>
					<th>Этаж</th>
					<th>Чем смотреть</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Долгая пауза до начала загрузки</td>
					<td>DNS resolution</td>
					<td><code>dig +trace example.com</code>, <code>resolvectl query</code></td>
			</tr>
			<tr>
					<td>Соединение висит на connect</td>
					<td>TCP handshake, файрвол</td>
					<td><code>tcpdump -ni any port 443</code>, <code>ss -tan</code>, <a href="/posts/iptables-manual/">iptables</a></td>
			</tr>
			<tr>
					<td>Мелкие запросы идут, крупные виснут</td>
					<td>PMTU black hole</td>
					<td><code>ping -M do -s 1472</code>, проверка ICMP на пути</td>
			</tr>
			<tr>
					<td>Высокий TTFB при быстрой сети</td>
					<td>backend, БД, application server</td>
					<td>логи с временем ответа (<a href="/posts/nginx-json-logs/">nginx в JSON</a>)</td>
			</tr>
			<tr>
					<td>Скачет latency под нагрузкой</td>
					<td>congestion control, очереди ядра</td>
					<td><code>ss -ti</code>, <code>netstat -s</code>, <a href="/posts/sysctl-hl/">тюнинг sysctl</a></td>
			</tr>
			<tr>
					<td>Запрос улетел не в тот бэкенд</td>
					<td>L7-роутинг, <code>Host</code>, <a href="/posts/virt-host-nginx/">server blocks</a></td>
					<td><code>curl -v -H 'Host: ...'</code>, логи балансировщика</td>
			</tr>
			<tr>
					<td>Страница загрузилась, но «тупит»</td>
					<td>Event Loop, long tasks, reflow</td>
					<td>вкладка Performance в DevTools</td>
			</tr>
			<tr>
					<td>Сеть «медленная» без конкретики</td>
					<td>пропускная способность канала</td>
					<td><a href="/posts/iperf3/"><code>iperf3</code></a> между узлами</td>
			</tr>
	</tbody>
</table>
<p>Эта же карта работает и в обратную сторону — когда систему надо не чинить, а спроектировать: примерно так я разбирал <a href="/posts/interview-task-0003/">задачку с собеса про высоконагруженный сервис</a>.</p>
<p>Порядок диагностики — сверху вниз по стеку, а не наугад: сначала убеждаешься, что имя резолвится, потом что соединение устанавливается, потом что сервер отвечает, и только потом лезешь в приложение. Половина «загадочных» инцидентов разваливается уже на первых двух шагах.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong>ICMP не «лишний трафик».</strong> Заблокировав его целиком «для безопасности», ты ломаешь Path MTU Discovery и получаешь соединения, которые устанавливаются, но зависают на крупных ответах. Симптом максимально неочевидный.</li>
<li><strong>TTL кэша DNS живёт дольше, чем ты думаешь.</strong> Между твоим изменением записи и реальным переключением трафика стоят кэши браузера, ОС, резолвера провайдера и промежуточных нод. Снижай TTL заранее, до миграции, а не в момент.</li>
<li><strong>HSTS липкий, но не необратимый.</strong> Отдал заголовок с большим <code>max-age</code> — и браузеры пользователей будут ходить только по HTTPS всё это время, даже если у тебя сломался сертификат. Обычную динамически полученную политику можно снять, прислав <code>Strict-Transport-Security: max-age=0</code>; а вот попадание в browser <strong>preload list</strong> — это уже надолго и откатывается медленно. Тестируй с маленьким <code>max-age</code>.</li>
<li><strong><code>301</code> прилипает надолго.</strong> Permanent-редирект браузеры кэшируют агрессивно (иногда эвристически, без явного срока), и ошибочный <code>301</code> потом тяжело выковырять из кэшей пользователей — управлять им со своей стороны уже почти нельзя. Пока не уверен — отдавай <code>302</code>.</li>
<li><strong>Первые ~14 КБ дешевле остального.</strong> Из-за initial window (обычно 10 сегментов) первая порция ответа приходит за один round-trip, а всё сверх того требует дополнительных RTT на «раскачку». Это грубая performance-интуиция, а не физический закон: размер считается в сегментах и зависит от MSS, на уже прогретом соединении cwnd другой, а QUIC вообще использует свой congestion control. Но вывод практичен: критический CSS в <code>&lt;head&gt;</code> — не мода, а арифметика.</li>
<li><strong>HTTP/2 не лечит потери пакетов.</strong> Мультиплексирование в одном TCP-соединении означает, что одна потеря тормозит все streams разом. На плохих каналах HTTP/2 может оказаться хуже HTTP/1.1 с несколькими соединениями — вот от этого и лечит QUIC.</li>
<li><strong>Каждый редирект — это RTT.</strong> Цепочка <code>http</code> → <code>https</code> → <code>www</code> на мобильном канале с RTT 150 мс стоит почти полсекунды ещё до того, как придёт первый байт HTML.</li>
<li><strong>Эфемерные порты кончаются.</strong> На балансировщике или прокси с большим числом исходящих соединений диапазон <code>net.ipv4.ip_local_port_range</code> и <code>TIME_WAIT</code> становятся реальным потолком. Проявляется внезапно и под нагрузкой.</li>
</ul>
<h2 id="заключение">Заключение</h2>
<p>От первого hardware interrupt до последнего пикселя — каждый этап этого пути вылизан десятилетиями инженерной работы. Но полезен он не как энциклопедическая справка, а как карта для дебага: когда понимаешь, где живут DNS resolution, TLS handshake, softirq и reflow, «сайт тормозит» перестаёт быть магией. У каждой проблемы появляется конкретный этаж, на котором её надо искать: где-то поможет <code>tcpdump</code>, где-то — вкладка Performance в DevTools, а где-то — просто <a href="/posts/wat-monitoring/">нормальный мониторинг</a>, который покажет проблему до того, как о ней напишут в поддержку.</p>
<p>Если хочется превратить эту карту в план обучения, а не в разовое чтение, — те же этажи разложены по уровням и ожиданиям от инженера в листах <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/networking/" rel="noopener noreferrer" target="_blank">Networking</a> и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/operating-systems/" rel="noopener noreferrer" target="_blank">Operating Systems</a> карты компетенций.</p>
<p>И в следующий раз, когда страница откроется «мгновенно», ты будешь знать, сколько всего успело случиться за эти пару сотен миллисекунд.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Ты вводишь адрес, жмёшь Enter — и через мгновение видишь страницу. Выглядит как одно действие. Под капотом же — <a href="https://aws.amazon.com/blogs/mobile/what-happens-when-you-type-a-url-into-your-browser/" rel="noopener noreferrer" target="_blank">каскад процессов</a>, прошивающий все уровни системы: от физических прерываний процессора до алгоритмов рендеринга браузера. Разберём <a href="https://habr.com/ru/companies/gnivc/articles/861432/" rel="noopener noreferrer" target="_blank">весь путь</a> — локальный и глобальный — от нажатия клавиши до отрисовки последнего пикселя на экране.</p>
<p>Спойлер: этот вопрос не зря любят на собеседованиях — по тому, где кандидат останавливается, видно всю его карту знаний. Про то, что там спрашивают ещё, я писал <a href="/posts/after-50-interview/">после полусотни собесов</a>. И полезен он не ради галочки: когда знаешь, из каких этажей состоит путь, «сайт тормозит» перестаёт быть магией и превращается в конкретный этаж, на котором надо копать.</p>
<h2 id="локальный-компьютер-до-первого-пакета">Локальный компьютер: до первого пакета</h2>
<h3 id="ввод-url-и-обработка-клавиатурных-событий">Ввод URL и обработка клавиатурных событий</h3>
<p>Всё начинается с физического нажатия клавиши. Каждое нажатие генерирует <strong>hardware interrupt</strong> (аппаратное прерывание), которое передаётся процессору. Клавиатура, как периферийное устройство, отправляет сигнал контроллеру прерываний, тот приостанавливает текущее выполнение процессора и вызывает <strong>interrupt service routine (ISR)</strong> — специальную процедуру обработки прерывания.</p>
<p>Вход в обработчик прерывания — это переключение в kernel mode и сохранение части состояния прерванного контекста (регистры, program counter, stack pointer). Но само по себе это ещё не <a href="https://www.lenovo.com/us/en/glossary/context-switch/" rel="noopener noreferrer" target="_blank"><strong>context switch</strong></a> в смысле планировщика: процессор часто возвращается в тот же поток, а переключение на другой процесс — отдельное событие, которое может и не произойти.</p>
<p>Дальше — важная тонкость, на которой легко ошибиться. Ввод в адресную строку обрабатывает <strong>UI-поток самого браузера (browser process)</strong>, а не JavaScript открытой веб-страницы: renderer-процесс страницы <code>keydown</code> из омнибокса вообще не видит. Браузер сам решает, что ты ввёл — URL или поисковый запрос, — и инициирует навигацию. (Те самые события <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent" rel="noopener noreferrer" target="_blank"><code>keydown</code></a>, JavaScript-<a href="https://javascript.plainenglish.io/understand-javascripts-event-loop-36c021f850f7" rel="noopener noreferrer" target="_blank"><strong>event loop</strong></a> и синхронизация с <strong>rendering pipeline</strong> — это уже про обработку ввода внутри самой страницы, а не про адресную строку.)</p>
<p>После ввода полного URL и нажатия Enter браузер парсит адрес, разделяя его на компоненты: протокол (<code>https://</code>), доменное имя (<code>example.com</code>), путь (<code>/page</code>) и query-параметры. По умолчанию браузеры используют HTTPS (порт 443).</p>
<h3 id="проверка-кэшей-браузера-до-сетевого-запроса">Проверка кэшей браузера до сетевого запроса</h3>
<p>Прежде чем инициировать DNS resolution, браузер проходит цепочку локальных кэшей — и во многих случаях сетевой запрос вообще не понадобится. Порядок такой: сначала <strong>HTTP-кэш браузера</strong> (memory cache для текущей сессии и disk cache между запусками) — если ресурс ещё свежий по <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching" rel="noopener noreferrer" target="_blank"><code>Cache-Control</code>/<code>Expires</code></a>, он отдаётся со статусом <code>from memory cache</code> / <code>from disk cache</code>, без единого пакета в сеть. Если ресурс формально устарел, но у него есть валидатор (<code>ETag</code> или <code>Last-Modified</code>), браузер отправляет <a href="https://datatracker.ietf.org/doc/html/rfc9111" rel="noopener noreferrer" target="_blank"><strong>conditional request</strong></a> (<code>If-None-Match</code> / <code>If-Modified-Since</code>) и может получить <code>304 Not Modified</code> — заголовки приходят, тело не передаётся.</p>
<p>Выше HTTP-кэша стоит <strong>Service Worker</strong>: если на origin зарегистрирован SW, он перехватывает <code>fetch</code> и способен отдать ответ из <strong>Cache Storage</strong> полностью офлайн, вообще минуя сеть. Отдельно существует <strong>bfcache (back/forward cache)</strong> — снимок полностью живой страницы (с DOM и состоянием JS), который позволяет мгновенно вернуться по кнопкам «назад/вперёд» без повторного парсинга и рендеринга.</p>
<p>Только промах во всей этой цепочке приводит к реальному сетевому пути, описанному ниже. Именно поэтому повторное открытие сайта визуально мгновенно: львиная доля ресурсов не доходит даже до стадии DNS.</p>
<h3 id="dns-resolution-преобразование-имени-в-ip-адрес">DNS Resolution: преобразование имени в IP-адрес</h3>
<p>Прежде чем браузер сможет установить соединение с сервером, ему надо преобразовать человекочитаемое доменное имя в IP-адрес — процесс, называемый <a href="https://cycle.io/learn/dns-resolution-process" rel="noopener noreferrer" target="_blank"><strong>DNS resolution</strong></a>. Процесс многоступенчатый, с несколькими уровнями кэширования.</p>
<p>Сначала браузер проверяет свой <strong>локальный DNS-кэш</strong>. Если записи нет, запрос уходит операционной системе, которая проверяет системный кэш (на современных дистрибутивах за это отвечает <code>systemd-resolved</code> — про его настройку у меня есть <a href="/posts/systemd-resolved/">отдельный пост</a>). При промахе в обоих кэшах запрос отправляется к <strong>DNS resolver</strong> — обычно это DNS-сервер провайдера или публичный DNS (Google 8.8.8.8, Cloudflare 1.1.1.1).</p>
<p>Resolver выполняет <strong>рекурсивный запрос</strong> от имени клиента. Если у него нет кэшированной записи, он обращается к <a href="https://www.geeksforgeeks.org/computer-networks/address-resolution-in-dns-domain-name-server/" rel="noopener noreferrer" target="_blank"><strong>Root DNS-серверам</strong></a> — 13 логических серверов (<code>a</code>–<code>m.root-servers.net</code>), каждый из которых на деле Anycast-кластер из сотен нод по всему миру. Root DNS не знает конкретного IP-адреса, но направляет запрос к <strong>TLD-серверам</strong>, отвечающим за зоны верхнего уровня (<code>.com</code>, <code>.org</code>, <code>.ru</code>).</p>
<p>TLD-сервер направляет запрос к <strong>Authoritative DNS-серверу</strong> конкретного домена, который и возвращает финальный IP. Весь процесс обычно занимает от 20 до 120 миллисекунд в зависимости от удалённости серверов и состояния кэшей.</p>
<p>Только это не строго линейная цепочка «один запрос → один IP → connect». Браузер обычно запрашивает записи <code>A</code> (IPv4) и <code>AAAA</code> (IPv6) параллельно, может учитывать <code>HTTPS</code>/<code>SVCB</code>-записи и ходить через DoH/DoT, а в ответ приходит <strong>несколько адресов</strong>. Дальше в дело вступает <a href="https://datatracker.ietf.org/doc/html/rfc8305" rel="noopener noreferrer" target="_blank"><strong>Happy Eyeballs</strong></a>: клиент запускает конкурентные попытки подключения по IPv6 и IPv4 со слегка разнесённым стартом, чтобы одна неработающая ветка не тормозила навигацию, — а иногда новое соединение вообще не нужно, потому что переиспользуется уже открытое. То есть реальная картина ближе к графу конкурентных операций, чем к последовательному списку.</p>
<p><a href="https://dev.to/wallacefreitas/understanding-domain-name-system-dns-recursive-iterative-queries-with-root-level-domains-581n" rel="noopener noreferrer" target="_blank">Рекурсивный и итеративный запросы</a> — не одно и то же: при рекурсивном resolver берёт на себя всю работу по обходу иерархии, при итеративном клиент сам последовательно обращается к каждому уровню.</p>
<h3 id="cdn-anycast-и-edge-куда-на-самом-деле-резолвится-имя">CDN, Anycast и edge: куда на самом деле резолвится имя</h3>
<p>Описанная выше цепочка для крупного сайта почти никогда не заканчивается на origin-сервере. Authoritative DNS для домена вроде <code>cnn.com</code> обычно делегирован <strong>CDN</strong> (Cloudflare, Akamai, Fastly, CloudFront), и он возвращает не фиксированный IP конкретного дата-центра, а адрес ближайшей <strong>edge-точки</strong>.</p>
<p>Работают два механизма приближения:</p>
<ul>
<li><strong>GeoDNS / EDNS Client Subnet</strong> — authoritative-сервер смотрит на подсеть резолвера (или клиента) и отдаёт разный ответ в зависимости от географии.</li>
<li><a href="https://www.cloudflare.com/learning/cdn/glossary/anycast-network/" rel="noopener noreferrer" target="_blank"><strong>Anycast</strong></a> — один и тот же IP-адрес анонсируется по BGP из десятков локаций мира, так что пакет естественным образом доходит до топологически ближайшей ноды.</li>
</ul>
<p>В результате TCP- и TLS-handshake происходят не с origin в другом полушарии, а с edge-нодой в десятках километров: <strong>RTT</strong> падает с ~100–150 мс до единиц миллисекунд. А это ключевой множитель для TCP slow start и числа TLS round-trips. На edge отдаётся закэшированный статический контент, а к origin запрос уходит только при cache miss или за динамикой — часто по уже прогретому keep-alive соединению. Попутно CDN обычно терминирует TLS на edge и первым в цепочке поддерживает свежие протоколы (HTTP/3, TLS 1.3), даже если origin о них не знает.</p>
<h3 id="установление-tcp-соединения-трёхстороннее-рукопожатие">Установление TCP-соединения: трёхстороннее рукопожатие</h3>
<p>Получив IP-адрес, браузер готов установить транспортное соединение. Для HTTP/1.1 и HTTP/2 используется <strong>TCP</strong> — надёжный протокол с установлением соединения (HTTP/3 работает иначе, поверх QUIC/UDP — о нём в разделе про версии протокола), а само установление TCP идёт через <a href="https://networkwalks.com/tcp-3-way-handshake-process/" rel="noopener noreferrer" target="_blank"><strong>three-way handshake</strong></a>.</p>
<p>Клиент отправляет <a href="https://www.geeksforgeeks.org/computer-networks/tcp-3-way-handshake-process/" rel="noopener noreferrer" target="_blank"><strong>SYN-пакет</strong></a> с начальным sequence number — случайным числом для синхронизации и защиты от подделки. Сервер отвечает <strong>SYN-ACK</strong>, который одновременно подтверждает SYN клиента (увеличивая sequence number на 1) и несёт собственный SYN со своим sequence number. Клиент завершает рукопожатие <strong>ACK-пакетом</strong>.</p>
<p>В процессе ОС выполняет <a href="https://www.gta.ufrj.br/ensino/eel878/sockets/syscalls.html" rel="noopener noreferrer" target="_blank">системные вызовы</a> <code>socket()</code> и <code>connect()</code> для создания сокета и инициации соединения. Сокет — это конечная точка двунаправленной связи между процессами. Клиент использует эфемерный порт (IANA-диапазон 49152–65535; Linux по умолчанию берёт из 32768–60999, см. <code>net.ipv4.ip_local_port_range</code>), а сервер слушает на стандартном порту (80 для HTTP, 443 для HTTPS). Кто хочет увидеть, как эфемерные порты ломают жизнь на практике, — почитай про <a href="/posts/ftp-passive-vs-active/">активный и пассивный режимы FTP</a>.</p>
<h3 id="управление-потоком-и-перегрузкой-mss-mtu-congestion-control">Управление потоком и перегрузкой: MSS, MTU, congestion control</h3>
<p>Three-way handshake — это не только синхронизация sequence numbers. В SYN-пакетах стороны согласуют опции, критичные для производительности:</p>
<ul>
<li><strong>MSS (Maximum Segment Size)</strong> — максимальный размер полезной нагрузки TCP-сегмента, выводимый из <strong>MTU</strong> канала (обычно 1500 байт для Ethernet → MSS ≈ 1460).</li>
<li><strong>Window Scale</strong> — множитель, позволяющий окну превышать 64 КБ.</li>
<li><strong>SACK (Selective Acknowledgment)</strong> и timestamps.</li>
</ul>
<p>Если по пути встречается канал с меньшим MTU, срабатывает фрагментация или (при выставленном бите DF) <a href="https://datatracker.ietf.org/doc/html/rfc1191" rel="noopener noreferrer" target="_blank"><strong>Path MTU Discovery</strong></a> через ICMP. А вот если ICMP заботливо прибит на файрволе — получаешь «PMTU black hole»: соединение устанавливается, мелкие пакеты ходят, а крупные молча теряются и передача виснет. Классика жанра, на которую убивают часы, прежде чем догадаться посмотреть на MTU.</p>
<p>После установки соединения объём данных «в полёте» ограничен сразу двумя окнами: <strong>receive window</strong> (flow control — сколько готов принять получатель) и <strong>congestion window / cwnd</strong> (congestion control — сколько сеть готова пропустить без потерь). TCP стартует с малого cwnd (<strong>initial window</strong>, обычно 10 сегментов) и в фазе <a href="https://datatracker.ietf.org/doc/html/rfc5681" rel="noopener noreferrer" target="_blank"><strong>slow start</strong></a> наращивает его экспоненциально каждый RTT, затем переходит в линейный рост (<strong>congestion avoidance</strong>); при потере (таймаут или три dup ACK) окно резко сокращается.</p>
<p>Именно slow start объясняет ценность первых ~14 КБ ответа (см. видео <a href="https://youtu.be/fmiqceS8ZNg" rel="noopener noreferrer" target="_blank">«Почему ваш сайт должен весить 14 КБ»</a>) — они умещаются в initial window и приходят за один round-trip, тогда как всё сверх того требует новых RTT на «раскачку». Современные стеки используют <strong>CUBIC</strong> (дефолт Linux, реагирует на потери) и <a href="https://queue.acm.org/detail.cfm?id=3022184" rel="noopener noreferrer" target="_blank"><strong>BBR</strong></a> (моделирует пропускную способность и RTT, а не ждёт потерь).</p>
<h3 id="tls-handshake-установление-защищённого-канала">TLS Handshake: установление защищённого канала</h3>
<p>Для HTTPS после установления TCP следует ещё один этап — <a href="https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/" rel="noopener noreferrer" target="_blank"><strong>TLS handshake</strong></a>. Он устанавливает зашифрованный канал и подтверждает, что сервер — тот, за кого себя выдаёт.</p>
<p>Клиент отправляет <strong>ClientHello</strong>: поддерживаемые версии (<code>supported_versions</code>, обычно <a href="https://datatracker.ietf.org/doc/html/rfc8446" rel="noopener noreferrer" target="_blank">TLS 1.3</a>), список <a href="https://en.wikipedia.org/wiki/Cipher_suite" rel="noopener noreferrer" target="_blank"><strong>cipher suites</strong></a>, 32 байта случайных данных и расширения — SNI (Server Name Indication), ALPN, <code>signature_algorithms</code> и, что принципиально для 1.3, <code>key_share</code> с уже сгенерированным эфемерным (EC)DHE-ключом. Cipher suites в TLS 1.3 используют только AEAD-алгоритмы вроде <a href="https://ciphersuite.info/cs/TLS_AES_256_GCM_SHA384/" rel="noopener noreferrer" target="_blank"><code>TLS_AES_256_GCM_SHA384</code></a> — и, в отличие от TLS 1.2, в само имя сьюта уже не входят алгоритмы обмена ключами и аутентификации (<code>ECDHE</code>, <code>RSA</code>), они вынесены в расширения. В обычном браузерном handshake с сертификатом ключи выводятся через эфемерный (EC)DHE — это и даёт forward secrecy (стандарт допускает ещё два режима: чистый PSK и PSK вместе с (EC)DHE, так что «всегда ECDHE» — упрощение).</p>
<p>Сервер отвечает <a href="https://www.ibm.com/docs/en/ibm-mq/9.3.x?topic=tls-overview-ssltls-handshake" rel="noopener noreferrer" target="_blank"><strong>ServerHello</strong></a> со своим <code>key_share</code> и выбранным cipher suite. С этого момента обе стороны уже могут вычислить общий (EC)DHE-секрет — и <strong>всё дальнейшее шифруется</strong>. Дальше сервер шлёт <strong>EncryptedExtensions</strong>, <strong>Certificate</strong> и <strong>CertificateVerify</strong> — подпись всего предыдущего handshake приватным ключом, доказывающую владение сертификатом. Клиент проверяет сам сертификат (публичный ключ, цепочку до доверенного Certificate Authority) и эту подпись.</p>
<p><strong>Отдельного обмена ключами в стиле «клиент шифрует секрет публичным ключом сервера» в TLS 1.3 нет.</strong> Общий (EC)DHE-секрет уже получен из двух <code>key_share</code>, и из него через <strong>HKDF</strong> выводятся все симметричные ключи. Никакого <a href="https://wiki.osdev.org/TLS_Handshake" rel="noopener noreferrer" target="_blank">pre-master secret</a> в <strong>ClientKeyExchange</strong> здесь не пересылается — это схема <strong>RSA key exchange из TLS 1.2 и более старых версий</strong>, которую 1.3 выкинул вместе с прочими небезопасными механизмами.</p>
<p>Финальный этап — обе стороны обмениваются сообщениями <strong>Finished</strong>, подтверждая целостность handshake. Отдельный <strong>ChangeCipherSpec</strong> в TLS 1.3 из настоящего handshake <a href="https://www.encryptionconsulting.com/tls-1-2-and-tls-1-3/" rel="noopener noreferrer" target="_blank">исключён</a> и оставлен лишь как пустой совместимый сигнал для старых middlebox. Полный handshake TLS 1.3 укладывается в <strong>один RTT</strong> (против двух в 1.2). С заранее полученным PSK возможен <strong>0-RTT</strong>: клиент отправляет ранние данные ещё до завершения нового handshake — но у них слабее гарантии безопасности и их можно воспроизвести повторно (replay), поэтому сервер не должен принимать через 0-RTT необратимые операции. То есть 0-RTT — это resumption с ранними данными, а не «handshake за ноль RTT».</p>
<h3 id="формирование-и-отправка-http-запроса">Формирование и отправка HTTP-запроса</h3>
<p>После установления защищённого соединения браузер готов отправить HTTP-запрос. <a href="https://www.linode.com/docs/guides/http-get-request/" rel="noopener noreferrer" target="_blank">Структура запроса</a> состоит из <strong>request line</strong> (метод, путь, версия HTTP), <strong>заголовков</strong> и опционального <strong>тела</strong>.</p>
<p>Request line для главной страницы обычно выглядит как <code>GET / HTTP/1.1</code>. За ним следуют <a href="https://beeceptor.com/docs/concepts/http-headers/" rel="noopener noreferrer" target="_blank">заголовки</a>, каждый на отдельной строке: <code>Host: example.com</code> (обязательный), <code>User-Agent: Mozilla/5.0...</code>, <code>Accept: text/html,application/xhtml+xml...</code>, <code>Accept-Language: ru,en</code>, <code>Accept-Encoding: gzip, deflate, br</code>, <code>Connection: keep-alive</code>, <code>Cache-Control: max-age=0</code>.</p>
<p>Сформированный запрос передаётся через системный вызов <code>send()</code> или <code>write()</code>, который копирует данные из user space в kernel space в <a href="https://www.linuxjournal.com/article/6345" rel="noopener noreferrer" target="_blank">socket buffer</a>. Дальше ядро обрабатывает их через network stack, готовя к передаче по сети.</p>
<h3 id="httphttps-редиректы-и-hsts">HTTP→HTTPS, редиректы и HSTS</h3>
<p>Реальный первый запрос редко сразу попадает в целевой ресурс. Если пользователь ввёл голый домен или <code>http</code>-URL, срабатывает несколько механизмов.</p>
<p>Первый — <a href="https://datatracker.ietf.org/doc/html/rfc6797" rel="noopener noreferrer" target="_blank"><strong>HSTS (HTTP Strict Transport Security)</strong></a>: если домен есть в HSTS <strong>preload list</strong>, вшитом в браузер, или ранее прислал заголовок <code>Strict-Transport-Security</code>, браузер ещё до выхода в сеть сам заменяет <code>http://</code> на <code>https://</code> (internal redirect 307), исключая небезопасный первый хоп и возможность SSL-stripping.</p>
<p>Второй — <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections" rel="noopener noreferrer" target="_blank"><strong>серверные редиректы</strong></a>: сервер отвечает статусами <code>301</code>/<code>302</code>/<code>307</code>/<code>308</code> с заголовком <code>Location</code>, и браузер повторяет путь для нового URL — иногда с новым DNS/TCP/TLS, если сменился хост.</p>
<p>Типичная цепочка выглядит как <code>http://cnn.com</code> → <code>https://cnn.com</code> → <code>https://www.cnn.com</code>. Каждый лишний редирект — дополнительный RTT (а при смене хоста ещё и новый TLS-handshake), поэтому их число минимизируют. Различается и семантика кэширования, хотя и не так жёстко, как принято думать: <code>301</code> и <code>308</code> (permanent) браузер <strong>может эвристически закэшировать</strong> даже без явного срока — но это не значит «навсегда». А <code>302</code> и <code>307</code> (temporary) тоже кэшируются, если в ответе есть подходящие директивы вроде <code>Cache-Control: max-age=...</code>. Фактическое повторное использование редиректа определяется <a href="https://datatracker.ietf.org/doc/html/rfc9110" rel="noopener noreferrer" target="_blank">freshness, revalidation и cache-control-политикой</a>, а не только кодом статуса.</p>
<h3 id="версии-протокола-http11-http2-http3-quic">Версии протокола: HTTP/1.1, HTTP/2, HTTP/3 (QUIC)</h3>
<p>Показанный выше <code>GET / HTTP/1.1</code> — упрощение: версия прикладного протокола согласуется ещё в TLS через <strong>ALPN (Application-Layer Protocol Negotiation)</strong> внутри ClientHello.</p>
<p><a href="https://httpwg.org/specs/rfc9112.html" rel="noopener noreferrer" target="_blank"><strong>HTTP/1.1</strong></a> — текстовый протокол. Формально в нём был <strong>pipelining</strong>: клиент мог отправить несколько запросов, не дожидаясь ответов. Но ответы всё равно обязаны приходить строго по порядку, что давало <strong>head-of-line (HoL) blocking</strong>; из-за этого браузеры от широкого pipelining практически отказались. Так что «один запрос за раз» — упрощение: точнее, на одном HTTP/1.1-соединении браузеры не делают полноценного независимого мультиплексирования, поэтому параллелизм исторически набирали открытием 6+ TCP-соединений на домен.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc9113" rel="noopener noreferrer" target="_blank"><strong>HTTP/2</strong></a> — бинарный: вместо текстовой request line используются псевдозаголовки (<code>:method</code>, <code>:scheme</code>, <code>:authority</code>, <code>:path</code>, <code>:status</code>) и бинарное фреймирование. Он мультиплексирует множество <strong>streams</strong> в одном TCP-соединении, добавляет сжатие заголовков (<strong>HPACK</strong>) и server push (ныне депрекейтед); заголовка <code>Connection: keep-alive</code> и механизма <code>Transfer-Encoding: chunked</code> в нём уже нет — их роль берёт на себя фреймовый уровень. Но HTTP/2 остаётся уязвим к <strong>TCP-level HoL blocking</strong> — потеря одного пакета тормозит все streams, потому что упорядочивание идёт на уровне TCP.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc9114" rel="noopener noreferrer" target="_blank"><strong>HTTP/3</strong></a> работает поверх <a href="https://datatracker.ietf.org/doc/html/rfc9000" rel="noopener noreferrer" target="_blank"><strong>QUIC</strong></a> — транспорта на UDP вместо TCP. QUIC объединяет транспортный и криптографический handshake (TLS 1.3 встроен внутрь), устанавливая соединение за 1-RTT или даже 0-RTT, а мультиплексирование делает на своём уровне — потеря пакета в одном stream не блокирует остальные, то есть transport-level HoL blocking устранён. Дополнительно QUIC переживает смену сети через <strong>connection migration</strong> по Connection ID: соединение не рвётся при переключении Wi-Fi ↔ LTE.</p>
<p>Для сайта за CDN клиент почти наверняка говорит по HTTP/2 или HTTP/3, а не по «учебному» HTTP/1.1.</p>
<h2 id="сетевой-путь-от-пакета-к-кадру">Сетевой путь: от пакета к кадру</h2>
<h3 id="прохождение-через-сетевой-стек-инкапсуляция">Прохождение через сетевой стек: инкапсуляция</h3>
<p>Данные HTTP-запроса проходят вниз по <a href="https://blog.nicolasmesa.co/posts/2018/08/what-happens-when-you-type-a-url-in-your-browser-and-press-enter/" rel="noopener noreferrer" target="_blank">сетевому стеку</a> через <strong>инкапсуляцию</strong> — последовательное добавление заголовков на каждом уровне.</p>
<p>На <strong>транспортном уровне (L4)</strong> данные разбиваются на <strong>TCP-сегменты</strong>. К каждому добавляется TCP-заголовок: source port (эфемерный порт клиента), destination port (443 для HTTPS), sequence number, acknowledgment number, flags (ACK, PSH и др.), window size, checksum.</p>
<p>На <strong>сетевом уровне (L3)</strong> сегмент инкапсулируется в <a href="https://textbook.cs168.io/routing/ip-header.html" rel="noopener noreferrer" target="_blank"><strong>IP-пакет</strong></a>. IP-заголовок включает: version (4 для IPv4), header length, total length, identification (для сборки фрагментов), flags и fragment offset, <a href="https://networklessons.com/ip-routing/ipv4-packet-header" rel="noopener noreferrer" target="_blank"><strong>TTL (Time To Live)</strong></a> — счётчик, уменьшающийся на каждом роутере, <strong>protocol field</strong> (6 для TCP, 17 для UDP), header checksum, source IP, destination IP.</p>
<p>Каждый роутер на пути декрементирует TTL на 1, и если значение достигает 0, пакет отбрасывается, а отправителю уходит ICMP Time Exceeded. Это предотвращает routing loops — и заодно на этом механизме работает <code>traceroute</code>.</p>
<p>На <a href="https://chessman7.substack.com/p/layer-2-network-operations-how-ethernet" rel="noopener noreferrer" target="_blank"><strong>канальном уровне (L2)</strong></a> IP-пакет инкапсулируется в <a href="https://www.networkacademy.io/ccna/network-fundamentals/data-link-layer" rel="noopener noreferrer" target="_blank"><strong>Ethernet frame</strong></a>. Заголовок содержит destination MAC (6 байт), source MAC (6 байт), EtherType (2 байта, <code>0x0800</code> для IPv4, <code>0x86DD</code> для IPv6). В конце кадра добавляется FCS (Frame Check Sequence) — контрольная сумма для обнаружения ошибок передачи.</p>
<p>Оговорка на весь этот раздел: дальше описан <strong>IPv4 поверх Ethernet</strong> — один распространённый вариант, а не универсальный «низ». В IPv6 структура заголовка другая, нет header checksum, вместо TTL — <strong>Hop Limit</strong>, а маршрутизаторы IPv6 пакеты не фрагментируют (это при необходимости делает источник). Да и физически соединение клиента может идти через Wi-Fi, cellular stack, VPN-туннель, виртуальный интерфейс или вообще loopback — ARP и Ethernet-кадр тогда выглядят иначе или отсутствуют вовсе.</p>
<h3 id="arp-resolution-преобразование-ip-в-mac-адрес">ARP Resolution: преобразование IP в MAC-адрес</h3>
<p>Прежде чем отправить кадр, система должна определить MAC-адрес следующего узла в локальной сети — обычно это адрес default gateway. Для этого используется <a href="https://en.wikipedia.org/wiki/Address_Resolution_Protocol" rel="noopener noreferrer" target="_blank"><strong>ARP (Address Resolution Protocol)</strong></a>.</p>
<p>Система сначала проверяет <a href="https://blogs.oracle.com/linux/arp-internals" rel="noopener noreferrer" target="_blank"><strong>ARP cache</strong></a> — таблицу соответствий IP → MAC в оперативной памяти. Если соответствия нет, отправляется <strong>ARP Request</strong> — широковещательный пакет с destination MAC <code>FF:FF:FF:FF:FF:FF</code>, который получают все устройства в локальной сети. Смысл запроса: «Кто имеет IP 192.168.1.1? Сообщите на MAC AA:BB:CC:DD:EE:FF».</p>
<p>Устройство с этим IP отвечает <a href="https://www.fortinet.com/resources/cyberglossary/what-is-arp" rel="noopener noreferrer" target="_blank"><strong>ARP Reply</strong></a> — unicast-пакетом со своим MAC. Система обновляет ARP cache и отправляет кадр. В Linux запись держится в состоянии reachable около 30 секунд (<code>base_reachable_time</code> с джиттером), потом переходит в stale и перед следующим использованием перепроверяется.</p>
<h3 id="маршрутизация-путь-через-роутеры">Маршрутизация: путь через роутеры</h3>
<p>После определения MAC-адреса кадр передаётся сетевому адаптеру (NIC) через <a href="https://en.wikipedia.org/wiki/Direct_memory_access" rel="noopener noreferrer" target="_blank"><strong>DMA (Direct Memory Access)</strong></a> — NIC напрямую копирует данные из системной памяти без участия CPU.</p>
<p>Кадр попадает в <a href="https://stackoverflow.com/questions/47450231/what-is-the-relationship-of-dma-ring-buffer-and-tx-rx-ring-for-a-network-card" rel="noopener noreferrer" target="_blank"><strong>transmit ring buffer</strong></a> — кольцевой буфер дескрипторов пакетов в оперативной памяти, разделяемый между драйвером и NIC. NIC читает дескрипторы, получает адреса пакетов в памяти и передаёт данные на физический уровень.</p>
<p>Пакет достигает первого роутера (обычно домашнего шлюза), который выполняет <strong>de-encapsulation и re-encapsulation</strong>: снимает Ethernet-заголовок, анализирует IP-заголовок, проверяет <a href="https://en.wikipedia.org/wiki/Routing_table" rel="noopener noreferrer" target="_blank"><strong>routing table</strong></a> для определения next hop, декрементирует TTL, пересчитывает IP checksum и создаёт новый Ethernet-кадр с новыми MAC-адресами.</p>
<p>Процесс повторяется на каждом роутере. Ключевое: <strong>IP-пакет (L3) остаётся неизменным от источника до получателя</strong> (за исключением TTL и checksum), тогда как <strong>Ethernet-кадр (L2) пересоздаётся на каждом hop</strong>. IP-адреса определяют конечную точку назначения, MAC-адреса используются только для доставки кадра на следующий hop.</p>
<p>Routing table содержит записи в формате: network prefix, netmask, next hop, interface. При получении пакета роутер выполняет <a href="https://jumpcloud.com/it-index/what-is-a-forwarding-table" rel="noopener noreferrer" target="_blank"><strong>longest prefix match</strong></a> — находит наиболее специфичное правило, соответствующее destination IP. Если соответствия нет, пакет отправляется через default route (<code>0.0.0.0/0</code>).</p>
<h3 id="bgp-маршрутизация-между-автономными-системами">BGP: маршрутизация между автономными системами</h3>
<p>Longest prefix match объясняет, как роутер выбирает next hop среди известных ему маршрутов, но не откуда берётся сама глобальная карта интернета. Домашний роутер знает лишь default route «всё незнакомое — провайдеру». Дальше пакет попадает в сеть ISP и движется между <strong>автономными системами (AS)</strong> — независимо управляемыми сетями (провайдеры, дата-центры, CDN), каждой из которых присвоен номер <strong>ASN</strong>. Маршруты между AS распространяет <a href="https://datatracker.ietf.org/doc/html/rfc4271" rel="noopener noreferrer" target="_blank"><strong>BGP (Border Gateway Protocol)</strong></a> — протокол, на котором держится весь междоменный роутинг.</p>
<p>BGP-роутеры на границах AS обмениваются анонсами достижимости префиксов (например, «сеть <code>151.101.0.0/16</code> доступна через <code>AS54113</code>») и выбирают путь не по кратчайшей метрике, а по <strong>политикам</strong>: длина AS-path, local preference, коммерческие отношения (peering vs transit). BGP-анонсы и делают возможным Anycast из раздела про CDN: один префикс анонсируется из многих точек, и каждый узел выбирает ближайшую по своим политикам.</p>
<p>Уязвимость модели — доверие к анонсам: ошибочный или злонамеренный анонс чужого префикса (<strong>BGP hijack</strong>) уводит трафик не туда. Классика жанра — 2008 год, когда Pakistan Telecom, пытаясь заблокировать YouTube внутри страны, анонсировал его префикс на весь мир и уронил сервис глобально. Частичная защита — <strong>RPKI</strong> и фильтрация маршрутов. Внутри одной AS работают уже IGP-протоколы (<strong>OSPF</strong>, <strong>IS-IS</strong>), а BGP отвечает именно за стык между AS.</p>
<h2 id="сервер-от-прерывания-до-приложения">Сервер: от прерывания до приложения</h2>
<h3 id="приём-пакета-hardware-interrupt-и-dma">Приём пакета: hardware interrupt и DMA</h3>
<p>Когда пакет достигает NIC сервера, начинается <a href="https://metebalci.com/blog/how-a-nic-rx-works/" rel="noopener noreferrer" target="_blank">обратный процесс</a>. NIC получает электрические сигналы, декодирует их в биты, проверяет FCS и сравнивает destination MAC с собственным адресом.</p>
<p>При успешной проверке NIC через <strong>DMA копирует пакет</strong> из своей внутренней памяти в системную RAM, помещая его в <a href="https://maxnilz.com/docs/004-network/005-linux-rx/" rel="noopener noreferrer" target="_blank"><strong>receive ring buffer</strong></a> — кольцевой массив дескрипторов в kernel space, предварительно выделенный драйвером.</p>
<p>После завершения DMA-передачи NIC генерирует <a href="https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-receiving-data/" rel="noopener noreferrer" target="_blank"><strong>hardware interrupt (IRQ)</strong></a>, сигнализируя CPU, что пакет готов к обработке. Прерывания сетевых карт бывают трёх типов: <strong>MSI-X</strong>, <strong>MSI</strong> и <strong>legacy interrupts</strong>. MSI-X предпочтительнее — позволяет назначить отдельное прерывание каждой receive queue, что улучшает параллелизм на многоядерных системах.</p>
<h3 id="обработка-прерывания-isr-и-переключение-в-ядро">Обработка прерывания: ISR и переключение в ядро</h3>
<p>При получении hardware interrupt процессор переключается в <strong>kernel mode</strong> и сохраняет состояние прерванного контекста — это смена уровня привилегий и сохранение регистров, а не обязательно полноценный context switch планировщика (тот меняет процесс и происходит отдельно). Процессоры поддерживают несколько <a href="https://www.geeksforgeeks.org/operating-systems/difference-between-user-mode-and-kernel-mode/" rel="noopener noreferrer" target="_blank"><strong>privilege levels</strong></a>: <strong>Ring 0 (kernel mode)</strong> — полный доступ ко всем системным ресурсам, и <strong>Ring 3 (user mode)</strong> — доступ только к адресному пространству процесса.</p>
<p>При входе в ядро CPU сохраняет текущее состояние (program counter, stack pointer, регистры, flags). Затем загружается адрес <strong>interrupt handler (ISR)</strong> из interrupt vector table и начинается его выполнение.</p>
<p>Для сетевых карт ISR выполняет минимум работы: подтверждает прерывание, <strong>отключает дальнейшие прерывания от NIC</strong> и планирует обработку пакета на уровне <strong>softirq</strong>. Зачем отключать: если бы NIC продолжал генерировать hardware interrupt на каждый пакет при высоком трафике, система тратила бы всё CPU-время на обработку прерываний, не успевая фактически обрабатывать пакеты — явление, известное как <a href="https://lwn.net/Articles/1008399/" rel="noopener noreferrer" target="_blank"><strong>interrupt storm</strong> или <strong>livelock</strong></a>.</p>
<h3 id="napi-и-softirq-эффективная-batch-обработка">NAPI и SoftIRQ: эффективная batch-обработка</h3>
<p>Для решения проблемы interrupt overload в Linux используется механизм <a href="https://docs.kernel.org/networking/napi.html" rel="noopener noreferrer" target="_blank"><strong>NAPI (New API)</strong></a>. NAPI комбинирует <a href="https://wiki.linuxfoundation.org/networking/napi" rel="noopener noreferrer" target="_blank">interrupt-driven подход с polling</a>: первый пакет генерирует прерывание, после чего драйвер переключается в режим polling для batch-обработки последующих.</p>
<p>ISR вызывает <code>napi_schedule()</code>, которая добавляет NAPI-структуру драйвера в список запланированных softirq и поднимает флаг <strong>NET_RX_SOFTIRQ</strong>. <a href="https://mirror.xyz/fanyadan.eth/OVMiK-NGKk_nYgX5AY75TcGJDZozfL2mYBdcslTnyi8" rel="noopener noreferrer" target="_blank">Softirq</a> — механизм отложенной обработки в ядре, выполняющийся с приоритетом ниже hardware interrupts, но выше обычных процессов.</p>
<p><strong>Softirq processing</strong> происходит в контексте ядра сразу после обработки hardware interrupts или при выходе из системного вызова. Softirq может выполняться <strong>параллельно на нескольких CPU</strong> — на этом держится масштабирование сетевой обработки. Для каждого CPU ядро поддерживает отдельную очередь softirq.</p>
<p>При выполнении NET_RX_SOFTIRQ вызывается <code>net_rx_action()</code>, которая обходит список NAPI-структур и вызывает <code>poll()</code> каждого драйвера. Poll-функция <a href="https://blog.packagecloud.io/illustrated-guide-monitoring-tuning-linux-networking-stack-receiving-data/" rel="noopener noreferrer" target="_blank">извлекает пакеты из receive ring buffer</a> (до определённого budget — обычно 64 пакета), обрабатывает их и передаёт выше по стеку. Дальше — развилка, и её легко описать наоборот. Если драйвер разгрёб <strong>всё доступное до исчерпания budget</strong>, он вызывает <code>napi_complete_done()</code> и снова включает hardware interrupts от NIC. Если же budget исчерпан, а пакеты ещё остались, poll-функция возвращает ровно значение budget — NAPI <strong>не</strong> завершается и планируется повторно, оставаясь в режиме polling. Возвращаться к чисто interrupt-driven обработке в этот момент как раз не нужно: иначе под нагрузкой поток пакетов не разгрести.</p>
<p>Если сервер держит серьёзную нагрузку, тюнинг всего этого хозяйства (размеры очередей, backlog, TCP-параметры) делается через <code>sysctl</code> — по этому поводу есть <a href="/posts/sysctl-hl/">отдельная шпаргалка</a>.</p>
<h3 id="rpsrfs-распределение-нагрузки-по-cpu">RPS/RFS: распределение нагрузки по CPU</h3>
<p>Для масштабирования на многоядерных системах Linux использует <a href="https://docs.kernel.org/networking/scaling.html" rel="noopener noreferrer" target="_blank"><strong>RPS (Receive Packet Steering)</strong> и <strong>RFS (Receive Flow Steering)</strong></a>. RPS — программная реализация RSS (Receive Side Scaling), распределяющая обработку пакетов по разным CPU.</p>
<p>RPS вычисляет хэш от заголовков пакета (source/destination IP и port) и по нему выбирает целевой CPU из настроенного списка. Пакет помещается в <strong>per-CPU backlog queue</strong> этого CPU, и генерируется <strong>IPI (Inter-Processor Interrupt)</strong>, чтобы разбудить обработку на удалённом CPU. Так все пакеты одного потока обрабатываются одним CPU — лучше cache locality.</p>
<p>RFS идёт дальше, пытаясь направить обработку пакета на тот же CPU, где работает приложение, принимающее эти данные. Данные с большой вероятностью уже в CPU cache — latency ниже.</p>
<h3 id="прохождение-через-network-stack-от-ethernet-к-tcp">Прохождение через network stack: от Ethernet к TCP</h3>
<p>Извлечённый из ring buffer пакет представлен в ядре структурой <a href="https://thinkpalm.com/blogs/how-linux-kernel-handles-network-packets/" rel="noopener noreferrer" target="_blank"><strong>sk_buff (socket buffer)</strong></a> — универсальной структурой для сетевых пакетов в Linux. Она содержит указатели на заголовки разных уровней и на сами данные, что позволяет передавать пакет между уровнями стека без копирования.</p>
<p>На <strong>канальном уровне (L2)</strong> ядро проверяет Ethernet-заголовок: сравнивает destination MAC с адресами локальных интерфейсов, проверяет FCS, извлекает EtherType.</p>
<p>На <strong>сетевом уровне (L3)</strong> вызывается <code>ip_rcv()</code>: проверяет версию, валидирует header length и total length, проверяет IP checksum и выполняет routing decision через lookup в routing table. Дальше развилка: если пакет предназначен локальной машине, он идёт в <code>ip_local_deliver()</code> и передаётся на транспортный уровень <strong>без уменьшения TTL</strong>. TTL/Hop Limit декрементируется не потому, что сервер принял пакет для локального сокета, а только на forwarding-пути (<code>ip_forward()</code>), когда узел пересылает пакет дальше — тогда и происходит уменьшение TTL (при TTL=0 пакет отбрасывается с ICMP Time Exceeded), пересчёт checksum и re-encapsulation. Здесь легко механически перенести операцию из forwarding path в общий receive path — но на приёме «для себя» её нет.</p>
<p>Перед передачей на транспортный уровень пакет проходит через <strong>Netfilter hooks</strong> — точки перехвата для firewall-правил (iptables/nftables), NAT и connection tracking. Именно здесь пакет может тихо умереть по правилу, которое кто-то накатил полгода назад — <a href="/posts/iptables-manual/">шпаргалка по iptables</a> в помощь.</p>
<p>На <strong>транспортном уровне (L4)</strong> функция протокола (для TCP — <code>tcp_v4_rcv()</code>) проверяет TCP checksum, выполняет <strong>socket demultiplexing</strong> — поиск соответствующего сокета по 4-tuple (source IP, source port, destination IP, destination port), обновляет состояние TCP-соединения, обрабатывает TCP flags и помещает данные в <strong>socket receive buffer</strong> — очередь, из которой приложение будет читать.</p>
<p>Для TLS-соединений данные всё ещё зашифрованы на этом этапе и будут расшифрованы уже в user space приложением или TLS-библиотекой.</p>
<h3 id="load-balancer-распределение-перед-web-server">Load balancer: распределение перед web server</h3>
<p>У крупного сайта пакет, дошедший до дата-центра, почти никогда не попадает напрямую на процесс web server — сначала он проходит через <a href="https://www.cloudflare.com/learning/performance/what-is-load-balancing/" rel="noopener noreferrer" target="_blank"><strong>балансировщик нагрузки</strong></a>. Различают два уровня.</p>
<p><strong>L4-балансировщик</strong> (транспортный) распределяет по IP и порту, не заглядывая в HTTP, и работает в двух режимах:</p>
<ul>
<li><strong>proxy / in-line</strong> — весь трафик, туда и обратно, идёт через балансировщик, который терминирует TCP;</li>
<li><strong>DSR (Direct Server Return)</strong> — запрос приходит через LB, но ответ backend отправляет клиенту напрямую, минуя балансировщик, что снимает с него обратный (обычно куда более объёмный) трафик.</li>
</ul>
<p><strong>L7-балансировщик / reverse proxy</strong> (прикладной) терминирует TLS, читает HTTP и маршрутизирует по <code>Host</code>/path/cookie, обеспечивает sticky sessions, health checks, retry и circuit breaking (Nginx, HAProxy, Envoy, облачные ALB). Про то, как это настраивается в nginx, у меня есть пост про <a href="/posts/nginx-lb/">балансировку бэкендов</a>, а про архитектуру в целом — <a href="/posts/ha-clustering/">про кластеризацию и доступность</a>.</p>
<p>Алгоритмы распределения — round-robin, least connections, hash по IP или URL, weighted. За балансировщиком стоит пул backend-серверов, и уже выбранный экземпляр проходит путь «сокет → приложение». На практике перед origin выстраивается целая цепочка: <strong>CDN edge → L4 LB → L7 reverse proxy → web server → application server</strong>, и на каждом стыке возможны свои keep-alive пулы соединений и собственный слой кэширования.</p>
<h3 id="web-server-от-сокета-к-приложению">Web Server: от сокета к приложению</h3>
<p>Web server (Nginx, Apache) узнаёт о доступности данных через механизмы <strong>event notification</strong>: <code>epoll</code> (Linux), <code>kqueue</code> (BSD) или <code>select</code>/<code>poll</code>. Они позволяют мониторить тысячи сокетов одновременно и получать уведомления, только когда данные доступны для чтения или записи.</p>
<p>При получении события сервер вызывает <a href="https://man7.org/linux/man-pages/man2/recv.2.html" rel="noopener noreferrer" target="_blank"><code>recv()</code></a> или <code>recvmsg()</code>, который копирует данные из kernel socket buffer в user space buffer приложения. Копирование пересекает границу kernel-user space, что стоит денег. Для минимизации существуют техники zero-copy (<code>sendfile</code>, <code>splice</code>).</p>
<p>Получив запрос, web server <strong>парсит его структуру</strong>: request line (метод, URI, версия HTTP), заголовки, опциональное тело.</p>
<p>Nginx использует <a href="https://nginx.org/en/docs/http/request_processing.html" rel="noopener noreferrer" target="_blank">асинхронную event-driven архитектуру</a> с небольшим числом worker-процессов (обычно по одному на CPU core). Каждый worker обрабатывает множество одновременных соединений в одном потоке через non-blocking I/O и event loop. Так Nginx держит десятки тысяч соединений с минимумом памяти. Apache традиционно использовал <strong>prefork MPM</strong>, где каждый запрос обрабатывается отдельным процессом, или <strong>worker MPM</strong> с thread pool; современные версии поддерживают и <strong>event MPM</strong>, аналогичный nginx.</p>
<p>Дальше web server выполняет <strong>routing</strong> на основе конфигурации: для статических файлов читает файл с диска и отправляет клиенту, для динамического контента перенаправляет запрос к application server (PHP-FPM, Python WSGI, Node.js) через FastCGI, HTTP proxy или другие протоколы. Какой именно <a href="/posts/virt-host-nginx/">server block</a> обработает запрос — определяется по заголовку <code>Host</code>, и это, кстати, частый источник сюрпризов, когда запрос улетает в дефолтный виртуалхост.</p>
<h3 id="обработка-приложением-и-формирование-ответа">Обработка приложением и формирование ответа</h3>
<p>Application server выполняет бизнес-логику: парсинг query-параметров и request body, валидацию и аутентификацию, обращения к базе, вызовы внешних API, рендеринг шаблонов, формирование ответа.</p>
<p>После обработки формируется <strong>HTTP response</strong>: status line (<code>HTTP/1.1 200 OK</code>), заголовки (Content-Type, Content-Length, Cache-Control, Set-Cookie), пустая строка-разделитель, тело ответа.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer" target="_blank"><strong>Cache-Control заголовки</strong></a> управляют <a href="https://web.dev/articles/http-cache" rel="noopener noreferrer" target="_blank">кэшированием на стороне клиента и промежуточных proxy</a>:</p>
<ul>
<li><code>Cache-Control: max-age=31536000, immutable</code> — для статики с версионированием (CSS, JS с hash в имени);</li>
<li><code>Cache-Control: no-cache</code> — для ресурсов, требующих revalidation перед использованием;</li>
<li><code>Cache-Control: no-store</code> — для приватных данных, которые кэшировать нельзя вообще.</li>
</ul>
<p>Response передаётся обратно через <code>send()</code> или <code>sendfile()</code> (для статических файлов), данные копируются в kernel space, TCP-стек сегментирует их, IP-стек инкапсулирует в пакеты, Ethernet-стек создаёт кадры. Пакеты помещаются в <strong>transmit ring buffer</strong>, NIC через DMA читает их и передаёт на физический уровень. Response идёт обратным путём через интернет к клиенту.</p>
<h2 id="браузер-рендеринг-и-отображение">Браузер: рендеринг и отображение</h2>
<h3 id="приём-и-парсинг-http-response">Приём и парсинг HTTP response</h3>
<p>Сервер ответил — мяч снова на стороне клиента. Response-пакеты проходят тот же путь: hardware interrupt, DMA в ring buffer, softirq, network stack, TCP reassembly — и попадают в socket buffer.</p>
<p>Браузерный network thread вызывает <code>recv()</code> для чтения данных. <a href="https://dev.to/bennyxguo/learn-how-the-browser-works-in-practice-http-request-and-parsing-4fdg" rel="noopener noreferrer" target="_blank">Response parser</a> обрабатывает структуру: status line (200, 301, 404, 500), response headers (Content-Type определяет тип контента и способ парсинга, Content-Length — для прогресса загрузки, <code>Transfer-Encoding: chunked</code> — для streaming), тело — собственно HTML, CSS, JavaScript, изображения.</p>
<h3 id="html-parsing-и-построение-dom">HTML Parsing и построение DOM</h3>
<p>Получив HTML, браузер начинает <a href="https://dev.to/ayush_maurya_/how-browsers-parse-and-render-html-from-request-to-paint-1ogp" rel="noopener noreferrer" target="_blank"><strong>parsing</strong></a> — преобразование текстового HTML в DOM (Document Object Model).</p>
<p>Процесс начинается с <strong>tokenization</strong>: парсер читает байты, декодирует их в символы согласно charset (обычно UTF-8), сканирует символы и выделяет токены — start tags (<code>&lt;div&gt;</code>), end tags (<code>&lt;/div&gt;</code>), attributes (<code>class=&quot;container&quot;</code>), text content, comments.</p>
<p>Из токенов строится <strong>DOM tree</strong>: открывающий тег создаёт новый node и становится текущим родителем, текстовое содержимое создаёт text node как дочерний, закрывающий тег возвращает контекст к родителю. DOM tree — иерархическая структура, где каждый HTML-элемент представлен объектом со свойствами, методами и связями parent-child-sibling.</p>
<p>«Стек тегов» — хорошая первая метафора, но реальный <a href="https://html.spec.whatwg.org/multipage/parsing.html" rel="noopener noreferrer" target="_blank">HTML-парсинг</a> сложнее: это токенизатор с большим числом состояний, tree-construction algorithm с <strong>insertion modes</strong>, автоматическое исправление некорректной вложенности, foster parenting для таблиц, adoption agency algorithm для форматирующих тегов и особая обработка parser-inserted скриптов. Именно поэтому браузер способен превратить формально «кривой» HTML в стабильный DOM — иногда совсем не такой, как наивно ожидаешь по разметке.</p>
<p>Во время парсинга браузер обнаруживает ссылки на внешние ресурсы (CSS, JavaScript, images, fonts) и начинает их загрузку параллельно. Для этого используется <strong>preload scanner</strong> — лёгковесный парсер, который сканирует HTML на предмет ресурсов, не дожидаясь основного парсера.</p>
<h3 id="css-parsing-и-cssom">CSS Parsing и CSSOM</h3>
<p>Когда браузер встречает <code>&lt;link rel=&quot;stylesheet&quot;&gt;</code> или <code>&lt;style&gt;</code>, он загружает CSS и начинает парсинг в <a href="https://leapcell.io/blog/how-css-affects-parsing-and-rendering" rel="noopener noreferrer" target="_blank"><strong>CSSOM (CSS Object Model)</strong></a>. CSS-парсер похож на HTML-парсер: tokenization, парсинг селекторов и правил, построение CSSOM tree, где каждый node представляет CSS rule с селектором и объявлениями свойств.</p>
<p><strong>CSS блокирует рендеринг — но не любой.</strong> Браузер не будет отрисовывать страницу, пока не загрузит и не распарсит render-blocking стили из <code>&lt;head&gt;</code>. При этом стиль с неподходящим <code>media</code>-условием (скажем, <code>media=&quot;print&quot;</code>) или помеченный как некритичный первый render не блокирует. В целом это предотвращает FOUC (Flash of Unstyled Content), но замедляет initial render. Поэтому критический CSS инлайнят в <code>&lt;head&gt;</code>, а некритический загружают асинхронно.</p>
<h3 id="javascript-выполнение-и-блокировка">JavaScript: выполнение и блокировка</h3>
<p>JavaScript имеет особое влияние на парсинг и рендеринг — правда, не всякий скрипт одинаково. Классический <strong>parser-inserted</strong> <code>&lt;script&gt;</code> без <code>async</code>/<code>defer</code> действительно <strong>останавливает парсинг HTML</strong>: скрипт может манипулировать DOM (<code>document.write()</code>, создание/удаление элементов), и браузер не знает заранее, что он сделает, поэтому обязан дождаться выполнения. А вот <code>async</code>, <code>defer</code>, module-скрипты и динамически добавленные элементы ведут себя иначе — так что «каждый <code>&lt;script&gt;</code> тормозит парсер» тоже упрощение.</p>
<p>Процесс для блокирующего скрипта: браузер загружает его (если src внешний), компилирует JavaScript в байт-код или machine code (JIT в V8, SpiderMonkey, JavaScriptCore), выполняет, и только после этого возобновляется HTML-парсинг.</p>
<p>Для оптимизации используются атрибуты:</p>
<ul>
<li><code>async</code> — скрипт загружается параллельно и выполняется, как только загрузится, не блокируя парсинг загрузкой (но само выполнение всё равно приостанавливает парсер и не сохраняет порядок относительно соседних async-скриптов);</li>
<li><code>defer</code> — скрипт загружается параллельно, но выполняется только после полного парсинга HTML, перед событием DOMContentLoaded;</li>
<li>module-скрипты по умолчанию ведут себя близко к <code>defer</code>.</li>
</ul>
<h3 id="render-tree-и-layout">Render Tree и Layout</h3>
<p>После построения DOM и CSSOM браузер объединяет их в <a href="https://bytebytego.com/guides/how-does-the-browser-render-a-web-page/" rel="noopener noreferrer" target="_blank"><strong>Render Tree</strong></a> — структуру, содержащую только видимые элементы с их вычисленными стилями. Render Tree не включает элементы с <code>display: none</code>, <code>&lt;head&gt;</code>, <code>&lt;script&gt;</code> и другие невидимые nodes.</p>
<p>Следующий этап — <strong>Layout (reflow)</strong>, где браузер вычисляет точное положение и размеры каждого элемента: расчёт box model (content, padding, border, margin), применение positioning (static, relative, absolute, fixed, sticky), вычисление flexbox и grid, обработку text flow и word wrapping.</p>
<p>Layout — дорогая операция, особенно на сложных страницах. Изменения, влияющие на layout (resize, изменение font-size, добавление/удаление элементов), вызывают reflow всего дерева или его части.</p>
<h3 id="painting-и-compositing">Painting и Compositing</h3>
<p>После Layout следует <strong>Paint</strong> — но, вопреки распространённому упрощению, это ещё не превращение элементов в пиксели. На стадии Paint браузер лишь <strong>строит список команд рисования</strong> (<strong>paint records</strong> / display list): «залить фон», «нарисовать границу», «вывести текст», «наложить тень», применить эффекты (box-shadow, border-radius). Сами пиксели тут ещё не заполняются — Chromium это прямо подчёркивает.</p>
<p>Отдельная стадия — <strong>rasterization (растеризация)</strong>: она исполняет эти команды по тайлам и превращает их в пиксели. Происходить она может позже и на других потоках, включая <strong>GPU raster threads</strong>. Именно поэтому paint и rasterization — разные этапы, а не одно и то же.</p>
<p>Дальше — <strong>compositing</strong>: разделение страницы на слои (layers) и отдельный рендеринг каждого. Элементы с <code>transform</code>, <code>opacity</code>, <code>position: fixed</code>, <code>will-change</code> часто создают отдельные слои. Compositing слоёв делает GPU — отсюда плавные анимации и скроллинг.</p>
<p>Финальный этап — <strong>presentation (display)</strong>: скомпозированный кадр отправляется в графический буфер, который выводится на экран. Условно вся цепочка выглядит так: <code>style calculation → layout → paint / display lists → rasterization → compositing → presentation</code>, причём последующие изменения могут инвалидировать лишь часть стадий, а не пересчитывать всю страницу.</p>
<h3 id="event-loop-и-rendering-pipeline">Event Loop и Rendering Pipeline</h3>
<p>Браузерный <a href="https://webperf.tips/tip/event-loop/" rel="noopener noreferrer" target="_blank"><strong>Event Loop</strong></a> координирует выполнение JavaScript и рендеринг. Цикл работает так: из <strong>task queue</strong> (macrotask queue) берётся одна задача (timer callbacks, I/O, events), задача выполняется до завершения, затем полностью обрабатывается <a href="https://adityaprabhat.hashnode.dev/event-loop-and-asynchrony-in-javascript" rel="noopener noreferrer" target="_blank"><strong>microtask queue</strong></a> (Promise callbacks, MutationObserver), после чего проверяется, нужно ли рендерить.</p>
<p>Важно, что момент обновления экрана (<strong>rendering opportunity</strong>) возникает <strong>отдельно</strong> от обработки задач и не привязан к жёсткому периоду. 16,67 мс — это длительность кадра при 60 Гц, а не фиксированный «такт» event loop: возможность обновить картинку зависит от частоты экрана, видимости вкладки, производительности страницы, throttling и наличия визуальных изменений. Стандарт HTML вообще не требует конкретного алгоритма выбора момента рендера — браузер вправе пропускать обновления или перейти, например, к 30 Гц. Если момент рендера подошёл: выполняются <strong>requestAnimationFrame callbacks</strong>, при необходимости пересчитываются Layout, Paint, rasterization и Compositing.</p>
<p>Если JavaScript-задача выполняется слишком долго (long task &gt; 50 мс), она блокирует Event Loop — рендеринг не происходит, страница перестаёт отвечать. Поэтому тяжёлые операции разбивают на chunks, а фоновые вычисления уносят в Web Workers.</p>
<h2 id="ключевые-прерывания-и-системные-вызовы">Ключевые прерывания и системные вызовы</h2>
<p>Путь пройден целиком — теперь шпаргалка: соберём в одном месте всё, что по дороге дёргало ядро.</p>
<h3 id="hardware-interrupts">Hardware Interrupts</h3>
<p>За время открытия сайта <strong>hardware interrupts</strong> случаются постоянно: <strong>keyboard interrupts</strong> при вводе URL, <strong>network interrupts (IRQ)</strong> при приёме/отправке пакетов, <strong>timer interrupts</strong> — для preemptive multitasking.</p>
<p>Каждое прерывание вызывает вход в обработчик с сохранением и восстановлением состояния CPU и выполнением ISR — но не обязательно полноценный context switch планировщика (смена процесса — отдельное событие). Частые прерывания всё равно создают overhead, поэтому применяют interrupt coalescing (группировка нескольких событий в одно прерывание) и NAPI polling для batch-обработки.</p>
<h3 id="software-interrupts-softirq">Software Interrupts (SoftIRQ)</h3>
<p><strong>Softirq</strong> — механизм отложенной обработки в ядре. Основные типы для сети: <strong>NET_RX_SOFTIRQ</strong> для приёма пакетов (обработка receive ring buffer, передача пакетов вверх по стеку) и <strong>NET_TX_SOFTIRQ</strong> для отправки (завершение передачи, освобождение буферов).</p>
<p>Softirq выполняются в kernel context на высоком приоритете, но ниже hardware interrupts. Они могут прерывать user processes, но не могут прерывать hardware interrupt handlers.</p>
<h3 id="system-calls">System Calls</h3>
<p><strong>System calls</strong> — интерфейс между user space и ядром:</p>
<table>
	<thead>
			<tr>
					<th>Группа</th>
					<th>Вызовы</th>
					<th>Что делают</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Socket operations</td>
					<td><code>socket()</code>, <code>bind()</code>, <code>listen()</code>, <code>accept()</code>, <code>connect()</code></td>
					<td>создание сокета, привязка к адресу, приём и инициация соединений</td>
			</tr>
			<tr>
					<td>Data transfer</td>
					<td><code>send()</code>, <code>sendto()</code>, <code>sendmsg()</code>, <code>recv()</code>, <code>recvfrom()</code>, <code>recvmsg()</code></td>
					<td>отправка и приём данных</td>
			</tr>
			<tr>
					<td>File operations</td>
					<td><code>read()</code>, <code>write()</code>, <code>open()</code>, <code>close()</code></td>
					<td>чтение/запись (работают и с сокетами), работа с дескрипторами</td>
			</tr>
			<tr>
					<td>I/O multiplexing</td>
					<td><code>select()</code>, <code>poll()</code>, <code>epoll()</code></td>
					<td>мониторинг множества файловых дескрипторов</td>
			</tr>
	</tbody>
</table>
<p>Каждый system call вызывает переключение из user mode в kernel mode: процессор переходит в <a href="https://unix-learn.moabukar.co.uk/kernel/kernel-privilege-levels/" rel="noopener noreferrer" target="_blank">Ring 0</a>, выполняет операцию в kernel space, копирует результаты обратно в user space и возвращается в Ring 3. Это <strong>не</strong> полноценный context switch планировщика и не обязательно полный TLB flush: на x86 с включённым Page Table Isolation вход в ядро может сопровождаться сменой таблиц страниц и лишней работой с TLB, но зависит это от архитектуры, поддержки PCID и включённых защит от уязвимостей — универсального правила «syscall → полный TLB flush» нет. Но и «дешёвыми» эти переключения не назвать, поэтому оптимизация часто сводится к минимизации числа system calls. Посмотреть, что именно процесс дёргает у ядра, можно через <a href="/posts/strace/"><code>strace</code></a> — очень отрезвляющее упражнение.</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<p>Главная ценность этой карты не в том, чтобы пересказать её на собеседовании, а в том, чтобы по симптому сразу понимать, на каком этаже искать. Вот примерное соответствие:</p>
<table>
	<thead>
			<tr>
					<th>Симптом</th>
					<th>Этаж</th>
					<th>Чем смотреть</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Долгая пауза до начала загрузки</td>
					<td>DNS resolution</td>
					<td><code>dig +trace example.com</code>, <code>resolvectl query</code></td>
			</tr>
			<tr>
					<td>Соединение висит на connect</td>
					<td>TCP handshake, файрвол</td>
					<td><code>tcpdump -ni any port 443</code>, <code>ss -tan</code>, <a href="/posts/iptables-manual/">iptables</a></td>
			</tr>
			<tr>
					<td>Мелкие запросы идут, крупные виснут</td>
					<td>PMTU black hole</td>
					<td><code>ping -M do -s 1472</code>, проверка ICMP на пути</td>
			</tr>
			<tr>
					<td>Высокий TTFB при быстрой сети</td>
					<td>backend, БД, application server</td>
					<td>логи с временем ответа (<a href="/posts/nginx-json-logs/">nginx в JSON</a>)</td>
			</tr>
			<tr>
					<td>Скачет latency под нагрузкой</td>
					<td>congestion control, очереди ядра</td>
					<td><code>ss -ti</code>, <code>netstat -s</code>, <a href="/posts/sysctl-hl/">тюнинг sysctl</a></td>
			</tr>
			<tr>
					<td>Запрос улетел не в тот бэкенд</td>
					<td>L7-роутинг, <code>Host</code>, <a href="/posts/virt-host-nginx/">server blocks</a></td>
					<td><code>curl -v -H 'Host: ...'</code>, логи балансировщика</td>
			</tr>
			<tr>
					<td>Страница загрузилась, но «тупит»</td>
					<td>Event Loop, long tasks, reflow</td>
					<td>вкладка Performance в DevTools</td>
			</tr>
			<tr>
					<td>Сеть «медленная» без конкретики</td>
					<td>пропускная способность канала</td>
					<td><a href="/posts/iperf3/"><code>iperf3</code></a> между узлами</td>
			</tr>
	</tbody>
</table>
<p>Эта же карта работает и в обратную сторону — когда систему надо не чинить, а спроектировать: примерно так я разбирал <a href="/posts/interview-task-0003/">задачку с собеса про высоконагруженный сервис</a>.</p>
<p>Порядок диагностики — сверху вниз по стеку, а не наугад: сначала убеждаешься, что имя резолвится, потом что соединение устанавливается, потом что сервер отвечает, и только потом лезешь в приложение. Половина «загадочных» инцидентов разваливается уже на первых двух шагах.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong>ICMP не «лишний трафик».</strong> Заблокировав его целиком «для безопасности», ты ломаешь Path MTU Discovery и получаешь соединения, которые устанавливаются, но зависают на крупных ответах. Симптом максимально неочевидный.</li>
<li><strong>TTL кэша DNS живёт дольше, чем ты думаешь.</strong> Между твоим изменением записи и реальным переключением трафика стоят кэши браузера, ОС, резолвера провайдера и промежуточных нод. Снижай TTL заранее, до миграции, а не в момент.</li>
<li><strong>HSTS липкий, но не необратимый.</strong> Отдал заголовок с большим <code>max-age</code> — и браузеры пользователей будут ходить только по HTTPS всё это время, даже если у тебя сломался сертификат. Обычную динамически полученную политику можно снять, прислав <code>Strict-Transport-Security: max-age=0</code>; а вот попадание в browser <strong>preload list</strong> — это уже надолго и откатывается медленно. Тестируй с маленьким <code>max-age</code>.</li>
<li><strong><code>301</code> прилипает надолго.</strong> Permanent-редирект браузеры кэшируют агрессивно (иногда эвристически, без явного срока), и ошибочный <code>301</code> потом тяжело выковырять из кэшей пользователей — управлять им со своей стороны уже почти нельзя. Пока не уверен — отдавай <code>302</code>.</li>
<li><strong>Первые ~14 КБ дешевле остального.</strong> Из-за initial window (обычно 10 сегментов) первая порция ответа приходит за один round-trip, а всё сверх того требует дополнительных RTT на «раскачку». Это грубая performance-интуиция, а не физический закон: размер считается в сегментах и зависит от MSS, на уже прогретом соединении cwnd другой, а QUIC вообще использует свой congestion control. Но вывод практичен: критический CSS в <code>&lt;head&gt;</code> — не мода, а арифметика.</li>
<li><strong>HTTP/2 не лечит потери пакетов.</strong> Мультиплексирование в одном TCP-соединении означает, что одна потеря тормозит все streams разом. На плохих каналах HTTP/2 может оказаться хуже HTTP/1.1 с несколькими соединениями — вот от этого и лечит QUIC.</li>
<li><strong>Каждый редирект — это RTT.</strong> Цепочка <code>http</code> → <code>https</code> → <code>www</code> на мобильном канале с RTT 150 мс стоит почти полсекунды ещё до того, как придёт первый байт HTML.</li>
<li><strong>Эфемерные порты кончаются.</strong> На балансировщике или прокси с большим числом исходящих соединений диапазон <code>net.ipv4.ip_local_port_range</code> и <code>TIME_WAIT</code> становятся реальным потолком. Проявляется внезапно и под нагрузкой.</li>
</ul>
<h2 id="заключение">Заключение</h2>
<p>От первого hardware interrupt до последнего пикселя — каждый этап этого пути вылизан десятилетиями инженерной работы. Но полезен он не как энциклопедическая справка, а как карта для дебага: когда понимаешь, где живут DNS resolution, TLS handshake, softirq и reflow, «сайт тормозит» перестаёт быть магией. У каждой проблемы появляется конкретный этаж, на котором её надо искать: где-то поможет <code>tcpdump</code>, где-то — вкладка Performance в DevTools, а где-то — просто <a href="/posts/wat-monitoring/">нормальный мониторинг</a>, который покажет проблему до того, как о ней напишут в поддержку.</p>
<p>Если хочется превратить эту карту в план обучения, а не в разовое чтение, — те же этажи разложены по уровням и ожиданиям от инженера в листах <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/networking/" rel="noopener noreferrer" target="_blank">Networking</a> и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/operating-systems/" rel="noopener noreferrer" target="_blank">Operating Systems</a> карты компетенций.</p>
<p>И в следующий раз, когда страница откроется «мгновенно», ты будешь знать, сколько всего успело случиться за эти пару сотен миллисекунд.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Как облачная синхронизация тихо ломает git и как это чинить</title>
      <link>https://jtprog.ru/posts/cloud-sync-breaks-git/</link>
      <pubDate>Mon, 13 Jul 2026 12:00:00 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/cloud-sync-breaks-git/</guid>
      <category>Git</category>
      <category>DevOps</category>
      <category>Backup</category>
      <category>Troubleshooting</category>
      <category domain="category">DevOps</category>
      <enclosure url="https://jtprog.ru/cover.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Обычный день, обычный <code>git push</code>. И вдруг вместо привычного «всё улетело на remote» терминал выдаёт:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>error: garbage at end of loose object &#39;2699e4e6…&#39;
fatal: loose object 2699e4e6… is corrupt</code></pre>
</div>
<p>Первая мысль — «ну ладно, запущу <code>git gc</code>, он приберётся». Запускаешь. <code>gc</code> падает ровно с той же ошибкой. Теперь у тебя не пушится и не собирается мусор — репозиторий выглядит как труп.</p>
<p>Спойлер: он живой. Данные целы, всё чинится за десять минут — но не той командой, на которую думаешь первым делом. <code>git gc</code> тебе тут не друг, а первый, кто спотыкается о поломку. А ещё, пока я разгребал эту историю, вскрылась вторая беда, никак не связанная с первой на первый взгляд: <code>.git</code> раздулся на сотни мегабайт мусором, который сидел в истории годами. Так что рецептов будет два.</p>
<p>Главный тезис статьи: <strong>порча объектов git — это не потеря данных, если жив origin. А раздутый <code>.git</code> лечится только переписью истории, и никак иначе.</strong> Разберём оба случая до винтика — с теорией, диагностикой и командами, которые можно копировать.</p>
<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<ul>
<li><strong>Если ты держишь репозиторий под облачной синхронизацией</strong> (iCloud, Dropbox, OneDrive, Яндекс.Диск — неважно) — узнаешь, почему это тикающая бомба под <code>.git/objects</code> и как разминировать уже случившийся взрыв.</li>
<li><strong>Если у тебя раздулся <code>.git</code></strong> и <code>git gc</code> не помогает — получишь рабочий рецепт чистки истории через <code>git-filter-repo</code>, вместе с обязательной страховкой, чтобы не потерять репозиторий по дороге.</li>
<li><strong>Если просто хочешь понять, что такое loose-объект</strong> и почему git вообще способен «побиться» — тут есть теория zlib-сжатия и внутренностей Git LFS, без которой команды выглядят как шаманство.</li>
</ul>
<h2 id="tldr-если-чинить-надо-прямо-сейчас">TL;DR (если чинить надо прямо сейчас)</h2>
<p>Порча loose-объектов (<code>garbage at end of loose object … is corrupt</code>):</p>
<ol>
<li>Не пытайся чинить битый объект на месте — garbage-байты неустранимы.</li>
<li><code>git reset --mixed origin/main</code> — вернуть HEAD на здоровую базу с remote, рабочие файлы не трогаем.</li>
<li><code>git add -A &amp;&amp; git commit</code> — пересобрать свои правки одним чистым коммитом, мимо битых блобов.</li>
<li><code>rm</code> битых объектов → <code>git reflog expire --expire=now --all</code> → <code>git gc --prune=now</code>. <strong>Порядок именно такой.</strong></li>
<li><code>git push</code> — обычный fast-forward, <code>--force</code> не нужен.</li>
</ol>
<p>Раздутый <code>.git</code> (балласт в истории):</p>
<ol>
<li><code>git bundle create backup.bundle --all</code> — страховка ПЕРЕД любой переписью.</li>
<li><code>git filter-repo --invert-paths --path &lt;мусорный путь&gt;</code> — вырезать мусор из всей истории.</li>
<li><code>git gc --prune=now &amp;&amp; git lfs prune</code> → вернуть <code>remote</code> → <code>git push --force</code>.</li>
<li>Переклонировать репозиторий на всех устройствах свежим <code>git clone</code>.</li>
</ol>
<p>А теперь по порядку и с объяснениями.</p>
<h2 id="симптомы-что-видно-в-терминале">Симптомы: что видно в терминале</h2>
<p>Полный набор ошибок, который сыпется в таком инциденте, выглядит примерно так:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>error: garbage at end of loose object &#39;2699e4e6…&#39;
fatal: loose object 2699e4e6… is corrupt
remote: fatal: early EOF
remote: index-pack failed
fatal: failed to run repack</code></pre>
</div>
<p>Важно уметь читать эту простыню, потому что тут причина и следствие вперемешку:</p>
<ul>
<li><code>garbage at end of loose object</code> и <code>loose object … is corrupt</code> — <strong>это и есть корень.</strong> Локальный объект в <code>.git/objects</code> побит.</li>
<li><code>remote: early EOF</code> / <code>index-pack failed</code> — <strong>это следствие.</strong> Ты пытался запушить, git начал слать пак-файл, но подавился на битом объекте и оборвал поток. До remote порча не доехала — сервер просто получил обрубленные данные и отказался их принимать. Это хорошая новость: на origin всё цело.</li>
<li><code>fatal: failed to run repack</code> — это уже <code>git gc</code> спотыкается о тот же самый битый объект, когда пытается перепаковать локальную базу.</li>
</ul>
<blockquote><p><strong>Главный аргумент:</strong> поломка локальная. Origin здоров, рабочие файлы на диске здоровы. Побит только промежуточный слой — объектная база в <code>.git</code>. Именно это делает починку без потери данных возможной.</p>
</blockquote><h2 id="что-такое-loose-объект-и-почему-garbage">Что такое loose-объект и почему «garbage»</h2>
<p>Чтобы команды дальше не выглядели как заклинания, нужно понимать, что именно побилось.</p>
<p>Git хранит содержимое в объектах. Пока объект «свежий» и ещё не упакован в pack-файл, он лежит отдельным файлом — это и есть <strong>loose-объект</strong>, по пути <code>.git/objects/XX/YYYY…</code>, где <code>XX</code> — первые два символа SHA-1, а <code>YYYY…</code> — остальные 38. Внутри такого файла лежит содержимое, <strong>сжатое zlib</strong> (deflate), с коротким заголовком вида <code>blob &lt;размер&gt;\0</code> перед сжатием.</p>
<p>Ключевой факт, из которого растёт вся диагностика: <strong>сжатый объект обязан быть меньше исходника.</strong> Это же компрессия — на то она и нужна. Даже плохо сжимаемый бинарник zlib ужмёт хотя бы чуть-чуть, а текст — в разы.</p>
<p>Поэтому первый же трюк для поиска порчи — просто посмотреть на размер файла:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ls -la .git/objects/26/99e4…</span></span></code></pre></div>
</div>
<p>Если loose-объект на диске <strong>в разы больше</strong> исходного файла, который он должен хранить, — всё, он раздут мусором. Внутрь zlib-потока налезли лишние байты, которых там быть не должно. Именно об этом git и говорит: <code>garbage at end of loose object</code> — «после валидного сжатого потока идёт мусор». Распаковщик zlib дошёл до конца легитимных данных, а файл ещё не кончился — дальше идёт то, что git честно называет garbage.</p>
<h2 id="откуда-берётся-порча">Откуда берётся порча</h2>
<p>Причина №1, и в моём случае главная — <strong>одновременная запись в <code>.git/objects</code> с нескольких устройств через облачную синхронизацию.</strong> Представь: у тебя репозиторий (в моём случае — <a href="/posts/inside-my-obsidian/">целое хранилище заметок</a>) лежит в папке, которую синхронизирует облако — iCloud, Dropbox, что угодно. На одном устройстве настроен авто-commit/push, оно пишет в <code>.git/objects</code>. В этот же момент облачный клиент тянет свою версию тех же файлов с другого устройства и кладёт поверх. Git писал объект атомарно, но синхронизатор про атомарность git ничего не знает — он видит два конфликтующих состояния одного файла и склеивает их по-своему. Результат — loose-объект, у которого к валидному zlib-потоку прилеплен хвост от другой версии. Тот самый garbage.</p>
<p>Облачные хранилища не проектировались под то, чтобы под ними жила объектная база git с конкурентной записью. Для них <code>.git/objects/ab/cdef…</code> — обычный файл, а не часть транзакционной базы данных. Отсюда и мораль (к ней вернёмся в профилактике): <code>.git</code> под живой облачной синхронизацией с нескольких устройств — это гонка, которая рано или поздно порвёт объект.</p>
<p>Причина №2 — <strong>бинарники, закоммиченные сырьём мимо Git LFS.</strong> Тут стоит на минуту нырнуть во внутренности LFS, потому что без этого диагностика ниже не сложится.</p>
<p>Git LFS (Large File Storage) работает как подмена через clean/smudge-фильтры. Когда файл подпадает под правило в <code>.gitattributes</code>, при коммите LFS перехватывает его (clean-фильтр) и вместо самого бинарника кладёт в историю крошечный текстовый <strong>указатель</strong>:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>version https://git-lfs.github.com/spec/v1
oid sha256:4d7a2…
size 1048576</code></pre>
</div>
<p>Сам бинарник уезжает в отдельное LFS-хранилище, а в git-истории живёт вот эта табличка на три строки. При checkout smudge-фильтр делает обратное — подтягивает бинарник по <code>oid</code>. Изящно и легковесно.</p>
<p>А теперь ломаем: если устройство коммитит <strong>без работающего LFS</strong> (плагин авто-бэкапа не умеет LFS, фильтр не настроен, <code>.gitattributes</code> не подхватился), то бинарник уходит в историю <strong>как есть</strong> — целиком, сырыми байтами. Большой, плохо сжимаемый, лежит loose-объектом. И вот такой жирный loose-объект под конкурентной записью через облако бьётся особенно охотно — просто потому, что он большой и запись его дольше.</p>
<h2 id="диагностика">Диагностика</h2>
<p>Прежде чем чинить, надо понять масштаб: какие объекты побиты и чему они соответствуют.</p>
<p><strong>Найти битые объекты:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git fsck --full</span></span></code></pre></div>
</div>
<p><code>fsck</code> (file system check) пройдёт по всей объектной базе и покажет <code>corrupt</code>, <code>missing</code> и прочие проблемы. Он же вывалит кучу <code>dangling</code> — это недостижимые, но валидные объекты (нормальный побочный продукт работы git), они шумят и к порче отношения не имеют. Отфильтруем:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git fsck --full <span class="p">|</span> grep -vE <span class="s2">&#34;dangling&#34;</span></span></span></code></pre></div>
</div>
<p>Остаётся то, что реально болит — строки про corrupt-объекты с их хешами.</p>
<p><strong>Понять, какому файлу соответствует битый блоб:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git ls-tree -r -l &lt;commit&gt; <span class="p">|</span> grep -E <span class="s2">&#34;&lt;hash1&gt;|&lt;hash2&gt;&#34;</span></span></span></code></pre></div>
</div>
<p><code>ls-tree -r -l</code> разворачивает дерево коммита рекурсивно с размерами и хешами блобов. Грепом по хешам битых объектов находишь имя файла — теперь ты знаешь, что именно побилось (у меня это оказались PDF, которые уехали в историю мимо LFS).</p>
<p><strong>Проверить, совпадает ли рабочий файл с тем, что записано:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git hash-object <span class="s2">&#34;&lt;path&gt;&#34;</span></span></span></code></pre></div>
</div>
<p>Тонкость: в репозитории с настроенным LFS <code>git hash-object</code> прогонит файл через clean-фильтр и выдаст хеш <strong>LFS-указателя</strong>, а не самого бинарника. Если ты этого не ждёшь — легко запутаться. Это не баг, это ровно тот самый механизм подмены из предыдущего раздела.</p>
<p><strong>Посмотреть, что лежит на здоровом origin:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git cat-file -p origin/main:<span class="s2">&#34;&lt;path&gt;&#34;</span></span></span></code></pre></div>
</div>
<p>Для файла под LFS здесь ты должен увидеть те самые 133 байта указателя <code>version https://git-lfs.github.com/spec/v1 …</code>. Если на origin лежит корректный указатель, а локально — раздутый бинарник, диагноз окончательный: локальная база побита, remote здоров, лечим сбросом на него.</p>
<h2 id="рецепт-1-починка-порчи-без-потери-данных">Рецепт 1: починка порчи без потери данных</h2>
<p>Соблазн — начать «чинить» битый объект: распаковать, обрезать хвост, запаковать обратно. Забудь. Garbage-байты неотделимы от валидных без гадания, где кончается одно и начинается другое. Реанимация битого объекта — тупиковый путь.</p>
<p>Правильная идея другая и куда проще: <strong>не чини битый объект — обойди его.</strong> Сбрось ветку на здоровый <code>origin</code> и пересобери своё рабочее дерево одним чистым коммитом. Это работает, потому что сходятся три вещи: на remote история цела, твои рабочие файлы на диске целы, а LFS при новом коммите заново обработает бинарники правильно. Битым объектам после такого сброса просто перестаёт кто-либо адресоваться — они становятся мусором, который остаётся физически удалить.</p>
<p>И сразу про то, <strong>почему <code>git gc</code> бесполезен как первая реакция.</strong> Интуиция «что-то побилось — запущу сборку мусора» здесь обманывает. <code>gc</code> первым делом идёт перепаковывать объекты — и натыкается на тот самый битый, падая с <code>failed to run repack</code>. Инструмент, которым ты собрался чинить, сам спотыкается о поломку. Сначала надо убрать битьё из-под ног, и только потом звать <code>gc</code>.</p>
<p>Полная последовательность:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. HEAD → здоровая база с remote. --mixed НЕ трогает рабочее дерево:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    все твои файлы на диске остаются как есть, сбрасывается только указатель ветки и индекс.</span>
</span></span><span class="line"><span class="cl">git reset --mixed origin/main
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. (опционально) поправить рабочее дерево — удалить/восстановить что нужно</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Пересобрать реальные правки заново, одним чистым коммитом — мимо битых блобов.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    Здесь же корректно отработает LFS, если он настроен.</span>
</span></span><span class="line"><span class="cl">git add -A <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;Пересборка рабочего дерева после порчи .git&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Физически удалить битые loose-объекты. После reset на них никто не ссылается.</span>
</span></span><span class="line"><span class="cl">rm <span class="s2">&#34;.git/objects/26/99e4…&#34;</span> <span class="s2">&#34;.git/objects/9a/06ce…&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. Выкинуть битые коммиты из reflog, чтобы они стали недостижимы</span>
</span></span><span class="line"><span class="cl">git reflog expire --expire<span class="o">=</span>now --all
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 6. Теперь перепаковка не спотыкается — мусор недостижим и удалён</span>
</span></span><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 7. Проверка — должно быть чисто</span>
</span></span><span class="line"><span class="cl">git fsck --full
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 8. Обычный fast-forward. force НЕ нужен: мы не переписывали общую историю,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    а до-собрали её поверх здорового origin.</span>
</span></span><span class="line"><span class="cl">git push origin main</span></span></code></pre></div>
</div>
<p>Отдельно подчеркну про <strong>порядок шагов 4–6</strong>, потому что переставить их местами — значит снова получить падение:</p>
<ol>
<li>Сначала <code>reflog expire</code> — пока reflog помнит битые коммиты, они «достижимы», и <code>gc</code> их не тронет (а <code>rm</code> без этого оставит висячие ссылки).</li>
<li>Потом <code>rm</code> битых блобов — убираем сами испорченные файлы с диска.</li>
<li>И только потом <code>gc</code> — ему больше не обо что спотыкаться, вся недостижимая труха вычищается штатно.</li>
</ol>
<p>Если запустить <code>gc</code> до того, как битьё стало недостижимым, — он снова упадёт на нём же. Дисциплина порядка тут не педантизм, а условие того, что рецепт вообще сработает.</p>
<p>После этого <code>git fsck --full</code> показывает чистоту, <code>push</code> проходит обычным fast-forward. Данные на месте: рабочие файлы ты не трогал ни разу, а история пересобралась поверх здоровой базы.</p>
<h2 id="вторая-беда-раздутый-git">Вторая беда: раздутый .git</h2>
<p>Пока я разбирался с порчей, вскрылось, что <code>.git</code> неприлично разбух — сотни мегабайт на репозитории, где рабочих файлов от силы на десятки. Причина оказалась не связана с порчей напрямую, но поучительна.</p>
<p>Балласт сидел <strong>в истории</strong>. Когда-то в репозиторий закоммитилась машинно-генерируемая папка плагина <code>.obsidian/plugins/vault-full-statistics</code> — индекс, кэш, что-то в этом духе. Потом её убрали из HEAD и добавили в <code>.gitignore</code>. В рабочем дереве её больше нет, <code>git status</code> чист. Но <strong>в истории она осталась навсегда</strong> — каждый её старый снапшот всё ещё лежит в объектной базе и честно занимает место. Плюс те самые PDF, уехавшие сырьём мимо LFS, — они тоже раздували <code>.git</code>, а не LFS-хранилище.</p>
<p>И вот тут легко забуксовать:</p>
<blockquote><p><strong>Главный аргумент:</strong> <code>git gc</code> и <code>git lfs prune</code> историю не переписывают. Они убирают недостижимое и осиротевшее, но объект, который достижим из старого коммита в истории, — не мусор с их точки зрения. Он «нужен». Пока он в истории, ни <code>gc</code>, ни <code>lfs prune</code> его не тронут.</p>
</blockquote><p>То есть балласт, вшитый в историю, стандартными средствами не убрать в принципе. Нужна <strong>перепись истории</strong> — операция, которая физически перестраивает коммиты так, будто мусорных путей там никогда не было.</p>
<h2 id="рецепт-2-чистка-истории-через-git-filter-repo">Рецепт 2: чистка истории через git-filter-repo</h2>
<p>Инструмент для переписи — <a href="https://github.com/newren/git-filter-repo" rel="noopener noreferrer" target="_blank"><code>git-filter-repo</code></a> (официальная замена устаревшему <code>git filter-branch</code>, быстрее и безопаснее). Порядок такой.</p>
<p><strong>Сначала найти, что вообще весит.</strong> Топ тяжёлых объектов в истории с путями:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rev-list --objects --all <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  git cat-file --batch-check<span class="o">=</span><span class="s1">&#39;%(objecttype) %(objectname) %(objectsize:disk) %(rest)&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  awk <span class="s1">&#39;/^blob/ {print $3, $2, substr($0, index($0,$4))}&#39;</span> <span class="p">|</span> sort -rn <span class="p">|</span> head -20</span></span></code></pre></div>
</div>
<p>Разберём конвейер: <code>rev-list --objects --all</code> перечисляет все объекты во всей истории вместе с их путями; <code>cat-file --batch-check</code> дописывает каждому тип, хеш, размер на диске и путь; <code>awk</code> оставляет только блобы и переставляет поля; <code>sort -rn | head -20</code> даёт двадцатку самых жирных. Ты сразу видишь, какие пути жрут место.</p>
<p><strong>Обязательная страховка ПЕРЕД переписью.</strong> Перепись истории необратима — сделай полный bundle всего репозитория, это твоя точка отката:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bundle create /tmp/repo-backup-<span class="k">$(</span>git rev-parse --short HEAD<span class="k">)</span>.bundle --all</span></span></code></pre></div>
</div>
<p>Из такого bundle репозиторий разворачивается целиком (<code>git clone repo-backup.bundle</code>), если что-то пойдёт не так.</p>
<p><strong>Собственно перепись</strong> — вырезаем мусорные пути из всей истории:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install git-filter-repo   <span class="c1"># или pip install git-filter-repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git filter-repo --invert-paths <span class="se">\
</span></span></span><span class="line"><span class="cl">  --path <span class="s2">&#34;.obsidian/plugins/vault-full-statistics/&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --path <span class="s2">&#34;_Система/2. Статика/pz-3_zadacha_koshi_dlja_uravnenija_teploprovodnos.pdf&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --force</span></span></code></pre></div>
</div>
<p><code>--invert-paths</code> инвертирует смысл <code>--path</code>: перечисленное — не «оставить только это», а «<strong>удалить это</strong> из всей истории». Каждый коммит пересобирается так, будто указанных путей там не было.</p>
<p><strong>После переписи</strong> — прибраться и вернуть remote:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now       <span class="c1"># теперь недостижимые старые объекты реально удаляются</span>
</span></span><span class="line"><span class="cl">git lfs prune            <span class="c1"># сбросить осиротевшие LFS-блобы из локального кэша</span>
</span></span><span class="line"><span class="cl"><span class="c1"># filter-repo НАРОЧНО удаляет remote — защита от случайного push. Возвращаем:</span>
</span></span><span class="line"><span class="cl">git remote add origin git@github.com:&lt;user&gt;/&lt;repo&gt;.git
</span></span><span class="line"><span class="cl">git push --force origin main</span></span></code></pre></div>
</div>
<p>Обрати внимание на два момента. Первый: <code>filter-repo</code> сам удаляет <code>origin</code> после работы — это не баг, а предохранитель, чтобы ты случайно не запушил переписанную историю до того, как всё проверил. Второй: <code>push --force</code> тут неизбежен — история переписана, fast-forward невозможен по определению.</p>
<p>По порядку величин на моём репозитории вышло примерно так: <code>.git</code> ужался с ~560 МБ до ~317 МБ. Основную долю дали два куска — вырезанная папка плагина (сама объектная база просела почти вдвое) и очистка LFS-кэша от сирот после PDF. Цифры у тебя будут свои, но масштаб «в полтора-два раза» — вполне типичный, если в истории годами копился генерируемый мусор.</p>
<h2 id="критично-после-переписи-истории">Критично после переписи истории</h2>
<p>Тут кроется грабля, на которую наступают все, кто впервые переписывает историю.</p>
<p><strong>Перепись меняет все SHA коммитов.</strong> Раз содержимое коммитов пересобрано, их хеши другие — а значит, старые клоны репозитория несовместимы с новой историей от слова совсем. И если у тебя, как у меня, есть автоматический бэкап (сервер, второе устройство, cron с <code>git push</code>), который держит <strong>старый</strong> клон, произойдёт вот что: при следующем автопуше он вернёт старую историю со всем мусором обратным push&rsquo;ем. Вся твоя чистка обнулится.</p>
<p>Поэтому железное правило: <strong>до следующего автобэкапа/автопуша переклонировать репозиторий заново на всех устройствах.</strong> Именно <code>git clone</code> начисто, а не <code>git pull</code> — pull попытается смёржить несовместимые истории и сделает только хуже. Снёс старый клон, склонировал свежий — и только тогда включай обратно автоматику.</p>
<p>И ещё одно про квоту: <strong>LFS-хранилище на GitHub перепись истории НЕ освобождает.</strong> Объекты остаются в remote-LFS, даже если ты вырезал ссылки на них из истории. Если тебе важна именно квота LFS — это отдельная операция (<code>git lfs</code> со стороны remote-инструментов провайдера), не путать с чисткой <code>.git</code>.</p>
<h2 id="профилактика">Профилактика</h2>
<p>Всё вышеописанное лечит последствия. Но правильнее — не доводить. Три правила, каждое из которых закрывает конкретную причину из разбора:</p>
<ul>
<li><strong>Не держи <code>.git</code> под одновременной записью с нескольких устройств через облако.</strong> Это корень порчи loose-объектов. Пусть коммитить и пушить будет что-то <strong>одно</strong> — одно устройство или один сервер с настроенным LFS. Остальные пусть только читают/синхронизируют рабочие файлы, но не воюют за <code>.git/objects</code>.</li>
<li><strong>Бинарники — через Git LFS, а не сырьём в историю.</strong> Настрой <code>.gitattributes</code> и убедись, что LFS реально работает на каждом устройстве, которое коммитит. Один коммит мимо LFS — и жирный объект уже в истории навсегда (см. рецепт 2).</li>
<li><strong>Машинно-генерируемые папки — в <code>.gitignore</code> с самого начала.</strong> Индексы, кэши плагинов, артефакты сборки — им не место в истории. Их дёшево перегенерировать и дорого потом вырезать переписью.</li>
</ul>
<h2 id="заключение">Заключение</h2>
<p>Если свести всё к паре мыслей, которые стоит унести:</p>
<ul>
<li><strong>git ломается тихо и чинится не тем, чем кажется.</strong> <code>garbage at end of loose object</code> выглядит как приговор, а <code>git gc</code> — как очевидное лекарство. На деле объект побит локально, а <code>gc</code> — первый, кто об него спотыкается.</li>
<li><strong>Порча объектов — это не потеря данных, пока жив origin.</strong> Не реанимируй битьё — сбрось ветку на здоровую базу и пересобери рабочее дерево одним коммитом. Данные в рабочих файлах и на remote к порче объектной базы не имеют отношения.</li>
<li><strong>Раздутый <code>.git</code> лечится только переписью истории.</strong> <code>gc</code> и <code>lfs prune</code> не трогают то, что достижимо из старых коммитов. Балласт из истории убирает <code>git-filter-repo</code> — и то под страховкой bundle и с обязательным переклонированием после.</li>
</ul>
<h3 id="что-сделать-прямо-сейчас">Что сделать прямо сейчас</h3>
<ol>
<li>Проверь, не синхронизируется ли <code>.git</code> твоего репозитория через облако с нескольких устройств. Если да — оставь коммиты за одним источником.</li>
<li>Убедись, что Git LFS реально работает там, где ты коммитишь бинарники: <code>git lfs status</code>, <code>git lfs track</code>, актуальный <code>.gitattributes</code>.</li>
<li>Прогони <code>git fsck --full</code> на репозиториях, которые давно живут под синхронизацией, — лучше найти порчу заранее, чем в момент срочного пуша.</li>
<li>Загляни в топ тяжёлых объектов истории (команда из рецепта 2) — вдруг у тебя тоже годами копится генерируемый мусор.</li>
</ol>
<p>Ловил такое? Через что синхронизируешь свои репозитории и не прилетала ли порча именно из-за облака между устройствами?</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Обычный день, обычный <code>git push</code>. И вдруг вместо привычного «всё улетело на remote» терминал выдаёт:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>error: garbage at end of loose object &#39;2699e4e6…&#39;
fatal: loose object 2699e4e6… is corrupt</code></pre>
</div>
<p>Первая мысль — «ну ладно, запущу <code>git gc</code>, он приберётся». Запускаешь. <code>gc</code> падает ровно с той же ошибкой. Теперь у тебя не пушится и не собирается мусор — репозиторий выглядит как труп.</p>
<p>Спойлер: он живой. Данные целы, всё чинится за десять минут — но не той командой, на которую думаешь первым делом. <code>git gc</code> тебе тут не друг, а первый, кто спотыкается о поломку. А ещё, пока я разгребал эту историю, вскрылась вторая беда, никак не связанная с первой на первый взгляд: <code>.git</code> раздулся на сотни мегабайт мусором, который сидел в истории годами. Так что рецептов будет два.</p>
<p>Главный тезис статьи: <strong>порча объектов git — это не потеря данных, если жив origin. А раздутый <code>.git</code> лечится только переписью истории, и никак иначе.</strong> Разберём оба случая до винтика — с теорией, диагностикой и командами, которые можно копировать.</p>
<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<ul>
<li><strong>Если ты держишь репозиторий под облачной синхронизацией</strong> (iCloud, Dropbox, OneDrive, Яндекс.Диск — неважно) — узнаешь, почему это тикающая бомба под <code>.git/objects</code> и как разминировать уже случившийся взрыв.</li>
<li><strong>Если у тебя раздулся <code>.git</code></strong> и <code>git gc</code> не помогает — получишь рабочий рецепт чистки истории через <code>git-filter-repo</code>, вместе с обязательной страховкой, чтобы не потерять репозиторий по дороге.</li>
<li><strong>Если просто хочешь понять, что такое loose-объект</strong> и почему git вообще способен «побиться» — тут есть теория zlib-сжатия и внутренностей Git LFS, без которой команды выглядят как шаманство.</li>
</ul>
<h2 id="tldr-если-чинить-надо-прямо-сейчас">TL;DR (если чинить надо прямо сейчас)</h2>
<p>Порча loose-объектов (<code>garbage at end of loose object … is corrupt</code>):</p>
<ol>
<li>Не пытайся чинить битый объект на месте — garbage-байты неустранимы.</li>
<li><code>git reset --mixed origin/main</code> — вернуть HEAD на здоровую базу с remote, рабочие файлы не трогаем.</li>
<li><code>git add -A &amp;&amp; git commit</code> — пересобрать свои правки одним чистым коммитом, мимо битых блобов.</li>
<li><code>rm</code> битых объектов → <code>git reflog expire --expire=now --all</code> → <code>git gc --prune=now</code>. <strong>Порядок именно такой.</strong></li>
<li><code>git push</code> — обычный fast-forward, <code>--force</code> не нужен.</li>
</ol>
<p>Раздутый <code>.git</code> (балласт в истории):</p>
<ol>
<li><code>git bundle create backup.bundle --all</code> — страховка ПЕРЕД любой переписью.</li>
<li><code>git filter-repo --invert-paths --path &lt;мусорный путь&gt;</code> — вырезать мусор из всей истории.</li>
<li><code>git gc --prune=now &amp;&amp; git lfs prune</code> → вернуть <code>remote</code> → <code>git push --force</code>.</li>
<li>Переклонировать репозиторий на всех устройствах свежим <code>git clone</code>.</li>
</ol>
<p>А теперь по порядку и с объяснениями.</p>
<h2 id="симптомы-что-видно-в-терминале">Симптомы: что видно в терминале</h2>
<p>Полный набор ошибок, который сыпется в таком инциденте, выглядит примерно так:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>error: garbage at end of loose object &#39;2699e4e6…&#39;
fatal: loose object 2699e4e6… is corrupt
remote: fatal: early EOF
remote: index-pack failed
fatal: failed to run repack</code></pre>
</div>
<p>Важно уметь читать эту простыню, потому что тут причина и следствие вперемешку:</p>
<ul>
<li><code>garbage at end of loose object</code> и <code>loose object … is corrupt</code> — <strong>это и есть корень.</strong> Локальный объект в <code>.git/objects</code> побит.</li>
<li><code>remote: early EOF</code> / <code>index-pack failed</code> — <strong>это следствие.</strong> Ты пытался запушить, git начал слать пак-файл, но подавился на битом объекте и оборвал поток. До remote порча не доехала — сервер просто получил обрубленные данные и отказался их принимать. Это хорошая новость: на origin всё цело.</li>
<li><code>fatal: failed to run repack</code> — это уже <code>git gc</code> спотыкается о тот же самый битый объект, когда пытается перепаковать локальную базу.</li>
</ul>
<blockquote><p><strong>Главный аргумент:</strong> поломка локальная. Origin здоров, рабочие файлы на диске здоровы. Побит только промежуточный слой — объектная база в <code>.git</code>. Именно это делает починку без потери данных возможной.</p>
</blockquote><h2 id="что-такое-loose-объект-и-почему-garbage">Что такое loose-объект и почему «garbage»</h2>
<p>Чтобы команды дальше не выглядели как заклинания, нужно понимать, что именно побилось.</p>
<p>Git хранит содержимое в объектах. Пока объект «свежий» и ещё не упакован в pack-файл, он лежит отдельным файлом — это и есть <strong>loose-объект</strong>, по пути <code>.git/objects/XX/YYYY…</code>, где <code>XX</code> — первые два символа SHA-1, а <code>YYYY…</code> — остальные 38. Внутри такого файла лежит содержимое, <strong>сжатое zlib</strong> (deflate), с коротким заголовком вида <code>blob &lt;размер&gt;\0</code> перед сжатием.</p>
<p>Ключевой факт, из которого растёт вся диагностика: <strong>сжатый объект обязан быть меньше исходника.</strong> Это же компрессия — на то она и нужна. Даже плохо сжимаемый бинарник zlib ужмёт хотя бы чуть-чуть, а текст — в разы.</p>
<p>Поэтому первый же трюк для поиска порчи — просто посмотреть на размер файла:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ls -la .git/objects/26/99e4…</span></span></code></pre></div>
</div>
<p>Если loose-объект на диске <strong>в разы больше</strong> исходного файла, который он должен хранить, — всё, он раздут мусором. Внутрь zlib-потока налезли лишние байты, которых там быть не должно. Именно об этом git и говорит: <code>garbage at end of loose object</code> — «после валидного сжатого потока идёт мусор». Распаковщик zlib дошёл до конца легитимных данных, а файл ещё не кончился — дальше идёт то, что git честно называет garbage.</p>
<h2 id="откуда-берётся-порча">Откуда берётся порча</h2>
<p>Причина №1, и в моём случае главная — <strong>одновременная запись в <code>.git/objects</code> с нескольких устройств через облачную синхронизацию.</strong> Представь: у тебя репозиторий (в моём случае — <a href="/posts/inside-my-obsidian/">целое хранилище заметок</a>) лежит в папке, которую синхронизирует облако — iCloud, Dropbox, что угодно. На одном устройстве настроен авто-commit/push, оно пишет в <code>.git/objects</code>. В этот же момент облачный клиент тянет свою версию тех же файлов с другого устройства и кладёт поверх. Git писал объект атомарно, но синхронизатор про атомарность git ничего не знает — он видит два конфликтующих состояния одного файла и склеивает их по-своему. Результат — loose-объект, у которого к валидному zlib-потоку прилеплен хвост от другой версии. Тот самый garbage.</p>
<p>Облачные хранилища не проектировались под то, чтобы под ними жила объектная база git с конкурентной записью. Для них <code>.git/objects/ab/cdef…</code> — обычный файл, а не часть транзакционной базы данных. Отсюда и мораль (к ней вернёмся в профилактике): <code>.git</code> под живой облачной синхронизацией с нескольких устройств — это гонка, которая рано или поздно порвёт объект.</p>
<p>Причина №2 — <strong>бинарники, закоммиченные сырьём мимо Git LFS.</strong> Тут стоит на минуту нырнуть во внутренности LFS, потому что без этого диагностика ниже не сложится.</p>
<p>Git LFS (Large File Storage) работает как подмена через clean/smudge-фильтры. Когда файл подпадает под правило в <code>.gitattributes</code>, при коммите LFS перехватывает его (clean-фильтр) и вместо самого бинарника кладёт в историю крошечный текстовый <strong>указатель</strong>:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>version https://git-lfs.github.com/spec/v1
oid sha256:4d7a2…
size 1048576</code></pre>
</div>
<p>Сам бинарник уезжает в отдельное LFS-хранилище, а в git-истории живёт вот эта табличка на три строки. При checkout smudge-фильтр делает обратное — подтягивает бинарник по <code>oid</code>. Изящно и легковесно.</p>
<p>А теперь ломаем: если устройство коммитит <strong>без работающего LFS</strong> (плагин авто-бэкапа не умеет LFS, фильтр не настроен, <code>.gitattributes</code> не подхватился), то бинарник уходит в историю <strong>как есть</strong> — целиком, сырыми байтами. Большой, плохо сжимаемый, лежит loose-объектом. И вот такой жирный loose-объект под конкурентной записью через облако бьётся особенно охотно — просто потому, что он большой и запись его дольше.</p>
<h2 id="диагностика">Диагностика</h2>
<p>Прежде чем чинить, надо понять масштаб: какие объекты побиты и чему они соответствуют.</p>
<p><strong>Найти битые объекты:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git fsck --full</span></span></code></pre></div>
</div>
<p><code>fsck</code> (file system check) пройдёт по всей объектной базе и покажет <code>corrupt</code>, <code>missing</code> и прочие проблемы. Он же вывалит кучу <code>dangling</code> — это недостижимые, но валидные объекты (нормальный побочный продукт работы git), они шумят и к порче отношения не имеют. Отфильтруем:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git fsck --full <span class="p">|</span> grep -vE <span class="s2">&#34;dangling&#34;</span></span></span></code></pre></div>
</div>
<p>Остаётся то, что реально болит — строки про corrupt-объекты с их хешами.</p>
<p><strong>Понять, какому файлу соответствует битый блоб:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git ls-tree -r -l &lt;commit&gt; <span class="p">|</span> grep -E <span class="s2">&#34;&lt;hash1&gt;|&lt;hash2&gt;&#34;</span></span></span></code></pre></div>
</div>
<p><code>ls-tree -r -l</code> разворачивает дерево коммита рекурсивно с размерами и хешами блобов. Грепом по хешам битых объектов находишь имя файла — теперь ты знаешь, что именно побилось (у меня это оказались PDF, которые уехали в историю мимо LFS).</p>
<p><strong>Проверить, совпадает ли рабочий файл с тем, что записано:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git hash-object <span class="s2">&#34;&lt;path&gt;&#34;</span></span></span></code></pre></div>
</div>
<p>Тонкость: в репозитории с настроенным LFS <code>git hash-object</code> прогонит файл через clean-фильтр и выдаст хеш <strong>LFS-указателя</strong>, а не самого бинарника. Если ты этого не ждёшь — легко запутаться. Это не баг, это ровно тот самый механизм подмены из предыдущего раздела.</p>
<p><strong>Посмотреть, что лежит на здоровом origin:</strong></p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git cat-file -p origin/main:<span class="s2">&#34;&lt;path&gt;&#34;</span></span></span></code></pre></div>
</div>
<p>Для файла под LFS здесь ты должен увидеть те самые 133 байта указателя <code>version https://git-lfs.github.com/spec/v1 …</code>. Если на origin лежит корректный указатель, а локально — раздутый бинарник, диагноз окончательный: локальная база побита, remote здоров, лечим сбросом на него.</p>
<h2 id="рецепт-1-починка-порчи-без-потери-данных">Рецепт 1: починка порчи без потери данных</h2>
<p>Соблазн — начать «чинить» битый объект: распаковать, обрезать хвост, запаковать обратно. Забудь. Garbage-байты неотделимы от валидных без гадания, где кончается одно и начинается другое. Реанимация битого объекта — тупиковый путь.</p>
<p>Правильная идея другая и куда проще: <strong>не чини битый объект — обойди его.</strong> Сбрось ветку на здоровый <code>origin</code> и пересобери своё рабочее дерево одним чистым коммитом. Это работает, потому что сходятся три вещи: на remote история цела, твои рабочие файлы на диске целы, а LFS при новом коммите заново обработает бинарники правильно. Битым объектам после такого сброса просто перестаёт кто-либо адресоваться — они становятся мусором, который остаётся физически удалить.</p>
<p>И сразу про то, <strong>почему <code>git gc</code> бесполезен как первая реакция.</strong> Интуиция «что-то побилось — запущу сборку мусора» здесь обманывает. <code>gc</code> первым делом идёт перепаковывать объекты — и натыкается на тот самый битый, падая с <code>failed to run repack</code>. Инструмент, которым ты собрался чинить, сам спотыкается о поломку. Сначала надо убрать битьё из-под ног, и только потом звать <code>gc</code>.</p>
<p>Полная последовательность:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. HEAD → здоровая база с remote. --mixed НЕ трогает рабочее дерево:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    все твои файлы на диске остаются как есть, сбрасывается только указатель ветки и индекс.</span>
</span></span><span class="line"><span class="cl">git reset --mixed origin/main
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. (опционально) поправить рабочее дерево — удалить/восстановить что нужно</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Пересобрать реальные правки заново, одним чистым коммитом — мимо битых блобов.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    Здесь же корректно отработает LFS, если он настроен.</span>
</span></span><span class="line"><span class="cl">git add -A <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;Пересборка рабочего дерева после порчи .git&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Физически удалить битые loose-объекты. После reset на них никто не ссылается.</span>
</span></span><span class="line"><span class="cl">rm <span class="s2">&#34;.git/objects/26/99e4…&#34;</span> <span class="s2">&#34;.git/objects/9a/06ce…&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. Выкинуть битые коммиты из reflog, чтобы они стали недостижимы</span>
</span></span><span class="line"><span class="cl">git reflog expire --expire<span class="o">=</span>now --all
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 6. Теперь перепаковка не спотыкается — мусор недостижим и удалён</span>
</span></span><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 7. Проверка — должно быть чисто</span>
</span></span><span class="line"><span class="cl">git fsck --full
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 8. Обычный fast-forward. force НЕ нужен: мы не переписывали общую историю,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    а до-собрали её поверх здорового origin.</span>
</span></span><span class="line"><span class="cl">git push origin main</span></span></code></pre></div>
</div>
<p>Отдельно подчеркну про <strong>порядок шагов 4–6</strong>, потому что переставить их местами — значит снова получить падение:</p>
<ol>
<li>Сначала <code>reflog expire</code> — пока reflog помнит битые коммиты, они «достижимы», и <code>gc</code> их не тронет (а <code>rm</code> без этого оставит висячие ссылки).</li>
<li>Потом <code>rm</code> битых блобов — убираем сами испорченные файлы с диска.</li>
<li>И только потом <code>gc</code> — ему больше не обо что спотыкаться, вся недостижимая труха вычищается штатно.</li>
</ol>
<p>Если запустить <code>gc</code> до того, как битьё стало недостижимым, — он снова упадёт на нём же. Дисциплина порядка тут не педантизм, а условие того, что рецепт вообще сработает.</p>
<p>После этого <code>git fsck --full</code> показывает чистоту, <code>push</code> проходит обычным fast-forward. Данные на месте: рабочие файлы ты не трогал ни разу, а история пересобралась поверх здоровой базы.</p>
<h2 id="вторая-беда-раздутый-git">Вторая беда: раздутый .git</h2>
<p>Пока я разбирался с порчей, вскрылось, что <code>.git</code> неприлично разбух — сотни мегабайт на репозитории, где рабочих файлов от силы на десятки. Причина оказалась не связана с порчей напрямую, но поучительна.</p>
<p>Балласт сидел <strong>в истории</strong>. Когда-то в репозиторий закоммитилась машинно-генерируемая папка плагина <code>.obsidian/plugins/vault-full-statistics</code> — индекс, кэш, что-то в этом духе. Потом её убрали из HEAD и добавили в <code>.gitignore</code>. В рабочем дереве её больше нет, <code>git status</code> чист. Но <strong>в истории она осталась навсегда</strong> — каждый её старый снапшот всё ещё лежит в объектной базе и честно занимает место. Плюс те самые PDF, уехавшие сырьём мимо LFS, — они тоже раздували <code>.git</code>, а не LFS-хранилище.</p>
<p>И вот тут легко забуксовать:</p>
<blockquote><p><strong>Главный аргумент:</strong> <code>git gc</code> и <code>git lfs prune</code> историю не переписывают. Они убирают недостижимое и осиротевшее, но объект, который достижим из старого коммита в истории, — не мусор с их точки зрения. Он «нужен». Пока он в истории, ни <code>gc</code>, ни <code>lfs prune</code> его не тронут.</p>
</blockquote><p>То есть балласт, вшитый в историю, стандартными средствами не убрать в принципе. Нужна <strong>перепись истории</strong> — операция, которая физически перестраивает коммиты так, будто мусорных путей там никогда не было.</p>
<h2 id="рецепт-2-чистка-истории-через-git-filter-repo">Рецепт 2: чистка истории через git-filter-repo</h2>
<p>Инструмент для переписи — <a href="https://github.com/newren/git-filter-repo" rel="noopener noreferrer" target="_blank"><code>git-filter-repo</code></a> (официальная замена устаревшему <code>git filter-branch</code>, быстрее и безопаснее). Порядок такой.</p>
<p><strong>Сначала найти, что вообще весит.</strong> Топ тяжёлых объектов в истории с путями:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rev-list --objects --all <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  git cat-file --batch-check<span class="o">=</span><span class="s1">&#39;%(objecttype) %(objectname) %(objectsize:disk) %(rest)&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  awk <span class="s1">&#39;/^blob/ {print $3, $2, substr($0, index($0,$4))}&#39;</span> <span class="p">|</span> sort -rn <span class="p">|</span> head -20</span></span></code></pre></div>
</div>
<p>Разберём конвейер: <code>rev-list --objects --all</code> перечисляет все объекты во всей истории вместе с их путями; <code>cat-file --batch-check</code> дописывает каждому тип, хеш, размер на диске и путь; <code>awk</code> оставляет только блобы и переставляет поля; <code>sort -rn | head -20</code> даёт двадцатку самых жирных. Ты сразу видишь, какие пути жрут место.</p>
<p><strong>Обязательная страховка ПЕРЕД переписью.</strong> Перепись истории необратима — сделай полный bundle всего репозитория, это твоя точка отката:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bundle create /tmp/repo-backup-<span class="k">$(</span>git rev-parse --short HEAD<span class="k">)</span>.bundle --all</span></span></code></pre></div>
</div>
<p>Из такого bundle репозиторий разворачивается целиком (<code>git clone repo-backup.bundle</code>), если что-то пойдёт не так.</p>
<p><strong>Собственно перепись</strong> — вырезаем мусорные пути из всей истории:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install git-filter-repo   <span class="c1"># или pip install git-filter-repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git filter-repo --invert-paths <span class="se">\
</span></span></span><span class="line"><span class="cl">  --path <span class="s2">&#34;.obsidian/plugins/vault-full-statistics/&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --path <span class="s2">&#34;_Система/2. Статика/pz-3_zadacha_koshi_dlja_uravnenija_teploprovodnos.pdf&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --force</span></span></code></pre></div>
</div>
<p><code>--invert-paths</code> инвертирует смысл <code>--path</code>: перечисленное — не «оставить только это», а «<strong>удалить это</strong> из всей истории». Каждый коммит пересобирается так, будто указанных путей там не было.</p>
<p><strong>После переписи</strong> — прибраться и вернуть remote:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now       <span class="c1"># теперь недостижимые старые объекты реально удаляются</span>
</span></span><span class="line"><span class="cl">git lfs prune            <span class="c1"># сбросить осиротевшие LFS-блобы из локального кэша</span>
</span></span><span class="line"><span class="cl"><span class="c1"># filter-repo НАРОЧНО удаляет remote — защита от случайного push. Возвращаем:</span>
</span></span><span class="line"><span class="cl">git remote add origin git@github.com:&lt;user&gt;/&lt;repo&gt;.git
</span></span><span class="line"><span class="cl">git push --force origin main</span></span></code></pre></div>
</div>
<p>Обрати внимание на два момента. Первый: <code>filter-repo</code> сам удаляет <code>origin</code> после работы — это не баг, а предохранитель, чтобы ты случайно не запушил переписанную историю до того, как всё проверил. Второй: <code>push --force</code> тут неизбежен — история переписана, fast-forward невозможен по определению.</p>
<p>По порядку величин на моём репозитории вышло примерно так: <code>.git</code> ужался с ~560 МБ до ~317 МБ. Основную долю дали два куска — вырезанная папка плагина (сама объектная база просела почти вдвое) и очистка LFS-кэша от сирот после PDF. Цифры у тебя будут свои, но масштаб «в полтора-два раза» — вполне типичный, если в истории годами копился генерируемый мусор.</p>
<h2 id="критично-после-переписи-истории">Критично после переписи истории</h2>
<p>Тут кроется грабля, на которую наступают все, кто впервые переписывает историю.</p>
<p><strong>Перепись меняет все SHA коммитов.</strong> Раз содержимое коммитов пересобрано, их хеши другие — а значит, старые клоны репозитория несовместимы с новой историей от слова совсем. И если у тебя, как у меня, есть автоматический бэкап (сервер, второе устройство, cron с <code>git push</code>), который держит <strong>старый</strong> клон, произойдёт вот что: при следующем автопуше он вернёт старую историю со всем мусором обратным push&rsquo;ем. Вся твоя чистка обнулится.</p>
<p>Поэтому железное правило: <strong>до следующего автобэкапа/автопуша переклонировать репозиторий заново на всех устройствах.</strong> Именно <code>git clone</code> начисто, а не <code>git pull</code> — pull попытается смёржить несовместимые истории и сделает только хуже. Снёс старый клон, склонировал свежий — и только тогда включай обратно автоматику.</p>
<p>И ещё одно про квоту: <strong>LFS-хранилище на GitHub перепись истории НЕ освобождает.</strong> Объекты остаются в remote-LFS, даже если ты вырезал ссылки на них из истории. Если тебе важна именно квота LFS — это отдельная операция (<code>git lfs</code> со стороны remote-инструментов провайдера), не путать с чисткой <code>.git</code>.</p>
<h2 id="профилактика">Профилактика</h2>
<p>Всё вышеописанное лечит последствия. Но правильнее — не доводить. Три правила, каждое из которых закрывает конкретную причину из разбора:</p>
<ul>
<li><strong>Не держи <code>.git</code> под одновременной записью с нескольких устройств через облако.</strong> Это корень порчи loose-объектов. Пусть коммитить и пушить будет что-то <strong>одно</strong> — одно устройство или один сервер с настроенным LFS. Остальные пусть только читают/синхронизируют рабочие файлы, но не воюют за <code>.git/objects</code>.</li>
<li><strong>Бинарники — через Git LFS, а не сырьём в историю.</strong> Настрой <code>.gitattributes</code> и убедись, что LFS реально работает на каждом устройстве, которое коммитит. Один коммит мимо LFS — и жирный объект уже в истории навсегда (см. рецепт 2).</li>
<li><strong>Машинно-генерируемые папки — в <code>.gitignore</code> с самого начала.</strong> Индексы, кэши плагинов, артефакты сборки — им не место в истории. Их дёшево перегенерировать и дорого потом вырезать переписью.</li>
</ul>
<h2 id="заключение">Заключение</h2>
<p>Если свести всё к паре мыслей, которые стоит унести:</p>
<ul>
<li><strong>git ломается тихо и чинится не тем, чем кажется.</strong> <code>garbage at end of loose object</code> выглядит как приговор, а <code>git gc</code> — как очевидное лекарство. На деле объект побит локально, а <code>gc</code> — первый, кто об него спотыкается.</li>
<li><strong>Порча объектов — это не потеря данных, пока жив origin.</strong> Не реанимируй битьё — сбрось ветку на здоровую базу и пересобери рабочее дерево одним коммитом. Данные в рабочих файлах и на remote к порче объектной базы не имеют отношения.</li>
<li><strong>Раздутый <code>.git</code> лечится только переписью истории.</strong> <code>gc</code> и <code>lfs prune</code> не трогают то, что достижимо из старых коммитов. Балласт из истории убирает <code>git-filter-repo</code> — и то под страховкой bundle и с обязательным переклонированием после.</li>
</ul>
<h3 id="что-сделать-прямо-сейчас">Что сделать прямо сейчас</h3>
<ol>
<li>Проверь, не синхронизируется ли <code>.git</code> твоего репозитория через облако с нескольких устройств. Если да — оставь коммиты за одним источником.</li>
<li>Убедись, что Git LFS реально работает там, где ты коммитишь бинарники: <code>git lfs status</code>, <code>git lfs track</code>, актуальный <code>.gitattributes</code>.</li>
<li>Прогони <code>git fsck --full</code> на репозиториях, которые давно живут под синхронизацией, — лучше найти порчу заранее, чем в момент срочного пуша.</li>
<li>Загляни в топ тяжёлых объектов истории (команда из рецепта 2) — вдруг у тебя тоже годами копится генерируемый мусор.</li>
</ol>
<p>Ловил такое? Через что синхронизируешь свои репозитории и не прилетала ли порча именно из-за облака между устройствами?</p>
</div>]]></content:encoded>
    </item><item>
      <title>Как объяснить маме, папе, бабушке и дедушке, что такое сложный пароль</title>
      <link>https://jtprog.ru/posts/passwords-for-parents/</link>
      <pubDate>Fri, 05 Jun 2026 20:00:00 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/passwords-for-parents/</guid>
      <category>Безопасность</category>
      <category>Password</category>
      <category>Мысли</category>
      <category domain="category">Opinions</category>
      <enclosure url="https://jtprog.ru/cover.svg" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.svg" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Наверняка ты ловил себя на мысли «да неее, меня-то уж точно не разведут», а потом «опаньки» — и диктуешь, сам того не понимая, код, рядом с которым написано «НИКОМУ НЕ НАЗЫВАТЬ».</p>
<p>Мамам, папам, бабушкам и дедушкам часто сложно с компьютером. А фраза «придумай сложный пароль» вызывает панику и просьбу «придумай сам». Сверху — «повсеместная цифровизация» (напомню про <a href="https://twtr.jtprog.ru/Fi5v7bAfn1h" rel="noopener noreferrer" target="_blank">&ldquo;Запись только через сайт&rdquo;</a>) и региональные МФЦ, где работают обычные люди, для которых «сложный пароль» автоматически превращается в <code>1955.12.21Яб</code>. Виновных пусть ищут те, в чьи обязанности это входит.</p>
<h2 id="как-меня-развели">Как меня развели</h2>
<p>Пришла ко мне мама и задала вопрос: «Как придумать сложный пароль?» К её несчастью, я у неё дурной на голову, и долго рассказывал про всякое разное — для понимания контекста, так сказать. Но простые советы таки смог выделить и озвучить голосом.</p>
<p>Через какое-то время я подумал «а дай я продублирую все рекомендации ещё и текстом», а когда счётчик символов перевалил за 1.5k — понял: пусть это будет самая простая статья в моём блоге, но она должна решить одну конкретную задачу — «объяснить любым маме/папе/бабушке/дедушке, зачем сложный пароль и как его придумать».</p>
<p>Осенью 2019 года я попался на тупой развод. Подробности опущу, но тот убийственный диалог не забуду:</p>
<blockquote><p><strong>М:</strong> «Вам СМСка пришла — видите, там написано <em>Никому не называть</em>?»<br>
<strong>Я:</strong> «Да! Вижу!»<br>
<strong>М:</strong> «Называйте!»<br>
<strong>Я:</strong> «2314»<br>
<strong>М:</strong> «Спасибо!»</p>
</blockquote><p>Через считанные секунды до меня дошла абсурдность ситуации, но настолько отвратительно я себя ещё никогда не чувствовал.</p>
<p>В октябре 2022 года в подобную ситуацию попала мама. Разница лишь в том, что я отдал доступ в банк, а мама — в Госуслуги.</p>
<p>Для справки: мошенники при попытке угнать аккаунт на Госуслугах просили именно код из СМС. Это значит, что пароль уже знали — поэтому «надо придумывать сложные пароли». А зная, что «помощь с регистрацией на Госуслугах» оказывают сотрудники МФЦ, через которые проходит большой поток людей — ребята и девчата стараются себе упростить жизнь. <a href="https://www.kaspersky.ru/blog/gosuslugi-telegram-bot-hr-scam/40956/" rel="noopener noreferrer" target="_blank">Свежая схема</a> — угон Госуслуг через «отдел кадров» в Telegram-боте, и работает она ровно потому, что пароль слабый или повторяющийся.</p>
<p>Раньше (буквально 5–7 лет назад) хорошим паролем считался пароль с использованием латинских букв в нижнем и верхнем регистре и цифр <code>0-9</code>, а достаточной длиной — 6–8 символов. Сейчас правила другие. NIST в актуальной редакции <a href="https://pages.nist.gov/800-63-3/sp800-63b.html" rel="noopener noreferrer" target="_blank">SP 800-63B</a> (<a href="https://www.enzoic.com/blog/nist-sp-800-63b-rev4/" rel="noopener noreferrer" target="_blank">Rev.4, август 2025</a>) формулирует прямо: длина важнее «сложности». Минимум — 15 символов, если пароль один и без второго фактора. Состав символов больше не обязателен — главное, чтобы пароль был длинным, уникальным и не светился в утечках.</p>
<h2 id="что-делать">Что делать</h2>
<p>То, что я предложу дальше, кому-то покажется слишком сложным, кому-то слишком простым. Но сделать это надо.</p>
<p>Для начала возьми отдельный блокнот и ручку и выпиши все сайты, на которые ходишь с логином и паролем — в простейшем формате:</p>
<ul>
<li>Сайт: <code>ya.ru</code></li>
<li>Логин/имя пользователя: <code>vasilisapetrovna@ya.ru</code></li>
<li>Пароль: <code>МойТекущийПароль</code></li>
</ul>
<p>Дальше — пройдись по списку и отметь, что не так. Имя, дата рождения, простое слово, которое легко соотнести с тобой (<code>vasya</code>, <code>10012990</code>, <code>prepod</code>) — под замену. Всё, что короче 15 символов, — под замену. Повторяющиеся пароли — самое опасное: узнают пароль от Одноквашников и в остальные сервисы попадут без проблем.</p>
<p>Теперь главный вопрос: «как придумать сложный пароль?» Это проще, чем кажется.</p>
<p>Бери четыре слова, которые с тобой никак не связаны, и склеивай в одну фразу. Например: <code>корзина-трактор-ягода-фонарь</code>. Запомнить — секунда, а для бытовых угроз (фишинг, утечки, перебор) — больше чем достаточно. По-научному это «парольная фраза», и она бьёт «сложный» восьмисимвольный шифр с <code>@</code>, <code>#</code> и <code>$</code> в одну калитку. Подробнее про парольные фразы и как их не забывать — у <a href="https://te-st.org/2025/09/26/top3-sposoba-pridumyvat-nadyozhnye-paroli-i-bolshe-ih-ne-zabyvat/" rel="noopener noreferrer" target="_blank">Теплицы социальных технологий</a> и у <a href="https://www.kaspersky.ru/blog/international-password-day-2025/39449/" rel="noopener noreferrer" target="_blank">Касперского</a>.</p>
<p>Только не используй мою комбинацию буквально — как только пост уйдёт в публикацию, она через пару дней приедет в словари для перебора. Возьми свои четыре слова, желательно — из разных смысловых полей (не «кот-собака-хомяк-попугай», а что-то вроде «фиалка-домкрат-печенье-маяк»).</p>
<p>Если придумывать совсем лень — поставь менеджер паролей. Из живых и проверенных — четыре на выбор:</p>
<ul>
<li><a href="https://bitwarden.com/" rel="noopener noreferrer" target="_blank">Bitwarden</a> — бесплатный, open-source, под все платформы и браузеры. По умолчанию пароли лежат в облаке самого Bitwarden, но можно поднять <a href="https://bitwarden.com/help/install-on-premise-linux/" rel="noopener noreferrer" target="_blank">свой сервер</a> — большинству не надо, берём облако и не паримся.</li>
<li><a href="https://keepass.info/" rel="noopener noreferrer" target="_blank">KeePass</a> и его форки — <a href="https://keepassxc.org/" rel="noopener noreferrer" target="_blank">KeePassXC</a> на десктопе, <a href="https://www.keepassdx.com/" rel="noopener noreferrer" target="_blank">KeePassDX</a> на Android, <a href="https://strongboxsafe.com/" rel="noopener noreferrer" target="_blank">Strongbox</a> на iOS. Тоже бесплатно и open-source, но всё хранится в одном зашифрованном файле — и ты сам решаешь, где этот файл лежит: на диске, в iCloud, в Яндекс.Диске, на флешке. Гибче, но и думать придётся больше.</li>
<li><a href="https://proton.me/pass" rel="noopener noreferrer" target="_blank">Proton Pass</a> — бесплатный (с лимитами) или ~$4 в месяц за полную версию, open-source, от команды ProtonMail. Швейцарская юрисдикция, end-to-end шифрование, есть встроенный 2FA-аутентификатор и алиасы почты. По духу ближе к Bitwarden, но интерфейс заметно более «человеческий».</li>
<li><a href="https://1password.com/" rel="noopener noreferrer" target="_blank">1Password</a> — платный (~$48 в год для одного, ~$72 в год за семейный план на пятерых, <a href="https://www.ubergizmo.com/2026/02/1password-price-increase/" rel="noopener noreferrer" target="_blank">с марта 2026 цена выросла</a>), зато с самым человеческим интерфейсом. Если настраиваешь маме и не хочешь объяснять, что такое «синхронизация vault&rsquo;а» — это, пожалуй, самый короткий путь.</li>
</ul>
<p>Один длинный пароль помнишь сам (тот самый, из четырёх слов) — это мастер-пароль от менеджера. Остальные пусть генерит и хранит он.</p>
<p>Тут есть ловушка, в которую попадают все, кто впервые ставит менеджер паролей: если потерял мастер-пароль — потерял всё. Никакой «восстановим по СМС» там не предусмотрено: это и есть фича, а не баг — иначе мошенник делал бы то же самое. Поэтому мастер-пароль выписываешь на бумажку (тот самый блокнот пригодится) и кладёшь в надёжное место дома. У 1Password есть ещё <code>Secret Key</code> — это второй ключ, который тоже нужен для входа на новом устройстве; его аналогично распечатываешь и хранишь.</p>
<p>А если ставить отдельное приложение всё равно лень — посмотри, что уже встроено. На iPhone и Mac это <a href="https://support.apple.com/ru-ru/HT204085" rel="noopener noreferrer" target="_blank">«Связка ключей iCloud»</a> (в iOS 18 и macOS Sequoia — уже отдельным приложением <a href="https://support.apple.com/ru-ru/120758" rel="noopener noreferrer" target="_blank">«Пароли»</a>). На Android и в Chrome — <a href="https://passwords.google.com/" rel="noopener noreferrer" target="_blank">Google Password Manager</a>. У тех, кто живёт в Яндекс.Браузере, — <a href="https://browser.yandex.ru/help/personal-data-protection/passwords.html" rel="noopener noreferrer" target="_blank">встроенный менеджер Яндекса</a>. Базовая механика везде одна: сгенерировать длинный пароль, запомнить, подставить в нужное поле и раскатать на остальные устройства. Для задачи «перестать использовать один и тот же пароль везде» этого хватает.</p>
<p>Цена простоты — привязка к экосистеме. Связка ключей не поедет на Android, Google Password Manager на iPhone — поедет, но криво. Яндекс — только в Яндекс.Браузере. Пин-код от карты или код от домофона туда тоже не положишь: это не сейф произвольных секретов, а хранилище логинов-паролей к сайтам. Поэтому встроенный менеджер — нормальный первый шаг, но как только сменишь телефон с iPhone на Android или наоборот — переезжать будет больно. Bitwarden, KeePass и 1Password в этом смысле нейтральны: работают одинаково везде.</p>
<p>А если и менеджер паролей — это уже слишком сложно (бабушке, дедушке, кому-то ещё, кто с компьютером на «вы»), есть рабочий вариант, над которым любят посмеяться безопасники: маленький блокнот и ручка. Да-да, тот самый, с которого мы начали — где выписаны сайт, логин и пароль. Пусть он и останется хранилищем для важного: Госуслуги, банк, почта, пин-коды от карт. Пароли в нём могут быть какими угодно длинными и страшными — записал и не запоминаешь.</p>
<p>Тут важно понимать, от чего этот способ защищает, а от чего — нет. От фишинга, утечек паролей с сайтов и звонков «из службы безопасности» — защищает: пароль уникальный, длинный, нигде не светится. От того, кто физически возьмёт блокнот в руки, — нет. Отсюда пара правил: блокнот живёт дома, в одном понятном месте, не в сумке и не в кошельке; никаких подписей «ПАРОЛИ» на обложке; и желательно — не на виду у курьера, мастера и случайных гостей. Для мамы с папой риск «пришли в квартиру и нашли блокнот» сильно меньше риска «надиктовал код по телефону мошеннику» — этим и берём.</p>
<p>Включи второй фактор (2FA) хотя бы на банке, почте и Госуслугах. Лучше через приложение-аутентификатор, а не через SMS — SMS уязвимы к SIM-swap (мошенник идёт в салон с поддельной доверенностью и перевыпускает симку на свой паспорт; все коды теперь идут ему) и к классическому выманиванию по телефону.</p>
<p>Из аутентификаторов на Android — <a href="https://getaegis.app/" rel="noopener noreferrer" target="_blank">Aegis</a> (open-source, можно делать зашифрованный бэкап) или <a href="https://2fas.com/" rel="noopener noreferrer" target="_blank">2FAS</a>. На iPhone — <a href="https://2fas.com/" rel="noopener noreferrer" target="_blank">2FAS</a> или встроенный кодогенератор прямо в приложении «Пароли». Кроссплатформенно — <a href="https://support.google.com/accounts/answer/1066447" rel="noopener noreferrer" target="_blank">Google Authenticator</a> (с облачной синхронизацией) или <a href="https://yandex.ru/support/id/authenticator/about.html" rel="noopener noreferrer" target="_blank">Яндекс Ключ</a>.</p>
<p>При подключении 2FA сервис показывает <strong>резервные коды</strong> — обычно 8–10 одноразовых кодов на случай, если телефон с аутентификатором утонет, разобьётся или потеряется. Их обязательно сохрани: в менеджер паролей, в тот же блокнот, в Telegram-избранное — куда угодно, но не игнорируй экран «вот ваши backup-коды, сохраните их». Без них при потере телефона восстановление доступа превращается в эпопею с фотографиями паспорта в техподдержку, а где-то — в полное «начать с нуля».</p>
<p>Где можно — переходи на passkeys. Google, Apple, Microsoft, GitHub уже поддерживают. Это вход без пароля, по биометрии. Продиктовать по телефону его в принципе нельзя — нечего диктовать.</p>
<h2 id="если-коротко">Если коротко</h2>
<p>Три правила, которые закроют львиную долю бытовых угроз:</p>
<ol>
<li>Пароль от 15 символов, не повторяется между сервисами, не светится в утечках.</li>
<li>На критичных аккаунтах (банк, почта, Госуслуги) — второй фактор через приложение.</li>
<li>Никаких кодов из SMS вслух по телефону. С 1 июня 2025 в России <a href="https://rg.ru/2025/05/30/vyzov-sbroshen.html" rel="noopener noreferrer" target="_blank">действует правило</a>: пока идёт голосовой вызов, SMS с кодами подтверждения (от Госуслуг, банков, маркетплейсов) задерживаются оператором связи и доставляются только после разговора. Но этот закон не помогает, если мошенник звонит через WhatsApp, Telegram или Zoom — для оператора это просто интернет-трафик, SMS пройдёт как обычно. Поэтому правило «положил трубку, перезвонил по номеру с карты» по-прежнему рабочее.</li>
</ol>
<p>Если ты помог маме/папе/бабушке/дедушке настроить менеджер паролей и 2FA — ты сделал больше, чем три статьи на эту тему. Передай дальше.</p>
<p>Если пароль нужен себе и прямо в терминале — <a href="/posts/cli-gen-pass/">генерируем в CLI</a>. А про то, чем заканчивается «да кому я нужен», у меня есть две старые истории с фронта: <a href="/posts/bad-story/">спам и шифрование</a> и <a href="/posts/vault-crypt/">очередной шифровальщик</a>.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Наверняка ты ловил себя на мысли «да неее, меня-то уж точно не разведут», а потом «опаньки» — и диктуешь, сам того не понимая, код, рядом с которым написано «НИКОМУ НЕ НАЗЫВАТЬ».</p>
<p>Мамам, папам, бабушкам и дедушкам часто сложно с компьютером. А фраза «придумай сложный пароль» вызывает панику и просьбу «придумай сам». Сверху — «повсеместная цифровизация» (напомню про <a href="https://twtr.jtprog.ru/Fi5v7bAfn1h" rel="noopener noreferrer" target="_blank">&ldquo;Запись только через сайт&rdquo;</a>) и региональные МФЦ, где работают обычные люди, для которых «сложный пароль» автоматически превращается в <code>1955.12.21Яб</code>. Виновных пусть ищут те, в чьи обязанности это входит.</p>
<h2 id="как-меня-развели">Как меня развели</h2>
<p>Пришла ко мне мама и задала вопрос: «Как придумать сложный пароль?» К её несчастью, я у неё дурной на голову, и долго рассказывал про всякое разное — для понимания контекста, так сказать. Но простые советы таки смог выделить и озвучить голосом.</p>
<p>Через какое-то время я подумал «а дай я продублирую все рекомендации ещё и текстом», а когда счётчик символов перевалил за 1.5k — понял: пусть это будет самая простая статья в моём блоге, но она должна решить одну конкретную задачу — «объяснить любым маме/папе/бабушке/дедушке, зачем сложный пароль и как его придумать».</p>
<p>Осенью 2019 года я попался на тупой развод. Подробности опущу, но тот убийственный диалог не забуду:</p>
<blockquote><p><strong>М:</strong> «Вам СМСка пришла — видите, там написано <em>Никому не называть</em>?»<br>
<strong>Я:</strong> «Да! Вижу!»<br>
<strong>М:</strong> «Называйте!»<br>
<strong>Я:</strong> «2314»<br>
<strong>М:</strong> «Спасибо!»</p>
</blockquote><p>Через считанные секунды до меня дошла абсурдность ситуации, но настолько отвратительно я себя ещё никогда не чувствовал.</p>
<p>В октябре 2022 года в подобную ситуацию попала мама. Разница лишь в том, что я отдал доступ в банк, а мама — в Госуслуги.</p>
<p>Для справки: мошенники при попытке угнать аккаунт на Госуслугах просили именно код из СМС. Это значит, что пароль уже знали — поэтому «надо придумывать сложные пароли». А зная, что «помощь с регистрацией на Госуслугах» оказывают сотрудники МФЦ, через которые проходит большой поток людей — ребята и девчата стараются себе упростить жизнь. <a href="https://www.kaspersky.ru/blog/gosuslugi-telegram-bot-hr-scam/40956/" rel="noopener noreferrer" target="_blank">Свежая схема</a> — угон Госуслуг через «отдел кадров» в Telegram-боте, и работает она ровно потому, что пароль слабый или повторяющийся.</p>
<p>Раньше (буквально 5–7 лет назад) хорошим паролем считался пароль с использованием латинских букв в нижнем и верхнем регистре и цифр <code>0-9</code>, а достаточной длиной — 6–8 символов. Сейчас правила другие. NIST в актуальной редакции <a href="https://pages.nist.gov/800-63-3/sp800-63b.html" rel="noopener noreferrer" target="_blank">SP 800-63B</a> (<a href="https://www.enzoic.com/blog/nist-sp-800-63b-rev4/" rel="noopener noreferrer" target="_blank">Rev.4, август 2025</a>) формулирует прямо: длина важнее «сложности». Минимум — 15 символов, если пароль один и без второго фактора. Состав символов больше не обязателен — главное, чтобы пароль был длинным, уникальным и не светился в утечках.</p>
<h2 id="что-делать">Что делать</h2>
<p>То, что я предложу дальше, кому-то покажется слишком сложным, кому-то слишком простым. Но сделать это надо.</p>
<p>Для начала возьми отдельный блокнот и ручку и выпиши все сайты, на которые ходишь с логином и паролем — в простейшем формате:</p>
<ul>
<li>Сайт: <code>ya.ru</code></li>
<li>Логин/имя пользователя: <code>vasilisapetrovna@ya.ru</code></li>
<li>Пароль: <code>МойТекущийПароль</code></li>
</ul>
<p>Дальше — пройдись по списку и отметь, что не так. Имя, дата рождения, простое слово, которое легко соотнести с тобой (<code>vasya</code>, <code>10012990</code>, <code>prepod</code>) — под замену. Всё, что короче 15 символов, — под замену. Повторяющиеся пароли — самое опасное: узнают пароль от Одноквашников и в остальные сервисы попадут без проблем.</p>
<p>Теперь главный вопрос: «как придумать сложный пароль?» Это проще, чем кажется.</p>
<p>Бери четыре слова, которые с тобой никак не связаны, и склеивай в одну фразу. Например: <code>корзина-трактор-ягода-фонарь</code>. Запомнить — секунда, а для бытовых угроз (фишинг, утечки, перебор) — больше чем достаточно. По-научному это «парольная фраза», и она бьёт «сложный» восьмисимвольный шифр с <code>@</code>, <code>#</code> и <code>$</code> в одну калитку. Подробнее про парольные фразы и как их не забывать — у <a href="https://te-st.org/2025/09/26/top3-sposoba-pridumyvat-nadyozhnye-paroli-i-bolshe-ih-ne-zabyvat/" rel="noopener noreferrer" target="_blank">Теплицы социальных технологий</a> и у <a href="https://www.kaspersky.ru/blog/international-password-day-2025/39449/" rel="noopener noreferrer" target="_blank">Касперского</a>.</p>
<p>Только не используй мою комбинацию буквально — как только пост уйдёт в публикацию, она через пару дней приедет в словари для перебора. Возьми свои четыре слова, желательно — из разных смысловых полей (не «кот-собака-хомяк-попугай», а что-то вроде «фиалка-домкрат-печенье-маяк»).</p>
<p>Если придумывать совсем лень — поставь менеджер паролей. Из живых и проверенных — четыре на выбор:</p>
<ul>
<li><a href="https://bitwarden.com/" rel="noopener noreferrer" target="_blank">Bitwarden</a> — бесплатный, open-source, под все платформы и браузеры. По умолчанию пароли лежат в облаке самого Bitwarden, но можно поднять <a href="https://bitwarden.com/help/install-on-premise-linux/" rel="noopener noreferrer" target="_blank">свой сервер</a> — большинству не надо, берём облако и не паримся.</li>
<li><a href="https://keepass.info/" rel="noopener noreferrer" target="_blank">KeePass</a> и его форки — <a href="https://keepassxc.org/" rel="noopener noreferrer" target="_blank">KeePassXC</a> на десктопе, <a href="https://www.keepassdx.com/" rel="noopener noreferrer" target="_blank">KeePassDX</a> на Android, <a href="https://strongboxsafe.com/" rel="noopener noreferrer" target="_blank">Strongbox</a> на iOS. Тоже бесплатно и open-source, но всё хранится в одном зашифрованном файле — и ты сам решаешь, где этот файл лежит: на диске, в iCloud, в Яндекс.Диске, на флешке. Гибче, но и думать придётся больше.</li>
<li><a href="https://proton.me/pass" rel="noopener noreferrer" target="_blank">Proton Pass</a> — бесплатный (с лимитами) или ~$4 в месяц за полную версию, open-source, от команды ProtonMail. Швейцарская юрисдикция, end-to-end шифрование, есть встроенный 2FA-аутентификатор и алиасы почты. По духу ближе к Bitwarden, но интерфейс заметно более «человеческий».</li>
<li><a href="https://1password.com/" rel="noopener noreferrer" target="_blank">1Password</a> — платный (~$48 в год для одного, ~$72 в год за семейный план на пятерых, <a href="https://www.ubergizmo.com/2026/02/1password-price-increase/" rel="noopener noreferrer" target="_blank">с марта 2026 цена выросла</a>), зато с самым человеческим интерфейсом. Если настраиваешь маме и не хочешь объяснять, что такое «синхронизация vault&rsquo;а» — это, пожалуй, самый короткий путь.</li>
</ul>
<p>Один длинный пароль помнишь сам (тот самый, из четырёх слов) — это мастер-пароль от менеджера. Остальные пусть генерит и хранит он.</p>
<p>Тут есть ловушка, в которую попадают все, кто впервые ставит менеджер паролей: если потерял мастер-пароль — потерял всё. Никакой «восстановим по СМС» там не предусмотрено: это и есть фича, а не баг — иначе мошенник делал бы то же самое. Поэтому мастер-пароль выписываешь на бумажку (тот самый блокнот пригодится) и кладёшь в надёжное место дома. У 1Password есть ещё <code>Secret Key</code> — это второй ключ, который тоже нужен для входа на новом устройстве; его аналогично распечатываешь и хранишь.</p>
<p>А если ставить отдельное приложение всё равно лень — посмотри, что уже встроено. На iPhone и Mac это <a href="https://support.apple.com/ru-ru/HT204085" rel="noopener noreferrer" target="_blank">«Связка ключей iCloud»</a> (в iOS 18 и macOS Sequoia — уже отдельным приложением <a href="https://support.apple.com/ru-ru/120758" rel="noopener noreferrer" target="_blank">«Пароли»</a>). На Android и в Chrome — <a href="https://passwords.google.com/" rel="noopener noreferrer" target="_blank">Google Password Manager</a>. У тех, кто живёт в Яндекс.Браузере, — <a href="https://browser.yandex.ru/help/personal-data-protection/passwords.html" rel="noopener noreferrer" target="_blank">встроенный менеджер Яндекса</a>. Базовая механика везде одна: сгенерировать длинный пароль, запомнить, подставить в нужное поле и раскатать на остальные устройства. Для задачи «перестать использовать один и тот же пароль везде» этого хватает.</p>
<p>Цена простоты — привязка к экосистеме. Связка ключей не поедет на Android, Google Password Manager на iPhone — поедет, но криво. Яндекс — только в Яндекс.Браузере. Пин-код от карты или код от домофона туда тоже не положишь: это не сейф произвольных секретов, а хранилище логинов-паролей к сайтам. Поэтому встроенный менеджер — нормальный первый шаг, но как только сменишь телефон с iPhone на Android или наоборот — переезжать будет больно. Bitwarden, KeePass и 1Password в этом смысле нейтральны: работают одинаково везде.</p>
<p>А если и менеджер паролей — это уже слишком сложно (бабушке, дедушке, кому-то ещё, кто с компьютером на «вы»), есть рабочий вариант, над которым любят посмеяться безопасники: маленький блокнот и ручка. Да-да, тот самый, с которого мы начали — где выписаны сайт, логин и пароль. Пусть он и останется хранилищем для важного: Госуслуги, банк, почта, пин-коды от карт. Пароли в нём могут быть какими угодно длинными и страшными — записал и не запоминаешь.</p>
<p>Тут важно понимать, от чего этот способ защищает, а от чего — нет. От фишинга, утечек паролей с сайтов и звонков «из службы безопасности» — защищает: пароль уникальный, длинный, нигде не светится. От того, кто физически возьмёт блокнот в руки, — нет. Отсюда пара правил: блокнот живёт дома, в одном понятном месте, не в сумке и не в кошельке; никаких подписей «ПАРОЛИ» на обложке; и желательно — не на виду у курьера, мастера и случайных гостей. Для мамы с папой риск «пришли в квартиру и нашли блокнот» сильно меньше риска «надиктовал код по телефону мошеннику» — этим и берём.</p>
<p>Включи второй фактор (2FA) хотя бы на банке, почте и Госуслугах. Лучше через приложение-аутентификатор, а не через SMS — SMS уязвимы к SIM-swap (мошенник идёт в салон с поддельной доверенностью и перевыпускает симку на свой паспорт; все коды теперь идут ему) и к классическому выманиванию по телефону.</p>
<p>Из аутентификаторов на Android — <a href="https://getaegis.app/" rel="noopener noreferrer" target="_blank">Aegis</a> (open-source, можно делать зашифрованный бэкап) или <a href="https://2fas.com/" rel="noopener noreferrer" target="_blank">2FAS</a>. На iPhone — <a href="https://2fas.com/" rel="noopener noreferrer" target="_blank">2FAS</a> или встроенный кодогенератор прямо в приложении «Пароли». Кроссплатформенно — <a href="https://support.google.com/accounts/answer/1066447" rel="noopener noreferrer" target="_blank">Google Authenticator</a> (с облачной синхронизацией) или <a href="https://yandex.ru/support/id/authenticator/about.html" rel="noopener noreferrer" target="_blank">Яндекс Ключ</a>.</p>
<p>При подключении 2FA сервис показывает <strong>резервные коды</strong> — обычно 8–10 одноразовых кодов на случай, если телефон с аутентификатором утонет, разобьётся или потеряется. Их обязательно сохрани: в менеджер паролей, в тот же блокнот, в Telegram-избранное — куда угодно, но не игнорируй экран «вот ваши backup-коды, сохраните их». Без них при потере телефона восстановление доступа превращается в эпопею с фотографиями паспорта в техподдержку, а где-то — в полное «начать с нуля».</p>
<p>Где можно — переходи на passkeys. Google, Apple, Microsoft, GitHub уже поддерживают. Это вход без пароля, по биометрии. Продиктовать по телефону его в принципе нельзя — нечего диктовать.</p>
<h2 id="если-коротко">Если коротко</h2>
<p>Три правила, которые закроют львиную долю бытовых угроз:</p>
<ol>
<li>Пароль от 15 символов, не повторяется между сервисами, не светится в утечках.</li>
<li>На критичных аккаунтах (банк, почта, Госуслуги) — второй фактор через приложение.</li>
<li>Никаких кодов из SMS вслух по телефону. С 1 июня 2025 в России <a href="https://rg.ru/2025/05/30/vyzov-sbroshen.html" rel="noopener noreferrer" target="_blank">действует правило</a>: пока идёт голосовой вызов, SMS с кодами подтверждения (от Госуслуг, банков, маркетплейсов) задерживаются оператором связи и доставляются только после разговора. Но этот закон не помогает, если мошенник звонит через WhatsApp, Telegram или Zoom — для оператора это просто интернет-трафик, SMS пройдёт как обычно. Поэтому правило «положил трубку, перезвонил по номеру с карты» по-прежнему рабочее.</li>
</ol>
<p>Если ты помог маме/папе/бабушке/дедушке настроить менеджер паролей и 2FA — ты сделал больше, чем три статьи на эту тему. Передай дальше.</p>
<p>Если пароль нужен себе и прямо в терминале — <a href="/posts/cli-gen-pass/">генерируем в CLI</a>. А про то, чем заканчивается «да кому я нужен», у меня есть две старые истории с фронта: <a href="/posts/bad-story/">спам и шифрование</a> и <a href="/posts/vault-crypt/">очередной шифровальщик</a>.</p>
</div>]]></content:encoded>
    </item><item>
      <title>IndexNow для статического блога: что это, зачем и как я его прикручивал</title>
      <link>https://jtprog.ru/posts/indexnow/</link>
      <pubDate>Tue, 02 Jun 2026 14:58:25 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/indexnow/</guid>
      <category>Hugo</category>
      <category>GitHub Actions</category>
      <category>Blog</category>
      <category>CI</category>
      <category domain="category">Howto</category>
      <enclosure url="https://jtprog.ru/cover.svg" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.svg" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Выкатил новый пост, поправил страницу, обновил title — и сидишь, ждёшь, пока поисковики соизволят переобойти сайт. День, два, неделя. А хочется, чтобы апдейт подхватился сразу после <code>git push</code>, без танцев с Search Console и ручных submission-форм.</p>
<p>Дальше — как закрыть эту боль одним шагом в GitHub Actions через мой CLI <a href="https://github.com/jtprogru/indexnow" rel="noopener noreferrer" target="_blank">indexnow</a> и почему оно вообще работает. Как устроен сам пайплайн блога, я рассказывал в <a href="/posts/github-actions/">«Github Actions для блогера»</a> — новый шаг встаёт туда же, после деплоя.</p>
<h2 id="что-такое-indexnow-и-зачем-он-нужен">Что такое IndexNow и зачем он нужен</h2>
<p>Протокол <a href="https://www.indexnow.org/documentation" rel="noopener noreferrer" target="_blank">IndexNow</a> — это простой способ сказать поисковику: «эти URL изменились, переобойди их побыстрее, пожалуйста». Ты делаешь один HTTP-запрос с ключом и списком URL, поисковик в ответ подтверждает приём (или отбивает с осмысленной ошибкой) — и дальше уже его проблема, когда именно он переобойдёт страницы.</p>
<p>Главное: <strong>submission автоматически шарится между всеми участниками протокола</strong>. По спеке достаточно дёрнуть <strong>один</strong> эндпоинт (например, <code>api.indexnow.org</code> или <code>bing.com/indexnow</code>), и тот же запрос провалидируется и расшарится со всеми: Bing, Yandex, Naver, Seznam, Yep и другие участники. Google к протоколу формально не присоединился — для него по-прежнему остаётся sitemap + Search Console.</p>
<p>Аутентификация — самая «забавная» часть протокола. Ты придумываешь ключ (произвольная hex-строка длиной 8–128 символов), хостишь его в виде текстового файла на собственном домене, например <code>https://example.com/&lt;key&gt;.txt</code>, и присылаешь этот ключ в каждом запросе. Поисковик идёт по URL, читает файл, сверяет содержимое с тем ключом, что ты прислал. Совпало — submission принят. Нет — 403. Просто и без OAuth.</p>
<p>И тут же — мой любимый перл из <a href="https://yandex.ru/support/webmaster/ru/indexnow/key" rel="noopener noreferrer" target="_blank">документации Яндекс.Вебмастера</a>: «Ключ менять необязательно. Вы можете изменить ключ, если он стал известен третьим лицам.» То есть ключ, который ты по спеке обязан выложить публичным <code>.txt</code> на свой домен и который любой может вытащить одним <code>curl</code>, Яндекс предлагает ротировать, если он «стал известен третьим лицам». А он по дизайну протокола известен им всегда. Ну камон.</p>
<h2 id="зачем-я-написал-свой-cli">Зачем я написал свой CLI</h2>
<p>Дёргать API руками лениво, особенно когда у тебя на пуш приходится 10–50 изменившихся URL и хочется делать это из CI. Можно, конечно, нагуглить готовый пример с <code>curl</code> и <code>jq</code>, но:</p>
<ul>
<li>ошибки разбирать в bash больно;</li>
<li>ретраи на 429/5xx писать руками лень;</li>
<li>sitemap парсить из XML в bash — отдельный жанр перформанса;</li>
<li>лимит протокола в 10 000 URL на запрос тоже надо учитывать.</li>
</ul>
<p>Поэтому я написал на <a href="/posts/want-coding-golang/">Go</a> маленький <code>indexnow</code> — сначала как CLI для разовых submission&rsquo;ов, а с релиза <code>v0.5.0</code> это уже и <strong>готовый GitHub Action</strong>. По дороге всё то, что обычно копится в самописных скриптах, я аккуратно унёс внутрь бинаря.</p>
<p>Заодно это стало хорошим pet-project&rsquo;ом: тесты с <code>-race</code>, <code>golangci-lint</code>, <code>mkdocs</code> для документации, релизы через <code>goreleaser</code>, а теперь и composite Action с верификацией бинарей. Маленький инструмент, который и время экономит, и руки в форме держит.</p>
<h2 id="минимальный-шаг-в-github-actions">Минимальный шаг в GitHub Actions</h2>
<p>Самый короткий путь — взять готовый Action и засунуть один шаг в workflow:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">jtprogru/indexnow@v0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.INDEXNOW_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sitemap</span><span class="p">:</span><span class="w"> </span><span class="l">https://example.com/sitemap.xml</span></span></span></code></pre></div>
</div>
<p>Всё. Этот шаг сам:</p>
<ul>
<li>определит ОС и архитектуру раннера (<code>linux/amd64</code>, <code>linux/arm64</code>, <code>darwin/arm64</code>, …);</li>
<li>скачает релизный бинарь нужной сборки со страницы релизов;</li>
<li>сверит его <code>sha256</code> с <code>checksums.txt</code>, который лежит рядом с бинарями в релизе — это защищает от «а что если кто-то подменил артефакт»;</li>
<li>закэширует бинарь в <code>runner.tool_cache</code>, чтобы следующие раны не качали его повторно;</li>
<li>запустит <code>indexnow submit</code> с переданными параметрами.</li>
</ul>
<p>Ключ передаётся через <code>env</code>, а не флагом, — даже если в твоём шаге случайно отрабатывает <code>set -x</code>, ключ в логах не светится. В <code>$GITHUB_STEP_SUMMARY</code> после прогона падает короткий отчёт: сколько URL улетело, сколько батчей упало, куда отправлял. Не нужно лезть в полный лог раннера, чтобы понять, прошёл submit или нет.</p>
<h2 id="submit-только-тех-url-что-изменились">Submit только тех URL, что изменились</h2>
<p>Слать весь sitemap при каждом пуше расточительно: поисковики переобойдут страницы и без тебя, IndexNow используешь именно для тех апдейтов, что важны прямо сейчас. Поэтому мой любимый рецепт — submit ровно того, что изменилось между предыдущим и текущим коммитом:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">jtprogru/indexnow@v0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.INDEXNOW_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sitemap</span><span class="p">:</span><span class="w"> </span><span class="l">https://example.com/sitemap.xml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sitemap-since</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.event.before }}</span></span></span></code></pre></div>
</div>
<p><code>sitemap-since</code> фильтрует записи sitemap по <code>&lt;lastmod&gt;</code>: всё, что старее переданного времени, отбрасывается.</p>
<p>Один граничный случай, который я заложил по умолчанию: <strong>записи без <code>&lt;lastmod&gt;</code> всегда проходят</strong>. «Нет сигнала о времени» трактуется как «могло измениться», и для IndexNow это безопасный дефолт — повторный submit идемпотентен, лишний раз дёрнуть не страшно. Куда хуже промолчать про реально изменившуюся страницу.</p>
<h2 id="а-если-sitemapу-не-доверяешь--urls-from">А если sitemap&rsquo;у не доверяешь — <code>urls-from</code></h2>
<p><code>sitemap-since</code> хорош, когда <code>&lt;lastmod&gt;</code> у тебя действительно проставляется. Если нет (или ты не хочешь полагаться на свежесть sitemap&rsquo;а вообще) — есть второй рецепт: собрать список URL из <code>git diff</code> между двумя коммитами и скормить его в Action через <code>urls-from</code>.</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">jtprogru/indexnow@v0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.INDEXNOW_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">urls-from</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      git diff --name-only &#34;${{ github.event.before }}..HEAD&#34; -- &#39;content/**/*.md&#39; |
</span></span></span><span class="line"><span class="cl"><span class="sd">        sed &#39;s#^content/\(.*\)/index\.md$#https://jtprog.ru/\1/#&#39;</span></span></span></code></pre></div>
</div>
<p><code>urls-from</code> — это просто bash-сниппет, чей stdout (по одной строке на URL) превращается в источник URL. Сниппет крутится в <code>$GITHUB_WORKSPACE</code>, так что <code>git</code> видит весь checked-out репозиторий и <code>git diff</code> работает как обычно. Пустой вывод — это не ошибка, шаг тихо выйдет с <code>submitted-count=0</code>.</p>
<p>Вот тот самый маппинг: <code>content/&lt;slug&gt;/index.md</code> → <code>https://jtprog.ru/&lt;slug&gt;/</code>. Никакого парсинга sitemap, никаких лишних submission&rsquo;ов на страницы, которые не трогал. <code>fetch-depth: 0</code> обязательно — без полной истории <code>git diff</code> против <code>github.event.before</code> молча отдаст пустоту.</p>
<h2 id="тег-v0-и-плавающие-версии">Тег <code>@v0</code> и плавающие версии</h2>
<p>Одна из мелких, но важных деталей — модель версионирования.</p>
<ul>
<li><code>@v0.7.0</code> — конкретный релиз. Поведение зафиксировано, обновления только если ты сам поправишь workflow.</li>
<li><code>@v0.7</code> — все патчи внутри <code>0.7.x</code>, обновляется автоматически.</li>
<li><code>@v0</code> — все релизы внутри <code>0.x</code>, едет с каждым новым минором.</li>
</ul>
<p>Плавающие теги (<code>v0</code>, <code>v0.5</code>) переставляются отдельным workflow на каждый релиз <code>v0.Y.Z</code>. То есть подписался один раз на <code>@v0</code> — и едешь на последнем <code>0.x</code> без правок workflow. Когда выкачу <code>v1.0.0</code>, тег <code>@v0</code> останется на последнем релизе ветки <code>0.x</code>: ты сам решаешь, переезжать ли на мажор. Никаких сюрпризов в духе «обновился — поведение сломалось».</p>
<h2 id="inputs-outputs-и-downstream-шаги">Inputs, outputs и downstream-шаги</h2>
<p>Inputs Action&rsquo;а зеркалят флаги CLI: источники URL (<code>urls</code> / <code>file</code> / <code>sitemap</code> / <code>urls-from</code>), <code>sitemap-since</code>, <code>endpoint</code>, <code>host</code>, <code>key-location</code>, <code>fail-on</code>, retry-knobs (<code>max-retries</code>, <code>base-backoff</code>, <code>max-backoff</code>), <code>dry-run</code>, <code>verbose</code>, <code>quiet</code>, <code>user-agent</code>. Между «попробовал локально, всё работает» и «засунул в CI» нет шага «а как там оно в Action называется» — один и тот же набор имён.</p>
<p>Outputs тоже из коробки:</p>
<ul>
<li><code>exit-code</code> — финальный код выхода CLI (<code>0</code> / <code>1</code> / <code>2</code>);</li>
<li><code>submitted-count</code> — сколько URL ушло (суммарно по батчам);</li>
<li><code>failed-count</code> — сколько батчей завершились не-2xx или ошибкой;</li>
<li><code>report</code> — одностраничная сводка; параллельно она же пишется в <code>$GITHUB_STEP_SUMMARY</code>.</li>
</ul>
<p>Этими outputs удобно дёргать downstream-шаги: уронить алерт в Telegram, если <code>failed-count &gt; 0</code>; или, наоборот, проигнорировать падение submit, если ты ему не доверяешь и не хочешь, чтобы из-за этого падал весь pipeline. Полный референс и готовые рецепты — в <a href="https://jtprogru.github.io/indexnow/guides/github-action/" rel="noopener noreferrer" target="_blank">docs → GitHub Action</a>.</p>
<h2 id="если-actions-не-вариант">Если Actions не вариант</h2>
<p>GitLab CI, Drone, Woodpecker, cron на VPS, локальный запуск перед публикацией поста — для всех этих кейсов есть тот же бинарь, ставится двумя способами:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew tap jtprogru/tap <span class="o">&amp;&amp;</span> brew install indexnow
</span></span><span class="line"><span class="cl"><span class="c1"># или</span>
</span></span><span class="line"><span class="cl">go install github.com/jtprogru/indexnow/cmd/indexnow@latest</span></span></code></pre></div>
</div>
<p>Готовые бинарники под Linux / macOS / FreeBSD в связках <code>amd64</code>/<code>arm64</code> лежат <a href="https://github.com/jtprogru/indexnow/releases" rel="noopener noreferrer" target="_blank">на странице релизов</a>. И дальше тот же <code>submit</code>, только из шелла:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">INDEXNOW_KEY</span><span class="o">=</span>8f7e6d5c4b3a29180706050403020100
</span></span><span class="line"><span class="cl">indexnow submit <span class="se">\
</span></span></span><span class="line"><span class="cl">    --sitemap https://example.com/sitemap.xml <span class="se">\
</span></span></span><span class="line"><span class="cl">    --sitemap-since 2026-06-01T00:00:00Z</span></span></code></pre></div>
</div>
<p>Источник URL — на выбор:</p>
<ul>
<li>позиционные аргументы: <code>indexnow submit https://a https://b</code>;</li>
<li><code>--file urls.txt</code> (по одному URL на строку, <code>#</code> — комментарий);</li>
<li><code>--stdin</code> — типичный pipe из другого CLI (<code>sitemap-to-urls | indexnow submit --stdin</code>);</li>
<li><code>--sitemap</code> — URL или локальный путь к XML.</li>
</ul>
<p>С sitemap&rsquo;ом отдельная история: <code>&lt;sitemapindex&gt;</code> раскрывается рекурсивно (ходишь по всем дочерним), а <code>.gz</code>-варианты гунзипятся прозрачно — отдельный шаг с <code>gunzip</code> не нужен. На больших проектах с десятками сегментированных sitemap&rsquo;ов это здорово упрощает жизнь.</p>
<h2 id="что-cli-делает-за-тебя">Что CLI делает за тебя</h2>
<p>Если коротко — всё то, что обычно копится в скриптах вокруг такой задачи:</p>
<ul>
<li><strong>Ретраи 429/5xx и transport-ошибок</strong> с экспоненциальным backoff&rsquo;ом, jitter&rsquo;ом и уважением к заголовку <code>Retry-After</code> (понимает и секунды, и HTTP-date). Ретраи без backoff превращают деградацию в катастрофу — особенно если поисковик уже под нагрузкой.</li>
<li><strong>Резка батчей</strong> по лимиту протокола (10 000 URL на запрос). Не нужно думать, что делать, когда у тебя 25 000 урлов: CLI сам режет на три батча, по каждому идёт отдельный submit и отдельный результат в отчёте.</li>
<li><strong>Несколько эндпоинтов сразу</strong>: <code>--endpoint bing,yandex,naver</code>. Если ты по какой-то причине не доверяешь «один эндпоинт расшарит всем», передай список — CLI отправит в каждый и склеит результат.</li>
<li><strong>Внятные exit code</strong> (<code>0</code> — всё ок, <code>1</code> — submission failed по сети/HTTP/<code>fail-on</code>, <code>2</code> — usage error: кривые флаги, нет источника URL, нет ключа) и флаг <code>--fail-on any|4xx|5xx|never</code>. Например, на 429 ты можешь не хотеть падать (это не твоя проблема), а на 4xx по ключу — обязательно.</li>
<li><strong>Управление ключом</strong> через <code>indexnow key</code>: <code>key gen --write public/</code> генерит ключ и кладёт hosted-файл в нужную директорию, <code>key verify --host example.com --key $KEY</code> дёргает GET по hosted-файлу и сверяет содержимое.</li>
</ul>
<p><code>key verify</code> стоит гонять до первого submit — чтобы убедиться, что key-файл реально отдаётся не только тебе из браузера, но и поисковикам. Иначе все submit&rsquo;ы будут возвращать 403, а ты будешь чесать репу и читать changelog в поисках сломанного релиза.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong><code>fetch-depth: 0</code> в <code>actions/checkout</code></strong>. Без полной истории <code>git diff</code> против <code>github.event.before</code> молча отдаст пустоту: shallow-клон не видит предыдущий коммит. Симптом — <code>submitted-count=0</code> на каждом пуше, и ты долго гадаешь, почему.</li>
<li><strong>Первый пуш в ветке.</strong> <code>github.event.before</code> там — нули (<code>0000000...</code>), <code>git diff 0000000..HEAD</code> сваливается с <code>fatal: bad revision</code>. Прикрой fallback&rsquo;ом на <code>HEAD~1</code>.</li>
<li><strong>CDN кеширует hosted key-файл.</strong> Cloudflare, Fastly и прочие радостно складывают <code>.txt</code> в кеш на пять-десять минут. Поменял ключ — поисковик в это окно ловит старую версию и отбивает 403 на твой свежий submit. После ротации — явный purge, без вариантов.</li>
<li><strong><code>&lt;lastmod&gt;</code> без timezone.</strong> <code>sitemap-since</code> ждёт RFC3339. Если Hugo (или чем ты там собираешь sitemap) пишет <code>2026-06-02T14:00:00</code> без <code>Z</code> и без смещения, фильтр поедет на таймзоне раннера: на GitHub-hosted это UTC, у себя на сервере — что настроил. Один и тот же sitemap начнёт давать разные срезы в зависимости от того, где крутится workflow.</li>
<li><strong>Параллельные эндпоинты ≠ страховка от лимитов.</strong> Отправил в <code>bing,yandex,naver</code> — формально получишь три ответа, но Bing всё равно поделится submission&rsquo;ом с остальными. Если поисковик отбил 429, лучше уменьшить частоту запусков, чем долбить параллельно во все доступные API.</li>
</ul>
<h2 id="где-взять-и-куда-нести-issue">Где взять и куда нести issue</h2>
<ul>
<li>Код, issues и звёздочки: <a href="https://github.com/jtprogru/indexnow" rel="noopener noreferrer" target="_blank">github.com/jtprogru/indexnow</a></li>
<li>Документация (EN + RU): <a href="https://jtprogru.github.io/indexnow/" rel="noopener noreferrer" target="_blank">jtprogru.github.io/indexnow</a></li>
<li>Гайд по GitHub Action: <a href="https://jtprogru.github.io/indexnow/guides/github-action/" rel="noopener noreferrer" target="_blank">jtprogru.github.io/indexnow/guides/github-action/</a></li>
</ul>
<p>Если интегрируешь — поделись опытом в issues или прямо в комментариях. Особенно интересны истории про GitLab/Drone: под GitHub Action у меня самые подробные тесты, а на других CI оно проверяется реже.</p>
<p>PS: расскажи, как у тебя сейчас устроена индексация — Search Console + ручной запрос, sitemap-only, или что-то более хитрое? И насколько вообще IndexNow дал прирост по скорости индексации? Хочется собрать чуть более широкие данные, чем у меня одного.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Выкатил новый пост, поправил страницу, обновил title — и сидишь, ждёшь, пока поисковики соизволят переобойти сайт. День, два, неделя. А хочется, чтобы апдейт подхватился сразу после <code>git push</code>, без танцев с Search Console и ручных submission-форм.</p>
<p>Дальше — как закрыть эту боль одним шагом в GitHub Actions через мой CLI <a href="https://github.com/jtprogru/indexnow" rel="noopener noreferrer" target="_blank">indexnow</a> и почему оно вообще работает. Как устроен сам пайплайн блога, я рассказывал в <a href="/posts/github-actions/">«Github Actions для блогера»</a> — новый шаг встаёт туда же, после деплоя.</p>
<h2 id="что-такое-indexnow-и-зачем-он-нужен">Что такое IndexNow и зачем он нужен</h2>
<p>Протокол <a href="https://www.indexnow.org/documentation" rel="noopener noreferrer" target="_blank">IndexNow</a> — это простой способ сказать поисковику: «эти URL изменились, переобойди их побыстрее, пожалуйста». Ты делаешь один HTTP-запрос с ключом и списком URL, поисковик в ответ подтверждает приём (или отбивает с осмысленной ошибкой) — и дальше уже его проблема, когда именно он переобойдёт страницы.</p>
<p>Главное: <strong>submission автоматически шарится между всеми участниками протокола</strong>. По спеке достаточно дёрнуть <strong>один</strong> эндпоинт (например, <code>api.indexnow.org</code> или <code>bing.com/indexnow</code>), и тот же запрос провалидируется и расшарится со всеми: Bing, Yandex, Naver, Seznam, Yep и другие участники. Google к протоколу формально не присоединился — для него по-прежнему остаётся sitemap + Search Console.</p>
<p>Аутентификация — самая «забавная» часть протокола. Ты придумываешь ключ (произвольная hex-строка длиной 8–128 символов), хостишь его в виде текстового файла на собственном домене, например <code>https://example.com/&lt;key&gt;.txt</code>, и присылаешь этот ключ в каждом запросе. Поисковик идёт по URL, читает файл, сверяет содержимое с тем ключом, что ты прислал. Совпало — submission принят. Нет — 403. Просто и без OAuth.</p>
<p>И тут же — мой любимый перл из <a href="https://yandex.ru/support/webmaster/ru/indexnow/key" rel="noopener noreferrer" target="_blank">документации Яндекс.Вебмастера</a>: «Ключ менять необязательно. Вы можете изменить ключ, если он стал известен третьим лицам.» То есть ключ, который ты по спеке обязан выложить публичным <code>.txt</code> на свой домен и который любой может вытащить одним <code>curl</code>, Яндекс предлагает ротировать, если он «стал известен третьим лицам». А он по дизайну протокола известен им всегда. Ну камон.</p>
<h2 id="зачем-я-написал-свой-cli">Зачем я написал свой CLI</h2>
<p>Дёргать API руками лениво, особенно когда у тебя на пуш приходится 10–50 изменившихся URL и хочется делать это из CI. Можно, конечно, нагуглить готовый пример с <code>curl</code> и <code>jq</code>, но:</p>
<ul>
<li>ошибки разбирать в bash больно;</li>
<li>ретраи на 429/5xx писать руками лень;</li>
<li>sitemap парсить из XML в bash — отдельный жанр перформанса;</li>
<li>лимит протокола в 10 000 URL на запрос тоже надо учитывать.</li>
</ul>
<p>Поэтому я написал на <a href="/posts/want-coding-golang/">Go</a> маленький <code>indexnow</code> — сначала как CLI для разовых submission&rsquo;ов, а с релиза <code>v0.5.0</code> это уже и <strong>готовый GitHub Action</strong>. По дороге всё то, что обычно копится в самописных скриптах, я аккуратно унёс внутрь бинаря.</p>
<p>Заодно это стало хорошим pet-project&rsquo;ом: тесты с <code>-race</code>, <code>golangci-lint</code>, <code>mkdocs</code> для документации, релизы через <code>goreleaser</code>, а теперь и composite Action с верификацией бинарей. Маленький инструмент, который и время экономит, и руки в форме держит.</p>
<h2 id="минимальный-шаг-в-github-actions">Минимальный шаг в GitHub Actions</h2>
<p>Самый короткий путь — взять готовый Action и засунуть один шаг в workflow:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">jtprogru/indexnow@v0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.INDEXNOW_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sitemap</span><span class="p">:</span><span class="w"> </span><span class="l">https://example.com/sitemap.xml</span></span></span></code></pre></div>
</div>
<p>Всё. Этот шаг сам:</p>
<ul>
<li>определит ОС и архитектуру раннера (<code>linux/amd64</code>, <code>linux/arm64</code>, <code>darwin/arm64</code>, …);</li>
<li>скачает релизный бинарь нужной сборки со страницы релизов;</li>
<li>сверит его <code>sha256</code> с <code>checksums.txt</code>, который лежит рядом с бинарями в релизе — это защищает от «а что если кто-то подменил артефакт»;</li>
<li>закэширует бинарь в <code>runner.tool_cache</code>, чтобы следующие раны не качали его повторно;</li>
<li>запустит <code>indexnow submit</code> с переданными параметрами.</li>
</ul>
<p>Ключ передаётся через <code>env</code>, а не флагом, — даже если в твоём шаге случайно отрабатывает <code>set -x</code>, ключ в логах не светится. В <code>$GITHUB_STEP_SUMMARY</code> после прогона падает короткий отчёт: сколько URL улетело, сколько батчей упало, куда отправлял. Не нужно лезть в полный лог раннера, чтобы понять, прошёл submit или нет.</p>
<h2 id="submit-только-тех-url-что-изменились">Submit только тех URL, что изменились</h2>
<p>Слать весь sitemap при каждом пуше расточительно: поисковики переобойдут страницы и без тебя, IndexNow используешь именно для тех апдейтов, что важны прямо сейчас. Поэтому мой любимый рецепт — submit ровно того, что изменилось между предыдущим и текущим коммитом:</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">jtprogru/indexnow@v0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.INDEXNOW_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sitemap</span><span class="p">:</span><span class="w"> </span><span class="l">https://example.com/sitemap.xml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sitemap-since</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.event.before }}</span></span></span></code></pre></div>
</div>
<p><code>sitemap-since</code> фильтрует записи sitemap по <code>&lt;lastmod&gt;</code>: всё, что старее переданного времени, отбрасывается.</p>
<p>Один граничный случай, который я заложил по умолчанию: <strong>записи без <code>&lt;lastmod&gt;</code> всегда проходят</strong>. «Нет сигнала о времени» трактуется как «могло измениться», и для IndexNow это безопасный дефолт — повторный submit идемпотентен, лишний раз дёрнуть не страшно. Куда хуже промолчать про реально изменившуюся страницу.</p>
<h2 id="а-если-sitemapу-не-доверяешь--urls-from">А если sitemap&rsquo;у не доверяешь — <code>urls-from</code></h2>
<p><code>sitemap-since</code> хорош, когда <code>&lt;lastmod&gt;</code> у тебя действительно проставляется. Если нет (или ты не хочешь полагаться на свежесть sitemap&rsquo;а вообще) — есть второй рецепт: собрать список URL из <code>git diff</code> между двумя коммитами и скормить его в Action через <code>urls-from</code>.</p>
<div class="codeblock codeblock--yaml" data-lang="yaml"><span class="codeblock__lang" aria-hidden="true">yaml</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">jtprogru/indexnow@v0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.INDEXNOW_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">urls-from</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      git diff --name-only &#34;${{ github.event.before }}..HEAD&#34; -- &#39;content/**/*.md&#39; |
</span></span></span><span class="line"><span class="cl"><span class="sd">        sed &#39;s#^content/\(.*\)/index\.md$#https://jtprog.ru/\1/#&#39;</span></span></span></code></pre></div>
</div>
<p><code>urls-from</code> — это просто bash-сниппет, чей stdout (по одной строке на URL) превращается в источник URL. Сниппет крутится в <code>$GITHUB_WORKSPACE</code>, так что <code>git</code> видит весь checked-out репозиторий и <code>git diff</code> работает как обычно. Пустой вывод — это не ошибка, шаг тихо выйдет с <code>submitted-count=0</code>.</p>
<p>Вот тот самый маппинг: <code>content/&lt;slug&gt;/index.md</code> → <code>https://jtprog.ru/&lt;slug&gt;/</code>. Никакого парсинга sitemap, никаких лишних submission&rsquo;ов на страницы, которые не трогал. <code>fetch-depth: 0</code> обязательно — без полной истории <code>git diff</code> против <code>github.event.before</code> молча отдаст пустоту.</p>
<h2 id="тег-v0-и-плавающие-версии">Тег <code>@v0</code> и плавающие версии</h2>
<p>Одна из мелких, но важных деталей — модель версионирования.</p>
<ul>
<li><code>@v0.7.0</code> — конкретный релиз. Поведение зафиксировано, обновления только если ты сам поправишь workflow.</li>
<li><code>@v0.7</code> — все патчи внутри <code>0.7.x</code>, обновляется автоматически.</li>
<li><code>@v0</code> — все релизы внутри <code>0.x</code>, едет с каждым новым минором.</li>
</ul>
<p>Плавающие теги (<code>v0</code>, <code>v0.5</code>) переставляются отдельным workflow на каждый релиз <code>v0.Y.Z</code>. То есть подписался один раз на <code>@v0</code> — и едешь на последнем <code>0.x</code> без правок workflow. Когда выкачу <code>v1.0.0</code>, тег <code>@v0</code> останется на последнем релизе ветки <code>0.x</code>: ты сам решаешь, переезжать ли на мажор. Никаких сюрпризов в духе «обновился — поведение сломалось».</p>
<h2 id="inputs-outputs-и-downstream-шаги">Inputs, outputs и downstream-шаги</h2>
<p>Inputs Action&rsquo;а зеркалят флаги CLI: источники URL (<code>urls</code> / <code>file</code> / <code>sitemap</code> / <code>urls-from</code>), <code>sitemap-since</code>, <code>endpoint</code>, <code>host</code>, <code>key-location</code>, <code>fail-on</code>, retry-knobs (<code>max-retries</code>, <code>base-backoff</code>, <code>max-backoff</code>), <code>dry-run</code>, <code>verbose</code>, <code>quiet</code>, <code>user-agent</code>. Между «попробовал локально, всё работает» и «засунул в CI» нет шага «а как там оно в Action называется» — один и тот же набор имён.</p>
<p>Outputs тоже из коробки:</p>
<ul>
<li><code>exit-code</code> — финальный код выхода CLI (<code>0</code> / <code>1</code> / <code>2</code>);</li>
<li><code>submitted-count</code> — сколько URL ушло (суммарно по батчам);</li>
<li><code>failed-count</code> — сколько батчей завершились не-2xx или ошибкой;</li>
<li><code>report</code> — одностраничная сводка; параллельно она же пишется в <code>$GITHUB_STEP_SUMMARY</code>.</li>
</ul>
<p>Этими outputs удобно дёргать downstream-шаги: уронить алерт в Telegram, если <code>failed-count &gt; 0</code>; или, наоборот, проигнорировать падение submit, если ты ему не доверяешь и не хочешь, чтобы из-за этого падал весь pipeline. Полный референс и готовые рецепты — в <a href="https://jtprogru.github.io/indexnow/guides/github-action/" rel="noopener noreferrer" target="_blank">docs → GitHub Action</a>.</p>
<h2 id="если-actions-не-вариант">Если Actions не вариант</h2>
<p>GitLab CI, Drone, Woodpecker, cron на VPS, локальный запуск перед публикацией поста — для всех этих кейсов есть тот же бинарь, ставится двумя способами:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew tap jtprogru/tap <span class="o">&amp;&amp;</span> brew install indexnow
</span></span><span class="line"><span class="cl"><span class="c1"># или</span>
</span></span><span class="line"><span class="cl">go install github.com/jtprogru/indexnow/cmd/indexnow@latest</span></span></code></pre></div>
</div>
<p>Готовые бинарники под Linux / macOS / FreeBSD в связках <code>amd64</code>/<code>arm64</code> лежат <a href="https://github.com/jtprogru/indexnow/releases" rel="noopener noreferrer" target="_blank">на странице релизов</a>. И дальше тот же <code>submit</code>, только из шелла:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">INDEXNOW_KEY</span><span class="o">=</span>8f7e6d5c4b3a29180706050403020100
</span></span><span class="line"><span class="cl">indexnow submit <span class="se">\
</span></span></span><span class="line"><span class="cl">    --sitemap https://example.com/sitemap.xml <span class="se">\
</span></span></span><span class="line"><span class="cl">    --sitemap-since 2026-06-01T00:00:00Z</span></span></code></pre></div>
</div>
<p>Источник URL — на выбор:</p>
<ul>
<li>позиционные аргументы: <code>indexnow submit https://a https://b</code>;</li>
<li><code>--file urls.txt</code> (по одному URL на строку, <code>#</code> — комментарий);</li>
<li><code>--stdin</code> — типичный pipe из другого CLI (<code>sitemap-to-urls | indexnow submit --stdin</code>);</li>
<li><code>--sitemap</code> — URL или локальный путь к XML.</li>
</ul>
<p>С sitemap&rsquo;ом отдельная история: <code>&lt;sitemapindex&gt;</code> раскрывается рекурсивно (ходишь по всем дочерним), а <code>.gz</code>-варианты гунзипятся прозрачно — отдельный шаг с <code>gunzip</code> не нужен. На больших проектах с десятками сегментированных sitemap&rsquo;ов это здорово упрощает жизнь.</p>
<h2 id="что-cli-делает-за-тебя">Что CLI делает за тебя</h2>
<p>Если коротко — всё то, что обычно копится в скриптах вокруг такой задачи:</p>
<ul>
<li><strong>Ретраи 429/5xx и transport-ошибок</strong> с экспоненциальным backoff&rsquo;ом, jitter&rsquo;ом и уважением к заголовку <code>Retry-After</code> (понимает и секунды, и HTTP-date). Ретраи без backoff превращают деградацию в катастрофу — особенно если поисковик уже под нагрузкой.</li>
<li><strong>Резка батчей</strong> по лимиту протокола (10 000 URL на запрос). Не нужно думать, что делать, когда у тебя 25 000 урлов: CLI сам режет на три батча, по каждому идёт отдельный submit и отдельный результат в отчёте.</li>
<li><strong>Несколько эндпоинтов сразу</strong>: <code>--endpoint bing,yandex,naver</code>. Если ты по какой-то причине не доверяешь «один эндпоинт расшарит всем», передай список — CLI отправит в каждый и склеит результат.</li>
<li><strong>Внятные exit code</strong> (<code>0</code> — всё ок, <code>1</code> — submission failed по сети/HTTP/<code>fail-on</code>, <code>2</code> — usage error: кривые флаги, нет источника URL, нет ключа) и флаг <code>--fail-on any|4xx|5xx|never</code>. Например, на 429 ты можешь не хотеть падать (это не твоя проблема), а на 4xx по ключу — обязательно.</li>
<li><strong>Управление ключом</strong> через <code>indexnow key</code>: <code>key gen --write public/</code> генерит ключ и кладёт hosted-файл в нужную директорию, <code>key verify --host example.com --key $KEY</code> дёргает GET по hosted-файлу и сверяет содержимое.</li>
</ul>
<p><code>key verify</code> стоит гонять до первого submit — чтобы убедиться, что key-файл реально отдаётся не только тебе из браузера, но и поисковикам. Иначе все submit&rsquo;ы будут возвращать 403, а ты будешь чесать репу и читать changelog в поисках сломанного релиза.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong><code>fetch-depth: 0</code> в <code>actions/checkout</code></strong>. Без полной истории <code>git diff</code> против <code>github.event.before</code> молча отдаст пустоту: shallow-клон не видит предыдущий коммит. Симптом — <code>submitted-count=0</code> на каждом пуше, и ты долго гадаешь, почему.</li>
<li><strong>Первый пуш в ветке.</strong> <code>github.event.before</code> там — нули (<code>0000000...</code>), <code>git diff 0000000..HEAD</code> сваливается с <code>fatal: bad revision</code>. Прикрой fallback&rsquo;ом на <code>HEAD~1</code>.</li>
<li><strong>CDN кеширует hosted key-файл.</strong> Cloudflare, Fastly и прочие радостно складывают <code>.txt</code> в кеш на пять-десять минут. Поменял ключ — поисковик в это окно ловит старую версию и отбивает 403 на твой свежий submit. После ротации — явный purge, без вариантов.</li>
<li><strong><code>&lt;lastmod&gt;</code> без timezone.</strong> <code>sitemap-since</code> ждёт RFC3339. Если Hugo (или чем ты там собираешь sitemap) пишет <code>2026-06-02T14:00:00</code> без <code>Z</code> и без смещения, фильтр поедет на таймзоне раннера: на GitHub-hosted это UTC, у себя на сервере — что настроил. Один и тот же sitemap начнёт давать разные срезы в зависимости от того, где крутится workflow.</li>
<li><strong>Параллельные эндпоинты ≠ страховка от лимитов.</strong> Отправил в <code>bing,yandex,naver</code> — формально получишь три ответа, но Bing всё равно поделится submission&rsquo;ом с остальными. Если поисковик отбил 429, лучше уменьшить частоту запусков, чем долбить параллельно во все доступные API.</li>
</ul>
<h2 id="где-взять-и-куда-нести-issue">Где взять и куда нести issue</h2>
<ul>
<li>Код, issues и звёздочки: <a href="https://github.com/jtprogru/indexnow" rel="noopener noreferrer" target="_blank">github.com/jtprogru/indexnow</a></li>
<li>Документация (EN + RU): <a href="https://jtprogru.github.io/indexnow/" rel="noopener noreferrer" target="_blank">jtprogru.github.io/indexnow</a></li>
<li>Гайд по GitHub Action: <a href="https://jtprogru.github.io/indexnow/guides/github-action/" rel="noopener noreferrer" target="_blank">jtprogru.github.io/indexnow/guides/github-action/</a></li>
</ul>
<p>Если интегрируешь — поделись опытом в issues или прямо в комментариях. Особенно интересны истории про GitLab/Drone: под GitHub Action у меня самые подробные тесты, а на других CI оно проверяется реже.</p>
<p>PS: расскажи, как у тебя сейчас устроена индексация — Search Console + ручной запрос, sitemap-only, или что-то более хитрое? И насколько вообще IndexNow дал прирост по скорости индексации? Хочется собрать чуть более широкие данные, чем у меня одного.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Надёжность строится в диалоге с бизнесом</title>
      <link>https://jtprog.ru/posts/reliability-is-a-conversation/</link>
      <pubDate>Thu, 28 May 2026 20:08:56 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/reliability-is-a-conversation/</guid>
      <category>SRE</category>
      <category>SLO</category>
      <category>Error Budget</category>
      <category>System Design</category>
      <category>Reliability</category>
      <category domain="category">SRE</category>
      <enclosure url="https://jtprog.ru/cover.svg" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.svg" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! В прошлой статье — <a href="/posts/slo-as-architecture-blueprint/">«SLO как чертёж архитектуры»</a> — я закончил тезисом, который тут же хочется развить: SLO выбирается не инженерами в одиночку, а вместе с бизнесом, потому что число в нём привязано к деньгам. А «вместе с бизнесом» в реальности чаще всего выглядит так: SRE придумал SLO, нарисовал дашборд, показал на демо, продакт покивал — и все разошлись. Чертёж готов. Надёжность&hellip; не выросла.</p>
<p>Так бывает, потому что чертёж сам по себе не работает. SLO/SLI/error budget — это <strong>не отчётный артефакт, а язык переговоров</strong>. Технический инструмент превращается в управленческий ровно в тот момент, когда у инженеров и у бизнеса одна и та же картинка целевой надёжности и одна и та же валюта компромиссов. Пока этого нет — у тебя есть красивый график, который никто не использует для принятия решений.</p>
<p>В этой статье я разворачиваю три практических следствия из этого тезиса, и все три — продолжение того же кейса с маркетплейсом (поиск каталога, 50M товаров, 30k RPS на пике), который я гонял в «чертеже»:</p>
<ol>
<li><strong>Диалог с бизнесом — это не презентация SLO, а формат принятия решений.</strong> SRE не «продаёт» таргет продакту, а вместе с ним выбирает уровень надёжности, политику бюджета ошибок и порядок разрешения спора «фичи против надёжности».</li>
<li><strong>System Design-собес проверяет именно этот навык.</strong> Большинство «вопросов по SLO» — не про арифметику девяток, а про умение собрать разговор с бизнесом, удержать рамку трейд-оффов и перевести «бизнес хочет 100%» в инженерную работу.</li>
<li><strong>Метрики SRE-команды меняются, когда продуктов больше одного.</strong> Ни «MTTR по компании», ни «SLO по сервису» как метрика команды не работают, когда команда обслуживает 5–10 продуктов. Нужен сводный язык.</li>
</ol>
<p>Если первая статья была про то, <strong>какой</strong> чертёж рисовать, то эта — про то, <strong>как сделать так, чтобы по чертежу действительно построили</strong>, а не повесили его в рамке на стену.</p>
<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<p>Статья длинная (~40 000 знаков). Сразу — что заберёт каждая роль:</p>
<ul>
<li><strong>SRE / SRE-lead.</strong> Как перестать «отдавать SLO» и начать вести по нему диалог: три признака, что разговор не состоялся; error budget policy как продуктовый, а не инженерный артефакт; и — отдельным блоком — какие метрики брать на команду, когда продуктов много, чтобы они не врали.</li>
<li><strong>Продакт / аналитик.</strong> Что именно бизнес должен уметь произнести сам (а не получать от SRE как откровение): где у нас критичные пользовательские пути, сколько стоит девятка в деньгах, кто и когда жмёт на стоп.</li>
<li><strong>Кандидат / интервьюер на System Design.</strong> Пять типовых собес-кейсов «про SLO», которые на самом деле проверяют переговорный навык, и разбор, что в них считается сильным ответом, а что — провалом.</li>
</ul>
<p>Общий блок для всех: почему <strong>надёжность нельзя «сделать», её можно только постоянно переторговывать</strong> — и как выглядит этот процесс в зрелой команде.</p>
<h2 id="tldr-если-читать-дальше-уже-некогда">TL;DR (если читать дальше уже некогда)</h2>
<p>Четыре тезиса, к которым приходит статья:</p>
<ol>
<li><strong>SLO без error budget policy — это украшение дашборда.</strong> Контракт из SLO делает записанное правило: что происходит, когда бюджет сгорел, и кто принимает по этому поводу решение. Само число ничего не гарантирует. Без правила — это график, мимо которого все ходят.</li>
<li><strong>Надёжность — это не состояние, а переговорный процесс.</strong> Её не «достигают» один раз. Каждый квартал бизнес и SRE заново выбирают уровень, потому что меняются нагрузка, деньги и приоритеты. SLO, который не пересматривают, врёт уже через полгода.</li>
<li><strong>На собесе по SLO проверяют не формулы, а умение поменять поведение бизнеса.</strong> Сильный ответ всегда начинается со сценария и денег, а не с топологии и девяток. «100% доступности» — не повод сказать «нельзя», а повод показать цену.</li>
<li><strong>Метрики SRE-команды при мультипродукте раскладываются на три слоя</strong> (надёжность продуктов / инженерное усилие / качество диалога) — один агрегат тут не справляется. Среднее по больнице — MTTR, composite SLO, число инцидентов — прячет ровно то, ради чего метрику заводили.</li>
</ol>
<p>Дальше — как до этого дойти. Если интересует только управленческая кульминация — переходи сразу к разделу <a href="#%d0%bc%d0%b5%d1%82%d1%80%d0%b8%d0%ba%d0%b8-sre-%d0%ba%d0%be%d0%bc%d0%b0%d0%bd%d0%b4%d1%8b-%d0%b4%d0%bb%d1%8f-%d0%bc%d1%83%d0%bb%d1%8c%d1%82%d0%b8%d0%bf%d1%80%d0%be%d0%b4%d1%83%d0%ba%d1%82%d0%b0">Метрики SRE-команды для мультипродукта</a>.</p>
<h2 id="чертёж-без-переговоров--это-просто-дашборд">Чертёж без переговоров — это просто дашборд</h2>
<p>Начнём с диагноза. В прошлой статье SLO получился как инженерный объект: числитель, знаменатель, окно, бюджет ошибок. Всё аккуратно. Но у этого объекта есть два режима существования, и разница между ними — это и есть вся статья.</p>
<h3 id="slo-как-артефакт-против-slo-как-контракта">SLO как артефакт против SLO как контракта</h3>
<p><strong>SLO-артефакт</strong> — это число на дашборде. Его придумали, согласовали галочкой, поставили мониторинг, иногда даже сделали красивый burn-rate-график. Он существует. Он измеряется. И он ни на что не влияет, потому что ни одно решение в компании не принимается, глядя на него.</p>
<p><strong>SLO-контракт</strong> — это то же число, но вокруг него выстроен процесс. Когда бюджет ошибок тает быстрее плана — кто-то останавливает рискованные деплои. Когда бюджет сгорел — спринт переключается на надёжность, и это не подвиг отдельного SRE, а записанное правило. Когда продакт хочет выкатить фичу к Чёрной пятнице — он сам спрашивает «сколько у нас осталось бюджета», потому что знает, что это его валюта тоже.</p>
<p>Разница видна по тому, как объект ведёт себя в жизни команды:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>SLO-артефакт</th>
					<th>SLO-контракт</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Где живёт</td>
					<td>На дашборде</td>
					<td>В процессе принятия решений</td>
			</tr>
			<tr>
					<td>Кто смотрит</td>
					<td>Дежурный SRE</td>
					<td>Продакт и SRE вместе</td>
			</tr>
			<tr>
					<td>Что происходит при сгорании</td>
					<td>Ничего («ну красный»)</td>
					<td>Срабатывает записанное правило</td>
			</tr>
			<tr>
					<td>Кто «владелец» числа</td>
					<td>Инженеры</td>
					<td>Продуктовый владелец + SRE</td>
			</tr>
			<tr>
					<td>Роль в споре «фича vs надёжность»</td>
					<td>Никакой</td>
					<td>Валюта, в которой спор решается</td>
			</tr>
			<tr>
					<td>Пересмотр</td>
					<td>Когда вспомнят</td>
					<td>Раз в квартал, ритуалом</td>
			</tr>
	</tbody>
</table>
<p>Разница между ними — не в качестве дашборда. Она в том, <strong>встроен ли SLO в принятие решений</strong>. И встроить его туда нельзя в одностороннем порядке: SRE не может «назначить» бизнесу, что теперь решения принимаются по бюджету ошибок. Это можно только договорить.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Главная подмена</p>
    <div class="callout__content"><p>«Мы внедрили SLO» почти всегда означает «мы настроили SLI и нарисовали дашборд». Это внедрение <strong>артефакта</strong>. Внедрение <strong>контракта</strong> — это когда на вопрос «что у нас происходит при сгорании бюджета» отвечает не SRE, а продакт, и отвечает одинаково с SRE. Пока ответы расходятся — контракта нет, есть график.</p></div>
  </div>
</aside><h3 id="три-признака-что-разговор-не-состоялся">Три признака, что разговор не состоялся</h3>
<p>Проверить, в каком режиме у тебя SLO, можно по трём симптомам. Если узнаёшь хотя бы один — диалога не было, был монолог SRE.</p>
<ol>
<li><strong>SLO «спущен сверху».</strong> Инженеры посчитали девятки, выбрали таргет «по аналогии с большими» и поставили продакта перед фактом. Продакт не участвовал в выборе числа — значит, он не считает его своим и не будет на него опираться в спорах. Для него это «инженерная штука», а не общий контракт.</li>
<li><strong>Нет error budget policy.</strong> SLO есть, бюджет считается, дашборд горит то зелёным, то красным. Но что конкретно делать, когда бюджет на исходе — нигде не записано. В итоге красный дашборд не меняет ничего: «ну красный, ну ладно, катим дальше».</li>
<li><strong>На пересечении бюджета никто не останавливает фичи.</strong> Самый жёсткий признак. Бюджет сгорел в ноль, а релизы продолжают выкатываться, потому что «дедлайн важнее». Это значит, что SLO проигрывает любому продуктовому приоритету — то есть фактически его нет.</li>
</ol>
<p>Все три признака — про одно: <strong>SLO не имеет оперативных последствий</strong>. А инструмент без последствий — это декорация.</p>
<h3 id="кейс-mercari-переименовать-мало-нужен-ритуал">Кейс Mercari: переименовать мало, нужен ритуал</h3>
<p>Хороший пример того, как легко перепутать артефакт с контрактом, — переход к User Journey SLO. Команды (в том числе Mercari, описавшие это в посте <a href="https://engineering.mercari.com/en/blog/entry/20241204-keeping-user-journey-slos-up-to-date-with-e2e-testing-in-a-microservices-architecture/" rel="noopener noreferrer" target="_blank">«Keeping User Journey SLOs Up-to-Date with E2E Testing»</a> в декабре 2024 года) приходят к тому, что SLO «на сервис» прячет пользовательскую боль, и переезжают на SLO, привязанные к пользовательскому пути. Идея правильная — она ровно из «чертежа»: SLI вешаются на шаги критичного пути, а не на сервис целиком.</p>
<p>Но вот ловушка: само по себе переименование «SLO сервиса X» → «SLO пользовательского пути Y» ничего не меняет, если за ним не стоит <strong>сопровождающий ритуал с продактом</strong>. Если User Journey SLO живёт там же, где жил старый — на дашборде, который смотрит только дежурный SRE, — это просто более точный артефакт. Та же декорация, только лучше нарисованная.</p>
<p>Работает переход тогда, когда вместе с новым SLO появляется регулярная встреча, на которой продакт смотрит на бюджет своего пути и принимает по нему решения. Не SRE показывает — продакт смотрит и решает. Переименование — это 10% работы. Остальные 90% — встроить новый объект в процесс продукта.</p>
<h3 id="почему-без-продуктового-владельца-это-не-лечится">Почему без продуктового владельца это не лечится</h3>
<p>Здесь стоит развести два понятия — <strong>продуктовую надёжность</strong> (видит ли пользователь, что сценарий работает) и <strong>техническую доступность</strong> (отвечает ли ручка <code>/search</code> двухсоткой). Важное следствие из этого разведения: продуктовая надёжность по определению не может принадлежать только SRE.</p>
<p>SRE владеет технической доступностью — это его зона: инфраструктура, репликация, failover, capacity. Но «работает ли поиск для пользователя так, что он покупает» — это вопрос, на который SRE физически не может ответить в одиночку, потому что «так, что он покупает» — это продуктовое определение, а не техническое. Кто решает, что пустая выдача по валидному запросу — это провал, а не «технически 200 OK»? Продакт. Кто решает, что цена на карточке старше минуты — это инцидент? Продакт.</p>
<p>Поэтому <strong>у продуктовой надёжности должен быть продуктовый владелец</strong>. Не «SRE отвечает за надёжность» — это и есть корень всех проблем из предыдущего раздела. А «SRE отвечает за инструментарий и техническую доступность, продакт — за то, что считается надёжностью с точки зрения пользователя, и они держат общий SLO». Диалог не состоится, пока с продуктовой стороны нет того, кто за этот SLO отвечает головой.</p>
<h2 id="что-мы-на-самом-деле-обсуждаем-с-бизнесом">Что мы на самом деле обсуждаем с бизнесом</h2>
<p>Допустим, продуктовый владелец есть и готов разговаривать. О чём конкретно идёт разговор? Не «покажи мне дашборд» — это не разговор. Настоящий диалог о надёжности крутится вокруг четырёх вещей.</p>
<h3 id="1-таргет-как-бюджет-на-компромисс">1. Таргет как бюджет на компромисс</h3>
<p>Главное, что должен понять бизнес: <strong>высокая надёжность — это не бесплатно и не всегда хорошо</strong>. Это позиция на шкале трейд-оффов.</p>
<ul>
<li>Высокий SLO = низкая скорость фич (бюджет ошибок мал, каждый релиз — ставка), дорогая инфраструктура (резервирование, регионы), медленные релизы (канарейка, постепенный rollout).</li>
<li>Низкий SLO = быстрые фичи, дешёвая инфраструктура, но больше видимых пользователю сбоев.</li>
</ul>
<p>Из «чертежа» помним конкретные множители для нашего маркетплейса: переход с 99.9% на 99.99% — это <code>4–6×</code> → <code>15–25×</code> baseline по инфраструктуре и <code>2–3×</code> → <code>8–10×</code> по людям. Это не «инженеры хотят красивую цифру». Это бизнес-решение, потому что платит за него бизнес, и отказывается от скорости фич — тоже бизнес.</p>
<p>Ключевая фраза, которую SRE должен уметь произнести в этом разговоре, звучит примерно так:</p>
<blockquote><p>«Если снизим SLO поиска с 99.99% до 99.9% — освободим вот этот регион и вот эту часть дежурной нагрузки, сможем катить фичи в полтора раза быстрее. Заплатим за это вот таким ростом видимых сбоев на пике. Берём?»</p>
</blockquote><p>Это и есть перевод надёжности в валюту, понятную обеим сторонам. Не «давайте сделаем понадёжнее», а «вот цена, вот что освобождается, решаем вместе».</p>
<h3 id="2-error-budget-policy-правила-пишутся-до-пожара-а-не-во-время">2. Error budget policy: правила пишутся до пожара, а не во время</h3>
<p>Error budget policy — это <strong>заранее зафиксированные правила игры</strong>. Не «что мы будем делать, когда бюджет сгорит» (это решается в панике и проигрывается дедлайну), а «что происходит автоматически, когда бюджет сгорел» — написанное и согласованное обеими сторонами заранее.</p>
<p>Минимальная политика отвечает на четыре вопроса:</p>
<ul>
<li><strong>Кто и при каком условии останавливает релизы?</strong> Например: при burn-rate выше <code>14.4×</code> за час — автоматический freeze рискованных деплоев до конца окна.</li>
<li><strong>Кто разрешает продолжить?</strong> Не «инженер решил» и не «продакт продавил», а названная роль с названным условием.</li>
<li><strong>Что переходит в roadmap при сгоревшем бюджете?</strong> Reliability-задачи получают приоритет над фичами — и это записано, а не выпрашивается каждый раз заново.</li>
<li><strong>Когда и как пересматривается сам SLO?</strong> Раз в квартал — короткий ритуал «изменилось ли что-то в бизнесе или системе, что меняет нужное число».</li>
</ul>
<p>На практике это умещается на одну страницу — по образцу error budget policy из главы <a href="https://sre.google/workbook/implementing-slos/" rel="noopener noreferrer" target="_blank">«Implementing SLOs»</a> в Google SRE Workbook. Для поиска нашего маркетплейса политика могла бы выглядеть так:</p>
<blockquote><p><strong>Error budget policy — поиск каталога (SLO 99.9%, окно 30 дней)</strong></p>
<ul>
<li><strong>Жёлтая зона</strong> (потрачено 50% бюджета): уведомление в канал продукта, ретроспектива на ближайшем синке, новых рискованных экспериментов не начинаем.</li>
<li><strong>Красная зона</strong> (потрачено 90%): freeze рискованных деплоев — катим только хотфиксы и security-патчи. Reliability-задачи поднимаются над фичами в текущем спринте.</li>
<li><strong>Бюджет исчерпан</strong> (100%): полный freeze фич до конца окна. Эскалация продуктовому владельцу, обязательный разбор причин.</li>
<li><strong>Кто снимает freeze:</strong> продуктовый владелец по согласованию с SRE-lead, не раньше чем burn-rate вернулся к норме.</li>
<li><strong>Пересмотр SLO:</strong> первый понедельник квартала, участвуют продакт и SRE-lead.</li>
<li><strong>Подписали:</strong> <em>имя продуктового владельца</em>, <em>имя SRE-lead</em>, дата.</li>
</ul>
</blockquote><p>Эта страница важнее любого дашборда. Дашборд показывает, что бюджет горит. Страница говорит, что с этим делать и кто за это отвечает.</p>
<aside class="callout callout--warn" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
  <div class="callout__body"><p class="callout__title">Без политики SLO проигрывает всегда</p>
    <div class="callout__content"><p>Если решение «морозим или катим» принимается в момент пожара, оно принимается властью: кто выше по иерархии или ближе к CEO, тот и победил. Записанная заранее политика — это способ вынуть это решение из-под давления момента. Её главная ценность не в тексте, а в том, что её согласовали, когда никто не был в панике.</p></div>
  </div>
</aside><p>Важнейшее уточнение, которое часто упускают: <strong>error budget policy — это продуктовый артефакт, а не инженерный</strong>. Его подписывает не тимлид SRE «для себя», а продуктовый владелец — потому что это он соглашается, что при сгоревшем бюджете его фичи подождут. Подпись здесь не метафора: должно быть понятно, кто конкретно сказал «да» этим правилам. Если не можешь назвать имя — политики нет.</p>
<h3 id="3-перевод-девяток-в-деньги">3. Перевод девяток в деньги</h3>
<p>Бизнесу нужны не проценты, а валюта. «99.9% против 99.95%» для продакта — пустой звук, пока ты не показал, что стоит за этой разницей в деньгах. Поэтому в зрелом диалоге всегда есть связка <code>SLI → ₽</code>.</p>
<p>Берём наш маркетплейс. Из «чертежа»: GMV порядка 1 млрд ₽ в день, поиск кормит 40–70% покупок, каждые <code>+100 ms</code> латентности на ключевом read path стоят примерно <code>−0.5…1.5%</code> конверсии. Отсюда — простой мост:</p>
<div class="math-block" data-png="https://jtprog.ru/math/f26382c26b874de2f689194fa5fc707d760b5a298a8ee7d013c92ae9bc3b08a7.png"><span class="math__tex">$$ \text{Стоимость простоя} = \text{GMV в час} \times \text{доля задетого трафика} \times \text{длительность} $$</span></div><p>Дальше бюджет ошибок перестаёт быть процентом и становится деньгами. Сгоревший за инцидент бюджет — это не «−2% за час на графике», а «вот столько недополученной выручки, если такой инцидент повторится на пике». Burn-rate из множителя (<code>14.4×</code>, <code>36×</code> — подробно про него в статье <a href="/posts/burn-rate-is-not-speed/">«Скорость сгорания бюджета ошибок»</a>) превращается в скорость утечки денег: при таком-то burn-rate на пике Чёрной пятницы мы теряем столько-то рублей в минуту.</p>
<p>Посчитаем на пальцах для одного и того же 15-минутного инцидента в поиске — полная недоступность пути. GMV 1 млрд ₽ в день размазан неравномерно: обычным днём через поиск проходит порядка 400 млн ₽ выручки (предположим, 40% покупок стартуют с поиска), на пике распродажи дневной GMV умножается в <code>×3–5</code>, и доля стартующих с поиска покупок растёт.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Обычный вторник</th>
					<th>Пик распродажи</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Выручка через поиск за день</td>
					<td>~400 млн ₽</td>
					<td>~1,5 млрд ₽</td>
			</tr>
			<tr>
					<td>Выручка через поиск в час (грубо)</td>
					<td>~17 млн ₽</td>
					<td>~120 млн ₽</td>
			</tr>
			<tr>
					<td>Полный простой поиска 15 минут</td>
					<td>~4,2 млн ₽</td>
					<td>~30 млн ₽</td>
			</tr>
			<tr>
					<td>Тот же burn-rate в долях бюджета</td>
					<td>один и тот же <code>~50×</code></td>
					<td>один и тот же <code>~50×</code></td>
			</tr>
	</tbody>
</table>
<p>Цифры грубые и зависят от профиля трафика — но порядок и так говорит главное: <strong>один и тот же инцидент с одним и тем же burn-rate стоит на пике в разы дороже</strong>. Поэтому «потеряли 15 минут» — бессмысленная фраза без контекста «когда». А burn-rate как безразмерный множитель тут особенно удобен: <code>50×</code> одинаково тревожен и во вторник, и в Чёрную пятницу, просто в деньгах разворачивается по-разному.</p>
<p>Это разговор, который продакт понимает мгновенно. Не «у нас burn-rate 14.4», а «при текущем burn-rate мы за час пикового трафика теряем выручку, равную недельной зарплате команды — продолжаем катить?». Конвертация в деньги и переводит SLO из инженерной плоскости в управленческую.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Сезонность — это не деталь, это половина разговора</p>
    <div class="callout__content"><p>Пять минут даунтайма обычным вторником и пять минут в Чёрную пятницу — это разные деньги в десятки раз. Бюджет ошибок «горит» неравномерно: на пике RPS ты расходуешь его в разы быстрее. Поэтому проектировать SLO и считать стоимость инцидента надо на пик, а не на среднюю нагрузку. Бизнес это, кстати, обычно понимает лучше инженеров — он живёт сезонностью.</p></div>
  </div>
</aside><h3 id="4-где-у-нас-критичные-пользовательские-пути">4. «Где у нас критичные пользовательские пути»</h3>
<p>И последнее, самое неочевидное. Список критичных пользовательских путей (CUJ — critical user journey) бизнес должен уметь произнести <strong>сам</strong>. Это не то, что SRE приносит продакту как откровение — «смотри, я тут проанализировал, у вас критичен поиск». Если CUJ известны только инженерам, значит, бизнес не владеет собственной надёжностью.</p>
<p>Хорошая проверка зрелости диалога: спроси продакта «назови три сценария, простой которых на 30 минут стоит нам больше всего». Если он отвечает не задумываясь — диалог здоровый. Если лезет к SRE «а какие у нас там пути» — надёжность пока живёт в инженерной резервации.</p>
<p>Для нашего маркетплейса CUJ — это путь «пришёл с намерением купить → нашёл → выбрал → положил в корзину → оплатил». И продакт должен понимать, что поиск (вход в этот путь) и оплата (выход) — разного класса критичности, и что на них вешаются разные SLO. Это понимание — не бонус, это условие, без которого все предыдущие три пункта повисают в воздухе.</p>
<h3 id="диалог--это-цикл-а-не-презентация">Диалог — это цикл, а не презентация</h3>
<p>Сложим четыре пункта вместе. Диалог о надёжности — это не разовое «защитили SLO на демо», а повторяющийся цикл:</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/6645021c4c1a29e764d5d3a587329b900a85f97d7cee794c4f9c0f193d89c1a8.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-6645021c4c1a-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1710.91px; background-color: transparent;" viewBox="0 0 1710.90625 148.5" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-6645021c4c1a-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-6645021c4c1a-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-light .error-icon{fill:#dce0e8;}#m-6645021c4c1a-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-6645021c4c1a-light .edge-thickness-normal{stroke-width:1px;}#m-6645021c4c1a-light .edge-thickness-thick{stroke-width:3.5px;}#m-6645021c4c1a-light .edge-pattern-solid{stroke-dasharray:0;}#m-6645021c4c1a-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-6645021c4c1a-light .edge-pattern-dashed{stroke-dasharray:3;}#m-6645021c4c1a-light .edge-pattern-dotted{stroke-dasharray:2;}#m-6645021c4c1a-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-6645021c4c1a-light .marker.cross{stroke:#5c5f77;}#m-6645021c4c1a-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-6645021c4c1a-light p{margin:0;}#m-6645021c4c1a-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-6645021c4c1a-light .cluster-label text{fill:#4c4f69;}#m-6645021c4c1a-light .cluster-label span{color:#4c4f69;}#m-6645021c4c1a-light .cluster-label span p{background-color:transparent;}#m-6645021c4c1a-light .label text,#m-6645021c4c1a-light span{fill:#4c4f69;color:#4c4f69;}#m-6645021c4c1a-light .node rect,#m-6645021c4c1a-light .node circle,#m-6645021c4c1a-light .node ellipse,#m-6645021c4c1a-light .node polygon,#m-6645021c4c1a-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-6645021c4c1a-light .rough-node .label text,#m-6645021c4c1a-light .node .label text,#m-6645021c4c1a-light .image-shape .label,#m-6645021c4c1a-light .icon-shape .label{text-anchor:middle;}#m-6645021c4c1a-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-6645021c4c1a-light .rough-node .label,#m-6645021c4c1a-light .node .label,#m-6645021c4c1a-light .image-shape .label,#m-6645021c4c1a-light .icon-shape .label{text-align:center;}#m-6645021c4c1a-light .node.clickable{cursor:pointer;}#m-6645021c4c1a-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-6645021c4c1a-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-6645021c4c1a-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-6645021c4c1a-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-6645021c4c1a-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-6645021c4c1a-light .edgeLabel p{background-color:#eff1f5;}#m-6645021c4c1a-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-6645021c4c1a-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-6645021c4c1a-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-6645021c4c1a-light .cluster text{fill:#4c4f69;}#m-6645021c4c1a-light .cluster span{color:#4c4f69;}#m-6645021c4c1a-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-6645021c4c1a-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-6645021c4c1a-light rect.text{fill:none;stroke-width:0;}#m-6645021c4c1a-light .icon-shape,#m-6645021c4c1a-light .image-shape{background-color:#eff1f5;text-align:center;}#m-6645021c4c1a-light .icon-shape p,#m-6645021c4c1a-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-6645021c4c1a-light .icon-shape .label rect,#m-6645021c4c1a-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-6645021c4c1a-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-6645021c4c1a-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-6645021c4c1a-light .node .neo-node{stroke:#bcc0cc;}#m-6645021c4c1a-light [data-look="neo"].node rect,#m-6645021c4c1a-light [data-look="neo"].cluster rect,#m-6645021c4c1a-light [data-look="neo"].node polygon{stroke:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].node path{stroke:url(#m-6645021c4c1a-light-gradient);stroke-width:1px;}#m-6645021c4c1a-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-6645021c4c1a-light [data-look="neo"].node circle{stroke:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-6645021c4c1a-light [data-look="neo"].icon-shape .icon{fill:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-6645021c4c1a-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M254.531,63.811L258.698,62.634C262.865,61.457,271.198,59.104,278.865,57.927C286.531,56.75,293.531,56.75,297.031,56.75L300.531,56.75" id="m-6645021c4c1a-light-L_CUJ_Target_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_CUJ_Target_0" data-points="W3sieCI6MjU0LjUzMTI1LCJ5Ijo2My44MTA4MDcyNTA1MDA1OH0seyJ4IjoyNzkuNTMxMjUsInkiOjU2Ljc1fSx7IngiOjMwNC41MzEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M563.578,56.75L567.745,56.75C571.911,56.75,580.245,56.75,587.911,56.75C595.578,56.75,602.578,56.75,606.078,56.75L609.578,56.75" id="m-6645021c4c1a-light-L_Target_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Target_Policy_0" data-points="W3sieCI6NTYzLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NTg4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NjEzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M873.578,56.75L877.745,56.75C881.911,56.75,890.245,56.75,897.911,56.75C905.578,56.75,912.578,56.75,916.078,56.75L919.578,56.75" id="m-6645021c4c1a-light-L_Policy_Run_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Run_0" data-points="W3sieCI6ODczLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6ODk4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6OTIzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M1168.672,56.75L1172.839,56.75C1177.005,56.75,1185.339,56.75,1193.005,56.75C1200.672,56.75,1207.672,56.75,1211.172,56.75L1214.672,56.75" id="m-6645021c4c1a-light-L_Run_Money_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Run_Money_0" data-points="W3sieCI6MTE2OC42NzE4NzUsInkiOjU2Ljc1fSx7IngiOjExOTMuNjcxODc1LCJ5Ijo1Ni43NX0seyJ4IjoxMjE4LjY3MTg3NSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M1412.141,56.75L1416.307,56.75C1420.474,56.75,1428.807,56.75,1436.5,57.766C1444.193,58.781,1451.245,60.812,1454.771,61.828L1458.297,62.844" id="m-6645021c4c1a-light-L_Money_Review_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Money_Review_0" data-points="W3sieCI6MTQxMi4xNDA2MjUsInkiOjU2Ljc1fSx7IngiOjE0MzcuMTQwNjI1LCJ5Ijo1Ni43NX0seyJ4IjoxNDYyLjE0MDYyNSwieSI6NjMuOTUwODE2ODA5MDcwODh9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M1462.141,133.299L1457.974,134.499C1453.807,135.699,1445.474,138.1,1421.018,139.3C1396.563,140.5,1355.984,140.5,1315.406,140.5C1274.828,140.5,1234.25,140.5,1189.37,140.5C1144.49,140.5,1095.307,140.5,1046.125,140.5C996.943,140.5,947.76,140.5,897.336,140.5C846.911,140.5,795.245,140.5,743.578,140.5C691.911,140.5,640.245,140.5,588.658,140.5C537.07,140.5,485.563,140.5,434.055,140.5C382.547,140.5,331.039,140.5,301.76,139.504C272.481,138.509,265.431,136.518,261.906,135.522L258.381,134.526" id="m-6645021c4c1a-light-L_Review_CUJ_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Review_CUJ_0" data-points="W3sieCI6MTQ2Mi4xNDA2MjUsInkiOjEzMy4yOTkxODMxOTA5MjkxfSx7IngiOjE0MzcuMTQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMzE1LjQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMTkzLjY3MTg3NSwieSI6MTQwLjV9LHsieCI6MTA0Ni4xMjUsInkiOjE0MC41fSx7IngiOjg5OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjc0My41NzgxMjUsInkiOjE0MC41fSx7IngiOjU4OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjQzNC4wNTQ2ODc1LCJ5IjoxNDAuNX0seyJ4IjoyNzkuNTMxMjUsInkiOjE0MC41fSx7IngiOjI1NC41MzEyNSwieSI6MTMzLjQzOTE5Mjc0OTQ5OTQzfV0=" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_CUJ_Target_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Target_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Run_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Run_Money_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Money_Review_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Review_CUJ_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-6645021c4c1a-light-flowchart-CUJ-0" data-look="classic" transform="translate(131.265625, 98.625)"><rect class="basic label-container" style="" x="-123.265625" y="-37.5" width="246.53125" height="75"/><g class="label" style="" transform="translate(-93.265625, -22.5)"><rect/><foreignObject width="186.53125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Бизнес называет<br />критичные пути и их цену</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Target-1" data-look="classic" transform="translate(434.0546875, 56.75)"><rect class="basic label-container" style="" x="-129.5234375" y="-37.5" width="259.046875" height="75"/><g class="label" style="" transform="translate(-99.5234375, -22.5)"><rect/><foreignObject width="199.046875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Вместе выбираем SLO<br />как бюджет на компромисс</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Policy-3" data-look="classic" transform="translate(743.578125, 56.75)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Записываем error budget policy<br />продакт подписывает</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Run-5" data-look="classic" transform="translate(1046.125, 56.75)"><rect class="basic label-container" style="" x="-122.546875" y="-37.5" width="245.09375" height="75"/><g class="label" style="" transform="translate(-92.546875, -22.5)"><rect/><foreignObject width="185.09375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Живём по бюджету:<br />деплои, инциденты, фичи</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Money-7" data-look="classic" transform="translate(1315.40625, 56.75)"><rect class="basic label-container" style="" x="-96.734375" y="-37.5" width="193.46875" height="75"/><g class="label" style="" transform="translate(-66.734375, -22.5)"><rect/><foreignObject width="133.46875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Считаем сгорание<br />в деньгах</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Review-9" data-look="classic" transform="translate(1582.5234375, 98.625)"><rect class="basic label-container" style="" x="-120.3828125" y="-37.5" width="240.765625" height="75"/><g class="label" style="" transform="translate(-90.3828125, -22.5)"><rect/><foreignObject width="180.765625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Квартальный пересмотр:<br />изменилось ли число</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-6645021c4c1a-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-6645021c4c1a-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-6645021c4c1a-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-6645021c4c1a-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1710.91px; background-color: transparent;" viewBox="0 0 1710.90625 148.5" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-6645021c4c1a-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-6645021c4c1a-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-dark .error-icon{fill:#181926;}#m-6645021c4c1a-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-6645021c4c1a-dark .edge-thickness-normal{stroke-width:1px;}#m-6645021c4c1a-dark .edge-thickness-thick{stroke-width:3.5px;}#m-6645021c4c1a-dark .edge-pattern-solid{stroke-dasharray:0;}#m-6645021c4c1a-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-6645021c4c1a-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-6645021c4c1a-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-6645021c4c1a-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-6645021c4c1a-dark .marker.cross{stroke:#a5adcb;}#m-6645021c4c1a-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-6645021c4c1a-dark p{margin:0;}#m-6645021c4c1a-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-6645021c4c1a-dark .cluster-label text{fill:#cad3f5;}#m-6645021c4c1a-dark .cluster-label span{color:#cad3f5;}#m-6645021c4c1a-dark .cluster-label span p{background-color:transparent;}#m-6645021c4c1a-dark .label text,#m-6645021c4c1a-dark span{fill:#cad3f5;color:#cad3f5;}#m-6645021c4c1a-dark .node rect,#m-6645021c4c1a-dark .node circle,#m-6645021c4c1a-dark .node ellipse,#m-6645021c4c1a-dark .node polygon,#m-6645021c4c1a-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-6645021c4c1a-dark .rough-node .label text,#m-6645021c4c1a-dark .node .label text,#m-6645021c4c1a-dark .image-shape .label,#m-6645021c4c1a-dark .icon-shape .label{text-anchor:middle;}#m-6645021c4c1a-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-6645021c4c1a-dark .rough-node .label,#m-6645021c4c1a-dark .node .label,#m-6645021c4c1a-dark .image-shape .label,#m-6645021c4c1a-dark .icon-shape .label{text-align:center;}#m-6645021c4c1a-dark .node.clickable{cursor:pointer;}#m-6645021c4c1a-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-6645021c4c1a-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-6645021c4c1a-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-6645021c4c1a-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-6645021c4c1a-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-6645021c4c1a-dark .edgeLabel p{background-color:#24273a;}#m-6645021c4c1a-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-6645021c4c1a-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-6645021c4c1a-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-6645021c4c1a-dark .cluster text{fill:#cad3f5;}#m-6645021c4c1a-dark .cluster span{color:#cad3f5;}#m-6645021c4c1a-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-6645021c4c1a-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-6645021c4c1a-dark rect.text{fill:none;stroke-width:0;}#m-6645021c4c1a-dark .icon-shape,#m-6645021c4c1a-dark .image-shape{background-color:#24273a;text-align:center;}#m-6645021c4c1a-dark .icon-shape p,#m-6645021c4c1a-dark .image-shape p{background-color:#24273a;padding:2px;}#m-6645021c4c1a-dark .icon-shape .label rect,#m-6645021c4c1a-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-6645021c4c1a-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-6645021c4c1a-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-6645021c4c1a-dark .node .neo-node{stroke:#494d64;}#m-6645021c4c1a-dark [data-look="neo"].node rect,#m-6645021c4c1a-dark [data-look="neo"].cluster rect,#m-6645021c4c1a-dark [data-look="neo"].node polygon{stroke:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].node path{stroke:url(#m-6645021c4c1a-dark-gradient);stroke-width:1px;}#m-6645021c4c1a-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-6645021c4c1a-dark [data-look="neo"].node circle{stroke:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-6645021c4c1a-dark [data-look="neo"].icon-shape .icon{fill:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-6645021c4c1a-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M254.531,63.811L258.698,62.634C262.865,61.457,271.198,59.104,278.865,57.927C286.531,56.75,293.531,56.75,297.031,56.75L300.531,56.75" id="m-6645021c4c1a-dark-L_CUJ_Target_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_CUJ_Target_0" data-points="W3sieCI6MjU0LjUzMTI1LCJ5Ijo2My44MTA4MDcyNTA1MDA1OH0seyJ4IjoyNzkuNTMxMjUsInkiOjU2Ljc1fSx7IngiOjMwNC41MzEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M563.578,56.75L567.745,56.75C571.911,56.75,580.245,56.75,587.911,56.75C595.578,56.75,602.578,56.75,606.078,56.75L609.578,56.75" id="m-6645021c4c1a-dark-L_Target_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Target_Policy_0" data-points="W3sieCI6NTYzLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NTg4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NjEzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M873.578,56.75L877.745,56.75C881.911,56.75,890.245,56.75,897.911,56.75C905.578,56.75,912.578,56.75,916.078,56.75L919.578,56.75" id="m-6645021c4c1a-dark-L_Policy_Run_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Run_0" data-points="W3sieCI6ODczLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6ODk4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6OTIzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M1168.672,56.75L1172.839,56.75C1177.005,56.75,1185.339,56.75,1193.005,56.75C1200.672,56.75,1207.672,56.75,1211.172,56.75L1214.672,56.75" id="m-6645021c4c1a-dark-L_Run_Money_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Run_Money_0" data-points="W3sieCI6MTE2OC42NzE4NzUsInkiOjU2Ljc1fSx7IngiOjExOTMuNjcxODc1LCJ5Ijo1Ni43NX0seyJ4IjoxMjE4LjY3MTg3NSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M1412.141,56.75L1416.307,56.75C1420.474,56.75,1428.807,56.75,1436.5,57.766C1444.193,58.781,1451.245,60.812,1454.771,61.828L1458.297,62.844" id="m-6645021c4c1a-dark-L_Money_Review_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Money_Review_0" data-points="W3sieCI6MTQxMi4xNDA2MjUsInkiOjU2Ljc1fSx7IngiOjE0MzcuMTQwNjI1LCJ5Ijo1Ni43NX0seyJ4IjoxNDYyLjE0MDYyNSwieSI6NjMuOTUwODE2ODA5MDcwODh9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M1462.141,133.299L1457.974,134.499C1453.807,135.699,1445.474,138.1,1421.018,139.3C1396.563,140.5,1355.984,140.5,1315.406,140.5C1274.828,140.5,1234.25,140.5,1189.37,140.5C1144.49,140.5,1095.307,140.5,1046.125,140.5C996.943,140.5,947.76,140.5,897.336,140.5C846.911,140.5,795.245,140.5,743.578,140.5C691.911,140.5,640.245,140.5,588.658,140.5C537.07,140.5,485.563,140.5,434.055,140.5C382.547,140.5,331.039,140.5,301.76,139.504C272.481,138.509,265.431,136.518,261.906,135.522L258.381,134.526" id="m-6645021c4c1a-dark-L_Review_CUJ_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Review_CUJ_0" data-points="W3sieCI6MTQ2Mi4xNDA2MjUsInkiOjEzMy4yOTkxODMxOTA5MjkxfSx7IngiOjE0MzcuMTQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMzE1LjQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMTkzLjY3MTg3NSwieSI6MTQwLjV9LHsieCI6MTA0Ni4xMjUsInkiOjE0MC41fSx7IngiOjg5OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjc0My41NzgxMjUsInkiOjE0MC41fSx7IngiOjU4OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjQzNC4wNTQ2ODc1LCJ5IjoxNDAuNX0seyJ4IjoyNzkuNTMxMjUsInkiOjE0MC41fSx7IngiOjI1NC41MzEyNSwieSI6MTMzLjQzOTE5Mjc0OTQ5OTQzfV0=" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_CUJ_Target_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Target_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Run_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Run_Money_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Money_Review_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Review_CUJ_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-6645021c4c1a-dark-flowchart-CUJ-0" data-look="classic" transform="translate(131.265625, 98.625)"><rect class="basic label-container" style="" x="-123.265625" y="-37.5" width="246.53125" height="75"/><g class="label" style="" transform="translate(-93.265625, -22.5)"><rect/><foreignObject width="186.53125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Бизнес называет<br />критичные пути и их цену</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Target-1" data-look="classic" transform="translate(434.0546875, 56.75)"><rect class="basic label-container" style="" x="-129.5234375" y="-37.5" width="259.046875" height="75"/><g class="label" style="" transform="translate(-99.5234375, -22.5)"><rect/><foreignObject width="199.046875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Вместе выбираем SLO<br />как бюджет на компромисс</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Policy-3" data-look="classic" transform="translate(743.578125, 56.75)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Записываем error budget policy<br />продакт подписывает</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Run-5" data-look="classic" transform="translate(1046.125, 56.75)"><rect class="basic label-container" style="" x="-122.546875" y="-37.5" width="245.09375" height="75"/><g class="label" style="" transform="translate(-92.546875, -22.5)"><rect/><foreignObject width="185.09375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Живём по бюджету:<br />деплои, инциденты, фичи</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Money-7" data-look="classic" transform="translate(1315.40625, 56.75)"><rect class="basic label-container" style="" x="-96.734375" y="-37.5" width="193.46875" height="75"/><g class="label" style="" transform="translate(-66.734375, -22.5)"><rect/><foreignObject width="133.46875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Считаем сгорание<br />в деньгах</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Review-9" data-look="classic" transform="translate(1582.5234375, 98.625)"><rect class="basic label-container" style="" x="-120.3828125" y="-37.5" width="240.765625" height="75"/><g class="label" style="" transform="translate(-90.3828125, -22.5)"><rect/><foreignObject width="180.765625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Квартальный пересмотр:<br />изменилось ли число</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-6645021c4c1a-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-6645021c4c1a-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-6645021c4c1a-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p>Каждый виток — это квартал. На входе бизнес приносит «что и сколько стоит», на выходе получает «вот сколько надёжности мы дали и какой ценой». Если хоть одно звено разорвано — цикл рассыпается, и SLO откатывается в режим артефакта. Именно эту целостность и проверяют на собесе — к нему и переходим.</p>
<h2 id="кейсы-с-system-design-собеса">Кейсы с System Design-собеса</h2>
<p>Те же тезисы, но в позе «кандидат под прессом». Когда меня самого недавно гоняли по этой теме на интервью, стало особенно ясно: вопросы «про SLO» почти никогда не про SLO. Они про то, умеешь ли ты вести разговор о надёжности так, чтобы бизнес поменял поведение. Разберу пять типовых сценариев — на том же маркетплейсе.</p>
<h3 id="кейс-1-зачем-тебе-slo-999-а-не-9999">Кейс 1. «Зачем тебе SLO 99.9, а не 99.99?»</h3>
<p><strong>Что проверяют:</strong> умеет ли кандидат держать рамку «бюджет → архитектура → деньги».</p>
<p><strong>Провальный ответ:</strong> начать с топологии. «Ну, 99.99 — это active-active в трёх регионах, а 99.9 — один регион с failover между AZ». Технически верно, но это ответ на вопрос «чем отличаются архитектуры», а не «зачем тебе эта надёжность».</p>
<p><strong>Сильный ответ всегда начинается со сценария бизнеса:</strong></p>
<blockquote><p>«Зависит от того, какой это путь. Если это поиск каталога — главный драйвер конверсии — то считаем: на 99.9% бюджет даунтайма на пике ~14 минут в месяц, на 99.99% — полторы минуты. Разница в архитектуре — лишних <code>15–25×</code> против <code>4–6×</code> baseline. Вопрос к бизнесу: эти 12 минут в месяц на пике стоят нам больше или меньше, чем утроение инфраструктуры? Если GMV такой, что минута пикового даунтайма — сотни тысяч, берём 99.99. Если нет — 99.9 с запасом».</p>
</blockquote><p>Ключевое: кандидат не выбирает девятку сам. Он показывает, что выбор девятки — это разговор с бизнесом, и приносит в этот разговор обе цифры — цену надёжности и цену её отсутствия.</p>
<h3 id="кейс-2-у-вас-9995-по-чекауту-но-30-оплат-не-проходит--что-не-так">Кейс 2. «У вас 99.95 по чекауту, но 30% оплат не проходит — что не так?»</h3>
<p><strong>Что проверяют:</strong> понимает ли кандидат разрыв между технической доступностью и продуктовой надёжностью.</p>
<p>Это мой любимый кейс, потому что он ловит самую частую ошибку. Дашборд зелёный — <code>99.95%</code> по эндпоинту оплаты. А бизнес кричит, что треть оплат не доходит. Противоречие? Нет. SLI повешен не туда.</p>
<p><strong>Что произошло:</strong> SLI считает «доля ответов <code>2xx</code> от ручки <code>/pay</code>». Ручка честно отвечает <code>200 OK</code> — она приняла запрос. А дальше платёж уходит в провайдера, провайдер таймаутит, ретрай не настроен, и для пользователя оплата «не прошла» — но для SLI всё прекрасно, ручка-то ответила.</p>
<p><strong>Сильный ответ:</strong></p>
<blockquote><p>«SLI измеряет здоровье ручки, а не сценарий. Его надо переповесить на пользовательский результат: „доля начатых оплат, завершившихся подтверждённым списанием за <code>&lt; N</code> секунд“. Тогда таймауты провайдера, потерянные колбэки и зависшие платежи попадут в знаменатель, и 99.95% превратятся в честные 70%. Дашборд начнёт показывать ту же боль, что чувствует бизнес».</p>
</blockquote><p>Это ровно разведение «продуктовая надёжность против технической доступности». Кандидат, который видит этот разрыв, понимает, что SLI вешается на сценарий, а не на success rate ручки. Кандидат, который не видит, будет до утра чинить инфраструктуру, которая и так зелёная.</p>
<h3 id="кейс-3-бизнес-требует-100-доступности-что-отвечаешь">Кейс 3. «Бизнес требует 100% доступности. Что отвечаешь?»</h3>
<p><strong>Что проверяют:</strong> переговорный навык в чистом виде.</p>
<p><strong>Провальный ответ:</strong> «100% невозможно». Технически правда, но это ответ инженера, который закрывает разговор, а не ведёт его. Бизнес услышит «не хочу» и пойдёт давить.</p>
<p><strong>Сильный ответ — не «нельзя», а цена:</strong></p>
<blockquote><p>«Окей, давай посчитаем, что такое 100%. Каждая девятка после 99.9 удорожает систему примерно в разы. 99.99 — это уже <code>15–25×</code> baseline и заморозка скорости релизов. 99.999 — это multi-cloud, независимые DNS, частично независимые кодовые базы, подходы из telecom. А 100% — это бесконечность: ноль допустимых сбоев означает ноль деплоев, ноль экспериментов, ноль изменений. Мы перестанем выпускать фичи вообще. Точно нужны эти девятки — или нам нужно, чтобы конкретный сценарий, скажем оплата, не падал заметно для пользователя? Это разные задачи, и вторая решается сильно дешевле».</p>
</blockquote><p>Кандидат переводит абсолютное требование в трейд-офф и возвращает бизнесу выбор. Часто за «хотим 100%» стоит «не хотим терять оплаты в пик» — а это не про 100%, это про правильный SLO на правильный путь плюс graceful degradation.</p>
<h3 id="кейс-4-кто-принимает-решение-о-фризе-релизов-при-сгорании-бюджета">Кейс 4. «Кто принимает решение о фризе релизов при сгорании бюджета?»</h3>
<p><strong>Что проверяют:</strong> есть ли у кандидата error budget policy в голове и понимает ли он, что это продуктовый артефакт.</p>
<p><strong>Провальный ответ:</strong> «SRE решает» или «тимлид решает». Это инженерный ответ, и он выдаёт, что у кандидата SLO живёт в инженерной резервации.</p>
<p><strong>Сильный ответ:</strong></p>
<blockquote><p>«Решает не человек в моменте, а записанная заранее error budget policy. В ней зафиксировано: при таком-то burn-rate за такое-то окно — автоматический freeze рискованных деплоев. И подписана эта политика продуктовым владельцем, потому что это он соглашается, что его фичи подождут. Если решение каждый раз принимается заново и под давлением — значит, политики нет, и побеждает тот, кто громче».</p>
</blockquote><p>Здесь проверяют понимание, что фриз — это не власть SRE над продуктом, а заранее согласованное обеими сторонами правило. Кандидат, который это понимает, не будет воевать с продактом — он сошлётся на то, что они вместе подписали.</p>
<h3 id="кейс-5-покажи-как-сожжённый-бюджет-влияет-на-дорожную-карту">Кейс 5. «Покажи, как сожжённый бюджет влияет на дорожную карту»</h3>
<p><strong>Что проверяют:</strong> встроен ли SRE в плановый цикл или живёт в параллельной вселенной с продуктом.</p>
<p><strong>Провальный ответ:</strong> «при сгорании бюджета мы делаем post-mortem». Post-mortem — это хорошо, но это про прошлое. Вопрос — про будущее, про roadmap.</p>
<p><strong>Сильный ответ:</strong></p>
<blockquote><p>«Сгоревший бюджет — это вход в планирование, а не только разбор инцидента. Механически: при систематическом сгорании reliability-задачи поднимаются в приоритете над фичами на следующий цикл — это записано в policy. На квартальном reliability review мы смотрим burn-rate за квартал по каждому пути и решаем, куда идёт инженерное усилие. Если поиск стабильно жжёт бюджет — значит, в следующем квартале часть roadmap уходит на его укрепление, и продакт это принимает, потому что видит связь „сгорание → потерянная выручка“».</p>
</blockquote><p>Это проверка на то, что SRE и продукт планируют из одного документа, а не из двух. Если бюджет ошибок никак не дотягивается до roadmap — SRE и продукт живут в разных вселенных, и SLO снова декорация.</p>
<h3 id="главный-паттерн-всех-пяти-кейсов">Главный паттерн всех пяти кейсов</h3>
<p>Сведём пять кейсов в одну таблицу — что на самом деле проверяет каждый:</p>
<table>
	<thead>
			<tr>
					<th>Вопрос на собесе</th>
					<th>Что реально проверяют</th>
					<th>Маркер слабого ответа</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Зачем 99.9, а не 99.99?</td>
					<td>Рамка «бюджет → архитектура → деньги»</td>
					<td>Начал с топологии, а не со сценария</td>
			</tr>
			<tr>
					<td>99.95, но 30% оплат не проходит?</td>
					<td>Разрыв «доступность vs надёжность»</td>
					<td>Полез чинить зелёную инфраструктуру</td>
			</tr>
			<tr>
					<td>Бизнес хочет 100%?</td>
					<td>Переговорный навык</td>
					<td>Сказал «невозможно» и закрыл тему</td>
			</tr>
			<tr>
					<td>Кто решает фриз?</td>
					<td>Error budget policy как продуктовый артефакт</td>
					<td>«SRE решает» / «тимлид решает»</td>
			</tr>
			<tr>
					<td>Бюджет → roadmap?</td>
					<td>Встроен ли SRE в плановый цикл</td>
					<td>«Делаем post-mortem» (про прошлое)</td>
			</tr>
	</tbody>
</table>
<p>На собесе не проверяют знание формул сгорания бюджета — это гуглится за минуту. Проверяют, <strong>можешь ли ты вести разговор о надёжности так, чтобы бизнес поменял своё поведение</strong>. Все пять сильных ответов имеют одну форму: начать со сценария и денег, перевести требование в трейд-офф, вернуть выбор бизнесу, сослаться на заранее согласованное правило. Это и есть навык, который «чертёж» описывал как «SLO — интерфейс между System Design и бизнесом», только теперь — со стороны человека, который этим интерфейсом пользуется вживую.</p>
<h2 id="метрики-sre-команды-для-мультипродукта">Метрики SRE-команды для мультипродукта</h2>
<p>Третий тезис — и самый практически болезненный. Пока продукт один, метрики команды SRE совпадают с метриками продукта: SLO продукта, его бюджет, его инциденты. Команда отчитывается надёжностью своего единственного сервиса, и всё сходится.</p>
<p>Когда продуктов становится 5–10, эта простая картина ломается. Команда обслуживает зоопарк сервисов с разными CUJ, разной критичностью, разной зрелостью. И тут возникает вопрос, на который у большинства команд нет хорошего ответа: <strong>как одной цифрой отчитаться перед менеджментом за надёжность, которую ты дал восьми разным продуктам?</strong> Соблазн — взять среднее. На нём всё и ломается.</p>
<h3 id="что-не-работает-как-метрика-команды">Что не работает как метрика команды</h3>
<p>Три популярных агрегата, каждый из которых врёт по-своему:</p>
<ul>
<li><strong>MTTR в среднем по всем инцидентам.</strong> Растворяет редкие тяжёлые случаи в массе мелких. Команда, которая быстро закрывает сто пустяковых алертов и месяц не может починить один критичный, покажет прекрасный средний MTTR. Хуже того, метрика поощряет «закрыть быстро, не разбираясь» — лишь бы цифра была хорошей.</li>
<li><strong>Composite SLO по всем продуктам.</strong> «Средняя надёжность по компании 99.94%» — это средняя температура по больнице. Она теряет CUJ: упавший в ноль критичный путь одного продукта тонет в здоровье семи остальных. Наивная агрегация SLO в мультипродукте математически врёт — этот эффект хорошо разобран в посте Алекса Идальго <a href="https://blog.relyabilit.ie/implicit-slos-and-their-dangers/" rel="noopener noreferrer" target="_blank">«Implicit SLOs and their dangers»</a>: композит почти всегда оптимистичнее, чем худший из путей, а пользователю больно именно от худшего.</li>
<li><strong>Количество инцидентов в абсолюте.</strong> Стимулирует молчать о мелких. Если команду меряют числом инцидентов, она перестаёт их заводить — и ты теряешь сигнал. Это классический закон Гудхарта: метрика, ставшая целью, перестаёт быть метрикой.</li>
</ul>
<p>Общая болезнь всех трёх — они сжимают разнородное в одно число, и в этом сжатии теряется ровно то, ради чего метрику заводили.</p>
<h3 id="что-осмысленно-мерить-три-слоя">Что осмысленно мерить: три слоя</h3>
<p>Вместо одного агрегата — раскладка по трём слоям, параллельная трём ролям из «чертежа»: бизнес / инженерия / управление. Каждый слой отвечает на свой вопрос и не пытается заменить остальные.</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/c763b06a1ef116a025ea10f9531dc8cd730d876c7a881e56427db9460d2d2e9e.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-c763b06a1ef1-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 896px; background-color: transparent;" viewBox="0 0 896 136" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-c763b06a1ef1-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-c763b06a1ef1-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-light .error-icon{fill:#dce0e8;}#m-c763b06a1ef1-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-c763b06a1ef1-light .edge-thickness-normal{stroke-width:1px;}#m-c763b06a1ef1-light .edge-thickness-thick{stroke-width:3.5px;}#m-c763b06a1ef1-light .edge-pattern-solid{stroke-dasharray:0;}#m-c763b06a1ef1-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-c763b06a1ef1-light .edge-pattern-dashed{stroke-dasharray:3;}#m-c763b06a1ef1-light .edge-pattern-dotted{stroke-dasharray:2;}#m-c763b06a1ef1-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-c763b06a1ef1-light .marker.cross{stroke:#5c5f77;}#m-c763b06a1ef1-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-c763b06a1ef1-light p{margin:0;}#m-c763b06a1ef1-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-c763b06a1ef1-light .cluster-label text{fill:#4c4f69;}#m-c763b06a1ef1-light .cluster-label span{color:#4c4f69;}#m-c763b06a1ef1-light .cluster-label span p{background-color:transparent;}#m-c763b06a1ef1-light .label text,#m-c763b06a1ef1-light span{fill:#4c4f69;color:#4c4f69;}#m-c763b06a1ef1-light .node rect,#m-c763b06a1ef1-light .node circle,#m-c763b06a1ef1-light .node ellipse,#m-c763b06a1ef1-light .node polygon,#m-c763b06a1ef1-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-c763b06a1ef1-light .rough-node .label text,#m-c763b06a1ef1-light .node .label text,#m-c763b06a1ef1-light .image-shape .label,#m-c763b06a1ef1-light .icon-shape .label{text-anchor:middle;}#m-c763b06a1ef1-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-c763b06a1ef1-light .rough-node .label,#m-c763b06a1ef1-light .node .label,#m-c763b06a1ef1-light .image-shape .label,#m-c763b06a1ef1-light .icon-shape .label{text-align:center;}#m-c763b06a1ef1-light .node.clickable{cursor:pointer;}#m-c763b06a1ef1-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-c763b06a1ef1-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-c763b06a1ef1-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-c763b06a1ef1-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-c763b06a1ef1-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-c763b06a1ef1-light .edgeLabel p{background-color:#eff1f5;}#m-c763b06a1ef1-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-c763b06a1ef1-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-c763b06a1ef1-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-c763b06a1ef1-light .cluster text{fill:#4c4f69;}#m-c763b06a1ef1-light .cluster span{color:#4c4f69;}#m-c763b06a1ef1-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-c763b06a1ef1-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-c763b06a1ef1-light rect.text{fill:none;stroke-width:0;}#m-c763b06a1ef1-light .icon-shape,#m-c763b06a1ef1-light .image-shape{background-color:#eff1f5;text-align:center;}#m-c763b06a1ef1-light .icon-shape p,#m-c763b06a1ef1-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-c763b06a1ef1-light .icon-shape .label rect,#m-c763b06a1ef1-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-c763b06a1ef1-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-c763b06a1ef1-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-c763b06a1ef1-light .node .neo-node{stroke:#bcc0cc;}#m-c763b06a1ef1-light [data-look="neo"].node rect,#m-c763b06a1ef1-light [data-look="neo"].cluster rect,#m-c763b06a1ef1-light [data-look="neo"].node polygon{stroke:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].node path{stroke:url(#m-c763b06a1ef1-light-gradient);stroke-width:1px;}#m-c763b06a1ef1-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-c763b06a1ef1-light [data-look="neo"].node circle{stroke:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-c763b06a1ef1-light [data-look="neo"].icon-shape .icon{fill:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-c763b06a1ef1-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M268,68L272.167,68C276.333,68,284.667,68,292.333,68C300,68,307,68,310.5,68L314,68" id="m-c763b06a1ef1-light-L_L3_L2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L3_L2_0" data-points="W3sieCI6MjY4LCJ5Ijo2OH0seyJ4IjoyOTMsInkiOjY4fSx7IngiOjMxOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-light_flowchart-v2-pointEnd)"/><path d="M578,68L582.167,68C586.333,68,594.667,68,602.333,68C610,68,617,68,620.5,68L624,68" id="m-c763b06a1ef1-light-L_L2_L1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L2_L1_0" data-points="W3sieCI6NTc4LCJ5Ijo2OH0seyJ4Ijo2MDMsInkiOjY4fSx7IngiOjYyOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_L3_L2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_L2_L1_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-c763b06a1ef1-light-flowchart-L1-0" data-look="classic" transform="translate(758, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 1. Надёжность продуктов суммарно<br />вопрос: какую надёжность мы дали бизнесу</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-light-flowchart-L2-1" data-look="classic" transform="translate(448, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 2. Инженерное усилие команды<br />вопрос: какой ценой и насколько устойчиво</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-light-flowchart-L3-2" data-look="classic" transform="translate(138, 68)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 3. Качество диалога<br />вопрос: встроена ли надёжность в продукт</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-c763b06a1ef1-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-c763b06a1ef1-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-c763b06a1ef1-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-c763b06a1ef1-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 896px; background-color: transparent;" viewBox="0 0 896 136" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-c763b06a1ef1-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-c763b06a1ef1-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-dark .error-icon{fill:#181926;}#m-c763b06a1ef1-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-c763b06a1ef1-dark .edge-thickness-normal{stroke-width:1px;}#m-c763b06a1ef1-dark .edge-thickness-thick{stroke-width:3.5px;}#m-c763b06a1ef1-dark .edge-pattern-solid{stroke-dasharray:0;}#m-c763b06a1ef1-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-c763b06a1ef1-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-c763b06a1ef1-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-c763b06a1ef1-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-c763b06a1ef1-dark .marker.cross{stroke:#a5adcb;}#m-c763b06a1ef1-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-c763b06a1ef1-dark p{margin:0;}#m-c763b06a1ef1-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-c763b06a1ef1-dark .cluster-label text{fill:#cad3f5;}#m-c763b06a1ef1-dark .cluster-label span{color:#cad3f5;}#m-c763b06a1ef1-dark .cluster-label span p{background-color:transparent;}#m-c763b06a1ef1-dark .label text,#m-c763b06a1ef1-dark span{fill:#cad3f5;color:#cad3f5;}#m-c763b06a1ef1-dark .node rect,#m-c763b06a1ef1-dark .node circle,#m-c763b06a1ef1-dark .node ellipse,#m-c763b06a1ef1-dark .node polygon,#m-c763b06a1ef1-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-c763b06a1ef1-dark .rough-node .label text,#m-c763b06a1ef1-dark .node .label text,#m-c763b06a1ef1-dark .image-shape .label,#m-c763b06a1ef1-dark .icon-shape .label{text-anchor:middle;}#m-c763b06a1ef1-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-c763b06a1ef1-dark .rough-node .label,#m-c763b06a1ef1-dark .node .label,#m-c763b06a1ef1-dark .image-shape .label,#m-c763b06a1ef1-dark .icon-shape .label{text-align:center;}#m-c763b06a1ef1-dark .node.clickable{cursor:pointer;}#m-c763b06a1ef1-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-c763b06a1ef1-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-c763b06a1ef1-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-c763b06a1ef1-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-c763b06a1ef1-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-c763b06a1ef1-dark .edgeLabel p{background-color:#24273a;}#m-c763b06a1ef1-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-c763b06a1ef1-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-c763b06a1ef1-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-c763b06a1ef1-dark .cluster text{fill:#cad3f5;}#m-c763b06a1ef1-dark .cluster span{color:#cad3f5;}#m-c763b06a1ef1-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-c763b06a1ef1-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-c763b06a1ef1-dark rect.text{fill:none;stroke-width:0;}#m-c763b06a1ef1-dark .icon-shape,#m-c763b06a1ef1-dark .image-shape{background-color:#24273a;text-align:center;}#m-c763b06a1ef1-dark .icon-shape p,#m-c763b06a1ef1-dark .image-shape p{background-color:#24273a;padding:2px;}#m-c763b06a1ef1-dark .icon-shape .label rect,#m-c763b06a1ef1-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-c763b06a1ef1-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-c763b06a1ef1-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-c763b06a1ef1-dark .node .neo-node{stroke:#494d64;}#m-c763b06a1ef1-dark [data-look="neo"].node rect,#m-c763b06a1ef1-dark [data-look="neo"].cluster rect,#m-c763b06a1ef1-dark [data-look="neo"].node polygon{stroke:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].node path{stroke:url(#m-c763b06a1ef1-dark-gradient);stroke-width:1px;}#m-c763b06a1ef1-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-c763b06a1ef1-dark [data-look="neo"].node circle{stroke:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-c763b06a1ef1-dark [data-look="neo"].icon-shape .icon{fill:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M268,68L272.167,68C276.333,68,284.667,68,292.333,68C300,68,307,68,310.5,68L314,68" id="m-c763b06a1ef1-dark-L_L3_L2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L3_L2_0" data-points="W3sieCI6MjY4LCJ5Ijo2OH0seyJ4IjoyOTMsInkiOjY4fSx7IngiOjMxOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-dark_flowchart-v2-pointEnd)"/><path d="M578,68L582.167,68C586.333,68,594.667,68,602.333,68C610,68,617,68,620.5,68L624,68" id="m-c763b06a1ef1-dark-L_L2_L1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L2_L1_0" data-points="W3sieCI6NTc4LCJ5Ijo2OH0seyJ4Ijo2MDMsInkiOjY4fSx7IngiOjYyOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_L3_L2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_L2_L1_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-c763b06a1ef1-dark-flowchart-L1-0" data-look="classic" transform="translate(758, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 1. Надёжность продуктов суммарно<br />вопрос: какую надёжность мы дали бизнесу</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-dark-flowchart-L2-1" data-look="classic" transform="translate(448, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 2. Инженерное усилие команды<br />вопрос: какой ценой и насколько устойчиво</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-dark-flowchart-L3-2" data-look="classic" transform="translate(138, 68)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 3. Качество диалога<br />вопрос: встроена ли надёжность в продукт</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-c763b06a1ef1-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-c763b06a1ef1-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-c763b06a1ef1-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><h4 id="слой-1-надёжность-продуктов-суммарно-бизнес-слой">Слой 1. Надёжность продуктов суммарно (бизнес-слой)</h4>
<p>Не «средний SLO», а покрытие и распределение:</p>
<ul>
<li><strong>Доля продуктов, у которых SLO существует, написан в терминах CUJ и формально подписан продактом.</strong> Это метрика зрелости, а не надёжности. «6 из 8 продуктов имеют подписанный SLO» говорит больше, чем любой композит.</li>
<li><strong>Доля продуктов с действующей error budget policy.</strong> Не «есть SLO», а «есть записанное правило, что делать при сгорании». Обычно эта доля сильно меньше первой — и разрыв между ними и есть фронт работ.</li>
<li><strong>Burn-rate бюджетов по всем продуктам как тепловая карта, а не среднее.</strong> Восемь строк, в каждой — сгорание за квартал. Красные клетки видно сразу, и ни одна не тонет в зелёных соседях.</li>
<li><strong>Сумма money loss за квартал</strong> — если связка <code>SLI → ₽</code> настроена. Это единственная цифра из этого слоя, которую менеджмент понимает без перевода.</li>
</ul>
<h4 id="слой-2-инженерное-усилие-команды-продуктивность">Слой 2. Инженерное усилие команды (продуктивность)</h4>
<p>Сколько стоит надёжность в человекочасах и насколько это устойчиво:</p>
<ul>
<li><strong>Toil как % времени команды.</strong> Гугловский потолок — 50% (правило из главы <a href="https://sre.google/sre-book/eliminating-toil/" rel="noopener noreferrer" target="_blank">«Eliminating Toil»</a> в Google SRE Book); всё, что выше, означает, что команда тонет в ручной рутине и не строит автоматизацию. Toil — отличная инженерная метрика зрелости именно потому, что растёт незаметно и душит команду медленно.</li>
<li><strong>Доля задач, переходящих от SRE обратно в продуктовую команду.</strong> Антипаттерн — «SRE как owner навсегда»: команда взяла продукт на поддержку и больше его не отпускает. Здоровая динамика — знания и ответственность постепенно возвращаются в продуктовую команду, а SRE остаётся на инструментарии и сложных случаях.</li>
<li><strong>Время от инцидента до закрытого action item</strong> — не до восстановления. Восстановили за 20 минут, а исправили причину через полгода — это плохо, и средний MTTR это скрывает, а эта метрика показывает.</li>
<li><strong>Доля постмортемов с реализованными action items.</strong> Постмортем, после которого ничего не сделали, — это терапия, а не инженерия. Эта доля показывает, учится ли команда на самом деле.</li>
</ul>
<h4 id="слой-3-качество-диалога-управленческий-слой">Слой 3. Качество диалога (управленческий слой)</h4>
<p>Самый неочевидный и самый важный слой — он меряет ровно то, о чём вся статья: встроена ли надёжность в продукт или живёт в инженерной резервации.</p>
<ul>
<li><strong>Регулярность product reliability review.</strong> Проводится ли квартальный разбор бюджета с каждым продактом — или только с теми, кто сам пришёл. Цель — со всеми и по расписанию.</li>
<li><strong>Доля продуктов, где бизнес сам приходит за обсуждением бюджета.</strong> Это лакмус. Если продакт сам спрашивает «сколько у нас бюджета осталось» — диалог здоров. Если SRE его догоняет — нет. Эта доля растёт медленно и означает культурный сдвиг, а не процессный.</li>
<li><strong>NPS/CSAT-аналог от команд разработки на сервис SRE.</strong> Команда SRE — это тоже сервис, у него есть внутренние пользователи (продуктовые команды), и их удовлетворённость измерима. Низкий внутренний CSAT при хороших технических метриках — сигнал, что SRE воспринимают как тормоз, а не как партнёра.</li>
</ul>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Слой 3 и культурная готовность</p>
    <div class="callout__content"><p>Третий слой пересекается с фазой 0 «cultural preparation» в Bear SRE Framework: SRE-практики невозможны без готовности бизнеса разговаривать. Метрики этого слоя, по сути, измеряют, насколько эта готовность реальна, а не декларирована. Можно вести их и как самостоятельную раскладку — но если у тебя уже есть рамка зрелости, слой 3 хорошо в неё ложится.</p></div>
  </div>
</aside><p>Соберём три слоя в одну таблицу — её удобно держать перед глазами при отчёте:</p>
<table>
	<thead>
			<tr>
					<th>Слой</th>
					<th>Вопрос</th>
					<th>Пример метрики</th>
					<th>Какой агрегат заменяет</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1. Надёжность продуктов</td>
					<td>Какую надёжность мы дали бизнесу</td>
					<td>Доля продуктов с подписанным CUJ-SLO; money loss за квартал; burn-rate тепловой картой</td>
					<td>Composite SLO</td>
			</tr>
			<tr>
					<td>2. Инженерное усилие</td>
					<td>Какой ценой и насколько устойчиво</td>
					<td>Toil %; время до закрытого action item; доля постмортемов с реализованными AI</td>
					<td>Средний MTTR</td>
			</tr>
			<tr>
					<td>3. Качество диалога</td>
					<td>Встроена ли надёжность в продукт</td>
					<td>Регулярность reliability review; доля «бизнес пришёл сам»; внутренний CSAT на SRE</td>
					<td>Число инцидентов</td>
			</tr>
	</tbody>
</table>
<p>Ни одна метрика из правой колонки («какой агрегат заменяет») не исчезает полностью — MTTR и счётчик инцидентов остаются полезными для внутренней диагностики. Но <strong>как метрика команды перед менеджментом</strong> они проигрывают трёхслойной раскладке, потому что усредняют то, что усреднять нельзя.</p>
<h3 id="как-это-звучит-в-отчёте-менеджменту">Как это звучит в отчёте менеджменту</h3>
<p>Финальная проверка всей раскладки — как команда отчитывается наверх. Сравни два варианта.</p>
<aside class="callout callout--warn" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
  <div class="callout__body"><p class="callout__title">Не делай SLO продуктов прямым KPI команды</p>
    <div class="callout__content"><p>Заманчиво объявить «продуктовую надёжность KPI команды SRE» — но это прямой путь к токсичным SLO. По <a href="https://t.me/jtprogru_channel/4460" rel="noopener noreferrer" target="_blank">закону Гудхарта</a>, как только SLO становится оценкой команды, она начинает оптимизировать цифру, а не надёжность: заниженные таргеты, «неудобные» события вне знаменателя, споры «считать ли инцидент инцидентом». Поэтому меряй команду не <strong>абсолютом</strong> SLO (он про продукт и принадлежит продакту), а <strong>дельтой и охватом</strong> — на сколько продуктов распространили подписанный SLO и policy, сократилось ли сгорание бюджета и money loss, вырос ли охват зрелым диалогом (слой 3). Занижением таргета это не саботируешь — потому и отчёт ниже построен на «было 4 → стало 6», а не на «composite SLO 99.94%».</p></div>
  </div>
</aside><p><strong>Плохо (агрегат):</strong> «У нас средний MTTR 22 минуты и composite SLO 99.94%». Менеджмент кивает, ничего не понимает, ни одно решение не меняется.</p>
<p><strong>Хорошо (три слоя):</strong> «За квартал укрепили подписанный SLO с error budget policy у 6 из 8 продуктов (было 4), суммарное сгорание бюджета упало на 35%, бизнес-потери от инцидентов сократились на N рублей. Toil держим на 40%. Два продукта вернули в зону ответственности продуктовых команд. Осталось два продукта без политики — это фронт на следующий квартал».</p>
<p>Второй отчёт показывает движение, цену и план. Это и есть язык, на котором SRE-команда разговаривает с менеджментом, когда продуктов больше одного, — не «средняя температура», а «вот что мы дали бизнесу и как конвертировали инженерное усилие в продуктовый результат».</p>
<h2 id="что-сделать-завтра-утром">Что сделать завтра утром</h2>
<p>Чек-лист «куда вернуться», когда дочитал. Пять вопросов — если на какой-то нет ответа, там и фронт работ.</p>
<ol>
<li><strong>Какой product reliability review встроен в наш квартальный план?</strong> Если ответ «никакой» или «по запросу» — начни с того, чтобы поставить в календарь регулярную встречу по бюджету с каждым продуктовым владельцем.</li>
<li><strong>Кто из бизнеса формально подписал error budget policy — и подписал ли вообще?</strong> Если не можешь назвать имя — у тебя SLO-артефакт, а не контракт. Это задача номер один.</li>
<li><strong>На какой панели я вижу „health по сценарию“, а не „health по сервису“?</strong> Если только по сервису — твои SLI повешены на ручки, а не на пути, и ты не видишь того, что чувствует пользователь.</li>
<li><strong>Сколько раз за последний квартал бизнес сам поднял разговор о трейд-оффе „фича против надёжности“?</strong> Если ноль — диалог пока односторонний, и слой 3 у тебя в красной зоне.</li>
<li><strong>Какой из метрик трёх слоёв у нас нет — и почему?</strong> Чаще всего отсутствует слой 3 (качество диалога), потому что его сложнее всего мерить. Но именно он отличает зрелую практику от набора дашбордов.</li>
</ol>
<h2 id="заключение">Заключение</h2>
<p>В «чертеже» я доказывал, что архитектура без SLO — это эстетика, а не инженерия. Эта статья добавляет вторую половину: <strong>SLO без диалога — это дашборд, а не управление.</strong></p>
<p>Чертёж надёжности сам по себе мёртв. Его надо защищать, торговать им и переписывать вместе с бизнесом каждый квартал. Технический инструмент — SLI, SLO, error budget — становится управленческим ровно в тот момент, когда у инженеров и бизнеса появляется одна картинка целевой надёжности и одна валюта компромиссов. До этого момента у тебя есть только красивые графики и предчувствия.</p>
<p>Три вещи, ради которых стоило городить всю статью:</p>
<ul>
<li><strong>Диалог — это формат принятия решений</strong>, а не презентация. Его сердце — записанная error budget policy, подписанная продуктовым владельцем.</li>
<li><strong>На собесе по SLO проверяют переговорный навык</strong>, а не арифметику. Сильный ответ начинается со сценария и денег и возвращает бизнесу выбор.</li>
<li><strong>Метрики команды при мультипродукте — это три слоя, а не один агрегат.</strong> Среднее по больнице прячет именно ту боль, ради которой метрику заводили.</li>
</ul>
<p>Надёжность не «достигается». Она каждый квартал заново строится в диалоге — и проигрывается ровно там, где этот диалог замолчал.</p>
<p>Формальную рамку вокруг того же самого — как выглядит регулярный ритуал ревью бюджета, кто в нём сидит и на каком языке разговаривает с не-инженерами — я разложил в карте компетенций: <a href="https://jtprogru.github.io/The-Way-of-SRE/culture/slo-budget-review/" rel="noopener noreferrer" target="_blank">SLO / Budget Review</a> и <a href="https://jtprogru.github.io/The-Way-of-SRE/culture/stakeholder-management/" rel="noopener noreferrer" target="_blank">Stakeholder Management</a>.</p>
<p>Спасибо, что дочитал до конца. Это продолжение цикла, начатого статьёй <a href="/posts/slo-as-architecture-blueprint/">«SLO как чертёж архитектуры»</a>. Если у тебя есть свой опыт диалога с бизнесом о надёжности — особенно истории, как error budget policy реально остановила релиз или как менялись метрики команды с ростом числа продуктов, — буду рад услышать.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! В прошлой статье — <a href="/posts/slo-as-architecture-blueprint/">«SLO как чертёж архитектуры»</a> — я закончил тезисом, который тут же хочется развить: SLO выбирается не инженерами в одиночку, а вместе с бизнесом, потому что число в нём привязано к деньгам. А «вместе с бизнесом» в реальности чаще всего выглядит так: SRE придумал SLO, нарисовал дашборд, показал на демо, продакт покивал — и все разошлись. Чертёж готов. Надёжность&hellip; не выросла.</p>
<p>Так бывает, потому что чертёж сам по себе не работает. SLO/SLI/error budget — это <strong>не отчётный артефакт, а язык переговоров</strong>. Технический инструмент превращается в управленческий ровно в тот момент, когда у инженеров и у бизнеса одна и та же картинка целевой надёжности и одна и та же валюта компромиссов. Пока этого нет — у тебя есть красивый график, который никто не использует для принятия решений.</p>
<p>В этой статье я разворачиваю три практических следствия из этого тезиса, и все три — продолжение того же кейса с маркетплейсом (поиск каталога, 50M товаров, 30k RPS на пике), который я гонял в «чертеже»:</p>
<ol>
<li><strong>Диалог с бизнесом — это не презентация SLO, а формат принятия решений.</strong> SRE не «продаёт» таргет продакту, а вместе с ним выбирает уровень надёжности, политику бюджета ошибок и порядок разрешения спора «фичи против надёжности».</li>
<li><strong>System Design-собес проверяет именно этот навык.</strong> Большинство «вопросов по SLO» — не про арифметику девяток, а про умение собрать разговор с бизнесом, удержать рамку трейд-оффов и перевести «бизнес хочет 100%» в инженерную работу.</li>
<li><strong>Метрики SRE-команды меняются, когда продуктов больше одного.</strong> Ни «MTTR по компании», ни «SLO по сервису» как метрика команды не работают, когда команда обслуживает 5–10 продуктов. Нужен сводный язык.</li>
</ol>
<p>Если первая статья была про то, <strong>какой</strong> чертёж рисовать, то эта — про то, <strong>как сделать так, чтобы по чертежу действительно построили</strong>, а не повесили его в рамке на стену.</p>
<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<p>Статья длинная (~40 000 знаков). Сразу — что заберёт каждая роль:</p>
<ul>
<li><strong>SRE / SRE-lead.</strong> Как перестать «отдавать SLO» и начать вести по нему диалог: три признака, что разговор не состоялся; error budget policy как продуктовый, а не инженерный артефакт; и — отдельным блоком — какие метрики брать на команду, когда продуктов много, чтобы они не врали.</li>
<li><strong>Продакт / аналитик.</strong> Что именно бизнес должен уметь произнести сам (а не получать от SRE как откровение): где у нас критичные пользовательские пути, сколько стоит девятка в деньгах, кто и когда жмёт на стоп.</li>
<li><strong>Кандидат / интервьюер на System Design.</strong> Пять типовых собес-кейсов «про SLO», которые на самом деле проверяют переговорный навык, и разбор, что в них считается сильным ответом, а что — провалом.</li>
</ul>
<p>Общий блок для всех: почему <strong>надёжность нельзя «сделать», её можно только постоянно переторговывать</strong> — и как выглядит этот процесс в зрелой команде.</p>
<h2 id="tldr-если-читать-дальше-уже-некогда">TL;DR (если читать дальше уже некогда)</h2>
<p>Четыре тезиса, к которым приходит статья:</p>
<ol>
<li><strong>SLO без error budget policy — это украшение дашборда.</strong> Контракт из SLO делает записанное правило: что происходит, когда бюджет сгорел, и кто принимает по этому поводу решение. Само число ничего не гарантирует. Без правила — это график, мимо которого все ходят.</li>
<li><strong>Надёжность — это не состояние, а переговорный процесс.</strong> Её не «достигают» один раз. Каждый квартал бизнес и SRE заново выбирают уровень, потому что меняются нагрузка, деньги и приоритеты. SLO, который не пересматривают, врёт уже через полгода.</li>
<li><strong>На собесе по SLO проверяют не формулы, а умение поменять поведение бизнеса.</strong> Сильный ответ всегда начинается со сценария и денег, а не с топологии и девяток. «100% доступности» — не повод сказать «нельзя», а повод показать цену.</li>
<li><strong>Метрики SRE-команды при мультипродукте раскладываются на три слоя</strong> (надёжность продуктов / инженерное усилие / качество диалога) — один агрегат тут не справляется. Среднее по больнице — MTTR, composite SLO, число инцидентов — прячет ровно то, ради чего метрику заводили.</li>
</ol>
<p>Дальше — как до этого дойти. Если интересует только управленческая кульминация — переходи сразу к разделу <a href="#%d0%bc%d0%b5%d1%82%d1%80%d0%b8%d0%ba%d0%b8-sre-%d0%ba%d0%be%d0%bc%d0%b0%d0%bd%d0%b4%d1%8b-%d0%b4%d0%bb%d1%8f-%d0%bc%d1%83%d0%bb%d1%8c%d1%82%d0%b8%d0%bf%d1%80%d0%be%d0%b4%d1%83%d0%ba%d1%82%d0%b0">Метрики SRE-команды для мультипродукта</a>.</p>
<h2 id="чертёж-без-переговоров--это-просто-дашборд">Чертёж без переговоров — это просто дашборд</h2>
<p>Начнём с диагноза. В прошлой статье SLO получился как инженерный объект: числитель, знаменатель, окно, бюджет ошибок. Всё аккуратно. Но у этого объекта есть два режима существования, и разница между ними — это и есть вся статья.</p>
<h3 id="slo-как-артефакт-против-slo-как-контракта">SLO как артефакт против SLO как контракта</h3>
<p><strong>SLO-артефакт</strong> — это число на дашборде. Его придумали, согласовали галочкой, поставили мониторинг, иногда даже сделали красивый burn-rate-график. Он существует. Он измеряется. И он ни на что не влияет, потому что ни одно решение в компании не принимается, глядя на него.</p>
<p><strong>SLO-контракт</strong> — это то же число, но вокруг него выстроен процесс. Когда бюджет ошибок тает быстрее плана — кто-то останавливает рискованные деплои. Когда бюджет сгорел — спринт переключается на надёжность, и это не подвиг отдельного SRE, а записанное правило. Когда продакт хочет выкатить фичу к Чёрной пятнице — он сам спрашивает «сколько у нас осталось бюджета», потому что знает, что это его валюта тоже.</p>
<p>Разница видна по тому, как объект ведёт себя в жизни команды:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>SLO-артефакт</th>
					<th>SLO-контракт</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Где живёт</td>
					<td>На дашборде</td>
					<td>В процессе принятия решений</td>
			</tr>
			<tr>
					<td>Кто смотрит</td>
					<td>Дежурный SRE</td>
					<td>Продакт и SRE вместе</td>
			</tr>
			<tr>
					<td>Что происходит при сгорании</td>
					<td>Ничего («ну красный»)</td>
					<td>Срабатывает записанное правило</td>
			</tr>
			<tr>
					<td>Кто «владелец» числа</td>
					<td>Инженеры</td>
					<td>Продуктовый владелец + SRE</td>
			</tr>
			<tr>
					<td>Роль в споре «фича vs надёжность»</td>
					<td>Никакой</td>
					<td>Валюта, в которой спор решается</td>
			</tr>
			<tr>
					<td>Пересмотр</td>
					<td>Когда вспомнят</td>
					<td>Раз в квартал, ритуалом</td>
			</tr>
	</tbody>
</table>
<p>Разница между ними — не в качестве дашборда. Она в том, <strong>встроен ли SLO в принятие решений</strong>. И встроить его туда нельзя в одностороннем порядке: SRE не может «назначить» бизнесу, что теперь решения принимаются по бюджету ошибок. Это можно только договорить.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Главная подмена</p>
    <div class="callout__content"><p>«Мы внедрили SLO» почти всегда означает «мы настроили SLI и нарисовали дашборд». Это внедрение <strong>артефакта</strong>. Внедрение <strong>контракта</strong> — это когда на вопрос «что у нас происходит при сгорании бюджета» отвечает не SRE, а продакт, и отвечает одинаково с SRE. Пока ответы расходятся — контракта нет, есть график.</p></div>
  </div>
</aside><h3 id="три-признака-что-разговор-не-состоялся">Три признака, что разговор не состоялся</h3>
<p>Проверить, в каком режиме у тебя SLO, можно по трём симптомам. Если узнаёшь хотя бы один — диалога не было, был монолог SRE.</p>
<ol>
<li><strong>SLO «спущен сверху».</strong> Инженеры посчитали девятки, выбрали таргет «по аналогии с большими» и поставили продакта перед фактом. Продакт не участвовал в выборе числа — значит, он не считает его своим и не будет на него опираться в спорах. Для него это «инженерная штука», а не общий контракт.</li>
<li><strong>Нет error budget policy.</strong> SLO есть, бюджет считается, дашборд горит то зелёным, то красным. Но что конкретно делать, когда бюджет на исходе — нигде не записано. В итоге красный дашборд не меняет ничего: «ну красный, ну ладно, катим дальше».</li>
<li><strong>На пересечении бюджета никто не останавливает фичи.</strong> Самый жёсткий признак. Бюджет сгорел в ноль, а релизы продолжают выкатываться, потому что «дедлайн важнее». Это значит, что SLO проигрывает любому продуктовому приоритету — то есть фактически его нет.</li>
</ol>
<p>Все три признака — про одно: <strong>SLO не имеет оперативных последствий</strong>. А инструмент без последствий — это декорация.</p>
<h3 id="кейс-mercari-переименовать-мало-нужен-ритуал">Кейс Mercari: переименовать мало, нужен ритуал</h3>
<p>Хороший пример того, как легко перепутать артефакт с контрактом, — переход к User Journey SLO. Команды (в том числе Mercari, описавшие это в посте <a href="https://engineering.mercari.com/en/blog/entry/20241204-keeping-user-journey-slos-up-to-date-with-e2e-testing-in-a-microservices-architecture/" rel="noopener noreferrer" target="_blank">«Keeping User Journey SLOs Up-to-Date with E2E Testing»</a> в декабре 2024 года) приходят к тому, что SLO «на сервис» прячет пользовательскую боль, и переезжают на SLO, привязанные к пользовательскому пути. Идея правильная — она ровно из «чертежа»: SLI вешаются на шаги критичного пути, а не на сервис целиком.</p>
<p>Но вот ловушка: само по себе переименование «SLO сервиса X» → «SLO пользовательского пути Y» ничего не меняет, если за ним не стоит <strong>сопровождающий ритуал с продактом</strong>. Если User Journey SLO живёт там же, где жил старый — на дашборде, который смотрит только дежурный SRE, — это просто более точный артефакт. Та же декорация, только лучше нарисованная.</p>
<p>Работает переход тогда, когда вместе с новым SLO появляется регулярная встреча, на которой продакт смотрит на бюджет своего пути и принимает по нему решения. Не SRE показывает — продакт смотрит и решает. Переименование — это 10% работы. Остальные 90% — встроить новый объект в процесс продукта.</p>
<h3 id="почему-без-продуктового-владельца-это-не-лечится">Почему без продуктового владельца это не лечится</h3>
<p>Здесь стоит развести два понятия — <strong>продуктовую надёжность</strong> (видит ли пользователь, что сценарий работает) и <strong>техническую доступность</strong> (отвечает ли ручка <code>/search</code> двухсоткой). Важное следствие из этого разведения: продуктовая надёжность по определению не может принадлежать только SRE.</p>
<p>SRE владеет технической доступностью — это его зона: инфраструктура, репликация, failover, capacity. Но «работает ли поиск для пользователя так, что он покупает» — это вопрос, на который SRE физически не может ответить в одиночку, потому что «так, что он покупает» — это продуктовое определение, а не техническое. Кто решает, что пустая выдача по валидному запросу — это провал, а не «технически 200 OK»? Продакт. Кто решает, что цена на карточке старше минуты — это инцидент? Продакт.</p>
<p>Поэтому <strong>у продуктовой надёжности должен быть продуктовый владелец</strong>. Не «SRE отвечает за надёжность» — это и есть корень всех проблем из предыдущего раздела. А «SRE отвечает за инструментарий и техническую доступность, продакт — за то, что считается надёжностью с точки зрения пользователя, и они держат общий SLO». Диалог не состоится, пока с продуктовой стороны нет того, кто за этот SLO отвечает головой.</p>
<h2 id="что-мы-на-самом-деле-обсуждаем-с-бизнесом">Что мы на самом деле обсуждаем с бизнесом</h2>
<p>Допустим, продуктовый владелец есть и готов разговаривать. О чём конкретно идёт разговор? Не «покажи мне дашборд» — это не разговор. Настоящий диалог о надёжности крутится вокруг четырёх вещей.</p>
<h3 id="1-таргет-как-бюджет-на-компромисс">1. Таргет как бюджет на компромисс</h3>
<p>Главное, что должен понять бизнес: <strong>высокая надёжность — это не бесплатно и не всегда хорошо</strong>. Это позиция на шкале трейд-оффов.</p>
<ul>
<li>Высокий SLO = низкая скорость фич (бюджет ошибок мал, каждый релиз — ставка), дорогая инфраструктура (резервирование, регионы), медленные релизы (канарейка, постепенный rollout).</li>
<li>Низкий SLO = быстрые фичи, дешёвая инфраструктура, но больше видимых пользователю сбоев.</li>
</ul>
<p>Из «чертежа» помним конкретные множители для нашего маркетплейса: переход с 99.9% на 99.99% — это <code>4–6×</code> → <code>15–25×</code> baseline по инфраструктуре и <code>2–3×</code> → <code>8–10×</code> по людям. Это не «инженеры хотят красивую цифру». Это бизнес-решение, потому что платит за него бизнес, и отказывается от скорости фич — тоже бизнес.</p>
<p>Ключевая фраза, которую SRE должен уметь произнести в этом разговоре, звучит примерно так:</p>
<blockquote><p>«Если снизим SLO поиска с 99.99% до 99.9% — освободим вот этот регион и вот эту часть дежурной нагрузки, сможем катить фичи в полтора раза быстрее. Заплатим за это вот таким ростом видимых сбоев на пике. Берём?»</p>
</blockquote><p>Это и есть перевод надёжности в валюту, понятную обеим сторонам. Не «давайте сделаем понадёжнее», а «вот цена, вот что освобождается, решаем вместе».</p>
<h3 id="2-error-budget-policy-правила-пишутся-до-пожара-а-не-во-время">2. Error budget policy: правила пишутся до пожара, а не во время</h3>
<p>Error budget policy — это <strong>заранее зафиксированные правила игры</strong>. Не «что мы будем делать, когда бюджет сгорит» (это решается в панике и проигрывается дедлайну), а «что происходит автоматически, когда бюджет сгорел» — написанное и согласованное обеими сторонами заранее.</p>
<p>Минимальная политика отвечает на четыре вопроса:</p>
<ul>
<li><strong>Кто и при каком условии останавливает релизы?</strong> Например: при burn-rate выше <code>14.4×</code> за час — автоматический freeze рискованных деплоев до конца окна.</li>
<li><strong>Кто разрешает продолжить?</strong> Не «инженер решил» и не «продакт продавил», а названная роль с названным условием.</li>
<li><strong>Что переходит в roadmap при сгоревшем бюджете?</strong> Reliability-задачи получают приоритет над фичами — и это записано, а не выпрашивается каждый раз заново.</li>
<li><strong>Когда и как пересматривается сам SLO?</strong> Раз в квартал — короткий ритуал «изменилось ли что-то в бизнесе или системе, что меняет нужное число».</li>
</ul>
<p>На практике это умещается на одну страницу — по образцу error budget policy из главы <a href="https://sre.google/workbook/implementing-slos/" rel="noopener noreferrer" target="_blank">«Implementing SLOs»</a> в Google SRE Workbook. Для поиска нашего маркетплейса политика могла бы выглядеть так:</p>
<blockquote><p><strong>Error budget policy — поиск каталога (SLO 99.9%, окно 30 дней)</strong></p>
<ul>
<li><strong>Жёлтая зона</strong> (потрачено 50% бюджета): уведомление в канал продукта, ретроспектива на ближайшем синке, новых рискованных экспериментов не начинаем.</li>
<li><strong>Красная зона</strong> (потрачено 90%): freeze рискованных деплоев — катим только хотфиксы и security-патчи. Reliability-задачи поднимаются над фичами в текущем спринте.</li>
<li><strong>Бюджет исчерпан</strong> (100%): полный freeze фич до конца окна. Эскалация продуктовому владельцу, обязательный разбор причин.</li>
<li><strong>Кто снимает freeze:</strong> продуктовый владелец по согласованию с SRE-lead, не раньше чем burn-rate вернулся к норме.</li>
<li><strong>Пересмотр SLO:</strong> первый понедельник квартала, участвуют продакт и SRE-lead.</li>
<li><strong>Подписали:</strong> <em>имя продуктового владельца</em>, <em>имя SRE-lead</em>, дата.</li>
</ul>
</blockquote><p>Эта страница важнее любого дашборда. Дашборд показывает, что бюджет горит. Страница говорит, что с этим делать и кто за это отвечает.</p>
<aside class="callout callout--warn" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
  <div class="callout__body"><p class="callout__title">Без политики SLO проигрывает всегда</p>
    <div class="callout__content"><p>Если решение «морозим или катим» принимается в момент пожара, оно принимается властью: кто выше по иерархии или ближе к CEO, тот и победил. Записанная заранее политика — это способ вынуть это решение из-под давления момента. Её главная ценность не в тексте, а в том, что её согласовали, когда никто не был в панике.</p></div>
  </div>
</aside><p>Важнейшее уточнение, которое часто упускают: <strong>error budget policy — это продуктовый артефакт, а не инженерный</strong>. Его подписывает не тимлид SRE «для себя», а продуктовый владелец — потому что это он соглашается, что при сгоревшем бюджете его фичи подождут. Подпись здесь не метафора: должно быть понятно, кто конкретно сказал «да» этим правилам. Если не можешь назвать имя — политики нет.</p>
<h3 id="3-перевод-девяток-в-деньги">3. Перевод девяток в деньги</h3>
<p>Бизнесу нужны не проценты, а валюта. «99.9% против 99.95%» для продакта — пустой звук, пока ты не показал, что стоит за этой разницей в деньгах. Поэтому в зрелом диалоге всегда есть связка <code>SLI → ₽</code>.</p>
<p>Берём наш маркетплейс. Из «чертежа»: GMV порядка 1 млрд ₽ в день, поиск кормит 40–70% покупок, каждые <code>+100 ms</code> латентности на ключевом read path стоят примерно <code>−0.5…1.5%</code> конверсии. Отсюда — простой мост:</p>
<div class="math-block" data-png="https://jtprog.ru/math/f26382c26b874de2f689194fa5fc707d760b5a298a8ee7d013c92ae9bc3b08a7.png"><span class="math__tex">$$ \text{Стоимость простоя} = \text{GMV в час} \times \text{доля задетого трафика} \times \text{длительность} $$</span></div><p>Дальше бюджет ошибок перестаёт быть процентом и становится деньгами. Сгоревший за инцидент бюджет — это не «−2% за час на графике», а «вот столько недополученной выручки, если такой инцидент повторится на пике». Burn-rate из множителя (<code>14.4×</code>, <code>36×</code> — подробно про него в статье <a href="/posts/burn-rate-is-not-speed/">«Скорость сгорания бюджета ошибок»</a>) превращается в скорость утечки денег: при таком-то burn-rate на пике Чёрной пятницы мы теряем столько-то рублей в минуту.</p>
<p>Посчитаем на пальцах для одного и того же 15-минутного инцидента в поиске — полная недоступность пути. GMV 1 млрд ₽ в день размазан неравномерно: обычным днём через поиск проходит порядка 400 млн ₽ выручки (предположим, 40% покупок стартуют с поиска), на пике распродажи дневной GMV умножается в <code>×3–5</code>, и доля стартующих с поиска покупок растёт.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Обычный вторник</th>
					<th>Пик распродажи</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Выручка через поиск за день</td>
					<td>~400 млн ₽</td>
					<td>~1,5 млрд ₽</td>
			</tr>
			<tr>
					<td>Выручка через поиск в час (грубо)</td>
					<td>~17 млн ₽</td>
					<td>~120 млн ₽</td>
			</tr>
			<tr>
					<td>Полный простой поиска 15 минут</td>
					<td>~4,2 млн ₽</td>
					<td>~30 млн ₽</td>
			</tr>
			<tr>
					<td>Тот же burn-rate в долях бюджета</td>
					<td>один и тот же <code>~50×</code></td>
					<td>один и тот же <code>~50×</code></td>
			</tr>
	</tbody>
</table>
<p>Цифры грубые и зависят от профиля трафика — но порядок и так говорит главное: <strong>один и тот же инцидент с одним и тем же burn-rate стоит на пике в разы дороже</strong>. Поэтому «потеряли 15 минут» — бессмысленная фраза без контекста «когда». А burn-rate как безразмерный множитель тут особенно удобен: <code>50×</code> одинаково тревожен и во вторник, и в Чёрную пятницу, просто в деньгах разворачивается по-разному.</p>
<p>Это разговор, который продакт понимает мгновенно. Не «у нас burn-rate 14.4», а «при текущем burn-rate мы за час пикового трафика теряем выручку, равную недельной зарплате команды — продолжаем катить?». Конвертация в деньги и переводит SLO из инженерной плоскости в управленческую.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Сезонность — это не деталь, это половина разговора</p>
    <div class="callout__content"><p>Пять минут даунтайма обычным вторником и пять минут в Чёрную пятницу — это разные деньги в десятки раз. Бюджет ошибок «горит» неравномерно: на пике RPS ты расходуешь его в разы быстрее. Поэтому проектировать SLO и считать стоимость инцидента надо на пик, а не на среднюю нагрузку. Бизнес это, кстати, обычно понимает лучше инженеров — он живёт сезонностью.</p></div>
  </div>
</aside><h3 id="4-где-у-нас-критичные-пользовательские-пути">4. «Где у нас критичные пользовательские пути»</h3>
<p>И последнее, самое неочевидное. Список критичных пользовательских путей (CUJ — critical user journey) бизнес должен уметь произнести <strong>сам</strong>. Это не то, что SRE приносит продакту как откровение — «смотри, я тут проанализировал, у вас критичен поиск». Если CUJ известны только инженерам, значит, бизнес не владеет собственной надёжностью.</p>
<p>Хорошая проверка зрелости диалога: спроси продакта «назови три сценария, простой которых на 30 минут стоит нам больше всего». Если он отвечает не задумываясь — диалог здоровый. Если лезет к SRE «а какие у нас там пути» — надёжность пока живёт в инженерной резервации.</p>
<p>Для нашего маркетплейса CUJ — это путь «пришёл с намерением купить → нашёл → выбрал → положил в корзину → оплатил». И продакт должен понимать, что поиск (вход в этот путь) и оплата (выход) — разного класса критичности, и что на них вешаются разные SLO. Это понимание — не бонус, это условие, без которого все предыдущие три пункта повисают в воздухе.</p>
<h3 id="диалог--это-цикл-а-не-презентация">Диалог — это цикл, а не презентация</h3>
<p>Сложим четыре пункта вместе. Диалог о надёжности — это не разовое «защитили SLO на демо», а повторяющийся цикл:</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/6645021c4c1a29e764d5d3a587329b900a85f97d7cee794c4f9c0f193d89c1a8.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-6645021c4c1a-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1710.91px; background-color: transparent;" viewBox="0 0 1710.90625 148.5" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-6645021c4c1a-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-6645021c4c1a-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-light .error-icon{fill:#dce0e8;}#m-6645021c4c1a-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-6645021c4c1a-light .edge-thickness-normal{stroke-width:1px;}#m-6645021c4c1a-light .edge-thickness-thick{stroke-width:3.5px;}#m-6645021c4c1a-light .edge-pattern-solid{stroke-dasharray:0;}#m-6645021c4c1a-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-6645021c4c1a-light .edge-pattern-dashed{stroke-dasharray:3;}#m-6645021c4c1a-light .edge-pattern-dotted{stroke-dasharray:2;}#m-6645021c4c1a-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-6645021c4c1a-light .marker.cross{stroke:#5c5f77;}#m-6645021c4c1a-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-6645021c4c1a-light p{margin:0;}#m-6645021c4c1a-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-6645021c4c1a-light .cluster-label text{fill:#4c4f69;}#m-6645021c4c1a-light .cluster-label span{color:#4c4f69;}#m-6645021c4c1a-light .cluster-label span p{background-color:transparent;}#m-6645021c4c1a-light .label text,#m-6645021c4c1a-light span{fill:#4c4f69;color:#4c4f69;}#m-6645021c4c1a-light .node rect,#m-6645021c4c1a-light .node circle,#m-6645021c4c1a-light .node ellipse,#m-6645021c4c1a-light .node polygon,#m-6645021c4c1a-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-6645021c4c1a-light .rough-node .label text,#m-6645021c4c1a-light .node .label text,#m-6645021c4c1a-light .image-shape .label,#m-6645021c4c1a-light .icon-shape .label{text-anchor:middle;}#m-6645021c4c1a-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-6645021c4c1a-light .rough-node .label,#m-6645021c4c1a-light .node .label,#m-6645021c4c1a-light .image-shape .label,#m-6645021c4c1a-light .icon-shape .label{text-align:center;}#m-6645021c4c1a-light .node.clickable{cursor:pointer;}#m-6645021c4c1a-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-6645021c4c1a-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-6645021c4c1a-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-6645021c4c1a-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-6645021c4c1a-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-6645021c4c1a-light .edgeLabel p{background-color:#eff1f5;}#m-6645021c4c1a-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-6645021c4c1a-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-6645021c4c1a-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-6645021c4c1a-light .cluster text{fill:#4c4f69;}#m-6645021c4c1a-light .cluster span{color:#4c4f69;}#m-6645021c4c1a-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-6645021c4c1a-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-6645021c4c1a-light rect.text{fill:none;stroke-width:0;}#m-6645021c4c1a-light .icon-shape,#m-6645021c4c1a-light .image-shape{background-color:#eff1f5;text-align:center;}#m-6645021c4c1a-light .icon-shape p,#m-6645021c4c1a-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-6645021c4c1a-light .icon-shape .label rect,#m-6645021c4c1a-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-6645021c4c1a-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-6645021c4c1a-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-6645021c4c1a-light .node .neo-node{stroke:#bcc0cc;}#m-6645021c4c1a-light [data-look="neo"].node rect,#m-6645021c4c1a-light [data-look="neo"].cluster rect,#m-6645021c4c1a-light [data-look="neo"].node polygon{stroke:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].node path{stroke:url(#m-6645021c4c1a-light-gradient);stroke-width:1px;}#m-6645021c4c1a-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-6645021c4c1a-light [data-look="neo"].node circle{stroke:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-6645021c4c1a-light [data-look="neo"].icon-shape .icon{fill:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-6645021c4c1a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-6645021c4c1a-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-6645021c4c1a-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M254.531,63.811L258.698,62.634C262.865,61.457,271.198,59.104,278.865,57.927C286.531,56.75,293.531,56.75,297.031,56.75L300.531,56.75" id="m-6645021c4c1a-light-L_CUJ_Target_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_CUJ_Target_0" data-points="W3sieCI6MjU0LjUzMTI1LCJ5Ijo2My44MTA4MDcyNTA1MDA1OH0seyJ4IjoyNzkuNTMxMjUsInkiOjU2Ljc1fSx7IngiOjMwNC41MzEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M563.578,56.75L567.745,56.75C571.911,56.75,580.245,56.75,587.911,56.75C595.578,56.75,602.578,56.75,606.078,56.75L609.578,56.75" id="m-6645021c4c1a-light-L_Target_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Target_Policy_0" data-points="W3sieCI6NTYzLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NTg4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NjEzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M873.578,56.75L877.745,56.75C881.911,56.75,890.245,56.75,897.911,56.75C905.578,56.75,912.578,56.75,916.078,56.75L919.578,56.75" id="m-6645021c4c1a-light-L_Policy_Run_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Run_0" data-points="W3sieCI6ODczLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6ODk4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6OTIzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M1168.672,56.75L1172.839,56.75C1177.005,56.75,1185.339,56.75,1193.005,56.75C1200.672,56.75,1207.672,56.75,1211.172,56.75L1214.672,56.75" id="m-6645021c4c1a-light-L_Run_Money_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Run_Money_0" data-points="W3sieCI6MTE2OC42NzE4NzUsInkiOjU2Ljc1fSx7IngiOjExOTMuNjcxODc1LCJ5Ijo1Ni43NX0seyJ4IjoxMjE4LjY3MTg3NSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M1412.141,56.75L1416.307,56.75C1420.474,56.75,1428.807,56.75,1436.5,57.766C1444.193,58.781,1451.245,60.812,1454.771,61.828L1458.297,62.844" id="m-6645021c4c1a-light-L_Money_Review_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Money_Review_0" data-points="W3sieCI6MTQxMi4xNDA2MjUsInkiOjU2Ljc1fSx7IngiOjE0MzcuMTQwNjI1LCJ5Ijo1Ni43NX0seyJ4IjoxNDYyLjE0MDYyNSwieSI6NjMuOTUwODE2ODA5MDcwODh9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/><path d="M1462.141,133.299L1457.974,134.499C1453.807,135.699,1445.474,138.1,1421.018,139.3C1396.563,140.5,1355.984,140.5,1315.406,140.5C1274.828,140.5,1234.25,140.5,1189.37,140.5C1144.49,140.5,1095.307,140.5,1046.125,140.5C996.943,140.5,947.76,140.5,897.336,140.5C846.911,140.5,795.245,140.5,743.578,140.5C691.911,140.5,640.245,140.5,588.658,140.5C537.07,140.5,485.563,140.5,434.055,140.5C382.547,140.5,331.039,140.5,301.76,139.504C272.481,138.509,265.431,136.518,261.906,135.522L258.381,134.526" id="m-6645021c4c1a-light-L_Review_CUJ_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Review_CUJ_0" data-points="W3sieCI6MTQ2Mi4xNDA2MjUsInkiOjEzMy4yOTkxODMxOTA5MjkxfSx7IngiOjE0MzcuMTQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMzE1LjQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMTkzLjY3MTg3NSwieSI6MTQwLjV9LHsieCI6MTA0Ni4xMjUsInkiOjE0MC41fSx7IngiOjg5OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjc0My41NzgxMjUsInkiOjE0MC41fSx7IngiOjU4OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjQzNC4wNTQ2ODc1LCJ5IjoxNDAuNX0seyJ4IjoyNzkuNTMxMjUsInkiOjE0MC41fSx7IngiOjI1NC41MzEyNSwieSI6MTMzLjQzOTE5Mjc0OTQ5OTQzfV0=" data-look="classic" marker-end="url(#m-6645021c4c1a-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_CUJ_Target_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Target_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Run_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Run_Money_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Money_Review_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Review_CUJ_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-6645021c4c1a-light-flowchart-CUJ-0" data-look="classic" transform="translate(131.265625, 98.625)"><rect class="basic label-container" style="" x="-123.265625" y="-37.5" width="246.53125" height="75"/><g class="label" style="" transform="translate(-93.265625, -22.5)"><rect/><foreignObject width="186.53125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Бизнес называет<br />критичные пути и их цену</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Target-1" data-look="classic" transform="translate(434.0546875, 56.75)"><rect class="basic label-container" style="" x="-129.5234375" y="-37.5" width="259.046875" height="75"/><g class="label" style="" transform="translate(-99.5234375, -22.5)"><rect/><foreignObject width="199.046875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Вместе выбираем SLO<br />как бюджет на компромисс</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Policy-3" data-look="classic" transform="translate(743.578125, 56.75)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Записываем error budget policy<br />продакт подписывает</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Run-5" data-look="classic" transform="translate(1046.125, 56.75)"><rect class="basic label-container" style="" x="-122.546875" y="-37.5" width="245.09375" height="75"/><g class="label" style="" transform="translate(-92.546875, -22.5)"><rect/><foreignObject width="185.09375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Живём по бюджету:<br />деплои, инциденты, фичи</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Money-7" data-look="classic" transform="translate(1315.40625, 56.75)"><rect class="basic label-container" style="" x="-96.734375" y="-37.5" width="193.46875" height="75"/><g class="label" style="" transform="translate(-66.734375, -22.5)"><rect/><foreignObject width="133.46875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Считаем сгорание<br />в деньгах</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-light-flowchart-Review-9" data-look="classic" transform="translate(1582.5234375, 98.625)"><rect class="basic label-container" style="" x="-120.3828125" y="-37.5" width="240.765625" height="75"/><g class="label" style="" transform="translate(-90.3828125, -22.5)"><rect/><foreignObject width="180.765625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Квартальный пересмотр:<br />изменилось ли число</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-6645021c4c1a-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-6645021c4c1a-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-6645021c4c1a-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-6645021c4c1a-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1710.91px; background-color: transparent;" viewBox="0 0 1710.90625 148.5" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-6645021c4c1a-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-6645021c4c1a-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-6645021c4c1a-dark .error-icon{fill:#181926;}#m-6645021c4c1a-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-6645021c4c1a-dark .edge-thickness-normal{stroke-width:1px;}#m-6645021c4c1a-dark .edge-thickness-thick{stroke-width:3.5px;}#m-6645021c4c1a-dark .edge-pattern-solid{stroke-dasharray:0;}#m-6645021c4c1a-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-6645021c4c1a-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-6645021c4c1a-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-6645021c4c1a-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-6645021c4c1a-dark .marker.cross{stroke:#a5adcb;}#m-6645021c4c1a-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-6645021c4c1a-dark p{margin:0;}#m-6645021c4c1a-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-6645021c4c1a-dark .cluster-label text{fill:#cad3f5;}#m-6645021c4c1a-dark .cluster-label span{color:#cad3f5;}#m-6645021c4c1a-dark .cluster-label span p{background-color:transparent;}#m-6645021c4c1a-dark .label text,#m-6645021c4c1a-dark span{fill:#cad3f5;color:#cad3f5;}#m-6645021c4c1a-dark .node rect,#m-6645021c4c1a-dark .node circle,#m-6645021c4c1a-dark .node ellipse,#m-6645021c4c1a-dark .node polygon,#m-6645021c4c1a-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-6645021c4c1a-dark .rough-node .label text,#m-6645021c4c1a-dark .node .label text,#m-6645021c4c1a-dark .image-shape .label,#m-6645021c4c1a-dark .icon-shape .label{text-anchor:middle;}#m-6645021c4c1a-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-6645021c4c1a-dark .rough-node .label,#m-6645021c4c1a-dark .node .label,#m-6645021c4c1a-dark .image-shape .label,#m-6645021c4c1a-dark .icon-shape .label{text-align:center;}#m-6645021c4c1a-dark .node.clickable{cursor:pointer;}#m-6645021c4c1a-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-6645021c4c1a-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-6645021c4c1a-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-6645021c4c1a-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-6645021c4c1a-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-6645021c4c1a-dark .edgeLabel p{background-color:#24273a;}#m-6645021c4c1a-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-6645021c4c1a-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-6645021c4c1a-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-6645021c4c1a-dark .cluster text{fill:#cad3f5;}#m-6645021c4c1a-dark .cluster span{color:#cad3f5;}#m-6645021c4c1a-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-6645021c4c1a-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-6645021c4c1a-dark rect.text{fill:none;stroke-width:0;}#m-6645021c4c1a-dark .icon-shape,#m-6645021c4c1a-dark .image-shape{background-color:#24273a;text-align:center;}#m-6645021c4c1a-dark .icon-shape p,#m-6645021c4c1a-dark .image-shape p{background-color:#24273a;padding:2px;}#m-6645021c4c1a-dark .icon-shape .label rect,#m-6645021c4c1a-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-6645021c4c1a-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-6645021c4c1a-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-6645021c4c1a-dark .node .neo-node{stroke:#494d64;}#m-6645021c4c1a-dark [data-look="neo"].node rect,#m-6645021c4c1a-dark [data-look="neo"].cluster rect,#m-6645021c4c1a-dark [data-look="neo"].node polygon{stroke:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].node path{stroke:url(#m-6645021c4c1a-dark-gradient);stroke-width:1px;}#m-6645021c4c1a-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-6645021c4c1a-dark [data-look="neo"].node circle{stroke:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-6645021c4c1a-dark [data-look="neo"].icon-shape .icon{fill:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-6645021c4c1a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-6645021c4c1a-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-6645021c4c1a-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-6645021c4c1a-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M254.531,63.811L258.698,62.634C262.865,61.457,271.198,59.104,278.865,57.927C286.531,56.75,293.531,56.75,297.031,56.75L300.531,56.75" id="m-6645021c4c1a-dark-L_CUJ_Target_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_CUJ_Target_0" data-points="W3sieCI6MjU0LjUzMTI1LCJ5Ijo2My44MTA4MDcyNTA1MDA1OH0seyJ4IjoyNzkuNTMxMjUsInkiOjU2Ljc1fSx7IngiOjMwNC41MzEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M563.578,56.75L567.745,56.75C571.911,56.75,580.245,56.75,587.911,56.75C595.578,56.75,602.578,56.75,606.078,56.75L609.578,56.75" id="m-6645021c4c1a-dark-L_Target_Policy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Target_Policy_0" data-points="W3sieCI6NTYzLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NTg4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6NjEzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M873.578,56.75L877.745,56.75C881.911,56.75,890.245,56.75,897.911,56.75C905.578,56.75,912.578,56.75,916.078,56.75L919.578,56.75" id="m-6645021c4c1a-dark-L_Policy_Run_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Policy_Run_0" data-points="W3sieCI6ODczLjU3ODEyNSwieSI6NTYuNzV9LHsieCI6ODk4LjU3ODEyNSwieSI6NTYuNzV9LHsieCI6OTIzLjU3ODEyNSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M1168.672,56.75L1172.839,56.75C1177.005,56.75,1185.339,56.75,1193.005,56.75C1200.672,56.75,1207.672,56.75,1211.172,56.75L1214.672,56.75" id="m-6645021c4c1a-dark-L_Run_Money_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Run_Money_0" data-points="W3sieCI6MTE2OC42NzE4NzUsInkiOjU2Ljc1fSx7IngiOjExOTMuNjcxODc1LCJ5Ijo1Ni43NX0seyJ4IjoxMjE4LjY3MTg3NSwieSI6NTYuNzV9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M1412.141,56.75L1416.307,56.75C1420.474,56.75,1428.807,56.75,1436.5,57.766C1444.193,58.781,1451.245,60.812,1454.771,61.828L1458.297,62.844" id="m-6645021c4c1a-dark-L_Money_Review_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Money_Review_0" data-points="W3sieCI6MTQxMi4xNDA2MjUsInkiOjU2Ljc1fSx7IngiOjE0MzcuMTQwNjI1LCJ5Ijo1Ni43NX0seyJ4IjoxNDYyLjE0MDYyNSwieSI6NjMuOTUwODE2ODA5MDcwODh9XQ==" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/><path d="M1462.141,133.299L1457.974,134.499C1453.807,135.699,1445.474,138.1,1421.018,139.3C1396.563,140.5,1355.984,140.5,1315.406,140.5C1274.828,140.5,1234.25,140.5,1189.37,140.5C1144.49,140.5,1095.307,140.5,1046.125,140.5C996.943,140.5,947.76,140.5,897.336,140.5C846.911,140.5,795.245,140.5,743.578,140.5C691.911,140.5,640.245,140.5,588.658,140.5C537.07,140.5,485.563,140.5,434.055,140.5C382.547,140.5,331.039,140.5,301.76,139.504C272.481,138.509,265.431,136.518,261.906,135.522L258.381,134.526" id="m-6645021c4c1a-dark-L_Review_CUJ_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Review_CUJ_0" data-points="W3sieCI6MTQ2Mi4xNDA2MjUsInkiOjEzMy4yOTkxODMxOTA5MjkxfSx7IngiOjE0MzcuMTQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMzE1LjQwNjI1LCJ5IjoxNDAuNX0seyJ4IjoxMTkzLjY3MTg3NSwieSI6MTQwLjV9LHsieCI6MTA0Ni4xMjUsInkiOjE0MC41fSx7IngiOjg5OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjc0My41NzgxMjUsInkiOjE0MC41fSx7IngiOjU4OC41NzgxMjUsInkiOjE0MC41fSx7IngiOjQzNC4wNTQ2ODc1LCJ5IjoxNDAuNX0seyJ4IjoyNzkuNTMxMjUsInkiOjE0MC41fSx7IngiOjI1NC41MzEyNSwieSI6MTMzLjQzOTE5Mjc0OTQ5OTQzfV0=" data-look="classic" marker-end="url(#m-6645021c4c1a-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_CUJ_Target_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Target_Policy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Policy_Run_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Run_Money_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Money_Review_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Review_CUJ_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-6645021c4c1a-dark-flowchart-CUJ-0" data-look="classic" transform="translate(131.265625, 98.625)"><rect class="basic label-container" style="" x="-123.265625" y="-37.5" width="246.53125" height="75"/><g class="label" style="" transform="translate(-93.265625, -22.5)"><rect/><foreignObject width="186.53125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Бизнес называет<br />критичные пути и их цену</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Target-1" data-look="classic" transform="translate(434.0546875, 56.75)"><rect class="basic label-container" style="" x="-129.5234375" y="-37.5" width="259.046875" height="75"/><g class="label" style="" transform="translate(-99.5234375, -22.5)"><rect/><foreignObject width="199.046875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Вместе выбираем SLO<br />как бюджет на компромисс</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Policy-3" data-look="classic" transform="translate(743.578125, 56.75)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Записываем error budget policy<br />продакт подписывает</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Run-5" data-look="classic" transform="translate(1046.125, 56.75)"><rect class="basic label-container" style="" x="-122.546875" y="-37.5" width="245.09375" height="75"/><g class="label" style="" transform="translate(-92.546875, -22.5)"><rect/><foreignObject width="185.09375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Живём по бюджету:<br />деплои, инциденты, фичи</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Money-7" data-look="classic" transform="translate(1315.40625, 56.75)"><rect class="basic label-container" style="" x="-96.734375" y="-37.5" width="193.46875" height="75"/><g class="label" style="" transform="translate(-66.734375, -22.5)"><rect/><foreignObject width="133.46875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Считаем сгорание<br />в деньгах</p></span></div></foreignObject></g></g><g class="node default" id="m-6645021c4c1a-dark-flowchart-Review-9" data-look="classic" transform="translate(1582.5234375, 98.625)"><rect class="basic label-container" style="" x="-120.3828125" y="-37.5" width="240.765625" height="75"/><g class="label" style="" transform="translate(-90.3828125, -22.5)"><rect/><foreignObject width="180.765625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Квартальный пересмотр:<br />изменилось ли число</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-6645021c4c1a-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-6645021c4c1a-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-6645021c4c1a-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p>Каждый виток — это квартал. На входе бизнес приносит «что и сколько стоит», на выходе получает «вот сколько надёжности мы дали и какой ценой». Если хоть одно звено разорвано — цикл рассыпается, и SLO откатывается в режим артефакта. Именно эту целостность и проверяют на собесе — к нему и переходим.</p>
<h2 id="кейсы-с-system-design-собеса">Кейсы с System Design-собеса</h2>
<p>Те же тезисы, но в позе «кандидат под прессом». Когда меня самого недавно гоняли по этой теме на интервью, стало особенно ясно: вопросы «про SLO» почти никогда не про SLO. Они про то, умеешь ли ты вести разговор о надёжности так, чтобы бизнес поменял поведение. Разберу пять типовых сценариев — на том же маркетплейсе.</p>
<h3 id="кейс-1-зачем-тебе-slo-999-а-не-9999">Кейс 1. «Зачем тебе SLO 99.9, а не 99.99?»</h3>
<p><strong>Что проверяют:</strong> умеет ли кандидат держать рамку «бюджет → архитектура → деньги».</p>
<p><strong>Провальный ответ:</strong> начать с топологии. «Ну, 99.99 — это active-active в трёх регионах, а 99.9 — один регион с failover между AZ». Технически верно, но это ответ на вопрос «чем отличаются архитектуры», а не «зачем тебе эта надёжность».</p>
<p><strong>Сильный ответ всегда начинается со сценария бизнеса:</strong></p>
<blockquote><p>«Зависит от того, какой это путь. Если это поиск каталога — главный драйвер конверсии — то считаем: на 99.9% бюджет даунтайма на пике ~14 минут в месяц, на 99.99% — полторы минуты. Разница в архитектуре — лишних <code>15–25×</code> против <code>4–6×</code> baseline. Вопрос к бизнесу: эти 12 минут в месяц на пике стоят нам больше или меньше, чем утроение инфраструктуры? Если GMV такой, что минута пикового даунтайма — сотни тысяч, берём 99.99. Если нет — 99.9 с запасом».</p>
</blockquote><p>Ключевое: кандидат не выбирает девятку сам. Он показывает, что выбор девятки — это разговор с бизнесом, и приносит в этот разговор обе цифры — цену надёжности и цену её отсутствия.</p>
<h3 id="кейс-2-у-вас-9995-по-чекауту-но-30-оплат-не-проходит--что-не-так">Кейс 2. «У вас 99.95 по чекауту, но 30% оплат не проходит — что не так?»</h3>
<p><strong>Что проверяют:</strong> понимает ли кандидат разрыв между технической доступностью и продуктовой надёжностью.</p>
<p>Это мой любимый кейс, потому что он ловит самую частую ошибку. Дашборд зелёный — <code>99.95%</code> по эндпоинту оплаты. А бизнес кричит, что треть оплат не доходит. Противоречие? Нет. SLI повешен не туда.</p>
<p><strong>Что произошло:</strong> SLI считает «доля ответов <code>2xx</code> от ручки <code>/pay</code>». Ручка честно отвечает <code>200 OK</code> — она приняла запрос. А дальше платёж уходит в провайдера, провайдер таймаутит, ретрай не настроен, и для пользователя оплата «не прошла» — но для SLI всё прекрасно, ручка-то ответила.</p>
<p><strong>Сильный ответ:</strong></p>
<blockquote><p>«SLI измеряет здоровье ручки, а не сценарий. Его надо переповесить на пользовательский результат: „доля начатых оплат, завершившихся подтверждённым списанием за <code>&lt; N</code> секунд“. Тогда таймауты провайдера, потерянные колбэки и зависшие платежи попадут в знаменатель, и 99.95% превратятся в честные 70%. Дашборд начнёт показывать ту же боль, что чувствует бизнес».</p>
</blockquote><p>Это ровно разведение «продуктовая надёжность против технической доступности». Кандидат, который видит этот разрыв, понимает, что SLI вешается на сценарий, а не на success rate ручки. Кандидат, который не видит, будет до утра чинить инфраструктуру, которая и так зелёная.</p>
<h3 id="кейс-3-бизнес-требует-100-доступности-что-отвечаешь">Кейс 3. «Бизнес требует 100% доступности. Что отвечаешь?»</h3>
<p><strong>Что проверяют:</strong> переговорный навык в чистом виде.</p>
<p><strong>Провальный ответ:</strong> «100% невозможно». Технически правда, но это ответ инженера, который закрывает разговор, а не ведёт его. Бизнес услышит «не хочу» и пойдёт давить.</p>
<p><strong>Сильный ответ — не «нельзя», а цена:</strong></p>
<blockquote><p>«Окей, давай посчитаем, что такое 100%. Каждая девятка после 99.9 удорожает систему примерно в разы. 99.99 — это уже <code>15–25×</code> baseline и заморозка скорости релизов. 99.999 — это multi-cloud, независимые DNS, частично независимые кодовые базы, подходы из telecom. А 100% — это бесконечность: ноль допустимых сбоев означает ноль деплоев, ноль экспериментов, ноль изменений. Мы перестанем выпускать фичи вообще. Точно нужны эти девятки — или нам нужно, чтобы конкретный сценарий, скажем оплата, не падал заметно для пользователя? Это разные задачи, и вторая решается сильно дешевле».</p>
</blockquote><p>Кандидат переводит абсолютное требование в трейд-офф и возвращает бизнесу выбор. Часто за «хотим 100%» стоит «не хотим терять оплаты в пик» — а это не про 100%, это про правильный SLO на правильный путь плюс graceful degradation.</p>
<h3 id="кейс-4-кто-принимает-решение-о-фризе-релизов-при-сгорании-бюджета">Кейс 4. «Кто принимает решение о фризе релизов при сгорании бюджета?»</h3>
<p><strong>Что проверяют:</strong> есть ли у кандидата error budget policy в голове и понимает ли он, что это продуктовый артефакт.</p>
<p><strong>Провальный ответ:</strong> «SRE решает» или «тимлид решает». Это инженерный ответ, и он выдаёт, что у кандидата SLO живёт в инженерной резервации.</p>
<p><strong>Сильный ответ:</strong></p>
<blockquote><p>«Решает не человек в моменте, а записанная заранее error budget policy. В ней зафиксировано: при таком-то burn-rate за такое-то окно — автоматический freeze рискованных деплоев. И подписана эта политика продуктовым владельцем, потому что это он соглашается, что его фичи подождут. Если решение каждый раз принимается заново и под давлением — значит, политики нет, и побеждает тот, кто громче».</p>
</blockquote><p>Здесь проверяют понимание, что фриз — это не власть SRE над продуктом, а заранее согласованное обеими сторонами правило. Кандидат, который это понимает, не будет воевать с продактом — он сошлётся на то, что они вместе подписали.</p>
<h3 id="кейс-5-покажи-как-сожжённый-бюджет-влияет-на-дорожную-карту">Кейс 5. «Покажи, как сожжённый бюджет влияет на дорожную карту»</h3>
<p><strong>Что проверяют:</strong> встроен ли SRE в плановый цикл или живёт в параллельной вселенной с продуктом.</p>
<p><strong>Провальный ответ:</strong> «при сгорании бюджета мы делаем post-mortem». Post-mortem — это хорошо, но это про прошлое. Вопрос — про будущее, про roadmap.</p>
<p><strong>Сильный ответ:</strong></p>
<blockquote><p>«Сгоревший бюджет — это вход в планирование, а не только разбор инцидента. Механически: при систематическом сгорании reliability-задачи поднимаются в приоритете над фичами на следующий цикл — это записано в policy. На квартальном reliability review мы смотрим burn-rate за квартал по каждому пути и решаем, куда идёт инженерное усилие. Если поиск стабильно жжёт бюджет — значит, в следующем квартале часть roadmap уходит на его укрепление, и продакт это принимает, потому что видит связь „сгорание → потерянная выручка“».</p>
</blockquote><p>Это проверка на то, что SRE и продукт планируют из одного документа, а не из двух. Если бюджет ошибок никак не дотягивается до roadmap — SRE и продукт живут в разных вселенных, и SLO снова декорация.</p>
<h3 id="главный-паттерн-всех-пяти-кейсов">Главный паттерн всех пяти кейсов</h3>
<p>Сведём пять кейсов в одну таблицу — что на самом деле проверяет каждый:</p>
<table>
	<thead>
			<tr>
					<th>Вопрос на собесе</th>
					<th>Что реально проверяют</th>
					<th>Маркер слабого ответа</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Зачем 99.9, а не 99.99?</td>
					<td>Рамка «бюджет → архитектура → деньги»</td>
					<td>Начал с топологии, а не со сценария</td>
			</tr>
			<tr>
					<td>99.95, но 30% оплат не проходит?</td>
					<td>Разрыв «доступность vs надёжность»</td>
					<td>Полез чинить зелёную инфраструктуру</td>
			</tr>
			<tr>
					<td>Бизнес хочет 100%?</td>
					<td>Переговорный навык</td>
					<td>Сказал «невозможно» и закрыл тему</td>
			</tr>
			<tr>
					<td>Кто решает фриз?</td>
					<td>Error budget policy как продуктовый артефакт</td>
					<td>«SRE решает» / «тимлид решает»</td>
			</tr>
			<tr>
					<td>Бюджет → roadmap?</td>
					<td>Встроен ли SRE в плановый цикл</td>
					<td>«Делаем post-mortem» (про прошлое)</td>
			</tr>
	</tbody>
</table>
<p>На собесе не проверяют знание формул сгорания бюджета — это гуглится за минуту. Проверяют, <strong>можешь ли ты вести разговор о надёжности так, чтобы бизнес поменял своё поведение</strong>. Все пять сильных ответов имеют одну форму: начать со сценария и денег, перевести требование в трейд-офф, вернуть выбор бизнесу, сослаться на заранее согласованное правило. Это и есть навык, который «чертёж» описывал как «SLO — интерфейс между System Design и бизнесом», только теперь — со стороны человека, который этим интерфейсом пользуется вживую.</p>
<h2 id="метрики-sre-команды-для-мультипродукта">Метрики SRE-команды для мультипродукта</h2>
<p>Третий тезис — и самый практически болезненный. Пока продукт один, метрики команды SRE совпадают с метриками продукта: SLO продукта, его бюджет, его инциденты. Команда отчитывается надёжностью своего единственного сервиса, и всё сходится.</p>
<p>Когда продуктов становится 5–10, эта простая картина ломается. Команда обслуживает зоопарк сервисов с разными CUJ, разной критичностью, разной зрелостью. И тут возникает вопрос, на который у большинства команд нет хорошего ответа: <strong>как одной цифрой отчитаться перед менеджментом за надёжность, которую ты дал восьми разным продуктам?</strong> Соблазн — взять среднее. На нём всё и ломается.</p>
<h3 id="что-не-работает-как-метрика-команды">Что не работает как метрика команды</h3>
<p>Три популярных агрегата, каждый из которых врёт по-своему:</p>
<ul>
<li><strong>MTTR в среднем по всем инцидентам.</strong> Растворяет редкие тяжёлые случаи в массе мелких. Команда, которая быстро закрывает сто пустяковых алертов и месяц не может починить один критичный, покажет прекрасный средний MTTR. Хуже того, метрика поощряет «закрыть быстро, не разбираясь» — лишь бы цифра была хорошей.</li>
<li><strong>Composite SLO по всем продуктам.</strong> «Средняя надёжность по компании 99.94%» — это средняя температура по больнице. Она теряет CUJ: упавший в ноль критичный путь одного продукта тонет в здоровье семи остальных. Наивная агрегация SLO в мультипродукте математически врёт — этот эффект хорошо разобран в посте Алекса Идальго <a href="https://blog.relyabilit.ie/implicit-slos-and-their-dangers/" rel="noopener noreferrer" target="_blank">«Implicit SLOs and their dangers»</a>: композит почти всегда оптимистичнее, чем худший из путей, а пользователю больно именно от худшего.</li>
<li><strong>Количество инцидентов в абсолюте.</strong> Стимулирует молчать о мелких. Если команду меряют числом инцидентов, она перестаёт их заводить — и ты теряешь сигнал. Это классический закон Гудхарта: метрика, ставшая целью, перестаёт быть метрикой.</li>
</ul>
<p>Общая болезнь всех трёх — они сжимают разнородное в одно число, и в этом сжатии теряется ровно то, ради чего метрику заводили.</p>
<h3 id="что-осмысленно-мерить-три-слоя">Что осмысленно мерить: три слоя</h3>
<p>Вместо одного агрегата — раскладка по трём слоям, параллельная трём ролям из «чертежа»: бизнес / инженерия / управление. Каждый слой отвечает на свой вопрос и не пытается заменить остальные.</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/c763b06a1ef116a025ea10f9531dc8cd730d876c7a881e56427db9460d2d2e9e.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-c763b06a1ef1-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 896px; background-color: transparent;" viewBox="0 0 896 136" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-c763b06a1ef1-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-c763b06a1ef1-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-light .error-icon{fill:#dce0e8;}#m-c763b06a1ef1-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-c763b06a1ef1-light .edge-thickness-normal{stroke-width:1px;}#m-c763b06a1ef1-light .edge-thickness-thick{stroke-width:3.5px;}#m-c763b06a1ef1-light .edge-pattern-solid{stroke-dasharray:0;}#m-c763b06a1ef1-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-c763b06a1ef1-light .edge-pattern-dashed{stroke-dasharray:3;}#m-c763b06a1ef1-light .edge-pattern-dotted{stroke-dasharray:2;}#m-c763b06a1ef1-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-c763b06a1ef1-light .marker.cross{stroke:#5c5f77;}#m-c763b06a1ef1-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-c763b06a1ef1-light p{margin:0;}#m-c763b06a1ef1-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-c763b06a1ef1-light .cluster-label text{fill:#4c4f69;}#m-c763b06a1ef1-light .cluster-label span{color:#4c4f69;}#m-c763b06a1ef1-light .cluster-label span p{background-color:transparent;}#m-c763b06a1ef1-light .label text,#m-c763b06a1ef1-light span{fill:#4c4f69;color:#4c4f69;}#m-c763b06a1ef1-light .node rect,#m-c763b06a1ef1-light .node circle,#m-c763b06a1ef1-light .node ellipse,#m-c763b06a1ef1-light .node polygon,#m-c763b06a1ef1-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-c763b06a1ef1-light .rough-node .label text,#m-c763b06a1ef1-light .node .label text,#m-c763b06a1ef1-light .image-shape .label,#m-c763b06a1ef1-light .icon-shape .label{text-anchor:middle;}#m-c763b06a1ef1-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-c763b06a1ef1-light .rough-node .label,#m-c763b06a1ef1-light .node .label,#m-c763b06a1ef1-light .image-shape .label,#m-c763b06a1ef1-light .icon-shape .label{text-align:center;}#m-c763b06a1ef1-light .node.clickable{cursor:pointer;}#m-c763b06a1ef1-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-c763b06a1ef1-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-c763b06a1ef1-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-c763b06a1ef1-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-c763b06a1ef1-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-c763b06a1ef1-light .edgeLabel p{background-color:#eff1f5;}#m-c763b06a1ef1-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-c763b06a1ef1-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-c763b06a1ef1-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-c763b06a1ef1-light .cluster text{fill:#4c4f69;}#m-c763b06a1ef1-light .cluster span{color:#4c4f69;}#m-c763b06a1ef1-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-c763b06a1ef1-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-c763b06a1ef1-light rect.text{fill:none;stroke-width:0;}#m-c763b06a1ef1-light .icon-shape,#m-c763b06a1ef1-light .image-shape{background-color:#eff1f5;text-align:center;}#m-c763b06a1ef1-light .icon-shape p,#m-c763b06a1ef1-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-c763b06a1ef1-light .icon-shape .label rect,#m-c763b06a1ef1-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-c763b06a1ef1-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-c763b06a1ef1-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-c763b06a1ef1-light .node .neo-node{stroke:#bcc0cc;}#m-c763b06a1ef1-light [data-look="neo"].node rect,#m-c763b06a1ef1-light [data-look="neo"].cluster rect,#m-c763b06a1ef1-light [data-look="neo"].node polygon{stroke:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].node path{stroke:url(#m-c763b06a1ef1-light-gradient);stroke-width:1px;}#m-c763b06a1ef1-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-c763b06a1ef1-light [data-look="neo"].node circle{stroke:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-c763b06a1ef1-light [data-look="neo"].icon-shape .icon{fill:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-c763b06a1ef1-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-c763b06a1ef1-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-c763b06a1ef1-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M268,68L272.167,68C276.333,68,284.667,68,292.333,68C300,68,307,68,310.5,68L314,68" id="m-c763b06a1ef1-light-L_L3_L2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L3_L2_0" data-points="W3sieCI6MjY4LCJ5Ijo2OH0seyJ4IjoyOTMsInkiOjY4fSx7IngiOjMxOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-light_flowchart-v2-pointEnd)"/><path d="M578,68L582.167,68C586.333,68,594.667,68,602.333,68C610,68,617,68,620.5,68L624,68" id="m-c763b06a1ef1-light-L_L2_L1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L2_L1_0" data-points="W3sieCI6NTc4LCJ5Ijo2OH0seyJ4Ijo2MDMsInkiOjY4fSx7IngiOjYyOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_L3_L2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_L2_L1_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-c763b06a1ef1-light-flowchart-L1-0" data-look="classic" transform="translate(758, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 1. Надёжность продуктов суммарно<br />вопрос: какую надёжность мы дали бизнесу</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-light-flowchart-L2-1" data-look="classic" transform="translate(448, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 2. Инженерное усилие команды<br />вопрос: какой ценой и насколько устойчиво</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-light-flowchart-L3-2" data-look="classic" transform="translate(138, 68)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 3. Качество диалога<br />вопрос: встроена ли надёжность в продукт</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-c763b06a1ef1-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-c763b06a1ef1-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-c763b06a1ef1-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-c763b06a1ef1-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 896px; background-color: transparent;" viewBox="0 0 896 136" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-c763b06a1ef1-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-c763b06a1ef1-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-c763b06a1ef1-dark .error-icon{fill:#181926;}#m-c763b06a1ef1-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-c763b06a1ef1-dark .edge-thickness-normal{stroke-width:1px;}#m-c763b06a1ef1-dark .edge-thickness-thick{stroke-width:3.5px;}#m-c763b06a1ef1-dark .edge-pattern-solid{stroke-dasharray:0;}#m-c763b06a1ef1-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-c763b06a1ef1-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-c763b06a1ef1-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-c763b06a1ef1-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-c763b06a1ef1-dark .marker.cross{stroke:#a5adcb;}#m-c763b06a1ef1-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-c763b06a1ef1-dark p{margin:0;}#m-c763b06a1ef1-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-c763b06a1ef1-dark .cluster-label text{fill:#cad3f5;}#m-c763b06a1ef1-dark .cluster-label span{color:#cad3f5;}#m-c763b06a1ef1-dark .cluster-label span p{background-color:transparent;}#m-c763b06a1ef1-dark .label text,#m-c763b06a1ef1-dark span{fill:#cad3f5;color:#cad3f5;}#m-c763b06a1ef1-dark .node rect,#m-c763b06a1ef1-dark .node circle,#m-c763b06a1ef1-dark .node ellipse,#m-c763b06a1ef1-dark .node polygon,#m-c763b06a1ef1-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-c763b06a1ef1-dark .rough-node .label text,#m-c763b06a1ef1-dark .node .label text,#m-c763b06a1ef1-dark .image-shape .label,#m-c763b06a1ef1-dark .icon-shape .label{text-anchor:middle;}#m-c763b06a1ef1-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-c763b06a1ef1-dark .rough-node .label,#m-c763b06a1ef1-dark .node .label,#m-c763b06a1ef1-dark .image-shape .label,#m-c763b06a1ef1-dark .icon-shape .label{text-align:center;}#m-c763b06a1ef1-dark .node.clickable{cursor:pointer;}#m-c763b06a1ef1-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-c763b06a1ef1-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-c763b06a1ef1-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-c763b06a1ef1-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-c763b06a1ef1-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-c763b06a1ef1-dark .edgeLabel p{background-color:#24273a;}#m-c763b06a1ef1-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-c763b06a1ef1-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-c763b06a1ef1-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-c763b06a1ef1-dark .cluster text{fill:#cad3f5;}#m-c763b06a1ef1-dark .cluster span{color:#cad3f5;}#m-c763b06a1ef1-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-c763b06a1ef1-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-c763b06a1ef1-dark rect.text{fill:none;stroke-width:0;}#m-c763b06a1ef1-dark .icon-shape,#m-c763b06a1ef1-dark .image-shape{background-color:#24273a;text-align:center;}#m-c763b06a1ef1-dark .icon-shape p,#m-c763b06a1ef1-dark .image-shape p{background-color:#24273a;padding:2px;}#m-c763b06a1ef1-dark .icon-shape .label rect,#m-c763b06a1ef1-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-c763b06a1ef1-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-c763b06a1ef1-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-c763b06a1ef1-dark .node .neo-node{stroke:#494d64;}#m-c763b06a1ef1-dark [data-look="neo"].node rect,#m-c763b06a1ef1-dark [data-look="neo"].cluster rect,#m-c763b06a1ef1-dark [data-look="neo"].node polygon{stroke:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].node path{stroke:url(#m-c763b06a1ef1-dark-gradient);stroke-width:1px;}#m-c763b06a1ef1-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-c763b06a1ef1-dark [data-look="neo"].node circle{stroke:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-c763b06a1ef1-dark [data-look="neo"].icon-shape .icon{fill:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-c763b06a1ef1-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-c763b06a1ef1-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-c763b06a1ef1-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M268,68L272.167,68C276.333,68,284.667,68,292.333,68C300,68,307,68,310.5,68L314,68" id="m-c763b06a1ef1-dark-L_L3_L2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L3_L2_0" data-points="W3sieCI6MjY4LCJ5Ijo2OH0seyJ4IjoyOTMsInkiOjY4fSx7IngiOjMxOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-dark_flowchart-v2-pointEnd)"/><path d="M578,68L582.167,68C586.333,68,594.667,68,602.333,68C610,68,617,68,620.5,68L624,68" id="m-c763b06a1ef1-dark-L_L2_L1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_L2_L1_0" data-points="W3sieCI6NTc4LCJ5Ijo2OH0seyJ4Ijo2MDMsInkiOjY4fSx7IngiOjYyOCwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-c763b06a1ef1-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_L3_L2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_L2_L1_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-c763b06a1ef1-dark-flowchart-L1-0" data-look="classic" transform="translate(758, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 1. Надёжность продуктов суммарно<br />вопрос: какую надёжность мы дали бизнесу</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-dark-flowchart-L2-1" data-look="classic" transform="translate(448, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 2. Инженерное усилие команды<br />вопрос: какой ценой и насколько устойчиво</p></span></div></foreignObject></g></g><g class="node default" id="m-c763b06a1ef1-dark-flowchart-L3-2" data-look="classic" transform="translate(138, 68)"><rect class="basic label-container" style="" x="-130" y="-48.75" width="260" height="97.5"/><g class="label" style="" transform="translate(-100, -33.75)"><rect/><foreignObject width="200" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Слой 3. Качество диалога<br />вопрос: встроена ли надёжность в продукт</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-c763b06a1ef1-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-c763b06a1ef1-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-c763b06a1ef1-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><h4 id="слой-1-надёжность-продуктов-суммарно-бизнес-слой">Слой 1. Надёжность продуктов суммарно (бизнес-слой)</h4>
<p>Не «средний SLO», а покрытие и распределение:</p>
<ul>
<li><strong>Доля продуктов, у которых SLO существует, написан в терминах CUJ и формально подписан продактом.</strong> Это метрика зрелости, а не надёжности. «6 из 8 продуктов имеют подписанный SLO» говорит больше, чем любой композит.</li>
<li><strong>Доля продуктов с действующей error budget policy.</strong> Не «есть SLO», а «есть записанное правило, что делать при сгорании». Обычно эта доля сильно меньше первой — и разрыв между ними и есть фронт работ.</li>
<li><strong>Burn-rate бюджетов по всем продуктам как тепловая карта, а не среднее.</strong> Восемь строк, в каждой — сгорание за квартал. Красные клетки видно сразу, и ни одна не тонет в зелёных соседях.</li>
<li><strong>Сумма money loss за квартал</strong> — если связка <code>SLI → ₽</code> настроена. Это единственная цифра из этого слоя, которую менеджмент понимает без перевода.</li>
</ul>
<h4 id="слой-2-инженерное-усилие-команды-продуктивность">Слой 2. Инженерное усилие команды (продуктивность)</h4>
<p>Сколько стоит надёжность в человекочасах и насколько это устойчиво:</p>
<ul>
<li><strong>Toil как % времени команды.</strong> Гугловский потолок — 50% (правило из главы <a href="https://sre.google/sre-book/eliminating-toil/" rel="noopener noreferrer" target="_blank">«Eliminating Toil»</a> в Google SRE Book); всё, что выше, означает, что команда тонет в ручной рутине и не строит автоматизацию. Toil — отличная инженерная метрика зрелости именно потому, что растёт незаметно и душит команду медленно.</li>
<li><strong>Доля задач, переходящих от SRE обратно в продуктовую команду.</strong> Антипаттерн — «SRE как owner навсегда»: команда взяла продукт на поддержку и больше его не отпускает. Здоровая динамика — знания и ответственность постепенно возвращаются в продуктовую команду, а SRE остаётся на инструментарии и сложных случаях.</li>
<li><strong>Время от инцидента до закрытого action item</strong> — не до восстановления. Восстановили за 20 минут, а исправили причину через полгода — это плохо, и средний MTTR это скрывает, а эта метрика показывает.</li>
<li><strong>Доля постмортемов с реализованными action items.</strong> Постмортем, после которого ничего не сделали, — это терапия, а не инженерия. Эта доля показывает, учится ли команда на самом деле.</li>
</ul>
<h4 id="слой-3-качество-диалога-управленческий-слой">Слой 3. Качество диалога (управленческий слой)</h4>
<p>Самый неочевидный и самый важный слой — он меряет ровно то, о чём вся статья: встроена ли надёжность в продукт или живёт в инженерной резервации.</p>
<ul>
<li><strong>Регулярность product reliability review.</strong> Проводится ли квартальный разбор бюджета с каждым продактом — или только с теми, кто сам пришёл. Цель — со всеми и по расписанию.</li>
<li><strong>Доля продуктов, где бизнес сам приходит за обсуждением бюджета.</strong> Это лакмус. Если продакт сам спрашивает «сколько у нас бюджета осталось» — диалог здоров. Если SRE его догоняет — нет. Эта доля растёт медленно и означает культурный сдвиг, а не процессный.</li>
<li><strong>NPS/CSAT-аналог от команд разработки на сервис SRE.</strong> Команда SRE — это тоже сервис, у него есть внутренние пользователи (продуктовые команды), и их удовлетворённость измерима. Низкий внутренний CSAT при хороших технических метриках — сигнал, что SRE воспринимают как тормоз, а не как партнёра.</li>
</ul>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Слой 3 и культурная готовность</p>
    <div class="callout__content"><p>Третий слой пересекается с фазой 0 «cultural preparation» в Bear SRE Framework: SRE-практики невозможны без готовности бизнеса разговаривать. Метрики этого слоя, по сути, измеряют, насколько эта готовность реальна, а не декларирована. Можно вести их и как самостоятельную раскладку — но если у тебя уже есть рамка зрелости, слой 3 хорошо в неё ложится.</p></div>
  </div>
</aside><p>Соберём три слоя в одну таблицу — её удобно держать перед глазами при отчёте:</p>
<table>
	<thead>
			<tr>
					<th>Слой</th>
					<th>Вопрос</th>
					<th>Пример метрики</th>
					<th>Какой агрегат заменяет</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1. Надёжность продуктов</td>
					<td>Какую надёжность мы дали бизнесу</td>
					<td>Доля продуктов с подписанным CUJ-SLO; money loss за квартал; burn-rate тепловой картой</td>
					<td>Composite SLO</td>
			</tr>
			<tr>
					<td>2. Инженерное усилие</td>
					<td>Какой ценой и насколько устойчиво</td>
					<td>Toil %; время до закрытого action item; доля постмортемов с реализованными AI</td>
					<td>Средний MTTR</td>
			</tr>
			<tr>
					<td>3. Качество диалога</td>
					<td>Встроена ли надёжность в продукт</td>
					<td>Регулярность reliability review; доля «бизнес пришёл сам»; внутренний CSAT на SRE</td>
					<td>Число инцидентов</td>
			</tr>
	</tbody>
</table>
<p>Ни одна метрика из правой колонки («какой агрегат заменяет») не исчезает полностью — MTTR и счётчик инцидентов остаются полезными для внутренней диагностики. Но <strong>как метрика команды перед менеджментом</strong> они проигрывают трёхслойной раскладке, потому что усредняют то, что усреднять нельзя.</p>
<h3 id="как-это-звучит-в-отчёте-менеджменту">Как это звучит в отчёте менеджменту</h3>
<p>Финальная проверка всей раскладки — как команда отчитывается наверх. Сравни два варианта.</p>
<aside class="callout callout--warn" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
  <div class="callout__body"><p class="callout__title">Не делай SLO продуктов прямым KPI команды</p>
    <div class="callout__content"><p>Заманчиво объявить «продуктовую надёжность KPI команды SRE» — но это прямой путь к токсичным SLO. По <a href="https://t.me/jtprogru_channel/4460" rel="noopener noreferrer" target="_blank">закону Гудхарта</a>, как только SLO становится оценкой команды, она начинает оптимизировать цифру, а не надёжность: заниженные таргеты, «неудобные» события вне знаменателя, споры «считать ли инцидент инцидентом». Поэтому меряй команду не <strong>абсолютом</strong> SLO (он про продукт и принадлежит продакту), а <strong>дельтой и охватом</strong> — на сколько продуктов распространили подписанный SLO и policy, сократилось ли сгорание бюджета и money loss, вырос ли охват зрелым диалогом (слой 3). Занижением таргета это не саботируешь — потому и отчёт ниже построен на «было 4 → стало 6», а не на «composite SLO 99.94%».</p></div>
  </div>
</aside><p><strong>Плохо (агрегат):</strong> «У нас средний MTTR 22 минуты и composite SLO 99.94%». Менеджмент кивает, ничего не понимает, ни одно решение не меняется.</p>
<p><strong>Хорошо (три слоя):</strong> «За квартал укрепили подписанный SLO с error budget policy у 6 из 8 продуктов (было 4), суммарное сгорание бюджета упало на 35%, бизнес-потери от инцидентов сократились на N рублей. Toil держим на 40%. Два продукта вернули в зону ответственности продуктовых команд. Осталось два продукта без политики — это фронт на следующий квартал».</p>
<p>Второй отчёт показывает движение, цену и план. Это и есть язык, на котором SRE-команда разговаривает с менеджментом, когда продуктов больше одного, — не «средняя температура», а «вот что мы дали бизнесу и как конвертировали инженерное усилие в продуктовый результат».</p>
<h2 id="что-сделать-завтра-утром">Что сделать завтра утром</h2>
<p>Чек-лист «куда вернуться», когда дочитал. Пять вопросов — если на какой-то нет ответа, там и фронт работ.</p>
<ol>
<li><strong>Какой product reliability review встроен в наш квартальный план?</strong> Если ответ «никакой» или «по запросу» — начни с того, чтобы поставить в календарь регулярную встречу по бюджету с каждым продуктовым владельцем.</li>
<li><strong>Кто из бизнеса формально подписал error budget policy — и подписал ли вообще?</strong> Если не можешь назвать имя — у тебя SLO-артефакт, а не контракт. Это задача номер один.</li>
<li><strong>На какой панели я вижу „health по сценарию“, а не „health по сервису“?</strong> Если только по сервису — твои SLI повешены на ручки, а не на пути, и ты не видишь того, что чувствует пользователь.</li>
<li><strong>Сколько раз за последний квартал бизнес сам поднял разговор о трейд-оффе „фича против надёжности“?</strong> Если ноль — диалог пока односторонний, и слой 3 у тебя в красной зоне.</li>
<li><strong>Какой из метрик трёх слоёв у нас нет — и почему?</strong> Чаще всего отсутствует слой 3 (качество диалога), потому что его сложнее всего мерить. Но именно он отличает зрелую практику от набора дашбордов.</li>
</ol>
<h2 id="заключение">Заключение</h2>
<p>В «чертеже» я доказывал, что архитектура без SLO — это эстетика, а не инженерия. Эта статья добавляет вторую половину: <strong>SLO без диалога — это дашборд, а не управление.</strong></p>
<p>Чертёж надёжности сам по себе мёртв. Его надо защищать, торговать им и переписывать вместе с бизнесом каждый квартал. Технический инструмент — SLI, SLO, error budget — становится управленческим ровно в тот момент, когда у инженеров и бизнеса появляется одна картинка целевой надёжности и одна валюта компромиссов. До этого момента у тебя есть только красивые графики и предчувствия.</p>
<p>Три вещи, ради которых стоило городить всю статью:</p>
<ul>
<li><strong>Диалог — это формат принятия решений</strong>, а не презентация. Его сердце — записанная error budget policy, подписанная продуктовым владельцем.</li>
<li><strong>На собесе по SLO проверяют переговорный навык</strong>, а не арифметику. Сильный ответ начинается со сценария и денег и возвращает бизнесу выбор.</li>
<li><strong>Метрики команды при мультипродукте — это три слоя, а не один агрегат.</strong> Среднее по больнице прячет именно ту боль, ради которой метрику заводили.</li>
</ul>
<p>Надёжность не «достигается». Она каждый квартал заново строится в диалоге — и проигрывается ровно там, где этот диалог замолчал.</p>
<p>Формальную рамку вокруг того же самого — как выглядит регулярный ритуал ревью бюджета, кто в нём сидит и на каком языке разговаривает с не-инженерами — я разложил в карте компетенций: <a href="https://jtprogru.github.io/The-Way-of-SRE/culture/slo-budget-review/" rel="noopener noreferrer" target="_blank">SLO / Budget Review</a> и <a href="https://jtprogru.github.io/The-Way-of-SRE/culture/stakeholder-management/" rel="noopener noreferrer" target="_blank">Stakeholder Management</a>.</p>
<p>Спасибо, что дочитал до конца. Это продолжение цикла, начатого статьёй <a href="/posts/slo-as-architecture-blueprint/">«SLO как чертёж архитектуры»</a>. Если у тебя есть свой опыт диалога с бизнесом о надёжности — особенно истории, как error budget policy реально остановила релиз или как менялись метрики команды с ростом числа продуктов, — буду рад услышать.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Как декодировать QR-код руками</title>
      <link>https://jtprog.ru/posts/how-read-qr-code/</link>
      <pubDate>Wed, 20 May 2026 11:07:45 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/how-read-qr-code/</guid>
      <category>Blog</category>
      <category>Programming</category>
      <category>Howto</category>
      <category domain="category">Howto</category>
      <enclosure url="https://jtprog.ru/howto.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/howto.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! QR-коды в 2026 году встречаются буквально везде: меню в кафе, оплата в магазине, ссылка на Wi-Fi в аэропорту, чек из терминала. Камера телефона декодирует такой код за десятки миллисекунд, и большинство людей воспринимают QR как магический чёрно-белый прямоугольник, который «как-то превращается в URL». А внутри — обычный документированный стандарт ISO/IEC 18004, и при желании его реально разобрать вручную: с карандашом, бумагой и базовой арифметикой.</p>
<p>В этом разборе пройдём по шагам, как декодировать QR-код глазами: что такое finder patterns и тайминг-полоски, где зашиты маска и уровень коррекции ошибок, как читать данные зигзагом снизу-справа вверх и собирать из битов исходную строку. В качестве подопытного — самый маленький Version 1 (21×21) с содержимым <code>&quot;HI&quot;</code>, чтобы маска и зигзаг укладывались в голове, а не в Excel.</p>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Стандарт — <strong>ISO/IEC 18004</strong>, всего 40 версий: V1 — 21×21 модуль, дальше шаг +4 модуля на сторону до V40 (177×177).</li>
<li>Три больших квадрата по углам — <strong>finder patterns</strong>, задают ориентацию. Четвёртого (в правом нижнем углу) нет — это и есть «низ-право».</li>
<li>15 бит <strong>format info</strong> вокруг finder-ов хранят уровень коррекции (L/M/Q/H) и номер маски (0–7); защищены своим BCH-кодом и продублированы дважды.</li>
<li>Маска нужна, чтобы зона данных не складывалась в длинные одноцветные полосы и не имитировала случайно finder-паттерн — сканер бы поплыл. После XOR с правильной маской биты читаются «честно».</li>
<li>Данные читаются <strong>зигзагом снизу-справа вверх</strong>, парами столбцов, перепрыгивая тайминг-колонку №6, по 8 бит на байт.</li>
<li>Первые 4 бита — <strong>Mode Indicator</strong> (numeric / alphanumeric / byte / kanji), дальше <strong>Character Count Indicator</strong> переменной длины, потом сами данные, терминатор <code>0000</code> и padding <code>EC 11 EC 11 ...</code>.</li>
<li>За payload идут <strong>байты Reed-Solomon</strong> для коррекции ошибок: код над полем GF(256), число codeword&rsquo;ов зависит от версии и уровня (для V1-L — 7, для V1-H — уже 17). В этом разборе их сознательно пропускаем — без таблиц и GF-арифметики на бумаге не возьмёшь.</li>
</ul>
<p>После пары часов практики можно вытащить из небольшого кода (V1, 21×21) короткую строку вроде URL без сканера. До «прочитал визитку взглядом за секунду» — уже из области цирковых номеров: Reed-Solomon без вычислений в уме не выкрутишь, а на V10+ зигзаг становится ощутимо больно держать в голове.</p>
<hr>
<h2 id="подопытный">Подопытный</h2>
<p>Самый маленький размер — <strong>Version 1 (21×21 модуль)</strong>. Содержимое: <code>&quot;HI&quot;</code>. Раскрашены функциональные зоны.</p>
<figure class="post-figure">
    <img loading="lazy" decoding="async"
         src="/qr_labeled.png?v=1791919947"
         alt="Схема Version 1 QR-кода 21×21 с раскрашенными зонами: finder patterns, тайминг, format info, dark module, данные"></figure><hr>
<h2 id="шаг-1-ориентируемся">Шаг 1. Ориентируемся</h2>
<p>Три синих квадрата 7×7 — <strong>finder patterns</strong>. Они нужны сканеру, чтобы понять, где верх и поворот кода. Правило простое: три есть, а четвёртого (в правом нижнем углу) нет — туда смотрит «низ-право».</p>
<p>Вокруг каждого finder-а идёт <strong>separator</strong> — белая рамка в один модуль шириной (на схеме это пустая полоса между синим квадратом и всем остальным). Назначение прямолинейное: изолировать finder от данных, чтобы сканер не пытался разобрать «полуfinder-полуданные» на стыке и не плыл по координатам.</p>
<p>Между ними тянутся <strong>красные тайминг-полоски</strong> (строка 6 и столбец 6) — чередование чёрный-белый-чёрный. Они задают «линейку», по которой считаются координаты модулей. Если что-то поплыло — выравниваешься по ним.</p>
<p>Фиолетовый одиночный модуль — <strong>dark module</strong>, всегда чёрный по стандарту. Просто маркер, не несёт смысла.</p>
<p>Оранжевая зона — <strong>format info</strong>. 15 бит, продублированные дважды (для надёжности): одна копия идёт буквой Г вокруг левого верхнего finder-а, вторая — полоской справа от правого верхнего и снизу от левого нижнего. В ней зашифрованы две критичные вещи: <strong>уровень коррекции ошибок</strong> (L/M/Q/H) и <strong>номер маски</strong> (0–7).</p>
<p>Всё зелёное — это собственно данные плюс байты Рида-Соломона.</p>
<h3 id="чего-в-v1-ещё-нет-но-в-больших-версиях-встретишь">Чего в V1 ещё нет, но в больших версиях встретишь</h3>
<p>В нашей маленькой сетке всё описанное выше — это полный набор функциональных зон. Но как только переходишь к версиям побольше, появляются ещё две вещи:</p>
<ul>
<li><strong>Alignment patterns</strong> — маленькие квадраты 5×5 с чёрной точкой в центре, разбросанные по полю. Появляются с V2 (один штук, в правом нижнем углу), к V7 их уже шесть, а у V40 — целых 46. Нужны для <strong>локальной</strong> компенсации искажений сетки: V1 ещё читается «по линейке тайминга», а у большого QR на смятой или сфотографированной под углом картинке координаты модулей плывут, и без локальных якорей сканер промахивается.</li>
<li><strong>Version info</strong> — два блока 6×3 рядом с правым верхним и левым нижним finder-ами, появляются с V7. Внутри 18 бит: 6 бит самого номера версии и 12 бит BCH-чётности. Подход тот же, что у format info, только код покрупнее — BCH(18, 6), тоже исправляет до 3 ошибок. Декодеру эта зона нужна, чтобы достоверно определить размер кода, не доверяя «посчитаю клетки по краю» (на размытом снимке такой подсчёт ломается легко).</li>
</ul>
<p>В V1 этого ничего нет, поэтому в нашем разборе обходимся без них — но если ты потом возьмёшь V3 с того же чека, держи их в голове: чёрная точка в правом нижнем — не часть данных, а alignment.</p>
<hr>
<h2 id="шаг-2-снимаем-format-info">Шаг 2. Снимаем format info</h2>
<p>Биты идут: вдоль строки 8 слева направо (с пропуском тайминг-колонки), а потом по столбцу 8 снизу вверх (тоже с пропуском тайминг-строки). Старший бит = #14, младший = #0.</p>
<figure class="post-figure">
    <img loading="lazy" decoding="async"
         src="/format_info.png?v=1791919947"
         alt="Фокус на левый верхний угол: 15 клеток format info с номерами #14…#0 и их значениями"></figure><p>Считываем по порядку от #14 до #0:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>сырые биты : 1 1 0 1 1 0 0 0 1 0 0 0 0 0 1
XOR-маска  : 1 0 1 0 1 0 0 0 0 0 1 0 0 1 0
─────────────────────────────────────────
результат  : 0 1 1 1 0 0 0 0 1 0 1 0 0 1 1</code></pre>
</div>
<p>Маска <code>101010000010010</code> зашита в стандарте — иначе формат, состоящий из нулей, тоже был бы валидным, и любая дырка на пустом месте всё ломала бы.</p>
<p>Разбираем результат <code>011100001010011</code>:</p>
<ul>
<li><strong>биты 14–13</strong> = <code>01</code> → уровень коррекции <strong>L</strong> (~7 % избыточности)</li>
<li><strong>биты 12–10</strong> = <code>110</code> → <strong>маска №6</strong></li>
<li>остальные 10 бит — BCH-код для коррекции ошибок в самих этих 15 битах (нам не нужны).</li>
</ul>
<p>Теперь самое важное: мы знаем маску. Значит, можно <strong>снять её</strong> с зелёной зоны данных и прочитать настоящие биты.</p>
<p>Маска №6 задаётся формулой: клетка <code>(r, c)</code> инвертируется, если</p>
<div class="math-block" data-png="https://jtprog.ru/math/9c2a91624158b6224c0c48837ec67e61c2ae9c08cba29a81054acacb4a0629bf.png"><span class="math__tex">$$
((r \cdot c) \bmod 2 &#43; (r \cdot c) \bmod 3) \bmod 2 == 0
$$</span></div><h3 id="кодирование-уровня-коррекции">Кодирование уровня коррекции</h3>
<table>
	<thead>
			<tr>
					<th>биты</th>
					<th>уровень</th>
					<th>избыточность</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>01</code></td>
					<td>L</td>
					<td>~7 %</td>
			</tr>
			<tr>
					<td><code>00</code></td>
					<td>M</td>
					<td>~15 %</td>
			</tr>
			<tr>
					<td><code>11</code></td>
					<td>Q</td>
					<td>~25 %</td>
			</tr>
			<tr>
					<td><code>10</code></td>
					<td>H</td>
					<td>~30 %</td>
			</tr>
	</tbody>
</table>
<h3 id="bch15-5-под-капотом">BCH(15, 5) под капотом</h3>
<p>Format info — это не просто 15 случайных бит. Это <strong>BCH-код (15, 5)</strong> над <code>GF(2)</code>: 5 содержательных бит (2 на EC level + 3 на номер маски) защищаются 10 битами чётности. Такой код <strong>исправляет до 3 ошибочных бит</strong> внутри своих 15 — то есть даже если три клетки в зоне format info замазаны или прокрашены неправильно, декодер всё равно вытянет верный уровень коррекции и маску.</p>
<p>Кодирование считается через <strong>порождающий полином</strong></p>
<div class="math-block" data-png="https://jtprog.ru/math/c2e75d6b87c20e3b31f173dcc8dec30bd513d8cd39cc989b53cd5081a09dc70a.png"><span class="math__tex">$$
G(x) = x^{10} &#43; x^8 &#43; x^5 &#43; x^4 &#43; x^2 &#43; x &#43; 1
$$</span></div><p>в двоичном виде — <code>10100110111</code>. Алгоритм такой:</p>
<ol>
<li>Берём 5 бит данных <code>d</code> = <code>[EC level (2 бита)] [mask (3 бита)]</code>.</li>
<li>Сдвигаем влево на 10 позиций: получаем <code>d · x^{10}</code>.</li>
<li>Делим в <code>GF(2)</code> (по сути — длинное XOR-вычитание) на <code>G(x)</code>, остаток — это 10 бит чётности.</li>
<li>Склеиваем: <code>[5 бит данных] [10 бит остатка]</code> = 15-битное кодовое слово.</li>
</ol>
<p>Чтобы проверить целостность при чтении, декодер делает то же самое в обратную сторону: делит прочитанные 15 бит на <code>G(x)</code>. Если остаток нулевой — ошибок не было. Если ненулевой — это <strong>синдром</strong>, по которому находятся испорченные биты. Для коротких BCH вроде нашего (всего 32 валидных слова, по числу комбинаций <code>EC × mask</code>) обычно держат готовую таблицу синдромов — это быстрее, чем гонять полный алгоритм исправления каждый раз.</p>
<p>Теперь становится понятно, <strong>зачем нужна XOR-маска</strong> <code>101010000010010</code>. Без неё формат из одних нулей был бы валидным кодовым словом («EC level M, маска 0, нулевой остаток») — а это совпадает с пустой или замазанной белой областью QR. После XOR-а сплошные нули или единицы гарантированно дают невалидный синдром, и декодер не примет шум за легитимный формат.</p>
<p>И ещё деталь: те же 15 бит <strong>продублированы дважды</strong>, причём копии разнесены по разным углам — буква Г вокруг левого верхнего finder-а и полоска снизу от правого верхнего + справа от левого нижнего. Это страховка от случая, когда одна копия повреждена сильнее, чем BCH способен починить (больше 3 ошибок): декодер читает обе, считает синдром у каждой и берёт ту, что чище.</p>
<hr>
<h2 id="шаг-3-зигзаг-и-снятие-маски">Шаг 3. Зигзаг и снятие маски</h2>
<p>Биты данных читаются <strong>парами столбцов справа налево, зигзагом</strong>: пара (20, 19) — снизу вверх, пара (18, 17) — сверху вниз, и так далее, перепрыгивая тайминг-колонку №6. Внутри каждого ряда читается сначала правая клетка, потом левая. Все функциональные модули (finder, тайминг, format, dark) — пропускаются.</p>
<figure class="post-figure">
    <img loading="lazy" decoding="async"
         src="/zigzag.png?v=1791919947"
         alt="Путь обхода данных: первые 32 бита подсвечены цветовой шкалой от красного к зелёному, серым — функциональные модули"></figure><p>Видно, как поток идёт парами столбцов: красный (биты 1–2) внизу справа, поднимается вверх по правым двум колонкам до битов 15–16, потом перепрыгивает в следующую пару (зелёный, биты 17–32) и идёт <strong>вниз</strong>.</p>
<p>Каждый бит, прочитанный из решётки, <strong>XOR-им с маской 6</strong>: если для координаты <code>(r, c)</code> маска срабатывает — бит инвертируется.</p>
<p>После маскинга первые 4 байта получаются такими:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>байт 1: 00100000  = 0x20
байт 2: 00010011  = 0x13
байт 3: 00001111  = 0x0F
байт 4: 00000000  = 0x00</code></pre>
</div>
<h3 id="восемь-масок-и-их-формулы">Восемь масок и их формулы</h3>
<p>Полный набор из стандарта — клетка <code>(r, c)</code> (строка, колонка) инвертируется, если выражение даёт <code>true</code>:</p>
<table>
	<thead>
			<tr>
					<th>№</th>
					<th>формула</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>0</td>
					<td><code>(r + c) mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>1</td>
					<td><code>r mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>2</td>
					<td><code>c mod 3 == 0</code></td>
			</tr>
			<tr>
					<td>3</td>
					<td><code>(r + c) mod 3 == 0</code></td>
			</tr>
			<tr>
					<td>4</td>
					<td><code>(⌊r/2⌋ + ⌊c/3⌋) mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>5</td>
					<td><code>(r · c) mod 2 + (r · c) mod 3 == 0</code></td>
			</tr>
			<tr>
					<td>6</td>
					<td><code>((r · c) mod 2 + (r · c) mod 3) mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>7</td>
					<td><code>((r + c) mod 2 + (r · c) mod 3) mod 2 == 0</code></td>
			</tr>
	</tbody>
</table>
<p>Маски устроены по-разному осмысленно: №0–№3 — простые «шахматные» / «полосатые» шаблоны, №4–№7 — более «шумные», с произведениями координат. Чем разнообразнее набор, тем больше шанс, что хотя бы одна из восьми разломает любой неудачный битовый рисунок в payload.</p>
<p>А выбирает кодировщик так: накладывает поочерёдно каждую из восьми масок на черновой код и считает <strong>штрафную функцию</strong> (penalty score). Штрафы накапливаются за длинные одноцветные полосы (5+ модулей подряд), за блоки 2×2 одного цвета, за паттерны, похожие на finder (<code>1:1:3:1:1</code>), и за дисбаланс чёрного и белого. Маска с минимальной суммой штрафов уходит в финальный QR. Поэтому маска №6 в нашем подопытном — не «любимая маска автора», а просто та, что для конкретного payload <code>&quot;HI&quot;</code> сложилась с минимальным penalty.</p>
<hr>
<h2 id="шаг-4-парсим-payload">Шаг 4. Парсим payload</h2>
<p>Первые 32 бита склеиваем в один поток и режем по логике формата:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>0010 | 000000010 | 01100001111 | 0000
 ↑         ↑            ↑         ↑
 │         │            │         └─ терминатор «конец данных»
 │         │            └─ символы (11 бит на пару)
 │         └─ счётчик: 2 символа (9 бит для V1 alpha)
 └─ режим: 0010 = alphanumeric</code></pre>
</div>
<p>Каждый payload в QR начинается одинаково: <strong>Mode Indicator</strong> (4 бита) + <strong>Character Count Indicator</strong> (CCI, ширина зависит от пары version+mode) + сами данные. Ширина CCI меняется ступеньками: для alphanumeric это 9 бит на V1–V9, 11 бит на V10–V26 и 13 бит на V27–V40. Сделано осмысленно: в маленьком V1 пол-байта не хочется тратить на нули перед длиной, а в большом V40 9 бит уже банально не хватит — туда влезает до 4296 alphanumeric-символов. Поэтому при ручном декодировании всегда первым делом смотришь на версию (размер сетки), и только потом — где у CCI заканчиваются биты.</p>
<h3 id="декодируем-сами-символы">Декодируем сами символы</h3>
<p>Декодируем 11 бит данных: <code>01100001111</code> в десятичном = <strong>783</strong>.</p>
<p>В alphanumeric-режиме два символа упаковываются как <code>c1 × 45 + c2</code>. Делим: <code>783 = 17 × 45 + 18</code>. Таблица alphanumeric-алфавита: <code>0-9, A-Z, [space] $ % * + - . / :</code> — итого 45 значений, отсюда и множитель.</p>
<ul>
<li>17 → <strong>H</strong></li>
<li>18 → <strong>I</strong></li>
</ul>
<p>Получили <code>&quot;HI&quot;</code>.</p>
<p>Если бы строка была нечётной длины, последний символ кодировался бы отдельно — не 11, а 6 бит. Это полезно держать в голове: после CCI всегда сначала идут полные пары, а потом — возможно — «хвост» в 6 бит. Аналогично numeric-режим режется тройками по 10 бит, а одиночный/парный хвост — 4 и 7 бит соответственно. Стандарт педантично оптимизирует каждый бит: считает, что место в QR — это дорого.</p>
<h3 id="терминатор-padding-и-reed-solomon">Терминатор, padding и Reed-Solomon</h3>
<p>Сразу за данными идёт терминатор <code>0000</code> — четыре нуля, говорящие «всё, payload закончился». По стандарту терминатор можно укоротить или вовсе выкинуть, если данные ровно дотягиваются до конца блока — но обычно его пишут целиком, и декодер на него ориентируется.</p>
<p>Дальше биты добиваются нулями до ближайшей границы байта, и потом блок данных заполняется до своей ёмкости чередующимися <strong>padding-байтами</strong> <code>0xEC</code> (<code>11101100</code>) и <code>0x11</code> (<code>00010001</code>). Эти два конкретных значения зашиты в стандарте — выбраны они так, чтобы их битовый рисунок под любой из восьми масок ломал длинные одноцветные полосы и не складывался во что-то, похожее на finder-pattern (1:1:3:1:1). Поэтому если вытащил из кода строку «HI», а дальше пошло <code>EC 11 EC 11 EC 11 ...</code> — это не повреждение, это норма. И наоборот: если padding выглядит как-то иначе — где-то в зигзаге ошибся.</p>
<p>Для V1 с уровнем коррекции L общий размер блока — <strong>26 codeword&rsquo;ов</strong> (байт), из которых под данные отведено 19, а остальные 7 — это <strong>байты Reed-Solomon</strong>: код над полем GF(256), способный поднять до 3 искажённых байт на блок. Для нашего <code>&quot;HI&quot;</code> это значит: 4 уже прочитанных байта (<code>20 13 0F 00</code>) + 15 байт padding&rsquo;а (<code>EC 11 EC 11 ... EC</code>) + 7 байт RS = ровно 26, полная ёмкость V1-L.</p>
<p>Раскручивать Reed-Solomon руками — занятие на отдельный пост: нужно считать синдромы, через алгоритм Берлекэмпа-Месси искать полином-локатор ошибок, потом Chien-search&rsquo;ем — позиции, потом формулой Форни — значения, и всё это в арифметике GF(256). В декодере без ошибок RS-байты можно просто игнорировать — и в этом разборе мы именно так и поступим. Поднимется тема — вернёмся.</p>
<h3 id="режимы-данных">Режимы данных</h3>
<table>
	<thead>
			<tr>
					<th>код</th>
					<th>режим</th>
					<th>бит на символ</th>
					<th>счётчик в V1</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>0001</code></td>
					<td>numeric</td>
					<td>3.33 (10 бит на 3 цифры)</td>
					<td>10 бит</td>
			</tr>
			<tr>
					<td><code>0010</code></td>
					<td>alphanumeric</td>
					<td>5.5 (11 бит на 2 символа)</td>
					<td>9 бит</td>
			</tr>
			<tr>
					<td><code>0100</code></td>
					<td>byte</td>
					<td>8</td>
					<td>8 бит</td>
			</tr>
			<tr>
					<td><code>1000</code></td>
					<td>kanji</td>
					<td>13</td>
					<td>8 бит</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<p>Когда сидишь с распечатанным QR и разбираешь его руками, есть пара мест, где мозг гарантированно даст осечку. Перечислю те, что встречал чаще всего — и в собственных первых попытках, и в чужих разборах в чате.</p>
<ul>
<li><strong>Забыл пропустить тайминг-колонку №6 в зигзаге.</strong> Самый популярный баг: пары столбцов должны «перепрыгивать» эту колонку, а не читать её клетки как данные. Если payload пошёл бредом с первого же байта — почти наверняка дело именно тут.</li>
<li><strong>Снял маску не только с данных.</strong> Маска накладывается <strong>исключительно</strong> на data + EC, то есть на зелёную зону. Finder, separators, тайминг, format info, dark module — не трогаем. Если случайно проинвертировал маской finder-патерны, дальше развалится всё, включая саму ориентацию.</li>
<li><strong>Перепутал порядок битов внутри пары столбцов.</strong> В зигзаге в каждой строке сначала правая клетка, потом левая. Прочитал наоборот — payload идёт зеркально по 2 бита, терминатор находится где-то в середине, и парсинг падает.</li>
<li><strong>Забыл, что зигзаг bounce-ит.</strong> Пары столбцов чередуют направление: (20, 19) — снизу вверх, (18, 17) — сверху вниз, (16, 15) — снова вверх. Это не «всегда снизу вверх», это «змейка». На стыках пар легко проскочить инерцией.</li>
<li><strong>Принял <code>EC 11 EC 11 ...</code> за продолжение данных.</strong> После терминатора <code>0000</code> идёт padding до конца блока данных. Если в декодированном байт-потоке после реального payload пошли повторяющиеся <code>EC 11</code> — не пытайся их парсить как «следующее сообщение», это просто забивание блока до 19 байт (для V1-L).</li>
</ul>
<hr>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<p>Чтобы из теории получилось ощущение «руками потрогал», самый рабочий заход — такой:</p>
<ol>
<li><strong>Сгенерируй V1 QR с заранее известным содержимым.</strong> Удобнее всего — через <a href="https://www.nayuki.io/page/qr-code-generator-library" rel="noopener noreferrer" target="_blank">онлайн-демо Nayuki</a> или через мой <a href="https://github.com/jtprogru/wifi-qr" rel="noopener noreferrer" target="_blank">wifi-qr</a> (для коротких SSID он как раз выдаёт V1–V2). Возьми короткую alphanumeric-строку: <code>&quot;HI&quot;</code>, <code>&quot;OK&quot;</code>, <code>&quot;TEST&quot;</code>. Не забудь зафиксировать <strong>уровень коррекции</strong> (L) и <strong>номер маски</strong> — потом будет с чем сверять.</li>
<li><strong>Распечатай крупно</strong> — A4 на 21×21 хватает с запасом. Альтернатива — нарисовать сетку на бумаге в клетку и закрасить модули вручную. Звучит занудно, но именно ручная отрисовка лучше всего ломает «магический квадратик» в голове.</li>
<li><strong>Подсвети функциональные зоны</strong> карандашом или маркерами разных цветов: три finder-а, separators, обе тайминг-полоски, dark module, обе копии format info. После этого зелёная зона данных выделится сама собой.</li>
<li><strong>Прочитай format info</strong> → XOR с <code>101010000010010</code> → выпиши уровень коррекции и номер маски. Сверь с тем, что задал в генераторе. Сошлось — значит, Шаг 2 ты понял правильно.</li>
<li><strong>Обведи клетки, инвертируемые выбранной маской.</strong> Берёшь формулу из таблицы выше и проходишь по всей data-зоне: где условие — <code>true</code>, ставишь крестик. Эти клетки будешь инвертировать при чтении.</li>
<li><strong>Пройди зигзагом первые 32 бита</strong>, для каждого инвертируй или нет в зависимости от маски, и распарси: 4 бита mode → CCI → данные. Должна получиться твоя исходная строка.</li>
</ol>
<p>Когда «свой» QR разобрался — попробуй на чужом: возьми QR с любого чека из магазина или с Wi-Fi-стикера в кофейне. Только сразу проверь, что это <strong>V1 или V2</strong> (для V1 сетка 21×21, для V2 — 25×25, считается по краю). На больших версиях ручной зигзаг быстро превращается в подвиг ради подвига.</p>
<hr>
<h2 id="итог">Итог</h2>
<p>Если потренироваться, то для маленьких QR (V1, alphanumeric, короткий текст) реально читать глазами за пару минут. Узкое горлышко — держать в голове маску и зигзаг, две операции, которые мозг плохо параллелит. Помогает распечатать QR на бумаге и фломастером заштриховывать клетки, которые маска инвертирует — после этого данные читаются «как обычный бинарный поток».</p>
<h3 id="алгоритм-по-шагам">Алгоритм по шагам</h3>
<ol>
<li>Найти три finder patterns → определить ориентацию.</li>
<li>Прочитать format info → XOR с <code>101010000010010</code> → выделить EC level и mask number.</li>
<li>Применить маску к данным (XOR по формуле выбранной маски).</li>
<li>Прочитать биты зигзагом снизу-справа вверх, парами столбцов.</li>
<li>Распарсить: 4 бита режим → N бит счётчик → данные → терминатор.</li>
<li>(Опционально) проверить Reed-Solomon коды.</li>
</ol>
<h3 id="дальше-копать">Дальше копать</h3>
<ul>
<li><a href="https://robertheaton.com/2018/05/03/decoding-a-qr-code-by-hand/" rel="noopener noreferrer" target="_blank">How to decode a QR code by hand — Robert Heaton</a> — пошаговый разбор того же V1 «HI», только не моими словами. Хорошо для проверки себя: если в каком-то месте разошлись — значит, не до конца понял шаг.</li>
<li><a href="https://www.thonky.com/qr-code-tutorial/" rel="noopener noreferrer" target="_blank">Thonky&rsquo;s QR Code Tutorial</a> — самый полный учебник в сети: все 40 версий, восемь масок с примерами, готовые таблицы для Reed-Solomon, разбор форматов. Если Heaton — обзорная статья, Thonky тянет уже на 30-страничный курс.</li>
<li><a href="https://github.com/nayuki/QR-Code-generator" rel="noopener noreferrer" target="_blank">nayuki/QR-Code-generator</a> — каноническая референсная реализация на C/C++/Java/JS/Python/Rust. Читается как продолжение этого поста: чёткие имена, в комментариях — формулы из стандарта, никакой магии. У того же автора на <code>nayuki.io</code> лежит онлайн-демо с настраиваемой версией и маской — удобно сверять свои выкладки с эталоном.</li>
<li><a href="https://github.com/jtprogru/wifi-qr" rel="noopener noreferrer" target="_blank">jtprogru/wifi-qr</a> — мой собственный мини-проектик «с обратной стороны»: тащит <code>WIFI_SSID</code> и <code>WIFI_PASSWD</code> из <code>.env</code> и собирает Wi-Fi-QR в PNG одной командой <code>make run</code>. Внутри — крошечный Python-скрипт поверх библиотеки <code>qrcode</code>. Удобный мостик от «понимаю по битам» к «генерирую за секунду», заодно — готовый QR для гостевой Wi-Fi.</li>
<li><a href="https://github.com/zxing/zxing" rel="noopener noreferrer" target="_blank">zxing/zxing</a> — production-grade декодер от Google, тот самый, который много лет крутился в Barcode Scanner на Android. Полезно посмотреть, чтобы понять разницу между «вытащить V1 в идеальных условиях» и «прочитать URL с мятого чека под углом 30° при тусклом свете».</li>
<li><a href="https://en.wikiversity.org/wiki/Reed-Solomon_codes_for_coders" rel="noopener noreferrer" target="_blank">Reed–Solomon codes for coders</a> на Wikiversity — лучший learn-by-doing разбор RS-кодов из всех, что попадались. GF(256), порождающие полиномы, синдромы, исправление ошибок — всё последовательно разложено по шагам, с примерами на Python. Без этой статьи в RS-кодах было сложно ухватиться хоть за что-то.</li>
<li><a href="https://en.wikipedia.org/wiki/QR_code" rel="noopener noreferrer" target="_blank">QR code — Wikipedia</a> — на удивление приличная обзорная статья: версии, режимы, история Denso Wave, лимиты по объёму данных, эволюция стандарта. Хорошо стартовать с неё, если QR в голове пока «магический квадратик».</li>
<li><strong>ISO/IEC 18004:2015</strong> — сам стандарт. Платный, но если возишься со штрихкодами всерьёз — без него никак: только там нормально расписаны все 40 версий, все 4 режима кодирования и точные алгоритмы коррекции.</li>
</ul>
<p>Если зашло само упражнение «разобрать бинарную структуру руками», у меня есть ещё два таких: <a href="/posts/what-is-mbr/">как выглядит MBR</a> и <a href="/posts/what-is-gpt/">как выглядит GPT</a> — там тоже <code>dd</code>, <code>hexdump</code> и байты по полочкам.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! QR-коды в 2026 году встречаются буквально везде: меню в кафе, оплата в магазине, ссылка на Wi-Fi в аэропорту, чек из терминала. Камера телефона декодирует такой код за десятки миллисекунд, и большинство людей воспринимают QR как магический чёрно-белый прямоугольник, который «как-то превращается в URL». А внутри — обычный документированный стандарт ISO/IEC 18004, и при желании его реально разобрать вручную: с карандашом, бумагой и базовой арифметикой.</p>
<p>В этом разборе пройдём по шагам, как декодировать QR-код глазами: что такое finder patterns и тайминг-полоски, где зашиты маска и уровень коррекции ошибок, как читать данные зигзагом снизу-справа вверх и собирать из битов исходную строку. В качестве подопытного — самый маленький Version 1 (21×21) с содержимым <code>&quot;HI&quot;</code>, чтобы маска и зигзаг укладывались в голове, а не в Excel.</p>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Стандарт — <strong>ISO/IEC 18004</strong>, всего 40 версий: V1 — 21×21 модуль, дальше шаг +4 модуля на сторону до V40 (177×177).</li>
<li>Три больших квадрата по углам — <strong>finder patterns</strong>, задают ориентацию. Четвёртого (в правом нижнем углу) нет — это и есть «низ-право».</li>
<li>15 бит <strong>format info</strong> вокруг finder-ов хранят уровень коррекции (L/M/Q/H) и номер маски (0–7); защищены своим BCH-кодом и продублированы дважды.</li>
<li>Маска нужна, чтобы зона данных не складывалась в длинные одноцветные полосы и не имитировала случайно finder-паттерн — сканер бы поплыл. После XOR с правильной маской биты читаются «честно».</li>
<li>Данные читаются <strong>зигзагом снизу-справа вверх</strong>, парами столбцов, перепрыгивая тайминг-колонку №6, по 8 бит на байт.</li>
<li>Первые 4 бита — <strong>Mode Indicator</strong> (numeric / alphanumeric / byte / kanji), дальше <strong>Character Count Indicator</strong> переменной длины, потом сами данные, терминатор <code>0000</code> и padding <code>EC 11 EC 11 ...</code>.</li>
<li>За payload идут <strong>байты Reed-Solomon</strong> для коррекции ошибок: код над полем GF(256), число codeword&rsquo;ов зависит от версии и уровня (для V1-L — 7, для V1-H — уже 17). В этом разборе их сознательно пропускаем — без таблиц и GF-арифметики на бумаге не возьмёшь.</li>
</ul>
<p>После пары часов практики можно вытащить из небольшого кода (V1, 21×21) короткую строку вроде URL без сканера. До «прочитал визитку взглядом за секунду» — уже из области цирковых номеров: Reed-Solomon без вычислений в уме не выкрутишь, а на V10+ зигзаг становится ощутимо больно держать в голове.</p>
<hr>
<h2 id="подопытный">Подопытный</h2>
<p>Самый маленький размер — <strong>Version 1 (21×21 модуль)</strong>. Содержимое: <code>&quot;HI&quot;</code>. Раскрашены функциональные зоны.</p>
<figure class="post-figure">
    <img loading="lazy" decoding="async"
         src="/qr_labeled.png?v=1791919947"
         alt="Схема Version 1 QR-кода 21×21 с раскрашенными зонами: finder patterns, тайминг, format info, dark module, данные"></figure><hr>
<h2 id="шаг-1-ориентируемся">Шаг 1. Ориентируемся</h2>
<p>Три синих квадрата 7×7 — <strong>finder patterns</strong>. Они нужны сканеру, чтобы понять, где верх и поворот кода. Правило простое: три есть, а четвёртого (в правом нижнем углу) нет — туда смотрит «низ-право».</p>
<p>Вокруг каждого finder-а идёт <strong>separator</strong> — белая рамка в один модуль шириной (на схеме это пустая полоса между синим квадратом и всем остальным). Назначение прямолинейное: изолировать finder от данных, чтобы сканер не пытался разобрать «полуfinder-полуданные» на стыке и не плыл по координатам.</p>
<p>Между ними тянутся <strong>красные тайминг-полоски</strong> (строка 6 и столбец 6) — чередование чёрный-белый-чёрный. Они задают «линейку», по которой считаются координаты модулей. Если что-то поплыло — выравниваешься по ним.</p>
<p>Фиолетовый одиночный модуль — <strong>dark module</strong>, всегда чёрный по стандарту. Просто маркер, не несёт смысла.</p>
<p>Оранжевая зона — <strong>format info</strong>. 15 бит, продублированные дважды (для надёжности): одна копия идёт буквой Г вокруг левого верхнего finder-а, вторая — полоской справа от правого верхнего и снизу от левого нижнего. В ней зашифрованы две критичные вещи: <strong>уровень коррекции ошибок</strong> (L/M/Q/H) и <strong>номер маски</strong> (0–7).</p>
<p>Всё зелёное — это собственно данные плюс байты Рида-Соломона.</p>
<h3 id="чего-в-v1-ещё-нет-но-в-больших-версиях-встретишь">Чего в V1 ещё нет, но в больших версиях встретишь</h3>
<p>В нашей маленькой сетке всё описанное выше — это полный набор функциональных зон. Но как только переходишь к версиям побольше, появляются ещё две вещи:</p>
<ul>
<li><strong>Alignment patterns</strong> — маленькие квадраты 5×5 с чёрной точкой в центре, разбросанные по полю. Появляются с V2 (один штук, в правом нижнем углу), к V7 их уже шесть, а у V40 — целых 46. Нужны для <strong>локальной</strong> компенсации искажений сетки: V1 ещё читается «по линейке тайминга», а у большого QR на смятой или сфотографированной под углом картинке координаты модулей плывут, и без локальных якорей сканер промахивается.</li>
<li><strong>Version info</strong> — два блока 6×3 рядом с правым верхним и левым нижним finder-ами, появляются с V7. Внутри 18 бит: 6 бит самого номера версии и 12 бит BCH-чётности. Подход тот же, что у format info, только код покрупнее — BCH(18, 6), тоже исправляет до 3 ошибок. Декодеру эта зона нужна, чтобы достоверно определить размер кода, не доверяя «посчитаю клетки по краю» (на размытом снимке такой подсчёт ломается легко).</li>
</ul>
<p>В V1 этого ничего нет, поэтому в нашем разборе обходимся без них — но если ты потом возьмёшь V3 с того же чека, держи их в голове: чёрная точка в правом нижнем — не часть данных, а alignment.</p>
<hr>
<h2 id="шаг-2-снимаем-format-info">Шаг 2. Снимаем format info</h2>
<p>Биты идут: вдоль строки 8 слева направо (с пропуском тайминг-колонки), а потом по столбцу 8 снизу вверх (тоже с пропуском тайминг-строки). Старший бит = #14, младший = #0.</p>
<figure class="post-figure">
    <img loading="lazy" decoding="async"
         src="/format_info.png?v=1791919947"
         alt="Фокус на левый верхний угол: 15 клеток format info с номерами #14…#0 и их значениями"></figure><p>Считываем по порядку от #14 до #0:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>сырые биты : 1 1 0 1 1 0 0 0 1 0 0 0 0 0 1
XOR-маска  : 1 0 1 0 1 0 0 0 0 0 1 0 0 1 0
─────────────────────────────────────────
результат  : 0 1 1 1 0 0 0 0 1 0 1 0 0 1 1</code></pre>
</div>
<p>Маска <code>101010000010010</code> зашита в стандарте — иначе формат, состоящий из нулей, тоже был бы валидным, и любая дырка на пустом месте всё ломала бы.</p>
<p>Разбираем результат <code>011100001010011</code>:</p>
<ul>
<li><strong>биты 14–13</strong> = <code>01</code> → уровень коррекции <strong>L</strong> (~7 % избыточности)</li>
<li><strong>биты 12–10</strong> = <code>110</code> → <strong>маска №6</strong></li>
<li>остальные 10 бит — BCH-код для коррекции ошибок в самих этих 15 битах (нам не нужны).</li>
</ul>
<p>Теперь самое важное: мы знаем маску. Значит, можно <strong>снять её</strong> с зелёной зоны данных и прочитать настоящие биты.</p>
<p>Маска №6 задаётся формулой: клетка <code>(r, c)</code> инвертируется, если</p>
<div class="math-block" data-png="https://jtprog.ru/math/9c2a91624158b6224c0c48837ec67e61c2ae9c08cba29a81054acacb4a0629bf.png"><span class="math__tex">$$
((r \cdot c) \bmod 2 &#43; (r \cdot c) \bmod 3) \bmod 2 == 0
$$</span></div><h3 id="кодирование-уровня-коррекции">Кодирование уровня коррекции</h3>
<table>
	<thead>
			<tr>
					<th>биты</th>
					<th>уровень</th>
					<th>избыточность</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>01</code></td>
					<td>L</td>
					<td>~7 %</td>
			</tr>
			<tr>
					<td><code>00</code></td>
					<td>M</td>
					<td>~15 %</td>
			</tr>
			<tr>
					<td><code>11</code></td>
					<td>Q</td>
					<td>~25 %</td>
			</tr>
			<tr>
					<td><code>10</code></td>
					<td>H</td>
					<td>~30 %</td>
			</tr>
	</tbody>
</table>
<h3 id="bch15-5-под-капотом">BCH(15, 5) под капотом</h3>
<p>Format info — это не просто 15 случайных бит. Это <strong>BCH-код (15, 5)</strong> над <code>GF(2)</code>: 5 содержательных бит (2 на EC level + 3 на номер маски) защищаются 10 битами чётности. Такой код <strong>исправляет до 3 ошибочных бит</strong> внутри своих 15 — то есть даже если три клетки в зоне format info замазаны или прокрашены неправильно, декодер всё равно вытянет верный уровень коррекции и маску.</p>
<p>Кодирование считается через <strong>порождающий полином</strong></p>
<div class="math-block" data-png="https://jtprog.ru/math/c2e75d6b87c20e3b31f173dcc8dec30bd513d8cd39cc989b53cd5081a09dc70a.png"><span class="math__tex">$$
G(x) = x^{10} &#43; x^8 &#43; x^5 &#43; x^4 &#43; x^2 &#43; x &#43; 1
$$</span></div><p>в двоичном виде — <code>10100110111</code>. Алгоритм такой:</p>
<ol>
<li>Берём 5 бит данных <code>d</code> = <code>[EC level (2 бита)] [mask (3 бита)]</code>.</li>
<li>Сдвигаем влево на 10 позиций: получаем <code>d · x^{10}</code>.</li>
<li>Делим в <code>GF(2)</code> (по сути — длинное XOR-вычитание) на <code>G(x)</code>, остаток — это 10 бит чётности.</li>
<li>Склеиваем: <code>[5 бит данных] [10 бит остатка]</code> = 15-битное кодовое слово.</li>
</ol>
<p>Чтобы проверить целостность при чтении, декодер делает то же самое в обратную сторону: делит прочитанные 15 бит на <code>G(x)</code>. Если остаток нулевой — ошибок не было. Если ненулевой — это <strong>синдром</strong>, по которому находятся испорченные биты. Для коротких BCH вроде нашего (всего 32 валидных слова, по числу комбинаций <code>EC × mask</code>) обычно держат готовую таблицу синдромов — это быстрее, чем гонять полный алгоритм исправления каждый раз.</p>
<p>Теперь становится понятно, <strong>зачем нужна XOR-маска</strong> <code>101010000010010</code>. Без неё формат из одних нулей был бы валидным кодовым словом («EC level M, маска 0, нулевой остаток») — а это совпадает с пустой или замазанной белой областью QR. После XOR-а сплошные нули или единицы гарантированно дают невалидный синдром, и декодер не примет шум за легитимный формат.</p>
<p>И ещё деталь: те же 15 бит <strong>продублированы дважды</strong>, причём копии разнесены по разным углам — буква Г вокруг левого верхнего finder-а и полоска снизу от правого верхнего + справа от левого нижнего. Это страховка от случая, когда одна копия повреждена сильнее, чем BCH способен починить (больше 3 ошибок): декодер читает обе, считает синдром у каждой и берёт ту, что чище.</p>
<hr>
<h2 id="шаг-3-зигзаг-и-снятие-маски">Шаг 3. Зигзаг и снятие маски</h2>
<p>Биты данных читаются <strong>парами столбцов справа налево, зигзагом</strong>: пара (20, 19) — снизу вверх, пара (18, 17) — сверху вниз, и так далее, перепрыгивая тайминг-колонку №6. Внутри каждого ряда читается сначала правая клетка, потом левая. Все функциональные модули (finder, тайминг, format, dark) — пропускаются.</p>
<figure class="post-figure">
    <img loading="lazy" decoding="async"
         src="/zigzag.png?v=1791919947"
         alt="Путь обхода данных: первые 32 бита подсвечены цветовой шкалой от красного к зелёному, серым — функциональные модули"></figure><p>Видно, как поток идёт парами столбцов: красный (биты 1–2) внизу справа, поднимается вверх по правым двум колонкам до битов 15–16, потом перепрыгивает в следующую пару (зелёный, биты 17–32) и идёт <strong>вниз</strong>.</p>
<p>Каждый бит, прочитанный из решётки, <strong>XOR-им с маской 6</strong>: если для координаты <code>(r, c)</code> маска срабатывает — бит инвертируется.</p>
<p>После маскинга первые 4 байта получаются такими:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>байт 1: 00100000  = 0x20
байт 2: 00010011  = 0x13
байт 3: 00001111  = 0x0F
байт 4: 00000000  = 0x00</code></pre>
</div>
<h3 id="восемь-масок-и-их-формулы">Восемь масок и их формулы</h3>
<p>Полный набор из стандарта — клетка <code>(r, c)</code> (строка, колонка) инвертируется, если выражение даёт <code>true</code>:</p>
<table>
	<thead>
			<tr>
					<th>№</th>
					<th>формула</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>0</td>
					<td><code>(r + c) mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>1</td>
					<td><code>r mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>2</td>
					<td><code>c mod 3 == 0</code></td>
			</tr>
			<tr>
					<td>3</td>
					<td><code>(r + c) mod 3 == 0</code></td>
			</tr>
			<tr>
					<td>4</td>
					<td><code>(⌊r/2⌋ + ⌊c/3⌋) mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>5</td>
					<td><code>(r · c) mod 2 + (r · c) mod 3 == 0</code></td>
			</tr>
			<tr>
					<td>6</td>
					<td><code>((r · c) mod 2 + (r · c) mod 3) mod 2 == 0</code></td>
			</tr>
			<tr>
					<td>7</td>
					<td><code>((r + c) mod 2 + (r · c) mod 3) mod 2 == 0</code></td>
			</tr>
	</tbody>
</table>
<p>Маски устроены по-разному осмысленно: №0–№3 — простые «шахматные» / «полосатые» шаблоны, №4–№7 — более «шумные», с произведениями координат. Чем разнообразнее набор, тем больше шанс, что хотя бы одна из восьми разломает любой неудачный битовый рисунок в payload.</p>
<p>А выбирает кодировщик так: накладывает поочерёдно каждую из восьми масок на черновой код и считает <strong>штрафную функцию</strong> (penalty score). Штрафы накапливаются за длинные одноцветные полосы (5+ модулей подряд), за блоки 2×2 одного цвета, за паттерны, похожие на finder (<code>1:1:3:1:1</code>), и за дисбаланс чёрного и белого. Маска с минимальной суммой штрафов уходит в финальный QR. Поэтому маска №6 в нашем подопытном — не «любимая маска автора», а просто та, что для конкретного payload <code>&quot;HI&quot;</code> сложилась с минимальным penalty.</p>
<hr>
<h2 id="шаг-4-парсим-payload">Шаг 4. Парсим payload</h2>
<p>Первые 32 бита склеиваем в один поток и режем по логике формата:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>0010 | 000000010 | 01100001111 | 0000
 ↑         ↑            ↑         ↑
 │         │            │         └─ терминатор «конец данных»
 │         │            └─ символы (11 бит на пару)
 │         └─ счётчик: 2 символа (9 бит для V1 alpha)
 └─ режим: 0010 = alphanumeric</code></pre>
</div>
<p>Каждый payload в QR начинается одинаково: <strong>Mode Indicator</strong> (4 бита) + <strong>Character Count Indicator</strong> (CCI, ширина зависит от пары version+mode) + сами данные. Ширина CCI меняется ступеньками: для alphanumeric это 9 бит на V1–V9, 11 бит на V10–V26 и 13 бит на V27–V40. Сделано осмысленно: в маленьком V1 пол-байта не хочется тратить на нули перед длиной, а в большом V40 9 бит уже банально не хватит — туда влезает до 4296 alphanumeric-символов. Поэтому при ручном декодировании всегда первым делом смотришь на версию (размер сетки), и только потом — где у CCI заканчиваются биты.</p>
<h3 id="декодируем-сами-символы">Декодируем сами символы</h3>
<p>Декодируем 11 бит данных: <code>01100001111</code> в десятичном = <strong>783</strong>.</p>
<p>В alphanumeric-режиме два символа упаковываются как <code>c1 × 45 + c2</code>. Делим: <code>783 = 17 × 45 + 18</code>. Таблица alphanumeric-алфавита: <code>0-9, A-Z, [space] $ % * + - . / :</code> — итого 45 значений, отсюда и множитель.</p>
<ul>
<li>17 → <strong>H</strong></li>
<li>18 → <strong>I</strong></li>
</ul>
<p>Получили <code>&quot;HI&quot;</code>.</p>
<p>Если бы строка была нечётной длины, последний символ кодировался бы отдельно — не 11, а 6 бит. Это полезно держать в голове: после CCI всегда сначала идут полные пары, а потом — возможно — «хвост» в 6 бит. Аналогично numeric-режим режется тройками по 10 бит, а одиночный/парный хвост — 4 и 7 бит соответственно. Стандарт педантично оптимизирует каждый бит: считает, что место в QR — это дорого.</p>
<h3 id="терминатор-padding-и-reed-solomon">Терминатор, padding и Reed-Solomon</h3>
<p>Сразу за данными идёт терминатор <code>0000</code> — четыре нуля, говорящие «всё, payload закончился». По стандарту терминатор можно укоротить или вовсе выкинуть, если данные ровно дотягиваются до конца блока — но обычно его пишут целиком, и декодер на него ориентируется.</p>
<p>Дальше биты добиваются нулями до ближайшей границы байта, и потом блок данных заполняется до своей ёмкости чередующимися <strong>padding-байтами</strong> <code>0xEC</code> (<code>11101100</code>) и <code>0x11</code> (<code>00010001</code>). Эти два конкретных значения зашиты в стандарте — выбраны они так, чтобы их битовый рисунок под любой из восьми масок ломал длинные одноцветные полосы и не складывался во что-то, похожее на finder-pattern (1:1:3:1:1). Поэтому если вытащил из кода строку «HI», а дальше пошло <code>EC 11 EC 11 EC 11 ...</code> — это не повреждение, это норма. И наоборот: если padding выглядит как-то иначе — где-то в зигзаге ошибся.</p>
<p>Для V1 с уровнем коррекции L общий размер блока — <strong>26 codeword&rsquo;ов</strong> (байт), из которых под данные отведено 19, а остальные 7 — это <strong>байты Reed-Solomon</strong>: код над полем GF(256), способный поднять до 3 искажённых байт на блок. Для нашего <code>&quot;HI&quot;</code> это значит: 4 уже прочитанных байта (<code>20 13 0F 00</code>) + 15 байт padding&rsquo;а (<code>EC 11 EC 11 ... EC</code>) + 7 байт RS = ровно 26, полная ёмкость V1-L.</p>
<p>Раскручивать Reed-Solomon руками — занятие на отдельный пост: нужно считать синдромы, через алгоритм Берлекэмпа-Месси искать полином-локатор ошибок, потом Chien-search&rsquo;ем — позиции, потом формулой Форни — значения, и всё это в арифметике GF(256). В декодере без ошибок RS-байты можно просто игнорировать — и в этом разборе мы именно так и поступим. Поднимется тема — вернёмся.</p>
<h3 id="режимы-данных">Режимы данных</h3>
<table>
	<thead>
			<tr>
					<th>код</th>
					<th>режим</th>
					<th>бит на символ</th>
					<th>счётчик в V1</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>0001</code></td>
					<td>numeric</td>
					<td>3.33 (10 бит на 3 цифры)</td>
					<td>10 бит</td>
			</tr>
			<tr>
					<td><code>0010</code></td>
					<td>alphanumeric</td>
					<td>5.5 (11 бит на 2 символа)</td>
					<td>9 бит</td>
			</tr>
			<tr>
					<td><code>0100</code></td>
					<td>byte</td>
					<td>8</td>
					<td>8 бит</td>
			</tr>
			<tr>
					<td><code>1000</code></td>
					<td>kanji</td>
					<td>13</td>
					<td>8 бит</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<p>Когда сидишь с распечатанным QR и разбираешь его руками, есть пара мест, где мозг гарантированно даст осечку. Перечислю те, что встречал чаще всего — и в собственных первых попытках, и в чужих разборах в чате.</p>
<ul>
<li><strong>Забыл пропустить тайминг-колонку №6 в зигзаге.</strong> Самый популярный баг: пары столбцов должны «перепрыгивать» эту колонку, а не читать её клетки как данные. Если payload пошёл бредом с первого же байта — почти наверняка дело именно тут.</li>
<li><strong>Снял маску не только с данных.</strong> Маска накладывается <strong>исключительно</strong> на data + EC, то есть на зелёную зону. Finder, separators, тайминг, format info, dark module — не трогаем. Если случайно проинвертировал маской finder-патерны, дальше развалится всё, включая саму ориентацию.</li>
<li><strong>Перепутал порядок битов внутри пары столбцов.</strong> В зигзаге в каждой строке сначала правая клетка, потом левая. Прочитал наоборот — payload идёт зеркально по 2 бита, терминатор находится где-то в середине, и парсинг падает.</li>
<li><strong>Забыл, что зигзаг bounce-ит.</strong> Пары столбцов чередуют направление: (20, 19) — снизу вверх, (18, 17) — сверху вниз, (16, 15) — снова вверх. Это не «всегда снизу вверх», это «змейка». На стыках пар легко проскочить инерцией.</li>
<li><strong>Принял <code>EC 11 EC 11 ...</code> за продолжение данных.</strong> После терминатора <code>0000</code> идёт padding до конца блока данных. Если в декодированном байт-потоке после реального payload пошли повторяющиеся <code>EC 11</code> — не пытайся их парсить как «следующее сообщение», это просто забивание блока до 19 байт (для V1-L).</li>
</ul>
<hr>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<p>Чтобы из теории получилось ощущение «руками потрогал», самый рабочий заход — такой:</p>
<ol>
<li><strong>Сгенерируй V1 QR с заранее известным содержимым.</strong> Удобнее всего — через <a href="https://www.nayuki.io/page/qr-code-generator-library" rel="noopener noreferrer" target="_blank">онлайн-демо Nayuki</a> или через мой <a href="https://github.com/jtprogru/wifi-qr" rel="noopener noreferrer" target="_blank">wifi-qr</a> (для коротких SSID он как раз выдаёт V1–V2). Возьми короткую alphanumeric-строку: <code>&quot;HI&quot;</code>, <code>&quot;OK&quot;</code>, <code>&quot;TEST&quot;</code>. Не забудь зафиксировать <strong>уровень коррекции</strong> (L) и <strong>номер маски</strong> — потом будет с чем сверять.</li>
<li><strong>Распечатай крупно</strong> — A4 на 21×21 хватает с запасом. Альтернатива — нарисовать сетку на бумаге в клетку и закрасить модули вручную. Звучит занудно, но именно ручная отрисовка лучше всего ломает «магический квадратик» в голове.</li>
<li><strong>Подсвети функциональные зоны</strong> карандашом или маркерами разных цветов: три finder-а, separators, обе тайминг-полоски, dark module, обе копии format info. После этого зелёная зона данных выделится сама собой.</li>
<li><strong>Прочитай format info</strong> → XOR с <code>101010000010010</code> → выпиши уровень коррекции и номер маски. Сверь с тем, что задал в генераторе. Сошлось — значит, Шаг 2 ты понял правильно.</li>
<li><strong>Обведи клетки, инвертируемые выбранной маской.</strong> Берёшь формулу из таблицы выше и проходишь по всей data-зоне: где условие — <code>true</code>, ставишь крестик. Эти клетки будешь инвертировать при чтении.</li>
<li><strong>Пройди зигзагом первые 32 бита</strong>, для каждого инвертируй или нет в зависимости от маски, и распарси: 4 бита mode → CCI → данные. Должна получиться твоя исходная строка.</li>
</ol>
<p>Когда «свой» QR разобрался — попробуй на чужом: возьми QR с любого чека из магазина или с Wi-Fi-стикера в кофейне. Только сразу проверь, что это <strong>V1 или V2</strong> (для V1 сетка 21×21, для V2 — 25×25, считается по краю). На больших версиях ручной зигзаг быстро превращается в подвиг ради подвига.</p>
<hr>
<h2 id="итог">Итог</h2>
<p>Если потренироваться, то для маленьких QR (V1, alphanumeric, короткий текст) реально читать глазами за пару минут. Узкое горлышко — держать в голове маску и зигзаг, две операции, которые мозг плохо параллелит. Помогает распечатать QR на бумаге и фломастером заштриховывать клетки, которые маска инвертирует — после этого данные читаются «как обычный бинарный поток».</p>
<h3 id="алгоритм-по-шагам">Алгоритм по шагам</h3>
<ol>
<li>Найти три finder patterns → определить ориентацию.</li>
<li>Прочитать format info → XOR с <code>101010000010010</code> → выделить EC level и mask number.</li>
<li>Применить маску к данным (XOR по формуле выбранной маски).</li>
<li>Прочитать биты зигзагом снизу-справа вверх, парами столбцов.</li>
<li>Распарсить: 4 бита режим → N бит счётчик → данные → терминатор.</li>
<li>(Опционально) проверить Reed-Solomon коды.</li>
</ol>
<h3 id="дальше-копать">Дальше копать</h3>
<ul>
<li><a href="https://robertheaton.com/2018/05/03/decoding-a-qr-code-by-hand/" rel="noopener noreferrer" target="_blank">How to decode a QR code by hand — Robert Heaton</a> — пошаговый разбор того же V1 «HI», только не моими словами. Хорошо для проверки себя: если в каком-то месте разошлись — значит, не до конца понял шаг.</li>
<li><a href="https://www.thonky.com/qr-code-tutorial/" rel="noopener noreferrer" target="_blank">Thonky&rsquo;s QR Code Tutorial</a> — самый полный учебник в сети: все 40 версий, восемь масок с примерами, готовые таблицы для Reed-Solomon, разбор форматов. Если Heaton — обзорная статья, Thonky тянет уже на 30-страничный курс.</li>
<li><a href="https://github.com/nayuki/QR-Code-generator" rel="noopener noreferrer" target="_blank">nayuki/QR-Code-generator</a> — каноническая референсная реализация на C/C++/Java/JS/Python/Rust. Читается как продолжение этого поста: чёткие имена, в комментариях — формулы из стандарта, никакой магии. У того же автора на <code>nayuki.io</code> лежит онлайн-демо с настраиваемой версией и маской — удобно сверять свои выкладки с эталоном.</li>
<li><a href="https://github.com/jtprogru/wifi-qr" rel="noopener noreferrer" target="_blank">jtprogru/wifi-qr</a> — мой собственный мини-проектик «с обратной стороны»: тащит <code>WIFI_SSID</code> и <code>WIFI_PASSWD</code> из <code>.env</code> и собирает Wi-Fi-QR в PNG одной командой <code>make run</code>. Внутри — крошечный Python-скрипт поверх библиотеки <code>qrcode</code>. Удобный мостик от «понимаю по битам» к «генерирую за секунду», заодно — готовый QR для гостевой Wi-Fi.</li>
<li><a href="https://github.com/zxing/zxing" rel="noopener noreferrer" target="_blank">zxing/zxing</a> — production-grade декодер от Google, тот самый, который много лет крутился в Barcode Scanner на Android. Полезно посмотреть, чтобы понять разницу между «вытащить V1 в идеальных условиях» и «прочитать URL с мятого чека под углом 30° при тусклом свете».</li>
<li><a href="https://en.wikiversity.org/wiki/Reed-Solomon_codes_for_coders" rel="noopener noreferrer" target="_blank">Reed–Solomon codes for coders</a> на Wikiversity — лучший learn-by-doing разбор RS-кодов из всех, что попадались. GF(256), порождающие полиномы, синдромы, исправление ошибок — всё последовательно разложено по шагам, с примерами на Python. Без этой статьи в RS-кодах было сложно ухватиться хоть за что-то.</li>
<li><a href="https://en.wikipedia.org/wiki/QR_code" rel="noopener noreferrer" target="_blank">QR code — Wikipedia</a> — на удивление приличная обзорная статья: версии, режимы, история Denso Wave, лимиты по объёму данных, эволюция стандарта. Хорошо стартовать с неё, если QR в голове пока «магический квадратик».</li>
<li><strong>ISO/IEC 18004:2015</strong> — сам стандарт. Платный, но если возишься со штрихкодами всерьёз — без него никак: только там нормально расписаны все 40 версий, все 4 режима кодирования и точные алгоритмы коррекции.</li>
</ul>
<p>Если зашло само упражнение «разобрать бинарную структуру руками», у меня есть ещё два таких: <a href="/posts/what-is-mbr/">как выглядит MBR</a> и <a href="/posts/what-is-gpt/">как выглядит GPT</a> — там тоже <code>dd</code>, <code>hexdump</code> и байты по полочкам.</p>
</div>]]></content:encoded>
    </item><item>
      <title>SLO как чертёж архитектуры</title>
      <link>https://jtprog.ru/posts/slo-as-architecture-blueprint/</link>
      <pubDate>Sun, 10 May 2026 20:57:37 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/slo-as-architecture-blueprint/</guid>
      <category>SRE</category>
      <category>SLO</category>
      <category>System Design</category>
      <category>Error Budget</category>
      <category>Programming</category>
      <category domain="category">SRE</category>
      <enclosure url="https://jtprog.ru/cover.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! На системном дизайне 9 из 10 кандидатов начинают с компонентов: API gateway, Kafka, Redis, Postgres-реплики, шардинг, CDN. Это не дизайн — это плакат с логотипами. Дизайн начинается с вопроса, на который почти никто не отвечает первым: «какой надёжности я обещаю и почему именно этой, а не другой?».</p>
<p>Без числа в этом ответе — <code>99.5</code>, <code>99.9</code>, <code>99.99</code> — все архитектурные решения ниже становятся вопросом эстетики. С числом — превращаются в инженерные ограничения, которые решают за тебя, нужна ли вторая БД-реплика, имеет ли смысл repair-aware кэш и сколько регионов держать в active-active.</p>
<p>В этой статье я беру одну каноничную задачу с собеса — <strong>поиск каталога маркетплейса</strong> — и прокручиваю её сквозь три уровня SLO: <code>99.0%</code>, <code>99.9%</code>, <code>99.99%</code>. На каждом уровне меняется не «настройка пары флагов», а сама архитектура. Главный тезис: SLI/SLO — это не SRE-декорация поверх готовой системы, а <strong>интерфейс между System Design и бизнесом</strong>. Тот самый, через который аналитик, бэкенд и SRE говорят на одном языке.</p>
<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<p>Статья длинная (~55 000 знаков), поэтому сразу — что заберёт каждая ЦА:</p>
<ul>
<li><strong>Аналитик / продакт.</strong> Рамка для перевода бизнес-метрик в SLI и для разговора с инженерами в стиле «а если снизим SLO с 99.99% до 99.9% — что освободится?».</li>
<li><strong>Бэкенд-инженер.</strong> Как один и тот же поиск каталога превращается в три разные системы при смене таргета SLO — read path, кэш, индекс, failover.</li>
<li><strong>SRE / DevOps.</strong> Error budget как валюта в спорах «фича vs рефакторинг» + типовые ошибки выбора SLI: что делать, когда дашборд зелёный, а пользователи — в саппорте.</li>
</ul>
<p>Общий блок для всех: разница между <strong>Observability</strong>, <strong>Monitoring</strong> и <strong>Alerting</strong> — в одной схеме.</p>
<h2 id="tldr-если-читать-дальше-уже-некогда">TL;DR (если читать дальше уже некогда)</h2>
<p>Если у тебя нет 30 минут на длинный разбор — четыре тезиса, к которым приходит статья:</p>
<ol>
<li><strong>SLO выбирается до архитектуры, а не после.</strong> Поднять SLO задним числом — это переписать систему, а не «добавить мониторинга». Снизить — освободить бюджет, но не вернуть деньги.</li>
<li><strong>Один и тот же кейс на трёх SLO (<code>99.0%</code> / <code>99.9%</code> / <code>99.99%</code>) — это три разные системы.</strong> Меняется не «настройка флагов», а топология, технологии, capacity, RTO/RPO, операционная модель и стоимость (<code>1×</code> → <code>4–6×</code> → <code>15–25×</code> baseline).</li>
<li><strong>SLI вешаются на шаги критичного пользовательского пути (CUJ), а не «на сервис в целом».</strong> «99.9% по системе» — это псевдо-метрика, которая прячет деградацию ключевого шага за здоровьем фоновых эндпоинтов.</li>
<li><strong>Error budget работает как валюта в спорах «фича vs рефакторинг», только если есть записанная error budget policy.</strong> Без неё SLO — это график на дашборде, а не дисциплина.</li>
</ol>
<p>Дальше — как до этих четырёх тезисов прийти через один сквозной кейс. Если интересует только инженерная кульминация — переходи сразу к разделу <a href="#%d0%be%d0%b4%d0%b8%d0%bd-%d0%ba%d0%b5%d0%b9%d1%81--%d1%82%d1%80%d0%b8-slo--%d1%82%d1%80%d0%b8-%d0%b0%d1%80%d1%85%d0%b8%d1%82%d0%b5%d0%ba%d1%82%d1%83%d1%80%d1%8b">Один кейс — три SLO — три архитектуры</a>.</p>
<h2 id="кейс-поиск-каталога-маркетплейса">Кейс: поиск каталога маркетплейса</h2>
<p>Чтобы не разговаривать абстракциями, возьмём одну сквозную задачу. Формулирую как на собеседовании:</p>
<blockquote><p>Спроектируйте сервис поиска и выдачи карточек товаров для крупного маркетплейса. На вход — поисковый запрос пользователя и фильтры (категория, цена, доставка, регион). На выход — отранжированный список карточек с превью, пагинация, фасеты.</p>
</blockquote><p>Параметры — реалистичные для маркетплейса масштаба Ozon / Wildberries / Avito, но «приглаженные» под удобство арифметики:</p>
<table>
	<thead>
			<tr>
					<th>Характеристика</th>
					<th>Значение</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Активный каталог</td>
					<td>~50M товаров</td>
			</tr>
			<tr>
					<td>Пиковый RPS на поиск</td>
					<td>~30 000 запросов/сек</td>
			</tr>
			<tr>
					<td>Активных одновременных пользователей</td>
					<td>~200 000</td>
			</tr>
			<tr>
					<td>Доля mobile-трафика</td>
					<td>~70%</td>
			</tr>
			<tr>
					<td>География</td>
					<td>5 регионов, основной — central RU</td>
			</tr>
			<tr>
					<td>Целевая P50 латентность read path</td>
					<td>&lt; 150 ms</td>
			</tr>
			<tr>
					<td>Целевая P99 латентность read path</td>
					<td>&lt; 500 ms</td>
			</tr>
			<tr>
					<td>Сезонные пики (BF, распродажи)</td>
					<td>x3–x5 от обычного RPS</td>
			</tr>
	</tbody>
</table>
<p>Бизнес-контекст, про который часто забывают на собесе, и зря:</p>
<ul>
<li><strong>Поиск — главный драйвер конверсии.</strong> По публичным отчётам (SimilarWeb, ContentSquare, материалы крупных маркетплейсов) на больших площадках 40–70% покупок начинается с поисковой строки, не с категорий или баннеров. Конкретный процент гуляет по сегментам и площадкам, но порядок одинаков. Любая деградация поиска — это прямая просадка GMV, не «инженерная неприятность».</li>
<li><strong>Эластичность по latency измерима.</strong> Цифры из публикаций Amazon (Greg Linden, 2006), Akamai (2017), Google/SOASTA (2017) и Deloitte (2020) разнятся по методологии и сегменту, но порядок сходится: каждые <code>+100 ms</code> latency на ключевом read path стоят примерно <code>−0.5…1.5%</code> конверсии. Для маркетплейса с GMV $10M в день это $50–150k недополученной выручки <strong>в день</strong>, если регресс закрепится. Это не разовый ущерб, а просадка, в которую конвертируется latency на ключевом read path.</li>
<li><strong>Стоимость недоступности нелинейна.</strong> 5 минут даунтайма обычным вторником и 5 минут даунтайма в Чёрную пятницу — это разные деньги в десятки раз. SLO должно учитывать сезонность, иначе оно меряет не то.</li>
</ul>
<p>Эти три факта — ровно то, что превращает SLI/SLO из «инженерной гигиены» в инструмент бизнес-разговора. Я буду к ним возвращаться в каждом следующем разделе.</p>
<h2 id="что-обычно-ломается-на-собесе-без-slo">Что обычно ломается на собесе без SLO</h2>
<p>Послушаешь сотню system-design интервью — и складывается узнаваемый паттерн ответа.</p>
<h3 id="типичная-траектория">Типичная траектория</h3>
<ol>
<li><strong>Уточнили требования.</strong> «Сколько пользователей? Какая нагрузка?» — кандидат услышал «много», и в голове щёлкнуло «значит, надо много всего».</li>
<li><strong>Нарисовали API.</strong> REST с парой эндпоинтов, gateway, auth-middleware. Пока без сюрпризов.</li>
<li><strong>Добавили слой данных.</strong> Postgres / MySQL — «реляционка для каталога норм». Кто-то сразу предложит Elasticsearch, потому что слышал про full-text search.</li>
<li><strong>Поставили кэш.</strong> Redis — потому что «Redis fast». Без обсуждения, что именно кэшируем, какой hit rate ожидаем, как инвалидируем при изменении цены или остатков.</li>
<li><strong>Подняли реплики.</strong> «На чтение пойдут реплики, на запись — мастер». Без обсуждения, насколько асинхронная репликация критична: а если пользователь не увидит свой только что выложенный товар?</li>
<li><strong>Зашардировали.</strong> Чаще всего по <code>category_id</code> или <code>seller_id</code>. Потому что «у больших так».</li>
<li><strong>Накинули CDN и очередь.</strong> Kafka — куда же без неё. Async-обработка событий, чтобы «развязать сервисы».</li>
</ol>
<p>К концу интервью на доске — красивая распределённая система. Интервьюер кивает. Кандидат уверен, что справился.</p>
<h3 id="что-в-этой-траектории-не-так">Что в этой траектории не так</h3>
<p>Простой тест. Ответь сам себе: <strong>какое из решений выше изменилось бы, если бы требование к доступности было <code>99.0%</code>, а не <code>99.99%</code>?</strong> А если бы P99 была не 500 ms, а 2 секунды?</p>
<p>Если ответ «никакое» — значит, дизайн не привязан к требованиям к надёжности. Кандидат строил систему «как принято», а не «как нужно бизнесу». А раз все решения принимались независимо от целевого SLO — значит, любые из них могут быть и преждевременными, и недостаточными одновременно. Точно сказать нельзя, потому что точки отсчёта нет.</p>
<p>Чтобы было совсем больно — разберём по решениям:</p>
<ul>
<li><strong>Кэш Redis.</strong> Имеет смысл, если твой SLO по latency — десятки миллисекунд для горячих запросов или ты упираешься в нагрузку на БД при текущем availability-таргете. При расслабленном SLO P99 = 2 секунды кэш в read path — преждевременная оптимизация: ты поднимаешь эксплуатационную сложность (инвалидация, прогрев, репликация Redis-кластера, отдельный SLO на сам кэш) ради цели, которая выполнима и без него.</li>
<li><strong>Реплики на чтение.</strong> Имеют смысл, если твой SLO availability такой, что мастер-only архитектура не уложится в бюджет ошибок при типовом MTTR. На SLO <code>99.0%</code> (~7 часов даунтайма в месяц) одного хорошо настроенного primary с мониторингом и быстрым backup-восстановлением часто достаточно.</li>
<li><strong>Шардинг.</strong> Оправдан, когда объём данных или write QPS упирается в один инстанс физически. До этого — это удвоение операционной нагрузки (rebalancing, hot shards, резолв cross-shard запросов) без выигрыша по SLI, ради которого его делают.</li>
<li><strong>Multi-region active-active.</strong> Имеет смысл при SLO availability <code>99.99%</code>+ или когда регуляторика обязывает (data residency, ФЗ-149/ФЗ-152/ФЗ-242/GDPR-аналоги). На <code>99.9%</code> обычно дешевле и предсказуемее active-passive с автоматическим failover и RTO в минутах.</li>
</ul>
<p>Каждое из этих решений — правильное в одних условиях и преждевременное в других. Условия задаёт SLO. Без него ты выбираешь архитектуру не под задачу, а под насмотренность.</p>
<h3 id="главный-сдвиг--направление-дизайна">Главный сдвиг — направление дизайна</h3>
<p>Хороший дизайн идёт <strong>сверху вниз</strong>: бизнес-цель → критичный пользовательский путь (CUJ, critical user journey) → SLI на каждый шаг этого пути → SLO как контракт → архитектура, которая в этот SLO укладывается с разумным запасом.</p>
<p>Плохой дизайн идёт <strong>снизу вверх</strong>: «у крутых ребят такая схема — нарисуем такую же». На выходе получается архитектура, в которую любой SLO «впихнуть можно постфактум» — потому что под её требования не задавали ни одного числа, и теперь любая цифра одинаково годится для отчётности и одинаково плохо отражает реальное поведение системы.</p>
<p>В следующих разделах я разворачиваю верхний путь — сверху вниз — на нашем кейсе с маркетплейсом. Начнём с шага 1: как из функциональных требований получается тот самый критичный пользовательский путь.</p>
<h2 id="шаг-1-от-фт-к-критичному-пользовательскому-пути">Шаг 1: от ФТ к критичному пользовательскому пути</h2>
<p>Раз направление дизайна сверху вниз — нужна точка отсчёта на самом верху. Это <strong>функциональные требования (ФТ)</strong>: что система делает в терминах пользователя и продукта. Не «использует Elasticsearch», а «возвращает релевантную выдачу по запросу».</p>
<h3 id="фт-для-нашего-кейса">ФТ для нашего кейса</h3>
<ul>
<li>Принять поисковый запрос (текст + фильтры) от авторизованного и анонимного пользователя.</li>
<li>Подбирать товары из активного каталога с учётом региона и доступной доставки.</li>
<li>Ранжировать результаты с учётом релевантности, цены, рейтинга продавца и персонализации.</li>
<li>Возвращать пагинированную выдачу с фасетами (категории, ценовые диапазоны, бренды).</li>
<li>Поддерживать sorting (price asc/desc, popularity, rating).</li>
<li>Подсказки и автодополнение по мере набора запроса.</li>
</ul>
<p>Это всё одинаково «функциональные требования» — но <strong>не одинаково критичные</strong>. Если автодополнение тормозит на 200 ms, пользователь поморщится. Если выдача не приходит вовсе — он закроет вкладку и пойдёт к конкуренту. ФТ сами по себе плоские, в них нет приоритета. Этот приоритет приносит <strong>критичный пользовательский путь</strong>.</p>
<h3 id="критичный-пользовательский-путь-cuj--critical-user-journey">Критичный пользовательский путь (CUJ — critical user journey)</h3>
<p>Это сценарий пользователя, который напрямую кормит ключевую бизнес-метрику. У маркетплейса бизнес-метрика — GMV, точка входа в неё — поиск, точка выхода — клик в карточку и далее в корзину. Получаем такой путь:</p>
<blockquote><p>Пользователь приходит на сайт с намерением купить → формулирует запрос → получает релевантную выдачу → выбирает товар → переходит в карточку.</p>
</blockquote><p>Это и есть «золотой путь», за который держимся в дизайне. Всё, что не обслуживает этот путь напрямую (статистика для аналитики, A/B-эксперименты, персональные рекомендации в стороннем виджете), может быть менее надёжным — и это нормально.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Терминологическая дисциплина</p>
    <div class="callout__content"><p>Дальше по тексту слово <strong>«сервис»</strong> работает в двух разных значениях, и я их буду стараться разводить явно:</p>
<ul>
<li><strong>Пользовательский сервис</strong> — это CUJ (поиск каталога целиком). К нему привязаны SLI и SLO.</li>
<li><strong>Микросервис</strong> — это юнит деплоя (Search Service, Ranking Service на схеме архитектуры). К нему привязаны Golden Signals для внутренней диагностики, но <strong>не отдельный набор SLI</strong>.</li>
</ul>
<p>Один пользовательский сервис может реализовываться десятью микросервисами — но SLI по-прежнему 4–8, не 4–8 × 10. К этому различию ещё вернёмся в разделе про типичные ошибки.</p></div>
  </div>
</aside><h3 id="декомпозиция-пути-на-шаги">Декомпозиция пути на шаги</h3>
<p>Дальше путь режется на дискретные шаги, каждый из которых станет отдельной точкой измерения:</p>
<table>
	<thead>
			<tr>
					<th>#</th>
					<th>Шаг</th>
					<th>Что делает система</th>
					<th>Что замечает пользователь</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>Загрузка поискового интерфейса</td>
					<td>Отдать HTML/SPA-bundle, инициализировать клиент</td>
					<td>Страница появилась, поле поиска кликабельно</td>
			</tr>
			<tr>
					<td>2</td>
					<td>Автодополнение (suggest)</td>
					<td>На каждое нажатие клавиши вернуть top-N подсказок</td>
					<td>Подсказки появляются «без задержки»</td>
			</tr>
			<tr>
					<td>3</td>
					<td>Отправка запроса</td>
					<td>Получить query + фильтры, направить в search backend</td>
					<td>Лоадер крутится недолго</td>
			</tr>
			<tr>
					<td>4</td>
					<td>Получение выдачи</td>
					<td>Поиск в индексе, ранжирование, обогащение карточек</td>
					<td>Выдача отрисовалась, скроллится плавно</td>
			</tr>
			<tr>
					<td>5</td>
					<td>Применение фильтра / сортировки</td>
					<td>Реквест с уточнёнными параметрами</td>
					<td>Список перестроился без рывков</td>
			</tr>
			<tr>
					<td>6</td>
					<td>Клик в карточку</td>
					<td>Переход на страницу товара, прелоад данных</td>
					<td>Карточка открылась, цена и наличие актуальны</td>
			</tr>
	</tbody>
</table>
<p>Эти шесть шагов — каркас, на котором будет висеть всё дальнейшее. На каждый шаг повесим SLI, на каждый SLI — SLO. И увидим, что они <strong>не равноценны</strong>: шаг 4 (получение выдачи) для бизнеса принципиально важнее, чем шаг 2 (автодополнение). А значит, и инженерных усилий на их надёжность нужно несоизмеримо больше.</p>
<h3 id="почему-это-важно-ловушка-среднего-по-системе">Почему это важно: ловушка «среднего по системе»</h3>
<p>На многих собеседованиях кандидат, отвечая на «какую ты доступность хочешь», говорит «99.9% по системе». Это бесполезное число: если оно посчитано как «суммарный аптайм всех эндпоинтов», оно скрывает то, что выдача упала на 30 минут, а суммарно «вышло 99.9%, потому что autocomplete и health-чеки крутились». Пользователь страдал, дашборд зелёный.</p>
<p>Шаги критичного пути — антидот этой ловушки. Каждый шаг получает свою цифру, и провал шага 4 невозможно «размазать» доступностью соседних шагов.</p>
<h2 id="шаг-2-нфт-как-свойства-шагов-а-не-системы">Шаг 2: НФТ как свойства шагов, а не системы</h2>
<p>Когда есть карта шагов, нефункциональные требования (НФТ) перестают быть размытым «надёжно, быстро, безопасно». Они становятся набором конкретных свойств <strong>каждого шага</strong>.</p>
<h3 id="свойства-которые-нас-интересуют">Свойства, которые нас интересуют</h3>
<p>Для read-heavy критичного пути маркетплейса базовый набор НФТ выглядит так:</p>
<ul>
<li><strong>Латентность.</strong> Сколько миллисекунд занял шаг. Меряем перцентилями (P50/P95/P99), не средним — оно скрывает хвосты, а хвосты и есть пользовательская боль.</li>
<li><strong>Доступность.</strong> Доля успешных шагов. Не «200/всего HTTP», а именно «шаг выполнен корректно с точки зрения сценария».</li>
<li><strong>Корректность.</strong> Получил ли пользователь ожидаемый результат. Для шага 4 — вернулась ли релевантная выдача или 0 результатов из-за бага в ранжировании.</li>
<li><strong>Свежесть.</strong> Насколько актуальны данные. Цена и наличие на карточке после клика — критичная свежесть; popularity-score в ранжировании — терпит часовую задержку.</li>
<li><strong>Целостность.</strong> Не «исчезает» ли часть результатов из-за частичного сбоя ноды или таймаута к шарду. Это специфично для распределённого поиска и шардированных индексов.</li>
</ul>
<h3 id="привязка-свойств-к-шагам-пути">Привязка свойств к шагам пути</h3>
<p>Не каждое свойство одинаково важно на каждом шаге. Покажу таблицей, какие НФТ для каких шагов нашего кейса критичны:</p>
<table>
	<thead>
			<tr>
					<th>Шаг</th>
					<th>Латентность</th>
					<th>Доступность</th>
					<th>Корректность</th>
					<th>Свежесть</th>
					<th>Целостность</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1. Загрузка интерфейса</td>
					<td>средне</td>
					<td>высоко</td>
					<td>низко</td>
					<td>низко</td>
					<td>низко</td>
			</tr>
			<tr>
					<td>2. Autocomplete</td>
					<td>высоко</td>
					<td>средне</td>
					<td>средне</td>
					<td>низко</td>
					<td>низко</td>
			</tr>
			<tr>
					<td>3. Отправка запроса</td>
					<td>средне</td>
					<td>высоко</td>
					<td>низко</td>
					<td>низко</td>
					<td>низко</td>
			</tr>
			<tr>
					<td>4. Получение выдачи</td>
					<td><strong>высоко</strong></td>
					<td><strong>высоко</strong></td>
					<td><strong>высоко</strong></td>
					<td>средне</td>
					<td><strong>высоко</strong></td>
			</tr>
			<tr>
					<td>5. Фильтр / сортировка</td>
					<td>высоко</td>
					<td>высоко</td>
					<td>высоко</td>
					<td>средне</td>
					<td>высоко</td>
			</tr>
			<tr>
					<td>6. Клик в карточку</td>
					<td>средне</td>
					<td>высоко</td>
					<td>высоко</td>
					<td><strong>высоко</strong></td>
					<td>низко</td>
			</tr>
	</tbody>
</table>
<p>Сразу видно: шаг 4 — концентратор НФТ, на нём горят почти все колонки. Шаг 6 особенно требователен к свежести — потому что показать пользователю карточку с «вчерашней» ценой (а на чекауте уже другая) — это худший UX и часто прямой ущерб бизнесу через возвраты и репутацию.</p>
<h3 id="главное-смещение-от-привычных-нфт-из-тз">Главное смещение от привычных НФТ из ТЗ</h3>
<p>Классические нефункциональные требования — «система должна обеспечивать 99.9% доступности» — это <strong>средневзвешенное по чему попало</strong>. Они одинаково оценивают и autocomplete, и финальную выдачу. В реальной эксплуатации это приводит к двум симметричным болям:</p>
<ol>
<li><strong>Деньги тратятся не туда.</strong> Команда вкладывает усилия в надёжность шагов, которые этого не требуют (autocomplete с 99.99% — приятно, но конверсию не трогает), а критичные шаги остаются на «общем уровне».</li>
<li><strong>Алерты молчат, когда болит.</strong> Дашборд показывает зелёный «99.9% по системе», пока шаг 4 деградирует и теряет каждую десятую выдачу. Эту потерю топит «здоровье» десятка других эндпоинтов и фоновых хелсчеков.</li>
</ol>
<p>НФТ-по-шагам убирают эту усреднённость. Следующий шаг сверху вниз — превращаем эти свойства в <strong>измеримые SLI</strong>, чтобы было что мерить и за что отчитываться. Но прежде чем давать формулу — нужен общий словарь, без него разговор про SLI получится поверхностным.</p>
<h2 id="шаг-3-sli-как-индикатор-пользовательского-опыта">Шаг 3: SLI как индикатор пользовательского опыта</h2>
<h3 id="сначала--общий-словарь-observability--monitoring--alerting">Сначала — общий словарь: Observability ≠ Monitoring ≠ Alerting</h3>
<p>В SRE-практике три термина часто валят в одну кучу, и тогда любой разговор про SLI разваливается на терминологические уточнения. Развожу их через <strong>цель</strong> каждой практики:</p>
<ul>
<li><strong>Monitoring (мониторинг)</strong> отвечает на заранее заданные вопросы. Ты знаешь, что хочешь видеть: «жив ли primary», «какая P99 у эндпоинта <code>/search</code>», «не растёт ли latency». На каждую заготовку — метрика или дашборд. Это про <strong>known unknowns</strong> — вещи, о которых ты знаешь, что они могут пойти не так, и хочешь их видеть.</li>
<li><strong>Observability (наблюдаемость)</strong> — свойство системы, которое позволяет дойти до <strong>нового</strong> вопроса. Не «жив ли primary», а «почему именно в этом конкретном запросе latency 4 секунды». Высокая observability означает, что у тебя достаточно данных (метрик, структурированных логов, трейсов с tail-based sampling, событий) и инструментов поверх них, чтобы задать системе вопрос, который ты не предусмотрел заранее. Это про <strong>unknown unknowns</strong> — про неожиданности, которых ты не ждал. Такое разведение «monitoring = known unknowns / observability = unknown unknowns» — рамка из «Distributed Systems Observability» Cindy Sridharan (O&rsquo;Reilly, 2018) и развитая инженерами Honeycomb (Charity Majors et al.); внутри Google SRE Workbook грань между ними размытее, и я сознательно беру более жёсткую формулировку — на ней легче строить дизайн.</li>
<li><strong>Alerting (алертинг)</strong> — действие по событию. Это слой, надстроенный над мониторингом и observability: правила, которые превращают наблюдаемое состояние в инцидент с пейджингом. Алерт — это не метрика, это <strong>обещание разбудить человека</strong>, и за него надо отвечать. Alert fatigue — отдельная боль, и лечится она burn-rate-алертами, а не увеличением количества правил.</li>
</ul>
<p>Связь между ними — на одной схеме:</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/02e1152b1ac5323bffbbebf499019cc686d27dbc624b8e2df7c0e398bbfeb612.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-02e1152b1ac5-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1109.69px; background-color: transparent;" viewBox="0 0 1109.6875 390.375" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-02e1152b1ac5-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-02e1152b1ac5-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-light .error-icon{fill:#dce0e8;}#m-02e1152b1ac5-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-02e1152b1ac5-light .edge-thickness-normal{stroke-width:1px;}#m-02e1152b1ac5-light .edge-thickness-thick{stroke-width:3.5px;}#m-02e1152b1ac5-light .edge-pattern-solid{stroke-dasharray:0;}#m-02e1152b1ac5-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-02e1152b1ac5-light .edge-pattern-dashed{stroke-dasharray:3;}#m-02e1152b1ac5-light .edge-pattern-dotted{stroke-dasharray:2;}#m-02e1152b1ac5-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-02e1152b1ac5-light .marker.cross{stroke:#5c5f77;}#m-02e1152b1ac5-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-02e1152b1ac5-light p{margin:0;}#m-02e1152b1ac5-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-02e1152b1ac5-light .cluster-label text{fill:#4c4f69;}#m-02e1152b1ac5-light .cluster-label span{color:#4c4f69;}#m-02e1152b1ac5-light .cluster-label span p{background-color:transparent;}#m-02e1152b1ac5-light .label text,#m-02e1152b1ac5-light span{fill:#4c4f69;color:#4c4f69;}#m-02e1152b1ac5-light .node rect,#m-02e1152b1ac5-light .node circle,#m-02e1152b1ac5-light .node ellipse,#m-02e1152b1ac5-light .node polygon,#m-02e1152b1ac5-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-02e1152b1ac5-light .rough-node .label text,#m-02e1152b1ac5-light .node .label text,#m-02e1152b1ac5-light .image-shape .label,#m-02e1152b1ac5-light .icon-shape .label{text-anchor:middle;}#m-02e1152b1ac5-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-02e1152b1ac5-light .rough-node .label,#m-02e1152b1ac5-light .node .label,#m-02e1152b1ac5-light .image-shape .label,#m-02e1152b1ac5-light .icon-shape .label{text-align:center;}#m-02e1152b1ac5-light .node.clickable{cursor:pointer;}#m-02e1152b1ac5-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-02e1152b1ac5-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-02e1152b1ac5-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-02e1152b1ac5-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-02e1152b1ac5-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-02e1152b1ac5-light .edgeLabel p{background-color:#eff1f5;}#m-02e1152b1ac5-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-02e1152b1ac5-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-02e1152b1ac5-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-02e1152b1ac5-light .cluster text{fill:#4c4f69;}#m-02e1152b1ac5-light .cluster span{color:#4c4f69;}#m-02e1152b1ac5-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-02e1152b1ac5-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-02e1152b1ac5-light rect.text{fill:none;stroke-width:0;}#m-02e1152b1ac5-light .icon-shape,#m-02e1152b1ac5-light .image-shape{background-color:#eff1f5;text-align:center;}#m-02e1152b1ac5-light .icon-shape p,#m-02e1152b1ac5-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-02e1152b1ac5-light .icon-shape .label rect,#m-02e1152b1ac5-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-02e1152b1ac5-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-02e1152b1ac5-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-02e1152b1ac5-light .node .neo-node{stroke:#bcc0cc;}#m-02e1152b1ac5-light [data-look="neo"].node rect,#m-02e1152b1ac5-light [data-look="neo"].cluster rect,#m-02e1152b1ac5-light [data-look="neo"].node polygon{stroke:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].node path{stroke:url(#m-02e1152b1ac5-light-gradient);stroke-width:1px;}#m-02e1152b1ac5-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-02e1152b1ac5-light [data-look="neo"].node circle{stroke:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-02e1152b1ac5-light [data-look="neo"].icon-shape .icon{fill:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-02e1152b1ac5-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M207.593,178L221.005,171.021C234.416,164.042,261.239,150.083,278.151,143.104C295.063,136.125,302.063,136.125,305.563,136.125L309.063,136.125" id="m-02e1152b1ac5-light-L_System_M_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_M_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoxNzh9LHsieCI6Mjg4LjA2MjUsInkiOjEzNi4xMjV9LHsieCI6MzEzLjA2MjUsInkiOjEzNi4xMjV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M207.593,253L221.005,259.979C234.416,266.958,261.239,280.917,282.315,287.896C303.391,294.875,318.719,294.875,326.383,294.875L334.047,294.875" id="m-02e1152b1ac5-light-L_System_O_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_O_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoyNTN9LHsieCI6Mjg4LjA2MjUsInkiOjI5NC44NzV9LHsieCI6MzM4LjA0Njg3NSwieSI6Mjk0Ljg3NX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M537.651,87.375L547.282,82.479C556.913,77.583,576.175,67.792,589.308,63.122C602.44,58.452,609.443,58.904,612.944,59.129L616.446,59.355" id="m-02e1152b1ac5-light-L_M_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_A_0" data-points="W3sieCI6NTM3LjY1MSwieSI6ODcuMzc1fSx7IngiOjU5NS40Mzc1LCJ5Ijo1OH0seyJ4Ijo2MjAuNDM3NSwieSI6NTkuNjEyOTAzMjI1ODA2NDV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M504.267,234.875L519.462,220.292C534.657,205.708,565.047,176.542,585.955,159.033C606.862,141.524,618.287,135.674,623.999,132.749L629.712,129.823" id="m-02e1152b1ac5-light-L_O_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_A_0" data-points="W3sieCI6NTA0LjI2Njk0OTE1MjU0MjQsInkiOjIzNC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjE0Ny4zNzV9LHsieCI6NjMzLjI3MjE0NTY2OTI5MTQsInkiOjEyOH1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M880.438,68L884.604,68C888.771,68,897.104,68,904.771,68C912.438,68,919.438,68,922.938,68L926.438,68" id="m-02e1152b1ac5-light-L_A_H_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_H_0" data-points="W3sieCI6ODgwLjQzNzUsInkiOjY4fSx7IngiOjkwNS40Mzc1LCJ5Ijo2OH0seyJ4Ijo5MzAuNDM3NSwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M525.58,184.875L537.223,191.646C548.866,198.417,572.152,211.958,588.033,218.729C603.914,225.5,612.391,225.5,616.629,225.5L620.867,225.5" id="m-02e1152b1ac5-light-L_M_D_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_D_0" data-points="W3sieCI6NTI1LjU3OTU0NTQ1NDU0NTUsInkiOjE4NC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjIyNS41fSx7IngiOjYyNC44NjcxODc1LCJ5IjoyMjUuNX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M545.453,336.204L553.784,339.525C562.115,342.845,578.776,349.485,596.022,352.805C613.268,356.125,631.099,356.125,640.014,356.125L648.93,356.125" id="m-02e1152b1ac5-light-L_O_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_R_0" data-points="W3sieCI6NTQ1LjQ1MzEyNSwieSI6MzM2LjIwNDQyNzYxMjg1MDc0fSx7IngiOjU5NS40Mzc1LCJ5IjozNTYuMTI1fSx7IngiOjY1Mi45Mjk2ODc1LCJ5IjozNTYuMTI1fV0=" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_System_M_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_System_O_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_H_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_D_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-02e1152b1ac5-light-flowchart-System-0" data-look="classic" transform="translate(135.53125, 215.5)"><rect class="basic label-container" style="" x="-127.53125" y="-37.5" width="255.0625" height="75"/><g class="label" style="" transform="translate(-97.53125, -22.5)"><rect/><foreignObject width="195.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Система<br />сервисы, БД, кэш, очереди</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-M-1" data-look="classic" transform="translate(441.75, 136.125)"><rect class="basic label-container" style="" x="-128.6875" y="-48.75" width="257.375" height="97.5"/><g class="label" style="" transform="translate(-98.6875, -33.75)"><rect/><foreignObject width="197.375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Monitoring<br />known unknowns:<br />заранее заданные метрики</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-O-3" data-look="classic" transform="translate(441.75, 294.875)"><rect class="basic label-container" style="" x="-103.703125" y="-60" width="207.40625" height="120"/><g class="label" style="" transform="translate(-73.703125, -45)"><rect/><foreignObject width="147.40625" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability<br />unknown unknowns:<br />возможность задать<br />новый вопрос</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-A-5" data-look="classic" transform="translate(750.4375, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Alerting<br />правила, которые<br />превращают сигнал в инцидент</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-H-9" data-look="classic" transform="translate(1016.0625, 68)"><rect class="basic label-container" style="" x="-85.625" y="-26.25" width="171.25" height="52.5"/><g class="label" style="" transform="translate(-55.625, -11.25)"><rect/><foreignObject width="111.25" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Человек on-call</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-D-11" data-look="classic" transform="translate(750.4375, 225.5)"><rect class="basic label-container" style="" x="-125.5703125" y="-26.25" width="251.140625" height="52.5"/><g class="label" style="" transform="translate(-95.5703125, -11.25)"><rect/><foreignObject width="191.140625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Дашборды и SLO-репорты</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-R-13" data-look="classic" transform="translate(750.4375, 356.125)"><rect class="basic label-container" style="" x="-97.5078125" y="-26.25" width="195.015625" height="52.5"/><g class="label" style="" transform="translate(-67.5078125, -11.25)"><rect/><foreignObject width="135.015625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Root cause analysis</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-02e1152b1ac5-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-02e1152b1ac5-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-02e1152b1ac5-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-02e1152b1ac5-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1109.69px; background-color: transparent;" viewBox="0 0 1109.6875 390.375" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-02e1152b1ac5-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-02e1152b1ac5-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-dark .error-icon{fill:#181926;}#m-02e1152b1ac5-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-02e1152b1ac5-dark .edge-thickness-normal{stroke-width:1px;}#m-02e1152b1ac5-dark .edge-thickness-thick{stroke-width:3.5px;}#m-02e1152b1ac5-dark .edge-pattern-solid{stroke-dasharray:0;}#m-02e1152b1ac5-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-02e1152b1ac5-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-02e1152b1ac5-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-02e1152b1ac5-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-02e1152b1ac5-dark .marker.cross{stroke:#a5adcb;}#m-02e1152b1ac5-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-02e1152b1ac5-dark p{margin:0;}#m-02e1152b1ac5-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-02e1152b1ac5-dark .cluster-label text{fill:#cad3f5;}#m-02e1152b1ac5-dark .cluster-label span{color:#cad3f5;}#m-02e1152b1ac5-dark .cluster-label span p{background-color:transparent;}#m-02e1152b1ac5-dark .label text,#m-02e1152b1ac5-dark span{fill:#cad3f5;color:#cad3f5;}#m-02e1152b1ac5-dark .node rect,#m-02e1152b1ac5-dark .node circle,#m-02e1152b1ac5-dark .node ellipse,#m-02e1152b1ac5-dark .node polygon,#m-02e1152b1ac5-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-02e1152b1ac5-dark .rough-node .label text,#m-02e1152b1ac5-dark .node .label text,#m-02e1152b1ac5-dark .image-shape .label,#m-02e1152b1ac5-dark .icon-shape .label{text-anchor:middle;}#m-02e1152b1ac5-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-02e1152b1ac5-dark .rough-node .label,#m-02e1152b1ac5-dark .node .label,#m-02e1152b1ac5-dark .image-shape .label,#m-02e1152b1ac5-dark .icon-shape .label{text-align:center;}#m-02e1152b1ac5-dark .node.clickable{cursor:pointer;}#m-02e1152b1ac5-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-02e1152b1ac5-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-02e1152b1ac5-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-02e1152b1ac5-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-02e1152b1ac5-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-02e1152b1ac5-dark .edgeLabel p{background-color:#24273a;}#m-02e1152b1ac5-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-02e1152b1ac5-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-02e1152b1ac5-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-02e1152b1ac5-dark .cluster text{fill:#cad3f5;}#m-02e1152b1ac5-dark .cluster span{color:#cad3f5;}#m-02e1152b1ac5-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-02e1152b1ac5-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-02e1152b1ac5-dark rect.text{fill:none;stroke-width:0;}#m-02e1152b1ac5-dark .icon-shape,#m-02e1152b1ac5-dark .image-shape{background-color:#24273a;text-align:center;}#m-02e1152b1ac5-dark .icon-shape p,#m-02e1152b1ac5-dark .image-shape p{background-color:#24273a;padding:2px;}#m-02e1152b1ac5-dark .icon-shape .label rect,#m-02e1152b1ac5-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-02e1152b1ac5-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-02e1152b1ac5-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-02e1152b1ac5-dark .node .neo-node{stroke:#494d64;}#m-02e1152b1ac5-dark [data-look="neo"].node rect,#m-02e1152b1ac5-dark [data-look="neo"].cluster rect,#m-02e1152b1ac5-dark [data-look="neo"].node polygon{stroke:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].node path{stroke:url(#m-02e1152b1ac5-dark-gradient);stroke-width:1px;}#m-02e1152b1ac5-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-02e1152b1ac5-dark [data-look="neo"].node circle{stroke:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-02e1152b1ac5-dark [data-look="neo"].icon-shape .icon{fill:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M207.593,178L221.005,171.021C234.416,164.042,261.239,150.083,278.151,143.104C295.063,136.125,302.063,136.125,305.563,136.125L309.063,136.125" id="m-02e1152b1ac5-dark-L_System_M_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_M_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoxNzh9LHsieCI6Mjg4LjA2MjUsInkiOjEzNi4xMjV9LHsieCI6MzEzLjA2MjUsInkiOjEzNi4xMjV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M207.593,253L221.005,259.979C234.416,266.958,261.239,280.917,282.315,287.896C303.391,294.875,318.719,294.875,326.383,294.875L334.047,294.875" id="m-02e1152b1ac5-dark-L_System_O_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_O_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoyNTN9LHsieCI6Mjg4LjA2MjUsInkiOjI5NC44NzV9LHsieCI6MzM4LjA0Njg3NSwieSI6Mjk0Ljg3NX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M537.651,87.375L547.282,82.479C556.913,77.583,576.175,67.792,589.308,63.122C602.44,58.452,609.443,58.904,612.944,59.129L616.446,59.355" id="m-02e1152b1ac5-dark-L_M_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_A_0" data-points="W3sieCI6NTM3LjY1MSwieSI6ODcuMzc1fSx7IngiOjU5NS40Mzc1LCJ5Ijo1OH0seyJ4Ijo2MjAuNDM3NSwieSI6NTkuNjEyOTAzMjI1ODA2NDV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M504.267,234.875L519.462,220.292C534.657,205.708,565.047,176.542,585.955,159.033C606.862,141.524,618.287,135.674,623.999,132.749L629.712,129.823" id="m-02e1152b1ac5-dark-L_O_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_A_0" data-points="W3sieCI6NTA0LjI2Njk0OTE1MjU0MjQsInkiOjIzNC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjE0Ny4zNzV9LHsieCI6NjMzLjI3MjE0NTY2OTI5MTQsInkiOjEyOH1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M880.438,68L884.604,68C888.771,68,897.104,68,904.771,68C912.438,68,919.438,68,922.938,68L926.438,68" id="m-02e1152b1ac5-dark-L_A_H_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_H_0" data-points="W3sieCI6ODgwLjQzNzUsInkiOjY4fSx7IngiOjkwNS40Mzc1LCJ5Ijo2OH0seyJ4Ijo5MzAuNDM3NSwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M525.58,184.875L537.223,191.646C548.866,198.417,572.152,211.958,588.033,218.729C603.914,225.5,612.391,225.5,616.629,225.5L620.867,225.5" id="m-02e1152b1ac5-dark-L_M_D_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_D_0" data-points="W3sieCI6NTI1LjU3OTU0NTQ1NDU0NTUsInkiOjE4NC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjIyNS41fSx7IngiOjYyNC44NjcxODc1LCJ5IjoyMjUuNX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M545.453,336.204L553.784,339.525C562.115,342.845,578.776,349.485,596.022,352.805C613.268,356.125,631.099,356.125,640.014,356.125L648.93,356.125" id="m-02e1152b1ac5-dark-L_O_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_R_0" data-points="W3sieCI6NTQ1LjQ1MzEyNSwieSI6MzM2LjIwNDQyNzYxMjg1MDc0fSx7IngiOjU5NS40Mzc1LCJ5IjozNTYuMTI1fSx7IngiOjY1Mi45Mjk2ODc1LCJ5IjozNTYuMTI1fV0=" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_System_M_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_System_O_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_H_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_D_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-02e1152b1ac5-dark-flowchart-System-0" data-look="classic" transform="translate(135.53125, 215.5)"><rect class="basic label-container" style="" x="-127.53125" y="-37.5" width="255.0625" height="75"/><g class="label" style="" transform="translate(-97.53125, -22.5)"><rect/><foreignObject width="195.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Система<br />сервисы, БД, кэш, очереди</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-M-1" data-look="classic" transform="translate(441.75, 136.125)"><rect class="basic label-container" style="" x="-128.6875" y="-48.75" width="257.375" height="97.5"/><g class="label" style="" transform="translate(-98.6875, -33.75)"><rect/><foreignObject width="197.375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Monitoring<br />known unknowns:<br />заранее заданные метрики</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-O-3" data-look="classic" transform="translate(441.75, 294.875)"><rect class="basic label-container" style="" x="-103.703125" y="-60" width="207.40625" height="120"/><g class="label" style="" transform="translate(-73.703125, -45)"><rect/><foreignObject width="147.40625" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability<br />unknown unknowns:<br />возможность задать<br />новый вопрос</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-A-5" data-look="classic" transform="translate(750.4375, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Alerting<br />правила, которые<br />превращают сигнал в инцидент</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-H-9" data-look="classic" transform="translate(1016.0625, 68)"><rect class="basic label-container" style="" x="-85.625" y="-26.25" width="171.25" height="52.5"/><g class="label" style="" transform="translate(-55.625, -11.25)"><rect/><foreignObject width="111.25" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Человек on-call</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-D-11" data-look="classic" transform="translate(750.4375, 225.5)"><rect class="basic label-container" style="" x="-125.5703125" y="-26.25" width="251.140625" height="52.5"/><g class="label" style="" transform="translate(-95.5703125, -11.25)"><rect/><foreignObject width="191.140625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Дашборды и SLO-репорты</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-R-13" data-look="classic" transform="translate(750.4375, 356.125)"><rect class="basic label-container" style="" x="-97.5078125" y="-26.25" width="195.015625" height="52.5"/><g class="label" style="" transform="translate(-67.5078125, -11.25)"><rect/><foreignObject width="135.015625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Root cause analysis</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-02e1152b1ac5-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-02e1152b1ac5-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-02e1152b1ac5-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p>В этой триаде SLI — это специально выделенные метрики из мониторинга, которые отражают пользовательский опыт. SLO — правило поверх SLI, которое мы хотим выполнять. Алерты по burn-rate — правила поверх SLO. Observability — страховка на случай, когда SLO сгорает, и нужно понять, <strong>почему именно сейчас и в этом запросе</strong>. Иначе говоря: мониторинг, SLI и SLO живут в плоскости «всё ли в порядке по тем критериям, которые мы назначили». Observability — в плоскости «что именно сейчас не так и почему», когда привычные метрики не дают ответа.</p>
<p>Теперь, с этим словарём — переходим к самому SLI.</p>
<h3 id="что-такое-sli">Что такое SLI</h3>
<p><strong>SLI (Service Level Indicator)</strong> — измеримая характеристика, которая отражает качество одного аспекта обслуживания пользователя. Ключевое слово — <strong>отражает</strong>. SLI меряет не «что система делает», а «насколько хорошо это видит пользователь».</p>
<p>Каноническая формула SLI — доля «хороших» событий от всех релевантных:</p>
<div class="math-block" data-png="https://jtprog.ru/math/a74101bc04f99ea2ff464350151f8a30174f6ef8290318bef84215f4765ab7cf.png"><span class="math__tex">$$ \text{SLI} = \dfrac{\text{количество хороших событий}}{\text{количество всех релевантных событий}} \times 100\% $$</span></div><p>Что такое «хорошее событие» и какие события «релевантны» — определяется <strong>явно</strong>, иначе SLI получится бесполезным. Самая частая ошибка здесь — посчитать «все HTTP 2xx» как «хорошие». Так делать нельзя по двум причинам:</p>
<ol>
<li><strong>Не все 2xx одинаковы.</strong> Запрос, ответивший <code>200 OK</code> за 5 секунд, формально успешен, но для пользователя это провал. SLI должен учитывать порог по латентности, иначе он маскирует деградацию.</li>
<li><strong>Не все запросы одинаковы.</strong> Health-чек с другого сервиса тоже летит как HTTP-запрос, но на пользовательский опыт не влияет. Если он попал в знаменатель — SLI размылся.</li>
</ol>
<h3 id="формат-хорошего-sli">Формат хорошего SLI</h3>
<p>У хорошего SLI всегда три явные части:</p>
<ul>
<li><strong>Числитель.</strong> Что считаем «хорошим» — порог по времени, корректности, объёму. Должно быть ясно, что туда входит, а что нет.</li>
<li><strong>Знаменатель.</strong> Какие события считаем релевантными — фильтр по эндпоинту, источнику трафика, типу запроса. Должно быть ясно, какие исключаем.</li>
<li><strong>Окно агрегации.</strong> За какой период считаем — 5 минут для алертов, 30 дней для отчётности. Без окна SLI — не число, а описание формы.</li>
</ul>
<h3 id="конкретные-sli-для-нашего-кейса">Конкретные SLI для нашего кейса</h3>
<p>Возьмём ключевые шаги из таблицы НФТ и сформулируем для них SLI. Останавливаюсь на четырёх — этого достаточно, чтобы показать механику; в реальном проде их обычно 4–8 на сервис.</p>
<p><strong>SLI-1 — Latency-SLI на шаге 4 «получение выдачи».</strong></p>
<blockquote><p>Доля поисковых запросов, на которые система ответила за время <code>&lt; 500 ms</code>, от всех релевантных поисковых запросов с пользовательского трафика, за окно 30 дней.</p>
</blockquote><ul>
<li><strong>Числитель:</strong> запросы с <code>status in {200, 204}</code> AND <code>request_duration &lt; 500 ms</code>.</li>
<li><strong>Знаменатель:</strong> запросы с <code>status in {200, 204, 5xx}</code> плюс таймауты, исключая 4xx-валидационные ответы (всё, что не health-check, не synthetic-проба, не вызов внутреннего сервиса).</li>
<li><strong>Окно:</strong> 30 дней rolling.</li>
</ul>
<p>Заметь две вещи. Во-первых, <strong>5xx в знаменателе</strong>: не учитывать их — значит нарисовать оптимистичную картинку, в которой провал «технически не считается». Во-вторых, <strong>4xx-ответы пользовательского трафика в знаменатель не идут</strong> — по канонической Google SRE-практике (глава «Service Level Objectives» в SRE Workbook, O&rsquo;Reilly, 2018) это корректное отвержение невалидного запроса (например, поисковая строка пустая или превышает лимит длины), система отработала ожидаемо. Включать их в знаменатель — раздуть его и сделать SLI оптимистичнее, чем он есть на самом деле.</p>
<p><strong>SLI-2 — Availability-SLI на шаге 4.</strong></p>
<blockquote><p>Доля поисковых запросов, завершившихся успешно (не 5xx, не таймаут), от всех релевантных запросов, за окно 30 дней.</p>
</blockquote><p>Это «классическая» availability. Но смотрим только на пользовательский трафик в шаг 4, не «по системе целиком». Знаменатель тот же, что у SLI-1, — 4xx-валидационные ответы исключены.</p>
<p><strong>SLI-3 — Correctness-SLI на шаге 4.</strong></p>
<blockquote><p>Доля поисковых запросов, вернувших непустую и валидную выдачу, от всех запросов, для которых должна была быть хотя бы одна позиция (не 0-result-by-design).</p>
</blockquote><p>Этот SLI сложнее, потому что нужно знать «должна была быть выдача или нет». Меряется офлайн-сэмплингом: берём X% запросов, прогоняем эталонный поиск, сравниваем долю «должно быть, но не было».</p>
<p><strong>SLI-4 — Freshness-SLI на шаге 6 «клик в карточку».</strong></p>
<blockquote><p>Доля переходов в карточку, на которых цена и наличие отображены не старше 60 секунд от истинного состояния товара.</p>
</blockquote><p>Этот SLI напрямую защищает от ситуации «увидел одну цену в выдаче — на чекауте другая». Без него ты рискуешь не техническим инцидентом, а <strong>репутационным</strong>: пользователь зол, возвраты растут, рейтинг продавцов падает.</p>
<p>Аналогичные SLI строятся для autocomplete, фильтра, отправки запроса по той же механике — повторять не буду. Достаточно принципа: один шаг — один или несколько SLI, каждый с явным числителем, знаменателем и окном.</p>
<h3 id="что-мы-не-берём-как-sli">Что мы НЕ берём как SLI</h3>
<p>Контрпримеры, чтобы калибровать чутьё:</p>
<ul>
<li><strong>CPU/Memory utilization.</strong> Это <strong>служебный сигнал</strong>, а не SLI. Пользователь не страдает от того, что у тебя CPU 80% — он страдает только если это превратилось в latency или 5xx. Утилизация — причина, SLI — следствие.</li>
<li><strong>Количество подов в реплике.</strong> Тоже служебный. Метрика для capacity planning, не для SLO.</li>
<li><strong>Среднее время ответа.</strong> Опасный «псевдо-SLI». Скрывает хвосты — а хвосты это и есть пользовательская боль. Только перцентили: P95 как минимум, P99 для критичных шагов.</li>
</ul>
<p>Дальше — превращаем SLI в обещание самим себе. То есть в SLO.</p>
<h2 id="шаг-4-slo-как-контракт-и-error-budget-математически">Шаг 4: SLO как контракт и error budget математически</h2>
<p><strong>SLO (Service Level Objective)</strong> — целевое значение SLI на оговорённом окне. Если SLI отвечает на вопрос «как было», то SLO — «как мы обещаем, что должно быть».</p>
<p>Формула:</p>
<div class="math-block" data-png="https://jtprog.ru/math/7b1d270e8247f093cbfecceb9778ab8cbde0cc290ff4846a300013fc0637c0c3.png"><span class="math__tex">$$ \text{SLO}: \text{SLI} \geq T \text{ (на окне } W) $$</span></div><p>Где <code>T</code> — таргет (например, <code>99.9</code>), <code>W</code> — окно агрегации (например, 30 дней rolling).</p>
<h3 id="slo-для-нашего-sli-1">SLO для нашего SLI-1</h3>
<p>Берём SLI-1 (latency-SLI поиска: доля запросов <code>&lt; 500 ms</code>):</p>
<blockquote><p>SLO: 99.9% поисковых запросов отвечают быстрее <code>500 ms</code> на rolling-окне 30 дней.</p>
</blockquote><p>Это полноценный контракт. И он автоматически рождает <strong>error budget</strong>.</p>
<h3 id="error-budget-математически">Error budget математически</h3>
<p>Error budget — это <strong>разрешённая доля «плохих» событий</strong> за период:</p>
<div class="math-block" data-png="https://jtprog.ru/math/ab13e9de75fcb2d92b3ef12a4747772aac7b91387d8ae6db1efca6eef589240f.png"><span class="math__tex">$$ \text{Error Budget} = 1 - \text{SLO} $$</span></div><p>Для SLO <code>99.9%</code> это <code>0.1%</code>, или 1 «плохой» запрос из 1000.</p>
<p>Дальше переводим в абсолютные значения. Берём типовой день нашего маркетплейса:</p>
<ul>
<li>Пиковый RPS: <code>30 000 запросов/сек</code>.</li>
<li>Средний по дню (с учётом ночи): <code>~10 000 запросов/сек</code>.</li>
<li>Запросов в сутки: <code>10 000 × 86 400 ≈ 864 000 000</code>.</li>
<li>Запросов за 30-дневное окно: <code>~26 миллиардов</code>.</li>
<li>Допустимая доля «плохих» при SLO <code>99.9%</code>: <code>0.1%</code>.</li>
<li><strong>Бюджет ошибок за 30 дней: ~26 миллионов запросов.</strong></li>
</ul>
<p>Если перевести в эквивалент простоя — двумя срезами, на пике и при средней нагрузке за сутки:</p>
<table>
	<thead>
			<tr>
					<th>SLO</th>
					<th>Бюджет ошибок</th>
					<th>На пике (30k RPS)</th>
					<th>При средней нагрузке (~10k RPS)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>99.0%</td>
					<td>~260M запросов</td>
					<td>~2 ч 24 мин</td>
					<td>~7 ч 13 мин</td>
			</tr>
			<tr>
					<td>99.9%</td>
					<td>~26M запросов</td>
					<td>~14 мин 27 с</td>
					<td>~43 мин 20 с</td>
			</tr>
			<tr>
					<td>99.99%</td>
					<td>~2.6M запросов</td>
					<td>~1 мин 27 с</td>
					<td>~4 мин 20 с</td>
			</tr>
			<tr>
					<td>99.999%</td>
					<td>~260K запросов</td>
					<td>~8.7 с</td>
					<td>~26 с</td>
			</tr>
	</tbody>
</table>
<p>Две колонки — потому что бюджет ошибок «горит» неравномерно. На пике RPS ты расходуешь запросы в три раза быстрее, чем в среднем по суткам, — поэтому инцидент в Чёрную пятницу обходится сильно дороже, чем тот же инцидент во вторник в 4 утра. Колонка «На пике» — про худший случай, на который надо проектировать SLO; колонка «При средней нагрузке» — про календарный «эквивалент полного простоя», который чаще видят в отчётах.</p>
<p>Это не просто разные числа — это разные <strong>физические возможности</strong>. На пике 8.7 секунд в месяц означают, что человек не успеет даже заметить пейдж: реакция должна быть автоматической. 7 часов в месяц при средней нагрузке — означают, что можно позволить себе ручной разбор каждого инцидента.</p>
<h3 id="что-error-budget-значит-на-практике">Что error budget значит на практике</h3>
<p>Бюджет — не «допустимый ущерб», а <strong>бюджет на риск</strong>. На него тратятся:</p>
<ul>
<li><strong>Деплои.</strong> Каждый релиз — ставка из бюджета. Откат + время на восстановление = израсходованные ошибки.</li>
<li><strong>Эксперименты.</strong> A/B-тесты, постепенный rollout, фиче-флаги — потенциальные источники регрессии.</li>
<li><strong>Учения.</strong> Game days, chaos engineering, плановые отказы — расходуют бюджет, но взамен дают уверенность в стабильности на реальных инцидентах.</li>
</ul>
<p>Если бюджет полностью сгорел до конца окна — это сигнал к <strong>freeze: остановить рискованные деплои и вкладываться в reliability</strong>, пока окно не сдвинется. Если бюджет жирный — это <strong>разрешение рисковать</strong>: ускорять выкатки, экспериментировать, рефакторить.</p>
<p>К теме «бюджет как валюта в спорах» вернусь в разделе про error budget как арбитр. Сейчас важно зафиксировать одно: <strong>SLO без error budget — это пожелание, а не контракт.</strong> Бюджет — то, что превращает SLO из табло в инструмент принятия решений.</p>
<p>Дальше — главный технический разбор статьи. Берём один и тот же поиск маркетплейса и прокручиваем сквозь три уровня SLO: 99.0%, 99.9%, 99.99%. Покажу, что меняется в архитектуре, что — в стоимости, что — в эксплуатационной нагрузке.</p>
<h2 id="один-кейс--три-slo--три-архитектуры">Один кейс — три SLO — три архитектуры</h2>
<p>Дальше беру тот же поиск маркетплейса с теми же параметрами и проектирую <strong>три разные системы</strong>. Меняется структура read path, выбор технологий, capacity, RTO/RPO, эксплуатационная модель. Остаётся постановка задачи, объём каталога (50M), пиковый RPS (30k), 5 регионов, 70% mobile.</p>
<p>Структура каждого подраздела одинаковая: бюджет → схема read path → числа (capacity, RTO/RPO, кэш, индекс) → что добавилось по сравнению с предыдущим уровнем и зачем → чего намеренно НЕ делаем.</p>
<h3 id="slo-990-дешёвая-надёжность-для-не-критичных-сценариев">SLO 99.0%: «дешёвая надёжность» для не-критичных сценариев</h3>
<p><strong>Бюджет:</strong> ~260M «плохих» запросов за 30 дней, или <strong>~2 ч 24 мин</strong> полного даунтайма на пике / <strong>~7 ч 13 мин</strong> при средней нагрузке. Почти полный рабочий день на инциденты в месяц — много, но выживаемо при ручной операционной модели.</p>
<p><strong>Когда уместно:</strong> внутренние инструменты, ранний MVP стартапа, неосновные feature-paths в большом продукте (например, search в каталоге сторонних услуг, не кормящий основную конверсию).</p>
<p><strong>Read path:</strong></p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/eda2ce005e57ab15f25c9baafc544fd9eb31156ffefe8f0a9a71a28ac37b44f3.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-eda2ce005e57-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1028.84px; background-color: transparent;" viewBox="0 0 1028.84375 138.9862823486328" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-eda2ce005e57-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-eda2ce005e57-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-light .error-icon{fill:#dce0e8;}#m-eda2ce005e57-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-eda2ce005e57-light .edge-thickness-normal{stroke-width:1px;}#m-eda2ce005e57-light .edge-thickness-thick{stroke-width:3.5px;}#m-eda2ce005e57-light .edge-pattern-solid{stroke-dasharray:0;}#m-eda2ce005e57-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-eda2ce005e57-light .edge-pattern-dashed{stroke-dasharray:3;}#m-eda2ce005e57-light .edge-pattern-dotted{stroke-dasharray:2;}#m-eda2ce005e57-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-eda2ce005e57-light .marker.cross{stroke:#5c5f77;}#m-eda2ce005e57-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-eda2ce005e57-light p{margin:0;}#m-eda2ce005e57-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-eda2ce005e57-light .cluster-label text{fill:#4c4f69;}#m-eda2ce005e57-light .cluster-label span{color:#4c4f69;}#m-eda2ce005e57-light .cluster-label span p{background-color:transparent;}#m-eda2ce005e57-light .label text,#m-eda2ce005e57-light span{fill:#4c4f69;color:#4c4f69;}#m-eda2ce005e57-light .node rect,#m-eda2ce005e57-light .node circle,#m-eda2ce005e57-light .node ellipse,#m-eda2ce005e57-light .node polygon,#m-eda2ce005e57-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-eda2ce005e57-light .rough-node .label text,#m-eda2ce005e57-light .node .label text,#m-eda2ce005e57-light .image-shape .label,#m-eda2ce005e57-light .icon-shape .label{text-anchor:middle;}#m-eda2ce005e57-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-eda2ce005e57-light .rough-node .label,#m-eda2ce005e57-light .node .label,#m-eda2ce005e57-light .image-shape .label,#m-eda2ce005e57-light .icon-shape .label{text-align:center;}#m-eda2ce005e57-light .node.clickable{cursor:pointer;}#m-eda2ce005e57-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-eda2ce005e57-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-eda2ce005e57-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-eda2ce005e57-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-eda2ce005e57-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-eda2ce005e57-light .edgeLabel p{background-color:#eff1f5;}#m-eda2ce005e57-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-eda2ce005e57-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-eda2ce005e57-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-eda2ce005e57-light .cluster text{fill:#4c4f69;}#m-eda2ce005e57-light .cluster span{color:#4c4f69;}#m-eda2ce005e57-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-eda2ce005e57-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-eda2ce005e57-light rect.text{fill:none;stroke-width:0;}#m-eda2ce005e57-light .icon-shape,#m-eda2ce005e57-light .image-shape{background-color:#eff1f5;text-align:center;}#m-eda2ce005e57-light .icon-shape p,#m-eda2ce005e57-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-eda2ce005e57-light .icon-shape .label rect,#m-eda2ce005e57-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-eda2ce005e57-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-eda2ce005e57-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-eda2ce005e57-light .node .neo-node{stroke:#bcc0cc;}#m-eda2ce005e57-light [data-look="neo"].node rect,#m-eda2ce005e57-light [data-look="neo"].cluster rect,#m-eda2ce005e57-light [data-look="neo"].node polygon{stroke:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].node path{stroke:url(#m-eda2ce005e57-light-gradient);stroke-width:1px;}#m-eda2ce005e57-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-eda2ce005e57-light [data-look="neo"].node circle{stroke:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-eda2ce005e57-light [data-look="neo"].icon-shape .icon{fill:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-eda2ce005e57-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M163.348,55.5L168.157,53.833C172.966,52.167,182.585,48.833,190.894,47.167C199.203,45.5,206.203,45.5,209.703,45.5L213.203,45.5" id="m-eda2ce005e57-light-L_U_LB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_LB_0" data-points="W3sieCI6MTYzLjM0NzUyMTU1MTcyNDEzLCJ5Ijo1NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjQ1LjV9LHsieCI6MjE3LjIwMzEyNSwieSI6NDUuNX1d" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M374.516,45.5L378.682,45.5C382.849,45.5,391.182,45.5,398.884,46.69C406.585,47.879,413.655,50.259,417.19,51.448L420.725,52.638" id="m-eda2ce005e57-light-L_LB_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_LB_A_0" data-points="W3sieCI6Mzc0LjUxNTYyNSwieSI6NDUuNX0seyJ4IjozOTkuNTE1NjI1LCJ5Ijo0NS41fSx7IngiOjQyNC41MTU2MjUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M589.938,53.914L594.104,52.511C598.271,51.109,606.604,48.305,614.271,46.902C621.938,45.5,628.938,45.5,632.438,45.5L635.938,45.5" id="m-eda2ce005e57-light-L_A_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_C_0" data-points="W3sieCI6NTg5LjkzNzUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fSx7IngiOjYxNC45Mzc1LCJ5Ijo0NS41fSx7IngiOjYzOS45Mzc1LCJ5Ijo0NS41fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M807.797,45.5L814.579,45.5C821.362,45.5,834.927,45.5,847.861,47.593C860.795,49.687,873.098,53.873,879.249,55.967L885.401,58.06" id="m-eda2ce005e57-light-L_C_P_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_C_P_0" data-points="W3sieCI6ODA3Ljc5Njg3NSwieSI6NDUuNX0seyJ4Ijo4NDguNDkyMTg3NSwieSI6NDUuNX0seyJ4Ijo4ODkuMTg3NSwieSI6NTkuMzQ4NjQzMTk3NjUzMTA0fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M889.188,104.151L882.405,106.459C875.622,108.768,862.057,113.384,834.504,115.692C806.951,118,765.409,118,726.483,118C687.557,118,651.247,118,629.558,116.81C607.868,115.621,600.798,113.241,597.263,112.052L593.729,110.862" id="m-eda2ce005e57-light-L_P_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_P_A_0" data-points="W3sieCI6ODg5LjE4NzUsInkiOjEwNC4xNTEzNTY4MDIzNDY5fSx7IngiOjg0OC40OTIxODc1LCJ5IjoxMTh9LHsieCI6NzIzLjg2NzE4NzUsInkiOjExOH0seyJ4Ijo2MTQuOTM3NSwieSI6MTE4fSx7IngiOjU4OS45Mzc1LCJ5IjoxMDkuNTg2Mjc2OTI3NTQwNDR9XQ==" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M424.516,109.586L420.349,110.989C416.182,112.391,407.849,115.195,386.406,116.598C364.964,118,330.411,118,295.859,118C261.307,118,226.755,118,205.3,116.552C183.844,115.103,175.486,112.207,171.306,110.758L167.127,109.31" id="m-eda2ce005e57-light-L_A_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_U_0" data-points="W3sieCI6NDI0LjUxNTYyNSwieSI6MTA5LjU4NjI3NjkyNzU0MDQ0fSx7IngiOjM5OS41MTU2MjUsInkiOjExOH0seyJ4IjoyOTUuODU5Mzc1LCJ5IjoxMTh9LHsieCI6MTkyLjIwMzEyNSwieSI6MTE4fSx7IngiOjE2My4zNDc1MjE1NTE3MjQxMywieSI6MTA4fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_LB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_LB_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_C_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(848.4921875, 45.5)"><g class="label" data-id="L_C_P_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_P_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-eda2ce005e57-light-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 81.75)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-LB-1" data-look="classic" transform="translate(295.859375, 45.5)"><rect class="basic label-container" style="" x="-78.65625" y="-37.5" width="157.3125" height="75"/><g class="label" style="" transform="translate(-48.65625, -22.5)"><rect/><foreignObject width="97.3125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Load Balancer<br />1 регион</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-A-3" data-look="classic" transform="translate(507.2265625, 81.75)"><rect class="basic label-container" style="" x="-82.7109375" y="-37.5" width="165.421875" height="75"/><g class="label" style="" transform="translate(-52.7109375, -22.5)"><rect/><foreignObject width="105.421875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>App-сервер<br />3–5 инстансов</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-C-5" data-look="classic" transform="translate(723.8671875, 45.5)"><rect class="basic label-container" style="" x="-83.9296875" y="-37.5" width="167.859375" height="75"/><g class="label" style="" transform="translate(-53.9296875, -22.5)"><rect/><foreignObject width="107.859375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>In-memory LRU<br />per-instance</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-P-7" data-look="classic" transform="translate(955.015625, 81.75)"><path d="M0,12.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 a65.828125,12.8241811761841 0,0,0 -131.65625,0 l0,72.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 l0,-72.8241811761841" class="basic label-container outer-path" style="" transform="translate(-65.828125, -49.23627176427615)"/><g class="label" style="" transform="translate(-58.328125, -12.5)"><rect/><foreignObject width="116.65625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres primary<br />+ 1 read replica</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-eda2ce005e57-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-eda2ce005e57-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-eda2ce005e57-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-eda2ce005e57-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1028.84px; background-color: transparent;" viewBox="0 0 1028.84375 138.9862823486328" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-eda2ce005e57-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-eda2ce005e57-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-dark .error-icon{fill:#181926;}#m-eda2ce005e57-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-eda2ce005e57-dark .edge-thickness-normal{stroke-width:1px;}#m-eda2ce005e57-dark .edge-thickness-thick{stroke-width:3.5px;}#m-eda2ce005e57-dark .edge-pattern-solid{stroke-dasharray:0;}#m-eda2ce005e57-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-eda2ce005e57-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-eda2ce005e57-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-eda2ce005e57-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-eda2ce005e57-dark .marker.cross{stroke:#a5adcb;}#m-eda2ce005e57-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-eda2ce005e57-dark p{margin:0;}#m-eda2ce005e57-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-eda2ce005e57-dark .cluster-label text{fill:#cad3f5;}#m-eda2ce005e57-dark .cluster-label span{color:#cad3f5;}#m-eda2ce005e57-dark .cluster-label span p{background-color:transparent;}#m-eda2ce005e57-dark .label text,#m-eda2ce005e57-dark span{fill:#cad3f5;color:#cad3f5;}#m-eda2ce005e57-dark .node rect,#m-eda2ce005e57-dark .node circle,#m-eda2ce005e57-dark .node ellipse,#m-eda2ce005e57-dark .node polygon,#m-eda2ce005e57-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-eda2ce005e57-dark .rough-node .label text,#m-eda2ce005e57-dark .node .label text,#m-eda2ce005e57-dark .image-shape .label,#m-eda2ce005e57-dark .icon-shape .label{text-anchor:middle;}#m-eda2ce005e57-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-eda2ce005e57-dark .rough-node .label,#m-eda2ce005e57-dark .node .label,#m-eda2ce005e57-dark .image-shape .label,#m-eda2ce005e57-dark .icon-shape .label{text-align:center;}#m-eda2ce005e57-dark .node.clickable{cursor:pointer;}#m-eda2ce005e57-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-eda2ce005e57-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-eda2ce005e57-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-eda2ce005e57-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-eda2ce005e57-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-eda2ce005e57-dark .edgeLabel p{background-color:#24273a;}#m-eda2ce005e57-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-eda2ce005e57-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-eda2ce005e57-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-eda2ce005e57-dark .cluster text{fill:#cad3f5;}#m-eda2ce005e57-dark .cluster span{color:#cad3f5;}#m-eda2ce005e57-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-eda2ce005e57-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-eda2ce005e57-dark rect.text{fill:none;stroke-width:0;}#m-eda2ce005e57-dark .icon-shape,#m-eda2ce005e57-dark .image-shape{background-color:#24273a;text-align:center;}#m-eda2ce005e57-dark .icon-shape p,#m-eda2ce005e57-dark .image-shape p{background-color:#24273a;padding:2px;}#m-eda2ce005e57-dark .icon-shape .label rect,#m-eda2ce005e57-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-eda2ce005e57-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-eda2ce005e57-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-eda2ce005e57-dark .node .neo-node{stroke:#494d64;}#m-eda2ce005e57-dark [data-look="neo"].node rect,#m-eda2ce005e57-dark [data-look="neo"].cluster rect,#m-eda2ce005e57-dark [data-look="neo"].node polygon{stroke:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].node path{stroke:url(#m-eda2ce005e57-dark-gradient);stroke-width:1px;}#m-eda2ce005e57-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-eda2ce005e57-dark [data-look="neo"].node circle{stroke:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-eda2ce005e57-dark [data-look="neo"].icon-shape .icon{fill:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-eda2ce005e57-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M163.348,55.5L168.157,53.833C172.966,52.167,182.585,48.833,190.894,47.167C199.203,45.5,206.203,45.5,209.703,45.5L213.203,45.5" id="m-eda2ce005e57-dark-L_U_LB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_LB_0" data-points="W3sieCI6MTYzLjM0NzUyMTU1MTcyNDEzLCJ5Ijo1NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjQ1LjV9LHsieCI6MjE3LjIwMzEyNSwieSI6NDUuNX1d" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M374.516,45.5L378.682,45.5C382.849,45.5,391.182,45.5,398.884,46.69C406.585,47.879,413.655,50.259,417.19,51.448L420.725,52.638" id="m-eda2ce005e57-dark-L_LB_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_LB_A_0" data-points="W3sieCI6Mzc0LjUxNTYyNSwieSI6NDUuNX0seyJ4IjozOTkuNTE1NjI1LCJ5Ijo0NS41fSx7IngiOjQyNC41MTU2MjUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M589.938,53.914L594.104,52.511C598.271,51.109,606.604,48.305,614.271,46.902C621.938,45.5,628.938,45.5,632.438,45.5L635.938,45.5" id="m-eda2ce005e57-dark-L_A_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_C_0" data-points="W3sieCI6NTg5LjkzNzUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fSx7IngiOjYxNC45Mzc1LCJ5Ijo0NS41fSx7IngiOjYzOS45Mzc1LCJ5Ijo0NS41fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M807.797,45.5L814.579,45.5C821.362,45.5,834.927,45.5,847.861,47.593C860.795,49.687,873.098,53.873,879.249,55.967L885.401,58.06" id="m-eda2ce005e57-dark-L_C_P_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_C_P_0" data-points="W3sieCI6ODA3Ljc5Njg3NSwieSI6NDUuNX0seyJ4Ijo4NDguNDkyMTg3NSwieSI6NDUuNX0seyJ4Ijo4ODkuMTg3NSwieSI6NTkuMzQ4NjQzMTk3NjUzMTA0fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M889.188,104.151L882.405,106.459C875.622,108.768,862.057,113.384,834.504,115.692C806.951,118,765.409,118,726.483,118C687.557,118,651.247,118,629.558,116.81C607.868,115.621,600.798,113.241,597.263,112.052L593.729,110.862" id="m-eda2ce005e57-dark-L_P_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_P_A_0" data-points="W3sieCI6ODg5LjE4NzUsInkiOjEwNC4xNTEzNTY4MDIzNDY5fSx7IngiOjg0OC40OTIxODc1LCJ5IjoxMTh9LHsieCI6NzIzLjg2NzE4NzUsInkiOjExOH0seyJ4Ijo2MTQuOTM3NSwieSI6MTE4fSx7IngiOjU4OS45Mzc1LCJ5IjoxMDkuNTg2Mjc2OTI3NTQwNDR9XQ==" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M424.516,109.586L420.349,110.989C416.182,112.391,407.849,115.195,386.406,116.598C364.964,118,330.411,118,295.859,118C261.307,118,226.755,118,205.3,116.552C183.844,115.103,175.486,112.207,171.306,110.758L167.127,109.31" id="m-eda2ce005e57-dark-L_A_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_U_0" data-points="W3sieCI6NDI0LjUxNTYyNSwieSI6MTA5LjU4NjI3NjkyNzU0MDQ0fSx7IngiOjM5OS41MTU2MjUsInkiOjExOH0seyJ4IjoyOTUuODU5Mzc1LCJ5IjoxMTh9LHsieCI6MTkyLjIwMzEyNSwieSI6MTE4fSx7IngiOjE2My4zNDc1MjE1NTE3MjQxMywieSI6MTA4fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_LB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_LB_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_C_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(848.4921875, 45.5)"><g class="label" data-id="L_C_P_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_P_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-eda2ce005e57-dark-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 81.75)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-LB-1" data-look="classic" transform="translate(295.859375, 45.5)"><rect class="basic label-container" style="" x="-78.65625" y="-37.5" width="157.3125" height="75"/><g class="label" style="" transform="translate(-48.65625, -22.5)"><rect/><foreignObject width="97.3125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Load Balancer<br />1 регион</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-A-3" data-look="classic" transform="translate(507.2265625, 81.75)"><rect class="basic label-container" style="" x="-82.7109375" y="-37.5" width="165.421875" height="75"/><g class="label" style="" transform="translate(-52.7109375, -22.5)"><rect/><foreignObject width="105.421875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>App-сервер<br />3–5 инстансов</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-C-5" data-look="classic" transform="translate(723.8671875, 45.5)"><rect class="basic label-container" style="" x="-83.9296875" y="-37.5" width="167.859375" height="75"/><g class="label" style="" transform="translate(-53.9296875, -22.5)"><rect/><foreignObject width="107.859375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>In-memory LRU<br />per-instance</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-P-7" data-look="classic" transform="translate(955.015625, 81.75)"><path d="M0,12.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 a65.828125,12.8241811761841 0,0,0 -131.65625,0 l0,72.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 l0,-72.8241811761841" class="basic label-container outer-path" style="" transform="translate(-65.828125, -49.23627176427615)"/><g class="label" style="" transform="translate(-58.328125, -12.5)"><rect/><foreignObject width="116.65625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres primary<br />+ 1 read replica</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-eda2ce005e57-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-eda2ce005e57-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-eda2ce005e57-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p><strong>Числовые прикидки:</strong></p>
<ul>
<li><strong>Capacity.</strong> 30k RPS пика → при <code>99.0%</code> можно потерять ~300 запросов/сек без нарушения SLO. Один хорошо настроенный Postgres primary с trigram-индексом на <code>title/description</code> тянет 1–3k RPS read на сложных запросах с фильтрами и фасетами, до 5–8k RPS — на простых коротких запросах с маленькой выборкой (P99 &lt; 200 ms, при правильном connection pooling). Read replica удваивает чтение, итого <code>~2–15k RPS</code> в зависимости от профиля. На пике 30k ты либо урежешь long-tail запросы кэшем приложения, либо примешь деградацию — бюджет позволяет.</li>
<li><strong>Кэш.</strong> In-memory LRU на каждом app-инстансе, размер <code>~256 MB</code>. Hit rate 30–40% на популярных запросах. Без Redis — отдельный сервис добавил бы операционную сложность, нерелевантную для этого SLO.</li>
<li><strong>Полнотекстовый индекс.</strong> <code>pg_trgm</code> + <code>tsvector</code> поверх 50M товаров занимают <code>3–5×</code> от размера индексируемых текстовых полей (title, description, attributes-text). Для нашего каталога с расширенными текстовыми атрибутами это порядка <code>1.5–2.5 TB</code> индексов. Реалистично на одной мощной машине (NVMe, 256 GB RAM).</li>
<li><strong>RTO.</strong> До 30–60 минут на ручной failover на реплику. Автоматизации нет — стоит отдельный сервис.</li>
<li><strong>RPO.</strong> До 5 минут (типовая асинхронная репликация Postgres).</li>
<li><strong>Алерты.</strong> На симптомы: 5xx-rate, P99 latency, replication lag. Реакция оператором, не автоматическая remediation.</li>
</ul>
<p><strong>Почему этого достаточно — счёт по бюджету.</strong> Бюджет 99.0% — это <code>~7 часов даунтайма в месяц</code> при средней нагрузке. Типовой худший случай выхода из строя primary с восстановлением из <code>pg_basebackup</code> + PITR — <code>30–90 минут</code> от пейджа до возобновления read path (включая ручное переключение приложений на реплику и прогрев кэша). Даже два таких инцидента в месяц съедают <code>~3 часа</code> — это меньше половины бюджета. Платить за синхронную репликацию, автоматический failover-tooling (Patroni / RepMgr / Stolon) и дежурную команду, способную их обслуживать, на этом уровне SLO нечем: бюджет покрывает ручной MTTR с большим запасом. На 99.9% этого запаса уже нет — отсюда и Active-passive failover на следующем уровне.</p>
<p><strong>Что намеренно НЕ делаем:</strong></p>
<ul>
<li>Не поднимаем Elasticsearch — он добавит +1 систему-источник истины, требующую отдельного индексирования, мониторинга и SLO. На 99.0% это лишний оверхед.</li>
<li>Не делаем шардинг — одна машина с 256 GB RAM в 2025 году утаскивает 50M документов с trigram-индексом без проблем.</li>
<li>Не делаем multi-region даже active-passive. Бюджет 7 часов покрывает любой региональный инцидент с ручным восстановлением.</li>
</ul>
<p><strong>Стоимость:</strong> обозначу как <strong>1× baseline</strong> для последующего сравнения.</p>
<h3 id="slo-999-рабочий-production-уровень">SLO 99.9%: рабочий production-уровень</h3>
<p><strong>Бюджет:</strong> ~26M «плохих» запросов за 30 дней, или <strong>~14 мин 27 с</strong> полного даунтайма на пике / <strong>~43 мин 20 с</strong> при средней нагрузке. На инцидент при средней нагрузке остаётся минут 20–30 — хватает на автоматический failover, но не на ручной разбор каждого 5xx.</p>
<p><strong>Когда уместно:</strong> основные пользовательские пути в среднем e-commerce, обычный production. Большинство сервисов в индустрии живут в этом диапазоне.</p>
<p><strong>Read path:</strong></p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/58b7ff999bbfb53bebce4d975ca50b9ee1edcc36aaecf424e8481a568361bcd3.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-58b7ff999bbf-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1236.55px; background-color: transparent;" viewBox="0 0 1236.546875 312.40679931640625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-58b7ff999bbf-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-58b7ff999bbf-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-light .error-icon{fill:#dce0e8;}#m-58b7ff999bbf-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-58b7ff999bbf-light .edge-thickness-normal{stroke-width:1px;}#m-58b7ff999bbf-light .edge-thickness-thick{stroke-width:3.5px;}#m-58b7ff999bbf-light .edge-pattern-solid{stroke-dasharray:0;}#m-58b7ff999bbf-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-58b7ff999bbf-light .edge-pattern-dashed{stroke-dasharray:3;}#m-58b7ff999bbf-light .edge-pattern-dotted{stroke-dasharray:2;}#m-58b7ff999bbf-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-58b7ff999bbf-light .marker.cross{stroke:#5c5f77;}#m-58b7ff999bbf-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-58b7ff999bbf-light p{margin:0;}#m-58b7ff999bbf-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-58b7ff999bbf-light .cluster-label text{fill:#4c4f69;}#m-58b7ff999bbf-light .cluster-label span{color:#4c4f69;}#m-58b7ff999bbf-light .cluster-label span p{background-color:transparent;}#m-58b7ff999bbf-light .label text,#m-58b7ff999bbf-light span{fill:#4c4f69;color:#4c4f69;}#m-58b7ff999bbf-light .node rect,#m-58b7ff999bbf-light .node circle,#m-58b7ff999bbf-light .node ellipse,#m-58b7ff999bbf-light .node polygon,#m-58b7ff999bbf-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-58b7ff999bbf-light .rough-node .label text,#m-58b7ff999bbf-light .node .label text,#m-58b7ff999bbf-light .image-shape .label,#m-58b7ff999bbf-light .icon-shape .label{text-anchor:middle;}#m-58b7ff999bbf-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-58b7ff999bbf-light .rough-node .label,#m-58b7ff999bbf-light .node .label,#m-58b7ff999bbf-light .image-shape .label,#m-58b7ff999bbf-light .icon-shape .label{text-align:center;}#m-58b7ff999bbf-light .node.clickable{cursor:pointer;}#m-58b7ff999bbf-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-58b7ff999bbf-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-58b7ff999bbf-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-58b7ff999bbf-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-58b7ff999bbf-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-58b7ff999bbf-light .edgeLabel p{background-color:#eff1f5;}#m-58b7ff999bbf-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-58b7ff999bbf-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-58b7ff999bbf-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-58b7ff999bbf-light .cluster text{fill:#4c4f69;}#m-58b7ff999bbf-light .cluster span{color:#4c4f69;}#m-58b7ff999bbf-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-58b7ff999bbf-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-58b7ff999bbf-light rect.text{fill:none;stroke-width:0;}#m-58b7ff999bbf-light .icon-shape,#m-58b7ff999bbf-light .image-shape{background-color:#eff1f5;text-align:center;}#m-58b7ff999bbf-light .icon-shape p,#m-58b7ff999bbf-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-58b7ff999bbf-light .icon-shape .label rect,#m-58b7ff999bbf-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-58b7ff999bbf-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-58b7ff999bbf-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-58b7ff999bbf-light .node .neo-node{stroke:#bcc0cc;}#m-58b7ff999bbf-light [data-look="neo"].node rect,#m-58b7ff999bbf-light [data-look="neo"].cluster rect,#m-58b7ff999bbf-light [data-look="neo"].node polygon{stroke:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].node path{stroke:url(#m-58b7ff999bbf-light-gradient);stroke-width:1px;}#m-58b7ff999bbf-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-58b7ff999bbf-light [data-look="neo"].node circle{stroke:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-58b7ff999bbf-light [data-look="neo"].icon-shape .icon{fill:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-58b7ff999bbf-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M119.801,158.43L131.868,148.593C143.935,138.756,168.069,119.081,186.669,109.244C205.268,99.407,218.333,99.407,224.866,99.407L231.398,99.407" id="m-58b7ff999bbf-light-L_U_CDN_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_CDN_0" data-points="W3sieCI6MTE5LjgwMTQ1MjU4NjIxOTkxLCJ5IjoxNTguNDMwMDg4MDQzMjEyOX0seyJ4IjoxOTIuMjAzMTI1LCJ5Ijo5OS40MDY3ODQwNTc2MTcxOX0seyJ4IjoyMzUuMzk4NDM3NSwieSI6OTkuNDA2Nzg0MDU3NjE3MTl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M126.287,210.93L137.273,218.385C148.259,225.839,170.231,240.748,184.717,248.202C199.203,255.657,206.203,255.657,209.703,255.657L213.203,255.657" id="m-58b7ff999bbf-light-L_U_GLB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GLB_0" data-points="W3sieCI6MTI2LjI4NzM3MzA1MjA2ODUsInkiOjIxMC45MzAwODgwNDMyMTI5fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjIxNy4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M422.656,255.657L426.823,255.657C430.99,255.657,439.323,255.657,446.99,255.657C454.656,255.657,461.656,255.657,465.156,255.657L468.656,255.657" id="m-58b7ff999bbf-light-L_GLB_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GLB_R_0" data-points="W3sieCI6NDIyLjY1NjI1LCJ5IjoyNTUuNjU2Nzg0MDU3NjE3Mn0seyJ4Ijo0NDcuNjU2MjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjQ3Mi42NTYyNSwieSI6MjU1LjY1Njc4NDA1NzYxNzJ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M626.813,260.518L633.618,260.947C640.424,261.376,654.036,262.235,666.997,261.32C679.958,260.406,692.267,257.718,698.422,256.375L704.576,255.031" id="m-58b7ff999bbf-light-L_R_SS_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_0" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI2MC41MTc4Mjk2ODgyNzMyfSx7IngiOjY2Ny42NDg0Mzc1LCJ5IjoyNjMuMDkzMjA4MzEyOTg4M30seyJ4Ijo3MDguNDg0Mzc1LCJ5IjoyNTQuMTc3NTUxNDE2NTMwNjZ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M883.965,194.343L898.2,187.049C912.435,179.755,940.905,165.167,963.995,151.054C987.084,136.941,1004.793,123.304,1013.648,116.485L1022.503,109.666" id="m-58b7ff999bbf-light-L_SS_ES_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_ES_0" data-points="W3sieCI6ODgzLjk2NTAyMDAxMTg5MTMsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo5NjkuMzc1LCJ5IjoxNTAuNTc4MzkyMDI4ODA4Nn0seyJ4IjoxMDI1LjY3MTg3NSwieSI6MTA3LjIyNTkxOTQ4NzY0MDl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M913.078,237.125L922.461,237.609C931.844,238.094,950.609,239.063,971.448,239.547C992.286,240.032,1015.198,240.032,1026.654,240.032L1038.109,240.032" id="m-58b7ff999bbf-light-L_SS_RR_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_RR_0" data-points="W3sieCI6OTEzLjA3ODEyNSwieSI6MjM3LjEyNTA0MTM1NzMzMTY3fSx7IngiOjk2OS4zNzUsInkiOjI0MC4wMzE3ODQwNTc2MTcyfSx7IngiOjEwNDIuMTA5Mzc1LCJ5IjoyNDAuMDMxNzg0MDU3NjE3Mn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M607.898,72.453L617.857,72.453C627.815,72.453,647.732,72.453,672.1,72.453C696.469,72.453,725.289,72.453,739.699,72.453L754.109,72.453" id="m-58b7ff999bbf-light-L_PG_Q_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PG_Q_0" data-points="W3sieCI6NjA3Ljg5ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NjY3LjY0ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NzU4LjEwOTM3NSwieSI6NzIuNDUzMzkyMDI4ODA4Nn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M863.453,72.453L881.107,72.453C898.76,72.453,934.068,72.453,960.438,72.453C986.807,72.453,1004.24,72.453,1012.956,72.453L1021.672,72.453" id="m-58b7ff999bbf-light-L_Q_ES_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Q_ES_0" data-points="W3sieCI6ODYzLjQ1MzEyNSwieSI6NzIuNDUzMzkyMDI4ODA4Nn0seyJ4Ijo5NjkuMzc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fSx7IngiOjEwMjUuNjcxODc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M626.813,240.09L633.618,238.716C640.424,237.341,654.036,234.592,666.982,233.218C679.927,231.843,692.206,231.843,698.345,231.843L704.484,231.843" id="m-58b7ff999bbf-light-L_R_SS_2" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_2" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI0MC4wOTAzMTM2MjEyMjIyNn0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MjMxLjg0MzIwODMxMjk4ODI4fSx7IngiOjcwOC40ODQzNzUsInkiOjIzMS44NDMyMDgzMTI5ODgyOH1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M721.228,194.343L712.298,190.604C703.368,186.864,685.508,179.386,656.926,175.646C628.344,171.907,589.039,171.907,552.374,171.907C515.708,171.907,481.682,171.907,443.382,171.907C405.081,171.907,362.505,171.907,319.93,171.907C277.354,171.907,234.779,171.907,209.986,172.335C185.193,172.763,178.183,173.619,174.679,174.047L171.174,174.475" id="m-58b7ff999bbf-light-L_SS_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_U_0" data-points="W3sieCI6NzIxLjIyODM1MjMxODI3MDEsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NTQ5LjczNDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NDQ3LjY1NjI1LCJ5IjoxNzEuOTA2Nzg0MDU3NjE3Mn0seyJ4IjozMTkuOTI5Njg3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTkyLjIwMzEyNSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTY3LjIwMzEyNSwieSI6MTc0Ljk1OTYzMTM3NjYwMzR9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_CDN_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_U_GLB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GLB_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(668.07871, 262.99927)"><g class="label" data-id="L_R_SS_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_ES_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_RR_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(667.6484375, 72.4533920288086)"><g class="label" data-id="L_PG_Q_0" transform="translate(-15.8359375, -11.25)"><foreignObject width="31.671875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>CDC</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(969.375, 72.4533920288086)"><g class="label" data-id="L_Q_ES_0" transform="translate(-31.296875, -11.25)"><foreignObject width="62.59375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>consume</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_R_SS_2" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-58b7ff999bbf-light-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 184.6800880432129)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-CDN-1" data-look="classic" transform="translate(319.9296875, 99.40678405761719)"><rect class="basic label-container" style="" x="-84.53125" y="-37.5" width="169.0625" height="75"/><g class="label" style="" transform="translate(-54.53125, -22.5)"><rect/><foreignObject width="109.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>CDN<br />статика, медиа</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-GLB-3" data-look="classic" transform="translate(319.9296875, 255.6567840576172)"><rect class="basic label-container" style="" x="-102.7265625" y="-48.75" width="205.453125" height="97.5"/><g class="label" style="" transform="translate(-72.7265625, -33.75)"><rect/><foreignObject width="145.453125" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Global LB<br />гео-роутинг<br />в 1 активный регион</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-R-5" data-look="classic" transform="translate(549.734375, 255.6567840576172)"><rect class="basic label-container" style="" x="-77.078125" y="-48.75" width="154.15625" height="97.5"/><g class="label" style="" transform="translate(-47.078125, -33.75)"><rect/><foreignObject width="94.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Redis Cluster<br />top queries +<br />autocomplete</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-SS-7" data-look="classic" transform="translate(810.78125, 231.84320831298828)"><rect class="basic label-container" style="" x="-102.296875" y="-37.5" width="204.59375" height="75"/><g class="label" style="" transform="translate(-72.296875, -22.5)"><rect/><foreignObject width="144.59375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Search Service<br />app-tier, 12–20 инст.</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-ES-9" data-look="classic" transform="translate(1127.109375, 72.4533920288086)"><path d="M0,15.468928707586732 a101.4375,15.468928707586732 0,0,0 202.875,0 a101.4375,15.468928707586732 0,0,0 -202.875,0 l0,97.96892870758673 a101.4375,15.468928707586732 0,0,0 202.875,0 l0,-97.96892870758673" class="basic label-container outer-path" style="" transform="translate(-101.4375, -64.4533930613801)"/><g class="label" style="" transform="translate(-93.9375, -23.75)"><rect/><foreignObject width="187.875" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Elasticsearch<br />8–12 нод, 6 primary shards<br />× 2 replicas</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-RR-11" data-look="classic" transform="translate(1127.109375, 240.0317840576172)"><rect class="basic label-container" style="" x="-85" y="-37.5" width="170" height="75"/><g class="label" style="" transform="translate(-55, -22.5)"><rect/><foreignObject width="110" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Ranking Service<br />отдельный SLO</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-PG-12" data-look="classic" transform="translate(549.734375, 72.4533920288086)"><path d="M0,12.050825509873746 a58.1640625,12.050825509873746 0,0,0 116.328125,0 a58.1640625,12.050825509873746 0,0,0 -116.328125,0 l0,72.05082550987375 a58.1640625,12.050825509873746 0,0,0 116.328125,0 l0,-72.05082550987375" class="basic label-container outer-path" style="" transform="translate(-58.1640625, -48.07623826481062)"/><g class="label" style="" transform="translate(-50.6640625, -12.5)"><rect/><foreignObject width="101.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres<br />source of truth</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-Q-13" data-look="classic" transform="translate(810.78125, 72.4533920288086)"><rect class="basic label-container" style="" x="-52.671875" y="-37.5" width="105.34375" height="75"/><g class="label" style="" transform="translate(-22.671875, -22.5)"><rect/><foreignObject width="45.34375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Kafka<br />events</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-58b7ff999bbf-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-58b7ff999bbf-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-58b7ff999bbf-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-58b7ff999bbf-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1236.55px; background-color: transparent;" viewBox="0 0 1236.546875 312.40679931640625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-58b7ff999bbf-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-58b7ff999bbf-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-dark .error-icon{fill:#181926;}#m-58b7ff999bbf-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-58b7ff999bbf-dark .edge-thickness-normal{stroke-width:1px;}#m-58b7ff999bbf-dark .edge-thickness-thick{stroke-width:3.5px;}#m-58b7ff999bbf-dark .edge-pattern-solid{stroke-dasharray:0;}#m-58b7ff999bbf-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-58b7ff999bbf-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-58b7ff999bbf-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-58b7ff999bbf-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-58b7ff999bbf-dark .marker.cross{stroke:#a5adcb;}#m-58b7ff999bbf-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-58b7ff999bbf-dark p{margin:0;}#m-58b7ff999bbf-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-58b7ff999bbf-dark .cluster-label text{fill:#cad3f5;}#m-58b7ff999bbf-dark .cluster-label span{color:#cad3f5;}#m-58b7ff999bbf-dark .cluster-label span p{background-color:transparent;}#m-58b7ff999bbf-dark .label text,#m-58b7ff999bbf-dark span{fill:#cad3f5;color:#cad3f5;}#m-58b7ff999bbf-dark .node rect,#m-58b7ff999bbf-dark .node circle,#m-58b7ff999bbf-dark .node ellipse,#m-58b7ff999bbf-dark .node polygon,#m-58b7ff999bbf-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-58b7ff999bbf-dark .rough-node .label text,#m-58b7ff999bbf-dark .node .label text,#m-58b7ff999bbf-dark .image-shape .label,#m-58b7ff999bbf-dark .icon-shape .label{text-anchor:middle;}#m-58b7ff999bbf-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-58b7ff999bbf-dark .rough-node .label,#m-58b7ff999bbf-dark .node .label,#m-58b7ff999bbf-dark .image-shape .label,#m-58b7ff999bbf-dark .icon-shape .label{text-align:center;}#m-58b7ff999bbf-dark .node.clickable{cursor:pointer;}#m-58b7ff999bbf-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-58b7ff999bbf-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-58b7ff999bbf-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-58b7ff999bbf-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-58b7ff999bbf-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-58b7ff999bbf-dark .edgeLabel p{background-color:#24273a;}#m-58b7ff999bbf-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-58b7ff999bbf-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-58b7ff999bbf-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-58b7ff999bbf-dark .cluster text{fill:#cad3f5;}#m-58b7ff999bbf-dark .cluster span{color:#cad3f5;}#m-58b7ff999bbf-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-58b7ff999bbf-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-58b7ff999bbf-dark rect.text{fill:none;stroke-width:0;}#m-58b7ff999bbf-dark .icon-shape,#m-58b7ff999bbf-dark .image-shape{background-color:#24273a;text-align:center;}#m-58b7ff999bbf-dark .icon-shape p,#m-58b7ff999bbf-dark .image-shape p{background-color:#24273a;padding:2px;}#m-58b7ff999bbf-dark .icon-shape .label rect,#m-58b7ff999bbf-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-58b7ff999bbf-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-58b7ff999bbf-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-58b7ff999bbf-dark .node .neo-node{stroke:#494d64;}#m-58b7ff999bbf-dark [data-look="neo"].node rect,#m-58b7ff999bbf-dark [data-look="neo"].cluster rect,#m-58b7ff999bbf-dark [data-look="neo"].node polygon{stroke:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].node path{stroke:url(#m-58b7ff999bbf-dark-gradient);stroke-width:1px;}#m-58b7ff999bbf-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-58b7ff999bbf-dark [data-look="neo"].node circle{stroke:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-58b7ff999bbf-dark [data-look="neo"].icon-shape .icon{fill:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M119.801,158.43L131.868,148.593C143.935,138.756,168.069,119.081,186.669,109.244C205.268,99.407,218.333,99.407,224.866,99.407L231.398,99.407" id="m-58b7ff999bbf-dark-L_U_CDN_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_CDN_0" data-points="W3sieCI6MTE5LjgwMTQ1MjU4NjIxOTkxLCJ5IjoxNTguNDMwMDg4MDQzMjEyOX0seyJ4IjoxOTIuMjAzMTI1LCJ5Ijo5OS40MDY3ODQwNTc2MTcxOX0seyJ4IjoyMzUuMzk4NDM3NSwieSI6OTkuNDA2Nzg0MDU3NjE3MTl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M126.287,210.93L137.273,218.385C148.259,225.839,170.231,240.748,184.717,248.202C199.203,255.657,206.203,255.657,209.703,255.657L213.203,255.657" id="m-58b7ff999bbf-dark-L_U_GLB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GLB_0" data-points="W3sieCI6MTI2LjI4NzM3MzA1MjA2ODUsInkiOjIxMC45MzAwODgwNDMyMTI5fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjIxNy4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M422.656,255.657L426.823,255.657C430.99,255.657,439.323,255.657,446.99,255.657C454.656,255.657,461.656,255.657,465.156,255.657L468.656,255.657" id="m-58b7ff999bbf-dark-L_GLB_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GLB_R_0" data-points="W3sieCI6NDIyLjY1NjI1LCJ5IjoyNTUuNjU2Nzg0MDU3NjE3Mn0seyJ4Ijo0NDcuNjU2MjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjQ3Mi42NTYyNSwieSI6MjU1LjY1Njc4NDA1NzYxNzJ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M626.813,260.518L633.618,260.947C640.424,261.376,654.036,262.235,666.997,261.32C679.958,260.406,692.267,257.718,698.422,256.375L704.576,255.031" id="m-58b7ff999bbf-dark-L_R_SS_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_0" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI2MC41MTc4Mjk2ODgyNzMyfSx7IngiOjY2Ny42NDg0Mzc1LCJ5IjoyNjMuMDkzMjA4MzEyOTg4M30seyJ4Ijo3MDguNDg0Mzc1LCJ5IjoyNTQuMTc3NTUxNDE2NTMwNjZ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M883.965,194.343L898.2,187.049C912.435,179.755,940.905,165.167,963.995,151.054C987.084,136.941,1004.793,123.304,1013.648,116.485L1022.503,109.666" id="m-58b7ff999bbf-dark-L_SS_ES_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_ES_0" data-points="W3sieCI6ODgzLjk2NTAyMDAxMTg5MTMsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo5NjkuMzc1LCJ5IjoxNTAuNTc4MzkyMDI4ODA4Nn0seyJ4IjoxMDI1LjY3MTg3NSwieSI6MTA3LjIyNTkxOTQ4NzY0MDl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M913.078,237.125L922.461,237.609C931.844,238.094,950.609,239.063,971.448,239.547C992.286,240.032,1015.198,240.032,1026.654,240.032L1038.109,240.032" id="m-58b7ff999bbf-dark-L_SS_RR_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_RR_0" data-points="W3sieCI6OTEzLjA3ODEyNSwieSI6MjM3LjEyNTA0MTM1NzMzMTY3fSx7IngiOjk2OS4zNzUsInkiOjI0MC4wMzE3ODQwNTc2MTcyfSx7IngiOjEwNDIuMTA5Mzc1LCJ5IjoyNDAuMDMxNzg0MDU3NjE3Mn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M607.898,72.453L617.857,72.453C627.815,72.453,647.732,72.453,672.1,72.453C696.469,72.453,725.289,72.453,739.699,72.453L754.109,72.453" id="m-58b7ff999bbf-dark-L_PG_Q_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PG_Q_0" data-points="W3sieCI6NjA3Ljg5ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NjY3LjY0ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NzU4LjEwOTM3NSwieSI6NzIuNDUzMzkyMDI4ODA4Nn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M863.453,72.453L881.107,72.453C898.76,72.453,934.068,72.453,960.438,72.453C986.807,72.453,1004.24,72.453,1012.956,72.453L1021.672,72.453" id="m-58b7ff999bbf-dark-L_Q_ES_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Q_ES_0" data-points="W3sieCI6ODYzLjQ1MzEyNSwieSI6NzIuNDUzMzkyMDI4ODA4Nn0seyJ4Ijo5NjkuMzc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fSx7IngiOjEwMjUuNjcxODc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M626.813,240.09L633.618,238.716C640.424,237.341,654.036,234.592,666.982,233.218C679.927,231.843,692.206,231.843,698.345,231.843L704.484,231.843" id="m-58b7ff999bbf-dark-L_R_SS_2" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_2" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI0MC4wOTAzMTM2MjEyMjIyNn0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MjMxLjg0MzIwODMxMjk4ODI4fSx7IngiOjcwOC40ODQzNzUsInkiOjIzMS44NDMyMDgzMTI5ODgyOH1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M721.228,194.343L712.298,190.604C703.368,186.864,685.508,179.386,656.926,175.646C628.344,171.907,589.039,171.907,552.374,171.907C515.708,171.907,481.682,171.907,443.382,171.907C405.081,171.907,362.505,171.907,319.93,171.907C277.354,171.907,234.779,171.907,209.986,172.335C185.193,172.763,178.183,173.619,174.679,174.047L171.174,174.475" id="m-58b7ff999bbf-dark-L_SS_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_U_0" data-points="W3sieCI6NzIxLjIyODM1MjMxODI3MDEsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NTQ5LjczNDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NDQ3LjY1NjI1LCJ5IjoxNzEuOTA2Nzg0MDU3NjE3Mn0seyJ4IjozMTkuOTI5Njg3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTkyLjIwMzEyNSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTY3LjIwMzEyNSwieSI6MTc0Ljk1OTYzMTM3NjYwMzR9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_CDN_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_U_GLB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GLB_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(668.07871, 262.99927)"><g class="label" data-id="L_R_SS_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_ES_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_RR_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(667.6484375, 72.4533920288086)"><g class="label" data-id="L_PG_Q_0" transform="translate(-15.8359375, -11.25)"><foreignObject width="31.671875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>CDC</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(969.375, 72.4533920288086)"><g class="label" data-id="L_Q_ES_0" transform="translate(-31.296875, -11.25)"><foreignObject width="62.59375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>consume</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_R_SS_2" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-58b7ff999bbf-dark-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 184.6800880432129)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-CDN-1" data-look="classic" transform="translate(319.9296875, 99.40678405761719)"><rect class="basic label-container" style="" x="-84.53125" y="-37.5" width="169.0625" height="75"/><g class="label" style="" transform="translate(-54.53125, -22.5)"><rect/><foreignObject width="109.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>CDN<br />статика, медиа</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-GLB-3" data-look="classic" transform="translate(319.9296875, 255.6567840576172)"><rect class="basic label-container" style="" x="-102.7265625" y="-48.75" width="205.453125" height="97.5"/><g class="label" style="" transform="translate(-72.7265625, -33.75)"><rect/><foreignObject width="145.453125" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Global LB<br />гео-роутинг<br />в 1 активный регион</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-R-5" data-look="classic" transform="translate(549.734375, 255.6567840576172)"><rect class="basic label-container" style="" x="-77.078125" y="-48.75" width="154.15625" height="97.5"/><g class="label" style="" transform="translate(-47.078125, -33.75)"><rect/><foreignObject width="94.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Redis Cluster<br />top queries +<br />autocomplete</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-SS-7" data-look="classic" transform="translate(810.78125, 231.84320831298828)"><rect class="basic label-container" style="" x="-102.296875" y="-37.5" width="204.59375" height="75"/><g class="label" style="" transform="translate(-72.296875, -22.5)"><rect/><foreignObject width="144.59375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Search Service<br />app-tier, 12–20 инст.</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-ES-9" data-look="classic" transform="translate(1127.109375, 72.4533920288086)"><path d="M0,15.468928707586732 a101.4375,15.468928707586732 0,0,0 202.875,0 a101.4375,15.468928707586732 0,0,0 -202.875,0 l0,97.96892870758673 a101.4375,15.468928707586732 0,0,0 202.875,0 l0,-97.96892870758673" class="basic label-container outer-path" style="" transform="translate(-101.4375, -64.4533930613801)"/><g class="label" style="" transform="translate(-93.9375, -23.75)"><rect/><foreignObject width="187.875" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Elasticsearch<br />8–12 нод, 6 primary shards<br />× 2 replicas</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-RR-11" data-look="classic" transform="translate(1127.109375, 240.0317840576172)"><rect class="basic label-container" style="" x="-85" y="-37.5" width="170" height="75"/><g class="label" style="" transform="translate(-55, -22.5)"><rect/><foreignObject width="110" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Ranking Service<br />отдельный SLO</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-PG-12" data-look="classic" transform="translate(549.734375, 72.4533920288086)"><path d="M0,12.050825509873746 a58.1640625,12.050825509873746 0,0,0 116.328125,0 a58.1640625,12.050825509873746 0,0,0 -116.328125,0 l0,72.05082550987375 a58.1640625,12.050825509873746 0,0,0 116.328125,0 l0,-72.05082550987375" class="basic label-container outer-path" style="" transform="translate(-58.1640625, -48.07623826481062)"/><g class="label" style="" transform="translate(-50.6640625, -12.5)"><rect/><foreignObject width="101.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres<br />source of truth</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-Q-13" data-look="classic" transform="translate(810.78125, 72.4533920288086)"><rect class="basic label-container" style="" x="-52.671875" y="-37.5" width="105.34375" height="75"/><g class="label" style="" transform="translate(-22.671875, -22.5)"><rect/><foreignObject width="45.34375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Kafka<br />events</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-58b7ff999bbf-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-58b7ff999bbf-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-58b7ff999bbf-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p><strong>Числовые прикидки:</strong></p>
<ul>
<li><strong>Capacity.</strong> 30k RPS пика → допустимо терять ~30 запросов/сек. На Elasticsearch при 50M документов и <code>6 primary shards × 1 replica</code> (~33 GB на шард, в коридоре best-practice 30–50 GB): запрос идёт в координатор, координатор делает fan-out на 6 шардов, агрегирует. Целевая нагрузка <code>30–40k RPS</code> с запасом. На 8–12 ES-нодах (<code>64–128 GB RAM</code> каждая) держится с P99 в районе <code>300–400 ms</code> чистого ES-времени.</li>
<li><strong>Кэш Redis.</strong> Кластер на 3–6 нодах, <code>~50 GB</code> под топовые запросы и autocomplete. Hit rate <code>65–75%</code> — ключевой множитель: при 70% hit rate на ES падает только 30% от 30k RPS, то есть <code>~9k RPS</code>. Это уже посильно для умеренного ES-кластера. Прогрев после деплоя — 5–10 минут на горячих топ-1000 запросах.</li>
<li><strong>Индексирование.</strong> Postgres → Debezium / собственный CDC → Kafka → потребитель в ES. Lag индексирования: <code>15–60 секунд</code> на типовых изменениях, <code>2–5 минут</code> на массовых обновлениях цен (например, при загрузке от продавца XML-фида). Это <strong>freshness-SLI</strong> — ценник старше 60 секунд считается «плохим событием» по SLI-4.</li>
<li><strong>Структура документа и размер индекса.</strong> Каждая карточка в ES — денормализованный документ: <code>title, description, attributes (10–20 полей), price, stock, seller_rating, geo, popularity</code>. Размер ~3–5 KB на документ. <code>50M × 4 KB = ~200 GB primary</code> + 200 GB replica = 400 GB Lucene-данных. На 12 нодах ≈ ~33 GB на ноду, рабочее множество легко помещается в RAM.</li>
<li><strong>Failover.</strong> Active-passive между двумя AZ внутри региона. RTO <code>1–3 минуты</code> через автоматический promote secondary. RPO ~30 секунд через CDC.</li>
<li><strong>Алерты.</strong> Multi-window multi-burn-rate: две пары окон со своими threshold — быстрая пара <code>5 мин + 1 час</code> с burn-rate <code>14.4×</code> (срабатывает на острый инцидент за минуты, 2% бюджета за час) и медленная пара <code>30 мин + 6 часов</code> с burn-rate <code>6×</code> (ловит «тихую» деградацию, 5% бюджета за 6 часов). Это табличные значения из главы Alerting on SLOs в Google SRE Workbook. Подробно про burn-rate как множитель — в моей предыдущей статье <a href="/posts/burn-rate-is-not-speed/">«Скорость сгорания бюджета ошибок»</a>.</li>
</ul>
<p><strong>Что добавилось по сравнению с 99.0%:</strong></p>
<ul>
<li>Отдельный поисковый индекс (ES) — потому что <code>pg_trgm</code> на 30k RPS + ranking + facets уже не вытянет с нужной P99.</li>
<li>Redis-кластер — без него ES под пиком пришлось бы масштабировать в 3–4 раза.</li>
<li>CDC + Kafka — из-за разделения source of truth и поискового индекса нужна асинхронная синхронизация. Это новый слой риска (lag, потеря событий) и новый SLO на freshness.</li>
<li>Active-passive failover — потому что 43 минуты бюджета мало для ручного переключения.</li>
<li>Burn-rate-алерты — потому что классические «5xx &gt; 1%» при 0.1% бюджета будут срабатывать слишком поздно или генерить шум.</li>
</ul>
<p><strong>Что намеренно НЕ делаем:</strong></p>
<ul>
<li>Не делаем active-active multi-region. На 99.9% это избыточно: один регион с грамотным failover между AZ покрывает бюджет с большим запасом, а active-active добавляет проблем синхронизации (split-brain, write-write конфликты, кросс-региональные чтения по сети).</li>
<li>Не делаем repair-aware кэш — на 99.9% достаточно правильного TTL и event-driven invalidation.</li>
<li>Не делаем graceful degradation на уровне UI («поиск работает, ranking упал»). Это усложнение, оправданное на <code>99.99%+</code>.</li>
</ul>
<p><strong>Стоимость:</strong> <strong>4–6× baseline</strong> в инфраструктуре, <strong>2–3× baseline</strong> в людях (нужна команда SRE с дежурствами, отдельные процессы для Kafka и ES).</p>
<h3 id="slo-9999-дорогая-надёжность-для-tier-1-сценариев">SLO 99.99%: дорогая надёжность для tier-1 сценариев</h3>
<p><strong>Бюджет:</strong> ~2.6M «плохих» запросов за 30 дней, или <strong>~1 мин 27 с</strong> полного даунтайма на пике / <strong>~4 мин 20 с</strong> при средней нагрузке. На инцидент остаются секунды-минуты — реакция должна быть <strong>автоматической</strong>. Ручной разбор перед remediation = превышение бюджета.</p>
<p><strong>Когда уместно:</strong> tier-1 пользовательские сценарии в крупном бизнесе уровня Ozon/Wildberries или сопоставимого по GMV, где минута даунтайма стоит сотни тысяч долларов и оправдывает мультипликатор инфраструктуры. Платёжные шлюзы, биржи, ключевые поисковые сценарии больших маркетплейсов.</p>
<p><strong>Read path:</strong></p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/94d254942e3ac4fe8b5363ef30d76426ec177af9250d290bfe29c533df8e7747.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-94d254942e3a-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1388.78px; background-color: transparent;" viewBox="0 -35 1388.78125 476.625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-94d254942e3a-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-94d254942e3a-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-94d254942e3a-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-94d254942e3a-light .error-icon{fill:#dce0e8;}#m-94d254942e3a-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-94d254942e3a-light .edge-thickness-normal{stroke-width:1px;}#m-94d254942e3a-light .edge-thickness-thick{stroke-width:3.5px;}#m-94d254942e3a-light .edge-pattern-solid{stroke-dasharray:0;}#m-94d254942e3a-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-94d254942e3a-light .edge-pattern-dashed{stroke-dasharray:3;}#m-94d254942e3a-light .edge-pattern-dotted{stroke-dasharray:2;}#m-94d254942e3a-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-94d254942e3a-light .marker.cross{stroke:#5c5f77;}#m-94d254942e3a-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-94d254942e3a-light p{margin:0;}#m-94d254942e3a-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-94d254942e3a-light .cluster-label text{fill:#4c4f69;}#m-94d254942e3a-light .cluster-label span{color:#4c4f69;}#m-94d254942e3a-light .cluster-label span p{background-color:transparent;}#m-94d254942e3a-light .label text,#m-94d254942e3a-light span{fill:#4c4f69;color:#4c4f69;}#m-94d254942e3a-light .node rect,#m-94d254942e3a-light .node circle,#m-94d254942e3a-light .node ellipse,#m-94d254942e3a-light .node polygon,#m-94d254942e3a-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-94d254942e3a-light .rough-node .label text,#m-94d254942e3a-light .node .label text,#m-94d254942e3a-light .image-shape .label,#m-94d254942e3a-light .icon-shape .label{text-anchor:middle;}#m-94d254942e3a-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-94d254942e3a-light .rough-node .label,#m-94d254942e3a-light .node .label,#m-94d254942e3a-light .image-shape .label,#m-94d254942e3a-light .icon-shape .label{text-align:center;}#m-94d254942e3a-light .node.clickable{cursor:pointer;}#m-94d254942e3a-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-94d254942e3a-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-94d254942e3a-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-94d254942e3a-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-94d254942e3a-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-94d254942e3a-light .edgeLabel p{background-color:#eff1f5;}#m-94d254942e3a-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-94d254942e3a-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-94d254942e3a-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-94d254942e3a-light .cluster text{fill:#4c4f69;}#m-94d254942e3a-light .cluster span{color:#4c4f69;}#m-94d254942e3a-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-94d254942e3a-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-94d254942e3a-light rect.text{fill:none;stroke-width:0;}#m-94d254942e3a-light .icon-shape,#m-94d254942e3a-light .image-shape{background-color:#eff1f5;text-align:center;}#m-94d254942e3a-light .icon-shape p,#m-94d254942e3a-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-94d254942e3a-light .icon-shape .label rect,#m-94d254942e3a-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-94d254942e3a-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-94d254942e3a-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-94d254942e3a-light .node .neo-node{stroke:#bcc0cc;}#m-94d254942e3a-light [data-look="neo"].node rect,#m-94d254942e3a-light [data-look="neo"].cluster rect,#m-94d254942e3a-light [data-look="neo"].node polygon{stroke:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].node path{stroke:url(#m-94d254942e3a-light-gradient);stroke-width:1px;}#m-94d254942e3a-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-94d254942e3a-light [data-look="neo"].node circle{stroke:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-94d254942e3a-light [data-look="neo"].icon-shape .icon{fill:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-94d254942e3a-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M146.97,265.5L154.509,268.833C162.048,272.167,177.125,278.833,188.164,282.167C199.203,285.5,206.203,285.5,209.703,285.5L213.203,285.5" id="m-94d254942e3a-light-L_U_GA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GA_0" data-points="W3sieCI6MTQ2Ljk3MDAxNjg5MTg5MTksInkiOjI2NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI4NS41fSx7IngiOjIxNy4yMDMxMjUsInkiOjI4NS41fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M419.359,281.993L423.526,281.848C427.693,281.703,436.026,281.414,443.693,281.27C451.359,281.125,458.359,281.125,461.859,281.125L465.359,281.125" id="m-94d254942e3a-light-L_GA_RegA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegA_0" data-points="W3sieCI6NDE5LjM1OTM3NSwieSI6MjgxLjk5MjUxNzY2MDE4MDk3fSx7IngiOjQ0NC4zNTkzNzUsInkiOjI4MS4xMjV9LHsieCI6NDY5LjM1OTM3NSwieSI6MjgxLjEyNX1d" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M395.715,334.25L403.822,339.354C411.93,344.458,428.145,354.667,455.072,359.771C482,364.875,519.641,364.875,565.663,364.875C611.685,364.875,666.089,364.875,705.182,366.999C744.276,369.123,768.06,373.371,779.952,375.496L791.844,377.62" id="m-94d254942e3a-light-L_GA_RegB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegB_0" data-points="W3sieCI6Mzk1LjcxNTA1OTA1NTExODEsInkiOjMzNC4yNX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjozNjQuODc1fSx7IngiOjU1Ny4yODEyNSwieSI6MzY0Ljg3NX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MzY0Ljg3NX0seyJ4Ijo3OTUuNzgxMjUsInkiOjM3OC4zMjMwNzc2MTAwNzQxfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M337.949,236.75L355.684,192.792C373.419,148.833,408.889,60.917,445.445,16.958C482,-27,519.641,-27,565.663,-27C611.685,-27,666.089,-27,722.449,-27C778.81,-27,837.128,-27,898.426,-27C959.724,-27,1024.003,-27,1084.016,17.539C1144.03,62.078,1199.778,151.156,1227.652,195.695L1255.526,240.234" id="m-94d254942e3a-light-L_GA_RegC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegC_0" data-points="W3sieCI6MzM3Ljk0OTQzNzUsInkiOjIzNi43NX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjotMjd9LHsieCI6NTU3LjI4MTI1LCJ5IjotMjd9LHsieCI6NzIwLjQ5MjE4NzUsInkiOi0yN30seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6LTI3fSx7IngiOjEwODguMjgxMjUsInkiOi0yN30seyJ4IjoxMjU3LjY0ODMxMDcyNTE1MjIsInkiOjI0My42MjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M626.468,329.875L642.139,340.917C657.81,351.958,689.151,374.042,716.703,385.083C744.255,396.125,768.018,396.125,779.9,396.125L791.781,396.125" id="m-94d254942e3a-light-L_RegA_RegB_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegB_0" data-points="W3sieCI6NjI2LjQ2ODQ5NTI0NDU2NTIsInkiOjMyOS44NzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjM5Ni4xMjV9LHsieCI6Nzk1Ljc4MTI1LCJ5IjozOTYuMTI1fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M795.781,420.336L783.233,423.384C770.685,426.432,745.589,432.529,714.738,417.915C683.887,403.301,647.282,367.977,628.98,350.315L610.677,332.653" id="m-94d254942e3a-light-L_RegB_RegA_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegA_0" data-points="W3sieCI6Nzk1Ljc4MTI1LCJ5Ijo0MjAuMzM1NjE0NDUwMjk5Mn0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6NDM4LjYyNX0seyJ4Ijo2MDcuNzk4OTIxMTMwOTUyNCwieSI6MzI5Ljg3NX1d" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M649.203,281.125L661.085,281.125C672.966,281.125,696.729,281.125,737.77,281.125C778.81,281.125,837.128,281.125,898.426,281.125C959.724,281.125,1024.003,281.125,1071.004,281.125C1118.005,281.125,1147.729,281.125,1162.591,281.125L1177.453,281.125" id="m-94d254942e3a-light-L_RegA_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegC_0" data-points="W3sieCI6NjQ1LjIwMzEyNSwieSI6MjgxLjEyNX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MjgxLjEyNX0seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6MjgxLjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjoyODEuMTI1fSx7IngiOjExODEuNDUzMTI1LCJ5IjoyODEuMTI1fV0=" data-look="classic" marker-start="url(#m-94d254942e3a-light_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M999.109,396.125L1013.971,396.125C1028.833,396.125,1058.557,396.125,1094.506,383.55C1130.454,370.975,1172.627,345.824,1193.714,333.249L1214.8,320.674" id="m-94d254942e3a-light-L_RegB_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegC_0" data-points="W3sieCI6OTk1LjEwOTM3NSwieSI6Mzk2LjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjozOTYuMTI1fSx7IngiOjEyMTguMjM1OTAzNTMyNjA4NywieSI6MzE4LjYyNX1d" data-look="classic" marker-start="url(#m-94d254942e3a-light_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M591.049,232.375L612.623,201.229C634.197,170.083,677.344,107.792,710.916,76.646C744.487,45.5,768.482,45.5,780.479,45.5L792.477,45.5" id="m-94d254942e3a-light-L_RegA_RAW_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RAW_0" data-points="W3sieCI6NTkxLjA0OTAzMDE3MjQxMzgsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjQ1LjV9LHsieCI6Nzk2LjQ3NjU2MjUsInkiOjQ1LjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M607.799,232.375L626.581,214.25C645.363,196.125,682.928,159.875,714.758,146.085C746.589,132.295,772.685,140.965,785.734,145.3L798.782,149.635" id="m-94d254942e3a-light-L_RegA_Deg_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_Deg_0" data-points="W3sieCI6NjA3Ljc5ODkyMTEzMDk1MjQsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjEyMy42MjV9LHsieCI6ODAyLjU3ODEyNSwieSI6MTUwLjg5NjU2MjY5NTM2NDgyfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M802.578,187.058L788.897,187.84C775.216,188.622,747.854,190.186,706.971,190.968C666.089,191.75,611.685,191.75,565.663,191.75C519.641,191.75,482,191.75,442.167,191.75C402.333,191.75,360.307,191.75,318.281,191.75C276.255,191.75,234.229,191.75,206.024,195.016C177.819,198.282,163.434,204.814,156.242,208.08L149.05,211.346" id="m-94d254942e3a-light-L_Deg_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Deg_U_0" data-points="W3sieCI6ODAyLjU3ODEyNSwieSI6MTg3LjA1ODExODI0NTk1ODc1fSx7IngiOjcyMC40OTIxODc1LCJ5IjoxOTEuNzV9LHsieCI6NTU3LjI4MTI1LCJ5IjoxOTEuNzV9LHsieCI6NDQ0LjM1OTM3NSwieSI6MTkxLjc1fSx7IngiOjMxOC4yODEyNSwieSI6MTkxLjc1fSx7IngiOjE5Mi4yMDMxMjUsInkiOjE5MS43NX0seyJ4IjoxNDUuNDA3Njg5MTQ0NzM2ODUsInkiOjIxM31d" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_GA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegC_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(720.4921875, 396.125)"><g class="label" data-id="L_RegA_RegB_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(692.02177, 411.15079)"><g class="label" data-id="L_RegB_RegA_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(895.4453125, 281.125)"><g class="label" data-id="L_RegA_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1088.28125, 396.125)"><g class="label" data-id="L_RegB_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_RAW_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_Deg_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Deg_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-94d254942e3a-light-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 239.25)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-GA-1" data-look="classic" transform="translate(318.28125, 285.5)"><rect class="basic label-container" style="" x="-101.078125" y="-48.75" width="202.15625" height="97.5"/><g class="label" style="" transform="translate(-71.078125, -33.75)"><rect/><foreignObject width="142.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Geo-Anycast<br />+ Global LB<br />health-aware routing</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RegA-3" data-look="classic" transform="translate(557.28125, 281.125)"><rect class="basic label-container" style="" x="-87.921875" y="-48.75" width="175.84375" height="97.5"/><g class="label" style="" transform="translate(-57.921875, -33.75)"><rect/><foreignObject width="115.84375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион A<br />Search + Redis +<br />ES + Postgres</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RegB-5" data-look="classic" transform="translate(895.4453125, 396.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион B<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RegC-7" data-look="classic" transform="translate(1281.1171875, 281.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион C<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RAW-17" data-look="classic" transform="translate(895.4453125, 45.5)"><rect class="basic label-container" style="" x="-98.96875" y="-37.5" width="197.9375" height="75"/><g class="label" style="" transform="translate(-68.96875, -22.5)"><rect/><foreignObject width="137.9375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Repair-aware Cache<br />per-region</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-Deg-19" data-look="classic" transform="translate(895.4453125, 181.75)"><rect class="basic label-container" style="" x="-92.8671875" y="-48.75" width="185.734375" height="97.5"/><g class="label" style="" transform="translate(-62.8671875, -33.75)"><rect/><foreignObject width="125.734375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Degradation Layer<br />ranking упал —<br />отдаём базовое</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-94d254942e3a-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-94d254942e3a-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-94d254942e3a-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-94d254942e3a-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1388.78px; background-color: transparent;" viewBox="0 -35 1388.78125 476.625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-94d254942e3a-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-94d254942e3a-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-94d254942e3a-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-94d254942e3a-dark .error-icon{fill:#181926;}#m-94d254942e3a-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-94d254942e3a-dark .edge-thickness-normal{stroke-width:1px;}#m-94d254942e3a-dark .edge-thickness-thick{stroke-width:3.5px;}#m-94d254942e3a-dark .edge-pattern-solid{stroke-dasharray:0;}#m-94d254942e3a-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-94d254942e3a-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-94d254942e3a-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-94d254942e3a-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-94d254942e3a-dark .marker.cross{stroke:#a5adcb;}#m-94d254942e3a-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-94d254942e3a-dark p{margin:0;}#m-94d254942e3a-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-94d254942e3a-dark .cluster-label text{fill:#cad3f5;}#m-94d254942e3a-dark .cluster-label span{color:#cad3f5;}#m-94d254942e3a-dark .cluster-label span p{background-color:transparent;}#m-94d254942e3a-dark .label text,#m-94d254942e3a-dark span{fill:#cad3f5;color:#cad3f5;}#m-94d254942e3a-dark .node rect,#m-94d254942e3a-dark .node circle,#m-94d254942e3a-dark .node ellipse,#m-94d254942e3a-dark .node polygon,#m-94d254942e3a-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-94d254942e3a-dark .rough-node .label text,#m-94d254942e3a-dark .node .label text,#m-94d254942e3a-dark .image-shape .label,#m-94d254942e3a-dark .icon-shape .label{text-anchor:middle;}#m-94d254942e3a-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-94d254942e3a-dark .rough-node .label,#m-94d254942e3a-dark .node .label,#m-94d254942e3a-dark .image-shape .label,#m-94d254942e3a-dark .icon-shape .label{text-align:center;}#m-94d254942e3a-dark .node.clickable{cursor:pointer;}#m-94d254942e3a-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-94d254942e3a-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-94d254942e3a-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-94d254942e3a-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-94d254942e3a-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-94d254942e3a-dark .edgeLabel p{background-color:#24273a;}#m-94d254942e3a-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-94d254942e3a-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-94d254942e3a-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-94d254942e3a-dark .cluster text{fill:#cad3f5;}#m-94d254942e3a-dark .cluster span{color:#cad3f5;}#m-94d254942e3a-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-94d254942e3a-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-94d254942e3a-dark rect.text{fill:none;stroke-width:0;}#m-94d254942e3a-dark .icon-shape,#m-94d254942e3a-dark .image-shape{background-color:#24273a;text-align:center;}#m-94d254942e3a-dark .icon-shape p,#m-94d254942e3a-dark .image-shape p{background-color:#24273a;padding:2px;}#m-94d254942e3a-dark .icon-shape .label rect,#m-94d254942e3a-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-94d254942e3a-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-94d254942e3a-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-94d254942e3a-dark .node .neo-node{stroke:#494d64;}#m-94d254942e3a-dark [data-look="neo"].node rect,#m-94d254942e3a-dark [data-look="neo"].cluster rect,#m-94d254942e3a-dark [data-look="neo"].node polygon{stroke:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].node path{stroke:url(#m-94d254942e3a-dark-gradient);stroke-width:1px;}#m-94d254942e3a-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-94d254942e3a-dark [data-look="neo"].node circle{stroke:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-94d254942e3a-dark [data-look="neo"].icon-shape .icon{fill:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-94d254942e3a-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M146.97,265.5L154.509,268.833C162.048,272.167,177.125,278.833,188.164,282.167C199.203,285.5,206.203,285.5,209.703,285.5L213.203,285.5" id="m-94d254942e3a-dark-L_U_GA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GA_0" data-points="W3sieCI6MTQ2Ljk3MDAxNjg5MTg5MTksInkiOjI2NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI4NS41fSx7IngiOjIxNy4yMDMxMjUsInkiOjI4NS41fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M419.359,281.993L423.526,281.848C427.693,281.703,436.026,281.414,443.693,281.27C451.359,281.125,458.359,281.125,461.859,281.125L465.359,281.125" id="m-94d254942e3a-dark-L_GA_RegA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegA_0" data-points="W3sieCI6NDE5LjM1OTM3NSwieSI6MjgxLjk5MjUxNzY2MDE4MDk3fSx7IngiOjQ0NC4zNTkzNzUsInkiOjI4MS4xMjV9LHsieCI6NDY5LjM1OTM3NSwieSI6MjgxLjEyNX1d" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M395.715,334.25L403.822,339.354C411.93,344.458,428.145,354.667,455.072,359.771C482,364.875,519.641,364.875,565.663,364.875C611.685,364.875,666.089,364.875,705.182,366.999C744.276,369.123,768.06,373.371,779.952,375.496L791.844,377.62" id="m-94d254942e3a-dark-L_GA_RegB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegB_0" data-points="W3sieCI6Mzk1LjcxNTA1OTA1NTExODEsInkiOjMzNC4yNX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjozNjQuODc1fSx7IngiOjU1Ny4yODEyNSwieSI6MzY0Ljg3NX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MzY0Ljg3NX0seyJ4Ijo3OTUuNzgxMjUsInkiOjM3OC4zMjMwNzc2MTAwNzQxfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M337.949,236.75L355.684,192.792C373.419,148.833,408.889,60.917,445.445,16.958C482,-27,519.641,-27,565.663,-27C611.685,-27,666.089,-27,722.449,-27C778.81,-27,837.128,-27,898.426,-27C959.724,-27,1024.003,-27,1084.016,17.539C1144.03,62.078,1199.778,151.156,1227.652,195.695L1255.526,240.234" id="m-94d254942e3a-dark-L_GA_RegC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegC_0" data-points="W3sieCI6MzM3Ljk0OTQzNzUsInkiOjIzNi43NX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjotMjd9LHsieCI6NTU3LjI4MTI1LCJ5IjotMjd9LHsieCI6NzIwLjQ5MjE4NzUsInkiOi0yN30seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6LTI3fSx7IngiOjEwODguMjgxMjUsInkiOi0yN30seyJ4IjoxMjU3LjY0ODMxMDcyNTE1MjIsInkiOjI0My42MjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M626.468,329.875L642.139,340.917C657.81,351.958,689.151,374.042,716.703,385.083C744.255,396.125,768.018,396.125,779.9,396.125L791.781,396.125" id="m-94d254942e3a-dark-L_RegA_RegB_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegB_0" data-points="W3sieCI6NjI2LjQ2ODQ5NTI0NDU2NTIsInkiOjMyOS44NzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjM5Ni4xMjV9LHsieCI6Nzk1Ljc4MTI1LCJ5IjozOTYuMTI1fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M795.781,420.336L783.233,423.384C770.685,426.432,745.589,432.529,714.738,417.915C683.887,403.301,647.282,367.977,628.98,350.315L610.677,332.653" id="m-94d254942e3a-dark-L_RegB_RegA_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegA_0" data-points="W3sieCI6Nzk1Ljc4MTI1LCJ5Ijo0MjAuMzM1NjE0NDUwMjk5Mn0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6NDM4LjYyNX0seyJ4Ijo2MDcuNzk4OTIxMTMwOTUyNCwieSI6MzI5Ljg3NX1d" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M649.203,281.125L661.085,281.125C672.966,281.125,696.729,281.125,737.77,281.125C778.81,281.125,837.128,281.125,898.426,281.125C959.724,281.125,1024.003,281.125,1071.004,281.125C1118.005,281.125,1147.729,281.125,1162.591,281.125L1177.453,281.125" id="m-94d254942e3a-dark-L_RegA_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegC_0" data-points="W3sieCI6NjQ1LjIwMzEyNSwieSI6MjgxLjEyNX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MjgxLjEyNX0seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6MjgxLjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjoyODEuMTI1fSx7IngiOjExODEuNDUzMTI1LCJ5IjoyODEuMTI1fV0=" data-look="classic" marker-start="url(#m-94d254942e3a-dark_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M999.109,396.125L1013.971,396.125C1028.833,396.125,1058.557,396.125,1094.506,383.55C1130.454,370.975,1172.627,345.824,1193.714,333.249L1214.8,320.674" id="m-94d254942e3a-dark-L_RegB_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegC_0" data-points="W3sieCI6OTk1LjEwOTM3NSwieSI6Mzk2LjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjozOTYuMTI1fSx7IngiOjEyMTguMjM1OTAzNTMyNjA4NywieSI6MzE4LjYyNX1d" data-look="classic" marker-start="url(#m-94d254942e3a-dark_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M591.049,232.375L612.623,201.229C634.197,170.083,677.344,107.792,710.916,76.646C744.487,45.5,768.482,45.5,780.479,45.5L792.477,45.5" id="m-94d254942e3a-dark-L_RegA_RAW_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RAW_0" data-points="W3sieCI6NTkxLjA0OTAzMDE3MjQxMzgsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjQ1LjV9LHsieCI6Nzk2LjQ3NjU2MjUsInkiOjQ1LjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M607.799,232.375L626.581,214.25C645.363,196.125,682.928,159.875,714.758,146.085C746.589,132.295,772.685,140.965,785.734,145.3L798.782,149.635" id="m-94d254942e3a-dark-L_RegA_Deg_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_Deg_0" data-points="W3sieCI6NjA3Ljc5ODkyMTEzMDk1MjQsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjEyMy42MjV9LHsieCI6ODAyLjU3ODEyNSwieSI6MTUwLjg5NjU2MjY5NTM2NDgyfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M802.578,187.058L788.897,187.84C775.216,188.622,747.854,190.186,706.971,190.968C666.089,191.75,611.685,191.75,565.663,191.75C519.641,191.75,482,191.75,442.167,191.75C402.333,191.75,360.307,191.75,318.281,191.75C276.255,191.75,234.229,191.75,206.024,195.016C177.819,198.282,163.434,204.814,156.242,208.08L149.05,211.346" id="m-94d254942e3a-dark-L_Deg_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Deg_U_0" data-points="W3sieCI6ODAyLjU3ODEyNSwieSI6MTg3LjA1ODExODI0NTk1ODc1fSx7IngiOjcyMC40OTIxODc1LCJ5IjoxOTEuNzV9LHsieCI6NTU3LjI4MTI1LCJ5IjoxOTEuNzV9LHsieCI6NDQ0LjM1OTM3NSwieSI6MTkxLjc1fSx7IngiOjMxOC4yODEyNSwieSI6MTkxLjc1fSx7IngiOjE5Mi4yMDMxMjUsInkiOjE5MS43NX0seyJ4IjoxNDUuNDA3Njg5MTQ0NzM2ODUsInkiOjIxM31d" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_GA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegC_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(720.4921875, 396.125)"><g class="label" data-id="L_RegA_RegB_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(692.02177, 411.15079)"><g class="label" data-id="L_RegB_RegA_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(895.4453125, 281.125)"><g class="label" data-id="L_RegA_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1088.28125, 396.125)"><g class="label" data-id="L_RegB_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_RAW_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_Deg_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Deg_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-94d254942e3a-dark-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 239.25)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-GA-1" data-look="classic" transform="translate(318.28125, 285.5)"><rect class="basic label-container" style="" x="-101.078125" y="-48.75" width="202.15625" height="97.5"/><g class="label" style="" transform="translate(-71.078125, -33.75)"><rect/><foreignObject width="142.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Geo-Anycast<br />+ Global LB<br />health-aware routing</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RegA-3" data-look="classic" transform="translate(557.28125, 281.125)"><rect class="basic label-container" style="" x="-87.921875" y="-48.75" width="175.84375" height="97.5"/><g class="label" style="" transform="translate(-57.921875, -33.75)"><rect/><foreignObject width="115.84375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион A<br />Search + Redis +<br />ES + Postgres</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RegB-5" data-look="classic" transform="translate(895.4453125, 396.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион B<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RegC-7" data-look="classic" transform="translate(1281.1171875, 281.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион C<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RAW-17" data-look="classic" transform="translate(895.4453125, 45.5)"><rect class="basic label-container" style="" x="-98.96875" y="-37.5" width="197.9375" height="75"/><g class="label" style="" transform="translate(-68.96875, -22.5)"><rect/><foreignObject width="137.9375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Repair-aware Cache<br />per-region</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-Deg-19" data-look="classic" transform="translate(895.4453125, 181.75)"><rect class="basic label-container" style="" x="-92.8671875" y="-48.75" width="185.734375" height="97.5"/><g class="label" style="" transform="translate(-62.8671875, -33.75)"><rect/><foreignObject width="125.734375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Degradation Layer<br />ranking упал —<br />отдаём базовое</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-94d254942e3a-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-94d254942e3a-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-94d254942e3a-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p><strong>Числовые прикидки:</strong></p>
<ul>
<li><strong>Capacity.</strong> 30k RPS пика делятся между 3 регионами <code>~по 10k RPS</code> нормально. Но <strong>каждый регион должен один переварить 100% трафика</strong> при отказе двух других — то есть sized под <code>30–40k RPS</code>. Это утроение инфраструктуры по сравнению с одним регионом 99.9%, плюс запас.</li>
<li><strong>Кросс-региональная синхронизация индексов.</strong> ES → Kafka MirrorMaker (или managed cross-region streaming) → ES в каждом регионе. Lag <code>5–30 секунд</code> между регионами в нормальном режиме, <code>1–3 минуты</code> при региональной сетевой деградации. Eventually consistent модель данных — для поиска приемлемо, для цен/наличия (freshness-SLI) — узкое место, требующее отдельной стратегии (часто — fetch цены при клике в карточку из локального region-БД).</li>
<li><strong>Repair-aware кэш.</strong> Когда регион вышел из деградации и снова healthy, кэш в нём может содержать <code>«грязные»</code> ключи — данные, менявшиеся в других регионах, но не доехавшие. Решение: каждый ключ в кэше помечен суффиксом версии данных (<code>hash</code> или <code>LSN</code>); при возврате региона — проактивный прогрев топовых ключей со свежими версиями. Это отдельный сервис, обычно in-house.</li>
<li><strong>Graceful degradation.</strong> Если ranking-сервис упал — поиск отдаёт базовое ранжирование (popularity + recency) без персонализации. Если упал autocomplete — прячем подсказки, оставляя ввод. Если упал ES — отдаём кэш-результаты + предупреждение «возможно, не самые свежие». Каждый из этих режимов — отдельный код-path и отдельный SLI.</li>
<li><strong>Failover.</strong> Автоматический на уровне geo-anycast / global LB, RTO <code>5–30 секунд</code> на регион. Health-чеки агрегируют SLI критичного пути, а не пинг сервера.</li>
<li><strong>RPO.</strong> Околонулевой для критичных данных (заказы, корзина) через synchronous replication между двумя регионами + async на третий. Для каталога/индекса — <code>до 1 минуты</code>.</li>
<li><strong>Алерты.</strong> Multi-window multi-burn-rate с поджатыми окнами: быстрая пара <code>2 мин + 1 час</code> с threshold <code>36×</code> (для tier-1 порог берут агрессивнее, чем 14.4× — иначе быстрый алерт догонит инцидент уже после исчерпания бюджета) + медленная пара <code>15 мин + 3 часа</code> с threshold <code>~10×</code> для затяжной деградации. Auto-remediation для известных классов: автоматический drain региона при превышении burn-rate, автоматический rollback деплоя.</li>
</ul>
<p><strong>Что добавилось по сравнению с 99.9%:</strong></p>
<ul>
<li>Multi-region active-active — потому что 1 минута 27 секунд бюджета на пике не покрывают ни один региональный инцидент (4 мин 20 с в среднем — тоже).</li>
<li>Repair-aware кэш — потому что после возврата региона данные могут разойтись, и без проактивного прогрева накопится freshness-долг, съедающий SLI-4.</li>
<li>Graceful degradation — потому что без неё любой отказ зависимости (ranking, persona) забирает весь бюджет.</li>
<li>Auto-remediation — потому что человек физически не успевает посмотреть пейдж за 1 мин 27 с пикового бюджета, не то что разобраться.</li>
<li>Канареечный rollout с автоматическим откатом по burn-rate — каждый деплой иначе ставит весь бюджет.</li>
</ul>
<p><strong>Что намеренно НЕ делаем (и здесь это особенно важно):</strong></p>
<ul>
<li>Не идём в <code>99.999%</code> без очень сильного бизнес-обоснования. 26 секунд в месяц требуют существенно более сложной модели резервирования, чем 99.99%: multi-cloud или multi-provider, независимые DNS, частично независимые кодовые базы, подходы из telecom/aerospace. Это часто <strong>дешевле решить через UX</strong> — например, кэшированием выдачи на клиенте на минуту, чтобы из 100% даунтайма пользователь увидел только новые сессии.</li>
</ul>
<p><strong>Стоимость:</strong> <strong>15–25× baseline</strong> в инфраструктуре, <strong>8–10× baseline</strong> в людях (24/7 SRE, регулярные game days, отдельная команда reliability engineering).</p>
<h3 id="сводная-таблица-slo-как-бюджет-на-сложность">Сводная таблица: SLO как бюджет на сложность</h3>
<table>
	<thead>
			<tr>
					<th>Аспект</th>
					<th>99.0%</th>
					<th>99.9%</th>
					<th>99.99%</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Бюджет на пике (30 дней)</td>
					<td>~2 ч 24 мин</td>
					<td>~14 мин 26 с</td>
					<td>~1 мин 27 с</td>
			</tr>
			<tr>
					<td>Бюджет при средней нагрузке (30 дней)</td>
					<td>~7 ч 12 мин</td>
					<td>~43 мин</td>
					<td>~4 мин 20 с</td>
			</tr>
			<tr>
					<td>Бюджет в запросах (30 дней)</td>
					<td>~260M</td>
					<td>~26M</td>
					<td>~2.6M</td>
			</tr>
			<tr>
					<td>Регионы</td>
					<td>1</td>
					<td>1 (active-passive AZ)</td>
					<td>3+ (active-active)</td>
			</tr>
			<tr>
					<td>Поисковый индекс</td>
					<td><code>pg_trgm</code> в Postgres</td>
					<td>Elasticsearch (8–12 нод)</td>
					<td>ES per region</td>
			</tr>
			<tr>
					<td>Кэш</td>
					<td>In-memory LRU на app</td>
					<td>Redis cluster (~50 GB)</td>
					<td>Repair-aware Redis × N</td>
			</tr>
			<tr>
					<td>Индексирование</td>
					<td>Триггеры в БД</td>
					<td>CDC + Kafka</td>
					<td>Cross-region MirrorMaker</td>
			</tr>
			<tr>
					<td>Failover</td>
					<td>Ручной, RTO 30–60 мин</td>
					<td>Авто между AZ, RTO 1–3 мин</td>
					<td>Авто между регионами, RTO 5–30 с</td>
			</tr>
			<tr>
					<td>RPO</td>
					<td>~5 мин</td>
					<td>~30 с</td>
					<td>~0 для tier-1 / ~1 мин для каталога</td>
			</tr>
			<tr>
					<td>Алерты</td>
					<td>На симптомы</td>
					<td>Multi-window burn-rate</td>
					<td>+ Auto-remediation</td>
			</tr>
			<tr>
					<td>Деплои</td>
					<td>Без особых процедур</td>
					<td>Постепенный rollout</td>
					<td>Канарейка + auto-откат</td>
			</tr>
			<tr>
					<td>Graceful degradation</td>
					<td>Нет</td>
					<td>Частично</td>
					<td>Полная</td>
			</tr>
			<tr>
					<td><strong>Инфраструктура (× baseline)</strong></td>
					<td><strong>1×</strong></td>
					<td><strong>4–6×</strong></td>
					<td><strong>15–25×</strong></td>
			</tr>
			<tr>
					<td><strong>Люди (× baseline)</strong></td>
					<td><strong>1×</strong></td>
					<td><strong>2–3×</strong></td>
					<td><strong>8–10×</strong></td>
			</tr>
			<tr>
					<td>Когда оправдано</td>
					<td>Внутренние tools, MVP, не-критичные пути</td>
					<td>Production e-commerce средней руки</td>
					<td>Tier-1 в крупном бизнесе</td>
			</tr>
	</tbody>
</table>
<h3 id="главный-вывод-раздела">Главный вывод раздела</h3>
<p>Один и тот же кейс на трёх SLO — это <strong>три разные инженерные задачи</strong>. Не «та же задача с разными настройками», а с разной топологией, разными технологиями, разной операционной моделью, разной стоимостью.</p>
<p>Из этого следуют две вещи, которые часто упускают:</p>
<ol>
<li><strong>Поднять SLO задним числом — это переписать систему</strong>, а не «добавить мониторинга». Если архитектура построена под 99.0%, поднять её до 99.99% означает: добавить регионы, переписать индексирование под cross-region, ввести graceful degradation, перепроектировать кэш. Не «настройка пары флагов» — <strong>рефакторинг порядка половины кодовой базы</strong>.</li>
<li><strong>Снизить SLO задним числом</strong> освобождает бюджет, но <strong>не возвращает потраченных денег</strong>. Если ты построил под 99.99%, а реально нужно 99.9% — лишний регион уже куплен, репликация уже настроена, команда SRE уже нанята. Снижение SLO в этом случае имеет смысл только перед следующим циклом капекса.</li>
</ol>
<p>Поэтому SLO выбирается <strong>до</strong> дизайна, а не после. И выбирается <strong>с участием бизнеса</strong> — потому что цифра в нём в итоге привязана к деньгам, а не к самочувствию инженеров.</p>
<h2 id="sla-что-уходит-наружу">SLA: что уходит наружу</h2>
<p>Третий и последний термин «троицы» — <strong>SLA (Service Level Agreement)</strong>. Это контракт с пользователем (или клиентом, партнёром, регулятором), оформленный обычно юридически, с финансовыми санкциями за нарушение.</p>
<p>Принципиальное отличие от SLO:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>SLO</th>
					<th>SLA</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Кому обещаем</td>
					<td>Самим себе (внутри команды/компании)</td>
					<td>Внешнему пользователю или клиенту</td>
			</tr>
			<tr>
					<td>Последствия нарушения</td>
					<td>Перераспределение бюджета на reliability, freeze</td>
					<td>Финансовые санкции, отток, репутация</td>
			</tr>
			<tr>
					<td>Гранулярность</td>
					<td>Может быть сколько угодно (по шагу, по сегменту)</td>
					<td>Обычно одно-два простых числа</td>
			</tr>
			<tr>
					<td>Частота пересмотра</td>
					<td>Раз в квартал</td>
					<td>Раз в год или реже</td>
			</tr>
	</tbody>
</table>
<h3 id="главное-правило-sla--slo--запас">Главное правило: SLA ≤ SLO − запас</h3>
<p>Самая частая ошибка — поставить SLA равным SLO. Например, инженеры обещают себе 99.99%, и юристы пишут пользователю «гарантируем 99.99%». В этом случае любое попадание в нижнюю границу SLO — уже автоматически нарушение SLA.</p>
<p>Правильно: между SLO и SLA должен быть <strong>запас</strong>, обычно один-два разряда <code>9</code>.</p>
<ul>
<li>SLO 99.99% → SLA 99.9% или 99.95%.</li>
<li>SLO 99.9% → SLA 99.5%.</li>
</ul>
<p>Запас не стоит ничего — это просто разница между «что мы хотим внутри» и «что мы готовы юридически защищать снаружи».</p>
<h3 id="и-ещё-одно-важное">И ещё одно важное</h3>
<p>SLA пишется не на ту же метрику, что SLO. SLA обычно — это <strong>availability в чистом виде</strong> (грубое «отвечает / не отвечает»). SLO — набор тонких индикаторов (latency, freshness, correctness). Ты внутри живёшь на сложной картине из 4–8 SLI, наружу выдаёшь одну простую цифру с запасом.</p>
<h2 id="golden-signals-red-use--какую-призму-брать-и-когда">Golden Signals, RED, USE — какую призму брать и когда</h2>
<p>Кейс выше показал три разные системы под три SLO. Прежде чем переходить к разговору про error budget как арбитр в спорах, важно понять — а каким набором метрик мы вообще смотрим на эти системы. В SRE-сообществе живут три популярных набора-призмы для метрик:</p>
<ul>
<li><strong>Golden Signals</strong> — <code>Latency, Traffic, Errors, Saturation</code>. Универсальный набор для пользовательских сервисов. Введён Rob Ewaschuk и закреплён в Google SRE Book (глава «Monitoring Distributed Systems», O&rsquo;Reilly, 2016).</li>
<li><strong>RED</strong> — <code>Rate, Errors, Duration</code>. Подмножество Golden Signals, ориентированное на запрос-ответные сервисы. Введён Tom Wilkie (Weaveworks, ныне Grafana Labs) в докладе «The RED Method» (2017).</li>
<li><strong>USE</strong> — <code>Utilization, Saturation, Errors</code>. Для ресурсов: CPU, диск, память, сеть. Введён Brendan Gregg в «Systems Performance» (2-е изд., 2020) и серии постов на brendangregg.com (2012+).</li>
</ul>
<p>Эти призмы часто валят в одну кучу и спорят, какая «правильная». Спор бессмысленный — они отвечают на <strong>разные вопросы</strong>.</p>
<h3 id="golden-signals--самая-полная-призма-для-сервиса">Golden Signals — самая полная призма для сервиса</h3>
<p>Если ты только начинаешь и не знаешь, что меряет — начни с Golden Signals на каждый твой сервис:</p>
<ul>
<li><strong>Latency.</strong> Сколько времени занял успешный запрос (отдельно — неуспешный, потому что медленный 500 искажает картину).</li>
<li><strong>Traffic.</strong> Сколько запросов обрабатываем (RPS, qps, events/sec).</li>
<li><strong>Errors.</strong> Доля «плохих» — 5xx, таймауты, протокольные ошибки, неуспешные заказы.</li>
<li><strong>Saturation.</strong> Насколько ресурс заполнен — очередь, пул соединений, утилизация worker-ов.</li>
</ul>
<p>В нашем кейсе для search-сервиса Golden Signals разворачиваются так:</p>
<ul>
<li><strong>Latency:</strong> P50/P95/P99 на эндпоинт <code>/search</code>, разбитые по типу запроса (autocomplete vs full search).</li>
<li><strong>Traffic:</strong> RPS на <code>/search</code>, отдельно по регионам.</li>
<li><strong>Errors:</strong> доля 5xx + таймаутов к ES + ошибок ranking-сервиса.</li>
<li><strong>Saturation:</strong> размер очереди в Kafka-consumer индексирования, пул коннектов в ES, hit rate Redis.</li>
</ul>
<p>Эти четыре сразу дают <strong>верхнеуровневую картину здоровья сервиса</strong>. SLI ты выбираешь из них (обычно — Latency и Errors), но мониторить нужно все четыре.</p>
<h3 id="red--тот-же-набор-проще-запомнить">RED — тот же набор, проще запомнить</h3>
<p>RED (<code>Rate, Errors, Duration</code>) — это Golden Signals минус <code>Saturation</code>. Удобный мнемоник, особенно для request/response-сервисов: каждый эндпоинт обязательно имеет три метрики и хотя бы один дашборд по ним.</p>
<p>Когда брать RED вместо Golden Signals: если ты делаешь дашборд per-endpoint в большом сервисе и не хочешь ставить четыре графика на каждый эндпоинт. Saturation — ресурсная метрика, и она часто живёт отдельно (на дашборде ресурсов или инфраструктуры), а не на дашборде эндпоинтов.</p>
<h3 id="use--призма-для-ресурсов-не-для-сервисов">USE — призма для ресурсов, не для сервисов</h3>
<p>USE (<code>Utilization, Saturation, Errors</code>) — про <strong>ресурс</strong>, не про сервис. Когда мерить:</p>
<ul>
<li><strong>CPU</strong> — utilization = % busy, saturation = run queue, errors = throttling.</li>
<li><strong>Диск</strong> — utilization = busy time, saturation = await, errors = bad sectors / IO errors.</li>
<li><strong>Сеть</strong> — utilization = bandwidth %, saturation = drops, errors = retransmits.</li>
<li><strong>Пул соединений</strong> — utilization = used/max, saturation = очередь ожидания, errors = таймауты.</li>
</ul>
<p>USE — для <strong>диагностики</strong>, не для SLO. Никогда не делай SLO на CPU utilization напрямую — это служебный сигнал. Но USE-метрики бесценны, когда burn-rate-алерт сработал и нужно быстро понять, <strong>где</strong> в стеке давит.</p>
<h3 id="когда-какую-призму-брать--простое-правило">Когда какую призму брать — простое правило</h3>
<table>
	<thead>
			<tr>
					<th>Ситуация</th>
					<th>Призма</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Дашборд верхнего уровня по сервису</td>
					<td>Golden Signals</td>
			</tr>
			<tr>
					<td>Дашборд per-endpoint в большом сервисе</td>
					<td>RED</td>
			</tr>
			<tr>
					<td>Дашборд ресурса (CPU, диск, БД-пул)</td>
					<td>USE</td>
			</tr>
			<tr>
					<td>Выбор кандидатов в SLI</td>
					<td>Golden Signals → Latency и Errors</td>
			</tr>
			<tr>
					<td>Диагностика после burn-rate-алерта</td>
					<td>USE — где жмёт по ресурсам</td>
			</tr>
	</tbody>
</table>
<h3 id="главное-смещение">Главное смещение</h3>
<p>Призмы не противоречат — они <strong>дополняют</strong>. У зрелой команды есть все три уровня дашбордов:</p>
<ul>
<li><strong>Верхний</strong> — Golden Signals + дашборд SLI/SLO с burn-rate. Заходишь и сразу видишь, всё ли в порядке с пользовательским опытом.</li>
<li><strong>Средний</strong> — RED по эндпоинтам. Заходишь, когда верхний показывает деградацию, и видишь, какой эндпоинт вносит вклад.</li>
<li><strong>Нижний</strong> — USE по ресурсам. Заходишь, когда RED показывает, какой эндпоинт болит, и нужно понять, в каком ресурсе упёрлись.</li>
</ul>
<h2 id="error-budget-как-арбитр-в-спорах-фича-vs-рефакторинг">Error budget как арбитр в спорах «фича vs рефакторинг»</h2>
<p>Самый недооценённый эффект от грамотного SLO — это не алерты и не отчёты. Это то, что бюджет ошибок становится <strong>валютой в архитектурных спорах</strong>, и решения принимаются на цифрах, а не на ощущениях.</p>
<h3 id="классический-спор-без-бюджета">Классический спор без бюджета</h3>
<p>Сценарий, знакомый каждому, кто работал в продуктовой команде дольше года:</p>
<blockquote><p><strong>Продакт:</strong> «Нам нужно срочно выкатить персонализированную выдачу до Чёрной пятницы».<br>
<strong>SRE:</strong> «Мы только что прошли через два инцидента из-за деплоя. Я хочу заморозить деплои на неделю и провести post-mortem».<br>
<strong>Продакт:</strong> «Каждая неделя задержки — это $500k недополученной выручки».<br>
<strong>SRE:</strong> «Каждый инцидент — это репутация».</p>
</blockquote><p>Без бюджета этот спор решается <strong>властью</strong>. Кто громче, выше по иерархии или ближе к CEO — тот побеждает. Решения принимаются эмоционально, обе стороны выходят с ощущением, что их аргументы недослушаны.</p>
<h3 id="тот-же-спор-с-бюджетом">Тот же спор с бюджетом</h3>
<blockquote><p><strong>Продакт:</strong> «Хочу выкатить персонализацию до BF».<br>
<strong>SRE:</strong> «Бюджет на этот месяц — 26M запросов. За последние две недели мы сожгли 22M на двух инцидентах. У нас осталось 4M на оставшиеся 16 дней. Вот burn-rate-график за неделю».<br>
<strong>Продакт:</strong> «Сколько бюджета съедает выкатка персонализации?»<br>
<strong>SRE:</strong> «По прошлым выкаткам сопоставимого риска — медианно 3–4M запросов на постепенный rollout. То есть канарейка съест от 75% до 100% оставшегося бюджета. Если что-то пойдёт не так — превышаем SLO до конца окна».<br>
<strong>Продакт:</strong> «Сколько ждать, пока бюджет восстановится?»<br>
<strong>SRE:</strong> «Окно rolling, считается на последних 30 днях — каждые сутки самые старые сутки выпадают, новые входят. Самые тяжёлые инциденты были 28 и 25 дней назад: через 2–5 дней они выпадут из окна, и мы откатимся к ~10M свободного бюджета без всяких новых усилий».<br>
<strong>Продакт:</strong> «До BF — 24 дня. Подождём 5 дней, потом выкатываем канарейку — рискуем уже на восстановленном бюджете, а не на остатках от инцидентов».</p>
</blockquote><p>Спор кончился за 5 минут, без эмоций, на цифрах. Это и есть «бюджет как валюта»: он переводит инженерное «надо подождать» и продуктовое «надо быстрее» в <strong>единицу измерения, значимую обеим сторонам</strong>.</p>
<p>Это сводится к трём операционным следствиям, которые я уже разбирал в Шаге 4 как «на что тратится бюджет»: решение «деплоить vs морозить», переключение на reliability при систематическом сгорании, осознанная трата бюджета на учения и эксперименты. Здесь добавляется только одно: каждое из этих следствий теперь становится <strong>цифровым аргументом в диалоге</strong>, а не «инженерным интуитивным решением».</p>
<h3 id="что-разрушает-бюджет-как-арбитр">Что разрушает «бюджет как арбитр»</h3>
<ul>
<li><strong>Несогласованный SLO.</strong> Если бюджет в глазах продакта — «инженерная штука», он не примет его как аргумент. SLO нужно <strong>согласовывать</strong> с продуктом, а не объявлять.</li>
<li><strong>SLO без явного оперативного следствия.</strong> Если «бюджет сгорел» не запускает freeze автоматически, это просто график на дашборде. Должна быть <strong>записанная политика</strong>, что делать.</li>
<li><strong>Слишком жёсткое SLO.</strong> Если SLO нереалистичный («99.99% на сервисе, который объективно может только 99.9%»), бюджет всё время в минусе, и команда привыкает его игнорировать. Лучше реалистичный SLO с дисциплиной, чем красивая цифра, в которую никто не верит.</li>
</ul>
<p>Бюджет — это не магический инструмент, а <strong>дисциплина</strong>. Он работает, если команда договорилась его соблюдать. Не работает — если нет.</p>
<h2 id="типичные-ошибки-выбора-slislo-и-чек-лист">Типичные ошибки выбора SLI/SLO и чек-лист</h2>
<p>Соберу в один блок самые частые ошибки, которые ломают SLO-практику. Часть из них уже разобрана в основных разделах — в этом блоке они сведены в одну строку для самопроверки. Развёрнутые контрпримеры — в соответствующих главах выше.</p>
<h3 id="ошибка-1-sli--всё-что-не-5xx">Ошибка 1. SLI = «всё, что не 5xx»</h3>
<p><strong>Правильно:</strong> SLI — это <code>success AND fast</code>, и оба критерия записаны явно. Полный разбор — в Шаге 3 (про «хорошее событие»).</p>
<h3 id="ошибка-2-slo-по-системе-вместо-slo-по-шагу-пути">Ошибка 2. SLO «по системе» вместо SLO «по шагу пути»</h3>
<p><strong>Правильно:</strong> SLO живёт на каждом шаге критичного пути, а не на сервисе в целом. Развёрнуто — в Шаге 1 (ловушка «среднего по системе»).</p>
<h3 id="ошибка-3-slo-без-error-budget-policy">Ошибка 3. SLO без error budget policy</h3>
<p>SLO написано, бюджет считается, но что делать, когда бюджет сгорел — не записано. В итоге команда живёт в режиме «дашборд красный, ну ладно, продолжаем выкатку».</p>
<p><strong>Правильно:</strong> записанная политика: при превышении burn-rate <code>X×</code> за окно <code>Y</code> минут — freeze деплоев до конца окна / привлечение reliability-команды / эскалация. Без процедуры SLO — это украшение. Развёрнуто — в разделе «Что разрушает „бюджет как арбитр“».</p>
<h3 id="ошибка-4-slo--sla">Ошибка 4. SLO = SLA</h3>
<p><strong>Правильно:</strong> между SLO и SLA — запас один-два разряда <code>9</code>. Подробнее — в разделе «SLA: что уходит наружу».</p>
<h3 id="ошибка-5-среднее-вместо-перцентилей">Ошибка 5. Среднее вместо перцентилей</h3>
<p><strong>Правильно:</strong> SLI на latency — это перцентиль (P95/P99), не среднее. Развёрнуто — в Шаге 3, блок «Что мы НЕ берём как SLI».</p>
<h3 id="ошибка-6-sli-на-cpumemory-utilization">Ошибка 6. SLI на CPU/Memory utilization</h3>
<p><strong>Правильно:</strong> SLI — на пользовательских следствиях (latency, errors). Утилизация — служебная метрика для USE-дашборда. Развёрнуто — в Шаге 3, блок «Что мы НЕ берём как SLI».</p>
<h3 id="ошибка-7-слишком-много-sli">Ошибка 7. Слишком много SLI</h3>
<p>«У нас 50 SLI на сервис» — означает, что SLI никто не читает, и алерты по ним — шум. SLI должен фокусировать внимание, а не размывать его.</p>
<p>Маленькое терминологическое уточнение, без которого пункт читается двусмысленно: <strong>«сервис» здесь — пользовательский сервис в смысле CUJ</strong>, а не каждый микросервис в твоей архитектуре. На один CUJ (поиск каталога) — 4–8 SLI; на ranking-микросервис, который обслуживает этот же CUJ, не нужно отдельно вешать ещё 4–8. Иначе «50 SLI» получаются арифметически — суммой по микросервисам, — и читать их никто не будет.</p>
<p><strong>Правильно:</strong> 4–8 SLI на пользовательский сервис (CUJ), привязанных к ключевым шагам пути. Если хочется больше — это, скорее всего, сигнал, что внутри одного «сервиса» спрятаны два CUJ, и его пора разделить. Развёрнуто — в Шаге 3, блок «Конкретные SLI для нашего кейса».</p>
<h3 id="ошибка-8-slo-выбран-копированием">Ошибка 8. SLO выбран копированием</h3>
<p>«У Google 99.99%, у нас тоже будет». Без разговора с бизнесом, без расчёта стоимости, без понимания, тянет ли архитектура.</p>
<p><strong>Правильно:</strong> SLO выбирается из двух чисел — что <strong>окупается</strong> (бизнес) и что <strong>достижимо</strong> (архитектура). Между ними может быть зазор — он отражает либо недоинвест в reliability, либо переинвест в обещания.</p>
<h3 id="ошибка-9-slo-не-пересматривается">Ошибка 9. SLO не пересматривается</h3>
<p>Поставили SLO 99.9% год назад, написали в Confluence, забыли. Бизнес-контекст изменился, нагрузка выросла в 10 раз, продукт перешёл в зрелую фазу — SLO остался прежним.</p>
<p><strong>Правильно:</strong> SLO пересматривается раз в квартал минимум, в idle-режиме — раз в полгода. Каждый пересмотр — короткий ритуал «изменилось ли что-то в бизнесе или в системе, что меняет нужное число».</p>
<h3 id="ошибка-10-slo-выбран-без-учёта-зависимостей">Ошибка 10. SLO выбран без учёта зависимостей</h3>
<p>Сервис обещает 99.99%, но критически зависит от внешнего сервиса с 99.5%. Математика: твой SLO не может быть выше произведения SLO твоих критичных зависимостей.</p>
<p><strong>Правильно:</strong> при выборе SLO учитывай <strong>dependency budget</strong>. Если у тебя 3 последовательные критичные зависимости с 99.9% каждая, твой достижимый максимум — <code>~99.7%</code>. Для опциональных или параллельных зависимостей формула мягче — но любую цепочку нужно явно проговорить.</p>
<h3 id="чек-лист-10-вопросов-до-утверждения-slo">Чек-лист: 10 вопросов до утверждения SLO</h3>
<p>Если ответ «нет» хотя бы на один — SLO ещё не готов.</p>
<ul>
<li><input disabled="" type="checkbox"> Привязан ли SLO к конкретному шагу критичного пути, а не к сервису целиком?</li>
<li><input disabled="" type="checkbox"> Прописаны ли числитель, знаменатель и окно агрегации?</li>
<li><input disabled="" type="checkbox"> Пройдён ли тест «5xx за 5 секунд = плохое событие»?</li>
<li><input disabled="" type="checkbox"> Используется ли перцентиль, а не среднее?</li>
<li><input disabled="" type="checkbox"> Есть ли error budget в абсолютных значениях для текущего объёма трафика?</li>
<li><input disabled="" type="checkbox"> Есть ли записанная error budget policy (что делать при сгорании)?</li>
<li><input disabled="" type="checkbox"> Есть ли запас между SLO и SLA (если SLA вообще обещаем)?</li>
<li><input disabled="" type="checkbox"> Согласованы ли числа с бизнесом (а не «команда придумала»)?</li>
<li><input disabled="" type="checkbox"> Учтены ли SLO критичных зависимостей в верхней границе нашего?</li>
<li><input disabled="" type="checkbox"> Назначена ли дата следующего пересмотра?</li>
</ul>
<h2 id="заключение-slo--это-мост-между-системой-и-деньгами">Заключение: SLO — это мост между системой и деньгами</h2>
<p>Я начинал статью с тезиса, что архитектура без SLO — это эстетика, не инженерия. К концу хочется его уточнить.</p>
<p>SLO — не просто <strong>constraint</strong> на архитектуру, хотя я почти всю статью акцентировал именно это. SLO — это <strong>общий язык</strong> между четырьмя ролями, которые иначе говорят на разных:</p>
<ul>
<li><strong>Продакт / аналитик</strong> говорит: конверсия, retention, GMV, NPS.</li>
<li><strong>Бэкенд</strong> говорит: P99, шарды, hit rate, репликация.</li>
<li><strong>SRE</strong> говорит: error budget, burn rate, MTTR, blast radius.</li>
<li><strong>Бизнес</strong> говорит: деньги, репутация, регуляторика.</li>
</ul>
<p>SLO — это формат, в котором все четверо могут договориться. «Поднять availability с 99.9% до 99.99% стоит 4× инфраструктуры и сэкономит N часов даунтайма в год, что на нашем GMV $X — это <code>Y</code> денег. Делаем или нет?». Этот вопрос имеет ответ, к которому пришли все четверо. Без SLO такого вопроса просто не существует — есть только предчувствия.</p>
<h3 id="что-сделать-в-понедельник">Что сделать в понедельник</h3>
<ol>
<li><strong>Возьми один пользовательский путь и разложи его на шаги.</strong> Не сервис целиком — один сценарий, шесть-семь шагов, как в нашем кейсе. На каждый шаг — кандидаты в SLI. Это упражнение делается за два часа и показывает, где ты измеряешь не то.</li>
<li><strong>Сравни свои текущие SLO с реальными зависимостями.</strong> Если твой SLO выше произведения SLO критичных зависимостей — ты обещаешь больше, чем можешь дать. Цифру лучше пересмотреть до того, как её пересмотрит пользователь.</li>
<li><strong>Напиши error budget policy на одну страницу.</strong> Что происходит при превышении burn-rate <code>X×</code> за <code>Y</code> минут? Кто принимает решение freeze? Без этого документа SLO остаётся графиком на дашборде, а не дисциплиной.</li>
</ol>
<p>Спасибо, что дочитал до конца. Эта статья — переработанная версия плана несостоявшегося доклада для <a href="https://t.me/system_design_world" rel="noopener noreferrer" target="_blank">System Design World</a>. Если у тебя есть свой опыт работы с SLO в системе высокой нагрузки — особенно интересны истории, как менялся выбор SLO с ростом продукта или после крупного инцидента — пиши в комменты или в мой Telegram-канал «<a href="https://t.me/&#43;gLeURJwFz5Q4NTA6" rel="noopener noreferrer" target="_blank">Мишка на сервере</a>».</p>
<p>Чертёж — это половина дела. Как именно выглядит тот самый диалог с бизнесом, без которого SLO остаётся картинкой на дашборде, я разбираю в продолжении: <a href="/posts/reliability-is-a-conversation/">«Надёжность строится в диалоге с бизнесом»</a>.</p>
<p>А если нужен не разбор одного кейса, а список того, что вокруг SLO вообще надо уметь и на каком уровне, — я собираю его в карте компетенций: <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/slo-engineering/" rel="noopener noreferrer" target="_blank">SLO Engineering</a> про инженерную часть и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/composite-slo-methodology/" rel="noopener noreferrer" target="_blank">Composite SLO Methodology</a> — про то самое произведение SLO зависимостей из второго пункта выше.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! На системном дизайне 9 из 10 кандидатов начинают с компонентов: API gateway, Kafka, Redis, Postgres-реплики, шардинг, CDN. Это не дизайн — это плакат с логотипами. Дизайн начинается с вопроса, на который почти никто не отвечает первым: «какой надёжности я обещаю и почему именно этой, а не другой?».</p>
<p>Без числа в этом ответе — <code>99.5</code>, <code>99.9</code>, <code>99.99</code> — все архитектурные решения ниже становятся вопросом эстетики. С числом — превращаются в инженерные ограничения, которые решают за тебя, нужна ли вторая БД-реплика, имеет ли смысл repair-aware кэш и сколько регионов держать в active-active.</p>
<p>В этой статье я беру одну каноничную задачу с собеса — <strong>поиск каталога маркетплейса</strong> — и прокручиваю её сквозь три уровня SLO: <code>99.0%</code>, <code>99.9%</code>, <code>99.99%</code>. На каждом уровне меняется не «настройка пары флагов», а сама архитектура. Главный тезис: SLI/SLO — это не SRE-декорация поверх готовой системы, а <strong>интерфейс между System Design и бизнесом</strong>. Тот самый, через который аналитик, бэкенд и SRE говорят на одном языке.</p>
<h2 id="что-ты-унесёшь-из-статьи">Что ты унесёшь из статьи</h2>
<p>Статья длинная (~55 000 знаков), поэтому сразу — что заберёт каждая ЦА:</p>
<ul>
<li><strong>Аналитик / продакт.</strong> Рамка для перевода бизнес-метрик в SLI и для разговора с инженерами в стиле «а если снизим SLO с 99.99% до 99.9% — что освободится?».</li>
<li><strong>Бэкенд-инженер.</strong> Как один и тот же поиск каталога превращается в три разные системы при смене таргета SLO — read path, кэш, индекс, failover.</li>
<li><strong>SRE / DevOps.</strong> Error budget как валюта в спорах «фича vs рефакторинг» + типовые ошибки выбора SLI: что делать, когда дашборд зелёный, а пользователи — в саппорте.</li>
</ul>
<p>Общий блок для всех: разница между <strong>Observability</strong>, <strong>Monitoring</strong> и <strong>Alerting</strong> — в одной схеме.</p>
<h2 id="tldr-если-читать-дальше-уже-некогда">TL;DR (если читать дальше уже некогда)</h2>
<p>Если у тебя нет 30 минут на длинный разбор — четыре тезиса, к которым приходит статья:</p>
<ol>
<li><strong>SLO выбирается до архитектуры, а не после.</strong> Поднять SLO задним числом — это переписать систему, а не «добавить мониторинга». Снизить — освободить бюджет, но не вернуть деньги.</li>
<li><strong>Один и тот же кейс на трёх SLO (<code>99.0%</code> / <code>99.9%</code> / <code>99.99%</code>) — это три разные системы.</strong> Меняется не «настройка флагов», а топология, технологии, capacity, RTO/RPO, операционная модель и стоимость (<code>1×</code> → <code>4–6×</code> → <code>15–25×</code> baseline).</li>
<li><strong>SLI вешаются на шаги критичного пользовательского пути (CUJ), а не «на сервис в целом».</strong> «99.9% по системе» — это псевдо-метрика, которая прячет деградацию ключевого шага за здоровьем фоновых эндпоинтов.</li>
<li><strong>Error budget работает как валюта в спорах «фича vs рефакторинг», только если есть записанная error budget policy.</strong> Без неё SLO — это график на дашборде, а не дисциплина.</li>
</ol>
<p>Дальше — как до этих четырёх тезисов прийти через один сквозной кейс. Если интересует только инженерная кульминация — переходи сразу к разделу <a href="#%d0%be%d0%b4%d0%b8%d0%bd-%d0%ba%d0%b5%d0%b9%d1%81--%d1%82%d1%80%d0%b8-slo--%d1%82%d1%80%d0%b8-%d0%b0%d1%80%d1%85%d0%b8%d1%82%d0%b5%d0%ba%d1%82%d1%83%d1%80%d1%8b">Один кейс — три SLO — три архитектуры</a>.</p>
<h2 id="кейс-поиск-каталога-маркетплейса">Кейс: поиск каталога маркетплейса</h2>
<p>Чтобы не разговаривать абстракциями, возьмём одну сквозную задачу. Формулирую как на собеседовании:</p>
<blockquote><p>Спроектируйте сервис поиска и выдачи карточек товаров для крупного маркетплейса. На вход — поисковый запрос пользователя и фильтры (категория, цена, доставка, регион). На выход — отранжированный список карточек с превью, пагинация, фасеты.</p>
</blockquote><p>Параметры — реалистичные для маркетплейса масштаба Ozon / Wildberries / Avito, но «приглаженные» под удобство арифметики:</p>
<table>
	<thead>
			<tr>
					<th>Характеристика</th>
					<th>Значение</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Активный каталог</td>
					<td>~50M товаров</td>
			</tr>
			<tr>
					<td>Пиковый RPS на поиск</td>
					<td>~30 000 запросов/сек</td>
			</tr>
			<tr>
					<td>Активных одновременных пользователей</td>
					<td>~200 000</td>
			</tr>
			<tr>
					<td>Доля mobile-трафика</td>
					<td>~70%</td>
			</tr>
			<tr>
					<td>География</td>
					<td>5 регионов, основной — central RU</td>
			</tr>
			<tr>
					<td>Целевая P50 латентность read path</td>
					<td>&lt; 150 ms</td>
			</tr>
			<tr>
					<td>Целевая P99 латентность read path</td>
					<td>&lt; 500 ms</td>
			</tr>
			<tr>
					<td>Сезонные пики (BF, распродажи)</td>
					<td>x3–x5 от обычного RPS</td>
			</tr>
	</tbody>
</table>
<p>Бизнес-контекст, про который часто забывают на собесе, и зря:</p>
<ul>
<li><strong>Поиск — главный драйвер конверсии.</strong> По публичным отчётам (SimilarWeb, ContentSquare, материалы крупных маркетплейсов) на больших площадках 40–70% покупок начинается с поисковой строки, не с категорий или баннеров. Конкретный процент гуляет по сегментам и площадкам, но порядок одинаков. Любая деградация поиска — это прямая просадка GMV, не «инженерная неприятность».</li>
<li><strong>Эластичность по latency измерима.</strong> Цифры из публикаций Amazon (Greg Linden, 2006), Akamai (2017), Google/SOASTA (2017) и Deloitte (2020) разнятся по методологии и сегменту, но порядок сходится: каждые <code>+100 ms</code> latency на ключевом read path стоят примерно <code>−0.5…1.5%</code> конверсии. Для маркетплейса с GMV $10M в день это $50–150k недополученной выручки <strong>в день</strong>, если регресс закрепится. Это не разовый ущерб, а просадка, в которую конвертируется latency на ключевом read path.</li>
<li><strong>Стоимость недоступности нелинейна.</strong> 5 минут даунтайма обычным вторником и 5 минут даунтайма в Чёрную пятницу — это разные деньги в десятки раз. SLO должно учитывать сезонность, иначе оно меряет не то.</li>
</ul>
<p>Эти три факта — ровно то, что превращает SLI/SLO из «инженерной гигиены» в инструмент бизнес-разговора. Я буду к ним возвращаться в каждом следующем разделе.</p>
<h2 id="что-обычно-ломается-на-собесе-без-slo">Что обычно ломается на собесе без SLO</h2>
<p>Послушаешь сотню system-design интервью — и складывается узнаваемый паттерн ответа.</p>
<h3 id="типичная-траектория">Типичная траектория</h3>
<ol>
<li><strong>Уточнили требования.</strong> «Сколько пользователей? Какая нагрузка?» — кандидат услышал «много», и в голове щёлкнуло «значит, надо много всего».</li>
<li><strong>Нарисовали API.</strong> REST с парой эндпоинтов, gateway, auth-middleware. Пока без сюрпризов.</li>
<li><strong>Добавили слой данных.</strong> Postgres / MySQL — «реляционка для каталога норм». Кто-то сразу предложит Elasticsearch, потому что слышал про full-text search.</li>
<li><strong>Поставили кэш.</strong> Redis — потому что «Redis fast». Без обсуждения, что именно кэшируем, какой hit rate ожидаем, как инвалидируем при изменении цены или остатков.</li>
<li><strong>Подняли реплики.</strong> «На чтение пойдут реплики, на запись — мастер». Без обсуждения, насколько асинхронная репликация критична: а если пользователь не увидит свой только что выложенный товар?</li>
<li><strong>Зашардировали.</strong> Чаще всего по <code>category_id</code> или <code>seller_id</code>. Потому что «у больших так».</li>
<li><strong>Накинули CDN и очередь.</strong> Kafka — куда же без неё. Async-обработка событий, чтобы «развязать сервисы».</li>
</ol>
<p>К концу интервью на доске — красивая распределённая система. Интервьюер кивает. Кандидат уверен, что справился.</p>
<h3 id="что-в-этой-траектории-не-так">Что в этой траектории не так</h3>
<p>Простой тест. Ответь сам себе: <strong>какое из решений выше изменилось бы, если бы требование к доступности было <code>99.0%</code>, а не <code>99.99%</code>?</strong> А если бы P99 была не 500 ms, а 2 секунды?</p>
<p>Если ответ «никакое» — значит, дизайн не привязан к требованиям к надёжности. Кандидат строил систему «как принято», а не «как нужно бизнесу». А раз все решения принимались независимо от целевого SLO — значит, любые из них могут быть и преждевременными, и недостаточными одновременно. Точно сказать нельзя, потому что точки отсчёта нет.</p>
<p>Чтобы было совсем больно — разберём по решениям:</p>
<ul>
<li><strong>Кэш Redis.</strong> Имеет смысл, если твой SLO по latency — десятки миллисекунд для горячих запросов или ты упираешься в нагрузку на БД при текущем availability-таргете. При расслабленном SLO P99 = 2 секунды кэш в read path — преждевременная оптимизация: ты поднимаешь эксплуатационную сложность (инвалидация, прогрев, репликация Redis-кластера, отдельный SLO на сам кэш) ради цели, которая выполнима и без него.</li>
<li><strong>Реплики на чтение.</strong> Имеют смысл, если твой SLO availability такой, что мастер-only архитектура не уложится в бюджет ошибок при типовом MTTR. На SLO <code>99.0%</code> (~7 часов даунтайма в месяц) одного хорошо настроенного primary с мониторингом и быстрым backup-восстановлением часто достаточно.</li>
<li><strong>Шардинг.</strong> Оправдан, когда объём данных или write QPS упирается в один инстанс физически. До этого — это удвоение операционной нагрузки (rebalancing, hot shards, резолв cross-shard запросов) без выигрыша по SLI, ради которого его делают.</li>
<li><strong>Multi-region active-active.</strong> Имеет смысл при SLO availability <code>99.99%</code>+ или когда регуляторика обязывает (data residency, ФЗ-149/ФЗ-152/ФЗ-242/GDPR-аналоги). На <code>99.9%</code> обычно дешевле и предсказуемее active-passive с автоматическим failover и RTO в минутах.</li>
</ul>
<p>Каждое из этих решений — правильное в одних условиях и преждевременное в других. Условия задаёт SLO. Без него ты выбираешь архитектуру не под задачу, а под насмотренность.</p>
<h3 id="главный-сдвиг--направление-дизайна">Главный сдвиг — направление дизайна</h3>
<p>Хороший дизайн идёт <strong>сверху вниз</strong>: бизнес-цель → критичный пользовательский путь (CUJ, critical user journey) → SLI на каждый шаг этого пути → SLO как контракт → архитектура, которая в этот SLO укладывается с разумным запасом.</p>
<p>Плохой дизайн идёт <strong>снизу вверх</strong>: «у крутых ребят такая схема — нарисуем такую же». На выходе получается архитектура, в которую любой SLO «впихнуть можно постфактум» — потому что под её требования не задавали ни одного числа, и теперь любая цифра одинаково годится для отчётности и одинаково плохо отражает реальное поведение системы.</p>
<p>В следующих разделах я разворачиваю верхний путь — сверху вниз — на нашем кейсе с маркетплейсом. Начнём с шага 1: как из функциональных требований получается тот самый критичный пользовательский путь.</p>
<h2 id="шаг-1-от-фт-к-критичному-пользовательскому-пути">Шаг 1: от ФТ к критичному пользовательскому пути</h2>
<p>Раз направление дизайна сверху вниз — нужна точка отсчёта на самом верху. Это <strong>функциональные требования (ФТ)</strong>: что система делает в терминах пользователя и продукта. Не «использует Elasticsearch», а «возвращает релевантную выдачу по запросу».</p>
<h3 id="фт-для-нашего-кейса">ФТ для нашего кейса</h3>
<ul>
<li>Принять поисковый запрос (текст + фильтры) от авторизованного и анонимного пользователя.</li>
<li>Подбирать товары из активного каталога с учётом региона и доступной доставки.</li>
<li>Ранжировать результаты с учётом релевантности, цены, рейтинга продавца и персонализации.</li>
<li>Возвращать пагинированную выдачу с фасетами (категории, ценовые диапазоны, бренды).</li>
<li>Поддерживать sorting (price asc/desc, popularity, rating).</li>
<li>Подсказки и автодополнение по мере набора запроса.</li>
</ul>
<p>Это всё одинаково «функциональные требования» — но <strong>не одинаково критичные</strong>. Если автодополнение тормозит на 200 ms, пользователь поморщится. Если выдача не приходит вовсе — он закроет вкладку и пойдёт к конкуренту. ФТ сами по себе плоские, в них нет приоритета. Этот приоритет приносит <strong>критичный пользовательский путь</strong>.</p>
<h3 id="критичный-пользовательский-путь-cuj--critical-user-journey">Критичный пользовательский путь (CUJ — critical user journey)</h3>
<p>Это сценарий пользователя, который напрямую кормит ключевую бизнес-метрику. У маркетплейса бизнес-метрика — GMV, точка входа в неё — поиск, точка выхода — клик в карточку и далее в корзину. Получаем такой путь:</p>
<blockquote><p>Пользователь приходит на сайт с намерением купить → формулирует запрос → получает релевантную выдачу → выбирает товар → переходит в карточку.</p>
</blockquote><p>Это и есть «золотой путь», за который держимся в дизайне. Всё, что не обслуживает этот путь напрямую (статистика для аналитики, A/B-эксперименты, персональные рекомендации в стороннем виджете), может быть менее надёжным — и это нормально.</p>
<aside class="callout callout--note" role="note">
  <div class="callout__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
  <div class="callout__body"><p class="callout__title">Терминологическая дисциплина</p>
    <div class="callout__content"><p>Дальше по тексту слово <strong>«сервис»</strong> работает в двух разных значениях, и я их буду стараться разводить явно:</p>
<ul>
<li><strong>Пользовательский сервис</strong> — это CUJ (поиск каталога целиком). К нему привязаны SLI и SLO.</li>
<li><strong>Микросервис</strong> — это юнит деплоя (Search Service, Ranking Service на схеме архитектуры). К нему привязаны Golden Signals для внутренней диагностики, но <strong>не отдельный набор SLI</strong>.</li>
</ul>
<p>Один пользовательский сервис может реализовываться десятью микросервисами — но SLI по-прежнему 4–8, не 4–8 × 10. К этому различию ещё вернёмся в разделе про типичные ошибки.</p></div>
  </div>
</aside><h3 id="декомпозиция-пути-на-шаги">Декомпозиция пути на шаги</h3>
<p>Дальше путь режется на дискретные шаги, каждый из которых станет отдельной точкой измерения:</p>
<table>
	<thead>
			<tr>
					<th>#</th>
					<th>Шаг</th>
					<th>Что делает система</th>
					<th>Что замечает пользователь</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>Загрузка поискового интерфейса</td>
					<td>Отдать HTML/SPA-bundle, инициализировать клиент</td>
					<td>Страница появилась, поле поиска кликабельно</td>
			</tr>
			<tr>
					<td>2</td>
					<td>Автодополнение (suggest)</td>
					<td>На каждое нажатие клавиши вернуть top-N подсказок</td>
					<td>Подсказки появляются «без задержки»</td>
			</tr>
			<tr>
					<td>3</td>
					<td>Отправка запроса</td>
					<td>Получить query + фильтры, направить в search backend</td>
					<td>Лоадер крутится недолго</td>
			</tr>
			<tr>
					<td>4</td>
					<td>Получение выдачи</td>
					<td>Поиск в индексе, ранжирование, обогащение карточек</td>
					<td>Выдача отрисовалась, скроллится плавно</td>
			</tr>
			<tr>
					<td>5</td>
					<td>Применение фильтра / сортировки</td>
					<td>Реквест с уточнёнными параметрами</td>
					<td>Список перестроился без рывков</td>
			</tr>
			<tr>
					<td>6</td>
					<td>Клик в карточку</td>
					<td>Переход на страницу товара, прелоад данных</td>
					<td>Карточка открылась, цена и наличие актуальны</td>
			</tr>
	</tbody>
</table>
<p>Эти шесть шагов — каркас, на котором будет висеть всё дальнейшее. На каждый шаг повесим SLI, на каждый SLI — SLO. И увидим, что они <strong>не равноценны</strong>: шаг 4 (получение выдачи) для бизнеса принципиально важнее, чем шаг 2 (автодополнение). А значит, и инженерных усилий на их надёжность нужно несоизмеримо больше.</p>
<h3 id="почему-это-важно-ловушка-среднего-по-системе">Почему это важно: ловушка «среднего по системе»</h3>
<p>На многих собеседованиях кандидат, отвечая на «какую ты доступность хочешь», говорит «99.9% по системе». Это бесполезное число: если оно посчитано как «суммарный аптайм всех эндпоинтов», оно скрывает то, что выдача упала на 30 минут, а суммарно «вышло 99.9%, потому что autocomplete и health-чеки крутились». Пользователь страдал, дашборд зелёный.</p>
<p>Шаги критичного пути — антидот этой ловушки. Каждый шаг получает свою цифру, и провал шага 4 невозможно «размазать» доступностью соседних шагов.</p>
<h2 id="шаг-2-нфт-как-свойства-шагов-а-не-системы">Шаг 2: НФТ как свойства шагов, а не системы</h2>
<p>Когда есть карта шагов, нефункциональные требования (НФТ) перестают быть размытым «надёжно, быстро, безопасно». Они становятся набором конкретных свойств <strong>каждого шага</strong>.</p>
<h3 id="свойства-которые-нас-интересуют">Свойства, которые нас интересуют</h3>
<p>Для read-heavy критичного пути маркетплейса базовый набор НФТ выглядит так:</p>
<ul>
<li><strong>Латентность.</strong> Сколько миллисекунд занял шаг. Меряем перцентилями (P50/P95/P99), не средним — оно скрывает хвосты, а хвосты и есть пользовательская боль.</li>
<li><strong>Доступность.</strong> Доля успешных шагов. Не «200/всего HTTP», а именно «шаг выполнен корректно с точки зрения сценария».</li>
<li><strong>Корректность.</strong> Получил ли пользователь ожидаемый результат. Для шага 4 — вернулась ли релевантная выдача или 0 результатов из-за бага в ранжировании.</li>
<li><strong>Свежесть.</strong> Насколько актуальны данные. Цена и наличие на карточке после клика — критичная свежесть; popularity-score в ранжировании — терпит часовую задержку.</li>
<li><strong>Целостность.</strong> Не «исчезает» ли часть результатов из-за частичного сбоя ноды или таймаута к шарду. Это специфично для распределённого поиска и шардированных индексов.</li>
</ul>
<h3 id="привязка-свойств-к-шагам-пути">Привязка свойств к шагам пути</h3>
<p>Не каждое свойство одинаково важно на каждом шаге. Покажу таблицей, какие НФТ для каких шагов нашего кейса критичны:</p>
<table>
	<thead>
			<tr>
					<th>Шаг</th>
					<th>Латентность</th>
					<th>Доступность</th>
					<th>Корректность</th>
					<th>Свежесть</th>
					<th>Целостность</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1. Загрузка интерфейса</td>
					<td>средне</td>
					<td>высоко</td>
					<td>низко</td>
					<td>низко</td>
					<td>низко</td>
			</tr>
			<tr>
					<td>2. Autocomplete</td>
					<td>высоко</td>
					<td>средне</td>
					<td>средне</td>
					<td>низко</td>
					<td>низко</td>
			</tr>
			<tr>
					<td>3. Отправка запроса</td>
					<td>средне</td>
					<td>высоко</td>
					<td>низко</td>
					<td>низко</td>
					<td>низко</td>
			</tr>
			<tr>
					<td>4. Получение выдачи</td>
					<td><strong>высоко</strong></td>
					<td><strong>высоко</strong></td>
					<td><strong>высоко</strong></td>
					<td>средне</td>
					<td><strong>высоко</strong></td>
			</tr>
			<tr>
					<td>5. Фильтр / сортировка</td>
					<td>высоко</td>
					<td>высоко</td>
					<td>высоко</td>
					<td>средне</td>
					<td>высоко</td>
			</tr>
			<tr>
					<td>6. Клик в карточку</td>
					<td>средне</td>
					<td>высоко</td>
					<td>высоко</td>
					<td><strong>высоко</strong></td>
					<td>низко</td>
			</tr>
	</tbody>
</table>
<p>Сразу видно: шаг 4 — концентратор НФТ, на нём горят почти все колонки. Шаг 6 особенно требователен к свежести — потому что показать пользователю карточку с «вчерашней» ценой (а на чекауте уже другая) — это худший UX и часто прямой ущерб бизнесу через возвраты и репутацию.</p>
<h3 id="главное-смещение-от-привычных-нфт-из-тз">Главное смещение от привычных НФТ из ТЗ</h3>
<p>Классические нефункциональные требования — «система должна обеспечивать 99.9% доступности» — это <strong>средневзвешенное по чему попало</strong>. Они одинаково оценивают и autocomplete, и финальную выдачу. В реальной эксплуатации это приводит к двум симметричным болям:</p>
<ol>
<li><strong>Деньги тратятся не туда.</strong> Команда вкладывает усилия в надёжность шагов, которые этого не требуют (autocomplete с 99.99% — приятно, но конверсию не трогает), а критичные шаги остаются на «общем уровне».</li>
<li><strong>Алерты молчат, когда болит.</strong> Дашборд показывает зелёный «99.9% по системе», пока шаг 4 деградирует и теряет каждую десятую выдачу. Эту потерю топит «здоровье» десятка других эндпоинтов и фоновых хелсчеков.</li>
</ol>
<p>НФТ-по-шагам убирают эту усреднённость. Следующий шаг сверху вниз — превращаем эти свойства в <strong>измеримые SLI</strong>, чтобы было что мерить и за что отчитываться. Но прежде чем давать формулу — нужен общий словарь, без него разговор про SLI получится поверхностным.</p>
<h2 id="шаг-3-sli-как-индикатор-пользовательского-опыта">Шаг 3: SLI как индикатор пользовательского опыта</h2>
<h3 id="сначала--общий-словарь-observability--monitoring--alerting">Сначала — общий словарь: Observability ≠ Monitoring ≠ Alerting</h3>
<p>В SRE-практике три термина часто валят в одну кучу, и тогда любой разговор про SLI разваливается на терминологические уточнения. Развожу их через <strong>цель</strong> каждой практики:</p>
<ul>
<li><strong>Monitoring (мониторинг)</strong> отвечает на заранее заданные вопросы. Ты знаешь, что хочешь видеть: «жив ли primary», «какая P99 у эндпоинта <code>/search</code>», «не растёт ли latency». На каждую заготовку — метрика или дашборд. Это про <strong>known unknowns</strong> — вещи, о которых ты знаешь, что они могут пойти не так, и хочешь их видеть.</li>
<li><strong>Observability (наблюдаемость)</strong> — свойство системы, которое позволяет дойти до <strong>нового</strong> вопроса. Не «жив ли primary», а «почему именно в этом конкретном запросе latency 4 секунды». Высокая observability означает, что у тебя достаточно данных (метрик, структурированных логов, трейсов с tail-based sampling, событий) и инструментов поверх них, чтобы задать системе вопрос, который ты не предусмотрел заранее. Это про <strong>unknown unknowns</strong> — про неожиданности, которых ты не ждал. Такое разведение «monitoring = known unknowns / observability = unknown unknowns» — рамка из «Distributed Systems Observability» Cindy Sridharan (O&rsquo;Reilly, 2018) и развитая инженерами Honeycomb (Charity Majors et al.); внутри Google SRE Workbook грань между ними размытее, и я сознательно беру более жёсткую формулировку — на ней легче строить дизайн.</li>
<li><strong>Alerting (алертинг)</strong> — действие по событию. Это слой, надстроенный над мониторингом и observability: правила, которые превращают наблюдаемое состояние в инцидент с пейджингом. Алерт — это не метрика, это <strong>обещание разбудить человека</strong>, и за него надо отвечать. Alert fatigue — отдельная боль, и лечится она burn-rate-алертами, а не увеличением количества правил.</li>
</ul>
<p>Связь между ними — на одной схеме:</p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/02e1152b1ac5323bffbbebf499019cc686d27dbc624b8e2df7c0e398bbfeb612.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-02e1152b1ac5-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1109.69px; background-color: transparent;" viewBox="0 0 1109.6875 390.375" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-02e1152b1ac5-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-02e1152b1ac5-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-light .error-icon{fill:#dce0e8;}#m-02e1152b1ac5-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-02e1152b1ac5-light .edge-thickness-normal{stroke-width:1px;}#m-02e1152b1ac5-light .edge-thickness-thick{stroke-width:3.5px;}#m-02e1152b1ac5-light .edge-pattern-solid{stroke-dasharray:0;}#m-02e1152b1ac5-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-02e1152b1ac5-light .edge-pattern-dashed{stroke-dasharray:3;}#m-02e1152b1ac5-light .edge-pattern-dotted{stroke-dasharray:2;}#m-02e1152b1ac5-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-02e1152b1ac5-light .marker.cross{stroke:#5c5f77;}#m-02e1152b1ac5-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-02e1152b1ac5-light p{margin:0;}#m-02e1152b1ac5-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-02e1152b1ac5-light .cluster-label text{fill:#4c4f69;}#m-02e1152b1ac5-light .cluster-label span{color:#4c4f69;}#m-02e1152b1ac5-light .cluster-label span p{background-color:transparent;}#m-02e1152b1ac5-light .label text,#m-02e1152b1ac5-light span{fill:#4c4f69;color:#4c4f69;}#m-02e1152b1ac5-light .node rect,#m-02e1152b1ac5-light .node circle,#m-02e1152b1ac5-light .node ellipse,#m-02e1152b1ac5-light .node polygon,#m-02e1152b1ac5-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-02e1152b1ac5-light .rough-node .label text,#m-02e1152b1ac5-light .node .label text,#m-02e1152b1ac5-light .image-shape .label,#m-02e1152b1ac5-light .icon-shape .label{text-anchor:middle;}#m-02e1152b1ac5-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-02e1152b1ac5-light .rough-node .label,#m-02e1152b1ac5-light .node .label,#m-02e1152b1ac5-light .image-shape .label,#m-02e1152b1ac5-light .icon-shape .label{text-align:center;}#m-02e1152b1ac5-light .node.clickable{cursor:pointer;}#m-02e1152b1ac5-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-02e1152b1ac5-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-02e1152b1ac5-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-02e1152b1ac5-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-02e1152b1ac5-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-02e1152b1ac5-light .edgeLabel p{background-color:#eff1f5;}#m-02e1152b1ac5-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-02e1152b1ac5-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-02e1152b1ac5-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-02e1152b1ac5-light .cluster text{fill:#4c4f69;}#m-02e1152b1ac5-light .cluster span{color:#4c4f69;}#m-02e1152b1ac5-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-02e1152b1ac5-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-02e1152b1ac5-light rect.text{fill:none;stroke-width:0;}#m-02e1152b1ac5-light .icon-shape,#m-02e1152b1ac5-light .image-shape{background-color:#eff1f5;text-align:center;}#m-02e1152b1ac5-light .icon-shape p,#m-02e1152b1ac5-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-02e1152b1ac5-light .icon-shape .label rect,#m-02e1152b1ac5-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-02e1152b1ac5-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-02e1152b1ac5-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-02e1152b1ac5-light .node .neo-node{stroke:#bcc0cc;}#m-02e1152b1ac5-light [data-look="neo"].node rect,#m-02e1152b1ac5-light [data-look="neo"].cluster rect,#m-02e1152b1ac5-light [data-look="neo"].node polygon{stroke:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].node path{stroke:url(#m-02e1152b1ac5-light-gradient);stroke-width:1px;}#m-02e1152b1ac5-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-02e1152b1ac5-light [data-look="neo"].node circle{stroke:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-02e1152b1ac5-light [data-look="neo"].icon-shape .icon{fill:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-02e1152b1ac5-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-02e1152b1ac5-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-02e1152b1ac5-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M207.593,178L221.005,171.021C234.416,164.042,261.239,150.083,278.151,143.104C295.063,136.125,302.063,136.125,305.563,136.125L309.063,136.125" id="m-02e1152b1ac5-light-L_System_M_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_M_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoxNzh9LHsieCI6Mjg4LjA2MjUsInkiOjEzNi4xMjV9LHsieCI6MzEzLjA2MjUsInkiOjEzNi4xMjV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M207.593,253L221.005,259.979C234.416,266.958,261.239,280.917,282.315,287.896C303.391,294.875,318.719,294.875,326.383,294.875L334.047,294.875" id="m-02e1152b1ac5-light-L_System_O_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_O_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoyNTN9LHsieCI6Mjg4LjA2MjUsInkiOjI5NC44NzV9LHsieCI6MzM4LjA0Njg3NSwieSI6Mjk0Ljg3NX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M537.651,87.375L547.282,82.479C556.913,77.583,576.175,67.792,589.308,63.122C602.44,58.452,609.443,58.904,612.944,59.129L616.446,59.355" id="m-02e1152b1ac5-light-L_M_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_A_0" data-points="W3sieCI6NTM3LjY1MSwieSI6ODcuMzc1fSx7IngiOjU5NS40Mzc1LCJ5Ijo1OH0seyJ4Ijo2MjAuNDM3NSwieSI6NTkuNjEyOTAzMjI1ODA2NDV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M504.267,234.875L519.462,220.292C534.657,205.708,565.047,176.542,585.955,159.033C606.862,141.524,618.287,135.674,623.999,132.749L629.712,129.823" id="m-02e1152b1ac5-light-L_O_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_A_0" data-points="W3sieCI6NTA0LjI2Njk0OTE1MjU0MjQsInkiOjIzNC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjE0Ny4zNzV9LHsieCI6NjMzLjI3MjE0NTY2OTI5MTQsInkiOjEyOH1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M880.438,68L884.604,68C888.771,68,897.104,68,904.771,68C912.438,68,919.438,68,922.938,68L926.438,68" id="m-02e1152b1ac5-light-L_A_H_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_H_0" data-points="W3sieCI6ODgwLjQzNzUsInkiOjY4fSx7IngiOjkwNS40Mzc1LCJ5Ijo2OH0seyJ4Ijo5MzAuNDM3NSwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M525.58,184.875L537.223,191.646C548.866,198.417,572.152,211.958,588.033,218.729C603.914,225.5,612.391,225.5,616.629,225.5L620.867,225.5" id="m-02e1152b1ac5-light-L_M_D_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_D_0" data-points="W3sieCI6NTI1LjU3OTU0NTQ1NDU0NTUsInkiOjE4NC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjIyNS41fSx7IngiOjYyNC44NjcxODc1LCJ5IjoyMjUuNX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/><path d="M545.453,336.204L553.784,339.525C562.115,342.845,578.776,349.485,596.022,352.805C613.268,356.125,631.099,356.125,640.014,356.125L648.93,356.125" id="m-02e1152b1ac5-light-L_O_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_R_0" data-points="W3sieCI6NTQ1LjQ1MzEyNSwieSI6MzM2LjIwNDQyNzYxMjg1MDc0fSx7IngiOjU5NS40Mzc1LCJ5IjozNTYuMTI1fSx7IngiOjY1Mi45Mjk2ODc1LCJ5IjozNTYuMTI1fV0=" data-look="classic" marker-end="url(#m-02e1152b1ac5-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_System_M_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_System_O_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_H_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_D_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-02e1152b1ac5-light-flowchart-System-0" data-look="classic" transform="translate(135.53125, 215.5)"><rect class="basic label-container" style="" x="-127.53125" y="-37.5" width="255.0625" height="75"/><g class="label" style="" transform="translate(-97.53125, -22.5)"><rect/><foreignObject width="195.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Система<br />сервисы, БД, кэш, очереди</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-M-1" data-look="classic" transform="translate(441.75, 136.125)"><rect class="basic label-container" style="" x="-128.6875" y="-48.75" width="257.375" height="97.5"/><g class="label" style="" transform="translate(-98.6875, -33.75)"><rect/><foreignObject width="197.375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Monitoring<br />known unknowns:<br />заранее заданные метрики</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-O-3" data-look="classic" transform="translate(441.75, 294.875)"><rect class="basic label-container" style="" x="-103.703125" y="-60" width="207.40625" height="120"/><g class="label" style="" transform="translate(-73.703125, -45)"><rect/><foreignObject width="147.40625" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability<br />unknown unknowns:<br />возможность задать<br />новый вопрос</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-A-5" data-look="classic" transform="translate(750.4375, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Alerting<br />правила, которые<br />превращают сигнал в инцидент</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-H-9" data-look="classic" transform="translate(1016.0625, 68)"><rect class="basic label-container" style="" x="-85.625" y="-26.25" width="171.25" height="52.5"/><g class="label" style="" transform="translate(-55.625, -11.25)"><rect/><foreignObject width="111.25" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Человек on-call</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-D-11" data-look="classic" transform="translate(750.4375, 225.5)"><rect class="basic label-container" style="" x="-125.5703125" y="-26.25" width="251.140625" height="52.5"/><g class="label" style="" transform="translate(-95.5703125, -11.25)"><rect/><foreignObject width="191.140625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Дашборды и SLO-репорты</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-light-flowchart-R-13" data-look="classic" transform="translate(750.4375, 356.125)"><rect class="basic label-container" style="" x="-97.5078125" y="-26.25" width="195.015625" height="52.5"/><g class="label" style="" transform="translate(-67.5078125, -11.25)"><rect/><foreignObject width="135.015625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Root cause analysis</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-02e1152b1ac5-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-02e1152b1ac5-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-02e1152b1ac5-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-02e1152b1ac5-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1109.69px; background-color: transparent;" viewBox="0 0 1109.6875 390.375" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-02e1152b1ac5-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-02e1152b1ac5-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-02e1152b1ac5-dark .error-icon{fill:#181926;}#m-02e1152b1ac5-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-02e1152b1ac5-dark .edge-thickness-normal{stroke-width:1px;}#m-02e1152b1ac5-dark .edge-thickness-thick{stroke-width:3.5px;}#m-02e1152b1ac5-dark .edge-pattern-solid{stroke-dasharray:0;}#m-02e1152b1ac5-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-02e1152b1ac5-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-02e1152b1ac5-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-02e1152b1ac5-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-02e1152b1ac5-dark .marker.cross{stroke:#a5adcb;}#m-02e1152b1ac5-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-02e1152b1ac5-dark p{margin:0;}#m-02e1152b1ac5-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-02e1152b1ac5-dark .cluster-label text{fill:#cad3f5;}#m-02e1152b1ac5-dark .cluster-label span{color:#cad3f5;}#m-02e1152b1ac5-dark .cluster-label span p{background-color:transparent;}#m-02e1152b1ac5-dark .label text,#m-02e1152b1ac5-dark span{fill:#cad3f5;color:#cad3f5;}#m-02e1152b1ac5-dark .node rect,#m-02e1152b1ac5-dark .node circle,#m-02e1152b1ac5-dark .node ellipse,#m-02e1152b1ac5-dark .node polygon,#m-02e1152b1ac5-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-02e1152b1ac5-dark .rough-node .label text,#m-02e1152b1ac5-dark .node .label text,#m-02e1152b1ac5-dark .image-shape .label,#m-02e1152b1ac5-dark .icon-shape .label{text-anchor:middle;}#m-02e1152b1ac5-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-02e1152b1ac5-dark .rough-node .label,#m-02e1152b1ac5-dark .node .label,#m-02e1152b1ac5-dark .image-shape .label,#m-02e1152b1ac5-dark .icon-shape .label{text-align:center;}#m-02e1152b1ac5-dark .node.clickable{cursor:pointer;}#m-02e1152b1ac5-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-02e1152b1ac5-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-02e1152b1ac5-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-02e1152b1ac5-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-02e1152b1ac5-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-02e1152b1ac5-dark .edgeLabel p{background-color:#24273a;}#m-02e1152b1ac5-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-02e1152b1ac5-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-02e1152b1ac5-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-02e1152b1ac5-dark .cluster text{fill:#cad3f5;}#m-02e1152b1ac5-dark .cluster span{color:#cad3f5;}#m-02e1152b1ac5-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-02e1152b1ac5-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-02e1152b1ac5-dark rect.text{fill:none;stroke-width:0;}#m-02e1152b1ac5-dark .icon-shape,#m-02e1152b1ac5-dark .image-shape{background-color:#24273a;text-align:center;}#m-02e1152b1ac5-dark .icon-shape p,#m-02e1152b1ac5-dark .image-shape p{background-color:#24273a;padding:2px;}#m-02e1152b1ac5-dark .icon-shape .label rect,#m-02e1152b1ac5-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-02e1152b1ac5-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-02e1152b1ac5-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-02e1152b1ac5-dark .node .neo-node{stroke:#494d64;}#m-02e1152b1ac5-dark [data-look="neo"].node rect,#m-02e1152b1ac5-dark [data-look="neo"].cluster rect,#m-02e1152b1ac5-dark [data-look="neo"].node polygon{stroke:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].node path{stroke:url(#m-02e1152b1ac5-dark-gradient);stroke-width:1px;}#m-02e1152b1ac5-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-02e1152b1ac5-dark [data-look="neo"].node circle{stroke:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-02e1152b1ac5-dark [data-look="neo"].icon-shape .icon{fill:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-02e1152b1ac5-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-02e1152b1ac5-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-02e1152b1ac5-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M207.593,178L221.005,171.021C234.416,164.042,261.239,150.083,278.151,143.104C295.063,136.125,302.063,136.125,305.563,136.125L309.063,136.125" id="m-02e1152b1ac5-dark-L_System_M_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_M_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoxNzh9LHsieCI6Mjg4LjA2MjUsInkiOjEzNi4xMjV9LHsieCI6MzEzLjA2MjUsInkiOjEzNi4xMjV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M207.593,253L221.005,259.979C234.416,266.958,261.239,280.917,282.315,287.896C303.391,294.875,318.719,294.875,326.383,294.875L334.047,294.875" id="m-02e1152b1ac5-dark-L_System_O_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_System_O_0" data-points="W3sieCI6MjA3LjU5MzI1Nzg3NDAxNTc2LCJ5IjoyNTN9LHsieCI6Mjg4LjA2MjUsInkiOjI5NC44NzV9LHsieCI6MzM4LjA0Njg3NSwieSI6Mjk0Ljg3NX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M537.651,87.375L547.282,82.479C556.913,77.583,576.175,67.792,589.308,63.122C602.44,58.452,609.443,58.904,612.944,59.129L616.446,59.355" id="m-02e1152b1ac5-dark-L_M_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_A_0" data-points="W3sieCI6NTM3LjY1MSwieSI6ODcuMzc1fSx7IngiOjU5NS40Mzc1LCJ5Ijo1OH0seyJ4Ijo2MjAuNDM3NSwieSI6NTkuNjEyOTAzMjI1ODA2NDV9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M504.267,234.875L519.462,220.292C534.657,205.708,565.047,176.542,585.955,159.033C606.862,141.524,618.287,135.674,623.999,132.749L629.712,129.823" id="m-02e1152b1ac5-dark-L_O_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_A_0" data-points="W3sieCI6NTA0LjI2Njk0OTE1MjU0MjQsInkiOjIzNC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjE0Ny4zNzV9LHsieCI6NjMzLjI3MjE0NTY2OTI5MTQsInkiOjEyOH1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M880.438,68L884.604,68C888.771,68,897.104,68,904.771,68C912.438,68,919.438,68,922.938,68L926.438,68" id="m-02e1152b1ac5-dark-L_A_H_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_H_0" data-points="W3sieCI6ODgwLjQzNzUsInkiOjY4fSx7IngiOjkwNS40Mzc1LCJ5Ijo2OH0seyJ4Ijo5MzAuNDM3NSwieSI6Njh9XQ==" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M525.58,184.875L537.223,191.646C548.866,198.417,572.152,211.958,588.033,218.729C603.914,225.5,612.391,225.5,616.629,225.5L620.867,225.5" id="m-02e1152b1ac5-dark-L_M_D_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_M_D_0" data-points="W3sieCI6NTI1LjU3OTU0NTQ1NDU0NTUsInkiOjE4NC44NzV9LHsieCI6NTk1LjQzNzUsInkiOjIyNS41fSx7IngiOjYyNC44NjcxODc1LCJ5IjoyMjUuNX1d" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/><path d="M545.453,336.204L553.784,339.525C562.115,342.845,578.776,349.485,596.022,352.805C613.268,356.125,631.099,356.125,640.014,356.125L648.93,356.125" id="m-02e1152b1ac5-dark-L_O_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_O_R_0" data-points="W3sieCI6NTQ1LjQ1MzEyNSwieSI6MzM2LjIwNDQyNzYxMjg1MDc0fSx7IngiOjU5NS40Mzc1LCJ5IjozNTYuMTI1fSx7IngiOjY1Mi45Mjk2ODc1LCJ5IjozNTYuMTI1fV0=" data-look="classic" marker-end="url(#m-02e1152b1ac5-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_System_M_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_System_O_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_H_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_M_D_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_O_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-02e1152b1ac5-dark-flowchart-System-0" data-look="classic" transform="translate(135.53125, 215.5)"><rect class="basic label-container" style="" x="-127.53125" y="-37.5" width="255.0625" height="75"/><g class="label" style="" transform="translate(-97.53125, -22.5)"><rect/><foreignObject width="195.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Система<br />сервисы, БД, кэш, очереди</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-M-1" data-look="classic" transform="translate(441.75, 136.125)"><rect class="basic label-container" style="" x="-128.6875" y="-48.75" width="257.375" height="97.5"/><g class="label" style="" transform="translate(-98.6875, -33.75)"><rect/><foreignObject width="197.375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Monitoring<br />known unknowns:<br />заранее заданные метрики</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-O-3" data-look="classic" transform="translate(441.75, 294.875)"><rect class="basic label-container" style="" x="-103.703125" y="-60" width="207.40625" height="120"/><g class="label" style="" transform="translate(-73.703125, -45)"><rect/><foreignObject width="147.40625" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Observability<br />unknown unknowns:<br />возможность задать<br />новый вопрос</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-A-5" data-look="classic" transform="translate(750.4375, 68)"><rect class="basic label-container" style="" x="-130" y="-60" width="260" height="120"/><g class="label" style="" transform="translate(-100, -45)"><rect/><foreignObject width="200" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Alerting<br />правила, которые<br />превращают сигнал в инцидент</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-H-9" data-look="classic" transform="translate(1016.0625, 68)"><rect class="basic label-container" style="" x="-85.625" y="-26.25" width="171.25" height="52.5"/><g class="label" style="" transform="translate(-55.625, -11.25)"><rect/><foreignObject width="111.25" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Человек on-call</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-D-11" data-look="classic" transform="translate(750.4375, 225.5)"><rect class="basic label-container" style="" x="-125.5703125" y="-26.25" width="251.140625" height="52.5"/><g class="label" style="" transform="translate(-95.5703125, -11.25)"><rect/><foreignObject width="191.140625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Дашборды и SLO-репорты</p></span></div></foreignObject></g></g><g class="node default" id="m-02e1152b1ac5-dark-flowchart-R-13" data-look="classic" transform="translate(750.4375, 356.125)"><rect class="basic label-container" style="" x="-97.5078125" y="-26.25" width="195.015625" height="52.5"/><g class="label" style="" transform="translate(-67.5078125, -11.25)"><rect/><foreignObject width="135.015625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Root cause analysis</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-02e1152b1ac5-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-02e1152b1ac5-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-02e1152b1ac5-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p>В этой триаде SLI — это специально выделенные метрики из мониторинга, которые отражают пользовательский опыт. SLO — правило поверх SLI, которое мы хотим выполнять. Алерты по burn-rate — правила поверх SLO. Observability — страховка на случай, когда SLO сгорает, и нужно понять, <strong>почему именно сейчас и в этом запросе</strong>. Иначе говоря: мониторинг, SLI и SLO живут в плоскости «всё ли в порядке по тем критериям, которые мы назначили». Observability — в плоскости «что именно сейчас не так и почему», когда привычные метрики не дают ответа.</p>
<p>Теперь, с этим словарём — переходим к самому SLI.</p>
<h3 id="что-такое-sli">Что такое SLI</h3>
<p><strong>SLI (Service Level Indicator)</strong> — измеримая характеристика, которая отражает качество одного аспекта обслуживания пользователя. Ключевое слово — <strong>отражает</strong>. SLI меряет не «что система делает», а «насколько хорошо это видит пользователь».</p>
<p>Каноническая формула SLI — доля «хороших» событий от всех релевантных:</p>
<div class="math-block" data-png="https://jtprog.ru/math/a74101bc04f99ea2ff464350151f8a30174f6ef8290318bef84215f4765ab7cf.png"><span class="math__tex">$$ \text{SLI} = \dfrac{\text{количество хороших событий}}{\text{количество всех релевантных событий}} \times 100\% $$</span></div><p>Что такое «хорошее событие» и какие события «релевантны» — определяется <strong>явно</strong>, иначе SLI получится бесполезным. Самая частая ошибка здесь — посчитать «все HTTP 2xx» как «хорошие». Так делать нельзя по двум причинам:</p>
<ol>
<li><strong>Не все 2xx одинаковы.</strong> Запрос, ответивший <code>200 OK</code> за 5 секунд, формально успешен, но для пользователя это провал. SLI должен учитывать порог по латентности, иначе он маскирует деградацию.</li>
<li><strong>Не все запросы одинаковы.</strong> Health-чек с другого сервиса тоже летит как HTTP-запрос, но на пользовательский опыт не влияет. Если он попал в знаменатель — SLI размылся.</li>
</ol>
<h3 id="формат-хорошего-sli">Формат хорошего SLI</h3>
<p>У хорошего SLI всегда три явные части:</p>
<ul>
<li><strong>Числитель.</strong> Что считаем «хорошим» — порог по времени, корректности, объёму. Должно быть ясно, что туда входит, а что нет.</li>
<li><strong>Знаменатель.</strong> Какие события считаем релевантными — фильтр по эндпоинту, источнику трафика, типу запроса. Должно быть ясно, какие исключаем.</li>
<li><strong>Окно агрегации.</strong> За какой период считаем — 5 минут для алертов, 30 дней для отчётности. Без окна SLI — не число, а описание формы.</li>
</ul>
<h3 id="конкретные-sli-для-нашего-кейса">Конкретные SLI для нашего кейса</h3>
<p>Возьмём ключевые шаги из таблицы НФТ и сформулируем для них SLI. Останавливаюсь на четырёх — этого достаточно, чтобы показать механику; в реальном проде их обычно 4–8 на сервис.</p>
<p><strong>SLI-1 — Latency-SLI на шаге 4 «получение выдачи».</strong></p>
<blockquote><p>Доля поисковых запросов, на которые система ответила за время <code>&lt; 500 ms</code>, от всех релевантных поисковых запросов с пользовательского трафика, за окно 30 дней.</p>
</blockquote><ul>
<li><strong>Числитель:</strong> запросы с <code>status in {200, 204}</code> AND <code>request_duration &lt; 500 ms</code>.</li>
<li><strong>Знаменатель:</strong> запросы с <code>status in {200, 204, 5xx}</code> плюс таймауты, исключая 4xx-валидационные ответы (всё, что не health-check, не synthetic-проба, не вызов внутреннего сервиса).</li>
<li><strong>Окно:</strong> 30 дней rolling.</li>
</ul>
<p>Заметь две вещи. Во-первых, <strong>5xx в знаменателе</strong>: не учитывать их — значит нарисовать оптимистичную картинку, в которой провал «технически не считается». Во-вторых, <strong>4xx-ответы пользовательского трафика в знаменатель не идут</strong> — по канонической Google SRE-практике (глава «Service Level Objectives» в SRE Workbook, O&rsquo;Reilly, 2018) это корректное отвержение невалидного запроса (например, поисковая строка пустая или превышает лимит длины), система отработала ожидаемо. Включать их в знаменатель — раздуть его и сделать SLI оптимистичнее, чем он есть на самом деле.</p>
<p><strong>SLI-2 — Availability-SLI на шаге 4.</strong></p>
<blockquote><p>Доля поисковых запросов, завершившихся успешно (не 5xx, не таймаут), от всех релевантных запросов, за окно 30 дней.</p>
</blockquote><p>Это «классическая» availability. Но смотрим только на пользовательский трафик в шаг 4, не «по системе целиком». Знаменатель тот же, что у SLI-1, — 4xx-валидационные ответы исключены.</p>
<p><strong>SLI-3 — Correctness-SLI на шаге 4.</strong></p>
<blockquote><p>Доля поисковых запросов, вернувших непустую и валидную выдачу, от всех запросов, для которых должна была быть хотя бы одна позиция (не 0-result-by-design).</p>
</blockquote><p>Этот SLI сложнее, потому что нужно знать «должна была быть выдача или нет». Меряется офлайн-сэмплингом: берём X% запросов, прогоняем эталонный поиск, сравниваем долю «должно быть, но не было».</p>
<p><strong>SLI-4 — Freshness-SLI на шаге 6 «клик в карточку».</strong></p>
<blockquote><p>Доля переходов в карточку, на которых цена и наличие отображены не старше 60 секунд от истинного состояния товара.</p>
</blockquote><p>Этот SLI напрямую защищает от ситуации «увидел одну цену в выдаче — на чекауте другая». Без него ты рискуешь не техническим инцидентом, а <strong>репутационным</strong>: пользователь зол, возвраты растут, рейтинг продавцов падает.</p>
<p>Аналогичные SLI строятся для autocomplete, фильтра, отправки запроса по той же механике — повторять не буду. Достаточно принципа: один шаг — один или несколько SLI, каждый с явным числителем, знаменателем и окном.</p>
<h3 id="что-мы-не-берём-как-sli">Что мы НЕ берём как SLI</h3>
<p>Контрпримеры, чтобы калибровать чутьё:</p>
<ul>
<li><strong>CPU/Memory utilization.</strong> Это <strong>служебный сигнал</strong>, а не SLI. Пользователь не страдает от того, что у тебя CPU 80% — он страдает только если это превратилось в latency или 5xx. Утилизация — причина, SLI — следствие.</li>
<li><strong>Количество подов в реплике.</strong> Тоже служебный. Метрика для capacity planning, не для SLO.</li>
<li><strong>Среднее время ответа.</strong> Опасный «псевдо-SLI». Скрывает хвосты — а хвосты это и есть пользовательская боль. Только перцентили: P95 как минимум, P99 для критичных шагов.</li>
</ul>
<p>Дальше — превращаем SLI в обещание самим себе. То есть в SLO.</p>
<h2 id="шаг-4-slo-как-контракт-и-error-budget-математически">Шаг 4: SLO как контракт и error budget математически</h2>
<p><strong>SLO (Service Level Objective)</strong> — целевое значение SLI на оговорённом окне. Если SLI отвечает на вопрос «как было», то SLO — «как мы обещаем, что должно быть».</p>
<p>Формула:</p>
<div class="math-block" data-png="https://jtprog.ru/math/7b1d270e8247f093cbfecceb9778ab8cbde0cc290ff4846a300013fc0637c0c3.png"><span class="math__tex">$$ \text{SLO}: \text{SLI} \geq T \text{ (на окне } W) $$</span></div><p>Где <code>T</code> — таргет (например, <code>99.9</code>), <code>W</code> — окно агрегации (например, 30 дней rolling).</p>
<h3 id="slo-для-нашего-sli-1">SLO для нашего SLI-1</h3>
<p>Берём SLI-1 (latency-SLI поиска: доля запросов <code>&lt; 500 ms</code>):</p>
<blockquote><p>SLO: 99.9% поисковых запросов отвечают быстрее <code>500 ms</code> на rolling-окне 30 дней.</p>
</blockquote><p>Это полноценный контракт. И он автоматически рождает <strong>error budget</strong>.</p>
<h3 id="error-budget-математически">Error budget математически</h3>
<p>Error budget — это <strong>разрешённая доля «плохих» событий</strong> за период:</p>
<div class="math-block" data-png="https://jtprog.ru/math/ab13e9de75fcb2d92b3ef12a4747772aac7b91387d8ae6db1efca6eef589240f.png"><span class="math__tex">$$ \text{Error Budget} = 1 - \text{SLO} $$</span></div><p>Для SLO <code>99.9%</code> это <code>0.1%</code>, или 1 «плохой» запрос из 1000.</p>
<p>Дальше переводим в абсолютные значения. Берём типовой день нашего маркетплейса:</p>
<ul>
<li>Пиковый RPS: <code>30 000 запросов/сек</code>.</li>
<li>Средний по дню (с учётом ночи): <code>~10 000 запросов/сек</code>.</li>
<li>Запросов в сутки: <code>10 000 × 86 400 ≈ 864 000 000</code>.</li>
<li>Запросов за 30-дневное окно: <code>~26 миллиардов</code>.</li>
<li>Допустимая доля «плохих» при SLO <code>99.9%</code>: <code>0.1%</code>.</li>
<li><strong>Бюджет ошибок за 30 дней: ~26 миллионов запросов.</strong></li>
</ul>
<p>Если перевести в эквивалент простоя — двумя срезами, на пике и при средней нагрузке за сутки:</p>
<table>
	<thead>
			<tr>
					<th>SLO</th>
					<th>Бюджет ошибок</th>
					<th>На пике (30k RPS)</th>
					<th>При средней нагрузке (~10k RPS)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>99.0%</td>
					<td>~260M запросов</td>
					<td>~2 ч 24 мин</td>
					<td>~7 ч 13 мин</td>
			</tr>
			<tr>
					<td>99.9%</td>
					<td>~26M запросов</td>
					<td>~14 мин 27 с</td>
					<td>~43 мин 20 с</td>
			</tr>
			<tr>
					<td>99.99%</td>
					<td>~2.6M запросов</td>
					<td>~1 мин 27 с</td>
					<td>~4 мин 20 с</td>
			</tr>
			<tr>
					<td>99.999%</td>
					<td>~260K запросов</td>
					<td>~8.7 с</td>
					<td>~26 с</td>
			</tr>
	</tbody>
</table>
<p>Две колонки — потому что бюджет ошибок «горит» неравномерно. На пике RPS ты расходуешь запросы в три раза быстрее, чем в среднем по суткам, — поэтому инцидент в Чёрную пятницу обходится сильно дороже, чем тот же инцидент во вторник в 4 утра. Колонка «На пике» — про худший случай, на который надо проектировать SLO; колонка «При средней нагрузке» — про календарный «эквивалент полного простоя», который чаще видят в отчётах.</p>
<p>Это не просто разные числа — это разные <strong>физические возможности</strong>. На пике 8.7 секунд в месяц означают, что человек не успеет даже заметить пейдж: реакция должна быть автоматической. 7 часов в месяц при средней нагрузке — означают, что можно позволить себе ручной разбор каждого инцидента.</p>
<h3 id="что-error-budget-значит-на-практике">Что error budget значит на практике</h3>
<p>Бюджет — не «допустимый ущерб», а <strong>бюджет на риск</strong>. На него тратятся:</p>
<ul>
<li><strong>Деплои.</strong> Каждый релиз — ставка из бюджета. Откат + время на восстановление = израсходованные ошибки.</li>
<li><strong>Эксперименты.</strong> A/B-тесты, постепенный rollout, фиче-флаги — потенциальные источники регрессии.</li>
<li><strong>Учения.</strong> Game days, chaos engineering, плановые отказы — расходуют бюджет, но взамен дают уверенность в стабильности на реальных инцидентах.</li>
</ul>
<p>Если бюджет полностью сгорел до конца окна — это сигнал к <strong>freeze: остановить рискованные деплои и вкладываться в reliability</strong>, пока окно не сдвинется. Если бюджет жирный — это <strong>разрешение рисковать</strong>: ускорять выкатки, экспериментировать, рефакторить.</p>
<p>К теме «бюджет как валюта в спорах» вернусь в разделе про error budget как арбитр. Сейчас важно зафиксировать одно: <strong>SLO без error budget — это пожелание, а не контракт.</strong> Бюджет — то, что превращает SLO из табло в инструмент принятия решений.</p>
<p>Дальше — главный технический разбор статьи. Берём один и тот же поиск маркетплейса и прокручиваем сквозь три уровня SLO: 99.0%, 99.9%, 99.99%. Покажу, что меняется в архитектуре, что — в стоимости, что — в эксплуатационной нагрузке.</p>
<h2 id="один-кейс--три-slo--три-архитектуры">Один кейс — три SLO — три архитектуры</h2>
<p>Дальше беру тот же поиск маркетплейса с теми же параметрами и проектирую <strong>три разные системы</strong>. Меняется структура read path, выбор технологий, capacity, RTO/RPO, эксплуатационная модель. Остаётся постановка задачи, объём каталога (50M), пиковый RPS (30k), 5 регионов, 70% mobile.</p>
<p>Структура каждого подраздела одинаковая: бюджет → схема read path → числа (capacity, RTO/RPO, кэш, индекс) → что добавилось по сравнению с предыдущим уровнем и зачем → чего намеренно НЕ делаем.</p>
<h3 id="slo-990-дешёвая-надёжность-для-не-критичных-сценариев">SLO 99.0%: «дешёвая надёжность» для не-критичных сценариев</h3>
<p><strong>Бюджет:</strong> ~260M «плохих» запросов за 30 дней, или <strong>~2 ч 24 мин</strong> полного даунтайма на пике / <strong>~7 ч 13 мин</strong> при средней нагрузке. Почти полный рабочий день на инциденты в месяц — много, но выживаемо при ручной операционной модели.</p>
<p><strong>Когда уместно:</strong> внутренние инструменты, ранний MVP стартапа, неосновные feature-paths в большом продукте (например, search в каталоге сторонних услуг, не кормящий основную конверсию).</p>
<p><strong>Read path:</strong></p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/eda2ce005e57ab15f25c9baafc544fd9eb31156ffefe8f0a9a71a28ac37b44f3.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-eda2ce005e57-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1028.84px; background-color: transparent;" viewBox="0 0 1028.84375 138.9862823486328" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-eda2ce005e57-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-eda2ce005e57-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-light .error-icon{fill:#dce0e8;}#m-eda2ce005e57-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-eda2ce005e57-light .edge-thickness-normal{stroke-width:1px;}#m-eda2ce005e57-light .edge-thickness-thick{stroke-width:3.5px;}#m-eda2ce005e57-light .edge-pattern-solid{stroke-dasharray:0;}#m-eda2ce005e57-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-eda2ce005e57-light .edge-pattern-dashed{stroke-dasharray:3;}#m-eda2ce005e57-light .edge-pattern-dotted{stroke-dasharray:2;}#m-eda2ce005e57-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-eda2ce005e57-light .marker.cross{stroke:#5c5f77;}#m-eda2ce005e57-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-eda2ce005e57-light p{margin:0;}#m-eda2ce005e57-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-eda2ce005e57-light .cluster-label text{fill:#4c4f69;}#m-eda2ce005e57-light .cluster-label span{color:#4c4f69;}#m-eda2ce005e57-light .cluster-label span p{background-color:transparent;}#m-eda2ce005e57-light .label text,#m-eda2ce005e57-light span{fill:#4c4f69;color:#4c4f69;}#m-eda2ce005e57-light .node rect,#m-eda2ce005e57-light .node circle,#m-eda2ce005e57-light .node ellipse,#m-eda2ce005e57-light .node polygon,#m-eda2ce005e57-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-eda2ce005e57-light .rough-node .label text,#m-eda2ce005e57-light .node .label text,#m-eda2ce005e57-light .image-shape .label,#m-eda2ce005e57-light .icon-shape .label{text-anchor:middle;}#m-eda2ce005e57-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-eda2ce005e57-light .rough-node .label,#m-eda2ce005e57-light .node .label,#m-eda2ce005e57-light .image-shape .label,#m-eda2ce005e57-light .icon-shape .label{text-align:center;}#m-eda2ce005e57-light .node.clickable{cursor:pointer;}#m-eda2ce005e57-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-eda2ce005e57-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-eda2ce005e57-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-eda2ce005e57-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-eda2ce005e57-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-eda2ce005e57-light .edgeLabel p{background-color:#eff1f5;}#m-eda2ce005e57-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-eda2ce005e57-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-eda2ce005e57-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-eda2ce005e57-light .cluster text{fill:#4c4f69;}#m-eda2ce005e57-light .cluster span{color:#4c4f69;}#m-eda2ce005e57-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-eda2ce005e57-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-eda2ce005e57-light rect.text{fill:none;stroke-width:0;}#m-eda2ce005e57-light .icon-shape,#m-eda2ce005e57-light .image-shape{background-color:#eff1f5;text-align:center;}#m-eda2ce005e57-light .icon-shape p,#m-eda2ce005e57-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-eda2ce005e57-light .icon-shape .label rect,#m-eda2ce005e57-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-eda2ce005e57-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-eda2ce005e57-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-eda2ce005e57-light .node .neo-node{stroke:#bcc0cc;}#m-eda2ce005e57-light [data-look="neo"].node rect,#m-eda2ce005e57-light [data-look="neo"].cluster rect,#m-eda2ce005e57-light [data-look="neo"].node polygon{stroke:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].node path{stroke:url(#m-eda2ce005e57-light-gradient);stroke-width:1px;}#m-eda2ce005e57-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-eda2ce005e57-light [data-look="neo"].node circle{stroke:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-eda2ce005e57-light [data-look="neo"].icon-shape .icon{fill:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-eda2ce005e57-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-eda2ce005e57-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-eda2ce005e57-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M163.348,55.5L168.157,53.833C172.966,52.167,182.585,48.833,190.894,47.167C199.203,45.5,206.203,45.5,209.703,45.5L213.203,45.5" id="m-eda2ce005e57-light-L_U_LB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_LB_0" data-points="W3sieCI6MTYzLjM0NzUyMTU1MTcyNDEzLCJ5Ijo1NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjQ1LjV9LHsieCI6MjE3LjIwMzEyNSwieSI6NDUuNX1d" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M374.516,45.5L378.682,45.5C382.849,45.5,391.182,45.5,398.884,46.69C406.585,47.879,413.655,50.259,417.19,51.448L420.725,52.638" id="m-eda2ce005e57-light-L_LB_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_LB_A_0" data-points="W3sieCI6Mzc0LjUxNTYyNSwieSI6NDUuNX0seyJ4IjozOTkuNTE1NjI1LCJ5Ijo0NS41fSx7IngiOjQyNC41MTU2MjUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M589.938,53.914L594.104,52.511C598.271,51.109,606.604,48.305,614.271,46.902C621.938,45.5,628.938,45.5,632.438,45.5L635.938,45.5" id="m-eda2ce005e57-light-L_A_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_C_0" data-points="W3sieCI6NTg5LjkzNzUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fSx7IngiOjYxNC45Mzc1LCJ5Ijo0NS41fSx7IngiOjYzOS45Mzc1LCJ5Ijo0NS41fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M807.797,45.5L814.579,45.5C821.362,45.5,834.927,45.5,847.861,47.593C860.795,49.687,873.098,53.873,879.249,55.967L885.401,58.06" id="m-eda2ce005e57-light-L_C_P_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_C_P_0" data-points="W3sieCI6ODA3Ljc5Njg3NSwieSI6NDUuNX0seyJ4Ijo4NDguNDkyMTg3NSwieSI6NDUuNX0seyJ4Ijo4ODkuMTg3NSwieSI6NTkuMzQ4NjQzMTk3NjUzMTA0fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M889.188,104.151L882.405,106.459C875.622,108.768,862.057,113.384,834.504,115.692C806.951,118,765.409,118,726.483,118C687.557,118,651.247,118,629.558,116.81C607.868,115.621,600.798,113.241,597.263,112.052L593.729,110.862" id="m-eda2ce005e57-light-L_P_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_P_A_0" data-points="W3sieCI6ODg5LjE4NzUsInkiOjEwNC4xNTEzNTY4MDIzNDY5fSx7IngiOjg0OC40OTIxODc1LCJ5IjoxMTh9LHsieCI6NzIzLjg2NzE4NzUsInkiOjExOH0seyJ4Ijo2MTQuOTM3NSwieSI6MTE4fSx7IngiOjU4OS45Mzc1LCJ5IjoxMDkuNTg2Mjc2OTI3NTQwNDR9XQ==" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/><path d="M424.516,109.586L420.349,110.989C416.182,112.391,407.849,115.195,386.406,116.598C364.964,118,330.411,118,295.859,118C261.307,118,226.755,118,205.3,116.552C183.844,115.103,175.486,112.207,171.306,110.758L167.127,109.31" id="m-eda2ce005e57-light-L_A_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_U_0" data-points="W3sieCI6NDI0LjUxNTYyNSwieSI6MTA5LjU4NjI3NjkyNzU0MDQ0fSx7IngiOjM5OS41MTU2MjUsInkiOjExOH0seyJ4IjoyOTUuODU5Mzc1LCJ5IjoxMTh9LHsieCI6MTkyLjIwMzEyNSwieSI6MTE4fSx7IngiOjE2My4zNDc1MjE1NTE3MjQxMywieSI6MTA4fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_LB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_LB_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_C_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(848.4921875, 45.5)"><g class="label" data-id="L_C_P_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_P_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-eda2ce005e57-light-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 81.75)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-LB-1" data-look="classic" transform="translate(295.859375, 45.5)"><rect class="basic label-container" style="" x="-78.65625" y="-37.5" width="157.3125" height="75"/><g class="label" style="" transform="translate(-48.65625, -22.5)"><rect/><foreignObject width="97.3125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Load Balancer<br />1 регион</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-A-3" data-look="classic" transform="translate(507.2265625, 81.75)"><rect class="basic label-container" style="" x="-82.7109375" y="-37.5" width="165.421875" height="75"/><g class="label" style="" transform="translate(-52.7109375, -22.5)"><rect/><foreignObject width="105.421875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>App-сервер<br />3–5 инстансов</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-C-5" data-look="classic" transform="translate(723.8671875, 45.5)"><rect class="basic label-container" style="" x="-83.9296875" y="-37.5" width="167.859375" height="75"/><g class="label" style="" transform="translate(-53.9296875, -22.5)"><rect/><foreignObject width="107.859375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>In-memory LRU<br />per-instance</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-light-flowchart-P-7" data-look="classic" transform="translate(955.015625, 81.75)"><path d="M0,12.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 a65.828125,12.8241811761841 0,0,0 -131.65625,0 l0,72.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 l0,-72.8241811761841" class="basic label-container outer-path" style="" transform="translate(-65.828125, -49.23627176427615)"/><g class="label" style="" transform="translate(-58.328125, -12.5)"><rect/><foreignObject width="116.65625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres primary<br />+ 1 read replica</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-eda2ce005e57-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-eda2ce005e57-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-eda2ce005e57-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-eda2ce005e57-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1028.84px; background-color: transparent;" viewBox="0 0 1028.84375 138.9862823486328" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-eda2ce005e57-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-eda2ce005e57-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-eda2ce005e57-dark .error-icon{fill:#181926;}#m-eda2ce005e57-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-eda2ce005e57-dark .edge-thickness-normal{stroke-width:1px;}#m-eda2ce005e57-dark .edge-thickness-thick{stroke-width:3.5px;}#m-eda2ce005e57-dark .edge-pattern-solid{stroke-dasharray:0;}#m-eda2ce005e57-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-eda2ce005e57-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-eda2ce005e57-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-eda2ce005e57-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-eda2ce005e57-dark .marker.cross{stroke:#a5adcb;}#m-eda2ce005e57-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-eda2ce005e57-dark p{margin:0;}#m-eda2ce005e57-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-eda2ce005e57-dark .cluster-label text{fill:#cad3f5;}#m-eda2ce005e57-dark .cluster-label span{color:#cad3f5;}#m-eda2ce005e57-dark .cluster-label span p{background-color:transparent;}#m-eda2ce005e57-dark .label text,#m-eda2ce005e57-dark span{fill:#cad3f5;color:#cad3f5;}#m-eda2ce005e57-dark .node rect,#m-eda2ce005e57-dark .node circle,#m-eda2ce005e57-dark .node ellipse,#m-eda2ce005e57-dark .node polygon,#m-eda2ce005e57-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-eda2ce005e57-dark .rough-node .label text,#m-eda2ce005e57-dark .node .label text,#m-eda2ce005e57-dark .image-shape .label,#m-eda2ce005e57-dark .icon-shape .label{text-anchor:middle;}#m-eda2ce005e57-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-eda2ce005e57-dark .rough-node .label,#m-eda2ce005e57-dark .node .label,#m-eda2ce005e57-dark .image-shape .label,#m-eda2ce005e57-dark .icon-shape .label{text-align:center;}#m-eda2ce005e57-dark .node.clickable{cursor:pointer;}#m-eda2ce005e57-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-eda2ce005e57-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-eda2ce005e57-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-eda2ce005e57-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-eda2ce005e57-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-eda2ce005e57-dark .edgeLabel p{background-color:#24273a;}#m-eda2ce005e57-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-eda2ce005e57-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-eda2ce005e57-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-eda2ce005e57-dark .cluster text{fill:#cad3f5;}#m-eda2ce005e57-dark .cluster span{color:#cad3f5;}#m-eda2ce005e57-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-eda2ce005e57-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-eda2ce005e57-dark rect.text{fill:none;stroke-width:0;}#m-eda2ce005e57-dark .icon-shape,#m-eda2ce005e57-dark .image-shape{background-color:#24273a;text-align:center;}#m-eda2ce005e57-dark .icon-shape p,#m-eda2ce005e57-dark .image-shape p{background-color:#24273a;padding:2px;}#m-eda2ce005e57-dark .icon-shape .label rect,#m-eda2ce005e57-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-eda2ce005e57-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-eda2ce005e57-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-eda2ce005e57-dark .node .neo-node{stroke:#494d64;}#m-eda2ce005e57-dark [data-look="neo"].node rect,#m-eda2ce005e57-dark [data-look="neo"].cluster rect,#m-eda2ce005e57-dark [data-look="neo"].node polygon{stroke:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].node path{stroke:url(#m-eda2ce005e57-dark-gradient);stroke-width:1px;}#m-eda2ce005e57-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-eda2ce005e57-dark [data-look="neo"].node circle{stroke:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-eda2ce005e57-dark [data-look="neo"].icon-shape .icon{fill:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-eda2ce005e57-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-eda2ce005e57-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-eda2ce005e57-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-eda2ce005e57-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M163.348,55.5L168.157,53.833C172.966,52.167,182.585,48.833,190.894,47.167C199.203,45.5,206.203,45.5,209.703,45.5L213.203,45.5" id="m-eda2ce005e57-dark-L_U_LB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_LB_0" data-points="W3sieCI6MTYzLjM0NzUyMTU1MTcyNDEzLCJ5Ijo1NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjQ1LjV9LHsieCI6MjE3LjIwMzEyNSwieSI6NDUuNX1d" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M374.516,45.5L378.682,45.5C382.849,45.5,391.182,45.5,398.884,46.69C406.585,47.879,413.655,50.259,417.19,51.448L420.725,52.638" id="m-eda2ce005e57-dark-L_LB_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_LB_A_0" data-points="W3sieCI6Mzc0LjUxNTYyNSwieSI6NDUuNX0seyJ4IjozOTkuNTE1NjI1LCJ5Ijo0NS41fSx7IngiOjQyNC41MTU2MjUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M589.938,53.914L594.104,52.511C598.271,51.109,606.604,48.305,614.271,46.902C621.938,45.5,628.938,45.5,632.438,45.5L635.938,45.5" id="m-eda2ce005e57-dark-L_A_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_C_0" data-points="W3sieCI6NTg5LjkzNzUsInkiOjUzLjkxMzcyMzA3MjQ1OTU2fSx7IngiOjYxNC45Mzc1LCJ5Ijo0NS41fSx7IngiOjYzOS45Mzc1LCJ5Ijo0NS41fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M807.797,45.5L814.579,45.5C821.362,45.5,834.927,45.5,847.861,47.593C860.795,49.687,873.098,53.873,879.249,55.967L885.401,58.06" id="m-eda2ce005e57-dark-L_C_P_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_C_P_0" data-points="W3sieCI6ODA3Ljc5Njg3NSwieSI6NDUuNX0seyJ4Ijo4NDguNDkyMTg3NSwieSI6NDUuNX0seyJ4Ijo4ODkuMTg3NSwieSI6NTkuMzQ4NjQzMTk3NjUzMTA0fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M889.188,104.151L882.405,106.459C875.622,108.768,862.057,113.384,834.504,115.692C806.951,118,765.409,118,726.483,118C687.557,118,651.247,118,629.558,116.81C607.868,115.621,600.798,113.241,597.263,112.052L593.729,110.862" id="m-eda2ce005e57-dark-L_P_A_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_P_A_0" data-points="W3sieCI6ODg5LjE4NzUsInkiOjEwNC4xNTEzNTY4MDIzNDY5fSx7IngiOjg0OC40OTIxODc1LCJ5IjoxMTh9LHsieCI6NzIzLjg2NzE4NzUsInkiOjExOH0seyJ4Ijo2MTQuOTM3NSwieSI6MTE4fSx7IngiOjU4OS45Mzc1LCJ5IjoxMDkuNTg2Mjc2OTI3NTQwNDR9XQ==" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/><path d="M424.516,109.586L420.349,110.989C416.182,112.391,407.849,115.195,386.406,116.598C364.964,118,330.411,118,295.859,118C261.307,118,226.755,118,205.3,116.552C183.844,115.103,175.486,112.207,171.306,110.758L167.127,109.31" id="m-eda2ce005e57-dark-L_A_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_U_0" data-points="W3sieCI6NDI0LjUxNTYyNSwieSI6MTA5LjU4NjI3NjkyNzU0MDQ0fSx7IngiOjM5OS41MTU2MjUsInkiOjExOH0seyJ4IjoyOTUuODU5Mzc1LCJ5IjoxMTh9LHsieCI6MTkyLjIwMzEyNSwieSI6MTE4fSx7IngiOjE2My4zNDc1MjE1NTE3MjQxMywieSI6MTA4fV0=" data-look="classic" marker-end="url(#m-eda2ce005e57-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_LB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_LB_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_C_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(848.4921875, 45.5)"><g class="label" data-id="L_C_P_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_P_A_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_A_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-eda2ce005e57-dark-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 81.75)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-LB-1" data-look="classic" transform="translate(295.859375, 45.5)"><rect class="basic label-container" style="" x="-78.65625" y="-37.5" width="157.3125" height="75"/><g class="label" style="" transform="translate(-48.65625, -22.5)"><rect/><foreignObject width="97.3125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Load Balancer<br />1 регион</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-A-3" data-look="classic" transform="translate(507.2265625, 81.75)"><rect class="basic label-container" style="" x="-82.7109375" y="-37.5" width="165.421875" height="75"/><g class="label" style="" transform="translate(-52.7109375, -22.5)"><rect/><foreignObject width="105.421875" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>App-сервер<br />3–5 инстансов</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-C-5" data-look="classic" transform="translate(723.8671875, 45.5)"><rect class="basic label-container" style="" x="-83.9296875" y="-37.5" width="167.859375" height="75"/><g class="label" style="" transform="translate(-53.9296875, -22.5)"><rect/><foreignObject width="107.859375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>In-memory LRU<br />per-instance</p></span></div></foreignObject></g></g><g class="node default" id="m-eda2ce005e57-dark-flowchart-P-7" data-look="classic" transform="translate(955.015625, 81.75)"><path d="M0,12.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 a65.828125,12.8241811761841 0,0,0 -131.65625,0 l0,72.8241811761841 a65.828125,12.8241811761841 0,0,0 131.65625,0 l0,-72.8241811761841" class="basic label-container outer-path" style="" transform="translate(-65.828125, -49.23627176427615)"/><g class="label" style="" transform="translate(-58.328125, -12.5)"><rect/><foreignObject width="116.65625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres primary<br />+ 1 read replica</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-eda2ce005e57-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-eda2ce005e57-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-eda2ce005e57-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p><strong>Числовые прикидки:</strong></p>
<ul>
<li><strong>Capacity.</strong> 30k RPS пика → при <code>99.0%</code> можно потерять ~300 запросов/сек без нарушения SLO. Один хорошо настроенный Postgres primary с trigram-индексом на <code>title/description</code> тянет 1–3k RPS read на сложных запросах с фильтрами и фасетами, до 5–8k RPS — на простых коротких запросах с маленькой выборкой (P99 &lt; 200 ms, при правильном connection pooling). Read replica удваивает чтение, итого <code>~2–15k RPS</code> в зависимости от профиля. На пике 30k ты либо урежешь long-tail запросы кэшем приложения, либо примешь деградацию — бюджет позволяет.</li>
<li><strong>Кэш.</strong> In-memory LRU на каждом app-инстансе, размер <code>~256 MB</code>. Hit rate 30–40% на популярных запросах. Без Redis — отдельный сервис добавил бы операционную сложность, нерелевантную для этого SLO.</li>
<li><strong>Полнотекстовый индекс.</strong> <code>pg_trgm</code> + <code>tsvector</code> поверх 50M товаров занимают <code>3–5×</code> от размера индексируемых текстовых полей (title, description, attributes-text). Для нашего каталога с расширенными текстовыми атрибутами это порядка <code>1.5–2.5 TB</code> индексов. Реалистично на одной мощной машине (NVMe, 256 GB RAM).</li>
<li><strong>RTO.</strong> До 30–60 минут на ручной failover на реплику. Автоматизации нет — стоит отдельный сервис.</li>
<li><strong>RPO.</strong> До 5 минут (типовая асинхронная репликация Postgres).</li>
<li><strong>Алерты.</strong> На симптомы: 5xx-rate, P99 latency, replication lag. Реакция оператором, не автоматическая remediation.</li>
</ul>
<p><strong>Почему этого достаточно — счёт по бюджету.</strong> Бюджет 99.0% — это <code>~7 часов даунтайма в месяц</code> при средней нагрузке. Типовой худший случай выхода из строя primary с восстановлением из <code>pg_basebackup</code> + PITR — <code>30–90 минут</code> от пейджа до возобновления read path (включая ручное переключение приложений на реплику и прогрев кэша). Даже два таких инцидента в месяц съедают <code>~3 часа</code> — это меньше половины бюджета. Платить за синхронную репликацию, автоматический failover-tooling (Patroni / RepMgr / Stolon) и дежурную команду, способную их обслуживать, на этом уровне SLO нечем: бюджет покрывает ручной MTTR с большим запасом. На 99.9% этого запаса уже нет — отсюда и Active-passive failover на следующем уровне.</p>
<p><strong>Что намеренно НЕ делаем:</strong></p>
<ul>
<li>Не поднимаем Elasticsearch — он добавит +1 систему-источник истины, требующую отдельного индексирования, мониторинга и SLO. На 99.0% это лишний оверхед.</li>
<li>Не делаем шардинг — одна машина с 256 GB RAM в 2025 году утаскивает 50M документов с trigram-индексом без проблем.</li>
<li>Не делаем multi-region даже active-passive. Бюджет 7 часов покрывает любой региональный инцидент с ручным восстановлением.</li>
</ul>
<p><strong>Стоимость:</strong> обозначу как <strong>1× baseline</strong> для последующего сравнения.</p>
<h3 id="slo-999-рабочий-production-уровень">SLO 99.9%: рабочий production-уровень</h3>
<p><strong>Бюджет:</strong> ~26M «плохих» запросов за 30 дней, или <strong>~14 мин 27 с</strong> полного даунтайма на пике / <strong>~43 мин 20 с</strong> при средней нагрузке. На инцидент при средней нагрузке остаётся минут 20–30 — хватает на автоматический failover, но не на ручной разбор каждого 5xx.</p>
<p><strong>Когда уместно:</strong> основные пользовательские пути в среднем e-commerce, обычный production. Большинство сервисов в индустрии живут в этом диапазоне.</p>
<p><strong>Read path:</strong></p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/58b7ff999bbfb53bebce4d975ca50b9ee1edcc36aaecf424e8481a568361bcd3.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-58b7ff999bbf-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1236.55px; background-color: transparent;" viewBox="0 0 1236.546875 312.40679931640625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-58b7ff999bbf-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-58b7ff999bbf-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-light .error-icon{fill:#dce0e8;}#m-58b7ff999bbf-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-58b7ff999bbf-light .edge-thickness-normal{stroke-width:1px;}#m-58b7ff999bbf-light .edge-thickness-thick{stroke-width:3.5px;}#m-58b7ff999bbf-light .edge-pattern-solid{stroke-dasharray:0;}#m-58b7ff999bbf-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-58b7ff999bbf-light .edge-pattern-dashed{stroke-dasharray:3;}#m-58b7ff999bbf-light .edge-pattern-dotted{stroke-dasharray:2;}#m-58b7ff999bbf-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-58b7ff999bbf-light .marker.cross{stroke:#5c5f77;}#m-58b7ff999bbf-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-58b7ff999bbf-light p{margin:0;}#m-58b7ff999bbf-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-58b7ff999bbf-light .cluster-label text{fill:#4c4f69;}#m-58b7ff999bbf-light .cluster-label span{color:#4c4f69;}#m-58b7ff999bbf-light .cluster-label span p{background-color:transparent;}#m-58b7ff999bbf-light .label text,#m-58b7ff999bbf-light span{fill:#4c4f69;color:#4c4f69;}#m-58b7ff999bbf-light .node rect,#m-58b7ff999bbf-light .node circle,#m-58b7ff999bbf-light .node ellipse,#m-58b7ff999bbf-light .node polygon,#m-58b7ff999bbf-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-58b7ff999bbf-light .rough-node .label text,#m-58b7ff999bbf-light .node .label text,#m-58b7ff999bbf-light .image-shape .label,#m-58b7ff999bbf-light .icon-shape .label{text-anchor:middle;}#m-58b7ff999bbf-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-58b7ff999bbf-light .rough-node .label,#m-58b7ff999bbf-light .node .label,#m-58b7ff999bbf-light .image-shape .label,#m-58b7ff999bbf-light .icon-shape .label{text-align:center;}#m-58b7ff999bbf-light .node.clickable{cursor:pointer;}#m-58b7ff999bbf-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-58b7ff999bbf-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-58b7ff999bbf-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-58b7ff999bbf-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-58b7ff999bbf-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-58b7ff999bbf-light .edgeLabel p{background-color:#eff1f5;}#m-58b7ff999bbf-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-58b7ff999bbf-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-58b7ff999bbf-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-58b7ff999bbf-light .cluster text{fill:#4c4f69;}#m-58b7ff999bbf-light .cluster span{color:#4c4f69;}#m-58b7ff999bbf-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-58b7ff999bbf-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-58b7ff999bbf-light rect.text{fill:none;stroke-width:0;}#m-58b7ff999bbf-light .icon-shape,#m-58b7ff999bbf-light .image-shape{background-color:#eff1f5;text-align:center;}#m-58b7ff999bbf-light .icon-shape p,#m-58b7ff999bbf-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-58b7ff999bbf-light .icon-shape .label rect,#m-58b7ff999bbf-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-58b7ff999bbf-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-58b7ff999bbf-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-58b7ff999bbf-light .node .neo-node{stroke:#bcc0cc;}#m-58b7ff999bbf-light [data-look="neo"].node rect,#m-58b7ff999bbf-light [data-look="neo"].cluster rect,#m-58b7ff999bbf-light [data-look="neo"].node polygon{stroke:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].node path{stroke:url(#m-58b7ff999bbf-light-gradient);stroke-width:1px;}#m-58b7ff999bbf-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-58b7ff999bbf-light [data-look="neo"].node circle{stroke:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-58b7ff999bbf-light [data-look="neo"].icon-shape .icon{fill:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-58b7ff999bbf-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-58b7ff999bbf-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-58b7ff999bbf-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M119.801,158.43L131.868,148.593C143.935,138.756,168.069,119.081,186.669,109.244C205.268,99.407,218.333,99.407,224.866,99.407L231.398,99.407" id="m-58b7ff999bbf-light-L_U_CDN_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_CDN_0" data-points="W3sieCI6MTE5LjgwMTQ1MjU4NjIxOTkxLCJ5IjoxNTguNDMwMDg4MDQzMjEyOX0seyJ4IjoxOTIuMjAzMTI1LCJ5Ijo5OS40MDY3ODQwNTc2MTcxOX0seyJ4IjoyMzUuMzk4NDM3NSwieSI6OTkuNDA2Nzg0MDU3NjE3MTl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M126.287,210.93L137.273,218.385C148.259,225.839,170.231,240.748,184.717,248.202C199.203,255.657,206.203,255.657,209.703,255.657L213.203,255.657" id="m-58b7ff999bbf-light-L_U_GLB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GLB_0" data-points="W3sieCI6MTI2LjI4NzM3MzA1MjA2ODUsInkiOjIxMC45MzAwODgwNDMyMTI5fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjIxNy4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M422.656,255.657L426.823,255.657C430.99,255.657,439.323,255.657,446.99,255.657C454.656,255.657,461.656,255.657,465.156,255.657L468.656,255.657" id="m-58b7ff999bbf-light-L_GLB_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GLB_R_0" data-points="W3sieCI6NDIyLjY1NjI1LCJ5IjoyNTUuNjU2Nzg0MDU3NjE3Mn0seyJ4Ijo0NDcuNjU2MjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjQ3Mi42NTYyNSwieSI6MjU1LjY1Njc4NDA1NzYxNzJ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M626.813,260.518L633.618,260.947C640.424,261.376,654.036,262.235,666.997,261.32C679.958,260.406,692.267,257.718,698.422,256.375L704.576,255.031" id="m-58b7ff999bbf-light-L_R_SS_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_0" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI2MC41MTc4Mjk2ODgyNzMyfSx7IngiOjY2Ny42NDg0Mzc1LCJ5IjoyNjMuMDkzMjA4MzEyOTg4M30seyJ4Ijo3MDguNDg0Mzc1LCJ5IjoyNTQuMTc3NTUxNDE2NTMwNjZ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M883.965,194.343L898.2,187.049C912.435,179.755,940.905,165.167,963.995,151.054C987.084,136.941,1004.793,123.304,1013.648,116.485L1022.503,109.666" id="m-58b7ff999bbf-light-L_SS_ES_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_ES_0" data-points="W3sieCI6ODgzLjk2NTAyMDAxMTg5MTMsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo5NjkuMzc1LCJ5IjoxNTAuNTc4MzkyMDI4ODA4Nn0seyJ4IjoxMDI1LjY3MTg3NSwieSI6MTA3LjIyNTkxOTQ4NzY0MDl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M913.078,237.125L922.461,237.609C931.844,238.094,950.609,239.063,971.448,239.547C992.286,240.032,1015.198,240.032,1026.654,240.032L1038.109,240.032" id="m-58b7ff999bbf-light-L_SS_RR_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_RR_0" data-points="W3sieCI6OTEzLjA3ODEyNSwieSI6MjM3LjEyNTA0MTM1NzMzMTY3fSx7IngiOjk2OS4zNzUsInkiOjI0MC4wMzE3ODQwNTc2MTcyfSx7IngiOjEwNDIuMTA5Mzc1LCJ5IjoyNDAuMDMxNzg0MDU3NjE3Mn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M607.898,72.453L617.857,72.453C627.815,72.453,647.732,72.453,672.1,72.453C696.469,72.453,725.289,72.453,739.699,72.453L754.109,72.453" id="m-58b7ff999bbf-light-L_PG_Q_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PG_Q_0" data-points="W3sieCI6NjA3Ljg5ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NjY3LjY0ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NzU4LjEwOTM3NSwieSI6NzIuNDUzMzkyMDI4ODA4Nn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M863.453,72.453L881.107,72.453C898.76,72.453,934.068,72.453,960.438,72.453C986.807,72.453,1004.24,72.453,1012.956,72.453L1021.672,72.453" id="m-58b7ff999bbf-light-L_Q_ES_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Q_ES_0" data-points="W3sieCI6ODYzLjQ1MzEyNSwieSI6NzIuNDUzMzkyMDI4ODA4Nn0seyJ4Ijo5NjkuMzc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fSx7IngiOjEwMjUuNjcxODc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M626.813,240.09L633.618,238.716C640.424,237.341,654.036,234.592,666.982,233.218C679.927,231.843,692.206,231.843,698.345,231.843L704.484,231.843" id="m-58b7ff999bbf-light-L_R_SS_2" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_2" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI0MC4wOTAzMTM2MjEyMjIyNn0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MjMxLjg0MzIwODMxMjk4ODI4fSx7IngiOjcwOC40ODQzNzUsInkiOjIzMS44NDMyMDgzMTI5ODgyOH1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/><path d="M721.228,194.343L712.298,190.604C703.368,186.864,685.508,179.386,656.926,175.646C628.344,171.907,589.039,171.907,552.374,171.907C515.708,171.907,481.682,171.907,443.382,171.907C405.081,171.907,362.505,171.907,319.93,171.907C277.354,171.907,234.779,171.907,209.986,172.335C185.193,172.763,178.183,173.619,174.679,174.047L171.174,174.475" id="m-58b7ff999bbf-light-L_SS_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_U_0" data-points="W3sieCI6NzIxLjIyODM1MjMxODI3MDEsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NTQ5LjczNDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NDQ3LjY1NjI1LCJ5IjoxNzEuOTA2Nzg0MDU3NjE3Mn0seyJ4IjozMTkuOTI5Njg3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTkyLjIwMzEyNSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTY3LjIwMzEyNSwieSI6MTc0Ljk1OTYzMTM3NjYwMzR9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_CDN_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_U_GLB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GLB_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(668.07871, 262.99927)"><g class="label" data-id="L_R_SS_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_ES_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_RR_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(667.6484375, 72.4533920288086)"><g class="label" data-id="L_PG_Q_0" transform="translate(-15.8359375, -11.25)"><foreignObject width="31.671875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>CDC</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(969.375, 72.4533920288086)"><g class="label" data-id="L_Q_ES_0" transform="translate(-31.296875, -11.25)"><foreignObject width="62.59375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>consume</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_R_SS_2" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-58b7ff999bbf-light-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 184.6800880432129)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-CDN-1" data-look="classic" transform="translate(319.9296875, 99.40678405761719)"><rect class="basic label-container" style="" x="-84.53125" y="-37.5" width="169.0625" height="75"/><g class="label" style="" transform="translate(-54.53125, -22.5)"><rect/><foreignObject width="109.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>CDN<br />статика, медиа</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-GLB-3" data-look="classic" transform="translate(319.9296875, 255.6567840576172)"><rect class="basic label-container" style="" x="-102.7265625" y="-48.75" width="205.453125" height="97.5"/><g class="label" style="" transform="translate(-72.7265625, -33.75)"><rect/><foreignObject width="145.453125" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Global LB<br />гео-роутинг<br />в 1 активный регион</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-R-5" data-look="classic" transform="translate(549.734375, 255.6567840576172)"><rect class="basic label-container" style="" x="-77.078125" y="-48.75" width="154.15625" height="97.5"/><g class="label" style="" transform="translate(-47.078125, -33.75)"><rect/><foreignObject width="94.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Redis Cluster<br />top queries +<br />autocomplete</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-SS-7" data-look="classic" transform="translate(810.78125, 231.84320831298828)"><rect class="basic label-container" style="" x="-102.296875" y="-37.5" width="204.59375" height="75"/><g class="label" style="" transform="translate(-72.296875, -22.5)"><rect/><foreignObject width="144.59375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Search Service<br />app-tier, 12–20 инст.</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-ES-9" data-look="classic" transform="translate(1127.109375, 72.4533920288086)"><path d="M0,15.468928707586732 a101.4375,15.468928707586732 0,0,0 202.875,0 a101.4375,15.468928707586732 0,0,0 -202.875,0 l0,97.96892870758673 a101.4375,15.468928707586732 0,0,0 202.875,0 l0,-97.96892870758673" class="basic label-container outer-path" style="" transform="translate(-101.4375, -64.4533930613801)"/><g class="label" style="" transform="translate(-93.9375, -23.75)"><rect/><foreignObject width="187.875" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Elasticsearch<br />8–12 нод, 6 primary shards<br />× 2 replicas</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-RR-11" data-look="classic" transform="translate(1127.109375, 240.0317840576172)"><rect class="basic label-container" style="" x="-85" y="-37.5" width="170" height="75"/><g class="label" style="" transform="translate(-55, -22.5)"><rect/><foreignObject width="110" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Ranking Service<br />отдельный SLO</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-PG-12" data-look="classic" transform="translate(549.734375, 72.4533920288086)"><path d="M0,12.050825509873746 a58.1640625,12.050825509873746 0,0,0 116.328125,0 a58.1640625,12.050825509873746 0,0,0 -116.328125,0 l0,72.05082550987375 a58.1640625,12.050825509873746 0,0,0 116.328125,0 l0,-72.05082550987375" class="basic label-container outer-path" style="" transform="translate(-58.1640625, -48.07623826481062)"/><g class="label" style="" transform="translate(-50.6640625, -12.5)"><rect/><foreignObject width="101.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres<br />source of truth</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-light-flowchart-Q-13" data-look="classic" transform="translate(810.78125, 72.4533920288086)"><rect class="basic label-container" style="" x="-52.671875" y="-37.5" width="105.34375" height="75"/><g class="label" style="" transform="translate(-22.671875, -22.5)"><rect/><foreignObject width="45.34375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Kafka<br />events</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-58b7ff999bbf-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-58b7ff999bbf-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-58b7ff999bbf-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-58b7ff999bbf-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1236.55px; background-color: transparent;" viewBox="0 0 1236.546875 312.40679931640625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-58b7ff999bbf-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-58b7ff999bbf-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-58b7ff999bbf-dark .error-icon{fill:#181926;}#m-58b7ff999bbf-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-58b7ff999bbf-dark .edge-thickness-normal{stroke-width:1px;}#m-58b7ff999bbf-dark .edge-thickness-thick{stroke-width:3.5px;}#m-58b7ff999bbf-dark .edge-pattern-solid{stroke-dasharray:0;}#m-58b7ff999bbf-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-58b7ff999bbf-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-58b7ff999bbf-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-58b7ff999bbf-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-58b7ff999bbf-dark .marker.cross{stroke:#a5adcb;}#m-58b7ff999bbf-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-58b7ff999bbf-dark p{margin:0;}#m-58b7ff999bbf-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-58b7ff999bbf-dark .cluster-label text{fill:#cad3f5;}#m-58b7ff999bbf-dark .cluster-label span{color:#cad3f5;}#m-58b7ff999bbf-dark .cluster-label span p{background-color:transparent;}#m-58b7ff999bbf-dark .label text,#m-58b7ff999bbf-dark span{fill:#cad3f5;color:#cad3f5;}#m-58b7ff999bbf-dark .node rect,#m-58b7ff999bbf-dark .node circle,#m-58b7ff999bbf-dark .node ellipse,#m-58b7ff999bbf-dark .node polygon,#m-58b7ff999bbf-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-58b7ff999bbf-dark .rough-node .label text,#m-58b7ff999bbf-dark .node .label text,#m-58b7ff999bbf-dark .image-shape .label,#m-58b7ff999bbf-dark .icon-shape .label{text-anchor:middle;}#m-58b7ff999bbf-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-58b7ff999bbf-dark .rough-node .label,#m-58b7ff999bbf-dark .node .label,#m-58b7ff999bbf-dark .image-shape .label,#m-58b7ff999bbf-dark .icon-shape .label{text-align:center;}#m-58b7ff999bbf-dark .node.clickable{cursor:pointer;}#m-58b7ff999bbf-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-58b7ff999bbf-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-58b7ff999bbf-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-58b7ff999bbf-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-58b7ff999bbf-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-58b7ff999bbf-dark .edgeLabel p{background-color:#24273a;}#m-58b7ff999bbf-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-58b7ff999bbf-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-58b7ff999bbf-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-58b7ff999bbf-dark .cluster text{fill:#cad3f5;}#m-58b7ff999bbf-dark .cluster span{color:#cad3f5;}#m-58b7ff999bbf-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-58b7ff999bbf-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-58b7ff999bbf-dark rect.text{fill:none;stroke-width:0;}#m-58b7ff999bbf-dark .icon-shape,#m-58b7ff999bbf-dark .image-shape{background-color:#24273a;text-align:center;}#m-58b7ff999bbf-dark .icon-shape p,#m-58b7ff999bbf-dark .image-shape p{background-color:#24273a;padding:2px;}#m-58b7ff999bbf-dark .icon-shape .label rect,#m-58b7ff999bbf-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-58b7ff999bbf-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-58b7ff999bbf-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-58b7ff999bbf-dark .node .neo-node{stroke:#494d64;}#m-58b7ff999bbf-dark [data-look="neo"].node rect,#m-58b7ff999bbf-dark [data-look="neo"].cluster rect,#m-58b7ff999bbf-dark [data-look="neo"].node polygon{stroke:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].node path{stroke:url(#m-58b7ff999bbf-dark-gradient);stroke-width:1px;}#m-58b7ff999bbf-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-58b7ff999bbf-dark [data-look="neo"].node circle{stroke:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-58b7ff999bbf-dark [data-look="neo"].icon-shape .icon{fill:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-58b7ff999bbf-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-58b7ff999bbf-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-58b7ff999bbf-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M119.801,158.43L131.868,148.593C143.935,138.756,168.069,119.081,186.669,109.244C205.268,99.407,218.333,99.407,224.866,99.407L231.398,99.407" id="m-58b7ff999bbf-dark-L_U_CDN_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_CDN_0" data-points="W3sieCI6MTE5LjgwMTQ1MjU4NjIxOTkxLCJ5IjoxNTguNDMwMDg4MDQzMjEyOX0seyJ4IjoxOTIuMjAzMTI1LCJ5Ijo5OS40MDY3ODQwNTc2MTcxOX0seyJ4IjoyMzUuMzk4NDM3NSwieSI6OTkuNDA2Nzg0MDU3NjE3MTl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M126.287,210.93L137.273,218.385C148.259,225.839,170.231,240.748,184.717,248.202C199.203,255.657,206.203,255.657,209.703,255.657L213.203,255.657" id="m-58b7ff999bbf-dark-L_U_GLB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GLB_0" data-points="W3sieCI6MTI2LjI4NzM3MzA1MjA2ODUsInkiOjIxMC45MzAwODgwNDMyMTI5fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjIxNy4yMDMxMjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M422.656,255.657L426.823,255.657C430.99,255.657,439.323,255.657,446.99,255.657C454.656,255.657,461.656,255.657,465.156,255.657L468.656,255.657" id="m-58b7ff999bbf-dark-L_GLB_R_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GLB_R_0" data-points="W3sieCI6NDIyLjY1NjI1LCJ5IjoyNTUuNjU2Nzg0MDU3NjE3Mn0seyJ4Ijo0NDcuNjU2MjUsInkiOjI1NS42NTY3ODQwNTc2MTcyfSx7IngiOjQ3Mi42NTYyNSwieSI6MjU1LjY1Njc4NDA1NzYxNzJ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M626.813,260.518L633.618,260.947C640.424,261.376,654.036,262.235,666.997,261.32C679.958,260.406,692.267,257.718,698.422,256.375L704.576,255.031" id="m-58b7ff999bbf-dark-L_R_SS_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_0" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI2MC41MTc4Mjk2ODgyNzMyfSx7IngiOjY2Ny42NDg0Mzc1LCJ5IjoyNjMuMDkzMjA4MzEyOTg4M30seyJ4Ijo3MDguNDg0Mzc1LCJ5IjoyNTQuMTc3NTUxNDE2NTMwNjZ9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M883.965,194.343L898.2,187.049C912.435,179.755,940.905,165.167,963.995,151.054C987.084,136.941,1004.793,123.304,1013.648,116.485L1022.503,109.666" id="m-58b7ff999bbf-dark-L_SS_ES_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_ES_0" data-points="W3sieCI6ODgzLjk2NTAyMDAxMTg5MTMsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo5NjkuMzc1LCJ5IjoxNTAuNTc4MzkyMDI4ODA4Nn0seyJ4IjoxMDI1LjY3MTg3NSwieSI6MTA3LjIyNTkxOTQ4NzY0MDl9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M913.078,237.125L922.461,237.609C931.844,238.094,950.609,239.063,971.448,239.547C992.286,240.032,1015.198,240.032,1026.654,240.032L1038.109,240.032" id="m-58b7ff999bbf-dark-L_SS_RR_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_RR_0" data-points="W3sieCI6OTEzLjA3ODEyNSwieSI6MjM3LjEyNTA0MTM1NzMzMTY3fSx7IngiOjk2OS4zNzUsInkiOjI0MC4wMzE3ODQwNTc2MTcyfSx7IngiOjEwNDIuMTA5Mzc1LCJ5IjoyNDAuMDMxNzg0MDU3NjE3Mn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M607.898,72.453L617.857,72.453C627.815,72.453,647.732,72.453,672.1,72.453C696.469,72.453,725.289,72.453,739.699,72.453L754.109,72.453" id="m-58b7ff999bbf-dark-L_PG_Q_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PG_Q_0" data-points="W3sieCI6NjA3Ljg5ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NjY3LjY0ODQzNzUsInkiOjcyLjQ1MzM5MjAyODgwODZ9LHsieCI6NzU4LjEwOTM3NSwieSI6NzIuNDUzMzkyMDI4ODA4Nn1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M863.453,72.453L881.107,72.453C898.76,72.453,934.068,72.453,960.438,72.453C986.807,72.453,1004.24,72.453,1012.956,72.453L1021.672,72.453" id="m-58b7ff999bbf-dark-L_Q_ES_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Q_ES_0" data-points="W3sieCI6ODYzLjQ1MzEyNSwieSI6NzIuNDUzMzkyMDI4ODA4Nn0seyJ4Ijo5NjkuMzc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fSx7IngiOjEwMjUuNjcxODc1LCJ5Ijo3Mi40NTMzOTIwMjg4MDg2fV0=" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M626.813,240.09L633.618,238.716C640.424,237.341,654.036,234.592,666.982,233.218C679.927,231.843,692.206,231.843,698.345,231.843L704.484,231.843" id="m-58b7ff999bbf-dark-L_R_SS_2" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_R_SS_2" data-points="W3sieCI6NjI2LjgxMjUsInkiOjI0MC4wOTAzMTM2MjEyMjIyNn0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MjMxLjg0MzIwODMxMjk4ODI4fSx7IngiOjcwOC40ODQzNzUsInkiOjIzMS44NDMyMDgzMTI5ODgyOH1d" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/><path d="M721.228,194.343L712.298,190.604C703.368,186.864,685.508,179.386,656.926,175.646C628.344,171.907,589.039,171.907,552.374,171.907C515.708,171.907,481.682,171.907,443.382,171.907C405.081,171.907,362.505,171.907,319.93,171.907C277.354,171.907,234.779,171.907,209.986,172.335C185.193,172.763,178.183,173.619,174.679,174.047L171.174,174.475" id="m-58b7ff999bbf-dark-L_SS_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SS_U_0" data-points="W3sieCI6NzIxLjIyODM1MjMxODI3MDEsInkiOjE5NC4zNDMyMDgzMTI5ODgyOH0seyJ4Ijo2NjcuNjQ4NDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NTQ5LjczNDM3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6NDQ3LjY1NjI1LCJ5IjoxNzEuOTA2Nzg0MDU3NjE3Mn0seyJ4IjozMTkuOTI5Njg3NSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTkyLjIwMzEyNSwieSI6MTcxLjkwNjc4NDA1NzYxNzJ9LHsieCI6MTY3LjIwMzEyNSwieSI6MTc0Ljk1OTYzMTM3NjYwMzR9XQ==" data-look="classic" marker-end="url(#m-58b7ff999bbf-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_CDN_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_U_GLB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GLB_R_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(668.07871, 262.99927)"><g class="label" data-id="L_R_SS_0" transform="translate(-15.6953125, -11.25)"><foreignObject width="31.390625" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>miss</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_ES_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_RR_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(667.6484375, 72.4533920288086)"><g class="label" data-id="L_PG_Q_0" transform="translate(-15.8359375, -11.25)"><foreignObject width="31.671875" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>CDC</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(969.375, 72.4533920288086)"><g class="label" data-id="L_Q_ES_0" transform="translate(-31.296875, -11.25)"><foreignObject width="62.59375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>consume</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_R_SS_2" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SS_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-58b7ff999bbf-dark-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 184.6800880432129)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-CDN-1" data-look="classic" transform="translate(319.9296875, 99.40678405761719)"><rect class="basic label-container" style="" x="-84.53125" y="-37.5" width="169.0625" height="75"/><g class="label" style="" transform="translate(-54.53125, -22.5)"><rect/><foreignObject width="109.0625" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>CDN<br />статика, медиа</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-GLB-3" data-look="classic" transform="translate(319.9296875, 255.6567840576172)"><rect class="basic label-container" style="" x="-102.7265625" y="-48.75" width="205.453125" height="97.5"/><g class="label" style="" transform="translate(-72.7265625, -33.75)"><rect/><foreignObject width="145.453125" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Global LB<br />гео-роутинг<br />в 1 активный регион</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-R-5" data-look="classic" transform="translate(549.734375, 255.6567840576172)"><rect class="basic label-container" style="" x="-77.078125" y="-48.75" width="154.15625" height="97.5"/><g class="label" style="" transform="translate(-47.078125, -33.75)"><rect/><foreignObject width="94.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Redis Cluster<br />top queries +<br />autocomplete</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-SS-7" data-look="classic" transform="translate(810.78125, 231.84320831298828)"><rect class="basic label-container" style="" x="-102.296875" y="-37.5" width="204.59375" height="75"/><g class="label" style="" transform="translate(-72.296875, -22.5)"><rect/><foreignObject width="144.59375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Search Service<br />app-tier, 12–20 инст.</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-ES-9" data-look="classic" transform="translate(1127.109375, 72.4533920288086)"><path d="M0,15.468928707586732 a101.4375,15.468928707586732 0,0,0 202.875,0 a101.4375,15.468928707586732 0,0,0 -202.875,0 l0,97.96892870758673 a101.4375,15.468928707586732 0,0,0 202.875,0 l0,-97.96892870758673" class="basic label-container outer-path" style="" transform="translate(-101.4375, -64.4533930613801)"/><g class="label" style="" transform="translate(-93.9375, -23.75)"><rect/><foreignObject width="187.875" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Elasticsearch<br />8–12 нод, 6 primary shards<br />× 2 replicas</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-RR-11" data-look="classic" transform="translate(1127.109375, 240.0317840576172)"><rect class="basic label-container" style="" x="-85" y="-37.5" width="170" height="75"/><g class="label" style="" transform="translate(-55, -22.5)"><rect/><foreignObject width="110" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Ranking Service<br />отдельный SLO</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-PG-12" data-look="classic" transform="translate(549.734375, 72.4533920288086)"><path d="M0,12.050825509873746 a58.1640625,12.050825509873746 0,0,0 116.328125,0 a58.1640625,12.050825509873746 0,0,0 -116.328125,0 l0,72.05082550987375 a58.1640625,12.050825509873746 0,0,0 116.328125,0 l0,-72.05082550987375" class="basic label-container outer-path" style="" transform="translate(-58.1640625, -48.07623826481062)"/><g class="label" style="" transform="translate(-50.6640625, -12.5)"><rect/><foreignObject width="101.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Postgres<br />source of truth</p></span></div></foreignObject></g></g><g class="node default" id="m-58b7ff999bbf-dark-flowchart-Q-13" data-look="classic" transform="translate(810.78125, 72.4533920288086)"><rect class="basic label-container" style="" x="-52.671875" y="-37.5" width="105.34375" height="75"/><g class="label" style="" transform="translate(-22.671875, -22.5)"><rect/><foreignObject width="45.34375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Kafka<br />events</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-58b7ff999bbf-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-58b7ff999bbf-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-58b7ff999bbf-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p><strong>Числовые прикидки:</strong></p>
<ul>
<li><strong>Capacity.</strong> 30k RPS пика → допустимо терять ~30 запросов/сек. На Elasticsearch при 50M документов и <code>6 primary shards × 1 replica</code> (~33 GB на шард, в коридоре best-practice 30–50 GB): запрос идёт в координатор, координатор делает fan-out на 6 шардов, агрегирует. Целевая нагрузка <code>30–40k RPS</code> с запасом. На 8–12 ES-нодах (<code>64–128 GB RAM</code> каждая) держится с P99 в районе <code>300–400 ms</code> чистого ES-времени.</li>
<li><strong>Кэш Redis.</strong> Кластер на 3–6 нодах, <code>~50 GB</code> под топовые запросы и autocomplete. Hit rate <code>65–75%</code> — ключевой множитель: при 70% hit rate на ES падает только 30% от 30k RPS, то есть <code>~9k RPS</code>. Это уже посильно для умеренного ES-кластера. Прогрев после деплоя — 5–10 минут на горячих топ-1000 запросах.</li>
<li><strong>Индексирование.</strong> Postgres → Debezium / собственный CDC → Kafka → потребитель в ES. Lag индексирования: <code>15–60 секунд</code> на типовых изменениях, <code>2–5 минут</code> на массовых обновлениях цен (например, при загрузке от продавца XML-фида). Это <strong>freshness-SLI</strong> — ценник старше 60 секунд считается «плохим событием» по SLI-4.</li>
<li><strong>Структура документа и размер индекса.</strong> Каждая карточка в ES — денормализованный документ: <code>title, description, attributes (10–20 полей), price, stock, seller_rating, geo, popularity</code>. Размер ~3–5 KB на документ. <code>50M × 4 KB = ~200 GB primary</code> + 200 GB replica = 400 GB Lucene-данных. На 12 нодах ≈ ~33 GB на ноду, рабочее множество легко помещается в RAM.</li>
<li><strong>Failover.</strong> Active-passive между двумя AZ внутри региона. RTO <code>1–3 минуты</code> через автоматический promote secondary. RPO ~30 секунд через CDC.</li>
<li><strong>Алерты.</strong> Multi-window multi-burn-rate: две пары окон со своими threshold — быстрая пара <code>5 мин + 1 час</code> с burn-rate <code>14.4×</code> (срабатывает на острый инцидент за минуты, 2% бюджета за час) и медленная пара <code>30 мин + 6 часов</code> с burn-rate <code>6×</code> (ловит «тихую» деградацию, 5% бюджета за 6 часов). Это табличные значения из главы Alerting on SLOs в Google SRE Workbook. Подробно про burn-rate как множитель — в моей предыдущей статье <a href="/posts/burn-rate-is-not-speed/">«Скорость сгорания бюджета ошибок»</a>.</li>
</ul>
<p><strong>Что добавилось по сравнению с 99.0%:</strong></p>
<ul>
<li>Отдельный поисковый индекс (ES) — потому что <code>pg_trgm</code> на 30k RPS + ranking + facets уже не вытянет с нужной P99.</li>
<li>Redis-кластер — без него ES под пиком пришлось бы масштабировать в 3–4 раза.</li>
<li>CDC + Kafka — из-за разделения source of truth и поискового индекса нужна асинхронная синхронизация. Это новый слой риска (lag, потеря событий) и новый SLO на freshness.</li>
<li>Active-passive failover — потому что 43 минуты бюджета мало для ручного переключения.</li>
<li>Burn-rate-алерты — потому что классические «5xx &gt; 1%» при 0.1% бюджета будут срабатывать слишком поздно или генерить шум.</li>
</ul>
<p><strong>Что намеренно НЕ делаем:</strong></p>
<ul>
<li>Не делаем active-active multi-region. На 99.9% это избыточно: один регион с грамотным failover между AZ покрывает бюджет с большим запасом, а active-active добавляет проблем синхронизации (split-brain, write-write конфликты, кросс-региональные чтения по сети).</li>
<li>Не делаем repair-aware кэш — на 99.9% достаточно правильного TTL и event-driven invalidation.</li>
<li>Не делаем graceful degradation на уровне UI («поиск работает, ranking упал»). Это усложнение, оправданное на <code>99.99%+</code>.</li>
</ul>
<p><strong>Стоимость:</strong> <strong>4–6× baseline</strong> в инфраструктуре, <strong>2–3× baseline</strong> в людях (нужна команда SRE с дежурствами, отдельные процессы для Kafka и ES).</p>
<h3 id="slo-9999-дорогая-надёжность-для-tier-1-сценариев">SLO 99.99%: дорогая надёжность для tier-1 сценариев</h3>
<p><strong>Бюджет:</strong> ~2.6M «плохих» запросов за 30 дней, или <strong>~1 мин 27 с</strong> полного даунтайма на пике / <strong>~4 мин 20 с</strong> при средней нагрузке. На инцидент остаются секунды-минуты — реакция должна быть <strong>автоматической</strong>. Ручной разбор перед remediation = превышение бюджета.</p>
<p><strong>Когда уместно:</strong> tier-1 пользовательские сценарии в крупном бизнесе уровня Ozon/Wildberries или сопоставимого по GMV, где минута даунтайма стоит сотни тысяч долларов и оправдывает мультипликатор инфраструктуры. Платёжные шлюзы, биржи, ключевые поисковые сценарии больших маркетплейсов.</p>
<p><strong>Read path:</strong></p>
<figure class="mermaid mermaid--prerendered" aria-label="Mermaid diagram" data-png="https://jtprog.ru/mermaid/94d254942e3ac4fe8b5363ef30d76426ec177af9250d290bfe29c533df8e7747.png">
<div class="mermaid-svg mermaid-svg--light">
<svg id="m-94d254942e3a-light" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1388.78px; background-color: transparent;" viewBox="0 -35 1388.78125 476.625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-94d254942e3a-light{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#4c4f69;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-94d254942e3a-light .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-94d254942e3a-light .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-94d254942e3a-light .error-icon{fill:#dce0e8;}#m-94d254942e3a-light .error-text{fill:#d20f39;stroke:#d20f39;}#m-94d254942e3a-light .edge-thickness-normal{stroke-width:1px;}#m-94d254942e3a-light .edge-thickness-thick{stroke-width:3.5px;}#m-94d254942e3a-light .edge-pattern-solid{stroke-dasharray:0;}#m-94d254942e3a-light .edge-thickness-invisible{stroke-width:0;fill:none;}#m-94d254942e3a-light .edge-pattern-dashed{stroke-dasharray:3;}#m-94d254942e3a-light .edge-pattern-dotted{stroke-dasharray:2;}#m-94d254942e3a-light .marker{fill:#5c5f77;stroke:#5c5f77;}#m-94d254942e3a-light .marker.cross{stroke:#5c5f77;}#m-94d254942e3a-light svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-94d254942e3a-light p{margin:0;}#m-94d254942e3a-light .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#4c4f69;}#m-94d254942e3a-light .cluster-label text{fill:#4c4f69;}#m-94d254942e3a-light .cluster-label span{color:#4c4f69;}#m-94d254942e3a-light .cluster-label span p{background-color:transparent;}#m-94d254942e3a-light .label text,#m-94d254942e3a-light span{fill:#4c4f69;color:#4c4f69;}#m-94d254942e3a-light .node rect,#m-94d254942e3a-light .node circle,#m-94d254942e3a-light .node ellipse,#m-94d254942e3a-light .node polygon,#m-94d254942e3a-light .node path{fill:#e6e9ef;stroke:#bcc0cc;stroke-width:1px;}#m-94d254942e3a-light .rough-node .label text,#m-94d254942e3a-light .node .label text,#m-94d254942e3a-light .image-shape .label,#m-94d254942e3a-light .icon-shape .label{text-anchor:middle;}#m-94d254942e3a-light .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-94d254942e3a-light .rough-node .label,#m-94d254942e3a-light .node .label,#m-94d254942e3a-light .image-shape .label,#m-94d254942e3a-light .icon-shape .label{text-align:center;}#m-94d254942e3a-light .node.clickable{cursor:pointer;}#m-94d254942e3a-light .root .anchor path{fill:#5c5f77!important;stroke-width:0;stroke:#5c5f77;}#m-94d254942e3a-light .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-94d254942e3a-light .edgePath .path{stroke:#5c5f77;stroke-width:1px;}#m-94d254942e3a-light .flowchart-link{stroke:#5c5f77;fill:none;}#m-94d254942e3a-light .edgeLabel{background-color:#eff1f5;text-align:center;}#m-94d254942e3a-light .edgeLabel p{background-color:#eff1f5;}#m-94d254942e3a-light .edgeLabel rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-94d254942e3a-light .labelBkg{background-color:rgba(239, 241, 245, 0.5);}#m-94d254942e3a-light .cluster rect{fill:#dce0e8;stroke:#ccd0da;stroke-width:1px;}#m-94d254942e3a-light .cluster text{fill:#4c4f69;}#m-94d254942e3a-light .cluster span{color:#4c4f69;}#m-94d254942e3a-light div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#bcdff0;border:1px solid #209fb5;border-radius:2px;pointer-events:none;z-index:100;}#m-94d254942e3a-light .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#4c4f69;}#m-94d254942e3a-light rect.text{fill:none;stroke-width:0;}#m-94d254942e3a-light .icon-shape,#m-94d254942e3a-light .image-shape{background-color:#eff1f5;text-align:center;}#m-94d254942e3a-light .icon-shape p,#m-94d254942e3a-light .image-shape p{background-color:#eff1f5;padding:2px;}#m-94d254942e3a-light .icon-shape .label rect,#m-94d254942e3a-light .image-shape .label rect{opacity:0.5;background-color:#eff1f5;fill:#eff1f5;}#m-94d254942e3a-light .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-94d254942e3a-light .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-94d254942e3a-light .node .neo-node{stroke:#bcc0cc;}#m-94d254942e3a-light [data-look="neo"].node rect,#m-94d254942e3a-light [data-look="neo"].cluster rect,#m-94d254942e3a-light [data-look="neo"].node polygon{stroke:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].node path{stroke:url(#m-94d254942e3a-light-gradient);stroke-width:1px;}#m-94d254942e3a-light [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].node .neo-line path{stroke:#bcc0cc;filter:none;}#m-94d254942e3a-light [data-look="neo"].node circle{stroke:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].node circle .state-start{fill:#000000;}#m-94d254942e3a-light [data-look="neo"].icon-shape .icon{fill:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-94d254942e3a-light-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-light :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-94d254942e3a-light_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-94d254942e3a-light_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M146.97,265.5L154.509,268.833C162.048,272.167,177.125,278.833,188.164,282.167C199.203,285.5,206.203,285.5,209.703,285.5L213.203,285.5" id="m-94d254942e3a-light-L_U_GA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GA_0" data-points="W3sieCI6MTQ2Ljk3MDAxNjg5MTg5MTksInkiOjI2NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI4NS41fSx7IngiOjIxNy4yMDMxMjUsInkiOjI4NS41fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M419.359,281.993L423.526,281.848C427.693,281.703,436.026,281.414,443.693,281.27C451.359,281.125,458.359,281.125,461.859,281.125L465.359,281.125" id="m-94d254942e3a-light-L_GA_RegA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegA_0" data-points="W3sieCI6NDE5LjM1OTM3NSwieSI6MjgxLjk5MjUxNzY2MDE4MDk3fSx7IngiOjQ0NC4zNTkzNzUsInkiOjI4MS4xMjV9LHsieCI6NDY5LjM1OTM3NSwieSI6MjgxLjEyNX1d" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M395.715,334.25L403.822,339.354C411.93,344.458,428.145,354.667,455.072,359.771C482,364.875,519.641,364.875,565.663,364.875C611.685,364.875,666.089,364.875,705.182,366.999C744.276,369.123,768.06,373.371,779.952,375.496L791.844,377.62" id="m-94d254942e3a-light-L_GA_RegB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegB_0" data-points="W3sieCI6Mzk1LjcxNTA1OTA1NTExODEsInkiOjMzNC4yNX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjozNjQuODc1fSx7IngiOjU1Ny4yODEyNSwieSI6MzY0Ljg3NX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MzY0Ljg3NX0seyJ4Ijo3OTUuNzgxMjUsInkiOjM3OC4zMjMwNzc2MTAwNzQxfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M337.949,236.75L355.684,192.792C373.419,148.833,408.889,60.917,445.445,16.958C482,-27,519.641,-27,565.663,-27C611.685,-27,666.089,-27,722.449,-27C778.81,-27,837.128,-27,898.426,-27C959.724,-27,1024.003,-27,1084.016,17.539C1144.03,62.078,1199.778,151.156,1227.652,195.695L1255.526,240.234" id="m-94d254942e3a-light-L_GA_RegC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegC_0" data-points="W3sieCI6MzM3Ljk0OTQzNzUsInkiOjIzNi43NX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjotMjd9LHsieCI6NTU3LjI4MTI1LCJ5IjotMjd9LHsieCI6NzIwLjQ5MjE4NzUsInkiOi0yN30seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6LTI3fSx7IngiOjEwODguMjgxMjUsInkiOi0yN30seyJ4IjoxMjU3LjY0ODMxMDcyNTE1MjIsInkiOjI0My42MjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M626.468,329.875L642.139,340.917C657.81,351.958,689.151,374.042,716.703,385.083C744.255,396.125,768.018,396.125,779.9,396.125L791.781,396.125" id="m-94d254942e3a-light-L_RegA_RegB_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegB_0" data-points="W3sieCI6NjI2LjQ2ODQ5NTI0NDU2NTIsInkiOjMyOS44NzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjM5Ni4xMjV9LHsieCI6Nzk1Ljc4MTI1LCJ5IjozOTYuMTI1fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M795.781,420.336L783.233,423.384C770.685,426.432,745.589,432.529,714.738,417.915C683.887,403.301,647.282,367.977,628.98,350.315L610.677,332.653" id="m-94d254942e3a-light-L_RegB_RegA_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegA_0" data-points="W3sieCI6Nzk1Ljc4MTI1LCJ5Ijo0MjAuMzM1NjE0NDUwMjk5Mn0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6NDM4LjYyNX0seyJ4Ijo2MDcuNzk4OTIxMTMwOTUyNCwieSI6MzI5Ljg3NX1d" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M649.203,281.125L661.085,281.125C672.966,281.125,696.729,281.125,737.77,281.125C778.81,281.125,837.128,281.125,898.426,281.125C959.724,281.125,1024.003,281.125,1071.004,281.125C1118.005,281.125,1147.729,281.125,1162.591,281.125L1177.453,281.125" id="m-94d254942e3a-light-L_RegA_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegC_0" data-points="W3sieCI6NjQ1LjIwMzEyNSwieSI6MjgxLjEyNX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MjgxLjEyNX0seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6MjgxLjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjoyODEuMTI1fSx7IngiOjExODEuNDUzMTI1LCJ5IjoyODEuMTI1fV0=" data-look="classic" marker-start="url(#m-94d254942e3a-light_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M999.109,396.125L1013.971,396.125C1028.833,396.125,1058.557,396.125,1094.506,383.55C1130.454,370.975,1172.627,345.824,1193.714,333.249L1214.8,320.674" id="m-94d254942e3a-light-L_RegB_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegC_0" data-points="W3sieCI6OTk1LjEwOTM3NSwieSI6Mzk2LjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjozOTYuMTI1fSx7IngiOjEyMTguMjM1OTAzNTMyNjA4NywieSI6MzE4LjYyNX1d" data-look="classic" marker-start="url(#m-94d254942e3a-light_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M591.049,232.375L612.623,201.229C634.197,170.083,677.344,107.792,710.916,76.646C744.487,45.5,768.482,45.5,780.479,45.5L792.477,45.5" id="m-94d254942e3a-light-L_RegA_RAW_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RAW_0" data-points="W3sieCI6NTkxLjA0OTAzMDE3MjQxMzgsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjQ1LjV9LHsieCI6Nzk2LjQ3NjU2MjUsInkiOjQ1LjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M607.799,232.375L626.581,214.25C645.363,196.125,682.928,159.875,714.758,146.085C746.589,132.295,772.685,140.965,785.734,145.3L798.782,149.635" id="m-94d254942e3a-light-L_RegA_Deg_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_Deg_0" data-points="W3sieCI6NjA3Ljc5ODkyMTEzMDk1MjQsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjEyMy42MjV9LHsieCI6ODAyLjU3ODEyNSwieSI6MTUwLjg5NjU2MjY5NTM2NDgyfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/><path d="M802.578,187.058L788.897,187.84C775.216,188.622,747.854,190.186,706.971,190.968C666.089,191.75,611.685,191.75,565.663,191.75C519.641,191.75,482,191.75,442.167,191.75C402.333,191.75,360.307,191.75,318.281,191.75C276.255,191.75,234.229,191.75,206.024,195.016C177.819,198.282,163.434,204.814,156.242,208.08L149.05,211.346" id="m-94d254942e3a-light-L_Deg_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Deg_U_0" data-points="W3sieCI6ODAyLjU3ODEyNSwieSI6MTg3LjA1ODExODI0NTk1ODc1fSx7IngiOjcyMC40OTIxODc1LCJ5IjoxOTEuNzV9LHsieCI6NTU3LjI4MTI1LCJ5IjoxOTEuNzV9LHsieCI6NDQ0LjM1OTM3NSwieSI6MTkxLjc1fSx7IngiOjMxOC4yODEyNSwieSI6MTkxLjc1fSx7IngiOjE5Mi4yMDMxMjUsInkiOjE5MS43NX0seyJ4IjoxNDUuNDA3Njg5MTQ0NzM2ODUsInkiOjIxM31d" data-look="classic" marker-end="url(#m-94d254942e3a-light_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_GA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegC_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(720.4921875, 396.125)"><g class="label" data-id="L_RegA_RegB_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(692.02177, 411.15079)"><g class="label" data-id="L_RegB_RegA_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(895.4453125, 281.125)"><g class="label" data-id="L_RegA_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1088.28125, 396.125)"><g class="label" data-id="L_RegB_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_RAW_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_Deg_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Deg_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-94d254942e3a-light-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 239.25)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-GA-1" data-look="classic" transform="translate(318.28125, 285.5)"><rect class="basic label-container" style="" x="-101.078125" y="-48.75" width="202.15625" height="97.5"/><g class="label" style="" transform="translate(-71.078125, -33.75)"><rect/><foreignObject width="142.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Geo-Anycast<br />+ Global LB<br />health-aware routing</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RegA-3" data-look="classic" transform="translate(557.28125, 281.125)"><rect class="basic label-container" style="" x="-87.921875" y="-48.75" width="175.84375" height="97.5"/><g class="label" style="" transform="translate(-57.921875, -33.75)"><rect/><foreignObject width="115.84375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион A<br />Search + Redis +<br />ES + Postgres</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RegB-5" data-look="classic" transform="translate(895.4453125, 396.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион B<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RegC-7" data-look="classic" transform="translate(1281.1171875, 281.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион C<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-RAW-17" data-look="classic" transform="translate(895.4453125, 45.5)"><rect class="basic label-container" style="" x="-98.96875" y="-37.5" width="197.9375" height="75"/><g class="label" style="" transform="translate(-68.96875, -22.5)"><rect/><foreignObject width="137.9375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Repair-aware Cache<br />per-region</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-light-flowchart-Deg-19" data-look="classic" transform="translate(895.4453125, 181.75)"><rect class="basic label-container" style="" x="-92.8671875" y="-48.75" width="185.734375" height="97.5"/><g class="label" style="" transform="translate(-62.8671875, -33.75)"><rect/><foreignObject width="125.734375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Degradation Layer<br />ranking упал —<br />отдаём базовое</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-94d254942e3a-light-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-94d254942e3a-light-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-94d254942e3a-light-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#bcc0cc" stop-opacity="1"/><stop offset="100%" stop-color="#ccd0da" stop-opacity="1"/></linearGradient></svg>
</div>
<div class="mermaid-svg mermaid-svg--dark">
<svg id="m-94d254942e3a-dark" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1388.78px; background-color: transparent;" viewBox="0 -35 1388.78125 476.625" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#m-94d254942e3a-dark{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;fill:#cad3f5;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#m-94d254942e3a-dark .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#m-94d254942e3a-dark .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#m-94d254942e3a-dark .error-icon{fill:#181926;}#m-94d254942e3a-dark .error-text{fill:#ed8796;stroke:#ed8796;}#m-94d254942e3a-dark .edge-thickness-normal{stroke-width:1px;}#m-94d254942e3a-dark .edge-thickness-thick{stroke-width:3.5px;}#m-94d254942e3a-dark .edge-pattern-solid{stroke-dasharray:0;}#m-94d254942e3a-dark .edge-thickness-invisible{stroke-width:0;fill:none;}#m-94d254942e3a-dark .edge-pattern-dashed{stroke-dasharray:3;}#m-94d254942e3a-dark .edge-pattern-dotted{stroke-dasharray:2;}#m-94d254942e3a-dark .marker{fill:#a5adcb;stroke:#a5adcb;}#m-94d254942e3a-dark .marker.cross{stroke:#a5adcb;}#m-94d254942e3a-dark svg{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:15px;}#m-94d254942e3a-dark p{margin:0;}#m-94d254942e3a-dark .label{font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#cad3f5;}#m-94d254942e3a-dark .cluster-label text{fill:#cad3f5;}#m-94d254942e3a-dark .cluster-label span{color:#cad3f5;}#m-94d254942e3a-dark .cluster-label span p{background-color:transparent;}#m-94d254942e3a-dark .label text,#m-94d254942e3a-dark span{fill:#cad3f5;color:#cad3f5;}#m-94d254942e3a-dark .node rect,#m-94d254942e3a-dark .node circle,#m-94d254942e3a-dark .node ellipse,#m-94d254942e3a-dark .node polygon,#m-94d254942e3a-dark .node path{fill:#1e2030;stroke:#494d64;stroke-width:1px;}#m-94d254942e3a-dark .rough-node .label text,#m-94d254942e3a-dark .node .label text,#m-94d254942e3a-dark .image-shape .label,#m-94d254942e3a-dark .icon-shape .label{text-anchor:middle;}#m-94d254942e3a-dark .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#m-94d254942e3a-dark .rough-node .label,#m-94d254942e3a-dark .node .label,#m-94d254942e3a-dark .image-shape .label,#m-94d254942e3a-dark .icon-shape .label{text-align:center;}#m-94d254942e3a-dark .node.clickable{cursor:pointer;}#m-94d254942e3a-dark .root .anchor path{fill:#a5adcb!important;stroke-width:0;stroke:#a5adcb;}#m-94d254942e3a-dark .arrowheadPath{fill:rgba(255, 255, 255, 0);}#m-94d254942e3a-dark .edgePath .path{stroke:#a5adcb;stroke-width:1px;}#m-94d254942e3a-dark .flowchart-link{stroke:#a5adcb;fill:none;}#m-94d254942e3a-dark .edgeLabel{background-color:#24273a;text-align:center;}#m-94d254942e3a-dark .edgeLabel p{background-color:#24273a;}#m-94d254942e3a-dark .edgeLabel rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-94d254942e3a-dark .labelBkg{background-color:rgba(36, 39, 58, 0.5);}#m-94d254942e3a-dark .cluster rect{fill:#181926;stroke:#363a4f;stroke-width:1px;}#m-94d254942e3a-dark .cluster text{fill:#cad3f5;}#m-94d254942e3a-dark .cluster span{color:#cad3f5;}#m-94d254942e3a-dark div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:12px;background:#2d4f5c;border:1px solid #7dc4e4;border-radius:2px;pointer-events:none;z-index:100;}#m-94d254942e3a-dark .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#cad3f5;}#m-94d254942e3a-dark rect.text{fill:none;stroke-width:0;}#m-94d254942e3a-dark .icon-shape,#m-94d254942e3a-dark .image-shape{background-color:#24273a;text-align:center;}#m-94d254942e3a-dark .icon-shape p,#m-94d254942e3a-dark .image-shape p{background-color:#24273a;padding:2px;}#m-94d254942e3a-dark .icon-shape .label rect,#m-94d254942e3a-dark .image-shape .label rect{opacity:0.5;background-color:#24273a;fill:#24273a;}#m-94d254942e3a-dark .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#m-94d254942e3a-dark .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#m-94d254942e3a-dark .node .neo-node{stroke:#494d64;}#m-94d254942e3a-dark [data-look="neo"].node rect,#m-94d254942e3a-dark [data-look="neo"].cluster rect,#m-94d254942e3a-dark [data-look="neo"].node polygon{stroke:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].node path{stroke:url(#m-94d254942e3a-dark-gradient);stroke-width:1px;}#m-94d254942e3a-dark [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].node .neo-line path{stroke:#494d64;filter:none;}#m-94d254942e3a-dark [data-look="neo"].node circle{stroke:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].node circle .state-start{fill:#000000;}#m-94d254942e3a-dark [data-look="neo"].icon-shape .icon{fill:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark [data-look="neo"].icon-shape .icon-neo path{stroke:url(#m-94d254942e3a-dark-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#m-94d254942e3a-dark :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="m-94d254942e3a-dark_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="m-94d254942e3a-dark_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M146.97,265.5L154.509,268.833C162.048,272.167,177.125,278.833,188.164,282.167C199.203,285.5,206.203,285.5,209.703,285.5L213.203,285.5" id="m-94d254942e3a-dark-L_U_GA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_U_GA_0" data-points="W3sieCI6MTQ2Ljk3MDAxNjg5MTg5MTksInkiOjI2NS41fSx7IngiOjE5Mi4yMDMxMjUsInkiOjI4NS41fSx7IngiOjIxNy4yMDMxMjUsInkiOjI4NS41fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M419.359,281.993L423.526,281.848C427.693,281.703,436.026,281.414,443.693,281.27C451.359,281.125,458.359,281.125,461.859,281.125L465.359,281.125" id="m-94d254942e3a-dark-L_GA_RegA_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegA_0" data-points="W3sieCI6NDE5LjM1OTM3NSwieSI6MjgxLjk5MjUxNzY2MDE4MDk3fSx7IngiOjQ0NC4zNTkzNzUsInkiOjI4MS4xMjV9LHsieCI6NDY5LjM1OTM3NSwieSI6MjgxLjEyNX1d" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M395.715,334.25L403.822,339.354C411.93,344.458,428.145,354.667,455.072,359.771C482,364.875,519.641,364.875,565.663,364.875C611.685,364.875,666.089,364.875,705.182,366.999C744.276,369.123,768.06,373.371,779.952,375.496L791.844,377.62" id="m-94d254942e3a-dark-L_GA_RegB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegB_0" data-points="W3sieCI6Mzk1LjcxNTA1OTA1NTExODEsInkiOjMzNC4yNX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjozNjQuODc1fSx7IngiOjU1Ny4yODEyNSwieSI6MzY0Ljg3NX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MzY0Ljg3NX0seyJ4Ijo3OTUuNzgxMjUsInkiOjM3OC4zMjMwNzc2MTAwNzQxfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M337.949,236.75L355.684,192.792C373.419,148.833,408.889,60.917,445.445,16.958C482,-27,519.641,-27,565.663,-27C611.685,-27,666.089,-27,722.449,-27C778.81,-27,837.128,-27,898.426,-27C959.724,-27,1024.003,-27,1084.016,17.539C1144.03,62.078,1199.778,151.156,1227.652,195.695L1255.526,240.234" id="m-94d254942e3a-dark-L_GA_RegC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_GA_RegC_0" data-points="W3sieCI6MzM3Ljk0OTQzNzUsInkiOjIzNi43NX0seyJ4Ijo0NDQuMzU5Mzc1LCJ5IjotMjd9LHsieCI6NTU3LjI4MTI1LCJ5IjotMjd9LHsieCI6NzIwLjQ5MjE4NzUsInkiOi0yN30seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6LTI3fSx7IngiOjEwODguMjgxMjUsInkiOi0yN30seyJ4IjoxMjU3LjY0ODMxMDcyNTE1MjIsInkiOjI0My42MjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M626.468,329.875L642.139,340.917C657.81,351.958,689.151,374.042,716.703,385.083C744.255,396.125,768.018,396.125,779.9,396.125L791.781,396.125" id="m-94d254942e3a-dark-L_RegA_RegB_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegB_0" data-points="W3sieCI6NjI2LjQ2ODQ5NTI0NDU2NTIsInkiOjMyOS44NzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjM5Ni4xMjV9LHsieCI6Nzk1Ljc4MTI1LCJ5IjozOTYuMTI1fV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M795.781,420.336L783.233,423.384C770.685,426.432,745.589,432.529,714.738,417.915C683.887,403.301,647.282,367.977,628.98,350.315L610.677,332.653" id="m-94d254942e3a-dark-L_RegB_RegA_0" class="edge-thickness-thick edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegA_0" data-points="W3sieCI6Nzk1Ljc4MTI1LCJ5Ijo0MjAuMzM1NjE0NDUwMjk5Mn0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6NDM4LjYyNX0seyJ4Ijo2MDcuNzk4OTIxMTMwOTUyNCwieSI6MzI5Ljg3NX1d" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M649.203,281.125L661.085,281.125C672.966,281.125,696.729,281.125,737.77,281.125C778.81,281.125,837.128,281.125,898.426,281.125C959.724,281.125,1024.003,281.125,1071.004,281.125C1118.005,281.125,1147.729,281.125,1162.591,281.125L1177.453,281.125" id="m-94d254942e3a-dark-L_RegA_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RegC_0" data-points="W3sieCI6NjQ1LjIwMzEyNSwieSI6MjgxLjEyNX0seyJ4Ijo3MjAuNDkyMTg3NSwieSI6MjgxLjEyNX0seyJ4Ijo4OTUuNDQ1MzEyNSwieSI6MjgxLjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjoyODEuMTI1fSx7IngiOjExODEuNDUzMTI1LCJ5IjoyODEuMTI1fV0=" data-look="classic" marker-start="url(#m-94d254942e3a-dark_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M999.109,396.125L1013.971,396.125C1028.833,396.125,1058.557,396.125,1094.506,383.55C1130.454,370.975,1172.627,345.824,1193.714,333.249L1214.8,320.674" id="m-94d254942e3a-dark-L_RegB_RegC_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegB_RegC_0" data-points="W3sieCI6OTk1LjEwOTM3NSwieSI6Mzk2LjEyNX0seyJ4IjoxMDg4LjI4MTI1LCJ5IjozOTYuMTI1fSx7IngiOjEyMTguMjM1OTAzNTMyNjA4NywieSI6MzE4LjYyNX1d" data-look="classic" marker-start="url(#m-94d254942e3a-dark_flowchart-v2-pointStart)" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M591.049,232.375L612.623,201.229C634.197,170.083,677.344,107.792,710.916,76.646C744.487,45.5,768.482,45.5,780.479,45.5L792.477,45.5" id="m-94d254942e3a-dark-L_RegA_RAW_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_RAW_0" data-points="W3sieCI6NTkxLjA0OTAzMDE3MjQxMzgsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjQ1LjV9LHsieCI6Nzk2LjQ3NjU2MjUsInkiOjQ1LjV9XQ==" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M607.799,232.375L626.581,214.25C645.363,196.125,682.928,159.875,714.758,146.085C746.589,132.295,772.685,140.965,785.734,145.3L798.782,149.635" id="m-94d254942e3a-dark-L_RegA_Deg_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_RegA_Deg_0" data-points="W3sieCI6NjA3Ljc5ODkyMTEzMDk1MjQsInkiOjIzMi4zNzV9LHsieCI6NzIwLjQ5MjE4NzUsInkiOjEyMy42MjV9LHsieCI6ODAyLjU3ODEyNSwieSI6MTUwLjg5NjU2MjY5NTM2NDgyfV0=" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/><path d="M802.578,187.058L788.897,187.84C775.216,188.622,747.854,190.186,706.971,190.968C666.089,191.75,611.685,191.75,565.663,191.75C519.641,191.75,482,191.75,442.167,191.75C402.333,191.75,360.307,191.75,318.281,191.75C276.255,191.75,234.229,191.75,206.024,195.016C177.819,198.282,163.434,204.814,156.242,208.08L149.05,211.346" id="m-94d254942e3a-dark-L_Deg_U_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_Deg_U_0" data-points="W3sieCI6ODAyLjU3ODEyNSwieSI6MTg3LjA1ODExODI0NTk1ODc1fSx7IngiOjcyMC40OTIxODc1LCJ5IjoxOTEuNzV9LHsieCI6NTU3LjI4MTI1LCJ5IjoxOTEuNzV9LHsieCI6NDQ0LjM1OTM3NSwieSI6MTkxLjc1fSx7IngiOjMxOC4yODEyNSwieSI6MTkxLjc1fSx7IngiOjE5Mi4yMDMxMjUsInkiOjE5MS43NX0seyJ4IjoxNDUuNDA3Njg5MTQ0NzM2ODUsInkiOjIxM31d" data-look="classic" marker-end="url(#m-94d254942e3a-dark_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_U_GA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegA_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_GA_RegC_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(720.4921875, 396.125)"><g class="label" data-id="L_RegA_RegB_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(692.02177, 411.15079)"><g class="label" data-id="L_RegB_RegA_0" transform="translate(-50.2890625, -11.25)"><foreignObject width="100.578125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>sync для tier-1</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(895.4453125, 281.125)"><g class="label" data-id="L_RegA_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1088.28125, 396.125)"><g class="label" data-id="L_RegB_RegC_0" transform="translate(-68.171875, -11.25)"><foreignObject width="136.34375" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>async для каталога</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_RAW_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_RegA_Deg_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_Deg_U_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="m-94d254942e3a-dark-flowchart-U-0" data-look="classic" transform="translate(87.6015625, 239.25)"><rect class="basic label-container" style="" x="-79.6015625" y="-26.25" width="159.203125" height="52.5"/><g class="label" style="" transform="translate(-49.6015625, -11.25)"><rect/><foreignObject width="99.203125" height="22.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Пользователь</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-GA-1" data-look="classic" transform="translate(318.28125, 285.5)"><rect class="basic label-container" style="" x="-101.078125" y="-48.75" width="202.15625" height="97.5"/><g class="label" style="" transform="translate(-71.078125, -33.75)"><rect/><foreignObject width="142.15625" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Geo-Anycast<br />+ Global LB<br />health-aware routing</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RegA-3" data-look="classic" transform="translate(557.28125, 281.125)"><rect class="basic label-container" style="" x="-87.921875" y="-48.75" width="175.84375" height="97.5"/><g class="label" style="" transform="translate(-57.921875, -33.75)"><rect/><foreignObject width="115.84375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион A<br />Search + Redis +<br />ES + Postgres</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RegB-5" data-look="classic" transform="translate(895.4453125, 396.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион B<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RegC-7" data-look="classic" transform="translate(1281.1171875, 281.125)"><rect class="basic label-container" style="" x="-99.6640625" y="-37.5" width="199.328125" height="75"/><g class="label" style="" transform="translate(-69.6640625, -22.5)"><rect/><foreignObject width="139.328125" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Регион C<br />полная копия стека</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-RAW-17" data-look="classic" transform="translate(895.4453125, 45.5)"><rect class="basic label-container" style="" x="-98.96875" y="-37.5" width="197.9375" height="75"/><g class="label" style="" transform="translate(-68.96875, -22.5)"><rect/><foreignObject width="137.9375" height="45"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Repair-aware Cache<br />per-region</p></span></div></foreignObject></g></g><g class="node default" id="m-94d254942e3a-dark-flowchart-Deg-19" data-look="classic" transform="translate(895.4453125, 181.75)"><rect class="basic label-container" style="" x="-92.8671875" y="-48.75" width="185.734375" height="97.5"/><g class="label" style="" transform="translate(-62.8671875, -33.75)"><rect/><foreignObject width="125.734375" height="67.5"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Degradation Layer<br />ranking упал —<br />отдаём базовое</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="m-94d254942e3a-dark-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="m-94d254942e3a-dark-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="m-94d254942e3a-dark-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#494d64" stop-opacity="1"/><stop offset="100%" stop-color="#363a4f" stop-opacity="1"/></linearGradient></svg>
</div>
</figure><p><strong>Числовые прикидки:</strong></p>
<ul>
<li><strong>Capacity.</strong> 30k RPS пика делятся между 3 регионами <code>~по 10k RPS</code> нормально. Но <strong>каждый регион должен один переварить 100% трафика</strong> при отказе двух других — то есть sized под <code>30–40k RPS</code>. Это утроение инфраструктуры по сравнению с одним регионом 99.9%, плюс запас.</li>
<li><strong>Кросс-региональная синхронизация индексов.</strong> ES → Kafka MirrorMaker (или managed cross-region streaming) → ES в каждом регионе. Lag <code>5–30 секунд</code> между регионами в нормальном режиме, <code>1–3 минуты</code> при региональной сетевой деградации. Eventually consistent модель данных — для поиска приемлемо, для цен/наличия (freshness-SLI) — узкое место, требующее отдельной стратегии (часто — fetch цены при клике в карточку из локального region-БД).</li>
<li><strong>Repair-aware кэш.</strong> Когда регион вышел из деградации и снова healthy, кэш в нём может содержать <code>«грязные»</code> ключи — данные, менявшиеся в других регионах, но не доехавшие. Решение: каждый ключ в кэше помечен суффиксом версии данных (<code>hash</code> или <code>LSN</code>); при возврате региона — проактивный прогрев топовых ключей со свежими версиями. Это отдельный сервис, обычно in-house.</li>
<li><strong>Graceful degradation.</strong> Если ranking-сервис упал — поиск отдаёт базовое ранжирование (popularity + recency) без персонализации. Если упал autocomplete — прячем подсказки, оставляя ввод. Если упал ES — отдаём кэш-результаты + предупреждение «возможно, не самые свежие». Каждый из этих режимов — отдельный код-path и отдельный SLI.</li>
<li><strong>Failover.</strong> Автоматический на уровне geo-anycast / global LB, RTO <code>5–30 секунд</code> на регион. Health-чеки агрегируют SLI критичного пути, а не пинг сервера.</li>
<li><strong>RPO.</strong> Околонулевой для критичных данных (заказы, корзина) через synchronous replication между двумя регионами + async на третий. Для каталога/индекса — <code>до 1 минуты</code>.</li>
<li><strong>Алерты.</strong> Multi-window multi-burn-rate с поджатыми окнами: быстрая пара <code>2 мин + 1 час</code> с threshold <code>36×</code> (для tier-1 порог берут агрессивнее, чем 14.4× — иначе быстрый алерт догонит инцидент уже после исчерпания бюджета) + медленная пара <code>15 мин + 3 часа</code> с threshold <code>~10×</code> для затяжной деградации. Auto-remediation для известных классов: автоматический drain региона при превышении burn-rate, автоматический rollback деплоя.</li>
</ul>
<p><strong>Что добавилось по сравнению с 99.9%:</strong></p>
<ul>
<li>Multi-region active-active — потому что 1 минута 27 секунд бюджета на пике не покрывают ни один региональный инцидент (4 мин 20 с в среднем — тоже).</li>
<li>Repair-aware кэш — потому что после возврата региона данные могут разойтись, и без проактивного прогрева накопится freshness-долг, съедающий SLI-4.</li>
<li>Graceful degradation — потому что без неё любой отказ зависимости (ranking, persona) забирает весь бюджет.</li>
<li>Auto-remediation — потому что человек физически не успевает посмотреть пейдж за 1 мин 27 с пикового бюджета, не то что разобраться.</li>
<li>Канареечный rollout с автоматическим откатом по burn-rate — каждый деплой иначе ставит весь бюджет.</li>
</ul>
<p><strong>Что намеренно НЕ делаем (и здесь это особенно важно):</strong></p>
<ul>
<li>Не идём в <code>99.999%</code> без очень сильного бизнес-обоснования. 26 секунд в месяц требуют существенно более сложной модели резервирования, чем 99.99%: multi-cloud или multi-provider, независимые DNS, частично независимые кодовые базы, подходы из telecom/aerospace. Это часто <strong>дешевле решить через UX</strong> — например, кэшированием выдачи на клиенте на минуту, чтобы из 100% даунтайма пользователь увидел только новые сессии.</li>
</ul>
<p><strong>Стоимость:</strong> <strong>15–25× baseline</strong> в инфраструктуре, <strong>8–10× baseline</strong> в людях (24/7 SRE, регулярные game days, отдельная команда reliability engineering).</p>
<h3 id="сводная-таблица-slo-как-бюджет-на-сложность">Сводная таблица: SLO как бюджет на сложность</h3>
<table>
	<thead>
			<tr>
					<th>Аспект</th>
					<th>99.0%</th>
					<th>99.9%</th>
					<th>99.99%</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Бюджет на пике (30 дней)</td>
					<td>~2 ч 24 мин</td>
					<td>~14 мин 26 с</td>
					<td>~1 мин 27 с</td>
			</tr>
			<tr>
					<td>Бюджет при средней нагрузке (30 дней)</td>
					<td>~7 ч 12 мин</td>
					<td>~43 мин</td>
					<td>~4 мин 20 с</td>
			</tr>
			<tr>
					<td>Бюджет в запросах (30 дней)</td>
					<td>~260M</td>
					<td>~26M</td>
					<td>~2.6M</td>
			</tr>
			<tr>
					<td>Регионы</td>
					<td>1</td>
					<td>1 (active-passive AZ)</td>
					<td>3+ (active-active)</td>
			</tr>
			<tr>
					<td>Поисковый индекс</td>
					<td><code>pg_trgm</code> в Postgres</td>
					<td>Elasticsearch (8–12 нод)</td>
					<td>ES per region</td>
			</tr>
			<tr>
					<td>Кэш</td>
					<td>In-memory LRU на app</td>
					<td>Redis cluster (~50 GB)</td>
					<td>Repair-aware Redis × N</td>
			</tr>
			<tr>
					<td>Индексирование</td>
					<td>Триггеры в БД</td>
					<td>CDC + Kafka</td>
					<td>Cross-region MirrorMaker</td>
			</tr>
			<tr>
					<td>Failover</td>
					<td>Ручной, RTO 30–60 мин</td>
					<td>Авто между AZ, RTO 1–3 мин</td>
					<td>Авто между регионами, RTO 5–30 с</td>
			</tr>
			<tr>
					<td>RPO</td>
					<td>~5 мин</td>
					<td>~30 с</td>
					<td>~0 для tier-1 / ~1 мин для каталога</td>
			</tr>
			<tr>
					<td>Алерты</td>
					<td>На симптомы</td>
					<td>Multi-window burn-rate</td>
					<td>+ Auto-remediation</td>
			</tr>
			<tr>
					<td>Деплои</td>
					<td>Без особых процедур</td>
					<td>Постепенный rollout</td>
					<td>Канарейка + auto-откат</td>
			</tr>
			<tr>
					<td>Graceful degradation</td>
					<td>Нет</td>
					<td>Частично</td>
					<td>Полная</td>
			</tr>
			<tr>
					<td><strong>Инфраструктура (× baseline)</strong></td>
					<td><strong>1×</strong></td>
					<td><strong>4–6×</strong></td>
					<td><strong>15–25×</strong></td>
			</tr>
			<tr>
					<td><strong>Люди (× baseline)</strong></td>
					<td><strong>1×</strong></td>
					<td><strong>2–3×</strong></td>
					<td><strong>8–10×</strong></td>
			</tr>
			<tr>
					<td>Когда оправдано</td>
					<td>Внутренние tools, MVP, не-критичные пути</td>
					<td>Production e-commerce средней руки</td>
					<td>Tier-1 в крупном бизнесе</td>
			</tr>
	</tbody>
</table>
<h3 id="главный-вывод-раздела">Главный вывод раздела</h3>
<p>Один и тот же кейс на трёх SLO — это <strong>три разные инженерные задачи</strong>. Не «та же задача с разными настройками», а с разной топологией, разными технологиями, разной операционной моделью, разной стоимостью.</p>
<p>Из этого следуют две вещи, которые часто упускают:</p>
<ol>
<li><strong>Поднять SLO задним числом — это переписать систему</strong>, а не «добавить мониторинга». Если архитектура построена под 99.0%, поднять её до 99.99% означает: добавить регионы, переписать индексирование под cross-region, ввести graceful degradation, перепроектировать кэш. Не «настройка пары флагов» — <strong>рефакторинг порядка половины кодовой базы</strong>.</li>
<li><strong>Снизить SLO задним числом</strong> освобождает бюджет, но <strong>не возвращает потраченных денег</strong>. Если ты построил под 99.99%, а реально нужно 99.9% — лишний регион уже куплен, репликация уже настроена, команда SRE уже нанята. Снижение SLO в этом случае имеет смысл только перед следующим циклом капекса.</li>
</ol>
<p>Поэтому SLO выбирается <strong>до</strong> дизайна, а не после. И выбирается <strong>с участием бизнеса</strong> — потому что цифра в нём в итоге привязана к деньгам, а не к самочувствию инженеров.</p>
<h2 id="sla-что-уходит-наружу">SLA: что уходит наружу</h2>
<p>Третий и последний термин «троицы» — <strong>SLA (Service Level Agreement)</strong>. Это контракт с пользователем (или клиентом, партнёром, регулятором), оформленный обычно юридически, с финансовыми санкциями за нарушение.</p>
<p>Принципиальное отличие от SLO:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>SLO</th>
					<th>SLA</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Кому обещаем</td>
					<td>Самим себе (внутри команды/компании)</td>
					<td>Внешнему пользователю или клиенту</td>
			</tr>
			<tr>
					<td>Последствия нарушения</td>
					<td>Перераспределение бюджета на reliability, freeze</td>
					<td>Финансовые санкции, отток, репутация</td>
			</tr>
			<tr>
					<td>Гранулярность</td>
					<td>Может быть сколько угодно (по шагу, по сегменту)</td>
					<td>Обычно одно-два простых числа</td>
			</tr>
			<tr>
					<td>Частота пересмотра</td>
					<td>Раз в квартал</td>
					<td>Раз в год или реже</td>
			</tr>
	</tbody>
</table>
<h3 id="главное-правило-sla--slo--запас">Главное правило: SLA ≤ SLO − запас</h3>
<p>Самая частая ошибка — поставить SLA равным SLO. Например, инженеры обещают себе 99.99%, и юристы пишут пользователю «гарантируем 99.99%». В этом случае любое попадание в нижнюю границу SLO — уже автоматически нарушение SLA.</p>
<p>Правильно: между SLO и SLA должен быть <strong>запас</strong>, обычно один-два разряда <code>9</code>.</p>
<ul>
<li>SLO 99.99% → SLA 99.9% или 99.95%.</li>
<li>SLO 99.9% → SLA 99.5%.</li>
</ul>
<p>Запас не стоит ничего — это просто разница между «что мы хотим внутри» и «что мы готовы юридически защищать снаружи».</p>
<h3 id="и-ещё-одно-важное">И ещё одно важное</h3>
<p>SLA пишется не на ту же метрику, что SLO. SLA обычно — это <strong>availability в чистом виде</strong> (грубое «отвечает / не отвечает»). SLO — набор тонких индикаторов (latency, freshness, correctness). Ты внутри живёшь на сложной картине из 4–8 SLI, наружу выдаёшь одну простую цифру с запасом.</p>
<h2 id="golden-signals-red-use--какую-призму-брать-и-когда">Golden Signals, RED, USE — какую призму брать и когда</h2>
<p>Кейс выше показал три разные системы под три SLO. Прежде чем переходить к разговору про error budget как арбитр в спорах, важно понять — а каким набором метрик мы вообще смотрим на эти системы. В SRE-сообществе живут три популярных набора-призмы для метрик:</p>
<ul>
<li><strong>Golden Signals</strong> — <code>Latency, Traffic, Errors, Saturation</code>. Универсальный набор для пользовательских сервисов. Введён Rob Ewaschuk и закреплён в Google SRE Book (глава «Monitoring Distributed Systems», O&rsquo;Reilly, 2016).</li>
<li><strong>RED</strong> — <code>Rate, Errors, Duration</code>. Подмножество Golden Signals, ориентированное на запрос-ответные сервисы. Введён Tom Wilkie (Weaveworks, ныне Grafana Labs) в докладе «The RED Method» (2017).</li>
<li><strong>USE</strong> — <code>Utilization, Saturation, Errors</code>. Для ресурсов: CPU, диск, память, сеть. Введён Brendan Gregg в «Systems Performance» (2-е изд., 2020) и серии постов на brendangregg.com (2012+).</li>
</ul>
<p>Эти призмы часто валят в одну кучу и спорят, какая «правильная». Спор бессмысленный — они отвечают на <strong>разные вопросы</strong>.</p>
<h3 id="golden-signals--самая-полная-призма-для-сервиса">Golden Signals — самая полная призма для сервиса</h3>
<p>Если ты только начинаешь и не знаешь, что меряет — начни с Golden Signals на каждый твой сервис:</p>
<ul>
<li><strong>Latency.</strong> Сколько времени занял успешный запрос (отдельно — неуспешный, потому что медленный 500 искажает картину).</li>
<li><strong>Traffic.</strong> Сколько запросов обрабатываем (RPS, qps, events/sec).</li>
<li><strong>Errors.</strong> Доля «плохих» — 5xx, таймауты, протокольные ошибки, неуспешные заказы.</li>
<li><strong>Saturation.</strong> Насколько ресурс заполнен — очередь, пул соединений, утилизация worker-ов.</li>
</ul>
<p>В нашем кейсе для search-сервиса Golden Signals разворачиваются так:</p>
<ul>
<li><strong>Latency:</strong> P50/P95/P99 на эндпоинт <code>/search</code>, разбитые по типу запроса (autocomplete vs full search).</li>
<li><strong>Traffic:</strong> RPS на <code>/search</code>, отдельно по регионам.</li>
<li><strong>Errors:</strong> доля 5xx + таймаутов к ES + ошибок ranking-сервиса.</li>
<li><strong>Saturation:</strong> размер очереди в Kafka-consumer индексирования, пул коннектов в ES, hit rate Redis.</li>
</ul>
<p>Эти четыре сразу дают <strong>верхнеуровневую картину здоровья сервиса</strong>. SLI ты выбираешь из них (обычно — Latency и Errors), но мониторить нужно все четыре.</p>
<h3 id="red--тот-же-набор-проще-запомнить">RED — тот же набор, проще запомнить</h3>
<p>RED (<code>Rate, Errors, Duration</code>) — это Golden Signals минус <code>Saturation</code>. Удобный мнемоник, особенно для request/response-сервисов: каждый эндпоинт обязательно имеет три метрики и хотя бы один дашборд по ним.</p>
<p>Когда брать RED вместо Golden Signals: если ты делаешь дашборд per-endpoint в большом сервисе и не хочешь ставить четыре графика на каждый эндпоинт. Saturation — ресурсная метрика, и она часто живёт отдельно (на дашборде ресурсов или инфраструктуры), а не на дашборде эндпоинтов.</p>
<h3 id="use--призма-для-ресурсов-не-для-сервисов">USE — призма для ресурсов, не для сервисов</h3>
<p>USE (<code>Utilization, Saturation, Errors</code>) — про <strong>ресурс</strong>, не про сервис. Когда мерить:</p>
<ul>
<li><strong>CPU</strong> — utilization = % busy, saturation = run queue, errors = throttling.</li>
<li><strong>Диск</strong> — utilization = busy time, saturation = await, errors = bad sectors / IO errors.</li>
<li><strong>Сеть</strong> — utilization = bandwidth %, saturation = drops, errors = retransmits.</li>
<li><strong>Пул соединений</strong> — utilization = used/max, saturation = очередь ожидания, errors = таймауты.</li>
</ul>
<p>USE — для <strong>диагностики</strong>, не для SLO. Никогда не делай SLO на CPU utilization напрямую — это служебный сигнал. Но USE-метрики бесценны, когда burn-rate-алерт сработал и нужно быстро понять, <strong>где</strong> в стеке давит.</p>
<h3 id="когда-какую-призму-брать--простое-правило">Когда какую призму брать — простое правило</h3>
<table>
	<thead>
			<tr>
					<th>Ситуация</th>
					<th>Призма</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Дашборд верхнего уровня по сервису</td>
					<td>Golden Signals</td>
			</tr>
			<tr>
					<td>Дашборд per-endpoint в большом сервисе</td>
					<td>RED</td>
			</tr>
			<tr>
					<td>Дашборд ресурса (CPU, диск, БД-пул)</td>
					<td>USE</td>
			</tr>
			<tr>
					<td>Выбор кандидатов в SLI</td>
					<td>Golden Signals → Latency и Errors</td>
			</tr>
			<tr>
					<td>Диагностика после burn-rate-алерта</td>
					<td>USE — где жмёт по ресурсам</td>
			</tr>
	</tbody>
</table>
<h3 id="главное-смещение">Главное смещение</h3>
<p>Призмы не противоречат — они <strong>дополняют</strong>. У зрелой команды есть все три уровня дашбордов:</p>
<ul>
<li><strong>Верхний</strong> — Golden Signals + дашборд SLI/SLO с burn-rate. Заходишь и сразу видишь, всё ли в порядке с пользовательским опытом.</li>
<li><strong>Средний</strong> — RED по эндпоинтам. Заходишь, когда верхний показывает деградацию, и видишь, какой эндпоинт вносит вклад.</li>
<li><strong>Нижний</strong> — USE по ресурсам. Заходишь, когда RED показывает, какой эндпоинт болит, и нужно понять, в каком ресурсе упёрлись.</li>
</ul>
<h2 id="error-budget-как-арбитр-в-спорах-фича-vs-рефакторинг">Error budget как арбитр в спорах «фича vs рефакторинг»</h2>
<p>Самый недооценённый эффект от грамотного SLO — это не алерты и не отчёты. Это то, что бюджет ошибок становится <strong>валютой в архитектурных спорах</strong>, и решения принимаются на цифрах, а не на ощущениях.</p>
<h3 id="классический-спор-без-бюджета">Классический спор без бюджета</h3>
<p>Сценарий, знакомый каждому, кто работал в продуктовой команде дольше года:</p>
<blockquote><p><strong>Продакт:</strong> «Нам нужно срочно выкатить персонализированную выдачу до Чёрной пятницы».<br>
<strong>SRE:</strong> «Мы только что прошли через два инцидента из-за деплоя. Я хочу заморозить деплои на неделю и провести post-mortem».<br>
<strong>Продакт:</strong> «Каждая неделя задержки — это $500k недополученной выручки».<br>
<strong>SRE:</strong> «Каждый инцидент — это репутация».</p>
</blockquote><p>Без бюджета этот спор решается <strong>властью</strong>. Кто громче, выше по иерархии или ближе к CEO — тот побеждает. Решения принимаются эмоционально, обе стороны выходят с ощущением, что их аргументы недослушаны.</p>
<h3 id="тот-же-спор-с-бюджетом">Тот же спор с бюджетом</h3>
<blockquote><p><strong>Продакт:</strong> «Хочу выкатить персонализацию до BF».<br>
<strong>SRE:</strong> «Бюджет на этот месяц — 26M запросов. За последние две недели мы сожгли 22M на двух инцидентах. У нас осталось 4M на оставшиеся 16 дней. Вот burn-rate-график за неделю».<br>
<strong>Продакт:</strong> «Сколько бюджета съедает выкатка персонализации?»<br>
<strong>SRE:</strong> «По прошлым выкаткам сопоставимого риска — медианно 3–4M запросов на постепенный rollout. То есть канарейка съест от 75% до 100% оставшегося бюджета. Если что-то пойдёт не так — превышаем SLO до конца окна».<br>
<strong>Продакт:</strong> «Сколько ждать, пока бюджет восстановится?»<br>
<strong>SRE:</strong> «Окно rolling, считается на последних 30 днях — каждые сутки самые старые сутки выпадают, новые входят. Самые тяжёлые инциденты были 28 и 25 дней назад: через 2–5 дней они выпадут из окна, и мы откатимся к ~10M свободного бюджета без всяких новых усилий».<br>
<strong>Продакт:</strong> «До BF — 24 дня. Подождём 5 дней, потом выкатываем канарейку — рискуем уже на восстановленном бюджете, а не на остатках от инцидентов».</p>
</blockquote><p>Спор кончился за 5 минут, без эмоций, на цифрах. Это и есть «бюджет как валюта»: он переводит инженерное «надо подождать» и продуктовое «надо быстрее» в <strong>единицу измерения, значимую обеим сторонам</strong>.</p>
<p>Это сводится к трём операционным следствиям, которые я уже разбирал в Шаге 4 как «на что тратится бюджет»: решение «деплоить vs морозить», переключение на reliability при систематическом сгорании, осознанная трата бюджета на учения и эксперименты. Здесь добавляется только одно: каждое из этих следствий теперь становится <strong>цифровым аргументом в диалоге</strong>, а не «инженерным интуитивным решением».</p>
<h3 id="что-разрушает-бюджет-как-арбитр">Что разрушает «бюджет как арбитр»</h3>
<ul>
<li><strong>Несогласованный SLO.</strong> Если бюджет в глазах продакта — «инженерная штука», он не примет его как аргумент. SLO нужно <strong>согласовывать</strong> с продуктом, а не объявлять.</li>
<li><strong>SLO без явного оперативного следствия.</strong> Если «бюджет сгорел» не запускает freeze автоматически, это просто график на дашборде. Должна быть <strong>записанная политика</strong>, что делать.</li>
<li><strong>Слишком жёсткое SLO.</strong> Если SLO нереалистичный («99.99% на сервисе, который объективно может только 99.9%»), бюджет всё время в минусе, и команда привыкает его игнорировать. Лучше реалистичный SLO с дисциплиной, чем красивая цифра, в которую никто не верит.</li>
</ul>
<p>Бюджет — это не магический инструмент, а <strong>дисциплина</strong>. Он работает, если команда договорилась его соблюдать. Не работает — если нет.</p>
<h2 id="типичные-ошибки-выбора-slislo-и-чек-лист">Типичные ошибки выбора SLI/SLO и чек-лист</h2>
<p>Соберу в один блок самые частые ошибки, которые ломают SLO-практику. Часть из них уже разобрана в основных разделах — в этом блоке они сведены в одну строку для самопроверки. Развёрнутые контрпримеры — в соответствующих главах выше.</p>
<h3 id="ошибка-1-sli--всё-что-не-5xx">Ошибка 1. SLI = «всё, что не 5xx»</h3>
<p><strong>Правильно:</strong> SLI — это <code>success AND fast</code>, и оба критерия записаны явно. Полный разбор — в Шаге 3 (про «хорошее событие»).</p>
<h3 id="ошибка-2-slo-по-системе-вместо-slo-по-шагу-пути">Ошибка 2. SLO «по системе» вместо SLO «по шагу пути»</h3>
<p><strong>Правильно:</strong> SLO живёт на каждом шаге критичного пути, а не на сервисе в целом. Развёрнуто — в Шаге 1 (ловушка «среднего по системе»).</p>
<h3 id="ошибка-3-slo-без-error-budget-policy">Ошибка 3. SLO без error budget policy</h3>
<p>SLO написано, бюджет считается, но что делать, когда бюджет сгорел — не записано. В итоге команда живёт в режиме «дашборд красный, ну ладно, продолжаем выкатку».</p>
<p><strong>Правильно:</strong> записанная политика: при превышении burn-rate <code>X×</code> за окно <code>Y</code> минут — freeze деплоев до конца окна / привлечение reliability-команды / эскалация. Без процедуры SLO — это украшение. Развёрнуто — в разделе «Что разрушает „бюджет как арбитр“».</p>
<h3 id="ошибка-4-slo--sla">Ошибка 4. SLO = SLA</h3>
<p><strong>Правильно:</strong> между SLO и SLA — запас один-два разряда <code>9</code>. Подробнее — в разделе «SLA: что уходит наружу».</p>
<h3 id="ошибка-5-среднее-вместо-перцентилей">Ошибка 5. Среднее вместо перцентилей</h3>
<p><strong>Правильно:</strong> SLI на latency — это перцентиль (P95/P99), не среднее. Развёрнуто — в Шаге 3, блок «Что мы НЕ берём как SLI».</p>
<h3 id="ошибка-6-sli-на-cpumemory-utilization">Ошибка 6. SLI на CPU/Memory utilization</h3>
<p><strong>Правильно:</strong> SLI — на пользовательских следствиях (latency, errors). Утилизация — служебная метрика для USE-дашборда. Развёрнуто — в Шаге 3, блок «Что мы НЕ берём как SLI».</p>
<h3 id="ошибка-7-слишком-много-sli">Ошибка 7. Слишком много SLI</h3>
<p>«У нас 50 SLI на сервис» — означает, что SLI никто не читает, и алерты по ним — шум. SLI должен фокусировать внимание, а не размывать его.</p>
<p>Маленькое терминологическое уточнение, без которого пункт читается двусмысленно: <strong>«сервис» здесь — пользовательский сервис в смысле CUJ</strong>, а не каждый микросервис в твоей архитектуре. На один CUJ (поиск каталога) — 4–8 SLI; на ranking-микросервис, который обслуживает этот же CUJ, не нужно отдельно вешать ещё 4–8. Иначе «50 SLI» получаются арифметически — суммой по микросервисам, — и читать их никто не будет.</p>
<p><strong>Правильно:</strong> 4–8 SLI на пользовательский сервис (CUJ), привязанных к ключевым шагам пути. Если хочется больше — это, скорее всего, сигнал, что внутри одного «сервиса» спрятаны два CUJ, и его пора разделить. Развёрнуто — в Шаге 3, блок «Конкретные SLI для нашего кейса».</p>
<h3 id="ошибка-8-slo-выбран-копированием">Ошибка 8. SLO выбран копированием</h3>
<p>«У Google 99.99%, у нас тоже будет». Без разговора с бизнесом, без расчёта стоимости, без понимания, тянет ли архитектура.</p>
<p><strong>Правильно:</strong> SLO выбирается из двух чисел — что <strong>окупается</strong> (бизнес) и что <strong>достижимо</strong> (архитектура). Между ними может быть зазор — он отражает либо недоинвест в reliability, либо переинвест в обещания.</p>
<h3 id="ошибка-9-slo-не-пересматривается">Ошибка 9. SLO не пересматривается</h3>
<p>Поставили SLO 99.9% год назад, написали в Confluence, забыли. Бизнес-контекст изменился, нагрузка выросла в 10 раз, продукт перешёл в зрелую фазу — SLO остался прежним.</p>
<p><strong>Правильно:</strong> SLO пересматривается раз в квартал минимум, в idle-режиме — раз в полгода. Каждый пересмотр — короткий ритуал «изменилось ли что-то в бизнесе или в системе, что меняет нужное число».</p>
<h3 id="ошибка-10-slo-выбран-без-учёта-зависимостей">Ошибка 10. SLO выбран без учёта зависимостей</h3>
<p>Сервис обещает 99.99%, но критически зависит от внешнего сервиса с 99.5%. Математика: твой SLO не может быть выше произведения SLO твоих критичных зависимостей.</p>
<p><strong>Правильно:</strong> при выборе SLO учитывай <strong>dependency budget</strong>. Если у тебя 3 последовательные критичные зависимости с 99.9% каждая, твой достижимый максимум — <code>~99.7%</code>. Для опциональных или параллельных зависимостей формула мягче — но любую цепочку нужно явно проговорить.</p>
<h3 id="чек-лист-10-вопросов-до-утверждения-slo">Чек-лист: 10 вопросов до утверждения SLO</h3>
<p>Если ответ «нет» хотя бы на один — SLO ещё не готов.</p>
<ul>
<li><input disabled="" type="checkbox"> Привязан ли SLO к конкретному шагу критичного пути, а не к сервису целиком?</li>
<li><input disabled="" type="checkbox"> Прописаны ли числитель, знаменатель и окно агрегации?</li>
<li><input disabled="" type="checkbox"> Пройдён ли тест «5xx за 5 секунд = плохое событие»?</li>
<li><input disabled="" type="checkbox"> Используется ли перцентиль, а не среднее?</li>
<li><input disabled="" type="checkbox"> Есть ли error budget в абсолютных значениях для текущего объёма трафика?</li>
<li><input disabled="" type="checkbox"> Есть ли записанная error budget policy (что делать при сгорании)?</li>
<li><input disabled="" type="checkbox"> Есть ли запас между SLO и SLA (если SLA вообще обещаем)?</li>
<li><input disabled="" type="checkbox"> Согласованы ли числа с бизнесом (а не «команда придумала»)?</li>
<li><input disabled="" type="checkbox"> Учтены ли SLO критичных зависимостей в верхней границе нашего?</li>
<li><input disabled="" type="checkbox"> Назначена ли дата следующего пересмотра?</li>
</ul>
<h2 id="заключение-slo--это-мост-между-системой-и-деньгами">Заключение: SLO — это мост между системой и деньгами</h2>
<p>Я начинал статью с тезиса, что архитектура без SLO — это эстетика, не инженерия. К концу хочется его уточнить.</p>
<p>SLO — не просто <strong>constraint</strong> на архитектуру, хотя я почти всю статью акцентировал именно это. SLO — это <strong>общий язык</strong> между четырьмя ролями, которые иначе говорят на разных:</p>
<ul>
<li><strong>Продакт / аналитик</strong> говорит: конверсия, retention, GMV, NPS.</li>
<li><strong>Бэкенд</strong> говорит: P99, шарды, hit rate, репликация.</li>
<li><strong>SRE</strong> говорит: error budget, burn rate, MTTR, blast radius.</li>
<li><strong>Бизнес</strong> говорит: деньги, репутация, регуляторика.</li>
</ul>
<p>SLO — это формат, в котором все четверо могут договориться. «Поднять availability с 99.9% до 99.99% стоит 4× инфраструктуры и сэкономит N часов даунтайма в год, что на нашем GMV $X — это <code>Y</code> денег. Делаем или нет?». Этот вопрос имеет ответ, к которому пришли все четверо. Без SLO такого вопроса просто не существует — есть только предчувствия.</p>
<h3 id="что-сделать-в-понедельник">Что сделать в понедельник</h3>
<ol>
<li><strong>Возьми один пользовательский путь и разложи его на шаги.</strong> Не сервис целиком — один сценарий, шесть-семь шагов, как в нашем кейсе. На каждый шаг — кандидаты в SLI. Это упражнение делается за два часа и показывает, где ты измеряешь не то.</li>
<li><strong>Сравни свои текущие SLO с реальными зависимостями.</strong> Если твой SLO выше произведения SLO критичных зависимостей — ты обещаешь больше, чем можешь дать. Цифру лучше пересмотреть до того, как её пересмотрит пользователь.</li>
<li><strong>Напиши error budget policy на одну страницу.</strong> Что происходит при превышении burn-rate <code>X×</code> за <code>Y</code> минут? Кто принимает решение freeze? Без этого документа SLO остаётся графиком на дашборде, а не дисциплиной.</li>
</ol>
<p>Спасибо, что дочитал до конца. Эта статья — переработанная версия плана несостоявшегося доклада для <a href="https://t.me/system_design_world" rel="noopener noreferrer" target="_blank">System Design World</a>. Если у тебя есть свой опыт работы с SLO в системе высокой нагрузки — особенно интересны истории, как менялся выбор SLO с ростом продукта или после крупного инцидента — пиши в комменты или в мой Telegram-канал «<a href="https://t.me/&#43;gLeURJwFz5Q4NTA6" rel="noopener noreferrer" target="_blank">Мишка на сервере</a>».</p>
<p>Чертёж — это половина дела. Как именно выглядит тот самый диалог с бизнесом, без которого SLO остаётся картинкой на дашборде, я разбираю в продолжении: <a href="/posts/reliability-is-a-conversation/">«Надёжность строится в диалоге с бизнесом»</a>.</p>
<p>А если нужен не разбор одного кейса, а список того, что вокруг SLO вообще надо уметь и на каком уровне, — я собираю его в карте компетенций: <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/slo-engineering/" rel="noopener noreferrer" target="_blank">SLO Engineering</a> про инженерную часть и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/composite-slo-methodology/" rel="noopener noreferrer" target="_blank">Composite SLO Methodology</a> — про то самое произведение SLO зависимостей из второго пункта выше.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Содержимое моей персональной базы знаний</title>
      <link>https://jtprog.ru/posts/inside-my-obsidian/</link>
      <pubDate>Fri, 08 May 2026 14:52:21 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/inside-my-obsidian/</guid>
      <category>Obsidian</category>
      <category>Знания</category>
      <category>Personal Knowledge Management</category>
      <category>Note-Taking</category>
      <category domain="category">Opinions</category>
      <enclosure url="https://jtprog.ru/opinions.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/opinions.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Какое-то время назад мне прилетал запрос (в том числе и в чате): <em>«Можно ли глянуть на вашу базу знаний? Интересны какие там темы и что вы считаете важным, что это записываете?»</em>. Попробую ответить максимально развернуто и, возможно (но это не точно), что-то покажу.</p>
<blockquote><p><strong>Кратко:</strong></p>
<ul>
<li>База знаний <strong>персональная</strong> — полностью не покажу, но опишу многое.</li>
<li>Инструмент — Obsidian с плагинами Dataview, Templater, Omnisearch, Git.</li>
<li>Структура: упрощённая PARA — Проекты / Сферы / Ресурсы / Архив / Дневник.</li>
<li>Темы: DevSecOps/SRE, Computer Science, Психология, PKB, Личное, Дневник и ещё куча всего.</li>
<li>Главное правило: сомневаешься — записывай. Удалять не нужно — есть архив.</li>
<li>Самое важное: <strong>важнее писать заметки, чем не писать их вовсе</strong>.</li>
</ul>
</blockquote><h2 id="можно-ли-глянуть">Можно ли глянуть</h2>
<p>Начну с того, что <em>персональная база знаний</em> — она в первую очередь <strong>персональная</strong>, а потом уже база знаний. Это значит, что показывать я ее точно не планирую, как минимум пока я живой. По крайней мере в полном объеме я точно не смогу показать каждую заметку, однако какие-то ее части в том или ином виде я и так показываю. Например, посты для блогов рождаются из заметок в моей базе знаний.</p>
<p>Я спокойно могу <strong>описать</strong>: как именно она устроена, какие темы я фиксирую, а также как я решаю &ldquo;записать или не записать&rdquo; (спойлер: <strong>записать</strong>). Именно об этом и будет ниже — может, что-то из этого окажется полезным и тебе.</p>
<blockquote><p><strong>Дисклеймер</strong>: всё описанное — это мой личный опыт и мои решения. Не универсальный рецепт. Бери то, что резонирует, и адаптируй под себя.</p>
</blockquote><h3 id="что-именно-публично">Что именно публично</h3>
<p>Хотя саму базу я не открываю, часть артефактов из неё так или иначе попадает в публичное пространство:</p>
<ul>
<li><strong>Посты канала</strong> — практически все посты «Мишки на сервере» рождаются из черновиков в базе.</li>
<li><strong>Слайды и выступления</strong> — например, доклад для студентов РТУ МИРЭА о PKB (<a href="https://jtprogru.github.io/pkbd/" rel="noopener noreferrer" target="_blank">слайды</a>) целиком сделан из заметок.</li>
<li><strong><a href="https://jtprog.ru/posts/pkb-check-list/">Чек-лист для организации персональной базы знаний</a></strong> — публичный артефакт в блоге.</li>
<li><strong>Шаблоны</strong> — часть шаблонов я периодически описываю в постах.</li>
</ul>
<p>Что никогда не попадает публично: рабочие детали с конкретными системами и командами, личные записи про людей, дневниковые записи, заметки про инциденты.</p>
<hr>
<h2 id="начну-со-структуры">Начну со структуры</h2>
<p>Довольно часто те, кто начинает заниматься заметковедением, начинают ломаться именно тут — попытка полностью продумать идеальную структуру базы знаний ломается ещё до первой заметки. Я и сам страдал этим — у меня сейчас примерно третья или четвёртая версия структуры.</p>
<p>Если кратко о том, как она эволюционировала: сначала была плоская куча файлов по папкам «Работа», «Личное», «Разное» — быстро стало непонятно, куда класть новое. Потом попытался следовать PARA (Projects-Areas-Resources-Archives) строго — оказалось слишком жёстко и я тратил больше времени на классификацию, чем на сами заметки. Нынешняя структура — это упрощённая PARA под себя.</p>
<p>Менять структуру — это нормально и правильно. Твоя персональная база знаний живёт и развивается вместе с тобой. Ты изучаешь что-то или пробуешь новое, находишь этому применение — и вот уже стоит задача реорганизовать размещение заметок.</p>
<p>Вернусь к себе. На текущий момент, структура директорий, которая используется у меня — в первую очередь удовлетворяет именно меня. Вот так у меня выглядит верхний уровень:</p>
<ul>
<li><code>00. Входящие</code> — просто мой инбокс заметок — сюда попадают абсолютно все заметки, которые я фиксирую.</li>
<li><code>01. Проекты</code> — преимущественно личные проекты, но иногда и рабочие там тоже оказываются.</li>
<li><code>02. Сферы</code> — содержит различные сферы моей жизни.</li>
<li><code>03. Ресурсы</code> — сюда я складываю информацию о книгах, статья и видео, а также навигационные карты.</li>
<li><code>04. Архив</code> — название говорит само за себя, я удаляю заметки крайне редко и чаще всего переношу их в архив.</li>
<li><code>05. Дневник</code> — еще одно очевидное название, тут лежат ежедневные заметки.</li>
<li><code>_Система</code> — тут лежат преимущественно служебные штуки, такие как шаблоны, картинки, холсты и прочие непотребства, которые я храню отдельно от заметок.</li>
<li><code>README.md</code> — моя входная точка в базу знаний (дашборд), а имя выбрано потому что бэкапы идут в Git.</li>
<li><code>AGENTS.md</code> — универсальные инструкции для всяких агентов, которые я иногда запускаю (преимущественно Claude Code).</li>
</ul>
<p>Всё это живёт в Obsidian — на данный момент у меня там порядка ≈5400 заметок. Из ключевых плагинов использую: <strong>Dataview</strong> (динамические запросы по базе), <strong>Templater</strong> (создание заметок из шаблонов), <strong>Omnisearch</strong> (полнотекстовый поиск) и <strong>Git</strong> (автоматические бэкапы).</p>
<h3 id="ключевые-понятия-чтобы-не-было-путаницы">Ключевые понятия (чтобы не было путаницы)</h3>
<p>Я использую несколько терминов, которые у разных людей означают разное. Вот как они работают у меня:</p>
<ul>
<li><strong>Входящие</strong> — «мусоропровод», куда летит всё подряд. Необработанное, без структуры. Задача — не потерять мысль, обрабатывать сразу не нужно.</li>
<li><strong>Черновик</strong> — заметка в процессе написания. Живёт уже в тематической папке, не во входящих. Отличие: уже есть намерение сделать из неё что-то конкретное.</li>
<li><strong>Дневник</strong> — ежедневные заметки. Отдельная категория, не входящие и не ресурсы. Не предназначен для переработки в посты.</li>
<li><strong>Карта / Map-of-Content (MOC)</strong> — навигационная заметка, которая собирает ссылки по теме. Не содержит знаний сама по себе — только указатели. Думай об этом как об оглавлении книги.</li>
<li><strong>Полноценная заметка</strong> — заметка с заголовком, 1-3 ключевыми тезисами, ссылками на связанные заметки и заполненным frontmatter. Не обязательно длинная — часто это 3-5 предложений.</li>
</ul>
<h3 id="обработка-входящих">Обработка входящих</h3>
<p>После обработки заметки из входящих, она отправляется в одну из категорий:</p>
<ul>
<li>Становится полноценной заметкой.</li>
<li>Становится картой.</li>
</ul>
<blockquote><p><strong>Замечание</strong>: Заметки по статьям, видео и книгам сразу складываются в свои директории.</p>
</blockquote><h3 id="жизненный-цикл-заметки">Жизненный цикл заметки</h3>
<p>Вот как заметка проходит путь от мысли до архива:</p>
<ol>
<li><strong>Захват</strong> → любая мысль/идея/факт фиксируется во <code>00. Входящие</code> или в дневнике. Формат не важен — главное не потерять.</li>
<li><strong>Обработка</strong> → раз в несколько дней разбираю инбокс: определяю тип заметки, добавляю frontmatter, перемещаю в нужную папку.</li>
<li><strong>Размещение</strong> → заметка отправляется в <code>01-04</code>, получает структуру по шаблону.</li>
<li><strong>Связывание</strong> → расставляю ссылки на связанные заметки, обновляю MOC если нужно.</li>
<li><strong>Использование</strong> → заметка живёт, дополняется при необходимости.</li>
<li><strong>Архив</strong> → если тема устарела — переношу в <code>04. Архив</code>, не удаляю.</li>
</ol>
<p>Инбокс обрабатываю без жёсткого расписания — по ощущению «накопилось». Обычно это 15-30 минут, 2-3 раза в неделю.</p>
<h3 id="правила-именования">Правила именования</h3>
<p>Заметки называю по принципу «чтобы было понятно без открытия»:</p>
<ul>
<li>Язык названия = язык заметки (русский по умолчанию, английский для технических терминов).</li>
<li>Никаких дат в названии, кроме дневниковых заметок (<code>2024-03-15</code>).</li>
<li>Существительные или глагольные фразы: «Ведение дневника лучше чем ведение todo-листа», «Kubernetes networking», «Почему SRE ≠ DevOps».</li>
<li>Никаких префиксов вроде <code>MOC-</code>, <code>BOOK-</code>, <code>IDEA-</code> — для этого есть теги и frontmatter.</li>
</ul>
<h3 id="как-я-нахожу-нужное">Как я нахожу нужное</h3>
<p>Страх «положу и забуду» — один из самых частых аргументов против ведения базы знаний. Но у меня он не работает, и вот почему.</p>
<p>Во-первых, поиск. Штатный поиск Obsidian слабоват, но я использую плагин Omnisearch — он быстрый и ищет по всему тексту, достаточно вспомнить любое слово из заметки. Во-вторых, связи между заметками: когда пишешь новую заметку и ставишь ссылки на старые, граф знаний сам напоминает, что у тебя уже есть по теме. В-третьих, Dataview-запросы в MOC-картах автоматически собирают заметки — не надо помнить, где что лежит.</p>
<p>На практике я редко «ищу» заметку целенаправленно. Чаще всего нужное всплывает само при написании новой заметки или при просмотре дневника.</p>
<p>Конкретные примеры навигации:</p>
<ul>
<li><strong>Идеи для постов</strong> → открываю MOC «Медийность» с Dataview-запросом, который тянет все заметки с тегом <code>#content</code>. Весь бэклог в одном месте.</li>
<li><strong>Инциденты</strong> → у каждого инцидента шаблон с тегом <code>#sre</code>. Dataview в MOC по SRE собирает их все.</li>
<li><strong>Книга, которую читал год назад</strong> → ищу по названию через Omnisearch или иду в <code>03. Ресурсы / Книги</code>.</li>
</ul>
<h3 id="шаблоны">Шаблоны</h3>
<p>Шаблонов у меня довольно много, и они со временем меняются. Вот те, что использую регулярно:</p>
<ul>
<li><code>Ежегодная заметка</code> — обзор года, использую в конце года, чтобы просто порефлексировать и подвести личные итоги.</li>
<li><code>Ежедневная заметка</code> — одна из основных заметок, используется для ведения дневника.</li>
<li><code>Конспект по лекции</code> — я студент и у меня есть отдельный шаблон для лекций, которые я посещаю.</li>
<li><code>Обзор конференции</code> — обзорная заметка про конкретную конференцию, где собираются все посещенные доклады и новые люди.</li>
<li><code>Шаблон тезисов по докладу</code> — доклады, которые я посетил на конференции и которые добавляются в обзорную заметку.</li>
<li><code>Созвон или встреча</code> — говорящее название, использую в том числе и для работы.</li>
<li><code>Черновик поста</code> — черновик поста (универсальный) и этот пост также был создан из этого шаблона.</li>
<li><code>Шаблон идея</code> — самый важный шаблон, который используется для всех заметок, которые я пишу сам сохраняю во входящих.</li>
<li><code>Шаблон инцидента</code> — для инцидентов, в которых я участвую лично у меня есть отдельный шаблон, информацию из которого я потом выношу в Postmortem.</li>
<li><code>Шаблон карты</code> — навигационные заметки или Map-of-Content.</li>
<li><code>Шаблон тезисов по видео</code>, <code>Шаблон тезисов по книге</code>, <code>Шаблон тезисов по статье</code> — для видео, книги или статьи используется свой шаблон, который в бОльшей степени является навигационной заметкой.</li>
<li><code>Шаблон человека</code> — заметки о людях.</li>
</ul>
<hr>
<h2 id="теги-и-свойства">Теги и свойства</h2>
<p>Теги у меня — это дополнительный слой навигации поверх папочной структуры. Живут в frontmatter, никаких инлайн-тегов.</p>
<p>Основные теги, которые реально использую:</p>
<table>
	<thead>
			<tr>
					<th>Тег</th>
					<th>Когда ставлю</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>#content</code> / <code>#telegram</code></td>
					<td>Черновик или идея для поста/канала</td>
			</tr>
			<tr>
					<td><code>#sre</code> / <code>#devops</code> / <code>#kubernetes</code></td>
					<td>Технические заметки по профилю</td>
			</tr>
			<tr>
					<td><code>#concept</code></td>
					<td>Термин, модель, фреймворк</td>
			</tr>
			<tr>
					<td><code>#thought</code></td>
					<td>Личная атомарная мысль</td>
			</tr>
			<tr>
					<td><code>#book</code> / <code>#article</code> / <code>#video</code></td>
					<td>Тип источника</td>
			</tr>
			<tr>
					<td><code>#inbox/review</code></td>
					<td>Требует обработки — снимаю тег после разбора</td>
			</tr>
			<tr>
					<td><code>#journal/daily</code></td>
					<td>Ежедневная запись</td>
			</tr>
	</tbody>
</table>
<p>В frontmatter также регулярно использую:</p>
<ul>
<li><code>up</code> / <code>down</code> — для навигации вверх/вниз по иерархии тем</li>
<li><code>status</code> — <code>Todo</code> / <code>WIP</code> / <code>Done</code> для проектов и черновиков</li>
<li><code>confidence</code> — <code>low</code> / <code>medium</code> / <code>high</code> для концептов и тезисов</li>
<li><code>sources</code> — список источников, из которых пришла информация</li>
</ul>
<hr>
<h2 id="темы-и-тематики">Темы и тематики</h2>
<p>Опишу крупными мазками — когортами тем — что у меня лежит в базе.</p>
<h3 id="преимущественно-рабочее">Преимущественно рабочее</h3>
<ul>
<li>Профильные темы — Dev(Sec)Ops/SRE, Кибербез/Защита информации, Процессы Incident&amp;Problem Management, (S)SDLC, etc&hellip;</li>
<li>Углубление в темы или Deep Dive — Infrastructure as/from Code, K8s, CICD, Configuration Management, etc&hellip;</li>
<li>Computer Science — Программирование, Основы CS, Golang/Python, Алгоритмы, Структуры данных, Парадигмы, etc&hellip;</li>
<li>Методологии — DevOps, SRE, Agile, Scrum, Kanban, ITSM, ITIL, TBM, etc&hellip;</li>
<li>Рабочее — Компании, Команды, Проекты, Встречи, Онбординги, etc&hellip;</li>
<li>Менеджмент — Управление людьми/командами/компаниями, Тимлидство и Техлидство.</li>
<li>Блог — Темы постов и готовые посты для канала и блога.</li>
</ul>
<p>Чтобы дать ощущение масштаба — вот несколько реальных названий заметок из рабочего блока (без содержания):</p>
<ul>
<li>«Чем SRE отличается от DevOps (и почему это важно)»</li>
<li>«SLO, SLA, SLI — разбор на пальцах»</li>
<li>«Почему постмортемы не работают без blame-free культуры»</li>
<li>«Kubernetes: как работает network policy»</li>
<li>«Разница между горизонтальным и вертикальным масштабированием»</li>
</ul>
<h3 id="я-мышление-и-работа-над-собой">Я, Мышление и работа над собой</h3>
<ul>
<li>Личная база знаний — Устройство, Воркфлоу, Второй мозг, etc&hellip;</li>
<li>Саморазвитие и Образование — Курсы, Книги, Хард- и Софт-скиллы, Области роста, Конспекты, etc&hellip;</li>
<li>Психология — Личная, Командная, Коммуникация, etc&hellip;</li>
<li>Мысли и принципы жизни — Разрозненные умозаключения и идеи, Проектирование себя, etc&hellip;</li>
<li>Личное — Семья, Финансы, Здоровье, Люди, etc&hellip;</li>
</ul>
<p>Примеры названий из этого блока:</p>
<ul>
<li>«Ведение дневника лучше чем ведение todo-листа»</li>
<li>«Как я принимаю сложные решения»</li>
<li>«Почему я перестал делать списки задач на день»</li>
<li>«Привязанность vs зависимость в отношениях»</li>
</ul>
<h3 id="разное">Разное</h3>
<p>Тут прям все подряд без списка: Философия, Право, Социология, Публичные выступления, Работа мозга, Драматургия.</p>
<h3 id="дневник">Дневник</h3>
<p>Это прям отдельная тема, которую я считаю крайне важной — дневник и ежедневные заметки, в которых фиксируется абсолютно всё, как в личном блокноте.</p>
<p>Когда-то подсмотрел у Рустама Агамалиева подтверждение того, что у меня чисто интуитивно вызывало комфорт: ведение дневника лучше, чем ведение todo-листа. В свободной форме пишу то, что необходимо сделать или уже сделано. О том, как я веду свой дневник, можно прочитать у меня в отдельном <a href="https://t.me/jtprogru_channel/4265" rel="noopener noreferrer" target="_blank">посте</a>.</p>
<hr>
<h2 id="записывать-или-не-записывать">Записывать или не записывать</h2>
<p>Буквально, как в монологе Гамлета — &ldquo;Быть или не быть&hellip;&rdquo; очередной заметке. И тут, как и везде — <em>все максимально индивидуально</em>. Именно поэтому я просто попытаюсь описать, чем именно я руководствуюсь, когда создаю очередную заметку.</p>
<h3 id="пригодится-или-справочная-информация">Пригодится или Справочная информация</h3>
<p>Заметка, как какой-то справочный материал, который мне может пригодиться в будущем. Для разных &ldquo;штук&rdquo;, которые бывает дольше гуглить и вспоминать, поэтому проще сохранить в виде заметки:</p>
<ul>
<li>разделение настроек для консольного git-клиента</li>
<li>возвращение режима модема на айфонах</li>
<li>расположение файлов библиотеки приложения Apple Books</li>
</ul>
<p>Проще говоря, это такие заметки, которые я первым делом пойду гуглить — какие-то инструкции, которые мне бывают не часто нужны и которые я не могу должным образом автоматизировать.</p>
<p>В эту же категорию относятся и заметки о каких-то людях. Будь то научный деятель, блогер, философ или &ldquo;крутой чувак&rdquo; с которым я познакомился на конференции.</p>
<h3 id="очередная-гениальная-идея">Очередная гениальная идея</h3>
<p>Не важно, когда она ко мне пришла в голову — в процессе чтения книги/статьи/видоса, либо во время выполнения рабочей задачи или даже во время созвона кем-то. Я запишу эту мысль, чем бы она ни была — связана она с текущим контекстом или деятельностью или нет.</p>
<p>Чаще всего такие мысли фиксируются в ежедневной заметке, а оттуда в ближайшее время извлекаются в отдельную заметку и попадают во входящие на дальнейшую обработку.</p>
<p>Тут могут быть и идеи по личной или рабочей автоматизации, и какие-то околофилософские мыслезлоключения, и идеи доработок чего-либо (особенно рабочее или в пет-проектах). Проще говоря — что-то, что не хочется забыть.</p>
<h3 id="рефлексия">Рефлексия</h3>
<p>Кто-то в рамках ежедневных заметок ведет полноценное планирование и управление проектами, а также через свойства отмечает сколько часов поспал и количество выпитой воды с настроением. Но это точно не я — мне такое пока не очень интересно отслеживать в своей базе знаний.</p>
<p>Я уже не однократно упоминал то, что я веду дневник и веду я его с 2021 года (самая первая дата — 2021-08-05). И вот именно дневник мне начал приносить ощутимую пользу — дневник позволяет ковыряться в собственных мозгах так, словно ты наблюдаешь со стороны.</p>
<p>Была куча разных приложений перепробована, но все они довольно быстро надоедают. А потребность в том, чтобы научиться разбираться в себе — была всегда и будет со мной всегда. Даже в работе с психологами дневник очень хорошо помогает мне.</p>
<h3 id="конспекты-и-обучение">Конспекты и обучение</h3>
<p>Книга, видео, лекция, статья — всё, из чего я что-то усвоил или хочу усвоить, попадает в базу. Не пересказ, а именно то, что меня зацепило: конкретная мысль, тезис, пример, который хочу вернуть потом.</p>
<p>Для каждого типа источника у меня отдельный шаблон (книга / видео / статья / доклад), потому что у них разная структура и разный контекст. Главное правило тут — <strong>не стараться законспектировать всё</strong>. Записываю только то, что хочу переосмыслить или применить.</p>
<h3 id="что-я-намеренно-не-записываю">Что я намеренно НЕ записываю</h3>
<p>Несколько категорий, которые осознанно остаются вне базы знаний:</p>
<ul>
<li><strong>Пароли и секреты</strong> — для этого есть парольный менеджер. В базу знаний принципиально не попадают.</li>
<li><strong>Персональные данные других людей</strong> — контакты храню минимально, без чувствительных деталей. Заметка о человеке — это профессиональный контекст, не личное досье.</li>
<li><strong>Рабочие секреты</strong> — архитектура клиентских систем, внутренние имена, конфигурации. Если что-то рабочее попадает в заметку — сразу обезличивается.</li>
<li><strong>Эмоции в моменте без контекста</strong> — «я злой», «всё плохо» без объяснения почему. Дневник — место для рефлексии, а не для фиксации сырого состояния без смысла.</li>
<li><strong>Задачи и todo-листы</strong> — для задач есть трекер. База знаний — не таск-менеджер.</li>
</ul>
<hr>
<h2 id="приватность-и-безопасность">Приватность и безопасность</h2>
<p>Раз уж я сказал «не покажу пока живой» — логично объяснить, как именно сделано так, чтобы случайно не утекло.</p>
<ul>
<li><strong>Шифрование диска</strong> — база хранится локально, диск зашифрован.</li>
<li><strong>iCloud</strong> — синхронизация между устройствами. Не идеально с точки зрения параноика, но для меня приемлемо.</li>
<li><strong>Git-бэкап</strong> — автоматический бэкап в приватный репозиторий. В <code>.gitignore</code> исключены чувствительные файлы.</li>
<li><strong>Нет публичных ссылок</strong> — никакого Obsidian Publish или аналогов. Если что-то публикую — делаю это руками, отдельным постом.</li>
</ul>
<h2 id="пересмотр-и-актуальность">Пересмотр и актуальность</h2>
<p>База знаний — это не музей. Заметки устаревают, взгляды меняются, какие-то темы перестают быть актуальными.</p>
<p>Как я с этим работаю:</p>
<ul>
<li><strong>Periodic review — по факту, не по расписанию</strong>. Нет жёсткого «раз в неделю смотрю базу». Пересматриваю заметки, когда возвращаюсь к теме по делу.</li>
<li><strong>Статус <code>WIP</code> как маркер</strong>. Если заметка давно в <code>WIP</code> и к ней нет желания возвращаться — это сигнал либо закрыть (→ <code>Done</code>), либо заархивировать.</li>
<li><strong>Дневник как встроенный review</strong>. Ежегодные и ежемесячные заметки — там я естественным образом возвращаюсь к тому, что было важно.</li>
<li><strong>Архив вместо удаления</strong>. Почти никогда не удаляю заметки. Если информация устарела — переношу в <code>04. Архив</code>. Потом можно найти и посмотреть, как думал раньше.</li>
</ul>
<hr>
<h2 id="итоги-и-полезности">Итоги и полезности</h2>
<p>У меня есть в блоге <a href="https://jtprog.ru/posts/pkb-check-list/">Чек-лист для организации персональной базы знаний</a> — если хочешь начать, это хорошая отправная точка. Также было небольшое выступление перед студентами РТУ МИРЭА про знакомство с личной базой знаний (<a href="https://jtprogru.github.io/pkbd/" rel="noopener noreferrer" target="_blank">слайды</a> есть, а видео нет).</p>
<p>Если вынести одну мысль из всего этого — <strong>важнее писать заметки, чем не писать их вовсе</strong>. Не нужна идеальная структура с первого дня. Не нужно читать все книги по PKB и Zettelkasten. Нужно просто начать фиксировать то, что кажется важным — и постепенно система сама начнёт складываться под тебя.</p>
<p><strong>Кому такой подход подойдёт:</strong></p>
<ul>
<li>Тем, кто работает со знаниями профессионально и хочет не забывать то, что узнаёт.</li>
<li>Тем, кто хочет думать более структурированно и видеть связи между идеями.</li>
<li>Тем, кому важно иметь «свой» инструмент, а не чужую систему.</li>
</ul>
<p><strong>Кому, скорее всего, не подойдёт:</strong></p>
<ul>
<li>Тем, кто хочет быстрый результат — первые месяцы база знаний больше потребляет времени, чем отдаёт.</li>
<li>Тем, кто не готов периодически разбирать инбокс — без обработки это просто свалка.</li>
</ul>
<p><strong>С чего начать новичку:</strong></p>
<ol>
<li>Выбери инструмент (Obsidian, Notion, Logseq — не важно, главное начать).</li>
<li>Заведи один шаблон для «идеи» — просто заголовок + 2-3 строки.</li>
<li>Первую неделю пиши всё подряд без структуры.</li>
<li>На второй неделе посмотри, что накопилось, и попробуй сгруппировать.</li>
<li>Структура появится сама — из того, что реально накапливается.</li>
</ol>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Какое-то время назад мне прилетал запрос (в том числе и в чате): <em>«Можно ли глянуть на вашу базу знаний? Интересны какие там темы и что вы считаете важным, что это записываете?»</em>. Попробую ответить максимально развернуто и, возможно (но это не точно), что-то покажу.</p>
<blockquote><p><strong>Кратко:</strong></p>
<ul>
<li>База знаний <strong>персональная</strong> — полностью не покажу, но опишу многое.</li>
<li>Инструмент — Obsidian с плагинами Dataview, Templater, Omnisearch, Git.</li>
<li>Структура: упрощённая PARA — Проекты / Сферы / Ресурсы / Архив / Дневник.</li>
<li>Темы: DevSecOps/SRE, Computer Science, Психология, PKB, Личное, Дневник и ещё куча всего.</li>
<li>Главное правило: сомневаешься — записывай. Удалять не нужно — есть архив.</li>
<li>Самое важное: <strong>важнее писать заметки, чем не писать их вовсе</strong>.</li>
</ul>
</blockquote><h2 id="можно-ли-глянуть">Можно ли глянуть</h2>
<p>Начну с того, что <em>персональная база знаний</em> — она в первую очередь <strong>персональная</strong>, а потом уже база знаний. Это значит, что показывать я ее точно не планирую, как минимум пока я живой. По крайней мере в полном объеме я точно не смогу показать каждую заметку, однако какие-то ее части в том или ином виде я и так показываю. Например, посты для блогов рождаются из заметок в моей базе знаний.</p>
<p>Я спокойно могу <strong>описать</strong>: как именно она устроена, какие темы я фиксирую, а также как я решаю &ldquo;записать или не записать&rdquo; (спойлер: <strong>записать</strong>). Именно об этом и будет ниже — может, что-то из этого окажется полезным и тебе.</p>
<blockquote><p><strong>Дисклеймер</strong>: всё описанное — это мой личный опыт и мои решения. Не универсальный рецепт. Бери то, что резонирует, и адаптируй под себя.</p>
</blockquote><h3 id="что-именно-публично">Что именно публично</h3>
<p>Хотя саму базу я не открываю, часть артефактов из неё так или иначе попадает в публичное пространство:</p>
<ul>
<li><strong>Посты канала</strong> — практически все посты «Мишки на сервере» рождаются из черновиков в базе.</li>
<li><strong>Слайды и выступления</strong> — например, доклад для студентов РТУ МИРЭА о PKB (<a href="https://jtprogru.github.io/pkbd/" rel="noopener noreferrer" target="_blank">слайды</a>) целиком сделан из заметок.</li>
<li><strong><a href="https://jtprog.ru/posts/pkb-check-list/">Чек-лист для организации персональной базы знаний</a></strong> — публичный артефакт в блоге.</li>
<li><strong>Шаблоны</strong> — часть шаблонов я периодически описываю в постах.</li>
</ul>
<p>Что никогда не попадает публично: рабочие детали с конкретными системами и командами, личные записи про людей, дневниковые записи, заметки про инциденты.</p>
<hr>
<h2 id="начну-со-структуры">Начну со структуры</h2>
<p>Довольно часто те, кто начинает заниматься заметковедением, начинают ломаться именно тут — попытка полностью продумать идеальную структуру базы знаний ломается ещё до первой заметки. Я и сам страдал этим — у меня сейчас примерно третья или четвёртая версия структуры.</p>
<p>Если кратко о том, как она эволюционировала: сначала была плоская куча файлов по папкам «Работа», «Личное», «Разное» — быстро стало непонятно, куда класть новое. Потом попытался следовать PARA (Projects-Areas-Resources-Archives) строго — оказалось слишком жёстко и я тратил больше времени на классификацию, чем на сами заметки. Нынешняя структура — это упрощённая PARA под себя.</p>
<p>Менять структуру — это нормально и правильно. Твоя персональная база знаний живёт и развивается вместе с тобой. Ты изучаешь что-то или пробуешь новое, находишь этому применение — и вот уже стоит задача реорганизовать размещение заметок.</p>
<p>Вернусь к себе. На текущий момент, структура директорий, которая используется у меня — в первую очередь удовлетворяет именно меня. Вот так у меня выглядит верхний уровень:</p>
<ul>
<li><code>00. Входящие</code> — просто мой инбокс заметок — сюда попадают абсолютно все заметки, которые я фиксирую.</li>
<li><code>01. Проекты</code> — преимущественно личные проекты, но иногда и рабочие там тоже оказываются.</li>
<li><code>02. Сферы</code> — содержит различные сферы моей жизни.</li>
<li><code>03. Ресурсы</code> — сюда я складываю информацию о книгах, статья и видео, а также навигационные карты.</li>
<li><code>04. Архив</code> — название говорит само за себя, я удаляю заметки крайне редко и чаще всего переношу их в архив.</li>
<li><code>05. Дневник</code> — еще одно очевидное название, тут лежат ежедневные заметки.</li>
<li><code>_Система</code> — тут лежат преимущественно служебные штуки, такие как шаблоны, картинки, холсты и прочие непотребства, которые я храню отдельно от заметок.</li>
<li><code>README.md</code> — моя входная точка в базу знаний (дашборд), а имя выбрано потому что бэкапы идут в Git.</li>
<li><code>AGENTS.md</code> — универсальные инструкции для всяких агентов, которые я иногда запускаю (преимущественно Claude Code).</li>
</ul>
<p>Всё это живёт в Obsidian — на данный момент у меня там порядка ≈5400 заметок. Из ключевых плагинов использую: <strong>Dataview</strong> (динамические запросы по базе), <strong>Templater</strong> (создание заметок из шаблонов), <strong>Omnisearch</strong> (полнотекстовый поиск) и <strong>Git</strong> (автоматические бэкапы).</p>
<h3 id="ключевые-понятия-чтобы-не-было-путаницы">Ключевые понятия (чтобы не было путаницы)</h3>
<p>Я использую несколько терминов, которые у разных людей означают разное. Вот как они работают у меня:</p>
<ul>
<li><strong>Входящие</strong> — «мусоропровод», куда летит всё подряд. Необработанное, без структуры. Задача — не потерять мысль, обрабатывать сразу не нужно.</li>
<li><strong>Черновик</strong> — заметка в процессе написания. Живёт уже в тематической папке, не во входящих. Отличие: уже есть намерение сделать из неё что-то конкретное.</li>
<li><strong>Дневник</strong> — ежедневные заметки. Отдельная категория, не входящие и не ресурсы. Не предназначен для переработки в посты.</li>
<li><strong>Карта / Map-of-Content (MOC)</strong> — навигационная заметка, которая собирает ссылки по теме. Не содержит знаний сама по себе — только указатели. Думай об этом как об оглавлении книги.</li>
<li><strong>Полноценная заметка</strong> — заметка с заголовком, 1-3 ключевыми тезисами, ссылками на связанные заметки и заполненным frontmatter. Не обязательно длинная — часто это 3-5 предложений.</li>
</ul>
<h3 id="обработка-входящих">Обработка входящих</h3>
<p>После обработки заметки из входящих, она отправляется в одну из категорий:</p>
<ul>
<li>Становится полноценной заметкой.</li>
<li>Становится картой.</li>
</ul>
<blockquote><p><strong>Замечание</strong>: Заметки по статьям, видео и книгам сразу складываются в свои директории.</p>
</blockquote><h3 id="жизненный-цикл-заметки">Жизненный цикл заметки</h3>
<p>Вот как заметка проходит путь от мысли до архива:</p>
<ol>
<li><strong>Захват</strong> → любая мысль/идея/факт фиксируется во <code>00. Входящие</code> или в дневнике. Формат не важен — главное не потерять.</li>
<li><strong>Обработка</strong> → раз в несколько дней разбираю инбокс: определяю тип заметки, добавляю frontmatter, перемещаю в нужную папку.</li>
<li><strong>Размещение</strong> → заметка отправляется в <code>01-04</code>, получает структуру по шаблону.</li>
<li><strong>Связывание</strong> → расставляю ссылки на связанные заметки, обновляю MOC если нужно.</li>
<li><strong>Использование</strong> → заметка живёт, дополняется при необходимости.</li>
<li><strong>Архив</strong> → если тема устарела — переношу в <code>04. Архив</code>, не удаляю.</li>
</ol>
<p>Инбокс обрабатываю без жёсткого расписания — по ощущению «накопилось». Обычно это 15-30 минут, 2-3 раза в неделю.</p>
<h3 id="правила-именования">Правила именования</h3>
<p>Заметки называю по принципу «чтобы было понятно без открытия»:</p>
<ul>
<li>Язык названия = язык заметки (русский по умолчанию, английский для технических терминов).</li>
<li>Никаких дат в названии, кроме дневниковых заметок (<code>2024-03-15</code>).</li>
<li>Существительные или глагольные фразы: «Ведение дневника лучше чем ведение todo-листа», «Kubernetes networking», «Почему SRE ≠ DevOps».</li>
<li>Никаких префиксов вроде <code>MOC-</code>, <code>BOOK-</code>, <code>IDEA-</code> — для этого есть теги и frontmatter.</li>
</ul>
<h3 id="как-я-нахожу-нужное">Как я нахожу нужное</h3>
<p>Страх «положу и забуду» — один из самых частых аргументов против ведения базы знаний. Но у меня он не работает, и вот почему.</p>
<p>Во-первых, поиск. Штатный поиск Obsidian слабоват, но я использую плагин Omnisearch — он быстрый и ищет по всему тексту, достаточно вспомнить любое слово из заметки. Во-вторых, связи между заметками: когда пишешь новую заметку и ставишь ссылки на старые, граф знаний сам напоминает, что у тебя уже есть по теме. В-третьих, Dataview-запросы в MOC-картах автоматически собирают заметки — не надо помнить, где что лежит.</p>
<p>На практике я редко «ищу» заметку целенаправленно. Чаще всего нужное всплывает само при написании новой заметки или при просмотре дневника.</p>
<p>Конкретные примеры навигации:</p>
<ul>
<li><strong>Идеи для постов</strong> → открываю MOC «Медийность» с Dataview-запросом, который тянет все заметки с тегом <code>#content</code>. Весь бэклог в одном месте.</li>
<li><strong>Инциденты</strong> → у каждого инцидента шаблон с тегом <code>#sre</code>. Dataview в MOC по SRE собирает их все.</li>
<li><strong>Книга, которую читал год назад</strong> → ищу по названию через Omnisearch или иду в <code>03. Ресурсы / Книги</code>.</li>
</ul>
<h3 id="шаблоны">Шаблоны</h3>
<p>Шаблонов у меня довольно много, и они со временем меняются. Вот те, что использую регулярно:</p>
<ul>
<li><code>Ежегодная заметка</code> — обзор года, использую в конце года, чтобы просто порефлексировать и подвести личные итоги.</li>
<li><code>Ежедневная заметка</code> — одна из основных заметок, используется для ведения дневника.</li>
<li><code>Конспект по лекции</code> — я студент и у меня есть отдельный шаблон для лекций, которые я посещаю.</li>
<li><code>Обзор конференции</code> — обзорная заметка про конкретную конференцию, где собираются все посещенные доклады и новые люди.</li>
<li><code>Шаблон тезисов по докладу</code> — доклады, которые я посетил на конференции и которые добавляются в обзорную заметку.</li>
<li><code>Созвон или встреча</code> — говорящее название, использую в том числе и для работы.</li>
<li><code>Черновик поста</code> — черновик поста (универсальный) и этот пост также был создан из этого шаблона.</li>
<li><code>Шаблон идея</code> — самый важный шаблон, который используется для всех заметок, которые я пишу сам сохраняю во входящих.</li>
<li><code>Шаблон инцидента</code> — для инцидентов, в которых я участвую лично у меня есть отдельный шаблон, информацию из которого я потом выношу в Postmortem.</li>
<li><code>Шаблон карты</code> — навигационные заметки или Map-of-Content.</li>
<li><code>Шаблон тезисов по видео</code>, <code>Шаблон тезисов по книге</code>, <code>Шаблон тезисов по статье</code> — для видео, книги или статьи используется свой шаблон, который в бОльшей степени является навигационной заметкой.</li>
<li><code>Шаблон человека</code> — заметки о людях.</li>
</ul>
<hr>
<h2 id="теги-и-свойства">Теги и свойства</h2>
<p>Теги у меня — это дополнительный слой навигации поверх папочной структуры. Живут в frontmatter, никаких инлайн-тегов.</p>
<p>Основные теги, которые реально использую:</p>
<table>
	<thead>
			<tr>
					<th>Тег</th>
					<th>Когда ставлю</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>#content</code> / <code>#telegram</code></td>
					<td>Черновик или идея для поста/канала</td>
			</tr>
			<tr>
					<td><code>#sre</code> / <code>#devops</code> / <code>#kubernetes</code></td>
					<td>Технические заметки по профилю</td>
			</tr>
			<tr>
					<td><code>#concept</code></td>
					<td>Термин, модель, фреймворк</td>
			</tr>
			<tr>
					<td><code>#thought</code></td>
					<td>Личная атомарная мысль</td>
			</tr>
			<tr>
					<td><code>#book</code> / <code>#article</code> / <code>#video</code></td>
					<td>Тип источника</td>
			</tr>
			<tr>
					<td><code>#inbox/review</code></td>
					<td>Требует обработки — снимаю тег после разбора</td>
			</tr>
			<tr>
					<td><code>#journal/daily</code></td>
					<td>Ежедневная запись</td>
			</tr>
	</tbody>
</table>
<p>В frontmatter также регулярно использую:</p>
<ul>
<li><code>up</code> / <code>down</code> — для навигации вверх/вниз по иерархии тем</li>
<li><code>status</code> — <code>Todo</code> / <code>WIP</code> / <code>Done</code> для проектов и черновиков</li>
<li><code>confidence</code> — <code>low</code> / <code>medium</code> / <code>high</code> для концептов и тезисов</li>
<li><code>sources</code> — список источников, из которых пришла информация</li>
</ul>
<hr>
<h2 id="темы-и-тематики">Темы и тематики</h2>
<p>Опишу крупными мазками — когортами тем — что у меня лежит в базе.</p>
<h3 id="преимущественно-рабочее">Преимущественно рабочее</h3>
<ul>
<li>Профильные темы — Dev(Sec)Ops/SRE, Кибербез/Защита информации, Процессы Incident&amp;Problem Management, (S)SDLC, etc&hellip;</li>
<li>Углубление в темы или Deep Dive — Infrastructure as/from Code, K8s, CICD, Configuration Management, etc&hellip;</li>
<li>Computer Science — Программирование, Основы CS, Golang/Python, Алгоритмы, Структуры данных, Парадигмы, etc&hellip;</li>
<li>Методологии — DevOps, SRE, Agile, Scrum, Kanban, ITSM, ITIL, TBM, etc&hellip;</li>
<li>Рабочее — Компании, Команды, Проекты, Встречи, Онбординги, etc&hellip;</li>
<li>Менеджмент — Управление людьми/командами/компаниями, Тимлидство и Техлидство.</li>
<li>Блог — Темы постов и готовые посты для канала и блога.</li>
</ul>
<p>Чтобы дать ощущение масштаба — вот несколько реальных названий заметок из рабочего блока (без содержания):</p>
<ul>
<li>«Чем SRE отличается от DevOps (и почему это важно)»</li>
<li>«SLO, SLA, SLI — разбор на пальцах»</li>
<li>«Почему постмортемы не работают без blame-free культуры»</li>
<li>«Kubernetes: как работает network policy»</li>
<li>«Разница между горизонтальным и вертикальным масштабированием»</li>
</ul>
<h3 id="я-мышление-и-работа-над-собой">Я, Мышление и работа над собой</h3>
<ul>
<li>Личная база знаний — Устройство, Воркфлоу, Второй мозг, etc&hellip;</li>
<li>Саморазвитие и Образование — Курсы, Книги, Хард- и Софт-скиллы, Области роста, Конспекты, etc&hellip;</li>
<li>Психология — Личная, Командная, Коммуникация, etc&hellip;</li>
<li>Мысли и принципы жизни — Разрозненные умозаключения и идеи, Проектирование себя, etc&hellip;</li>
<li>Личное — Семья, Финансы, Здоровье, Люди, etc&hellip;</li>
</ul>
<p>Примеры названий из этого блока:</p>
<ul>
<li>«Ведение дневника лучше чем ведение todo-листа»</li>
<li>«Как я принимаю сложные решения»</li>
<li>«Почему я перестал делать списки задач на день»</li>
<li>«Привязанность vs зависимость в отношениях»</li>
</ul>
<h3 id="разное">Разное</h3>
<p>Тут прям все подряд без списка: Философия, Право, Социология, Публичные выступления, Работа мозга, Драматургия.</p>
<h3 id="дневник">Дневник</h3>
<p>Это прям отдельная тема, которую я считаю крайне важной — дневник и ежедневные заметки, в которых фиксируется абсолютно всё, как в личном блокноте.</p>
<p>Когда-то подсмотрел у Рустама Агамалиева подтверждение того, что у меня чисто интуитивно вызывало комфорт: ведение дневника лучше, чем ведение todo-листа. В свободной форме пишу то, что необходимо сделать или уже сделано. О том, как я веду свой дневник, можно прочитать у меня в отдельном <a href="https://t.me/jtprogru_channel/4265" rel="noopener noreferrer" target="_blank">посте</a>.</p>
<hr>
<h2 id="записывать-или-не-записывать">Записывать или не записывать</h2>
<p>Буквально, как в монологе Гамлета — &ldquo;Быть или не быть&hellip;&rdquo; очередной заметке. И тут, как и везде — <em>все максимально индивидуально</em>. Именно поэтому я просто попытаюсь описать, чем именно я руководствуюсь, когда создаю очередную заметку.</p>
<h3 id="пригодится-или-справочная-информация">Пригодится или Справочная информация</h3>
<p>Заметка, как какой-то справочный материал, который мне может пригодиться в будущем. Для разных &ldquo;штук&rdquo;, которые бывает дольше гуглить и вспоминать, поэтому проще сохранить в виде заметки:</p>
<ul>
<li>разделение настроек для консольного git-клиента</li>
<li>возвращение режима модема на айфонах</li>
<li>расположение файлов библиотеки приложения Apple Books</li>
</ul>
<p>Проще говоря, это такие заметки, которые я первым делом пойду гуглить — какие-то инструкции, которые мне бывают не часто нужны и которые я не могу должным образом автоматизировать.</p>
<p>В эту же категорию относятся и заметки о каких-то людях. Будь то научный деятель, блогер, философ или &ldquo;крутой чувак&rdquo; с которым я познакомился на конференции.</p>
<h3 id="очередная-гениальная-идея">Очередная гениальная идея</h3>
<p>Не важно, когда она ко мне пришла в голову — в процессе чтения книги/статьи/видоса, либо во время выполнения рабочей задачи или даже во время созвона кем-то. Я запишу эту мысль, чем бы она ни была — связана она с текущим контекстом или деятельностью или нет.</p>
<p>Чаще всего такие мысли фиксируются в ежедневной заметке, а оттуда в ближайшее время извлекаются в отдельную заметку и попадают во входящие на дальнейшую обработку.</p>
<p>Тут могут быть и идеи по личной или рабочей автоматизации, и какие-то околофилософские мыслезлоключения, и идеи доработок чего-либо (особенно рабочее или в пет-проектах). Проще говоря — что-то, что не хочется забыть.</p>
<h3 id="рефлексия">Рефлексия</h3>
<p>Кто-то в рамках ежедневных заметок ведет полноценное планирование и управление проектами, а также через свойства отмечает сколько часов поспал и количество выпитой воды с настроением. Но это точно не я — мне такое пока не очень интересно отслеживать в своей базе знаний.</p>
<p>Я уже не однократно упоминал то, что я веду дневник и веду я его с 2021 года (самая первая дата — 2021-08-05). И вот именно дневник мне начал приносить ощутимую пользу — дневник позволяет ковыряться в собственных мозгах так, словно ты наблюдаешь со стороны.</p>
<p>Была куча разных приложений перепробована, но все они довольно быстро надоедают. А потребность в том, чтобы научиться разбираться в себе — была всегда и будет со мной всегда. Даже в работе с психологами дневник очень хорошо помогает мне.</p>
<h3 id="конспекты-и-обучение">Конспекты и обучение</h3>
<p>Книга, видео, лекция, статья — всё, из чего я что-то усвоил или хочу усвоить, попадает в базу. Не пересказ, а именно то, что меня зацепило: конкретная мысль, тезис, пример, который хочу вернуть потом.</p>
<p>Для каждого типа источника у меня отдельный шаблон (книга / видео / статья / доклад), потому что у них разная структура и разный контекст. Главное правило тут — <strong>не стараться законспектировать всё</strong>. Записываю только то, что хочу переосмыслить или применить.</p>
<h3 id="что-я-намеренно-не-записываю">Что я намеренно НЕ записываю</h3>
<p>Несколько категорий, которые осознанно остаются вне базы знаний:</p>
<ul>
<li><strong>Пароли и секреты</strong> — для этого есть парольный менеджер. В базу знаний принципиально не попадают.</li>
<li><strong>Персональные данные других людей</strong> — контакты храню минимально, без чувствительных деталей. Заметка о человеке — это профессиональный контекст, не личное досье.</li>
<li><strong>Рабочие секреты</strong> — архитектура клиентских систем, внутренние имена, конфигурации. Если что-то рабочее попадает в заметку — сразу обезличивается.</li>
<li><strong>Эмоции в моменте без контекста</strong> — «я злой», «всё плохо» без объяснения почему. Дневник — место для рефлексии, а не для фиксации сырого состояния без смысла.</li>
<li><strong>Задачи и todo-листы</strong> — для задач есть трекер. База знаний — не таск-менеджер.</li>
</ul>
<hr>
<h2 id="приватность-и-безопасность">Приватность и безопасность</h2>
<p>Раз уж я сказал «не покажу пока живой» — логично объяснить, как именно сделано так, чтобы случайно не утекло.</p>
<ul>
<li><strong>Шифрование диска</strong> — база хранится локально, диск зашифрован.</li>
<li><strong>iCloud</strong> — синхронизация между устройствами. Не идеально с точки зрения параноика, но для меня приемлемо.</li>
<li><strong>Git-бэкап</strong> — автоматический бэкап в приватный репозиторий. В <code>.gitignore</code> исключены чувствительные файлы.</li>
<li><strong>Нет публичных ссылок</strong> — никакого Obsidian Publish или аналогов. Если что-то публикую — делаю это руками, отдельным постом.</li>
</ul>
<h2 id="пересмотр-и-актуальность">Пересмотр и актуальность</h2>
<p>База знаний — это не музей. Заметки устаревают, взгляды меняются, какие-то темы перестают быть актуальными.</p>
<p>Как я с этим работаю:</p>
<ul>
<li><strong>Periodic review — по факту, не по расписанию</strong>. Нет жёсткого «раз в неделю смотрю базу». Пересматриваю заметки, когда возвращаюсь к теме по делу.</li>
<li><strong>Статус <code>WIP</code> как маркер</strong>. Если заметка давно в <code>WIP</code> и к ней нет желания возвращаться — это сигнал либо закрыть (→ <code>Done</code>), либо заархивировать.</li>
<li><strong>Дневник как встроенный review</strong>. Ежегодные и ежемесячные заметки — там я естественным образом возвращаюсь к тому, что было важно.</li>
<li><strong>Архив вместо удаления</strong>. Почти никогда не удаляю заметки. Если информация устарела — переношу в <code>04. Архив</code>. Потом можно найти и посмотреть, как думал раньше.</li>
</ul>
<hr>
<h2 id="итоги-и-полезности">Итоги и полезности</h2>
<p>У меня есть в блоге <a href="https://jtprog.ru/posts/pkb-check-list/">Чек-лист для организации персональной базы знаний</a> — если хочешь начать, это хорошая отправная точка. Также было небольшое выступление перед студентами РТУ МИРЭА про знакомство с личной базой знаний (<a href="https://jtprogru.github.io/pkbd/" rel="noopener noreferrer" target="_blank">слайды</a> есть, а видео нет).</p>
<p>Если вынести одну мысль из всего этого — <strong>важнее писать заметки, чем не писать их вовсе</strong>. Не нужна идеальная структура с первого дня. Не нужно читать все книги по PKB и Zettelkasten. Нужно просто начать фиксировать то, что кажется важным — и постепенно система сама начнёт складываться под тебя.</p>
<p><strong>Кому такой подход подойдёт:</strong></p>
<ul>
<li>Тем, кто работает со знаниями профессионально и хочет не забывать то, что узнаёт.</li>
<li>Тем, кто хочет думать более структурированно и видеть связи между идеями.</li>
<li>Тем, кому важно иметь «свой» инструмент, а не чужую систему.</li>
</ul>
<p><strong>Кому, скорее всего, не подойдёт:</strong></p>
<ul>
<li>Тем, кто хочет быстрый результат — первые месяцы база знаний больше потребляет времени, чем отдаёт.</li>
<li>Тем, кто не готов периодически разбирать инбокс — без обработки это просто свалка.</li>
</ul>
<p><strong>С чего начать новичку:</strong></p>
<ol>
<li>Выбери инструмент (Obsidian, Notion, Logseq — не важно, главное начать).</li>
<li>Заведи один шаблон для «идеи» — просто заголовок + 2-3 строки.</li>
<li>Первую неделю пиши всё подряд без структуры.</li>
<li>На второй неделе посмотри, что накопилось, и попробуй сгруппировать.</li>
<li>Структура появится сама — из того, что реально накапливается.</li>
</ol>
</div>]]></content:encoded>
    </item><item>
      <title>Как выглядит MBR</title>
      <link>https://jtprog.ru/posts/what-is-mbr/</link>
      <pubDate>Tue, 05 May 2026 20:36:08 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/what-is-mbr/</guid>
      <category>MBR</category>
      <category>Disks</category>
      <category>Разметка Диска</category>
      <category>Linux</category>
      <category>Администрирование</category>
      <category domain="category">OS</category>
      <enclosure url="https://jtprog.ru/OS.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/OS.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Сегодня MBR воспринимается как что-то из музея — на новых машинах давно <a href="/posts/what-is-gpt/">GPT</a> и UEFI, а слово «активный раздел» молодёжь слышит впервые в жизни. Но MBR никуда не делся: он живёт на старых ноутбуках, флешках, виртуалках с BIOS-загрузкой и в виде Protective MBR на тех самых GPT-дисках. И когда что-то «перестало грузиться», понимать, что лежит в первых 512 байтах диска, всё ещё бывает очень полезно.</p>
<p>В этой шпаргалке разберём по байтам, что такое MBR, как устроены его четыре записи о разделах, зачем нужны extended/logical партиции через EBR и какие архитектурные ограничения в итоге привели к появлению GPT.</p>
<h2 id="что-такое-mbr">Что такое MBR</h2>
<p><strong>MBR (Master Boot Record)</strong> — самый первый сектор диска (LBA 0, 512 байт), в котором одновременно живут две вещи: маленький загрузочный код для BIOS и таблица из четырёх записей о разделах. Появился в 1983 году вместе с PC DOS 2.0 и больше тридцати лет был стандартом разметки x86-машин, пока его наконец не вытеснил <a href="/posts/what-is-gpt/">GPT</a>.</p>
<p>Главная идея простая до неприличия: BIOS после POST читает первый сектор первого устройства, проверяет, что он заканчивается сигнатурой <code>0x55AA</code>, и передаёт управление в самое начало этого сектора как обычному 16-битному коду. Дальше уже сам код решает, какой раздел грузить. Никаких CRC, никаких backup-копий, никакой защиты — затёр сектор и привет.</p>
<h2 id="карта-сектора-512-байт">Карта сектора (512 байт)</h2>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>Offset  Size   Назначение
------  ----   ----------
0x000   440    Bootstrap code (загрузчик, stage 1)
0x1B8   4      Disk Signature (NT Disk Signature / MBR ID)
0x1BC   2      Reserved (обычно 0x0000)
0x1BE   16     Partition Entry #1
0x1CE   16     Partition Entry #2
0x1DE   16     Partition Entry #3
0x1EE   16     Partition Entry #4
0x1FE   2      Boot Signature (0x55 0xAA)</code></pre>
</div>
<p>Итого: 440 + 4 + 2 + 64 + 2 = <strong>512 байт</strong> ровно. Именно столько было в одном секторе у дисков 80-х, и именно эта цифра до сих пор определяет всё устройство MBR.</p>
<h2 id="bootstrap-code-0x0000x1b7">Bootstrap code (<code>0x000–0x1B7</code>)</h2>
<p>440 байт машинного кода в реальном режиме x86. Что туда обычно кладут:</p>
<ul>
<li>Минимальный загрузчик, который ищет «активный» раздел (с флагом <code>0x80</code>)</li>
<li>Читает с него первый сектор (VBR — Volume Boot Record) и передаёт ему управление</li>
<li>Либо это <strong>stage 1 GRUB / LILO / syslinux</strong>, который читает stage 1.5 / stage 2 с заранее известных секторов</li>
</ul>
<p>В 440 байт нельзя засунуть драйвер файловой системы — поэтому stage 1 GRUB хранит в себе блок-лист (LBA-адреса) первых секторов stage 1.5 в т. н. «post-MBR gap» (секторы 1–62, до начала первого раздела). Если переразметил диск так, что эти сектора съехали — система не грузится, и это самая частая причина «всё работало, потом подвинул раздел и сломалось».</p>
<h2 id="disk-signature-0x1b80x1bb">Disk Signature (<code>0x1B8–0x1BB</code>)</h2>
<p>4 байта — уникальный идентификатор диска, который Windows называет <strong>NT Disk Signature</strong>. По нему ОС привязывает диск к буквам в <code>BCD</code> и реестре. Если склонировать диск побайтово на другой — Windows увидит два диска с одинаковой подписью и одну из них автоматически переведёт в offline. Лечится через <code>diskpart → uniqueid disk id=...</code>, но узнать про эту весёлую особенность обычно успеваешь уже после нескольких часов поиска «почему второй диск не виден».</p>
<p>В Linux эта же подпись видна через <code>fdisk -l</code>, поле <code>Disk identifier: 0x...</code>.</p>
<h2 id="partition-entry-16-байт">Partition Entry (16 байт)</h2>
<p>Главная структура разметки. Четыре штуки, фиксированно, на любом MBR-диске в мире:</p>
<table>
	<thead>
			<tr>
					<th style="text-align: right">Offset</th>
					<th style="text-align: right">Size</th>
					<th>Поле</th>
					<th>Описание</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: right">0</td>
					<td style="text-align: right">1</td>
					<td><code>Boot indicator</code></td>
					<td><code>0x80</code> — активный (бутабельный), <code>0x00</code> — нет, прочее — invalid</td>
			</tr>
			<tr>
					<td style="text-align: right">1</td>
					<td style="text-align: right">3</td>
					<td><code>Starting CHS</code></td>
					<td>Cylinder/Head/Sector начала раздела (legacy)</td>
			</tr>
			<tr>
					<td style="text-align: right">4</td>
					<td style="text-align: right">1</td>
					<td><code>Partition Type</code></td>
					<td>Тип раздела (см. ниже)</td>
			</tr>
			<tr>
					<td style="text-align: right">5</td>
					<td style="text-align: right">3</td>
					<td><code>Ending CHS</code></td>
					<td>CHS конца раздела (legacy)</td>
			</tr>
			<tr>
					<td style="text-align: right">8</td>
					<td style="text-align: right">4</td>
					<td><code>Starting LBA</code></td>
					<td>LBA первого сектора раздела (little-endian)</td>
			</tr>
			<tr>
					<td style="text-align: right">12</td>
					<td style="text-align: right">4</td>
					<td><code>Size in LBA</code></td>
					<td>Длина раздела в секторах</td>
			</tr>
	</tbody>
</table>
<p>Из этих 16 байт реально работают только последние 8: <code>Starting LBA</code> + <code>Size in LBA</code>. Поля CHS остались для совместимости с BIOS-ами 80-х; современные ОС их игнорируют, но для надёжности туда обычно пишут <code>0xFFFFFF</code>, если раздел выходит за пределы CHS-адресации (а это всё, что больше ~8 ГБ — то есть фактически любой диск, который ты держал в руках за последние двадцать лет).</p>
<h3 id="лимиты-которые-отсюда-растут">Лимиты, которые отсюда растут</h3>
<ul>
<li><code>Starting LBA</code> и <code>Size in LBA</code> — 32-битные → максимум <strong>2³² × 512 байт = 2 ТиБ</strong>. Дальше адрес физически не лезет, и MBR на 4-терабайтнике увидит ровно половину диска. Если столкнулся — это знак, что пора на GPT</li>
<li>Записей всего 4 → максимум <strong>4 первичных раздела</strong>. Чтобы обойти этот лимит, придумали extended-партиции</li>
</ul>
<h2 id="partition-type-известные-коды">Partition Type (известные коды)</h2>
<table>
	<thead>
			<tr>
					<th>Код</th>
					<th>Тип</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>0x00</code></td>
					<td>Пусто</td>
			</tr>
			<tr>
					<td><code>0x05</code></td>
					<td>Extended (CHS)</td>
			</tr>
			<tr>
					<td><code>0x07</code></td>
					<td>NTFS / exFAT / HPFS</td>
			</tr>
			<tr>
					<td><code>0x0B</code></td>
					<td>FAT32 (CHS)</td>
			</tr>
			<tr>
					<td><code>0x0C</code></td>
					<td>FAT32 (LBA)</td>
			</tr>
			<tr>
					<td><code>0x0E</code></td>
					<td>FAT16 (LBA)</td>
			</tr>
			<tr>
					<td><code>0x0F</code></td>
					<td>Extended (LBA)</td>
			</tr>
			<tr>
					<td><code>0x82</code></td>
					<td>Linux swap</td>
			</tr>
			<tr>
					<td><code>0x83</code></td>
					<td>Linux filesystem</td>
			</tr>
			<tr>
					<td><code>0x8E</code></td>
					<td>Linux LVM</td>
			</tr>
			<tr>
					<td><code>0xA5</code></td>
					<td>FreeBSD</td>
			</tr>
			<tr>
					<td><code>0xAF</code></td>
					<td>Apple HFS / HFS+</td>
			</tr>
			<tr>
					<td><code>0xEE</code></td>
					<td>GPT Protective MBR</td>
			</tr>
			<tr>
					<td><code>0xEF</code></td>
					<td>EFI System (на MBR-диске)</td>
			</tr>
			<tr>
					<td><code>0xFD</code></td>
					<td>Linux RAID auto-detect</td>
			</tr>
	</tbody>
</table>
<p>Тип — это просто метка-подсказка для ОС, что внутри. Сам диск от смены типа не «форматируется», но Windows может перестать видеть раздел, если поставить «чужой» код. Кстати, никакого центрального реестра этих кодов нет — список держится на честном слове и Wikipedia, поэтому для разных ОС одинаковые байты иногда означают разное. Привет, GPT с его GUID-ами.</p>
<h2 id="extended-и-logical-partitions">Extended и logical partitions</h2>
<p>Чтобы сделать больше четырёх разделов, одну из четырёх записей объявляют <strong>extended</strong> (тип <code>0x05</code> или <code>0x0F</code>). Внутри extended-области строится <strong>связный список</strong> через дополнительные структуры — <strong>EBR (Extended Boot Record)</strong>.</p>
<p>Каждый EBR — это сектор с такой же таблицей из 4 записей, но реально используются только две:</p>
<ul>
<li><strong>Запись 1</strong> — описывает logical-раздел, лежащий сразу за этим EBR</li>
<li><strong>Запись 2</strong> — указывает на следующий EBR в цепочке (тип <code>0x05</code>/<code>0x0F</code>); смещения здесь относительные</li>
</ul>
<p>Цепочка обрывается, когда вторая запись пустая. В Linux logical-разделы нумеруются с <code>5</code> (<code>/dev/sda5</code>, <code>/dev/sda6</code>, …) — независимо от того, сколько было primary-разделов. Лимит на количество — обычно 60 logical в Linux, ограничение драйвера ядра.</p>
<p>Минусы конструкции:</p>
<ul>
<li>Список последовательный → удалил logical посередине, и нумерация поехала, фрагментация в полный рост</li>
<li>Затёр EBR — потерял всю цепочку дальше; backup-копии нет</li>
<li>Активным может быть только primary; нельзя загрузиться напрямую с logical, нужен посредник</li>
</ul>
<p>Это всё, очевидно, костыль. И именно поэтому на серверах с пятнадцатью разделами под логи, БД, /var и прочее переход на GPT воспринимается как глоток свежего воздуха.</p>
<h2 id="boot-signature-0x1fe0x1ff">Boot Signature (<code>0x1FE–0x1FF</code>)</h2>
<p>Два байта: <code>0x55 0xAA</code>. Если их нет — BIOS считает диск незагрузочным и идёт к следующему. Это самая частая жертва «битого MBR»: код есть, таблица есть, а сигнатуру кто-то затёр. Внешне выглядит как «диск пропал», хотя на самом деле там всего два байта надо вернуть на место.</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<ul>
<li><strong>Посмотреть таблицу</strong>: <code>fdisk -l /dev/sda</code>, <code>sfdisk -d /dev/sda</code>, <code>parted /dev/sda print</code></li>
<li><strong>Сырой дамп</strong>: <code>dd if=/dev/sda bs=512 count=1 | hexdump -C</code> — увидишь bootstrap, таблицу разделов и <code>55 AA</code> в самом конце</li>
<li><strong>Бэкап MBR целиком</strong>: <code>dd if=/dev/sda of=mbr.bin bs=512 count=1</code>. Только таблица без бутстрапа: <code>dd if=/dev/sda of=ptable.bin bs=1 skip=446 count=64</code></li>
<li><strong>Восстановить bootloader</strong>: <code>grub-install /dev/sda</code> (Linux), <code>bootrec /fixmbr</code> (Windows)</li>
<li><strong>Сменить тип раздела</strong>: <code>fdisk</code> → <code>t</code> → код. Не форматирует, только меняет байт</li>
<li><strong>Активировать раздел</strong>: <code>fdisk</code> → <code>a</code>. Помним: <code>0x80</code> должен стоять только у одного раздела</li>
</ul>
<h2 id="чем-плох-mbr-и-зачем-родился-gpt">Чем плох MBR (и зачем родился GPT)</h2>
<ul>
<li>Потолок 2 ТиБ — на современных дисках это ровно половина</li>
<li>Только 4 первичных раздела, остальное — через костыль с EBR</li>
<li>Нет контрольных сумм — один битфлип в нужном месте, и таблица «уехала», а ОС об этом не узнает</li>
<li>Нет резервной копии — затёртый сектор 0 равняется диску-«кирпичу» для BIOS</li>
<li>Тип раздела — 1 байт без пространства имён, поэтому коды конфликтуют между ОС и существуют по сути «по договорённости»</li>
</ul>
<p>Подробнее про преемника — в <a href="/posts/what-is-gpt/">Как выглядит GPT</a>.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong>CHS vs LBA</strong>: до сих пор встречаются прошивки, которые читают именно CHS. Если поставить туда «битые» значения, BIOS может отказаться грузиться даже при корректном LBA. Утилиты обычно сами пишут <code>0xFFFFFF</code> в CHS, но если правил руками — проверь</li>
<li><strong>Активный раздел</strong>: <code>0x80</code> должен быть <strong>только у одного</strong> раздела. Несколько активных — поведение зависит от загрузчика, и бывает классическое «грузится не то, что ожидал»</li>
<li><strong>MBR на USB</strong>: некоторые BIOS грузятся только если на флешке <code>0x80</code> стоит и тип — <code>0x0C</code>/<code>0x0B</code>. Нарезал через <code>dd</code> ISO-образ, а флешка не грузится — проверь именно это</li>
<li><strong>Hybrid MBR на GPT-дисках</strong>: записи в Protective MBR подменяют на «настоящие», чтобы BIOS-only ОС увидели разделы. Жуткий хак, рассинхрон с GPT ломает всё разом — лучше не использовать, но если унаследовал — знай, что оно есть</li>
<li><strong>NT Disk Signature и клонирование</strong>: после <code>dd</code> диска побайтово обязательно меняй подпись, иначе Windows унесёт один из дисков в offline. На Linux-only стенде проблемы нет, но как только подключишь к Windows — словишь весёлый сюрприз</li>
</ul>
<p>Практическая сторона вопроса — <a href="/posts/gpt-to-mbr/">конвертация GPT в MBR через <code>diskpart</code></a>, когда старая Windows отказывается ставиться.</p>
<p>На этом всё. MBR — простая структура из эпохи, когда диски измерялись в мегабайтах, а 4 разделов хватало всем. Сегодня его удел — старые системы и роль Protective MBR на GPT-дисках, но базу знать всё ещё полезно: на боевой железке в 3 часа ночи хочется понимать, что именно ты сейчас правишь в <code>dd</code> и <code>hexdump</code>.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Сегодня MBR воспринимается как что-то из музея — на новых машинах давно <a href="/posts/what-is-gpt/">GPT</a> и UEFI, а слово «активный раздел» молодёжь слышит впервые в жизни. Но MBR никуда не делся: он живёт на старых ноутбуках, флешках, виртуалках с BIOS-загрузкой и в виде Protective MBR на тех самых GPT-дисках. И когда что-то «перестало грузиться», понимать, что лежит в первых 512 байтах диска, всё ещё бывает очень полезно.</p>
<p>В этой шпаргалке разберём по байтам, что такое MBR, как устроены его четыре записи о разделах, зачем нужны extended/logical партиции через EBR и какие архитектурные ограничения в итоге привели к появлению GPT.</p>
<h2 id="что-такое-mbr">Что такое MBR</h2>
<p><strong>MBR (Master Boot Record)</strong> — самый первый сектор диска (LBA 0, 512 байт), в котором одновременно живут две вещи: маленький загрузочный код для BIOS и таблица из четырёх записей о разделах. Появился в 1983 году вместе с PC DOS 2.0 и больше тридцати лет был стандартом разметки x86-машин, пока его наконец не вытеснил <a href="/posts/what-is-gpt/">GPT</a>.</p>
<p>Главная идея простая до неприличия: BIOS после POST читает первый сектор первого устройства, проверяет, что он заканчивается сигнатурой <code>0x55AA</code>, и передаёт управление в самое начало этого сектора как обычному 16-битному коду. Дальше уже сам код решает, какой раздел грузить. Никаких CRC, никаких backup-копий, никакой защиты — затёр сектор и привет.</p>
<h2 id="карта-сектора-512-байт">Карта сектора (512 байт)</h2>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>Offset  Size   Назначение
------  ----   ----------
0x000   440    Bootstrap code (загрузчик, stage 1)
0x1B8   4      Disk Signature (NT Disk Signature / MBR ID)
0x1BC   2      Reserved (обычно 0x0000)
0x1BE   16     Partition Entry #1
0x1CE   16     Partition Entry #2
0x1DE   16     Partition Entry #3
0x1EE   16     Partition Entry #4
0x1FE   2      Boot Signature (0x55 0xAA)</code></pre>
</div>
<p>Итого: 440 + 4 + 2 + 64 + 2 = <strong>512 байт</strong> ровно. Именно столько было в одном секторе у дисков 80-х, и именно эта цифра до сих пор определяет всё устройство MBR.</p>
<h2 id="bootstrap-code-0x0000x1b7">Bootstrap code (<code>0x000–0x1B7</code>)</h2>
<p>440 байт машинного кода в реальном режиме x86. Что туда обычно кладут:</p>
<ul>
<li>Минимальный загрузчик, который ищет «активный» раздел (с флагом <code>0x80</code>)</li>
<li>Читает с него первый сектор (VBR — Volume Boot Record) и передаёт ему управление</li>
<li>Либо это <strong>stage 1 GRUB / LILO / syslinux</strong>, который читает stage 1.5 / stage 2 с заранее известных секторов</li>
</ul>
<p>В 440 байт нельзя засунуть драйвер файловой системы — поэтому stage 1 GRUB хранит в себе блок-лист (LBA-адреса) первых секторов stage 1.5 в т. н. «post-MBR gap» (секторы 1–62, до начала первого раздела). Если переразметил диск так, что эти сектора съехали — система не грузится, и это самая частая причина «всё работало, потом подвинул раздел и сломалось».</p>
<h2 id="disk-signature-0x1b80x1bb">Disk Signature (<code>0x1B8–0x1BB</code>)</h2>
<p>4 байта — уникальный идентификатор диска, который Windows называет <strong>NT Disk Signature</strong>. По нему ОС привязывает диск к буквам в <code>BCD</code> и реестре. Если склонировать диск побайтово на другой — Windows увидит два диска с одинаковой подписью и одну из них автоматически переведёт в offline. Лечится через <code>diskpart → uniqueid disk id=...</code>, но узнать про эту весёлую особенность обычно успеваешь уже после нескольких часов поиска «почему второй диск не виден».</p>
<p>В Linux эта же подпись видна через <code>fdisk -l</code>, поле <code>Disk identifier: 0x...</code>.</p>
<h2 id="partition-entry-16-байт">Partition Entry (16 байт)</h2>
<p>Главная структура разметки. Четыре штуки, фиксированно, на любом MBR-диске в мире:</p>
<table>
	<thead>
			<tr>
					<th style="text-align: right">Offset</th>
					<th style="text-align: right">Size</th>
					<th>Поле</th>
					<th>Описание</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: right">0</td>
					<td style="text-align: right">1</td>
					<td><code>Boot indicator</code></td>
					<td><code>0x80</code> — активный (бутабельный), <code>0x00</code> — нет, прочее — invalid</td>
			</tr>
			<tr>
					<td style="text-align: right">1</td>
					<td style="text-align: right">3</td>
					<td><code>Starting CHS</code></td>
					<td>Cylinder/Head/Sector начала раздела (legacy)</td>
			</tr>
			<tr>
					<td style="text-align: right">4</td>
					<td style="text-align: right">1</td>
					<td><code>Partition Type</code></td>
					<td>Тип раздела (см. ниже)</td>
			</tr>
			<tr>
					<td style="text-align: right">5</td>
					<td style="text-align: right">3</td>
					<td><code>Ending CHS</code></td>
					<td>CHS конца раздела (legacy)</td>
			</tr>
			<tr>
					<td style="text-align: right">8</td>
					<td style="text-align: right">4</td>
					<td><code>Starting LBA</code></td>
					<td>LBA первого сектора раздела (little-endian)</td>
			</tr>
			<tr>
					<td style="text-align: right">12</td>
					<td style="text-align: right">4</td>
					<td><code>Size in LBA</code></td>
					<td>Длина раздела в секторах</td>
			</tr>
	</tbody>
</table>
<p>Из этих 16 байт реально работают только последние 8: <code>Starting LBA</code> + <code>Size in LBA</code>. Поля CHS остались для совместимости с BIOS-ами 80-х; современные ОС их игнорируют, но для надёжности туда обычно пишут <code>0xFFFFFF</code>, если раздел выходит за пределы CHS-адресации (а это всё, что больше ~8 ГБ — то есть фактически любой диск, который ты держал в руках за последние двадцать лет).</p>
<h3 id="лимиты-которые-отсюда-растут">Лимиты, которые отсюда растут</h3>
<ul>
<li><code>Starting LBA</code> и <code>Size in LBA</code> — 32-битные → максимум <strong>2³² × 512 байт = 2 ТиБ</strong>. Дальше адрес физически не лезет, и MBR на 4-терабайтнике увидит ровно половину диска. Если столкнулся — это знак, что пора на GPT</li>
<li>Записей всего 4 → максимум <strong>4 первичных раздела</strong>. Чтобы обойти этот лимит, придумали extended-партиции</li>
</ul>
<h2 id="partition-type-известные-коды">Partition Type (известные коды)</h2>
<table>
	<thead>
			<tr>
					<th>Код</th>
					<th>Тип</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>0x00</code></td>
					<td>Пусто</td>
			</tr>
			<tr>
					<td><code>0x05</code></td>
					<td>Extended (CHS)</td>
			</tr>
			<tr>
					<td><code>0x07</code></td>
					<td>NTFS / exFAT / HPFS</td>
			</tr>
			<tr>
					<td><code>0x0B</code></td>
					<td>FAT32 (CHS)</td>
			</tr>
			<tr>
					<td><code>0x0C</code></td>
					<td>FAT32 (LBA)</td>
			</tr>
			<tr>
					<td><code>0x0E</code></td>
					<td>FAT16 (LBA)</td>
			</tr>
			<tr>
					<td><code>0x0F</code></td>
					<td>Extended (LBA)</td>
			</tr>
			<tr>
					<td><code>0x82</code></td>
					<td>Linux swap</td>
			</tr>
			<tr>
					<td><code>0x83</code></td>
					<td>Linux filesystem</td>
			</tr>
			<tr>
					<td><code>0x8E</code></td>
					<td>Linux LVM</td>
			</tr>
			<tr>
					<td><code>0xA5</code></td>
					<td>FreeBSD</td>
			</tr>
			<tr>
					<td><code>0xAF</code></td>
					<td>Apple HFS / HFS+</td>
			</tr>
			<tr>
					<td><code>0xEE</code></td>
					<td>GPT Protective MBR</td>
			</tr>
			<tr>
					<td><code>0xEF</code></td>
					<td>EFI System (на MBR-диске)</td>
			</tr>
			<tr>
					<td><code>0xFD</code></td>
					<td>Linux RAID auto-detect</td>
			</tr>
	</tbody>
</table>
<p>Тип — это просто метка-подсказка для ОС, что внутри. Сам диск от смены типа не «форматируется», но Windows может перестать видеть раздел, если поставить «чужой» код. Кстати, никакого центрального реестра этих кодов нет — список держится на честном слове и Wikipedia, поэтому для разных ОС одинаковые байты иногда означают разное. Привет, GPT с его GUID-ами.</p>
<h2 id="extended-и-logical-partitions">Extended и logical partitions</h2>
<p>Чтобы сделать больше четырёх разделов, одну из четырёх записей объявляют <strong>extended</strong> (тип <code>0x05</code> или <code>0x0F</code>). Внутри extended-области строится <strong>связный список</strong> через дополнительные структуры — <strong>EBR (Extended Boot Record)</strong>.</p>
<p>Каждый EBR — это сектор с такой же таблицей из 4 записей, но реально используются только две:</p>
<ul>
<li><strong>Запись 1</strong> — описывает logical-раздел, лежащий сразу за этим EBR</li>
<li><strong>Запись 2</strong> — указывает на следующий EBR в цепочке (тип <code>0x05</code>/<code>0x0F</code>); смещения здесь относительные</li>
</ul>
<p>Цепочка обрывается, когда вторая запись пустая. В Linux logical-разделы нумеруются с <code>5</code> (<code>/dev/sda5</code>, <code>/dev/sda6</code>, …) — независимо от того, сколько было primary-разделов. Лимит на количество — обычно 60 logical в Linux, ограничение драйвера ядра.</p>
<p>Минусы конструкции:</p>
<ul>
<li>Список последовательный → удалил logical посередине, и нумерация поехала, фрагментация в полный рост</li>
<li>Затёр EBR — потерял всю цепочку дальше; backup-копии нет</li>
<li>Активным может быть только primary; нельзя загрузиться напрямую с logical, нужен посредник</li>
</ul>
<p>Это всё, очевидно, костыль. И именно поэтому на серверах с пятнадцатью разделами под логи, БД, /var и прочее переход на GPT воспринимается как глоток свежего воздуха.</p>
<h2 id="boot-signature-0x1fe0x1ff">Boot Signature (<code>0x1FE–0x1FF</code>)</h2>
<p>Два байта: <code>0x55 0xAA</code>. Если их нет — BIOS считает диск незагрузочным и идёт к следующему. Это самая частая жертва «битого MBR»: код есть, таблица есть, а сигнатуру кто-то затёр. Внешне выглядит как «диск пропал», хотя на самом деле там всего два байта надо вернуть на место.</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<ul>
<li><strong>Посмотреть таблицу</strong>: <code>fdisk -l /dev/sda</code>, <code>sfdisk -d /dev/sda</code>, <code>parted /dev/sda print</code></li>
<li><strong>Сырой дамп</strong>: <code>dd if=/dev/sda bs=512 count=1 | hexdump -C</code> — увидишь bootstrap, таблицу разделов и <code>55 AA</code> в самом конце</li>
<li><strong>Бэкап MBR целиком</strong>: <code>dd if=/dev/sda of=mbr.bin bs=512 count=1</code>. Только таблица без бутстрапа: <code>dd if=/dev/sda of=ptable.bin bs=1 skip=446 count=64</code></li>
<li><strong>Восстановить bootloader</strong>: <code>grub-install /dev/sda</code> (Linux), <code>bootrec /fixmbr</code> (Windows)</li>
<li><strong>Сменить тип раздела</strong>: <code>fdisk</code> → <code>t</code> → код. Не форматирует, только меняет байт</li>
<li><strong>Активировать раздел</strong>: <code>fdisk</code> → <code>a</code>. Помним: <code>0x80</code> должен стоять только у одного раздела</li>
</ul>
<h2 id="чем-плох-mbr-и-зачем-родился-gpt">Чем плох MBR (и зачем родился GPT)</h2>
<ul>
<li>Потолок 2 ТиБ — на современных дисках это ровно половина</li>
<li>Только 4 первичных раздела, остальное — через костыль с EBR</li>
<li>Нет контрольных сумм — один битфлип в нужном месте, и таблица «уехала», а ОС об этом не узнает</li>
<li>Нет резервной копии — затёртый сектор 0 равняется диску-«кирпичу» для BIOS</li>
<li>Тип раздела — 1 байт без пространства имён, поэтому коды конфликтуют между ОС и существуют по сути «по договорённости»</li>
</ul>
<p>Подробнее про преемника — в <a href="/posts/what-is-gpt/">Как выглядит GPT</a>.</p>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong>CHS vs LBA</strong>: до сих пор встречаются прошивки, которые читают именно CHS. Если поставить туда «битые» значения, BIOS может отказаться грузиться даже при корректном LBA. Утилиты обычно сами пишут <code>0xFFFFFF</code> в CHS, но если правил руками — проверь</li>
<li><strong>Активный раздел</strong>: <code>0x80</code> должен быть <strong>только у одного</strong> раздела. Несколько активных — поведение зависит от загрузчика, и бывает классическое «грузится не то, что ожидал»</li>
<li><strong>MBR на USB</strong>: некоторые BIOS грузятся только если на флешке <code>0x80</code> стоит и тип — <code>0x0C</code>/<code>0x0B</code>. Нарезал через <code>dd</code> ISO-образ, а флешка не грузится — проверь именно это</li>
<li><strong>Hybrid MBR на GPT-дисках</strong>: записи в Protective MBR подменяют на «настоящие», чтобы BIOS-only ОС увидели разделы. Жуткий хак, рассинхрон с GPT ломает всё разом — лучше не использовать, но если унаследовал — знай, что оно есть</li>
<li><strong>NT Disk Signature и клонирование</strong>: после <code>dd</code> диска побайтово обязательно меняй подпись, иначе Windows унесёт один из дисков в offline. На Linux-only стенде проблемы нет, но как только подключишь к Windows — словишь весёлый сюрприз</li>
</ul>
<p>Практическая сторона вопроса — <a href="/posts/gpt-to-mbr/">конвертация GPT в MBR через <code>diskpart</code></a>, когда старая Windows отказывается ставиться.</p>
<p>На этом всё. MBR — простая структура из эпохи, когда диски измерялись в мегабайтах, а 4 разделов хватало всем. Сегодня его удел — старые системы и роль Protective MBR на GPT-дисках, но базу знать всё ещё полезно: на боевой железке в 3 часа ночи хочется понимать, что именно ты сейчас правишь в <code>dd</code> и <code>hexdump</code>.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Как выглядит GPT</title>
      <link>https://jtprog.ru/posts/what-is-gpt/</link>
      <pubDate>Tue, 05 May 2026 20:36:01 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/what-is-gpt/</guid>
      <category>GPT</category>
      <category>Disks</category>
      <category>Разметка Диска</category>
      <category>Linux</category>
      <category>Администрирование</category>
      <category domain="category">OS</category>
      <enclosure url="https://jtprog.ru/OS.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/OS.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Когда ты говоришь «разбить диск на разделы», за этим стоит вполне конкретная структура в нулевых секторах накопителя — и в современных машинах это почти всегда GPT, а не его легендарный предок <a href="/posts/what-is-mbr/">MBR</a>. Снаружи видны только <code>/dev/sda1</code>, <code>/dev/nvme0n1p3</code> и буквы дисков, но если заглянуть внутрь — там аккуратный заголовок, таблица записей по 128 байт и вторая копия в самом хвосте диска «на случай чего».</p>
<p>В этой шпаргалке разберём по байтам, как устроен GPT-диск: какие поля где лежат, зачем нужны Protective MBR и backup-копия, и что со всем этим можно делать руками, когда «оно вдруг не грузится».</p>
<h2 id="что-такое-gpt-и-зачем-он-появился">Что такое GPT и зачем он появился</h2>
<p><strong>GPT (GUID Partition Table)</strong> — современная схема разметки диска, часть спецификации <strong>UEFI</strong>. Пришла на смену <a href="/posts/what-is-mbr/">MBR</a>, у которого было два бесивших ограничения: максимум 4 первичных раздела и потолок в 2 ТБ из-за 32-битной адресации LBA. GPT сносит оба разом: 64-битная адресация (теоретически — до 9.4 ZB, на практике — сколько у тебя денег на диски) и 128 разделов «из коробки».</p>
<p>Ключевая идея GPT — <strong>избыточность и самопроверка</strong>. Заголовок и таблица разделов хранятся в двух копиях: одна в начале диска, вторая — в самом конце. Всё защищено CRC32. Первая копия побилась? Система спокойно прочитает вторую и пойдёт дальше, как будто ничего и не было.</p>
<h2 id="карта-диска">Карта диска</h2>
<p>Если смотреть «сверху вниз», GPT-диск выглядит так:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>LBA 0          : Protective MBR              (1 сектор)
LBA 1          : Primary GPT Header          (1 сектор)
LBA 2..33      : Primary Partition Entries   (32 сектора, 128 записей × 128 байт)
LBA 34..N-34   : Сами разделы (данные)
LBA N-33..N-1  : Backup Partition Entries    (32 сектора)
LBA N          : Backup GPT Header           (последний сектор диска)</code></pre>
</div>
<p><code>N</code> — последний LBA на диске. Размер сектора — обычно 512 байт, на современных «Advanced Format / 4Kn» дисках — 4096. Это важно: вся арифметика смещений считается в секторах, и на 4Kn-диске массив записей займёт уже не 32 сектора, а 8.</p>
<h2 id="protective-mbr-lba-0">Protective MBR (LBA 0)</h2>
<p>Первый сектор — фейковый MBR. Нужен ровно для одного: чтобы старые утилиты, не знающие про GPT, увидели «занятый» диск с одним разделом типа <code>0xEE</code> на весь объём и не предложили его «отформатировать как пустой». По сути это табличка «здесь уже что-то есть, не лезь».</p>
<p>Внутри — стандартная структура MBR, но с единственной осмысленной записью:</p>
<ul>
<li><code>Boot indicator</code>: <code>0x00</code></li>
<li><code>Starting CHS</code>: <code>0x000200</code></li>
<li><code>OS type</code>: <code>0xEE</code> (GPT Protective)</li>
<li><code>Ending CHS</code>: <code>0xFFFFFF</code></li>
<li><code>Starting LBA</code>: <code>0x00000001</code></li>
<li><code>Size in LBA</code>: <code>0xFFFFFFFF</code> или фактический размер диска (если он меньше 2 ТиБ)</li>
</ul>
<h2 id="primary-gpt-header-lba-1">Primary GPT Header (LBA 1)</h2>
<p>Это сердце разметки. Размер — 92 байта (остальное в секторе — нули). Структура:</p>
<table>
	<thead>
			<tr>
					<th style="text-align: right">Offset</th>
					<th style="text-align: right">Size</th>
					<th>Поле</th>
					<th>Описание</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: right">0</td>
					<td style="text-align: right">8</td>
					<td><code>Signature</code></td>
					<td>Магия <code>EFI PART</code> (<code>0x5452415020494645</code>)</td>
			</tr>
			<tr>
					<td style="text-align: right">8</td>
					<td style="text-align: right">4</td>
					<td><code>Revision</code></td>
					<td>Версия, обычно <code>0x00010000</code> (1.0)</td>
			</tr>
			<tr>
					<td style="text-align: right">12</td>
					<td style="text-align: right">4</td>
					<td><code>HeaderSize</code></td>
					<td>Размер заголовка в байтах, обычно 92</td>
			</tr>
			<tr>
					<td style="text-align: right">16</td>
					<td style="text-align: right">4</td>
					<td><code>HeaderCRC32</code></td>
					<td>CRC32 самого заголовка (с обнулённым этим полем при расчёте)</td>
			</tr>
			<tr>
					<td style="text-align: right">20</td>
					<td style="text-align: right">4</td>
					<td><code>Reserved</code></td>
					<td>Должно быть 0</td>
			</tr>
			<tr>
					<td style="text-align: right">24</td>
					<td style="text-align: right">8</td>
					<td><code>MyLBA</code></td>
					<td>LBA, где лежит этот заголовок (1 для primary)</td>
			</tr>
			<tr>
					<td style="text-align: right">32</td>
					<td style="text-align: right">8</td>
					<td><code>AlternateLBA</code></td>
					<td>LBA резервного заголовка (последний сектор)</td>
			</tr>
			<tr>
					<td style="text-align: right">40</td>
					<td style="text-align: right">8</td>
					<td><code>FirstUsableLBA</code></td>
					<td>Первый LBA, доступный для разделов (обычно 34)</td>
			</tr>
			<tr>
					<td style="text-align: right">48</td>
					<td style="text-align: right">8</td>
					<td><code>LastUsableLBA</code></td>
					<td>Последний LBA для разделов (N − 34)</td>
			</tr>
			<tr>
					<td style="text-align: right">56</td>
					<td style="text-align: right">16</td>
					<td><code>DiskGUID</code></td>
					<td>Уникальный идентификатор диска</td>
			</tr>
			<tr>
					<td style="text-align: right">72</td>
					<td style="text-align: right">8</td>
					<td><code>PartitionEntryLBA</code></td>
					<td>LBA, с которого начинается таблица записей (2)</td>
			</tr>
			<tr>
					<td style="text-align: right">80</td>
					<td style="text-align: right">4</td>
					<td><code>NumberOfPartitionEntries</code></td>
					<td>Сколько записей выделено (обычно 128)</td>
			</tr>
			<tr>
					<td style="text-align: right">84</td>
					<td style="text-align: right">4</td>
					<td><code>SizeOfPartitionEntry</code></td>
					<td>Размер одной записи (обычно 128)</td>
			</tr>
			<tr>
					<td style="text-align: right">88</td>
					<td style="text-align: right">4</td>
					<td><code>PartitionEntryArrayCRC32</code></td>
					<td>CRC32 всего массива записей</td>
			</tr>
	</tbody>
</table>
<p>Два CRC — отдельная проверка для самого заголовка и отдельная для массива записей. Если хоть один не сходится — UEFI идёт за резервной копией. Никаких «оно как-то загрузилось со сбойной таблицей» — либо всё чисто, либо берём backup.</p>
<h2 id="partition-entry-запись-о-разделе">Partition Entry (запись о разделе)</h2>
<p>Каждая запись занимает 128 байт. По умолчанию таблица содержит 128 записей → 16 КБ на массив. Это и есть тот самый «лимит в 128 разделов из коробки» — чисто административное ограничение, в спеке его можно поднять.</p>
<table>
	<thead>
			<tr>
					<th style="text-align: right">Offset</th>
					<th style="text-align: right">Size</th>
					<th>Поле</th>
					<th>Описание</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: right">0</td>
					<td style="text-align: right">16</td>
					<td><code>PartitionTypeGUID</code></td>
					<td>Тип раздела (например, EFI System, Linux filesystem, Microsoft Basic Data)</td>
			</tr>
			<tr>
					<td style="text-align: right">16</td>
					<td style="text-align: right">16</td>
					<td><code>UniquePartitionGUID</code></td>
					<td>Уникальный GUID конкретного раздела</td>
			</tr>
			<tr>
					<td style="text-align: right">32</td>
					<td style="text-align: right">8</td>
					<td><code>StartingLBA</code></td>
					<td>Первый LBA раздела</td>
			</tr>
			<tr>
					<td style="text-align: right">40</td>
					<td style="text-align: right">8</td>
					<td><code>EndingLBA</code></td>
					<td>Последний LBA раздела (включительно)</td>
			</tr>
			<tr>
					<td style="text-align: right">48</td>
					<td style="text-align: right">8</td>
					<td><code>Attributes</code></td>
					<td>Битовые атрибуты (см. ниже)</td>
			</tr>
			<tr>
					<td style="text-align: right">56</td>
					<td style="text-align: right">72</td>
					<td><code>PartitionName</code></td>
					<td>UTF-16LE имя раздела, до 36 символов</td>
			</tr>
	</tbody>
</table>
<p>Если запись «пустая» — <code>PartitionTypeGUID</code> равен нулям. Никаких флажков «эта запись валидна» — нули в типе и есть «меня нет».</p>
<h3 id="известные-partitiontypeguid">Известные <code>PartitionTypeGUID</code></h3>
<p>Запоминать их наизусть смысла нет, но в <code>/etc/fstab</code> или в выводе <code>gdisk</code> они мелькают регулярно:</p>
<ul>
<li><code>C12A7328-F81F-11D2-BA4B-00A0C93EC93B</code> — EFI System Partition (ESP), та самая, с которой грузится UEFI</li>
<li><code>0FC63DAF-8483-4772-8E79-3D69D8477DE4</code> — Linux filesystem</li>
<li><code>E6D6D379-F507-44C2-A23C-238F2A3DF928</code> — Linux LVM</li>
<li><code>0657FD6D-A4AB-43C4-84E5-0933C84B4F4F</code> — Linux swap</li>
<li><code>EBD0A0A2-B9E5-4433-87C0-68B6B72699C7</code> — Microsoft Basic Data (под этим GUID живут и NTFS, и exFAT, и FAT32 — ОС определяет уже по сигнатуре ФС)</li>
<li><code>48465300-0000-11AA-AA11-00306543ECAC</code> — Apple HFS+</li>
<li><code>7C3457EF-0000-11AA-AA11-00306543ECAC</code> — Apple APFS</li>
</ul>
<h3 id="атрибуты-биты">Атрибуты (биты)</h3>
<ul>
<li><strong>Bit 0</strong> — <code>Required Partition</code>. Системный раздел платформы, руками не трогаем</li>
<li><strong>Bit 1</strong> — <code>No Block IO Protocol</code>. UEFI не выдаёт обработчик блочного I/O</li>
<li><strong>Bit 2</strong> — <code>Legacy BIOS Bootable</code>. Рудимент для совместимости</li>
<li><strong>Биты 48–63</strong> — определяются конкретным типом раздела. Например, для Microsoft Basic Data: 60 — read-only, 62 — hidden, 63 — no drive letter (тот самый трюк, которым Windows прячет свой recovery-раздел)</li>
</ul>
<h2 id="backup-gpt-хвост-диска">Backup GPT (хвост диска)</h2>
<p>В последних 33 секторах диска лежат <strong>те же</strong> структуры, но в обратном порядке: сначала backup partition entries, потом backup header. У backup header <code>MyLBA</code> указывает на последний сектор, <code>AlternateLBA</code> — обратно на 1.</p>
<p>Это спасает в классической ситуации «кто-то умный сделал <code>dd if=/dev/zero of=/dev/sda bs=512 count=1</code> и пошёл пить чай». Primary улетел, диск превратился в кирпич — но <code>gdisk</code> спокойно увидит backup в конце и предложит восстановить начало. С MBR такой фокус не пройдёт: там единственная копия, и затёртый сектор 0 = всё, грузиться нечем.</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<ul>
<li><strong>Посмотреть разметку</strong>: <code>gdisk -l /dev/sda</code>, <code>parted /dev/sda print</code>, <code>lsblk -o NAME,PARTUUID,PARTLABEL,PARTTYPE</code></li>
<li><strong>Сырой дамп заголовка</strong>: <code>dd if=/dev/sda bs=512 count=1 skip=1 | hexdump -C</code> — увидишь <code>EFI PART</code> в первых байтах, дальше поля по таблице выше</li>
<li><strong>PARTUUID vs UUID</strong> — частая путаница. <code>PARTUUID</code> — это <code>UniquePartitionGUID</code> из GPT-записи, живёт в самой разметке. <code>UUID</code> — внутри файловой системы. В <code>/etc/fstab</code> для смонтированных ФС обычно используют <code>UUID</code>, а для raw-разделов или swap иногда удобнее <code>PARTUUID</code> (он не меняется при <code>mkfs</code>)</li>
<li><strong>Восстановить primary из backup</strong>: <code>sgdisk --backup=...</code> / <code>sgdisk --load-backup=...</code>, или интерактивно <code>gdisk</code> → <code>recovery and transformation options</code> → дальше по подсказкам</li>
</ul>
<h2 id="чем-gpt-лучше-mbr">Чем GPT лучше MBR</h2>
<ul>
<li>64-битные LBA → разделы и диски больше 2 ТиБ</li>
<li>128 разделов «из коробки», без всех этих primary/extended/logical костылей</li>
<li>CRC32 на заголовок и таблицу → повреждение детектируется, а не «молча работает с битой таблицей»</li>
<li>Резервная копия в конце диска — спасение в 90% случаев «затёр начало»</li>
<li>У каждого раздела есть свой GUID и читаемое имя. Можно ссылаться по <code>PARTUUID</code>/<code>PARTLABEL</code>, а не по <code>/dev/sdaX</code>, который меняется при перетыкании дисков и переименовании контроллера</li>
<li>Часть UEFI-стека: ESP с типом <code>C12A7328-...</code> — стандартный способ грузиться, без танцев с активным разделом</li>
</ul>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong>Гибридные MBR</strong> (например, старые Mac с Boot Camp) — это хак: внутри Protective MBR сделали реальные записи, дублирующие GPT. Опасно ровно тем, что рассинхрон между двумя таблицами ломает загрузчик. По возможности — не использовать</li>
<li><strong>4Kn-диски</strong>: ещё раз напомню, что вся арифметика в секторах. На 4Kn таблица записей занимает 8 секторов вместо 32. Утилиты обычно справляются сами, но если правишь руками — проверь размер сектора через <code>blockdev --getss /dev/sda</code></li>
<li><strong>CRC ≠ подпись</strong>: GPT защищает от случайных битфлипов, но не от целенаправленной модификации. Кто угодно может пересчитать CRC после правки — это не криптография</li>
<li><strong><code>sgdisk -Z</code></strong> обнуляет обе копии разом. Если потом окажется, что данные ещё были нужны — восстановления через backup уже нет, придётся идти к <code>testdisk</code> и молиться</li>
<li><strong>Resize и <code>LastUsableLBA</code></strong>: после <code>dd</code>-клона на больший диск backup останется на старом месте, а <code>LastUsableLBA</code> будет указывать в середину нового диска. <code>sgdisk -e /dev/sda</code> переносит backup в конец и обновляет поля</li>
</ul>
<p>Из соседнего: если надо не разобраться, а срочно <a href="/posts/gpt-to-mbr/">сделать MBR из GPT перед установкой Windows</a> — там короткая памятка по <code>diskpart</code>.</p>
<p>На этом всё. Ничего магического в GPT нет — обычная структура данных, которую можно прочитать <code>dd | hexdump</code>. Зато когда снова придётся вытаскивать систему из «оно не грузится», понимать, что именно лежит в первых 33 секторах диска, очень помогает.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Когда ты говоришь «разбить диск на разделы», за этим стоит вполне конкретная структура в нулевых секторах накопителя — и в современных машинах это почти всегда GPT, а не его легендарный предок <a href="/posts/what-is-mbr/">MBR</a>. Снаружи видны только <code>/dev/sda1</code>, <code>/dev/nvme0n1p3</code> и буквы дисков, но если заглянуть внутрь — там аккуратный заголовок, таблица записей по 128 байт и вторая копия в самом хвосте диска «на случай чего».</p>
<p>В этой шпаргалке разберём по байтам, как устроен GPT-диск: какие поля где лежат, зачем нужны Protective MBR и backup-копия, и что со всем этим можно делать руками, когда «оно вдруг не грузится».</p>
<h2 id="что-такое-gpt-и-зачем-он-появился">Что такое GPT и зачем он появился</h2>
<p><strong>GPT (GUID Partition Table)</strong> — современная схема разметки диска, часть спецификации <strong>UEFI</strong>. Пришла на смену <a href="/posts/what-is-mbr/">MBR</a>, у которого было два бесивших ограничения: максимум 4 первичных раздела и потолок в 2 ТБ из-за 32-битной адресации LBA. GPT сносит оба разом: 64-битная адресация (теоретически — до 9.4 ZB, на практике — сколько у тебя денег на диски) и 128 разделов «из коробки».</p>
<p>Ключевая идея GPT — <strong>избыточность и самопроверка</strong>. Заголовок и таблица разделов хранятся в двух копиях: одна в начале диска, вторая — в самом конце. Всё защищено CRC32. Первая копия побилась? Система спокойно прочитает вторую и пойдёт дальше, как будто ничего и не было.</p>
<h2 id="карта-диска">Карта диска</h2>
<p>Если смотреть «сверху вниз», GPT-диск выглядит так:</p>
<div class="codeblock"><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><pre tabindex="0"><code>LBA 0          : Protective MBR              (1 сектор)
LBA 1          : Primary GPT Header          (1 сектор)
LBA 2..33      : Primary Partition Entries   (32 сектора, 128 записей × 128 байт)
LBA 34..N-34   : Сами разделы (данные)
LBA N-33..N-1  : Backup Partition Entries    (32 сектора)
LBA N          : Backup GPT Header           (последний сектор диска)</code></pre>
</div>
<p><code>N</code> — последний LBA на диске. Размер сектора — обычно 512 байт, на современных «Advanced Format / 4Kn» дисках — 4096. Это важно: вся арифметика смещений считается в секторах, и на 4Kn-диске массив записей займёт уже не 32 сектора, а 8.</p>
<h2 id="protective-mbr-lba-0">Protective MBR (LBA 0)</h2>
<p>Первый сектор — фейковый MBR. Нужен ровно для одного: чтобы старые утилиты, не знающие про GPT, увидели «занятый» диск с одним разделом типа <code>0xEE</code> на весь объём и не предложили его «отформатировать как пустой». По сути это табличка «здесь уже что-то есть, не лезь».</p>
<p>Внутри — стандартная структура MBR, но с единственной осмысленной записью:</p>
<ul>
<li><code>Boot indicator</code>: <code>0x00</code></li>
<li><code>Starting CHS</code>: <code>0x000200</code></li>
<li><code>OS type</code>: <code>0xEE</code> (GPT Protective)</li>
<li><code>Ending CHS</code>: <code>0xFFFFFF</code></li>
<li><code>Starting LBA</code>: <code>0x00000001</code></li>
<li><code>Size in LBA</code>: <code>0xFFFFFFFF</code> или фактический размер диска (если он меньше 2 ТиБ)</li>
</ul>
<h2 id="primary-gpt-header-lba-1">Primary GPT Header (LBA 1)</h2>
<p>Это сердце разметки. Размер — 92 байта (остальное в секторе — нули). Структура:</p>
<table>
	<thead>
			<tr>
					<th style="text-align: right">Offset</th>
					<th style="text-align: right">Size</th>
					<th>Поле</th>
					<th>Описание</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: right">0</td>
					<td style="text-align: right">8</td>
					<td><code>Signature</code></td>
					<td>Магия <code>EFI PART</code> (<code>0x5452415020494645</code>)</td>
			</tr>
			<tr>
					<td style="text-align: right">8</td>
					<td style="text-align: right">4</td>
					<td><code>Revision</code></td>
					<td>Версия, обычно <code>0x00010000</code> (1.0)</td>
			</tr>
			<tr>
					<td style="text-align: right">12</td>
					<td style="text-align: right">4</td>
					<td><code>HeaderSize</code></td>
					<td>Размер заголовка в байтах, обычно 92</td>
			</tr>
			<tr>
					<td style="text-align: right">16</td>
					<td style="text-align: right">4</td>
					<td><code>HeaderCRC32</code></td>
					<td>CRC32 самого заголовка (с обнулённым этим полем при расчёте)</td>
			</tr>
			<tr>
					<td style="text-align: right">20</td>
					<td style="text-align: right">4</td>
					<td><code>Reserved</code></td>
					<td>Должно быть 0</td>
			</tr>
			<tr>
					<td style="text-align: right">24</td>
					<td style="text-align: right">8</td>
					<td><code>MyLBA</code></td>
					<td>LBA, где лежит этот заголовок (1 для primary)</td>
			</tr>
			<tr>
					<td style="text-align: right">32</td>
					<td style="text-align: right">8</td>
					<td><code>AlternateLBA</code></td>
					<td>LBA резервного заголовка (последний сектор)</td>
			</tr>
			<tr>
					<td style="text-align: right">40</td>
					<td style="text-align: right">8</td>
					<td><code>FirstUsableLBA</code></td>
					<td>Первый LBA, доступный для разделов (обычно 34)</td>
			</tr>
			<tr>
					<td style="text-align: right">48</td>
					<td style="text-align: right">8</td>
					<td><code>LastUsableLBA</code></td>
					<td>Последний LBA для разделов (N − 34)</td>
			</tr>
			<tr>
					<td style="text-align: right">56</td>
					<td style="text-align: right">16</td>
					<td><code>DiskGUID</code></td>
					<td>Уникальный идентификатор диска</td>
			</tr>
			<tr>
					<td style="text-align: right">72</td>
					<td style="text-align: right">8</td>
					<td><code>PartitionEntryLBA</code></td>
					<td>LBA, с которого начинается таблица записей (2)</td>
			</tr>
			<tr>
					<td style="text-align: right">80</td>
					<td style="text-align: right">4</td>
					<td><code>NumberOfPartitionEntries</code></td>
					<td>Сколько записей выделено (обычно 128)</td>
			</tr>
			<tr>
					<td style="text-align: right">84</td>
					<td style="text-align: right">4</td>
					<td><code>SizeOfPartitionEntry</code></td>
					<td>Размер одной записи (обычно 128)</td>
			</tr>
			<tr>
					<td style="text-align: right">88</td>
					<td style="text-align: right">4</td>
					<td><code>PartitionEntryArrayCRC32</code></td>
					<td>CRC32 всего массива записей</td>
			</tr>
	</tbody>
</table>
<p>Два CRC — отдельная проверка для самого заголовка и отдельная для массива записей. Если хоть один не сходится — UEFI идёт за резервной копией. Никаких «оно как-то загрузилось со сбойной таблицей» — либо всё чисто, либо берём backup.</p>
<h2 id="partition-entry-запись-о-разделе">Partition Entry (запись о разделе)</h2>
<p>Каждая запись занимает 128 байт. По умолчанию таблица содержит 128 записей → 16 КБ на массив. Это и есть тот самый «лимит в 128 разделов из коробки» — чисто административное ограничение, в спеке его можно поднять.</p>
<table>
	<thead>
			<tr>
					<th style="text-align: right">Offset</th>
					<th style="text-align: right">Size</th>
					<th>Поле</th>
					<th>Описание</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: right">0</td>
					<td style="text-align: right">16</td>
					<td><code>PartitionTypeGUID</code></td>
					<td>Тип раздела (например, EFI System, Linux filesystem, Microsoft Basic Data)</td>
			</tr>
			<tr>
					<td style="text-align: right">16</td>
					<td style="text-align: right">16</td>
					<td><code>UniquePartitionGUID</code></td>
					<td>Уникальный GUID конкретного раздела</td>
			</tr>
			<tr>
					<td style="text-align: right">32</td>
					<td style="text-align: right">8</td>
					<td><code>StartingLBA</code></td>
					<td>Первый LBA раздела</td>
			</tr>
			<tr>
					<td style="text-align: right">40</td>
					<td style="text-align: right">8</td>
					<td><code>EndingLBA</code></td>
					<td>Последний LBA раздела (включительно)</td>
			</tr>
			<tr>
					<td style="text-align: right">48</td>
					<td style="text-align: right">8</td>
					<td><code>Attributes</code></td>
					<td>Битовые атрибуты (см. ниже)</td>
			</tr>
			<tr>
					<td style="text-align: right">56</td>
					<td style="text-align: right">72</td>
					<td><code>PartitionName</code></td>
					<td>UTF-16LE имя раздела, до 36 символов</td>
			</tr>
	</tbody>
</table>
<p>Если запись «пустая» — <code>PartitionTypeGUID</code> равен нулям. Никаких флажков «эта запись валидна» — нули в типе и есть «меня нет».</p>
<h3 id="известные-partitiontypeguid">Известные <code>PartitionTypeGUID</code></h3>
<p>Запоминать их наизусть смысла нет, но в <code>/etc/fstab</code> или в выводе <code>gdisk</code> они мелькают регулярно:</p>
<ul>
<li><code>C12A7328-F81F-11D2-BA4B-00A0C93EC93B</code> — EFI System Partition (ESP), та самая, с которой грузится UEFI</li>
<li><code>0FC63DAF-8483-4772-8E79-3D69D8477DE4</code> — Linux filesystem</li>
<li><code>E6D6D379-F507-44C2-A23C-238F2A3DF928</code> — Linux LVM</li>
<li><code>0657FD6D-A4AB-43C4-84E5-0933C84B4F4F</code> — Linux swap</li>
<li><code>EBD0A0A2-B9E5-4433-87C0-68B6B72699C7</code> — Microsoft Basic Data (под этим GUID живут и NTFS, и exFAT, и FAT32 — ОС определяет уже по сигнатуре ФС)</li>
<li><code>48465300-0000-11AA-AA11-00306543ECAC</code> — Apple HFS+</li>
<li><code>7C3457EF-0000-11AA-AA11-00306543ECAC</code> — Apple APFS</li>
</ul>
<h3 id="атрибуты-биты">Атрибуты (биты)</h3>
<ul>
<li><strong>Bit 0</strong> — <code>Required Partition</code>. Системный раздел платформы, руками не трогаем</li>
<li><strong>Bit 1</strong> — <code>No Block IO Protocol</code>. UEFI не выдаёт обработчик блочного I/O</li>
<li><strong>Bit 2</strong> — <code>Legacy BIOS Bootable</code>. Рудимент для совместимости</li>
<li><strong>Биты 48–63</strong> — определяются конкретным типом раздела. Например, для Microsoft Basic Data: 60 — read-only, 62 — hidden, 63 — no drive letter (тот самый трюк, которым Windows прячет свой recovery-раздел)</li>
</ul>
<h2 id="backup-gpt-хвост-диска">Backup GPT (хвост диска)</h2>
<p>В последних 33 секторах диска лежат <strong>те же</strong> структуры, но в обратном порядке: сначала backup partition entries, потом backup header. У backup header <code>MyLBA</code> указывает на последний сектор, <code>AlternateLBA</code> — обратно на 1.</p>
<p>Это спасает в классической ситуации «кто-то умный сделал <code>dd if=/dev/zero of=/dev/sda bs=512 count=1</code> и пошёл пить чай». Primary улетел, диск превратился в кирпич — но <code>gdisk</code> спокойно увидит backup в конце и предложит восстановить начало. С MBR такой фокус не пройдёт: там единственная копия, и затёртый сектор 0 = всё, грузиться нечем.</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<ul>
<li><strong>Посмотреть разметку</strong>: <code>gdisk -l /dev/sda</code>, <code>parted /dev/sda print</code>, <code>lsblk -o NAME,PARTUUID,PARTLABEL,PARTTYPE</code></li>
<li><strong>Сырой дамп заголовка</strong>: <code>dd if=/dev/sda bs=512 count=1 skip=1 | hexdump -C</code> — увидишь <code>EFI PART</code> в первых байтах, дальше поля по таблице выше</li>
<li><strong>PARTUUID vs UUID</strong> — частая путаница. <code>PARTUUID</code> — это <code>UniquePartitionGUID</code> из GPT-записи, живёт в самой разметке. <code>UUID</code> — внутри файловой системы. В <code>/etc/fstab</code> для смонтированных ФС обычно используют <code>UUID</code>, а для raw-разделов или swap иногда удобнее <code>PARTUUID</code> (он не меняется при <code>mkfs</code>)</li>
<li><strong>Восстановить primary из backup</strong>: <code>sgdisk --backup=...</code> / <code>sgdisk --load-backup=...</code>, или интерактивно <code>gdisk</code> → <code>recovery and transformation options</code> → дальше по подсказкам</li>
</ul>
<h2 id="чем-gpt-лучше-mbr">Чем GPT лучше MBR</h2>
<ul>
<li>64-битные LBA → разделы и диски больше 2 ТиБ</li>
<li>128 разделов «из коробки», без всех этих primary/extended/logical костылей</li>
<li>CRC32 на заголовок и таблицу → повреждение детектируется, а не «молча работает с битой таблицей»</li>
<li>Резервная копия в конце диска — спасение в 90% случаев «затёр начало»</li>
<li>У каждого раздела есть свой GUID и читаемое имя. Можно ссылаться по <code>PARTUUID</code>/<code>PARTLABEL</code>, а не по <code>/dev/sdaX</code>, который меняется при перетыкании дисков и переименовании контроллера</li>
<li>Часть UEFI-стека: ESP с типом <code>C12A7328-...</code> — стандартный способ грузиться, без танцев с активным разделом</li>
</ul>
<h2 id="тонкие-места-на-которых-легко-споткнуться">Тонкие места, на которых легко споткнуться</h2>
<ul>
<li><strong>Гибридные MBR</strong> (например, старые Mac с Boot Camp) — это хак: внутри Protective MBR сделали реальные записи, дублирующие GPT. Опасно ровно тем, что рассинхрон между двумя таблицами ломает загрузчик. По возможности — не использовать</li>
<li><strong>4Kn-диски</strong>: ещё раз напомню, что вся арифметика в секторах. На 4Kn таблица записей занимает 8 секторов вместо 32. Утилиты обычно справляются сами, но если правишь руками — проверь размер сектора через <code>blockdev --getss /dev/sda</code></li>
<li><strong>CRC ≠ подпись</strong>: GPT защищает от случайных битфлипов, но не от целенаправленной модификации. Кто угодно может пересчитать CRC после правки — это не криптография</li>
<li><strong><code>sgdisk -Z</code></strong> обнуляет обе копии разом. Если потом окажется, что данные ещё были нужны — восстановления через backup уже нет, придётся идти к <code>testdisk</code> и молиться</li>
<li><strong>Resize и <code>LastUsableLBA</code></strong>: после <code>dd</code>-клона на больший диск backup останется на старом месте, а <code>LastUsableLBA</code> будет указывать в середину нового диска. <code>sgdisk -e /dev/sda</code> переносит backup в конец и обновляет поля</li>
</ul>
<p>Из соседнего: если надо не разобраться, а срочно <a href="/posts/gpt-to-mbr/">сделать MBR из GPT перед установкой Windows</a> — там короткая памятка по <code>diskpart</code>.</p>
<p>На этом всё. Ничего магического в GPT нет — обычная структура данных, которую можно прочитать <code>dd | hexdump</code>. Зато когда снова придётся вытаскивать систему из «оно не грузится», понимать, что именно лежит в первых 33 секторах диска, очень помогает.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Скорость сгорания бюджета ошибок — что тут не так?</title>
      <link>https://jtprog.ru/posts/burn-rate-is-not-speed/</link>
      <pubDate>Sun, 26 Apr 2026 20:43:02 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/burn-rate-is-not-speed/</guid>
      <category>SRE</category>
      <category>Error Budget</category>
      <category>Reliability</category>
      <category>Мониторинг</category>
      <category domain="category">SRE</category>
      <enclosure url="https://jtprog.ru/cover.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/cover.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! В индустрии прижился термин «скорость сгорания бюджета ошибок» (Error Budget Burn Rate). Однако слово «скорость» здесь часто вводит в заблуждение, заставляя мозг искать привычные физические размерности там, где их нет.</p>
<p>Цена этой путаницы вполне материальна. Алерт, настроенный на абсолютную интенсивность ошибок, на ночном спаде трафика молчит весь инцидент, а на утреннем пике будит дежурного на ровном месте. Разберемся, почему Burn Rate — это в первую очередь <strong>безразмерный коэффициент</strong>, и как из него собирается алерт, который срабатывает по делу.</p>
<h2 id="начнем-с-математики">Начнем с математики</h2>
<p>Сейчас мы посчитаем два числа: сам бюджет ошибок и интенсивность, с которой его можно тратить. Ни одно из них не будет Burn Rate — и вот это как раз самое интересное.</p>
<p>Представим сервис со стабильной нагрузкой в <strong>1 000 запросов в минуту</strong> (req/min).</p>
<p>Мы установили <strong>SLO = 99.9%</strong>.</p>
<ol>
<li><strong>Определяем бюджет ошибок:</strong> <span class="math-inline" data-png="https://jtprog.ru/math/320d496cd5586944237fbd8ef42fa2a8684b33d66f58d8a20576a77e464cf280.png" data-w="304" data-h="32"><span class="math__tex">\(\text{Error Budget} = 1 - \text{SLO} = 0.1\%\)</span></span> (или <span class="math-inline" data-png="https://jtprog.ru/math/3b6d5b7be5e3bc670788f43343a2e7d88c1a3709d90ecfeea1e4089b71984d7f.png" data-w="58" data-h="32"><span class="math__tex">\(0.001\)</span></span> в долях).</li>
<li><strong>Считаем бюджет в абсолютных величинах:</strong>
<ul>
<li>За 30-дневное окно общее количество запросов составит: <span class="math-inline" data-png="https://jtprog.ru/math/ce447e5a79d95122e528c41b326d8bf0a858720f390decf5cf7af8105c5d47c3.png" data-w="657" data-h="32"><span class="math__tex">\(30 \text{ дней} \times 24 \text{ часа} \times 60 \text{ минут} \times 1\,000 \text{ req/min} = 43\,200\,000 \text{ запросов}\)</span></span>.</li>
<li>Допустимое количество ошибок: <span class="math-inline" data-png="https://jtprog.ru/math/9aaeeecb7e20c7544361dce1ce76a8ed2a1d99645506fee734a294e0680c907e.png" data-w="349" data-h="33"><span class="math__tex">\(43\,200\,000 \times 0.001 = \mathbf{43\,200 \text{ ошибок}}\)</span></span>.</li>
</ul>
</li>
</ol>
<p>Это наш исчерпаемый ресурс на 30 дней.</p>
<h3 id="как-можно-тратить-этот-бюджет">Как можно тратить этот бюджет?</h3>
<p>Мы можем распределить эти 43 200 ошибок равномерно по всему месяцу. Чтобы понять «нормальную» интенсивность ошибок, разделим бюджет на время:</p>
<div class="math-block" data-png="https://jtprog.ru/math/db98932414e57fe8990d2451851a58744bf8dd06d976dcd89c251f79b1894bda.png"><span class="math__tex">$$\frac{43\,200 \text{ ошибок}}{43\,200 \text{ минут}} = 1 \text{ ошибка в минуту}$$</span></div><p>Это — <strong>Absolute Rate</strong> (абсолютный показатель). У него есть размерность (ошибки/мин), и он намертво привязан к текущему трафику: на ночном спаде в 100 req/min та же одна ошибка в минуту означает уже десятикратное превышение нормы. Полезное число, но это всё еще не Burn Rate.</p>
<h2 id="формула-коэффициента">Формула коэффициента</h2>
<p>Burn Rate считается не через абсолютные значения, а через доли:</p>
<div class="math-block" data-png="https://jtprog.ru/math/7d3c05c1e67128d68491edf35f978da33027396adc89d250d4ee36202b45ae1a.png"><span class="math__tex">$$\text{Burn Rate} = \frac{\text{Фактическая доля ошибок (за интервал)}}{\text{Допустимая доля ошибок (по SLO)}}$$</span></div><p>Возьмем наш сервис с SLO 99.9% (допустимо 0.1% ошибок). Если за последние 5 минут мы поймали 2% ошибок:</p>
<div class="math-block" data-png="https://jtprog.ru/math/fc4067afc10670216ac0043a957a14072d28610707d3a1b99337c32b886c7eec.png"><span class="math__tex">$$\text{Burn Rate} = \frac{0.02}{0.001} = 20\times$$</span></div><p>Обрати внимание на то, что произошло с единицами измерения. И в числителе, и в знаменателе стоит одна и та же величина — доля неуспешных запросов от общего числа. При делении они сокращаются, и на выходе остается голое число. Не «двадцать чего-то в час», а просто двадцать раз.</p>
<h2 id="два-понятия-которые-часто-путают">Два понятия, которые часто путают</h2>
<p>Теперь, когда оба числа посчитаны, разница видна в упор.</p>
<table>
	<thead>
			<tr>
					<th><strong>Характеристика</strong></th>
					<th><strong>Absolute Error Rate (Абсолютный показатель)</strong></th>
					<th><strong>Burn Rate (Коэффициент сгорания)</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Что измеряет</strong></td>
					<td>Фактическую интенсивность ошибок «здесь и сейчас».</td>
					<td>Во сколько раз текущее потребление превышает норму.</td>
			</tr>
			<tr>
					<td><strong>Размерность</strong></td>
					<td>Имеет единицы измерения (ошибок/сек, % ошибок).</td>
					<td><strong>Безразмерный множитель</strong> (<span class="math-inline" data-png="https://jtprog.ru/math/62e694fc13a72da9e694c64dc0b6ae4de22e67eff140c1167fda5f1a7761ae0f.png" data-w="139" data-h="32"><span class="math__tex">\(1\times, 14.4\times, 50\times\)</span></span>).</td>
			</tr>
			<tr>
					<td><strong>Вопрос</strong></td>
					<td>«Сколько ошибок произошло за последнюю минуту?»</td>
					<td>«Если такая интенсивность сохранится, уложимся ли мы в бюджет?»</td>
			</tr>
			<tr>
					<td><strong>Интерпретация</strong></td>
					<td>Зависит от объема трафика в данный момент.</td>
					<td>Универсальна: <span class="math-inline" data-png="https://jtprog.ru/math/100d09e4b6b7154d5d09b5cca8665d1fac977cd7bb1f3720ed0a81f9af37e5db.png" data-w="37" data-h="32"><span class="math__tex">\(&gt;1\)</span></span> — бюджет тает слишком быстро, <span class="math-inline" data-png="https://jtprog.ru/math/56343cfaca5962f919c13b5fa2b18ff5b148901394efa298b95c2253a12ef44c.png" data-w="18" data-h="32"><span class="math__tex">\(1\)</span></span> — норма.</td>
			</tr>
	</tbody>
</table>
<h2 id="почему-скорость--плохое-слово">Почему «скорость» — плохое слово?</h2>
<p>В физике скорость (<span class="math-inline" data-png="https://jtprog.ru/math/aded00d4272851011f5371402d8f69726fea6cb9738aadd53c770b938935bdaa.png" data-w="64" data-h="33"><span class="math__tex">\(v = s/t\)</span></span>) всегда привязана к единицам времени (км/ч, м/с). Когда мы называем Burn Rate скоростью, мы подсознательно ищем время в знаменателе. А его там нет — оно сократилось еще на этапе деления долей.</p>
<p>На самом деле <strong>Burn Rate — это моментальный снимок соотношения</strong>, а не процесс, растянутый во времени.</p>
<ul>
<li><strong>Burn Rate = 1</strong>: Мы тратим бюджет ровно с той интенсивностью, которая позволит исчерпать его в последнюю секунду периода (при условии, что такая интенсивность сохранится).</li>
<li><strong>Burn Rate = 20</strong>: При такой нагрузке месячный бюджет сгорит за полтора дня (<span class="math-inline" data-png="https://jtprog.ru/math/d4cf031abff38ad4cd4311b26328a9bfef05ba1aeac335c7f6d041f0784051cf.png" data-w="113" data-h="32"><span class="math__tex">\(30 / 20 = 1.5\)</span></span>).</li>
</ul>
<h3 id="откуда-берется-каноническое-144">Откуда берется каноническое 14.4</h3>
<p>В статьях про алертинг постоянно всплывает множитель <span class="math-inline" data-png="https://jtprog.ru/math/31ed905bc04a6956e18960fb542a412d2494a340bb22e7784f5cfd608dea9081.png" data-w="46" data-h="32"><span class="math__tex">\(14.4\)</span></span>, и выглядит он как магическое число. На самом деле его выводят из простого требования: считать инцидентом ситуацию, когда сервис сжигает <strong>2% месячного бюджета за один час</strong>.</p>
<p>Один час — это <span class="math-inline" data-png="https://jtprog.ru/math/dde7eeba567000e3fe1f99777e5446c45d5a769fe2827d5f673bbd7683116dd0.png" data-w="36" data-h="33"><span class="math__tex">\(\tfrac{1}{720}\)</span></span> часть 30-дневного окна. Значит, коэффициент равен доле потраченного бюджета, деленной на долю прошедшего времени:</p>
<div class="math-block" data-png="https://jtprog.ru/math/38a0e12b1c8ee1471a0ecb63cf5ad4e8a2b58c14c5cb080b27aa4012b525d959.png"><span class="math__tex">$$\text{Burn Rate} = \dfrac{0.02}{\dfrac{1}{720}} = 0.02 \times 720 = 14.4$$</span></div><p>Обратная проверка сходится: при постоянном коэффициенте <span class="math-inline" data-png="https://jtprog.ru/math/31ed905bc04a6956e18960fb542a412d2494a340bb22e7784f5cfd608dea9081.png" data-w="46" data-h="32"><span class="math__tex">\(14.4\)</span></span> месячный бюджет заканчивается за <span class="math-inline" data-png="https://jtprog.ru/math/e85df2cc7087b3474a5a800d84945a6464a1c9ee4a30fa418b241dd54dfb27d0.png" data-w="141" data-h="32"><span class="math__tex">\(30 / 14.4 \approx 2.08\)</span></span> дня. Из той же арифметики получаются и остальные пороги: 5% бюджета за 6 часов дают множитель <span class="math-inline" data-png="https://jtprog.ru/math/4159070140229c7e64ea0cf8244e796dcf64c2e924725af5750919c2076e3899.png" data-w="18" data-h="32"><span class="math__tex">\(6\)</span></span>, а 10% за трое суток — множитель <span class="math-inline" data-png="https://jtprog.ru/math/56343cfaca5962f919c13b5fa2b18ff5b148901394efa298b95c2253a12ef44c.png" data-w="18" data-h="32"><span class="math__tex">\(1\)</span></span>.</p>
<blockquote><p><strong>Главный аргумент:</strong> Burn rate — это не про то, «как быстро» что-то движется, а про то, «во сколько раз» мы отклонились от нормы потребления.</p>
</blockquote><h2 id="важные-нюансы">Важные нюансы</h2>
<h3 id="1-burn-rate-возможен-только-при-наличии-бюджета">1. Burn Rate возможен только при наличии бюджета</h3>
<p>Безразмерность — необходимое, но не достаточное условие. Для Burn Rate нужен <strong>исчерпаемый ресурс</strong>.</p>
<ul>
<li>Сравнение <span class="math-inline" data-png="https://jtprog.ru/math/d5db5f0431ebbea30c05e2a5c8b726617925808c639723ed71191b844426829b.png" data-w="260" data-h="32"><span class="math__tex">\(\text{Latency p99} / \text{SLA threshold}\)</span></span> — это просто относительный коэффициент. Он не показывает сгорание ресурса, так как «время ожидания» само по себе не накапливается и не восстанавливается.</li>
<li>Однако, если SLO определен как <em>«% запросов быстрее 200 мс»</em>, то каждый медленный запрос — это «трата» из бюджета ошибок. В этом случае расчет через доли запросов превращает метрику в настоящий Burn Rate.</li>
</ul>
<h3 id="2-скользящее-окно-и-восстановление-бюджета">2. Скользящее окно и «восстановление» бюджета</h3>
<p>В отличие от денег на счету, бюджет ошибок в SRE обычно считается на <strong>скользящем окне</strong> (например, последние 30 дней). Это значит, что бюджет не просто сгорает, но и восстанавливается: когда старые инциденты (31-дневной давности) выходят за пределы окна, ваш доступный бюджет снова увеличивается.</p>
<h3 id="3-проблема-малого-трафика">3. Проблема малого трафика</h3>
<p>Burn Rate плохо работает на сервисах с низкой нагрузкой. Если у вас 1 запрос в час, то одна случайная ошибка даст Burn Rate в тысячи единиц, что приведет к ложному алерту. Для таких систем используют другие подходы (например, агрегацию за очень большие промежутки времени).</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<p>Первое — почини терминологию. Не используй слово «скорость», если хочешь подчеркнуть кратность превышения нормы; называй это <strong>коэффициентом сгорания</strong> или <strong>множителем</strong>. Звучит как придирка, но именно из-за слова «скорость» в дашборды регулярно уезжает абсолютная интенсивность ошибок вместо коэффициента.</p>
<p>Второе — собери на этом множителе алерты. Один порог не работает: низкий даст шум, высокий проспит медленную утечку бюджета. Поэтому берут несколько окон одновременно, и для каждого свой множитель:</p>
<table>
	<thead>
			<tr>
					<th>Доля бюджета</th>
					<th>Окно</th>
					<th>Burn Rate</th>
					<th>Реакция</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/2e49a13bb9f2fa1560a1edbe70f15010e73ec65edb51545db1219f34a141277e.png" data-w="37" data-h="32"><span class="math__tex">\(2\%\)</span></span></td>
					<td>1 час</td>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/cbad243d49cac31dddcb0d18fd1d0dabe6554b3d5859161213a036b0beb5368e.png" data-w="59" data-h="32"><span class="math__tex">\(14.4\times\)</span></span></td>
					<td>будим дежурного</td>
			</tr>
			<tr>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/d7d1c1d27b8be1dd7c829a26bb5305579b29f2c31ebf9450adde6d92cee6c685.png" data-w="37" data-h="32"><span class="math__tex">\(5\%\)</span></span></td>
					<td>6 часов</td>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/cc40abc1bdec6493326d58dbdd2d1dec08e03d110cb3f45b9e88f85ad21a27fc.png" data-w="30" data-h="32"><span class="math__tex">\(6\times\)</span></span></td>
					<td>будим дежурного</td>
			</tr>
			<tr>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/d3f8cf9806c3c1b5a18186770eb8886ab1e303c0655a7f5228e744657e6be8de.png" data-w="48" data-h="32"><span class="math__tex">\(10\%\)</span></span></td>
					<td>3 дня</td>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/6fd63359d2ce3bb4c665eb0323d4500cac625c71eabed0a64afb85794c1ee459.png" data-w="30" data-h="32"><span class="math__tex">\(1\times\)</span></span></td>
					<td>заводим тикет</td>
			</tr>
	</tbody>
</table>
<p>К каждому длинному окну добавляют короткое (обычно <span class="math-inline" data-png="https://jtprog.ru/math/a09accf9ec1c0927634ddf8d839c489d6b6a16b33dbd8077724845eee4afe328.png" data-w="28" data-h="33"><span class="math__tex">\(\tfrac{1}{12}\)</span></span> от длинного: 5 минут к часу, 30 минут к шести часам) — оно гасит алерт, когда всплеск уже закончился, и дежурный не разбирается с тем, что починилось само.</p>
<p>И вот здесь безразмерность из абстрактного свойства превращается в рабочий инструмент. Порог <span class="math-inline" data-png="https://jtprog.ru/math/cbad243d49cac31dddcb0d18fd1d0dabe6554b3d5859161213a036b0beb5368e.png" data-w="59" data-h="32"><span class="math__tex">\(14.4\times\)</span></span> одинаково валиден и для сервиса с миллионом RPS, и для сервиса с сотней RPS, и для ночного спада, и для черной пятницы. Один и тот же конфиг алерта переезжает между сервисами без пересчета абсолютных значений — ровно потому, что делить пришлось долю на долю, а время сократилось.</p>
<p>А как этот безразмерный множитель превращается в язык разговора с бизнесом про деньги — в статье <a href="/posts/reliability-is-a-conversation/">«Надёжность строится в диалоге с бизнесом»</a>. Как эти пороги живут рядом с остальным алертингом и почему будят на симптом, а не на причину, — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/sli-based-alerting/" rel="noopener noreferrer" target="_blank">в листе про SLI-based alerting</a>.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! В индустрии прижился термин «скорость сгорания бюджета ошибок» (Error Budget Burn Rate). Однако слово «скорость» здесь часто вводит в заблуждение, заставляя мозг искать привычные физические размерности там, где их нет.</p>
<p>Цена этой путаницы вполне материальна. Алерт, настроенный на абсолютную интенсивность ошибок, на ночном спаде трафика молчит весь инцидент, а на утреннем пике будит дежурного на ровном месте. Разберемся, почему Burn Rate — это в первую очередь <strong>безразмерный коэффициент</strong>, и как из него собирается алерт, который срабатывает по делу.</p>
<h2 id="начнем-с-математики">Начнем с математики</h2>
<p>Сейчас мы посчитаем два числа: сам бюджет ошибок и интенсивность, с которой его можно тратить. Ни одно из них не будет Burn Rate — и вот это как раз самое интересное.</p>
<p>Представим сервис со стабильной нагрузкой в <strong>1 000 запросов в минуту</strong> (req/min).</p>
<p>Мы установили <strong>SLO = 99.9%</strong>.</p>
<ol>
<li><strong>Определяем бюджет ошибок:</strong> <span class="math-inline" data-png="https://jtprog.ru/math/320d496cd5586944237fbd8ef42fa2a8684b33d66f58d8a20576a77e464cf280.png" data-w="304" data-h="32"><span class="math__tex">\(\text{Error Budget} = 1 - \text{SLO} = 0.1\%\)</span></span> (или <span class="math-inline" data-png="https://jtprog.ru/math/3b6d5b7be5e3bc670788f43343a2e7d88c1a3709d90ecfeea1e4089b71984d7f.png" data-w="58" data-h="32"><span class="math__tex">\(0.001\)</span></span> в долях).</li>
<li><strong>Считаем бюджет в абсолютных величинах:</strong>
<ul>
<li>За 30-дневное окно общее количество запросов составит: <span class="math-inline" data-png="https://jtprog.ru/math/ce447e5a79d95122e528c41b326d8bf0a858720f390decf5cf7af8105c5d47c3.png" data-w="657" data-h="32"><span class="math__tex">\(30 \text{ дней} \times 24 \text{ часа} \times 60 \text{ минут} \times 1\,000 \text{ req/min} = 43\,200\,000 \text{ запросов}\)</span></span>.</li>
<li>Допустимое количество ошибок: <span class="math-inline" data-png="https://jtprog.ru/math/9aaeeecb7e20c7544361dce1ce76a8ed2a1d99645506fee734a294e0680c907e.png" data-w="349" data-h="33"><span class="math__tex">\(43\,200\,000 \times 0.001 = \mathbf{43\,200 \text{ ошибок}}\)</span></span>.</li>
</ul>
</li>
</ol>
<p>Это наш исчерпаемый ресурс на 30 дней.</p>
<h3 id="как-можно-тратить-этот-бюджет">Как можно тратить этот бюджет?</h3>
<p>Мы можем распределить эти 43 200 ошибок равномерно по всему месяцу. Чтобы понять «нормальную» интенсивность ошибок, разделим бюджет на время:</p>
<div class="math-block" data-png="https://jtprog.ru/math/db98932414e57fe8990d2451851a58744bf8dd06d976dcd89c251f79b1894bda.png"><span class="math__tex">$$\frac{43\,200 \text{ ошибок}}{43\,200 \text{ минут}} = 1 \text{ ошибка в минуту}$$</span></div><p>Это — <strong>Absolute Rate</strong> (абсолютный показатель). У него есть размерность (ошибки/мин), и он намертво привязан к текущему трафику: на ночном спаде в 100 req/min та же одна ошибка в минуту означает уже десятикратное превышение нормы. Полезное число, но это всё еще не Burn Rate.</p>
<h2 id="формула-коэффициента">Формула коэффициента</h2>
<p>Burn Rate считается не через абсолютные значения, а через доли:</p>
<div class="math-block" data-png="https://jtprog.ru/math/7d3c05c1e67128d68491edf35f978da33027396adc89d250d4ee36202b45ae1a.png"><span class="math__tex">$$\text{Burn Rate} = \frac{\text{Фактическая доля ошибок (за интервал)}}{\text{Допустимая доля ошибок (по SLO)}}$$</span></div><p>Возьмем наш сервис с SLO 99.9% (допустимо 0.1% ошибок). Если за последние 5 минут мы поймали 2% ошибок:</p>
<div class="math-block" data-png="https://jtprog.ru/math/fc4067afc10670216ac0043a957a14072d28610707d3a1b99337c32b886c7eec.png"><span class="math__tex">$$\text{Burn Rate} = \frac{0.02}{0.001} = 20\times$$</span></div><p>Обрати внимание на то, что произошло с единицами измерения. И в числителе, и в знаменателе стоит одна и та же величина — доля неуспешных запросов от общего числа. При делении они сокращаются, и на выходе остается голое число. Не «двадцать чего-то в час», а просто двадцать раз.</p>
<h2 id="два-понятия-которые-часто-путают">Два понятия, которые часто путают</h2>
<p>Теперь, когда оба числа посчитаны, разница видна в упор.</p>
<table>
	<thead>
			<tr>
					<th><strong>Характеристика</strong></th>
					<th><strong>Absolute Error Rate (Абсолютный показатель)</strong></th>
					<th><strong>Burn Rate (Коэффициент сгорания)</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Что измеряет</strong></td>
					<td>Фактическую интенсивность ошибок «здесь и сейчас».</td>
					<td>Во сколько раз текущее потребление превышает норму.</td>
			</tr>
			<tr>
					<td><strong>Размерность</strong></td>
					<td>Имеет единицы измерения (ошибок/сек, % ошибок).</td>
					<td><strong>Безразмерный множитель</strong> (<span class="math-inline" data-png="https://jtprog.ru/math/62e694fc13a72da9e694c64dc0b6ae4de22e67eff140c1167fda5f1a7761ae0f.png" data-w="139" data-h="32"><span class="math__tex">\(1\times, 14.4\times, 50\times\)</span></span>).</td>
			</tr>
			<tr>
					<td><strong>Вопрос</strong></td>
					<td>«Сколько ошибок произошло за последнюю минуту?»</td>
					<td>«Если такая интенсивность сохранится, уложимся ли мы в бюджет?»</td>
			</tr>
			<tr>
					<td><strong>Интерпретация</strong></td>
					<td>Зависит от объема трафика в данный момент.</td>
					<td>Универсальна: <span class="math-inline" data-png="https://jtprog.ru/math/100d09e4b6b7154d5d09b5cca8665d1fac977cd7bb1f3720ed0a81f9af37e5db.png" data-w="37" data-h="32"><span class="math__tex">\(&gt;1\)</span></span> — бюджет тает слишком быстро, <span class="math-inline" data-png="https://jtprog.ru/math/56343cfaca5962f919c13b5fa2b18ff5b148901394efa298b95c2253a12ef44c.png" data-w="18" data-h="32"><span class="math__tex">\(1\)</span></span> — норма.</td>
			</tr>
	</tbody>
</table>
<h2 id="почему-скорость--плохое-слово">Почему «скорость» — плохое слово?</h2>
<p>В физике скорость (<span class="math-inline" data-png="https://jtprog.ru/math/aded00d4272851011f5371402d8f69726fea6cb9738aadd53c770b938935bdaa.png" data-w="64" data-h="33"><span class="math__tex">\(v = s/t\)</span></span>) всегда привязана к единицам времени (км/ч, м/с). Когда мы называем Burn Rate скоростью, мы подсознательно ищем время в знаменателе. А его там нет — оно сократилось еще на этапе деления долей.</p>
<p>На самом деле <strong>Burn Rate — это моментальный снимок соотношения</strong>, а не процесс, растянутый во времени.</p>
<ul>
<li><strong>Burn Rate = 1</strong>: Мы тратим бюджет ровно с той интенсивностью, которая позволит исчерпать его в последнюю секунду периода (при условии, что такая интенсивность сохранится).</li>
<li><strong>Burn Rate = 20</strong>: При такой нагрузке месячный бюджет сгорит за полтора дня (<span class="math-inline" data-png="https://jtprog.ru/math/d4cf031abff38ad4cd4311b26328a9bfef05ba1aeac335c7f6d041f0784051cf.png" data-w="113" data-h="32"><span class="math__tex">\(30 / 20 = 1.5\)</span></span>).</li>
</ul>
<h3 id="откуда-берется-каноническое-144">Откуда берется каноническое 14.4</h3>
<p>В статьях про алертинг постоянно всплывает множитель <span class="math-inline" data-png="https://jtprog.ru/math/31ed905bc04a6956e18960fb542a412d2494a340bb22e7784f5cfd608dea9081.png" data-w="46" data-h="32"><span class="math__tex">\(14.4\)</span></span>, и выглядит он как магическое число. На самом деле его выводят из простого требования: считать инцидентом ситуацию, когда сервис сжигает <strong>2% месячного бюджета за один час</strong>.</p>
<p>Один час — это <span class="math-inline" data-png="https://jtprog.ru/math/dde7eeba567000e3fe1f99777e5446c45d5a769fe2827d5f673bbd7683116dd0.png" data-w="36" data-h="33"><span class="math__tex">\(\tfrac{1}{720}\)</span></span> часть 30-дневного окна. Значит, коэффициент равен доле потраченного бюджета, деленной на долю прошедшего времени:</p>
<div class="math-block" data-png="https://jtprog.ru/math/38a0e12b1c8ee1471a0ecb63cf5ad4e8a2b58c14c5cb080b27aa4012b525d959.png"><span class="math__tex">$$\text{Burn Rate} = \dfrac{0.02}{\dfrac{1}{720}} = 0.02 \times 720 = 14.4$$</span></div><p>Обратная проверка сходится: при постоянном коэффициенте <span class="math-inline" data-png="https://jtprog.ru/math/31ed905bc04a6956e18960fb542a412d2494a340bb22e7784f5cfd608dea9081.png" data-w="46" data-h="32"><span class="math__tex">\(14.4\)</span></span> месячный бюджет заканчивается за <span class="math-inline" data-png="https://jtprog.ru/math/e85df2cc7087b3474a5a800d84945a6464a1c9ee4a30fa418b241dd54dfb27d0.png" data-w="141" data-h="32"><span class="math__tex">\(30 / 14.4 \approx 2.08\)</span></span> дня. Из той же арифметики получаются и остальные пороги: 5% бюджета за 6 часов дают множитель <span class="math-inline" data-png="https://jtprog.ru/math/4159070140229c7e64ea0cf8244e796dcf64c2e924725af5750919c2076e3899.png" data-w="18" data-h="32"><span class="math__tex">\(6\)</span></span>, а 10% за трое суток — множитель <span class="math-inline" data-png="https://jtprog.ru/math/56343cfaca5962f919c13b5fa2b18ff5b148901394efa298b95c2253a12ef44c.png" data-w="18" data-h="32"><span class="math__tex">\(1\)</span></span>.</p>
<blockquote><p><strong>Главный аргумент:</strong> Burn rate — это не про то, «как быстро» что-то движется, а про то, «во сколько раз» мы отклонились от нормы потребления.</p>
</blockquote><h2 id="важные-нюансы">Важные нюансы</h2>
<h3 id="1-burn-rate-возможен-только-при-наличии-бюджета">1. Burn Rate возможен только при наличии бюджета</h3>
<p>Безразмерность — необходимое, но не достаточное условие. Для Burn Rate нужен <strong>исчерпаемый ресурс</strong>.</p>
<ul>
<li>Сравнение <span class="math-inline" data-png="https://jtprog.ru/math/d5db5f0431ebbea30c05e2a5c8b726617925808c639723ed71191b844426829b.png" data-w="260" data-h="32"><span class="math__tex">\(\text{Latency p99} / \text{SLA threshold}\)</span></span> — это просто относительный коэффициент. Он не показывает сгорание ресурса, так как «время ожидания» само по себе не накапливается и не восстанавливается.</li>
<li>Однако, если SLO определен как <em>«% запросов быстрее 200 мс»</em>, то каждый медленный запрос — это «трата» из бюджета ошибок. В этом случае расчет через доли запросов превращает метрику в настоящий Burn Rate.</li>
</ul>
<h3 id="2-скользящее-окно-и-восстановление-бюджета">2. Скользящее окно и «восстановление» бюджета</h3>
<p>В отличие от денег на счету, бюджет ошибок в SRE обычно считается на <strong>скользящем окне</strong> (например, последние 30 дней). Это значит, что бюджет не просто сгорает, но и восстанавливается: когда старые инциденты (31-дневной давности) выходят за пределы окна, ваш доступный бюджет снова увеличивается.</p>
<h3 id="3-проблема-малого-трафика">3. Проблема малого трафика</h3>
<p>Burn Rate плохо работает на сервисах с низкой нагрузкой. Если у вас 1 запрос в час, то одна случайная ошибка даст Burn Rate в тысячи единиц, что приведет к ложному алерту. Для таких систем используют другие подходы (например, агрегацию за очень большие промежутки времени).</p>
<h2 id="что-с-этим-делать-на-практике">Что с этим делать на практике</h2>
<p>Первое — почини терминологию. Не используй слово «скорость», если хочешь подчеркнуть кратность превышения нормы; называй это <strong>коэффициентом сгорания</strong> или <strong>множителем</strong>. Звучит как придирка, но именно из-за слова «скорость» в дашборды регулярно уезжает абсолютная интенсивность ошибок вместо коэффициента.</p>
<p>Второе — собери на этом множителе алерты. Один порог не работает: низкий даст шум, высокий проспит медленную утечку бюджета. Поэтому берут несколько окон одновременно, и для каждого свой множитель:</p>
<table>
	<thead>
			<tr>
					<th>Доля бюджета</th>
					<th>Окно</th>
					<th>Burn Rate</th>
					<th>Реакция</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/2e49a13bb9f2fa1560a1edbe70f15010e73ec65edb51545db1219f34a141277e.png" data-w="37" data-h="32"><span class="math__tex">\(2\%\)</span></span></td>
					<td>1 час</td>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/cbad243d49cac31dddcb0d18fd1d0dabe6554b3d5859161213a036b0beb5368e.png" data-w="59" data-h="32"><span class="math__tex">\(14.4\times\)</span></span></td>
					<td>будим дежурного</td>
			</tr>
			<tr>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/d7d1c1d27b8be1dd7c829a26bb5305579b29f2c31ebf9450adde6d92cee6c685.png" data-w="37" data-h="32"><span class="math__tex">\(5\%\)</span></span></td>
					<td>6 часов</td>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/cc40abc1bdec6493326d58dbdd2d1dec08e03d110cb3f45b9e88f85ad21a27fc.png" data-w="30" data-h="32"><span class="math__tex">\(6\times\)</span></span></td>
					<td>будим дежурного</td>
			</tr>
			<tr>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/d3f8cf9806c3c1b5a18186770eb8886ab1e303c0655a7f5228e744657e6be8de.png" data-w="48" data-h="32"><span class="math__tex">\(10\%\)</span></span></td>
					<td>3 дня</td>
					<td><span class="math-inline" data-png="https://jtprog.ru/math/6fd63359d2ce3bb4c665eb0323d4500cac625c71eabed0a64afb85794c1ee459.png" data-w="30" data-h="32"><span class="math__tex">\(1\times\)</span></span></td>
					<td>заводим тикет</td>
			</tr>
	</tbody>
</table>
<p>К каждому длинному окну добавляют короткое (обычно <span class="math-inline" data-png="https://jtprog.ru/math/a09accf9ec1c0927634ddf8d839c489d6b6a16b33dbd8077724845eee4afe328.png" data-w="28" data-h="33"><span class="math__tex">\(\tfrac{1}{12}\)</span></span> от длинного: 5 минут к часу, 30 минут к шести часам) — оно гасит алерт, когда всплеск уже закончился, и дежурный не разбирается с тем, что починилось само.</p>
<p>И вот здесь безразмерность из абстрактного свойства превращается в рабочий инструмент. Порог <span class="math-inline" data-png="https://jtprog.ru/math/cbad243d49cac31dddcb0d18fd1d0dabe6554b3d5859161213a036b0beb5368e.png" data-w="59" data-h="32"><span class="math__tex">\(14.4\times\)</span></span> одинаково валиден и для сервиса с миллионом RPS, и для сервиса с сотней RPS, и для ночного спада, и для черной пятницы. Один и тот же конфиг алерта переезжает между сервисами без пересчета абсолютных значений — ровно потому, что делить пришлось долю на долю, а время сократилось.</p>
<p>А как этот безразмерный множитель превращается в язык разговора с бизнесом про деньги — в статье <a href="/posts/reliability-is-a-conversation/">«Надёжность строится в диалоге с бизнесом»</a>. Как эти пороги живут рядом с остальным алертингом и почему будят на симптом, а не на причину, — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/sli-based-alerting/" rel="noopener noreferrer" target="_blank">в листе про SLI-based alerting</a>.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Как не сойти с ума, чиня прод – практические заметки SRE</title>
      <link>https://jtprog.ru/posts/sre-recipes-summary/</link>
      <pubDate>Sat, 14 Feb 2026 11:29:21 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/sre-recipes-summary/</guid>
      <category>SRE</category>
      <category>Reliability</category>
      <category>Monitoring</category>
      <category>Docker</category>
      <category>Chaos-Engineering</category>
      <category>DevOps</category>
      <category domain="category">Opinions</category>
      <enclosure url="https://jtprog.ru/opinions.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/opinions.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Иногда кажется, что про надежность уже сказано всё: SLO, error budget, postmortem’ы, Chaos Engineering и вот это всё. Но потом ты открываешь очередной инцидент в проде — и понимаешь, что падает не «абстрактная система», а вполне конкретная «штука», которую ты вчера трогал руками. И вот ты сидишь, смотришь в логи и графики, и главный вопрос звучит примерно так: «А можно было сделать так, чтобы сегодня я спал?»</p>
<p>На написание этой статьи меня сподвигла книга &ldquo;<a href="https://www.litres.ru/book/natalya-savenkova/sre-recepty-vyzhivaniya-v-prodakshene-dlya-inzhenera-po-70232503/" rel="noopener noreferrer" target="_blank">SRE. Рецепты выживания в продакшне для инженера по надежности</a>&rdquo; за авторством <a href="https://www.litres.ru/author/natalya-savenkova/" rel="noopener noreferrer" target="_blank">Натальи Савенковой</a>. Эта статья — попытка собрать в одном месте практические тезисы, которые помогают не просто «чинить прод», а строить такую эксплуатацию, где падения становятся реже, а восстановление — проще и спокойнее.</p>
<h2 id="принципы-вместо-героизма">Принципы вместо героизма</h2>
<p>Все начинается не с Grafana и не с Kubernetes, а с принципов команды: что мы считаем нормой, что считаем неприемлемым и как вообще привыкли работать.</p>
<ul>
<li>Надежность <strong>начинается</strong> с того, как команда договаривается работать: что считаем ок, а что считаем нарушением здравого смысла.</li>
<li>Если надежность сервиса нельзя управлять, измерять и регулировать — у тебя нет сервиса, у тебя набор случайно работающих скриптов. Измеряемость начинается с <a href="/posts/slo-as-architecture-blueprint/">SLI и SLO, выведенных из пользовательского пути</a>, а управляемость — с <a href="/posts/reliability-is-a-conversation/">разговора о них с бизнесом</a>.</li>
<li>Принцип «если страшно — делай чаще» отлично работает и для процедур (релизы, переключения, учения) — чем больше повторов, тем ближе автомация и тем меньше шансов, что «руки дрогнут».</li>
</ul>
<p>Характерная мысль: надежность — это не героические ночные починки, а заранее сформулированные правила, по которым команда принимает решения, особенно неприятные. Ровно об этом, только на языке производственного цеха, — <a href="/posts/project-phoenix/">«Проект Феникс»</a>: там незапланированная работа съедает команду именно потому, что нет договорённостей, по которым её отсекают.</p>
<h2 id="иммунитет-системы-dis-gd-fallback">Иммунитет системы: DIS, GD, fallback</h2>
<p>Крутая метафора — иммунитет продукта: система рождается с каким‑то базовым «здоровьем», но дальше его нужно постоянно тренировать учебными встрясками и интеграцией опыта прошлых инцидентов.</p>
<ul>
<li>Всё, что с системой случается, должно попадать в общую память: DIS (<a href="/posts/digital-immune-system/">Digital Immune System</a>, назови как хочешь) — не украшение, а база для эволюции. Чем зрелый иммунитет отличается от набора купленных практик — <a href="/posts/digital-immune-system-maturity/">разбирал отдельно</a>.</li>
<li>Если смежный сервис начал чаще отвечать ошибками — это не только про ретраи, это триггер к запуску graceful degradation (GD) или fallback именно на этом участке.</li>
<li>Exponential backoff — не про «красивые алгоритмы», а про то, чтобы ретраи не продлевали агонию инцидента, а давали системе шанс «встать с колен».</li>
</ul>
<p>Идеальная связка: DIS аккумулирует историю ошибок, GD даёт «план Б», fallback закрывает совсем плохие сценарии, чтобы пользователю не прилетал унылый «502 Bad Gateway».</p>
<h2 id="graceful-degradation-всегда-отвечать-но-не-всегда-всем">Graceful Degradation: всегда отвечать, но не всегда всем</h2>
<p>Если в голове GD = «мы иногда отваливаемся по лёгкому», то ничего хорошего не выйдет.</p>
<ul>
<li>Правильная GD — это когда сервис <strong>всегда отвечает</strong>, но не всегда полной функциональностью.</li>
<li>Нужны <strong>уровни</strong> деградации: от выключения «сопутствующих товаров» до отдачи статики «техработы, вот наши лучшие предложения».</li>
<li>Кэширование и заглушки — такие же инструменты GD: лучше показать «Топ‑5» или дефолтный вариант, чем ничего.</li>
</ul>
<p>Очень важный момент — GD надо согласовывать с продактом заранее: какие блоки отключаем сначала, какие — потом, что для бизнеса критично, а что можно отрубить без слёз.</p>
<h2 id="fallback-когда-даже-gd-не-спасает">Fallback: когда даже GD не спасает</h2>
<p>Fallback — это не «ещё одна страница с ошибкой», это реальный последний шанс дать пользователю хоть какую‑то ценность.</p>
<ul>
<li>Fallback лучше любого 5xx: даже тетрис или одна страница с лучшим товаром лучше, чем пустой 502.</li>
<li>Важно мониторить, <strong>как часто</strong> показывается fallback: это отдельный сигнал о классах запросов, которые система не может обработать корректно.</li>
<li>Fallback и GD — не взаимоисключающие штуки: GD работает «по пути» запроса, fallback — когда пути уже не осталось.</li>
</ul>
<p>По сути, GD и fallback — это про уважение к пользователю: либо ты честно пытаешься дать хотя бы что‑то, либо молча падаешь в 5xx и надеешься, что он когда-нибудь вернётся.</p>
<h2 id="мониторинг-который-действительно-помогает">Мониторинг, который действительно помогает</h2>
<p>Мониторинг «чтобы был» только создает иллюзию контроля. Полезный мониторинг строится из нескольких идей.</p>
<h3 id="абсолютные-значения-и-тренды">Абсолютные значения и тренды</h3>
<ul>
<li>Контролируй трафик по <strong>абсолютным значениям</strong> сверху и снизу — чтобы видеть и внезапные всплески, и подозрительные провалы.</li>
<li>Используй <strong>тренды</strong>, чтобы ловить аномалии в поведении (типичное для дня недели, часа и т.д.), но помни: плавные изменения тренд может спокойно «проглотить».</li>
<li>Рабочий рецепт: совмещать мониторинг по абсолюту и по тренду, а не выбирать что‑то одно.</li>
</ul>
<h3 id="среднее-minmax-и-перцентили">Среднее, min/max и перцентили</h3>
<ul>
<li>Среднее без min/max — это классика «средней температуры по больнице».</li>
<li>Важно отслеживать <strong>min/max</strong> и перцентили (p90, p95, p99), потому что именно они часто первыми намекают на будущий сбой.</li>
<li>Если по агрегатам всё красиво, а один хост уже умирает — без min/max ты про это узнаешь только из инцидента.</li>
<li>Если поверх перцентилей у тебя живут SLO, то алерты вешаются на burn rate. Только помни, что <a href="/posts/burn-rate-is-not-speed/">это безразмерный коэффициент, а не «скорость»</a>, и на маленьком трафике он врёт.</li>
</ul>
<h3 id="молчаливый-мониторинг-и-резервный-мониторинг">Молчаливый мониторинг и резервный мониторинг</h3>
<ul>
<li>«Если мониторинг не пишет о проблемах — возможно, он просто не работает» — очень неприятная правда.</li>
<li>Резервный мониторинг на других технологиях (хоть отдельная Grafana+VM на retention 2 дня) — это не паранойя, а нормальная страховка.</li>
<li>Мониторинг должен быть не только внутренним, но и внешним: проверять доступность глазами пользователя из важных локаций.</li>
</ul>
<p>По хорошему, у тебя должны мониториться: сами сервисы, их зависимости, мониторинг, срок годности сертификатов/лицензий и <a href="/posts/wat-monitoring/">всё, что когда‑нибудь «протухнет»</a>.</p>
<h2 id="нагрузочное-тестирование-и-правильные-учения">Нагрузочное тестирование и «правильные» учения</h2>
<p>Учения и нагрузка — самая недооценённая часть надёжности: все про них говорят, но мало кто делает это регулярно и по уму.</p>
<ul>
<li>Учения без подготовки — must‑have: либо автоматика работает, либо в ходе учений становится ясно, что её нет и её надо сделать.</li>
<li>Сценарии учений надо <strong>рандомизировать</strong> и пересматривать, иначе это будет не Chaos Engineering, а театральная постановка «все всё знают заранее».</li>
<li>Нагрузочные тесты с одинаковыми результатами — тревожный знак: значит, тесты устарели или слишком аккуратно обходят острые углы.</li>
</ul>
<p>Как это выглядит вживую, хорошо было слышно в докладе Mindbox <a href="/posts/yandex-scale-2024/">на Yandex Scale</a>: описания экспериментов лежат рядом с кодом сервиса, на каждый эксперимент готов runbook по восстановлению, а окно для запуска — середина нагрузки, а не пик и не ночь.</p>
<p>Система с «отличными» нагрузочными тестами, которые никогда ничего не ломают, обычно падает от первого же реального всплеска.</p>
<h2 id="failover-расселение-сервисов-и-слон-с-моськой">Failover, расселение сервисов и «слон с моськой»</h2>
<p>Любая история про отказоустойчивость со временем приходит к трем вопросам: где у тебя точки отказа, кто с кем соседствует и как ты это всё переключаешь.</p>
<ul>
<li>Failover <strong>дороже</strong>, чем кажется: нужен не только второй инстанс, но и координатор, логика консенсуса и вся инфраструктура вокруг.</li>
<li>Не сажай «слона и моську» в одну базу: не смешивай критичные сервисы с теми, у кого неконтролируемый трафик (сбор аналитики, события с фронта и т.п.).</li>
<li>Микросервисный подход с явной разметкой по критичности: выделенные ресурсы для разных классов сервисов.</li>
</ul>
<p>Как это раскладывается по слоям — что резервируем на фронте, что на бэке, а что в базе — <a href="/posts/ha-clustering/">разбирал в посте про кластеризацию и доступность</a>.</p>
<p>Критичный сервис плюс «шумный» некритичный — идеальный рецепт для самодельной DDoS‑атаки на самого себя.</p>
<h2 id="capacity-management-и-finops-сколько-мощности-тебе-реально-нужно">Capacity Management и FinOps: сколько мощности тебе реально нужно</h2>
<p>«Просто добавить ресурсов» — худшая из возможных стратегий, особенно в облаке.</p>
<ul>
<li>Любой запас мощности должен быть осмысленным: под какой сценарий он нужен, на какой срок и при каких ограничениях по бюджету.</li>
<li>Capacity‑планирование нужно делать по <strong>CUJ</strong> (Critical User Journey): самые важные клиентские маршруты должны иметь понятный запас.</li>
<li>Это не только про железо, но и про архитектуру: где узкие места, какой компонент ограничивает пропускную способность и как быстро ты можешь масштабироваться.</li>
</ul>
<p>По сути, это чистый FinOps: договариваешься с бизнесом, какой трафик обязан выдерживать, а какой — нет, и уже под это строишь схему резервирования и бюджетов. И это не разовая мода — <a href="/posts/sre-evolution-2020-2025/">за 2020-2025 деньги окончательно въехали в зону ответственности SRE</a> вместе с Platform Engineering.</p>
<h2 id="gitops-версии-и-самодостаточный-релиз">GitOps, версии и «самодостаточный релиз»</h2>
<p>Очень много боли в инцидентах рождается не из багов в коде, а из конфигов и «плавающих» зависимостей.</p>
<ul>
<li>Всё, что имеет ценность и выглядит как файл, должно жить в VCS: скрипты, конфиги, инфраструктурные артефакты. Дашборды Grafana сюда тоже входят — их проще <a href="/posts/grafana-exports/">выгрузить через API и положить в provisioning</a>, чем перекликивать мышкой после переезда.</li>
<li>GitOps‑подход: релиз — это не только «бинарь» или образ, но и конфиги, секреты, схемы, миграции, всё с версионированием.</li>
<li>Самодостаточный релиз должен гарантированно уметь работать сам по себе и уметь откатываться вместе с конфигами и внешними зависимостями.</li>
</ul>
<p>Отдельная боль — изменения «на проде руками без бэкапа»: перед любым вмешательством в БД или конфиги должен быть свежий бэкап или хотя бы сохранённая копия файла с суффиксом даты.</p>
<h2 id="фичатогглы-canary-whitelistblacklist">Фичатогглы, canary, whitelist/blacklist</h2>
<p>Любая новая фича потенциально ломает прод — вопрос только в том, насколько контролируемо.</p>
<ul>
<li>Фичи надо выкатывать <strong>выключенными</strong> и включать через конфиги/рубильники: это даёт быстрый roll‑back без пересборки.</li>
<li>Whitelist/blacklist помогают управлять долей трафика: сначала включаешь фичу на сегмент, потом расширяешь, потом можешь точечно отключать.</li>
<li>В стабилизированном состоянии прод должен иметь одну версию сервиса: параллельные версии «Ok» только на время A/B и canary, а не «так и живём».</li>
</ul>
<p>Фичи, которые нельзя быстро и адресно отключить — это не фичи, а долговые ямы для дежурных.</p>
<h2 id="процессы-инструкции-и-людочка-на-ресепшене">Процессы, инструкции и «Людочка на ресепшене»</h2>
<p>Хорошие процессы — это не бюрократия, а защита от хаоса в самый неподходящий момент.</p>
<ul>
<li>Стандартизированные процессы с чёткими, зафиксированными шагами — лучший фундамент для будущей автоматизации.</li>
<li>Инструкция должна быть такова, чтобы её могла выполнить «Людочка» или условная секретарша: копировать команды, открывать ссылки и получать предсказуемый результат. Писать её стоит как код, с тем же ревью и версиями — <a href="/posts/rtfm-dry-kiss/">про RTFM, DRY и документацию у меня есть отдельный текст</a>.</li>
<li>Плановые работы должны делаться <strong>по регламенту</strong>, без «давай заодно тут поправим, пока уже всё отключили».</li>
</ul>
<p>Сюда же — план Б на каждое нетипичное вмешательство: если «всё пойдёт не так», должен быть понятный сценарий возврата в состояние «как было». Предел мечтаний тут — инструкция, которую вообще не надо читать руками: <a href="/posts/tron-as-sre/">Tron из одноимённого фильма</a> как раз про автоматический runbook remediation, только с мечом.</p>
<h2 id="люди-психология-и-работа-в-стрессе">Люди, психология и работа в стрессе</h2>
<p>Любой серьёзный инцидент — это ещё и психологическая нагрузка, и к ней тоже надо готовиться.</p>
<ul>
<li>У каждого свой «деструктор» в стрессе: кто‑то начинает хаотично нажимать всё подряд, кто‑то «замерзает», кто‑то исчезает из коммуникации.</li>
<li>Личный чек‑лист дежурного («подышать», «записать идеи действий», «пройтись», «открыть инструкцию по оповещению») — очень мощный инструмент.</li>
<li>Интуицию стоит слушать: если внутри зудит «сделать бэкап» или «снять трафик» — лучше перебдеть, чем недобдеть.</li>
</ul>
<p>Отдельная история — групповые дежурства: если «ответственность на всех», то по факту не отвечает никто, поэтому нужны конкретные фамилии в графике. Как делить зоны ответственности внутри команды, чтобы график не был формальностью, <a href="/posts/organize-sysadmins/">писал ещё во времена сисадминских будней</a>.</p>
<h2 id="коммуникация-смежники-продакты-пользователи-твоих-инструментов">Коммуникация: смежники, продакты, пользователи твоих инструментов</h2>
<p>Надёжность ломается не только в коде, но и на стыках людей и команд.</p>
<ul>
<li>Если сервис <em>А</em> положил сервис <em>Б</em> десятикратным ростом запросов — виноваты оба: <em>А</em> не ограничил нагрузку, <em>Б</em> не предусмотрел верхнюю границу того, что он может переварить.</li>
<li>Разборы инцидентов должны быть <strong>публичными</strong>: любой участник должен иметь возможность задать вопрос, особенно из смежных команд.</li>
<li>С теми, кто пользуется твоими панелями и инструментами, надо регулярно говорить и смотреть «их глазами» на интерфейс — очень часто это приносит внезапные открытия, потому что очевидное для тебя — неочевидно для других.</li>
</ul>
<p>Плюс базовая дисциплина: общий календарь маркетинговых запусков и регламентных работ, понятные термины и общий словарь. Это и есть та часть <a href="/posts/wat-devops/">DevOps</a>, которая про совместную работу команд, а не про очередной пайплайн.</p>
<h2 id="безопасность-как-часть-надёжности">Безопасность как часть надёжности</h2>
<p>Про ИБ легко забыть, пока не прилетело.</p>
<ul>
<li>Требования информационной безопасности — это не внешняя бюрократия, а часть надёжности и доступности.</li>
<li>Чем раньше ты начнёшь приводить сервис к ИБ‑гайдам, тем меньше будет боли потом: всё равно придётся, но лучше не в режиме горящей пятой точки.</li>
<li>Debug‑режимы, скрытые параметры, хардкод в фронтенде, незащищённые «магические» урлы — всё это при первом же fuzzing&rsquo;е превращается в инцидент.</li>
</ul>
<p>ИБ — просто ещё один класс рисков для надёжности, и игнорировать его — то же самое, что не мониторить диск или БД. Как это выглядит, когда риск реализуется целиком, хорошо видно на <a href="/posts/hacking-aeroflot/">разборе истории с «Аэрофлотом»</a>.</p>
<h2 id="несколько-практических-выводов">Несколько практических выводов</h2>
<p>Соберём самое важное в виде коротких тезисов — то, что полезно прямо взять и примерить к своему проекту:</p>
<ul>
<li><strong>Формализуй принципы команды.</strong> Пойми, что для вас нормально, а что — нет, и как вы принимаете решения в проде.</li>
<li><strong>Строй иммунитет системы.</strong> Собирай данные об инцидентах, запускай учения, внедряй DIS, GD и fallback как связанный комплекс.</li>
<li><strong>Наведи порядок в мониторинге.</strong> Абсолютные значения + тренды, min/max + перцентили, резервный мониторинг и внешние проверки. И добавь сверху <a href="/posts/business-metrics/">бизнес-метрики</a> — без них ты видишь живые хосты, но не видишь, что оплата не проходит.</li>
<li><strong>Инвестируй в процесс.</strong> Стандартизированные инструкции, регламенты, планы отката, регулярные учения по релизам и rollback’ам.</li>
<li><strong>Живи через GitOps и версии.</strong> Всё важное — в VCS, релиз самодостаточен, откат — быстрый и предсказуемый. <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/gitops/" rel="noopener noreferrer" target="_blank">Лист про GitOps</a> — про то, чем модель pull отличается от «задеплоил из пайплайна».</li>
<li><strong>Не забывай про людей.</strong> Психология дежурств, личные чек‑листы, явные дежурные, нормальная коммуникация с продактами и смежниками. <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/on-call-rotation/" rel="noopener noreferrer" target="_blank">Про устройство ротации</a> — покрытие, справедливость и выносливость — есть отдельный лист.</li>
</ul>
<p>Если всё это собрать вместе, то «чинить прод» перестаёт быть вечным героическим подвигом и превращается в спокойную инженерную работу. А это, кажется, ровно то, к чему хочется прийти.</p>
<p>PS: Читать книгу &ldquo;<a href="https://www.litres.ru/book/natalya-savenkova/sre-recepty-vyzhivaniya-v-prodakshene-dlya-inzhenera-po-70232503/" rel="noopener noreferrer" target="_blank">SRE. Рецепты выживания в продакшне для инженера по надежности</a>&rdquo; за авторством <a href="https://www.litres.ru/author/natalya-savenkova/" rel="noopener noreferrer" target="_blank">Натальи Савенковой</a> было и смешно и грустно одновременно. Но главная ценность лично для меня — я увидел, что я не одинок в своих выводах!</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Иногда кажется, что про надежность уже сказано всё: SLO, error budget, postmortem’ы, Chaos Engineering и вот это всё. Но потом ты открываешь очередной инцидент в проде — и понимаешь, что падает не «абстрактная система», а вполне конкретная «штука», которую ты вчера трогал руками. И вот ты сидишь, смотришь в логи и графики, и главный вопрос звучит примерно так: «А можно было сделать так, чтобы сегодня я спал?»</p>
<p>На написание этой статьи меня сподвигла книга &ldquo;<a href="https://www.litres.ru/book/natalya-savenkova/sre-recepty-vyzhivaniya-v-prodakshene-dlya-inzhenera-po-70232503/" rel="noopener noreferrer" target="_blank">SRE. Рецепты выживания в продакшне для инженера по надежности</a>&rdquo; за авторством <a href="https://www.litres.ru/author/natalya-savenkova/" rel="noopener noreferrer" target="_blank">Натальи Савенковой</a>. Эта статья — попытка собрать в одном месте практические тезисы, которые помогают не просто «чинить прод», а строить такую эксплуатацию, где падения становятся реже, а восстановление — проще и спокойнее.</p>
<h2 id="принципы-вместо-героизма">Принципы вместо героизма</h2>
<p>Все начинается не с Grafana и не с Kubernetes, а с принципов команды: что мы считаем нормой, что считаем неприемлемым и как вообще привыкли работать.</p>
<ul>
<li>Надежность <strong>начинается</strong> с того, как команда договаривается работать: что считаем ок, а что считаем нарушением здравого смысла.</li>
<li>Если надежность сервиса нельзя управлять, измерять и регулировать — у тебя нет сервиса, у тебя набор случайно работающих скриптов. Измеряемость начинается с <a href="/posts/slo-as-architecture-blueprint/">SLI и SLO, выведенных из пользовательского пути</a>, а управляемость — с <a href="/posts/reliability-is-a-conversation/">разговора о них с бизнесом</a>.</li>
<li>Принцип «если страшно — делай чаще» отлично работает и для процедур (релизы, переключения, учения) — чем больше повторов, тем ближе автомация и тем меньше шансов, что «руки дрогнут».</li>
</ul>
<p>Характерная мысль: надежность — это не героические ночные починки, а заранее сформулированные правила, по которым команда принимает решения, особенно неприятные. Ровно об этом, только на языке производственного цеха, — <a href="/posts/project-phoenix/">«Проект Феникс»</a>: там незапланированная работа съедает команду именно потому, что нет договорённостей, по которым её отсекают.</p>
<h2 id="иммунитет-системы-dis-gd-fallback">Иммунитет системы: DIS, GD, fallback</h2>
<p>Крутая метафора — иммунитет продукта: система рождается с каким‑то базовым «здоровьем», но дальше его нужно постоянно тренировать учебными встрясками и интеграцией опыта прошлых инцидентов.</p>
<ul>
<li>Всё, что с системой случается, должно попадать в общую память: DIS (<a href="/posts/digital-immune-system/">Digital Immune System</a>, назови как хочешь) — не украшение, а база для эволюции. Чем зрелый иммунитет отличается от набора купленных практик — <a href="/posts/digital-immune-system-maturity/">разбирал отдельно</a>.</li>
<li>Если смежный сервис начал чаще отвечать ошибками — это не только про ретраи, это триггер к запуску graceful degradation (GD) или fallback именно на этом участке.</li>
<li>Exponential backoff — не про «красивые алгоритмы», а про то, чтобы ретраи не продлевали агонию инцидента, а давали системе шанс «встать с колен».</li>
</ul>
<p>Идеальная связка: DIS аккумулирует историю ошибок, GD даёт «план Б», fallback закрывает совсем плохие сценарии, чтобы пользователю не прилетал унылый «502 Bad Gateway».</p>
<h2 id="graceful-degradation-всегда-отвечать-но-не-всегда-всем">Graceful Degradation: всегда отвечать, но не всегда всем</h2>
<p>Если в голове GD = «мы иногда отваливаемся по лёгкому», то ничего хорошего не выйдет.</p>
<ul>
<li>Правильная GD — это когда сервис <strong>всегда отвечает</strong>, но не всегда полной функциональностью.</li>
<li>Нужны <strong>уровни</strong> деградации: от выключения «сопутствующих товаров» до отдачи статики «техработы, вот наши лучшие предложения».</li>
<li>Кэширование и заглушки — такие же инструменты GD: лучше показать «Топ‑5» или дефолтный вариант, чем ничего.</li>
</ul>
<p>Очень важный момент — GD надо согласовывать с продактом заранее: какие блоки отключаем сначала, какие — потом, что для бизнеса критично, а что можно отрубить без слёз.</p>
<h2 id="fallback-когда-даже-gd-не-спасает">Fallback: когда даже GD не спасает</h2>
<p>Fallback — это не «ещё одна страница с ошибкой», это реальный последний шанс дать пользователю хоть какую‑то ценность.</p>
<ul>
<li>Fallback лучше любого 5xx: даже тетрис или одна страница с лучшим товаром лучше, чем пустой 502.</li>
<li>Важно мониторить, <strong>как часто</strong> показывается fallback: это отдельный сигнал о классах запросов, которые система не может обработать корректно.</li>
<li>Fallback и GD — не взаимоисключающие штуки: GD работает «по пути» запроса, fallback — когда пути уже не осталось.</li>
</ul>
<p>По сути, GD и fallback — это про уважение к пользователю: либо ты честно пытаешься дать хотя бы что‑то, либо молча падаешь в 5xx и надеешься, что он когда-нибудь вернётся.</p>
<h2 id="мониторинг-который-действительно-помогает">Мониторинг, который действительно помогает</h2>
<p>Мониторинг «чтобы был» только создает иллюзию контроля. Полезный мониторинг строится из нескольких идей.</p>
<h3 id="абсолютные-значения-и-тренды">Абсолютные значения и тренды</h3>
<ul>
<li>Контролируй трафик по <strong>абсолютным значениям</strong> сверху и снизу — чтобы видеть и внезапные всплески, и подозрительные провалы.</li>
<li>Используй <strong>тренды</strong>, чтобы ловить аномалии в поведении (типичное для дня недели, часа и т.д.), но помни: плавные изменения тренд может спокойно «проглотить».</li>
<li>Рабочий рецепт: совмещать мониторинг по абсолюту и по тренду, а не выбирать что‑то одно.</li>
</ul>
<h3 id="среднее-minmax-и-перцентили">Среднее, min/max и перцентили</h3>
<ul>
<li>Среднее без min/max — это классика «средней температуры по больнице».</li>
<li>Важно отслеживать <strong>min/max</strong> и перцентили (p90, p95, p99), потому что именно они часто первыми намекают на будущий сбой.</li>
<li>Если по агрегатам всё красиво, а один хост уже умирает — без min/max ты про это узнаешь только из инцидента.</li>
<li>Если поверх перцентилей у тебя живут SLO, то алерты вешаются на burn rate. Только помни, что <a href="/posts/burn-rate-is-not-speed/">это безразмерный коэффициент, а не «скорость»</a>, и на маленьком трафике он врёт.</li>
</ul>
<h3 id="молчаливый-мониторинг-и-резервный-мониторинг">Молчаливый мониторинг и резервный мониторинг</h3>
<ul>
<li>«Если мониторинг не пишет о проблемах — возможно, он просто не работает» — очень неприятная правда.</li>
<li>Резервный мониторинг на других технологиях (хоть отдельная Grafana+VM на retention 2 дня) — это не паранойя, а нормальная страховка.</li>
<li>Мониторинг должен быть не только внутренним, но и внешним: проверять доступность глазами пользователя из важных локаций.</li>
</ul>
<p>По хорошему, у тебя должны мониториться: сами сервисы, их зависимости, мониторинг, срок годности сертификатов/лицензий и <a href="/posts/wat-monitoring/">всё, что когда‑нибудь «протухнет»</a>.</p>
<h2 id="нагрузочное-тестирование-и-правильные-учения">Нагрузочное тестирование и «правильные» учения</h2>
<p>Учения и нагрузка — самая недооценённая часть надёжности: все про них говорят, но мало кто делает это регулярно и по уму.</p>
<ul>
<li>Учения без подготовки — must‑have: либо автоматика работает, либо в ходе учений становится ясно, что её нет и её надо сделать.</li>
<li>Сценарии учений надо <strong>рандомизировать</strong> и пересматривать, иначе это будет не Chaos Engineering, а театральная постановка «все всё знают заранее».</li>
<li>Нагрузочные тесты с одинаковыми результатами — тревожный знак: значит, тесты устарели или слишком аккуратно обходят острые углы.</li>
</ul>
<p>Как это выглядит вживую, хорошо было слышно в докладе Mindbox <a href="/posts/yandex-scale-2024/">на Yandex Scale</a>: описания экспериментов лежат рядом с кодом сервиса, на каждый эксперимент готов runbook по восстановлению, а окно для запуска — середина нагрузки, а не пик и не ночь.</p>
<p>Система с «отличными» нагрузочными тестами, которые никогда ничего не ломают, обычно падает от первого же реального всплеска.</p>
<h2 id="failover-расселение-сервисов-и-слон-с-моськой">Failover, расселение сервисов и «слон с моськой»</h2>
<p>Любая история про отказоустойчивость со временем приходит к трем вопросам: где у тебя точки отказа, кто с кем соседствует и как ты это всё переключаешь.</p>
<ul>
<li>Failover <strong>дороже</strong>, чем кажется: нужен не только второй инстанс, но и координатор, логика консенсуса и вся инфраструктура вокруг.</li>
<li>Не сажай «слона и моську» в одну базу: не смешивай критичные сервисы с теми, у кого неконтролируемый трафик (сбор аналитики, события с фронта и т.п.).</li>
<li>Микросервисный подход с явной разметкой по критичности: выделенные ресурсы для разных классов сервисов.</li>
</ul>
<p>Как это раскладывается по слоям — что резервируем на фронте, что на бэке, а что в базе — <a href="/posts/ha-clustering/">разбирал в посте про кластеризацию и доступность</a>.</p>
<p>Критичный сервис плюс «шумный» некритичный — идеальный рецепт для самодельной DDoS‑атаки на самого себя.</p>
<h2 id="capacity-management-и-finops-сколько-мощности-тебе-реально-нужно">Capacity Management и FinOps: сколько мощности тебе реально нужно</h2>
<p>«Просто добавить ресурсов» — худшая из возможных стратегий, особенно в облаке.</p>
<ul>
<li>Любой запас мощности должен быть осмысленным: под какой сценарий он нужен, на какой срок и при каких ограничениях по бюджету.</li>
<li>Capacity‑планирование нужно делать по <strong>CUJ</strong> (Critical User Journey): самые важные клиентские маршруты должны иметь понятный запас.</li>
<li>Это не только про железо, но и про архитектуру: где узкие места, какой компонент ограничивает пропускную способность и как быстро ты можешь масштабироваться.</li>
</ul>
<p>По сути, это чистый FinOps: договариваешься с бизнесом, какой трафик обязан выдерживать, а какой — нет, и уже под это строишь схему резервирования и бюджетов. И это не разовая мода — <a href="/posts/sre-evolution-2020-2025/">за 2020-2025 деньги окончательно въехали в зону ответственности SRE</a> вместе с Platform Engineering.</p>
<h2 id="gitops-версии-и-самодостаточный-релиз">GitOps, версии и «самодостаточный релиз»</h2>
<p>Очень много боли в инцидентах рождается не из багов в коде, а из конфигов и «плавающих» зависимостей.</p>
<ul>
<li>Всё, что имеет ценность и выглядит как файл, должно жить в VCS: скрипты, конфиги, инфраструктурные артефакты. Дашборды Grafana сюда тоже входят — их проще <a href="/posts/grafana-exports/">выгрузить через API и положить в provisioning</a>, чем перекликивать мышкой после переезда.</li>
<li>GitOps‑подход: релиз — это не только «бинарь» или образ, но и конфиги, секреты, схемы, миграции, всё с версионированием.</li>
<li>Самодостаточный релиз должен гарантированно уметь работать сам по себе и уметь откатываться вместе с конфигами и внешними зависимостями.</li>
</ul>
<p>Отдельная боль — изменения «на проде руками без бэкапа»: перед любым вмешательством в БД или конфиги должен быть свежий бэкап или хотя бы сохранённая копия файла с суффиксом даты.</p>
<h2 id="фичатогглы-canary-whitelistblacklist">Фичатогглы, canary, whitelist/blacklist</h2>
<p>Любая новая фича потенциально ломает прод — вопрос только в том, насколько контролируемо.</p>
<ul>
<li>Фичи надо выкатывать <strong>выключенными</strong> и включать через конфиги/рубильники: это даёт быстрый roll‑back без пересборки.</li>
<li>Whitelist/blacklist помогают управлять долей трафика: сначала включаешь фичу на сегмент, потом расширяешь, потом можешь точечно отключать.</li>
<li>В стабилизированном состоянии прод должен иметь одну версию сервиса: параллельные версии «Ok» только на время A/B и canary, а не «так и живём».</li>
</ul>
<p>Фичи, которые нельзя быстро и адресно отключить — это не фичи, а долговые ямы для дежурных.</p>
<h2 id="процессы-инструкции-и-людочка-на-ресепшене">Процессы, инструкции и «Людочка на ресепшене»</h2>
<p>Хорошие процессы — это не бюрократия, а защита от хаоса в самый неподходящий момент.</p>
<ul>
<li>Стандартизированные процессы с чёткими, зафиксированными шагами — лучший фундамент для будущей автоматизации.</li>
<li>Инструкция должна быть такова, чтобы её могла выполнить «Людочка» или условная секретарша: копировать команды, открывать ссылки и получать предсказуемый результат. Писать её стоит как код, с тем же ревью и версиями — <a href="/posts/rtfm-dry-kiss/">про RTFM, DRY и документацию у меня есть отдельный текст</a>.</li>
<li>Плановые работы должны делаться <strong>по регламенту</strong>, без «давай заодно тут поправим, пока уже всё отключили».</li>
</ul>
<p>Сюда же — план Б на каждое нетипичное вмешательство: если «всё пойдёт не так», должен быть понятный сценарий возврата в состояние «как было». Предел мечтаний тут — инструкция, которую вообще не надо читать руками: <a href="/posts/tron-as-sre/">Tron из одноимённого фильма</a> как раз про автоматический runbook remediation, только с мечом.</p>
<h2 id="люди-психология-и-работа-в-стрессе">Люди, психология и работа в стрессе</h2>
<p>Любой серьёзный инцидент — это ещё и психологическая нагрузка, и к ней тоже надо готовиться.</p>
<ul>
<li>У каждого свой «деструктор» в стрессе: кто‑то начинает хаотично нажимать всё подряд, кто‑то «замерзает», кто‑то исчезает из коммуникации.</li>
<li>Личный чек‑лист дежурного («подышать», «записать идеи действий», «пройтись», «открыть инструкцию по оповещению») — очень мощный инструмент.</li>
<li>Интуицию стоит слушать: если внутри зудит «сделать бэкап» или «снять трафик» — лучше перебдеть, чем недобдеть.</li>
</ul>
<p>Отдельная история — групповые дежурства: если «ответственность на всех», то по факту не отвечает никто, поэтому нужны конкретные фамилии в графике. Как делить зоны ответственности внутри команды, чтобы график не был формальностью, <a href="/posts/organize-sysadmins/">писал ещё во времена сисадминских будней</a>.</p>
<h2 id="коммуникация-смежники-продакты-пользователи-твоих-инструментов">Коммуникация: смежники, продакты, пользователи твоих инструментов</h2>
<p>Надёжность ломается не только в коде, но и на стыках людей и команд.</p>
<ul>
<li>Если сервис <em>А</em> положил сервис <em>Б</em> десятикратным ростом запросов — виноваты оба: <em>А</em> не ограничил нагрузку, <em>Б</em> не предусмотрел верхнюю границу того, что он может переварить.</li>
<li>Разборы инцидентов должны быть <strong>публичными</strong>: любой участник должен иметь возможность задать вопрос, особенно из смежных команд.</li>
<li>С теми, кто пользуется твоими панелями и инструментами, надо регулярно говорить и смотреть «их глазами» на интерфейс — очень часто это приносит внезапные открытия, потому что очевидное для тебя — неочевидно для других.</li>
</ul>
<p>Плюс базовая дисциплина: общий календарь маркетинговых запусков и регламентных работ, понятные термины и общий словарь. Это и есть та часть <a href="/posts/wat-devops/">DevOps</a>, которая про совместную работу команд, а не про очередной пайплайн.</p>
<h2 id="безопасность-как-часть-надёжности">Безопасность как часть надёжности</h2>
<p>Про ИБ легко забыть, пока не прилетело.</p>
<ul>
<li>Требования информационной безопасности — это не внешняя бюрократия, а часть надёжности и доступности.</li>
<li>Чем раньше ты начнёшь приводить сервис к ИБ‑гайдам, тем меньше будет боли потом: всё равно придётся, но лучше не в режиме горящей пятой точки.</li>
<li>Debug‑режимы, скрытые параметры, хардкод в фронтенде, незащищённые «магические» урлы — всё это при первом же fuzzing&rsquo;е превращается в инцидент.</li>
</ul>
<p>ИБ — просто ещё один класс рисков для надёжности, и игнорировать его — то же самое, что не мониторить диск или БД. Как это выглядит, когда риск реализуется целиком, хорошо видно на <a href="/posts/hacking-aeroflot/">разборе истории с «Аэрофлотом»</a>.</p>
<h2 id="несколько-практических-выводов">Несколько практических выводов</h2>
<p>Соберём самое важное в виде коротких тезисов — то, что полезно прямо взять и примерить к своему проекту:</p>
<ul>
<li><strong>Формализуй принципы команды.</strong> Пойми, что для вас нормально, а что — нет, и как вы принимаете решения в проде.</li>
<li><strong>Строй иммунитет системы.</strong> Собирай данные об инцидентах, запускай учения, внедряй DIS, GD и fallback как связанный комплекс.</li>
<li><strong>Наведи порядок в мониторинге.</strong> Абсолютные значения + тренды, min/max + перцентили, резервный мониторинг и внешние проверки. И добавь сверху <a href="/posts/business-metrics/">бизнес-метрики</a> — без них ты видишь живые хосты, но не видишь, что оплата не проходит.</li>
<li><strong>Инвестируй в процесс.</strong> Стандартизированные инструкции, регламенты, планы отката, регулярные учения по релизам и rollback’ам.</li>
<li><strong>Живи через GitOps и версии.</strong> Всё важное — в VCS, релиз самодостаточен, откат — быстрый и предсказуемый. <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/gitops/" rel="noopener noreferrer" target="_blank">Лист про GitOps</a> — про то, чем модель pull отличается от «задеплоил из пайплайна».</li>
<li><strong>Не забывай про людей.</strong> Психология дежурств, личные чек‑листы, явные дежурные, нормальная коммуникация с продактами и смежниками. <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/on-call-rotation/" rel="noopener noreferrer" target="_blank">Про устройство ротации</a> — покрытие, справедливость и выносливость — есть отдельный лист.</li>
</ul>
<p>Если всё это собрать вместе, то «чинить прод» перестаёт быть вечным героическим подвигом и превращается в спокойную инженерную работу. А это, кажется, ровно то, к чему хочется прийти.</p>
<p>PS: Читать книгу &ldquo;<a href="https://www.litres.ru/book/natalya-savenkova/sre-recepty-vyzhivaniya-v-prodakshene-dlya-inzhenera-po-70232503/" rel="noopener noreferrer" target="_blank">SRE. Рецепты выживания в продакшне для инженера по надежности</a>&rdquo; за авторством <a href="https://www.litres.ru/author/natalya-savenkova/" rel="noopener noreferrer" target="_blank">Натальи Савенковой</a> было и смешно и грустно одновременно. Но главная ценность лично для меня — я увидел, что я не одинок в своих выводах!</p>
</div>]]></content:encoded>
    </item><item>
      <title>Digital Immune System: цифровой иммунитет твоей инфраструктуры</title>
      <link>https://jtprog.ru/posts/digital-immune-system/</link>
      <pubDate>Tue, 10 Feb 2026 22:14:14 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/digital-immune-system/</guid>
      <category>SRE</category>
      <category>DIS</category>
      <category>Observability</category>
      <category>Chaos-Engineering</category>
      <category>Auto-Remediation</category>
      <category>DevOps</category>
      <category>Reliability</category>
      <category domain="category">Opinions</category>
      <enclosure url="https://jtprog.ru/opinions.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/opinions.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Сегодня поговорим о концепции, которую Gartner включил в топ-10 стратегических технологических трендов — <strong>Digital Immune System (DIS)</strong>, или «Цифровая иммунная система». Звучит футуристично, но на самом деле это про то, как сделать наши системы такими же адаптивными и самовосстанавливающимися, как человеческий организм.</p>
<h2 id="что-это-такое">Что это такое?</h2>
<p>Gartner определяет DIS как комбинацию практик и технологий проектирования, разработки, автоматизации, эксплуатации и аналитики ПО, направленных на снижение бизнес-рисков и создание превосходного пользовательского опыта. Главная идея — система должна не просто реагировать на сбои, а <strong>предвидеть их, противостоять им и автоматически восстанавливаться</strong>.</p>
<p>Проще говоря: вместо того чтобы тушить пожары каждый раз, когда что-то падает, мы строим систему, которая сама умеет обнаруживать проблемы, минимизировать их влияние и быстро возвращаться в норму — без ручного вмешательства или с минимальным участием команд разработки и эксплуатации.</p>
<h2 id="шесть-столпов-dis">Шесть столпов DIS</h2>
<p>По версии Gartner, цифровой иммунитет держится на шести ключевых практиках:</p>
<ol>
<li><strong>Observability (Наблюдаемость)</strong> — глубокое понимание состояния системы через логи, метрики и трейсы. Это не просто мониторинг, а возможность в реальном времени видеть, что происходит внутри.</li>
<li><strong>AI-augmented Testing (Тестирование с AI)</strong> — использование машинного обучения для автоматизации тестирования, предсказания дефектов и адаптации тест-кейсов к изменениям кода.</li>
<li><strong>Chaos Engineering (Хаос-инженерия)</strong> — контролируемые эксперименты с отказами, чтобы выявить слабые места до того, как они ударят по пользователям. Для SRE это прямой путь к снижению MTTR и повышению MTBF.</li>
<li><strong>Auto Remediation (Автоматическое исправление)</strong> — система сама устраняет проблемы без участия человека: перезапускает сервисы, масштабирует ресурсы, откатывает изменения.</li>
<li><strong>Site Reliability Engineering (SRE)</strong> — обеспечение непрерывности работы через автоматизацию, <a href="/posts/slo-as-architecture-blueprint/">SLO/SLA</a> и культуру надежности.</li>
<li><strong>Software Supply Chain Security</strong> — защита всей цепочки поставок ПО, от зависимостей до деплоя, чтобы предотвратить внедрение уязвимостей.</li>
</ol>
<h2 id="почему-это-важно-для-sre-и-devops">Почему это важно для SRE и DevOps?</h2>
<p>Gartner прогнозирует, что организации, инвестирующие в DIS, снизят даунтайм и значительно повысят удовлетворенность клиентов. В реальности это означает:</p>
<ul>
<li><strong>Меньше инцидентов</strong> — благодаря превентивному обнаружению проблем через observability и chaos engineering;</li>
<li><strong>Быстрее восстановление</strong> — auto remediation сокращает MTTR, автоматически реагируя на сбои;</li>
<li><strong>Выше надежность</strong> — системы проектируются с учетом отказоустойчивости с самого начала (Secure by Design);</li>
<li><strong>Меньше ручной работы</strong> — автоматизация освобождает время на стратегические задачи, а не на тушение пожаров.</li>
</ul>
<h2 id="digital-immune-system-vs-традиционный-подход">Digital Immune System vs традиционный подход</h2>
<p>Традиционно мы реагируем на проблемы постфактум: упало — подняли, нашли баг — зафиксили. DIS переворачивает этот подход: системы становятся <strong>проактивными</strong>. Они постоянно тестируют себя на прочность (chaos engineering), адаптируются к изменениям (AI-testing), мониторят каждый аспект работы (observability) и самостоятельно исправляют ошибки (auto remediation).</p>
<p>Это особенно актуально для распределенных систем и микросервисной архитектуры, где один сбой может каскадом распространиться на десятки зависимых сервисов.</p>
<h2 id="связь-с-другими-концепциями">Связь с другими концепциями</h2>
<p>Если ты следил за эволюцией подходов к безопасности и надежности, то заметишь, что DIS пересекается с <strong>Cyber Resilience</strong> (готовность к взлому), <strong>Zero Trust</strong> (никому не доверяй, всегда проверяй) и <strong>DevSecOps</strong> (безопасность на всех этапах SDLC). Но ключевое отличие DIS — это акцент на <strong>автоматическом самовосстановлении</strong> и <strong>встроенной безопасности с момента проектирования</strong> (а не в виде наложенных средств защиты).</p>
<p>В российском контексте похожий подход развивают «Лаборатория Касперского» и «Авито» под названием <strong>кибериммунитет</strong> — системы, где безопасность встроена в архитектуру и код, минимизирована доверенная кодовая база, а защита активирована по умолчанию (Secure by Design, Secure by Default). Аналогичные подходы развивают и другие компании, с той лишь разницей, что они не собирают это в единую экосистему надежности и их подходы находятся в более фрагментарном состоянии.</p>
<h2 id="как-применять-dis-на-практике">Как применять DIS на практике?</h2>
<p>Не обязательно внедрять все шесть столпов сразу. Начни с того, что ближе твоей команде:</p>
<ul>
<li>Уже используешь Prometheus и Grafana? Расширь их до full-stack observability с трейсингом (OpenTelemetry) и добавь к техническим метрикам <a href="/posts/business-metrics/">бизнесовые</a> — иначе иммунитет видит железо, но не видит, что пользователю плохо.</li>
<li>Есть CI/CD? Интегрируй AI-powered тестирование и SAST/SCA для раннего обнаружения багов.</li>
<li>Хочешь проверить устойчивость? Запусти chaos experiments с инструментами типа Chaos Monkey или Gremlin.</li>
<li>Настроил алерты? Добавь auto remediation — скрипты, которые автоматически реагируют на типовые инциденты.</li>
</ul>
<p>Главное — не пытаться достичь идеала сразу. DIS — это итеративный процесс, где каждая новая практика усиливает предыдущие. Про то, чем зрелый иммунитет отличается от набора купленных практик и где цикл «измерили → подействовали» чаще всего разомкнут, я написал отдельно — <a href="/posts/digital-immune-system-maturity/">Digital Immune System: инженерия устойчивости как продукт</a>. А совсем приземлённые рецепты, которые работают ещё до всякого DIS, собраны в <a href="/posts/sre-recipes-summary/">заметках про починку прода</a>. Если раскладывать столпы по конкретным навыкам, ближе всего <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/resilience-patterns/" rel="noopener noreferrer" target="_blank">Resilience Patterns</a> и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/chaos-engineering/" rel="noopener noreferrer" target="_blank">Chaos Engineering</a> из карты компетенций.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Сегодня поговорим о концепции, которую Gartner включил в топ-10 стратегических технологических трендов — <strong>Digital Immune System (DIS)</strong>, или «Цифровая иммунная система». Звучит футуристично, но на самом деле это про то, как сделать наши системы такими же адаптивными и самовосстанавливающимися, как человеческий организм.</p>
<h2 id="что-это-такое">Что это такое?</h2>
<p>Gartner определяет DIS как комбинацию практик и технологий проектирования, разработки, автоматизации, эксплуатации и аналитики ПО, направленных на снижение бизнес-рисков и создание превосходного пользовательского опыта. Главная идея — система должна не просто реагировать на сбои, а <strong>предвидеть их, противостоять им и автоматически восстанавливаться</strong>.</p>
<p>Проще говоря: вместо того чтобы тушить пожары каждый раз, когда что-то падает, мы строим систему, которая сама умеет обнаруживать проблемы, минимизировать их влияние и быстро возвращаться в норму — без ручного вмешательства или с минимальным участием команд разработки и эксплуатации.</p>
<h2 id="шесть-столпов-dis">Шесть столпов DIS</h2>
<p>По версии Gartner, цифровой иммунитет держится на шести ключевых практиках:</p>
<ol>
<li><strong>Observability (Наблюдаемость)</strong> — глубокое понимание состояния системы через логи, метрики и трейсы. Это не просто мониторинг, а возможность в реальном времени видеть, что происходит внутри.</li>
<li><strong>AI-augmented Testing (Тестирование с AI)</strong> — использование машинного обучения для автоматизации тестирования, предсказания дефектов и адаптации тест-кейсов к изменениям кода.</li>
<li><strong>Chaos Engineering (Хаос-инженерия)</strong> — контролируемые эксперименты с отказами, чтобы выявить слабые места до того, как они ударят по пользователям. Для SRE это прямой путь к снижению MTTR и повышению MTBF.</li>
<li><strong>Auto Remediation (Автоматическое исправление)</strong> — система сама устраняет проблемы без участия человека: перезапускает сервисы, масштабирует ресурсы, откатывает изменения.</li>
<li><strong>Site Reliability Engineering (SRE)</strong> — обеспечение непрерывности работы через автоматизацию, <a href="/posts/slo-as-architecture-blueprint/">SLO/SLA</a> и культуру надежности.</li>
<li><strong>Software Supply Chain Security</strong> — защита всей цепочки поставок ПО, от зависимостей до деплоя, чтобы предотвратить внедрение уязвимостей.</li>
</ol>
<h2 id="почему-это-важно-для-sre-и-devops">Почему это важно для SRE и DevOps?</h2>
<p>Gartner прогнозирует, что организации, инвестирующие в DIS, снизят даунтайм и значительно повысят удовлетворенность клиентов. В реальности это означает:</p>
<ul>
<li><strong>Меньше инцидентов</strong> — благодаря превентивному обнаружению проблем через observability и chaos engineering;</li>
<li><strong>Быстрее восстановление</strong> — auto remediation сокращает MTTR, автоматически реагируя на сбои;</li>
<li><strong>Выше надежность</strong> — системы проектируются с учетом отказоустойчивости с самого начала (Secure by Design);</li>
<li><strong>Меньше ручной работы</strong> — автоматизация освобождает время на стратегические задачи, а не на тушение пожаров.</li>
</ul>
<h2 id="digital-immune-system-vs-традиционный-подход">Digital Immune System vs традиционный подход</h2>
<p>Традиционно мы реагируем на проблемы постфактум: упало — подняли, нашли баг — зафиксили. DIS переворачивает этот подход: системы становятся <strong>проактивными</strong>. Они постоянно тестируют себя на прочность (chaos engineering), адаптируются к изменениям (AI-testing), мониторят каждый аспект работы (observability) и самостоятельно исправляют ошибки (auto remediation).</p>
<p>Это особенно актуально для распределенных систем и микросервисной архитектуры, где один сбой может каскадом распространиться на десятки зависимых сервисов.</p>
<h2 id="связь-с-другими-концепциями">Связь с другими концепциями</h2>
<p>Если ты следил за эволюцией подходов к безопасности и надежности, то заметишь, что DIS пересекается с <strong>Cyber Resilience</strong> (готовность к взлому), <strong>Zero Trust</strong> (никому не доверяй, всегда проверяй) и <strong>DevSecOps</strong> (безопасность на всех этапах SDLC). Но ключевое отличие DIS — это акцент на <strong>автоматическом самовосстановлении</strong> и <strong>встроенной безопасности с момента проектирования</strong> (а не в виде наложенных средств защиты).</p>
<p>В российском контексте похожий подход развивают «Лаборатория Касперского» и «Авито» под названием <strong>кибериммунитет</strong> — системы, где безопасность встроена в архитектуру и код, минимизирована доверенная кодовая база, а защита активирована по умолчанию (Secure by Design, Secure by Default). Аналогичные подходы развивают и другие компании, с той лишь разницей, что они не собирают это в единую экосистему надежности и их подходы находятся в более фрагментарном состоянии.</p>
<h2 id="как-применять-dis-на-практике">Как применять DIS на практике?</h2>
<p>Не обязательно внедрять все шесть столпов сразу. Начни с того, что ближе твоей команде:</p>
<ul>
<li>Уже используешь Prometheus и Grafana? Расширь их до full-stack observability с трейсингом (OpenTelemetry) и добавь к техническим метрикам <a href="/posts/business-metrics/">бизнесовые</a> — иначе иммунитет видит железо, но не видит, что пользователю плохо.</li>
<li>Есть CI/CD? Интегрируй AI-powered тестирование и SAST/SCA для раннего обнаружения багов.</li>
<li>Хочешь проверить устойчивость? Запусти chaos experiments с инструментами типа Chaos Monkey или Gremlin.</li>
<li>Настроил алерты? Добавь auto remediation — скрипты, которые автоматически реагируют на типовые инциденты.</li>
</ul>
<p>Главное — не пытаться достичь идеала сразу. DIS — это итеративный процесс, где каждая новая практика усиливает предыдущие. Про то, чем зрелый иммунитет отличается от набора купленных практик и где цикл «измерили → подействовали» чаще всего разомкнут, я написал отдельно — <a href="/posts/digital-immune-system-maturity/">Digital Immune System: инженерия устойчивости как продукт</a>. А совсем приземлённые рецепты, которые работают ещё до всякого DIS, собраны в <a href="/posts/sre-recipes-summary/">заметках про починку прода</a>. Если раскладывать столпы по конкретным навыкам, ближе всего <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/resilience-patterns/" rel="noopener noreferrer" target="_blank">Resilience Patterns</a> и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/chaos-engineering/" rel="noopener noreferrer" target="_blank">Chaos Engineering</a> из карты компетенций.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Эволюция практик Site Reliability Engineering: краткий анализ тенденций 2020-2025</title>
      <link>https://jtprog.ru/posts/sre-evolution-2020-2025/</link>
      <pubDate>Mon, 05 Jan 2026 19:16:29 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/sre-evolution-2020-2025/</guid>
      <category>SRE</category>
      <category>Observability</category>
      <category domain="category">Opinions</category>
      <enclosure url="https://jtprog.ru/opinions.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/opinions.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Попытался разобраться, куда катится Site Reliability Engineering (SRE) за последние лет пять. Ниже накидал мысли по этому поводу. Ссылки на все отчеты вроде не забыл, так что зацени, а потом го в комменты – обсудим вместе.</p>
<h2 id="что-там-в-отчетах">Что там в отчетах?</h2>
<p>Короче, за последние лет пять SRE стала прямо хайповой темой для исследований. Вот самые толковые источники, чтобы быть в теме, что в SRE происходит:</p>
<ul>
<li><strong>Catchpoint — The SRE Report</strong> (<a href="https://www.catchpoint.com/asset/2020-sre-report" rel="noopener noreferrer" target="_blank">2020</a>, <a href="https://www.catchpoint.com/asset/2021-sre-report" rel="noopener noreferrer" target="_blank">2021</a>, <a href="https://www.catchpoint.com/asset/2023-sre-report" rel="noopener noreferrer" target="_blank">2023</a>, <a href="https://www.catchpoint.com/asset/2024-sre-report" rel="noopener noreferrer" target="_blank">2024</a>, <a href="https://www.catchpoint.com/asset/2025-sre-report" rel="noopener noreferrer" target="_blank">2025</a>): Это, наверное, самый жирный отчет прям по SRE. В 2025 опросили больше 300 спецов из разных областей. Говорят, это типа реальное мнение людей из SRE команд, без воды и прикрас.</li>
<li><strong>DORA (DevOps Research and Assessment)</strong>: Каждый год (<a href="https://dora.dev/research/2020/" rel="noopener noreferrer" target="_blank">2020</a>, <a href="https://dora.dev/research/2021/" rel="noopener noreferrer" target="_blank">2021</a>, <a href="https://dora.dev/research/2022/" rel="noopener noreferrer" target="_blank">2022</a>, <a href="https://dora.dev/research/2023/" rel="noopener noreferrer" target="_blank">2023</a>, <a href="https://dora.dev/research/2024/" rel="noopener noreferrer" target="_blank">2024</a>, <a href="https://dora.dev/research/2022/" rel="noopener noreferrer" target="_blank">2025</a>) катают отчеты, где уже лет десять меряют, насколько круто DevOps и SRE работают. Смотрят на четыре главные метрики. В 2025 даже целый отчет выпустили про то, как AI влияет на разработку.</li>
<li><strong><a href="https://www.devopsinstitute.com/global-sre-pulse/" rel="noopener noreferrer" target="_blank">DevOps Institute — Global SRE Pulse 2022</a></strong>: Это было первое большое исследование, где опросили больше 460 SRE спецов и манагеров из средних и больших компаний. В отчете можно глянуть, как сейчас вообще живёт SRE, какие штуки юзают, как команды себя чувствуют и насколько все заавтоматизировано.</li>
<li><strong><a href="https://www.puppet.com/resources/history-of-devops-reports" rel="noopener noreferrer" target="_blank">Puppet — State of DevOps</a></strong> (2020-2021, 2023): В 2021 отметили десятилетие этих отчетов и выяснили, что у 78% компаний с DevOps дела так себе, где-то на середине. В 2023 больше смотрели на платформенную инженерию.</li>
<li><strong>Forrester Research</strong> (<a href="https://www.forrester.com/report/five-key-trends-to-benchmark-devops-progress/RES120481" rel="noopener noreferrer" target="_blank">2020</a>): Выкатили отчет про десять главных трендов в инфраструктуре и сказали, что SRE – это база для стабильной работы всего.</li>
<li><strong>Gartner Hype Cycle</strong> (<a href="https://www.gartner.com/en/articles/hype-cycle-for-emerging-technologies" rel="noopener noreferrer" target="_blank">2025</a>): Закинули SRE и Platform Engineering на свой график взросления технологий. Ещё упомянули всякие приколы для управления окружениями, чтоб каждый мог сам себе все сделать.</li>
<li><strong>ThoughtWorks Technology Radar</strong> (постоянно, <a href="https://www.thoughtworks.com/radar/archive" rel="noopener noreferrer" target="_blank">2020-2025</a>): Эти ребята вообще всегда следят, как меняются инструменты и практики в SRE, наблюдаемости и чтоб системы были надежными.</li>
</ul>
<h2 id="главные-изменения-в-sre">Главные изменения в SRE</h2>
<h3 id="1-рост-toil-неожиданный-поворот-в-2025">1. Рост Toil: неожиданный поворот в 2025</h3>
<p><em>Что-то пошло не так</em>: Впервые за пять лет нудной работы (Toil) стало больше. Раньше было 14% времени, а теперь все 20%. Задачи по операционке тоже времени жрут больше – теперь 30% вместо 20%.</p>
<p>Все думали, с AI станет легче. Эксперты из Catchpoint удивились: ждали, что AI уберет рутину, а он наоборот, ее добавил. Может, все дело в том, что AI помогает быстрее делать полезные вещи, а свободное время забивается всякой фигней.</p>
<p><em>Как было раньше</em>: С 2020 по 2024 нудной работы становилось все меньше. Спасибо автоматизации, IaC, CI/CD и системам, которые сами чинятся. Google советует, чтоб такой работы было не больше половины времени инженера, а у SRE – не больше половины на операционку.</p>
<p><strong>Как бороться с Toil в 2025</strong>:</p>
<ul>
<li>Автоматизация через Terraform, Ansible, CI/CD пайплайны.</li>
<li>Внедряйте self-healing системы, чтоб все чинилось само.</li>
<li>Стандартизируйте все через плейбуки и runbooks.</li>
<li>AIOps инструменты – чтобы алерты сами между собой разбирались и мониторили все на будущее.</li>
<li>Переходите на self-service порталы вместо тикетов.</li>
</ul>
<p>Считать toil при этом надо раньше, чем автоматизировать: без классификации и замера автоматизируется то, что громче болит, а не то, что дороже обходится. <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/toil-tracking/" rel="noopener noreferrer" target="_blank">Про учёт toil</a> — отдельный лист в карте компетенций.</p>
<h3 id="2-ai-и-machine-learning-уже-не-просто-побаловаться-а-без-них-никуда">2. AI и Machine Learning: уже не просто побаловаться, а без них никуда</h3>
<p><strong>Как развивалось</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали пробовать AI в мониторинге и поиске аномалий.</li>
<li><strong>2021-2022</strong>: Все больше интересуются, как AI может предсказывать проблемы и автоматом чинить инциденты.</li>
<li><strong>2023-2024</strong>: Вовсю внедряют AIOps платформы (Datadog, Splunk, Dynatrace).</li>
<li><strong>2025</strong>: GenAI – это революция! 39% спецов говорят, что AI круто генерит код.</li>
</ul>
<p><strong>Где AI нужен в SRE 2025</strong>:</p>
<ul>
<li><strong>Предсказывать инциденты</strong>: AI изучает старые данные и говорит, где может рвануть.</li>
<li><strong>Автоматически генерить тесты</strong>: ML создает тесты, когда код меняется.</li>
<li><strong>AI помогает в ChatOps</strong>: встраивают LLM в процессы управления инцидентами.</li>
<li><strong>Системы, которые сами чинятся</strong>: AI находит аномалии и сам все исправляет.</li>
<li><strong>Улучшение производительности</strong>: AI анализирует, что можно улучшить в SLO.</li>
</ul>
<p><strong>Важный момент из DORA 2024</strong>: второй год подряд получается, что AI делает доставку софта хуже. Короче, AI – не волшебная таблетка, надо аккуратно внедрять и смотреть, что получается.</p>
<p><strong>Что думают про AI</strong>: 53% SRE думают, что AI сделает их работу легче, и только 4% боятся, что AI их заменит. При этом 30% решили, что надо учиться AI.</p>
<h3 id="3-platform-engineering-и-sre-теперь-вместе-навсегда">3. Platform Engineering и SRE: теперь вместе навсегда</h3>
<p><strong>Как менялись отношения</strong>:</p>
<ul>
<li><strong>2020</strong>: Platform Engineering как отдельную штуку вообще не выделяли, все было в DevOps.</li>
<li><strong>2021-2022</strong>: Начали говорить про Platform Engineering.</li>
<li><strong>2023</strong>: Puppet выпустили отчет State of Platform Engineering, и 93% сказали, что это круто.</li>
<li><strong>2024-2025</strong>: Platform Engineering и SRE активно работают вместе.</li>
</ul>
<p><strong>В чем разница и как помогают друг другу</strong>:</p>
<ul>
<li><strong>Platform Engineering</strong> создает платформы, чтобы разрабам было проще работать: self-service, не надо думать про инфраструктуру, все стандартизировано.</li>
<li><strong>SRE</strong> следит, чтоб все работало надежно, SLO выполнялись, мониторит продакшн, разруливает инциденты и чтоб всего было в меру – и новых фич, и стабильности.</li>
<li><strong>Вместе им хорошо</strong>: если платформа сделана правильно, то у SRE меньше Toil, потому что все деплоится по стандарту и куча задач автоматизирована. А SRE, в свою очередь, делает платформы надежнее.</li>
</ul>
<p>Что при этом отличает платформу от кучи скриптов с общим README — границы, интерфейсы и измерение спроса — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/platform-engineering/" rel="noopener noreferrer" target="_blank">в листе про platform engineering</a>.</p>
<h3 id="4-finops-sre-теперь-еще-и-за-бабки-отвечает">4. FinOps: SRE теперь еще и за бабки отвечает</h3>
<p><strong>Как росла важность</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали понимать, что надо следить за расходами в облаке.</li>
<li><strong>2021-2022</strong>: FinOps стала отдельной темой.</li>
<li><strong>2023-2024</strong>: FinOps активно встраивается в SRE практики.</li>
<li><strong>2025</strong>: SRE команды вовсю юзают FinOps, чтобы тратить меньше денег в облаке.</li>
</ul>
<p><strong>Что SRE должны знать про FinOps</strong>:</p>
<ul>
<li>Ответственность за деньги – часть DevOps.</li>
<li>Как тратить меньше, но чтоб все работало надежно.</li>
<li>Как правильно выделять деньги на облачные ресурсы.</li>
<li>Оптимизация производительности за свои деньги.</li>
<li>Управление ресурсами.</li>
</ul>
<p>Catchpoint 2025 говорит, что Capacity Management теперь в приоритете у компаний, наравне с SRE, SLO и автоматизацией починки инцидентов. Как эта ответственность выглядит инженерно, а не как отчётность для финансов, — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/cloud-cost-control/" rel="noopener noreferrer" target="_blank">в листе про управление стоимостью облака</a>.</p>
<h3 id="5-opentelemetry-все-стандартизировали-наблюдаемость">5. OpenTelemetry: все стандартизировали наблюдаемость</h3>
<p><strong>Как быстро все начали юзать</strong>:</p>
<ul>
<li><strong>2020</strong>: Никто толком не знал (~5%).</li>
<li><strong>2021-2022</strong>: Уже многие в теме (~20-25% планируют или юзают).</li>
<li><strong>2023-2024</strong>: Уже хорошо (~48% юзают).</li>
<li><strong>2025</strong>: Практически все (~73% юзают или собираются).</li>
</ul>
<p><strong>Почему это круто</strong>:</p>
<ul>
<li><strong>Стандартизация</strong>: один стандарт для всего – как собирать данные, как их отправлять.</li>
<li><strong>Не надо зависеть от поставщиков</strong>: можно не привязываться к конкретному вендору.</li>
<li><strong>ROI</strong>: 46% компаний говорят, что ROI больше 20%, а еще 40% – 10-20%.</li>
<li><strong>Экономия</strong>: 84% компаний, которые стали меньше тратить на observability, говорят, что сэкономили минимум 10%.</li>
<li><strong>Зрелость</strong>: 81% считают, что OpenTelemetry уже достаточно взрослая штука.</li>
</ul>
<p><strong>Почему это важно для AI-SRE</strong>: OpenTelemetry – это база для AI, потому что он дает одинаковые, понятные данные для анализа.</p>
<p><strong>Но есть и проблемы</strong>: сложно внедрить, данных становится слишком много (в 4-5 раз больше), надо интегрировать со старыми системами и следить за расходами.</p>
<h3 id="6-chaos-engineering-теперь-это-не-просто-прикол-а-необходимость">6. Chaos Engineering: теперь это не просто прикол, а необходимость</h3>
<p><strong>Как это стало популярно</strong>:</p>
<ul>
<li><strong>2020</strong>: Какая-то странная штука, которую Netflix придумал.</li>
<li><strong>2021</strong>: 60% спецов хоть раз пробовали chaos-эксперименты.</li>
<li><strong>2022-2023</strong>: Все больше компаний юзают это, чтобы проверять, как системы работают при сбоях.</li>
<li><strong>2024-2025</strong>: Chaos Engineering стал важной частью SRE.</li>
</ul>
<p><strong>Как это работает с SRE</strong>:</p>
<ul>
<li><strong>Проверка на отказоустойчивость</strong>: специально ломают систему, чтоб найти слабые места.</li>
<li><strong>Валидация SLO/SLI</strong>: убеждаются, что все работает, даже когда что-то идет не так.</li>
<li><strong>Подготовка к инцидентам</strong>: тренируются реагировать, чтоб пользователи ничего не заметили.</li>
<li><strong>Уменьшение Toil</strong>: автоматом проверяют отказоустойчивость через CI/CD.</li>
<li><strong>Быстрее чинят</strong>: если заранее знают, что может сломаться, то быстрее восстановят.</li>
</ul>
<p><strong>Популярные инструменты 2025</strong>: Qinfinite by Quinnox, Gremlin, Chaos Mesh, LitmusChaos, AWS Fault Injection Simulator.</p>
<p><strong>Важный момент</strong>: Chaos Engineering помогает создать атмосферу, где не ищут виноватых, а стараются улучшить систему.</p>
<h3 id="7-slo-и-error-budgets-все-становится-серьезно">7. SLO и Error Budgets: все становится серьезно</h3>
<p><strong>Как взрослели</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали внедрять SLO, но часто не понимали, зачем это.</li>
<li><strong>2021-2022</strong>: Стало больше понимания, но 99% SRE не знают, как правильно определить SLO.</li>
<li><strong>2023-2024</strong>: SLO стали важны при принятии решений.</li>
<li><strong>2025</strong>: SLO помогают решать, что важнее – надежность или новые фичи, появились composite SLO и reliability scoring.</li>
</ul>
<p><strong>Что нового в SLO</strong>:</p>
<ul>
<li><strong>Composite SLOs</strong>: объединяют несколько SLO в одну метрику, чтобы видеть картину целиком.</li>
<li><strong>Reliability scoring</strong>: оценивают надежность системы в цифрах.</li>
<li><strong>Мониторинг в реальном времени и алерты</strong>: помогают управлять SLO.</li>
<li><strong>SLO Development Life Cycle (SLODLC)</strong>: как правильно внедрять SLO.</li>
</ul>
<p><strong>Error Budget Management – это целый рынок</strong>: он вырос с $1.2 млрд в 2024 году и, по прогнозам, будет $5.8 млрд к 2033 году. Это значит, что DevOps, SRE и cloud-native технологии становятся все популярнее.</p>
<p><strong>Проблемы с SLO</strong>:</p>
<ul>
<li>51% считают, что observability не очень, особенно там, где мало инструментов.</li>
<li>SLO бесполезны, если их не использовать постоянно, потому что системы меняются.</li>
<li>Надо думать о пользователях: SLO должны показывать, что пользователи чувствуют, а не только технические метрики. Как выводить SLI из критичного пользовательского пути, я разобрал в <a href="/posts/slo-as-architecture-blueprint/">«SLO как чертёж архитектуры»</a>, а про <a href="/posts/burn-rate-is-not-speed/">сгорание бюджета ошибок</a> — отдельно, потому что burn rate почти все понимают неверно.</li>
</ul>
<h3 id="8-автоматизация-управления-инцидентами-и-aiops">8. Автоматизация управления инцидентами и AIOps</h3>
<p><strong>Как развивалось</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали автоматом делать алерты и тикеты.</li>
<li><strong>2021-2022</strong>: Активно развивали runbook automation и self-healing.</li>
<li><strong>2023-2024</strong>: Внедрили AIOps, чтобы связывать события и предсказывать проблемы.</li>
<li><strong>2025</strong>: AI помогает управлять инцидентами, общаться на обычном языке и автоматом искать причины проблем.</li>
</ul>
<p><strong>Как выглядит incident management в 2025</strong>:</p>
<ul>
<li><strong>Видят все в реальном времени</strong>: AI анализирует логи и события и сразу находит закономерности.</li>
<li><strong>Алерты на будущее</strong>: ML предсказывает, где может случиться инцидент.</li>
<li><strong>Автоматически чинят</strong>: системы сами исправляют проблемы, без людей.</li>
<li><strong>Все общаются в одном месте</strong>: все стейкхолдеры сидят в одной системе.</li>
<li><strong>Удобные workflows</strong>: инструменты для управления инцидентами работают прямо в Slack.</li>
</ul>
<p><strong>Важный момент</strong>: 40% инженеров участвовали в 1-5 инцидентах за последние 30 дней. При этом руководители тоже в деле, не меньше, чем обычные инженеры. 14% сильно стрессуют после инцидентов, потому что им не хватает поддержки.</p>
<p><strong>Как дела с incident management (Global SRE Pulse 2022)</strong>: больше 70% говорят, что у них хорошо спроектирован процесс управления инцидентами и алерты обрабатываются четко.</p>
<p><strong>Как AI все изменил в 2025</strong>:</p>
<ul>
<li>На 56% точнее находят угрозы, чем старые системы.</li>
<li>Анализ инцидентов занимает 2.8 минуты вместо 18.5.</li>
<li>Автоматизируют 78% действий при обычных инцидентах, раньше было 35%.</li>
<li>Ложных срабатываний стало меньше – с 25% до 8%.</li>
</ul>
<h3 id="9-надежность-vs-скорость-кто-победит">9. Надежность vs. скорость: кто победит?</h3>
<p><strong>Плохая новость</strong>: большинство спецов (больше двух третей) чувствуют, что их заставляют выпускать релизы быстрее, чем это безопасно для надежности.</p>
<p><strong>Почему так происходит</strong>:</p>
<ul>
<li><strong>Уходят те, кто создавал SRE</strong>: эксперты уходят из компаний, и приоритеты меняются.</li>
<li><strong>Бизнес давит</strong>: хотят новые фичи и больше денег.</li>
<li><strong>Не все понимают одинаково</strong>: инженеры и руководители по-разному оценивают надежность – чем выше должность, тем больше уверенности, что все хорошо.</li>
</ul>
<p><strong>Как было раньше</strong>: это старая проблема – что важнее, быстро или безопасно. SRE пытаются это решить с помощью error budgets и SLO. Но в 2025 давление стало слишком сильным.</p>
<p><strong>Что делать</strong>:</p>
<ul>
<li>Регулярно проверять и обновлять оценки процессов.</li>
<li>Звать внешних экспертов.</li>
<li>Улучшать культуру SRE и учить всех в компании, что надежность – это важно.</li>
<li>Использовать error budgets, чтобы принимать решения.</li>
</ul>
<h3 id="10-структуры-sre-команд-и-как-их-организовать">10. Структуры SRE-команд и как их организовать</h3>
<p><strong>Основные варианты</strong> (из книги Establishing SRE Foundations):</p>
<ol>
<li><strong>You Build It, You Run It</strong>: разрабы сами отвечают и за разработку, и за эксплуатацию. Быстро деплоят, но много операционных задач и сложно управлять изменениями.</li>
<li><strong>You Build It, SRE Run It</strong>: разрабы пилят код, SRE занимаются операционкой. Может случиться разрыв между командами.</li>
<li><strong>You Build It, You and SRE Run It</strong>: все в ответе вместе – SRE управляют операционными задачами, разрабы пишут код, но все вместе смотрят на метрики.</li>
</ol>
<p><strong>Где команда SRE находится в компании</strong> (Global SRE Pulse 2022):</p>
<ul>
<li>30% — IT operations</li>
<li>22% — отдельная команда</li>
<li>18% — application design &amp; development</li>
<li>18% — IT infrastructure</li>
<li>3% — IT security</li>
</ul>
<p><strong>Типы команд</strong>:</p>
<ul>
<li><strong>Centralized</strong>: помогают многим продуктам/сервисам.</li>
<li><strong>Dedicated</strong>: занимаются конкретными продуктами.</li>
<li><strong>Platform-based</strong>: строят облачные платформы.</li>
<li><strong>Stack-based</strong>: отдельные команды для разных стеков приложений/инфраструктуры.</li>
</ul>
<p><strong>Главное правило Google SRE</strong>: максимум 50% времени на операционку, минимум 50% на engineering (улучшения, автоматизация, исправление проблем).</p>
<h3 id="11-observability-от-мониторинга-к-ai">11. Observability: от мониторинга к AI</h3>
<p><strong>Как менялось</strong>:</p>
<ul>
<li><strong>2020</strong>: Просто мониторили (метрики, логи).</li>
<li><strong>2021-2022</strong>: Перешли к observability (метрики + логи + трассировки).</li>
<li><strong>2023-2024</strong>: Observability всего стека, смотрят на user experience.</li>
<li><strong>2025</strong>: AI сам находит полезную информацию в телеметрии.</li>
</ul>
<p><strong>Что важно в 2025</strong>:</p>
<ul>
<li><strong>Slow is the new down</strong>: 53% компаний согласны, что плохая производительность – это так же плохо, как если бы все упало.</li>
<li><strong>Много инструментов – это нормально</strong>: большинство компаний используют 2-10 инструментов для мониторинга/observability, и это не проблема, если они приносят пользу.</li>
<li><strong>Разные данные</strong>: 81% компаний используют 2+ типа телеметрии, 43% – 4+ типа.</li>
<li><strong>Что в приоритете</strong>: логи (65%), метрики (56%), события и трассировки.</li>
</ul>
<p><strong>Как все поменялось</strong>: раньше observability была нужна, чтобы люди могли понять терабайты данных. Сейчас, с AI, главное, чтобы машина могла быстро найти нужную информацию.</p>
<h3 id="12-обучение-и-развитие">12. Обучение и развитие</h3>
<p><strong>Учиться надо</strong>: технические тренинги очень важны, чтобы освоить новые технологии, включая AI. Но каждая пятая компания на это забивает.</p>
<p><strong>Как учатся</strong> (Catchpoint 2025):</p>
<ul>
<li>Онлайн-платформы (55%).</li>
<li>Очное обучение (45%).</li>
<li>Конференции (41%).</li>
</ul>
<p><strong>Странно</strong>: все говорят, что учиться важно, но времени нет. Руководители учатся меньше других и, наверное, думают, что им это не нужно.</p>
<p><strong>Важно для карьеры</strong>: DevOps Institute говорит, что SRE – это хороший способ продвинуться в IT, если прокачивать нужные навыки.</p>
<h2 id="выводы-и-что-будет-дальше">Выводы и что будет дальше</h2>
<h3 id="главное-из-анализа">Главное из анализа</h3>
<ol>
<li><strong>Странности 2025</strong>: несмотря на AI и автоматизацию, Toil впервые вырос за пять лет, и AI пока не оправдывает надежд. Значит, дело не только в технологиях, но и в культуре и правильном подходе.</li>
<li><strong>SRE взрослеет</strong>: SRE стала серьезной штукой – 62% компаний в той или иной степени используют SRE (19% во всей компании, 55% в отдельных командах, 23% пробуют). SLO, error budgets и chaos engineering – это уже не ново, а стандарт.</li>
<li><strong>Все работают вместе</strong>: SRE, Platform Engineering, DevOps, DevSecOps и FinOps – это не конкуренты, а друзья, которые помогают друг другу. Компании, которые это понимают, получают больше выгоды.</li>
<li><strong>AI – это помощник, а не замена</strong>: AI помогает инженерам, а не заменяет их. AI должен быть частью культуры и процессов.</li>
<li><strong>Культура важнее</strong>: Puppet 2021 показал, что 78% компаний застряли на середине пути в DevOps/SRE, потому что думают только о технологиях, а не о культуре. Надо менять культуру, а не просто внедрять новые инструменты.</li>
<li><strong>Надежность под угрозой</strong>: все хотят быстро, но забывают про надежность. Надо этим управлять с помощью error budgets, SLO и объяснять всем, что надежность – это важно.</li>
</ol>
<h3 id="что-ждет-нас-после-2025">Что ждет нас после 2025</h3>
<ul>
<li><strong>AI будет еще круче</strong>: AI будет лучше предсказывать проблемы, автоматом искать причины и чинить системы. Но важно помнить, что надо измерять, как AI влияет на производительность.</li>
<li><strong>Platform Engineering станет важнее</strong>: платформы будут строить по строгим правилам, чтобы разрабам было удобнее. Internal Developer Platforms (IDP) станут стандартом для больших компаний.</li>
<li><strong>Больше контроля</strong>: будут следить за тем, чтобы все было безопасно и надежно. Regulation as code станет важной темой.</li>
<li><strong>Инструменты станут проще</strong>: все инструменты DevOps/SRE объединят в одну платформу, чтобы было проще управлять.</li>
<li><strong>Экология</strong>: будут думать о том, как технологии влияют на экологию, и стараться делать все более зеленым.</li>
<li><strong>eBPF</strong>: будут больше использовать eBPF, чтобы видеть, что происходит внутри системы, без лишних инструментов.</li>
<li><strong>Kubernetes</strong>: Kubernetes останется главной системой для управления приложениями.</li>
<li><strong>Zero Trust</strong>: безопасность станет еще важнее, и будут использовать принципы Zero Trust.</li>
</ul>
<h3 id="что-посоветовать-компаниям">Что посоветовать компаниям</h3>
<ol>
<li><strong>Считайте Toil</strong>: регулярно проверяйте, сколько времени уходит на рутину, и выделяйте время на автоматизацию.</li>
<li><strong>Observability – это важно</strong>: переходите от <a href="/posts/wat-monitoring/">простого мониторинга</a> к полноценной observability с OpenTelemetry, а сверху добавляйте <a href="/posts/business-metrics/">бизнес-метрики</a>.</li>
<li><strong>SLO – это культура</strong>: SLO должны быть не просто цифрами, а <a href="/posts/reliability-is-a-conversation/">частью разговора с бизнесом</a> и процесса принятия решений.</li>
<li><strong>Пробуйте Chaos Engineering</strong>: начните с маленьких экспериментов в staging и постепенно переходите к production — это один из столпов <a href="/posts/digital-immune-system/">Digital Immune System</a>.</li>
<li><strong>Культура и люди</strong>: учите людей, создавайте атмосферу, где не боятся ошибаться, и поддерживайте друг друга.</li>
<li><strong>Не перегибайте с централизацией</strong>: выберите подходящую структуру SRE-команды, чтобы централизация помогала, а не мешала.</li>
<li><strong>Думайте, зачем вам AI</strong>: внедряйте AI, только если понимаете, какие проблемы он решает, и измеряйте результат.</li>
</ol>
<p>Если после этого списка хочется не «что почитать», а «что уметь и в каком порядке», — я как раз собираю такую карту: <a href="https://jtprogru.github.io/The-Way-of-SRE/priorities/" rel="noopener noreferrer" target="_blank">The Way of SRE</a>, три ветви развития и приоритет по каждой компетенции.</p>
<p>SRE продолжает развиваться и становится все важнее для компаний. Чтобы добиться успеха в SRE, нужно уметь соблюдать баланс – между технологиями и культурой, между новым и стабильным, между скоростью и надежностью. Компании, которые это понимают, будут готовы к будущему.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Попытался разобраться, куда катится Site Reliability Engineering (SRE) за последние лет пять. Ниже накидал мысли по этому поводу. Ссылки на все отчеты вроде не забыл, так что зацени, а потом го в комменты – обсудим вместе.</p>
<h2 id="что-там-в-отчетах">Что там в отчетах?</h2>
<p>Короче, за последние лет пять SRE стала прямо хайповой темой для исследований. Вот самые толковые источники, чтобы быть в теме, что в SRE происходит:</p>
<ul>
<li><strong>Catchpoint — The SRE Report</strong> (<a href="https://www.catchpoint.com/asset/2020-sre-report" rel="noopener noreferrer" target="_blank">2020</a>, <a href="https://www.catchpoint.com/asset/2021-sre-report" rel="noopener noreferrer" target="_blank">2021</a>, <a href="https://www.catchpoint.com/asset/2023-sre-report" rel="noopener noreferrer" target="_blank">2023</a>, <a href="https://www.catchpoint.com/asset/2024-sre-report" rel="noopener noreferrer" target="_blank">2024</a>, <a href="https://www.catchpoint.com/asset/2025-sre-report" rel="noopener noreferrer" target="_blank">2025</a>): Это, наверное, самый жирный отчет прям по SRE. В 2025 опросили больше 300 спецов из разных областей. Говорят, это типа реальное мнение людей из SRE команд, без воды и прикрас.</li>
<li><strong>DORA (DevOps Research and Assessment)</strong>: Каждый год (<a href="https://dora.dev/research/2020/" rel="noopener noreferrer" target="_blank">2020</a>, <a href="https://dora.dev/research/2021/" rel="noopener noreferrer" target="_blank">2021</a>, <a href="https://dora.dev/research/2022/" rel="noopener noreferrer" target="_blank">2022</a>, <a href="https://dora.dev/research/2023/" rel="noopener noreferrer" target="_blank">2023</a>, <a href="https://dora.dev/research/2024/" rel="noopener noreferrer" target="_blank">2024</a>, <a href="https://dora.dev/research/2022/" rel="noopener noreferrer" target="_blank">2025</a>) катают отчеты, где уже лет десять меряют, насколько круто DevOps и SRE работают. Смотрят на четыре главные метрики. В 2025 даже целый отчет выпустили про то, как AI влияет на разработку.</li>
<li><strong><a href="https://www.devopsinstitute.com/global-sre-pulse/" rel="noopener noreferrer" target="_blank">DevOps Institute — Global SRE Pulse 2022</a></strong>: Это было первое большое исследование, где опросили больше 460 SRE спецов и манагеров из средних и больших компаний. В отчете можно глянуть, как сейчас вообще живёт SRE, какие штуки юзают, как команды себя чувствуют и насколько все заавтоматизировано.</li>
<li><strong><a href="https://www.puppet.com/resources/history-of-devops-reports" rel="noopener noreferrer" target="_blank">Puppet — State of DevOps</a></strong> (2020-2021, 2023): В 2021 отметили десятилетие этих отчетов и выяснили, что у 78% компаний с DevOps дела так себе, где-то на середине. В 2023 больше смотрели на платформенную инженерию.</li>
<li><strong>Forrester Research</strong> (<a href="https://www.forrester.com/report/five-key-trends-to-benchmark-devops-progress/RES120481" rel="noopener noreferrer" target="_blank">2020</a>): Выкатили отчет про десять главных трендов в инфраструктуре и сказали, что SRE – это база для стабильной работы всего.</li>
<li><strong>Gartner Hype Cycle</strong> (<a href="https://www.gartner.com/en/articles/hype-cycle-for-emerging-technologies" rel="noopener noreferrer" target="_blank">2025</a>): Закинули SRE и Platform Engineering на свой график взросления технологий. Ещё упомянули всякие приколы для управления окружениями, чтоб каждый мог сам себе все сделать.</li>
<li><strong>ThoughtWorks Technology Radar</strong> (постоянно, <a href="https://www.thoughtworks.com/radar/archive" rel="noopener noreferrer" target="_blank">2020-2025</a>): Эти ребята вообще всегда следят, как меняются инструменты и практики в SRE, наблюдаемости и чтоб системы были надежными.</li>
</ul>
<h2 id="главные-изменения-в-sre">Главные изменения в SRE</h2>
<h3 id="1-рост-toil-неожиданный-поворот-в-2025">1. Рост Toil: неожиданный поворот в 2025</h3>
<p><em>Что-то пошло не так</em>: Впервые за пять лет нудной работы (Toil) стало больше. Раньше было 14% времени, а теперь все 20%. Задачи по операционке тоже времени жрут больше – теперь 30% вместо 20%.</p>
<p>Все думали, с AI станет легче. Эксперты из Catchpoint удивились: ждали, что AI уберет рутину, а он наоборот, ее добавил. Может, все дело в том, что AI помогает быстрее делать полезные вещи, а свободное время забивается всякой фигней.</p>
<p><em>Как было раньше</em>: С 2020 по 2024 нудной работы становилось все меньше. Спасибо автоматизации, IaC, CI/CD и системам, которые сами чинятся. Google советует, чтоб такой работы было не больше половины времени инженера, а у SRE – не больше половины на операционку.</p>
<p><strong>Как бороться с Toil в 2025</strong>:</p>
<ul>
<li>Автоматизация через Terraform, Ansible, CI/CD пайплайны.</li>
<li>Внедряйте self-healing системы, чтоб все чинилось само.</li>
<li>Стандартизируйте все через плейбуки и runbooks.</li>
<li>AIOps инструменты – чтобы алерты сами между собой разбирались и мониторили все на будущее.</li>
<li>Переходите на self-service порталы вместо тикетов.</li>
</ul>
<p>Считать toil при этом надо раньше, чем автоматизировать: без классификации и замера автоматизируется то, что громче болит, а не то, что дороже обходится. <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/toil-tracking/" rel="noopener noreferrer" target="_blank">Про учёт toil</a> — отдельный лист в карте компетенций.</p>
<h3 id="2-ai-и-machine-learning-уже-не-просто-побаловаться-а-без-них-никуда">2. AI и Machine Learning: уже не просто побаловаться, а без них никуда</h3>
<p><strong>Как развивалось</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали пробовать AI в мониторинге и поиске аномалий.</li>
<li><strong>2021-2022</strong>: Все больше интересуются, как AI может предсказывать проблемы и автоматом чинить инциденты.</li>
<li><strong>2023-2024</strong>: Вовсю внедряют AIOps платформы (Datadog, Splunk, Dynatrace).</li>
<li><strong>2025</strong>: GenAI – это революция! 39% спецов говорят, что AI круто генерит код.</li>
</ul>
<p><strong>Где AI нужен в SRE 2025</strong>:</p>
<ul>
<li><strong>Предсказывать инциденты</strong>: AI изучает старые данные и говорит, где может рвануть.</li>
<li><strong>Автоматически генерить тесты</strong>: ML создает тесты, когда код меняется.</li>
<li><strong>AI помогает в ChatOps</strong>: встраивают LLM в процессы управления инцидентами.</li>
<li><strong>Системы, которые сами чинятся</strong>: AI находит аномалии и сам все исправляет.</li>
<li><strong>Улучшение производительности</strong>: AI анализирует, что можно улучшить в SLO.</li>
</ul>
<p><strong>Важный момент из DORA 2024</strong>: второй год подряд получается, что AI делает доставку софта хуже. Короче, AI – не волшебная таблетка, надо аккуратно внедрять и смотреть, что получается.</p>
<p><strong>Что думают про AI</strong>: 53% SRE думают, что AI сделает их работу легче, и только 4% боятся, что AI их заменит. При этом 30% решили, что надо учиться AI.</p>
<h3 id="3-platform-engineering-и-sre-теперь-вместе-навсегда">3. Platform Engineering и SRE: теперь вместе навсегда</h3>
<p><strong>Как менялись отношения</strong>:</p>
<ul>
<li><strong>2020</strong>: Platform Engineering как отдельную штуку вообще не выделяли, все было в DevOps.</li>
<li><strong>2021-2022</strong>: Начали говорить про Platform Engineering.</li>
<li><strong>2023</strong>: Puppet выпустили отчет State of Platform Engineering, и 93% сказали, что это круто.</li>
<li><strong>2024-2025</strong>: Platform Engineering и SRE активно работают вместе.</li>
</ul>
<p><strong>В чем разница и как помогают друг другу</strong>:</p>
<ul>
<li><strong>Platform Engineering</strong> создает платформы, чтобы разрабам было проще работать: self-service, не надо думать про инфраструктуру, все стандартизировано.</li>
<li><strong>SRE</strong> следит, чтоб все работало надежно, SLO выполнялись, мониторит продакшн, разруливает инциденты и чтоб всего было в меру – и новых фич, и стабильности.</li>
<li><strong>Вместе им хорошо</strong>: если платформа сделана правильно, то у SRE меньше Toil, потому что все деплоится по стандарту и куча задач автоматизирована. А SRE, в свою очередь, делает платформы надежнее.</li>
</ul>
<p>Что при этом отличает платформу от кучи скриптов с общим README — границы, интерфейсы и измерение спроса — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/platform-engineering/" rel="noopener noreferrer" target="_blank">в листе про platform engineering</a>.</p>
<h3 id="4-finops-sre-теперь-еще-и-за-бабки-отвечает">4. FinOps: SRE теперь еще и за бабки отвечает</h3>
<p><strong>Как росла важность</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали понимать, что надо следить за расходами в облаке.</li>
<li><strong>2021-2022</strong>: FinOps стала отдельной темой.</li>
<li><strong>2023-2024</strong>: FinOps активно встраивается в SRE практики.</li>
<li><strong>2025</strong>: SRE команды вовсю юзают FinOps, чтобы тратить меньше денег в облаке.</li>
</ul>
<p><strong>Что SRE должны знать про FinOps</strong>:</p>
<ul>
<li>Ответственность за деньги – часть DevOps.</li>
<li>Как тратить меньше, но чтоб все работало надежно.</li>
<li>Как правильно выделять деньги на облачные ресурсы.</li>
<li>Оптимизация производительности за свои деньги.</li>
<li>Управление ресурсами.</li>
</ul>
<p>Catchpoint 2025 говорит, что Capacity Management теперь в приоритете у компаний, наравне с SRE, SLO и автоматизацией починки инцидентов. Как эта ответственность выглядит инженерно, а не как отчётность для финансов, — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/cloud-cost-control/" rel="noopener noreferrer" target="_blank">в листе про управление стоимостью облака</a>.</p>
<h3 id="5-opentelemetry-все-стандартизировали-наблюдаемость">5. OpenTelemetry: все стандартизировали наблюдаемость</h3>
<p><strong>Как быстро все начали юзать</strong>:</p>
<ul>
<li><strong>2020</strong>: Никто толком не знал (~5%).</li>
<li><strong>2021-2022</strong>: Уже многие в теме (~20-25% планируют или юзают).</li>
<li><strong>2023-2024</strong>: Уже хорошо (~48% юзают).</li>
<li><strong>2025</strong>: Практически все (~73% юзают или собираются).</li>
</ul>
<p><strong>Почему это круто</strong>:</p>
<ul>
<li><strong>Стандартизация</strong>: один стандарт для всего – как собирать данные, как их отправлять.</li>
<li><strong>Не надо зависеть от поставщиков</strong>: можно не привязываться к конкретному вендору.</li>
<li><strong>ROI</strong>: 46% компаний говорят, что ROI больше 20%, а еще 40% – 10-20%.</li>
<li><strong>Экономия</strong>: 84% компаний, которые стали меньше тратить на observability, говорят, что сэкономили минимум 10%.</li>
<li><strong>Зрелость</strong>: 81% считают, что OpenTelemetry уже достаточно взрослая штука.</li>
</ul>
<p><strong>Почему это важно для AI-SRE</strong>: OpenTelemetry – это база для AI, потому что он дает одинаковые, понятные данные для анализа.</p>
<p><strong>Но есть и проблемы</strong>: сложно внедрить, данных становится слишком много (в 4-5 раз больше), надо интегрировать со старыми системами и следить за расходами.</p>
<h3 id="6-chaos-engineering-теперь-это-не-просто-прикол-а-необходимость">6. Chaos Engineering: теперь это не просто прикол, а необходимость</h3>
<p><strong>Как это стало популярно</strong>:</p>
<ul>
<li><strong>2020</strong>: Какая-то странная штука, которую Netflix придумал.</li>
<li><strong>2021</strong>: 60% спецов хоть раз пробовали chaos-эксперименты.</li>
<li><strong>2022-2023</strong>: Все больше компаний юзают это, чтобы проверять, как системы работают при сбоях.</li>
<li><strong>2024-2025</strong>: Chaos Engineering стал важной частью SRE.</li>
</ul>
<p><strong>Как это работает с SRE</strong>:</p>
<ul>
<li><strong>Проверка на отказоустойчивость</strong>: специально ломают систему, чтоб найти слабые места.</li>
<li><strong>Валидация SLO/SLI</strong>: убеждаются, что все работает, даже когда что-то идет не так.</li>
<li><strong>Подготовка к инцидентам</strong>: тренируются реагировать, чтоб пользователи ничего не заметили.</li>
<li><strong>Уменьшение Toil</strong>: автоматом проверяют отказоустойчивость через CI/CD.</li>
<li><strong>Быстрее чинят</strong>: если заранее знают, что может сломаться, то быстрее восстановят.</li>
</ul>
<p><strong>Популярные инструменты 2025</strong>: Qinfinite by Quinnox, Gremlin, Chaos Mesh, LitmusChaos, AWS Fault Injection Simulator.</p>
<p><strong>Важный момент</strong>: Chaos Engineering помогает создать атмосферу, где не ищут виноватых, а стараются улучшить систему.</p>
<h3 id="7-slo-и-error-budgets-все-становится-серьезно">7. SLO и Error Budgets: все становится серьезно</h3>
<p><strong>Как взрослели</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали внедрять SLO, но часто не понимали, зачем это.</li>
<li><strong>2021-2022</strong>: Стало больше понимания, но 99% SRE не знают, как правильно определить SLO.</li>
<li><strong>2023-2024</strong>: SLO стали важны при принятии решений.</li>
<li><strong>2025</strong>: SLO помогают решать, что важнее – надежность или новые фичи, появились composite SLO и reliability scoring.</li>
</ul>
<p><strong>Что нового в SLO</strong>:</p>
<ul>
<li><strong>Composite SLOs</strong>: объединяют несколько SLO в одну метрику, чтобы видеть картину целиком.</li>
<li><strong>Reliability scoring</strong>: оценивают надежность системы в цифрах.</li>
<li><strong>Мониторинг в реальном времени и алерты</strong>: помогают управлять SLO.</li>
<li><strong>SLO Development Life Cycle (SLODLC)</strong>: как правильно внедрять SLO.</li>
</ul>
<p><strong>Error Budget Management – это целый рынок</strong>: он вырос с $1.2 млрд в 2024 году и, по прогнозам, будет $5.8 млрд к 2033 году. Это значит, что DevOps, SRE и cloud-native технологии становятся все популярнее.</p>
<p><strong>Проблемы с SLO</strong>:</p>
<ul>
<li>51% считают, что observability не очень, особенно там, где мало инструментов.</li>
<li>SLO бесполезны, если их не использовать постоянно, потому что системы меняются.</li>
<li>Надо думать о пользователях: SLO должны показывать, что пользователи чувствуют, а не только технические метрики. Как выводить SLI из критичного пользовательского пути, я разобрал в <a href="/posts/slo-as-architecture-blueprint/">«SLO как чертёж архитектуры»</a>, а про <a href="/posts/burn-rate-is-not-speed/">сгорание бюджета ошибок</a> — отдельно, потому что burn rate почти все понимают неверно.</li>
</ul>
<h3 id="8-автоматизация-управления-инцидентами-и-aiops">8. Автоматизация управления инцидентами и AIOps</h3>
<p><strong>Как развивалось</strong>:</p>
<ul>
<li><strong>2020</strong>: Только начали автоматом делать алерты и тикеты.</li>
<li><strong>2021-2022</strong>: Активно развивали runbook automation и self-healing.</li>
<li><strong>2023-2024</strong>: Внедрили AIOps, чтобы связывать события и предсказывать проблемы.</li>
<li><strong>2025</strong>: AI помогает управлять инцидентами, общаться на обычном языке и автоматом искать причины проблем.</li>
</ul>
<p><strong>Как выглядит incident management в 2025</strong>:</p>
<ul>
<li><strong>Видят все в реальном времени</strong>: AI анализирует логи и события и сразу находит закономерности.</li>
<li><strong>Алерты на будущее</strong>: ML предсказывает, где может случиться инцидент.</li>
<li><strong>Автоматически чинят</strong>: системы сами исправляют проблемы, без людей.</li>
<li><strong>Все общаются в одном месте</strong>: все стейкхолдеры сидят в одной системе.</li>
<li><strong>Удобные workflows</strong>: инструменты для управления инцидентами работают прямо в Slack.</li>
</ul>
<p><strong>Важный момент</strong>: 40% инженеров участвовали в 1-5 инцидентах за последние 30 дней. При этом руководители тоже в деле, не меньше, чем обычные инженеры. 14% сильно стрессуют после инцидентов, потому что им не хватает поддержки.</p>
<p><strong>Как дела с incident management (Global SRE Pulse 2022)</strong>: больше 70% говорят, что у них хорошо спроектирован процесс управления инцидентами и алерты обрабатываются четко.</p>
<p><strong>Как AI все изменил в 2025</strong>:</p>
<ul>
<li>На 56% точнее находят угрозы, чем старые системы.</li>
<li>Анализ инцидентов занимает 2.8 минуты вместо 18.5.</li>
<li>Автоматизируют 78% действий при обычных инцидентах, раньше было 35%.</li>
<li>Ложных срабатываний стало меньше – с 25% до 8%.</li>
</ul>
<h3 id="9-надежность-vs-скорость-кто-победит">9. Надежность vs. скорость: кто победит?</h3>
<p><strong>Плохая новость</strong>: большинство спецов (больше двух третей) чувствуют, что их заставляют выпускать релизы быстрее, чем это безопасно для надежности.</p>
<p><strong>Почему так происходит</strong>:</p>
<ul>
<li><strong>Уходят те, кто создавал SRE</strong>: эксперты уходят из компаний, и приоритеты меняются.</li>
<li><strong>Бизнес давит</strong>: хотят новые фичи и больше денег.</li>
<li><strong>Не все понимают одинаково</strong>: инженеры и руководители по-разному оценивают надежность – чем выше должность, тем больше уверенности, что все хорошо.</li>
</ul>
<p><strong>Как было раньше</strong>: это старая проблема – что важнее, быстро или безопасно. SRE пытаются это решить с помощью error budgets и SLO. Но в 2025 давление стало слишком сильным.</p>
<p><strong>Что делать</strong>:</p>
<ul>
<li>Регулярно проверять и обновлять оценки процессов.</li>
<li>Звать внешних экспертов.</li>
<li>Улучшать культуру SRE и учить всех в компании, что надежность – это важно.</li>
<li>Использовать error budgets, чтобы принимать решения.</li>
</ul>
<h3 id="10-структуры-sre-команд-и-как-их-организовать">10. Структуры SRE-команд и как их организовать</h3>
<p><strong>Основные варианты</strong> (из книги Establishing SRE Foundations):</p>
<ol>
<li><strong>You Build It, You Run It</strong>: разрабы сами отвечают и за разработку, и за эксплуатацию. Быстро деплоят, но много операционных задач и сложно управлять изменениями.</li>
<li><strong>You Build It, SRE Run It</strong>: разрабы пилят код, SRE занимаются операционкой. Может случиться разрыв между командами.</li>
<li><strong>You Build It, You and SRE Run It</strong>: все в ответе вместе – SRE управляют операционными задачами, разрабы пишут код, но все вместе смотрят на метрики.</li>
</ol>
<p><strong>Где команда SRE находится в компании</strong> (Global SRE Pulse 2022):</p>
<ul>
<li>30% — IT operations</li>
<li>22% — отдельная команда</li>
<li>18% — application design &amp; development</li>
<li>18% — IT infrastructure</li>
<li>3% — IT security</li>
</ul>
<p><strong>Типы команд</strong>:</p>
<ul>
<li><strong>Centralized</strong>: помогают многим продуктам/сервисам.</li>
<li><strong>Dedicated</strong>: занимаются конкретными продуктами.</li>
<li><strong>Platform-based</strong>: строят облачные платформы.</li>
<li><strong>Stack-based</strong>: отдельные команды для разных стеков приложений/инфраструктуры.</li>
</ul>
<p><strong>Главное правило Google SRE</strong>: максимум 50% времени на операционку, минимум 50% на engineering (улучшения, автоматизация, исправление проблем).</p>
<h3 id="11-observability-от-мониторинга-к-ai">11. Observability: от мониторинга к AI</h3>
<p><strong>Как менялось</strong>:</p>
<ul>
<li><strong>2020</strong>: Просто мониторили (метрики, логи).</li>
<li><strong>2021-2022</strong>: Перешли к observability (метрики + логи + трассировки).</li>
<li><strong>2023-2024</strong>: Observability всего стека, смотрят на user experience.</li>
<li><strong>2025</strong>: AI сам находит полезную информацию в телеметрии.</li>
</ul>
<p><strong>Что важно в 2025</strong>:</p>
<ul>
<li><strong>Slow is the new down</strong>: 53% компаний согласны, что плохая производительность – это так же плохо, как если бы все упало.</li>
<li><strong>Много инструментов – это нормально</strong>: большинство компаний используют 2-10 инструментов для мониторинга/observability, и это не проблема, если они приносят пользу.</li>
<li><strong>Разные данные</strong>: 81% компаний используют 2+ типа телеметрии, 43% – 4+ типа.</li>
<li><strong>Что в приоритете</strong>: логи (65%), метрики (56%), события и трассировки.</li>
</ul>
<p><strong>Как все поменялось</strong>: раньше observability была нужна, чтобы люди могли понять терабайты данных. Сейчас, с AI, главное, чтобы машина могла быстро найти нужную информацию.</p>
<h3 id="12-обучение-и-развитие">12. Обучение и развитие</h3>
<p><strong>Учиться надо</strong>: технические тренинги очень важны, чтобы освоить новые технологии, включая AI. Но каждая пятая компания на это забивает.</p>
<p><strong>Как учатся</strong> (Catchpoint 2025):</p>
<ul>
<li>Онлайн-платформы (55%).</li>
<li>Очное обучение (45%).</li>
<li>Конференции (41%).</li>
</ul>
<p><strong>Странно</strong>: все говорят, что учиться важно, но времени нет. Руководители учатся меньше других и, наверное, думают, что им это не нужно.</p>
<p><strong>Важно для карьеры</strong>: DevOps Institute говорит, что SRE – это хороший способ продвинуться в IT, если прокачивать нужные навыки.</p>
<h2 id="выводы-и-что-будет-дальше">Выводы и что будет дальше</h2>
<h3 id="главное-из-анализа">Главное из анализа</h3>
<ol>
<li><strong>Странности 2025</strong>: несмотря на AI и автоматизацию, Toil впервые вырос за пять лет, и AI пока не оправдывает надежд. Значит, дело не только в технологиях, но и в культуре и правильном подходе.</li>
<li><strong>SRE взрослеет</strong>: SRE стала серьезной штукой – 62% компаний в той или иной степени используют SRE (19% во всей компании, 55% в отдельных командах, 23% пробуют). SLO, error budgets и chaos engineering – это уже не ново, а стандарт.</li>
<li><strong>Все работают вместе</strong>: SRE, Platform Engineering, DevOps, DevSecOps и FinOps – это не конкуренты, а друзья, которые помогают друг другу. Компании, которые это понимают, получают больше выгоды.</li>
<li><strong>AI – это помощник, а не замена</strong>: AI помогает инженерам, а не заменяет их. AI должен быть частью культуры и процессов.</li>
<li><strong>Культура важнее</strong>: Puppet 2021 показал, что 78% компаний застряли на середине пути в DevOps/SRE, потому что думают только о технологиях, а не о культуре. Надо менять культуру, а не просто внедрять новые инструменты.</li>
<li><strong>Надежность под угрозой</strong>: все хотят быстро, но забывают про надежность. Надо этим управлять с помощью error budgets, SLO и объяснять всем, что надежность – это важно.</li>
</ol>
<h3 id="что-ждет-нас-после-2025">Что ждет нас после 2025</h3>
<ul>
<li><strong>AI будет еще круче</strong>: AI будет лучше предсказывать проблемы, автоматом искать причины и чинить системы. Но важно помнить, что надо измерять, как AI влияет на производительность.</li>
<li><strong>Platform Engineering станет важнее</strong>: платформы будут строить по строгим правилам, чтобы разрабам было удобнее. Internal Developer Platforms (IDP) станут стандартом для больших компаний.</li>
<li><strong>Больше контроля</strong>: будут следить за тем, чтобы все было безопасно и надежно. Regulation as code станет важной темой.</li>
<li><strong>Инструменты станут проще</strong>: все инструменты DevOps/SRE объединят в одну платформу, чтобы было проще управлять.</li>
<li><strong>Экология</strong>: будут думать о том, как технологии влияют на экологию, и стараться делать все более зеленым.</li>
<li><strong>eBPF</strong>: будут больше использовать eBPF, чтобы видеть, что происходит внутри системы, без лишних инструментов.</li>
<li><strong>Kubernetes</strong>: Kubernetes останется главной системой для управления приложениями.</li>
<li><strong>Zero Trust</strong>: безопасность станет еще важнее, и будут использовать принципы Zero Trust.</li>
</ul>
<h3 id="что-посоветовать-компаниям">Что посоветовать компаниям</h3>
<ol>
<li><strong>Считайте Toil</strong>: регулярно проверяйте, сколько времени уходит на рутину, и выделяйте время на автоматизацию.</li>
<li><strong>Observability – это важно</strong>: переходите от <a href="/posts/wat-monitoring/">простого мониторинга</a> к полноценной observability с OpenTelemetry, а сверху добавляйте <a href="/posts/business-metrics/">бизнес-метрики</a>.</li>
<li><strong>SLO – это культура</strong>: SLO должны быть не просто цифрами, а <a href="/posts/reliability-is-a-conversation/">частью разговора с бизнесом</a> и процесса принятия решений.</li>
<li><strong>Пробуйте Chaos Engineering</strong>: начните с маленьких экспериментов в staging и постепенно переходите к production — это один из столпов <a href="/posts/digital-immune-system/">Digital Immune System</a>.</li>
<li><strong>Культура и люди</strong>: учите людей, создавайте атмосферу, где не боятся ошибаться, и поддерживайте друг друга.</li>
<li><strong>Не перегибайте с централизацией</strong>: выберите подходящую структуру SRE-команды, чтобы централизация помогала, а не мешала.</li>
<li><strong>Думайте, зачем вам AI</strong>: внедряйте AI, только если понимаете, какие проблемы он решает, и измеряйте результат.</li>
</ol>
<p>Если после этого списка хочется не «что почитать», а «что уметь и в каком порядке», — я как раз собираю такую карту: <a href="https://jtprogru.github.io/The-Way-of-SRE/priorities/" rel="noopener noreferrer" target="_blank">The Way of SRE</a>, три ветви развития и приоритет по каждой компетенции.</p>
<p>SRE продолжает развиваться и становится все важнее для компаний. Чтобы добиться успеха в SRE, нужно уметь соблюдать баланс – между технологиями и культурой, между новым и стабильным, между скоростью и надежностью. Компании, которые это понимают, будут готовы к будущему.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Почему тебе НЕ стоит идти работать в АйТи</title>
      <link>https://jtprog.ru/posts/pochemu-ne-idti-v-it/</link>
      <pubDate>Tue, 23 Dec 2025 20:02:29 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/pochemu-ne-idti-v-it/</guid>
      <category>Career</category>
      <category>Work</category>
      <category>Мысли</category>
      <category domain="category">Work</category>
      <enclosure url="https://jtprog.ru/work.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/work.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Слушай, прежде чем ты понесёшь свои кровные на очередные курсы «стань программистом за три месяца», давай честно поговорим о том, о чём тебе не расскажут продавцы этих самых курсов. Все эти истории про гибкий график, работу с ноутбуком на Бали и зарплаты в долларах — это как картинки в меню в забегаловке: выглядят аппетитно, а на деле ты получаешь совсем не то, что ожидал.</p>
<p>Я собрал для тебя реальные истории о рутинной, скучной, монотонной работе в АйТи — для каждой профессии отдельно. Потому что рутина везде своя, но одинаково тоскливая. Погнали разбираться.</p>
<h2 id="devopssre-инженер-добро-пожаловать-в-кошмар-247">DevOps/SRE инженер: добро пожаловать в кошмар 24/7</h2>
<p>Знаешь, что самое «клёвое» в работе DevOps-инженера? Возможность проснуться в 6 утра от звонка, когда у клиента упала нода в кластере БД. И это не какой-то редкий случай — это твоя новая реальность, которая будет случаться 1–2 раза в месяц. Только представь: ты ещё толком не проснулся, одним глазом смотришь в телефон, копируешь логи и машинально запускаешь стандартный набор инструментов восстановления.</p>
<h3 id="бесконечный-мониторинг">Бесконечный мониторинг</h3>
<p>Твой рабочий день начинается с проверки дашбордов. Grafana, Prometheus, Datadog — твои новые лучшие друзья. Ты часами смотришь на графики, анализируешь метрики: время отклика, загрузку CPU, потребление памяти. Звучит захватывающе? Попробуй делать это каждый день в течение года.</p>
<p>А ещё есть логи. Море логов. Океан логов. ELK-стек становится твоим вторым домом, где ты проводишь больше времени, чем в настоящем. Тебе нужно найти иголку в стоге сена — вот только стог размером с небоскрёб, а игла может быть где угодно.</p>
<h3 id="дежурства-и-инциденты">Дежурства и инциденты</h3>
<p>Помнишь, как тебе обещали гибкий график? Ну так вот, он действительно гибкий — гнётся во все стороны, только не в твою пользу. Ты дежуришь по ночам и выходным. Алерт может прилететь в любой момент. Семейный ужин? Прогулка с собакой? Извини, приятель, но продакшен горит, и тушить его будешь ты.</p>
<p>И после того как ты всё починил, наступает самое «весёлое» — написание отчёта клиенту. Нужно расписать, что произошло, почему произошло, как ты это починил и что сделал, чтобы это больше не повторилось. На 200 страниц. Потому что некоторые заказчики испытывают благоговейный ужас перед кипой бумаг.</p>
<h3 id="рутинная-автоматизация">Рутинная автоматизация</h3>
<p>Ирония в том, что половину своего времени ты тратишь на автоматизацию… чтобы автоматизировать рутину. Пишешь CI/CD-пайплайны, настраиваешь мониторинг, конфигурируешь инфраструктуру через Ansible или Terraform. Но это тоже рутина! Просто рутина для автоматизации рутины. Рутинный Inception, понимаешь?</p>
<p>По концепции SRE ты не должен тратить больше 50% времени на операционные задачи (toil). Звучит неплохо, правда? Только вот в реальности эти 50% превращаются в 80%, а оставшиеся 20% ты тратишь на то, чтобы объяснять менеджерам, почему у тебя нет времени на разработку.</p>
<h2 id="системный-администратор-а-можешь-включить-принтер">Системный администратор: &ldquo;а можешь включить принтер?&rdquo;</h2>
<p>Если ты думаешь, что системный администратор — это крутой хакер из фильмов, который взламывает Пентагон за пять минут, у меня для тебя плохие новости.</p>
<h3 id="техподдержка-first-line">Техподдержка first-line</h3>
<p>Реальная жизнь сисадмина выглядит так: ты бежишь в бухгалтерию, чтобы «включить процессор». Да-да, именно так пользователи называют системный блок. Или меняешь картридж в принтере. Снова. В третий раз за день. В том же самом принтере.</p>
<p>Тебе прилетает 5–10 заявок в день через ServiceDesk. «Не работает интернет» (кабель не воткнут), «Компьютер тормозит» (забита оперативка хромом с 50 вкладками), «Пропал Word» (иконку случайно удалили с рабочего стола). И так каждый день. Месяц за месяцем. Год за годом.</p>
<h3 id="обслуживание-инфраструктуры">Обслуживание инфраструктуры</h3>
<p>А ещё есть «настоящая» работа. Резервное копирование данных — каждый день, каждую ночь. Обновление систем — вручную, на каждом сервере. Проверка работоспособности — по списку, в Excel-табличке. Настройка учётных записей — одна за другой.</p>
<p>Знаешь, что самое страшное? «Панический ступор от осознания, что бэкапа нет, а ты только что отформатировал диск с документами целого отдела». Это не шутка. Это реальная история.</p>
<h3 id="работа-247">Работа 24/7</h3>
<p>Ты всегда на связи. Всегда. В отпуске, в выходные, на пляже с коктейлем. Бизнес не может обходиться без IT. Даже пробить товар невозможно без софта. Поэтому, когда что-то падает, — звонят тебе. В любое время суток.</p>
<p>Монотонность, усидчивость, умение выполнять однообразную работу — вот твои главные навыки. А ещё стрессоустойчивость, потому что работать приходится в ненормированном графике при постоянных форс-мажорах.</p>
<h2 id="программист-код-баги-рефакторинг-повтори">Программист: код, баги, рефакторинг, повтори</h2>
<p>Теперь — о самой, казалось бы, творческой профессии в АйТи. Спойлер: творчества там не больше, чем в работе на конвейере.</p>
<h3 id="backend-разработчик-crud-до-потери-пульса">Backend-разработчик: CRUD до потери пульса</h3>
<p>CRUD: Create, Read, Update, Delete. Четыре операции, которые ты будешь писать до конца своих дней. Создать запись в базе, прочитать, обновить, удалить. Снова и снова. С разными таблицами, разными сущностями, но суть одна и та же.</p>
<p>API-endpoints. Сотни, тысячи API-endpoints. Принять запрос, валидировать данные, сделать запрос в базу, отдать ответ. Меняются только названия. Для разнообразия можешь добавить аутентификацию и авторизацию — но это тоже шаблонный код.</p>
<p>А ещё есть legacy. Старый код, написанный предыдущим разработчиком, который давно уволился и теперь занимается разведением кроликов на даче. Тебе нужно разобраться в этом коде, понять логику (если она там есть) и что-то исправить. «Тебе приходится исправлять написанный не очень хорошо проект за кем-то. Это в итоге выходит дороже, чем если бы его изначально дали мне».</p>
<h3 id="frontend-разработчик-подвигай-кнопку">Frontend-разработчик: подвигай кнопку</h3>
<p>«Одна кнопка — одна задача на день». Это не преувеличение, это реальность многих фронтендеров. Вся логика на бэке, а ты просто двигаешь элементы туда-сюда. «Подвигать кнопку или перекрасить её».</p>
<p>Адаптация под разные браузеры и устройства. Chrome, Firefox, Safari, Edge. Desktop, tablet, mobile. Android, iOS. И у каждого свои приколы. То, что работает в Chrome, падает в Safari. То, что красиво на desktop, разваливается на mobile.</p>
<p>Работа с отступами. «14px вместо 16px, нарушая дефолтные шаги в 8px». Дизайнер внезапно решает, что нужен отступ не 16, а 14 пикселей. Зачем? А фиг его знает. Просто так захотелось. И ты меняешь эти 2 пикселя во всём проекте.</p>
<h3 id="общая-рутина-для-всех">Общая рутина для всех</h3>
<p>Код-ревью. Часами. Днями. Проверять чужой код, оставлять комментарии, спорить о том, нужна ли тут переменная или можно инлайнить. «Раздражает проверка pull request. Это может продолжаться часами (и днями!)».</p>
<p>Написание документации. Это не просто скучно — это смертельно скучно. «Раздражает создавать и упорядочивать документацию. В процессе бывает сложно перевести знания в голове на человеческий язык».</p>
<p>Тестирование. Написание тестов. Перезапуск тестов. Запустил тест — упал. Починил — запустил снова. Упал в другом месте. И так по кругу.</p>
<h2 id="qa-тестировщик-регресс-регресс-регресс">QA-тестировщик: регресс, регресс, регресс</h2>
<p>Думаешь, тестировщики просто играют в игры или тыкают в приложения? Ха. Если бы.</p>
<h3 id="регрессионное-тестирование">Регрессионное тестирование</h3>
<p>«Регрессионное тестирование — сотый раз прогонять». Представь, что тебя заставляют играть в одну и ту же игру, проходить один и тот же уровень, ровно по тому же маршруту, делать те же действия. Сто раз подряд. А потом ещё сто.</p>
<p>Smoke-тесты после каждого деплоя. Один и тот же набор проверок: открыть приложение, залогиниться, открыть главную страницу, проверить основные функции. И так после каждого обновления.</p>
<p>«Играть в одно и то же раз за разом». Это не просто скучно — это убивает мотивацию. «Чаще всего работа трудная, но интересная. Иногда — безумно скучная».</p>
<h3 id="документация-test-кейсов">Документация test-кейсов</h3>
<p>Составление подробных тест-кейсов. Шаг 1: открыть приложение. Шаг 2: нажать на кнопку входа. Шаг 3: ввести логин. Шаг 4: ввести пароль. И так на 100 шагов для одного простого сценария. А сценариев — сотни.</p>
<p>Баг-репорты. Каждый баг нужно задокументировать: что делал, что ожидал, что получил, окружение, версия билда, шаги для воспроизведения. И таких багов в день может быть десятки.</p>
<p>Поддержание документации в актуальном состоянии. Изменилась кнопка — нужно обновить все тест-кейсы, где она упоминается. Добавился новый экран — нужно написать новые тест-кейсы. Бесконечная рутина.</p>
<h2 id="team-lead--tech-lead-совещания-вместо-кода">Team Lead / Tech Lead: совещания вместо кода</h2>
<p>Ты думал стать лидером, чтобы больше влиять на проект? Приятель, у меня для тебя новости.</p>
<h3 id="встречи-встречи-встречи">Встречи, встречи, встречи</h3>
<p>«Марафон встреч на несколько часов». Ежедневные стендапы, груминги, планирования, ретроспективы, one-to-one-встречи. Твой календарь похож на тетрис, где каждый блок — это совещание. И между этими блоками нет места для того, чтобы что-то сделать.</p>
<p>Утро начинается с дейли. Потом — планирование спринта. После обеда — груминг бэклога. Вечером — one-to-one с разработчиками. А между всем этим нужно ещё и работу делать. Когда? После работы. Ночью. На выходных.</p>
<h3 id="код-ревью-как-основная-работа">Код-ревью как основная работа</h3>
<p>Проверка pull request&rsquo;ов. Это теперь не просто часть твоей работы — это основная часть. Ты проверяешь код, оставляешь комментарии, споришь с разработчиками о том, почему этот подход лучше или хуже. Часами.</p>
<p>Парное программирование с джунами. Объяснять базовые вещи. Показывать, как правильно. Смотреть, как они делают ошибки, которые ты совершал пять лет назад. Терпеть и снова объяснять.</p>
<h3 id="административная-рутина">Административная рутина</h3>
<p>Распределение задач: кому что дать, кто с чем справится, у кого какая загрузка. Excel-таблички или Jira-борды — твои новые инструменты.</p>
<p>Решение конфликтов. Вася не может работать с Петей, потому что они поругались из-за подхода к написанию тестов. Маша выгорает, потому что все задачи скучные. Коля хочет уйти, потому что нет карьерного роста.</p>
<p>Заполнение отчётов: что сделали, сколько времени потратили, какие проблемы возникли, какие планы на следующую неделю. Каждую неделю. Каждый спринт. Каждый месяц.</p>
<h2 id="product-manager-созвоны-метрики-согласования">Product Manager: созвоны, метрики, согласования</h2>
<p>PM — это человек, который общается. Много общается. Очень много общается. И потом ещё немного общается.</p>
<h3 id="марафон-встреч">Марафон встреч</h3>
<p>Типичный день продакта: «утренний скрам, архитектурный созвон, созвон с клиентом, синк с инженерами, груминг бэклога». Твой день — это бесконечная череда встреч. Созвон за созвоном, без перерывов.</p>
<p>С 11 до 14:00, иногда до 16:00 — ты в постоянной коммуникации. Команда, клиенты, стейкхолдеры, партнёры. Все хотят с тобой поговорить. Все хотят что-то обсудить. Все хотят твоего внимания.</p>
<p>Focus time? Первые пару часов утром, до того как начнутся встречи. Вот это твоё время, чтобы подумать, написать документацию, сформулировать гипотезы. А потом — добро пожаловать в марафон.</p>
<h3 id="проверка-метрик-каждое-утро">Проверка метрик каждое утро</h3>
<p>«Первым делом всегда смотрю все метрики по продукту на дашборде». Каждый день. Без исключений. Retention, NPS, конверсия, активность пользователей, revenue. Цифры, цифры, цифры.</p>
<p>Анализ результатов A/B-тестов. Запустили тест — нужно посмотреть результаты. Победила версия B — нужно раскатать на всех. Не сработало — нужно понять, почему, и придумать новую гипотезу.</p>
<h3 id="документация-документация-документация">Документация, документация, документация</h3>
<p>Написание ТЗ и требований. Для каждой фичи нужно написать, что хотим сделать, зачем, для кого, как это будет выглядеть, как это должно работать.</p>
<p>Подготовка бизнес-планов. Что даст эта фича, как будем измерять успех, какие метрики должны вырасти, на сколько, почему именно на столько.</p>
<p>Формирование бэклога. Приоритизация задач: что делаем сейчас, что потом, что не делаем вообще. И объяснение команде, почему именно в таком порядке.</p>
<h2 id="data-scientist-80-времени-на-подготовку-данных">Data Scientist: 80% времени на подготовку данных</h2>
<p>Забудь про крутые нейросети и машинное обучение. Реальность DS — это работа с данными. Очень. Долгая. Работа. С. Данными.</p>
<h3 id="разметка-очистка-препроцессинг">Разметка, очистка, препроцессинг</h3>
<p>«Data scientist тратит большую часть своего времени на сбор, разметку, классификацию данных». Не на обучение моделей. Не на анализ. На работу с сырыми данными.</p>
<p>Разметка данных. Вручную. Тысячи, десятки тысяч образцов. «Это кошка. Это собака. Это тоже кошка. А это размытая фигня, которую хрен пойми что».</p>
<p>Очистка данных. Удаление дубликатов, обработка пропусков, исправление ошибок, нормализация форматов. Excel, Python, pandas — твои инструменты. Рутина — твоя реальность.</p>
<h3 id="документация-на-200-страниц">Документация на 200 страниц</h3>
<p>«Написание 200 страниц текста для заказчика». Техническая документация, сопроводительная документация. Описание модели, описание данных, описание метрик, описание результатов.</p>
<p>«Что, как, почему именно так». Каждое решение нужно обосновать: почему выбрали эту модель, а не ту; почему эти гиперпараметры, а не другие; почему эта метрика, а не другая.</p>
<h3 id="работа-в-изоляции">Работа в изоляции</h3>
<p>«Data scientists в итоге работают одни, даже если сидят за тем же столом, что и разработчики». Один DS на всю команду. Не с кем посоветоваться, не с кем обсудить подходы.</p>
<p>«Рассчитывают финансы в Excel». Иногда вместо машинного обучения ты просто заполняешь Excel-таблички. Потому что бизнесу нужен быстрый результат, а не красивая модель.</p>
<h2 id="it-архитектор-документация-и-согласования">IT-архитектор: документация и согласования</h2>
<p>Ты думал, будешь проектировать крутые системы? Будешь. Но сначала напишешь 300 страниц документации.</p>
<h3 id="рутина-документации">Рутина документации</h3>
<p>«Подготовка документации по проектам. Рутины более чем достаточно». Техническая документация, архитектурная документация, пользовательская документация.</p>
<p>«Вычертить огромный генплан месяцами». Схемы, диаграммы, чертежи. Одно и то же, но с небольшими вариациями. Для одного проекта. Потом для другого. Потом для третьего.</p>
<p>Проектирование типовых проектов («коробок»). Не каждый раз что-то новое. Часто — одно и то же, со стандартными решениями, стандартными подходами.</p>
<h3 id="согласования">Согласования</h3>
<p>Координация команды. Распределение задач, контроль сроков, проверка качества. People management, как ни крути.</p>
<p>Согласования с бизнесом. Объяснять, почему это решение лучше, почему оно стоит таких денег, почему нужно столько времени. Презентации, совещания, обоснования.</p>
<h2 id="дизайнер-правки-правки-ещё-правки">Дизайнер: правки, правки, ещё правки</h2>
<p>«Бесконечные правки, а потом просят вернуться к первоначальному варианту».</p>
<h3 id="рутинные-задачи">Рутинные задачи</h3>
<p>Разработка технических эскизов. Создание макетов. Адаптация под разные разрешения. Экспорт в разные форматы. Подготовка ассетов для разработчиков.</p>
<p>Многочасовые правки. Клиент посмотрел — что-то не нравится. Изменил — опять не то. Сделал как было в начале — вот теперь хорошо. Три дня работы — в урну.</p>
<p>Монотонность и выгорание. «Дизайнер заставляет себя работать над рутинным процессом, пока все однотипные и скучные задачи не будут завершены».</p>
<h2 id="общие-паттерны-рутины-что-объединяет-всех">Общие паттерны рутины: что объединяет всех</h2>
<p>Изучив десятки источников и истории реальных людей, я вывел паттерны, которые присутствуют в каждой АйТи-профессии:</p>
<h3 id="1-документация--бесит-всех">1. Документация — бесит всех</h3>
<p>От джуна до архитектора, от тестировщика до продакта — все ненавидят писать документацию. Но все обязаны это делать. Снова и снова.</p>
<h3 id="2-встречи-вместо-работы">2. Встречи вместо работы</h3>
<p>Стендапы, планирования, груминги, ретро, синки, one-to-one. Твой день превращается в марафон встреч, между которыми нужно ещё и работу делать.</p>
<h3 id="3-legacy-код-и-технический-долг">3. Legacy-код и технический долг</h3>
<p>«30% всего объёма работ по итогу не используется». Работа в стол. Или работа с кодом, который написали пять лет назад и который теперь никто не понимает.</p>
<h3 id="4-работа-247">4. Работа 24/7</h3>
<p>Особенно для DevOps, SRE, сисадминов. Ты всегда на связи. Отпуск, выходные, праздники — неважно. Если упало — звонят тебе.</p>
<h3 id="5-выгорание-через-рутину">5. Выгорание через рутину</h3>
<p>«78% ИТ-работников страдают от выгорания». «89% специалистов трудятся в режиме 5/2, 69% работают сверхурочно и дома». Монотонные задачи, большая нагрузка, постоянный стресс.</p>
<h3 id="6-ненормированный-рабочий-день">6. Ненормированный рабочий день</h3>
<p>Никаких доплат за переработку. В качестве компенсации — три дополнительных дня отпуска. И это в лучшем случае. А в худшем — просто работай больше за те же деньги.</p>
<h3 id="7-можно-легко-разлюбить-свою-работу">7. &ldquo;Можно легко разлюбить свою работу&rdquo;</h3>
<p>«Проблемы имеют свойство накапливаться как снежный ком. Большой технический долг, давление руководства, административно-бюрократические проблемы — если не уметь с этим справляться, можно легко разлюбить свою работу».</p>
<h2 id="итого-суровая-правда-о-работе-в-айти">Итого: суровая правда о работе в АйТи</h2>
<p>Знаешь, что самое смешное? Я не придумал ничего из этого. Всё, что ты прочитал выше, — это реальные истории реальных людей.</p>
<p>АйТи — это не работа мечты с кофе на Бали. Это рутина, стресс, выгорание и постоянное ощущение, что ты бежишь на месте.</p>
<p>Это ненормированный рабочий день, где «гибкий график» означает «работай, когда хочешь, но 24/7». Это постоянное обучение, потому что технологии устаревают быстрее, чем молоко в холодильнике.</p>
<p>Это работа, где «30% всего объёма работ по итогу не используется». Где проекты закрываются на полпути. Где требования меняются каждую неделю.</p>
<h3 id="но-стоит-ли-оно-того">Но стоит ли оно того?</h3>
<p>Если после всего прочитанного ты всё ещё хочешь в АйТи — значит, это твоё. Потому что, несмотря на всю эту рутину, в АйТи есть кайф от решённой сложной задачи. Есть драйв от того, что твой код работает на миллионах устройств. Есть удовлетворение от автоматизации, которая избавляет людей от рутины (хотя и создаёт свою).</p>
<p>Но если ты идёшь в АйТи за «лёгкими деньгами», «гибким графиком» и «работой на Бали» — разворачивайся и уходи. Потому что это миф. Красивая сказка, которую рассказывают продавцы курсов.</p>
<p>Реальность — она другая. Реальность — это твой 50-й pull request за день, 20-е совещание за неделю, 100-й регресс-тест за месяц. Реальность — это алерт в 3 часа ночи и вопрос «а где бэкап?».</p>
<p>И знаешь что? Многим это нравится. Потому что рутина — это часть работы. Любой работы. В АйТи, как и везде, есть своя грязная работа, которую кто-то должен делать.</p>
<p>Так что решай сам. Я просто показал тебе, что находится за красивой витриной «работы мечты в АйТи». А дальше — твой выбор.</p>
<p>Если всё-таки решил идти: моя собственная дорога сюда описана в <a href="/posts/my-it-way/">«Как я вошёл в IT»</a> — без курсов и красивой траектории. Про то, как учиться дальше, — <a href="/posts/education/">«Обучение и образование»</a>, а про то, что происходит на входе в компанию, — <a href="/posts/after-50-interview/">«Я прошел 50 собесов и вот что я понял»</a>.</p>
<p>И одна оговорка про дежурную часть, раз уж я её тут расписал в самых мрачных красках: она лечится не выносливостью, а устройством. <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/on-call-rotation/" rel="noopener noreferrer" target="_blank">Как собирают ротацию</a>, чтобы она не выжигала людей, и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/alert-fatigue-management/" rel="noopener noreferrer" target="_blank">как разгребают алерт-фатигу</a>, чтобы будили по делу, — в карте компетенций.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Слушай, прежде чем ты понесёшь свои кровные на очередные курсы «стань программистом за три месяца», давай честно поговорим о том, о чём тебе не расскажут продавцы этих самых курсов. Все эти истории про гибкий график, работу с ноутбуком на Бали и зарплаты в долларах — это как картинки в меню в забегаловке: выглядят аппетитно, а на деле ты получаешь совсем не то, что ожидал.</p>
<p>Я собрал для тебя реальные истории о рутинной, скучной, монотонной работе в АйТи — для каждой профессии отдельно. Потому что рутина везде своя, но одинаково тоскливая. Погнали разбираться.</p>
<h2 id="devopssre-инженер-добро-пожаловать-в-кошмар-247">DevOps/SRE инженер: добро пожаловать в кошмар 24/7</h2>
<p>Знаешь, что самое «клёвое» в работе DevOps-инженера? Возможность проснуться в 6 утра от звонка, когда у клиента упала нода в кластере БД. И это не какой-то редкий случай — это твоя новая реальность, которая будет случаться 1–2 раза в месяц. Только представь: ты ещё толком не проснулся, одним глазом смотришь в телефон, копируешь логи и машинально запускаешь стандартный набор инструментов восстановления.</p>
<h3 id="бесконечный-мониторинг">Бесконечный мониторинг</h3>
<p>Твой рабочий день начинается с проверки дашбордов. Grafana, Prometheus, Datadog — твои новые лучшие друзья. Ты часами смотришь на графики, анализируешь метрики: время отклика, загрузку CPU, потребление памяти. Звучит захватывающе? Попробуй делать это каждый день в течение года.</p>
<p>А ещё есть логи. Море логов. Океан логов. ELK-стек становится твоим вторым домом, где ты проводишь больше времени, чем в настоящем. Тебе нужно найти иголку в стоге сена — вот только стог размером с небоскрёб, а игла может быть где угодно.</p>
<h3 id="дежурства-и-инциденты">Дежурства и инциденты</h3>
<p>Помнишь, как тебе обещали гибкий график? Ну так вот, он действительно гибкий — гнётся во все стороны, только не в твою пользу. Ты дежуришь по ночам и выходным. Алерт может прилететь в любой момент. Семейный ужин? Прогулка с собакой? Извини, приятель, но продакшен горит, и тушить его будешь ты.</p>
<p>И после того как ты всё починил, наступает самое «весёлое» — написание отчёта клиенту. Нужно расписать, что произошло, почему произошло, как ты это починил и что сделал, чтобы это больше не повторилось. На 200 страниц. Потому что некоторые заказчики испытывают благоговейный ужас перед кипой бумаг.</p>
<h3 id="рутинная-автоматизация">Рутинная автоматизация</h3>
<p>Ирония в том, что половину своего времени ты тратишь на автоматизацию… чтобы автоматизировать рутину. Пишешь CI/CD-пайплайны, настраиваешь мониторинг, конфигурируешь инфраструктуру через Ansible или Terraform. Но это тоже рутина! Просто рутина для автоматизации рутины. Рутинный Inception, понимаешь?</p>
<p>По концепции SRE ты не должен тратить больше 50% времени на операционные задачи (toil). Звучит неплохо, правда? Только вот в реальности эти 50% превращаются в 80%, а оставшиеся 20% ты тратишь на то, чтобы объяснять менеджерам, почему у тебя нет времени на разработку.</p>
<h2 id="системный-администратор-а-можешь-включить-принтер">Системный администратор: &ldquo;а можешь включить принтер?&rdquo;</h2>
<p>Если ты думаешь, что системный администратор — это крутой хакер из фильмов, который взламывает Пентагон за пять минут, у меня для тебя плохие новости.</p>
<h3 id="техподдержка-first-line">Техподдержка first-line</h3>
<p>Реальная жизнь сисадмина выглядит так: ты бежишь в бухгалтерию, чтобы «включить процессор». Да-да, именно так пользователи называют системный блок. Или меняешь картридж в принтере. Снова. В третий раз за день. В том же самом принтере.</p>
<p>Тебе прилетает 5–10 заявок в день через ServiceDesk. «Не работает интернет» (кабель не воткнут), «Компьютер тормозит» (забита оперативка хромом с 50 вкладками), «Пропал Word» (иконку случайно удалили с рабочего стола). И так каждый день. Месяц за месяцем. Год за годом.</p>
<h3 id="обслуживание-инфраструктуры">Обслуживание инфраструктуры</h3>
<p>А ещё есть «настоящая» работа. Резервное копирование данных — каждый день, каждую ночь. Обновление систем — вручную, на каждом сервере. Проверка работоспособности — по списку, в Excel-табличке. Настройка учётных записей — одна за другой.</p>
<p>Знаешь, что самое страшное? «Панический ступор от осознания, что бэкапа нет, а ты только что отформатировал диск с документами целого отдела». Это не шутка. Это реальная история.</p>
<h3 id="работа-247">Работа 24/7</h3>
<p>Ты всегда на связи. Всегда. В отпуске, в выходные, на пляже с коктейлем. Бизнес не может обходиться без IT. Даже пробить товар невозможно без софта. Поэтому, когда что-то падает, — звонят тебе. В любое время суток.</p>
<p>Монотонность, усидчивость, умение выполнять однообразную работу — вот твои главные навыки. А ещё стрессоустойчивость, потому что работать приходится в ненормированном графике при постоянных форс-мажорах.</p>
<h2 id="программист-код-баги-рефакторинг-повтори">Программист: код, баги, рефакторинг, повтори</h2>
<p>Теперь — о самой, казалось бы, творческой профессии в АйТи. Спойлер: творчества там не больше, чем в работе на конвейере.</p>
<h3 id="backend-разработчик-crud-до-потери-пульса">Backend-разработчик: CRUD до потери пульса</h3>
<p>CRUD: Create, Read, Update, Delete. Четыре операции, которые ты будешь писать до конца своих дней. Создать запись в базе, прочитать, обновить, удалить. Снова и снова. С разными таблицами, разными сущностями, но суть одна и та же.</p>
<p>API-endpoints. Сотни, тысячи API-endpoints. Принять запрос, валидировать данные, сделать запрос в базу, отдать ответ. Меняются только названия. Для разнообразия можешь добавить аутентификацию и авторизацию — но это тоже шаблонный код.</p>
<p>А ещё есть legacy. Старый код, написанный предыдущим разработчиком, который давно уволился и теперь занимается разведением кроликов на даче. Тебе нужно разобраться в этом коде, понять логику (если она там есть) и что-то исправить. «Тебе приходится исправлять написанный не очень хорошо проект за кем-то. Это в итоге выходит дороже, чем если бы его изначально дали мне».</p>
<h3 id="frontend-разработчик-подвигай-кнопку">Frontend-разработчик: подвигай кнопку</h3>
<p>«Одна кнопка — одна задача на день». Это не преувеличение, это реальность многих фронтендеров. Вся логика на бэке, а ты просто двигаешь элементы туда-сюда. «Подвигать кнопку или перекрасить её».</p>
<p>Адаптация под разные браузеры и устройства. Chrome, Firefox, Safari, Edge. Desktop, tablet, mobile. Android, iOS. И у каждого свои приколы. То, что работает в Chrome, падает в Safari. То, что красиво на desktop, разваливается на mobile.</p>
<p>Работа с отступами. «14px вместо 16px, нарушая дефолтные шаги в 8px». Дизайнер внезапно решает, что нужен отступ не 16, а 14 пикселей. Зачем? А фиг его знает. Просто так захотелось. И ты меняешь эти 2 пикселя во всём проекте.</p>
<h3 id="общая-рутина-для-всех">Общая рутина для всех</h3>
<p>Код-ревью. Часами. Днями. Проверять чужой код, оставлять комментарии, спорить о том, нужна ли тут переменная или можно инлайнить. «Раздражает проверка pull request. Это может продолжаться часами (и днями!)».</p>
<p>Написание документации. Это не просто скучно — это смертельно скучно. «Раздражает создавать и упорядочивать документацию. В процессе бывает сложно перевести знания в голове на человеческий язык».</p>
<p>Тестирование. Написание тестов. Перезапуск тестов. Запустил тест — упал. Починил — запустил снова. Упал в другом месте. И так по кругу.</p>
<h2 id="qa-тестировщик-регресс-регресс-регресс">QA-тестировщик: регресс, регресс, регресс</h2>
<p>Думаешь, тестировщики просто играют в игры или тыкают в приложения? Ха. Если бы.</p>
<h3 id="регрессионное-тестирование">Регрессионное тестирование</h3>
<p>«Регрессионное тестирование — сотый раз прогонять». Представь, что тебя заставляют играть в одну и ту же игру, проходить один и тот же уровень, ровно по тому же маршруту, делать те же действия. Сто раз подряд. А потом ещё сто.</p>
<p>Smoke-тесты после каждого деплоя. Один и тот же набор проверок: открыть приложение, залогиниться, открыть главную страницу, проверить основные функции. И так после каждого обновления.</p>
<p>«Играть в одно и то же раз за разом». Это не просто скучно — это убивает мотивацию. «Чаще всего работа трудная, но интересная. Иногда — безумно скучная».</p>
<h3 id="документация-test-кейсов">Документация test-кейсов</h3>
<p>Составление подробных тест-кейсов. Шаг 1: открыть приложение. Шаг 2: нажать на кнопку входа. Шаг 3: ввести логин. Шаг 4: ввести пароль. И так на 100 шагов для одного простого сценария. А сценариев — сотни.</p>
<p>Баг-репорты. Каждый баг нужно задокументировать: что делал, что ожидал, что получил, окружение, версия билда, шаги для воспроизведения. И таких багов в день может быть десятки.</p>
<p>Поддержание документации в актуальном состоянии. Изменилась кнопка — нужно обновить все тест-кейсы, где она упоминается. Добавился новый экран — нужно написать новые тест-кейсы. Бесконечная рутина.</p>
<h2 id="team-lead--tech-lead-совещания-вместо-кода">Team Lead / Tech Lead: совещания вместо кода</h2>
<p>Ты думал стать лидером, чтобы больше влиять на проект? Приятель, у меня для тебя новости.</p>
<h3 id="встречи-встречи-встречи">Встречи, встречи, встречи</h3>
<p>«Марафон встреч на несколько часов». Ежедневные стендапы, груминги, планирования, ретроспективы, one-to-one-встречи. Твой календарь похож на тетрис, где каждый блок — это совещание. И между этими блоками нет места для того, чтобы что-то сделать.</p>
<p>Утро начинается с дейли. Потом — планирование спринта. После обеда — груминг бэклога. Вечером — one-to-one с разработчиками. А между всем этим нужно ещё и работу делать. Когда? После работы. Ночью. На выходных.</p>
<h3 id="код-ревью-как-основная-работа">Код-ревью как основная работа</h3>
<p>Проверка pull request&rsquo;ов. Это теперь не просто часть твоей работы — это основная часть. Ты проверяешь код, оставляешь комментарии, споришь с разработчиками о том, почему этот подход лучше или хуже. Часами.</p>
<p>Парное программирование с джунами. Объяснять базовые вещи. Показывать, как правильно. Смотреть, как они делают ошибки, которые ты совершал пять лет назад. Терпеть и снова объяснять.</p>
<h3 id="административная-рутина">Административная рутина</h3>
<p>Распределение задач: кому что дать, кто с чем справится, у кого какая загрузка. Excel-таблички или Jira-борды — твои новые инструменты.</p>
<p>Решение конфликтов. Вася не может работать с Петей, потому что они поругались из-за подхода к написанию тестов. Маша выгорает, потому что все задачи скучные. Коля хочет уйти, потому что нет карьерного роста.</p>
<p>Заполнение отчётов: что сделали, сколько времени потратили, какие проблемы возникли, какие планы на следующую неделю. Каждую неделю. Каждый спринт. Каждый месяц.</p>
<h2 id="product-manager-созвоны-метрики-согласования">Product Manager: созвоны, метрики, согласования</h2>
<p>PM — это человек, который общается. Много общается. Очень много общается. И потом ещё немного общается.</p>
<h3 id="марафон-встреч">Марафон встреч</h3>
<p>Типичный день продакта: «утренний скрам, архитектурный созвон, созвон с клиентом, синк с инженерами, груминг бэклога». Твой день — это бесконечная череда встреч. Созвон за созвоном, без перерывов.</p>
<p>С 11 до 14:00, иногда до 16:00 — ты в постоянной коммуникации. Команда, клиенты, стейкхолдеры, партнёры. Все хотят с тобой поговорить. Все хотят что-то обсудить. Все хотят твоего внимания.</p>
<p>Focus time? Первые пару часов утром, до того как начнутся встречи. Вот это твоё время, чтобы подумать, написать документацию, сформулировать гипотезы. А потом — добро пожаловать в марафон.</p>
<h3 id="проверка-метрик-каждое-утро">Проверка метрик каждое утро</h3>
<p>«Первым делом всегда смотрю все метрики по продукту на дашборде». Каждый день. Без исключений. Retention, NPS, конверсия, активность пользователей, revenue. Цифры, цифры, цифры.</p>
<p>Анализ результатов A/B-тестов. Запустили тест — нужно посмотреть результаты. Победила версия B — нужно раскатать на всех. Не сработало — нужно понять, почему, и придумать новую гипотезу.</p>
<h3 id="документация-документация-документация">Документация, документация, документация</h3>
<p>Написание ТЗ и требований. Для каждой фичи нужно написать, что хотим сделать, зачем, для кого, как это будет выглядеть, как это должно работать.</p>
<p>Подготовка бизнес-планов. Что даст эта фича, как будем измерять успех, какие метрики должны вырасти, на сколько, почему именно на столько.</p>
<p>Формирование бэклога. Приоритизация задач: что делаем сейчас, что потом, что не делаем вообще. И объяснение команде, почему именно в таком порядке.</p>
<h2 id="data-scientist-80-времени-на-подготовку-данных">Data Scientist: 80% времени на подготовку данных</h2>
<p>Забудь про крутые нейросети и машинное обучение. Реальность DS — это работа с данными. Очень. Долгая. Работа. С. Данными.</p>
<h3 id="разметка-очистка-препроцессинг">Разметка, очистка, препроцессинг</h3>
<p>«Data scientist тратит большую часть своего времени на сбор, разметку, классификацию данных». Не на обучение моделей. Не на анализ. На работу с сырыми данными.</p>
<p>Разметка данных. Вручную. Тысячи, десятки тысяч образцов. «Это кошка. Это собака. Это тоже кошка. А это размытая фигня, которую хрен пойми что».</p>
<p>Очистка данных. Удаление дубликатов, обработка пропусков, исправление ошибок, нормализация форматов. Excel, Python, pandas — твои инструменты. Рутина — твоя реальность.</p>
<h3 id="документация-на-200-страниц">Документация на 200 страниц</h3>
<p>«Написание 200 страниц текста для заказчика». Техническая документация, сопроводительная документация. Описание модели, описание данных, описание метрик, описание результатов.</p>
<p>«Что, как, почему именно так». Каждое решение нужно обосновать: почему выбрали эту модель, а не ту; почему эти гиперпараметры, а не другие; почему эта метрика, а не другая.</p>
<h3 id="работа-в-изоляции">Работа в изоляции</h3>
<p>«Data scientists в итоге работают одни, даже если сидят за тем же столом, что и разработчики». Один DS на всю команду. Не с кем посоветоваться, не с кем обсудить подходы.</p>
<p>«Рассчитывают финансы в Excel». Иногда вместо машинного обучения ты просто заполняешь Excel-таблички. Потому что бизнесу нужен быстрый результат, а не красивая модель.</p>
<h2 id="it-архитектор-документация-и-согласования">IT-архитектор: документация и согласования</h2>
<p>Ты думал, будешь проектировать крутые системы? Будешь. Но сначала напишешь 300 страниц документации.</p>
<h3 id="рутина-документации">Рутина документации</h3>
<p>«Подготовка документации по проектам. Рутины более чем достаточно». Техническая документация, архитектурная документация, пользовательская документация.</p>
<p>«Вычертить огромный генплан месяцами». Схемы, диаграммы, чертежи. Одно и то же, но с небольшими вариациями. Для одного проекта. Потом для другого. Потом для третьего.</p>
<p>Проектирование типовых проектов («коробок»). Не каждый раз что-то новое. Часто — одно и то же, со стандартными решениями, стандартными подходами.</p>
<h3 id="согласования">Согласования</h3>
<p>Координация команды. Распределение задач, контроль сроков, проверка качества. People management, как ни крути.</p>
<p>Согласования с бизнесом. Объяснять, почему это решение лучше, почему оно стоит таких денег, почему нужно столько времени. Презентации, совещания, обоснования.</p>
<h2 id="дизайнер-правки-правки-ещё-правки">Дизайнер: правки, правки, ещё правки</h2>
<p>«Бесконечные правки, а потом просят вернуться к первоначальному варианту».</p>
<h3 id="рутинные-задачи">Рутинные задачи</h3>
<p>Разработка технических эскизов. Создание макетов. Адаптация под разные разрешения. Экспорт в разные форматы. Подготовка ассетов для разработчиков.</p>
<p>Многочасовые правки. Клиент посмотрел — что-то не нравится. Изменил — опять не то. Сделал как было в начале — вот теперь хорошо. Три дня работы — в урну.</p>
<p>Монотонность и выгорание. «Дизайнер заставляет себя работать над рутинным процессом, пока все однотипные и скучные задачи не будут завершены».</p>
<h2 id="общие-паттерны-рутины-что-объединяет-всех">Общие паттерны рутины: что объединяет всех</h2>
<p>Изучив десятки источников и истории реальных людей, я вывел паттерны, которые присутствуют в каждой АйТи-профессии:</p>
<h3 id="1-документация--бесит-всех">1. Документация — бесит всех</h3>
<p>От джуна до архитектора, от тестировщика до продакта — все ненавидят писать документацию. Но все обязаны это делать. Снова и снова.</p>
<h3 id="2-встречи-вместо-работы">2. Встречи вместо работы</h3>
<p>Стендапы, планирования, груминги, ретро, синки, one-to-one. Твой день превращается в марафон встреч, между которыми нужно ещё и работу делать.</p>
<h3 id="3-legacy-код-и-технический-долг">3. Legacy-код и технический долг</h3>
<p>«30% всего объёма работ по итогу не используется». Работа в стол. Или работа с кодом, который написали пять лет назад и который теперь никто не понимает.</p>
<h3 id="4-работа-247">4. Работа 24/7</h3>
<p>Особенно для DevOps, SRE, сисадминов. Ты всегда на связи. Отпуск, выходные, праздники — неважно. Если упало — звонят тебе.</p>
<h3 id="5-выгорание-через-рутину">5. Выгорание через рутину</h3>
<p>«78% ИТ-работников страдают от выгорания». «89% специалистов трудятся в режиме 5/2, 69% работают сверхурочно и дома». Монотонные задачи, большая нагрузка, постоянный стресс.</p>
<h3 id="6-ненормированный-рабочий-день">6. Ненормированный рабочий день</h3>
<p>Никаких доплат за переработку. В качестве компенсации — три дополнительных дня отпуска. И это в лучшем случае. А в худшем — просто работай больше за те же деньги.</p>
<h3 id="7-можно-легко-разлюбить-свою-работу">7. &ldquo;Можно легко разлюбить свою работу&rdquo;</h3>
<p>«Проблемы имеют свойство накапливаться как снежный ком. Большой технический долг, давление руководства, административно-бюрократические проблемы — если не уметь с этим справляться, можно легко разлюбить свою работу».</p>
<h2 id="итого-суровая-правда-о-работе-в-айти">Итого: суровая правда о работе в АйТи</h2>
<p>Знаешь, что самое смешное? Я не придумал ничего из этого. Всё, что ты прочитал выше, — это реальные истории реальных людей.</p>
<p>АйТи — это не работа мечты с кофе на Бали. Это рутина, стресс, выгорание и постоянное ощущение, что ты бежишь на месте.</p>
<p>Это ненормированный рабочий день, где «гибкий график» означает «работай, когда хочешь, но 24/7». Это постоянное обучение, потому что технологии устаревают быстрее, чем молоко в холодильнике.</p>
<p>Это работа, где «30% всего объёма работ по итогу не используется». Где проекты закрываются на полпути. Где требования меняются каждую неделю.</p>
<h3 id="но-стоит-ли-оно-того">Но стоит ли оно того?</h3>
<p>Если после всего прочитанного ты всё ещё хочешь в АйТи — значит, это твоё. Потому что, несмотря на всю эту рутину, в АйТи есть кайф от решённой сложной задачи. Есть драйв от того, что твой код работает на миллионах устройств. Есть удовлетворение от автоматизации, которая избавляет людей от рутины (хотя и создаёт свою).</p>
<p>Но если ты идёшь в АйТи за «лёгкими деньгами», «гибким графиком» и «работой на Бали» — разворачивайся и уходи. Потому что это миф. Красивая сказка, которую рассказывают продавцы курсов.</p>
<p>Реальность — она другая. Реальность — это твой 50-й pull request за день, 20-е совещание за неделю, 100-й регресс-тест за месяц. Реальность — это алерт в 3 часа ночи и вопрос «а где бэкап?».</p>
<p>И знаешь что? Многим это нравится. Потому что рутина — это часть работы. Любой работы. В АйТи, как и везде, есть своя грязная работа, которую кто-то должен делать.</p>
<p>Так что решай сам. Я просто показал тебе, что находится за красивой витриной «работы мечты в АйТи». А дальше — твой выбор.</p>
<p>Если всё-таки решил идти: моя собственная дорога сюда описана в <a href="/posts/my-it-way/">«Как я вошёл в IT»</a> — без курсов и красивой траектории. Про то, как учиться дальше, — <a href="/posts/education/">«Обучение и образование»</a>, а про то, что происходит на входе в компанию, — <a href="/posts/after-50-interview/">«Я прошел 50 собесов и вот что я понял»</a>.</p>
<p>И одна оговорка про дежурную часть, раз уж я её тут расписал в самых мрачных красках: она лечится не выносливостью, а устройством. <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/on-call-rotation/" rel="noopener noreferrer" target="_blank">Как собирают ротацию</a>, чтобы она не выжигала людей, и <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/alert-fatigue-management/" rel="noopener noreferrer" target="_blank">как разгребают алерт-фатигу</a>, чтобы будили по делу, — в карте компетенций.</p>
</div>]]></content:encoded>
    </item><item>
      <title>Мониторинг бизнес-метрик: как понять, что твой сервис действительно работает</title>
      <link>https://jtprog.ru/posts/business-metrics/</link>
      <pubDate>Mon, 22 Dec 2025 10:23:38 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/business-metrics/</guid>
      <category>Monitoring</category>
      <category>SRE</category>
      <category>DevOps</category>
      <category>Мысли</category>
      <category>Observability</category>
      <category domain="category">Work</category>
      <enclosure url="https://jtprog.ru/work.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/work.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Сегодня поговорим о теме, которая часто остается в тени инфраструктурного мониторинга, но при этом не менее важна — о <strong>мониторинге бизнес-метрик</strong>. Про <a href="/posts/wat-monitoring/">мониторинг вообще — что, куда и зачем собирать</a> — у меня есть отдельный текст, а тут речь про слой выше. Если ты SRE, DevOps или TeamLead, то наверняка сталкивался с ситуацией: все графики зеленые, CPU не плавится, память в норме, а бизнес приходит и говорит &ldquo;что-то не работает&rdquo;. И вот тут начинается самое интересное.</p>
<h2 id="зачем-вообще-нужны-бизнес-метрики">Зачем вообще нужны бизнес-метрики?</h2>
<p>Давай начнем с простого вопроса: зачем мы вообще запускаем сервисы? Правильно — чтобы они <strong>решали чьи-то проблемы и приносили деньги</strong>. И вот тут классический технический мониторинг начинает пробуксовывать. Ты можешь знать, что у тебя загрузка CPU 15%, время выполнения запроса (latency) 50ms, а количество 500-ошибок в пределах нормы. Но ты <strong>не знаешь</strong>, сколько пользователей прямо сейчас платят за твой сервис, какой процент из них доходит до формы оплаты, и приносит ли вообще твой продукт деньги.</p>
<p>Представь себе ситуацию: твой сайт работает отлично с технической точки зрения, но пользователи не могут найти кнопку оплаты, потому что она потерялась где-то внизу страницы после последнего релиза. Инфраструктурный мониторинг тебе об этом <strong>не скажет</strong>. А вот бизнес-метрики — скажут сразу, как только конверсия в оплату упадет.</p>
<h2 id="три-слоя-мониторинга-от-железа-до-бизнеса">Три слоя мониторинга: от железа до бизнеса</h2>
<p>В НСПК (Национальная система платежных карт, те самые ребята, что делают карты &ldquo;Мир&rdquo;) придерживаются подхода, который я считаю очень правильным: сервис нужно раскладывать на три слоя, и <strong>мониторить строго сверху вниз</strong>.</p>
<p><strong>Бизнес-слой</strong> — это то, что видит конечный пользователь. Для платежной системы это объемы успешных авторизационных запросов, количество операций по типам, время обработки платежей. Для интернет-магазина — это количество успешных заказов, средний чек, конверсия в покупку. Для SaaS-продукта — количество активных подписок, revenue per user, churn rate. Строгая версия этого слоя — <a href="/posts/slo-as-architecture-blueprint/">SLI и SLO, выведенные из критичного пользовательского пути</a>: те же «успешные заказы», только с порогом, окном и бюджетом ошибок.</p>
<p><strong>Прикладной слой</strong> — данные о работе приложений: статусы модулей, метрики используемого ПО, технические логи. Тут уже можно задействовать готовые экспортеры и интеграции.</p>
<p><strong>Инфраструктурный слой</strong> — классика: CPU, память, диск, сеть, состояние железа.</p>
<p>Почему именно сверху вниз? Потому что <strong>бизнес-метрики — это главное</strong>. Если у тебя бизнес-метрики в порядке, но один из серверов немного подгружен — это не критично. А вот если бизнес-метрики падают, даже при зеленой инфраструктуре — это уже проблема, которую надо расследовать немедленно.</p>
<h2 id="методологии-мониторинга-use-red-и-uca">Методологии мониторинга: USE, RED и UCA</h2>
<p>Существует несколько классических подходов к сбору метрик, и каждый из них решает свою задачу.</p>
<h3 id="use-метод-следим-за-ресурсами">USE-метод: следим за ресурсами</h3>
<p><strong>USE</strong> — это акроним от <strong>Utilization, Saturation, Errors</strong> (Утилизация, Насыщение, Ошибки). Метод предложил Brendan Gregg, и он отлично работает для <strong>низкоуровневых метрик</strong>.</p>
<p>Если упростить, то для каждого ресурса системы (CPU, память, диск, сеть) ты смотришь три показателя:</p>
<ul>
<li><strong>Utilization</strong> — насколько загружен ресурс (например, CPU загружен на 80%)</li>
<li><strong>Saturation</strong> — есть ли очередь к ресурсу (например, 10 процессов ждут своей очереди на выполнение)</li>
<li><strong>Errors</strong> — количество ошибок, которые возвращает этот ресурс</li>
</ul>
<p>USE помогает быстро выявить узкие места в системе. Если у тебя диск утилизирован на 100%, а очередь на запись растет — вот тебе и проблема.</p>
<h3 id="red-метод-следим-за-сервисами">RED-метод: следим за сервисами</h3>
<p><strong>RED</strong> — это <strong>Rate, Errors, Duration</strong> (Скорость, Ошибки, Продолжительность). Метод предложил Tom Wilkie специально для <strong>микросервисных архитектур</strong>.</p>
<p>Для каждого сервиса ты отслеживаешь:</p>
<ul>
<li><strong>Rate</strong> — количество запросов в секунду</li>
<li><strong>Errors</strong> — количество неудачных запросов в секунду</li>
<li><strong>Duration</strong> — время обработки каждого запроса</li>
</ul>
<p>RED дает тебе возможность посмотреть на систему <strong>глазами пользователя</strong>. Если у тебя растет количество ошибок или увеличивается время ответа — пользователи это сразу почувствуют. Метод отлично работает для веб-сервисов, API, баз данных — везде, где есть концепция &ldquo;запрос-ответ&rdquo;.</p>
<h3 id="uca-метод-следим-за-бизнесом">UCA-метод: следим за бизнесом</h3>
<p>И вот мы подходим к самому интересному — <strong>UCA</strong> (Users, Conversions, Activity). Этот метод предложил Mike Julian, и он <strong>целенаправленно нацелен на измерение бизнес-метрик</strong>.</p>
<p><strong>Users</strong> (Пользователи) — кто является пользователями твоего сервиса? Для разных отраслей это могут быть разные показатели: количество активных пользователей, посетителей на сайте, звонков в колл-центр, покупателей в час.</p>
<p><strong>Conversions</strong> (Конверсии) — это про то, как пользователь приносит доход твоему сервису. Купил подписку? Забронировал номер в отеле? Оформил заказ? Это всё конверсии.</p>
<p><strong>Activity</strong> (Активность) — показывает, что пользователи <strong>продолжают</strong> пользоваться твоим продуктом. Входы на сайт, просмотры товаров, добавления в корзину, время на сайте.</p>
<p>UCA позволяет собирать метрики, которые показывают, <strong>как бизнес себя чувствует</strong>. И отталкиваясь от этих метрик, ты уже можешь искать проблемы на уровне USE и RED — понимать, из-за ухудшения каких показателей стоит поднимать инженеров.</p>
<h2 id="какие-метрики-собирать-на-практике">Какие метрики собирать на практике</h2>
<p>Давай конкретнее. Вот список метрик, которые стоит отслеживать на разных уровнях:</p>
<p><strong>Мониторинг бизнес-метрик:</strong></p>
<ul>
<li>Сколько пользователей зарегистрировано в системе?</li>
<li>Сколько пользуется приложением раз в день/неделю/месяц (DAU/WAU/MAU)?</li>
<li>Какой процент людей после регистрации доходит до формы оплаты?</li>
<li>Сколько они платят (средний чек, медианный чек)?</li>
<li>Как люди пользуются услугами — какие фичи популярны, а какие нет?</li>
<li>Какова общая выручка и прибыль?</li>
</ul>
<p><strong>Мониторинг состояния приложений:</strong></p>
<ul>
<li>Количество запросов в единицу времени (RPS)</li>
<li>Количество активных пользователей в системе</li>
<li>Количество записей в БД (общее и новых в единицу времени)</li>
<li>Количество ошибок в сервисе</li>
<li>Время выполнения запросов или latency (50-й, 95-й, 99-й процентили)</li>
</ul>
<p><strong>Мониторинг оборудования:</strong></p>
<ul>
<li>Нагрузка на CPU</li>
<li>Свободное место в RAM</li>
<li>Свободное место на дисках</li>
<li>Нагрузка на сеть</li>
<li>IOPS на дисках</li>
<li>Количество запущенных процессов</li>
</ul>
<h2 id="инструменты-для-мониторинга-бизнес-метрик">Инструменты для мониторинга бизнес-метрик</h2>
<p>Теперь поговорим об инструментах. В идеальном мире у тебя должен быть стек, который покрывает все три уровня.</p>
<h3 id="для-бизнес-метрик-google-analytics-и-яндексметрика">Для бизнес-метрик: Google Analytics и Яндекс.Метрика</h3>
<p>Если речь идет о веб-сервисах, то <strong>Google Analytics</strong> и <strong>Яндекс.Метрика</strong> — это базовый минимум. Они позволяют отслеживать базовые переходы и конверсии готовыми средствами.</p>
<p>Яндекс.Метрика особенно хороша для российского рынка — она корректнее учитывает трафик из Яндекса, работает в соответствии с ФЗ-152, и у нее интуитивный интерфейс для русскоязычных пользователей. Google Analytics дает больше возможностей для крупных проектов с большими объемами данных (до 200 000 запросов в сутки против 5000 у Метрики).</p>
<p>Но если тебе нужно <strong>углубленное понимание</strong> — придется работать в связке с разработкой. Настроить сбор кастомных метрик, которые важны именно для твоего продукта.</p>
<h3 id="для-прикладных-метрик-prometheus--grafana">Для прикладных метрик: Prometheus + Grafana</h3>
<p><strong>Prometheus</strong> — это система сбора и хранения метрик временных рядов, которую изначально разработали в SoundCloud. Она стала стандартом де-факто для сбора метрик в современных инфраструктурах.</p>
<p><strong>Grafana</strong> — универсальная обертка для визуализации (<a href="/posts/install-grafana7/">как её поставить</a> — на пару абзацев дела). Она сама ничего не собирает и не хранит, но умеет красиво отображать данные из Prometheus, PostgreSQL, ClickHouse и еще десятков источников.</p>
<p>Связка <strong>Prometheus + Grafana</strong> позволяет собирать как технические метрики (через экспортеры — node_exporter для системных метрик, nginx-prometheus-exporter для Nginx и т.д.), так и <strong>бизнес-метрики</strong> твоего приложения. Да-да, Prometheus прекрасно справляется со сбором бизнес-метрик: выручка за день, количество активных пользователей, количество заказов — достаточно сделать свой endpoint <code>/metrics</code> в приложении и экспортировать туда нужные данные.</p>
<h3 id="для-операционной-аналитики-splunk">Для операционной аналитики: Splunk</h3>
<p>В НСПК для комплексного анализа используют <strong>Splunk</strong> — платформу операционной аналитики. Splunk умеет:</p>
<ul>
<li>Анализировать тысячи метрик в фоновом режиме с использованием статистических анализаторов</li>
<li>Обнаруживать аномалии и делать прогнозы с помощью машинного обучения</li>
<li>Создавать сложные параметризованные отчеты для дежурной службы</li>
<li>Коррелировать данные из разных источников</li>
</ul>
<p>Splunk — это уже не просто мониторинг, а <strong>платформа для принятия решений</strong>. Он позволяет не только видеть, что что-то пошло не так, но и понимать, <strong>почему</strong> это произошло, и <strong>как это влияет на бизнес</strong>.</p>
<h3 id="для-логирования-elk-stack">Для логирования: ELK Stack</h3>
<p><strong>ELK</strong> (Elasticsearch, Logstash, Kibana) или его альтернатива <strong>EFK</strong> (где вместо Logstash используется Fluentd) — это стек для агрегации и анализа логов.</p>
<p>Когда у тебя есть бизнес-метрики и прикладные метрики, логи становятся третьим измерением, которое помогает <strong>расследовать инциденты</strong>. Ты видишь на графике, что конверсия упала в 14:37, переходишь в логи и находишь, что именно в это время начались ошибки при работе с платежным шлюзом.</p>
<h2 id="как-выстраивать-систему-мониторинга">Как выстраивать систему мониторинга</h2>
<p>Теперь самое важное — <strong>как это всё организовать</strong>.</p>
<h3 id="строй-ресурсно-сервисную-модель">Строй ресурсно-сервисную модель</h3>
<p>Первый шаг — понять, что именно ты мониторишь. В НСПК разделяют данные на три слоя, а в большой компании нужна еще и <strong>ресурсно-сервисная модель</strong> (РСМ) — структурированное описание того, какие компоненты обеспечивают работу конкретного сервиса.</p>
<p>Например, сервис &ldquo;Интернет-магазин&rdquo; состоит из:</p>
<ul>
<li>Frontend (Nginx, React-приложение)</li>
<li>Backend API (несколько микросервисов на Java)</li>
<li>База данных (PostgreSQL)</li>
<li>Кэш (Redis)</li>
<li>Очередь сообщений (RabbitMQ)</li>
<li>Платежный шлюз (внешний API)</li>
</ul>
<p>Для каждого компонента ты настраиваешь сбор метрик на всех трех уровнях. И когда бизнес-метрика &ldquo;конверсия в оплату&rdquo; начинает падать, ты уже знаешь, на какие компоненты смотреть.</p>
<h3 id="используй-зонтичный-мониторинг">Используй &ldquo;зонтичный&rdquo; мониторинг</h3>
<p>Одна из лучших практик — использовать <strong>&ldquo;зонтик&rdquo;</strong>, единый слой, который агрегирует данные от различных систем: Zabbix, Prometheus, ELK, и т.д.. Это дает целостную картину и упрощает анализ.</p>
<p>Представь: у тебя Prometheus собирает метрики приложений, Zabbix мониторит железо, ELK собирает логи, а Google Analytics собирает поведенческие данные. Если у каждой команды свой дашборд в своем инструменте — <strong>корреляцию данных не увидеть</strong>. А вот если всё это агрегировано в одном месте (например, в Grafana или Splunk) — картина становится прозрачной.</p>
<h3 id="настраивай-алерты-правильно">Настраивай алерты правильно</h3>
<p><strong>Система оповещений</strong> — это компонент мониторинга, который выполняет действия на основе изменений метрик. Основная цель алертов — <strong>привлечь внимание человека</strong> к текущему состоянию системы.</p>
<p>Важные моменты:</p>
<ul>
<li>Алерт должен содержать информацию о том, <strong>что</strong> пошло не так и <strong>где</strong> найти дополнительную информацию</li>
<li>Не алерть на всё подряд — это приводит к &ldquo;усталости от алертов&rdquo;</li>
<li>Настраивай алерты сначала на <strong>бизнес-метрики</strong>, потом на прикладные, и только в конце — на инфраструктурные</li>
</ul>
<p>Например, алерт &ldquo;CPU загружен на 80%&rdquo; — это информационный сигнал. А вот &ldquo;Конверсия в оплату упала на 50% за последние 10 минут&rdquo; — это уже <strong>критический инцидент</strong>, который требует немедленного реагирования.</p>
<p>Разница между этими двумя строчками — это <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/symptom-vs-cause-alerting/" rel="noopener noreferrer" target="_blank">дисциплина «симптом, а не причина»</a>: будим человека на то, что чувствует пользователь, а метрики причин оставляем для отладки. А если шума уже накопилось столько, что дежурный смотрит мимо, — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/alert-fatigue-management/" rel="noopener noreferrer" target="_blank">про разгребание усталости от алертов</a> есть отдельный лист.</p>
<h3 id="делай-метрики-прозрачными-для-всех">Делай метрики прозрачными для всех</h3>
<p>Один из ключевых принципов — <strong>все члены команды должны видеть одну и ту же информацию из одного источника</strong>. QA, разработчики, админы, аналитики, менеджеры продукта — все должны смотреть на одни и те же дашборды.</p>
<p>Когда у всех единая точка входа, начинает работать <strong>коллективный разум</strong>. Разработчик может заметить, что после его релиза упала конверсия. Админ увидит, что в момент падения бизнес-метрики начались таймауты при обращении к базе. Аналитик обратит внимание, что падение началось после того, как изменилась логика расчета скидок. Вместе они найдут причину быстрее.</p>
<h2 id="ошибки-которых-стоит-избегать">Ошибки, которых стоит избегать</h2>
<p>Давай поговорим о том, чего <strong>не надо</strong> делать.</p>
<h3 id="ошибка-1-отсутствие-связи-между-бизнес-задачами-и-аналитикой">Ошибка 1: Отсутствие связи между бизнес-задачами и аналитикой</h3>
<p>Самая частая проблема — когда ты собираешь метрики &ldquo;потому что так надо&rdquo;, но не понимаешь, <strong>зачем</strong> ты их собираешь.</p>
<p>Правильный подход: начинай с бизнес-целей. Бизнес хочет увеличить выручку на 20%? Отлично, декомпозируй это на метрики: нужно увеличить количество заказов или средний чек? Если заказы — то нужно больше пользователей или выше конверсия существующих? И так далее.</p>
<h3 id="ошибка-2-мониторинг-без-проверок-качества-данных">Ошибка 2: Мониторинг без проверок качества данных</h3>
<p>Представь: ты построил красивый дашборд, но данные в него приходят с ошибками. Счетчик установлен не на всех страницах. Конверсионные события дублируются. В результате твои метрики показывают 30% конверсии, а на самом деле она 3%.</p>
<p><strong>Проверяй качество данных</strong>: делай периодические проверки полноты и согласованности, тестируй бизнес-правила, отслеживай источники данных и трансформации.</p>
<h3 id="ошибка-3-плавающее-руководство-по-сбору-данных">Ошибка 3: &ldquo;Плавающее&rdquo; руководство по сбору данных</h3>
<p>Когда у тебя нет четкого стандарта, как называть метрики, как их собирать, и где их хранить — начинается хаос. Один разработчик называет метрику <code>orders_count</code>, другой — <code>order.count</code>, третий — <code>OrdersTotal</code>. В итоге ты не можешь нормально искать и агрегировать данные.</p>
<p><strong>Установи стандарты</strong>: naming conventions для метрик, формат лейблов, структуру дашбордов. И следи, чтобы все их соблюдали.</p>
<h3 id="ошибка-4-разрозненные-данные">Ошибка 4: Разрозненные данные</h3>
<p>Когда метрики инфраструктуры хранятся в Zabbix, прикладные — в Prometheus, бизнесовые — в Google Analytics, а логи — в ELK, и всё это <strong>никак не связано</strong> между собой — ты не можешь проводить корреляцию.</p>
<p><strong>Стремись к единой точке анализа</strong>: используй зонтичный мониторинг, настраивай интеграции между системами, делай так, чтобы все данные были доступны в одном месте.</p>
<h3 id="ошибка-5-исправление-ошибок-по-факту">Ошибка 5: Исправление ошибок &ldquo;по факту&rdquo;</h3>
<p>Если у тебя нет мониторинга, ты узнаешь о проблемах только тогда, когда <strong>клиенты начинают жаловаться</strong>. И это очень дорого — и в деньгах, и в репутации.</p>
<p>Мониторинг — это <strong>профилактика</strong>. Гораздо дешевле обнаружить проблему на графике и исправить её до того, как она повлияла на пользователей, чем тушить пожар, когда половина клиентов уже не может сделать заказ.</p>
<h2 id="практический-чек-лист">Практический чек-лист</h2>
<p>Если ты хочешь построить нормальную систему мониторинга бизнес-метрик, вот тебе чек-лист для самопроверки:</p>
<p><strong>Базовый уровень:</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Определен список основных бизнес-метрик (выручка, количество заказов, конверсии)</li>
<li><input disabled="" type="checkbox"> Бизнес-метрики декомпозированы от уровня руководства до команд</li>
<li><input disabled="" type="checkbox"> Определены сопутствующие метрики (MAU, количество товаров в чеке и т.д.)</li>
<li><input disabled="" type="checkbox"> Определен список прикладных метрик (RPS, latency, error rate)</li>
<li><input disabled="" type="checkbox"> Для каждой метрики определен период обратной связи (как часто проверять)</li>
</ul>
<p><strong>Продвинутый уровень:</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Определены и автоматизированы источники данных по всем метрикам</li>
<li><input disabled="" type="checkbox"> Определены события/встречи, на которых обсуждаются метрики</li>
<li><input disabled="" type="checkbox"> Планирование работы осуществляется на основании метрик</li>
<li><input disabled="" type="checkbox"> Для каждой задачи есть понимание, на какую метрику она влияет</li>
<li><input disabled="" type="checkbox"> Все заинтересованные лица имеют доступ к метрикам</li>
<li><input disabled="" type="checkbox"> Есть система алертинга на критические изменения метрик</li>
</ul>
<p><strong>Экспертный уровень:</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Используется зонтичный мониторинг, агрегирующий данные из всех систем</li>
<li><input disabled="" type="checkbox"> Настроена корреляция между метриками разных уровней</li>
<li><input disabled="" type="checkbox"> Внедрены проверки качества данных</li>
<li><input disabled="" type="checkbox"> Существует документированный стандарт сбора и именования метрик</li>
<li><input disabled="" type="checkbox"> Метрики используются для принятия бизнес-решений</li>
</ul>
<h2 id="выводы">Выводы</h2>
<p>Мониторинг бизнес-метрик — это не просто &ldquo;давайте нарисуем еще один дашборд в Grafana&rdquo;. Это <strong>комплексный подход</strong> к пониманию того, как работает твой продукт с точки зрения бизнеса.</p>
<p>Ключевые мысли:</p>
<ol>
<li><strong>Мониторь сверху вниз</strong>: начинай с бизнес-метрик, потом прикладные, и только в конце инфраструктурные</li>
<li><strong>Используй правильные методологии</strong>: USE для ресурсов, RED для сервисов, UCA для бизнеса</li>
<li><strong>Стремись к единой точке анализа</strong>: все команды должны смотреть на одни и те же данные</li>
<li><strong>Автоматизируй сбор данных</strong>: ручной сбор метрик — это прошлый век</li>
<li><strong>Делай метрики основой для принятия решений</strong>: не просто смотри на графики, а делай выводы и действуй — как именно этот разговор ведётся с бизнесом, разбирал в <a href="/posts/reliability-is-a-conversation/">«Надёжность строится в диалоге с бизнесом»</a></li>
</ol>
<p>Управление проектом без метрик — это как плавание ночью на корабле без карты и навигации. Ты вроде бы куда-то плывешь, но рано или поздно это закончится плохо.</p>
<p>А с правильно настроенным мониторингом бизнес-метрик ты <strong>всегда знаешь</strong>, где ты находишься, куда движешься, и что нужно исправить, чтобы прийти к цели.</p>
<h3 id="полезные-ссылки">Полезные ссылки</h3>
<ul>
<li><a href="https://habr.com/ru/company/nspk/blog/523352/" rel="noopener noreferrer" target="_blank">Как устроен прикладной и бизнес-мониторинг сервисов НСПК</a> — про трехслойную модель мониторинга</li>
<li><a href="https://logrocon.ru/news/monitoring" rel="noopener noreferrer" target="_blank">Основы мониторинга и сбора метрик</a> — про методологии USE, RED, UCA</li>
<li><a href="https://otus.ru/nest/post/1988/" rel="noopener noreferrer" target="_blank">Мониторинг событий и бизнес-метрик</a> — про важность метрик для бизнеса</li>
<li><a href="https://vc.ru/u/744996-evgeniy-ivanov/216307-pro-biznes-i-metriki" rel="noopener noreferrer" target="_blank">Про бизнес и метрики</a> — про структуру и типы метрик</li>
</ul>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Сегодня поговорим о теме, которая часто остается в тени инфраструктурного мониторинга, но при этом не менее важна — о <strong>мониторинге бизнес-метрик</strong>. Про <a href="/posts/wat-monitoring/">мониторинг вообще — что, куда и зачем собирать</a> — у меня есть отдельный текст, а тут речь про слой выше. Если ты SRE, DevOps или TeamLead, то наверняка сталкивался с ситуацией: все графики зеленые, CPU не плавится, память в норме, а бизнес приходит и говорит &ldquo;что-то не работает&rdquo;. И вот тут начинается самое интересное.</p>
<h2 id="зачем-вообще-нужны-бизнес-метрики">Зачем вообще нужны бизнес-метрики?</h2>
<p>Давай начнем с простого вопроса: зачем мы вообще запускаем сервисы? Правильно — чтобы они <strong>решали чьи-то проблемы и приносили деньги</strong>. И вот тут классический технический мониторинг начинает пробуксовывать. Ты можешь знать, что у тебя загрузка CPU 15%, время выполнения запроса (latency) 50ms, а количество 500-ошибок в пределах нормы. Но ты <strong>не знаешь</strong>, сколько пользователей прямо сейчас платят за твой сервис, какой процент из них доходит до формы оплаты, и приносит ли вообще твой продукт деньги.</p>
<p>Представь себе ситуацию: твой сайт работает отлично с технической точки зрения, но пользователи не могут найти кнопку оплаты, потому что она потерялась где-то внизу страницы после последнего релиза. Инфраструктурный мониторинг тебе об этом <strong>не скажет</strong>. А вот бизнес-метрики — скажут сразу, как только конверсия в оплату упадет.</p>
<h2 id="три-слоя-мониторинга-от-железа-до-бизнеса">Три слоя мониторинга: от железа до бизнеса</h2>
<p>В НСПК (Национальная система платежных карт, те самые ребята, что делают карты &ldquo;Мир&rdquo;) придерживаются подхода, который я считаю очень правильным: сервис нужно раскладывать на три слоя, и <strong>мониторить строго сверху вниз</strong>.</p>
<p><strong>Бизнес-слой</strong> — это то, что видит конечный пользователь. Для платежной системы это объемы успешных авторизационных запросов, количество операций по типам, время обработки платежей. Для интернет-магазина — это количество успешных заказов, средний чек, конверсия в покупку. Для SaaS-продукта — количество активных подписок, revenue per user, churn rate. Строгая версия этого слоя — <a href="/posts/slo-as-architecture-blueprint/">SLI и SLO, выведенные из критичного пользовательского пути</a>: те же «успешные заказы», только с порогом, окном и бюджетом ошибок.</p>
<p><strong>Прикладной слой</strong> — данные о работе приложений: статусы модулей, метрики используемого ПО, технические логи. Тут уже можно задействовать готовые экспортеры и интеграции.</p>
<p><strong>Инфраструктурный слой</strong> — классика: CPU, память, диск, сеть, состояние железа.</p>
<p>Почему именно сверху вниз? Потому что <strong>бизнес-метрики — это главное</strong>. Если у тебя бизнес-метрики в порядке, но один из серверов немного подгружен — это не критично. А вот если бизнес-метрики падают, даже при зеленой инфраструктуре — это уже проблема, которую надо расследовать немедленно.</p>
<h2 id="методологии-мониторинга-use-red-и-uca">Методологии мониторинга: USE, RED и UCA</h2>
<p>Существует несколько классических подходов к сбору метрик, и каждый из них решает свою задачу.</p>
<h3 id="use-метод-следим-за-ресурсами">USE-метод: следим за ресурсами</h3>
<p><strong>USE</strong> — это акроним от <strong>Utilization, Saturation, Errors</strong> (Утилизация, Насыщение, Ошибки). Метод предложил Brendan Gregg, и он отлично работает для <strong>низкоуровневых метрик</strong>.</p>
<p>Если упростить, то для каждого ресурса системы (CPU, память, диск, сеть) ты смотришь три показателя:</p>
<ul>
<li><strong>Utilization</strong> — насколько загружен ресурс (например, CPU загружен на 80%)</li>
<li><strong>Saturation</strong> — есть ли очередь к ресурсу (например, 10 процессов ждут своей очереди на выполнение)</li>
<li><strong>Errors</strong> — количество ошибок, которые возвращает этот ресурс</li>
</ul>
<p>USE помогает быстро выявить узкие места в системе. Если у тебя диск утилизирован на 100%, а очередь на запись растет — вот тебе и проблема.</p>
<h3 id="red-метод-следим-за-сервисами">RED-метод: следим за сервисами</h3>
<p><strong>RED</strong> — это <strong>Rate, Errors, Duration</strong> (Скорость, Ошибки, Продолжительность). Метод предложил Tom Wilkie специально для <strong>микросервисных архитектур</strong>.</p>
<p>Для каждого сервиса ты отслеживаешь:</p>
<ul>
<li><strong>Rate</strong> — количество запросов в секунду</li>
<li><strong>Errors</strong> — количество неудачных запросов в секунду</li>
<li><strong>Duration</strong> — время обработки каждого запроса</li>
</ul>
<p>RED дает тебе возможность посмотреть на систему <strong>глазами пользователя</strong>. Если у тебя растет количество ошибок или увеличивается время ответа — пользователи это сразу почувствуют. Метод отлично работает для веб-сервисов, API, баз данных — везде, где есть концепция &ldquo;запрос-ответ&rdquo;.</p>
<h3 id="uca-метод-следим-за-бизнесом">UCA-метод: следим за бизнесом</h3>
<p>И вот мы подходим к самому интересному — <strong>UCA</strong> (Users, Conversions, Activity). Этот метод предложил Mike Julian, и он <strong>целенаправленно нацелен на измерение бизнес-метрик</strong>.</p>
<p><strong>Users</strong> (Пользователи) — кто является пользователями твоего сервиса? Для разных отраслей это могут быть разные показатели: количество активных пользователей, посетителей на сайте, звонков в колл-центр, покупателей в час.</p>
<p><strong>Conversions</strong> (Конверсии) — это про то, как пользователь приносит доход твоему сервису. Купил подписку? Забронировал номер в отеле? Оформил заказ? Это всё конверсии.</p>
<p><strong>Activity</strong> (Активность) — показывает, что пользователи <strong>продолжают</strong> пользоваться твоим продуктом. Входы на сайт, просмотры товаров, добавления в корзину, время на сайте.</p>
<p>UCA позволяет собирать метрики, которые показывают, <strong>как бизнес себя чувствует</strong>. И отталкиваясь от этих метрик, ты уже можешь искать проблемы на уровне USE и RED — понимать, из-за ухудшения каких показателей стоит поднимать инженеров.</p>
<h2 id="какие-метрики-собирать-на-практике">Какие метрики собирать на практике</h2>
<p>Давай конкретнее. Вот список метрик, которые стоит отслеживать на разных уровнях:</p>
<p><strong>Мониторинг бизнес-метрик:</strong></p>
<ul>
<li>Сколько пользователей зарегистрировано в системе?</li>
<li>Сколько пользуется приложением раз в день/неделю/месяц (DAU/WAU/MAU)?</li>
<li>Какой процент людей после регистрации доходит до формы оплаты?</li>
<li>Сколько они платят (средний чек, медианный чек)?</li>
<li>Как люди пользуются услугами — какие фичи популярны, а какие нет?</li>
<li>Какова общая выручка и прибыль?</li>
</ul>
<p><strong>Мониторинг состояния приложений:</strong></p>
<ul>
<li>Количество запросов в единицу времени (RPS)</li>
<li>Количество активных пользователей в системе</li>
<li>Количество записей в БД (общее и новых в единицу времени)</li>
<li>Количество ошибок в сервисе</li>
<li>Время выполнения запросов или latency (50-й, 95-й, 99-й процентили)</li>
</ul>
<p><strong>Мониторинг оборудования:</strong></p>
<ul>
<li>Нагрузка на CPU</li>
<li>Свободное место в RAM</li>
<li>Свободное место на дисках</li>
<li>Нагрузка на сеть</li>
<li>IOPS на дисках</li>
<li>Количество запущенных процессов</li>
</ul>
<h2 id="инструменты-для-мониторинга-бизнес-метрик">Инструменты для мониторинга бизнес-метрик</h2>
<p>Теперь поговорим об инструментах. В идеальном мире у тебя должен быть стек, который покрывает все три уровня.</p>
<h3 id="для-бизнес-метрик-google-analytics-и-яндексметрика">Для бизнес-метрик: Google Analytics и Яндекс.Метрика</h3>
<p>Если речь идет о веб-сервисах, то <strong>Google Analytics</strong> и <strong>Яндекс.Метрика</strong> — это базовый минимум. Они позволяют отслеживать базовые переходы и конверсии готовыми средствами.</p>
<p>Яндекс.Метрика особенно хороша для российского рынка — она корректнее учитывает трафик из Яндекса, работает в соответствии с ФЗ-152, и у нее интуитивный интерфейс для русскоязычных пользователей. Google Analytics дает больше возможностей для крупных проектов с большими объемами данных (до 200 000 запросов в сутки против 5000 у Метрики).</p>
<p>Но если тебе нужно <strong>углубленное понимание</strong> — придется работать в связке с разработкой. Настроить сбор кастомных метрик, которые важны именно для твоего продукта.</p>
<h3 id="для-прикладных-метрик-prometheus--grafana">Для прикладных метрик: Prometheus + Grafana</h3>
<p><strong>Prometheus</strong> — это система сбора и хранения метрик временных рядов, которую изначально разработали в SoundCloud. Она стала стандартом де-факто для сбора метрик в современных инфраструктурах.</p>
<p><strong>Grafana</strong> — универсальная обертка для визуализации (<a href="/posts/install-grafana7/">как её поставить</a> — на пару абзацев дела). Она сама ничего не собирает и не хранит, но умеет красиво отображать данные из Prometheus, PostgreSQL, ClickHouse и еще десятков источников.</p>
<p>Связка <strong>Prometheus + Grafana</strong> позволяет собирать как технические метрики (через экспортеры — node_exporter для системных метрик, nginx-prometheus-exporter для Nginx и т.д.), так и <strong>бизнес-метрики</strong> твоего приложения. Да-да, Prometheus прекрасно справляется со сбором бизнес-метрик: выручка за день, количество активных пользователей, количество заказов — достаточно сделать свой endpoint <code>/metrics</code> в приложении и экспортировать туда нужные данные.</p>
<h3 id="для-операционной-аналитики-splunk">Для операционной аналитики: Splunk</h3>
<p>В НСПК для комплексного анализа используют <strong>Splunk</strong> — платформу операционной аналитики. Splunk умеет:</p>
<ul>
<li>Анализировать тысячи метрик в фоновом режиме с использованием статистических анализаторов</li>
<li>Обнаруживать аномалии и делать прогнозы с помощью машинного обучения</li>
<li>Создавать сложные параметризованные отчеты для дежурной службы</li>
<li>Коррелировать данные из разных источников</li>
</ul>
<p>Splunk — это уже не просто мониторинг, а <strong>платформа для принятия решений</strong>. Он позволяет не только видеть, что что-то пошло не так, но и понимать, <strong>почему</strong> это произошло, и <strong>как это влияет на бизнес</strong>.</p>
<h3 id="для-логирования-elk-stack">Для логирования: ELK Stack</h3>
<p><strong>ELK</strong> (Elasticsearch, Logstash, Kibana) или его альтернатива <strong>EFK</strong> (где вместо Logstash используется Fluentd) — это стек для агрегации и анализа логов.</p>
<p>Когда у тебя есть бизнес-метрики и прикладные метрики, логи становятся третьим измерением, которое помогает <strong>расследовать инциденты</strong>. Ты видишь на графике, что конверсия упала в 14:37, переходишь в логи и находишь, что именно в это время начались ошибки при работе с платежным шлюзом.</p>
<h2 id="как-выстраивать-систему-мониторинга">Как выстраивать систему мониторинга</h2>
<p>Теперь самое важное — <strong>как это всё организовать</strong>.</p>
<h3 id="строй-ресурсно-сервисную-модель">Строй ресурсно-сервисную модель</h3>
<p>Первый шаг — понять, что именно ты мониторишь. В НСПК разделяют данные на три слоя, а в большой компании нужна еще и <strong>ресурсно-сервисная модель</strong> (РСМ) — структурированное описание того, какие компоненты обеспечивают работу конкретного сервиса.</p>
<p>Например, сервис &ldquo;Интернет-магазин&rdquo; состоит из:</p>
<ul>
<li>Frontend (Nginx, React-приложение)</li>
<li>Backend API (несколько микросервисов на Java)</li>
<li>База данных (PostgreSQL)</li>
<li>Кэш (Redis)</li>
<li>Очередь сообщений (RabbitMQ)</li>
<li>Платежный шлюз (внешний API)</li>
</ul>
<p>Для каждого компонента ты настраиваешь сбор метрик на всех трех уровнях. И когда бизнес-метрика &ldquo;конверсия в оплату&rdquo; начинает падать, ты уже знаешь, на какие компоненты смотреть.</p>
<h3 id="используй-зонтичный-мониторинг">Используй &ldquo;зонтичный&rdquo; мониторинг</h3>
<p>Одна из лучших практик — использовать <strong>&ldquo;зонтик&rdquo;</strong>, единый слой, который агрегирует данные от различных систем: Zabbix, Prometheus, ELK, и т.д.. Это дает целостную картину и упрощает анализ.</p>
<p>Представь: у тебя Prometheus собирает метрики приложений, Zabbix мониторит железо, ELK собирает логи, а Google Analytics собирает поведенческие данные. Если у каждой команды свой дашборд в своем инструменте — <strong>корреляцию данных не увидеть</strong>. А вот если всё это агрегировано в одном месте (например, в Grafana или Splunk) — картина становится прозрачной.</p>
<h3 id="настраивай-алерты-правильно">Настраивай алерты правильно</h3>
<p><strong>Система оповещений</strong> — это компонент мониторинга, который выполняет действия на основе изменений метрик. Основная цель алертов — <strong>привлечь внимание человека</strong> к текущему состоянию системы.</p>
<p>Важные моменты:</p>
<ul>
<li>Алерт должен содержать информацию о том, <strong>что</strong> пошло не так и <strong>где</strong> найти дополнительную информацию</li>
<li>Не алерть на всё подряд — это приводит к &ldquo;усталости от алертов&rdquo;</li>
<li>Настраивай алерты сначала на <strong>бизнес-метрики</strong>, потом на прикладные, и только в конце — на инфраструктурные</li>
</ul>
<p>Например, алерт &ldquo;CPU загружен на 80%&rdquo; — это информационный сигнал. А вот &ldquo;Конверсия в оплату упала на 50% за последние 10 минут&rdquo; — это уже <strong>критический инцидент</strong>, который требует немедленного реагирования.</p>
<p>Разница между этими двумя строчками — это <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/symptom-vs-cause-alerting/" rel="noopener noreferrer" target="_blank">дисциплина «симптом, а не причина»</a>: будим человека на то, что чувствует пользователь, а метрики причин оставляем для отладки. А если шума уже накопилось столько, что дежурный смотрит мимо, — <a href="https://jtprogru.github.io/The-Way-of-SRE/engineering/alert-fatigue-management/" rel="noopener noreferrer" target="_blank">про разгребание усталости от алертов</a> есть отдельный лист.</p>
<h3 id="делай-метрики-прозрачными-для-всех">Делай метрики прозрачными для всех</h3>
<p>Один из ключевых принципов — <strong>все члены команды должны видеть одну и ту же информацию из одного источника</strong>. QA, разработчики, админы, аналитики, менеджеры продукта — все должны смотреть на одни и те же дашборды.</p>
<p>Когда у всех единая точка входа, начинает работать <strong>коллективный разум</strong>. Разработчик может заметить, что после его релиза упала конверсия. Админ увидит, что в момент падения бизнес-метрики начались таймауты при обращении к базе. Аналитик обратит внимание, что падение началось после того, как изменилась логика расчета скидок. Вместе они найдут причину быстрее.</p>
<h2 id="ошибки-которых-стоит-избегать">Ошибки, которых стоит избегать</h2>
<p>Давай поговорим о том, чего <strong>не надо</strong> делать.</p>
<h3 id="ошибка-1-отсутствие-связи-между-бизнес-задачами-и-аналитикой">Ошибка 1: Отсутствие связи между бизнес-задачами и аналитикой</h3>
<p>Самая частая проблема — когда ты собираешь метрики &ldquo;потому что так надо&rdquo;, но не понимаешь, <strong>зачем</strong> ты их собираешь.</p>
<p>Правильный подход: начинай с бизнес-целей. Бизнес хочет увеличить выручку на 20%? Отлично, декомпозируй это на метрики: нужно увеличить количество заказов или средний чек? Если заказы — то нужно больше пользователей или выше конверсия существующих? И так далее.</p>
<h3 id="ошибка-2-мониторинг-без-проверок-качества-данных">Ошибка 2: Мониторинг без проверок качества данных</h3>
<p>Представь: ты построил красивый дашборд, но данные в него приходят с ошибками. Счетчик установлен не на всех страницах. Конверсионные события дублируются. В результате твои метрики показывают 30% конверсии, а на самом деле она 3%.</p>
<p><strong>Проверяй качество данных</strong>: делай периодические проверки полноты и согласованности, тестируй бизнес-правила, отслеживай источники данных и трансформации.</p>
<h3 id="ошибка-3-плавающее-руководство-по-сбору-данных">Ошибка 3: &ldquo;Плавающее&rdquo; руководство по сбору данных</h3>
<p>Когда у тебя нет четкого стандарта, как называть метрики, как их собирать, и где их хранить — начинается хаос. Один разработчик называет метрику <code>orders_count</code>, другой — <code>order.count</code>, третий — <code>OrdersTotal</code>. В итоге ты не можешь нормально искать и агрегировать данные.</p>
<p><strong>Установи стандарты</strong>: naming conventions для метрик, формат лейблов, структуру дашбордов. И следи, чтобы все их соблюдали.</p>
<h3 id="ошибка-4-разрозненные-данные">Ошибка 4: Разрозненные данные</h3>
<p>Когда метрики инфраструктуры хранятся в Zabbix, прикладные — в Prometheus, бизнесовые — в Google Analytics, а логи — в ELK, и всё это <strong>никак не связано</strong> между собой — ты не можешь проводить корреляцию.</p>
<p><strong>Стремись к единой точке анализа</strong>: используй зонтичный мониторинг, настраивай интеграции между системами, делай так, чтобы все данные были доступны в одном месте.</p>
<h3 id="ошибка-5-исправление-ошибок-по-факту">Ошибка 5: Исправление ошибок &ldquo;по факту&rdquo;</h3>
<p>Если у тебя нет мониторинга, ты узнаешь о проблемах только тогда, когда <strong>клиенты начинают жаловаться</strong>. И это очень дорого — и в деньгах, и в репутации.</p>
<p>Мониторинг — это <strong>профилактика</strong>. Гораздо дешевле обнаружить проблему на графике и исправить её до того, как она повлияла на пользователей, чем тушить пожар, когда половина клиентов уже не может сделать заказ.</p>
<h2 id="практический-чек-лист">Практический чек-лист</h2>
<p>Если ты хочешь построить нормальную систему мониторинга бизнес-метрик, вот тебе чек-лист для самопроверки:</p>
<p><strong>Базовый уровень:</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Определен список основных бизнес-метрик (выручка, количество заказов, конверсии)</li>
<li><input disabled="" type="checkbox"> Бизнес-метрики декомпозированы от уровня руководства до команд</li>
<li><input disabled="" type="checkbox"> Определены сопутствующие метрики (MAU, количество товаров в чеке и т.д.)</li>
<li><input disabled="" type="checkbox"> Определен список прикладных метрик (RPS, latency, error rate)</li>
<li><input disabled="" type="checkbox"> Для каждой метрики определен период обратной связи (как часто проверять)</li>
</ul>
<p><strong>Продвинутый уровень:</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Определены и автоматизированы источники данных по всем метрикам</li>
<li><input disabled="" type="checkbox"> Определены события/встречи, на которых обсуждаются метрики</li>
<li><input disabled="" type="checkbox"> Планирование работы осуществляется на основании метрик</li>
<li><input disabled="" type="checkbox"> Для каждой задачи есть понимание, на какую метрику она влияет</li>
<li><input disabled="" type="checkbox"> Все заинтересованные лица имеют доступ к метрикам</li>
<li><input disabled="" type="checkbox"> Есть система алертинга на критические изменения метрик</li>
</ul>
<p><strong>Экспертный уровень:</strong></p>
<ul>
<li><input disabled="" type="checkbox"> Используется зонтичный мониторинг, агрегирующий данные из всех систем</li>
<li><input disabled="" type="checkbox"> Настроена корреляция между метриками разных уровней</li>
<li><input disabled="" type="checkbox"> Внедрены проверки качества данных</li>
<li><input disabled="" type="checkbox"> Существует документированный стандарт сбора и именования метрик</li>
<li><input disabled="" type="checkbox"> Метрики используются для принятия бизнес-решений</li>
</ul>
<h2 id="выводы">Выводы</h2>
<p>Мониторинг бизнес-метрик — это не просто &ldquo;давайте нарисуем еще один дашборд в Grafana&rdquo;. Это <strong>комплексный подход</strong> к пониманию того, как работает твой продукт с точки зрения бизнеса.</p>
<p>Ключевые мысли:</p>
<ol>
<li><strong>Мониторь сверху вниз</strong>: начинай с бизнес-метрик, потом прикладные, и только в конце инфраструктурные</li>
<li><strong>Используй правильные методологии</strong>: USE для ресурсов, RED для сервисов, UCA для бизнеса</li>
<li><strong>Стремись к единой точке анализа</strong>: все команды должны смотреть на одни и те же данные</li>
<li><strong>Автоматизируй сбор данных</strong>: ручной сбор метрик — это прошлый век</li>
<li><strong>Делай метрики основой для принятия решений</strong>: не просто смотри на графики, а делай выводы и действуй — как именно этот разговор ведётся с бизнесом, разбирал в <a href="/posts/reliability-is-a-conversation/">«Надёжность строится в диалоге с бизнесом»</a></li>
</ol>
<p>Управление проектом без метрик — это как плавание ночью на корабле без карты и навигации. Ты вроде бы куда-то плывешь, но рано или поздно это закончится плохо.</p>
<p>А с правильно настроенным мониторингом бизнес-метрик ты <strong>всегда знаешь</strong>, где ты находишься, куда движешься, и что нужно исправить, чтобы прийти к цели.</p>
<h3 id="полезные-ссылки">Полезные ссылки</h3>
<ul>
<li><a href="https://habr.com/ru/company/nspk/blog/523352/" rel="noopener noreferrer" target="_blank">Как устроен прикладной и бизнес-мониторинг сервисов НСПК</a> — про трехслойную модель мониторинга</li>
<li><a href="https://logrocon.ru/news/monitoring" rel="noopener noreferrer" target="_blank">Основы мониторинга и сбора метрик</a> — про методологии USE, RED, UCA</li>
<li><a href="https://otus.ru/nest/post/1988/" rel="noopener noreferrer" target="_blank">Мониторинг событий и бизнес-метрик</a> — про важность метрик для бизнеса</li>
<li><a href="https://vc.ru/u/744996-evgeniy-ivanov/216307-pro-biznes-i-metriki" rel="noopener noreferrer" target="_blank">Про бизнес и метрики</a> — про структуру и типы метрик</li>
</ul>
</div>]]></content:encoded>
    </item><item>
      <title>Запуск Kali Linux в UTM на macOS под ARM</title>
      <link>https://jtprog.ru/posts/kali-apple-utm/</link>
      <pubDate>Thu, 18 Dec 2025 06:20:34 +0300</pubDate>
      <dc:creator>jtprogru</dc:creator>
      <guid isPermaLink="true">https://jtprog.ru/posts/kali-apple-utm/</guid>
      <category>DevOps</category>
      <category>SRE</category>
      <category>Security</category>
      <category>Virtualization</category>
      <category>MacOS</category>
      <category>Hardware</category>
      <category>Виртуализация</category>
      <category>Безопасность</category>
      <category>Linux</category>
      <category>Howto</category>
      <category domain="category">Howto</category>
      <enclosure url="https://jtprog.ru/howto.png" type="image/jpeg" length="0" />
      <media:thumbnail url="https://jtprog.ru/howto.png" /><description><![CDATA[<div><p>Привет, <code>%username%</code>! Сегодня я расскажу тебе про то, как можно развернуть Kali Linux на маках с процессорами Apple Silicon (M1/M2/M3), используя бесплатную альтернативу Parallels Desktop — виртуализатор <a href="https://mac.getutm.app/" rel="noopener noreferrer" target="_blank">UTM</a>. И да, это не только для пентестеров, но и для всех, кому нужна тестовая среда на базе Linux.</p>
<h2 id="зачем-это-вообще-нужно">Зачем это вообще нужно?</h2>
<p>Представь, что тебе нужно проверить, как твой сервис ведет себя в определенном окружении. Или протестировать Ansible-плейбуки на свежей системе. Или проверить безопасность своей инфраструктуры с помощью инструментов пентеста. В таких случаях виртуальная машина — это лучший друг.</p>
<p>Kali Linux — это не просто дистрибутив для хакеров из голливудских фильмов. Это полноценная платформа для аудита безопасности, в которой из коробки доступны инструменты вроде Nmap, Metasploit, Burp Suite и Wireshark. Для DevOps и SRE это полезный набор для проверки защищенности инфраструктуры, тестирования сетевой безопасности и поиска уязвимостей еще до того, как их найдут настоящие злоумышленники.</p>
<h2 id="почему-utm-а-не-parallels">Почему UTM, а не Parallels?</h2>
<p>Давай сразу расставим точки над Ё. Parallels Desktop — это крутой и удобный инструмент с отличной интеграцией с macOS, поддержкой 3D-ускорения и простым мастером установки. Но есть одна проблема: за него нужно платить около 100 долларов в год.</p>
<p>UTM — это open-source решение на базе QEMU, которое использует фреймворк Apple Hypervisor для виртуализации ARM-систем на скоростях, близких к нативным. Да, UTM требует больше ручных настроек, чем Parallels, и не имеет такой же бесшовной интеграции с macOS. Но зато он бесплатный, активно развивается и отлично справляется с задачами для разработки, тестирования и обучения.</p>
<p>Вот краткое сравнение:</p>
<p><strong>Parallels Desktop</strong>: автоматическая установка, отличная производительность, профессиональная поддержка, но стоит денег.</p>
<p><strong>UTM</strong>: бесплатный, открытый исходный код, активное комьюнити, хорошая производительность для ARM-систем, но требует больше ручной настройки.</p>
<p>Для большинства задач DevOps и SRE, особенно если речь идет про эксперименты, тестовые стенды или обучение, UTM — это более чем достаточно.</p>
<h2 id="что-нам-понадобится">Что нам понадобится?</h2>
<p>Чтобы все заработало, тебе нужно:</p>
<ol>
<li>Mac с процессором Apple Silicon (M1/M2/M3)</li>
<li>Скачанный UTM с официального сайта</li>
<li>Installer-образ Kali Linux для ARM64 (именно ARM, не x86!)</li>
</ol>
<p>Почему именно installer-образ? Потому что он позволит тебе установить систему с нуля на виртуальный диск, настроить все под себя и получить полноценную рабочую среду.</p>
<h2 id="шаг-1-устанавливаем-utm">Шаг 1: Устанавливаем UTM</h2>
<p>Заходишь на официальный сайт UTM (mac.getutm.app), скачиваешь .dmg-файл, открываешь его и перетаскиваешь UTM в папку &ldquo;Приложения&rdquo;. Все как обычно для macOS — никаких подводных камней.</p>
<h2 id="шаг-2-скачиваем-образ-kali-linux">Шаг 2: Скачиваем образ Kali Linux</h2>
<p>Идешь на официальный сайт Kali Linux и качаешь installer-образ для Apple M1/M2 (ARM64). Обрати внимание: нужен именно ISO-файл для установки, а не готовый образ виртуальной машины. Рекомендую качать через торрент — это быстрее и надежнее.</p>
<h2 id="шаг-3-создаем-виртуальную-машину">Шаг 3: Создаем виртуальную машину</h2>
<p>Запускаешь UTM и жмешь &ldquo;Create VM&rdquo;. Дальше важно: выбираешь <strong>&ldquo;Virtualize&rdquo;</strong>, а не &ldquo;Emulate&rdquo;. Виртуализация использует нативные инструкции ARM-процессора через Apple Hypervisor, что дает тебе максимальную производительность. Эмуляция нужна только если ты хочешь запустить x86-систему на ARM, но это будет работать медленно.</p>
<p>На следующем экране выбираешь <strong>&ldquo;Other&rdquo;</strong> в качестве типа операционной системы.</p>
<p>Теперь самое важное: на странице настроек нажимаешь &ldquo;Boot ISO Image&rdquo; и выбираешь скачанный ранее ISO-файл Kali Linux. Размер диска можешь оставить по умолчанию или увеличить в зависимости от того, сколько места планируешь использовать — для базовой установки с основными инструментами хватит 20-30 GB.</p>
<h2 id="шаг-4-важный-хак--serial-режим">Шаг 4: Важный хак — Serial-режим</h2>
<p>Вот тут начинается интересное. Из-за бага в текущих версиях UTM, установку Kali нужно проводить в console-only режиме через Serial-устройство. Звучит страшно, но на деле это просто:</p>
<ol>
<li>После создания VM переходишь в её настройки</li>
<li>В разделе &ldquo;Devices&rdquo; добавляешь устройство <strong>&ldquo;Serial&rdquo;</strong></li>
<li>Сохраняешь и запускаешь VM</li>
</ol>
<p>Когда VM загрузится, в меню установщика выбираешь <strong>&ldquo;Install&rdquo;</strong> (не Graphical Install!). Дальше процесс установки стандартный: выбираешь язык, страну, настраиваешь сеть, создаешь пользователя и пароль.</p>
<h2 id="шаг-5-установка-kali-linux">Шаг 5: Установка Kali Linux</h2>
<p>Процесс установки довольно стандартный для Debian-based систем:</p>
<ol>
<li>Выбираешь язык интерфейса (можешь выбрать русский или оставить английский)</li>
<li>Настраиваешь сеть — UTM автоматически создаст NAT-подключение</li>
<li>Задаешь hostname для своей виртуалки</li>
<li>Создаешь пользователя и пароль</li>
<li>На этапе разметки диска выбираешь <strong>&ldquo;Guided — Use entire disk&rdquo;</strong> и <strong>&ldquo;All files in one partition&rdquo;</strong></li>
<li>Выбираешь инструменты для установки — можешь установить полный набор или только базовую систему</li>
</ol>
<p>После завершения установки перезагружаешь VM.</p>
<h2 id="шаг-6-финальная-настройка">Шаг 6: Финальная настройка</h2>
<p>После перезагрузки ты увидишь, что установщик снова предлагает установить Kali. Это нормально — просто нужно удалить ISO-образ из виртуального CD-привода.</p>
<p>Выключаешь VM и делаешь следующее:</p>
<ol>
<li>Переходишь в настройки VM</li>
<li>В разделе &ldquo;Drives&rdquo; удаляешь ISO-образ установщика</li>
<li>В разделе &ldquo;Devices&rdquo; удаляешь Serial-устройство (правый клик → Remove)</li>
<li>В разделе &ldquo;Display&rdquo; меняешь эмуляцию видеокарты на <strong>virtio-gpu-pci</strong></li>
</ol>
<p>Эти изменения нужны для того, чтобы переключиться с консольного режима установки на полноценный графический интерфейс.</p>
<h2 id="шаг-7-устанавливаем-spice-guest-tools">Шаг 7: Устанавливаем SPICE Guest Tools</h2>
<p>Чтобы все работало максимально комфортно — общий буфер обмена, автоматическое изменение разрешения экрана и лучшая производительность — нужно установить SPICE-инструменты.</p>
<p>Запускаешь VM, логинишься и выполняешь в терминале:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update <span class="o">&amp;&amp;</span> sudo apt install spice-vdagent spice-webdavd -y</span></span></code></pre></div>
</div>
<p>После установки можешь настроить оптимальное разрешение экрана — рекомендуется 1400×900 для большинства макбуков.</p>
<h2 id="что-дальше">Что дальше?</h2>
<p>Теперь у тебя есть полноценная виртуальная машина с Kali Linux, которая работает практически на нативной скорости благодаря ARM-виртуализации. Что с ней делать?</p>
<p><strong>Для DevOps/SRE:</strong></p>
<ul>
<li>Тестировать Ansible-плейбуки на чистой системе</li>
<li>Проверять безопасность своей инфраструктуры с помощью Nmap и Metasploit</li>
<li>Создавать изолированные тестовые среды для экспериментов</li>
<li>Анализировать сетевой трафик с помощью Wireshark и tcpdump</li>
<li>Проводить аудит безопасности веб-приложений через Burp Suite</li>
</ul>
<p><strong>Для инженеров эксплуатации:</strong></p>
<ul>
<li>Отлаживать проблемы с сетью</li>
<li>Тестировать конфигурации служб в изолированной среде</li>
<li>Проводить симуляцию инцидентов безопасности</li>
</ul>
<h2 id="бонус-снапшоты-и-клонирование">Бонус: снапшоты и клонирование</h2>
<p>UTM поддерживает создание снапшотов виртуальных машин. Это невероятно удобно: настроил идеальное окружение один раз, сделал снапшот, и можешь откатываться к нему в любой момент. Экспериментируешь с новой конфигурацией? Сделал снапшот. Что-то сломалось? Откатился за пару секунд.</p>
<p>Также можешь клонировать VM для создания нескольких изолированных окружений — например, для тестирования кластерных решений или распределенных систем.</p>
<h2 id="альтернативы-и-сравнение">Альтернативы и сравнение</h2>
<p>Конечно, UTM — не единственный вариант для виртуализации на Mac. Вот краткое сравнение популярных решений:</p>
<p><strong>Parallels Desktop</strong>: лучшая интеграция с macOS, автоматическая установка, отличная производительность, но платный.</p>
<p><strong>VMware Fusion</strong>: есть бесплатная версия Player, хорошая производительность, но интерфейс не такой удобный как у Parallels.</p>
<p><strong>UTM</strong>: бесплатный, open-source, отличная производительность для ARM, активное комьюнити, но требует больше ручной настройки.</p>
<p><strong>VirtualBox</strong>: классика виртуализации, но плохо работает на Apple Silicon и требует эмуляции.</p>
<p>Для большинства задач DevOps и SRE, особенно если речь про обучение, эксперименты и разработку, UTM — это оптимальный выбор по соотношению цена/качество.</p>
<h2 id="возможные-проблемы-и-их-решения">Возможные проблемы и их решения</h2>
<p><strong>Черный экран после выбора режима установки</strong>: это известная проблема, решается использованием Serial-режима, как описано выше.</p>
<p><strong>Не работает общий буфер обмена</strong>: убедись, что установил <code>spice-vdagent</code> и <code>spice-webdavd</code>, и включил Clipboard Sharing в настройках VM.</p>
<p><strong>Низкая производительность</strong>: проверь, что выбрал &ldquo;Virtualize&rdquo;, а не &ldquo;Emulate&rdquo; при создании VM, и выделил достаточно RAM (минимум 3-4 GB).</p>
<p><strong>Проблемы с сетью</strong>: UTM автоматически создает NAT-подключение, но если что-то не работает, можешь попробовать режим Bridge в настройках сети.</p>
<h2 id="итоги">Итоги</h2>
<p>Развернуть Kali Linux на Mac с Apple Silicon через UTM — это вполне реальная задача, которая не требует покупки дорогих коммерческих решений. Да, придется немного повозиться с настройками, но результат того стоит: ты получаешь полноценную виртуальную машину с отличной производительностью и всеми необходимыми инструментами для тестирования, разработки и аудита безопасности.</p>
<p>UTM — это отличная демонстрация того, как open-source решения могут конкурировать с коммерческими продуктами, особенно в руках тех, кто не боится немного покопаться в настройках. Для DevOps и SRE это особенно актуально: мы привыкли читать YAML и bash-скрипты, разбираться в документации и решать проблемы нестандартными способами.</p>
<p>Так что если у тебя есть Mac на Apple Silicon и нужна виртуалка с Linux — смело пробуй UTM — это работает, и это open-source.</p>
<p>Только держи в голове, что виртуалка с Kali — это инструмент, а не практика. Чтобы находки превращались в починенные CVE, а не в PDF-отчёт, нужен процесс: обнаружение, триаж, починка, проверка. <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/vulnerability-management/" rel="noopener noreferrer" target="_blank">Про него — лист про vulnerability management</a>.</p>
</div>]]></description>
      <content:encoded><![CDATA[<style>/* Inline-CSS, который тема вкладывает в <description> каждого item'а
   RSS-фида (layouts/index.rss.xml). RSS-ридеры (Feedly, Inoreader,
   NetNewsWire) уважают <style> внутри description с переменной
   степенью энтузиазма — это базовый fallback, достаточный для читаемой
   типографики, не более.

   Цвета продублированы литералами из светлой палитры дизайн-системы:
   CSS-переменные тут не работают, ридер получает голый фрагмент без токенов.
   Единственное место темы, где хекс написан руками, — поправь синхронно, если
   в mishka-ds изменятся --bg / --fg / --accent / --border. */

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #4c4f69;
  max-width: 720px;
  margin: 0 auto;
  padding: 1em;
}
h1, h2, h3 { line-height: 1.25; margin-top: 1.4em; }
a { color: #0b7285; }
pre {
  background: #24273a;
  color: #cad3f5;
  padding: 1em;
  border-radius: 6px;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
}
code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: #dce0e8;
  padding: 0.1em 0.4em;
  border-radius: 4px;
}
pre code { background: transparent; padding: 0; }
blockquote {
  border-left: 3px solid #0b7285;
  padding-left: 1em;
  color: #6c6f85;
  font-style: italic;
  margin-left: 0;
}
img { max-width: 100%; height: auto; border-radius: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccd0da; padding: 6px 10px; text-align: left; }
</style><div><p>Привет, <code>%username%</code>! Сегодня я расскажу тебе про то, как можно развернуть Kali Linux на маках с процессорами Apple Silicon (M1/M2/M3), используя бесплатную альтернативу Parallels Desktop — виртуализатор <a href="https://mac.getutm.app/" rel="noopener noreferrer" target="_blank">UTM</a>. И да, это не только для пентестеров, но и для всех, кому нужна тестовая среда на базе Linux.</p>
<h2 id="зачем-это-вообще-нужно">Зачем это вообще нужно?</h2>
<p>Представь, что тебе нужно проверить, как твой сервис ведет себя в определенном окружении. Или протестировать Ansible-плейбуки на свежей системе. Или проверить безопасность своей инфраструктуры с помощью инструментов пентеста. В таких случаях виртуальная машина — это лучший друг.</p>
<p>Kali Linux — это не просто дистрибутив для хакеров из голливудских фильмов. Это полноценная платформа для аудита безопасности, в которой из коробки доступны инструменты вроде Nmap, Metasploit, Burp Suite и Wireshark. Для DevOps и SRE это полезный набор для проверки защищенности инфраструктуры, тестирования сетевой безопасности и поиска уязвимостей еще до того, как их найдут настоящие злоумышленники.</p>
<h2 id="почему-utm-а-не-parallels">Почему UTM, а не Parallels?</h2>
<p>Давай сразу расставим точки над Ё. Parallels Desktop — это крутой и удобный инструмент с отличной интеграцией с macOS, поддержкой 3D-ускорения и простым мастером установки. Но есть одна проблема: за него нужно платить около 100 долларов в год.</p>
<p>UTM — это open-source решение на базе QEMU, которое использует фреймворк Apple Hypervisor для виртуализации ARM-систем на скоростях, близких к нативным. Да, UTM требует больше ручных настроек, чем Parallels, и не имеет такой же бесшовной интеграции с macOS. Но зато он бесплатный, активно развивается и отлично справляется с задачами для разработки, тестирования и обучения.</p>
<p>Вот краткое сравнение:</p>
<p><strong>Parallels Desktop</strong>: автоматическая установка, отличная производительность, профессиональная поддержка, но стоит денег.</p>
<p><strong>UTM</strong>: бесплатный, открытый исходный код, активное комьюнити, хорошая производительность для ARM-систем, но требует больше ручной настройки.</p>
<p>Для большинства задач DevOps и SRE, особенно если речь идет про эксперименты, тестовые стенды или обучение, UTM — это более чем достаточно.</p>
<h2 id="что-нам-понадобится">Что нам понадобится?</h2>
<p>Чтобы все заработало, тебе нужно:</p>
<ol>
<li>Mac с процессором Apple Silicon (M1/M2/M3)</li>
<li>Скачанный UTM с официального сайта</li>
<li>Installer-образ Kali Linux для ARM64 (именно ARM, не x86!)</li>
</ol>
<p>Почему именно installer-образ? Потому что он позволит тебе установить систему с нуля на виртуальный диск, настроить все под себя и получить полноценную рабочую среду.</p>
<h2 id="шаг-1-устанавливаем-utm">Шаг 1: Устанавливаем UTM</h2>
<p>Заходишь на официальный сайт UTM (mac.getutm.app), скачиваешь .dmg-файл, открываешь его и перетаскиваешь UTM в папку &ldquo;Приложения&rdquo;. Все как обычно для macOS — никаких подводных камней.</p>
<h2 id="шаг-2-скачиваем-образ-kali-linux">Шаг 2: Скачиваем образ Kali Linux</h2>
<p>Идешь на официальный сайт Kali Linux и качаешь installer-образ для Apple M1/M2 (ARM64). Обрати внимание: нужен именно ISO-файл для установки, а не готовый образ виртуальной машины. Рекомендую качать через торрент — это быстрее и надежнее.</p>
<h2 id="шаг-3-создаем-виртуальную-машину">Шаг 3: Создаем виртуальную машину</h2>
<p>Запускаешь UTM и жмешь &ldquo;Create VM&rdquo;. Дальше важно: выбираешь <strong>&ldquo;Virtualize&rdquo;</strong>, а не &ldquo;Emulate&rdquo;. Виртуализация использует нативные инструкции ARM-процессора через Apple Hypervisor, что дает тебе максимальную производительность. Эмуляция нужна только если ты хочешь запустить x86-систему на ARM, но это будет работать медленно.</p>
<p>На следующем экране выбираешь <strong>&ldquo;Other&rdquo;</strong> в качестве типа операционной системы.</p>
<p>Теперь самое важное: на странице настроек нажимаешь &ldquo;Boot ISO Image&rdquo; и выбираешь скачанный ранее ISO-файл Kali Linux. Размер диска можешь оставить по умолчанию или увеличить в зависимости от того, сколько места планируешь использовать — для базовой установки с основными инструментами хватит 20-30 GB.</p>
<h2 id="шаг-4-важный-хак--serial-режим">Шаг 4: Важный хак — Serial-режим</h2>
<p>Вот тут начинается интересное. Из-за бага в текущих версиях UTM, установку Kali нужно проводить в console-only режиме через Serial-устройство. Звучит страшно, но на деле это просто:</p>
<ol>
<li>После создания VM переходишь в её настройки</li>
<li>В разделе &ldquo;Devices&rdquo; добавляешь устройство <strong>&ldquo;Serial&rdquo;</strong></li>
<li>Сохраняешь и запускаешь VM</li>
</ol>
<p>Когда VM загрузится, в меню установщика выбираешь <strong>&ldquo;Install&rdquo;</strong> (не Graphical Install!). Дальше процесс установки стандартный: выбираешь язык, страну, настраиваешь сеть, создаешь пользователя и пароль.</p>
<h2 id="шаг-5-установка-kali-linux">Шаг 5: Установка Kali Linux</h2>
<p>Процесс установки довольно стандартный для Debian-based систем:</p>
<ol>
<li>Выбираешь язык интерфейса (можешь выбрать русский или оставить английский)</li>
<li>Настраиваешь сеть — UTM автоматически создаст NAT-подключение</li>
<li>Задаешь hostname для своей виртуалки</li>
<li>Создаешь пользователя и пароль</li>
<li>На этапе разметки диска выбираешь <strong>&ldquo;Guided — Use entire disk&rdquo;</strong> и <strong>&ldquo;All files in one partition&rdquo;</strong></li>
<li>Выбираешь инструменты для установки — можешь установить полный набор или только базовую систему</li>
</ol>
<p>После завершения установки перезагружаешь VM.</p>
<h2 id="шаг-6-финальная-настройка">Шаг 6: Финальная настройка</h2>
<p>После перезагрузки ты увидишь, что установщик снова предлагает установить Kali. Это нормально — просто нужно удалить ISO-образ из виртуального CD-привода.</p>
<p>Выключаешь VM и делаешь следующее:</p>
<ol>
<li>Переходишь в настройки VM</li>
<li>В разделе &ldquo;Drives&rdquo; удаляешь ISO-образ установщика</li>
<li>В разделе &ldquo;Devices&rdquo; удаляешь Serial-устройство (правый клик → Remove)</li>
<li>В разделе &ldquo;Display&rdquo; меняешь эмуляцию видеокарты на <strong>virtio-gpu-pci</strong></li>
</ol>
<p>Эти изменения нужны для того, чтобы переключиться с консольного режима установки на полноценный графический интерфейс.</p>
<h2 id="шаг-7-устанавливаем-spice-guest-tools">Шаг 7: Устанавливаем SPICE Guest Tools</h2>
<p>Чтобы все работало максимально комфортно — общий буфер обмена, автоматическое изменение разрешения экрана и лучшая производительность — нужно установить SPICE-инструменты.</p>
<p>Запускаешь VM, логинишься и выполняешь в терминале:</p>
<div class="codeblock codeblock--bash" data-lang="bash"><span class="codeblock__lang" aria-hidden="true">bash</span><button class="codeblock__copy" type="button" data-copy aria-label="Скопировать" title="Скопировать"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update <span class="o">&amp;&amp;</span> sudo apt install spice-vdagent spice-webdavd -y</span></span></code></pre></div>
</div>
<p>После установки можешь настроить оптимальное разрешение экрана — рекомендуется 1400×900 для большинства макбуков.</p>
<h2 id="что-дальше">Что дальше?</h2>
<p>Теперь у тебя есть полноценная виртуальная машина с Kali Linux, которая работает практически на нативной скорости благодаря ARM-виртуализации. Что с ней делать?</p>
<p><strong>Для DevOps/SRE:</strong></p>
<ul>
<li>Тестировать Ansible-плейбуки на чистой системе</li>
<li>Проверять безопасность своей инфраструктуры с помощью Nmap и Metasploit</li>
<li>Создавать изолированные тестовые среды для экспериментов</li>
<li>Анализировать сетевой трафик с помощью Wireshark и tcpdump</li>
<li>Проводить аудит безопасности веб-приложений через Burp Suite</li>
</ul>
<p><strong>Для инженеров эксплуатации:</strong></p>
<ul>
<li>Отлаживать проблемы с сетью</li>
<li>Тестировать конфигурации служб в изолированной среде</li>
<li>Проводить симуляцию инцидентов безопасности</li>
</ul>
<h2 id="бонус-снапшоты-и-клонирование">Бонус: снапшоты и клонирование</h2>
<p>UTM поддерживает создание снапшотов виртуальных машин. Это невероятно удобно: настроил идеальное окружение один раз, сделал снапшот, и можешь откатываться к нему в любой момент. Экспериментируешь с новой конфигурацией? Сделал снапшот. Что-то сломалось? Откатился за пару секунд.</p>
<p>Также можешь клонировать VM для создания нескольких изолированных окружений — например, для тестирования кластерных решений или распределенных систем.</p>
<h2 id="альтернативы-и-сравнение">Альтернативы и сравнение</h2>
<p>Конечно, UTM — не единственный вариант для виртуализации на Mac. Вот краткое сравнение популярных решений:</p>
<p><strong>Parallels Desktop</strong>: лучшая интеграция с macOS, автоматическая установка, отличная производительность, но платный.</p>
<p><strong>VMware Fusion</strong>: есть бесплатная версия Player, хорошая производительность, но интерфейс не такой удобный как у Parallels.</p>
<p><strong>UTM</strong>: бесплатный, open-source, отличная производительность для ARM, активное комьюнити, но требует больше ручной настройки.</p>
<p><strong>VirtualBox</strong>: классика виртуализации, но плохо работает на Apple Silicon и требует эмуляции.</p>
<p>Для большинства задач DevOps и SRE, особенно если речь про обучение, эксперименты и разработку, UTM — это оптимальный выбор по соотношению цена/качество.</p>
<h2 id="возможные-проблемы-и-их-решения">Возможные проблемы и их решения</h2>
<p><strong>Черный экран после выбора режима установки</strong>: это известная проблема, решается использованием Serial-режима, как описано выше.</p>
<p><strong>Не работает общий буфер обмена</strong>: убедись, что установил <code>spice-vdagent</code> и <code>spice-webdavd</code>, и включил Clipboard Sharing в настройках VM.</p>
<p><strong>Низкая производительность</strong>: проверь, что выбрал &ldquo;Virtualize&rdquo;, а не &ldquo;Emulate&rdquo; при создании VM, и выделил достаточно RAM (минимум 3-4 GB).</p>
<p><strong>Проблемы с сетью</strong>: UTM автоматически создает NAT-подключение, но если что-то не работает, можешь попробовать режим Bridge в настройках сети.</p>
<h2 id="итоги">Итоги</h2>
<p>Развернуть Kali Linux на Mac с Apple Silicon через UTM — это вполне реальная задача, которая не требует покупки дорогих коммерческих решений. Да, придется немного повозиться с настройками, но результат того стоит: ты получаешь полноценную виртуальную машину с отличной производительностью и всеми необходимыми инструментами для тестирования, разработки и аудита безопасности.</p>
<p>UTM — это отличная демонстрация того, как open-source решения могут конкурировать с коммерческими продуктами, особенно в руках тех, кто не боится немного покопаться в настройках. Для DevOps и SRE это особенно актуально: мы привыкли читать YAML и bash-скрипты, разбираться в документации и решать проблемы нестандартными способами.</p>
<p>Так что если у тебя есть Mac на Apple Silicon и нужна виртуалка с Linux — смело пробуй UTM — это работает, и это open-source.</p>
<p>Только держи в голове, что виртуалка с Kali — это инструмент, а не практика. Чтобы находки превращались в починенные CVE, а не в PDF-отчёт, нужен процесс: обнаружение, триаж, починка, проверка. <a href="https://jtprogru.github.io/The-Way-of-SRE/practices/vulnerability-management/" rel="noopener noreferrer" target="_blank">Про него — лист про vulnerability management</a>.</p>
</div>]]></content:encoded>
    </item>
  </channel>
</rss>
