每个客户端一个线程,可行吗?

19

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

handshake();

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

cleanup();

Executors.newCachedThreadPool()创建的线程,因此启动它们不应该有任何显着的开销。

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

在Linux上使用Hotspot VM,在具有8个内核和12GB RAM的服务器上使用这种设置,您认为这种设置是否适合处理数千个连接? 如果不是,有什么替代方案?

10个回答

7

这种方法适用于处理数百个连接,但不适用于处理数千个连接。一个问题是Java线程需要相当多的堆栈(例如256K),操作系统将无法调度所有线程。

可以考虑使用Java NIO或框架来更轻松地开始处理复杂的事情(例如Apache Mina)。


1
每个线程都会占用一些堆栈,但如果我将其转换为非阻塞模型,则每个连接将需要更多的堆上数据,例如“我们现在正在读取消息的哪个阶段”,这目前仅由线程的指令指针(正确的词吗?)确定。调度可能是一个问题。 - Bart van Heukelom
1
@Bart:每个连接的额外空间远远不及堆栈大;转向NIO将提高可伸缩性。代价是大多数人发现很难理解发生了什么(而Java没有协程,协程可以用于平衡问题)。 - Donal Fellows
正如ivy所说,扩展到数千个的主要考虑因素确实是它们最终将要消耗的堆栈。另一方面,我并不完全相信调度会成为一个重要因素。它会是一个因素,但可能不像人们想象得那么大。如果你的主要活动是I/O,那么在大多数情况下,你的线程将迅速让出CPU,并且只有在接收到数据时才会被分页。如果你仔细想想,即使使用NIO,这种情况也没有太大区别。只是更多数量的线程参与了请求大约相同数量的CPU周期的过程。 - sjlee
你可以配置更小的堆栈。此外,未使用的堆栈部分是否会被交换出去或者甚至不会分配到物理页面?看起来像是一个空闲线程只需要一个内存页的工作集。可能这仍然比它完成请求所需的(一些指针)要多得多,但相当容易管理。 - Dobes Vandermeer
你可以将堆栈配置得更小(例如,缩小4倍),但这只会稍微有所帮助。异步框架是解决这个问题的关键。在Java世界中,我很满意使用Netty。 - ivy

5
可能会有数千个客户端同时使用,但是具体有多少个“几千”是下一个问题。
一种常见的替代方法是使用选择器和非阻塞I/O,这些功能可以在java.nio包中找到。
最终你需要考虑的问题是是否有必要将服务器设置为集群配置,并在多台物理机器之间平衡负载。

3
为了在处理多个套接字时获得良好的性能,通常使用“select”方法,这是Unix API处理单线程多套接字应用程序所需的许多资源的方式。
这可以通过“java.nio”包完成,该包具有“Selector”类,该类基本上能够遍历所有打开的套接字,并在新数据可用时通知您。
您可以在单个“Selector”中注册所有打开的流,然后只需一个线程即可处理所有流。
您可以通过此处的教程获取其他信息。

我偶然发现了一个关于InputStream.available()方法的SO问题,这个方法我已经忘记了。您能告诉我使用Selector相比使用线程处理多个连接并使用available()来防止阻塞的优势是什么吗? - Bart van Heukelom
因为您避免在编程上重新实现一些涉及各种套接字的内容,而是将使用专门为此目的量身定制的东西 :) 主要优点是节省调试和实施时间。 - Jack
嗯,但在我这种情况下,实现起来将非常简单。我的协议是:1.读取指定消息长度的整数;2.一次性读取该数量的字节。 - Bart van Heukelom
在这种情况下,这是一个偏好问题。当然,如果你不了解nio或者不需要它,避免使用它可能会过度杀伤力,但是如果你计划拥有许多套接字,为什么不给通道和异步IO一个机会呢?请注意,您将需要关注中断线程以避免在数据未准备好时进入无限循环。 - Jack

3

LinuxJVM 使用一对一线程映射。这意味着每个 Java 线程都映射到一个本地操作系统线程。

因此,创建一千个或更多线程不是一个好主意,因为它会影响您的性能(上下文切换, 缓存 刷新/ 未命中, 同步 延迟等)。如果您少于一千个 CPU,这也没有任何意义。

为了同时为多个客户端提供服务,唯一的适当解决方案是使用异步I/O。有关详细信息,请参见此答案Java NIO

另请参阅:


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

2

尝试使用Netty

“一个请求一个线程”的模型是大多数Java应用服务器编写的方式。你的实现可以像它们一样扩展。


1
仅仅因为“大多数”人这样做,这是否意味着它是正确的或最好的方法?唯一确定答案的方法是使用两种方法编写代码并测试哪种方法在CPU和内存使用方面表现更好。由于Windows和Linux之间存在差异,您可能需要考虑在两个环境中进行测试以确保准确性。话虽如此,我认为每个请求一个线程不可扩展。 - Jay

1

现在的线程不像以前那么昂贵,因此“普通”的IO实现可以达到一定的效果。但是,如果您想要扩展到数千个或更多连接,那么值得考虑一些更复杂的解决方案。

Java.nio包通过提供套接字多路复用/非阻塞IO来解决这个问题,允许您将多个连接绑定到一个选择器上。然而,由于涉及到多线程和非阻塞方面,这种解决方案比简单的阻塞方法更难以正确实现。

如果您希望追求更高级的IO操作,我建议您看看市面上一些优秀的网络抽象库。从个人经验来看,我可以推荐Netty,它为您处理大部分繁琐的NIO操作。不过,它确实有一定的学习曲线,但一旦您习惯了基于事件的方法,它就非常强大。


1

如果您有兴趣利用现有容器的部署和管理,您可以考虑在Tomcat内创建一个新的协议处理程序。请参见this answer以获取相关问题的答案。

更新: Matthew Schmidt的此帖子声称Filip Hanik编写的Tomcat 6基于NIO的连接器实现了16,000个并发连接。

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


有趣,但我的应用程序目前没有使用容器。我也对自己使用NIO(如果必要的话)进行教育目的的工作感兴趣。 - Bart van Heukelom

1
我建议这更取决于服务器在处理消息时所做的其他工作。如果它相对较轻,则您的机器规格应该轻松处理成千上万个此类进程的连接。数万个进程可能是另一个问题,但您只需要在同一网络上使用两台机器来进行实际测试并得到明确的答案。

0
为什么要自己编写?你可以使用带有Servlet的Servlet容器、消息队列或ZeroMQ。

0

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

当然,我认为异步I/O会使它变得更好、更快,但只有在处理套接字和网络问题时才会帮助你。当您的线程因I/O而阻塞时,JVM会将其置于睡眠状态,并更换为另一个线程,直到阻塞的I/O返回。但这只会阻塞线程。您的处理器将继续运行并开始处理其他线程。因此,除了创建线程的时间外,您使用I/O的方式对模型的影响不大。 如果您不创建线程(使用池),则问题得到解决。


我确实使用了Executor,但由于每个线程基本上都执行while(!closed) read();,这并没有减少线程的数量,只是减少了创建线程的开销。 - Bart van Heukelom
这正是我的观点。当你调用read()时,你的线程将会阻塞直到有任务可执行。不管你有多少个线程,重要的是你有多少个可运行的线程,而被阻塞的线程并不计算在内。它们不会争夺处理器时间。因此,最终只有那些有任务可执行的线程才会竞争处理器时间。 - Plínio Pantaleão

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接