Перейти к содержанию

B11 · Производительность и стоимость#

Два свойства, которые обычно обсуждают порознь, хотя это одна тема: производительность определяет, сколько ресурсов нужно, а ресурсы стоят денег. Оптимизация без чисел — угадывание; оптимизация без стоимости — работа непонятно ради чего.

Кроме отдельных приёмов — числа задержек, оптимизация API, Big-O, сборка мусора — блок даёт метод:

  • как измерять;
  • как читать перцентили;
  • почему задержка растёт нелинейно;
  • как считать стоимость запроса.

Две карточки сознательно оставлены короткими справками.

Навигация: PERF-01 · PERF-02 · PERF-03 · PERF-04 · PERF-05 · PERF-06 · PERF-07 · PERF-08 · PERF-09 · PERF-10 · PERF-11


PERF-01 · Числа задержек и бюджет времени ответа#

Шаг W1

TL;DR. Порядки величин, которые надо помнить, чтобы на этапе оценки отличать выполнимое от невыполнимого. Точные числа меняются, соотношения — нет.

Проблема. «Сделаем быстро» превращается в проверяемое утверждение только при наличии ориентиров. Без них дизайн может заложить, скажем, сто обращений к БД на запрос при цели в пятьдесят миллисекунд. И выяснится это на нагрузочном тесте, а не на бумаге.

Ориентиры (порядки, не точные значения).

Операция Порядок
Обращение к кэшу процессора наносекунды
Обращение к оперативной памяти ~100 нс
Чтение с SSD ~100 мкс
Обмен внутри дата-центра ~0.5 мс
Чтение с жёсткого диска ~10 мс
Обмен между континентами 100–200 мс

Из соотношений следуют выводы, не требующие измерений:

  • память быстрее SSD на три порядка, а жёсткого диска — на пять, поэтому кэш работает;
  • обмен внутри дата-центра быстрее чтения с жёсткого диска, поэтому обращение к соседнему сервису дешевле запроса к БД, который ушёл на диск;
  • межконтинентальный обмен дороже всего локального вместе взятого, поэтому число обращений через океан важнее их содержимого.

Бюджет времени ответа. Цель раскладывается по участкам, и сумма должна оставлять запас. Пример для цели 200 мс:

  • сеть до сервера — 20;
  • API gateway — 5;
  • приложение — 30;
  • БД — 40;
  • сериализация — 5;
  • обратный путь — 20;
  • запас — 80.

Такая раскладка сразу показывает, что двадцать последовательных обращений к БД по 40 мс не помещаются. На шаге W1 нужен только этот вывод.

Правила использования.

  • считать в порядках, а не в точных значениях;
  • умножать на количество: дешёвая операция, выполненная тысячу раз, может обойтись дороже одной дорогой;
  • помнить, что параллельные обращения складываются не суммой, а максимумом, поэтому число последовательных участков важнее общего числа вызовов.

Trade-offs и режимы отказа. Числа устаревают: диски и сети быстрее, чем десять лет назад, соотношения устойчивее абсолютных значений. Оценка по средним не учитывает хвост распределения (PERF-02) — а как раз он определяет, что почувствует пользователь.

Что мерить.

  • фактические задержки по участкам против заложенных в бюджет;
  • долю сценариев, где сумма участков превышает цель;
  • RTT (круговую задержку) до ключевых регионов.

Связано: PERF-02 перцентили · PERF-07 закон Литтла · NET-01 как работает интернет · W1

Источники: Latency Numbers Every Programmer Should Know (интерактивно) · Jeff Dean — Designs, Lessons and Advice from Building Large Distributed Systems · ByteByteGo — Latency Numbers Programmer Should Know: Crash Course System Design #1


PERF-02 · Перцентили и хвост распределения#

Шаг W1

TL;DR. Среднее время ответа не описывает опыт пользователя. Решения принимаются по p95, p99 и по тому, как хвост (tail latency) ведёт себя под нагрузкой.

Проблема. Среднее в 80 мс выглядит прекрасно и совместимо с тем, что каждый двадцатый запрос идёт две секунды. Хуже того, экран, который собирает данные десятью параллельными вызовами, ждёт самый медленный из них. Поэтому вероятность увидеть «хвост» гораздо выше одного процента. В распределённых системах это усиливается: цепочка из пяти сервисов означает, что хвост каждого участвует в общем результате.

Как читать перцентили.

  • p50 — типичный опыт; p95 — заметная доля пользователей; p99 и p99.9 — то, что определяет жалобы и нарушения целей (OPS-09).
  • Перцентили нельзя усреднять. Среднее из p99 десяти серверов — не p99 системы. Агрегировать нужно распределения (гистограммы), а не готовые перцентили — иначе цифра на дашборде не значит ничего.
  • Считать по сценариям, а не по всем запросам вместе. Быстрые служебные вызовы разбавляют выборку и скрывают медленный ключевой эндпоинт.

Отчего растёт хвост.

  • конкуренция за ресурс и очередь (PERF-07);
  • паузы сборщика мусора (PERF-10);
  • промахи кэша (CACHE-04);
  • повторы после таймаута (REL-01);
  • «шумные соседи» и CPU throttling в контейнере (OPS-04);
  • неравномерность данных: тяжёлый пользователь, большой заказ, hot key.

Что с этим делать.

  • hedged request (дублирующий запрос) по истечении p95 для дешёвых идемпотентных чтений;
  • ограничение размера ответа и работы на запрос;
  • отсев медленных экземпляров балансировщиком (EDGE-01);
  • уменьшение числа последовательных участков.

Ловушка измерения — coordinated omission. Классическая ошибка нагрузочных инструментов. Когда система тормозит, генератор нагрузки по схеме «отправил — дождался — отправил следующий» сам снижает темп и не фиксирует запросы, которые должны были быть отправлены. В результате измеренная задержка занижена как раз тогда, когда система работает хуже всего. Инструмент должен удерживать заданный темп независимо от ответов (PERF-08).

Trade-offs и режимы отказа. Дублирующие запросы улучшают хвост ценой пяти-десяти процентов лишней нагрузки. Сбор гистограмм дороже счётчиков и увеличивает объём метрик. Оптимизация хвоста обычно стоит дороже оптимизации медианы. Поэтому цель ставится по перцентилю, значимому для сценария, а не по максимально достижимому.

Что мерить.

  • гистограммы по сценариям;
  • отношение p99 к p50 — устойчивый рост означает насыщение;
  • долю запросов сверх целевого порога;
  • максимум за окно.

Связано: PERF-07 очереди и насыщение · PERF-08 нагрузочное тестирование · OPS-09 SLO · PERF-10 сборка мусора

Источники: Gil Tene — How NOT to Measure Latency · Dean & Barroso — The Tail at Scale (CACM 2013) · Kleppmann, DDIA, гл. 1


PERF-03 · Big-O и структуры данных#

Справка

Коротко. Асимптотика отвечает на вопрос, как растёт стоимость с объёмом. В дизайне систем она нужна ровно в двух местах: чтобы не заложить квадратичный алгоритм на растущих данных и чтобы понимать структуры, на которых стоит инфраструктура.

Что важно на практике. Константы и локальность памяти часто важнее степени: линейный проход по массиву быстрее обхода списка с той же асимптотикой. Значение имеет рост: алгоритм, квадратичный по числу элементов, работает на тысяче и падает на миллионе — а объёмы растут.

Структуры, встречающиеся в инфраструктуре.

  • хэш-таблица — основа кэшей и индексов по равенству;
  • B-tree и LSM-дерево — движки хранилищ (DATA-05);
  • skip list (список с пропусками) — упорядоченные множества в Redis (CACHE-05);
  • фильтр Блума — дешёвая проверка «точно нет» перед дорогим обращением (CACHE-04);
  • куча (heap) — приоритетные очереди и выборка топ-K;
  • trie (префиксное дерево) — автодополнение (CASE-06);
  • инвертированный индекс — поиск (DATA-13);
  • consistent hashing (согласованное хэширование) — распределение ключей (DATA-11).

Связано: DATA-05 индексы · PERF-09 профилирование · PERF-06 N+1

Источники: Kleppmann, DDIA, гл. 3 · ByteByteGo — Big-O Notation in 3 Minutes · ByteByteGo — 10 Key Data Structures We Use Every Day


PERF-04 · Конкурентность и параллелизм#

Шаг W5

TL;DR. Конкурентность (concurrency) — про то, как система справляется с несколькими задачами сразу; параллелизм (parallelism) — про то, как она выполняет их одновременно. Для серверных систем первое обычно важнее.

Проблема. Сервер большую часть времени ждёт: БД, сеть, диск. Модель, в которой каждый запрос занимает поток на всё время ожидания, упирается не в CPU, а в число потоков и память под них. Отсюда разные модели исполнения и разные потолки.

Модели.

  • Поток на запрос. Просто, отлаживается стеком вызовов, ограничено стоимостью потоков. Практический потолок — тысячи одновременных запросов.
  • Асинхронная модель (event loop, цикл событий). Один или несколько потоков обслуживают десятки тысяч соединений, переключаясь на ожидании. Цена — заражение асинхронностью всего кода и то, что одна блокирующая операция останавливает всех: тяжёлое вычисление или синхронный вызов в цикле событий убивает пропускную способность.
  • Лёгкие потоки (горутины, виртуальные потоки, корутины). Компромисс: код выглядит синхронным, среда переключает на ожидании. Сегодня — путь по умолчанию там, где он доступен.

Что определяет выбор. Если нагрузка упирается в ожидание — нужна конкурентность (больше одновременных запросов на ядро). Если в вычисления — нужен параллелизм (больше ядер, распределение работы). Диагноз ставится профилированием (PERF-09), а не предположением.

Общие грабли. Разделяемое изменяемое состояние даёт гонки (race conditions) — их устраняют отсутствием разделяемого состояния, а не расстановкой блокировок. Блокировки создают точку сериализации: закон Амдала ограничивает выигрыш от параллелизма долей последовательного участка. Неограниченный параллелизм переносит перегрузку вниз, на БД (MSG-09).

Модели исполнения

Модель Потолок одновременных запросов Цена
Поток на запрос Тысячи Память под стеки, стоимость переключений
Цикл событий Десятки тысяч Асинхронность проникает во весь код; одна блокирующая операция останавливает всех
Лёгкие потоки Десятки тысяч Нужна поддержка в среде исполнения

Trade-offs и режимы отказа. Асинхронная модель поднимает потолок одновременных запросов и усложняет код и отладку. Лёгкие потоки убирают часть этой цены, но доступны не в каждой среде. Рост параллелизма упирается в общий ресурс: доля последовательного участка ограничивает выигрыш независимо от числа ядер.

Что мерить.

  • число одновременных запросов на экземпляр;
  • время ожидания против времени вычисления в профиле;
  • насыщенность пулов;
  • долю времени в блокировках.

Связано: PERF-06 пулы соединений · PERF-07 закон Литтла · PERF-09 профилирование · MSG-09 backpressure

Источники: Rob Pike — Concurrency is not Parallelism · Brendan Gregg — Systems Performance · ByteByteGo — Concurrency Vs Parallelism!


PERF-05 · Оптимизация производительности API#

Шаг W5

TL;DR. Порядок действий: измерить, найти доминирующий участок, устранить его. Приёмы из списков помогают только после этого.

Проблема. Оптимизация обычно начинается с приёмов: добавить кэш, включить сжатие, поставить пул. Часть из них не влияет ни на что, потому что узкое место в другом месте, а часть добавляет сложность и новые режимы отказа. Единственный способ не тратить усилия впустую — сначала измерить.

Порядок.

  1. Измерить сценарий целиком — не эндпоинт, а путь пользователя: сколько обращений, сколько времени где (OPS-07).
  2. Найти доминирующий участок. Если 80 % времени уходит в БД, оптимизация сериализации почти ничего не даст.
  3. Устранить лишнюю работу — самый результативный шаг: не делать запрос вовсе. Убрать N+1 (PERF-06), не выбирать лишние поля, не считать то, что не показывается.
  4. Сократить число последовательных участков. Параллельные вызовы складываются максимумом, последовательные — суммой (PERF-01).
  5. Приблизить данные. Кэш (CACHE-01), индекс под запрос (DATA-05), денормализация (DATA-03).
  6. Уменьшить объём. Пагинация (NET-14), сжатие и выбор формата (NET-07), поля по запросу.
  7. Вынести из синхронного пути всё, что не нужно клиенту немедленно (MSG-02).
  8. Переиспользовать соединения (PERF-06) — часто самый дешёвый выигрыш из всех.

Правило остановки. Оптимизация заканчивается, когда достигнута цель из W1, а не когда закончились идеи: дальнейшее ускорение стоит сложности, которую придётся сопровождать.

Trade-offs и режимы отказа. Кэш добавляет устаревание и инвалидацию (CACHE-03); денормализация — рассогласование; асинхронность — отложенную согласованность и промежуточные состояния в интерфейсе. Локальная оптимизация может ухудшить общее: увеличенный пул соединений разгружает приложение и добивает БД.

Что мерить.

  • время по сценариям до и после;
  • число обращений к зависимостям на запрос;
  • размер ответов;
  • долю времени по участкам;
  • стоимость запроса (PERF-11).

Связано: PERF-06 пулы и N+1 · PERF-09 профилирование · CACHE-01 кэширование · NET-07 сериализация и сжатие

Источники: High Performance Browser Networking · Brendan Gregg — USE Method · ByteByteGo — Top 7 Ways to 10x Your API Performance


PERF-06 · Пулы соединений, N+1 и батчинг#

Шаг W5

TL;DR. Три причины, по которым системы тормозят чаще всего, — и все три не требуют архитектурных изменений.

Проблема. Приложение, которое «внезапно стало медленным на проде», обычно страдает от одной из трёх причин:

  • открывает соединение на каждый запрос;
  • делает сотню запросов вместо одного;
  • ходит по сети по одному элементу там, где можно пачкой.

Все три невидимы на разработческом объёме данных.

Четвёртая причина появляется при разделении на сервисы — множитель. Предел хранилища по соединениям упирается не в нагрузку, а в произведение: число сервисов × экземпляров каждого × размер пула. Десять сервисов по пять экземпляров с пулом в двадцать соединений дают тысячу соединений, и БД откажет на этом раньше, чем на запросах. Пул, разумный для одного сервиса, становится неразумным для десяти: настройка одного сервиса ломает общий предел, хотя сама не менялась. Лечится внешним пулом (pgbouncer и аналоги), уменьшением числа прямых потребителей хранилища и пределом на сервис, а не на экземпляр. Учитывать это надо при разделении данных (D-07, сила «управление соединениями»), а не после первого отказа.

N+1. Запрос списка из ста заказов, затем сто запросов за клиентом каждого. Возникает сам собой при ленивой загрузке (lazy loading) в ORM и при наивных резолверах GraphQL (NET-11). Решается жадной загрузкой (eager loading) одним запросом, JOIN или батчингом по идентификаторам (собрать все нужные ключи и запросить разом). Обнаруживается счётчиком запросов на сценарий — это самая полезная метрика в отладке производительности приложения.

Пулы соединений. Установка соединения с БД стоит рукопожатия, аутентификации и памяти на стороне сервера. Пул переиспользует соединения; ключевой вопрос — размер. Здесь важно контринтуитивное правило: больше не значит быстрее. БД обслуживает ограниченное число одновременных запросов; лишние соединения не ускоряют обработку, а создают конкуренцию за CPU, память и блокировки, увеличивая время ответа для всех. Разумный размер обычно измеряется десятками, а не сотнями, и подбирается измерением. Когда экземпляров приложения много (или это функции без состояния, ARCH-09), между ними и БД ставят посредника, ограничивающего суммарное число соединений.

Батчинг. Замена N сетевых обращений одним с N элементами убирает N задержек. Применяется к чтению по идентификаторам, к записи, к вызовам внешних API (NET-15). Компромисс — задержка накопления пачки: чем больше окно, тем выше пропускная способность и хуже время ответа.

Три проблемы и их признаки

Проблема Признак Лечение
N+1 Число запросов к БД растёт вместе с размером списка Жадная загрузка, JOIN, батчинг по идентификаторам
Маленький пул Ожидание соединения и таймауты при здоровой БД Увеличить — в разумных пределах
Слишком большой пул Растёт время ответа БД при низкой загрузке приложения Уменьшить, поставить посредника перед БД
Поштучные вызовы Задержка, умноженная на количество Батчинг с ограниченным окном накопления

Trade-offs и режимы отказа. Исчерпание пула проявляется как таймауты, похожие на отказ БД, поэтому важно различать эти состояния по метрикам. Слишком большой пул перекладывает перегрузку на БД, где её тяжелее локализовать. Жадная загрузка вместо N+1 может вытащить в память гораздо больше данных, чем нужно, — лечится выбором полей и постраничностью.

Что мерить.

  • число запросов к БД на запрос пользователя;
  • время ожидания соединения из пула и насыщенность пула;
  • число активных соединений на стороне БД;
  • средний размер пачки.

Связано: PERF-05 оптимизация API · PERF-07 закон Литтла · DATA-06 оптимизация SQL · NET-11 GraphQL

Источники: HikariCP — About Pool Sizing · PgBouncer — Documentation · GraphQL — DataLoader


PERF-07 · Очереди, закон Литтла и насыщение#

Шаг W1

TL;DR. Задержка растёт нелинейно с загрузкой. При 80 % утилизации время ответа уже более чем вдвое выше, чем при 50 %, и это свойство математики, а не плохого кода.

Проблема. Естественное ожидание: пока ресурс не загружен на 100 %, всё в порядке. На практике время ответа начинает заметно расти уже после 70–80 %, и рост ускоряется по мере приближения к пределу. Не понимая этого, планируют ёмкость впритык и получают деградацию задолго до исчерпания ресурсов.

Закон Литтла. L = λ × W: среднее число запросов в системе равно интенсивности потока, умноженной на среднее время пребывания. Формула проста и полезна: она связывает три величины, которые обычно измеряют по отдельности, и по любым двум даёт третью. Практические применения:

  • прикинуть нужное число одновременных обработчиков: число = темп × время обработки;
  • проверить правдоподобность измерений;
  • оценить глубину очереди при заданном темпе.

Почему растёт задержка. При случайном потоке запросы приходят неравномерно; когда очередной приходит, а обработчик занят, он ждёт. Чем ближе загрузка к единице, тем длиннее ожидание, и рост близок к гиперболическому: время ожидания пропорционально ρ / (1 − ρ), где ρ — загрузка. При загрузке 0.5 ожидание сравнимо со временем обслуживания, при 0.9 — вдевятеро больше, при 0.99 — почти в сто раз. Отсюда практические правила:

  • планировать ёмкость с целевой загрузкой 60–70 %;
  • помнить, что вариативность времени обслуживания ухудшает картину: один тяжёлый запрос задерживает всю очередь за собой (PERF-02);
  • держать очереди короткими (MSG-09).

Насыщение важнее утилизации. Утилизация показывает, занят ли ресурс; насыщение — сколько работы ждёт очереди. Деградацию предсказывают как раз длина очереди и время ожидания в ней, поэтому мерить нужно их, а не только загрузку CPU (OPS-11).

Trade-offs и режимы отказа. Целевая загрузка ниже — выше стоимость простаивающих ресурсов; это осознанный размен между деньгами и хвостом задержки. Добавление обработчиков помогает, только если узкое место в них: при общем ресурсе (БД, блокировка) увеличение параллелизма усугубляет конкуренцию.

Что мерить.

  • длину очереди и время ожидания в ней отдельно от времени обработки;
  • утилизацию ключевых ресурсов;
  • отношение p99 к p50 как индикатор приближения к насыщению.

Связано: PERF-02 перцентили · OPS-11 ёмкость · MSG-09 backpressure · REL-03 load shedding

Источники: Neil Gunther — Universal Scalability Law · Brendan Gregg — USE Method: насыщение · Google SRE Book, гл. 21 «Handling Overload»


PERF-08 · Нагрузочное тестирование: методика#

Шаг W9

TL;DR. Тест отвечает на конкретный вопрос: какая ёмкость, где ломается, как ведёт себя при перегрузке. «Погоняем нагрузку» без вопроса даёт числа, с которыми нечего делать.

Проблема. Оценки из W1 — гипотезы. Проверяются они единственным способом: воспроизведением нагрузки. Но плохо поставленный тест хуже отсутствующего: он даёт ложную уверенность. Типичные ошибки:

  • нереалистичный профиль: все запросы к одному объекту, который каждый раз попадает в кэш;
  • маленькая БД;
  • отсутствие разогрева;
  • генератор нагрузки, упирающийся в собственные пределы.

Виды и вопросы, на которые они отвечают.

Вид Вопрос
Тест ёмкости Сколько держим при приемлемой задержке?
Стресс-тест Где и как ломается — плавно или обрывом?
Тест на выносливость (soak) Что деградирует за часы: утечки, рост очередей, фрагментация
Всплеск (spike) Как переживает мгновенный рост втрое
Тест восстановления Возвращается ли к норме после снятия перегрузки

Как ставить.

  1. Профиль нагрузки из реальности. Соотношение операций, распределение параметров (популярные и редкие объекты), доля новых и постоянных пользователей. Равномерное распределение ключей даёт нереалистично хороший кэш.
  2. Данные объёма прода. На пустой таблице любой запрос быстрый.
  3. Постоянный темп, а не «сколько получится» — иначе измерение занижает задержку ровно тогда, когда система деградирует (PERF-02).
  4. Разогрев перед измерением: кэши, пулы, компиляция.
  5. Наблюдать систему, а не только генератор. Без метрик изнутри тест показывает лишь факт замедления, но не причину.
  6. Проверять сам генератор. Нередко потолок принадлежит ему, а не системе.
  7. Изолированная среда или отдельный контур — тестирование на проде возможно, но это отдельная практика с ограничением радиуса поражения (REL-07).

Что делать с результатом.

  • ёмкость на экземпляр идёт в планирование (OPS-11);
  • точка перегиба задаёт порог автомасштабирования;
  • поведение при перегрузке проверяет работу load shedding (сброса нагрузки, REL-03).

Регулярный прогон в конвейере ловит регрессии производительности до прода.

Trade-offs и режимы отказа. Полноценный тест дорог: нужна среда, данные и время. Результаты быстро устаревают. Тест против заглушек внешних систем измеряет не то, что будет в проде. Но и полагаться на чужие песочницы под нагрузкой нельзя, поэтому границу проводят явно.

Что мерить.

  • ёмкость при целевой задержке;
  • точку перегиба;
  • поведение p99 по мере роста нагрузки;
  • ресурсы в момент насыщения;
  • время восстановления после перегрузки.

Связано: PERF-02 перцентили · OPS-11 ёмкость · OPS-12 тестирование · PERF-07 очереди

Источники: Gil Tene — How NOT to Measure Latency · k6 — Load testing guides · Google SRE Workbook — Canarying и нагрузочные проверки


PERF-09 · Профилирование#

Шаг W5

TL;DR. Инструмент, отвечающий на вопрос «куда уходит время (или память)». Заменяет догадки измерением — и часто показывает не то, что ожидалось.

Проблема. Интуиция о том, где узкое место, ошибается систематически: подозревают алгоритм, а время уходит на сериализацию; подозревают БД, а виноват разбор JSON. Оптимизация по догадке тратит недели на участок, дающий два процента.

Виды профилирования.

  • По CPU — где выполняется код. Профиль на выборках (сотни раз в секунду снимается стек) даёт малые накладные расходы и годится для прода.
  • По времени ожидания — где код не выполняется, а ждёт: блокировки, ввод-вывод, сеть. Для серверных приложений это чаще и есть ответ.
  • По памяти — что и где выделяется, что удерживается. Показывает причины пауз сборщика (PERF-10) и утечек.
  • Системный уровень — снаружи процесса: динамическая трассировка ядра, наблюдение за системными вызовами, диском и сетью (OPS-13).

Метод. Начинать сверху: система → процесс → функция. Метод USE (utilization, saturation, errors) для ресурсов даёт направление, профиль — точку. Читать профиль в виде flame graph (флейм-графа): ширина — доля времени, вложенность — стек. Сравнивать профили «до» и «после» или с базовой линией — так видно изменение, а не абсолютные величины.

Непрерывное профилирование (continuous profiling) в проде снимает главную проблему: нагрузку прода на стенде обычно не воспроизвести. Профиль из прода отвечает на вопрос напрямую и хранится для сравнения по времени.

Trade-offs и режимы отказа. Инструменты создают накладные расходы: профиль на выборках — единицы процентов, инструментирование каждого вызова — гораздо больше, вплоть до искажения картины. Профиль отражает конкретную нагрузку: снятый в непиковое время, он не объяснит проблему пика. Оптимизация по профилю без цели (PERF-05) превращается в бесконечное занятие.

Что мерить.

  • долю времени в верхних участках профиля;
  • изменение профиля между версиями;
  • накладные расходы самого профилирования.

Связано: PERF-05 оптимизация API · PERF-04 конкурентность · OPS-13 отладка в проде · PERF-10 сборка мусора

Источники: Brendan Gregg — Flame Graphs · Brendan Gregg — Linux Performance Tools · Google-Wide Profiling (IEEE Micro 2010) · ByteByteGo — Linux Performance Tools!


PERF-10 · Сборка мусора и хвост задержки#

Справка

Коротко. Для дизайна систем важно одно свойство сборщика мусора: он периодически останавливает работу, и эти паузы формируют хвост распределения времени ответа.

Что нужно знать.

  • Поколенческая гипотеза (generational hypothesis). Большинство объектов умирает молодыми, поэтому сборка молодого поколения частая и дешёвая, старого — редкая и дорогая. Всплески p99 обычно совпадают со вторым.
  • Компромисс «пропускная способность против пауз». Современные сборщики умеют держать паузы в миллисекундах ценой части CPU и памяти. Выбор — настройка под требования, а не «включить лучший».
  • Размер кучи и лимит контейнера. Рантайм, не знающий об ограничении контейнера, ориентируется на память всей машины и получает OOM kill (OPS-04).
  • Меньше мусора — меньше сборок. Переиспользование буферов, отказ от лишних промежуточных объектов на горячем пути дают больше, чем настройка параметров.
  • Языки без сборщика (Rust, C++) убирают паузы, но переносят стоимость в модель владения памятью.

Практика. Паузы сборщика — часть бюджета задержки (PERF-01): их измеряют, а экземпляр с длинной паузой временно исключается из ротации балансировщиком (EDGE-01), а не считается здоровым.

Связано: PERF-02 перцентили · PERF-09 профилирование · OPS-04 контейнеры

Источники: Java — HotSpot Garbage Collection Tuning Guide · Go — Guide to the Go Garbage Collector · ByteByteGo — How the Garbage Collector Works in Java, Python, and Go!


PERF-11 · Стоимость, TCO и unit-экономика#

Шаг W10

TL;DR. Стоимость — такое же нефункциональное требование, как задержка. Считается на единицу нагрузки, иначе сравнивать архитектуры не с чем.

Проблема. Архитектурные варианты сравнивают по производительности и сложности, а счёт приходит потом. При этом разница между вариантами бывает кратной, и дешевле её узнать на бумаге. Отдельная ловушка — считать только инфраструктуру: труд инженеров и стоимость эксплуатации часто больше счёта за серверы.

Как считать.

  • Unit-экономика. Стоимость на тысячу запросов, на активного пользователя, на гигабайт хранения в месяц. Только такая нормировка позволяет сравнивать варианты и замечать деградацию: рост абсолютного счёта при росте бизнеса нормален, рост стоимости запроса — нет.
  • TCO (total cost of ownership, полная стоимость владения). Вычисления + хранение + трафик + управляемые сервисы + лицензии + observability (наблюдаемость, OPS-08) + труд на эксплуатацию. Последние две статьи забывают чаще всего, а они бывают крупнее первой.
  • Исходящий трафик — регулярный сюрприз в облаке: между регионами и наружу он платный, и архитектура, гоняющая данные между зонами, платит за каждый гигабайт (REL-08).
  • Хранение растёт монотонно, поэтому политика хранения (DATA-19) — статья экономии, а не только требование приватности.
  • Модель оплаты под профиль нагрузки. Резервирование и долгосрочные обязательства — для базовой части, оплата по факту — для пиков, spot-инстансы (прерываемые мощности) — для фоновых задач (ARCH-08).

Где деньги теряются чаще всего.

  • простаивающие ресурсы, выделенные «с запасом»;
  • забытые окружения и тестовые стенды;
  • логи и метрики с избыточной детализацией и сроком хранения;
  • неоптимальный класс хранения для холодных данных;
  • лишний межзональный трафик;
  • автомасштабирование без верхнего предела (OPS-11).

Разбор. Случай Prime Video показывает, что стоимость зависит от профиля нагрузки. Конвейер из функций с передачей данных через хранилище оказался на порядок дороже единого процесса при постоянной нагрузке (ARCH-09).

Статьи затрат

Статья Часто забывают Что с ней делать
Вычисления — Резервирование под базовую часть, оплата по факту под пики
Хранение — Политика хранения и классы хранилища
Исходящий трафик Да Минимизировать межзональный обмен, отдавать через CDN
Управляемые сервисы — Считать на единицу нагрузки, а не общей суммой
Observability Да Кардинальность, сэмплирование, сроки хранения
Труд на эксплуатацию Да Включать в сравнение архитектурных вариантов

Trade-offs и режимы отказа. Экономия на резервировании увеличивает риск (REL-06), поэтому решение принимают вместе с целями надёжности, а не отдельно. Оптимизация стоимости инженерным трудом может стоить дороже сэкономленного. Жёсткая привязка к скидочным обязательствам снижает гибкость.

Что мерить.

  • стоимость на единицу нагрузки и её динамику;
  • долю статей в общем счёте;
  • утилизацию оплаченных ресурсов;
  • долю затрат без владельца;
  • отношение стоимости observability к стоимости инфраструктуры.

Связано: ARCH-09 serverless · OPS-11 ёмкость · OPS-08 стоимость observability · W10

Источники: AWS Well-Architected — Cost Optimization Pillar · FinOps Foundation — Framework · Prime Video Tech Blog · ByteByteGo — Amazon Prime Video Ditches AWS Serverless, Saves 90%