Глубокое понимание распределенных технологий – ограничение тока
SpringBoot — Элегантная настройка【Flow Control】
MQ-19 Security_Design схемы ограничения тока
Ежедневный блог — беседа о том, как бороться с «всплеском трафика»
Текущий алгоритм ограничения — это технология, широко используемая в распределенных системах для управления скоростью доступа к системным ресурсам с целью защиты системы от злонамеренных атак или перегрузки, вызванной пакетным трафиком.
В реальных бизнес-сценариях широко используются политики ограничения тока интерфейса. Ниже приведены некоторые типичные сценарии.
простой счетчик является самым основным ограничением В токеалгоритме его принцип калибровки относительно интуитивен.
простой счетчикработапринципследующее:
package com.artisan.counter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CounterRateLimit {
/**
* Количество запросов
*
* @return
*/
int maxRequest();
/**
* временное окно, Единица секунды
*
* @return
*/
int timeWindow();
}
package com.artisan.counter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Aspect
@Component
public class CounterRateLimitAspect {
// Храните количество запросов, соответствующих каждому методу.
private Map<String, AtomicInteger> REQUEST_COUNT = new ConcurrentHashMap<>();
// Сохраните временную метку каждого метода
private Map<String, Long> REQUEST_TIMESTAMP = new ConcurrentHashMap<>();
/**
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("@annotation(com.artisan.counter.CounterRateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
// Получить информацию об аннотации
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
CounterRateLimit annotation = method.getAnnotation(CounterRateLimit.class);
// Получить аннотированные параметры
int maxRequest = annotation.maxRequest();
long timeWindowInMillis = TimeUnit.SECONDS.toMillis(annotation.timeWindow());
// Получить имя метода
String methodName = method.toString();
// Инициализируйте счетчики и временные метки
AtomicInteger count = REQUEST_COUNT.computeIfAbsent(methodName, x -> new AtomicInteger(0));
long startTime = REQUEST_TIMESTAMP.computeIfAbsent(methodName, x -> System.currentTimeMillis());
// Получить текущее время
long currentTimeMillis = System.currentTimeMillis();
// Решение: еслитекущее времявневременное окно, затем сброс
if (currentTimeMillis - startTime > timeWindowInMillis) {
count.set(0);
REQUEST_TIMESTAMP.put(methodName, currentTimeMillis);
}
// Атомарно увеличить счетчик и проверить его значение
if (count.incrementAndGet() > maxRequest) {
// Если максимальное количество запросов превышено, уменьшите счетчик и сообщите об ошибке.
count.decrementAndGet();
throw new RuntimeException("Too many requests, please try again later.");
}
// оригинальное выполнение метода
return joinPoint.proceed();
}
}
package com.artisan.counter;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@RestController
public class CounterRateLimitController {
/**
* раз в секунду
*
* @return
*/
@GetMapping("/counter")
@CounterRateLimit(maxRequest = 2, timeWindow = 2)
public ResponseEntity counter() {
System.out.println("Request Coming Coming....");
return ResponseEntity.ok("Artisan OK");
}
}
Запустите проект, Интерфейс доступа http://localhost:8080/counte
Преимущество простого счетчикалгоритма в том, что он прост, но и недостатки очевидны:
В этом примере есть метод счетчика, помеченный @CounterRateLimit. Согласно параметрам аннотации, этот метод можно вызвать только дважды в течение 2-секундного временного окна. Если в течение 2 секунд поступят дополнительные вызовы, эти дополнительные вызовы будут ограничены и будет возвращено сообщение об ошибке.
Предположим, что период времени составляет 1 минуту, при этом за каждый период времени может быть отправлено не более 100 запросов. Существует крайняя ситуация: когда 100 запросов собираются в 10:00:58, порог достигается, когда 100 запросов собираются в 10:01:02, порог достигается. В такой ситуации 200 запросов будут обработаны всего за 4 секунды, тогда как в остальное время ток будет ограничен.
алгоритм скользящего окнадавыполнить Ограничение тока Распространенный метод,Он контролирует количество запросов в единицу времени, поддерживая временное окно.,Это защищает систему от неожиданных или злонамеренных атак. Его основной принцип — количество запросов в пределах статистического временного окна.,И решите, разрешать ли прохождение новых запросов на основе заданного порога.
Из рисунка видно, что времятворение продвигается скользящим образом. Стратегия ограничения тока скользящего окна позволяет существенно снизить влияние критических проблем, но не может полностью устранить его. Скользящие окна работают путем отслеживания и ограничения запросов в течение непрерывного временного окна. В отличие от простого подхода со счетчиком, вместо внезапного сброса счетчика в конце окна он постепенно удаляет старые запросы из окна и со временем добавляет новые запросы.
Например: предположим, что временное окно составляет 10 с, лимит запросов равен 3, первый запрос инициируется в 10:00:00, второй инициируется в 10:00:05, а третий инициируется в 10:00: 11, затем счетчик. Время начала следующего окна стратегии — 10:00:11, а скользящего окна — 10:00:05. Вот почему скользящее окно может уменьшить влияние проблем с критичностью, но не может полностью устранить их.
package com.artisan.slidingwindow;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SlidingWindowRateLimit {
/**
* Количество запросов
*
* @return
*/
int maxRequest();
/**
* временное окно, Единица секунды
*
* @return
*/
int timeWindow();
}
package com.artisan.slidingwindow;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Aspect
@Component
public class SlidingWindowRateLimitAspect {
/**
* использовать ConcurrentHashMap Очередь, содержащая временные метки запроса для каждого метода.
*/
private final ConcurrentHashMap<String, ConcurrentLinkedQueue<Long>> REQUEST_TIMES_MAP = new ConcurrentHashMap<>();
@Around("@annotation(com.artisan.slidingwindow.SlidingWindowRateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SlidingWindowRateLimit rateLimit = method.getAnnotation(SlidingWindowRateLimit.class);
// Максимально разрешенное количество запросов
int requests = rateLimit.maxRequest();
// Размер скользящего окна (секунды)
int timeWindow = rateLimit.timeWindow();
// Получить имя метод указанная строка
String methodName = method.toString();
// Если очередь временных меток запроса для текущего метода не существует, инициализируйте новую очередь.
ConcurrentLinkedQueue<Long> requestTimes = REQUEST_TIMES_MAP.computeIfAbsent(methodName,
k -> new ConcurrentLinkedQueue<>());
// текущее время
long currentTime = System.currentTimeMillis();
// Вычислить начальную временную метку временного окна
long thresholdTime = currentTime - TimeUnit.SECONDS.toMillis(timeWindow);
// Этот фрагмент кода представляет собой скользящее окно. Ключевая часть алгоритма тока, его функция — удалить временную метку запроса перед текущим скользящим окном. Это сделано для того, чтобы в окне сохранялись только записи запросов за самый последний период времени.
// requestTimes.isEmpty() Условие проверки того, пуста ли очередь. Если очередь пуста, это означает, что записей запросов нет и операция удаления не требуется.
// requestTimes.peek() < thresholdTime заключается в проверке того, находится ли временная метка в начале очереди раньше, чем время начала скользящего окна. Если это так, это означает, что эта временная метка больше не находится в текущем временном окне и ее следует удалить.
while (!requestTimes.isEmpty() && requestTimes.peek() < thresholdTime) {
// Удалить метку времени истечения срока действия из головы очереди.
requestTimes.poll();
}
// 检查текущее время窗口内的请求次数да否超过限制
if (requestTimes.size() < requests) {
// Лимит не превышен, и текущее время запроса фиксируется.
requestTimes.add(currentTime);
return joinPoint.proceed();
} else {
// Лимит превышен, бросьте Ограничение ток ненормальный
throw new RuntimeException("Too many requests, please try again later.");
}
}
}
package com.artisan.slidingwindow;
import com.artisan.leakybucket.LeakyBucketRateLimit;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@RestController
public class SlidingWindowController {
@GetMapping("/slidingWindow")
@SlidingWindowRateLimit(maxRequest = 2, timeWindow = 2)
public ResponseEntity slidingWindow() {
return ResponseEntity.ok("artisan slidingWindow ");
}
}
алгоритм скользящего Преимущество окна в том, что оно может контролировать поток относительно плавно, допуская определенную степень внезапности потока и одновременно ограничивая средний поток. По сравнению с алгоритмом с фиксированным окном, алгоритм сдвижного окна позволяет более точно контролировать количество запросов в единицу времени,Потому что учитывается распределение запросов внутри временного окна,Не только громкость запроса в начале и конце окна.
Существует множество вариантов алгоритма выдвижного окна.,Например, алгоритм на основе ведра токенов и дырявого ведра.,Эти алгоритмы добавляют более сложный механизм генерации и потребления токенов, основанный на скользящем окне.,Используйте для более точного управления потоком.
В Дырявом Bucketалгоритмсередина,Контейнеры имеют фиксированную вместимость.,Похоже на утечкуемкость ковша。Данные поступают в контейнер с фиксированной скоростью,если контейнер полон,Лишние данные переполнятся. Данные в контейнере вытекают снизу с постоянной скоростью.,Подобно каплям воды в дырявом ведре. Если в контейнере недостаточно данных для удовлетворения скорости оттока,Он будет ждать, пока не накопится достаточно данных для вывода. Это обеспечивает плавное управление потоком данных.
package com.artisan.leakybucket;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LeakyBucketRateLimit {
/**
* емкость ковша
*
* @return
*/
int capacity();
/**
* Скорость воронки, обычно в секундах
*
* @return
*/
int leakRate();
}
package com.artisan.leakybucket;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Aspect
@Component
public class LeakyBucketRateLimitAspect {
@Around("@annotation(com.artisan.leakybucket.LeakyBucketRateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LeakyBucketRateLimit leakyBucketRateLimit = method.getAnnotation(LeakyBucketRateLimit.class);
int capacity = leakyBucketRateLimit.capacity();
int leakRate = leakyBucketRateLimit.leakRate();
// Подпись метода как уникальный идентификатор
String methodKey = method.toString();
LeakyBucketLimiter limiter = LeakyBucketLimiter.createLimiter(methodKey, capacity, leakRate);
if (!limiter.tryAcquire()) {
// Лимит превышен, бросьте Ограничение ток ненормальный
throw new RuntimeException("Too many requests, please try again later.");
}
return joinPoint.proceed();
}
}
package com.artisan.leakybucket;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
public class LeakyBucketLimiter {
/**
* емкость ковша
*/
private final int capacity;
/**
* Скорость утечки дырявого ведра, количество воды, вытекающей за единицу времени.
*/
private final int leakRate;
/**
* Текущее количество воды в ведре
*/
private volatile int water = 0;
/**
* Время последней утечки
*/
private volatile long lastLeakTime = System.currentTimeMillis();
/**
* дырявый контейнер-ведро
*/
private static final ConcurrentHashMap<String, LeakyBucketLimiter> LIMITER_MAP = new ConcurrentHashMap<>();
/**
* статический фабричный метод,Убедитесь таким же образомиспользовать Тот же экземпляр дырявого ведра
*
* @param methodKey имя метода
* @param capacity
* @param leakRate
* @return
*/
public static LeakyBucketLimiter createLimiter(String methodKey, int capacity, int leakRate) {
return LIMITER_MAP.computeIfAbsent(methodKey, k -> new LeakyBucketLimiter(capacity, leakRate));
}
private LeakyBucketLimiter(int capacity, int leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
}
/**
* Попробуйте получить разрешение (попробуйте to acquire a разрешено), если приобретение прошло успешно, оно возвращает true, в противном случае — false.
*
* @return
*/
public boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
synchronized (this) {
// Рассчитайте последнюю утечку воды по течению время интервал времени
long leakDuration = currentTime - lastLeakTime;
// Если временной интервал больше или равен 1 секунде, это означает, что из дырявого ведра вытекло определенное количество воды.
if (leakDuration >= TimeUnit.SECONDS.toMillis(1)) {
// Рассчитайте количество вытекшей воды.
long leakQuantity = leakDuration / TimeUnit.SECONDS.toMillis(1) * leakRate;
// После того, как из дырявого ведра протекает вода, количество воды в ведре обновляется, но оно не может быть меньше 0.
water = (int) Math.max(0, water - leakQuantity);
lastLeakTime = currentTime;
}
// Определите, меньше ли количество воды в ведре, чем емкость. Если да, то можно продолжать доливать воду (эквивалентно получению жетона).
if (water < capacity) {
water++;
return true;
}
}
// Если ведро заполнено, получить токен не удастся.
return false;
}
}
package com.artisan.leakybucket;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@RestController
public class LeakBucketController {
/**
*
*
* @return
*/
@GetMapping("/leakyBucket")
@LeakyBucketRateLimit(capacity = 10, leakRate = 2)
public ResponseEntity leakyBucket() {
return ResponseEntity.ok("leakyBucket test ok!");
}
}
алгоритм дырявого ведраи Алгоритм сегмента Самая очевидная разница между токенами – это Алгоритм. сегмента токенов допускает поток определенной степени выброса. Поскольку алгоритм по умолчанию сегмента токенов,Чтобы забрать жетон, не потребуется время.,То есть,Предположим, что в ведре 100 токенов.,Тогда 100 запросов можно будет разрешить мгновенно.
Алгоритм сегмента токеновпотому чтовыполнить Простой,И разрешить определенные чрезвычайные ситуации с потоком,удобный,Поэтому он широко применяется в отрасли. Конечно, нам необходимо детально проанализировать конкретную ситуацию.,Только наиболее подходящий алгоритм,Оптимального алгоритма не существует.
использоватьGuavaВходит в комплектRateLimiterвыполнить
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.1-jre</version>
</dependency>
Алгоритм сегмента токенов описывается через образную метафору: представьте, что есть ведро.,Ведро содержит определенное количество жетонов. Система добавляет токены в корзину по фиксированной ставке.,Перед отправкой каждый пакет должен получить токен из корзины. Если в ведре достаточно жетонов,Пакет можно отправить немедленно, если в корзине нет токена;,Затем пакет данных должен подождать,Пока в ведре не будет достаточно жетонов.
Ключевые параметры:
Алгоритм процесса:
package com.artisan.tokenbucket;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TokenBucketRateLimit {
/**
* Скорость генерации токенов (xx штук в секунду)
*
* @return
*/
double permitsPerSecond();
}
package com.artisan.tokenbucket;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@Aspect
@Component
public class TokenBucketRateLimitAspect {
// ИспользуйтеConcurrentHashMap для хранения Ограничения для каждого метода. токаустройство private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
// Совет по объему, используемый для добавления Ограничения до и после выполнения метода. токологика
@Around("@annotation(com.artisan.tokenbucket.TokenBucketRateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
// Получить подпись метода, используемую для получения информации о методе.
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// Получить объект метода на основе сигнатуры метода
Method method = signature.getMethod();
// Получить ограничение из объекта метода токааннотация TokenBucketRateLimit rateLimit = method.getAnnotation(TokenBucketRateLimit.class);
// Получите количество токенов в секунду, определенное в аннотации.
double permitsPerSecond = rateLimit.permitsPerSecond();
// Получить имя метода,как Ограничение Уникальный идентификатор устройства тока
String methodName = method.toString();
// если Ограничение Ограничения для этого метода в кеше сервера нет. токар, создай новый
RateLimiter rateLimiter = limiters.computeIfAbsent(methodName, k -> RateLimiter.create(permitsPerSecond));
// Попытайтесь получить токен, если его можно получить, продолжите выполнение метода.
if (rateLimiter.tryAcquire()) {
return joinPoint.proceed();
} else {
// Если токен не может быть получен, создается исключение, информирующее пользователя о том, что запрос выполняется слишком часто.
throw new RuntimeException("Too many requests, please try again later.");
}
}
}
package com.artisan.tokenbucket;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author маленький мастер
* @version 1.0
* @mark: show me the code , change the world
*/
@RestController
public class TokenBucketController {
@GetMapping("/tokenBucket")
@TokenBucketRateLimit(permitsPerSecond = 0.5)
public ResponseEntity tokenBucket() {
return ResponseEntity.ok("artisan token bucket");
}
}
В реализации интерфейса Ограничение В текущей стратегии вам следует выбрать подходящее Ограничение, исходя из конкретных бизнес-сценариев и системных требований. токаалгоритмивыполнить Способ,Также обратите внимание на влияние стратегии «Ограничение тока» на пользовательский опыт.,Для защиты стабильной работы системы,Это не доставит особых хлопот законным пользователям.