Перейти к основному содержимому

4.06. Ресурсопотребление и метрики

Разработчику Архитектору Инженеру

Ресурсопотребление и метрики

Ресурсы

Ресурсы вычислительной системы — это ограниченные компоненты, необходимые для выполнения программ.

Основные категории ресурсов:

  • процессорное время (CPU time)
  • оперативная память (RAM)
  • дисковое пространство и операции ввода-вывода
  • сетевые ресурсы
  • специализированные ресурсы (GPU, TPU)

Ресурсы делятся на:

  • вычислительные — процессор, сопроцессоры
  • хранилища — оперативная память, дисковое пространство
  • коммуникационные — сетевые интерфейсы, шины данных

Что измерять — CPU, память, дисковый I/O, сеть

Метрики производительности — количественные показатели использования ресурсов системы.

РесурсКлючевые метрикиЕдиницы измерения
CPUИспользование, время в пользовательском/системном режиме, контекстные переключенияпроценты, миллисекунды
ПамятьИспользуемая память, выделенная память, свопинг, частота сборки мусорамегабайты, гигабайты
ДискОперации ввода-вывода в секунду (IOPS), пропускная способность, задержкаоперации/сек, МБ/с, мс
СетьПропускная способность, задержка, количество пакетов, ошибкиМбит/с, мс, пакеты/с

CPU — на что обращать внимание

Процессорное время — основной ресурс для выполнения инструкций программы.

Важные аспекты анализа CPU:

  1. Утилизация процессора

    • 0-70% — нормальная нагрузка
    • 70-90% — высокая нагрузка, возможны задержки
    • 90-100% — критическая нагрузка, риск деградации производительности
  2. Режимы выполнения

    • Пользовательский режим — выполнение кода приложения
    • Системный режим — выполнение системных вызовов ядром ОС Высокий процент системного времени может указывать на чрезмерное количество системных вызовов
  3. Контекстные переключения Частые переключения между потоками создают накладные расходы. Норма — до нескольких тысяч переключений в секунду на ядро.

  4. Прерывания Аппаратные прерывания от устройств могут конкурировать за процессорное время.

Пример анализа в Linux:

# Просмотр загрузки CPU
top

# Детальная статистика
vmstat 1

# Процент времени в разных режимах
mpstat -P ALL 1

Пример анализа в Windows через PowerShell:

# Загрузка CPU по процессам
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, CPU

# Системная статистика
Get-Counter '\Processor(_Total)\% Processor Time'

Память — на что обращать внимание

Оперативная память — временно хранилище данных и кода во время выполнения программы.

Ключевые метрики памяти:

  1. Используемая память Объём памяти, выделенный процессом. Включает:

    • кучу (heap) — динамически выделяемая память
    • стек (stack) — память для локальных переменных и вызовов
    • сегмент кода — исполняемые инструкции
    • сегмент данных — глобальные и статические переменные
  2. Резидентная память (RSS) Фактически загруженная в физическую память часть процесса.

  3. Виртуальная память Общий адресное пространство процесса, включая своп и зарезервированные области.

  4. Свопинг Перемещение страниц памяти между оперативной памятью и диском. Частый свопинг указывает на нехватку оперативной памяти.

  5. Утечки памяти Постепенный рост используемой памяти без освобождения.

Пример обнаружения утечки памяти в C#:

public class MemoryLeakExample
{
private static readonly List<byte[]> _cache = new List<byte[]>();

public void AddToCache()
{
// Добавляем данные в кэш, но никогда не удаляем старые
_cache.Add(new byte[10 * 1024 * 1024]); // 10 МБ
}

// Правильный вариант с ограничением размера кэша
public void AddToCacheWithLimit()
{
_cache.Add(new byte[10 * 1024 * 1024]);
if (_cache.Count > 100)
{
_cache.RemoveRange(0, _cache.Count - 100); // Оставляем последние 100 элементов
}
}
}

Пример мониторинга памяти в Python:

import tracemalloc
import gc

# Включение трассировки выделения памяти
tracemalloc.start()

# Выполнение кода
# ...

# Снимок текущего состояния
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("[ Top 10 memory allocations ]")
for stat in top_stats[:10]:
print(stat)

# Принудительная сборка мусора
gc.collect()

Диск — на что обращать внимание

Дисковая подсистема — критический ресурс для операций ввода-вывода.

Важные метрики дисковой подсистемы:

  1. IOPS (Input/Output Operations Per Second) Количество операций чтения/записи в секунду. Зависит от типа носителя:

    • HDD: 50-200 IOPS
    • SATA SSD: 10 000-100 000 IOPS
    • NVMe SSD: 100 000-1 000 000+ IOPS
  2. Пропускная способность Объём данных, передаваемых в секунду (МБ/с или ГБ/с).

  3. Задержка (latency) Время от начала операции до её завершения:

    • хороший показатель: < 10 мс
    • приемлемый: 10-50 мс
    • проблемный: > 50 мс
  4. Очереди операций Накопление запросов на диск указывает на перегрузку подсистемы.

Пример анализа дисковой активности в Linux:

# Мониторинг в реальном времени
iostat -x 1

# Детальная статистика по устройствам
iotop

# Просмотр очередей
cat /proc/diskstats

Пример оптимизации дисковых операций:

// Плохо: множественные мелкие записи
using (var writer = new StreamWriter("log.txt"))
{
foreach (var item in items)
{
writer.WriteLine(item.ToString()); // Отдельная операция ввода-вывода
}
}

// Хорошо: пакетная запись с буферизацией
using (var writer = new StreamWriter("log.txt", bufferSize: 8192))
{
var batch = new StringBuilder();
foreach (var item in items)
{
batch.AppendLine(item.ToString());
if (batch.Length > 8192)
{
writer.Write(batch.ToString());
batch.Clear();
}
}
writer.Write(batch.ToString());
}

Сеть — на что обращать внимание

Сетевые ресурсы — каналы передачи данных между системами.

Ключевые сетевые метрики:

  1. Пропускная способность Максимальный объём данных, передаваемых по сети в единицу времени.

  2. Задержка (latency) Время прохождения пакета от отправителя к получателю:

    • локальная сеть: 0.1-1 мс
    • городская сеть: 1-10 мс
    • межконтинентальная: 50-200 мс
  3. Джиттер (jitter) Вариативность задержки. Высокий джиттер проблематичен для реального времени.

  4. Потери пакетов Процент пакетов, не достигших получателя. Приемлемый уровень — < 1%.

  5. Количество соединений Ограничение на количество одновременных TCP-соединений.

Пример мониторинга сети в Linux:

# Статистика сетевых интерфейсов
iftop

# Детальная статистика
nload

# Просмотр активных соединений
netstat -an | grep ESTABLISHED | wc -l

Пример оптимизации сетевых вызовов:

// Плохо: множество мелких запросов
public async Task<List<User>> GetUsersBad(List<int> userIds)
{
var users = new List<User>();
foreach (var id in userIds)
{
var user = await _httpClient.GetAsync($"api/users/{id}");
users.Add(await user.Content.ReadAsAsync<User>());
}
return users;
}

// Хорошо: пакетный запрос
public async Task<List<User>> GetUsersGood(List<int> userIds)
{
var response = await _httpClient.PostAsJsonAsync(
"api/users/batch",
new { userIds }
);
return await response.Content.ReadAsAsync<List<User>>();
}

Метрики кода — cyclomatic complexity, cognitive complexity, coupling, cohesion

Метрики качества кода — количественные показатели структуры и сложности программного кода.

МетрикаОписаниеИдеальное значение
Цикломатическая сложностьКоличество линейно независимых путей выполнения< 10 на метод
Когнитивная сложностьСложность понимания кода человеком< 15 на метод
Связность (coupling)Степень зависимости между модулямиМинимальная
Связность (cohesion)Степень объединения функциональности внутри модуляМаксимальная
Глубина наследованияКоличество уровней в иерархии наследования< 5
Количество параметровАргументы метода< 5

Пример высокой цикломатической сложности:

// Цикломатическая сложность: 8 (плохо)
public decimal CalculateDiscount(Order order, Customer customer, DateTime now)
{
if (customer.IsPremium)
{
if (order.Total > 1000)
return order.Total * 0.2m;
else
return order.Total * 0.15m;
}
else
{
if (order.Total > 1000)
{
if (now.DayOfWeek == DayOfWeek.Monday)
return order.Total * 0.1m;
else
return order.Total * 0.05m;
}
else
{
return 0;
}
}
}

Рефакторинг для снижения сложности:

// Цикломатическая сложность: 2 (хорошо)
public decimal CalculateDiscount(Order order, Customer customer, DateTime now)
{
var baseDiscount = GetBaseDiscount(customer, order.Total);
var dayBonus = GetDayBonus(now, order.Total);
return order.Total * (baseDiscount + dayBonus);
}

private decimal GetBaseDiscount(Customer customer, decimal total)
{
if (!customer.IsPremium) return 0;
return total > 1000 ? 0.15m : 0.1m;
}

private decimal GetDayBonus(DateTime now, decimal total)
{
if (total <= 1000) return 0;
return now.DayOfWeek == DayOfWeek.Monday ? 0.05m : 0;
}

Профилировщики: CPU profiling, memory profiling, allocation tracking

Профилировщик — инструмент для измерения производительности и потребления ресурсов программой.

Типы профилировщиков:

  1. Сэмплинговые профилировщики Периодически снимают состояние стека вызовов. Низкие накладные расходы, но могут пропустить короткие вызовы.

  2. Инструментирующие профилировщики Вставляют код измерения в каждый метод. Точны, но создают значительные накладные расходы.

  3. Трассировочные профилировщики Записывают последовательность событий выполнения. Подходят для анализа временных зависимостей.

Пример использования профилировщика памяти в .NET:

using System.Diagnostics;

// Создание снимка памяти
var snapshot1 = Process.GetCurrentProcess().WorkingSet64;

// Выполнение кода
ProcessData();

// Второй снимок
var snapshot2 = Process.GetCurrentProcess().WorkingSet64;

Console.WriteLine($"Использовано памяти: {(snapshot2 - snapshot1) / 1024 / 1024} МБ");

Пример использования профилировщика в Java (VisualVM):

// Запуск приложения с агентом профилирования
java -agentpath:/path/to/libprofiler.so=cpu=sample,alloc=5m MyApplication

// Или подключение к работающему процессу через JMX

Разбивка по стеку: attribution of resource usage to call paths

Атрибуция ресурсов по стеку вызовов — распределение потребления ресурсов между различными путями вызовов в программе.

Пример атрибуции памяти:

Выделено 100 МБ:
├─ ProcessOrders() — 60 МБ (60%)
│ ├─ ValidateOrder() — 10 МБ (10%)
│ ├─ CalculateTotal() — 30 МБ (30%)
│ │ └─ ApplyDiscounts() — 25 МБ (25%)
│ └─ SaveOrder() — 20 МБ (20%)
└─ GenerateReport() — 40 МБ (40%)
└─ FormatData() — 35 МБ (35%)

Инструменты для атрибуции:

  • async-profiler (JVM) — атрибуция по стеку с низкими накладными расходами
  • eBPF (Linux) — системная атрибуция без модификации приложения
  • Windows Performance Toolkit — детальная атрибуция для Windows

Пример использования eBPF для атрибуции памяти:

# Трассировка выделений памяти с привязкой к стеку вызовов
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc { @[ustack()] = count(); }'

Бюджеты производительности: SLA, latency targets

Бюджет производительности — количественное ограничение на время выполнения операции или потребление ресурсов.

Типы бюджетов:

  1. Временные бюджеты

    • время отклика (latency): 95-й перцентиль < 200 мс
    • время загрузки страницы: < 3 секунды
    • время запуска приложения: < 5 секунд
  2. Ресурсные бюджеты

    • потребление памяти: < 500 МБ на процесс
    • использование CPU: < 30% в среднем
    • размер пакета: < 2 МБ
  3. Сетевые бюджеты

    • количество запросов: < 10 на страницу
    • общий размер ресурсов: < 1.5 МБ

Пример определения бюджета в конфигурации:

{
"performanceBudget": {
"timeToInteractive": 3500,
"firstContentfulPaint": 1800,
"largestContentfulPaint": 2500,
"cumulativeLayoutShift": 0.1,
"totalBlockingTime": 200,
"resourceSizes": {
"total": 1572864,
"scripts": 524288,
"images": 786432
}
}
}

Пример мониторинга бюджета в коде:

public class PerformanceMonitor
{
private readonly Stopwatch _stopwatch = new Stopwatch();
private const long LatencyBudgetMs = 200;

public async Task<T> ExecuteWithMonitoring<T>(Func<Task<T>> operation)
{
_stopwatch.Restart();
var result = await operation();
_stopwatch.Stop();

var latency = _stopwatch.ElapsedMilliseconds;
if (latency > LatencyBudgetMs)
{
_logger.LogWarning(
"Превышен бюджет задержки: {Latency}мс > {Budget}мс",
latency,
LatencyBudgetMs
);
}

return result;
}
}