> Как спроектировать систему url shortener на уровне микросервисов (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Black Wall Group (BWG)
Стек: Go
> Пример ответа
Короткий ответ
Проектирование url shortener на микросервисах сводится к разделению на три ключевых сервиса: API gateway (приём запросов), core service (генерация и хранение коротких кодов) и analytics service (сбор статистики переходов). Хранение - распределённая key-value база (например, Redis для кэша + Cassandra или DynamoDB для персистентности). Генерация кодов - либо счётчик с base62, либо случайные строки с проверкой коллизий. Для надёжности - репликация, шардирование по ключу, eventual consistency для аналитики.
Подробное объяснение
Архитектура микросервисов для url shortener должна решать три задачи: создание короткой ссылки, редирект по ней и сбор аналитики. Каждая задача - отдельный сервис со своей базой и масштабированием.
Core service - главный сервис. Принимает длинный URL, генерирует короткий код, сохраняет пару в хранилище. Код генерируется двумя способами:
- счётчик (например, Snowflake ID или глобальный sequence) с последующим переводом в base62 - детерминированно, без коллизий, но требует координации между инстансами;
- случайная строка фиксированной длины (например, 7 символов из алфавита [a-zA-Z0-9]) - проще, но нужна проверка на коллизии и повторная генерация при конфликте.
Для хранения используется key-value хранилище с ключом = короткий код, значением = длинный URL. Redis - для горячих данных (TTL), Cassandra/DynamoDB - для персистентного слоя. Шардирование - по хэшу короткого кода (consistent hashing).
Redirect service - принимает GET-запрос с коротким кодом, ищет длинный URL в кэше, при промахе - в базе, возвращает 302 Redirect. Для повышения производительности - локальный кэш (LRU) на каждом инстансе + Redis как общий кэш второго уровня.
Analytics service - асинхронно получает события о переходах (через message queue, например Kafka или NATS), агрегирует по времени, IP, User-Agent. Данные хранятся в columnar базе (ClickHouse) или time-series базе (InfluxDB). Не критичен к задержкам, поэтому eventual consistency.
API gateway - единая точка входа. Маршрутизирует запросы: POST /shorten → core service, GET /:code → redirect service, GET /stats/:code → analytics service. Также выполняет rate limiting, аутентификацию (если нужна), валидацию входных данных.
Межсервисное взаимодействие - синхронное (gRPC или REST) для core и redirect, асинхронное (event-driven) для analytics. Для обнаружения сервисов - Consul или etcd, для балансировки - Envoy или NGINX.
Масштабирование - каждый сервис горизонтально масштабируется независимо. Core service - stateless, вся координация в базе. Redirect service - stateless, масштабируется за счёт кэширования. Analytics - масштабируется за счёт партиционирования очереди.
Надёжность - репликация базы (Raft для Cassandra, multi-AZ для DynamoDB), circuit breaker для синхронных вызовов, retry с exponential backoff для асинхронных.
На практике
Начинаем с монолита, разбиваем на микросервисы только когда появляется реальная нагрузка. Для senior-позиции важно показать понимание trade-off: микросервисы добавляют сложность в деплой, observability и согласованность данных, поэтому оправданы только при определённом масштабе.
Практические решения:
- генерация кодов через случайные строки с проверкой в Redis (SETNX) - проще, чем глобальный счётчик;
- для редиректа - сначала локальный кэш, потом Redis, потом база. Промах в кэше - редкий случай, поэтому можно блокировать запрос на время чтения из базы;
- аналитика - не блокирует основной поток, пишем событие в Kafka и забываем. Если Kafka недоступен - пишем в локальный файл и отправляем позже (outbox pattern);
- rate limiting на gateway - token bucket на IP или API key.
Примерная схема деплоя: Kubernetes, каждый сервис - отдельный deployment, HPA по CPU и RPS. Базы - managed (AWS RDS для реляционной части, ElastiCache для Redis, DynamoDB для основного хранилища).
Пример кода
Генерация короткого кода на Go:
GOpackage mainimport ("crypto/rand""encoding/base64")const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"func generateCode(length int) (string, error) {bytes := make([]byte, length)if _, err := rand.Read(bytes); err != nil {return "", err}for i, b := range bytes {bytes[i] = alphabet[int(b)%len(alphabet)]}return string(bytes), nil}
Редирект с кэшем:
GOfunc (s *Server) Redirect(w http.ResponseWriter, r *http.Request) {code := r.URL.Path[1:]// локальный кэшif url, ok := s.localCache.Get(code); ok {http.Redirect(w, r, url, http.StatusFound)return}// redisurl, err := s.redis.Get(r.Context(), code).Result()if err == nil {s.localCache.Set(code, url, time.Hour)http.Redirect(w, r, url, http.StatusFound)return}// базаurl, err = s.db.GetLongURL(r.Context(), code)if err != nil {http.NotFound(w, r)return}s.redis.Set(r.Context(), code, url, time.Hour)s.localCache.Set(code, url, time.Hour)http.Redirect(w, r, url, http.StatusFound)}
Как отвечать на собеседовании
Начни с верхнеуровневой архитектуры: три сервиса + gateway. Затем уточни детали по мере вопросов. Обязательно проговори:
- как генерируешь код и почему выбрал этот способ;
- как хранишь данные и как шардируешь;
- как обеспечиваешь консистентность между кэшем и базой;
- как обрабатываешь пиковые нагрузки на редирект;
- как обрабатываешь недоступность аналитики.
Покажи, что понимаешь trade-off: микросервисы - это не серебряная пуля, для маленького сервиса монолит проще. Если интервьюер спрашивает про конкретные технологии - называй те, с которыми работал, и объясняй почему.
Что проверяет интервьюер
- Понимание декомпозиции системы на сервисы с чёткими границами ответственности;
- умение обосновать выбор хранилища и стратегии кэширования;
- знание паттернов асинхронного взаимодействия (event-driven, message queue);
- способность проектировать под нагрузку: шардирование, репликация, горизонтальное масштабирование;
- понимание компромиссов между консистентностью и доступностью (CAP theorem применительно к конкретным сервисам);
- практический опыт: как деплоить, мониторить, обрабатывать отказы.
Типичные ошибки
- предлагать единую монолитную базу для всех сервисов - это убивает смысл микросервисов;
- использовать глобальный auto-increment ID в распределённой системе без координации - будет узкое место;
- игнорировать кэширование для редиректа - это самый горячий путь, без кэша не выдержит нагрузку;
- синхронно вызывать analytics при каждом переходе - это заблокирует редирект;
- не продумывать обработку коллизий при генерации кодов;
- забывать про rate limiting и защиту от злоупотреблений (например, спам-генерация ссылок);
- не учитывать, что короткие коды должны быть устойчивы к угадыванию (если ссылки приватные) - случайные строки лучше счётчика.
> Похожие задачи по Go
Что нужно помнить при добавлении элементов в слайс Go без знания точного размера
Можно ли шарить данные между потоками
Как уведомить горутины в Go о необходимости завершения при получении первого результата
Используешь ли багтрекинговые системы
> Похожие задачи по backend
Что нужно помнить при добавлении элементов в слайс Go без знания точного размера
Можно ли шарить данные между потоками
Как уведомить горутины в Go о необходимости завершения при получении первого результата
Используешь ли багтрекинговые системы
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью