IT-аутсорсинг и консалтинг

Надёжная IT-инфраструктура для вашего бизнеса

Проектируем, строим и поддерживаем облачные решения, приложения и сервисы. От стартапа до энтерпрайза.

150+
проектов
8 лет
на рынке
99.9%
uptime
40+
специалистов

Наши услуги

Полный спектр IT-решений для компаний любого масштаба

Облачная инфраструктура

Проектирование и миграция в облако. AWS, GCP, частные облака. Kubernetes-кластеры, отказоустойчивость и автоскейлинг.

⟨/⟩

Веб-разработка

Fullstack-разработка веб-приложений любой сложности. React, Vue, Node.js, Go. От MVP до high-load систем.

🔒

Информационная безопасность

Аудит безопасности, пентесты, внедрение Zero Trust. Защита данных и соответствие требованиям регуляторов.

📊

Аналитика и BI

Построение дата-платформ, ETL-пайплайны, дашборды и отчёты. ClickHouse, Apache Spark, Metabase.

DevOps и SRE

CI/CD-пайплайны, Infrastructure as Code, мониторинг и алертинг. Сокращение Time to Market в разы.

📱

Мобильная разработка

Нативная и кросс-платформенная разработка. iOS, Android, Flutter. Интеграция с backend и аналитикой.

MaxTimes — команда профессионалов

Технологии, которым доверяют лидеры рынка

Мы — команда инженеров, архитекторов и разработчиков с опытом работы в высоконагруженных системах. Наши клиенты — компании из секторов ФИНТЕХ, РИТЕЙЛ, МЕДИА, ТЕЛЕКОМ и EDTECH.

  • Прозрачные процессы и Agile-методология
  • Выделенная команда под каждый проект
  • SLA 99.9% на все инфраструктурные решения
  • Техническая поддержка 24/7
  • Опыт работы с нагрузками 1M+ RPM

Блог

Делимся опытом, кейсами и лучшими практиками

DevOps
28 марта 2026

Миграция с Docker Compose на Kubernetes: пошаговый гайд

Разбираем поэтапный процесс миграции сервисов из Docker Compose в Kubernetes-кластер.

Читать →
Безопасность
22 марта 2026

Zero Trust в 2026: почему VPN уже недостаточно

Анализируем концепцию Zero Trust и почему традиционные подходы к безопасности устарели.

Читать →
Инфраструктура
15 марта 2026

PostgreSQL vs ClickHouse: когда что выбирать

Сравниваем две СУБД для разных сценариев: OLTP, аналитика, логи и метрики.

Читать →
Разработка
8 марта 2026

Как мы ускорили CI/CD с 40 до 4 минут

Практический кейс оптимизации пайплайна: кеширование, параллелизация и умные триггеры.

Читать →
Кейс
1 марта 2026

Масштабирование e-commerce до 1M заказов в сутки

Как мы перестроили архитектуру интернет-магазина для обработки пиковых нагрузок.

Читать →
AI/ML
20 февраля 2026

Внедрение LLM в корпоративные процессы: практика

Реальные кейсы использования больших языковых моделей в бизнесе.

Читать →
IaC
14 февраля 2026

Terraform: 10 best practices для production

Лучшие практики для управления инфраструктурой с Terraform в продакшн-среде.

Читать →
Мониторинг
8 февраля 2026

Prometheus и Alertmanager: настройка алертинга с нуля

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

Читать →
Архитектура
1 февраля 2026

gRPC vs REST: когда выбрать gRPC

Детальное сравнение двух подходов к проектированию API с примерами.

Читать →
Базы данных
25 января 2026

Redis: паттерны кэширования для высоких нагрузок

Рассматриваем стратегии кэширования: Cache-Aside, Write-Through и другие.

Читать →
Инфраструктура
18 января 2026

Nginx: тонкая настройка для 100K+ RPS

Оптимизация Nginx для обработки высоких нагрузок: воркеры, буферы и кеширование.

Читать →
Безопасность
12 января 2026

Linux hardening: чеклист безопасности сервера

Комплексный чеклист для защиты Linux-сервера от типовых угроз.

Читать →
DevOps
5 января 2026

GitOps: полный workflow с Flux и Kustomize

Настраиваем полный GitOps-цикл: от коммита до автоматического деплоя.

Читать →
SRE
28 декабря 2025

Disaster Recovery: стратегии и автоматизация

Планирование аварийного восстановления: RPO, RTO и автоматизация фейловера.

Читать →
Cloud
21 декабря 2025

Оптимизация затрат в AWS: экономим 50% бюджета

Практические методы сокращения расходов на облачную инфраструктуру.

Читать →
Архитектура
14 декабря 2025

Service Mesh: Istio vs Linkerd в production

Сравнение двух популярных Service Mesh решений на реальных сценариях.

Читать →
Базы данных
7 декабря 2025

Шардирование PostgreSQL: горизонтальное масштабирование

Подходы к шардированию PostgreSQL: Citus, ручное шардирование и pgpool.

Читать →
Архитектура
30 ноября 2025

Event-driven архитектура на Apache Kafka

Проектирование событийно-ориентированных систем с Apache Kafka.

Читать →
Мониторинг
23 ноября 2025

Observability: метрики, логи и трейсы в одном стеке

Строим полноценный observability-стек: Prometheus, Loki, Tempo и Grafana.

Читать →
QA
16 ноября 2025

Нагрузочное тестирование с k6: от скрипта до отчёта

Полное руководство по нагрузочному тестированию с помощью k6.

Читать →

Отзывы клиентов

Что говорят о нас наши партнёры

«MaxTimes помогли нам мигрировать в облако за 3 месяца без простоев. Команда профессионалов, которые понимают бизнес-задачи и предлагают оптимальные технические решения.»

ЕК
Евгений Краснов
CTO, компания из сектора ФИНТЕХ

«Благодаря MaxTimes наш CI/CD-пайплайн сократился с 45 минут до 5. Внедрили мониторинг, настроили алертинг — теперь мы реагируем на инциденты до того, как они влияют на пользователей.»

АЛ
Анна Лебедева
VP of Engineering, компания из сектора РИТЕЙЛ

«Работаем с MaxTimes уже 3 года. Они полностью управляют нашей инфраструктурой в Kubernetes. SLA соблюдается безупречно, коммуникация прозрачная.»

ДМ
Дмитрий Михайлов
Технический директор, компания из сектора МЕДИА

Готовы обсудить проект?

Свяжитесь с нами, и мы подберём оптимальное решение для вашего бизнеса

📧 info@maxtimes.ru

📞 +7 (495) 123-45-67

Написать нам
← Назад к блогу

Миграция с Docker Compose на Kubernetes: пошаговый гайд

Docker Compose — отличный инструмент для локальной разработки и небольших проектов. Однако по мере роста системы возникает необходимость в оркестрации контейнеров на продакшн-уровне. Kubernetes предоставляет автоматическое масштабирование, самовосстановление, rolling updates и множество других возможностей, которых нет в Docker Compose. В этой статье мы пошагово разберём процесс миграции реального проекта.

Почему стоит мигрировать

Docker Compose управляет контейнерами на одном хосте. Это означает, что при падении сервера вся система становится недоступной. Kubernetes решает эту проблему, распределяя нагрузку между несколькими узлами кластера. Помимо отказоустойчивости, Kubernetes предоставляет декларативное управление конфигурацией, встроенный service discovery, управление секретами и автоматическое горизонтальное масштабирование.

Типичные причины миграции: необходимость масштабирования на несколько серверов, требования к высокой доступности (HA), потребность в rolling updates без простоя, желание использовать экосистему Kubernetes (Helm, Operators, Service Mesh) и стандартизация деплоя в компании.

Подготовка: анализ текущего docker-compose.yml

Начнём с типичного docker-compose.yml, который содержит веб-приложение, API-сервер, базу данных PostgreSQL и Redis для кеширования. Для каждого сервиса нам нужно определить: образ и тег, переменные окружения, тома (volumes), порты, зависимости между сервисами и ресурсные лимиты.

version: '3.8'
services:
  web:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - API_URL=http://api:8080
    depends_on:
      - api

  api:
    build: ./backend
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app

  cache:
    image: redis:7-alpine

volumes:
  pgdata:

Шаг 1: Создание Docker-образов

В Docker Compose часто используется директива build для сборки образов из Dockerfile. В Kubernetes образы должны быть предварительно собраны и размещены в container registry. Можно использовать Docker Hub, GitHub Container Registry (ghcr.io), GitLab Container Registry или собственный registry.

# Сборка и публикация образов
docker build -t registry.example.com/myapp/frontend:v1.0.0 ./frontend
docker build -t registry.example.com/myapp/backend:v1.0.0 ./backend

docker push registry.example.com/myapp/frontend:v1.0.0
docker push registry.example.com/myapp/backend:v1.0.0

Важно использовать конкретные теги версий (например, v1.0.0), а не latest. Это обеспечивает воспроизводимость деплоев и возможность отката к предыдущей версии.

Шаг 2: Namespace и секреты

Первым делом создадим namespace для изоляции ресурсов нашего приложения и Kubernetes Secret для хранения чувствительных данных (пароли, строки подключения).

apiVersion: v1
kind: Namespace
metadata:
  name: myapp
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: myapp
type: Opaque
stringData:
  database-url: "postgres://user:pass@db-svc:5432/app"
  redis-url: "redis://cache-svc:6379"

Шаг 3: Преобразование сервисов в Deployments

Каждый сервис из docker-compose.yml превращается в пару Deployment + Service в Kubernetes. Deployment управляет подами (replicas), а Service обеспечивает сетевой доступ к ним. Рассмотрим на примере API-сервера.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: registry.example.com/myapp/backend:v1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: redis-url
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
  name: api-svc
  namespace: myapp
spec:
  selector:
    app: api
  ports:
  - port: 8080
    targetPort: 8080

Обратите внимание на несколько ключевых отличий от Docker Compose. Мы явно указываем replicas: 3 для запуска трёх экземпляров API-сервера. Переменные окружения берутся из Secret, а не задаются напрямую. Добавлены readinessProbe и livenessProbe для проверки здоровья контейнера. Указаны resource requests и limits для планирования ресурсов кластера.

Шаг 4: StatefulSet для базы данных

Для stateful-сервисов, таких как PostgreSQL, используется StatefulSet вместо Deployment. StatefulSet обеспечивает стабильные сетевые идентификаторы, упорядоченный запуск и завершение подов, а также привязку к Persistent Volumes. Каждый под получает свой PersistentVolumeClaim, который сохраняется при рестарте.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
  namespace: myapp
spec:
  serviceName: db-svc
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_USER
          value: user
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: postgres-password
        - name: POSTGRES_DB
          value: app
        volumeMounts:
        - name: pgdata
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            cpu: 250m
            memory: 256Mi
          limits:
            cpu: "1"
            memory: 1Gi
  volumeClaimTemplates:
  - metadata:
      name: pgdata
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 20Gi

Шаг 5: Ingress для внешнего доступа

В Docker Compose мы просто пробрасывали порты на хост. В Kubernetes для маршрутизации внешнего трафика используется Ingress. Ingress позволяет настроить маршрутизацию по hostname и path, терминацию TLS (HTTPS), балансировку нагрузки и rate limiting.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: myapp
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - app.example.com
    secretName: app-tls
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-svc
            port:
              number: 8080
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-svc
            port:
              number: 3000

Шаг 6: Деплой и проверка

После подготовки всех манифестов применяем их к кластеру. Рекомендуется использовать Kustomize или Helm для управления конфигурацией, но для начала можно применить манифесты напрямую через kubectl.

# Применение манифестов
kubectl apply -f namespace.yaml
kubectl apply -f secrets.yaml
kubectl apply -f db-statefulset.yaml
kubectl apply -f cache-deployment.yaml
kubectl apply -f api-deployment.yaml
kubectl apply -f web-deployment.yaml
kubectl apply -f ingress.yaml

# Проверка статуса
kubectl get pods -n myapp
kubectl get svc -n myapp
kubectl logs -f deployment/api -n myapp

Типичные ошибки при миграции

Первая и самая частая ошибка — использование тега latest для образов. В Kubernetes это приводит к непредсказуемым деплоям и невозможности отката. Всегда используйте семантическое версионирование. Вторая ошибка — отсутствие resource limits. Без лимитов один контейнер может потребить все ресурсы узла и вызвать OOM-kill других подов. Третья ошибка — хранение секретов в открытом виде в манифестах. Используйте Kubernetes Secrets, а лучше — внешние системы управления секретами (Vault, AWS Secrets Manager). Четвёртая ошибка — отсутствие health checks (probes). Без них Kubernetes не может определить, работает ли приложение корректно, и не будет автоматически перезапускать зависшие поды.

Заключение

Миграция с Docker Compose на Kubernetes — это не просто перенос контейнеров в другую среду. Это переход к cloud-native подходу, который требует изменения мышления. Kubernetes предоставляет мощные возможности для масштабирования, отказоустойчивости и автоматизации, но требует инвестиций в изучение экосистемы. Начните с малого: перенесите один некритичный сервис, освойте основные концепции, а затем постепенно мигрируйте остальные компоненты. Используйте Helm или Kustomize для управления конфигурациями окружений (dev, staging, prod) и автоматизируйте деплой через CI/CD-пайплайн.

АП
Алексей ПетровDevOps-инженер, MaxTimes
← Назад к блогу

Zero Trust в 2026: почему VPN уже недостаточно

Концепция Zero Trust (нулевого доверия) за последние годы превратилась из теоретической модели в обязательный стандарт для компаний, которые серьёзно относятся к информационной безопасности. Традиционная модель безопасности, основанная на защите периметра сети, оказалась неэффективной в условиях распределённых команд, облачных сервисов и постоянно эволюционирующих угроз. В этой статье мы разберём, почему классический VPN больше не является достаточной мерой защиты и как правильно внедрить Zero Trust.

Проблемы традиционной модели

Классическая модель безопасности работает по принципу «крепости»: есть защищённый периметр (корпоративная сеть), и всё, что внутри, считается доверенным. VPN является средством расширения этого периметра — он позволяет удалённым пользователям «войти внутрь крепости». Однако эта модель имеет фундаментальные проблемы.

Во-первых, после подключения через VPN пользователь обычно получает доступ ко всей внутренней сети. Если учётные данные скомпрометированы (фишинг, утечка, социальная инженерия), злоумышленник получает такой же широкий доступ. Во-вторых, VPN создаёт единую точку отказа: если VPN-сервер недоступен, вся удалённая работа останавливается. В-третьих, VPN не учитывает контекст: он не проверяет, с какого устройства подключается пользователь, соответствует ли оно политикам безопасности, какое приложение запрашивает доступ.

Статистика подтверждает эти опасения. Согласно исследованиям, более 80% атак используют легитимные учётные данные. Среднее время обнаружения взлома составляет 197 дней — за это время злоумышленник свободно перемещается внутри «защищённого» периметра (lateral movement). VPN не способен обнаружить такое перемещение, потому что атакующий уже «внутри крепости».

Принципы Zero Trust

Модель Zero Trust базируется на трёх ключевых принципах. Первый: «Никогда не доверяй, всегда проверяй» (Never trust, always verify). Каждый запрос к ресурсу должен быть аутентифицирован и авторизован, независимо от того, откуда он приходит — из корпоративной сети, из дома сотрудника или из кофейни. Второй принцип: минимальные привилегии (Least Privilege). Пользователь или сервис получает доступ только к тем ресурсам, которые необходимы для выполнения конкретной задачи, и только на необходимый период времени. Третий принцип: предполагай взлом (Assume Breach). Архитектура должна быть спроектирована так, чтобы компрометация одного компонента не давала доступ ко всей системе.

Компоненты Zero Trust архитектуры

Полноценная реализация Zero Trust включает несколько ключевых компонентов. Identity Provider (IdP) — централизованный сервис аутентификации с поддержкой MFA (многофакторной аутентификации). Это может быть Okta, Azure AD, Google Workspace или KeyCloak. Каждый запрос должен содержать валидный identity token.

Policy Engine — система принятия решений о доступе на основе контекста. Она учитывает: кто запрашивает доступ (роль, отдел), с какого устройства (корпоративное, личное, соответствует ли политикам), откуда (геолокация, IP), к какому ресурсу, в какое время и какой уровень риска у текущей сессии.

Микросегментация — разделение сети на мелкие сегменты, каждый из которых имеет свои политики доступа. Вместо плоской сети, где все серверы видят друг друга, каждый сервис может общаться только с теми сервисами, которые ему необходимы. Это реализуется через Network Policies в Kubernetes, Security Groups в облаке или через Service Mesh.

# Kubernetes Network Policy: API может обращаться только к БД
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    ports:
    - protocol: TCP
      port: 5432

Внедрение: пошаговый план

Переход на Zero Trust — это не одномоментный проект, а эволюционный процесс. Мы рекомендуем следующий план из пяти этапов.

Этап 1 — аудит и инвентаризация. Составьте полную карту: пользователи и роли, сервисы и приложения, потоки данных между ними, текущие механизмы аутентификации и авторизации. Без понимания текущего состояния невозможно спроектировать целевую архитектуру.

Этап 2 — централизация идентификации. Внедрите единый IdP с обязательной MFA для всех пользователей. Настройте SSO (Single Sign-On) для всех корпоративных приложений. Откажитесь от локальных учётных записей на серверах и сервисах.

Этап 3 — микросегментация сети. Разделите сеть на сегменты по принципу минимальных привилегий. Начните с критически важных систем: базы данных, секреты, платёжные сервисы. Внедрите Network Policies, настройте мониторинг сетевого трафика.

Этап 4 — защита endpoints. Внедрите систему проверки устройств перед предоставлением доступа. Устройство должно соответствовать политикам: актуальная ОС, наличие антивируса, шифрование диска, корпоративный сертификат.

Этап 5 — continuous monitoring. Настройте непрерывный мониторинг и анализ поведения пользователей и сервисов. Используйте SIEM для корреляции событий безопасности, UEBA для обнаружения аномалий поведения и автоматические response-политики.

Zero Trust для сервис-сервис коммуникации

Zero Trust касается не только пользователей. Взаимодействие между микросервисами также должно быть аутентифицировано и зашифровано. Для этого используется mutual TLS (mTLS): каждый сервис имеет собственный сертификат и проверяет сертификат другого сервиса при установлении соединения. Service Mesh решения (Istio, Linkerd) автоматизируют управление mTLS-сертификатами, избавляя разработчиков от необходимости вручную настраивать TLS для каждого сервиса.

# Istio PeerAuthentication: обязательный mTLS для namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT
---
# Istio AuthorizationPolicy: доступ только для конкретных сервисов
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: api-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: api
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - "cluster.local/ns/production/sa/frontend"

Мониторинг и аналитика

Zero Trust требует непрерывного мониторинга. Каждый запрос на доступ должен логироваться и анализироваться. Важно отслеживать: неудачные попытки аутентификации, доступ из необычных локаций или в нетипичное время, попытки доступа к ресурсам за пределами роли пользователя и аномальные паттерны сетевого трафика. Современные SIEM-системы и UEBA-решения позволяют автоматически выявлять подозрительную активность и реагировать на неё — от отправки уведомления администратору до автоматической блокировки сессии.

Заключение

Переход на Zero Trust — это инвестиция в безопасность, которая окупается многократно. VPN по-прежнему может использоваться как один из механизмов транспорта, но он не должен быть единственным рубежом защиты. Zero Trust обеспечивает многоуровневую защиту, где каждый уровень (identity, device, network, application, data) проверяет и авторизует доступ независимо. Начните с малого — внедрите MFA и SSO, затем постепенно добавляйте микросегментацию и мониторинг. Помните, что Zero Trust — это не продукт, который можно купить и установить, а философия безопасности, которая должна пронизывать всю архитектуру.

МК
Мария КозловаСпециалист по информационной безопасности, MaxTimes
← Назад к блогу

PostgreSQL vs ClickHouse: когда что выбирать

Выбор базы данных — одно из ключевых архитектурных решений, которое определяет производительность, масштабируемость и стоимость владения системой на годы вперёд. PostgreSQL и ClickHouse — две принципиально разные СУБД, каждая из которых превосходна в своей нише. В этой статье мы детально разберём их архитектурные отличия, сценарии применения и поможем определить, какая СУБД подходит для вашей задачи.

Архитектурные различия

PostgreSQL — это реляционная СУБД общего назначения, ориентированная на транзакционные нагрузки (OLTP). Данные хранятся построчно (row-oriented storage): каждая строка таблицы хранится целиком, что оптимально для операций чтения и записи отдельных записей. PostgreSQL поддерживает полноценные ACID-транзакции, внешние ключи, триггеры, хранимые процедуры и богатую экосистему расширений.

ClickHouse — это колоночная аналитическая СУБД (OLAP), разработанная в Яндексе. Данные хранятся по столбцам (column-oriented storage): значения одного столбца лежат рядом на диске. Это даёт колоссальные преимущества для аналитических запросов, которые обрабатывают миллионы строк, но обращаются только к нескольким столбцам. ClickHouse оптимизирован для вставки данных крупными батчами и агрегирующих запросов.

Производительность: сравнение на цифрах

Рассмотрим типичные сценарии. Для вставки одиночных строк (INSERT) PostgreSQL обрабатывает тысячи INSERT в секунду с полной ACID-гарантией. ClickHouse неэффективен для одиночных вставок — он оптимизирован для батчей от 1000+ строк. Для аналитических запросов (агрегации по миллионам строк) ClickHouse в 10-100 раз быстрее PostgreSQL благодаря колоночному хранению и векторному исполнению запросов.

-- Аналитический запрос: средний чек по категориям за месяц
-- PostgreSQL: ~15 секунд на 100M строк
-- ClickHouse: ~0.2 секунды на 100M строк

SELECT
    category,
    count(*) AS orders,
    avg(amount) AS avg_amount,
    sum(amount) AS total
FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at < '2026-02-01'
GROUP BY category
ORDER BY total DESC;

Разница в производительности объясняется просто: для этого запроса ClickHouse читает с диска только 3 столбца (category, amount, created_at), тогда как PostgreSQL вынужден читать все столбцы каждой строки, даже если их 50. Кроме того, ClickHouse применяет сжатие на уровне столбцов (LZ4, ZSTD), достигая степени сжатия 5-10x, что уменьшает объём I/O.

Когда выбирать PostgreSQL

PostgreSQL — правильный выбор для OLTP-нагрузок: веб-приложения, API-серверы, системы управления заказами — любые сценарии, где преобладают операции чтения и записи отдельных записей с транзакционной целостностью.

  • Вам нужны ACID-транзакции с уровнями изоляции
  • Данные обновляются и удаляются часто (UPDATE, DELETE)
  • Нужны внешние ключи и сложные связи между таблицами
  • Запросы обращаются к небольшому количеству строк (по PK или индексу)
  • Нужна богатая экосистема (PostGIS, Full-Text Search, JSONB)

Когда выбирать ClickHouse

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

  • Хранение и анализ логов, метрик, событий
  • Аналитические дашборды с агрегациями по миллионам строк
  • Хранение данных с append-only паттерном (INSERT без UPDATE)
  • Нужна горизонтальная масштабируемость для петабайтных объёмов
  • Требуется сжатие данных для экономии дискового пространства
-- Создание таблицы в ClickHouse для хранения событий
CREATE TABLE events (
    event_id UUID,
    user_id UInt64,
    event_type LowCardinality(String),
    properties String,
    created_at DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (event_type, user_id, created_at)
TTL created_at + INTERVAL 90 DAY;

-- Материализованное представление для агрегации в реальном времени
CREATE MATERIALIZED VIEW events_daily_mv
ENGINE = SummingMergeTree()
ORDER BY (event_date, event_type)
AS SELECT
    toDate(created_at) AS event_date,
    event_type,
    count() AS event_count,
    uniqExact(user_id) AS unique_users
FROM events
GROUP BY event_date, event_type;

Гибридная архитектура

На практике лучшее решение — использовать обе СУБД. PostgreSQL работает как основная транзакционная база данных, а ClickHouse — как аналитическое хранилище. Данные реплицируются из PostgreSQL в ClickHouse через CDC (Change Data Capture) с помощью Debezium, или через ETL-пайплайны.

Такая архитектура позволяет получить лучшее из двух миров: надёжную транзакционную обработку в PostgreSQL и молниеносную аналитику в ClickHouse. Разработчики работают с привычным PostgreSQL, а аналитики получают доступ к данным через ClickHouse с задержкой в секунды.

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

Не пытайтесь использовать ClickHouse для OLTP-нагрузок — он для этого не предназначен. Операции UPDATE и DELETE в ClickHouse работают асинхронно через мутации, что принципиально отличается от поведения PostgreSQL. Не пытайтесь строить аналитику на больших данных в PostgreSQL без предварительной агрегации — это будет медленно и ресурсоёмко. Если ваш объём данных менее 10 миллионов строк и аналитика нужна нечасто, PostgreSQL может справиться с обеими задачами благодаря параллельным запросам и материализованным представлениям.

Заключение

PostgreSQL и ClickHouse — не конкуренты, а комплементарные инструменты. PostgreSQL отвечает за целостность данных и транзакционные операции, ClickHouse — за скорость аналитики на больших объёмах. Выбирайте инструмент под задачу, а при необходимости — комбинируйте оба в гибридной архитектуре. Ключевой вопрос для принятия решения: «Какой паттерн нагрузки преобладает?» Если это много мелких чтений и записей — PostgreSQL. Если это агрегации по миллионам строк — ClickHouse.

ДВ
Дмитрий ВолковBackend-разработчик, MaxTimes
← Назад к блогу

Как мы ускорили CI/CD с 40 до 4 минут

Быстрый CI/CD-пайплайн — это не роскошь, а необходимость. Когда каждый push в репозиторий запускает 40-минутный пайплайн, разработчики теряют продуктивность: переключаются на другие задачи, забывают контекст, а фидбек приходит слишком поздно. В этой статье мы расскажем, как последовательно сократили время CI/CD с 40 до 4 минут для проекта из 12 микросервисов.

Исходное состояние

Наш пайплайн в GitLab CI состоял из пяти стадий: lint, test, build, security scan и deploy. Все стадии выполнялись последовательно. Каждый сервис собирал Docker-образ с нуля, тесты запускались на выделенном runner без кеширования, а линтеры проверяли весь монорепозиторий целиком — даже если изменился один файл. Суммарное время: 38-42 минуты.

Оптимизация 1: Docker layer caching

Самой «тяжёлой» стадией была сборка Docker-образов. Каждая сборка скачивала зависимости заново, потому что Docker layer cache не сохранялся между запусками на CI. Мы внедрили BuildKit с inline caching и registry-based cache.

# .gitlab-ci.yml — оптимизированная сборка
build:
  stage: build
  script:
    - docker buildx build
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE:cache
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:cache,mode=max
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        --push .

Результат: при изменении только кода приложения (без изменений зависимостей) сборка сократилась с 8 минут до 45 секунд. Docker переиспользовал закешированные слои с зависимостями и пересобирал только слой с исходным кодом.

Оптимизация 2: параллелизация стадий

Lint, unit-тесты и security scan не зависят друг от друга и могут выполняться параллельно. Мы реструктурировали пайплайн, объединив независимые задачи в одну стадию. Это позволило сократить общее время на 30%, так как самая длинная параллельная задача определяла длительность стадии вместо суммы всех задач.

stages:
  - check      # lint + test + security (параллельно)
  - build
  - deploy

lint:
  stage: check
  script: make lint
  rules:
    - changes: ["**/*.go", "**/*.ts"]

test:
  stage: check
  script: make test
  parallel:
    matrix:
      - SERVICE: [auth, orders, payments, catalog]

security:
  stage: check
  script: trivy image --severity HIGH,CRITICAL .

Оптимизация 3: инкрементальные проверки

В монорепозитории нет смысла тестировать все 12 сервисов, если изменился только один. Мы написали скрипт, который определяет затронутые сервисы по git diff и запускает только необходимые задачи.

#!/bin/bash
# scripts/affected-services.sh
CHANGED_FILES=$(git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA HEAD)

SERVICES=""
for service_dir in services/*/; do
  service=$(basename "$service_dir")
  if echo "$CHANGED_FILES" | grep -q "^services/$service/"; then
    SERVICES="$SERVICES $service"
  fi
done

# Если изменились общие библиотеки — тестируем всё
if echo "$CHANGED_FILES" | grep -q "^libs/"; then
  SERVICES="all"
fi

echo "$SERVICES"

Этот подход сократил количество запускаемых тестов в среднем на 70%. Вместо 12 сервисов тестируется 1-3, что пропорционально уменьшает время пайплайна.

Оптимизация 4: кеширование зависимостей

Скачивание npm-модулей, Go-модулей, pip-пакетов при каждом запуске CI — огромная потеря времени. Мы настроили кеширование зависимостей на уровне CI runner, используя hash от lock-файла как ключ кеша.

test:
  stage: check
  cache:
    key:
      files:
        - go.sum
    paths:
      - /go/pkg/mod/
    policy: pull-push
  script:
    - go test ./...

При неизменном go.sum зависимости загружаются из кеша за 2-3 секунды вместо 60-90 секунд на скачивание из интернета. Аналогично для npm (node_modules с ключом от package-lock.json) и pip (.pip-cache с ключом от requirements.txt).

Оптимизация 5: test splitting

Для сервисов с большим количеством тестов (1000+ в сервисе заказов) мы внедрили параллельное выполнение тестов с разделением на шарды. GitLab CI поддерживает это нативно через директиву parallel.

test-orders:
  stage: check
  parallel: 4
  script:
    - go test ./... -count=1 -p=4
        -run "$(./scripts/test-shard.sh $CI_NODE_INDEX $CI_NODE_TOTAL)"

4 параллельных runner выполняют по 250 тестов каждый. Время тестирования сервиса заказов сократилось с 8 минут до 2.5 минут.

Результаты

Совокупность всех оптимизаций дала следующие результаты. Среднее время пайплайна сократилось с 40 до 4 минут. Для изменений в одном сервисе без изменения зависимостей пайплайн укладывается в 2-3 минуты. Количество пайплайнов в день увеличилось вдвое — разработчики стали коммитить чаще, меньшими порциями, что улучшило качество code review.

Рекомендации

Начните с профилирования: определите, какие стадии занимают больше всего времени. Обычно это сборка Docker-образов и тесты. Внедряйте оптимизации инкрементально, измеряя эффект каждой. Не забывайте про надёжность: агрессивное кеширование может привести к «протухшим» кешам. Добавьте механизм принудительной инвалидации кеша. Мониторьте метрики CI/CD: среднее время пайплайна, процент успешных пайплайнов, время ожидания в очереди runner. Эти метрики покажут, где ещё есть потенциал для улучшений.

АП
Алексей ПетровDevOps-инженер, MaxTimes
← Назад к блогу

Масштабирование e-commerce до 1M заказов в сутки

Когда интернет-магазин растёт, инфраструктура, рассчитанная на тысячу заказов в день, перестаёт справляться с десятками и сотнями тысяч. В этом кейсе мы расскажем, как перестроили архитектуру e-commerce платформы, чтобы она стабильно обрабатывала более миллиона заказов в сутки, выдерживая пиковые нагрузки распродаж.

Исходная архитектура

Платформа представляла собой классический монолит на PHP (Laravel), работающий на двух серверах за Nginx-балансировщиком. База данных — единственный инстанс MySQL 8 на выделенном сервере с 32 ГБ RAM. Сессии хранились в файловой системе, кеш — в Memcached, очередь задач — в Redis. При нагрузке 5000 заказов в день система работала стабильно, но при 15000 начинались деградации: увеличивалось время ответа, появлялись таймауты при обращении к базе данных.

Выявление узких мест

Первым шагом мы провели нагрузочное тестирование и профилирование. Основные bottleneck-и оказались предсказуемыми. База данных — единственный MySQL-инстанс не справлялся с нагрузкой: длинные транзакции блокировали таблицу заказов, а аналитические запросы конкурировали с транзакционными. Монолитная архитектура — масштабирование было возможно только вертикально (больше CPU и RAM), а горизонтальное масштабирование ограничивалось состоянием в файловой системе. Синхронная обработка — создание заказа включало синхронные вызовы: проверка наличия, расчёт доставки, отправка email, обновление остатков — всё в одном HTTP-запросе, который занимал 2-3 секунды.

Шаг 1: разделение на микросервисы

Мы выделили из монолита пять критических сервисов: каталог (товары, категории, поиск), заказы (создание, статусы, история), оплата (интеграция с платёжными системами), склад (остатки, резервирование), нотификации (email, SMS, push). Каждый сервис получил собственную базу данных (database per service pattern), что устранило конкуренцию за блокировки и позволило масштабировать базы независимо.

Шаг 2: асинхронная обработка заказов

Ключевое архитектурное решение — переход от синхронного создания заказа к асинхронному. Раньше пользователь ждал 2-3 секунды, пока все операции завершатся. Теперь процесс выглядит иначе: API принимает заказ, валидирует, сохраняет в БД со статусом «создан» и отправляет событие в очередь. Ответ пользователю возвращается за 100-200 мс. Дальнейшая обработка (резервирование на складе, оплата, расчёт доставки, отправка уведомлений) происходит асинхронно через очередь сообщений.

# Пример: обработчик создания заказа (Python)
from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers=['kafka-1:9092', 'kafka-2:9092'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

def create_order(request):
    order = validate_and_save(request)

    producer.send('orders.created', {
        'order_id': order.id,
        'user_id': order.user_id,
        'items': order.items,
        'total': order.total,
        'created_at': order.created_at.isoformat()
    })

    return {'order_id': order.id, 'status': 'created'}

Шаг 3: кеширование на нескольких уровнях

Для каталога товаров мы внедрили многоуровневое кеширование. CDN (CloudFlare) кеширует страницы каталога для неавторизованных пользователей. Redis кеширует данные товаров, категорий и результаты поиска. In-memory cache на уровне приложения (local cache) хранит наиболее популярные товары. Инвалидация кеша происходит через события: при обновлении товара публикуется событие, которое получают все инстансы сервиса каталога и очищают локальный кеш.

Шаг 4: шардирование базы заказов

При миллионе заказов в сутки таблица заказов растёт на 30+ миллионов строк в месяц. Один сервер базы данных не может обеспечить необходимую пропускную способность для записи. Мы внедрили шардирование по user_id: заказы каждого пользователя всегда попадают на один шард, что позволяет эффективно выполнять запросы «все заказы пользователя» без кросс-шардовых join-ов.

-- Функция определения шарда
-- shard_id = user_id % number_of_shards
-- 16 шардов на 4 сервера (по 4 шарда на сервер)

-- Таблица на каждом шарде
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    status ENUM('created','paid','shipped','delivered','cancelled'),
    total DECIMAL(12,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_status (user_id, status),
    INDEX idx_created (created_at)
) ENGINE=InnoDB;

Шаг 5: автоскейлинг в Kubernetes

Все сервисы деплоятся в Kubernetes с настроенным Horizontal Pod Autoscaler (HPA). При увеличении нагрузки (CPU > 70% или custom metrics: RPS, queue depth) Kubernetes автоматически добавляет поды. При снижении нагрузки — убирает, экономя ресурсы.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: orders-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: orders-api
  minReplicas: 4
  maxReplicas: 32
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"

Результаты

После всех оптимизаций платформа стабильно обрабатывает 1.2 миллиона заказов в сутки с пиком до 2 миллионов во время распродаж. Среднее время ответа API: 80 мс (было 2-3 секунды). Время создания заказа: 150 мс (было 3 секунды). Доступность: 99.95% за последние 6 месяцев. Стоимость инфраструктуры выросла в 3 раза, но revenue вырос в 15 раз — окупаемость очевидна.

Уроки

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

ИС
Иван СоколовSRE-инженер, MaxTimes
← Назад к блогу

Внедрение LLM в корпоративные процессы: практика

Большие языковые модели (LLM) перестали быть экспериментальной технологией и превратились в реальный инструмент повышения эффективности бизнес-процессов. Однако между демонстрацией возможностей ChatGPT и внедрением LLM в production — огромная пропасть. В этой статье мы поделимся практическим опытом интеграции LLM в корпоративные процессы: от выбора модели до мониторинга качества в production.

Сценарии использования

Не все задачи подходят для LLM. Успешные сценарии объединяет несколько характеристик: задача связана с обработкой неструктурированного текста, допустима нечёткость результата (или есть механизм верификации), ручное выполнение задачи дорого и масштабируется линейно. Вот конкретные сценарии, которые мы реализовали на проектах.

Первый сценарий — автоматическая классификация и маршрутизация обращений в техподдержку. LLM анализирует текст обращения и определяет: категорию (техническая проблема, вопрос по оплате, жалоба), приоритет (critical, high, medium, low), подразделение для маршрутизации. Точность классификации: 94% — выше, чем у предыдущей rule-based системы (82%).

Второй сценарий — генерация описаний товаров для e-commerce. По структурированным характеристикам товара (бренд, категория, параметры) LLM генерирует SEO-оптимизированные описания. Это позволило обрабатывать 10 000 товаров в сутки вместо 200 силами контент-менеджеров.

Третий сценарий — суммаризация встреч и создание протоколов. Транскрипция созвонов (через Whisper) передаётся LLM, которая создаёт структурированный протокол: ключевые решения, задачи с ответственными, открытые вопросы.

Выбор модели: API vs Self-hosted

Критический вопрос — использовать API коммерческих моделей (OpenAI, Anthropic, Google) или развернуть модель на собственной инфраструктуре. Каждый подход имеет свои преимущества. API коммерческих моделей: быстрый старт, высокое качество, не нужна GPU-инфраструктура, но зависимость от провайдера и передача данных наружу. Self-hosted модели (Llama, Mistral, Qwen): полный контроль над данными, фиксированная стоимость, возможность fine-tuning, но требуют GPU-серверов и экспертизы.

Для обработки чувствительных данных (персональные данные, финансовая информация) мы рекомендуем self-hosted решения. Для менее критичных задач API коммерческих моделей позволяет быстрее запуститься и получить результат.

Архитектура RAG-системы

Для задач, требующих знания корпоративной документации (ответы на вопросы сотрудников, поиск по базе знаний), мы используем паттерн RAG (Retrieval-Augmented Generation). Документы разбиваются на чанки, для каждого чанка вычисляется embedding-вектор, векторы хранятся в векторной базе данных. При запросе находятся наиболее релевантные чанки, которые передаются в LLM как контекст.

# RAG-пайплайн на Python (упрощённый пример)
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQA

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " "]
)

chunks = splitter.split_documents(documents)

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)

llm = ChatOpenAI(model="gpt-4o", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=vectorstore.as_retriever(
        search_kwargs={"k": 5}
    ),
    return_source_documents=True
)

result = qa_chain.invoke({"query": "Какая процедура оформления отпуска?"})
print(result["result"])
print("Источники:", [doc.metadata for doc in result["source_documents"]])

Промпт-инжиниринг для production

Качество ответов LLM напрямую зависит от качества промптов. В production промпты — это код: они версионируются, тестируются и мониторятся. Мы используем несколько принципов. Структурированный вывод: просим модель возвращать JSON с заданной схемой, что упрощает парсинг и валидацию. Few-shot примеры: включаем 3-5 примеров входа и ожидаемого выхода. Guardrails: явно указываем ограничения (не генерировать неверные данные, признавать незнание). Chain of Thought: для сложных задач просим модель рассуждать пошагово.

Мониторинг и оценка качества

В production необходимо отслеживать метрики качества LLM. Latency — время ответа модели (p50, p95, p99). Стоимость — расход токенов на запрос. Качество — процент корректных ответов (оценивается через sampling и ручную разметку). Hallucinations — процент ответов с выдуманными фактами (для RAG — проверка, что ответ подкреплён источниками). Мы настраиваем дашборды в Grafana, которые отображают все эти метрики в реальном времени, и алерты при деградации качества.

Стоимость и оптимизация

Стоимость API-вызовов может быстро вырасти при больших объёмах. Мы используем несколько стратегий оптимизации: кеширование ответов для повторяющихся запросов (semantic cache на основе embedding similarity), каскадная обработка (сначала дешёвая модель, и только при низкой уверенности — дорогая), батчирование запросов и сжатие промптов (удаление избыточного контекста).

Заключение

LLM — мощный инструмент, но не серебряная пуля. Успешное внедрение требует чёткого понимания бизнес-задачи, правильного выбора архитектуры и непрерывного мониторинга качества. Начните с одного конкретного сценария, измерьте ROI и масштабируйте на основе данных. Помните, что LLM — это вероятностная система, и всегда нужен план B для случаев, когда модель ошибается.

АМ
Анна МорозоваData Engineer, MaxTimes
← Назад к блогу

Terraform: 10 best practices для production

Terraform — де-факто стандарт для управления облачной инфраструктурой как кодом. Однако между «работает на моей машине» и production-grade Terraform-кодом — существенная разница. За годы работы с Terraform на проектах разного масштаба мы выработали набор практик, которые помогают избежать типичных проблем: дрифтов конфигурации, конфликтов состояний и незапланированных изменений.

1. Используйте remote state с блокировкой

Локальный файл terraform.tfstate — рецепт катастрофы. Его можно случайно удалить, он не поддерживает совместную работу, и при потере state Terraform потеряет связь с реальной инфраструктурой. Используйте remote backend с поддержкой блокировки состояний.

terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/vpc/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

DynamoDB-таблица обеспечивает блокировку: если кто-то уже выполняет terraform apply, второй пользователь получит ошибку и не сможет внести конфликтующие изменения.

2. Структурируйте код по окружениям и компонентам

Не держите всю инфраструктуру в одном state-файле. Разделяйте по окружениям (dev, staging, prod) и по компонентам (networking, compute, database). Это уменьшает blast radius изменений и ускоряет terraform plan.

infrastructure/
├── modules/
│   ├── vpc/
│   ├── eks/
│   ├── rds/
│   └── monitoring/
├── environments/
│   ├── dev/
│   │   ├── vpc/
│   │   │   ├── main.tf
│   │   │   ├── variables.tf
│   │   │   └── outputs.tf
│   │   ├── eks/
│   │   └── rds/
│   ├── staging/
│   └── production/
└── global/
    ├── iam/
    └── dns/

3. Версионируйте модули

Переиспользуемые модули должны иметь версии. Это позволяет обновлять модуль в dev, протестировать и только потом применить в production. Используйте Git-теги или Terraform Registry.

module "vpc" {
  source  = "git::https://github.com/company/tf-modules.git//vpc?ref=v2.1.0"
  
  cidr_block       = "10.0.0.0/16"
  availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
  enable_nat       = true
}

4. Используйте terraform plan в CI/CD

Каждый pull request с изменениями Terraform-кода должен автоматически запускать terraform plan. Результат plan выкладывается как комментарий к PR, чтобы ревьюер мог оценить масштаб изменений до merge.

# .github/workflows/terraform.yml
name: Terraform Plan
on:
  pull_request:
    paths: ['infrastructure/**']
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: hashicorp/setup-terraform@v3
    - run: terraform init
      working-directory: infrastructure/environments/production/vpc
    - run: terraform plan -no-color -out=plan.out
      working-directory: infrastructure/environments/production/vpc
    - uses: actions/github-script@v7
      with:
        script: |
          const output = require('fs').readFileSync('plan.out', 'utf8');
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: '```\n' + output + '\n```'
          });

5. Не храните секреты в Terraform-коде

Пароли, API-ключи и другие секреты не должны находиться в .tf файлах или terraform.tfvars. Используйте внешние системы управления секретами: AWS Secrets Manager, HashiCorp Vault или переменные окружения CI/CD.

6. Используйте data sources вместо hardcode

Вместо хардкода AMI ID, VPC ID и других идентификаторов используйте data sources, чтобы Terraform сам находил нужные ресурсы по тегам или фильтрам.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.medium"
}

7. Добавляйте lifecycle-правила

Используйте prevent_destroy для критических ресурсов (базы данных, S3-бакеты), чтобы случайный terraform destroy не удалил данные. Используйте ignore_changes для полей, которые изменяются вне Terraform.

8. Тестируйте модули

Используйте terraform validate и tflint для статического анализа. Для интеграционного тестирования — Terratest (Go) или pytest-terraform. Тесты создают реальную инфраструктуру в изолированном аккаунте, проверяют её и уничтожают.

9. Используйте moved блоки для рефакторинга

При переименовании ресурсов или реструктуризации модулей используйте блок moved вместо terraform state mv. Это декларативно, версионируется и безопаснее.

moved {
  from = aws_instance.web_server
  to   = aws_instance.app_server
}

10. Мониторьте drift

Drift — расхождение между Terraform state и реальной инфраструктурой — происходит, когда кто-то вносит изменения через консоль или CLI, минуя Terraform. Настройте регулярный запуск terraform plan по расписанию (cron) с отправкой алерта при обнаружении drift.

Заключение

Terraform — мощный инструмент, но без дисциплины и процессов он может стать источником проблем. Внедряйте эти практики постепенно, начиная с remote state и CI/CD-интеграции. Помните: Infrastructure as Code — это прежде всего код, и к нему применимы все лучшие практики разработки: ревью, тестирование, версионирование и модульность.

АП
Алексей ПетровDevOps-инженер, MaxTimes
← Назад к блогу

Prometheus и Alertmanager: настройка алертинга с нуля

Мониторинг без алертинга — это просто красивые графики. Настоящая ценность системы мониторинга раскрывается, когда она автоматически уведомляет о проблемах до того, как их заметят пользователи. В этой статье мы построим полноценную систему алертинга на базе Prometheus и Alertmanager — от установки до production-ready конфигурации.

Архитектура системы

Prometheus — это time-series база данных с pull-моделью сбора метрик. Prometheus периодически опрашивает (scrape) эндпоинты приложений и инфраструктурных компонентов, собирая метрики в формате OpenMetrics. Alertmanager — отдельный компонент, который получает алерты от Prometheus, группирует их, подавляет дублирующие и отправляет уведомления в настроенные каналы (Slack, email, PagerDuty, Telegram).

Установка в Kubernetes

Самый распространённый способ установки — через Helm-чарт kube-prometheus-stack. Он включает Prometheus, Alertmanager, Grafana и набор предустановленных правил и дашбордов.

# Установка kube-prometheus-stack
helm repo add prometheus-community \
  https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  --set grafana.adminPassword=secretpass \
  --set prometheus.prometheusSpec.retention=30d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi

Типы метрик

Prometheus поддерживает четыре типа метрик. Counter — монотонно возрастающий счётчик (количество запросов, ошибок). Gauge — значение, которое может увеличиваться и уменьшаться (использование CPU, количество активных соединений). Histogram — распределение значений по корзинам (время ответа). Summary — аналогично histogram, но рассчитывает квантили на клиенте.

Инструментирование приложения

Для сбора метрик приложение должно экспортировать эндпоинт /metrics в формате Prometheus. Для Go, Python, Java и Node.js существуют официальные клиентские библиотеки.

# Python: экспорт метрик с prometheus_client
from prometheus_client import Counter, Histogram, start_http_server
import time

REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

REQUEST_LATENCY = Histogram(
    'http_request_duration_seconds',
    'HTTP request latency',
    ['method', 'endpoint'],
    buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)

def handle_request(method, endpoint):
    start = time.time()
    try:
        result = process_request()
        REQUEST_COUNT.labels(method, endpoint, '200').inc()
        return result
    except Exception:
        REQUEST_COUNT.labels(method, endpoint, '500').inc()
        raise
    finally:
        REQUEST_LATENCY.labels(method, endpoint).observe(
            time.time() - start
        )

start_http_server(9090)

Правила алертинга

Правила (alerting rules) определяют условия, при которых срабатывает алерт. Каждое правило содержит: PromQL-выражение, длительность (for) — сколько времени условие должно выполняться, severity — критичность алерта и annotations — описание и инструкции по реагированию.

# prometheus-rules.yaml
groups:
- name: application
  rules:
  - alert: HighErrorRate
    expr: |
      sum(rate(http_requests_total{status=~"5.."}[5m]))
      /
      sum(rate(http_requests_total[5m]))
      > 0.05
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "Высокий процент ошибок ({{ $value | humanizePercentage }})"
      description: "Более 5% запросов завершаются ошибкой 5xx"
      runbook: "https://wiki.example.com/runbooks/high-error-rate"

  - alert: HighLatency
    expr: |
      histogram_quantile(0.95,
        sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
      ) > 1.0
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "Высокая задержка: p95 = {{ $value | humanizeDuration }}"

  - alert: PodCrashLooping
    expr: |
      increase(kube_pod_container_status_restarts_total[1h]) > 5
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "Pod {{ $labels.pod }} перезапускается"

  - alert: DiskSpaceLow
    expr: |
      (node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 < 10
    for: 15m
    labels:
      severity: warning
    annotations:
      summary: "Мало места на диске: {{ $value | humanize }}% осталось"

Настройка Alertmanager

Alertmanager отвечает за маршрутизацию алертов. Критические алерты должны немедленно уходить в PagerDuty и Telegram, warning — в Slack-канал команды, info — только в email-дайджест.

# alertmanager.yaml
global:
  resolve_timeout: 5m
  slack_api_url: 'https://hooks.slack.com/services/XXX/YYY/ZZZ'

route:
  group_by: ['alertname', 'namespace']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: slack-default
  routes:
  - match:
      severity: critical
    receiver: pagerduty-critical
    repeat_interval: 1h
  - match:
      severity: warning
    receiver: slack-warning

receivers:
- name: slack-default
  slack_configs:
  - channel: '#monitoring'
    title: '{{ .GroupLabels.alertname }}'
    text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'

- name: pagerduty-critical
  pagerduty_configs:
  - service_key: 'your-pagerduty-key'

- name: slack-warning
  slack_configs:
  - channel: '#alerts-warning'

inhibit_rules:
- source_match:
    severity: critical
  target_match:
    severity: warning
  equal: ['alertname', 'namespace']

Борьба с alert fatigue

Alert fatigue — состояние, когда команда получает столько алертов, что перестаёт на них реагировать. Чтобы этого избежать, следуйте правилам: каждый алерт должен требовать действия (если не требует — это не алерт, а информация для дашборда). Используйте inhibition — подавляйте менее важные алерты, когда сработал более важный на ту же тему. Настройте адекватные пороги и длительность (for): кратковременные спайки не должны вызывать алерт. Регулярно проводите ревью алертов: удаляйте неактуальные, корректируйте пороги.

Заключение

Хорошо настроенная система алертинга — это ваша первая линия защиты от инцидентов. Prometheus и Alertmanager предоставляют гибкую и мощную платформу, но её нужно правильно настроить и непрерывно поддерживать. Начните с базовых алертов (high error rate, high latency, disk space), добавьте runbooks для каждого алерта и постепенно расширяйте покрытие по мере понимания поведения вашей системы.

ИС
Иван СоколовSRE-инженер, MaxTimes
← Назад к блогу

gRPC vs REST: когда выбрать gRPC

REST на базе HTTP/JSON остаётся доминирующим подходом к проектированию API. Однако gRPC — фреймворк удалённого вызова процедур от Google — набирает популярность, особенно в микросервисных архитектурах. В этой статье мы разберём, чем gRPC принципиально отличается от REST, в каких сценариях он даёт реальные преимущества и когда лучше остаться на REST.

Что такое gRPC

gRPC — это высокопроизводительный RPC-фреймворк, который использует HTTP/2 для транспорта и Protocol Buffers (protobuf) для сериализации данных. В отличие от REST, где API описывается через URL-пути и HTTP-методы, в gRPC API описывается через .proto файлы — строгие контракты, которые определяют сервисы, методы и структуры данных.

// order_service.proto
syntax = "proto3";
package orders;

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (stream Order);
  rpc TrackOrder(TrackOrderRequest) returns (stream OrderStatus);
}

message CreateOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2;
  Address shipping_address = 3;
}

message Order {
  string id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  double total = 5;
  google.protobuf.Timestamp created_at = 6;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}

Преимущества gRPC

Производительность — главное преимущество gRPC. Protobuf — бинарный формат сериализации, который в 3-10 раз компактнее JSON и в 5-20 раз быстрее при сериализации и десериализации. На больших объёмах данных (тысячи RPC-вызовов в секунду) разница становится значительной. HTTP/2 добавляет мультиплексирование — множество запросов отправляется по одному TCP-соединению, что устраняет проблему head-of-line blocking.

Строгая типизация — proto-файлы являются контрактом между клиентом и сервером. Код клиента и сервера генерируется автоматически из proto-файлов для более чем десяти языков программирования. Это устраняет целые классы ошибок: несоответствие типов, опечатки в именах полей, отсутствие обязательных полей.

Потоковая передача (streaming) — gRPC поддерживает четыре модели коммуникации: unary (один запрос — один ответ), server streaming (один запрос — поток ответов), client streaming (поток запросов — один ответ), bidirectional streaming (поток в обе стороны). REST ограничен моделью запрос-ответ; для потоковых сценариев приходится использовать WebSocket или SSE.

Когда выбрать gRPC

  • Межсервисное взаимодействие в микросервисной архитектуре, где важна производительность
  • Потоковая передача данных: real-time обновления, потоковое видео, IoT-телеметрия
  • Полиглотная среда: разные сервисы написаны на разных языках, и proto-файлы обеспечивают единый контракт
  • Высоконагруженные системы, где экономия на сериализации и транспорте даёт ощутимый выигрыш

Когда остаться на REST

  • Публичные API для внешних разработчиков — REST проще для понимания и интеграции, не требует генерации кода
  • Браузерные клиенты — прямое обращение к gRPC из браузера требует gRPC-Web прокси
  • Простые CRUD-приложения, где производительность не является bottleneck-ом
  • Команда не имеет опыта с gRPC — кривая обучения существенна

Пример реализации

# gRPC-сервер на Python
import grpc
from concurrent import futures
import order_service_pb2 as pb2
import order_service_pb2_grpc as pb2_grpc

class OrderServicer(pb2_grpc.OrderServiceServicer):
    def CreateOrder(self, request, context):
        order = create_order_in_db(
            user_id=request.user_id,
            items=request.items,
            address=request.shipping_address
        )
        return pb2.Order(
            id=order.id,
            user_id=order.user_id,
            status=pb2.OrderStatus.CREATED,
            total=order.total
        )

    def TrackOrder(self, request, context):
        for status_update in track_order_stream(request.order_id):
            yield pb2.OrderStatus(
                order_id=request.order_id,
                status=status_update.status,
                timestamp=status_update.timestamp
            )

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
pb2_grpc.add_OrderServiceServicer_to_server(OrderServicer(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()

Гибридный подход

На практике оптимальный подход — использовать оба протокола. REST — для публичного API, браузерных клиентов и внешних интеграций. gRPC — для внутреннего межсервисного взаимодействия. При необходимости gRPC-gateway может автоматически генерировать REST API из proto-файлов, обеспечивая единый контракт для обоих протоколов.

Заключение

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

ДВ
Дмитрий ВолковBackend-разработчик, MaxTimes
← Назад к блогу

Redis: паттерны кэширования для высоких нагрузок

Redis — один из самых популярных инструментов для кэширования, но эффективное использование требует понимания паттернов и их trade-off-ов. Неправильно настроенный кеш хуже его отсутствия: устаревшие данные, cache stampede, лишнее потребление памяти. В этой статье разберём основные паттерны кэширования и их применение в высоконагруженных системах.

Cache-Aside (Lazy Loading)

Самый распространённый паттерн. Приложение сначала проверяет кеш. При cache hit данные возвращаются из Redis. При cache miss приложение запрашивает данные из БД, записывает их в кеш и возвращает клиенту.

# Python: Cache-Aside с Redis
import redis
import json

r = redis.Redis(host='redis', port=6379, decode_responses=True)

def get_user(user_id: int) -> dict:
    cache_key = f"user:{user_id}"
    
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)
    
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    
    r.setex(cache_key, 3600, json.dumps(user))
    
    return user

Преимущества: в кеш попадают только запрашиваемые данные, простая реализация. Недостатки: первый запрос всегда идёт в БД (cold start), данные в кеше могут устаревать.

Write-Through

При каждой записи в БД данные одновременно записываются в кеш. Это гарантирует, что кеш всегда содержит актуальные данные.

def update_user(user_id: int, data: dict):
    db.execute(
        "UPDATE users SET name=%s, email=%s WHERE id=%s",
        data['name'], data['email'], user_id
    )
    
    cache_key = f"user:{user_id}"
    r.setex(cache_key, 3600, json.dumps(data))

Преимущества: кеш всегда актуален, отсутствует cache miss для существующих данных. Недостатки: увеличивается латентность записи (две операции вместо одной), в кеш попадают данные, которые могут никогда не быть прочитаны.

Write-Behind (Write-Back)

Приложение записывает данные только в кеш, а фоновый процесс асинхронно синхронизирует кеш с БД. Этот паттерн максимизирует производительность записи, но усложняет обеспечение консистентности.

Cache Stampede Prevention

Cache stampede (thundering herd) — ситуация, когда у популярного ключа истекает TTL и множество параллельных запросов одновременно идут в БД. Это может перегрузить базу данных. Решение — использование блокировки (lock) при обновлении кеша.

import time

def get_user_safe(user_id: int) -> dict:
    cache_key = f"user:{user_id}"
    lock_key = f"lock:user:{user_id}"
    
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)
    
    if r.set(lock_key, "1", nx=True, ex=10):
        try:
            user = db.query("SELECT * FROM users WHERE id = %s", user_id)
            r.setex(cache_key, 3600, json.dumps(user))
            return user
        finally:
            r.delete(lock_key)
    else:
        for _ in range(50):
            time.sleep(0.1)
            cached = r.get(cache_key)
            if cached:
                return json.loads(cached)
        return db.query("SELECT * FROM users WHERE id = %s", user_id)

Стратегии инвалидации

Инвалидация кеша — одна из двух самых сложных проблем в CS (вторая — именование переменных). Существует несколько стратегий. TTL-based — самый простой подход: устанавливаем TTL, и Redis автоматически удаляет устаревшие данные. Event-based — при изменении данных в БД публикуем событие, которое инвалидирует кеш. Это обеспечивает быструю актуализацию, но усложняет архитектуру. Version-based — ключ кеша включает версию, при обновлении данных увеличиваем версию — старый кеш автоматически перестаёт использоваться.

Структуры данных для кэширования

Redis предоставляет богатый набор структур данных, и правильный выбор структуры может значительно оптимизировать потребление памяти и производительность. Strings — для простых ключ-значение данных. Hashes — для объектов, когда нужно читать или обновлять отдельные поля. Sorted Sets — для рейтингов, лидербордов, данных, отсортированных по score. Sets — для проверки уникальности, тегов, списков подписчиков.

# Использование Hashes для пользователя — экономичнее по памяти
r.hset("user:123", mapping={
    "name": "Иван Петров",
    "email": "ivan@example.com",
    "role": "admin",
    "last_login": "2026-01-25T10:30:00"
})

name = r.hget("user:123", "name")

r.hset("user:123", "last_login", "2026-01-25T14:00:00")

Мониторинг Redis

Для production-системы критически важно мониторить несколько метрик Redis: hit rate (процент запросов, обслуженных из кеша), used memory (потребление памяти), evicted keys (количество ключей, удалённых из-за нехватки памяти), connected clients (количество подключений), latency (задержка операций). Если hit rate падает ниже 80%, стоит пересмотреть стратегию кэширования — возможно, TTL слишком короткий или данные слишком динамичные для кеширования.

Заключение

Правильно настроенный кеш на Redis может снизить нагрузку на БД на 80-95% и значительно улучшить время отклика. Выбирайте паттерн кэширования исходя из требований к консистентности и паттернов нагрузки. Для большинства сценариев Cache-Aside с TTL-инвалидацией — оптимальный баланс между простотой и эффективностью. Не забывайте о мониторинге и защите от cache stampede.

ДВ
Дмитрий ВолковBackend-разработчик, MaxTimes
← Назад к блогу

Nginx: тонкая настройка для 100K+ RPS

Nginx — самый популярный веб-сервер и reverse proxy в мире. При правильной настройке один сервер на обычном железе способен обрабатывать более 100 000 запросов в секунду. Однако конфигурация «из коробки» рассчитана на универсальность, а не на максимальную производительность. В этой статье мы разберём ключевые параметры, которые нужно настроить для обработки высоких нагрузок.

Worker processes и connections

Nginx использует событийно-ориентированную архитектуру с worker-процессами. Каждый worker обрабатывает тысячи соединений одновременно благодаря неблокирующему I/O. Количество worker-процессов должно соответствовать количеству CPU-ядер.

worker_processes auto;  # автоматически = число ядер CPU
worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
    multi_accept on;
    use epoll;          # Linux: самый эффективный механизм I/O
}

Параметр worker_connections определяет максимальное количество одновременных соединений на один worker. При 8 ядрах и 16384 connections на worker, Nginx может обслуживать до 131 072 одновременных соединений. worker_rlimit_nofile увеличивает лимит открытых файлов (каждое соединение — два файловых дескриптора).

Настройка HTTP

Основные параметры блока http, влияющие на производительность: keepalive, буферы, таймауты и сжатие.

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    
    keepalive_timeout 30;
    keepalive_requests 1000;
    
    client_body_buffer_size 16k;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    client_max_body_size 8m;
    
    types_hash_max_size 2048;
    server_tokens off;
    
    # Сжатие
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_types
        text/plain text/css text/xml text/javascript
        application/json application/javascript application/xml
        application/rss+xml image/svg+xml;
}

sendfile on включает системный вызов sendfile(), который передаёт файлы напрямую из дискового буфера в сетевой, минуя копирование в userspace. tcp_nopush оптимизирует отправку HTTP-заголовков и начала файла в одном TCP-пакете. tcp_nodelay отключает алгоритм Nagle, уменьшая задержку для мелких пакетов.

Upstream и балансировка

Для reverse proxy важно правильно настроить upstream с keepalive-соединениями к backend-серверам. Без keepalive Nginx будет устанавливать новое TCP-соединение к backend на каждый запрос, что создаёт значительный overhead.

upstream backend {
    least_conn;
    
    server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.13:8080 max_fails=3 fail_timeout=30s;
    
    keepalive 128;
    keepalive_timeout 60s;
    keepalive_requests 10000;
}

server {
    listen 80;
    
    location /api/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 30s;
        
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 16k;
    }
}

Кеширование

Nginx может кешировать ответы от backend-серверов, значительно снижая нагрузку. Для статических ресурсов и не часто меняющихся API-ответов это даёт колоссальный эффект.

proxy_cache_path /var/cache/nginx levels=1:2
    keys_zone=api_cache:10m
    max_size=1g
    inactive=60m
    use_temp_path=off;

location /api/catalog/ {
    proxy_pass http://backend;
    proxy_cache api_cache;
    proxy_cache_valid 200 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_use_stale error timeout updating;
    proxy_cache_background_update on;
    proxy_cache_lock on;
    
    add_header X-Cache-Status $upstream_cache_status;
}

proxy_cache_use_stale позволяет отдавать устаревший кеш при ошибках backend — это критически важно для доступности. proxy_cache_lock предотвращает cache stampede: при одновременных запросах к одному ключу только один запрос идёт к backend, остальные ждут результат.

Rate Limiting

Для защиты от DDoS и злоупотреблений настройте rate limiting.

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

location /api/ {
    limit_req zone=api_limit burst=200 nodelay;
    limit_conn conn_limit 50;
    limit_req_status 429;
    
    proxy_pass http://backend;
}

Настройка ядра Linux

Nginx не существует в вакууме — производительность зависит от настроек ядра Linux. Ключевые sysctl-параметры для high-load серверов необходимо адаптировать: увеличить размеры буферов TCP, включить TCP Fast Open, настроить параметры TIME_WAIT и увеличить лимиты на количество файловых дескрипторов.

# /etc/sysctl.d/99-nginx-tuning.conf
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_fastopen = 3
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
fs.file-max = 2097152

Заключение

Настройка Nginx для высоких нагрузок — это комплексная задача, включающая оптимизацию worker-процессов, буферов, keepalive-соединений, кеширования и параметров ядра Linux. Каждое изменение должно быть протестировано под нагрузкой — используйте инструменты вроде wrk, ab или k6 для бенчмаркинга. Начните с профилирования текущей конфигурации, определите bottleneck и оптимизируйте итеративно.

ИС
Иван СоколовSRE-инженер, MaxTimes
← Назад к блогу

Linux hardening: чеклист безопасности сервера

Свежеустановленный Linux-сервер — открытая дверь для атакующих. Конфигурация «из коробки» оптимизирована для удобства, а не безопасности. В этой статье мы собрали комплексный чеклист действий по укреплению (hardening) Linux-сервера, которые значительно повышают уровень защиты.

1. Обновление системы

Первый и самый простой шаг — установить все обновления безопасности. Большинство взломов используют известные уязвимости, для которых давно выпущены патчи.

# Ubuntu/Debian
sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

# CentOS/RHEL
sudo dnf update -y
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic-install.timer

2. Настройка SSH

SSH — основной вектор атаки на серверы. Измените конфигурацию для максимальной безопасности.

# /etc/ssh/sshd_config
Port 2222                          # нестандартный порт
PermitRootLogin no                 # запрет входа под root
PasswordAuthentication no          # только SSH-ключи
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
AllowUsers deployer admin
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding no
Protocol 2

# Криптография
KexAlgorithms curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Перезапуск SSH (не забудьте проверить доступ!)
sudo sshd -t && sudo systemctl restart sshd

3. Файрвол (iptables/nftables)

Настройте файрвол по принципу «запрещено всё, что не разрешено явно». Оставьте открытыми только необходимые порты.

# UFW (Ubuntu)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw enable

# Проверка
sudo ufw status verbose

4. Fail2Ban

Fail2Ban анализирует логи и блокирует IP-адреса, совершающие подозрительные действия (брутфорс SSH, множественные 404 на веб-сервере).

# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = ufw

[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5

5. Управление пользователями

Создайте отдельного пользователя для каждой роли. Не используйте root для повседневных операций. Настройте sudo с ограничениями.

# Создание пользователя с ограниченными правами sudo
sudo adduser deployer
sudo usermod -aG sudo deployer

# Ограничение sudo только для конкретных команд
# /etc/sudoers.d/deployer
deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, \
    /usr/bin/systemctl restart app, \
    /usr/bin/journalctl

6. Аудит и логирование

Настройте auditd для отслеживания критических действий: изменения файлов конфигурации, запуск процессов от root, изменения в /etc/passwd и /etc/shadow. Логи должны отправляться на удалённый сервер (чтобы атакующий не мог их удалить).

# /etc/audit/rules.d/hardening.rules
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/ssh/sshd_config -p wa -k sshd_config
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands

7. Шифрование дисков

Для серверов с чувствительными данными настройте шифрование дисков с помощью LUKS. Это защищает данные при физическом доступе к серверу или краже дисков.

8. Kernel hardening

Настройте параметры ядра для защиты от типовых атак.

# /etc/sysctl.d/99-hardening.conf
# Защита от IP spoofing
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Игнорировать ICMP broadcast
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Защита от SYN flood
net.ipv4.tcp_syncookies = 1

# Отключить маршрутизацию
net.ipv4.ip_forward = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

# Защита адресного пространства
kernel.randomize_va_space = 2
kernel.kptr_restrict = 2

# Ограничение dmesg
kernel.dmesg_restrict = 1

9. Ограничение сервисов

Отключите все ненужные сервисы. Каждый запущенный сервис — потенциальная точка входа для атакующего. Используйте systemctl list-units --type=service --state=running для просмотра запущенных сервисов и отключите всё лишнее.

10. Регулярный аудит

Безопасность — это непрерывный процесс, а не разовое действие. Настройте автоматические проверки с помощью инструментов: Lynis для аудита конфигурации, ClamAV для антивирусного сканирования, rkhunter для обнаружения руткитов и AIDE для мониторинга целостности файловой системы.

# Автоматический аудит с Lynis
sudo apt install lynis
sudo lynis audit system --quick

# Проверка руткитов
sudo apt install rkhunter
sudo rkhunter --check --skip-keypress

Заключение

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

МК
Мария КозловаСпециалист по информационной безопасности, MaxTimes
← Назад к блогу

GitOps: полный workflow с Flux и Kustomize

GitOps — это операционная модель, при которой Git-репозиторий является единственным источником истины для инфраструктуры и конфигурации приложений. Любое изменение — деплой новой версии, изменение конфигурации, масштабирование — проходит через pull request, ревью и мерж. В этой статье мы настроим полный GitOps-цикл с использованием Flux CD и Kustomize.

Почему GitOps

Традиционный CI/CD подход — push-модель: CI-сервер после сборки напрямую применяет изменения к кластеру через kubectl apply. Это создаёт несколько проблем: CI-серверу нужны credentials для доступа к кластеру, нет гарантии, что состояние кластера соответствует тому, что в Git, ручные изменения (через kubectl) не отслеживаются.

GitOps решает эти проблемы с помощью pull-модели: оператор внутри кластера (Flux) периодически проверяет Git-репозиторий и синхронизирует состояние кластера с тем, что описано в Git. Если кто-то вручную изменит ресурс в кластере, Flux автоматически вернёт его к состоянию из Git.

Установка Flux

# Установка Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash

# Bootstrap — создание GitOps-репозитория и установка Flux в кластер
flux bootstrap github \
  --owner=mycompany \
  --repository=gitops-infra \
  --branch=main \
  --path=clusters/production \
  --personal

Bootstrap создаёт Git-репозиторий (если не существует), устанавливает Flux-контроллеры в кластер и настраивает синхронизацию с репозиторием. Flux-контроллеры работают внутри кластера и не требуют внешних credentials для деплоя.

Структура репозитория

gitops-infra/
├── clusters/
│   ├── production/
│   │   ├── flux-system/        # автосгенерированные манифесты Flux
│   │   ├── infrastructure.yaml # ссылка на ./infrastructure
│   │   └── apps.yaml           # ссылка на ./apps
│   └── staging/
├── infrastructure/
│   ├── sources/                # Helm-репозитории, OCI-источники
│   ├── nginx-ingress/
│   ├── cert-manager/
│   └── monitoring/
└── apps/
    ├── base/                   # базовые манифесты
    │   ├── api/
    │   │   ├── deployment.yaml
    │   │   ├── service.yaml
    │   │   └── kustomization.yaml
    │   └── web/
    ├── staging/                # overlay для staging
    │   ├── kustomization.yaml
    │   └── patches/
    └── production/             # overlay для production
        ├── kustomization.yaml
        └── patches/

Kustomize: управление окружениями

Kustomize позволяет иметь единую базу манифестов и применять патчи для разных окружений. Вместо дублирования YAML-файлов мы описываем отличия staging от production.

# apps/base/api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: registry.example.com/api:latest
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
# apps/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
  - ../base/api
  - ../base/web
patches:
  - target:
      kind: Deployment
      name: api
    patch: |
      - op: replace
        path: /spec/replicas
        value: 5
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/cpu
        value: 500m
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/memory
        value: 512Mi
images:
  - name: registry.example.com/api
    newTag: v2.3.1

Автоматическое обновление образов

Flux может автоматически обнаруживать новые версии Docker-образов в registry и обновлять манифесты в Git. Это замыкает цикл: CI собирает образ и пушит в registry, Flux обнаруживает новый тег, создаёт коммит в Git с обновлённым тегом, синхронизирует кластер.

# Image policy: принимать только semver-теги
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: api
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: api
  policy:
    semver:
      range: ">=2.0.0"

# Image update automation
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: flux-system
  namespace: flux-system
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: flux-system
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: flux@example.com
        name: Flux
      messageTemplate: "chore: update images {{range .Changed.Changes}}{{.OldValue}} -> {{.NewValue}}{{end}}"
    push:
      branch: main
  update:
    path: ./apps
    strategy: Setters

Мониторинг и алерты

Flux интегрируется с системами уведомлений. При успешном или неудачном деплое можно получать сообщения в Slack, Teams или через webhook.

apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Provider
metadata:
  name: slack
  namespace: flux-system
spec:
  type: slack
  channel: deployments
  secretRef:
    name: slack-webhook
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Alert
metadata:
  name: on-call
  namespace: flux-system
spec:
  providerRef:
    name: slack
  eventSeverity: error
  eventSources:
  - kind: Kustomization
    name: '*'
  - kind: HelmRelease
    name: '*'

Заключение

GitOps с Flux и Kustomize обеспечивает предсказуемый, аудируемый и автоматизированный процесс деплоя. Git становится единственным источником истины, каждое изменение проходит через code review, а Flux гарантирует, что кластер всегда соответствует желаемому состоянию. Начните с базовой настройки Flux и постепенно добавляйте автоматизацию: image updates, уведомления и политики.

АП
Алексей ПетровDevOps-инженер, MaxTimes
← Назад к блогу

Disaster Recovery: стратегии и автоматизация

Disaster Recovery (DR) — это совокупность процессов и инструментов для восстановления IT-систем после катастрофических сбоев: выход из строя дата-центра, потеря данных, масштабная кибератака. Компании, не имеющие DR-плана, рискуют полностью потерять бизнес при серьёзном инциденте. В этой статье мы разберём стратегии DR, ключевые метрики и подходы к автоматизации фейловера.

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

Два фундаментальных параметра DR-плана: RPO (Recovery Point Objective) — максимально допустимый объём потерянных данных, измеряется в единицах времени. RPO = 1 час означает, что при восстановлении вы потеряете максимум 1 час данных. RPO определяет частоту бэкапов или репликации. RTO (Recovery Time Objective) — максимально допустимое время простоя. RTO = 4 часа означает, что система должна быть восстановлена в течение 4 часов после инцидента. RTO определяет выбор стратегии DR.

Стратегии DR

Существует четыре основных стратегии, различающихся стоимостью и RTO. Backup & Restore — самая дешёвая, но самая медленная стратегия. Данные регулярно бэкапятся в другой регион, при катастрофе инфраструктура разворачивается с нуля и данные восстанавливаются из бэкапа. RTO: часы-дни. Pilot Light — минимальная инфраструктура (база данных) постоянно реплицируется в DR-регион. При катастрофе поднимаются остальные компоненты. RTO: десятки минут. Warm Standby — полная инфраструктура в DR-регионе работает в уменьшенном масштабе. При катастрофе масштабируется до production-нагрузки. RTO: минуты. Active-Active (Multi-Region) — обе площадки обслуживают трафик одновременно. При выходе одной из строя вторая принимает всю нагрузку. RTO: секунды.

Автоматизация бэкапов

Бэкапы должны быть автоматическими, шифрованными и регулярно проверяемыми на восстановимость. Самая частая ошибка — бэкапы делаются, но никто не проверяет, можно ли из них восстановиться.

#!/bin/bash
# backup-postgres.sh — автоматический бэкап PostgreSQL в S3
set -euo pipefail

DB_NAME="production"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="/tmp/${DB_NAME}_${TIMESTAMP}.sql.gz"
S3_BUCKET="s3://company-backups/postgres"
RETENTION_DAYS=30

pg_dump "$DB_NAME" | gzip | \
  gpg --encrypt --recipient backup@company.com \
  > "$BACKUP_FILE"

aws s3 cp "$BACKUP_FILE" "${S3_BUCKET}/${DB_NAME}_${TIMESTAMP}.sql.gz.gpg"

rm -f "$BACKUP_FILE"

aws s3 ls "$S3_BUCKET/" | \
  while read -r line; do
    file_date=$(echo "$line" | awk '{print $1}')
    file_name=$(echo "$line" | awk '{print $4}')
    if [[ $(date -d "$file_date" +%s) -lt $(date -d "-${RETENTION_DAYS} days" +%s) ]]; then
      aws s3 rm "${S3_BUCKET}/${file_name}"
    fi
  done

echo "Backup completed: ${DB_NAME}_${TIMESTAMP}"

Автоматический фейловер базы данных

Для критических систем ручной фейловер неприемлем — RTO будет слишком высоким. Автоматический фейловер для PostgreSQL реализуется с помощью Patroni — кластерного менеджера, который автоматически промотирует реплику в мастер при недоступности текущего мастера.

# patroni.yml — конфигурация Patroni
scope: postgres-cluster
name: node1

restapi:
  listen: 0.0.0.0:8008

etcd:
  hosts: etcd1:2379,etcd2:2379,etcd3:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      parameters:
        max_connections: 200
        shared_buffers: 4GB
        wal_level: replica
        max_wal_senders: 5
        max_replication_slots: 5

postgresql:
  listen: 0.0.0.0:5432
  data_dir: /var/lib/postgresql/data
  authentication:
    replication:
      username: replicator
      password: secret
    superuser:
      username: postgres
      password: secret

DR для Kubernetes

Для Kubernetes-кластеров DR включает бэкап etcd (хранилище состояния кластера), бэкап Persistent Volumes, бэкап ресурсов Kubernetes (Deployments, ConfigMaps, Secrets). Velero — популярный инструмент для бэкапа и восстановления ресурсов Kubernetes. Он создаёт снапшоты PV и бэкапы ресурсов, которые можно восстановить в другом кластере.

# Установка Velero с AWS S3 backend
velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.8.0 \
  --bucket velero-backups \
  --backup-location-config region=eu-west-1 \
  --snapshot-location-config region=eu-west-1

# Создание расписания бэкапов
velero schedule create daily-backup \
  --schedule="0 2 * * *" \
  --ttl 720h \
  --include-namespaces production,staging

# Восстановление
velero restore create --from-backup daily-backup-20251228

Тестирование DR

DR-план, который не тестируется, — это не план, а набор надежд. Тестирование должно проводиться регулярно (минимум раз в квартал) и включать: tabletop exercises (обсуждение сценариев на бумаге), частичные тесты (восстановление отдельных компонентов из бэкапа), полные тесты (имитация катастрофы с фейловером на DR-площадку) и chaos engineering (намеренное создание сбоев в production для проверки устойчивости).

Заключение

Disaster Recovery — это страховка, которая стоит денег, но окупается при первом серьёзном инциденте. Определите RPO и RTO на основе бизнес-требований, выберите соответствующую стратегию и автоматизируйте все процессы. Самое важное — регулярное тестирование: только так вы убедитесь, что DR-план работает, когда он реально понадобится.

ИС
Иван СоколовSRE-инженер, MaxTimes
← Назад к блогу

Оптимизация затрат в AWS: экономим 50% бюджета

Облачная инфраструктура даёт гибкость, но при отсутствии контроля за расходами счёт может вырасти в разы. По нашему опыту, типичная компания переплачивает за AWS на 30-60%. В этой статье мы разберём конкретные методы оптимизации, которые позволили нашим клиентам сократить облачные расходы на 50% без потери производительности.

1. Right-sizing инстансов

Самая частая проблема — oversized инстансы. Разработчики выбирают тип инстанса с запасом «на всякий случай», и этот запас остаётся навсегда. Используйте AWS Compute Optimizer или CloudWatch для анализа утилизации. Если CPU загружен менее чем на 20% и память — менее чем на 40%, инстанс oversized.

# AWS CLI: анализ утилизации EC2
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2025-12-01T00:00:00Z \
  --end-time 2025-12-21T00:00:00Z \
  --period 86400 \
  --statistics Average Maximum \
  --output table

Переход с m5.xlarge (4 vCPU, 16 GB RAM, ~$140/мес) на m5.large (2 vCPU, 8 GB RAM, ~$70/мес) при средней утилизации 15% CPU экономит 50% без какого-либо влияния на производительность.

2. Reserved Instances и Savings Plans

On-demand — самый дорогой режим оплаты. Для стабильных рабочих нагрузок используйте Reserved Instances (RI) или Savings Plans (SP). RI на 1 год с частичной предоплатой экономит ~40%, на 3 года — ~60%. Savings Plans более гибкие: скидка применяется к любому типу инстанса в рамках семейства.

3. Spot Instances для stateless-нагрузок

Spot Instances стоят на 60-90% дешевле On-demand, но могут быть отозваны AWS с 2-минутным предупреждением. Подходят для CI/CD-runners, batch-обработки, нагрузочного тестирования и stateless-компонентов в Kubernetes. EKS поддерживает mixed-node groups, где часть нод — On-demand (для стабильности), часть — Spot (для экономии).

# EKS Node Group с Spot-инстансами (Terraform)
resource "aws_eks_node_group" "spot" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "spot-workers"
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = aws_subnet.private[*].id
  capacity_type   = "SPOT"
  instance_types  = ["m5.large", "m5a.large", "m5d.large", "m4.large"]

  scaling_config {
    desired_size = 4
    min_size     = 2
    max_size     = 10
  }

  labels = {
    "workload-type" = "spot"
  }

  taint {
    key    = "spot"
    value  = "true"
    effect = "NO_SCHEDULE"
  }
}

4. S3 Lifecycle Policies

Данные в S3 часто хранятся в стандартном классе хранения, даже когда к ним не обращаются месяцами. Настройте lifecycle policies для автоматического перемещения данных в более дешёвые классы хранения.

{
  "Rules": [
    {
      "ID": "ArchiveOldData",
      "Status": "Enabled",
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        },
        {
          "Days": 365,
          "StorageClass": "DEEP_ARCHIVE"
        }
      ],
      "Expiration": {
        "Days": 730
      }
    }
  ]
}

Standard IA стоит ~45% дешевле Standard, Glacier — ~80% дешевле, Deep Archive — ~95% дешевле. Для логов и архивных данных экономия может быть существенной.

5. Удаление неиспользуемых ресурсов

«Мусорные» ресурсы — незаметная, но значительная статья расходов. Неиспользуемые Elastic IP (стоят денег, когда не привязаны к инстансу), устаревшие EBS-снапшоты, забытые load balancer-ы без target groups, неиспользуемые RDS-инстансы в dev-окружениях. Автоматизируйте обнаружение с помощью AWS Trusted Advisor или скриптов.

6. Auto-scaling и schedule-based scaling

Dev и staging окружения часто работают 24/7, хотя используются только в рабочие часы. Настройте schedule-based scaling для выключения нод в нерабочее время.

# Останавливаем dev-кластер на ночь и выходные
aws autoscaling put-scheduled-action \
  --auto-scaling-group-name dev-asg \
  --scheduled-action-name scale-down-night \
  --recurrence "0 20 * * 1-5" \
  --desired-capacity 0 \
  --min-size 0

aws autoscaling put-scheduled-action \
  --auto-scaling-group-name dev-asg \
  --scheduled-action-name scale-up-morning \
  --recurrence "0 8 * * 1-5" \
  --desired-capacity 3 \
  --min-size 1

7. Мониторинг затрат

Без мониторинга оптимизация одноразовая. Настройте AWS Cost Explorer, Budget Alerts и теги для атрибуции расходов по проектам и командам. Установите бюджетные алерты на аномальный рост расходов.

Заключение

Оптимизация облачных затрат — непрерывный процесс, а не разовый проект. Начните с аудита текущих расходов, определите top-5 статей затрат и применяйте оптимизации итеративно, измеряя эффект каждой. Комбинация right-sizing, reserved capacity, spot instances и lifecycle policies обычно даёт экономию 40-60% без негативного влияния на производительность.

СК
Сергей КузнецовАрхитектор, MaxTimes
← Назад к блогу

Service Mesh: Istio vs Linkerd в production

Service Mesh — инфраструктурный слой для управления межсервисной коммуникацией в микросервисной архитектуре. Он берёт на себя задачи, которые иначе пришлось бы решать в коде каждого сервиса: mTLS, балансировку нагрузки, circuit breaking, retries, observability. Istio и Linkerd — два наиболее зрелых решения. В этой статье мы сравним их на основе реального опыта эксплуатации.

Как работает Service Mesh

Оба решения используют паттерн sidecar proxy: к каждому поду добавляется контейнер с proxy-сервером, который перехватывает весь входящий и исходящий трафик. Это позволяет реализовать политики безопасности, наблюдаемости и маршрутизации без изменения кода приложений. Istio использует Envoy proxy, Linkerd — собственный micro-proxy на Rust (linkerd2-proxy).

Istio: мощь и сложность

Istio — наиболее функциональный Service Mesh с поддержкой множества протоколов, сложных правил маршрутизации, расширяемой модели через WASM-плагины. Istio включает собственную плоскость управления (istiod), которая конфигурирует Envoy-прокси. Преимущества Istio: богатая функциональность (traffic management, security, observability), поддержка multi-cluster и multi-mesh, расширяемость через WebAssembly, большое сообщество и backing от Google. Недостатки: высокое потребление ресурсов (sidecar Envoy использует 50-100 MB RAM на под), сложная конфигурация, крутая кривая обучения.

Linkerd: простота и производительность

Linkerd фокусируется на простоте, безопасности и производительности. Micro-proxy на Rust потребляет значительно меньше ресурсов, чем Envoy. Установка и настройка занимают минуты, а не часы. Преимущества Linkerd: минимальное потребление ресурсов (sidecar ~10-20 MB RAM), простая установка и эксплуатация, автоматический mTLS из коробки, отличные встроенные дашборды. Недостатки: менее гибкая маршрутизация по сравнению с Istio, нет поддержки WASM-расширений, меньше enterprise-функций.

Сравнение ресурсов

На кластере из 100 подов Istio добавляет ~5-10 GB суммарного потребления RAM (sidecar + control plane), Linkerd — ~1-2 GB. Latency: Istio sidecar добавляет ~2-3 мс к каждому запросу, Linkerd — ~0.5-1 мс. При тысячах межсервисных вызовов на один пользовательский запрос разница становится ощутимой.

mTLS

Оба решения поддерживают автоматический mTLS для шифрования межсервисного трафика. Linkerd включает mTLS автоматически при установке, без дополнительной конфигурации. Istio требует настройки PeerAuthentication policy.

# Linkerd: mTLS включён автоматически
# Проверка статуса mTLS
linkerd viz edges deployment -n production

# Istio: явная настройка mTLS
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

Traffic Management

Istio предоставляет более гибкие возможности управления трафиком: canary deployments с процентным распределением, A/B-тестирование по HTTP-заголовкам, fault injection для тестирования устойчивости, traffic mirroring (зеркалирование трафика для тестирования). Linkerd поддерживает базовое процентное распределение через TrafficSplit (SMI spec), но не имеет таких расширенных возможностей, как fault injection или header-based routing.

# Istio: Canary deployment (90% → stable, 10% → canary)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api
spec:
  hosts:
  - api
  http:
  - route:
    - destination:
        host: api
        subset: stable
      weight: 90
    - destination:
        host: api
        subset: canary
      weight: 10

Когда что выбрать

Выбирайте Linkerd, если: команда небольшая и нет выделенного platform-team, приоритет — простота эксплуатации и низкий overhead, достаточно базовых функций (mTLS, observability, retries). Выбирайте Istio, если: нужны сложные правила маршрутизации (canary, A/B, fault injection), требуется multi-cluster mesh, есть выделенная platform-команда для эксплуатации, нужна расширяемость через WASM.

Заключение

Оба решения зрелые и production-ready. Linkerd — лучший выбор для команд, которые ценят простоту и хотят быстро получить mTLS и observability. Istio — для организаций с более сложными требованиями к маршрутизации и безопасности, готовых инвестировать в эксплуатацию. Помните, что Service Mesh — это дополнительный слой сложности, и внедрять его стоит только тогда, когда задачи (mTLS, observability, traffic management) действительно актуальны для вашей архитектуры.

СК
Сергей КузнецовАрхитектор, MaxTimes
← Назад к блогу

Шардирование PostgreSQL: горизонтальное масштабирование

PostgreSQL — мощная СУБД, но у одного сервера есть физические пределы: объём данных, количество записей в секунду, количество соединений. Когда вертикальное масштабирование (bigger hardware) достигает потолка, а read replicas не решают проблему записи, остаётся горизонтальное масштабирование — шардирование. Эта статья разбирает подходы к шардированию PostgreSQL, их плюсы и минусы.

Когда шардировать

Шардирование — сложная операция с серьёзными последствиями для архитектуры. Перед тем как принять решение о шардировании, убедитесь, что вы исчерпали более простые варианты: оптимизация запросов (индексы, EXPLAIN ANALYZE), партиционирование таблиц (partitioning), вертикальное масштабирование (больше CPU, RAM, SSD), read replicas для распределения читающей нагрузки, вынос аналитических запросов в отдельную реплику или ClickHouse.

Шардирование оправдано, если: размер таблицы превышает 1 ТБ, количество записей превышает возможности одного сервера, нужна географическая распределённость данных.

Выбор ключа шардирования

Ключ шардирования (shard key) — самое важное решение. Он определяет, на какой шард попадёт каждая строка. Хороший ключ обеспечивает равномерное распределение данных между шардами, минимизирует кросс-шардовые запросы и не создаёт «горячих» шардов, куда идёт непропорционально много записей.

Типичные варианты: user_id — для многоарендных систем (все данные пользователя на одном шарде), tenant_id — для SaaS (все данные организации вместе), hash(id) — для равномерного распределения, geography — для геораспределённых систем.

-- Определение шарда через хеш-функцию
-- shard_number = hashtext(user_id::text) % num_shards

-- Пример для 8 шардов
SELECT hashtext('user_12345'::text) % 8 AS shard_id;
-- Результат: 3 (данные этого пользователя на шарде 3)

Подход 1: Citus (расширение PostgreSQL)

Citus — расширение PostgreSQL, которое добавляет прозрачное шардирование. Данные распределяются по worker-нодам, а координатор маршрутизирует запросы. Для приложения Citus выглядит как обычный PostgreSQL.

-- Установка Citus и создание распределённой таблицы
CREATE EXTENSION citus;

-- Добавление worker-нод
SELECT citus_add_node('worker-1', 5432);
SELECT citus_add_node('worker-2', 5432);
SELECT citus_add_node('worker-3', 5432);

-- Создание таблицы
CREATE TABLE orders (
    id BIGSERIAL,
    user_id BIGINT NOT NULL,
    product_id BIGINT,
    amount DECIMAL(12,2),
    status TEXT,
    created_at TIMESTAMPTZ DEFAULT now()
);

-- Распределение по user_id (32 шарда)
SELECT create_distributed_table('orders', 'user_id');

-- Запросы работают как обычно
SELECT * FROM orders WHERE user_id = 12345;
INSERT INTO orders (user_id, product_id, amount) VALUES (12345, 67, 99.90);

Преимущества Citus: прозрачность для приложения (стандартный SQL), co-location связанных таблиц (все данные пользователя на одном шарде), поддержка распределённых JOIN-ов и агрегаций, online rebalancing шардов.

Подход 2: Application-level sharding

При application-level шардировании логика маршрутизации реализуется в приложении. Приложение знает о всех шардах и направляет запросы к нужному на основе shard key. Этот подход даёт максимальный контроль, но увеличивает сложность приложения.

# Python: application-level sharding
import hashlib
from sqlalchemy import create_engine

SHARDS = {
    0: "postgresql://user:pass@shard-0:5432/orders",
    1: "postgresql://user:pass@shard-1:5432/orders",
    2: "postgresql://user:pass@shard-2:5432/orders",
    3: "postgresql://user:pass@shard-3:5432/orders",
}

engines = {i: create_engine(url) for i, url in SHARDS.items()}

def get_shard(user_id: int) -> int:
    return int(hashlib.md5(str(user_id).encode()).hexdigest(), 16) % len(SHARDS)

def get_user_orders(user_id: int):
    shard_id = get_shard(user_id)
    engine = engines[shard_id]
    with engine.connect() as conn:
        result = conn.execute(
            "SELECT * FROM orders WHERE user_id = %s ORDER BY created_at DESC",
            (user_id,)
        )
        return result.fetchall()

Подход 3: Foreign Data Wrappers (FDW)

PostgreSQL FDW позволяет создать «виртуальную» таблицу, которая объединяет данные с нескольких серверов. В сочетании с партиционированием это даёт базовое шардирование без внешних инструментов. Однако производительность FDW ограничена, и для высоконагруженных систем лучше использовать Citus или application-level sharding.

Проблемы шардирования

Шардирование создаёт новые проблемы, которые нужно решать. Кросс-шардовые JOIN-ы — невозможны или дороги; проектируйте схему данных так, чтобы связанные данные были на одном шарде (co-location). Распределённые транзакции — ACID-транзакции внутри одного шарда работают нормально, но кросс-шардовые требуют 2PC (two-phase commit) с его overhead-ом. Global unique IDs — автоинкремент не работает через шарды; используйте UUID, Snowflake ID или выделенный сервис генерации ID. Resharding — при добавлении новых шардов данные нужно перераспределять; планируйте это заранее (consistent hashing упрощает задачу).

Заключение

Шардирование PostgreSQL — мощный, но сложный инструмент. Используйте его только когда более простые методы масштабирования исчерпаны. Citus — лучший выбор для большинства сценариев благодаря прозрачности для приложения. Application-level sharding даёт максимальный контроль, но требует значительных инвестиций в инфраструктурный код. Независимо от подхода, правильный выбор shard key — ключевое решение, определяющее успех шардирования.

ДВ
Дмитрий ВолковBackend-разработчик, MaxTimes
← Назад к блогу

Event-driven архитектура на Apache Kafka

Event-driven архитектура (EDA) — подход к проектированию систем, в котором компоненты взаимодействуют через события (events), а не через прямые синхронные вызовы. Apache Kafka — самая популярная платформа для реализации EDA, обеспечивающая надёжную, масштабируемую и отказоустойчивую передачу событий. В этой статье разберём ключевые концепции Kafka и паттерны проектирования event-driven систем.

Почему Event-driven

Синхронная архитектура (REST-вызовы между сервисами) создаёт жёсткую связанность: если один сервис недоступен, все зависимые сервисы деградируют. EDA решает эту проблему: сервис-производитель (producer) публикует событие в Kafka, не зная, кто его получит. Сервисы-потребители (consumers) подписываются на нужные топики и обрабатывают события асинхронно.

Преимущества EDA: слабая связанность сервисов (loose coupling), масштабируемость (потребители масштабируются независимо), отказоустойчивость (Kafka буферизирует события при недоступности потребителя), возможность воспроизведения событий (event replay) и auditability (полная история всех событий).

Ключевые концепции Kafka

Topic — именованный поток событий (аналог таблицы в БД). Partition — раздел топика, обеспечивающий параллельную обработку. Событие с одинаковым ключом всегда попадает в одну партицию, что гарантирует порядок обработки. Consumer Group — группа потребителей, между которыми партиции распределяются автоматически. Это обеспечивает параллельную обработку и отказоустойчивость: при падении одного consumer его партиции перераспределяются между оставшимися.

Проектирование событий

Правильное проектирование событий — ключ к успешной EDA. Событие должно быть самодостаточным (содержать всю информацию для обработки), иммутабельным (нельзя изменить, только добавить новое) и версионированным (для обратной совместимости).

// Пример события в формате JSON (с Avro/Protobuf schema в production)
{
  "event_id": "evt_7a8b9c",
  "event_type": "order.created",
  "event_version": "v2",
  "timestamp": "2025-11-30T14:30:00Z",
  "source": "orders-service",
  "data": {
    "order_id": "ord_123456",
    "user_id": "usr_789",
    "items": [
      {"product_id": "prod_42", "quantity": 2, "price": 1500.00},
      {"product_id": "prod_17", "quantity": 1, "price": 3200.00}
    ],
    "total": 6200.00,
    "currency": "RUB",
    "shipping_address": {
      "city": "Москва",
      "street": "ул. Пушкина, д. 10"
    }
  },
  "metadata": {
    "correlation_id": "req_abc123",
    "user_agent": "mobile-app/2.1"
  }
}

Паттерн: Event Sourcing

Event Sourcing — паттерн, при котором состояние системы определяется последовательностью событий, а не текущим снимком в БД. Вместо UPDATE записей мы добавляем новые события: OrderCreated, ItemAdded, PaymentReceived, OrderShipped. Текущее состояние заказа восстанавливается путём воспроизведения всех событий.

# Python: восстановление состояния из событий
class Order:
    def __init__(self):
        self.id = None
        self.status = None
        self.items = []
        self.total = 0

    def apply(self, event):
        handler = getattr(self, f"_apply_{event['event_type']}", None)
        if handler:
            handler(event['data'])

    def _apply_order_created(self, data):
        self.id = data['order_id']
        self.status = 'created'
        self.items = data['items']
        self.total = data['total']

    def _apply_payment_received(self, data):
        self.status = 'paid'

    def _apply_order_shipped(self, data):
        self.status = 'shipped'

def rebuild_order(events):
    order = Order()
    for event in events:
        order.apply(event)
    return order

Паттерн: CQRS

CQRS (Command Query Responsibility Segregation) часто используется вместе с Event Sourcing. Идея: разделить модели для записи (Command) и чтения (Query). Команды обрабатываются и порождают события, которые обновляют read-модели — оптимизированные представления данных для конкретных запросов. Это позволяет оптимизировать чтение и запись независимо друг от друга.

Kafka Streams и обработка событий

# docker-compose.yml для Kafka-кластера
version: '3.8'
services:
  kafka-1:
    image: confluentinc/cp-kafka:7.5.0
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093"
      KAFKA_LOG_RETENTION_HOURS: 168
      KAFKA_NUM_PARTITIONS: 12
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"

  kafka-2:
    image: confluentinc/cp-kafka:7.5.0
    environment:
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093"
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"

  kafka-3:
    image: confluentinc/cp-kafka:7.5.0
    environment:
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093"
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"

Гарантии доставки

Kafka поддерживает три уровня гарантий: at-most-once (сообщение может быть потеряно, но не продублировано), at-least-once (сообщение точно доставлено, но может быть продублировано), exactly-once (каждое сообщение обработано ровно один раз). Exactly-once семантика в Kafka достигается через идемпотентный producer (enable.idempotence=true) и транзакционный API. Однако end-to-end exactly-once требует, чтобы потребитель также обеспечивал идемпотентность обработки.

Мониторинг Kafka

Критические метрики для мониторинга Kafka: consumer lag (отставание потребителя от producer), under-replicated partitions (партиции с недостаточным количеством реплик), request latency (задержка обработки запросов), broker disk usage (использование диска). Consumer lag — самая важная метрика: если потребитель не успевает обрабатывать события, lag растёт, и данные становятся устаревшими.

Заключение

Apache Kafka — мощная платформа для event-driven архитектуры, но она добавляет сложность: необходимо управлять топиками, партициями, consumer groups и мониторить lag. Начните с простых сценариев (асинхронные уведомления, логирование событий), прежде чем переходить к Event Sourcing и CQRS. Правильно спроектированные события и топики — фундамент успешной EDA. Помните, что EDA — не серебряная пуля: не все задачи требуют асинхронной обработки, и иногда простой REST-вызов — лучшее решение.

СК
Сергей КузнецовАрхитектор, MaxTimes
← Назад к блогу

Observability: метрики, логи и трейсы в одном стеке

Observability (наблюдаемость) — это способность понять внутреннее состояние системы по её внешним данным. В отличие от мониторинга, который отвечает на заранее известные вопросы («работает ли сервис?»), observability позволяет расследовать неизвестные проблемы («почему запросы от пользователей из Казани медленнее?»). Три столпа observability: метрики, логи и трейсы. В этой статье мы построим единый стек на основе Prometheus, Loki, Tempo и Grafana.

Три столпа observability

Метрики — числовые показатели, агрегированные во времени: RPS, latency (p50, p95, p99), error rate, CPU/RAM utilization. Метрики отвечают на вопрос «что происходит?» и являются основой для алертинга. Инструмент: Prometheus.

Логи — текстовые записи о событиях в системе. Логи отвечают на вопрос «почему это произошло?» — содержат детальную информацию о каждом запросе, ошибке, действии пользователя. Инструмент: Loki (или ELK Stack).

Трейсы (распределённые трейсы) — полный путь запроса через все сервисы. Трейс показывает, какие сервисы были вызваны, сколько времени заняла каждая операция, где возникло узкое место. Инструмент: Tempo (или Jaeger).

Стек: Prometheus + Loki + Tempo + Grafana

Grafana Labs предлагает полный open-source стек для observability, где все компоненты тесно интегрированы через Grafana. Из дашборда с метриками можно перейти к логам конкретного сервиса, а из лога — к трейсу конкретного запроса. Эта корреляция — ключевое преимущество единого стека.

Установка стека в Kubernetes

# Установка через Helm
helm repo add grafana https://grafana.github.io/helm-charts

# Loki (логи)
helm install loki grafana/loki-stack \
  --namespace monitoring \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.size=50Gi

# Tempo (трейсы)
helm install tempo grafana/tempo \
  --namespace monitoring \
  --set persistence.enabled=true

# Grafana (визуализация)
helm install grafana grafana/grafana \
  --namespace monitoring \
  --set adminPassword=secret \
  --set "datasources.datasources\\.yaml.apiVersion=1" \
  --set "datasources.datasources\\.yaml.datasources[0].name=Prometheus" \
  --set "datasources.datasources\\.yaml.datasources[0].type=prometheus" \
  --set "datasources.datasources\\.yaml.datasources[0].url=http://prometheus:9090"

Инструментирование приложения

Для полноценной observability приложение должно экспортировать все три типа данных. OpenTelemetry (OTel) — стандарт для инструментирования, который поддерживает метрики, логи и трейсы в одном SDK.

# Python: инструментирование с OpenTelemetry
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
import structlog

provider = TracerProvider()
provider.add_span_processor(
    BatchSpanExporter(OTLPSpanExporter(endpoint="tempo:4317"))
)
trace.set_tracer_provider(provider)

FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument(engine=db_engine)

logger = structlog.get_logger()

@app.route('/api/orders')
def get_orders():
    span = trace.get_current_span()
    trace_id = span.get_span_context().trace_id
    
    logger.info("fetching_orders",
                trace_id=format(trace_id, '032x'),
                user_id=request.user_id)
    
    orders = db.query_orders(request.user_id)
    
    logger.info("orders_fetched",
                trace_id=format(trace_id, '032x'),
                count=len(orders))
    
    return jsonify(orders)

Корреляция данных

Магия observability — в корреляции трёх типов данных. Когда алерт сообщает о высоком error rate (метрика), вы переходите к логам ошибок за этот период. В логе видите trace_id, по которому находите полный трейс запроса — и видите, что проблема в медленном ответе от сервиса рекомендаций. Grafana поддерживает такие переходы нативно: derived fields в Loki извлекают trace_id из логов и создают ссылку на Tempo.

Structured logging

Для эффективного поиска по логам используйте структурированный формат (JSON) вместо произвольного текста. Каждый лог содержит фиксированные поля: timestamp, level, service, trace_id, user_id, message. Это позволяет быстро фильтровать и агрегировать логи в Loki.

# Loki LogQL: запросы к логам
# Все ошибки в orders-service за последний час
{service="orders"} |= "error" | json | level="error"

# Логи конкретного запроса по trace_id
{service=~".+"} | json | trace_id="abc123def456"

# Топ-10 самых частых ошибок
sum by (error_message) (
  count_over_time({service="orders"} | json | level="error" [1h])
) | sort desc | limit 10

Sampling трейсов

Сохранение трейса для каждого запроса нецелесообразно при высокой нагрузке — это огромный объём данных. Используйте sampling: head-based sampling (решение о сохранении принимается в начале запроса, например, 10% всех запросов) или tail-based sampling (решение принимается после завершения запроса — сохраняются все ошибочные и медленные запросы). Tail-based sampling предпочтительнее, так как гарантирует сохранение трейсов проблемных запросов.

Заключение

Observability — это не набор инструментов, а культура. Инструментируйте каждый сервис, используйте единый trace_id для сквозной корреляции, настройте дашборды и алерты. Стек Prometheus + Loki + Tempo + Grafana предоставляет полноценное решение с tight integration и без vendor lock-in. Начните с метрик и алертов, затем добавьте структурированные логи и трейсы. Инвестиции в observability окупаются при первом серьёзном инциденте, когда вместо многочасового гадания вы находите причину за минуты.

ИС
Иван СоколовSRE-инженер, MaxTimes
← Назад к блогу

Нагрузочное тестирование с k6: от скрипта до отчёта

Нагрузочное тестирование — критически важный этап перед запуском в production и перед событиями с повышенной нагрузкой (распродажи, маркетинговые кампании). k6 — современный инструмент для нагрузочного тестирования от Grafana Labs, который выгодно отличается от JMeter и Gatling: скрипты пишутся на JavaScript, инструмент работает из командной строки и легко интегрируется в CI/CD.

Почему k6

k6 — open-source инструмент, написанный на Go, что обеспечивает высокую производительность при генерации нагрузки. Один инстанс k6 может генерировать десятки тысяч RPS. Скрипты на JavaScript понятны любому разработчику. Встроенная интеграция с Prometheus, Grafana Cloud, InfluxDB и другими системами мониторинга. Поддержка различных протоколов: HTTP, WebSocket, gRPC, SQL.

Первый скрипт

// load-test.js — базовый нагрузочный тест
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';

const errorRate = new Rate('errors');
const orderDuration = new Trend('order_creation_duration');

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // ramp-up до 50 VU
    { duration: '5m', target: 50 },   // удерживаем 50 VU
    { duration: '2m', target: 200 },  // ramp-up до 200 VU
    { duration: '5m', target: 200 },  // удерживаем 200 VU
    { duration: '2m', target: 0 },    // ramp-down
  ],
  thresholds: {
    http_req_duration: ['p(95) < 500', 'p(99) < 1000'],
    errors: ['rate < 0.05'],
    order_creation_duration: ['p(95) < 800'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://api.example.com';

export default function () {
  const catalogRes = http.get(`${BASE_URL}/api/catalog?page=1&limit=20`);
  check(catalogRes, {
    'catalog status 200': (r) => r.status === 200,
    'catalog has products': (r) => JSON.parse(r.body).length > 0,
  });
  errorRate.add(catalogRes.status !== 200);
  
  sleep(1);
  
  const productRes = http.get(`${BASE_URL}/api/products/42`);
  check(productRes, {
    'product status 200': (r) => r.status === 200,
  });
  
  sleep(0.5);
  
  const orderStart = Date.now();
  const orderRes = http.post(
    `${BASE_URL}/api/orders`,
    JSON.stringify({
      items: [{ product_id: 42, quantity: 1 }],
      shipping_address: { city: 'Москва', street: 'ул. Тестовая, д. 1' }
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  orderDuration.add(Date.now() - orderStart);
  
  check(orderRes, {
    'order created': (r) => r.status === 201,
    'order has id': (r) => JSON.parse(r.body).order_id !== undefined,
  });
  errorRate.add(orderRes.status !== 201);
  
  sleep(2);
}

Сценарии нагрузки

k6 поддерживает различные модели нагрузки. Constant VUs — фиксированное количество виртуальных пользователей. Ramping VUs — постепенное увеличение нагрузки (как в примере выше). Constant arrival rate — фиксированное количество итераций в секунду, независимо от времени ответа. Ramping arrival rate — нарастающая интенсивность запросов.

// Сценарий с разными типами нагрузки
export const options = {
  scenarios: {
    browse: {
      executor: 'constant-vus',
      vus: 100,
      duration: '10m',
      exec: 'browseCatalog',
    },
    purchase: {
      executor: 'ramping-arrival-rate',
      startRate: 10,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 200,
      stages: [
        { duration: '5m', target: 50 },
        { duration: '5m', target: 100 },
      ],
      exec: 'purchaseFlow',
    },
  },
};

export function browseCatalog() {
  http.get(`${BASE_URL}/api/catalog`);
  sleep(2);
}

export function purchaseFlow() {
  http.post(`${BASE_URL}/api/orders`, orderPayload);
}

Аутентификация и state

Реальные сценарии требуют аутентификации. k6 поддерживает работу с JWT-токенами, cookies и сессиями.

import http from 'k6/http';
import { check } from 'k6';

export function setup() {
  const loginRes = http.post(`${BASE_URL}/api/auth/login`, 
    JSON.stringify({
      email: 'loadtest@example.com',
      password: 'testpassword'
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  
  const token = JSON.parse(loginRes.body).access_token;
  return { token };
}

export default function (data) {
  const params = {
    headers: {
      'Authorization': `Bearer ${data.token}`,
      'Content-Type': 'application/json',
    },
  };
  
  const res = http.get(`${BASE_URL}/api/profile`, params);
  check(res, { 'authenticated': (r) => r.status === 200 });
}

Пороговые значения и критерии успеха

Thresholds определяют критерии прохождения теста. Если хотя бы один threshold не пройден, k6 завершается с кодом возврата 99, что позволяет использовать его в CI/CD для блокировки деплоя при деградации производительности.

export const options = {
  thresholds: {
    http_req_duration: [
      'p(95) < 500',    // 95% запросов быстрее 500мс
      'p(99) < 1500',   // 99% запросов быстрее 1.5с
      'max < 5000',     // максимум 5с
    ],
    http_req_failed: [
      'rate < 0.01',    // менее 1% ошибок
    ],
    checks: [
      'rate > 0.95',    // 95% проверок пройдено
    ],
  },
};

Интеграция с CI/CD

# .github/workflows/load-test.yml
name: Load Test
on:
  pull_request:
    branches: [main]
jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: grafana/k6-action@v0.3.0
      with:
        filename: tests/load/smoke-test.js
      env:
        BASE_URL: ${{ secrets.STAGING_URL }}

Визуализация результатов

k6 может отправлять метрики в реальном времени в Prometheus (через Remote Write), InfluxDB или Grafana Cloud. Это позволяет наблюдать за тестом в Grafana-дашборде и сопоставлять нагрузочные метрики с метриками приложения.

# Запуск k6 с выводом в Prometheus
k6 run \
  --out experimental-prometheus-rw \
  -e K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write \
  load-test.js

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

После завершения теста анализируйте не только средние значения, но и хвостовые задержки (p95, p99). Обращайте внимание на корреляцию: растёт ли latency с увеличением нагрузки (линейно, экспоненциально?), при каком количестве VU начинаются ошибки (точка насыщения), какие эндпоинты деградируют первыми. Типичные проблемы, выявляемые нагрузочным тестированием: утечки соединений к БД (connection pool exhaustion), неэффективные SQL-запросы (N+1 problem), отсутствие кеширования, блокировки (locks) в базе данных и исчерпание ресурсов (CPU, RAM, file descriptors).

Заключение

Нагрузочное тестирование с k6 — это не разовое мероприятие, а часть CI/CD-процесса. Запускайте smoke-тесты при каждом PR, полноценные нагрузочные тесты перед релизом и стресс-тесты перед крупными событиями. k6 упрощает написание, запуск и анализ тестов, а интеграция с Grafana позволяет визуализировать результаты в контексте других метрик системы. Начните с простого скрипта, определите baseline производительности и постепенно усложняйте сценарии, приближая их к реальным паттернам использования.

АМ
Анна МорозоваData Engineer, MaxTimes