> Как масштабировать сервис коротких ссылок и настроить балансировку нагрузки (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: amoCRM
Стек: Go
> Пример ответа
Короткий ответ
Масштабирование сервиса коротких ссылок сводится к горизонтальному масштабированию stateless API-слоя и масштабированию storage. Балансировка нагрузки - через L4/L7 балансировщик (nginx, HAProxy, ALB) с least-connections или round-robin. Критично: генерация коротких кодов должна быть распределённой (snowflake, base62 от ID, или pre-generated пулы), чтобы избежать гонок на запись. Для чтения - кэш (Redis) перед БД, для записи - шардирование по хэшу кода или диапазону ID. Сессии и sticky sessions не нужны - сервис должен быть полностью stateless.
Подробное объяснение
Основная проблема сервиса коротких ссылок - это не столько объём данных, сколько соотношение read/write. Обычно чтение на порядки превышает запись (переходы по ссылкам). Поэтому архитектура строится вокруг кэширования и горизонтального масштабирования read-пути.
Ключевые компоненты:
-
API-слой (Go-сервисы): полностью stateless, хранят только конфигурацию и подключения к внешним системам. Масштабируются простым добавлением инстансов за балансировщиком.
-
Балансировка нагрузки:
- L7 (HTTP) - если нужно маршрутизировать по пути или заголовкам, терминировать TLS.
- L4 (TCP) - если нужна минимальная задержка и нет потребности в HTTP-логике.
- Алгоритм: least-connections предпочтительнее round-robin, так как запросы могут быть неравномерными по стоимости (redirect дешевле, чем создание ссылки).
-
Генерация коротких кодов - самое узкое место при масштабировании записи:
- Вариант A: централизованный ID-генератор (snowflake, Twitter-style) - выдаёт уникальные 64-битные ID, которые затем кодируются в base62. Плюс: нет коллизий, просто шардировать. Минус: зависимость от генератора.
- Вариант B: pre-generated пулы кодов - сервис заранее генерирует миллионы кодов и хранит их в Redis/БД как "свободные". При создании ссылки берёт код из пула. Плюс: нет гонок на генерацию. Минус: нужно пополнять пул.
- Вариант C: хэш от URL + проверка коллизий - плохо для масштабирования, так как требует проверки в БД при каждой записи.
-
Хранение:
- Для чтения: Redis-кэш с TTL (например, 24 часа) перед PostgreSQL/MySQL. При попадании в кэш - отдаём redirect без обращения к БД.
- Для записи: шардирование по хэшу короткого кода (consistent hashing) или по диапазону ID. Шардирование по коду проще, так как код известен при чтении.
- Для аналитики (количество переходов) - отдельный поток, не блокирующий основной read-путь (асинхронная запись в ClickHouse или Kafka + consumer).
-
Особенности Go:
- Использовать
net/httpс настройкойReadTimeout,WriteTimeout,MaxHeaderBytes. - Пул соединений к Redis/БД - настраивать
MaxOpenConns,MaxIdleConns. - Graceful shutdown для обновлений без потери трафика.
- Использовать
На практике
Реальный сценарий для senior-позиции: сервис обрабатывает 10k RPS на чтение и 100 RPS на запись. База - PostgreSQL, кэш - Redis.
-
Ставим 3-5 инстансов Go-сервиса за nginx (least_connections). Каждый инстанс подключается к Redis-кластеру (3 мастера + реплики) и PostgreSQL (шардирована на 4 шарда по хэшу кода).
-
При GET /{code}: сначала Redis GET code → если есть, 302 redirect. Если нет - запрос в нужный шард PostgreSQL, запись в Redis с TTL, ответ.
-
При POST /: генерируем код через snowflake-подобный генератор (или берём из пула в Redis), пишем в шард, отвечаем.
-
Для аналитики переходов - асинхронно отправляем событие в Kafka (или просто инкрементим счётчик в Redis с периодическим сбросом в БД).
-
Мониторинг: latency p99, hit rate кэша, количество ошибок 5xx, saturation CPU/connections. Автоскейлинг по CPU или RPS.
Пример кода
GO// Основной обработчик redirect с кэшемfunc (s *Service) redirectHandler(w http.ResponseWriter, r *http.Request) {code := strings.TrimPrefix(r.URL.Path, "/")if code == "" {http.Error(w, "empty code", http.StatusBadRequest)return}// 1. Пытаемся получить из кэшаurl, err := s.cache.Get(r.Context(), code)if err == nil {http.Redirect(w, r, url, http.StatusFound)s.metrics.RedirectCacheHit()return}// 2. Кэш промахнулся - идём в шардированную БДshard := s.shardForCode(code) // consistent hashurl, err = s.db[shard].GetURL(r.Context(), code)if err != nil {http.Error(w, "not found", http.StatusNotFound)s.metrics.RedirectMiss()return}// 3. Пишем в кэш с TTLs.cache.Set(r.Context(), code, url, 24*time.Hour)http.Redirect(w, r, url, http.StatusFound)}// Генерация кода через snowflake-подобный генераторfunc (s *Service) createShortURL(r *http.Request) {var req CreateRequestif err := json.NewDecoder(r.Body).Decode(&req); err != nil {http.Error(w, "bad request", http.StatusBadRequest)return}id := s.idGen.NextID() // 64-bit уникальный IDcode := base62.Encode(id) // например, "aB3dE9"shard := s.shardForCode(code)if err := s.db[shard].InsertURL(r.Context(), code, req.URL); err != nil {http.Error(w, "internal", http.StatusInternalServerError)return}w.WriteHeader(http.StatusCreated)json.NewEncoder(w).Encode(map[string]string{"short_url": "https://s.example/" + code})}
Как отвечать на собеседовании
Начните с главного: разделите read и write пути. Скажите, что сервис должен быть stateless, а балансировка - на уровне L7 с least-connections. Затем переходите к генерации кодов - это ключевая проблема масштабирования записи. Упомяните snowflake или pre-generated пулы. Потом расскажите про кэширование и шардирование. Если спросят про конкретные цифры - прикиньте RPS и latency. В конце упомяните мониторинг и автоскейлинг.
Что проверяет интервьюер
- Понимание разницы между масштабированием чтения и записи.
- Знание паттернов распределённой генерации уникальных ID.
- Умение проектировать кэш-слой и выбирать стратегию инвалидации.
- Понимание trade-off между consistency и availability (например, что делать при недоступности Redis).
- Практический опыт с балансировщиками и настройкой Go-сервисов под нагрузку.
Типичные ошибки
- Предложение использовать автоинкремент в БД для генерации кодов - не масштабируется на запись.
- Игнорирование кэша - без него read-путь упирается в БД.
- Использование sticky sessions - сервис должен быть stateless, иначе теряется смысл горизонтального масштабирования.
- Отсутствие graceful degradation: если Redis упал, сервис должен продолжать работать через БД (с повышенной latency), а не падать целиком.
- Неучёт аналитики переходов: если писать каждый переход синхронно в БД, write-путь забьётся.
> Похожие задачи по Go
Как происходит балансировка нагрузки в планировщике Go
Что такое starvation локальной очереди и когда она возникает
Подходит ли слайс в Go для хранения и частого увеличения массива событий
Как организовать мьютексы для многопоточной работы с данными по разным пользователям без блокировки всего стореджа
> Похожие задачи по backend
Что такое starvation локальной очереди и когда она возникает
Что происходит, если по userId пользователь не найден?
Подходит ли слайс в Go для хранения и частого увеличения массива событий
Как организовать мьютексы для многопоточной работы с данными по разным пользователям без блокировки всего стореджа
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью