Files
progcode/editorial/agent-rewrites/058.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
18 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": 58,
"slug": "editorial-2026-05-field-systems-performance",
"title": "Низкий CPU, длинный запрос: где возникает задержка",
"excerpt": "Пользователь ждёт ответ, хотя CPU почти свободен. Разбираем очередь, вложенные интервалы, сопоставимую нагрузку и отказ от ложной оптимизации.",
"contentHtml": "<p>Пользователь ждёт страницу десять секунд, а график CPU держится на двадцати процентах. Разработчик видит свободный процессор и меняет запрос, добавляет поток или увеличивает таймаут. Иногда это случайно скрывает симптом. Часто задержка остаётся: запрос ждал допуска в очередь, соединение с базой или ответ внешней системы. Цена ошибки — лишний релиз, рост нагрузки и потеря исходного сигнала. После изменения уже трудно восстановить исходные условия сравнения.</p>\n<p>Тезис простой: низкая загрузка CPU не опровергает медленный запрос. Сначала разложите end-to-end интервал на наблюдаемые части и назовите границу сравнения. Только после этого выбирайте действие. Один trace показывает структуру пути. Он не доказывает, что изменение ускорит систему.</p>\n<h2>Механизм: задержка не равна работе процессора</h2>\n<p>Общее время запроса включает ожидание и работу. Запрос может стоять в очереди, пока CPU свободен. Он может ждать соединение, блокировку строки, диск, DNS, TLS или ответ удалённого сервиса. В эти моменты процессор не обязан быть занят. Метрика CPU отвечает на вопрос о занятости вычислительного ресурса, но не о времени ответа конкретного запроса.</p>\n<p>Trace отделяет участки пути, если дерево полно и интервалы используют одну временную основу. Root span задаёт end-to-end границу. Дочерний span показывает названную операцию внутри неё. Если дочерний интервал не покрывает разницу, остаток остаётся неизвестным. Его нельзя без отдельного сигнала назвать очередью, сетью или базой.</p>\n<p>Сравнение требует второй границы. Записи до и после изменения должны иметь один класс входа, одинаковое число запросов, одинаковую конкурентность и одинаковую форму данных. Если один прогон обрабатывает 12 запросов при concurrency 3, а второй — 24 при concurrency 6, разница времени ничего не говорит об изменении кода. Более короткий интервал может означать другую нагрузку.</p>\n<figure><img src='/assets/editorial/2026/systems-performance-2026-bottleneck-evidence-loop.svg' alt='Проверка задержки: полный trace, контрольная граница нагрузки, контрпример и остановка при нехватке данных' loading='lazy' /><figcaption>Схема связывает путь запроса с границей сравнения. Если одного условия не хватает, вывод о причине задержки прекращается. Это учебная схема, а не измерение.</figcaption></figure>\n<h2>Учебный пример: не перепутать очередь с причиной</h2>\n<p>Ниже — учебный пример с заранее заданными значениями. Он не обращается к сети, базе, часам или профайлеру. Единицы условны. Код показывает проверку структуры, а не результат работы сервиса.</p>\n<pre><code>const trace = {\n root: { id: 'root-01', start: 0, end: 1000 },\n spans: [\n { id: 'queue-01', parent: 'root-01', name: 'admission-queue', start: 40, end: 560 },\n { id: 'db-01', parent: 'root-01', name: 'db-call', start: 570, end: 720 },\n { id: 'catalog-01', parent: 'root-01', name: 'catalog-call', start: 730, end: 930 }\n ],\n load: { cohort: 'load-a', requests: 12, concurrency: 3, shape: 'read-shape-a' }\n};\n\nfunction inspectTrace(input) {\n const ids = new Set(input.spans.map((span) =&gt; span.id));\n const connected = input.spans.every((span) =&gt;\n span.parent === input.root.id || ids.has(span.parent)\n );\n const ordered = input.spans.every((span) =&gt;\n Number.isFinite(span.start) &amp;&amp; Number.isFinite(span.end) &amp;&amp; span.end &gt;= span.start\n );\n\n if (!connected) return { status: 'stop-incomplete-trace' };\n if (!ordered) return { status: 'stop-invalid-interval' };\n return { status: 'observation-ready', claim: 'not-measured' };\n}\n\nconsole.log(inspectTrace(trace));</code></pre>\n<p>Результат <code>observation-ready</code> означает только, что учебная запись связна и содержит интервалы. Queue span занимает 520 условных единиц из 1000. Это повод проверить очередь отдельным сигналом. Код не доказывает, что очередь является корнем задержки, что база виновата или что удаление очереди ускорит пользователя.</p>\n<p>Отрицательный путь важнее короткого положительного. Если у span <code>parent</code> равен <code>missing-01</code>, функция возвращает <code>stop-incomplete-trace</code>. Если записи до и после изменения используют разные поля <code>load</code>, их нельзя сравнивать. Если в записи стоит <code>effect: 'faster-after-change'</code>, это не измерение. Такое утверждение нельзя принимать без наблюдаемых данных.</p>\n<h2>Симптом → причина → проверка → действие</h2>\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>CPU низкий, запрос медленный</td><td>Очередь, блокировка или внешний ответ входит в end-to-end время</td><td>Открыть trace и разделить ожидание, локальную работу и дочерние вызовы</td><td>Назвать только покрытый span; неизвестный остаток оставить unknown</td></tr><tr><td>Самый длинный span совпал с пиком latency</td><td>Span включает ожидание upstream или retry</td><td>Проверить parent/child, status, retry count и дочерние интервалы</td><td>Не объявлять span причиной; добавить недостающую границу</td></tr><tr><td>Вторая запись короче первой</td><td>Изменилась нагрузка, cohort или форма входа</td><td>Сверить requests, concurrency, cohort и input shape</td><td>Снять сравнение и повторить с одной control boundary</td></tr><tr><td>Дочерний span без parent</td><td>Потеря записи, ошибка экспорта или неверный ID</td><td>Проверить полный экспорт, уникальность ID и формат связи</td><td>Вернуть stop; не дорисовывать дерево по времени</td></tr><tr><td>После изменения есть одна короткая запись</td><td>Нет сопоставимой пары и распределения наблюдений</td><td>Сравнить тот же сценарий до и после на заданном окне</td><td>Назвать observation, а не improvement</td></tr></tbody></table></div>\n<h2>Как читать интервалы</h2>\n<p>Сначала найдите root span и его границы. Затем проверьте, что каждый дочерний span имеет существующего родителя, начало не позже конца, а единицы времени совпадают. Интервалы могут перекрываться. Нельзя складывать все длительности и получать время ответа: параллельные операции будут посчитаны дважды.</p>\n<p>Если root длится 1000 условных единиц, очередь — 520, база — 150, а каталог — 200, сумма дочерних интервалов равна 870. Она не означает, что оставшиеся 130 — сеть. Часть времени могла пересекаться, а часть могла прийтись на неразмеченную работу. Корректная формулировка: «в записи есть 130 единиц, которые не покрыты названными span-ами». Их нельзя приписывать компоненту без отдельной границы.</p>\n<p>Время ожидания и время исполнения также нельзя смешивать. База могла выполнить запрос быстро после освобождения соединения. Внешний вызов мог вернуть ответ быстро, но запрос долго ждал его начала. Название span должно отражать проверенное содержание. <code>db-call</code> не равно «всё время до базы», если выдача соединения записывается отдельно.</p>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксируйте исходный симптом: маршрут, метод, статус, длительность, размер ответа, timestamp и идентификатор запроса.</li><li>Сохраните trace до изменения кода или конфигурации. Отметьте root, дочерние операции, пропуски и неизвестные интервалы.</li><li>Проверьте связность дерева и единицы времени. Отдельно отметьте перекрывающиеся span-ы; не складывайте их механически.</li><li>Назовите контрольную границу: cohort, число логических запросов, concurrency и форму входных данных.</li><li>Разделите ожидание и работу. Не называйте остаток причиной, пока для него нет отдельного сигнала.</li><li>Сформулируйте две конкурирующие гипотезы. Для каждой запишите проверку, которая может её опровергнуть.</li><li>Проверьте отрицательный случай: отсутствующий parent, неизвестная задержка или другая нагрузка должны вернуть точный stop.</li><li>Измените один фактор. Повторите тот же сценарий и сохраните записи до и после рядом.</li><li>Сопоставьте исходный симптом с соседними сигналами: ошибки, таймауты, очередь, throughput и потребление ресурсов. Не заменяйте пользовательскую задержку одним CPU-графиком.</li></ol>\n<h2>Что следует из записи</h2>\n<p>Узкий результат может быть полезным. Например: «В trace-01 при load-a root равен 1000 условных единиц. Названный queue span занимает 520. Дерево связано. Сравнение до и после не выполнялось». Это указывает на очередь как на место для отдельной проверки. Формулировка не содержит обещания исправления.</p>\n<p>Сильнее звучит, но не следует из записи: «очередь стала bottleneck», «изменение БД ускорит путь» и «latency снизилась». Для каждого утверждения нужна отдельная граница доказательства. Нельзя получить контрфактический эффект из одного trace: он не показывает, что произошло бы без выбранного вызова или при другой конкуренции.</p>\n<h2>Ограничения</h2>\n<p>Sampling может убрать нужный span. Collector может потерять запись или доставить события не по порядку. Асинхронный worker может продолжить работу после root span. Часы узлов могут расходиться. Retry может создать несколько операций с похожими именами. Эти условия не делают trace бесполезным, но снижают силу вывода. Их нужно записать рядом с наблюдением.</p>\n<p>Карта интервалов не заменяет нагрузочный тест. Она не измеряет throughput, хвост распределения, стоимость соединений, поведение при исчерпании пула или влияние кэша. Она также не задаёт SLA. HTTP-стандарт описывает семантику запроса и ответа, а не конкретный бюджет latency. Для этих вопросов нужны собственные измерения и критерии.</p>\n<p>Учебный код нельзя считать проверкой реальной телеметрии. В нём заранее заданные числа, одна запись и известные поля. Он не проверяет экспорт, трассировку через прокси, поведение клиента и права доступа. Результат для работающего сервиса появляется только после измерения в описанной среде.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Проверка достаточна для технического вывода, если другой инженер получает тот же вход и без устных пояснений может найти root, проверить parent/child-связи и интервалы, увидеть контрольную границу нагрузки, отличить названную задержку от unknown и воспроизвести stop на неполном trace или несопоставимой нагрузке. Сравнение до и после допустимо, если записи сопоставимы, изменён один фактор, исходный симптом измерен тем же способом, а новый результат не маскирует ошибку ростом таймаутов или потерей сигнала.</p>\n<p>Если критерий не выполнен, вывод ограничивается нехваткой данных. Нельзя выбирать знакомый компонент только потому, что он виден на графике.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://opentelemetry.io/docs/concepts/signals/traces/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Traces</a> — официальное описание trace и span; источник не доказывает причинность конкретной задержки.</li><li><a href='https://www.w3.org/TR/trace-context/' target='_blank' rel='noopener noreferrer'>W3C Trace Context</a> — стандарт передачи trace-контекста между границами; он не гарантирует полноту сбора.</li><li><a href='https://www.rfc-editor.org/rfc/rfc9110.html' target='_blank' rel='noopener noreferrer'>RFC 9110: HTTP Semantics</a> — семантика HTTP-запросов и ответов; RFC не задаёт SLA или бюджет времени приложения.</li></ul>"
}