Netty权威指南学习笔记
Netty实战
Scalable IO in Java
大多数的现代应用程序都利用了复杂的多线程处理技术以有效地利用系统资源。
在早期的 Java 语言中,我们使用多线程处理的主要方式无非是按需创建和启动新的 Thread 来执行并发的任务单元——一种在高负载下工作得很差的原始方式。 Java 5 随后引入了 Executor API,其线程池通过缓存和重用Thread 极大地提高了性能。
java基本的线程池化模式可以描述为:
public static void test01() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100));
IntStream.range(0, 100).forEach(i -> {
pool.execute(() -> {
System.out.println("start-->" + i);
try {
Thread.sleep(11);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end-->" + i);
});
});
pool.shutdown();
}
学习Netty线程模型时,首先会想到Reactor线程模型。
Reactor单线程模型
Reactor单线程模型,是指所有I/O操作都在同一个I/O线程上完成的。I/O线程职责如下:
在绝大多数情况下,Reactor多线程模型 可以满足需求。但是,在个别特殊场景,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户连接。
主从Reactor多线程模型
主从Reactor多线程模型的特点:
- 服务端用于接收客户端连接请求的不在是一个单独的NIO线程,而是一个独立的线程池
- Acceptor Pool 仅仅用来处理客户端的登录,握手和安全认证,一旦链路创建成功,就将链路注册到Sub Reactor Thread Pool(I/O Pool)上
- Sub Reactor Thread Pool(I/O Pool) :负责SocketChannel的读写和编码工作。
Netty线程模型
Netty的线程模型不是一成不变的,它实际取决于用户的启动参数设置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型,Reactor多线程模型,主从Reactor多线程模型
例
private void bind(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
}
});
//绑定端口,同步等待成功
ChannelFuture f = b.bind(8080).sync();
//监听端口关闭
f.channel().closeFuture().sync();
} finally {
//优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
用于接收客户端请求的线程池(bossGroup)作用:
- 接收客户端TCP连接,初始化Channel参数
- 将链路状态变更事件通知给ChannelPipeline
处理I/O线程池(workerGroup)作用:
最佳实践
推荐的线程数量计算公式:
- 公式一:线程数量 = (线程总时间/瓶颈资源时间) * 瓶颈资源的线程并行数
- 公式二:QPS=1000/线程总时间*线程数
Netty 4 中的 I/O 和事件处理
I/O 操作触发的事件将流经安装了一个或者多个ChannelHandler 的 ChannelPipeline。传播这些事件的方法调用可以随后被 ChannelHandler 所拦截, 并且可以按需地处理事件。事件的性质通常决定了它将被如何处理;它可能将数据从网络栈中传递到你的应用程序中,或者进行逆向操作,或者 执行一些截然不同的操作。
在Netty 4 中(Netty 5亦然), 所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。
EventLoop的类图
由类图可以看见,EventLoop 实现了:ExecutorService,ScheduledExecutorService等juc包下接口。
//使用 EventLoop 执行任务
ch.eventLoop().execute(()->{
//execute sth
});
//使用 EventLoop 调度任务
ch.eventLoop().schedule(() -> {
//schedule sth
}, 60, TimeUnit.SECONDS);
//使用 EventLoop 周期性调度任务
ScheduledFuture future = ch.eventLoop().scheduleAtFixedRate(() -> {
//schedule sth -- cyclical
}, 60, 60, TimeUnit.SECONDS);
//取消任务,防止它再次运行
future.cancel(false);
EventLoop执行逻辑
每个 EventLoop 都有它自已的任务队列,独立于任何其他的 EventLoop。
如果(当前)调用线程正是支撑 EventLoop 的线程, 那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当 EventLoop下次处理它的事件时, 它会执行队列中的那些任务/事件。
再次重申:“永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任何其他任务。”
EventLoop的线程分配
服务于 Channel 的 I/O 和事件的 EventLoop 包含在 EventLoopGroup 中。根据不同的传输实现, EventLoop 的创建和分配方式也不同。
异步传输
异步实现使用只有少数 EventLoop(和Threads)共享于 Channel 之间 。这使得可以通过尽可能少量的 Thread 来支撑大量的 Channel,而不是每个 Channel 分配一个 Thread。
阻塞传输
用于像 OIO(旧的阻塞 I/O) 这样的其他传输的设计略有不同,这里每一个 Channel 都将被分配给一个 EventLoop(以及它的 Thread)。
NioEventLoop类图
NioEventLoop源码简析
作为NIO的Reactor线程,NIOEventLoop需要处理网络的I/O读写事件,因此它需要有一个多路复用器对象(Selector)
//多路复用器
Selector selector;
private SelectedSelectionKeySet selectedKeys;
private final SelectorProvider provider;
//开关是否启用优化项
private static final boolean DISABLE_KEYSET_OPTIMIZATION =
SystemPropertyUtil.getBoolean("io.netty.noKeySetOptimization", false);
private Selector openSelector() {
final Selector selector;
try {
//通过provider.openSelector()创建并打开多路复用器
selector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
if (DISABLE_KEYSET_OPTIMIZATION) {
//若没有开启优化开关,就立刻返回
return selector;
}
try {
SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Class> selectorImplClass =
Class.forName("sun.nio.ch.SelectorImpl", false, ClassLoader.getSystemClassLoader());
// 判断selector是否为selectorImplClass的超类or吵接口
if (!selectorImplClass.isAssignableFrom(selector.getClass())) {
return selector;
}
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true);
//通过反射将"selectedKeys","publicSelectedKeys"设置为selectedKeySet,将原JDK的selectedKeys替换掉
selectedKeysField.set(selector, selectedKeySet);
publicSelectedKeysField.set(selector, selectedKeySet);
selectedKeys = selectedKeySet;
} catch (Throwable t) {
//...略
}
return selector;
}
下面着重看 run方法
private final Queue taskQueue;
private final AtomicBoolean wakenUp = new AtomicBoolean();
private boolean oldWakenUp;
//是否存在任务
protected boolean hasTasks() {
assert inEventLoop();
return !taskQueue.isEmpty();
}
@Override
protected void run() {
for (;;) {
//首先要将wakeUp还原为false,并将原先值赋给oldWakeUp
oldWakenUp = wakenUp.getAndSet(false);
try {
//当前消息队列中是否存在未完成的任务
if (hasTasks()) {
//调用selector.selectNow();立即执行一次select操作,看是否存在准备就绪的Channel需要处理
selectNow();
} else {
//selector轮询,看是否有准备就绪的Channel
select();
if (wakenUp.get()) {
selector.wakeup();
}
}
}
}
//.....
//执行所有task
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
private void select() throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
//下一个将要触发定时任务的执行时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
//到任务超时时刻的剩余时间, +0.5毫秒调整值
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
//如果超时,则立即执行,退出当前循环
selector.selectNow();
selectCnt = 1;
}
break;
}
//将到任务超时时刻的剩余时间作为参数,执行select();
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
//存在以下任一条件,退出循环
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {
break;
}
//...............略.....................
}
}
}