Java服务器调优
随着成千上万的Java服务器运行在企业线上环境,Java已经成为构建线上系统的语言之一。如果希望我们的机器表现出可接受的性能,那么就需要对它们进行定期调优。这篇文章详细阐述了Java服务器调优的各项技术。
衡量性能
为了让我们的调优有意义,我们需要某种方法来衡量性能是否提高。让我们记住两个重要的性能指标:延迟和吞吐量。
-
延迟(Latency) 衡量的是端到端的某个操作的处理时间。在分布式环境中我们通常用发送请求和接收到响应整个来回的时间来衡量延迟。在那些场景,延迟是从客户端机器开始衡量的,并且也包括网络传输开销
-
吞吐量(Throughput)衡量的是服务器在某段时间间隔(比如一秒)内处理的消息数。吞吐量可以用下面的公式来计算:
吞吐量 = 请求数量 / 完成这些请求花的时间
理想情况下我们想要获得最大的吞吐量同时获得最小的延迟。然而鱼和熊掌不可兼得,一个设计良好的服务器系统中必然存在这二者的折衷(tradeoff)。 例如下图所示,如果你想要获得更大的吞吐量,那么你必须增大并发度,但是相应地会导致平均延迟增加。通常,你必须实现最大吞吐量的同时保持延迟在一个可接 受范围内。例如,你可能会选择某个范围的吞吐量,并且保证延迟低于10ms。
下图捕捉了服务器的行为。正如图中所示,服务器性能是通过衡量延迟和吞吐量与并发度的关系来实现的。
图中,“ideal path”是理想中的情况。实际上,大多数服务器在并发度过高情况下将崩溃或者出现性能下降。
服务器调优
在调优环节,我们试着深入服务器内部理解它的行为,并且要么证明服务器按预期在运行,要么寻找途径提高服务器性能。性能调优的目标有三个:
-
增大吞吐量 - 最大化系统单位时间内能够处理的消息数
-
减小延迟 - 确保我们满足响应时间SLAs(Service Level Agreement)
-
发现并修复泄露(比如内存、文件、线程、连接泄露)
正如之前所说,我们的目标是获得最大吞吐量同时保持延迟在可接受范围内。如果你还没有进行过任何调优,最好从调优吞吐量开始。那么我们就先从吞吐量调优开始吧。
吞吐量调优
在我们开始前,我们先弄懂什么会限制性能。把服务器设想为水管系统是很有帮助的;增大并发度就好比增大往水管系统中灌入的水量。增加更多的水并不能保证管道中将有更多的水流动;水流由管道系统中最慢的部分决定。
类似地,应用性能由系统最稀缺的资源决定。计算机系统有很多种资源:CPU、内存、磁盘和网络IO。任何其中一种资源都可能限制我们系统的性能。
当尝试从外部衡量系统性能时,我们可以逐渐增大负载直到一种资源消耗殆尽。那将有助于发现限制性资源,然后我们要么分配更多的那种类型资源,要么修改系统以更节省地使用那种资源。
下一步,我们搭建起系统并且对系统施加相当量的工作负载(更多细节,可以参考我的博客如何衡量服务器性能)。当负载在运行时,下一步我们将找出各种类型资源的利用程度。
基于最稀缺资源维度,我们将服务器性能下降归结为以下三类:
-
CPU紧缺型 - 服务器阻塞等待CPU
-
IO紧缺型 - 服务器因为磁盘或者网络带宽而阻塞
-
延迟密集型 - 服务器等待某些事件发生(比如等待数据从磁盘传输到网络)
下面让我们来研究下某示例系统中那种资源最稀缺。我们先在Unix/Linux系统中运行top命令:
工程师常犯的一个错误是在还没有真正确定是否真正属于CPU紧缺型的场景时,就开始调优CPU。虽然上图显示的CPU利用率很低,当前机器可能正忙于做IO操作(比如读磁盘、写数据到网络)。Load Average是衡量机器是否满载的更好指标。
Load Average表示在OS调度队列中等待的进程数。不像CPU,Load Average将因为任何一种资源的紧缺而增大(比如CPU、网络、磁盘、内存...)。更多细节请参考理解Linux的Load Average
我们可以利用下面的Load Average值来确定机器是否处于高负载状态:
-
如果Load Average < CPU核数,那么机器就非满载
-
如果Load Average == CPU核数,那么机器资源就被充分利用了
-
如果Load Average >= 4*CPU核数,那么机器就处于过载状态
-
如果Load Average >= ~40*CPU核数,那么机器就处于不可用状态
如果机器非满载,一般表示它处于空闲状态。有好几个因素可能引起这种情况,可以通过下面的方法来校正:
-
试着增大负载(通常表示增大并发度)。例如,用一两个客户端测试服务器通常不会让服务器满载;通常需要成百上千个客户端同时测试服务器才可能让其满载
-
剖析锁状况(如果大部分线程都在等待锁或者发生死锁,那么吞吐量也会下降)。尽可能发现并修复那些你可以找到的情况,并尽可能使用非阻塞数据结构。我们将在“延迟调优”小节更详细讨论锁剖析
-
调整线程池大小(有时候系统配置过少的线程数,可能会引起系统运行缓慢)。例如我发现增加Tomcat线程池的大小通常会增大吞吐量
-
确保网络未饱和(如果网络处于饱和状态,那么能够到达机器的有效负载就可能非常小了)。在大多数机器中你可以用Linux提供的iftop命令来检查这种情况
另一方面如果机器满载,那么很明显它在做什么事情,但你仍然需要确保它在做一些有意义的事情!
-
确保你的应用是运行在同一台机器上的唯一一个重量级进程。你应该不想看到其他应用使你的测试结果扭曲
-
如果排除了上面这种情况,然后用top命令检查CPU利用率。如果CPU利用率很高,使用一个profiler工具来看CPU Profile信息
例如上图是从JProfiler截取过来的CPU Profile信息。它显示了服务器中Java方法执行树,标明了每个方法的执行时间。检查最耗CPU时间的方法并确保它们在做有意义的事情。(注意:这 篇文章中我们使用JProfiler,也可以使用其他工具如Youkit Profiler和JDK 1.7+提供的Java Mission Control)
-
计算应用花在GC上的时间。如果GC时间占比超过10%那么你就需要JVM GC调优了(你可以使用类似VisualVM GC插件这样的工具来完成,参考我的博客我是否该进行GC调优?)。如果GC存在问题,Profiler的allocation view可以帮助你发现内存分配热点并且修复它们。这里有个演讲是GC调优方面不错的参考资源:talk by Kirk pepperdine
-
再下一步是检查网络和磁盘的IO状况(假设你的程序写磁盘)下面的截图显示了从JProfiler截取的IO Profile信息。验证高IO节点确实出现在预期位置。然后对数据库访问做同样操作
-
最后检查下你的机器是否在进行内存分页(比如 Check Swap Usage in Linux)。一般来说,你需要避免内存切换因为那将大幅度拖慢服务器性能。如果存在内存切换,你要么修改服务器以需要更少的内存,要么为机器增加物理内存
如果你把上面所有步骤都尝试遍了仍然没有达到预期性能,很可能是服务器达到了它自身的性能瓶颈。你要么通过加服务器来Scale Up,要么重新设计服务器架构。
延迟调优
高延迟是由费时请求处理操作引起的。磁盘访问、网络访问和锁是引起处理操作时间长的几大罪魁祸首。
当进行延迟调优时,我们首先要检查网络和磁盘状况,正如我们上一节讨论的,并且发现并减少IO操作数量。下面是一些可能有用的校正方法:
-
避免不必要的IO操作。尽可能消灭它们或者用Cache替代
-
尝试批量IO,批量IO将比多个单次IO节省开销
-
如果你能提前猜到所需要的数据,那么可以尝试预加载(或者预取)数据
现在让我们来看一个JVM线程视图。下图显示了线程的数目和状态随时间变化情况。红色区域表示很多线程阻塞在锁上。
如果从线程视图发现许多线程处于等待状态,那么可以在展开“Monitor and Locks”视图找出引起阻塞的线程:
上面的截图显示了哪一段代码阻塞了最长时间和那一段代码在这段时间内持有锁。以下是两条总原则:
-
尽量避免synchronzied语句块和锁。你通常可以使用性能表现更良好的java.util.concurrent包中的并发数据结构
-
当你不得不使用锁或者写synchronized语句块时,尽可能早的释放锁。当持有锁时,尽量减少耗时操作,比如IO。此外在Lock或者synchronized语句块中尽量避免再获取其他锁
再下一步检查客户端与服务器之间的网络行为。你可以通过类似ping和iftop之类的命令来操作,但你最好咨询网络管理员获取详细的网络行为信息。
最后一个可选项是引入更多的服务器从而减少每个服务器的并发度进而减小延迟。一个极端的例子是运行两份服务器实例拷贝然后使用Jeff Dean在他的演讲Taming Latency Variability中提到的第一份结果。此外,如果你想要获得非常低的延迟,考虑使用类似 LMX disruptor这样的工具。
性能调优清单
在文中我们讨论了吞吐量调优和延迟调优。或许你现在会感叹:Java服务器调优是一项技巧活,文中所提到的也只是冰山一角。下面是性能调优步骤总清单。它或许不会告诉你有关调优的一切,但它可能帮助你避开很多陷阱:
-
检查机器的Load Average。如果它大于4*CPU核数,那么机器处于过载状态,你可以直接跳到CPU调优部分。
-
你是否给系统施加了足够的负载?通过增加线程数来模拟更多的客户端操作。如果那样做提高了吞吐量那么继续增大负载知道达到最大吞吐量。
-
线程是否处于空闲状态?如果你有太多的锁或者太少线程,系统可能提供不了足够的吞吐量。使用一个Profiler工具查看锁状态并且尝试移除锁。尽管一些锁难以避免,但大部分情况是没必要的。
-
尝试增加线程池中的线程数,检查那样做是否能增加吞吐量。
-
现在检查CPU热点代码
-
查看CPU热点代码的树视图并确保热点代码出现在预期位置。例如一个XML解析器预期会消耗很多CPU。但如果你发现非预期行为消耗了过多CPU,那么就需要修复它了。
-
检查内存和GC,如果GC吞吐量小于90%,那么就需要进行GC调优。如果内存不断切换分页,那么增大内存并观察是否有帮助。
-
观察DB Profiles,确保最高负载部分出现在预期位置。
-
观察网络IO Profiles。找到热点,确保大多数写都在你的预期之中。
-
确保底层资源如网络和Disk Mounting就绪
Java服务器调优非常具有技巧性,但相应的回报也很丰厚。有时候它更像是艺术而非工程,但根据我提供的步骤应该能让你走得更远。