用 Go 从零实现一个简易负载均衡器

🚀 用 Go 从零实现一个简易负载均衡器

在日常开发中,我们经常会遇到这样的场景:前端用户请求数量太多,而一台后端服务器已经撑不住了。这个时候,**负载均衡(Load Balancing)**就派上用场了。

本文将带你一步步用 Go 实现一个 简易的 HTTP 负载均衡器,支持轮询调度,把请求转发到多台后端服务器,就像一个 mini 版的 Nginx/HAProxy。


🔎 什么是负载均衡?

负载均衡的目标是:把用户的请求合理地分配到多台后端服务器,从而避免某一台服务器过载,提高系统整体性能和可用性。

常见的策略有:

  • 轮询 (Round Robin):请求依次分发到不同服务器,像轮流排队。
  • 随机 (Random):每次随机挑选一台服务器。
  • 最少连接数 (Least Connections):优先选择当前连接最少的服务器。

在实际生产中,Nginx、HAProxy 等软件会结合更多特性(健康检查、权重、会话保持等)。

但我们今天的目标是写一个 能跑起来的简易版本


🛠️ 基础架构

我们要实现的是:

复制代码
Client -----> Go 负载均衡器 -----> 后端服务器池
  • 客户端:发 HTTP 请求到负载均衡器
  • 负载均衡器:接收请求,挑选一台后端,转发请求
  • 后端服务器:返回响应

💻 核心代码

下面给出一个简易的 HTTP 轮询负载均衡器

1. 定义负载均衡器结构

go 复制代码
package main

import (
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"net/http/httputil"
	"net/url"
	"sync"
	"time"
)

// 后端服务器结构
type Backend struct {
	URL          *url.URL
	ReverseProxy *httputil.ReverseProxy
	Alive        bool
	mu           sync.RWMutex
}

// 设置服务器状态
func (b *Backend) SetAlive(alive bool) {
	b.mu.Lock()
	b.Alive = alive
	b.mu.Unlock()
}

// 获取服务器状态
func (b *Backend) IsAlive() bool {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.Alive
}

// 负载均衡器
type LoadBalancer struct {
	backends []*Backend
	counter  int
	mu       sync.Mutex
}

2. 轮询调度算法

go 复制代码
// 选择下一个可用的后端(轮询)
func (lb *LoadBalancer) NextBackend() *Backend {
	lb.mu.Lock()
	defer lb.mu.Unlock()

	// 简单轮询
	for i := 0; i < len(lb.backends); i++ {
		b := lb.backends[lb.counter%len(lb.backends)]
		lb.counter++
		if b.IsAlive() {
			return b
		}
	}
	return nil
}

3. 健康检查(确保后端存活)

go 复制代码
// 定期检查后端是否存活
func (lb *LoadBalancer) HealthCheck() {
	for {
		for _, b := range lb.backends {
			resp, err := http.Head(b.URL.String())
			if err != nil || resp.StatusCode >= 500 {
				b.SetAlive(false)
				fmt.Printf("❌ 后端 %s 下线\n", b.URL)
			} else {
				b.SetAlive(true)
				fmt.Printf("✅ 后端 %s 存活\n", b.URL)
			}
		}
		time.Sleep(5 * time.Second)
	}
}

4. 请求处理(转发到后端)

go 复制代码
func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	backend := lb.NextBackend()
	if backend != nil {
		backend.ReverseProxy.ServeHTTP(w, r)
	} else {
		http.Error(w, "Service not available", http.StatusServiceUnavailable)
	}
}

5. 主函数

go 复制代码
func main() {
	// 后端服务器池
	backends := []*Backend{}
	servers := []string{
		"http://localhost:8081",
		"http://localhost:8082",
		"http://localhost:8083",
	}

	// 初始化反向代理
	for _, addr := range servers {
		serverURL, _ := url.Parse(addr)
		proxy := httputil.NewSingleHostReverseProxy(serverURL)
		backends = append(backends, &Backend{
			URL:          serverURL,
			ReverseProxy: proxy,
			Alive:        true,
		})
	}

	// 创建负载均衡器
	lb := &LoadBalancer{backends: backends}

	// 启动健康检查
	go lb.HealthCheck()

	// 启动负载均衡器
	fmt.Println("🚀 负载均衡器启动,监听 :8080")
	if err := http.ListenAndServe(":8080", lb); err != nil {
		log.Fatal(err)
	}
}

🎯 测试效果

  1. 启动三个后端服务(例如写简单的 HTTP 服务器返回不同响应):
go 复制代码
// backend.go
package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	port := os.Args[1]
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from backend %s\n", port)
	})
	http.ListenAndServe(":"+port, nil)
}

运行:

bash 复制代码
go run backend.go 8081
go run backend.go 8082
go run backend.go 8083
  1. 启动负载均衡器:
bash 复制代码
go run main.go
  1. 测试访问:
bash 复制代码
curl http://localhost:8080/

多次请求,你会发现它会轮流返回:

复制代码
Hello from backend 8081
Hello from backend 8082
Hello from backend 8083

✅ 总结

在这篇文章中,我们:

  • 理解了负载均衡的概念和常见策略
  • 用 Go 实现了一个支持轮询调度的 简易 HTTP 负载均衡器
  • 加入了 健康检查,确保下线的后端不会被调度
  • 测试了多台后端的请求分发

虽然这个实现还很基础,但它已经展示了负载均衡的核心思想。如果要进一步增强,可以考虑:

  • 加权轮询(不同服务器分配不同权重)
  • 会话保持(同一用户请求始终打到同一后端)
  • TLS 支持
  • Prometheus 监控指标

这样,一个小巧的 Go 版 mini Nginx 就完成了 🎉。