Files
progcode/editorial/agent-rewrites/117.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": 117,
"slug": "editorial-2024-10-practice-capacity-cost",
"title": "Ёмкость и стоимость: почему зелёный SLO не означает дешёвую систему",
"excerpt": "Как связать нагрузку, резерв ресурсов и единицу тарификации, чтобы рост расхода не маскировался выполненным SLO.",
"contentHtml": "<p>Сервис держит p95 на уровне 235 мс при целевом SLO 300 мс, но резерв CPU и памяти растёт каждую неделю. На графике задержка зелёная. В счёте появляется лишняя базовая ёмкость. Если принять зелёный SLO за доказательство эффективности, команда платит за резерв, которого не связывает ни с нагрузкой, ни с единицей тарификации.</p>\n<p>Ошибка возникает в месте, где смешивают четыре разные величины: поток запросов, выделенный ресурс, фактическое использование и цену. SLO отвечает за задержку или долю успешных запросов. Он не объясняет, сколько CPU зарезервировано и как провайдер выставляет счёт. Тезис статьи простой: стоимость ёмкости проверяют цепочкой <code>workload → resource → billing unit → stop condition</code>. Если звено пропущено, число на дашборде остаётся симптомом.</p>\n<h2>Сначала разделите наблюдаемые факты</h2>\n<p><strong>Workload</strong> описывает поток работы: requests per second, размер сообщения, число фоновых задач и период измерения. <strong>Resource</strong> описывает выделение: replicas, CPU request, memory request, limit и квоту. <strong>Usage</strong> показывает фактическое потребление за интервал. <strong>Billing unit</strong> говорит, за что считают деньги: час инстанса, CPU-hour, GiB-hour, запрос, байт или составную единицу.</p>\n<p>Эти поля связаны, но не заменяют друг друга. Низкий usage не доказывает, что request можно уменьшить: запас может защищать от пика. Высокий usage не доказывает рост цены: тариф может быть фиксированным. Quota ограничивает суммарное потребление пространства имён, но сама по себе не является счётом. Сначала нужно назвать роль каждого значения.</p>\n<figure><img src=\"/assets/editorial/2024/capacity-cost-2024-load-resource-cost.svg\" alt=\"Схема связи между нагрузкой, резервом CPU и памяти, единицей стоимости и пределом решения\"><figcaption>Учебная схема проверки: нагрузка задаёт контекст, ресурс фиксирует резерв, billing unit задаёт формулу, а предел останавливает сравнение. Иллюстрация не показывает реальный кластер, тариф или счёт.</figcaption></figure>\n<h2>Механизм: SLO и стоимость смотрят на разные слои</h2>\n<p>Представьте один сервис с 120 запросами в секунду в обычный час и 180 в пиковый. Его p95 равен 235 мс. На каждый экземпляр задано 1,2 CPU и 2 ГиБ памяти, а предел равен 1,6 CPU и 3 ГиБ. Для учебного расчёта возьмём фиксированную часть 0,36 условной единицы в час, 0,08 за CPU-hour и 0,01 за GiB-hour.</p>\n<p>Если модель считает зарезервированный request, стоимость часа равна <code>0.36 + 1.2 * 0.08 + 2 * 0.01 = 0.476</code>. За 24 часа это 11,424 условной единицы. Это не валюта и не счёт провайдера. Числа нужны, чтобы показать границу: формула использует allocation rule, а не процент CPU на графике. При двух репликах результат удваивается. При изменении тарифа меняется billing unit, а не SLO.</p>\n<p>Теперь увеличим request до 1,4 CPU и 2,2 ГиБ. p95 в учебном сценарии остаётся ниже 300 мс, но дневная стоимость становится <code>(0.36 + 1.4 * 0.08 + 2.2 * 0.01) * 24 = 11.856</code>. Разница равна 0,432 условной единицы за сутки на одну реплику. Нельзя назвать её экономией или потерей в реальной среде: для этого нужны настоящий тариф, число реплик, период, скидки, shared overhead и подтверждённая нагрузка.</p>\n<pre><code>const card = {\n scope: 'checkout-api',\n period: '24h',\n workload: { baselineRps: 120, peakRps: 180, p95Ms: 235, sloMs: 300 },\n resource: { replicas: 2, requestCpu: 1.2, requestMemoryGiB: 2, limitCpu: 1.6 },\n billing: { fixedPerHour: 0.36, cpuHour: 0.08, memoryGiBHour: 0.01 },\n stop: 'p95 headroom < 10 ms or saturation is observed'\n};\n\nconst hourly = card.billing.fixedPerHour\n + card.resource.requestCpu * card.billing.cpuHour\n + card.resource.requestMemoryGiB * card.billing.memoryGiBHour;\nconst daily = hourly * 24 * card.resource.replicas;\nconsole.log({ hourly, daily, sloHeadroomMs: card.workload.sloMs - card.workload.p95Ms });</code></pre>\n<p>Код — учебный пример. Он не читает Kubernetes API, облачный биллинг или метрики. В нём намеренно видны период, replicas и правило тарификации. В рабочем контуре эти значения нужно получить из разрешённых источников и сохранить вместе с timestamp. Если источник не различает request и usage, расчёт нельзя выдавать за стоимость резервирования.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина-кандидат</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>SLO зелёный, расход растёт</td><td>Увеличили request или replicas</td><td>Сравнить deployment, период и число реплик</td><td>Разделить изменение ресурса и изменение нагрузки</td></tr><tr><td>CPU usage низкий, уменьшение не проходит</td><td>Нужен запас на пик или действует quota</td><td>Сверить peak RPS, p95, eviction и admission policy</td><td>Проверить один меньший request в одинаковом интервале</td></tr><tr><td>Процент CPU вырос, цена не изменилась</td><td>Фиксированная тарификация</td><td>Прочитать billing unit и rate card</td><td>Не считать usage заменой счёта</td></tr><tr><td>Цена сравнивается у двух сервисов</td><td>Разные scope или периоды</td><td>Сверить регион, replicas, shared overhead и окно</td><td>Остановить сравнение до выравнивания входа</td></tr><tr><td>Новый limit отклонён</td><td>Quota или LimitRange запрещает значение</td><td>Проверить policy и сообщение admission</td><td>Сначала исправить контракт ресурса, затем считать стоимость</td></tr></tbody></table>\n<p>Таблица задаёт порядок проверки, а не автоматическое решение. Один симптом может иметь несколько причин. Проверка должна исключить хотя бы очевидные альтернативы. Например, низкий CPU при большом p95 может указывать на ожидание сети или блокировку, а не на свободный запас вычислений. Уменьшение request в таком случае меняет риск, но не устраняет задержку.</p>\n<h2>Что именно считать резервом</h2>\n<p>В Kubernetes scheduler учитывает requests при размещении Pod. Limits задают отдельные ограничения выполнения. Поэтому запись «сервис использует 60% CPU» не говорит, что он занимает 60% оплачиваемой ёмкости. Нужно знать, от какой базы рассчитан процент и какую величину использует финансовая модель.</p>\n<p>Память требует отдельной осторожности. Краткий средний usage может скрыть редкий пик. Если процесс получает OOMKilled, зелёный средний график не спасает запросы. Для памяти полезнее сверять peak, рабочий набор, ошибки и время окна. Для CPU важны throttling, очередь и p95. Один общий порог utilisation не подходит обоим ресурсам.</p>\n<p>Quota и LimitRange задают допустимый диапазон на уровне политики. Они могут отклонить Pod с большим request или назначить значения по умолчанию. Но policy не знает цену часа инстанса. Она проверяет допустимость ресурса, а billing-система применяет свою формулу. Смешение этих слоёв рождает ложный вывод: «quota равна capacity» или «limit равен тарифу».</p>\n<h2>Как выбрать stop condition</h2>\n<p>Сравнение нельзя продолжать бесконечно. До расчёта назовите условие остановки. Для latency это может быть запас p95 до SLO. Для ресурса — сигнал saturation, throttling или нехватка памяти. Для стоимости — максимально допустимая дневная дельта. Для политики — отказ admission. Stop condition не говорит, какой вариант выбрать. Он говорит, когда следующий вариант нельзя считать продолжением того же эксперимента.</p>\n<p>В учебном примере зададим запас 10 мс. При p95 235 мс запас до SLO равен 65 мс, и сравнение двух соседних reservation допустимо как расчётная иллюстрация. Если p95 стал 295 мс, запас равен 5 мс. Следующий рост request уже требует отдельного решения о надёжности. Нельзя спрятать этот риск в таблице стоимости.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте scope и окно.</strong> Запишите сервис, регион, число реплик и интервал. Не сравнивайте сутки одного сервиса с часом другого.</li><li><strong>Опишите workload.</strong> Сохраните baseline и peak, p95, ошибки, размер сообщения и долю фоновой работы. Назовите источник каждого значения.</li><li><strong>Разделите request, limit и usage.</strong> Выпишите CPU и память отдельно. Не подставляйте процент utilisation вместо request.</li><li><strong>Проверьте политики.</strong> Прочитайте quota, LimitRange и правила admission. Убедитесь, что сравниваемый вариант вообще допустим.</li><li><strong>Назовите billing unit.</strong> Укажите фиксированную и переменную часть, тариф, tier, скидку и период. Если поле неизвестно, оставьте его неизвестным.</li><li><strong>Посчитайте два соседних варианта.</strong> Меняйте одну величину за раз. Сохраните формулу, вход и результат с единицами.</li><li><strong>Проверьте stop condition.</strong> Сверьте запас до SLO, saturation и допустимую дельту. При нарушении остановите подбор.</li><li><strong>Сформулируйте открытый вопрос.</strong> Укажите, какой владелец должен подтвердить тариф, нагрузку или риск. Без этого расчёт остаётся учебным.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Такая карточка не заменяет capacity planning. Она не моделирует autoscaling, cold start, сеть, хранилище, резервирование, скидки, burst-кредиты, простои и общие узлы. Она не доказывает, что меньший request безопасен. Она только не даёт связать цену с SLO напрямую.</p>\n<p>Отрицательный путь важнее удачного расчёта. Если метрики собраны за разные окна, остановитесь. Если тариф относится к узлу, а request — к Pod, не складывайте их без правила распределения. Если неизвестно число реплик в пике, не называйте дневную стоимость точной. Если policy изменилась после замера, пересчитайте вход. Не подставляйте ноль вместо неизвестного поля: это превращает отсутствие данных в ложную экономию.</p>\n<p>Не стоит запускать уменьшение ресурса только потому, что модель дала меньшую цифру. Сначала проверьте peak и p95 на том же окне, затем выполните изменение по обычному безопасному процессу, а после него сравните ошибки, задержку, throttling и billing. В этой статье нет production-результата и нет обещания экономии. Есть только критерии, по которым такой результат можно будет подтвердить.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Проверка готова, если второй инженер может по одной карточке ответить на пять вопросов: какой workload измеряли; какой resource зарезервирован; что означает usage; какая billing unit применена; при каком сигнале сравнение останавливается. Формула должна воспроизводиться на том же входе. Ссылки на тариф и политику должны быть доступны владельцу. При отсутствии любого ответа статус должен быть «данных недостаточно», а не «дешевле».</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes Documentation: Resource Management for Pods and Containers</a> — семантика requests и limits.</li><li><a href=\"https://kubernetes.io/docs/concepts/policy/resource-quotas/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes Documentation: Resource Quotas</a> — ограничения суммарных ресурсов namespace.</li><li><a href=\"https://opentelemetry.io/docs/concepts/signals/metrics/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Metrics</a> — измерение числовых сигналов и их временной контекст.</li></ul>"
}