Java和队列:多线程I/O的饱和问题

3
这个问题与Java的最新版本有关。
30个生产者线程将字符串推送到抽象队列。一个写者线程从相同的队列中弹出并将字符串写入驻留在5400 rpm HDD RAID阵列上的文件中。数据以大约111 MBps的速率推送,以大约80MBps的速率弹出/写入。该程序存在5600秒,足够让大约176 GB的数据在队列中积累。另一方面,我限制使用总共64GB的主内存。
我的问题是:我应该使用哪种类型的队列?
到目前为止我尝试了以下方法。
1)ArrayBlockingQueue。有界队列的问题是,无论数组的初始大小如何,一旦填满就会出现存活问题。实际上,在程序启动几秒钟后,top报告只有单个活动线程。分析表明,平均而言,生产者线程大部分时间都在等待队列释放。这不管我是否使用公平访问策略(构造函数中的第二个参数设置为true)。
2)ConcurrentLinkedQueue。就活力而言,这个无界队列表现更好。直到我耗尽64GB的内存,大约七百秒后,所有三十个生产者线程都是活跃的。然而,在超过限制之后,情况变得非常缓慢。我推测这是由于分页问题造成的,尽管我没有进行任何实验来证明这一点。
我预见到了两种解决方法。
1)购买固态硬盘。希望I/O速率的增加会有所帮助。
2)在写入文件之前压缩输出流。
是否有其他选择?我在上述队列构建/使用方式上漏掉了什么?是否有更聪明的使用它们的方法?Java并发实践书中提出了许多饱和策略(第8.3.3节),以防有界队列填满得比可以用尽的速度快,但不幸的是,在我的情况下,没有一个(中断,调用方运行和两个丢弃策略)适用。

正如您所意识到的那样,磁盘是您的瓶颈,无论您如何使用软件,您必须围绕磁盘的性能进行工作。这意味着压缩数据和/或购买更快的磁盘。请注意,您可以在约100美元左右购买到400 MB/s的固态硬盘。 - Peter Lawrey
1
如果你的生产者比消费者生产得更快,那么你必须让你的消费者变得更快或者让你的生产者变慢。 - Raedwald
你似乎很惊讶,试图在一台64GB的电脑上存储176GB的对象会使你的程序变慢。 - Raedwald
5个回答

3
寻找瓶颈。你的生产大于消费,一个有界队列是有意义的,因为你不想耗尽内存。
试着让你的消费者更快。剖析和寻找最耗时间的地方。由于你要写入磁盘,以下是一些思路:
  • 你可以使用NIO解决你的问题吗?(也许是FileChannel#transferTo())
  • 仅在需要时刷新。
  • 如果你有足够的CPU储备,可以压缩流吗?(如你已经提到的)
  • 优化您的磁盘速度(RAID缓存等)
  • 更快的硬盘
正如@Flavio所说,对于生产者-消费者模式,我认为目前没有问题,它应该是现在这样。最终,最慢的一方控制速度。

使用有限队列会有效地降低生产者的速度,直到它们的总速度与消费者的速度相匹配。 - Raedwald
我接受这个答案,因为它提到了NIO,这是我之前不知道的。谢谢。 - user2650947

2

我看不出问题所在。在生产者和消费者的情况下,系统总是以较慢一方的速度运行。如果生产者比消费者更快,在队列填满时会被降到消费者的速度。

如果您的限制是不能减慢生产者的速度,那么您就必须找到一种加速消费者的方法。对消费者进行分析(不要过于繁琐,一些 System.nanoTime() 的调用通常足够了),检查它花费大部分时间的地方,并从那里开始优化。如果您遇到 CPU 瓶颈,可以改进算法、添加更多线程等。如果您遇到磁盘瓶颈,尝试写入更少的数据(压缩是个好主意),获取更快的磁盘,使用两个而不是一个磁盘来写入...


1
根据Java "队列实现",其他一些类也可能适合您:
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • DelayQueue
  • SynchronousQueue
  • LinkedTransferQueue
  • TransferQueue

我不知道这些类的性能或内存使用情况,但您可以自己尝试。

希望这能对您有所帮助。


1

为什么你有30个生产者。这个数量是由问题域固定的,还是只是你随意选择的?如果是后者,你应该减少生产者的数量,直到它们的总产出率仅比消耗率高一点点,并使用阻塞队列(正如其他人建议的那样)。这样,你将保持消费者忙碌,这是性能限制的部分,同时最小化对其他资源(内存、线程)的使用。


1
你只有两种出路:让供应商变慢或让消费者更快。通过使用有界队列,可以采用许多方法使生产者变慢。要使消费者更快,请尝试使用https://www.google.ru/search?q=java+memory-mapped+file。请查看https://github.com/peter-lawrey/Java-Chronicle
另一种方法是将写入线程从准备字符串的工作中解放出来。让生产线程发出准备好的缓冲区,而不是字符串。使用有限数量的缓冲区,例如2*threadnumber=60。在开始时分配所有缓冲区,然后重复使用它们。使用一个队列来存储空缓冲区。生产线程从该队列中取一个缓冲区,填充并放入写入队列。写入线程从写入队列中取出缓冲区,写入磁盘并将其放回空缓冲区队列。
另一种方法是使用异步I/O。生产者自己启动写操作,而无需特殊的写线程。完成处理程序将使用的缓冲区返回到空缓冲区队列中。

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