HTTP 重定向

2018年06月06日 08:36 | 3236次浏览 作者原创 版权保护

对于 HTTP 重定向,你一定不陌生,它可以将 HTTP 请求进行转移,在 Web 开发中我们经常会用它来完成自动跳转,比如用户登录成功后跳转到相应的管理页面。

这种重定向完全由 HTTP 定义,并且由 HTTP 代理和 Web 服务器共同实现。很简单,当 HTTP 代理(比如浏览器)向 Web服务器请求某个 URL 后,Web 服务器可以通过 HTTP 响应头信息中的 Location 标记来返回一个新的 URL,这意味着 HTTP代理需要继续请求这个新的 URL,这便完成了自动跳转。当然,如果你自己写了一个 HTTP 代理,也可以不支持重定向,也就是对于 Web 服务器返回的 Location 标记视而不见,虽然这可能不符合 HTTP 标准,但这完全取决于你的应用需要。

也正是因为 HTTP 重定向具备了请求转移和自动跳转的本领,所以除了满足应用程序需要的各种自动跳转之外,它还可以用于实现负载均衡,以达到 Web 扩展的目的。

熟悉的镜像下载

你也许有过从 php.net 下载 PHP 源代码的经历,那么你是否注意过它是如何实现镜像下载的呢?没错,那就是 HTTP 重定向,而镜像下载的目的便是实现负载均衡,值得一提的是,这里我们暂且认为所有镜像服务器上的内容都是一致的,后续章节中我们会介绍内容分发与同步的一些方法和策略。

我们来看这次下载的重定向过程,首先,记住我们请求的 URL 为:

www.php.net/get/php-5.2.9.tar.gz/from/a/mirror

接下来,我们在浏览器中打开这个地址,并且通过 HttpWatch 监视 HTTP 请求和响应,如下所示

GET /get/php-5.2.9.tar.gz/from/a/mirror HTTP/1.1
Accept:
image/gif,
image/x-xbitmap,image/jpeg,image/pjpeg,
application/
x-shockwave-flash,
application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Referer: http://www.php.net/downloads.php
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; GTB6; CIBA; .NET CLR 2.0.50727)
Host: www.php.net
Connection: Keep-Alive
HTTP/1.1 302 Found
Date: Wed, 13 May 2009 12:08:51 GMT
Server: Apache/1.3.41 (Unix) PHP/5.2.9RC3-dev
X-Powered-By: PHP/5.2.9RC3-dev
Content-language: en
X-PHP-Load: 0.60546875, 0.568359375, 0.5634765625
Location: http://cn.php.net/get/php-5.2.9.tar.gz/from/a/mirror
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8

可以看到,HTTP 响应的状态码为 302,并通过 Location 标记返回了新的 URL,这个 URL 位于 cn.php.net 这个新的域名下,按照命名规则,我们知道这个域名指向部署在中国的服务器,这样一来,我们刚才向 php.net 主站点发出的下载请求便被转移到了国内的服务器上,这相当于主站点将一部分负载转移到了其他服务器上。

接下来,我们再次请求刚才那个位于 php.net 主站点的 URL,这次获得的 HTTP 响应如下所示:

HTTP/1.1 302 Found
Date: Wed, 13 May 2009 12:08:51 GMT
Server: Apache/1.3.41 (Unix) PHP/5.2.9RC3-dev
X-Powered-By: PHP/5.2.9RC3-dev
Content-language: en
X-PHP-Load: 0.60546875, 0.568359375, 0.5634765625
Location: http://cn2.php.net/get/php-5.2.9.tar.gz/from/a/mirror
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8

这回 Location 中的 URL 发生了变化,域名变成了 cn2.php.net。我们继续重复请求多次,通过 HttpWatch 的截图(如图 2-2 所 示)我们可以看到这些请求的重定向过程。

图 2-2 php.net 站点的 HTTP 重定向监视

可见,php.net 使用了两台位于国内的镜像服务器,分别对应以下的 URL: 

cn.php.net/get/php-5.2.9.tar.gz/from/a/mirror 

cn2.php.net/get/php-5.2.9.tar.gz/from/a/mirror

通过图 2-2 我们看到两个镜像地址轮流上阵,但实际上它们并不是严格地交替出现,通过持续观察,你会发现它们似乎是随 机出现,但是从整体来看,这已经做到了一定程度上的负载均衡。

 在这里,php.net 主站点负责进行地域来源判断以及选择镜像服务器,值得一提的是,这里的重定向方案,在实现负载均衡的 同时,也达到了就近访问的目的,加快了用户的下载速度,并且在一定程度上避免了国际带宽的浪费。

 当然,通过不同的地域来源来转移请求只是负载均衡的一种策略,在网络 I/O 成为主要瓶颈(比如下载服务)的时候,这种 策略的优势表现得尤为突出。但是在其他一些时候,通过地域来源来划分请求并不那么合理,比如来自各个地域的请求到达 Web 服务器的响应时间差异不大,并且各地域的请求比例存在较大差异时,这种策略显然不太适合,它会造成各镜像服务器 的负载分配不均衡,从而造成资源的浪费。

用代码来实现 

相比于随后要介绍的其他负载均衡方法,基于 HTTP 重定向的方式非常简单,基本上完全由 Web 应用程序即可实现,但是它 的性能表现如何呢?我们这里来实现一个基于 HTTP 重定向的负载均衡系统。

 假设我们的站点域名为 www.highperfweb.com,它指向的 IP 地址为 10.0.1.100,我们希望将所有对于站点首页的请求随机转移 到其他三台实际服务器上,为此,我们进行了以下的域名 DNS 设置:

www.highperfweb.com.   IN   A   10.0.1.100
www1.highperfweb.com.  IN   A   10.0.1.101
www2.highperfweb.com.  IN   A   10.0.1.102
www3.highperfweb.com.  IN   A   10.0.1.103

可以看到,我们为其他三台服务器准备了新的二级域名,不过看起来毫无内涵,这里我们暂时认为这三台服务器都拥有一致 的内容。 

这样一来,你完全可以告诉用户直接访问某一台服务器,比如 www2.highperfweb.com,已达到分散主站工作量的目的,但是 很显然,这样做不会有任何好处,这等于让用户跳过了我们的负载均衡策略,而且也没有多少用户愿意记住这些古怪的域名, 架构师永远不要给用户添麻烦。 

提示: 在这个例子中的 IP 地址都是内部地址,但在实际应用中,你必须使用用户可以访问的互联网公开 IP 地址。接下来,我们编写了首页 index.php 的代码,如下所示:

<?php
$domains = array(
'www1.highperfweb.com',
'www2.highperfweb.com',
'www3.highperfweb.com'
);
$index = substr(microtime(), 5, 3) % count($domains);
$domain = $domains[$index];
header("Location: http://$domain");
?>

看看这段代码,很简单,当用户每次访问站点入口,也就是 index.php 的时候,程序会随机挑选三台服务器中的一台,然后返回重定向指令。我们用 curl 来试试看:

s-colin:~ # curl -i www.highperfweb.com
HTTP/1.1 302 Found
Date: Thu, 14 May 2009 02:15:59 GMT
Server: Apache/2.2.11 (Unix) PHP/5.2.1 DAV/2 SVN/1.4.3
X-Powered-By: PHP/5.2.1
Location: www3.highperfweb.com
Content-Length: 0
Content-Type: text/html

看来这次 www3.highperfweb.com 这台服务器比较幸运,它被随机选中,在随后的多次请求中,三台镜像服务器对应的域名都分别出现在了 Location 标记中,从整体上看,首页请求转移到三台服务器的次数基本上是相等的,所以我们已经做到了一定程度的均衡。

这里为什么没有采用依次轮询的重定向策略呢?也就是 RR(Round Robin),它是一种基本的调度算法,在这个例子中如果采用 RR 方式的话,站点首页程序必须将所有请求按照顺序依次转移给三台服务器,实现绝对意义上的均衡,但是,与此同时,性能上的代价也是不可避免的,因为: 

HTTP 本身是无状态的,如果要实现按顺序转移请求,我们必须将最后一次转移至的实际服务器序号进行持久化保存,以便在多次 HTTP 请求之间可以共享,比如将它存入 Memcache,显然,这将会带来额外的开销; 要实现绝对的按顺序转移,必然会使得主站点请求转移计算的并发性大打折扣,因为转移状态(最后一次转移至的实际服务器序号)是互斥资源,转发程序必须通过一定的锁机制来保证任何时刻只能有一个请求可以修改它。

这就好比一个人做事,独立决策并执行的速度肯定要比上报领导等待批准快。那么,你可能需要在两者之间进行权衡,随后我们将通过测试来比较一下它们的性能差异

重定向的性能和扩展能力

这里我们指的性能,实际上是针对主站点来说的,因为它负责转移请求到其他服务器,就像前面故事中的外包接口人一样重要,决定着整个负载均衡系统的扩展能力,也意味着整个系统的最大承载能力。

对于刚才的随机转移方案,我们通过 ab 对主站点的首页程序进行压力测试,结果如下所示

我们看到吞吐率为 6438.46reqs/s,这意味着什么呢?我们知道主站点的这个首页程序只是负责转移请求,刚才也看到了它的代码实现,它的开销并不大,但是位于其他三台服务器上实际首页的吞吐率如何呢?也许它们由于频繁的数据库访问而只有几百的吞吐率,也许通过实施一些缓存策略可以达到几千的吞吐率,你甚至可以花点心思通过完全静态化来达到上万的吞吐率,前面的章节对此已经有很详细的介绍,但是在这里,这个问题其实并不是我们所关心的,而我们关心的是主站点的最大 吞吐率和它的扩展能力有什么关系。

 其实这很简单,通过 HTTP 重定向的原理以及我们采取的转移均衡策略,不难得知,当主站点首页的吞吐率达到 6438.46reqs/s 这个极限时,它转移给其他三台服务器的请求均相当于这个吞吐率的 1/3,即 2146.15reqs/s,那么,我们要做的就是保证这三 台服务器能够承载这样的压力吗?当然如果你能做到的话最好,但如果不行,那也正常,不要忘了,扩展是把握在我们自己 手里的。 

我们假设这三台实际服务器的首页能承受的最大吞吐率为 500reqs/s,那么理论上当主站点的实际吞吐率小于 1500reqs/s 的时 候,实际服务器可以在承受范围内提供服务。当主站点的实际吞吐率大于 1500reqs/s 后,我们便需要增加实际服务器,但是 理论上最多可以增加到 13 台,这取决于主站点首页的最大吞吐率(6438.46reqs/s),它几乎是实际服务器上首页最大吞吐率的 13 倍。

RR 策略下的性能

 还记得前面提到的 RR 方式吗?我们现在对主站点首页程序进行修改,希望它能够更加均衡地转移请求到多台实际服务器, 代码如下所示

<?php
$memcache = memcache_connect('10.0.1.200', 11711);
$domain_index = $memcache->increment('last_index', 1);
if ($domain_index === false || $domain_index > 100000)
{
$memcache->set('last_index', 0, null, 0);
$domain_index = 0;
}
$domains = array(
'www1.smartdeveloper.cn',
'www2.smartdeveloper.cn',
'www3.smartdeveloper.cn'
);
$domain = $domains[$domain_index % count($domains)];
header("Location: http://$domain");
?>

可以看到,我们利用 Memcache 服务器创建了一个共享计数器 last_index,在每次转移请求之前,都要对计数器进行递增操作, 这是一个原子操作,所以保证了计数器对于所有请求的一致性。我们来对这个新的首页程序进行压力测试,结果如下所示

这次的首页吞吐率只有前边的 53%,而实际服务器的最大数量也由前面的 13 台变为 7 台,这意味着整个系统的承载能力缩水 了一半左右。

 如此大幅度的性能下降,也印证了前面我们对于顺序转移性能代价的分析,另一方面,也许你觉得这与我们使用 Web 应用程 序来实现 RR 调度策略有关,那么,我们来看看 Apache 的 mod_rewrite 模块,它可以轻松地支持 RR 重定向,因为 Web 服务 器要维护一个全局资源并不困难。 

我们对 Apache 的 Vhost 进行以下配置

<VirtualHost *:80>
DocumentRoot /data/www/highperfweb/htdocs
ServerName www.highperfweb.com
RewriteEngine on
RewriteMap
lb
prg:/data/www/lb.pl
RewriteRule
^/(.+)$ ${lb:$1}
</VirtualHost>

这里可以看到我们引入了一个第三方脚本 lb.pl,它是 mod_rewrite 支持的一种可编程模式,这个脚本会跟随 Apache 一起启动, 并在自己的逻辑空间中运行,直到 Apache 停止后被释放。我们来看看这个脚本的代码,如下所示:

#!/usr/bin/perl
$| = 1;
$name
= "www";
$first
= 1;
$last
= 3;
$domain = "highperfweb.com";
$cnt = 0;
while (<STDIN>)
{
$cnt = ($cnt + 1) % $last;
$server = sprintf("%s%d.%s", $name, $cnt + $first, $domain);
print "http://$server/$_";
}

这个脚本不难理解,Apache 每次接入新的请求后,便会触发 while 循环,这时候程序会计算出当前应该使用的实际服务器序 号,然后根据事前定义的规则组合成最终实际服务器的域名。我们再次通过 ab 进行同样的压力测试,结果如下所示:

从结果来看,吞吐率并没有提高,而且更重要的是,在这次压力测试中,失败率非常高,Length 为 4035,达到总请求数的 40% 左右,这意味着 ab 认为有 4035 个请求的响应数据长度可疑,也就是说这些请求的响应数据可能不是预期的正确结果。为了 获得失败的处理结果,我们在压力测试的同时,通过其他会话进行请求,偶尔会获得如下所示的结果

s-colin:~ # curl -i http://www.highperfweb.com/
HTTP/1.1 400 Bad Request
Date: Wed, 20 May 2009 08:37:22 GMT
Server: Apache/2.2.11 (Unix) PHP/5.2.1 DAV/2 SVN/1.4.3
Content-Length: 226
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body></html>

可见,这种失败的处理结果是非常严重的。我们将压力测试的模拟并发用户数从 500 降到了 2,失败率仍然为总请求数的 10% 左右,当我们将模拟并发用户数降为 1 的时候,全部请求都处理成功,结果如下所示:


的确,我们看到 Apache 在并发环境下对于 mod_rewrite 模块的可编程脚本机制的处理很不理想,当然,这取决于 Apache 在 该处的实现。理论上在 Web 服务器中实现专用的重定向策略逻辑,其性能肯定要优于通过 Web 应用程序来实现。也许你可以 自己编写专用的 Web 服务器或者插件来实现高效的 RR 重定向,但是,无论如何,顺序调度的性能总是比不上随机调度的性 能,特别是在这里,负责转移请求的逻辑本身并没有太大开销,所以更加凸显了顺序调度的性能代价。

 如果你使用 RR 策略的目的在于希望做到请求转移的绝对均衡,那么可以肯定的一点是,根据概率统计理论,随着吞吐率的 增加,随机调度也会逐渐趋近于顺序调度的均衡效果。所以,你会考虑使用需要一定性能代价的 RR 方式吗?

 这里顺便提一下,你也可以考虑使用其他的调度策略,比如根据用户的 IP 地址来散列计算实际服务器序号,这样做同样可以 避免 RR 策略的性能代价,事实上凡是 HTTP 请求处理逻辑本身可以独立决策的调度策略,都拥有较高的性能表现。另外, 这种根据用户 IP 地址来转移请求的策略可以使得用户每次都访问同一台实际服务器,这种策略在后面我们会再次介绍,它为 一些 Web 应用提供了很好的支持。


更多考虑 

前面我们费尽心思来实现请求转移次数的均衡,我们知道这样做是没错的,它可以为多台实际服务器平均分配工作量,最大 程度地提高利用率。但是在实际环境中,次数的均衡真的代表负载的均衡吗?我们必须思考这个问题。

 你是否还记得我们刚才的例子都是针对站点首页的重定向,也就是站点的入口,而站点内页的链接地址我们一般在首页上采 用相对地址或者根相对地址,这样一来,一旦用户通过入口时被转移到某台实际服务器后,用户的一系列请求将直接进入这 台实际服务器,我们知道不同用户对站点内页的访问深度是不同的,这也是我们无法控制的,这样一来,多台实际服务器的 负载差异是不可预料的,而主站点对此却一无所知。

 而更加让人无奈的是,你无法保证用户始终从站点首页进入站点,也就是说总有或多或少的用户会跳过你精心设计的主站点 调度程序,这的确让人很尴尬,也许用户已经收藏了某个实际服务器上的 URL,比如: http://www3.highperfweb.com/book/load_balancing.php

随后即便是你改变了转移策略,但这个 URL 仍然会指向原来的实际服务器,你对它毫无办法,只能想出各种歪门邪道的办法 来再次进行重定向,于是策略开始变得混乱和不可控。

 所以,大多数情况下通过重定向来实现整个站点的负载均衡,并不那么让人满意,随后我们会探讨其他的扩展方法来实现站 点负载均衡。 

相比之下,对于文件下载、广告展示等一次性的请求,主站点调度程序可以牢牢地把握控制权,实际服务器的 URL 甚至可以 含蓄地隐藏起来,与此同时,这种一次性的请求,也比较容易让多台实际服务器保持均衡的负载,但是也必须考虑一些现实 的问题,比如分配给不同实际服务器下载的文件可能尺寸差异较大,我们需要在次数分配均衡的情况尽量保证文件尺寸分配 均衡,也就是带宽使用分配均衡,这也许需要借助于应用程序,你可以记录下给每个实际服务器派发的下载文件的尺寸,从 而在每次下载转移前挑选你认为比较轻松的实际服务器,但这样做存在风险,你可能是在毫无根据地指手画脚,因为某个用 户可能请求下载一个 1GB 的文件,却在下载了 1%后突然终止,而你却毫不知情,仍然以为某台实际服务器在卖力工作,随后一段时间你对这台服务器实施保护,而它却无所事事。

 为了使提供下载服务的多台实际服务器比较均衡地使用带宽,另一个方法值得考虑,事实上它也是负载均衡系统中比较重要 的一部分,那就是负载反馈。在这里,我们可以让主站点的定时任务不断获取每个实际服务器的实时流量,这可以通过 SNMP 获得原始数据并计算得出,这些数据将作为下载转移的权重参考。也许你觉得在请求转移逻辑中加入各实际服务器流量权重 分析会带来额外的开销,但是,相比于下载的时间开销而言,这些额外的开销实在是九牛一毛。 

刚才提到的下载服务只是一个例子,除此之外,对于不同的应用场景,我们仍然需要认真考虑基于重定向的负载均衡是否适 用,虽然我们不能一一列举,但是有一点是可以肯定的,我们需要权衡转移请求的开销和处理实际请求的开销,前者相对于 后者越小,那么重定向的意义就越大,刚才的下载转移就是个很好的例子



小说《我是全球混乱的源头》
此文章本站原创,地址 https://www.vxzsk.com/1042.html   转载请注明出处!谢谢!

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