使用 Go 构建简单的负载均衡器

2025-05-28

使用 Go 构建简单的负载均衡器

负载均衡器在现代软件开发中至关重要。如果您想知道请求如何在多台服务器上分配,或者为什么某些网站即使在流量高峰时也能感觉更快,答案通常在于高效的负载均衡。

不使用负载均衡器

在本文中,我们将使用Go 语言的Round Robin 算法构建一个简单的应用程序负载均衡器。本文旨在逐步讲解负载均衡器的内部工作原理。

什么是负载均衡器?

负载均衡器是一种将传入的网络流量分配到多台服务器的系统。它确保单个服务器不会承担过高的负载,从而避免瓶颈并提升整体用户体验。负载均衡方法还能确保如果一台服务器发生故障,流量可以自动重新路由到另一台可用的服务器,从而减少故障的影响并提高可用性。

我们为什么要使用负载均衡器?

  • 高可用性:通过分配流量,负载均衡器确保即使一台服务器发生故障,流量也可以路由到其他健康的服务器,从而使应用程序更具弹性。
  • 可扩展性:随着流量的增加,负载平衡器允许您通过添加更多服务器来水平扩展系统。
  • 效率:通过确保所有服务器平等分担工作负载来最大限度地提高资源利用率。

负载均衡算法

有不同的算法和策略来分配流量:

  • 循环:最简单的方法之一。它将请求按顺序分发到可用的服务器。一旦到达最后一个服务器,就从头开始。
  • 加权循环:类似于循环算法,但每个服务器都被分配了固定的数值权重。该权重用于确定路由流量的服务器。
  • 最少连接:将流量路由到具有最少活动连接的服务器。
  • IP 哈希:根据客户端的 IP 地址选择服务器。

在这篇文章中,我们将重点介绍如何实现循环负载均衡器。

什么是循环算法?

循环算法会循环地将每个传入请求发送到下一个可用的服务器。如果服务器 A 处理了第一个请求,则服务器 B 将处理第二个请求,服务器 C 将处理第三个请求。所有服务器都收到请求后,将从服务器 A 重新开始。

现在,让我们进入代码并构建我们的负载均衡器!

步骤 1:定义负载均衡器和服务器

type LoadBalancer struct {
    Current int
    Mutex   sync.Mutex
}
Enter fullscreen mode Exit fullscreen mode

我们首先定义一个简单的LoadBalancer结构体,其中包含一个Current字段,用于跟踪哪个服务器应该处理下一个请求。这Mutex确保了我们的代码可以安全地并发使用。

我们负载平衡的每个服务器都由以下结构定义Server

type Server struct {
    URL       *url.URL
    IsHealthy bool
    Mutex     sync.Mutex
}
Enter fullscreen mode Exit fullscreen mode

这里,每个服务器都有一个 URL 和一个IsHealthy标志,指示该服务器是否可用于处理请求。

第 2 步:循环算法

我们的负载均衡器的核心是循环调度算法。其工作原理如下:

func (lb *LoadBalancer) getNextServer(servers []*Server) *Server {
    lb.Mutex.Lock()
    defer lb.Mutex.Unlock()

    for i := 0; i < len(servers); i++ {
        idx := lb.Current % len(servers)
        nextServer := servers[idx]
        lb.Current++

        nextServer.Mutex.Lock()
        isHealthy := nextServer.IsHealthy
        nextServer.Mutex.Unlock()

        if isHealthy {
            return nextServer
        }
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode
  • 此方法以循环方式循环遍历服务器列表。如果所选服务器运行正常,则返回该服务器来处理传入的请求。
  • 我们使用它来Mutex确保Current一次只有一个 goroutine 可以访问和修改负载均衡器的字段。这确保了在并发处理多个请求时,循环算法能够正确运行。
  • 每个Server都有自己的Mutex。当我们检查IsHealthy字段时,我们会锁定服务器,Mutex以防止多个 goroutine 并发访问。
  • 如果没有Mutex锁定,另一个 goroutine 可能会更改该值,从而导致读取不正确或不一致的数据。
  • Mutex一旦更新Current字段或读取字段值,我们就会立即解锁IsHealthy,以保持临界区尽可能小。这样,我们就Mutex可以避免任何竞争条件。

步骤3:配置负载均衡器

我们的配置存储在一个config.json文件中,其中包含服务器 URL 和健康检查间隔(更多信息请参见下面的部分)。

type Config struct {
    Port                string   `json:"port"`
    HealthCheckInterval string   `json:"healthCheckInterval"`
    Servers             []string `json:"servers"`
}
Enter fullscreen mode Exit fullscreen mode

配置文件可能如下所示:

{
  "port": ":8080",
  "healthCheckInterval": "2s",
  "servers": [
    "http://localhost:5001",
    "http://localhost:5002",
    "http://localhost:5003",
    "http://localhost:5004",
    "http://localhost:5005"
  ]
}
Enter fullscreen mode Exit fullscreen mode

步骤 4:健康检查

在将任何传入流量路由到服务器之前,我们希望确保服务器的健康状况。为此,我们会定期向每台服务器发送健康检查:

func healthCheck(s *Server, healthCheckInterval time.Duration) {
    for range time.Tick(healthCheckInterval) {
        res, err := http.Head(s.URL.String())
        s.Mutex.Lock()
        if err != nil || res.StatusCode != http.StatusOK {
            fmt.Printf("%s is down\n", s.URL)
            s.IsHealthy = false
        } else {
            s.IsHealthy = true
        }
        s.Mutex.Unlock()
    }
}
Enter fullscreen mode Exit fullscreen mode

每隔几秒(根据配置中指定),负载均衡器会HEAD向每台服务器发送请求,检查其是否正常运行。如果服务器宕机,则IsHealthy标志位会被设置为false,从而阻止将来的流量路由到该服务器。

步骤5:反向代理

当负载均衡器收到请求时,它会使用反向代理将请求转发到下一个可用的服务器。在 Golang 中,httputil包提供了一种内置的方法来处理反向代理,我们将通过以下ReverseProxy函数在代码中使用它:

func (s *Server) ReverseProxy() *httputil.ReverseProxy {
    return httputil.NewSingleHostReverseProxy(s.URL)
}
Enter fullscreen mode Exit fullscreen mode
什么是反向代理?

反向代理是位于客户端和一个或多个后端服务器之间的服务器。它接收客户端的请求,将其转发到其中一台后端服务器,然后将该服务器的响应返回给客户端。客户端与代理交互时,并不知道具体是哪台后端服务器在处理请求。

在我们的例子中,负载均衡器充当反向代理,位于多个服务器前面并在它们之间分发传入的 HTTP 请求。

步骤 6:处理请求

当客户端向负载均衡器发出请求时,它会使用函数中实现的轮询算法选择下一个可用的健康服务器,getNextServer并将客户端请求代理到该服务器。如果没有可用的健康服务器,我们会向客户端发送服务不可用错误。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        server := lb.getNextServer(servers)
        if server == nil {
            http.Error(w, "No healthy server available", http.StatusServiceUnavailable)
            return
        }
        w.Header().Add("X-Forwarded-Server", server.URL.String())
        server.ReverseProxy().ServeHTTP(w, r)
    })
Enter fullscreen mode Exit fullscreen mode

ReverseProxy方法将请求代理到实际的服务器,并且我们还添加了一个自定义标头X-Forwarded-Server以用于调试目的(尽管在生产中,我们应该避免像这样暴露内部服务器详细信息)。

步骤 7:启动负载均衡器

最后,我们在指定端口上启动负载均衡器:

log.Println("Starting load balancer on port", config.Port)
err = http.ListenAndServe(config.Port, nil)
if err != nil {
        log.Fatalf("Error starting load balancer: %s\n", err.Error())
}
Enter fullscreen mode Exit fullscreen mode

工作演示

TL;DR

在本文中,我们使用 Golang 语言,基于轮询算法从零开始构建了一个基本的负载均衡器。这是一种简单而有效的方法,可以将流量分配到多个服务器,并确保您的系统能够高效地处理更高的负载。

还有很多内容需要探索,例如添加复杂的健康检查、实现不同的负载均衡算法或提升容错能力。但这个基本示例可以作为构建的坚实基础。

您可以在此GitHub 存储库中找到源代码。

文章来源:https://dev.to/vivekalhat/building-a-simple-load-balancer-in-go-70d
PREV
How to Protect Your Server From Hackers Keep your software up to date
NEXT
改变我人生的 7 个 Git 技巧