快速可扩展的Ajax流代理——提供持续下载跨域数据
简介
由于浏览器禁止跨域的XMLHTTP调用,所有的Ajax网站都必须有一个服务端代理来从外部域比如Flickr或者Digg来抓去内容。对客户端Javascript代码来说,一个XMLHttp的调用将请求传递给宿主在相同域里的服务端代理,然后由代理来从外部服务器上下载内容,并回传给客户端。通常,所有从外部服务器获取内容的Ajax站点都采用这种代理方案,除了一些罕见的使用JSONP的人。当网站上的许多组件正在从外部域下载内容时,这样的代理将会被大量地调用。所以,当代理开始被百万次地调用时,它将变成一个可扩展的问题。另外,一个页面整体的负载均衡很大程度上依赖于当代理向页面提供内容时它的性能。这篇文章,我们来看看我们如何能将传统的Ajax代理变得更快,异步,持续提供内容流,从而使其更具扩展性。
Ajax代理 进行时
当你访问Pageflakes.com时,你可以看到这样的代理正在工作。你将看到许多不同的内容,例如天气预报、flickr图片、YouTuBe的视频以及RSS等,像一个个部件一样从许多不同的外部域中被加载。所有这些加载都需要使用一个内容代理。该内容代理在最近一个月为差不多42.3百万的URL提供过服务。使它既快又有扩展性,是对我们相当大的挑战!有时内容代理需要提供兆字节数据的服务,这更加提高了这些挑战的高度。因为这样的代理会被大量的调用,如果我们能够为每个调用平均节约大概100ms,我们每个月就可以为下载、上传、处理的时间节约大概4.23百万秒。这大概1175人时都被全世界百万人在浏览器前等待下载内容而浪费掉了。
这样的内容代理将外部服务器的URL作为一个查询参数。它从这些URL下载内容,然后将这些内容作为响应回传给服务器。
图片:内容代理像一个中间人一样在浏览器和外部域之间工作。
上面的时间线显示了,一个请求如何到达服务器,然后服务器向外部服务器发出请求,下载响应,并将它传输给客户端。从代理到浏览器的响应箭头比从外部服务器到代理的箭头更长,这是因为通常一个代理服务器的宿主环境比用户的互联网连接有更快的下载速度。
一个基本的代理
这种内容代理也同样存在与我的开源Ajax网站——Dropthings.com中。你可以去CodePlex看它的代码是如何实现这样一个代理的。
下面是一个非常简单,同步,没有流的阻塞代理。
![](http://hi.csdn.net/attachment/201110/20/0_1319119759uT4U.gif)
尽管它显示了通常的原则,但它没有接近一个真实的代理,因为:
(1) 他是一个同步代理,因此没有可扩展性。每一个对该Web方法的调用,都导致Asp.net线程处于等待状态,直到对外部URL的调用完成。
(2) 它是非流式的。它第一次从服务器上下载整个内容,将内容存储在一个字符串中,然后向浏览器更新整个内容。如果你点击一个MSDN Feed URL,它将从服务器下载220KB的巨大的RSS XML,将它存储到一个220KB的长字符串中(总得来说,是.net内置String类型的双倍大小,并且都是Unicode字符),然后将这220KB写到asp.net响应对象(Response)的缓冲区(buffer)中,并将另外的220KB的UTF8的字节数组存储在内存中。然后,那220KB将被传递到IIS,以便可以传输到浏览器。
(3) 它没有在服务端存产生一个正确的响应头来缓存响应。它也没有从源文件中提供重要的头部,例如Content-Type。
(4) 如果一个外部URL提供对内容的GZIP压缩,它解压内容到一个字符串来表示,因此它浪费了服务器的内存。
(5) 它没有在服务端缓存内容。因此,重复对相同外部URL的调用也将从外部URL重新下载数据,因此浪费了你服务端的带宽。
我们需要一个异步的streaming proxy当它从外部域服务器下载后传输内容到浏览器。因此,它将从外部服务器下载字节流到一个小块儿中,并且直接将其传输到浏览器。结果是,在调用Web Service之后浏览器将看到一个持续的字节传输。当内容已经完全从服务器上下载下来后,将没有延迟。
一个更好的代理
之前,我展示了复杂的基于流代理的代码,让我们讨论一个改进式的方案。让我们建立一个比上面更好的内容代理。上面的代理是一个同步并且非流式,但没有其他问题的代理。我们将构建一个命名为Regular.ashx的HTTP Handler,它将把URL作为查询参数。它也将缓存作为一个查询参数,该查询参数将用来产生一个正确的响应头来在浏览器上缓存内容。因此,它将一次又一次减少浏览器重复下载相同内容的时间。
![](http://hi.csdn.net/attachment/201110/20/0_13191198083kD7.gif)
上面的代理主要增强了两点功能:
l 它允许服务端缓存内容。在一段时间内来自不同浏览器的相同URL请求,在服务端将不会再次下载,而是从服务端缓存中获取数据。
l 它产生一个正确的输出响应头,可以让内容缓存到浏览器端。
l 它没有在内存中解压下载的内容。它保持原始字节流的完整。它节省了内存。
l 它用一种无缓冲的形式发送数据,这意味着asp.net的Response对象没有缓冲响应,因此节约了内存。
然而,这是一个“阻塞”式的代理。更好的代理——基于流
我们需要构建一个基于流的异步代理来提供更好的性能。下面的图阐述了这是为什么:
![](http://hi.csdn.net/attachment/201110/20/0_1319119865lLdG.gif)
图片:持续的流式代理
就像你所看到的,当服务器下载内容的时候,数据从服务器被发送回浏览器,服务端下载的延时被消除。因此,如果服务器花300ms来从一个外部资源下载内容,然后花700ms将它发送回服务器,你就可以在服务器和浏览器之间节约300ms的网络延迟(因为异步就会边下载边传输数据流)。这种方案在当外部服务器提供的内容非常慢并且需要花相当长的一段时间来提供内容时效果非常好。外部站点越慢,采用这种持续流式代理,你节约的时间就越多。当你的站点很远时,这比起阻塞式的方案在性能上是一个很大的提升。
这种持续式的代理方案是:
l 用一个特殊的线程(读取器线程)来从外部服务器读取一个8KB的字节块,目的是使其不阻塞。
l 将读取到的块存储在一个被称为管线流的内存队列里面。
l 从该队列里将块写到Asp.net的Response对象。
l 如果队列完成,使其处于等待状态直到更多的字节被读取器线程下载。
![](http://hi.csdn.net/attachment/201110/20/0_13191199047800.gif)
这种管线流需要是线程安全的,并且它需要支持“阻塞式”的读取。“阻塞式”的读取,这意味着如果一个线程尝试读取一个块儿并且流是空的,将暂停该线程直到另一个线程在流上写完东西。一旦在流上的一个写操作发生了,它将恢复读线程并且允许它继续读。我从CodeProjectarticle by James Kolpack那里获得了管线流的代码,并且测试了并确信它有很高的性能,支持存储字节块而不是单个字节,支持等待超时等等。
我将普通的代理(阻塞、同步、下载完所有之后才传输数据)和流式代理(从外部服务器到浏览器持续传输数据)做了一些对比。两个代理下载MSDN资源并且传输到浏览器。显示在下面的时间是从浏览器发出请求到代理然后回传整个响应到客户端。
这不是一个非常科学的图片,并且响应的时间还取决于从浏览器到代理服务器之间连接的速度和从代理服务器到外部服务器的速度。但,它显示了大部分时间里,流式代理比通常的代理表现出更好的性能。
构建流式代理
构建一个性能好于普通流式代理并不是那么简单。我尝试了三种方式,最终找出了最佳组合可以表现出比普通代理更好的性能。
该流式代理使用HttpWebRequest和HttpWebResponse来从一个外部服务器下载数据。它们被用来对如何读取数据取得更多得控制,更具体地说,是读取WebClient不提供的字节块儿。另外,这里有某些对构建一个代理需要的快速、可扩展HttpWebRequest的优化。
![](http://hi.csdn.net/attachment/201110/20/0_1319120087Z77M.gif)
DownloadData方法从输出流(连接到外部服务器)下载数据,然后将其发送给asp.net的Response流。
![](http://hi.csdn.net/attachment/201110/20/0_1319120129h1eL.gif)
这里,我曾尝试了三种不同的方案。一个是现在被注释掉的,叫做TransmitDataAsyncOptimized,是最好的解决方案。我将解释这三种方案。DownloadData方法的目的是在发送数据之前准备asp.net的输出流。然后,它使用这三种方案里的其中一种发送数据,并缓存下载的字节到内存流中。
第一种方案是从连接到外部服务器的输出流中读取8192字节,然后直接写到响应(TransmitDataAsyncOptimized)。
![](http://hi.csdn.net/attachment/201110/20/0_1319120181yY5M.gif)
这里,readStream是从HttpWebResponse.GetResponseStream调用返回的输出流。它从外部服务器下载。responseBuffer仅仅是用来在内存中存储整个响应一个内存流,目的是我们能够缓存它。
这个方案甚至比一个通常的代理更慢。在做了一些代码级别的性能分析之后,看起来写入OutputStream花费了相当长得时间,因为IIS尝试发送数据到浏览器。所以,这里会有网络延迟和传输数据的延迟。从频繁调用累积的网络延时到OutputStream.Write加起来显著地延迟了整个操作。
第二个方案是尝试多线程。一个从asp.net线程创建的新线程,持续地从Socket读取,甚至不用等待Response.OutputStream发送字节到浏览器端。主asp.net线程等待直到所有的字节被收集完成,然后直接传输它们到response。
![](http://hi.csdn.net/attachment/201110/20/0_1319120239m5z6.gif)
这里,读取是在PipeStream上进行的,而不是从asp.net线程的socket上。这里有一个新线程被催生,它将数据写入PipeStream就像它从外部站点下载字节一下。结果,我们有asp.net线程持续地将数据写入OutputStream,并且有另外一个线程从外部服务器不断地下载数据。接下来的代码从外部服务器下载数据,然后存储到PipeStream中。
![](http://hi.csdn.net/attachment/201110/20/0_1319120276cAbv.gif)
这个方案的问题是,仍然有很多Response.OutputStream.Write的调用发送。外部服务器发送各种不同字节数的内容,有时3592字节,有时8192字节,并且有时仅仅501字节。它完全依赖于从你的服务器到外部服务器的连接有多快。通常,微软的服务器都是非常快速的,当你调用_ResponseStream.Read从MSDN读取资源的时候,你总是能获得8192(缓存区的最大容量)的字节,但,当你连接到一个非可靠的服务器,例如在澳大利亚,你将无法在每次读取调用时都获得8192个字节的数据。所以你将以超过你对Response.OutputStream.Write预期调用次数而结束。所以,一个更好的最终方案是介绍另一个缓冲区,它将存储将写到asp.netResponse的字节,并且一旦8192个字节已经准备好传输时它将自己清空缓冲区到Response.OutputStream。在这中间缓冲区将确保总是有8192个字节被发送到Response.OutputStream。
![](http://hi.csdn.net/attachment/201110/20/0_1319120313ylwA.gif)
![](http://hi.csdn.net/attachment/201110/20/0_131912032928W1.gif)
上面的方法确保一次仅有8192字节被写入asp.netResponse.Stream中。以这种方式,写的次数是总字节数/8192。
用异步httphandler构建流式代理
现在,我们正基于流来传输字节,我们需要让这个代理“异步”让它不把持着asp.net的主线程太长时间。变成异步,这意味着一旦asp.net线程向外部服务器发出一个调用,它就会被释放。当外部服务器调用完成并且字节都下载完成,它将从asp.net抢占一个线程然后完成执行。
当代理不是异步的时候,它会使得asp.net线程很繁忙,直到整个的连接以及下载操作完成。如果外部服务器响应很慢,那它就没有必要持有asp.net线程太长时间。结果如果代理正对一个很慢的服务器发起太多的请求,asp.net线程不久就将消耗殆尽,并且你的服务器将停止响应任何新请求。
构建一个异步代理的第一步是实现IhttpAsyncHandler然后将ProcessRequest方法分成两部分:BeginProcessRequest和EndProcessRequest。Begin方法将对HttpWebRequest.BeginGetResponse发出一个调用,然后线程返回到asp.net的线程池。
![](http://hi.csdn.net/attachment/201110/20/0_13191203714564.gif)
当BeginProcessRequest调用完成并且外部服务器已经开始向我们发送响应数据,asp.net会调用EndProcessRequest方法。该方法从外部服务器下载数据,然后发送回浏览器端。
![](http://hi.csdn.net/attachment/201110/20/0_1319120446yz5x.gif)
现在你已经拥有了它——一个快速的,可扩展的,持续流式的代理,它总是会比通常的代理有更好的性能。
如果,你正在考虑写HttpHelper,AsyncState,以及SyncResult类,这里有一些现成的类,下面是这些帮助类的代码:
![](http://hi.csdn.net/attachment/201110/20/0_13191204795bH6.gif)
源代码下载地址: