问题

在这些tomcat服务中,如果在很短的时间剧增的流量,会导致这些机子变成CPU饥饿(cpu-starved),并且服务会没响应。会导致这些服务的客户端体验很差,如读超时和连接超时。特别如果将读超时设置得非常高,会导致特别差的体验,客户端会等到好久好久。在SOA架构中,客户端的客户端也会请求超时,从而导致雪崩效应(ripple effect),以至于整个应用的其他服务也一起都变慢或不可用,最后全部服务变慢或不可用。在正常情况,机子都是有大量的CPU资源,服务也不是CPU密集型(cpu intensive)。所以,为什么会导致上述那些异常情况发发生?
在测试环境模拟流量激增时,发现CPU不足(cpu starvation)的原因是tomcat配置不当。当突然增加流量,大量tomcat线程也在繁忙。系统CPU有个巨大的跳跃,是当大部分CPU时间都在处理上下文切换,就没有线程可以做任何有意义的工作时。

解决方法

为了了解为什么tomcat的线程会繁忙,我们需要了解tomcat的线程模型。

高层次的描述Tomcat Http Connectotr的线程模型

Tomcat有一个acceptor线程来接收连接(这里就涉及到对网络的熟悉,熟悉就知道连接不仅仅是socket的表面,更是tcp的三次握手过程)。另外还有线程池来做实际的工作。于是一个请求的过程是:

  1. OS和客户端建立连接的TCP握手。这取决于OS的实现,可能是有一个队列来保存这些连接或多个队列来保存。在多队列的情况,一条队列保存未完成的连接,这些连接都是还没完成三次握手的。一旦完成握手,连接就会被移到保存完成连接的队列,应用就会消费这队列里的连接。“acceptCount”这tomcat参数就用于控制这些队列的长度。(应该是包括未完成握手和完成握手的连接数)
  2. tomcat的acceptor线程接收连接,这些队列都是来自于已完成握手的队列。
  3. 检查工作线程池是否有空闲的线程,如果没有且活动线程数小于maxThreads,则会创建工作线程,否则等待空闲线程。
  4. 一旦有空闲工作线程,acceptor线程就会将连接交给工作线程后,然后继续监听新的连接。
  5. 工作线程做的就是实际的工作,如从连接读取输入,处理请求,然后发送响应给客户端。如果连接不是keep alive则会关闭连接,然后将自己放回线程池。如果是keep aliave连接,继续等待该连接读取输入。如果数据一直没到,那么keep alive情况,会有个keepAliveTimeout,超过该时间,则会关闭连接,然后将自己放回线程池。

考虑这种情况,tomcat的maxThreads和acceptCount设置都很大,突增的流量会填满OS的队列和让tomcat的所有线程都变得繁忙。当更多的请求发送到这台机子,从而超过系统所能处理的数量时,这种请求的“排队”是不可避免的,并会导致繁忙线程的增加,最终导致CPU不足(cpu starvation)。因此,解决方法的关键是避免多个点(OS和tomcat线程)上有太多排队的请求,并在应用程序达到最大容量时快速失败(返回http状态503)。以下是实际操作的一个推荐:

当达到系统容量,应快速失败

预估在峰值负载时繁忙的线程数。如果服务器平均5ms内对请求作出响应,那么单个线程每秒则可处理200个请求(rps)。如果机子是4核CPU,则可以达到800rps。假设4个请求并行发送到机子(假设机子有4核),这会让4个线程繁忙5ms,所以下个5ms,4个或更多的请求让4个线程繁忙。随后的请求会选取一个空闲线程。所以理论上,在800rps时,平均不应该有超过8个线程处理繁忙状态。但实际当中,会有些不同,是因为系统所有资源都是共享的。因此,应该对系统能够维持的总吞吐量进行实验,并计算繁忙线程的期望数量。这将为维持峰值负载所需的线程数量提供一个基线。为了提供一些缓冲区,需要将线程数增加三倍以上,达到30个。这个缓冲区是任意的,如果需要还可以进一步调优。在我们的实验中,我们使用了略多于3倍的缓冲区,效果很好。
跟踪内存中运行中并发请求的数量,并将其用于快速失败。例如,当并发请求的数量接近刚刚预估的繁忙线程数据(8个),则返回一个http状态码503。这将防止太多的工作线程变得繁忙,因为一旦达到峰值吞吐了,任何变得活跃的额外线程都将执行非常轻量级的工作,即返回503。

设置操作系统参数

acceptCount参数用于tomcat表示最长队列,这队列是操作系统级别,用于处理还未完成tcp握手的操作(具体取决于OS)。这是一个很重要的调优参数,否则在建立连接时会出现连接不上,活着导致OS队列中的连接过度排队,从而导致读超时。当然每个OS处理正在握手和完成握手的连接细节是不同,可能会时只有一个连接队列,或多个连接队列用于区分存放未完成握手和完成握手的连接(请阅读相关的文档来获取这些细节)。所以,有一个很好的方法调优这个acceptCount参数,那就是从很小的值开始测试,逐步增大,增达到没有连接错误即可。
太大的acceptCount值意味OS层面可以接收更多的请求,但是,如果rps大于该机子能够处理的能力,所有工作线程会变得繁忙,于是aceeptor线程会等待,直至有worker线程空闲。更多的请求将继续堆积在OS队列中,因为只有当工作线程可用时,acceptor线程才可以使用它们。在最糟糕的情况,这些请求还在OS队列时就已经超时了,但是tomcat的acceptor线程依然会去获取它来交给工作线程处理。这种是完全浪费资源,而客户端也没收到任何响应。
如果acceptCount设置太小,则会在很高的rps时无法有足够的OS空间来接收连接,这样客户端就会手连接超时的错误(connect time out error),实际吞吐会低于服务器能够承载的。

因此可以实验从很小的值如10开始,然后逐步增加acceptCount的值,直至没有连接错误出现。

当完成上面两个改变后,就算最差的情况,所有工作线程都很繁忙,但机子不会cpu不足,依然有能力做更多的工作(最大吞吐)。

其他考虑

如上所述,每个连接最终都被tomcat的一个工作线程处理。假如keep alive被打开,工作线程就会继续监听该连接,从而不会变成空闲而返回到线程池中。所以,如果客户端不够智能从而不会关闭连接,那么服务端很快就会用完它的线程。如果keep alive被打开,那么必须通过记住这种情况,来调节服务器群的大小。
或者,如果keep alive是关闭的,那么就不必担心使用工作线程处理非活动连接的问题。但是,在这种情况,每个调用就要付出打开和关闭连接的代价。此外,这还会创建很多TIME_WAIT状态的socket,这样会给服务器造成压力。

最好根据应用程序的用力进行选择,并通过运行实验来测试性能。

备注

“连接”这两个字做于名词时,实际指的是socket。而当用于动词时,其实就是TCP三次握手。
线程池里的线程在本文,有时称为空闲线程或工作线程,或空闲工作线程。


电脑杂技集团
208 声望32 粉丝

这家伙好像很懂计算机~