”Twitter搜索现在快3倍啦“

在2010年春季,为了服务不断增长的流量、提升最终用户的响应时延和服务的可用性、有能力快速开发新的搜索功能,搜索团队开始重写我们的搜索引擎。作为不努力的一部分,我们发布了新的实时搜索引擎,将后端从Mysql迁到了lucene的实时版。上周我们发布新的前端,替换了Ruby on Rails版:我们称之为Blender的Java服务器。我们很高兴地宣布这些改变使我们的搜索时延下降了3倍,同时也使得我们有能力在接下来的几个月里快速迭搜索特性。

性能收益

Twitter搜索是世界上注量最重的搜索引擎之一,每天服务超过10亿的查询。在我们发布Blender的前一周,日本 #tsunami 产生了很大的查询负载,并产生了查询响应时间的峰值。随着Blender的发布,我们的95%时延减少了3倍,从800毫秒到250毫秒,同时前端的CPU负载降低了一半。我们每台机器的容量能服务10倍的请求增长。这也意味着我们能能够用更少的机器支持同样的请求数,从而降低前端服务的成本。

Twitter搜索现在快3倍啦

blender发布前后的95%的查询接口时延对比图

Twitter改进后的搜索架构

为了弄明白这些性能收益,你首先必须明白我们以前的RoR前端是如何地低效。前端运行固定数量的单线程工作进程,每一个做下面的事情:

. 解析请求
. 同步查询索引服务
. 聚合并展现结果

我们很早就知道,同步方式处理请求会低效地使用我们的CPU。经过一段时间,在Ruby的代码层累积欠了重大的技术债,这样很难增加新特性和改进我们搜索系统的可靠性。

Blender通过下面的方式解决了这些问题:

 创建了一个完全异步的聚合服务。没有线程会去等待网络IO完成
. 从后端服务中聚合结果,例如,实时的,最流行的微推和地理分片
. 优雅的服务之间的依赖关系处理,工作流自动处理后端服务之间的传递性依赖

下图显示了Twitter的搜索引擎架构。从网站、接口或内部客户过来的查询请求通过硬件负载均衡设备转发到Blender上。Blender解析请求,然页转发给后端服务,使用工作流来处理服务之间的依赖。最后,将后端服务返回的结果进行合并,以合适的语言呈现给客户。

Twitter搜索现在快3倍啦
Twitter跟Blender有关的搜索架构图

Blender简介

Blender基于Netty http://www.jboss.org/netty打造的Thrift和HTTP服务,Netty是用Java写的高扩展的客户和服务器库,基于它能快速地开发各种协议的客户和服务器端程序。我们没有用其它产品,如Mina和Jetty,主要是因为在其它项目中已经用过它,当然它的接口更简洁、文档也写得不错,这也是我们用它的原因之一。为了让Netty跟Trift一起工作,我们写了解析Thift的代码,专门解析从Netty的channel缓存中的过来的Thift请求,当它从socket读请求或需要封装Thrift响应时,都会调用它些代码。

Netty定义名叫通道的抽象功能,用来封装到网络套接字的连接,提供读、写和连接等IO请求的接口。所有通道的IO请求天然都是异步的。这意味着任何IO调用立即返回一个通道实例对象,以通知请求是否完成、失败或被取消。

当Netty服务器收到一个新的连接,它会创建一个通道流水线来处理这个连接。一个通道流水线不是别的,其实就是通道处理程序的序列,通过它实现处理连接的业务逻辑。在随后的一个小节里,我们会说一下Blender是如何将这些通道流水线映射到查询的工作流处理上的。

工作流框架

对于Blender来说,一个工作流就是一组后端服务,服务之间有依赖关系,每个后端服务都需要处理才能处理收到的查询请求。Blender自动解决后端服务之间的依赖,例如,如果服务A依赖于服务B,首先会请求A,把它返回的结果返回给B。工作流可以很方便地用有向无环图来表示(见下图)

Twitter搜索现在快3倍啦
6个后端服务的Blender工作流示例图

在上面的示例图中,我们有6个服务{s1,s2,s3,s4,s5,s6},之间有依赖。从S3到S1的边意味着先要访问S3,之后才是S1,因为访问S1时需要访问S3后的结果。对于这样的一个工作流,Blender框架先会进行拓扑排序以确实所有服务的顺序,它们也必須以这样的顺序依次调用。上图的服务执行顺序将是{(s3,s4),(s1,s5,s6),(s2)}。这也表明s3和s3可以在第一轮里并发调用,一旦它们返回结果,S1、S5和S6可以在第二轮里并发调用,最后才调用S2。

一旦Blender确定了工作流的执行顺序,工作流将会映射成Netty的流水线。流水线是一个处理程序序列,请求依次会通过这些处理程序。

复用进来的请求

因为工作流映射成了Netty的管道,我们需要将进来的客户端请求转发给合适的流水线。因为这个原因,我们设置了一个代理层,它负现复用并转发客户端请求到流水线上:

. 当一个远程Thrift客户端跟Blender建立一个长连接时,代理层会创建一组本地的客户,跟每个本地的工作流服务对应。注意当Blender进程起动时,会在同一个java虚拟机里起动所有的本地工作流服务
. 当从套接字上收到一个请求时,代理层读取这个请求,计算出需要请求那个工作流服务,然后转发给对应的工作流服务
. 类似地,当收到本地的工作流服务的响应时,代理层读取结果,并把结果转发给远程的客户

我们利用Netty的事件驱动模型来异步完成上面的所有任务,这样也没有任何线程需要等待IO。

分配后端请求

一旦请求转到了工作流流水线,将依次由工作流定义好的服务处理程序处理。每个服务处理程序创建一个合适的后端请求,转发给远程的服务器。例如,实时处理程序会创建一个实时搜索请求并异步地将它发给一个或多个实时索引服务器。我们使用twitter commons库https://github.com/twitter/commons(最近开源了!)来提供连接池管理、负载均衡和死主机检测。

当请求分配给所有后端之后,请求对应的IO线程就将被释放。一个定时线程每隔几毫秒就检查有没有后端的请求返回,并设置对应的标记,表明请求成功、超时或者失败了。在一个查询请求的存在周期内我们为它维持一个对象,用来记录上面这些状态信息。

成功的响应会聚合在一起,再发给工作流中的下一批处理程序。当第一批所有请求的响应的返回之后,将异步地发出第二批请求。这个过程会一直重复直到我们完成工作流或者超出工作流设置的超时时间。

你能够看出,在工作流执行期间内,没有任何线程会去等待IO。这让我们有效地利用Blender机器的CPU,并且处理大量的并发请求。我们能将大量请求并发发给后端去执行,从而也能减少请求的时延。

Blender部署及以后的工作

为了在我们的系统中增加Blender的时候,能确保提供高质量的服务。我们用老的RoR前端作为代理,转发请求给Blender集群。这样使得我们在对后边的技术进行大的调整时,给用户一个一致服务体验。我们下一阶段部署时,会从搜索的服务栈中完全去掉RoR,让用户直接去连接Blender,从而进一步降低访问时延。

原文链接:
Twitter Search is Now 3x Faster

添加评论