В рамках микросервисной архитектуры на базе Spring Cloud необходимо добавить функцию ограничения тока на шлюзе: например, при доступе к определенному интерфейсу с заданного ip-адреса частота доступа ограничена до 100 раз/с.
Общий принцип таков: на основе выполнения требований реализация проста и удобна в обслуживании.
Инфраструктура всей платформы выглядит следующим образом:
nginx -> [шлюз1, шлюз2, …] -> [сервисA1, сервисA2, сервисB1, …]
1. Автономное ограничение тока на основе памяти
A: Прежде всего, рассмотрим ограничение тока на одной машине на основе памяти, его преимущества в основном заключаются в простоте реализации и хорошей производительности;
В: Однако, чтобы повысить доступность и производительность системы, мне нужно развернуть несколько экземпляров шлюза, а память не может быть разделена между несколькими экземплярами;
О: Допустим, сформулирована текущая политика ограничения: частота доступа интерфейса А ограничена 100 раз/с, при развертывании двух шлюзов и настройке балансировки нагрузки на nginx частота доступа на каждом шлюзе ограничена 50 раз/сек. , раз, это может в основном удовлетворить потребности.
В: Но если мне нужно добавить еще один экземпляр шлюза сейчас или если один из двух развернутых экземпляров шлюза зависнет, первоначальная текущая политика ограничения не может быть удовлетворена.
A: В этом случае должен быть механизм, чтобы понять, все ли службы шлюза работают нормально. Поскольку он основан на платформе Spring Cloud, должен быть реестр сервисов. Взяв, к примеру, консул, текущую политику ограничения можно сохранить в хранилище ключей/значений консула. С определенной периодичностью (например, каждые 30 с) вызывается интерфейс центра регистрации, и шлюз может воспринимать количество всех экземпляров шлюза в текущем состоянии (при условии n) и динамически корректировать свою текущую политику ограничения на 100/n раз в секунду.
Вопрос. Когда экземпляр шлюза добавляется или аварийно зависает, в приведенной выше реализации будет неточная текущая политика ограничения на короткий период времени (например, 30 секунд). Однако, учитывая, что такая нештатная ситуация случается относительно редко, и это время можно уменьшить, это не проблема, если требования не такие строгие.
Q: Другая проблема заключается в том, что эта реализация зависит от коэффициента распределения запросов на каждом шлюзе. Например, если на nginx настроена переадресация запросов, вес шлюза 1 равен 3, вес шлюза 2 равен 1, а вес шлюза 3 равен 1, то соответственно политику шлюза 1 нужно настроить на ограничение до 60 посещений в секунду, а шлюз 2 и шлюз 3 — 20 раз в секунду. То есть текущая политика ограничения шлюза привязана к конфигурации nginx, что неразумно. Кроме того, если шлюз 3 в это время ненормально зависает, то, как шлюзы 1 и 2 корректируют свои соответствующие текущие политики ограничения, также станет более сложным.
2. Распределенное ограничение тока (функция ограничения тока как отдельная служба RPC)
A: Инкапсулируйте функцию ограничения тока в отдельную службу RPC. Когда шлюз получает запрос, он сначала запрашивает через интерфейс, предоставляемый текущей службой ограничения, и решает, следует ли освободить или отклонить его в соответствии с возвращенным результатом.
В: В этом методе реализации сначала необходимо развернуть службу ограничения тока, что увеличивает затраты на эксплуатацию и обслуживание; кроме того, каждый запрос будет иметь дополнительные сетевые издержки (шлюз обращается к службе ограничения тока), поэтому Узкое место в производительности, скорее всего, возникнет в шлюзе и ограничителе При обмене данными RPC между потоковыми сервисами. Если текущая функция ограничения предоставляет общий http-интерфейс, считается, что производительность не будет идеальной; если она предоставляет интерфейс бинарного протокола (например, бережливость), то шлюзу придется немного переписать код (в конце концов, это разработан на основе Spring Cloud и WebFlux).
В целом, это реализация, которую стоит попробовать. Система ограничения тока Sentinel с открытым исходным кодом от Alibaba реализует как распределенное ограничение тока, так и ограничение тока на основе памяти, что кажется хорошим выбором. (После прочтения общего введения я не стал углубляться)
3. Распределенное ограничение тока на основе redis
О: Используйте однопоточную функцию сценариев Redis и Lua для достижения распределенного ограничения тока. Когда запросы от нескольких шлюзов обращаются к Redis, они выполняются последовательно в Redis, и нет проблемы параллелизма, один запрос будет включать в себя несколько операций Redis.В качестве примера возьмем алгоритм ведра с токенами: получить текущее количество токенов, получить последний get Время токена, время обновления и количество токенов и т. д. могут быть гарантированы атомарностью с помощью сценария lua, а также уменьшить сетевые накладные расходы на шлюз, многократно обращающийся к Redis.
Ключом здесь является lua-скрипт.В версии Spring Cloud.Greenwich spring-cloud-gateway имеет фильтр ограничения тока.Скрипт lua выглядит следующим образом:
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*10)
-- 当前令牌的数量
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
-- 上次取令牌的时间
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
-- 新增令牌 delta*rate,更新令牌数量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
-- 更新 redis 中令牌数量和时间
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return { allowed_num, new_tokens }
В: В реальном тесте нет проблем, если включен только один экземпляр шлюза; если включено несколько экземпляров шлюза, фактическое ограничение тока оказывается неточным. Последняя причина заключается в том, что время нескольких серверов, включенных со шлюзом не синхронизируется.
О: При добавлении токенов в корзину токенов с определенной скоростью используется следующая формула: скорость*(текущее время-время последнего добавления токена), а значение текущего времени передается шлюзом. находится в Время сервера не точное, значит логика этого скрипта неверна. Один способ — всегда обеспечивать синхронизацию времени, что практически невозможно, другой — использовать время сервера redis, то есть поставить 6-ю строчку кодаlocal now = tonumber(ARGV[3])
изменить на:local now = redis.call("time")[1]
.
Уведомление:
существуетДизайн и реализация Redis: сценарии LuaКак упоминалось в: В lua-скриптах нельзя задавать случайные значения. Ниже приводится соответствующий контент:
При копировании сценариев Lua на вспомогательные узлы или при записи сценариев Lua в файлы AOF Redis необходимо решить такую проблему: если сценарий Lua имеет случайные свойства или побочные эффекты, то при запуске сценария на вспомогательном узле или при запуске AOF файл загружен и повторно запущен, он может получить совершенно другие результаты, чем при предыдущем запуске.
Рассмотрим следующий фрагмент кода, где
get_random_number()
Имея случайный характер, мы выполняем этот код на сервере SERVER и сохраняем результат случайного числа в ключnumber
начальство:# 虚构例子,不会真的出现在脚本环境中 redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number OK redis> GET number "10086"
Сейчас еслиEVALКод копируется на подчиненный узел SLAVE, т.к.
get_random_number()
случайный характер , он имеет высокую вероятность генерации суммы10086
совершенно разные значения, такие как65535
:# 虚构例子,不会真的出现在脚本环境中 redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number OK redis> GET number "65535"
Как видите, написание скрипта с рандомом создает серьезную проблему: нарушает согласованность между данными сервера и спутника.
Та же проблема возникает при загрузке скриптов случайного характера из файлов AOF.
Случайность вредна только тогда, когда пишутся сценарии со случайностью.
Случайность безвредна, если скрипт выполняет операции только для чтения. Например, если скрипт просто выполняет
RANDOMKEY
команду, то она безвредна, но если выполнятьRANDOMKEY
После этого на основанииRANDOMKEY
результат операции записи, то скрипт вреден.Подобно случайности, если выполнение сценария зависит от каких-либо побочных эффектов, результат может быть разным при каждом выполнении сценария.
Чтобы решить эту проблему, Redis накладывает строгий предел на скрипты, которые могут быть выполнены в среде LUA - все сценарии должны быть чистыми функциями без побочных эффектов.
С этой целью Redis предпринял ряд соответствующих мер для среды Lua:
- Библиотеки для доступа к состоянию системы (например, библиотека системного времени) не предусмотрены.
- Запрещено использоватьloadfileфункция.
- Если сценарий выполняет случайную команду (например,RANDOMKEY) или команды с побочными эффектами (например,TIME) после попытки выполнить команду записи (например,SET), то Redis предотвратит продолжение сценария и вернет ошибку.
- Если сценарий выполняет случайную команду чтения (например,SMEMBERS), то до того, как выходные данные скрипта будут возвращены в Redis, автоматическилексикографическая сортировка, тем самым обеспечивая упорядоченность выходных результатов.
- Используйте функцию случайной генерации, определенную Redis, для замены среды Lua.
math
таблица оригиналmath.randomфункция иmath.randomseedфункции, новые функции обладают тем свойством, что каждый раз при выполнении Lua-скрипта, если они явно не вызываютсяmath.randomseed
,иначеmath.random
Генерируемая последовательность псевдослучайных чисел всегда одна и та же.После этой серии настроек Redis может гарантировать выполнение скриптов:
- Никаких побочных эффектов.
- Нет вредной случайности.
- Для одних и тех же входных параметров и набора данных всегда генерируется одна и та же команда записи.
Затем я действительно проверил это и обнаружил, что ошибки нет? !
10.201.0.30:6379> eval "local now = redis.call('time')[1]; return redis.call('set', 'time-test', now)" 0
OK
10.201.0.30:6379> get time-test
"1552628054"
Итак, ознакомьтесь с официальной документацией:
Note: starting with Redis 5, the replication method described in this section (scripts effects replication) is the default and does not need to be explicitly enabled.
Starting with Redis 3.2, it is possible to select an alternative replication method. Instead of replication whole scripts, we can just replicate single write commands generated by the script. We call this script effects replication.
In this replication mode, while Lua scripts are executed, Redis collects all the commands executed by the Lua scripting engine that actually modify the dataset. When the script execution finishes, the sequence of commands that the script generated are wrapped into a MULTI / EXEC transaction and are sent to replicas and AOF.
This is useful in several ways depending on the use case:
- When the script is slow to compute, but the effects can be summarized by a few write commands, it is a shame to re-compute the script on the replicas or when reloading the AOF. In this case to replicate just the effect of the script is much better.
- When script effects replication is enabled, the controls about non deterministic functions are disabled. You can, for example, use the TIMEor SRANDMEMBER commands inside your scripts freely at any place.
- The Lua PRNG in this mode is seeded randomly at every call.
In order to enable script effects replication, you need to issue the following Lua command before any write operated by the script:
redis.replicate_commands()
The function returns true if the script effects replication was enabled, otherwise if the function was called after the script already called some write command, it returns false, and normal whole script replication is used.
Проще говоря: начиная с Redis 3.2, метод репликации на основе эффектов был добавлен в репликацию redis master-slave или при записи файлов AOF. Вместо того, чтобы копировать весь скрипт, мы можем просто скопировать единственную команду записи, сгенерированную скриптом, а это значит, что в lua-скрипте можно устанавливать случайные значения, например, системное время. Redis версии 5 и выше использует этот метод репликации по умолчанию.