🚀 用 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)
}
}
🎯 测试效果
- 启动三个后端服务(例如写简单的 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
- 启动负载均衡器:
bash
go run main.go
- 测试访问:
bash
curl http://localhost:8080/
多次请求,你会发现它会轮流返回:
Hello from backend 8081
Hello from backend 8082
Hello from backend 8083
✅ 总结
在这篇文章中,我们:
- 理解了负载均衡的概念和常见策略
- 用 Go 实现了一个支持轮询调度的 简易 HTTP 负载均衡器
- 加入了 健康检查,确保下线的后端不会被调度
- 测试了多台后端的请求分发
虽然这个实现还很基础,但它已经展示了负载均衡的核心思想。如果要进一步增强,可以考虑:
- 加权轮询(不同服务器分配不同权重)
- 会话保持(同一用户请求始终打到同一后端)
- TLS 支持
- Prometheus 监控指标
这样,一个小巧的 Go 版 mini Nginx 就完成了 🎉。