Finagle是Twitter基于Netty开发的支持容错的、协议无关的RPC框架,该框架支撑了Twitter的核心服务。来自Twitter的软件工程师Jeff Smick撰文详细描述了该框架的工做原理和使用方式。
在Jeff Smick的博客文章中,介绍了Twitter的架构演进历程。Twitter面向服务的架构是由一个庞大的Ruby on Rails应用转化而来的。为了适应这种架构的变化,须要有一个高性能的、支持容错的、协议无关且异步的RPC框架。在面向服务的架构之中,服务会将大多数的时间花费在等待上游服务的响应上,所以使用异步的库可以让服务并发地处理请求,从而充分发挥硬件的潜能。Finagle构建在Netty之上,并非直接在原生NIO之上构建的,这是由于Netty已经解决了许多Twitter所遇到的问题并提供了干净整洁的API。
Twitter构建在多个开源协议之上,包括HTTP、Thrift、Memcached、MySQL以及Redis。所以,网络栈须要足够灵活,以保证能与这些协议进行交流而且还要具备足够的可扩展性以支持添加新的协议。Netty自己没有与任何特定的协议绑定,对其添加协议支持很是简单,只需建立对应的事件处理器(event handler)便可。这种可扩展性产生了众多社区驱动的协议实现,包括SPDY、PostrgreSQL、WebSockets、IRC以及AWS。
Netty的链接管理以及协议无关性为Finagle奠基了很好的基础,不过Twitter的有些需求是Netty没有原生支持的,由于这些需求都是“较高层次的”。好比,客户端须要链接到服务器集群而且要进行负载均衡。全部的服务均须要导出指标信息(如请求率、延迟等),这些指标为调试服务的行为提供了有价值的内部信息。在面向服务架构中,一个请求可能会通过数十个服务,因此若是没有跟踪框架的话,调试性能问题几乎是不可能的。为了解决这些问题,Twitter构建了Finagle。简而言之,Finagle依赖于Netty的IO多路复用技术(multiplexing),并在Netty面向链接的模型之上提供了面向事务(transaction-oriented)的框架。web
Finagle强调模块化的理念,它会将独立的组件组合在一块儿。每一个组件能够根据配置进行替换。好比,全部的跟踪器(tracer)都实现了相同的接口,这样的话,就能够建立跟踪器将追踪数据存储到本地文件、保持在内存中并暴露为读取端点或者将其写入到网络之中。
在Finagle栈的底部是Transport,它表明了对象的流,这种流能够异步地读取和写入。Transport实现为Netty的ChannelHandler,并插入到ChannelPipeline的最后。当Finagle识别到服务已经准备好读取数据时,Netty会从线路中读取数据并使其穿过ChannelPipeline,这些数据会被codec解析,而后发送到Finagle的Transport。从这里开始,Finagle将数据发送到本身的栈之中。对于客户端的链接,Finagle维持了一个Transport的池,经过它来平衡负载。根据所提供的链接池语义,Finagle能够向Netty请求一个新的链接,也能够重用空闲的链接。当请求新的链接时,会基于客户端的codec建立一个Netty ChannelPipeline。一些额外的ChannelHandler会添加到ChannelPipeline之中,以完成统计(stats)、日志以及SSL的功能。若是全部的链接都处于忙碌的状态,那么请求将会按照所配置的排队策略进行排队等候。
在服务端,Netty经过所提供的ChannelPipelineFactory来管理codec、统计、超时以及日志等功能。在服务端ChannelPipeline中,最后一个ChannelHandler是Finagle桥(bridge)。这个桥会等待新进入的链接并为每一个链接建立新的Transport。Transport在传递给服务器实现以前会包装一个新的channel。而后,会从ChannelPipeline之中读取信息,并发送到所实现的服务器实例中。编程
Finagle客户端使用ChannelConnector来桥接Finagle与Netty。ChannelConnector是一个函数,接受SocketAddress并返回Future Transport。当请求新的Netty链接时,Finagle使用ChannelConnector来请求一个新的Channel,并使用该Channel建立Transport。链接会异步创建,若是链接成功的话,会使用新创建的Transport来填充Future,若是没法创建链接的话,就会产生失败。Finagle客户端会基于这个Transport分发请求。
Finagle服务器会经过Listener绑定一个接口和端口。当新的链接建立时,Listener建立一个Transport并将其传入一个给定的函数。这样,Transport会传给Dispatcher,它会根据指定的策略未来自Transport的请求分发给Service。json
Finagle的核心概念是一个简单的函数(在这里函数式编程很关键),这个函数会从Request生成包含Response的Future:api
type Service[Req, Rep] = Req => Future[Rep]
Future是一个容器,用来保存异步操做的结果,这样的操做包括网络RPC、超时或磁盘的I/O操做。Future要么是空——此时还没有有可用的结果,要么成功——生成者已经完成操做并将结果填充到了Future之中,要么失败——生产者发生了失败,Future中包含告终果异常。这种简单性可以促成很强大的结构。在客户端和服务器端,Service表明着相同的API。服务器端实现Service接口,这个服务器能够用来进行具体的测试,Finagle也能够将其在某个网络接口上导出。客户端能够获得Service的实现,这个实现能够是虚拟的,也能够是远程服务器的具体实现。
好比说,咱们能够经过实现Service建立一个简单的HTTP Server,它接受HttpReq并返回表明最终响应的Future[HttpRep]:安全
val s: Service[HttpReq, HttpRep] = new Service[HttpReq, HttpRep] { def apply(req: HttpReq): Future[HttpRep] = Future.value(HttpRep(Status.OK, req.body)) } Http.serve(":80", s)
这个样例在全部接口的80端口上暴露该服务器,而且经过twitter.com的80端口进行使用。可是,咱们也能够选择不暴露服务器而是直接使用它:服务器
server(HttpReq("/")) map { rep => transformResponse(rep) }
在这里,客户端代码的行为方式是同样的,可是并不须要网络链接,这就使得客户端和服务器的测试变得很简单直接。
客户端和服务器提供的都是应用特定的功能,但一般也会须要一些与应用自己无关的功能,举例来讲认证、超时、统计等等。Filter为实现应用无关的特性提供了抽象。网络
Filter接受一个请求以及要进行组合的Service:架构
type Filter[Req, Rep] = (Req, Service[Req, Rep]) => Future[Rep]
在应用到Service以前,Filter能够造成链:并发
recordHandletime andThen traceRequest andThen collectJvmStats andThen myService
这样的话,就可以很容易地进行逻辑抽象和关注点分离。Finagle在内部大量使用了Filter,Filter有助于促进模块化和可重用性。app
Filter还能够修改请求和响应的数据及类型。下图展示了一个请求穿过过滤器链到达Service以及响应反向穿出的过程:
在扩展性的架构中,失败是常见的事情,硬件故障、网络阻塞以及网络链接失败都会致使问题的产生。对于支持高吞吐量和低延迟的库来讲,若是它不能处理失败的话,那这样库是没有什么意义的。为了获取更好的失败管理功能,Finagle在吞吐量和延迟上作了必定的牺牲。
Finagle可使用主机集群实现负载的平衡,客户端在本地会跟踪它所知道的每一个主机。它是经过计数发送到某个主机上的未完成请求作到这一点的。这样的话,Finagle就能将新的请求发送给最低负载的主机,同时也就能得到最低的延迟。
若是发生了失败的请求,Finagle会关闭到失败主机的链接,并将其从负载均衡器中移除。在后台,Finagle会不断地尝试从新链接,若是Finagle可以从新创建链接的话,就会再次将其添加到负载均衡器之中。
Finagle将服务做为函数的理念可以编写出简单且具备表述性的代码。例如,某个用户对其时间线(timeline)的请求会涉及到多个服务,核心包括认证服务、时间线服务以及Tweet服务。它们之间的关系能够很简洁地进行表述:
val timelineSvc = Thrift.newIface[TimelineService](...) // #1 val tweetSvc = Thrift.newIface[TweetService](...) val authSvc = Thrift.newIface[AuthService](...) val authFilter = Filter.mk[Req, AuthReq, Res, Res] { (req, svc) => // #2 authSvc.authenticate(req) flatMap svc(_) } val apiService = Service.mk[AuthReq, Res] { req => timelineSvc(req.userId) flatMap {tl => val tweets = tl map tweetSvc.getById(_) Future.collect(tweets) map tweetsToJson(_) } } } //#3 Http.serve(":80", authFilter andThen apiService) // #4 // #1 建立每一个服务的客户端 // #2 建立过滤器来认证传入的请求 // #3 建立服务,将已认证的时间线请求转换为json响应 // #4 使用认证过滤器和服务,在80端口启动新的HTTP服务器
在上面的例子中,建立了时间线服务、Tweet服务以及认证服务的客户端,建立了一个Filter来认证原始的请求,最后,实现了服务,将其与认证过滤器组合起来,并暴露在80端口上。 当收到请求时,认证过滤器会尝试进行认证,若是失败的话就会当即返回并不会影响到核心服务。认证成功后,AuthReq将会发送到API服务上。服务会使用附带的userId借助时间线服务查找该用户的时间线,这里会返回一个tweet id的列表,而后对其进行遍历获取关联的tweet,最终请求的tweet列表会收集起来并做为JSON返回。在这里咱们将并发的事情所有留给了Finagle处理,并不用关心线程池以及竞态条件的问题,代码整洁清晰而且很安全。 以上介绍了Finagle的基本功能以及简单的用法。Finagle支撑了Tweet巨大的网络传输增加,同时还下降了延迟以及对硬件的需求。目前,Finagle与Netty社区积极合做,在完善产品的同时,也为社区作出了贡献。Finagle内部会更加模块化,从而为升级到Netty 4铺平道路。