itmo_conspects

Разработка нагруженных систем

Заметка 1. Введение

Нагруженная система (High-load system) - это система, в которой нагрузка (число пользователей, запросов, объём данных или другое) настолько велика, что одного сервера или простой архитектуры уже недостаточно

Нагрузка бывает разной:

Для системы, отвечающей на внешние запросы, выделяют такие характеристики:

Основные проблемы высоконагруженных систем являются:

Заметка 2. Ограничение нагрузки

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

Ограничения нужны, чтобы:

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


Здесь применяют такие подходы:

Для ограничения частоты популярны 4 алгоритма:

  1. Фиксированное окно (Fixed window)
  2. Скользящее окно (Sliding window)
  3. Маркерная корзина (Token bucket)
  4. Протекающее ведро (Leaky bucket)

Алгоритм фиксированного окна

Время делится на окна - непересекающиеся интервалы по $t$ секунд, в каждом окне ведется подсчет запросов. Если число запросов превысило порог $N$ в этом окне, то следующие запросы отклоняются

Пример реализации фиксированного окна на языке Kotlin:

class FixedWindowLimiter(
    private val limit: Int,    // N, лимит на окно
    private val windowMs: Long // t, длительность окна
) {
    private var windowStart = System.currentTimeMillis()
    private var count = 0

    @Synchronized
    fun allow(): Boolean {
        val now = System.currentTimeMillis()

        // окно закончилось, начинаем новое
        if (now - windowStart >= windowMs) {
            windowStart = now
            count = 0
        }

        if (count < limit) {
            count++
            return true
        }
        return false
    }
}

Достоинства такого алгоритма заключаются в том, что он очень прост, не требует много памяти, и легко реализуется в базах данных типа NoSQL (например, в Redis)

Недостаток состоит в том, что на границе переходов окон может произойти всплеск запросов, например, 100 запросов в последнюю секунду первого окна и 100 запросов в первую следующего, из-за чего получится 200 запросов за 2 секунды, тогда как порог - 100 запросов

Алгоритм скользящего окна

В такой версии окно зафиксировано относительно текущего момента времени, то есть проверяется, не перешел ли счетчик через порог за последние $t$ секунд

У скользящего окна есть два варианта подсчета запросов за промежуток времени:

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

Алгоритм маркерной корзины

Суть алгоритма: есть “корзина”, которая вмещает $N$ маркеров (или токенов). Каждый запрос забирает некоторое число маркеров, но при этом со временем в корзину добавляется некоторое число маркеров с указанной скоростью $v$. Если в корзине нет свободных маркеров для нового запроса, то он отклоняется

Пример реализации на Kotlin:

class TokenBucket(
    private val ratePerSec: Double,   // v, скорость пополнения маркеров
    private val capacity: Double      // N, максимум маркеров в ведре
) {
    private var tokens = capacity
    private var lastRefillNs = System.nanoTime()

    @Synchronized
    fun allow(cost: Double = 1.0): Boolean {
        refill()
        if (tokens >= cost) {
            tokens -= cost
            return true
        }
        return false
    }

    // пополняем ведро пропорционально прошедшему времени
    private fun refill() {
        val now = System.nanoTime()
        val elapsedSec = (now - lastRefillNs) / 1_000_000_000.0
        tokens = minOf(capacity, tokens + elapsedSec * ratePerSec)
        lastRefillNs = now
    }
}

Свойства такого алгоритма:

Алгоритм протекающего ведра

Суть алгоритма: есть “ведро”, которое заполняется запросами; у этого ведра есть вместимость $N$ - если новый запрос не вмещается в ведро, то он отклоняется. Запросы со временем “утекают” из ведра с фиксированной скоростью $v$

У протекающего ведра есть два варианта реализации:

Свойства такого алгоритма:

В отличие от маркерной корзины протекающее ведро как очередь сглаживает выходной поток запросов