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(','));
Если карта активно перемещается, сервер получает множество одинаковых запросов.
Большинство перегрузок происходит не из-за атак, а из-за ошибок реализации:
Пример ошибочной реализации:
map.on('move', () => {
loadData();
});
Событие move вызывается десятки раз в секунду.
Злоумышленник может написать скрипт:
setInterval(() => {
fetch('/api/search?q=test');
}, 10);
Такой код создаёт сотни запросов в секунду.
Многие сервисы Mapbox тарифицируются по количеству запросов.
Без ограничений возможно:
Клиентское ограничение не является полноценной защитой, поскольку его можно обойти, однако оно существенно снижает нагрузку.
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 мс после прекращения ввода.
Вместо события move лучше использовать задержку.
const loadVisibleData = debounce(() => {
fetchData();
}, 300);
map.on('move', loadVisibleData);
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)
);
Функция будет запускаться не чаще одного раза в секунду.
Во многих случаях отдельное ограничение вообще не требуется.
Вместо:
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();
}
Теперь старые запросы будут автоматически отменяться.
Основная защита должна находиться на сервере.
Клиент нельзя считать доверенной стороной.
Наиболее распространённый подход.
Пример логики:
100 запросов за 1 минуту
от одного IP-адреса
При превышении лимита сервер отвечает:
HTTP 429 Too Many Requests
Для авторизованных систем лучше использовать идентификатор пользователя.
Пример:
user_1456:
200 запросов в минуту
user_2210:
200 запросов в минуту
Такой подход корректно работает даже при использовании общего IP.
Подходит для публичных API.
API KEY:
abcd1234
Лимит:
5000 запросов в час
Простейшая схема.
Пример:
Окно:
1 минута
Лимит:
100 запросов
После окончания минуты счётчик обнуляется.
Недостаток:
100 запросов в 12:00:59
+
100 запросов в 12:01:00
Фактически получается 200 запросов за несколько секунд.
Использует плавающее окно времени.
Последние 60 секунд
Подсчёт ведётся непрерывно.
Преимущества:
Один из самых популярных алгоритмов.
Принцип работы:
Ведро:
100 токенов
Стоимость запроса:
1 токен
Каждый запрос расходует токен.
Токены постепенно восстанавливаются.
Например:
Пополнение:
10 токенов в секунду
Пользователь может выполнять короткие всплески активности, не нарушая ограничений.
Запросы помещаются в очередь.
Входящий поток
|
V
Очередь
|
V
Постоянная скорость обработки
Алгоритм сглаживает резкие пики нагрузки.
Пример с 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 могут иметь различные ограничения.
Многие запросы повторяются.
Например:
/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);
Клиент обязан корректно реагировать на ограничения.
Проверка ответа:
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);
}
Для анализа эффективности защиты полезно собирать метрики:
Типичная схема выглядит следующим образом:
Mapbox GL JS
|
V
Backend API
|
V
Rate Limiter
|
+----> Логи
|
+----> Метрики
|
+----> Мониторинг
Предпочтительно:
map.on('moveend', loadData);
Вместо:
map.on('move', loadData);
const search = debounce(searchPlace, 400);
Это существенно уменьшает число обращений к Geocoding API.
controller.abort();
Позволяет не расходовать ресурсы на результаты, которые уже не нужны.
Обязательные элементы защиты:
Особенно эффективно для:
Наиболее надёжная архитектура включает несколько уровней:
Mapbox GL JS
|
+--> Debounce
|
+--> Throttle
|
+--> AbortController
|
+--> Client Cache
|
V
Backend API
|
+--> Rate Limiter
|
+--> Redis Cache
|
+--> Monitoring
|
V
Mapbox Services
Подобная многоуровневая схема обеспечивает устойчивую работу картографического приложения, предотвращает злоупотребления, снижает затраты на использование внешних API и сохраняет высокую производительность интерфейса даже при значительной пользовательской активности.