高性能IO 第一次分享
声明:本文用于内部分享使用,图文多来源于网络
总述
IO(Input/Output)在计算机中主要指文件读写,网络通讯。本文的概念只针对网络IO,文件读写可能与本文有所差异。
本文是高性能IO主题的第一次分享,内容包括三部分:基本IO模型,Netty概述,性能瓶颈点概述。
1、基本IO模型
1.1 阻塞IO
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态。当数据就绪之后,内核会通知用户线程,用户线程从阻塞状态恢复执行。
阻塞IO对应java中的BIO,下图和代码简述了BIO的基本使用。阻塞IO对应java中的BIO,下图和代码简述了BIO的基本使用。
public class IOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8000);
// (1) 接收新连接线程
new Thread(() -> {
while (true) {
try {
// (1) 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// (2) 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true) {
int len;
// (3) 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
}
} catch (IOException e) {
}
}).start();
} catch (IOException e) {
}
}
}).start();
}
}
阻塞IO因模型简单,同步等的方式使代码流程清晰,并可与多线程、进程配合提高吞吐量的优点,一些服务器比如apache。另外客户端或者服务端中代码访问数据库,网络的一般也是同步IO的方式。
1.2 非阻塞IO
调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。
1.3 多路复用
IO multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态,来同时管理多个I/O流以尽量多的提高服务器的吞吐能力
public class NIOServer {
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
handler(key);
}
}
}
}
select, poll, epoll 都是I/O多路复用的具体的实现 对应NIO中的 Selector 会根据系统选择最高性能的实现 linux系统下均为epoll,可以参考这篇文章了解epoll为何高效?
1.4 AIO异步IO
异步io 是只当数据由操作系统写入用户准备的缓冲区后,回调用户提供的注册函数进行处理的IO方式
public class AioServer {
private void init(int port) {
System.out.println("server starting at port "+port+"..");
// 初始化定长线程池
service = Executors.newFixedThreadPool(4);
try {
// 初始化 AsyncronousServersocketChannel
serverChannel = AsynchronousServerSocketChannel.open();
// 监听端口
serverChannel.bind(new InetSocketAddress(port));
// 监听客户端连接,但在AIO,每次accept只能接收一个client,所以需要
serverChannel.accept(this, new AioHandler());
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class AioHandler implements CompletionHandler<AsynchronousSocketChannel, AioServer> {
private void doRead(AsynchronousSocketChannel clientChannel) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(
buffer, // 用于数据中转缓冲区
buffer, // 用于存储client发送的数据的缓冲区
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("from client : " +
new String(attachment.array(), StandardCharsets.UTF_8));
// 向client写入数据
doWrite(clientChannel);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
}
}
);
}
}
AIO 这么好用的模型为何并没有得到大量应用?
netty为何没有使用aio
聊聊BIO,NIO和AIO
2、Netty网络框架
2.1 Reactor模式
reactor是一种基于事件驱动,由分发器通知使用者完成读取处理的网络模型,可以以极少的线程处理大量的连接。
具体到netty 可以描述为 NioEventLoop基于Selector多路复用器获取事件并分发到注册的用户程序,用户程序读取数据完成业务的过程。EventLoop即为分发器 ,Selector即为事件驱动器。
额外内容:
Proactor模式
proactor模式区别在于用户准备缓冲区,当缓冲区数据写入完成后,通知用户程序,是异步的处理过程。而reactor则是通知的就绪事件,需要用户同步读取就绪数据。
2.2 为什么用netty?
1.使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞,比如空轮询。
2.Netty自带的拆包解包,异常检测等机制,支持各种协议,让你只需要关心业务逻辑。
3.Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大。
2.3 netty的结构
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
.bind(8000);
}
}
1.netty中一个EventLoop对应一个多路复用器Selector,一个EventLoopGroup对应多个EventLoop
2.netty 将用于accept的事件使用单独的EventLoopGroup,一般只有一个EventLoop来处理应用的连接事件。
3.netty使用区分于accept的EventLoopGroup 一般会有对应cpu core数量对应的EventLoop来处理应用的读写操作。4.一般来说用户在自行管理的线程池中完成业务处理过程。
3、瓶颈在哪?
回头查看同步IO部分的描述--客户端或者服务端中代码访问数据库,网络的一般也是同步IO的方式--,同时结合netty使用实践中主要的使用模式均为使用线程池来处理用户业务可能存在的同步业务逻辑。
这样有什么问题吗?
随着机器性能增加,支持的并发量增加的同时,以下问题开始导致新的性能问题
1.大量线程处于等待状态 线程本身至少分配1M以上的预留栈空间 对于并发要求很高的模块是很大的浪费。
2.大量的线程上下文切换。
如下代码
String redisStr = redisTemplate.get(key); //wait-1
if (redisStr == null) {
List<Object> myDatas = jpa.get();//wait-2
redisTemplate.set(key, gson.toJson(myDatas));//wait-3
return myDatas;
}
上面这段代码是使用缓存miss时最场景的场景 在简单的几句代码中线程就会有3*2的状态上下文切换,线程大部分时间都在空等mysql或者redis操作。