- 网站越快,用户的黏性就越高;
- 网站越快,用户忠诚度更高;
- 网站越快,用户转化率越高。
简言之,速度是关键。
——《Web 性能权威指南》
显然,高性能意味着“快”。但对快的定义,在不同的系统中,标准是不一样的。为了获得快的体验,通常我们需要平衡成本和收益等方面制定优化方法。
如果说“快”的标准不好把握的话,但我们都对类似这样的典型论述有一致的结论,比如:
- 内存是快的
- U 盘是慢的
之所以对这些问题容易有统一的认知,是因为我们已经对需要度量的范围与标准有了较清晰的概念,我们不自觉地预设一些应该受到控制的变量,并将它们与其他某些事物做了对比。比如,在谈 U 盘慢时,往往指的是在同一台计算机上与硬盘、内存等其他存储方法作比较。
类似地,当我们谈论应用的性能时,也需要把应用所在的环境、处理的数据量等情况一同纳入考虑范围内。这些因素常常让速度的定义变得复杂。因此,在谈论高性能之前,我们首先需要对性能进行建模,确定合适的用于反映性能状况的指标,并据此制定明确在各项因素在各种情况下的性能目标。比如,在谈论 Web 应用程序的速度时,我们经常谈到 QPS、TPS 和响应时间等指标,而确定性能目标时,则往往要考虑数据量的多少、并发用户数目和网络带宽等情况。
在性能建模时,越细致、越全面的数据就越真实地反映出系统的状况,从而能为做出正确的决策提供更准确的依据。一般来说,网络、磁盘等 IO 设备、操作系统,以及应用程序运行时(如 CLR、JVM)往往都提供了丰富的性能数据。对于 Web 和云原生系统而言,设计良好的服务端框架(如 Kubernetes),以及对开发人员友好的浏览器(如 Chrome)等也都会提供相关的性能接口。
举例来说,如果考虑一个桌面应用程序,我们需要收集的指标有:
- 第一个界面出现的时间
- 第一个界面加载完毕的时间
- 某些典型事务处理时间
- 关闭窗口时的等待时间
- 从开始加载文件到能够编辑文件的时间
在制定性能目标时,我们要选用一些典型的用户机型进行模拟。在收集测试数据时,摆在面前的第一个问题是如何准确获得这么多种类时间?靠人肉掐秒表也是一种思路,不过还是显得太原始了一些。这时,我们需要在应用中添加一些性能埋点,并利用统一的工具收集这些数据(如操作系统提供的性能计数器)。
下面是利用 Windows 性能工具来查看 IO 操作在物理磁盘上执行的情况:
这些性能数据不但能够在制定科学的性能目标时提供帮助,更能够在系统的性能未达预期时,在第一时间展示出系统瓶颈所在。一段时间以来,当网站慢时,我总是能够通过 Chrome 开发人员工具提供的网络时间线和性能分析工具快速地发现问题所在,并且运用《高性能网站建设指南》等经典书籍中给出的切实的建议针对性地做出优化。
下面是一个 Chrome 浏览器中加载资源时的资源瀑布图:
有人说,所发现了问题时,就是解决了一半,对于一些容易解决的问题尤其如此。不过,一般来说,性能问题往往没那么容易解决——除非是程序的实现有明显的瑕疵(比如陷入了死循环)。相比于逻辑错误问题,解决性能问题一般需要具有一定经验的工程师才能胜任。
在优化应用程序的性能时,一般会由根据距离应用的远近,从外部到内部将优化分为这三个层次:
- 链路上的优化
- 结构性优化
- 应用内优化
链路上的优化指的是在用户操作发出之后、真正到达应用程序逻辑执行之前的过程期间执行的优化;结构性优化指的是对应用程序所处的环境、依赖的资源,以及应用本身的子系统间的关系等方面的优化;而应用内优化则指的是对应用内的实现方式(比如算法和数据结构)进行优化。
举例来说,下图是一个简单的 Web 系统的结构,用户的操作指令发出之后,需要经过互联网到达负载均衡服务器,最后到达两台应用服务器中执行运算逻辑,而应用依赖了读写分离的两个数据库。
在上述三种层次上,对系统的性能进行优化时,可以考虑以下措施:
- (链路上)使用 DNS prefetch 技术减少域名查找时间
- (链路上)增加负载均衡服务器的出口带宽
- (结构性)增加一个新的应用服务器
- (结构性)在应用服务器增加 CPU 核心数和内存
- (应用内)用多线程技术把操作数据库和访问磁盘的操作同步进行
- (应用内)消除应用加载数据时的 n+1 问题
一般来说,链路上和结构性方面的优化,对于使用各类编程平台实现的应用程序都一样适用。对于一个 Web 系统来说,HTTP 请求到达 Web 应用之前的所有优化,都属于这两类。时下,关于大流量、大并发系统的性能优化方法,人们已经讨论的很多了。不过,纵观那些优化方法,实际上也都可以从应用内的优化方法上看到类似的方法,可以认为这些优化思想大体是相通的。
从应用内部优化时,业务专家可以通过一些业务流程上的简化等方法来有效地改善应用程序的性能,例如,在某些情形下省略一些不必要的运算等。这类优化主要取决于业务规则的自身特点,难以统一地归纳出具体的经验。而在业务之外的优化方法,各种应用程序之间却是相通的。
通常,在应用内,要在业务流程之外进行性能优化,我们考虑以下方法:
并发与并行 也就是让程序同时做多件事,以及将一件事分为多个小块同时完成。 将按顺序执行(串行)的多个任务改为并行之后,处理时间从多个任务之和变成了多个任务中最慢的任务的时间。计算机系统中某个层次(如应用程序)的并行能力是由它的底层(如操作系统)的并发能力提供支持的。既然多核计算机早已普及,那么就没有理由不好好利用并行处理能力了。
异步 在计算机系统中,运算往往要比输入输出(IO)操作快得多,不少看起来很慢的系统经常是在空置地等待完成 IO 操作。异步化 IO 操作能让计算机不再等待 IO 完成,从而能最大化地利用系统的运算能力。
缓存 不论是运算,还是 IO 操作都是要耗费时间的。“利用空间换时间”经常是有效的优化方法:典型地,在运算斐波那契数列时,如果不是有通项公式,在不用缓存的情况下几乎不可能写出符合时间要求的算法。
精减与压缩 还是 IO 的问题。如果 IO 慢,就更不能让它执行不必要的操作了——比如多执行的 SQL、多加载的 JavaScript 文件等。经常,当数据量少了之后,由于需要处理的内容少了,还同时能够节省运算的时间。
技巧性优化 上面所述的几种方式,是在代码级别的“通用”方法。而面对具体的问题,经常会有一些技巧性优化方法。比如,采用更具针对性的数据结构,或已经证实更高效的算法,更利于运行时进行垃圾回收的代码风格等。
在上述这些方面的优化,在不同类型的应用程序里,有具体的表现形式;而在不同编程平台上,又可能有不同的实现。举例来说,在 Web 前端应用里,异步可表现为异步加载 JavaScript/CSS 文件和异步执行 Ajax;而在一个 Web 后端应用里,异步则表现为对文件、网络等操作的异步执行。如果具体到编程语言,Node.js 应用可能使用一个 nextTick
操作,而一个 ASP.NET 应用则使用一个 async
方法。需要参考其对应编程平台的优化经验进行实施。
最后,当我们掌握了优化性能的方法,也不意味着我们一定要用尽各种方法去优化我们编写的每一个应用程序。在编写代码过程中,优先选用更高性能的写法通常是有益的(例如,使用 HashSet 替换 List 来存储需要快速查找的集合)。另一方面,过早优化是所有开发人员都容易忽略的陷阱(例如,集合的元素并不多,又大量用于循环时用 HashSet 替换了 List)。几乎所有新的开发人员都听说过“字符串是不可变量,应该尽量使用 StringBuilder”、“反射的性能很糟糕,应该尽量避免使用”之类的经验之谈。单从字面来理解确实都是金玉良言,但作为一种优化手段,性能优化往往会要求应用程序遵守一些约束,这些约束有时会破坏代码的可读性,有时会改变编写代码的习惯,而这些往往意味着在更广的维度上给团队带来成本。
作者和其他三位 ThoughtWorks 同事一同翻译的《.NET 性能优化》一书已于近日出版,各大在线书店有售。本书详细解释了影响应用程序性能的 Windows、CLR 的内部结构,并为读者提供了衡量代码如何独立于外部因素执行优化的知识和工具。书中提供了大量的 C# 代码示例和技巧,将帮您最大限度地提高算法和应用的性能。
尽管成书于数年之前,书中所述的大多数经验仍适用于最新的 .NET 框架和运行时。它不光讲述适用于 .NET 平台的性能优化方法,还详细地讲解了性能度量的指标,以及普适性的性能优化的思路和原理。既授人以鱼,又授人以渔,是不可多得的上佳之作。
读者可以通过扫码、点击链接等方式在异步社区购买 《.NET 性能优化》时,在结算时输入优惠码 d4c43c-e 即可获得10元现金优惠。优惠购买链接:https://www.epubit.com/book/detail/921 :
也可通过亚马逊、京东和当当等网站自行购买。