Netty对三种I/O模式的支持
BIO,NIO,还有AIO(已被移除),BIO也称之为OIO(已被弃用)。
| NIO-COMMON | NIO-Linux | NIO-macOS/BSD | ||
|---|---|---|---|---|
| NioEventLoopGroup | EpollEventLoopGroup | KQueueEventLoopGroup | ||
| NioEventLoop | EpollEventLoop | KQueueEventLoop | ||
| NioServerSocketChannel | EpollServerSocketChannel | KQueueServerSocketChannel | ||
| NioSocketChannel | EpollSocketChannel | KQueueSocketChannel |
以上的EventLoopGroup是需要手动指定的,对应的Channel则是根据工厂模式进行切换:
AbstractBootstrap
public B channel(Class<? extends C> channelClass) {
// 反射工厂的实现
return channelFactory(new ReflectiveChannelFactory<C>(
ObjectUtil.checkNotNull(channelClass, "channelClass")
));
}
Netty对Reactor模式的支持
上一篇文章对Reactor模式进行了介绍,这里看下Netty是如何实现该模式的。这里贴一张上篇文章的图回忆回忆
以NioEventLoop为例,NioEventLoop可以简单理解为一个线程,其中的run()方法如下:
// 死循环监听、处理事件
protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// fall-through to SELECT since the busy-wait is not supported with NIO
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
...
}
是不是和NIO的处理方式很像。
mainReactor
回到EchoServer
// 无参的构造方法:线程组,多个线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
bossGroup即为mainReactor,workerGroup即为subReactor。进入group方法:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
ObjectUtil.checkNotNull(childGroup, "childGroup");
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
我们关心的mainReactor在这里对应的是parentGroup,进入super.group(parentGroup)看一眼:
public B group(EventLoopGroup group) {
ObjectUtil.checkNotNull(group, "group");
if (this.group != null) {
throw new IllegalStateException("group set already");
}
this.group = group;
return self();
}
原来是ServerBootstrap的父类AbstractBootstrap管理着这个group,即mainReactor。前面我们说mainReactor只会分一个线程处理acceptor,那么再看代码 。
ChannelFuture f = b.bind(PORT).sync();
进入bind方法,直到doBind方法:
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
...
}
进入initAndRegister方法:
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// 1 创建一个ServerSocketChannel(NioServerSocketChannel)
channel = channelFactory.newChannel();
// 2 初始化ServerSocketChannel(NioServerSocketChannel)
init(channel);
} catch (Throwable t) {
if (channel != null) {
channel.unsafe().closeForcibly();
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
}
// 3 Register:给ServerSocketChannel从Bossgroup中选择一个NioEventLoop
// 这里是返回一个Future,说明后面要走异步过程了
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
initAndRegister主要做了三件事,注释中有描述。这里先关心第三步:
ChannelFuture regFuture = config().group().register(channel);
这里的config().group()点进去看其实就是前面说的AbstractBootstrap中的group。再进入register方法,这里是MultithreadEventLoopGroup中的register方法:
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
这里只会调用一次,也就是这里一般也只会有一个线程。即给给ServerSocketChannel从boss group中选择一个NioEventLoop。
subReactor
再回到ServerBootstrap中的group方法,看到childGroup赋值给了ServerBootstrap的childGroup属性
this.childGroup = childGroup;
回到initAndRegister方法中的init(channel),这里用于初始化ServerSocketChannel。进入ServerBootstrap中的init方法:
@Override
void init(Channel channel) {
// 参数和属性设置
setChannelOptions(channel, options0().entrySet().toArray(newOptionArray(0)), logger);
setAttributes(channel, attrs0().entrySet().toArray(newAttrArray(0)));
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions =
childOptions.entrySet().toArray(newOptionArray(0));
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));
// 这里需要了解责任链模式pipeline
// ChannelInitializer一次性、初始化handler:
// 负责添加一个ServerBootstrapAcceptor handler,添加完后,自己就移除了:
// ServerBootstrapAcceptor handler: 负责接收客户端连接创建连接后,对连接的初始化工作。
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
先不管Netty中的pipeline是咋回事。看到这里用到的是ServerBootstrapAcceptor的channelRead方法:
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);
try {
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
这里调用的是childGroup.register。前面NioEventLoopGroup使用了无参构造方法,默认nThreads参数为0,那么实际里面的现成数为DEFAULT_EVENT_LOOP_THREADS。
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
所以这里每接收客户端连接创建连接后,就会创建线程对链接L进行初始化工作。
如何给Channel分配NIO eventLoop
前面提到了MultithreadEventLoopGroup中的register方法,进入next方法,最终能看到用的是EventExecutorChooser接口的next方法。他有两个实现PowerOfTwoEventExecutorChooser和GenericEventExecutorChooser:
@SuppressWarnings("unchecked")
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
// 根据待绑定的executor是否是2的幂次方,做出不同的选择
if (isPowerOfTwo(executors.length)) {
return new PowerOfTwoEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
}
private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
}
private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
// executors总数必须是2的幂次方才会用,&运算效率更高,同时当idx累加成最大值之后,相对于GenericEventExecutorChooser更公平
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
}
private static final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
public EventExecutor next() {
// 递增、取模,取正值,不然可能是负数,另外:有个非常小的缺点,当idx累加成最大值后,有短暂的不公平:
//1,2,3,4,5,6,7,0,7(注意这里不是1,而是7,然而往前的第二个也是7,所以不够公平),6,5
// 此处为取模运算,效率不如与运算。会出现短暂的不公平。
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
为啥会出现不公平呢,简单做个示例:
public static void main(String[] args) {
int length = 8;
AtomicInteger atomicInteger = new AtomicInteger(Integer.MAX_VALUE - length + 1);
for (int i = 0; i < length * 2; i++) {
int integer = atomicInteger.getAndIncrement();
int abs = Math.abs(integer % length);
System.out.println("int: " + integer + " after %: " + abs);
}
}
输出:
int: 2147483640 after %: 0
int: 2147483641 after %: 1
int: 2147483642 after %: 2
int: 2147483643 after %: 3
int: 2147483644 after %: 4
int: 2147483645 after %: 5
int: 2147483646 after %: 6
int: 2147483647 after %: 7
int: -2147483648 after %: 0
int: -2147483647 after %: 7
int: -2147483646 after %: 6
int: -2147483645 after %: 5
int: -2147483644 after %: 4
int: -2147483643 after %: 3
int: -2147483642 after %: 2
int: -2147483641 after %: 1
可见在atomicInteger到达极大值时会出现短暂不公平。
这里也可以看出Netty对性能做了极致的优化。
实现多路复用器是怎么跨平台的
在NioEventLoopGroup构造方法中有这么一段:
public NioEventLoopGroup(int nThreads, Executor executor) {
this(nThreads, executor, SelectorProvider.provider());
}
这里的SelectorProvider.provider()中调用了SelectorProvider的create方法。随着jdk不同,这里的实现也不同。
mac系统为sun.nio.ch.KQueueSelectorProvider
windows系统为WindowsSelectorProvider
Netty对处理粘包/半包的支持
什么是粘包和半包?
假设客户端分别发送了两个数据包ABC和DEF给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况:
- 服务端分两次读取到了两个独立的数据包,分别是ABC和DEF,没有粘包和拆包;
- 服务端一次接收到了两个数据包,ABC和DEF粘合在一起,被称为TCP粘包;
- 服务端分两次读取到了两个数据包,第一次读取到了完整的ABC包和DEF包的部分内容(ABCD),第二次读取到了DEF包的剩余内容(EF),这被称为TCP拆包
- 服务端分两次读取到了两个数据包,第一次读取到了ABC包的部分内容(AB),第二次读取到了CD,第三次读到了EF。
TCP粘包/半包解决实战
| 方式/比较 | 消息边界 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| TCP短连接 | 建立连接到释放连接质检的信息即为传输信息 | 简单 | 效率低 | 不推荐 |
| 固定长度 | 满足固定长度即可 | 简单 | 空间浪费 | 不推荐 |
| 分隔符 | 分隔符质检 | 空间不浪费 | 分隔符需要转义 | 推荐 |
| 消息头 | 将消息分位消息头和消息体 | 精确且不用转义 | 实现相对复杂 | 推荐+ |
具体用到的类分别为:
- 固定长度FixedLengthFrameDecoder
- 分隔符,行分隔符LineBasedFrameDecoder,自定义分隔符DelimiterBasedFrameDecoder
- 消息头和消息体LengthFieldBasedFrameDecoder
Netty中的零拷贝
什么是零拷贝?
个人理解,零拷贝并不是真的不拷贝,而是站在CPU的角度减少内核态和用户态的拷贝。
- 可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
- 减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
Linux的I/O机制与DMA
在早期计算机中,用户进程需要读取磁盘数据,需要CPU中断和CPU参与,因此效率比较低,发起IO请求,每次的IO中断,都带来CPU的上下文切换。因此出现了DMA。
DMA(Direct Memory Access,直接内存存取)是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU的大量中断负载。DMA控制器,接管了数据读写请求,减少CPU的负担。这样一来,CPU能高效工作了。现代硬盘基本都支持DMA。
实际因此IO读取,涉及两个过程:
- DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
- 用户进程,将内核缓冲区的数据copy到用户空间。
这两个过程,都是阻塞的。
传统数据传送机制
比如:读取文件,再用socket发送出去,实际经过四次copy。
- 第一次:将磁盘文件,读取到操作系统内核缓冲区;
- 第二次:将内核缓冲区的数据,copy到应用程序的buffer;
- 第三步:将应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
- 第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
分析上述的过程,虽然引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。
sendfile零拷贝
linux 2.1支持的sendfile
当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接拷贝到socket buffer。在硬件支持的情况下,甚至数据都并不需要被真正复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中,DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制(CPU copy)。
一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、代表数据转化的完成。socket buffer里的数据就能在网络传输了。
sendfile会经历:3次拷贝,1次CPU copy ,2次DMA copy;
硬件支持的情况下,则是2次拷贝,0次CPU copy, 2次DMA copy。
Netty中的零拷贝
在DefaultFileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝:
long written = file.transferTo(this.position + position, count, target);
案例性能对比
代码如下,Server:
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10086);
while (true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
int byteCount = 0;
try {
byte[] bytes = new byte[1024];
while (true) {
int readCount = dataInputStream.read(bytes, 0, bytes.length);
byteCount = byteCount + readCount;
if (readCount == -1) {
System.out.println("服务端接受:" + byteCount + "字节");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
四次拷贝client:
public class TraditionClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 10086);
String fileName = "C:\\Users\\admin\\Pictures\\test.jpg";
// 创建输⼊流对象
InputStream inputStream = Files.newInputStream(Paths.get(fileName));
// 创建输出流
DataOutputStream dataOutputStream = new
DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[1024];
long readCount = 0;
long total = 0;
long startTime = System.currentTimeMillis();
// 这里要发生2次copy
while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
// 网络发送:这里要发生2次copy
dataOutputStream.write(buffer);
}
long endTime = System.currentTimeMillis();
System.out.println("发送总字节数:" + total + ",耗时:" + (endTime - startTime) + " ms");
//释放资源
dataOutputStream.close();
socket.close();
inputStream.close();
}
}
sendfile client:
public class ZeroCopyClient {
public static void main(String[] args) throws Exception {
// socket套接字
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 10086));
socketChannel.configureBlocking(true);
// 文件
String fileName = "C:\\Users\\admin\\Pictures\\test.jpg";
// FileChannel 文件读写、映射和操作的通道
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
long startTime = System.currentTimeMillis();
// transferTo⽅法⽤到了零拷⻉,底层是sendfile,这里只需要发生2次copy和2次上下文切换
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
long endTime = System.currentTimeMillis();
System.out.println("发送总字节数:" + transferCount + " 耗时:" + (endTime - startTime) + " ms");
// 释放资源
fileChannel.close();
socketChannel.close();
}
}
可以拿个稍微大一点的图片尝试一下,可以看到两者的耗时不是一个数量级的。
Netty中的锁机制
锁优化技术:
- 减少锁的粒度
- 减少锁对象的空间占用
- 提高锁的性能
- 根据不同的业务场景选择适合的锁
- 能不用锁则不用锁
减少锁的粒度
查看老版本(netty-4.1.15.Final)Bootstrap的init方法:
@Override
@SuppressWarnings("unchecked")
void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline();
p.addLast(config.handler());
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
两次操作分别对两个对象进行锁,没有直接对方法或类上锁。
减少锁对象的空间占用
在ChannelOutboundBuffer中,没有直接使用AtomicLong,而是这样:
// 使用原子操作类确保多线程安全
private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");
// 统计待发送的字节数
private volatile long totalPendingSize;
private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
if (size == 0) {
return;
}
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
// 判断待发送的数据的size是否高于高水位线
if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
setUnwritable(invokeLater);
}
}
- Atomic long:是一个对象,包含对象头(object header)以用来保存hashcode、lock等信息,32位系统占用8字节;64位系统占16字节。以64位系统为例,占用大小 = 8 bytes (volatile long)+ 16bytes (对象头)+ 8 bytes (引用) = 32 bytes
- long: 只占用8个字节。
虽然代码实现麻烦一些,但是能够节约至少24个字节。
提高锁的性能
PlatformDependent中的newLongCounter方法:
public static LongCounter newLongCounter() {
if (javaVersion() >= 8) {
// 继承了LongAdder
return new LongAdderCounter();
} else {
return new AtomicLongCounter();
}
}
阿里开发手册也说明了:如果是jdk8,则推荐使用LongAdder,相比AtomicLong性能更好,因为减少了乐观锁的重试次数。
简单理解的话,AtomicLong在每次操作时,所有线程都在CAS都在竞争。而LongAdder可以理解为分了多个cell,每个cell会有多个线程竞争,最后获取value时会将所有cell的值相加得来。LongAdder相比AtomicLong用到了分治的思想。
根据不同的业务场景选择适合的锁
SingleThreadEventExecutor中CountDownLatch threadLock = new CountDownLatch(1),这里入参为1,直接替代了wait/notify,代码更清晰更简单那。
能不用锁则不用锁
Recycler中用到了ThreadLocal,属于轻量级的线程池实现,避免资源竞争。
FastThreadLocal中用到的InternalThreadLocalMap,继承了UnpaddedInternalThreadLocalMap,其中用到了ThreadLocal:
static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();