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

Java服务器模型——TCP连接-流量优化

时间:2023-03-15 10:00:06 科技观察

通常,我们的应用不需要并行处理上千个用户,也不需要一秒钟处理上千条消息。我们只需要应付几十上百个并发连接的用户,在内部应用或者一些微服务应用中就可以承受这么大的负载。在这种情况下,我们可以使用一些高级框架/库,这些框架/库在使用的线程模型/内存方面没有优化,但仍然提供一些合理的资源和合理的快速交付时间。然而,有时我们会遇到系统的一部分需要比其他应用程序更好地扩展的情况。使用传统方法或框架编写系统的这一部分会导致巨大的资源消耗,并且需要启动同一服务的许多实例来处理负载。导致处理数千个连接的算法和方法也称为C10K问题。在本文中,我将主要关注在TCP连接/流量方面可以优化的内容,优化(微)服务实例以尽可能少地浪费资源,深入了解操作系统如何使用TCP和套接字,最后但并非最不重要也是最不重要的一点是,如何查明所有这些事情的真相。让我们开始吧。I/O编程策略让我们描述一下我们目前有什么类型的I/O编程模型,以及在设计应用程序时我们需要选择哪些选项。首先,方法没有好坏之分,只有更适合我们当前用例的方法。选择错误的方法会给将来带来非常不便的后果。它可能导致资源浪费甚至从头开始重写应用程序。通过阻塞处理每个连接服务器的线程来阻塞I/O这种方法背后的想法是,如果没有任何专用/空闲线程,则不接受套接字连接(稍后我们将说明这意味着什么)。在此上下文中阻塞意味着特定线程绑定到连接并且在读取或写入连接时始终阻塞。publicstaticvoidmain(String[]args)throwsIOException{try(ServerSocketserverSocket=newServerSocket(5050)){while(true){SocketclientSocket=serverSocket.accept();vardis=newDataInputStream(clientSocket.getInputStream());vardos=newDataOutputSocketStream(客户端());newThread(newClientHandler(dis,dos)).start();}}}套接字服务器的最简单版本,从端口5050开始,以阻塞方式从InputStream读取并写入OutputStream。当我们需要通过一个连接传输少量对象时很有用,然后关闭它并在需要时启动一个新的连接。即使没有任何高级库也可以实现。使用阻塞流读取/写入(等待阻塞InputStream读取操作,该操作使用当时TCP接收缓冲区中可用的字节填充提供的字节数组并返回字节数或-1-流结束)并消耗字节部分直到我们有足够的数据来构建请求。当我们开始为无限制的传入连接创建线程时,就会出现一个大问题和低效率。我们将为非常昂贵的线程创建和内存影响付出代价,这与将Java线程映射到内核线程密不可分。它不适合“真正的”生产,除非我们真的需要一个低内存占用的应用程序并且不想加载大量属于某个框架的类。具有阻塞处理的非阻塞I/O基于线程池的服务器这是最著名的企业HTTP服务器属于的类别。总的来说,这种模型使用多个线程池,使得在多CPU环境下的处理效率更高,更适合企业应用。配置线程池的方法有多种,但基本思想在所有HTTP服务器中都是完全相同的。有关通常可以根据基于线程池的非阻塞服务器配置的所有可能策略,请参阅HTTPGrizzlyI/O策略。第一个线程池用于接受新连接。如果一个线程可以管理传入连接的速率,它甚至可以是一个单线程池。通常有两个backlog可以被填满而下一个传入连接被拒绝。如果可能,检查持久连接是否被正确使用。第二个线程池,用于以非阻塞方式写入/从套接字(选择器线程或IO线程)。每个选择器线程处理多个客户端(通道)。第三个线程池,用于分离请求处理的非阻塞和阻塞部分(通常称为工作线程)。某些阻塞操作无法阻塞选择器线程,因为所有其他通道都无法取得任何进展(通道组只有一个线程,该线程将被阻塞)。非阻塞读/写是使用缓冲区实现的,每当处理请求的特定线程不满足时(因为它们没有足够的数据来构造例如HTTP请求),选择器线程从套接字读取新字节并写入专用缓冲区(池缓冲区)。我们需要澄清一下非阻塞术语:我们是在Socket服务器的上下文中谈论的,那么非阻塞意味着线程没有绑定到一个打开的连接,并且不等待传入的数据(即使在TCP发送时写入bufferisfull)incomingdata),只是尝试读取,如果没有字节,则不会向缓冲区添加任何字节以进行进一步处理(构造请求),给定的选择器线程将继续从另一个打开的连接读取读。然而,在处理请求时,代码大多是阻塞的,这意味着我们执行一些阻塞当前线程的代码,等待I/O绑定处理(数据库查询、HTTP调用、从磁盘读取等)或一些长时间的CPU绑定处理(计算哈希/阶乘、加密货币挖掘,...)。如果执行完成,线程被唤醒,继续执行一些业务逻辑。业务逻辑的阻塞性质是工作池如此之大的主要原因,我们只需要有很多线程工作来提高吞吐量。否则,在高负载的情况下(比如HTTP请求较多),我们可能会导致所有线程都被阻塞,没有线程可用于请求处理(CPU上没有可运行的线程可以执行)。优势即使请求的数量非常多,并且我们的许多工作线程在某些阻塞操作上被阻塞,我们也能够接受新连接,即使我们可能无法立即处理他们的请求并且数据必须在TCP中接收缓冲区等待。这种编程模型被许多框架/库(SpringControllers、Jersey等)和HTTP服务器(Jetty、Tomcat、Grizzly等)隐式使用,因为它非常容易编写业务代码并在确实需要时让线程阻塞。缺点并行度通常不是由CPU的数量决定的,而是由阻塞操作的性质和工作线程的数量决定的。一般来说,这意味着如果阻塞操作(I/O)和进一步执行(在请求期间)之间的时间比率太高,那么我们可以得到:许多阻塞线程在阻塞操作(数据库查询......)等待处理工作线程的大量请求,以及非常未使用的CPU的大型线程池,因为没有线程可以继续执行,导致上下文切换和CPU缓存的低效使用。如何设置线程池好的,我们有一个或多个线程池来处理阻塞的业务操作。但是,线程池的最佳大小是多少?我们可能会遇到两个问题:线程池太小,我们没有足够的线程来覆盖线程阻塞的所有时间,比如等待I/O操作,而您的CPU没有得到有效使用。线程池太大了,我们为许多实际上空闲的线程付出了代价(见下文运行许多线程的成本)。我认为您可以参考BrianGoetz的JavaConcurrencyinPractice一书,其中说调整线程池的大小并不是一门精确的科学,更多的是了解您的环境和任务的性质。您的环境有多少CPU和多少内存?任务主要执行计算、I/O还是某种组合?它们是否需要稀缺资源(JDBC连接)?线程池和连接池相互影响,当我们充分利用连接池时,增加线程池以获得更好的吞吐量可能没有意义。如果我们的程序包含I/O或其他阻塞操作,你需要一个更大的池,因为你的线程不能一直在CPU上。您需要使用一些分析器或基准来估计等待时间与计算任务时间的比率,并观察生产工作负载不同阶段(高峰时间与非高峰时间)的CPU利用率。Non-blockingI/Ofornon-blockingprocessingServersbasedonthenumberofthreadsasCPUcores如果我们能够以非阻塞方式管理大部分工作负载,则此策略最有效。这意味着处理套接字(接受连接、读取、写入)是使用非阻塞算法实现的,但即使是业务处理也不包含任何阻塞操作。该策略的典型代表是Netty框架,因此让我们深入研究该框架如何实现的架构基础,以了解为什么它最适合解决C10K问题。如果您想了解更多关于它是如何工作的,那么我可以推荐以下资源:NettyinAction-作者NormanMoore。由NettyFramework的作者NormanMauer撰写。这是了解如何使用具有各种协议的处理程序基于Netty实现客户端或服务器的宝贵资源。具有异步编程模型的I/O库Netty是一个I/O库和框架,它简化了非阻塞IO编程,并为服务器生命周期和传入连接期间发生的事件提供异步编程模型。我们只需要将回调与我们的lambda连接起来,我们就可以免费获得一切。许多协议可以在不依赖大型库的情况下使用。开始使用纯JDKNIO构建应用程序可能会非常令人沮丧,但Netty包含使程序员处于较低级别并提供使许多事情更高效的可能性的功能。Netty已经包含了大多数众所周知的协议,这意味着我们可以比使用高级库中的大量样板文件(例如用于HTTP/REST的Jersey/SpringMVC)更有效地使用它们。确定正确的非阻塞用例以充分利用Netty的功能I/O处理、协议实现和所有其他处理应该使用非阻塞操作来永不停止当前线程。我们总是可以使用一个额外的线程池来阻塞操作。但是,如果我们需要将每个请求的处理切换到一个专门的线程池来进行阻塞操作,那么我们就几乎没有使用Netty的能力,因为我们很可能会遇到与非阻塞IO相同的情况,即阻塞处理-一个大的线程池恰好位于应用程序的不同部分。在上图中,我们可以看到Netty架构的主要组件。EventLoopGroup-收集事件循环并提供一个通道来注册其中一个。事件循环——为给定的事件循环处理注册通道上的所有I/O操作。EventLoop只在一个线程上运行。因此,对于EventLoopGroup,事件循环的最佳数量是cpus的数量(某些框架使用多个cpus+1在出现页面错误时拥有额外的线程)。管道-维护处理程序的执行顺序(当某个输入或输出事件发生时排序和执行的组件包含实际的业务逻辑)。管道和处理程序在属于EventLoop的线程上执行,因此,处理程序中的阻塞操作会阻塞给定EventLoop上的所有其他处理/通道。