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

Как справлялись с high load

1.7 Middle🔥 231 комментариев
#Docker, Kubernetes и DevOps#REST API и микросервисы#Spring Boot и Spring Data#Кэширование и NoSQL#Многопоточность

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

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

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

Работа с высокой нагрузкой (High Load): стратегии и инструменты

High load — это когда приложение должно обработать тысячи или миллионы запросов в секунду, миллиарды данных и нужно сохранить отзывчивость. Это одна из самых сложных и интересных задач в разработке.

Этап 1: Идентификация узких мест (Profiling)

Инструменты:

  • JProfiler — графический профайлер
  • YourKit — мощный профайлер для production
  • Java Flight Recorder (JFR) — встроенный в JVM
  • Prometheus + Grafana — мониторинг метрик
  • New Relic, DataDog — APM решения
// Базовый profiling с System.nanoTime()
long startTime = System.nanoTime();
// код
long endTime = System.nanoTime();
long durationMs = (endTime - startTime) / 1_000_000;
logger.info("Operation took {}ms", durationMs);

// Лучше используй MeterRegistry от Micrometer
@Component
public class PerformanceMonitoring {
    private final MeterRegistry meterRegistry;
    
    public PerformanceMonitoring(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
    
    public void recordOperation(String operationName, long durationMs) {
        meterRegistry.timer("operation.duration", "operation", operationName)
            .record(durationMs, TimeUnit.MILLISECONDS);
    }
}

Стратегия 1: Оптимизация БД — КЛЮЧЕВАЯ

Проблема 1: Медленные запросы

-- ❌ ПЛОХО — N+1 запрос
SELECT * FROM users;  -- 1000 запросов
for each user:
    SELECT * FROM orders WHERE user_id = user.id;  -- 1000 запросов!

-- ✅ ХОРОШО — 1 запрос с JOIN
SELECT u.*, o.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

На Java:

// ❌ Плохо с Hibernate
List<User> users = userRepository.findAll();
for (User user : users) {
    Set<Order> orders = user.getOrders();  // Здесь полтора запроса!
}

// ✅ Хорошо
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();

// ✅ Ещё лучше с Projection
@Query("SELECT new UserWithOrdersDTO(u.id, u.name, o.id, o.amount) FROM User u LEFT JOIN u.orders o")
List<UserWithOrdersDTO> findAllWithOrders();

Проблема 2: Отсутствие индексов

-- ❌ Медленно
SELECT * FROM orders WHERE user_id = 123 AND status = 'PENDING';

-- ✅ Быстро с индексом
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

Проблема 3: Неоптимальный размер результата

// ❌ Получаем 1 млн строк в памяти
List<Order> orders = orderRepository.findAll();

// ✅ Используем pagination
Page<Order> orders = orderRepository.findAll(PageRequest.of(0, 100));

// ✅ Или streaming для больших объёмов
orderRepository.findAllAsStream()
    .forEach(order -> processOrder(order));

Стратегия 2: Кэширование — САМОЕ МОЩНОЕ

Уровень 1: Application Cache (в памяти процесса)

// Spring Cache Abstraction
@Service
public class UserService {
    
    @Cacheable(value = "users", key = "#userId")
    public User getUserById(Long userId) {
        // Результат кэшируется на первый вызов
        return userRepository.findById(userId).orElse(null);
    }
    
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        return userRepository.save(user);
    }
    
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }
}

// Конфигурация кэша (Caffeine)
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager()
            // Конфигурация
    }
}

Уровень 2: Distributed Cache (Redis, Memcached)

@Service
public class RedisUserService {
    private final RedisTemplate<String, User> redisTemplate;
    private final UserRepository userRepository;
    
    private static final String USER_KEY = "user:";
    
    public User getUserById(Long userId) {
        String key = USER_KEY + userId;
        
        // Попытка получить из Redis
        User cachedUser = redisTemplate.opsForValue().get(key);
        if (cachedUser != null) {
            return cachedUser;
        }
        
        // Если нет — получить из БД
        User user = userRepository.findById(userId)
            .orElse(null);
        
        // И сохранить в Redis на 1 час
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 
                Duration.ofHours(1));
        }
        
        return user;
    }
}

Redis с Lua скриптами для атомарности:

// Например, для rate limiting
public class RateLimiter {
    private final RedisScript<Long> rateLimitScript = 
        RedisScript.of(
            "local current = redis.call('INCR', KEYS[1])\n" +
            "if current == 1 then\n" +
            "  redis.call('EXPIRE', KEYS[1], ARGV[1])\n" +
            "end\n" +
            "return current",
            Long.class
        );
    
    public boolean isAllowed(String key, int limit, int windowSeconds) {
        Long current = redisTemplate.execute(
            rateLimitScript,
            Arrays.asList(key),
            windowSeconds
        );
        return current <= limit;
    }
}

Стратегия 3: Асинхронная обработка

Задача 1: Отправить email

// ❌ Синхронно — блокирует поток
@Service
public class UserService {
    public void registerUser(UserRegisterRequest request) {
        User user = createUser(request);
        emailService.sendWelcomeEmail(user);  // Может ждать 1-2 сек!
    }
}

// ✅ Асинхронно
@Service
public class UserService {
    @Autowired
    private EmailService emailService;
    
    public void registerUser(UserRegisterRequest request) {
        User user = createUser(request);
        sendEmailAsync(user);  // Возвращаемся сразу
    }
    
    @Async
    public void sendEmailAsync(User user) {
        emailService.sendWelcomeEmail(user);
    }
}

// Конфигурация
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(500);
        executor.initialize();
        return executor;
    }
}

Задача 2: Обработка очереди сообщений (Message Queue)

// RabbitMQ
@Service
public class OrderEventPublisher {
    private final RabbitTemplate rabbitTemplate;
    
    public void publishOrderCreated(Order order) {
        // Отправить сообщение в очередь — мгновенно!
        rabbitTemplate.convertAndSend(
            "order.exchange",
            "order.created",
            new OrderEvent(order.getId())
        );
    }
}

@Service
public class OrderEventListener {
    @RabbitListener(queues = "order.created.queue")
    public void handleOrderCreated(OrderEvent event) {
        // Обработать асинхронно, когда будет готово
        Order order = orderRepository.findById(event.getOrderId());
        // ... дорогостоящая обработка
    }
}

Стратегия 4: Горизонтальное масштабирование

Проблема: Один сервер не справляется с нагрузкой

Решение: Несколько одинаковых сервисов + Load Balancer

Client Request
    ↓
[Load Balancer] (nginx, HAProxy)
    ↓
┌─────────┬──────────┬──────────┐
│ App 1   │ App 2    │ App 3    │
│ :8080   │ :8081    │ :8082    │
└────┬────┴────┬─────┴────┬─────┘
     └─────────┬──────────┘
               ↓
          [Database]

Конфигурация nginx:

upstream backend {
    server app1:8080 weight=5;
    server app2:8080 weight=3;
    server app3:8080 weight=2;
    keepalive 32;
}

server {
    listen 80;
    
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Стратегия 5: Optimistic Locking вместо Pessimistic

// ❌ Pessimistic (заблокировать строку)
@Transactional
public void updateBalance(Long userId, BigDecimal amount) {
    User user = entityManager.find(
        User.class, userId, LockModeType.PESSIMISTIC_WRITE
    );  // Заблокирована!
    user.setBalance(user.getBalance().add(amount));
}
// Проблема: если много обновлений — очередь блокировок

// ✅ Optimistic (проверить версию)
@Entity
public class User {
    @Id
    private Long id;
    
    @Version  // Автоматическая версия
    private Integer version;
    
    private BigDecimal balance;
}

@Transactional
public void updateBalance(Long userId, BigDecimal amount) {
    User user = userRepository.findById(userId).orElse(null);
    user.setBalance(user.getBalance().add(amount));
    // Если версия не совпала — выбросит OptimisticLockingException
    userRepository.save(user);
}

Стратегия 6: Connection Pooling

// HikariCP (рекомендуемый пул соединений)
@Configuration
public class DataSourceConfig {
    @Bean
    public HikariConfig hikariConfig() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost/mydb");
        config.setUsername("postgres");
        config.setPassword("password");
        config.setMaximumPoolSize(20);      // Макс соединений
        config.setMinimumIdle(5);           // Мин соединений в покое
        config.setIdleTimeout(600000);      // 10 мин
        config.setConnectionTimeout(20000); // 20 сек
        config.setMaxLifetime(1800000);     // 30 мин
        return config;
    }
    
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource(hikariConfig());
    }
}

Стратегия 7: Batch обработка

// ❌ Плохо — 1000 INSERT в цикле
for (Order order : orders) {
    orderRepository.save(order);  // 1000 SQL запросов!
}

// ✅ Хорошо — Batch INSERT
public void saveOrdersBatch(List<Order> orders) {
    try (Statement stmt = connection.createStatement()) {
        for (Order order : orders) {
            stmt.addBatch(
                String.format(
                    "INSERT INTO orders (user_id, amount) VALUES (%d, %f)",
                    order.getUserId(), order.getAmount()
                )
            );
        }
        stmt.executeBatch();  // 1 SQL запрос!
    }
}

// Или с Spring Data JPA
public void saveOrdersBatch(List<Order> orders) {
    final int BATCH_SIZE = 1000;
    for (int i = 0; i < orders.size(); i++) {
        orderRepository.save(orders.get(i));
        if (i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        }
    }
}

Пример реального сценария high load

Сценарий: Приложение для покупок на чёрную пятницу (100,000 одновременных пользователей)

@Service
public class CheckoutService {
    private final RedisTemplate<String, Product> productCache;
    private final OrderEventPublisher eventPublisher;
    private final RateLimiter rateLimiter;
    
    @Transactional
    public Order checkout(Long userId, List<CartItem> items) {
        // 1. Проверка rate limit
        if (!rateLimiter.isAllowed("checkout:" + userId, 10, 60)) {
            throw new TooManyRequestsException();
        }
        
        // 2. Получить products из кэша (не БД!)
        List<Product> products = items.stream()
            .map(item -> productCache.opsForValue()
                .get("product:" + item.getProductId()))
            .collect(Collectors.toList());
        
        // 3. Создать заказ с optimistic locking
        Order order = new Order(userId, items);
        order = orderRepository.save(order);
        
        // 4. Опубликовать событие асинхронно
        eventPublisher.publishOrderCreated(order);
        
        return order;
    }
}

Инструменты мониторинга

# Prometheus для сбора метрик
# Grafana для визуализации
# ELK Stack для логов
# Jaeger для трейсинга

metrics:
  - http.requests.total
  - http.request.duration.seconds
  - db.connection.pool.available
  - cache.hits / cache.misses
  - queue.depth

Практический чеклист для high load

  1. Профайлинг — найти узкие места
  2. БД оптимизация — индексы, JOIN, pagination
  3. Кэширование — Redis, Memcached
  4. Асинхронность — message queues, @Async
  5. Горизонтальное масштабирование — несколько инстансов
  6. Connection pooling — HikariCP
  7. Batch обработка — массовые операции
  8. Rate limiting — защита от перегруза
  9. Мониторинг — Prometheus, Grafana
  10. Load testing — JMeter, Gatling

Заключение

Работа с high load требует:

  • Глубокого понимания архитектуры системы
  • Постоянного мониторинга и анализа метрик
  • Итеративного улучшения узких мест
  • Knowledge лучших практик кэширования, асинхронности, масштабирования

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

Как справлялись с high load | PrepBro