Files
progcode/editorial/agent-rewrites/039.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
15 KiB
JSON
Raw 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": 39,
"slug": "editorial-2026-12-practice-portfolio-case",
"title": "Инженерный кейс: как связать симптом, решение и доказательство",
"excerpt": "Практический способ разобрать инженерную проблему: отделить наблюдаемый симптом от причины, сравнить варианты, проверить отрицательный путь и не приписать решению эффект без данных.",
"contentHtml": "<p>Запрос к API иногда занимает две секунды вместо двухсот миллисекунд. Пользователь видит пустой экран, оператор получает всплеск таймаутов, а команда увеличивает лимит ожидания. Цена ошибки выше самого запроса: задержка становится менее заметной, но причина остаётся, а зависшие соединения занимают пул.</p><p>Такой эпизод легко описать фразой: «добавили кэш, и экран ускорился». В ней смешаны симптом, причина, изменение и эффект. Если между ними нет наблюдений, читатель не отличит факт от догадки и не сможет повторить решение в другой системе.</p><h2>Тезис: кейс должен показывать причинную цепочку</h2><p>Хороший инженерный кейс связывает проверяемые звенья: входное условие, симптом, гипотезу, действие, наблюдение и границу вывода. Что случилось? Почему это объяснение правдоподобно? Что изменили? Что измерили? Что осталось неизвестным?</p><p>Сила вывода не может быть выше силы наблюдения. Лог подтверждает запись. Трасса подтверждает путь запроса и его длительность. Сравнение двух групп подтверждает различие между группами. Ни один источник сам по себе не доказывает, что изменение улучшило всю систему.</p><h2>Механизм причинной цепочки</h2><p>Начните с наблюдаемого симптома. Укажите маршрут, условие, временной диапазон и единицу измерения. «Медленно» недостаточно. «P95 запроса <code>GET /portfolio</code> вырос с 240 до 1900 мс при 20 параллельных запросах» уже задаёт объект проверки.</p><p>Отделите симптом от гипотезы. Симптом виден в логе или трассе. Гипотеза объясняет его и может оказаться неверной. Здесь гипотеза такая: задержка возникает на повторном получении одного профиля для каждой позиции портфеля, а не в базе данных. Проверка должна различить эти варианты.</p><p>Зафиксируйте варианты до выбора. Первый вариант — исправить цикл и передать профиль одним запросом. Второй — добавить короткий кэш на границе запроса. Третий — увеличить таймаут. Он может убрать ранний отказ, но не сокращает работу. Если не назвать этот путь, любое изменение легко принять за устранение причины.</p><p>Решение должно описывать механизм. «Добавили кэш» — слабая запись. «В пределах одного запроса сохраняем профиль по ключу пользователя; повторный вызов читает это значение; кэш живёт только во время обработки запроса» — проверяемый контракт. Из него следуют тесты, ограничения и способ наблюдения.</p><h2>Пример: кэш только в пределах одного запроса</h2><p>Ниже учебный пример на JavaScript. Он показывает форму локального кэша. Он не сообщает, что конкретная система получила ускорение. Данные, нагрузку и результат нужно измерять отдельно.</p><pre><code>export async function loadPortfolio(userId, api) { const profile = new Map(); async function getProfile(id) { if (!profile.has(id)) profile.set(id, api.get('/profiles/' + id)); return profile.get(id); } const positions = await api.get('/portfolios/' + userId); return Promise.all(positions.map(async (position) =&gt; ({ ...position, owner: await getProfile(position.ownerId) }))); }</code></pre><p>Карта создаётся внутри <code>loadPortfolio</code>. Поэтому два параллельных вызова не делят состояние. Значение сохраняется как promise, а не как готовый ответ. Два одновременных обращения к одному владельцу получают один запрос, даже если первый ещё не завершился.</p><p>Отрицательная ветка — часть механизма. Если поместить карту на уровень модуля, данные одного пользователя могут попасть в обработку другого. Если сохранять только готовый ответ, параллельные обращения создадут дубликаты. Если ключом сделать имя, разные идентификаторы сольются. Ошибка меняет не только скорость, но и корректность данных.</p><h2>Симптом → причина → проверка → действие</h2><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 растёт вместе с числом позиций</td><td>Повторный запрос профиля</td><td>Сравнить число позиций и запросов в одной трассе</td><td>Устранить дубликаты внутри запроса</td></tr><tr><td>Таймаутов меньше, длительность та же</td><td>Изменён лимит, не механизм</td><td>Сопоставить длительность и число запросов</td><td>Вернуться к гипотезе о лишней работе</td></tr><tr><td>Появляются чужие данные</td><td>Состояние живёт дольше запроса</td><td>Проверить ключ и два разных userId</td><td>Перенести хранилище внутрь обработчика</td></tr><tr><td>Скачки только при параллельной нагрузке</td><td>Не сохраняется promise</td><td>Запустить два одинаковых вызова до завершения первого</td><td>Сохранять общий promise и обработать ошибку</td></tr></tbody></table></div><h2>Иллюстрация причинной границы</h2><figure><img src=\"/assets/editorial/2026/portfolio-case-2026-causal-timeline.svg\" alt=\"Схема причинной цепочки: симптом, гипотеза, варианты, изменение, наблюдение и остаточный риск\" loading=\"lazy\" /><figcaption>Схема показывает порядок связи между наблюдением и выводом. Она иллюстрирует структуру разбора, а не измеренный результат конкретной системы.</figcaption></figure><p>Различайте стрелку «после» и стрелку «из-за». Изменение могло произойти до наблюдения, но этого мало для причинного вывода. Нужны одинаковые условия сравнения, источник данных и проверка альтернативных объяснений. Снижение задержки после включения кэша может совпасть с уменьшением нагрузки или прогревом соединений.</p><h2>Порядок действий</h2><ol><li>Запишите один симптом с маршрутом, условием, периодом и измерением.</li><li>Отделите наблюдение от гипотезы и назовите альтернативную причину.</li><li>Назовите варианты, включая путь, который меняет симптом, но не механизм.</li><li>Опишите область жизни состояния, ключи, ошибки и параллельные вызовы.</li><li>Сделайте минимальное изменение и сохраните исходное поведение для сравнения.</li><li>Проверьте положительный путь: повторное чтение использует тот же promise или значение.</li><li>Проверьте отрицательный путь: разные пользователи не делят данные, ошибка не оставляет битое значение, пустой список не вызывает лишних обращений.</li><li>Сравните одинаковые показатели до и после в сопоставимых условиях.</li><li>Запишите остаточный риск и сформулируйте вывод не шире найденных данных.</li></ol><h2>Что считать доказательством</h2><p>Для учебного примера достаточно проверить область жизни и параллельность. Для реального кейса нужен источник наблюдений. Трасса должна содержать идентификатор запроса, длительность и ключевые внешние вызовы. Метрика должна иметь название, единицу, окно и условия сбора. Лог должен связывать ошибку с операцией и не раскрывать секреты.</p><p>Сравнение требует базовой линии. Если до изменения измеряли среднее, а после — P95, вывода о сравнении нет. Если объём запросов различался на порядки, различие может отражать нагрузку. Если менялись код, база и лимит одновременно, эффект нельзя надёжно приписать одному фактору.</p><p>Отрицательный результат тоже важен. Если число запросов не уменьшилось, кэш не достиг цели. Если длительность снизилась только в локальном запуске, этого мало для вывода о другой среде. Если тест обнаружил утечку между пользователями, изменение нужно остановить и исправить границу состояния.</p><h2>Ограничения</h2><p>Кэш внутри запроса не помогает между запросами и не заменяет общий кэш, если источник дорогой для всех клиентов. Он увеличивает память пропорционально числу ключей в одном ответе. Ошибка при загрузке профиля должна удалять сохранённый promise или завершать запрос по явному контракту.</p><p>Схема не решает проблемы устаревших данных, распределённой согласованности и лимитов внешнего API. Она подходит только тогда, когда обращения дублируют работу внутри одной операции, а область жизни результата совпадает с областью жизни запроса. Иначе локальный кэш скрывает проблему или создаёт новую.</p><h2>Проверяемый критерий готовности</h2><p>Кейс готов, если независимый читатель может назвать симптом, проверить гипотезу, воспроизвести положительный и отрицательный путь, увидеть источник измерения и понять, какой вывод запрещено делать. Для <code>loadPortfolio</code> минимальный критерий таков: запрос с двумя позициями одного владельца вызывает один запрос профиля; два разных вызова не делят карту; ошибка профиля не оставляет пригодное значение; в выводе нет обещания эффекта, которого не измеряли.</p><h2>Проверяемые источники</h2><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://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: Promise</a> — официальная справка о состоянии и совместном ожидании асинхронного результата.</li></ul>"
}