Martin Thompson是LMAX的联合创始人,在QCon圣保罗2016上做过关于性能的keynote演讲。他初计划的演讲题目为“关于性能的神话与传说”,不过Thompson后来将演讲命名为“十大性能错误”,因为“我们都会犯错误,而且很容易会出现错误”。
  下面列出了他在生产环境下所见到的十大性能错误,并且还包含了如何避免的建议。
  10.不进行升级
  很多人抱怨他们的系统不够快,并通过编写更好的算法和数据结构来寻求帮助,Thompson认为实际上“他们所需的仅仅是进行升级”。升级操作系统、JVM、CLR等等。不进行升级的常见借口是“在新版本中可能会有bug。”
  为了避免这种状况,可以进行定期的持续集成和测试,这应该是开发流程的基础组成部分。Thompson以一个实时系统进行了例证,开发人员针对新版本的数据库进行了测试,在所有的测试通过之后,他们将其发布到了生产环境之中。
  9.重复性的工作
  Thompson讲述了某个系统的故事,这个系统是用来提供Web页面的,它非常缓慢,开发人员初认为是数据库的问题并试图在这方面进行调优。但是当他在系统上运行profiler时,发现在一个循环中,ORM被调用了7,000次,这才是页面加载缓慢的罪魁祸首。当这个循环的问题修复之后,系统的响应变得完全正常。这里学到的经验是“对系统进行度量。如果系统是一个黑盒的话,你无法说明时间都耗费在了哪里。”
  8. 加载性能依赖于数据
  Thompson展现了一个基准测试结果,它会执行一项操作,该操作会对内存(RAM)中1GB数组的所有long型进行求和。这里所耗费的时间取决于内存是如何访问的,如下面的表格所示:

  这个基准测试的结果显示并非所有的内存操作都是等价的,我们需要关注它是如何进行处理的。Thompson认为非常重要的一点在于了解各种数据结构的性能,他指出对于2GB以上的场景,Java的HashMap要比.NET的Dictionary慢十倍以上。他还补充说,也有一些场景.NET要比Java慢得多。
  7. 分配的内存太多
  尽管在很多场景中,内存分配几乎是没有什么成本的,但是它们的回收却并非如此,因为在面对大量的数据集时,垃圾收集器需要更多的时间。当分配大量的数据时,缓存会被填满,较旧的数据会被舍弃,使得在数据操作上的效率变为90ns/op而不是7ns/op,这里变慢了不止一个数量级。
  6. 采用并行
  尽管对于特定的算法来说,采用并行很有吸引力,但是它也有一些局限性和相关的开销。Thompson引用了“可扩展性!但是其COST如何?”这篇论文,论文的作者通过引入COST(胜过单线程的配置,Configuration that Outperforms a Single Thread)对比了并行系统以及单线程的系统,COST的定义如下:
  在特定的平台中,特定问题的COST指的是优于单线程方案所需的硬件配置。COST将系统的扩展性与系统所引入的开销进行了权衡,并指明了系统实际所能取得的性能,它们可能并没有带来实际的收益,却增加了并行所引入了开销。
  作者分析了各种数据并行系统的测量结果,并得出如下的结论:“很多的系统要么具有非常高的COST,通常会需要上百个核心,要么针对他们所报告的配置,其性能要比单线程方案更差。”
  在这个话题中,Thompson指出,并行任务会有相关的通信和同步开销,并且有些活动本质上要求是串行的,不能实现并行。按照Amdahl定律,如果系统中有5%的活动需要串行,那么不管使用了多少个处理器,系统的速度提升多只能达到20倍。
  Thompson还提到了Neil J. Gunther在1993年所提出的通用可扩展性定律(Universal Scalability Law,PDF),该定律指出在并行非共享系统(shared-nothing)中甚至会存在更多的局限性,当所使用的处理器数量达到一定程度后,速度会出现下降,这取决于并发、竞争以及同步的水平。(更多的细节可以参考如何量化可扩展性这个页面。)按照上述两个规律所总结的速度与处理器数量之间的关系如下图所示:

  Thompson指出通过USL能够看到性能的下降,这要归因于并行系统中组件之间进行通信所消耗的成本:“在系统中,所投入资源越多,通信路径也会随之增多,这会使算法的效率降低。”
  Thompson补充说,在构建并行系统时,主要的建议是避免共享可变(mutable)的状态,因为“它非常难以进行判断……终你会遇到很多的bug”。推荐的方式是要么采用非共享架构,要么针对特定的一块数据,只使用一个写入器。
  对这个性能问题,他的终建议:如果你想提升算法的速度的话,在尝试并行方案之前,先设法提升单线程版本的性能,因为并行方案实在是太难了。
  5. 不理解TCP
  针对这个话题,Thompson认为很多在考虑微服务架构的人对TCP并没有充分的理解。在特定的场景中,有可能会遇到延迟的ACK,它会限制链路上所发送的数据包,每秒钟只会有2-5个数据包。这是因为TCP两个算法所引起的死锁:Nagle以及TCP Delayed Acknowledgement。在200-500ms的超时之后,会打破这个死锁,但是微服务之间的通信却会分别受到影响。推荐的方案是使用TCP_NODELAY,它会禁用Nagle的算法,多个更小的包可以依次发送。按照Thompson的说法,其中的差别在5到500 req/sec。