反向代理负载均衡

2017年07月22日 09:03 | 1063次浏览 作者原创 版权保护

我们前面曾经介绍过 Web 反向代理,的确,反向代理缓存让我为之振奋,然而,反向代理的作用不止如此,它同样可以作为

调度器来实现负载均衡系统。

显然,反向代理服务器的核心工作便是转发 HTTP 请求,因此它工作在 HTTP 层面,也就是 TCP 七层结构中的应用层(第七层),所以基于反向代理的负载均衡也称为七层负载均衡,实现它并不困难,目前几乎所有主流的 Web 服务器都热衷于支持

基于反向代理的负载均衡,随后我们的介绍中便会不同程度地涉及这些内容。

转移和转发

相比于前面介绍的 HTTP 重定向和 DNS 解析,作为负载均衡调度器的反向代理,对于 HTTP 请求的调度体现在“转发”上,而前者则是“转移”。

不论是转移还是转发,它们的根本目的都是相同的,那就是希望扩展系统规模,来提高承载能力,这是毋庸置疑的。

单纯地区分这些概念并没有什么意义,你需要明白的是,这种机制的改变,使得调度器完全扮演用户和实际服务器的中间人,

这意味着:

任何对于实际服务器的 HTTP 请求都必须经过调度器;

调度器必须等待实际服务器的 HTTP 响应,并将它反馈给用户。

看到这里,你是否想起前面故事中的外包接口人呢?没错,接口人的工作职能也正是“转发”

,因为他不希望让客户直接和外包公司接触,原因就不用我多说了吧。

正因为这样,基于反向代理的负载均衡需要我们用另一种思考方式来评判它,我们接着来看下面的内容。

按照权重分配任务

刚才我们提到,在这种全新的调度模式下,任何对于实际服务器的 HTTP 请求都必须经过调度器,这使得我们一直以来苦恼的问题终于有望解决了,那就是可以将调度策略落实到每一个 HTTP 请求,从而实现更加可控的负载均衡策略。

顺便说明一下,在基于反向代理的负载均衡系统中,我们也常把实际服务器称为后端服务器(Back-end Server)。

当不同能力的后端服务器并存的时候,调度器并不希望平均分配任务给它们,这很容易理解,的确,大锅饭时代的结束已经宣告了平均分配的愚昧。

本着能者多劳的原则,有些反向代理服务器可以非常精确地控制分配权重,这里我们用 Nginx 作为反向代理服务器,来看看权重设置对于整体吞吐率的影响。

我们准备了两台服务器作为后端,分别为 10.0.1.200 和 10.0.1.201,它们的 80 端口都运行着 Apache。同时,我们用 PHP 编写了一个动态程序,它可以模拟不同程度的计算任务,为了尽可能不依赖其他资源,我们将它设计成 CPU 密集型的程序,代码

如下所示:

<?php
$num = $_GET['num'];
$sum = 0;
for ($i = 0; $i < $num; ++$i)
{
$sum += $i;
}
echo $sum;
?>

可以看出,这个 PHP 程序可以根据 URL 参数的传递进行不同开销的计算。那么,接下来我们分别来对这两台后端服务器进 行压力测试,结果如下所示:


可以看到,同样是 10 万次的循环计算,两台后端服务器的吞吐率存在很大的差异,这是怎么回事呢?忘了告诉你了,这里我 们故意挑选了两台配置不同的服务器,它们的配置如表 2-1 所示。

表 2-1 两台后端服务器的具体配置


所以,10.0.1.201 这台服务器的吞吐率显得逊色一些,不过没有关系,权重分配正好能够派上用场。 接下来我们在另一台 IP 地址为 10.0.1.50 的服务器上运行 Nginx,作为反向代理服务器,也就是负载均衡调度器,并为它配置 两个后端服务器,如下所示:

upstream backend { 
server 10.0.1.200:80; server 10.0.1.201:80;
 }

以上的配置只是这里提到的关键部分,更多的配置你可以查阅 Nginx 的官方文档,里面有很详细的介绍。

经过这样的配置后,Nginx 将任务平均分配给两个后端,虽然我们知道这样做不太明智,但还是希望先来看看这样做的结果。我们对调度器进行同样条件的压力测试,结果如下所示:

Requests per second:
144.26 [#/sec] (mean)

吞吐率只有 144.26reqs/s,还不如那个 197reqs/s 的后端服务器,的确,罪魁祸首就是另一台后端服务器,它拖了后腿,帮了倒忙,但是也不能怪它,调度器应该对此负责。

接下来,我们来改变权重分配,如下所示:

upstream backend {
server 10.0.1.200:80 weight=2;
server 10.0.1.201:80 weight=1;
}

这意味着让更强的那台服务器比另一台多干一倍的任务,它们的分配权重为 2:1,再来看看测试结果:

Requests per second:
224.37 [#/sec] (mean)

现在的吞吐率已经超过了任意一台后端服务器的单独成绩,但只是微微超出,这显得另一台后端服务器没帮上什么忙,似乎可有可无,不行,它必须证明自己的存在是有价值的。

接下来,我们将权重分配调整为 3:1,如下所示:

upstream backend {
server 10.0.1.200:80 weight=3;
server 10.0.1.201:80 weight=1;
}

再次进行同样的压力测试,结果如下所示:

Requests per second:
266.28 [#/sec] (mean)

可以看到,吞吐率又有明显的提升,现在已经比较接近两台后端服务器的独立成绩之和,那么,如果我们继续调整分配权重

为 4:1,结果又会如何呢?你一定很想知道,下面我们进行一下调整:

upstream backend {
server 10.0.1.200:80 weight=4;
server 10.0.1.201:80 weight=1;
}

再来看看测试结果:

Requests per second:
249.18 [#/sec] (mean)

好,我们将这四种权重比例下的整体吞吐率绘制成曲线图,如图 2-7 所示

图 2-7 两台后端服务器不同权重分配比例下的吞吐率

事实告诉我们,吞吐率开始下降了,也就是说,对于以上两台后端服务器,当它们的分配权重为 3:1 的时候,整个系统可以获得最佳的性能表现。你也许已经想到了,之所以是 3:1,原因就在于两台后端服务器的独立成绩刚好近似等于 3:1,所以按照它们的能力来分配权重比例,显然可以最大程度地物尽其用。

当然,支持带有权重分配机制的 RR 调度策略,不只出现在 Nginx 中,很多其他的反向代理服务器软件也都将此视为一个必备的内置功能,但是需要强调的是,不论是哪个具体的实现,它们的权重分配机制本身都是相同的,由于这种机制和工作方式的局限,不同实现所表现出的性能几乎不会有太大差异。

这里我们不妨使用 HAProxy 来代替 Nginx,完成同样的工作,并且依次进行各种权重比例下的压力测试。HAProxy 也是一款主流的反向代理服务器,可以作为负载均衡调度器,我们对 HAProxy 进行后端配置,其中关键部分示例如下:

listen
proxy_1 10.0.1.50:8003
mode http
option httplog
option dontlognull
balance roundrobin
stats uri /hastat
server backend_1 10.0.1.200:80 weight 3
server backend_2 10.0.1.201:80 weight 1

可以看到,我们将 HAProxy 监听在 8003 端口,然后为它设置了前面用到的两台后端服务器,HAProxy 也同样支持权重分配

机制,接下来我们进行同样的压力测试,这里只列出各种权重分配比例下的整体吞吐率,如表 2-2 所示

表 2-2 HAProxy 在不同权重分配比例下的吞吐率

的确,在以上四种权重分配比例下,测试结果几乎与使用 Nginx 时不相上下。

所以,这里我们不会将太多的笔墨放在比较或者推荐具体的某个反向代理服务器软件,因为那些都是不停变化的商业产品,其中一些可能经不起时间的考验,而唯有真理和本质是相对持久的,一旦你了解了这些内容之后,至于如何选择,就像去逛超市一样,各家产品都说自己很强大,而你需要的是分析和测量。

调度器的并发处理能力

对于作为负载均衡调度器的反向代理服务器来说,它首先必须是一台经得起考验的 Web 服务器,没错,因为它工作在 HTTP层面,它的一切工作都得从处理用户的 HTTP 请求开始。

所以,反向代理服务器本身的并发处理能力显得尤为重要,幸运的是,早在第 3 章中,我们就已经对影响服务器并发处理能力的各种因素进行了深入的探讨,你可以再次重温那些内容。

为此,请不要将反向代理服务器放到一个神秘的位置上,一旦你将它视为某种意义上的 Web 服务器,那些你曾经熟悉的概念

将再次上演,比如 I/O 模型和并发策略。

这也或多或少地影响了反向代理服务器软件的市场格局,主流的 Web 服务器都争先恐后地支持反向代理机制,比如 Apache、Lighttpd、Nginx 等,因为它们作为 Web 服务器所取得的辉煌成就让它们不费吹灰之力就可以转型成为一台马力强劲的负载均衡调度器。

扩展的制约

到现在为止,作为负载均衡调度器的反向代理服务器似乎出尽了风头,我们接下来将目光转移到另一个重要的方面,那就是这种负载均衡系统的扩展能力。理所当然,作为调度器的反向代理服务器成为我们关注的重点,因为它扮演着接口人的重要角色。

我们知道反向代理服务器工作在 HTTP 层面,对于所有 HTTP 请求都要亲自转发,可谓是大事小事亲历亲为,这也让我们为它捏了一把冷汗,你也许在怀疑它究竟有多大能耐,能支撑多少后端服务器,的确,这直接关系到整个系统的扩展能力。

接下来,我们选择了两台承载能力基本相当的后端服务器,仍然通过反向代理服务器实现负载均衡,它们的网络结构如图 2-8所示,其中各服务器的用途和 IP 地址如表 2-3 所示。

表 2-3 反向代理负载均衡系统网络结构示意图中的服务器说明


然后,我们对后端服务器和调度器分别进行了一系列的压力测试,值得一提的是,这一次我们并不是在被测试的服务器上执

行 ab,而是在与被测试服务器同一网段的其他服务器上执行 ab 进行压力测试,这样可以减少 ab 本身的开销对测试结果的影响,同时也是为了和随后的 IP 负载均衡测试结果保持可比性


图 2-8 反向代理负载均衡系统网络结构示意图

我们来看看测试结果,如表 2-4 所示

表 2-4 后端服务器和反向代理服务器分别对于不同内容的压力测试结果

在分析这些数据之前,我先来解释一下,在表 2-4 中,左面第一列给出了 8 种 Web 内容,它们对应着不同程度的 CPU 计算和I/O 操作,对 Web 服务器来说这意味着处理这些内容将花费不同的开销,其中的动态内容正是前面那个可以根据指定循环次数进行计算的 PHP 程序,在这里它又派上了用场。右面的三列分别列出了两个后端服务器的独立测试结果和整个负载均衡系统的测试结果,当然,它们的单位都是 reqs/s。

为了更好地分析表格中的数据,我们绘制了一幅柱状对比图,如图 2-9 所示


图 2-9 后端服务器和反向代理服务器分别对于不同内容的压力测试结果柱状对比图

从上往下看,内容处理的开销逐渐减少,两台后端服务器的吞吐率逐渐增加,反向代理服务器的吞吐率也随之增加,但是,我们需要注意的是,对于以上各种类型的内容,反向代理服务器的吞吐率是否为两台后端服务器的吞吐率叠加之和呢?当然,这是我们希望看到的。

从图上可以清晰地看出,当 num=100000 时,内容处理的开销最大,负载均衡系统的整体吞吐率几乎等于两台后端服务器的吞吐率之和,随后当 num 逐渐减少,整体吞吐率开始渐渐地落后于两台后端服务器的吞吐率之和,当 num=1000 的时候,整体吞吐率甚至还不如任意一个后端服务器的吞吐率高,如图 2-10 所示的对比图更加直观地反映了整体吞吐率的变化趋势。

这里我们虽然只用了两台后端服务器,但是已经充分暴露了调度器的“接口人瓶颈”,这种瓶颈效应随着后端服务器内容处理时间的减少而逐渐明显,这不难解释,反向代理服务器进行转发操作本身是需要一定开销的,比如创建线程、与后端服务器建立 TCP 连接、接收后端服务器返回的处理结果、分析 HTTP 头信息、用户空间和内核空间的频繁切换等,通常这部分时间并不长,但是当后端服务器处理请求的时间非常短时,转发的开销就显得尤为突出



图 2-10 后端服务器和反向代理服务器分别对于不同内容的压力测试结果曲线对比图

另一方面,我们前面也提到了调度器本身的并发处理能力,这也使得当反向代理服务器的吞吐率逐渐接近极限时,无论添加多少后端服务器都将无济于事,因为调度器已经忙不过来了。

所以,你已经意识到,工作在 HTTP 层面的反向代理服务器扩展能力的制约,不仅来自于自身的对外服务能力,也归咎于其转发开销是否上升为主要时间。

为此,我们再来如图 2-9 所示的对比图,从上到下算是两个极端,从图上可以看出,最适合通过反向代理服务器来实现负载均衡的,正是位于图表上方的几种内容,它们就像从事劳动密集型工作一样,人多力量大;而位于图表底部的几种内容,反

向代理服务器逐渐变得吃力,尤其是最后的静态内容,整体不但没有提升吞吐率,还浪费了多台服务器资源,对于这种情况,更适合使用前面介绍的基于 DNS 的负载均衡方式。

健康探测

在前面介绍基于 DNS 的负载均衡时,我们曾经提到用于了解实际服务器状态的监控系统,通过它,我们可以第一时间发现存在故障的服务器,然后快速动态更新 DNS 服务器。

当然,这里提到的监控系统并不是 DNS 服务器的组成部分,你可以选择一些开源项目或者商业产品,也可以根据需要自己开发,但是无论如何,你都需要付出不少的人力或者金钱。

幸运的是,一些反向代理服务器软件希望出人头地,成为更加出色的负载均衡调度器,它们将监控后端服务器的职责视为自己的神圣使命,得益于反向代理服务器的工作机制,它们可以轻松有效地监视后端服务器的任何举动,而你只需要简单配置

即可。

通常来说,我们希望能够监控后端服务器的很多方面,比如系统负载、响应时间、是否可用、TCP 连接数、流量等,它们都是负载均衡调度策略需要考虑的因素。在这里,我们使用 Varnish 作为调度器,来监控后端服务器的可用性。

你一定还记得 Varnish 吧,前面我们介绍反向代理缓存的时候曾经提到过它,没错,它同时也支持负载均衡,我们来修改 Varnish的配置文件,增加后端服务器和负载均衡策略等配置,如下所示:

backend b1 {
.host = "10.0.1.201";
.port = "80";
.probe = {
.url = "/probe.htm";
.interval = 5s;
.timeout = 1s;
.window = 5;
.threshold = 3;
}
}
backend b2 {
.host = "10.0.1.202";
.port = "80";
.probe = {
.url = "/probe.htm";
.interval = 5s;
.timeout = 1s;
.window = 5;
.threshold = 3;
}
}
director lb round-robin {
{
.backend = b1;
}
{
.backend = b2;
}
}

可以看到,我们通过 backend 关键字定义了两个后端服务器,并且用 director 关键字定义了一个名为 lb 的负载均衡调度器,同时采用了 RR 调度策略。这里需要说明一下,Varnish 目前对于 RR 调度似乎不支持分配权重的设置,所以这里我们选用了

两台承载能力基本相同的后端服务器。

在 backend 的定义部分中,我们看到了.probe 设置,这代表了 Varnish 的探测器,根据这些配置,调度器将会每隔 5 秒钟请求后端服务器的/probe.htm,只有当 HTTP 响应头代码为 200 时,调度器才认为该后端服务器是可用的。

作为后端服务器上的被探测内容,这里我们使用了一个静态文件 probe.htm,你也可以使用 PHP 等动态程序,甚至在程序中包含数据库查询等操作,这样可以更加贴近真实场景,反应实际状态,但是有一点需要注意,那就是当你要报告一个不可用故障时,只需要返回一个不是 200 的状态码即可。

同时,别忘了还要在 Varnish 的 vcl_recv 回调过程中调用刚才定义的调度器,如下所示

sub vcl_recv {
if (req.request != "GET" &&
req.request != "HEAD" &&
req.request != "PUT" &&
req.request != "POST" &&
req.request != "TRACE" &&
req.request != "OPTIONS" &&
req.request != "DELETE") {
return (pipe);
}
if (req.request != "GET" && req.request != "HEAD") {
return (pass);
}
if (req.http.Authorization || req.http.Cookie) {
return (pass);
}
set req.backend = lb;
return (pass);
}

以上的粗体部分正是我们添加的调用语句,你一定还记得 vcl_recv 这个过程的触发条件,它是在反向代理服务器接收到用户的 HTTP 请求后被调用,所以这时候需要通过调度器来选择适当的后端服务器并转发请求。同时,最后一行的 return (pass)也是经过了我们的修改,原来的 return (lookup)意味着反向代理缓存将可能发挥作用,而这里我们并不需要反向代理缓存。

在验证探测器之前,我们先对两台后端服务器分别进行压力测试,它们的吞吐率表现如表 2-5 所示

表 2-5 两台后端服务器的独立吞吐率

现在我们使用 Varnish 作为调度器,它会将我们的请求轮流转发给两台后端服务器。但是,我们暂时不开启 Varnish 的探测器,来看看会发生什么结果

接下来,关键的时刻到了,我们将其中一台后端服务器的 Web 服务关闭,这导致它将无法处理任何 HTTP 请求。然后,我们对调度器进行压力测试,结果如下所示:

很糟糕,有一半数量的请求都失败了,虽然整体吞吐率基本接近于两个后端服务器的独立吞吐率之和,但是这又有什么意义 呢? 现在我们开启刚才配置好的探测器,重新启动 Varnish,为了让探测器工作,我们等待了 5 秒钟,再次进行压力测试,结果如 下所示:


可以看到,调度器已经放弃了关闭 Web 服务的那台后端服务器,即便整体吞吐率大幅度下降,但至少不会给用户返回一个错误页面。

在实际应用中,为了提高整个负载均衡系统的可用性而不影响性能,我们可以部署一定数量的备用后端服务器,这样即便是一些后端服务器出现故障后被调度器放弃,备用后端服务器也可以接替它们的工作,保证整体的性能。

粘滞会话

负载均衡调度器最大程度地让用户不必关心后端服务器,我们知道,当采用 RR 调度策略时,即便是同一用户对同一内容的多次请求,也可能被转发到了不同的后端服务器,这听起来似乎没什么大碍,但有时候,或许会带来一些问题。

当某台后端服务器启用了 Session 来本地化保存用户的一些数据后,下次用户的请求如果转发给了其他后端服务器,

将导致之前的 Session 数据无法访问;

后端服务器实现了一定的动态内容缓存,而毫无规律的转发使得这些缓存的利用率下降。

如何解决这些问题呢?从表面上看,我们需要做的就是调整调度策略,让用户在一次会话周期内的所有请求始终转发到一台特定的后端服务器上,这种机制也称为粘滞会话(Sticky Sessions),要实现它的关键在于如何设计持续性调度算法。

既然要让调度器可以识别用户,那么将用户的 IP 地址作为识别标志最为合适,一些反向代理服务器对此都有支持,比如 Nginx

和 HAProxy,它们可以将用户的 IP 地址进行 Hash 计算并散列到不同的后端服务器上

对于 Nginx,只需要在 upstream 中声明 ip_hash 即可,如下所示:

upstream backend {
ip_hash;
server 10.0.1.200:80;
server 10.0.1.201:80;

而对于 HAProxy 的配置也非常简单,你需要在 balance 关键字后面添加 source 策略名称,同时要使用 TCP 模式,如下所示:

listen
proxy_1 10.0.1.50:8003
mode tcp
option httplog
option dontlognull
balance source
stats uri /hastat
server backend_1 10.0.1.200:80
server backend_2 10.0.1.201:80

除此之外,你还可以利用 Cookies 机制来设计持久性算法,比如调度器将某个后端服务器的编号追加到写给用户的 Cookies中,这样调度器便可以在该用户随后的请求中知道应该转发给哪台后端服务器。这样做可以更加细粒度地追踪到每一个用户,

试想一下,当有很多用户隐藏在一个公开 IP 地址后面时,利用 Cookies 的持久性算法将显得更加有效。

另一方面,回到我们刚才提到的第二个问题,我们希望将对同一个 URL 的请求始终转发到同一台特定的后端服务器,以充分利用后端服务器针对该 URL 进行的本地化缓存,要实现这一点,HAProxy 也提供了支持,我们使用 uri 策略名称配置如下:

listen
proxy_1 10.0.1.50:8003
mode http
option httplog
option dontlognull
balance uri
stats uri /hastat
server backend_1 10.0.1.200:80
server backend_2 10.0.1.201:80

这使得作为调度器的 HAProxy 将对请求的 URL 进行 Hash 计算,然后散列到多台后端服务器上。

好,我们已经实现了粘滞会话,但是如此一来,粘滞会话可能或多或少地破坏了均衡策略,至少像权重分配这样的动态策略已经无法工作,我们对此不能视而不见,否则前面的努力即将付诸东流。

当然,问题的关键在于,我们究竟是否要通过实现粘滞会话来迁就系统的特殊需要呢?在权衡代价之后你认为是否值得呢?

最为关键的问题是前面提到的两个问题是否能从根本上避免呢?如果可以,这很值得去考虑。

事实上,在后端服务器上保存 Session 数据和本地化缓存,的确是一件不明智的事情,它使得后端服务器显得过于个性化,以至于和整个系统格格不入,如果允许的话,我们应该尽量避免这样的设计,比如采用分布式 Session 或者分布式缓存等,让后端服务器的应用尽量与本地无关,也可更好地适应环境。


此文章本站原创,地址 https://www.vxzsk.com/1219.html   转载请注明出处!谢谢!

感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程