当前位置: 首页 > 科技观察

为什么Redis、Nginx、Netty这么香?

时间:2023-03-13 15:38:42 科技观察

Redis、Nginx、Netty、Node.js为什么这么香?这些技术都与Linux内核迭代中提供的系统调用一起出现,以有效地处理网络请求。今天我们从操作系统层面来了解Linux下的网络IO模型!图片来自PexelsI/O(INPUT/OUTPUT),包括文件I/O、网络I/O。计算机世界里的速度鄙视:内存读取数据:纳秒级。千兆网卡读取数据:细微级。1微秒=1000纳秒,网卡比内存慢一千倍。磁盘读取数据:毫秒级。1毫秒=10万纳秒,硬盘比内存慢10万倍。CPU每个时钟周期大约需要1纳秒,而内存离CPU比较近,其他的等不起。CPU处理数据的速度比I/O准备数据的速度快得多。任何编程语言都会遇到CPU处理速度和I/O速度不匹配的问题!网络编程中如何优化网络I/O?如何高效利用CPU进行网络数据处理?相关概念如何从操作系统层面理解网络I/O?计算机世界有一套自己定义的概念。如果不理解这些概念,就无法真正理解技术的设计思想和本质。所以在我看来,这些概念是理解技术和计算世界的基础。同步与异步,阻塞与非阻塞了解网络I/O绕不开的话题:同步与异步,阻塞与非阻塞。以山治开水为例(山治的行为就像是一个用户程序,而开水就像是内核提供的系统调用),这两组概念可以这样翻译成大白话:同步/异步的重点是沸水我需要处理吗。阻塞/非阻塞关注的是在水烧开的这段时间里有没有做其他事情。同步堵:点火后,傻等,坚决不干,等到开水(堵),开水关火(同步)。同步无阻:点火后,看电视(无阻),时不时检查水是否开,关水后关火(同步)。异步阻塞:按下开关后,傻等水开(阻塞),水开后自动关闭电源(异步)。网络编程中不存在的模型。异步非阻塞:按下开关后,做什么(非阻塞),水烧开后自动断电(异步)。内核空间、用户空间内核空间、用户空间如上图所示:内核负责读写网络和文件数据。用户程序通过系统调用获取网络和文件数据。内核态和用户态如上图所示:程序要进行系统调用来读写数据。通过系统调用接口,线程从用户态切换到内核态,内核读写数据后,再切换回来。进程或线程的不同空间状态。线程切换如上图所示。在用户模式和内核模式之间切换需要时间并消耗资源(内存,CPU)。优化建议:减少切换。共享空间。Socket:Socket套接字的作用如下:只有有了套接字才能进行网络编程。应用程序通过系统调用socket()建立连接、接收和发送数据(I/O)。如果Socket支持非阻塞,则应用程序可以进行非阻塞调用,如果支持异步,则应用程序可以进行异步调用。文件描述符:FD句柄网络编程需要知道FD???FD是什么鬼???Linux:万物皆文件,fd是对文件的引用。就像Java中的一切都是对象?程序中操作的是对象的引用。Java中创建对象的数量是有内存限制的,FD的数量也是有限制的。Linux在处理文件和网络连接时需要打开和关闭FD。每个进程都会有一个默认的FD:0标准输入stdin1标准输出stdout2错误输出stderr服务器处理网络请求的过程服务器处理网络请求的过程如上图所示:建立连接后。等待数据就绪(CPU空闲)。将数据从内核复制到进程(CPU空闲)。如何优化呢?对于一次I/O访问(以read为例),数据会被复制到操作系统内核的缓冲区中,然后再从操作系统内核的缓冲区中复制到应用程序的地址空间中。因此,当发生读取操作时,它会经历两个阶段:等待数据就绪。将数据从内核复制到进程(Copyingthedatafromthekerneltotheprocess)。也正是因为这两个阶段,在Linux系统升级迭代中出现了以下三种网络模式方案。I/OModelBlockingI/O:阻塞式I/O简介:最原始的网络I/O模型。该过程将阻塞,直到数据复制完成。缺点:高并发时,服务端和客户端是对等的。线程过多带来的问题:CPU资源浪费,上下文切换。内存成本呈几何级数增长,一个JVM线程的成本约为1MB。publicstaticvoidmain(String[]args)throwsIOException{ServerSocketss=newServerSocket();ss.bind(newInetSocketAddress(Constant.HOST,Constant.PORT));intidx=0;while(true){finalSocketsocket=ss.accept();//阻塞方法newThread(()->{handle(socket);},"thread["+idx+"]").start();}}staticvoidhandle(Socketsocket){byte[]bytes=newbyte[1024];try{StringserverMsg="serversss[Thread:"+Thread.currentThread().getName()+"]";socket.getOutputStream().write(serverMsg.getBytes());//阻塞方法socket.getOutputStream()。flush();}catch(Exceptione){e.printStackTrace();}}Non-blockingI/O:NonBlockingIO简介:进程反复调用系统并立即返回结果。缺点:当进程有1000fds时,意味着用户进程轮询调用内核1000次,用户态和内核态的来回切换,成本呈几何级数增长。publicstaticvoidmain(String[]args)throwsIOException{ServerSocketChannelss=ServerSocketChannel.open();ss.bind(newInetSocketAddress(Constant.HOST,Constant.PORT));System.out.println("NIOserverstarted...");ss。configureBlocking(false);intidx=0;while(true){finalSocketChannelsocket=ss.accept();//阻塞方法newThread(()->{handle(socket);},"Thread["+idx+"]").start();}}staticvoidhandle(SocketChannelsocket){try{socket.configureBlocking(false);ByteBufferbyteBuffer=ByteBuffer.allocate(1024);socket.read(byteBuffer);byteBuffer.flip();System.out.println("请求:"+newString(byteBuffer.array()));Stringresp="服务器响应";byteBuffer.get(resp.getBytes());socket.write(byteBuffer);}catch(IOExceptione){e.printStackTrace();}}I/O多路复用:IO多路复用简介:单个线程可以同时处理多个网络连接。内核负责轮询所有的Socket,当一个Socket有数据到达时,通知用户进程。Multiplexing在Linux内核代码的迭代过程中依次支持三种调用,即Select、Poll和Epoll,三种多路复用的网络I/O模型。下面结合Java代码来讲解绘图。①I/O多路复用:Select介绍:连接请求到达时进行检查和处理。缺点如下:句柄上限:默认打开FD有1024个限制。重复初始化:每次调用select(),都需要将FD集合从用户态复制到内核态,内核遍历。一一检查所有FD状态效率不高。服务器端的Select就像一条布满插座的条带。client端的connection连接其中一个socket,建立一个channel,然后在channel上依次注册读写事件。记得在处理完ready,read,write事件的时候删除,不然下次可以处理。publicstaticvoidmain(String[]args)throwsIOException{ServerSocketChannelssc=ServerSocketChannel.open();//管道ServerSocketssc.socket().bind(newInetSocketAddress(Constant.HOST,Constant.PORT));ssc.configureBlocking(false);//设置非阻塞System.out.println("NIOsingleserverstarted,listeningon:"+ssc.getLocalAddress());Selectorselector=Selector.open();ssc.register(selector,SelectionKey.OP_ACCEPT);//在已建立的管道上,已注册的事件已准备就绪=it.next();it.remove();//必须删除处理过的事件handle(key);}}}privatestaticvoidhandle(SelectionKeykey)throwsIOException{if(key.isAcceptable()){ServerSocketChannelssc=(ServerSocketChannel)key.channel();SocketChannelsc=ssc.accept();sc.configureBlocking(false);//设置非阻塞sc.register(key.selector(),SelectionKey.OP_READ);//在已建立的管道上,前夕注册的nts是可读的}elseif(key.isReadable()){//flipSocketChannelsc=null;sc=(SocketChannel)key.channel();ByteBufferbuffer=ByteBuffer.allocate(512);buffer.clear();intlen=sc.read(buffer);if(len!=-1){System.out.println(""+Thread.currentThread().getName()+"]recv:"+newString(buffer.array(),0,len));}ByteBufferbufferToWrite=ByteBuffer.wrap("HelloClient".getBytes());sc.write(bufferToWrite);}}②I/O多路复用:Poll介绍:设计一个新的数据结构(链表)来提供效率。Poll与Select相比,本质上变化不大,只是Poll没有Select模式下最大的文件描述。字数限制。缺点:一一检查所有FD状态效率不高。③I/O多路复用:epoll介绍:FD的个数没有限制。只需要从用户态复制到内核态一次,使用事件通知机制触发。通过epoll_ctl注册FD,一旦FD就绪,就会通过回调机制激活对应的FD,进行相关的I/O操作。缺点如下:跨平台,linux支持最好。底层实现很复杂。同步。publicstaticvoidmain(String[]args)throwsException{finalAsynchronousServerSocketChannelserverChannel=AsynchronousServerSocketChannel.open().bind(newInetSocketAddress(Constant.HOST,Constant.PORT));serverChannel.accept(null,newCompletionHandler(){@Overridepublicvoidcompleted(finalAsynchronousSocketChannelclient),Objectattachment){serverChannel.accept(null,this);ByteBufferbuffer=ByteBuffer.allocate(1024);client.read(buffer,buffer,newCompletionHandler(){@Overridepublicvoidcompleted(Integerresult,ByteBufferattachment){attachment.flip();client.write(ByteBuffer.wrap("HelloClient".getBytes()));//业务逻辑}@Overridepublicvoidfailed(Throwableexc,ByteBufferattachment){System.out.println(exc.getMessage());//失败处理}});}@Overridepublicvoidfailed(Throwableexc,Objectattachment){exc.printStackTrace();//失败处理}});while(true){//不是whiletruemain方法一闪而过}}当然是上面与它相比的缺点优点可以忽略。JDK提供了异步的实现,但是在实际的Linux环境中,底层还是Epoll,只是多了一层循环,并不是真正的异步非阻塞。而且就像上图中的代码调用一样,处理网络连接的代码和业务代码解耦不够好。Netty提供了简洁、解耦、结构清晰的API。publicstaticvoidmain(String[]args){newNettyServer().serverStart();System.out.println("Nettyserverstarted!");}publicvoidserverStart(){EventLoopGroupbossGroup=newNioEventLoopGroup();EventLoopGroupworkerGroup=newNioEventLoopGroup();ServerBootstrapb=newServerBootstrap();b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer(){@OverrideprotectedvoidinitChannel(SocketChannelch)throwsException{ch.pipeline().addLast(newHandler());}});try{ChannelFuturef=b.localAddress(Constant.HOST,Constant.PORT).bind().sync();f.channel().closeFuture().sync();}catch(InterruptedExceptione){e.printStackTrace();}最后{workerGroup.shutdownGracefully();bossGroup.shutdownGracefully();}}}classHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{ByteBufbuf=(ByteBuf)msg;ctx.writeAndFlush(msg);ctx.close();}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{cause.printStackTrace();ctx.close();}}bossGroup是处理网络请求的steward(s),当网络连接就绪时,workers(who)交给workGrouptowork总结回顾上面总结如下:同步/异步,连接建立后,用户程序读写时,如果用户程序还需要调用系统read()读取数据,则是同步的,否则就是异步的。Windows是真正的异步,内核代码非常复杂,但对用户程序是透明的。阻塞/非阻塞,连接建立后,用户程序可以在等待读写的同时做其他事情。如果可能,它是非阻塞的,否则是阻塞的。大多数操作系统都支持。为什么Redis、Nginx、Netty、Node.js这么香?这些技术都与Linux内核迭代中提供的系统调用一起出现,以有效地处理网络请求。只有了解了计算机的底层知识,才能对I/O有更深刻的理解,知其然,知其所以然。与你分享!作者:周生帅简介:宜信支付清算部支付研发团队高级工程师编辑:陶家龙、孙淑娟经验、实战案例、技术人员成长等。