Rate limiting защита

Назначение механизма Rate Limiting

Rate Limiting — это механизм ограничения количества запросов, которые клиент может отправлять за определённый промежуток времени. В контексте приложений, построенных на Mapbox GL JS, данная технология играет важную роль в обеспечении стабильности работы картографического сервиса, защите инфраструктуры от злоупотреблений и контроле потребления API-ресурсов.

При работе с интерактивными картами пользователь может выполнять множество действий:

  • перемещать карту;
  • изменять масштаб;
  • выполнять поиск объектов;
  • запрашивать маршруты;
  • загружать геоданные;
  • получать информацию о точках интереса.

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


Какие запросы требуют защиты

В приложениях на Mapbox GL JS можно выделить несколько категорий запросов.

Запросы к тайлам карты

Во время перемещения карты библиотека автоматически загружает новые тайлы:

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [37.6176, 55.7558],
    zoom: 10
});

При быстром перемещении карты количество запросов может резко возрастать.


Геокодирование

Поиск адресов обычно выполняется через Geocoding API:

async function searchPlace(query) {
    const response = await fetch(
        `https://api.mapbox.com/geocoding/v5/mapbox.places/${query}.json?access_token=${MAPBOX_TOKEN}`
    );

    return response.json();
}

Каждый вводимый символ способен инициировать новый запрос.


Построение маршрутов

Directions API также является чувствительным к частоте обращений сервисом.

async function getRoute(start, end) {
    const response = await fetch(
        `https://api.mapbox.com/directions/v5/mapbox/driving/${start};${end}?access_token=${MAPBOX_TOKEN}`
    );

    return response.json();
}

При автоматическом пересчёте маршрута на каждом изменении координат число запросов быстро увеличивается.


Запросы к собственному серверу

Часто приложение использует собственный Backend.

Например:

fetch('/api/points?bbox=' + bounds.toArray().join(','));

Если карта активно перемещается, сервер получает множество одинаковых запросов.


Причины внедрения Rate Limiting

Защита от случайных перегрузок

Большинство перегрузок происходит не из-за атак, а из-за ошибок реализации:

  • отсутствие дебаунса;
  • циклические запросы;
  • некорректные обработчики событий;
  • бесконечные обновления данных.

Пример ошибочной реализации:

map.on('move', () => {
    loadData();
});

Событие move вызывается десятки раз в секунду.


Защита от автоматизированных ботов

Злоумышленник может написать скрипт:

setInterval(() => {
    fetch('/api/search?q=test');
}, 10);

Такой код создаёт сотни запросов в секунду.


Контроль расходов

Многие сервисы Mapbox тарифицируются по количеству запросов.

Без ограничений возможно:

  • быстрое исчерпание месячного лимита;
  • рост расходов;
  • временная блокировка доступа.

Клиентское ограничение запросов

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

Debounce

Debounce позволяет выполнять запрос только после завершения серии действий.

Пример:

function debounce(fn, delay) {
    let timeout;

    return (...args) => {
        clearTimeout(timeout);

        timeout = setTimeout(() => {
            fn(...args);
        }, delay);
    };
}

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

const search = debounce(async (query) => {
    const result = await searchPlace(query);
    console.log(result);
}, 500);

Теперь запрос будет выполнен только через 500 мс после прекращения ввода.


Debounce для карты

Вместо события move лучше использовать задержку.

const loadVisibleData = debounce(() => {
    fetchData();
}, 300);

map.on('move', loadVisibleData);

Throttle

Throttle ограничивает максимальную частоту выполнения функции.

Реализация:

function throttle(fn, delay) {
    let lastExecution = 0;

    return (...args) => {
        const now = Date.now();

        if (now - lastExecution >= delay) {
            lastExecution = now;
            fn(...args);
        }
    };
}

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

map.on(
    'move',
    throttle(() => {
        updateStatistics();
    }, 1000)
);

Функция будет запускаться не чаще одного раза в секунду.


Использование события moveend

Во многих случаях отдельное ограничение вообще не требуется.

Вместо:

map.on('move', loadData);

Предпочтительно:

map.on('moveend', loadData);

Событие возникает только после завершения перемещения карты.

Это один из самых эффективных способов уменьшения количества запросов.


Отмена предыдущих запросов

Частая проблема — накопление устаревших запросов.

Например, пользователь быстро перемещает карту:

map.on('moveend', loadData);

Каждое новое перемещение создаёт новый запрос.

Для решения применяется AbortController.

let controller;

async function loadData() {
    if (controller) {
        controller.abort();
    }

    controller = new AbortController();

    const response = await fetch('/api/data', {
        signal: controller.signal
    });

    return response.json();
}

Теперь старые запросы будут автоматически отменяться.


Серверный Rate Limiting

Основная защита должна находиться на сервере.

Клиент нельзя считать доверенной стороной.


Ограничение по IP

Наиболее распространённый подход.

Пример логики:

100 запросов за 1 минуту
от одного IP-адреса

При превышении лимита сервер отвечает:

HTTP 429 Too Many Requests

Ограничение по пользователю

Для авторизованных систем лучше использовать идентификатор пользователя.

Пример:

user_1456:
200 запросов в минуту

user_2210:
200 запросов в минуту

Такой подход корректно работает даже при использовании общего IP.


Ограничение по API-ключу

Подходит для публичных API.

API KEY:
abcd1234

Лимит:
5000 запросов в час

Алгоритмы Rate Limiting

Fixed Window

Простейшая схема.

Пример:

Окно:
1 минута

Лимит:
100 запросов

После окончания минуты счётчик обнуляется.

Недостаток:

100 запросов в 12:00:59
+
100 запросов в 12:01:00

Фактически получается 200 запросов за несколько секунд.


Sliding Window

Использует плавающее окно времени.

Последние 60 секунд

Подсчёт ведётся непрерывно.

Преимущества:

  • более точный контроль;
  • отсутствие резких скачков нагрузки;
  • предсказуемое поведение.

Token Bucket

Один из самых популярных алгоритмов.

Принцип работы:

Ведро:
100 токенов

Стоимость запроса:
1 токен

Каждый запрос расходует токен.

Токены постепенно восстанавливаются.

Например:

Пополнение:
10 токенов в секунду

Пользователь может выполнять короткие всплески активности, не нарушая ограничений.


Leaky Bucket

Запросы помещаются в очередь.

Входящий поток
      |
      V
   Очередь
      |
      V
Постоянная скорость обработки

Алгоритм сглаживает резкие пики нагрузки.


Реализация ограничения на Node.js

Пример с Express:

import express fr om 'express';
import rateLimit from 'express-rate-lim it';

const app = express();

const limiter = rateLimit({
    windowMs: 60 * 1000,
    max: 100
});

app.use(limiter);

app.listen(3000);

Все маршруты будут ограничены сотней запросов в минуту.


Отдельный лимит для картографических данных

app.use(
    '/api/map-data',
    rateLimit({
        windowMs: 60 * 1000,
        max: 30
    })
);

Разные API могут иметь различные ограничения.


Кэширование как дополнение к Rate Limiting

Многие запросы повторяются.

Например:

/api/places?id=15

Если результат не изменяется, сервер может вернуть данные из кэша.

Преимущества:

  • уменьшение нагрузки;
  • снижение задержек;
  • сокращение числа обращений к базе данных.

Кэширование данных карты

Для объектов, зависящих от области просмотра:

const cache = new Map();

Ключом может выступать область карты:

const key = bounds.toArray().join(',');

Перед загрузкой выполняется проверка:

if (cache.has(key)) {
    return cache.get(key);
}

После получения результата:

cache.set(key, data);

Обработка ошибки 429

Клиент обязан корректно реагировать на ограничения.

Проверка ответа:

const response = await fetch(url);

if (response.status === 429) {
    console.log('Слишком много запросов');
}

Повторная попытка

Некоторые серверы возвращают заголовок:

Retry-After: 60

Пример обработки:

if (response.status === 429) {
    const retryAfter =
        Number(response.headers.get('Retry-After')) * 1000;

    setTimeout(loadData, retryAfter);
}

Мониторинг частоты запросов

Для анализа эффективности защиты полезно собирать метрики:

  • количество запросов в секунду;
  • число ответов 429;
  • среднее время ответа;
  • количество отменённых запросов;
  • число уникальных пользователей;
  • нагрузку по маршрутам API.

Типичная схема выглядит следующим образом:

Mapbox GL JS
      |
      V
 Backend API
      |
      V
 Rate Limiter
      |
      +----> Логи
      |
      +----> Метрики
      |
      +----> Мониторинг

Практические рекомендации для приложений на Mapbox GL JS

Загрузка данных после завершения перемещения карты

Предпочтительно:

map.on('moveend', loadData);

Вместо:

map.on('move', loadData);

Использование Debounce для поиска

const search = debounce(searchPlace, 400);

Это существенно уменьшает число обращений к Geocoding API.


Отмена устаревших запросов

controller.abort();

Позволяет не расходовать ресурсы на результаты, которые уже не нужны.


Серверное ограничение запросов

Обязательные элементы защиты:

  • лимиты по IP;
  • лимиты по пользователям;
  • лимиты по API-ключам;
  • возврат кода 429;
  • журналирование превышений.

Кэширование часто используемых данных

Особенно эффективно для:

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

Комбинированная стратегия защиты

Наиболее надёжная архитектура включает несколько уровней:

Mapbox GL JS
      |
      +--> Debounce
      |
      +--> Throttle
      |
      +--> AbortController
      |
      +--> Client Cache
      |
      V
Backend API
      |
      +--> Rate Limiter
      |
      +--> Redis Cache
      |
      +--> Monitoring
      |
      V
Mapbox Services

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