Какие существуют стратегии балансировки нагрузки и как реализовать простейший балансировщик на Go?

Ответ

Балансировка нагрузки — это процесс распределения входящего трафика между несколькими серверами (нодами) для обеспечения отказоустойчивости и высокой производительности.

Основные стратегии:

  1. Round Robin (Циклический) — запросы распределяются по серверам по очереди. Самый простой метод, но не учитывает текущую нагрузку на серверы.
  2. Least Connections (Наименьшее количество соединений) — новый запрос отправляется на сервер с наименьшим количеством активных соединений. Более интеллектуальный подход, чем Round Robin.
  3. IP Hash — сервер для клиента выбирается на основе хеша его IP-адреса. Это гарантирует, что запросы от одного клиента всегда будут попадать на один и тот же сервер, что полезно для управления сессиями.
  4. Weighted Round Robin (Взвешенный циклический) — аналог Round Robin, но серверам назначаются веса в зависимости от их мощности. Более мощные серверы получают больше запросов.

В Go можно легко реализовать свой балансировщик. Вот пример потокобезопасного Round Robin балансировщика:

import (
    "net/http"
    "net/http/httputil"
    "sync"
)

type RoundRobinBalancer struct {
    sync.Mutex
    servers []string
    current int
}

// GetServer выбирает следующий сервер для обработки запроса
func (b *RoundRobinBalancer) GetServer() string {
    b.Lock()
    defer b.Unlock()
    server := b.servers[b.current]
    b.current = (b.current + 1) % len(b.servers)
    return server
}

// Пример использования с ReverseProxy
func main() {
    servers := []string{"http://localhost:8081", "http://localhost:8082"}
    balancer := &RoundRobinBalancer{servers: servers}

    director := func(req *http.Request) {
        // Выбираем следующий сервер
        server := balancer.GetServer()
        req.URL.Scheme = "http"
        req.URL.Host = server
    }

    proxy := &httputil.ReverseProxy{Director: director}
    http.ListenAndServe(":8080", proxy)
}

Ключевым аспектом является мониторинг состояния (health checks) серверов, чтобы автоматически исключать неработающие ноды из пула и возвращать их после восстановления.

Ответ 18+ 🔞

Да ты посмотри, блядь, на эту всю хуйню с балансировкой! Сидят сервера, как сука, три толстяка на диване, а на них трафик, как из ведра, льётся. Надо ж это добро как-то по-человечески поделить, чтобы один не сдох, пока другие пальцы в жопе крутят.

Вот тебе, сука, основные закидоны, как это можно провернуть:

  1. По кругу (Round Robin) — взял да по очереди каждому в рот запихал запрос. Раз-два-три, опять раз-два-три. Просто, как три копейки, но если один сервер уже еле дышит, а другой прохлаждается, ему всё равно — он продолжает по списку тыкать. Тупость, блядь, но иногда работает.

  2. Кому легче (Least Connections) — тут уже умнее, ёпта. Смотрит, у кого меньше всего клиентов на шее висит, и нового бедолагу этому счастливчику и подсовывает. Логично же, блядь!

  3. По роже клиента (IP Hash) — смотри, какая хитрая жопа! Берёт IP адрес клиента, мешает его в каком-то своём ебучем алгоритме, и получается, что один и тот же юзер всегда будет к одному и тому же серверу ходить. Удобно, если у тебя там сессии, куки, всякая хуйня, которую таскать между серверами — овердохуища мороки.

  4. По кругу, но с жирными (Weighted Round Robin) — а это когда у тебя есть сервер-монстр с 128 ядрами и хлипкая мартышлюшка на двух гигабайтах оперативки. Первому даёшь вес 5, второй — 1. И вот могучий получает пять запросов подряд, а слабак — один, и все довольны. Справедливость, блядь, в действии!

А на Go, ёпта, это вообще как два пальца обоссать. Смотри, вот тебе потокобезопасный крутильник запросов, чтоб его в ротберунчик:

import (
    "net/http"
    "net/http/httputil"
    "sync"
)

type RoundRobinBalancer struct {
    sync.Mutex
    servers []string
    current int
}

// GetServer выбирает следующий сервер для обработки запроса
func (b *RoundRobinBalancer) GetServer() string {
    b.Lock()
    defer b.Unlock()
    server := b.servers[b.current]
    b.current = (b.current + 1) % len(b.servers)
    return server
}

// Пример использования с ReverseProxy
func main() {
    servers := []string{"http://localhost:8081", "http://localhost:8082"}
    balancer := &RoundRobinBalancer{servers: servers}

    director := func(req *http.Request) {
        // Выбираем следующий сервер
        server := balancer.GetServer()
        req.URL.Scheme = "http"
        req.URL.Host = server
    }

    proxy := &httputil.ReverseProxy{Director: director}
    http.ListenAndServe(":8080", proxy)
}

Но самое главное, блядь, что все эти пляски — нихуя не стоят, если ты не следишь, живы ли твои подопечные! Надо постоянно тыкать их палкой, делать health checks. Упал сервер — выкидывай его из списка, чтоб не трафик в чёрную дыру слать. Очнулся — принимай обратно в стадо. Без этого — пидарас шерстяной, а не балансировщик.