多语言微服务:与Apache Thrift的应用程序集成


现代软件系统生活在一个网络化的世界中。网络通信对于物联网中最小的嵌入式系统,直到锚定传统多层应用程序的最重要的关系数据库,都是至关重要的。随着新的软件系统越来越多地采用动态调度的容器化微服务,轻量级高性能语言不可知论的网络通信变得越来越重要。

但是如何把这些东西,新旧的,大的和小的连接在一起呢?我们如何将来自用一种语言编写的服务的消息以任何其他语言都可以读取的方式打包?我们如何设计对高性能后端云系统足够快,但可由前端脚本技术访问的服务?我们如何保持东西的轻量级,以支持高效的容器和嵌入式系统?我们如何创建能够随时间演进而又不破坏现有组件的接口?我们如何以一种开放的,与供应商无关的方式来完成所有这些工作,也许最重要的是,我们如何能够一次完成所有这些工作,在一个广泛的平台上重用相同的通信原语?对于Facebook,Evernote和Twitter这样的公司来说,答案是Apache Thrift。

本文介绍Apache Thrift框架及其在现代分布式应用程序中的作用。我们将看一看为什么要创建Apache Thrift,以及它如何帮助程序员构建高性能的跨语言服务。首先,我们将考虑日益增长的多语言集成需求,并研究Apache Thrift在多语言应用程序开发中扮演的角色。接下来,我们将研究通信框架的两个关键功能,并逐步构建一个简单的Apache Thrift服务。在本文的最后,我们将把Apache Thrift与其他几个提供类似特性的工具进行比较,以帮助您确定Apache Thrift何时更适合。

多语种,快乐与痛苦

近年来,通用商业程序设计语言的数量有了很大的增长。2003年Tiobe指数的80%(http://www.tiobe.com/index.php/tiobe_index)被认为是六种编程语言:Java,C,C++,Perl,Visual Basic和PHP.2013年,用了将近两倍的语言才能获得同样的80%,还增加了Objective-C,C#,Python,JavaScript和Ruby.2016年初,整个Tiobe前20名的思想份额加起来还不到80%。2014年第四季度,Github报告称,19种语言都拥有超过1万个活跃存储库(http://githut.info/),将Swift,Go,Scala等添加到in Click中。

越来越多的开发人员和架构师选择最适合手头任务的编程语言。一个从事大数据项目的开发人员可能认为Clojure是最好的语言,与此同时,大厅下面的人可能在用Dart做前端工作,而地下室的程序员可能在嵌入式系统中使用C语言(这并不意味着厌恶阳光)。几年前,这种类型的多样性在一家公司是很少见的,而现在,它可以在一个团队中找到。

选择一种特别适合于解决特定问题的编程语言可以提高生产率和更好的软件质量。当语言适合问题时,摩擦就减少了,编程变得更直接,代码也变得更简单,更容易维护。例如,在大规模数据分析中,水平缩放对于实现可接受的性能非常重要。像Haskell,Scala和Clojure这样的函数式编程语言倾向于自然地适应这里,允许分析系统向外扩展而不需要复杂的并发问题。

平台也推动了语言的采用。Objective-C在苹果发布iPhone时大受欢迎,Swift也紧随其后。Go是蓬勃发展的容器生态系统的语言,负责Docker,Kubernetes,etcd等要领。那些为浏览器编程的团队将拥有能够使用JavaScript,TypeScript和/或Dart的能力,而游戏和GUI世界仍然使用C++编写代码,以获得性能最好的图形。这些选择既有历史的原因,也有令人信服的技术支撑。即使这样的团队内部只使用一种语言,当他们跨业务边界协作时,语言也会混合。

许多声称单一语言的组织使用一系列支持语言进行测试和原型设计。动态编程语言(如Groovy和Ruby)经常用于测试,而Lua,Perl和Python则流行于原型开发,PHP与Web有着悠久的历史。构建系统,例如基于Groovy的Gradle和基于Ruby的Rake,也提供了创新的功能。

然而,多国语言的故事并不全是葡萄酒和歌曲。掌握一门编程语言是不小的壮举,更不用说它附带的工具和库了。随着每一种新语言的出现,这种负担成倍增加,公司可能会经历收益递减的情况。在产品计划中引入多种语言可能会带来与跨语言集成,开发人员培训以及构建和测试的复杂性相关的大量成本。如果管理不当,这些成本会很快盖过多语言策略的好处。


Apache Thrift语言支持

AS3

c

C++

C#

d

飞镖

德尔菲

二郎

哈斯克尔

哈克斯

Java

JavaScript

卢阿

node.js

目标-C

奥卡梅尔

Perl

PHP

大蟒

红宝石

Smalltalk

打字稿



Apache Thrift的关键优势之一是它能够简化,集中和封装系统的跨语言方面。ApacheThrift在树中为多语言应用程序开发提供了广泛的支持。上面提到的每一种语言都受到Apache Thrift项目的支持,总共有20多种语言,而且还在不断增长。这种对现有语言的无与伦比的直接支持,以及Apache Thrift社区对新语言的快速添加支持,可以帮助组织最大限度地发挥多语种的潜力,同时最大限度地减少负面影响。

与Apache Thrift的应用程序集成

无论您的应用程序是否使用多种平台和语言,其操作都很可能跨越网络和时间上的多个进程。在某些时候,这些进程将需要通信,或者通过磁盘上的文件,通过内存中的缓冲区,或者跨网络。有两个与进程间通信相关的核心问题:

  • 类型序列化

  • 服务实现

让我们依次考虑每一个。

类型序列化

序列化是任何跨平台/语言交换中的基本功能。例如,考虑一个用于音乐行业的应用程序,它使用Apache QPID作为消息传递系统来通信歌曲数据。使用QPID消息代理,Java和Python程序可以使用队列发送/接收消息。问题是,Python和Java程序能够读取彼此的音乐信息吗?Python对象在内存中的表示方式与Java对象不同。如果一个Python程序将其音乐曲目数据的原始内存位发送给一个Java程序,就会产生焰火。

为了解决这个问题,我们需要在消息传递平台之上有一个数据序列化层。人们可能会问,为什么不在JSON中来回发送所有内容呢?使用像JSON这样的标准格式是解决方案的一部分,但是,我们仍然必须回答这样的问题:在发送多字段消息时,数据字段是如何排序的,缺少字段时会发生什么,不直接支持某个数据类型的语言在接收该数据类型时会做什么?这些问题以及其他许多问题都不能用像JSON,YAML或XML这样的数据布局规范来回答。不同的语言通常会为同一数据集生成不同的文档,尽管格式是合法的。

IDL和类型

Apache Thrift提供了一个模块化序列化框架来解决这些问题。使用Apache Thrift,开发人员可以用接口定义语言(IDL)定义抽象数据类型。然后可以将此IDL编译为任何受支持语言的源代码。生成的代码为用户定义的所有类型提供完整的序列化和反序列化逻辑。Apache Thrift确保任何语言编写的类型都可以被任何其他语言读取。

namespace * music

enum PerfRightsOrg {
    ASCAP = 1
    BMI = 2
    SESAC = 3
    Other = 4
}

typedef double Minutes

struct MusicTrack {
    1: string title
    2: string artist
    3: string publisher
    4: string composer
    5: Minutes duration
    6: PerfRightsOrg pro
}

有些人抱怨说,创建IDL是一个额外的步骤,会减缓开发过程。我发现情况恰恰相反。IDL迫使您独立地仔细考虑您的接口,远离嘈杂的实现代码。这可能是您在一个系统设计上花费的最重要的时间。IDL也是轻量级的,易于修改和试验,并且通常作为业务方面的通信工具也很有用。

有人说无模式系统更灵活,而IDL很脆弱。事实是,无论您是否记录了您的模式,如果您正在读取和解释数据,您仍然拥有一个模式。隐含的(未记录的)模式可能是相当危险的应用程序错误的来源,并给需要与数据交互或扩展系统的开发人员造成负担。如果除了读写数据布局的代码之外,您没有读写数据布局的定义,那么当您想要扩展系统时,这将是缓慢的。整个系统中有多少位代码依赖于这个隐含的模式?你怎么改变这样的事情?

NoSQL系统的流行(其中许多是无模式的)为IDL创造了另一个角色。您现在有机会在一个地方记录类型,并在服务调用,消息传递系统和Redis,MongoDB等存储系统中使用这些类型。

有些系统反向处理,从给定的编码解决方案生成它们的模式。注释驱动系统(如Java的JAX-RS)可以这样工作。这种方法使得实现细节容易影响接口定义,从而影响可移植性和清晰度。修改实现代码通常比修改IDL要多得多。另外,如果给出来自不同供应商的模式,也不能保证另一个供应商的代码生成器会创建兼容的代码。在通信解决方案中涉及多个供应商时,这是一个问题。

Apache Thrift方面通过提供单一的真相来源IDL来解决其中的一些问题。Apache Thrift为跨多种编程语言的单个IDL提供了独立于供应商的支持,并且随着框架的发展,Apache Thrift跨语言测试套件一直在验证互操作性。

界面演化

IDL创建了一个契约,所有各方都可以依赖该契约,代码生成器可以使用该契约创建可工作的序列化操作,从而确保契约得到遵守。然而,IDL模式不一定是脆弱的。Apache Thrift IDL支持一系列接口演化特性,如果使用得当,这些特性允许添加和删除字段,更改类型等等。

对接口演进的支持大大简化了正在进行的软件维护和扩展的任务。微服务,持续集成(CI)和持续交付(CD)等现代工程敏感性要求系统支持增量改进,而不影响平台的其余部分。不提供某种形式的接口演化的工具在被改变时往往会“打破世界”。在这样的系统中,更改接口意味着使用该接口的所有客户机和服务器必须重写和/或重新编译,然后进行大规模的重新部署。

Apache Thrift接口演进特性允许多个接口版本在单一操作环境中无缝共存。这使得增量更新变得可行,支持CI/CD管道,并使单个敏捷团队能够以自己的节奏交付业务价值。

模块化序列化

Apache Thrift提供了可插入的串行化程序(称为协议),允许您使用几种串行化格式中的任意一种进行数据交换,包括二进制格式(用于速度),压缩格式(用于大小)和JSON格式(用于可读性)。即使您更改序列化协议以改进系统的操作方面,也可以保留相同的协定(IDL)。这种模块化方法还允许添加自定义序列化协议。因为Apache Thrift是社区管理的,并且是开源的,所以您可以轻松地更改或增强功能,并在需要时将其推向上游(Apache Thrift项目总是欢迎修补程序)。

服务实现

服务是模块化的应用程序组件,它提供可通过网络(物理或虚拟)访问的接口。Apache Thrift IDL允许您除了定义类型之外还定义服务。与类型一样,可以编译IDL服务以生成存根代码。服务存根用于以多种语言连接客户端和服务器。

service SailStats {
   double get_sailor_rating(1: string sailor_name)
   double get_team_rating(1: string team_name)
   double get_boat_rating(1: i64 boat_serial_number)
   list<string> get_sailors_on_team(1: string team_name)
   list<string> get_sailors_rated_between(1: double min_rating, 
                                          2: double max_rating)
   string get_team_captain(1: string team_name)
}

假设您有一个跟踪和计算帆船队统计数据的模块,并且该模块被内置到一个Windows C++GUI应用程序中,该应用程序设计用于可视化风流动态。碰巧,您公司的web开发团队希望使用sail stats模块来增强Linux上面向Node.js的客户端web应用程序。面对多种语言和平台,不同的模块基数(我们可能需要运行10个Node.js代码实例,但只需要运行几个C++模块实例),以及懒惰的特点(希望编写尽可能少的代码),Apache Thrift可能是一个很好的解决方案。

使用Apache Thrift,我们可以将sail stats函数重新打包为一个微服务,并通过一个易于使用的Node.js客户机存根向Node.js程序员提供对该服务的访问。要创建sail stats微服务,我们只需要在IDL中定义服务接口,编译IDL以创建服务的客户机和服务器存根,选择一个预构建的Apache Thrift服务器来托管服务,然后组装部件。

预构建的服务器外壳

需要注意的是,与独立的序列化解决方案不同,Apache Thrift附带了一套完整的服务器shell,几乎所有受支持的语言都可以使用。这就消除了构建自定义网络服务器的困难而重复的过程。预构建的Apache Thrift服务器也很小,而且很集中,只提供承载Apache Thrift服务所必需的功能。典型的Apache Thrift服务器将比等效的Tomcat部署少消耗一个数量级的内存。这使得Apache Thrift服务器成为容器化微服务和嵌入式系统的一个很好的选择,因为这些系统没有运行完整的web或应用程序服务器所必需的资源。

模块化运输系统

Apache Thrift还提供了可插拔传输系统。Apache Thrift客户端和服务器通过将Apache Thrift数据流适配到外部世界的传输进行通信。例如,TSocket传输允许Apache Thrift应用程序通过TCP/IP套接字通信。对于其他通信方案,例如命名管道,有预构建的传输,定制传输也很容易创建。Apache Thrift还支持脱机传输,允许将数据序列化到磁盘,内存和其他设备。

Apache Thrift传输模型的一个特别优雅的方面是对分层传输的支持。协议将应用程序数据序列化为位流。传输简单地读写字节,使任何类型的操作成为可能。例如,TZLibTransport在许多Apache Thrift语言库中都可用,它可以分层在任何其他传输之上,以实现高比率数据压缩。您可以将数据分支到记录器,将请求分叉到并行服务器,加密以及使用自定义分层传输执行任何其他方式的操作。

Apache Thrift Service演练

为了更好地理解Apache Thrift的实际方面,我们将构建一个简单的微服务。我们的服务将被设计为向我们企业的各个部分提供每日问候语。该服务将公开一个“hello_func”函数,该函数不接受参数并返回一个问候字符串。为了了解Apache Thrift如何跨语言工作,我们将用C++,Python和Java构建客户机。

Hello IDL

大多数涉及Apache Thrift的项目都是从仔细考虑所涉及的接口组件开始的。Apache Thrift IDL在表示法上与C类似,使定义跨系统共享的类型和服务变得容易。Apache Thrift IDL代码保存在扩展名为“。Thrift”的纯文本文件中。

# hello.thrift
service HelloSvc {
   string hello_func()
}

我们的hello.thrift IDL文件声明了一个名为HelloSvc的单一服务接口,其中包含一个函数hello_func()。该函数不接受任何参数,并返回一个字符串。要使用这个接口,我们可以用Apache Thrift IDL编译器编译它。IDL编译器二进制文件在类UNIX系统上被命名为“Thrift”,在Windows上被命名为“Thrift.exe”。编译器需要两个命令行参数,一个要编译的IDL文件和一个(或多个)要为其生成代码的目标语言。下面是一个为我们的hellosvc生成Python存根的示例会话:

$ls
你好,节俭
$thrift--Gen py你好,thrift
$ls
gen-py你好,节俭

在上面的会话中,IDL编译器创建了一个gen-py目录,用于存放我们的hello.thrift IDL发出的所有Python代码。该目录包含所有已定义服务的客户机/服务器存根和所有用户定义类型的序列化代码。

Hello服务器

现在我们已经生成了支持代码,我们可以实现我们的服务,并使用预构建的Apache Thrift服务器来容纳它。下面是一个用Python编码的示例服务器:

# hello_server.py
import sys
sys.path.append("gen-py")
from hello import HelloSvc

from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

class HelloHandler:
    def hello_func(self):
        return "Hello from the python server"

handler = HelloHandler()
proc = HelloSvc.Processor(handler)

trans_ep = TSocket.TServerSocket(port=9090)
trans_fac = TTransport.TBufferedTransportFactory()
proto_fac = TBinaryProtocol.TBinaryProtocolFactory()
server = TServer.TSimpleServer(proc, trans_ep, trans_fac, proto_fac)
server.serve()

在服务器清单的顶部,我们使用内置的Python sys模块将gen-py目录添加到Python路径中。这允许我们为我们的HelloSvc服务导入生成的服务存根。

我们的下一步是导入几个Apache Thrift库包。TSocket为我们的客户机提供了一个连接到的端点,TTransport提供了一个缓冲层,TBinaryProtocol将处理数据序列化,TServer将允许我们访问一些预构建的Python服务器类。

下一个代码块实现了HelloSvc服务本身。用Apache Thrift的说法来说,这个类被称为处理程序。所有服务方法都必须在处理程序类中表示,在我们的示例中,这只是hello_func()方法。在现实世界中,您的所有时间和精力都花在这里,实现您的服务,因为Apache Thrift负责处理其余的工作。

接下来,我们创建一个处理程序的实例,并使用它来初始化服务的处理器。处理器是由IDL编译器生成的服务器端存根,它将网络服务请求转换为对适当的处理程序函数的调用。

Apache Thrift库为文件,内存和各种网络提供了端点传输。这里的示例创建了一个TCP服务器套接字端点,以接受端口9090上的客户端连接。缓冲层确保我们有效地利用底层网络,只有当整个消息已经序列化时才传输比特。二进制序列化协议以快速的二进制格式传输我们的数据,开销很小。

Apache Thrift提供了一系列可供选择的服务器,每个服务器都具有独特的功能。这里使用的服务器是TSimpleServer类的实例,顾名思义,它提供基本的服务器功能。一旦构造好,我们就可以通过调用serve()方法来启动服务器运行循环。

下面的示例会话运行我们的Python服务器:

$ls
gen-py hello_server.py hello.thrift
$python hello_server.py

我们的Python服务器大约花了7行代码,不包括导入和服务实现。在C++,Java和大多数其他语言中也是如此。这是一个非常基本的服务器,但是这个示例应该让您了解到,在快速创建跨语言微服务时,Apache Thrift为您提供了多大的帮助。

Python客户端

现在我们已经运行了服务器,让我们创建一个简单的Python客户机来测试它。

# hello_client.py
import sys
sys.path.append("gen-py")
from hello import HelloSvc

from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

trans = TSocket.TSocket("localhost", 9090)
trans = TTransport.TBufferedTransport(trans)
proto = TBinaryProtocol.TBinaryProtocol(trans)
client = HelloSvc.Client(proto)

trans.open()
msg = client.hello_func()
print("[Client] received: %s" % msg)
trans.close()

我们的Python客户机从导入服务器使用的相同HelloSvc模块开始,但是客户机将使用hello服务的客户端存根。我们还从Apache Thrift Python库导入了三个模块。第一个是TSocket,它用于客户端与服务器套接字建立TCP连接,您可能会猜到客户端必须使用与服务器传输兼容的客户端传输。下一个导入将引入TTransport,它将提供一个网络缓冲区,而TBinaryProtocol导入允许我们将消息序列化到服务器,这也必须与服务器实现相匹配。

我们的下一个代码块用要连接的主机和端口初始化TSocket。我们将套接字传输包装在一个缓冲区中,最后将整个传输堆栈包装在TBinaryProtocol中,创建了一个I/O堆栈,它可以将数据串行化到服务器端点和从服务器端点进行数据串行化。

I/O堆栈由充当远程服务代理的客户机存根使用。打开传输会导致客户端连接到服务器,从而允许我们使用客户端存根对服务进行调用。调用客户机对象上的hello_func()方法将使用二进制协议序列化我们的调用请求,并通过套接字将其传输到服务器。我们的程序打印出结果,然后使用transport close()方法关闭连接。

下面是一个运行上述客户机的示例会话(Python服务器必须运行在另一个shell中才能做出响应)。

$ls
gen-py hello_client.py hello_server.py hello.thrift
$python hello_client.py
[Client]收到:来自python服务器的Hello

虽然比运行mill hello world程序要多做一些工作,但几行IDL和几行Python代码已经允许我们创建一个语言不可知,操作系统不可知和平台不可知的服务API,并具有一个工作的客户机和服务器。还不错。

C++客户端

为了拓宽我们的视角并演示Apache Thrift的跨语言方面,让我们为hello server再构建两个客户机,一个用C++,一个用Java。我们将从C++客户端开始。

首先我们需要再次编译服务定义,这次生成C++存根:

$thrift--Gen cpp你好。thrift
$ls
gen-cpp gen-py hello_client.py hello_server.py hello.thrift

运行带有“--gen cpp”开关的IDL编译器会使它在gen-cpp目录中发出与为Python生成的C++文件大致相当的C++文件,为我们的hello.thrift IDL生成头文件(。h)和源文件(。cpp)。gen-cpp/hellosvc.h头包含服务的声明,gen-cpp/hellosvc.cpp源文件包含服务存根组件的实现。

以下是HelloSvc C++客户机的代码,其功能与上面的Python客户机相同:

#include "gen-cpp/HelloSvc.h"
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/protocol/TBinaryProtocol.h>
#include <boost/make_shared.hpp>
#include <iostream>
#include <string>                         

using namespace apache::thrift::transport;
using namespace apache::thrift::protocol;
using boost::make_shared;

int main() {
    auto trans_ep = make_shared<TSocket>("localhost", 9090);
    auto trans_buf = make_shared<TBufferedTransport>(trans_ep);
    auto proto = make_shared<TBinaryProtocol>(trans_buf);
    HelloSvcClient client(proto);

    trans_ep->open();
    std::string msg;
    client.hello_func(msg);
    std::cout << "[Client] received: " << msg << std::endl;
    trans_ep->close();
}

我们的C++客户机代码在结构上与Python客户机代码相同。除了少数例外,Apache Thrift元模型在不同语言之间是一致的,这使得开发人员更容易跨语言工作。

C++main()函数与Python代码逐行对应,但有一个例外,hello_func()不按常规返回字符串,而是通过out参数引用返回字符串。

Apache Thrift语言库通常包装在名称空间中,以避免宿主语言的全局名称空间中的冲突。在C++中,所有Apache Thrift库代码都位于“Apache.Thrift”命名空间中。这里的using语句提供了对必要的apache thrift库代码的隐式访问。

Apache Thrift努力维护尽可能少的依赖关系,以保持开发环境的简单性和可移植性,然而,也有例外。例如,Apache Thrift C++库依赖于开源Boost库。在我们的示例中,有几个对象包装在boost::shared_ptr中。Apache Thrift使用shared_ptr来管理C++服务操作中涉及的几乎所有关键对象的生存期。

熟悉C++的人会知道,shared_ptr已经成为C++11中标准库的一部分。虽然我们的示例代码是用C++11编写的,但Apache Thrift也支持C++98,因此需要使用shared_ptr的boost版本(将来某个时候,将所有boost命名空间元素移到std命名空间,C++98支持可能会被删除)。

下面是一个bash会话,它构建并运行我们的C++客户机。

$ls
gen-cpp gen-py hello_client.cpp hello_client.py hello_server.py
你好,节俭
$G++--std=C++11 hello_client.cpp gen-cpp/hellosvc.cpp-lthrift
$ls
a.out gen-cpp gen-py hello_client.cpp hello_client.py
hello_server.py hello.thrift
$./a.out
[客户端]收到:Hello thrift,来自python服务器

在本例中,我们使用Gnu C++编译器将hello_client.cpp文件构建为可执行程序。Clang,Visual C++和其他编译器也通常用于构建Apache Thrift C++应用程序。

对于C++编译,我们还必须编译在hellosvc.cpp源文件中找到的生成的客户端存根。在链接阶段,“-lthrift”开关告诉链接器扫描标准Apache Thrift C++库,以解析TSocket和TBinaryProtocol库依赖项(使用G++时,此开关必须遵循。cpp文件列表,否则将被忽略,从而导致链接错误)。

假设Python Hello服务器仍处于启动状态,我们可以运行可执行C++客户机并进行跨语言RPC调用。C++编译器将我们的源代码构建为一个a.out文件,该文件在执行时产生与Python客户机相同的结果。

Java客户端

作为最后一个示例,让我们为我们的服务组合一个Java客户机。我们的第一步是为服务生成Java存根。

$thrift--Gen java Hello.thrift
$ls
a.out gen-cpp gen-java gen-py hello_client.cpp
hello_client.py hello_server.py hello.thrift

“--gen Java”开关使IDL编译器在gen-java目录中为我们的接口发出Java代码,从而创建一个具有嵌套的客户机和服务器存根类的HelloSvc类。下面是Java客户机的源代码,它与先前的Python和C++客户机并行:

import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.TException;

public class HelloClient {
    public static void main(String[] args) throws TException {
        TSocket trans = new TSocket("localhost", 9090);
        TBinaryProtocol protocol = new TBinaryProtocol(trans);
        HelloSvc.Client client = new HelloSvc.Client(protocol);

        trans.open();
        String str = client.hello_func();
        System.out.println("[Client] received: " + str);
        trans.close();
    }
}

我们的Java main()方法位于与包含文件同名的类中。剩下的就是我们以前客户的复述。一个明显的区别是Java客户机在端点传输之上没有缓冲层。这是因为Java中的套接字实现基于流类,该流类在内部进行缓冲,因此不需要额外的缓冲。

下面是Java客户机的构建和运行会话:

$javac-cp/usr/local/lib/libthrift-1.0.0.jar:/usr/share/java/slf4j-api.jar:/usr/share/java/slf4j-nop.jar helloclient.java gen-java/hellosvc.java
$ls
a.out gen-cpp gen-java gen-py helloclient.class
helloclient.java hello_client.cpp hello_client.py hello_server.py hello.thrift
$java-cp/usr/local/lib/libthrift-1.0.0.jar:/usr/share/java/slf4j-api.jar:/usr/share/java/slf4j-nop.jar:。/gen-java:。Helloclient
[客户端]收到:Hello thrift,来自python服务器

我们的Java编译包括三个依赖项,第一个是Apache Thrift Java library JAR。为我们的服务生成的IDL代码还依赖于SLF4J,这是一种流行的Java日志记录外观。slf4j-api jar是外观,slf4j-nop jar是非操作记录器,它只是忽略日志调用。java文件在。class文件中为我们的HelloClient类和HelloSvc类生成字节码。

要在JVM下运行我们的Java HelloClient类,我们必须像在编译步骤中所做的那样修改Java类路径,添加当前目录和gen-java目录,在那里可以找到HelloClient类和HelloSvr类文件。运行客户端会产生与我们在Python和C++中看到的结果相同的结果。

除了在我们各自的语言中运行标准构建工具之外,我们只需花费很少的精力就可以生成Apache Thrift服务器和三个客户机。在短时间内,我们构建了一个微服务,它可以处理来自使用多种语言创建的客户机的请求(源代码here)。现在我们已经了解了基本的Apache Thrift程序是如何创建的,让我们来看看Apache Thrift是如何融入整个应用程序集成的。

比较工具包

SOAP,REST,协议缓冲区和Apache Avro可能是最常被认为是Apache Thrift的替代技术,尽管还有许多其他技术。每种技术都是独一无二的,都有自己的一席之地。以下各节简要概述了软件通信领域的主要参与者,然后总结了Apache Thrift提供的特性,并讨论了它在环境中的适用范围。

肥皂

简单对象访问协议(SOAP)是W3C建议(https://www.w3.org/TR/2007/REC-soap12-part1-20070427/)指定HTTP上的面向服务体系结构(SOA)样式的远程过程调用(RPC)系统。SOAP依赖于XML在客户机和服务器之间承载其有效负载,并且通常通过HTTP部署,尽管也可以使用其他传输。有一些优化可以尝试减少传输XML的负担,还有一些使用JSON的SOAP版本,以及其他一些新版本。相关的技术,如XML-RPC,也是按照类似的原则操作的。与直接利用HTTP头,谓词和状态代码的RESTful服务不同,SOAP和XMP-RPC系统通过HTTP POST操作隧道函数调用,错过了RESTful服务中的大多数缓存和系统分层优点。

HTTP友好技术的主要优点是其广泛的互操作性。通过在无处不在的HTTP协议上传输基于标准的文本文档(XML,JSON等),几乎可以使用任何应用程序或语言。人类可读的XML/JSON有效负载还极大地简化了原型制作,测试和调试。在缺点方面,每一种语言,供应商以及通常的每一个公司都提供了自己的生成存根的方案。不能保证不同SOAP WSDL(Web服务描述语言)工具生成的代码会协作。另一个缺点是,由于缺乏对传统web基础设施(尤其是缓存)的支持,这些HTTP隧道系统往往在规模上表现不如REST。

SOAP是面向服务的发展过程中使用的主要技术之一,至今仍被广泛使用。SOAP还提供了许多由Oasis标准机构建立的有用的WS-*标准,用于解决身份验证,事务和其他问题(https://www.oasis-open.org/standards话虽如此,似乎很少有新的SOAP服务即将上线,大多数考虑SOAP的人发现REST作为一个公共API解决方案更简单,规模更快,也更有吸引力。

休息

REST是表征状态转移的首字母缩写,这个术语是由Roy Fielding博士在他2000年的论文(https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)。REST是web浏览器从web服务器检索内容的典型方法。RESTful web服务使用REST体系结构样式以利用web基础设施。广泛理解和广泛支持的HTTP协议为基于REST的服务提供了广泛的覆盖范围。基于REST的服务通常使用JSON格式进行有效负载传输,使得客户机/服务器请求易于阅读和处理。

RESTful服务的独特之处在于,它们的接口基于通过URI访问并通过HTTP谓词(如GET,PUT,POST和DELETE)操作的资源。如果做得好,这被称为面向资源的体系结构(ROA)。当在Web上扩展时,ROA会产生显著的好处。例如,标准的基于Web的缓存系统可以缓存使用GET动词获取的资源,防火墙可以对HTTP交付的流量做出更智能的决策,应用程序可以利用与现有Web服务器基础设施相关联的丰富技术。HTTP头可用于协商有效负载格式,缓存到期,安全特性等。浏览器中的客户机可以利用浏览器的本机特性。

当开发人员今天提到API或服务时,他们通常谈论的是REST API/服务。当涉及到实现公共接口时,RESTful方法已经变得几乎无处不在。生态系统是广阔的,开发人员的技能也是广泛的。然而,休息也有缺点。

记住REST是一种架构风格,而不是标准或技术框架,这一点很重要。两个不同的团队可能以非常不同且不兼容的方式构建相同的REST服务。对于任何解决方案都可以这样说,但是对于REST来说尤其如此,因为对于REST应该如何进行,以及使用的几个工具包,模式机制和文档系统有着广泛的观点。例如,RESTful世界为服务定义和代码生成提供了三个相互竞争的平台:RAML,Swagger和API Blueprint。

还有REST没有解决的通信模型。REST的定义是客户机/服务器体系结构,在实践中,它是通过HTTP(一种基于请求/响应的协议)实现的。REST不解决序列化问题,也不支持消息传递或数据流。

RESTful接口最重要的问题之一是它们在后端系统中的开销。HTTP/2(https://http2.github.io/)对解决与HTTP报头和JSON文本传输相关的开销做了很多工作,然而,再多的外部优化也不可能允许REST服务在专门构建的二进制解决方案(如Apache Thrift)的级别上执行。事实上,协议缓冲区和Thrift分别由Google和Facebook创建,以减轻高负载服务器系统中与RESTful服务相关的性能问题。

协议缓冲区

Google协议缓冲区(https://developers.google.com/protocol-buffers/)和Apache Thrift在功能,性能以及从序列化和IDL角度来看都是相似的。它们是由不同的公司建造的,用来做同样的事情。官方的Google Protocol Buffer(PB)语言支持仅限于PB2中的C++,Go,Java,Python,C#和Objective-C。这是一个移动的目标,随着时间的推移,新的语言被添加(例如,Protocol Buffers3将Ruby添加到列表中)。协议缓冲区被大量的开发人员社区使用,web上的许多项目扩展了可用的树外语言,使之成为与Apache Thrift相当的列表。

Google Protocol Buffers专注于通过主项目提供单片集成消息序列化系统。在其他项目中,有几个用于协议缓冲区的RPC样式系统可用。对于某些人来说,Apache Thrift框架的模块化序列化和传输特性以及in tree语言和服务器支持提供了优势。其他人则更喜欢PB提供的简单集成序列化方案。

这两个平台之间的另一个区别是对集合传输的支持。Apache Thrift支持三种常见容器类型的传输:列表,集和映射。协议缓冲区提供了一个重复字段特性,而不是对容器的支持,通过较低级别的构造产生类似的功能。较新版本的PB增加了地图模拟,但有一些限制。协议缓冲区支持有符号和无符号整数,而Apache Thrift仅支持有符号整数。然而,ApacheThrift支持联合和其他一些在协议缓冲区中没有的次要IDL特性。当同时进行比较时,协议缓冲区序列化可能比Apache Thrift更快,并且产生的有效负载也更小,因为它可以用于更少的高级抽象。

协议缓冲区是健壮的,有很好的文档记录并由大型公司支持,这与Apache Thrift的社区驱动性质形成了对比。这一点在两个项目的文档质量上表现得最为明显,谷歌的表现明显优于其他项目(我也很友善)。

阿帕奇·阿夫罗

Apache Avro(https://avro.apache.org/)是一个序列化框架,用于将序列化架构与序列化的数据打包。这与Apache Thrift和Protocol Buffer形成了鲜明对比,两者都在IDL中描述模式(数据类型和服务接口)。ApacheAvro动态地解释模式,而大多数其他系统在编译时生成代码来解释模式。通常,对于序列化到磁盘的长寿命对象,将模式与数据结合起来可以很好地工作。然而,这样的模型会给实时RPC样式的通信增加复杂性和开销。当然,可以通过参数和优化来改变这些观察结果,但Apache Avro最实际的用途是将对象序列化到磁盘上。

在撰写本文时,ApacheAvro支持七种编程语言,并且还提供了一个基本的RPC框架。Avro支持Apache Thrift中存在的相同容器,尽管Apache Avro映射只允许字符串作为键。在Apache Avro中使用动态解释的嵌入式模式和在Apache Thrift中使用编译的IDL是这两个平台之间的关键区别。

Apache Thrift

Apache Thrift(https://thrift.apache.org/)平台的关键在于它的包的完整性,它的性能和灵活性以及它的IDL的表现力。Apache Thrift的创建是为了提供与REST相媲美的跨语言能力,但它的性能有了显著的提高,而且体积也大大减小。

性能

为了了解这里描述的几种通信方法的相对性能,我们可以查看一些简单的测试结果。下图显示了对几个服务进行一百万次API调用所需的时间。所有服务器都是用Java编写的,并且在所有情况下都使用了同样用Java编写的相同客户机,不过需要使用必要的绑定来调用被测试的服务后端。每个栏显示针对同一台计算机上运行的不同实现完成请求所用的秒数。测试是在没有其他活动的系统上通过本地环回单独执行的。完成了每个测试的多次运行,没有发现异常值。sole服务函数接受一个字符串并返回一个小结构。服务实现在所有情况下都是相同的,不执行任何逻辑,只是返回一个静态结构来突出显示服务和序列化开销。

第一条显示了使用SOAP实现时服务所经过的时间。测试使用了部署在Tomcat7上,用JAX-WS编码的标准Java SOAP服务。与XML相关的序列化开销以及Tomcat和HTTP引起的负载使它在组中表现最差,超过350秒。

第二个栏显示了相同测试的结果,但是针对使用Java和JAX-RS创建的REST服务。虽然比较使尽可能多的变量规范化,但是基于REST的服务是用HTTP谓词和IRIs而不是函数定义的。这里的实现是一个简单的GET请求(没有缓存),将输入字符串作为查询参数传递,并在JSON有效负载中接收结果结构。这明显比SOAP示例快了大约300秒,这是因为JSON优于XML的序列化性能得到了提高,而且JSON有效负载也显著减少了,而JSON有效负载也仅存在于响应中。

最后三条是Apache Thrift服务器案例。第一个示例与REST示例的比较非常接近,就像Apache Thrift一样。Apache Thrift服务器使用相同的one method服务创建,打包为servlet,部署在Tomcat上,并配置为通过HTTP传输使用JSON协议。Apache Thrift客户机使用JSON向服务器发送参数,并使用JSON接收结果。REST API使用GET谓词并在IRI中接收其参数,从而消除了请求上的JSON,并使REST API传输的总字节小于Apaceh Thrift JSON实现。结果,也许令人惊讶的是,在使用Apache Thrift时,性能有了显著提高。这归因于专门构建的Apache Thrift客户机/服务器存根所产生的序列化好处以及其他效率。当REST API中需要POST请求时,这一差距会进一步扩大,从而导致在客户机和服务器上进行JSON序列化。

真正的性能提升是在Tomcat和HTTP被抛在后面的时候实现的。最后两条显示了编译后的Apache Thrift服务器分别在使用JSON和Compact协议的TCP上运行的性能。这两个服务器在运行时都快了一个数量级,而在内存上则小了一个数量级。

虽然不同的语言,不同的并发级别,不同的服务器外壳,不同的服务和不同的框架会使您的里程发生变化,但上面的示例提供了一个参考框架,并解释了为什么许多公司在面临性能压力时将大规模后端服务从REST/SOAP和/或JSON序列化中移除。只需从REST或SOAP迁移到Apache Thrift,相同的硬件就可以支持10到20倍的流量。

一些设计人员选择REST,使用协议缓冲区或Apache Thrift序列化有效负载,这使工具包的负担和复杂性加倍,错过了消除HTTP所带来的显著好处,同时也放弃了通常与REST相关的可爱的“人类可读有效负载”属性。一个完全不能令人满意的组合。

谈到性能,Apache Thrift提供了一个完整的包,具有接近REST类的互操作性,显著改进的性能以及最广泛的协议和传输选择。

到达

ApacheThrift在树中提供了对多种编程语言的支持,同时也提供了一系列令人印象深刻的平台。Apache Thrift非常适合嵌入式系统,它支持Java的紧凑实现,并提供C++和其他语言的小型服务器。

Apache Thrift天生适合于典型的企业开发环境,支持Windows,Linux和OSX上的Java/JVM和C#/CLR。Apache Thrift也非常适合云原生系统,它提供多种语言的最小,占用空间小的服务器,非常适合容器打包。

Apache Thrift还与web世界进行了很好的集成。对JavaScript和Dart等语言的原生支持可以与使用Node.js,C++,Java,C#等编写的后端系统中的HTTP,TLS,WebSocket和JSON支持相结合。在支持Objective-C和Java的情况下,iOS和Android上的移动解决方案也很容易构建。

摘要

今天有许多可行的通信方案可供选择,它们都有自己的位置。作为默认API选项,尤其是如果您希望在公共Internet上获得广泛的可访问性,REST可能是您的最佳选择。如果您需要绝对的速度,您可以编写自己的本地二进制协议,或者使用像cap'n Proto(https://capnproto.org/)。如果您主要是序列化到磁盘,请查看Apache Avro.如果您想要一个可靠的,名牌的高速序列化系统,请考虑Flatbuffers(https://google.github.io/flatbuffers/),或者如果您还需要RPC服务,可能还需要协议缓冲区。

但是,如果您想:

  • 服务器和序列化-树中完整的序列化和服务解决方案

  • 模块化-可插入的串行化协议和传输,提供了一系列实现

  • 性能-重量轻,可伸缩的服务器,具有快速高效的序列化

  • Reach-支持多种语言,协议和平台

  • 丰富的独立于IDL语言的表达类型和服务抽象支持

  • 柔性集成类型和服务演进特性

  • 社区驱动的开源--Apache Software Foundation托管和社区管理

。。。在一个软件包中,Apache Thrift是您考虑的首要问题。

作者和他的同事向许多开源软件的贡献者,特别是Apache Thrift团队中的Jake,Jens,Roger和Aki表示热烈的感谢。

本文作者是Apache Thrift项目管理委员会成员,也是The Programmer's Guide to Apache Thrift from Manning Publications[39%折扣代码:abernethydz]。