十大性能方面的错误

阅读数:4669 2016 年 7 月 18 日 19:16

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。

4. 同步通信。客户端和服务器之间的同步通信会带来时间的损耗,对于需要快速通信的系统来说,这会成为一个问题。Thompson 说,它的解决方案并不是购买更加昂贵和快速的硬件,而是使用异步通信。在这种场景下,客户端可以发送多个请求到服务器端,而不必等待它们之间的响应。采用这种方式需要改变客户端发送请求的方式,但这是值得的。

3. 文本编码。开发人员很多时候会选择使用文本编码格式实现链路上的数据传输,比如 JSON、XML 或 Base64,因为“这对人类是可读的”。但是 Thompson 指出在两个系统之间进行对话的时候,是没有人读这些数据的。借助这种方式,使用简单的文本编辑器就能很容易地进行调试,但是在将二进制数据与文本之间进行互相转换的时候,这会带来很高的 CPU 损耗。该问题的解决方案是使用能够理解二进制的更好的工具,Thompson 提到了 Wireshark

2. API 设计。按照 Thompson 的说法,有一些与性能相关的最负面影响是由 API 引起的。它使用如下的代码来阐述较差的代码签名:

public void startElement( String uri, String localName, String qName, Attributes atts) throws SAXException

描述:

在处理 XML 的时候,通常我们并不会使用这些值 [三个 String 以及属性的集合]。我们分配了很多的内容,但是却将其浪费并抛弃掉了。这会损耗电池的寿命,白白地浪费资源。我们需要使其更加简单一些。

他建议采用如下的签名,实现更加简单的方法:

public void characters( char[] ch, int start, int length) throws SAXException

有些人可能会抱怨后面的这个方法要比前一个更难用,Thompson 建议采用组合的方式,将其中一个用另一个封装起来,这样的话,能够给用户多一个选择。如果性能不是什么问题的话,可以采用第一种(使用 String),否则的话,第二个方案会更好一些。

Thompson 提到的第二个样例是字符串拆分:

public String[] split(String regex)

这个方法签名相关的性能问题包括:

  • 每次方法调用的时候,正则表达式都需要进行编译;
  • 需要实例化一个动态的结构,用来存储字符串中所包含的初始数量未知的 token;
  • 返回的结构是一个固定大小的数组,这就必须要将 token 收集到一个临时的结构中,然后再拷贝到数组里面;
  • 如果调用者想要对这些 token 进行一些操作的话,比如排序,需要将它们拷贝到另外一个结构之中。

更好的方案是使用 Iterable,它能够避免在内存中创建中间状态的 token 副本:

public Iterable split(String regex)

另外一种方案是允许调用者提供存储 token 的集合。如果调用者想要对 token 列表去重的话,应该传递一个 Set 进来,如果想得到有序列表的话,就需要传递一个 TreeMap 进来:

public void split( String regex, Collection dst)

1. 日志。 Thompson 所列的排名第一的性能问题是写日志所耗费的时间。他通过一个图表展现了当线程数增加的时候,日志操作所耗费的平均时间:

这个图显示了一个 100% 的顺序操作,不管使用多少线程来记录日志,所需的时间均呈线性增长。Thompson 说大多数已有的日志系统都可以得出这样一幅图表,“Logger 是系统中最大的瓶颈之一”。这个问题的解决方案是使用异步的 Logger。

另外,Logger 所记录的数据应该是结构化的数据,便于后续的工具进行读取和处理,而不应该是一堆 String。如果是记录重复的错误,他建议在错误第一次出现的时候进行记录,后续出现时只需对一个计数器进行递增,告知对应的错误出现了多少次即可。对于实时系统的调试,Thompson 建议使用代码编织(code weaver)的技术,如 Byte Buddy ,因为它能够避免编写和运行不必要的日志代码。

这个分享的讲义可以在线获取( PDF )。

关于作者

Abel Avram从 2008 年以来参与了很多 InfoQ 的编辑活动,热衷于编写移动领域、HTML、.NET、云计算、EA 以及其他话题的新闻报道。他是 《Domain-Driven Design Quickly》一书的合著者。

过去,他曾经担任多年的 Java 和.NET 软件工程师、遗留系统的项目 / 团队领导等职务。目前,他的职业是罗马尼亚蒂米什瓦拉技术大学计算机与自动化系的助教。

如果你有意提交新闻或教育方面的文章,可以通过 abel [at] infoq.com 联系到他。

查看英文原文: Top 10 Performance Mistakes

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论