主机运维
当前位置:时讯云电脑 > 资讯 > 主机运维 >
Java服务器的模型
发布日期:2019-12-11 阅读次数: 字体大小:

  一般来说,我们的应用程序不需要并行处理数千个用户,也不需要在一秒钟内处理数千条消息。我们只需要处理几十或几百个并发用户,我们可以在内部应用程序或一些微服务应用程序中承受如此巨大的负担。

  在这种情况下,我们可以使用一些在线程模型/内存使用方面没有优化的高级框架/库,并且仍然可以承载一些合理的资源和相对较快的交付时间。

  然而,有时我们会遇到系统的一部分需要比其他应用程序更好地扩展的情况。使用传统方法或框架编写系统的这一部分可能会导致巨大的资源消耗,并且需要启动相同服务的许多实例来处理负载。导致处理数千个连接的算法和方法也被称为C10K问题。

  在本文中,我将重点关注在TCP连接/流量中可以进行的优化,以优化(微)服务实例,从而尽可能少地浪费资源,深入了解操作系统如何与TCP和套接字一起工作,最后但同样重要的是,如何深入了解所有这些内容。让我们开始吧。

  让我们描述一下我们目前有什么类型的输入输出编程模型,以及在设计应用程序时可以选择哪些选项。首先,没有好的或坏的方法,只有一种更适合我们当前的用例。选择错误的方法在将来会产生非常不方便的后果。这可能导致资源浪费,甚至从头重写应用程序。

  每个连接服务器的线程数

  这种方法背后的想法是,如果没有专用/空闲线程,套接字连接将不会被接受(我们将在后面展示它的含义)。在这种情况下,阻塞意味着特定线程绑定到连接,并且在读取或写入连接时总是阻塞。

  套接字服务器的最简单版本,从端口5050开始,以阻塞方式从输入流读写输出流。当我们需要通过连接传输少量对象,然后关闭它,并在需要时启动一个新对象时,这很有用。

  即使没有任何高级库,它也可以实现。使用阻塞流读/写(等待阻塞输入流读操作,该操作用当时在TCP接收缓冲区中可用的字节填充所提供的字节数组,并返回字节数或流的-1- end)并消耗字节,直到我们有足够的数据来构造请求。当我们开始为无边界的传入连接创建线程时,存在一个大问题和低效率。我们将为非常昂贵的线程创建和内存影响买单,这与将一个Java线程映射到一个内核线程是分不开的。它不适合“真实”生产,除非我们真的需要一个低内存使用量的应用程序,并且不想加载属于某些框架的许多类。

        基于线程池的服务器

  这是大多数知名企业的超文本传输协议服务器所属的类别。一般来说,该模型使用多线程池,这使得多cpu环境中的处理更加高效,更适合企业应用程序。有几种方法可以配置线程池,但是基本思想在所有的超文本传输协议服务器中是完全相同的。有关通常可以基于基于线程池的非阻塞服务器配置的所有可能策略,请参考HTTP Grizzly输入/输出策略。

  用于接受新连接的第一个线程池。如果一个线程可以管理传入连接的速度,它甚至可以是一个线程池。通常有两个积压可以填补,下一个传入连接被拒绝。如果可能,检查持久连接是否正确使用。第二个线程池,用于以非阻塞方式从套接字写入/写入套接字(选择器线程或输入输出线程)。每个选择器线程处理多个客户端(通道)。第三个线程池用于将请求处理的非阻塞部分与阻塞部分(通常称为工作线程)分开。

      某些阻塞操作无法阻塞选择器线程,因为所有其他通道都无法取得任何进展(通道组只有一个线程,该线程将被阻塞)。使用缓冲区实现非阻塞读/写。只要处理请求的特定线程没有得到满足(因为它们没有足够的数据来构造,例如,HTTP请求),选择器线程就从套接字读取新字节,并将它们写入专用缓冲区(池缓冲区)。我们需要澄清非阻塞术语:我们在套接字服务器的上下文中交谈,然后非阻塞意味着线程没有绑定到开放的连接,并且不等待输入数据(甚至在TCP发送缓冲区已满时写入数据)。只要我们尝试读取,如果没有字节,那么就不会有字节被添加到缓冲区用于进一步处理(构造请求),并且给定的选择器线程将继续从另一个打开的连接读取。然而,在处理请求方面,代码在大多数情况下被阻塞,这意味着我们执行一些代码来阻塞当前线程,该线程等待输入/输出绑定处理(数据库查询、HTTP调用、从磁盘读取等)。

       或一些长时间的中央处理器绑定处理(散列/阶乘计算、加密挖掘,)。如果执行完成,线程将会醒来并继续在一些业务逻辑中执行。业务逻辑的阻塞特性是工作池如此之大的主要原因。我们只需要让大量线程工作来提高吞吐量。否则,在高负载条件下(例如,更多的HTTP请求),我们可能会导致所有线程被阻塞,并且没有线程可用于请求处理(没有处于可运行状态的线程可在CPU上执行)。尽管请求数量很高,并且我们的许多工作线程在一些阻塞操作中被阻塞,但是我们可以接受新的连接,即使我们可能无法立即处理它们的请求,并且数据必须在TCP接收缓冲区中等待。这个编程模型被许多框架/库秘密使用和超文本传输协议服务器(码头、雄猫、灰熊…,)因为如果真的需要,编写业务代码和阻塞线程非常容易。

        并行性通常不是由中央处理器的数量决定的,而是由阻塞操作的性质和工作线程的数量决定的。通常,这意味着如果阻塞操作(输入/输出)和进一步执行(在请求过程中)之间的时间比太高,那么我们可以得到:阻塞操作(数据库查询)上的许多阻塞线程.)正在等待处理来自工作线程的大量请求,并且由于没有线程可以继续执行而非常未使用的大量CPU线程池导致上下文切换和CPU缓存的低效使用。好的,我们有一个或多个线程池来处理被阻止的业务操作。然而,线程池的最佳大小是多少?

        我们可能会遇到两个问题:

  线程池太小。我们没有足够的线程来覆盖所有线程被阻塞的时间,例如等待输入/输出操作,而您的CPU没有得到有效的使用。线程池太大,我们必须为许多实际空闲的线程付费(请参见下面运行许多线程的成本)。

  我想我可以参考布赖恩·戈茨(Brian Goetz)的书《Java并发实践》,书中说调整线程池的大小不是一门精确的科学,它更多的是关于理解您的环境和任务的本质。

  您的环境有多少CPU和内存?任务主要是执行计算、输入输出还是某种组合?他们需要稀缺资源吗(JDBC关系)?线程池和连接池会相互影响。当我们充分利用连接池时,增加线程池以获得更好的吞吐量可能没有意义。

  如果我们的程序包含输入/输出或其他阻塞操作,您需要一个更大的池,因为您的线程不允许一直在中央处理器上。您需要使用一些分析器或基准来估计等待时间与计算任务时间的比率,并观察生产工作负载不同阶段(高峰和非高峰时间)的CPU利用率。

  基于与中央处理器内核相同线程数的服务器

  如果我们能够以非阻塞方式管理大多数工作负载,这种策略最有效。这意味着使用非阻塞算法实现处理套接字(接受连接、读取、写入),但是即使是业务处理也不包含任何阻塞操作。

  这个策略的典型代表是Netty框架,所以让我们更仔细地看看如何实现这个框架的架构基础,以理解为什么它最适合解决C10K问题。如果您想了解更多关于它是如何工作的,我可以推荐以下资源:

  诺曼·摩尔的《行动中的Action——。作者netty framework normemauer。这是了解如何使用带有各种协议的处理程序来实现基于Netty的客户机或服务器的宝贵资源。

  Netty是一个输入输出库和框架,它简化了非阻塞输入输出编程,并为服务器生命周期和传入连接期间发生的事件提供了异步编程模型。我们只需要通过lambdas连接回拨,就可以免费获得一切。

  许多协议可以在不依赖大型库的情况下使用

  开始用纯JDK NIO构建应用程序是非常令人沮丧的,但是Netty包含的特性使程序员处于较低的水平,并提供了提高许多事情效率的可能性。Netty已经包含了大部分众所周知的协议,这意味着我们可以比在更高级别的库中使用大量的模板文件更有效地使用它们,比如泽西/斯普林的超文本传输协议。输入/输出处理、协议实现和所有其他处理程序应该使用非阻塞操作来永不停止当前线程。我们总是可以使用额外的线程池来阻止操作。但是,如果我们需要将每个请求的处理切换到专用线程池来执行阻塞操作,那么我们很少使用Netty的函数,因为我们可能会遇到与非阻塞输入输出相同的情况,即阻塞处理——一个大线程池位于应用程序的不同部分。

  事件循环组-收集事件循环,并提供一个通道来注册其中一个事件循环。

  事件循环-处理给定事件循环的注册通道的所有输入/输出操作。EventLoop只在一个线程上运行。因此,对于事件循环组(EventLoopGroup),事件循环的最佳数量是CPU的数量(一些框架使用多个CPU来在页面错误的情况下拥有额外的线程)。

  管道——维护处理程序的执行顺序(当输入或输出事件发生时被排序和执行的组件包含实际的业务逻辑)。管道和处理程序在属于事件循环的线程上执行,因此处理程序中的阻塞操作会阻塞给定事件循环中的所有其他进程/通道。

  这都是为了分享。我希望你能注意它。等待你表达不同的意见来讨论。