前言

当我们需要处理成千上万的的用户请求时,当一台服务器可能无法满足这千万级别的请求时就可能需要扩容,增加服务器的数量来维持响应请求。服务器数量增多了,那流量如何均衡分配也是一个问题。这时就需要一个负载均衡,进行协调可用服务器之间的流量。

本文主要讲述使用 Golang 实现一个简单的流浪均衡器。

负载均衡

负载均衡器就是一个能够对请求流量通过算法将请求转发到相应的后端服务。其基本是在所有后端服务前置。一个用户请求过来先经过负载均衡器,然后由负载均衡器转发请求到具体的后端服务上。所以其功能相对来说其实很简单:转发请求。其核心就是,均衡算法然后实现均衡转发请求。常用的算法有以下几种:

轮询算法

轮询法就是通过顺序轮流转发到后端服务器上。这种方式是最简单的,只需要按照顺序进行分配,不关心各服务负载情况,只需关心服务是否可用。如果检查服务存活可用则直接转发到当前服务。

所以这种方法比较适合用在多个服务器间硬件能力和处理能力都差不多的情况,然后对请求进行拆分均衡的转发到各服务上。

简单实现轮询例子:

type roundRobin struct {
    targets []*url.URL
    index   int
}

func (rr *roundRobin) nextTarget() *url.URL {
    target := rr.targets[rr.index]
    rr.index = (rr.index + 1) % len(rr.targets)
    return target
}

func (rr *roundRobin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    target := rr.nextTarget()
    proxy := httputil.NewSingleHostReverseProxy(target)
    proxy.ServeHTTP(w, r)
}

定义了一个名为 roundRobin 的结构体,并实现了一个名为 nextTarget 的方法,该方法返回下一个目标服务器的 URL。在 ServeHTTP 方法中,我们使用 nextTarget 方法获取下一个目标服务器的 URL,并创建一个反向代理对象来将请求转发到选定的服务器。

加权轮询算法

加圈轮询算法是基于轮询算法的。当后端服务处理能力有差别的时候,比如可能A服务处理能力强于B服务2倍,那可以通过加权方式控制A服务处理请求多于B服务。

type serverWeight int

const (
	ServerA serverWeight = 6
	ServerB serverWeight = 3
	ServerC serverWeight = 1
)

比如以上配置表示,每有10个请求就会有6个会转发到A服务器,3个转发到B服务器,1个转发到C服务器。

最少连接数算法

最小连接数是转发依据始终是按照服务器当前连接数最小的那个进行转发。这种方式可以更加有效的利用后端服务,合理的分流到各服务器。该算法不仅要检查各服务是否有效,还需要记录各服务已存在的连接数量。有点像连接池管理。

当然最少连接数算法,也可以对它们增加权重,即在比较连接数时同时考虑各服务的权重,被选中的服务器其连接数与权重的比要最小才能被选中。

详细实现

定义结构体

其他的算法,这里就不一一展开说明,想了解可以自己搜索。本文主要实践使用最少连接算法的负责均衡器。所以一个均衡器的工作是查找连接数最少的服务,然后根据服务地址转发出去。当然还需要知道当前可用服务是否存活。所以可以先定义一个记录这些信息的结构体,如服务请求地址,存活状态、连接数等相关信息。

type Backend interface {
    GetURL() string
    GetConnections() int
}

type leastConn struct {
    targets []Backend
    mutex   sync.Mutex
}

func (lc *leastConn) leastTarget() Backend {
    var target Backend
    minConns := math.MaxInt32

    for _, t := range lc.targets {
        lc.mutex.Lock()
        conns := t.GetConnections()
        lc.mutex.Unlock()

        if conns < minConns {
            minConns = conns
            target = t
        }
    }
    return target
}

func (lc *leastConn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    target := lc.leastTarget()
    targetURL := target.GetURL()

    proxy := httputil.NewSingleHostReverseProxy(targetURL)
    proxy.ServeHTTP(w, r)
}

为了避免出现竞争情况,通过增加锁来控制。RWMutex 适合用于读多写少的场景。支持多个 goroutie 可以同时获取读锁,但是写时仅支持一个 goroutine 占有。reverseProxy 官网的解释:是一种 Http Handler ,接收请求并发送到另一台服务器中,把响应代理回客户端。所以 ReverseProxy 的作用就是转发原始的请求。

在上面的代码中国,我们定义了一个名为 Backend 的接口,并实现了一个名为 leastConn 的结构体。在 leastConn 结构体中,我们使用 Backend 接口代替直接使用 URL 来表示服务器,这样我们可以将任何实现了 Backend 接口的服务器添加到负载均衡器中。

leastTarget 方法中,我们使用 Backend 接口的方法来获取每个服务器的当前连接数,并选择连接数最少的服务器来处理该请求。