N инстансов сервиса обслуживают поток запросов через общий интерфейс Node.
Нужен балансировщик, который сам реализует Node и раскидывает запросы по
инстансам. Две стратегии: по кругу и «наименее загруженный».
type Req any
type Resp any
type Node interface {
Invoke(ctx context.Context, req Req) (Resp, error)
}
var ErrNoNodes = errors.New("no nodes")
func NewRoundRobin(nodes []Node) *RoundRobin // *RoundRobin реализует Node
func NewLeastLoaded(nodes []Node) *LeastLoaded // *LeastLoaded реализует Node
В проде узлы строятся из адресов (NewNode("host:port")), здесь балансировщик
получает уже готовые Node.
Требования
- Балансировщик вызывает
Invoke ровно одного узла с теми же ctx и req и
возвращает его ответ и ошибку как есть. Пустой список узлов —
ErrNoNodes.
- Round robin: запросы идут на узлы по кругу
0, 1, …, N-1, 0, ….
При конкурентных вызовах каждый узел получает ровно свою долю: 3000
запросов на 3 узла — ровно по 1000.
- Least loaded: запрос уходит на узел с наименьшим числом запросов,
которые сейчас в работе (вызваны, но ещё не вернулись). При равенстве — на
узел с меньшим индексом. Запрос, завершившийся ошибкой, тоже перестаёт
считаться активным.
- Выбор узла и учёт нагрузки корректны при любом числе конкурентных вызовов:
если 100 медленных запросов идут одновременно на 4 узла, нагрузка узлов
отличается не больше чем на 1. Узлов немного (десятки), перебор допустим.
- Код проходит
go test -race.
Пример
lb := NewRoundRobin([]Node{a, b, c})
lb.Invoke(ctx, r1) // → a
lb.Invoke(ctx, r2) // → b
lb.Invoke(ctx, r3) // → c
lb.Invoke(ctx, r4) // → a
Устно: инстансы ненадёжны — как не слать запросы на мёртвый узел
(health-check, пассивное исключение по ошибкам, повтор на другом узле)? Когда
least loaded лучше round robin? Как выбирать узел за O(log N), если узлов
тысячи?
Разбор и эталонное решение — после своей попытки: на собеседовании подсказок не будет.
Сначала попробуй сам — подсказки не дают готовый ответ.