Files

8 lines
22 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 38,
"slug": "editorial-2026-12-mechanism-portfolio-case",
"title": "Инженерный кейс: как проверить причинность, а не приписать результат изменению",
"excerpt": "Если после изменения стало лучше, это ещё не доказывает причинность. Разбираем, как отделить событие от наблюдения, проверить механизм и остановить вывод, когда данных недостаточно.",
"contentHtml": "<p>После релиза команда видит знакомый симптом: задержка ответа снизилась, ошибок стало меньше, а в отчёте появляется фраза «изменение дало результат». Через неделю показатель снова меняется. Уже непонятно, помог релиз, закончилась нагрузка, изменился состав трафика или перестал отвечать другой компонент. Цена ошибки — неверное решение на следующем шаге: команда закрепит бесполезный код, отменит полезный откат или объявит временный эффект доказанным.</p><p>Разберём учебный кейс с кешем страницы каталога. Его цель — не показать красивый процент улучшения, а дать маршрут, который можно повторить на своём сервисе. Причинное утверждение требует цепочки «изменение → механизм → наблюдение → сопоставимое сравнение → ограниченный вывод». Если одно звено отсутствует, корректный результат — ослабить формулировку или остановить проверку.</p>\n<h2>Почему «после» не значит «из-за»</h2>\n<p>Событие — действие, которое действительно произошло: сервис начал читать ответ из локального кеша. Наблюдение — запись измерения: время ответа, число ошибок, трасса запроса или лог промаха кеша. Вывод — утверждение о связи между событием и наблюдением. Эти слои нельзя подменять друг другом.</p>\n<p>Фраза «после включения кеша p95 снизился» описывает последовательность. Фраза «кеш снизил p95» утверждает причинность. Для неё нужно знать маршрут, окно, класс нагрузки, число запросов и альтернативные изменения. Один график latency показывает разницу, но не объясняет, почему она появилась.</p>\n<p>OpenTelemetry называет traces, metrics и logs сигналами системы и описывает их как разные углы наблюдения за одной активностью. Поэтому в кейсе полезно разделить два вопроса: какой сигнал подтверждает работу механизма и какой сигнал показывает эффект для пользователя. Метрика p95 без признаков чтения из кеша оставляет несколько равно правдоподобных объяснений.</p>\n<figure><img src=\"/assets/editorial/2026/systems-performance-2026-load-latency-curve.svg\" alt=\"График показывает, почему сравнение требует одной фиксированной границы: разная нагрузка сама по себе не доказывает ускорение\" loading=\"lazy\" /><figcaption>Иллюстрация фиксирует границу сравнения. Разные нагрузка и входы могут изменить latency без причинного эффекта кеша.</figcaption></figure>\n<h2>Сформулируйте механизм до просмотра результата</h2>\n<p>Начните с одного изменения. В примере это кеширование ответа каталога на пять минут для повторных запросов с одинаковым ключом. Механизм должен быть коротким и проверяемым: запрос с тем же ключом читает локальное значение, не вызывает upstream и возвращает ответ без ожидания сети. Ветка промаха продолжает обращаться к upstream.</p>\n<p>Из механизма следуют два наблюдения. Во-первых, после изменения должны появиться cache hits для подходящих ключей. Во-вторых, число upstream-вызовов на сопоставимом наборе запросов должно уменьшиться. Только затем смотрим на p95, ошибки и стоимость памяти. Если p95 улучшился, но hits не появились, кеш не является подтверждённым объяснением.</p>\n<p>Запишите также, что кеш не должен менять содержимое ответа, права доступа и срок актуальности данных. Пять минут — условие примера, а не рекомендация для любого каталога. Для цены и наличия такой TTL может быть неприемлемым; для справочного списка он может оказаться допустимым. Ограничение входит в гипотезу, а не добавляется после удачного графика.</p>\n<h2>Соберите сопоставимое сравнение</h2>\n<p>Сравнение «час до релиза с ночью после релиза» не проверяет эффект кеша. Минимум нужно зафиксировать одинаковый маршрут, длительность окна, класс трафика и близкое число запросов. Если одновременно менялись размер ответа, регион, лимит upstream или формат сериализации, их нужно записать как возможные причины.</p>\n<p>Ниже — маленький fixture для проверки правил сравнения. Значения вымышлены и нужны только для воспроизведения логики: до изменения было 10 000 запросов, после — 9 800. Такой пример не заявляет реальный результат и не заменяет измерение.</p>\n<pre><code>function compareWindows(before, after) {\n const requestDelta = Math.abs(after.requestCount - before.requestCount);\n const requestRatio = requestDelta / before.requestCount;\n const comparable = before.windowMinutes === after.windowMinutes\n &amp;&amp; before.route === after.route\n &amp;&amp; before.trafficClass === after.trafficClass\n &amp;&amp; requestRatio &lt;= 0.05;\n const mechanismObserved = after.cacheHits &gt; before.cacheHits\n &amp;&amp; after.upstreamCalls &lt; before.upstreamCalls;\n const effectObserved = after.p95Ms &lt; before.p95Ms;\n\n if (!comparable) {\n return { status: 'stop', reason: 'windows-not-comparable' };\n }\n if (!mechanismObserved) {\n return { status: 'stop', reason: 'cache-mechanism-not-observed' };\n }\n if (!effectObserved) {\n return { status: 'stop', reason: 'user-facing-effect-not-observed' };\n }\n return { status: 'hypothesis-supported', confidence: 'bounded' };\n}\n\nconst before = {\n windowMinutes: 60, route: '/catalog', trafficClass: 'catalog',\n requestCount: 10000, cacheHits: 0, upstreamCalls: 10000, p95Ms: 780,\n};\nconst after = {\n windowMinutes: 60, route: '/catalog', trafficClass: 'catalog',\n requestCount: 9800, cacheHits: 6200, upstreamCalls: 3800, p95Ms: 510,\n};\nconsole.log(compareWindows(before, after));</code></pre>\n<p>Ключевая проверка здесь — не число 510, а условие сопоставимости. Для окон до и после не требуется одинаковая отметка времени: требуется одинаковая длительность и близкая популяция запросов. Функция останавливается при несовпоставимом входе, при отсутствии признака кеша или при отсутствии улучшения. Положительный статус означает только «данные согласуются с гипотезой».</p>\n<h2>Проверьте механизм отдельным сигналом</h2>\n<p>Разложите измерения по уровням. Счётчик cache hits отвечает на вопрос о ветке чтения. Счётчик upstream calls показывает, уменьшилась ли работа внешней зависимости. p95 отвечает на вопрос о распределении времени ответа, но не сообщает, какой путь его сформировал. Ошибки и просроченные записи показывают цену побочных эффектов.</p>\n<p>Сопоставьте один и тот же request или trace там, где это возможно: вход в обработчик, решение hit или miss, вызов upstream и итоговый ответ. Для метрик без связи с конкретным запросом оставьте агрегаты и явно запишите их границы. Нельзя выдавать отсутствие события в непокрытой телеметрии за доказательство, что события не было.</p>\n<p>Проверьте отрицательные ветки отдельно. Холодный кеш должен приводить к miss и вызову upstream. Истёкший TTL не должен отдавать устаревшую запись. Ошибка десериализации не должна превращаться в тихий ответ из повреждённого значения. Если эти ветки не проверены, вывод ограничивается тёплым кешем и успешным ответом.</p>\n<div class=\"table-scroll\"><table><caption>Как отделить эффект кеша от конкурирующих объяснений</caption><thead><tr><th scope=\"col\">Наблюдение</th><th scope=\"col\">Возможное объяснение</th><th scope=\"col\">Проверка</th><th scope=\"col\">Решение</th></tr></thead><tbody><tr><td>p95 ниже, cache hits не выросли</td><td>Изменился трафик или upstream</td><td>Сопоставить маршрут, класс нагрузки и traces</td><td>Не приписывать эффект кешу</td></tr><tr><td>hits выросли, upstream calls прежние</td><td>Кеш не участвует в итоговом пути</td><td>Проверить ключ, ветку чтения и вызов API</td><td>Исправить механизм или остановиться</td></tr><tr><td>upstream calls снизились, ошибки выросли</td><td>Промахи или повреждённые записи скрываются</td><td>Сравнить коды ошибок, TTL и размер выборки</td><td>Откатить изменение до разбирательства</td></tr><tr><td>Окна различаются по трафику</td><td>Состав запросов изменился</td><td>Разбить данные по региону, маршруту и типу клиента</td><td>Собрать новое сопоставимое окно</td></tr><tr><td>Данные неполны</td><td>Сигнал не собирался на нужной ветке</td><td>Проверить покрытие логов и трасс</td><td>Назвать пробел, не достраивать факт</td></tr></tbody></table></div>\n<h2>Отделите альтернативы контрфактом</h2>\n<p>Контрфактический вопрос звучит так: «Что увидели бы мы, если кеш не вызвал улучшение?» Например, p95 мог снизиться из-за падения нагрузки на upstream. Тогда похожее снижение должно наблюдаться и на маршруте без кеша или вместе с уменьшением общей очереди. Если этого контроля нет, формулировка остаётся слабой: «изменение совпало с улучшением в выбранном окне».</p>\n<p>Второй вопрос: «Что должно измениться, если механизм работает?» В примере должны вырасти hits, снизиться upstream calls и сохраниться корректность ответа. Ищите альтернативы до того, как увидели итог: другой релиз, изменение конфигурации, сдвиг регионов, сезонный пик, прогрев инфраструктуры, изменение лимита или сбой зависимости.</p>\n<p>Идеальный эксперимент доступен не всегда. Подойдут поэтапное включение, контрольный маршрут, чередование вариантов, повторные окна с одинаковыми фильтрами или сравнение запросов одного класса. Но каждый вариант имеет цену: контрольный маршрут может быть меньше по объёму, повторные окна — зависеть от времени, а чередование — давать нагрузочный след. Укажите этот компромисс рядом с результатом.</p>\n<h2>Не подменяйте данные историей</h2>\n<p>Технический текст часто становится убедительным за счёт деталей, которых никто не измерял: «команда увидела проблему в 14:30», «после переключения график сразу стабилизировался», «пользователи перестали жаловаться». Если таких записей нет, это не факты кейса. В учебном материале называйте их условиями примера; в рабочем — прикладывайте запрос к метрике, trace, лог или ссылку на изменение.</p>\n<p>Разделяйте факт, интерпретацию и решение. Факт: в окне 60 минут зафиксировано 6 200 cache hits. Интерпретация: механизм кеша согласуется с уменьшением upstream calls. Решение: оставить изменение только для маршрута и TTL, которые прошли проверку. Такая разметка даёт читателю воспроизводимый следующий шаг и не обещает переносимость результата в другую систему.</p>\n<p>Не используйте число как замену неопределённости. NIST SP 800-30 связывает риск с возможным неблагоприятным воздействием и вероятностью, а также отдельно описывает неполное знание, нераспознанные зависимости и ограниченную применимость оценки во времени. Для инженерного кейса достаточно указать остаточный риск словами: устаревшие данные, стоимость памяти, рост miss после рестарта и неизвестное поведение при пике.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Запишите симптом, маршрут и цену ошибки до изменения.</li><li>Сформулируйте одно изменение и механизм, который должен его связать с эффектом.</li><li>Выберите сигнал механизма и сигнал результата: например, cache hits и p95.</li><li>Сделайте окна сопоставимыми по длительности, маршруту, трафику и близкому числу запросов.</li><li>Назовите хотя бы одну альтернативную причину и сформулируйте контрфакт.</li><li>Проверьте hit, miss, TTL, ошибку чтения и корректность ответа.</li><li>Сверьте агрегаты с traces или логами там, где это технически возможно.</li><li>Снизьте силу вывода до уровня данных и запишите остаточный риск.</li><li>Назовите критерий остановки и действие после него: собрать сигнал, выровнять окно или откатить изменение.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Сравнение двух окон не доказывает причинность, если одновременно изменялись несколько факторов. Контрольный маршрут может не представлять всех пользователей. Sampling может скрыть редкую ошибку. p95 может улучшиться для одного endpoint и ухудшиться для критичного сценария. Агрегатная метрика не заменяет проверку содержимого ответа, прав доступа и свежести данных.</p>\n<p>Пример с кешем применим только к идемпотентному чтению, определённому ключу, оговорённому TTL и измеряемому upstream. Его нельзя автоматически переносить на операции записи, персонализированные ответы, денежные расчёты или данные, для которых устаревшее значение опасно. Учебный код не является нагрузочным тестом и не проверяет инвалидацию, распределённый кеш, отказ хранилища и стоимость памяти.</p>\n<p>Если нет сопоставимого окна или сигнала механизма, остановка — это результат. Следующий шаг должен быть конкретным: добавить счётчик на ветку hit/miss, связать trace с upstream-вызовом, повторить измерение на одном классе трафика или отменить сильное утверждение. Нельзя заполнять пробел правдоподобной цифрой.</p>\n<h2>Критерий готовности кейса</h2>\n<p>Кейс готов, когда читатель без доверия к автору может ответить на четыре вопроса: что наблюдали; какое изменение должно было сработать; какое сравнение отделяет его от альтернативы; что произойдёт при нехватке данных. У каждого вывода должны быть окно, маршрут, сигналы, условия применимости и остаточный риск.</p>\n<p>Поэтому финальная фраза редко должна звучать как «кеш ускорил систему». Точнее написать: «В сопоставимых окнах для маршрута <code>/catalog</code> наблюдение совместимо с гипотезой кеша: hits появились, upstream calls снизились, p95 уменьшился; влияние других изменений и поведение холодного кеша требуют отдельной проверки». Такая формулировка оставляет место для следующего измерения и не превращает локальную корреляцию в универсальное правило.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — официальное описание сигналов системы и различий между traces, metrics и logs.</li><li><a href=\"https://doi.org/10.6028/NIST.SP.800-30r1\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-30 Rev. 1: Guide for Conducting Risk Assessments</a> — официальное руководство о воздействии, вероятности, неопределённости и границах оценки.</li></ul>"
}