MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Apache Thrift:跨语言的 RPC 解决方案

2022-11-127.0k 阅读

微服务架构与 RPC

在当今的后端开发微服务架构中,不同服务之间的高效通信至关重要。远程过程调用(RPC,Remote Procedure Call)作为一种常用的进程间通信方式,允许程序像调用本地函数一样调用远程服务器上的函数,而无需显式地处理网络通信细节。这大大简化了分布式系统中不同组件之间的交互逻辑,使得开发者可以将更多精力放在业务逻辑上。

微服务架构将一个大型应用拆分成多个小型、独立运行的服务,每个服务专注于完成特定的业务功能。这些服务通常使用不同的编程语言开发,以适应不同业务场景的需求。例如,一个服务可能使用 Python 进行数据处理,另一个服务可能使用 Java 构建企业级应用。在这种情况下,实现跨语言的高效通信就成为了关键挑战,而 Apache Thrift 正是为此而生的强大解决方案。

Apache Thrift 简介

Apache Thrift 是一个由 Facebook 开发并开源的软件框架,用于可扩展的跨语言服务开发。它提供了一个完整的栈,从接口定义语言(IDL,Interface Definition Language)到生成各种编程语言的代码,再到运行时库,支持多种常见的传输协议和序列化格式。

Thrift 的核心是其 IDL,通过 IDL,开发者可以定义服务的接口、数据类型和函数。Thrift 编译器根据这些定义生成目标语言的代码,这些代码包含了客户端和服务器端的骨架。开发者只需在生成的代码基础上填充业务逻辑,就可以快速搭建起一个跨语言的 RPC 服务。

Thrift 的优势

  1. 跨语言支持:Thrift 支持众多编程语言,包括 C++、Java、Python、PHP、Ruby、Node.js 等。这使得不同团队可以根据自身的技术栈和业务需求选择最合适的语言来开发微服务,而不用担心通信问题。
  2. 高效的序列化和传输:Thrift 提供了多种序列化格式,如二进制、JSON 等,以及多种传输协议,如 TCP、HTTP 等。二进制序列化格式具有很高的效率,适合在性能要求较高的场景下使用;而 JSON 则更易于调试和与其他系统集成。传输协议的多样性使得 Thrift 可以适应不同的网络环境和应用需求。
  3. 可扩展性:Thrift 的设计考虑了分布式系统的可扩展性。它可以轻松地处理大量的并发请求,并且支持服务的动态扩展。通过合理的负载均衡和集群部署,基于 Thrift 的微服务可以应对高流量的业务场景。
  4. 代码生成:Thrift 的代码生成机制大大减少了开发 RPC 服务的工作量。开发者只需编写一份 IDL 文件,就可以生成多种语言的客户端和服务器端代码,避免了重复编写跨语言通信的基础代码。

Thrift IDL 详解

Thrift IDL 是定义 Thrift 服务的关键。它类似于其他接口定义语言,如 Protocol Buffers 的 .proto 文件,但有自己独特的语法和特性。

数据类型定义

  1. 基本数据类型:Thrift 支持常见的基本数据类型,如 bool(布尔型)、byte(8 位有符号整数)、i16(16 位有符号整数)、i32(32 位有符号整数)、i64(64 位有符号整数)、double(64 位浮点数)、string(字符串)。例如:
typedef i32 MyInteger;
typedef string MyString;

这里定义了两个类型别名 MyIntegerMyString,分别对应 i32string 类型。

  1. 结构体(Struct):结构体用于组合多个不同类型的数据。例如,定义一个用户信息的结构体:
struct User {
  1: required i32 id,
  2: required string name,
  3: optional string email
}

在结构体定义中,每个字段前面的数字是字段 ID,用于在序列化和反序列化过程中标识字段。required 表示该字段必须存在,optional 表示该字段可以不存在。

  1. 枚举(Enum):枚举类型用于定义一组命名的常量。例如,定义一个用户性别枚举:
enum Gender {
  MALE = 0,
  FEMALE = 1
}

这里定义了 MALEFEMALE 两个枚举值,分别对应 0 和 1。

  1. 容器类型:Thrift 支持三种容器类型:列表(List)、集合(Set)和映射(Map)。例如:
list<i32> IntList;
set<string> StringSet;
map<i32, string> IntToStringMap;

列表用于存储有序的元素,集合用于存储无序且不重复的元素,映射用于存储键值对。

服务定义

服务定义是 Thrift IDL 的核心部分,它定义了客户端可以调用的远程函数。例如,定义一个简单的用户服务:

service UserService {
  User getUserById(1: i32 id),
  void createUser(1: User user)
}

这里定义了一个 UserService 服务,包含两个函数:getUserById 用于根据用户 ID 获取用户信息,createUser 用于创建新用户。函数定义中,参数前面同样有字段 ID,并且每个函数都有返回类型,void 表示无返回值。

Thrift 代码生成与使用

安装 Thrift 编译器

在使用 Thrift 之前,需要安装 Thrift 编译器。不同操作系统有不同的安装方式。例如,在 Ubuntu 系统上,可以使用以下命令安装:

sudo apt-get install thrift-compiler

在 macOS 上,可以使用 Homebrew:

brew install thrift

生成代码

假设我们有一个 user.thrift 文件,内容如下:

namespace py user_service
namespace java com.example.user_service

struct User {
  1: required i32 id,
  2: required string name,
  3: optional string email
}

service UserService {
  User getUserById(1: i32 id),
  void createUser(1: User user)
}

生成 Python 代码,可以使用以下命令:

thrift -r --gen py user.thrift

生成 Java 代码:

thrift -r --gen java user.thrift

-r 选项表示递归生成,--gen 后面跟着目标语言。生成的代码会放在 gen - py(Python)或 gen - java(Java)目录下。

Python 客户端与服务器端实现

  1. 服务器端实现
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

from gen_py.user_service import UserService
from gen_py.user_service.ttypes import User


class UserServiceImpl(UserService.Iface):
    def getUserById(self, id):
        user = User(id=id, name='John Doe', email='johndoe@example.com')
        return user

    def createUser(self, user):
        print(f'Creating user: {user.name}')


if __name__ == '__main__':
    processor = UserService.Processor(UserServiceImpl())
    transport = TSocket.TServerSocket('localhost', 9090)
    tfactory = TTransport.TBufferedTransportFactory()
    pfactory = TBinaryProtocol.TBinaryProtocolFactory()

    server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
    print('Starting the server...')
    server.serve()
  1. 客户端实现
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

from gen_py.user_service import UserService
from gen_py.user_service.ttypes import User


def main():
    transport = TSocket.TSocket('localhost', 9090)
    transport = TTransport.TBufferedTransport(transport)
    protocol = TBinaryProtocol.TBinaryProtocol(transport)
    client = UserService.Client(protocol)

    transport.open()
    user = client.getUserById(1)
    print(f'User: {user.name}, {user.email}')

    new_user = User(id=2, name='Jane Smith', email='janesmith@example.com')
    client.createUser(new_user)
    transport.close()


if __name__ == '__main__':
    main()

Java 客户端与服务器端实现

  1. 服务器端实现
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;
import org.apache.thrift.transport.TTransportFactory;

import com.example.user_service.User;
import com.example.user_service.UserService;
import com.example.user_service.UserService.Iface;


public class UserServiceImpl implements Iface {
    @Override
    public User getUserById(int id) throws org.apache.thrift.TException {
        User user = new User();
        user.setId(id);
        user.setName("John Doe");
        user.setEmail("johndoe@example.com");
        return user;
    }

    @Override
    public void createUser(User user) throws org.apache.thrift.TException {
        System.out.println("Creating user: " + user.getName());
    }


    public static void main(String[] args) {
        try {
            TProcessor processor = new UserService.Processor<>(new UserServiceImpl());
            TServerTransport serverTransport = new TServerSocket(9090);
            TTransportFactory transportFactory = new org.apache.thrift.transport.TFramedTransport.Factory();
            TProtocolFactory protocolFactory = new TBinaryProtocol.Factory();

            TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor)
                  .transportFactory(transportFactory).protocolFactory(protocolFactory));

            System.out.println("Starting the server...");
            server.serve();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 客户端实现
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

import com.example.user_service.User;
import com.example.user_service.UserService;


public class UserServiceClient {
    public static void main(String[] args) {
        try {
            TTransport transport = new TSocket("localhost", 9090);
            transport.open();

            TProtocol protocol = new TBinaryProtocol(transport);
            UserService.Client client = new UserService.Client(protocol);

            User user = client.getUserById(1);
            System.out.println("User: " + user.getName() + ", " + user.getEmail());

            User newUser = new User();
            newUser.setId(2);
            newUser.setName("Jane Smith");
            newUser.setEmail("janesmith@example.com");
            client.createUser(newUser);

            transport.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Thrift 的传输协议与序列化格式

传输协议

  1. TSocket:基于 TCP 套接字的传输协议,是最常用的传输协议之一。它提供了可靠的字节流传输,适合在大多数网络环境下使用。例如,在前面的 Python 和 Java 示例中,我们都使用了 TSocket 来建立服务器和客户端之间的连接。
  2. THTTPTransport:基于 HTTP 的传输协议,适用于需要通过 HTTP 进行通信的场景,例如与 Web 应用集成。它可以使用标准的 HTTP 方法(如 GET、POST)来发送和接收数据,便于与其他 Web 服务进行交互。
  3. TFramedTransport:一种带帧的传输协议,它在数据传输时添加了帧头,用于标识数据的长度。这在处理二进制数据或需要按帧进行数据分割的场景下非常有用,可以提高数据传输的效率和可靠性。在 Java 服务器端示例中,我们使用了 TFramedTransport.Factory 来创建带帧的传输。

序列化格式

  1. TBinaryProtocol:二进制序列化格式,具有很高的效率。它将数据序列化为紧凑的二进制格式,减少了数据传输的大小和处理时间。在性能要求较高的场景下,如高并发的微服务通信,通常会选择二进制序列化格式。前面的 Python 和 Java 示例中都使用了 TBinaryProtocol
  2. TJSONProtocol:JSON 序列化格式,它将数据序列化为 JSON 字符串。JSON 格式具有良好的可读性和跨平台性,便于调试和与其他支持 JSON 的系统集成。但由于 JSON 格式相对二进制格式较为冗长,在性能要求极高的场景下可能不太适用。
  3. TCompactProtocol:紧凑二进制序列化格式,它在保证高效的同时,尽量减少了序列化后的数据大小。与 TBinaryProtocol 相比,TCompactProtocol 在某些场景下可以进一步提高传输效率,尤其是在带宽有限的情况下。

Thrift 在微服务架构中的应用场景

  1. 不同语言微服务间通信:在一个大型微服务项目中,可能有使用 Python 开发的数据处理微服务,使用 Java 开发的业务逻辑微服务,以及使用 Node.js 开发的前端接口微服务。通过 Thrift,可以轻松实现这些不同语言微服务之间的高效通信,避免了因语言差异带来的通信难题。
  2. 异构系统集成:当需要将新开发的微服务与遗留系统集成时,Thrift 可以发挥重要作用。遗留系统可能使用了古老的技术栈,但通过 Thrift 定义接口并生成相应语言的代码,可以实现新老系统之间的无缝通信。
  3. 性能敏感的分布式系统:在一些对性能要求极高的分布式系统中,如实时数据分析系统,Thrift 的高效序列化和传输协议可以满足大量数据快速传输和处理的需求。通过合理选择传输协议和序列化格式,如使用 TFramedTransportTBinaryProtocol,可以最大化系统的性能。

Thrift 与其他 RPC 框架的比较

  1. 与 gRPC 的比较
    • 语言支持:gRPC 主要侧重于对现代主流编程语言的支持,如 Go、Java、Python 等。而 Thrift 的语言支持更为广泛,几乎涵盖了所有常用编程语言。
    • 序列化格式:gRPC 主要使用 Protocol Buffers 作为序列化格式,性能高且二进制格式紧凑。Thrift 除了二进制格式外,还支持 JSON 等多种序列化格式,在灵活性上更胜一筹。
    • 生态系统:gRPC 有强大的 Google 支持,在云原生领域有很好的生态集成。Thrift 虽然没有像 gRPC 那样强大的背后支持,但也有自己活跃的社区,并且在一些传统企业和大型分布式系统中有广泛应用。
  2. 与 Dubbo 的比较
    • 应用场景:Dubbo 最初是为阿里巴巴内部的大规模分布式服务治理而设计,更侧重于服务治理方面,如服务注册、发现、负载均衡等。Thrift 则更专注于跨语言的 RPC 通信本身,虽然也可以与一些服务治理框架结合使用,但在服务治理功能上不如 Dubbo 原生支持得好。
    • 语言支持:Dubbo 主要支持 Java 语言,虽然也有一些对其他语言的适配,但相对有限。而 Thrift 的跨语言支持是其核心优势之一。

总结

Apache Thrift 作为一种跨语言的 RPC 解决方案,在微服务架构中具有重要的地位。它通过简洁强大的 IDL、丰富的传输协议和序列化格式,以及高效的代码生成机制,为开发者提供了一种便捷、高效的跨语言通信方式。无论是在不同语言微服务间的通信,还是异构系统集成、性能敏感的分布式系统中,Thrift 都能发挥出色的作用。与其他 RPC 框架相比,Thrift 在语言支持和灵活性方面具有独特的优势。通过合理地运用 Thrift,开发者可以构建出更加健壮、可扩展的微服务架构。