Масштабирование и лидерство

Перед началом: Модули 1-9. Инструменты: Yandex Cloud monitoring, auto-scaling. Время: ~4 ч.

Из M9: мы развернули инфраструктуру через Terraform с CI/CD. Теперь — что делать, когда нагрузка вырастет за пределы serverless.

Опциональный модуль — путь роста. Когда serverless-ядро перестаёт справляться или становится дороже VM, часть нагрузки выносят в Managed Kubernetes. Здесь же — load-тестирование, тюнинг Go, бюджетирование и tech-лидерские практики (RFC, архитектурное ревью).

🔥 Зачем это вообще нужно

Чат вырос до 100k пользователей. YDB упирается в лимиты, Functions не успевают холодным стартом, API Gateway отдаёт 503. Вы открываете Cloud Monitoring и видите: RPS ×10 за месяц. Что масштабировать первым? Где узкое место?

📖 Теория

Сигналы к переходу

Путь масштабирования: serverless ядро → Managed Kubernetes

Serverless остаётся ядром (Auth, Ingestion, YDB, Object Storage). В Kubernetes уходят части, требующие контроля над долгоживущими соединениями и высокой пропускной способности: WS-воркеры (pods), Kafka (вместо YMQ для high-throughput), Redis (горячий кэш).

Когда пора: p99 latency выше SLO; стоимость serverless превышает VM; исчерпан free-tier; много долгоживущих WebSocket-соединений с жёсткими требованиями к задержке.

Что уходит в Kubernetes, что остаётся

КомпонентРешение при масштабеПочему
WebSocket-воркерыK8s pods (Deployment + HPA)контроль над соединениями, graceful shutdown, оркестрация
Очередь high-throughputKafka (managed)replay, партиции, выше пропускная
Горячий кэшRedis (managed)автокомплит, сессии, rate-limit
Auth, Ingestion (CRUD)остаются в Cloud Functionsstateless, отлично масштабируются
YDB, Object Storageостаютсяserverless, не требуют переноса

Когда делить монолит на микросервисы

Не «сразу», а по границам: когда разные части меняются в разном темпе, требуют разного масштабирования или разных команд. Границы сервисов — по бизнес-возможностям (auth, rooms, messages, notifications), контракты — через явные API (REST/gRPC) с версионированием. Сначала — выделение WS-слоя, затем — разделение по доменам.

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

k6 — сценарии реальной нагрузки: подключение, отправка сообщения, автокомплит, загрузка медиа. Замеряем p95/p99, CPU, память, GC-паузы.

Тюнинг Go:

  • GOGC — как быстро растёт куча до GC (по умолчанию 100). Меньше → чаще GC, меньше память, больше CPU.
    bash
    GOGC=50  go run ./cmd/server   # чаще GC, меньше память, больше CPU
    GOGC=200 go run ./cmd/server   # реже GC, больше память, меньше CPU
  • GOMEMLIMIT — мягкий лимит памяти; ценится в контейнерах с жёстким limit.
    bash
    GOMEMLIMIT=4GiB GOGC=100 go run ./cmd/server
  • pprof — профиль CPU/памяти; не настраивайте GC «вслепую», сначала профиль.
    bash
    go test -cpuprofile cpu.prof -memprofile mem.prof ./...
    go tool pprof cpu.prof      # интерактивный анализ
    go tool pprof -http=:8080   # веб-интерфейс (flame graph)
  • Минимизируйте аллокации (переиспользование объектов, буферы вместо конкатенации); arena — для узких «горячих» участков.

k6 — нагрузочный скрипт (WebSocket-чат):

javascript
import ws from "k6/ws"
import { check } from "k6"

export const options = {
  stages: [
    { duration: "30s", target: 100 },   // плавный рост до 100 соединений
    { duration: "1m",  target: 100 },   // держим 100
    { duration: "30s", target: 500 },   // поднимаем до 500
    { duration: "1m",  target: 500 },   // держим 500
    { duration: "30s", target: 0 },     // плавно завершаем
  ],
}

export default function () {
  const url = `ws://localhost:8080/ws?token=${__ENV.TOKEN}`
  const res = ws.connect(url, {}, (socket) => {
    socket.on("open",  () => socket.send(JSON.stringify({ type: "message", text: "k6 test" })))
    socket.on("message", (data) => console.log(`recv: ${data}`))
    socket.setTimeout(() => socket.close(), 5000)
  })
  check(res, { "status is 101": (r) => r && r.status === 101 })
}

Docker-ресурсы: явные requests/limits CPU/памяти — чтобы планировщик K8s корректно размещал поды и HPA работал по метрикам.

Бюджет и планирование

  • HPA (Horizontal Pod Autoscaler) — автомасштабирование подов по CPU/RPS или кастомным метрикам.
  • Прогноз ресурсов: RPS × avg_duration — требуемая пропускная способность; закладывайте запас 20–30% под пики (утро/вечер).
  • Сравнивайте TCO: serverless (оплата за вызовы) vs Reserved instances/K8s (фиксированная стоимость) — при стабильной высокой нагрузке K8s часто дешевле.

Tech-лидерские практики

  • RFC — короткий документ на архитектурное решение (контекст, варианты, плюсы/минусы, выбор). Пример для курса: «RFC: переход с Serverless на Kubernetes для чата».
  • Архитектурное ревью — проверка решений на соответствие стандартам, рискам, стоимости.
  • Защита решений перед бизнесом — цифры (TCO, latency, SLO), а не «мне нравится».

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

  • Serverless → Managed Kubernetes: контроль над долгоживущими соединениями и пропускной способностью, но операционный overhead (эксплуатация кластера).
  • GOGC tuning: меньше → чаще GC, ниже память, выше CPU; больше → реже GC, выше память, ниже CPU.
  • Reserved instances/K8s vs serverless: фиксированная стоимость выгоднее при стабильной нагрузке; serverless — при пиковой/непредсказуемой.
  • Монолит → микросервисы: независимое масштабирование и деплой ↔ сложность контрактов и эксплуатации.

Практика

  • Написать RFC «Переход с Serverless на Kubernetes для чата»: 1 страница, 3 варианта, TCO, риски.
  • Провести нагрузочное тестирование k6 и предложить план масштабирования на 10× рост пользователей.
  • Снять профиль pprof горячего сценария и предложить оптимизацию аллокаций.

🔗 К следующему модулю: Система масштабируется. Но как за ней наблюдать?


🧩 Проверь себя

1. Cloud Functions concurrency — что задаёт?
2. Cold Start в Serverless Containers для Go — как...
3. Single-flight (dedup) при масштабировании — зачем?
4. Serverless Cloud Functions масштабируются...
5. Leader election в Go (graceful, в Container) — зачем playerName?

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

Система масштабируется. Но как за ней наблюдать? Метрики, логи, трейсы — и всё с trace_id.