IO和网络
本片文章参考借鉴文章:https://cloud.tencent.com/developer/article/1754078,涉及到版权联系删除
1、IO
1.1 IO相关概念
IO中主要两个维度进行区分:同步和异步,以及阻塞和非阻塞。
区分同步或异步(synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。
区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理。
同步和异步IO就看程序在执行IO后面的程序是不是需要等IO获取到数据才可以继续执行;阻塞和非阻塞就看在进行IO的时候,线程是不是被挂起,阻塞。
1.2 同步阻塞IO
IO的交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
好处是比较简单、直观;
缺点则是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈,在这种网络模型下,需要为每个链接单独使用一个线程处理,在高并发的场景下需要维护大量线程,内存、线程的切换开销会变得巨大。。
同步阻塞IO的相关代码示例:
public static void main(String[] args) throws IOException {
ExecutorService threadPool = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// socket监听到连接,交给线程池等待读取数据
Socket socket = serverSocket.accept();
// 每个线程等待一个连接
threadPool.execute(() -> {
handler(socket);
});
}
}
/**
* 处理客户端请求
*/
private static void handler(Socket socket) throws IOException {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
socket.close();
while (true) {
// 阻塞获取数据,流式。在数据发送完成之前【连接刚连接还没发送数据也会阻塞等待数据发送】都会阻塞。
// 这就导致这个线程会一直等待数据发送完成
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println("msg from client: " + new String(bytes, 0, read));
} else {
break;
}
}
}
1.3 同步非阻塞IO
用户进程发起一个IO操作后可返回做其他事情,但是用户进程需要不断进行系统调用,轮询检查数据报是否准备好,在内核缓冲区有数据的情况下用户进程同步的等待数据复制到进程缓冲区,系统调用返回成功。
1.4 IO多路复用
一个进程可以监视多个文件描述符,一旦有描述符就绪后,操作系统通知应用程序进行处理,依赖于操作系统的select/poll/epoll系统调用。业务线程将数据通过channel发送到用户缓冲区就可以执行其他任务,等数据回来selector会执行相应的程序【可以使用线程池执行这部分程序】
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 设置 serverSocketChannel 为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册 serverSocketChannel 到 selector,关注 OP_ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 执行selector.select和相应事件是同一个线程,netty的相应事件是由eventLoop【线程池】执行,
// 不会阻塞selector继续监听
while (true) {
// 没有事件发生,阻塞1000ms,没有相关事件,继续循环
if (selector.select(1000) == 0) {
continue;
}
// 有事件发生,找到发生事件的 Channel 对应的 SelectionKey 的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 发生 OP_ACCEPT 事件,处理连接请求
if (selectionKey.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
// 将 socketChannel 也注册到 selector,关注 OP_READ
// 事件,并给 socketChannel 关联 Buffer
// socket连接之后,会把这个channel也注册到这个selector上,监听数据
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 发生 OP_READ 事件,读客户端数据
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
// 这里是阻塞的,也就是为什么网上都说,nio在内核态数据准备好,
// 从内核态到用户态拷贝数据的时候是阻塞的
channel.read(buffer);
System.out.println("msg form client: " + new String(buffer.array()));
}
// 手动从集合中移除当前的 selectionKey,防止重复处理事件
iterator.remove();
}
}
}
1.5 异步IO
应用程序向操作系统发起异步 I/O 请求,并提供一个回调函数或事件。发起请求后,程序不会等待,而是继续执行。当操作系统完成 I/O 操作后,会调用事先提供的回调函数,或者通过某种机制(如事件、信号等)通知程序。
2、网络通信数据交互
- 客户端请求服务器,服务端操作系统通过网卡将数据读取到内核缓冲区,再将数据从内核缓冲区读取到进程缓冲区
- 服务端业务处理,处理客户端的请求并构造返回结果,并从进程缓冲区写入系统缓冲区
- 操作系统将内核缓冲区中的数据写入网卡,通过底层协议发送目标客户端
3、 nettyIO
3.1 nettyIO是什么
-
Netty 是 JBoss 开源项目,是异步的、基于事件驱动的网络应用框架,它以高性能、高并发著称。所谓基于事件驱动,说得简单点就是 Netty 会根据客户端事件(连接、读、写等)做出响应
-
Netty 主要用于开发基于 TCP 协议的网络 IO 程序(TCP/IP 是网络通信的基石,当然也是 Netty 的基石,Netty 并没有去改变这些底层的网络基础设施,而是在这之上提供更高层的网络基础设施)
-
Netty 是基于 Java NIO 构建出来的,Java NIO 又是基于 Linux 提供的高性能 IO 接口/系统调用构建出来的
特点: -
异步事件驱动框架,用于快速开发高性能服务端和客户端
-
封装了JDK底层BIO和NIO模型,提供高度可用的API,提供了很多的扩展点,通过某个参数可以选择使用底层的NIO或者BIO
-
自带编解码器解决拆包粘包问题,用户只用关心业务逻辑
-
Netty 提供了多款开箱即用的编解码器:
(1)FixedLengthFrameDecoder 固定长度解码器
(2)DelimiterBasedFrameDecoder 指定分隔符解码器
(3)LengthFieldBasedFrameDecoder 基于数据包长度解码器
(4)等等……这里不再列举 -
精心设计的reactor线程模型支持高并发海量连接
-
自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
3.2 nettyIO调用流程图
nettyIO客户端:
nettyIO服务端:
3.3 netty重要的类
3.4 通过一个代码讲解整体流程图
public final class Server {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue")
.handler(new ServerHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new AuthHandler());
//..
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class ServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("channelActive");
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
System.out.println("channelRegistered");
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
System.out.println("handlerAdded");
}
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
new Thread(new Runnable() {
@Override
public void run() {
// 耗时的操作
String result = loadFromDB();
ctx.channel().writeAndFlush(result);
ctx.executor().schedule(new Runnable() {
@Override
public void run() {
// ...
}
}, 1, TimeUnit.SECONDS);
}
}).start();
}
private String loadFromDB() {
return "hello world!";
}
}
原理:
ServerBootStrap继承于AbstractBootStrap
netty服务端启动(创建channel,并且channel分别进行注册到selector以及channel绑定端口):
- 创建服务端channel
- AbstractBootStrap.bind() -> AbstractBootStrap.doBind() -> AbstractBootStrap.initAndRegister()
- AbstractBootStrap.bind() -> AbstractBootStrap.doBind() -> AbstractBootStrap.initAndRegister()
- 初始化服务端channel
- AbstractBootStrap.bind() -> AbstractBootStrap.doBind() -> AbstractBootStrap.initAndRegister() -> ServerBootStrap.init()
- AbstractBootStrap.bind() -> AbstractBootStrap.doBind() -> AbstractBootStrap.initAndRegister() -> ServerBootStrap.init()
- 注册到selector
- 端口绑定
3.5 NioEventLoop
3.5.1 NioEventLoop的创建
- 创建线程执行器:ThreadPerTaskExecutor
- 线程创建器作用:每次执行任务都会创建一个线程实体
- NioEventLoop线程命名规则nioEventLoop-1-xx (1:增加一个EvenLoopGroup,这个数字增加1,xx:第几个线程)
- 构造NioEventLoop
- 保存线程执行器ThreadPerTaskExecutor
- 创建一个MpscQueue:用来存放外部的任务,交给NioEventLoop线程来执行
- 创建一个selector
- 创建线程选择器chooser = chooserFactory.newChooser(children);
- chooser作用:当来了一个channel,决定将这个channel分配到那个EventLoop上面
- children:所有的EventLoop
3.5.2 NioEventLoop启动
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
private void doStartThread() {
assert thread == null;
// EventLoop开启一个线程执行这个任务
executor.execute(new Runnable() {
@Override
public void run() {
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
boolean success = false;
updateLastExecutionTime();
try {
SingleThreadEventExecutor.this.run(); // 这里调用NioEventLoop的run()
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
} finally {
for (;;) {
int oldState = STATE_UPDATER.get(SingleThreadEventExecutor.this);
if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
break;
}
}
// Check if confirmShutdown() was called at the end of the loop.
if (success && gracefulShutdownStartTime == 0) {
logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +
SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " +
"before run() implementation terminates.");
}
try {
// Run all remaining tasks and shutdown hooks.
for (;;) {
if (confirmShutdown()) {
break;
}
}
} finally {
try {
cleanup();
} finally {
STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
threadLock.release();
if (!taskQueue.isEmpty()) {
logger.warn(
"An event executor terminated with " +
"non-empty task queue (" + taskQueue.size() + ')');
}
terminationFuture.setSuccess(null);
}
}
}
}
});
}
}
public final class NioEventLoop extends SingleThreadEventLoop {
// 开启的线程会在这里进行一直轮询
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
// 'wakenUp.compareAndSet(false, true)' is always evaluated
// before calling 'selector.wakeup()' to reduce the wake-up
// overhead. (Selector.wakeup() is an expensive operation.)
//
// However, there is a race condition in this approach.
// The race condition is triggered when 'wakenUp' is set to
// true too early.
//
// 'wakenUp' is set to true too early if:
// 1) Selector is waken up between 'wakenUp.set(false)' and
// 'selector.select(...)'. (BAD)
// 2) Selector is waken up between 'selector.select(...)' and
// 'if (wakenUp.get()) { ... }'. (OK)
//
// In the first case, 'wakenUp' is set to true and the
// following 'selector.select(...)' will wake up immediately.
// Until 'wakenUp' is set to false again in the next round,
// 'wakenUp.compareAndSet(false, true)' will fail, and therefore
// any attempt to wake up the Selector will fail, too, causing
// the following 'selector.select(...)' call to block
// unnecessarily.
//
// To fix this problem, we wake up the selector again if wakenUp
// is true immediately after selector.select(...).
// It is inefficient in that it wakes up the selector for both
// the first case (BAD - wake-up required) and the second case
// (OK - no wake-up required).
if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
}
NioEventLoop这个线程每次for循环的时候执行的逻辑:
select()方法执行逻辑
- deadline以及任务穿插逻辑处理
- 定时任务队列的第一个任务截止时间到、任务队列taskQueue、tailQueue中有任务select结束
- 阻塞时select
- 阻塞定时任务队列的第一个任务截止时间到(如果定时任务队列空,默认阻塞1s)
- 阻塞之后,有任务(定时任务、taskQueue、tailQueue)或者有注册感兴趣的IO发生,select结束
- 避免jdk空轮询的bug
- 如果阻塞时间小于要阻塞的时间【阻塞定时任务队列的第一个任务截止时间到(如果定时任务队列空,默认阻塞1s)】并且感兴趣的IO个数为0,说明出现了jdk空轮训
- 如果空轮训个数超过512,则创建一个新的selector,感兴趣的selectionKey组测到新的selector上
processSelectedKey()执行逻辑
- selected KeySet优化
- 优化Selector选择器的selectedKeys属性,这个属性是Set,优化成了数组,这样add变成了O(1),selectedKeys只加不减
- processSelectedKeysOptimized()
- 根据不同的事件SelectionKey.OP_READ、SelectionKey.OP_WRITE 、SelectionKey.OP_CONNECT 、SelectionKey.OP_ACCEPT 进行相应的处理
runAllTasks()执行逻辑
- task的分类和添加:
- scheduledTaskQueue
- taskQueue
- tailTasks
- 任务聚合
- 将定时任务聚合到taskQueue中
- 任务执行
3.6 新连接接入
3.6.1 新连接接入概述
3.6.2 新连接检测
基础信息:
在Java NIO中,ServerSocketChannel和SocketChannel扮演不同的角色:
ServerSocketChannel:这是服务端专用的通道,它主要用于监听新的客户端连接。它类似于传统的服务器套接字(ServerSocket),但它可以配置为非阻塞模式,在这种模式下,可以接受多个连接,而不会阻塞等待任何一个特定的连接。
SocketChannel:这是一个可以进行网络读写的通道。无论是服务端还是客户端,一旦建立了连接,双方都会使用SocketChannel来进行数据的发送和接收。
当一个客户端发起连接请求时,它会创建一个SocketChannel并尝试连接到服务端的ServerSocketChannel监听的端口。一旦服务端的ServerSocketChannel接收到连接请求,它就会为这个特定的客户端连接创建一个新的SocketChannel实例。这样,服务端就有了一个与该客户端专门对应的SocketChannel,可以用来与这个客户端进行全双工通信。
这两个SocketChannel不是同一个。
当客户端发起连接请求时,它创建的SocketChannel是在客户端这一侧的,它用于发起连接并与服务端通信。
服务端的ServerSocketChannel在监听端口上等待客户端的连接请求。一旦服务端ServerSocketChannel接收到客户端的连接请求,它会创建一个新的SocketChannel实例,这个新的SocketChannel是在服务端这一侧的,用于与刚刚那个特定的客户端进行通信。
这样,每个客户端都有自己的SocketChannel,服务端也为每个连接的客户端创建了对应的SocketChannel。这两个SocketChannel分别存在于客户端和服务端,它们通过网络连接彼此通信,但它们是两个独立的实例。
从更高的层次来看,这两个SocketChannel实例代表了网络中的同一个连接的两端,它们通过操作系统的网络堆栈和网络硬件实现了两个应用程序之间的全双工通信。
Channel有两个:服务端channel和客户端channel,netty对应NioServerSocketChannel和NioSocketChannel。这两个channel都有属性Unsafe。服务端的Unsafe的read()读取的是SocketChannel,客户端读取的是IO数据。
3.6.3 新连接NioEventLoop的分配和selector注册
服务端的channel的pipeline构成:
在服务端启动的时候,将ServerBootstrapAcceptor注册到BossGroup的pipeline上。ServerBootstrapAcceptor做了什么事?BossGroup中的EventLoop监听到新连接进来,会创建NioSocketChannel客户端连接。就会调用pipeline,然后pipeline中的ServerBootstrapAcceptor会对新创建的NioSocketChannel客户端连接做如下事情:
- 添加childHandler 【child.pipeline().addLast(childHandler);】
- 设置childOptions、childAttr 【给子channel(服务端NioEventLoop创建的NioSocketChannel)设置属性】
- 选择NioEventLoop并注册selector【读事件】
3.7 pipeline
3.7.1 pipeline的初始化
pipeline是在创建channel的时候创建的,无论是服务端channel还是客户端channel,一个channel对应一个pipeline。
3.7.2 channelHandler添加
- channelHandler是否重复添加
- 创建节点并添加至链表
- 回调添加完成事件
举例:ChannelInitializer 这个就是在handlerAdded()方法中添加别的ChannelHandler,然后再删除自己
3.7.3 channelHandler删除
- 找到节点
- 链表的删除
- 回调删除Handler事件
3.7.4 InBound和outBound事件的传播
inBound是从Head往后tail传播,即读是从前往后,只有inbound的handler才进行处理;
outBound是从Tail往前head传播,即写是从后往前,只有outBound的handler才进行处理。
针对于异常,从当前节点开始往后传播,一直传播到tail,不区分这个handler是inBound还是outBound。基于上述理解,所以最好在所有的channel之后(tail之前)增加一个ExceptionCaughtHandler用来处理异常,而不是交给Tail进行打印到控制台进行输出。
3.8 ByteBuf
3.8.1 ByteBuf的结构
public static void main(String[] args) throws Exception {
ByteBuf buffer = Unpooled.buffer(10); // 分配一个初始容量为10的ByteBuf
System.out.println("Initial writeIndex: " + buffer.writerIndex()); // 输出初始的writeIndex,应该为0
System.out.println("Initial readerIndex: " + buffer.readerIndex()); // 输出初始的writeIndex,应该为0
buffer.writeInt(1); // 写入一个int值,占用4个字节
System.out.println("writeIndex after writing an int: " + buffer.writerIndex()); // 输出写入int后的writeIndex,应该为4
System.out.println("readerIndex after writing an int: " + buffer.readerIndex()); // 输出写入int后的writeIndex,应该为4
buffer.writeBytes(new byte[]{2, 3, 4, 5}); // 写入一个4字节的byte数组
System.out.println("writeIndex after writing 4 bytes: " + buffer.writerIndex());
System.out.println("readerIndex after writing 4 bytes: " + buffer.readerIndex());
int i = buffer.readInt();
System.out.println(i);
System.out.println("writeIndex after read 4 bytes: " + buffer.writerIndex());
System.out.println("readerIndex after read 4 bytes: " + buffer.readerIndex());
buffer.writeBytes(new byte[]{6,7,8}); // 写入一个4字节的byte数组
System.out.println("writeIndex after writing 3 bytes: " + buffer.writerIndex());
System.out.println("readerIndex after writing 3 bytes: " + buffer.readerIndex());
}
通过代码调试发现,writerIndex不会到达数组最大的时候(array.length-1)就从0开始,循环,而是会进行扩容数组。
3.8.2 ByteBuf的分类
ByteBuf主要通过三部分进行分类:
- Pooled和Unpooled:Pool是每次从预先分配好的内存中选择一块连续内存作为ByteBuf,Unpooled每次都是调用API向操作系统申请内存
- Unsafe和非Unsafe:Unsafe可以直接拿到内存的地址,通过偏移量获取,可以直接调用jdk的Unsafe进行操作;非Unsafe是使用数组索引
- Heap(堆内缓存)和direct(直接缓存):Heap是使用jvm的内存,会被GC;direct是使用操作系统的内存,不受JVM控制,不受GC控制,需要手动释放。heap底层是byte[]数组,而direct底层是java nio的ByteBuffer(java nio的ByteBuffer可以分配堆内存也可以分配操作系统内存,而这里就是使用的操作系统内存)
3.8.3 ByteBufAllocator
用来分配ByteBuf。分配上述分类的三种所有(8种)ByteBuf。
PooledByteBufAllocator
PooledByteBufAllocator有几个重要的属性:
在创建PooledByteBufAllocator的时候就创建该数组PoolArena<byte[]>[],提前开辟创建好内存,到时候直接使用
- heapArenas:堆内
- directArenas:堆外直接内存
使用这三个属性创建对应的MemoryRegionCache
- tinyCacheSize
- smallCacheSize
- normalCacheSize
每一个线程会有一个PoolThreadCache(等价于ThreadLocal的变量,PoolThreadLocalCache等价于ThreadLocal)。
final class PoolThreadCache {
// 这两个是从PooledByteBufAllocator对象中的heapArenas和directArenas数组中获取的,获取根据PoolArena.numThreadCaches属性获取线程使用最少的。netty的EventLoopGroup
// 默认是2*CPU核数,heapArenas和directArenas数组的大小默认也是2*CPU核数。所以默认情况下一个线程对应1个PoolArena(heapArena和directArena)
final PoolArena<byte[]> heapArena;
final PoolArena<ByteBuffer> directArena;
// Hold the caches for the different size classes, which are tiny, small and normal.
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
}
使用Pool获取ByteBuf的流程:
3.9 缓存
文章推荐:
netty高性能内存管理一
netty高性能内存管理二
3.9.1 内存规格
netty以Chunk(16M)的大小向操作系统申请内存。如果Chunk太大,那么netty会把Chunk切分为Page(8K).
3.9.2 缓存数据结构
PoolThreadCache
final class PoolThreadCache {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(PoolThreadCache.class);
final PoolArena<byte[]> heapArena;
final PoolArena<ByteBuffer> directArena;
// Hold the caches for the different size classes, which are tiny, small and normal.
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
}
PoolArena数据结构
abstract class PoolArena<T> implements PoolArenaMetric {
static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe();
enum SizeClass {
Tiny,
Small,
Normal
}
static final int numTinySubpagePools = 512 >>> 4;
final PooledByteBufAllocator parent;
private final int maxOrder;
final int pageSize;
final int pageShifts;
final int chunkSize;
final int subpageOverflowMask;
final int numSmallSubpagePools;
private final PoolSubpage<T>[] tinySubpagePools;
private final PoolSubpage<T>[] smallSubpagePools;
private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;
private final List<PoolChunkListMetric> chunkListMetrics;
// Metrics for allocations and deallocations
private long allocationsNormal;
// We need to use the LongCounter here as this is not guarded via synchronized block.
private final LongCounter allocationsTiny = PlatformDependent.newLongCounter();
private final LongCounter allocationsSmall = PlatformDependent.newLongCounter();
private final LongCounter allocationsHuge = PlatformDependent.newLongCounter();
private final LongCounter activeBytesHuge = PlatformDependent.newLongCounter();
private long deallocationsTiny;
private long deallocationsSmall;
private long deallocationsNormal;
// We need to use the LongCounter here as this is not guarded via synchronized block.
private final LongCounter deallocationsHuge = PlatformDependent.newLongCounter();
// Number of thread caches backed by this arena.
final AtomicInteger numThreadCaches = new AtomicInteger();
}
netty对于对象的复用用到了极致。对Chunk Page Subpage Entry ByteBuf等要么将对象加入对象池、要么进行了缓存以此复用。
3.9.2 ByteBuf扩容
-
检查对象是否已经被回收,refCnt =0?如果被回收(refCnt=0)抛异常
-
如果最小的需要字节(minWritableBytes)小于可写的字节,直接返回 无需扩容
-
如果最小的需要字节+writerIndex(所有写的字节,所需容量) 大于开始设置的最大容量,直接抛异常
-
将 最小的需要字节+writerIndex 格式化为2的次幂
- 如果所需容量 = 4M,直接返回4M
- 如果所需容量 > 4M,选择超过所需容量的4M倍数的最小数字(除非超过最大容量,返回最大容量)
- 如果所需容量 < 4M,从64字节开始乘以2增多,选择超过所需容量的最小值(除非超过最大容量,返回最大容量)
-
扩容(不同类型的底层实现不同,所以进行扩容操作不同)
- UnpooledHeapByteBuf: 底层存储bute[], 直接进行开辟新的数组,进行数据拷贝即可
- UnpooledDirectByteBuf:底层存储调用nio的ByteBuffer,调用java nio的ByteBuffer
- PoolDirectByteBuf:使用arena进行分配新的page或者subpage,将老的内存存放到缓存中
3.10 netty编解码器
3.10.1 ByteToMessageDecoder
ByteToMessageDecoder解码步骤:
- 累加字节流
- 调用子类的decode方法进行解析
- 将解析到的ByteBuf向下传播
ByteToMessageDecoder将网络中的二进制数据合并到cumulation属性中;然后调用子类的decode方法进行解析,解析出数据传递给下一个channelHandler,循环判断cumulation中数据是否解析完成,如果没有继续解析传递给下游。
3.10.2 自带的解码器
FixedLengthFrameDecoder
固定长度的解码器。按照固定长度进行解码
LineBasedFrameDecoder
基于行(\n 或者\r\n)进行分割。
DelimiterBasedFrameDecoder
首先遍历所有的分隔符,找到一个能够分隔最短的分隔符。
LengthFieldBasedFrameDecode
长度域解码器
3.11 Netty编码
3.11.1 执行逻辑
调用channel.writeAndFlush()之后:
- 从pipeline的tail节点开始往前传播
- 逐个调用channelHandler的write方法(最后只是写在了ByteBuf中)
- 逐个调用channelHandler的flush方法(真正进行写到socket中)
3.11.2 MessageToByteEncoder
write方法:
3.11.3 写Buffer
写write方法一直会通过pipeline传递到head节点,该节点Head做的事情就是把前面传过来的ByteBuf写入到buffer队列中:
- direct化ByteBuf:如果Head节点之前传过来的ByteBuf不是堆外的话,会进行转换为堆外的ByteBuf
- 插入写队列:有三个指针flushedEntry(指向第一个被刷新的entry)、unflushedEntry(指向第一个未被刷新的entry)、tailEntry(指向最后一个entry)
- ChannelOutboundBuffer:Entry(flushedEntry) --> … Entry(unflushedEntry) --> … Entry(tailEntry)
- 设置写的状态:如果这个写队列太长了(超过高水位线,默认64k),则设置pipeline为不可写状态
3.11.4 刷新Buffer队列
- 添加刷新标志并设置写状态:这个写buffer队列进行刷新为flush(unflushedEntry=null),表示flushEntry到tail都需要刷新,并且更新pipeline为可写状态
- 遍历Buffer队列,过滤ByteBuf
- 调用jdk底层API进行自旋写:将ByteBuf转换为jdk nio的ByteBuffer(复制出来),并通过jdk的socketChannel写出去
3.12 netty优化点
3.12.1 FastThreadLocal
调用FastThreadLocal.get()方法之后:
- 获取InternalThreadLocalMap
- 根据当前线程是不是FastThreadLocalThread,获取InternalThreadLocalMap的方式不同
- 根据当前线程是不是FastThreadLocalThread,获取InternalThreadLocalMap的方式不同
- 直接通过索引获取对象
- 获取不到初始化,并塞到InternalThreadLocalMap中
备注:InternalThreadLocalMap中数组的第一个元素存放的Set(用来存放存活的FastThreadLocal)
为什么netty使用自己创建的FastThreadLocal?
FastThreadLocal可以利用自己的index直接去InternalThreadLocalMap数组中获取,而jdk Thread的ThreadLocalMap获取某个ThreadLocal是通过threadlocal的hash获取到索引,如果这个位置的key(弱引用)不等于这个threadlocal,往后遍历。
3.12.2 Recycler
借助的FastThreadLocal实现。
3.13 netty高并发性能调优
3.13.1 单机百万连接调优
突破局部文件句柄限制 -单个进程所能打开最大的文件描述
一个tcp连接对应一个文件描述符,查看一个进程打开最多的文件描述符命令: ulimit -n。
如果想要增加或者修改可以打开的文件描述符:
在/etc/security/limits.conf中增加
soft nofile 1000000
hard nofile 1000000
突破全局文件句柄限制 - 所有进程所能打开最大的文件描述符
查看所有进程能够打开的文件:cat /proc/sys/fs/file-max
如果想要增加或者修改所有进程可以打开的文件描述符:
echo 20000 > /proc/sys/fs/file-max
3.13.2 应用级别性能调优
- 方法一:将复杂的业务逻辑交给业务线程池执行
- 方法二:将与执行业务逻辑的最相关、耗时的channelHandler交给另一个线程执行
public class Server {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
EventLoopGroup businessGroup = new NioEventLoopGroup(1000);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new FixedLengthFrameDecoder(Long.BYTES));
ch.pipeline().addLast(ServerBusinessHandler.INSTANCE);
// ch.pipeline().addLast(ServerBusinessThreadPoolHandler.INSTANCE);
// 方法二:将与执行业务逻辑的最相关、耗时的channelHandler交给另一个线程执行
// ch.pipeline().addLast(businessGroup, ServerBusinessHandler.INSTANCE);
}
});
bootstrap.bind(PORT).addListener((ChannelFutureListener) future -> System.out.println("bind success in port: " + PORT));
}
}
// 在workerGroup执行业务耗时逻辑,性能最差
public class ServerBusinessHandler extends SimpleChannelInboundHandler<ByteBuf> {
public static final ChannelHandler INSTANCE = new ServerBusinessHandler();
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
ByteBuf data = Unpooled.directBuffer();
data.writeBytes(msg);
Object result = getResult(data);
ctx.channel().writeAndFlush(result);
}
protected Object getResult(ByteBuf data) {
// 90.0% == 1ms
// 95.0% == 10ms 1000 50 > 10ms
// 99.0% == 100ms 1000 10 > 100ms
// 99.9% == 1000ms1000 1 > 1000ms
int level = ThreadLocalRandom.current().nextInt(1, 1000);
int time;
if (level <= 900) {
time = 1;
} else if (level <= 950) {
time = 10;
} else if (level <= 990) {
time = 100;
} else {
time = 1000;
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
}
return data;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// ignore
}
}
//方法一:将复杂的业务逻辑交给业务线程池执行
public class ServerBusinessThreadPoolHandler extends ServerBusinessHandler {
public static final ChannelHandler INSTANCE = new ServerBusinessThreadPoolHandler();
private static ExecutorService threadPool = Executors.newFixedThreadPool(1000);
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
ByteBuf data = Unpooled.directBuffer();
data.writeBytes(msg);
threadPool.submit(() -> {
Object result = getResult(data);
ctx.channel().writeAndFlush(result);
});
}
}