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

Какие знаешь проблемы горизонтального масштабирования?

2.4 Senior🔥 121 комментариев
#REST API и микросервисы#Базы данных и SQL

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

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

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

Проблемы горизонтального масштабирования

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

1. Управление сессиями и состоянием

Проблема: каждый сервер имеет свою локальную память. Когда пользователь обращается к разным серверам, его сессия теряется.

// Плохо — сессия хранится локально
HttpSession session = request.getSession();
session.setAttribute("user", user);  // Потеряется при обращении к другому серверу

// Хорошо — сессия в распределенном хранилище
@Configuration
@EnableSpringSession
public class SessionConfig {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}
// Сессии хранятся в Redis, доступны всем серверам

Решения:

  • Redis/Memcached для хранения сессий
  • Sticky Sessions (привязка сессии к серверу, но теряет отказоустойчивость)
  • JWT токены (stateless, рекомендуется)

2. Синхронизация данных между серверами

Проблема: если один сервер кэширует данные, другие серверы могут работать с устаревшей информацией.

// Проблема: каждый сервер имеет свой кэш
@Cacheable("users")
public User getUser(Long id) {
    return userRepository.findById(id).orElse(null);
}

// Если один сервер обновит пользователя, кэш других серверов не обновится

// Решение: распределенный кэш
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.create(factory);
    }
}

// С инвалидацией кэша при изменениях
@CacheEvict(value = "users", key = "#id")
public void updateUser(Long id, UserUpdateRequest request) {
    userService.update(id, request);
}

Решения:

  • Redis / Memcached для распределенного кэша
  • Message Broker (RabbitMQ, Kafka) для оповещения о изменениях
  • Event-driven архитектура с pub/sub

3. Распределенные транзакции

Проблема: операции могут выполняться на разных серверах/БД, гарантировать ACID сложно.

// Проблема: операция может быть выполнена частично
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    // Запрос идет на сервер 1
    Account from = accountRepository.findById(fromId).orElseThrow();
    from.setBalance(from.getBalance().subtract(amount));
    accountRepository.save(from);
    
    // Сервер 1 падает перед выполнением следующей операции
    
    // Запрос идет на сервер 2
    Account to = accountRepository.findById(toId).orElseThrow();
    to.setBalance(to.getBalance().add(amount));
    accountRepository.save(to);
}

// Решение: Saga Pattern (событийная компенсация)
@Service
public class MoneyTransferSaga {
    @Transactional
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        try {
            // Фаза 1: Дебет со счета
            accountService.debit(fromId, amount);
            // Публикуем событие для фазы 2
            eventPublisher.publish(new MoneyDebitedEvent(fromId, toId, amount));
        } catch (Exception e) {
            // Компенсирующая транзакция
            accountService.credit(fromId, amount);
            throw e;
        }
    }
    
    @EventListener
    public void onMoneyDebited(MoneyDebitedEvent event) {
        try {
            accountService.credit(event.getToId(), event.getAmount());
            eventPublisher.publish(new MoneyTransferCompletedEvent(...));
        } catch (Exception e) {
            // Откат первой операции
            accountService.credit(event.getFromId(), event.getAmount());
        }
    }
}

Решения:

  • Saga Pattern (орхестрация или хореография событий)
  • Two-phase commit (но медленно и не масштабируется)
  • BASE модель вместо ACID (Basically Available, Soft state, Eventual consistency)

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

Проблема: неравномерное распределение трафика между серверами, отказ балансировщика приводит к отказу всей системы.

// Конфигурация балансировщика нагрузки (Nginx)
upstream backend {
    server server1.example.com weight=5;  // 50% трафика
    server server2.example.com weight=5;  // 50% трафика
    
    # Проверка здоровья
    check interval=3000 rise=2 fall=5 timeout=1000 type=http;
    check_http_send "GET /health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

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

Решения:

  • Nginx / HAProxy для балансировки
  • Round-robin, Least connections, IP hash алгоритмы
  • Health checks для исключения неработающих серверов
  • Circuit breaker паттерн

5. Согласованность данных (Data Consistency)

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

// Проблема: Race condition
@Service
public class UserService {
    public void incrementVisitCount(Long userId) {
        // Сервер 1 читает: visits = 100
        // Сервер 2 читает: visits = 100
        // Сервер 1 пишет: visits = 101
        // Сервер 2 пишет: visits = 101  <- НЕПРАВИЛЬНО, должно быть 102
        
        User user = userRepository.findById(userId).orElseThrow();
        user.setVisitCount(user.getVisitCount() + 1);
        userRepository.save(user);
    }
}

// Решение 1: Database-level increment
public void incrementVisitCount(Long userId) {
    userRepository.incrementVisitCount(userId);
    // SQL: UPDATE users SET visit_count = visit_count + 1 WHERE id = ?
}

// Решение 2: Distributed lock
@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public void incrementVisitCount(Long userId) {
        String lockKey = "user:" + userId + ":lock";
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "locked", Duration.ofSeconds(5));
        
        if (acquired) {
            try {
                User user = userRepository.findById(userId).orElseThrow();
                user.setVisitCount(user.getVisitCount() + 1);
                userRepository.save(user);
            } finally {
                redisTemplate.delete(lockKey);
            }
        }
    }
}

Решения:

  • Optimistic Locking (версионирование)
  • Distributed Locks (Redis, Zookeeper)
  • Event sourcing (запись всех изменений)
  • Accept eventual consistency (Eventual consistency)

6. Отказоустойчивость и восстановление

Проблема: отказ одного сервера влияет на всю систему, восстановление должно быть автоматическим.

// Решение: Health check endpoint
@RestController
public class HealthController {
    @GetMapping("/health")
    public ResponseEntity<HealthStatus> getHealth() {
        HealthStatus status = new HealthStatus();
        status.setStatus(checkDatabaseConnection() ? "UP" : "DOWN");
        status.setTimestamp(System.currentTimeMillis());
        return ResponseEntity.ok(status);
    }
}

// Решение: Circuit Breaker
@Service
public class ExternalServiceClient {
    @CircuitBreaker(name = "externalApi", fallbackMethod = "fallback")
    public Data callExternalApi() {
        return restTemplate.getForObject("https://api.example.com/data", Data.class);
    }
    
    public Data fallback(Exception e) {
        // Возвращаем кэшированные данные при отказе
        return cacheService.getLastData();
    }
}

Решения:

  • Health checks и автоматическое исключение неработающих серверов
  • Graceful shutdown для корректного завершения
  • Circuit breaker паттерн для защиты от каскадных отказов
  • Retry logic с exponential backoff
  • Containers/Kubernetes для автоматического восстановления

7. Логирование и мониторинг

Проблема: сложно отследить запрос через несколько серверов при возникновении проблемы.

// Решение: Distributed tracing
@Service
public class UserService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    
    @Autowired
    private Tracer tracer;
    
    public User getUser(Long id) {
        Span span = tracer.startSpan("getUser");
        try {
            // Trace ID используется для отслеживания запроса
            // во всех сервисах
            span.setTag("userId", id);
            log.info("Fetching user {}", id);
            return userRepository.findById(id).orElse(null);
        } finally {
            span.finish();
        }
    }
}

Решения:

  • Distributed tracing (Jaeger, Zipkin)
  • Centralized logging (ELK stack, Splunk)
  • Metrics collection (Prometheus, Grafana)
  • APM tools (New Relic, Datadog)

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

Какие знаешь проблемы горизонтального масштабирования? | PrepBro