Начало работы с ограничением тока Guava RateLimiter

Java

предисловие

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

  • 缓存: Кэш предназначен для повышения скорости доступа к системе и увеличения вычислительной мощности системы.

  • 降级: Даунгрейд означает, что когда возникает проблема со службой или затрагивается основной процесс, его необходимо временно заблокировать и открыть после пика или решения проблемы.

  • 限流: Целью ограничения тока является защита системы путем ограничения скорости одновременного доступа/запросов или ограничения скорости запросов в пределах временного окна.По достижении предела скорости он может отказать в обслуживании, поставить в очередь или ждать, понизить версию, и Т. Д.

Общие алгоритмы ограничения тока

  1. алгоритм дырявого ведра

Идея алгоритма дырявого ведра очень проста.Вода (просьба) сначала поступает в дырявое ведро, а дырявое ведро вытекает с определенной скоростью.Когда скорость притока воды слишком высока, она будет переливаться напрямую.Это видно, что алгоритм дырявого ведра может принудительно ограничивать скорость передачи данных.

  1. Алгоритм ведра токенов

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

Использование RateLimiter и анализ исходного кода

Инструментарий Google с открытым исходным кодом Guava предоставляет инструмент класса RateLimiter для ограничения скорости, который реализует ограничение трафика на основе алгоритма Token Bucket, который очень удобен и эффективен в использовании.

Использование RateLimiter

Во-первых, краткое введение в использование RateLimiter

public void testAcquire() {
      RateLimiter limiter = RateLimiter.create(1);
      for(int i = 1; i < 10; i = i + 2 ) {
          double waitTime = limiter.acquire(i);
          System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
      }
  }

Выходной результат:

cutTime=1535439657427 acq:1 waitTime:0.0
cutTime=1535439658431 acq:3 waitTime:0.997045
cutTime=1535439661429 acq:5 waitTime:2.993028
cutTime=1535439666426 acq:7 waitTime:4.995625
cutTime=1535439673426 acq:9 waitTime:6.999223

сначала черезRateLimiter.create(1)Создайте ограничитель с параметром, представляющим количество токенов, генерируемых в секунду, черезlimiter.acquire(i)получить токен в блокировке или, конечно,tryAcquire(int permits, long timeout, TimeUnit unit)Чтобы установить способ ожидания в течение времени, если тайм-аут равен 0, это будет представлять собой неблокировку, и получение будет немедленно возвращено.

С точки зрения вывода RateLimiter поддерживает предварительное потребление. Например, при получении (5) время ожидания составляет 3 секунды. Когда был получен последний токен, было предварительно использовано 3 две строки. Необходимо дождаться 3*1 секунда, а затем предварительное потребление. Было израсходовано 5 токенов и так далее.

RateLimiter поддерживает определенный уровень пакетных запросов (предварительное потребление), ограничивая время ожидания последующих запросов., на этот момент нужно обращать внимание в процессе использования, а конкретный принцип реализации будет разобран позже.

Принцип реализации RateLimiter

Guava имеет два режима ограничения тока: один — стабильный режим (SmoothBursty: скорость генерации токенов постоянна), другой — прогрессивный режим (SmoothWarmingUp: скорость генерации токенов медленно увеличивается, пока не будет поддерживаться стабильное значение). режимы имеют схожие идеи реализации. , основное отличие в расчете времени ожидания, в данной статье речь пойдет о SmoothBursty

Ранесимиторное создание

Экземпляр создается путем вызова интерфейса создания RateLimiter, который на самом деле является экземпляром, созданным вызванным стабильным режимом SmoothBuisty.

public static RateLimiter create(double permitsPerSecond) {
    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
  }

  static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }

SmoothBurstyЗначение двух параметров построения в :

  • SleepingStopwatch: экземпляр класса часов в гуаве, который будет рассчитывать время и токены посредством этого
  • maxBurstSeconds: официальное объяснение состоит в том, что когда ReteLimiter не используется, токен будет сохраняться не более нескольких секунд, по умолчанию 1.

Прежде чем анализировать принцип SmoothBurty, сосредоточьтесь на объяснении значения нескольких атрибутов в SmoothBursty.

/**
 * The work (permits) of how many seconds can be saved up if this RateLimiter is unused?
 * 在RateLimiter未使用时,最多存储几秒的令牌
 * */
 final double maxBurstSeconds;
 

/**
 * The currently stored permits.
 * 当前存储令牌数
 */
double storedPermits;

/**
 * The maximum number of stored permits.
 * 最大存储令牌数 = maxBurstSeconds * stableIntervalMicros(见下文)
 */
double maxPermits;

/**
 * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
 * per second has a stable interval of 200ms.
 * 添加令牌时间间隔 = SECONDS.toMicros(1L) / permitsPerSecond;(1秒/每秒的令牌数)
 */
double stableIntervalMicros;

/**
 * The time when the next request (no matter its size) will be granted. After granting a request,
 * this is pushed further in the future. Large requests push this further than small requests.
 * 下一次请求可以获取令牌的起始时间
 * 由于RateLimiter允许预消费,上次请求预消费令牌后
 * 下次请求需要等待相应的时间到nextFreeTicketMicros时刻才可以获取令牌
 */
private long nextFreeTicketMicros = 0L; // could be either in the past or future

Далее представлены несколько ключевых функций

  • setRate
public final void setRate(double permitsPerSecond) {
  checkArgument(
      permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
  synchronized (mutex()) {
    doSetRate(permitsPerSecond, stopwatch.readMicros());
  }
}

Этот интерфейс обеспечивается количеством токенов через токен, генерируемых в секунду, что достигается путем вызова внутреннего времени doSetRate SmoothRateLimiter.

  • doSetRate
@Override
  final void doSetRate(double permitsPerSecond, long nowMicros) {
    resync(nowMicros);
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
    this.stableIntervalMicros = stableIntervalMicros;
    doSetRate(permitsPerSecond, stableIntervalMicros);
  }

Здесь сначала создается токен путем вызова повторной синхронизации, затем обновляется время генерации следующего токена, затем обновляется стабильный интервалMicros и, наконец, вызывается doSetRate SmoothBursty.

  • resync
/**
 * Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.
 * 基于当前时间,更新下一次请求令牌的时间,以及当前存储的令牌(可以理解为生成令牌)
 */
void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      storedPermits = min(maxPermits, storedPermits + newPermits);
      nextFreeTicketMicros = nowMicros;
    }
}

Согласно алгоритму ведра токенов, токены в ведре постоянно генерируются и хранятся.При наличии запроса токен необходимо получить из ведра перед выполнением.Кто будет постоянно генерировать токен и хранить его?

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

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

  • DoSetRate от SmoothBurty
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
  double oldMaxPermits = this.maxPermits;
  maxPermits = maxBurstSeconds * permitsPerSecond;
  if (oldMaxPermits == Double.POSITIVE_INFINITY) {
    // if we don't special-case this, we would get storedPermits == NaN, below
    // Double.POSITIVE_INFINITY 代表无穷啊
    storedPermits = maxPermits;
  } else {
    storedPermits =
        (oldMaxPermits == 0.0)
            ? 0.0 // initial state
            : storedPermits * maxPermits / oldMaxPermits;
  }
}

Максимальное количество токенов, которое может храниться в корзине, вычисляется из maxBurstSeconds, что означает, что токены, сгенерированные maxBurstSeconds, могут быть сохранены максимально. Функция этого параметра заключается в более гибком управлении потоком. Например, некоторые интерфейсы ограничены 300 раз/20 секунд, некоторые интерфейсы ограничены 50 разами/45 секундами и т. д. То есть трафик не ограничивается qps

Ссылаться на

Эпилог

Добро пожаловать в общедоступную учетную запись WeChat «code zonE», посвященную обмену контентом, связанным с Java и облачными вычислениями, включая SpringBoot, SpringCloud, микросервисы, Docker, Kubernetes, Python и другие сопутствующие технические товары, с нетерпением жду встречи с вами!