Проектирование и реализация регулирования запросов

Проектирование и реализация регулирования запросов

2 ноября 2024 г.

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

Это вторая часть из трех частей.Прочитайте часть 1 здесь.

Регулирование: ваша первая линия защиты

Реализация механизмов регулирования помогает предотвратить чрезмерное использование системных ресурсов и обеспечивает справедливое распределение доступа. Давайте рассмотрим различные стратегии регулирования от уровня инфраструктуры до уровня приложений.

1.Регулирование на уровне сети с помощью балансировщиков нагрузки

Балансировщики нагрузки выступают в качестве первой линии обороны, регулируя поток трафика через несколько внутренних серверов. Они выявляют аномальные шаблоны трафика, такие как всплеск запросов с определенного IP-адреса, и ограничивают или блокируют доступ до того, как будет затронут внутренний сервер. Интегрированные межсетевые экраны дополнительно защищают систему от атак типа «распределенный отказ в обслуживании» (DDoS) и предотвращают истощение ресурсов в периоды пиковой нагрузки.

2.Регулирование API-шлюза

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

3.Регулирование на уровне приложений

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

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

Теоретическое понимание — это только часть решения, успешная реализация требует тщательного планирования и опыта кодирования. Давайте рассмотрим два ключевых метода:Ограничитель скорости TPS (т.е. транзакций в секунду), управление параллельными запросами, основанный на ресурсахиРегулирование на основе пользователя/IP.

1. Ограничитель скорости TPS

TheВедро токеновАлгоритм — популярный метод ограничения скорости. Он позволяет обрабатывать фиксированное количество токенов (запросов) в течение определенного периода. Когда контейнер токенов пуст, дальнейшие запросы задерживаются или отклоняются. Такой подход гарантирует, что системы будут плавно обрабатывать всплески трафика, не перегружая их.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class RateLimiter {
    private final int maxTokens;         // Maximum tokens allowed in the bucket
    private final int refillRate;        // Tokens added per second
    private AtomicInteger availableTokens;  // Current available tokens
    private long lastRefillTimestamp;    // Last time the bucket was refilled

    // Constructor to initialize the rate limiter
    public RateLimiter(int maxTokens, int refillRate) {
        this.maxTokens = maxTokens;
        this.refillRate = refillRate;
        this.availableTokens = new AtomicInteger(maxTokens);
        this.lastRefillTimestamp = System.nanoTime();
    }

    // Method to try consuming a token. Returns true if successful, false if rejected.
    public synchronized boolean tryAcquire() {
        refillTokens();  // Refill the tokens before processing the request
        if (availableTokens.get() > 0) {
            availableTokens.decrementAndGet();  // Consume one token
            return true;  // Request allowed
        } else {
            return false; // Request rejected
        }
    }

    // Method to refill tokens based on elapsed time
    private void refillTokens() {
        long now = System.nanoTime();
        long elapsedTime = now - lastRefillTimestamp;

        // Calculate the number of tokens to refill
        int tokensToAdd = (int) (TimeUnit.NANOSECONDS.toSeconds(elapsedTime) * refillRate);
        if (tokensToAdd > 0) {
            // Add tokens up to the max limit
            int newTokens = Math.min(maxTokens, availableTokens.get() + tokensToAdd);
            availableTokens.set(newTokens);
            lastRefillTimestamp = now;  // Update the last refill timestamp
        }
    }

    public static void main(String[] args) throws InterruptedException {
        RateLimiter rateLimiter = new RateLimiter(5, 1);  // Max 5 tokens, 1 token/second

        // Simulate 10 requests
        for (int i = 1; i <= 10; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request " + i + " processed.");
            } else {
                System.out.println("Request " + i + " rejected. Too many requests.");
            }
            Thread.sleep(500);  // Simulate 0.5 second between requests
        }
    }
}

2.Управление параллельными запросами

Когда одновременно поступает несколько запросов,семафорыпомогают поддерживать порядок, контролируя количество параллельных процессов. Если емкость семафора исчерпана, новые запросы либо ставятся в очередь, либо отклоняются. Использование семафоров гарантирует, что система будет поддерживать стабильную производительность даже при столкновении с большими объемами одновременных запросов.

import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConcurrentRequestLimiter {
    private final Semaphore semaphore;  // Semaphore to control concurrent access

    // Constructor to initialize with max concurrent requests
    public ConcurrentRequestLimiter(int maxConcurrentRequests) {
        this.semaphore = new Semaphore(maxConcurrentRequests);
    }

    // Method to acquire permission to process a request
    public boolean tryAcquire() {
        return semaphore.tryAcquire();  // Returns true if permit is available, otherwise false
    }

    // Method to release a permit after processing
    public void release() {
        semaphore.release();
    }

    // Simulate handling a request
    public void handleRequest(int requestId) {
        if (tryAcquire()) {
            System.out.println("Processing Request " + requestId);
            try {
                // Simulate processing time for the request
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                release();  // Release the permit after processing
                System.out.println("Finished Request " + requestId);
            }
        } else {
            System.out.println("Rejected Request " + requestId + ". Too many concurrent requests.");
        }
    }

    public static void main(String[] args) {
        int maxConcurrentRequests = 3;  // Allow up to 3 concurrent requests
        ConcurrentRequestLimiter limiter = new ConcurrentRequestLimiter(maxConcurrentRequests);

        // Use a thread pool to simulate multiple clients sending requests
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // Simulate 10 incoming requests
        for (int i = 1; i <= 10; i++) {
            final int requestId = i;
            executor.submit(() -> limiter.handleRequest(requestId));
        }

        executor.shutdown();
    }
}

3.Регулирование на основе ресурсов

Регулирование на основе ресурсов вступает в игру, когда системные ресурсы, такие как ЦП или память, достигают критических уровней. Чтобы предотвратить атаки DDoS, которые могут перегрузить системы, мы реализуем регулирование на основе ресурсов. Устанавливая предопределенные пороговые значения, такие как максимальная загрузка ЦП в 80%, механизмы регулирования гарантируют, что дополнительный трафик будет задержан или отклонен до ухудшения производительности.

Example in Practice:

Scenario: An API must maintain < 200ms latency.
Method:
Use a load testing tool to gradually increase traffic.
Monitor CPU, memory, and response times.
Identify that:
At 400 TPS: CPU usage is 70%, and latency is 150ms.
At 500 TPS: CPU usage jumps to 85%, latency spikes to 300ms, and 5% of requests fail.
Thresholds:
CPU Threshold: 75-80% (to avoid bottlenecks).
Max Safe TPS: 450 TPS (with existing infrastructure).

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

4.Регулирование на основе пользователя или IP в распределенных системах

РеализацияРегулирование на основе пользователя или IP-адресавраспределенная систематребует скоординированного подхода для обеспечения управления запросами на нескольких серверах или экземплярах. Вот пример реализации регулирования на основе пользователя в распределенной системе с использованиемРедисдля подхода с использованием токен-ведра:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class DistributedRateLimiter {
    private final JedisPool jedisPool;
    private final int maxTokens;
    private final int refillRate;  // Tokens added per second

    public DistributedRateLimiter(JedisPool jedisPool, int maxTokens, int refillRate) {
        this.jedisPool = jedisPool;
        this.maxTokens = maxTokens;
        this.refillRate = refillRate;
    }

    public boolean isAllowed(String userId) {
        try (Jedis jedis = jedisPool.getResource()) {
            long currentTime = System.currentTimeMillis();
            String tokenKey = "ratelimit:" + userId;
            String lastRefillKey = "lastRefill:" + userId;

            // Get current tokens
            String currentTokens = jedis.get(tokenKey);
            Long tokens = currentTokens != null ? Long.parseLong(currentTokens) : (long) maxTokens;

            // Get last refill time
            String lastRefillTime = jedis.get(lastRefillKey);
            long lastRefill = lastRefillTime != null ? Long.parseLong(lastRefillTime) : currentTime;

            // Calculate new tokens based on time passed since last refill
            long timePassed = currentTime - lastRefill;
            long newTokens = Math.min(maxTokens, tokens + (timePassed / 1000) * refillRate);

            // Update token and refill time in Redis
            jedis.set(tokenKey, String.valueOf(newTokens));
            jedis.set(lastRefillKey, String.valueOf(currentTime));

            // Check if a token can be consumed
            if (newTokens > 0) {
                jedis.decr(tokenKey);  // Consume a token
                return true;  // Request allowed
            }

            return false;  // Request throttled
        }
    }
}

Заключение

Эффективное управление запросами заключается не только в ограничении доступа — оно заключается в поддержании тонкого баланса между удобством использования и защитой. Будь то ограничение скорости, параллельный контроль, регулирование на основе пользователя/IP или ресурсов, каждая стратегия играет жизненно важную роль в стабильности системы. Организации должны постоянно контролировать, совершенствовать и адаптировать свои политики регулирования для соответствия изменяющимся требованиям и моделям трафика. При наличии этих стратегий системы остаются устойчивыми, отзывчивыми и готовыми к масштабированию.

Оставайтесь с нами дляЧасть 3: Перегазовка и недогазовка — достижение баланса


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE