← Назад к вопросам

Какие свойства нужны приложению для масштабирования

2.7 Senior🔥 131 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Свойства приложения для масштабирования (Scalability)

Масштабируемость — это способность приложения обрабатывать растущий объём данных и нарастающую нагрузку без деградации производительности. Существуют ключевые архитектурные и технические свойства, которые необходимы для этого.

1. Statelessness (Отсутствие состояния)

Важность: Stateless приложение можно легко масштабировать горизонтально (добавлять серверы).

// Плохо - STATEFUL (привязан к сессии)
@RestController
public class ShoppingCartController {
    @GetMapping("/cart")
    public Cart getCart(HttpSession session) {
        // Состояние хранится в session - сложно масштабировать
        return (Cart) session.getAttribute("cart");
    }
}

// Хорошо - STATELESS
@RestController
public class ShoppingCartController {
    @GetMapping("/users/{id}/cart")
    public Cart getCart(@PathVariable Long userId) {
        // Состояние загружается из БД, каждый запрос независим
        return cartService.getCart(userId);
    }
}

// Результат:
// Stateless → можно обрабатывать запрос на любом сервере
// Server 1: request 1
// Server 2: request 2
// Server 3: request 3
// Все работают параллельно

Конфигурация для statelessness:

# application.yml
server:
  servlet:
    session:
      persistent: false
      timeout: 30m

spring:
  session:
    store-type: none # Не хранить сессию

2. Горизонтальное масштабирование (Horizontal Scaling)

Характеристики приложения:

  • Load Balancer распределяет запросы между несколькими инстансами
  • Не привязано к локальному файловой системе
  • Не хранит состояние в памяти
Request → Load Balancer (nginx, HAProxy)
          ├→ Instance 1 (port 8080)
          ├→ Instance 2 (port 8080)
          ├→ Instance 3 (port 8080)
          └→ Instance 4 (port 8080)

Любой инстанс может обработать запрос

Пример конфигурации:

# nginx.conf
upstream backend {
    server app1:8080;
    server app2:8080;
    server app3:8080;
    server app4:8080;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Kubernetes:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3 # Автоматически масштабирует на 3 инстанса
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "256Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"

3. Кэширование (Caching Strategy)

Многоуровневое кэширование:

// 1. L1 Cache - In-Memory (Caffeine)
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager("products", "users");
    }
}

@Service
public class ProductService {
    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id);
    }
}

// 2. L2 Cache - Redis (Distributed)
@Configuration
public class RedisConfig {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}

@Service
public class OrderService {
    @Cacheable(value = "orders", key = "#userId", cacheManager = "redisManager")
    public List<Order> getUserOrders(Long userId) {
        return orderRepository.findByUserId(userId);
    }
}

// 3. Cache Invalidation
@CacheEvict(value = "orders", key = "#userId")
public void updateOrder(Long userId, Order order) {
    orderRepository.save(order);
}

Cache Warming:

@Component
public class CacheWarmer {
    @EventListener(ContextRefreshedEvent.class)
    public void warmCache() {
        // Предварительно загрузить часто используемые данные
        List<Category> categories = categoryRepository.findAll();
        categories.forEach(c -> cacheService.cache(c));
    }
}

4. База данных масштабирования

Database replication и sharding:

Master (Write) → Replication → Slave 1 (Read)
                            → Slave 2 (Read)
                            → Slave 3 (Read)
// Master-Slave конфигурация
@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://master.db:5432/myapp")
            .build();
    }
    
    @Bean
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://slave.db:5432/myapp")
            .build();
    }
}

// Использование
@Service
public class UserService {
    @Transactional(readOnly = true)
    public User getUser(Long id) {
        // Используется slave для чтения
        return userRepository.findById(id);
    }
    
    @Transactional(readOnly = false)
    public void updateUser(User user) {
        // Использует master для записи
        userRepository.save(user);
    }
}

Database Sharding (горизонтальное разделение):

// Sharding по user_id
@Service
public class UserOrderService {
    private List<DataSource> shards; // Несколько БД
    
    public Order getOrder(Long userId, Long orderId) {
        int shardIndex = (int) (userId % shards.size());
        DataSource shard = shards.get(shardIndex);
        // Запросить из конкретной БД
        return getOrderFromShard(shard, orderId);
    }
}

// Пример распределения:
// userId 1, 4, 7, 10... → Shard 1
// userId 2, 5, 8, 11... → Shard 2
// userId 3, 6, 9, 12... → Shard 3

5. Асинхронная обработка (Async Processing)

Отделение long-running operations:

// Плохо - синхронный процесс блокирует
@PostMapping("/orders")
public ResponseEntity<OrderDTO> createOrder(@RequestBody CreateOrderRequest request) {
    Order order = orderService.createOrder(request);
    sendEmailNotification(order); // Ждём отправки email (2 секунды)
    generateInvoice(order);        // Ждём генерации инвойса (3 секунды)
    return ResponseEntity.ok(OrderMapper.toDTO(order));
} // Итого 5 секунд для пользователя

// Хорошо - асинхронная обработка
@PostMapping("/orders")
public ResponseEntity<OrderDTO> createOrder(@RequestBody CreateOrderRequest request) {
    Order order = orderService.createOrder(request);
    
    // Отправить в очередь - пользователь получит ответ сразу
    eventPublisher.publish(new OrderCreatedEvent(order.getId()));
    
    return ResponseEntity.ok(OrderMapper.toDTO(order));
} // Итого < 100ms

// Обработчики
@Component
public class OrderEventHandler {
    @EventListener
    @Async
    public void handleOrderCreated(OrderCreatedEvent event) {
        Order order = orderService.getOrder(event.getOrderId());
        emailService.send(order); // 2 секунды
    }
    
    @EventListener
    @Async
    public void generateInvoice(OrderCreatedEvent event) {
        Order order = orderService.getOrder(event.getOrderId());
        invoiceService.generate(order); // 3 секунды
    }
}

Message Queue для масштабирования:

// RabbitMQ / Kafka
@Configuration
public class MessagingConfig {
    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable("orders").build();
    }
}

@Service
public class OrderPublisher {
    public void publishOrderCreated(Order order) {
        rabbitTemplate.convertAndSend("orders", 
            new OrderCreatedMessage(order));
    }
}

@Component
public class OrderConsumer {
    @RabbitListener(queues = "orders")
    public void handleOrderCreated(OrderCreatedMessage message) {
        // Несколько потребителей могут обрабатывать очередь
        processOrder(message.getOrderId());
    }
}

6. Connection Pooling

Оптимальная конфигурация HikariCP:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20        # Количество соединений в пуле
      minimum-idle: 5              # Минимум idle соединений
      connection-timeout: 20000    # Timeout получить соединение (20 сек)
      idle-timeout: 300000         # Timeout закрыть idle соединение (5 мин)
      max-lifetime: 1200000        # Max lifetime соединения (20 мин)
      auto-commit: true
      leak-detection-threshold: 15000 # Detect leaked connections

7. API Rate Limiting

Защита от перегрузки:

@Configuration
public class RateLimitingConfig {
    @Bean
    public RateLimiter rateLimiter() {
        return RateLimiter.create(100); // 100 requests/sec
    }
}

@Component
public class RateLimitingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                       FilterChain chain) throws IOException, ServletException {
        if (!rateLimiter.tryAcquire()) {
            ((HttpServletResponse) response).setStatus(429); // Too Many Requests
            return;
        }
        chain.doFilter(request, response);
    }
}

8. Мониторинг и Автоскейлинг

Kubernetes HPA (Horizontal Pod Autoscaler):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70 # Масштабируется если CPU > 70%
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80 # Масштабируется если memory > 80%

9. Обработка больших объёмов данных

Stream обработка вместо batch:

// Плохо - загружаем 1 млн записей в памяь
List<User> users = userRepository.findAll(); // OutOfMemoryError
users.forEach(user -> processUser(user));

// Хорошо - потоковая обработка
final int BATCH_SIZE = 1000;
userRepository.findAll().forEach(users -> {
    users.forEach(user -> processUser(user));
}, BATCH_SIZE);

// Или использовать Stream
try (Stream<User> stream = userRepository.streamAll()) {
    stream.forEach(user -> processUser(user));
}

Заключение

Масштабируемое приложение должно быть stateless, использовать горизонтальное масштабирование, иметь многоуровневое кэширование, асинхронную обработку, оптимальный database scaling и мониторинг для автоматического масштабирования. Все эти свойства должны быть встроены в архитектуру с самого начала, а не добавлены потом.

Какие свойства нужны приложению для масштабирования | PrepBro