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": 117,
"slug": "editorial-2024-10-practice-capacity-cost",
"title": "Ёмкость и стоимость: почему зелёный SLO не означает дешёвую систему",
"excerpt": "Практическая схема для случая, когда задержка укладывается в SLO, а счёт растёт: разделяем нагрузку, резерв, фактическое использование и единицу тарификации.",
"contentHtml": "<p>Сервис обрабатывает почти столько же запросов, что и месяц назад. Его p95 — 235 мс при целевом значении 300 мс, ошибки не выросли, но ежемесячный счёт за вычисления увеличился. На дашборде всё зелёное, поэтому первая гипотеза звучит неправильно: «раз SLO выполнен, система уже достаточно экономна».</p>\n<p>Зелёный SLO доказывает только соответствие выбранному показателю и окну измерения. Он не говорит, сколько ресурсов зарезервировано, сколько фактически потреблено и по какой единице провайдер выставляет счёт. Разберём учебный сценарий, в котором сначала разделим эти величины, затем сверим их командами Kubernetes и посчитаем два соседних варианта. Результат — не обещание экономии, а воспроизводимый способ понять, каких данных не хватает для решения.</p>\n<h2>Начните с четырёх разных вопросов</h2>\n<p>До изменения Deployment запишите четыре значения. <strong>Workload</strong> — сколько работы приходит: requests per second (RPS), размер сообщений, фоновые задачи и окно наблюдения. <strong>Allocation</strong> — что системе выделено: число реплик, CPU и memory в <code>requests</code> и <code>limits</code>. <strong>Usage</strong> — фактическое потребление за тот же интервал. <strong>Billing unit</strong> — что именно превращает ресурс в деньги: час виртуальной машины, CPU-hour, GiB-hour, запрос, байт или составной тариф.</p>\n<p>Каждый вопрос имеет другого владельца. Нагрузку подтверждает владелец сервиса или аналитики, allocation — манифест и платформа, usage — система метрик, billing unit — тариф и счёт провайдера. Если два источника используют одно слово «CPU», это ещё не делает их значения сопоставимыми. В частности, низкий usage не доказывает, что request можно без риска уменьшить, а высокий usage не доказывает, что цена вырастет: тариф может считать зарезервированный ресурс или фиксированный инстанс.</p>\n<figure><img src=\"/assets/editorial/2024/capacity-cost-2024-scenario-matrix.svg\" alt=\"Схема разделяет фактическую загрузку, выделенный ресурс, насыщение, квоту и формулу стоимости\"><figcaption>Загрузка помогает искать насыщение, allocation задаёт вход модели, quota и limit ограничивают допустимый вариант, а billing unit переводит ресурс в стоимость. Схема учебная: она не показывает реальный кластер или тариф.</figcaption></figure>\n<h2>Почему SLO не является ценником</h2>\n<p>SLI (service level indicator) — измеряемый показатель услуги, например задержка или доля успешных запросов. SLO (service level objective) — целевое значение этого показателя. Если SLO сформулирован как «99% запросов быстрее 300 мс за сутки», он отвечает на вопрос о качестве для пользователя. Он не отвечает на вопрос, оплачивается ли CPU по факту потребления, по резерву или как часть фиксированного инстанса.</p>\n<p>Процентиль тоже требует контекста. p95 в 235 мс может быть рассчитан по одному региону и только по успешным запросам, а счёт — по всем регионам и часам работы. Поэтому рядом с p95 храните окно, фильтр запросов, регион и число реплик. Иначе зелёный показатель создаёт ложное ощущение сравнимости: мы сопоставляем качество одного среза с ценой другого.</p>\n<p>У SLO есть полезная роль в решении о ёмкости: он задаёт границу, после которой уменьшение ресурса нельзя считать приемлемым. Но граница должна включать и другие сигналы — ошибки, очередь, throttling CPU, пики памяти и время восстановления. SLO — контроль качества сервиса, а не доказательство низкой себестоимости.</p>\n<h2>Что на самом деле делают requests, limits и quota</h2>\n<p>В Kubernetes scheduler учитывает <code>requests</code>, когда выбирает узел: сумма запросов размещённых контейнеров должна помещаться в доступную ёмкость узла. Фактическое потребление в момент планирования может быть ниже. Это объясняет, почему свободный CPU на графике не превращается автоматически в возможность уменьшить request.</p>\n<p><code>Limit</code> задаёт другой контракт. Для CPU это верхняя граница, применение которой может проявляться throttling. Для памяти превышение лимита может закончиться убийством процесса механизмом OOM. Поэтому предел нельзя использовать как синоним usage или стоимости. В некоторых конфигурациях admission-механизм подставляет request из limit, если request не задан; итоговый объект нужно читать после применения политик, а не угадывать по исходному YAML.</p>\n<p><code>ResourceQuota</code> ограничивает суммарное потребление ресурсов и объектов в namespace. Квота может не пропустить новый Pod или изменение Deployment, но не сообщает цену часа. Она отвечает на вопрос «разрешён ли такой объём в namespace», а billing отвечает на вопрос «как этот объём тарифицируется». Эти проверки полезно выполнять вместе, но не смешивать их результаты.</p>\n<h2>Диагностическая последовательность</h2>\n<p>Сначала снимите allocation и политику, затем usage, и только после этого сравнивайте с инвойсом. Команды ниже не меняют кластер. Переменные задают namespace и Deployment; <code>kubectl top</code> требует установленного Metrics Server и показывает текущую оценку usage, а не платёжный документ.</p>\n<pre><code>NS=checkout\nDEPLOY=checkout-api\n\n# Фактические requests/limits контейнеров в шаблоне Deployment\nkubectl -n \"$NS\" get deploy \"$DEPLOY\" -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{\"\\t\"}{.resources.requests.cpu}{\"\\t\"}{.resources.requests.memory}{\"\\t\"}{.resources.limits.cpu}{\"\\t\"}{.resources.limits.memory}{\"\\n\"}{end}'\n\n# Реплики и доступное состояние rollout\nkubectl -n \"$NS\" get deploy \"$DEPLOY\" -o wide\n\n# Квота namespace\nkubectl -n \"$NS\" get resourcequota\n\n# Текущий usage; это не billing unit\nkubectl -n \"$NS\" top pod -l app=\"$DEPLOY\" --containers</code></pre>\n<p>Сохраните вывод вместе с timestamp, окном метрик и commit манифеста. Если label-селектор или Metrics Server в конкретном кластере устроены иначе, команда не даст данных — это повод уточнить интерфейс, а не подставить нули. Для счёта дополнительно сохраните регион, размер инстанса, число часов, скидку и строку тарифа. Без этих полей стоимость нельзя честно распределить на Pod.</p>\n<h2>Учебный расчёт двух соседних вариантов</h2>\n<p>Возьмём две реплики одного сервиса. В первом варианте каждая получает request 1,2 vCPU и 2 GiB памяти. Во втором — 1,4 vCPU и 2,2 GiB. Фиксированная часть модели — 0,36 условной единицы на реплику в час, CPU — 0,08 за vCPU-hour, память — 0,01 за GiB-hour. Все числа вымышлены для проверки арифметики; их нельзя переносить в счёт конкретного облака.</p>\n<p>Цена одной реплики в час для первого варианта равна <code>0,36 + 1,2 × 0,08 + 2 × 0,01 = 0,476</code>. Для двух реплик за сутки — <code>0,476 × 24 × 2 = 22,848</code>. Во втором варианте получаем <code>0,36 + 1,4 × 0,08 + 2,2 × 0,01 = 0,494</code>, или <code>0,494 × 24 × 2 = 23,712</code>. Разница модели — 0,864 условной единицы за сутки.</p>\n<pre><code>const scenario = {\n replicas: 2,\n hours: 24,\n fixedPerReplicaHour: 0.36,\n cpuHour: 0.08,\n memoryGiBHour: 0.01,\n variants: [\n { name: 'current', requestCpu: 1.2, requestMemoryGiB: 2.0, p95Ms: 235 },\n { name: 'larger-request', requestCpu: 1.4, requestMemoryGiB: 2.2, p95Ms: 235 }\n ]\n};\n\nfunction dailyCost(variant) {\n const replicaHour = scenario.fixedPerReplicaHour\n + variant.requestCpu * scenario.cpuHour\n + variant.requestMemoryGiB * scenario.memoryGiBHour;\n return replicaHour * scenario.hours * scenario.replicas;\n}\n\nfor (const variant of scenario.variants) {\n console.log(variant.name, {\n dailyModelCost: Number(dailyCost(variant).toFixed(3)),\n sloHeadroomMs: 300 - variant.p95Ms\n });\n}</code></pre>\n<p>Ожидаемый вывод: <code>current</code> — 22.848 и запас 65 мс; <code>larger-request</code> — 23.712 и тот же запас. Код показывает связь только внутри заранее выбранной модели. Он не учитывает autoscaling, стоимость узла, shared overhead, storage, сеть, налоги, резервирование и скидки. Если провайдер считает целый инстанс, подстановка CPU request в формулу будет неверной даже при безошибочной арифметике.</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>Изменились requests, реплики или тариф</td><td>Манифест, число реплик, регион, строку инвойса</td><td>Разделить изменение allocation и изменение цены</td></tr><tr><td>Usage CPU ниже request</td><td>Request оставляет запас на пик или задаёт размещение</td><td>Пиковый RPS, очередь, throttling, расписание</td><td>Сравнить один меньший request на том же окне</td></tr><tr><td>Memory usage в среднем низкий</td><td>Редкий пик скрыт агрегацией</td><td>Максимум, OOM, eviction и длительность окна</td><td>Не уменьшать memory только по среднему</td></tr><tr><td>Цена не изменилась после роста usage</td><td>Тариф фиксирован по инстансу или tier</td><td>Billing unit и границы tier</td><td>Не заменять счёт метрикой usage</td></tr><tr><td>Новый Pod не создаётся</td><td>Quota, LimitRange или admission отклонили объект</td><td>Событие namespace и итоговый Pod spec</td><td>Исправить допустимый диапазон до расчёта цены</td></tr></tbody></table>\n<p>Таблица задаёт порядок исключения гипотез, а не готовую оптимизацию. Низкий CPU при высоком p95 может означать ожидание внешнего сервиса или блокировку. В таком случае меньший request не устраняет причину задержки и лишь уменьшает запас. Наоборот, рост стоимости при неизменном usage может быть полностью ожидаемым, если оплата идёт за фиксированный инстанс.</p>\n<h2>Как выбрать границу остановки</h2>\n<p>Сравнивайте соседние варианты, пока у эксперимента остаётся одинаковый workload и понятен риск. Для сценария задайте заранее: SLO p95 не выше 300 мс, error rate не растёт, нет нового throttling, memory peak не приближается к лимиту, а дневная дельта модели не выше согласованного порога. Это не универсальные значения, а поля конкретного эксперимента.</p>\n<p>В учебном расчёте p95 235 мс оставляет запас 65 мс. Если после изменения он стал 295 мс, запас сократился до 5 мс, даже если модель обещает экономию. Если выросли ошибки или появился OOM, эксперимент нужно остановить независимо от зелёного среднего графика. Граница должна быть измерима другим инженером: «кажется, стало хуже» для неё недостаточно.</p>\n<h2>Порядок проверки перед изменением</h2>\n<ol><li><strong>Зафиксируйте scope.</strong> Укажите сервис, namespace, регион, реплики, тип workload и точное окно.</li><li><strong>Снимите baseline.</strong> Сохраните RPS, p50/p95/p99, error rate, peak memory, CPU throttling и текущий Deployment.</li><li><strong>Разделите величины.</strong> Запишите requests, limits и usage отдельными полями с единицами и источниками.</li><li><strong>Проверьте допустимость.</strong> Прочитайте ResourceQuota, LimitRange и события admission; убедитесь, что новый объект может быть размещён.</li><li><strong>Назовите billing unit.</strong> Подтвердите, считается ли резерв, usage, целый инстанс, запрос или tier. Если правило неизвестно, пометьте стоимость неизвестной.</li><li><strong>Измените одну величину.</strong> Не меняйте одновременно request, replicas, тип инстанса и autoscaling: иначе причинность потеряется.</li><li><strong>Сравните то же окно.</strong> Проверьте SLO, ошибки, saturation и счёт в одинаковом scope. Примените stop condition.</li><li><strong>Сохраните решение.</strong> Запишите результат, оставшийся риск, владельца тарифа и план отката.</li></ol>\n<h2>Границы применимости</h2>\n<p>Схема подходит для первичной диагностики расхождения между качеством сервиса и затратами на вычисления. Она не заменяет capacity planning, load test или финансовую сверку. Она не моделирует нелинейный autoscaling, cold start, сетевой трафик, хранилище, стоимость control plane, shared nodes и минимальную плату провайдера.</p>\n<p>Её нельзя применять как доказательство, что меньший request безопасен. Нужны пиковые данные, запас по SLO, проверка memory pressure и обычная процедура изменения с откатом. Не переносите формулу между провайдерами без подтверждённого rate card. Не сравнивайте usage из одного часового окна с инвойсом за календарный месяц и не распределяйте стоимость узла на Pod без правила аллокации.</p>\n<p>Отрицательный результат тоже полезен. Если окна, регионы, реплики или единицы различаются, статус проверки — «данных недостаточно». Это лучше, чем назвать нулевую неизвестную величину экономией. Точный вывод появляется только тогда, когда второй инженер может повторить сбор входов, расчёт и проверку stop condition.</p>\n<h2>Критерий готовности</h2>\n<p>Решение можно передавать на изменение, если по одной карточке видны workload, allocation, usage и billing unit; у каждого поля есть источник и окно; формула воспроизводится; SLO и технические stop condition определены; quota и admission не блокируют вариант; владелец тарифа подтвердил финансовую часть. Если хотя бы один пункт отсутствует, разрешённый результат — продолжить сбор данных, а не объявить систему дешёвой.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://sre.google/sre-book/service-level-objectives/\" target=\"_blank\" rel=\"noopener noreferrer\">Google SRE Book: Service Level Objectives</a> — определения SLI, SLO, percentile и роль error budget.</li><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, планирование и ограничения CPU/памяти.</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 Documentation: Metrics</a> — метрики как runtime measurements, временной контекст и агрегации.</li></ul>"
}