Netty、t-io、Voovan 框架浅谈
Netty、t-io、Voovan 框架浅谈
愚民日记 发表于6个月前
Netty、t-io、Voovan 框架浅谈
  • 发表于 6个月前
  • 阅读 4194
  • 收藏 130
  • 点赞 10
  • 评论 43

腾讯云 新注册用户 域名抢购1元起>>>   

声明: 欢迎本着技术讨论为主的讨论者一起探讨。让我们一起共同维护好技术圈的和谐氛围。以下内容均为个人理解,如果不妥的地方请大家指出。如果发现错字,只能麻烦你暂时脑补一下。

    我作为 Voovan 的设计及主创人员,针对目前流行的几款框架在设计和功能方面做一个分析,分析的内容我选取了一些我个人比较关注的性能和开发的便利性有影响的点,当然一定会不够全面的,就像大家关注的点总是不同的,有人喜欢大长腿,有人喜欢大XX。这篇文章中不涉及性能的好坏评价(因为没有对三个框架做全面的横向评测),也不做框架设计优劣的评论,存在及合理。

    首先目的是要在熟悉和了解其他框架的过程中,学习他们好的设计思想和编码技巧,这里我不做结论性的陈述。目的是在于帮助开发者更好的选择适合自己的框架。

    在编写这篇文章时与 t-io 的作者进行了沟通,当然就我们两个人的理解也极有可能有错误的地方,欢迎大家指出错误,我们会尽快修复的。

 

以下是我对三个框架在设计或者说是编码特点中选取的几个我比较关注的点的对比图:

首先我们对几个关键的概念进行一些解析,方便大家更好的理解上面表中的概念:

  • NIO、AIO 的区别?

    在这里我们来看一下两者最明显的区别,NIO 是由 JDK 来处理异步事件的,就是说由 JDK 来探测系统缓冲区及Socket 的连接状态并通知用户事件被触发,最明显的就是编写 NIO 的时候我们需要对 Selector 进行处理,然后自己对事件进行处理,那么这个时候如果不使用线程来处理的话,就是一个同步的通信模型。而 AIO 这是由 JDK 所在的操作系统通知 JDK 事件被触发,这是基于 Linux 的 Epoll 或者 window 的 Iocp 来完成,且事件在被触发时就是在一个独立的线程中处理的。 理论上 AIO 的性能更好,如果传言是真的不知道为什么 Netty 的作者停止了对 Netty5 AIO 版本的维护。

 

  • 事件驱动

   大家都知道异步程序的编写必然伴随着不断的回调,而事件驱动就是将这些回调分类整理统一成不同的事件并出发,暴露给使用者,举个简单的例子,断开连接这个事件,广义的讲有两种情况:服务端主动断开,客户端主动断开,但是对于使用者来说都是断开,都需要出发 close 这个事件,所以框架就需要对这两种事件进行统一,在被触发时给用户一个 close,或者说调用用户的 close 回调。

   模拟事件驱动就是框架中没有建立事件处理模型,在整个框架的编码过程中在代码的不同位置统一事件的处理。

 

  • TCP/SSL

    首先说明一下只有 TCP 协议才能支持 SSL 通信。而 SSL 通信是通过使用非对称的密钥保证通信的内容不会被中间人进行攻击,因此现在众多的支付功能都采用 SSL 通信的形式,而不是很多朋友理解的加密,因为密钥是公开,所以服务端返回给客户端的信息是完全可以被解密。

 

  • 非堆内存及零拷贝
  1. 非堆内存: 首先JVM在管理对象的时候可以使用的有堆内存和非堆内存,堆内存由 JVM 自动管理,大家常见到的 OutOfMemroy 则由于堆内存被完全占用而导致,那么非堆内存不是 JVM 自动管理的,所以理论上可以申请到目前物理内存的最大值,而为什么要使用非对内存呢? 答案是应为 GC, 在高并发的情况下堆内存被不断被申请,当达到 GC 条件时 JVM 会停止响应进行无关联对象的回收,而使用非对内存的时候,由于对象的申请和释放都由开发人员手工实现并完成,所以在临时的某个对象或者缓冲区使用完后就可以进行针对性的释放,同时不占用堆内存,从而减少 GC 的次数,总提上减少 JVM 因 GC 导致的停顿。当然这对开发者也提出了更高的要求。因此理论上使用非堆内存都可以做到并发时的极低的内存消耗。

  2. 零拷贝:首先零拷贝的作用也是在于减少JVM堆内存消耗,主要目的是在从缓冲区接收到数据后对数据的解析或处理后在到达开发者提供的回调函数之前不对其进行 copy 操作 或者 降低 copy 操作,那么什么是 copy 操作呢,比如: HeapByteBuffer.get(byte[]) 方法会对数据进行一次拷贝(使用的是System.arraycopy),而DirectByteBuffer虽然分配的是非堆内存单在做 get 方法时也会将数据 copy 到堆内存中,而这种操作在大多数开发者中是会被频繁使用,所以不知道是哪位大神(具我的了解应当的 Netty 的开发者)提出了零拷贝,拯救了我们。

 

  • 粘包处理与业务分离

    这个可能就是在框架设计时如何给用户提供更好的体验的问题了(当然并不会取得每个人的欢心,有人爱有人恨,世间万物除了 money 皆是如此),目的是在于能够统一的封装粘包处理代码达到清晰且模块化复用的目的,就我个人而言,我非常厌恶将粘包处理代码编写到解包过滤器或者解码器,这样会导致我在开发新的系统或者某些功能是需要复制并不断的粘贴代码或者某个类,所以 voovan 提出了粘包处理与业务分离的方式,将粘包处理部分的代码作为一个独立的可插入的功能提供给开发者,当然如果用户喜欢在协议解析部分处理,只要在构造服务的时候不要注册粘包处理类,就可以在协议解析的部分自己处理了。

     2017-07-01: 关于Netty粘包处理大家有疑问,我在这里补充说明一下 Voovan 的粘包处理的不同:

      Voovan 的粘包处理是个独立的一个实现,和解包处理是分开的,这也就是我们在进行解包处理的时候不用考虑包是否完整,同时也方便丢弃那一部分非法的探测包(不需要读取再丢弃,仅仅操作一下指针即可:直接在canSplite方法中清空并重置bytebuffer就可以做到,无须内存操作)从而提升安全性. Voovan 在包不完整的时候会尝试等待一个完整的包。至于为什么拆分粘包处理为一个独立的逻辑? 个人认为判断包是否完整是一套独立的逻辑,可以写的很简单,独立出来对于相当多的一部分把解包处理和粘包处理用一套逻辑来处理的开发者而言,独立的粘包可以提示他们写一个很简单的逻辑来处理粘包,从而有效的提高判断完整报文运行效率。Voovan也提供透传(默认粘包处理器,即不设置粘包处理), 方便开发者保留自己的习惯在解包时处理粘包。当然最终这一切都取决开发者的选择。最后,并不是说 Netty 不支持粘包处理,粘包处理是无论什么语言所有涉及导到Socket 通信都必须要解决的问题,Netty 是否支持粘包处理,仅仅凭借常识推断就可以知道一定是有的。

  • 异步开发与同步开发

    异步开发与同步开发的本质区别在编码上就是是否使用回调,异步开发需要开发者注册回调的函数,供框架在需要时调用,不会对当前线程产生阻塞,没有阻塞也就意味着Socket 通信的缓冲区中的内容不会因为阻塞而长时间的等待造成并发性能的下降。同步开发则是用户调用 send 或者 recive 时线程是阻塞的必须等到有合规的内容时才会继续接收缓冲区内未处理的数据。

   那么是否是异步就会一定比同步好呢?答案是否定的,异步虽然性能高,但也同时加大了编码和调试的难度,所以我个人推荐仅在提供Socket服务的时候使用,因为我们无法预测并发情况,所以还是做万全的准备比较好。那么同步使用在作为Socket客户端时就有了他得天独后的优势,方便开发且方便调试。

 

  • 心跳及重连

    关于心跳及重连相信我就不做过多的描述了,简单介绍一下:

  1. 心跳:Socket服务端和客户端之间定时发送和应答的内容,用于判断连接是否断开以便通知开发者进行响应的处理。

  2. 重连:主要是指在Socket客户端通过心跳或者 Socket 事件发现连接被断开后自动的重新发起到服务端的连接。

 

  • 什么是 TCP 长连接和短连接?

        个人认为长连接和短连接没有实际本质的区别只是开发者使用场景的不同。

        TCP长连接: 长连接就是一个持续不断的 TCP 连接 ,  主要作用是在一次 connet 后,不断的进行无数组业务单元的信息传递,直到关闭,长连接长时间不断开最少会霸占一个线程进行事件监听,所以过多的客户端容易降低性能。典型场景: 部分IM通信软件,以及数据同步系统。

        TCP短连接:短连接就是在和服务端通信的过程中是一次connect 交互完一组业务单元后连接关闭,下次交互的过程中再建立连接。短连接虽然不会长期霸占一个线程用作监听,但他每次的连接和断开会消耗一定 IO 资源,但好在Socket 通信往往不像磁盘或者内存 IO 操作需要纳秒级的响应。

        就我个人的理解 HTTP1.0、HTTP1.1、HTTP2、网游服务端都是短连接的形式,一组相关的业务单元传输完毕后,等待超时后,就会主动关闭连接。一般 web 服务器的 keepalive 的超时时间都不会设置的太长,而且会根据当前线程的情况自动调整,线程越多超时时间越短,线程越少超时时间越长。

 

关于性能测试:

     并发性能的概念

        QPS(query per second)平均每秒请求数, 如每秒处理请求 10k 。

        BPS (bytes per second)平均每秒传输字节数, 如每秒传输80mb 。

        首先要说的是 QPS,这个是对异步通信框架线程和竞争锁的管理,线程和竞争锁管理的不好会数出现锁阻塞导致系统停止响应,测试 QPS 会导致不断的接受连接申请线程处理业务关闭连接释放线程等操作。调大线程池有助提高 QPS 的测试结果。

        个人对异步框架的测试QPS时的理解是连接->发送->响应->关闭为一个 QPS.因为这样才能相对准确的测试出其基线性能.

        例如:voovan 在 [连接->发送->响应->关闭 为 1 个QPS]  的测试情况下是10000+ 的QPS.

       而增加 keepalive 方式后 voovan 在 [连接->发送->响应->发送->响应->关闭 为 2 个QPS]  的测试情况下是18000+ 的QPS.

        可见不同的QPS 的定义对测试结果有相当大的影响.

        BPS 测试最大吞吐量是对内存和 IO 性能管理的考验,管理的不好的话就会出现频繁的 GC 停顿直到系统无响应或者 OutOfMemroy,测试BPS会不断在发送过程中充满整个Socket 缓冲区,充分启用网卡的能力发送数据。如果是从文件读数据,还要考虑你的磁盘性能。调大缓冲区设置有助于提高 BPS 的测试结果。

        QPS 实测得到的结果可能更适用于多数场景,因为无论是游戏服务端,APP 服务端还是日常的 Web 服务都是客户端数量无法估量,而每次交互过程中的数据量又是非常有限的,多则上百k,少则几 k,几十k,而且大多数应该是几 k,几十 k 的场景,所以网络上很多的测试案例更关注的是 QPS。

        而 BPS 的应用场景多处于有限个节点的大量数据同步,这个场景更关注的是吞吐性能,而非响应情况。

    大家如果自己进行性能测试的时候如果想要测试出某个框架的极限性能需要关注上面两个黑体字所提到的优化内容.因为每个框架对于出厂时的设置是由倾向的,最起码要站在一个相对公平的环境下进行测试。

    OK,写到这里我想要介绍的内容已经介绍完毕了,希望能够帮助大家更好的根据自己需要选择合适的异步通信框架为自己服务。

    欢迎一起探讨学习,如果有错误欢迎指正.

最后请允许我无耻的推广一下 Voovan,试一下说不定正是你想要的呢?

讨论请加入一下 QQ 群。

交流QQ群:454201740

开源协议:Apache v2 License

Voovan开源项目源代码主要托管于 Git@OSC.

Issues地址: Git@OSC

共有 人打赏支持
粉丝 56
博文 9
码字总数 14581
作品 4
评论 (43)
天籁111
厉害了,学习一下
小山羊
我去,错别字这么多
风云决
厉害了,来学习
渔泯小镇
不错, 后面可以出点本博客小标题的相关实战代码.
涉及过相关技术的都看得懂博客, 但新手看会云里雾里.
NoSuchMan
很不错得文章 受教了 也学习了很多错别字
HaydnSyx
想请教一下作者关于内存模型的知识,作者说的堆和非堆应该指的就是Java heap和native memory(本地内存)这两块吧(jdk7之后),但是我看还有一块叫做direct memory(直接内存),这个native memory和direct memory之间有什么区别吗?netty使用的是哪块内存作为零copy实现的呢?
愚民日记

引用来自“小山羊”的评论

我去,错别字这么多
写完后有工作上的事情要做,就没有校对,抱歉啊,不过我用的是拼音,相信木有太大问题,嘿嘿
愚民日记

引用来自“HaydnSyx”的评论

想请教一下作者关于内存模型的知识,作者说的堆和非堆应该指的就是Java heap和native memory(本地内存)这两块吧(jdk7之后),但是我看还有一块叫做direct memory(直接内存),这个native memory和direct memory之间有什么区别吗?netty使用的是哪块内存作为零copy实现的呢?
direct指的就是非堆内存,这个是JDK 对 Bytebuffer 的实现类的命名方式,意思是直接访问内存,而访问的内存在 JVM 中则是非堆内存,非堆内存指的应该是JVM进程內的未被JVM的 GC 管理的内存,可以理解为不会被GC自动回收的那部分内存,你所说的native memory应该和你说的direct memory指的都是非对内存,零拷贝主要是为了避免不断产生的临时对象占用对象计数和堆内的内存,所以尽量减少缓冲对象的生成.很直观零拷贝就是不会或者极少在 jvm 中通过拷贝内存产生临时对象,比如在流操作的时候是无法避免的,但是使用 bytebuffer 时可以大量避免,netty 应该是和Voovan 是采用类似的方式.
愚民日记

引用来自“渔泯小镇”的评论

不错, 后面可以出点本博客小标题的相关实战代码.
涉及过相关技术的都看得懂博客, 但新手看会云里雾里.
嗯嗯.之前写过一个 Voovan 入门教程 http://voovanturorial.mydoc.io/
后面会针对性的写一些入门的博客
杨()杨
文章非常干货,一语命中事物的本质!socket小白也能看明白
杨亮no9242
心跳功能对于局域网项目还是比较重要的,一般都会选择自己写,所以刚需程度较小.
罗格林
干货,非常支持。有一点问题:TPS 不是平均每秒传输量,是每秒事务数。参见:https://www.zhihu.com/question/21556347
愚民日记

引用来自“罗格林”的评论

干货,非常支持。有一点问题:TPS 不是平均每秒传输量,是每秒事务数。参见:https://www.zhihu.com/question/21556347
感谢指出,是我使用概念错误.谢谢
A_NOOB
厉害了
ming133
学习了!文章不错!
乌龟壳
感觉你的零拷贝的描述和netty的概念不一致。我试着说下netty的零拷贝。

Netty通过自定义的Buffer那一套类,实现在不进行任何Array.copy操作的前提下,实现创建新的Buffer
1. 多个Buffer零拷贝合并成一个
2. 零拷贝裁剪Buffer
3. 复合场景,零拷贝裁剪多个Buffer零拷贝合并成的Buffer

所以这个零拷贝主要是尽可能减少拷贝操作,和gc没有直接关系,new一个零拷贝的类实际就是个普通对象,还是要gc的,但是gc的只是这个零拷贝对象本身而已。
乌龟壳
我选netty就是因为它已经实现了比较多常用的网络协议,并非觉得它的设计多么好之类的。

基于这个立场想问下Voovan为何选择自己做一套呢?
愚民日记

引用来自“乌龟壳”的评论

感觉你的零拷贝的描述和netty的概念不一致。我试着说下netty的零拷贝。

Netty通过自定义的Buffer那一套类,实现在不进行任何Array.copy操作的前提下,实现创建新的Buffer
1. 多个Buffer零拷贝合并成一个
2. 零拷贝裁剪Buffer
3. 复合场景,零拷贝裁剪多个Buffer零拷贝合并成的Buffer

所以这个零拷贝主要是尽可能减少拷贝操作,和gc没有直接关系,new一个零拷贝的类实际就是个普通对象,还是要gc的,但是gc的只是这个零拷贝对象本身而已。

是的,你描述的是正确的,确实是netty零拷贝的实现,写博客的时候主要还是站在大多数读者能够理解的形式来描述~我只是举了一个例子,说明什么是零拷贝,以及他的作用,实际使用的时候还是要自己思考如何实现
愚民日记

引用来自“乌龟壳”的评论

我选netty就是因为它已经实现了比较多常用的网络协议,并非觉得它的设计多么好之类的。

基于这个立场想问下Voovan为何选择自己做一套呢?

最早是出于研究的兴趣,后来干脆开源出来,其实我主要还是出于对粘包处理的困扰,没有一个我觉得优雅的方式,也许有很多开发者不认可,到最早的出发点是自己用的舒服,嘿嘿
乌龟壳

引用来自“愚民日记”的评论

引用来自“乌龟壳”的评论

我选netty就是因为它已经实现了比较多常用的网络协议,并非觉得它的设计多么好之类的。

基于这个立场想问下Voovan为何选择自己做一套呢?

最早是出于研究的兴趣,后来干脆开源出来,其实我主要还是出于对粘包处理的困扰,没有一个我觉得优雅的方式,也许有很多开发者不认可,到最早的出发点是自己用的舒服,嘿嘿

回复@愚民日记 : 有点难以置信粘包会很难处理的样子。比如netty对http的支持来说,这里面隐含的逻辑就非常多,粘包处理只是其中一小块而已。粘包说白了就是边界划分的问题,两个方向,要么状态机字节级解析协议,自然不受粘包影响,要么协议实现高层分包协议比如len:data:len:data使得可以统一分包后再分发到下一层
×
愚民日记
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: