文章目录

  • 1. netty高并发架构设计精髓
    • ①:主从、Reactor线程模型
    • ②:NIO多路复用非阻塞
    • ③:无锁串行化设计思想
    • ④:高可用、可扩展架构
    • ⑤:直接内存和零拷贝
    • ⑥:ByteBuf内存池设计
    • ⑦:灵活的TCP参数配置能力
  • 2. ByteBuf扩容机制
  • 3. 业务Handler的生命周期回调顺序

1. netty高并发架构设计精髓

netty作为客户端与服务端通信组件,可以支持极大的并发连接,那么它在处理高并发时有什么值得借鉴的设计思路呢?

①:主从、Reactor线程模型

  1. 主从:用主线程组boosGroup中的NioEventLoop来接受请求,生成客户端channel,并把客户端channel注册到WorkerGroup中的NioEventLoop中去。从线程组WorkerGroup负责处理所有的读写请求
  2. Reactor线程模型:基于事件响应的模型,底层使用epoll函数,使用操作系统的硬中断方式判断如果连接有事件响应,就把该连接放入就绪事件列表中,后续只处理就绪事件列表中的连接!避免空轮询

②:NIO多路复用非阻塞

  1. 多路复用:netty底层使用NIO多路复用器selector,实现模式为把多个连接(请求)放入集合中,只用一个线程可以处理多个请求(连接),也就是多路复用。不同于BIO的一个连接开启一个线程。
  2. 非阻塞:表现为线程不会一直在等待,把连接加入集合后,线程会一直轮询集合中的连接,有则处理,无则继续接受请求。

③:无锁串行化设计思想

在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。从表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

NIO的多路复用只允许一个线程处理所有请求的读写事件,其实就是一种无锁串行化的设计思想。这种思想也被应用到了 redis 和 netty 的线程模型中,这也是redis 和 netty不仅能保证高并发,且不需要加锁的原因。

Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的自定义Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。

④:高可用、可扩展架构

Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来,让你可以专注业务的开发,而不必兼顾网络io层面的问题。在使用时只需要在ChindHandler中嵌入自己的业务Handler即可。netty架构代码解耦、可扩展性强,支持高性能序列化协议(自定义protostuff编解码序列化机制)。

⑤:直接内存和零拷贝

直接内存:

要弄清楚零拷贝就要先搞明白直接内存的含义。直接内存也称为堆外内存,它并不是java虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,而是指机器的物理内存。元空间对应的内存也叫作直接内存,某些情况下这部分内存也会被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

JDK的ByteBuffer类提供了对应的方法去分配堆内存和堆外内存

   //分配堆内存       ByteBuffer buffer = ByteBuffer.allocate(1000);//分配直接内存    ByteBuffer buffer = ByteBuffer.allocateDirect(1000);

allocate:分配堆内存,写入的数据被放在堆内存中
allocateDirect:分配堆外内存,使用C语言的函数开辟一块堆外内存空间,把写入的数据被放在堆外内存中,并通过堆内存的局部变量引用(DirectByteBuffer)指向堆外内存地址。

堆外内存的申请

ByteBuffer.allocateDirect(1000)底层通过unsafe.allocateMemory(size)实现,接下去看看在JVM层面是如何实现的:

        可以发现,最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。

堆外内存的回收

JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,这个Cleaner对象会在DirectByteBuffer对象被GC回收后执行unsafe.freeMemory(address),从而回收这块堆外内存。

当初始化一块堆外内存时,对象的引用关系如下:

其中 first 是Cleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。

如果该DirectByteBuffer对象在一次GC中被回收了,如下所示:


此时,只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。

Cleaner对象的clean方法主要有两个作用:

  1. 把自身从Clener链表删除,从而在下次GC时能够被回收
  2. 释放堆外内存

如果JVM一直没有执行FGC的话,无效的Cleaner对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆?

其实在初始化DirectByteBuffer对象时,如果当前堆外内存的条件很苛刻时,会主动调用System.gc()强制执行FGC。

 //判断是否有足够的直接内存空间分配,可通过-XX:MaxDirectMemorySize=<size>参数指定直接内存最大可分配空间,如果不指定默认为最大堆内存大小,    //在分配直接内存时如果发现空间不够会显示调用System.gc()触发一次full gc回收掉一部分无用的直接内存的引用对象,同时直接内存也会被释放掉    //如果释放完分配空间还是不够会抛出异常java.lang.OutOfMemoryError   Bits.reserveMemory(size, cap);

使用直接内存的优缺点

优点

  1. 直接内存访问效率高,访问速度比堆内存快
  2. 不占用堆内存空间,减少了发生GC的可能
  3. 使用直接内存 可以减少一次数据拷贝(直接内存 >> 系统调用 >> 硬盘/网卡),如果使用堆内存则需要经历(堆内存 >> 直接内存 >> 系统调用 >> 硬盘/网卡)多一次拷贝过程

缺点

  1. 初始分配较慢
  2. 没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存耗完。我们可以指定直接内存的最大值,通过 -XX:MaxDirectMemorySize 来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉

零拷贝

在客户端与服务端交互的过程中

  1. 如果使用传统的JVM堆内存,有数据发送时,服务端会调用unsafe.read()方法读取socketChannel中的数据,这个read函数是操作系统来执行的。由于JVM无法直接读取socketChannel中的数据,需要操作系统把socketChannel中的ByteBuf数据拷贝一份到操作系统的直接内存中去,然后jvm再从直接内存中拷贝一份到jvm的堆内存中使用。读数据这个过程经历了两次拷贝。
  2. 如果使用直接内存(堆外内存),在处理客户端与服务端连接时,read函数把socketChannel中的ByteBuf数据拷贝到操作系统的直接内存中去就完事了,不再进行 直接内存 >> jvm堆内存 的拷贝过程。jvm内部通过直接内存的引用 管理着ByteBuf数据。

零拷贝并不是说没有拷贝过程,而是减少了用户空间和内核空间的数据相互拷贝,增加了处理效率,netty正是使用零拷贝保证了客户端和服务端交互的高性能。堆内存和直接内存示意图如下:

注:

  1. jvm内存被称为用户空间
  2. 直接内存被称为内核空间

从哪里看出netty默认使用的内核空间呢?channelRead方法会接收来自客户端的消息

⑥:ByteBuf内存池设计

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer(相当于一个内存块),情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于ByteBuf内存池的缓冲区重用机制。需要的时候直接从池子里获取ByteBuf使用即可,使用完毕之后就重新放回到池子里去。

⑦:灵活的TCP参数配置能力

合理设置TCP参数在某些场景下对于性能的提升可以起到显著的效果,例如接收缓冲区SO_RCVBUF和发送缓冲区SO_SNDBUF。如果设置不当,对性能的影响是非常大的。通常建议值为128K或者256K。

Netty在启动辅助类ChannelOption中可以灵活的配置TCP参数,满足不同的用户场景。

2. ByteBuf扩容机制

当我们向ByteBuf中写入数据时

        for (int i = 0; i < 8; i++) {byteBuf.writeByte("客户端数据")}

如果ByteBuf原内存不够,则会触发扩容机制,要了解ByteBuf的扩容,我们需要先了解ByteBuf中定义的几个成员变量,再从源码的角度来分析扩容。

上图三个核心成员变量释义如下:

  1. minNewCapacity:表用户需要写入的值大小
  2. threshold:阈值,为Bytebuf内部设定容量的最大值
  3. maxCapacity:Netty最大能接受的容量大小,一般为int的最大值

进入源码 ByteBuf.writeByte() >> AbstractByteBuf >> calculateNewCapacity


注意:为了防止ByteBuf容量大于阈值4MB后,还按照指数倍增长,导致空间浪费,所以在ByteBuf容量大于阈值4MB后,采用步进4MB的方式扩容,例如 4MB > 8MB > 16MB > …

3. 业务Handler的生命周期回调顺序

在使用netty时,我们大部分情况下只需要在childHandler中添加自己的业务Handler即可,如下面代码中的NettyServerHandler

  serverBootstrap.group(boosGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {//对workerGroup的SocketChannel设置处理器,设置客户端发来的数据的处理逻辑socketChannel.pipeline().addLast(new NettyServerHandler());}});

NettyServerHandler 需要实现出站/入站接口,并重写其方法,那么其内部的方法调用顺序是怎样呢?

handler的生命周期回调接口调用顺序如下:

handlerAdded >> channelRegistered >> channelActive >> channelRead >> channelReadComplete >> channelInactive >> channelUnRegistered >> handlerRemoved

  1. handlerAdded: 新建立的连接会按照初始化策略,把handler添加到该channel的pipeline里面,也就是channel.pipeline.addLast(new NettyServerHandler )执行完成后的回调;
  2. channelRegistered: 当该连接分配到具体的worker线程后,该回调会被调用。
  3. channelActive:channel的准备工作已经完成,所有的pipeline添加完成,并分配到具体的线上上,说明该channel准备就绪,可以使用了。
  4. channelRead:客户端向服务端发来数据,每次都会回调此方法,表示有数据可读;
  5. channelReadComplete:服务端每次读完一次完整的数据之后,回调该方法,表示数据读取完毕;
  6. channelInactive:当连接断开时,该回调会被调用,说明这时候底层的TCP连接已经被断开了。
  7. channelUnRegistered: 对应channelRegistered,当连接关闭后,释放绑定的workder线程;
  8. handlerRemoved: 对应handlerAdded,将handler从该channel的pipeline移除后的回调方法。

最后附上:netty源码流程图

netty的零拷贝、架构设计、ByteBuf扩容机制详解相关推荐

  1. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  2. 架构设计:远程调用服务架构设计及zookeeper技术详解(上篇)

    一.序言 Hadoop是一个技术生态圈,zookeeper是hadoop生态圈里一个非常重要的技术,当我研究学习hadoop的相关技术时候,有两块知识曾经让我十分的困惑,一个是hbase,一个就是zo ...

  3. kafka 同步提交 异步_详解Kafka设计架构核心——Kafka副本机制详解

    所谓的副本机制(Replication),也可以称之为备份机制,通常是指分布式系统在多台网络互联的机器上保存有相同的数据拷贝.副本机制有什么好处呢? 1. 提供数据冗余.即使系统部分组件失效,系统依然 ...

  4. Hashmap实现原理及扩容机制详解

    目录 HashMap基础 HashMap实现原理 Node和Node链 拉链法 关于Node数组 table 散列算法 HashMap和红黑树 关于TreeNode 红黑树基础 HashMap扩容机制 ...

  5. JVM 架构解释 + 垃圾回收机制 详解(基于JDK8版本)

    文章目录 1. JVM 内存结构 2. JVM 之 堆 3. JVM 之 垃圾回收器(GC, Garbage Collector) 3.1 垃圾回收器 分类 + 组合方式 3.2 年轻代的 垃圾处理器 ...

  6. Netty高并发高性能架构设计NIO空轮训BUG

    Netty高并发高性能架构设计&NIO空轮训BUG Netty高并发高性能架构设计 Netty线程模型 Netty主从Reactor模型设计的精髓 无锁串行化设计思想 零拷贝 直接内存 Net ...

  7. MySQL系列---架构与SQL执行流程详解

    文章目录 1. 背景 2. 架构体系 2.1 架构图 2.2 模块详解 2.3 架构分层 3. 查询SQL语句执行流程 3.1 连接 3.1.1 MySQL支持的通信协议 3.1.2 通信方式 3.2 ...

  8. 最新NVIDIA Ada Lovelace架构 和 RTX 40系列详解

    最新NVIDIA Ada Lovelace架构 和 RTX 40系列详解 Nvidia 的 Ada 架构和 GeForce RTX 40 系列显卡计划于 10 月 12 日开始到货,从 GeForce ...

  9. 【Image captioning】Show, Attend, and Tell 从零到掌握之三--train.py代码详解

    [Image captioning]Show, Attend, and Tell 从零到掌握之三–train.py代码详解 作者:安静到无声 个人主页 作者简介:人工智能和硬件设计博士生.CSDN与阿 ...

最新文章

  1. 我在哪里可以在线找到Android源代码? [关闭]
  2. 《走遍中国》珍藏版(四)
  3. eslint vscode 自动格式化_使用 VSCode 的必备三大神器,这才是开发 Vue 的真香解决方案...
  4. mysql binlog DDL_mysql一个事务中有DDL语句的binlog情况
  5. 四种方法下载网络文本数据到本地内存
  6. oracle 数字处理函数,ORACLE 数字型函数
  7. GTJ2018如何导出全部工程量_新清单计量规范征求意见稿第二期来啦!来看看那些让你烦恼的操作如何解决...
  8. 绑定流详解——网络测试仪实操
  9. switchhost
  10. ds18b20温度转换指令_使用STM32控制DS18B20温度传感器
  11. Apicloud+Vue开发App专题
  12. 多媒体开发之视频格式---1080p逐行和1080i隔行
  13. python写微信小程序商城,oejia_weshop
  14. Flexbox布局基础入门
  15. Qt动态图表更新实现
  16. 文学随笔:《错过独白》
  17. win7系统wifi没有网络连接到服务器,Win7无线Wifi无法连接怎么办?win7系统Wifi无法连接的三种故障原因及解决方法...
  18. 华为交换机MPU、LPU硬件信息解释
  19. R 语言怎么保存工作目录到当前路径_第一讲 R基本介绍及安装
  20. 马斯洛的五大需求层次理论

热门文章

  1. Django Bootstrap开发笔记01 - 添加简单网页
  2. 计算机行业的pest分析,2014-2018年中国电脑外设行业市场发展现状及未来趋势调研报告...
  3. 启动修复可以尝试将您的计算机还原到,如何修复Windows Bootloader问题(如果您的计算机不启动) | MOS86...
  4. nginx 调用dll_使用DLL中的资源
  5. 美国百亿亿次超算要比中国晚两三年,但真实性能更恐怖
  6. 冲刺!11.14-11.15
  7. 阻止SPY++类似的程序捕捉软件窗口
  8. 1.[数据结构和算法分析笔记]数组
  9. 一起谈.NET技术,ASP.NET MVC2.0在Tab页中实现异步无刷新分页
  10. oracle 10g SQL* Plus主机字符串