8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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>"
|
||
}
|