实际场景下,百万级并发请求是较多的,比如电商的促销、12306抢票、健康码查询等等,这些场景要求应用服务稳定,能最大化资源利用。所以实际应用程序设计时,会对QPS并发做预估处理,通过QPS选择合适的设计实现和对应的服务器资源数。一般会做如下要求:
面临高并发场景诉求,解决办法有三个主题:
IO在计算机中指Input/Output,也就是输入和输出。计算机运行时数据是在内存中驻留,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
LINUX中进程无法直接操作I/O设备,必须通过系统调用请求kernel来协助完成I/O动作。内核会为每个I/O设备维护一个缓冲区。IO输入时,应用进程请求内核,内核会先看缓冲区中有没有相应的缓存数据,有数据则直接复制到进程空间,没有的话再到设备中读取。通常用户进程中的一个完整IO分为两阶段:用户进程空间<-->内核空间、内核空间<- ->设备空间。
由于CPU和内存的速度远远高于外设的速度,所以在IO编程中,就存在速度严重不匹配的问题,所以有了同步/异步,阻塞和非阻塞IO之分。
IO模型分为:BIO、NIO、IO多路复用、信号驱动IO和AIO。
异步IO是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待,所以只有AIon才是真正的一步IO。
先了解一下Reactor模式,
在Reactor模式中,有5种角色:
Reactor 单线程模型:
优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点:性能问题,单线程无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈 。
多线程Reactor
优点:可以充分的利用多核cpu 的处理能力
缺点:多线程数据共享和访问比较复杂,但是reactor处理所有的事件的监听和响应,在单线程运行, 在高并发场景容易出现性能瓶颈。
主从Reactor
优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
缺点:编程复杂度较高
对于缓冲区来说,尤其是对于堆外直接内存的分配和回收,是一种耗时的操作。为了尽量重复利用缓冲区内存,Netty设计了一套基于内存池的缓冲区重用机制。
大多数应用场景下,并行多线程处理可以提升系统的并发性能。但如果共享资源竞争激烈,就会造成严重的锁竞争,导致系统性能的下降。为了尽可能避免锁竞争带来的性能损耗,可以通过串行化设计来避免多线程竞争和同步锁,即消息的处理尽可能在同一个线程内完成,不进行线程切换。
Netty的高效并发编程主要体现在如下几点:
volatile关键字的大量且正确的使用。
CAS和原子类的广泛使用。
线程安全容器的使用。
灵活的TCP参数配置能力。
影响序列化性能的关键因素有:序列化后的码流大小(网络带宽的占用);序列化/反序列化的性能(CPU资源占用)和是否支持跨语言。Netty默认提供了对Google Protobuf的支持,用户也可以通过扩展Netty的编解码接口接入其他高性能的序列化框架进行编解码,例如Thrift的压缩二进制编解码框架。
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- EventLoopGroup workerGroup = new NioEventLoopGroup();
- try {
- ServerBootstrap bootstrap = new ServerBootstrap();
- bootstrap.option(ChannelOption.SO_REUSEADDR, true).option(ChannelOption.SO_BACKLOG, 1024)
- .option(ChannelOption.SO_RCVBUF, 1024 * 128)
- .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
-
- bootstrap.childOption(ChannelOption.TCP_NODELAY, true)
- .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
- .childOption(ChannelOption.SO_KEEPALIVE, true);
-
- bootstrap.group(bossGroup,workerGroup)
- .channel(NioServerSocketChannel.class)
- .localAddress(new InetSocketAddress(port))
- .childHandler(new ChannelInitializer
() { - EventExecutorGroup logicGroup = new DefaultEventExecutorGroup(16);
- @Override
- protected void initChannel(SocketChannel ch) throws Exception {
- ch.pipeline().addLast(logicGroup,new EchoServerWithExecutorHandler());
- }
- });
-
- ChannelFuture f = bootstrap.bind().sync();
- System.out.println(App.class.getName() + " started and listen on " + f.channel().localAddress());
- f.channel().closeFuture().sync();
- } finally {
- bossGroup.shutdownGracefully().sync();
- workerGroup.shutdownGracefully().sync();
- }