Netty核心知识整理
2022-11-10 14:59:48 26 举报
AI智能生成
Netty核心知识整理
作者其他创作
大纲/内容
Netty是什么<br>
Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。<br>
Netty是基于nio的,它封装了jdk的nio<br>
使用场景:公司项目定制私有化协议/自己的开源项目中参考RocketMQ对Netty的使用定制的协议<br>
NIOEventLoopGroup源码?<br>
每个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。<br>
Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事件进行通知,传播方向是 tail到head。<br>Inbound 事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头到尾。
epoll空轮训 cpu 100%的bug解决
重要组件<br>
1 Chanel和EventLoop的关系?<br>
Channel 为 Netty 网络操作(读写等操作)抽象类,每个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。<br>
2 channelHandler和ChannelPipeline之间的关系?<br>
channelHandler是消息的具体处理器,负责处理读写操作,客户端连接等。channelPipeline是handler的链,提供了一个用于沿着链传播的入站和出站的事件流的API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。<br>
3 EventloopGroup 了解么?和 EventLoop 啥关系?
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程)EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。
NioEventLoopGroup 默认的构造函数会起多少线程?
DEFAULT_EVENT_LOOP_THREADS 的值为CPU核心数*2
ByteBuf
支持自动扩容(4M),保证put方法不会抛出异常、通过内置的复合缓冲类型,实现零拷贝(zero-copy);<br>
不需要调用flip()来切换读/写模式,读取和写入索引分开;方法链;<br>
引用计数基于AtomicIntegerFieldUpdater用于内存回收;<br>
PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象
零拷贝<br>
普通IO操作<br>
File file = new File("XXX.txt");<br>RandomAccessFile raf = new RandomAccessFile(file,"rw");<br>byte[] arr = new byte[(int)file.length()];<br>raf.read(arr);<br><br>Socket socket = new ServerSocket(8080).accept();<br>socket.getOutputStreadm().write(arr);<br>
read:用户态 -> 内核态 && 磁盘数据 -> DMA引擎拷贝 -> 内核缓冲区<br>
read:用户态 -> 内核态 && 磁盘数据 -> DMA引擎拷贝 -> 内核缓冲区<br>
write:用户态 -> 内核态 && 用户缓冲区数据 -> CPU拷贝 -> Socket缓冲区
内核态 -> 用户态&&Socket缓冲区数据 -> DMA引擎拷贝 -> 网络协议引擎
即普通的IO操作,需要执行四次内核态切换 && 四次数据拷贝<br>
mmap内存映射技术<br>
直接将磁盘文件数据映射到内核缓冲区,这个映射过程是基于DMA拷贝的<br>
同时用户缓冲区是跟内核缓冲区共享一块映射数据的<br>
建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了<br>
即减少了一次数据拷贝<br>
零拷贝<br>
基于linux提供的sendfile,也就是零拷贝技术<br>
read:用户态 -> 内核态 , 磁盘数据 -> DMA拷贝 -> 内核缓冲区,同时从内核缓冲区拷贝一些offset和length到Socket缓冲区<br>
read:用户态 -> 内核态 , 磁盘数据 -> DMA拷贝 -> 内核缓冲区,同时从内核缓冲区拷贝一些offset和length到Socket缓冲区<br>
Kafka源码,transferFrom和transferTo两个方法,<br>从磁盘上读取文件,把数据通过网络发送出去
这个offset和length的量很少,几乎可以忽略不计<br>
Netty零拷贝<br>
具体
使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。<br>
ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。<br>
通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.<br>
vs OS zero-copy<br>
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。<br>
参考链接TODO<br>
https://zhuangxiaoyan.blog.csdn.net/article/details/116447618<br>
IO多路复用<br>
1、概念:IO多路复⽤是指内核⼀旦发现进程指定的⼀个或者多个IO条件准备读取,它就通知该进程<br>
2、优势:与多进程和多线程技术相⽐,I/O多路复⽤技术的最⼤优势是系统开销⼩,系统不必创建进程/线程,也不必维护这些进程/线程,从⽽⼤⼤减⼩了系统的开销。<br>
3、系统:⽬前⽀持I/O多路复⽤的系统调⽤有 select,pselect,poll,epoll。<br>
select:select⽬前⼏乎在所有的平台上⽀持。<br>
select的⼀个缺点在于单个进程能够监视的⽂件描述符的数量存在最⼤限制,在Linux上⼀般为1024,可以通过修改宏定义甚⾄重新编译内核的⽅式提升这⼀限制,但是这样也会造成效率的降低。
其良好跨平台⽀持也是它的⼀个优点
poll:它没有最⼤连接数的限制,原因是它是基于链表来存储的,但是同样有⼀个缺点:<br>a. ⼤量的fd的数组被整体复制于⽤户态和内核地址空间之间,⽽不管这样的复制是不是有意义。<br>b. poll还有⼀个特点是“⽔平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll跟select都能提供多路I/O复⽤的解决⽅案。在现在的Linux内核⾥有都能够⽀持,其中epoll是Linux所特有,⽽select则应该是<br>POSIX所规定,⼀般操作系统均有实现。
NIOEventLoopGroup 源码?<br>
NioEventLoopGroup(其实是 MultithreadEventExecutorGroup) 内部维护一个类型为<br>EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化<br>EventExecutor 时 NioEventLoopGroup 重载 newChild 方法,所以 children 元素的实际类型为<br>NioEventLoop。<br>线程启动时调用 SingleThreadEventExecutor 的构造方法,执行 NioEventLoop 类的 run 方<br>法,首先会调用 hasTasks()方法判断当前 taskQueue 是否有元素。如果 taskQueue 中有元<br>素,执行 selectNow() 方法,最终执行 selector.selectNow(),该方法会立即返回。如果<br>taskQueue 没有元素,执行 select(oldWakenUp) 方法<br>select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录 selector.select 方法的<br>执行次数和标识是否执行过 selector.selectNow(),若触发了 epoll 的空轮询 bug,则会反复<br>执行 selector.select(timeoutMillis),变量 selectCnt 会逐渐变大,当 selectCnt 达到阈值(默<br>认 512),则执行 rebuildSelector 方法,进行 selector 重建,解决 cpu 占用 100%的 bug。<br>rebuildSelector 方法先通过 openSelector 方法创建一个新的 selector。然后将 old selector 的<br>selectionKey 执行 cancel。最后将 old selector 的 channel 重新注册到新的 selector 中。<br>rebuild 后,需要重新执行方法 selectNow,检查是否有已 ready 的 selectionKey。<br>接下来调用 processSelectedKeys 方法(处理 I/O 任务),当 selectedKeys != null 时,调用<br>processSelectedKeysOptimized 方法,迭代 selectedKeys 获取就绪的 IO 事件的 selectkey 存<br>放在数组 selectedKeys 中, 然后为每个事件都调用 processSelectedKey 来处理它,<br>processSelectedKey 中分别处理 OP_READ;OP_WRITE;OP_CONNECT 事件。<br>最后调用 runAllTasks 方法(非 IO 任务),该方法首先会调用 fetchFromScheduledTaskQueue<br>方法,把 scheduledTaskQueue 中已经超过延迟执行时间的任务移到 taskQueue 中等待被执<br>行,然后依次从 taskQueue 中取任务执行,每执行 64 个任务,进行耗时检查,如果已执行<br>时间超过预先设定的执行时间,则停止执行非 IO 任务,避免非 IO 任务太多,影响 IO 任务<br>的执行。<br>每个 NioEventLoop 对应一个线程和一个 Selector,NioServerSocketChannel 会主动注册到某<br>一个 NioEventLoop 的 Selector 上,NioEventLoop 负责事件轮询。<br>Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事<br>件进行通知,传播方向是 tail 到 head。Inbound 事件发起者是 unsafe,事件的处理者是<br>Channel, 是通知事件,传播方向是从头到尾。<br>内存管理机制,首先会预申请一大块内存 Arena,Arena 由许多 Chunk 组成,而每个 Chunk<br>默认由 2048 个 page 组成。Chunk 通过 AVL 树的形式组织 Page,每个叶子节点表示一个<br>Page,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被<br>分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都<br>已被分配了。大于 8k 的内存分配在 poolChunkList 中,而 PoolSubpage 用于分配小于 8k 的<br>内存,它会把一个 page 分割成多段,进行内存分配。<br>ByteBuf 的特点:支持自动扩容(4M),保证 put 方法不会抛出异常、通过内置的复合缓冲<br>类型,实现零拷贝(zero-copy);不需要调用 flip()来切换读/写模式,读取和写入索引分<br>开;方法链;引用计数基于 AtomicIntegerFieldUpdater 用于内存回收;PooledByteBuf 采用<br>二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区<br>对象。UnpooledHeapByteBuf 每次都会新建一个缓冲区对象。<br>
Netty 的零拷贝实现?<br>
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读<br>写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM 会将堆内存<br>Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。ByteBuffer 由 ChannelConfig 分配,<br>而 ChannelConfig 创建 ByteBufAllocator 默认使用 Direct Buffer<br>CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过<br>内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。addComponents 方法将 header<br>与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在 CompositeByteBuf 内部都是单<br>独存在的, CompositeByteBuf 只是逻辑上是一个整体<br>通过 FileRegion 包装的 FileChannel.tranferTo 方法 实现文件传输, 可以直接将文件缓冲区<br>的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。<br>通过 wrap 方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty<br>ByteBuf 对象, 进而避免了拷贝操作。<br>Selector BUG:若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮<br>询,CPU 使用率 100%,<br>Netty 的解决办法:对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进<br>行一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 epoll 死循环 bug。重建<br>Selector,判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的<br>Selector 上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。
Netty的特点<br>
一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持<br>使用更高效的 socket 底层,对 epoll 空轮询引起的 cpu 占用飙升在内部进行了处理,避免<br>了直接使用 NIO 的陷阱,简化了 NIO 的处理方式。<br>采用多种 decoder/encoder 支持,对 TCP 粘包/分包进行自动化处理<br>可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持<br>可配置 IO 线程数、TCP 参数, TCP 接收和发送缓冲区使用直接内存代替堆内存,通过内存<br>池的方式循环利用 ByteBuf<br>通过引用计数器及时申请释放不再引用的对象,降低了 GC 频率<br>使用单线程串行化的方式,高效的 Reactor 线程模型<br>大量使用了 volitale、使用了 CAS 和原子类、线程安全类的使用、读写锁的使用<br>4.Netty 的线程模型?<br>Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,<br>boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收<br>到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work<br>线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。<br>单线程模型:所有 I/O 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个<br>Reactor 线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请<br>求或应答/响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度<br>慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。<br>多线程模型:有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接<br>请求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线<br>程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发<br>操作问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性<br>能不足问题。<br>主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel<br>从主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上,用于<br>处理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作;
Netty性能调优<br>
如何做到数百万台车联网设备同时在线 0 故障<br>
参考链接:https://zhuanlan.zhihu.com/p/283055781<br>
思路:RabbitMQ的扩容很鸡肋,kafka扩容会有平滑过渡的问题,最终采用阿里云版本的RocketMQ<br>
服务端客户端启动流程<br>
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理<br> EventLoopGroup bossGroup = new NioEventLoopGroup(1);<br> EventLoopGroup workerGroup = new NioEventLoopGroup();<br> try {<br> //2.创建服务端启动引导/辅助类:ServerBootstrap<br> ServerBootstrap b = new ServerBootstrap();<br> //3.给引导类配置两大线程组,确定了线程模型<br> b.group(bossGroup, workerGroup)<br> // (非必备)打印日志<br> .handler(new LoggingHandler(LogLevel.INFO))<br> // 4.指定 IO 模型<br> .channel(NioServerSocketChannel.class)<br> .childHandler(new ChannelInitializer<SocketChannel>() {<br> <span class="tag">@Override</span><br> public void initChannel(SocketChannel ch) {<br> ChannelPipeline p = ch.pipeline();<br> //5.可以自定义客户端消息的业务处理逻辑<br> p.addLast(new HelloServerHandler());<br> }<br> });<br> // 6.绑定端口,调用 sync 方法阻塞知道绑定完成<br> ChannelFuture f = b.bind(port).sync();<br> // 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)<br> f.channel().closeFuture().sync();<br> } finally {<br> //8.优雅关闭相关线程组资源<br> bossGroup.shutdownGracefully();<br> workerGroup.shutdownGracefully();<br> }<br>
BIO vs NIO vs AIO<br>
BIO
Block IO , 顾名思义 , 同步阻塞<br> 原理:服务器通过一个 Acceptor 线程,负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。
弊端:典型的一请求一应答模式
如果有大量客户端的时候,那么服务端的线程数量可能达到几千、几万甚至几十万<br> 然后服务器OOM
改良为用线程池的方式代替新增线程,被称为伪异步 IO 。<br>
但是高并发又会有各种排队和延时的问题<br>
NIO
New IO , 同步非阻塞,基于Reactor模型<br>
Buffer缓冲区,将数据写入Buffer中,然后从Buffer中读取数据Buffer缓冲区<br>
IntBuffer、LongBuffer、CharBuffer等很多种针对基础数据类型的Buffer<br>
Channel,NIO中都是通过Channel来进行数据读写的<br>
Selector,多路复用器<br>
selector会不断轮询注册的channel,如果某个channel上发生了读写事件,selector就会将这些channel获取出来<br>
一个Selector就通过一个线程,就可以轮询成千上万个channel,这就意味着你的服务端可以介入成千上万的客户端<br>
核心即非阻塞,selector一个线程就可以不停的轮询缠你了,所有客户端请求都不会阻塞,直接就会进来,大不了排下队<br>
NIO的优化思想就是一个请求一个线程,只有某个客户端发送了一个请求的时候,才会启动一个线程来处理<br>
处理时还是要先读取数据,处理,再返回的,这是个同步的过程<br>
IO磁盘操作时需不断的while询问CPU是否处理完成<br>
AIO<br>
Async IO, 异步非阻塞,基于Proactor模型<br>
每个连接发送过来的请求,都会绑定一个buffer,然后通知操作系统去一部完成读<br>
此时你的程序是回去干别的事儿的,等操作系统完成数据读取后,就会回调你的接口,给你操作系统异步读完的数据<br>
然后你对这个数据处理一下,接着将结果往回写<br>
写的时候也是个操作系统一个buffer,让操作系统自己获取数据去完成写操作,写完以后再回来通知你<br>
工作线程(读写同理)<br>
读取数据的时候,提供给操作系统一个buffer,空的,然后你就可以干别的事儿了,把读数据的事儿交给系统去干<br>
内核读数据将数据放入buffer,完事了,来回调你的一个接口,告诉你说,ok,数据读好了<br>
同步vs异步,阻塞非阻塞<br>
BIO<br> 用BIO的流读写文件时,你发起个IO请求直接hang死,必须等搞完了这次IO才能返回<br> 这个针对的是磁盘文件的IO读写<br> FileInputStream ,BIO,卡在那儿,直到你读写完成了才可以
NIO<br> 通过NIO的FileChannel发起个文件IO操作,其实发起之后就返回了,你可以干别的事儿,这就是非阻塞<br> 但是接下来你还得不断地去轮询操作系统,看IO操作完事儿了没有
AIO<br> 通过AIO发起个文件IO操作之后,你立马就可以返回干别的事儿了,接下来你也不用管了<br> 操作系统自己干完了IO之后,告诉你说ok了。<br> 同步就是你自己还得主动去轮询操作系统,异步就是操作系统反过来通知你
BIO 是面向流的,NIO 是面向缓冲区的;BIO 的各种流是阻塞的。而 NIO 是非阻塞的;BIO<br>的 Stream 是单向的,而 NIO 的 channel 是双向的。<br>
再深入理解:https://mp.weixin.qq.com/s/39Q4iG6XGjGp7K_zmlpnpA<br>
Netty高性能表现在?<br>
ByteBuf<br>
通过内存池重用 ByteBuf;ByteBuf 的解码保护;优雅停机:不再接收新消息、退出前的预处理操作、资源的释放操作。<br>
Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。<br>
高效并发编程 <br>
volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。IO 通信性能三原则:传输(AIO)、协议(Http)、线程(主从多线程)<br>
NIO的封装和优化<br>
零拷贝和mmap内存映射,尽量减少不必要的内存拷贝,实现了更高效率的传输<br>
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。<br>
reactor线程模型<br>
串行无锁化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁<br>
链路空闲检测机制,读/写空闲超时机制,idleStateHandler 类 用来检测会话状态<br>
TCP参数配置:SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K<br>
SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;<br>
TCP 粘包 / 拆包<br>
原因
要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。<br>待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。<br>要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。<br>接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。<br>
Netty解决方法<br>
定长消息解码器:FixedLengthFrameDecoder。发送方和接收方规定一个固定的消息长度,不够用空格等字符补全,这样接收方每次从接受到的字节流中读取固定长度的字节即可,长度不够就保留本次接受的数据,再在下一个字节流中获取剩下数量的字节数据。<br>
分隔符解码器:LineBasedFrameDecoder或DelimiterBasedFrameDecoder。LineBasedFrameDecoder是行分隔符解码器,分隔符为\n或\r\n;DelimiterBasedFrameDecoder是自定义分隔符解码器<br>
数据长度解码器:LengthFieldBasedFrameDecoder。将发送的消息分为header和body,header存储消息的长度(字节数),body是发送的消息的内容。<br>
Netty内存池<br>
https://www.jianshu.com/p/8d894e42b6e6<br>
Netty对象池<br>
https://www.jianshu.com/p/3bfe0de2b022<br>
TCP协议如何保证可靠传输<br>
TCP通过序列号、检验和、确认应答信号、重发控制、连接管理、窗口控制、流量控制、拥塞控制实现可靠性。<br>
https://www.jianshu.com/p/6aac4b2a9fd7<br>
0 条评论
下一页