14,656 讀數
14,656 讀數

这个150行走脚本实际上是一个完整的负载平衡器

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

太長; 讀書

本文将向您展示如何在Go中创建一个简单的HTTP负载平衡器,仅使用标准库。它在后端服务器上执行圆轮分布、健康检查以检测故障,并在约150行代码中进行请求代理。
featured image - 这个150行走脚本实际上是一个完整的负载平衡器
Rez Moss HackerNoon profile picture

如果您正在运行服务,应该能够处理大量的流量,您可以在您的后端服务器之间加载大量的流量平衡。


简单的HTTP负载平衡器使用标准库,在这个实现中,我们将使用一个圆形机器人算法,以平衡地在一组后端服务器之间分配进来的请求。

基本结构

首先,我们需要定义我们的核心数据结构,我们的负载平衡器将跟踪多个后端服务器及其健康状况:


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. 后端结构代表一个单一的后端服务器,其URL和活状态。
  2. 我们正在使用一个Mutex来安全地更新和检查每个后端在同步环境中的活跃状态。
  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 的标准库,这个例子说明了网络编程中的重要概念和 Go 内置的网络功能的强大。


虽然这不是生产准备的,但它为了解负载平衡器如何工作提供了坚实的基础. 完整的解决方案是约150行代码 - 证明了Go的表达力和其标准库的强度。


对于生产用途,你想构建更多的功能并添加强大的错误处理,但核心概念都是相同的。


你可以在这里找到源代码。https://github.com/rezmoss/simple-load-balancer

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks