负载均衡器在 Web 架构中扮演了很关键的角色。它们能在一组后端机器分配负载。这使得服务扩展性更好。因为配置了很多的后端机器,服务也因此能在某次请求失败后找到正常运行的服务器而变得高可用。

在使用了像 等专业的负载均衡器后,我自己也尝试着用 创建了一个简易负载均衡器。Go 是一种现代语言,第一特性是支持并发。Go 有丰富的标准库,使用这些库你可以用更少的代码写出高性能的应用程序。对每一个发行版本它都有静态链接库。

我们的简易负载均衡器工作原理

负载均衡器有不同的策略用来在一组后端机器中分摊负载。

例如:

  • 轮询 平等分摊,认为后端的所有机器处理能力相同
  • 加权轮询 基于后端机器不同的处理能力,为其加上不同的权重
  • 最少连接数 负载被分流到活跃连接最少的服务器

至于我们的简易负载均衡器,我们会实现这里边最简单的方式 轮询

轮询选择

轮询无疑是很简单的。它轮流给每个 worker 相同的执行任务的机会。

上图已经说明了,负载均衡器周期性地选择某台服务器。但是我们不能直接使用它,不是吗?

如果后端机器宕机了怎么办?恐怕我们不会希望流量被路由到挂掉的机器上去。因此除非我们添加一些条件,否则不能直接使用这个算法。我们需要把流量只路由到没有挂掉且正常运行的后端机器上

定义几个结构体

修正思路后,现在我们清楚我们是想要一种能跟踪后端机器状态信息的方法。我们需要检查机器是否存活,也需要跟踪 Url。

我们可以简单地定义一个下面的结构体来维护我们的后端机器。

Backend
ServerPool

ReverseProxy 的使用

前面已经声明过了,负载均衡器是专门用来把流量路由到不同的后端机器以及把结果返回给来源客户端的。

Go 官方文档的描述:

ReverseProxy 是一种 HTTP Handler,接收请求并发送到另一台服务器,把响应代理回客户端。
ReverseProxy
httputil.NewSingleHostReverseProxy(url)url
httpHandlerFunc

你可以在中找到更多例子。

ReverseProxyBackendURLReverseProxyReverseProxyURL

选择处理过程

我们要在下一次轮询中跳过挂掉的后端机器。但是无论如何我们需要一种计数的方式。

mutexServerPool
atomic

这里我们的加 1 是原子操作,通过对切片的长度取模返回了 index。这意味着返回的值一定在 0 与切片长度之间。归根结底,我们需要的是一个特定的 index,而不是所有数。

选中存活的后端机器

我们已经知道我们的请求是被周期性的路由到每台后端机器上的。我们要做的就是跳过挂掉的机器。

GetNext()
next + length

当我们通过搜索找到了一台正常工作的后端机器时,我们把它标记为 current。

下面是对应上面操作的代码。

在 Backend 结构体中避免竞争条件

Backend
RWMutexAlive

让负载均衡器发请求

所有的准备工作都做完了,我们可以用下面的方法对我们的请求实现负载均衡。只有在所有的后端机器都离线后它才会返回失败。

HandleFunc

仅把流量路由到健康的后端机器

lb

我们可以用两种方法实现:

  • **主动:**在处理当前的请求时,选中的某台机器没有响应,我们把它标记为挂掉。
  • **被动:**我们可以以固定的周期 ping 后端机器,检查其状态

主动检查后端健康机器

ReverseProxyErrorHandler

这里我们利用闭包的特性来设计这个错误 handler。我们能把 serverUrl 等外部的变量捕获到方法内。它会检查已存在的重试次数,如果小于 3,就会再发送同样的请求到同一台机器。这么做的原因是可能会有临时的错误,服务器可能暂时拒绝你的请求而在短暂的延迟之后又变得可用了(可能服务器为了接受更多的客户端耗尽了 socket)。因此我们加了一个定时器,延迟 10 毫秒左右进行重试。每次请求都会加一次重试次数的计数。

3 次请求都失败后,我们把这台后端机器标记为挂掉。

lb
lb

我们可以简单地从请求中取得尝试次数,如果它超过了最大数,取消这次请求。

这个实现是递归的。

context 的使用

context
iotaiota

然后我们可以像在 HashMap 中检索值一样检索定义的值。返回的默认值随实际用例的不同而不同。

被动健康检查

通过被动健康检查我们可以恢复挂掉的后端机器或识别它们。我们以固定的时间周期 ping 后端机器来检查它们的状态。

/status

现在我们可以像下面这样遍历服务器并标记它们的状态。

在 Go 中,我们可以起一个定时器来周期性地运行它。当定时器创建后,你可以用通道监听事件。

<-t.Cselectdefaultselect

最后,在一个单独的协程中运行。

总结

本文讲了很多

  • 轮询选择
  • 标准库中的 ReverseProxy
  • Mutex
  • 原子操作
  • 闭包
  • 回调
  • select 操作

还有很多可以做的来改进我们的简易负载均衡器。

例如:

  • 用堆来对后端机器进行排序,减少搜索范围
  • 采集统计信息
  • 实现加权轮询/最少连接
  • 支持配置文件

等等。

你可以在找到代码库。

感谢阅读????


本文由 原创编译, 荣誉推出