TiDB与gRPC的那点事

通过对 gRPC 的诞生背景与设计原则的介绍,作者分享了 TiDB 选择 gRPC 的原因,并介绍了在这个过程中为了适应 TiDB 而对 gRPC 做出的调整与完善。最后,关于性能方面,介绍了调优的思路。\

如果有关注 TiDB 的朋友可能注意到,我们在上个月的 RC3 版本中已经完成了将 TiDB 中的 RPC 框架替换成了 gRPC,这个工作其实已经铺垫了快一年了,如果装逼一点说的话,其实 gRPC 开源的第一天看了一眼设计和哲学,就决定在 TiDB 中使用它。\

今天抽空写一下背后的一些思考和在这个过程中的一些经验,本次分享不太会介绍大家怎么去用 gRPC,可能更加偏向一些为什么的问题。\

gRPC 背景介绍

\

其实做分布式系统那么久,几乎也是天天和 RPC 打交道,要说 各个模块是系统的筋肉,那 RPC 就是整个系统的血管,数据的流通,信令的传递,都离不开 RPC。\

RPC 并不是一个固定的东西,可重可轻,重的如同 MS 的 DCOM,JAVA 的 EJB,轻的 HTTP 也可以说是 RPC,甚至自己写个 TCP 的文本通信协议也算。\

大家也都知道 Google 内部其实没怎么用 gRPC,大量使用的是 Stubby,它作为 gRPC 的前身,也是一个 Protobuf RPC 的实现,因为大量依赖了 Google 的其他基础服务所以不太方便开放出来给社区使用。\

随着 SPDY / QUIC,乃至 HTTP/2  的成熟,Google 决定用这些更加标准的组件来构建一个新的 RPC 框架,也就是 gRPC。不过这个项目过于庞大,而且 Google 内部 Stubby 已经用了超过 10 年,很难直接替换,毕竟程序员最烦的事情之一就是去改跑着好好的老代码。。。\

不过 anyway,尽管 gRPC 没有在 Google 内部广泛使用,也是给社区带来了一个非常好的基础组件,现在为止包括ETCD / Kubernetes / TiDB在内的大量社区顶级开源分布式项目都在使用它。\

为何选择 gRPC?

\

有人说,RPC 多简单啊,不就是一个长连接,Sender 和 Reciver 来回发包嘛,顶多再搞个服务发现做 Failover,搞得那么复杂为啥。另外要强大不是已经有 EJB 什么的嘛,gRPC 的意义何在?我想从官方的 gRPC 的设计动机和原则说起:\

1、Google 应该是践行服务化的先驱之一,在业界没那么推崇微服务的时代,Google 就已经大规模的微服务化。\

微服务的精髓之一就是服务之间传递的是可序列化消息,而不是对象和引用,这个思想是和 DCOM 及 EJB 完全相反的。只有数据,不包含逻辑;这个设计的好处不用我多说也很好理解,参考 CSP 。\

2、Protobuf 作为一个良好的序列化方案,注意,只是 序列化(尽管 pb 也有定义 rpc service 的能力,Protobuf 默认生成的代码并不包含 RPC 的实现),它并不像 Thrift 天生就带一个 RPC Framework,相对的来说比较轻。\

在 gRPC 的设计中,一个很重要的原则就是 Payload agnostic,RPC 框架不应该规定用的是什么 payload 格式,可以是 Protobuf,JSON,XML,这也让 gRPC 的设计和层次更加清晰。\

3、比传统的 Request / Response 更丰富的 API Interface,这个是我们使用 gRPC 的重要理由,gRPC 不仅支持传统的一应一答的模式,更是支持三种 Streaming 的调用方式,现代的业务经常会需要传输大的数据流,Streaming API 的设计让这些业务写起来轻松很多。\

4、有了 Streaming 就不可避免地需要引入 Flow-control ,这点 gRPC 的处理很聪明,直接依赖了 HTTP/2,在流控这边不怎么用操心,顺带还可以用 HTTP 反向代理做负载均衡。\

但是另一方面也会带来更多的调优复杂度,毕竟和 Web 的使用场景不太一样,比如默认的 INITIAL_WINDOW_SIZE 在 gRPC 里是 64k,太小,吞吐上不去,需要人工改大。\

5、另一方面由于直接使用了 HTTP/2,TLS 的支持就几乎是天然的,对于 TiDB 这样的商业数据库而言,传输层加密是一个很重要的功能,在 gRPC 中直接就可以支持。本着不重新造轮子的原则,直接用 gRPC 就好了。\

gRPC-rs 顺势而生

\

下面是 TiDB 整个项目的架构图:\

4d57c46e0b9d5bded41011a769488fc3.jpg

\\

大家也都知道,TiDB 的底层存储 (TiKV) 是使用 Rust 开发的,至于为啥用 Rust 我在其他文章里说的比较多了,也不是今天的重点就不展开了。\

当时我们决定采用 gRPC 的时候摆在我们面前的一个很现实的问题是 gRPC 并没有 Rust 语言的实现,而且另一个更大的问题是,Rust 甚至还没有 HTTP/2 的实现。\

但是呢,不能因为这个原因不用呀,我们公司的做事风格还是拥抱社区,如果没有社区就自己创造社区。\

刚好那个时候我在旧金山,在 Mozilla 总部和 Rust core team 的团队提到这个事情,后来对方介绍了 Yandex 的一个工程师,也是 Rust proto3 库的作者,他开了个坑开始实现 Rust HTTP/2 library 和 gRPC 的 pure Rust 实现,应该是 2016 年 9 月前后,一开始我们非常期待啊,也一直在帮助这个库完善。\

后来大概在 2017 年 3 月,整个 rust gRPC 觉得大概可用了,然后 Yandex 这个哥们进度有点慢了,我们于是只好把这个坑接过来自己填,同时往 TiKV 上整合。\

大概花了一个多月的时间,完成以后在我们的测试平台上一测,发现稳定性有很大的问题,经过大概两个月艰苦的修 Bug 的过程,仍然看不到希望。\

而且毕竟不是官方的作品,和主干 Features 的合并也牵扯了很大精力,虽然也想过把这个项目捐给 gRPC 官方,但是估计 gRPC 官方也没有人能维护这个项目,所以也还是我们自己维护,最后没办法,我们发版本的压力也很大,只好另想办法。\

大家也都知道 gRPC 的官方主要维护的就三种语言:C / Java / Go,至于 C++ / Python / Ruby 什么的都是在 C 的 gRPC core 之上进行封装的,但是没有 Rust。\

9233acc160472b8f15c9a5af053371ee.jpg

\

幸运的是,Rust 对于 C ABI 支持很好,毕竟后端直接就是 llvm ,性能上更没有什么损失,直接可以封装一下得到一个 Rust 的 gRPC 库。其实现在看看,一开始就应该这样,在追求纯 Rust 实现 gRPC 库上我们浪费了一些时间,是一个失误。\

在我们官方 Blog 的一篇文章里,我们描述了我们的 gRPC-rs 的设计.\

这里我也不想赘述,总的来说,从最后的完成时间来看,估计也就花了大概 1 人月的时间,而且整个 core 的稳定性也有保证。\

值得一提的是在我们的 gRPC-rs 中,并不是简单地做了一层 gRPC core 的 wrapper 就完事了,我们使用了 futures-rs,将 Futures 引入 RPC 的调用 API 中的一个好处就是很多异步逻辑可以用近似同步的书写方式(组合子)来写,程序看起来会更加清晰。\

详细内容就不展开了,有兴趣的可以看看.\

也欢迎参与一起开发。\

c6102a322deb1b654f04b4d8084253a4.jpg

\

性能调优

\

在完成 gRPC 库的 Rust 语言移植后,摆在我们眼前的一个重大的问题就是性能问题,在 gRPC 之前,我们使用的是一个自己写的很裸的 Protobuf RPC 实现,简单得不能再简单,长连接,Protobuf Payload,只有 Req / Resp 模式,但是简单也有简单的好处,几乎没有太多性能的损失,但是也有简单的坏处:\

之前的实现 scale 起来比较麻烦,用 gRPC 的话 scale 只需要改改 gRPC 线程数就好。最开始直接换成 gRPC 后,延迟性能和吞吐都有 30% 以上的下降,同时观察到 CPU 的消耗是原来的 200%,然后就开始了调优之路。\

其实 gRPC 本身的设计并不差,核心 task 的异步化调用的设计采用了组合子还是蛮巧妙的.\

另外基于 epoll 封装了一套类似 IOCP 机制,在官方的设计文档中有很好的解释。\

但是由于整体依赖了 HTTP/2,所以比裸的 RPC 还是多出了很多工作,主要集中在 HTTP/2 的包处理上,所以我们的性能调优也是集中在 HTTP/2 这边。\

比如,上面提到的用于 HTTP/2 流控的  INITIAL_WINDOW_SIZE ,默认 64k,调大有助于提高吞吐,比如参见社区的这个 issue.\

另外 HTTP/2 是单连接的,实际测试发现也制约了吞吐,我们实践中不管是 TiDB 连接 TiKV 还是 TiKV 之间的连接都是采用多个 gRPC client 的方式来同时建立多个 HTTP/2 连接。\

如果你知道自己的 workload 的大小,通过适当的调整 GRPC_WRITE_BUFFER_HINT 改变 write buffer 的大小也能显著减少 syscall 的调用.\

GRPC_ARG_MAX_CONCURRENT_STREAMS规定在一个 HTTP/2 连接中最多存在多少 stream,在 gRPC 中一次 RPC 就是一个 stream。在 TiKV 的应用场景中,适当调高该参数同样有助于提高吞吐。\

还有就是 gRPC 本身不适用于传送大文件的场景,见 issue。\

TiKV 之间发送 snapshot 就是采用 issue 中推荐的方案,把大文件拆成多个 chunk 后使用 client streaming 发送。\

总结

\

总体感觉,现在 gRPC 这个项目还不是太成熟,从不断在重构 iomgr 这部分就能看出来,现在的 poll engine 的设计还是有很大的进步空间。\

目前的效果 TiKV 吞吐已经和原来我们的手写的 RPC 框架持平,但是 CPU 的消耗略高一些,但是功能上已经让我们新功能的开发简化很多,总体来说一定是利大于弊的,我们也在紧跟 gRPC 社区,相信这些性能问题都能被解决。\

作者介绍

\

黄东旭,知名开源软件作者,代表作品分布式 Redis 缓存方案 Codis,以及分布式关系型数据库 TiDB。曾就职与微软亚洲研究院,网易有道及豌豆荚,现任 PingCAP 联合创始人兼 CTO,资深基础软件工程师,架构师。擅长分布式系统以及数据库开发,在分布式存储领域有丰富的经验和独到的见解。\\\


感谢林日华对本文的审校。

\\

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值