【问题标题】:One thread per client. Doable?每个客户端一个线程。可行吗?
【发布时间】:2011-04-21 11:45:22
【问题描述】:

我正在编写一个 Java 服务器,它使用普通套接字来接受来自客户端的连接。我使用的是相当简单的模型,其中每个连接都有自己的线程以阻塞模式读取它。伪代码:

handshake();

while(!closed) {
  length = readHeader(); // this usually blocks a few seconds
  readMessage(length);
}

cleanup();

(线程是从Executors.newCachedThreadPool() 创建的,因此在启动它们时不应该有任何重大开销)

我知道这有点幼稚,如果线程是专用的操作系统线程,它就不能很好地扩展到许多连接。但是,我听说 Java 中的多个线程可以共享一个硬件线程。这是真的吗?

知道我将在 Linux 上使用 Hotspot VM,在具有 8 个内核和 12GB 内存的服务器上,您认为这种设置适用于数千个连接吗?如果没有,有什么替代方案?

【问题讨论】:

    标签: java multithreading networking io nio


    【解决方案1】:

    这可能会扩展到成千上万的客户。但是有多少千是下一个问题。

    一种常见的替代方法是使用 java.nio 包中的选择器和非阻塞 I/O。

    最终您会遇到这样一个问题:在集群配置中设置服务器是否有用,从而在多台物理机上平衡负载。

    【讨论】:

      【解决方案2】:

      这将很好地扩展到数百个连接,而不是数千个。一个问题是 Java 线程也需要相当多的堆栈(例如 256K),并且操作系统会在调度所有线程时遇到问题。

      查看 Java NIO 或框架,它们将帮助您更轻松地开始处理复杂的事情(例如 Apache Mina)

      【讨论】:

      • 每个线程都会占用一些堆栈,但如果我将其转换为非阻塞模型,每个连接都需要堆上的更多数据,例如“我们现在处于读取消息的哪个阶段” ,目前只是由线程的指令指针(正确的字?)确定。不过,时间安排可能是个问题。
      • @Bart:每个连接的额外空间远不及堆栈那么大;去 NIO 将提高可扩展性。代价是大多数人发现很难理解正在发生的事情(而且 Java 没有协程,可以用来平衡关注点)。
      • 正如 ivy 所说,扩展到数千的主要考虑因素实际上是它们最终将消耗的堆栈。另一方面,我并不完全相信日程安排将是一个主要因素。这将是一些因素,但可能没有人们想象的那么大。如果您的主要活动是 I/O,那么您的线程在大多数情况下会立即让出 CPU,并且只有在接收到数据时才会调入。如果你仔细想想,即使你使用 NIO,那幅画也没有太大的不同。只是请求大致相同数量的 CPU 周期涉及更多的线程。
      • 您可以将堆栈配置得更小。此外,不使用的堆栈部分不会被换出,甚至永远不会分配给物理页面吗?似乎空闲线程将只有一个内存页的工作集。可能这仍然比完成请求(少量指针)所需的要多得多,但非常易于管理。
      • 您可以将堆栈配置得更小(例如小 4 倍),但这只会有一点帮助。异步框架是解决这个问题的关键。我很高兴在 Java 世界中使用 Netty。
      【解决方案3】:

      为了在处理多个套接字时获得良好的性能,您通常使用select 方法,这是 Unix API 处理需要大量资源的单线程多套接字应用程序的方式。

      这可以通过具有Selector 类的java.nio 包来完成,该类基本上能够通过所有打开的套接字并在有新数据可用时通知您。

      您在单个 Selector 中注册所有打开的流,然后您可以从一个线程中处理所有这些流。

      您可以通过教程here获得更多信息

      【讨论】:

      • 我偶然发现了一个关于 InputStream.available() 方法的 SO 问题,我已经忘记了。你能告诉我使用Selector 与让线程处理多个连接相比,使用available() 来防止阻塞的优势吗?
      • 因为您避免以编程方式重新实现通过各种套接字的东西,因为您将使用专门为此目的量身定制的东西:) 主要优势是节省调试和实施时间..
      • 嗯,但至少在我的情况下,它的实现将非常简单。我的协议是 1. 读取指定消息长度的整数。 2. 一次读取该字节数
      • 在这种情况下,这是一个偏好问题。当然避免使用nio,当你不知道它也不需要它时,它会是矫枉过正,但如果你打算有很多套接字,为什么不给通道和异步 IO 一个机会呢?请注意,您必须关心中断线程以避免在数据未准备好时使其陷入无限循环..
      【解决方案4】:

      线程不像以前那么昂贵了,所以“普通”的 IO 实现在一定程度上是可以的。但是,如果您正在考虑扩展到数千甚至更多,则可能值得研究更复杂的东西。

      java.nio 包通过提供套接字多路复用/非阻塞 IO 解决了这个问题,它允许您将多个连接绑定到一个 Selector。然而,由于多线程和非阻塞方面,这种解决方案比简单的阻塞方法更难实现。

      如果您希望追求超越简单 IO 的东西,那么我建议您查看其中一个优质的网络抽象库。根据个人经验,我可以推荐Netty,它会为您完成大部分繁琐的 NIO 处理。然而,它确实有一点学习曲线,但是一旦你习惯了基于事件的方法,它就会非常强大。

      【讨论】:

        【解决方案5】:

        如果您对利用现有容器的部署和管理感兴趣,您可能会考虑在 Tomcat 内部创建一个新的协议处理程序。有关相关问题,请参阅 this answer

        更新:来自 Matthew Schmidt 的 This post 声称 Tomcat 6 中基于 NIO 的连接器(由 Filip Hanik 编写)实现了 16,000 个并发连接。

        如果您想编写自己的连接器,请查看 MINA 以帮助进行 NIO 抽象。 MINA 还具有管理功能,可以消除对另一个容器的需求(您是否应该关心许多单元的部署及其操作等)

        【讨论】:

        • 有趣,但我的应用程序当前没有使用容器。出于教育目的,我自己也很感兴趣与 NIO 合作(如果有必要的话)。
        【解决方案6】:

        LinuxJVM 使用一对一的线程映射。这意味着每个 Java 线程都映射到一个本地 OS 线程。

        因此创建一千个或更多线程并不是一个好主意,因为它会影响您的性能(context switchingcache 刷新/missessynchronization 延迟等)。如果您的 CPU 少于一千个,这也没有任何意义。

        要同时为多个客户端提供服务,唯一合适的解决方案是使用异步 I/O。详情请见Java NIOthis answer

        另见:

        【讨论】:

        • 谢谢,这是一个有用的答案,但我不同意“如果你的 CPU 少于一千,这也没有任何意义。”。将连接的当前状态(“我们现在是在读取标头还是数据”,“我们收到了多少消息”等)保持在堆栈而不是堆上是有意义的,后者更快(对吧?),并且更容易编程。
        • 它只会让你觉得它更容易编程(两种方法都相对容易 IMO)。事实上,使用堆栈并不能让它更快,这完全取决于你在做什么。无论如何,拥有 1K 线程的权衡将大于维护具有预分配状态的会话列表。
        • @Bart:不,它不会有效,即使您将使用堆栈。异步 I/O 比创建数千个线程并使用每个线程的堆栈检查任何内容要高效得多。
        【解决方案7】:

        试试Netty

        “每个请求一个线程”模型是大多数 Java 应用服务器的编写方式。您的实施可以像他们一样进行扩展。

        【讨论】:

        • 仅仅因为“大多数”人做某事就意味着它是正确的或最好的方法。唯一可以肯定地回答这个问题的方法是使用这两种方法编写代码并进行测试,看看哪个在 CPU 和内存使用方面表现更好。由于 windows 和 linux 之间的差异,您可能需要考虑在这两种环境中进行测试才能确定。也就是说,我相信每个请求一个线程是不可扩展的。
        【解决方案8】:

        我认为更好的方法是不自己处理线程。创建一个池(ThreadExecutor 或其他一些东西)并将简单的分派工作发送到您的池。

        当然,我认为异步 I/O 会使其更好更快,但会帮助您解决套接字和网络问题。仅有的。当你的线程因为 I/O 而阻塞时,JVM 会让它进入休眠状态并切换到另一个线程,直到阻塞的 I/O 返回。但这只会阻塞线程。您的处理器将继续运行并开始处理其他线程。所以,减去创建线程的时间,你使用 I/O 的方式对你的模型影响不大。 如果您不创建线程(使用池),您的问题就解决了。

        【讨论】:

        • 我确实使用了 Executor,但由于每个线程基本上都在执行 while(!closed) read();,这实际上并没有减少线程数,它只是减少了创建它们的开销。
        • 这正是我的观点。当您调用 read() 时,您的线程将阻塞,直到它有事可做。不管你有多少线程,但你有多少 RUNNABLE 线程,阻塞的线程不计算在内。他们不会竞争处理器时间。所以,最终你将只有那些有事情要做的线程通过处理器时间竞争。
        【解决方案9】:

        我建议它更多地取决于服务器在处理消息时正在做什么。如果它相对轻量级,那么您的机器规格应该可以轻松应对仅处理数千个此类进程的连接。数万可能是另一个问题,但您只需要在同一网络上的两台机器进行实际经验测试并获得明确答案。

        【讨论】:

          【解决方案10】:

          为什么要自己动手?您可以将 servlet 容器与 servlet、消息队列或 ZeroMQ 一起使用。

          【讨论】:

            猜你喜欢
            • 2015-03-24
            • 2014-06-08
            • 2021-03-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-03-24
            相关资源
            最近更新 更多