今天我跟几个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,对于其他语言的开发体验不做评价。

RPC的故事的更多相关文章

  1. Redola.Rpc 的一个小目标

    Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标:20000 tps. Concurrency level: 8 threads Complete requests: 20000 ...

  2. 你应该知道的 RPC 原理

    作者:伯乐在线 - meituanalibaba   网址:http://blog.jobbole.com/92290/ 在校期间大家都写过不少程序,比如写个hello world服务类,然后本地调用 ...

  3. 一个故事讲清楚NIO

    转载请引用:一个故事讲清楚NIO 假设某银行只有10个职员.该银行的业务流程分为以下4个步骤: 1) 顾客填申请表(5分钟): 2) 职员审核(1分钟): 3) 职员叫保安去金库取钱(3分钟): 4) ...

  4. 你应该知道的RPC原理

    你应该知道的RPC原理 在学校期间大家都写过不少程序,比如写个hello world服务类,然后本地调用下,如下所示.这些程序的特点是服务消费方和服务提供方是本地调用关系. 而一旦踏入公司尤其是大型互 ...

  5. 基于RPC原理的dubbo

    在校期间大家都写过不少程序,比如写个hello world服务类,然后本地调用下,如下所示.这些程序的特点是服务消费方和服务提供方是本地调用关系. 而一旦踏入公司尤其是大型互联网公司就会发现,公司的系 ...

  6. RPC 原理的前生今世

    (如果感觉有帮助,请帮忙点推荐,添加关注,谢谢!你的支持是我不断更新文章的动力.本博客会逐步推出一系列的关于大型网站架构.分布式应用.设计模式.架构模式等方面的系列文章) 在校期间大家都写过不少程序, ...

  7. C# 的轻量级 RPC 框架

    Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标:20000 tps. Concurrency level: 8 threads Comple ...

  8. 一个故事讲清楚NIO(转)

    转载请引用:一个故事讲清楚NIO 假设某银行只有10个职员.该银行的业务流程分为以下4个步骤: 1) 顾客填申请表(5分钟): 2) 职员审核(1分钟): 3) 职员叫保安去金库取钱(3分钟): 4) ...

  9. 那些年,我们追过的RPC

    1974年冬,互联网大师 Jon Postel发表了RFC674:“Procedure Call Protocol Documents,Version 2”,尝试定义一种在包含70个节点的网络中共享资 ...

随机推荐

  1. Activity Fragment转场动画

    Activity转场动画 先介绍个动画的好例子:https://github.com/lgvalle/Material-Animations Activity的转场动画是通过overridePendi ...

  2. Android-原生对话框

    package liudeli.ui.all; import android.app.Activity; import android.app.AlertDialog; import android. ...

  3. [LeetCode 题解]: String to Interger (atoi)

    Implement atoi to convert a string to an integer. Hint: Carefully consider all possible input cases. ...

  4. Swagger简单实例

    随着技术的不断发展,网站框架也开始向:前后端分离的形态发展,而且前端技术和后端技术在各自的道路上越走越远.而web api 接口成了前后端唯一的联系.所以web api会变得越来越重要. 那么什么是W ...

  5. pageadmin网站制作 如何修改和管理网站模板

    在使用pageadmin CMS 的同时,遇到问题可以参考官网帮助中心.1.网站模板目录地址/templates目录, 2.点击展开后,每个目录就是一个网站模板, 前端设计师制作的新版本都可以放到这个 ...

  6. day02.5-集合内置方法

    集合——set的定义:test = {1,2,3,4,5} 或 test = frozenset(1,2,3,4,5) 特点:1. 集合是可变数据类型 2. 集合中元素是无序的,可以是数字.字符串与元 ...

  7. javascript设为首页、加入收藏

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. apply和call的用法

    以前对与apply()还有call()一直处于一种我知道这是干什么,但是不知道怎么使用的情况,今天看别人的博客的时候,看到了一点这类知识,感觉有点感觉,现在把新的心得写下来. A.apply(B,[a ...

  9. 使用httpclient对zip格式的响应数据解压

    HttpResponse response = httpClient.execute(httpPost); //对zip进行解压 response.setEntity(new GzipDecompre ...

  10. file.delete()的优化

    //删除暂存的pdf File file =new File(pdfFilename); file.delete(); Path path2 = Paths.get(pdfFilename); Fil ...