最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

Twitter 的 RPC 框架 Finagle 简介

  • 2014-05-30
  • 本文字数:4138 字

    阅读完需:约 14 分钟

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)的框架。

Finagle**** 的工作原理

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 之中读取信息,并发送到所实现的服务器实例中。

  1. Finagle 客户端位于 Finagle Transport 之上,这个 Transport 为用户抽象了 Netty;
  2. Netty ChannelPipeline 包含了所有的 ChannelHandler 实现,这些实现完成实际的工作;
  3. 对于每一个连接都会创建 Finagle 服务器,并且会为其提供一个 Transport 来进行读取和写入;
  4. ChannelHandler 实现了协议的编码 / 解码逻辑、连接级别的统计以及 SSL 处理。

桥接Netty与 ****Finagle

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。

Finagle**** 的抽象

Finagle 的核心概念是一个简单的函数(在这里函数式编程很关键),这个函数会从 Request 生成包含 Response 的 Future:

复制代码
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))
}
<p>Http.serve(":80", s)</p>

这个样例在所有接口的 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 有助于促进模块化和可重用性。

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 铺平道路。

关于 Finagle 的更多文档和样例,可以参考其站点

2014-05-30 07:5432479

评论

发布
暂无评论
发现更多内容

【LeetCode】奇数值单元格的数目Java题解

Albert

LeetCode 7月月更

TDesign 组件库技术方案指北

TDesign

开源 Vue React 组件库

Node.js异步编程之Promise

是乃德也是Ned

node.js 前端 7月月更

QT和MFC的优缺点比较

乌龟哥哥

7月月更

你想知道的数组易错知识都在这了-C

芒果酱

7月月更

Wireshark抓包分析Eureka注册发现协议

程序员欣宸

Java SpringCloud Eureka 7月月更

iOS 中的 Category

NewBoy

ios 前端 移动端 iOS 知识体系 7月月更

纯CSS实现四种方式文本反差色效果

南城FE

CSS 前端 css动画 7月月更 反差色

面对裁员?焦虑?不如好好投资自己

沃德

程序员 7月月更

《深入 Linux 设备驱动程序那和机制》读书笔记

贾献华

7月月更

深入JS函数中默认参数的使用

猪痞恶霸

前端 js 7月月更

图解网络:访问控制列表ACL,功能堪比防火墙!

wljslmz

防火墙 acl 网络技术 7月月更 访问控制列表

多传感器时间同步

秃头小苏

时间同步 7月月更

QT实现 文件夹复制

小肉球

qt 7月月更

5G NR RRC连接控制

柒号华仔

5G 7月月更

不习惯的Vue3起步四 の 生命周期&provide/inject

空城机

Vue3 7月月更

Qt | 串口通信 QSerialPort

YOLO.

通信 qt 串口 串口通信 7月月更

Pro 单店版和多店版,你还傻傻分不清楚?

CRMEB

Linux 0.12 源码阅读

贾献华

7月月更

SLSA 框架与软件供应链安全防护

SEAL安全

SLSA 软件供应链安全

适合初学者的 10 个JavaScript 代码整洁技巧

devpoint

JavaScript ES6 7月月更 数据操作

Java面向对象基础

五分钟学大数据

Java 7月月更

在线直播系统源码——开源源码

开源直播系统源码

软件开发 直播源码 开源源码 在线直播系统源码

项目git commit时卡主不良代码:husky让Git检查代码规范化工作

zhoulujun

git husky lint-stated

自己搭建git服务器:linux自己Gitlab服务

zhoulujun

gitlab git私有参考 git部署 git服务端

Qt|字符排序以及转化问题

中国好公民st

qt 7月月更

GaussDB云数据库初步使用记录

DS小龙哥

7月月更

老司机狂飙之路--EventBus原理简要分析

芝麻粒儿

android EventBus 7月月更

RocketMQ之消费者启动与消费流程

vivo互联网技术

RocketMQ 中间件 ACK

注意!软件供应链安全挑战持续升级

SEAL安全

安全 软件供应链

使用AssemblyScript 构建 WebAssembly 应用

devpoint

webassembly Wasm 7月月更 assemblyscript

Twitter的RPC框架Finagle简介_Java_张卫滨_InfoQ精选文章