С ростом популярности микросервисов зависимости и отношения вызовов между сервисами становятся все более сложными, а стабильность сервисов становится особенно важной. Бизнес-сценарии часто включают в себя мгновенное воздействие на трафик, что может привести к тайм-аутам запросов и ответов или даже к перегрузке, отключению и недоступности сервера. Чтобы защитить саму систему, а также вышестоящие и нижестоящие службы, мы обычно ограничиваем поток запросов и быстро отклоняем запросы, которые превышают предел конфигурации, чтобы обеспечить стабильность системы или вышестоящих и нижестоящих сервисных систем. Разумные стратегии могут эффективно справляться с последствиями дорожного движения и обеспечивать доступность и производительность системы. В этой статье подробно представлены несколько алгоритмов ограничения тока, сравниваются преимущества и недостатки каждого алгоритма, даются некоторые предложения по выбору алгоритмов ограничения тока, а также предлагаются некоторые решения для ограничения распределенного тока, обычно используемые в бизнесе.
Существует три основных инструмента для защиты стабильности высокопараллельных сервисов: кэширование, понижение версии и ограничение тока.
Каждый из этих трех «острых инструментов» имеет свои особенности и обычно используется в сочетании для достижения наилучших результатов. Например, кэширование можно использовать для уменьшения доступа к базе данных, переход на более раннюю версию можно использовать для борьбы с сбоями системы, а ограничение тока можно использовать для предотвращения перегрузки системы. При проектировании системы с высоким уровнем параллелизма эти технологии необходимо использовать рационально, исходя из конкретных потребностей и характеристик системы. Далее в этой статье будут представлены некоторые современные методы ограничения, обычно используемые в отрасли.
Ограничение тока — это ключевое техническое средство ограничения количества запросов или одновременного выполнения с целью обеспечения нормальной работы системы. Когда ресурсы службы и возможности обработки ограничены, ограничение потока может ограничить восходящие запросы на вызов служб, чтобы предотвратить остановку службы из-за исчерпания ресурсов.
При ограничении тока необходимо понимать две важные концепции:
Устанавливая разумные пороговые значения и выбирая соответствующие стратегии отклонения, технология ограничения тока может помочь системе справиться с внезапными скачками объема запросов, злонамеренным доступом пользователей или чрезмерной частотой запросов, обеспечивая стабильность и доступность системы. Схемы ограничения тока можно разделить на Ограничение тока одной машины и ограничение распределенного тока Среди них алгоритм ограничения тока отдельной машины можно разделить на; Существует четыре распространенных типа: фиксированное окно, скользящее окно, дырявое ведро и ограничение тока ведра токенов. . В этой статье будет подробно представлена описанная выше схема ограничения тока.
Алгоритм фиксированного окна — это простой и интуитивно понятный алгоритм ограничения тока. Его принцип состоит в том, чтобы разделить время на окна фиксированного размера и ограничить количество или скорость запросов в каждом окне. В конкретной реализации счетчик может использоваться для записи количества запросов в текущем окне и сравнения его с заданным пороговым значением. Принцип алгоритма фиксированного окна заключается в следующем:
type FixedWindowLimiter struct {
windowSize time.Duration // размер окна
maxRequests int // Максимальное количество запросов
requests int // Количество запросов в текущем окне
lastReset int64 // Время сброса последнего окна (метка времени второго уровня)
resetMutex sync.Mutex // сбросить блокировку
}
func NewFixedWindowLimiter(windowSize time.Duration, maxRequests int) *FixedWindowLimiter {
return &FixedWindowLimiter{
windowSize: windowSize,
maxRequests: maxRequests,
lastReset: time.Now().Unix(),
}
}
func (limiter *FixedWindowLimiter) AllowRequest() bool {
limiter.resetMutex.Lock()
defer limiter.resetMutex.Unlock()
// Проверьте, нужно ли сбросить окно
if time.Now().Unix()-limiter.lastReset >= int64(limiter.windowSize.Seconds()) {
limiter.requests = 0
limiter.lastReset = time.Now().Unix()
}
// Проверьте, не превышает ли количество запросов порог
if limiter.requests >= limiter.maxRequests {
return false
}
limiter.requests++
return true
}
func main() {
limiter := NewFixedWindowLimiter(1*time.Second, 3) // Разрешить до 3 запросов в секунду
for i := 0; i < 15; i++ {
now := time.Now().Format("15:04:05")
if limiter.AllowRequest() {
fmt.Println(now + " Запрос через ")
} else {
fmt.Println(now + " Запросы ограничиваются")
}
time.Sleep(100 * time.Millisecond)
}
}
Результат выполнения:
преимущество:
недостаток:
Алгоритм с фиксированным окном подходит для сценариев, где существуют четкие требования к скорости запросов и трафик относительно стабилен. Однако в ситуациях, когда пакетный трафик и запросы распределяются неравномерно, может потребоваться рассмотреть другие более гибкие алгоритмы ограничения тока.
Выше было объяснено, что при возникновении критических изменений во временном окне алгоритм фиксированного окна может оказаться неспособным гибко реагировать на изменения в трафике. Например, если в конце временного окна внезапно появляется большое количество запросов, алгоритм фиксированного окна может привести к отклонению запросов, даже если в следующем временном окне запросов не так много. В этом случае нам нужен более гибкий алгоритм для борьбы с пакетным трафиком и неравномерным распределением запросов — алгоритм скользящего окна. Этот алгоритм является усовершенствованием алгоритма фиксированного окна, который динамически регулирует размер окна, чтобы лучше адаптироваться к изменениям трафика. В отличие от алгоритма фиксированного окна, алгоритм скользящего окна может регулировать размер окна перед появлением следующего временного окна, чтобы лучше контролировать скорость запросов. Принцип алгоритма следующий:
package main
import (
"fmt"
"sync"
"time"
)
type SlidingWindowLimiter struct {
windowSize time.Duration // размер окна
maxRequests int // Максимальное количество запросов
requests []time.Time // Время запроса в пределах окна
requestsLock sync.Mutex // запросить блокировку
}
func NewSlidingWindowLimiter(windowSize time.Duration, maxRequests int) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
windowSize: windowSize,
maxRequests: maxRequests,
requests: make([]time.Time, 0),
}
}
func (limiter *SlidingWindowLimiter) AllowRequest() bool {
limiter.requestsLock.Lock()
defer limiter.requestsLock.Unlock()
// Удалить запросы с истекшим сроком действия
currentTime := time.Now()
for len(limiter.requests) > 0 && currentTime.Sub(limiter.requests[0]) > limiter.windowSize {
limiter.requests = limiter.requests[1:]
}
// Проверьте, не превышает ли количество запросов порог
if len(limiter.requests) >= limiter.maxRequests {
return false
}
limiter.requests = append(limiter.requests, currentTime)
return true
}
func main() {
limiter := NewSlidingWindowLimiter(500*time.Millisecond, 2) // Позволяет до 4 запросов в секунду
for i := 0; i < 15; i++ {
now := time.Now().Format("15:04:05")
if limiter.AllowRequest() {
fmt.Println(now + " Запрос через ")
} else {
fmt.Println(now + " Запросы ограничиваются")
}
time.Sleep(100 * time.Millisecond)
}
}
Результат выполнения:
преимущество:
недостаток:
С точки зрения кода мы можем обнаружить, что алгоритм скользящего окна на самом деле представляет собой алгоритм фиксированного окна с меньшей детализацией. Он может в определенной степени повысить точность и производительность ограничения тока в реальном времени, но не может фундаментально решить проблему неравномерности. распространение запроса. Алгоритм ограничен размером окна и временным интервалом. Особенно в крайних случаях, например, когда пакетный трафик слишком велик или распределение запросов крайне неравномерно, это все равно может привести к неточному ограничению тока. Поэтому в практических приложениях необходимо использовать более сложные алгоритмы или стратегии для дальнейшей оптимизации эффекта ограничения тока.
Хотя алгоритм скользящего окна может обеспечить определенный эффект ограничения потока.,Но оно по-прежнему ограничено размером окна и временным интервалом. в некоторых случаях,Внезапный всплеск может привести к тому, что количество запросов в окне превысит ограничение. Для лучшего сглаживания запросите изпоток,ограничение тока дырявого ведраалгоритм можно усовершенствовать как алгоритм скользящего окна. Принцип алгоритма прост: он поддерживает дырявое ведро фиксированной емкости, запросы поступают в дырявое ведро с переменной скоростью, а из дырявого ведра вытекает с фиксированной скоростью. Если на момент поступления запроса дырявое ведро заполнено, сработает Запретить. политику。Можно получить измодель производитель-потребительчтобы понять этоалгоритм,Просьба выступить в роли продюсера,Каждая просьба как капля воды,когда поступит запрос,Он помещается в очередь (дырявое ведро). Дырявое ведро выступает в роли потребителя,Потребляйте запросы из очереди с фиксированной скоростью.,Это как вода, капающая из дырок на дне бочки. Скорость потребления равна текущему пределу порога,Например, обрабатывать 2 запроса в секунду.,Прямо сейчас500Потребляйте один в миллисекундупросить。утечка Емкость ствол похож на очередь из-за переполнения,Когда количество запросов превышает указанную емкость,Вызовет Запретить политики, то есть вновь поступающие запросы будут отбрасываться или задерживаться. алгоритм реализуется следующим образом:
package main
import (
"fmt"
"time"
)
type LeakyBucket struct {
rate float64 // Частота дырявого сегмента, количество запросов в секунду
capacity int // Емкость дырявого сегмента, максимальное количество запросов, которые можно сохранить.
water int // Текущий объем воды указывает количество запросов в текущем дырявом ведре.
lastLeakMs int64 // Временная метка последней утечки воды, в секундах
}
func NewLeakyBucket(rate float64, capacity int) *LeakyBucket {
return &LeakyBucket{
rate: rate,
capacity: capacity,
water: 0,
lastLeakMs: time.Now().Unix(),
}
}
func (lb *LeakyBucket) Allow() bool {
now := time.Now().Unix()
elapsed := now - lb.lastLeakMs
// Утечка воды, расчет количества утекшей воды по временному интервалу
leakAmount := int(float64(elapsed) / 1000 * lb.rate)
if leakAmount > 0 {
if leakAmount > lb.water {
lb.water = 0
} else {
lb.water -= leakAmount
}
}
// Определите, превышает ли текущий объем воды емкость
if lb.water > lb.capacity {
lb.water-- // Если емкость превышена, вычтите количество только что добавленной воды.
return false
}
// увеличить объем воды
lb.water++
lb.lastLeakMs = now
return true
}
func main() {
// Создайте дырявое ведро со скоростью 3 запроса в секунду и емкостью 4 запроса.
leakyBucket := NewLeakyBucket(3, 4)
// Имитировать запрос
for i := 1; i <= 15; i++ {
now := time.Now().Format("15:04:05")
if leakyBucket.Allow() {
fmt.Printf(now+" Нет. %d запросы переданы\n", i)
} else {
fmt.Printf(now+" Нет. %d запросы ограничены\n", i)
}
time.Sleep(200 * time.Millisecond) // Имитировать интервал запроса
}
}
Результат выполнения:
преимущество:
недостаток:
Алгоритм ведра токенов — это распространенная идея реализации текущего ограничения, которое используется для ограничения скорости запросов. Это гарантирует, что система по-прежнему сможет предоставлять стабильные услуги в условиях высокой нагрузки, и предотвращает перегрузку системы пакетным трафиком. Наиболее часто используемый класс инструмента ограничения скорости RateLimiter в наборе инструментов Google для разработки Java Guava представляет собой реализацию корзины токенов. Алгоритм корзины токенов основан на концепции корзины токенов, где токены генерируются с фиксированной скоростью и помещаются в корзину. Каждый токен представляет запрошенное разрешение. Когда поступает запрос, для его передачи необходимо получить токен из корзины токенов. Если в корзине токенов недостаточно токенов, запрос регулируется или отбрасывается. Шаги реализации алгоритма корзины токенов следующие:
package main
import (
"fmt"
"sync"
"time"
)
// TokenBucket Представляет сегмент токенов.
type TokenBucket struct {
rate float64 // Скорость, с которой токены добавляются в корзину.
capacity float64 // Ведро максимальной вместимости.
tokens float64 // Количество токенов в текущем сегменте.
lastUpdate time.Time // Последнее обновление количества токенов за раз.
mu sync.Mutex // Блокировка мьютекса для обеспечения потокобезопасности.
}
// NewTokenBucket Создает новую корзину токенов с учетом скорости добавления токенов и емкости корзины.
func NewTokenBucket(rate float64, capacity float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: capacity,
tokens: capacity, // Изначально ведро полное.
lastUpdate: time.Now(),
}
}
// Allow Проверьте, можно ли удалить токен из корзины. Если это возможно, он удаляет токен и возвращает true。
// Если нет, он возвращает false。
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
// Рассчитайте количество добавляемых токенов на основе затраченного времени.
now := time.Now()
elapsed := now.Sub(tb.lastUpdate).Seconds()
tokensToAdd := elapsed * tb.rate
tb.tokens += tokensToAdd
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity // Убедитесь, что количество токенов не превышает емкость корзины.
}
if tb.tokens >= 1.0 {
tb.tokens--
tb.lastUpdate = now
return true
}
return false
}
func main() {
tokenBucket := NewTokenBucket(2.0, 3.0)
for i := 1; i <= 10; i++ {
now := time.Now().Format("15:04:05")
if tokenBucket.Allow() {
fmt.Printf(now+" Нет. %d запросы переданы\n", i)
} else { // Если токен невозможно удалить, запрос отклоняется.
fmt.Printf(now+" Нет. %d запросы ограничены\n", i)
}
time.Sleep(200 * time.Millisecond)
}
}
Результат выполнения:
преимущество:
недостаток:
Ограничение тока для одной машины относится к ситуации с одним сервером путем ограничения количества запросов, обрабатываемых одним сервером в единицу времени, чтобы предотвратить перегрузку сервера. Общий алгоритм ограничения тока был представлен выше. Его преимуществами являются простота реализации, высокая эффективность и очевидный эффект. В связи с популярностью микросервисной архитектуры системные службы обычно развертываются на нескольких серверах. В настоящее время для обеспечения стабильности всей системы требуется распределенное ограничение тока. В этой статье будут представлены несколько распространенных технологических решений по ограничению распределенного тока:
Все запросы к серверу контролируются через централизованный ограничитель потока. Метод реализации:
package main
import (
"context"
"fmt"
"go.uber.org/atomic"
"sync"
"git.code.oa.com/pcg-csd/trpc-ext/redis"
)
type RedisClient interface {
Do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error)
}
// Client база данных
type Client struct {
client RedisClient // redis действовать
script string // Lua-скрипт
}
// NewBucketClient Создать корзину токенов Redis
func NewBucketClient(redis RedisClient) *Client {
helper := redis
return &Client{
client: helper,
script: `
-- Ограничение тока сегмента токенов Скрипт -- KEYS[1]: Название ствола
-- ARGV[1]: Емкость ствола
-- ARGV[2]: Скорость генерации токенов
local bucket = KEYS[1]
local capacity = tonumber(ARGV[1])
local tokenRate = tonumber(ARGV[2])
local redisTime = redis.call('TIME')
local now = tonumber(redisTime[1])
local tokens, lastRefill = unpack(redis.call('hmget', bucket, 'tokens', 'lastRefill'))
tokens = tonumber(tokens)
lastRefill = tonumber(lastRefill)
if not tokens or not lastRefill then
tokens = capacity
lastRefill = now
else
local intervalsSinceLast = (now - lastRefill) * tokenRate
tokens = math.min(capacity, tokens + intervalsSinceLast)
end
if tokens < 1 then
return 0
else
redis.call('hmset', bucket, 'tokens', tokens - 1, 'lastRefill', now)
return 1
end
`,
}
}
// Получите токен. Если приобретение прошло успешно, он немедленно вернет true, в противном случае — false.
func (c *Client) isAllowed(ctx context.Context, key string, capacity int64, tokenRate int64) (bool, error) {
result, err := redis.Int(c.client.Do(ctx, "eval", c.script, 1, key, capacity, tokenRate))
if err != nil {
fmt.Println("Redis Ошибка выполнения: ", err)
return false, err
}
return result == 1, nil
}
// обнаружение звонков
func main() {
c := NewBucketClient(redis.GetPoolByName("redis://127.0.0.1:6379"))
gw := sync.WaitGroup{}
gw.Add(120)
count := atomic.Int64{}
for i := 0; i < 120; i++ {
go func(i int) {
defer gw.Done()
status, err := c.isAllowed(context.Background(), "test", 100, 10)
if status {
count.Add(1)
}
fmt.Printf("go %d status:%v error: %v\n", i, status, err)
}(i)
}
gw.Wait()
fmt.Printf("allow %d\n\n", count.Load())
}
Результат выполнения:
Официальная таблица тестирования производительности Redis
Видно, что централизованное решение по ограничению тока имеет высокий риск отказа в одной точке, а узкое место в полосе пропускания является серьезным. На основе этого в данной статье разрабатывается новое решение для ограничения распределенного тока, сочетающее ограничение тока на одной машине с локальным кэшем и балансировку нагрузки. Конкретные планы таковы:
Используйте службы распределенной координации, такие как ZooKeeper или etcd, для реализации ограничения тока. Каждый сервер подает заявку на получение токена от службы распределенной координации, и обрабатываться могут только те запросы, которые получают токен. Базовый план:
Преимущество этого решения заключается в том, что оно позволяет добиться точного глобального ограничения тока и избежать единых точек отказа. Однако недостатком этого решения является сложность реализации и высокие требования к производительности ZooKeeper. Если ZooKeeper не может обрабатывать большое количество операций применения и выпуска токенов, это может стать узким местом в системе.
Короче говоря, не существует лучшего решения, есть только правильное решение.В выборе подходящегоиз При использовании схемы ограничения тока,Нам нужно учитывать множество факторов,Включая требования к системе, существующий стек технологий, условия нагрузки системы, базовую производительность системы и т. д. Понимание принципов работы и характеристик каждого решения.,Чтобы сделать лучший выбор в практическом применении.
Хорошая конструкция ограничения тока должна учитывать характеристики и потребности бизнеса и иметь следующие шесть пунктов:
Ограничение тока является важным средством обеспечения стабильной и эффективной работы системы, но не единственным решением.Нам также необходимо рассмотреть другиеизсистема Инструменты проектирования и оптимизации,Например, балансировка нагрузки, кэширование, асинхронная обработка и т. д. (в условиях взрывного объема,Расширение – всегда лучший способ,Вот только это дорого! ). Эти средства работают вместе,Чтобы построить систему, способную обрабатывать большое количество одновременных запросов,Это также обеспечивает качество обслуживания изсистемы.