Twirp 框架简介

2019-06-20

Twitch 开源了一套叫 Twrip 的 RPC 框架,还发表了 Twirp: a sweet new RPC framework for Go 加以介绍。我在部门技术选型的时候力排众异,选用了 Twirp。现在新业务已经平稳运行了一年,可以说 Twirp 经受住了实践的检验。但酒香也怕巷子深,Twirp 在 2018 年 1 月开源之后竟没引起多大反响。我觉得有必要为它写点科普性的内容了。

现在一提到 RPC 大家言必称 gRPC。那就先说说 gRPC (主要是 grpc-go)的几个问题:

  1. 不支持 http/1.1。gRPC 使用了 http/2 的 trailer 头和 stream 机制,所以没法支持 http/1.1。这样会导致如下问题:
    1. gRPC 服务不能直接暴露给浏览器,所以就有了 grpc-web 项目
    2. gRPC 服务没法利用通用的中间件(不过现在 envoy 和 nginx 已经支持了)
    3. 调用方必须升级到 http/2 客户端,双方需要维护复杂的 http/2 连接状态
  2. 二进制数据很难处理 一方面,从协议上看,gRPC 并不要求只用 protobuf 做为序列化协议,但 grpc-go 貌似只支持 protobuf。这让开发、调试和测试都很痛苦。 另一个方面,gRPC 规定所有消息都加一个五字节的前缀,一字节表示是否压缩,另外四个表示消息长度。这样一来所有消息都是二进制的。
  3. 协议栈复杂 grpc-go 重新实现了一套 http/2 协议栈,难懂难改,还经常引入 breaking change。

但瑕不掩瑜, gRPC 也有很多值得借鉴的地方:

  1. 使用 protobuf 做为 idl,明确定义参数类型
  2. 自动生成接口代码,省时省力又标准
  3. 接口映射规则简单

那如何继承 gRPC 的优点而规避掉其不足呢?这就是 Twirp 要解决的问题。

首先,你可以使用跟 gRPC 完相同的 proto 文件描述 RPC 服务,比如:

syntax = "proto3";
package twitch.users.email;

service EmailBoss {
 rpc UpdateEmail(UpdateEmailReq) returns (UpdateEmailResp);
}
message UpdateEmailRequest {
 int64 user_id = 1;
 string new_email = 2;
}
message UpdateEmailResponse {
 string cleaned_email = 1;
}

可以使用如下 curl 发起调用:

curl \
 -header 'Content-Type:application/json' \
 -data '{"user": "spencer", "email": "spencer@twitch.tv"}' \
 http://localhost:9090/twirp/twitch.example.EmailBoss/UpdateEmail
 
 # response body
 {"cleaned_email":"spencer@twitch.tv"}

这里指明 Content-Type 为 application/json,json 内容跟 UpdateEmailRequest 对应。Protobuf 本来就支持与 json 互相转换。如果你想提高传输效率,可以使用 protobuf 编码,这时需要将 Content-Type 指定为 application/protobuf,而 body 则需要传序列化后的 pb 数据。

Twirp 没有自定义任何 http 头,对 http 协议没有任何假设。你可以使用 http/1.1 也可以使用 http/2,甚至还可以用 http/3。你可以使用任何 http 工具对 Twirp 服务进行调试、抓包、压测、转发。你也可以使用 http 内建的压缩机制对传输内容(主要是 json)进行压以提高传输效率。

说了这么多 Twirp 的优点,难道它就没有缺点吗?有,而且还很严重。Twirp 不支持 gRPC 的 stream 接口,只能支持 unary 接口。这就是一个取舍问题了。gRPC 之所以要依赖 http/2,之所以要定义5字节的消息前缀,就是让这个 stream 接口给闹的。大家扪心自问,又有多少业务场景需要用到 stream 类型的接口呢?我们厂也在推 gRPC,但几乎没有见过 stream 接口。以我个人的经验来看,这个比例不会超过5%。gRPC 就是为实现额外 20% 特性(stream)而多引入 80% 复杂性的典型例子。从这个角度来看 Twirp 对 gRPC 的简化是有意义。

最后,Twirp 提供了一 protoc 插件,可以生成服务端和客户端的代码。但因为 Twirp 协义太过简单,我们的客户端都自己写客户端代码,使用自动生成的客户端代码反而不灵活