94

谈谈golang的rpc | 鱼儿的博客

 6 years ago
source link: https://yuerblog.cc/2017/11/03/golang-rpc/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

谈谈golang的rpc

之前因为工作需要,写过2个golang的http协议的服务,并没有发现性能上有什么明显的问题。

http/1

之所以如此,主要是因为golang的http客户端默认就支持keepalived长连接复用,并且支持对同一个Host维护连接池(多个连接),所以并没有受到短连接问题的性能影响,当然你也要注意配置一下http客户端的一些参数来优化一下keepalived的行为,具体可以参考:《go HTTP Client大量长连接保持》。

既然Http客户端已经这么好用,还需要研究其他的么?显然,http存在一些问题:

  • 服务端串行应答:在一条连接上的连续请求,在服务端必须按照请求到来的顺序返回应答。
    • 这导致前一个请求处理慢直接影响后续所有请求响应时间变长,显然是个不可忽视的问题。
  • http协议解析性能:解析http header是文本协议,不如二进制协议快。
  • payload解析麻烦:一般我们业务协议采用json/xml等格式,golang里直接进行序列/反序列化很麻烦,或者说当协议升级时可能会出现兼容问题,只能采用google protobuf库来解决,要么就是冗长的代码一点一点的反序列化,很头疼。

http/2很不错!

其实一个不错的优化方案是http2协议,也就是http1的升级版,它解决了2个问题:

  • 应答乱序:不仅仅请求和应答通过ID关联满足乱序应答,而且应答还可以分帧传输,解决大文件传输带来的连接占用问题。
  • 二进制协议:解决协议解析的性能问题,减少传输量。

golang标准库目前对http/2的支持比较成熟的是https/2,也就是加密版本,需要生成公私钥来使用。

它具有golang http/1客户端的优势(连接池,空闲自动断连,长连接),是一个非常不错的rpc方案。甚至谷歌的grpc也是基于Http/2协议来做的,所以是推荐优先考虑的。

关于http/1和http/2的背景知识,可以在这里补充学习《HTTP/2笔记之连接建立》。

jsonrpc怎么样?

jsonrpc是一个标准RPC协议,简单的说请求就是若干JSON串连续发送出去,应答就是连续的JSON串返回回来,因为是RPC的原因请求和应答通过ID关联,从而实现并发乱序,相关说明可以在《维基百科》了解。

golang对jsonrpc的支持有限,主要有这么几个问题:

  • 不支持自动重连
  • 不支持一个客户端访问多个下游
  • 不支持单个下游多个连接
  • 暴露了transport(底层TCP连接)给用户管理,增加了复杂性。

不要小看这些缺失的特性,它无疑给我们拿来即用造成了很大的障碍,并没有体现出golang简化网络开发的优势。

不过我仅仅是抱着学习的态度体验了一下jsonrpc,它是golang标准库里的一部分,虽然它拿来即用的价值并不高,相关代码见github:https://github.com/owenliang/go-jsonrpc

jsonrpc作为一个RPC库最大的优点就是自动将json请求和应答通过反射技术,转化json字段到定义好的struct结构字段中去,避免了手动解析请求的复杂性。

json请求穿插发送在TCP数据流中,服务端采用流式的json解析,从而从TCP数据流中剥离出一个一个彼此独立的json串,这是它通讯的基本原理。

jsonrpc非常类似于protobuf,由于是结构体和json之间的序列/反序列化,在协议升级增加字段或者减少字段并不会造成异常,反序列化时相应字段若没有传值就是默认值,仅此而已。

最麻烦的还是上面提到的各种特性如何进一步封装,我只实现了一个简陋的重连机制,通过一个独立的协程定时的检测连接状态并发起重连,这将导致连接丢失期间的请求失败。

学习http库

我刻意看了一下http库在连接期间的请求处理,大概思路是如果连接池中有正常连接那么就复用它发送请求(实际上是在这个连接上排队请求),如果连接池中没有正常连接那么就去异步建立一个连接。

问题是多个并发请求都会发现当前没有正常连接的事实,并各自发起自己的连接建立,这样就无法重用一条连接了,怎么办呢?

golang解决这个问题的思路是,首先没有连接那么就让他们并发的建立各自的连接。其次,只要哪个请求的连接成功收到一次应答,就会把连接向一个公共的size=1的channel丢进去,用来通知其他正在等待自己连接完成的请求,同时会把这个新连接加入到连接池数组中,后续请求就可以直接复用了。

那么其他正在等待自己连接的请求,可能会优先收到这个channel的通知,而不是自己的连接完成通知,一旦发生这种情况就会直接使用channel传送来的连接进行复用,而自己的连接则启动一个goroutine等待连接完成后直接关闭掉或者放到连接池中(假设连接池没有满)。

当然,可能多个并发请求总有几个使用了自己建立的新连接,这一点没有关系。这些连接首次完成请求后,也可以加入到连接池数组里保存,但是后续请求复用连接的时候总是优先使用最近使用过的连接,所以多余的连接会逐渐没有更多请求排队,慢慢的被关闭淘汰。

这就是HTTP库的思路,可以看出来还是个挺取巧的方案:

  1. 先判断连接池是否有可用连接,如果有直接复用它;一定要注意,复用不是独占,而是直接将请求排队到这个连接上。
  2. 如果连接池为空,那么立即发起一个新连接,同时等待其他请求创建的连接与自己的连接,谁先完成就用谁。而多余出来的连接要么放到连接池里,要么作为多余的连接直接关闭。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

mm_reward_qrcode_1545359795775-2.png

本条目发布于2017年11月3日。属于GO分类。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK