106-性能测试实战30讲

开篇词丨“老板，之前咱TPS是100，我优化完是10000”

你好，我是高楼，网名叫 Zee。 很高兴能在这里和你聊性能测试。

在课程开始之前，我先介绍下我自己的从业经历。

从 2005 年毕业开始，除了第一年在做路由器方面的功能、性能测试之外，我后面的工作几乎都是围绕着性能测试分析展开的。

那时我还年轻，喜欢混迹于各大测试论坛，从而认识了很多行业内的高手，很多人也是从那里认识我的。再后来我开始自己弄测试论坛，其实主要是将自己在工作中的积攒的经验分享了出去，虽然一直没有商业化运营，但是不得不说，这个过程对我的知识体系积累起到了非常重要的作用。渐渐地，我用这个论坛形成了自己关于性能测试完整的知识链。

再后来，我开始带团队，我做性能项目的宗旨就是上线不死，死了不收钱。

我从四五个人的小团队开始，一直到有 300 余人的国内外混合团队。我带着这些团队，完整地做过大概 40 多个项目。你可能会问，“完整的项目”是什么意思？它指的就是持续时间在 2 个月左右的性能项目。

为什么会耗时这么长呢？这就涉及到了性能测试的真正含义和工作内容。

我一开始也和大多数人一样，以为做性能测试，就是做些脚本、参数化、关联，压起来之后，再扔出一个结果。

随着时间的增长，我越做越多。慢慢地，我发现，性能测试好像远不止这些内容。

当我把性能分析也加入到工作中之后，性能工作一下子变得丰富起来。现在，我更关注一个性能测试项目在分析调优了之后，响应时间有多大的提升，TPS 有多大的提高，资源有多少的节省。

我曾经在一个零售业大厂做过一个性能咨询。他们的硬件资源很多，256C512G 的机器有一堆，在生产环境中，几乎没有把 CPU 用得超过 5% 的，但是性能问题还不断出现。后来经过两周的性能分析，最后把硬件降到了原来的四分之一，但同时又把性能提高了 10 倍，降硬件的同时，性能也提高了。

类似的工作还有很多，正是这些经历让我觉得，在一个性能测试项目中，分析是必然的过程，只有这样，性能测试的工作才有落地的价值。而这个过程，最好是性能工程师来做，不是别人，因为只有性能工程师才可以串起完整的链路。

真正的性能工程师，可以把结果整理清楚之后，又可以下结论，提出解决方案：线上根据这个测试结果，做对应的配置，系统肯定可以稳定运行。又或者是这样的：当前测试说明了线上不能支持，后面应该如何优化。

你看，这样做，性能工程师的价值是不是立刻就显现出来了？

所以，我们努力的方向是性能的完整工程，这就是我在开头提到的，既要有前期的测试，还要有中间的分析，以及最后的调优，而不仅仅是做做脚本。

当然了，做脚本和参数、压场景、出报告，这是所有新手都必经的一个过程，就像写代码先从“Hello World”开始一样。但是这个过程，必然要在短时间内渡过。

如果你想把性能测试做好，就不要局限自己的技术范围和认知范围。无论是系统、数据库、代码、中间件、存储、网络，你遇到什么问题，都要试着去分析下该如何判断，并考虑如何在后续的过程中进行调优。

在此我需要强调一下，也希望借此可以纠正你的认知，那就是，在我们这个课程中，“性能测试”不仅仅包括测试，还包括分析和调优。

学习性能测试的方法到底是什么？

那现在你心里是不是有个问题：好，我知道了这些，但是到底怎样才能做到呢？

在性能行业中，我看到很多人还在拿着一些看似合理实际没用的概念套在当前的性能领域中。

比如说，性能策略中的性能测试、压力测试、衰减测试、配置测试等等。这些概念你可能听了不下百遍了，但如果问你，你在项目中是否用到了这些策略？估计你都不大能想得起来，自己做的某个场景用到过什么样的策略。

比如说“二八原则”、“响应时间 258 或 2510”、“理发店模型”、“最大 TPS 拐点”等等指标类的紧箍咒。在我看来，在项目的实践中，它们不只是百无一用，而且还产生了错误的导向。

因此，针对当前性能行业的现状，我结合自己多年来的经验，写了这个专栏。在专栏中，我将以实际的项目经历，告诉你在一个具体的项目是如何一步步落实到性能领域的每一个环节中的。

那这个专栏是怎么组织的呢？我主要分了四个模块。

第一个模块是性能测试基础篇。我想在这个模块里澄清一些性能测试的基础概念，讲解一些关键部分。但并不是对概念的简单描述，而是根据实际项目，告诉你真正具有指导价值的性能测试概念是什么，并解析这些概念在实际操作中的指导性作用。

在第二个模块中，我将通过性能测试工具的实际操作实例，对应性能测试的前后逻辑关系。在这一部分中，我会重点给你讲解，为什么要使用某些工具的某些功能，以便确保工具的使用及结果是为性能测试需求指标和性能分析报告而服务的，而不是浮于表面的“炫技”。

在第三个模块中，我将通过操作系统、应用服务器、数据库、缓存服务器、Java、C++ 等监控工具的使用和分析方法，告诉你它们产生的数据在性能分析过程中该如何判断，为测试报告及性能分析提供有效的历史数据。

最后一个模块是对前三个模块的凝练，我会讲解不同实际操作场景中的性能测试分析过程，比如实际的瓶颈判断的过程是怎样的，怎么分析出根本的原因，如何提出具体的解决方案，最后的实施效果又是怎样的。

总的来说，这门课我自己有一个原则，那就是：我不想用空中楼阁似的理论获得情感上的激情，也不想用未经实践的过程获得短暂认同。

性能工程师的前景到底在哪里？

看到这里，如果你已经跃跃欲试想要一探性能测试分析的究竟了，热烈欢迎你。不过我还是有些心里话要再唠叨几句。

性能领域要求的专业技能并不少，发展的宽度和深度完全取决于你自己的意愿。你可以选择只做一个写脚本的工程师，也可以选择成为一个性能调优的专家。从技术范围上说，测试工具、操作系统、开发语言、实现架构、数据库、网络、存储、部署架构等，都是你需要掌握的内容。

所以，我希望这个专栏可以抛出一个价值观——让性能变得有价值。以此刷新你对性能测试的认识，知道这个方向可以干很多事情。

那价值体现在哪里呢？

在性能测试分析优化之前，如果 TPS 是 100，你做完了之后 TPS 是 10000，这就是价值。

在性能测试分析优化之前，如果响应时间是 0.1ms，你做完了之后是 0.01ms，这就是有价值。

在性能测试分析优化之前，如果 CPU 使用率是 100%，你做完了之后是 50%，这就是有价值。

希望你可以从实用的角度，理性看待性能市场，而不是人云亦云。 更希望通过这个专栏，你能够在性能领域这条路上坚定地走下去，并获得长足的发展。可以骄傲地说，我的目标是性能工程师，我的职位是性能工程师。

好了，如果你准备好了，那我们就正式开始吧，欢迎你留言说说自己的情况，你心中的性能测试是怎样的？我们下一讲见！

01丨性能综述：性能测试的概念到底是什么？

在性能测试行业中，长久以来，都存在几个关键的概念误差。在我从业性能测试十几年的经历中，也看到过书籍或网上传播着各种性能测试的概念、方法论等，但是究其本质，再对应到具体的项目工作中，我发现这些概念以及方法论实在没有指导的价值，并且有些概念的产出，也没有确凿的证据来源。

所以在今天，专栏正式更新的第一天，我希望能把这些内容做些梳理，同时这些梳理的内容也会对应到后续的篇幅之中，以便保持理念的一致性。

性能测试概念

我们经常看到的性能测试概念，有人或称之为性能策略，或称之为性能方法，或称之为性能场景分类，大概可以看到性能测试、负载测试、压力测试、强度测试等一堆专有名词的解释。

针对这些概念，我不知道你看到的时候会不会像我的感觉一样：乱！一个小小的性能测试，就延伸出了这么多的概念，并且概念之间的界限又非常模糊。

就拿“压力测试”、“容量测试”和“极限测试”这三个概念来说吧。

网上针对这三个名词的解释是这样的：

压力测试

压力测试是评估系统处于或超过预期负载时系统的运行情况。压力测试的关注点在于系统在峰值负载或超出最大载荷情况下的处理能力。在压力级别逐渐增加时，系统性能应该按照预期缓慢下降，但是不应该崩溃。压力测试还可以发现系统崩溃的临界点，从而发现系统中的薄弱环节。

容量测试

确定系统可处理同时在线的最大用户数，使系统承受超额的数据容量来发现它是否能够正确处理。

极限测试

在过量用户下的负载测试。

恕我直言，这三个概念，对我这个从事性能测试十几年的老鸟来说，都看不出来有啥区别。

也许你会说，你看那里不是说了吗？

“压力测试是在超过预期负载时系统的运行情况”，“容量测试是使系统承受超额的数据容量来发现它是否能够正确处理”。

来吧，就算我语文不好，我也认字的，谁能告诉我这两者的区别是什么？除了字不一样。

如果再抽象一层说一下这些概念，那就是，这些概念都在描述性能测试的不同侧面。而这些侧面本身构不成策略，构不成方法，不能说是概念，也不能说是理论。

此文一出，肯定会有人说，既然你评价当前的概念混乱，那你有什么好建议呢？作为可能被集体轰炸的话题，既然已经摆上了台面，我还是要冒死给一个自己认为的合理定义：

性能测试针对系统的性能指标，建立性能测试模型，制定性能测试方案，制定监控策略，在场景条件之下执行性能场景，分析判断性能瓶颈并调优，最终得出性能结果来评估系统的性能指标是否满足既定值。

这是我觉得唯一合理的概念定义，下面我就把这个概念详细解释一下。

性能测试需要有指标

有人说，我们在做项目的时候，就没有指标，老板只说一句，系统压死为止。听起来很儿戏，但这样的场景不在少数。在我看来，把系统压死也算是一种指标。至于你用什么手段把系统“压死”，那就是实现的问题了。你可以采用很多种手段，告诉老板，这系统还没压就死了！ 这也是你的贡献。

而对“有指标”这个定义来说，理论上合理的，并且应该有的指标是：时间指标、容量指标和资源利用率指标。

而这里的指标又会有细分，细分的概念又是一团乱。这个话题我们后面再描述。

性能测试需要有模型

模型是什么？它是真实场景的抽象，可以告诉性能测试人员，业务模型是什么样子。比如说，我们有 100 种业务，但不是每个业务都需要有并发量，可能只有 50 个业务有，那就要把这些有并发的业务统计出来，哪个业务并发多，哪个业务并发少，做压力时就要控制好这样的比例。

这种做法需要的数据通常都是从生产环境中的数据中统计来的，很多在线上不敢直接压测的企业都是这样做的。

而随着互联网中零售业、云基础架构的全面发展，有些企业直接在线上导流来做性能测试，这种思路上的转变来源于架构的发展及行业的真实需要。但这并不能说明性能测试不需要模型了，因为这个模型已经从生产流量中导过来了。这一点，还是需要你认清的。

但是对于其他的一些行业，比如银行这类金融机构，线上一个交易都不能错。像上面这样做的难度就太大了。所以这些行业中，仍然需要在测试环境中用业务模型来模拟出生产的流量。

同时也请你认清一点，现在的全链路压测，并没有像吹嘘得那么神乎其神，很多企业也只是在线上的硬件资源上做压力而已，并不是真正的逻辑链路修改。

我在工作中经常会被问到，性能流量直接从生产上导的话，是不是就可以不用性能测试人员了？性能测试人员就要被淘汰了？

这未免太短视了，大家都盯着最新鲜的技术、方法、概念，各层的领导也都有自己的知识偏好，万一做了一个决定，影响了最终的结果，有可能会让很多人跟着受罪。

我之前带过的一个团队中，开发架构们一开始就规划了特别详细的微服务架构，说这一套非常好。我说这个你们自己决定，我只要在这里面拿到可用的结果就行。结果开发了不到两个月，一个个微服务都被合并了，还得天天加班做系统重构，只留了几大中台组件。这是为什么呢？因为不适用呀。

同理，性能测试也要选择适合自己系统业务逻辑的方式，用最低的成本、最快的时间来做事情。

性能测试要有方案

方案规定的内容中有几个关键点，分别是测试环境、测试数据、测试模型、性能指标、压力策略、准入准出和进度风险。基本上有这些内容就够了，这些内容具体的信息还需要精准。

你可能会说，怎么没有测试计划？我的建议是，用项目管理工具单独画测试计划，比如用 Project 或 OmniPlan 之类的工具。这是因为在方案中，写测试计划，基本上只能写一个里程碑，再细化一点，就是在里面再加几个大阶段的条目。但是用项目管理工具做计划就不同了，它不仅可以细分条目，还能跟踪各个工作的动态进度，可以设置前后依赖关系，填入资源和成本以便计算项目偏差。

性能测试中要有监控

这个部分的监控，要有分层、分段的能力，要有全局监控、定向监控的能力。关于这一点，我将在第三模块详细说明。

性能测试要有预定的条件

这里的条件包括软硬件环境、测试数据、测试执行策略、压力补偿等内容。要是展开来说，在场景执行之前，这些条件应该是确定的。

有人说，我们压力中也会动态扩展。没问题，但是动态扩展的条件或者判断条件，也是有确定的策略的，比如说，我们判断 CPU 使用率达到 80% 或 I/O 响应时间达到 10ms 时，就做动态扩展。这些也是预定的条件。

关于这一点，在我的工作经历中，经常看到有性能测试工程师，对软硬件资源、测试数据和执行策略分不清楚，甚至都不明白为什么要几分钟加几个线程。在这种情况之下，就不能指望这个场景是有效的了。

性能测试中要有场景

可以说，“性能场景”这个词在性能测试中占据着举足轻重的地位，只是我们很多人都不理解“场景”应该如何定义。场景来源于英文的 scenario，对性能场景中的“场景”比较正宗的描述是：在既定的环境（包括动态扩展等策略）、既定的数据（包括场景执行中的数据变化）、既定的执行策略、既定的监控之下，执行性能脚本，同时观察系统各层级的性能状态参数变化，并实时判断分析场景是否符合预期。

这才是真正的场景全貌。

性能场景也要有分类，在我有限的工作经验中，性能场景从来都没有超出过这几个分类。

基准性能场景：这里要做的是单交易的容量，为混合容量做准备（不要跟我说上几个线程跑三五遍脚本叫基准测试，在我看来，那只是场景执行之前的预执行，用来确定有没有基本的脚本和场景设计问题，不能称之为一个分类）。

容量性能场景：这一环节必然是最核心的性能执行部分。根据业务复杂度的不同，这部分的场景会设计出很多个，在概念部分就不细展开了，我会在后面的文章中详细说明。

稳定性性能场景：稳定性测试必然是性能场景的一个分类。只是现在在实际的项目中，稳定性测试基本没和生产一致过。在稳定性测试中，显然最核心的元素是时间（业务模型已经在容量场景中确定了），而时间的设置应该来自于运维周期，而不是来自于老板、产品和架构等这些人的心理安全感。

异常性能场景：要做异常性能场景，前提就是要有压力。在压力流量之下，模拟异常。这个异常的定义是很宽泛的，在下一篇文章里，我们再细说。

很多性能测试工程师，都把场景叫成了测试用例。如果只是叫法不同，我觉得倒是可以接受，关键是内容也出现了很大的偏差，这个偏差就是，把用例限定在了描述测试脚本和测试数据上，并没有描述需要实时的判断和动态的分析。这就严重影响了下一个概念：性能结果。

性能测试中要有分析调优

一直以来，需不需要在性能测试项目中调优，或者说是不是性能测试工程师做调优，人们有不同的争论。

从性能市场的整体状态来看，在性能测试工程师中，可以做瓶颈判断、性能分析、优化的人并不多，所以很多其他职位上的人对性能测试的定位也就是性能验证，并不包括调优的部分。于是有很多性能项目都定义在一两周之内。这类项目基本上也就是个性能验证，并不能称之为完整的性能项目。而加入了调优部分之后，性能项目就会变得复杂。对于大部分团队来说，分析瓶颈都可能需要很长时间，这里会涉及到相关性分析、趋势分析、证据链查找等等手段。

所以，就要不要进行调优，我做了如下划分。

对性能项目分为如下几类。

新系统性能测试类：这样的项目一般都会要求测试出系统的最大容量，不然上线心里没底。

旧系统新版本性能测试类：这样的项目一般都是和旧版本对比，只要性能不下降就可以根据历史数据推算容量，对调优要求一般都不大。

新系统性能测试优化类：这类的系统不仅要测试出最大容量，还要求调优到最好。

对性能团队的职责定位有如下几种。

性能验证：针对给定的指标，只做性能验证。第三方测试机构基本上都是这样做的。

性能测试：针对给定的系统，做全面的性能测试，可以得到系统最大容量，但不涉及到调优。

性能测试 + 分析调优：针对给定的系统，做全面的性能测试，同时将系统调优到最优状态。

当只能做性能验证的团队遇到旧系统新版本性能测试类和新系统性能测试优化类项目，那就会很吃力，这样的团队只能做新系统性能测试类项目。

当做性能测试的团队，遇到需要新系统性能测试优化类项目，照样很吃力。这样的团队能做前两种项目。

只有第三个团队才能做第三种项目。

性能测试肯定要有结果报告

性能结果如何来定义呢？有了前面监控的定义，有了场景执行的过程，产生的数据就要整理到结果报告中了。这个文档工作也是很重要的，是体现性能团队是否专业的一个重要方面。并不是整理一个 Word，美化一下格式就可以了。测试报告是需要汇报或者归档的。

如果是内部项目，测试报告可能就是一个表格，发个邮件就完整了，另外归档也是必须的。而对一些有甲乙方的项目，就需要汇报了。

那么，如何汇报呢？

我们要知道，大部分老板或者上司关心的是测试的结果，而不是用了多少人，花了多少时间这些没有意义的数字。我们更应该在报告中写上调优前后的 TPS、响应时间以及资源对比图。

有了上面的的解析，相信你对性能测试的定义有了明确的感觉了。这个定义其实就是描述了性能测试中要做的事情。

当然，也许会有人跳出来说，你这个说得太重了，不够敏捷。现在不都用 DevOps 了吗？还要按这个流程来走一遍吗？

显然有这种说法的人，没有理解我要说的主旨。以上的内容是针对一个完整的项目，或系统或公司的系统演进。对于一些半路就跟着版本和新需求一轮轮迭代做下去的人的处境会不同，因为这样的人只看到了当前的部分，而不是整个过程。

并且这个过程也是不断在迭代演进的。

不管是敏捷开发过程还是 DevOps，你可以一条条去仔细分析下项目中的各个环节（我说的是整个项目从无到有），都不会跳出以上定义，如果有的话，请随时联系我，我好改定义。:)

通过图示最后总结一下性能测试的概念：

有了这个图示之后，就比较清晰了。

所以，前面所说的压力测试、容量测试、负载测试等等，在实际的项目实施过程中，都不具备全局的指导价值。我个人认为，你应该在性能领域中抛弃这些看似非常有道理实则毫无价值的概念。

总结

今天的内容我只讲了一点，那就是性能测试的概念。请不要再使用像性能测试、负载测试、容量测试这样的词来概括性能执行策略，这是对实施过程没有任何指导价值的。

在性能测试的概念中，性能指标、性能模型、性能场景、性能监控、性能实施、性能报告，这些既是概念中的关键词，也可以说是性能测试的方法和流程。

而这些概念我们在实际的工作中，都是非常重要的。因为它们要抹平沟通的误解。让不同层级，不同角色的人，可以在同样的知识背景下沟通，也可以让做事情的人有清晰的逻辑思路，同时对同行间的交流，也有正向的促进作用。

思考题

最后给你留两道思考题吧，我为什么不推荐使用性能测试、负载测试、容量测试这样的词来概括性能执行策略呢？以及，为什么性能测试中要有监控和分析？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

02丨性能综述：TPS和响应时间之间是什么关系？

我们在上一篇文章中讲了性能测试的概念，肯定会有人觉得，那些概念很重要，怎么能轻易抹杀呢？那么，在今天的文章中，我们就来扒一扒性能场景，看看概念与实际之间的差别。

前面我们说了性能要有场景，也说了性能场景要有基准性能场景、容量性能场景、稳定性性能场景、异常性能场景。在我有限的十几年性能生涯中，从来没有见过有一个性能场景可以超出这几个分类。下面我将对前面说到的概念进行一一对应。

学习性能的人，一定看吐过一张图，现在让你再吐一次。如下：

在这个图中，定义了三条曲线、三个区域、两个点以及三个状态描述。

三条曲线：吞吐量的曲线（紫色）、使用率 / 用户数曲线（绿色）、响应时间曲线（深蓝色）。

三个区域：轻负载区（Light Load）、重负载区（Heavy Load）、塌陷区（Buckle Zone）。

两个点：最优并发用户数（The Optimum Number of Concurrent Users）、最大并发用户数（The Maximum Number of Concurrent Users）。

三个状态描述：资源饱和（Resource Saturated）、吞吐下降（Throughput Falling）、用户受影响（End Users Effected）。

我在很多地方，都看到了对这张图的引用。应该说，做为一个示意图，它真的非常经典，的确描述出了一个基本的状态。但是，示意图也只能用来做示意图，在具体的项目中，我们仍然要有自己明确的判断。

我们要知道，这个图中有一些地方可能与实际存在误差。

首先，很多时候，重负载区的资源饱和，和 TPS 达到最大值之间都不是在同样的并发用户数之下的。比如说，当 CPU 资源使用率达到 100% 之后，随着压力的增加，队列慢慢变长，但是由于用户数增加的幅度会超过队列长度，所以 TPS 仍然会增加，也就是说资源使用率达到饱和之后还有一段时间 TPS 才会达到上限。

大部分情况下，响应时间的曲线都不会像图中画得这样陡峭，并且也不一定是在塌陷区突然上升，更可能的是在重负载区突然上升。

另外，吞吐量曲线不一定会出现下降的情况，在有些控制较好的系统中会维持水平。曾经在一个项目中，因为 TPS 维持水平，并且用户数和响应时间一直都在增加，由于响应时间太快，一直没有超时。我跟我团队那个做压力的兄弟争论了三个小时，我告诉他接着压下去已经没有意义，就是在等超时而已。他倔强地说，由于没有报错，时间还在可控范围，所以要一直加下去。关于这一点争论，我在后续的文章中可能还会提及。

最优并发数这个点，通常只是一种感觉，并没有绝对的数据用来证明。在生产运维的过程中，其实我们大部分人都会更为谨慎，不会定这个点为最优并发，而是更靠前一些。

最大并发数这个点，就完全没有道理了，性能都已经衰减了，最大并发数肯定是在更前的位置呀。这里就涉及到了一个误区，压力工具中的最大用户数或线程数和 TPS 之间的关系。在具体的项目实施中，有经验的性能测试人员，都会更关心服务端能处理的请求数即 TPS，而不是压力工具中的线程数。

这张图没有考虑到锁或线程等配置不合理的场景，而这类场景又比较常见。也就是我们说的，TPS 上不去，资源用不上。所以这个图默认了一个前提，只要线程能用得上，资源就会蹭蹭往上涨。

这张图呢，本来只是一个示意，用以说明一些关系。但是后来在性能行业中，有很多没有完全理解此图的人将它做为很有道理的“典范”给一些人讲，从而引起了越来越多的误解。

此图最早的出处是 2005 年 Quest Software 的一个 PSO Consultant 写的一个白皮书《Performance Testing Methodology》。在 18 页论述了这张图，原文摘录一段如下：

You can see that as user load increases, response time increases slowly and resource utilization increases almost linearly. This is because the more work you are asking your application to do, the more resources it needs. Once the resource utilization is close to 100 percent, however, an interesting thing happens – response degrades with an exponential curve. This point in the capacity assessment is referred to as the saturation point. The saturation point is the point where all performance criteria are abandoned and utter panic ensues. Your goal in performing a capacity assessment is to ensure that you know where this point is and that you will never reach it. You will tune the system or add additional hardware well before this load occurs.

按照这段描述，这个人只是随着感觉在描述一种现象，除此无它。比如说，The saturation point is the point where all performance criteria are abandoned and utter panic ensues. 在我的工作经验中，其实在 saturation point 之前，性能指标就已经可以显示出问题了，并且已经非常 panic 了，而我们之所以接着再加压力是为了让指标显示得更为明显，以便做出正确的判断。而调优实际上是控制系统在饱和点之前，这里有一个水位的问题，控制容量到什么样的水位才是性能测试与分析的目标。

我们简化出另一个图形，以说明更直接一点的关系。如下所示：

上图中蓝线表示 TPS，黄色表示响应时间。

在 TPS 增加的过程中，响应时间一开始会处在较低的状态，也就是在 A 点之前。接着响应时间开始有些增加，直到业务可以承受的时间点 B，这时 TPS 仍然有增长的空间。再接着增加压力，达到 C 点时，达到最大 TPS。我们再接着增加压力，响应时间接着增加，但 TPS 会有下降（请注意，这里并不是必然的，有些系统在队列上处理得很好，会保持稳定的 TPS，然后多出来的请求都被友好拒绝）。

最后，响应时间过长，达到了超时的程度。

在我的工作中，这样的逻辑关系更符合真实的场景。我不希望在这个关系中描述资源的情况，因为会让人感觉太乱了。

为什么要把上面描述得如此精细？这是有些人将第一张图中的 Light load 对应为性能测试，Heavy Load 对应为负载测试，Buckle Zone 对应为压力测试……还有很多的对应关系。

事实上，这是不合理的。

下面我将用场景的定义来替换这些混乱的概念。

为什么我要如此划分？因为在具体场景的操作层面，只有场景中的配置才是具体可操作的。而通常大家认为的性能测试、负载测试、压力测试在操作的层面，只有压力工具中线程数的区别，其他的都在资源分析的层面，而分析在很多人的眼中，都不算测试。

拿配置测试和递增测试举例吧。

在性能中，我们有非常多的配置，像 JVM 参数、OS 参数、DB 参数、网络参数、容器参数等等。如果我们把测试这些配置参数，称为”配置测试“，我觉得未免过于狭隘了。因为对于配置参数来说，这只是做一个简单的变更，而性能场景其实没有任何变化呀。配置更改前后，会用同样的性能场景来判断效果，最多再增加一些前端的压力，实际的场景并没有任何变化，所以，我觉得它不配做为一个单独的分类。

再比如递增测试，在性能中，基准性能场景也好，容量性能场景也好，哪个是不需要递增的呢？我知道现在市场上经常有测试工程师，直接就上了几百几千线程做压力（请你不要告诉我这是个正常的场景，鉴于我的精神有限，承受不了这样的压力）。除了秒杀场景，同时上所有线程的场景，我还没有见到过。在一般的性能场景中，递增都是必不可少的过程。同时，递增的过程，也要是连续的，而不是 100 线程、200 线程、300 线程这样断开执行场景，这样是不合理的。关于这一点，我们将在很多地方着重强调。所以我觉得递增也不配做一个单独的分类。

其他的概念，就不一一批驳了。其实在性能测试中，在实际的项目实施中，我们并不需要这么多概念，这些杂七杂八的概念也并没有对性能测试领域的发展起到什么推进作用。要说云计算、AI、大数据这些概念，它们本身在引导着一个方向。

而性能测试中被定为“测试”，本身就处在软件生存周期的弱势环节，当前的市场发展也并不好。还被这些概念冲乱了本来应该有的逻辑的思路，实在是得不偿失。

总结

总之，在具体的性能项目中，性能场景是一个非常核心的概念。因为它会包括压力发起策略、业务模型、监控模型、性能数据（性能中的数据，我一直都不把它称之为模型，因为在数据层面，测试并没有做过什么抽象的动作，只是使用）、软硬件环境、分析模型等。

有了清晰的、有逻辑的场景概念之后，在后面的篇幅当中，我们将从场景的各个角度去拆解。在本专栏中，我们将保持理念的连贯性，以示我不变的职业初心。

思考题

如果你理解了今天的内容，不妨说说为什么说现在市场上的概念对性能项目的实施并没有太大的价值？其次，性能场景为什么要连续？而不是断开？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

03丨性能综述：怎么理解TPS、QPS、RT、吞吐量这些性能指标？

在上一篇文章中，我们讲述了性能场景，下面就要说性能需求指标了。

通常我们都从两个层面定义性能场景的需求指标：业务指标和技术指标。

这两个层面需要有映射关系，技术指标不能脱离业务指标。一旦脱离，你会发现你能回答”一个系统在多少响应时间之下能支持多少 TPS“这样的问题，但是回答不了”业务状态是什么“的问题。

举例来说，如果一个系统要支持 1000 万人在线，可能你能测试出来的结果是系统能支持 1 万 TPS，可是如果问你，1000 万人在线会不会有问题？这估计就很难回答了。

我在这里画一张示意图以便你理解业务指标和性能指标之间的关系。

这个示意显然不够详细，但也能说明关系了。所有的技术指标都是在有业务场景的前提下制定的，而技术指标和业务指标之间也要有详细的换算过程。这样一来，技术指标就不会是一块飞地。同时，在回答了技术指标是否满足的同时，也能回答是否可以满足业务指标。

有了这样的关联关系，下面我们看一下性能测试行业常用的性能指标表示法。

我将现在网上能看到的性能指标做了罗列，其中不包括资源的指标。因为资源类的比较具体，并且理解误差并不大，但业务类的差别就比较大了。

对这些性能指标都有哪些误解

我记得我还年轻的时候，还没有 QPS、RPS、CPS 这样的概念，只有 TPS。那个时候，天总是那么蓝，时间总是那么慢，“你锁了人家就懂了”。

QPS 一开始是用来描述 MySQL 中 SQL 每秒执行数 Query Per Second，所有的 SQL 都被称为 Query。后来，由于一些文章的转来转去，QPS 被慢慢地移到了压力工具中，用来描述吞吐量，于是这里就有些误解，QPS 和 TPS 到底是什么关系呢？

RPS 指的是每秒请求数。这个概念字面意思倒是容易理解，但是有个容易误解的地方就是，它指的到底是哪个层面的 Request？如果说 HTTP Request，那么和 Hits Per Second 又有什么关系呢？

HPS，这也是个在字面意思上容易理解的概念。只是 Hit 是什么？有人将它和 HTTP Request 等价，有人将它和用户点击次数等价。

CPS，用的人倒是比较少，在性能行业中引起的误解范围并不大。同时还有喜欢用 CPM（Calls Per Minute，每分钟调用数）的。这两个指标通常用来描述 Service 层的单位时间内的被其他服务调用的次数，这也是为什么在性能行业中误解不大的原因，因为性能测试的人看 Service 层东西的次数并不多。

为了区分这些概念，我们先说一下 TPS（Transactions Per Second）。我们都知道 TPS 是性能领域中一个关键的性能指标概念，它用来描述每秒事务数。我们也知道 TPS 在不同的行业、不同的业务中定义的粒度都是不同的。所以不管你在哪里用 TPS，一定要有一个前提，就是所有相关的人都要知道你的 T 是如何定义的。

经常有人问，TPS 应该如何定义？这个实在是没有具体的“法律规定”，那就意味着，你想怎么定就怎么定。

通常情况下，我们会根据场景的目的来定义 TPS 的粒度。如果是接口层性能测试，T 可以直接定义为接口级；如果业务级性能测试，T 可以直接定义为每个业务步骤和完整的业务流。

我们用一个示意图来说明一下。

如果我们要单独测试接口 1、2、3，那 T 就是接口级的；如果我们要从用户的角度来下一个订单，那 1、2、3 应该在一个 T 中，这就是业务级的了。

当然，这时我们还要分析系统是如何设计的。通常情况下，积分我们都会异步，而库存不能异步哇。所以这个业务，你可以看成只有 1、2 两个接口，但是在做这样的业务级压力时，3 接口也是必须要监控分析的。

所以，性能中 TPS 中 T 的定义取决于场景的目标和 T 的作用。一般我们都会这样来定事务。

接口级脚本：

——事务 start（接口 1）

接口 1 脚本

——事务 end（接口 1）

——事务 start（接口 2）

接口 2 脚本

——事务 end（接口 2）

——事务 start（接口 3）

接口 3 脚本

——事务 end（接口 3）

业务级接口层脚本（就是用接口拼接出一个完整的业务流）：

——事务 start（业务 A）

接口 1 脚本 - 接口 2（同步调用）

接口 1 脚本 - 接口 3（异步调用）

——事务 end（业务 A）

用户级脚本

——事务 start（业务 A）

点击 0 - 接口 1 脚本 - 接口 2（同步调用）

点击 0 - 接口 1 脚本 - 接口 3（异步调用）

——事务 end（业务 A）

你要创建什么级别的事务，完全取决于测试的目的是什么。

一般情况下，我们会按从上到下的顺序一一地来测试，这样路径清晰地执行是容易定位问题的。

重新理解那些性能指标概念

搞清楚了 TPS 的 T 是什么，下面就要说什么是 TPS 了。字面意思非常容易理解，就是：每秒事务数。

在性能测试过程中，TPS 之所以重要，是因为它可以反应出一个系统的处理能力。我在很多场景中都说过，事务就是统计了一段脚本的执行时间，并没有什么特别的含义。而现在又多加了其他的几个概念。

首先是 QPS，如果它描述的是数据库中的 Query Per Second，从上面的示意图中来看，其实描述的是服务后面接的数据库中 SQL 的每秒执行条数。如果描述的是前端的每秒查询数，那就不包括插入、更新、删除操作了。显然这样的指标用来描述系统整体的性能是不够全面的。所以不建议用 QPS 来描述系统整体的性能，以免产生误解。

RPS（Request per second），每秒请求数。看似简单的理解，但是对于请求数来说，要看是在哪个层面看到的请求，因为请求这个词，实在是太泛了。我们把上面的图做一点点变化来描述一下请求数。

如果一个用户点击了一次，发出来 3 个 HTTP Request，调用了 2 次订单服务，调用了 2 次库存服务，调用了 1 次积分服务，那么这个 Request 该如何计算？如果你是算 GDP 的专家，我觉得可能会是：3+2+2+1=8（次）。而在具体的项目中，我们会单独描述每个服务，以便做性能统计。如果要描述整体，最多算是有 3 个 RPS。如果从 HTTP 协议的角度去理解，那么 HTTP Request 算是一个比较准确的描述了，但它本身的定义并没有包含业务。如果赋予它业务的含义，那么用它来描述性能也是可以的。

HPS（Hits Per Second），每秒点击数。Hit 一般在性能测试中，都用来描述 HTTP Request。但是，也有一些人用它描述真正的客户在界面上的点击次数。关于这一点，就只有在具体的项目中才能规定得具体一些。当它描述 HTTP Request 时，如果 RPS 也在描述 HTTP Request，那这两个概念就完全一样了。

CPS/CPM：Calls Per Second/ Calls Per Minutes，每秒 / 每分钟调用次数。这个描述在接口级是经常用到的，比如说上面的订单服务。显然一次客户界面上的点击调用两次。这个比较容易理解。但是，在操作系统级，我们也经常会听到系统调用用 call 来形容，比如说用 strace 时，你就会看见 Calls 这样的列名。

这些概念本身并没有问题，但是当上面的概念都用来描述一个系统的性能能力的时候，就混乱了。对于这种情况，我觉得有几种处理方式：

用一个概念统一起来。我觉得直接用 TPS 就行了，其他的都在各层面加上限制条件来描述。比如说，接口调用 1000 Calls/s，这样不会引起混淆。

在团队中定义清楚术语的使用层级。

如果没有定义使用层级，那只能在说某个概念的时候，加上相应的背景条件。

所以，当你和同事在沟通性能指标用哪些概念时，应该描述得更具体一些。在一个团队中，应该先有这些术语统一的定义，再来说性能指标是否满足。

响应时间 RT

在性能中，还有一个重要的概念就是响应时间（Response Time）。这个比较容易理解。我们接着用这张示意图说明：

RT = T2-T1。计算方式非常直接简单。但是，我们要知道，这个时间包括了后面一连串的链路。

响应时间的概念简单至极，但是，响应时间的定位就复杂了。

性能测试工具都会记录响应时间，但是，都不会给出后端链路到底哪里慢。经常有人问问题就直接说，我的响应时间很慢。问题在哪呢？在这种情况下，只能回答：不知道。

因为我们要先画架构图，看请求链路，再一层层找下去。比如说这样：

在所有服务的进出口上都做记录，然后计算结果就行了。在做网关、总线这样的系统时，基本上都会考虑这个功能。

而现在，随着技术的发展，链路监控工具和一些 Metrics 的使用，让这个需求变得简单了不少。比如说这样的展示：

它很直观地显示了，在一个请求链路上，每个节点消耗的时间和请求的持续时间。

我顺便在这里说一下调优在当前性能项目中的状态。

对于响应时间来说，时间的拆分定位是性能瓶颈定位分析中非常重要的一节。但是请注意，这个环节并不是性能测试工程师的最后环节。

在工作中，我经常看到有很多性能测试工程师连时间拆分都不做，只报一个压力工具中看到的响应时间，就给一个通过不通过的结论，丝毫没有定位。

另外，有一些性能测试工程师，倒是用各种手段分析了时间的消耗点，但是也觉得自己的工作就此结束了，而不做根本原因的分析或协调其他团队来分析。

当然在不同的企业里，做分析的角色和要求各不相同，所以也要根据实际的企业现状来说。

在我的观点中，性能只测不调，那就是性能验证的工作，称不上是完整的性能项目。第三方性能测试的机构可以这样做，但是在一个企业内部这样做的话，性能团队的价值肯定就大打折扣了。

但是现在有很多人都不把性能调优做为性能团队的工作，主要原因有几点：

性能测试团队的人能力有限做不到；

性能调优代价高，耗时长，不值得做。

在我带的性能项目中，基本上调优的工作都是我的团队主导的。性能团队当然不可能完全没有技术弱点，所以在很多时候都是协调其他团队的人一起来分析瓶颈点。那为什么是我的团队来主导这个分析的过程呢？

因为每个技术人员对性能瓶颈的定义并不相同，如果不细化到具体的计数器的值是多少才有问题，有误解的可能性就很大。

曾经我在某零售业大厂做性能咨询的时候，一房间的技术人员，开发、运维、DBA 都有，结果性能瓶颈出现了，所有人都说自己的部分是没问题的。于是我一个个问他们是如何判断的，判断的是哪个计数器，值又是多少。结果发现很多人对瓶颈的判断都和我想像的不一样。

举例来说，DB 的 CPU 使用率达到 90% 以上，DBA 会觉得没有问题，因为都是业务的 SQL，并不是 DB 本身有问题。开发觉得 SQL 执行时间慢是因为 DB 有问题，而不是自己写的有问题，因为业务逻辑并没有错，有问题的点应该是 DB 上索引不合理、配置不合理。

你看，同样的问题，每个人的看法都有区别。当然也不能排除有些人就是想推诿责任。

这时怎么办呢？如果你可以把执行计划拿出来，告诉大家，这里应该创建索引，而那里应该修改业务条件，这时就具体了。

压力工具中的线程数和用户数与 TPS

总是有很多人在并发线程数和 TPS 之间游荡，搞不清两者的关系与区别。这两个概念混淆的点就是，好像线程是真实的用户一样，那并发的线程是多少就描述出了多少真实的用户。

但是做性能的都会知道，并发线程数在没有模拟真实用户操作的情况下，和真实的用户操作差别非常远。

在 LoadRunner 还比较红火的时候，Mercury 提出一个 BTO 的概念，就是业务科技优化。在 LoadRunner 中也提出”思考时间“的概念，其实在其他的性能工具中是没有”思考时间“这个词的。这个词的提出就是为了性能工具模拟真实用户。

但是随着性能测试的地位不断下降，以及一些概念和名词不断地被以讹传讹，导致现在很多人都没有明白压力工具中的线程数和用户以及 TPS 之间是怎样的关系。同样，我们先画一个示意图来说明一下。

这里先说明一个前提，上面的一个框中有四个箭头，每个都代表着相同的事务。

在说这个图之前，我们要先说明”并发“这个概念是靠什么数据来承载的。

在上面的内容中，我们说了好多的指标，但并发是需要具体的指标来承载的。你可以说，我的并发是 1000TPS，或者 1000RPS，或者 1000HPS，这都随便你去定义。但是在一个具体的项目中，当你说到并发 1000 这样没有单位的词时，一定要让大家都能理解这是什么。

在上面这张示意图中，其实压力工具是 4 个并发线程，由于每个线程都可以在一秒内完成 4 个事务，所以总的 TPS 是 16。这非常容易理解吧。而在大部分非技术人的脑子里，这样的场景就是并发数是 4，而不是 16。

要想解释清楚这个非常困难，我的做法就是，直接告诉别人并发是 16 就好了，不用关心 4 个线程这件事。这在我所有项目中几乎都是一样的，一直也没有什么误解。

那么用户数怎么来定义呢？涉及到用户就会比较麻烦一点。因为用户有了业务含义，所以有些人认为一个系统如果有 1 万个用户在线，那就应该测试 1 万的并发线程，这种逻辑实在是不技术。通常，我们会对在线的用户做并发度的分析，在很多业务中，并发度都会低于 5%，甚至低于 1%。

拿 5% 来计算，就是 10000 用户 x5%=500(TPS)，注意哦，这里是 TPS，而不是并发线程数。如果这时响应时间是 100ms，那显然并发线程数是 500TPS/(1000ms/100ms)=50(并发线程)。

通过这样简单的计算逻辑，我们就可以看出来用户数、线程数和 TPS 之间的关系了。

但是！响应时间肯定不会一直都是 100ms 的嘛。所以通常情况下，上面的这个比例都不会固定，而是随着并发线程数的增加，会出现趋势上的关系。

所以，在性能分析中，我一直在强调着一个词：趋势！

业务模型的 28 原则是个什么鬼？

我看到有些文章中写性能测试要按 28 原则来计算并发用户数。大概的意思就是，如果一天有 1000 万的用户在使用，系统如果开 10 个小时的话，在计算并发用户数的时候，就用 2 小时来计算，即 1000 万用户在 2 小时内完成业务。

我要说的是，这个逻辑在一个特定的业务系统中是没有任何价值的。因为每个系统的并发度都由业务来确定，而不是靠这样的所谓的定律来支配着业务。

如果我们做了大量的样本数据分析，最后确实得出了 28 的比例，我觉得那也是可以的。但是如果什么数据都没有分析，直接使用 28 比例来做评估和计算，那就跟耍流氓没有区别。

业务模型应该如何得到呢？这里有两种方式是比较合理的：

根据生产环境的统计信息做业务比例的统计，然后设定到压力工具中。有很多不能在线上直接做压力测试的系统，都通过这种方式获取业务模型。

直接在生产环境中做流量复制的方式或压力工具直接对生产环境发起压力的方式做压力测试。这种方式被很多人称为全链路压测。其实在生产中做压力测试的方式，最重要的工作不是技术，而是组织协调能力。相信参与过的人都能体会这句话的重量。

响应时间的 258 原则合理吗？

对于响应时间，有很多人还在说着 258 或 2510 响应时间是业内的通用标准。然后我问他们这个标准的出处在哪里？谁写的？背景是什么？几乎没有人知道。真是不能想像，一个谁都不知道出处的原则居然会有那么大的传播范围，就像谣言一样，出来之后，再也找不到源头。

其实这是在 80 年代的时候，英国一家 IT 媒体对音乐缓冲服务做的一次调查。在那个年代，得到的结果是，2 秒客户满意度不错；5 秒满意度就下降了，但还有利润；8 秒时，就没有利润了。于是他们就把这个统计数据公布了出来，这样就出现了 258 principle，翻译成中文之后，它就像一个万年不变的定理，深深影响着很多人。

距离这个统计结果的出现，已经过去快 40 年了，IT 发展的都能上天了，这个时间现在已经完全不适用了。所以，以后出去别再提 258/2510 响应时间原则这样的话了，太不专业。

那么响应时间如何设计比较合理呢？这里有两种思路推荐给你。

同行业的对比数据。

找到使用系统的样本用户（越多越好），对他们做统计，将结果拿出来，就是最有效的响应时间的制定标准。

性能指标的计算方式

我们在网上经常可以看到有人引用这几个公式。

公式（1）：

并发用户数计算的通用公式：C=TnL​

其中 C 是平均的并发用户数；n 是 login session 的数量；L 是 login session 的平均长度；T 指考察的时间段长度。

公式（2）：

并发用户数峰值： C’≈C+3C​

C’指并发用户数的峰值，C 就是公式（1）中得到的平均的并发用户数。该公式是假设用户的 login session 产生符合泊松分布而估算得到的。

仔细搜索之后发现会发现这两个公式的出处是 2004 年一个叫 Eric Man Wong 的人写的一篇名叫《Method for Estimating the Number of Concurrent Users》的文章。中英文我都反复看到很多篇。同时也会网上看到有些文章中把这个文章描述成“业界公认”的计算方法。

在原文中，有几个地方的问题。

C 并不是并发用户，而是在线用户。

这两个公式做了很多的假设条件，比如说符合泊松分布什么的。为什么说这个假设有问题？我们都知道泊松分布是一个钟型分布，它分析的是一个系统在全周期中的整体状态。

如果要让它在实际的项目中得到实用，还需要有大量的统计数据做样本，代入计算公式才能验证它的可信度。

峰值的计算，我就不说了，我觉得如果你是做性能的，应该一看就知道这个比例不符合大部分真实系统的逻辑。

有些人把这两个公式和 Little 定律做比较。我要说 Little 定律是最基础的排队论定律，并且这个定律只说明了：系统中物体的平均数量等于物体到达系统的平均速率和物体在系统中停留的平均时间的乘积。我觉得这句话，就跟秦腔中的”出门来只觉得脊背朝后“是一样一样的。

有人说应该如何来做系统容量的预估呢。我们现在很多系统的预估都是在一定的假设条件之下的，之所以是预估，说明系统还不在，或者还没达到那样的量。在这种情况下，我们可以根据现有的数据，做统计分析、做排队论模型，进而推导以后的系统容量。

但是我们所有做性能的人都应该知道，系统的容量是演进来的，而不是光凭预估就可以得出准确数值的。

总结

今天的这一篇和前两篇文章是一个体系，我利用这三篇文章对当前的性能测试市场上的一些关键概念进行一些拆解。性能测试策略、性能测试场景、性能测试指标，这些关键的概念在性能测试中深深地影响着很多人。我们简化它的逻辑，只需要记住几个关键字就可以，其他的都不必使用。

性能测试概念中：性能指标、性能模型、性能场景、性能监控、性能实施、性能报告。

性能场景中：基准场景、容量场景、稳定性场景、异常场景。

性能指标中：TPS、RT。 （记住 T 的定义是根据不同的目标来的）

有了这些之后，一个清晰的性能框架就已经出现了。

思考题

你能思考一下，为什么 258 响应时间不合理吗？像“业务模型用 28 原则”这些看似常识性的知识点，错在哪里呢？

欢迎你在评论区写下你的思考，我会和你一起交流，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

04丨JMeter和LoadRunner：要知道工具仅仅只是工具

做性能测试工作的人总是离不了性能测试工具，但当我们刚开始接触这类工具或者压测平台的时候，总是难免处在一种顾此失彼，焦虑又没想法的状态。

性能工程师的三大学习阶段

在我看来，对性能测试工程师本身来，多半会处在以下三个大的阶段。

性能工具学习期

JMeter 和 LoadRunner 是我们常用的两个性能测试工具。曾经有人问我，应该学 JMeter 还是 LoadRunner 呢？我反问的是，你学这样的工具需要多久呢？一般对方因为初学并不清楚要多久，然后我会告诉他，如果你是认真努力的，想要全职学习，那么我觉得一个工具，纯从功能的使用的角度来说，自学两个星期应该就差不多了。如果你是在工作中学习，那就更简单了，工作中需要什么就学习什么，不用纠结。

而应该纠结的是什么呢？当你把 JMeter、LoadRunner 的基本功能学会了，你会发现这些工具其实就做了两件事情，做脚本和发压力。

但问题在于，脚本的逻辑和压力场景的逻辑，和工具本身无关，和业务场景有关。这时你可能就会问，场景怎么配置呢？

这才进入到了另一个阶段。

通常在这个阶段的时候，你会觉得自己有非常明确的疑问，有经验的人可能一句话就可以指点你了，解决掉你的疑问，就是告诉你选择什么工具，如何来用。

性能场景学习期

第二个阶段就是性能场景学习期。我们平时在很多场合下所说的场景范围都有些狭隘，觉得场景就是业务比例，就是用多少数据。而实际做过多个性能项目之后，你就会发现，工具中的一个小小的配置，也会对结果产生巨大的影响。

比如说压力策略，应该用一秒 Ramp up 10 个用户，还是 20 个用户，还是 100 个用户？这应该怎么判断呢？

比如说，参数化数据应该用 100 条，还是 100 万条？还是有确定的值呢？有人说根据场景配置，可是根据什么样的场景怎么配置才合理呢？

比如说，在执行场景时应该看哪些数据？压力工具中的 TPS、响应时间这些常规数据都会去看，其他的还要看什么呢？这就涉及到了监控策略。

再比如说，业务应该用什么样的比例设置到压力工具中？有人说直接在线上做测试不是挺直接？但是你知道什么样的业务可以，什么样的业务不可以吗？如何控制线上的性能测试？

在性能场景学习期这个阶段，你关心的将不再是工具的使用操作，而是如何做一个合理的性能测试。你可以学会调整业务比例，并设计到压力工具中；你可以学会参数化数据的提取逻辑；你可以学会场景中要观察哪些数据。

按照这个思路，再做几个项目，你就会慢慢摸着一些门道。

性能分析学习期

学会使用工具了，也有了场景设计的经验，通过监控工具也拿到了一堆大大小小的数据。可是，数据也太多了，还在不断的变化。我又怎么判断性能瓶颈在哪里呢？

做性能的人都会有这样的一个茫然。当你把一个性能测试结果发给了别人，别人会顺理成章地去问你：“响应时间为什么这么长？有没有优化空间？”

听到这种问题，你有没有无助的感觉？心里台词是：“我怎么知道？”但是嘴上却不敢说出来，因为似乎这是我应该给出的答案？

但是当你尝试给出答案时，你就进入了一个大坑，对这个问题做出回答，近乎一个无底洞，需要太多的基础知识，需要很强的逻辑分析，需要清晰的判断思路。

如果你到了这个阶段，你可能会发现自己走得非常痛苦，好像自己也不知道自己会什么，不会什么。要说工具吧，也完全会用，场景吧，也会配置，但为什么就是不会分析结果，不会整理数据，不会下结论呢？

但实际上，我觉得你不要焦虑自己不会什么，而应该把目光聚焦到你要解决的问题上。问题的解决，靠的是思维逻辑，靠的是判断，而不是靠工具。

也就是说，这时面对问题，你应该说的是“我想要看什么数据”，而不是“把数据都给我看看”。

看到这里，希望你能清晰地理解这两者之间的区别。

公司性能团队成长阶段

我刚才分析了一下作为个人的性能工程师是如何一步步成长的，在实际工作中，我们更多的需要与团队合作，团队的成长与我们个人的成长息息相关。

对于一个公司的一个性能团队来说，大概会处在这些阶段。

性能团队初建

这时的团队，可以执行场景，可以拿出数据，但工作出的结果并不理想。团队整体的价值就体现在每天跟着版本跑来跑去，一轮轮地测试下去，一个版本短则一两个星期，长则一个月。没有时间去考虑测试结果对整个软件生命周期的价值，在各种琐碎的项目中疲于奔命。做脚本，拿出 TPS 和响应时间，做版本基线比对，出数据罗列式的性能测试报告。

唉，想想人生就这么过去了，真是心有不甘。这时有多少人希望能有一个性能测试平台来拯救团队啊。

性能团队初成熟

到了这个阶段，团队已经可以应付版本的更迭带来的性能工作压力，团队合作良好，稍有余力，开始考虑团队价值所在，在公司的组织结构中应该承担什么样的职责。在产品的流水线上终于可以占有一席之地了。这样很好，只是从实际的技术细节上来说，仍然没有摆脱第一阶段中琐碎的工作，没有把性能的价值体现出来，只是一个报告提供机器。

这时就需要考虑平台上是不是可以加个 SLA 来限制一下？在各个流程的关卡上，是不是可以做些性能标准？是不是该考虑下准入准出规则了？是的，这时一个团队开始慢慢走向成熟，站住脚之后要开始争取尊重了。

性能团队已成熟

有了标准、流程，团队的合作能力也成熟了之后，团队“是时候展示真正的实力了”。但问题来了，什么才是性能团队的真正实力呢？

直观上说，主要体现在一下几个方面。

1. 通过你的测试和分析优化之后，性能提升了多少？

这是一句非常简单直接的话。但是我相信有很多做性能测试工程师的人回答不出这样的问题。因为看着混乱的 TPS 曲线，自己都已经晕了，谁还知道性能提升了多少呢？

而一个成熟的团队应该回答的是：提升了 10 倍，我们调优了什么。这样的回答有理有据，底气十足。

2. 通过你的测试和分析优化之后，节省了多少成本？

这个问题就没有那么好回答了，因为你要知道整体的容量规划，线上的真实运营性能。如果之前的版本用了 200 台机器，而通过我们的测试分析优化之后，只用到了 100 台机器，那成本就很明显了。

但是，在我的职业生涯中，很少看到有人这样来体现性能存在的价值。有些场合是不需要这样体现，有些场合是不知道这样体现。

对个人以及团队来说，工具应该如何选择

理顺了性能测试工程师和性能团队的成长路径，下面我们来说说个人或者团队选择工具的时候，应该如何考量。

在我十几年工作的生涯中，可以说有很多性能工具都是知道的，但是要说起用得熟练的，也无非就是那几个市场占有率非常高的工具。

下面列一下市场上大大小小、老老少少、长长短短的性能测试工具，以备大家查阅。

市面上大大小小的性能测试工作一共有四十余种。这里面有收费的，也有免费的；有开源的，有闭源的；有新鲜的，有不新鲜的；有活跃的，有半死不活的；有可以监控系统资源的，有只能做压力发起的。

你是不是有一种生无可恋的感觉？一个性能测试而已，有必要搞出这么多工具吗？

然而，你要记住，这些都是压力发起工具。

下面我对一些比较常见的工具做下比对，这些工具主要包括 Apache JMeter、HP LoadRunner、Silk Performer、Gatling、nGrinder、Locust 和 Tsung。

仅比对这几个工具吧，因为从市场上来说，这几个算是经常看到的工具，以后我们再加入其他的工具和其他的属性。我们现在只说性能工具，不说一些企业做的性能平台云服务，因为云服务都是对企业来说的，我们放到后面再讲。

你从网络上可以很容易地找到这几个工具的特点，这几个都支持分布式。从上面那张表格中，你可以很容易对比出来，知道自己应该学什么工具了。

Gatling 有免费版和收费版，基于 Scala 语言，而 Scala 又是基于 Java 的，你看这复杂的关系就让人不想用，但是这个工具性能很高，虽说只支持 HTTP，但是由于支持 Akka Actors 和 Async IO，可以达到很高的性能。Actors 简化并发编译的异步消息特性让 Gatling 性能很高。

Locust 这个工具是基于 Python 的，中文名翻译过来就是蝗虫，这名字取得挺有意思。在一个压力场景下，对服务器来说确实就像一堆蝗虫来了。

对市场的占有率来说，JMeter 和 LoadRunner 以绝对的优势占据前两名，同时 JMeter 又以绝对的优势占据第一名。

下面来看一下，这两个工具的热度趋势。

这是全球范围近 5 年 JMeter 和 LoadRunner 热度（红色线是 LoadRunner，蓝色线是 JMeter）：

中国范围近 5 年 JMeter 和 LoadRunner 热度：

从上面的比对来看，我们可以很容易发现，近五年来，LoadRunner 就一直在走下坡路，而 JMeter 一直处在上升的趋势。

JMeter 和 LoadRunner 的历史兴衰

我下面只说一下 JMeter 和 LoadRunner 的历史，让你对性能工具的兴衰史有一定的了解。

先说说 LoadRunner 吧，应该说，LoadRunner 的历史，就是一段悲惨的回忆。2006 年 11 月份以前，在 Mercury 时代，LoadRunner 由于市场策略和工具优势很快占了第一名，势头很猛。当时还有另一个同样功能的工具 Silk Performer，被打压得几乎抬不起头来。06 年以后，Mercury 以 45 亿美元被 HP 收购，包括 QC、QTP 等工具。但从那之后，LoadRunner 的体积就在飞速膨胀，8.1 的 LoadRunner 只有 600M 左右（如果我没记错的话），经历了几个版本的迭代，LoadRunner 成功膨胀到 4 个 G，并在后面规划 performance center，在各地做质量中心。HP 这一步步走得理直气壮，把市场折腾没了。现在 LoadRunner 如果想装到一台压力机上，都是很吃力的事情。我要是用的话，宁愿在 XP 系统上安装 8.1 版本，速度飞快。2016 年，HPE 和 MicroFocus 合并，LoadRunner 也成了 MicroFous 产品线的一部分，搞到现在，在中国的市场依然疲软。

而拥有同样竞品工具 Silk Performer、Silk Test 和 Silk Test Manager 的 Segue 公司，同年仅以 1 亿美元被另一个企业 Borland 收购。3 年之后，Borland 连同自己，以 7500 万美元卖给了 MicroFocus。

至此，MicroFocus 同时拥有了 LoadRunner 和 Silk Performer。但可惜的是，这也照样干不过一个无心插柳柳成荫的开源工具 JMeter。

JMeter 的历史，可以说是屌丝逆袭的典型案例。1998 年，Apache 基金会的 Stefano Mazzocchi 是它最初的开发者，当时他只是为了测试 Apache JServlet 的性能（这个项目后来被 Tomcat 取代），后来 JMeter 被重构，又被用来测试 Tomcat。其实一开始，JMeter 的功能很简单。但是 Apache Tomcat 的势头实在是阻挡不住，再加上 Java 市场覆盖率实在是太高了，而 JMeter 做为一个开源免费的 Java 压力工具，有着众多的 contributors，顶着 Apache 的大旗，想失败都难。就像 ab 工具是为了测试 Apache HTTP server 一样，JMeter 应该说是和 Apache Tomcat 一起成长的。

与此同时，还有另一个 Java 开源工具 The Grinder，这个工具的主要贡献者是 Philip Aston、Calum Fitzgerald。

当时有一个开源测试平台叫 NGrinder，是韩国公司 NHN 开源的。有很多所谓企业内部自研发的性能测试平台，就是从 NGrinder 借鉴来的，而 NGrinder 就是以 The Grinder 为基础开发的。可惜的是，The Grinder 没有 Apache 这样的平台，作为一个很优秀的工具，它的维护更新还是不够快，不过 NGrinder 也给它带来了一定的荣耀。

到现在为止，JMeter 还没有一个非常成熟的云测试平台支撑它，只有一些商业公司改动，加一些管理和项目属性，做为企业内部平台使用。还有一些企业把 JMeter 改造成商业产品，加上云基础架构的管理功能，就成了一套完整的商业平台，再加上炫丽的操作页面，棒棒的，有没有？

那么你有没有想过，为什么没有以 JMeter 为基础的开源云测试平台呢？难道 JMeter 的热爱者看不到云测试平台的价值吗？在我看来，做为性能测试工具，它实在是没有必要做成一个开源的测试平台，因为轮子就是轮子，要装成什么样的车就自己装吧。要是再换个角度来说，性能测试真的有必要用平台吗？

使用性能测试工具的误区在哪里

现在很多人都是看互联网大厂的技术栈，但是有没有想过自己企业需要的到底是什么样的产品？曾经有个测试工程师跟我说，他们公司为了解决性能问题，特意买了压测云服务，花了 20 万，结果问题还是没找出来。

所以工具应该如何用，完全取决于用的人，而不是工具本身。

压测工具也好，压测平台也好，都没有一个工具可以直接告诉你瓶颈在哪里，能告诉你的只是数据是什么。分析只有靠自己，在这个过程中，我们也会用到很多的分析剖析工具，用这些工具的人也都会知道，工具也只提供数据，不会告诉你瓶颈点在哪里。

那这个时候就有人提出疑问了：“有些工具不是说，上了这个工具之后，耗时一眼看透嘛？”是的呀，关键是你看过是什么耗时了吗？给你一个 Java 栈，那么长的栈，每个方法的消耗都给你，但是长的就肯定有问题吗？

关于剖析工具的，我们后面再写。本篇重点在压测工具上。

有人说 JMeter BIO 有问题，应该用 AIO；有人说，压测工具没有后端系统性能监控数据，应该加一个监控插件。像 JMeter 中就有一个插件叫 perfmon，把后端的系统资源拉到 JMeter 的界面中来看。在这一点上，LoadRunner 老早就做过了，并且在之前的版本中还有个专门的组件叫 tuning，目的就是把后端所有的系统、应用、数据库都配置到一个架构图中，压力一发起，就把有问题的组件标红。想法很好，可是这个功能为什么没有被广泛使用？当然，后面被 HP 收购后，这和 HP 的市场策略有关，但是在收购前的 Mercury 时代，该功能也没有被广泛使用。

我们从实际的生产场景来看，压测工具模拟的是真实用户，而监控在哪里，在运维后台里，数据的流向都不一样。如果你使用压测工具的同时，也把它做为收集性能监控数据的工具，本身流量就会冲突。

所以在压测工具中同时收集监控计数器，就是不符合真实场景的。

这样压测平台就有出现的必要了，我们可以看到出现了五花八门的压测平台，也会有后端监控数据的曲线，乍看起来，就两个字：全面！

可是，同样也没有告诉你瓶颈在哪里。

如果选择合适自己的工具？

所以我们用工具，一定要知道几点：

工具能做什么？

工具不能做什么？

我们用工具的目标是什么？

当工具达不到目标时，我们怎么办？

把这几个问题在用工具之前就想清楚，才是个成熟的测试工程师，有这样的工程师的团队，才是成熟的性能测试团队（当然，成熟的测试团队还要有其他的技术）。

对企业，举例来说：

如果是一个需要支持万级、亿级 TPS 的电商网站，本身就是云基础架构，那么可能最简单的就是直接买这家的云压测工具就好了。

这样做的优点是不用再买机器做压力了。压力发起，主要就是靠压力机的量堆出来大并发。

但缺点也很明显，一是不能长期使用，长期用，费用就高了。二是数据也只能自己保存比对，如果测试和版本跨度大，还是要自己比对，无法自动比对。最后一个缺点就是 压力机不受控了。

所以如果有这样需求的企业，也基本上可以自己开发一套云压测工具了，从使用周期和长远的成本上来看，自已开发，都是最划算的。

如果是一个需要支持每秒 100TPS 的企业内部业务系统，就完全没必要买什么云服务了，自己找一台 4C8G 的机器，可能就压得够了。

这样的话完全可控，压测结果数据也都可以随时查看，可以留存。

如果是一个需要支持万级 TPS，但又不能用云服务的事业单位或政企，比如，军工业，那只能自己搭建一套测试环境了。这样做的优点是完全内部可控，数据非常安全，但缺点就是投入成本高。

对私企来说，开源永远是最好的选择，成本低，但是需要相关人员能力稍强一些，因为没有技术支持。

对政企和事业单位来说，收费是一个好的选择，因为有第三方服务可以叫过来随时支持。

对一个做短平快项目的企业来说，云服务会是一个好选择，成本低，不用长期维护数据。

对想做百年老店的企业来说，肯定是自己开发平台，尽量不选择云服务，因为技术是需要积累的。

对个人来说呢，不用举例，压测工具市场，现在肯定是首选学习 JMeter，其次是 LoadRunner。

JMeter 的势头已经很明显了，并且功能在慢慢扩展。开源免费是巨大的优势。

而 LoadRunner，不管它的市场现在有多凋零，它仍然是性能测试市场上，功能最为齐全的工具，没有之一。

总结

总体来说，性能测试工具的市场中，可以说现在的工具已经种类繁多了，并且各有优点。在项目中，根据具体的实施成本及企业中的规划，选择一个最适合的就可以了。也可以用它们来组建自己的平台。但是请注意， 不要觉得做平台可以解决性能测试的问题，其实平台只是解决了人工的成本。

如果单纯为了追潮流而把性能测试工具的使用成本升得特别高，那就不划算了。

思考题

今天的内容有点多，我提几个思考题，你就当是对文章的回顾吧。

你觉得企业选择性能工具应该考虑哪些方面呢？以及性能测试工具中是否必须做监控呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

05丨指标关系：你知道并发用户数应该怎么算吗？

我在性能综述的那三篇文章中，描述了各种指标，比如 TPS、RPS、QPS、HPS、CPM 等。我也强调了，我们在实际工作的时候，应该对这些概念有统一的认识。

这样的话，在使用过程中，一个团队或企业从上到下都具有同样的概念意识，就可以避免出现沟通上的偏差。

我说一个故事。

我以前接触过一个咨询项目。在我接触之前，性能测试团队一直给老板汇报着一个数据，那就是 10000TPS。并且在每个版本之后，都会出一个性能测试报告，老板一看，这个数据并没有少于 10000TPS，很好。 后来，我进去一看，他们一直提的这个 10000TPS 指的是单业务的订单，并且是最基础的订单逻辑。那么问题来了，如果混合起来会怎么样呢？于是我就让他们做个混合容量场景，显然，提容量不提混合，只说单接口的容量是不能满足生产环境要求的。

结果怎么样呢？只能测试到 6000TPS。于是我就要去跟老板解释说系统达到的指标是 6000TPS。老板就恼火了呀，同样的系统，以前报的一直是 10000TPS，现在怎么只有 6000TPS 了？不行，你们开发的这个版本肯定是有问题的。于是老板找到了研发 VP，研发 VP 找到了研发经理，研发经理找了研发组长，研发组长又找到了开发工程师，开发工程师找到了我。我说之前不是混合场景的结果，现在混合容量场景最多达到 6000TPS，你们可以自己来测。

然后证明，TPS 确实只能达到 6000。然后就是一轮又一轮的向上解释。

说这个故事是为了告诉你，你用 TPS 也好，RPS 也好，QPS 也好，甚至用西夏文来定义也不是不可以，只要在一个团队中，大家都懂就可以了。

但是，在性能市场上，我们总要用具有普适性的指标说明，而不是用混乱的体系。

在这里，我建议用 TPS 做为关键的性能指标。那么在今天的内容里，我们就要说明白 TPS 到底是什么。在第 3 篇文章中，我提到过在不同的测试目标中设置不同的事务，也就是 TPS 中的 T 要根据实际的业务产生变化。

那么问题又来了，TPS 和并发数是什么关系呢？ 在并发中谁来承载”并发“这个概念呢？

说到这个，我们先说一下所谓的“绝对并发”和“相对并发”这两个概念。绝对并发指的是同一时刻的并发数；相对并发指的是一个时间段内发生的事情。

你能详细说一下这两个概念之间的区别吗？如果说不出来那简直太正常了，因为这两个概念把事情说得更复杂了。

什么是并发

下面我们就来说一下“并发”这个概念。

我们假设上图中的这些小人是严格按照这个逻辑到达系统的，那显然，系统的绝对并发用户数是 4。如果描述 1 秒内的并发用户数，那就是 16。是不是显而易见？

但是，在实际的系统中，用户通常是这样分配的：

也就是说，这些用户会分布在系统中不同的服务、网络等对象中。这时候”绝对并发“这个概念就难描述了，你说的是哪部分的绝对并发呢？

要说积分服务，那是 2；要说库存服务，那是 5；要说订单服务，它自己是 5 个请求正在处理，但同时它又 hold 住了 5 个到库存服务的链接，因为要等着它返回之后，再返回给前端。所以将绝对并发细分下去之后，你会发现头都大了，不知道要描述什么了。

有人说，我们可以通过 CPU 啊，I/O 啊，或者内存来描述绝对并发，来看 CPU 在同一时刻处理的任务数。如果是这样的话，绝对并发还用算吗？那肯定是 CPU 的个数呀。有人说 CPU 1ns 就可以处理好多个任务了，这里的 1ns 也是时间段呀。要说绝对的某个时刻，任务数肯定不会大于 CPU 物理个数。

所以“绝对并发”这个概念，不管是用来描述硬件细化的层面，还是用来描述业务逻辑的层面，都是没什么意义的。

我们只要描述并发就好了，不用有“相对”和“绝对”的概念，这样可以简化沟通，也不会出错。

那么如何来描述上面的并发用户数呢？在这里我建议用 TPS 来承载“并发”这个概念。

并发数是 16TPS，就是 1 秒内整个系统处理了 16 个事务。

这样描述就够了，别纠结。

在线用户数、并发用户数怎么计算

那么新问题又来了，在线用户数和并发用户数应该如何算呢？下面我们接着来看示意图：

如上图所示，总共有 32 个用户进入了系统，但是绿色的用户并没有任何动作，那么显然，在线用户数是 32 个，并发用户数是 16 个，这时的并发度就是 50%。

但在一个系统中，通常都是下面这个样子的。

为了能 hold 住更多的用户，我们通常都会把一些数据放到 Redis 这样的缓存服务器中。所以在线用户数怎么算呢，如果仅从上面这种简单的图来看的话，其实就是缓存服务器能有多大，能 hold 住多少用户需要的数据。

最多再加上在超时路上的用户数。如下所示：

所以我们要是想知道在线的最大的用户数是多少，对于一个设计逻辑清晰的系统来说，不用测试就可以知道，直接拿缓存的内存来算就可以了。

假设一个用户进入系统之后，需要用 10k 内存来维护一个用户的信息，那么 10G 的内存就能 hold 住 1,048,576 个用户的数据，这就是最大在线用户数了。在实际的项目中，我们还会将超时放在一起来考虑。

但并发用户数不同，他们需要在系统中执行某个动作。我们要测试的重中之重，就是统计这些正在执行动作的并发用户数。

当我们统计生产环境中的在线用户数时，并发用户数也是要同时统计的。这里会涉及到一个概念：并发度。

要想计算并发用户和在线用户数之间的关系，都需要有并发度。

做性能的人都知道，我们有时会接到一个需求，那就是一定要测试出来系统最大在线用户数是多少。这个需求怎么做呢？

很多人都是通过加思考时间（有的压力工具中叫等待时间，Sleep 时间）来保持用户与系统之间的 session 不断，但实际上的并发度非常非常低。

我曾经看到一个小伙，在一台 4C8G 的笔记本上用 LoadRunner 跑了 1 万个用户，里面的 error 疯狂上涨，当然正常的事务也有。我问他，你这个场景有什么意义，这么多错？他说，老板要一个最大在线用户数。我说你这些都错了呀。他说，没事，我要的是 Running User 能达到最大就行，给老板交差。我只能默默地离开了。

这里有一个比较严重的理解误区，那就是压力工具中的线程或用户数到底是不是用来描述性能表现的？我们通过一个示意图来说明：

通过这个图，我们可以看到一个简单的计算逻辑：

如果有 10000 个在线用户数，同时并发度是 1%，那显然并发用户数就是 100。

如果每个线程的 20TPS，显然只需要 5 个线程就够了（请注意，这里说的线程指的是压力机的线程数）。

这时对 Server 来说，它处理的就是 100TPS，平均响应时间是 50ms。50ms 就是根据 1000ms/20TPS 得来的（请注意，这里说的平均响应时间会在一个区间内浮动，但只要 TPS 不变，这个平均响应时间就不会变）。

如果我们有两个 Server 线程来处理，那么一个线程就是 50TPS，这个很直接吧。

请大家注意，这里我有一个转换的细节，那就是并发用户数到压力机的并发线程数。这一步，我们通常怎么做呢？就是基准测试的第一步。关于这一点，我们在后续的场景中交待。

而我们通常说的“并发”这个词，依赖 TPS 来承载的时候，指的都是 Server 端的处理能力，并不是压力工具上的并发线程数。在上面的例子中，我们说的并发就是指服务器上 100TPS 的处理能力，而不是指 5 个压力机的并发线程数。请你切记这一点，以免沟通障碍。

在我带过的所有项目中，这都是一个沟通的前提。

所以，我一直在强调一点，这是一个基础的知识：不要在意你用的是什么压力工具，只要在意你服务端的处理能力就可以了。

示例

上面说了这么多，我们现在来看一个实例。这个例子很简单，就是：

JMeter（1 个线程） - Nginx - Tomcat - MySQL

通过上面的逻辑，我们先来看看 JMeter 的处理情况：

summary +   5922 in 00:00:30 =  197.4/s Avg:     4 Min:     0 Max:    26 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary =  35463 in 00:03:05 =  192.0/s Avg:     5 Min:     0 Max:   147 Err:     0 (0.00%)
summary +   5922 in 00:00:30 =  197.5/s Avg:     4 Min:     0 Max:    24 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary =  41385 in 00:03:35 =  192.8/s Avg:     5 Min:     0 Max:   147 Err:     0 (0.00%)
summary +   5808 in 00:00:30 =  193.6/s Avg:     5 Min:     0 Max:    25 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary =  47193 in 00:04:05 =  192.9/s Avg:     5 Min:     0 Max:   147 Err:     0 (0.00%)

我们可以看到，JMeter 的平均响应时间基本都在 5ms，因为只有一个压力机线程，所以它的 TPS 应该接近 1000ms/5ms=200TPS。从测试结果上来看，也确实是接近的。有人说为什么会少一点？因为这里算的是平均数，并且这个数据是 30s 刷新一次，用 30 秒的时间内完成的事务数除以 30s 得到的，但是如果事务还没有完成，就不会计算在内了；同时，如果在这段时间内有一两个时间长的事务，也会拉低 TPS。

那么对于服务端呢，我们来看看服务端线程的工作情况。

可以看到在服务端，我开了 5 个线程，但是服务端并没有一直干活，只有一个在干活的，其他的都处于空闲状态。

这是一种很合理的状态。但是你需要注意的是，这种合理的状态并不一定是对的性能状态。

并发用户数（TPS）是 193.6TPS。如果并发度为 5%，在线用户数就是 193.6/5%=3872。

响应时间是 5ms。

压力机并发线程数是 1。这一条，我们通常也不对非专业人士描述，只要性能测试工程师自己知道就可以了。

下面我们换一下场景，在压力机上启动 10 个线程。结果如下：

summary +  11742 in 00:00:30 =  391.3/s Avg:    25 Min:     0 Max:   335 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  55761 in 00:02:24 =  386.6/s Avg:    25 Min:     0 Max:   346 Err:     0 (0.00%)
summary +  11924 in 00:00:30 =  397.5/s Avg:    25 Min:     0 Max:    80 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  67685 in 00:02:54 =  388.5/s Avg:    25 Min:     0 Max:   346 Err:     0 (0.00%)
summary +  11884 in 00:00:30 =  396.2/s Avg:    25 Min:     0 Max:   240 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  79569 in 00:03:24 =  389.6/s Avg:    25 Min:     0 Max:   346 Err:     0 (0.00%)

平均响应时间在 25ms，我们来计算一处，(1000ms/25ms)*10=400TPS，而最新刷出来的一条是 396.2，是不是非常合理？

再回来看看服务端的线程：

同样是 5 个线程，现在就忙了很多。

并发用户数（TPS）是 396.2TPS。如果并发度为 5%，在线用户数就是 396.2/5%=7924。

响应时间是 25ms。

压力机并发线程数是 10。这一条，我们通常也不对非专业人士描述，只要性能测试工程师自己知道就可以了。

如果要有公式的话，这个计算公式将非常简单：

TPS=响应时间(单位ms)1000ms​∗压力机线程数

我不打算再将此公式复杂化，所以就不再用字母替代了。

这就是我经常提到的，对于压力工具来说，只要不报错，我们就关心 TPS 和响应时间就可以了，因为 TPS 反应出来的是和服务器对应的处理能力，至少压力线程数是多少，并不关键。我想这时会有人能想起来 JMeter 的 BIO 和 AIO 之争吧。

你也许会说，这个我理解了，服务端有多少个线程，就可以支持多少个压力机上的并发线程。但是这取决于 TPS 有多少，如果服务端处理的快，那压力机的并发线程就可以更多一些。

这个逻辑看似很合理，但是通常服务端都是有业务逻辑的，既然有业务逻辑，显然不会比压力机快。

应该说，服务端需要更多的线程来处理压力机线程发过来的请求。所以我们用几台压力机就可以压几十台服务端的性能了。

如果在一个微服务的系统中，因为每个服务都只做一件事情，拆分得很细，我们要注意整个系统的容量水位，而不是看某一个服务的能力，这就是拉平整个系统的容量。

我曾经看一个人做压力的时候，压力工具中要使用 4000 个线程，结果给服务端的 Tomcat 上也配置了 4000 个线程，结果 Tomcat 一启动，稍微有点访问，CS 就特别高，结果导致请求没处理多少，自己倒浪费了不少 CPU。

总结

通过示意图和示例，我描述了在线用户数、并发用户数、TPS（这里我们假设了一个用户只对应一个事务）、响应时间之间的关系。有几点需要强调：

通常所说的并发都是指服务端的并发，而不是指压力机上的并发线程数，因为服务端的并发才是服务器的处理能力。

性能中常说的并发，是用 TPS 这样的概念来承载具体数值的。

压力工具中的线程数、响应时间和 TPS 之间是有对应关系的。

这里既没有复杂的逻辑，也没有复杂的公式。希望你在性能项目中，能简化概念，注重实用性。

思考题

如果你吸收了今天的内容，不妨思考一下这几个问题：

如何理解“服务端的并发能力”这一描述？我为什么不提倡使用“绝对并发”和“相对并发”的概念呢？以及，我们为什么不推荐用 CPU 来计算并发数？

06丨倾囊相授：我毕生所学的性能分析思路都在这里了

我还年轻的时候，经常听一些大会或者演讲。有些人说，思路逻辑非常重要。我那时就想，你肯定是瞎忽悠的，因为我怎么就没听懂你说的思路呢？

而现在轮到自己来写或者讲一些东西的时候，才发现他们说得很对，而我之所以不理解，也是有原因的。

性能分析思路和具体的实现之间，有一道鸿沟，那就是操作的能力。之前我为什么听不懂那些人的思路，其实是因为我没有操作的功底。

而有了操作的功底之后，还有一个大的鸿沟要越过去，那就是从操作到对监控计数器的理解。这一步可以说让很多性能测试人员都望而却步了。

但是这还不算完，这一步迈过去之后，还有一个跳跃，就是相关性分析和证据链分析的过程。

如此一来，就会得到一张性能测试分析的能力阶梯视图，如下：

工具操作：包括压力工具、监控工具、剖析工具、调试工具。

数值理解：包括上面工具中所有输出的数据。

趋势分析、相关性分析、证据链分析：就是理解了工具产生的数值之后，还要把它们的逻辑关系想明白。这才是性能测试分析中最重要的一环。

最后才是调优：有了第 3 步之后，调优的方案策略就有很多种了，具体选择取决于调优成本和产生的效果。

那么怎么把这些内容都融会贯通呢？下面我们就来说说性能测试分析的几个重要环节。

应该说，从我十几年的性能工作中，上面讲的这些内容是我觉得最有价值的内容了。在今天的文章中，我们将对它做一次系统的说明。我先把性能分析思路大纲列在这里：

瓶颈的精准判断；

线程递增的策略；

性能衰减的过程；

响应时间的拆分；

构建分析决策树；

场景的比对。

瓶颈的精准判断

TPS 曲线

对性能瓶颈做出判断是性能分析的第一步，有了问题才能分析调优。

之前有很多人在描述性能测试的过程中，说要找到性能测试中曲线上的“拐点”。我也有明确说过，大部分系统其实是没有明确的拐点的。

举例来说，TPS 的视图如下：

TPS 图 1

显然，这是一个阶梯式增加的场景，非常好。但是拐点在哪呢？有人说，显然在 1200TPS 左右的时候。也有人说了，显然是到 1500TPS 才是拐点呀。但是也有人说，这都已经能到 2000TPS 了，显然 2000TPS 是拐点。

我们再来看一下这张图对应的响应时间视图：

响应时间图 1

是不是有人要说响应时间为 4.5ms 时是拐点了？

其实这些对拐点的判断，都是不合理的。如果我们对 TPS 的增加控制得更为精准的话，那么这个 TPS 的增加是有一个有清晰的弧度，而不是有一个非常清晰的拐点。

但是至少我们可以有一个非常明确的判断，那就是瓶颈在第二个压力阶梯上已经出现了。因为响应时间增加了，TPS 增加得却没有那么多，到第三个阶梯时，显然增加的 TPS 更少了，响应时间也在不断地增加，所以，性能瓶颈在加剧，越往后就越明显。

那么我们的判断就是：

有瓶颈！

瓶颈和压力有关。

压力呈阶梯，并且增长幅度在衰减。

如果你觉得上面的瓶颈还算清晰的话，那么我们再来看一张图：

TPS 图 2

在这个 TPS 的曲线中，你还能判断出拐点在哪吗？

显然是判断不出来拐点的，但是我们根据图得出以下几个结论：

有瓶颈！

瓶颈和压力有关。

压力也是阶梯的，但是并没有明确的拐点。

我们再来看一个 TPS 图：

TPS 图 3

看到这张图，是不是明显感觉系统有瓶颈呢？那么瓶颈是不是和压力大小有关呢？

这种比较有规律的问题，显然不是压力大小的原因。为什么呢？因为 TPS 周期性地出现降低，并且最大的 TPS 也都恢复到了差不多的水位上。所以，即使是压力降低，也最多降低最大的 TPS 水位，会让问题出现得更晚一点，但是不会不出现。

综合以上，如果画一个示意图的话，TPS 的衰减过程大概会如下所示：

随着用户数的增加，响应时间也在缓慢增加。

TPS 前期一直都有增加，但是增加的幅度在变缓，直到变平。

在这样的趋势图中，我们是看不到明确的拐点的。但是我们能做的清晰的判断就是：有瓶颈！

所以对 TPS 曲线来说，它可以明确告诉我们的就是：

有没有瓶颈：其实准确说所有的系统都有性能瓶颈，只看我们在哪个量级在做性能测试了。

瓶颈和压力有没有关系：TPS 随着压力的变化而变化，那就是有关系。不管压力增不增加，TPS 都会出现曲线趋势问题，那就是无关。

这时你可能会问，为什么不看响应时间就武断地下此结论呢？其实响应时间是用来判断业务有多快的，而 TPS 才是用来判断容量有多大的。

响应时间的曲线

我们还是来看看响应时间，下面看一张响应时间图：

它对应的线程图是：

多明显的问题，随着线程的增多，响应时间也在增加，是吧。再来看它们对应的 TPS 图：

到第 40 个线程时，TPS 基本上达到上限，为 2500 左右。响应时间随着线程数的增加而增加了，系统的瓶颈显而易见地出现了。

但是，如果只让你看 TPS 曲线，你是不是也会有同样的判断？那就是：有瓶颈！并且和压力有关？所以说，其实 TPS 就可以告诉我们系统有没有瓶颈了，而响应时间是用来判断业务有多快的。

后面我们还会提到响应时间会是性能分析调优的重要分析对象。

线程递增的策略

讲完响应时间之后，我们再来看下线程递增。

在见识了很多性能测试人员做的场景之后，必须得承认，有些场景的问题太多了。

首先，我们来看两个场景的执行对比。

场景 1 的线程图：

场景 1 的 TPS 图：

场景 1 的响应时间图：

场景 2 的线程图：

场景 2 的 TPS 图：

场景 2 的响应时间图：

这两个场景的比对如下：

有了这些对比数据之后，你是不是觉得哪里似乎是有问题的？

对的！

TPS 都是达到 400，但两个场景中线程递增的策略不同，产生的响应时间完全不同。虽然都没有报错，但是第一种场景是完全不符合真实的业务场景的。这是为什么呢？

在场景的执行过程中，首先，响应时间应该是从低到高的，而在场景 1 中不是这样。其次，线程应该是递增的，而场景 1 并没有这样做（这里或许有人会想到秒杀的场景，认为场景 1 符合秒杀的业务设定，这个问题我们稍后提及）。最后，在两个场景中，TPS 的上限都达到了 400TPS。但是你可以看到，在场景 2 中，只要 40 个线程即可达到，但场景 1 中居然用到了 500 线程，显然压力过大，所以响应时间才那么长。

其实在生产环境中，像场景 1 这样的情形是不会出现的。如果它出现了，那就是你作为性能测试的责任，因为你没有给出生产环境中应该如何控制流量的参数配置说明。

同时，我们从上面的场景对比可以看到，对一个系统来说，如果仅在改变压力策略（其他的条件比如环境、数据、软硬件配置等都不变）的情况下，系统的最大 TPS 上限是固定的。

场景 2 使用了递增的策略，在每个阶梯递增的过程中，出现了抖动，这就明显是系统设置的不合理导致的。设置不合理，有两种可能性：1. 资源的动态分配不合理，像后端线程池、内存、缓存等等；2. 数据没有预热。

我们再回到之前说的秒杀场景。

说到秒杀场景，有人觉得用大线程并发是合理的，其实这属于认识上的错误。因为即使线程数增加得再多，对已经达到 TPS 上限的系统来说，除了会增加响应时间之外，并无其他作用。所以我们描述系统的容量是用系统当前能处理的业务量（你用 TPS 也好，RPS 也好，HPS 也好，它们都是用来描述服务端的处理能力的），而不是压力工具中的线程数。这一点，我在第 5 篇文章中已经做了详细的解析，你可以回去再看看。

那么，对于场景中线程（有些工具中叫虚拟用户）递增的策略，我们要做到以下几点：

场景中的线程递增一定是连续的，并且在递增的过程中也是有梯度的。

场景中的线程递增一定要和 TPS 的递增有比例关系，而不是突然达到最上限。后面在场景的篇幅中我们会再说它们之间的比例关系。

上面两点针对的是常规的性能场景。对于秒杀类的场景，我们前期一定是做好了系统预热的工作的，在预热之后，线程突增产生的压力，也是在可处理范围的。这时，我们可以设计线程突增的场景来看系统瞬间的处理能力。如果不能模拟出秒杀的陡增，就是不合理的场景。

这里给出我做性能场景递增的经验值：

当然这里也不会是放在哪个系统中都适合的递增幅度，你还是要根据实际的测试过程来做相应的判断。

有了这些判断之后，相信大家都能做出合理的场景来了。

性能衰减的过程

有了瓶颈的判断能力，也有了线程递增的意识，那么下面在场景执行中，我们就要有判断性能衰减的能力了吧。

来，我们先看一个压力过程中产生的结果图。

在递增的压力过程中，随着用户数的增加。我们可以做几次计算。

第一次计算，在线程达到 24 时，TPS 为 1810.6，也就是每线程每秒发出 75.44 个请求。

第二次计算，在线程达到 72 时，TPS 为 4375.1，也就是每线程每秒发出 60.77 个请求。

第三次计算，在线程达到 137 时，TPS 为 5034，也就是每线程每秒发出 36.74 个请求。

通过这三次计算，我们是不是可以看到，每线程每秒发出的请求数在变少，但是整体 TPS 是在增加的。

我们有很多做性能测试的人，基本上，只看 TPS 和响应时间的时候，在上面这个示例中，肯定会一直往上加用户。虽然响应时间在增加，但是增加得也不多嘛。

但实际上，通过我们的计算可以知道，性能是在不断地衰减的。我们来看一张统计图：

通过红线的大致比对可以知道，当每线程每秒的请求数降到 55 左右的时候，TPS 就达到上限了，大概在 5000 左右，再接着往上增加线程已经没有用了，响应时间开始往上增加了。

这就是性能衰减的过程（题外话，在上图中，其实还有一个问题，就是在红线前面，性能在上升的过程中有几次抖动，这个抖动到后面变大了，也变频繁了，如果这是必然出现的抖动，那也是配置问题，希望你注意到这一点）。

为什么要这么细致地描述性能衰减的过程呢？

其实我就是想告诉你，只要每线程每秒的 TPS 开始变少，就意味着性能瓶颈已经出现了。但是瓶颈出现之后，并不是说服务器的处理能力（这里我们用 TPS 来描述）会下降，应该说 TPS 仍然会上升，在性能不断衰减的过程中，TPS 就会达到上限。

这也是前面我说的，性能瓶颈其实在最大 TPS 之前早就已经出现了。

那么我们是不是应该在性能衰减到最大 TPS 时就停止场景呢？这个不一定的哦。

因为停不停场景，取决于我们的场景目标，如果我们只是为了得到最大 TPS，那确实可以停止场景了。但是，如果我们要扩大化性能瓶颈，也就是说为了让瓶颈更为明显，就完全不需要停止场景，只要不报错，就接着往上压，一直压到我们要说的下一个话题——响应时间变长，需要拆分。

响应时间的拆分

在性能分析中，响应时间的拆分通常是一个分析起点。因为在性能场景中，不管是什么原因，只要系统达到了瓶颈，再接着增加压力，肯定会导致响应时间的上升，直到超时为止。

在判断了瓶颈之后，我们需要找到问题出现在什么地方。在压力工具上看到的响应时间，都是经过了后端的每一个系统的。

那么，当响应时间变长，我们就要知道，它在哪个阶段时间变长了。

我们看下这张图。

这应该是最简单的一个压力测试逻辑了。一个应用，一个 DB，结果也拆分出了 8 个时间段，这还是在我没有加上压力工具自己所消耗的时间的情况下。

如果我们要分析压力工具中的响应时间，拆分的逻辑就是上面这个示意图。

但是在真实的场景中，基本上不是这样的。如果是内网，那基本上都是连在一个交换机上，所以通常是这样的：

在这样的拓扑中，我们仍然可以拆出来 t1 到 t8 的时间。只是实际动手的时候，思路一定要清晰，时间拆分是从哪里到哪里，要画出来，不能混乱。

我们有很多手段可以进行时间的拆分，当然要看我们的应用支持哪一种。

如果我们是这样的架构，拆分时间应该是比较清楚的。

首先我们需要查看 Nginx 上的时间。日志里就可以通过配置 requestt​imeupstream_response_time 得到日志如下信息：

14.131.17.129 - - [09/Dec/2019:08:08:09 +0000] "GET / HTTP/1.1" 200 25317 0.028 0.028

最后两列中，前面是请求时间的 28ms，后面是后端响应时间的 28ms。

同时，我们再到 Tomcat 上去看时间。

172.18.0.1 - - [09/Dec/2019:08:08:09 +0000] "GET / HTTP/1.1" 200 25317 28 27 http-nio-8080-exec-1

请求时间消耗了 28ms，响应时间消耗了 27ms。

接着再来看一下前端的时间消耗。

从这里可以看到，从发出请求到接收到第一个字节，即 TTFB 是 55.01ms，内容下载用了 11.75ms。从这就可以看得出 Nginx 基本上没消耗时间，因为它和 Tomcat 上的请求响应时间非常接近。

那么网络上的消耗时间怎么样呢？我看到有很多人用 TTFB 来描述网络的时间。先来说明一下，TTFB 中显然包括了后端一系列处理和网络传输的时间。如下图所示。

下面的红色点是指要接收的内容。上面的红色线就是 TTFB。

如果接收完了呢？就是这个状态。

所以，我觉得用 TTFB 描述网络的健康状态并不合理。如果用 Content Download 来描述会更为合理。比如我们上面的这个例子中，那就是 11.75ms 下载了 25317 Bytes 的内容。

Tomcat 上基本上是消耗了处理的所有时间，当然这中间也包括了 MySQL 花费的时间。而前端看到的其他时间就消耗在了网络中。

在这个例子中，主要说明了响应时间怎么一步步拆。当然，如果你是下面这种情况的话，再一个个拆就比较辛苦了，需要换另一种方式。

你肯定想知道每个系统消耗了多长时间，那么我们就需要链路监控工具来拆分时间了。比如像这样来拆分：

从 User 开始，每个服务之间的调用时间，都需要看看时间消耗的监控。这就是时间拆分的一种方式。

其实不管我们用什么样的工具来监控，最终我们想得到的无非是每个环节消耗了多长时间。用日志也好，用链路监控工具也好，甚至抓包都可以。

当我们拆分到了某个环节之后，就有了下一步的动作：构建分析决策树。

构建分析决策树

关于分析决策树，我在很多场合也都有提及。

分析决策树，对性能测试分析人员实在是太重要了，是性能分析中不可或缺的一环。它是对架构的梳理，是对系统的梳理，是对问题的梳理，是对查找证据链过程的梳理，是对分析思路的梳理。它起的是纵观全局，高屋建瓴的指导作用。

性能做到了艺术的层级之后，分析决策树就是提炼出来的，可以触类旁通的方法论。

而我要在这里跟你讲的，就是这样的方法论。

应该说，所有的技术行业在面对自己的问题时，都需要有分析决策树。再广而推之的话，所有的问题都要有分析决策树来协助。

通过上面的几个步骤，我们就会知道时间消耗在了哪个节点上。那么之后呢？又当如何？

总要找到根本的原因才可以吧，我画了如下的分析决策图：

从压力工具中，只需要知道 TPS、响应时间和错误率三条曲线，就可以明确判断瓶颈是否存在。再通过分段分层策略，结合监控平台、日志平台，或者其他的实时分析平台，知道架构中的哪个环节有问题，然后再根据更细化的架构图一一拆解下去。

我在这里，以数据库分析和操作系统分析举一下例子。

首先我们看一下数据库分析决策树。

比如针对 RDBMS 中的 MySQL，我们就可以画一个如下的决策树：

由于这里面的内容实在过多，无法一次性展现在这里。我举几个具体的例子给你说明一下。

MySQL 中的索引统计信息有配置值，有状态值。我们要根据具体的结果来判断是否需要增加 key_buffer_size 值的大小。比如这种就无所谓了。

    Buffer used     3.00k of   8.00M  %Used:   0.04

从上面的数据可以看到，key buffer size 就用到了 4%，显然不用增加。

再比如，我们看到这样的数据：

       __Tables_______________________
    Open             2000 of 2000    %Cache: 100.00
    Opened         15.99M     4.1/s

这就明显有问题了。配置值为 2000 的 Open Table Cache，已经被占满了。显然这里需要分析。但是，看到状态值达到配置值并不意味着我们需要赶紧加大配置值，而是要分析是否合理，再做相应的处理。比如说上面这个，Table 确实打开得多，但是如果我们再对应看下这一条。

    Slow 2 s        6.21M     1.6/s

你是不是觉得应该先去处理慢 SQL 的问题了？

关于数据库的我们就不举更多的例子了。在这里只是为了告诉你，在分析决策树的创建过程中，有非常多的相互依赖关系。

然后我们再来看一下操作系统分析决策树，我在这里需要强调一下，操作系统的分析决策树，不可以绕过。

如果你想到操作系统架构图就头大，那么这时候应该觉得有了希望。那就是我觉得操作系统上的问题判断是比较清晰的，所以基于此决策树，每个人都可以做到对操作系统中性能问题的证据链查找。

但是！对嘛，总得有个但是。

对操作系统的理解是个必然的前提。我看过很多人写的操作系统性能分析方面的书籍或资料，发现大部分人把描述计数器的数值当成性能分析。

怎么理解这句话呢？比如说

“CPU 使用率在 TPS 上升的过程中，从 10% 增加到 95%，超过了预期值。” “内存使用率达到 99%，所以是瓶颈点。” “I/O 使用率达到 100%。” 等等。

像这样的描述，在我的性能团队中，一定会被骂回去重写。我要这些描述有什么用？我要的是为什么达到了这样的值，原因在哪？怎么解决？

就像分析决策树中所描述的那样，性能工程师要做的是一步步地细化分析，给出最终的原因。

有人说，如果按这个路子，似乎操作系统的分析并不复杂嘛。大概三五个命令就可以跳到代码层了。是的，对于操作来说，确实不多，但是对于判断来说，那就复杂了。举个例子来说明一下：

看到这样的图，你是不是有种手足无措的感觉？中断能占 40%，sy CPU 也能占 40%。这系统还用干业务的事吗？全干自己的事去了，可见操作系统有问题！你是不是要做这个判断了？

而实际情况是，这个主机上只有一个网卡队列，而请求量又比较大。

所以要解决的是网卡队列的问题，至于怎么解决，那手段就多了。可以换个服务器，可以多加几个队列，可以多接几个节点…

以上只是给出几个性能分析过程中常见的决策树示例。在后续的分析过程实例中，我们将秉承着这种分析思路，一步步地走到瓶颈的面前。

场景的比对

为什么要写这一部分呢？因为我看到很多人对瓶颈的判断，并不那么精确，所以想写一下场景比对的建议。

其实简单来说，就一句话：当你觉得系统中哪个环节不行的时候， 又没能力分析它，你可以直接做该环节的增加。

举例来，我们现在有一个如下的架构：

可以得到这样的结果：

从 TPS 曲线中，我们可以明显看到系统是有瓶颈的，但是并不知道在哪里。鉴于系统架构如此简单，我们索性直接在某环节上加上一台服务器，变成这样：

然后得到如下数据：

哟，没好使！

怎么办？再接着加其他节点，我加了更多的 JMeter 机器。

再来看下结果：

真巧，TPS 增加了！

看到了吧，这就是我说的场景比对。

当我们不知道系统中哪个环节存在性能瓶颈时，对架构并不复杂的系统来说，可以使用这样的手段，来做替换法，以快速定位问题。

总结

在这一篇中，我说到了瓶颈的精准判断、线程递增的策略、性能衰减的过程、响应时间的拆分、构建分析决策树以及场景的比对，这几个环节，是性能分析过程中非常重要的环节。

从我的经验上来说，这一篇文章可能是我工作十几年的精华所在了。而这里的每一个环节，又有非常多的细分，特别是构建分析决策树这一块，它需要太多的架构知识、系统知识、数据库知识等等。鉴于本文只是想起到一个提纲挈领的作用，所以无法展开描述，希望在后续的篇幅中，我们尽量细致拆解。

思考题

今天的内容虽然有点多，但总的来说，思路比较清晰，理解起来也比较容易。如果你认真学习了今天的内容，不妨思考两个问题，为什么线程递增过程不能断？构建分析决策树的关键是什么？

欢迎你在评论区写下你的思考，我会和你一起交流，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

07丨性能测试工具：如何录制脚本？

对于一个性能测试工具来说，如果能实现以下几大功能，那么就基本上就满足了性能测试工具的功能。

录制或编写脚本功能

参数化功能

关联功能

场景功能

报告生成功能

但是除此以外，在工作的细节上还有更多要求，就要看工具的实施能力了。

有很多性能测试工程师希望工具能做得非常全面，又人性化，而纵观当前的性能工具，真正能够做到傻瓜式录制完脚本，自动设置好参数化、关联、场景，直接产出结果的工具是没有的。不管是云性能测试平台，还是分布式性能测试工具（当然性能测试工具几乎全部具有分布式能力），都需要性能测试人员来定义参数化数据、设置关联、配置场景。

因此，在性能测试的过程中，对工具的配置就成为了性能测试工程师的基本能力。

今天，我们就来看下在性能测试工具中，如何录制脚本。今天的文章有些特殊，可能是专栏中少有的，有详细操作的文章。

性能工具的脚本能力

性能测试工具的脚本编写能力分为两类，一个是录制，另一个是手工编写。

现在市场上的性能测试工具虽然支持录制功能，但大部分也只是支持 HTTP 协议。在我们熟知的工具中，也只有 LoadRunner 支持更多协议的录制能力。不过幸好，现在我们所面对的应用大部分是 HTTP 协议的应用。

对手工编写脚本的部分，因为大部分都取决于业务场景，所以很难提出共性。如果有人提出针对性的场景，我们再做相应的示例就行。

因此今天的文章将着重讲一下测试工具的录制功能。很多人以为性能工具录制功能非常简单，点几下就能生成一个脚本，但是录制完之后，针对脚本的增强完善就做得非常少了。事实上，针对脚本，我们不仅要录制下来，还要了解录制的原理和录制完之后的脚本增强。不然，在场景中还是会遇到各种各样的问题。

性能工具中的录制功能

录制功能从原理上来说，分成两种：

本地录制：通过截取并解析与服务器的交互协议包，生成脚本文件。比如说 LoadRunner 调起 IE 的时候，不用修改 IE 的代理设置，就可以直接抓取 HTTP 包，并通过自己的解析器解析成脚本。

代理录制：通过代理服务器设置，转发客户端和服务器的交互协议包，生成脚本文件。JMeter 中的脚本录制功能就是这样做的。

这两者的不同点主要在于操作上。本地录制相对简单，但有些场景受限，比如说操作只能在某台服务器上，但是这台服务器又不允许安装工具；代理录制操作复杂一些，但可以满足更多的场景。

通过这张图，我们可以简单看到代理录制的逻辑：

我们在 IP 为 2.2.2.2 上的主机上，打开一个代理程序，开 81 端口，所有到 81 端口的都转发到 1.1.1.1 的 80 端口。

当 3.3.3.3 主机要访问 1.1.1.1 的的 80 端口，可以通过访问 2.2.2.2 的 81 端口进行转发。

这里需要你注意的是，代理是用来转发数据包的，并不是重定向哦。不管是在本机用代理，还是远程用代理，这个逻辑都是不会变的。

有了这个逻辑之后，你要明白的一点是，客户机不一定要和代理服务器在同一台机器上。

为什么要强调这一点呢？因为有很多人用工具来录制时，都不知道这个逻辑，只知道工具是那么操作的。这也是很多人不能理解 Port mapping 的原因。

不同的工具录制方式略有不同。今天我们用常见的两个性能测试工具 LoadRunner 和 JMeter 做为示例工具。

JMeter 的录制功能

首先打开 JMeter，添加一个线程组，再添加一个 HTTP(S) Test Script Recorder。界面如下：

这里有几个关键点说明一下：

Target Controller：这里指定录制出的脚本要放到哪里去。如果你想把不同的脚本放到不同的线程组中去，在录制的时候就可以拆分开。

Grouping：分组，这个分组功能很实用。但是如何分组就和具体的目标相关了，这一点下面我们再细说。

点击 start 按钮时，会提示创建一个根 CA 证书。这个证书生成在 bin 目录中，文件名是：ApacheJMeterTemporaryRootCA.crt，七天有效期。这个证书将被用来客户端转发 HTTPS 的请求。与此同时，还有另一个证书在同目录中生成，名字是 proxyserver.jks，这是 JMeter 自己生成的根证书。

前面我们说到了，JMeter 是用代理的方式来录制的。如果服务端用了 SSL 证书，在代理时也要加 SSL 证书，那么代理录制的结构就会变成这样。

上面的 SSL 证书就是用来处理上图中蓝色的这一部分。

我们点击 ok 之后，就会出现这个界面。在这个界面中，只有两个配置项。

Prefix：请求名的前缀。

Create new transaction after request(ms)：一个请求完成之后，如果下一个请求超出了这里设置的时间间隔，就创建一个新的事务。

然后到主机上设置代理。

注意，这里我要敲黑板了呀：这里的代理设置，是在需要访问的客户机上。这个客户机，不一定是压力机所在的机器。这里的 localhost，也应该设置的是代理服务所在的主机 IP。

请注意，如果你要设置为录制 HTTPS，还需要做如下两步。

第一步是，浏览器代理要把 Secure Web Proxy(HTTPS) 选择上，同时填上相应的代理 IP 和端口，下图是 macOS 上的图示。

但你会发现，这时仍然录制不了 HTTPS 应用，访问时会出现如下提示：

这时就要在客户端机器上导入上面提到的 ApacheJMeterTemporaryRootCA.crt。我们打开证书管理软件，在 macOS 上是 Keychain Access，Windows 上是 certmgr.msc。

这里以 macOS 为例。

首先打开 Keychain Access。

点击上图中的 Import Items。选择 ApacheJMeterTemporaryRootCA.crt，导入之后选择证书。会看到如下提示：

因为这个证书不在系统信任的默认列表里，所以会提示证书不可信。

另外这里我可以再多说一句，你注意的是，全球的可信任的根证书都是默认添加到系统中的，如果你在访问网站时，提示你要安装什么证书，一定要明确知道证书是从哪来的，不要随意安装未知来源的证书。目前国内的 HTTPS 覆盖度不高，仍然有大量的 HTTP 网页，这是需要推进的网络安全之一。

然后我们双击此证书。

改为 Always Trust 即可。提示如下：

这时，HTTP 和 HTTPS 都会被录制下来。然后在客户机上打开浏览器，访问你的页面，这样就录制到脚本了。

下面我们再来说下 Grouping 这个功能。

Grouping 的设置有如下几种，如果需要将脚本分开，先确定需要如何拆分。示例如下：

第一个选项是 Do not group samples，也就是不分组。

这是很多人使用的默认选项，这就相当于没有事务的概念了，每个请求都会单独统计 TPS 和响应时间信息。

第二个选项是 Add separators between groups，在组间添加分隔，就为了好看！

第三个选项是，Put each group in a new controller，每个组放一个新的控制器。这是一个 Simple Controller，它的作用也是只有一个：就为了好看！

因为脚本太长了，看起来不方便，所以分个组，看着清晰一些。话说回来，你们见过在 JMeter 中有很长脚本的吗？是不是很多人都没有见过？

第四个选项是，Put each group in a new transaction controller，将每个组放入一个新的事务控制器中。

Transaction Controller 和 Simple Controller 的区别就是 Transaction Controller 会做为事务统计脚本执行的时间，而 Simple controller 不会。

第五个选项是 Store 1st sampler for each group only，只存储每个组的第一个样本。

网上大部分都只描述了上面这句，但是请注意我这里还有一句关键的：从 HTML 文件获取所有内含的资源和自动重定向将开启。也就是说，虽说只记录了一个 Sampler，但是资源也会下载，重定向也会开启。

我们把这个过程抓出来看一下，因为 JMeter 没有把这个过程显示出来。所以这里用 Chrome Developer Tool 抓一下看看。举例来说，我们在浏览器里只输入了一个 https://www.jd.com。抓出如下结果。

在上面的图中，你可以看到，www.jd.com，第一个就是 307 Internal Redirect。接着请求 Document，然后下面是静态资源。在录制时，选择 Store 1st sampler for each group only 之后，只会录制到第一个请求，而后面这些在回放脚本时也都会访问。

在 JMeter 的代理录制中，还有一个界面如下：

中文界面中通常将之翻译为包含模式、排除模式。“模式”一词一加就显得格外高大上了。

通常这里都会写上正则表达式，比如说常用的一些：

.*
.*.png
.*.gif
.*.jpg
.*.php
.*.jsp
.*.html
.*.htm
.*.js
..(js|css|PNG|jpg|ico|png|gif).

由于正则是一个很大的话题，这里我们就不展开了，只要你懂正则，在这里就可以适用。

通过上面的内容，我们已经把 JMeter 录制的原理和操作的过程都详细地描述了一遍，关于 JMeter 的录制功能，就介绍到这里。

在此重点提醒你一下，录制是通过代理做的，一定要知道代理的原理，代理就是转发的功能。

承上启下的话

为什么 JMeter 这样的功能单一，性能又不好的性能测试工具能这么快的占领市场呢？

在我看来，工具能不能用取决于它能不能满足需要。在很多的性能测试场景中，JMeter 已经够用了。因为性能压力工具只需要两条曲线：TPS 和响应时间（如果出错最多就再看一下错误率曲线）。这些功能，JMeter 都可以提供。

现在的性能项目中，我们要的压力其实并没有很大，并且大部分都是 HTTP、TCP 之类的常见协议，脚本所使用的资源并不多。一般能达到万级 TPS 的都很少很少，所以弄几个机器，JMeter 也就够用了，再加上免费开源，何乐而不为呢？

而 LoadRunner 的失败之处就是价格高，更新慢。一想到 HP 糟蹋了 LoadRunner，我就伤心落泪。

LoadRunner 中的录制功能

我们都知道 LoadRunner 其实可以录制很多协议，这也是它前期扩展市场的很重要的功能。应该说，在录制这个功能点上，所有的性能测试工具都不如 LoadRunner。并且 LoadRunner 在其他很多功能上都是强大的，强大到什么程度呢？就是有很多你不需要的，不常用的功能，它都具备。

很多人都知道，LoadRunner 中的 Vuser Generator 只支持 Windows。你有没有想过这是为什么？其实解释起来也简单，LoadRunner 一开始是基于 WinInet 做的，就是 Windows Internet API。后来可能是觉得 WinInet 太恶心了，于是换成了 Windows socket。而 Windows socket 跟 UNIX socket 还是有一些小区别。

所以从历史延续下来，Vuser Gnenerator 就一直在 Windows 上了。

为什么不做 UNIX 的版本呢？其实在我看来，完全没有这个必要。因为 Load Generator 已经支持 UNIX 了。从使用的角度说，Vuser Gnenerator 没有必要做 UNIX 的版本，因为它还有 Port Mapping 的功能，这样在 UNIX 上的操作也照样录得下来。。

下面我们就单说 LoadRunner 的录制功能。

常规录制

首先，我们打开 Vuser Generator，点击 Start Record，出现如下界面：

在这个图中，首先的选择是：

这里用 IE 或者应用程序都可以，只要支持我们选择的 HTTP 协议就行。

Recording into action 这里是默认的 action，请你一定要注意的是 init、action、end 这三个都是 action，并没有什么区别。控制 init 和 end 只执行一遍和 action 会重复执行多次的功能也不在它们自己身上，而是在 run logic 里。这一点我将在后面的文章中再细说。

点击 Options 之后，跳出界面如下：

在这个界面中，有很多可以调的内容。这里举几个重要的点。

首先是 HTML-based script 和 URL-based script。

这个功能点之所以重要，是因为这两个选项录制出来的脚本有很大差别。

其实这一点和 JMeter 的 Store 1st sampler for each group only 是一样的含义。

如果选择了 HTML-based script，就是一个页面一个请求了，而在回放和压力时，这个页面的所有资源都会请求。

如果选择了 URL-based script，就是每个资源一个请求。这个选项有好处是，便于控制和查找问题。如果不想要某个资源，直接注释掉就好。

其次，我们需要注意关联功能。

你可以在这里事先设置好关联的规则，比如说这样的：

JSESSIONID=5687300192384o4^&&^&890523#123456;

你就可以设置左边界为：JSESSIONID=，左边界为冒号，然后在你录制的时候，如果规则匹配到就会自动创建关联。

点击 OK 之后就开始录制了。出现一个工具条，如下所示：

在这个功能条上具有的功能是：暂停、停止、新建 Action，创建集合点、创建事务的起点和终端、加备注、加文检查点。

一般在业务流比较长的脚本中，

性能测试工程师都会通过新建 Action 把操作区分开，也会在录制过程中创建好必要的事务。

最后录制出的脚本如下：

注意哦，URL-based script 的时候，有一个 concurrent group，这个并发组是同时发出请求的。

在 JMeter 中有一个 Parallel Downloads，你还记得吗？

这两者功能一样。

上面就是 LR 中常规的录制功能。录制前，看下 readme，看 LR 支持什么浏览器。在版本 12.6 的 readme 中，已经声明支持 Windows 10 + IE 了。但是我们在使用的过程中还是遇到各种各样的问题，比如调不出浏览器、录不出脚本、卡死的问题。

还有，有些应用只支持 Chrome，而有时，有些应用只能在某些特定的机器的执行，而那些机器又不能装 Vuser Generator。

在这样的场景中，我们只能使用 Port Mapping 的功能。是的，在 LoadRunner 中，Port Mapping 就是代理录制的方式。

Port Mapping

首先打开 Vuser Generator，点击 Start Record，配置成如下界面：

注意，这里一定要选择的是 LoadRunner 安装目录 bin 中的 wplus_init_wsock.exe，从这个名字你也能知道它是基于 Windows Socket 的。

然后，点击 Options - Port Mapping，如下所示：

点击 New Entry。配置如下：

从上图中你可以看到，它的代理功能是很全面和强大的，不仅支持不同的 Service ID，也支持 SSL。

这时的访问逻辑是下面这样的：

一路 OK，返回之后我们就可以开始录制了。会打开一个代理程序。截图如下：

这时候本地会开一个 92 的端口。

请注意，这时如果是远程访问，要注意不要让防火墙拦截了。

接着打开浏览器，输入地址 http://10.211.55.3:92/，可以看到打开的是 http://39.105.21.22:91/ 的界面。

同时，录制工具条中也显示出有事件产生。

当我们停止录制后，查看脚本如下：

看到没有，这里的访问 IP 在直接回放时是不对的。所以要将 ip:port 换成 39.105.21.22:91 才能回放。替换后如下：

这样就可以回放成功了。

如果回放不成功，我们就需要根据出错日志判断要做什么样的脚本增强。大部分的脚本都是需要做关联的，所以后面我们将讲一下关联的功能如何做，以及关联的原理。

Loadrunner 的 Port Mapping 还可以支持 FTP、SOCKET、POP 等协议。这个功能点也不复杂，操作起来也简单，只要想明白访问链路就可以了。

LR 的录制常用功能基本就这些了。

总结

这篇文章，应该是我写的所有的文章中，最最基础的一篇了，并且，从操作上，一步步地描述，也比较清晰。如果你有性能工具使用经验，肯定会觉得这篇过于简单。

可是为什么还要写呢？

因为在性能测试的过程中，有很多新手对录制的逻辑并不清楚。代理录制的这个动作他们也可以很快学会。但是很快就忘记了，我曾经给一些人手把手教过如何做代理录制。结果第二天就不记得了。其实并不是不记得动作，而是出了问题，脑子里没有判断问题的逻辑，所以根本无从下手排查。

另外，你需要注意的是，录制功能并不是性能测试工具必备的功能。对性能测试工具来说，关键功能是能实现模拟批量的真实请求逻辑。至于脚本是如何实现的，怎么做就是可以的。所以我们可以用其他的工具，比如说 BadBoby、Fiddler 甚至 Wireshark 抓到交互请求，再放到 JMeter 中实现脚本，也完全是可以的。

当然没有脚本就无从实现压力，所以脚本的实现是性能测试工程师必备的基础技术，理解原理也是必须的。

思考题

学完今天的文章后，你能用自己的话说一下代理录制的逻辑是什么吗？以及，当访问网页时，为什么第一个请求至关重要？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流进步一下。

08丨案例： 手把手教你编写最简单的性能脚本

通常我们会遇到要手写脚本的时候，就要针对一些接口编写脚本。这时候，我们需要知道接口规范和后台的数据是什么。而有些性能测试工程师写脚本时，并不知道后端的逻辑，只知道实现脚本，事实上，只知道实现脚本是远远不够的。

在这一篇文章中，我不打算讲复杂的内容，只想针对新手写一步步的操作，描述最简单的脚本编写。如果你已经具有丰富的脚本编写经验，会觉得本文很简单。

我没有打算把 JMeter 的功能点一一罗列出来，作为一个性能测试的专栏，不写一下脚本的实现似乎不像个样子。在脚本实现中，我们最常用的协议就是 HTTP 和 TCP 了吧，所以在今天的内容里，我简单地说一下如何编写 HTTP 和 TCP 脚本，以应测试主题。

我先画个图说明一下。

这样的图做性能的人一定要知道，相信很多人也画的出来。

我们知道 HTTP 是应用层的协议之一，现在很多场景都在用它，并且是用的 HTTP1.1 的版本，对应的是 RFC2616，当然还有补充协议 RFC7231、6265。

HTTP 中只规定了传输的规则，规定了请求、响应、连接、方法、状态定义等。我们写脚本的时候，必须符合这些规则。比如为什么要在脚本中定义个 Header？Header 里为什么要那样写？这些在 RFC 中都说得明明白白了。

还有一点也需要注意，HTTP 是通过 Socket 来使用 TCP 的，Socket 做为套接层 API，它本身不是协议，只规定了 API。

而我们通常在 JMeter 中写 TCP 脚本，就是直接调用 Socket 层的 API。TCP 脚本和 HTTP 脚本最大的区别就是，TCP 脚本中发送和接收的内容完全取决于 Socket server 是怎么处理的，并没有通用的规则。所以脚本中也就只有根据具体的项目来发挥了。

手工编写 HTTP 脚本

服务端代码逻辑说明

我们先自己编写一小段服务端代码的逻辑。现在用 Spring Boot 写一个示例，其实就是分分钟的事情。我们做性能测试的人至少要知道访问的是什么东西。

Controller 关键代码如下：

@RestController
@RequestMapping(value = "pa")
public class PAController {

  @Autowired
  private PAService paService;

  // 查询
  @GetMapping("/query/{id}")
  public ResultVO<User> getById(@PathVariable("id") String id) {
    User user = paService.getById(id);
    return ResultVO.<User>builder().success(user).build();
  }
}

Service 关键代码如下：

  public User getById(String id) {
    return mapper.selectByPrimaryKey(id);
  }

用 MyBatis 组件实现对 Mapper 的操作。由于不是基础开发教程，这里只是为了说明逻辑，如果你感兴趣的话，可以自己编写一个接口示例。

逻辑调用关系如下：

数据库中表的信息如下：

我们先看这个接口的访问逻辑：JMeter——SprintBoot 的应用——MySQL。

1. 编写 JMeter 脚本

1.1 创建线程组

首先创建一个线程组，配置如下：

在这个线程组中，有几个关键配置，我来一一说明一下。

Number of Threads(users)：我们都知道这是 JMeter 中的线程数，也可以称之为用户数。但是在第 2 篇文章中，我已经说得非常明确了，这个线程数是产生 TPS 的，而一个线程产生多少 TPS，取决于系统的响应时间有多快。所以我们用 TPS 这个概念来承载系统的负载能力，而不是用这里的线程数。

Ramp-up Period(in seconds)：递增时间，以秒为单位。指的就是上面配置的线程数将在多长时间内会全部递增完。如果我们配置了 100 线程，这里配置为 10 秒，那么就是 100/(10s*1000ms)=1 线程 /10ms；如果我们配置了 10 线程，这里配置为 1 秒，则是 10/1000=1 线程 /10ms。这时我们要注意了哦，在 10 线程启动的这个阶段中，对服务器的压力是一样的。示意图如下：

Loop Count 这个值指的是一个线程中脚本迭代的次数。这里你需要注意，这个值和后面的 Scheduler 有一个判断关系，下面我们会提到。

Delay Thread creation until needed：这个含义从字面看不是特别清楚。这里有一个默认的知识点，那就是 JMeter 所有的线程是一开始就创建完成的，只是递增的时候会按照上面的规则递增。如果选择了这个选项，则不会在一开始创建所有线程，只有在需要时才会创建。这一点和 LoadRunner 中的初始化选项类似。只是不知道你有没有注意过，基本上，我们做性能测试的工程师，很少有选择这个选项的。选与不选之间，区别到底是什么呢？

如果不选择，在启动场景时，JMeter 会用更多的 CPU 来创建线程，它会影响前面的一些请求的响应时间，因为压力机的 CPU 在做其他事情嘛。

如果选择了的话，就会在使用时再创建，CPU 消耗会平均一些，但是这时会有另一个隐患，就是会稍微影响正在跑的线程。这个选项，选择与否，取决于压力机在执行过程中，它能产生多大的影响。如果你的线程数很多，一旦启动，压力机的 CPU 都被消耗在创建线程上了，那就可以考虑选择它，否则，可以不选择。

Scheduler Configuration：这里有一句重要的话，If Loop Count is not -1 or Forever, duration will be min(Duration, Loop Count * iteration duration)。举例来说，如果设置了 Loop Count 为 100，而响应时间是 0.1 秒，那么 Loop Count * iteration duration(这个就是响应时间) = 100 * 0.1 = 10秒。

即便设置了 Scheduler 的 Duration 为 100 秒，线程仍然会以 10 秒为结束点。

如果没有设置 Scheduler 的 Duration，那么你会看到，在 JMeter 运行到 10 秒时，控制台中会出现如下信息：

  2019-11-26 10:39:20,521 INFO o.a.j.t.JMeterThread: Thread finished: Thread Group 1-10

有些人不太理解这一点，经常会设置迭代次数，同时又设置 Scheduler 中的 Duration。而对 TPS 来说，就会产生这样的图：

场景没执行完，结果 TPS 全掉下去了，于是开始查后端系统，其实和后端没有任何关系。

1.2 创建 HTTP Sampler

1.2.1 GET 接口

看上图，我将 Method 选择为 GET。为什么要选择它？往上看我们的接口注解，这是一个 GetMapping，所以这里要选择 GET。

再看 path 中，这里是/pa/query/0808050c-0ae0-11ea-af5f-00163e124cff，对应着“/query/{id}”。

然后执行：

User user = paService.getById(id);

返回执行结果：

return ResultVO.<User>builder().success(user).build();

为什么要解释这一段呢？

做开发的人可能会觉得，你这个解释毫无意义呀，代码里已经写得很清楚了。事实上，在我的工作经历中，会发现很多做性能测试脚本的，实际上并不知道后端采用了什么样的技术，实现的是什么样的逻辑。

所以还是希望你可以自己写一些 demo，去了解一些逻辑，然后在排除问题的时候，就非常清楚了。

接着我们执行脚本，就得到了如下结果：

这样一个最简单的 GET 脚本就做好了。

前面我们提到过，URL 中的 ID 是 0808050c-0ae0-11ea-af5f-00163e124cff，这个数据来自于数据库中的第一条。

如果我们随便写一个数据，会得到什么结果呢？

你会看到，结果一样得到了 200 的 code，但是这个结果明显就不对了，明明没有查到，还是返回了成功。

所以说，业务的成功，只能靠业务来判断。这里只是查询成功了，没返回数据也是查询成功了。我将在后面给你说明如何加断言。

1.2.2 POST 接口

下面我将 Method 改为 POST，POST 接口与 GET 接口的区别有这么几处：

要把 Path 改为 /pa/add；

输入 JSON 格式的 Body Data。

执行起来，查看下结果。

你会发现上来就错了，提示如下：

"status":415,"error":"Unsupported Media Type","message":"Content type 'text/plain;charset=UTF-8' not supported"

这里你需要注意，无论遇到什么问题，都要针对问题来处理。当看不懂问题信息时，先查资料，想办法看懂。这是处理问题的关键，我发现很多做性能测试的新同学，一旦碰到问题就懵了，晕头转向地瞎尝试。

我经常对我的团队成员说，先看懂问题，再处理问题，别瞎蒙！

上面这个问题其实提示得很清楚：“不支持的媒体类型”。这里就两个信息，一个是 Content type，一个是 charset。它们是 JMeter 中 HTTP Header 里默认自带的。我们要发送的是 JSON 数据，而 JMeter 默认是把它当成 text 发出去的，这就出现了问题。所以我们要加一个 Header，将 Content type 指定为 JSON。

加一个 HTTP Header，如下所示：

如果你不知道加什么样的 Header，建议你用 HTTP 抓包工具抓一个看一看，比如说用 Charles，抓到如下信息：

这时你就会知道头里的 Content-Type 原来是application/json;charset=UTF-8。这里的 charset=UTF-8 可以不用写，因为它和默认的一样。

这时再回放，你就会看到如下结果：

到此，一个 POST 脚本就完成了。是不是很简单。

在这里，我需要跟你强调的是，手工编写 HTTP 脚本时，要注意以下几点：

要知道请求的类型，我们选择的类型和后端接口的实现类型要是一致的。

业务的成功要有明确的业务判断（在下面的 TCP 中，我们再加断言来判断）。

判断问题时，请求的逻辑路径要清晰。

编写完 HTTP 脚本时，我们再来看一下如何编写 TCP 脚本。

手工编写 TCP 脚本

服务端代码逻辑说明

我在这里写一个非常简单的服务端接收线程（如果你是开发，不要笑话，我只是为了说明脚本怎么写）。

package demo.socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class SocketReceiver {
  // 定义初始
  public static final int corePoolSize = 5;
  // 定义最大线程池
  public static final int maximumPoolSize = 5;
  // 定义 socket 队列长度
  public static final int blockingQueue = 50;

  /**
   * 初始化并启动服务
   */
  public void init() {
    // 定义线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L,
        TimeUnit.MILLISECONDS, new ArrayBlockingQueue(blockingQueue));
    // 定义 serverSocket
    ServerSocket serverSocket = null;
    try {
      // 启动 serverSocket
      serverSocket = new ServerSocket(Constants.PORT);
      // 输出服务启动地址
      System.out.println(" 服务已启动:" + serverSocket.getLocalSocketAddress().toString());
      // 接收信息并传递给线程池
      while (true) {
        Socket socket = serverSocket.accept();
        executor.submit(new Handler(socket));
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (serverSocket != null) {
        try {
          serverSocket.close(); // 释放 serverSocket
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  // 处理请求类
  class Handler implements Runnable {

    private Socket socket;

    public Handler(Socket socket) {
      this.socket = socket;
    }

    public void run() {
      try {
        // 接收客户端的信息
        InputStream in = socket.getInputStream();
        int count = 0;
        while (count == 0) {
          count = in.available();
        }
        byte[] b = new byte[count];
        in.read(b);
        String message = new String(b);
        System.out.println(" receive request: " + socket.getInetAddress() + " " + message);

        // 睡 2 秒模拟思考时间，这里是为了模拟服务器端的业务处理时间
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        // 向客户端发送确认消息
        // 定义输出流 outer
        OutputStream outer = socket.getOutputStream();
        // 将客户端发送的信息加上确认信息 ok
        String response = message + " is OK";
        // 将输入信息保存到 b_out 中
        byte[] b_out = response.getBytes();
        // 写入输入流
        outer.write(b_out);
        // 推送输入流到客户端
        outer.flush();

      } catch (IOException e) {
        e.printStackTrace();
      } finally {
        // 关闭 socket
        try {
          socket.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  // 程序入口
  public static void main(String[] args) {
    // 定义服务端
    SocketReceiver receiver = new SocketReceiver();
    // 启动服务端
    receiver.init();
  }
}

编写 JMeter 脚本

首先创建 TCP Sampler。右键点击 Thread Group - Add - Sampler - TCP Sampler 即可创建。

输入配置和要发送的信息。

IP 地址和端口是必须要输入的。对于创建一个 TCP 协议的 JMeter 脚本来说，简单地说，过程就是这样的：创建连接 - 发数据 - 关闭连接。

就这样，这个手工的脚本就完成了。

你可能会问，就这么简单吗？是的，手工编写就是这么简单。

但是（对嘛，但是才是重点），通常我们在创建 TCP 协议的脚本时，都是根据业务接口规范来说的，复杂点其实不在脚本本身上，而是在接口的规则上。

添加断言

我回放了一下脚本，发现如下情况：

都执行对了呀，为什么下面的没有返回信息呢？这种情况下只有第一个请求有返回信息，但是下面也没有报错。这里就需要注意了。

测试工具的成功，并不等于业务的成功。

所以我们必须要做的就是响应断言，也就是返回值的判断。在 JMeter 中，断言有以下这些：

因为今天的文章不是工具的教程，所以我不打算全讲一遍。这里我只用最基础的响应断言。什么是断言呢？

断言指的就是服务器端有一个业务成功的标识，会传递给客户端，客户端判断是否正常接收到了这个标识的过程。

在这里我添加了一个断言，用以判断服务器是否返回了 OK。 你要注意这个“OK”是从哪来的哦，它是从服务端的这一行代码中来的。

 String response = message + " is OK";

请注意，这个断言的信息，一是可以判断出业务的正确性。我在工作中发现有些人用页面中一些并不必要的文字来判断，这样就不对了，我们应该用有业务含义的判断标识。

如果我们再次回放脚本，你会发现除了第一个请求，后面 9 个请求都错了。

所以，在做脚本时，请你一定要注意，断言是必须要加的。

长短连接的问题

既然有错，肯定是要处理。我们查看一下 JMeter 的控制台错误信息：

2019-11-26 09:51:51,587 ERROR o.a.j.p.t.s.TCPSampler:
java.net.SocketException: Broken pipe (Write failed)
  at java.net.SocketOutputStream.socketWrite0(Native Method) ~[?:1.8.0_111]
  at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109) ~[?:1.8.0_111]
  at java.net.SocketOutputStream.write(SocketOutputStream.java:141) ~[?:1.8.0_111]
  at org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl.write(TCPClientImpl.java:78) ~[ApacheJMeter_tcp.jar:5.1.1 r1855137]
  at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.sample(TCPSampler.java:401) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:622) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:546) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:486) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:253) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]

从字面上来看，就是通道瓦塔（被破坏）了，Broken pipe。这个提示表明客户端上没有这个连接了，而 JMeter 还以为有这个链接，于是接着用这个链接来发，显然是找不到这个通道，于是就报错了。

这是一个典型的压力工具这边的问题。

而服务端，只收到了一条请求。

为什么会报这个错呢？因为我们代码是短链接的，服务端处理完之后，就把这个链接给断掉了。

这里是压力机上的抓包信息：

// 从这里开始，上面已经看到了有 Fin（结束）包了，后面还在发 Push（发送数据）包。显然是通不了，还被服务端啪啪抽了两次 reset。
11:58:07.042915 IP localhost.57677 > 60.205.107.9.m-oap: Flags [P.], seq 34:67, ack 41, win 4119, options [nop,nop,TS val 163718903 ecr 2122793206], length 33
11:58:07.046075 IP localhost.57677 > 60.205.107.9.m-oap: Flags [FP.], seq 67:331, ack 41, win 4119, options [nop,nop,TS val 163718906 ecr 2122793206], length 264
11:58:07.076393 IP 60.205.107.9.m-oap > localhost.57677: Flags [R], seq 3986768192, win 0, length 0
11:58:07.079156 IP 60.205.107.9.m-oap > localhost.57677: Flags [R], seq 3986768192, win 0, length 0

服务端的抓包信息：

// 服务端也是没有办法，只能在看到了 Push 包之后，给回了个 Reset 包。
11:58:07.047001 IP 124.64.16.240.bones > 7dgroup1.enc-eps-mc-sec: Flags [P.], seq 34:67, ack 41, win 4119, options [nop,nop,TS val 163718903 ecr 2122793206], length 33
11:58:07.047077 IP 7dgroup1.enc-eps-mc-sec > 124.64.16.240.bones: Flags [R], seq 3986768192, win 0, length 0
11:58:07.054757 IP 124.64.16.240.bones > 7dgroup1.enc-eps-mc-sec: Flags [FP.], seq 67:331, ack 41, win 4119, options [nop,nop,TS val 163718906 ecr 2122793206], length 264
11:58:07.054844 IP 7dgroup1.enc-eps-mc-sec > 124.64.16.240.bones: Flags [R], seq 3986768192, win 0, length 0

这是为什么呢？因为在 JMeter 中，默认是复用 TCP 连接的，但是在我们这个示例中，服务端并没有保存这个连接。所以，我们应该在脚本中，把下图中的 Re-use connection 给去掉。

这时再回放脚本，你就会发现 10 次迭代全都对了。如下图所示：

但是，这里还有一个知识点，希望你注意。短连接的时候，必然会产生更多的 TCP 连接的创建和销毁，对性能来说，这会让系统变得缓慢。

所以你可以看到上面 10 条迭代全都对了的同时，响应时间也增加了。

可能会有人问，那这怎么办呢？长短连接的选择取决于业务的需要，如果必须用短链接，那可能就需要更多的 CPU 来支撑；要是长连接，就需要更多的内存来支撑（用以保存 TCP 连接）。

根据业务需要，我们选择一个合适的就好。

TCP 连接超时

这个问题，应该说非常常见，我们这里只做问题的现象说明和解决，不做原理的探讨。原理的部分，我会在监控和分析部分加一说明。

下面这个错误，属于典型的主机连不上。

java.net.ConnectException: Operation timed out (Connection timed out)
  at java.net.PlainSocketImpl.socketConnect(Native Method) ~[?:1.8.0_111]
  at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[?:1.8.0_111]
  at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[?:1.8.0_111]
  at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) ~[?:1.8.0_111]
  at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) ~[?:1.8.0_111]
  at java.net.Socket.connect(Socket.java:589) ~[?:1.8.0_111]
  at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.getSocket(TCPSampler.java:168) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
  at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.sample(TCPSampler.java:384) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:622) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:546) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:486) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:253) [ApacheJMeter_core.jar:5.1.1 r1855137]
  at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]

time out 是个如果你理解了逻辑，就觉得很简单，如果没理解逻辑，就觉得非常复杂的问题。

要想解决这个问题，就要先确定服务端是可以正常连通的。

如果不能正常连通，那么通常都是 IP 不正确、端口不正确、防火墙阻止之类的问题。解决了网络连通性的问题，就可以解决 connection timed out 的问题。

编写 LoadRunner 脚本

针对上面这个示例，如果你要想编写一个 LoadRunner 的示例脚本，也是简单到不行。

首先创建一个空的 winsock 脚本，复制下面代码到 action 里面。

// 创建 socket1
lrs_create_socket("socket1", "TCP", "RemoteHost=60.205.10.9:5567", LrsLastArg);
// 走 socket1, 发送 buf1 中定义的数据
lrs_send ("socket1", "buf1", LrsLastArg );
// 走 socket1，接收数据保存在 buf2 中
lrs_receive("socket1", "buf2",  LrsLastArg);
// 关掉 socket1
lrs_close_socket("socket1");

从上面的信息就可以看到，socket1 这个标识是我们操作的基础。如果你在一个脚本中想处理两个 socket，也是可以的，只要控制好你的标识不会乱就行。

接着再将下面的内容复制到 data.ws 里面。

send buf1 5
    "12345"

recv buf2 10

你可能会问，这个 recv 怎么不写返回的值是什么？

当你手写 socket 脚本的时候，都还没有运行，你怎么知道返回值是什么呢？所以这里，可以不用写。

而 recv 后面的 10 是指接收 10 个字节。如果多了怎么办？截掉？！不会的，LoadRunner 还是会把所有信息全部接收并保存下来，除非你提前定义了截取字符长度的函数。

最后看下我们回放的结果：

Action.c(6): lrs_create_socket(socket1, TCP, ...)
Action.c(7): lrs_send(socket1, buf1)
Action.c(8): lrs_receive(socket1, buf2)
Action.c(8): Mismatch in buffer's length (expected 10 bytes, 11 bytes actually received, difference in 1 bytes)
================================EXPECTED BUFFER================================
===============================================================================
================================RECEIVED BUFFER================================
  "12345 is OK"
===============================================================================
Action.c(8): callRecv:11 bytes were received
Action.c(9): lrs_close_socket(socket1)

看，脚本正常执行了，只是报了一个 Mismatch，这是因为我们定义了 buf2 是 10 字节，而我们实际上接收了 11 字节，所以这里给出了 Mismatch。

到此，一个 LoadRunner 的手工 TCP 脚本就完成了。后面我们就可以根据需要，增强脚本了，加个参数化、关联、检查点等等。

总结

其实这篇文章只想告诉你一件事情，手工编写脚本，从基础上说，是非常简单的，只是有三点需要特别强调：

涉及到业务规则和逻辑判断之后，编写脚本就复杂了起来。但是了解业务规则是做脚本的前提条件，也是性能测试工程师的第一步。

编写脚本的时候，要知道后端的逻辑。这里的意思不是说，你一开始写脚本的时候，就要去读后端的代码，而是说你在遇到问题的时候，要分析整个链路上每个环节使用到了什么技术，以便快速地分析判断。

写脚本是以最简为最佳，用不着故意复杂。

脚本的细节功能有很多，而现在我们可以看到市场上的书籍也好，文档也好，基本上是在教人如何用工具，很少会从前到后地说明一个数据从哪发到哪，谁来处理这样的逻辑。

希望学习性能测试工具的你，不仅知其然，更知其所以然。

思考题

学习完今天的内容，你不妨思考一下，HTTP 的 GET 和 POST 请求，在后端处理中有什么不同？断言的作用是什么？如何使用断言呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

09丨关联和断言：一动一静，核心都是在取数据

对每一个性能测试工具来说，关联和断言都是应该具备的基本功能。

但是有很多新手对关联的逻辑并不是十分理解，甚至有人觉得关联和参数化是一样的，因为它们用的都是动态的数据，并且关联过来的数据也可以用到参数化中，但不一样的点是，关联的数据后续脚本中会用到，参数化则不会。断言倒是比较容易理解，就是做判断。

那么到底该怎样理解关联和断言呢？下面我们通过两个例子来思考一下。

关联

现在做性能测试的，有很多都是单纯的接口级测试，这样一来，关联就用得更少了。因为接口级的测试是一发一收就结束了，不需要将数据保存下来再发送出去。

那么什么样的数据需要关联呢？满足如下条件的数据都是需要关联的：

数据是由服务器端生成的；

数据在每一次请求时都是动态变化的；

数据在后续的请求中需要再发送出去。

示意图如下：

其实我们可以把关联的功能理解为取服务端返回的某个值。在这样的前提之下，我们可以把它用在很多场景之下。

举个例子，我们常见的 Session ID 就是一个典型的需要关联的数据。它需要在交互过程中标识一个客户端身份，这个身份要在后续的交互中一直存在，否则服务端就不认识这个客户端了。

再比如，我们现在用微服务已经非常多了，在 Spring Boot 中有一个 spring-boot-starter-security，默认会提供一个基于 HTTP Basic 认证的安全防护策略。它在登录时会产生一个 CSRF（Cross-Site Request Forgery）值，这个值典型地处于动态变化中。

下面我们来看一下这个值如何处理。

首先，录制登录、退出的脚本。操作如下：

录出的脚本如下所示：

这时直接回放会得到如下结果：

这回你会看到提示了，Unauthorized，没权限。

在回放的脚本中，我们看到了如下的登录返回信息。

同时，在脚本中，我们可以看到登录时会使用到这个值。

下面我们就把它关联了。

首先添加 Cookies Manage。JMeter 在处理 CSRF 时，需要添加一个 Cookies manager。如下：

这里的 Cookie Policy 一定要选择 compatibility，以兼容不同的 cookie 策略。

然后取动态值，在返回 CSRF 值的地方加一个正则表达式提取器来做关联。当然还有更多的提取器，我将在后面提及。

这里的<input name="_csrf" type="hidden" value="(.+?)" />，就是要取出这个动态的变化值，保存到变量 csrfNumber 中去。

然后，发送动态值出去，将发送时的 CSRF 值替换成变量。

最后，再回放，就会得到如下结果。

这样我们就能看到可以正常访问了。

这就是一个典型的关联过程。

上面是用的正则提取器，在 JMeter 中，还有其他的提取器，如下图所示：

使用什么样的提取器取决于业务的需要，比如说如果你返回的是 JSON 格式，就可以使用上图中的 JSON Extractor。

我们在很多的业务中，都可以看到大量的动态数据。所以做关联一定要有耐心，不然就会找得很混乱。

断言

在第 8 篇文章中，我们讲到手工编写脚本，有一个添加断言的动作。断言就是判断服务端的返回是不是正确的。

它的判断逻辑是这样的：

在压力工具中，我们已经知道要比对的值是什么了，接下来就看服务端返回的对不对了。下面我们来详细说一下这个逻辑。

先写一个 POST 接口脚本。

执行下，看到如下结果：

添加断言。

关键点来了，我们知道图片中的这个“true”服务端返回的，可是它到底是从服务端的什么地方产生的呢？

下面我们来看一下服务端的代码。处理我们的 add 请求的，是这样的代码段：

 @PostMapping("/add")
  public ResultVO<Boolean> add(@RequestBody User user) {
    Boolean result = paService.add(user);
    return ResultVO.<Boolean>builder().success(result).build();
  }

我们 post 出去的数据是：

{
    "userNumber": "00009496",
    "userName": "Zee_2",
    "orgId": null,
    "email": "Zee_2@7dtest.com",
    "mobile": "18600000000"
}

代码中对应的是：

@Override
    public String toString() {
        return "User{" +
            "id='" + id + '\'' +
            ", userNumber='" + userNumber + '\'' +
            ", userName='" + userName + '\'' +
            ", orgId='" + orgId + '\'' +
            ", email='" + email + '\'' +
            ", mobile='" + mobile + '\'' +
            ", createTime=" + createTime +
            '}';
    }

ID 是自增的：

  @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "select uuid()")
    private String id;

然后由 paServer.add 添加到数据库里去：

Boolean result = paService.add(user);

add 的实现是：

  public Boolean add(User user) {
    return mapper.insertSelective(user) > 0;
  }

这就是一个关键了。这里 return 的是mapper.insertSelective(user) > 0的结果，也就是一个 true，也就是说，这时在数据库中插入了一条数据：

然后，build 返回信息：

public ResultVO<T> build() {
      return new ResultVO<>(this.code, this.msg, this.data);
    }

这个时候，我们才看到下面的提示信息：

{"data":true,"code":200,"msg":" 成功 "}

也就是说，在数据库中成功插入 1 条数据之后，把 1>0 的判断结果，也就是 true 返回给 result 这个变量，然后通过public ResultVO<Boolean> add(@RequestBody User user)中的 ResultVO 返回给压力工具。

用图片来说的话，逻辑就是下面这样的：

通过这一系列的解释，我就是想说明一个问题：断言中的 true 是从哪来的。

知道了这个问题的答案之后，我们就能理解，为什么这个 true 很重要了。因为有了它，就说明我们在数据库成功插入了数据。

断言是根据需要来设计的，而设计断言的前提就是完全理解这个逻辑。

当然我们也可以直接这样来写 Controller：

 public String add(@RequestBody User user) {
    Boolean result = paService.add(user);
    return "Added successfully!";
  }

这时就没有 true 了。脚本运行结果如下：

这时断言看似是失败的，因为我们断言判断的是“true”，但服务端没有返回“true”这个字符。而实际上，当我们从数据库中查看时，插入是成功的。

但是这种写法是有问题的，不管数据有没有插入成功，只要在 add 方法执行了，就会提示“Added successfully!”。

在实际的工作中，也有开发这样写代码，这样的话，断言似乎都是对的，事务也是成功的，但实际上数据库中可能没有插进去数据。

总结

实际上，关联和断言的前半部分是一样的，都是从服务器返回信息中取出数据。但不同的是，关联取来的数据每次都会不同；而断言取出来的数据基本上都是一样的，除非出了错。

对服务端生成的，并且每次生成都不一样的动态变化的数据，那么将其取回来之后，在后续的请求中使用，这种逻辑就是关联。

对服务端返回的，可标识业务成功与否的数据，将其取回来之后，做判断。这种逻辑就是断言。

思考题

最后给你留道思考题吧，你能说一下关联和断言的逻辑是什么吗？它们取数据的特点又是什么呢？

欢迎你在评论区写下你的思考，我会和你一起交流，也欢迎把这篇文章分享给你的朋友或者同事，一起交流进步。

10丨案例：在JMeter中如何设置参数化数据？

今天我们来做一个实例，看下在 JMeter 中，如何合理地设置参数化数据。

正式场景前的基准测试

在没有做业务混合场景之前，我们需要先做 Benchmark 测试，来确定一个登录业务能支持多少的业务量，这样就可以在业务混合场景中，根据场景中各业务的比例来确定登录的数据需要多少真实的数据。

我们继续用上一篇文章中用户登录的例子，单独测试登录业务，结果如下：

Java
summary +    125 in 00:00:04 =   31.0/s Avg:    28 Min:     0 Max:   869 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   3404 in 00:00:30 =  113.2/s Avg:    31 Min:     0 Max:   361 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary +   4444 in 00:00:30 =  148.4/s Avg:    57 Min:     0 Max:   623 Err:    10 (0.23%) Active: 11 Started: 11 Finished: 0

从上面的结果可以看到登录业务能达到的 TPS 是 113 左右，这里我们取整为 100，以方便后续的计算。

在测试工具中配置参数

在上面的试探性测试场景中，不需要观察系统的资源，只需要根据 TPS 做相应的数据统计即可。

前面我们知道，在这个示例中只做了近 10 万条的用户数据，为了方便示例进程。

下面我们从数据库中查询可以支持登录 5 分钟不重复的用户数据。根据前面的公式，我们需要 30000 条数据。

Java
100x5mx60s=30000 条

接下来连接数据库，取 30000 条数据，存放到文本中，如下所示：

Java
username,password
test00001,test00001
test00002,test00002
test00003,test00003
test00004,test00004
test00005,test00005
test00006,test00006
test00007,test00007
...................
test30000,test30000

参数化配置在 JMeter 中的使用说明

我们将这些用户配置到测试工具的参数当中，这里以 JMeter 的 CSV Data Set Config 功能为例。配置如下：

在 JMeter 的参数化配置中，有几个技术点，在这里说明一下。

“Allow quoted data?”这里有两个选择，分别是 False 和 True。它的含义为是否允许带引号的数据，比如说在参数化文件中有这样的数据。

Java
"username","password"
"test00001","test00001"
"test00002","test00002"
...................
"test30000","test30000"

如果有引号，这个选择必须是 True。如果设置为 False，那么我们在脚本中会看到如下的数据：

Java
username=%22test00001%22password=%22test00001%22

由于设置为 False，JMeter 将（"）转换为了 %22 的 URL 编码，很显然这个数据是错的。如果选择为 True，则显示如下：

Java
username=test00001password=test00001

这里就显示对了。

除此之外，还有如下几个功能点需要说明：

Recycle on EOF? ：这里有三个选择，False、True 和 Edit。前两个选择非常容易理解。False 是指在没有参数的时候不循环使用；True 是指在没有参数的时候循环使用。Edit 是指在没有参数的时候会根据定义的内容来调用函数或变量。

Stop thread on EOF?：这里有三个选择，False、True 和 Edit。含义和上面一致。

Sharing mode : 这里有四个选择，All threads、Current thread group、Current thread、Edit。

Sharing mode 的前三个选择是比较容易理解的，参数是在所有线程中生效，在当前线程组生效，还是在当前线程中生效。但这里的 Edit 和前两个参数中的 Edit 相比，有不同的含义。这里选择了 Edit 之后，会出现一个输入框，就是说这里并不是给引用函数和参数使用的，而是要自己明确如何执行 Sharing mode。那如何来使用呢？

举例来说，假设我们有 Thread Group 1-5 四个线程组，但是参数化文件只想在 Thread Group 1、3、5 中使用，不想在线程组 2、4 中使用，那么很显然前面的几个选项都达不到目的，这时我们就可以选择 Edit 选项，在这里输入SharedWithThreadGroup1and3and5。而在其他的线程组中配置其他参数化文件。

也就是说同样的一个变量名，在线程组 1/3/5 中取了一组数据，在线程组 2/4 中取了另一组数据。

以上三个参数的选项可以随意组合。于是就会得到如下表。

需要注意的是，EOF 是文件结束符的意思。在下面的解释中，为了更符合性能测试中的术语，特意解释为参数不足时。

以上三个功能点根据参数设计得不同，会产生不同的组合，我们依次查看一下。

这个组合显然是矛盾的，没有参数时不让循环，还不让停止线程，这不是耍流氓吗？真实的结果是什么呢？当我们执行时就会发现，参数变成了这样：

username=%3CEOF%3E&password=%3CEOF%3E

服务端果然返回了：{"flag":false,"errMsg":"账号不存在"}。

这个组合中第二个选项显然是没意义的，既然参数允许重复使用了，又怎么会发生参数不足停止线程的情况呢？

这个组合因为第一个选项为“Edit”所以变得不确定了，如果在 Edit 的函数或变量返回为 True，则和第 2 种组合一样；如果返回为 False，则和第 1 种组合一样。

这是一个完全合情合理的组合！

同第二个组合一样，第二个选项显然没有意义。

这个组合同样因为第一个选项为 Edit，所以变得不确定了，如果在 Edit 的函数或变量返回为 True，则和第 3 种组合一样；如果返回为 False，则和第 4 种组合一样。

这个组合因为是否停止线程的不确定性会出现两种可能，有可能是第 1 种组合，也有可能是第 4 种组合。

这个组合中是否停止线程的 Edit 配置没有意义，因为可循环使用参数，所以不会发生参数不足导致线程停止的情况。

这是一个古怪的组合，具有相当的不确定性，有可能变成第 1、2、4、5 种组合。

下面我们再来看下其他衍生的设置组合。

真实场景下的 JMeter 参数配置和执行结果

根据以上的描述，我们先用 10 个用户来测试下，将 Stop thread on EOF?改为 True，将Recycle on EOF?改为 False，其他不变。同时将线程组中配置为 1 个线程循环 11 次。这样设置的目的是为了看在数据不足时，是否可以根据规则停掉线程组。如下所示：

线程组配置如下：

执行之后，我们会在日志中看到如下信息：

Java
2019-09-05 22:56:30,171 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 1 1-1, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false

可以看到在参数用完又不可循环使用参数的情况下，JMeter 主动停止了线程。

我们延续使用上文中场景二的条件，即希望场景中每个线程的每次迭代都用不同的数据。

为了能很快地整理出实际的结果，我们只使用 10 条数据来模拟，条件设置如下：

线程组：2
线程（每线程组）：6
参数化数据：10 条

执行完场景后，会在日志中看到如下信息：

Java
2019-09-07 23:24:25,585 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 1 1-1, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
2019-09-07 23:24:25,452 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 1 1-2, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
2019-09-07 23:24:23,406 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 2 2-1, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
2019-09-07 23:24:25,517 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 2 2-2, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false

可见所有的线程都按我们的配置停止了线程，同时各线程取得参数如下表所示：

每次执行场景会有不同，不同点是线程组 1 有可能执行 6 次，而线程组 2 只执行 4 次；或者线程组 1 中的线程 2执行次数比线程 1 执行次数多。但总体执行次数会是 10 次。

如果数据可以被线程平均分配，则每个线程的迭代次数会相同。如果数据不能被线程平均分配，则每个线程的迭代次数不会相同，但相差不会大。

参数化配置在 LoadRunner 中的使用说明

在 LoadRunner 中参数配置页面如下：

它的取值组合如下所示：

以上的组合中，组合 7 对应着上文中 JMeter 真实场景中每次迭代取不同数据的组合，即 JMeter 中的参数组合 4。

总结

通过今天的内容，我们对性能测试中的参数化做了一次解析，在执行性能测试时，我们需要根据实际的业务场景选择不同的数据量和参数设置组合。

不同的压力工具在参数化的实现逻辑上也会不同，但是参数化必须依赖业务逻辑，而不是工具中能做到什么功能。所以在参数化之前，我们必须分析真实业务逻辑中如何使用数据，再在工具中选择相对应的组合参数的方式去实现。

这里我总结一下性能工作中参数化的逻辑，希望对你有所启发。

分析业务场景；

罗列出需要参数化的数据及相对应的关系；

将参数化数据从数据库中取出或设计对应的生成规则；

合理地将参数化数据保存在不同的文件中；

在压力工具中设置相应的参数组合关系，以便实现模拟真实场景。

通过以上步骤，我们就可以合理的参数化数据，模拟出真实场景。

思考题

你可以思考一下下面几个问题：

为什么参数化数据要符合生产环境的数据分布？

为什么参数化数据要关注组合逻辑关系，而不是随意设置组合？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

11丨性能脚本：用案例和图示帮你理解HTTP协议

当前使用得最为广泛的应用层协议就是 HTTP 了。我想了好久，还是觉得应该把 HTTP 协议写一下。

因为做性能测试分析的人来说，HTTP 协议可能是绕不过去的一个槛。在讲 HTTP 之前，我们得先知道一些基本的信息。

HTTP（HyperText Transfer Protocol，超文本传输协议），显然是规定了传输的规则，但是它并没有规定内容的规则。

HTML（HyperText Marked Language，超文本标记语言），规定的是内容的规则。浏览器之所以能认识传输过来的数据，都是因为浏览器具有相同的解析规则。

希望你先搞清楚这个区别。

我们首先关注一下 HTTP 交互的大体内容。想了很久，画了这么一张图，我觉得它展示了我对 HTTP 协议在交互过程上的理解。

在这张图中，可以看到这些信息：

在交互过程中，数据经过了 Frame、Ethernet、IP、TCP、HTTP 这些层面。不管是发送和接收端，都必须经过这些层。这就意味着，任何每一层出现问题，都会影响 HTTP 传输。

在每次传输中，每一层都会加上自己的头信息。这一点要说重要也重要，说不重要也不重要。重要是因为如果这些头出了问题，非常难定位（在我之前的一个项目中，就曾经出现过 TCP 包头的一个 option 因为 BUG 产生了变化，查了两个星期，一层层抓包，最后才找到原因）。不重要是因为它们基本上不会出什么问题。

HTTP 是请求 - 应答的模式。就是说，有请求，就要有应答。没有应答就是有问题。

客户端接收到所有的内容之后，还要展示。而这个展示的动作，也就是前端的动作。在当前主流的性能测试工具中，都是不模拟前端时间的，比如说 JMeter。我们在运行结束后只能看到结果，但是不会有响应的信息。你也可以选择保存响应信息，但这会导致压力机工作负载高，压力基本上也上不去。也正是因为不存这些内容，才让一台机器模拟成千上百的客户端有了可能。

如果你希望能理解这些层的头都是什么，可以直接抓包来看，比如如下示图：

从这个图中，我们就能看到各层的内容都是什么。当然了，这些都属于网络协议的知识范围，如果你有兴趣，可以去看一下《TCP/IP 详解 卷 1：协议》。

我们还是主要来说一说 HTTP 层的内容。同样，我希望通过最简单的示例的方式，给你解释一下 HTTP 的知识，而不是纯讲压力工具，或纯理论。

在我看来，只有实践的操作和理论的结合，才能真正的融会贯通。只讲压力工具而不讲原理，是不可能学会处理复杂问题的；空有理论没有动手能力是不可能解决实际问题的。

由于压力工具并不处理客户端页面解析、渲染等动作，所以，以下内容都是从协议层出发的，不包括前端页面层的部分。

JMeter 脚本

在这里，我写了一个简单的 HTTP GET 请求（由于 HTTP2.0 在市场上还没有普及，所以这里不做特别说明的话，就是 HTTP1.1）。

在前面的文章中，我已经写过了 HTTP GET 和 POST 请求。在这里只解释几个重要信息：

第一个就是 Protocol。

这个当然重要。从“HTTP”这几个字符中，我们就能知道这个协议有什么特点。 HTTP 的特点是建议在 TCP 之上、无连接（TCP 就是它的连接）、无状态（后来加了 Cookies、Session 技术，用 KeepAlive 来维持，也算是有状态吧）、请求 - 响应模式等。

第二个是 Method 的选项 GET。

HTTP 中有多少个 Method 呢？我在这里做个说明。在 RFC 中的 HTTP 相关的定义中（比如 RFC2616、2068），定义了 HTTP 的方法，如下：GET、POST、PUT、PATCH、DELETE、COPY、HEAD、OPTIONS、LINK、UNLINK、PURGE。

回到我们文章中的选项中来。GET 方法是怎么工作的呢？

The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.

也就是说，GET 可以得到由 URI 请求（定义）的各种信息。同样的，其他方法也有清楚的规定。我们要注意的是，HTTP 只规定了你要如何交互。它是交互的协议，就是两个人对话，如何能传递过去？小时候一个人手上拿个纸杯子，中间有根线，相互说话能听到，这就是协议。

第三个是 Path，也就是请求的路径。这个路径是在哪里规定的呢？在我这个 Spring Boot 的示例中。

   @RequestMapping(value = "pabcd")
    public class PABCDController {
        @Autowired
        private PABCDService pabcdService;
        @Autowired
        private PABCDRedisService pabcdRedisService;
        @Autowired
        private PABCDRedisMqService pabcdRedisMqService;
        @GetMapping("/redis_mq/query/{id}")
        public ResultVO<User> getRedisMqById(@PathVariable("id") String id) {
            User user = pabcdRedisMqService.getById(id);
            return ResultVO.<User>builder().success(user).build();
        }

看到了吧。因为我们定义了 request 的路径，所以，我们必须在 Path 中写上/pabcd/redis_mq/query这样的路径。

第四个是 Redirect，重定向。HTTP 3XX 的代码都和重定向有关，从示意上来看，如下所示。

用户发了个 URL A 到服务 A 上，服务 A 返回了 HTTP 代码 302 和 URL B。 这时用户看到了接着访问 URL B，得到了服务 B 的响应。对于 JMeter 来说，它可以处理这种重定向。

第五个是 Content-Encoding，内容编码。它是在 HTTP 的标准中对服务端和客户端之间处理内容做的一种约定。当大家都用相同的编码时，相互都认识，或者有一端可以根据对端的编码进行适配解释，否则就会出现乱码的情况。

默认是 UTF8。但是我们经常会碰到这种情况。当我们发送中文字符的时候。比如下面的名字。

当我们发送出去时，会看到它变成了这种编码。如下图所示：

如果服务端不去处理，显然交互就错了。如下图所示：

这时，只能把配置改为如下：

我们这里用 GBK 来处理中文。就会得到正确的结果。

你就会发现现在用了正常的中文字符。在这个例子，有人选择用 URL 编码来去处理，会发现处理不了。这是需要注意的地方。

第六个是超时设置。在 HTTP 协议中，规定了几种超时时间，分别是连接超时、网关超时、响应超时等。

如下所示，JMeter 中可以设置连接和响应超时：

在工具中，我们可以定义连接和响应的超时时间。但通常情况下，我们不用做这样的规定，只要跟着服务端的超时走就行了。但在有些场景中，不止是应用服务器有超时时间，网络也会有延迟，这些会影响我们的响应时间。如果 HTTP 默认的 120s 超时时间不够，我们可以将这里放大。

在这里为了演示，我将它设置为 100ms。我们来看一下执行的结果是什么样。

从栈的信息上就可以看到，在读数据的时候，超时了。

超时的设置是为了保证数据可以正常地发送到客户端。做性能分析的时候，经常有人听到“超时”这个词就觉得是系统慢导致的，其实有时也是因为配置。

通常，我们会对系统的超时做梳理，每个服务应该是什么样的超时设置，我们要有全局的考量。比如说：

超时应该是逐渐放大的（不管你后面用的是什么协议，超时都应该是这个样子）。而我们现在的系统，经常是所有的服务超时都设置得一样大，或者都是跟着协议的默认超时来。在压力小的时候，还没有什么问题，但是在压力大的时候，就会发现系统因为超时设置不合理而导致业务错误。

如果倒过来的话，你可以想像，用户都返回超时报错了，后端还在处理着呢，那就更有问题了。

而我们性能测试人员，都是在压力工具中看到的超时错误。如果后端的系统链路比较长，就需要一层层地往后端去查找，看具体是哪个服务有问题。所以在架构层级来分析超时是非常有必要的。

在上图中，还有一个参数是客户端实现（Client Implementation）。其中有三个选项：空值、HTTPClient4、Java。

官方给出如下的解释。

JAVA： 使用 JVM 提供的 HTTP 实现，相比 HttpClient 实现来说，这个实现有一些限制，这个限制我会在后面提到。

HTTPClient4：使用 Apache 的 HTTP 组件 HttpClient 4.x 实现。

空值：如果为空，则依赖 HTTP Request 默认方法，或在jmeter.properties文件中的jmeter.httpsample定义的值。

用 JAVA 实现可能会有如下限制。

在连接复用上没有任何控制。就是当一个连接已经释放之后，同一个线程有可能复用这个已经释放掉的连接。

API 最适用于单线程，但是很多设置都是依赖系统属性值的，所以都应用到所有连接上了。

不支持 Kerberos Authentication（这是一种计算机网络授权协议，用在非安全网络中，对个人通信以安全的手段进行身份认证）。

不支持通过 keystore 配置的客户端证书。

更容易控制重试机制。

不支持 Virtual hosts。

只支持这些方法： GET、POST、HEAD、OPTIONS、PUT、DELETE 和 TRACE。

使用 DNS Cache Manager 更容易控制 DNS 缓存。

第八个就是 HTTP 层的压缩。我们经常会听到在性能测试过程中，因为没有压缩，导致网络带宽不够的事情。当我们截获一个 HTTP 请求时，你会看到如下内容。

这就是有压缩的情况。在我们常用的 Nginx 中，会用如下常见配置来支持压缩：

    gzip  on;   # 打开 gzip
    gzip_min_length 2k; # 低于 2kb 的资源不用压缩
    gzip_comp_level 4; # 压缩级别【1-9】值越大，压缩率就越高，但是 CPU 消耗也越多，根据我们在网上看到建议，大部分都是建议设置为中间 4、5 之类的，这里我建议大家根据自己的项目实际情况，在压力测试之后给出适合的值。
    gzip_types text/plain application/javascript;  # 设置压缩类型
    gzip_disable "MSIE [1-6]\.";  # 禁用 gzip 的条件，支持正则

在 RFC2616 中，Content Codings 部分定义了压缩的格式 gzip 和 Deflate，不过我们现在看到的大部分都是 gzip。

不过在压缩这件事情上，我们在压力工具中并不需要做什么太多的动作，最多也就是加个头。

第九个就是并发。在 RFC2616 中的 8.1.1 节明确说明了为什么要限制浏览器的并发。大概翻译如下，有兴趣的去读下原文：

少开 TCP 链接，可以节省路由和主机（客户端、服务端、代理、网关、通道、缓存）的 CPU 资源和内存资源。

HTTP 请求和响应可以通过 Pipelining 在一个连接上发送。Pipelining 允许客户端发出多个请求而不用等待每个返回，一个 TCP 连接更为高效。

通过减少打开的 TCP 来减少网络拥堵，也让 TCP 有充足的时间解决拥堵。

后续请求不用在 TCP 三次握手上再花时间，延迟降低。

因为报告错误时，没有关闭 TCP 连接的惩罚，而使 HTTP 可以升级得更为优雅（原文使用 gracefully）。

如果不限制的话，一个客户端发出很多个链接到服务器，服务器的资源可以同时服务的客户端就会减少。

我们常见的浏览器有如下的并发限制。

在压力工具中，并没有参数来控制这个并发值，如果是在同一个线程中，就是并行着执行下去。

HTTPS 只是加了一个 S，就在访问中加了一层。这一层可以说的话题有很多，因为技术原理比较多。还好对性能测试中的脚本部分来说，关系并不大，需要时导进去就可以了。而在性能分析中，基本上除了看下不同产品、不同软件硬件的性能验证之外，其他的也没什么可分析的部分。因为证书是个非常标准的产品，加在中间，就是加密算法和位数也会对性能产生影响。如果执行场景时报：javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake，就应该把证书也加载进来。

有了前面这些压力工具中常用的 HTTP 知识之后，有些人肯定会有一种感觉，总觉得有什么内容没有讲到。对了，就是 HTML。前面我们提到了，HTML 是属于内容的规则，前端是个宏大的话题，以后有机会详聊。

其实对我们做性能测试的人来说，无需关心 HTTP 的内容，我们只要关心数据的流向和处理的逻辑就可以了。至于你是 A 业务还是 B 业务，在性能分析中都是一样的，逻辑仍然没有变化。

从性能测试的角度来看，如果你要模拟页面请求，最多也就是正常实现 HTTP 的方法 GET、POST 之类的。它发送和接收的内容，只要符合业务系统的正常流程就可以，这样业务才能正常运行。

比如说，前面提到的 POST 请求。如果我们发送了一段 JSON。内容如下：

{
    "userNumber": "${Counter}",
    "userName": "Zee_${Counter}",
    "orgId": null,
    "email": "test${Counter}@dunshan.com",
    "mobile": "18611865555"
}

代码中的 Service 负责接收 User 对象，同时转换它的是如下代码：

@Override
    public String toString() {
        return "User{" +
            "id='" + id + '\'' +
            ", userNumber='" + userNumber + '\'' +
            ", userName='" + userName + '\'' +
            ", orgId='" + orgId + '\'' +
            ", email='" + email + '\'' +
            ", mobile='" + mobile + '\'' +
            ", createTime=" + createTime +
            '}';
    }

然后通过 Service 的 add 方法 insert 到数据库中，这里后面使用的 MyBatis：

    Boolean result = paRedisService.add(user);

而这些，都属于业务逻辑处理的部分，我们分析时把这个链路都想清楚才可以一层层剥离。

总结

对于 HTTP 协议来说，我们在性能分析中，主要关心的部分就是传输字节的大小、超时的设置以及压缩等内容。在编写脚本的时候，要注意 HTTP 头部，至于 Body 的内容，只要能让业务跑起来即可。

思考题

你能说一下为什么压力机不模拟前端吗？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

12丨性能场景：做参数化之前，我们需要考虑什么？

在性能测试中，我们要关注的数据主要有以下几类，分别是参数化数据、监控数据和基础铺底数据。

我们今天先描述第一种参数化数据，在后面的文章中再描述其他数据。

首先我们需要了解，为什么要关注性能场景中的参数化数据呢？我以下面的两个例子说明一下。

在我的工作经历中，见过很多初级性能测试工程师不知道如何设置合理的参数化数据，以至于数据会出现这两种情况。

1. 数据不均衡

有些人直接用同一个数据执行混合场景测试，在这种情况下对服务器的压力和真实环境下的完全不一样。有时我们不得不造很多参数化数据，也有很多工程师不考虑数据库表中的数据直方图，就直接在少量的参数化数据中创建了大量的相关记录。比如说，在电商系统中造出大量的购买记录；在银行系统中造出大量的个人流水记录。

这些都不能满足真实用户场景的需要，导致的结果就是整个测试结果都毫无意义。

2. 参数化数据量不足

有时候，如果我们选择用非常少量的数据运行大量业务操作的场景，就会导致压力和真实生产环境完全不一致。比如说，用 100 个数据运行出上万甚至上亿的业务操作。

那么到底该怎样才能合理地设置参数化数据呢？

参数化数据的疑问

根据我的经验，在参数化测试数据的获取和考虑上，我们一般会有以下四个常见的疑问。

参数化数据应该用多少数据量？

参数化数据从哪里来？

参数多与少的选择对系统压力有什么影响？

参数化数据在数据库中的直方图是否均衡？

接下来，我们对这些问题一一做出解答。

参数化数据应该用多少数据量？

首先，参数化数据要用到多少取决于场景，举例来说，对一个压力工具线程数为 100，TPS 有 1000 的系统，如果要运行 30 分钟，则应该取得的参数化数据是下面这样的。

数据类型、限制条件和数据量计算的方式如下表所示：

从技术角度看，根据数据类型就可以确定应该用多少条参数化数据了。但是这样考虑就够了吗？当然是不够的。因为除了技术的限制之外，还有业务场景的需求。

根据业务场景计算参数化数据量

在性能场景中，我们需要根据实际的业务场景来分析需要用到什么样的数据，以便计算数据量。这里的数据类型包括可循环使用的数据和不可循环使用的数据。用户登录是一个在各行业中几乎都会遇到的事务，我们拿它来举例说明，下面这张图是一个用户登录的界面。

这里需要用到两种数据，一个是帐号，一个是密码。帐号和密码一定是可以真实登录到系统的，不然无法完成后续的业务。很显然对于登录来说，不同的人一定是用不同的用户登录的。

场景一

首先我们来看下场景一。有时候我们做脚本时考虑的是，有多少线程（Thread）就配置多少用户，让每个线程在同一个用户上循环执行。

如下图所示：

需要注意的是，在本文中，每一个“—user1→”代表一次脚本完全的迭代。

这样的用户参数化配置，只能满足一些比较特定的场景。比如说，用户在早上登录系统之后，一直在系统中带着登录 session 做业务操作，并且不会退出，只有在下班时才退出系统。

当我们要模拟一天中的业务峰值时，可以像上面这样配置。登录一次，循环使用同一用户的 Session 信息。这就是前面提到的部分可循环数据。

在这样的场景中，有多少线程就需要准备多少用户数据。即：

Java
用户数据 = 线程个数

场景二

但在有些场景中，这是完全错误的配置方式。比如说电商系统，用同一个用户账号不停循环购买商品，就是不符合真实场景的。

这时侯怎么办？我们可以用在压力测试工具中模拟出来的线程的每一次迭代来代表一个用户，如下所示：

这就是不可循环使用的数据。在这样的场景中，就需要考虑场景的 TPS 和持续时间了。用户数据的计算方法是：

Java
tpsx 持续时间（秒级）

我们举个例子，假如有一个 100TPS 的场景，持续 30 分钟。那么计算方式如下：

Java
100x30*60=180000（条用户数据）

这里再多说一句，压力工具的线程数量也并不是常说的并发数的概念。这个我们在前面已经说得很清楚了。

场景三

但是还有一种情况，就是在一个线程之中，可以循环使用固定条目的数据。如下所示：

在这种情况下，我们就需要根据实际的业务场景判断了。在同样的 100TPS 场景中，如果准备了 1000 条数据，就可以让每个线程用 10 个不同的数据。

这样的场景没有固定的条数限制，只能根据实际的业务判断。

所以在配置参数之前，我们需要先判断这个参数是什么类型的数据。

如果是可循环使用的数据，那么它在真实的性能场景中非常少，也就是说只使用一条或几条测试数据的真实业务场景是非常少的。

参数化数据从哪里来？

计算了参数化数据量之后，还有一个重要的问题需要解决，就是参数化数据从哪里来呢？这一步的目的是要确保参数的有效性。

参数化数据从大体上划分，主要有两个来源。

第一类

用户输入的数据在后台数据库中已存在，比如我们上面示例中的用户数据。这类数据的特点是什么呢？

存在后台数据库中；

需要用户主动输入；

用户输入的数据会和后台数据库中的数据做比对。

这类数据必须查询数据库之后再参数化到工具中。

第二类

用户输入的数据在后台数据库中不存在。在业务流中，这些数据会 Insert 或 Update 到数据库中。这类数据的特点是什么呢？

数据库中原本不存在这些数据；

在脚本执行成功后会将这些数据 insert 或 update 到数据库中；

每个用户输入的数据可能相同，也可能不同，这取决于业务特点。

这类数据必须通过压力工具做参数化，同时也必须满足业务规则。

我同样用前面的用户参数为例，由于用户登录的时候一定要和数据库中的用户数据做比对，只有用户名密码都完全正确的情况下才可以成功登录，所以这样的用户参数一定要从后台数据库中查询得到。

在本例中，通过后台数据库用户表的查询真实可用的用户数共有 10 万。

如果在业务场景中，是不可循环使用的用户数据，那么很显然，在可以支持 100TPS 并发的系统中，这些用户数量只够使用 16.67 分钟。

Java
100000/100/60=16.67（分钟）

总之，参数化时需要确保数据来源以保证数据的有效性，千万不能随便造数据。这类数据应该满足两个条件：

要满足生产环境中数据的分布；

要满足性能场景中数据量的要求。

参数取多与少对系统压力有什么影响？

根据上文中的第二个条件，这里就要说一下数据量的要求了。

从经验上判断，对一个系统来说，获取的参数化数据是否合理，会直接影响压力测试的结果有没有意义。

我们根据下面这张图来理解一下数据在系统中的流转。

这张图中，绿色部分代表数据在各系统中的正常大小，而黑色部分代表压力工具中使用的数据量大小。如果压力工具使用的数据量少，那么应用服务器、缓存服务器、数据库服务器，都将使用少量的缓存来处理。

显然图中所示的黑色部分是很少的，完全不能把后端各类服务器的缓存占用到真实场景中应该有的大小，所以在这种状态之下是测试不出来真实场景下的压力的。

对数据库连接的存储设备来说同样也有影响。如果数据量少，则相应的存储的 I/O 使用就少。对于一个没有被 Cache 的数据来说，首次使用肯定会触发 I/O，也就是会产生寻址、PageFalut 等情况。

参数取得过多，对系统的压力就会大；参数取得过少，不符合真实场景中的数据量，则无法测试出系统真实的压力。

参数化数据在数据库中的直方图是否均衡？

对于参数化数据来说，如果数据取自于数据图，我们通常要检查一下数据库中的数据直方图。 对于直接从生产上拿的数据来说，数据的分布更为精准。但是对于一些在测试环境中造的数据，则一定要在造数据之后，检查下数据分布是否与生产一致。

我们以一个案例开始。

在性能场景执行过程中，有一个业务的 TPS 如下图所示：

很明显，图中 TPS 中间掉下来的情况是非常不合理的。

为什么会导致这个情况呢？在这个示例中，这种现象是由抽取的数据量不合理导致的，我们来看一下数据分布。

客户 iD，客户流水记录数
'客户 ID1', '69865'
'客户 ID2', '55075'
......
'客户 ID5374728, '177'
'客户 ID5374729', '176'
'客户 ID5374730', '175'

显然通过统计之后，我们可以发现客户的流水记录数是完全不均衡的，而这个业务脚本是会返回客户的流水记录的。当用到记录数多的客户 ID 时，就会导致 TPS 严重下降，这是因为这些数据都要从存储设备中获取，一旦数据量多，就会导致一系列的资源开销；而用到记录数少的客户 ID 时，TPS 就很高。

那么针对这种情况，我们该怎么处理呢？

首先分析业务逻辑，确认客户流水是否应该这么多。在这个场景中，我们分析过业务，客户的流水通常情况下都会在 100~200 之间，这是合理的情况，而上万的数据量就是完全不合理的。

然后我们过滤掉不合理的数据即可。

这样得到的参数化数据就符合真实场景了。

总结

在今天的文章中，需要你领悟到的是，参数化数据的合理性对性能场景有着举足轻重的作用。通常，我们在做参数化数据之前，需要先分析实际业务的逻辑。比如说：

什么数据是唯一的？什么数据是可重复使用的？

数据是客户主动输入，后端只保存即可，还是客户输入后，后端需要比对？

这些都是我们在做参数化之前要分析的部分。而参数化的数据量的重要性，不仅和业务需求相关，也和数据存储和查询的方式相关。这个话题我们在后面也会讨论到。

思考题

如果你吸收了这篇文章的内容，不妨思考一下下面这两道题：

参数化数据的分析重点是哪些？在不同的场景中为什么参数化数据有如此大的差异？

参数化数据的来源和获取要符合哪些规则？当不符合获取规则时，会产生什么问题？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流学习一下。

13丨性能测试场景：如何进行场景设计？

我们在前面屡次强调了场景的重要性，今天终于到了要把实际场景拿出来解析的时候了。

在本篇文章中，为了保证数据的连续性，我用之前的项目资料来作明确地说明。同时为了模糊关键业务信息，以及让场景的描述更通用性，我会把所有的业务名隐去。

根据之前我们所说的，基准性能场景是为了测试出单业务的最大容量，以便在混合容量场景中判断哪个业务对整体容量最有影响。

今天的场景设计需要说明两个前提条件：

这些业务都是实时的业务，不涉及批处理、大数据等业务。

因为本篇着重讲场景的设计和具体项目的操作，所以不加系统资源的分析，避免信息混乱。

在这个场景设计中，首先，我们要列出自己要测试的业务比例、业务目标 TPS 和响应时间指标。

在这个项目中，响应时间指标是统一的，就是不大于 100ms。

其实我们在做项目的时候，经常会这样制定一个统一的响应时间指标，这样做也不是完全因为懒，更多的是根本不知道如何定每个业务的时间。但我们性能测试人员要知道，这显然是不对的，因为业务不同，响应时间指标也应该不同，合理的做法是给出每个业务的响应时间指标。下面我们还会遇到响应时间定得不够细致的问题。

有了这个列表，下一步就是做基准性能测试了。

基准性能场景

有很多人做接口测试的时候，觉得接口的 TPS 真是高呀，于是就按照最高的 TPS 跟老板汇报。但我们一定要知道的是，接口的 TPS 再高，都无法说明容量场景的情况，除非这个服务只有这一个接口，并且也只为了测试服务，这时就不必有混合的情况了。

首先，我们要知道，每个业务在系统中的最大容量是多少。那么接下来，我们用上面的业务一个一个地做基准，看看结果如何。

业务 1

场景执行时长：17 分钟。

先看 Statistics。

很多人喜欢用这个表中的数据来做报告，特别是 90th pct、95th pct、99th pct。我不是说不能用，但是，我们要先知道这个场景是什么样，再来确定这些值是不是可以用。

从上图来看，TPS 达到 573.24，平均响应时间是 109.83ms，发送字节很少，这里都没统计到，接收字节 966.22KB/sec，这个值也非常低，最小响应时间 43ms，最大响应时间 694ms。

但是！这能说明什么呢？什么都说明不了呀。是好是坏？不知道呀。所以我们还需要看其他图。

我们先看一下线程图。

以每分钟 15 个用户的速度往上递增。

对应的响应时间图是下面这样的。

随着用户的增加，响应时间一直都在增加，显然瓶颈已经出现了。

我们再结合 Statistics 表格中几个和时间有关的值来想想一想，90th pct、95th pct、99th pct、平均响应时间还可以用吗？ Statistics 的平均响应时间是 109.83ms，但是你从响应时间图和线程图比对就可以看到，在不同的线程阶梯，响应时间是有很大差别的。所以 Statistics 中的响应时间都是针对整个场景来说的，然而在梯度加压的过程中，用 Statistics 中的数据是不合理的。

接着我们再来看下 TPS 图：

我们可以从 TPS 图上看到，最大 TPS 能达到 680 左右。我再啰嗦一句，请你不要再用所谓的”最大 TPS 拐点“这样的描述来说明 TPS 曲线，我在第 6 篇文章中也说过，性能的衰减是逐步的（也有突然的情况，那是非常明显的性能瓶颈了），在最大 TPS 出现之前，就已经可以判断瓶颈是否出现了。

结合上面四个图，我们就有了如下的判断：

场景是递增的。

压力线程上升到 55（第四个阶梯）时，TPS 达到上限 680 左右，但是明显的，性能在第三个阶梯就已经接近上限了，

在压力线程达到 55 时，响应时间达到 85ms 左右，这个值并不高。

除此之外，其他的似乎不需要我们再做什么判断了。

也许这时候你会想问，那么瓶颈在哪里呢？总有人看到现象就想知道结果。但是这一次呢，我不打算满足这样的好奇心，因为本篇只是为了讲场景的逻辑，而不是瓶颈的分析。哈哈。

业务 2

从业务 2 开始，我们不做啰嗦的数据解释了，只说明一下关键点。我们看图。

Statistics 图：

线程数：

响应时间图：

TPS 图：

基于上面的四张图，我们可以看到：

这个单业务的最大 TPS 在 6000 以上。

响应时间变化比较小，基本上都在 10ms 以下，但也能明显看出在线程增加的过程中，响应时间也是在增加的。

这个业务由于 TPS 太高，响应时间太短，实在没啥可分析的。

业务 3

接下来再看一下业务 3 的情况。

Statistics：

线程数：

响应时间图：

TPS 图：

基于上面四张图，我们可以看到：

最大 TPS 将近 5000。

响应时间随着用户的增加而增加，在达到 4500TPS 时，响应时间在 6.5ms 左右。

业务 4

Statistics：

线程数：

响应时间图：

TPS 图：

基于上面四张图，我们可以看到：

最大 TPS 超过了 300。

响应时间随着用户的增而增加，在达到 300TPS 时，响应时间在 70ms 左右。

业务 5

Statistics：

线程数：

响应时间图：

TPS 图：

基于上面四张图，我们可以看到：

最大 TPS 在 550 左右。

响应时间随着用户的增而增加，在达到 550TPS 时，响应时间在 55ms 左右。

业务 6

Statistics：

线程数：

响应时间图：

TPS 图：

基于上面四张图，我们可以看到：

最大 TPS 超过了 2500。

响应时间随着用户的增而增加，在达到 4500TPS 时，响应时间在 16ms 左右。

有了上面这些单业务的容量结果，我们就可以做一个表格了：

还记得我们前面提到响应时间都不能大于 100ms 吧。通过测试结果我们可以看到，业务 1 已经接近这个指标了，也就是说这个业务如果在活动或促销期，有可能出现峰值最大 TPS 超过承受值的情况，超过了前面制定的响应时间指标。

有了这些基础数据之后，下面我们就可以设计容量场景了。

容量性能场景

我们希望得到的容量场景在本文的一开始就已经给出。下面我们通过设计线程来得到这个容量场景的结果。

你需要记住我们的重点：

场景不断。

控制比例。

我们这里只说一个容量性能场景，并且这个场景是峰值业务场景。如果在你的项目中，有特定的业务日，那就要根据业务日的业务比例，重新做一个针对性的场景。

在满足了最开始提到的业务比例之后，我们不断增加压力，得到如下结果。

Statistics：

线程数：

响应时间图：

总 TPS 图：

TPS 细分图：

从上面的结果可以看到，业务 4 和业务 5 的响应时间，随着业务的增加而增加，这显然在容量上会影响整体的性能。

在具体的项目中，这就是我们要分析调优的后续方向。

还有一点请你注意，并不是说，看到了性能瓶颈就一定要解决，事实上，只要业务指标可控，不调优仍然可以上线。这一点也是很多做性能测试的人会纠结的地方，感觉看到这种有衰减趋势的，就一定要把它给调平了。其实这是没有必要的。我们做性能是为了让系统能支持业务，即使性能衰减已经出现，性能瓶颈也在了，只要线上业务没有超出容量场景的范围，那就仍然可以上线。

另外再说几句闲话，做技术的人容易钻进这样的牛角尖，觉得明显有问题，结果公司老板不支持去调优处理，显然是老板不重视性能测试，于是深感自己不得志，工作也无精打采的。这就没必要了，做性能不是为了炫技，应该为业务服务。

我们再说回来，从总 TPS 图上看到，在容量测试中，我们仍然测试到了系统的上限。这是一个好事情，让我们可以判断出线上的系统配置应该是什么样的。

在达到了系统上限时，我们来看一个业务的比例（请你注意，我是不赞同用表格来承载分析数据的，但是作为最终的结果，给老板看的时候，还是要尽量说得通俗易懂）。

如下所示：

我们可以从上面的数据中看到，业务目标 TPS 已经达到，响应时间也没有超过指标。很好，这个容量就完全满足业务需求了。

但是！

如果业务要扩展的话，有两个业务将会先受到影响，那就是业务 4 和业务 5，因为它们的测试 TPS 和最大 TPS 最为接近。这是在我们推算业务扩展之后，再做架构分析时要重点考虑的内容。如果是在实际的项目中，这里会标记一个业务扩展风险。

请你注意，根据架构，性能测试组需要根据当前的测试状态整理架构的关键配置给线上系统做为参考，并且每个项目都会不一样，所以并不是固定的内容。我想做运维的看到这些值可能会更为亲切。

在这里，我给一个之前项目中的示例（由于属于项目交付类文档，所以这里只截取部分技术片断），如下所示：

配置整理的范围包括架构中所有和性能相关的技术参数。如下所示：

当然，这时我们也是要分析系统的资源使用率的。在本文为了避免混乱，所以我没有提及。在实际的项目中，我们还是要分析的哦。

说完了混合容量场景之后，我们回忆一下之前说过的两个重点，我的混合业务场景是不是没有断？是不是保持了业务比例？

下面我们就该说一下稳定性场景了。

稳定性性能场景

我在第 1 篇文章就提到过，稳定性场景的时间长度取决于系统上线后的运维周期。

在这个示例中，业务 + 运维部门联合给出了一个指标，那就是系统要稳定运行一周，支持 2000 万业务量。运维团队每周做全面系统的健康检查。当然谁没事也不用去重启系统，只要检查系统是否还在健康运行即可。大部分时候运维是等着系统警告的。

那么针对前面给出的容量结果，容量 TPS 能达到 3800（业务 1 到业务 6 的容量测试结果 TPS 总和）。所以稳定性场景时间应该是：20000000/3800 = 1.46 小时。

下面是两小时的稳定性场景运行情况，我在这里只做一下大概的说明。

Statistics：

线程数：

响应时间图：

TPS 细分图：

总 TPS 图：

从上面几张图可以看出，业务 2 和业务 3 对总 TPS 的动荡产生了影响，但系统并没有报错。这种周期性的影响，你可以去分析具体的原因，由于本篇是场景篇，所以这里不写分析过程，直接给出原因，这种影响是参数化数据周期性使用所导致的，有些数据的关联记录数多，有些数据的关联记录数少，数据库中变化倒是不大，但由于 TPS 过高，表现出来得就比较明显了。

其他的业务都比较正常，也比较稳定，没有报错。

总体业务量达到 27299712，也达到了稳定性业务量级的要求。

有一点，估计会有人提出疑问，你这个稳定性的总体 TPS，看起来和容量测试场景中差不多呀，有必要用容量测试中的最大 TPS 来跑稳定性吗？

这里就涉及到另一个被误解的稳定性知识点了。有很多人在资料中看到，稳定性测试应该用最大 TPS 的 80% 来跑。看到没有，这似乎又是一个由 28 原则导致的惯性思维。

在这里我要澄清一下。在具体的项目实施过程中，完全没有必要遵守这些看似有道理，实则对具体项目没什么用的原则。

这个系统用最大 TPS 能跑下来，业务一直很正常，稳定性目标能达到，为什么不能用最大 TPS 来跑呢？本来稳定性场景就是为了知道会不会由于长时间处理业务而引发潜在瓶颈（像内存泄露是个典型问题）。至于用多大的 TPS 来运行，又有什么关系？只要系统在正常处理，资源没有出现问题，也没有报错，那这个场景就是有效的，目标也是能达到的。

所以说，这里的稳定性场景，完全合理。但是，你觉得这样就完了吗？当然没有，我们还有异常场景要做嘛。

异常性能场景

我之前有提到过，异常性能场景要看架构图，但是涉及到职业素养的问题，我这里只能画个示意图来说明此系统的架构，以此来实现逻辑的完整性。示意图如下所示：

这是一个完全按生产架构来的示意图，在真实的测试过程中，也是这样搭建的。在这里有六个业务区域（包含基础架构区），也有 DMZ 区。

其实在每个区域中，根据架构中用到的技术组件，异常测试都有细化的场景设计，而在这里，我给你展示一个全局架构层的场景，用来说明异常场景。

这里运行的场景是：用容量场景中最大 TPS 的 50% 来做异常的压力背景。

咦，是不是会有人问：这里为什么只用 50% 了？稳定性性能场景不是还用 100% 的压力背景嘛？

这里我就要再说一遍，看目标！异常性能场景的目标是为了判断所要执行的操作对系统产生的影响，如果 TPS 不稳定，怎么能看出来异常点？当然是稳定无抖动的 TPS 是最容易做出异常动作产生的影响了。所以这里的 50% 是为了得到更为平稳的 TPS 曲线，以便做出正确的判断。

下面我们就要看异常场景的设计了。这是一个大的异常场景。

我分别对各区域中的业务应用服务器、数据库服务器以及基础架构服务器做异常操作（为了方便理解，下文我直接用 kill 来说明，其实在操作中，有些不是直接 kill，像断电、断网卡的手段也都可以用，取决于如何操作更为准确）。

下面是具体的操作步骤和时间记录。

第一部分：业务应用服务器。停下如下区域的一半应用服务器，查看 TPS、响应时间及其他服务器压力。

kill 区域三：17:02；

kill 区域一：17:15；

kill 区域二：17:20；

kill 区域五：17:25。

第二部分：基础架构服务器。停下一半的基础架构主机，查看 TPS、响应时间及另外主机压力的恢复情况。

kill 一台基础架构主机中的某个服务的某个节点：17:30。

第三部分：数据库服务器。停下 master 数据库，查看切换时间，slave 的压力及 TPS 的恢复情况。

reboot DB-20：17:36。6 分钟之后恢复；

reboot DB-26：18:07。1 分钟左右恢复；

reboot DB-2：18:20。2 分钟之后恢复。

第四部分：启动 master 数据库。

第五部分：启动被停的应用服务。

start 区域五：18:28；

start 区域三：18:33；

start 区域一：18:38；

start 区域二：18:43。

第六部分：启动被停基础架构主机中的某个服务的某个节点。

start 基础架构主机中的某个服务的某个节点：18:48。

根据上面的步骤，这里我放出 TPS 图来做分析。

从上图中的 TPS 趋势可以看出，停掉一半的区域三应用服务器后，对 TPS 有明显的影响。为啥呢？

我们来看一下细分的 TPS 图：

从图上看，并不是所有的 TPS 都在步骤 1 的时候受到了影响。受影响的只有业务 2 和业务 3。显然只有业务 2 和业务 3 走到了这个区域。但是这仍然是一个 BUG！！！

理论上，用一半的压力，停了一半的服务器，即便当前正在运行在被停掉的服务器上的 session 受到了影响，那 TPS 也应该会恢复的，但是 TPS 没恢复。所以先这里提个 BUG。

另外，停掉区域一、二、五的一半应用服务器，影响不大，TPS 有些许下降，但并没有报错，这个结果还可以接受。

停掉基础架构服务器时，TPS 有下降，但很快恢复了，非常好。

在步骤 6 时，记录的信息是在 6 分钟之后恢复的，这个时间有点久了。我在这里拆开 TPS 细节来看一下。

显然这段报错得比较多，6 分钟，一个 master 库切换过去。这怎么能接受呢？报 BUG！！！

另外，步骤 8 中，TPS 显然下降到底了。还好时间并不长，在 2 分钟后恢复。这个可以报 BUG。为什么说是可以报呢？因为这个时间并不算长。这里就有一个预期的问题。通常情况下，我们做 DB master 的异常切换，在这个架构中，是期望在 1 分钟内完成切换的。在我这个场景中，最快的数据库 master 切换是 40s。

请你注意，我看到有些厂商说数据库可以达到秒级切换，这种说法未免过于空泛。如果把”不到 1 分钟“称为秒级的话，那就欲盖弥彰了。我理解的秒级切换是一秒内，而不是单位是秒就可以。

通常这种 1 秒内切换说的都只是数据库实例的前面一层，有叫 Plus 的，有叫 Proxy 的，并且说的不是从出现异常，到判断切换的过程。而是说从切换动作开始到结束。另外，这个秒级切换也是有背景条件的。我们不要看广告，要看实际的操作结果。

请注意，上面提到的容量场景和异常场景，都只是项目中的一个场景。其实在这个项目中，我还有很多其它的容量场景和异常场景。从场景设计上来说，这些场景都大同小异，但都需要在大量的调研分析之后才能设计得出来。

总结

在对基准场景、容量场景、稳定性场景和异常场景做了详细的，有逻辑的描述之后，相信你已经能体会到场景的博大精深了。

在本篇中，由于字数有限，我只对场景的具体执行过程中的关键点做了细致地描述。但是场景绝对不止这些哦，还有很多细节的内容。

同时，为了让理解更为清晰，这里我只描述了场景本身，场景包括的其他内容，比如说参数设计、监控设计等等，在本篇中都没有描述。

从这里，你大概能明白我说的场景对性能有多么重要了吧。

希望今天的这篇文章能给你在设计性能场景时提供参考。

思考题

看完了今天的文章，你能说一下性能场景应该按什么样的逻辑设计吗？以及在稳定性场景中，为什么不用最大 TPS 的 80% 来做？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

14丨性能测试场景：如何理解业务模型？

性能场景中的业务模型是性能测试工作中非常重要的一部分。而在我们真实的项目中，业务模型跟线上的业务模型不一样的情况实在是太多了。原因可能多种多样，这些原因大大降低了性能测试的价值。

有人说，就是因为这样才应该直接用生产流量的方式来做嘛，这样就不用管业务模型了，直接就有生产的业务模型了。没错，只要你能通过生产流量扩大回放的方式实现压力部分，确实可以不用考虑业务场景了。但这么做的前提也必须是你的生产流量来源是可以覆盖想要测试的业务场景的。

回放的逻辑

回放的逻辑是这样的。

如果你喜欢的话，还可以在每一个业务产品和基础架构的层面做接口的回放，甚至我们可以直接在数据库中回放 SQL。而这些手段，都是为了模拟生产的业务模型。

这是非常容易理解的逻辑吧。

这里要批驳一个观点，就是有些人觉得只有通过生产流量回放的方式，才是真实地 模拟了线上的流量。事实上，这个观点是偏颇的。前几天有一个性能测试工程师问我一个流量回放过程中遇到的问题，谈到为什么要用流量回放。他说他们领导觉得这个最新潮最直接最正确，但实际上他得到的那段业务流量根本不能完全覆盖想测试的场景，最后折腾了一个月也是无功而返。

我知道，现在有很多人在各种场合说，可以直接在生产环境中，通过业务统计动态统计出业务场景，并实时实现到性能平台中去。这当然是一个很好的路子，但这个路子需要完整的技术实现，并且在不同的企业中，这种方式难就难在创建业务模型的统计算法，此外还要有高层领导的支持，才能真正实现完整的逻辑。

所以在今天的文章中，我想写的是最朴素的逻辑。那就是从生产数据统计，怎么转化到具体的场景中的业务模型。明白了这个逻辑之后，不管你是用生产流量回放，还是用实时业务量统计，还是线下业务量统计，你会发现原理都是一样的。

这是一个真实的案例，我已经把所有的业务名都替换掉了，同时对业务量级也做了降级调整，但这并不影响描述获取业务场景的完整性。

原系统的量级如下图所示：

这里我将降低 10 倍处理。

生产数据统计

首先我们从生产环境取出数据，粒度到秒级，取出所有业务的交易量数据。

业务量级按天统计的生成图如下：

我为什么要取这一段时间的数据呢？答案很简单，因为这一段时间完整地体现了这个业务系统的峰值数据。

从这样的数据中取出业务量最高的一天，最大的业务量是 2000 万左右。

注意，我这里说的是业务量最高的一天，并不是说我们的业务场景只从这一天产生，还有别的时间，可能业务量不多，但是业务比例会完全不同，这也是要取出来的场景，所以这个统计数据到业务模型的分析过程会比较细致。我们把这一天的逻辑说完后，你就会明白其他的场景获取方式。

接着，我再以小时为单位统计出业务量比例。如下图所示：

从上图显然可以看出哪个小时的业务量最大，那就是 9 点。

但是呢，你不要忘记了，在 16 点的时候，明显蓝色表示的那个业务量是大于 9 点时的业务量的。这个也是要取出来的场景。

如果需要更细的数据，我们可以以分钟为单位看一下这个小时内的业务量分布。

如果你的业务有必要从分钟或秒来看的话，可以按分钟或秒来取场景比例。在我们今天的这个案例中，取到小时就已经足够。因为我要的是业务模型，而不是生产 TPS 量级。

另外，既然说到了这里，我再把生产 TPS 量级的统计说一下。有了上面的分钟统计比例，就可以很容易统计出生产环境中每个业务的最大 TPS 了。这里得到的 TPS 将是最有效的测试是否通过的 SLA 指标。

下面我们再以小时为单位做出百分比图。

为什么要做百分比图呢？因为这个比例才是我们在性能场景中设置的 TPS 比例，是最直接有效的比例。

业务模型计算过程

针对这一天中的数据，我们将做出以下三个业务模型。

通用业务场景模型。就是将这一天的所有业务数加在一起，再将各业务整天的交易量加在一起，计算各业务量的比例。

9 点钟的业务模型。将 9 点钟的业务比例直接拿出来用。

16 点的业务模型。将 16 点钟的业务比例直接拿出来用。

首先我们看一下通用业务模型。

我们将上面的 0% 的业务全部删除，再计算一次百分比，得到测试场景中的业务比例。如下所示：

做为最基础的业务比例，这个可以覆盖大部分的业务时间了。

在通用的业务场景中，如果业务团队有给出明确的 TPS 指标，那就有依赖了。但是，如果没有给的话，也不要气馁。我们可以根据系统的运行时段，计算平均值即可。

因为我们这个系统是 24 小时系统，所以我用 24 小时来计算。得到如下值：

TPS1=24∗360020000000​=231

也就是说通用场景中，TPS 不能低于 231。

接着我们看下 9 点的业务模型。计算方法和上面一样，最后得出比例。

我们可以从小时图中看到，9 点的业务量总和有 120 万左右。为了方便，这里我拿 120 万来计算。它的生产 TPS 就是：1,200,000 / 3600 = 333。

TPS2=36001200000​=333

显然，这个模型下做场景时就不能低于 333TPS。

最后看一下 16 点的业务场景。

从小时图中，我们可以看到，16 点的业务量总和有 100 万左右。为了方便，这里我拿 100 万来计算。它的生产 TPS 就是：

TPS3=36001,000,000​=277

显然，这个模型下做场景时就不能低于 277TPS。

但是请注意，像 9 点业务模型中的业务 11，占比达到 30.25%；而 16 点业务模型中只有 8.69%。虽然 TPS 差不多，但是业务比例差别大，这两种业务模型下，对系统资源的消耗会完全不一样。

最后我稍微说一下 TPS 的控制。

有了这个计算过程，当我们把这些比例设计到场景中去的时候，一定要注意这些 TPS 的比例在运行过程中，不能发生大的变化。一旦压力发起后，由于各业务的响应时间随着压力的增加发生的变化量不同，就会导致运行过程中业务比例出现很大的偏差。

我们做性能测试工程师的，都有过这样的经历。通常，在 LoadRunner 里，会用pacing来控制 TPS，而用 JMeter 的，则会用Constant Throughput Timer来控制 TPS。

总结

在这一篇中，我描述了业务模型的来源和计算过程。其实就是一些常规的求和平均计算，只要判断出哪一段业务是我们需要的就可以了。

另外我也强调了，不管用什么炫酷的手段来实现生产环境的流量模拟，最终的目标是实现和线上比较接近的业务模型。不是说一定用生产流量回放才是正确的，只有适合自己企业的手段才是最正确的。

问题

那么最后给你留两个问题，为什么业务比例对性能场景如此重要？以及如何在执行场景过程中控制 TPS 比例呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

15丨性能测试场景：如何进行监控设计？

在性能测试中，我觉得监控是非常重要的环节。因为这是做性能分析的前提，走出这一步，才有后面的分析。

监控是性能分析承上启下的关键点。

设计监控是我们性能测试工程师必须要做的事情。当然了，仅仅设计监控是不够的，还要看懂监控数据才能分析。我们将在后面的篇幅一一拆解。

我觉得性能测试工程师也一定要自己去实现一遍监控的环节，而不是直接用其他团队搭建的监控工具。你可以自己找个 demo 服务器做一遍，这样才能真正理解后续要关注的点在哪里。

之前在一个项目上，我跟团队成员说，把监控一层层部署起来。有个小姑娘提出一个疑问：“监控有什么要部署的吗？不是用 JConsole 就好了吗？”我说每个工具都有功能的局限性，所以要多种工具配合在一起才能有完整的数据可分析。然后我又问她这个想法从哪来的。她说之前带她的一个测试经理说的，对 Java 的应用，只要用 JConsole 监控就好了。我不知道他们的沟通上下文，但我理解如果不是这姑娘在断章取义，那就是这个测试经理引导错误了。

监控平台还指望别人给搭好，点个链接就能出数据了，这显然不是一个技术人员该有的样子。

监控设计步骤

如果要让性能测试人员设计监控逻辑，要如何做呢？

首先，你要分析系统的架构。在知道架构中使用的组件之后，再针对每个组件进行监控。

其次，监控要有层次，要有步骤。有些人喜欢一上来就把方法执行时间、SQL 执行时间给列出来，直接干到代码层，让人觉得触摸到了最顶级的技能。然而在我的工作中，通常不这么做，应该是先全局，后定向定量分析。

最后，通过分析全局、定向、分层的监控数据做分析，再根据分析的结果决定下一步要收集什么信息，然后找到完整的证据链。

这才是监控应该有的步骤，才能体现监控的价值。

监控技术图谱

这张图是我认为在一个性能测试中，该有的技术图谱。

从这个图中我们可以看到，除了压力工具之外，还有很多技术细节。通常在各种场合下，我都会说，这些都是我们要学习的范围，做性能分析的人，不一定能完全能掌握这些内容，那你所在的性能团队就应该有这样的能力。因为性能团队要推进瓶颈的定位解决，所以要有和其他团队正面沟通的能力。

下面我们就以具体的操作过程来说明设计的落地过程。

现在的流行框架（比如说 Spring Cloud）中的熔断监控、限流服务、服务健康检查／监控、链路监控、服务跟踪、聚合监控等等，都是非常好的监控手段。比如说下面这样的架构图：

这是比较常见的微服务技术架构。其中很多开源工具已经提供了监控的能力。在网上也能找到一些部署搭建的资料，好像不提微服务、全链路就不好意思见人了似的。

对技术的发展，我们要拥抱。但对思路的梳理更为重要，因为框架平台工具都是为了实现目标而存在的。

在本篇中，我们还是回归根本，说一下监控设计的思路，讲清楚性能测试中应该如何拆分监控的点。当你看完了之后，即使是面对不同的架构，也有监控部署的思路。

架构图

那么我们就来到开始的位置了。做性能监控之前，先画一个最简单的架构图，看一下架构中各有什么组件，各有什么服务，将这些列下来，再找对应的监控手段和方式，看哪种手段和方式在性能测试过程中成本最低，效率最高。

如果把性能归到测试的这个阶段，那就必须先考虑测试的具体情况。

有些企业因为有长期的积累，监控平台完整又稳定，那显然是最好的。如果是短期项目类的性能测试，又涉及到多方企业的，基本上不要想有完整成熟的监控平台这件事了。

但是不管怎么样，我们都需要拿到架构的全局监控数据。针对下面的这个不大的架构，我们来考虑下如何拆分。

需要监控的内容如下：

操作系统

Nginx

Tomcat

Redis

MySQL

下面我就来细化下这个简单架构的监控设计。

监控设计

下图可以大概说明我对监控的整体设计理念。

我来说明一下：

我们要对整个架构做分层。

在每一个层级上列出要监控的计数器。

寻找相应的监控工具，实现对这些计数器的监控。如果一个工具做不到，在定位过程中考虑补充工具。

要做到对每层都不遗漏。

从大的分类上来看，我们识别出每个监控的节点和层级，再对应到架构中，如下图所示：

最适合的监控方式是什么样的呢？那就是成本最低，监控范围最大，效率最快。而是否持久就不再是考虑的重点了，因为项目结束了，监控工具可能也被拆了。

在企业中，我们也是首先考虑快速的监控实现。但是，还要一点要考虑，就是监控的持久有效性，能一直用下去。所以，在快速实现了之后，在必要时，会做一些二次开发，定制监控。

对了，这里再提一句，我不建议一开始就把代码级的监控给加进来。不光是因为它消耗资源，更重要的是，真的没有太大的必要。像方法的执行时间这类监控，如果没有定位到它们有问题，我们为什么要去看呢？当我们有了证据链的时候，是不是更一针见血呢？

所以最重要的是，我想看到的数据，到底能不能看得到。

对于上述的每个组件，我都建议用下面这样的监控思路。敲黑板！下图是重点！

有人可能会想说：就这几个字还值当画个图吗？我觉得非常有必要。因为全局到定向的思路帮我解决了很多的问题。

全局监控设计

那么什么是全局监控呢？

OS 层（CentOS 为例）

就拿 OS 来说吧，我们一般进到系统中，看的就是 CPU、I/O、内存、网络的使用率，这是很常规的计数器。在很多人看来，这些计数器是可以反应出一个系统的全局健康状态的。

先不管通过这些计数器得到的结论是不是对的。我们首先要知道的是，要有这样全局监控的潜意识，之所以说潜意识，是因为很多人不知道为什么看这些，但还是这样看了。

那么实际上做一个 OS 的全局监控需要看多少个计数器呢？我们看下架构图。

因为新版内核没有给更细的内核架构图，所以我用 2.6.26 版本的 Linux 内核架构图来说明思路。

给这张图的目的就是建议先看架构图，再考虑要监控的大分类有多少。从上图中，我们可以看到有这么几类，system、processing、memory、storage、networking 等。

这里画出一个思维导图，给出我的经验计数器。

针对 OS，我通常看上图中红色计数器的部分，这是 OS 查看的第一层。有第一层就有第二层，所以才需要定向的监控。后面我们再说定向监控的思路。

DB 层（MySQL 为例）

我们再说 DB 层，以 MySQL 为例。和上面的理念一样，我们也要看架构图。

此图来自于 MySQL 官方，各大技术网站均有展示。

接着我们看下全局监控的分类，如下图所示：

同样，这也是 MySQL 全局监控的第一层。

这个内容的整理并不具有什么技术性。稍微了解一下 Linux 和 MySQL 的架构，就可以整理出来。我们依此类推，按照这个思路，就可以把其他的组件都整理出第一层监控组件。

有了全局监控，接着就是定向监控了。这是寻找证据链的关键一节。

定向监控

有了 OS 层的全局监控计数器，我们首先要学会的，就是判断这些计数器说明了什么问题。我在第三模块中写监控分析工具，会详细说明这部分。

这里呢，我先把定向监控细化地解释一下，把这个思路给你讲得明明白白，通通透透。

OS 层之定向监控细化 1

当你看到 CPU 消耗得多，那么你就得按照下面这张图细化思路（从左向右看）：

列出流程图来就是如下所示：

st=>start: 开始
e=>end: 结束
op1=>operation: 通过si CPU找到对应的软中断号及中断设备
op2=>operation: 再找到软中断对应的模块
op3=>operation: 再到模块对应的实现原理
op4=>operation: 给出解决方案
st->op1->op2->op3->op4->e

OS 层之定向监控细化 2

当你看到 OS 全局监控图中的 Network 中的 Total 总流量比较大时，就要有这样的分析思路（从右向左看）：

列出流程图来就是如下所示：

st=>start: 开始
e=>end: 结束
op1=>operation: 网络总流量
op2=>operation: 分析性能场景中的业务流量
op3=>operation: 分析网络带宽
op4=>operation: 分析网络队列
op5=>operation: 解决方案
st->op1->op2->op3->op4->op5->e

依此类推，就可以列出更多 OS 层的定向监控分析的思路。

DB 层之定向监控细化 1

同 OS 层的定向监控细化思路一样，在 DB 层中要想找到完整的链路，那么在 MySQL 中也必须把逻辑想明白。

当你发现查询和排序的报表有问题时，比如说下面这样（数据来自于 MySQL Report）：

__ SELECT and Sort _____________________________________________________
Scan            7.88M     2.0/s %SELECT:  38.04
Range         237.84k     0.1/s            1.15
Full join       5.97M     1.5/s           28.81
Range check   913.25k     0.2/s            4.41
Full rng join  18.47k     0.0/s            0.09
Sort scan     737.86k     0.2/s
Sort range     56.13k     0.0/s
Sort mrg pass 282.65k     0.1/s

居然每秒就能有 2 次全表扫描！那该怎么办呢？定向细化，如下所示：

相信这样常规的动作，你肯定能掌握得了。

那么来看下一个。

DB 层之定向监控细化 2

当你看到锁数据的时候，如下所示：

__ InnoDB Lock _________________________________________________________
Waits          227829     0.1/s
Current             1
Time acquiring
  Total     171855224 ms
  Average         754 ms
  Max            6143 ms

当前等待并不多，只有 1。但是你看下面的平均时间为 754ms，这还能不能愉快地玩耍了？

下面我也同样列出定向监控细化的思路。

分析产生锁的 SQL，看 SQL 的 Profiling 信息，再根据信息找下一步原因，最终给出解决方案。

有了上面的全局—定向监控思路，并且将每个组件的第一层的计数器一一列出。这是我们监控分析的第一步。

至于定向监控部分，我不建议一开始就列，主要原因有三个：

耗费太多时间；

列出来也可能半辈子也用不上；

照搬列出来的定向监控逻辑，有可能误导你对实时数据的判断。

所以最好的定向监控就是在实际的性能执行过程中，根据实际的场景画出来。这帮助我在工作中无往不利，理清了很多一开始根本想不到的问题。

监控工具

有了思路，工具都不是事儿。

针对上面我们画的架构图，我大概列出相应的监控工具及优缺点。这里列得并不详尽，只供借鉴思路使用。

如果要选择的话，肯定是用 Prometheus + Exporter 的思路会好一点。于是我们这样实现全局的监控。

OS：

DB：

Nginx：

Redis：

上面图看腻了，你能换个吗？客官别着急，现在就换。

Tomcat：

好了，有了这些监控工具，基本上对每个组件的全局监控就解决了。

这时可能会有人说，你这个架构也太不新潮了。现在都玩 Kubernetes、Docker、Spring Cloud、微架构啥的了。

那同样，我们还是要列出有哪些监控的组件。

Node：就是 OS。

Cluster；

Pod；

微服务链路。

然后再实现相应的全局监控。我们可以在 Kubernetes+Docker 下可以看到这样的部分全局监控数据。

DashBoard：

Cluster：

Pod：

微服务链路：

那么具体的定向监控细化呢？在你的具体场景中，照样可以通过上面的思路标识出来。

有人说，那我还有什么什么组件，相信你通看全篇，已经学会思路了，那就自己动手吧。

我想说的是，不管你的架构有多么复杂，组件有多少，这样的监控逻辑都是一定要有的。适合的工具要用，并且尽量多用，但工具还远远替代不了分析的思维逻辑。没有判断的能力，再强悍的工具也只是个花架子。

PS：如没有注明引用，本专栏所有的截图都是在我搭建的环境中截来的，所以不存在在其他地方看到相同图的可能性。

总结

在本篇中，我描述了监控设计的思维逻辑。对架构中的组件进行了分析之后，通过全局—定向的思路列出要看的计数器，再通过相应的监控工具去实现，拿到要分析的数据。

这就完成了要做的监控设计和具体实施。

至于你是用什么工具去实现的，这并不重要，因为拿到监控数据，可供分析证据链最重要。

思考题

看完了今天的文章，你不妨说下为什么要先有全局监控，再有定向监控？以及我为什么不建议一开始就上代码级的监控工具呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

春节策划丨快来挑战一下自己的分析逻辑吧！

Hi，我之前给你出了一套试卷，不知道你答得怎么样了呢？

今天我们继续来挑战！

在性能测试分析的过程中，有两个关键点是做分析的人必须要关注的，第一点就是，在压力工具执行完成之后，要学会判断瓶颈是否出现，以及是否应该继续加压。其次，在分析中，要学会分段会层，如果发现是某个节点出现了问题，就要学会从已有问题的计数器顺藤摸瓜找到具体的证据链。

要想做到这一点，就需要有基础知识的积累和清晰的判断思路。

鉴于此，选择题之外，我又想了两道主观题，帮助你复习一下我们之前讲到的核心思路。

从压力工具中的响应时间和 TPS 图，如何判断出性能瓶颈？说明判断逻辑。

从操作系统的 us CPU 高或者 wa CPU 高，如何判断出根本原因，请画出你的分析逻辑证据链。

你可以从专栏的第 2 篇、第 6 篇和第 17 篇找到相关的线索。

这是一个可简单、可复杂的主观题。在面试中，我经常会问到这样的问题，一般来说，回答都会反应出面试者的综合能力。有的人可能三两句就说完了，那显然功底不够，而有的人会有很多的经验可以分享，逻辑清晰。本专栏从前到后都在描述着一个完整的分析蓝图，而这个蓝图就是靠证据链来完成的。

所以对这个题目的回答就体现了你对性能分析理念的理解程度。

把你想到的判断逻辑放到 GitHub 上吧！我会查看这些答案，与你一起交流！

春节策划丨性能评估和性能分析试题，等你挑战！

你好，今天是除夕，先祝你春节快乐，同时也要注意保护身体！

在春节这 7 天假期里呢，为了让你能轻松愉快地欢度佳节，同时也能巩固复习之前讲到的知识，我特意做了两份试卷，供你测验。今天发布的题目是一些选择题，题目不多，祝你答题愉快！

里面一共包含 25 道选择题，这些内容全部来自之前专栏中所讲的内容，希望可以帮你巩固一些知识。快来检测一下吧！

在初五这天呢，还有两道主观题在等着你。

让我们春节之后整装待发，继续学习新的内容！

16丨案例：性能监控工具之Grafana+Prometheus+Exporters

在本模块中，我将把几个常用的监控部分给梳理一下。前面我们提到过，在性能监控图谱中，有操作系统、应用服务器、中间件、队列、缓存、数据库、网络、前端、负载均衡、Web 服务器、存储、代码等很多需要监控的点。

显然这些监控点不能在一个专栏中全部覆盖并一一细化，我只能找最常用的几个，做些逻辑思路的说明，同时也把具体的实现描述出来。如果你遇到了其他的组件，也需要一一实现这些监控。

在本篇中，主要想说明白下图的这个监控逻辑。

这应该是现在最流行的一套监控逻辑了吧。

我今天把常见的使用 Grafana、Prometheus、InfluxDB、Exporters 的数据展示方式说一下，如果你刚进入性能测试领域，也能有一个感性的认识。

有测试工具，有监控工具，才能做后续的性能分析和瓶颈定位，所以有必要把这些工具的逻辑跟你摆一摆。

所有做性能的人都应该知道一点，不管数据以什么样的形式展示，最要紧的还是看数据的来源和含义，以便做出正确的判断。

我先说明一下 JMeter 和 node_exporter 到 Grafana 的数据展示逻辑。至于其他的 Exporter，我就不再解释这个逻辑了，只说监控分析的部分。

JMeter+InfluxDB+Grafana 的数据展示逻辑

一般情况下，我们用 JMeter 做压力测试时，都是使用 JMeter 的控制台来查看结果。如下图所示：

或者装个插件来看结果：

或者用 JMeter 来生成 HTML：

这样看都没有问题，我们在前面也强调过，对于压力工具来说，我们最多只关心三条曲线的数据：TPS（T 由测试目标定义）、响应时间、错误率。这里的错误率还只是辅助排查问题的曲线，没有问题时，只看 TPS 和响应时间即可。

不过采取以上三种方式有几个方面的问题。

整理结果时比较浪费时间。

在 GUI 用插件看曲线，做高并发时并不现实。

在场景运行时间比较长的时候，采用生成 HTML 的方式，会出现消耗内存过大的情况，而实际上，在生成的结果图中，有很多生成的图我们并不是那么关注。

生成的结果保存之后再查看比较麻烦，还要一个个去找。

那么如何解决这几个问题呢？

用 JMeter 的 Backend Listener 帮我们实时发送数据到 InfluxDB 或 Graphite 可以解决这样的问题。Graphite Backend Listener 的支持是在 JMeter 2.13 版本，InfluxdDB Backend Listener 的支持是在 JMeter 3.3 的版本，它们都是用异步的方式把数据发送出来，以便查看。

其实有这个 JMeter 发送给 InfluxDB 的数据之后，我们不需要看上面的那些 HTML 数据，也可以直观地看到系统性能的性能趋势。并且这样保存下来的数据，在测试结束后想再次查看也比较方便比对。

JMeter+InfluxDB+Grafana 的结构如下：

在这个结构中，JMeter 发送压力到服务器的同时，统计下 TPS、响应时间、线程数、错误率等信息。默认每 30 秒在控制台输出一次结果（在 jmeter.properties 中有一个参数 #summariser.interval=30 可以控制）。配置了 Backend Listener 之后，将统计出的结果异步发送到 InfluxDB 中。最后在 Grafana 中配置 InfluxDB 数据源和 JMeter 显示模板。

然后就可以实时查看 JMeter 的测试结果了，这里看到的数据和控制台的数据是一样。

但如果这么简单就说完了，这篇文章也就没价值了。下面我们来说一下，数据的传输和展示逻辑。

JMeter 中 Backend Listener 的配置

下面我们就 InfluxDB 的 Backend Listener 做个说明。它的配置比较简单，在脚本中加上即可。

我们先配置好 influxdb Url、application 等信息，application 这个配置可以看成是场景名。

那么 JMeter 如何将数据发给 InfluxDB 呢？请看源码中的关键代码，如下所示：

    private void addMetrics(String transaction, SamplerMetric metric) {
        // FOR ALL STATUS
        addMetric(transaction, metric.getTotal(), metric.getSentBytes(), metric.getReceivedBytes(), TAG_ALL, metric.getAllMean(), metric.getAllMinTime(),
                metric.getAllMaxTime(), allPercentiles.values(), metric::getAllPercentile);
        // FOR OK STATUS
        addMetric(transaction, metric.getSuccesses(), null, null, TAG_OK, metric.getOkMean(), metric.getOkMinTime(),
                metric.getOkMaxTime(), okPercentiles.values(), metric::getOkPercentile);
        // FOR KO STATUS
        addMetric(transaction, metric.getFailures(), null, null, TAG_KO, metric.getKoMean(), metric.getKoMinTime(),
                metric.getKoMaxTime(), koPercentiles.values(), metric::getKoPercentile);
​
​
        metric.getErrors().forEach((error, count) -> addErrorMetric(transaction, error.getResponseCode(),
                    error.getResponseMessage(), count));
    }

从这段代码可以看出，站在全局统计的视角来看，这里把 JMeter 运行的统计结果，比如事务的 Total 请求、发送接收字节、平均值、最大值、最小值等，都加到 metric 中，同时也会把成功和失败的事务信息添加到 metric 中去。

在源码中，还有更多的添加 metric 的步骤，你有兴趣的话，也可以看一下 JMeter 源码中的InfluxdbBackendListenerClient.java。

保存了 metric 之后，再使用 InfluxdbMetricsSender 发送到 Influxdb 中去。发送关键代码如下：

   @Override
    public void writeAndSendMetrics() {
 ........
        if (!copyMetrics.isEmpty()) {
            try {
                if(httpRequest == null) {
                    httpRequest = createRequest(url);
                }
                StringBuilder sb = new StringBuilder(copyMetrics.size()*35);
                for (MetricTuple metric : copyMetrics) {
                    // Add TimeStamp in nanosecond from epoch ( default in InfluxDB )
                    sb.append(metric.measurement)
                        .append(metric.tag)
                        .append(" ") //$NON-NLS-1$
                        .append(metric.field)
                        .append(" ")
                        .append(metric.timestamp+"000000")
                        .append("\n"); //$NON-NLS-1$
                }

                StringEntity entity = new StringEntity(sb.toString(), StandardCharsets.UTF_8);

                httpRequest.setEntity(entity);
                lastRequest = httpClient.execute(httpRequest, new FutureCallback<HttpResponse>() {
                    @Override
                    public void completed(final HttpResponse response) {
                        int code = response.getStatusLine().getStatusCode();
                        /*
                         * HTTP response summary 2xx: If your write request received
                         * HTTP 204 No Content, it was a success! 4xx: InfluxDB
                         * could not understand the request. 5xx: The system is
                         * overloaded or significantly impaired.
                         */
                        if (MetricUtils.isSuccessCode(code)) {
                            if(log.isDebugEnabled()) {
                                log.debug("Success, number of metrics written: {}", copyMetrics.size());
                            }
                        } else {
                            log.error("Error writing metrics to influxDB Url: {}, responseCode: {}, responseBody: {}", url, code, getBody(response));
                        }
                    }
                    @Override
                    public void failed(final Exception ex) {
                        log.error("failed to send data to influxDB server : {}", ex.getMessage());
                    }
                    @Override
                    public void cancelled() {
                        log.warn("Request to influxDB server was cancelled");
                    }
                });
 ........
            }
        }
    }

通过 writeAndSendMetrics，就将所有保存的 metrix 都发给了 InfluxDB。

InfluxDB 中的存储结构

然后我们再来看下 InfluxDB 中如何存储：

> show databases
name: databases
name
----
_internal
jmeter
> use jmeter
Using database jmeter
>
> show MEASUREMENTS
name: measurements
name
----
events
jmeter
> select * from events where application='7ddemo'
name: events
time                application text                title
----                ----------- ----                -----
1575255462806000000 7ddemo      Test Cycle1 started ApacheJMeter
1575256463820000000 7ddemo      Test Cycle1 ended   ApacheJMeter
..............
n> select * from jmeter where application='7ddemo' limit 10
name: jmeter
time                application avg                count countError endedT hit max maxAT meanAT min minAT pct90.0            pct95.0           pct99.0 rb responseCode responseMessage sb startedT statut transaction
----                ----------- ---                ----- ---------- ------ --- --- ----- ------ --- ----- -------            -------           ------- -- ------------ --------------- -- -------- ------ -----------
1575255462821000000 7ddemo                                          0              0     0          0                                                                                     0               internal
1575255467818000000 7ddemo      232.82352941176472 17    0                 17  849              122       384.9999999999996  849               849     0                               0           all    all
1575255467824000000 7ddemo      232.82352941176472 17                          849              122       384.9999999999996  849               849     0                               0           all    0_openIndexPage
1575255467826000000 7ddemo      232.82352941176472 17                          849              122       384.9999999999996  849               849                                                 ok     0_openIndexPage
1575255467829000000 7ddemo                                          0              1     1          1                                                                                     1               internal
1575255472811000000 7ddemo      205.4418604651163  26    0                 26  849              122       252.6              271.4             849     0                               0           all    all
1575255472812000000 7ddemo                                          0              1     1          1                                                                                     1               internal
1575255472812000000 7ddemo      205.4418604651163  26                          849              122       252.6              271.4             849                                                 ok     0_openIndexPage
1575255472812000000 7ddemo      205.4418604651163  26                          849              122       252.6              271.4             849     0                               0           all    0_openIndexPage
1575255477811000000 7ddemo      198.2142857142857  27    0                 27  849              117       263.79999999999995 292.3500000000001 849     0                               0           all    all

这段代码也就是说，在 InfluxDB 中，创建了两个 MEASUREMENTS，分别是 events 和 jmeter。这两个各自存了数据，我们在界面中配置的 testtile 和 eventTags 放在了 events 这个 MEASUREMENTS 中。在模板中这两个值暂时都是不用的。

在 jmeter 这个 MEASUREMENTS 中，我们可以看到 application 和事务的统计信息，这些值和控制台一致。

在 Grafana 中显示的时候，就是从这个表中取出的数据，根据时序做的曲线。

Grafana 中的配置

有了 JMeter 发送到 InfluxDB 中的数据，下面就来配置一下 Grafana 中的展示。首先，要配置一个 InfluxDB 数据源。如下所示：

在这里配置好 URL、Database、User、Password 之后，直接点击保存即可。

然后添加一个 JMeter dashboard，我们常用的 dashboard 是 Grafana 官方 ID 为 5496 的模板。导入进来后，选择好对应的数据源。

然后就看到界面了。

这时还没有数据，我们稍后做个示例，看下 JMeter 中的数据怎么和这个界面的数据对应起来。

我们先看下图中两个重要的数据查询语句吧。

TPS 曲线：

SELECT last("count") / $send_interval FROM "$measurement_name" WHERE ("transaction" =~ /^$transaction$/ AND "statut" = 'ok') AND $timeFilter GROUP BY time($__interval)

上面这个就是 Total TPS 了，在这里称为 throughput。关于这个概念，我在第一篇中就已经有了说明，这里再次提醒，概念的使用在团队中要有统一的认识，不要受行业内一些传统信息的误导。

这里取的数据来自 MEASUREMENTS 中成功状态的所有事务。

响应时间曲线：

SELECT mean("pct95.0") FROM "$measurement_name" WHERE ("application" =~ /^$application$/) AND $timeFilter GROUP BY "transaction", time($__interval) fill(null)

这里是用 95 pct 内的响应时间画出来的曲线。

整体展示出来的效果如下：

数据比对

首先，我们在 JMeter 中配置一个简单的场景。10 个线程，每个线程迭代 10 次，以及两个 HTTP 请求。

也就是说，这时会产生 10x10x2=200 次请求。我们用 JMeter 跑起来看一下。

看到了吧，这个请求数和我们预想的一样。下面我们看一下 Grafana 中展示出来的结果。

还有针对每个事务的统计情况。

至此，JMeter 到 Grafana 的展示过程就完成了。以后我们就不用再保存 JMeter 的执行结果了，也不用等着 JMeter 输出 HTML 了。

node_exporter+Prometheus+Grafana 的数据展示逻辑

对性能测试来说，在常用的 Grafana+Prometheus+Exporter 的逻辑中，第一步要看的就是操作系统资源了。所以在这一篇中，我们将以 node_exporter 为例来说明一下操作系统抽取数据的逻辑，以便知道监控数据的来源，至于数据的含义，我们将在后续的文章中继续描述。

首先，我们还是要画一个图。

现在 node_exporter 可以支持很多个操作系统了。官方列表如下：

当然不是说只支持这些，你也可以扩展自己的 Exporter。

配置 node_exporter

node_exporter 目录如下：

[root@7dgroup2 node_exporter-0.18.1.linux-amd64]# ll
total 16524
-rw-r--r-- 1 3434 3434    11357 Jun  5 00:50 LICENSE
-rwxr-xr-x 1 3434 3434 16878582 Jun  5 00:41 node_exporter
-rw-r--r-- 1 3434 3434      463 Jun  5 00:50 NOTICE

启动：

[root@7dgroup2 node_exporter-0.18.1.linux-amd64]#./node_exporter --web.listen-address=:9200 &

是不是很简洁？如果想看更多的功能 ，可以查看下它的帮助。

配置 Prometheus

先下载 Prometheus：

[root@7dgroup2 data]# wget -c https://github.com/prometheus/prometheus/releases/download/v2.14.0/prometheus-2.14.0.linux-amd64.tar.gz
..........
100%[=============================================================================================>] 58,625,125   465KB/s   in 6m 4s

2019-11-29 15:40:16 (157 KB/s) - ‘prometheus-2.14.0.linux-amd64.tar.gz’ saved [58625125/58625125]

[root@7dgroup2 data]

解压之后，我们可以看到目录结构如下：

[root@7dgroup2 prometheus-2.11.1.linux-amd64]# ll
total 120288
drwxr-xr-x. 2 3434 3434     4096 Jul 10 23:26 console_libraries
drwxr-xr-x. 2 3434 3434     4096 Jul 10 23:26 consoles
drwxr-xr-x. 3 root root     4096 Nov 30 12:55 data
-rw-r--r--. 1 3434 3434    11357 Jul 10 23:26 LICENSE
-rw-r--r--. 1 root root       35 Aug  7 23:19 node.yml
-rw-r--r--. 1 3434 3434     2770 Jul 10 23:26 NOTICE
-rwxr-xr-x. 1 3434 3434 76328852 Jul 10 21:53 prometheus
-rw-r--r--  1 3434 3434     1864 Sep 21 09:36 prometheus.yml
-rwxr-xr-x. 1 3434 3434 46672881 Jul 10 21:54 promtool
[root@7dgroup2 prometheus-2.11.1.linux-amd64]#

在prometheus.yml中添加如下配置，以取数据：

  - job_name: 's1'
    static_configs:
    - targets: ['172.17.211.143:9200']

启动：

[root@7dgroup2 data]# ./prometheus --config.file=prometheus.yml &

这样就行了吗？当然不是。根据上面的流程图，我们还需要配置 Grafana。

配置 Grafana

首先配置一个数据源，非常简单。如下所示：

再配置一个 node_exporter 的模板，比如我这里选择了官方模板（ID：11074），展示如下：

数据逻辑说明

说明完上面的过程之后，对我们做性能测试和分析的人来说，最重要的，就是要知道数据的来源和含义了。

拿上面图中的 CPU 使用率来说吧（因为 CPU 使用率是非常重要的一个计数器，所以我们今天先拿它来开刀）。

我们先点一下 title 上的 edit，看一下它的 query 语句。

avg(irate(node_cpu_seconds_total{instance=~"$node",mode="system"}[30m])) by (instance)
avg(irate(node_cpu_seconds_total{instance=~"$node",mode="user"}[30m])) by (instance)
avg(irate(node_cpu_seconds_total{instance=~"$node",mode="iowait"}[30m])) by (instance)
1 - avg(irate(node_cpu_seconds_total{instance=~"$node",mode="idle"}[30m])) by (instance)

这些都是从 Prometheus 中取出来的数据，查询语句读了 Prometheus 中node_cpu_seconds_total的不同的模块数据。

下面我们来看一下，node_exporter暴露出来的计数器。

这些值和 top 一样，都来自于/proc/目录。下面这张图是 top 数据，我们可以比对一下。

到此，我们就了解到了操作系统中监控数据的取值逻辑了，也就是从操作系统本身的计数器中取出值来，然后传给 Prometheus，再由 Grafana 中的 query 语句查出相应的数据，最后由 Grafana 展示在界面上。

总结

为什么要解释数据的逻辑呢？

因为最近在工作中遇到一些情况，有人觉得有了 Prometheus+Grafana+Exportor 这样的组合工具之后，基本上都不再用手工执行什么命令了。但我们要了解的是，对于监控平台来说，它取的所有的数据必然是被监控者可以提供的数据，像 node_exporter 这样小巧的监控收集器，它可以获取的监控数据，并不是整个系统全部的性能数据，只是取到了常见的计数器而已。

这些计数器不管是用命令查看，还是用这样炫酷的工具查看，它的值本身都不会变。所以不管是在监控平台上看到的数据，还是在命令行中看到的数据，我们最重要的是要知道含义以及这些值的变化对性能测试和分析的下一步骤的影响。

后面我们将着重来解释这些细节。

问题

最后我问两个问题吧，，第一个问题是，JMeter 如何把数据推送到 Grafana 中呢？另外，同样是监控操作系统的计数器，监控平台中的数据和监控命令中的数据有什么区别？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

17丨CentOS：操作系统级监控及常用计数器解析（上）

我相信有一些人看到这篇文章的标题肯定有种不想看的感觉，因为这样的内容实在被写得太多太多了。操作系统分析嘛，无非就是 CPU 使用率、I/O 使用率、内存使用率、网络使用率等各种使用率的描述。

然而因为视角的不同，在性能测试和分析中，这始终是我们绕不过去的分析点。我们得知道什么时候才需要去分析操作系统，以及要分析操作系统的什么内容。

首先，我们前面在性能分析方法中提到，性能分析要有起点，通常情况下，这个起点就是响应时间、TPS 等压力工具给出来的信息。

我们判断了有瓶颈之后，通过拆分响应时间就可以知道在哪个环节上出了问题，再去详细分析这个操作系统。这就需要用到我们的分析决策树了。

你还记得我们在第 6 篇文章中提到的分析决策大树吗？今天我们单独把操作系统的这一环节给提出来，并加上前面说的细化过程，就可以得到下面的这个分析决策树。

在分段分层确定了这个系统所运行的应用有问题之后，还要记起另一件事情，就是前面提到的“全局—定向”的监控思路。

既然说到了全局，我们得先知道操作系统中，都有哪些大的模块。这里就到了几乎所有性能测试人员看到就想吐的模块了，CPU、I/O、Memory、Network…

没办法，谁让操作系统就这么点东西呢。我先画一个思维导图给你看一下。

我很努力地把一些常见指标的相应关系都画到了图中，你是不是已经看晕了？看晕就对了，别着急。

我们先要知道的是，面对这些大的模块，到底要用什么的监控手段来实现对它们的监控呢？要知道，在一篇文章中不可能详尽地描述操作系统，我会尽量把我工作中经常使用到的一些和性能分析相关的、使用频度高的知识点整理给你。

监控命令

我们经常用到的 Linux 监控命令大概有这些：top、atop、vmstat、iostat、iotop、dstat、sar等……请你注意我这里列的监控命令是指可以监控到相应模块的计数器，而不是说只能监控这个模块，因为大部分命令都是综合的工具集。

像这样的监控工具还能列上一堆，但这并不是关键，关键的是我们在什么时候能想起来用这些工具，以及知道这些工具的局限性。

比如说 top，它能看 CPU、内存、Swap、线程列表等信息，也可以把 I/O 算进去，因为它有 CPU 的 wa 计数器，但是它看不了 Disk 和 Network，这就是明显的局限性。之后出现的atop对很多内容做了整理，有了 Disk 和 Net 信息，但是呢，在一些 Linux 发行版中又不是默认安装的。vmstat呢？它能看 CPU、内存、队列、Disk、System、Swap 等信息，但是它又看不了线程列表和网络信息。

像这样的局限，我还能说上两千字。

当工具让你眼花缭乱的时候，不要忘记最初的目标，我们要监控的是这几大模块：CPU、I/O、Memory、Network、System、Swap。

然后，我们再来对应前面提到的“全局—定向”监控的思路。如果你现在仅用命令来监控这个系统，你要执行哪几个呢？

对应文章前面的思维导图，我们做一个细致的表格。

你会发现，vmstat可以看 Swap，但它能看的是si和so，看不到其他的计数器，但是top可以看到这些计数器……像这样的细节还有很多。

因为计数器非常多，又不是每个都常用。但是万一某个时候就需要用了呢？这个时候如果你不知道的话，就无法继续分析下去。

这里我主要想告诉你什么呢？就是用命令的时候，你要知道这个命令能干什么，不能干什么。你可能会说，有这些么多的计数器，还有这么多的命令，光学个 OS 我得学到啥时候去？

我要告诉你的是监控的思考逻辑。你要知道的是，正是因为你要监控 CPU 的某个计数器才执行了这个命令，而不是因为自己知道这个命令才去执行。这个关系我们一定要搞清楚。

那么逻辑就是这样的：

比如说，我想看下 OS 各模块的性能表现，所以执行 top 这个命令看了一些计数器，同时我又知道，网络的信息在top中是看不到的，所以我要把 OS 大模块看完，还要用netstat看网络，以此类推。

如果你还是觉得这样不能直接刺激到你的神经，懵懂不知道看哪些命令。那么在这里，我用上面的工具给你做一个表格。

命令模块对照表：

我虽然给出了这张表，但要想融会贯通，还需要你亲手画几遍，每个命令都练习很多遍。好，这个时候，我们就已经把全局监控的第一层的计数器基本看完了。

如果你站起来说：“高老师！你忽悠我，我这还有个想看的，你这里还没有！”

那你就在上面的表格中加上你想看的计数器和相关的命令就可以了，你的这个表格就会越来越丰富，丰富的过程中，也就慢慢厘清了自己的思路。

有了这些命令垫底之后，下面我们来看常用的监控平台。

监控平台 Grafana+Prometheus+node_exporter

这是现在用得比较多的监控平台了。在微服务时代，再加上 Kubernetes+Docker 的盛行，这个监控套装几乎是干 IT 的都知道。

我们来看一下常用的 Dashboard。为了理解上的通用性，我这里都用默认的信息，不用自己定制的。

Grafana.com 官方 ID：8919 的模板内容如下：

还记得我们要看系统的模块是哪几个吗？

CPU

Memory

I/O

Network

System

Swap

你可以自己对一下，是不是大模块都没有漏掉？确实没有。但是！上面的计数器你得理解。

我们先来看一下 CPU。

上图中有了 System、User、I/O Wait、Total，还记得我们上面说 top 里有 8 个 CPU 计数器吧，这里就 4 个怎么办？

Total 这个值的计算方式是这样的：

    1 - avg(irate(node_cpu_seconds_total{instance=~"$node",mode="idle"}[30m])) by (instance)

也就是说，它包括除了空闲 CPU 的其他所有 CPU 使用率，这其实就有 ni、hi、si、st、guset、gnice 的值。当我们在这个图中看到 System、User、I/O Wait 都不高时，如果 Total 很高，那就是 ni、hi、si、st、guset、gnice 计数器中的某个值大了。这时你要想找问题，就得自己执行命令查看了。

看完 CPU 之后，再看一下 Network。

上图中有网络流量图。可以看到只有“上传下载”，这个值似乎容易理解，但是不够细致。node_exportor 还提供了一个“网络连接信息”图。可以看到 Sockets_used、CurrEstab、TCP_alloc、TCP_tw、UDP_inuse 这些值，它们所代表的含义如下：

Sockets_used：已使用的所有协议套接字总量

CurrEstab：当前状态为 ESTABLISHED 或 CLOSE-WAIT 的 TCP 连接数

TCP_alloc：已分配（已建立、已申请到 sk_buff）的 TCP 套接字数量

TCP_tw：等待关闭的 TCP 连接数

UDP_inuse：正在使用的 UDP 套接字数量

这些值也可以通过查看“cat /proc/net/sockstat”知道。这是监控工具套装给我们提供的便利，

然后我们再来看下 Memory。

上图中有总内存、可用内存、已用内存这三个值。如果从应用的角度来看，我们现在对内存的分析，就要和语言相关了。像 Java 语言，一般会去分析 JVM。我们对操作系统的物理内存的使用并不关注，在大部分场景下物理内存并没有成为我们的瓶颈点，但这并不是说在内存上就没有调优的空间了。

关于内存这一块，我不想展开太多。因为展开之后内容太多了，如果你有兴趣的话，可以找内存管理的资料来看看。

其他几个模块我就不再一一列了，I/O、System、Swap 也都是有监控数据的。

从全局监控的角度上看，这些计数器也基本够看。但是对于做性能分析、定位瓶颈来说，这些值显然是不够的。

还记得我在前面提到的“先全局监控再定向监控”找证据链的理念吧。像 node_exporter 这样的监控套装给我们提供的就是全局监控的数据，就是大面上覆盖了，细节上仍然不够。

那怎么办呢？下面我就来一一拆解一下。

CPU

关于 CPU 的计数器，已经有很多的信息了。这里我再啰嗦一下。CPU 常见的计数器是 top 中的 8 个值，也就是下面这些：

%Cpu(s):  0.7 us,  0.5 sy,  0.0 ni, 98.7 id,  0.0 wa,  0.0 hi,  0.2 si,  0.0 st

含义我就不写了，你搜一下就会知道。

在 mpstat（Multi-Processor Statistics）中看到的是 10 个计数器：

[root@7dgroup3 ~]# mpstat -P ALL 3
Linux 3.10.0-957.21.3.el7.x86_64 (7dgroup3)   12/27/2019   _x86_64_  (2 CPU)

03:46:25 PM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03:46:28 PM  all    0.17    0.00    0.17    0.00    0.00    0.00    0.00    0.00    0.00   99.66
03:46:28 PM    0    0.33    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   99.67
03:46:28 PM    1    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00

这里多出来%guest、%gnice两个值。他们的含义在 Linux man page 中有详细的说明。

%guest ：Show the percentage of time spent by the CPU or CPUs to run a virtual processor.

%gnice：Show the percentage of time spent by the CPU or CPUs to run a niced guest.

你可以看到计数器的名字稍有不同，像top中的wa在mpstat中是%iowait，si是 mpstat 中的%soft。

在 Linux 中，这就是我们经常查看的 CPU 计数器了。在我的性能生涯中，常见的问题大部分都是体现在这么几个计数器上（排名有先后）：

us

wa

sy

si

首先，为了确定看到 CPU 高之后，接着往下分析的方向是绝对没有错的，建议你用Perf top -g先看一下 CPU 热点。perf默认用的是cpu-clock事件。这一步只是为了确定方向对不对。

那么如何从这几个计数器找到后续的证据链呢？下面就是我们定向监控分析的过程了。我要狂敲黑板了！！！

us CPU 是用户态进程消耗的 CPU 百分比。大家都知道怎么往下落。这个链就是下面这样的：

当然不是只有这几个命令可以用，你可以找到大把的工具用，我只是列出来常用的。你要是想炫技，可以自己写些脚本来做。这个过程几乎被写烂了，所以，我就不多余举例子了。

wa cpu是 I/O 读写等待消耗的 CPU 百分比。这个证据链怎么往下落呢？来看一下。

你看中间有一步跳跃，这就是 wa CPU 直接跳到线程了。为什么没有进程了呢？那是因为 iotop 有直接到线程的能力。如果你想先看进程也可以，记得执行 iotop -P。

sy CPU 是内核消耗的 CPU 百分比。这个问题就有点复杂了，因为它并没有一个固定的套路。但是它的分析链路仍然和 us CPU 高的分析链路差不多，只是这个进程可能不是应用的，而是系统自己的。但是，是什么导致内核进程占用 CPU 高呢。这可能和应用有关，当然也可能和配置有关。那么现在我们画一个它的分析链路。

其实在实际的分析过程中，也是这样的。如果我们看到一个系统的进程消耗了更多的资源，那就要去查一下这个进程是干吗的，看它的运行逻辑和配置文件。不一定所有情况都是配置的问题，但绝大多数情况是这个原因，只能说，在系统级别，我们遇到的内核进程本身有性能问题的情况还是很少的。大部分时候都只是配置问题。

si CPU 是软中断消耗的 CPU 百分比。什么是软中断呢？在 wikipedia.org 中有这样的描述：

In digital computers, an interrupt is an input signal to the processor indicating an event that needs immediate attention. An interrupt signal alerts the processor and serves as a request for the processor to interrupt the currently executing code, so that the event can be processed in a timely manner. If the request is accepted, the processor responds by suspending its current activities, saving its state, and executing a function called an interrupt handler (or an interrupt service routine, ISR) to deal with the event. This interruption is temporary, and, unless the interrupt indicates a fatal error, the processor resumes normal activities after the interrupt handler finishes.

简单点来说，当出现异常或资源争用时，它是用来管理秩序的。CPU 正在吭哧吭哧着干活呢，突然来了一个优先级高的，needs immediate attention，这时就会发一个中断信号给 CPU。作为一个干活的，CPU 谁的话都得听，这时候就把手头的工作现场保存一下，干这个优先级高的活。除非这个中断是致命的，不然 CPU 会在干完这个活之后再回去干之前的活，这就是一次软中断。

这个值，越多就越有问题，关键是它有多少才是有问题呢？这一点你从来没有看过有人给建议值对不对？因为它根本没有可以参考的值，在不同的应用和硬件环境中，si CPU 都会有很大差别。我见过软中断每秒几万多就有问题的，也见过软中断每秒 20 万都没有问题的。

下面我来照例画个分析的图看一下。

在这个判断的链路中，就是要把 si 的中断模块找出来，然后再分析这个模块的功能和配置。比如我们看网卡的中断，这是常见的一种性能问题。我们要知道网络是带宽不够？还是配置得不对？还是防火墙？还是啥啥啥别的原因？

如果是其他的模块也是一样的逻辑。

好，在知道了上面这几个常见的 CPU 计数器的分析证据链逻辑之后，我就不再详细画其他的 CPU 的计数器了。

ni 呢，在我职业生涯中就没见过它高过；hi 倒是见过，不过是因为硬件坏了；st 呢，只有无良商家不断超卖虚拟服务器才会出现。如果你在一个企业内部搭建的虚拟化基础平台上也经常看见这种情况，那就是公司太穷了，硬件都不够用。

总结

在操作系统的分析过程中，CPU 绝对是一个重点分析的对象，它的一举一动都牵动着性能分析的人，所以我们需要在 CPU 的高低上花很多的时间去分析判断。

幸运的是，当 CPU 使用率高的时候，我们可以有很多的手段来找到对应的根本原因。这个过程不仅分析思路完整，而且工具繁多。它不像网络那么绕，也不像内存那么复杂，逻辑上还是很清楚的。

思考题

看完了今天的内容，你不妨说一下为什么 CPU 是很多性能问题分析的方向性指标？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

18丨CentOS：操作系统级监控及常用计数器解析（下）

在上一篇文章中，我们已经讲了监控系统层面的分析思路以及 CPU 分析，今天我们分析一下操作系统中其他的层面。

首先是 I/O。

I/O

I/O 其实是挺复杂的一个逻辑，但我们今天只说在做性能分析的时候，应该如何定位问题。

对性能优化比较有经验的人（或者说见过世面比较多的人）都会知道，当一个系统调到非常精致的程度时，基本上会卡在两个环节上，对计算密集型的应用来说，会卡在 CPU 上；对 I/O 密集型的应用来说，瓶颈会卡在 I/O 上。

我们对 I/O 的判断逻辑关系是什么呢？

我们先画一个 I/O 基本的逻辑过程。我们很多人嘴上说 I/O，其实脑子里想的都是 Disk I/O，但实际上一个数据要想写到磁盘当中，没那么容易，步骤并不简单。

这个简化的图是思虑再三的结果。

I/O 有很多原理细节，那我们如何能快速地做出相应的判断呢？首先要祭出的一个工具就是iostat。

在这张图中，我们取出一条数据来做详细看下：

Device:   rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s  avgrq-sz
vda        0.00     0.67   18.33  114.33   540.00 54073.33   823.32
avgqu-sz   await r_await w_await  svctm  %util
127.01  776.75    1.76  901.01   7.54 100.00

我解释一下其中几个关键计数器的含义。

svctm代表 I/O 平均响应时间。请注意，这个计数器，有很多人还把它当个宝一样，实际上在 man 手册中已经明确说了：“Warning! Do not trust this field any more. This field will be removed in a future sysstat version.” 也就是说，这个数据你爱看就爱，不一定准。

w_await表示写入的平均响应时间；r_await表示读取的平均响应时间；r/s表示每秒读取次数；w/s表示每秒写入次数。

而 IO/s 的关键计算是这样的：

IO/s = r/s + w/s = 18.33+114.33 = 132.66
%util = ( (IO/s * svctm) /1000) * 100% = 100.02564%

这个%util是用svctm算来的，既然svctm都不一定准了，那这个值也只能参考了。还好我们还有其他工具可以接着往深了去定位，那就是iotop。

Total DISK READ :       2.27 M/s | Total DISK WRITE :    574.86 M/s
Actual DISK READ:       3.86 M/s | Actual DISK WRITE:      34.13 M/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
  394 be/3 root        0.00 B/s  441.15 M/s  0.00 % 85.47 % [jbd2/vda1-8]
32616 be/4 root     1984.69 K/s    3.40 K/s  0.00 % 42.89 % kube-controllers
13787 be/4 root        0.00 B/s    0.00 B/s  0.00 % 35.41 % [kworker/u4:1]
...............................

从上面的Total DISK WRITE/READ就可以知道当前的读写到底有多少了，默认是按照I/O列来排序的，这里有Total，也有Actual，并且这两个并不相等，为什么呢？

因为 Total 的值显示的是用户态进程与内核态进程之间的速度，而 Actual 显示的是内核块设备子系统与硬件之间的速度。

而在I/O交互中，由于存在cache和在内核中会做I/O排序，因此这两个值并不会相同。那如果你要说磁盘的读写能力怎么样，我们应该看的是Actual。这个没啥好说的，因为Total再大，不能真实写到硬盘上也是没用的。

在下面的线程列表中，通过排序，就可以知道是哪个线程（注意在第一列是 TID 哦）占的I/O高了。

Memory

关于内存，要说操作系统的内存管理，那大概开一个新专栏也不为过。但是在性能测试的项目中，如果不做底层的测试，基本上在上层语言开发的系统中，比如说 Java、Go、C++ 等，在分析过程中都直接看业务系统就好了。

在操作系统中，分析业务应用的时候，我们会关注的内存内容如下面的命令所示：

[root@7dgroup ~]# free -m
        total        used        free      shared  buff/cache   available
Mem:           3791        1873         421         174        1495        1512
Swap:             0           0           0
[root@7dgroup ~]#

total肯定是要优先看的，其次是available，这个值才是系统真正可用的内存，而不是free。

因为 Linux 通常都会把用的内存给cache，但是不一定会用，所以free肯定会越来越少，但是available是计算了buff和cache中不用的内存的，所以只要available多，就表示内存够用。

当出现内存泄露或因其他原因导致物理内存不够用的时候，操作系统就会调用OOM Killer，这个进程会强制杀死消耗内存大的应用。这个过程是不商量的，然后你在“dmesg”中就会看到如下信息。

[12766211.187745] Out of memory: Kill process 32188 (java) score 177 or sacrifice child
[12766211.190964] Killed process 32188 (java) total-vm:5861784kB, anon-rss:1416044kB, file-rss:0kB, shmem-rss:0kB

这种情况只要出现，TPS 肯定会掉下来，如果你有负载均衡的话，压力工具中的事务还是可能有成功的。但如果你只有一个应用节点，或者所有应用节点都被OOM Killer给干掉了，那 TPS 就会是这样的结果。

对内存监控，可以看到这样的趋势：

内存慢慢被耗光，但是杀掉应用进程之后，free内存立即就有了。你看上面这个图，就是一个机器上有两个节点，先被杀了一个，另一个接着泄露，又把内存耗光了，于是又被杀掉，最后内存全都空闲了。

在我的性能工作生涯中，这样的例子还挺常见。当然对这种情况的分析定位，只看物理内存已经没有意义了，更重要的是看应用的内存是如何被消耗光的。

对于内存的分析，你还可以用nmon和cat/proc/meminfo看到更多信息。如果你的应用是需要大页处理的，特别是大数据类的应用，需要关注下HugePages相关的计数器。

内存我们就说到这里，总之，要关注available内存的值。

NetWork

这里我们就来到了网络分析的部分了，在说握手之前，我们先看网络的分析决策链。

请看上图中，在判断了瓶颈在网络上之后，如果知道某个进程的网络流量大，首先肯定是要考虑减少流量，当然要在保证业务正常运行，TPS 也不降低的情况下。

Recv_Q 和 Send_Q

当然我们还要干一件事，就是可能你并不知道是在哪个具体的环节上出了问题，那就要学会判断了。网络I/O栈也并不简单，看下图：

数据发送过程是这样的。

应用把数据给到tcp_wmem就结束它的工作了，由内核接过来之后，经过传输层，再经过队列、环形缓冲区，最后通过网卡发出去。

数据接收过程则是这样的。

网卡把数据接过来，经过队列、环形缓冲区，再经过传输层，最后通过tcp_rmem给到应用。

你似乎懂了对不对？那么在这个过程中，我们有什么需要关注的呢？

首先肯定是看队列，通过netstat或其他命令可以看到Recv_Q和Send_Q，这两项至少可以告诉你瓶颈会在哪一端。如下图所示：

我画个表清晰地判断一下瓶颈点。

其实这个过程中，我还没有把防火墙加进去，甚至我都没说NAT的逻辑，这些基础知识你需要自己先做足功课。

在我们做性能分析的过程中，基本上，基于上面这个表格就够通过接收和发送判断瓶颈点发生在谁身上了。

但是，要是这些队列都没有值，是不是网络就算好了呢？还不是。

三次握手和四次挥手

我们先看握手图：

我发现一点，很多人以为三次握手是非常容易理解的，但是没几个人能判断出和它相关的问题。

握手的过程，我就不说了，主要看这里面的两个队列：半连接队列和全连接队列。

在 B 只接到第一个syn包的时候，把这个连接放到半连接队列中，当接到ack的时候才放到全连接队列中。这两个队列如果有问题，都到不了发送接收数据的时候，你就看到报错了。

查看半连接全连接溢出的手段也很简单，像下面这种情况就是半连接没建立起来，半连接队列满了，syn包都被扔掉了。

[root@7dgroup ~]# netstat -s |grep -i listen
8866 SYNs to LISTEN sockets dropped

那么半连接队列和什么参数有关呢？

代码中的backlog：你是不是想起来了ServerSocket(int port, int backlog)中的backlog？是的，它就是半连接的队列长度，如果它不够了，就会丢掉syn包了。

还有操作系统的内核参数net.ipv4.tcp_max_syn_backlog。

而像下面这样的情况呢，就是全连接队列已经满了，但是还有连接要进来，已经超过负荷了。

[root@7dgroup2 ~]# netstat -s |grep overflow
154864 times the listen queue of a socket overflowed

这是在性能分析过程中经常遇到的连接出各种错的原因之一，它和哪些参数有关呢？我列在这里。

net.core.somaxconn：系统中每一个端口最大的监听队列的长度。

net.core.netdev_max_backlog：每个网络接口接收数据包的速率比内核处理这些包的速率快时，允许送到队列的数据包的最大数目。

open_file：文件句柄数。

我们再来看下四次挥手。我遇到性能测试过程中的挥手问题，有很多都是做性能分析的人在不了解的情况下就去做各种优化动作而产生的。

先看一下 TCP 挥手图：

在挥手的逻辑中，和性能相关的问题真的非常少。

但有一个点是经常会问到的，那就是TIME_WAIT。不知道为什么，很多人看到TIME_WAIT就紧张，就想去处理掉，于是搜索一圈，哦，要改recycle/reuse的 TCP 参数，要改fin_time_out值。

至于为什么要处理TIME_WAIT，却没几个人能回答得上来。

在我的性能工作经验中，只有一种情况要处理TIME_WAIT，那就是端口不够用的时候。

TCP/IPv4的标准中，端口最大是 65535，还有一些被用了的，所以当我们做压力测试的时候，有些应用由于响应时间非常快，端口就会不够用，这时我们去处理TIME_WAIT的端口，让它复用或尽快释放掉，以支持更多的压力。

所以处理TIME_WAIT的端口要先判断清楚，如果是其他原因导致的，即使你处理了TIME_WAIT，也没有提升性能的希望。

如果还有人说，还有一种情况，就是内存不够用。我必须得说，那是我没见过世面了，我至今没见过因为TIME_WAIT的连接数把内存耗光了的。

一个 TCP 连接大概占 3KB，创建 10 万个连接，才100000x3KB≈300M左右，何况最多才 65535 呢？服务器有那么穷吗？

System

确切地说，在性能测试分析的领域里，System 似乎实在是没有什么可写的地方。

我们最常见的 System 的计数器是in(interrupts:中断)和cs(context switch：上下文切换)。

因为这是我能找得到的最疯狂的 System 计数器了。

中断的逻辑在前面跟你说过了。

cs也比较容易理解，就是 CPU 不得不转到另一件事情上，听这一句你就会知道，中断时肯定会有cs。但是不止中断会引起 cs，还有多任务处理也会导致cs。

因为cs是被动的，这个值的高和低都不会是问题的原因，只会是一种表现，所以它只能用来做性能分析中的证据数据。

在我们的这个图中，显然是由于in引起的cs，CPU 队列那么高也是由in导致的。像这样的问题，你可以去看我们在上篇文章中提到的si CPU高的那个分析链了。

Swap

Swap 的逻辑是什么呢？它是在磁盘上创建的一个空间，当物理内存不够的时候，可以保存物理内存里的数据。如下图所示：

先看和它相关的几个参数。

也就是说，当操作系统中配置了vm.swappiness是 30%，那么当内存用到1-30%=70%的时候，就会发生 Swap。

swapiness默认是 60%。注意，下面还有一个参数叫vm.min_free_kbytes。即使把vm.swappiness改为 0，当内存用到小于vm.min_free_kbytes时照样会发生 Swap。

想关掉 Swap 就swapoff -a。

和 Swap 相关的计数器有：top中的Total、free、used和vmstat里的si、so。

说到 Swap，在性能测试和分析中，我的建议是直接把它关了。

为什么呢？因为当物理内存不足的时候，不管怎么交换性能都是会下降的，不管是 Swap 还是磁盘上的其他空间，都是从磁盘上取数据，性能肯定会刷刷往下掉。

总结

对操作系统的监控及常用计数器的分析会涉及到很多的内容，所以两篇文章可能也是覆盖不全的，我只把在性能测试分析工作中经常见到的计数器解析了一遍。总体来说，你需要记住以下三点：

监控平台再花哨，都只是提供数据来给你分析的。只要知道了数据的来源、原理、含义，用什么工具都不重要。

性能分析的时候，不会只看操作系统一个模块或哪几个固定计数器的。这些动态的数据，需要有分析链把它们串起来。

操作系统提供的监控数据是分析链路中不可缺少的一环，除非你能绕过操作系统，又能很确切地定位出根本原因。

思考题

我为什么说用什么监控平台并不重要呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流进步。

19丨Java & C ++：代码级监控及常用计数器解析（上）

在性能测试分析中，有一部分人存在着一个思路上的误解，那就是一开始就一头扎进代码里，折腾代码性能。这是我非常反对的一种做法。

事实上，要想这么做，有一个前提，那就是架构中的其他组件都经过了千锤百炼，出现问题的可能性极低。

实际上，我凭着十几年的经验来看，大部分时候，代码出现严重性能瓶颈的情况还真是不多。再加上现在成熟的框架那么多，程序员们很多情况下只写业务实现。在这种情况下，代码出现性能瓶颈的可能性就更低了。

但我们今天终归要说代码级的监控及常用的计数器。如何去评估一个业务系统的代码性能呢？在我看来，分析的思路是下面这个样子的。

从上图可以看到，分析的时候有两个关键点：执行时间和执行空间。我相信很多人都清楚，我们要很快找到执行时间耗在哪一段和空间耗在哪里。

现在我们来实际操作一下，看如何判断。

Java 类应用查找方法执行时间

首先你得选择一个合适的监控工具。Java 方法类的监控工具有很多，这里我选择 JDK 里自带的 jvisualvm。

顺便说一下，我的 Java 版本号是这个：

(base) GaoLouMac:~ Zee$ java -version
java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)

打开应用服务器上的 JMX 之后，连上 jvisualvm，你会看到这样的视图。

这里再啰嗦一下我们的目标，这时我们要找到消耗 CPU 的方法，所以要先点Sampler - CPU，你可以看到如下视图。

从上图可以看到方法执行的累积时间，分别为自用时间百分比、自用时间、自用时间中消耗 CPU 的时间、总时间、总时间中消耗 CPU 的时间、样本数。

从这些数据中就可以看到方法的执行效率了。

但是，这里面 Method 这么多，我怎么知道哪个跟我的方法执行时间有关呢？比如说上面这个应用中，最消耗 CPU 的是 JDBC 的一个方法 fill。这合理吗？

先来看一下我的脚本。

从结构上你就能看出来，我做了登录，然后就做了创建的动作，接着就退出了。

这几个操作和数据库都有交互。拿 create 这个步骤来说，它的脚本非常直接，就是一个 POST 接口。

还记得前面我们怎么说查看后端的运行逻辑的吗？后端接收这个 POST 的代码如下：

    @RequestMapping("/save")
    @ResponseBody
    public Object save(Blog blog, HttpSession session){
        try{
            Long id = blog.getId();
            if(id==null){
                User user = (User)session.getAttribute("user");
                blog.setAuthor(user.getName());
                blog.setUserId(user.getId());
                blog.setCreateTime(new Date());
                blog.setLastModifyTime(new Date());
                blogWriteService.create(blog);
            }else {
                blog.setLastModifyTime(new Date());
                blogWriteService.update(blog);
            }
        }catch (Exception e){
            throw new JsonResponseException(e.getMessage());
        }
        return true;
    }

这段代码的功能就是讲前端内容接收过来放到实体中，然后通过 create 方法写到数据库中。那么 create 是怎么实现的呢？

    public void  create(Blog blog) {
        mapper.insert(blog);
        BlogStatistics blogStatistics = new BlogStatistics(blog.getId());
        blogStatisticsMapper.insert(blogStatistics);

它就是一个 mapper.insert，显然这个 create 是我们自己实现的代码，里面其实没有什么逻辑。而 ReadAheadInputStream.fill 是 create 中的 MyBatis 调用的 JDBC 中的方法。 从压力工具到数据库的调用逻辑就是：

而我们看到的最耗时的方法是最后一个，也就是 fill。实际上，我们应该关心的是 save 接口到底怎么样。我们来过滤下看看。

从 save 的结果上来看，它本身并没有耗什么时间，都是后面的调用在消耗时间。

我们再来看看 cerate。

它本身也没消耗什么时间。

顺着逻辑图，我们再接着看 MyBatis 中的 insert 方法。

就这样一层层找下去，最后肯定就找到了 fill 这个方法了。但是你怎么知道整个调用逻辑中有哪些层级呢？你说我可以看源码。当然不是不可以。但要是没有源码呢？做性能分析的人经常没有源码呀。

这个时候，我们就要来看栈了。这里我打印了一个调用栈，我们来看下这个逻辑。

"http-nio-8080-exec-1" - Thread t@42
   java.lang.Thread.State: RUNNABLE
  ...............
  at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:100)
    ...............
    ...............
    at com.sun.proxy.$Proxy87.create(Unknown Source)
  ...............
  at com.blog.controller.BackBlogController.save(BackBlogController.java:85)
  ...............
  at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
  - locked <4b6968c3> (a java.util.concurrent.ThreadPoolExecutor$Worker)

我把其他的都给清掉了，我们只看最简单的栈逻辑，其中 UnknownSource 的部分是因为反射实现的 insert 没有把源码反编译出来。

其实这个栈有 117 行，我怕你看晕。

从这一层一层的关系中，我们就可以知道调用逻辑了。知道调用逻辑的方法有很多，看源码也行，看编译后运行的代码也行，关键在于知道谁调了谁，这样就行了。

我这个还算是清晰的调用逻辑，要是代码调用关系再复杂一些，分分钟有想死有没有？

不过比较好的是，像 jvisualvm 这样的工具给我们提供了很多便利。这时可能有人会跳起来了，为什么不用 Arthas、BTrace 之类的工具呢？如果你喜欢的话，可以把 Athas 弄上，像下面这样。

[arthas@1]$ trace com.blog.controller.BackBlogController save
Press Q or Ctrl+C to abort.
Affect(class-cnt:2 , method-cnt:2) cost in 320 ms.
`---ts=2020-01-06 10:38:37;thread_name=http-nio-8080-exec-2;id=2b;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.ParallelWebappClassLoader@4f2895f8
    `---[29.048684ms] com.blog.controller.BackBlogController$$EnhancerBySpringCGLIB$$586fe45c:save()
        `---[28.914387ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #0
            `---[27.897315ms] com.blog.controller.BackBlogController:save()
                ...............
                `---[24.192784ms] com.blog.service.BlogWriteService:create() #85

这也能看出来 creat 是消耗了时间的。如果你接着跟踪 create 方法。如下所示：

[arthas@1]$ trace com.blog.service.BlogWriteService create //这一行是arthas中跟踪class中的某个具体方法的语句。
Press Q or Ctrl+C to abort.
Affect(class-cnt:2 , method-cnt:2) cost in 199 ms. //被跟踪方法的处理次数和时长
`---ts=2020-01-06 10:41:51;thread_name=http-nio-8080-exec-4;id=2f;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.ParallelWebappClassLoader@4f2895f8
    `---[6.939189ms] com.sun.proxy.$Proxy87:create()
`---ts=2020-01-06 10:41:51;thread_name=http-nio-8080-exec-10;id=38;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.ParallelWebappClassLoader@4f2895f8
        `---[4.144799ms] com.blog.service.BlogWriteServiceImpl:create()  //写接口中create方法耗时
            +---[2.131934ms] tk.mybatis.mapper.common.Mapper:insert() #24   //mybatis中insert方法耗时
                ...............
            `---[1.95441ms] com.blog.mapper.BlogStatisticsMapper:insert() #26 //insert方法调用了上面的create方法耗时

要是接着往下跟踪，就可以看到反射这一块了。

[arthas@1]$ trace tk.mybatis.mapper.common.Mapper insert
Press Q or Ctrl+C to abort.
Affect(class-cnt:5 , method-cnt:5) cost in 397 ms.
`---ts=2020-01-06 10:44:01;thread_name=http-nio-8080-exec-5;id=33;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.ParallelWebappClassLoader@4f2895f8
    `---[3.800107ms] com.sun.proxy.$Proxy80:insert()

类似的，你还可以玩 JDK 自带的工具 jdb，它也可以直接 attach 到一个进程上，调试能力也是不弱的。

在我看来，这些工具、手段都是为了实现从响应时间长<->代码行的分析过程。思路是最重要的。

另外也要说一下，现在有的 APM 工具也可以实现这样的功能，但是呢，我并不建议一开始就上这么细致的工具，因为不管 APM 产品吹得有多牛逼，它都是要消耗 10% 左右的 CPU 的。并且，你觉得直接在生产上装一个 APM 工具的 agent 到业务系统中是合理的吗？如果是自己实现的 metrics 方法，输出性能数据尚可接受，如果是别人的这类工具，还是算了。

在大部分时候，我都不建议在生产上用 APM 工具。万一生产上真的有极端的情况，需要看细致的性能问题，再临时 attach 上去，也可以做到。何必为了可能出现的问题而长时间地消耗资源呢。

总结

大部分时间里，性能测试和分析都在和时间打交道，而在时间的拆分逻辑中，我们在前面也提到过思路，如何一步步把时间拆解到应用当中，那就是分段。

当拆解到应用当中之后，就是抓函数方法的执行时间了。这是保证我们从前到后分析逻辑的关键一环，请你注意，是关键一环，而不是最初的一环。

通过这篇文章我想告诉你，在大部分的开发语言中，都有手段直接将方法的执行时间消耗抓出来，你可能现在还不知道是什么方法，没关系，因为跟踪的手段有很多，你可以临时去学习如何操作。

我只要在你的脑子里种下这样的一种印象，那就是，有办法抓到函数方法的执行时间消耗在哪里！

思考题

最后给你留两道思考题吧。我为什么不建议在生产环境中一开始就上 APM 类工具来抓取方法的执行时间呢？你有什么方法可以抓取到 Java 语言中的方法执行时间？如果你擅长其他语言，也可以描述其他语言中的方法执行时间抓取工具。

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

20丨Java & C ++：代码级监控及常用计数器解析（下）

在上一篇文章中，我们描述了在 Java 开发语言中如何抓取方法的执行时间，其中描述的操作也是我们在分析时经常使用的。

今天我们将接着描述如下几点内容：

Java 语言中如何查找有问题的内存对象。

简单介绍一下在 C/C++ 语言中如何查找方法执行时间和对象的内存消耗。

之所以要描述 C/C++ 语言的相关内容，就是为了告诉你，几乎在任何一语言中都有相应的工具，都有办法捕获到相应的内容。

下面我们来看看如何抓取 Java 应用中对象占用多大内存，以及如何分辨占用是合理的和不合理的。

Java 类应用查找对象内存消耗

对 Java 的内存分析通常都落在对 JVM 的使用上（不要认为我这句话说得片面），再具体一点，说的就是内存泄露和内存溢出。由于现在对象都是可变长的，内存溢出就不常见了；而由于底层框架的慢慢成熟，内存泄露现在也不常见了。

有人说了，那你还啰嗦个什么劲呢？别捉急呀，不常见不等于没有。只是说它不再是 No.1 级的问题，但是排在 No.2 级还是没问题的。

如果你的应用有了问题，看到了像这样的图：

这是我在一个项目中遇到的问题，图片不够清晰，我们只要关注黄线的趋势就好。

之所以把它拿出来说事，是因为这个问题太极端了。上图是近 20 天的 JVM 使用率，从曲线的趋势上就可以看出来，它存在明显的内存泄露，但是又泄露得非常非常慢。这个系统要求 24x365 运行。

做过运维的人会知道，如此长时间的运行，运维时间长了之后，只会对这样的系统做常规的健康检查，因为前期天天关注它，又不出问题，眼睛都看瞎了，也不值得，于是后期就放松了警惕，慢慢懈怠。

而这个系统在生产上出现事故是在正常运行快到一年的时候，这个系统的业务量不大，十几个 TPS 的业务量级。这是一个外贸的系统，业务量虽然不大，但每个业务涉及的金额很大。其实出故障时间倒也不长，才几个小时，但是也干掉了几个总监级职位及相关人员。

如何对内存进行分析，仍然是性能测试分析的从业人员应该知道的知识点。

我们从技术的角度来说一下内存问题的排查思路。

这下我换个实例程序。我们照样用 jvisualvm，记住哦，这时候 Arthas 之类的工具就没得玩了，因为 Arthas 只会操作栈，有很多在 Java 方面做性能分析的工具都是只分析栈的。在 Java 中动态操作对象，其实资源消耗非常高。打个比方，你可以想像一下，在一个课间休息的校园，像寻找一个特定的孩子有多难。

其实操作一个对象还有迹可循，但是内存中那么多对象，要想全都控制，那几乎是不理智的。所以，我们首先要看内存整体的健康状态。

内存趋势判断

场景一：典型的正常内存的场景

看了这个图后，要有如下几个反应：

内存使用很正常，回收健康。

内存从目前的压力级别上来看，够用，无需再增加。

无内存泄露的情况，因为回收之后基本回到了同一水位上。

基本也能看得出来 GC 够快。为什么说基本呢？因为最好还是看一下这张图。

从这张图可以看到，当应用在压力场景之后，GC 并没有消耗过多的 CPU。

场景二：典型的内存分配过多的场景

从这张图我们可以看出来：

内存使用很正常，回收健康。

从目前的压力级别上来看，内存不仅够用，而且过多。

无内存泄露的情况。

场景三：典型的内存不够用的场景

从这张图我们可以看出来：

内存使用很正常，回收健康。

从目前的压力级别上来看，内存不够用，需再增加。

CPU 可看可不看，因为现在看似乎没多大意义，先加了内存再说。

无内存泄露的情况，因为回收之后基本回到了同一水位上。

场景四：典型的内存泄露到爆的场景

为了显示我能力的多样性，我换个工具的监控结果。

看到上面这张图，你可能觉得人生面对着挑战：“啥玩意？”

实际上，这张图说明以下四点：

年轻代（第三列）、年老代（第四列）全满了，持久代在不断增加，并且也没有释放过。

两个保留区（第一列、第二列）都是空的。

Yonug GC（第六列）已经不做了。

Full GC（第八列）一直都在尝试做回收的动作，但是一直也没成功，因为年轻代、年老代都没回收下来，持久代也在不停涨。

如果出现了 1 和 2 的话，不用看什么具体对象内存的消耗，只要像网上那些只玩 JVM 参数的人一样，调调参数就行了。

但是如果出现 3 和 4，对于 3 还要再判断一下，之前的内存是不是设置得太小了？如果是，就调大，看能不能到场景一的状态。如果不是，那就得像场景四一样，查一下内存到底消耗在哪个对象上了。

查找增加的内存

逻辑一

下面我们来说说如何判断性能测试过程中内存的变化。

我们在内存中经常看到的对象是这样的。

如果你用 jmap 的话，会看到如下信息。

你可能会问，这么多的内容，我到底要看什么呢？这也是性能测试人员经常遇到的问题，明明数据都在眼前，就是不知道从哪下嘴。

我建议你不要看这些底层的对象类型，因为实在是有点多哇。在这里我们最好是看自己代码调用的对象的内存占用大小增量。

先过滤下我们自己的包。

点击一下 Deltas，就能看到下面的截图。

在刚开始点击 Deltas 之后，会看到全是零的对象。

下面我们来做下压力，观察一下。

你看现在对象的实体都在往上增加对吧？但是当压力停止之后，该回收的都回收了，而有些必须长久使用的对象，在架构设计上也应该清晰地判断增量，不然就有可能导致内存不够。出现这种情况一般是架构师的失职。像这类东西应该写到公司的代码规范里。

当内存正常回收之后，再观察 Deltas，应该会看到大部分对象都回收了的状态。如下所示：

临时的对象也都清理了。 这就是正常的结果。

如果停止压力之后，又做了正常的 FullGC 回收了之后，还是像下面这样。

那就显然有问题了。回收不了的对象就是典型的内存泄露了。

逻辑二

我们看下面这个图。这是 jmap 做出来的 heapdump，然后用 MAT 打开的。

1. 第一个可疑的内存泄露点占了 466.4MB 的内存。

2. 找到内存消耗点的多的内容。如下所示。

这是一个实体 bean。每个倒是不大，但是架不住有 79 万个。

3. 看它对应的栈。

就是一个数据库操作。

4. 取出 SQL，查看执行计划如下。

这是曲线的 SQL 查询数据过多，导致内存不够用。这个不叫泄露，这是溢出。因为要是一个人查询，就可能没事嘛，但是多个人一起查了，才会出问题。从业务的代码实现的角度上说，这绝对是个有问题的设计逻辑。如果真是必须全表扫描的，你得规定这个功能怎么用呀。如果不用全表扫描，干嘛不做过滤呢？

其实在 Java 中查找内存消耗的手段还有很多。你喜欢怎么玩就怎么玩，只要找得到就好。我只是给两种我觉得常用又易用的方式。

C/C++ 类应用查找方法执行时间

对 C/C++ 的应用来说，我们可以用 google-perftools 查找方法执行时间。当然，在这之时，你需要先安装配置好 google-perftools 和 libunwind。

google-perftools 是针对 C/C++ 程序的性能分析工具。使用它，可以对 CPU 时间片、内存等系统资源的分配和使用进行分析。

使用步骤如下：

编译目标程序，加入对 google-perftools 库的依赖。

运行目标程序，在代码中加入启动 / 终止剖析的开关。

将生成的结果通过剖析工具生成相应的调用图。

你可以在代码中加入固定输出剖析数据的开关，当运行到某段代码时就会执行。当然你也可以在代码中只加入接收信号的功能，然后在运行的过程中，通过 kill 命令给正在运行的程序发送指令，从而控制开关。

我来举个例子。如果我们有一个函数 f，我想知道它的执行效率。硬编码的方式就是在调用这个函数的前后加上剖析开关。

ProfilerStart("test.prof");//开启性能分析
f();
ProfilerStop();//停止性能分析

在程序编译之后，会在同目录生成一个叫 a.out 的可执行文件。

执行这个文件，就会生成 test.prof 文件。

然后执行命令：

pprof --pdf a.out test.prof >test.pdf

打开这个 PDF 就可以看到如下图：

你看到上面有很多只有地址而没有函数名的调用吗？那是没有符号表。这里我们不分析那些不是我们自己的函数，我们只看自己的函数 f。

看这一段，它有三行。

第一行：函数名；

第二行：不包含内部函数调用的样本数 (百分比) ；

第三行：of 包含内部函数调用的样本数 (百分比)。

是不是和 Java 中 self time/total time 有异曲同工之妙？它也可以实现从 CPU 使用率高到具体函数的定位。

你也许会说，这个有点复杂，还要在代码里加这么多，编译还要加上动态库啥的。当然了，你还可以用 perf 工具来跟踪 CPU clock，在代码编译时加上调试参数，就可以直接用 perf top -g 看调用过程由每个函数所消耗的 CPU 时钟。你还可以用 systemtap 来自己写代码进行动态跟踪。

C/C++ 类应用查找对象内存消耗

其实 googler perftools 也可以分析内存，但是我觉得它没有 Valgrind 好使。所以在这一部分，我用 valgrind 来告诉你如何查找到 C/C++ 的内存消耗。

valgrind 能实现这些功能：

这里举一个内存泄露的小例子。这是一段再无聊不过的代码：

 #include <stdlib.h>

  void f(void)
  {
     int* x = malloc(10 * sizeof(int));
     x[10] = 0;        // problem 1: heap block overrun
  }                    // problem 2: memory leak -- x not freed

  int main(void)
  {
     f();
     return 0;

我们不断分配，而不释放。

编译运行之后，我们可以看到如下结果。

[root@7dgroup Sample10]# gcc -Wall -o test5 test5.c
[root@7dgroup Sample10]# valgrind --tool=memcheck --leak-check=full ./test5
==318== Memcheck, a memory error detector
==318== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==318== Using Valgrind-3.10.0 and LibVEX; rerun with -h for copyright info
==318== Command: ./test5
==318==
==318== Invalid write of size 4
==318==    at 0x40054E: f (in /root/GDB/Sample10/test5)
==318==    by 0x40055E: main (in /root/GDB/Sample10/test5)
==318==  Address 0x51f7068 is 0 bytes after a block of size 40 alloc'd
==318==    at 0x4C29BFD: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==318==    by 0x400541: f (in /root/GDB/Sample10/test5)
==318==    by 0x40055E: main (in /root/GDB/Sample10/test5)
==318==
==318==
==318== HEAP SUMMARY:
==318==     in use at exit: 40 bytes in 1 blocks
==318==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==318==
==318== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==318==    at 0x4C29BFD: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==318==    by 0x400541: f (in /root/GDB/Sample10/test5)
==318==    by 0x40055E: main (in /root/GDB/Sample10/test5)
==318==
==318== LEAK SUMMARY:
==318==    definitely lost: 40 bytes in 1 blocks
==318==    indirectly lost: 0 bytes in 0 blocks
==318==      possibly lost: 0 bytes in 0 blocks
==318==    still reachable: 0 bytes in 0 blocks
==318==         suppressed: 0 bytes in 0 blocks
==318==
==318== For counts of detected and suppressed errors, rerun with: -v
==318== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 1 from 1)
[root@7dgroup Sample10]#

主要看一下这行。

==318==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated

这里分配了 40 个字节的一块内存，但是 0 释放，所以就泄露了。

请你注意，在调试自己的程序时，要像 Java 一样，分析内存的泄露，在压力前和压力后做内存的比对。在压力中则不用做。

总结

不管是什么语言的应用，在性能分析的过程中，都是分析两个方法。

执行速度够不够快。只有够快才能满足更高的 TPS。

执行过程中内存用得多不多。内存用得少，才可以同时支持更多的请求。

我觉得对性能测试过程中的分析来说，这两点足够你解决代码上的问题了。有人说，为什么不说 I/O 的事情呢。其实 I/O 仍然是读写量的多少，也会反应用内存中。至于磁盘本身性能跟不上，那是另一个话题。

思考题

最后给你留两个思考题吧。对代码的性能分析过程中，主要是哪两点呢？针对代码分析的这两点，有什么样的分析链路？

欢迎你在评论区写下自己的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

21丨Tomcat：中间件监控及常用计数器解析

在当今 Spring Cloud 微服务架构盛行的时代，Tomcat 仍然作为应用最广的应用服务器而存在着，所以我们不得不说一说对它的性能分析。

很多时候，我们做性能测试分析时，都会把 Tomcat 这类的应用弄混淆。对它的监控和分析，总是会和 JDK、框架代码、业务代码混合来看，这就导致了分析上的混乱。我们应该把这些分析内容分隔开来，哪些是 tomcat，哪些是 JDK 等。

在我看来，Tomcat、WebLogic、WebSphere、JBoss 等，它们都具有同样的分析思路。因为 Tomcat 的市场范围更大，所以，今天，我们以它为例来说明这类应用应该如何分析。

首先我们得知道它的架构是什么样的。

这是一个在网上随处可见的架构图，它能告诉我们 Tomcat 内部如何运作。如果你有兴趣，还可以看一下官方对它的架构描述。

然而，我们做性能分析的人真的要完全掌握这些细节吗？当然不是。从经验上来说，基本上有几大方面，是 Tomcat 优化时需要关注的。

如下图所示：

最上面，我放了两个框，分别是操作系统和 JDK。因为要调优 Tomcat，和这两者非常相关，但是操作系统和 JDK 又各自有独立的分析逻辑，而在本篇中，我专门讲 Tomcat 类型的组件，所以上面两块的内容我将尽量不涉及，以免混乱。

在 Tomcat 的性能分析中，我将我认为重要的几个技术点列在了思维导图中，同时也对它们做了重要程度的标识。在我分析经验中，这些内容已经包括了大部分的优化场景。

运行模式之争

有很多人对运行模式非常敏感，大家也看到经常有文章说：“对于性能来说，显然是 BIO<NIO<APR 的。”然而也有人做过测试说，其实不见得 BIO 性能就最差，这取决于应用场景；也有人说在压力低的情况下，显然 BIO 的性能更高。

从我的经验上来说，真的没必要纠结这一点。本着对 Tomcat 官方的信任，我觉得最好就是用官方给的默认运行模式，它肯定是经过了更严格的测试才被选择的。这就跟相信世上好人多是一样的道理。

现在新的 Tomcat 版本中默认的是 NIO 了，你可以在启动日志中看到相应的信息，如下所示：

21-Jan-2020 16:50:57.989 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["https-jsse-nio-443"]

简单地说 BIO 和 NIO 的区别如下面两张图所示。

BIO 图示：

NIO 图示：

要理解这个区别，首先得知道几个知识点：

Acceptor 是 TCP 层面的东西，它负责完成 TCP 握手的过程，放入全连接队列，然后将数据生成 request，调用 servlet 容器处理。

而 Worker 干的就是接到 request 数据，处理后给出 response。

Poller 是一个队列，属于典型的生产者 - 消费者模式。

知道了这些，你就会明白其实对于 Acceptor 和 Worker 本身来说，仍然是阻塞的。而这个 Poller 只能是在大并发的时候，可以 hold 住更多的请求而已，看起来 Tomcat 处理请求的容量增加了，但是我们还是要在具体的应用中去测试，来比对响应时间的差异。

但是还有一点区别我们得知道，Tomcat 的 keepAliveTimeout 参数默认使用的是 connectionTimeout 的值。这样一来，由于使用 BIO 时，Acceptor 读取 socket 中的数据并传递给 Worker 这个过程是阻塞的，意味着当代码执行到 Worker 中时，这个 socket 仍然被占着；而使用 AIO 时，Acceptor 读取 socket 数据后交给了 Poller 了，Worker 从 Poller 中得到请求内容并处理，这个过程就分开了，这样 Worker 在处理时就不会阻塞 socket，所以 Tomcat 可以处理更多的 socket，这才是 AIO 性能提升的关键点。

而 APR 是个啥呢？它是利用了 OS 中的能力来进行高并发地文件读取或者网络传输，来提高对静态文件的处理。有很多网上的实验结果都可以证明，在具体的应用中它并没有比 NIO 的性能高到哪里去，并且配置起来还麻烦，所以这个模式现在并没有很广泛的使用。

所以在 Tomcat 中，运行模式之争应该说已经不存在了。

请求量、请求时间、响应时间

这是我希望你能在分析 Tomcat 时关注的内容。我们有很多种方式可以看这些信息，最简单的就是访问日志了。通过在 conf/server.xml 中做如下配置：

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
             prefix="localhost_access_log" suffix=".txt"
             pattern="%h %l %u %t &quot;%r&quot; %s %b %D %F" />

其中 %D 就是请求时间，%F 是响应时间。配置了之后，在日志中就会看到如下内容：

172.17.211.144 - - [21/Jan/2020:18:06:57 +0800] "POST /back/save HTTP/1.1" 200 14 29 29
172.17.211.144 - - [21/Jan/2020:18:06:57 +0800] "GET /validate/code/pic HTTP/1.1" 200 541 5 0

最后两列就是请求时间和响应时间。通过这两个时间的比对，你就可以知道，Tomcat 本身消耗了多少时间以及 Tomcat 之后的操作又消耗了多少时间。

当然，如果你喜欢的话，也可以看这样的监控图表。

这是一个小工具 Probe 的监控数据，从这里，你可以知道 Tomcat 这段时间处理了多少请求，以及处理这些请求的时间、最大时间、最小时间和平均时间。

但是！我不建议用这个工具来监控 Tomcat，因为它性能差。你可能会问，那你还说它干吗？因为其他的性能监控工具中，很少有见到这个角度的图表展示，在这里只是为了告诉你，分析 Tomcat 全局性能状态时，可以通过总请求数，以及平均响应时间来看 Tomcat 的全局处理能力如何。

当然这些数据你同样可以通过分析访问日志获取。

显然有了这些数据，我们就可以做一个大体的判断了。在服务节点多的时候，只要看这里的平均响应时间，你就能知道在这个 Tomcat 上有没有消耗掉你在压力工具中看到的响应时间。

下面我通过测试结果来说明几个 Tomcat 中常用的优化动作。

在展示优化动作之前，先看一下 connector 基本配置：

<Connector
    SSLEnabled="true"
    acceptCount="100"
    clientAuth="false"
    disableUploadTimeout="true"
    enableLookups="false"
    maxThreads="25"
    port="443"
    connectionTimeout="20000"
    keystoreFile="/PathToTomcat/bin/server.keystore"
    keystorePass="12345678"
    protocol="org.apache.coyote.http11.Http11NioProtocol"
    scheme="https"
    secure="true"
    sslProtocol="TLS" />

这只是一个基本配置。至于是不是最优的配置，我们需要在针对一个应用测试的过程中慢慢来看。

比如有人会说，你这里为什么不配置 minSpareThreads 和 maxSpareThreads 之类的参数？首先，我们要知道，为什么要配置这两个参数？对于一个线程数超高的应用来说，长期维护大量的线程肯定会导致操作系统中 context switch 的增加，在一个应用的波峰波谷差别较大的时候，我们用这两个参数其实是为了减少在波谷时产生的维护成本。但是同时你也要知道，在线程不够用的时候，开新的线程也同样需要成本，所以这两个值需不需要配置，完全取决于应用场景的具体测试结果。

总之，所有的配置都需要在具体的应用场景测试了之后，再下定论，别凭感觉。

协议 HTTP、HTTPS

我们知道在 HTTPS 的协议中，因为加入了 SSL 证书会导致性能下降，但是对有些应用来说，又不得不用 SSL 证书。在这里我自己配置了一个证书，来给你看看证书对性能产生的影响。

我的证书配置是这样的：

根证书：RSA 证书、sha256，8192 位

中级证书：RSA，sha256、4096 位

终端证书：RSA，sha256、4096 位

一般情况下，SSL 证书都是分为三层的。在这个例子中，我生成的时候还特意用了高位数，位数越高，对性能影响越大，因为计算成本增加了。现在我们在市场上买的证书，根据价值的不同，加密方式和位数都会不一样，请稍微注意一下。

下面来看看测试结果。

Tomcat with SSL：

summary +    588 in 00:00:13 =   46.0/s Avg:    10 Min:     1 Max:   804 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   4403 in 00:00:30 =  146.8/s Avg:     4 Min:     0 Max:    87 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   4991 in 00:00:43 =  116.7/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)
summary +   7107 in 00:00:30 =  237.1/s Avg:     4 Min:     0 Max:    77 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =  12098 in 00:01:13 =  166.3/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  11121 in 00:00:30 =  370.7/s Avg:     4 Min:     0 Max:    72 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =  23219 in 00:01:43 =  226.0/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  12709 in 00:00:30 =  423.6/s Avg:     4 Min:     0 Max:    87 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  35928 in 00:02:13 =  270.6/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  14548 in 00:00:30 =  485.0/s Avg:     4 Min:     0 Max:    69 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  50476 in 00:02:43 =  310.1/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  15810 in 00:00:30 =  527.0/s Avg:     5 Min:     0 Max:    83 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  66286 in 00:03:13 =  343.9/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  15242 in 00:00:30 =  508.0/s Avg:     5 Min:     0 Max:    77 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  81528 in 00:03:43 =  366.0/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  16709 in 00:00:30 =  557.1/s Avg:     5 Min:     0 Max:    75 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary =  98237 in 00:04:13 =  388.7/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  17099 in 00:00:30 =  570.0/s Avg:     6 Min:     0 Max:   161 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary = 115336 in 00:04:43 =  407.9/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)

Tomcat without SSL：

summary +     12 in 00:00:03 =    4.2/s Avg:   148 Min:     4 Max:   937 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   3531 in 00:00:30 =  117.8/s Avg:     4 Min:     0 Max:    63 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   3543 in 00:00:33 =  107.8/s Avg:     4 Min:     0 Max:   937 Err:     0 (0.00%)
summary +   7283 in 00:00:30 =  242.8/s Avg:     4 Min:     0 Max:    90 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =  10826 in 00:01:03 =  172.2/s Avg:     4 Min:     0 Max:   937 Err:     0 (0.00%)
summary +   9554 in 00:00:30 =  318.5/s Avg:     3 Min:     0 Max:    35 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =  20380 in 00:01:33 =  219.5/s Avg:     4 Min:     0 Max:   937 Err:     0 (0.00%)
summary +  14747 in 00:00:30 =  491.6/s Avg:     3 Min:     0 Max:    49 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  35127 in 00:02:03 =  285.9/s Avg:     3 Min:     0 Max:   937 Err:     0 (0.00%)
summary +  16844 in 00:00:30 =  561.4/s Avg:     3 Min:     0 Max:    47 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  51971 in 00:02:33 =  340.0/s Avg:     3 Min:     0 Max:   937 Err:     0 (0.00%)
summary +  17547 in 00:00:30 =  585.0/s Avg:     3 Min:     0 Max:    47 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  69518 in 00:03:03 =  380.2/s Avg:     3 Min:     0 Max:   937 Err:     0 (0.00%)
summary +  18798 in 00:00:30 =  626.6/s Avg:     4 Min:     0 Max:   213 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  88316 in 00:03:33 =  414.9/s Avg:     3 Min:     0 Max:   937 Err:     0 (0.00%)
summary +  18529 in 00:00:30 =  617.6/s Avg:     4 Min:     0 Max:   204 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary = 106845 in 00:04:03 =  439.9/s Avg:     3 Min:     0 Max:   937 Err:     0 (0.00%)
summary +  18837 in 00:00:30 =  627.9/s Avg:     4 Min:     0 Max:    53 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary = 125682 in 00:04:33 =  460.6/s Avg:     4 Min:     0 Max:   937 Err:     0 (0.00%)

这里稍微啰嗦一下，我们以这种终端直接输出的数据看 JMeter 的结果时，主要关注下"summary +"的数据，因为"summary ="的数据是整个场景执行的平均数据。另外，第一行的数据会不准确，可以忽略，如果你把粒度调低一些，可以看到更细的数据。

通过上面的数据可以看到，没有 SSL 比有 SSL 证书是要高出一些 TPS 的。如下图所示：

显然 SSL 证书对性能有明显的影响了，最大的影响到 18.93%，是在 8 个线程时，而在五六个线程时，TPS 损耗有 13% 左右。

这和加密位数、应用场景等都有关系，所以这一段可以给你的结论就是：SSL 证书对性能会有损耗。但具体损耗是多少，在你的应用场景中需要具体测试。

线程池

Tomcat 的线程池，一直是调优 Tomcat 的重点对象。在我的工作经验中，我发现经常有人不太清楚对 Tomcat 应该配置多大的线程池。

之前，我见过有一个人在一个 4C8G 的机器上把一个 Tomcat 节点的线程池配置到了 4000。我问他为什么要这么配置，他说想支持 4000 的并发用户。

我们先不说他有没有理解在线用户、并发用户和 TPS 之间的逻辑关系，只说把 Tomcat 配置为 4000 这个事情。就算 Tomcat 能支撑得住 4000，但机器能撑得住吗？结果还没跑多少压力线程，操作系统的 CS 就不断走高，消耗了大量的 sy CPU，只有少量的 us CPU 能处理正常的业务。

然后我告诉他把 Tomcat 线程数调到默认的 200 先看看，结果 TPS 上升了好几倍。

这就是对线程在系统中运行的逻辑不理解导致的情况。

我们在测试的时候，先得学会判断：线程数到底够不够用。要是不够用，但又有足够的硬件资源，那你可以增加线程。

但是在增加线程之前，先要判断，代码是不是运行得足够快，如果代码本身就慢，那就先优化代码，再调整线程。

这里就有一个小的分析链路了。如下所示：

怎么来判断代码运行得足够快呢？

下面我们来看几个例子，然后我会说一下如何判断代码快不快（当然这个具体的应用也有关，你还需要在具体的应用中做详细地分析哦）。

场景一：当压力线程远远小于服务端线程数时

测试结果：

summary +    930 in 00:00:16 =   59.7/s Avg:     7 Min:     0 Max:   922 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   4546 in 00:00:30 =  151.6/s Avg:     3 Min:     0 Max:    46 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary =   5476 in 00:00:46 =  120.2/s Avg:     3 Min:     0 Max:   922 Err:     0 (0.00%)
summary +   5822 in 00:00:30 =  194.0/s Avg:     2 Min:     0 Max:    32 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary =  11298 in 00:01:16 =  149.5/s Avg:     3 Min:     0 Max:   922 Err:     0 (0.00%)
summary +   5295 in 00:00:24 =  216.5/s Avg:     2 Min:     0 Max:    26 Err:     0 (0.00%) Active: 0 Started: 1 Finished: 1
summary =  16593 in 00:01:40 =  165.9/s Avg:     2 Min:     0 Max:   922 Err:     0 (0.00%)

线程监控结果：

jvisuavlvm 的 thread 监控图是每秒刷新一次，其中橙色代表 TIMED_WAITING 状态，没活干；绿色代表 RUNNABLE 状态，在干活。

通过这个测试结果，你可以看到，在只有一个压力线程的情况下，这 10 个 Worker 是轮流提供响应的。

这就是典型的线程足够用的状态。

场景二：当压力线程数通过递增，慢慢超过服务端线程数时

测试结果：

summary +   4529 in 00:00:22 =  203.6/s Avg:     2 Min:     0 Max:   464 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary +  11023 in 00:00:30 =  367.7/s Avg:     2 Min:     0 Max:    71 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =  15552 in 00:00:52 =  297.8/s Avg:     2 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  15131 in 00:00:30 =  504.4/s Avg:     2 Min:     0 Max:   166 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  30683 in 00:01:22 =  373.2/s Avg:     2 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  17420 in 00:00:30 =  580.7/s Avg:     3 Min:     0 Max:    68 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  48103 in 00:01:52 =  428.6/s Avg:     3 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  17416 in 00:00:30 =  580.5/s Avg:     3 Min:     0 Max:    72 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  65519 in 00:02:22 =  460.7/s Avg:     3 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  17389 in 00:00:30 =  579.6/s Avg:     4 Min:     0 Max:    71 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary =  82908 in 00:02:52 =  481.4/s Avg:     3 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18591 in 00:00:30 =  619.8/s Avg:     4 Min:     0 Max:    82 Err:     0 (0.00%) Active: 11 Started: 11 Finished: 0
summary = 101499 in 00:03:22 =  501.9/s Avg:     3 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18692 in 00:00:30 =  623.1/s Avg:     5 Min:     0 Max:    72 Err:     0 (0.00%) Active: 12 Started: 12 Finished: 0
summary = 120191 in 00:03:52 =  517.6/s Avg:     4 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18577 in 00:00:30 =  619.2/s Avg:     6 Min:     0 Max:    83 Err:     0 (0.00%) Active: 14 Started: 14 Finished: 0
summary = 138768 in 00:04:22 =  529.2/s Avg:     4 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  19371 in 00:00:30 =  645.7/s Avg:     6 Min:     0 Max:   113 Err:     0 (0.00%) Active: 15 Started: 15 Finished: 0
summary = 158139 in 00:04:52 =  541.2/s Avg:     4 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18891 in 00:00:30 =  629.7/s Avg:     7 Min:     0 Max:   146 Err:     0 (0.00%) Active: 17 Started: 17 Finished: 0
summary = 177030 in 00:05:22 =  549.4/s Avg:     4 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  19075 in 00:00:30 =  635.6/s Avg:     7 Min:     0 Max:    99 Err:     0 (0.00%) Active: 18 Started: 18 Finished: 0
summary = 196105 in 00:05:52 =  556.7/s Avg:     5 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18782 in 00:00:30 =  625.4/s Avg:     8 Min:     0 Max:   122 Err:     0 (0.00%) Active: 20 Started: 20 Finished: 0
summary = 214887 in 00:06:22 =  562.1/s Avg:     5 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18911 in 00:00:30 =  631.3/s Avg:     8 Min:     0 Max:   146 Err:     0 (0.00%) Active: 21 Started: 21 Finished: 0
summary = 233798 in 00:06:52 =  567.2/s Avg:     5 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18153 in 00:00:30 =  605.1/s Avg:     9 Min:     0 Max:   147 Err:     0 (0.00%) Active: 23 Started: 23 Finished: 0
summary = 251951 in 00:07:22 =  569.7/s Avg:     5 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  14704 in 00:00:30 =  490.1/s Avg:    10 Min:     0 Max:   175 Err:     0 (0.00%) Active: 24 Started: 24 Finished: 0
summary = 266655 in 00:07:52 =  564.7/s Avg:     6 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  18196 in 00:00:30 =  606.6/s Avg:    10 Min:     0 Max:   147 Err:     0 (0.00%) Active: 26 Started: 26 Finished: 0
summary = 284851 in 00:08:22 =  567.2/s Avg:     6 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  17768 in 00:00:30 =  592.3/s Avg:    10 Min:     0 Max:   227 Err:     0 (0.00%) Active: 27 Started: 27 Finished: 0
summary = 302619 in 00:08:52 =  568.6/s Avg:     6 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  16754 in 00:00:30 =  558.2/s Avg:    12 Min:     0 Max:   218 Err:     0 (0.00%) Active: 29 Started: 29 Finished: 0
summary = 319373 in 00:09:22 =  568.0/s Avg:     7 Min:     0 Max:   464 Err:     0 (0.00%)
summary +  17216 in 00:00:30 =  574.0/s Avg:    12 Min:     0 Max:   249 Err:     0 (0.00%) Active: 30 Started: 30 Finished: 0
summary = 336589 in 00:09:52 =  568.3/s Avg:     7 Min:     0 Max:   464 Err:     0 (0.00%)

线程监控结果：

在这个场景中，我特意把压力工具中的线程数设置得高于 Tomcat 线程数，并且通过递增的方式加压。

一开始线程数是足够用的，还有挺多的时间处于空闲状态。但随着压力的增加，Tomcat 的线程越来越忙，直到不够用，于是 Tomcat 就自己调整了线程数，直到 maxThreads 的值。然后线程的空闲状态就越来越少，到最后几乎没有空闲状态了。

你也可以看到响应时间随着线程数的不够用而不断的增加。

这就是典型的线程配置不够的状态。

场景三：当压力线程数远高于服务端线程数时

测试结果：

summary +      1 in 00:00:02 =    0.5/s Avg:  1724 Min:  1724 Max:  1724 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary +   5821 in 00:00:28 =  204.3/s Avg:   128 Min:     1 Max:  1798 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =   5822 in 00:00:31 =  190.8/s Avg:   129 Min:     1 Max:  1798 Err:     0 (0.00%)
summary +  10881 in 00:00:30 =  362.7/s Avg:    57 Min:     1 Max:   405 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  16703 in 00:01:01 =  276.0/s Avg:    82 Min:     1 Max:  1798 Err:     0 (0.00%)
summary +  11548 in 00:00:30 =  384.9/s Avg:    52 Min:     1 Max:   308 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  28251 in 00:01:31 =  312.1/s Avg:    70 Min:     1 Max:  1798 Err:     0 (0.00%)
summary +   3747 in 00:00:10 =  387.8/s Avg:    51 Min:     1 Max:   328 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =  31998 in 00:01:40 =  319.4/s Avg:    67 Min:     1 Max:  1798 Err:     0 (0.00%)

线程监控结果：

在这个压力场景中，我直接把压力线程加上来，并且高于 Tomcat 的处理线程，Tomcat 一下就上到了 maxThreads 的上限，然后就几乎没再闲过。同时你也可以看到响应时间远远大于场景二中的响应时间。

这就是典型的压力过高的状态。

对比前面三个场景，我们再比对之前专栏中的文章内容，就可以理解几个关键的知识点了。

首先，压力工具中的线程数到底应不应该在没有报错的情况下无休止地增加？

显然即使你增加，对服务端能处理的请求来说，并没有什么意义，只会导致响应时间的变长。

其次，把压力线程理解为并发用户数到底对不对？

如果你把压力线程理解为并发用户，通过这几个示例就可以看到，服务端能处理的压力线程必然不会超过自身的线程上限，也就是说，把压力线程理解为并发用户，并发用户的上限是固定的。你都不用测试，直接看服务端的线程数配置为多大就够了。这也是为什么我一再强调并发用户数不能用压力线程来描述，不能用压力线程来承载服务端性能指标的关键点。

而用 TPS 来描述的时候，因为有了“秒”的概念，就有了时间段，而在这个时间段内得到的响应都认为是被支持了的，所以用 TPS 来描述会更为合理。

而 T 的定义具有业务含义时，也就对应起了技术和业务之间的关系，这个逻辑也就完整了。

从这里你也可以看到，我在整个专栏中所描述的概念在具体的落地时，都会秉承它的连贯性，要不然一个飞在空中的概念就没有意义了。

场景四：设置最大最小空闲线程数

测试结果：

summary +      1 in 00:00:01 =    0.9/s Avg:   730 Min:   730 Max:   730 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   2915 in 00:00:30 =   97.5/s Avg:     5 Min:     0 Max:  1126 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   2916 in 00:00:31 =   94.1/s Avg:     5 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +   6690 in 00:00:30 =  222.9/s Avg:     4 Min:     0 Max:    81 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =   9606 in 00:01:01 =  157.5/s Avg:     4 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +   9968 in 00:00:30 =  332.4/s Avg:     4 Min:     0 Max:    88 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  19574 in 00:01:31 =  215.2/s Avg:     4 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  14140 in 00:00:30 =  471.1/s Avg:     3 Min:     0 Max:    87 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  33714 in 00:02:01 =  278.6/s Avg:     4 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  14985 in 00:00:30 =  499.8/s Avg:     4 Min:     0 Max:    84 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  48699 in 00:02:31 =  322.6/s Avg:     4 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  16446 in 00:00:30 =  548.1/s Avg:     4 Min:     0 Max:    76 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  65145 in 00:03:01 =  360.0/s Avg:     4 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  16171 in 00:00:30 =  539.2/s Avg:     5 Min:     0 Max:   410 Err:     0 (0.00%) Active: 11 Started: 11 Finished: 0
summary =  81316 in 00:03:31 =  385.4/s Avg:     4 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  16419 in 00:00:30 =  547.2/s Avg:     7 Min:     0 Max:   646 Err:     0 (0.00%) Active: 13 Started: 13 Finished: 0
summary =  97735 in 00:04:01 =  405.6/s Avg:     5 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  15981 in 00:00:30 =  532.7/s Avg:     9 Min:     0 Max:   832 Err:     0 (0.00%) Active: 14 Started: 14 Finished: 0
summary = 113716 in 00:04:31 =  419.7/s Avg:     5 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  16064 in 00:00:30 =  535.6/s Avg:    11 Min:     0 Max:   888 Err:     0 (0.00%) Active: 16 Started: 16 Finished: 0
summary = 129780 in 00:05:01 =  431.2/s Avg:     6 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  15446 in 00:00:30 =  514.8/s Avg:    10 Min:     0 Max:  1022 Err:     0 (0.00%) Active: 17 Started: 17 Finished: 0
summary = 145226 in 00:05:31 =  438.8/s Avg:     6 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  14643 in 00:00:30 =  488.1/s Avg:    12 Min:     0 Max:  1114 Err:     0 (0.00%) Active: 19 Started: 19 Finished: 0
summary = 159869 in 00:06:01 =  442.9/s Avg:     7 Min:     0 Max:  1126 Err:     0 (0.00%)
summary +  14805 in 00:00:30 =  493.5/s Avg:    13 Min:     0 Max:  1250 Err:     0 (0.00%) Active: 20 Started: 20 Finished: 0
summary = 174674 in 00:06:31 =  446.8/s Avg:     7 Min:     0 Max:  1250 Err:     0 (0.00%)
summary +  14446 in 00:00:30 =  481.5/s Avg:    15 Min:     0 Max:  1385 Err:     0 (0.00%) Active: 22 Started: 22 Finished: 0
summary = 189120 in 00:07:01 =  449.2/s Avg:     8 Min:     0 Max:  1385 Err:     0 (0.00%)
summary +  14310 in 00:00:30 =  477.1/s Avg:    17 Min:     0 Max:  1454 Err:     0 (0.00%) Active: 23 Started: 23 Finished: 0
summary = 203430 in 00:07:31 =  451.1/s Avg:     9 Min:     0 Max:  1454 Err:     0 (0.00%)
summary +  13856 in 00:00:30 =  461.8/s Avg:    18 Min:     0 Max:  1454 Err:     0 (0.00%) Active: 25 Started: 25 Finished: 0
summary = 217286 in 00:08:01 =  451.8/s Avg:     9 Min:     0 Max:  1454 Err:     0 (0.00%)
summary +  13643 in 00:00:30 =  454.8/s Avg:    18 Min:     0 Max:  1591 Err:     0 (0.00%) Active: 26 Started: 26 Finished: 0
summary = 230929 in 00:08:31 =  451.9/s Avg:    10 Min:     0 Max:  1591 Err:     0 (0.00%)
summary +  13605 in 00:00:30 =  453.5/s Avg:    21 Min:     0 Max:  1593 Err:     0 (0.00%) Active: 28 Started: 28 Finished: 0
summary = 244534 in 00:09:01 =  452.0/s Avg:    10 Min:     0 Max:  1593 Err:     0 (0.00%)
summary +  13316 in 00:00:30 =  443.7/s Avg:    23 Min:     0 Max:  1598 Err:     0 (0.00%) Active: 29 Started: 29 Finished: 0
summary = 257850 in 00:09:31 =  451.6/s Avg:    11 Min:     0 Max:  1598 Err:     0 (0.00%)
summary +  12538 in 00:00:30 =  418.1/s Avg:    24 Min:     0 Max:  1599 Err:     0 (0.00%) Active: 30 Started: 30 Finished: 0
summary = 270388 in 00:10:01 =  449.9/s Avg:    12 Min:     0 Max:  1599 Err:     0 (0.00%)

线程监控结果：

这个场景是为了描述 minSpareThreads 和 maxSpareThreads 的能力，我们可以看到场景结束了之后，线程确实被回收了。

记住，这个回收的价值在于，当再次有少量请求进来时不会导致过多的维护线程的成本，从而导致 TPS 的下降。如果你的应用中，本身线程数就不是非常大，即使长时间维护着固定的线程池也不会有大的成本，那么不配置这两个参数也是可以的。

禁用 AJP

什么是 AJP 呢？你可以点击这里看一下。

AJP 是个二进制的 TCP 传输协议，相比 HTTP 来说有更高的性能和效率，只是支持 AJP 的代理服务器不多。在我们常用的应用场景中，用 Nginx 来连接 Tomcat 较多，AJP 协议是用不上的，因为 Nginx 官方根本就没有支持 AJP 协议的模块。当然也有人提供过 AJP 的 Nginx 代理模块，只是实际应用的也不多。

下面我们看禁用 AJP 和启用 AJP 产生的效果。

首先看下启用 AJP 的测试结果。

启动 AJP（在 Tomcat 的 conf/server.xml 中配置）：

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

启动日志：

21-Jan-2020 01:14:58.404 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [ajp-nio-8009]

测试结果为下面这样：

summary +      4 in 00:00:02 =    1.8/s Avg:   482 Min:   188 Max:   903 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   2704 in 00:00:29 =   92.8/s Avg:     5 Min:     0 Max:    83 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   2708 in 00:00:31 =   86.4/s Avg:     5 Min:     0 Max:   903 Err:     0 (0.00%)
summary +   6154 in 00:00:30 =  205.1/s Avg:     4 Min:     0 Max:    89 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =   8862 in 00:01:01 =  144.5/s Avg:     5 Min:     0 Max:   903 Err:     0 (0.00%)
summary +   8818 in 00:00:30 =  293.9/s Avg:     4 Min:     0 Max:    71 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =  17680 in 00:01:31 =  193.6/s Avg:     4 Min:     0 Max:   903 Err:     0 (0.00%)
summary +  13267 in 00:00:30 =  442.3/s Avg:     4 Min:     0 Max:    66 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  30947 in 00:02:01 =  255.0/s Avg:     4 Min:     0 Max:   903 Err:     0 (0.00%)
summary +  13004 in 00:00:30 =  433.4/s Avg:     4 Min:     0 Max:    59 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  43951 in 00:02:31 =  290.4/s Avg:     4 Min:     0 Max:   903 Err:     0 (0.00%)
summary +  15800 in 00:00:30 =  526.6/s Avg:     4 Min:     0 Max:    88 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  59751 in 00:03:01 =  329.5/s Avg:     4 Min:     0 Max:   903 Err:     0 (0.00%)
summary +  16766 in 00:00:30 =  559.0/s Avg:     5 Min:     0 Max:    90 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  76517 in 00:03:31 =  362.1/s Avg:     4 Min:     0 Max:   903 Err:     0 (0.00%)
summary +  16760 in 00:00:30 =  558.6/s Avg:     5 Min:     0 Max:    67 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary =  93277 in 00:04:01 =  386.5/s Avg:     4 Min:     0 Max:   903 Err:     0 (0.00%)
summary +  16991 in 00:00:30 =  566.4/s Avg:     6 Min:     0 Max:   148 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary = 110268 in 00:04:31 =  406.4/s Avg:     5 Min:     0 Max:   903 Err:     0 (0.00%)

然后我们再看下禁用 AJP 的测试结果。

禁用 AJP：

<!-- Connector port="8009" protocol="AJP/1.3" redirectPort="8443" -->

无启动日志。

测试结果：

summary +     90 in 00:00:05 =   16.9/s Avg:    33 Min:     2 Max:   812 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   3443 in 00:00:30 =  115.0/s Avg:     4 Min:     0 Max:    93 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   3533 in 00:00:35 =  100.2/s Avg:     5 Min:     0 Max:   812 Err:     0 (0.00%)
summary +   7245 in 00:00:30 =  241.5/s Avg:     4 Min:     0 Max:    75 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =  10778 in 00:01:05 =  165.2/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  11029 in 00:00:30 =  367.6/s Avg:     4 Min:     0 Max:   335 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =  21807 in 00:01:35 =  228.9/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  12227 in 00:00:30 =  407.5/s Avg:     4 Min:     0 Max:    67 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  34034 in 00:02:05 =  271.7/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  14735 in 00:00:30 =  491.4/s Avg:     4 Min:     0 Max:    72 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  48769 in 00:02:35 =  314.1/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  16574 in 00:00:30 =  552.5/s Avg:     4 Min:     0 Max:    65 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  65343 in 00:03:05 =  352.7/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  17488 in 00:00:30 =  582.9/s Avg:     4 Min:     0 Max:    70 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  82831 in 00:03:35 =  384.8/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  16933 in 00:00:30 =  564.5/s Avg:     5 Min:     0 Max:    87 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary =  99764 in 00:04:05 =  406.8/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)
summary +  17363 in 00:00:30 =  578.8/s Avg:     6 Min:     0 Max:    76 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary = 117127 in 00:04:35 =  425.5/s Avg:     4 Min:     0 Max:   812 Err:     0 (0.00%)

通过比对，如下图所示：

禁用 AJP 确实性能会高一点，在这个场景中最高的时候，同压力线程下，性能高出近 20%。

压缩

在很多的压力测试中，我们说压缩这个功能都是基于两个目标：

减少带宽的消耗；

减少传输的时间；

但是这必然会导致服务端 CPU 消耗的增加，这是个必然的过程。所以它的配置前提就是 CPU 足够用，带宽不够用。如果你带宽足够用，CPU 不够用时，显然这样做是不理智的。

下面我们来看一下 Tomcat 中压缩和不压缩产生的结果。

首先是不压缩。

summary +    588 in 00:00:13 =   46.0/s Avg:    10 Min:     1 Max:   804 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   4403 in 00:00:30 =  146.8/s Avg:     4 Min:     0 Max:    87 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   4991 in 00:00:43 =  116.7/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)
summary +   7107 in 00:00:30 =  237.1/s Avg:     4 Min:     0 Max:    77 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =  12098 in 00:01:13 =  166.3/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  11121 in 00:00:30 =  370.7/s Avg:     4 Min:     0 Max:    72 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =  23219 in 00:01:43 =  226.0/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  12709 in 00:00:30 =  423.6/s Avg:     4 Min:     0 Max:    87 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  35928 in 00:02:13 =  270.6/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  14548 in 00:00:30 =  485.0/s Avg:     4 Min:     0 Max:    69 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  50476 in 00:02:43 =  310.1/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  15810 in 00:00:30 =  527.0/s Avg:     5 Min:     0 Max:    83 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  66286 in 00:03:13 =  343.9/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  15242 in 00:00:30 =  508.0/s Avg:     5 Min:     0 Max:    77 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  81528 in 00:03:43 =  366.0/s Avg:     4 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  16709 in 00:00:30 =  557.1/s Avg:     5 Min:     0 Max:    75 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary =  98237 in 00:04:13 =  388.7/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)
summary +  17099 in 00:00:30 =  570.0/s Avg:     6 Min:     0 Max:   161 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary = 115336 in 00:04:43 =  407.9/s Avg:     5 Min:     0 Max:   804 Err:     0 (0.00%)

网络流量：

然后是压缩。

在这里，我为了让压缩生效的范围更大，把最小值设置为了 10bytes。

compression="on" compressionMinSize="10" noCompressionUserAgents="gozilla,traviata" compressableMimeType="text/html,text/xml,application/javascript,text/javascript,text/css,text/plain,text/json"

summary +   1037 in 00:00:09 =  117.5/s Avg:     4 Min:     0 Max:   418 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +   5832 in 00:00:30 =  194.2/s Avg:     3 Min:     0 Max:    74 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =   6869 in 00:00:39 =  176.8/s Avg:     3 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  10378 in 00:00:30 =  346.3/s Avg:     3 Min:     0 Max:    71 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =  17247 in 00:01:09 =  250.6/s Avg:     3 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  12670 in 00:00:30 =  422.2/s Avg:     3 Min:     0 Max:    64 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =  29917 in 00:01:39 =  302.7/s Avg:     3 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  13917 in 00:00:30 =  464.0/s Avg:     4 Min:     0 Max:    78 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =  43834 in 00:02:09 =  340.3/s Avg:     3 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  14815 in 00:00:30 =  493.9/s Avg:     4 Min:     0 Max:    79 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =  58649 in 00:02:39 =  369.3/s Avg:     3 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  15710 in 00:00:30 =  523.6/s Avg:     5 Min:     0 Max:    89 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  74359 in 00:03:09 =  393.8/s Avg:     4 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  16059 in 00:00:30 =  535.3/s Avg:     5 Min:     0 Max:    70 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  90418 in 00:03:39 =  413.2/s Avg:     4 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  15909 in 00:00:30 =  530.2/s Avg:     6 Min:     0 Max:    96 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary = 106327 in 00:04:09 =  427.3/s Avg:     4 Min:     0 Max:   418 Err:     0 (0.00%)
summary +  16011 in 00:00:30 =  533.8/s Avg:     6 Min:     0 Max:    75 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary = 122338 in 00:04:39 =  438.8/s Avg:     4 Min:     0 Max:   418 Err:     0 (0.00%)

网络流量：

通过上面的测试比对结果可以看到：

确实在 CPU 资源足够用的时候，采用压缩，TPS 要大一些，但随着压力的增加，CPU 资源不够了之后，压缩就没啥用了。

从带宽传输上可以看到，不压缩时最大带宽达到 40M，而压缩时最大带宽只有 14M，可见压缩对带宽的作用还是很显著的。

acceptCount

在上面的思维导图中，线程池最后一个参数就是 acceptCount，这个值就比较容易理解，就是 TCP 的接收队列长度。

这次我直接用一个大压力的场景说明它的值大小的区别。我直接上 50 个线程。为什么要这么做呢，就是为了让队列产生得多一些。

下面我们直接来看结果吧。

acceptCount=“10000”

测试结果：

summary +   2400 in 00:00:11 =  217.6/s Avg:   108 Min:     3 Max:  1176 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary +  10322 in 00:00:30 =  344.1/s Avg:    62 Min:     1 Max:   470 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  12722 in 00:00:41 =  310.1/s Avg:    71 Min:     1 Max:  1176 Err:     0 (0.00%)
summary +  12660 in 00:00:30 =  422.0/s Avg:    49 Min:     0 Max:   331 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  25382 in 00:01:11 =  357.4/s Avg:    60 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  13337 in 00:00:30 =  444.6/s Avg:    46 Min:     1 Max:   410 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  38719 in 00:01:41 =  383.3/s Avg:    55 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  14176 in 00:00:30 =  472.4/s Avg:    43 Min:     0 Max:   302 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  52895 in 00:02:11 =  403.7/s Avg:    52 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  14696 in 00:00:30 =  489.7/s Avg:    42 Min:     1 Max:   261 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  67591 in 00:02:41 =  419.7/s Avg:    50 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  16191 in 00:00:30 =  539.9/s Avg:    38 Min:     0 Max:   320 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  83782 in 00:03:11 =  438.6/s Avg:    47 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +   5299 in 00:00:09 =  580.1/s Avg:    36 Min:     1 Max:   201 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =  89081 in 00:03:20 =  445.0/s Avg:    47 Min:     0 Max:  1176 Err:     0 (0.00%)

acceptCount=“100”

测试结果：

summary +   1115 in 00:00:11 =  100.1/s Avg:   306 Min:    11 Max:  1936 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary +   7923 in 00:00:30 =  264.1/s Avg:    87 Min:     1 Max:   521 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =   9038 in 00:00:41 =  219.7/s Avg:   114 Min:     1 Max:  1936 Err:     0 (0.00%)
summary +  11414 in 00:00:30 =  380.5/s Avg:    56 Min:     1 Max:   381 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  20452 in 00:01:11 =  287.5/s Avg:    81 Min:     1 Max:  1936 Err:     0 (0.00%)
summary +  11949 in 00:00:30 =  398.4/s Avg:    51 Min:     0 Max:   390 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  32401 in 00:01:41 =  320.4/s Avg:    70 Min:     0 Max:  1936 Err:     0 (0.00%)
summary +  13403 in 00:00:30 =  446.7/s Avg:    46 Min:     0 Max:   326 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  45804 in 00:02:11 =  349.3/s Avg:    63 Min:     0 Max:  1936 Err:     0 (0.00%)
summary +  13271 in 00:00:30 =  442.4/s Avg:    45 Min:     0 Max:   295 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  59075 in 00:02:41 =  366.6/s Avg:    59 Min:     0 Max:  1936 Err:     0 (0.00%)
summary +  14307 in 00:00:30 =  476.9/s Avg:    43 Min:     1 Max:   288 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  73382 in 00:03:11 =  383.9/s Avg:    56 Min:     0 Max:  1936 Err:     0 (0.00%)
summary +   4292 in 00:00:09 =  476.3/s Avg:    42 Min:     1 Max:   226 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =  77674 in 00:03:20 =  388.1/s Avg:    55 Min:     0 Max:  1936 Err:     0 (0.00%)

通过上面的结果，可以得到下图：

可见当 acceptCount 大时，对 TPS 还是有明显帮助的。

接下来，我们再看下 connectionTimeout。

connectionTimeout

我需要说明的是，以下场景均基于以下配置：acceptCount=“10000”。

这个值对我们来说，也是非常关键的数据，因为它影响着 KeepAlive 的超时和 connection 的超时。

我们在性能分析的时候，会遇到一种情况是，很多应用都用默认超时，而在压力大的时候，会有少量的报错是因为前端的超时已经到了，而后端还没到，因为毕竟后端是后接收到的请求。这就导致了大压力下少量的错误是因为超时配置导致的。

所以我们通常都会画一个图如下所示：

而在真实的应用场景中，应该配置多长时间的超时一定是经过严格的测试的。

下面我们来看看结果。

当connectionTimeout="20000"时，这个结果直接拿上面的测试结果复用。

测试结果：

summary +   2400 in 00:00:11 =  217.6/s Avg:   108 Min:     3 Max:  1176 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary +  10322 in 00:00:30 =  344.1/s Avg:    62 Min:     1 Max:   470 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  12722 in 00:00:41 =  310.1/s Avg:    71 Min:     1 Max:  1176 Err:     0 (0.00%)
summary +  12660 in 00:00:30 =  422.0/s Avg:    49 Min:     0 Max:   331 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  25382 in 00:01:11 =  357.4/s Avg:    60 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  13337 in 00:00:30 =  444.6/s Avg:    46 Min:     1 Max:   410 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  38719 in 00:01:41 =  383.3/s Avg:    55 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  14176 in 00:00:30 =  472.4/s Avg:    43 Min:     0 Max:   302 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  52895 in 00:02:11 =  403.7/s Avg:    52 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  14696 in 00:00:30 =  489.7/s Avg:    42 Min:     1 Max:   261 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  67591 in 00:02:41 =  419.7/s Avg:    50 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +  16191 in 00:00:30 =  539.9/s Avg:    38 Min:     0 Max:   320 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  83782 in 00:03:11 =  438.6/s Avg:    47 Min:     0 Max:  1176 Err:     0 (0.00%)
summary +   5299 in 00:00:09 =  580.1/s Avg:    36 Min:     1 Max:   201 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =  89081 in 00:03:20 =  445.0/s Avg:    47 Min:     0 Max:  1176 Err:     0 (0.00%)

当 connectionTimeout="200"时，测试结果为：

summary +   1540 in 00:00:13 =  117.5/s Avg:   258 Min:     7 Max:  2150 Err:    15 (0.97%) Active: 50 Started: 50 Finished: 0
summary +  10080 in 00:00:30 =  336.4/s Avg:    65 Min:     1 Max:   465 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  11620 in 00:00:43 =  269.8/s Avg:    90 Min:     1 Max:  2150 Err:    15 (0.13%)
summary +  12691 in 00:00:30 =  422.8/s Avg:    49 Min:     0 Max:   317 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  24311 in 00:01:13 =  332.6/s Avg:    69 Min:     0 Max:  2150 Err:    15 (0.06%)
summary +  12707 in 00:00:30 =  423.8/s Avg:    48 Min:     1 Max:   312 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  37018 in 00:01:43 =  359.2/s Avg:    62 Min:     0 Max:  2150 Err:    15 (0.04%)
summary +  13530 in 00:00:30 =  450.7/s Avg:    45 Min:     0 Max:   306 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  50548 in 00:02:13 =  379.8/s Avg:    57 Min:     0 Max:  2150 Err:    15 (0.03%)
summary +  13791 in 00:00:30 =  460.0/s Avg:    44 Min:     1 Max:   344 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  64339 in 00:02:43 =  394.5/s Avg:    55 Min:     0 Max:  2150 Err:    15 (0.02%)
summary +  14840 in 00:00:30 =  494.6/s Avg:    41 Min:     0 Max:   319 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =  79179 in 00:03:13 =  410.1/s Avg:    52 Min:     0 Max:  2150 Err:    15 (0.02%)
summary +   3734 in 00:00:07 =  530.5/s Avg:    39 Min:     1 Max:   282 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =  82913 in 00:03:20 =  414.3/s Avg:    51 Min:     0 Max:  2150 Err:    15 (0.02%)

看到没有，一开始就有少量的报错产生了，但一些我没加断言的报错应该没有在这里没显示出来。

所以根据应用的重要性，超时长度在具体的应用场景中，一定要做严格的测试。把完整的业务链路图画出来之后，一个个环节分析超时应该设置为多大，才是合理的做法。

总结

至于其他的 Tomcat 调优参数，你可以在自己的场景中实际操作一下。

总之，Tomcat 的优化就是这么几个关键环节：协议、运行模式（尽管现在我认为它已经不再有争议了，但是当你用老版本的 Tomcat 时还是要注意一下）、线程池（关键中的关键）等。

不止是 Tomcat 这样，其他类似的应用服务器也是一样。尽管这些应用服务器在架构设计上会不同，但是在我的调优生涯中，针对这样的应用服务器，可调优的关键点真的就这么几个。

可见这样的应用服务器本身可调优的点并不多。如果你要调的是 SpringBoot 中的 Tomcat 组件，也可以用同样的思路。

最后我还是要说，在你的具体工作中，一定要拿实际测试结果来分析判断，以免产生偏差。在任何时候，都要知道，性能测试的计数器中，没有哪个计数器的值可以直接告诉你性能问题的原因，只有通过自己的分析判断才能找得到。

思考题

最后给你留两道思考题吧。类似 Tomcat 的应用服务器，应该如何拆解监控计数器呢？我们应该如何判断应用服务器的线程是否够用？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流进步一下。

22丨MySQL：数据库级监控及常用计数器解析（上）

数据库是一个非常大的话题，我们在很多地方，都会看到对数据库的性能分析会包括以下部分。

但其实呢，以上这些内容都是我们应该具备的基础知识，所以我今天要讲的就是，具备了这些基础知识之后我们应该干什么事情。

也就是说，从性能瓶颈判断分析的角度入手，才是性能从业人员该有的逻辑。每次我分析一个性能问题时，逻辑总是这样的：

先画出整个系统的架构图。

列出整个系统中用到了哪些组件。这一步要确定用哪些监控工具来收集数据，具体的内容你可以看下之前讲到的监控设计相关的内容。

掌握每个组件的架构图。在这一步中需要列出它们的关键性能配置参数。

在压力场景执行的过程中收集状态计数器。

通过分析思路画出性能瓶颈的分析决策树。

找到问题的根本原因。

提出解决方案并评估每个方案的优缺点和成本。

这是我一直强调的分析决策树的创建逻辑。有了这些步骤之后，即使不熟悉一个系统，你也可以进行性能分析。

对于 MySQL 数据库来说，我们想对它进行分析，同样也需要看它的架构图。如下图所示（这是 MySQL5 版本的架构示意图）：

这里就有一个问题了：看架构图是看什么？这个图够细吗？

首先，看架构图，一开始肯定是看大而全的架构。比如说上图，我们知道了，MySQL 中有 Connection Pool、SQL Interface、Parser 等这些大的模块。

其次，我们得知道这些模块的功能及运行逻辑。比如说，我们看到了这些模块之后，需要知道，当一个 SQL 通过 Connection Pool 进到系统之后，需要先进入 SQL Interface 模块判断这个语句，知道它是一个什么样的 SQL，涉及到了什么内容；然后通过 Parser 模块进行语法语义检查，并生成相应的执行计划；接着到 Optimizer 模块进行优化，判断走什么索引，执行顺序之类的；然后就到 Caches 中找数据，如果在 Caches 中找不到数据的话，就得通过文件系统到磁盘中找。

这就是一个大体的逻辑。但是知道了这个逻辑还不够。还记得前面我们说的对一个组件进行“全局—定向”的监控思路吧。

这里我们也得找工具实现对 MySQL 的监控，还好 MySQL 的监控工具非常多。

在讲 MySQL 的监控工具之前，我们先来了解下 MySQL 中的两个 Schema，分别是information_schema和performance_schema 。

为什么呢？

information_schema保存了数据库中的所有表、列、索引、权限、配置参数、状态参数等信息。像我们常执行的show processlist;就来自于这个 schema 中的 processlist 表。

performance_schema提供了数据库运行时的资源消耗情况，它以较低的代价收集信息，可以提供不少性能数据。

所以这两个 Schema 对我们来说就非常重要了。

你没事的时候，也可以查一下它们相关的各个表，一个个看着玩。监控工具中的很多数据来自于它们。

还有两个命令是你在分析 MySQL 时一定要学会的：SHOW GLOBAL VARIABLES;和SHOW GLOBAL status;。前一个用来查看配置的参数值，后一个用来查询状态值。当你没有其他工具可用的时候，就可以用这两个命令的输出结果来分析。对于全局监控来说，这两个命令绝对够用。

对于 MySQL 的监控工具有很多，但我主要讲的是以下几个工具：

mysqlreport、pt-query-digest、mysql_exportor+Prometheus+Grafana。

今天我们先来说一下 mysqlreport。

全局分析：mysqlreport

这个工具执行之后会生成一个文本文件，在这个文本文件中包括了如下这些内容。

我觉得这个工具是属于既不浪费资源，又能全局监控 MySQL 的很好的工具。

在我们执行性能场景时，如果想让 mysqlreport 抓取到的数据更为准确，可以先重启一下数据库。如果你觉得重启数据库这个动作实在是有点大，可以先把状态计数器、打开表、查询缓存等数据给刷新一下。

我认为 mysqlreport 有一些重要的知识点需要你知道，在这里我找一个例子给你解释一下。

索引报表

_ Key _________________________________________________________________
Buffer used     5.00k of   8.00M  %Used:   0.06
  Current       1.46M            %Usage:  18.24

请注意，这里所指的 Key Buffer 是指 MyISAM 引擎使用的Shared Key Buffer，InnoDB 所使用的Key Buffer不在这里统计。

从上面的数据来看，MySQL 每次分配的Key Buffer最大是 5K，占 8M 的 0.06%，还是很小的。下一行中的数据可以看到的是当前只用了 1.46M，占 8M 的 18.24%。

显然这个 Key Buffer 是够用的，如果这个使用率高，你就得增加key_buffer_size的值了。

操作报表

__ Questions ___________________________________________________________
Total         126.82M    32.5/s
  +Unknown     72.29M    18.5/s  %Total:  57.00
  Com_         27.63M     7.1/s           21.79
  DMS          26.81M     6.9/s           21.14
  COM_QUIT     45.30k     0.0/s            0.04
  QC Hits      38.18k     0.0/s            0.03
Slow 2 s        6.21M     1.6/s            4.90  %DMS:  23.17  Log:
DMS            26.81M     6.9/s           21.14
  SELECT       20.73M     5.3/s           16.34         77.30
  INSERT        3.68M     0.9/s            2.90         13.71
  UPDATE        1.43M     0.4/s            1.13          5.33
  DELETE      983.11k     0.3/s            0.78          3.67
  REPLACE           0       0/s            0.00          0.00
Com_           27.63M     7.1/s           21.79
  admin_comma  11.86M     3.0/s            9.35
  set_option   10.40M     2.7/s            8.20
  commit        5.15M     1.3/s            4.06

从这个数据可以看到的信息量就有点大了，它可以反应出来这个数据库现在忙不忙。

从 32.5 每秒的操作量上来说，还是有点忙的。你还可以看到下面有操作数的细分，其实我不太愿意看下面的这些细分，描述上除了QC Hits和DMS的意思比较清晰之外，其他的几个值理解起来比较费劲。我也不建议你看下面那几个，因为它们对性能分析来说没起到什么正向的作用。

而 Slow 那这一行就很重要了，从这行可以看出slow log的时间是设置为 2 秒的，并且每秒还出现 1.6 个的慢日志，可见这个系统的 SQL 的慢日志实在是有点多。

DMS部分可以告诉我们这个数据库中各种 SQL 所占的比例。其实它是具有指向性的，像我们的这个例子中，显然是SELECT多，那如果要做 SQL 优化的话，肯定优先考虑SELECT的语句，才会起到立竿见影的效果。

查询和排序报表

__ SELECT and Sort _____________________________________________________
Scan            7.88M     2.0/s %SELECT:  38.04
Range         237.84k     0.1/s            1.15
Full join       5.97M     1.5/s           28.81
Range check   913.25k     0.2/s            4.41
Full rng join  18.47k     0.0/s            0.09
Sort scan     737.86k     0.2/s
Sort range     56.13k     0.0/s
Sort mrg pass 282.65k     0.1/s

这个报表具有着绝对的问题指向性。这里的Scan（全表扫描）和Full join（联合全表扫描）在场景执行过程中实在是太多了，这显然是 SQL 写得有问题。

Range 范围查询很正常，本来就应该多。

查询缓存报表

__ Query Cache _________________________________________________________
Memory usage  646.11k of   1.00M  %Used:  63.10
Block Fragmnt  14.95%
Hits           38.18k     0.0/s
Inserts         1.53k     0.0/s
Insrt:Prune    2.25:1     0.0/s
Hit:Insert    24.94:1

在这部分中，我们看的关键点是，Query Cache没用！因为各种query都没有缓存下来。同时这里我们还要看一个关键值，那就是Block Fragment，它是表明Query Cache碎片的，值越高，则说明问题越大。

如果你看到下面这样的数据，就明显没有任何问题。

__ Query Cache ______________________________________________________
Memory usage   38.05M of 256.00M  %Used:  14.86
Block Fragmnt   4.29%
Hits           12.74k    33.3/s
Inserts        58.21k   152.4/s
Insrt:Prune  58.21k:1   152.4/s
Hit:Insert     0.22:1

这个数据明显看到缓存了挺多的数据。Hits 这一行指的是每秒有多少个 SELECT 语句从Query Cache中取到了数据，这个值是越大越好。

而通过Insrt:Prune的比值数据，我们可以看到 Insert 远远大于 Prune（每秒删除的Query Cache碎片)，这个比值越大就说明Query Cache越稳定。如果这个值接近 1：1 那才有问题，这个时候就要加大Query Cache或修改你的 SQL 了。

而通过下面的Hit:Insert的值，我们可以看出命中要少于插入数，说明插入的比查询的还要多，这时就要去看这个性能场景中是不是全是插入了。如果我们查看了，发现 SELECT 语句还是很多的，而这个比值又是 Hit 少，那么我们的场景中使用的数据应该并不是插入的数据。其实在性能场景的执行过程中经常这样。所以在性能分析的过程中，我们只要知道这个值就可以了，并不能说明Query Cache就是无效的了。

表信息报表

__ Table Locks _________________________________________________________
Waited              0       0/s  %Total:   0.00
Immediate         996     0.0/s

__ Tables ______________________________________________________________
Open             2000 of 2000    %Cache: 100.00
Opened         15.99M     4.1/s

这个很明显了，表锁倒是不存在。但是你看现在table_open_cache已经达到上限了，设置为 2000，而现在已经达到了 2000，同时每秒打开表 4.1 个。

这些数据说明了什么呢？首先打开的表肯定是挺多的了，因为达到上限了嘛。这时候你会自然而然地想到去调table_open_cache参数。但是我建议你调之前先分析下其他的部分，如果在这个性能场景中，MySQL 的整体负载就会比较高，同时也并没有报错，那么我不建议你调这个值。如果负载不高，那再去调它。

连接报表和临时表

__ Connections _________________________________________________________
Max used          521 of 2000      %Max:  26.05
Total          45.30k     0.0/s

__ Created Temp ________________________________________________________
Disk table    399.77k     0.1/s
Table           5.81M     1.5/s    Size:  16.0M
File            2.13k     0.0/s

这个数据连接还完全够用，但是从临时表创建在磁盘（Disk table）和临时文件（File) 上的量级来说，还是有点偏大了，所以，可以增大tmp_table_size。

线程报表

__ Threads _____________________________________________________________
Running            45 of   79
Cached              9 of   28      %Hit:  72.35
Created        12.53k     0.0/s
Slow                0       0/s

__ Aborted _____________________________________________________________
Clients             0       0/s
Connects            7     0.0/s

__ Bytes _______________________________________________________________
Sent          143.98G   36.9k/s
Received       21.03G    5.4k/

当 Running 的线程数超过配置值时，就需要增加thread_cache_size。但是从这里来看，并没有超过，当前配置了 79，只用到了 45。而这里 Cached 的命中%Hit是越大越好，我们通常都希望在 99% 以上。

InnoDB 缓存池报表

__ InnoDB Buffer Pool __________________________________________________
Usage           1.87G of   4.00G  %Used:  46.76
Read hit      100.00%
Pages
  Free        139.55k            %Total:  53.24
  Data        122.16k                     46.60 %Drty:   0.00
  Misc            403                      0.15
  Latched                                  0.00
Reads         179.59G   46.0k/s
  From file    21.11k     0.0/s            0.00
  Ahead Rnd         0       0/s
  Ahead Sql                 0/s
Writes         54.00M    13.8/s
Flushes         3.16M     0.8/s
Wait Free           0       0/s

这个部分对 MySQL 来说是很重要的，innodb_buffer_pool_size为 4G，它会存储表数据、索引数据等。通常在网上或书籍里，你能看到有人建议将这个值设置为物理内存的 50%，当然这个值没有绝对的，还要在具体的应用场景中测试才能知道。

这里的Read hit达到 100%，这很好。

下面还有些其他的读写数据，这部分的数据将和我们在操作系统上看到的 I/O 有很大关系。有些时候，由于写入的过多，导致操作系统的I/O wait很高的时候，我们不得不设置innodb_flush_log_at_trx_commit参数（0：延迟写，实时刷；1：实时写，实时刷；2：实时写，延迟刷）和sync_binlog 参数（0：写入系统缓存，而不刷到磁盘；1：同步写入磁盘；N：写 N 次系统缓存后执行一次刷新操作）来降低写入磁盘的频率，但是这样做的风险就是当系统崩溃时会有数据的丢失。

这其实是我们做测试时，存储性能不高的时候常用的一种手段，为了让 TPS 更高一些。但是，你一定要知道生产环境中的存储是什么样的能力，以确定在生产环境中应该如何配置这个参数。

InnoDB 锁报表

__ InnoDB Lock _________________________________________________________
Waits          227829     0.1/s
Current             1
Time acquiring
  Total     171855224 ms
  Average         754 ms
  Max            6143 ms

这个信息就有意思了。显然在这个例子中，锁的次数太多了，并且锁的时间都还不短，平均时间都能达到 754ms，这显然是不能接受的。

那就会有人问了，锁次数和锁的平均时间多少才是正常呢？在我的经验中，锁平均时间最好接近零。锁次数可以有，这个值是累加的，所以数据库启动时间长，用得多，锁次数就会增加。

InnoDB 其他信息

__ InnoDB Data, Pages, Rows ____________________________________________
Data
  Reads        35.74k     0.0/s
  Writes        6.35M     1.6/s
  fsync         4.05M     1.0/s
  Pending
    Reads           0
    Writes          0
    fsync           0

Pages
  Created      87.55k     0.0/s
  Read         34.61k     0.0/s
  Written       3.19M     0.8/s

Rows
  Deleted     707.46k     0.2/s
  Inserted    257.12M    65.9/s
  Read        137.86G   35.3k/s
  Updated       1.13M     0.3/

这里的数据可以明确告诉你的一点是，在这个性能场景中，插入占有着绝对的量级。

总结

好了，我们拿一个 mysqlreport 报表从上到下看了一遍之后，你是不是觉得对 MySQL 有点感觉了？这里我给一个结论性的描述吧：

在这个性能场景中，慢日志太多了，需要定向监控看慢 SQL，找到慢 SQL 的执行计划。

在这个插入多的场景中，锁等待太多，并且等待的时候又太长，解决慢 SQL 之后，这里可能会解决，但还是要分析具体的原因的，所以这里也是指向了 SQL。

这里为什么要描述得这么细致呢？主要是因为当你看其他一些工具的监控数据时，分析思路是可以共用的。

但是有人说这里还有一个问题：SQL 怎么看？

其实对于我们分析的逻辑来说，在数据库中看 SQL 就是在做定向的分析了。请你不要相信一些人所吹嘘的那样，一开始就把所有的 SQL 执行时间统计出来，这真的是完全没有必要的做法。因为成本太高了。

在下一篇文章里，我们换个工具来看看 SQL 的执行时间到底应该怎么分析。

思考题

最后给你留两道思考题吧，MySQL 中全局监控工具可以给我们提供哪些信息？以及，如何判断 MySQL 状态值和配置值之间的关系呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事。

23丨MySQL：数据库级监控及常用计数器解析（下）

上一篇文章中，我们讲了有关数据库的全局分析，那么在今天的文章中，我们继续看看在数据库中，如何做定向分析。

还记得我在上篇文章中提到的工具吗？mysqlreport、pt-query-digest 和 mysql_exportor+Prometheus+Grafana。我们在上一篇中已经讲完了 mysqlreport，今天我们来看看剩下的这几个。

定向抓取 SQL：pt-query-digest

pt-query-digest是个挺好的工具，它可以分析slow log、general log、binary log，还能分析 tcpdump 抓取的 MySQL 协议数据，可见这个工具有多强大。pt-query-digest属于 Percona-tool 工具集，这个 Percona 公司还出了好几个特别好使的监控 MySQL 的工具。

pt-query-digest分析 slow log 时产生的报告逻辑非常清晰，并且数据也比较完整。执行命令后就会生成一个报告。

我来稍微解释一下这个报告。我们先看这个报告的第一个部分：

# 88.3s user time, 2.5s system time, 18.73M rss, 2.35G vsz
# Current date: Thu Jun 22 11:30:02 2017
# Hostname: localhost
# Files: /Users/Zee/Downloads/log/10.21.0.30/4001/TENCENT64-slow.log.last
# Overall: 210.18k total, 43 unique, 0.26 QPS, 0.14x concurrency _________
# Time range: 2017-06-12 21:20:51 to 2017-06-22 09:26:38
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time        118079s   100ms      9s   562ms      2s   612ms   293ms
# Lock time            15s       0     7ms    71us   119us    38us    69us
# Rows sent          1.91M       0  48.42k    9.53   23.65  140.48    2.90
# Rows examine      13.99G       0   3.76M  69.79k 101.89k  33.28k  68.96k
# Rows affecte       3.36M       0   1.98M   16.76    0.99   4.90k       0
# Query size       102.82M       6  10.96k  512.99  719.66  265.43  719.66

从上表中可以看得出来，在这个慢日志中，总执行时间达到了 118079s，平均执行时间为 562ms，最长执行时间为 9s，标准方差为 612ms。

可见在此示例中，SQL 执行还是有点慢的。

这时也许会有人问，SQL 执行多长时间才是慢呢？之前在一个金融机构，我跟一个做核心系统的团队讨论他们的 SQL 执行时间指标。他们判断之后说，希望 SQL 平均执行时间指标定在 500ms。我说，你们要 500ms，那前面还有一连串的节点才能到达最终的用户，如果每个环节都这样要求自己，那最终的用户不就明显感觉到很慢了吗？

经过一轮轮的讨论，最后定在了 100ms 以内。

其实从我的经验上来看，对于大部分实时的业务，一个 SQL 执行的平均时间指标定在 100ms 都多了。但是对性能来说就是这样，在所有的环节中都没有固定的标准，只有经验数据和不断演化的系统性能能力。

我们再接着分析上面的数据。再来看pt-query-digest给出的负载报表：

# Profile
# Rank Query ID           Response time    Calls  R/Call V/M   Item
# ==== ================== ================ ====== ====== ===== ===========
#    1 0x6A516B681113449F 73081.7989 61.9%  76338 0.9573  0.71 UPDATE mb_trans
#    2 0x90194A5C40980DA7 38014.5008 32.2% 105778 0.3594  0.20 SELECT mb_trans mb_trans_finan
#    3 0x9B56065EE2D0A5C8  3893.9757  3.3%   9709 0.4011  0.11 UPDATE mb_finan
# MISC 0xMISC              3088.5453  2.6%  18353 0.1683   0.0 <40 ITEMS>

从这个表中可以看到，有两个 SQL 的执行时间占了总执行时间的 94%，显然这两个 SQL 是要接下来要分析的重点。

我们再接着看这个工具给出的第一个 SQL 的性能报表：

# Query 1: 0.30 QPS, 0.29x concurrency, ID 0x6A516B681113449F at byte 127303589
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.71
# Time range: 2017-06-16 21:12:05 to 2017-06-19 18:50:59
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         36   76338
# Exec time     61  73082s   100ms      5s   957ms      2s   823ms   672ms
# Lock time     19      3s    20us     7ms    38us    66us    29us    33us
# Rows sent      0       0       0       0       0       0       0       0
# Rows examine  36   5.06G   3.82k 108.02k  69.57k 101.89k  22.70k  68.96k
# Rows affecte   2  74.55k       1       1       1       1       0       1
# Query size    12  12.36M     161     263  169.75  192.76   11.55  158.58
# String:
# Databases    db_bank
# Hosts        10.21.16.50 (38297/50%)... 1 more
# Users        user1
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms
#  10ms
# 100ms  ################################################################
#    1s  #########################################
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `db_bank` LIKE 'mb_trans'\G
#    SHOW CREATE TABLE `db_bank`.`mb_trans`\G
UPDATE mb_trans
    SET
    resCode='PCX00000',resultMes='交易成功',payTranStatus='P03',payRouteCode='CMA'
    WHERE
    seqNo='20170619PM010394356875'\G
# Converted for EXPLAIN
# EXPLAIN /*!50100 PARTITIONS*/
select
    resCode='PCX00000',resultMes='交易成功',payTranStatus='P03',payRouteCode='CMA' from mb_trans where
    seqNo='20170619PM010394356875'\G

从查询时间分布图上来看，这个语句的执行时间在 100ms~1s 之间居多，95% 的执行时间在 2s 以下。那么这个 SQL 就是我们接下来要调优的重点了。

第二个 SQL 我就不赘述了，因为逻辑是完全一样的。

通过对慢日志的分析，我们可以很快知道哪个 SQL 是慢的了。当然你用mysqldumpslow分析，也会得到一样的结果。

SQL 剖析：profiling

在分析数据库的性能时，显然对 SQL 的分析是绕不过去的一个环节。但是我之前也说过了，上来就对 SQL 进行全面剖析也是不合逻辑的，因为 SQL 那么多，如果对每个 SQL 都进行详细的执行步骤解析，显然会拖慢整个系统，而且，对一些执行快的 SQL 进行分析也没有什么必要，徒增资源消耗。

通过上面的分析过程，我们已经定位到了具体是哪个 SQL 执行得慢，那么下面就是要知道 SQL 的执行细节。无论是在 Oracle 还是在 MySQL 中，我们都要去看执行计划。

比如说下面这样的：

上图中select_type是子句类型的意思，有简单有复杂，但是它不能说明什么成本的问题。在这里，最重要的内容是 type，因为 type 可以告诉你访问这个表的时候，是通过什么样的方式访问的。上图中的 ALL 是全表扫描的意思。type 还有如下几个值：

执行计划中的possible_keys会列出可能使用到的索引值。key 这一列会列出执行时使用到的索引值。

以上信息就是 MySQL 的执行计划中比较重要的部分了。这些信息可以帮助我们做 SQL 的分析，为优化提供证据。

除了执行计划外，MySQL 还提供了profiling，这个有什么用呢？它可以把 SQL 执行的每一个步骤详细列出来，从一个 SQL 进入到数据库中，到执行完这整个生命周期。

MySQL 的profiling在session级生效，所以当你用了慢日志，知道哪个 SQL 有问题之后，再用这个功能是最见成效的。如果想一开始就把所有session的SQL profiling功能打开，那成本就太高了。

下面我来详细解释一下 profiling 的用法和功能。

profiling 操作步骤

profiling 的操作步骤比较简单，如下所示：

步骤一 ：set profiling=1;  //这一步是为了打开profiling功能
步骤二 ：执行语句           //执行你从慢日志中看到的语句
步骤三 ：show profiles;    //这一步是为了查找步骤二中执行的语句的ID
步骤四 ：show profile all for query id; //这一步是为了显示出profiling的结果

我们实际执行一下上面的步骤：

// 步骤一：打开profiling功能
mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
// 这一步只是为了确认一下profiles列表有没有值，可以不用执行。
mysql> show profiles;
Empty set, 1 warning (0.00 sec)
// 步骤二：执行语句
mysql> select * from t_user where user_name='Zee0355916';
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
| id                                   | user_number | user_name  | org_id | email                | mobile      | create_time         |
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
| 00000d2d-32a8-11ea-91f8-00163e124cff | 00009496    | Zee0355916 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 14:19:32 |
| 77bdb1ef-32a6-11ea-91f8-00163e124cff | 00009496    | Zee0355916 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 14:08:34 |
| d4338339-32a2-11ea-91f8-00163e124cff | 00009496    | Zee0355916 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 13:42:31 |
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
3 rows in set (14.33 sec)
// 步骤三：查看profiles列表中，有了我们刚才执行的语句
mysql> show profiles;
+----------+-------------+---------------------------------------------------+
| Query_ID | Duration    | Query                                             |
+----------+-------------+---------------------------------------------------+
|        1 | 14.34078475 | select * from t_user where user_name='Zee0355916' |
+----------+-------------+---------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
// 步骤四：看这个语句的profile信息
mysql> show profile all for query 1;
+--------------------------------+-----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+------------------+-------------+
| Status                         | Duration  | CPU_user | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function       | Source_file      | Source_line |
+--------------------------------+-----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+------------------+-------------+
| starting                       |  0.000024 | 0.000012 |   0.000005 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | NULL                  | NULL             |        NULL |
| Waiting for query cache lock   |  0.000004 | 0.000003 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| init                           |  0.000003 | 0.000002 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| checking query cache for query |  0.000052 | 0.000036 |   0.000015 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | send_result_to_client | sql_cache.cc     |        1601 |
| checking permissions           |  0.000007 | 0.000005 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | check_access          | sql_parse.cc     |        5316 |
| Opening tables                 |  0.000032 | 0.000023 |   0.000009 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | open_tables           | sql_base.cc      |        5095 |
| init                           |  0.000042 | 0.000029 |   0.000013 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_prepare_select  | sql_select.cc    |        1051 |
| System lock                    |  0.000016 | 0.000011 |   0.000004 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_lock_tables     | lock.cc          |         304 |
| Waiting for query cache lock   |  0.000003 | 0.000002 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| System lock                    |  0.000020 | 0.000014 |   0.000006 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| optimizing                     |  0.000012 | 0.000009 |   0.000004 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc |         139 |
| statistics                     |  0.000019 | 0.000013 |   0.000005 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc |         365 |
| preparing                      |  0.000015 | 0.000010 |   0.000005 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc |         488 |
| executing                      |  0.000004 | 0.000003 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | exec                  | sql_executor.cc  |         110 |
| Sending data                   | 14.324781 | 4.676869 |   0.762349 |              1316 |                 132 |      2499624 |           288 |             0 |                 0 |                 8 |             30862 |     0 | exec                  | sql_executor.cc  |         190 |
| end                            |  0.000015 | 0.000007 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_select  | sql_select.cc    |        1106 |
| query end                      |  0.000006 | 0.000005 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_command | sql_parse.cc     |        5015 |
| closing tables                 |  0.000016 | 0.000013 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_command | sql_parse.cc     |        5063 |
| freeing items                  |  0.000013 | 0.000010 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 2 |     0 | mysql_parse           | sql_parse.cc     |        6490 |
| Waiting for query cache lock   |  0.000003 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| freeing items                  |  0.000014 | 0.000012 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| Waiting for query cache lock   |  0.000003 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| freeing items                  |  0.000003 | 0.000002 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| storing result in query cache  |  0.000004 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | end_of_result         | sql_cache.cc     |        1034 |
| logging slow query             |  0.015645 | 0.000084 |   0.000020 |                 2 |                   0 |           16 |             8 |             0 |                 0 |                 0 |                 2 |     0 | log_slow_do           | sql_parse.cc     |        1935 |
| cleaning up                    |  0.000034 | 0.000024 |   0.000006 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | dispatch_command      | sql_parse.cc     |        1837 |
+--------------------------------+-----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+------------------+-------------+
26 rows in set, 1 warning (0.02 sec)

非常长，从这样的数据中，我们就看到了一个语句在数据库中从开始到结束的整个生命周期。

对生命周期中的每个步骤进行统计之后，我们就可以看到每个步骤所消耗的时间。不仅如此，还能看到如下这些信息：

BLOCK IO

Context Switches

CPU

IPC

MEMORY

Page Fault

SOURCE

SWAPS

有了这些信息，我们基本上就可以判断语句哪里有问题了。

从上面这个示例语句中，你可以看到Sending data这一步消耗了 14 秒的时间，并且从后面的数据中，也可以看到主动上下文切换有 1316 次，被动的有 132 次，块操作的量也非常大。

碰到这样的情况，我们就得先知道这个Sending data到底是什么东西。下面我们结合之前说的到的执行计划，一起看一下：

mysql> explain select * from t_user where user_name='Zee0355916';
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | t_user | ALL  | NULL          | NULL | NULL    | NULL | 3868195 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
1 row in set (0.00 sec)

这就是个典型的全表扫描，所以下一步就是检查有没有创建索引。

mysql> show indexes from t_user;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t_user |          0 | PRIMARY  |            1 | id          | A         |     3868195 |     NULL | NULL   |      | BTREE      |         |               |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set (0.00 sec)

mysql>

还是有一个主键索引的，但由于我们没用主键来查，所以用不到。

有些性能测试工程师面对这种情况可能会有这种想法：第一次没有查索引，但是把所有数据都调到缓存里了呀，所以第二次就快了嘛，于是有些人可能想尽快“完成”工作，就用重复的数据。

这里我再执行一遍，你可以看看是什么结果：

+----------+-------------+-----------------------------------------------------------+
| Query_ID | Duration    | Query                                                     |
+----------+-------------+-----------------------------------------------------------+
|        1 | 14.34078475 | select * from t_user where user_name='Zee0355916'         |
|        2 |  0.00006675 | show profile all for 1                                    |
|        3 |  0.00031700 | explain select * from t_user where user_name='Zee0355916' |
|        4 |  0.00040025 | show indexes from t_user                                  |
+----------+-------------+-----------------------------------------------------------+
6 rows in set, 1 warning (0.00 sec)

mysql> select * from t_user where user_name='Zee0355916';
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
| id                                   | user_number | user_name  | org_id | email                | mobile      | create_time         |
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
| 00000d2d-32a8-11ea-91f8-00163e124cff | 00009496    | Zee0355916 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 14:19:32 |
| 77bdb1ef-32a6-11ea-91f8-00163e124cff | 00009496    | Zee0355916 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 14:08:34 |
| d4338339-32a2-11ea-91f8-00163e124cff | 00009496    | Zee0355916 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 13:42:31 |
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
3 rows in set (0.00 sec)

mysql> show profiles;
+----------+-------------+-----------------------------------------------------------+
| Query_ID | Duration    | Query                                                     |
+----------+-------------+-----------------------------------------------------------+
|        1 | 14.34078475 | select * from t_user where user_name='Zee0355916'         |
|        2 |  0.00006675 | show profile all for 1                                    |
|        3 |  0.00031700 | explain select * from t_user where user_name='Zee0355916' |
|        4 |  0.00040025 | show indexes from t_user                                  |
|        5 |  0.00027325 | select * from t_user where user_name='Zee0355916'         |
+----------+-------------+-----------------------------------------------------------+
7 rows in set, 1 warning (0.00 sec)

mysql> show profile all for query 5;
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+--------------+-------------+
| Status                         | Duration | CPU_user | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function       | Source_file  | Source_line |
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+--------------+-------------+
| starting                       | 0.000029 | 0.000018 |   0.000004 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | NULL                  | NULL         |        NULL |
| Waiting for query cache lock   | 0.000006 | 0.000003 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc |         468 |
| init                           | 0.000003 | 0.000003 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc |         468 |
| checking query cache for query | 0.000008 | 0.000006 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | send_result_to_client | sql_cache.cc |        1601 |
| checking privileges on cached  | 0.000003 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | send_result_to_client | sql_cache.cc |        1692 |
| checking permissions           | 0.000010 | 0.000192 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | check_access          | sql_parse.cc |        5316 |
| sending cached result to clien | 0.000210 | 0.000028 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 1 |     0 | send_result_to_client | sql_cache.cc |        1803 |
| cleaning up                    | 0.000006 | 0.000006 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | dispatch_command      | sql_parse.cc |        1837 |
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+--------------+-------------+
8 rows in set, 1 warning (0.00 sec)

mys

看到没有，在用重复数据的时候确实会让响应时间快很多，因为数据直接从cache中发给client了。

但是，这种作法请你坚决制止，因为它不符合真实生产的样子。当你再换一个数据的时候，就会歇菜，还要再经过 14 秒的时间做全表扫描。

所以正确的做法是创建合适的索引，让语句在执行任何一条数据时都能快起来，那么，我们现在就创建一个索引，再看执行结果。

// 创建索引
mysql> ALTER TABLE t_user ADD INDEX username_idx (user_name);
Query OK, 0 rows affected (44.69 sec)
Records: 0  Duplicates: 0  Warnings: 0
// 分析表
mysql> analyze table t_user;
+-----------+---------+----------+----------+
| Table     | Op      | Msg_type | Msg_text |
+-----------+---------+----------+----------+
| pa.t_user | analyze | status   | OK       |
+-----------+---------+----------+----------+
1 row in set (0.08 sec)
// 执行语句
mysql> select * from t_user where user_name='Zee0046948';
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
| id                                   | user_number | user_name  | org_id | email                | mobile      | create_time         |
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
| 000061a2-31c2-11ea-8d89-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-08 10:53:08 |
| 047d7ae1-32a2-11ea-91f8-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 13:36:42 |
| 1abfa543-318f-11ea-8d89-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-08 04:48:48 |
| 671c4014-3222-11ea-91f8-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-08 22:23:12 |
| 9de16dd3-32a5-11ea-91f8-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 14:02:28 |
| dd4ab182-32a4-11ea-91f8-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 13:57:05 |
| f507067e-32a6-11ea-91f8-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-09 14:12:04 |
| f7b82744-3185-11ea-8d89-00163e124cff | 00009496    | Zee0046948 | NULL   | test9495@dunshan.com | 17600009498 | 2020-01-08 03:43:24 |
+--------------------------------------+-------------+------------+--------+----------------------+-------------+---------------------+
8 rows in set (0.02 sec)
// 查看Query_ID
mysql> show profiles;
+----------+-------------+-----------------------------------------------------------+
| Query_ID | Duration    | Query                                                     |
+----------+-------------+-----------------------------------------------------------+
|        1 | 14.34078475 | select * from t_user where user_name='Zee0355916'         |
|        2 |  0.00006675 | show profile all for 1                                    |
|        3 |  0.00031700 | explain select * from t_user where user_name='Zee0355916' |
|        4 |  0.00005875 | show indexes for table t_user                             |
|        5 |  0.00005850 | show indexes for t_user                                   |
|        6 |  0.00040025 | show indexes from t_user                                  |
|        7 |  0.00027325 | select * from t_user where user_name='Zee0355916'         |
|        8 |  0.00032100 | explain select * from t_user where user_name='Zee0355916' |
|        9 | 12.22490550 | select * from t_user where user_name='Zee0046945'         |
|       10 |  0.00112450 | select * from t_user limit 20                             |
|       11 | 44.68370500 | ALTER TABLE t_user ADD INDEX username_idx (user_name)     |
|       12 |  0.07385150 | analyze table t_user                                      |
|       13 |  0.01516450 | select * from t_user where user_name='Zee0046948'         |
+----------+-------------+-----------------------------------------------------------+
13 rows in set, 1 warning (0.00 sec)
// 查看profile信息
mysql> show profile all for query 13;
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+------------------+-------------+
| Status                         | Duration | CPU_user | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function       | Source_file      | Source_line |
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+------------------+-------------+
| starting                       | 0.000030 | 0.000017 |   0.000004 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | NULL                  | NULL             |        NULL |
| Waiting for query cache lock   | 0.000005 | 0.000004 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| init                           | 0.000003 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| checking query cache for query | 0.000060 | 0.000050 |   0.000011 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | send_result_to_client | sql_cache.cc     |        1601 |
| checking permissions           | 0.000009 | 0.000007 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | check_access          | sql_parse.cc     |        5316 |
| Opening tables                 | 0.000671 | 0.000412 |   0.000000 |                 1 |                   0 |            8 |             0 |             0 |                 0 |                 0 |                 1 |     0 | open_tables           | sql_base.cc      |        5095 |
| init                           | 0.006018 | 0.000082 |   0.000899 |                 1 |                   0 |         5408 |             0 |             0 |                 0 |                 1 |                 0 |     0 | mysql_prepare_select  | sql_select.cc    |        1051 |
| System lock                    | 0.000017 | 0.000011 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_lock_tables     | lock.cc          |         304 |
| Waiting for query cache lock   | 0.000003 | 0.000003 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| System lock                    | 0.000019 | 0.000015 |   0.000004 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| optimizing                     | 0.000012 | 0.000010 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc |         139 |
| statistics                     | 0.001432 | 0.000167 |   0.000037 |                 1 |                   0 |           32 |             0 |             0 |                 0 |                 0 |                 4 |     0 | optimize              | sql_optimizer.cc |         365 |
| preparing                      | 0.000026 | 0.000043 |   0.000009 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 1 |     0 | optimize              | sql_optimizer.cc |         488 |
| executing                      | 0.000034 | 0.000005 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | exec                  | sql_executor.cc  |         110 |
| Sending data                   | 0.006727 | 0.000439 |   0.001111 |                13 |                   0 |         1536 |             0 |             0 |                 0 |                 0 |                 1 |     0 | exec                  | sql_executor.cc  |         190 |
| end                            | 0.000014 | 0.000007 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_select  | sql_select.cc    |        1106 |
| query end                      | 0.000009 | 0.000008 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_command | sql_parse.cc     |        5015 |
| closing tables                 | 0.000015 | 0.000012 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_command | sql_parse.cc     |        5063 |
| freeing items                  | 0.000010 | 0.000008 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_parse           | sql_parse.cc     |        6490 |
| Waiting for query cache lock   | 0.000003 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| freeing items                  | 0.000027 | 0.000022 |   0.000005 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| Waiting for query cache lock   | 0.000003 | 0.000002 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| freeing items                  | 0.000003 | 0.000002 |   0.000000 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | try_lock              | sql_cache.cc     |         468 |
| storing result in query cache  | 0.000004 | 0.000004 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | end_of_result         | sql_cache.cc     |        1034 |
| cleaning up                    | 0.000015 | 0.000012 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | dispatch_command      | sql_parse.cc     |        1837 |
+--------------------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+------------------+-------------+
25 rows in set, 1 warning (0.01 sec)

mysql>

从上面最后的 profile 信息你可以看出来，步骤一点没少，但是速度快了很多，这才是正确的优化思路。

在上一篇文章中，我描述了在一个数据库中，如何从全局监控的角度查看数据，今天讲的是如何找到具体慢的 SQL，以及如何定位这个 SQL 的问题。

当然不是所有的情况下，都是 SQL 的问题，也有可能是配置的问题，也有可能是硬件的问题。不管什么样的问题，其分析思路都是这样的，也就是我总是在强调的：全局监控 - 定向监控。

当然，在这里我也应该给出 MySQL 分析决策树的思路。从mysqlreport的划分上，给出几个具体的分析决策树的树枝。

这是常见的问题，如果你有兴趣，可以自己完善这棵完整的树，因为你可能会有不一样的划分计数器的工具或思路，所以这个树是可以灵活变化的。

你一定要记得，别人给你的东西，永远变不成自己的东西，它们只能引导你。如果你自己动手去做一遍，哪怕只画出一个分枝来，都会是很大的进步。

如果你想用其他的全局监控工具，也可以考虑如下的组合，也就是 mysql_exportor+Prometheus+Grafana。

mysql_exportor+Prometheus+Grafana

我在前面也屡次提到过这类组合，不同的 exportors 结合 Prometheus+Grafana，可以实现实时监控及数据的保存。

在这里我们看一下mysql_exportor可以给我们提供什么样的监控数据。这里截几个图，给你大概看一下这个套装工具能看什么内容，有兴趣的话，你也可以自己搭建一下。

总结

有关数据库的知识实在是太多了，在这两篇文章中，我重点想告诉你的，就是分析数据库应该具有的思路。至于其他的知识点，我想应该是你打开文章之前就应该储备的东西。

我们再来总结一下，在数据库的分析中，最有可能在三个方面出现问题：

硬件配置

数据库配置

SQL 语句

对于硬件配置来说，我们只能在解决了 2 和 3 的问题之后，再来评估到底多少硬件够用的。而面对数据库配置问题，这个实在没什么好招，只能去了解数据库架构等一系列的知识之后，再学着解决。而 SQL 的问题呢，应该说是我们在性能测试和分析中最常见的了。SQL 性能问题的分析思路也比较清晰，那就是判断出具体的 SQL 瓶颈点，进而做相应的优化，切记不要蒙！

现在的数据库类别比之前多太多了，每种数据库都有自己的架构和使用场景，我们要在充分了解了之后，才能下手去调。

思考题

我在这里照例留两个问题。你能说一下数据库分析的大体思路是什么吗？如何在数据库中迅速找到一个慢 SQL 的根本原因呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

24丨Kafka：性能监控工具之队列级监控及常用计数器解析

在我看来队列服务器是最简单的一种组件了。因为队列给我们下手的机会实在是并不多。我们只是用它，如果想改变它就只能去改代码，其他的都只是配置问题。

在当前的市场中，Kafka 算是用得非常火的一个队列服务器了，所以今天，我选择它来做一些解读。

虽然我在前面一直在强调分析的思路，但在这一篇中，我打算换个思路，不是像以前那样，直接给你一个结论型的思维导图，而是一起来分析一个组件，让我们看看从哪里下手，来观察一个被分析对象的相关配置。

了解 Kafka 的基本知识

我们先看一下这张图，以便更好地了解一个队列服务器。

这是 Kafka 官网上的一个图。从这个图中可以看到，对 Kafka 来说，这就像一个典型的集线器。那它里面的结构是什么样子的呢？根据我的理解，我画了一个如下的示意图：

在这个图中，有三个 Broker，也就是三个集群节点。每个消息有一个 leader partition，还有两个 follower partition。我没有画更多的 Producer 和 Consumer、Consumer Group，是觉得线太多了容易乱。

因为 Producer 和 Consumer 肯定会从 leader partition 中读写数据，而 Kafka 也保证了 leader 在不同 broker 上的均衡，所以 Kafka 的集群能力很好。

我们再看一下消息是如何在 Kafka 中被存储的。

上图是 Kafka 数据的存储方式，也就是每个分区都是一直往后面加的。

我们再来看一下它的数据存储方式。

首先是目录：

drwxr-xr-x 2 root root 4096 Feb  7 23:39 test-0
drwxr-xr-x 2 root root 4096 Feb  7 01:34 test_perf-1
drwxr-xr-x 2 root root 4096 Feb  7 01:34 test_perf-4

Kafka 的目录是根据 topic 创建的，每个目录名中也包括一个 partition。比如上面名字中的 test_perf-1 就是 topic 名是 test_perf，partition 就是 1。

接着再来看下文件：

[root@node-2 test-2]# ll
total 10850656
-rw-r--r-- 1 root root     493128 Feb  9 14:14 00000000000000000000.index
-rw-r--r-- 1 root root 1073739646 Feb  9 14:14 00000000000000000000.log
-rw-r--r-- 1 root root     630504 Feb  9 14:14 00000000000000000000.timeindex
-rw-r--r-- 1 root root     443520 Feb  9 14:16 00000000000000240212.index
-rw-r--r-- 1 root root 1073727327 Feb  9 14:16 00000000000000240212.log
-rw-r--r-- 1 root root     551052 Feb  9 14:16 00000000000000240212.timeindex
-rw-r--r-- 1 root root     448840 Feb  9 14:18 00000000000000453584.index
-rw-r--r-- 1 root root 1073729759 Feb  9 14:18 00000000000000453584.log
-rw-r--r-- 1 root root     556920 Feb  9 14:18 00000000000000453584.timeindex
.........................
-rw-r--r-- 1 root root         12 Feb  9 13:14 leader-epoch-checkpoint
[root@node-2 test-2]#

有索引文件，有数据文件，也有时间索引文件，非常明显的三个后缀名。索引文件显然就是指向 message 在数据文件中的什么位置，而这些数据文件就是一个个的 Segment，也就是一段一段的。这些文件的大小受 server.properties 文件中的 log.segment.bytes 参数限制，默认为 1G。

要查到相应的 message 就要先查索引文件，找到 message 的位置；然后从 log 文件中找到具体的 message。

在这个逻辑中，Segment 的大小就很有讲究了，太细就会导致索引文件过大，查找索引费时间；太粗了就会导致查找得不够精准。那么该如何配置呢？也要通过性能测试才能知道。

有了这些信息之后，我们再看下 Kafka 高效的原因：

Kafka 直接使用 Linux 文件系统的 Cache 来高效缓存数据。

Kafka 采用 Linux Zero-Copy 技术提高发送性能（不懂 Linux Zero-copy 的请自行补课）。

Kafka 服务端采用的是 selector 多线程模式（从逻辑上理解，它和 Tomcat 的 NIO 类似，我就不单独画图了，以免占篇幅）。

Kafka 采用二分法找数据。

总体来说，就是一个 Java 的应用，直接使用文件系统和操作系统的特性实现了队列的高效应用场景。

配置文件

我们先来查看一下 Kafka 的配置文件中都有什么，为了简洁，在这里，我把一些注释以及和性能无关的配置删除了。当然如果你有兴趣的话，可以到 Kafka 的 config 目录中找到 server.properties 中，以查看这些内容。

############################# Socket Server Settings #############################
num.network.threads=3
num.io.threads=8
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600

############################# Log Basics #############################
num.partitions=10
num.recovery.threads.per.data.dir=1

############################# Internal Topic Settings  #############################
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1

############################# Log Flush Policy #############################
log.flush.interval.messages=10000
log.flush.interval.ms=1000

############################# Log Retention Policy #############################
log.retention.check.interval.ms=300000

############################# Zookeeper #############################
zookeeper.connection.timeout.ms=6000

############################# Group Coordinator Settings #############################
group.initial.rebalance.delay

其实配置文件并不多，对不对？从配置名称上也很容易知道它们和什么相关。这里比较重要的参数就是 Socket Server 相关的，以及和 log 相关的。

我觉得到了这里，这个逻辑就基本清楚了，对 Kafka 的性能优化也就有了大体的判断。

构建 Kafka 的性能优化思维导图

我们可以根据以上的知识画出如下所示的，Kafka 的基本优化点：

同样的，我把操作系统和 JDK 方面的优化当成独立的部分，在上图中只把 Kafka 相关的内容列出来。

有了上面的知识，也有了这个思维逻辑，那么就可以理出针对一个 Kafka 应用要干的事情：

先分析一下具体的应用场景，关键是 topic、partition 数量、message 大小。

确定要支撑的业务容量和时间长度。

分析架构中需要的 broker 量级、partition、Segment 等配置。这些配置应该是架构师给出的准确预估，如果不能给出，那只能靠我们，也就是做性能测试的人给出具体的结论了。

对组件的性能分析思路

我想告诉你的是对一个组件的性能分析思路。如果你有了下面这张图所示的思路，那至少可以覆盖大部分的性能问题了。这个思路就是：

对于 Kafka 这样的队列服务器来说，状态计数器是啥子呢？让我们看一下 Kafka 的一个 Grafana Dashboard。

从这几个图就能看得出来，最重要的是每秒产生了多少 message，以及消费时间间隔。这两个对我们来说是最重要的队列计数器了。

但是它们能不能告诉我们现在的队列服务器有没有瓶颈呢？显然是不能的。

对于队列来说，消息都是异步被消费者取走的。所以队列中要有保存消息的能力，但是保存多久呢？永远保存吗？显然不现实。但是如果保存得太短了，正常的业务都可能做不下去，所以，我们要制定策略，哪些 topic 是实时处理的，处理不完怎么办？内存多大，能保存多少消息，积压了怎么办？

所以对于队列服务器，只看上面的那几个计数器，我觉得过于片面。

我们前面提到的 grafana+prometheus 监控操作系统、MySQL 的 DashBoard 都有非常完整的数据，但是 Kafka 的 DashBoard 显然信息不够，不能判断它自己有没有问题。

操作系统的监控指标对 Kafka 来说，也是异常的重要。就像之前我说过的那样，操作系统是不可绕过的分析节点。所以所有要做性能测试和性能分析的人，首先要学的就是操作系统方面的知识。

示例

下面我们来看一个简单测试示例。

生产 10W 消息

在这个示例中，共生产 10W 的消息，每个消息大小是 2000 字节，每秒产生 5000 个消息。

[root@node-1 Kafka_2.13-2.4.0]# /home/zee/Kafka/Kafka_2.13-2.4.0/bin/Kafka-producer-perf-test.sh --topic test --num-records 100000 --record-size 2000 --throughput 5000 --producer-props bootstrap.servers=172.18.0.2:9092,172.19.0.14:9092,172.20.0.7:9092
24997 records sent, 4999.4 records/sec (9.54 MB/sec), 15.8 ms avg latency, 398.0 ms max latency.
25010 records sent, 5001.0 records/sec (9.54 MB/sec), 26.0 ms avg latency, 514.0 ms max latency.
25000 records sent, 5000.0 records/sec (9.54 MB/sec), 1.1 ms avg latency, 24.0 ms max latency.
100000 records sent, 4998.000800 records/sec (9.53 MB/sec), 11.03 ms avg latency, 514.00 ms max latency, 1 ms 50th, 52 ms 95th, 305 ms 99th, 501 ms 99.9th.

可以看到每秒有 9.53MB 的消息产生，平均响应时延是 11.03ms，最大时延是 514ms。

生产 100W 消息

在这个示例中，共生产 100W 的消息，每个消息大小是 2000 字节，每秒产生 5000 个消息。

[root@node-4 bin]# /home/zee/Kafka/Kafka_2.13-2.4.0/bin/Kafka-producer-perf-test.sh --topic test_perf --num-records 1000000 --record-size 2000 --throughput 5000 --producer-props bootstrap.servers=172.17.0.11:9092,172.19.0.14:9092,172.20.0.7:9092
24992 records sent, 4996.4 records/sec (9.53 MB/sec), 21.7 ms avg latency, 482.0 ms max latency.
25025 records sent, 5004.0 records/sec (9.54 MB/sec), 0.9 ms avg latency, 16.0 ms max latency.
........
25000 records sent, 5000.0 records/sec (9.54 MB/sec), 0.6 ms avg latency, 9.0 ms max latency.
25005 records sent, 5001.0 records/sec (9.54 MB/sec), 0.7 ms avg latency, 30.0 ms max latency.
1000000 records sent, 4999.625028 records/sec (9.54 MB/sec), 2.05 ms avg latency, 482.00 ms max latency, 1 ms 50th, 1 ms 95th, 16 ms 99th, 267 ms 99.9th.

可以看到每秒有 9.54MB 的消息产生，平均响应时延是 2.05ms，最大时延是 482ms。

生产 1000W 消息

在这个示例中，生产 1000W 消息，其他参数不变：

[root@node-4 bin]# /home/zee/Kafka/Kafka_2.13-2.4.0/bin/Kafka-producer-perf-test.sh --topic test_perf --num-records 10000000 --record-size 2000 --throughput 5000 --producer-props bootstrap.servers=172.17.0.11:9092,172.19.0.14:9092,172.20.0.7:9092
24992 records sent, 4998.4 records/sec (9.53 MB/sec), 22.7 ms avg latency, 480.0 ms max latency.
25015 records sent, 5002.0 records/sec (9.54 MB/sec), 0.8 ms avg latency, 13.0 ms max latency.
25005 records sent, 5000.0 records/sec (9.54 MB/sec), 0.7 ms avg latency, 21.0 ms max latency.
..........
25000 records sent, 5000.0 records/sec (9.54 MB/sec), 0.7 ms avg latency, 26.0 ms max latency.
25010 records sent, 5001.0 records/sec (9.54 MB/sec), 0.7 ms avg latency, 24.0 ms max latency.
10000000 records sent, 4999.900002 records/sec (9.54 MB/sec), 0.83 ms avg latency, 532.00 ms max latency, 1 ms 50th, 1 ms 95th, 4 ms 99th, 65 ms 99.9th.

从结果可以看到，每秒还是 9.54MB 大小的消息，平均时延 0.83ms，最大时延是 532ms。

来做一个图比对一下：

从这个图就明显看出生产的消息越少，平均响应时间越长。可见顺序写得越多，那每次写的平均时间就会越小，所以 Kafka 在大数据量的读写中会表现得非常好。

总结

严格来说，这一篇文章是为了告诉你一个逻辑，那就是对一个组件不了解的时候，如何用你的基础技术知识把对组件的性能优化方向整理出来，以及如何通过自己的基础知识来做一个非常合理的分析。

这个逻辑就是：

先了解这个组件的基本知识：包括架构、实现原理等信息。

再整理出这个组件的配置参数。

找到合适的全局监控工具。

做压力测试时给出明显的判断。

这是个大体的逻辑，当然这个逻辑还有一个前提，那就是你得有相应的基础知识，在 Kafka 的这个分析中，要有操作系统和 Java 的基础知识，在实操中还需要多找几个不懂的组件做些练习才能理解这个逻辑的真谛。

就我自己来说，我会找一个完全没有接触过的组件，从安装部署开始直到性能测试、瓶颈判断、优化分析，看看需要多长时间，我才能理解得了这个组件。

这种思维方式，给了我很多的安全感，就是遇到了没接触过的内容，也不至心慌气短。

思考题

最后给你留两道思考题吧，你觉得如何分析一个未知组件呢？Kafka 的分析逻辑又是什么？

欢迎你用自己的理解思考一下这两个问题，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

25丨SkyWalking：性能监控工具之链路级监控及常用计数器解析

在微服务横行的年代，没有链路级监控简直就是灾难。技术在不断的发展过程中，总是会有新的工具被推出来，它们存在的价值就是解决问题。链路监控工具存在的价值就是尽快找到微服务中哪一个环节是最慢的。

我们对微服务的态度究竟是怎样的？

但是在讲链路级监控之前，我需要再说一下我们对微服务的态度。

我们认为微服务性能好，具有良好的扩展性，于是很多企业会把自己的架构改为微服务，但是呢，在不需要使用新技术的系统中，盲目地迷信和使用市场上被推崇的新技术，只会带来问题。

在前面我举过一个例子，我见过一个物流项目，开发团队认为拆分服务是可以让性能提高的一个重要技术思路，于是他们不断地细分项目。在不到百万级的车辆体量的项目阶段中，一个物流系统已经被拆分成了三四十个子项目，最后开发团队和运维团队都不堪重负，因为维护的成本变高了。于是，开发团队开始反省，认为应该合并一些子项目，最后就形成了中台项目。

这不是玩笑，而是一个真实的案例。

至于项目能不能拆分成多个微服务，你需要对整个项目的容量、硬件的体量进行评估。如果评估完觉得不需要，那么尽量不要干这种为了炫耀技术，而害人害已害公司的事情。如果确实有需要，那没什么说的，该拆就拆。

但是在拆的过程也一定要知道，对于多个微服务来说，相关的支持手段（比如说服务健康状态、链路跟踪、日志收集分析系统等）一定要跟上，如果跟不上，那就是灾难了。

链路级监控对微服务的重要作用

在微服务架构的系统中，没有好的链路监控工具，对性能分析来说就是灾难，因为体力活实在是太多了。这就是链路级监控在微服务中存在的价值了。

特别是现在 Kubernetes+Docker 的基础架构也在不断地被应用。微服务往里面一放，有问题想查都不知道从哪下手。因为容器出现问题被重启之后，可能你都不知道问题在哪个 node 上，只能通过 Kubernetes 先查在哪个 node 上，再查相应的日志等信息，这样就增加了很多工作量。

说回我们的链路监控，在一个合理的微服务架构中，一开始的架构规划就应该包括链路监控。可以用开源的，可以用商业的，也可以自己编写度量器做埋点。总之不管怎么实现，只要实现了就好。

常见的链路监控工具，我们都称之为 APM（Application Performance Management 或者 Application Performance Monitoring），而这些 APM 工具都是参考 2010 年 Google 的一个论文来做的。这篇有名的论文就叫 Dapper, a Large-Scale Distributed Systems Tracing Infrastructure，点击原文链接就能看到。在Github上有人翻译成中文，有兴趣的话，你可以去看看。

在商业市场当然有很多 APM 工具可以使用了。在这里，我就不给它们打广告了。重点推荐几个开源的好用的工具，它们分别是 Pinpoint、SkyWalking、Zipkin、CAT。

我们简单比对一下：

每个产品都有自己的特点，产品之间也有细微的差别。网络上也有人对这几个工具做过测试比对，得到的结论是每个产品对性能的影响都在 10% 以下，其中 SkyWalking 对性能的影响最小。

在今天的内容里，我之所以用 SkyWalking 说明链路监控，原因只有一个：中国开发者贡献的，必须得支持！

SkyWalking 的链路关系

为了说明链路的调用逻辑，我这里准备了几个非常简单的 Spring Cloud 应用。在这个示例中，我有四个服务。这四个服务会有相互的调用关系。如下所示：

其中用户可以直接访问 Project A、B、C、D 中的任何一个服务。同时这些服务器都会注册到 Eureka 中去，它们也会分别连 Redis、RabbitMQ、MySQL 等服务。

我们来看看 SkyWalking 是怎么体现链路关系的。

启动微服务 Project A

首先我们启动 Project A。为了让你能清晰地看到配置，我直接把 Agent 放到执行的命令行中。如下所示：

nohup java -javaagent:/data/pa/agent/skywalking-agent.jar -jar -Xms128m -Xmx128m /data/pb/target/pa.jar --spring.profiles.active=prod &

接着我们访问 Project A，通过 Postman 来访问这个示例提供的接口：

然后我们再来查看 SkyWalking 首页：

最后我们查看拓扑图：

从上面这张拓扑图中可以看到，当我访问了 pa 之后，这个 pa 链接了另外两个系统，一个是 Eureka，另一个是 MySQL。

加了 Agent 之后，至少我们可以看到这个服务现在在访问谁了。但是因为只有一个服务，所以还看不出来链路在哪里，现在我们启动 Project B。

启动微服务 Project B

首先启动 Project B：

nohup java -javaagent:/data/pb/agent/skywalking-agent.jar -jar -Xms128m -Xmx128m /data/pb/target/pb.jar --spring.profiles.active=prod &

接着查看 SkyWalking 的拓扑图：

你可以看到 pb（即 Project B）服务上有一个有一个问号，因为它还没有被人访问过。不过已经连上了 Eureka，所以和 Eureka 之间有条线。

然后我们使用 Postman 访问 Project B：

请注意，这里我直接访问了 Project B，并没有通过 Project A 访问。

然后我们再来查看一下拓扑图：

现在可以看到上图中 User 和 pb（即 Project B）之间有连线了吧。

这还只是单独的访问逻辑。

通过 Project A 访问 Project B

首先我们使用 Postman 访问 pab 接口：

注意，这里我通过 Project A 访问了 Project B。

我们再来看拓扑图：

看到了吧，pa（即 Project A）和 pb（即 Project B）之间有连接线了。这就是 SkyWalking 的 trace 功能了。

下面我将 Project C、Project D 都启动，也都串行访问一下，即 pa-pb-pc-pd。

我们通过拓扑图看下时间消耗，启动其他微服务并查看各服务响应时间。

首先，pa-pb-pc-pd 串行访问：

然后我们查看一下链路拓扑图：

这样，我们就看到了 pa-pb-pc-pd 的一条完整的链路。因为我们在前面直接访问了 pb，所以可以看到 user 到 pb 之间也有连线。

当然 SkyWaling 的功能不止是拓扑图。我们来看一个拓扑图的全部界面。

从上图就可以看到，当你点击两个服务之间连线上的那个蓝色点的时候，在右边会显示一个状态曲线。在这个状态曲线中，我们可以看到这两个服务之间的平均吞吐量（cpm）和平均响应时间（ms）。这样，我们就知道在这个链路上哪里消耗了时间。

现在，让我们来点一下看看。

通过这四个图，就能看到在每一个服务节点上，响应时间消耗了多少。

通过上面的时间，显然也可以算出每个服务自己消耗了多少时间。用前面的时间减去后面的时间，就可以算出来，很简单吧。

上面的 User 是通过公网访问的，所以这里的时间也包括了网络的时间，不是说 ProjectA 上就会消耗 600 多 ms。

追踪请求

从上面的图中，我们可以看到服务之间的整体性能表现，但是一个具体的请求节点是哪些，并体现不出来。SkyWalking 提供了追踪功能，可以看到一个请求被拆分成了哪些子请求。如下所示：

通过追踪页面，我们可以看到请求都已经按照响应时间的高低排好序了，点击就可以看到这个请求的拆分以及拆分的每个子请求所消耗的时间。

此外它还提供了树结构。

这样一来，调用过程清晰明了。

下面我们就压起来，通过 SkyWalking 来判断下性能瓶颈，以确定它在性能测试和分析中的作用。

这个场景是用 JMeter 来压 pabcd 接口，以此查看 SkyWalking 可以给我们一个什么样的结果。

JMeter 结果如下：

[root@7dgroup2 script]# /home/gaolou/apache-jmeter-5.1.1/bin/jmeter -n -t pabcddemo.jmx
Creating summariser <summary>
Created the tree successfully using pabcddemo.jmx
Starting the test @ Wed Feb 12 02:21:47 CST 2020 (1581445307090)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary +    165 in 00:00:12 =   13.8/s Avg:    69 Min:    36 Max:   153 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
summary +    687 in 00:00:30 =   22.9/s Avg:    59 Min:    26 Max:   206 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary =    852 in 00:00:42 =   20.3/s Avg:    61 Min:    26 Max:   206 Err:     0 (0.00%)
summary +   1182 in 00:00:30 =   39.4/s Avg:    60 Min:    24 Max:   240 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =   2034 in 00:01:12 =   28.3/s Avg:    60 Min:    24 Max:   240 Err:     0 (0.00%)
summary +   1717 in 00:00:30 =   57.3/s Avg:    58 Min:    18 Max:   238 Err:     0 (0.00%) Active: 4 Started: 4 Finished: 0
summary =   3751 in 00:01:42 =   36.8/s Avg:    59 Min:    18 Max:   240 Err:     0 (0.00%)
summary +   2148 in 00:00:30 =   71.7/s Avg:    60 Min:    13 Max:   509 Err:     0 (0.00%) Active: 5 Started: 5 Finished: 0
summary =   5899 in 00:02:12 =   44.7/s Avg:    60 Min:    13 Max:   509 Err:     0 (0.00%)
summary +   2105 in 00:00:30 =   70.1/s Avg:    76 Min:    16 Max:   507 Err:     0 (0.00%) Active: 6 Started: 6 Finished: 0
summary =   8004 in 00:02:42 =   49.4/s Avg:    64 Min:    13 Max:   509 Err:     0 (0.00%)
summary +   2090 in 00:00:30 =   69.6/s Avg:    91 Min:    15 Max:   507 Err:     0 (0.00%) Active: 7 Started: 7 Finished: 0
summary =  10094 in 00:03:12 =   52.6/s Avg:    70 Min:    13 Max:   509 Err:     0 (0.00%)
summary +   2093 in 00:00:30 =   69.8/s Avg:   105 Min:    17 Max:   987 Err:     0 (0.00%) Active: 8 Started: 8 Finished: 0
summary =  12187 in 00:03:42 =   54.9/s Avg:    76 Min:    13 Max:   987 Err:     0 (0.00%)
summary +   2080 in 00:00:30 =   69.4/s Avg:   120 Min:    10 Max:  1135 Err:     0 (0.00%) Active: 9 Started: 9 Finished: 0
summary =  14267 in 00:04:12 =   56.6/s Avg:    82 Min:    10 Max:  1135 Err:     0 (0.00%)
summary +   2074 in 00:00:30 =   69.1/s Avg:   136 Min:    11 Max:   961 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  16341 in 00:04:42 =   58.0/s Avg:    89 Min:    10 Max:  1135 Err:     0 (0.00%)
summary +   2057 in 00:00:30 =   68.6/s Avg:   144 Min:     9 Max:  1237 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  18398 in 00:05:12 =   59.0/s Avg:    95 Min:     9 Max:  1237 Err:     0 (0.00%)
summary +   2063 in 00:00:30 =   68.7/s Avg:   145 Min:     9 Max:  1123 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  20461 in 00:05:42 =   59.8/s Avg:   100 Min:     9 Max:  1237 Err:     0 (0.00%)
summary +   2058 in 00:00:30 =   68.6/s Avg:   145 Min:     8 Max:  1113 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =  22519 in 00:06:12 =   60.5/s Avg:   104 Min:     8 Max:  1237 Err:     0 (0.00%)
..................

为了不占篇幅，这里我把后面的部分截掉了，因为后面的 10 个线程的数据和上面的 10 个线程的数据差不多。

从 JMeter 的结果中可以看到，当线程增加的时候，确实响应时间也在不断增加。下面我们来看一下 SkyWalking 中的健康状态。

Service Dashboard：

在 Service DashBoard 中，你可以看到热力图，也就是在哪个时间段微服务比较繁忙，也可以看到微服务吞吐量的排序和慢端点的排序。

通过这个图，我们可以很快定位到哪个服务的哪个接口慢。图中直接就给出来了。

在 Service DashBoard 中，你还可以选择当前服务、当前端点和当前实例。

它们之间的关系是什么呢？实例就是一个具体的进程，服务是微服务的名字，至于端点，那就是接口了。你可以在多个主机上执行同样的微服务，那么每个主机上启动的就是具体的实例，有着共同的功能。

Database Dashboard

在 Database Dashboard 中，你可以看到数据库的处理能力——数据库的吞吐能力、SQL 的执行时间百分比，以及哪些 SQL 慢。

通过这个图，我们可以直接判断出，现在数据库服务在压力下的处理能力。比如说我的这个示例：明显的就是压力上来了之后，数据库的 SQL 执行时间变长了。

在左下角的图中可以看到，90% 的 SQL 在 250ms 左右，99% 的 SQL 在压力下处于 500ms 以下。

这就是明显要调优的 SQL，因为一个 select，还是精确查找，怎么可能这么慢。

在右下角可以直接看到哪个 SQL 慢，这就可以用到前面文章中提到的数据库剖析功能了。

总结

对微服务来说，链路监控工具是标配。在性能分析中，需要查看微服务的性能状态时必须用到链路监控工具。

我们用 APM 工具要实现的就是以下四点：

查看微服务节点的健康状态。

判断响应时间的消耗点。

通过我们前文中提到的定向监控手段进行详细地问题定位，细化到组件的配置、代码行和 SQL 层级。

最后根据定位的根本原因，提出具体的性能瓶颈解决方案。

从上面的步骤就可以看出，从性能瓶颈的判断逻辑上，APM 工具给我们提供了很多便利。但是，APM 工具也不能告诉你性能瓶颈的根本原因，因此还是需要定向分析来做细化。这也是我一直推崇的，创建性能瓶颈分析决策树，要有完整的逻辑思路的原因。

一个小声明

后续的一篇文章原计划是《性能分析之 CPU 分析：从 CPU 调用高到具体代码行》。可是想来想去，这里面的内容在整个专栏中的多篇文章中均有涉及，你可以参考《CentOS：操作系统级监控及常用计数器解析》《Java & C++：代码级监控及常用计数器解析》这两篇文章。

至于分析决策树，我在《倾囊相授：我毕生所学的性能分析思路都在这里了》中也都已经提到。

基于此，我认为没必要单独再写一篇文章凑篇幅，所以后面将跳过这一篇，请知悉。

思考题

了解了今天的内容后，你可以说一下 APM 工具可以为我们提供哪些分析便利吗？SkyWalking 有哪些具体的功能点可以帮助我们进行性能分析呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

26丨案例：手把手带你理解TPS趋势分析

在性能分析中，前端的性能工具，我们只需要关注几条曲线就够了：TPS、响应时间和错误率。这是我经常强调的。

但是关注 TPS 到底应该关注什么内容，如何判断趋势，判断了趋势之后，又该如何做出调整，调整之后如何定位原因，这才是我们关注 TPS 的一系列动作。

今天，我们就通过一个实际的案例来解析什么叫 TPS 的趋势分析。

案例描述

这是一个案例，用一个 2C4G 的 Docker 容器做服务器。结构简单至极，如下所示：

当用个人电脑（上图中压力工具 1）测试云端服务器时，达到 200 多 TPS。但是当用云端同网段压力机（上图中压力工具 2）测试时，TPS 只有 30 多，并且内网压力机资源比本地压力机要高出很多，服务器资源也没有用完。

在这样的问题面前，我通常都会有一堆的问题要问。

现象是什么？

脚本是什么？

数据是什么？

架构是什么？

用了哪些监控工具？

看了哪些计数器？

在分析之前，这些问题都是需要收集的信息，而实际上在分析的过程中，我们会发现各种数据的缺失，特别是远程分析的时候，对方总是不知道应该给出什么数据。

我们针对这个案例实际说明一下。

这个案例的现象是 TPS 低，资源用不上。

下面是一个 RPC 脚本的主要代码部分。

public SampleResult runTest(JavaSamplerContext arg0) {
    // 定义results为SampleResult类
    SampleResult results = new SampleResult();
    // 定义url、主机、端口
    String url = arg0.getParameter("url");
    String host = arg0.getParameter("host");
    int port = Integer.parseInt(arg0.getParameter("port"));
    results.sampleStart();

    try {
        message=detaildata_client.detaildata_test(url);// 访问URL并将结果保存在message中
        System.out.println(message); //打印message，注意这里
        results.setResponseData("返回值："+ message, "utf-8");
        results.setDataType(SampleResult.TEXT);
        results.setSuccessful(true);
    } catch (Throwable e) {
        results.setSuccessful(false);
        e.printStackTrace();
    } finally {
        String temp_results=results.getResponseDataAsString();
        results.setResponseData("请求值："+arg0.getParameter("url")+"\n"+"返回值:"+temp_results, "utf-8");
        results.sampleEnd();
    }

    return results;

JMeter 脚本关键部分：

<stringProp name="ThreadGroup.num_threads">100</stringProp>
//我们来看这里，ramp_time只有1秒，意味着线程是在1秒内启动的，这种场景基本上都和真实的生产场景不相符。
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
...............
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="CSV Data File Config" enabled="true">
  <stringProp name="delimiter">,</stringProp>
  <stringProp name="fileEncoding">utf-8</stringProp>
  <stringProp name="filename">filename</stringProp>
  <boolProp name="ignoreFirstLine">false</boolProp>
  <boolProp name="quotedData">false</boolProp>
  <boolProp name="recycle">true</boolProp>
  <stringProp name="shareMode">shareMode.all</stringProp>
  <boolProp name="stopThread">false</boolProp>
  <stringProp name="variableNames">url</stringProp>
</CSVDataSet>

在这个脚本中，逻辑非常简单，一个 RPC 接口：1. 发出请求；2. 返回响应；3. 打印返回信息。

本机跑出来的结果如下：

在这个案例中，参数化数据就是根据真实的业务量来计算的，这个可以肯定没有问题。

那么架构呢？在最上面的图中已经有了部署的说明。在逻辑实现上，也就是一个很简单的服务端，内部并没有复杂的逻辑。所用到的监控工具是 top、Vmstat。

看了哪些计数器呢？CPU、内存、I/O 等。

下面我们开始分析。

第一阶段

对公网上的测试来说，基本上压力都会在网络上，因为出入口带宽会成为瓶颈，所以先要看一眼自己的带宽用到了多少，再比对一下出口路由上的带宽。

这里 1Gbps 只用到了 0.01%，也就是 (1000/8)x0.01%=12.5k（这里是将带宽 bit 换成 byte 计算）。

在这样的带宽使用率之下，即使是公网也不见得会有问题，更别说在内网了。可见带宽不是瓶颈点。

既然这样，我们直接在内网里来做分析，看原因是什么。

但是我们要以什么样的场景来跑呢？因为带宽现在看到用得并不多，但 TPS 也上不去。首先应该想到的场景就是把 TPS 曲线给做出梯度来。

为什么要这么做？最重要的就是要知道到底 TPS 在多少压力线程下会达到最大值，也就是我在各种场合经常强调的一个场景，最大 TPS 场景。关于这种曲线，我们不需要性能指标应该就可以做得出来。如下图所示：

在一个既定场景、既定数据、既定环境的压力场景中，我们一定要拿到这样趋势的 TPS 和 RT 曲线。其中绿色和红色的点都是不需要业务指标来限定的，而是通过压力场景中观察 TPS 趋势线来确定。

我来解读一下这个趋势图：

响应时间一定是从低到高慢慢增加的；

TPS 一定也是从低到高慢慢增加的，并且在前面的梯度中，可以和线程数保持正比关联。举例来说，如果 1 个线程 TPS 是 10，那 2 个线程的 TPS 要在 20。依次类推。

而在这个例子中，前面有提到 100 线程 1 秒加载完，这样的比例完全看不出来梯度在哪，所以，改为 100 秒加载 100 个线程，再来看看梯度。

测试结果如下：

从这个结果可以看出几点：

1.TPS 一点梯度没看出来。为什么说没有看出来呢？这里我发一个有明显梯度的 TPS 曲线出来以备参考（这张图不是本实例中的，只用来做分析比对）：

2. 响应时间增加的太快了，明显不符合前面我们说的那个判断逻辑。那什么才是我们判断的逻辑呢？这里我发一个有明显梯度的出来以备参考（这张图不是本实例中的，只用来做分析比对）：

粒度太粗，对一个 duration 只有五分钟的场景来说，这样的粒度完全看不出过程中产生的毛刺。

至少看到内网的 TPS 能到 180 了，但是这里没有做过其他改变，只是把 Ramp-up 放缓了一些，所以我觉得这个案例的信息是有问题的。

第二阶段

针对以上的问题，下面要怎么玩？我们列一下要做的事情。

将 Ramp-up 再放缓，改为 300 秒。这一步是为了将梯度展示出来。

将粒度改小，JMeter 默认是 60 秒，这里改为 1 秒。这一步是为了将毛刺显示出来。强调一点，如果不是调优过程，而是为了出结果报告的话，粒度可以设置大一些。至于应该设置为多大，完全取决于目标。

接着我们再执行一遍，看看测试结果：

这样看下来，有点意思了哈。明显可以看到如下几个信息了。

响应时间随线程数的增加而增加了。

TPS 的梯度还是没有出来。

显然还是没有达到我们说的梯度的样子。但是这里我们可以看到一个明显的信息，线程梯度已经算是比较缓的了，为什么响应时间还是增加得那么快？

这里的服务器端压力情况呢？如下所示：

从监控图大概看一下，服务端 CPU、内存、网络几乎都没用到多少，有一种压力没有到服务端的感觉。

在这一步要注意，压力在哪里，一定要做出明确的判断。

在这里，当我们感觉服务端没有压力的时候，一定要同时查看下网络连接和吞吐量、队列、防火墙等等信息。查看队列是非常有效的判断阻塞在哪一方的方式。

如果服务端的 send-Q 积压，那就要查一下压力端了。如下所示：

State       Recv-Q Send-Q                      Local Address:Port                                     Peer Address:Port
......
LISTEN      0    54656                                    :::10001                                               :::*
......

在网络问题的判断中，我们一定要明确知道到底在哪一段消耗时间。我们来看一下发送数据的过程：

从上图可以看出，发送数据是先放到tcp_wmem缓存中，然后通过tcp_transmit_skb()放到TX Queue 中，然后通过网卡的环形缓冲区发出去。而我们看到的 send-Q 就是 Tx 队列了。

查看压力端脚本，发现一个问题。

System.out.println(message);

一般情况下，我们在调试脚本的时候会打印日志，因为要看到请求和响应都是什么内容。但是压力过程中，基本上我们都会把日志关掉。一定要记住这一点，不管是什么压力工具，都要在压力测试中把日志关掉，不然 TPS 会受到很严重的影响。

了解 JMeter 工具的都知道 -n 参数是命令行执行，并且不打印详细的返回信息的。但是这里，一直在打印日志，并且这个日志在 JMeter 中执行时加了 -n 参数也是没用的。

这样一来，时间全耗在打印日志中了。知道这里就好办了。我们在这里做两件事：

把打印日志这一行代码注释掉，再执行一遍。

把 ramp-up 时间再增加到 600 秒。

为什么我要执着于把 ramp-up 时间不断增加？在前面也有强调，就是要知道 TPS 和响应时间曲线的趋势。

在性能分析的过程中，我发现有很多性能工程师都是看平均值、最大值、最小值等等这些数据，并且也只是描述这样的数据，对曲线的趋势一点也不敏感。这是完全错误的思路，请注意，做性能分析一定要分析曲线的趋势，通过趋势的合理性来判断下一步要做的事情。

什么叫对曲线的趋势敏感？就是要对趋势做出判断，并且要控制曲线的趋势。

有时，我们经常会看到 TPS 特别混乱的曲线，像前面发的 TPS 图一样，抖动幅度非常大，这种情况就是完全不合理的，在遇到这种情况时，一定要记得降低压力线程。

你可能会问，降到多少呢？这里会有一个判断的标准，就是一直降到 TPS 符合我们前面看到的那个示意图为止。

再给你一个经验，如果实在不知道降多少，就从一个线程开始递增，直到把梯度趋势展示出来。

第三阶段

通过注释掉打印日志的代码，可以得到如下结果：

从TPS 曲线上可以看到，梯度已经明显出来了。在有一个用户的时候，一秒就能达到 1000 多 TPS，并且在持续上升；两个线程时达到 2500 以上，并且也是在持续上升的。

从响应时间上来看，也是符合这个趋势的，前面都在 1ms 以下，后面慢慢变长。

压力越大，曲线的毛刺就会越多，所以在 TPS 达到 6000 以上后，后面的 TPS 在每增加一个线程，都会出现强烈的抖动。

在这种情况下，我们再往下做，有两条路要走，当然这取决于我们的目标是什么。

接着加压，看系统什么时候崩溃。做这件事情的目标是找到系统的崩溃点，在以后避免出现。

将线程最大值设置为 10，增加 ramp up 的时间，来看一下更明确的递增梯度，同时分析在线程增加过程中，系统资源分配对 TPS 的影响，以确定线上应该做相对应的配置。

总结

在这个案例中，我们将 TPS 从 150 多调到 6000 以上，就因为一句日志代码。

我分析过非常多的性能案例，到最后发现，很多情况下都是由各种简单的因素导致的，这一反差也会经常让人为这一路分析的艰辛不值得。

但我要说的是，性能分析就是这样，当你不知道问题在哪里的时候，有一个思路可以引导着你走向最终的原因，那才是最重要的。

我希望通过本文可以让你领悟到，趋势这个词对曲线分析的重要性。在本文中，我们通过对曲线的不合理性做出判断，你需要记住以下三点：

性能分析中，TPS 和响应时间的曲线是要有明显的合逻辑的趋势的。如果不是，则要降线程，增加 Ramp-up 来让 TPS 趋于平稳。

我们要对曲线的趋势敏感，响应时间的增加不可以过于陡峭，TPS 的增幅在一开始要和线程数对应。

当 TPS 和响应时间曲线抖动过于强烈，要想办法让曲线平稳下来，进而分析根本原因，才能给出线上的建议配置。

思考题

今天我结合案例具体说明了下如何分析 TPS 的趋势，如果你吸收了文章的内容，不妨思考一下这两个问题？

Ramp-up 配置有什么样的作用？

为什么说压力工具中 TPS 和响应时间曲线抖动过大会不易于分析？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起学习交流一下。

27丨案例：带宽消耗以及Swap（上）

今天我们来看一个真实的案例。事情是这样的，之前有人在微信上问我一个问题，这个问题的现象很典型：典型的 TPS 上不去，响应时间增加，资源用不上。

大概的情况是这样的：有两台 4C8G 的服务器，一台服务器上有 2 个 Tomcat，一台服务器上是 DB。压测的混合场景有 4 个功能模块，其中 3 个访问一个 Tomcat，另外一个访问一个 Tomcat。

Tomcat 的监控页面如下：

应用服务器系统资源监控页面如下：

数据库服务器系统资源监控如下：

JMeter 结果如下：

综上现象就是，单业务场景执行起来并不慢，但是一混合起来就很慢，应用服务器和数据库服务器的系统资源使用率并不高。请问慢在哪？

这是非常典型的询问性能问题的方式，虽然多给了系统资源信息，但是这些信息也不足以说明瓶颈在哪。

为什么呢？在现在多如牛毛的监控工具中，除非我们在系统中提前做好分析算法或警告，否则不会有监控工具主动告诉你， 监控出的某个数据有问题，而这个只能靠做性能分析的人来判断。

我在很多场合听一些“专家”说：性能分析判断要遵守木桶原理。但是在做具体技术分析的时候，又不给人说明白木桶的短板在哪里。这就好像，一个赛车手说要是有一个各方面都好的车，我肯定能得第一名，但是，我没有车。

话说出来轻而易举，但是请问木桶的短板怎么判断呢？因为 CPU 高，所以 CPU 就是短板吗？所以就要加 CPU 吗？这肯定是不对的。

因为这个例子并不大，所以可以细细地写下去。今天文章的目的就是要告诉你，性能问题分析到底应该是个什么分析思路。

分析的第一阶段

画架构图

做性能测试时，我们需要先画一个架构图，虽然简单，但是让自己脑子里时时记得架构图，是非常有必要的。因为架构级的分析，就是要胸怀架构，在看到一个问题点时，可以从架构图中立即反应出来问题的相关性。

上面这张图是自己脑子里的逻辑图，数据在网络中的流转并不是这样，而是像下图这样。

数据流是从压力机到应用服务器，应用服务器再到网络设备，再到数据库服务器；数据库把数据返回给应用服务器，应用服务器再通过网络设备给压力机。

如果把里面的 Redis、ActiveMQ 和 MySQL 的逻辑再细说明白，那么这个小小的应用都可以描述好一会。所以这里，我先大概描述一下，如果后面的分析中涉及到了相应的逻辑，再一点点加进来。

应用服务器只有一台，上面有两个 Tomcat 实例；数据库服务器有三个应用。混合场景中有四个业务，其中三个访问 Tomcat1，第四个访问 Tomcat2。

场景描述

有了场景大概的画像之后，我们再来看场景。根据测试工程师描述：

响应时间慢的，都是可视化页面，有不少图片、JS、CSS 等静态资源。公网上是用 CDN 的，现在只测试内网的部分。静态资源已经做了压缩。

单业务测试的容量是可以满足要求的，但混合场景响应时间就长。系统资源用得并不多。

压力场景是 300 线程，Ramp-up period 是 1 秒。

Duration 是 72000。

各参数化都已经做了，参数化数据也合理。

测试环境都是内网。

服务器是 CentOS，压力机是 Win10。

既然这样，我们还是要看看系统的各个资源，再来判断方向。我在很多场合都强调证据链。对架构比较简单的应用来说，我们都不用再做时间的拆分了，直接到各主机上看看资源就好了。

瓶颈分析定位

根据我们之前画的架构图，我们从应用服务器、数据库服务器和压力数据分别定位一下。

应用服务器

从前面的应用服务器资源来看，CPU 使用率现在还不高，也基本都在 us CPU（就是 user 消耗的 CPU）上，比例也比较合理。

物理内存 8G，还有 3.5G。即使不多，对 Java 应用来说，也要先看 JVM，只要不是 page fault 过多，我们可以先不用管物理内存。

网络资源接收 900KB/s 左右，发送 11M 左右。这样的带宽应该说是比较敏感的，因为对 100Mbps 和 1000Mbps 来说，我们要心里有一个数，一个是 12.5MB（对应 100Mbps），一个是 125MB（对应 1000Mbps），当和这样的带宽值接近的时候，就要考虑下是不是带宽导致的压力上不去。不过这里我们也不用先下定论。只是留个疑问在这里。

磁盘资源，基本上没有读写，很正常。

从 Process 列表中，也没看到什么异常数据。faults 也都是 min，major fault 并没有。但在这个案例中，还部署了另一个监控工具，上面显示如下：

为什么这个 Swapping 要标黄呢？那肯定是过大了嘛。是的，你可以觉得 swap 过多。这个扣，我们也记在心里。在这里标红加粗敲黑板！

数据库服务器

照样分析，CPU、内存、网络、磁盘、Process 列表，并没看到哪里有异常的数据，连网络都只有 500 多 k 的发送。

这样的数据库资源状态告诉我们，它不在我们的问题分析主线上。接下来是压力数据。

压力数据

这是 JMeter 中的聚合报告：

从上面这张图也能看出，响应时间确实挺长的，并且，300 线程只有 37 的 TPS，带宽总量 10M 左右。这个带宽倒是和应用服务器上的带宽使用量相当。关于这个带宽的判断请你一定注意，对于性能分析来说，带宽能不能对得上非常重要。比如，客户端接收了多少流量，服务端就应该是发出了多少流量。如果服务端发了很多包，但是客户端没有接收，那就是堵在队列上了。

既然其它的资源暂时没出现什么瓶颈。其实在这个时间里，如果是复杂的应用的话，我们最应该干的一件事情就是拆分时间。如下图所示：

这里我把时间拆为 t1-t5，具体分析哪一段为什么消耗了时间。我们可以在 Tomcat 中加上 %D 和 %F 两个参数来记录 request 和 response 的时间。

在没有做这个动作之前，我们先把前面的扣解决一下。首先，带宽是不是受了 100Mbps 的限制？

一般来说，判断网络的时候，我们会有几个判断点。

首先是带宽的流量大小，也就是前面我们看到的 11M 左右的值。一般来说，100Mbps 是指的 bit per second，但是在应用层基本上都是 byte，所以对 100Mbps 来说，是 12.5MB。

其次是，全连接和半连接队列是否已经溢出？

我们通过 SYNs to LISTEN sockets dropped 来判断半连接队列是否溢出，通过 times the listen queue of a socket overflowed 来判断全连接队列是否溢出。

通过实时的查看，这两个值的增加并不多。所以这里不会是问题点。

最后是发送和接收队列是否堆积？

通过应用服务器上的 send-Q（前面数第三列），可以看到服务器和压力机之间的的队列还是很长的，基本上每次查看都存在，这说明队列一直都有堆积。

我们再到压力机上看看带宽用了多少：

看这里也是用到了 93Mbps，那么到这里我们就可以确定是网络问题导致的 TPS 上不去，响应时间增加，系统资源也用不上了。

和系统管理员确认宿主机的带宽后，被告知宿主机确实是 100Mbps。

似乎这个分析到这里就可以结束了，直接把带宽加上再接着测试呗。但是，从项目实施的角度上说，这个问题，并不是阻塞性的。

为了把更多的性能问题提前找出来，现在我们先不下载静态资源，只发接口请求找下其他性能问题。这个带宽的问题，记一个 bug 就行了。

优化结果

我们将静态资源全都过滤掉之后，再次执行场景，结果是下面这样的。

JMeter 压力数据：

应用服务器带宽：

数据库服务器带宽：

应用服务器网络队列：

应用服务器资源监控：

通过上面的结果可以看出：

TPS 可以达到 221.5 了，并且 Received 和 Sent 的字节加一起不到 4MB。

应用服务器和数据库服务器的带宽都用到了近 40Mbps，和 JMeter 结果也相当。

应用服务器上的网络队列也没有堆积。

应用服务器的 CPU 也已经能消耗到 66% 了，

当正在想通过过滤掉静态资源绕过带宽不足的现状来测试其他性能问题的时候，这时，Swap 双向都标黄了。这时，性能测试工程师更纠结了，它为什么双向都黄了？CPU 使用率才 66% 嘛。

其实，这两句话之间并没有什么关系，CPU 使用率不管是多少，Swap 该黄还是会黄。

这是为什么呢？这里卖个关子，在下一篇文章中，我们接着分析。

总结

带宽问题是性能分析中常见的问题之一，其难点就在于，带宽不像 CPU 使用率那么清晰可理解，它和 TCP/IP 协议的很多细节有关，像三次握手，四次挥手，队列长度，网络抖动、丢包、延时等等，都会影响性能，关键是这些判断点还不在同一个界面中，所以需要做分析的人有非常明确的分析思路才可以做得到。而且现在的监控分析工具中，对网络的判断也是非常薄弱的。

而 Swap 问题不能算是常见，只要出现，基本上就会很多人晕乎。解决的关键就是要明白 Swap 的原理，查到关联参数，然后就可以很快地定位了。

思考题

结合今天的内容，你能说一下网络的瓶颈如何判断吗？有哪几个队列？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

28丨案例：带宽消耗以及Swap（下）

上一篇文章我主要分析了带宽消耗，今天，我们来看一下分析的第二和第三阶段，也就是 Swap 分析和数据库分析。

分析的第二阶段

Swap 的原理和对 TPS 的影响

前面有一个扣，是说 swap 多的问题。要理解 swap 为什么是黄的，得先知道什么是 swap。我先画个简易的示意图。

这里先解释一下，对于一个 Linux 系统来说，如果配置并开启了 swap 分区，那么默认的 swappiness 参数是 60。

当上图中已用内存超过 40%（100%-60%）时，系统就会主动切换 swap 和 active 内存中 file 类型的比例。

swappiness 是在内存 reclaim 的时候生效的，而 reclaim 方式同时有两个动作：1. 将 file 相关内存进行回收；2. 将 anon 内存交换到 swap 分区。

所以 swapiness 值越大，swap 分区就用得越多。

对我们现在分析的这个系统来说，来看一下：

我们看到这里配置了一个内存为 8G 左右，已经使用了 7G 多了，swappiness 配置为 30%。也就是说当内存使用超过 8Gx(100%-30%)=5.6G 时，就会使用 swap 分区。

通过 free 看到现在只有 145M 的物理内存剩余，可用内存也只有 254M 了，也就是说现在只有 145（剩余物理内存大小）/7821（总物理内存大小）≈1.9% 的空闲内存，这个比例已经远远小于 swappiness 的 30% 了。

也就是说这系统早就开始用 swap 了。所以上面图中的 swap 飘黄也是很合理的喽！

下面我们就针对应用服务器的 swap 来看是不是可优化。

所有人都知道，当 swap 被用的时候，性能肯定会下降，所以在我的测试过程中，一般我都建议把 swap 直接关掉测试性能，有人说这样有什么问题？

那就是没有 swap，让不常用的对象直接占用物理内存，如果物理内存不够用，就把对象删了，后面再创建，这时会增加的是 major fault，那就增加好了，反正是要性能差的。

说得如此硬气，那在生产中怎么办呢？开还是关？有人觉得关了心里有安稳，有人觉得开着心里会安稳。而一个系统、一个容器、一个节点，如果容量控制的非常好的情况下，我建议关掉。开着它，也只是心里上的安慰，不会有 TPS 处理能力上的提升。

瓶颈分析定位

既然知道了上面的大概原理。对一个运行 Tomcat 应用的服务器来说，那肯定是要先检查一下 JVM 设置为多大。先执行 ps 命令，看一下 Java 进程吧。

关键参数如下：

JAVA_OPTS="$JAVA_OPTS -server -Xms2048M -Xmx8192m -XX:PermSize=256M -XX:MaxNewSize=2790m -XX:MaxPermSize=512m -XX:SurvivorRatio=8"

JVM 是 1.8.0_65。

这个参数配置有很大的问题。物理内存只有 8 个 G，一个 JVM heap 就配置了 8G，这让其他的东西怎么玩得起来？并且 JDK 是 1.8 了，配置 permsize 是又为啥呢？

虽说有多个地方配置不合理，但是我们也得要知道一下应该配置多少是合理的吧。

看参数的时候，JMX 也配置上了，那就用工具来看吧。

首先来看一下系统资源。先看一下系统资源在压力下的表现：

队列已经出现，CS 2 万多，in 2 万多，说多不多。我们可以先放着。

I/O 没什么压力，swap 也一直有值，我们要解决的就是它。

us：sy 接近 2：1，这个是不良信号，记在心里，后面再说。

其次再看下 JVM 的情况：

CPU 使用在应用上的时间达到 60%，GC 上没耗什么时间，并且从堆的回收能力上来看，比较正常，只是只用到了 3G 左右，这里有必要给 8G 吗？

线程活动的达到 347，看起来还是在增加的，这里也可能是个问题点，只是现在我们不用关心，它还没跳出来。

从这个 JVM 状态上来看，它完全用不到 8G。在这种状态下，还有另一个 Tomat，并且另一个 Tomcat 中也没有配置 -Xmx -Xms 参数，当没有配置时，默认 -Xmx 是物理内存的 1/4。再加上 thread 用的，物理内存很快就会消耗到 5.6G，所以 swap 飘黄也是吻合的。

优化结果

首先，我们把 JVM 配置成最简，JVM 设置为 4G。

JAVA_OPTS="$JAVA_OPTS -server -Xms4096M -Xmx4096m"

perm 区在 1.8 里都没有了，这几个参数也没啥用。在我的习惯中，MaxNewSize 也是先看要用到多少，再决定配置不配置。有些应用自己不熟悉，也无法直接给出配置，只有测试之后再配置。

各部分配置为多大，都没有定数，要通过测试看需要多少。

而我们现在最重要的是先把性能调整上去，再考虑这些细节内容。这样修改 JVM 就是为了把物理内存使用率低下来，先不修改 swapiness 的比例是为了看下结果，如果用不到 swap 就不再调了，如果还是用了 swap，再来调它。

当我们把 JVM 修改了之后，再执行起来场景。看到内容如下：

CPU 使用率相对前面没有什么变化，但是堆 4G 只用到了 1.5G，可见这个堆连 4G 都用不到。当然我们还是要分析下其他的内容。

还记得我们要解决的是什么问题吧？swap 飘黄了！

从这张图可以看到 Swapping 不报警了！CPU 占用 70% 左右。说明现在 available 的内存是充足的。

这时我们再看一下系统资源，首先是应用服务器系统资源。

应用服务器系统资源 vmstat 如下：

应用服务器系统资源 top：

应用服务器系统资源 iftop:

上图中可以看到，对比之前的资源，swap 基本上没有了，CPU 使用率多起来了。但是队列依旧长，sy CPU 消耗还是有点多了。

应用服务器的 si 已经到了 13.1% 了，这个值要关注下，暂时还不能说是问题，但是接着增加下去，肯定会是问题。

网络已经超过 70Mbps 了，峰值上到 87Mbps，这是一个好事，它说明现在处理的业务量确定多了。

接下来是数据库服务器系统资源：

你可以看到数据库 CPU 都用到这么高了？

TPS 能到 259.2 了，较之前的 221.5 没有提升多少。但是我们解决了 swap 的问题，还是有了一点点的提升。

那下一个瓶颈在哪里呢？通过上面的数据库资源来看，数据库早就已经被用到了 100% 的 CPU，队列也嗖嗖地涨到了好几十，高的都超过 100 了。

可见我们在处理应用服务器的时候，数据库这边已经早就吃不消了。那下面，我们就先把应用服务器的优化部分放一下，再去分析下一个短板：数据库。

后续性能工作建议

但是这里并不是说应用服务器的优化工作就完成了，还有一些部分需要做的。

优化 JVM 配置参数，至于应该配置成什么值，还需要再测试，可能会有人说，这个测试人员怎么知道呢？请你相信，如果这个值性能测试人员都测试不出来的话，一般的架构师也不可能知道该设置为多少。

通过监控分析确定 swapiness 的值。

网络带宽又快到占满了，如果 TPS 再提高，网络肯定又支撑不了。

这些扣也都放在这里。因为我们主要是找到系统的短板，并一一解决，才能使整体的 TPS 增加，虽说现在应用服务器上还有优化的空间，但是现在它不是最短的板。

我们在不忘记应用服务器这些问题的同时，再将目光转向数据库。

分析的第三阶段

瓶颈分析定位

先来看看数据库的系统资源。

我在很多场合都在强调一个词：证据链。所以基本上分析也会是从 OS 层面开始。

但是证据链这个词说起来容易理解，实际上要想真的有链起来的能力，必须具有基础知识，像分析数据库就更明显。因为当我们不了解系统架构时，想说明一个事情就非常困难。

像上面的这个 top，显然 us CPU 使用率非常地高，idle 几乎没有了，只有一个 si 占了 5.7%，这个 si 并不算高，我们在上一阶段看到的应用服务器的 si 都已经达到了 13% 了。

我们说 si 的高或者低，倒不是关键，关键的是它有没有成为我们的瓶颈点。在这个系统中，us cpu 才是我们要关注的重点，因为它实在是太高了。

对于一个数据库来说，要干的事情就是执行 SQL。当分析多了数据库之后，基本上也形成了套路。不管怎么样，还是先看一下基本的监控信息，以下截取一些 Spotlight on MySQL 的有用的图，如果你没有这个工具，用其他的监控工具也是一样的。

从上面的图可以看到，CPU 使用率 99%，Query Cache 是 OFF 的。记下这个位置！

从上图看到，负载队列非常长，但 Disk I/O 没多少，说明队列和I/O 无关，只是 CPU 的队列，非常好！

Network 也不算大，进出每秒 5000 多个包，我们再来看一下网络用到多少了？

峰值也才 70Mbps 左右，即使是 100Mbps 带宽，现在仍然认为有余量（注意！我这里说有余量是因为我同时也检查了网络队列，并没有阻塞，并不是只看了这个值就武断地做了判断）。

通过上面的图可以看到，每秒执行 2500-3000 的 SQL，Sorts per second 达到 800-1000，Sort rows per second 达到 8000-10000。

session 用得倒是也不多，但 Miss Rates 在压力过程中 Query Cache 都是在 100%，并且从最上面的 summary 中可以看到 Query Cache 也是 OFF 的。

为什么没有在看到 Query Cache 是 OFF 的时候就敲黑板呢，这是因为在一些应用中，如果不是查询多的话，这个值 OFF 也不能说有问题，但是在这个应用中几乎所有的语句都是 select，那这个 Query Cache 再不打开就说不过去了呀。这里先记录下这个问题，待会我们的优化动作就是打开 Query Cache。

不管怎么说，对一个数据库来说，主要是执行 SQL 嘛，而对 MySQL 来说，不看 slow log，还能看什么呢。

通过整理 slow log，看到如下内容：

# Overall: 280 total, 1 unique, 0.59 QPS, 9.53x concurrency ______________
# Time range: 2019-09-26T13:44:08 to 2019-09-26T13:52:06
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time          4555s     12s     19s     16s     18s      2s     16s
# Lock time           52ms   130us   662us   185us   273us    53us   167us
# Rows sent              0       0       0       0       0       0       0
# Rows examine      30.81M 112.69k 112.69k 112.69k 112.69k       0 112.69k
# Query size       186.48k     682     682     682     682       0     682

# Profile
# Rank Query ID           Response time    Calls R/Call  V/M   Item
# ==== ================== ================ ===== ======= ===== ===========
#    1 0xBED932B8C940697E 4555.0867 100.0%   280 16.2682  0.16 SELECT test2

什么情况？只有 1 unique？0.59TPS？我前面的 TPS 可是有 259.2，这结果一看就感觉不对。

查看一下 slow_launch_time，配置成了 10s，怪不得看不到慢 SQL。

改 slow_launch_time 为 1s，再跑一遍。看到如下结果：

# Overall: 620.47k total, 30 unique, 259.39 QPS, 16.76x concurrency ______
# Time range: 2019-09-26T13:44:08 to 2019-09-26T14:24:00
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time         40082s    88us     23s    65ms   323ms   649ms   332us
# Lock time            61s    30us   363ms    97us   152us     1ms    69us
# Rows sent          1.92M       0     633    3.25   21.45    7.43    0.99
# Rows examine     987.99M       0 112.69k   1.63k  10.29k   5.49k  420.77
# Query size       240.33M      89   1.52k  406.15  833.10  258.19  246.02

# Profile
# Rank Query ID           Response time    Calls  R/Call  V/M   Item
# ==== ================== ================ ====== ======= ===== ==========
#    1 0x160FA75270C56FB8 22892.5442 57.1%  65352  0.3503  0.16 SELECT test1
#    2 0xBED932B8C940697E 15914.4501 39.7%    986 16.1404  0.44 SELECT test2
#    4 0xF0AE7AFA7851C7E8   245.0176  0.6%    175  1.4001  0.11 SELECT test3
#    5 0xFB5A64603A53BFCE    97.2016  0.2%     77  1.2624  0.04 SELECT test4
#   14 0x1E088E88CDC208BE     8.7701  0.0%     14  0.6264  0.23 SELECT test5

嗯，这看着顺眼多了。前两个 SQL 占了所有执行时间的 96.8%！第一个 SQL 平均执行时间 350ms，方差 16%。而第二个语句更夸张，平均执行时间 16s，方差 44%。这得收拾！

但是要不要优化这样的 SQL，我们就需要根据 SQL 的分析和业务的分析来判断了。这里我先把执行计划列出来看看。

SQL1 的执行计划：

没有分区。

不包含子查询或者 union 操作。

全表扫描。

第一个表所查有 70 行，第二个表所查有 631 行，此值仅做为参考，并不精准。

第一个表返回结果只占了读取行数的 1.43%（优化点），第二个表返回结果只占了 0.16%（优化点）。

在第一个表中，Extra 有一个值，using where。

在第二个表中，Extra 有一个值，Range checked for each record (index map: 0x1) 。

SQL2 的执行计划：

没有分区。

不包含子查询或者 union 操作。

非唯一索引查找，也列出了具体的索引。

第一个表索引列上有 102570 行，第二个表索引列上有 118 行。此值仅做为参考，并不精准。

第一个表返回结果只占了读取行数的 3.33%（优化点），第二个表返回结果占了 100%。

在第一个表中，Extra 有三个值，using index condition; using where; using filesort。

在第二个表中，Extra 有一个值，using where。

这里我要敲黑板了！！！你是不是不记得 Extra 这些值的含义了？是不是要祭出你的搜索引擎，要开始查了！

我们这里再来回顾一遍。

using where：对结果用 where 子句中的条件过滤。

Range checked for each record (index map: 0x1)：MySQL 没有找到可以使用的索引，如果前面的表的列值已知，可能会部分使用索引。

using index condition：先条件过滤索引，找到所有符合索引条件的数据行，再用 where 子句中的条件做过滤。

using filesort：Query 中有 Order By 操作，又无法用索引完成排序，MySQL 不得不选择相应的排序算法来实现。是不是对应上了前面的 sorts per second？

知道了这些基础知识之后，下面再来看一下，两句语句很显然都有优化的空间，尽量使用 filtered 的比例能大一些，至于能不能用到索引，那就看业务的需要了，如果确实是要查很大的索引，表扫还能快点。所以这两个语句，要丢给开发做业务分析了。

优化结果

对数据库，我们有两个优化的方向还记得吧，第一个是 SQL 语句，第二个是 Query Cache。

我们先做第 2 个，将 Query Cache 开启，看一下效果如何。

mysql> show variables like 'query_cache%';

查看结果如下：

query_cache_type  ON
query_cache_size  1048576

再执行起来场景，看系统资源：

效果还不错哦，us CPU 降到了 50% 以下。

网络峰值时能达到 90Mbps 了，又快把带宽占完了。

查看网络队列：

再检查下队列，这时看到已经有接收队列了。

从 TPS 上来看，现在能到 300 多一点，同时网络接收发送加在一起 8M 左右。

后续性能工作建议

接下来数据库的优化方向就是优化 SQL。

当然还有别的优化建议，我们将在后面再说。

总结

这个案例从一个概括的描述开始，到各阶段的分析定位，是一个非常完整的过程。从一个项目的角度上来说，现在是不是性能已经达标，要有两方面的判断。

技术方面来说，显然这系统还有很多优化的空间，我们在文中也留了不少的扣。

业务方面来说，系统是否可以上线，就取决于业务指标了。

但是这个性能是不是已经做得完整了呢？显然还没有。现在只是调了一个节点而已。因为这是在测试环境中做的，硬件环境显得非常简单。线上部署结构也会包括分布式多节点集群等。所以从一个性能项目的角度来说，还远远没有结束。我想如果把这个项目完整地写下来，一本书的容量应该不为过。

从技术细节上来说，通过几个阶段的具体操作，可以让你有一个性能分析定位的宏观感受，这也是这两篇内容的初衷。性能优化是无止境的，我们要做的是以最少的时间和金钱成本，达到最大的优化效果。

思考题

这一篇文章延续上一篇的分析思路，你能讲一下 Swap 的原理和逻辑，以及分析思路吗？另外，慢 SQL 如何定位出来呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

29丨案例：如何应对因网络参数导致的TPS呈锯齿状？

在苍茫的性能分析道路上，不管你是一只多老的鸟，在经历了多个性能测试的项目之后，你都会发现对于性能问题而言，你仍然不敢说能全部解决。因为下一个问题可能真的是你完全没有见过的。

再加上技术的飞速发展，想跟得上技术的进步都是一件痛苦的事情，更别说要完全掌握并且融会贯通了。

我经常看到有些人在简历中动辄说自己做过上百个性能项目，以彰显自己有充足的经验。事实上，如果一个性能项目需要做两个星期的话，基本上做不到调优的层面，最多是弄个脚本压个报告。在我的经验中，基本上一个完整的架构级的性能项目从准备开始到写出测试报告、调优报告，需要 1.5 个月以上。你可以想像，这样的项目，就算一年不停地做，做 10 个都算是非常快的了，而要做上百个这样的项目，至少需要 10 年的时间。

并且不是每一个项目都能让你有分析性能瓶颈的机会，因为有很多问题都是重复的。

所以性能分析是一个需要不断总结出自己的分析逻辑的工作，有了这些分析逻辑，才能在新项目中无往不利。请注意我的描述，我强调的是要有自己分析的逻辑，而不是经历多少个性能问题。因为问题可能会遇到新的，但是分析逻辑却是可以复用的。

在今天的文章中，我仍然用一个之前项目中出现过的案例给你讲一讲性能分析的思路。

案例问题描述

这个项目是我调优过两次的项目。我介入这个项目之后，和同事们一起从 100TPS 调到 1000TPS。

但是调到这个阶段，也只是在测试环境中调的，并没有按生产的架构调优。从测试部署架构上来说，就是 Tomcat+Redis+MySQL，负载均衡的 Nginx 部分还没有加进去。

本来想着如果只是加个 Nginx，也复杂不到哪里去。于是，我就撤了。但是当我离开一周之后，那个项目组同事又给我打电话，说这个项目仍然有问题，加了 Nginx 之后，TPS 达不到 1000 了。啊，这还得了，要砸我招牌呀。

于是我又介入这个项目了，直到再次解决这个新的性能问题。

在今天的内容里，我会将记忆中所有的细节都记录下来，有些是同事调的步骤，有些是我调的步骤。在这个久远的项目中，希望我能写的完整。

下面来看这个具体的问题分析过程。

这个系统的简单架构图如下所示：

压力工具图

照例，我们先看压力工具中给出来的重要曲线。

用户递增图：

TPS 图：

响应时间图：

从上面的曲线可以看到，TPS 在上升到一个量级的时候就会掉下来，然后再上到同样的量级再掉下来，非常规律。而响应时间呢，在第一次 TPS 掉下来之后，就变得乱七八糟了。响应时间不仅上升了，而且抖动也很明显。

这是什么情况？从来没见过呀。

分析过程

我们需要经过一系列的工作——看操作系统的 CPU、I/O、Memory、NET 等资源；看数据库、Tomcat、Nginx 监控数据等等。

经过分析，我们觉得其他数据显示正常，网络连接状态是有问题的。如下所示：

tcp        0      0 ::ffff:192.168.1.12:59103   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59085   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59331   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:46381   ::ffff:192.168.1.104:3306   TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59034   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59383   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59138   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59407   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59288   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:58905   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:58867   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:58891   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59334   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:46129   ::ffff:192.168.1.100:3306   TIME_WAIT   timewait (0.00/0/0)
tcp        0      0 ::ffff:192.168.1.12:59143   ::ffff:192.168.1.11:3306    TIME_WAIT   timewait (0.00/0/0)

从这里我们可以看到，网络中有大量的 timewait 存在，这是有价值的信息了，但也只是现象。

尝试优化

尝试一：为 TIME_WAIT 修改 TCP 参数

通过检查 sysctl.conf，我们看到所有的配置均为默认，于是尝试如下修改。

其实这个修改，应该说是在分析得不够精准的情况下做的判断。

因为在服务端出现大量的 timewait，说明是服务端主动断开的 TCP 连接。而我们处理这样的连接，无非就是释放服务端的句柄和内存资源，但是不能释放端口，因为服务端只开了一个 listen 端口。

net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 3
net.ipv4.tcp_keepalive_time = 3

回归测试，问题依旧。

尝试二：修改 Nginx 的 proxy_ignore_client_abort

考虑到当客户端主动断开时，服务器上也会出现大量的 timewait，所以我们打开 proxy_ignore_client_abort，让 Nginx 忽略客户端主动中断时出现的错误。

proxy_ignore_client_abort on;

修改后，重启 Nginx，问题依旧。

这个操作纯属根据经验做的猜测。因为是加了 Nginx 之后才出现的问题。但是这个调整并没有起到作用，可见不是压力端主动断开连接而导致的问题，也就是说，和压力机无关了。

尝试三：换 Nginx 和 Nginx 所在的服务器

因为这个案例是通过 Nginx 服务器才看到 TPS 上到 300 就会下降，所以我们考虑是 Nginx 的问题。但是查来查去，也没看到 Nginx 有什么明显的问题，于是我们就下载了 Nginx 的源码，重新编译一下，什么模块都不加，只做转发。

通过回归测试发现，并没有解决问题。

到这里，那就显然和 Nginx 本身没什么关系了，那么我们就换个服务器吧。于是我们在另一台机器上重新编译了 Nginx，但是问题依旧。

服务器和 Nginx 都换了，但还是没有解决问题，那么问题会在哪呢？想来想去，还是在操作系统层面，因为 Nginx 实在是没啥可调的，只做转发还能复杂到哪去。

但是操作系统层面又有什么东西会影响 TPS 到如此规律的状态呢？在考虑了应用发送数据的逻辑之后（请参考《18 丨 CentOS：操作系统级监控及常用计数器解析（下）》中的网络部分中的“数据发送过程”和“数据接收过程”），我觉得操作系统本身应该不会存在这样的限制，网络配置参数我也看过，不会导致这样的问题。

那么在操作系统发送和接收数据的过程中，只有一个大模块我们还完全没有涉及到，那就是防火墙。于是我查了一下系统的防火墙状态。

激活状态的防火墙是如下这样的：

[root@node-1 zee]# systemctl status firewalld.service
● firewalld.service - firewalld - dynamic firewall daemon
   Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)
   Active: active (running) since Mon 2015-02-17 23:34:55 CST; 2s ago
     Docs: man:firewalld(1)
 Main PID: 32052 (firewalld)
    Tasks: 2
   Memory: 32.4M
   CGroup: /system.slice/firewalld.service
           └─32052 /usr/bin/python -Es /usr/sbin/firewalld --nofork --nopid

从上面的“active (running)”可以看到防火墙确实是开着的。那怎么办？果断地先停掉再说。

尝试四：停掉防火墙

和网络连接有关的内容，剩下的就只有防火墙了。于是执行如下命令：

Service iptables stop

之后我们就会看到 TPS 立即就上去了，这就明显和防火墙相关了。

dmesg 查下系统日志，可以看到如下的大量信息：

Nov  4 11:35:48 localhost kernel: __ratelimit: 108 callbacks suppressed
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:48 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:53 localhost kernel: __ratelimit: 592 callbacks suppressed
Nov  4 11:35:53 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:53 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:57 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: __ratelimit: 281 callbacks suppressed
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:35:58 localhost kernel: nf_conntrack: table full, dropping packet.
Nov  4 11:36:14 localhost kernel: __ratelimit: 7 callbacks suppressed

这个信息很明显，那就是 nf_conntrack 的表满了，于是把包给丢了。可是看到这里的时候，我还不知道 nf_conntrack 是个啥玩意。

后来通过查资料才知道，nf_conntrack（以前叫 ip_conntrack）是 Linux 中一个跟踪 TCP 连接条目的模块，它会用一个哈希表记录 TCP 的连接信息。当这个哈希表满了之后，就会报nf_conntrack: table full,dropping packet这样的错误。

__ratelimit: N callbacks suppressed的意思是系统中这样重复的日志太多了，达到了内核参数中的 net_ratelimit() 的上限。Linux 中也是用这个参数来避免 DDos 的。

可是，为啥要丢掉这些包呢？满就满了呗，你可以报错呀。丢人家的包，这一招不是很地道呀。

原理分析

下面来看一下 nf_conn 的数据结构：

struct nf_conn {
        / Usage count in here is 1 for hash table/destruct timer, 1 per skb, plus 1 for any connection(s) we are `master' for /
        struct  nf_conntrack  ct_general;                / 连接跟踪的引用计数 /
        spinlock_t  lock;
        /* Connection tracking(链接跟踪)用来跟踪、记录每个链接的信息(目前仅支持IP协议的连接跟踪)。每个链接由“tuple”来唯一标识，这里的“tuple”对不同的协议会有不同的含义，例如对TCP,UDP来说就是五元组: (源IP，源端口，目的IP, 目的端口，协议号)，对ICMP协议来说是: (源IP, 目的IP, id, type, code), 其中id,type与code都是icmp协议的信息。链接跟踪是防火墙实现状态检测的基础，很多功能都需要借助链接跟踪才能实现，例如NAT、快速转发、等等。*/
        struct  nf_conntrack_tuple_hash  tuplehash[IP_CT_DIR_MAX];

        unsigned long  status;  / 可以设置由enum ip_conntrack_status中描述的状态 /

        struct  nf_conn  master;  / 如果该连接是某个连接的子连接，则master指向它的主连接 */
        / Timer function; drops refcnt when it goes off. /
        struct  timer_list  timeout;

        union nf_conntrack_proto proto;                / 用于保存不同协议的私有数据 /
        / Extensions /
        struct nf_ct_ext ext;                        / 用于扩展结构 */
}

上面的 nf_conn 有最大个数限制，它是通过 nf_conntrack_max 来限制的，默认值是 65535。

通过查找资料，我发现 nf_conn 的主要作用，如下描述：

在 nf_conntrack 模块中，实现了对连接跟踪。它利用 netfilter 框架中的 nf_register_hook/nf_unregister_hook 函数来注册钩子项，调用 nf_conntrack_in 来建立相应连接，ipv4_conntrack_in 挂载在 NF_IP_PRE_ROUTEING 点上（该函数主要实现了创建连接），从而实现连接跟踪。

从上面的描述我们就可以看到，原来这个模块并不是防火墙必须的模块，并且从实现手法上来说，是通过注册 hook 函数实现的功能。有黑客经验的人应该经常听到 hook 函数，很多病毒软件都用这个思路往其他的应用中插入自己的代码模块的。

参数调整

知道了是什么模块之后，就很容易找到和它相关的参数了，这个信息你可以直接在操作系统的官网信息中找得到。

那么 nf_conntrack 的参数是什么呢？

net.netfilter.nf_conntrack_max
//是允许的最大跟踪连接条目，是在内核内存中netfilter可以同时处理的“任务”。
net.netfilter.nf_conntrack_tcp_timeout_established
//是TCP连接创建时的超时时间。

其实还有其他参数，只是这两个和性能相关，所以我在这里单独列了出来。

根据官方的公式，nf_conntrack_max 的最大值是CONNTRACK_MAX = RAMSIZE(in bytes)/16384/(ARCH/32)。

其中，RAMSIZE 是物理内存大小，ARCH 是操作系统位数，16384 是 netfilter 框架中 hash 桶参数 nf_conntrack_buckets 的默认大小。

以 32G 内存，64 位操作系统为例：

CONNTRACK_MAX = 3210241024*1024/16384/2 = 1048576条

也就是说，如果你有内存，就可以把这个值调大点。

我在这里就直接往后面加了个 0，改为：

net.netfilter.nf_conntrack_max=655350

知道了原理，就不止一个调优方案了，下面我们就来看看。

其他解决方案

关闭防火墙

chkconfig iptables off
chkconfig ip6tables off
service iptables stop
service ip6tables stop

禁用在这个端口上跟踪（也称为使用裸表）

在防火墙规则中这样配置：

-A PREROUTING -p tcp -m tcp --dport 80 -j NOTRACK

你也可以直接对 TCP 禁用跟踪，至于规则配置，就看你的想像力了（这也是我比较不喜欢弄防火墙的原因，配置也太灵活了，容易乱）。

删除跟踪模块

你还可以直接删除跟踪模块。首先，查看有哪些模块：

lsmod | grep nf_conntrack

再用 rmmod 删除它们。

总之，不管你用什么方式，都需要再你的应用可以安全并能正常提供服务的前提下使用。

知道了解决方案之后，我们就来看下解决之后的 TPS 是怎样的。

解决问题后的 TPS 图

上图中有两次 TPS 下降的过程，这是因为我又尝试修改了防火墙的禁用参数配置，重启了两次防火墙。并不是出现了新的问题，所以不要紧张。

后来在这个项目中又调优了一些其他的参数，下面是最终达到的调优效果。

蓝色是调优前的，红色是调优后的。

我们可以看到响应时间下降了很多，TPS 也上去了，客户非常满意，还请我吃了顿忘不了的牛肉火锅。哈。

总结

性能问题总是层出不穷，不管你以前多有经验，都可能会遇到不懂的性能问题。

如果建立了分析问题的思路逻辑，并且又善于学习和查找各种资料，找到根本原因，最后都会给出完整的解决方案。

这个案例应该说是个比较偏门的性能问题了，解决问题的思路就是我上面说的那样。

其实你也可以看到，有很多时候，我们的性能问题和代码并没有关系，所以这里也提醒那些一玩性能就想看代码的人，眼光要放开阔一些。还有就是遇到性能问题时，一定要记住，不要慌！

思考题

这是个刁钻的案例，你能说一下为什么在本例中，最后想到了看防火墙呢？以及，为什么说 timewait 的 TCP 链接只是问题的现象呢？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

30丨案例：为什么参数化数据会导致TPS突然下降？

写这篇文章的时候，我想起来一句似乎无关紧要的话：“我离你如此之近，你却对我视而不见。”

在性能测试中，参数化数据是少有的每个性能测试工程师都会用得到，却经常出现问题的技术点之一。从我的角度来说，究其原因，大部分是因为对性能参数化数据的理解不足。导致的结果就是用了参数化，但和真实的用户场景不一致，从而使得整个性能测试场景都失去了意义。

这样的例子不在少数。

一个项目开始之初，由于没有历史沉淀的数据，所以我们需要造一些数据来做性能测试。造多少呢？并不是按未来生产的容量来造，而是按性能场景中需要的数据量级来造。这种错误的做法是很多项目中真实出现的事情。

这并不止是性能测试工程师之过，还有很多其他的复杂原因，比如时间不够；经验不足，只能造重复的数据等等。

那么性能测试参数化数据的获取逻辑到底是什么呢？我们来看一个图吧。

在这个图中，我用不同的颜色表示不同组件中的数据。压力工具中的参数化数据有两种，这一点，我们前面有提到过，参数化数据有两大类型：

用户输入的数据同时在后台数据库中已存在。

用户输入的数据同时在后台数据库中不存在。

当我们使用数据库中已存在的数据时，就必须考虑到这个数据是否符合真实用户场景中的数据分布。当我们使用数据库中不存在的数据时，就必须考虑输入是否符合真实用户的输入。

在本篇要说的案例中，我们来看一下参数化数据如果做错了，对性能结果会产生什么样的影响。

案例问题描述

在一次压力测试的过程中，出现了如下所示的 TPS 数据（本篇文章中一些截图会有些模糊，因为来自于之前项目中的具体案例，在当时截图时，也并没有考虑清晰度，不过我们只要看趋势就好）。

在下图中，我们可以看到，在压力测试过程中，出现了 TPS 陡减到底的情况。这显然是不合理的。

这个曲线的趋势把性能瓶颈呈现得非常明显。在出现这个问题之后，当时我们也尝试过把线程数降低，观察 TPS 的趋势，结果从 300 到 100 到 50 到 10，最后到 1，发现都会出现这样的 TPS 陡减到底的情况，只是时间长度不同而已。

这非常像某个资源因为处理业务量的累积达到了某个临界点而产生的情形。

但不管怎样，我们还是要按正常分析的思路来分析它。

分析过程

首先，仍然是画一个架构图。

在这个图中，我们可以看到，JMeter 是连接到第一层服务（这里是有两个 Tomcat 实例），再到第二层服务（这里是也有两个 Tomcat 实例），然后再连到 DB 中。这个 DB 是一个互联网金融 DB（通过 MySQL 改造来的）。

了解了架构图之后，现在就开始查看下性能数据吧。

查操作系统

先看一下操作系统的性能数据：

从 top 中，我们可以看到这个应用服务器没啥压力，在这样的状态中，你可能都不用再去查其他的操作系统信息了，因为目前的压力对这个系统来说确实是小了点。

查应用

再看下应用的状态，这里用的工具仍然是前文中提到过多次的 JvisualVM（请你在用性能监控工具的时候，不要纠结，只要工具好使，用到吐都行，不用跟风）。

从这个图中可以看到的是，这个应用使用到的 CPU 确实很低，并且堆也没用多少。其实在这一步，我查了四个 Tomcat 的状态，只是截了一个图而已。

在这里还是要啰嗦一下了，对这样的曲线，我们一定要一眼就能看出问题在哪里。出现上图这样的情况是因为以下两个原因：

应用 CPU 使用率（橙色 CPU 线）确实是太低了，才 15% 左右。这和前面的 top 也是能对得上的。Java 的 GC 几乎没占 CPU（蓝色 CPU 线），也就是说 Tomcat 在这里没压力。

从堆曲线的趋势上来看，1G 的堆才到了 400M 多一点，并且回收一直都非常正常。怎么判断这个“正常”呢？首先，年轻代、年老代回收很有规律，并且没消耗什么 CPU；其次，每次 FullGC 都能回到 150M 左右，非常平稳。可见这个内存使用没啥问题。

当然到了这里，我当时也是查了网络的，只是也没什么压力，所以没做具体的记录（从这点可以看出，如果你在做性能测试的时候，要想记录性能瓶颈的分析过程，一定要记得把数据记全了，不然以后你可能都想不起来当时做了什么事情）。

查 DB

既然上面都没啥问题，DB 又是一个 MySQL，所以这里，我先手动执行了几个常规的查询语句。在 DB 中查看如下信息。

查processlist、innodb_trx、innodb_locks、innodb_lock_waits。在没有监控工具时，这几个是我经常在 MySQL 数据库检查的表，因为数据库如果慢的话，基本上会在这几个表中留些蛛丝马迹。

processlist 是看当然数据库中的 session 的，并且也会把正在执行的 SQL 列出来，快速刷新几次，就可以看到是不是有 SQL 一直卡在那里。

innodb_trx是正在执行的 SQL 事务表，这个表很重要。

innodb_locks和innodb_lock_waits是为了看有没有锁等待。

拿一条业务 SQL 执行一下，看看在压力之中会不会慢。这是在没有数据库监控时，快速判断业务的方法。因为这个业务很单一，用的 SQL 也单一，所以我在这里可以这样做。执行了之后，并没有发现业务 SQL 慢。

由此基本判断 DB 没什么问题。

注意，判断到了这里，其实已经出现了证据不完整产生的方向偏离！

陷入困局之后的手段

更悲催的是这个业务系统的日志记录的非常“简洁”，连时间消耗都没有记录下来。想来想去，在这么简单的一个架构中，没什么可查的东西了吧，除非网络中有设备导致了这个问题的出现？

在没有其它监控工具的情况下，当时我们上了最傻最二最基础又最有效的时间拆分手段：抓包！

抓包其实是个挺需要技巧的活，不止是说你能把包抓出来，还要能分析出来时间消耗在谁那里。这时我提醒一下，当你学会抓包工具的使用时，不要在每个场景下都想露一手你的抓包能力，通过抓的包分析响应时间的消耗点。

在我的工作中，只有万般无奈时才会祭出“抓包”这样的手段，并不是因为我对网络不够了解。恰恰是因为了解得足够多的，我才建议不要随便抓包。因为但凡在应用层有工具可以分析响应时间，都会比抓网络层的包来得更加简单直观。

经过一段段的分析之后，在数据库的一个主机上看到了如下信息：

看到这里的 TCP segment of a reassembled PDU 没有？它之上是 ACK。放大一下，看看这里的时间：

看到没有，这里有两秒的时间才发数据，那它是在干吗呢？

这里就要说明一下TCP segment of a reassembled PDU了，PDU 就是Protocol Data Unit。

以下高能烧脑，不喜可跳过！

它是指在 TCP 层接收到应用层发的非常大的数据之后，需要将数据大刀阔斧地砍成几段之后再发出去。就是这个砍数据的过程消耗了 2 秒的时间。

可是为什么 TCP 层要干这个事呢？上层应用给了你一大块数据包，你直接往外扔不就行了吗？还要自己 reassemble（重新装配），费老大劲。

这其实 TCP 的一个参数来决定的，它就是 MSS（Maximum Segment Size）。在 TCP 一开始打招呼的时候（就是握手的过程），已经通过 MSS 这个可选项告诉对方自己能接收的最大报文是多少了，这是不加任何信息的大小，纯的。而在以太网上，这个值是设置为 1460 字节的，为啥是 1460 呢？因为加上 TCP 头的 20 个字节和 IP 头的 20 个字节，刚好不大不小 1500 字节。

当你看到 1500 字节的时候，是不是有一种似曾相识的感觉？它就是现在普遍设置的 MTU（Maximum Transmission Unit）的大小呀。

这时你可能会说了，那我可以把 MTU 设置大嘛。可是你自己设置不行呀，别人（各主机和网络设备）都得跟着你设置才行，要不然到了 MTU 不大的地方，还得分包，还是要费时间。

而接收端呢？接数据时接到这些包的 ACK 序号都是一样的，但 Sequence Number 不同，并且后一个 Sequence Number 是前一个 Sequence Number+ 报文大小的值，那接收端就可以判断这是一个 TCP Segment 了。

好了，解释完这些之后，回到前面的问题。数据库自己耗时了两秒来做 reassemble PDU。至于吗？不就是过来查个数据吗？考虑了一下业务特征，这就是根据客户 ID 查一个帐户的一个月或三个月的记录信息，通常是 100 条左右，最多也就 200 条，也不至于有这么大。但是不管怎么样，还是数据库的问题！

这就是我前面说的查 DB 的时候，由于证据不全导致了分析思路的偏差。因为我手动执行了这个语句的时候并不慢，只要 10 几毫秒，所以，那时候我觉得数据库不是问题点。

但是经过了抓包之后，发现问题还是出在 DB 上。有时候真不能那么自信呀，容易给自己挖坑，要是早把活干得细致一点，也不至于要抓包了。

接着分析 DB

那我们肯定要接着看 DB 上的信息了，既然数据量大，SQL 执行得慢，那就先捞出慢日志看看。

查看如下负载信息：

# Profile

# Rank Query ID           Response time     Calls R/Call  V/M   Item
# ==== ================== ================= ===== ======= ===== ==========
#    1 0xB5DEC0207094BA2F 117365.8906 44.9% 14120  8.3120  8.46 SELECT
#    2 0xFF8A1413823E401F  62050.0287 23.7% 12078  5.1374  2.78 SELECT
#    3 0xC861142E667B5663  36004.3209 13.8% 21687  1.6602  0.13 SELECT
#    4 0xFB7DBC1F41799DDD  32413.9030 12.4% 19615  1.6525  0.09 SELECT
#    5 0xC065900AEAC5717F  11056.5444  4.2%  9304  1.1884  0.02 SELECT
#    9 0x6422DFBA813FC194    202.4342  0.1%    54  3.7488  1.83 INSERT
#   11 0x197C9DCF5DB927C8    137.4273  0.1%    36  3.8174  1.14 INSERT
#   13 0x1A9D64E72B53D706     97.9536  0.0%    31  3.1598  2.65 UPDATE
#   36 0x3B44178A8B9CE1C3     20.1134  0.0%    16  1.2571  0.04 INSERT
#   39 0x370753250D9FB9EF     14.5224  0.0%    11  1.3202  0.04 INSERT
# MISC 0xMISC               2152.2442  0.8%   151 14.2533   0.0 <72 ITEMS>

你可以看到确实有四个 SQL 消耗了更多的时间，并且时间还不短。这是明显的性能问题，但是我把这 SQL 拿出来执行过呀，并不慢。

怎么回事呢？

我让做数据库运维的人把 DB proxy 层的所有 SQL 日志拿出来分析一遍。为什么我要 DB proxy 层的数据呢？因为这一段会把所有执行的 SQL 都记录下来，而慢日志记录的是 1s 以上的（取决于 DB 中的配置）。首先是把 time cost 大于 200ms 的 SQL 都拉出来，结果发现，真的在 TPS 下降的那个时间段，出现了 SQL 执行超长的情况，并且和我执行的，还是同样的业务 SQL。

怎么办？既然到这个层面了，这些执行的 SQL 只有一点区别，那就是查询条件。慢的 SQL 的查询条件，我拿回来试了，果然是慢，查出来的数据也是完全不一样的，居然能查出几万条数据来。前面说了，这个语句是根据客户 ID 查出记录数的，那么就根据客户 ID，做一次 group by，看下数据量为啥有这么多大差别。

于是得到了如下的结果：

客户ID, 数量
'这一列只是客户id，无它', '91307'
'这一列只是客户id，无它', '69865'
'这一列只是客户id，无它', '55075'
'这一列只是客户id，无它', '54990'
'这一列只是客户id，无它', '54975'
'这一列只是客户id，无它', '54962'
'这一列只是客户id，无它', '54899'
'这一列只是客户id，无它', '54898'
'这一列只是客户id，无它', '54874'
'这一列只是客户id，无它', '54862'
....................
'这一列只是客户id，无它', '161'
'这一列只是客户id，无它', '161'
'这一列只是客户id，无它', '161'
'这一列只是客户id，无它', '161'
'这一列只是客户id，无它', '161'
'这一列只是客户id，无它', '161'
'这一列只是客户id，无它', '160'
'这一列只是客户id，无它', '160'

从这个结果可以看到，不同客户 ID 的记录条数差别太大了。这是谁干的好事？！我们一开始就强调数据需要造均衡，要符合生产真实用户的数据分布。

到这里，问题基本上就明确了，查一下参数化的数据，里面有 10 万条数据，而取到记录数在五六万左右的客户 ID 的时候，才出现了响应时间长的问题。

而我之前的执行的 SQL，恰好试了多次都是数据量少的。

下面怎么办呢？先做个最规矩的实验，把 5 万条往后的数据全都删掉！场景再执行一遍。

于是就得到了如下的结果：

问题完美解决。

可是问题怎么出现的呢？

经过询问负责产生基础数据的人，最后得知，一开始数据库里的基础数据不够。由于我在项目中要求基础数据量和参数化数据量要达到生产级别。于是把这个工作安排给了一个同事，就是造出每个客户都和生产环境中差不多量级的记录。当时是用压力脚本做客户 ID 的参数化，然后用执行压力的方式造的数据。

本来这个事情在做的时候，应该是把每个客户 ID 都加到差不多的记录的。但是这个人在做的时候，觉得一个个循环加下去实在是太消耗时间了，终于等不急了，于是在干了几个小时之后，觉得每个客户 ID 上都有了一些数据量之后，自己做了个决定，把客户 ID 减少到只有几百个，这样很快就干完了！

哭笑不得的感觉有没有？！

总结

很多性能问题，在出现的时候，都会觉得无从下手，而当分析到根本原因的时候，就觉得啼笑皆非。

但很多时候，在真实的场景中，很多性能问题连原因都没有分析出来，连啼笑皆非的机会都没有，就开始寻找规避的手段了，这就像用一个坑去埋另一个坑，于是大坑套小坑、小坑套水洼。

还有，在做性能分析的时候，有经验固然是好事，但是经验也并不是在所有的场景中都能有效地帮你解决问题，相反，它们有时也会成为累赘，成为判断出现偏差的原因。

所以我现在都会诚心地告诫一些性能测试从业人员：一定要全局监控、定向监控一层层数据查，不要觉得查了某个点就判断这个组件没问题了。像我这样的老鸟也照样得从全局查起，不然也是掉坑里。而这个“全局 - 定向”的思路，也照样适用一些新手，可以形成排查手册。

在我带过的项目中，我经常会讲这样的思路，制作排查手册（因为每个项目用的东西都会有些区别），而这些思路和排查手册，现在就变成了你一篇篇看过的文章。

所以我希望看专栏的人都能知道真正的分析性能瓶颈的过程是什么样子。不要在意自己现在会什么，要多在意以后会什么。

问题

讲完了今天的内容，你能说一下为什么通过抓包可以判断出响应时间的拆分吗？以及，数据分布不均衡还会带来哪些性能问题？

欢迎你在评论区写下你的思考，也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

31丨案例：当磁盘参数导致I/O高的时候，应该怎么办？

在大部分的性能项目中，当系统调优到一定程度的时候，性能的瓶颈往往会体现在两类计数器上：一个是 CPU，另一个就是磁盘 I/O 了。所以我们也经常会在一些性能优化的文章中看到两个分类，分别是 CPU 密集型和磁盘 I/O 密集型。

有人说为什么不说内存呢？内存是那么重要。不是说内存不会成为瓶颈，只不过内存的瓶颈基本上都可以转嫁给 CPU 和磁盘 I/O。当内存不够的时候，大不了就是清理得快一点。内存能表现出来的，就是满不满，而谁去清理呢？那就是 CPU 了。清理得快就得 CPU 转得快。

我们经常会听到有人说什么性能优化到最后就是“空间转时间、时间转空间的优化”。如此带有禅意的一句话，其实意思就是，CPU 不够用，就扩大内存；内存不够，就让 CPU 计算得更快一些。

举个例子，当我们需要在内存中使用很多变量时，如果内存不够，就会导致 CPU 不断清理内存中没被引用的变量来释放内存，这就导致了释放内存的动作会消耗更多的 CPU。而这时，我们就可以用增加内存的方式，让 CPU 不那么繁忙。

但这个“空间、时间转化”的论点并不会在所有的场景下成立。比如说一个应用并不需要多大内存，就是纯计算型的，那你加内存也没啥用。另外这里提到的“空间”也不是硬盘，因为如果 CPU 不够用，拿再多磁盘补也无济于事。

所以这句话只能是做为高深谒语让初入性能的小白们仰望，实际上适用的场景非常少。

而磁盘 I/O 和内存有很大的区别。只要系统需要保存运行过程中留下的数据，那就必然会用到磁盘 I/O。分析磁盘 I/O 的时候，相对于其他的分析环节还是要复杂一些，因为磁盘 I/O 栈是比较长的。有了 Direct I/O 技术之后，磁盘 I/O 栈短了一段，速度快了，但是同样也没有给性能测试分析人员带来什么福音，因为从分析的角度来说，我们看计数器并没有减少什么。

在性能分析过程中，操作系统是绕不过去的一个环节。在当前的技术栈中，我们仍然要把操作系统当成一个非常重要的环节。

现在对于 I/O 的分析和判断，大部分还停留在磁盘百分比、队列、磁盘延时之类上。可是这样的计数器的值多了，少了，你要干什么呢？怎么解决它呢？是磁盘参数有问题，还是应用有问题？还是磁盘硬件能力就那样了？

这是性能分析人员应该给出的答案, 可是有很多性能测试工程师还真的给不出来这样的答案，甚至有些人连哪个线程导致的 I/O 高都判断不出来。至少现在的状态说明了，性能测试行业还远远不够成熟，我们还有很多的机会。

现在我们所说的 I/O，基本上把 File I/O 和 Disk I/O 都说进去了，所以在分析上，层级更是非常模糊。如下图所示的一个 I/O 栈：

(此图来自于：https://www.thomas-krenn.com/de/wikiDE/images/b/ba/Linux-storage-stack-diagram_v4.0.png)

在这个图中，我们可以明确看到磁盘 I/O 栈有多长。从应用层开始，产生系统调用，再通过 visual FS Layer → FS Layer → Block Layer 这样一层一层地调用下去，最后才写到了硬件存储里。

当然还有一层，我们需要关注的就是从系统的全局状态到具体的线程，这个我在操作系统那一篇的 I/O 部分已经描述过。

下面我们来看一个 I/O 高的分析案例。

案例现象

在这个项目的压力测试过程当中，发现了经典的现象：

TPS 上不去；

硬件资源用不上。

应该说，性能分析要是分类比较宽泛，基本上是两种。

第一种是资源用不上，TPS 上不去，响应时间随压力而增加，如下图所示：

第二种是资源用得上，TPS 上不去，响应时间随压力而增加。如下图所示：

这样的划分方式看似有调侃味道，却是很多人疑惑的起点。为什么这么说呢？

我们在开始做性能分析的时候，经常会面对这种情况，那就是资源用不上去。也就是路不够宽，硬件资源才用不上的。我们要解决的，就是把路扩宽一些。

但是，当我们面对一些客户或领导的时候，如果去汇报某资源使用率达到 100% 这样高的值的时候，有可能得到的一个反馈是，能不能把资源使用率降下来，因为资源使用率高会让人有种不安全感，而 TPS 反而不是关注的重点。这就让我们很疑惑：TPS 上去了，资源必然用得多呀，难道让资源使用率稳定保持在 50%，而 TPS 蹭蹭往上涨吗？这不现实呀。

我的建议是，不用怕资源使用率高，做为专业性能分析人员，更应该怕的是资源使用率不高！

在这个例子中，一开始同事跟我描述的时候，说资源怎么也用不上去，TPS 也上不去。这样的描述很常见。

还是看看压力结果，如下所示：

（场景开始时报了少量的错，不过这不是我们这个案例要分析的部分，请看官忽略。）

像这种描述，基本上没办法让人分析，因为信息太少了。特别是有些人直接给一个固定线程数的压力结果，用来描述资源上不去的情况，这就更得不到有用的信息了，所以问性能问题的人一定要注意收集足够的信息，不然只能让人从头查一遍。

分析过程

因为没有什么具体的信息，所以我只能让同事把压力再压起来。

我登录到服务器上之后，顺手执行了一个 top 命令：

这里给个小提醒，执行 top 的时候一定要习惯性地点一下数字 1，这样可以把所有的 CPU 都列出来，因为我要看到每个 CPU 的各个计数器的使用率。别看这个动作不起眼，就算是这一个小小的操作，也是工作经验的积累。

果不其然，看到了一个问题，那就是上图中 CPU0 的 wa 达到了 72%。像这样的问题，如果不看每个 CPU 的每个计数器，是发现不了的。

看到这样的东西，我们要有个基本的判断，对于一般的 Java 应用来说，I/O 线程数基本上不会是 1 个，除非是特别的理由，所以这种情况还是很少的。

不过既然是 I/O 的问题导致了 wa CPU 使用率高，那么我来看一下 CPU 的热点在哪个层面。这是我在工作中经常执行的一个动作，因为我不想在后面的分析过程中偏离方向，而看 CPU 热点可以让我记得这个方向。

如下图所示：

有人说，即然 wa CPU 使用率已经高了，接下去直接看磁盘 I/O 不就行了，看不看 CPU 热点还重要吗？我要说，当然重要！看到前面的磁盘 I/O 栈了吗？那么长，怎么知道是在哪一层呢？

这是定向细化分析过程中的经验之一。

在 CPU 热点中我们看到了，是内核模块中的 blk_queue_bio 消耗的 CPU 大。blk_queue_bio 是用来做 I/O 调度合并的，可以对 I/O 请求进入后向、前向合并操作，互斥方式就是加锁。

根据它我们就可以知道，这是一个有 queue 的调度过程，再接着看它下面的几个函数也可以看出，都是 kernel 级的调用，并没有 user 级的调用，这几个函数基本上也说明了当前系统忙于 journal 写。

接着执行 iotop，看是哪个进程在做这个动作。这里可以看到 jbd2，和上面 CPU 热点的函数也是对应得上的。

那么 jbd2 是个啥玩意呢？jbd2 英文名是这样的：Journaling Block Device 2。

它是跟着 ext4 格式来的，当然它也可以在其他磁盘格式下工作。这个进程的工作原理是这样的：文件系统写入数据，要提交给驱动程序，而 jbd2 就是在文件系统调用驱动之前工作的。文件系统要先调用 jbd2，然后 jbd2 会根据系统的设置（设置有 writeback、ordered、journal），进行数据的备份，然后再让文件系统提交数据。当文件系统将数据写入了块设备之后，jbd2 就会把备份的数据删除。如果文件系统写块设备时出了问题（比如说可悲的断电），那 jbd2 这里还有一个备份，在进行完整性检查时就会把数据补全，所以数据不会丢。

jbd2 就是这样保证数据的完整性的。

但是！对嘛，我就喜欢说但是。jbd2 保证的数据完整性也只是一个原子操作的完整性，即一个原子操作如果操作了 2M 的数据，它就只能保证这 2M 的数据不会丢失。如果你一下子操作了 1T 的数据，jbd2 也不能保证完整性。

jbd2 有一个参数 barrier，它用来开启磁盘屏障。就是设置一个栅栏，要先把 barrier 之前的数据全都写到磁盘设备之后，才会写 barrier 之后的数据，也就是说它是用来保证原子操作中数据的完整性的。当然了，开启 barrier 的一个后果就是性能下降。

了解了这些内容之后，我们就要判断一下了。这个应用有没有必要用这个功能来保证原子操作的完整性？我考虑了一下之后，觉得这个应用就是写日志而已，也没有其他什么重要的东西是要写日志的，重要的业务数据都写到数据库去了。

所以，没必要保证数据写入的完整性，就算丢了一些日志数据又怎么样呢？如果真的就那么寸，应用出现大 bug，恰好块设备也出问题，或者断电，那真的只有烧香拜佛了。

于是我觉得这个功能不用也罢，把原理说明白之后，和架构组、开发组、运维组以及领导层们沟通了一圈，告诉了他们原理，实在是没有打开的必要。

最后大家一致同意：关掉！

我们再回过头来看下这个参数：

[root@主机A]# cat /proc/mounts
/dev/mapper/data_lvm-data_lv /data ext4 rw,relatime,barrier=1,data=ordered 0 0

上面的这个 barrier=1，就是我们要找的目标。想关了它，就是挂载磁盘的时候使用挂载选项 -o barrier=0 或者 nobarrier。我这里是直接设置为 0 的。

你也可以直接关掉 journal 功能：

tune2fs -o journal_data_writeback /dev/mapper/data_lvm-data_lv tune2fs -O "^has_journal" /dev/mapper/data_lvm-data_lv e2fsck -f /dev/mapper/data_lvm-data_lv

问题解决之后

我们再来看下问题解决后的压力工具结果：

从压力结果来看，同样的压力之下，TPS 增加了一倍，响应时间快了一倍。

top 如下所示：

集中的 wa CPU 也消失不见了，现在使用的也挺均衡的了。

iotop：

在 I/O top 的结果中，也看不到 I/O 非常高的 jbd2 了。

问题得到了完美地解决。

总结

不管是什么样的性能问题，其实从分析思路上仍然逃不开我一直提到的思路——那就是一个分析的完整链路。当你一层一层往下找问题时，只要抓住了重点，思路不断，找到根本原因就可以解决问题。

在这个 I/O 的问题中，难点在于怎么能知道 jbd2 的原理和参数。应该说，不管是谁，都不能保证自己的知识体系是完整的，那怎么办呢？查资料，各种学习，看源码，看逻辑。实在看不懂，那也没办法，接着修炼基础内功呗。

所以说性能测试行业中，经常只测不分析，也是因为做性能分析需要的背景知识量有点大，还要不断分析各种新的知识点。不过也就是因为如此，性能测试和性能分析才真的有价值。只测不调只是做了一半工作，价值完全体现不出来。

思考题

最后问你两个问题吧：为什么 TPS 上不去时，资源用不上才是更让人着急的问题？以及为什么要在 CPU 高时查看 CPU 热点函数呢？

欢迎你在评论区写下你的思考，我会和你一起交流。也欢迎你把这篇文章分享给你的朋友或者同事，一起交流一下。

32丨当Postgres磁盘读引起I/O高的时候，应该怎么办？

在性能分析的人眼里，性能瓶颈就是性能瓶颈。无论这个性能瓶颈出现在代码层、操作系统层、数据库层还是其他层，最终的目的只有一个结果：解决掉！

有人可能会觉得这种说法过于霸道。

事实上，我要强调的性能分析能力，是一套分析逻辑。在这一套分析逻辑中，不管是操作系统、代码还是数据库等，所涉及到的都只是基础知识。如果一个人都掌握这些内容，那确实不现实，但如果是对一个性能团队的要求，我觉得一点也不高。

在性能测试和性能分析的项目中，没有压力发起，就不会有性能瓶颈，也就谈不上性能分析了。所以每个问题的前提，都是要有压力。

但不是所有的压力场景都合理，再加上即使压力场景不合理，也能压出性能瓶颈，这就会产生一种错觉：似乎一个错误的压力场景也是有效的。

我是在介入一个项目时，首先会看场景是否有效。如果无效，我就不会下手去调了，因为即使优化好了，可能也给不出生产环境应该如何配置的结论，那工作就白做了。

所以要先调场景。

我经常会把一个性能测试项目里的工作分成两大阶段：

整理阶段

在这个阶段中，要把之前项目中做错的内容纠正过来。不止有技术里的纠正，还有从上到下沟通上的纠正。

调优阶段

这才真是干活的阶段。

在这个案例中，同样，我还是要表达一个分析的思路。

案例问题描述

这是一个性能从业人员问的问题：为什么这个应用的 update 用了这么长时间呢？他还给了我一个截图：

从这个图中可以看到时间在 100 毫秒左右。根据我的经验，一个 SQL 执行 100ms，对实时业务来说，确实有点长了。

但是这个时间是长还是短，还不能下结论。要是业务需要必须写成这种耗时的 SQL 呢？

接着他又给我发了 TPS 图。如下所示：

这个 TPS 图确实……有点乱！还记得前面我对 TPS 的描述吧，在一个场景中，TPS 是要有阶梯的。

如果你在递增的 TPS 场景中发现了问题，然后为了找到这个问题，用同样的 TPS 级别快速加起来压力，这种方式也是可以的。只是这个结果不做为测试报告，而是应该记录到调优报告当中。

而现在我们看到的这个 TPS 趋势，那真是哪哪都挨不上呀。如此混乱的 TPS，那必然是性能有问题。

他还告诉了我两个信息。

有 100 万条参数化数据；

GC 正常，dump 文件也没有死锁的问题。

这两个信息应该说只能是信息，并不能起到什么作用。另外，我也不知道他说的“GC 正常”是怎么个正常法，只能相信他说的。

以上就是基本的信息了。

分析过程

照旧，先画个架构图出来看看。

每次做性能分析的时候，我几乎都会先干这个事情。只有看了这个图，我心里才踏实。才能明确知道要面对的系统范围有多大；才能在一个地方出问题的时候，去考虑是不是由其他地方引起的；才能跟着问题找到一条条的分析路径……

下面是一张简单的架构图，从下面这张架构图中可以看到，这不是个复杂的应用，是个非常典型的微服务结构，只是数据库用了 PostgreSQL 而已。

由于这个问题反馈的是从服务集群日志中看到的 update 慢，所以后面的分析肯定是直接对着数据库去了。

这里要提醒一句，我们看到什么现象，就跟着现象去分析。这是非常正规的思路吧。但就是有一些人，明明看着数据库有问题，非要瞪着眼睛跟应用服务器较劲。

前不久就有一个人问了我一个性能问题，说是在压力过程中，发现数据库 CPU 用完了，应用服务器的 CPU 还有余量，于是加了两个数据库 CPU。但是加完之后，发现数据库 CPU 使用率没有用上去，反而应用服务器的 CPU 用完了。我一听，觉得挺合理的呀，为什么他在纠结应用服务器用完了呢？于是我就告诉他，别纠结这个，先看时间耗在哪里。结果发现应用的时间都耗在读取数据库上了，只是数据库硬件好了一些而已。

因为这是个在数据库上的问题，所以我直接查了数据库的资源。

查看 vmstat，从这个结果来看，系统资源确实没用上。不过，请注意，这个 bi 挺高，能达到 30 万以上。那这个值说明了什么呢？我们来算一算。

bi 是指每秒读磁盘的块数。所以要先看一下，一块有多大。

[root@7dgroup1 ~]# tune2fs -l /dev/vda1 | grep "Block size"
Block size:               4096
[root@7dgroup1 ~]#

那计算下来大约就是：

(300000∗1024)/1024/1024≈293M

将近 300M 的读取，显然这个值是不低的。

接下来查看 I/O。再执行下 iostat 看看。

从这个结果来看，%util 已经达到了 95% 左右，同时看 rkB/s 那一列，确实在 300M 左右。

接着在 master 上面的执行 iotop。

我发现 Walsender Postgres 进程达到了 56.07% 的使用率，也就是说它的读在 300M 左右。但是写的并不多，从图上看只有 5.77M/s。

结合上面几个图，我们后面的优化方向就是：降低读取，提高写入。

到这里，我们就得说道说道了。这个 Walsender Postgres 进程是干吗的呢？

我根据理解，画了一个 Walsender 的逻辑图：

从这个图中就可以看得出来，Walsender 和 Walreceiver 实现了 PostgreSQL 的 Master 和 Slave 之间的流式复制。Walsender 取归档目录中的内容（敲黑板了呀！），通过网络发送给 Walreceiver，Walreceiver 接收之后在 slave 上还原和 master 数据库一样的数据。

而现在读取这么高，那我们就把读取降下来。

先查看一下几个关键参数：

这两个参数对 PostgreSQL 非常重要。checkpoint_completion_target 这个值表示这次 checkpoint 完成的时间占到下一次 checkpoint 之间的时间的百分比。

这样说似乎不太好理解。画图说明一下：

在这个图中 300s 就是 checkpoint_timeout，即两次 checkpoint 之间的时间长度。这时若将 checkpoint_completion_target 设置为 0.1，那就是说 CheckPoint1 完成时间的目标就是在 30s 以内。

在这样的配置之下，你就会知道 checkpoint_completion_target 设置得越短，集中写的内容就越多，I/O 峰值就会高；checkpoint_completion_target 设置得越长，写入就不会那么集中。也就是说 checkpoint_completion_target 设置得长，会让写 I/O 有缓解。

在我们这个案例中，写并没有多少。所以这个不是什么问题。

但是读取的 I/O 那么大，又是流式传输的，那就是会不断地读文件，为了保证有足够的数据可以流式输出，这里我把 shared_buffers 增加，以便减轻本地 I/O 的的压力。

来看一下优化动作：

checkpoint_completion_target = 0.1
checkpoint_timeout = 30min
shared_buffers = 20G
min_wal_size = 1GB
max_wal_size = 4GB

其中的 max_wal_size 和 min_wal_size 官方含义如下所示。

max_wal_size (integer)：

Maximum size to let the WAL grow to between automatic WAL checkpoints. This is a soft limit; WAL size can exceed max_wal_size under special circumstances, like under heavy load, a failing archive_command, or a high wal_keep_segments setting. The default is 1 GB. Increasing this parameter can increase the amount of time needed for crash recovery. This parameter can only be set in the postgresql.conf file or on the server command line.

min_wal_size (integer)：

As long as WAL disk usage stays below this setting, old WAL files are always recycled for future use at a checkpoint, rather than removed. This can be used to ensure that enough WAL space is reserved to handle spikes in WAL usage, for example when running large batch jobs. The default is 80 MB. This parameter can only be set in the postgresql.conf file or on the server command line.

请注意，上面的 shared_buffers 是有点过大的，不过我们先验证结果再说。

优化结果

再看 iostat：

看起来持续的读降低了不少。效果是有的，方向没错。再来看看 TPS：

看这里 TPS 确实稳定了很多，效果也比较明显。

这也就达到我们优化的目标了。就像在前面文章中所说的，在优化的过程中，当你碰到 TPS 非常不规则时，请记住，一定要先把 TPS 调稳定，不要指望在一个混乱的 TPS 曲线下做优化，那将使你无的放矢。

问题又来了？

在解决了上一个问题之后，没过多久，另一个问题又抛到我面前了，这是另一个接口，因为是在同一个项目上，所以对问问题的人来说，疑惑还是数据库有问题。

来看一下 TPS：

这个问题很明显，那就是后面的成功事务数怎么能达到 8000 以上？如果让你蒙的话，你觉得会是什么原因呢？

在这里，告诉你我对 TPS 趋势的判断逻辑，那就是 TPS 不能出现意外的趋势。

什么叫意外的趋势？就是当在运行一个场景之前就已经考虑到了这个 TPS 趋势应该是个什么样子（做尝试的场景除外），当拿到运行的结果之后，TPS 趋势要和预期一致。

如果没有预期，就不具有分析 TPS 的能力了，最多也就是压出曲线，但不会懂曲线的含义。

像上面的这处 TPS 图，显然就出现意外了，并且是如此大的意外。前面只有 1300 左右的 TPS，后面怎么可能跑到 8000 以上，还全是对的呢？

所以我看到这个图之后，就问了一下：是不是没加断言？

然后他查了一下，果然没有加断言。于是重跑场景。得到如下结果：

从这个图上可以看到，加了断言之后，错误的事务都正常暴露出来了。像这种后台处理了异常并返回了信息的时候，前端会收到正常的 HTTP Code，所以才会出现这样的问题。

这也是为什么通常我们都要加断言来判断业务是否正常。

总结

在性能分析的道路上，我们会遇到各种杂七杂八的问题。很多时候，我们都期待着性能测试中的分析像破案一样，并且最好可以破一个惊天地泣鬼神的大案，以扬名四海。

然而分析到了根本原因之后，你会发现优化的部分是如此简单。

其实对于 PostgreSQL 数据库来说，像 buffer、log、replication 等内容，都是非常重要的分析点，在做项目之前，我建议先把这样的参数给收拾一遍，不要让参数配置成为性能问题，否则得不偿失。

思考题

最后问你两个问题吧。为什么加大 buffer 可以减少磁盘 I/O 的压力？为什么说 TPS 趋势要在预期之内？

欢迎你在评论区写下你的思考，我会和你一起交流。也欢迎把这篇文章分享给你的朋友或者同事，一起交流一下。

结束语丨见过林林总总的乱象，才知未来的无限可能

这是专栏的结束语了。

我是不是应该用欢乐范表达一下？

（眉飞色舞地）时光飞逝，终于就写完了，顿感轻松。如今大赦，心情特好，就等着疫情结束出门去浪？

或者用获奖感言范表达一下？

（声泪俱下地）专栏写得真辛苦，我搭建了很多环境来证明一个个技术点。每天干到一两点，一周也可能憋不出一篇。为了一个 k8s 截图，我自己搭了一个 k8s 环境，并且弄个应用跑起来，以确保我所有的内容都是原创（除了为了抨击而引用的部分）。

亦或者用胸怀天下范（又称喷子范）表达一下？

（痛心疾首地）当今性能市场杂乱无章，误导横行。写这个专栏就是为了给性能领域中一些迷途的羔羊以指导，让你找到职业的方向。也让你知道，在性能领域中，还有很多真正在干性能的人，而不是只有那些会一两个压力工具出一个罗列数据的报告就在各个场合张牙舞爪吹嘘的人。

然而以上各个角度的描述都显得单调了。虽然专栏完成之后有轻松感，但是从真实的感觉上说，也没有特别轻松，就是平平淡淡地完成了一件事情，一个项目。

性能市场还是有很大空间，性能也还有很长的路要走，它远远没有到精致的程度。

我在很多安全性很高的机房中看到过大量的硬件资源浪费，也看到过很多的线上系统配置得一塌糊涂，也看到过经过了性能测试但上线就死的系统，也见过明知道会死但为了任务必须上线的系统。

我也优化过上线多年的系统，发现长年运行在性能瓶颈已经出现的状态当中，就是因为没有性能调优，业务部门苦不堪言，技术团队频招白眼。其实只需几个优化步骤就能让各方满意。

这林林总总的现象都告诉我，性能的价值是完全可以体现出来的。但为什么现在的性能和性能分析职位却如此没落？

绝大部分性能工程师只是懂些压力工具、监控工具的使用，但对分析一筹莫展。业务团队、架构团队，包括老板们对性能团队的期望均因为立场、角度不同，才让性能团队左右为难。最典型的体现就是性能需求的不确定性以及沟通的困难。

而性能行业不具备统一的专业认识也让行业迷失在众多岔道之中。人人各持己见，却少有人能结合实际工作经验做精致的落地。让空中楼阁似的理念和实施过程产生了严重脱节。

而初入性能行业的人因为看不到全景概貌，从而云里雾里，东一榔头西一棒槌地学习着片面的知识点，又使职场和行业更为混乱。

那希望在哪里呢？

其实我在很多场合都说过，技术职位的存在是由于业务发展的需要。至少经过测试分析优化之后，生产环境不会出现低端的问题以及资源的大量浪费。

说到这里，性能优化的价值其实就已经非常明确了，性能团队的存在也就有了充分的理由。

那就是必须有结论。

如何才能合理地看待性能这个话题呢？

我一直都觉得性能应该是从需求到运维的视角去解析。只有这样去看它，只有这样去具体实施它，它才具有真正的价值。

而在这个时候，它就不再是测试角度的一个附属，它就是一个完整的性能工程。

前几年，我也一直在思考一个问题，那就是：我的经验，我走过的弯路，能给这个行业，或者说相关的从业者带来什么？

工具的使用？我觉得没有什么价值。对于一个爱学习的人来说，工具只要看说明书，也能玩个八九不离十了，为什么要花钱买工具使用的书或专栏呢？

写分析的逻辑吗？一个个具体的分析案例，兴许是一个可写的点，可是这些分析的逻辑如果不能复用，它们存在的价值就只能是“仅供参考”。

我在很长的时间内都只做企业内训，因为我觉得站在企业架构级的视角，性能才有价值。

后来我终于想明白写专栏的价值，那就是：将性能领域中的理念和分析逻辑按自己认为正确的方向梳理一遍。

整体来说，我的性能测试分析调优观就如下图所示：

我把这个提炼逻辑总结一下就是：RESAR 性能方法论。

这里不是在简单的描述测试过程，而强调其中每个环节中要做的事情，注重每个环节的精致细节。

希望你能体会到这一点。

这个梳理的过程，更多的是为了把我认为有价值的东西抛出去，不再死守着自己认为对而别人又不知道的道理，痛心疾首地埋怨着行业的不争。写出去之后，我就可以放下这些内容再往前走了。

不管你看到这个专栏的感觉是什么样，可能会不认可，也可能认可我说的话。无论你觉得我自大自负也好，觉得我狂悖也好，觉得酣畅淋漓也好，我都接受。

因为我写出了自己想写的内容。

专栏已经完结，里面的案例、分析思路都是经验所得，可能有些细节上不够考究，所以我会在后面把这些不考究的点在思维中完善起来，也很感谢给我提过意见的你。

希望你能喜欢这个专栏，能记得有一个做性能的人如此偏执。

我会一直站在性能的路上，希望以后和你有切磋的机会！