architecture

Введение в масштабирование

Основы масштабирования приложений: вертикальное и горизонтальное масштабирование

#scaling #performance #architecture #databases

Введение в масштабирование

Масштабирование — это способность системы справляться с растущей нагрузкой путем добавления ресурсов. Существует два основных подхода: вертикальное и горизонтальное масштабирование.

Типы масштабирования

Вертикальное масштабирование (Scale Up)

Увеличение мощности существующего сервера.

// До масштабирования
const server = {
  cpu: '4 cores',
  ram: '8 GB',
  disk: '100 GB SSD'
};

// После вертикального масштабирования
const upgradedServer = {
  cpu: '16 cores',
  ram: '64 GB',
  disk: '1 TB NVMe'
};

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

  • Простота реализации
  • Не требует изменений в архитектуре
  • Меньше сложности в управлении

Недостатки:

  • Физические ограничения оборудования
  • Высокая стоимость топовых конфигураций
  • Единая точка отказа
  • Простой (downtime) при обновлении

Горизонтальное масштабирование (Scale Out)

Добавление большего количества серверов.

// До масштабирования
const infrastructure = {
  servers: [
    { id: 'server-1', capacity: 1000 }
  ]
};

// После горизонтального масштабирования
const scaledInfrastructure = {
  servers: [
    { id: 'server-1', capacity: 1000 },
    { id: 'server-2', capacity: 1000 },
    { id: 'server-3', capacity: 1000 }
  ],
  loadBalancer: 'nginx'
};

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

  • Практически неограниченное масштабирование
  • Отказоустойчивость
  • Более низкая стоимость
  • Возможность обновления без простоя

Недостатки:

  • Сложность архитектуры
  • Необходимость балансировки нагрузки
  • Проблемы с консистентностью данных
  • Сложность отладки

Метрики для принятия решений

Когда масштабировать?

interface SystemMetrics {
  cpu: number;        // Использование CPU (%)
  memory: number;     // Использование памяти (%)
  responseTime: number; // Время ответа (ms)
  throughput: number;   // Запросов в секунду
  errorRate: number;    // Процент ошибок
}

class ScalingDecision {
  shouldScale(metrics: SystemMetrics): boolean {
    return (
      metrics.cpu > 80 ||
      metrics.memory > 85 ||
      metrics.responseTime > 1000 ||
      metrics.errorRate > 1
    );
  }
  
  getScalingType(metrics: SystemMetrics): 'vertical' | 'horizontal' {
    // Если проблема в одном узле - вертикальное
    if (metrics.cpu > 90 && metrics.memory < 70) {
      return 'vertical';
    }
    
    // Если нагрузка распределяется - горизонтальное
    return 'horizontal';
  }
}

Закон Амдала

Теоретический предел ускорения при параллелизации:

class AmdahlsLaw {
  /**
   * Рассчитывает максимальное ускорение
   * @param parallelPortion - доля кода, которая может быть распараллелена (0-1)
   * @param processors - количество процессоров
   */
  calculateSpeedup(parallelPortion: number, processors: number): number {
    const serialPortion = 1 - parallelPortion;
    return 1 / (serialPortion + parallelPortion / processors);
  }
}

const law = new AmdahlsLaw();

// Если 95% кода параллелится
console.log(law.calculateSpeedup(0.95, 2));  // ~1.9x
console.log(law.calculateSpeedup(0.95, 4));  // ~3.5x
console.log(law.calculateSpeedup(0.95, 8));  // ~6.1x
console.log(law.calculateSpeedup(0.95, 16)); // ~10.3x

// Если только 50% кода параллелится
console.log(law.calculateSpeedup(0.5, 16));  // ~1.9x (!)

Паттерны масштабирования

Stateless приложения

// Хорошо: stateless сервис легко масштабируется
class StatelessOrderService {
  constructor(
    private database: Database,
    private cache: Cache
  ) {}
  
  async getOrder(orderId: string): Promise<Order> {
    // Состояние хранится вне сервиса
    const cached = await this.cache.get(`order:${orderId}`);
    if (cached) return cached;
    
    const order = await this.database.query(
      'SELECT * FROM orders WHERE id = ?',
      [orderId]
    );
    
    await this.cache.set(`order:${orderId}`, order, 300);
    return order;
  }
}
// Плохо: stateful сервис сложно масштабировать
class StatefulOrderService {
  // Состояние хранится в памяти сервиса
  private ordersCache = new Map<string, Order>();
  private activeConnections = new Set<WebSocket>();
  
  async getOrder(orderId: string): Promise<Order> {
    // При горизонтальном масштабировании
    // разные запросы попадут на разные серверы
    // и не найдут данные в кэше
    return this.ordersCache.get(orderId);
  }
}

Кэширование

class CachingStrategy {
  constructor(
    private redis: Redis,
    private database: Database
  ) {}
  
  // Cache-Aside паттерн
  async getCacheAside(key: string): Promise<any> {
    // 1. Проверяем кэш
    let data = await this.redis.get(key);
    
    if (!data) {
      // 2. Если нет - загружаем из БД
      data = await this.database.query(key);
      
      // 3. Сохраняем в кэш
      await this.redis.set(key, data, 'EX', 3600);
    }
    
    return data;
  }
  
  // Write-Through паттерн
  async writeThrough(key: string, value: any): Promise<void> {
    // 1. Пишем в БД
    await this.database.save(key, value);
    
    // 2. Обновляем кэш
    await this.redis.set(key, value, 'EX', 3600);
  }
  
  // Write-Behind паттерн
  async writeBehind(key: string, value: any): Promise<void> {
    // 1. Сразу пишем в кэш
    await this.redis.set(key, value, 'EX', 3600);
    
    // 2. Асинхронно пишем в БД
    setImmediate(async () => {
      await this.database.save(key, value);
    });
  }
}

Балансировка нагрузки

Round Robin

class RoundRobinBalancer {
  private currentIndex = 0;
  
  constructor(private servers: string[]) {}
  
  getNextServer(): string {
    const server = this.servers[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.servers.length;
    return server;
  }
}

const balancer = new RoundRobinBalancer([
  'server-1:3000',
  'server-2:3000',
  'server-3:3000'
]);

// Запросы распределяются равномерно
console.log(balancer.getNextServer()); // server-1:3000
console.log(balancer.getNextServer()); // server-2:3000
console.log(balancer.getNextServer()); // server-3:3000
console.log(balancer.getNextServer()); // server-1:3000

Least Connections

class LeastConnectionsBalancer {
  private connections = new Map<string, number>();
  
  constructor(private servers: string[]) {
    servers.forEach(server => this.connections.set(server, 0));
  }
  
  getNextServer(): string {
    let minConnections = Infinity;
    let selectedServer = this.servers[0];
    
    for (const server of this.servers) {
      const connections = this.connections.get(server) || 0;
      if (connections < minConnections) {
        minConnections = connections;
        selectedServer = server;
      }
    }
    
    this.connections.set(
      selectedServer,
      (this.connections.get(selectedServer) || 0) + 1
    );
    
    return selectedServer;
  }
  
  releaseConnection(server: string): void {
    const current = this.connections.get(server) || 0;
    this.connections.set(server, Math.max(0, current - 1));
  }
}

Мониторинг и автомасштабирование

interface AutoScalingConfig {
  minInstances: number;
  maxInstances: number;
  targetCPU: number;
  targetMemory: number;
  scaleUpThreshold: number;
  scaleDownThreshold: number;
  cooldownPeriod: number; // секунды
}

class AutoScaler {
  private lastScaleTime = 0;
  
  constructor(
    private config: AutoScalingConfig,
    private currentInstances: number
  ) {}
  
  async evaluate(metrics: SystemMetrics): Promise<number> {
    const now = Date.now();
    const timeSinceLastScale = (now - this.lastScaleTime) / 1000;
    
    // Проверяем cooldown период
    if (timeSinceLastScale < this.config.cooldownPeriod) {
      return this.currentInstances;
    }
    
    // Решаем о масштабировании
    if (this.shouldScaleUp(metrics)) {
      this.lastScaleTime = now;
      return Math.min(
        this.currentInstances + 1,
        this.config.maxInstances
      );
    }
    
    if (this.shouldScaleDown(metrics)) {
      this.lastScaleTime = now;
      return Math.max(
        this.currentInstances - 1,
        this.config.minInstances
      );
    }
    
    return this.currentInstances;
  }
  
  private shouldScaleUp(metrics: SystemMetrics): boolean {
    return (
      metrics.cpu > this.config.scaleUpThreshold ||
      metrics.memory > this.config.scaleUpThreshold
    );
  }
  
  private shouldScaleDown(metrics: SystemMetrics): boolean {
    return (
      metrics.cpu < this.config.scaleDownThreshold &&
      metrics.memory < this.config.scaleDownThreshold &&
      this.currentInstances > this.config.minInstances
    );
  }
}

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

Чек-лист готовности к масштабированию

interface ScalabilityChecklist {
  stateless: boolean;           // Сервис без состояния
  externalizedConfig: boolean;  // Конфигурация вынесена
  externalizedSession: boolean; // Сессии в Redis/БД
  healthChecks: boolean;        // Health endpoints
  gracefulShutdown: boolean;    // Корректное завершение
  idempotentAPIs: boolean;      // Идемпотентные операции
  distributedLogging: boolean;  // Централизованные логи
  distributedTracing: boolean;  // Distributed tracing
}

class ScalabilityValidator {
  validate(checklist: ScalabilityChecklist): string[] {
    const issues: string[] = [];
    
    if (!checklist.stateless) {
      issues.push('Сервис содержит состояние - сложно масштабировать');
    }
    
    if (!checklist.externalizedSession) {
      issues.push('Сессии хранятся локально - sticky sessions required');
    }
    
    if (!checklist.healthChecks) {
      issues.push('Нет health checks - балансировщик не сможет определить статус');
    }
    
    if (!checklist.gracefulShutdown) {
      issues.push('Нет graceful shutdown - потеря запросов при рестарте');
    }
    
    return issues;
  }
}

Заключение

Масштабирование — это не только добавление серверов. Это комплексный подход, включающий:

  1. Архитектуру — stateless сервисы, правильное разделение
  2. Инфраструктуру — балансировщики, кэши, очереди
  3. Мониторинг — метрики, алерты, автомасштабирование
  4. Операционные практики — graceful shutdown, health checks

В следующих уроках мы подробно рассмотрим репликацию, кэширование, шардирование и другие техники масштабирования.