14,587 leituras
14,587 leituras

Este script 150-Line Go é realmente um balanceador de carga completo

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

Muito longo; Para ler

Este artigo mostrará como criar um balanceador de carga HTTP simples no Go, usando apenas a biblioteca padrão. Ele executa distribuição rotativa de robôs em servidores backend, verificação de saúde para detectar falhas e solicitação de proxy – tudo em cerca de 150 linhas de código. Ótimo para aprender os fundamentos da rede no Go
featured image - Este script 150-Line Go é realmente um balanceador de carga completo
Rez Moss HackerNoon profile picture

Se você estiver executando serviços que devem ser capazes de lidar com uma grande quantidade de tráfego, você pode carregar muito desse tráfego entre seus servidores de backend.Existem muitos balançadores de carga de nível de produção no mercado (NGINX, HAProxy, etc.) mas saber como eles funcionam por trás da cena é um bom conhecimento.


A Simple HTTP Load Balancer in Go Using Standard Library, Nesta implementação, usaremos um algoritmo rotativo para distribuir uniformemente as solicitações de entrada entre uma coleção de servidores de backend.

A estrutura básica

Primeiro, precisamos definir nossas estruturas de dados principais. Nosso balançador de carga rastreará vários servidores backend e sua saúde:


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
}


Agora, alguns pontos-chave a ter em mente:

  1. O backend struct representa um único servidor backend com seu URL e status vivo.
  2. Estamos usando um mutex para atualizar com segurança e verificar o status ao vivo de cada backend em um ambiente simultâneo.
  3. O LoadBalancer mantém um registro de uma lista de back-ends e mantém um contador para o algoritmo de round-robin.
  4. Usamos operações atômicas para incrementar com segurança o contador em um ambiente concorrente.
  5. O método NextBackend implementa o algoritmo de round-robin, ignorando os backends não saudáveis.

Verificação de saúde

Detectar a indisponibilidade do backend é uma das funções críticas de qualquer balançador de carga. Implementa um mecanismo de verificação de saúde muito simples:


// 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()
		}
	}
}


Este controle de saúde é simples:

  1. Nós tentamos iniciar uma conexão TCP com o backend.
  2. Se essa conexão for bem-sucedida, então o backend está vivo; caso contrário, está morto.
  3. A verificação é executada em um intervalo especificado na função HealthCheckPeriodically.


Em um ambiente de produção, você provavelmente gostaria de uma verificação de saúde mais avançada que realmente faça uma solicitação HTTP para algum endpoint especificado, mas isso nos dá início.

O HTTP Handler

Vamos seguir em frente e implementar o manipulador HTTP que receberá a solicitação e os encaminhará para nossos backends:


// 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)
}


E é aí que a magia acontece:

  1. Nosso algoritmo de round-robin nos dá o próximo backend disponível.
  2. No caso de nenhum backend estar disponível, devolvemos um erro 503 Serviço indisponível.
  3. Caso contrário, nós proxy a solicitação para o backend selecionado através do proxy reverso interno do Go.


Observe como onet/http/httputilO pacote fornece oReverseProxytipo, que lida com todas as complexidades da proxy de solicitações HTTP para nós.

Colocando tudo em conjunto

Por fim, vamos implementar omainFunção para configurar e iniciar o nosso balançador de carga:


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)
	}
}


Aqui está o que está acontecendo na função principal:

  1. Nós configuramos a porta com base nas bandeiras da linha de comando que parsamos.
  2. Criamos uma lista de servidores de back-end.
  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. Realizamos um primeiro check-up de saúde.
  5. Iniciamos uma goroutina para exames de saúde periódicos.
  6. Finalmente, iniciamos o servidor HTTP com o nosso balanceador de carga como manipulador.

Testando o Balanço de Carga

Então, novamente, para testar nosso balanceador de carga, precisamos de alguns servidores backend.


// 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)
	}
}


Execute várias instâncias deste backend em diferentes portas:


go build -o backend backend.go
./backend -port 8081 &
./backend -port 8082 &
./backend -port 8083 &


Em seguida, crie e execute o balançador de carga:


go build -o load-balancer main.go
./load-balancer


Agora, faça alguns pedidos para testá-lo:


curl http://localhost:8080/test


Faça múltiplos pedidos e você os verá sendo distribuídos em seus servidores de backend de forma rotativa.

Possíveis melhorias

Esta é uma implementação mínima, mas há muitas coisas que você pode fazer para melhorá-la:


  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.


Construímos um balanceador de carga HTTP simples, mas eficaz, usando apenas a biblioteca padrão do Go. Este exemplo ilustra conceitos importantes na programação de rede e o poder dos recursos de rede integrados do Go.


Embora esta não esteja pronta para a produção, fornece uma base sólida para entender como os balanceadores de carga funcionam.A solução completa é de cerca de 150 linhas de código - um testemunho da expressividade do Go e da força de sua biblioteca padrão.


Para uso de produção, você gostaria de construir mais recursos e adicionar gerenciamento de erros robusto, mas os conceitos principais são todos os mesmos.Uma compreensão desses princípios básicos irá prepará-lo para configurar e depurar melhor qualquer balançador de carga que você usará na estrada.


Você pode encontrar o código fonte aquihttps://github.com/rezmoss/simple-load-balancer

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks