Масштабирование
Распределённый rate limiting, кеширование и защита от stampede, генерация ключей, связь между сервисами и realtime-транспорт.
10 вопросов
JuniorДизайнОчень частоСпроектируйте ограничитель частоты запросов для Go HTTP-сервиса, который работает как несколько одинаковых инстансов за балансировщиком. Он должен ограничивать, как часто каждый клиент (по API-ключу или IP) может звать сервис. Требования:
- Применяемый лимит — на клиента и общий по всему парку: N инстансов не должны каждый разрешать полную квоту, позволяя клиенту слать в N раз больше задуманной частоты.
- Шаг проверки-и-учёта корректен при конкуренции: два одновременных запроса одного клиента не должны оба пройти, когда остаётся лишь один слот (никакой гонки потерянного обновления на общем счётчике).
- Клиент может потратить короткий всплеск до предела, затем ограничивается ровной скоростью пополнения, а не жёстко режется на границе фиксированного окна.
- Отклонённый запрос получает понятный стандартный сигнал, сообщающий клиенту, что его придержали, и примерно когда повторить.
Укажите алгоритм ограничения, состояние на клиента и где оно живёт, чтобы все инстансы делили один лимит.
Спроектируйте ограничитель частоты запросов для Go HTTP-сервиса, который работает как несколько одинаковых инстансов за балансировщиком. Он должен ограничивать, как часто каждый клиент (по API-ключу или IP) может звать сервис. Требования: - Применяемый лимит — на клиента и общий по всему парку: N инстансов не должны каждый разрешать полную квоту, позволяя клиенту слать в N раз больше задуманной частоты. - Шаг проверки-и-учёта корректен при конкуренции: два одновременных запроса одного клиента не должны оба пройти, когда остаётся лишь один слот (никакой гонки потерянного обновления на общем счётчике). - Клиент может потратить короткий всплеск до предела, затем ограничивается ровной скоростью пополнения, а не жёстко режется на границе фиксированного окна. - Отклонённый запрос получает понятный стандартный сигнал, сообщающий клиенту, что его придержали, и примерно когда повторить. Укажите алгоритм ограничения, состояние на клиента и где оно живёт, чтобы все инстансы делили один лимит.
Используйте token bucket на ключ клиента (API-ключ или IP): корзина хранит токены, которые пополняются с фиксированной скоростью до предела, и запрос проходит, только если может забрать токен. Состояние корзины держите в Redis (атомарный Lua-скрипт), чтобы все инстансы делили один лимит; оберните это в middleware, отдающий 429 с заголовком Retry-After, когда корзина пуста.
Типичные ошибки
- ✗Держать счётчик в памяти процесса, из-за чего каждый инстанс применяет свой лимит, а реальный предел в N раз выше
- ✗Делать read-then-write в общем хранилище неатомарно, что гонится при конкуренции и пропускает лишние запросы
- ✗Использовать фиксированное окно вместо корзины, допуская двойной всплеск на границе окна
Уточняющие вопросы
- →Как сделать проверку-и-уменьшение в
Redisатомарной? - →По какому ключу ограничивать и как не наказывать пользователей за общим NAT?
JuniorДизайнОчень частоСпроектируйте сервис-сократитель URL на Go: клиенты присылают длинный URL и получают короткую ссылку, а переход по этой короткой ссылке редиректит браузер на исходный URL. Спроектируйте его так, чтобы он удовлетворял требованиям:
- Каждый сгенерированный короткий ключ уникален — два разных длинных URL никогда не схлопываются в один ключ, даже при множестве конкурентных запросов на создание.
- Ключи остаются короткими (несколько символов) и пригодны для пути вида https://sho.rt/{key}.
- Путь редиректа — горячий и должен быть быстрым: поиск ключа и возврат исходного URL должны быть дешёвыми и не требовать сканирования.
- Браузер, заходящий по существующей короткой ссылке, реально переводится на длинный URL (настоящий HTTP-редирект), а неизвестный ключ возвращает понятный not-found.
Опишите, как вы генерируете ключ (и почему он не может коллизить), где и как храните отображение key → длинный URL и как ведёт себя эндпоинт редиректа для известных и неизвестных ключей.
Спроектируйте сервис-сократитель URL на Go: клиенты присылают длинный URL и получают короткую ссылку, а переход по этой короткой ссылке редиректит браузер на исходный URL. Спроектируйте его так, чтобы он удовлетворял требованиям:
- Каждый сгенерированный короткий ключ уникален — два разных длинных URL никогда не схлопываются в один ключ, даже при множестве конкурентных запросов на создание.
- Ключи остаются короткими (несколько символов) и пригодны для пути вида https://sho.rt/{key}.
- Путь редиректа — горячий и должен быть быстрым: поиск ключа и возврат исходного URL должны быть дешёвыми и не требовать сканирования.
- Браузер, заходящий по существующей короткой ссылке, реально переводится на длинный URL (настоящий HTTP-редирект), а неизвестный ключ возвращает понятный not-found.
Опишите, как вы генерируете ключ (и почему он не может коллизить), где и как храните отображение key → длинный URL и как ведёт себя эндпоинт редиректа для известных и неизвестных ключей.
Генерируйте уникальный ключ, кодируя в base62 монотонный id (последовательность БД или id в стиле Snowflake) — это гарантирует отсутствие коллизий и короткие ключи. Храните ключ → длинный URL в таблице с индексом по key, по желанию с кэшем в Redis. Редиректы отдавайте из GET /{key} через 301/302, с веткой not-found для неизвестных ключей.
Типичные ошибки
- ✗Хешировать URL и обрезать — это коллизит и тихо отображает два разных URL на один ключ
- ✗Случайные ключи с check-then-insert, который гонится, и два запроса берут один ключ под нагрузкой
- ✗Возвращать цель в теле
200вместо HTTP-редиректа, из-за чего браузер не переходит реально
Уточняющие вопросы
- →Почему кодирование последовательности избавляет от ретраев на коллизиях, нужных случайным ключам?
- →Выберете
301или302и как этот выбор влияет на аналитику и кэширование?
JuniorДизайнЧастоСпроектируйте Go HTTP-обработчик, возвращающий значение, которое считает forecast() — вызов длится около секунды. Обработчик обслуживает горячий эндпоинт примерно на 10k запросов в секунду, поэтому он не должен звать forecast() на пути запроса. Требования:
- Запрос обязан отвечать быстро — читает кешированное значение, никогда не зовя медленную функцию встроенно.
- Кешированное значение обновляется в фоне, чтобы оставаться достаточно свежим по мере изменения входа со временем.
- Конкурентные чтения должны быть безопасны и дёшевы, ведь чтений намного больше, чем записей.
- При остановке процесса фоновое обновление должно завершаться чисто.
Укажите, где живёт кешированное значение, как координируются чтения и обновление и как избежать многократного пересчёта одного значения разом (cache stampede).
Спроектируйте Go HTTP-обработчик, возвращающий значение, которое считает forecast() — вызов длится около секунды. Обработчик обслуживает горячий эндпоинт примерно на 10k запросов в секунду, поэтому он не должен звать forecast() на пути запроса. Требования:
- Запрос обязан отвечать быстро — читает кешированное значение, никогда не зовя медленную функцию встроенно.
- Кешированное значение обновляется в фоне, чтобы оставаться достаточно свежим по мере изменения входа со временем.
- Конкурентные чтения должны быть безопасны и дёшевы, ведь чтений намного больше, чем записей.
- При остановке процесса фоновое обновление должно завершаться чисто.
Укажите, где живёт кешированное значение, как координируются чтения и обновление и как избежать многократного пересчёта одного значения разом (cache stampede).
Кешируйте результат в общей переменной под RWMutex. Одна фоновая goroutine пересчитывает его по тикеру и пишет под Lock; обработчик читает под RLock, так что медленный forecast() никогда не блокирует запрос. Запускайте goroutine от context, чтобы остановка её чисто отменяла.
Типичные ошибки
- ✗Звать 1-секундный
forecast()на пути запроса, из-за чего каждый запрос платит полную задержку - ✗Читать и писать кешированное значение без синхронизации, что является гонкой под нагрузкой
- ✗Позволять многим запросам пересчитывать одно устаревшее значение разом вместо одного фонового обновляющего
Уточняющие вопросы
- →Как
RWMutexпускает много читателей, всё же сериализуя писателя? - →Если кеш с ключом по городу, как избежать stampede, когда несколько ключей протухают вместе?
JuniorТеорияЧастоЧто такое WebSocket и чем он отличается от обычного HTTP-запроса?
Что такое WebSocket и чем он отличается от обычного HTTP-запроса?
WebSocket — это постоянное полнодуплексное TCP-соединение между клиентом и сервером, открытое апгрейдом исходного HTTP-запроса через заголовок Upgrade: websocket. В отличие от модели запрос/ответ HTTP — где клиент должен спросить, прежде чем сервер ответит — любая сторона может слать сообщения в любой момент по одному открытому соединению, что подходит для живых лент и чата.
Типичные ошибки
- ✗Думать, что WebSocket — это просто быстрый HTTP-опрос, а не одно постоянное соединение
- ✗Считать, что слать может только сервер — WebSocket полнодуплексный, шлют обе стороны
- ✗Забывать, что соединение начинается как HTTP-запрос, апгрейженный через заголовок
Upgrade
Уточняющие вопросы
- →Что обменивает HTTP-рукопожатие
Upgrade, чтобы переключить соединение на WebSocket? - →Чем WebSocket отличается от Server-Sent Events?
MiddleТеорияЧастоСинхронный RPC, очередь сообщений или long-polling — как выбрать способ межсервисного взаимодействия?
Синхронный RPC, очередь сообщений или long-polling — как выбрать способ межсервисного взаимодействия?
Синхронный RPC (HTTP/gRPC) подходит для запрос-ответа, когда вызывающему нужен результат немедленно и тесная связанность допустима. Очередь сообщений (Kafka/RabbitMQ) разъединяет сервисы, гасит всплески и даёт повторы, но согласованность отложенная. Long-polling доставляет серверные события простым HTTP-клиентам.
Типичные ошибки
- ✗Считать очередь сообщений низколатентным синхронным вызовом, а не асинхронным, отложенно согласованным каналом
- ✗Считать gRPC асинхронным и разъединяющим только потому, что он использует потоки HTTP/2
- ✗Путать long-polling с полнодуплексным соединением WebSocket
Уточняющие вопросы
- →Когда стоит поставить очередь перед синхронным эндпоинтом и как работает back-pressure?
- →Как паттерн outbox держит согласованными запись в БД и публикацию в очередь?
MiddleТеорияЧастоКакие есть распространённые алгоритмы rate limiting и чем отличаются token bucket и sliding window?
Какие есть распространённые алгоритмы rate limiting и чем отличаются token bucket и sliding window?
Fixed window считает запросы за интервал по часам — просто, но допускает двойной всплеск на границе. Sliding window взвешивает предыдущее окно, сглаживая этот край. Leaky bucket сливает очередь с постоянной скоростью, формируя выход. Token bucket пополняет токены с ровной скоростью до предела, разрешая ограниченный всплеск, затем ровную скорость — обычный выбор по умолчанию.
Типичные ошибки
- ✗Путать token bucket (допускает всплеск) со счётчиком fixed window (жёсткий предел)
- ✗Думать, что fixed window предотвращает двойной всплеск на границе
- ✗Менять местами роли leaky bucket и token bucket насчёт всплесков
Уточняющие вопросы
- →Почему fixed window позволяет клиенту слать почти двойной лимит вблизи момента сброса?
- →Как реализовать token bucket атомарно в
Redisсразу для многих инстансов?
MiddleТеорияИногдаКак работает long polling и чем он отличается от обычного HTTP-запроса и от WebSockets?
Как работает long polling и чем он отличается от обычного HTTP-запроса и от WebSockets?
При long polling клиент шлёт HTTP-запрос, который сервер держит открытым, пока не появятся данные или не сработает таймаут, затем отвечает; клиент тут же открывает новый запрос. В отличие от обычного запроса он не возвращается сразу, а в отличие от WebSocket остаётся однонаправленным запрос-ответом с HTTP-накладными на каждое сообщение.
Типичные ошибки
- ✗Путать long polling с коротким опросом по фиксированному таймеру
- ✗Считать удержанный запрос полнодуплексным, как WebSocket
- ✗Отождествлять long polling с server-push из HTTP/2
Уточняющие вопросы
- →Возможен ли long polling поверх HTTP/3 и что меняется в удержанном соединении?
- →Когда для односторонних потоков выбрать Server-Sent Events вместо long polling?
MiddleТеорияИногдаКаковы компромиссы применения rate limit на клиенте против сервера?
Каковы компромиссы применения rate limit на клиенте против сервера?
Лимит на клиенте (вызывающий сам себя придерживает) экономит трафик и защищает внешний сторонний API, которым вы не управляете, но кривой или злонамеренный клиент может его проигнорировать — он лишь рекомендательный. Лимит на сервере авторитетен и защищает сервис от любого клиента, но всё равно тратит ресурсы на приём и отклонение каждого запроса. Реальные системы обычно делают и то, и другое.
Типичные ошибки
- ✗Считать лимит на клиенте авторитетным, а не рекомендательным
- ✗Считать, что серверное отклонение ничего не стоит, хотя трафик всё равно приходит
- ✗Думать, что лимит достаточно применять только на одной стороне
Уточняющие вопросы
- →Как защитить сторонний API с лимитом, когда ваши собственные клиенты ведут себя некорректно?
- →Почему серверный лимит всё равно тратит ресурсы даже на отклонённом запросе?
MiddleТеорияИногдаКогда выбирать WebSocket вместо стандарта серверной отправки Server-Sent Events (SSE) или long-polling?
Когда выбирать WebSocket вместо стандарта серверной отправки Server-Sent Events (SSE) или long-polling?
Выбирайте WebSocket для низколатентного двунаправленного трафика — чат, мультиплеер, живой трейдинг — где клиент тоже часто шлёт. Server-Sent Events подходят для однонаправленных потоков сервер→клиент (уведомления, дашборды): они идут по обычному HTTP с авто-переподключением, поэтому проще и дружелюбны к прокси. Long-polling — запасной вариант, когда ни то ни другое не годится, ценой накладных на повторные запросы.
Типичные ошибки
- ✗Хвататься за WebSocket для односторонних уведомлений, где SSE проще и дружелюбнее к прокси
- ✗Считать SSE двунаправленным — он только сервер→клиент
- ✗Выбирать транспорт по языку, а не по форме и направлению трафика
Уточняющие вопросы
- →Почему SSE легче переживает прокси и балансировщики, чем WebSocket?
- →Как каждый вариант ведёт себя, когда соединение рвётся и нужно переподключиться?
SeniorДизайнРедкоGo-сервис в контейнере с 1 ГБ RAM следит за входящим файлом из строк по 8 символов, разделённых переводом строки, сортирует строки и пишет отсортированный результат в новый файл. Теперь он должен сортировать файлы до 2 ГБ — больше всего бюджета памяти, — а RAM увеличить нельзя. Диска много. Как отсортировать файл, не помещающийся в память? Опишите фазы, что ограничивает пиковую память при слиянии, и назовите алгоритм.
Go-сервис в контейнере с 1 ГБ RAM следит за входящим файлом из строк по 8 символов, разделённых переводом строки, сортирует строки и пишет отсортированный результат в новый файл. Теперь он должен сортировать файлы до 2 ГБ — больше всего бюджета памяти, — а RAM увеличить нельзя. Диска много. Как отсортировать файл, не помещающийся в память? Опишите фазы, что ограничивает пиковую память при слиянии, и назовите алгоритм.
External merge sort (внешняя сортировка слиянием). Фаза 1: читать файл кусками, каждый помещается в RAM, сортировать кусок в памяти и писать как отсортированный run-файл. Фаза 2: k-путевое слияние run-ов, читая из каждого лишь небольшой буфер за раз и многократно выдавая наименьшую текущую строку — пиковая память ограничена числом run-ов плюс размером буферов, а не размером файла. Результат стримится в выходной файл.
Типичные ошибки
- ✗Пытаться загрузить весь файл и надеяться, что GC или уплотнение его вместят
- ✗Считать, что
mmapснимает ограничение памяти, а не просто откладывает подкачку - ✗Забывать, что слияние читает в память ограниченные буферы, а не целые run-ы
Уточняющие вопросы
- →Как размер куска балансирует число run-ов против памяти на run?
- →Почему k-путевое слияние с min-heap лучше повторных двупутевых при многих run-ах?