生成式AI领域的最新成果都在这里!抢 QCon 展区门票 了解详情
写点什么

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:5432468

评论

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

即构小程序直播组件集成教程

ZEGO即构

矿机挖矿APP系统模式开发平台

v16629866266

在数据分析、挖掘方面,有哪些ETL工具值得推荐?

TASKCTL

大数据 kettle 海豚调度 批量任务 ETL

自动量化搬砖套利交易机器人系统软件APP开发

系统开发

一文带你探究Sentinel的独特初始化

华为云开发者联盟

redis sentinel 框架

区块链十年与传统金融的变化

CECBC

区块链 金融

敏捷里为何倡导固定迭代周期?

万事ONES

敏捷开发 研发管理 迭代

从CPU到XPU进化,英特尔对业界放了什么大招?

E科讯

遇到代码缺陷不要慌,马上教你快速检测和修复

华为云开发者联盟

代码 bug 缺陷检测 代码缺陷

Kubernetes概念篇:基本概念和术语

xcbeyond

Kubernetes 容器 pod 28天写作 Kubernetes从入门到精通

万字多图 | UML 入门指南

白色蜗牛

Java 程序员 后端 架构设计 UML

架构师训练营第十三周作业

李日盛

PageRank

区块链科普系列:区块链是什么?

CECBC

区块链

“反垄断”来袭,对产业区块链有什么启发

CECBC

市场垄断

特斯拉自建ERP的背后

明道云

当音乐学博士搞起编程...

程序猿DD

Spring Frame

Volcano架构设计与原理介绍

华为云原生团队

大数据 AI 云原生 高性能 批量计算

「产品经理训练营」第一章作业

Sòrγy_じò ぴé

产品经理训练营

物流快递公司APP架构设计

jorden wang

初识ClickHouse——安装与入门

Simon

Clickhouse

面试官:你真的了解Redis分布式锁吗?

鄙人薛某

redis 分布式锁 线程安全 RedLock

用AI「驯服」人类幼崽,手头有娃的可以试试

博文视点Broadview

人工智能 联邦学习 强化学习 集成学习 技术宅

不同公司产品经理岗位对比

LouisN

为什么我认为 Deno 是一个迈向错误方向的 JavaScript 运行时?

hylerrix

typescript rust nodejs deno V8

iOS性能优化 — 五、App启动优化

iOSer

ios 性能优化 性能分析

『CDN』让你的网站访问起来更加柔顺丝滑

古时的风筝

CDN

来不及解释!Linux常用命令大全,先收藏再说

华为云开发者联盟

Linux 编程 命令行 命令

生产者与消费者模式,数组阻塞队列(ArrayBlockingQueue)

码农架构

Java 学习 架构 并发编程

一文解析DDD中台和微服务设计

欧创新

中台 微服务 领域驱动设计 DDD

DevSecOps安全检查清单

啸天

安全 DevSecOps 应用安全

架构师训练营W13作业

Geek_f06ede

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