中间件知识点(Netty)二

深入Linux内核理解epoll

同步和异步
关注的是调用方是否主动获取结果
同步:同步的意思就是调用方需要主动等待结果的返回
异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,
回调函数等。

阻塞和非阻塞
关注等待结果是立即返回还是等待
阻塞:是指结果返回之前,当前线程被挂起,不做任何事
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。

进程内核空间和用户空间
1.物理内存是有限的(比如16G内存),怎么把有限的内存分配给不同的进程?
a.Linux 给每个进程虚拟出一块很大的地址空间,比如 32 位机器上进程的虚拟内存地址空间是 4GB,从 0x00000000 到 0xFFFFFFFF。但这 4GB 并不是真实的物理内存,而是进程访问到了某个虚拟地址,如果这个地址还没有对应的物理内存页,就会产生缺页中断,分配物理内存,MMU(内存管理单元)会将虚拟地址与物理内存页的映射关系保存在页表中,再次访问这个虚拟地址,就能找到相应的物理内存页。每个进程的这 4GB 虚拟地址空间分布如下图所示:
在这里插入图片描述
b.每个进程的虚拟地址空间总体分为用户空间和内核空间,低地址上的 3GB 属于用户空间,高地址的 1GB 是内核空间,这是基于安全上的考虑,用户程序只能访问用户空间,内核程序可以访问整个进程空间,并且只有内核可以直接访问各种硬件资源,比如磁盘和网卡。
c.那用户程序需要访问这些硬件资源该怎么办呢?
答案是通过系统调用,系统调用可以理解为内核实现的函数,比如应用程序要通过网卡接收数据,会调用 Socket 的 read 函数:
ssize_t read(int fd,void *buf,size_t nbyte)
CPU 在执行系统调用的过程中会从用户态切换到内核态,CPU 在用户态下执行用户程序,使用的是用户空间的栈,访问用户空间的内存;当 CPU 切换到内核态后,执行内核代码,使用的是内核空间上的栈。
2.进程用户空间
用户空间包含:
a.用户空间从低到高依次是代码区、数据区、堆、共享库与 mmap 内存映射区、栈、环境变量。
其中堆向高地址增长,栈向低地址增长。也就是说,当你向堆中分配内存时,分配的内存地址会逐渐增大。
b.mmap相关(零拷贝知识相关)
用户空间上还有一个共享库和 mmap 映射区,Linux 提供了内存映射函数 mmap, 它可将文件内容映射到这个内存区域,用户通过读写这段内存,从而实现对文件的读取和修改,无需通过read/write 系统调用来读写文件,省去了用户空间和内核空间之间的数据拷贝,Java 的MappedByteBuffer 就是通过它来实现的;用户程序用到的系统共享库也是通过 mmap 映射到了这个区域。
3.进程内核空间
内核空间包含:task_struct。
task_struct结构体本身是分配在内核空间,它的vm_struct成员变量保存了各内存区域的起始和终止地址,此外task_struct中还保存了进程的其他信息,比如进程号、打开的文件、创建的Socket 以及 CPU 运行上下文

阻塞与唤醒
1.在 Linux 中,线程是一个轻量级的进程,轻量级说的是线程只是一个 CPU 调度单元,因此线程有自己的task_struct结构体和运行栈区,但是线程的其他资源都是跟父进程共用的,比如虚拟地址空间、打开的文件和 Socket 等。
2.当用户线程发起一个阻塞式的 read 调用,数据未就绪时,线程就会阻塞,那阻塞具体是如何实现的呢?
(阻塞本质是将进程(上面说了线程也算是进程,轻量级进程,有自己的task_struct)的task_struct移出运行队列,添加到等待队列)
a.Linux 内核将线程当作一个进程进行 CPU 调度,内核维护了一个可运行的进程队列,所有处于TASK_RUNNING状态的进程都会被放入运行队列中,本质是用双向链表将task_struct链接起来,排队使用 CPU 时间片,时间片用完重新调度 CPU。所谓调度就是在可运行进程列表中选择一个进程,再从 CPU 列表中选择一个可用的 CPU,将进程的上下文恢复到这个 CPU 的寄存器中,然后执行进程上下文指定的下一条指令。
b.而阻塞的本质就是将进程的task_struct移出运行队列,添加到等待队列,并且将进程的状态的置为TASK_UNINTERRUPTIBLE或者TASK_INTERRUPTIBLE,重新触发一次 CPU调度让出 CPU。
3.线程是如何唤醒的呢?
(唤醒本质是将task_struct从等待队列移到运行队列,并将task_struct状态置为TASK_RUNNING)
线程在加入到等待队列的同时向内核注册了一个回调函数,告诉内核我在等待这个
Socket 上的数据,如果数据到了就唤醒我。这样当网卡接收到数据时,产生硬件中断,内核再通过调用回调函数唤醒进程。唤醒的过程就是将进程的task_struct从等待队列移到运行队列,并且将task_struct的状态置为TASK_RUNNING,这样进程就有机会重新获得CPU 时间片。
这个过程中,内核还会将数据从内核空间拷贝到用户空间的堆上。
当 read 系统调用返回时,CPU 又从内核态切换到用户态,继续执行 read 调用的下一行代码,并且能从用户空间上的 Buffer 读到数据了。

Socket Read 系统调用的过程
以Linux操作系统为例,一次socket read 系统调用的过程:
a.首先 CPU 在用户态执行应用程序的代码,访问进程虚拟地址空间的用户空间;
b.read 系统调用时 CPU 从用户态切换到内核态,执行内核代码,内核检测到
Socket 上的数据未就绪时,将进程的task_struct结构体从运行队列中移到等待队
列,并触发一次 CPU 调度,这时进程会让出 CPU;
c.当网卡数据到达时,内核将数据从内核空间拷贝到用户空间的 Buffer,接着将
进程的task_struct结构体重新移到运行队列,这样进程就有机会重新获得 CPU 时间
片,系统调用返回,CPU 又从内核态切换到用户态,访问用户空间的数据。
总结:
当用户线程发起 I/O 调用后,网络数据读取操作会经历两个步骤:
a.用户线程等待内核将数据从网卡拷贝到内核空间。
b.内核将数据从内核空间拷贝到用户空间(应用进程的缓冲区)。
各种 I/O 模型的区别就是:它们实现这两个步骤的方式是不一样的。

Linux 五种 I/O 模型
(NIO就是使用的I/O多路复用模型,所以能处理的连接数比较多,netty更多)
1.阻塞I/O(blocking I/O)
2.非阻塞I/O(nonblocking I/O)
3.I/O复用(select、poll和epoll)(I/O multiplexing)
4.信号驱动I/O(signal driven I/O(SIGIO))
5.异步I/O(asynchronous I/O)

I/O 模型是为了解决内存和外部设备速度差异的问题。

阻塞 IO 就是 JDK 里的 BIO 编程,IO 复用就是 JDK 里的 NIO 编程,Linux 下异步IO 的实现建立在 epoll 之上,是个伪异步实现,而且相比 IO 复用,没有体现出性能优势,使用不广。非阻塞 IO 使用轮询模式,会不断检测是否有数据到达,大量的占用 CPU 的时间,是绝不被推荐的模型。信号驱动 IO 需要在网络通信时额外安装信号处理函数,使用也不广泛。所以,一般使用第3种I/O模型-I/O复用,即NIO。

注意这里所说的非阻塞I/O(nonblocking I/O),并非Java的NIO(New IO)库。

I/O 复用模型(select、poll、epoll)
IO 复用需要使用两个系统调用(select 和 recvfrom),而 blocking IO 只调用了一个系统调用(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用
multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。
阻塞 IO 模型:
在这里插入图片描述
I/O 复用模型:
在这里插入图片描述

select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

IO复用(NIO)是同步的,非阻塞的?
epoll是同步的,其需要主动等待结果的返回。
epoll分两步,select和recvfrom。从I/O 复用模型可以看出select、recvfrom都是阻塞的。调用select用于判断数据是否就绪,其返回时是很快的,不用阻塞很长时间,但也算阻塞。等有数据返回的时候会收到通知然后调用recvfrom主动去调用拷贝数据。所以分开看的话,他们是阻塞的。
如果当成是一步来看的话,他们是非阻塞。

select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,并等待读写完成。

文件描述符 FD
fd 本质上就是一个非负整数。它是一个索引值,指向一个文件。
在 Linux 操作系统中,可以将一切都看作是文件,包括普通文件,目录文件,字符设备文件(如键盘,鼠标…),块设备文件(如硬盘,光驱…),套接字等等,所有一切均抽象成文件,提供了统一的接口,方便应用程序调用。
系统为了维护文件描述符建立了 3 个表:进程级的文件描述符表、系统级的文件描述符表、文件系统的 i-node 表。所谓进程级的文件描述符表,指操作系统为每一个进程维护了一个文件描述符表,该表的索引值都从从 0 开始的,所以在不同的进程中可以看到相同的文件描述符,这种情况下相同的文件描述符可能指向同一个实际文件,也可能指向不同的实际文件。

select、poll、epoll 的比较
1、支持一个进程所能打开的最大连接数
select:由FD_SETSIZE 宏定义,默认32*32
poll:poll 本质上和 select 没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll:连接数基本上只受限于机器的内存大小。
2、FD 剧增后带来的 IO 效率问题
select:每次调用时都会对连接进行线性遍历,所以随着 FD 的增加会造
成遍历速度慢的“线性下降性能问题”。
poll:同上
epoll:因为 epoll 内核中实现是根据每个 fd 上的 callback 函数来实现的,只
有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,
使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活
跃的情况下,可能会有性能问题。(所以会有多线程主从模式的实现,如netty)
3、 消息传递方式
select:内核需要将消息传递到用户空间,都需要内核拷贝动作
poll:同上
epoll:epoll 通过内核和用户空间共享一块内存来实现的。

为什么使用epoll,而不使用select/poll来进行多路复用,为什么epoll连接数能支持更高,能监听更多socket?
主要是从连接数和fd剧增后带来的IO效率、消息传递方式来考虑。具体可以看epoll 高效原理和底层机制分析。

select,poll,epoll如何选择?
在选择 select,poll,epoll 时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看 epoll 的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。
2、select 低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

epoll 高效原理和底层机制分析
(即通过eventpoll,通过减少遍历等优化来提升性能)(epoll基于事件响应编程,不像poll需要轮询,轮询所以pollfd,看是否有数据到达)
总结:
当某一进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,在内核cache 里除了建了个红黑树用于存储监视以后 epoll_ctl 传来的 socket 外,还会再建立一个 rdlist 双向链表,用于存储准备就绪的事件关联的socket集合,当 epoll_wait 调用时,仅仅观察这个 rdlist 双向链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。
同时,所有添加到 epoll 中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做 ep_poll_callback,它会把这样的事件放到上面的 rdlist 双向链表中。
当调用 epoll_wait 检查是否有发生事件的连接时,只是检查 eventpoll 对象中的 rdlist双向链表是否有 epitem 元素而已,如果 rdlist 链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此 epoll_waitx 效率非常高,可以轻易地处理百万级别的并发连接。

epoll设计原理
红黑树:内核cache创建的用于存储epoll_ctl 指定要监听的socket.
epoll_create(int size) 对应于NIO selector = Selector.open(),用于创建选择器Selector对象,管理多个通道。通过 Selector 可以注册多个 Channel,用于监听指定的事件类型,如读、写、连接、接收等事件。
epoll_ctl ,理解为NIO的里的 socketChannel.register()。用于指定要监听的fd(socket)的什么事件,监听socket读还是写等。
epoll_wait ,,当返回>0,说明有数据返回。
rdlist存放那些有数据发送过来的socket,rdlist是双向链表,其不放数据,是socket集合。
eventpoll对象由每个进程调用 epoll_create 方法时创建,具体是调用epoll_create系统方法后由内存创建,每个进程都会创建一个eventpoll。eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等待队列。
等待队列是个非常重要的结构,它指向所有需要等待该 socket 事件的进程。
即eventpoll使用了两个关键的数据结构:rdlist 和等待队列。
eventpoll的rdlist(ready list,就绪列表)和等待队列是两个不同的东西。

创建 epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 socket。以添加 socket 为例,如下图,如果通过 epoll_ctl 添加 sock1、sock2 和 sock3 的监视,内核会将 eventpoll 添加到这三个 socket 的等待队列中。
在这里插入图片描述
rdllist 双向链表,用于存储准备就绪的事件关联的socket集合。
当 socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。中断程序会给 eventpoll 的“就绪列表”添加 socket 引用。
在这里插入图片描述
当某个进程调用 epoll_wait,判断是否有就绪事件时,如果没有就绪事件(即rdlist为空),则会将进程放到等待队列。
在这里插入图片描述
当rdlist不为空时会唤醒该进程。此时进程A变成运行状态,继续执行代码
在这里插入图片描述

Netty使用和常用组件

Tomcat采用的是NIO,因为Tomcat出来的时候netty还没出来。
netty只是一个网络框架,类似okhttp/volley。而不是中间件。

基于Netty的知名项目:比如RocketMq gRPC dubbo zookeeper等。

NIO是单线程reactor实现,也可以是多线程实现

tomcat使用netty了吗?
Tomcat采用的是NIO,因为Tomcat出来的时候netty还没出来。
另外tomcat和netty一样。tomcat也是采用的I/O多路复用的epoll机制。tomcat的底层网络NIO通信也是基于主从Reactor多线程模型。即tomcat也和netty一样,也是基于nio,并对nio做了定制化处理。

Netty集成了各种协议如http http2 mqtt websocket。

Netty 的优势
1、API 使用简单,开发门槛低;
2、功能强大,预置了多种编解码功能,支持多种主流协议;
3、定制能力强,可以通过 ChannelHandler 对通信框架进行灵活地扩展;
4、性能高,通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优;
5、成熟、稳定,Netty 修复了已经发现的所有 JDK NIO BUG,业务开发人员不需要再为NIO 的 BUG 而烦恼;
6、社区活跃,版本迭代周期短,发现的 BUG 可以被及时修复,同时,更多的新功能会加入;
7、经历了大规模的商业应用考验,质量得到验证。

怎么使用?
使用Netty就是要编写自定义各种Handler。包括出站和入站。

netty就是应用层协议全家桶,即封装了各种协议。

Netty常用的类
Bootstrap、EventLoop(Group) 、Channel:
1.Bootstrap 是 Netty 框架的启动类和主入口类,分为客户端类 Bootstrap 和服务器类ServerBootstrap 两种。
Channel 是 Java NIO 的一个基本构造。
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的 I/O 操作的程序组件)的开放连接,如读操作和写操作。
EventLoop 暂时可以看成一个线程、EventLoopGroup 自然就可以看成线程组,用于循环处理事件。
ServerBootstrap可以接收两个EventLoopGroup,一个负责连接,一个负责从网络读写数据。
Channel接口封装了Socket/ServerSocket的操作

2.EventLoop:在一个 while 循环中 select 出事件,然后依次处理每种事件。我们可以把它称为事件循环,这就是 EventLoop。用于处理网络连接的生命周期中所发生的事件。
EventLoopGroup:EventLoopGroup 负责为每个新创建的 Channel 分配一个 EventLoop。

事件和 ChannelHandler、ChannelPipeline:
事件分入站事件和出站事件,类似输入流/输出流事件。
每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法去处理。
ChannelHandler比如加解密handler、压缩handler。类似责任链模式。
这些 ChannelHandler 都放在 ChannelPipeline 中统一管理,事件就会在 ChannelPipeline 中流动,并被其中一个或者多个 ChannelHandler 处理。

ChannelFuture(一般用在出站操作):
1.ChannelFuture,用于在执行异步操作的时候使用。
2.一般来说,每个Netty 的出站 I/O 操作都将返回一个 ChannelFuture。
在Netty中,每个出站(Outbound)的IO操作都会返回一个ChannelFuture对象。这个ChannelFuture代表了尚未完成的IO操作的结果。具体来说:
当你发起一个出站IO操作(比如写操作)时,该操作可能不会立即完成,而是会被放入一个队列中等待执行。
返回的ChannelFuture对象允许你注册一个监听器(Listener),以便在操作完成时收到通知,或者在操作失败时执行相应的处理。
你可以通过ChannelFuture来检查操作是否已经完成、获取操作的结果、添加监听器以执行后续的逻辑等。
总之,ChannelFuture提供了一种异步处理IO操作结果的方式,使得你可以在IO操作完成后采取相应的措施,而不必阻塞等待操作完成。

ChannelPipeline 和 ChannelHandlerContext:
1.ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联。
Handler 在放入 ChannelPipeline 的时候必须要有两个指针pre 和 next 来说明它的前一个元素和后一个元素,但是 Handler 本身来维护这两个指针不合适。想想我们在使用 JDK 的 LinkedList 的时候,我们放入 LinkedList 的数据是不会带这两个指针的,LinkedList 内部会用类 Node 对我们的数据进行包装,将包装后的Node添加到链表中,而类 Node 则带有两个指针 pre和 next。
在这里插入图片描述
所以,ChannelHandlerContext 的主要作用就和 LinkedList 内部的类 Node 类似。
在 ChannelHandler 被添加到 ChannelPipeline 中或者被从 ChannelPipeline 中移除时会调用一些方法。这些方法中的每一个都接受一个 ChannelHandlerContext 参数。
2.ChannelHandler是ChannelHandlerContext的一部分。在创建ChannelHandlerContext的时候会把ChannelHandler传进去。

EventLoop、EventLoopGroup、Channel和Thread关系:
1.一个EventLoopGroup包含一个或者多个EventLoop;
2.EventLoopGroup将为每个新创建的Channel分配一个EventLoop
3.一个EventLoop在它的生命周期内只和一个Thread绑定,所以所有由EventLoop处理的I/0事件都将在它专有的Thread上被处理。所以在每个Channel生命周期内,所有的操作都是由相同的Thread执行。
4.因为一个 EventLoop 通常会被用于支撑多个 Channel,所以多个Channel会用同一个Thread。所以对于所有相关联的 Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上下文中,它仍然可以被用于在多个 Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。
5.对于那么多的线程,如何进行线程管理,这就用到了EventLoopGroup。

线程管理
EventLoopGroup(接口) extends EventExecutorGroup(接口)
EventExecutorGroup extends ScheduledExecutorService
ScheduledExecutorService extends ExecutorService
ExecutorService extends Executor
Executor {
void execute(Runnable command);
}
所以EventLoopGroup是一个管理线程的服务,即也是一个线程池。
在内部,当提交任务的线程是EventLoop绑定的线程,那么所提交的代码块将会被执行。否则,EventLoop 将该任务并将它放入到内部队列中。当 EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件。
NioEventLoopGroup绑定了个线程池。NioEventLoop绑定了个单线程池,而非线程,即都绑定了线程池。

ByteBuf:
1.发网络数据的基本单位是字节。java nio提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂,而且有些繁琐,因为读和写是共用了bytebuffer,读写切换时需要切换模式。Netty提供了更加方便的api,使用ByteBuf替代了ByteBuffer。
2.ByteBuf API 的优点:
它可以被用户自定义的缓冲区类型扩展;
通过内置的复合缓冲区类型实现了透明的零拷贝(在缓存操作上,Netty 提供了CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝);
容量可以按需增长(类似于 JDK 的 StringBuilder);
在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
读和写使用了不同的索引;
支持方法的链式调用;
支持引用计数;
支持池化。

ByteBuf读写切换时不需要切换模式的原理是?
因为ByteBuf使用了独立的读写索引。而ByteBuffer只使用了一个position字段来维护,读写切换时需要调用flip()来切换模式。
当你调用 ByteBuf 的读取方法(如 readInt()、readBytes() 等),ByteBuf 会自动将 readerIndex 向前移动,并返回相应的数据。读取操作不会改变 writerIndex。
当你调用 ByteBuf 的写入方法(如 writeInt()、writeBytes() 等),ByteBuf 会自动将 writerIndex 向前移动,并将数据写入到相应位置。写入操作不会改变 readerIndex。

ByteBuffer读写是如何切换的?
通过flip()切换。
从写模式切换到读模式:调用 flip()方法会将 position 设回 0,并将 limit设置成之前 position 的值。此时的position的意思是从头开始读取数据,最多读取到limit。
从读模式切换到写模式:同样的,调用 flip()方法会将position 设回 0,limit 设置为当前 position。此时的position的意思是从头开始写数据,最多写到limit。从这可以看出,新写入的数据不会覆盖之前没有读到的数据。即position>=limit的数据。

解决粘包/半包
归根结底是由于服务器不知道客户发送过来的数据是以什么作为一串数据的结束符才产生了粘包/半包(拆包),所以我们需要设置结束符让服务器知道怎么组合数据包,解决粘包半包的方案主要有3种,下面有说。拆包也叫半包。
之所以会出现粘包/半包是因为客户端可能会采用Nagle算法将多个数据包进行发送,此时会出现粘包/半包(即合并包后会导致粘包)。另外由于服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,此时就算是禁用Nagle算法也造成粘包现象。
UDP不会有粘包一说:
UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有 Nagle 算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP 头+IP 头等等发一次数据封装一次)也就没有粘包一说了。

Nagle 算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送。虽然该方式有效提高网络的有效负载,但是他会造成延时。
Netty可以通过设置ChannelOption.TCP_NODELAY参数:禁止使用 Nagle 算法,使用于小数据即时传输。
ChannelOption.TCP_CORK参数:该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

编解码器框架
什么是编解码器?
编码器是将消息转换为适合于传输的格式,比如字节流;
而解码器则是将网络字节流转换回应用程序的消息格式

Netty 是如何解决 JDK 中的 Selector BUG 的?
Selector BUG:JDK NIO 的 BUG,例如臭名昭著的 epoll bug,它会导致 Selector 空轮询,最终导致 CPU 100%。
Netty 解决办法:对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进行一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 epoll 死循环 bug。重建Selector,判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的 Selector上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。

如何让单机下 Netty 支持百万长连接?
a.操作系统
1)修改单个进程打开最大文件数限制。$ ulimit –n 1000000 支持百万socket句柄。
2)修改系统打开最大文件数限制
b.Netty 调优
1)设置合理线程数
对于线程池的调优,主要集中在用于接收海量设备 TCP 连接、TLS 握手的Acceptor线程池( Netty 通常叫 boss NioEventLoop Group)上,以及用于处理网络数据读写、心跳发送的 IO 工作线程池(Nety 通常叫 work Nio EventLoop Group)上。
对于 IO 工作线程池的优化,可以先采用系统默认值(即 CPU 内核数×2)进行性能测试。
设置线程池线程数可以如下:
1.在创建NioEventLoopGroup时指定线程数: 在创建NioEventLoopGroup时,可以指定线程数,以决定EventLoop的个数。例如:
EventLoopGroup group = new NioEventLoopGroup(4); // 指定4个线程
2.通过系统属性设置: 可以通过设置系统属性来调整EventLoop线程池的大小。例如,在应用启动时可以通过Java系统属性进行设置
System.setProperty(“io.netty.eventLoopThreads”, “4”); // 设置EventLoop线程池大小为4
2)心跳优化
要能够及时检测失效的连接,并将其剔除,防止无效的连接句柄积压,导致 OOM 等问题。
设置合理的心跳周期,防止心跳定时任务积压,造成频繁的老年代 GC(新生代和老年代都有导致 STW 的 GC,不过耗时差异较大),导致应用暂停。
使用 Nety 提供的链路空闲检测机制,不要自己创建定时任务线程池,加重系统的负担,以及增加潜在的并发安全问题。
3)接收和发送缓冲区调优:通过调小 TCP 的接收和发送缓冲区来降低单个 TCP 连接的资源占用率。
由于每个socket连接都有发送/接收缓冲区,发送/接收缓冲区分别占用4k,则需要8k内存,百万连接的话就要8k*1000000=8g,所以光维持百万连接都要8g内存。
4)合理使用内存池:不需要我们设置,netty默认采用内存池。
不要每次新建byteBuf。
为了尽量重用缓冲区,Nety 提供了基于内存池的缓冲区重用机制。
Netty 默认的 IO 读写操作采用的都是内存池的堆外直接内存模式。
5)IO线程和业务线程分离
6)针对并发连接数的流控
如果达到流控阈值,则拒绝该连接,调用 ChannelHandlerContext 的 close(方法关闭连接。
c.JVM 层面相关性能优化
百万连接导致服务器宕机原理:当客户端的并发连接数达到数十万或者数百万时,系统一个较小的抖动就会导致很严重的后果,例如服务端的 GC,导致应用暂停(STW)的 GC 持续几秒,就会导致海量的客户端设备掉线或者消息积压,一旦系统恢复,会有海量的设备接入或者海量的数据发送很可能瞬间就把服务端冲垮(OOM)。
JVM 层面的调优主要涉及 GC 参数优化,GC 参数设置不当会导致频繁 GC,甚至 OOM 异常,对服务端的稳定运行产生重大影响。
1)、GC 数据的采集和研读
2)、设置合适的 JVM 堆大小
3)、选择合适的垃圾回收器和回收策略

什么是水平触发(LT)和边缘触发(ET)?
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完,那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完,那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!

select(),poll()模型都是水平触发模式,信号驱动 IO 是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。JDK NIO 中的 select 实现是水平触发,而 Netty提供的 Epoll 的实现中是边缘触发。

请说说 DNS 域名解析的全过程
“浏览器中输入 URL 到返回页面的全过程”:
1.根据域名,进行 DNS 域名解析;
2.拿到解析的 IP 地址,建立 TCP 连接;
3.向 IP 地址,发送 HTTP 请求;
4.服务器处理请求;
5.返回响应结果;
6.关闭 TCP 连接;
7.浏览器解析 HTML;
8.浏览器布局渲染;

Netty高并发高性能架构设计精髓总结
1.主从Reactor线程模型(典型高并发架构设计),即bossGroup(主) workerGroup(从)
Reactor:使用epoll基于事件响应编程,不像poll需要轮询
2.NIO多路复用非阻塞,也叫无锁串行化设计思想(一个线程处理所有请求,不存在并发,reids线程模型就使用到了无锁串行化思想,一个线程处理所有连接),netty无锁串行化体现在所有handler都在一个线程进行处理,不存在并发。个人认为无锁串行化设计思想其实就是串行,不能在高并发带来太大提升,高并发的提升主要是NIO多路复用和主从Reactor线程模型。
3.支持各种高性能序列化协议
4.零拷贝(通过使用直接内存、复合内存等实现了零拷贝)
5.ByteBuf内存池设计
6.灵活的TCP参数配置能力
合理设置TCP参数在某些场景下对于性能的提升可以起到显著的效果,例如接收缓冲区SO_RCVBUF和发送缓冲区SO_SNDBUF。如果设置不当,对性能的影响是非常大的。通常建议值为128K或者256K。
Netty在启动辅助类ChannelOption中可以灵活的配置TCP参数,满足不同的用户场景。
7.并发优化
volatile的大量、正确使用;
CAS和原子类的广泛使用;
线程安全容器的使用;
通过读写锁提升并发性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值