繁体   English   中英

Tomcat Servlet性能:StringBuilder与直接写入

[英]Tomcat Servlet performance: StringBuilder vs. direct write

这是供Tomcat /网络专家使用的工具。 我会对其进行基准测试/ Wireshark,但这要求很高,也许有人会立即知道答案。

比较这两种生成servlet输出的方法,从用户的角度来看,这是最快的方法:

  1. 直接写入servlet输出流:

    for(int i = 0; i <10000; i ++){servletOutputStream.write(“ a”); / *有点延迟* /}

  2. 创建一个缓冲区并一圈写入

    for(int i = 0; i <10000; i ++){stringbuffer.append(“ a”); } servletOutputStream.write(stringBuffer.toString())

我可以想象方法1的优点是响应可以快速开始发送内容,而方法2中的发送则稍后开始。

另一方面,方法1可以生成更多/较小的TCP数据包,进而需要更长的时间才能完全传输?

问候

PS:请不要告诉我这是过早的优化。 在这种情况下,我有一个同时提供toStringwrite(Appendable a)方法的对象。 我只需要选择在这里使用哪个。 另外,从理论的角度以及关于Servlet的一般设计,我发现这非常有趣。

编辑:谢谢大家的答案。 但是看来我不清楚我的问题还是简化了我的例子。

我完全不担心不缓冲。 我知道发送队列中至少必须在一个地方缓冲。 可能在多个地方(Java,OS,硬件)。 我认为我真正的问题是: 这些缓冲区何时刷新?

因此,更清楚地说,假设我们的MTU为1000,并且连续数据包的发送是由硬件的缓冲区空中断触发的。 然后,在第一种情况下,它可能看起来像:

. packet( "a" ) //triggered by the first write( "a" ),
. packet( "aaaaaaa" ) // triggered by buffer-empty, sending the amount of "a"s which have been written in the meantime
. packet( "aaaa" ) // and so on
. packet( "aaaaaaaaaaa" )
...x1000 // or so in this example

对于第二种情况,发送开始时所有10000字节已经可用,因此结果将是:

. packet( "aaaa....a(x1000)" )
. packet( "aaaa....a(x1000)" )
...x10

即使对于较小的数据大小(小于MTU,可以说100个“ a”),创建输出的速度也要比发送时间快,结果可能看起来像:

. packet( "a" ) // first write
. packet( "aaaa...a(x99) ) // all remaining data available when buffer-empty interrupt.

当然,如果缓冲区的工作方式不同,那么所有这些都将是安静的。 例如,如果他们等待更多的数据发送或等待刷新发送所有信息……(但是这反过来也会在某些方面减慢发送速度)

所以这就是我所不知道的: tomcat中的这种缓冲到底是如何工作的 ,使用它的最佳策略是什么?

(而且我并不担心或期望获得更大的速度提升。我只是想知道事情的运行方式。)

我希望ServletOutputStream实际上是

    org.apache.tomcat.core.BufferedServletOutputStream 

(顾名思义)这是一个缓冲的流。 这将意味着最好直接将字符写入流中,而不是将它们组装在StringBufferStringBuilder并写入结果。 直接书写将避免字符的至少一个副本。

如果事实证明您的ServletOutputStream尚未被缓冲,则可以将其包装在BufferedOutputStream ,您将获得相同的结果。


假设现在您正在谈论流。 (刷新StringBuffer没有意义。)

这些缓冲区什么时候刷新?

当它们已满时,当您在流上调用flush或关闭流时。

...而使用它的最佳策略是什么?

通常,写入数据,完成后关闭文件。 除非有充分的理由,否则不要显式刷新。 如果要传递普通的HTTP响应,则很少。 (刷新可能会导致网络堆栈通过发送更多网络数据包来传输相同数量的信息。这可能会影响整体网络吞吐量。)

对于Servlet框架,我记得Servlet规范说,当请求/响应处理完成时, ServletOutputStream将自动刷新并关闭。 如果您没有包装ServletOutputStream ,那么甚至不需要关闭流。 (虽然没有害处。)

毫无疑问,由于多种原因,直接写入输出流会更快:

  1. 输出缓冲区是固定的
  2. 当它的全部输出缓冲区将被自动刷新(我要说,当发生这种情况不要紧,所以不用担心它)
  3. 输出缓冲区将被重用
  4. 您的StringBuilder可能会变得非常大,占用大量堆空间
  5. 您的StringBuilder会定期重新分配其空间,从而导致创建新对象,在各处复制数据等
  6. 所有这些内存活动将创建GC必须处理的“垃圾”

然而

我认为您的分析没有考虑到一个非常重要的因素:检测和从错误中恢复。

如果您的servlet正在执行一个半复杂的过程,则该过程随时可能失败。 如果在渲染一半输出后失败,则您将无法执行以下任何操作:

  1. 发出“错误” HTTP状态代码(例如500服务器错误)
  2. 将用户重定向到另一个页面(错误页面?)
  3. 在屏幕上显示漂亮的错误消息而不会破坏/中断页面

因此,即使手动缓冲的方法(基于StringBuilder )效率较低,但我相信它为您提供了极大的灵活性来处理错误。

这比其他任何事情都更具宗教性,但是您会发现许多Web应用程序程序员会说您的servlet根本不产生任何输出,并且应该将生成响应的任务委派给更适合该任务的另一个组件。 (例如JSP,Velocity,FreeMarker等)。

但是,如果您要着眼于原始速度来编写servlet,则一定要:直接写入输出流。 它将在微基准测试和负载下的整体速度方面提供最佳性能。

编辑2016-01-26

这些缓冲区何时被清空?

Servlet规范不保证ServletOutputStream是否已缓冲,但不使用缓冲区将是一个实际错误:一次仅发送一个字符的TCP数据包肯定会降低性能。

如果您绝对需要确保响应被缓冲,则必须使用自己的BufferedOutputStream ,因为servlet容器可以随时更改其实现,并且如上所述,不能保证为您缓冲响应。

Tomcat中的这种缓冲到底如何工作?

当前在Tomcat中实现的缓冲的工作方式与标准JDK类中的缓冲相同:当缓冲区填满时,将其刷新到较低的流,然后在调用后将字节余量保留在缓冲区中。

如果您在流上手动调用flush ,则将强制使用Transfer-Encoding: chunked ,这意味着将需要通过网络发送其他数据,因为没有Content-Length (除非您在手动设置之前设置一个)开始填充缓冲区)。 如果可以避免分块编码,则可以节省一些网络流量。 另外,如果客户端知道响应的Content-Length ,则他们在下载资源时可以显示准确的进度栏。 使用chunked编码,客户端在下载完所有数据之前永远不知道会有多少数据。

servletOutputStream包装在BufferedOutputStream (除非已经存在),而您不必担心类似这样的愚蠢的事情。

我肯定会用第一个。 Servlet输出流已缓冲,因此您不必担心发送速度太快。 另外,您每次都会在第二个字符串中分配一个新字符串,这可能会导致GC超时开销。 使用第一个,并在循环后调用flush。

它已经被缓冲,在某些情况下,它已写入ByteArrayOutputStream以便Tomcat可以在Content-Length标头之前。 不用担心

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM