Цели модуля

После модуля студент сможет:

  • Использовать pprof для CPU и memory profiling
  • Анализировать выполнение программы через go tool trace
  • Находить и устранять memory leaks
  • Оптимизировать CPU-затратные операции
  • Проводить нагрузочное тестирование с k6
  • Писать и интерпретировать benchmarks
  • Применять паттерны оптимизации (sync.Pool, object reuse)
  • Устанавливать performance SLO и мониторить деградации

9.1 Performance mindset

Преждевременная оптимизация

"Premature optimization is the root of all evil." — Donald Knuth

Правила:

  1. Сначала измерь, потом оптимизируй
  2. Оптимизируй bottleneck, не весь код
  3. Оптимизация должна быть обоснована метриками
  4. Простота и читаемость важнее микрооптимизаций

Когда оптимизировать

Оптимизация нужна когда:

  • SLO по latency нарушается
  • Ресурсы (CPU/memory) на пределе
  • Стоимость инфраструктуры растёт
  • Пользователи жалуются на медленную работу

Оптимизация НЕ нужна когда:

  • Код медленный, но SLO выполняется
  • Оптимизация усложняет код без измеримой пользы
  • Нет профилирования (оптимизация вслепую)

Цикл оптимизации

1. Измерь (profile, benchmark)
2. Найди bottleneck
3. Оптимизируй bottleneck
4. Измерь снова
5. Повтори если нужно

Без измерения оптимизация — гадание.

Performance SLO

Определите целевые показатели:

МетрикаЦельКак измерить
p50 latency< 50mshistogram_quantile(0.5, ...)
p99 latency< 200mshistogram_quantile(0.99, ...)
Throughput> 1000 RPSrate(http_requests_total[5m])
CPU usage< 70%container_cpu_usage_seconds_total
Memory usage< 512MBcontainer_memory_usage_bytes
GC pause< 1msgo_gc_duration_seconds

9.2 Profiling с pprof

Что такое pprof

pprof — инструмент профилирования в Go. Позволяет увидеть:

  • Какие функции потребляют больше всего CPU
  • Какие функции аллоцируют больше всего памяти
  • Где происходят блокировки (mutex, block)
  • Сколько горутин запущено и где они заблокированы

Типы профилей

ПрофильЧто показывает
cpuИспользование CPU по функциям
heapАллокации памяти (текущие)
allocsВсе аллокации за время работы
goroutineСтек всех горутин
mutexКонкуренция за мьютексы
blockБлокировки на каналах/мьютексах
threadcreateСоздание потоков ОС

Подключение pprof к HTTP серверу

go
import _ "net/http/pprof"

func main() {
    // pprof endpoints на отдельном порту (безопасность!)
    go func() {
        log.Println(http.ListenAndServe(":6060", nil))
    }()

    // Основной сервер на :8080
    // ...
}

Это добавляет эндпоинты:

  • /debug/pprof/ — индекс
  • /debug/pprof/profile — CPU профиль
  • /debug/pprof/heap — heap профиль
  • /debug/pprof/goroutine — горутины
  • /debug/pprof/mutex — mutex профиль
  • /debug/pprof/block — block профиль

ВАЖНО: не expose pprof на публичный порт. Используйте отдельный внутренний порт или аутентификацию.

Сбор CPU профиля

bash
# Через HTTP endpoint (30 секунд профилирования)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Сохранить в файл
curl -o cpu.prof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof cpu.prof

Сбор heap профиля

bash
# Текущий heap
curl -o heap.prof http://localhost:6060/debug/pprof/heap
go tool pprof heap.prof

# Все аллокации
curl -o allocs.prof http://localhost:6060/debug/pprof/allocs
go tool pprof allocs.prof

Интерактивный режим pprof

bash
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

(pprof) top          # Топ функций по CPU
(pprof) top 20       # Топ 20 функций
(pprof) list FuncName # Показать код функции с CPU
(pprof) web          # Открыть граф вызовов в браузере
(pprof) peek FuncName # Кто вызывает функцию
(pprof) tree         # Дерево вызовов

Пример вывода top

(pprof) top
Showing nodes accounting for 5.23s, 85.4% of 6.12s total

  flat  flat%   sum%        cum   cum%
 2.15s 35.13% 35.13%      2.15s 35.13%  encoding/json.(*Encoder).Encode
 1.23s 20.10% 55.23%      1.23s 20.10%  github.com/jackc/pgx/v5.(*Conn).Query
 0.89s 14.54% 69.77%      0.89s 14.54%  runtime.mallocgc
 0.56s  9.15% 78.92%      0.56s  9.15%  sync.(*Pool).Get

Чтение:

  • flat: время в самой функции
  • cum: время в функции + вызванных из неё
  • flat%: процент от общего времени

Визуализация через web UI

bash
# Веб-интерфейс pprof
go tool pprof -http=:8081 cpu.prof

Откроется браузер с:

  • Flame graph (огненный граф)
  • Graph (граф вызовов)
  • Top (топ функций)
  • Source (код с аннотациями)

Профилирование в production

Для production используйте непрерывный profiling:

  • Pyroscope (Grafana)
  • Parca
  • Datadog Continuous Profiler
go
import "github.com/grafana/pyroscope-go"

func main() {
    pyroscope.Start(pyroscope.Config{
        ApplicationName: "git-server",
        ServerAddress:   "http://pyroscope:4040",
    })
    // ...
}

9.3 go tool trace

Что такое trace

go tool trace показывает выполнение программы во времени:

  • Горутины и их состояние (running, blocked, waiting)
  • Системные вызовы
  • GC паузы
  • Сетевые операции
  • Планировщик горутин

Когда использовать trace

  • Диагностика latency (почему запрос медленный)
  • Поиск гонок и блокировок
  • Анализ работы планировщика
  • Понимание параллелизма

Сбор trace

Через HTTP endpoint

bash
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5

Через код

go
import "runtime/trace"

func main() {
    f, _ := os.Create("trace.out")
    trace.Start(f)
    defer trace.Stop()

    // Код для профилирования
}

Для тестов

bash
go test -trace=trace.out ./...

Просмотр trace

bash
go tool trace trace.out

Откроется веб-интерфейс с:

  • View trace: временная диаграмма всех горутин
  • Goroutine analysis: анализ горутин
  • Network/Sync/Syscall blocking profiles
  • Scheduler latency profile

Чтение trace

Временная диаграмма показывает:

  • Ось X: время
  • Ось Y: процессоры (P) и горутины (G)
  • Цвета: running (зелёный), blocked (красный), waiting (жёлтый)

Ищем:

  • Длинные блокировки (красные участки)
  • Простой процессоров (пустые участки)
  • GC паузы (серые участки)
  • Network blocking (долгие сетевые вызовы)

9.4 Memory leaks диагностика

Что такое memory leak в Go

Memory leak — объекты остаются в памяти потому что на них есть ссылки, хотя они больше не нужны.

Частые причины:

  1. Забытые горутины (goroutine leak)
  2. Глобальные мапы которые только растут
  3. Незакрытые ресурсы (connections, files)
  4. Кэши без eviction policy
  5. Подписки без отписки

Goroutine leak

Горутина заблокирована навсегда:

go
// УТЕЧКА: горутина ждёт на канале без писателя
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch  // заблокирована навсегда
        process(val)
    }()
    // выходим из функции, горутина осталась
}

Диагностика:

bash
# Количество горутин растёт со временем
curl http://localhost:6060/debug/pprof/goroutine?debug=1

# Или метрика
go_goroutines  # должно быть стабильным

Исправление:

go
// ПРАВИЛЬНО: используем context для отмены
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            process(val)
        case <-ctx.Done():
            return  // выходим при отмене
        }
    }()
}

Глобальные мапы без очистки

go
// УТЕЧКА: мапа только растёт
var cache = make(map[string]*Data)

func AddToCache(key string, data *Data) {
    cache[key] = data  // никогда не удаляется!
}

Исправление:

go
// ПРАВИЛЬНО: кэш с TTL и eviction
type Cache struct {
    mu    sync.Mutex
    items map[string]CacheItem
    ttl   time.Duration
}

type CacheItem struct {
    Data      *Data
    ExpiresAt time.Time
}

func (c *Cache) Cleanup() {
    c.mu.Lock()
    defer c.mu.Unlock()
    now := time.Now()
    for key, item := range c.items {
        if now.After(item.ExpiresAt) {
            delete(c.items, key)
        }
    }
}

Незакрытые ресурсы

go
// УТЕЧКА: connection не закрыт
func query() {
    conn := pool.Acquire(ctx)
    conn.Query(ctx, "SELECT ...")
    // забыли conn.Release()!
}

Исправнение:

go
// ПРАВИЛЬНО: defer для закрытия
func query(ctx context.Context) error {
    conn, err := pool.Acquire(ctx)
    if err != nil {
        return err
    }
    defer conn.Release()

    _, err = conn.Query(ctx, "SELECT ...")
    return err
}

Диагностика memory leak

  1. Мониторинг memory usage:
# Метрика
process_resident_memory_bytes{job="myapp"}

# Должно быть стабильным. Если растёт — leak.
  1. Heap профиль:
bash
# Снимаем heap профиль в начале
   curl -o heap1.prof http://localhost:6060/debug/pprof/heap

   # Ждём 30 минут, снимаем снова
   curl -o heap2.prof http://localhost:6060/debug/pprof/heap

   # Сравниваем
   go tool pprof -diff_base=heap1.prof heap2.prof
  1. Goroutine профиль:
bash
# Если количество горутин растёт — goroutine leak
   curl http://localhost:6060/debug/pprof/goroutine?debug=1 | head -5

   # goroutine profile: version 1, 1543 goroutines

9.5 CPU optimization

Типичные CPU bottlenecks

  1. Сериализация/десериализация JSON
  2. Регулярные выражения
  3. Шифрование/хеширование
  4. Аллокации памяти (GC overhead)
  5. Логирование

Оптимизация JSON

go
// ПЛОХО: encoding/json медленный для hot paths
data, err := json.Marshal(obj)

// БЫСТРЕЕ: pre-allocated буфер
buf := make([]byte, 0, 1024)
encoder := json.NewEncoder(bytes.NewBuffer(buf))
err := encoder.Encode(obj)

// ЕЩЁ БЫСТРЕЕ: sonic, jsoniter (внешние библиотеки)
import "github.com/bytedance/sonic"
data, err := sonic.Marshal(obj)

Оптимизация регулярных выражений

go
// ПЛОХО: компиляция regex на каждый вызов
func validate(email string) bool {
    re := regexp.MustCompile(`^[a-z]+@[a-z]+\.[a-z]+$`)
    return re.MatchString(email)
}

// ПРАВИЛЬНО: компиляция один раз
var emailRegex = regexp.MustCompile(`^[a-z]+@[a-z]+\.[a-z]+$`)

func validate(email string) bool {
    return emailRegex.MatchString(email)
}

Оптимизация строк

go
// ПЛОХО: конкатенация в цикле (O(n^2) аллокаций)
result := ""
for _, s := range items {
    result += s
}

// ПРАВИЛЬНО: strings.Builder
var builder strings.Builder
builder.Grow(estimatedSize)  // pre-allocate
for _, s := range items {
    builder.WriteString(s)
}
result := builder.String()

Оптимизация аллокаций

go
// ПЛОХО: аллокация на каждый запрос
func handler(w http.ResponseWriter, r *http.Request) {
    buf := make([]byte, 4096)  // аллокация
    // ...
}

// ПРАВИЛЬНО: sync.Pool для переиспользования
var bufPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 4096)
    },
}

func handler(w http.ResponseWriter, r *http.Request) {
    buf := bufPool.Get().([]byte)
    defer bufPool.Put(buf)
    // ...
}

Оптимизация логирования

go
// ПЛОХО: логирование в hot path с форматированием
for _, item := range items {
    log.Printf("processing item %s with data %v", item.ID, item.Data)
}

// ПРАВИЛЬНО: проверяем уровень перед логированием
if logger.IsEnabled(slog.LevelDebug) {
    for _, item := range items {
        logger.Debug("processing item", "id", item.ID)
    }
}

9.6 Нагрузочное тестирование с k6

Что такое k6

k6 — инструмент нагрузочного тестирования. Пишется на JavaScript, запускается как Go бинарник.

Установка:

bash
# macOS
brew install k6

# Linux
sudo apt install k6

# Docker
docker pull grafana/k6

Базовый тест

js
// k6/basic-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    vus: 50,             // 50 виртуальных пользователей
    duration: '30s',     // в течение 30 секунд
};

export default function () {
    const res = http.get('http://localhost:8080/api/v1/repos');

    check(res, {
        'status is 200': (r) => r.status === 200,
        'response time < 200ms': (r) => r.timings.duration < 200,
    });

    sleep(1);
}

Запуск:

bash
k6 run k6/basic-test.js

Ramp-up тест

js
// k6/rampup-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    stages: [
        { duration: '1m', target: 50 },    // Ramp up до 50 VU за 1 минуту
        { duration: '3m', target: 50 },    // Держим 50 VU 3 минуты
        { duration: '1m', target: 200 },   // Ramp up до 200 VU
        { duration: '3m', target: 200 },   // Держим 200 VU 3 минуты
        { duration: '1m', target: 0 },     // Ramp down до 0
    ],
    thresholds: {
        http_req_duration: ['p(99)<500'],   // p99 < 500ms
        http_req_failed: ['rate<0.01'],     // < 1% ошибок
    },
};

export default function () {
    const res = http.get('http://localhost:8080/api/v1/repos');

    check(res, {
        'status is 200': (r) => r.status === 200,
    });

    sleep(1);
}

Stress тест (поиск предела)

js
// k6/stress-test.js
import http from 'k6/http';

export const options = {
    stages: [
        { duration: '2m', target: 100 },
        { duration: '2m', target: 500 },
        { duration: '2m', target: 1000 },
        { duration: '2m', target: 2000 },   // Ищем где ломается
        { duration: '5m', target: 2000 },
    ],
};

export default function () {
    http.get('http://localhost:8080/api/v1/repos');
}

Spike тест (пиковая нагрузка)

js
// k6/spike-test.js
import http from 'k6/http';

export const options = {
    stages: [
        { duration: '10s', target: 100 },   // Нормальная нагрузка
        { duration: '10s', target: 100 },
        { duration: '5s', target: 5000 },   // SPIKE!
        { duration: '10s', target: 5000 },
        { duration: '10s', target: 100 },   // Возврат к норме
    ],
};

export default function () {
    http.get('http://localhost:8080/api/v1/repos');
}

Тест с POST запросами

js
// k6/create-test.js
import http from 'k6/http';
import { check } from 'k6';

export const options = {
    vus: 20,
    duration: '1m',
};

export default function () {
    const payload = JSON.stringify({
        name: 'test-repo-' + __VU + '-' + __ITER,
        owner_id: 'user-123',
        is_private: false,
    });

    const params = {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer test-token',
        },
    };

    const res = http.post(
        'http://localhost:8080/api/v1/repos',
        payload,
        params
    );

    check(res, {
        'status is 201': (r) => r.status === 201,
        'has id': (r) => JSON.parse(r.body).id !== undefined,
    });
}

Интерпретация результатов k6

http_req_duration..............: avg=45.2ms  min=12ms  med=38ms  max=450ms
    p(90)=78ms  p(95)=120ms  p(99)=250ms
http_req_failed................: 0.5%
http_reqs......................: 15234  253.9/s
vus............................: 50

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

  • http_req_duration: распределение задержек
  • http_req_failed: процент ошибок
  • http_reqs: throughput (запросов в секунду)
  • vus: количество виртуальных пользователей

Thresholds (критерии прохождения)

js
export const options = {
    thresholds: {
        // p99 latency < 500ms
        http_req_duration: ['p(99)<500'],

        // Error rate < 1%
        http_req_failed: ['rate<0.01'],

        // Throughput > 1000 RPS
        http_reqs: ['rate>1000'],

        // p95 < 200ms для конкретного эндпоинта
        'http_req_duration{endpoint:/api/v1/repos}': ['p(95)<200'],
    },
};

Если threshold не выполнен, k6 возвращает non-zero exit code. Используйте в CI для автоматической проверки.

k6 в CI

yaml
// .github/workflows/load-test.yml
name: Load Test

on:
  schedule:
    - cron: '0 2 * * 1'  # Каждый понедельник в 2:00

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start service
        run: docker compose up -d

      - name: Wait for service
        run: sleep 10

      - name: Run k6
        uses: grafana/k6-action@v0.3.0
        with:
          filename: k6/rampup-test.js

      - name: Cleanup
        run: docker compose down

---

## 9.7 Benchmarks в Go

### Написание бенчмарков

```go
// internal/service/repo_service_bench_test.go
package service

import (
    "testing"
)

func BenchmarkRepoService_GetRepo(b *testing.B) {
    // Setup
    mockRepo := NewMockRepoRepository(b)
    svc := NewRepoService(mockRepo)
    ctx := context.Background()

    // Reset timer после setup
    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        svc.GetRepo(ctx, "repo-123")
    }
}

func BenchmarkJSONMarshal(b *testing.B) {
    data := &Repo{
        ID:   "repo-123",
        Name: "test-repo",
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        json.Marshal(data)
    }
}

Запуск бенчмарков

bash
# Все бенчмарки в пакете
go test -bench=. ./internal/service/

# Конкретный бенчмарк
go test -bench=BenchmarkRepoService_GetRepo ./internal/service/

# С profiling
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof ./internal/service/

# Больше итераций для точности
go test -bench=. -benchtime=10s ./internal/service/

Чтение результатов

text
BenchmarkRepoService_GetRepo-8    5000000    250 ns/op    128 B/op    2 allocs/op

Чтение:

  • -8: GOMAXPROCS (количество CPU)
  • 5000000: количество итераций
  • 250 ns/op: среднее время на операцию
  • 128 B/op: байт аллоцировано на операцию
  • 2 allocs/op: количество аллокаций на операцию

Сравнение бенчмарков

Используйте benchstat для сравнения:

bash
# Запускаем до оптимизации
go test -bench=. -count=10 ./... > old.txt

# Запускаем после оптимизации
go test -bench=. -count=10 ./... > new.txt

# Сравниваем
go install golang.org/x/perf/cmd/benchstat@latest
benchstat old.txt new.txt

Результат:

text
name                        old time/op  new time/op  delta
RepoService_GetRepo-8        250ns ± 5%   180ns ± 3%  -28.0%  (p=0.000)

9.8 Optimization patterns

sync.Pool

Переиспользование объектов для снижения аллокаций:

go
var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process(data []byte) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()

    buf.Write(data)
    // ... обработка
    return buf.String()
}

Когда использовать:

  • Создание объекта дорогое
  • Объект используется многократно
  • Объект можно переиспользовать после Reset

Когда НЕ использовать:

  • Объект содержит состояние которое нельзя сбросить
  • Объект используется только один раз

Pre-allocation

go
// ПЛОХО: append растёт массив динамически
result := make([]Item, 0)
for _, item := range items {
    result = append(result, transform(item))
}

// ПРАВИЛЬНО: pre-allocate если знаем размер
result := make([]Item, 0, len(items))
for _, item := range items {
    result = append(result, transform(item))
}

Избегание лишних аллокаций

go
// ПЛОХО: передача по значению копирует большую структуру
func process(data LargeStruct) { ... }

// ПРАВИЛЬНО: передача по указателю
func process(data *LargeStruct) { ... }

// ПЛОХО: string([]byte) создаёт копию
s := string(bytes)

// ПРАВИЛЬНО: unsafe conversion (осторожно!)
s := unsafe.String(&bytes[0], len(bytes))

Кэширование

go
type CachedService struct {
    underlying Service
    cache      *Cache
}

func (s *CachedService) GetRepo(ctx context.Context, id string) (*Repo, error) {
    // Проверяем кэш
    if cached, ok := s.cache.Get("repo:" + id); ok {
        return cached.(*Repo), nil
    }

    // Кэш miss — идём в БД
    repo, err := s.underlying.GetRepo(ctx, id)
    if err != nil {
        return nil, err
    }

    // Сохраняем в кэш
    s.cache.Set("repo:"+id, repo, 5*time.Minute)

    return repo, nil
}

Batch operations

go
// ПЛОХО: N отдельных запросов
for _, id := range ids {
    repo, _ := repoRepo.GetByID(ctx, id)
}

// ПРАВИЛЬНО: один batch запрос
repos, _ := repoRepo.GetByIDs(ctx, ids)

// Реализация
func (r *RepoRepository) GetByIDs(ctx context.Context, ids []string) ([]*Repo, error) {
    query := "SELECT ... FROM repos WHERE id = ANY($1)"
    rows, err := r.pool.Query(ctx, query, ids)
    // ...
}

Parallelism

go
// ПЛОХО: последовательная обработка
for _, item := range items {
    process(item)
}

// ПРАВИЛЬНО: параллельная обработка с errgroup
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)  // максимум 10 параллельных

for _, item := range items {
    item := item
    g.Go(func() error {
        return process(ctx, item)
    })
}

return g.Wait()

Избегание lock contention

go
// ПЛОХО: один мьютекс на всё
type Cache struct {
    mu    sync.Mutex
    items map[string]interface{}
}

// ПРАВИЛЬНО: segmented cache (шардирование)
type ShardedCache struct {
    shards [256]CacheShard
}

type CacheShard struct {
    mu    sync.RWMutex
    items map[string]interface{}
}

func (c *ShardedCache) getShard(key string) *CacheShard {
    hash := fnv32(key)
    return &c.shards[hash%256]
}

Привязка к шаблону проекта

Где performance в шаблоне

k6/
├── bulkhead.js              # Нагрузочный тест bulkhead
└── cache-stampede.js        # Тест cache stampede

internal/observability/metrics/
└── metrics.go               # Метрики для мониторинга производительности

Что изучаем на примере шаблона

  1. k6 тесты для нагрузочного тестирования
  2. Метрики latency и throughput
  3. Мониторинг GC
  4. Connection pool метрики

Задание: изучите k6/

  • Какие сценарии тестируются?
  • Какие thresholds установлены?
  • Как интерпретировать результаты?

💱 Компромиссы

  • sync.Pool vs прямые аллокации: переиспользование буферов снимает нагрузку с GC на hot path, но усложняет код и требует аккуратного Reset; обычные аллокации читаемее, однако платят allocs/op и паузами сборщика мусора.
  • encoding/json vs sonic: стандартная библиотека идёт без внешних зависимостей, но медленна на hot paths; sonic/jsoniter быстрее, однако тянут зависимость и могут разойтись с stdlib в поведении.
  • Continuous profiling vs разовый pprof: Pyroscope в production ловит редкие деградации с полным контекстом, но постоянно платит оверхедом CPU и хранением профилей; профиль по запросу бесплатен, однако проблема к моменту сбора уже прошла.
  • Stress/spike vs load-тесты: разгон до 2000 VU находит предел и bottleneck, но может уронить staging и испортить его данные; щадящий load-профиль безопасен, однако возможности сервиса остаются невыясненными.
  • Кэш с TTL vs чтение из БД: кэш на 5 минут резко снижает latency и нагрузку на БД, но до истечения TTL отдаёт устаревшие данные; прямое чтение всегда актуально, однако платит каждым запросом в базу.

Практическое задание

Задание 1: CPU profiling

Подключите pprof к вашему сервису:

go
import _ "net/http/pprof"

go func() {
    http.ListenAndServe(":6060", nil)
}()

Соберите CPU профиль под нагрузкой:

bash
# Запускаем нагрузку
k6 run k6/basic-test.js &

# Собираем профиль
curl -o cpu.prof http://localhost:6060/debug/pprof/profile?seconds=30

# Анализируем
go tool pprof -http=:8081 cpu.prof

Найдите топ-3 CPU-затратных функции.

Задание 2: Memory profiling

Соберите heap профиль:

bash
curl -o heap.prof http://localhost:6060/debug/pprof/heap
go tool pprof -http=:8082 heap.prof

Найдите функции с наибольшими аллокациями.

Задание 3: Goroutine leak detection

Проверьте количество горутин под нагрузкой:

bash
# До нагрузки
curl http://localhost:6060/debug/pprof/goroutine?debug=1 | head -5

# После 10 минут нагрузки
curl http://localhost:6060/debug/pprof/goroutine?debug=1 | head -5

Если количество выросло — найдите утечку.

Задание 4: Trace analysis

Соберите trace:

bash
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out

Найдите:

  • Долгие блокировки
  • GC паузы
  • Простой процессоров

Задание 5: k6 load test

Напишите нагрузочный тест:

js
// k6/load-test.js
export const options = {
    stages: [
        { duration: '1m', target: 50 },
        { duration: '3m', target: 50 },
        { duration: '1m', target: 200 },
        { duration: '3m', target: 200 },
    ],
    thresholds: {
        http_req_duration: ['p(99)<500'],
        http_req_failed: ['rate<0.01'],
    },
};

Запустите и проанализируйте результаты.

Задание 6: Stress test

Найдите предел вашего сервиса:

js
// k6/stress-test.js
export const options = {
    stages: [
        { duration: '2m', target: 100 },
        { duration: '2m', target: 500 },
        { duration: '2m', target: 1000 },
        { duration: '2m', target: 2000 },
    ],
};

Определите:

  • Максимальный RPS до деградации
  • Bottleneck (CPU, memory, DB, network)
  • Рекомендации по масштабированию

Задание 7: Benchmarks

Напишите бенчмарки для критичных функций:

go
func BenchmarkRepoService_GetRepo(b *testing.B)
func BenchmarkJSONMarshal(b *testing.B)
func BenchmarkRepoRepository_List(b *testing.B)

Запустите и запишите baseline.

Задание 8: Оптимизация

Выберите одну CPU-затратную функцию из profiling:

  1. Запишите baseline бенчмарк
  2. Оптимизируйте (sync.Pool, pre-allocation, кэширование)
  3. Запустите бенчмарк снова
  4. Сравните через benchstat
  5. Убедитесь что тесты проходят

Задание 9: Performance SLO

Определите performance SLO для вашего сервиса:

text
p50 latency: < 50ms
p99 latency: < 200ms
Throughput: > 1000 RPS
CPU usage: < 70%

Добавьте метрики и алерты для мониторинга.


Quiz "Проверь себя"

1. Как правильно подойти к оптимизации?
2. Как обнаружить goroutine leak?
3. Что показывает flame graph?
4. Как найти CPU bottleneck через pprof?
5. Что такое thresholds в k6?
6. Чем heap профиль отличается от allocs профиля?
7. Какие типы нагрузочных тестов существуют? — выберите все подходящие
8. Что означает эта строка вывода бенчмарка?
text
BenchmarkRepoService_GetRepo-8    5000000    250 ns/op    128 B/op    2 allocs/op
9. Когда уместен sync.Pool?
go
var bufPool = sync.Pool{
    New: func() interface{} { return new(bytes.Buffer) },
}

✅ Итоги модуля

Студент должен:

  • Использовать pprof для CPU и memory profiling
  • Анализировать выполнение через go tool trace
  • Находить и устранять memory leaks
  • Оптимизировать CPU-затратные операции
  • Писать и запускать k6 тесты (load, stress, spike)
  • Писать и интерпретировать benchmarks
  • Применять sync.Pool, pre-allocation, кэширование
  • Устанавливать performance SLO
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 10

Следующий модуль: Модуль 10. CI/CD and Delivery - test pipeline design, security scanning, build artifacts, containerization, infrastructure as code, deployment automation with Terraform / Ansible, GitOps workflow. Привязка к .github/workflows/ и deploy/ шаблона.