Масштабирование
Пока сервис крутится в одном процессе, многое прощается — счётчик в памяти, синхронный вызов соседа, кеш без защиты. Масштабирование начинается там, где экземпляров становится много: состояние в памяти больше не общее, нагрузка приходит всплесками, а клиент хочет получать обновления в реальном времени, а не дёргать сервер опросом. Здесь решает не алгоритм на одном узле, а то, как этот алгоритм ведёт себя, когда узлов сто.
Главная ловушка темы — проектировать под один процесс, а разворачивать на флот. Локальный счётчик rate limiter масштабируется числом экземпляров — это не погрешность, а кратное превышение лимита. Наивный кеш, у которого ключ протух у всех сразу, отправляет весь трафик в базу одновременно — cache stampede. Синхронный вызов медленного соседа превращает его задержку в вашу. А выбор realtime-транспорта (опрос, long polling, SSE, WebSocket) определяет, платите ли вы трафиком вхолостую или держите постоянное соединение. Эта тема разбирает масштабирование по слоям — от глобального лимита до полнодуплексного канала.
Карта темы
- Распределённый rate limiter — общее состояние на весь флот и атомарная проверка, чтобы лимит был глобальным, а не на каждый экземпляр.
- Алгоритмы rate limiting — fixed/sliding window, token bucket и leaky bucket и их свойства на границах интервалов.
- Стратегии кэширования — значение под
RWMutexс фоновым обновлением и защита от cache stampede. - Генерация коротких ключей —
base62от монотонного id даёт уникальные короткие ключи без проверки коллизий. - Связь между сервисами — синхронный RPC против очереди сообщений против long-polling и их компромиссы.
- Long polling и realtime-транспорт — опрос, long polling, SSE и WebSocket по направлению обмена и сценарию.
- WebSocket — постоянное полнодуплексное соединение через
Upgradeисходного HTTP-запроса.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Держать счётчик rate limiter в памяти процесса | Лимит множится на число экземпляров — кратное превышение |
| Считать рассинхрон локальных счётчиков «небольшой погрешностью» | На флоте это кратное превышение лимита, не округление |
| Игнорировать поведение алгоритма на границе окна | Fixed window пропускает двойной всплеск на стыке двух интервалов |
| Не защищать кеш от одновременного протухания ключа | Cache stampede — весь трафик уходит в базу разом и кладёт её |
| Проверять коллизии при генерации ключа из уникального id | Лишняя работа — base62 от уникального id уже уникален |
| Держать один генератор id на весь кластер | Узкое место контеншена — нужен распределённый монотонный id |
| Вызывать медленного соседа синхронно в горячем пути | Его задержка становится вашей задержкой и вашим отказом |
Возвращать пустой 200 вместо 404 на отсутствующий ключ | Клиент не отличает «нет данных» от «всё хорошо, пусто» |
| Чаще опрашивать сервер, чтобы снизить задержку realtime | Платишь дважды — трафик вхолостую и всё равно задержка до интервала |
Считать Sec-WebSocket-Key аутентификацией | Это проверка протокола, а не авторизация — путаница в модели безопасности |
Значение для собеседований
Масштабирование — обязательная тема на senior-уровне Go-интервью, и спрашивают не алгоритм на одном узле, а его поведение на флоте. Интервьюер проверяет, держите ли вы в голове, что состояние в памяти процесса перестаёт быть общим, когда экземпляров много.
Что обычно проверяют:
- Почему rate limiter обязан хранить состояние централизованно и проверять лимит атомарно, чтобы он был глобальным.
- Чем отличаются fixed window, sliding window, token bucket и leaky bucket — особенно на границах интервалов.
- Что такое cache stampede и как фоновое обновление под
RWMutexего предотвращает. - Почему
base62от монотонного id даёт короткий уникальный ключ без проверки коллизий, и где узкое место генератора. - Как выбирать между синхронным RPC, очередью и long-polling — по связанности, задержке и устойчивости к сбою.
- Чем опрос, long polling, SSE и WebSocket отличаются по направлению обмена и когда какой уместен.
Типичный неверный ответ: «поставим счётчик запросов в память каждого сервиса — этого хватит». Это запускает разбор того, что локальный счётчик масштабируется числом экземпляров и кратно превышает общий лимит, а глобальный лимит требует общего состояния (например, в Redis) с атомарной проверкой-инкрементом.