之前因为工作需要,写过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库的思路,可以看出来还是个挺取巧的方案:
- 先判断连接池是否有可用连接,如果有直接复用它;一定要注意,复用不是独占,而是直接将请求排队到这个连接上。
- 如果连接池为空,那么立即发起一个新连接,同时等待其他请求创建的连接与自己的连接,谁先完成就用谁。而多余出来的连接要么放到连接池里,要么作为多余的连接直接关闭。