104-性能工程高手课

开篇词 | 代码不要眼前的苟且，而要效率的提升和性能的优化

你好，我是庄振运。万分高兴能有机会和你分享我的知识和经验。

最近十年，我一直在美国硅谷工作，也非常关注中国的互联网发展，经常和国内同行交流。我曾经在 QCon 做过两次分享。感谢参会同行的青睐，两次都被评为“明星讲师”。

2019 年初，极客时间的编辑找到我，希望我能用专栏的形式作更多的分享。我确实也想总结一下这些年的知识、体验和感悟，于是就写了这个专栏。

得性能者得天下

过去的三十年，我们见证了互联网的蓬勃发展和风云变幻。虽然互联网公司潮起潮落，提供的服务也日新月异，但是互联网界一个永恒的追求就是性能和效率。

程序员所写代码的性能直接决定了互联网服务的质量，也就决定了客户的去留。同时，互联网服务终归要运行在各种服务器上，公司需要部署数据中心和网络等容量，这些容量的效率直接影响了公司的运营成本。

所以，程序性能的优化和容量效率的提升也必然是每个互联网从业人员和公司的重要工作。

回首过去的学习、研究和工作经历，蓦然发现我已经专注于性能优化和容量效率这个领域将近二十年了。所涉及的范围也很广泛，从软件到硬件，从前端到后端，从服务器到数据中心，几乎涵盖了计算机和互联网的方方面面。平时，我也经常通过发表论文和参加会议的方式来总结和分享所学的知识。

这些年的从业经历也让我有机会对这一行业有些自己的观察、体会和思考。

首先，我注意到性能优化和容量效率的工作在 IT 界越来越重要。

当今互联网服务的特点是大规模（大数据、用户多等）和高要求（低延迟、高吞吐等）。这样的特点也就要求服务的高性能和容量的高效率。

不夸张地说，当今互联网，得性能者得天下。对 IT 公司而言，提高了服务性能和容量效率也就降低了公司的运营成本，增强了公司的竞争力。

第二，性能和容量效率和每个 IT 从业人员息息相关。

每个 IT 从业人员，尤其是程序员和运维，都需要关心服务性能和容量效率，这些知识会为自己的职业发展锦上添花，甚至是工作中不可或缺的。 性能和容量效率的知识对于每个程序员、运维、测试人员和管理人员都会有帮助。

具备了这些知识，程序员在开发软件时会写出高性能的代码；运维人员会懂得如何监测和提高系统的性能；软件测试人员会通览软件测试的分类和方法；管理人员可以了解如何进行容量管理，提升服务效率并降低运营成本，等等。

第三，性能优化和容量效率这一领域的工作是“越老越吃香”。

在互联网行业工作，很多人担心的就是年龄问题。性能和容量领域的工作特点是需要多方面的知识和技能，以及实际的经验积累。这种学习和积累需要相当长的时间，不太可能一蹴而就。

我在很多硅谷公司工作过，周围的很多同事都是年龄比较大的。他们往往是技术牛人，是挑大梁的角色。公司为什么会重用这些“大龄员工”呢？就是因为他们能够帮助公司提升业务性能和容量效率，节省运营成本。随着公司业务规模的扩展，节省下来的成本也越来越大，公司也越来越离不开他们。

第四，这方面的工作要求比较特殊——需要广泛的知识面和软技能。

除了软硬件结合的知识，以及理论联系实践以外，性能和容量效率的工作还需要各种软技能，从而和其他员工、其他团队有效地进行沟通和合作。 我用一张思维导图大体表示了这些知识和能力。

最后，这一领域和职业于总体而言缺少合适的介绍和相关资料。

比如性能工程这个领域，虽然各种“性能测试”和“性能调优”相关的知识有很多，比如 JVM 调优、操作系统调优等，但是系统地介绍“性能工程”的资料很少。“容量效率和管理”方面，更是很少见了。就连“性能和容量工程师”这个职业也很少人讲。

专栏设计

性能问题通常是复杂的，性能工程师就像“医生”一样，需要懂得多方面的知识才能为“病患”确诊“病因”。

医生遇到病人，会“望闻问切”，利用 X 光等手段做各种分析。根据病人的表象和分析的数据，医生会做出诊断，确定是什么病。然后会开药方或者给予治疗。病人服药或者接受治疗后，会再次进行复检，来确定治疗效果。

性能工程师对待计算机和互联网的性能问题也是如此，会观察各种参数，甚至进行主动的性能测试。根据各种参数和性能测试的结果，可以做出分析，并最终确定性能问题的根因。这之后进行性能优化来消除对应的性能问题。采取优化后，还需要重新测试来验证问题是否真正解决，亦或是另有他因，从而需要重新分析。

我用下面这张图片来类比这两种场景。

第一个场景是医生诊断病情和治疗病人。第二个场景是性能工程师分析问题并且优化性能。你可以直观地看出两种场景中的每一步的相似之处。在专栏中，我也是根据这样现实中解决问题的思路（问题→测试→分析→优化→实践）来为你讲解的。

为了帮助你循序渐进地了解并掌握性能和容量工程相关知识，我将这个专栏的内容设置为八个模块，共 36 讲核心内容。

从今天开始，让我用 36 讲的课程，帮助你在性能和容量效率领域迈出坚实的 36 大步。学习完这个专栏之后，我相信你一定能对这一领域有更多、更广和更深的了解。

书山有路，勤劳为径；学海无涯，辛苦作舟。希望这个专栏可以成为这样的一条捷径和一叶轻舟，我们一起整装出发，扬帆起航。

有道是“知音难觅”。王勃在《滕王阁序》里也说过：“杨意不逢，抚凌云而自惜；钟期既遇，奏流水以何惭？”很荣幸能够和你在此相遇相知，一起学习交流，也欢迎你给我留言，说说你对性能优化和容量效率的看法和疑问。

01 | 程序员为什么要关心代码性能？

你好，我是庄振运。

感谢你加入这个专栏的学习，我也非常高兴能有机会和你一起探索这个领域。

我在计算机和互联网行业已经研究和工作近 20 年了，一直从事性能优化和容量管理相关的工作。从今天起，我就和你分享我这些年的经验和感悟。

提起计算机和互联网，多数人首先想到的职业是程序员。中国有多少程序员呢？很多人估计有 600 万左右。全球的人数就更多了，肯定超过 2000 万。

我虽然也在互联网领域，也做过几年写程序的工作，但是现在的工作，严格意义上不算程序员，而是性能工程师。不过我和很多程序员朋友一起工作过，也讨论过。谈到性能优化和系统容量管理的时候，一开始他们经常会问我一个问题，就是程序员为什么需要了解性能和容量这些东西？通俗点说，这个问题就是：我就是一介程序员，性能和系统容量听起来很重要，但与我何干？

这个问题问得很好。我可以和你肯定地说，程序员应该关心，也必须关心代码性能和系统容量。今天这一讲，我们先说说程序员为什么需要关心性能。

怎么定义“性能”和 “性能好”？

说起代码性能，首先我们需要弄清楚什么样的代码算是性能好？怎么样算是性能不好？

代码性能表现在很多方面和指标，比较常见的几个指标有吞吐量（Throughput）、服务延迟（Service latency）、扩展性（Scalability）和资源使用效率（Resource Utilization）。

吞吐量：单位时间处理请求的数量。

服务延迟：客户请求的处理时间。

扩展性：系统在高压的情况下能不能正常处理请求。

资源使用效率：单位请求处理所需要的资源量（比如 CPU，内存等）。

必须说明的是，这几个指标之外，根据场景，还可以有其他性能指标，比如可靠性（Reliability）。可靠性注重的是在极端情况下能不能持续处理正常的服务请求。不过，我们这个专栏的讨论，主要围绕前四个更常见的目标。

性能好的代码，可以用四个字来概括：“多快好省”。

看到这四个字，你可能想起了咱们国家当年制定的大跃进总路线，那就是：“鼓足干劲、力争上游、多快好省地建设社会主义”。没错，高性能代码的要求和这个“社会主义建设总路线”相当一致。这里的“多”，就是吞吐量大；“快”，就是服务延迟低；“好”，就是扩展性好；“省”，就是资源使用量低（也即是资源使用效率高）。

用这样的四个指标来衡量，那么性能不好的代码的表现就是：吞吐量小、延迟大、扩展性差、资源使用高（资源使用效率低）。

程序员为什么要关心代码性能？

对程序员来讲，写出的代码就是他的产品、他的生命线、他的形象和价值。代码性能不好，就是质量差，不靠谱。轻者影响程序员的声誉，重者影响他的工作。

对一个公司来讲，产品质量差，公司或许会倒闭。对程序员所在的互联网公司而言，如果公司的业务依赖于程序员写的代码，那么代码性能差，关键时刻掉链子，比如双十一促销的时候，公司的业务性能就会经常出问题，进而会影响公司的运营和营收，这可是天大的事情。

因此，如果一个程序员写出性能很差的代码，无异于耍流氓，并且相关程序员的工作也很难保住。

反过来讲，如果写出的代码性能很高，那代码的作者必定是我们大家认可的“靠谱”程序员，少不了“人见人爱”——客户喜欢，同事喜欢，领导也喜欢。

不同级别的程序员都需要关心性能

还有些朋友或许认为：代码性能是某些人或者其他人应该负责的；我就负责把代码写出来，优化的事，他们负责。这里的“某些人和其他人”可以是指软件测试人员、运维人员、技术专家，或者是性能工程师。

这种想法也是不对的。我下面就用几个案例来举例说明，代码性能是各个级别的程序员都应该关心和负责的。事实上，程序员从学校出来开始，一步步地在职业上攀升，每一步都应该和性能结伴而行。

我用一张图来表示一个成功程序员的技术职业轨迹（注意里面的职位和年限仅供参考）。

学生刚刚从学校毕业，加入互联网公司，一般是入门级程序员。工作 1 到 3 年后，就成为普通的程序员。工作三五年后，可以算是资深程序员。工作 6 到 10 年后，可以成长为技术专家。10 年以上，可能成为高级专家或者架构师。

举例 1：刚入门的程序员

小李刚刚大学毕业，进入一个互联网公司。

领导给他的任务是写一个小模块，其中有一个需求是统计两个日期之间有几个正常工作日（也就是多少是周一到周五）。小李采取的是简单暴力法，就是用一个循环，循环的起始和截至日期就是给定的两个日期。在循环里面，对每一个日期判定一次，确定是工作日还是休息日，然后把工作日累加起来。

这样的代码显然性能不高，生产环境里面跑起来很快就会出问题。比如，如果两个日期差距很大，这个模块可能就需要很长时间才能处理完。

如果小李注重代码性能，他完全可以用更高效的方法，比如快速判定给定的两个日期间有多少个星期，然后乘以 5，因为每个星期有 5 个工作日。然后，对头尾的星期进行特殊处理。这样的代码跑起来快多了。我可以想象，小李在优化完代码后，或许会吟诵两句“何当金络脑，快走踏清秋”来形容新代码的性能。

举例 2：普通的程序员

小王做程序员 2 年了，在公司里已经可以独立负责一个模块了。有一天，他需要把一个二维整数数组进行重新赋值，于是，他写出了下面的二重循环：

如果小王了解计算机内存和缓存的知识以及大小，他或许会写出下面的循环。虽然只有两个字母的差别，性能却提升了很多倍。

原因是什么呢？

因为计算机通常都会有数量不大的缓存。数组在内存里是连续存放的，所以，如果访问数组元素的时候能够按照顺序来，缓存可以起到极大的加速作用。

小王一开始的二重循环，恰恰没有有效地使用缓存，反而对数组元素类似随机访问。第二个版本就改正了这个错误，优化了性能。

举例 3：资深的程序员

小赵工作 4 年了，已经算是资深的 C++ 程序员，负责一个程序的开发和设计。他的一个程序需要使用一个 Map 的数据结构。他开始使用的是 STD 库的标准实现：unordered_map。但是他发现，在数据量大的时候，键值的插入操作需要的时间很长。虽然做了各种代码优化，但性能总是不尽人意。

其实，如果他了解 C++ 有些库有更高效的 Map 实现，比如google::dense_hash_map，他或许可以酌情采用，从而大幅度提升性能。

很多的测试结果显示，google::dense_hash_map的性能可以比std::unordered_map快好几倍。下图（图片来自https://tessil.github.io/ ）正是同一种测试环境下，两种实现的处理时间比较，我们可以清楚地看出性能的差距。

举例 4：技术专家

小刘工作 8 年了，在公司里已经算是不大不小的技术专家了。

有一天，他看到一份项目计划，其中有一段引起了他的兴趣。这份计划是为了提高服务器的 CPU 使用效率，提出把应用程序的线程池增大，建议程序线程池的主线程数目应该和服务器的逻辑 CPU 的数目相等。当然，这里的逻辑 CPU，就是我们通常说的虚拟内核数。

小刘这几年对硬件和操作系统钻研良多，他立刻指出，这样部署不妥，他建议降低主线程池大小到逻辑 CPU 的一半。技术讨论过程中，小刘给大家仔细讲解了原因，大家最后认可了他的建议，小刘也获得了大家的青睐。

小刘之所以这样建议，是因为他知道，服务器的逻辑 CPU 不是物理 CPU。在超线程技术（Hyper Threading）的情况下，服务器的吞吐量不是严格按照逻辑 CPU 的使用率来提升的，因为两个逻辑 CPU 其实共享很多物理资源。

比如下面这张图，就表示了在一台有８个逻辑 CPU 的服务器上，如果部署超过 4 个线程，得到的性能提升非常有限，甚至可能会带来其他不好的后果。这里具体的提升率和效果，取决于线程和应用程序的特性。（图片来自http://blog.stuffedcow.net）

举例５：高级专家（架构师）

老周是公司里的架构师和高级专家。他最近对公司的一个重要业务进行了性能优化，用很小的代码改动，就给公司节省了几百万美元的运营成本（这是我身边发生的一个真实案例，除了名字不一样）。

这个业务的性能瓶颈是 CPU。因为业务量大，这个业务部署了 1 万台以上的服务器，占用了很大一部分数据中心的容量。

老周仔细研究了业务的逻辑，并且进行了性能测试和分析。他发现代码的执行过程卡在了 CPU 取指令的速度上：因为内存和缓存的物理特性，CPU 花了很大一部分时间在等待指令获取，从而造成了 CPU 浪费。

他经过考虑，决定进行指令级别的提前获取优化。具体来讲，就是用 GCC 的__builtin_prefetch指令来预先提取关键指令，从而降低缓存的缺失比例，也就提高了 CPU 的使用效率。

下图是 GCC 关于这个指令的官方文档。

经过这样的优化，一台服务器可以处理比以前多 50% 的请求，从而节省了相应比例的服务器和容量。从公司成本角度来看，这一优化节省了 3 千台以上的服务器，价值几百万美元，老周被 CEO 开会表扬，也是自然的事情了。

有趣的是，整个的代码改进只需要几行代码的改动，真真切切是“一字万金”。

总结

重要的事情需要多说几遍：每个 IT 从业人员，尤其是程序员，都需要关心代码性能。

如果不了解性能的知识，也许能写出可运行但性能不好的代码。但一个真正对工作、对公司和对自己负责的程序员一定会发现，性能不好的代码无异于耍流氓，不经用还隐患无穷，万万要不得。

换句话说，对程序员来说，生活不仅是眼前的代码，还有效率和性能的优化。唐代诗人孟郊在考中进士后写了一首《登科后》，其中有两句：“春风得意马蹄疾，一日看尽长安花。”

我们谁不希望写出来的代码也运行飞快，自己能春风得意呢？！

思考题

无论你工作几年了，也无论是现在具体做什么工作，你能举出一两个，因为代码性能不好并导致严重后果的例子吗？是什么样的性能问题呢？

换个角度来说，如果写代码的程序员一开始就考虑到各种性能问题，并且提前在代码里面解决，写出的代码跑得飞快，而且很稳定。这样靠谱的程序员你会不会给他点赞？

欢迎留言和我分享你的观点，也欢迎你把今天的内容分享给身边的朋友，和他一起讨论。

02 | 程序员也要关心整个系统和公司成本吗？

你好，我是庄振运。

上一讲我们谈了，作为一个程序员，你所负责的软件模块的性能是很重要的。如果写的程序性能不好，轻则通不过开发过程中的性能测试这一关，严重的话，还会为以后的业务生产环境埋下很多地雷和炸弹，随时会踩响和爆炸，从而影响公司的业务和运营。

代码性能的重要性，不仅仅局限于程序员所直接负责的软件模块，它对其他相关软件模块、模块所在的应用程序、单机系统的设计、互联网服务的质量、公司的运营成本，甚至对我们共同生活的地球都很重要。

这一讲，我们就来说说这几个方面。为了方便说清这几方面的关系，我画了下面这张图。

我来简单解释一下这张图：

首先，红色模块是我们负责的模块（标示 0），它和其他模块一起构成了整个应用程序（标示 1）；

这个应用程序运行在服务器和 OS 上面，构成了一个单机系统（标示 2）；

几个单机系统一起组成一个互联网服务（标示 3），来面向客户；

这个服务和其他服务一起，需要公司的硬件容量支持，从而占用公司的商业成本（标示 4）；

最后，别忘了，我们共同生活在这个可爱的绿色星球上。

应用程序的性能（标示 1）

我们先从标示 0 和 1 开始，也就是模块和应用程序。

我们每个人负责的代码模块，一般都不是孤立存在的，都要和其他模块交互。模块之间是唇齿相依的。如果一个模块性能不好，一定会在某种情况下影响到其他模块，甚至是整个程序的性能和服务质量。唇亡齿寒的道理我们都懂，所以每个软件模块的性能都需要严格把关。

具体到我们自己的模块来说，或许在开发、测试的环境中，这个模块看起来运行正常，但等到了生产环境，一旦流量上去，性能不好的模块很快就会被曝光。尤其是在流量很高的时候，如果性能不佳，公司的运维同事一定会疲于奔命，更甚者会导致公司业务受损，这时性质就很严重了。

很多性能问题都会被根因分析。如果根因分析定位到是某人所负责的模块拖了大家的后腿，写这个模块的程序员不仅会被其他程序员鄙视，还可能会被老板找去“喝茶”。

回到性能问题上。性能问题可以表现在很多指标上，比如吞吐量（Throughput），服务延迟（Latency)，可靠性（Reliability)，延展性（Scalability）等等。根据模块所在的应用程序的性质，其中的一个或者几个指标会相对比较重要些。举个简单的例子，如果一个应用程序所在的互联网服务是面向终端客户的（比如微信用户），那么客户的服务延迟一定是极为重要的性能指标。

具体来说，假如端到端的服务延迟有最大的延迟允许，比如不能超过 2 秒钟，那么这个服务所需要的应用程序或者微服务，一般也都会有自己的最大延迟预算。假设这个端到端服务需要三个微服务或应用程序来串联，那么，每个应用程序都会分到一定的延迟预算，比如最大 1 秒。

同理，我们所负责的模块也会根据程序的逻辑设计分到相应的延迟预算，比如 300 毫秒，如下图红色模块所示。

这种情况下，如果我们的模块在流量适度变大时，处理时间超过 300 毫秒的预算，那这个模块的延展性显然就不够了，很可能会导致整个端到端服务的延迟超标。

单机系统的性能（标示 2）

讨论完了模块和程序，我们再看看单机系统。

我们的模块所在的应用程序（或者微服务），是运行在服务器的硬件和操作系统上面的。对这台服务器而言，这是个单机系统，包括软件和硬件的整个垂直全栈。现在的系统都非常复杂，软硬件之间的交互也复杂而微妙，并且随着各个构件的升级而经常变化。

单机系统的软硬件构件包括操作系统、程序库、存储系统、CPU、内存、还有网络等等。这些构件都会或多或少地影响上层程序的性能。

我举一个简单的例子，这个例子在后面的文章中还会仔细介绍。在我以前的公司，曾经有一个应用程序需要不断地把数据同步写到底层存储系统。这个应用程序后来被发现有性能问题，我们花了很多时间去做分析，后来终于找到原因。

这个性能问题的表现是：数据同步写入的延迟有时候会非常高。原因是底层的存储系统同时服务好几个应用程序。

由于底层存储系统有各方面的限制，当多个应用程序同时使用这个存储系统时，每个应用程序延迟方面的性能并不能得到保证，因此导致了某个应用程序的读写被严重推迟，并最终导致了后者的性能问题。

更进一步到设计层面来讲，从我们负责的模块和应用程序角度来看，对下层的软硬件构件越是了解，就越有可能设计出性能优越的模块和应用程序。

比如，很多数据存储方面的服务和应用程序在设计时，需要仔细考虑各种存储系统的技术趋势和性能特征。这些性能特征包括存储速度、价格、容量大小、易失性等。比如传统的 DRAM 内存就是一种存储，它速度很快，但是价格贵、容量小，并且所存数据不能长期保存，一断电数据就会丢失。

最近几年一种新的非易失性内存（NVM，Non-volatile Memory）的出现，打破了这一传统，数据可以长期保持，但是速度稍微慢一些。同样的，传统的硬盘存储容量大、价格低，但是速度最慢。最近几年固态硬盘（SSD） 的大量采用，在很多新设计的在线系统中已经作为标准配置，几乎取代了传统硬盘。

我们程序员作为自己模块甚至整个应用程序的设计者，如果能充分考虑这些硬件的性能特征和技术趋势，就可以设计出性能好、高效率的软件。

下面这张表格我列举了四种存储硬件，分别是传统 DRAM 内存、NVM 内存、硬盘和固态硬盘；并且比较了它们的五个指标。

互联网服务的性能（标示 3）

现代的互联网服务往往需要很多模块交互，并且客户流量会很大。我们所在的应用程序和系统经常只是整个互联网大服务的一部分，会有上游服务对我们产生请求，我们也会对下游服务发送请求。

比如下面的图示，我们所在的服务模块用红色标识，上游服务模块用绿色标识，下游服务模块用黄色标识。

从公司运营的角度来看，整个互联网大服务的性能才是我们每个程序员真正关心和负责的。我们每人都需要从这个大局出发来考虑和分析问题，来设计自己的模块以及各种交互机制。否则，可能会出现我们的模块本身看起来设计得不错，但却对上下游模块造成不好的影响，进而影响整个大服务的性能。我来举一个真实的案例。

这个案例是从一次生产环境下的服务问题中发现的。某个下游模块出现延展性问题，服务的延迟变大，上游模块发出的请求排了很长的队。这个时候上游模块已经感觉到下游的性能问题，因为对下游请求的处理延迟已经大幅度增加了。

此时上游模块本应该怎么做呢？

它应该降低对下游模块的请求速度，从而减轻下游模块的负担。但是案例中的上游模块设计没有考虑到这一点。不但没有降低请求速度，反而发送了更多的请求，以求得更快的回答。这样无异于火上浇油，最后导致下游模块彻底挂掉，引发了整个服务的瘫痪。

后来我们学到的教训就是，串联的服务模块中，上游模块必须摒弃这样雪上加霜的服务异常尝试，应该采用指数退避机制（Exponential Backoff ），通过快速地降低请求速度来帮助下游模块恢复（上游模块对下游资源进行重试请求的时间间隔，要随着失败次数的增加而指数加长）。

公司的成本（标示 4）

我们所负责的互联网服务的性能直接影响公司的成本。

一个高性能的服务，在服务同等数量的客户时，需要的成本会比较小。具体来说，如果我们的服务是计算密集型，那么就应该尽量优化算法和数据结构等方面来降低 CPU 的使用量，这样就可以用尽量少的服务器来完成同样的需求，从而降低公司的成本。

现如今是大数据时代，公司在服务器和数据中心以及网络等容量方面的支出是很可观的。尤其是大的公司比如脸书，腾讯等，公司有很多的数据中心和几百万台的服务器。如果公司的每个服务都做到高性能，替公司节省的运营成本是非常巨大的。

同时，面向互联网服务的容量规划和效率管理也很重要。如果能科学地管理容量，准确地预测未来需求，并逐步提升容量的效率，就能把公司这方面的成本管理和节省好，从而不至于浪费资金在不必要的多余容量上。

我们共同的绿色地球

最后，让我们跳出“我们的公司”这样的小格局，放眼全球，甚至我们人类的大格局。我们只有一个共同的地球，我们有责任让她保持绿色。

现在的时代，感谢互联网的发展和大数据时代的来临，全球各公司的数据中心已经在消耗大量的能源。从咱们国家来看，2018 年，国内的数据中心用掉的电量比整个上海市用电量还大，占全国用电量的 2.3%。全球来看也类似，数据中心在 2018 年消耗了全球 3% 以上的电量。这个耗电量已经是差不多整个英国全国用电量的两倍。更严重的是，这样的用电还在飞速增长，差不多每三年或四年就翻一倍！

所以，我们每个人，其实都负有责任来降低能源消耗。虽然生活中有多种方式可以降低能源消耗，我们的日常工作其实也是重要的一环。如果每个人能把负责的代码优化一下，服务高效一些，我们就是在拯救我们共同的地球，让她永葆绿色！

总结

对代码和程序的性能优化，以及对系统容量的效率提升，和我们共同关心爱护的东西息息相关。从代码模块，到整个系统，到互联网服务，到公司运营，再到我们的社会，都依赖于我们每个人的责任和贡献。

你和我或许是一介普通工程师和程序员，但人们常说“位卑未敢忘忧国”。我们虽然没必要拔高到忧国忧民的高度，但是也要认真做好我们的份内份外的事情。

思考题

回顾你最近接触过的软件模块或者正在写的代码，有没有和其他的系统模块有交互关系，它们之间是如何交互的？

如果你的模块性能不好（不管是响应时间很慢，还是发出过多请求），有没有可能对其他系统模块造成影响？这个影响会不会造成整个系统和服务的严重后果甚至瘫痪？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

03 | 导读：专栏是怎么设计的？需要哪些知识？

你好，我是庄振运。

在前面两讲中，我们看到了性能优化和容量效率提升的重要性，如果程序员在这方面的技能和知识有欠缺，只知道写代码，那么写出来的代码很可能效率低、性能差。在代码性能差的情况下，如果你再被老板或者同事威逼利诱去做性能优化，那么就成了赶鸭子上架的苦差事了，你只能感叹：“问君能有几多愁，恰似写完代码去调优”。

玩笑开完，我们还是从正能量的角度去看一看吧。

这就要从一个典故说起了，美国福特汽车公司当年要排除一台大型发动机的故障，请了很多专家，但都束手无策。最后请来了著名的电机专家斯坦门茨（Charles Proteus Steinmetz）。斯坦门茨仔细检查了机器后，用粉笔在机器外壳的某处画了一道线，然后说：“把做记号处的电机线匝减少 16 圈。”难题居然就迎刃而解了。

斯坦门茨索要了 1 万美元作为报酬，很多人觉得实在是太多了。因为当时福特公司最著名的薪酬口号就是“日薪 5 美元”，也就是说一个工人每年能赚 1 千美元已经是很高薪了。但是斯坦门茨回答道：“用粉笔画一条线，顶多值 1 美元；但是知道在哪里画线值 9999 美元。”当公司总裁福特先生得知后，十分欣赏斯坦门茨，并很痛快地给了 1 万美元的酬金。

我们做性能工作也是如此，虽然性能优化的方法和最终解决方案或许看起来很简单直白，但是要知道在哪里做优化和做什么样的优化，却需要很多的测试和分析的工作经验。

每个程序员和运维人员都应该尽量多地了解性能优化和容量效率提升的知识，学习这方面的技能，因为在自己的职业的不同阶段都可以从这些技能获得收益。

那么，性能优化和容量效率提升需要什么样的知识和技能呢？ 这一讲我就和你聊一聊。

总体要求

性能和容量管理工作需要的知识面比较广， 而且最好具有其他相关方面的能力，比如要有一定的和人打交道的软技能，从而和其他员工以及其他团队进行有效地沟通和合作。

大体上讲，性能相关的工作有三个方面的特点，我分别说明一下。

第一个特点是知识面要广，并且软硬结合。

计算机方面的知识可以大体上分为两大类：软件相关和硬件相关。很多程序员对软件相关的知识了解多些，但对硬件方面了解不多。但是，因为很多性能问题会牵扯到硬件，所以基本要求就是“能软能硬”，两方面的知识都要足够。

第二个特点是“理论联系实际”。

一方面需要理论根底深厚，因为有时候性能问题是某些不明显的原因导致的，所以需要对各种协议，对软件和操作系统，对硬件和网络都要非常熟悉。同时，性能分析的过程需要做些实验来使真正的问题暴露出来，也就是需要进行验证，所以对动手能力的要求也比较高。

第三个特点是不但要会性能测试，还要会性能分析和性能优化。

我画了张图，希望能帮助你理清思路。通俗点说吧，这些方面对我们的要求就是：写得了代码，查得出异常；理得清问题，做得了测量；找得到病根，开得出药方。

广阔的知识面

我们现在先从知识方面讲起，看看性能和容量工作需要什么样的知识（参考上面的图）。

首先是软件方面，其实内容很多。图里面仅仅表示了几个方面，你可以参照我们上学时开的各种计算机方面的课程，尤其是操作系统、数据结构和算法、编译原理以及各类协议。这方面也包括很多数学和统计方面的知识。

硬件方面包括服务器本身、存储系统、各类网络、数据中心等等。这方面的重点是服务器本身的部件，比如 CPU、内存等等。

至于实践经验，主要是关于性能方面的。比如很多系统的命令来观察系统资源的使用率，各种调试和测试工具，以及如何进行性能分析和性能优化的实践。

软技能方面，我会在这个专栏的最后两讲详细进行介绍，这里大概讲一下。做性能优化和容量管理工作经常需要和其他的团队和人员打交道。这些团队包括开发团队、数据中心团队、运维团队等等，需要和他们紧密而愉快地合作。同时，性能分析和优化经常需要把数据和分析展示给别人和领导看，这就需要你拥有一定的演讲能力。

性能工程师的工作很像“医生”的工作

性能问题通常是复杂的，性能工程师的工作就很像“医生”的工作，需要多方面的知识才能确诊（这个观点我在开篇词里有介绍，这里还是有必要再详细说说）。

虽然性能问题有时候明显的是表现在某个组件的问题，比如网络拥塞，但很多情况下并不容易确定是哪里出了问题。所以，就需要做很多种不同的测试和数据分析。

另外，很多程序和服务内部有很多模块，外部也牵扯到其他子系统。即使每个子系统分开来测试时都性能不错，但是在某些特殊情况下，会发生级联和连锁故障。所以，除了需要了解每个子系统内部的原理，还必须弄清楚不同模块和子系统之间的协作交互关系。

具体来讲，软件硬件各个子系统都可能因为各种情况下的交互而产生性能问题。尤其是垂直的软件硬件栈（比如程序、操作系统、硬件等），这样的跨层交互，更会产生各种复杂的性能特性，这方面的内容后面会详细展开。

我经常把性能工程师比作医生。

医生遇到有健康问题的病人，会做“望闻问切”，利用 B 超、X 光等做各种分析。根据各种数据和病人的表象，医生会做出诊断，确定是什么病。然后会开药方或者给予相应的治疗。病人吃药和接受治疗后，会再次进行复检，来确定病情是已痊愈？是稍有缓解？还是加重了？

性能工程师对待计算机和互联网的性能问题也是如此，会首先观察各种参数，甚至进行主动的场景测试。根据性能测试的结果，可以做出分析，并最终确定性能问题的根因。这之后可以进行相应的性能优化来消除对应的性能问题。采取优化后，还需要进行重新测试来确定问题真正得到解决，亦或是另有他因，从而需要重新分析。

我用下图来类比这两种场景。

第一个场景是医生诊断病情和治疗病人。第二个场景是性能工程师分析问题并且优化性能。我们可以直观地看出两种场景中的每一步的相似之处。

这个专栏的组织架构

我是怎样组织这个专栏的呢？首先重申，写这个专栏的初心是向你介绍性能和容量管理工程这一工作，并分享我近二十年来的学习和工作经验。

需要说明的是，这一领域和工作牵涉的内容其实非常广泛，包括所有的计算机互联网方面的知识。所以在我们这个专栏有限的时间内，面面俱到是不太现实的。出于这样的考虑，在这个专栏里我的侧重点及讲述方式和其它相关专栏和课程不太一样。

我在专栏文章中尽量做到深浅结合，既要让你能了解这一工作的重要性和大体情况，也要有足够的干货，希望能让你有恍然大悟的感觉。

计算机科学在国外大学里面一般不在工程学院下面开设，而是归类于艺术，“Art”，设置在艺术和科学学院下面。不管这种分类的渊源如何，我确实觉得计算机科学和性能调优真的是一门艺术，它需要知识，需要经验，也需要天分。既然是艺术，就和其它艺术形式，比如文学有其相通之处。

我个人喜好诗词古文，对生活中和学习中的很多感悟，我经常会跳出一层去体会。很多时候我发现不同领域的东西，它们的感觉和道理是完全相似或相通的。修辞学上有一种手法叫“通感”，就是不同的感觉方式的类似体验（比如听觉和视觉的相通等），也是这个道理。

所以，我在分享知识和经验的时候，有时会忍不住加上几句唐诗宋词古文，目的有好几个，第一是不希望一直干巴巴地讲课，希望加点调味料，提高你的兴趣；第二是理工科的技术和文科的文艺的确经常有异曲同工之妙，希望帮助你体会；第三呢，顺便帮你复习一下诗词，增加学习乐趣。

先一起来看看我们要学习的内容大纲。

开篇：代码性能和系统容量

考虑到很多朋友对这方面的工作不是特别了解，所以我开始在开篇这一部分用两讲来做一个宏观介绍，让你了解性能问题为什么对每一个 IT 人员都重要，尤其是对程序员。我分成“代码性能”和“系统性能及公司成本”来分别说明。

性能定律和数理基础

性能工程离不开理论基础。我接下来会用几讲来介绍最基础但也是最重要的数理基础和几大定律。这些数理基础包括一些基本的统计知识，以及对数据的分析和展示的方法。我还会把一些重要的性能相关的数字总结出来，让你参考和记忆。对待这些数字，你应该像对待九九乘法表一样，每个都铭记在心，因为工作中时时要用到。

性能测试

性能工程离不开测试。性能测试是一切性能工作的基础和开端。

很多公司的性能工程师其实多数时间是花在性能测试上，包括进行测试的设计和分析测试结果。虽然测试工作看起来简单直白，可是真正做好性能测试并不容易，这里有相当多需要注意的地方。我会梳理其中一些经验和指导原则讲解给你。

当然，测试的工具也很重要，一个好的测试工具绝对让你事半功倍，所以我也会介绍常用的好工具。

性能分析

我们需要对性能测试得到的数据进行仔细的分析和研究，只有这样才能发现真正的问题和找到性能问题的根本原因。而性能分析就是关键的一步。我会首先介绍进行性能数据分析的原则，然后抓住几个重点领域，包括 CPU、内存、存储和网络，来分别介绍常见的性能问题，让你以后碰到这方面的问题时心里有数。

性能优化

性能分析的目的是找到性能问题的根因，然后进行性能优化来解决问题。性能优化做得好，必须有相关方面的知识和实践经验。我会给你介绍性能优化的六大原则和十大常用策略，并分几个领域用生产中的案例做具体的展示。

性能工程的进阶实践

我还特意准备了几讲稍微进阶的内容和实践案例。这些内容是我过去在几个大公司的亲身实践，每一讲都是针对某个具体场景的生产实战经验。

容量规划和服务管理

对公司，尤其是大公司来说，容量的规划管理和效率提升是很重要的，因为这直接关系着公司的运营成本。我注意到这方面的参考资料比较少，所以特意来和你介绍这一领域的知识。我会分成几部分来分享，包括服务器的部署、数据中心、容量规划、容量的效率提升以及服务需求的控制等等。

一个公司要成功运营，成本和对应的容量是总有限的，所以需要量入为出，对服务的需求进行适当的管理，尽量精打细算。管理的实践需要考虑很多因素，一方面尽量节省容量，另一方面也不能妨碍公司业务的扩展。如何把握这个度，我会讲讲经验。

专栏总结：性能和容量工程的工作特点

最后两讲是介绍性能和容量工程师这个职业，包括这一工作的特点和职业前景。

随着大数据和互联网的飞速发展，我坚信这方面的工作越来越重要。尤其是大公司，都会专门招聘这方面的人才组成特殊的性能优化和容量管理团队。所以，针对有志于从事这一行业的朋友，我会分享这方面的面试经验。

总结

得益于这种工作的特点，性能优化和容量效率需要了解的知识和技能比较广泛。人们常说：“读书破万卷，下笔如有神。”读书写文章是这个道理，做性能优化工作也是如此。只有不断学习计算机各方面的相关知识，博览群书，才能在解决性能问题时得心应手。

我们在前三讲里面一起探讨了为什么我们需要关心代码性能，系统性能和容量效率；并且了解了这方面的工作需要什么样的知识和技能。从下一讲开始，我们就一起学习这些知识和技能。

思考题

你碰到过或者听说过什么领域的性能问题吗？

对这个性能问题做根因分析和解决需要哪方面的知识和经验？

彻底解决这个问题需要和其他人和团队合作吗？

如果需要和其他人和团队合作，再假设你是带头人，你会怎么去推动呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

04 | 性能工程三定律：IT业和性能优化工作的“法律法规”

你好，我是庄振运。

在开篇的几讲里，我谈了性能工程的重要性以及所需要的知识面，接下来我们就正式地进入相关的学习。

不过不要着急，第一个模块我们并不会直接进入性能问题的现场，一上来就去解决问题，而是要先耐下心来，学习一些必备的基础知识。为什么呢？因为学习任何事情，打好坚实的基础是至关重要的。

古人云：“合抱之木，生于毫末；九层之台，起于累土。”

所以接下来的几讲，我们需要先学习一些基础知识，包括和性能工程相关的几个重要定律法则和数理基础。这一讲我先和你探讨三个定律法则：帕累托法则、阿姆达尔定律和利特尔法则。

帕累托法则

我想你可能知道帕累托法则，它也被称为 80/20 法则、关键少数法则，或者八二法则。

这个法则是基于我们生活中的认识产生的，人们在生活中发现很多变量的分布是不均匀的——在很多场景下，大约 20% 的因素操控着 80% 的局面。也就是说，所有的变量中，比较重要的只有 20%，是所谓的“关键少数”。剩下的多数，却没有那么重要。

举例来讲，在企业销售中，根据帕累托法则，大约“80％的销售额来自 20％的客户”。认识到这一点对企业管理至关重要，比如需要重视大客户的关系。

虽然帕累托法则在生活中很多方面都适用，但我们今天的重点是来看看帕累托法则是怎么应用到我们 IT 界的，尤其是怎么指导我们的代码开发和性能优化相关的领域的。

我总结了一下，这个法则可以有以下几个领域的指导。

1. 应用程序的使用

一个应用程序往往可以提供很多功能。但是，如果我们统计用户的使用情况，会经常发现大约 80% 的用户使用集中在 20% 的程序功能。

所以，对我们开发程序的指导意义是，要花足够心思在最常用的少数功能模块上，对它的设计和实现都要充分地优化。

2. 程序代码开发时间的分配

一个程序的开发过程中，大约 80% 的代码开发只占用了 20% 的总体开发时间。一般来讲，一个应用程序开发时，一开始往往花不了多少时间就可以快速地搭建一个大体可以工作的原型。反而是后面的剩余代码和代码改进需要更多的时间。

也就是说，80% 的开发时间往往用在最精髓的 20% 代码上。帕累托法则对我们的指导意义是，对代码开发工作量的安排上，需要合理有序地规划好开发时间。

3. 程序代码的维护

如果观察代码的维护和改进历史，你经常会发现少数代码被不断地改动，甚至经常被改得面目全非，而多数代码几乎从第一次写完后就一成不变了。按照帕累托法则，80% 的代码演进和改动发生在大约 20% 的代码上。

对程序维护而言，我们需要对这 20% 的代码尽量熟悉，这样才能对其他程序员的代码改动了如指掌，一目了然。

4. 程序代码的修正和纠错

统计数字也表明，所有的代码错误中，差不多有 80% 的错误发生在大约 20% 的代码上。

所以，根据帕累托法则，代码修复和纠错时要重点修复最容易产生 Bug 的代码，这样才能把保证整个应用程序的合理质量。

5. 客户流量的时间分布

估计 80％的流量将在总时间段的特定 20％内发生。比如一个商业软件，客户的流量峰值往往是上班时间开始的那几个小时（比如上午 9 点到 12 点）。

所以，我们在应用程序设计和部署时，要充分考虑帕累托法则带来的影响，尤其是客户访问的峰值时段和空闲时段。

6. 程序代码的优化

一个程序完成后必然会去运行，如果我们统计代码的运行时间，往往会发现程序的 80% 的时间是在运行大约 20% 的代码。也就是说，只有少数代码非常频繁地被调用。

所以，如果我们想提高程序的性能，最好找出这些少数代码，并做重点优化，这样就可以用很少的改动大幅度地提升整个程序和系统的性能。

那么现在，我们就从性能优化的角度，来看看如何参照帕累托法则，来规划我们性能优化工作的投入和产出。

假设我们的性能优化投入永远是按照代码的优先级来投入的，也就是说，总是要先优化最值得优化的代码。那么我们看到，只要投入差不多 20% 的努力，就能产出 80% 的性能优化产出，获得最大的投入产出比。

下图是一个根据帕累托法则来投入努力和产出效果的过程。

假如先解决 20% 的最重要的问题，就可以达到总体效果的 80%。以后再花费 80% 的努力，也只能解决剩下的 20% 的问题。

在应用帕累托法则的时候，需要注意的是，里面的 80% 或者 20% 都是大约数字，实际的场景千差万别，不可能是恰好这两个数字。这个法则的精髓是，我们的生活和自然界万物的分布不是均匀的，总有些因素比其他因素更重要。

阿姆达尔定律

接下来我们来看第二个定律。这个定律你可能会感觉有点陌生。

阿姆达尔定律（Amdahl’s law / Amdahl’s argument）是计算机科学界非常重要的一个定律和法则。它本来用于衡量处理器进行并行处理时总体性能的提升度。但其实阿姆达尔定律可以用在很多方面，为了方便你理解，我们就从一个简单的生活例子开始。

我们用洗衣服和晾衣服来举例。这里假设我们不用洗衣机，而是用传统的方式，先洗再晾。再假设洗衣服和晾衣服各需要 10 分钟，那么整个过程进行完需要 20 分钟。

如果我们对晾衣服的过程进行优化，从 10 分钟缩短到 5 分钟，相当于进行了两倍优化。现在整个过程需要多长时间呢？需要 15 分钟，因为洗衣服的模块还是需要 10 分钟。

在这个基础上，我们继续对晾衣服模块进行优化，速度提升 5 倍，从 10 分钟缩短到 2 分钟。整个过程现在需要 12 分钟完成。

在这个基础上继续进行类推，我们就会发现，无论对晾衣服模块进行多大的优化，整个洗衣服、晾衣服的过程所需的时间不会小于 10 分钟，也就是整体加速比不会超过 2。

根据阿姆达尔定律描述，科学计算中用多处理器进行并行加速时，总体程序受限于程序所需的串行时间百分比。譬如说，一个程序 50% 是串行的，其他一半可以并行，那么，最大的加速比就是 2。无论用多少处理器并行，这个加速比不可能提高到大于 2。

所以在这种情况下，改进程序本身的串行算法可能比用多核处理器并行更有效。

用公式来讲，假设一个系统的整体运行时间是 1，其中要进行优化加速的模块运行用时是 P。如果对这个模块的加速比是 N，那么新系统的处理时间可以用下面的公式来表示。

这里面（1-P）是未被加速的其他模块运行时间，而 N 分之 P 是优化后的模块运行时间。它们的和就是新系统的总体运行时间。

相对于旧系统，运行时间的加速比就是：

阿姆达尔定律对我们进行性能优化的指导意义有以下 2 点。

优先加速占用时间最多的模块，因为这样可以最大限度地提升加速比。

对一个性能优化的计划可以做出准确的效果预估和整个系统的性能预测。

下面这张图描述了不同的并行百分比场景下分别进行并行优化的曲线。不同的曲线对应不同的并行模块百分比。横轴是并行程度，也就是多少个并行处理器。纵轴是速度提升度。

对每一条曲线我们都可以看到，超过一定的并行度后，就很难进行进一步的速度提升了。

另外说明一点，阿姆达尔定律其实是另外一个定律的简化版本。这个更复杂的定律叫通用扩展定律（USL, Universal Scalability Law），你有兴趣的话可以去学习一下。

利特尔法则

接下来我们来看利特尔法则（Little’s Law）。这个法则描述的是：在一个稳定的系统中，长期的平均客户人数（N）等于客户抵达速度（X）乘以客户在这个系统中平均处理时间（W），也就是说 N=XW。

这个法则看起来有点不直观，但从整个系统的宏观角度仔细想想的话就容易理解了。

如下图所示，客户按照一定的速度不断地进入我们的系统，假设这个速度是每分钟 X 个客户。每个客户在我们系统里的平均处理时间是 W 分钟。一旦处理完毕，客户就不会滞留在我们的系统里。

所以，如果这个状态稳定，也就是说，我们的系统处理速度恰恰好赶上客户到达速度的话，一方面系统没有空闲，另外一方面客户也不需要排队在系统外等待。那么在这个稳定状态下，我们的系统的总容量就恰好等于系统里面正在处理的客户数目。也就是说，N 就等于 X 和 W 的乘积。

我举一个服务器性能提升的例子来解释吧。

假定我们所开发的服务器程序可以进行并发处理，同时处理多个客户请求。并发的客户访问速度是每分钟到来 1000 个客户，每个客户在我们的服务器上花费的平均时间为 2 分钟。根据利特尔法则，在任何时刻，我们服务器系统里面将容纳 1000×2＝2000 个客户。这 2000 个客户都在被服务。

过了一段时间，由于客户群的增大，并发的访问速度从每分钟 1000 客户增大到每分钟 2000 个客户。在这样的情况下，我们该如何改进我们系统的性能来应对呢？

根据利特尔法则，我们可以有两种方案来解决这一需求。

第一种方案是把客户的处理时间减半，从 2 分钟减到 1 分钟。这样我们的系统容量可以不变，客户滞留在我们系统的时间减半，刚刚好可以适应访问速率加倍的要求。系统容量就等于 2000 客户每分钟乘以 1 分钟，还是 2000 个客户。

第二种方案是扩大系统容量，维持处理时间不变。因为客户访问速度加倍了，所以系统容量也需要加倍，变成 4000。假如原来的系统需要 500 台服务器，那么新系统就需要 1000 台服务器。

从这里可以引申出利特尔法则在性能优化工作中的两种用处：

帮助我们设计性能测试的环境。性能测试的内容我们后面会详细讲到，这里简单提一下。比如当我们需要模拟一个固定容量的系统，那么性能测试的客户请求流量速度和每个请求的延时都需要仔细考虑。

帮助我们验证测试结果的正确性。有时候，如果性能测试的工作没有仔细地规划，得出的测试结果会出奇得好，或者出奇得差，从而让我们抓脑壳。这时如果采用利特尔法则，就可以很快地发现问题所在之处。

总结

我们今天讨论的性能工程相关的三大法则，分别是帕累托法则、阿姆达尔定律和利特尔法则。

可以说，这些法则就是 IT 业和性能优化工作的“法律法规”，有了它们，我们在实际工作中才能做到“有法可依，有法必依”。熟悉并熟练应用这几个法则，对我们的工作是会有很大的帮助的。

孟子说：“不以规矩，不能成方圆。”熟悉并熟练应用这几个“规律法则”，对我们的工作是会有很大的帮助的。

思考题

帕累托法则可以用到很多场景下，除了本讲中讨论的场景，你还能想到什么场景可以使用帕累托法则呢？使用这个法则会帮助你对问题的把握和找寻解决思路吗？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

05 | 概率统计和排队论：做性能工作必须懂的数理基础

你好，我是庄振运。

上一讲我们讲了和性能优化有关的三大基础定律法则。今天我们继续打基础，讲一点统计方面的数理知识，包括重要的概率统计和排队论。

或许你对概率统计和排队论有点发怵，但这些内容是必须学会的，因为它们很重要。因为它们是性能测试和优化这座高楼大厦的地基。地基打不好，性能测试和优化也不会做得很好。

而且我想强调的是：你完全没有必要惧怕，因为你只需要学习一部分最基础的知识，这些知识对多数人和多数场合大体就够了。还记得上一讲的帕累托法则吗？根据帕累托法则，这一讲的内容或许占不到平时教科书内容的 20%，但却可以覆盖 80% 以上的应用场合。

概率和置信区间

今天的内容我们先从概率和置信区间讲起。

概率（Probability），也称几率和机率，是一个在 0 到 1 之间的实数，是对随机事件发生之可能性的度量。这个你应该都懂，不需要我多说。

但概率论中有一个很重要的定理，叫贝叶斯定理，我们做性能测试和分析中经常需要用到，所以我们稍微讲讲。

贝叶斯定理（Bayes’ theorem）描述的是在已知一些条件下，某事件的发生概率。比如，如果已知某癌症与寿命有关，合理运用贝叶斯定理就可以通过得知某人年龄，来更加准确地计算出他患上该癌症的概率。

具体来讲，对两个事件 A 和 B 而言，“发生事件 A”在“事件 B 发生”的条件下的概率，与“发生事件 B”在“事件 A 发生”的条件下的概率是不一样的。

然而，这两者的发生概率却是有确定的关系的。就是 A 事件发生的概率，乘以 A 事件下 B 事件发生的概率，这个乘积等于 B 事件发生概率乘以 B 事件下 A 发生的概率。听起来有点拗口，可如果用公式来表示的话其实很简单。

贝叶斯定理的一个用途在于通过已知的任意三个概率函数推出第四个。

另外一个重要的概念是置信区间。置信区间（Confidence interval，CI）是对产生样本的总体参数分布（Parametric Distribution）中的某一个未知参数值，以区间形式给出的估计。相对于后面我们要讲到的点估计指标（比如均值，中位数等），置信区间蕴含了估计精确度的信息。

置信区间是对分布（尤其是正态分布）的一种深入研究。通过对样本的计算，得到对某个总体参数的区间估计，展现为总体参数的真实值有多少概率落在所计算的区间里。比如下图是一个标准正态分布的图，阴影部分显示的是置信区间 [-1.7,1.7]，占了 91% 的概率。

不难理解，置信水平越高，置信区间就会越宽。一般来说，如果需要涵盖绝大多数的情况，置信区间一般会选择 90% 或者 95%。

了解了概率和置信区间，我们下面去看看如何分析大量数据。

数理统计的点估计指标

做性能测试和优化的过程中会产生大量的数据，比如客户请求的吞吐率，请求的延迟等等。获得这些大量数据后，如何分析和理解这些数据就是一门学问了。通常我们需要处理一下这些数据来求得另外的指标，以方便描述和理解。

描述性统计分析是传统数据分析的基础，这个分析过程可以产生一些描述性指标，比如平均值、中位数、最大值、最小值、百分位数等。

这些描述性指标通常也被称为“点估计”，相对于前面讲到的置信区间，是用一个样本统计量来估计参数值，比较容易理解。这些点估计指标分别有不同的优点和缺点。

平均值：（Mean，或称均值，平均数）是最常用测度值，它的目的是确定一组数据的均衡点。但不足之处是它容易受极端值影响。比如公司的平均收入，如果有一两个员工有特别高的收入，会把大家的平均收入拉高，就是平时我们经常调侃的“被平均”。

需要注意的是，我们有好几种不同的平均值算法。我们平时比较常用的是算术平均值，就是把 N 个数据相加后的和除以 N。但是还有几种其他计算方法，分别适用不同的情况。比如几何平均数，就是把 N 个数据相乘后的乘积开 N 次方。

中位数（Median，又称中值），将数值集合划分为相等的上下两部分，一般是把数据以升序或降序排列后，处于最中间的数。它的优点是不受极端值的影响，但是如果数据呈现一些特殊的分布，比如二向分布，中位数的表达会受很大的负面影响。

四分位数（Quartile）是把所有数值由小到大排列，并分成四等份，处于三个分割点位置的数值就是四分位数。 从小到大分别叫做第一四分位数，第二四分位数等等。四分位数的优点是简单，固定了三个分割点位置。缺点也正是这几个位置太固定，因此不能更普遍地描述其他位置。

百分位数（Percentile）可以看作是四分位数的扩展，是将一组数据从小到大排序，某一百分位所对应数据的值就称为这一百分位的百分位数，以 Pk 表示第 k 个百分位数。比如常用的百分位数是 P90，P95 等等。百分位数不容易受极端值影响，因为有 100 个位置可以选取，相对四分位数适用范围更广。

几个特殊的百分位数也很有意思，比如 P50 其实就是中位数，P0 其实就是最小值，P100 其实就是最大值。

还要注意的是，面对同一组数据，平均值和中位数以及百分位数这些点估计指标，谁大谁小是不一定的，这取决于这组数据的具体离散程度。

比如，我在面试的时候我经常问来面试的人一个问题，就是平均值和 P99 哪个比较大？答案就是：不确定。

方差 / 标准差（Variance，Standard Variance），描述的是变量的离散程度，也就是该变量离其期望值的距离。

重要的分布模型

以上的几个描述性的点估计统计指标很简单，但是描述数据的功能很有限。如果需要更加直观并准确的描述，就需要了解分布模型了。

举例来讲，假设我们有一个系统，观察对客户请求的响应时间。如果面对一万个这样的数据，如何对这个数据集合进行描述呢？这时候用分布模型来描述就很合适。

我们简单提一下几个最重要的分布模型，包括泊松分布、二项式分布和正态分布。

泊松分布（Poisson distribution）适合于描述单位时间内随机事件发生的次数的概率分布。如某一服务设施在一定时间内收到的服务请求的次数等。

具体讲，如果随机变量 X 取 0 和一切正整数值，在 n 次独立试验中出现的次数 x 恰为 k 次的概率 P（X=k）就是：

公式中λ是单位时间内随机事件的平均发生次数。像下面这个图，表示的就是λ=5 的分布。红色部分是 P(X=4) 的概率，约为 0.17。

当 n 很大，且在一次试验中出现的概率 P 很小时，泊松分布近似二项式分布。

二项分布（Binomial distribution），是 n 个独立的是 / 非试验中成功的次数的离散概率分布。

这里通常重复 n 次独立的伯努利试验（Bernoulli trial）。在每次试验中只有两种可能的结果，而且两种结果发生与否互相对立，并且相互独立。也就是说事件发生与否的概率在每一次独立试验中都保持不变，与其它各次试验结果无关。

当试验次数为 1 时，二项分布服从比较简单的 0-1 分布。

在 n 重伯努利试验中，假设一个事件 A 成功的概率是 p, 那么恰好发生 k 次的概率为：

比如下图就是一个二项分布的图，图中红色的是 P(X=5) 的概率，约为 0.18。

正态分布（Normal distribution），也叫高斯分布（Gaussian distribution）。经常用来代表一个不明的随机变量。

正态分布的曲线呈钟型，两头低，中间高，左右对称，因此经常被称之为钟形曲线。比如下图：

一个正态分布往往记为 N(μ，σ^2)。其中的期望值μ决定了其位置，其标准差σ决定了分布的幅度。概率密度函数如下：

当μ = 0，σ = 1 时的正态分布是标准正态分布。上图就是一个标准正态分布，线段的值代表了置信区间。比如在期望值附近，左右各一个标准差的范围内，差不多可以囊括 68.2% 的概率；各两个标准差的范围内，囊括 95.4% 的概率；各三个标准差的范围内，囊括 99.7% 的概率。

正态分布的重要性在于，大多数我们碰到的未知数据都呈正态分布状。这意味着我们在不清楚总体分布情况时，可以用正态分布来模拟。

好了，我们这里学习了三个重要分布。如果你看不懂或者记不住这三个分布的公式也没有关系，你只要知道每个分布的大概适应场景就可以了。实际的工作中，很多工具都能帮助我们分析，很少需要我们去具体推导。

排队的理论

上面谈到的三个分布经常被应用到排队理论中，而排队理论在性能工程方面是非常重要的。计算机系统中的很多模块，比如网络数据发送和接收、CPU 的调度、存储 IO、数据库查询处理等等，都是用队列来缓冲请求的，因此排队理论经常被用来做各种性能的建模分析。

排队论（Queuing Theory），也被称为随机服务系统理论。这个理论能帮助我们正确地设计和有效运行各个服务系统，使之发挥最佳效益。

排队论的系统里面有几个重要模块，比如顾客输入过程、队列、排队规则、服务机构等。几个模块之间的关系大体上可以用下面这张图来表示。

主要的输入参数是到达速度、顾客到达分布、排队的规则、服务机构处理速度和处理模型等。

排队系统的输出也有很多的参数，比较重要的是排队长度、等待时间、系统负载水平和空闲率等。所有这些输入、输出参数和我们进行的性能测试和优化都息息相关。

排队的模型有很多，平时我们用得多的有单队列单服务台和多队列多服务台。系统里面各个模块的模型都可以变化，排队论里面还有很多延伸理论。

总结

要想精通任何一门学问和工作，牢固的基础是必须的。对 IT 工作，包括设计系统、编写程序、系统维护和性能优化而言，牢固的数学基础会使我们的工作如虎添翼。这正如古人赞赏梅花时所说得：“不经一番寒彻骨，怎得梅花扑鼻香“。

我们也常说“根深才能叶茂”，今天讲的内容，包括概率统计和分布模型的知识，都是这样的基础和根基，希望你能牢牢掌握。

思考题

你们公司的系统和提供的服务中，有没有性能方面的指标要求？

这种指标要求是怎么表述的？比如是平均值，还是某些百分位数？

为什么要这样规定指标要求？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

06 | 性能数据的分析：如何从大量数据中看出想要的信号？

你好，我是庄振运。

我们这一讲来谈谈如何分析我们所得到的性能数据。现代的应用程序和互联网服务系统都比较复杂，要关心的性能参数也很多，所以你从各种渠道得到的性能相关的数据量往往很大。那么要如何从大量的数据中找出我们所关心的特征和规律呢？这就需要你对数据做各种分析和对比了。

这一讲要解决的核心问题就是：如何从大量数据中看出想看的信号？

当人沉浸在大量数据中时，是很容易迷失的。而“不识庐山真面目”的原因，当然是“只缘身在此山中”了。但这不能作为借口，我们需要练就“慧眼识珠”的本领，做到对各种性能数据一目了然，才能够做出一针见血的分析。

为了帮助你练就这样的本领，今天我们首先讲一下常见的算法复杂度和性能分析的目的，然后针对一个性能指标来分析，再延伸到对多个性能指标进行对比分析，最后谈谈进行数据分析的几个教训和注意点。

算法的时间复杂度

先简单地聊一下算法的时间复杂度（Time Complexity）。复杂度一般表示为一个函数，来定性描述该算法的期待运行时间，常用大 O 符号表述。

考虑程序和算法的时间复杂度时，大家通常关注的是某个解决方案属于哪个时间复杂度。具体来讲，有六种复杂度是比较普遍的，这里按照从快到慢的次序依次介绍：

常数时间，O(1)：判断一个数字是奇数还是偶数。

对数时间，O(Log(N))：你很熟悉的对排序数组的二分查找。

线性时间，O(N)：对一个无序数组的搜索来查找某个值。

线性对数时间，O(N Log(N))：最快的排序算法，比如希尔排序，就是这个复杂度。

二次时间，O(N^2)：最直观也最慢的排序算法，比如冒泡，就是这个复杂度。

指数时间，O(2^N)： 比如使用动态规划解决旅行推销员问题。这种复杂度的解决方案一般不好。

把这几个算法复杂度放在一张图中表示出来，你可以清楚地看出它们的增长速度。

大体上来讲，前四种算法复杂度比较合理，而后面两种（也就是 N 平方和指数时间）就不太能接受了，因为在数据量大的时候，运行时间很快就超标了。

性能数据分析的目的

说完常见的算法复杂度，我们再来看看做性能数据分析的目的是什么。

性能相关的数据有很多种，比如系统和模块的运行时间、客户访问延迟、客户滞留时间、服务吞吐率、程序的 CPU 占用时间等等。最常见的性能数据就是客户的访问延迟、吞吐量和运行时间。比如，我们如果去分析某个应用程序或者代码模块的运行时间，往往就会立刻暴露其相应的性能。

当收集到性能数据以后，我们首先需要判断它们的值到底是正常还是不正常，这就需要经验和知识才能判断。比如一个简单的数据库服务查询，一般端到端的延迟也就几百毫秒。如果这个值是 10 秒钟，那么就不太正常。发现不正常的数据，一般就需要做更多的分析和测试来发现原因，比如是网络延迟，数据库的服务延迟，抑或是其他问题。

再举一个例子，如果我们是在测量硬盘的访问时间，如果发现这个指标超过 1 秒，那么很可能磁盘这边出异常了。

如果观测到针对某个指标的一系列性能数据，那就需要判断这个指标有没有随着时间或者其他变量的变化而变差（Regression）和变好（Improvement），然后需要根据这个判断，进行进一步的分析及采取措施。

我们还经常需要对某个性能指标做预测。这种情况就需要研究数据的趋势（Trend）。根据情况做些预测分析，比如根据这个指标的历史数据来进行曲线拟合。

有时候某个性能指标会出现问题，比如如果应用程序有内存消耗和内存泄漏，那么我们就需要测量更多的相关指标来做深层的分析，来发现到底是哪个模块，哪行代码导致了内存泄漏。

我们也经常需要把几个性能指标的数据联系起来一起分析。比如发现系统的性能瓶颈在哪里？是在 CPU 的使用上、网络上，还是存储的 IO 读写上等等。如果应用程序和系统性能出了问题，那么考察多个性能指标的数据和它们的关系，可以帮我们做根因分析，发现真正的问题所在。

对一个时间序列的分析

一般来讲，一个性能指标，按照时间顺序得到的观测值，可以看作是一个时间序列。很多分析就是针对这个时间序列进行的。

首先需要指出的是，性能数据的时间序列往往不是均匀平滑的，反而会有各种有规律的峰值。比如，客户对一个网站的访问量是随着时间变化而变化的。每天 24 小时都不同，对多数互联网系统来讲，白天上班时间的访问量比较大；每周 7 天里面，工作日流量比较大；而节假日（比如新年）的流量又和其他时间不同。所以，需要你根据具体的情况来决定要不要做特殊的考虑。

再有一点，就是对网站而言，客户响应时间往往需要考虑百分位的数字，比如 P90、P95、P99，甚至 P99.9 的用户响应时间。因为这些数字可以保证一个系统的响应时间是不是满足了绝大多数用户的要求。

那么，针对一个性能数据的时间序列，我们要如何看数据的规律和趋势呢？

经常使用的方法是进行线性回归分析（Linear Regression）。线性回归是通过拟合自变量与因变量之间最佳线性关系，来预测目标变量的方法。线性回归往往可以预测未来的数据点。比如根据过去几年的每月消费支出数据，来预测明年的每月支出是多少。

注意所谓的“最佳”线性关系，是指在给定形状的情况下，没有其他位置会产生更少的误差。如下图所示，以平面点为例，如果有 N 个样本点，线性回归算法就是求一条直线 Y=f(X)。使得各点到这个曲线的距离的绝对值之和最小。

除了研究数据的趋势和未来预测，还有几种重要的分析，比如分类（Classification）、聚类（Clustering）以及决策树（Decision Tree）。分类是将类别分配给数据集合，帮助更准确地预测和分析。聚类是把相似的东西分到一组。决策树也叫分类树或回归树，每个叶节点存放一个类别，每个非叶节点表示一个特征属性上的测试。

对不同时间序列的分析

在一大堆性能数据面前，经常需要比较各个性能指标的时间序列来确定一个系统和服务的瓶颈，也就是最制约系统性能扩展的资源。

在多数情况下，瓶颈资源是常用的几种，比如 CPU、网络、内存和存储。但是有些情况下其他不太常见的资源也可能成为瓶颈，比如转换检测缓冲区 TLB（Translation Lookaside Buffer）（这个我们以后会讲到）。

如果几个时间序列在时间上是一致的，但是对应不同的性能指标，比如一个是 CPU 的使用率，另一个是吞吐率。我们有时候需要研究时间序列的相关性（Correlation of time series），从中可以得出很多有用的观察推断。

数据的相关性是指数据之间存在某种关系，可以是正相关，也可以是负相关。两个数据之间有很多种不同的相关关系。比如，我们经常需要计算两个随机矢量 X 和 Y 之间的协方差 cov（X, Y）（Covariance) 来衡量它们之间是正相关还是负相关，以及它的具体相关度。

数据分析的教训和陷阱

性能数据的分析并不容易，一不小心就会落入各种陷阱或者踩到坑。下面举几个需要特别注意的方面。

第一是数据的相关和因果关系。

有时候几个时间序列之间可以很清楚地看出有很强的相关性质，但是对它们之间的因果关系却不能判定。换句话说，通过单纯的数据分析可以证实数据的相关性，但是还需要其他知识才能更准确地判断谁是因、谁是果。这一点我们必须非常清楚，因为在很多性能问题讨论和根因分析的场合，我们非常容易武断地犯这样的错误，而导致走弯路。

更复杂的情况是有时候系统性能变坏，是因为几个指标互为因果，或者构成环形因果，也就是互相推波助澜。实际分析起来非常有挑战性，这就需要我们对整个系统和各个性能指标了如指掌。

第二是数据的大小和趋势。

面对性能相关的数据并判断它们是“好”还是“坏”是很难的。经常听到有人问一个问题：

“客户平均访问逗留时间多长比较好？”

这个问题没有一个简单的答案。这取决于每个网站的特性，以及我们想要实现的目标。对一个网站而言很正常的逗留时间，可能对另一个网站而言非常糟糕。在很多情况下，比单纯数字大小更重要的是数据的趋势，比如某个时期是上升还是下降，变化的幅度有多大等等。

第三是数据干净与否。

如果数据集合来自多个数据源，或者来自复杂的测试环境，我们需要特别注意这些数据里面有没有无效数据。如果不能剔除无效数据，那么整体数据就“不干净”，由此而得出的结论经常会“失之毫厘，谬以千里”。

第四是对性能数据内在关系的理解。

性能数据分析的核心，就是要理解各个性能指标的关系，并且根据数据的变化来推断得出各种结论，比如故障判别、根因分析。如果简单地把性能数据当作普通的时间序列来分析，那就往往没有抓住精髓。举个简单例子，Linux 系统的空闲内存其实就是一条时间序列，它或许显示快到 0 了，看起来性能问题出在这里。但是稍微了解 Linux 系统内存管理知识的人，就知道这个指标非常不可靠。

总结

性能工程和优化离不开对大量性能数据的研究和分析，那么如何“拨开云雾见天日”，看出里面的端倪和问题呢？我们这一讲就讨论了几种情况，包括对一个和多个时间序列的分析；也讨论进行数据分析的时候需要注意的地方。

古人讲“格物致知”；对我们来讲，性能数据就是我们要“格”的物。只有合理而系统地分析这些数据，才能收获“守得云开见月明”的恍然大悟之感。

思考题

对数据的统计分析和处理需要遵循科学的方法，否则，如果处理不当，根据数据得出的结论会严重误导你。回想一下过去的工作中，有没有这样的例子？从这些例子中有没有学到教训？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

07 | 性能数据的展示：一图胜千言，说出你的数据故事

你好，我是庄振运。

这一讲我们来探讨一下如何把性能数据合情合理地展示出来，让别人一目了然。如果你想有效地说服别人，那么你展示的过程就要像讲一个有趣的故事，娓娓道来，让别人爱听才行。

数据的展示根据场景有不同的目的，所以不能千篇一律，需要量体裁衣。每一种场景下，数据展示要根据你的具体目的、听众的特点和内容的特点而采用合适的图表。然后用这些图表做支持，把一个精美的数据分析的“故事”讲出来。

性能数据展示的目标和目的

我们先理清一下性能数据展示的目的是什么。

我们前面讲过，性能工作相对复杂，牵扯的模块和构件较多，而且有时候也要牵扯几个不同的部门。数据分析有时是为了性能优化，有时是为了根因分析等等。所以性能数据展示的目的也就有很多种，主要有如下三种：

向上级报告性能趋势和流量预测的结果；

向运维部门描述性能问题的根因分析；

向开发部门建议性能提升和代码优化。

目标不同，听众不同，如何展示数据自然也就不能相同。但不管如何，都要做到有根有据，条理清楚，层次分明。这样大家才会被你和你展示的数据所说服。具体来讲，你希望你的听众：

同意你的理论方法和过程；

信服你的推理分析；

理解问题的核心；

看出问题的根因；

同意你的建议和方案；

性能数据展示的挑战和难点

目的清楚了，下面就是怎么做了。但是我们必须认识到，相对于一般数据的展示，性能数据的展示有其独到的特点、挑战和难点：

数据量大 ：做性能测量时往往性能指标很多，每个指标一般都是一个时间序列。一个复杂系统 / 性能问题有几十甚至几百个性能指标是经常遇到的，所以总的数据样本经常以百万以上计。

数据复杂：几十或几百的性能指标互相交互和影响，需要考虑它们之间的复杂关系。

性能问题复杂：性能分析不是简单地死扣数据，还需要考虑互联网系统的协议，计算机的设计，和软硬知识的结合。

牵扯的模块多：很多性能问题都不是孤立的，都和其他模块和子系统，甚至客户请求有关系。

数据展示的经验

在考虑用什么方式来展示数据时，的确有些讲究，这就需要多借鉴别人的经验。在这方面，我总结了下面六条经验。

按客人来备菜：既然展示的听众不同，所以千篇一律的用同一种展示方法绝不可行。比如向领导和管理层汇报时，就要注重宏观层次，技术不要钻地太深。

有啥菜吃啥饭：要根据手头数据的特点来决定展示方法。比如你有好几组相关的数据，有很有趣的趋势，那就要用线图来展示趋势和相关性。

给足上下文：很多工程人员容易犯的错误，就是想当然地以为别人也都了解问题的背景，然后一上来就展示很多细节。其实除了你自己，没有第二个人更了解你要解决的问题和要展示的数据，所以一定要给足背景介绍和上下文信息。

用图讲故事：人人喜欢听故事，而且是有趣的故事。如果你能把整个分析和推理的过程，变成一个引人入胜和图文并茂的故事来讲，那我保证你的展示会非常地成功。

和听众交互：尽量鼓励听众参与讲故事的过程。有两种类型的数据叙事：叙述型和探索型。叙述型是通过描述告诉观众具体结论；而探索型是鼓励观众探索数据以得出结论。虽然探索型叙事稍微多花一点时间，但若能成功运用，听众更容易被说服；因为结论是他们自己得出的，而不是你告诉他们的。

总结重要点：在数据展示的最后，一定要简洁明了地总结你的展示。比如你希望听众最后只记住三句话，是哪三句呢？根据你的展示目的，这三句或是相关数字，或是趋势，或是问题本质，或是解决方案。

数据图表的种类

数据往往是枯燥的，所以我们要想办法变枯燥未有趣。用图表来展示数据就是一个好方法，但关键是要用合适的图表来展示不同的数据。

虽然数据图表的种类很多，但常用的就 10 种左右。我来简单地给你总结一下这几种常用的图表，分别适合什么情况和表示什么样的数据。

表格（Table）

表格你一定经常用，它的优点是可以结构化的方式显示大量信息；不幸的是，这个优点恰恰也是缺点。很多用户其实对数据的趋势比具体的数值更有兴趣，这种情况下用表格就不是那么直观。

线图（Line Chart)

如果我们希望针对一个变量，显示一段时间内这个变量的变化或趋势，线图就最合适了。比如我们经常表示的时间序列图就是最直白的线图。

另外，它也适用多个变量的情况，可以很直观地显示两个或多个变量之间的关系，比如趋势和相关性。

PDF 和 CDF 图

对于一个变量的概率数据，我们经常会显示这两个互相关联的图。二者可以看作是稍微特殊的线图。

PDF：概率密度函数（Probability Density Function）是连续型随机变量的概率密度函数（或简称为密度函数）。就是一个描述这个随机变量的输出值，在某个确定的取值点附近的可能性的函数。

CDF: 累积分布函数 (Cumulative Distribution Function)，又叫分布函数，是概率密度函数 PDF 的积分。换句话说，PDF 就是 CDF 的导数。CDF 能完整描述一个实随机变量 X 的概率分布。

我们举一个前面讲过的例子，比如标准正态分布。它的 PDF 和对应的 CDF 是这个样子。

面积图（Area Charts）

面积图类似于线图，但两者之间有细微的差别。面积图的重点是阴影线下方的区域。

如果想用面积图表示几组数据，那么具体也有两种选择：可以选择采用叠加面积图或非叠加面积图。

比如下图就是一个叠加面积图，它显示了几个产品在不同季度的单季产量和总产量。

柱状图和条形图（Bar Charts）

用于比较不同类别的数量，比较容易理解。如果你想表示好几个变量，那么也可以选择叠加方式来显示。

比如下面这个柱状叠加图，显示的是和上面的面积图同样的数据，只是表示的方法不同。

散点图（Scatter Plots）和气泡图（Bubble Charts）

散点图显示沿两个轴绘制的两个变量的值，用点的模式揭示它们之间存在的任何相关性。比如两个变量分别是 CPU 使用量和客户吞吐率，那么我们可以期望散点图会显示比较强的同一趋势。

气泡图类似于散点图，但它可以显示三个数据项之间的变化。除了两个变量分别为是横轴和纵轴外，气泡的大小代表第三个变量。

我们举一个例子。比如一个公司里面有十个互联网产品，每个产品运行在不同数目的服务器上面。这些产品分别有不同的内存使用率和网络使用率。假设我们想分析一下产品的内存和网络的使用情况，然后决定下一个季度购买什么样的服务器。我们就可以用气泡图来表示，网络和内存的使用率来作为横轴和竖轴，气泡的大小代表每个产品的服务器数目。

通过这个气泡图，我们非常直观地发现，某个产品的气泡比其他产品都大，而且它的两种资源使用率都很低。整体来言，下个季度新采购的服务器可以不需要那么多内存和网络资源。

饼图和圆环图（Pie Charts， Donut Charts）

当需要显示比例数据或者百分比时，饼图最佳。由于饼图表示零件与整个实体之间的大小关系，因此零件需要总和必须为有意义的整体。

一个适合饼图表示的性能数据例子是客户请求的来源分布。但你在使用时需要注意的是，饼图最好用来显示六个或更少的类别。如果太多类别的话反而描述不清楚。

举一个前面讲面积图时候用过的例子，公司的四种产品在春季的销售分布，就可以用下面这个饼图表示。

树形图（Treemaps）

树形图对于显示类别和子类别之间的层次结构和比较值非常有用，同时也能保留较多细节。树形图可以帮助我们很直觉地感知哪些区域最重要。另外，还可以通过将颜色编码的矩形嵌套在彼此内部，来更好地实现目的，并用加权以反映它们在整体中的份额。

比如下面这个树形图，它描绘了一个国外的产品，采用不同营销渠道的价值，然后按国家 / 地区细分。

这里的营销渠道包括 Email、Social Media、AdWords 等。从这个图中，你一眼就能看出从营销渠道上来看，AdWords 是最成功的营销渠道，因为它对应的面积最大。但是从国家层面来讲，美国（United States）是所有渠道中最有价值的目的地，同样是因为对应的面积最大。

热图（Heatmaps）

热表以表格格式来表示数据，其中每个格子是用颜色，而不仅仅是用数字来展示。这些颜色分别对应包含定义的范围，如绿色代表小的值，黄色代表一般的值和红色代表大的值。

比如分析几台服务器的在一天里面 CPU 的使用率。我们可以把每台服务器在每个时段的 CPU 使用率分为小、较小、中等、较大和大等几个范围。然后相应地着色。

我们看下图，横坐标代表时段，纵坐标是服务器，每个值就是 CPU 使用率。

虽然每个使用率其实是数字，但是把数字编码为颜色的好处是，颜色编码格式使数据更容易理解。比如，我们马上就可以看出早上 10 点的时候，服务器 1 和 5 的 CPU 使用率很高，因为它们的格子是红色。

总结

我们都学过宋朝的词人柳永的词，他有两句词是这么写的：

“便纵有千种风情，更与何人说？”

这里他其实是在感叹自己虽有满腹经纶和深厚情感，但无人可以诉说。我们虽然不是词人，但在做过性能测试或者根因分析后，可能经常会有很多新奇的发现，希望向同事和领导讲解。

但是性能问题往往牵涉很广，描述起来并不容易，别人可能很难理解。这时候我们的感觉就是“我有千种风情”，可是如何说啊？

希望通过这一讲，能让你对在图文并茂地来“诉说”这方面能有些提升。

思考题

对本讲介绍的几种图形，你对哪些比较熟悉并经常使用？

哪些是不熟悉的但觉得可能会很有用的？

对不熟悉的图形，你能不能花十几分钟时间去仔细学习一下并尝试使用呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

08 | 经验总结：必须熟记的一组常用性能数字

你好，我是庄振运。

今天这一讲是”数理基础“这一部分的最后一讲，我在这一讲会给你总结一组性能相关的常用的数字。这些数字在做性能和设计方面的工作时经常用到。它们就像九九乘法表一样，我希望你能熟记在心。

记住这些数字的好处是，每次看到一个性能相关的数据的时候，我们立刻就能知道这个性能数据有没有问题。

举个简单例子，如果我们看到一个硬盘的 IO 读写延迟经常在 500 毫秒左右，我们立刻就知道这里面有性能问题。反之，如果硬盘 IO 读写延迟小于 1 毫秒，我们可以马上推断——这些 IO 读写并没有到达硬盘那里，是被操作系统缓存挡住了。这就是大家常说的“对数字有感觉”。

人们常说“腹有诗书气自华”。同理，如果我们能对系统和程序运行中常见的性能指标了如指掌，就有可能达到那种“一眼就看出问题”的大师级别。

为了方便理解和记忆，我把这些数字分成几大类，分别是存储、CPU、操作系统、内存和网络等，并且会给出具体的单个数值。

有言在先

但是我必须强调说明的是，我之所以给出具体的单个数值，是为了方便你记忆，并让你对性能指标“有感觉”。因为单个值比给出数值范围更直观。

比如传统硬盘的 IO 延迟，如果我冠冕堂皇地说：“IO 延迟的大小取决于很多因素，比如硬盘型号、IO 大小、随机还是连续、磁头离数据的远近等，从小于 1 毫米到几秒钟不等。“这样的说法当然对，但是并不能帮助你找到数字的感觉，所以直观指导意义不是很大。

所以我想强调，我给出的数字仅供参考，帮助你记忆和理解，更重要的目的是让你对不同环境下的性能数据有所感觉。你要更加注重它们之间的量级比较，比如 SSD 的随机 IOPS 的性能，可以轻松地达到普通硬盘 HDD 的 1000 倍以上。

至于具体的性能数字的值大小，却也可能对一个非常具体的场景不那么匹配。这有几个原因：

因为市场上有很多种类和很多厂家的产品，具体的值都不一样，比如 SSD 的一些性能指标就是如此。

因为每个具体场景都不同，比如 IO 的读写大小。还有就是我们的技术不断进步，就如同 CPU 的频率，具体的值是一直在变的。

这些性能数据多半和延迟有关，所以要弄清楚这些延迟的单位。你应该都知道，一秒钟是 1000 毫秒（ms），一毫秒是 1000 微秒（us），一微秒是 1000 纳秒（ns）。

存储相关

我们先看存储相关的性能数据。存储有很多种，常用的是传统硬盘（HDD, Hard Drive Disk）和固态硬盘（SSD, Solid State Drive）。硬盘的厂家和产品多种多样，而且具体的配置也有很多种，比如大家熟悉的磁盘阵列（RAID)。我们这里仅仅选取最普遍的硬盘和最简单的配置。

值得一说的是 SSD。最近几年，SSD 的技术发展和市场演化非常迅速。随着市场规模的增大和技术的进步，SSD 的价格已经极大地降低了。在很多大规模的在线后台系统中，SSD 几乎已经成了标准配置。

SSD 的种类很多，按照技术来说有单层（SLC）和多层（MLC，TLC 等）。按照质量和性能来分，有企业级和普通级。根据安装的接口和协议来分，有 SAS、SATA、PCIe 和 NVMe 等。

对所有的存储来说，有三个基本的性能指标。

IO 读写延迟。一般是用 4KB 大小的 IO 做基准来测试；

IO 带宽，一般是针对比较大的 IO 而言；

IOPS，就是每秒钟可以读写多少个小的随机 IO。

下面这个表格列出了几种存储介质和它们的性能数值。

我们这里考虑三种情况： 传统硬盘，SATA SSD 和 NVMe SSD。你可以看到，一般传统硬盘的随机 IO 读写延迟是 8 毫秒的样子，IO 带宽大约 100MB 每秒，而随机 IO 读写一般就是每秒 100 出头。

SSD 的随机 IO 延迟比传统硬盘快百倍以上，IO 带宽也高很多倍，随机 IOPS 更是快了上千倍。

CPU 和内存相关

再来看看 CPU。说起 CPU 相关的性能数字，就必须先说 CPU 的时钟频率，也就是主频。主频反映了 CPU 工作节拍，也就直接决定了 CPU 周期大小。

主频和周期大小。比如基于英特尔 Skylake 微处理器架构的 i7 的一款，其主频为 4GHz，那么每一个时钟周期（Cycle）大约0.25 纳秒（ns）。

CPU 运行程序时，最基本的执行单位是指令。而每一条指令的执行都需要经过四步：指令获取、指令解码、指令执行、数据存入。这些操作都是按照 CPU 周期来进行的，一般需要好几个周期。

CPI 和 IPC

每个指令周期数 CPI 和每个周期指令数 IPC 其实是孪生兄弟，衡量的是同一个东西。

CPI（cycles per instruction）衡量平均每条指令的平均时钟周期个数。它的反面是 IPC（instructions per cycle）。虽然一个指令的执行过程需要多个周期，但 IPC 是可以大于 1 的，因为现代 CPU 都采用流水线结构。一般来讲，测量应用程序运行时的 IPC，如果低于 1，这个运行的系统性能就不是太好，需要做些优化来提高 IPC。

MIPS

MIPS 就是每秒执行的百万指令数。

我们经常会需要比较不同 CPU 硬件的性能，MIPS 就是一个很好的指标，一般来讲，MIPS 越高，CPU 性能越高。MIPS 可以通过主频和 IPC 相乘得到，也就是说 MIPS= 主频×IPC。这个很容易理解，比如一个 CPU 频率再高，IPC 是 0 的话，性能就是 0。假设一个 CPU 的主频是 4GHz，IPC 是 1，那么这个 CPU 的 MIPS 就是 4000。注意的是，MIPS 是理论值，实际运行环境数量一般小于这个值。

CPU 缓存

一般 CPU 都有几级缓存，分别称为 L1、L2、L3，按这个顺序越来越慢，也越来越大，当然成本也越来越低。L3 有时候也称为 LLC（Last Level Cache），因为 L3 经常是最后一级缓存。多核 CPU 的情况下，一般 L1 和 L2 在核上，而 L3 是各个核共享的。

我用下面的表格来表示一款 2GHz 主频的 CPU，进行寄存器和缓存访问的一般延迟，分别用时钟周期数和绝对时间来表示，同时也给出在每个 CPU 核上面的字节大小。重复一下，数字仅供参考，因为每款 CPU 都不同。

比如一般 L3 的访问需要 40 个时钟周期，2GHz 主频的话就是 20 纳秒，大小一般是每个核平均下来 2MB 的样子。

为了方便对比，我们把内存的性能也放在同一个表格里。

值得一提的是现在的 NUMA（非统一内存访问，Non-Uniform Memory Access）处理器会有本地和远端内存的区别，当访问本地节点的内存是会快一些。

操作系统和应用程序相关

我们刚刚谈了硬件方面，下面看看软件，也就是操作系统和应用程序。

首先，你需要弄清楚如下的几个重要概念和指标。

1. 指令分支延迟

CPU 需要先获取指令，然后才能执行。获取下一条指令时需要知道指令地址，如果这个地址需要根据现有的指令计算结果才能决定，那么就构成了指令分支。CPU 通常会采取提前提取指令这项优化来提高性能，但是如果是指令分支，那么就可能预测错误，预先提取的指令分支并没有被执行。

指令分支判断错误（Branch Mispredict）的时间代价是很昂贵的。如果判断预测正确，可能只需要一个时钟周期；如果判断错误，就需要十几个时钟周期来重新提取指令，这个延迟一般在 10 纳秒左右。

2. 互斥加锁和解锁

互斥锁 Mutex（也叫 Lock）是在多线程中用来同步的，可以保证没有两个线程同时运行在受保护的关键区域。使用互斥锁的时候需要加锁和解锁，都是时间很昂贵的操作，每个操作一般需要几十个时钟周期，10 纳秒以上。

3. 上下文切换

多个进程或线程共享 CPU 的时候，就需要经常做上下文切换（Context switch）。这种切换在 CPU 时间和缓存上都很大代价；尤其是进程切换。在时间上，上下文切换可能需要几千个时钟周期，1 微秒（1us）级别。在缓存代价上，多级 CPU 缓存和 TLB 缓存都需要恢复，所以可能极大地降低程序线程和进程性能。

网络相关

互联网服务最终是要面向终端客户的，客户和服务器的延迟对用户的服务体验至关重要。

网络的传输延迟是和地理距离相关的。网络信号传递速度不可能超过光速，一般光纤中速度是每毫秒 200 公里左右。如果考虑往返时间（RTT，Round Trip Time），那么可以大致说每 100 公里就需要一毫秒。北京到深圳约 2,000 公里，RTT 就是 20 毫秒；上海到乌鲁木齐或者美国的东西海岸之间距离差不多 4,000 公里，所以 RTT 是 40 毫秒左右；中国到美国（比如北京到美国西海岸旧金山）差不多 10,000 公里，RTT 就是 100 毫秒。

在数据中心里面，一般的传输 RTT 不超过半毫秒。如果是同一个机柜里面的两台主机之间，那么延迟就更小了，小于 0.1 毫秒。

仔细想想的话，你就会发现直线距离本身还不够，因为数据是通过骨干网光纤网络传播的。如果光纤网络绕路的话，那么实际的 RTT 会超过以上估算数值。

另外要注意的是，传输延迟也取决于传输数据的大小，因为各种网络协议都是按照数据包来传输的，包的大小会有影响。比如一个 20KB 大小的数据，用 1Gbps 的网络传输，仅仅网卡发送延迟就是 0.2 毫秒。

下面这个表格就总结了几种环境下的端到端的距离和 RTT。

总结

今天讲了几十个平时经常用到的性能数字，希望起到抛砖引玉的效果。你可以在此基础上，在广度和深度上继续扩展记忆。

宋代诗人苏轼曾经作诗夸奖朋友：“前身子美只君是，信手拈来俱天成”，这里的“子美”是唐朝大诗人杜甫的字。这两句是夸朋友写文章写得好，能自由纯熟的选用词语或应用典故，用不着怎么思考，不必费心寻找，如同杜甫转世。

我们如果对各种性能数据足够熟悉，如掌上观纹，自然也就能达到那种对性能问题的分析信手拈来的境界。

思考题

假设你们公司有个互联网服务要上线，服务的要求是，用户端到端响应时间不能超过 40 毫秒。假设服务器在武汉，那么对上海的用户可以达到响应时间的要求吗？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

09 | 性能测试的种类：如何快准狠地抓住一个测试的本质？

你好，我是庄振运。

从这一讲开始我们讨论性能测试。性能测试是一种特殊的软件测试，它的目的是确保软件应用程序在一定的负载流量下运行良好。性能测试是性能分析和性能优化的基础，它的目标是发现和性能相关的各种问题和性能瓶颈，从而进一步去消除错误和性能瓶颈。

由于性能测试本身就有好多种类；加上各种测试之间的界限其实很模糊，这就造成了很多人理解上的混乱。

比如大家在工作讨论时，经常说做性能测试，但对于做什么样的“性能测试”，每个人有不同的看法，而且又经常表达不清。这就造成来交流不畅，甚至是误解，从而严重地影响了工作的速度。我见过很多次因为对性能测试定义和交代不清，造成了老板和员工之间 / 员工和员工之间的理解误差。

性能测试的种类颇多，各自有不同的测试目的、测试环境、负载等等；这里面最重要的是测试目的和负载的大小变化。我们这一讲就一起来分一下类。

性能测试的分类方式

性能测试如何分类呢？我们需要从几个方面来看，包括测试目的、测试环境、负载流量、测试对象、负载数据、黑盒白盒等。

测试目的

测试目的是最重要的方面。大体上有几种目的：

测量服务速度（Speed）：确定程序是否能够快速地响应用户的请求，这个服务速度一般包括延迟和吞吐率两个指标。速度通常是应用程序最重要的属性之一，因为运行缓慢的应用程序容易丢失用户。

测量可扩展性（Scalability）：确定应用程序是否可以在用户负载和客户流量增大情况下还能正常地运行。

测量稳定性（Stability）：确定在各种极端和恶劣环境下，应用程序是否能稳定运行。

测量性能瓶颈（Performance Bottleneck）：性能瓶颈是应用程序和系统中的最影响整体性能的因素。瓶颈是指某个资源不足而导致某些负载下的性能降低。一些常见的性能瓶颈是 CPU、内存、网络、存储等。

测试环境

性能测试的环境也有几种，主要是开发环境还是生产环境。开发环境里面更多的是简单的测试来发现一些明显问题，而生产环境测试一般是开发环境测试通过后才进行的。

负载流量

根据测试的负载大小来分：是小流量，正常流量，还是超大流量。除了大小，负载变化的速度也需要考虑。

测试对象

测试的对象可以是只针对一个代码功能，或是整个代码模块，亦或是整个系统。

负载数据

测试的负载数据可以是真正的生产环境中的请求和数据，比如终端客户的网站请求和上传数据，也可以是人工模拟出来的请求及数据。

黑盒白盒

如果把被测试的对象当作一个整体，不关心它的内部工作机理，也就是把它当作一个黑盒子，那么这种测试就是黑盒测试。反之，如果你也关心它的内部构件和内部设计，就是把它当作白盒子来测试。

性能测试的种类

我们现在看看你可能经常会提到的各种测试，包括负载测试、容量测试、压力测试、断点测试、瓶颈测试、尖峰测试、耐力测试、基准测试、可扩展性测试和冒烟测试这 10 种。我分别说说它们是什么特点，尽量用刚刚讲过的分类方式归归类，并且适当举例说明。

需要说明的是，业界对于不同的测试类别其实也是各说各道，没有特别统一而严格的定义。我也是尽我所能，根据我的经验和实践来帮你理一理。

因为性能测试的种类多，我尽量把它们按照某种方式归类一下，帮助你理解和记忆。大体上可以按照负载流量的大小分成三类：低流量、中等流量和高流量。这里的流量高低是相对于生产环境中的流量而言的。当然，它们的实际界限其实很模糊。

冒烟测试（Smoke Testing）

冒烟测试是开发人员在开发环境里执行的简单测试，以确定新的程序代码不出故障。冒烟测试目的是确认系统和程序基本功能正常。冒烟测试的执行者往往就是开发人员，但有时也让运维人员参与。

耐力测试（Endurance Testing）和浸泡测试（Soak Testing）

耐力测试（或者耐久测试）有时也叫浸泡测试，是一种非功能性测试。耐力测试是长时间测试具有预期负载量的系统，以验证系统的行为是否正常。举一个例子，假设系统设计工作时间是 3 小时；我们可以对这一系统进行超过 3 小时的测试，比如持续 6 小时的耐力测试，以检查系统的耐久性。

执行耐力测试最常见的用例是暴露某些不易重现的问题，如内存问题、系统故障或其他随机问题。

这里的偏重点是测试时间，因为有些程序和系统的问题只有在长期运行后才暴露出来。一个最明显的例子就是内存泄漏。一个程序或许短时间内运行正常，但是如果有内存泄漏，只要运行时间足够，就一定会暴露出这个问题。

基准测试（Benchmark Testing）/ 性能回归测试（Performance Regression Testing）

基准测试或者性能回归测试是着重“前后”对比的测试。

这种测试往往是开发过程的一部分，一般不需要具体的性能要求。代码的演化过程中经常需要确保新的代码不会对整个模块或系统的性能产生任何不好的影响。最简单的方法是对代码修改前后进行基准测试，并比较前后的性能结果。执行基准测试的重点是保证前后测试环境的一致，比如负载流量的特征和大小。

负载测试（Load Testing）

负载测试用于验证被测试系统或者程序是否可以处理预期的负载流量，并验证正常和峰值负载条件下的系统和程序行为。这里的负载可以是真正的客户请求，也可以是仿真的人工产生的负载。

我认为负载测试的定义有时太广泛和模糊，很多其他测试都可以看作是负载测试的一种，比如马上就要讲到的容量测试，其实就是一种负载测试。

断点测试（Breakpoint Testing）

断点测试类似于压力测试或者容量测试。这种测试的过程是随着时间的推移而增大流量负载，同时监视系统的预定故障条件。

断点测试也可以用来确定系统将达到其所需规范或服务水平协议的最大容量，并且自动采取措施来纠正或者缓解。比如云计算环境中，我们可以设置某种性能断点，用它们来驱动某种扩展和伸缩策略。

比如一种性能断点可以是根据用户的访问延迟。如果延迟性能测量的结果是已经超过预定的阈值，就自动进行系统容量调整，比如增加云计算的服务器。反之，系统容量也可以根据断点的规则来减少，以节省成本。如下图所示。

尖峰测试（Spike Testing）

尖峰测试用于确定系统在负载（比如用户请求数）突然变化时的系统行为。这种测试是通过突然增加或减少由用户产生的负载来观察系统的行为。

测试的目标是确定性能在这样的场景下是否会受损，系统是否会失败，或者是否能够处理负载的显著变化。尖峰测试的核心是负载变化的突然性，所以也算是一种压力测试。

可扩展性测试（Scalability Testing）

可扩展性（或者叫可伸缩性）测试用于确定一个程序和系统的非功能性特征能不能在变化的环境里合理扩展。这里的环境变化包括系统环境的变化、负载量的大小、请求的多样性、数据量的大小等。

在系统环境变化时，同步的测量和观察各种性能指标，并进行数据的分析，从而确定在各种环境下被测试系统的可扩展性。如下图所示。

这个测试的主要目的是了解系统在什么样的环境中，以及什么样的变化会导致系统不能扩展。发现这些环境后，可以进一步有针对性的分析和加强。

容量测试（Capacity Testing）

容量测试（或者叫体积测试，Volume Testing）是用于确定一个单位容量能够支持的最大负载。比如一个程序运行在某种服务器上，我们有时需要知道每台服务器能够支持的最大负载（例如客户数），从而决定需要部署多少台服务器才能满足预定的总负载要求。

容量测试一般是会不断增大负载，并且不断地测量各种性能指标。在性能目标变得不可接受之前，系统和程序可以成功处理的负载大小，就是单位容量可以承担的负载。为了尽量让得到的结果匹配实际生产环境，采用的负载流量最好是真正的生产环境的请求和数据。

正常生产环境中的流量和数据或许不够大到让一台服务器超载，因此我们需要解决这个问题。很多公司的解决方案是把其他服务器上的请求重定向到某一台被测试服务器，从而让这台服务器适度超载。这种机制我后面会用一讲专门讨论。

容量测试是确保系统稳定的重要一环。只有进行彻底的容量测试，并有相对应问题的解决方案，才可以使我们能够避免将来出现潜在的超载问题，例如增加的用户数或增加的数据量。

如下图所示，容量测试至少包含三个部分：可调节的流量负载、性能的测量、可以接受的性能指标。这三个部分一起就可以决定单位容量（比如一台服务器）的最大负载容量。这个数据可以帮助我们做各种决策，包括预估系统能负担的总负载；或者根据预期负载来决定部署多少台服务器。

瓶颈测试（Bottleneck Testing）

瓶颈测试其实可以看作一种特殊的压力测试。它的目的是找到被测试系统和程序的最制约的资源类型（比如 CPU 或者存储）。瓶颈测试并不局限于只找到最制约的一个瓶颈，它也可以同时找多个性能瓶颈。

找多个性能瓶颈的的意义主要有两点：

如果最制约的瓶颈资源解决了，那么其他制约资源类型就自动会成为下一个瓶颈，所以需要未雨绸缪。

系统设计时可以考虑在几个资源之间做些平衡，比如用内存空间来换取 CPU 资源的使用。

压力测试（Stress Testing）

压力测试也是一种负载测试，不过它偏重的是在负载增加到超过系统设计预期后观察和验证系统的行为。当我们通过增加负载，对系统施压到超出设计期望的负载时，就能发现哪个模块或组件首先因超载而失败。这样我们就可以通过提升失败组件的性能来设计出更健壮、性能更优的系统。

相对于容量测试，压力测试的目的是为了暴露系统的问题，因此采用的负载不一定是真正的生产数据和客户请求。

总结

这一讲重点讲了几种性能测试。

在工作中和别人交流时，你一定还会听到各种不同叫法的性能测试。我从业多年的总体感觉就是，性能测试的种类太多，甚至对某一种测试怎么进行也众口纷纭。给人的感觉，就像古诗里面所说的，“乱花渐欲迷人眼”。

虽然各种性能测试叫法不一，但万变不离其宗，你只要主要抓住几点就行，比如分类的方式，流量的大小和测试的目的。

希望通过本讲的讨论，你能对不同的性能测试之间的区别更清楚一些了，以后工作交流和阅读文献时能搞清楚它们是什么种类的测试，从而对症下药，做好测试规划和合理的分析。

思考题

今天讲了这么多种测试，你平时用过几种？

对于没有用过的测试种类，你清楚了解它们的使用场景吗？

如果有的测试种类看起来还挺有用但自己没有试过，尝试着设计一个，并在工作中表现一下，把这种测试和其他已经有的测试种类的区别好好分析一下，让同事们和领导知道你才是性能测试专家！

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

10 | 性能测试的规划和步骤：为什么性能测试不容易一蹴而就呢？

你好，我是庄振运。

上一讲我们讲了不同类型的性能测试。今天我们来讲如何规划一个性能测试和具体的执行步骤。在规划任何一种性能测试时，最重要的事情是搞清楚被测试的实体，也就是 SUT（System Under Test），对应的性能指标和度量，以及期望的结果。在此基础上，根据测试的类型来决定和规划具体的测试步骤，然后执行测试，最后再合理地分析测试的结果。

为方便描述，我们用下图来表示整个性能测试的过程，总共七个部分。

大体上分为前后两大部分。前面四个部分分别是：决定 SUT、决定性能指标、决定指标的度量、决定期望结果。后面三个部分是性能测试的规划、测试的执行和结果分析。这三个部分根据测试的结果或许需要重复多次。

搞清楚测试对象

性能测试当然首先要搞清楚测试对象。但说起来有意思，我看到过很多做性能测试的人，对“什么才是他的测试对象”这个问题糊里糊涂的。经常碰到的情况是，有些人做了一大堆测试，但后来发现搞错测试对象了，所以大量的工作白做了。

为什么会导致这样的误会呢？

因为一个被测试的系统往往是复杂的，包含多个子系统和模块。如果对测试的类型和规划没有搞透彻，就很容易搞不清真正的测试对象。

测试的对象一般叫 SUT（System Under Test），它可以是一段代码、一个模块、一个子系统或者一个整个的系统。比如要测试一个在线互联网服务的性能，那么这整个系统，包括软件、硬件和网络，都算是 SUT。再比如，SUT 也可以是一个子系统，比如运行在某台服务器上的一个进程。

搞清楚 SUT 的重要之处，是让测试做到有的放矢。除了 SUT 本身，其他所有的模块和构件在整个性能测试的过程中都不能有任何性能瓶颈。

比如测试一个在线服务，那么所使用的负载和流量模块就不能成为瓶颈。如果这一点不能得到保证，那么性能测试得出的数据和结论就是不正确的。拿上面的互联网服务来举个例子，如果性能测试中负责产生负载流量的模块成了瓶颈，一秒钟只能发出一千个请求，那么你测出的吞吐量最多也就每秒一千请求。这样的结果和结论显然是不对的。

决定测试的性能指标

搞清楚测试对象 SUT 之后，下一步就是决定具体的性能指标。

对一个面向终端客户的 SUT 而言，一般就是和客户直接相关的性能指标，比如客户（端到端的）服务延迟。如果 SUT 是系统中的某个模块，那么测试的指标有可能是资源的使用率，比如 CPU 或者内存使用率。

平时用的最多的性能指标有三个，就是服务响应时间，服务吞吐量和资源利用率。这三个指标各有侧重，分别对应了终端客户、业务平台以及容量系统。通常，响应时间是用户关注的指标，吞吐量是业务关注的指标，资源利用率是系统关注的指标。

决定测试指标的度量

决定性能指标后，还需要更加具体到统计上的度量。比如你关注的是平均值，还是百分位数（例如 P99）；也或许是某个置信区间的大小。

举例来说，对服务响应时间延迟的指标而言，一般需要同时考虑平均值、中位数和几个高端的百分位数，比如 99 百分位。

决定性能测试的期望结果

SUT 和性能指标都确定了，那么下一步就是决定我们期望从测试中得出什么样的结论，比如是为了确认 SUT 的性能满足一定的指标呢，还是只希望获取一些性能数据做参考。搞清楚了测试的期望结果，才能决定什么样的测试结果是可以接受的，什么样是不能接受的。

假设 SUT 是一个互联网服务，测试指标是端到端的平均服务延迟。我们或许已经知道可以接受的平均服务延迟的最大值，比如 500 毫秒。如果性能测试测出的结果显示平均服务延迟是 600 毫秒，那么这个测试结果显然是负面的，就是被测互联网服务不够好，不能接受。如果只是想获取性能数据，那么这个 600 毫秒就是测试结果。

再举几个更复杂一点的测试期望结果的例子：

当 2000 个用户同时访问网站时，所有客户的 P99 响应时间不超过 2 秒；

测试应用程序崩溃前可以处理的最大并行用户数；

测试同时读取 / 写入 500 条记录的数据库执行时间；

在峰值负载条件下，检查应用程序和数据库服务器的 CPU 和内存使用情况；

验证应用程序在不同负载条件下，比如较低、正常、中等和重载条件下的响应时间。

性能测试的规划

一个成功的性能测试离不开具体的规划，比如如下的几个方面的重要内容，包括负载流量的特征、负载如何注入、测试的数据、黑盒还是白盒测试、测试的工具、测试的环境等，我们逐一说明。

负载流量的特征和我们上一讲讲过的测试类型直接相关。首先我们需要决定是用真正的生产环境的负载还是仿真的负载。

那么负载如何注入呢？负载流量即使已经确定用真正的生产负载，还需要继续决定几个问题：

是用实时的流量呢，还是用过去捕捉的流量来重新注入？

流量的大小，是完全模拟生产环境呢，还是加大负载。

如果不使用实时生产流量，那么如何注入呢？

很多情况下，直接采用开源的工具就够了，但有些情况下需要自己开发或者对开源工具进行二次开发。

很多负载需要操作数据，比如数据库查询。所以，我们就需要决定测试的数据，是用真正的用户数据还是仿真的数据。

测试选黑盒还是白盒呢？黑盒就是不改变 SUT，完全做被动观察。白盒就是允许改变 SUT，比如在程序中间输出更多的性能日志信息等。白盒的问题就是改变了 SUT 的行为，可能导致最终得到的数据失真。

市场上有各种各样的性能测试工具，比如 JMeter，但是选择什么样的测试工具将取决于许多因素，例如支持的协议类型、许可证成本、硬件要求、平台支持等。我们后面会有一讲专门讲各种工具。

配置一个合适的测试环境很重要，理想情况下，应该尽量用与生产平台相同的硬件、路由器配置、网络，甚至是网络背景流量等。不过值得说明的是，有时候我们会特意选取和生产环境不同的测试环境，比如当我们希望提高可重复性，降低测试环境的噪音；那么我们就会选取一个单独的不受干扰的环境来测试。

性能测试的执行

测试规划完毕后就是执行了，这个过程相对简单。

但是需要强调的是，测试结果的可重复性非常重要。性能测试和性能优化很多情况下是一个长期的行为，所以需要固定测试性能指标、测试负载、测试环境，这样才能客观反映性能的实际情况，也能展现出优化的效果。

很多性能测试比较复杂，所以不要期望一次测试就能成功让整个测试环境工作。经常需要实验好几次才能真正让整个测试环境搭配成功。

所以，复杂的性能测试需要多次迭代执行，一般有以下几种方式迭代：

分步进行：把复杂的测试验证过程分成几步，一次验证一步，最后一步是整个完整的测试。这样的好处是每一步的问题都可以及早暴露，快速解决。

先短时间测试，再长时间测试：有些测试需要执行很长时间，比如一周。如果一周后才发现测试过程有错误，那就浪费了一周时间。所以，为了避免浪费时间，会先进行短期测试，比如半小时。然后分析结果，来发现其中的问题。这样可以比较快速地纠正测试中的错误。

模拟测试：在实际使用负载测试之前，先执行简单的负载测试以检查各种工具的正确性。

分析测试结果

测试完毕，就需要分析测试结果了。

如果对一次测试的结果我们不满意，我们就需要重新回到以前的步骤上，或者重新执行测试的步骤，或者重新规划测试的方法。

根据我的经验，几乎所有的性能测试，就算是看起来非常简单直白的测试，都需要反复进行多次，才能达到满意的效果。 所以如果发生这样的情况，你千万不要气馁。

为什么性能测试不容易一蹴而就呢？

这是因为任何测试，其实都依赖于很多其他模块，比如流量的产生、数据的注入、环境的搭建、干扰的排除、数据的收集、结果的稳定等，这些模块都不简单。所以寄希望于“毕其功于一役”，一次就完美地规划和执行一个测试，几乎是不可能的。

每一次测试完毕，我们都要认真分析一下结果，如果不满意，就需要看看如何改进。如果是测试方法不对，就需要重新规划。如果是环境不稳定，有干扰，那么就需要考虑如何消除干扰，净化测试环境。如果数据的收集不够多，就需要从测试模块中输出更多的信息。

总结

郑板桥在他的七言绝句《竹石》中说：“咬定青山不放松，立根原在破岩中“，赞扬竹子目标明确，基础扎实，而且百折不挠。

我们做性能测试也是如此。只有条理分明，目标清楚，目的明确，规划仔细，执行得力，才能“千磨万击还坚劲，任尔东西南北风“。

这样的测试或许需要执行很多次，因为经常需要调整测试的方法，但不达目的，我们决不罢休。

思考题

假设你需要重复做一种性能测试，但是你发现每次的测试结果都很不一样，你可以想一想，会有哪些原因呢？举几个例子，或许 SUT 服务器上面还在跑其他程序，也或许注入的负载流量不稳定，还有其他因素吗？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

11 | 性能测试的工具：七大测试场景如何选择高质量的测试工具？

你好，我是庄振运。

我们在前面两讲讨论了如何进行性能测试的规划和设计。性能测试离不开合适的工具，那么这一讲，我们来讨论一下测试工具的分类和构成，并根据七个不同的测试场景，分别学习几个常用的高质量测试工具，尤其是开源的。

这七个测试场景分别是：Web 测试、系统测试、数据库测试、文件 IO 测试、存储测试、网络测试以及移动 App 测试。

我们应该尽量借助这些好用的工具，而不要自己去重新开发。

自己开发的话，费时费力不说，开发出来的工具也不见得会比这些工具好用。更重要的是，性能测试工具的开发和使用过程中，有很多需要注意的地方和容易陷入的坑，一不小心就会掉坑。我们下一讲就会讨论常见的坑。

而这些常用的工具已经经过很多人的努力和长时间的改进，在很多方面避免了容易出现的各种问题，所以尽量使用它们吧。

测试工具的分类

首先你必须认识到，性能测试工具是繁多的。之所以繁多，是因为每种工具适合的场合不同，所以它们各有特点。比如如下几个方面：

测试场景：是针对 Web 环境、移动 App、系统、数据库，还是模块测试？

测试类型：是基准测试还是峰值测试？

免费还是收费：开源工具一般都是免费的；但是很多收费工具也的确物有所值。

支持的协议：比如是否支持 HTTP 协议、FTP 协议等等。

支持的功能：比如并发性支持度，能否分析测试结果，能否录制性能测试脚本等。

测试工具的模块

要评价一个测试工具的优劣好坏，我们就需要知道测试工具的模块和测试的一般过程。

大规模性能测试的一般过程是：通过录制、回放定制的脚本，模拟多用户同时访问被测试系统（SUT）来产生负载压力，同时监控并记录各种性能指标，最后生成性能分析结果和报告，从而完成性能测试的基本任务。

比照这个过程，一个稍微健全的测试工具都会包括以下的模块：

负载生成模块：负责产生足够的流量负载。多少流量算足够？这得根据测试类型和具体需求来定。如果测试类型是压力测试，那么产生的流量一定要大到 SUT 不能处理的程度。

测试数据收集模块：负责获取测试的数据，包括具体的各种性能数据。这个收集可以是实时的，就是在测试进行之中收集；也可以是后期，等测试完成之后收集的。

结果分析和展示：有了大量的测试数据，就需要进行分析并展示。同样的，这个过程可以是实时的，也可以是等全部测试完成后进行。

资源监控模块：测试过程中离不开对 SUT 和流量生成模块的实时资源监控，目的是确保这两个模块运行正常。具体来说，流量生成模块必须不能负载过量，否则很可能产生的流量不够大。SUT 也要确保运行还是正常的，否则整个测试就失去意义了。

控制中心模块：测试者需要用这个模块和整个测试系统来交互，比如开始或停止测试，改变测试的各种参数等等。

这几个模块的关系我用下面这张图表示。

Web 测试场景

我们先看第一个测试场景：Web 测试。这一场景的测试工具很多，我们介绍几个。

JMeter 是一款优秀而小巧精致的开源测试工具，是用 Java 写的。JMeter 安装简单，使用方便，所以很流行，建议每个性能测试者都掌握它。熟练使用它，在绝大多数场合都能大大提高测试效率。JMeter 的测试是基于 HTTP 协议的，所以最好对 HTTP 协议熟悉一些才能快速上手和理解里面的概念。

LoadRunner 是 HP 公司的一款测试工具，功能和资料都比较全，也好用，但不是开源的。它的组成模块都很强大，比如分析模块中的 AutoCorrelation 向导。这个向导会自动整理所有的监控和诊断数据，并找出导致性能降低的最主要的几个原因。这样就将性能测试结果转化为可处理的精确数据，从而使开发团队大大减少了解决问题的时间。

Locust 是基于 Python 的开源测试工具，支持 http、https 等协议。它的一个突出优点是可扩展性很好。

系统测试场景

这种场景下的测试工具很多，我主要介绍两个。

UnixBench 是一个 Unix 系统（比如 Unix、BSD、Linux）下的性能测试工具，是开源的，而且被普遍用于测试 linux 系统主机的性能。这个工具可以测试很多模块和场景，比如系统调用、读写、进程、图形化测试、2D、3D、管道、运算、C 库等，它的测试结果可以作为基准性能测试数据。

比如，你可以用它测试从一个文件向另外一个文件传输数据的速率，要求每次测试使用不同大小的缓冲区。再比如测试两个进程通过一个管道（Pipe）交换一个不断增大的整数的速度；类似现实编程中的一些应用，这个测试会首先创建一个子进程，再和这个子进程进行双向的管道传输。它也可以测试进入和离开操作系统内核的开销，即一次系统调用（System Call）的开销代价。

这是通过反复地调用 getpid 函数的小程序来进行的。

Perf 是 Linux 下最普遍使用的性能分析工具，功能强大全面，俗称性能测试的“瑞士军刀”。比如，Perf 可以对程序进行函数级别的采样，从而了解程序的性能瓶颈究竟在哪里。或者计算每个时钟周期内的指令数等等。

Perf 的原理是使用特殊的计数器来进行性能统计。它既可以分析指定应用程序的性能问题，也可以用来分析内核的性能问题，所以可以全面理解应用程序中的性能瓶颈。

我个人建议每个关心性能的人都了解和学习一下 Perf 的使用。

数据库测试场景

数据库的测试工具也是汗牛充栋。

SysBench 是一个容易使用的的开源多线程测试工具，主要用于测试数据库性能，比如 MySQL， Oracle 和 PostgreSQL，但也可以测试 CPU，内存，文件系统等性能。它的强项包括数据分析和展示模块，多线程并发性比较好，而且开销低。另外我们可以很容易地定制脚本，来创建新的测试。

mysqlslap 是 Mysql 自带的压力测试工具，它可以轻松模拟出大量客户端同时操作数据库的情况。

文件 IO 和存储测试场景

对文件系统和存储系统的性能测试工具也有很多，比如 ioZone，可以测试不同操作系统中的文件系统的读写性能。比如可以测试不同 IO 读写方式下硬盘的性能。

**Bonnie++** 是一个用来测试 UNIX 文件系统和磁盘性能的测试工具，它可以通过一系列的简单测试来生成硬盘和文件系统的性能参数。这个工具很容易使用，输出结果显示方面很不错。

如果你希望在 Linux 中很快地测试硬盘读写性能，dd 这个很有用的命令经常就够用了。这个工具就是用指定大小的块拷贝一个文件，并在拷贝的同时进行指定的转换。

网络测试场景

网络测试也有各种各样的工具。

Netperf 是很不错的一个网络性能的测量工具，主要针对基于 TCP 或 UDP 的传输。它有两种基本模式，即批量数据传输（bulk data transfer）模式和请求 / 应答（request/reponse）模式，我们可以根据应用的不同来选择不同模式。

测试结果所反映的，是两个系统之间发送和接受数据的速度和效率，即一个系统能够以多快的速度向另一个系统发送数据，以及后者能够以多快的速度接收数据。

Iperf 可以测试最大 TCP 和 UDP 带宽性能，具有多种参数和 UDP 特性，可以根据需要调整，可以报告带宽、延迟抖动和数据包丢失。这个工具使用起来很简单，一条命令行就可以。比如如下的测试：“采用 UDP 协议，以 100Mbps 的数据发送速率，从本机到服务器 192.168.1.1 作上传带宽测试；测试时间为 120 秒“，或者“以 50Mbps 的发送速率，同时向服务器端发起 100 个连接线程“。

移动 App 测试场景

移动 App 测试的性能指标主要是内存、CPU、电量使用、启动时长、显示帧率、网络流量等。 针对 App 的性能测试工具和平台可以按照两种方式分类。

第一种分类是根据线下还是线上。线下 App 性能测试主要依靠传统测试手段和方法，比如不同的版本，框架等等都需要进行 App 性能测试。 线上测试算是场景化测试，主要针对大规模或者动态环境，让 App 在特定场合和特别条件下，更精准地衡量 App 的核心性能。

另外一种分类方式是平台生态系统，现在主要有安卓 Android 和 iOS。有的测试工具可以兼容多个平台，比如 Appium，就是一个可以同时支持安卓和 iOS 的测试框架的工具，功能强大。

对 Android 的测试工具常用的有 adb（Android Debug Bridge）和 Monkey。Monkey 是 Android SDK 自带的测试工具。Monkey 的意思是猴子， 顾名思义，就是在电脑面前乱敲键盘在测试。 在测试过程中会向系统发送伪随机的用户事件流，如按键输入、触摸屏输入、手势输入等，对 App 进行压力测试，也有日志输出。

对 iOS App 的测试工具也很多，比如 XCTest、Frank、KIF 等等，这里就不展开了。

总结

古人说：“工欲善其事，必先利其器”。业界已经有很多好用的工具，一般都能满足大多数场合的测试要求，会让我们的测试工作如虎添翼。

虽然有些环境下我们经常倾向于自己开发某些工具，但是往往会花费很多时间，而且很容易陷入各种各样的坑中，因此尽量避免自己开发。

这一讲介绍的几款工具适用于不同的场合，如果你能合理运用它们，应该会取得事半功倍的效果。

思考题

你曾经用过什么样的性能测试工具？

为什么会选择这些工具，而不是其他的工具？

你使用的测试工具有什么优点和缺点？

你是怎么克服它的缺点的？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

12 | 九条性能测试的经验和教训：如何保证测试结果可靠且可重复？

你好，我是庄振运。

上一讲我们介绍了十几种常用的性能测试工具。我们知道，性能测试的一个关键是保证测试结果可靠、可重复，否则就没有意义。所以，我们今天来学习一下进行性能测试时，这方面的经验和教训。

根据以前做过的相关工作，我总结了九条这样的经验和教训。按照逻辑时间顺序，我将它们大体上分成三大类别，就是测试前的规划、测试中的变化和测试后的结果分析；每一类又有三条要点。

测试规划

三大类别的第一类别是测试规划，我们先来说说测试规划时的要注意的三条要点。

1. 详细记录测试环境和测试过程

做每个性能测试时，测试的环境至关重要。这里的环境包括软件硬件、操作系统、测试的负载、使用的数据等等。

测试的环境不同，性能测试的结果可能会迥异。除了测试环境，其它几个因素比如测试的过程，包括步骤和配置的改变也有相似的重要性。所以，我们每次测试都要把测试环境和测试过程记录下来，为将来分析数据做参考。

这些测试环境信息包括什么呢？大体上是操作系统和程序的版本号，以及各种软件参数的设置等等。

记录测试环境的目的是为了以后的各种分析。比如我们如果发现两次测试结果不匹配，需要找到不匹配的原因，那么这些测试环境就是相当关键的信息。如果两次测试结果的不同是因为软件配置不同导致的，根据记录的测试环境信息，我们就很容易根因出来。

至于如何记录，我们可以去手工去记录，但最好是自动记录，因为手工记录既费时费力，又容易出错。如果我们知道常用的环境配置的路径，可以很方便的写个程序来自动记录。或者依靠能自动记录配置的软件也可以。

2. 快速地复位测试环境

有时候性能测试需要重复进行多次，那么就需要在每一次测试后，能够有快速“复位”到初始测试环境的机制。这个复位机制越简单有效越好，最好能达到所谓“一键复位”的程度，从而最大限度地降低手工复位的工作量。

复位的具体内容和方式要根据性能测试的情况而定，这就要分析一下在每个测试过程中，哪些子系统的配置和参数被改动或者影响了。

虽然理论上说，我们只需要恢复那些会影响测试结果的配置和参数，但是由于系统的复杂性，有些乍看起来不会影响测试结果的参数，其实或多或少也会影响测试结果。所以一般来讲，我们会把所有的配置和参数都恢复一下。环境复位的具体方式包括重新拷贝文件、重置配置参数、清空各种缓存等。

举个具体例子，如果是测试一个存储系统的性能，比如硬盘的性能。如果测试过程中写入了大量数据到存储系统，那么测试完毕，恢复环境的时候，就需要删掉这些数据，以便使被测系统回到初始状态。

再举个类似的例子，如果是测试一个数据库的性能，那么测试过程可能发出了很多查询请求。因为数据库经常会缓存各种情况和结果，所以在恢复测试环境时，不要忘记把缓存也清空。

一个现代计算机系统中往往有很多种缓存，比如数据库缓存、文件系统缓存、存储缓存（比如 SAN）等。不同种类的缓存自然有不同的清空方式。对于数据库缓存，在每个测试之前可以用命令行来刷新数据库缓存；如果数据库不提供这样的命令，则需要重新启动数据库。对于文件系统缓存，一般需要重新启动服务器。对于 SAN 缓存，可以考虑在测试期间减少甚至关闭缓存，或者用大量的随机数据来“污染”缓存。

3. 足够的负载请求和数据

性能测试需要流量负载以及相关的数据，我们需要特别注意保证它们的多样化和代表性。否则测试结果会严重失真。

当使用相同的测试数据进行重复测试时，如果负载请求不够大，那么各种缓存可能会严重影响结果。

如何识别缓存的影响呢？

各级缓存系统都有相应的统计指标和命令，比如文件系统缓存和 SAN 缓存中的缓存命中率，就可以通过统计信息中报告的延迟，并且结合经验来识别。举例来说，如果一个随机 8KB 或 16KB 数据的对硬盘的读写，测量出的延迟不到 1 毫秒，那就实在是“太快”了，快得让人不敢相信；可以肯定它是命中某种缓存了。

除了合理清空缓存外，更有效地方式是保证测试时间足够长、测试的负载请求足够多和数据足够多样化，从而最大限度地减少或者掩盖缓存等其他因素的影响。

测试进行

测试规划之后，我们就要关注测试中的变化了。

1. 性能数据日志要适当输出

性能测试过程中，也需要实时输出有关的性能数据和日志，比如 CPU 使用率数据。这些数据对于测试完成后的分析至关重要。

输出的数据和日志最好保存起来，以方便后期处理 / 重新处理。比如执行过很多不同参数配置的测试之后，我们经常需要进行测试之间的比较，这时候就需要仔细检查以前输出的日志了。

输出数据的多少也需要注意，虽然我们希望尽量多地输出，但是也要意识到，太多的输出有时候会起反作用。

存储的开销：可以用压缩存储来解决。

数据处理时间的开销：注意压缩和解压缩也要花时间。

可能影响性能测试的结果：有时候因为某些原因，日志输出会影响被测系统的性能，比如往一个文件写入日志，可能会导致系统的暂停，从而影响测试的结果（这个我们下一讲会详细剖析）。

2. 测试环境要稳定

性能测试的环境在测试进行过程中，以及重复测试时一定要保持稳定和一致，否则测试结果就不可靠或者不能重复。

比如一个测试进行中，背景流量的负载产生了剧烈变化，导致被测系统的延迟增加。假如这个背景流量变化不是预期产生在测试规划之内的，就会造成测试结果失真。

类似的，如果测试过程中有不可控的因素，造成每次重复测试结果都不同，那这样的测试就不可靠。无论测试结果好坏，都不能用来作出有用的结论。

一般的解决方案，是尽量在一个独立无干扰的环境中进行测试，加上每次测试都准确地恢复测试环境，就能最大限度地保证测试环境的稳定。

3. 一次调一个参数的利弊

在性能调优测试中，就是通过实验来找出系统的最优配置。

对这种测试，我们经常听到的经验是，“一次只调一个参数，通过对比实验，就能知道这个参数的最佳值“。你觉得这个经验对吗？

我们先看这个经验的出发点。性能调优过程中有很多可调参数和配置，互相之间的影响不清楚，因此不宜对系统的各种参数进行随意的改动。应该以基本参考设置为基础，逐次根据实际测试结果进行优化，一次只对某个领域进行性能调优，并且每次只改动一个设置和参数，避免其他参数和相关因素的干扰。

这个经验有它的道理，但我觉得这样做既对也不对，你不能盲从，否则就会错过最优配置的机会。

我举个生活中的例子来说明。假设有一个装水的木桶，这个木桶由多块可调整高度的木条组成。假设这些木条的初始高度不一，我们的目的是找到一个木条高度组合，从而实现最大的装水量。这个问题看起来很简单，根据木桶定律，就是把每块木条都调到最高嘛。

我们假设这个木桶是我们的被测系统，每块木条就是一个参数。再假设我们对木条之间的关系不清楚。如果一次只调整一个参数，然后实验测试装水量。因为木桶装水量取决于高度最低的木条，我们或许会得出结论——只有那块最低木条值得调，其他木条的高度都不重要。

这种结论的结果就是把最低木条调高，比如调到最高。那么这个所谓的“最优系统”，也就是整个木桶的装水量取决于新的最低木条，也就是原来木桶中高度次低的那个木条。

我们很容易看出，如果一次调整多个木条，那么我们就会很快地找出一个更优的系统，也就是一个装水更多的木桶。

结果分析

测试后的结果分析也是需要你关注的重点，三条经验如下：

1. 根因分析要由易到难

如果在性能测试过程中需要查找性能瓶颈，查找的过程一定要由易到难逐步排查。因为参考我们学过的帕累托法则，从最明显的性能瓶颈来开始，往往可以事半功倍。

首先从最常见的几种资源和几个指标查起，比如 CPU 使用率、存储 IO 繁忙度、内存大小、网络发送和接收速度等。

进一步的分析就可以针对不太明显的资源，比如内存带宽，缓存击中率，线程加锁解锁等；从而过渡到应用程序和系统的一些配置参数。这些配置参数包括应用服务器及中间件，操作系统瓶颈，数据库、WEB 服务器的配置；还有应用业务瓶颈，比如 SQL 语句、数据库设计、业务逻辑、算法、数据等。

2. 几种测试最好互相验证

各种性能测试工具和测试手段都有自己的局限性或缺陷，从而可能会造成测试结果出现偏差。所以，如果条件和时间允许，最好使用几种不同测试工具或手段，分别进行独立的进行测试，并将结果相互比较和验证。

如果几种测试比较后结果相似，那么皆大欢喜。

否则就需要进行深入比较分析，弄明白造成结果不同的原因。分析以后，如果能够清楚地了解根因并作出合理解释，那么很多时候就够了，可以止于此。最后形成结论时，只要稍加有针对性地说明就可以。

3. 测试结果和生产环境比较

如果性能测试是在非生产环境中进行的，那么得出的测试结果或许会和生产环境大相径庭。如果我们测试的目的是尽量和生产环境一致，就需要仔细审查每个测试的环节，包括测试环境和测试流程。

假如非生产环境的测试结果和生产环境的测量不同，很多情况下是由于测试环境不同导致，尤其要注意的是网络环境的差异。

比如测试的环境是在局域网，而真正的生产环境是无线网或者 4G，那么可以肯定，这样的局域网测试没有什么意义。网络差异的影响经常会大大超过很多人的预期，因为实际的生产环境还牵扯很多上层协议和背景流量，比如 HTTP 协议。

假设 4G 用户在网络层或者链路层上，相对局域网而言只是多了 30 毫秒的延迟。在 Web 服务器中等负载的情况下，这几十毫秒的链路层延迟，就可能导致应用层响应时间增加惊人的几十秒延迟。

对网络环境这点，最理想的解决方式当然是在生产环境进行测试。如果实现这点有困难，那么可以考虑使用一定的网络仿真，来模拟真实生产环境。

除了网络环境的影响，真实客户的操作流程也可能造成测量结果的不同。比如一个真正的 Web 用户或者 APP 用户在访问我们的系统时，往往会有思考或者阅读时间；所以真实世界的用户，不太会在 1 秒钟内发出背对背的好几个页面请求。对于这种情况的差异，可以通过人为地添加停顿来模拟思考时间。

现在很多测试工具都支持这种思考时间的引入。比如使用 JMeter，可以使用高斯随机计时器（Gaussian Random Timer），来模拟现实世界中的用户，以随机方式和页面进行交互。

总结

当年毛主席给彭德怀写过几句诗：

山高路远坑深，

大军纵横驰奔。

谁敢横刀立马？

唯我彭大将军。

打仗有很多坑，做性能工作也有很多坑。我们要把性能测试的工作做好，本身就不容易。山高路远都不在乎，但一定要注意其中的陷阱。

所以只有不断地学习别人总结的经验，吸取别人的教训，解决测试的各种挑战，才能得出可靠，可重复并且有意义的性能测试结果。

思考题

回想一下你做过的性能测试，有没有踩过今天介绍的几个坑？

踩过之后，是怎么防止以后不再踩同样的坑的？

对没有踩过的坑，你有没有方法避免以后中招呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

13 | 性能测试的工程集成：如何与产品开发和运维业务有机集成？

你好，我是庄振运。

前面几讲，我们讨论了性能测试的几个方面，包括测试的种类、测试的规划、工具以及经验和教训。

今天我们讨论性能测试如何和其他系统进行智能集成，也就是如何让性能测试这一工作从单独的、一次性的、手工发起的、传统的人工操作，进化成一个和开发及运维过程相结合的、持续的、自动重复执行的智能操作。

性能测试模式的演化

性能测试作为 IT 公司的一种重要工作，它的工作模式正在从传统的手工模式，不断进化成智能集成的自动模式。

这样的演化主要归功于这些年互联网技术的进步和业务需求的提高，包括数据量的加大和业务的日益复杂化、客户需求的多元化、公司业务规模的扩大，以及人工智能和机器学习的不断成熟。

那么，性能测试的模式进化表现在哪些方面呢？主要有四个方面，如下图所示：

1. 从独立的操作演化成和其他系统（比如开发和运维）的有机集成。

公司中很多业务都和性能测试有关，尤其是产品开发和系统运维业务。性能测试需要和这些业务紧密结合，从而使相关工作的效率极大提高。

2. 从一次次的单独执行演化成持续而重复的执行。

从开发角度来看，一个产品的程序代码会不断被开发和增强，包括加入新的功能和修补发现的错误。每次代码改变都需要进行性能测试，以确保程序面对客户的端到端性能和资源利用效率没有变低。从运维角度来看，公司的产品系统也需要持续地进行性能测试，以尽早发现可能的问题。

3. 从手工进行测试演化成自动化的测试。

每当需要进行性能测试的时候，系统越来越需要自动触发，并按照既定规则来开始测试。测试完成后也需要自动进行分析，并根据分析的结果继续进行后面的定义好的步骤。整个过程最好不需要测试人员的人工参与，以降低运营成本并减少人为错误。

4. 从常规的人工操作演化成基于人工智能（AI）和机器学习（ML）的智能操作。

数据量的变大让有效的机器学习成为可能。同时伴随着人工智能技术的不断成熟，传统常规的性能测试一步步地走向智能的自动性能优化。

和产品开发的系统集成

性能测试和产品开发密切相关，我用下面这张图片表示它们之间的关系。我分成两部分讲，先讲和产品开发的系统集成。

性能测试和产品开发的集成也是所谓“持续集成”（Continuous Integration）的一部分。

什么是“持续集成”呢？

这个概念已经在软件开发领域存在很多年了，本来是为了配合敏捷开发（Agile Developement）的速度和效率而产生的，是把源代码管理、代码检查、程序编译、性能和功能测试、产品部署等一系列操作整合在一起的概念和工具。

怎么才叫“持续”呢？

就是从程序员提交了源码开始，相关工具就会自动进行编译、测试等一系列运作，并将结果反馈给程序员。这样，提交代码的程序员很快就会知道刚刚提交的代码有没有问题。

可能发生的问题有很多种，包括编译错误、整个程序不能运作、能编译运行但是整体性能变差等等。如果发现这样的问题，程序员和团队就可以迅速进行改正，不必等到开发周期后期才寻找和修复缺陷。

什么叫“集成”？

就是上述一套操作都是有相应工具支持的，几个工具又集成在一起，构成一个完整的系统。

持续集成包括两个重要阶段：持续交付和持续部署。我们先说一下持续交付。

持续交付（Continuous delivery）指的是开发人员的每一次代码提交，都会自动地编译，并且运行已经定义好的测试程序。

测试程序里面就包括性能测试和结果分析。如果分析的结果确认这次代码提交没有性能问题，就成功接受这次提交。否则，就会采取措施通知代码提交者去修正代码；并重复这个过程，直到通过。

持续部署（Continuous deployment）是持续交付的下一步，指的是代码通过评审以后，自动部署到真正的生产环境。

持续部署的目标是，代码在任何时刻都是可部署的，并且是可以进入生产阶段的。持续部署的前提是能自动化完成测试、构建、部署等步骤。同时，如果一旦部署了的版本发生不能接受的问题，就需要回滚到上一个版本。这个回滚的过程也需要简单方便，否则会造成非常大的混乱。

持续集成的价值何在？

持续集成的价值主要有几点：降低代码开发风险，及早发现集成错误，减少手动测试过程，快速生成测试结果，提高程序员和开发团队的安全感。同时，频繁的提交代码，也会鼓励和促使开发人员创建模块化和低复杂性的代码。

工具集

支持持续集成的工具有很多，按照领域分可以有下面几大类：代码版本管理、代码检查、编译连接、功能测试、性能测试、性能分析、代码覆盖率、结果展示等。有些流行的持续集成服务器可以整合这些工具或功能，比如 Jenkins、CruiseControl、Travis 等。

我举一个完整持续集成的工具例子。比如一个 Java 开发团队，在开发某项目时用了如下的一系列工具：

用 JStyle 来分析 Java 源代码

用 Ant 构建运行 JUnit 来进行简单的功能测试

用 DbUnit 执行长时间的数据库组件测试

用 JUnitPerf 来进行负载和压力测试

用 JProfiler 来进行全功能的代码性能剖析

用 Selenium 运行基于 Web 的功能测试

用 Cobertura 测量代码覆盖率

用 CruiseControl 作为服务器来管理持续集成

和运维业务的系统集成

讲完了性能测试和开发过程的集成，我们接着谈谈和运维业务的系统集成。

广义上来讲，性能工作也是运维的一部分，包括性能测试、性能分析和性能优化。但是，如果把性能工作和其他运维业务分开来看，它就需要和其他运维业务有机而智能地集成。

运维业务的一个趋势是智能化。

智能化的原因，是互联网数据量的变大，运维业务的多样化和复杂化，以及对运维服务质量要求的提高（比如低成本、低延迟、高防范）。这样一来，很多传统的运维技术和解决方案已经不能满足当前运维所需。另一方面，机器学习（ML）和 人工智能（AI）技术在飞速发展，这就推生了智能运维 AIOps（Artificial Intelligence for IT Operations），这是运维未来发展的必然趋势。

在这样一个趋势里，运维就需要和性能测试的过程紧密集成。

一是通过持续而智能的性能测试，能及时发现已有的和将来可能会发生的性能问题，从而快速修复和及时预防。比如，根据性能测试的结果，可能会发现在不久的将来，整个系统的某项资源会用光，有可能导致系统挂掉。这种情况，我们就可以提前采取相应的措施，来避免这一问题的发生。

二是通过不同种类的性能测试，来找出最佳的解决方案。或许，对有些简单的性能问题，我们能很容易发现和解决，但对很多复杂的性能问题，就比较难找出原因和确定最优方案。要找出最主要的性能问题根因，经常需要进一步的性能测试。即使找到根因，现代互联网服务产品的子模块之间依存度很高，互相的交互多而复杂。那么什么样的解决方案才是效果最好，最容易实现的的，往往需要进行进一步测试来验证。

性能测试和运维的集成必须有两个特点：有机和自动。

不管是性能衡量、问题预测、根因分析还是性能优化，人工去执行都非常费时费力，从而不可取。唯一可行的就是借助于人工智能来尽量自动化。除了持续自动地进行性能测试，发现性能问题还需要进行自动分析，找出问题后也要执行自动调整优化。

我们的目标是让性能测试和运维业务进行智能的有机集成，其中的智能来自于对大数据的分析和机器学习。

无论是本业务系统的历史数据，还是其他业务系统的数据，甚至是业界其他公司的数据和经验，都是机器学习的对象和分析的基础。同时，我们还需要注入适当的知识和规则，来帮助这一套集成的持续优化。

在这一过程中，数据的采集和整理是一切的基础。公司层面需要全方位，实时、多维度、全量地对各种运维数据采集、整理和存储。这里的运维数据包括基础架构的机器监控数据，内网和外网的网络数据，公司业务流量数据，工单系统数据，日志监控数据等。这些数据需要有统一而合理的接口，以方便访问。

总结

性能测试虽然有很多讲究和注意的地方，但它本身也是作为整个公司业务的一个子系统而存在的。我们需要把它和其他几个子系统，尤其是产品开发子系统和运维子系统有机地整合集成起来，让这几个子系统之间的操作“浑然天成”，才能获取整个公司业务的最大收益。

白居易的《长恨歌》有两句：“在天愿作比翼鸟，在地愿为连理枝”，期盼的是一种比翼齐飞，一种同心同德。

这种期盼用在这一讲的子系统集成也挺合适。性能测试和产品开发子系统的集成，可以使得开发过程的迭代更快、更高效，并保证代码质量。性能测试和运维子系统的集成，可以让整个程序和业务保持高性能运转，提高公司业务质量和公司营收。

思考题

你现在的开发环境中有没有整合这样的自动测试系统？如果没有，你觉得值得考虑一下吗？如果你能从无到有的搭建一个这样的系统，你老板会不会对你刮目相看？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

14 | 性能分析概述：性能问题归根结底是什么原因？

你好，我是庄振运。

前面几讲，我们讨论了有关性能测试的内容，了解了各种测试的种类、测试的规划、测试的工具以及执行测试的经验教训。在整个性能优化的世界里，性能测试是基础，在这一基础上，我们才能对各种性能问题进行性能分析，并进行相应的性能优化。

从这一讲开始，我们来探讨一下常见的各种性能问题，和如何进行相应的性能分析。我们首先宏观地介绍一下性能的外部指标、内部瓶颈和资源制约，以及如何正确地进行性能分析。然后接下来的几讲分别重点讨论 CPU、内存、存储和网络几个领域。

性能的外部指标

谈论应用程序系统和互联网服务的性能时，我们首先要清楚外部的性能指标是什么。最重要也最普遍的指标有三个：服务延迟（Service Latency）、吞吐率（Throughput）和资源使用率（Resource Utilization）。我们分别来看看。

服务延迟

服务延迟（或者叫访问延迟），指的是客户发出的请求被成功服务的时间。

虽然具体的定义和度量有很多种，比如有些度量只考虑系统本身的服务时间，而不考虑其它因素（比如网络传输时间）。但我个人坚持，这个指标是直接针对客户体验的，因此不能仅仅从自己的系统角度衡量，而必须是端到端的延迟度量（End To End, or E2E）。因为只有这样来定义这一指标，才能准确地抓住“外部性能”这一特点。

任何系统和服务的设计和搭建都是为了给客户来用的，如果不紧紧抓住“客户体验”这一根本目标，性能测试、性能分析，以至于性能优化也就成了无的放矢，基本失去了意义。

吞吐率

吞吐率指的是单位时间（比如每秒钟）可以成功处理的请求数或任务数。

这一指标和前面讲的访问延迟指标相辅相成，一个注重时间，就是服务延迟；一个注重空间，也就是系统容量。一个系统的外部性能主要受到这两个条件的约束，缺一不可。

比如，一个在线聊天服务系统，可以提供每秒钟一百万的吞吐率，但是客户的访问延迟是 5 分钟以上，那么这个“一百万的吞吐率”没啥意义。反之，访问延迟很短，但是吞吐率很低，同样没有意义。

所以，一个系统的性能必然受到这两个条件的同时作用。

资源使用率

一个系统和服务总是需要软硬件容量来支撑的，那么资源的使用率就至关重要了。因为它直接决定了系统和服务的运营成本。

这一指标虽然主要是面向系统容量的，但其实和客户也直接相关。如果资源使用率低，比如 CPU 使用率低，在系统容量固定（比如服务器数目固定）的情况下，吞吐率也会较低，或者访问延迟会较高，因为系统资源没有被充分利用。

外部性能指标的变化

我们还需要知道，这三个性能指标的变化有它们自己的特点，而且经常会互相影响。

对一个系统而言，如果吞吐率很低，服务延迟往往会非常稳定。当吞吐率增高时，访问延迟一般会快速增加。

下图展示了一个有代表性的系统的吞吐率和访问延迟的变化。

在吞吐率低于每秒 600 时，访问延迟小于 5 毫秒，这个延迟相对稳定。然后随着吞吐率变大，访问延迟飞速攀升。一般而言，根据系统的延迟可接受大小，我们需要控制负载流量，以免访问延迟过大而影响客户体验。

我们在测量访问延迟的时候，不仅要计算均值，还需要注意延迟的分布情况，比如，有百分之几的在服务允许的范围，有百分之几的略微超出了，有百分之几的完全不可接受。多数情况下，平均延迟达标了，但是其中可能有很大比例（比如 20%）远远超出了我们可接受的范围。

所以，我们在规定延迟标准的时候，除了均值，还需要定义百分位数的可接受值。比如，平均延迟 10 毫秒，P90 小于 30 毫秒，P99 小于 50 毫秒等等。

关于吞吐率，现实中的系统往往有一个峰值极限。

超过这个峰值极限，系统就会超载，除了服务延迟超标，还会造成一系列的性能问题（比如系统挂掉）。这个峰值极限往往需要经过仔细的性能测试，并且结合访问延迟标准来确定。有了这个峰值极限值后，系统的设计和运维就需要确保系统的负载不要超过这个值。

除了影响运营成本和系统容量，资源使用率的标准也需要考虑其他几个重要因素。

一个因素是意外事件的缓冲（Buffer）和灾难恢复（Disaster Recovery, or DR）。一个现实世界中的系统，随时都会有意外事件（比如流量波动）或者部分网络故障，这就需要整个系统资源保留一定的缓冲，来应付这些意外和从发生的灾难中恢复。比如 CPU 的使用率，虽然理论上可以到 100%，但考虑这些因素，实际的使用率指标往往远远低于 100%。

性能问题归根结底是某个资源不够

所有的性能问题，虽然表现方式各异，归根结底都是某种资源受到制约，不够用了。这里的资源指的是一个计算机系统，程序和互联网服务会用到的每一种资源，比如 CPU、网络等。换句话说，客户的请求在处理时在某个地方“卡住了”。这个卡住的地方就叫“瓶颈”（或者叫卡点，Choke point）。

根据我的经验，我在下面这张图表中展示了一个系统常见的十大瓶颈，基本上覆盖了所有可能出现性能问题的地方。

这十大瓶颈可以大致分为四类，在后面的几讲中我们会详细讨论分析每一类别里面的具体性能问题。这四类是：

软件系统：包括操作系统、应用程序、各种类库以及文件系统。

CPU 和内存：包括 CPU 性能、QPI（QuickPath Interconnect，处理器之间的快速通道互联）和缓存内存。

存储和外部 IO：包括处理器的 IO 的接口性能、各种存储系统（尤其是 HDD 和 SSD 性能）。

网络：包括服务器到机柜交换机的网络、数据中心的网络、CDN 和互联网。

总体上来讲，性能分析的目的，是提供高性能、低延迟、高效率的服务。

要实现这一目的，就需要找到系统和服务的性能瓶颈，然后尽可能的消除瓶颈，或者降低瓶颈带来的影响。系统和服务有性能瓶颈就说明这个地方的资源不够用了。所谓最大的性能瓶颈，就是说这个地方的资源短缺程度最大，相对而言，其他地方的资源有富余。

如何找到最大的性能瓶颈？

这就需要进行性能测试和性能分析了。性能分析时需要知道三个层次的知识：

第一个层次是可能的性能瓶颈，比如我们刚刚讨论的十大瓶颈。知道了瓶颈才能有目标的去分析。

第二个层次是每个瓶颈有哪些资源有可能短缺。比如内存就有很多种不同的资源，不仅仅是简单的内存大小。除了内存使用量，还有内存带宽和内存访问延迟。

第三个层次是对每个瓶颈的每种资源要了解它和其他模块是如何交互的，对整个系统性能是如何影响的，它的正常值和极限值是多少，如何分析测量等等。

找到性能最大瓶颈后，具体的优化方式就是什么资源不够就加什么资源，同时尽量降低资源消耗，这样就可以做到在资源总量一定的情况下，有能力支撑更高的吞吐率和实现更低的延迟。

依据数据和剖析（Profiling）来分析

做性能分析时，必须采用科学的方法，尽量依据数据，来引导我们的分析和验证我们的推论，而不是完全凭空猜测。

当我们有了比较多的性能分析和优化的经验后，慢慢就会对一个系统的内部各个模块的交互，以及各种性能问题肚里有数了。这种时候，适度地做一些理论推测是合理的。就像一个有经验的医生，往往稍微了解一下病人的情况，就猜个八九不离十。这就是经验的重要性。

不过，再有经验的医生，还是需要做进一步的检验，尤其是面对复杂的病人和病情。同样的，无论性能分析的经验多丰富，我们也需要谨慎地做性能测试和数据分析，尤其是在针对重要系统的时候。

这一点可以说是性能分析和优化的第一原则。当我们怀疑性能有问题的时候，应该通过合理的测试、日志分析，并作合适的剖析（Profillig），来分析出哪里有问题，从而有的放矢，而不是凭感觉、撞运气。

比如，如果是 CPU 相关的性能问题。按照我们学过的帕累托 80/20 定律，系统绝大多数的时间应该都耗费在少量的代码片段里面。如何找出这些需要优化的代码呢？唯一可靠的办法就是 profile。现代流行的各种编程语言，比如 Java 和 Python 等，都有相关的 profile 工具。所以，会熟练使用这些 profile 工具是性能分析和优化的必要条件。

我们举几个例子，比如 Java 语言，就有很多工具，像 JVMTI（JVM Tools Interface，JVM 工具接口），就为性能分析器提供了方便的钩子，可以用来跟踪诸如函数调用、线程相关的事件、类加载之类的事件。再比如对 Python 语言来说，我们可以用 sys.setprofile 函数，跟踪 Python 的函数调用返回异常等事件。

总结

我们的儒家思想提倡“格物致知”，就是说要深入探究事物的原理，而从中获得知识和智慧。性能优化能否成功，也需要探究一个系统中性能的真正问题，找到性能的最大瓶颈。

格物致知时，还需要“正心“和”诚意”，就是要实事求是和端正态度，从而科学而系统地收获知识。我们做性能分析时候，也是需要根据实际的数据和 Profiling 等测试结果，找到性能的瓶颈，并合理地解决性能问题。

思考题

你工作中有没有碰到没有搞明白的性能问题？如果有，想想能不能按照今天讲的几个可能的性能问题领域来一个个考虑并验证一下？说不定会有“守得云开见月明”的恍然大悟呢。

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

15 | 常见的性能问题之CPU篇：如何让CPU的运行不受阻碍？

你好，我是庄振运。

从这一讲开始，我们探讨分析几个最容易发生性能问题的领域：CPU、内存、存储和网络。

这一讲先来讨论关于 CPU 的常见性能问题。首先我们从硬件的角度，来看看 CPU 的性能取决于哪些因素，然后分析一下 CPU 的内部结构。接着我们探讨和 CPU 性能相关的软件系统，看看 CPU 运行时侯的调度和切换。

CPU 的性能决定因素

宏观来讲，一台服务器里面的 CPU 性能取决于好几个因素，包括有多少处理器、多少个核、时钟主频是多少、有没有 Turbo 模式、处理器内部的运算架构以及和 CPU 紧密交互的其他部件的性能。

CPU 的更新换代很频繁，基本上每两年就会更新一代。比如 Intel 的 CPU，最近 10 年已经经历了 5 代左右。每一代都有主频的变化，而且有好几个变种。

下面的表格描述了从十年前（也就是 2009 年）的 SandyBridge，到后来的 IvyBridge、Haswell、Broadwell，直到 Skylake。注意，对后面的三代，我分别列出了其中的两种变化——单处理器（1P）和双处理器（2P）。

大体上我们可以看出，虽然 CPU 更新换代，但是处理器的时钟主频基本不再提高，甚至变得更低了。这样的目的是降低 CPU 的功耗。比如 SandyBridge 的时钟频率是 2.6GHz，但是到了 Skylake，反而降低到了 2GHz。

为了提升单个处理器的性能，每个处理器里面的核数却越来越多，这样就可以尽量的提升并行处理能力。比如 SandyBridge 的每个处理器只有 8 个核，而 Skylake 则多达 20 个核。

而且我们也看到，每一代 CPU 都允许 Turbo 模式，就是让 CPU 的主频提高。目的是可以让处理器在特殊情况下，用提高功耗的代价来增加主频，从而获得更高性能。

CPU 的内部结构

CPU 的性能也取决于它的内部结构设计。很多程序员对 CPU 的内部机构不是完全清楚，尤其是对相关的术语之间的区别和联系一知半解，比如多处理器和多核、逻辑 CPU 和硬件线程、超线程，以及 L1/L2/L3 三级缓存等。

之所以对这些结构不甚了解，主要原因是现代处理器变得复杂，普遍采用多处理器，多核以及内部的各种优化处理来提高 CPU 性能。我们今天就从外到内，从宏观到微观地介绍一下。

我注意到，这方面的很多中文术语，大家有时候用法不一致，所以很容易混淆。为了清楚描述，你尤其要注意一下我用的术语（包括英文）。

多处理器和 NUMA

现在的 CPU 普遍采用多处理器（Socket）来提高 CPU 性能，每个处理器都有自己可以直接访问的本地内存（Local Memory）。一般来讲，这里面每个处理器的性能和内存大小都是一样的。每个处理器也都可以访问其他处理器的内存，这些内存就相当于是外地 / 远程内存（Remote Memory）。

当 CPU 处理器访问本地内存时，会有较短的响应时间（称为本地访问 Local Access）。而如果需要访问外地 / 远程内存时候，就需要通过互联通道访问，响应时间就相比本地内存变慢了（称为远端访问 Remote Access）。所以 NUMA（Non-Uniform Memory Access）就此得名。

下图展示了两个处理器的 NUMA 架构。

如果处理器 A 访问内存 A，就是本地访问。如果它访问内存 B，就是远端访问，内存的访问延迟大大增加。

采用多处理器和 NUMA 架构的主要原因，是提高整个 CPU 的并行处理性能。每一台服务器可以同时运行很多程序和进程。对每一个进程和线程而言，当它运行在某一个处理器上时，它所对应的内存使用默认的分配方案是——优先尝试在请求线程当前所处的处理器的本地内存上分配。如果本地内存不足，才会分配到外地 / 远程内存上去。

多核结构和多级缓存

我们再看看每个处理器内部的结构。我们刚刚讲到的处理器，内部一般都是多核（Core）架构。随着多核处理器的发展，CPU 的缓存通常分成了三个级别：L1、L2 和 L3。

级别越小就越接近 CPU，速度更快，同时容量也越小。L1 和 L2 一般在核的内部，我们下一讲还会详细讲。L3 缓存是三级缓存中最大的一级，同时也是最慢的一级；在同一个处理器内部的核会共享同一个 L3 缓存。

除了多个核以及 L3 缓存外，处理器上一般还有非核心处理器（Uncore），里面含有和指令运行不直接相关的组件，包括 QPI 控制器和存储器一致性监测组件，如下图所示。

超线程（Hyperthreading，HT）

一个核还可以进一步分成几个逻辑核，来执行多个控制流程，这样可以进一步提高并行程度，这一技术就叫超线程，有时叫做 simultaneous multi-threading（SMT）。

超线程技术主要的出发点是，当处理器在运行一个线程，执行指令代码时，很多时候处理器并不会使用到全部的计算能力，部分计算能力就会处于空闲状态。而超线程技术就是通过多线程来进一步“压榨”处理器。

举个例子，如果一个线程运行过程中，必须要等到一些数据加载到缓存中以后才能继续执行，此时 CPU 就可以切换到另一个线程，去执行其他指令，而不用去处于空闲状态，等待当前线程的数据加载完毕。

通常，一个传统的处理器在线程之间切换，可能需要几万个时钟周期。而一个具有 HT 超线程技术的处理器只需要 1 个时钟周期。因此就大大减小了线程之间切换的成本，从而最大限度地让处理器满负荷运转。

一个核分成几个超线程呢？

这个数字会根据 CPU 架构有所变化；Intel 一般是把一个核分成 2 个。

“这台计算机有多少 CPU？”

我们经常会问这个问题，结合我们刚刚讲的知识，就很容易回答了。

比如，如果一台计算机有两个处理器，每个处理器有 12 个核，而且采用了 HT 超线程，那么总的 CPU 数目就是 48，就是 2×12×2。这个数字 48，就是我们平时用监控软件和命令看到的 CPU 的数量。比如，Linux 的 top 或者 vmstat 命令，显示的 CPU 个数就是这样算出来的。

CPU 性能指标和常见性能问题

我们继续探讨 CPU 的性能指标和常见性能问题，这方面很多资料都有涉及，我们提纲挈领地总结一下。

最表层的 CPU 性能指标，就是 CPU 的负载情况和使用率。CPU 使用率又进一步分成系统 CPU、用户 CPU、IO 等待 CPU 等几个指标。你执行一下 top 命令就会看到。

需要注意的是，因为 CPU 架构的复杂性，以及和其他部件的交互，CPU 的使用率和负载的关系往往不是线性的。

也就是说，如果 10% 的 CPU 使用率可以每秒处理 1 千个请求，那么 80% 的 CPU 使用率能够处理多少请求呢？不太可能处理每秒 8 千个请求，往往会远远小于这个数字。

衡量一个应用程序对 CPU 使用效率时，往往会考察 CPI（Cycles Per Instruction，每指令的周期数）和 IPC（Instructions Per Cycle，每周期的指令数）。这两个指标有助于识别运行效率高或低的应用程序。而一台计算机的 CPU 性能一般用 MIPS（Millions of Instructions Per Second）来衡量，表示每秒能运行多少个百万指令，MIPS 越高，性能越高。MIPS 的计算很简单，就是时钟频率×IPC。

继续往深处分析，CPU 常见的各种中断包括软中断和硬中断。除此之外，还有一种特殊的中断：上下文切换。这些指标需要和每个核挂钩，理想情况下是各个核上的中断能够均衡。如果数量不均衡，往往会造成严重的性能问题——有的核会超载而导致系统响应缓慢，但是其他的核反而空闲。

和 CPU 相关的性能问题，基本上就是表现为 CPU 超载或者空闲。

如果是 CPU 超载，那么就要分析为什么超载。多数情况下都不一定是合理的超载，比如说多核之间的负载没有平衡好，或者 CPU 干了很多没用的活，或者应用程序本身的设计需要优化等等。反之，如果是 CPU 空闲，那就需要了解为什么空闲，或许是指令里面太多内存数据操作，从而造成 CPU 停顿，也或许是太多的分支预测错误等，这就需要具体分析和对症下药的优化。

CPU 对多线程的执行顺序是谁定的呢？

是由内核的进程调度来决定的。内核进程调度负责管理和分配 CPU 资源，合理决定哪个进程该使用 CPU，哪个进程该等待。进程调度给不同的线程和任务分配了不同的优先级，优先级最高的是硬件中断，其次是内核（系统）进程，最后是用户进程。每个逻辑 CPU 都维护着一个可运行队列，用来存放可运行的线程来调度。

CPU 的性能监测工具

我们最后讲一下 CPU 性能监测方面的工具。和 CPU 监测相关的工具挺多的，而且往往每个工具都包含很多不同的有用的信息。

比如在 Linux 上，最常用的 Top 系统进程监控命令。Top 是一个万金油的工具，可以显示出 CPU 的使用、内存的使用、交换内存和缓存大小、缓冲区大小、各个进程信息等。

如果想要查看过去的 CPU 负载情况，可以用 uptime。也可以用 mpstat 和 pidstat，来分别查看每个核还有每个进程的情况。另一个常用的 vmstat 命令可以用于显示虚拟内存、内核线程、磁盘、系统进程、I/O 模块、中断等信息。

对有经验的性能工程师来讲，有一个类似于“瑞士军刀”一样的好工具：Perf。

Perf 是 Linux 上的性能剖析（profiling）工具，极为有用。它是基于事件采样原理，以性能事件为基础，利用内核中的计数器来进行性能统计。它不但可以分析指定应用程序的性能问题，也可以用来分析内核的性能问题。

总结

这一讲我们讨论了计算机的运算核心，CPU 的结构，尤其是它内部的和性能相关的部件，并澄清了一些术语。

CPU 是服务器性能的最重要的部分；因为不管程序代码如何优化，最后都要转换成指令，让 CPU 来执行。不管其他部件如何和 CPU 交互，最终目的是让 CPU 尽快地拿到指令，并满载执行。

唐代有个诗人叫李贺，他曾经形容跨马奔驰的愿景：

“大漠沙如雪，燕山月似钩。

何当金络脑，快走踏清秋。”

我们常把 CPU 类比大脑，CPU 性能优化的目标，就是让它的运行不受阻碍，如千里马一样任意驰骋。

现代 CPU 提升性能的主要途径是并行化，这方面的策略包括：多处理器、多核、超线程，另外还有流水线架构和超标量等等，都是为了提高并行处理能力。

思考题

你工作中一定碰到过 CPU 方面的性能问题吧？总结一下，有几种表现形式？

比如是总体 CPU 使用量太高，还是计算机的几个核负载不均衡？负载不均衡是什么原因导致的呢？是线程数不够，还是系统调度的问题？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

16 | 内存篇：如何减少延迟提升内存分配效率？

你好，我是庄振运。

上一讲我们讨论了关于 CPU 的性能指标和分析。CPU 和内存是和程序性能最相关的两个领域；那么这一讲，我们就来讨论和内存相关的性能指标和性能分析的工具。

内存方面的性能指标，主要有缓存命中率、缓存一致性、内存带宽、内存延迟、内存的使用大小及碎片、内存的分配和回收速度等，接下来我会逐一进行介绍。现代很多 CPU 都是 NUMA 架构的，所以我也会介绍 NUMA 的影响和常用的工具。

缓存和缓存命中率

我们先看看缓存，也就是 Cache。

缓存是 CPU 与内存之间的临时数据交换器，是为了解决两种速度不匹配的矛盾而设计的。这个矛盾就是 CPU 运行处理速度与内存读写速度不匹配的矛盾。CPU 处理指令的速度，比内存的速度快得多了，有百倍的差别，这一点我们已经在上一讲讨论过。

缓存的概念极为重要。不止是 CPU，缓存的策略也用在计算机和互联网服务中很多其他的地方，比如外部存储、文件系统，以及程序设计上。有人甚至开玩笑说，计算机的各种技术说到底就是三种——Cache（缓存）、Hash（哈希处理）和 Trash（资源回收）。这种说法当然有点偏颇，但你也能从中看到缓存技术的重要性。

现在回到 CPU 缓存的讨论上来。

我们前面也讲了，随着多核 CPU 的发展，CPU 缓存通常分成了三个级别：L1、L2、L3。一般而言，每个核上都有 L1 和 L2 缓存。L1 缓存其实分成两部分：一个用于存数据，也就是 L1d Cache（Data Cache），另外一个用于存指令，L1i Cache（Instruction Cache）。

L1 缓存相对较小，每部分差不多只有几十 KB。L2 缓存更大一些，有几百 KB，速度就要慢一些了。L2 一般是一个统一的缓存，不把数据和指令分开。L3 缓存则是三级缓存中最大的一级，可以达到几个 MB 大小，同时也是最慢的一级了。你要注意，在同一个处理器上，所有核共享一个 L3 缓存。

为什么要采用多级缓存，并逐级增加缓存大小呢？

这个目的，就是为了提高各级缓存的命中率，从而最大限度地降低直接访问内存的概率。每一级缓存的命中率都很重要，尤其是 L1 的命中率。这是因为缓存的命中率对总体的访问时间延迟影响很大，而且下一级缓存的访问延迟往往是上一级缓存延迟的很多倍。

为了加深你的理解，我还是用文章中图片里的延迟数据来举例说明一下。

在图片里你可以看到，L1 的访问时间是 3 个时钟周期，L2 的访问时间是 12 个时钟周期。假如在理想情况下，L1i 的命中率是 100%，就是说每条指令都从 L1i 里面取；那么平均指令访问时间也是 3 个时钟周期。作为对比，如果 L1i 命中率变成 90%，也就是只有 90% 的指令从 L1i 里面取到，而剩下的 10% 需要 L2 来提供。

那么平均指令访问时间就变成了 3.9 个指令周期（也就是：90%*3+10%*12）。虽然看起来只有 10% 的指令没有命中，但是相对于 L1 命中率 100% 的情况，平均访问时间延迟差不多增大了多少呢？高达 30%。

缓存一致性

虽然缓存能够极大地提升运算性能，但也带来了一些其他的问题，比如“缓存一致性问题（cache coherence）”。

如果不能保证缓存一致性，就可能造成结果错误。因为每个核都有自己的 L1 和 L2 缓存，当在不同核上运行同一个进程的不同线程时，如果这些线程同时操作同一个进程内存，就可能互相冲突，最终产生错误的结果。

举个例子，你可以设想这样一个场景，假设处理器有两个核，core-A 和 core-B。这两个核同时运行两个线程，都操作共同的变量，i。假设它们并行执行 i++。如果 i 的初始值是 0，当两个线程执行完毕后，我们预期的结果是 i 变成 2。但是，如果不采取必要的措施，那么在实际执行中就可能会出错，什么样的错误呢？我们来探讨一下。

运行开始时，每个核都存储了 i 的值 0。当第 core-A 做 i++ 的时候，其缓存中的值变成了 1，i 需要马上回写到内存，内存之中的 i 也就变成了 1。但是 core-B 缓存中的 i 值依然是 0，当运行在它上面的线程执行 i++，然后回写到内存时，就会覆盖 core-A 内核的操作，使得最终 i 的结果是 1，而不是预期中的 2。

为了达到数据访问的一致，就需要各个处理器和内核，在访问缓存和写回内存时遵循一些协议，这样的协议就叫缓存一致性协议。常见的缓存一致性协议有 MSI、MESI 等。

缓存一致性协议解决了缓存内容不一致的问题，但同时也造成了缓存性能的下降。在有些情况下性能还会受到严重影响。我们下一讲还会仔细分析这一点，并且讨论怎样通过优化代码来克服这样的性能问题。

内存带宽和延迟

我们讨论了缓存，接下来探讨内存带宽和内存访问延迟。

计算机性能方面的一个趋势就是，内存越来越变成主要的性能瓶颈。内存对性能的制约包括三个方面：内存大小、内存访问延迟和内存带宽。

第一个方面就是内存的使用大小，这个最直观，大家都懂。这方面的优化方式也有很多，包括采用高效的，使用内存少的算法和数据结构。

第二个方面是内存访问延迟，这个也比较好理解，我们刚刚讨论的各级缓存，都是为了降低内存的直接访问，从而间接地降低内存访问延迟的。如果我们尽量降低数据和程序的大小，那么各级缓存的命中率也会相应地提高，这是因为缓存可以覆盖的代码和数据比例会增大。

第三个方面就是内存带宽，也就是单位时间内，可以并行读取或写入内存的数据量，通常以字节 / 秒为单位表示。一款 CPU 的最大内存带宽往往是有限而确定的。并且一般来说，这个最大内存带宽只是个理论最大值，实际中我们的程序使用只能达到最大带宽利用率的 60％。如果超出这个百分比，内存的访问延迟会急剧上升。

文章中的图片就展示了几款 Intel 的 CPU 的内存访问延迟和内存带宽的关系（图片来自https://images.anandtech.com）。

图中内存带宽使用是横轴，相对应的，内存访问延迟是纵轴。你可以清楚地看到，当内存带宽较小时，内存访问延迟很小，而且基本固定，最多缓慢上升。而当内存带宽超过一定值后，访问延迟会快速上升，最终增加到不能接受的程度。

那么一款处理器的内存总带宽取决于哪些因素呢？

答案是，有四个因素，内存总带宽的大小就是这些因素的乘积。这四个因素是：DRAM 时钟频率、每时钟的数据传输次数、内存总线带宽（一般是 64 字节）、内存通道数量。

我们来用几个实际的 Intel CPU 为例，来看看内存带宽的变化。

文章中的这个表格大体上总结了 5 款 Intel CPU 的各级缓存大小、内存通道数目、可使用内存带宽（这里取最大值的 50%）、内存频率和速度。你可以看到，每款新的 CPU，它的内存带宽一般还是增加的，这主要归功于内存频率的提升。

内存的分配

讲过内存带宽，我们再来看看内存的分配。程序使用的内存大小很关键，是影响一个程序性能的重要因素，所以我们应该尽量对程序的内存使用大小进行调优，从而让程序尽量少地使用内存。

不知道你有没有过系统内存用光的经验？每当发生这种情况，系统就会被迫杀掉一些进程，并且抛出一个系统错误：内存用光“OOM（Out of memory）”。所以，一个应用程序用的内存越少，那么 OOM 错误就越不太可能发生。

还有，服务器等容量也是公司运营成本的一部分。如果一台服务器的内存资源足够，那么这样一个服务器系统就可以同时运行多个程序或进程，以最大限度地提高系统利用率，这样就节省了公司运营成本。

再进一步讲，应用程序向操作系统申请内存时，系统会分配内存，这中间总要花些时间，因为操作系统需要查看可用内存并分配。一个系统的空闲内存越多，应用程序向操作系统申请内存的时候，就越快地拿到所申请的内存。反之，应用程序就有可能经历很大的内存请求分配延迟。

比如说，在系统空闲内存很少的时候，程序很可能会变得超级慢。因为操作系统对内存请求进行（比如 malloc()）处理时，如果空闲内存不够，系统需要采取措施回收内存，这个过程可能会阻塞。

我们写程序时，或许习惯直接使用 new、malloc 等 API 申请分配内存，直观又方便。但这样做有个很大的缺点，就是所申请内存块的大小不定。当这样的内存申请频繁操作时，会造成大量的内存碎片；这些内存碎片会导致系统性能下降。

一般来讲，开发应用程序时，采用内存池（Memory Pool）可以看作是一种内存分配方式的优化。

所谓的内存池，就是提前申请分配一定数量的、大小仔细考虑的内存块留作备用。当线程有新的内存需求时，就从内存池中分出一部分内存块。如果已分配的内存块不够，那么可以继续申请新的内存块。同样，线程释放的内存也暂时不返还给操作系统，而是放在内存池内留着备用。

这样做的一个显著优点是尽量避免了内存碎片，使得内存分配效率和系统的总体内存使用效率得到提升。

NUMA 的影响

我们刚刚谈了内存性能的几个方面，最后看看多处理器使用内存的情景，也就是 NUMA 场景。NUMA 系统现在非常普遍，它和 CPU 和内存的性能都很相关。简单来说，NUMA 包含多个处理器（或者节点），它们之间通过高速互连网络连接而成。每个处理器都有自己的本地内存，但所有处理器可以访问全部内存。

因为访问远端内存的延迟远远大于本地内存访问，操作系统的设计已经将内存分布的特点考虑进去了。比如一个线程运行在一个处理器中，那么为这个线程所分配的内存，一般是该处理器的本地内存，而不是外部内存。但是，在特殊情况下，比如本地内存已经用光，那就只能分配远端内存。

我们部署应用程序时，最好将访问相同数据的多个线程放在相同的处理器上。根据情况，有时候也需要强制去绑定线程到某个节点或者 CPU 核上。

工具

内存相关的工具也挺多的。比如，你最熟的内存监测命令或许是 free 了。这个命令会简单地报告总的内存、使用的内存、空闲内存等。

vmstat（Virtual Meomory Statistics， 虚拟内存统计）也是 Linux 中监控内存的常用工具，可以对操作系统的虚拟内存、进程、CPU 等的整体情况进行监视。

我建议你也尽量熟悉一下 Linux 下的 /proc 文件系统。这是一个虚拟文件系统，只存在内存当中，而不占用外存空间。这个目录下有很多文件，每一个文件的内容都是动态创建的。这些文件提供了一种在 Linux 内核空间和用户间之间进行通信的方法。比如 /proc/meminfo 就对内存方面的监测非常有用。这个文件里面有几十个条目，比如 SwapFree，显示的是空闲 swap 总量等。

另外，/proc 这个目录下还可以根据进程的 ID 来查看每个进程的详细信息，包括分配到进程的内存使用。比如 /proc/PID/maps 文件，里面的每一行都描述进程或线程中连续虚拟内存的区域；这些信息提供了更深层次的内存剖析。

总结

我们都知道宋代词人辛弃疾，他曾经这样憧憬他的战场梦想：“马作的卢飞快，弓如霹雳弦惊。” 我们开发的应用程序对内存的分配请求延迟，也有相似的期盼，就是要动作飞快。如果内存分配延迟太大，整个程序的性能自然也高不上去。

如何实现这个梦想呢？就需要我们的代码和程序，尽量降低对内存的使用大小和内存带宽，尽量少地请求分配和释放内存，帮助系统内存状态不至于太过碎片化，并且对代码结构做一些相应地优化。

思考题

你正在开发的系统或者模块，会运行在什么样的服务器上？服务器上有多少内存？如果内存大小可能不够，你会采取什么措施来降低内存使用量呢？再进一步，内存带宽会是瓶颈吗？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

17 | 存储篇：如何根据性能优缺点选择最合适的存储系统？

你好，我是庄振运。

前面两讲我们讨论了 CPU 和内存，今天我们讨论第三个重要的主题：存储系统。现在是大数据时代，这些数据终归要保存到各种存储系统里面，以供读写和分析，因此讨论存储系统的性能问题就很有必要了。

狭义上的存储往往是硬件，比如磁盘、磁带还有固态硬盘。而广义上的存储系统除了指硬件的硬盘，还包括基于网络的存储系统，比如 SAN（Storage Area Network, 存储区域网络）和 NAS 存储（Network Attached Storage，网络接入存储）。

各种存储系统各有优缺点，尤其是性能和成本，所以对不同的需求，我们要选择最合适的存储系统。

我们首先讲存储系统最重要的三大性能指标：IOPS、访问延迟和带宽，然后讲传统硬盘 HDD（Hard Disk Drive）的性能。因为传统硬盘的特性相对简单直白（毕竟业界已经用了几十年了）。这之后再讲固态硬盘的性能（固态硬盘就是 SSD，也叫 Flash）。相对于传统硬盘，SSD 的内部工作原理很不一样，这也就导致它们的性能特性大相径庭。 最后，我们再延伸到基于网络的存储系统，并且介绍几个常用的和存储相关的工具。

存储系统的三大性能指标

一个存储系统的性能最主要的是三个：IOPS、访问延迟、吞吐率 / 带宽。这三个指标其实是互相关联和影响的，但是我们一般还是分开来衡量。

IOPS（Input/Output Per Second），即每秒钟能处理的读写请求数量，这是衡量存储性能的主要指标之一。每个 IO 的请求都有自己的特性，比如读还是写，是顺序读写还是随机读写，IO 的大小是多少等。

什么是顺序读写呢？就是访问存储设备中相邻位置的数据；随机读写呢，则是访问存储设备中非相邻位置的数据。对随机读写进行性能衡量时，一般假定 IO 大小是 4KB。

既然 IO 有这些特点，所以我们讨论存储系统 IOPS 性能的时候，经常需要更加具体的描述。比如顺序读 IOPS、随机写 IOPS 等。

IOPS 的数值会随这样的参数不同而有很大的不同，这些参数的变化，包括读取和写入的比例、其中顺序读写及随机读写的比例、读写大小、线程数量及读写队列深度等。此外，系统配置等因素也会影响 IOPS 的结果，例如操作系统的设置、存储设备的驱动程序特点、操作系统后台运行的作业等。

访问延迟（Access Time）和响应时间（Response Time），指的是从发起 IO 请求，到存储系统把 IO 处理完成的时间间隔，常以毫秒（ms）或者微妙（us）为单位。对这一性能指标，我们通常会考虑它的平均值和高位百分数，比如 P99、P95。

吞吐率（Throughput）或者带宽（Bandwidth），衡量的是存储系统的实际数据传输速率，通常以 MB/s 或 GB/s 为单位。一般来讲，IOPS 与吞吐率是紧密相关的；它们之间的关系是，吞吐率等于 IOPS 和 IO 大小的乘积。

这个也很容易理解，比如对一个硬盘的读写 IO 是 1MB，硬盘的 IOPS 是 100，那么硬盘总的吞吐率就是 100MB/s。需要强调的是，这里 IO 的具体特性很重要，比如是顺序还是随机，IO 大小等。

还有一点要注意，有些存储系统会因为其 IO 队列深度增加，而获得更好的 IO 性能；比如吞吐率会升高，平均访问延迟会降低。

这是为什么呢？这是因为存储系统的 IO 队列处理机制，可以对 IO 进行重新排序，从而获得好的性能。比如，它可以合并几个相邻的 IO，把随机 IO 重新排序为顺序 IO 等。

HDD（传统硬盘）的性能

我们先从你熟知的传统硬盘开始讨论。对于传统硬盘，我们应该比较熟悉它的内部是如何操作的。

简单来说，当应用程序发出硬盘 IO 请求后，这个请求就会进入硬盘的 IO 队列。如果前面有其他 IO，那么这个请求可能需要排队等待。当轮到这个 IO 来存取数据时，磁头需要机械运动到数据存放的位置，这就需要磁头寻址到相应的磁道，并旋转到相应的扇区，然后才是数据的传输。所以，讨论硬盘 IO 的性能时，需要充分考虑这一点。

我们有时候需要把硬盘响应时间和硬盘访问时间分开对待。它们之间的关系是，硬盘响应时间除了包括访问时间外，还包括 IO 排队的延迟，如下图所示。

我们如果拿起一块硬盘仔细看看，硬盘上面往往会标注后面三个参数，分别是平均寻址时间、盘片旋转速度，以及数据传输速度，这三个参数就可以提供给我们计算上述三个步骤的时间。

平均寻址时间一般是几个毫秒。平均旋转时间可以从硬盘转动速度 RPM 来算出。因为每个 IO 请求平均下来需要转半圈，那么如果硬盘磁头每分钟转一万圈（10K RPM），转半圈就需要 3 毫秒。

要注意的是，硬盘上面标注的数据传输速度参数往往是最大值，实际的数据传输时间要取决于 IO 的大小。

对于一块普通硬盘而言，我们前面讲常用的性能数字时也提过，随机 IO 读写延迟就是 8 毫秒左右，IO 带宽大约每秒 100MB，而随机 IOPS 一般是 100 左右。

硬盘的技术也在发展，现代的硬盘也有很多变种。比如采用了多磁头技术，或者几块硬盘组成磁盘阵列，这样的整体 IO 性能也会相应地提升。

SSD（固态硬盘）的技术背景

讲完了传统硬盘，我们接着看看固态硬盘——SSD。SSD 的内部工作方式和 HDD 大相径庭，我们来了解一下。

单元（Cell）、页面（Page）、块（Block）

当今的主流 SSD 是基于 NAND 的，它是将数字位存储在单元中。每个 SSD 单元可以存储一位（SLC，Single Level Cell，单级单元）、两位（MLC，多级单元）、三位（TLC，三级单元），甚至四位（QLC）。

SSD 的特点是，对 SSD 单元的每次擦除，都会降低单元的寿命，因此每一个单元只能承受一定数量的擦除。所以，不同的 SSD 就有这几方面的考虑和平衡。单元存储的位数越多，制造成本就越少，SSD 的容量也就越大。但是耐久性（擦除次数）也会降低。所以高端的 SSD，比如企业级的，基本都是基于 SLC 的。

一个页面包括很多单元，典型的页面大小是 4KB。页面也是读写的最小存储单位。我们知道，HDD 可以直接对任何字节重写和覆盖；但是对 SSD 而言，不能直接进行上述的“覆盖”操作。SSD 的一个页面里面的所有单元，一旦写入内容后就不能进行重写，必须和其它相邻页面一起，被整体擦除、重置。

在 SSD 内部，多个页面会组合成块。一个块的典型大小为 512KB 或 1MB，也就是大约 128 或 256 页。块是擦除的基本单位，每次擦除，都是整个块内的所有页面都被重置。

I/O 和垃圾回收（Garbage Collection）

我们总结一下，对 SSD 的 IO 操作，一共有三种类型：读取、写入和擦除。读取和写入是以页为单位的，也就是说最少也要读取写入一个页面。

IO 写入的延迟，具体取决于磁盘的历史状态，因为如果 SSD 已经存储了许多数据，那么对页的写入，有时需要移动已有的数据，这种情况下写入延迟就比较大。但多数情况下，读写延迟都很低，一般在微秒级别，远远低于 HDD。

擦除是以块为单位。擦除速度相对很慢，通常为几毫秒。所以，对同步的 IO 请求，发出 IO 的应用程序，可能会因为块的擦除而经历很大的写入延迟。为了尽量地减少这样的场景发生，一块 SSD 最好保持一定数量的空闲块，这样可以保证 SSD 的写入速度足够快。

SSD 内部有垃圾回收（GC）机制，它的目的就在于此，就是不断回收不用的块，进行擦除，从而产生新的空闲块来备用。这样可以确保以后的页写入能快速分配到一个全新的页。

写入放大（Write Amplification, or WA）

这是 SSD 相对于 HDD 的一个缺点，即实际写入 SSD 的物理数据量，有可能是应用层写入数据量的多倍。

这是因为，一方面页级别的写入需要移动已有的数据来腾空页面来写入。另一方面，GC 的操作, 也会移动用户数据来进行块级别的擦除。

所以，对 SSD 真正的写操作的数据，肯定比实际写的数据量大，这就是写入放大。因为一块 SSD 只能进行有限的擦除次数，也称为编程 / 擦除（P/E）周期，所以写入放大效用会缩短 SSD 的寿命。

耗损平衡（Wear Leveling）

对每一个块而言，一旦擦除造成的损耗达到最大数量，该块就会“死亡”，再也不能存储数据了。对于 SLC 类型的块，P/E 周期的典型数目是十万次；对于 MLC 块，P/E 周期的数目是一万；而对于 TLC 块，则可能是几千。为了确保整块 SSD 的容量、性能和可靠性，SSD 内部需要对整个 SSD 的各块做平衡，尽量在擦除次数上保持类似。

SSD 控制器具有这样一种机制，也叫“耗损平衡”，来实现这一目标。在损耗平衡操作时，数据在各个块之间移动，以实现均衡的损耗。但是这种机制也有害处，就是会对前面讲的写入放大推波助澜。

SSD 的性能和应用程序的设计

性能方面，SSD 的 IO 性能相对于 HDD 来说，IOPS 和访问延迟提升了上千倍，吞吐率也是提高了几十倍。但是 SSD 的缺点也很明显。主要有三个缺点：

贵；

容量小；

易损耗。

好消息是，随着技术的发展，这三个缺点近几年在弱化。

如今，越来越多的应用程序采用 SSD 来减轻 I/O 性能瓶颈。许多测试和实践的结果都表明，与 HDD 相比，采用 SSD 带来了极大的应用程序性能提升。

但是，我想强调的一点是，在大多数采用 SSD 的部署方案中，SSD 仅被视为一种“更快的 HDD”，并没有真正发挥 SSD 的潜力。

我为什么这么说呢？因为尽管使用 SSD 作为存储时，应用程序可以获得更好的性能，但是这些收益，主要归因于 SSD 提供的更高的 IOPS 和带宽。

但是，SSD 除了提供这些之外，它还有其它特点，比如易损耗，以及其独特的内部机制。如果应用程序的设计能充分考虑 SSD 的内部机制，设计出对 SSD 友好的应用程序，就可以更大程度地优化 SSD，从而进一步提高应用程序性能，也可以延长 SSD 的寿命，并降低运营成本。关于这方面，我后面会有一讲专门讨论。

基于网络的存储系统

我们前面讨论了存储硬件，这些存储硬件可以直接安装在服务器上，构成单机系统。和单机系统和场景相对应，也有很多非单机使用的场景。

在非单机使用的场景里，这些存储硬件也被包装在各种基于网络的存储系统里。这样的存储系统也有很多种，比如 DAS、NAS 和 SAN。

DAS（Directed Attached Storage）是直连式存储。这是以服务器为中心的存储系统，存储设备直接通过 I/O 总线连在服务器主机上。这种存储一般运行 SATA 或者 SAS 等协议，可以让网络的客户端直接使用。

NAS（Network Attached Storage）是网络接入存储。在 NAS 存储结构中，存储系统不再通过 I/O 总线只属于某个特定的服务器，而是通过网络接口直接与网络相连。NAS 提供的是文件服务器的功能（比如 NFS 和 CIFS），供客户通过网络访问。

SAN（Storage Area Network）是存储区域网络。SAN 是一种以网络为中心的存储系统，通常有高性能专用网络（比如光纤）来支持，运行 iSCSI 等协议。

工具

最后，我们看看常用的存储系统性能监测和测试工具。存储系统的测试和监控命令工具非常多，下面简单介绍几个。

Linux 系统上可以采用 fio 工具进行各种组合的 IO 测试。这些组合包括读写比例、随机还是顺序读写、IO 大小等等。

IOMeter 也是不错的测试磁盘性能的工具，比如可以测试 I/O 的传输速度和平均的 I/O 响应时间。

IOZone 是一个文件系统基准测试工具，可以测试不同的操作系统中文件系统的读写性能。

Bonnie++ 是基于 Linux 平台的开源磁盘 IO 测试的工具，可以用它来测试磁盘和文件系统的 I/O 性能。

hdparm 可以用来作跳过文件系统的纯硬件操作测试。

iostat 这个工具可以查看进程发出 IO 请求的数量、系统处理 IO 请求的耗时、磁盘的利用率等，也可以分析进程与操作系统的交互过程中 IO 方面是否存在瓶颈。

总结

存储系统，顾名思义，是用来存放我们各种程序和服务的数据。随着现代互联网服务的大数据化，几乎所有的业务都离不开存储系统的支持。

各种存储系统的基础是传统硬盘或者固态硬盘，这两种硬盘在成本、性能、大小方面各有千秋。我想起了宋代有首诗叫《雪梅》，里面比较了白雪和梅花的优缺点：

“梅须逊雪三分白，雪却输梅一段香。”

这句诗用来形容传统硬盘和固态硬盘的关系还挺合适的。

在实际使用中，我们要注意它们的性能优缺点，从而适当来作取舍。比如，如果系统对 IOPS 或者延迟要求很高，恐怕只有 SSD 才能满足要求。反之，如果数据量极大需要降低成本，那么只能选择磁盘或者磁带系统了。

思考题

你对 SSD 这种新型存储了解多少？你公司里面有没有系统使用 SSD？它们碰到的性能问题有哪几个方面？运维和开发人员是怎么一起解决这些问题的？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

18 | 网络篇：如何步步拆解处理复杂的网络性能问题？

你好，我是庄振运。

前面几讲，我们讨论了 CPU、内存和存储系统的性能。不过你也清楚，各种互联网服务的数据传递终归是需要通过网络来传输的，所以网络性能也是至关重要的。所以，今天我们就来接着学习网络性能相关的内容。

这一讲我的讲解，依然是会逐步递进，先介绍有关网络的几个性能指标，并从单机的网络性能说起，之后推进到数据中心内部的网络性能，接着再进一步阐释互联网和内容分发网络的性能。最后，再为你介绍几个常用网络性能工具。

网络的性能指标

先来说说网络的性能指标。网络性能的五个常用的指标是：

可用性（availability）

响应时间（response time）

网络带宽容量（network bandwidth capacity）

网络吞吐量（network throughput）

网络利用率（network utilization）

第一个指标是可用性，理解起来比较简单。你肯定有过网络不通（也就是断网）的糟糕体验。没有网络，你几乎啥都干不了。所以，对于网络来讲，最重要的就是，网络是否可以正常联通。

如何测试网络可用性呢？

最简单的方法，就是使用 ping 命令。这个命令其实就是向远端的机器发送 ICMP 的请求数据包，并等待接收对方的回复。通过请求和应答返回的对比，来判断远端的机器是否连通，也就是网络是否正常工作。

第二个指标是响应时间。端到端的数据一次往返所花费时间，就是响应时间。响应时间受很多因素的影响，比如端到端的物理距离、所经过网络以及负荷、两端主机的负荷等。

第三个指标是网络带宽容量。它指的是在网络的两个节点之间的最大可用带宽，这一指标一般是由设备和网络协议决定的，比如网卡、局域网和 TCP/IP 的特性。如果是向网络提供商购买的带宽，那么购买的数量就是网络的带宽容量。

第四个指标网络吞吐量是指在某个时刻，在网络中的两个节点之间，端到端的实际传输速度。网络吞吐量取决于当前的网络负载情况，而且是随着时间不同而不断变化的。

第五个指标网络利用率是指网络被使用的时间占总时间的比例，一般以百分比来表示。因为数据传输的突发性，所以实际中的网络利用率一般不会太高。否则的话，那么响应时间就不能保证。

单机的网络性能

了解了网络方面性能的指标后，我们接着讨论具体的网络性能。虽然网络传输需要两端进行，但是我们必须从单机开始，来清楚地了解网络的协议栈。

网络协议其实相当复杂，而且分很多层级。操作系统内核中，最大的一个子系统或许就是网络。网络子系统由多个协议组成，其中每个协议都在更原始的协议之上工作。大学时你应该学过 OSI 模型，或 TCP / IP 协议栈，里面的分层定义对网络协议非常适用。当用户数据通过网络协议栈传递时，数据会被封装在该协议的数据包中。

在考虑多层协议的交互时，我们尽量把思路简化。其实它们之间的关系很简单，就是网络协议栈的每一层都有其职责，与其他高层和低层协议无关。

举个例子来说，我们看 IP 层的协议。IP 是第三层协议，它是通过路由器和网络发送端到端的数据报；它的主要目的，就是在网络中的每一段找到路由路径，从而最终能够到达数据报的接收地。但它不保证数据的有序性，也就是转发过程中几个数据报会重新排序；同时也不能保证整个数据的完整性和可靠性，比如丢失的数据报那就是丢失了，IP 层不会重传。

那么数据的可靠性是谁保证的呢？这就需要更上层的 TCP 协议实现。TCP 层协议通过检测数据丢失并且重传，来保证数据的可靠性，也通过序列号来保证数据的有序性。

但是这两层协议：TCP 和 IP 两层协议，都只能传输原始的数据；而对于数据本身是否被压缩过，这些数据表示什么，是在更高层的协议（如 HTTP 和应用层）上实现的。

我们接着具体到网络协议的程序实现，在 Unix 系统中，网络协议栈可以大体分为三层。从上到下，第一层是通过一系列系统调用，实现 BSD 套接字的套接字层，比如 sendmsg() 函数；第二层是中间协议的程序，例如 TCP/IP/UDP；第三层是底部的媒体访问控制层，提供对网络接口卡（NIC）本身的访问。

如下图所示（图片来自于https://myaut.github.io/ ）。

注意这个图，右边是发送端，左边是接收端。先从右边看起，最上层就是 BSD 套接字。它通过一系列 API 调用（比如 connect() 等），然后是 TCP 的发送，再到 IP 的发送，最后到网卡的发送缓冲区，并最终通过网卡发出。 接收端，也就是图的左边，则会经过相反的顺序，逐层到达应用层。

数据中心的网络性能

谈完单机的网络性能，我们接着讨论端到端的互联网数据传输。

一台服务器和互联网的远端服务器进行数据传输的时候，需要经过好几层交换器和不同的网络。数据从一台服务器的网卡出来之后，下一步就是经过机柜上面的交换器，然后是数据中心内部的网络，再进入互联网骨干网络。接收端的情况正好相反。

对于这些中间的网络构件，我们一个一个地讨论一下。

机柜交换器（TOR, Top Of Rack; or RSW, Rack Switch）

数据中心里面的服务器不是单独放置的，一般是几十台服务器组成了一个机柜。机柜上面会有机柜交换器。这个机柜交换器的作用，一方面让机柜内部的服务器直接互通；另一方面，机柜交换器会有外联线路，连接到数据中心的骨干网络。

数据中心网络

数据中心网络里面也分了好几层，从 TOR 到集群交换器（Cluter Switch），再到集合交换器（Aggregation Switch）等，最后到数据中心路由器。

下面的图示，简单展示了机柜内部的网络和 POD 内部网络。TOR1 是机柜交换器，负责机柜内部几十台服务器之间的数据交换。POD1 内部网络包含很多机柜。

这里有一个值得注意的地方是，就是这个多层次结构，一般是越往上层，总的带宽越少。

比如，服务器的网卡带宽是 25Gbps，即使这个机柜内有 30 台服务器，总带宽就是 750Gbps，机柜交换器 TOR 的外联带宽有多少呢？可能只有 100Gbps。这个差距就叫带宽超订（over-subscription）。同样的，POD 交换器之间的带宽会继续变小。之所以允许带宽超订，是因为多数的数据交换是在内部进行的，不会全部都和外部进行交换。

知道这一点是必要的，因为我们做网络方面和服务部署优化的时候，需要考虑这点，不要让高层的网络带宽成为性能瓶颈。比如假设两个服务分别用不同的服务器，它们之间的数据交换如果很多的话，就尽量让它们运行在同一个机柜的服务器里面；如果不能保证同一个机柜，就尽量是同一个 POD 内部。

互联网的网络性能

讲完了数据中心内部的网络，我们继续往外扩展，讨论互联网的网络性能。互联网上运行的是 TCP/IP 协议。这两个协议本身比较复杂，有很多和性能相关的特性值得仔细学习。

一个常见的性能问题就是丢包。TCP 对丢包非常敏感，因为每次丢包，TCP 都认为是网络发生了拥塞，因此就会降低传输速度，并且采取重传来恢复，这就影响网络性能。

实际情况中，造成丢包的原因有很多，不一定就是网络拥塞。因此我们需要进行各种测试观测来做根因分析。

通常的丢包原因，是端到端的网络传输中的某一段发生了问题，或许是拥塞，或许是硬件问题，也或许是其他软件原因。我们需要一步步地逐段逐层地排除。

对于网络的每段，可以用工具（比如 Traceroute）来发现每一段路由，然后逐段测试。

对于协议的每层，你都可以用相关工具进行分析。比如，你可以分为 TCP 层、操作系统、网卡驱动层，分别分析。具体来讲，对 TCP 层，可以用比如 netstat 来观察是不是套接字缓存不够；对操作系统，可以观察 softnet_stat，来判断 CPU 的查询队列；对网卡驱动层，可以用 ethtool 等来进行分析。

内容分发网络（CDN）的性能

当今互联网几乎普遍采用内容分发网络来提高网络性能。内容分发网络，也叫 CDN（Content Delivery Network 或 Content Distribution Network，CDN），是一种分布式网络，它可以有效地将 Web 内容交付给用户。

内容分发网络的基本原理是，利用最靠近每位用户的服务器，更快、更可靠地将（音乐、图片及其他）文件发送给终端用户，而不是每次都依赖于中心服务器。靠近用户的服务器，一般叫边缘服务器，会把请求的内容最大限度地缓存，以尽量地减少延迟。

我们用下图讲述它的基本工作方式，图里有两个用户和一个边缘服务器（Edge Server），以及一个源头服务器（Origin Server）。

第 1 步：用户 A 通过使用具有特殊域名的 URL 来请求文件。DNS 将请求重定向到性能最佳的边缘服务器，该位置通常是地理位置上最接近用户的服务器。

第 2 步：如果重定向的边缘服务器 A 中没有文件，则边缘服务器会向源头服务器请求该文件。源头服务器可以是任何可公开或非公开访问的 Web 服务器。

第 3 步：源头服务器将文件返回到边缘服务器。

第 4 步：边缘服务器先缓存文件，并将文件返回给原始请求者。该文件将保留在边缘服务器上，这样下次这个文件就可以迅速返回请求的客户。这个文件会保存到什么时候呢？时间由其 HTTP 标头指定的生存时间（TTL）到期为止。

第 5 步：如果用户 B 用相同 URL 请求相同的文件，也可能定向到相同的边缘服务器。

第 6 步：如果文件的 TTL 尚未过期，则边缘服务器直接从缓存中返回文件。这样的用户体验就更快。

内容分发网络的边缘服务器节点会在多个地点，多个不同的网络上摆放。这些节点之间通常会互相传输内容，对用户的下载行为最优化，并借此改善用户的下载速度，提高系统的稳定性。同时，将边缘服务器放到不同地点，也可以减少网络互连的流量，进而降低带宽成本。

内容分发网络提供商往往有很大的规模，比如几十万台服务器。对服务的客户提供所需要的节点数量会随着需求而不同。

工具

我们最后来看看网络性能有关的工具。这方面的性能测试和观测工具也不少，这里简单介绍几个。

Netperf 是一个很有用的网络性能的测量工具，主要针对基于 TCP 或 UDP 的传输。Netperf 有两种操作模式：批量数据传输和请求 / 应答模式，根据应用的不同，可以进行不同模式的网络性能测试，Netperf 测试结果所反映的，是一个端到端的系统能够以多快的速度发送数据和接收数据。

Iperf 这个工具也以测试 TCP 和 UDP 带宽质量，比如最大 TCP 带宽，延迟抖动和数据包丢失等性能参数。

Netstat 这一命令可以显示与 IP、TCP、UDP 和 ICMP 协议相关的统计数据，提供 TCP 连接列表，TCP 和 UDP 监听，进程内存管理的相关报告，一般用于检验本机各端口的网络连接情况。

Traceroute 这一命令可以帮我们知道，数据包从我们的计算机到互联网远端的主机，是走的什么网络路径。

总结

不管计算机技术如何发展，网络永远是极其重要的一部分；而且随着互联网业务的多样化和复杂化，对网络性能的要求只会越来越高。不管应用如何变化，高性能网络必不可少，而且会变得更复杂。这让我想起了宋朝大词人张先的《千秋岁》里面的几句词，“天不老，情难绝。心似双丝网，中有千千结。”

网络是整个互联网服务的一部分，各种因素之间互相影响。和网络性能演化密切相关的因素包括：上层服务需求的越来越多样化，计算机硬件和操作系统的持续进化，各层网络协议的不断优化，以及底层介质的演化（比如 5G）。

复杂的网络性能问题会受所有这些因素的影响，似有“千千结”。所以，网络性能分析和优化，需要仔细分析和考虑这些方面。

我们这一讲讨论了网络性能的几个性能指标，也讨论了单机网络协议，数据中心的网络，互联网和内容分发网络。

对于网络性能要求，简单来说就是能联通、响应快、带宽高；并且在部署大流量服务时，可以优化服务器的部署，来尽量减少外部网络流量；也可以使用 CDN 来加速数据传输。

思考题

你们公司的互联网服务使用内容分发网络，也就是 CDN 吗？是哪个 CDN 公司提供的？

除了你们使用的这家 CDN 公司，市场上还有哪些 CDN 提供商？选择 CDN 提供商的时候，你们会考虑哪些方面吗？比如规模、带宽、价格、服务质量等等。

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

19 | 性能优化六大原则：三要三不要，快速有效地进行优化

你好，我是庄振运。

今天我们进入了专栏的新模块：性能优化。在这个模块里，我会先从“性能优化的六大原则”开始讲起，然后再为你讲解实践中普遍采用的十个性能优化策略，并且分别针对 CPU、系统、存储以及跨层这几个领域，讲讲具体的优化案例。

我们今天先探讨性能优化的原则。在讲具体原则之前，我想先给你讲一个有趣的往事。

我曾经负责过一个存储服务的性能优化和容量效率。那个服务的容量需求很大，但它的最大性能瓶颈不是 CPU，而是存储的空间。

所以，虽然有很多人给我们各种建议，让我们花时间做 CPU 优化，我都尽量挡了回去。因为我知道 CPU 不是最大问题，所以坚持不懈地通过各种途径优化数据大小，甚至以牺牲 CPU 为代价。最后的结果很好，大幅度地降低了那个服务的容量需求。

在这个性能优化的场景，我们遵循了一个原则，那就是优先优化最大性能瓶颈。这其实就是马上要讲到的“三要”原则中的第一个。

性能优化的原则概述

在实际的性能优化中，我们需要考虑的因素很多，也经常需要在多个角度和目标间做一些平衡和取舍。为了帮助你把握这些，我个人总结出了六条原则，我把它们概括为：“三要，三不要”。

三个“要”原则是：要优先查最大的性能瓶颈，性能分析要确诊性能问题的根因，性能优化要考虑各种的情况。

三个“不要”的原则是：不要做过度的、反常态的优化，不要过早做不成熟的优化，不要做表面的肤浅优化。

接下来，我们一个个地讲一下这六个优化原则。

三个“要”的原则

你可能已经发现了，这三个“要”的原则之间，其实是步步递进的关系。也就是首先需要查找最大性能瓶颈，然后确诊性能瓶颈产生的原因，最后针锋相对地提出最好的解决方案。

而这个最优解，往往是在考虑各种情况之后提出来，并最终被选中的。

要优先查最大的性能瓶颈

任何一个应用程序或者系统，总会有很多地方可以优化。

可是你知道要从何处下手吗？

其实你可以参考古人的观点，唐代的诗人杜甫说过：“射人先射马，擒贼先擒王”。会打仗的将军，一定会从“最值得打的地方”开始，性能优化工作也是如此。

我们永远都要优先从最大的性能瓶颈入手。

一般来讲，如果找到最大的性能瓶颈，并且解决了它，那这个系统的性能会得到最大的提升（参见第 4 讲帕累托法则）。反之，如果不解决最大的性能瓶颈，反而退而求其次，去解决了其他的性能问题，整个系统的性能或许会更高一些，但是提升的程度往往是非常有限的。

比如，一个应用程序的最大性能瓶颈是 CPU 的使用率太高；而其他种类型的资源，比如网络和存储，都很有富余。这种情况下，如果你去优化网络和存储方面，显然是不能大幅度地提升整体性能的。

不过这里有个稍微特殊的情况，就是内存。因为内存的分配和回收也会消耗一些 CPU 资源，如果这时去优化内存使用，很多时候，的确会帮助你降低 CPU 使用率，不过降低的幅度一定是很有限的。

归根结底，要降低 CPU 的使用率，最有效的方法是做性能分析和剖析，找出程序中使用 CPU 最多的地方，然后对症下药地做优化。

要确诊性能问题的根因

我们前面讲过，程序和系统如果在某个地方有性能瓶颈，肯定是这个地方的资源不够用了，不管是 CPU、内存还是网络（参见第 14 讲）。

所以，当我们确定了最大的性能瓶颈后，就需要对这一性能瓶颈做彻底的性能分析，找出资源不够使用的原因，也就是考察使用资源的地方。

一种资源被使用的地方往往有好几个，我们需要一个一个地去分析考虑。只有彻底分析了各种使用的情况，才能进一步找出最主要的，也是最可能优化的原因，对症下药。

有些资源使用的原因也许是完全合理的。对这些合理的使用，有些或许已经仔细优化过了，很难再做优化。而另外一些则有可能继续优化。对资源的不合理使用，我们就要尽量想办法去掉。

对于需要优化的地方，我们就需要进一步考虑优化工作的投入产出比例，就是既考虑成本，也考虑带来的好处。因为有些情况下，虽然你可以去优化，但获得的收益并不大，所以不值得去做。

另外要提醒你的是，确诊性能问题的原因有时候非常困难，需要做多方面的性能测试、假设分析并验证。

比如 CPU 的使用，有操作系统的原因，有应用程序的原因，但也有些 CPU 的问题，是在非常边缘的场景下才发生的。为了暴露问题，我们经常需要创造特殊的场景来重现遇到的性能问题。

要考虑各种情况下的性能

性能问题确诊原因后，我们就可以进入下一步，找解决方案了。一般说来，找一个解决方案并不难，甚至找好几个方案也不难；但是找出一个好的、最优的解决方案是真心不容易。

为什么这么说呢？

因为实际生产环境很复杂，而且会出现各种各样的特殊场景。

针对某个具体场景提出的一个解决方案，多半并不能适应所有的场景。

所以，对提出的各种方案进行评估时，我们必须考虑各种情况下这个方案可能的表现。如果一个方案在某些情况下会导致其他严重的问题，这个方案或许就不是一个好的方案。

但同时你也需要意识到，任何解决方案都有长短，有 Tradeoff。如果苛求一个能在所有场景下都最优的解决方案，往往是缘木求鱼，是不现实的。

比如一种优化方案，可以让平均响应时间最小，但高百分位比较高。另外一种优化方案正好相反。那我们就需要考虑对自己来说哪种指标更重要。也就是说，我们经常需要在不同性能指标间权衡，以找到一个最优解能达到总体和整体最优。

这就需要我们有一个整体的意识和判断。

或许一个方案并不能面面俱到，在有些场景下性能不好。但是不同场景的出现概率不同，对其它模块造成的影响也不一样，并且最终客户的体验也不尽相同。这些因素都要考虑到取舍的决策过程中。

综合以上因素，在实际的优化过程中，我们经常会反复权衡利弊和取舍来做最终决定。

三个“不要”的原则

讲完了三个“要”的原则，我们接着来看三个“不要”的原则。

不要过度地反常态优化

性能优化的目标，是追求最合适的性价比或最高的投入产出比，在满足要求的情况下，尽量不要做过度的优化。过度的优化会增加系统复杂度和维护成本，使得开发和测试周期变长。虽然性能上带来了一定程度的提升，但是和导致的缺点来比，孰轻孰重尚不可知，需要仔细斟酌，衡量得失。

我的建议是，根据产品的性能要求来决策。

在设计产品时，我们对产品的性能会有一定的要求，比如吞吐量，或者客户响应时间要达到多少多少。如果达不到这个既定指标，就需要去优化。反之，如果能满足这些指标，那么就不必要花费太多时间精力去优化。

比如，我们要设计一个内部查询系统，预计最多只有一百个人同时在线使用的话，就完全不用按照百万在线用户的目标去过度优化。

更重要的是，多数的优化方法是并不是完美无缺的，是有缺点的，尤其是可能会对系统设计的简化性，对代码的可读性和可维护性有副作用。如果系统简化性和代码可读性更加重要，当然就更不能过度优化。

不要过早的不成熟优化

要体会这一原则，我们先引用著名计算机科学家高德纳（Donald Knuth）的一段话：“现实中的最大问题是，程序员往往花太多时间，来在错误的地方和错误的时间来试图提高效率和性能。过早的优化，是编程中所有邪恶和悲剧（或至少是大多数邪恶和悲剧）的根源。”

你只要稍微思考一下高德纳的话，就会发现，这句话在很多场景下都是很有道理的。

比如，在敏捷开发过程中，尤其是在面对一个全新的产品时，在业界没有先例和经验可遵循的情况下，最看重的特点是快速的迭代与试错，“尽快推出产品”是最重要的。这时，过早的优化很可能优化错地方，也就是优化的地方并非真正的性能瓶颈，因此让“优化工作”成为了无用功。而且，越早的优化就越容易造成负面影响，比如影响代码的可读性和维护性。

我个人认为，如果一个产品已经在业界很成熟，大家非常清楚它的生产环境特点和性能瓶颈，那么优化的重要性可以适当提高。否则的话，在没有实际数据指标的基础上，为了一点点的性能提升而进行盲目优化，的确是得不偿失的。

不要表面的肤浅优化

性能优化很忌讳表面和肤浅的优化，也就是那种“头痛医头，脚痛医脚”的所谓“优化”。如果对一个程序和服务没有全局的把握，没有理解底层运行机制，任何优化方案都很难达到最好的优化效果。

比如，如果你发现一个应用程序的 CPU 使用率并不高，但是吞吐率上不去，表面的优化方式可能是增大线程池来提升 CPU 使用率。这样的简单“优化”或许当时能马上看到效果，比如吞吐率也上去了，但是如果你仔细想想，就会发现如此的表面优化非常有问题。

这样的情况下，线程池开多大最合适？需不需要根据底层硬件和上层请求的变化而对线程池的大小调优呢？如果需要，那么手工调整线程池大小就是一个典型的“头痛医头”的优化。

为什么呢？

因为部署环境不会一成不变，比如以后 CPU 升级了，核数变多了，你怎么办？再次手工去调整吗？这样做很快会让人疲于奔命，难以应付，并且很容易出错。

对这样的场景，正确的优化方式，是彻底了解线程的特性，以优化线程为主。至于线程池的大小，最好能够自动调整。千万别动不动就手工调优。如果这样手工调整的参数多了，就会做出一个有很多可调参数的复杂系统，很难用，也很难调优，很不可取。就比如我们都熟悉的 JVM 调优，有上千个可调参数，非常被人诟病。

总结

唐朝的名相魏征说过：“求木之长者，必固其根本”。意思是说，如果要一棵树长得高，必须让它的根牢固。否则的话，正如魏征自己所说：“根不固而求木之长”，一定是“知其不可”。根基不牢固，就不可能长成参天大树。

同理，对现代互联网的服务和系统来说，性能问题是根本的问题。如果不知道系统的性能瓶颈，查不出性能根因，不知道如何解决，无法做合理的优化，这个服务和系统一定不会高效。

这一讲我们总结了六个性能优化的原则，这些原则的终极目的，就是找出性能的最大瓶颈，查出根因，并作做相应的最优优化，从而让我们的系统这棵树长高、长大。

思考题

今天介绍的六大原则里面，你认为哪个原则最重要？为什么？

你过去的工作中有没有因为没有遵循这几个原则而吃后悔药的时候？比如你在选择一个数据结构的具体实现时（比如 Set），有没有考虑各种场景下的性能？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

20 | 性能优化十大策略：如何系统地有层次地优化性能问题？

你好，我是庄振运。

上一讲中，我们聊了性能优化的六大原则。原则有了，但是在针对实际的性能问题的时候，用什么样的解决方案才可以提升性能呢？这就需要你了解具体的优化策略了。

现实中的性能问题和具体领域千差万别，我也不可能面面俱到。但是为了帮助你理解，我总结了十大常用的优化策略。

我将这十大策略分成五个类别，每个类别对应两个相关策略，帮助你掌握。这五个类别是：时空相互转换、并行 / 异步操作、预先 / 延后处理、缓存 / 批量合并、算法设计和数据结构。我们现在一个个来讲。

时空转换

第一个策略类别是“时空转换”。我们看科幻电影和小说的时候，经常会看到时空转换这个题材。性能优化里面有两个策略恰好组成了这个类别，包括“用时间换空间”和“用空间换时间”这两个看似互相对立的策略。

1. 用时间换空间

用时间换空间的策略，出发点是内存和存储这样的“空间”资源，有时会成为最稀缺的资源，所以需要尽量减少占用的空间。比如，一个系统的最大性能瓶颈如果是内存使用量，那么减少内存的使用就是最重要的性能优化。

这个策略具体的操作方法有几种：

改变应用程序本身的数据结构或者数据格式，减少需要存储的数据的大小；

想方设法压缩存在内存中的数据，比如采用某种压缩算法，真正使用时再解压缩；

把一些内存数据，存放到外部的、更加便宜的存储系统里面，到需要时再取回来。

这些节省内存空间的方法，一般都需要付出时间的代价。

除了内存，还有一种常见的场景是，降低数据的大小来方便网络传输和外部存储。压缩的方法和算法有很多种， 比如现在比较流行的 ZStandard（ZSTD）和 LZ4。这些算法之间有空间和时间的取舍。

衡量任何压缩算法，基本上看三个指标：压缩比例、压缩速度以及使用内存。

如果系统的瓶颈在网络传输速度或者存储空间大小上，那就尽量采取高压缩比的算法，这样用时间来换空间，就能够节省时间或者其他方面的成本。

2. 用空间换时间

“用空间换时间”就是对“用时间换空间”策略反其道而行之。有些场景下，时间和速度更加重要，但是空间尚有富余，这时我们就可以考虑用空间来换时间。

这里要注意的一点是，我们后面还会讲一条关于使用缓存的策略。虽然缓存的策略理论上也是一种“空间换时间”的方式，但我们在这里把它分开来讲，这是因为缓存策略的“空间”定义与一般的“空间换时间”不同。一般来讲，“缓存”使用的空间，和原来的空间不在同一个层次上，添加的缓存往往比原来的空间高出一个档次。而我们这里“空间换时间”的策略，里面的“空间”是和原来的空间相似的空间。

互联网的服务往往规模很大，比如全国的服务甚至是全球的服务。用户分布在各地，它们对访问时间的要求很高，这就要求被访问的数据和服务，要尽量放在离他们很近的地方。“空间换时间”就是对数据和服务进行多份拷贝，尽可能地完美覆盖大多数的用户。我们前面讲过的 CDN 内容分发网络技术就可以归类于此。

其实我们部署的任何大规模系统，都或多或少地采用了用空间换时间的策略，比如在集群和服务器间进行负载均衡，就是同时用很多个服务器（空间）来换取延迟的减少（时间）。

预先和延后处理

优化策略的第二大类是“预先和延后处理”，这一类别也有两个互相对立的策略。一个是预先或者提前处理，另外一个是延后或者惰性处理。

3. 预先 / 提前处理

预先 / 提前处理策略同样也表现在很多领域，比如网站页面资源的提前加载。Web 标准规定了至少两种提前加载的方式：preload 和 prefetch，分别用不同的优先级来加载资源，可以显著地提升页面下载性能。

很多文件系统有预读的功能，就是提前从磁盘读取额外的数据，为下次上层应用程序读数据做准备。这个功能对顺序读取非常有效，可以明显地减少磁盘请求的数量，从而提升读数据的性能。

CPU 和内存也有相应的预取操作，就是将内存中的指令和数据，提前存放到缓存中，从而加快处理器执行速度。缓存预取可以通过硬件或者软件实现，也就是分为硬件预取和软件预取两类。

硬件预取是通过处理器中的硬件来实现的。该硬件会一直监控正在执行程序中请求的指令或数据，并且根据既定规则，识别下一个程序需要的数据或指令并预取。

软件预取是在程序编译的过程中，主动插入预取指令（prefetech），这个预取指令可以是编译器自己加的，也可以是我们加的代码。这样在执行过程中，在指定位置就会进行预取的操作。

4. 延后 / 惰性处理

延后 / 惰性处理策略和前面说的预先 / 提前处理正好相反。就是尽量将操作（比如计算），推迟到必需执行的时刻，这样很可能避免多余的操作，甚至根本不用操作。

运用这一策略最有名的例子，就是 COW（Copy On Write，写时复制）。假设多个线程都想操作一份数据，一般情况下，每个线程可以自己拷贝一份，放到自己的空间里面。但是拷贝的操作很费时间。系统如果采用惰性处理，就会将拷贝的操作推迟。如果多个线程对这份数据只有读的请求，那么同一个数据资源是可以共享的，因为“读”的操作不会改变这份数据。当某个线程需要修改这一数据时（写操作），系统就将资源拷贝一份给该线程使用，允许改写，这样就不会影响别的线程。

COW 最广为人知的应用场景有两个。一个是 Unix 系统 fork 调用产生的子进程共享父进程的地址空间，只有到某个子进程需要进行写操作才会拷贝一份。另一个是高级语言的类和容器，比如 Java 中的 CopyOnWrite 容器，用于多线程并发情况下的高效访问。C++ 里面经常使用的 STL 标准模板库中的很多类，比如 string 类，也是具有写时才拷贝技术的类。

并行 / 异步操作

优化策略的第三大类是“并行 / 异步操作”。并行和异步两种操作虽然看起来很不一样，其实有异曲同工之妙，就是都把一条流水线和处理过程分成了几条，不管是物理上分还是逻辑上分。

5. 并行操作

并行操作是一种物理上把一条流水线分成好几条的策略。直观上说，一个人干不完的活，那就多找几个人来干。并行操作既增加了系统的吞吐量，又减少了用户的平均等待时间。比如现代的 CPU 都有很多核，每个核上都可以独立地运行线程，这就是并行操作。

并行操作需要我们的程序有扩展性，不能扩展的程序，就无法进行并行处理。这里的并行概念有不同的粒度，比如是在服务器的粒度（所谓的横向扩展），还是在多线程的粒度，甚至是在指令级别的粒度。

绝大多数互联网服务器，要么使用多进程，要么使用多线程来处理用户的请求，以充分利用多核 CPU。另外一种情况就是在有 IO 阻塞的地方，也是非常适合使用多线程并行操作的，因为这种情况 CPU 基本上是空闲状态，多线程可以让 CPU 多干点活。

6. 异步操作

异步操作这一策略和并行操作不同，这是一种逻辑上把一条流水线分成几条的策略。

我们首先在编程的领域澄清一下概念：同步和异步。同步和异步的区别在于一个函数调用之后，是否直接返回结果。如果函数挂起，直到获得结果才返回，这是同步；如果函数马上返回，等数据到达再通知函数，那么这就是异步。

我们知道 Unix 下的文件操作，是有 block 和 non-block 的方式的，有些系统调用也是 block 式的，如：Socket 下的 select 等。如果我们的程序一直是同步操作，那么就会非常影响性能。采用异步操作的话，虽然稍微增加一点程序的复杂度，但会让性能的吞吐率有很大提升。

现代的语言往往对异步操作有比较好的支持，使得异步编程变得更加简单，可读性也更好。

缓存 / 批量合并

“缓存 / 批量合并”是优化策略中的第四大类。缓存和批量合并这两个策略，有些场景下会同时起作用，所以我把它们放在一起。

7. 缓存数据

缓存的本质是加速访问。这是一个用得非常普遍的策略，几乎体现在计算机系统里面每一个模块和领域，CPU、内存、文件系统、存储系统、内容分布、数据库等等，都会遵循这样的策略。

我们最熟悉的应该就是 CPU 的各级缓存了。在文件系统、存储系统和数据库系统里面，也有快速缓存来存储经常访问的数据，目的是尽量提高缓存命中率，从而避免访问比较慢的存储介质。

对于一个基于 Web 的应用服务，前端会有浏览器缓存，有 CDN 存放在边缘服务器上，有反向代理提供的静态内容缓存；后端则还会有服务器本地缓存。

程序设计中，对于可能重复创建和销毁，且创建销毁代价很大的对象（比如套接字和线程），也可以缓存，对应的缓存形式，就是连接池和线程池等。

对于消耗较大的计算，也可以将计算结果缓存起来，下次可以直接读取结果。比如对递归代码的一个有效优化手段，就是缓存中间结果。

8. 批量合并处理

在有 IO（比如网络 IO 和磁盘 IO）的时候，合并操作和批量操作往往能提升吞吐量，提高性能。

我们最常见的是批量 IO 读写。就是在有多次 IO 的时候，可以把它们合并成一次读写数据。这样可以减少读写时间和协议负担。比如，GFS 写文件的时候，尽量批量写，以减少 IO 开销。

对数据库的读写操作，也可以尽量合并。比如，对键值数据库的查询，最好一次查询多个键，而不要分成多次。

涉及到网络请求的时候，网络传输的时间可能远大于请求的处理时间，因此合并网络请求也很有必要。上层协议呢，端到端对话次数尽量不要太频繁（Chatty），否则的话，总的应用层吞吐量不会很高。

更先进算法和数据结构

优化策略中的最后一个大类就是“更先进算法和数据结构”。这两个策略是紧密配合的，比如先进的算法有时候会需要先进的数据结构；而且它们往往和程序的设计代码直接相关，所以放在一起。

9. 先进的算法

同一个问题，肯定会有不同的算法实现，进而就会有不同的性能。比如各种排序算法，就是各有千秋。有的实现可能是时间换空间，有的实现可能是空间换时间，那么就需要根据你自己的实际情况做权衡。

对每一种具体的场景（包括输入集合大小、时间空间的要求、数据的大小分布等），总会有一种算法是最适合的。我们需要考虑实际情况，来选择这一最优的算法。

10. 高效的数据结构

和算法的情况类似，不同的数据结构的特性，也是千差万别。

没有一个数据结构是在所有情况下都是最好的，比如你可能经常用到的 Java 里面列表的各种实现，包括各种口味的 List、Vector、LinkedList，它们孰优孰劣，取决于很多个指标：添加元素、删除元素、查询元素、遍历耗时等等。我们同样要权衡取舍，找出实际场合下最适合的高效的数据结构。

总结

各种性能问题的解决，需要采用一些策略；而且不同的人和不同的场景中，会采用有时相同有时迥异的策略，恰如韩愈所说的“草树知春不久归，百般红紫斗芳菲”。但花草树木争奇斗艳，说到底是因为“知春不久归”。

同样的道理，这些性能优化策略，有时候很容易想到，有时候并不是那么直观。所以，我们需要系统地有层次地思考，而这一讲就是帮助你建立这样的思路。

通过总结十大策略，希望你可以多从不同角度，思考同一个问题；有时候一个问题看似无解，但多方位思考，可能会突然发现非常好的解决方案。

陆游曾经说：“山重水复疑无路，柳暗花明又一村”。我们做性能优化的时候，也会经常有这样的感觉的。

思考题

这十大策略也许你已经在工作中使用过，你曾经用过哪些呢？你自己归纳过它们吗？

你现在正在使用的编程语言，有没有对一种数据结构（比如列表，集合）提供了很多种不同的实现方法，它们之间在不同场景下的性能对比如何？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

21 | CPU案例：如何提高LLC（最后一级缓存）的命中率？

你好，我是庄振运。

前面两讲中，我介绍了性能优化的六大原则和十大策略。从今天开始，我们来通过具体案例的解决方案讲解，了解这些原则和策略是如何应用的。

首先，我们要来探讨的是一个 CPU 相关的性能优化案例。

这个性能案例，是关于CPU 的最后一级缓存的。你应该知道，最后一级缓存（一般也就是 L3），如果命中率不高的话，对系统性能会有极坏的影响（相关基础知识建议回顾第 15 讲）。所以对这一问题，我们要及时准确地监测、暴露出来。

至于具体解决方案，我这里建议采取三种性能优化策略，来提高最后一级缓存的命中率。分别是：紧凑化数据结构、软件预取数据和去除伪共享缓存。它们分别适用于不同的情况。

性能问题：最后一级缓存（LLC）不命中率太高

一切问题的解决都要从性能问题开始入手，我们首先来看看最后一级缓存不命中率太高这个性能问题本身。

缓存的命中率，是 CPU 性能的一个关键性能指标。我们知道，CPU 里面有好几级缓存（Cache），每一级缓存都比后面一级缓存访问速度快。最后一级缓存叫 LLC（Last Level Cache）；LLC 的后面就是内存。

当 CPU 需要访问一块数据或者指令时，它会首先查看最靠近的一级缓存（L1）；如果数据存在，那么就是缓存命中（Cache Hit），否则就是不命中（Cache Miss），需要继续查询下一级缓存。

缓存不命中的比例对 CPU 的性能影响很大，尤其是最后一级缓存的不命中时，对性能的损害尤其严重。这个损害主要有两方面的性能影响：

第一个方面的影响很直白，就是 CPU 的速度受影响。我们前面讲过，内存的访问延迟，是 LLC 的延迟的很多倍（比如五倍）；所以 LLC 不命中对计算速度的影响可想而知。

第二个方面的影响就没有那么直白了，这方面是关于内存带宽。我们知道，如果 LLC 没有命中，那么就只能从内存里面去取了。LLC 不命中的计数，其实就是对内存访问的计数，因为 CPU 对内存的访问总是要经过 LLC，不会跳过 LLC 的。所以每一次 LLC 不命中，就会导致一次内存访问；反之也是成立的：每一次内存访问都是因为 LLC 没有命中。

更重要的是，我们知道，一个系统的内存带宽是有限制的，很有可能会成为性能瓶颈。从内存里取数据，就会占用内存带宽。因此，如果 LLC 不命中很高，那么对内存带宽的使用就会很大。内存带宽使用率很高的情况下，内存的存取延迟会急剧上升。更严重的是，最近几年计算机和互联网发展的趋势是，后台系统需要对越来越多的数据进行处理，因此内存带宽越来越成为性能瓶颈。

LLC 不命中率的测量

针对 LLC 不命中率高的问题，我们需要衡量一下问题的严重程度。在 Linux 系统里，可以用 Perf 这个工具来测量 LLC 的不命中率（在第 15 讲中提到过）。

那么 Perf 工具是怎么工作的呢？

它是在内部使用性能监视单元，也就是 PMU（Performance Monitoring Units）硬件，来收集各种相关 CPU 硬件事件的数据（例如缓存访问和缓存未命中），并且不会给系统带来太大开销。 这里需要你注意的是，PMU 硬件是针对每种处理器特别实现的，所以支持的事件集合以及具体事件原理，在处理器之间可能有所不同。

PMU 尤其可以监测 LLC 相关的指标数据，比如 LLC 读写计数、LLC 不命中计数、LLC 预先提取计数等指标。具体用 Perf 来测量 LLC 各种计数的命令格式是：

perf stat -e LLC-loads,LLC-load-misses,LLC-stores,LLC-store-misses

下图显示的是一次 Perf 执行结果。

我们可以看到，在这段取样时间内，有 1951M（19.51 亿）次 LLC 的读取，大约 16% 是不命中。有 313M（3.13 亿）次 LLC 的写入，差不多 24% 是不命中。

如何降低 LLC 的不命中率？

那么如何降低 LLC 的不命中率，也就是提高它的命中率呢？根据具体的问题，至少有三个解决方案。而且，这三个方案也不是互相排斥的，完全可以同时使用。

第一个方案，也是最直白的方案，就是缩小数据结构，让数据变得紧凑。

这样做的道理很简单，对一个系统而言，所有的缓存大小，包括最后一级缓存 LLC，都是固定的。如果每个数据变小，各级缓存自然就可以缓存更多条数据，也就可以提高缓存的命中率。这个方案很容易理解。

举个例子，开源的 C++ Folly 库里面有很多类，比如 F14ValueMap，就比一般的标准库实现小很多，从而占用比较少的内存；采用它的话，自然缓存的命中率就比较高。

第二个方案，是用软件方式来预取数据。

这个方案也就是通过合理预测，把以后可能要读取的数据提前取出，放到缓存里面，这样就可以减少缓存不命中率。“用软件方式来预取数据”理论上也算是一种“用空间来换时间”的策略（参见第 20 讲），因为付出的代价是占用了缓存空间。当然，这个预测的结果可能会不正确。

第三个方案，是具体为了解决一种特殊问题：就是伪共享缓存。伪共享缓存这个问题，我会在后面详细讲到。这个方案也算是一种“空间换时间”的策略，是通过让每个数据结构变大，牺牲一点存储空间，来解决伪共享缓存的问题。

除了最直白的缩小数据结构，另外两个解决方案（用软件方式来预取数据、去除伪共享缓存）都需要着重探讨。

软件提前预取指令

我们先展开讨论一下第二种方案，也就是用软件提前预取指令。

现代 CPU 其实一般都有硬件指令和数据预取功能，也就是根据程序的运行状态进行预测，并提前把指令和数据预取到缓存中。这种硬件预测针对连续性的内存访问特别有效。

但是在相当多的情况下，程序对内存的访问模式是随机、不规则的，也就是不连续的。硬件预取器对于这种随机的访问模式，根本无法做出正确的预测，这就需要使用软件预取。

软件预取就是这样一种预取到缓存中的技术，以便及时提供给 CPU，减少 CPU 停顿，从而降低缓存的不命中率，也就提高了 CPU 的使用效率。

现代 CPU 都提供相应的预取指令，具体来讲，Windows 下可以使用 VC++ 提供的 _mm_prefetch 函数，Linux 下可以使用 GCC 提供的 __builtin_prefetch 函数。GCC 提供了这样的接口，允许开发人员向编译器提供提示，从而帮助 GCC 为底层的编译处理器产生预取指令。这种策略在硬件预取不能正确、及时地预取数据时，极为有用。

但是软件预取也是有代价的。

一是预取的操作本身也是一种 CPU 指令，执行它就会占用 CPU 的周期。更重要的是，预取的内存数据总是会占用缓存空间。因为缓存空间很有限，这样可能会踢出其他的缓存的内容，从而造成被踢出内容的缓存不命中。如果预取的数据没有及时被用到，或者带来的好处不大，甚至小于带来的踢出其他缓存相对应的代价，那么软件预取就不会提升性能。

我自己在这方面的实践经验，有这么几条：

软件预取最好只针对绝对必要的情况，就是对会实际严重导致 CPU 停顿的数据进行预取。

对于很长的循环（就是循环次数比较多），尽量提前预取后面的两到三个循环所需要的数据。

而对于短些的循环（循环次数比较少），可以试试在进入循环之前，就把数据提前预取到。

去除伪共享缓存

好了，我们接着来讨论第三个方案：去除伪共享缓存。

什么是伪共享缓存呢？

我们都知道，内存缓存系统中，一般是以缓存行（Cache Line）为单位存储的。最常见的缓存行大小是 64 个字节。现代 CPU 为了保证缓存相对于内存的一致性，必须实时监测每个核对缓存相对应的内存位置的修改。如果不同核所对应的缓存，其实是对应内存的同一个位置，那么对于这些缓存位置的修改，就必须轮流有序地执行，以保证内存一致性。

但是，这将导致核与核之间产生竞争关系，因为一个核对内存的修改，将导致另外的核在该处内存上的缓存失效。在多线程的场景下就会导致这样的问题。当多线程修改看似互相独立的变量时，如果这些变量共享同一个缓存行，就会在无意中影响彼此的性能，这就是伪共享。

你可以参考下面这张 Intel 公司提供的图，两个线程运行在不同的核上，每个核都有自己单独的缓存，并且两个线程访问同一个缓存行。

如果线程 0 修改了缓存行的一部分，比如一个字节，那么为了保证缓存一致性，这个核上的整个缓存行的 64 字节，都必须写回到内存；这就导致其他核的对应缓存行失效。其他核的缓存就必须从内存读取最新的缓存行数据。这就造成了其他线程（比如线程 1）相对较大的停顿。

这个问题就是伪共享缓存。之所以称为“伪共享”，是因为，单单从程序代码上看，好像线程间没有冲突，可以完美共享内存，所以看不出什么问题。由于这种冲突性共享导致的问题不是程序本意，而是由于底层缓存按块存取和缓存一致性的机制导致的，所以才称为“伪共享”。

我工作中也观察到好多次这样的伪共享缓存问题。经常会有产品组来找我们，说他们的产品吞吐量上不去，后来发现就是这方面的问题。所以，我们开发程序时，不同线程的数据要尽量放到不同的缓存行，避免多线程同时频繁地修改同一个缓存行。

举个具体例子，假如我们要写一个多线程的程序来做分布式的统计工作，为了避免线程对于同一个变量的竞争，我们一般会定义一个数组，让每个线程修改其中一个元素。当需要总体统计信息时，再将所有元素相加得到结果。

但是，如果这个数组的元素是整数，因为一个整数只占用几个字节，那么一个 64 字节的缓存行会包含多个整数，就会导致几个线程共享一个缓存行，产生“伪共享”问题。

这个问题的解决方案，是让每个元素单独占用一个缓存行，比如 64 字节，也就是按缓存行的大小来对齐（Cache Line Alignment）。具体方法怎么实现呢？其实就是插入一些无用的字节（Padding）。这样的好处，是多个线程可以修改各自的元素和对应的缓存行，不会存在缓存行竞争，也就避免了“伪共享”问题。

总结

这一讲，我们介绍了 CPU 方面的优化案例，重点讨论了如何降低 LLC 的缓存不命中率。我们提出了三个方案，分别是紧凑化数据、软件指令预取和去除伪共享缓存。

尤其是第三个方案解决的伪共享缓存问题，对大多数程序员和运维人员而言，不太容易理解。为什么难理解？是因为它牵扯了软件（比如多线程）和硬件（比如缓存一致性和缓存行的大小）的交互。

当多线程共用同一个缓存行，并且各自频繁访问时，会导致严重的称为“伪共享”的性能问题。这种问题，恰如清代词人朱彝尊的两句词，“共眠一舸听秋雨，小簟轻衾各自寒”。所以需要我们狠狠心，把它们强行分开；“棒打鸳鸯”，让它们“大难临头各自飞”，其实呢，是为了它们都好。

思考题

硬件指令预取的基本原理是什么？为什么有时候非常有效，但有时候会有害呢？分别试举出一个具体的模块开发中的例子，来说明为什么有效和有害。

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

22 | 系统案例：如何提高iTLB（指令地址映射）的命中率？

你好，我是庄振运。

我们今天继续探讨性能优化的实践，介绍一个系统方面的优化案例。这个案例涉及好几个方面，包括 CPU 的使用效率、地址映射、运维部署等。

开发项目时，当程序开发完成后，生成的二进制程序需要部署到服务器上并运行。运行这个程序时，我们会不断衡量各种性能指标。而生产实践中，我们经常发现一个问题：是指令地址映射的不命中率太高（High iTLB miss rate），导致程序运行不够快。我们今天就探讨这个问题。

在我过去的生产实践中，针对这一问题，曾经采取的一个行之有效的解决方案，就是同时进行二进制程序的编译优化和采用大页面的部署优化。我下面就详细地分享这两个优化策略，并介绍如何在公司生产环境中，把这两个策略进行无缝整合。

为什么要关注指令地址映射的不命中率？

我们先来看看为什么需要关注 iTLB 的命中率。

在以往从事的性能工作实践中，我观察到 CPU 资源是最常见的性能瓶颈之一，因此提高 CPU 性能，一直是许多性能工作的重点。

导致 CPU 性能不高的原因有很多，其中有一种原因就是较高的 iTLB 不命中率。这里的 iTLB 就是 Instruction Translation Lookaside Buffer，也就是指令转换后备缓冲区。iTLB 命中率不高，就会导致 CPU 无法高效运行。

那么 TLB（转换后备缓冲区）又起到了什么作用呢？

我们知道，在虚拟内存管理中，内核需要维护一个地址映射表，将虚拟内存地址映射到实际的物理地址，对于每个内存里的页面操作，内核都需要加载相关的地址映射。在 x86 计算体系结构中，是用内存的页表（page table），来存储虚拟内存和物理内存之间的内存映射的。但是，内存页表的访问，相对于 CPU 的运算速度，那是远远不够快的。

所以，为了能进行快速的虚拟到物理地址转换，TLB（转换后备缓冲区）这种专门的硬件就被发明出来了，它可以作为内存页表的缓存。TLB 有两种：数据 TLB（Data）和指令 TLB（Instruction），也就是 iTLB 和 dTLB；因为处理器的大小限制，这两者的大小，也就是条目数，都不是很大，只能存储为数不多的地址条目。

为什么说 TLB 的命中率很重要呢？

这是因为，内存页表的访问延迟比 TLB 高得多；因此命中 TLB 的地址转换，比未命中 TLB 也就快得多。因为 CPU 无时无刻不在执行指令，所以 iTLB 的性能尤其关键。iTLB 未命中率，是衡量因 iTLB 未命中而导致的性能损失的度量标准。当 iTLB 未命中率很高时，CPU 将花费大量周期来处理未命中，这就导致指令执行速度变慢。

具体来讲，iTLB 命中和不命中之间的访问延迟，差异可能是 10 到 100 倍。命中的话，仅需要 1 个时钟周期，而不命中，就需要 10-100 个时钟周期，因此 iTLB 不命中的代价是极高的。

我们可以用具体的数据来感受一下。假设这两种情况分别需要 1 和 60 个时钟周期，未命中率为 1％，将导致平均访问延迟为 1.59 个周期，相比全部命中的情况（即 1 个周期）的访问延迟，足足高出 59％。

如何提高指令地址映射的命中率？

对于 iTLB 命中率不高的系统，如果能提高命中率，可以大大提高 CPU 性能并加快服务运行时间。我们在生产过程中实践过两种方案，下面分别介绍。

第一种方案，是优化软件的二进制文件来减少 iTLB 不命中率。

一般而言，根据编译源代码的不同阶段（即编译、链接、链接后等阶段），分别存在三种优化方法。这样的例子包括优化编译器选项，来对函数进行重新排序，以便将经常调用的所谓“热函数”放置在一起，或者使用 FDO（Feedback-Directed Optimization，就是基于反馈的优化）来减少代码区域的大小。

FDO 是什么呢？简单来说，就是把一个程序放在生产环境中运行，剖析真实的生产数据，并且用这些信息来对这个程序进行精准地优化。比如，可以确切地知道在生产环境中，每个函数的调用频率。

那么要如何进行二进制的优化呢？

我们可以通过编译优化来将频繁被访问的指令汇总到一起，放在二进制文件中的同一个地方，以提高空间局部性，这样就可以提高 iTLB 命中。这块放置频繁访问指令的区域，就叫热区域（Hot Text）。

在热区域的指令，它们的提取和预取会更快地完成。还记得我们在第 4 讲学过的帕累托法则吗？根据对许多服务的研究，帕累托法则在这里依然适用。

通常情况下，有超过 80％的代码是“冷的指令”，其余的是“热指令”。通过将热指令与冷指令分开，昂贵的微体系结构资源（比如 iTLB 和缓存）就可以更有效地处理二进制文件的热区域，从而提高系统性能。

具体的二进制优化过程，包括以下三个大体步骤：

首先，是通过分析正在运行的二进制文件来识别热指令。我们可以用 Linux 的 perf 工具来达成此目的。你有两种方法来进行识别：可以使用堆栈跟踪，也可以使用LBR（Last Branch Record，最后分支记录）。LBR 比较适宜，是因为它的好处是能提高数据质量，并减少数据占用量。

其次，根据函数的访问频率，对配置文件函数进行排序。我们可以使用名为HFSort的工具，来为热函数创建优化表单。

最后，链接器脚本将根据访问顺序，优化二进制文件中的函数布局。

这些步骤执行完毕后的结果就是一个优化的二进制文件。我说明一下，如果这里面提到的工具你没有用过，也没有关系。这里知道大体原理就行了，当你真正用到的时候，可以再仔细去研究。

第二种方案就是采用大页面。什么是大页面呢？

现代计算机系统，除了传统的 4KB 页面大小之外，通常还支持更大的页面大小，比如 x86_64 上分别为 2MB 和 1GB。这两种页面都称为大页面。使用较大的页面好处是，减少了覆盖二进制文件的工作集所需的 TLB 条目数，从而用较少的页面表就可以覆盖所有用到的地址，也就相应地降低了采用页面表地址转换的成本。

在 Linux 上，有两种获取大页面的方法：

手工：预先为应用程序预留大页面；

自动：使用透明大页面，也就是THP（Transparent Huge Pages）。

THP，就像名字一样，是由操作系统来自动管理大页面，不需要用户去预留大页面。THP 的显著优点是不需要对应用程序做任何更改；但是也有缺点，就是不能保证大页面的可用性。预留大页面的方式，则需要在启动内核时应用配置。假如我们想保留 64 个大页面，每个 2MB，就用下面的配置。

hugepagesz = 2MB， hugepages = 64

我们在服务器上运行程序时，需要将相应的二进制文件加载到内存中。二进制文件由一组函数指令组成，它们共同位于二进制文件的文本段中，每个页面都尝试占用一个 iTLB 条目来进行虚拟到物理页面的转换。

如果内存页（比如 4KB）很小，那么对于一定大小的程序，需要加载的内存页就会较多，内核会加载更多的映射表条目，而这会降低性能。通常在执行过程中，我们使用 4KB 的普通页面。如果使用“大内存页”，页面变大了（比如 2MB 是 4KB 的 512 倍），自然所需要的页数就变少了，也就大大减少了由内核加载的映射表的数量。这样就提高了内核级别的性能，最终提升了应用程序的性能。这就是大页面为什么会被引入的原因。

由于服务器通常只有数量有限的 iTLB 条目，如果文本段太大，大于 iTLB 条目可以覆盖的范围，则会发生 iTLB 不命中。

例如，Intel HasWell架构中 4KB 页面有 128 个条目，每个条目覆盖 4KB，总共只能覆盖 512KB 大小的文本段。如果应用程序大于 512KB，就会有 iTLB 不命中，从而需要去访问内存的地址映射表，这就比较慢了。iTLB 未命中的处理是计入 CPU 使用时间的，所以等待访问内存地址映射的过程，就实际上浪费了 CPU 时间。

我们提出的第二个方案，就是使用大页面来装载程序的热文本区域。通过在大页面上放置热文本，可以进一步提升 iTLB 命中率。使用大页面 iTLB 条目时，单个 TLB 条目覆盖的代码是标准 4K 页面的 512 倍。

更重要的是，当代的 CPU 体系结构，通常为大页面提供一些单独的 TLB 条目，如果我们不使用大页面，这些条目将处于空闲状态。所以，通过使用大页面，也可以充分利用那些 TLB 条目。

如何获得最佳优化结果？

我们总共提出了两个方案，就是采用热文本和采用大页面放置。这两个其实是互补的优化方案，它们可以独立工作，也可以整合起来一起作用，这样可以获得最佳的优化结果。

采用热文本和大页面放置的传统方法需要多个步骤，比如在链接阶段，将源代码和配置文件数据混合在一起，并进行各种手动配置和刷新，这就导致整个过程非常复杂。这样的整合方案也就很难广泛应用到所有的系统中。

我们在生产中构建了一个流程，来自动化整个过程。这样，该解决方案就成为能被几乎所有服务简单采用的方案，而且几乎是免维护的解决方案。这个解决方案的流程图如下，整个系统包含三大模块：程序剖析（Profiling）、编译链接（Linking）和加载部署（loading）。

程序剖析

剖析模块显示在图的顶部。这个模块定期，比如每周执行一次数据收集作业，以剖析测量正在运行的服务。

Dataswarm 是我们曾经采用的数据收集框架，这是 Facebook 自己开发和使用的数据存储和处理的解决方案。这个作业剖析了服务的运行信息（例如，热函数），并且对配置文件进行控制以使其开销很小。最后，它会把分析好的数据发送到名为Everstore的永久存储，其实这是一个基于磁盘的存储服务。

编译链接

在构建服务包时，链接程序脚本会从 Everstore 检索已配置的热函数，并根据配置文件，对二进制文件中的功能进行重新排序。这个模块的运行结果就是已经优化的二进制程序。

加载部署

加载服务二进制文件时，操作系统会尽最大努力，在大页面上放置热文本。如果没有可用的大页面，则放在常规内存页面上。

生产环境的性能提升

这样的解决方案实际效果如何呢？

我们曾经在 Facebook 的生产环境中广泛地采用这一优化策略。通过几十个互联网服务观察和测量，我们发现，应用程序和服务器系统的性能都得到了不错的提升，应用程序的吞吐量差不多提高了 15％，服务等待时间减少了 20％。

我们也观察了系统级别的指标。系统级别的指标，我们一般考虑主机 cpu 使用情况和 iTLB 不命中率。 iTLB 的不命中率几乎降低了一半，CPU 使用率降低了 5％到 10%。我们还估计，在这里面约有一半的 CPU 使用率降低是来自热文本，另一半来自大页面。

为了帮你更好的认识和体会性能的提升，我下面展示一个具体的互联网服务，在采用这个解决方案后的性能对比。

这张图显示了 iTLB 不命中率的变化。

在应用该解决方案之前，iTLB 不命中率在峰值期间高达每百万条指令 800 个，如蓝色线表示。采用我们的解决方案优化部署后，iTLB 不命中率几乎下降了一半。具体而言，在峰值期间，最高的 iTLB 不命中率降低为每百万条指令 425 次，如黄线表示，相对优化以前下降了 49％，差不多是一半。

应用程序级别指标，显示在下图中。

蓝色曲线为优化前，黄色曲线为优化后。我们可以看到，应用程序请求查询延迟的中位数（P50）下降最多 25％，P90 百分位数下降最多 10％，P99 下降最多 60％。

应用程序吞吐量（QPS）如下图所示，优化后增长了 15％，也就是说，峰值吞吐量从 3.6 万 QPS 增加到了 4.1 万 QPS。你可以看到，应用程序级别的吞吐量和访问延迟都得到了改善。

总结

这一讲我们讨论了如何有效地降低指令地址映射的不命中率太高的问题。

完整的解决方案包括两部分：编译优化和部署优化。对编译的优化，是进行指令级别的划分，把经常访问的指令放在一起，形成 Hot Text 区域。对程序部署的优化，是采用大页面。这两个部分在真正的生产环境中可以一起使用。

唐代的诗人张籍曾经鼓励一个出身寒门的朋友说：“越女新妆出镜心，自知明艳更沉吟。齐纨未足时人贵，一曲菱歌敌万金”。最后两句的意思是，大家追求的畅销东西，比如齐国的珍贵丝绸，恰如社会上横流的物欲，虽然贵重，但是见得多了也就不足为奇了。倒是平时不流行的东西，比如一首好听的采菱歌曲，更值得人称道看重。

操作系统的内存页面管理也有类似的道理，虽然普通的 4KB 页面容易管理，几乎每个程序都在用，大家已经习以为常；但是在某些部署场景下，大页面的使用会让系统性能大增，颇有点惊艳的效果。

思考题

Linux 操作系统中，一个常用的大页面，相当于多少个普通页面的大小？相对于普通页面，大页面有哪些优点，又有哪些缺点呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

23 | 存储案例：如何降低SSD峰值延迟？

你好，我是庄振运。

我们之前讲过，存储系统的性能很关键（参见第 17 讲）。我们这一讲就探讨存储方面的优化案例，是关于 SSD 性能的。

现在很多公司里面的高性能存储系统，一般都是基于 SSD 的，这主要归功于 SSD 价格在近几年的大幅度下降。但是，SSD 也不是包治百病的灵丹妙药，也有自己的特殊性能问题。我们今天就重点讲述两点：SSD 的损耗和 IO 访问延迟偶尔过大的问题。

这里的第二个问题可能听起来很让人吃惊：不是说 SSD 延迟很低吗？

一般情况下，是的。但是特殊情况下就不一定了，这个就说来话长了，它和 SSD 的内部原理有关。我们会一步步地探讨问题形成的原因和解决的策略。

SSD 为什么会损耗？

我们的第 17 讲是关于存储系统的，讲过 SSD 的工作原理和性能。为了防止你忘记，我们就在这里快速地回顾一下其中的一个重要概念：写入放大。

什么是写入放大呢？当写入 SSD 的物理数据量，大于应用程序打算写入 SSD 的逻辑数据量时，就会造成“写入放大”。

如果是传统硬盘 HDD，就不会有写入放大的问题。那么 SSD 为什么会有写入放大呢？这是因为 SSD 内部的工作原理和硬盘很不一样。

我们知道，HDD 是可以直接写入覆盖的。和 HDD 不同，SSD 里面的页面只能写入一次，要重写的话，必须先回收擦除，而且只能在“块”这个级别进行擦除。因此呢，SSD 内部就需要不断地移动所存储的数据，来清空需要回收的块。也就是说，SSD 内部需要进行块级别的“垃圾回收”。垃圾收集器必须有效地在 SSD 内部不断地回收块，回收以前使用的页面空间，然后才能在这个块上写入新数据。

因此，对 SSD 的写入需求，比对 HDD 的写入需求更高。

写入放大的缺点是什么？就是会更快地损耗 SSD 的生命。

每个 SSD 都有固定数量的擦除周期，如果在很短时间内写到 SSD 太多数据，就会导致 SSD 损耗太快，有可能过早烧坏 SSD。换句话说，很高的写入速率，可能会导致 SSD 在到达其预期使用寿命之前就发生故障。

所以，我们要注意一个常用的指标叫：年损耗率（Burn Rate）。这个指标是怎么定义的呢？是用 SSD 的预期寿命推导出来的。比如一个 SSD 预期寿命是 4 年。那么每年可以损耗 25%，这就是年损耗率。

如何减少 SSD 损耗？

前面讲的“写入放大”，其实也可以用一个相应的具体指标来衡量，就是“写入放大系数”；它代表物理写入 SSD 的数据与应用程序写入的逻辑数据之比。比如，如果写入放大系数是 2，就表示写入每 10KB 的逻辑数据，SSD 实际上写了 20KB。为了控制 SSD 的年损耗率，我们需要尽量降低写入放大系数。

那么如何减少写入放大系数呢？常见的方法有两种：

是保留一定的空闲存储空间，这是因为写入放大系数是和 SSD 存储空闲率相关的。

是使用 Trim。

这两种方法可以同时使用，我们下面分别介绍。

我们先简单说一下第一种方法。每个 SSD 都有一定数量的预留空间，这个空间不是 SSD 可用容量的一部分。这样做是有原因的。尽管我们可以使用工具来调整 SSD 卡上的可用容量，但是我不建议你减少预配置的可用空间，因为这将降低写入性能，并可能大大缩减 SSD 的使用寿命。

我们在存储数据到 SSD 时候，也不要存得太满，也就是不要追求太高的空间使用率。那么我们将 SSD 可用存储容量的使用率目标定为多少比较合适呢？一般来说，我们可以定为 80％至 85％，以保持较低的写入放大率。

SSD 的空闲可用空间越多，内部垃圾收集的开销就越低，就越有可能降低写入放大系数。但是这种关系不是线性的，所以存在着收益递减的问题。

第二种方法是用 Trim。我首先为你讲解一下什么是 Trim。

Trim 是个命令，是操作系统发给 SSD 控制器的特殊命令。使用 Trim 命令，操作系统可以通知 SSD 某些页面存储的数据不再有效了。比如，对于文件删除操作，操作系统会将文件的扇区标记为空闲，以容纳新数据，然后就可以将 Trim 命令发送到 SSD。

Trim 命令有什么好处呢？

SSD 收到 Trim 命令后，SSD 内部的控制器会更新其内部数据页面地图，以便在写入新数据时不去保留无效页面。并且，在垃圾回收期间不会复制无效页面，这样就实现了更有效的垃圾收集，也就减少了写操作和写入放大系数，同时获得了更高的写吞吐量，延长了驱动器的使用寿命。

Trim 命令和机制虽然看起来很美好，但是实际中会产生一些问题。原因在于，不同的 SSD 厂商对 Trim 命令的处理方式，以及具体的垃圾回收机制很不一样；有的实现还不错，有的就差强人意了，因此 Trim 的性能在每种 SSD 那里会有所不同。我们后面会提到，有些 SSD 的厂商的某些 SSD，因为对 Trim 的支持不太好，会造成某些情况下性能非常差。

还要注意的是，默认情况下，操作系统一般不启用 Trim。因此，当文件系统删除文件时，它只是将数据块标记为“未使用”。但是 SSD 控制器并不知道设备上的哪些页面可用，因此无法真正释放设备上的无效空间。所以，在没有启动 Trim 的情况下，一旦 SSD 设备的可用容量填满，即使文件系统知道设备上有可用容量，SSD 也会认为它自己已经存满。

那么怎么启动 Trim 呢？要在 SSD 上启用连续 Trim，必须在 mount 这块 SSD 的时候使用“Discard”安装选项。如果一块 SSD 已经安装了，想启动 Trim，那就需要卸载后重新安装，“Discard”选项才能生效。也就是说，使用 remount 命令是不起作用的。

所以，对于单个系统而言，最好在 grub 中启用 mount 选项，并重新启动。

想减少 SSD 损耗，却导致访问延迟过大？

Trim 的使用，虽然带来了降低 SSD 损耗的好处，但也带来了一些坏处，特别是 IO 访问可能延迟加大的问题。

为什么 Trim 会影响应用程序性能呢？

原因和 SSD 内部的实际机制有关。每个 SSD 内部都有一个 FTL（Flash Translation Layer）映射表，该表将操作系统的逻辑块地址（LBA，Logical Block Address）映射到 SSD 上的物理页面地址（PPA，Physical Page Address）。映射表在驱动器被写入时不断更新，以后每个读取和写入 IO 都要引用。

一般来说，映射表是存储在 SSD 驱动器的 RAM 中，以便快速访问；但是它的副本也存储在 SSD 中，目的是在电源故障时能够保留 LBA 到 PPA 的映射。随着 SSD 上面内容和数据的不断变化，这些变化包括新写入 IO 或垃圾回收，RAM 中的映射表也不断更新，并且持续写入 SSD 中。

如果在文件系统上启用了 Discard 选项，那么每次删除文件时，都会生成 Trim 命令。因为每次 Trim 都会更改映射表，所以对映射表的更改也就实际地记录到 SSD 中。这项操作可能需要花费比较长的时间，比如几毫秒的时间才能完成，在这个更改过程中，普通的数据读取和写入 I /O 会阻塞，并且阻塞到所有的映射表调整都被完全处理为止。当今业界的大多数 SSD 都是这样工作的。

上面我们看到，由于 Trim 的处理会阻塞普通的数据读取和写入 I /O，直到 Trim 完成映射表记录才返回，所以 Trim 的延迟对普通读写 I O 的延迟具有重大影响，尤其对高分位数（比如 P99、Ｐ99.9) 的读写 IO 延迟影响更大。减少 Trim 延迟就是减少 IO 延迟。所以，我们需要尽量减少 Trim 的等待处理时间。

另外值得你注意的是，每个 SSD 厂商和每款 SSD，对 Trim 的具体处理方式都可能不同，颇有些厂商的某些 SSD 具有严重的问题。我们生产实践中碰到过好几种这样的 SSD，比如有厂商的一种 SSD 在大量删除数据时有很大的延迟。这就要求我们在选购 SSD 时候，要特别小心，尤其是要做彻底的性能测试。

如何避免 Trim 带来的延迟？

我们刚才讲了用 Trim 的好处和坏处。好处是可以减少 SSD 的损耗，延长 SSD 的寿命；坏处是会造成应用程序的 IO 读写延迟变大。

那么怎么才能尽量避免 Trim 带来的坏处呢？我们这里谈两种方式：一是对 Discard 选项本身的调优，二是使用fstrim命令。这两种方式分别对应使用 Discard 被启用和不被启用的两种情况。

第一种方式是在启用了 Discard 后，对 Discard 的调优。对于已经启用 Discard 的场景下，Trim 命令默认是没有大小限制，也就是说，一次发送会尽可能多的删除命令。但是如果一次删除的数据太多，SSD 可能需要很长的时间才能返回，其他读写 IO 就会感受到很大的延迟。

那么我们就可以微调了，这里我们就可以借助另外一个参数，discard_max_bytes 对 Discard 进行调优。这个参数是一个操作系统内核参数，从名字也听得出，它可以指定一次 Trim 的最大数据量。

调整这个参数的优点，是可以根据实际可接受 IO 延迟的需要，来随意微调。举个例子，假如可接受 IO 延迟比较大，那就可以设置一个较大的 discard_max_byes 数值，比如 2GB。使用这个参数的坏处是，当有大文件删除时，如果没有相应的重新调整参数，Trim 的吞吐量会受影响。

第二种方式，是在没有启用 Discard 的场景下，采用 fstrim 来调优。fstrim 也是一个命令，它可以控制 Trim，来删除掉 SSD 上的文件系统不再使用的数据。默认情况下，fstrim 将删除文件系统中所有未使用的块，但是这个命令有其他的选项，根据删除范围或大小来进行微调。

这个命令一般用于 Discard 没有被启用的场景下。为了达到最好的效果，都是周期性的，或者采用外部事件触发来运行这个命令，比如用 Cron 来每天固定时间运行；或者每当 SSD 存储使用率到了某个大小就运行。

下图展示了一个实际生产环境中的性能数据。

这是一个采用 fstrim 而降低 IO 延迟的例子。横轴是时间，纵轴是对 SSD 进行读操作的 IO 延迟。红色箭头是运行 fstrim 的时间。我们可以看到，在 fstrim 后，IO 延迟大幅度地降低了。

采用 fstrim 这个方式的优点是，可以根据实际需要来决定何时运行，并且更好地微调和控制 Trim 的工作。

这个方式也有缺点，就是如果不够小心，运行这个命令时可能导致很长时间的 SSD 读写挂起阻塞，在这个阻塞的过程中，SSD 完全没有响应，不能读写。我见过几次这样的生产例子，阻塞了好几分钟甚至几个小时的时间，整个 SSD 完全不能写入和读取数据。

总结

SSD 不断地重写会损坏其存储能力，就如同一口宝刀，不断地征战砍伐后，也会有缺口。这让我想起了唐代诗人马戴写的一首气势磅礴的《出塞词》：“金带连环束战袍，马头冲雪度临洮。卷旗夜劫单于帐，乱斫胡兵缺宝刀。”

为了延长 SSD 的寿命，我们可以采用 Trim 方式，以去除不必要的内部重写。

但是这种方式在某些特殊情况下，会增大外部 IO 的访问延迟。解决这一问题的方法是对 Trim 进行调优。我们这一讲就集中探讨了几种调优解决方案，来解决这一特殊情况下的问题。

思考题

你们公司部署 SSD 了吗？有没有遇到关于损耗过大的问题，以及 Trim 的问题和相关讨论？最后采取的解决方案是什么呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

24 | 跨层案例：如何优化程序、OS和存储系统的交互？

你好，我是庄振运。

我们前面几讲介绍了性能优化的原则和策略，并且集中探讨了 CPU、内存和存储三个最关键的领域。

今天我们来讲一个比较复杂的 JVM 场景和超大延迟的性能问题；这是本模块，也就是性能优化模块的最后一讲。

我们会一步步地探讨这个性能问题的表象、问题的重现、性能分析的过程和解决方案。这个性能问题的复杂性，表现在它牵扯了计算机技术的很多层次——从最上层的应用程序，到中间层 JVM 的机制，再到操作系统和文件系统的特性，最后还涉及到硬件存储的特点。

更重要的是，这几个层次互相影响，最后导致了平时我们不容易看到的严重性能问题——非常大的 JVM 卡顿。

今天我会把问题的核心和分析过程阐述清楚，而对于其他的一些背景和更多的性能数据，你可以参考我发表在IEEE Cloud上的论文。

生产环境下偶尔很大的响应延迟是怎么回事？

我们先来看看这个性能问题的表象：就是在生产环境中，偶尔会出现非常大的响应延迟。

由于大多数互联网业务都是面向在线客户的（例如在线游戏和在线聊天），所以，确保客户相应的低延迟非常重要。各种研究也都表明，200 毫秒延迟，是多数在线用户可以忍受的最大延迟。因此，确保低于 200 毫秒（甚至更短）的延迟，已经成为定义的 SLA（服务水平协议）的一部分。

鉴于 Java 的普及和强大功能，当今的互联网服务中有很大一部分都在运行 Java。Java 程序的一个问题是 JVM 卡顿，也就是大家常说的 STW（Stop The World）、JVM（Java 虚拟机）暂停。根据我的经验，尽管我们或许已经仔细考虑了很多方面来优化，但 Java 应用程序有时仍会遇到很大的响应延迟。

这个 STW 的产生和 JVM 的运行机制是直接相关的。

Java 应用程序在 JVM 中运行，使用的内存空间叫堆。JVM 负责管理应用程序在内存里面的对象。堆空间经常被 GC 回收（垃圾收集），这个过程是 JVM 操作的。Java 应用程序可能在 GC 和 JVM 活动期间停止，这就会给应用程序带来 STW 暂停。

这些 GC 和 JVM 活动信息很重要，根据启动 JVM 时提供的 JVM 选项，各种类型的相关信息，都将记录到 GC 的日志文件中。

尽管某些 GC 引起的 STW 暂停众所周知（比如 JVM 导致的 Full GC），但是我们在生产中发现，其他因素，比如操作系统本身，也会导致一些相当大的 STW 暂停。

比如我举个例子。文中的图片就显示了一个 STW 暂停和 GC 日志，这个暂停时间超过了 11 秒；我就经常在生产环境中看到这样的 STW 暂停。

你注意一下图片中有较大的字体的那行，里面显示了 User、Sys 和 Real 的计时，分别对应着用户、系统和实际的计时。比如“Real=11.45”就表示实际的 STW 暂停是 11.45 秒钟。

在这个 GC 日志中，这种暂停 11 秒钟的 STW 非常讨厌，因为这样大的暂停是不能容忍的。而且这个问题很难理解，完全不能用 GC 期间的应用程序活动和垃圾回收活动来解释。

从日志中我们也看到，这个 JVM 的堆并不大，只有 4GB，垃圾收集基本不会超过 1 秒钟。正如图中显示的那样，用户和系统时间都可以忽略不计，User 和 Sys 的暂停时间分别是 0.18 秒钟和 0.01 秒钟，但是实际上 JVM 暂停了 11.45 秒钟！

因此，GC 所做的工作量，根本无法解释如此之大的暂停值。

搭建测试环境重现问题

为了搞清这个问题，我们做了彻底的性能分析和各种测试。为了去掉很多其他的干扰因素，以便方便根因分析，我们首先希望实验室环境中重现该问题，这样就比较方便从根本上解释原因。

出于可控制性和可重复性的考虑，我们使用了自己设计的一个简单 Java 程序。为了方便对比，我们根据有没有后台背景 IO 活动，而测试了两种场景（这里的后台背景 IO 就是各种磁盘 IO）。不存在后台 IO 的场景是基准场景，而引入后台 IO 的另一场景，将重现我们观测到的性能问题。

我们使用的 Java 程序的逻辑也很简单直白，就是不断地分配和删除特定大小的对象。程序一直在不断分配对象，当对象数目达到某阈值时，就会删除堆中的对象。堆的大小约为 1GB。每次运行固定时间，是 5 分钟。

为了真实地模拟生产环境，我们在第二种场景中注入后台 IO。 这些 IO 由 bash 脚本生成，该脚本就是不断复制很大的文件。在我们的实验室环境中，后台工作模块能够产生每秒 150MB 的磁盘写入负载，差不多可以使服务器配备的镜像硬盘驱动器饱和。这个应用程序和后台 IO 脚本的源代码在GitHub上开源。

我们考虑的主要性能指标，是关于应用程序的 STW 暂停，具体考虑了两个指标：

总暂停时间，即所有 STW 暂停的总暂停时间；

较大的 STW 暂停计数和。

下面让我们一起来看看结果。

场景 A 是基准场景，Java 程序在没有后台 IO 负载的情况下运行。我们在实验室环境中执行了许多次运行，得到的结果基本是一致的。

图片中显示的是一个持续 5 分钟的运行，就是沿着 5 分钟的时间线，显示了所有 JVM STW 暂停的时间序列数据。我们观察到，所有暂停都非常小，并且 STW 暂停都不会超过 0.25 秒。 STW 的总暂停时间约为 32.8 秒。

场景 B 是有后台 IO 负载情况下运行相同的 Java 程序。在实际的生产过程中，IO 负载可能来自很多地方，比如操作系统、同一机器上的其他应用程序，或者来自同一 Java 应用程序的各种 IO 活动。

在图片中，我们同样沿着 5 分钟的时间线，显示了所有 JVM STW 暂停的时间序列数据。我们发现，当后台 IO 运行时，相同的 Java 程序，在短短 5 分钟的运行中，看到 1 个 STW 暂停超过 3.6 秒，3 个暂停超过 0.5 秒！结果是，STW 的总暂停时间为 36.8 秒，比基准场景多了 12％。

而 STW 总暂停时间多，也就意味着应用程序的实际工作吞吐量比较低，因为 JVM 多花了时间在 STW 暂停上面。

响应延迟的根本原因在哪里？

为了弄清楚 STW 暂停的原因，我们接着进行了深入的分析。

我们发现 STW 大的暂停是由 GC 日志记录，write() 调用被阻塞导致的。这些 write() 调用，虽然以缓冲写入模式（即非阻塞 IO）发出，但由于操作系统有“回写”IO 的机制，所以仍然可能被操作系统的“回写”IO 阻塞。

操作系统的“回写”机制是什么呢？就是文件系统定期地把一些被改变了的磁盘文件，从内存页面写回存储系统。

具体来说，当缓冲的 write() 需要写入文件时，它首先需要写入 OS 缓存中的内存页面。这些内存页是有可能被“回写”的 OS 缓存机制锁定的；而且当后台 IO 流量很重时，该机制可能导致这些内存页面被锁定相当长的时间。

为了彻底查清原因，我们使用 Linux 下的 Strace 工具，来剖析函数调用和 STW 暂停的时间相关性。

这个图表示的是 JVM STW 暂停，和 strace 工具报告的 JVM 进行 write() 系统调用的延迟。图片集中显示了一个 1.59 秒的 JVM STW 暂停的快照。

我们仔细检查了两个时间序列数据，发现尽管 JVM 的 GC 日志记录使用缓冲写入，但是 GC 暂停和 write() 延迟之间有极大的相关性。

这些时间序列的相关性表明，由于某些原因，GC 日志记录的缓冲写入仍然被阻塞了。

那这个原因是什么呢？我们就要通过仔细阅读分析 JVM GC 的日志和 Strace 的输出日志（如下图所示）来寻找。

让我沿着时间轴线来具体解释一下图片中的数据。

在时间 35.04 秒时，也就是第 1 行日志，一个新的 JVM 新生代 GC 启动，并用了 0.12 秒才完成。新生代 GC 在 35.17 秒时结束，并且 JVM 尝试发出 write() 系统调用（第 4 行），将新生代 GC 统计的信息输出到 GC 日志文件。write() 调用被阻塞 1.47 秒，所以最后在时间 36.64（第 5 行）结束，总共耗时 1.47 秒。当 write() 调用在 36.64 返回给 JVM 时，JVM 记录此 STW 暂停为 1.59 秒（即 0.12 + 1.47）（第 3 行）。

这些数据表明，GC 日志记录过程，恰好位于 JVM 的 STW 暂停路径上，而日志记录所花费的时间也是 STW 暂停的一部分。如果日志记录，也就是 write() 调用被阻塞，那么就会导致 STW 暂停。换句话说，实际的 STW 暂停时间由两部分组成：

实际的 GC 时间，例如，新生代 GC 时间。

GC 日志记录时间，例如，write() 执行时被 OS 阻塞的时间。

我用下图来清楚地表示它们之间的关系。

左边是 JVM 的活动，右边是操作系统的活动。时间 T1 时垃圾回收开始，T2 时 GC 结束，并开始调用 write() 写日志。这之间的延迟，就是 GC 的延迟。T3 时 write() 调用开始，T4 时 write() 调用返回。这之间的延迟，就是 OS 导致的阻塞延迟。所以，总的 STW 暂停就是这两部分延迟的和，也就是 T4-T1。

那么接下来的一个很难理解的问题是：为什么非阻塞 IO，还会被阻塞？！

在深入研究各种资源（包括操作系统内核源代码）后，我们意识到，非阻塞 IO 写入还是可能会停留，并被阻塞在内核代码执行过程中。具体原因有好几个，其中包括：页面写入稳定（Stable Page Writing）和文件系统日志提交（Journal Logging）。下面分别说明。

JVM 写入 GC 日志文件时，首先会改变（也就是“弄脏”）相应的文件缓存页面。即使以后通过操作系统的写回机制，将缓存页面写到磁盘文件中，被改变内存中的缓存页面，仍然会由于稳定的页面写入而导致页面竞争。

根据页面写入稳定的机制，如果页面处于 OS 回写状态，则对该页面的 write() 必须等待回写完成。这是为了避免将部分全新的页面保留到磁盘（会导致数据不一致），来确保磁盘数据的一致性。

对于日志文件系统，在文件写入过程中会生成适当的日志。当附加到 GC 日志文件，而需要分配新的文件块时，文件系统需要首先将日志数据保存到磁盘。在日志保存期间，如果操作系统具有其他 IO 活动，则可能需要等待。如果后台 IO 活动繁重，则等待时间可能会很长。

解决方案如何落地？

我们已经看到，由于操作系统的各种机制的原因，包括页面缓存写回、日志文件系统等，JVM 可能在 GC 日志期间，被长时间阻塞。

那么怎么解决这个问题呢？

我们思考了三种方案可以缓解这个问题，分别是：

修改 JVM；

减少后台 IO；

将 GC 日志记录与其他 IO 分开。

修改 JVM 是将 GC 日志记录活动，与导致 STW 暂停的关键 JVM GC 进程分开。这样一来由 GC 日志阻塞引起的问题就将消失。

比如 JVM 可以将 GC 日志放到另一个线程中，该线程可以独立处理日志文件的写入，也就不会造成另外一个应用程序线程的 STW 暂停。这个方案的缺点是，采用分线程方法，可能会在 JVM 崩溃期间丢失最后的 GC 日志信息。

第二种方案是减少后台 IO。

后台 IO 引起的 STW 暂停的程度，取决于后台 IO 的强度。因此，可以采用各种方法来降低这些 IO 的强度。比如在 JVM 应用程序运行的服务器上，不要再部署其他 IO 密集型应用程序。

第三种方案是将 GC 日志与其他 IO 分开。

对延迟敏感的应用程序，例如为交互式用户提供服务的在线应用程序，通常无法忍受较大的 STW 暂停。这时可以考虑将 GC 日志记录到其他地方，比如另外一个文件系统或者磁盘上。

比如这个文件系统可以是临时文件系统（tmpfs，一种基于内存的文件系统）。它具有非常低的写入延迟的优势，因为它不会引起实际的磁盘写入。但是，基于 tmpfs 的方法存在持久性问题。由于 tmpfs 没有备份磁盘，因此在系统崩溃期间，GC 日志文件将丢失。

而另一种方法是将 GC 日志文件放在更快的磁盘上，例如 SSD。我们知道，就写入延迟和 IOPS 而言，SSD 具有更好的 IO 性能。

如何证明解决方案是否有效？

得出方案后，我们就需要对它们进行验证了。

在我们提出的三种方案中，第一种方案，也就是改进 JVM，是暂时难以实现的，因为它需要修改 JVM 的实现机制。第二种方案呢，是不言自明。如果没有背景 IO 或者有较少背景 IO，那么自然背景 IO 的影响就变小了；或者减少 GC 日志输出的频率，就不会有那么多次 STW 暂停了。

因此，我们这里直接来验证第三种方案：将 GC 日志记录与其他 IO 分开。

我们的方法，是将 GC 日志文件放在 SSD 文件系统上来验证这种方案。我们运行与前面实验场景中相同的 Java 应用程序和后台 IO 负载。下图中显示了一个 5 分钟运行时段的所有 STW 暂停和相应的时间戳。

对于 STW 暂停的信息，我们注意到所有的 JVM 的暂停都非常小，所有暂停都在 0.25 秒以下。可以说，延迟暂停方面的性能，得到了很大的提高，这表明，如果这样分开 GC 日志文件，即使有很大的后台 IO 负载，也不会导致 JVM 程序发生较大的 STW 暂停；这样的结果也就验证了这一方案的有效性。

总结

今天我们探讨了一个跨层的性能分析和优化案例。由于计算机几个层面的技术互相影响，实际生产环境中，会出现非常大的响应时间延时，严重影响公司业务。这些层面包括应用程序、JVM 机制、操作系统和存储系统。

我们通过合理的性能测试和性能分析，包括搭建合适的测试环境进行问题重现、详细的根因分析，最终提出了几种解决方案。简单来说，就是 JVM 在 GC 时会输出日志文件，写入磁盘时会因为背景 IO 而被阻塞。把日志文件和背景 IO 分开放在不同磁盘，从而让它们互相不影响的话，就能大幅度降低 STW 延迟。

从这个案例我们也可以再次了解，从事性能优化工作需要通晓几乎所有层面的知识。而且不光要知识面广，还要能深入下去，才能进行彻底的根因分析。

唐代诗人刘禹锡有一首诗说：“莫道谗言如浪深，莫言迁客似沙沉。千淘万漉虽辛苦，吹尽狂沙始到金。”对待这样复杂的性能问题，我们也需要不怕困难，不惧浪深沙沉。因为只有经过千淘万漉的辛苦，才能淘到真金，发现问题的本质并彻底解决它。

思考题

既然这个问题是 Java 应用程序偶尔发生大延迟，原因是 JVM 垃圾回收 GC 的 Log 日志，写到硬盘的文件系统引起的，我们如何可以让 JVM 把 GC 日志写到内存去吗？

Tips：考虑一下 JVM 参数。

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

25 | 如何在生产环境中进行真实的容量测试？

你好，我是庄振运。

从今天开始，我们进入新的模块：性能工程实践。在这一模块中，我会讲述在实际生产环境中应用性能工程的场景、案例。这些场景和案例都是针对大规模互联网服务，是在解决实际性能问题后总结的经验。

今天我要讲的主题是“在生产环境中进行真实场景的压力测试”。这来源于我对 LinkedIn 公司生产实践的总结。

LinkedIn 为超过 5.9 亿用户提供服务，在性能优化的过程中，经常会遇到这类问题：一个服务可以承受的最大 QPS 是多少？要满足 100K QPS 的服务需求，我需要多少服务器？……

怎么解决这些问题呢，有一个大招就是在生产环境中进行真实的容量测试。

关于这个实践的详细方案和技术细节，我们曾经发表过一篇研究论文（IEEE ICWS），并且很荣幸地获得了 IEEE 最佳论文奖，推荐你去读一读。

今天我就带你从这个场景的源头出发，一步步探索到最后的具体落地方案。

为什么需要在生产环境中进行容量测试？

既然我们要解决一个具体的问题，那我们一定要先问个“为什么”。

为什么要在生产环境中进行容量测试呢？要回答这个问题，我们还得再往前追问，为什么说真实的容量测试很重要呢？

我想这个问题不难回答，你应该已经有自己的答案了。

我们都知道，一个在线互联网公司的存活和发展，靠的是它提供的互联网在线服务，自然也就依赖于这些服务的性能和稳定性。要保证每个服务都能够稳定地运行，我们必须为之提供足够的服务容量，比如适当数量的服务器。

那么要如何保证服务容量足够呢？首先就必须做好服务容量的预测。

要预测服务容量，我们就需要做容量的性能测试。一般是先确定每台单独的服务器可以支撑多少服务流量；然后用这个单台服务器的数据，来决定这个服务整体需要多少台服务器。这种测试其实就是我们前面讲过的容量测试。

举个具体例子，如果测试结果表明，一台服务器最多可以支撑 100 个 QPS，那么要满足 100K QPS 的服务需求，总共就需要部署一千台服务器。

讲到这里，我想再补充几句。我们讲了这么多服务容量的事儿，那“服务”到底都指的是什么呢？

一般来说，公司提供的服务大致上分为两种：前端服务和后端服务。前端服务是什么样呢？包括各种服务的登陆页面和移动 App，这些服务会直接影响用户的体验。后端服务呢，一般是为前端服务和其他后端服务提供数据和结果。后端服务可以有多种，比如键值数据存储服务（例如，Apache Cassandra），和公司内部的各种微服务。

服务容量预测的重要性我们了解了，那测试为什么需要在生产环境中进行呢？

你是不是想到了，在非生产环境中的容量测试，执行起来肯定更简单啊！没错，的确会更简单，但是，在实验环境或者其他非生产环境中做这样的测试，比如采用人工合成的流量负载，会非常不准确。

这是因为实际的生产环境里面，有多个特殊因素会导致和非生产环境中不同的结果。比如：

客户需求会随着时间而变化，例如高峰时段与非高峰时段的流量就很不一样；

用户请求的多样性，例如不同国家的查询类型不同；

负载流量的规模，基础架构设施的变化，例如服务的软件版本更新，微服务互相调用的变化等等。

所以，对一个重要而复杂的互联网在线服务，由于难以在非生产环境中进行准确的容量测试，我们经常需要转向真正的生产环境，使用实时而真实的客户流量负载来测试。

所以，想要在非生产环境中进行准确的容量测试基本上是做不到的。而对一个重要而复杂的互联网在线服务，能够做到准确的容量测试又太重要了。因为准确的容量数据，是保证线上服务的可靠运行和控制公司成本的基础。

那怎么办呢？这时候我们就需要转向真正的生产环境，使用实时而真实的客户流量负载来测试。

如何在真实生产环境中进行容量测试？

那在真实生产环境中进行容量测试，要如何做呢？

一般来说，我们需要把生产环境的流量进行重定向，让这些重定向的流量，实时地驱动运行 SUT 的单个或者几个服务器。根据重定向的流量大小，会产生不同级别的流量负载。通过仔细地操作重定向的多少，并且把握测试的时间，我们可以获得非常准确的运行 SUT 服务器的容量值。

但是，生产环境中的容量测量也有诸多挑战，你需要特别小心。

第一个挑战是需要设计一个控制重定向流量的机制。这个机制要能够根据其他一些参数，来调整重定向的流量多少。

第二个挑战（也是最大的挑战）是这种重定向生产流量可能会影响真正的客户。因为我们会把客户请求重定向到某个服务器，并且会不断给服务器加压，直到这个服务器接近超载，那么这个服务器上所有的客户请求的延迟都会受影响，也就是可能会变大，用户性能也就可能会受到损害。

为了尽量减少对客户的影响，我们的容量测试需要设计合适的机制，来将这种可能的损害降到最低点。系统里面必须有一个模块，来不断地监测客户的性能；一旦到达临界点，就停止继续加压的操作，甚至适当减压。这就是所谓的“非侵入性”（Non-Intrusive）。

还有一个挑战是对于测试时间的控制。既然重定向生产流量可能影响客户性能，当然是测试的时间越短越好。可是测试时间太短的话，又可能会影响数据的稳定性。

为了应对这些挑战，准确地确定服务容量的极限，并精确定位容量瓶颈，LinkedIn 采用了一个解决方案，我们将它命名为 RedLiner。

宏观来讲，Redliner 是固定一个生产环境中的 SUT，这个 SUT 包括服务器和上面运行的被测服务，然后不断地把其他服务器上的流量，重定向到这个 SUT 服务器上面。随着流量的不断增大，这个 SUT 服务器的资源使用也就越来越多，所服务的客户请求的性能，比如端到端延迟，就会越来越差。

直到客户请求的性能差到一个定好的阈值，比如端到端延迟是 200 毫秒，流量重定向才会停止。这个时候，基本就可以确定 SUT 服务器不能再处理任何额外的负载。此时获得的容量结果就是 SUT 服务器的最大容量。

想要实现上述的测试，需要好几个模块一起合作。不过在讲 RedLiner 具体的各个模块之前，我们要先梳理一下合理方案的设计原则，一共是五条：

要使用实时流量以确保准确性。

尽量不影响生产流量，这就需要实时的监测和反馈模块。

可以定制重定向等行为规则；对不同的服务和不同的场景的测试，各种性能指标和阈值都会不同。

能自动终止测试，并把测试环境复位，尽量减少人工干预。

支持基于日历和事件的自动触发和调度。

那这个方案具体是怎么实现的呢？

现在我们来看看解决方案的高层架构，如下图所示，这个方案主要包括四个组件：

核心控制器 LiveRedliner

重定向流量的 TrafficRedirector

收集性能数据的 PerfCollector

分析性能数据的 PerfAnalyzer

我先来一一给你介绍下这四个关键组件。

1.核心控制组件 LiveRedliner

核心控制组件是整个系统的神经中枢，负责总体调度容量测试的过程。比如何时发起测试，何时终止测试，何时需要增加更大的流量等等。

用户可以自己定义一些特殊的规则，来更好地控制整个测试。用户可以自定义性能指标的阈值。这些性能指标，可以是用户的端到端延迟，包括各种统计指标，比如 P99。也可以把几个不同的性能指标组合起来，实现复杂的逻辑，比如“端到端延迟不超过 200 毫秒，并且错误率不超过 0.1% 等”。

2.重定向流量组件 TrafficRedirector

重定向流量组件负责对生产流量进行重定向。具体的实现机制，根据被测试的服务类型分为两种：前端服务和后端服务。

用于前端服务时，Redliner 是通过客户请求的属性（例如用户 ID、语言或帐户创建日期），来决定是否对一个客户请求来进行重定向的。这个转换也很简单，可以是取模机制。

举个例子来说，如果 Redliner 需要重定向 1％的流量，它可以把用户的一个属性比如 userid 转换成整数，然后执行用 100 来取模的操作，并且和一个固定整数值作比较。

用于后端服务时，重定向流量可以通过另外一个叫做“资源动态发现和负载均衡”的模块来实现。在 LinkedIn，我们很多的服务负载均衡机制，一般会采用一个服务器列表（URI 集群），以相对应的权重值来决定一个请求发送到哪个服务器。

假设这样一个机制有 10 个可用的 URI 集群，并且最初所有这些集群都接收等量的流量（即每个 URI 的权重为 10％）。如果 Redliner 决定将 20％的流量重定向到特定的 URI（即 SUT），那么它可以为 SUT 的 URI 分配 20％的权重。

3.性能数据收集组件 PerfCollector

容量测试必须采集各种类型的性能指标，例如 CPU，内存和 QPS 等。这些性能指标的作用，就是确定 SUT 何时达到其容量最大值，以及容量值是多少。

PerfCollector 组件负责收集各种性能指标，包括系统级和服务级的指标。这个组件运行在所有受监视的节点。组件传递的数据量通常很大，因为一般要监测较多的性能指标。所以最好采用扩展性好的实时消息传递系统，来把这些性能数据及时传到其他组件，尤其是下面要介绍的性能分析组件 PerfAnalyer。

我们采用的消息传递系统是Kafka。Kafka 也是由 LinkedIn 设计并开源的，扩展性和性能都很好，建议你也尝试采用。

4.性能分析组件 PerfAnalyzer

收集性能指标是第一步，下一步就是分析性能，并采取相应的措施。具体来说，可以根据性能指标的值来确定 SUT 是否饱和。

根据性能数据和用户定义的规则，如果发现当前的 SUT，仍有空间来承担更多的负载，我们可以将更大比例的实时流量重定向到该 SUT。否则，如果 SUT 显示饱和迹象，那么重定向的流量百分比就应该降低。PerfAnalyzer 组件能分析收集的数据，并确定是否有特定指标是否违反用户定义的规则。

Redliner 作为一个完整的解决方案，通过几个模块互相配合来实现真实的线上容量测试。通过动态地调整线上的流量，直到让被测系统临近超载状态，从而获得准确的单位服务容量。

现在我们再来换个角度，看一下 RedLiner 的具体操作流程。流程图如下所示。

首先是跟据以前的测试历史数据，决定一个初始重定向数量，也就决定了 SUT 的初始负载。理论上讲，每个测试都可以从０开始；但是如果起点流量太低，整个测试需要花很长时间，所以最好能利用历史数据，从一个比较高的起点开始。

在使用历史数据的基础上，我们还需要决定总体测试时间。决定了时间后，每次调整重定向流量的百分比也就确定了。这个调整的数值大小也就是所谓的“步距”，如同人迈步走路，每一步都有大小。对每个百分比，我们一般固定测试 3 分钟，让数据稳定下来，然后调用 PerfAnalyzer 分析并决定下一步。

举个例子，假如我们决定总体测试 60 分钟，并且是从 0% 开始。因为每一个百分点需要测试 3 分钟，那么我们就会决定调整的“步距”大小是 5%，因为最大就是 100%。如果 PerfAnalyzer 决定需要继续增加或者减少重定向百分比，那么就按照前面决定的步距，进行相应的调整。

一起来看两个生产环境的数据

刚才讲了一大堆如何实现这个解决方案的内容，现在我们来看看两个生产环境中的实际数据，来直观地感受一下这个系统的特点。下图显示了典型 Redliner 运行的特征。

这是一次完整的容量测试，持续了一个小时。

第一幅图是 QPS，也就是系统吞吐量。蓝色线，表示 SUT 有望实现的目的 QPS。红色线，是 SUT 实现的实际 QPS。我们可以看到，实际的 QPS 值持续变化，这个变化表明了 Redliner 的控制和探测过程，就是一直在动态增加和减少重定向流量百分比。

第二幅图，标识出了所测量的客户查询等待时间的中值。

比较这两幅图，你可以看到，开始测试的阶段，RedLiner 不断提高重定向百分比，实际的流量持续增加；同时客户感受的查询等待时间也慢慢加大。

等到 SUT 不堪重负时，查询等待时间也就太大了，超出了客户能接受的阈值（40 毫秒）。所以，RedLiner 决定逐步降低重定向百分比，最后重置到初始状态。

总结

我们这一讲介绍了一个在生产环境中，进行真实场景压力和容量测试的方案，这里面的关键点，是逐步而智能地把一部分流量重定向到被测试的系统上面。

这个案例是我们在领英的生产实践，但我觉得在你的公司里实现这么一个类似的系统一点也不难。希望这些分享能帮助你设计和实现。

唐代诗人白居易的《长相思》最后的几句是：“愿作远方兽，步步比肩行。愿作深山木，枝枝连理生。”说的是主人公愿意与心爱的人相守到老，哪怕是做山林中的野兽和树木，一起步步连心，亦步亦趋，比肩而行，并肩而居。

我们用实际的生产负载做容量测试时，要小心控制重定向的流量“步距”大小，尽量“小步勤挪”，并且实时地观测，才不会影响客户的体验，并且得到比较准确的结果。

思考题

你们公司有没有类似的解决方案，来准确地测量一个服务需要的容量呢？如果有，和我讲的具体方案有何异同？不同的地方是基于什么考虑呢？

如果没有，你可以考虑实现一个，我相信一定会让老板对你另眼相看的。

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

26 | 怎么规划和控制数据库的复制延迟大小？

你好，我是庄振运。

在正文开始前，首先要祝你新年快乐！今天我们要通过我在 LinkedIn（领英）公司做过的一个项目，来学习如何控制数据库复制延迟。

我在 LinkedIn 工作的时候，就遇到过因为数据复制延迟太大而导致的生产事故。当时出现了这样的情况：一个用户刚刚更新了自己的照片，可是他的朋友们却迟迟看不到更新。

你是不是觉得 LinkedIn 又不是微博，看个照片而已，早点晚点也没什么大不了的？

但换个内容，这性质就严重了。LinkedIn 网站上面会播放广告，在一定时间内，广告商往往会设置一定的广告投放预算限额，比如一天一万元。如果广告投放的收入统计数据被延迟，就会导致很严重的统计错误，实际支出严重地超过预算。一切有关钱的事，可都是大事。

所以，从用户数据的有效性、时间性和一致性来考虑，数据的传输复制延迟当然是越小越好。

什么是数据库的复制延迟？

那到底什么是数据库的复制延迟呢？如果让我用一句话来说，数据库的复制延迟其实就是当线上服务需要多个数据库时（比如为了分散流量），一条信息从源头数据库传递复制到下游数据库时经过的延迟。

还是用 LinkedIn 的例子来说明。LinkedIn 的全球用户已经超过 5.9 亿，用户流量异常庞大。这些用户流量，是由用户的各种活动而生成的事件数据。LinkedIn 的系统要求是，实时地捕获这些用户数据，并不断存储在后台数据库中。这些存放在数据库中的用户数据很重要，会被各种应用程序和其他服务读取，比如广告投放就需要消费这些数据。

在消费这些数据的时候，理论上来说，数据的消费者可以直接连接到后台数据库，去读取这些数据。但是，这些用户数据流具有大数据的几个特征，包括大规模和高可变性，并不适合直接连接后台数据库。因为，这会对后台数据库造成很大压力，而且不能保证读取的延迟。

为了解决这一挑战，当今主要的互联网公司，往往部署一整套的数据处理系统。这一整套系统通常由几个模块构成，包括数据事件的捕获、数据的存储、数据的复制传输、数据的读取。

在这个系统中，数据的复制传输的作用很特别，是用来隔离数据库和数据使用者的。

采用数据复制传输模块的好处非常大，它可以减轻源头数据库的压力。因为复制传输可以进行级联，分散了用户流量，能够让系统的可扩展性更好。但这里有个前提，那就是，要保证数据的复制延迟不能太大，否则会造成很糟糕的业务影响，比如影响广告业务的收入。

我们今天就针对这个数据复制传输系统，来探讨如何通过合适的容量规划和分析，来控制数据的复制延迟。

要想理解后面要讲到的容量规划，你得先去了解生产环境中的用户流量特点才行。

下面的图显示了一个互联网服务的在线用户流量。

时间范围是连续的六周，也就是 42 天的流量。你可以看到，用户流量呈现出了非常强的重复模式：基本上是以一周为一个周期，每周内五个工作日流量比较大，而周末的两天流量比较小。而且无论哪天，每天内部都有一个峰值。

我们接着研究每一天内流量的周期性模式。对于每个工作日和周末，流量的形状也是非常有规律的曲线。下图显示了一个工作日中，也就是 24 小时内的流量变化。你可以清楚地看到，每天都会有一段时间的高峰期，约为 8 小时。

我们也对比了工作日和周末的流量变化，结果也是毫不奇怪的：工作日峰值一般远高于周末的峰值。二者的峰值之间，差不多有 4 倍的差距。

你只有了解了用户流量特征，才能在实际操作中选对模型。

了解了生产环境中的用户流量特征后，你还要了解 LinkedIn 采用的数据传输复制系统才行。如下图所示，LinkedIn 的系统有如下几个模块：事件生成、数据库存储、数据复制 / 传输和数据的消费 / 读取。

具体来讲，当用户与公司的网页互动时，相应的用户更新事件就被发送到了数据库。这些事件包括：用户点击了其他链接、阅读了其他用户的动态、向其他用户发信息等等。每个用户事件都由数据复制模块传输，并提供给下游消费者服务。

同一个用户数据，可能被很多应用程序和服务模块读取，所以这样上下游级联的系统设计，有比较好的扩展性，可以轻松应对规模的扩展。比如，如果数据复制模块不堪重负，而成为性能瓶颈，那么可以采用发散式级联的方式来扩展，从而分散读取的流量。

需要注意的是，数据传输复制模块除了可以提供高扩展性，也可以提供数据的一致性。像 LinkedIn 这样的公司，服务的用户遍布全球，在全球也就有很多数据中心。因为互联网流量分布在多个数据库或多个数据中心，所以就需要一个整合而一致的数据视图。这样一个目的，是可以通过传输复制实时数据库事件来得到的。

这个系统的另外一个特性是，虽然用户流量可能随着时间变化很大，但是数据复制和传输的能力相对稳定。我们通过测量和观察，发现复制传输模块的吞吐量能力非常稳定，如下图所示。

那么面对不断变化的客户流量（也就是事件流量），我们要如何规划整个系统，来让稳定的数据复制能力去适应用户流量的不稳定性呢？

怎么解决数据库复制延迟问题？

要达成目的，我们必须做好容量规划和分析。

要减少数据库复制延迟，我们在进行容量规划的时候需要考虑几个因素，包括用户流量、复制模块的吞吐量、复制的延迟以及复制延迟的 SLA（Service Level Agreements，服务水平协议）；从而确保所需的复制延迟，不要超过 SLA 的规定。

另外，通过充分考虑用户流入的流量和复制容量，我们还可以做出一些预测，比如未来一定时间内的预期数据复制延迟。而且，大多数互联网公司的流量往往有不断增长的趋势，我们需要持续地提高复制处理能力，来应对数据流量的增加。

数据库复制的容量规划和分析，其实就是在三个基本变量中辗转腾挪。哪三个变量呢？就是用户流量大小、传输复制容量的能力和传输复制的延迟。给定任何两个变量，都可以确定第三个变量。

具体来说，数据库复制的容量规划和分析可以帮助回答以下几个问题：

未来的流量预测

数据复制延迟的预测

确定数据复制的容量

确定用户流量的增长空间

帮助确定延迟 SLA

未来的流量预测，也就是根据历史流量的数据，预期未来的流量（这个问题也能帮助回答后面的问题）。

数据复制延迟的预测，也就是给定传入流量和复制处理能力，预期的复制延迟是多少？这些数据，可以帮助我们确定复制延迟的 SLA。

确定数据复制的容量，就是在假设给定传入流量和最大允许的复制延迟（SLA）的情况下，确定我们需要部署多少数据复制容量？这将有助于定义复制容量需求。

确定用户流量的增长空间，就是在给定复制的容量和延迟的 SLA 的情况下，确定最大可以支持多少用户流量？这有助于计划将来的容量要求。

帮助确定延迟 SLA，就是在给定输入流量，现在或将来的复制处理能力的情况下，如何确定适当的 SLA？显然，作为一个公司或者部门，我们不想过度承诺或低估 SLA。

解决方案如何落地？

那么这具体是如何操作的呢？现在我来为你介绍一下我们采用的规划和分析模型（这是基于统计模型的）。

你要知道，数据的传输复制模块，其实是一个排队系统，是一个有无限缓冲的先进先出队列。

从输入角度讲，所有的用户流量数据都进入这个先进先出队列，然后传输复制模块会一个一个地处理。如果在任何时候，用户流量大小超过传输能力，那么队列就会加长。反之，队列就会变短。不过与典型的排队论问题不同，在这里，我们会更加关注所有事件的“最长”等待时间，并根据这个值来决定 SLA。

你还记得我们在最开始对用户流量特征的观察吗？基于前面对流量的观察和测量，你认为该用什么模型来预测未来数据呢？

没错，最好把用户流量看作是一个时间序列模型。并通过这个模型来预测未来的数据。

对于这类时间序列数据，通常选择 ARIMA（Autoregressive Integrated Moving Average，自回归积分移动平均线）模型，来进行建模和预测。什么是 ARIMA 呢？ ARIMA 是一个常见的统计模型，是用来对时间序列进行预测的模型。它的全称是自回归移动平均模型（ARIMA, Autoregressive Integrated Moving Average Model)。

ARIMA 的工作机制有较大的计算开销，所以 ARIMA 不太适合大规模的建模，比如超过几百个数据点就不太合适了。一般来讲，对于一年内的预测，ARIMA 只能预测到每天的尺度，因为一年只有 365 天。对这个模型来讲，比天更细的粒度，不太适合。但是我们这里的容量规划，又偏偏需要获取更加精细粒度的数据，比如每小时的预测数据，而不仅仅是每天的数据。

考虑到这几个特点，对于较长期的预测，比如半年期间的话，该怎么办呢？我们提出了两步预测模型来获取未来的每小时流量。

简而言之，这个两步预测模型采取两步走的办法。

第一步是工作在星期这个粒度上，获取每周的聚合流量；第二步再将聚合数字“分布”（或“转换”）到一天内的每个小时。这样做的好处是，一方面减少了数据的点数，比如一年也就 52 周；另一方面仍然可以得到每小时的数据。

你可能想问，这个第二步的转换，也就是流量从一周到小时的“转换”，是怎么做的呢？事实上，我们采用了一个季节性指数，该指数大致代表一周内每小时的流量部分。你也可以直观地把这个指数看作是一个分配函数。

下图就是一周到小时的季节指数展示。一周内有 168 小时，也就是有 168 个数据点。这些数据点的值大小，其实就是分布概率。所有的数据点的值加在一起正好等于 1。

具体来说，该模型包括两个步骤：

使用 ARIMA 模型，预测未来几周的每周总体流量；

使用季节指数，预测一周内每个小时的流量。

采用这个模型就可以回答我们前面提出的问题了，比如可以轻松地预测未来每小时的流量。

那么具体的算法怎么实现呢？比如回答与容量规划相关的其他几种类型的问题，我们采用了数值计算和二进制搜索的类似机制。

这里的二进制搜索原理，不难理解，其实就是不断地尝试，直到找到一个最合适的值为止。比如我们需要一个确定总的容量，一方面希望容量越小越好（因为省钱）；另一方面又不能违反传输复制延迟的 SLA。

怎么用二进制搜索呢？

就是随便假定一个容量值，然后带入模型去推导出传输复制的延迟，然后判断这个延迟是否违反了 SLA。如果违反了，说明我们一开始的容量值太小，应该增加一点。怎么寻找呢，就是每次取中间的值，重新推导。这个过程如同二进制搜索。

为了便于理解，我们假设时间粒度为每小时。一旦获得流量，则将使用数值计算方法，来判断任何时间点的复制延迟。基于数值计算的结果，也就可以使用二进制搜索，来获得特定流量所需的数据复制容量。

同样的道理，还可以用此模型，来确定任何固定场景可以支持的最大用户流量，以及相应的将来日期。也就是根据预测的增长，未来什么时候用户的流量就会超越这个最大用户流量。

最后，检查每种场景下的预期复制延迟，我们还可以确定适当的复制延迟 SLA。

任何针对流量的预测模型，都不可避免地会出现误差。产生预测误差的主要原因是网络流量的高变化性。

当实际流量小于预测流量时，实际的复制延迟也将低于预期延迟。注意，这种低估误差不会违反基于预测值定义的延迟性能 SLA。唯一付出的成本，是部署了一些额外的资源。但是，当预测的流量小于实际流量时，就可能会违反延迟 SLA。

为了解决此问题，在确定各种指标（例如 SLA）时，有必要预留一定的空间来避免预测误差。预留的量取决于一系列因素，包括：

预测模型的历史表现，也就是预测的精准度；

违反延迟 SLA 的后果，包括业务付出的成本；

过度部署容量资源的成本。

好了，到这里这个规划和分析模型就讲完了，内容比较多，我们来复习一下。对于数据库复制系统来说，可以把它当作一个排队系统来对待。不断到来的输入流量，是一个有季节性的不断变化的时间序列。通过对这个时间序列来适当建模，我们可以比较准确地预测未来的输入流量。有了预测的输入流量，就可以根据服务处理的速度和复制延迟的关系，来计算出其他结果：比如预期的复制延迟，所需要的复制容量等等。

实际生产实践的验证

我们在 LinkedIn 的生产实践中，采用了这个规划，下面我们来看看一些结果。

我们还是使用前面介绍的 42 天数据，来做未来的流量预测。使用 ARIMA 模型的实践建议是，预测数据的长度最好不要超过历史数据长度的一半。所以，既然我们有 42 天的历史数据，通常可以在未来的 21 天之内，使用 ARIMA 模型进行预测。

下图展示了这 21 天的两个数据，一个是生产环境中的实际数据，另外一个是根据我们模型的预测值。蓝颜色的线条是实际观察到的用户流量，绿颜色的线条是我们的预测值。

从图中可以观察到，虽然有些地方还是有误差，但是从解决实际问题的角度，我们认为已经足够了。我们也把 ARIMA 方式和其他预测方式做了比较，比较预测的时间序列结果，我们发现 ARIMA 模型给出了更好的精度；但差异不大，仅仅有 6％左右的差异。

总结

唐代诗人杜甫有两句诗说：“迟日江山丽，春风花草香。”说的是春天越来越长，沐浴在春光下的江山格外秀丽，春风也送来花草的芳香。数据传输复制的延迟，如果不仔细控制，也会变得越来越长，最终会导致严重问题。到时候就不是春风送暖和花草飘香，而是秋风萧瑟，老板发威了。

我们今天探讨了如何通过合理的规划和分析，来控制这个数据库复制延迟。具体就是使用 ARIMA 统计模型，并且用两步走的策略，来回答一系列的各种相关问题。

我想特意说明的是，这一整套时间序列的预测原理和方法，其实也可以用在很多其他的很多种预测场景中。比如一个在线广告显示系统（输入的流量是线上用户活动量，要进行的处理是决定合适的广告来显示）。

通过今天的分享，我希望你能掌握的要点是对于一个线上的数据处理系统，我们可以进行基于排队理论和时间序列的建模，通过这个建模来回答各式各样的相关问题。这个方案其实还牵涉到一些稍微复杂的统计理论，如果你还有精力，我建议你去读一下我的一篇论文，发表在ACM ICPE上面，里面阐述了所有的细节，你在阅读的过程中有什么问题或者思考，也可以留言和我讨论。

思考题

如果数据的传输复制延迟过大，会造成很多种不同场合的业务影响，都会有什么样的业务影响呢？

提示：假如你们公司的网站上面帮助别人投放广告，如果广告的数据有延迟，会造成广告商和你们公司之间的什么样的纠纷呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

27 | 多任务环境中的Java性能问题，怎样才能不让程序互相干扰？

你好，我是庄振运。

我们来继续学习生产实践中的案例。在生产实践中，为了降低公司运营成本，更好地利用系统容量，并提高资源使用率，我们经常会让多个应用程序，同时运行在同一台服务器上。

但是，万事有利就有弊。这几个共存的应用程序，有可能会互相影响；有时还会导致严重的性能问题。我就遇到过，几个程序同时运行，最后导致吞吐量急剧下降的情况。

所以，今天我们就来探讨，当多个 Java 应用程序共存在一个 Linux 系统上的时候，会产生哪些性能问题？我们又该怎么解决这些问题？

怎样理解多程序互相干扰？

为了更好地理解后面的性能问题，你需要先了解一下应用程序内存管理机制的背景知识。我们运行的是 Java 程序，所以先快速复习一下 Java 的 JVM 内存管理机制。

Java 程序在 Java 虚拟机 JVM 中运行，JVM 使用的内存区域称为堆。JVM 堆用于支持动态 Java 对象的分配，并且分为几个区域，称为“代”（例如新生代和老年代）。Java 对象首先在新生代中分配；当这些对象不再被需要时，它们会被称为 GC（Garbage Collection）的垃圾回收机制收集。发生 GC 时，JVM 会从根对象开始，一个个地检查所有对象的引用计数。如果对象的引用计数降为零，那就删除这个对象，并回收使用这个对象相应的存储空间。

GC 运行的某些阶段，会导致应用程序停止响应其他请求，这种行为，通常称为 STW（Stop The Word 暂停）。 JVM 调优的重要目标之一，就是最大程度地减少 GC 暂停的持续时间。

复习完 JVM 内存管理机制，我们还要看一下与它相关的 Linux 的内存管理机制。

在 Linux 操作系统上，虚拟内存空间基本上是固定大小（例如 4KB）的页面。Linux 近年来有很多内存管理的优化，来提高内存使用效率和运行进程的性能。

Linux 内存管理有一个页面回收的机制。它在内部维护一个空闲页面（Free Page）列表，来满足未来应用程序的内存请求。当空闲页面的数量下降到一定水平时，操作系统就开始回收页面，并将新回收的页面添加到空闲列表中。

执行页面回收时，操作系统需要进行页面扫描（Page Scanning），以检查已经分配页面的活动性。Linux 有两个策略来进行页面扫描：后台扫描（由 kswapd 守护程序执行）和前台扫描（由进程自己执行）。

通常情况下，后台扫描就够了，应用程序的性能一般不会受到影响。但是当操作系统的内存使用非常大，空闲页面严重不足时，Linux 就会启动前台页面回收，也被称为直接回收或同步回收。在前台页面回收过程中，应用程序会停止运行，因此对应用程序影响很大。

Linux 还有一个页面交换（Page Swapping）的机制。是当可用内存不足时，Linux 会将某些内存页面换出到外部存储，以回收内存空间来运行新进程。当对应于换出页面的内存空间，再次处于活动状态时，系统会把这些页面重新从外部存储换入内存。

内存管理方面，THP（Transparent Huge Pages，透明大页面）是另外一个机制，也是为了提高进程的性能。我们在第 22 讲讨论过，如果系统用较大的页面，比如 2MB，而不是传统的 4KB，那么会带来一些好处，尤其是所需的地址转换条目数会减少。

尽管使用大页面的好处很早就为人所理解，但在 THP 引入之前，程序想使用大型页面并不容易。例如，操作系统启动时，需要保留大页面，并且进程必须显式调用才能分配大页面。而 THP 就是为了避免这两个问题而设计的，因此操作系统默认情况下就启用 THP。

了解了背景知识，你再看多个应用程序共存时的两个场景就不会有障碍了。第一个场景是应用程序启动时，第二个场景是应用程序稳定运行时。

应用程序启动时为什么会被其他程序干扰？

我们先看应用程序启动时的场景。当几个共存的应用程序共享有限的计算资源（包括内存和 cpu）时，它们之间会相互影响。如果各自独立地运行，导致对系统计算资源的消耗无法协调一致，那么某些应用程序会出现问题。

我们要做个实验来暴露这些性能问题，看看这个问题的表象是什么，然后一起分析产生问题的原因。

这个实验采用了两个相同的 Java 程序。我们首先启动第一个程序，来占用一些内存，系统剩下约 20GB 的未使用内存。然后我们开始启动另外一个 Java 程序，这个程序需要 20GB 的堆。

在图中你可以看到，启动第二个程序之后，它的吞吐量是 12K/ 秒，持续时间约 30 秒。然后，吞吐量开始急剧下降。最坏的情况，在大约 20 秒的时间内，吞吐量几乎为零。有趣的是，过了一会儿，吞吐量又再次回到了稳定状态：12KB/ 秒。

下图显示了同一时间段的 GC 暂停信息。

最初的 GC 暂停很低，都低于 50 毫秒；然后暂停就跳到数百毫秒之大。你甚至可以看到两次大于 1 秒的超大的暂停！大约 1 分钟后，GC 暂停再次下降至低于 50 毫秒，并变得稳定。

我们看到在启动期间，Java 程序的性能很差。因为问题是在启动 JVM 时发生的，我们有理由怀疑这与 JVM 的启动方式有关。我们检查了程序的的内存驻留大小（RES，Resident Size），也就是进程使用的未交换的物理内存，图示如下。

从图中你可以看到，尽管我们在启动 JVM 时，用参数将 JVM 的堆大小指定为 20GB（-Xmx20g 和 -Xms20g），但是 JVM 并不会从内存中一次全部拿到 20GB 的堆空间。相反，操作系统会在 JVM 的运行过程中不断地分配。也就是说，随着 JVM 实例化越来越多的对象，JVM 会从操作系统逐渐拿到更多的内存页面来容纳它们。

在分配过程中，操作系统将不断地检查空闲页面列表。如果发现可用内存量低于一定水平，操作系统就会开始回收页面，这个过程会花费 CPU 的时间。根据可用内存短缺的严重程度，回收过程可能会严重阻塞应用程序。在下图中，我们看到，可用内存明显地下降到了非常低的水平。

下面这张图显示了 CPU 的空闲百分比（CPU 空闲百分比和繁忙百分比的和是 100%）。对比时间线，我们可以清楚地看到，页面回收过程会导致 CPU 开销，也就是空闲百分比下降了。

那么怎么进行内存回收呢？

在 Linux 上，当可用内存不足时，操作系统会唤醒 kswapd 守护程序，开始在后台回收空闲页面。如果内存压力很大，操作系统就会被迫采取另外一种措施，就是直接地同步释放内存的前台。具体来讲，当可用空闲页面降到一个阈值之下，就会触发这种直接前台回收。

当发生直接前台回收时，Linux 会冻结正在申请内存的执行代码的应用程序，从而间接地导致大量的 GC 暂停。

此外，直接回收通常会扫描大量内存页面，以释放未使用的页面。那么我们就来看看 Linux 直接回收内存页面的繁忙程度。下图就画出了 Linux 通过直接回收路径扫描的页面数。

我们看到，在峰值时，通过直接回收，每秒扫描约 48K 个页面（即 200 MB）；这个回收工作量是很大的，CPU 会不堪重负。

运行中的应用程序为什么会被别的程序干扰？

了解过程序启动时互相干扰的场景，我们再来考虑第二个场景：应用程序在持续运行中。

我们的实验是这样进行的。第一个 Java 程序以 20GB 的堆启动，并进入稳定状态。然后另外一个程序启动，并开始分配 50GB 的内存。

下图中体现了第一个程序的吞吐量。

从图中我们看到，第一个程序从一开始就实现了稳定的 12K/ 秒的吞吐量。然后，吞吐量急剧下降到零，这个零吞吐量的过程持续了约 2 分钟。从那时起，吞吐量一直在发生相当大的变化：有时吞吐量是 12K/ 秒，其他时候又降为零。

我们也观察了 JVM 的暂停，用下图所示。

从图中我们看到，在稳定状态下，GC 暂停几乎为零，然后居然有一个超级大的暂停；多大呢？55 秒！从那时起，GC 暂停持续变化，但很少恢复为零。大多数暂停时间为几秒钟。

我们观察到，其他应用程序的运行会严重影响本程序的性能。各种观察的结论是，系统处于内存压力之下，操作系统内存会有很多和外部存储的页面交换活动。在下图中，我们看到操作系统交换出了很多内存页面到外部存储空间。

这些换出的内存页面很多属于 Java 程序（也就是堆空间）。如果 JVM 需要进行堆上的垃圾回收，也就是 GC，那么 GC 需要扫描 JVM 对象，以收集失效的对象。如果扫描的对象恰好是分配在换出的页面上，那么 JVM 需要先将它们从外部存储交换空间重新载入到内存中。从外部存储载入内存需要一些时间，因为交换空间通常位于磁盘驱动器上。

所有这些时间，都会算在 GC 暂停之中。因此，程序会看到较大的 GC 暂停。下图就显示了大量的从外部存储载入页面的活动。

尽管页面交换活动会增加 GC 暂停时间，似乎可以解释刚刚看到的 JVM 暂停。但是，我怀疑，仅是这个原因根本无法解释生产中看到的很大暂停，比如超过 55 秒的暂停。你可能会问，我为什么有这样的怀疑？因为我在许多 GC 暂停的过程中，观察到了较高的系统 CPU 使用。

比如在下图中，我们观察到，系统也处于严重的 CPU 压力下。

CPU 的高使用率不能完全归因于页面交换活动，因为页面交换通常不会占用大量 CPU。所以，其中“必有隐情”：一定是有其他活动在大量使用 CPU。我们通过检查了各种系统性能指标，最终确定了根因：是由于 THP 的机制，该机制严重加剧了程序性能和系统性能的下降。

具体来说，Linux 启用 THP 后，当应用程序分配内存时，会优先选择 2MB 大小的透明大页面，而不是 4KB 的常规页面。这一点我们可以轻易验证，比如下图中显示了透明大页面的瞬时数量。在峰值时，我们看到约 34,000 个 THP，即约 68GB 的内存量。

我们还观察到，THP 的数量一开始很高，一段时间后开始下降。这是因为某些 THP 被拆分成小的常规页面，以补充可用内存的不足。

为什么需要拆分大页面呢？是因为当 Linux 在有内存压力时，它会将 THP 分为常规的、要准备交换的页面。为什么必需拆分大页面？这是因为当前的 Linux，仅支持常规大小页面的交换。

拆分活动的数量我们也用下图画出来了。你可以看到，在五分钟内大约有 5K 个 THP 页面被拆分，对应于 10GB 的内存。

除了大页面拆分，同时，Linux 也会尝试将常规页面重新聚合为 THP 大页面，这就需要额外的页面扫描，并消耗 CPU。如果你在实践中注意观察的话，可以发现这种活动会占用大量 CPU。

使用 THP 可能遇到的更糟糕的情况是，聚合和拆分这两个相互矛盾的活动，是来回执行的。也就是说，当系统承受内存压力时，THP 被拆分成常规页面，而不久之后，常规页面则又被聚合成 THP，依此类推。我们已经观察到，这种行为会严重损害我们生产系统中的应用程序性能。

如何解决多程序互相干扰？

那么程序在启动和运行时互相干扰的性能问题，到底该怎么解决呢？我们现在就来看解决方案。

我们的解决方案由三个设计元素组成，每个设计元素都针对问题的特定方面。部署任何单独的元素都将在一定程度上对问题有所帮助。但是，所有设计元素协同工作，才能获得最好的效果。

第一个设计元素是预分配 JVM 的堆空间。

我们知道，对 JVM 而言，只有在实际使用堆空间之时，就是当需要增大堆空间来容纳新对象分配请求时，Linux 才会为之分配新的内存页面，这时就可能会触发大量页面回收，并损害程序和系统性能。

这个设计元素就是预分配所有堆空间，从而避免了 Linux 实时分配页面的不利场景。要预先执行堆预分配，需要使用一个特殊的 JVM 参数：“ -XX：+ AlwaysPreTouch”，来启动 Java 应用程序。

但这个设计元素也有副作用，就是增加了 JVM 启动所需的时间，在部署时你需要考虑这一点。我们也做过一些实际测量，这个额外启动时间并不大，一般在几秒钟内，通常是可以接受的。

第二个设计元素，是关于如何保护 JVM 的堆空间不被唤出到外部存储。

我们知道，当发生 GC 时，JVM 需要扫描相应的内存页。如果这些页面被操作系统换出到外部存储，则需要先换入它们到内存，这就会导致延迟，会增加 JVM 的暂停时间。

这个设计元素就可以防止 JVM 的堆页面被换出。 我们知道，Linux 操作系统上是可以关闭内存页面交换的，但是这个设置如果是在系统级别进行，就会影响所有应用程序和所有内存空间。我推荐你一个更好的实现，就是采用微调，你来选择哪个应用程序和哪个存储区域可以页面交换。例如，你可以使用 cgroup 来精确控制要交换的应用程序。

公司中的大多数平台，一般都用来运行同类 Java 应用程序；这些程序往往配置差不多。在这些情况下，在系统级别关闭应用程序交换，倒也是非常合理的。

第三个设计元素是动态调整 THP。

我们已经看到，启用 THP 功能可能会在某些场景下，导致严重的性能损失；但是 THP 在其他场景的确提高了性能，所以到底是否要启用 THP 呢？我们需要仔细考虑。

当 THP 影响性能时，系统的可用内存往往也恰好严重不足。发生这种情况时，现有的 THP 需要拆分成常规页面以进行页面换出。所以，我建议你用一个可用内存大小的阈值来决定 THP 的开关。

具体来说，就是建议你使用应用程序的堆大小作为内存阈值，来决定是否打开或关闭 THP。当可用内存远远大于应用程序的内存可能占用量大小时，就启用 THP，因为系统不太可能在启动特定应用程序后出现内存压力。否则的话，就关闭 THP。

由于许多后端服务器都是运行同类应用程序，通常情况下，你都很容易知道，部署的应用程序预期会占用多少内存空间。

此外，常规页面需要聚合成 THP，才能将大页面分配给应用程序。因此，这个元素的另外一部分机制是进行微调，是决定何时允许 THP 聚合。我建议你根据操作系统的直接页面扫描率和聚合进程的 CPU 使用率来决定。

总结

今天我们讲述了，将多个应用程序放置在同一台服务器上时，由于应用程序和操作系统机制的互相作用，引发的一系列性能问题。这些问题的根本原因，就是程序之间的互相影响。

应用程序之间的关系和人际关系一样，有时和谐，有时不和谐。唐代诗人刘禹锡有几句诗说：“常恨言语浅，不如人意深。今朝两相视，脉脉万重心。”说的是，语言的表达能力通常很有限，所以两人只能用眼神传达更复杂的情感。应用程序之间的关系，甚至程序和操作系统及硬件之间的关系，也会很复杂，也需要做足够的性能分析，才能理清它们之间的关系。

今天的讲述，主要集中在多任务共存环境中的两个问题，重点在分析问题产生的复杂根因。 如果你对这方面的具体算法和生产验证有兴趣，可以参考我的一篇论文。这篇论文发表在International Journal of Cloud Computing上面。

思考题

Linux 操作系统的 THP 机制的设计初衷，本是为了提升系统性能。可是在有些情况下反而导致了系统性能下降。想一想操作系统的其他机制，有没有类似的情况发生？

Tips：文件系统的预先读取等。

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

28 | 网络数据传输慢，问题到底出在哪了？

你好，我是庄振运。

你一定有过在网页或者手机上下载照片的体验，如果数据传输太慢，那你的体验一定十分糟糕。你看，互联网实体之间的数据快速传输对用户体验至关重要。这里涉及到的其实就是网络传输问题。所以，今天我们就通过生产实践中的案例，来探讨一下互联网服务中的数据传输性能。

说到底，网络传输问题其实就分两种：

数据根本没有传递；

数据传送速度较慢。

“数据没有传递”虽然看起来更严重，但是相对“数据传送缓慢”来说，更容易判断和解决。所以，这一讲，我们就重点解决第二种问题。我们一起来看看，为什么网络传送速度会慢，在众多原因中怎么快速诊断出关键问题来，又该如何去解决。

造成网络传输缓慢的原因很多，我们这一讲，就是帮助你快速诊断问题出在哪里：是客户端，是服务器端，还是网络本身？在此基础上，你才能专门针对具体的领域继续分析。

为什么数据传输慢？

我们先看一下，都有哪些可能的原因会导致数据传输缓慢呢？在宏观上，这种问题的可能原因可以分为三种场景：

客户端应用程序的原因；

网络的原因；

服务器应用程序的原因。

也就是说，可能是由于数据发送方过载，而没有向接收方发送数据；也可能是网络通道很慢；又或者是数据接收方的服务器太忙，从而无法从网络缓冲区读取数据。

为了描述方便，我们根据平时客户浏览网页的场景，假设客户端是数据接收方，而服务器端是数据发送方。

进行此类分析诊断时，负责的工程师通常需要快速隔离出上述不同场景，以便他们可以专注于特定场景里面的可疑组件，并对本质原因进行更深入的分析。但这一快速诊断过程会遇到很多难点。

首先，数据传输涉及多个网络实体，包括两台机器（也就是发送者和接收者）和网络路由，这与仅涉及一台机器的常见性能问题形成鲜明对比。

其次，这种诊断涉及多层信息，包括应用程序层和网络传输层。为了找出原因，工程师必须检查各种数据，包括客户端日志、服务器日志、网络统计信息、CPU 使用情况等。这些检查需要花费很多时间和精力，并且通常需要性能工程师的经验和专业知识。

更加让人郁闷的是，这些日志往往分散在不同的地方，比如客户端和服务器。为了节省时间和精力，性能工程师迫切需要更智能的工具，以帮助他们快速找出根本原因。

所以今天，我专注于解决这样的一个问题，就是：快速确定应归咎的组件范围和场景（无论是发送方、接收方还是网络本身）。我提出了一种当发生数据传输缓慢的问题时，可以自动隔离原因的解决方案。毕竟，你只有找出了要对“数据传输慢”负责任的那一部分，才可以进行后续分析工作，最终确定真正的问题。

如何判断问题所在位置？

要想快速诊断，我们需要先看看三种问题场景的不同特征。

这个解决方案本质上依靠的是客户端和服务器端的 TCP 层面的特征。TCP 是传输层协议之一，可提供有序且可靠的流字节传输，是当今使用最广泛的传输协议。TCP 具有流控制功能，可避免接收方过载。接收方设置专用的接收缓冲区，发送方设置相应的发送缓冲区。数据发送方（服务器）的发送缓冲区和数据接收方（客户端）的接收缓冲区，都可以通过操作系统来监测当前队列大小。

为了能够识别瓶颈，你需要在发送方和接收方的传输层上，收集有关队列大小的信息。有很多收集此类信息的方法。你有两种工具可以使用，分别是 Netstat 和 ss。

Netstat 是一个命令行工具，可以显示网络连接和网络协议统计信息。我们主要是用它来观察 TCP / IP 套接字的发送队列和接收队列的大小。而 ss 命令，可以显示套接字统计信息，包括显示 TCP 以及其他类型套接字的统计信息。类似于 Netstat，ss 还可以显示发送和接收队列大小。

除了相应的工具的介绍，为了帮助你理解，我们还需要先重温一下传输数据时候，应用层和 TCP 层的交互。

上图显示了任何基于 TCP 的数据传输中的典型流程。关于系统调用和网络传输的五个步骤如下：

在步骤 A，服务器应用程序发出 write() 系统调用，并将应用程序数据复制到套接字发送缓冲区。

在步骤 B，服务器的 TCP 层发出 send() 调用，并将一些数据发送到网络；数据量受 TCP 的拥塞控制和流控制。

在步骤 C，网络将数据逐跳路由到接收方（IP 路由协议在这部分中发挥作用）。

在步骤 D，客户端的 TCP 层将通过 recv() 系统调用接收数据，数据放入接收缓冲区。

在步骤 E，客户端应用程序发出 read() 调用，以接收数据并将其复制到用户空间。

接下来，我们就来看看三种不同场景下的问题特征是什么样的。

我们先看第一个场景，客户端接收数据缓慢的情况。为了重现这一场景，我们做一个实验，让发送端发送一段固定大小的数据给接收方。我们强制接收方，也就是客户端，减慢数据的接收速度。具体做法，就是在应用程序代码的 read() 调用之前，注入了一定的延迟，这种场景代表了客户端数据接收成为瓶颈的情况。

上图显示的是数据发送方的发送缓冲区，SendQ（Send Queue）的大小变化。开始时候，数据发送调用 send()，立刻注满 SendQ。随着数据的传输，慢慢变为 0。

第二张图是客户端的接收缓冲区，RecvQ（Receive Queue）的大小变化，客户端因为应用程序运行缓慢，所以 RecvQ 具有一定的积累，这可以由非零值来看出。这些非零值持续了一段时间，随着应用程序不断地读取，最终 RecvQ 减为 0。

对于第二种场景，也就是数据发送方是瓶颈的情况，我们强制发送方（即服务器端），放慢数据的发送速度。具体来说，我们在应用程序代码中，对 write() 的调用之前注入了一定的延迟，模拟了发送者是瓶颈的情况。

上图显示了服务器端的 SendQ 的值，你可以看到，SendQ 几乎全部是零。这是因为发送端是瓶颈，其他地方不是瓶颈，所以任何 SendQ 的数据会被很快发送出去。

你可以在图片中看到一个持续时间很短的峰值，这是因为 SendQ 取样的时候恰好取到数据还没有被传输到网络中的时候。但因为这个峰值持续时间很短，简单的过滤就可以去掉。

接收端的 RecvQ 显示在下图，你可以看到，因为接收端不是瓶颈，RecvQ 是零。

第三个场景，是网络本身是瓶颈造成的数据传输缓慢。我们通过向网络路径注入延迟来创造这一场景，以使 TCP 仅能以非常低的吞吐量进行传输。

图片中显示了发送端的 SendQ 值，你可以看到它的值不为零，因为那些数据不能很快地被传送出去。

再来看接收端的 RecvQ，如下图。RecvQ 全为零，这些零值就代表了快速的数据传递。

通过上面三种场景的分析，尤其是对发送端 SendQ 和接收端 RecvQ 的观察，我们不难总结出规律来。正常的数据传输情况下，客户端的接收队列和服务器端的发送队列都应该是零。

反之，如果数据传输缓慢，则有如下几种情况：

如果客户端上的接收队列 RecvQ 不为零，则客户端应用程序是性能瓶颈；

如果服务器上的发送队列 SendQ 为零，则服务器应用程序是性能瓶颈；

如果客户端的接收队列 RecvQ 为零，而服务器的发送队列 SendQ 为非零，则网络本身是性能瓶颈。

为了帮助你加深记忆，我用表格来做了个归纳。

你可以通过这个表格，快速判断问题出现的位置。

解决方案如何落地？

根据前面的分析和总结，我们现在提出解决方案。这是一个基于状态转移的方案，需要从客户端和服务器端收集几个关键点的信息。

为了帮助你理解，我们需要先来看看数据请求和传输流程图。就用常见的 HTTP 协议的 Request 和 Response 方式来描述，如下图所示。

当客户端需要下载服务器的数据时，首先在 T0 发出数据请求；网络将请求发送到服务器后，服务器在 T1 收到数据。

然后，服务器开始准备数据，数据准备好后，服务器将开始在 T2 时发回数据。通过一系列 write() 调用。发送在 T4 完成。网络传输后，客户端在 T3 开始接收数据，并在 T5 完成接收。请注意，尽管其他时间戳是按照严格的顺序，T3 和 T4 的顺序可能会因实际情况而异。具体来说，对于小数据传输时，T4 可以先于 T3，因为单个 write() 调用就足够了。对于大数据传输，通常使用 T3 在 T4 之前。

接下来，我们来看看基于状态机的解决方案，它是一个针对 HTTP 数据传输问题的，完整而具体的解决方案。

从上面的过程中，我们可以看到，如果服务器无法接收到数据请求，则数据传递将不会发生，因此不会完成。

我用下图来表示整个状态机。这个状态机展示了整个 HTTP 数据传输的过程，包括 Request 和 Response。如果数据传递成功，状态机最后会到达状态 F。

如上图，数据传递的初始状态为 State-S。客户端发出请求后，它将移至状态 A；当网络通道完成其工作，并将请求传递到服务器 OS 时，状态变为 B。当服务器准备好数据，并开始发出数据的第一个字节时，状态变为 C。

当客户端收到第一个字节后，状态变为 D；最后当服务器发出最后一个字节时，状态变为 E。或者这两个次序交换，成功进行数据传递的最终状态是 State-F。

在整个过程中，如果发生其他的转移，那么就是网络传输有问题了。我们就可以根据发送端的发送队列和接收端的接收队列长度的变化，轻松判断是谁的问题，比如是客户端，服务器或是网路。

总结

今天我们讲述了，互联网服务在传输数据时，如果发生传输速度太慢的问题，怎样才能快速地诊断到底是客户端、服务器端，还是网络的问题。

唐代诗人高适的《燕歌行》有几句诗：“山川萧条极边土，胡骑凭陵杂风雨。战士军前半死生，美人帐下犹歌舞。”说的是前方的战士，在前线出生入死；后方却有人逍遥自在的观赏美人歌舞，醉生梦死。这种冰火两重天的讽刺，部分原因，就是责任没有分清，以至于滥竽充数者可以逍遥自在。

对待数据传输缓慢问题，我们也很希望能快速地搞清责任，分清是哪里的问题，然后才能有针对性地继续分析。我们的解决方案就是根据 TCP 的 Send 和 Receive 队列大小变化，来快速诊断的方案。它能智能而快速地分清问题的大致范围：就是数据发送方、数据接收方，还是网络。

思考题

根据你平时的观察，公司业务有没有发生数据传输太慢的问题？如果发生了，你们一般怎么根因呢？如果采用本讲的思路，会不会更加快速地诊断？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

29 | 如何彻底发挥SSD的潜力？

你好，我是庄振运。

今天是“性能工程实践”这个模块的最后一讲，我们来讨论一种“软硬件结合”的性能工程优化实践，与 SSD（硬件）有关。现在 SSD 用的越来越普遍的情况你一定非常清楚，但是你设计的应用程序（软件）真的充分利用了 SSD 的特点，并发挥 SSD 的潜力了吗？

要知道，SSD 可不仅仅是“更快的 HDD”。

SSD 的好处显而易见，它作为存储时，应用程序可以获得更好的 I/O 性能。但是这些收益，主要归因于 SSD 提供的更高的 IOPS 和带宽。如果你因此只将 SSD 视为一种“更快的 HDD”，那就真是浪费了 SSD 的潜力。

如果你在设计软件时，能够充分考虑 SSD 的工作特点，把应用程序和文件系统设计为“对 SSD 友好”，会使服务性能有个质的飞跃。

今天我们就来看看，如何在软件层进行一系列 SSD 友好的设计更改。

为什么要设计 SSD 友好的软件？

设计对 SSD 友好的软件有什么好处呢？简单来说，你可以获得三种好处：

提升应用程序等软件的性能；

提高 SSD 的 I/O 效率；

延长 SSD 的寿命。

先看第一种好处——更好的应用程序性能。在不更改应用程序设计的情况下，简单地采用 SSD 可以获得性能提升，但无法获得最佳性能。

我为你举个例子来说明。我们曾经有一个应用程序，它需要不断写入文件以保存数据，主要性能瓶颈就是硬盘 I/O。使用 HDD 时，最大应用程序吞吐量为 142 个查询 / 秒（QPS）。无论我们对应用程序设计进行什么样的更改或调优，这就是使用 HDD 可以获得的最好性能了。

而当迁移到具有相同应用程序的 SSD 时，吞吐量提高到了 20,000 QPS，速度提高了 140 倍。这种提高，主要来自 SSD 提供的更高的 IOPS。你是不是觉得与 HDD 相比，应用程序吞吐量有了显着提高，性能已经很好了？

但这并不是 SSD 能实现的最佳性能。

我们对应用程序设计进行了优化，使其对 SSD 友好之后，应用程序吞吐量提高到 100,000QPS，与简单设计相比，提高了 4 倍。

你可能会问，这是如何做到的？

这其中的秘密，就是使用多个并发线程来执行 I/O。如下图所示，这利用了 SSD 的内部并行性。

这里你需要注意的是，在这个系统中，多个 I/O 线程对 HDD 是毫无益处的。因为 HDD 只有一个磁头，所以用多个 I/O 线程，并不能提高旧系统的吞吐量。

第二种好处是更高效的存储 I/O。

我在第 17 讲中提到过，SSD 上的最小内部 I/O 单元是一页，比如 4KB 大小。因此对 SSD 的单字节读 / 写，必须在页面级进行。应用程序对 SSD 的写操作，可能会导致对 SSD 上的物理写操作变大，这就是“写入放大（参见第 23 讲）”。

因为有这个特性，如果应用程序的数据结构或 I/O 对 SSD 不友好，就会让写放大效果更大，导致 SSD 的 I/O 不能被充分利用。

设计 SSD 友好软件的最后一个好处是延长 SSD 的寿命。

SSD 会磨损，是因为每个存储单元，只能维持有限数量的写入擦除周期。实际上，SSD 的寿命取决于四个因素：

SSD 大小

最大擦除周期数

写入放大系数

应用程序写入速率

例如，假设有一个 1TB 大小的 SSD，一个写入速度为 100MB/ 秒的应用程序和一个擦除周期数为 10,000 的 SSD。当写入放大倍数为 4 时，SSD 仅可持续 10 个月。具有 3,000 个擦除周期和写入放大系数为 10 的 SSD 只能使用一个月。

你也知道，相对于 HDD 而言，SSD 的成本比较高，我们自然希望自己设计的应用程序对 SSD 友好，从而延长 SSD 的使用寿命。

怎么设计对 SSD 友好的软件？

说了这么多设计 SSD 友好软件的好处，那我们具体该从哪里入手呢？

其实在软件领域，有很多地方都可以做对 SSD 友好的设计，比如文件系统、数据库系统、数据基础架构层和应用程序。接下来我会一一为你介绍。尤其是应用程序这个领域，它是这一讲的重点。

第一个可以对 SSD 友好的领域是文件系统。文件系统层直接处理存储，因此我们需要在此级别进行优化设计更改，来更有效地发挥 SSD 的特长。一般而言，这些设计更改集中在 SSD 和 HDD 迥异的三个关键差异特征上：

SSD 的随机访问与顺序访问具有相同的性能；

需要在块级别进行擦除后才能重写；

内部损耗均衡的机制会导致写入放大。

现在比较流行的有两种对 SSD 友好的文件系统。第一种，是适用于 SSD 的通用文件系统，主要支持 Trim 的新功能，比如 Ext4 和 Btrfs。第二种，是专门为 SSD 设计的文件系统。基本思想是采用日志结构的数据布局（相对于 B 树或 Htree），来容纳 SSD 的“复制 - 修改 - 写入”属性，比如 NVFS（非易失性文件系统）、FFS / JFFS2 和 F2FS。

除了文件系统，数据库系统也可以设计成对 SSD 友好。

SSD 和 HDD 之间的差异在数据库设计中尤其重要。近几十年来，数据库的各个组件（比如查询处理、查询优化和查询评估等）都已经在考虑 HDD 特性的情况下进行了诸多优化。

举个例子，因为普通硬盘的随机访问比顺序访问要慢得多，所以数据库组件会尽量减少随机访问。而使用 SSD 时，类似这样的假设就不成立了。

因此，业界设计了新的、对 SSD 友好的数据库。对 SSD 友好的数据库主要有两种：第一种是专门针对 SSD 的数据库，例如 AreoSpike，这种数据库主要采用对 SSD 友好的 Join 算法；第二种是 HDD 和 SSD 混合的数据库，一般是使用 SSD 来缓存数据。

第三个领域是数据基础架构层。

对于分布式数据系统的设计而言，数据来源大体上有两个地方：计算机上的本地磁盘或另一台计算机上的内存。这两个来源哪个更快更高效呢？还真不一定。随着技术的演化，业界也一直在争论。

过去很多年，这样的争论比较倾向于远程计算机的内存，因为速度更快；Memcached 就是一个例子。传统的本地 HDD 访问延迟，大约是好几个毫秒；而远程内存访问的延迟，包括了 RAM 访问延迟和网络传输延迟，也仅处于微秒级。同时，远程内存的 I/O 带宽与本地 HDD 大致相同甚至更多。因此，远端的内存反而比本地的硬盘的访问时间更短。

而 SSD 的出现，正在改变这个趋势和业界的决定。使用 SSD 作为存储设备后，本地 SSD 变得比远程内存访问更为高效。

首先，SSD 的 I/O 延迟降低到了微秒级，而 I/O 带宽可是比 HDD 高一个数量级。这些结果就导致了数据基础架构层的新设计。比如，新设计更希望尽可能与应用程序共同分配数据，以避免额外的节点和网络的限制，从而降低系统的复杂性和成本。

这么说可能你的感受不太明显，我来用 Netflix 为你举例说明一下。Netflix 就是采用这种设计的公司之一。Netflix 公司曾经采用 memcached 来缓存 Cassandra 层数据。假设 Netflix 需要缓存 10TB 的数据，如果每个内存缓存节点在 RAM 中保存 100GB 数据，则需要部署 100 个内存缓存节点。后来，Netflix 只使用 10 个 Cassandra 节点，并为每个 Cassandra 节点配备 1TB SSD 来完全取代 memcached 层。

你看，采用这种设计，就不需要 100 个 Memcached 节点，只需 10 台配备 SSD 的节点，节省了大量成本。

在应用程序层，我们也可以对 SSD 进行友好的设计，以获得前面提到的三种好处（更好的应用程序性能、更高效的 I/O 和更长的 SSD 寿命）。

在这一领域，我总结了七大 SSD 友好的设计原则，大体上分为三类：数据结构、I/O 处理和线程使用。

1. 数据结构：避免就地更新优化

传统的 HDD 的寻址延迟很大，因此，使用 HDD 的应用程序通常会进行各种优化，以执行不需要寻址的就地更新，比如只在一个文件后面写入。

比如下图所示，在执行随机更新时，吞吐量一般只能达到约 170 QPS；而对于同一个 HDD，就地更新可以达到 280QPS，远高于随机更新。

不过在设计与 SSD 配合使用的应用程序时，这些考虑就没什么意义了。对 SSD 而言，随机读写和顺序读写性能类似，就地更新不会获得任何 IOPS 优势。

此外，就地更新实际上是会导致 SSD 性能下降的。包含数据的 SSD 页面无法直接重写，因此在更新存储的数据时，必须先将相应的 SSD 页面读入 SSD 缓冲区，然后将数据写入干净的页面。 SSD 中的“读取 - 修改 - 写入”过程与 HDD 上的直接“仅写入”行为形成了鲜明的对比。

相比之下，SSD 上的随机更新，就不会引起读取和修改步骤（即仅仅“写入”），因此速度更快。

使用 SSD，以上相同的应用程序，可以通过随机更新或就地更新来达到大约 2 万 QPS；而且随机更新和就地更新的吞吐量大体相似，随机更新其实还稍微好些。如下图所示。

2. 数据结构：区分热、冷数据

几乎所有处理存储的应用程序，磁盘上存储的数据的访问概率均不相同。

用一个需要跟踪活动用户活动的社交网络应用程序来给你举个例子。对于用户数据存储，简单的解决方案，是基于用户属性（例如注册时间）将所有用户压缩在同一位置（例如某个 SSD 上的文件）。以后需要更新热门用户的活动时，SSD 需要在页面级别进行访问（即读取 / 修改 / 写入）。

因此，如果用户的数据大小是小于一页，那么每次读取这个用户的数据，附近的用户数据也将一起被访问。如果应用程序其实并不需要附近用户的数据，那么额外的数据访问，不仅会浪费 I/O 带宽，而且会不必要地磨损 SSD。

为了缓解这种性能问题，在将 SSD 用作存储设备时，应将热数据与冷数据分开。以不同级别或不同方式来进行分隔。例如，把它们存在不同的文件、文件的不同部分或不同的表格里。

3. 数据结构：采用紧凑的数据结构

在 SSD 的世界中，最小的更新单位是页面（4KB），因此，即使是一个字节的更新，也将导致至少 4KB SSD 写入。由于写入放大的效果，实际写入 SSD 的字节可能远大于 4KB。读取操作也是类似，因为 OS 具有预读机制，会预先主动地读入文件数据，以期改善读取文件时的缓存命中率。

所以当将数据保留在 SSD 上时，你最好使用紧凑的数据结构，避免分散的更新，以获得更快的应用程序性能、更有效的存储 I/O 以及节省 SSD 的寿命。

4. I/O 处理：避免长而繁重的持续写入

SSD 通常具有 GC 机制，不断地回收存储块以供以后使用。GC 可以用后台或前台的方式工作。 SSD 控制器通常保持一个空闲块的阈值。每当可用块数下降到阈值以下时，后台 GC 就会启动。由于后台 GC 是异步发生的（即非阻塞），因此它不会影响应用程序的 I/O 延迟。但是，如果块的请求速率超过了 GC 速率，并且后台 GC 无法跟上，则将触发前台 GC。

在前台 GC 期间，必须即时擦除（即阻塞）每个块以供应用程序使用，这时发出写操作的应用程序所经历的写延迟就会受到影响。具体来说，释放块的前台 GC 操作，可能会花费数毫秒以上的时间，从而导致较大的应用程序 I/O 延迟。

因此，你最好避免进行长时间的大量写入操作，这样就可能永远不触发前台 GC。

5. I/O 处理：避免 SSD 存储太满

SSD 磁盘存储的满存程度，会影响写入放大系数和 GC 导致的写入性能。在 GC 期间，需要擦除块以创建空闲块。擦除块前，需要移动并保留有效数据才能获得空闲块。有时为了获得一个空闲块，需要压缩好几个存储块。而每个空闲块的生产需要压缩的块数，取决于磁盘的空间使用率。

假设磁盘满百分比平均为 A％，要释放一个块，则需要压缩 1 /（1-A％）块。显然，SSD 的空间使用率越高，将需要移动更多的块以释放一个块，这将占用更多的资源，并导致更长的 I/O 等待时间。

例如，如果 A=80％，则大约移动五个数据块以释放一个块。当 A=95％时，将移动约 20 个块。

6. 线程：使用多个线程执行小的 I/O

SSD 内部大量使用了并行的设计，这种并行表现在多个层面上。一个 I/O 线程无法充分利用这些并行性，会导致访问时间更长。而使用多个线程，就可以充分利用 SSD 内部的并行性了。

SSD 可以有效地在可用通道之间分配读写操作，从而提供高水平的内部 I/O 并发性。例如，我们使用一个应用程序执行 10KB 写入 I/O（10KB 算是比较小的 IO 大小）。使用一个 I/O 线程，它可以达到 115MB/ 秒。使用两个线程基本上使吞吐量加倍；使用四个线程再次将其加倍；使用八个线程可达到约 500MB/ 秒，如下图所示。

你可能会很自然地问出一个问题：这里的“小”IO，到底有多小？

答案是，只要是不能充分利用内部并行性的任何 I/O 大小，都被视为“小”。例如，SSD 页面大小为 4KB，内部并行度为 16，则阈值应约为 64KB。

7. 线程：使用较少的线程来执行大 I/O

第七个原则同样关于线程，它与第六条原则相对应（但并不矛盾）。

对于大型 I/O，SSD 内部已经充分优化使用了 SSD 的内部并行性，因此，应使用更少的线程（即小于四个）以实现最大的 I/O 吞吐量。从吞吐量的角度来看，用太多线程不会有太大益处。更重要的是，使用太多线程可能导致线程之间的资源竞争，以及诸如 OS 级的预读和回写之类的后台活动。

例如，根据我们的实验，当写入大小为 10MB 时，一个线程可以达到 414MB/ 秒，两个线程可以达到 816MB/ 秒，而四个线程达到 912MB/ 秒，八个线程实际上只有 520MB/ 秒。如下图所示。

总结

这一讲我们讨论了软硬件结合的优化。

与使用 HDD 的应用程序相比，使用 SSD 的应用程序通常具有更好的性能水平。但是，如果不更改应用程序设计，则应用程序是无法获得最佳性能的。因为 SSD 的工作方式不同于 HDD。

为了发挥 SSD 的全部性能潜能，应用程序设计必须对 SSD 友好。

唐代的王维有一首《少年行》，讲几个好哥们一起喝酒：“新丰美酒斗十千，咸阳游侠多少年。相逢意气为君饮，系马高楼垂柳边。” 说的是性情投机的好朋友一起互相帮助，就会互相促进。

硬件和软件也是如此，如果互相对对方友好，互相都受益，总体性能也就更高。

我们的软件系统如果能充分考虑硬件，比如 SSD 的特性，做出的设计就会获得更好的性能和稳定性。基于这一点，我在这一讲里面提出了七个对 SSD 友好的软件设计原则，分为三类：数据结构、I/O 处理和线程使用。你在设计使用 SSD 存储的软件时，可以参考采用。

思考题

你正在开发、维护、使用的系统，有没有使用 SSD 作为存储的？如果有，这个系统有没有考虑到 SSD 的特殊机制和寿命问题？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

30 | 服务器的管理和部署：工业界近几年有哪些发展趋势？

你好，我是庄振运。

说起服务器，你一定不陌生。那你知道 Facebook 的服务器是什么样的吗？要知道，Facebook 同时使用着很多不同的服务器。

在应对需要高速缓存的 Facebook 新闻、广告投放和搜索时，Facebook 使用的是有比较大内存和较强 CPU 的服务器。现在使用的每台服务器都有 256GB 的主内存和两个处理器的 CPU。

而在需要存储大量照片和视频的时候，Facebook 就选择了适用于数据和对象存储的服务器，这种服务器只有很少的内存，但是却有几百 TB 的硬盘存储空间。

今天，我们就从“服务器”入手，进入一个新的专题：容量管理工程。我们一起来看，要如何针对服务器设计、规划和部署的特点，开发出性能优越，能充分利用硬件资源的应用程序和服务。

如何设计一种新的服务器？

就像我前面用来举例的 Facebook 一样，大规模互联网公司的服务器，和我们家里以及办公室用的电脑可不一样，一般不是直接从市场上买的，而是自己设计的。

那么这些服务器是怎么设计出来的呢？其实服务器的设计和其他的硬件设计一样，也需要经过好几个阶段，你可以看一下它们的设计路线图（如下图所示）。

在最终进入大规模批量生产 MP 阶段之前，服务器的设计需要经历四个阶段，也就是：

Pre-EVT（Pre-Engineering Validation Test）：预先工程验证测试

EVT（Engineering Validation Test）：工程验证测试

DVT（Design Validation Test）：设计验证测试

PVT（Production Validation Test）：生产验证测试

这之后才是批量生产 MP 阶段。

每个阶段的目的和任务都有所区别，具体来讲，Pre-EVT 阶段进行的是比较初级的模块测试，且往往是和硬件供应商一起进行的。这一阶段通过基准测试，得出基本的模块性能，为下一步的、更具体的设计提供性能数据。所以，此阶段一般只需要少数几台服务器原型就可以。

第二个阶段是 EVT，这个阶段是服务器开发的初期设计验证，重点在考虑服务器设计的完整度是否有遗漏。这些测试，包括功能和安全规格测试。这时候的服务器是样品，问题可能较多，测试可能会做 N 次。但是这一阶段需要有一个完整的服务器，会运行更加具体的基准测试，并根据测试结果，来调整服务器设计。一般需要几十台服务器来进行测试。

DVT 阶段的测试，需要生产厂商把服务器运送到数据中心。到此时，几乎所有的设计已全部完成，重点是找出可能的设计问题，确保所有的设计都符合规格。此时产品基本定型，可以进行负载测试，是为了发现在生产环境中可能出现的问题。这个阶段的测试可能需要用上百台服务器。

PVT 阶段更加注重生产流程。服务器需要用类似真实的生产线来生产，并且运送到数据中心。这时需要进一步，用更加真实的应用程序来测试。这个阶段一般就要有百台以上甚至千台的服务器了。

MP 阶段，就是批量生产了。到这个阶段，服务器硬件准备就绪，而且需要使用真实生产流程，来进行大规模生产和应用测试。所有设计及生产问题，应该没有任何遗漏及错误，成为正式面市的产品。

服务器设计的机遇在哪？

仅仅知道如何设计一种新服务器还不够，你还需要知道整个业界如今的趋势，才能给出最有性价比的服务器设计。虽然业界会有比如全球 DRAM 短缺这样的短期波动，但是长期来看，也是有迹可循的，比如摩尔定律的放缓，越来越大的 HDD 硬盘，新的 SSD 技术等。

这些变化给我们的服务器设计带来了挑战，但同时也带来了机遇。CPU、内存、硬盘、网络、单处理器等几个方面的变化，不仅仅影响各公司数据中心未来服务器的发展方向，也对公司的软件和服务设计有重大而深远的影响。

CPU 的趋势

从 CPU 的趋势来看，简单来讲，摩尔定律正在失效，业界对 CPU 的性能增强，一般是通过添加更多内核来增大吞吐量。对于英特尔和其他 CPU 厂商（比如 ARM）来说，这意味着，每一代处理器将拥有更多的内核，但运行频率一般要低于当前一代的处理器。之所以有这些趋势，归根结底，是因为用于生产芯片的材料所面临的挑战，以及物理方面的基本限制，例如，光子和电子的特性等。

对软件来说，传统上，我们比较看重针对单线程的性能进行优化。鉴于这一硬件趋势的影响，这样的优化策略需要进行调整。无论处理器的体系结构（x86、ARM 等）如何，CPU 单位价格的性能和每瓦性能的提升都在逐代地显著放缓。

所以，我们的性能优化策略，应该着重服务的水平扩展性，就是通过多线程和多服务器来扩展服务的总体容量。

DRAM 内存的趋势

DRAM 内存的趋势如何呢？在全球市场上，DRAM 内存仍然相对稀缺，价格很高。自 2017 年以来，每 GB 的价格已翻了一番以上。与影响 CPU 一样，相同的基本问题和挑战，也影响着 DRAM 性能和容量的提高。所以，各公司的服务器设计，都开始转向使用尽量少的 DRAM 内存。

一个好消息是，新型技术比如 NVM（非易失性内存）等也在成熟；这就为许多应用，提供了更便宜的 DRAM 替代品。现在已经有越来越多的服务器设计，开始考虑使用 NVM。

如果你的互联网服务和程序使用内存较多，并且 SSD 这样的快速存储还是不能满足服务的要求，除了尽量减少内存使用外，建议你考虑采用 NVM。

硬盘的趋势

硬盘方面，硬盘的存储容量继续增加，但增速开始降低。这种趋势，是因为硬盘行业面临的材料挑战，以及物理方面的基本限制（例如控制磁场）。

虽然存储容量变大，但是硬盘提供的 IO 性能，比如每秒随机访问 IOPS 的性能并没有增加。为了利用这些更大容量的驱动器，我们最好把硬件和软件设计一体化。比如，你可以把热数据缓存在闪存或内存中，并将相对较冷的数据存储在旋转硬盘上。

网络的趋势

与服务器功耗相比，数据中心的网络流量增长得更快。具体来说，与计算和存储有关的网络功耗不断增长，所以需要使用更快、更多的交换机间的网络链接。

另一方面，新的技术比如硅光子技术，进一步降低了成本，也引发了新一轮的创新和数据中心的网络升级。比如有的数据中心，已经开始使用 400G、800G 甚至 1.6T 的高速网络。

但是你要记住，不管网络技术如何发展，还是需要尽量地减少跨数据中心和跨机架的网络流量，也就是尽量让网络在本地消化掉。这里的“本地”可以是服务器本身、本机架、本数据大厅、本数据中心等等。为什么呢？还记得我以前说过的“带宽超订”吗（参考第 18 讲），因为越往外走，网络的带宽总是越小。

如何才能尽量让网络流量本地消化呢？这就需要你考察公司内部各种服务之间的数据交互，尽量让有大量数据交换的服务在一起部署（比如部署在同一个机架内）。

单处理器的趋势

回到服务器的趋势上，有一个有趣的趋势就是：未来是单处理器的天下。

以前服务器采用多个处理器，是因为每个处理器上的内核数目有限。所以，如果我们希望一台服务器有更多的计算能力，只能采用更多的处理器。

但是现在这种情况也正在改变，我们正进入高度集成的多核 CPU 时代，也就是一个处理器上面，有几十个内核越来越平常，一个处理器就已经有足够强大的计算能力。比如，随着每一代 x86 处理器的发布，都会增加更多的内核和功能，性能大大提高。

同时，服务器的内存密度也在增加。现在的单处理器服务器，能够置入的内存大小已经超过了以前服务器支持的内存密度。在很多生产环境中，内存容量和内存带宽，才是主要的性能瓶颈，而不是 CPU。因此，在双处理器环境中，CPU 使用率往往很低。

综合以上因素，采用单个处理器的服务器，可以降低服务器硬件成本和软件许可证的成本，让各种硬件资源得到更加充分的使用。具体来说，我们在开发大型服务和程序的时候，可以尽量采用模块化的设计，比如分解成几个可以在单个处理器上运行的微服务。

总结

我们这一讲，介绍了服务器设计的不同阶段，讨论了工业界这几年的发展趋势，包括 CPU、内存、网络、磁盘等不同的服务器资源类型。

这些趋势，对于我们去把握下一代服务器硬件会有帮助。我们开发的互联网服务，总归是要运行在这些服务器上面，了解了这些趋势，我们才能开发出性能优越，能充分利用硬件资源的应用程序和服务。

清朝的赵翼有一首诗说：“李杜诗篇万口传，至今已觉不新鲜。江山代有才人出，各领风骚数百年。”其实服务器的发展，又何尝不是如此呢？服务器的更新换代速度，虽然可能没有软件那么快，但是也是几乎每年都有很大变化的，每种服务器，也是“各领风骚一两年”。

思考题

你们公司的服务器有几种？是不是也是按照资源的大小，比如内存和存储的大小来划分的呢？

你如果在公司已经工作几年了，有没有感受到硬件更新换代的特点？如果你在开发程序和互联网服务，想一想怎么设计你的程序和服务才能充分利用这些特点呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

31 | 规划部署数据中心要考虑哪些重要因素？

你好，我是庄振运。

上一讲我们讲了服务器的设计和部署，今天我们就来聊聊一个轻松的话题，一起来看看数据中心的秘密。

你们公司肯定有很多服务器。根据公司和服务的规模，可能有几十台甚至几万台服务器，大些的公司甚至会达到几百万台服务器。那么这么多的服务器都放在哪里呢？它们的家，就是数据中心。

你平时可能和服务器打交道比较多，离“数据中心”就比较遥远。但是我可以肯定地说，数据中心的知识，与我们每个 IT 从业人员（尤其是对于运维和性能工程的人员）是非常相关的。

数据中心长什么样？

要讲数据中心，你可能会问数据中心到底长得什么样子？

其实许多科幻片里都会有类似数据中心的场景出现。一个数据中心往往有好几个大楼，每个大楼建筑物内部，都有着很好的空调和通风设施。大楼内一般分成几个数据大厅（Data Hall），每个大厅都有一排排的机架，中间留出足够的通道，方便数据中心的技术人员进行维护。

这是机架的背面。你可以看到，机架的布线必须非常整齐，太乱的话，日常中是很难进行维护的。

数据中心的规划和部署

对一个有全球用户的大互联网公司而言，它的容量通常也需要部署在全球范围内。运行公司各种服务的服务器，就放置在全球的数据中心里面。数据中心的建造成本很高，周期也较长，也有足够的复杂度，所以中小公司，往往会租赁别人的数据中心的空间，来放置自己的服务器，甚至是直接租赁服务器。

但对于大公司，比如 Google、Facebook、Amazon、Microsoft、阿里巴巴、腾讯等，它们规模很大，大到不能靠租赁来运营。而且因为规模大，自己建造数据中心，从经济上看更加划算。比如亚马逊，就自己建造了很多数据中心。这些数据中心分布在全球各地，如下图所示。

之所以要在全球范围内建造数据中心，主要是为了性能，而不是为了节省成本。因为一个服务如果有全球的用户，这些用户就都需要和公司提供的服务快速交互，比如上传照片，播放视频等等。对于在全球范围内运行服务的公司而言，“数据中心离用户距离近”就是唯一的选择。

那么公司需要建多少数据中心呢？这个数量问题是容易解决的，就是根据公司的规模和实际的需求，并且适当的做一些预测和远景规划，而这就是我们下一讲会专门讨论的容量规划和预测。

数据中心要建在哪里呢？表面上看，这个问题也容易回答。前面说过，全球建造数据中心的初衷，就是让它们靠近客户，那当然是根据客户的地点来建造数据中心了，哪里有客户就把数据中心建在哪里。

这样的回答，道理上没错，只是考虑得不够全面。数据中心的选址还需要考虑很多因素，比如电力供应的稳定性、自然灾害发生情况、社会稳定性、所在国法律、人力资源、容量供应、建造成本等等。

这些因素都很容易理解，不过有意思的是，“所在国法律”是其中非常重要的一个因素。

一个公司总会存储各种用户数据，而公司是需要保护用户隐私的。但是很多国家的法律要求，建造在本国的数据中心，必须允许本国政府访问这些用户数据。这就与公司应尽的职责构成了冲突。

我们有时候开玩笑说，放眼全球，还真找不到几个国家，能够在该国不用担心警察会突然破门而入，用枪指着头，强迫数据中心员工交出客户的数据。如果考虑诸多这些因素，地球虽大，却也难找到合适的地方建造数据中心。

数据中心建造还有一个特点，就是建造周期很长，从选址、规划，一直到建造完成，最少也需要好几年。所以，为了让一个数据中心能够长时间有效使用，建造一般也会刻意地分期完成。比如假设一个数据中心，最终会建造 6 个大楼，公司通常会分成 3 个阶段，一次建造两个大楼。

数据中心内服务器的生命周期

我们上一讲讨论了服务器的设计。一种服务器设计完成后，公司就可以部署了，你也需要对这个部署过程有个了解。对于一台服务器，它的生命周期经过 4 个阶段，包括购买和运送、按服务分配、运行管理、最终退休。

购买和运送

公司给了预算并且确定数据中心有了放置的空间和计划，就可以订购服务器了。服务器一般都是按照机架的单位批量购买。之所以要提前做好购买计划，是因为从购买到运送，一般需要几个月的时间。

服务分配

服务器放置在预定义的机架位置后，需要给它们通电。通电后，机架会自动在资产跟踪系统中注册。然后预配操作将安装操作系统以及许多其他软件。新服务器安装完成后，一般是放到备用池等待分配。服务所有者会提出申请服务器的要求，然后负责分配的团队，按照要求将容量分配给他们。

运行管理

在服务器的整个生命周期中，服务器和机架可能都需要进行维护。维护可能需要置换有故障的磁盘、更换坏的主板、重新启动服务器、重新安装 / 升级 / 修补操作系统、运行修复软件、诊断软件等。

我们通常的服务器设计，实际上已经考虑到了很多可能的维护工作。例如，既然换出磁盘是常见的修复任务，那么服务器设计可以让更换磁盘非常容易，无需任何工具比如螺丝刀等。

光荣退休

机架和服务器在数据中心的使用寿命是多久呢？通常约为 3-4 年。之后，我们需要让它光荣退休，将其从数据中心中移除，擦除所有数据，切碎磁盘，并遵循所有硬件的报废流程。

为什么需要让服务器及时退休？因为当超过使用寿命时，它们的组件就容易发生故障。在处理维修单和更换这些组件方面，会给数据中心的技术人员带来负担。同样，各个组件的保修期也可能快过期，甚至可能因为技术的发展，置换部件已经很难买到了。更重要的是，新的服务器替换旧的，也可以提高性能和效率，比如每一代的 CPU 都会比上一代更加强大。

Facebook 数据中心的网络部署

讲完了数据中心的架构和服务器的生命周期，我们再看看数据中心的网络部署。

数据中心内部的服务器之间，以及用户和服务器之间有大量的数据交换，这些对数据中心的网络部署也提出了很高的要求。为了能够更好地扩展，现代数据中心的网络设置也在不断地演化。下面我就用 Facebook 来举例，看看现代数据中心的情况。

Facebook 的生产网络本身，就是一个大型分布式系统，包括边缘网络、骨干网和数据中心内部网络。Facebook 的网络基础架构也在不断扩展。从 Facebook 到 Internet 的流量，我们称其为“机器到用户”的流量，非常庞大，并且还在不断增加。但是，这种流量，相对于数据中心内部发生的“机器到机器”流量，就只是冰山一角了，后者是前者的百倍以上。而且这种流量的增长速度，几乎每年都增长一倍。由此，我们也可以看出数据中心内部网络的重要性。

我们公司以前的数据中心网络，是使用集群 Cluster 构建的。集群是一个大型部署单元，涉及数百个服务器机柜，这些机柜的顶部（TOR）交换机，聚集在一组大型的交换机上。但是这种以集群为中心的体系结构有很大的局限性。

所以我们的新一代数据中心网络设计，就不是基于集群的，也就是说，不是按层次分配的集群系统。我们将网络分解为多个小的相同单元，也就是服务器 Pod，而不是大型集群，并在数据中心的所有 Pod 之间，创建了统一的高性能网络连接。

这里的 Pod，只是我们新架构中的标准“网络单元”。每个 Pod，由一组称为设备交换器的四个设备提供服务，从而可以根据需要进行扩展。当前的多数机架顶部交换机具有 4 个 40G 上行链路，为被连接的服务器提供 160G 的总带宽容量。下图就展示了一个 Pod 和 48 个机架的网络连接。

每个 Pod 的大小都一样，都是 48 个机柜，所以只需要基本的中型交换机就可以支持。对于机柜交换机的每个下行链路端口，我们在 Pod 的交换矩阵交换机上，保留相同数量的上行链路容量，这使我们能够将网络性能扩展到在统计上无阻塞的水平。

为了实现大楼范围的连通性，我们数据中心内部，创建了四个独立的骨干交换机“平面”，每个平面，可以最多扩展 48 个独立设备。

每个 Pod 的各个交换矩阵，都连接到其本地平面内的主干交换机，共同构成一个模块化的网络拓扑，能够容纳成千上万个连接 10G 的服务器。如下图所示：

对于外部连接，我们用光纤网络配备了数量灵活的边缘 Pod。每个边缘 Pod，能够为骨干网和数据中心站点上的后端提供很多 Tbps 的带宽，并且可扩展到 100G 和更高的端口速度。

这种高度模块化的设计，使我们能够在一个简单统一的框架内，快速扩展任何维度的容量。

当我们需要更多计算能力时，就简单地添加服务器 Pod。当我们需要更多的内部网络容量时，就可以在所有平面上添加骨干交换机。当我们需要更多的连接时，我们可以在现有边缘交换机上添加边缘 Pod，或扩展上行链路。

总结

我们今天讨论了数据中心这个重要的容量载体，它的内部结构、网络设置、服务器的生命周期，以及规划数据中心的一些考虑因素。

数据中心可以说是服务器的“家”，也是我们的程序最终部署的地方。唐代诗人王建说：“今夜月明人尽望，不知秋思落谁家。”我们开发程序和部署服务，最好要了解数据中心的知识和架构，因为各种互联网服务，总归是需要数据中心的服务器和网络来支撑的。了解数据中心的配置，对我们服务的开发和部署是很有用的。

对一些大规模的服务，数据中心的网络或者服务器资源，可能会成为性能和业务发展的瓶颈，所以我们也需要不断优化，并提前规划数据中心。尤其是现在的互联网服务，往往有很大的数据量，所以数据中心网络的扩展性尤其重要。

思考题

你们公司的服务器一定也在数据中心里，这些数据中心的地理位置在哪里？地理因素对你的互联网服务的性能比如端到端延迟有什么影响？

你们公司用的数据中心是自己建造的还是租赁的？公司对数据中心的运营成本有什么要求和策略？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

32 | 服务的容量规划：怎样才能做到有备无患？

你好，我是庄振运。

今天我们来讨论一下在公司运营方面很重要的容量规划。容量规划，就是根据互联网服务的需求和公司发展目标，而决定容量的供应能力的过程。

光说概念你可能不太明白，不过你可以这么理解，容量规划是为了回答一系列和公司业务运营有关的重要问题而产生的：

单台服务器的最大处理能力是多少？

未来半年公司还会有多少可用容量？

春节前如果要进行一次大促，当前的容量能不能支撑？

……

今天我先讲容量规划的目标和基本过程，然后再一个一个地讲容量规划的六大挑战。

我们前面也大概提到过，容量规划的目的有两方面。一方面是为了保证公司的正常运营和业务增长，及时地提供足够的容量，来满足未来所需。另一方面，也是希望空闲的容量越少越好，因为每一台空闲的服务器，都消耗公司的运营成本。

现代的互联网服务规模的扩展，主要是用“水平扩展”而不是“垂直扩展”（通过增加服务器的数量和网络的带宽来提供更多的容量，而不是通过升级服务器）。相对于垂直扩展，水平扩展的优点显而易见，就是成本相对较低、调整容易、扩展性好。

怎么做容量规划？

整个容量规划的过程分为几部分：首先是容量测试来决定单位容量的负载能力；同时确定公司的业务增长需求，并且获取公司的运营预算。然后集成其他的考虑因素（包括时间、地域、灾难恢复等），做出合理的规划和决策。最后根据决策结果，进行容量订购。

为了方便把握，我把这几个因素模块整合在一起，如下图所示，我们一个模块一个模块地讲。

容量测试决定单位容量的能力

容量测试可以决定单位容量的能力。容量规划的基础，就是确定单位容量，比如一台服务器系统的处理能力。而进行容量测试，一般是让服务器处于最大负载状态，或某项性能指标达到所能接受的最大阈值下，对请求的最大处理能力。

容量测试是性能测试里的一种测试方法，它的目的就是测量系统的最大容量，为系统扩容、性能优化提供参考，从而达到节省成本投入，提高资源利用率的目的。

容量测试大致分为两种：线上测试和线下测试。我们在第 25 讲专门讲了线上测试，这里快速地复习一下。线上测试相对比较准确，因为是用真实的生产流量负载。线下测试，则比较容易进行。但是你要特别注意测试过程和结果的代表性，就是它能否真实客观地反映生产环境中的数据。尤其是测试环境的配置和驱动数据，一定要和线上的生产环境尽量保持一致。

预测未来需要的容量需求

要做好容量规划，除了单位容量的处理能力，另外一个必须要知道的输入，是服务和业务的需求。通俗点说，就是期望达到的用户数和数据流量大小。比如一个前端服务，要决定明年的容量需求，就需要确定明年的预期用户数目。

对于未来的容量需求的预测，主要是参考历史的相关数据，然后建立合适的模型。比如我们在第 26 讲提到的针对时间序列的 ARIMA 模型，就是一个不错的模型。如果针对的业务已经有足够长的历史，那么过去的数据就很值得参考。

反之，如果一个业务还没有上线，或者上线时间很短，那么历史数据就不是很可靠。如果你面对的是这种情况，有两种解决方法：一是可以类比其他相似的服务，不管是公司内部的还是业界的，都可以拿来参考。二是需要借助一些主观的推测。当然这种主观推测也不能凭空乱猜，而是要有理有据。

容量规划时，公司的业务运营预算也要考虑进去。一个正常运行的公司，运营成本总是有限的（每个业务都有预算定额）。如果对一个业务预期的容量需求，超过已经设定的预算定额，那么，你要么是把容量需求降低到预算定额，要么就得向公司申请增加预算。

考虑各种限制因素和挑战

另外还有一点要注意的是，未来容量的需求预测总会和实际情况有差距，也就是说，总是有不确定的因素。

所以，我们一般会根据估计的信心指数，对预测结果做几种不同的预测。比如 90% 的信心和 50% 的信心。这里信心指数越大，容量数值也越大，就是说越有把握满足需求。举个例子，比如我们要估计明年一个服务的容量需求，可能得出结论：

如果只需要保证 50% 的信心，1000 台服务器就够了；

要保证 90% 的信心，则需要 1200 台服务器。

但是，无论我们多么努力地去预测，总有些因素没有考虑到，从而导致实际情况和预测不吻合。所以，我们一般都要对容量的需求预测加上一个冗余 Buffer，我个人的经验是 10% 的 Buffer。比如我们的模型预计需要 1000 台服务器，那就加上 100 台的 Buffer，也就是需要 1100 台服务器。

及时和实时地调整

也正因为容量的规划和使用过程中，有很多的不确定因素，随着时间的推移，这些不确定因素慢慢就会变成确定的因素。比如，在一个服务还没有上线的时候，做容量规划非常困难，因为不能准确地预测用户数量。但是随着服务的上线，开始积累用户量，对未来用户数量的预测就会变得越来越容易。

所以，容量的预测和规划，也是一个持续和不断迭代的过程。开始的时候靠模型，靠历史数据和一些直觉来做大体预测。然后，定期地和实际的数据做对比，从而调整和纠正以前的预测。

容量规划的挑战在哪？

容量规划和预测时，除了前面讲的步骤和因素外，还有其他几个限制因素和挑战值得考虑。我总结了六个这样的挑战，包括地域的限制、服务器种类的区分、时间和季节性等等。

容量规划不仅仅是钱的问题

第一个挑战就是，容量规划不仅仅是钱的问题。你可能听说过：“能用钱解决的问题，都不是问题。”

但容量的供应问题，很多时候还真不是砸钱就能解决。

为什么有时候钱不能解决容量问题呢？如果公司的运营规模很大的话，需要的容量供应也就很大。在很多情况下，市场上能够找到的数据中心很有限，所以即使公司愿意投入很多资金，也不一定就能在需要的地点和需要的时间租赁到足够的容量。

有的公司自己建造数据中心，那就面临更多新的的问题和挑战，尤其是时间方面。建立数据中心的交货时间至少是两、三年。数据中心建造和扩建，需要寻找可用的建筑商和建筑工人，但是找到足够的合格建筑商和工人并非易事。因此，数据中心的建设并不像把钱扔在问题上那么简单，需要提前很久就规划和考虑，要未雨绸缪。

数据中心的地域因素

第二个挑战是地域因素。当公司的业务需要部署服务器在多个数据中心时，每个数据中心都需要做相应的容量规划。也就是说，仅仅只有预测和规划一个总体的容量是不够的，还需要具体到每个数据中心的内部。

数据中心往往分布在不同的地域，它们内部的服务器，并不能在数据中心之间随意置换。为什么呢？因为我们的业务也要求服务器尽量地离客户近些，从而降低网络延迟，提高带宽和可靠性。像 Facebook、Google、腾讯、阿里这样的公司尤其是如此，它们往往在全球有很多数据中心，而且用户的规模很大，如果容量规划做不到数据中心这个层次，是肯定不行的。

服务器种类的因素

下一个挑战是服务器种类的挑战。公司往往会有不同种类的服务器，以便针对特定类型的工作负载进行量身定制。例如，有专门针对内存密集型工作负载的大内存服务器。当有多种服务器可选的时候，容量规划就需要选择最合适、最经济的服务器类型，这样可以降低成本并改善服务质量。

对于一种具体的服务器而言，它的性能也是随着时间的推进不断改善的。比如一种计算密集型的服务器，虽然基本的机架设计不会改变，但是服务器使用的 CPU 还是会一直更新换代的，一般每年都会有新的 CPU 型号。

所以，在做容量规划时，也要把这个因素考虑在内。比如要支撑一个服务，如果用现在的 CPU 型号预测，明年需要一千台服务器。可是同时明年的 CPU 会升级，性能提高了 10%。那么考虑到这点，可能只要 900 台服务器就够了。

时间和季节的因素

容量规划也要充分考虑时间和季节的因素。在第 26 讲里，我们一起看过正常生产环境的负载变化。对大多数互联网服务来讲，在一天内，每天的上午上班时间的负载较大；在一周内，工作日的业务负载比较大。同时，当经历节日时，比如春节、国庆等，也要注意流量的预期变化。

对于面向普通用户的社交网站，比如 Facebook，每当新年、圣诞节时候，有些业务的用户使用量会好几倍的增长。这些业务包括照片和视频上传，朋友分享等。所以我们的容量规划过程会充分考虑到这些时间的业务需求，提前部署足够的容量来迎接这些峰值的服务流量。

不同的灾难恢复场景

还有，为了保证业务的可靠性，灾难恢复（DR，Disaster Recover）场景是必须考虑的。

灾难恢复准备，意味着公司的容量可以容忍某个容量单位的损失。互联网服务其实有很多种不同的 DR 场景，最常见的是数据中心的 DR，也就是说，假如一个数据中心因为天灾人祸的原因完全不能访问，其余的容量如何支撑整个互联网服务的正常运作。

灾难怎样影响服务性能呢？损失的容量单位，会导致其他容量单位的负载增加。负载的增加量，取决于丢失单位在整个容量系统中的比例。具体来说，假设丢失的容量单位，平时承担的负载百分比为 x％，那么在丢失该容量单位之后，所有其他单位的流量将增加[1 /（1-x％）-1] * 100％。

为了帮助你理解，我先举个简单例子。假设一个服务有两个数据中心支持，这两个数据中心大小是同样的，都分别承担 50% 的服务流量。那么，如果一个数据中心不能用了，另外的数据中心就需要承担 2 倍的服务流量，也就是 1/（1-50%）=2。

再举个稍微复杂的例子。考虑以下情形，其中 A、B、C 的三个容量单位分别占流量的 40％、40％和 20％。如果单位 A 丢失，那么单位 B 和 C 的流量将增加 1 /（1-0.4）-1 = 67％。如果 C 单位丢失，则 A 和 B 单位的流量将增加 1 /（1-0.2）-1 = 25％。

不同服务之间的互相影响

最后，互联网公司的业务，往往会分解成很多的服务。面对外部客户的是前端服务；还有很多后端和内部的服务来支撑前端服务。所以，如果前端服务的负载有变化，后端服务也会受到影响。要预测后端服务的容量需求，也必须充分考虑前端服务，以及其他上下游服务的影响。

一般来讲，每层服务调用都会有一定的负载均衡机制，在规划每层服务的容量需求，以及每层服务的不同容量单元的容量需求时，就要了解这些负载均衡机制，从而确定相关因子。比如，如果前端服务负载增加 100％，则下游服务预计会增加多少负载？如果某个下游服务的负载增加也是 100％，则相关因子为 1。

每个服务的总体负载确定后，还要考虑具体到这个服务在不同数据中心的分配，也就是需要确定每个数据中心分别分配多少。

总结

我们今天讨论了容量规划的过程和需要注意的地方。容量规划这个工作非常重要，正如清朝人陈澹然讲的：“不谋万世者，不足谋一时；不谋全局者，不足谋一域”。如果规划做不好，公司业务一定会受到严重的影响。要不就是没有容量可用，要不就是容量太多造成浪费。

容量规划的工作并不好做，你从我说的几大挑战就看得出。所以需要对公司的业务特点和服务架构非常熟悉，并不断及时地调整，才能尽可能地达到预定的目标。

思考题

你们公司或者你的部门的业务，需要什么样的容量来支持呢？比如何种服务器？

对于容量规划，你们部门对未来半年甚至一年的容量要求是多少？是哪个部门帮你们做容量规划，他们是怎样确保容量规划的准确性呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

33 | 服务效率提升：如何降低公司运营成本？

你好，我是庄振运。

我们都知道，支持大量用户的互联网公司，通常会部署相当规模的系统容量来运行各种服务。

如果你想要有效地运行业务，就应使业务的容量需求和容量供应尽可能地相等。为什么这么说呢？如果容量供应不能满足需求，那么部分业务将​​因容量不足，不能部署或扩展。如果容量供应过多，那么公司基础设施的效率就降低了。

服务效率与业务的容量和公司预算直接相关。因此，提高服务效率和适当地预测容量需求，对于公司的持续成功至关重要。

那么如何才能降低公司的服务容量需求呢？这就需要运行的服务尽量高效，提高服务效率将会帮助降低容量的需求。

今天我就为你介绍八种提升服务效率的途径和相关的生产经验，以及提升执行服务效率的原则，希望对你有所启发。

服务效率提升的途径和类别

当各个服务团队的领导和技术骨干下定决心，要花时间在服务效率的时候，经常提出的问题是：怎么样做才能提高服务效率呢？

要回答这个问题，你就必须先搞清楚一个问题，就是——什么类型的工作才是提升服务效率的工作？

我多年来帮助很多不同的服务提升了服务效率；在这个过程中我逐渐意识到，一个公司的服务千差万别，可以提升效率的方式也是多种多样。但核心的一点是：任何能够让这个服务降低容量需求的工作，都是服务效率提升的工作。

我把各种各样的服务效率提升工作分为八类：软件效率（Software Efficiency）、服务架构效率（Service Architecture Efficiency）、服务部署效率（Service Deployment Efficiency）、跨平台效率（Cross-platform Efficiency）、硬件效率（Hardware Efficiency）、容量组织效率（Capacity Orgnization Efficiency）、容量资源回收（Capacity Resource Reclamation）、用户效率（User Efficiency）等。

这八种效率大体上分为两组：软件服务和非软件容量。

对每一种服务效率，我先讲基本概念，然后再用具体例子来诠释，希望可以给你些启发。

1. 软件效率

服务的软件程序实现千差万别，但不管哪种实现，它总是可以优化的，比如通过采用更好的数据结构和语言库等等。这方面的效率提升，都可以归类为软件效率。

软件方面的效率提升比较直白，就是把程序的性能提高。比如减少 CPU 和内存的使用等等；这方面我们这个专栏前面讲了很多，多数内容都可以归到这方面。

2. 服务架构效率

对一种服务而言，如果能够改进服务的实现架构，比如重新整合了内部的微服务，从而变得更高效，那么就是服务架构的效率提升。

服务架构的效率又可以分为两种：一种是单独的一个服务，通过进行服务设计的优化来提高效率；另外一种就是通过合理借助其他服务来优化。

比如一个数据库服务，如果有很多的查询请求，那么采用另外一个缓存服务，就或许可以大幅度地降低数据库服务的资源消耗。如果两个服务（数据库和缓存）的资源使用总和，还是小于仅仅使用数据库（而没有缓存服务）的资源使用，那么这两种服务的结合就是一种更加高效的服务架构。

3. 服务部署效率

一个服务的软件程序总是要部署到硬件容量上。生产实践中，如何部署软件也很重要，也会对服务效率产生影响。比如采用什么操作系统，进行什么样的系统和软件配置，要不要采用 NUMA 绑定等。这些方面的优化都可以算是服务部署方面的效率提升。

还记得我们在第 22 讲讲过的“使用内存大页面，来装载程序的热文本区域”吗？这其实就是一项服务部署的优化，就是通过提高系统和程序 iTLB 命中率，从而达到更高的服务效率。

4. 跨平台效率

如果一个公司（尤其是后端）同时提供几种服务平台，提供的功能有重合，而且服务效率不同，那么用户可以迁移到效率更高的服务上去。这种迁移就是跨平台效率提升。

一个互联网公司，随着时间的推移和规模的增长，内部往往有很多种服务提供重叠的功能。一个明显的例子是数据存储，经常有很多服务都可以提供数据存储的功能。虽然每种存储服务提供的功能并不完全一样，但对于一个要使用存储服务的用户来说，经常同时会有好几个选择。

这种情况下，这个需要存储的用户，就可以考虑每种存储服务的服务效率；在满足所需功能的前提下，选择一种高效的服务，会帮助公司降低成本。

对于一个已经在使用某种服务的用户而言，可以重新考虑各种可用服务的效率；如果另外一种服务更加高效，就可以考虑迁移过去。这种操作就是跨平台的迁移，其结果也是能够提升公司的服务效率的。

我最近几年做过很多这方面的优化。比如曾经把一个用户的数据从一个低效的存储服务，整体转移到另外一个高效的存储服务，从而大幅度地降低了公司的容量和运营成本，节省了大约几千万美元。重要的是，用户的端到端性能并没有收到影响。

5. 硬件效率

一个服务会使用各种容量资源（比如 CPU）。所以，根据资源使用最大瓶颈的不同，采用不同种的服务器硬件，也会导致不同的服务效率。比如，假设一个服务的存储是最大瓶颈时，那么采用相对较大存储的服务器，结果就会比较高效。

我就提升过一个存储服务的硬件效率。该服务受到存储空间的限制，并且近年来增长迅速。我评估后得出的结论是，如果这个服务继续快速地增长，公司很快就没有足够的容量来提供给它。

所以我们决定，通过提升服务效率来减少服务的容量需求。在检查了所有效率提升的方式后，我决定首先提升硬件效率。 具体来说，我们采用了一种新型服务器，这种服务器相对旧的服务器而言，有较大存储容量，并逐步部署。

下图分别显示了服务的容量规模（也就是服务器的数目），和已部署的总存储容量。

在 20 天的时间内，我们将服务迁移到新的硬件类型，服务器的数目减少了 5％，而存储容量增加了 11％。值得一提的是，新旧服务器的单位服务器成本差别不大，所以整个服务的效率得到了很好的提升。

6. 容量组织效率

一个互联网服务在使用数据中心提供的容量时候，一般都是用某种方式把容量（比如服务器）组织在一起，形成一个容量单位。比如把服务器分组、组成集群等。这种容量组织的方式，也可以根据服务的特点，进行调整优化，从而变得更高效，这就是容量组织效率。

一个规模很大的服务可能会需要很多容量。我曾经合作的一个后台服务，就使用了几十万台服务器。这么多服务器经常会分成几个容量单元，这样做当然有利有弊，但都是经过各种考虑的决定。有时候服务容量的分割也有历史的原因，比如一开始使用一个容量单元，但是随着服务的规模不断扩大，就建立了一个又一个的容量单元。

分成几个容量单元的一个好处，是降低了容量操作的复杂度。不过也有坏处，其中之一就是资源效率使用率不会特别高，因为不同容量之间的资源不能互补。比如两个容量单元，每个有一千台服务器。一个容量单元是 CPU 吃紧，另外一个容量单元是内存吃紧。如果二者合并成一个容量单元，或许只需要一千五百台服务器就够了。

所以适度地优化容量组织，合并容量单元，就可以降低容量的需求，提高容量的效率。

7. 容量资源回收

一个服务所使用的容量，随着时间的变化，有些容量（比如一些服务器）就会处于空闲状态。如果及时回收，从而进行容量的再利用，也可以提升服务效率。

一个服务从诞生到成熟，会不断地演化；表现在对容量的需求方面，也是不断变化的。可能有一段时期，部署的容量恰好满足服务的需求，容量的效率很高。但是过一段时间，服务的需求和资源使用特征发送了变化，有些容量和相对应的资源使用不再使用。这种情况下就需要进行容量和资源回收。

这方面一个最直白的表现，就是可能有些容量处在空闲状态。举几个例子，假如有些服务器已经不能运行此种服务了，那么就把这些服务器回收，给其他合适的服务用。又假如一个服务对存储资源的使用下降了，那么对应的存储资源就可以减少，分给别的服务用。

8. 用户效率

使用服务的用户，如果采用合理的方式，也可以帮助提升所使用服务的效率。这方面的效率提升就是用户效率。

用户在使用服务时，可以尽量做到高效使用。我们曾经帮助一个存储服务提升效率，通过和客户合作，大幅度降低了客户的存储数据量。具体来说，这个客户以前是将数据对象的本来数据和一大堆元数据，都放到了存储服务上，这导致了相当大的物理数据存储。

我们是怎么提高用户效率的呢？我们首先分析了客户的业务逻辑和数据使用场景，意识到客户存储的目的，是判断数据对象是否已经更改。我们先将存储的数据分为两部分，然后分别存储对象的哈希（例如 MD5），而不是对象本身。另外，我们把数据的 TTL（生存时间）缩短，并在对象哈希中定期强制清除陈旧数据。

这样的优化工作取得了很好的效果，节省了 93％的存储空间。具体来说，把客户数据大小从 2.7PB，降到了 0.2PB，如下图所示。

执行服务效率提升的原则

除了前面说过的八类服务效率提升方法，在提高服务效率的生产实践中，我也获得了许多执行方面的经验。这些经验对于如何在部门之间协调，有效地执行效率提升，完成预定的效率提升目标等方面，有比较好的指导作用，希望能对你有所帮助。

1. 明确的所有权和承诺。

根据公司的规模，公司可能会有许多需要提高效率的服务。对于每个这样的服务，每个效率提升工作都需要有明确的拥有者团队，并且需要得到团队的工作承诺。如果不能明确所有者，或者所有者不能承诺完成的时间，那么最后经常会互相扯皮，互相指责，工作失败。

2. 以效率目标为导向的计划和执行。

对于每个服务及其效率组件，要定期设定要实现的效率目标。例如，针对存储服务的效率目标可以是：“将明年的存储空间利用率从 30％提高到 40％”。但是注意，目标设定的频率和大小，必须适应服务的特征和团队的工作特点。

3. 为每个效率组件部署检测工具。

你需要开发一定的工具，来监视整个服务和每个组件的效率水平，以便快速检测到异常。对于异常的效率降低，这些工具必须有自动触发或警报功能。有效的工具，能帮助你及时发现问题，并立即采取行动。

4. 紧密的跨团队协作。

鉴于当今业务和服务的复杂性，许多服务效率的提高，都需要多个团队共同合作。例如，某个服务可能依赖其他服务，并且该服务的效率提高，也可能需要所依赖服务团队的协作。

5. 公司范围内的协调。

尽管每个主要的独立服务，都可以通过采用我们提出的框架来独立工作，但也需要在公司范围内进行协调，因为公司需要保证总体业务的需求。

总结

互联网公司业务复杂，规模庞大，需要有大量的基础设施和容量去支撑。我们这一讲讨论了如何提升服务效率，降低容量需求，从而节省公司成本。

通常一个互联网服务的规模是不断增长的。开始的时候，或许你不在乎它的服务效率和系统容量，但是很快你就需要重视了。所以，你最好未雨绸缪，把相关工作定义好。正如唐朝诗人杜荀鹤的一首《小松》，有两句说：“时人不识凌云木，直待凌云始道高。”如果公司业务发展快，它的服务规模增长也很快，那么服务效率提升的工作就会越发重要。

我通过多年的容量规划和服务效率提升工作，积累了大量的服务效率优化的知识，今天的分享，希望能帮你开拓思路，提供经验。

思考题

提升一个互联网服务的效率有很多种方式，其中一个是服务软件的效率。我们前面几讲提到了很多性能优化的工作。你能举出几个可以归类于软件效率提升的例子吗？ 提示一下，优化代码算不算是提升服务软件的效率？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

34 | 服务需求控制管理：每种需求都是必需的吗？

你好，我是庄振运。

上一讲，我们探讨了如何通过提高互联网服务的效率，降低对公司服务容量的要求。今天我们讨论另一个有效手段——互联网服务的内部需求控制管理。

互联网公司内部，往往有很多后端服务，比如 Key-Value，也就是键值数据库服务。公司内部对这些后端服务，会有很多使用的需求。需求自然有合理的，也有不是很合理的。我们要做的，就是确保那些合理的需求，能够使用这些后端服务，并且将那些不合理的需求，尽量挡回去，或者，让提出需求的内部客户提高容量使用效率。

你不要小看了互联网服务的需求管理，它对保证容量供给和服务质量很重要。因为公司给一个服务的容量总是有限的，如果不能控制需求，那么有限的容量很快就会被用光，从而影响公司的正常运行。

今天我先讲讲服务需求控制管理的总体目标，然后分享一整套大规模需求控制管理的方法给你。这套方法是我在多年实践中总结出来的，实践证明它是行之有效的。

为什么要做服务需求控制？

想做好大规模需求控制的管理，你就需要先宏观地弄清楚服务需求控制的目标。

一个公司要想成功、有效地运行互联网业务，它就应该尽量让各个内部服务的容量需求和供应尽可能接近。

如上图所示，左边是公司业务增长，导致容量需求越来越大；右边是公司预算成本有限，容量供应有限。

我们总是希望这两者能够匹配，越接近越好。如果容量供应不能满足服务需求，那么会导致部分业务的发展受到阻碍。如果容量供应过量，就会有闲置容量，造成浪费，从而降低公司基础架构的效率。

要降低容量需求有两种方法，一种就是第 33 讲我们谈过的提高服务效率，另外一种就是这一讲要说的控制需求。这二者最好互相配合，共同努力。

需求控制管理难在哪里？

现在我们就来看看需求控制管理要怎么做。不过，要理解这套方法为什么是这样的，又为什么可以说它是“行之有效”的，你需要先了解做需求控制有哪些挑战。我总结一下，主要有下面几个方面的挑战：

一是很多公司，尤其是大公司，服务的规模会越来越大。这个规模表现在很多方面，包括服务数量很多，使用服务的客户也很多。注意这里的客户不仅仅是外部客户，更是内部客户。这就要求我们的工作不能仅靠一个人或者一个团队，必须是分布式的。

二是服务的内部客户的容量使用情况很不一样。有的客户对容量使用量很小，增长也不快；有的使用量就很大，或者飞速增长。这就要求我们，在宏观上需要对客户的使用设置一定的配额，否则一个服务的容量，一旦被某些客户用光，其他客户的性能就会非常差。

三是内部客户的资源使用可能不稳定。客户使用的不稳定，会导致特别大的峰值出现，对服务的稳定性造成影响。所以，我们必须在微观上，实时监测客户的资源使用情况，一旦发现超出期望的效率衰退，就需要及时通知客户，让他们尽快解决。

四是停止服务的代价高。一个内部客户一旦开始使用一个服务，就很难让人家离开。所以必须严把入口关，不要随意地让服务上马。

五是资源使用效率的提升，必须要经常与用户沟通，和用户一起不断地提高使用效率，同时也需要开发一些工具来帮助用户。

需求控制框架如何构成？

我把整个需求控制的框架按照逻辑，分成了五大模块。如下图所示，我们一个一个地来展开阐述。

服务入口控制：需求审核

第一个模块是服务入口控制。一个服务的需求在开始使用服务之前，必须进行上马前的审核。这个审核，需要统筹考虑需求优先级、和什么产品相关、对公司业务的影响、需求量的大小和轻重缓急、是不是已经计划好的等很多因素。

每个服务需求的请求，要求以任务的形式进行，审核人员要定期的处理这些任务。为了帮助审核人员做出适当的决定，请求者最好提供足够的信息。一般来说，按照刚刚提到的几个方面来准备就可以，越详细越好。

出于平衡利益的考量，审核人员的组成十分多样，至少要包括服务开发团队、运维团队，以及提供容量的团队。为什么这样设置呢？给你举个例子，服务的开发团队，可能希望服务的内部客户越多越好，因为客户多的话，这个服务就变得越来越重要。服务的运维团队，可能更关心服务和系统的稳定性，不要出现各种性能和可靠性问题。而提供容量的团队为了保证容量供应，可能不希望用户有太多的需求。

经过审核后，每个请求会产生几种不同的结果：或批准，或拒绝，或需要客户提供更多信息，或者要求客户降低请求的大小，又或者推迟这个请求。

之所以把这个审核放在第一位，是因为我在实践中发现，很多的客户请求都是“拍脑袋”想出来的，其实根本不合理，而且请求的大小，往往是狮子大开口。当你审核一个“需要几千台服务器容量”的请求后，你必然要与提出者沟通。而在很多情况下，他们的请求在沟通后降到了只需要几台服务器。

这个“沟通”也不难，你只要对每个请求多问几句，比如这三个“劝退”问题：

你为什么有这么大的需求？

你能讲讲你的理由吗？

能不能推迟一下这个请求？

差不多有一半客户在答完这三个问题后会降低请求的大小，甚至很多会直接取消请求，说：“仔细考虑后，我们其实不需要这个请求。”

分布式的需求控制管理和效率提升

第二个模块是分布式的需求控制管理和效率提升。

为了适应大规模服务的场景，需求控制的方法必须是分布式的。我们一般是把整个公司的各种使用，分成差不多 10 到 15 个产品组，比如按照产品种类来分。这样我们就可以针对这些产品组，分别合作，让每个产品组设置特定人员和团队来和我们合作。

为什么这么做呢？因为只有这样，我们的工作才能合理扩展。

我曾经为一个互联网服务做过三年的需求控制，学到了很多经验和教训。这个服务有几千个内部客户，如果只有我一个人和所有的客户打交道，根本行不通，累死也做不过来。

更重要的是，每个产品组的内部人员，其实才是最适合审核本组需求的人，因为他们对这些请求最清楚。这个请求到底需不需要？请求的大小到底合不合适？由他们来做审核，远远比我做得好。

产品组级别的审核，本质上是一种有效的，可扩展的分布式模型。而且，除了需求控制和审核，还有其他一些类型的工作，也更适合在产品组级别执行。比如下面我们会提到的配额限制等。

采用这个模式，所有的服务需求，都会先确定是哪个产品组，然后将需求任务分配给产品组，让他们进行产品组级别的审核。产品组的审核决定也不是随意决定的，同样要基于几个因素，包括请求的优先级（例如功能的重要性）、增长配额以及产品组内下层团队之间的协作等。

宏观需求增长控制：设置增长配额

第三个模块是在宏观上控制增长，也就是设置增长限额和配额。我通过四个问题来阐述。

1. 在哪个级别设置增长配额？

我刚刚讲了，每个产品组会对自己内部的服务需求进行审核。如果没有产品组级别的配额限制，产品组有可能胡乱批准他们自己的请求，从而滥用这个服务。如果对他们的服务需求，加上配额限制，事情就好办了。所以，增长配额必须设置在产品组的级别。

另外，在产品组下面的级别，也可以设置配额。因为有的产品组很大，内部也有几十甚至几百个团队，所以每个团队也需要设置相关的增长配额。这也有利于产品组内部的协调。这里的要求就是，产品组内部的配额总和，必须不能超过产品组级别的总配额。

2. 怎样设置增长配额的频率？

这个设置可以是一年、半年，或者是一个季度。太频繁会增加工作负担，但是时间太长也有弊端。所以，我们一般都是半年或者一年设置一次。如果是一年，一般是年初设定一年的增长配额。

3. 如何设置增长配额的大小？

每个产品组的实际配额大小也需要考虑几个因素，比如产品的特征、重要性、预期增长、产品发布计划、内部客户端效率等等。对每个产品组的情况都了解后，就可以根据实际情况和业务影响，来平衡各个产品组的配额。当然前提是，总体配额不超过总容量。

4. 如何及时地控制需求的增长，保证不超过配额？

我们需要搭建一些工具和配额使用情况面板。这样就可以每天监控每个产品组的资源使用情况，并将使用情况与增长配额进行比较。如果产品组的资源使用量，连续 7 天比每日的配额快，我们就通知产品组，并创建对应的任务。

任务的优先级别在一开始要设置成最低的。但是如果产品组的使用量，已经超过了年度总增长配额，那么你可以将任务更改为高优先级来解决。如果有连续 3 周以上的实际使用量，都超过了年度总增长配额，并且产品组没有采取任何措施，那么对应的任务就会提升优先级到最高级。

微观增长控制：资源使用的实时监测

第四个模块是在微观上进行增长控制，就是实时地进行资源使用检测。

产品组对一个互联网服务的使用有很多实例。虽然每个实例的情况都不同，需要区别对待，但不会变的核心点是：每个使用服务的实例，都需要进行实时的、微观层面的监测，以确保它的效率不衰退。

一旦你发现实例的效率衰退，就需要及时地通知这个实例的所有者。一般是以任务的形式发给客户，让客户诊断和根因，然后解决。

这个部分，和刚刚讲过的宏观层面的配额限制是互相呼应的。只有在微观增长方面检测和控制得好，才能更容易保证配额不超标。

资源使用效率：不断提升

最后一个模块，就是不断提升资源使用效率。最好的措施，就是我们上一讲说过的提高服务效率。这里的服务效率，主要是客户端的效率，目的是满足内部客户基本要求的情况下，尽可能地降低资源量。

但我们实际中碰到的一个难点是，内部客户通常只对自己的指标比较了解，比如 QPS，或者数据的大小。既然我们的最终目的是节省成本，那么，就需要让客户直观地了解指标和成本的对应关系，比如 1TB 的数据相当于多少钱。

有了指标和成本的对应关系，产品组就能方便地按照任务的重要性来安排工作、调配人员。一个能节省 1 千万元的任务，当然比只能节省 1 百万元的任务更重要。

为此，我们针对每个互联网服务，都提出了一种易于计算和理解的对应指标。比如，对某个存储服务，我们就考虑了数据大小和 QPS 这两个输入指标，然后根据整个服务的成本来做映射。一般来讲，为了反映最新的服务状态，这个指标是需要定期更新的，例如，服务增长、硬件效率、服务效率等。

总结

这一讲我们讨论了容量控制的管理方法，这套方法可以有效地降低服务的容量需求，也就节省了公司的运营成本。实际执行这条管理方法的核心是：在不影响公司正常发展和内部客户合理需求的条件下，尽量降低内部客户对每个互联网服务的需求。

总体来讲，这套方法的核心是分布式和系统性。对于新的内部客户需求，要进行审核以确保需求请求是合理的。而对于现有客户的需求，要在宏观级别，确保需求配额不要超过；在微观层面上，确保资源使用效率不退化，还要不断地寻找效率提升优化的机会。

唐代诗人岑参有首很有名的边塞诗，有两句是描写塞外的寒冷：“将军角弓不得控，都护铁衣冷难着。”说的是边塞都护府的将军，手冻得拉不开弓，几乎不能控制弓箭了，铁甲也冰冷得让人难以穿着，非常辛苦和困难。我们做容量控制管理的工作也着实不容易，需要和各个内部客户产品组打交道，互相理解，为了共同的目标和公司的利益一起工作。

思考题

你的公司里最大的互联网服务是什么？这个服务的容量增长是怎么控制的？

如果这个服务的客户要求实现一个新的功能，所以需要很多服务器，而你负责这个服务的容量管理，你会如何回答客户呢？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

35 | 职业发展：从“锦上添花”到“不可或缺”

你好，我是庄振运。

到今天，我们已经基本讲完了所有的技术方面的内容。通过这些内容，希望你对性能和容量工程这一领域有了更多的认识和收获。

从今天开始，我想和你介绍一下，性能优化和容量工程这个职业在公司里面的定位、工作的形式特点，以及职业的发展前景。

在公司里面的定位

性能优化和容量工程这个工作在公司各种业务中的定位是什么样的呢？

其实性能和容量工程这样的职位，几乎每个互联网公司都有，只是具体的职位名称和工作内容不同。甚至很多非互联网公司，也会有这样的职位，毕竟现代社会每个公司都有 IT 部门，而 IT 的性能对每个公司都是很重要的。

根据我这些年的从业经验和观察结果来看，由于性能优化这个领域覆盖面变广、复杂度变高，性能工程师的重要性越发提高，相关职位也越来越多。

其实这种情况的出现非常合理。从广义上来讲，性能工程和容量工程涵盖了很多方面，比如软件测试、软件部署、互联网服务监测、性能的优化、性能问题的根因及处理、容量的管理和分配、容量效率的提升等等。这些领域的工作，都是公司里面必不可少的。

最近几年，互联网正在发生巨大的变化。

第一个变化，是数据量增大了。你也知道，今天已经是大数据横行的时代，现在要是说自己不懂大数据，估计都不好意思出门。

第二个变化，是互联网服务更加多样化了。层出不穷的新业务（比如共享经济），提出了新的互联网服务需求，这些需求最终都需要部署到公司的基础设施上，需要相应的容量支持。

第三个变化，是提高了对性能质量要求。伴随着这些新业务、新需求，互联网服务越来越需要提高效率，降低延迟，增加可靠性。

所以，如果说前几年性能工程在公司里面的定位，还是“锦上添花”“没有无所谓，有了也挺好”的话，那么现在的定位就是“不可或缺”的、“一个也不能少”的工作之一了。

工作的形式和性质

你或许对“性能工程师”（Performance Engineer）这一工作有些了解，可能已经发现，这一工作的具体形式在不同的公司里面很不一样。有的公司里 QA（Quality assurance ，质量控制或者测试）的工作，就称为性能工程；有的公司里负责整个软件服务架构的工作叫性能工程。

“性能工程”这一角色的具体工作内容，的确是取决于公司业务和职位定位，大体上有这么七种：

软件性能测试是最普遍的，就是我们常说的 QA。虽然它的工作内容相对简单，但对于性能工程领域的新人来说，这其实是一个不错的开始。通过软件测试，你可以学到很多相关的知识。

性能问题诊断这个工作经常和运维工作捆绑在一起。比如系统和平台出了各种性能问题，就需要做诊断和根因分析。

系统性能优化就是在性能测试和性能诊断的基础上，做到提升系统性能和效率。这样的工作经常要和很多模块打交道，比如硬件、软件、网络等等，就需要你拥有比较全面的知识。

网站平台性能监测和优化也是性能工程的一种。现代互联网业务，对性能要求越来越高，比如端到端低延迟、网站高可靠性等。这就需要实时监测各种指标，并且不断地进行提升。

互联网服务架构的优化很容易理解。互联网业务复杂了，公司内部的各种支撑服务和架构，也就需要不断优化。要做好这种工作，需要你对公司的各种服务性质和功能足够了解才行。

同时，不要小看了性能相关的工具开发，多数的性能工作，包括监控、检测、分析等，总是需要一些相关的工具和 UI 界面面板的。这些工具和面板也需要去开发和维护。

最后一种，是混合型，就是综合了一部分前面说的六种工作。我举个例子，比如下面这个微软的性能工程师招聘广告，就是一个典型的混合型的例子。

这个职位需要做性能测试，也需要做性能优化、性能诊断，并且开发相关的工具。

职业的发展前景：越老越吃香

了解过性能优化相关的工作后，你最关心的可能是——这种工作的发展前景如何？

说实话，在互联网行业工作，很多人担心的就是年龄问题。比如你经常看到的程序员招聘要求，说必须多少岁以下（比如 35 岁以下）。在网上你是不是也常常看到各种消息，说某某公司要清退多少岁以上的员工。对于互联网行业，收入方面是风光无限，但年龄是一个不能忍受的痛。

对于这种“年龄的痛”，很多人会说，年龄大了可以转岗，比如做管理。但是管理层毕竟人数更少，对多数人来讲，并不可行。而少数不容易遇到这类问题的互联网岗位中，就包括性能和容量工程。这个工作的特点我们也讨论了，它需要多方面的知识、各角度的技能和实际的经验积累，这需要相当长的时间，不太可能一蹴而就。因此，“年龄”并不会成为一个性能和容量工程师的短板。

我在很多硅谷公司工作过，都是性能和容量工程的岗位。我的很多同事都是年龄比较大的，常有超过 40 甚至 50 岁的。这些大龄同事往往是技术的牛人，是挑大梁的角色。可以不夸张地说，这一行业是“越老越吃香”的。

也许你会怀疑硅谷公司重用大龄的员工是出于怜悯之心，但事实并不是这样。事实上，这些“大龄员工”给公司带来了更大的收益。性能和容量工程师的工作，很大一部分内容，是帮助公司提升业务性能和容量效率，降低运营成本。随着公司业务规模的扩大，他们给公司降低的成本也越来越大，公司也越来越离不开他们。

还记得我开篇词中提到的电机专家斯坦门茨的故事吗？他的一个简单建议就帮助福特公司解决了一个超级大问题。在第一讲里面，我也举了一个同事的真实经历，一个架构师用几行代码优化系统，帮助公司节省了数百万美元。

这些年，我自己也做了很多的性能优化和容量效率提升的工作，我经常开玩笑说，我给公司节省的成本，早已经远远超过了公司付给我的工资，没有 100 倍，几十倍也是有的。

性能和容量工程师的工作：怎么去找？

说了这么多，你可能会问，怎么进入这一职业呢？也就是如何去找这样的工作呢？现在是互联网时代，我们都知道要去招聘网站。无论你是否打算寻找海外工作，我个人都比较建议你用 LinkedIn。你也知道，我曾经在 LinkedIn 工作过四年，不过我可不是在帮 LinkedIn 做广告。虽然我对 LinkedIn 这个老东家印象非常好，很喜欢它的文化，但是我推荐 LinkedIn 是有别的原因。

我推荐 LinkedIn 的原因有三：

LinkedIn 在国外的工作人才市场中拥有垄断地位。不管是员工找工作，还是公司找人，一般都会用这个网站。

LinkedIn 的定位就是针对比较专业和高端的人才市场。你找工作当然是想找一份好的工作，所以用 LinkedIn 比较合适。

LinkedIn 上的猎头多。如果你创建一个账号，放上足够的信息，很快就会不断地收到猎头的信件，希望你去某某公司面试。我经常开玩笑说，这种感觉，不是“你在找工作”，而是“工作在找你”。

当然，你也可以主动用 LinkedIn 去搜寻合适的工作机会，也可以适当的过滤，比如根据工作地点和公司。比如，下面这个截图就是我用“Performance Engineer”搜索出来的工作机会。

还有个建议，建好账号后，你一定要放上足够的和你相关的信息，尤其是专业的信息，否则就和没有建账号一样。

为什么呢？这是因为猎头们都是用关键词和关键领域来搜索员工的。比如你要找性能工程的工作，你就需要把你的相关工作经验和技能列上，因为猎头可能会用如 CPU Profiling、Performance Optimization 等相关的词语去搜索候选人。

总结

我今天讲了性能和容量工程工作的特点、重要性，以及如何找这样的工作。

根据多年国内国外的观察和工作经验，我总体的感觉是，对这一方面的工作，真正了解的人非常少。甚至很多业界人员，都不知道有这样的工作。

我记得唐代诗人杨巨源的佳作《城东早春》中说：“诗家清景在新春，绿柳才黄半未匀。若待上林花似锦，出门俱是看花人。”说的是早春的时候，花开的还很少，这样的清新景色，正是诗人的最爱，适合出门赏花。若是等到晚春之际，虽然到处是花团锦簇，但满城也都是赏花的人，会拥挤不堪。 这一点，我们都有体会，国庆中秋出游过的人，对塞车拥挤都有很深的记忆。

一个行业和领域，如果处于发展期，内行不多，知名度不高，这自然是严峻的挑战，但也是巨大的机遇。对我们每个人而言，一个还没有很多人涉足的领域，恰恰是充满各种机会的沃土。我觉得性能工程和容量效率就是这样的一个领域，还有待我们去持续开拓。

思考题

你们公司里面有没有从事性能和容量工程相关的部门和人员呢？比如性能测试、性能优化、互联网服务效率提升、数据中心容量管理等。他们是在同一个部门还是分成不同的部门？他们的工作内容有没有明显的区分或者重合？

你觉得做这些工作的哪个部门和哪个工程师比较牛？牛在哪里？你可以向他们学习一下吗？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

36 | 如何成为优秀的性能和容量工程师？

你好，我是庄振运。

上一讲，我们探讨了性能工程师的职业特点和工作性质。今天我们就接着讨论如何才能成为优秀的性能工程师。

性能工程师的工作有些特殊，不同于一般程序员、运维以及测试。这一工作需要比较全面的知识、技能和经验。具体来说，需要软硬两方面的技能。

硬技能方面要有扎实而广泛的理论知识和丰富的实践经验。你不仅要了解计算机软件和硬件知识，还得具备性能测试、性能分析和性能优化的经验。

除了硬技能之外，软技能也同样重要。我们这一讲就重点聊一下这方面。我觉得软技能主要包括四个方面：

英文和技术跟踪能力；

多部门协调能力；

项目驱动和领导能力；

人际交往和沟通能力。

最后，我也会分享一些性能工程师的面试要求和经验供你参考。

英文和技术跟踪能力

英文是最流行的国际交流语言。互联网上的绝大多数资料是用英语写的，尤其是和互联网技术相关的资料。

在这个全球信息共享的时代，网上有很多知识宝库，还有大量的优质学习资源，包括文章、视频、问题解答等。全球很多优秀的技术牛人和程序员开发的软件、库、工具、源代码，也大都放在上面，比如我们熟知的 GitHub 平台上面就有很多有用的代码和工程。而英语，就是打开这些知识宝库的钥匙。

如果英文好的话，你可以直接获取第一手的新技术资料，并能快速地跟踪技术发展。在开发软件时遇到什么问题，在 Google 上搜索一下，一般都能找到答案。你碰到的技术问题，大概率是别人早就碰到了的，而且很可能已经有现成的解决方案了。于是，很多情况下你只要复制粘贴就可以解决问题了。

对性能工程而言也是如此。毕竟很多的硬件（比如 CPU）、软件（Linux 和 Windows 操作系统）以及大量的开发库和应用程序都来自国外。要想知道这些最新硬件和软件的性能特性，你就需要及时、直接地从国外的网站上获取。

所以，熟练的英文和相关的技术跟踪能力，对一个从事性能优化的工程人员而言至关重要。如果你能在这方面胜出一筹，那么你成为同行崇拜的技术牛人的可能性就更大。因为你可能知道别人不知道的，或者你可能会比别人知道得更早、更详细、更准确。

多部门协调能力

对于一个正常发展的公司而言，规模一般是会越来越大的，部门也会越来越多。在公司的业务变得复杂之后，几乎任何工作都需要和其他部门打交道，需要对方的配合，所以拥有多部门协调能力非常重要。

做性能工程更是如此。性能优化的工作，总是需要和很多部门打交道。即使是简单的软件测试，也需要和软件开发部门、使用部门以及运维紧密合作。如果是系统优化和容量管理，打交道的部门就更多了。

牵扯的部门多了，就需要你有一定的协调能力。各个部门的利益和需求经常不同，甚至会有冲突和矛盾。这时候就需要我们来统筹考虑，把关系理顺，适当地调和利益，只有这样，才能把性能工程工作往前推进。

多部门协调的能力是你的职位越高就越需要。当你还是一个初级工程师时，你可以只埋头做领导安排的工作，可以不和别人合作。但是当你走到了高级工程师，甚至是架构师这样的高级职位时，你基本不可能靠自己单干完成任务。

项目驱动和领导能力

性能优化和容量管理的工作和其他工作一样，也是通过划分成一个一个的项目来推进执行的。很多项目中的问题，一开始并不是那么显而易见，这就需要我们来发现问题、分析问题的性质、思考问题的解决方法，并把这一问题转化为一个合适的工程项目。

做每个工程项目的时候，你还需要和管理层和合作团队充分沟通。在各方认可之后，一步步地推进执行，定期汇报进展，最后才能够完成预设目标，胜利完成项目。

这一整套的步骤体现的其实就是项目驱动和领导能力。如果你能在这个过程里面起主导作用，包括发现问题、提出方案、说服别人、实现方案并且验证成功，那么你就是真正的项目带头人。

在实际工作中，每个公司的部门分工不同，每个人的具体工作领域也不同。当项目复杂时，需要很多人参与，每个人可能只需要负责或者执行一个小的步骤或模块。所以，在这整个“端到端”并且互相影响的过程中，充分和其他部门和同事合作是很必要的。

人际交往和沟通能力

职场讲究效率，如果你不能进行有效的人际沟通，又怎么会有效率？所以，人际交往能力和沟通能力至关重要。遗憾的是，这些年我遇到了太多在这方面非常欠缺的人（尤其是工程师）。

人际间的沟通有很多种，比如需要和上级、下级和同事沟通，也需要和合作部门甚至客户沟通等等。有效的沟通有四个特点：要及时、要透明、要一致、要清楚。

所谓“及时”，你很容易理解，就是不要太早或太晚。“透明”就是不要私下甚至黑箱操作，尽量通过正式的途径沟通，做到有案可查。“一致”就是说，即使是对不同的人、在不同时间，信息也要尽量一致，不能互相矛盾，尤其是同一部门的人，要“用一个声音讲话”。“清楚”就是信息不要有含糊的地方——允许就是允许，不同意就是不同意，不要让别人误解。

和其他员工之间的人际关系你也要重视。俗话说“三分做事，七分做人”，讲的就是人的因素起到的作用。你的人际关系好，就会左右逢源，在处处有人帮忙的情况下，项目自然会进展顺利。反之，如果人见人厌，项目的推进就会处处受阻，很难把事情做成。平时我们所说的“情商”，很大部分就是在说这个人际关系的处理。

当然，你还需要一定的演讲和写作能力。我们总是需要把自己的东西讲给别人听或者写给别人看。不管是问题展示、进展汇报，还是项目总结，都需要你把事情讲清楚、写清楚。

面试

接着我说说面试，分享一下这个职业的面试要求和经验。我在这一行业学习和工作十几年了，在美国好几家公司工作和实习过。

我被面试过几十次，也面试过别人几百次。

面试的内容和我前面讲过的这一行业需要的知识技能是一致的，大体上就是硬技能和软技能这两类。我要强调这里容易出现的一个误区，那就是很多刚刚踏上职场的朋友经常认为只有硬技能才是所谓的“真本事”，并轻视甚至鄙视软技能，认为那是“虚头八脑的东西”，这样想是完全错误的。

那么我们要如何根据行业需要的知识模块来应对面试呢？虽然实际的面试不会严格地按这八个模块来考，而是会根据你面试公司的情况和具体的职位要求各有侧重，但是“万变不离其宗”，你只要了解这些模块的要求，就可以做到心里有数，无往而不胜了。

硬技能面试

性能和容量工程的硬技能面试就比较直白，很多模块的要求和普通程序员的面试并无本质区别，比如写代码和系统设计。但是由于这一工作的特殊性，我还是要给你讲一讲它的侧重点。

写代码能力不用我多解释，就是我们平时所说的“刷题”。往往是给你一个问题，让你用程序实现。和普通程序员面试模块的唯一区别是它的难度一般不会太高。

性能优化模块算是个特殊模块，一般的程序员和运维面试不会有这方面的要求。这个模块所涉及的知识范围非常广泛，我们第 4 讲到第 29 讲的内容都会涉及到。这些内容都需要你平时积累，是很难临时速成的。

和普通程序员面试类似，系统设计模块也会给你一个开放的问题，让你做出自己的设计。我的经验是，性能和容量工程方面的系统设计面试是有侧重点的，最后都会面向互联网服务性能和容量规划方面。比如给你一个场景，让你预测系统的流量和需要的容量。

容量规划模块就更是直接针对容量工程了。面试时会提出场景性问题，让你根据给定的条件，推导出所需要的系统容量。比如，给你一个社交网站场景，让你详细演算出每个数据中心需要多少服务器（参见第 32 讲）。在计算时，一般要充分考虑季节性因素（比如春节、双十一）和灾难恢复的要求。

软技能面试

软技能方面的面试主要考察四个方面：部门协调能力、人际沟通能力、公司文化的匹配、员工的行为个性。

一般来讲，职位层次越高，对软技能的要求也越高，相应的面试比例也就越大。一个刚刚毕业的职场新人的面试中不会有太多的软技能要求。尤其是部门协调和人际沟通方面的面试，就算有，它的重要性也没有那么大。

但是，对于新人来说，行为个性方面的要求还是有的，毕竟一个员工如果和别人打交道都困难，谁会愿意和他做同事呢？又有谁愿意做他的老板呢？所以，千万别小看软技能，一个人的职场发展越往后走，软技能就越重要。好消息是，这方面的书籍资料的资源很多，并不难准备。

员工行为个性模块就是所谓的“Behavior Interview”，通过要求面试者描述其过去某个工作或者生活经历的具体情况，来了解此人各方面行为素质的特征。比如，面试官可能会问：能给我讲一个你成功说服他人接受你思路的例子吗？又或者，面试官会考察你的时间管理能力，问你：当很多任务同时进行时，你如何规划安排自己的时间？

公司文化匹配是评测一个人的个性特点是否和公司的文化等规定匹配。换句话说，就是这个人能不能在这个公司里面生存下去。如果一个公司的传统是周六加班，而面试者因为种种原因周末完全不能加班，那么这就是不匹配。

部门协调能力主要是看看面试者能不能有效地和其他部门合作，这就需要一定的领导能力和沟通表达能力了。比如，你发现前端客户上传图片的延迟突然增加了，诊断发现是后台服务的性能突然下降导致的。你作为负责这个项目的人，你如何去和后台部门协调？如果他们那边不配合，有各种各样的理由，你又要如何处理？

任何一个员工都是要和同事合作的，团队凝聚在一起才有战斗力。要有人际沟通能力，指的就是需要员工能和别人交往沟通，是所谓的“team player”。毕竟，一个团队最不想看到的，就是有所谓的“猪队友”，不能和别人合作，甚至会拖后腿。

如果你想升职，这些能力都是你必须具备的。记住一点，老板们决定是否给你升职的时候，更多的是考虑你是不是已经到了那个新岗位的要求，而不是先把你升上去，再让你去学。

总结

中国的互联网事业一直在高速前进。性能和容量工程相关的工作，也是这一事业相当重要的一部分，我们的工作会让公司的业务效率和性能不断地提升，也就是对社会做出了贡献。

这种工作需要软硬两方面的知识、技能和经验。若想在这一领域做得出色，你尤其要加强对英文、协调能力和情商方面的训练。古人云：天助自助者，天道酬勤。哪怕你天资有限，机遇不足，现在还做不了“第一名”，但是只要每天进步，不断超越自我，那么你其实就是“成功者”。

古人说：“泰山不让土壤，故能成其大；河海不择细流，故能就其深。”我自己也经常会想，我们每个人只要每天收获一点细流，日积月累就足以汇成奔腾的大河、宽阔的大海，实现辉煌的人生。

思考题

你平时的工作一定需要和别人以及别的组打交道，你在里面是什么地位？需要主动有效地沟通和协调吗？

如果你不断地升职，在每一个级别的岗位上，对这样的协调、沟通和学习能力有什么要求呢？你愿意现在就未雨绸缪吗？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。

结束语 | 不愁明月尽，自有夜珠来

到今天，我们的专栏就正式谢幕了。衷心感谢你在这三个月里，能和我一起学习、探讨这一领域。

专栏写作的这段经历，对我自己而言，算是记忆深刻。我出国也十几年了，这么多年来很少有机会使用中文写作，自知中文有些退化。但是这个专栏，给了我一个挑战和机会，让我去总结过去的学习和工作。

我把这些知识和心得写出来，是希望它对你和其他朋友们有所帮助，对互联网的发展，尤其是对中国互联网的发展尽一份绵薄之力。若能如此，我和极客时间的编辑们在这半年里一起付出的时间和精力就值得了，我也就没有遗憾了。

其实这个专栏的结束，只是你我职业提升的开始。对你来说，如何继续提升呢？我在专栏里面讲过，性能优化和容量管理这方面的工作，需要比较广泛的技能——既需要技术知识方面的硬技能，也需要团队合作等方面的软技能。

锻炼你的软硬技能

关于硬技能，除了我在专栏里面讲过的，我还想说的是，这个领域的职业技术方面发展得很快，各种软件和工具会不断推陈出新。这就需要你持续不断地跟踪、学习，才能保持职场竞争力。

在这方面，我的经验是定期地浏览好的网站和博客，比如Brendan D. Gregg 的博客就有很多不错的内容（性能优化领域）。此外，很多公司的工程博客也值得你一看。比如 LinkedIn 公司的Engineering Blog、Netflix 公司的Netflix TechBlog就写得很好。

如果你有条件，也可以读些书和会议论文。经典的书籍就不用我重复了，相信你都有所了解。而每个具体的专业领域，比如数据库方面，都有不错的会议（比如 VLDB、SIGMOD），你如果工作的内容匹配，也可以看看上面的论文，相信它们会给你一些好的启发。

关于软技能方面，我想强调情商的重要性，尤其是在职场。“职场生存学”你一定也听说过一些。人是自私的生物，在职场上尤甚。技术出身的我们，也许情商方面有“先天欠缺”，所以工作中需要特别注意和其他同事的配合，避免在人际上和团队合作上出现意想不到的问题。

“长恨人心不如水，等闲平地起波澜。”

刘禹锡的《竹枝词》道出了他的职场感慨，你也可以慢慢体会这句话。

要想在职场上成功，必须尽量体会别人的意图和心态，考虑别人的出发点，才能互相愉快合作，努力达到共赢。我本人决不算是高情商的人，这些年来也学到了很多的教训，因此常常自省。之所以在这里提出来，也是希望你能避免这些坑。

在工作中不断学习

唐朝初期的时候，曾经举办过一次中央官员的“诗词大会”，由上官婉儿做裁判。官员们写的诗，经过层层筛选，最后只剩下大诗人沈佺期和宋之问的两首，都是上品，上官婉儿一度难以取舍。

最后宋之问被选为冠军。大家问为什么呢？上官婉儿当众评说：“沈宋二诗，功力悉敌”，但宋之问胜在气势和意境上。

沈佺期诗的最后两句是：“微臣雕朽质，羞睹豫章材”（这里的“豫章材”指的是青年才俊）。这两句虽然用词谦逊，但显得暮气沉沉，词句写完，感觉气势也用尽了。

再看人家宋之问的诗，最后两句是：“不愁明月尽，自有夜珠来。”虽然诗文结束了，但意犹未尽，气势尚在，细细品来，韵味无穷。

我和你回顾这个典故的意义何在呢？

我们的专栏有 36 讲，一起学习了也差不多有 3 个月，时间不算短了。但是要想成为一位好的性能工程师，只看专栏的内容远远不够，还需要你在工作中和工作外不断地学习。我希望专栏的内容能够起到抛砖引玉的作用，启发和引导你去探索性能优化和容量效率这一领域。

最后，我邀请你花三分钟填写一下毕业结课问卷，希望你能在问卷里说出你的学习经历、感受和意见，毕竟专栏结课后的优化离不开你的反馈。

古人云：行百里者半于九十。何况我们才刚刚踏上征程呢？

任何征程都不会是一帆风顺的。学习顺利的时候，希望你戒骄戒躁，毕竟“长江后浪推前浪，浮事新人换旧人”。学习困难的时候，也不要灰心气馁，切记“长风破浪会有时，直挂云帆济沧海”！