Если вы используете услуги, которые должны иметь возможность обрабатывать большое количество трафика, вы можете балансировать большую часть этого трафика между вашими серверами.На рынке есть много балансировщиков нагрузки производственного уровня (NGINX, HAProxy и т. Д.), но знать, как они работают за сценой, это хорошее знание.
Simple HTTP Load Balancer in Go Using Standard Library В этой реализации мы будем использовать круглый алгоритм для равномерного распределения поступающих запросов между коллекцией серверов-бак-эндов.
Основная структура
Во-первых, мы должны определить наши основные структуры данных. Наш балансировщик нагрузки будет отслеживать несколько серверов и их состояние:
package main
import (
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"sync/atomic"
"time"
)
// Backend represents a backend server
type Backend struct {
URL *url.URL
Alive bool
mux sync.RWMutex
ReverseProxy *httputil.ReverseProxy
}
// SetAlive updates the alive status of backend
func (b *Backend) SetAlive(alive bool) {
b.mux.Lock()
b.Alive = alive
b.mux.Unlock()
}
// IsAlive returns true when backend is alive
func (b *Backend) IsAlive() (alive bool) {
b.mux.RLock()
alive = b.Alive
b.mux.RUnlock()
return
}
// LoadBalancer represents a load balancer
type LoadBalancer struct {
backends []*Backend
current uint64
}
// NextBackend returns the next available backend to handle the request
func (lb *LoadBalancer) NextBackend() *Backend {
// Simple round-robin
next := atomic.AddUint64(&lb.current, uint64(1)) % uint64(len(lb.backends))
// Find the next available backend
for i := 0; i < len(lb.backends); i++ {
idx := (int(next) + i) % len(lb.backends)
if lb.backends[idx].IsAlive() {
return lb.backends[idx]
}
}
return nil
}
Теперь несколько ключевых моментов, чтобы иметь в виду:
- Backend struct представляет собой единый сервер с URL-адресами и живым статусом.
- Мы используем мутекс для безопасного обновления и проверки живого состояния каждого бак-энда в сопутствующей среде.
- LoadBalancer отслеживает список бак-эндов и поддерживает счетчик для алгоритма круглого рубина.
- Мы используем атомные операции, чтобы безопасно увеличить счетчик в сопутствующей среде.
- Метод NextBackend реализует алгоритм круглого рубина, пропуская нездоровые задние окончания.
Проверка здоровья
Обнаружение недоступности бак-энда является одной из критических функций любого балансера нагрузки.Внедряет очень простой механизм проверки здоровья:
// isBackendAlive checks whether a backend is alive by establishing a TCP connection
func isBackendAlive(u *url.URL) bool {
timeout := 2 * time.Second
conn, err := net.DialTimeout("tcp", u.Host, timeout)
if err != nil {
log.Printf("Site unreachable: %s", err)
return false
}
defer conn.Close()
return true
}
// HealthCheck pings the backends and updates their status
func (lb *LoadBalancer) HealthCheck() {
for _, b := range lb.backends {
status := isBackendAlive(b.URL)
b.SetAlive(status)
if status {
log.Printf("Backend %s is alive", b.URL)
} else {
log.Printf("Backend %s is dead", b.URL)
}
}
}
// HealthCheckPeriodically runs a routine health check every interval
func (lb *LoadBalancer) HealthCheckPeriodically(interval time.Duration) {
t := time.NewTicker(interval)
for {
select {
case <-t.C:
lb.HealthCheck()
}
}
}
Этот медицинский осмотр прост:
- Мы пытаемся инициировать TCP-соединение с бак-эндом.
- Если это соединение удается, то бак-энд жив; в противном случае он мертв.
- Проверка выполняется с интервалом, указанным в функции HealthCheckPeriodically.
В производственной среде вы, вероятно, хотели бы более продвинутую проверку здоровья, которая фактически делает запрос HTTP в определенную конечную точку, но это дает нам начало.
HTTP трейдер
Давайте пойдем дальше и реализуем HTTP-трейдер, который будет получать запрос и направлять их в наши задние контуры:
// ServeHTTP implements the http.Handler interface for the LoadBalancer
func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
backend := lb.NextBackend()
if backend == nil {
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
return
}
// Forward the request to the backend
backend.ReverseProxy.ServeHTTP(w, r)
}
Вот где происходит волшебство:
- Наш круглый алгоритм дает нам следующий доступный бак-энд.
- В случае отсутствия резервных окон, мы возвращаем ошибку 503 Служба недоступна.
- В противном случае мы проксизируем запрос на выбранный бак-энд через внутренний обратный прокси Go.
Обратите внимание, какnet/http/httputil
Пакет предусматриваетReverseProxy
тип, который обрабатывает все сложности прокси-запросов HTTP для нас.
Положить все вместе
Наконец, давайте реализуемmain
Функция для настройки и запуска нашего балансера нагрузки:
func main() {
// Parse command line flags
port := flag.Int("port", 8080, "Port to serve on")
flag.Parse()
// Configure backends
serverList := []string{
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
}
// Create load balancer
lb := LoadBalancer{}
// Initialize backends
for _, serverURL := range serverList {
url, err := url.Parse(serverURL)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Error: %v", err)
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
}
lb.backends = append(lb.backends, &Backend{
URL: url,
Alive: true,
ReverseProxy: proxy,
})
log.Printf("Configured backend: %s", url)
}
// Initial health check
lb.HealthCheck()
// Start periodic health check
go lb.HealthCheckPeriodically(time.Minute)
// Start server
server := http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: &lb,
}
log.Printf("Load Balancer started at :%d\n", *port)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Вот что происходит в главной функции:
- Мы конфигурируем порт на основе флагов командной строки, которые мы анализируем.
- Мы создали список резервных серверов.
- For each backend server, we:
- Parse the URL
- Create a reverse proxy for that backend
- Add error handling for when a backend fails
- Add the backend to our load balancer
- Проводится первоначальный медицинский осмотр.
- Мы начинаем рутину для периодических медицинских осмотров.
- Наконец, мы запускаем HTTP-сервер с нашим балансером нагрузки в качестве манипулятора.
Испытание балансировки нагрузки
Так что, чтобы снова проверить наш балансировщик нагрузки, нам нужны некоторые серверы бак-эндов.
// Save this to backend.go
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := flag.Int("port", 8081, "Port to serve on")
flag.Parse()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
hostname, _ := os.Hostname()
fmt.Fprintf(w, "Backend server on port %d, host: %s, Request path: %s\n", *port, hostname, r.URL.Path)
})
log.Printf("Backend started at :%d\n", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil {
log.Fatal(err)
}
}
Запускайте несколько инстанций этого резервного кода на разных портах:
go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &
Затем постройте и запустите балансировщик нагрузки:
go build -o load-balancer main.go
./load-balancer
Теперь сделайте несколько запросов, чтобы проверить его:
curl http://localhost:8080/test
Сделайте несколько запросов, и вы увидите, как они распределяются по вашим серверам в круглой рубине.
Потенциальные улучшения
Это минимальное выполнение, но есть много вещей, которые вы можете сделать, чтобы улучшить его:
-
Different balancing algorithms: Implement weighted round-robin, least connections, or IP hash-based selection.
-
Better health checking: Make full HTTP requests to a health endpoint instead of just checking TCP connectivity.
-
Metrics collection: Track request counts, response times, and error rates for each backend.
-
Sticky sessions: Ensure requests from the same client always go to the same backend.
-
TLS support: Add HTTPS for secure connections.
-
Dynamic configuration: Allow updating the backend list without restarting.
Мы построили простой, но эффективный балансировщик нагрузки HTTP, используя не что иное, как стандартную библиотеку Go.
Хотя это не готово к производству, это обеспечивает прочную основу для понимания того, как работают балансировщики нагрузки.Комплексное решение составляет около 150 строк кода - свидетельство выразительности Go и силы его стандартной библиотеки.
Для использования в производстве вы хотите построить больше функций и добавить надежное управление ошибками, но основные концепции одинаковы.Понимание этих основных принципов подготовит вас к лучшей конфигурации и устранению любых балансировщиков нагрузки, которые вы будете использовать на дороге.
Здесь вы можете найти исходный кодhttps://github.com/rezmoss/simple-load-balancer