14,874 чтения
14,874 чтения

Этот сценарий 150-Line Go на самом деле является полноценным балансировщиком нагрузки

к Rez Moss7m2025/04/23
Read on Terminal Reader
Read this story w/o Javascript

Слишком долго; Читать

Эта статья покажет вам, как создать простой балансировщик нагрузки HTTP в Go, используя только стандартную библиотеку. Он выполняет круглое распределение по серверам backend, проверку состояния здоровья, чтобы заметить сбои, и запрос прокси — все в около 150 строк кода.
featured image - Этот сценарий 150-Line Go на самом деле является полноценным балансировщиком нагрузки
Rez Moss HackerNoon profile picture

Если вы используете услуги, которые должны иметь возможность обрабатывать большое количество трафика, вы можете балансировать большую часть этого трафика между вашими серверами.На рынке есть много балансировщиков нагрузки производственного уровня (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
}


Теперь несколько ключевых моментов, чтобы иметь в виду:

  1. Backend struct представляет собой единый сервер с URL-адресами и живым статусом.
  2. Мы используем мутекс для безопасного обновления и проверки живого состояния каждого бак-энда в сопутствующей среде.
  3. LoadBalancer отслеживает список бак-эндов и поддерживает счетчик для алгоритма круглого рубина.
  4. Мы используем атомные операции, чтобы безопасно увеличить счетчик в сопутствующей среде.
  5. Метод 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()
		}
	}
}


Этот медицинский осмотр прост:

  1. Мы пытаемся инициировать TCP-соединение с бак-эндом.
  2. Если это соединение удается, то бак-энд жив; в противном случае он мертв.
  3. Проверка выполняется с интервалом, указанным в функции 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)
}


Вот где происходит волшебство:

  1. Наш круглый алгоритм дает нам следующий доступный бак-энд.
  2. В случае отсутствия резервных окон, мы возвращаем ошибку 503 Служба недоступна.
  3. В противном случае мы проксизируем запрос на выбранный бак-энд через внутренний обратный прокси 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)
	}
}


Вот что происходит в главной функции:

  1. Мы конфигурируем порт на основе флагов командной строки, которые мы анализируем.
  2. Мы создали список резервных серверов.
  3. 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
  4. Проводится первоначальный медицинский осмотр.
  5. Мы начинаем рутину для периодических медицинских осмотров.
  6. Наконец, мы запускаем 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


Сделайте несколько запросов, и вы увидите, как они распределяются по вашим серверам в круглой рубине.

Потенциальные улучшения

Это минимальное выполнение, но есть много вещей, которые вы можете сделать, чтобы улучшить его:


  1. Different balancing algorithms: Implement weighted round-robin, least connections, or IP hash-based selection.

  2. Better health checking: Make full HTTP requests to a health endpoint instead of just checking TCP connectivity.

  3. Metrics collection: Track request counts, response times, and error rates for each backend.

  4. Sticky sessions: Ensure requests from the same client always go to the same backend.

  5. TLS support: Add HTTPS for secure connections.

  6. Dynamic configuration: Allow updating the backend list without restarting.


Мы построили простой, но эффективный балансировщик нагрузки HTTP, используя не что иное, как стандартную библиотеку Go.


Хотя это не готово к производству, это обеспечивает прочную основу для понимания того, как работают балансировщики нагрузки.Комплексное решение составляет около 150 строк кода - свидетельство выразительности Go и силы его стандартной библиотеки.


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


Здесь вы можете найти исходный кодhttps://github.com/rezmoss/simple-load-balancer

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks