Причудливые трюки с высоким параллелизмом и ограничением тока

Spring Cloud

1 сцена

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

2 Ограничение тока

Помимо управления трафиком, целью ограничения тока является контроль поведения пользователей и предотвращение нежелательных запросов. Например, пользователи, публикующие сообщения, отвечающие и ставящие лайки на Tieba, должны строго контролироваться, а определенное поведение может происходить только N раз в течение определенного периода времени.

2.1 Схема ограничения тока

  • Ограничение тока прикладного уровня: NGINX, шлюз
  • Алгоритм ограничения тока: Корзины с маркерами, дырявые корзины и счетчики также могут быть реализованы с грубым ограничением тока.

2.2 Ограничение тока прикладного уровня

  • NGINX

Все знакомы с ним, он может быть не только легким веб-сервером, но также может выполнять балансировку нагрузки и ограничение тока.

Сервер ежедневно принимает сотни миллионов запросов, как контролировать доступ в единицу времени запроса?

В NGINX используйте geo, map, limit_req для достижения цели текущего ограничения, см. следующую конфигурацию:

##IP白名单
geo $whiteiplist {
     default 1;
     127.0.0.1 0;
     10.0.0.0/8 0;
  
}
map $whiteiplist $limit {
     1 $limit_key;
     0 "";
}
## 接口白名单
map $uri $limit2{
     default $limit;
     /api/sample "";

}
limit_req_status 406;
### 频率控制
limit_req_zone $limit2 zone=freq_controll:100m rate=10r/s;
limit_req_zone $limit2 zone=freq_controll_2:100m rate=500r/m;

Используйте алгоритм ограничения тока, определенный выше в местоположении

location / {
	limit_req zone=freq_controll burst=5 nodelay;
	limit_req zone=freq_controll_2 burst=10 nodelay;
	error_page 406 =406  @f406;
	location @f406 {
		access_log syslog:server=127.0.0.1:12301;
		return 406;
		}
	}

Я заметил, что в limit_req лопнули два странных параметра.Я также был озадачен, когда запустился nodelay.Поняв логику и алгоритм, стоящий за ним, я понял смысл.

Алгоритм модуля limit_req принадлежитАлгоритм ведра токенов.Может справиться с внезапными нагрузками Функция параметра разброса следующая: предположим, что за одну секунду на сервер одновременно отправляется 120 запросов.Согласно традиционному алгоритму воронки, дополнительные 20 запросов будут напрямую отклонены или помещены в очередь ожидания.И в токен Алгоритм ведра - это другая сцена.На самом деле в ведре токенов 100 токенов.Но разрешено 10 одновременных запросов.Еще 10 запросов будут отклонены.

  1. Если нодлей не настроен, эти 10 запросов будут поставлены в очередь. Они извлекаются со скоростью 0,001 секунды, потребляя в общей сложности 0,1 секунды. Обработка 110 запросов заняла 1,1 секунды. Фактически, это ожидание не нужно.

  2. При настройке нодлея лишние десять запросов будут обрабатываться нормально, но количество всплесков будет очищено.В ожидании пополнения токена будет получен запрос снова.Обработка 110 запросов заняла 1 секунду.Но вышеописанная ситуация то же самое, вам нужно дождаться пополнения токена, чтобы получить запрос.

  • Ограничение тока шлюза

Упомянутый здесь шлюз — это клиентский шлюз, репрезентативные — zuul и spring cloud gateway, которые ограничивают ток на едином входе и предотвращают сбой сервиса из-за высокочастотных запросов между различными серверами.

Используется в весеннем облачном шлюзеRequestRateLimiterФильтры можно использовать для ограничения потока, используя реализацию RateLimiter, чтобы определить, следует ли разрешить выполнение текущего запроса.Если запрос слишком велик, он по умолчанию возвращает статус HTTP 429 — слишком много запросов.

  1. Добавьте связанные зависимости в pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
  1. Добавьте класс конфигурации текущей политики ограничения.Здесь есть две стратегии: одна — ограничить ток в соответствии с именем пользователя в параметре запроса, а другая — ограничить ток в соответствии с IP-адресом доступа;
@Configuration
public class RedisRateLimiterConfig {
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("username"));
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}
  1. Мы используем Redis для ограничения тока, поэтому нам нужно добавить конфигурацию Redis и RequestRateLimiter, здесь все GET-запросы ограничены IP.
server:
  port: 9201
spring:
  redis:
    host: localhost
    password: 123456
    port: 6379
  cloud:
    gateway:
      routes:
        - id: requestratelimiter_route
          uri: http://localhost:8201
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 #每秒允许处理的请求数量
                redis-rate-limiter.burstCapacity: 2 #每秒最大处理的请求数量
                key-resolver: "#{@ipKeyResolver}" #限流策略,对应策略的Bean
          predicates:
            - Method=GET
logging:
  level:
    org.springframework.cloud.gateway: debug

2.2 Алгоритм ограничения тока

2.2.1 Счетчик

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

counter.png

Пример рукописного кода счетчика:

/**
 * 功能说明: 手写计数器
 */
public class LimitService {
	private int limtCount = 60;// 限制最大访问的容量
	AtomicInteger atomicInteger = new AtomicInteger(0); // 每秒钟 实际请求的数量
	private long start = System.currentTimeMillis();// 获取当前系统时间
	private int interval = 60;// 间隔时间60秒
	public boolean acquire() {
		long newTime = System.currentTimeMillis();
		if (newTime > (start + interval)) {
			// 判断是否是一个周期
			start = newTime;
			atomicInteger.set(0); // 清理为0
			return true;
		}
		atomicInteger.incrementAndGet();// i++;
		return atomicInteger.get() <= limtCount;
	}
	static LimitService limitService = new LimitService();
	public static void main(String[] args) {
		ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
		for (int i = 1; i < 100; i++) {
			final int tempI = i;
			newCachedThreadPool.execute(new Runnable() {
				public void run() {
					if (limitService.acquire()) {
						System.out.println("你没有被限流,可以正常访问逻辑 i:" + tempI);
					} else {
						System.out.println("你已经被限流了呢  i:" + tempI);
					}
				}
			});
		}
	}
}

вопрос: счетчик может генерироватькритическийПроблема в том, что если в критический момент собирается большой объем трафика, например, 10 запросов обрабатываются за 59 секунд, а 10 запросов обрабатываются за 61 секунду. Таким образом, в течение 2 секунд выполняется 20 запросов, что нарушает наш набор из 10 запросов в течение 60 секунд.

В этом вопросе мы можемс помощью раздвижного окнаРешить проблему порогов счетчика

2.2.2 Счетчик скользящего окна

Принцип скользящего окна: каждый раз, когда приходит посещение, сначала определяем, превышает ли общее количество посещений в первые N единиц времени установленный порог, и прибавляем 1 к количеству запросов в текущем временном интервале.

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

Пример скользящего окна: разделен на 6 сеток в течение минуты, каждая сетка имеет время доступа 10 секунд, и каждая сетка имеет свой независимый счетчик. Разрешено только 1000 запросов за 60 секунд.

image.png

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

2.2.3 Алгоритм корзины токенов

Алгоритм ведра токенов — это ведро, в котором хранятся токены с фиксированной емкостью, и токены добавляются в ведро с фиксированной скоростью.

Принцип алгоритма Token Bucket: Предполагая, что ограничение составляет 2r/s, токены добавляются в корзину с фиксированной скоростью 500 миллисекунд; в корзине хранится не более b токенов,Когда ведро заполнено, вновь добавленные токены отбрасываются или отклоняются.; когда приходит пакет размером n байт, n токенов удаляются из корзины, и пакет отправляется в сеть; если в корзине меньше n токенов, токен не удаляется, а пакет будет дросселироваться (либо удалены, либо буферизованы).

image.png

Пример

  • Используйте RateLimiter для реализации текущего лимита корзины токенов.

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

  1. Представьте maven-зависимости гуавы.
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>25.1-jre</version>
</dependency>
  1. Реализовать алгоритм корзины токенов с использованием RateLimiter
@RestController
public class IndexController {
	@Autowired
	private OrderService orderService;
	// 解释:1.0 表示 每秒中生成1个令牌存放在桶中
	RateLimiter rateLimiter = RateLimiter.create(1.0);
	// 下单请求
	@RequestMapping("/order")
	public String order() {
		// 1.限流判断
		// 如果在500秒内 没有获取不到令牌的话,则会一直等待
		System.out.println("生成令牌等待时间:" + rateLimiter.acquire());
		boolean acquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
		if (!acquire) {
			System.out.println("你在怎么抢,也抢不到,因为会一直等待的,你先放弃吧!");
			return "你在怎么抢,也抢不到,因为会一直等待的,你先放弃吧!";
		}
		// 2.如果没有达到限流的要求,直接调用订单接口
		boolean isOrderAdd = orderService.addOrder();
		if (isOrderAdd) {
			return "恭喜您,抢购成功!";
		}
		return "抢购失败!";
	}
}

Это простое автономное ограничение тока, которое также можно преобразовать в плавный интерфейс ввода через аннотацию плюс АОП. 3. Пакетный ограничитель скорости

  • пользовательская аннотация
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtRateLimiter {
	double value();
	long timeOut();
}
  • Написание АОП
@Aspect
@Component
@Slf4j
public class RateLimiterAspect {

    /**
     * 存放接口是否已经存在
     */
    private static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();

    @Pointcut("@annotation(extRateLimiter)")
    public void pointcut(ExtRateLimiter extRateLimiter) {

    }

    @Around(value = "pointcut(extRateLimiter)", argNames = "proceedingJoinPoint,extRateLimiter")
    public Object doBefore(ProceedingJoinPoint proceedingJoinPoint, ExtRateLimiter extRateLimiter) throws Throwable {

        // 获取配置的速率
        double permitsPerSecond = extRateLimiter.permitsPerSecond();
        // 获取等待令牌等待时间
        long timeOut = extRateLimiter.timeOut();
        RateLimiter rateLimiter = getRateLimiter(permitsPerSecond);
        boolean acquire = rateLimiter.tryAcquire(timeOut, TimeUnit.MILLISECONDS);
        if (acquire) {
            return proceedingJoinPoint.proceed();
        }
        HttpServletUtil.write2ExceptionResult("执行降级方法,亲,服务器忙!请稍后重试!");
        return null;
    }

    private RateLimiter getRateLimiter(double permitsPerSecond) {
        // 获取当前URL
        HttpServletRequest servletRequest = HttpServletUtil.getRequest();
        String requestURI = servletRequest.getRequestURI();
        RateLimiter rateLimiter = rateLimiterMap.get(requestURI);
        if (ObjectUtils.isEmpty(rateLimiter)) {
            rateLimiter = RateLimiter.create(permitsPerSecond);
            rateLimiterMap.put(requestURI, rateLimiter);
        }
        return rateLimiter;
    }
}
  • Пример использования
@RequestMapping("/myOrder")
@ExtRateLimiter(value = 10.0, timeOut = 500)
public String myOrder() throws InterruptedException {
		System.out.println("myOrder");
		return "SUCCESS";
}

2.2.4 Алгоритм дырявого ведра

Алгоритм дырявого ведра в качестве измерителя может использоваться для формирования трафика и контроля трафика.

  • Принцип алгоритма дырявого ведра: Негерметичное ведро фиксированной вместимости, из которого капли воды вытекают с постоянной фиксированной скоростью; если ведро пусто, капли воды не должны вытекать; капли воды могут стекать в негерметичное ведро с любой скоростью; если втекающие капли воды превышают ведра, втекающие капли воды переливаются (выбрасываются), а вместимость негерметичного ведра не изменяется.

image.png

  • Сравнение ведра с токенами и дырявого ведра:
  1. Разница в методе:ведро с жетонамиЭто добавление токенов в ведро с фиксированной скоростью.Обработка запроса зависит от того, достаточно ли токенов в ведре.Когда количество токенов уменьшается до нуля, новый запрос отклоняется;дырявое ведроИсходящий запрос имеет постоянную фиксированную скорость, а скорость входящего запроса является произвольной.Когда количество входящих запросов накапливается до емкости дырявого ведра, новый входящий запрос отклоняется;
  2. Преимущества и недостатки:ведро с жетонамиОграничение — это средняя скорость притока (разрешены пакетные запросы, которые могут обрабатываться до тех пор, пока есть токены, и поддерживается прием 3 токенов и 4 токенов за раз), а также допускается определенная степень пакетного трафика;дырявое ведроПредел - постоянная скорость оттока (то есть скорость оттока - фиксированное постоянное значение, например, скорость оттока равна 1, вместо 1 в один момент времени и 2 в следующий раз), тем самым сглаживая всплеск скорости притока;

Примечание. Ведро с жетонами допускает определенную степень взрыва, а основная цель дырявого ведра — сгладить скорость притока;

  • Сценарии применения: «Алгоритм дырявого ведра» можетПринудительно ограничить скорость передачи данных, а «Алгоритм Token Bucket» допускает определенную степень пакетной передачи в дополнение к ограничению средней скорости передачи данных. В «Алгоритме корзины маркеров», пока в корзине маркеров есть токены, данные могут передаваться в пакетах до тех пор, пока не будет достигнут порог, настроенный пользователем, поэтому он подходит для тех, у кого естьХарактеристика взрыватрафик

  • выполнить

@Slf4j
public class LeakyBucketLimiter {
    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
 
    // 桶的容量
    public int capacity = 10;
    // 当前水量
    public int water = 0;
    //水流速度/s
    public int rate = 4;
    // 最后一次加水时间
    public long lastTime = System.currentTimeMillis();
 
    public void acquire() {
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            long now = System.currentTimeMillis();
            //计算当前水量
            water = Math.max(0, (int) (water - (now - lastTime) * rate /1000));
            int permits = (int) (Math.random() * 8) + 1;
            log.info("请求数:" + permits + ",当前桶余量:" + (capacity - water));
            lastTime = now;
            if (capacity - water < permits) {
                // 若桶满,则拒绝
                log.info("限流了");
            } else {
                // 还有容量
                water += permits;
                log.info("剩余容量=" + (capacity - water));
            }
        }, 0, 500, TimeUnit.MILLISECONDS);
    }
 
    public static void main(String[] args) {
        LeakyBucketLimiter limiter = new LeakyBucketLimiter();
        limiter.acquire();
    }
}
  • бегать

image.png

3 Резюме

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

image.png