今天我跟几个RPC框架之间发生了一些事,情节跌宕起伏一波三折,不吐不快,以至于我这个从来不写博客的人也忍不住写下来分享一下。

背景

主系统部署在Windows上(.NET 4.5),子系统(.NET CORE)部署在各种Linux上(Ubuntu/CentOS/RHEL等),两者之间通信用RPC框架。由于涉及到文件的读写(目前最大是4M,不过不排除增大的可能),因此选用了基于Protocol Buffers的GRPC框架,毕竟PB的序列化效率在请求比较大的时候还是很重要的。在功能已经完成之后,发现如果项目中包含了Google.ProtocolBuffers这样的依赖的话,有可能会被公司内部的工具扫描到然后被challenge(之前我就觉得友商的名字好扎眼哈哈),为了避免不必要的麻烦,最好还是不要用它了。由于项目中别处已经采用了thrift,所以首选还是用thrift。有人要说了既然之前就用了thrift,你这次怎么还用GRPC你是不是傻?其实用GRPC是因为真的很适合这个场景,而且2个thrift又必须版本不同(历史遗留问题)会有一个很恶心的坑,就是为了避免这个坑才用的GRPC。结果现在还是要改回thrift,又要用一个workaround 去解决双thrift版本的坑……头疼!话不多说,LET'S GO。

THRIFT从入门到放弃

第一个拦路虎就是现在nuget上的THRIFT版本真是五花八门,官方那个package已经3年没更新了,不少热心网友自己编的版本不过都不支持NETCORE,只有一个叫 apache-thrift-netcore的版本可用,并且上传者是官方package的维护者之一,感觉是个尝鲜版。该版本的thrift compiler依然只支持-gen csharp而不是官网上的-gen netcore。其实不管哪个,生成出来的要么是同步方法,要么就是原始的BeginXXX EndXXX,因为他们压根就没打算支持async(这个issue开了4年了啊我的哥)。这样的话我得在调用的地方包上一层Task.Run,因为之前实现的代码都是异步,真是恶心……效率低点代码难看点,算了忍吧。

改了一会发现传输压缩没找到实现方式,心想没道理应该有的啊,查了下果然有TZlibTransport ,但是只支持Java Python等(貌似就C#没有),这个功能不是netcore版本阉割的而是从来就没有,没有压缩的话传输效率会低很多,只能先加个TODO,回头有空自己实现一个压缩的子类了。挺生气的感觉C#对于THRIFT真是二等公民,各种缺功能各种不更新。

其实server挺快就改完了,然后开始改client,虽然有上面说的不完美,但是都不是block issue。Client的话之前GRPC的client是thread safe的所以都是复用的,thrift的client查了下是线程不安全,只能给每次会话创建一个新的client……等等,每次创建一个新的?然后构造函数里每次还都创建一个socket?那调用方的端口还不爆炸?简单测试了一下,如果真的这么干的话会跟HttpClient不复用一样有端口TIME_WAIT问题(即使正确Dispose),在高峰时期端口会被用完然后崩盘(我估计现在项目里另外一个用thrift的地方就有这个问题)所以必须得用一个连接池……对不起C#又没有,只能自己写!掀桌!

再看GRPC

冷静了下来,要么还是用原来的方案算了,上面这么多问题就算都解决了或者忍了,还要经受稳定性考验。毕竟apache-thrift-netcore的package description里作者自己都写着 “use with caution…” 这省略号我真是无比的虚,检查了下GRPC的几台服务器已经run了好几个星期了一点问题都没。或者换个RPC框架吧,反正不是thrift就行。等等为什么要换RPC框架,我们只是不要protocol buffers而已,所以只要不用它做序列化就可以了。但是GRPC能脱离PB吗?确实有可能可以,一个是RPC,一个是序列化,完全可以把PB设计成RPC的一个功能模块或者说插件,而不是依赖绑定的关系。看了一眼GRPC,发现了新世界……GRPC居然不依赖PB!是的,我没有看错,GRPC不依赖PB!虽然跟我刚才想的一样,但是你怎么能不依赖呢,你对得起官网上的描述吗 Define your service using Protocol Buffers, a powerful binary serialization toolset and language?你对得起IDL compiler生成的文件吗,里面全是Google.Protobuf这个namespace下的类?那就把PB依赖删掉吧……果然编译不过,能过才见鬼了……可能,GRPC不依赖PB只是因为手抖搞错了?不应该啊,从设计角度来讲,他们也确实不应该绑定,不过移除了PB也确实会编译不过是事实……等等!编译不过的其实都是IDL compiler生成的文件,这些文件里包含了序列化的行为所以是依赖PB的,移除PB必然会编译错误!所以说如果有其他的序列化组件的话,应该有自己的IDL compiler才对(甚至有自己的IDL语法),这样的话就说的通了,GRPC应该是这种设计思路。问题是……谁没事去给你做一个序列化组件啊,你都已经有PB了啊!

Bond拯救世界

抱着不见棺材不落泪的心态我随便搜了下,我去还真有个叫 Bond 的项目,看了下IDL,简直良心!各种贴心小棉袄,简直就是为C#量身定做的!尝试了几个demo,立刻就爱上了它!不仅更新很及时昨天还发布了一版,而且到处都能感受到C#是被放在第一位的,之前thrift带来的不爽一扫而光!顺便说一下Bond 也带一个RPC框架叫 Bond Comm,不过官方说Comm已经废弃了推荐配合GRPC试用。所以我把原来PB的几个IDL文件用Bond的规范重写一遍,其他什么都不用改就可以了……完美啊完美!

结语

其实我主要是想说,thrift真心不适合C#,如果你还在用它,赶紧去看看有没有我上面说的那几个问题。就算没有,还是早日改成GRPC吧,从C#角度看GRPC各方面体验都好很多。序列化的话可以用PB,但是我知道Bond以后我都会选择Bond,因为它的IDL更适合C#(毕竟微软家产品肯定.NET优先)。光从IDL来说,thrift还是比PB更好用一些,尤其是PB现在都用proto3了,proto2正在淘汰中一些功能马上就被砍掉(比如optional field),PB的IDL是简单至上,大道至简的感觉(可能也正是这个原因它的多语言支持很好吧)。Bond的IDL光从C#的角度来讲是比PB和thrift要强大的(居然还支持attribute你敢信),但是有得必有失,可能因为这些它对一些其他语言的支持就未必友好(这个是我猜的)。

说完IDL再说说RPC,这方面GRPC真心完爆Thrift。Thrfit的各种 Transport/Protocol 以及你能看到的任何 Txxx 类,看起来可以高度自定义实则是过度设计,因为很少有人去实现自己的 Transport和Protocol,RPC框架应当把这类细节对使用者隐藏掉。GRPC这方面做的很好,HTTP2足够标准,足够效率,足够应付可见的未来出现的其他可能,使用者只要关注自己的实现就可以,传输协议什么的,RPC框架会帮我做的很好,至少肯定比我自己做的好,我不需要关心,我只要相信它就可以。还有一个我印象很深的就是Error handling。GRPC中只有一种Exception就是RpcException,用StatusCode进行分类。服务器端无论过程中是什么Exception,在出口处catch然后根据类型赋予不同的StatusCode和Message。就算请求根本没有到服务器端,client端也会有对应的RpcException,这样在client端handle起来就很舒服很统一。Thrift允许自定义Exception,看起来很强大,但是为了client端处理起来容易,通常还是得有一个GeneralException(或者间接达到这个效果),往往最终这个GeneralException就是GRPC里的RpcException的作用。当请求无法达到服务器时,Thrift client端的异常就是五花八门了,完全取决于transport,各种各样的花式Exception,然而client端其实只需要知道是connection failure就可以了。

总的来说thrift给我的用户体验很差,就如同几年前一样,这么多年一点长进都没(事实上确实也没有新版本,无奈~) GRPC+PB 是更好的选择,对于C#来说GRPC+Bond可能会更适合。以上结论只针对C#/.NET,对于其他语言的开发体验不做评价。

05-20 11:34