
На скрине показано 40 минут графиков с балансировки некоторого эндпоинта. На выделенном участке видно 129.01 рпс успехов и 7.27 ошибок 4xx, которые являлись 429 от рпс-лимитера. Настройка рпс-лимитера находилась на уровне “не более 150 запросов с интервалом в 1 секунду”. Не странно ли видеть такое уверенное и постоянное превышение лимита?
На соседнем эндпоинте в прошлый раз уже замечал, что пуассоновское распределение неплохо справляется с описанием поведения объёма потока в интервал времени (*). Давайте попробуем, используя его же, попробовать объяснить наблюдаемое.
Во-первых, давайте прикинем - какова вероятность что у нас на интервале в 1 секунду будет хотя бы 151 запрос:
def get_prob_at_least(border, lmbd): sum = 1 mult = 1 for i in range(1, border): mult = mult * lmbd / i sum += mult return 1 - sum * math.exp(-lmbd) get_prob_at_least(151, 129.01 + 7.27) 0.1127
То есть - где-то 11%, но это на 1-секундный интервал. На скрине график имеет 15-секундный шаг, что даёт > 1 интервала с превышением в среднем на точке графика. Уже неплохо: нам хотя бы не удивительно что график уверенно показывает положительное число 429.
Давайте попробуем теперь объяснить наблюдаемый рпс ошибок. Посчитаем условное мат-ожидание (**) числа запросов, при условии что у нас их пришло хотя бы 151:
def get_expectation_via_conditional_at_least(border, lmbd): sum_prob = 0 exp_sum = 0 mult = 1 for i in range(1, 10000): mult = mult * lmbd / i if (i >= border): exp_sum += i * mult sum_prob += mult return exp_sum / sum_prob def get_expected_errors_num(events, border): return (get_expectation_via_conditional_at_least(border, events) - border) * get_prob_at_least(border, events)
get_expectation_via_conditional_at_least(151, 129.01 + 7.27) даёт 156.4, а ожидаемое число ошибок выходит всего лишь 0.6 рпс - существенная разница с 7.27, которые мы хотели бы объяснить.
Я стал искать причины смещений. Первое что я нашёл - скорее всего клиентская сторона делает безусловный 1 ретрай. То есть надо исходный рпс событий считать 129.01 + 7.27/2, а результат числа ошибок удваивать. Эта поправка почти ничего не даёт - 0.72 рпс ошибок.
Далее, порядок ошибки и проверка некоторых графиков заставили меня вспомнить что данный сервис рпс-лимитера - это 6-подовый сервис (стандартная история - по 2 пода на 3 дц). Распределённые рпс-лимитеры можно реализовывать по разному, а также можно сильно по разному делать логику раздачи разрешений (Leaky-Bucket, сброс в начале секунды и другие).
Давайте предположим, что рпс-лимитер работает так:
каждый лимитер раз в икс времени узнаёт число живых соседей, и делит разрешённый лимит на найденное число соседей
синхронизации числа не выданных разрешений не происходит
сброс счётчика разрешённых запросов в начале периода
В такой ситуации - у нас 6 независимых точек принятия решения, и формулы выходят такими
limiters_num = 6 retry_num = 2 rps_one_limiter=(129.01 + 7.27 / 2)/limiters_num border_one_limiter=int(150/limiters_num) + 1 one_limiter_erros = get_expected_errors_num(rps_one_limiter, border_one_limiter) limited_num = one_limiter_erros * limiters_num * retry_num
Один лимитер при таких числах имеет ожидаемое число ошибок в 0.57, а общее число ошибок (умножением на 12) получается 6.85.
Кажется, на этом можно успокоиться и считать что поведение (на уровне принципов) объяснено, возможно лимитер действительно примерно предположенным образом и работает.
NOTE: конечно, в этой простой модели многое учтено не идеально. Например, ретрай после первого 429 не понятно в какой под лимитера попадёт, в расчётах выше мы считали будто он попадает в тот же.
В качестве отдельного мини-вывода (в дополнении к тому что теоретические распределения полезны) я бы ещё раз заметил, как сильно дробление (шардирование) увеличивает ожидаемое число превышений - почти линейно.
(*) Имхо, это работает потому, что здесь идёт речь о достаточно чистом консьюмерском трафике (без роботного трафика) - действительно “около бесконечное число монеток” (юзеров) решают в каждый момент времени “воспользоваться ли сервисом”, то есть примерно условия предельного перехода для пуассоновского распределения и получаются.
(**) Даже мне немного неожиданно, как-то редко умо применяется в чисто прикладных историях
