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;
}
}
Заключение
Масштабирование — это не только добавление серверов. Это комплексный подход, включающий:
- Архитектуру — stateless сервисы, правильное разделение
- Инфраструктуру — балансировщики, кэши, очереди
- Мониторинг — метрики, алерты, автомасштабирование
- Операционные практики — graceful shutdown, health checks
В следующих уроках мы подробно рассмотрим репликацию, кэширование, шардирование и другие техники масштабирования.