毕业论文
您现在的位置: 在线软件 >> 在线软件介绍 >> 正文 >> 正文

Go语言网络库getty的那些事阿里云

来源:在线软件 时间:2023/1/23

个人从事互联网基础架构系统研发十年余,包括我自己在内的很多朋友都是轮子党。

年我在某大厂干活时,很多使用C语言进行开发的同事都有一个自己的私人SDK库,尤其是网络通信库。个人刚融入这个环境时,觉得不能写一个基于epoll/iocp/kqueue接口封装一个异步网络通信库,会在同事面前矮人三分。现在想起来当时很多同事很大胆,把自己封装的通信库直接在测试生产环境上线使用,据说那时候整个公司投入生产环境运行的RPC通信库就有个之多。

个人当时耗费两年周末休息时间造了这么一个私人C语言SDK库:大部分C++STL容器的C语言实现、定时器、TCP/UDP通信库、输出速度可达MiB/s的日志输出库、基于CAS实现的各种锁、规避了ABA问题的多生产者多消费者无锁队列等等。自己当时不懂PHP,其实稍微封装下就可以做出一个类似于Swoole的框架。如果一直坚持写下来,可能它也堪媲美老朋友郑树新老师的ACL库了。

自己年开始接触Go语言,经过一段时间学习之后,发现了它有C语言一样的特点:基础库太少--又可以造轮子了。我清晰地记得自己造出来的第一个轮子每个element只使用一个指针的双向链表xorlist。

年6月我在做一个即时通讯项目时,原始网关是一个基于netty实现的Java项目,后来使用Go语言重构时其TCP网路库各个接口实现就直接借鉴了netty。同年8月份在其上添加了websocket支持时,觉得websocket提供的onopen/onclose/onmessage网络接口极其方便,就把它的网络接口改为OnOpen/OnClose/OnMessage/OnClose,把全部代码放到了github上,并在小范围内进行了宣传。

Getty分层设计

Getty严格遵循着分层设计的原则。主要分为数据交互层、业务控制层、网络层,同时还提供非常易于扩展的监控接口,其实就是对外暴露的网络库使用接口。

1、数据交互层

很多人提供的网络框架自己定义好了网络协议格式,至少把网络包头部格式定义好,只允许其上层使用者在这个头部以下做扩展,这就限制了其使用范围。Getty不对上层协议格式做任何假设,而是由使用者自己定义,所以向上提供了数据交互层。

就其自身而言,数据交互层做的事情其实很单一,专门处理客户端与服务器的数据交互,是序列化协议的载体。使用起来也非常简单,只要实现ReadWriterinterface即可。

Getty定义了ReadWriter接口,具体的序列化/反序列化逻辑则交给了用户手动实现。当网络连接的一端通过net.Conn读取到了peer发送来的字节流后,会调用Read方法进行反序列化。而Writer接口则是在网络发送函数中被调用,一个网络包被发送前,Getty先调用Write方法将发送的数据序列化为字节流,再写入到net.Conn中。

ReadWriter接口定义代码如上。Read接口之所以有三个返回值,是为了处理TCP流粘包情况:

-如果发生了网络流错误,如协议格式错误,返回(nil,0,error)-如果读到的流很短,其头部(header)都无法解析出来,则返回(nil,0,nil)-如果读到的流很短,可以解析出其头部(header)但无法解析出整个包(package),则返回(nil,pkgLen,nil)-如果能够解析出一个完整的包(package),则返回(pkg,0,error)

2、业务控制层

业务控制层是Getty设计的精华所在,由Connection和Session组成。

Connection

负责建立的Socket连接的管理,主要包括:连接状态管理、连接超时控制、连接重连控制、数据包的相关处理,如数据包压缩、数据包拼接重组等。

Session

负责客户端的一次连接建立的管理、记录着本次连接的状态数据、管理Connection的创建、关闭、控制数据的发送/接口的处理。

2.1Session

Session可以说是Getty中最核心的接口了,每个Session代表着一次会话连接。

向下

Session对Go内置的网络库做了完善的封装,包括对net.Conn的数据流读写、超时机制等。

向上

Session提供了业务可切入的接口,用户只需实现EventListener就可以将Getty接入到自己的业务逻辑中。

目前Session接口的实现只有session结构体,Session作为接口仅仅是提供了对外可见性以及遵循面向编程接口的机制,之后我们谈到Session,其实都是在讲session结构体。

2.2Connection

Connection根据不同的通信模式对Go内置网络库进行了抽象封装,Connection分别有三种实现:

gettyTCPConn:底层是*net.TCPConngettyUDPConn:底层是*net.UDPConngettyWSConn:底层使用第三方库实现

2.3网络API接口EventListener

本文开头提到,Getty网络API接口命名是从WebSocket网络API接口借鉴而来。Getty维护者之一郝洪范同学喜欢把它称为“监控接口”,理由是:网络编程最麻烦的地方当出现问题时不知道如何排查,通过这些接口可以知道每个网络连接在每个阶段的状态。

「OnOpen」:连接建立时提供给用户使用,若当前连接总数超过用户设定的连接数,则可以返回一个非nil的error,Getty就会在初始阶段关闭这个连接。「OnError」:用于连接有异常时的监控,Getty执行这个接口后关闭连接。「OnClose」:用于连接关闭时的监控,Getty执行这个接口后关闭连接。「OnMessage」:当Getty调用Reader接口成功从TCP流/UDP/WebSocket网络中解析出一个package后,通过这个接口把数据包交给用户处理。「OnCron」:定时接口,用户可以在这里接口函数中执行心跳检测等一些定时逻辑。

这五个接口中最核心的是OnMessage,该方法有一个interface{}类型的参数,用于接收对端发来的数据。

可能大家有个疑惑,网络连接最底层传输的是二进制,到我们使用的协议层一般以字节流的方式对连接进行读写,那这里为什么要使用interface{}呢?

这是Getty为了让我们能够专注编写业务逻辑,将序列化和反序列化的逻辑抽取到了EventListener外面,也就是前面提到的Reader/Writer接口,session在运行过程中,会先从net.Conn中读取字节流,并通过Reader接口进行反序列化,再将反序列化的结果传递给OnMessage方法。

如果想把对应的指标接入到Prometheus,在这些EventListener接口中很容易添加各种metrics的收集。

Getty网络端数据流程

下图是Getty核心结构的类图,囊括了整个Getty框架的设计。

|说明:图中灰色部分为Go内置库

下面以TCP为例介绍下Getty如何使用以及该类图里各个接口或对象的作用。其中server/client是提供给用户使用的封装好的结构,client的逻辑与server很多程度上一致,因此本章只讲server。

Gettyserver启动代码流程图如上。在Getty中,server服务的启动流程只需要两行代码:

第一行非常明显是一个创建server的过程,options是一个func(*ServerOptions)函数,用于给server添加一些额外功能设置,如启用ssl,使用任务队列提交任务的形式执行任务等。

第二行的server.RunEventLoop(NewHelloServerSession)则是启动server,同时也是整个server服务的入口,它的作用是监听某个端口(具体监听哪个端口可以通过options指定),并处理client发来的数据。RunEventLoop方法需要提供一个参数NewSessionCallback,该参数的类型定义如下:

这是一个回调函数,将在成功建立和client的连接后被调用,一般提供给用户用于设置网络参数,如设置连接的keepAlive参数、缓冲区大小、最大消息长度、read/write超时时间等,但最重要的是,用户需要通过该函数,为session设置好要用的Reader、Writer以及EventListener。

至此,Getty中server的处理流程大体如下图:

对于这些接口的使用,除了getty自身提供的代码示例外,另一篇极好的例子就是seata-golang,感兴趣的朋友可参阅《分布式事务框架seata-golang通信模型》一文。

优化

软件开发一条经验法则是:“Makeitwork,makeitright,makeitfast”,过早优化是万恶之源。

比较早期的一个例子是erlang的发明者JoeArmstrong早年花费了相当多的精力去熬夜加班改进erlang性能,其后果之一是他在后期发现早期做的一些优化工作很多是无用功,其二是过早优化损坏了Joe的健康,导致年在他68岁的年纪便挂掉了。

把时间单位拉长到五年甚至是十年,可能会发现早期所做的一些优化工作在后期会成为维护工作的累赘。年时很多专家还在推荐大家只用Java做ERP开发,不要在互联网后台编程中使用Java,理由是在当时的单核CPU机器上与C/C++相比其性能确实不行,理由当然可以怪罪于其解释语言的本质和JVMGC,但是年之后就几乎很少听见有人再抱怨其性能了。

年在一次饭局上碰到支付宝前架构师周爱民老师,周老师当时还调侃道,如果支付宝把主要业务编程语言从Java切换到C++,大概服务器数量可以省掉2/3。

类比之,作为一个比Java年轻很多的新语言,Go语言定义了一种编程范式,编程效率是其首要考虑,至于其程序性能尤其是网络IO性能,这类问题可以交给时间,五年之后当前大家抱怨的很多问题可能就不是问题了。如果程序真的遇到网络IO性能瓶颈且机器预算紧张,可以考虑换成更低级的语言如C/C++/Rust。

年时MOSN的底层网络库使用了Go语言原生网络库,每个TCP网络连接使用了两个goroutine分别处理网络收发,当然后来经优化后做到了单个TCP连接做到了单TCP仅使用一个goroutine,并没有采用epoll系统调用的方式进行优化。

再举个例子。

字节跳动从年便在知乎开始发文宣传其Go语言网络框架kitex的优秀性能,说是基于原生的epoll后“性能已远超官方net库”云云。当时它没开源代码,大家也只能姑妄信之。年年初,头条又开始出来宣传了一把,宣称“测试数据表明,当前版本(.12)相比于上次分享时(.05),吞吐能力↑30%,延迟AVG↓25%,TP99↓67%,性能已远超官方net库”。然后终于把代码开源了。8月初鸟窝大佬经过测试,并在《年Go生态圈rpc框架benchmark》(链接见参考5)一文中给出了测试结论。

说了这么多,收回话题,总结一句话就是:Getty只考虑使用Go语言原生的网络接口,如果遇到网络性能瓶颈也只会在自身层面寻找优化突破点。

Getty每年都会一次重大的升级,本文给出Getty近年的几次重大升级。

1、GoroutinePool

Getty初始版本针对一个网络连接启用两个goroutine:一个goroutine进行网络字节流的接收、调用Reader接口拆解出网络包(package)、调用EventListener.OnMessage()接口进行逻辑处理;另一个goroutine负责发送网络字节流、调用EventListener.OnCron()执行定时逻辑。

后来出于提升网络吞吐的需要,Getty进行了一次大的优化:将逻辑处理这步逻辑从第一个goroutine任务中分离,添加GoroutinePool专门处理网络逻辑。

即网络字节流接收、逻辑处理和网络字节流发送都有单独的goroutine处理。

GrPool成员有任务队列和Gr数组以及任务,根据N的数目变化其类型分为可伸缩Grpool与固定大小Grpool。可伸缩GrPool好处是可以随着任务数目变化增减N以节约CPU和内存资源。

1.1固定大小GrPool

按照M与N的比例,固定大小GrPool又区分为1:1、1:N、M:N三类。

1:N类型的GrPool最易实现,个人年在项目kafka-connect-elasticsearch中实现过此类型的GrPool:作为消费者从kafka读取数据然后放入消息队列,然后各个workergr从此队列中取出任务进行消费处理。

这种模型的Grpool整个pool只创建一个chan,所有Gr去读取这一个chan,其缺点是:队列读写模型是一写多读,因为gochannel的低效率造成竞争激烈,当然其网络包处理顺序更无从保证。

Getty初始版本的Grpool模型为1:1,每个Gr多有自己的chan,其读写模型是一写一读,其优点是可保证网络包处理顺序性,如读取kafka消息时候,按照kafkamessage的key的hash值以取余方式将其投递到某个taskqueue,则同一key的消息都可以保证处理有序。但这种模型的缺陷:每个task处理要有时间,此方案会造成某个Gr的chan里面有task堵塞,就算其他Gr闲着,也没办法处理之。

更进一步的1:1模型的改进方案:每个Gr一个chan,如果Gr发现自己的chan没有请求,就去找别的chan,发送方也尽量发往消费快的协程。这个方案类似于goruntime内部的MPG调度算法使用的goroutine队列,但其算法和实现会过于复杂。

Getty后来实现了M:N模型版本的Grpool,每个taskqueue被N/M个Gr消费,这种模型的优点是兼顾处理效率和锁压力平衡,可以做到总体层面的任务处理均衡,Task派发采用RoundRobin方式。

其整体实现如上图所示。具体代码实现请参见grpool连接中的TaskPool实现。

1.2无限制GrPool

使用固定量资源的Grpool,在请求量加大的情况下无法保证吞吐和RT,有些场景下用户希望尽可能用尽所有的资源保证吞吐和RT。

后来借鉴"AMillionWebSocketsandGo"一文中的“Goroutinepool”实现了一个可无限扩容的grpool。

具体代码实现请参见grpool连接中的taskPoolSimple实现。

1.3网络包处理顺序

固定大小的grpool优点是限定了逻辑处理流程对机器CPU/MEMORY等资源的使用,而无限制GrPool虽然保持了弹性但有可能耗尽机器的资源导致容器被内核杀掉。但无论使用何种形式的grpool,getty无法保证网络包的处理顺序。

譬如Getty服务端收到了同一个客户端发来的A和B两个网络包,Grpool模型可能造成服户端先处理B包后处理A包。同样,客户端也可能先收到服务端对B包的response,然后才收到A包的response。

如果客户端的每次请求都是独立的,没有前后顺序关系,则带有Grpool特性的Getty不考虑顺序关系是没有问题的。如果上层用户

转载请注明:http://www.0431gb208.com/sjszlfa/3151.html