mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-19 07:33:47 +08:00
del
This commit is contained in:
175
极客时间专栏/geek/性能工程高手课/性能工程实践/25 | 如何在生产环境中进行真实的容量测试?.md
Normal file
175
极客时间专栏/geek/性能工程高手课/性能工程实践/25 | 如何在生产环境中进行真实的容量测试?.md
Normal file
@@ -0,0 +1,175 @@
|
||||
<audio id="audio" title="25 | 如何在生产环境中进行真实的容量测试?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cf/28/cfd9d042814405dbc6c5693f2833c728.mp3"></audio>
|
||||
|
||||
你好,我是庄振运。
|
||||
|
||||
从今天开始,我们进入新的模块:性能工程实践。在这一模块中,我会讲述在实际生产环境中应用性能工程的场景、案例。这些场景和案例都是针对大规模互联网服务,是在解决实际性能问题后总结的经验。
|
||||
|
||||
今天我要讲的主题是“在生产环境中进行真实场景的压力测试”。这来源于我对LinkedIn公司生产实践的总结。
|
||||
|
||||
LinkedIn为超过5.9亿用户提供服务,在性能优化的过程中,经常会遇到这类问题:一个服务可以承受的最大QPS是多少?要满足100K QPS的服务需求,我需要多少服务器?……
|
||||
|
||||
怎么解决这些问题呢,有一个大招就是在生产环境中进行真实的容量测试。
|
||||
|
||||
关于这个实践的详细方案和技术细节,我们曾经发表过一篇研究论文([IEEE ICWS](https://ieeexplore.ieee.org/document/8029816)),并且很荣幸地获得了IEEE最佳论文奖,推荐你去读一读。
|
||||
|
||||
今天我就带你从这个场景的源头出发,一步步探索到最后的具体落地方案。
|
||||
|
||||
## 为什么需要在生产环境中进行容量测试?
|
||||
|
||||
既然我们要解决一个具体的问题,那我们一定要先问个“为什么”。
|
||||
|
||||
为什么要在生产环境中进行容量测试呢?要回答这个问题,我们还得再往前追问,为什么说真实的容量测试很重要呢?
|
||||
|
||||
我想这个问题不难回答,你应该已经有自己的答案了。
|
||||
|
||||
我们都知道,一个在线互联网公司的存活和发展,靠的是它提供的互联网在线服务,自然也就依赖于这些服务的性能和稳定性。要保证每个服务都能够稳定地运行,我们必须为之提供足够的服务容量,比如适当数量的服务器。
|
||||
|
||||
那么要如何保证服务容量足够呢?首先就必须做好**服务容量的预测**。
|
||||
|
||||
要预测服务容量,我们就需要做容量的性能测试。一般是先确定每台单独的服务器可以支撑多少服务流量;然后用这个单台服务器的数据,来决定这个服务整体需要多少台服务器。这种测试其实就是我们前面讲过的容量测试。
|
||||
|
||||
举个具体例子,如果测试结果表明,一台服务器最多可以支撑100个QPS,那么要满足100K QPS的服务需求,总共就需要部署一千台服务器。
|
||||
|
||||
讲到这里,我想再补充几句。我们讲了这么多服务容量的事儿,那“服务”到底都指的是什么呢?
|
||||
|
||||
一般来说,公司提供的服务大致上分为两种:前端服务和后端服务。前端服务是什么样呢?包括各种服务的登陆页面和移动App,这些服务会直接影响用户的体验。后端服务呢,一般是为前端服务和其他后端服务提供数据和结果。后端服务可以有多种,比如键值数据存储服务(例如,Apache Cassandra),和公司内部的各种微服务。
|
||||
|
||||
服务容量预测的重要性我们了解了,那测试为什么需要在生产环境中进行呢?
|
||||
|
||||
你是不是想到了,在非生产环境中的容量测试,执行起来肯定更简单啊!没错,的确会更简单,但是,在实验环境或者其他非生产环境中做这样的测试,比如采用人工合成的流量负载,会非常不准确。
|
||||
|
||||
这是因为实际的生产环境里面,有多个特殊因素会导致和非生产环境中不同的结果。比如:
|
||||
|
||||
1. 客户需求会随着时间而变化,例如高峰时段与非高峰时段的流量就很不一样;
|
||||
1. 用户请求的多样性,例如不同国家的查询类型不同;
|
||||
1. 负载流量的规模,基础架构设施的变化,例如服务的软件版本更新,微服务互相调用的变化等等。
|
||||
|
||||
所以,对一个重要而复杂的互联网在线服务,由于难以在非生产环境中进行准确的容量测试,我们经常需要转向真正的生产环境,使用实时而真实的客户流量负载来测试。
|
||||
|
||||
所以,想要在非生产环境中进行准确的容量测试基本上是做不到的。而对一个重要而复杂的互联网在线服务,能够做到准确的容量测试又太重要了。因为准确的容量数据,是保证线上服务的可靠运行和控制公司成本的基础。
|
||||
|
||||
那怎么办呢?这时候我们就需要转向真正的生产环境,使用实时而真实的客户流量负载来测试。
|
||||
|
||||
## 如何在真实生产环境中进行容量测试?
|
||||
|
||||
那在真实生产环境中进行容量测试,要如何做呢?
|
||||
|
||||
一般来说,我们需要把生产环境的流量进行重定向,让这些重定向的流量,实时地驱动运行SUT的单个或者几个服务器。根据重定向的流量大小,会产生不同级别的流量负载。通过仔细地操作重定向的多少,并且把握测试的时间,我们可以获得非常准确的运行SUT服务器的容量值。
|
||||
|
||||
但是,生产环境中的容量测量也有诸多挑战,你需要特别小心。
|
||||
|
||||
第一个挑战是需要**设计一个控制重定向流量的机制**。这个机制要能够根据其他一些参数,来调整重定向的流量多少。
|
||||
|
||||
第二个挑战(也是最大的挑战)是这种**重定向生产流量可能会影响真正的客户**。因为我们会把客户请求重定向到某个服务器,并且会不断给服务器加压,直到这个服务器接近超载,那么这个服务器上所有的客户请求的延迟都会受影响,也就是可能会变大,用户性能也就可能会受到损害。
|
||||
|
||||
为了尽量减少对客户的影响,我们的容量测试需要设计合适的机制,来将这种可能的损害降到最低点。系统里面必须有一个模块,来不断地监测客户的性能;一旦到达临界点,就停止继续加压的操作,甚至适当减压。这就是所谓的“非侵入性”(Non-Intrusive)。
|
||||
|
||||
还有一个挑战是**对于测试时间的控制**。既然重定向生产流量可能影响客户性能,当然是测试的时间越短越好。可是测试时间太短的话,又可能会影响数据的稳定性。
|
||||
|
||||
为了应对这些挑战,准确地确定服务容量的极限,并精确定位容量瓶颈,LinkedIn采用了一个解决方案,我们将它命名为RedLiner。
|
||||
|
||||
宏观来讲,Redliner是固定一个生产环境中的SUT,这个SUT包括服务器和上面运行的被测服务,然后不断地把其他服务器上的流量,重定向到这个SUT服务器上面。随着流量的不断增大,这个SUT服务器的资源使用也就越来越多,所服务的客户请求的性能,比如端到端延迟,就会越来越差。
|
||||
|
||||
直到客户请求的性能差到一个定好的阈值,比如端到端延迟是200毫秒,流量重定向才会停止。这个时候,基本就可以确定SUT服务器不能再处理任何额外的负载。此时获得的容量结果就是SUT服务器的最大容量。
|
||||
|
||||
想要实现上述的测试,需要好几个模块一起合作。不过在讲RedLiner具体的各个模块之前,我们要先梳理一下合理方案的设计原则,一共是五条:
|
||||
|
||||
1. 要使用**实时流量**以确保准确性。
|
||||
1. 尽量不影响生产流量,这就需要**实时的监测和反馈**模块。
|
||||
1. 可以**定制重定向**等行为规则;对不同的服务和不同的场景的测试,各种性能指标和阈值都会不同。
|
||||
1. 能**自动终止测试**,并把**测试环境复位**,尽量减少人工干预。
|
||||
1. 支持基于日历和事件的**自动触发和调度**。
|
||||
|
||||
那这个方案具体是怎么实现的呢?
|
||||
|
||||
现在我们来看看解决方案的高层架构,如下图所示,这个方案主要包括四个组件:
|
||||
|
||||
- 核心控制器LiveRedliner
|
||||
- 重定向流量的TrafficRedirector
|
||||
- 收集性能数据的PerfCollector
|
||||
- 分析性能数据的PerfAnalyzer
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/60/a3/60eb47fd1b2749af61715eaa7dda93a3.png" alt="">
|
||||
|
||||
我先来一一给你介绍下这四个关键组件。
|
||||
|
||||
1.**核心控制组件LiveRedliner**
|
||||
|
||||
核心控制组件是整个系统的神经中枢,负责总体调度容量测试的过程。比如何时发起测试,何时终止测试,何时需要增加更大的流量等等。
|
||||
|
||||
用户可以自己定义一些特殊的规则,来更好地控制整个测试。用户可以自定义性能指标的阈值。这些性能指标,可以是用户的端到端延迟,包括各种统计指标,比如P99。也可以把几个不同的性能指标组合起来,实现复杂的逻辑,比如“端到端延迟不超过200毫秒,并且错误率不超过0.1%等”。
|
||||
|
||||
2.**重定向流量组件TrafficRedirector**
|
||||
|
||||
重定向流量组件负责对生产流量进行重定向。具体的实现机制,根据被测试的服务类型分为两种:前端服务和后端服务。
|
||||
|
||||
用于**前端服务**时,Redliner是通过客户请求的属性(例如用户ID、语言或帐户创建日期),来决定是否对一个客户请求来进行重定向的。这个转换也很简单,可以是取模机制。
|
||||
|
||||
举个例子来说,如果Redliner需要重定向1%的流量,它可以把用户的一个属性比如userid转换成整数,然后执行用100来取模的操作,并且和一个固定整数值作比较。
|
||||
|
||||
用于**后端服务**时,重定向流量可以通过另外一个叫做“资源动态发现和负载均衡”的模块来实现。在LinkedIn,我们很多的服务负载均衡机制,一般会采用一个服务器列表(URI集群),以相对应的权重值来决定一个请求发送到哪个服务器。
|
||||
|
||||
假设这样一个机制有10个可用的URI集群,并且最初所有这些集群都接收等量的流量(即每个URI的权重为10%)。如果Redliner决定将20%的流量重定向到特定的URI(即SUT),那么它可以为SUT的URI分配20%的权重。
|
||||
|
||||
3.**性能数据收集组件PerfCollector**
|
||||
|
||||
容量测试必须采集各种类型的性能指标,例如CPU,内存和QPS等。这些性能指标的作用,就是确定SUT何时达到其容量最大值,以及容量值是多少。
|
||||
|
||||
PerfCollector组件负责收集各种性能指标,包括系统级和服务级的指标。这个组件运行在所有受监视的节点。组件传递的数据量通常很大,因为一般要监测较多的性能指标。所以最好采用扩展性好的实时消息传递系统,来把这些性能数据及时传到其他组件,尤其是下面要介绍的性能分析组件PerfAnalyer。
|
||||
|
||||
我们采用的消息传递系统是[Kafka](https://time.geekbang.org/column/intro/100029201)。Kafka也是由LinkedIn设计并开源的,扩展性和性能都很好,建议你也尝试采用。
|
||||
|
||||
4.**性能分析组件PerfAnalyzer**
|
||||
|
||||
收集性能指标是第一步,下一步就是分析性能,并采取相应的措施。具体来说,可以根据性能指标的值来确定SUT是否饱和。
|
||||
|
||||
根据性能数据和用户定义的规则,如果发现当前的SUT,仍有空间来承担更多的负载,我们可以将更大比例的实时流量重定向到该SUT。否则,如果SUT显示饱和迹象,那么重定向的流量百分比就应该降低。PerfAnalyzer组件能分析收集的数据,并确定是否有特定指标是否违反用户定义的规则。
|
||||
|
||||
Redliner作为一个完整的解决方案,通过几个模块互相配合来实现真实的线上容量测试。通过动态地调整线上的流量,直到让被测系统临近超载状态,从而获得准确的单位服务容量。
|
||||
|
||||
现在我们再来换个角度,看一下RedLiner的具体操作流程。流程图如下所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f1/51/f1da037ebb3a7e3a98fcdf31b7a37851.png" alt="">
|
||||
|
||||
首先是**跟据以前的测试历史数据,决定一个初始重定向数量,也就决定了SUT的初始负载**。理论上讲,每个测试都可以从0开始;但是如果起点流量太低,整个测试需要花很长时间,所以最好能利用历史数据,从一个比较高的起点开始。
|
||||
|
||||
在使用历史数据的基础上,我们还需要**决定总体测试时间**。决定了时间后,每次调整重定向流量的百分比也就确定了。这个调整的数值大小也就是所谓的“步距”,如同人迈步走路,每一步都有大小。对每个百分比,我们一般固定测试3分钟,让数据稳定下来,然后调用PerfAnalyzer分析并决定下一步。
|
||||
|
||||
举个例子,假如我们决定总体测试60分钟,并且是从0%开始。因为每一个百分点需要测试3分钟,那么我们就会决定调整的“步距”大小是5%,因为最大就是100%。如果PerfAnalyzer决定需要继续增加或者减少重定向百分比,那么就按照前面决定的步距,进行相应的调整。
|
||||
|
||||
## 一起来看两个生产环境的数据
|
||||
|
||||
刚才讲了一大堆如何实现这个解决方案的内容,现在我们来看看两个生产环境中的实际数据,来直观地感受一下这个系统的特点。下图显示了典型Redliner运行的特征。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/0f/b532a67de294378cd7ee9bf4ede3b00f.png" alt="">
|
||||
|
||||
这是一次完整的容量测试,持续了一个小时。
|
||||
|
||||
第一幅图是QPS,也就是系统吞吐量。蓝色线,表示SUT有望实现的目的QPS。红色线,是SUT实现的实际QPS。我们可以看到,实际的QPS值持续变化,这个变化表明了Redliner的控制和探测过程,就是一直在动态增加和减少重定向流量百分比。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/53/45/53a13bbb560a6c03ac89507e5e42c845.png" alt="">
|
||||
|
||||
第二幅图,标识出了所测量的客户查询等待时间的中值。
|
||||
|
||||
比较这两幅图,你可以看到,开始测试的阶段,RedLiner不断提高重定向百分比,实际的流量持续增加;同时客户感受的查询等待时间也慢慢加大。
|
||||
|
||||
等到SUT不堪重负时,查询等待时间也就太大了,超出了客户能接受的阈值(40毫秒)。所以,RedLiner决定逐步降低重定向百分比,最后重置到初始状态。
|
||||
|
||||
## 总结
|
||||
|
||||
我们这一讲介绍了一个在生产环境中,进行真实场景压力和容量测试的方案,这里面的关键点,是**逐步而智能地把一部分流量重定向到被测试的系统上面**。
|
||||
|
||||
这个案例是我们在领英的生产实践,但我觉得在你的公司里实现这么一个类似的系统一点也不难。希望这些分享能帮助你设计和实现。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2d/94/2d5aac1c39d5c8ce8467d5752f080e94.png" alt="">
|
||||
|
||||
唐代诗人白居易的《长相思》最后的几句是:“愿作远方兽,步步比肩行。愿作深山木,枝枝连理生。”说的是主人公愿意与心爱的人相守到老,哪怕是做山林中的野兽和树木,一起步步连心,亦步亦趋,比肩而行,并肩而居。
|
||||
|
||||
我们用实际的生产负载做容量测试时,要小心控制重定向的流量“步距”大小,尽量“小步勤挪”,并且实时地观测,才不会影响客户的体验,并且得到比较准确的结果。
|
||||
|
||||
## 思考题
|
||||
|
||||
你们公司有没有类似的解决方案,来准确地测量一个服务需要的容量呢?如果有,和我讲的具体方案有何异同?不同的地方是基于什么考虑呢?
|
||||
|
||||
如果没有,你可以考虑实现一个,我相信一定会让老板对你另眼相看的。
|
||||
|
||||
欢迎你在留言区分享自己的思考,与我和其他同学一起讨论,也欢迎你把文章分享给自己的朋友。
|
||||
182
极客时间专栏/geek/性能工程高手课/性能工程实践/26 | 怎么规划和控制数据库的复制延迟大小?.md
Normal file
182
极客时间专栏/geek/性能工程高手课/性能工程实践/26 | 怎么规划和控制数据库的复制延迟大小?.md
Normal file
@@ -0,0 +1,182 @@
|
||||
<audio id="audio" title="26 | 怎么规划和控制数据库的复制延迟大小?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/38/0c/38196cd5ac5de8b76e29e825c142a60c.mp3"></audio>
|
||||
|
||||
你好,我是庄振运。
|
||||
|
||||
在正文开始前,首先要祝你新年快乐!今天我们要通过我在LinkedIn(领英)公司做过的一个项目,来学习如何控制数据库复制延迟。
|
||||
|
||||
我在LinkedIn工作的时候,就遇到过因为数据复制延迟太大而导致的生产事故。当时出现了这样的情况:一个用户刚刚更新了自己的照片,可是他的朋友们却迟迟看不到更新。
|
||||
|
||||
你是不是觉得LinkedIn又不是微博,看个照片而已,早点晚点也没什么大不了的?
|
||||
|
||||
但换个内容,这性质就严重了。LinkedIn网站上面会播放广告,在一定时间内,广告商往往会设置一定的广告投放预算限额,比如一天一万元。如果广告投放的收入统计数据被延迟,就会导致很严重的统计错误,实际支出严重地超过预算。一切有关钱的事,可都是大事。
|
||||
|
||||
所以,从用户数据的有效性、时间性和一致性来考虑,数据的传输复制延迟当然是越小越好。
|
||||
|
||||
## 什么是数据库的复制延迟?
|
||||
|
||||
那到底什么是数据库的复制延迟呢?如果让我用一句话来说,数据库的复制延迟其实就是当线上服务需要多个数据库时(比如为了分散流量),一条信息从源头数据库传递复制到下游数据库时经过的延迟。
|
||||
|
||||
还是用LinkedIn的例子来说明。LinkedIn的全球用户已经超过5.9亿,用户流量异常庞大。这些用户流量,是由用户的各种活动而生成的事件数据。LinkedIn的系统要求是,实时地捕获这些用户数据,并不断存储在后台数据库中。这些存放在数据库中的用户数据很重要,会被各种应用程序和其他服务读取,比如广告投放就需要消费这些数据。
|
||||
|
||||
在消费这些数据的时候,理论上来说,数据的消费者可以直接连接到后台数据库,去读取这些数据。但是,这些用户数据流具有大数据的几个特征,包括大规模和高可变性,并不适合直接连接后台数据库。因为,这会对后台数据库造成很大压力,而且不能保证读取的延迟。
|
||||
|
||||
为了解决这一挑战,当今主要的互联网公司,往往部署一整套的数据处理系统。这一整套系统通常由几个模块构成,包括数据事件的捕获、数据的存储、数据的复制传输、数据的读取。
|
||||
|
||||
在这个系统中,**数据的复制传输**的作用很特别,是用来隔离数据库和数据使用者的。
|
||||
|
||||
采用数据复制传输模块的好处非常大,它可以减轻源头数据库的压力。因为复制传输可以进行级联,分散了用户流量,能够让系统的可扩展性更好。但这里有个前提,那就是,要保证数据的复制延迟不能太大,否则会造成很糟糕的业务影响,比如影响广告业务的收入。
|
||||
|
||||
我们今天就针对这个数据复制传输系统,来探讨如何通过合适的容量规划和分析,来控制数据的复制延迟。
|
||||
|
||||
要想理解后面要讲到的容量规划,你得先去**了解生产环境中的用户流量特点**才行。
|
||||
|
||||
下面的图显示了一个互联网服务的在线用户流量。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/97/18468a1e9d01b03525183ee05ec67597.png" alt="">
|
||||
|
||||
时间范围是连续的六周,也就是42天的流量。你可以看到,用户流量呈现出了非常强的重复模式:基本上是以一周为一个周期,每周内五个工作日流量比较大,而周末的两天流量比较小。而且无论哪天,每天内部都有一个峰值。
|
||||
|
||||
我们接着研究每一天内流量的周期性模式。对于每个工作日和周末,流量的形状也是非常有规律的曲线。下图显示了一个工作日中,也就是24小时内的流量变化。你可以清楚地看到,每天都会有一段时间的高峰期,约为8小时。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/e7/2bbe9265fdb91c6364ead53533c46ae7.png" alt="">
|
||||
|
||||
我们也对比了工作日和周末的流量变化,结果也是毫不奇怪的:工作日峰值一般远高于周末的峰值。二者的峰值之间,差不多有4倍的差距。
|
||||
|
||||
你只有了解了用户流量特征,才能在实际操作中选对模型。
|
||||
|
||||
了解了生产环境中的用户流量特征后,你还要了解LinkedIn采用的数据传输复制系统才行。如下图所示,LinkedIn的系统有如下几个模块:事件生成、数据库存储、数据复制/传输和数据的消费/读取。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/6c/8e71357e44827c3c35075cebb041896c.png" alt="">
|
||||
|
||||
具体来讲,当用户与公司的网页互动时,相应的用户更新事件就被发送到了数据库。这些事件包括:用户点击了其他链接、阅读了其他用户的动态、向其他用户发信息等等。每个用户事件都由数据复制模块传输,并提供给下游消费者服务。
|
||||
|
||||
同一个用户数据,可能被很多应用程序和服务模块读取,所以这样上下游级联的系统设计,有比较好的扩展性,可以轻松应对规模的扩展。比如,如果数据复制模块不堪重负,而成为性能瓶颈,那么可以采用发散式级联的方式来扩展,从而分散读取的流量。
|
||||
|
||||
需要注意的是,**数据传输复制模块除了可以提供高扩展性,也可以提供数据的一致性**。像LinkedIn这样的公司,服务的用户遍布全球,在全球也就有很多数据中心。因为互联网流量分布在多个数据库或多个数据中心,所以就需要一个整合而一致的数据视图。这样一个目的,是可以通过传输复制实时数据库事件来得到的。
|
||||
|
||||
这个系统的另外一个特性是,**虽然用户流量可能随着时间变化很大,但是数据复制和传输的能力相对稳定**。我们通过测量和观察,发现复制传输模块的吞吐量能力非常稳定,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/57/2693bcbbf13ac5659cae8013d4d5c757.png" alt="">
|
||||
|
||||
那么面对不断变化的客户流量(也就是事件流量),我们要如何规划整个系统,来让稳定的数据复制能力去适应用户流量的不稳定性呢?
|
||||
|
||||
## 怎么解决数据库复制延迟问题?
|
||||
|
||||
要达成目的,我们必须做好容量规划和分析。
|
||||
|
||||
要减少数据库复制延迟,我们在进行容量规划的时候需要考虑几个因素,包括**用户流量**、**复制模块的吞吐量**、**复制的延迟**以及**复制延迟的SLA**(Service Level Agreements,服务水平协议);从而确保所需的复制延迟,不要超过SLA的规定。
|
||||
|
||||
另外,通过充分考虑用户流入的流量和复制容量,我们还可以做出一些预测,比如未来一定时间内的预期数据复制延迟。而且,大多数互联网公司的流量往往有不断增长的趋势,我们需要持续地提高复制处理能力,来应对数据流量的增加。
|
||||
|
||||
数据库复制的容量规划和分析,其实就是在三个基本变量中辗转腾挪。哪三个变量呢?就是**用户流量大小**、**传输复制容量的能力**和**传输复制的延迟**。给定任何两个变量,都可以确定第三个变量。
|
||||
|
||||
具体来说,数据库复制的容量规划和分析可以帮助回答以下几个问题:
|
||||
|
||||
1. 未来的流量预测
|
||||
1. 数据复制延迟的预测
|
||||
1. 确定数据复制的容量
|
||||
1. 确定用户流量的增长空间
|
||||
1. 帮助确定延迟SLA
|
||||
|
||||
未来的流量预测,也就是根据历史流量的数据,预期未来的流量(这个问题也能帮助回答后面的问题)。
|
||||
|
||||
数据复制延迟的预测,也就是给定传入流量和复制处理能力,预期的复制延迟是多少?这些数据,可以帮助我们确定复制延迟的SLA。
|
||||
|
||||
确定数据复制的容量,就是在假设给定传入流量和最大允许的复制延迟(SLA)的情况下,确定我们需要部署多少数据复制容量?这将有助于定义复制容量需求。
|
||||
|
||||
确定用户流量的增长空间,就是在给定复制的容量和延迟的SLA的情况下,确定最大可以支持多少用户流量?这有助于计划将来的容量要求。
|
||||
|
||||
帮助确定延迟SLA,就是在给定输入流量,现在或将来的复制处理能力的情况下,如何确定适当的SLA?显然,作为一个公司或者部门,我们不想过度承诺或低估SLA。
|
||||
|
||||
## 解决方案如何落地?
|
||||
|
||||
那么这具体是如何操作的呢?现在我来为你介绍一下我们采用的规划和分析模型(这是基于统计模型的)。
|
||||
|
||||
你要知道,数据的传输复制模块,其实是一个排队系统,是一个**有无限缓冲的先进先出队列**。
|
||||
|
||||
从输入角度讲,所有的用户流量数据都进入这个先进先出队列,然后传输复制模块会一个一个地处理。如果在任何时候,用户流量大小超过传输能力,那么队列就会加长。反之,队列就会变短。不过与典型的排队论问题不同,在这里,我们会更加**关注所有事件的“最长”等待时间,并根据这个值来决定SLA**。
|
||||
|
||||
你还记得我们在最开始对用户流量特征的观察吗?基于前面对流量的观察和测量,你认为该用什么模型来预测未来数据呢?
|
||||
|
||||
没错,最好把用户流量看作是一个时间序列模型。并通过这个模型来预测未来的数据。
|
||||
|
||||
对于这类时间序列数据,通常选择**ARIMA**(Autoregressive Integrated Moving Average,自回归积分移动平均线)模型,来进行建模和预测。什么是ARIMA呢? ARIMA是一个常见的统计模型,是用来对时间序列进行预测的模型。它的全称是自回归移动平均模型(ARIMA, Autoregressive Integrated Moving Average Model)。
|
||||
|
||||
ARIMA的工作机制有较大的计算开销,所以ARIMA不太适合大规模的建模,比如超过几百个数据点就不太合适了。一般来讲,对于一年内的预测,ARIMA只能预测到每天的尺度,因为一年只有365天。对这个模型来讲,比天更细的粒度,不太适合。但是我们这里的容量规划,又偏偏需要获取更加精细粒度的数据,比如每小时的预测数据,而不仅仅是每天的数据。
|
||||
|
||||
考虑到这几个特点,对于较长期的预测,比如半年期间的话,该怎么办呢?我们提出了**两步预测模型**来获取未来的每小时流量。
|
||||
|
||||
简而言之,这个两步预测模型采取两步走的办法。
|
||||
|
||||
第一步是工作在**星期**这个粒度上,获取每周的聚合流量;第二步再将聚合数字“分布”(或“转换”)到一天内的每个小时。这样做的好处是,一方面减少了数据的点数,比如一年也就52周;另一方面仍然可以得到每小时的数据。
|
||||
|
||||
你可能想问,这个第二步的转换,也就是流量从一周到小时的“转换”,是怎么做的呢?事实上,我们采用了一个**季节性指数**,该指数大致代表一周内每小时的流量部分。你也可以直观地把这个指数看作是一个分配函数。
|
||||
|
||||
下图就是一周到小时的季节指数展示。一周内有168小时,也就是有168个数据点。这些数据点的值大小,其实就是分布概率。所有的数据点的值加在一起正好等于1。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/91/1ddcfe432bf88d0729dbeb9def89c591.png" alt="">
|
||||
|
||||
具体来说,该模型包括两个步骤:
|
||||
|
||||
1. 使用ARIMA模型,预测未来几周的每周总体流量;
|
||||
1. 使用季节指数,预测一周内每个小时的流量。
|
||||
|
||||
采用这个模型就可以回答我们前面提出的问题了,比如可以轻松地预测未来每小时的流量。
|
||||
|
||||
那么具体的算法怎么实现呢?比如回答与容量规划相关的其他几种类型的问题,我们采用了**数值计算**和**二进制搜索**的类似机制。
|
||||
|
||||
这里的二进制搜索原理,不难理解,其实就是不断地尝试,直到找到一个最合适的值为止。比如我们需要一个确定总的容量,一方面希望容量越小越好(因为省钱);另一方面又不能违反传输复制延迟的SLA。
|
||||
|
||||
怎么用二进制搜索呢?
|
||||
|
||||
就是随便假定一个容量值,然后带入模型去推导出传输复制的延迟,然后判断这个延迟是否违反了SLA。如果违反了,说明我们一开始的容量值太小,应该增加一点。怎么寻找呢,就是每次取中间的值,重新推导。这个过程如同二进制搜索。
|
||||
|
||||
为了便于理解,我们假设时间粒度为每小时。一旦获得流量,则将使用数值计算方法,来判断任何时间点的复制延迟。基于数值计算的结果,也就可以使用二进制搜索,来获得特定流量所需的数据复制容量。
|
||||
|
||||
同样的道理,还可以用此模型,来确定任何固定场景可以支持的最大用户流量,以及相应的将来日期。也就是根据预测的增长,未来什么时候用户的流量就会超越这个最大用户流量。
|
||||
|
||||
最后,检查每种场景下的预期复制延迟,我们还可以确定适当的复制延迟SLA。
|
||||
|
||||
任何针对流量的预测模型,都不可避免地会出现误差。产生预测误差的主要原因是网络流量的高变化性。
|
||||
|
||||
当实际流量小于预测流量时,实际的复制延迟也将低于预期延迟。注意,这种低估误差不会违反基于预测值定义的延迟性能SLA。唯一付出的成本,是部署了一些额外的资源。但是,当预测的流量小于实际流量时,就可能会违反延迟SLA。
|
||||
|
||||
为了解决此问题,在确定各种指标(例如SLA)时,有必要**预留一定的空间来避免预测误差**。预留的量取决于一系列因素,包括:
|
||||
|
||||
1. 预测模型的历史表现,也就是预测的精准度;
|
||||
1. 违反延迟SLA的后果,包括业务付出的成本;
|
||||
1. 过度部署容量资源的成本。
|
||||
|
||||
好了,到这里这个规划和分析模型就讲完了,内容比较多,我们来复习一下。对于数据库复制系统来说,可以把它当作一个排队系统来对待。不断到来的输入流量,是一个有季节性的不断变化的时间序列。通过对这个时间序列来适当建模,我们可以比较准确地预测未来的输入流量。有了预测的输入流量,就可以根据服务处理的速度和复制延迟的关系,来计算出其他结果:比如预期的复制延迟,所需要的复制容量等等。
|
||||
|
||||
## 实际生产实践的验证
|
||||
|
||||
我们在LinkedIn的生产实践中,采用了这个规划,下面我们来看看一些结果。
|
||||
|
||||
我们还是使用前面介绍的42天数据,来做未来的流量预测。使用ARIMA模型的实践建议是,预测数据的长度最好不要超过历史数据长度的一半。所以,既然我们有42天的历史数据,通常可以在未来的21天之内,使用ARIMA模型进行预测。
|
||||
|
||||
下图展示了这21天的两个数据,一个是生产环境中的实际数据,另外一个是根据我们模型的预测值。蓝颜色的线条是实际观察到的用户流量,绿颜色的线条是我们的预测值。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/04/b3/04f88352b68339474a30f983631672b3.png" alt="">
|
||||
|
||||
从图中可以观察到,虽然有些地方还是有误差,但是从解决实际问题的角度,我们认为已经足够了。我们也把ARIMA方式和其他预测方式做了比较,比较预测的时间序列结果,我们发现ARIMA模型给出了更好的精度;但差异不大,仅仅有6%左右的差异。
|
||||
|
||||
## 总结
|
||||
|
||||
唐代诗人杜甫有两句诗说:“迟日江山丽,春风花草香。”说的是春天越来越长,沐浴在春光下的江山格外秀丽,春风也送来花草的芳香。数据传输复制的延迟,如果不仔细控制,也会变得越来越长,最终会导致严重问题。到时候就不是春风送暖和花草飘香,而是秋风萧瑟,老板发威了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/8a/c376d8c69669920a04ea41bd60d7128a.png" alt="">
|
||||
|
||||
我们今天探讨了如何通过合理的规划和分析,来控制这个数据库复制延迟。具体就是使用ARIMA统计模型,并且用两步走的策略,来回答一系列的各种相关问题。
|
||||
|
||||
我想特意说明的是,这一整套时间序列的预测原理和方法,其实也可以用在很多其他的很多种预测场景中。比如一个在线广告显示系统(输入的流量是线上用户活动量,要进行的处理是决定合适的广告来显示)。
|
||||
|
||||
通过今天的分享,我希望你能掌握的要点是对于一个线上的数据处理系统,我们可以进行基于排队理论和时间序列的建模,通过这个建模来回答各式各样的相关问题。这个方案其实还牵涉到一些稍微复杂的统计理论,如果你还有精力,我建议你去读一下我的一篇论文,发表在[ACM ICPE](https://dl.acm.org/citation.cfm?id=2688054)上面,里面阐述了所有的细节,你在阅读的过程中有什么问题或者思考,也可以留言和我讨论。
|
||||
|
||||
## 思考题
|
||||
|
||||
如果数据的传输复制延迟过大,会造成很多种不同场合的业务影响,都会有什么样的业务影响呢?
|
||||
|
||||
提示:假如你们公司的网站上面帮助别人投放广告,如果广告的数据有延迟,会造成广告商和你们公司之间的什么样的纠纷呢?
|
||||
|
||||
欢迎你在留言区分享自己的思考,与我和其他同学一起讨论,也欢迎你把文章分享给自己的朋友。
|
||||
185
极客时间专栏/geek/性能工程高手课/性能工程实践/27 | 多任务环境中的Java性能问题,怎样才能不让程序互相干扰?.md
Normal file
185
极客时间专栏/geek/性能工程高手课/性能工程实践/27 | 多任务环境中的Java性能问题,怎样才能不让程序互相干扰?.md
Normal file
@@ -0,0 +1,185 @@
|
||||
<audio id="audio" title="27 | 多任务环境中的Java性能问题,怎样才能不让程序互相干扰?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/82/92/8257fec023b6d2a472ef2989495f1792.mp3"></audio>
|
||||
|
||||
你好,我是庄振运。
|
||||
|
||||
我们来继续学习生产实践中的案例。在生产实践中,为了降低公司运营成本,更好地利用系统容量,并提高资源使用率,我们经常会让多个应用程序,同时运行在同一台服务器上。
|
||||
|
||||
但是,万事有利就有弊。这几个共存的应用程序,有可能会互相影响;有时还会导致严重的性能问题。我就遇到过,几个程序同时运行,最后导致吞吐量急剧下降的情况。
|
||||
|
||||
所以,今天我们就来探讨,当多个Java应用程序共存在一个Linux系统上的时候,会产生哪些性能问题?我们又该怎么解决这些问题?
|
||||
|
||||
## 怎样理解多程序互相干扰?
|
||||
|
||||
为了更好地理解后面的性能问题,你需要先了解一下应用程序内存管理机制的背景知识。我们运行的是Java程序,所以先快速复习一下**Java的JVM内存管理机制**。
|
||||
|
||||
Java程序在Java虚拟机JVM中运行,JVM使用的内存区域称为**堆**。JVM堆用于支持动态Java对象的分配,并且分为几个区域,称为“代”(例如新生代和老年代)。Java对象首先在新生代中分配;当这些对象不再被需要时,它们会被称为GC(Garbage Collection)的垃圾回收机制收集。发生GC时,JVM会从根对象开始,一个个地检查所有对象的引用计数。如果对象的引用计数降为零,那就删除这个对象,并回收使用这个对象相应的存储空间。
|
||||
|
||||
GC运行的某些阶段,会导致应用程序停止响应其他请求,这种行为,通常称为STW(Stop The Word暂停)。 JVM调优的重要目标之一,就是最大程度地减少GC暂停的持续时间。
|
||||
|
||||
复习完JVM内存管理机制,我们还要看一下与它相关的**Linux的内存管理机制**。
|
||||
|
||||
在Linux操作系统上,虚拟内存空间基本上是固定大小(例如4KB)的页面。Linux近年来有很多内存管理的优化,来提高内存使用效率和运行进程的性能。
|
||||
|
||||
Linux内存管理有一个**页面回收**的机制。它在内部维护一个空闲页面(Free Page)列表,来满足未来应用程序的内存请求。当空闲页面的数量下降到一定水平时,操作系统就开始回收页面,并将新回收的页面添加到空闲列表中。
|
||||
|
||||
执行页面回收时,操作系统需要进行页面扫描(Page Scanning),以检查已经分配页面的活动性。Linux有两个策略来进行页面扫描:**后台扫描**(由kswapd守护程序执行)和**前台扫描**(由进程自己执行)。
|
||||
|
||||
通常情况下,后台扫描就够了,应用程序的性能一般不会受到影响。但是当操作系统的内存使用非常大,空闲页面严重不足时,Linux就会启动前台页面回收,也被称为**直接回收或同步回收**。在前台页面回收过程中,应用程序会停止运行,因此对应用程序影响很大。
|
||||
|
||||
Linux还有一个**页面交换**(Page Swapping)的机制。是当可用内存不足时,Linux会将某些内存页面换出到外部存储,以回收内存空间来运行新进程。当对应于换出页面的内存空间,再次处于活动状态时,系统会把这些页面重新从外部存储换入内存。
|
||||
|
||||
内存管理方面,THP(Transparent Huge Pages,透明大页面)是另外一个机制,也是为了提高进程的性能。我们在[第22讲](https://time.geekbang.org/column/article/189200)讨论过,如果系统用较大的页面,比如2MB,而不是传统的4KB,那么会带来一些好处,尤其是所需的地址转换条目数会减少。
|
||||
|
||||
尽管使用大页面的好处很早就为人所理解,但在THP引入之前,程序想使用大型页面并不容易。例如,操作系统启动时,需要保留大页面,并且进程必须显式调用才能分配大页面。而THP就是为了避免这两个问题而设计的,因此操作系统默认情况下就启用THP。
|
||||
|
||||
了解了背景知识,你再看多个应用程序共存时的两个场景就不会有障碍了。第一个场景是应用程序启动时,第二个场景是应用程序稳定运行时。
|
||||
|
||||
## 应用程序启动时为什么会被其他程序干扰?
|
||||
|
||||
我们先看应用程序启动时的场景。当几个共存的应用程序共享有限的计算资源(包括内存和cpu)时,它们之间会相互影响。如果各自独立地运行,导致对系统计算资源的消耗无法协调一致,那么某些应用程序会出现问题。
|
||||
|
||||
我们要做个实验来暴露这些性能问题,看看这个问题的表象是什么,然后一起分析产生问题的原因。
|
||||
|
||||
这个实验采用了两个相同的Java程序。我们首先启动第一个程序,来占用一些内存,系统剩下约20GB的未使用内存。然后我们开始启动另外一个Java程序,这个程序需要20GB的堆。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/87/250f92b847f23868da8910e7134e2687.png" alt="">
|
||||
|
||||
在图中你可以看到,启动第二个程序之后,它的吞吐量是12K/秒,持续时间约30秒。然后,吞吐量开始急剧下降。最坏的情况,在大约20秒的时间内,吞吐量几乎为零。有趣的是,过了一会儿,吞吐量又再次回到了稳定状态:12KB/秒。
|
||||
|
||||
下图显示了同一时间段的GC暂停信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/d5/2e76dec3ff4a1928dd1162da13dabed5.png" alt="">
|
||||
|
||||
最初的GC暂停很低,都低于50毫秒;然后暂停就跳到数百毫秒之大。你甚至可以看到两次大于1秒的超大的暂停!大约1分钟后,GC暂停再次下降至低于50毫秒,并变得稳定。
|
||||
|
||||
我们看到在启动期间,Java程序的性能很差。因为问题是在启动JVM时发生的,我们有理由怀疑这与JVM的启动方式有关。我们检查了程序的的内存驻留大小(RES,Resident Size),也就是进程使用的未交换的物理内存,图示如下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/7b/d96aaba8c8ef2119901a914985170d7b.png" alt="">
|
||||
|
||||
从图中你可以看到,尽管我们在启动JVM时,用参数将JVM的堆大小指定为20GB(-Xmx20g和-Xms20g),但是JVM并不会从内存中一次全部拿到20GB的堆空间。相反,操作系统会在JVM的运行过程中不断地分配。也就是说,随着JVM实例化越来越多的对象,JVM会从操作系统逐渐拿到更多的内存页面来容纳它们。
|
||||
|
||||
在分配过程中,操作系统将不断地检查空闲页面列表。如果发现可用内存量低于一定水平,操作系统就会开始回收页面,这个过程会花费CPU的时间。根据可用内存短缺的严重程度,回收过程可能会严重阻塞应用程序。在下图中,我们看到,可用内存明显地下降到了非常低的水平。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/ec/db352ff5d0cf5ac1cb0a65e1d14d67ec.png" alt="">
|
||||
|
||||
下面这张图显示了CPU的空闲百分比(CPU空闲百分比和繁忙百分比的和是100%)。对比时间线,我们可以清楚地看到,页面回收过程会导致CPU开销,也就是空闲百分比下降了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b1/12/b1daec744f3ec004c0b48260b1d06d12.png" alt="">
|
||||
|
||||
那么怎么进行内存回收呢?
|
||||
|
||||
在Linux上,当可用内存不足时,操作系统会唤醒**kswapd守护程序**,开始在后台回收空闲页面。如果内存压力很大,操作系统就会被迫采取另外一种措施,就是**直接地同步释放内存的前台**。具体来讲,当可用空闲页面降到一个阈值之下,就会触发这种直接前台回收。
|
||||
|
||||
当发生直接前台回收时,Linux会冻结正在申请内存的执行代码的应用程序,从而间接地导致大量的GC暂停。
|
||||
|
||||
此外,直接回收通常会扫描大量内存页面,以释放未使用的页面。那么我们就来看看Linux直接回收内存页面的繁忙程度。下图就画出了Linux通过直接回收路径扫描的页面数。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/95/58acbaa1b20764b08a0cc7725cfff795.png" alt="">
|
||||
|
||||
我们看到,在峰值时,通过直接回收,每秒扫描约48K个页面(即200 MB);这个回收工作量是很大的,CPU会不堪重负。
|
||||
|
||||
## 运行中的应用程序为什么会被别的程序干扰?
|
||||
|
||||
了解过程序启动时互相干扰的场景,我们再来考虑第二个场景:应用程序在持续运行中。
|
||||
|
||||
我们的实验是这样进行的。第一个Java程序以20GB的堆启动,并进入稳定状态。然后另外一个程序启动,并开始分配50GB的内存。
|
||||
|
||||
下图中体现了第一个程序的吞吐量。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/45/a9b05fb6c7d9cec5085b86f06196f645.png" alt="">
|
||||
|
||||
从图中我们看到,第一个程序从一开始就实现了稳定的12K/秒的吞吐量。然后,吞吐量急剧下降到零,这个零吞吐量的过程持续了约2分钟。从那时起,吞吐量一直在发生相当大的变化:有时吞吐量是12K/秒,其他时候又降为零。
|
||||
|
||||
我们也观察了JVM的暂停,用下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ed/5d/eded62489c1d07997998bd460d8d8e5d.png" alt="">
|
||||
|
||||
从图中我们看到,在稳定状态下,GC暂停几乎为零,然后居然有一个超级大的暂停;多大呢?55秒!从那时起,GC暂停持续变化,但很少恢复为零。大多数暂停时间为几秒钟。
|
||||
|
||||
我们观察到,其他应用程序的运行会严重影响本程序的性能。各种观察的结论是,系统处于内存压力之下,操作系统内存会有很多和外部存储的页面交换活动。在下图中,我们看到操作系统交换出了很多内存页面到外部存储空间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/57/3f9408097dcdb03ec54c5f49a71f5657.png" alt="">
|
||||
|
||||
这些换出的内存页面很多属于Java程序(也就是堆空间)。如果JVM需要进行堆上的垃圾回收,也就是GC,那么GC需要扫描JVM对象,以收集失效的对象。如果扫描的对象恰好是分配在换出的页面上,那么JVM需要先将它们从外部存储交换空间重新载入到内存中。从外部存储载入内存需要一些时间,因为交换空间通常位于磁盘驱动器上。
|
||||
|
||||
所有这些时间,都会算在GC暂停之中。因此,程序会看到较大的GC暂停。下图就显示了大量的从外部存储载入页面的活动。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/62/e4207f1105b7b8ef720f1eb1cdc19a62.png" alt="">
|
||||
|
||||
尽管页面交换活动会增加GC暂停时间,似乎可以解释刚刚看到的JVM暂停。但是,我怀疑,仅是这个原因根本无法解释生产中看到的很大暂停,比如超过55秒的暂停。你可能会问,我为什么有这样的怀疑?因为我在许多GC暂停的过程中,观察到了较高的系统CPU使用。
|
||||
|
||||
比如在下图中,我们观察到,系统也处于严重的CPU压力下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/4c/f946efa1b98a07302d7fd2b52daf9b4c.png" alt="">
|
||||
|
||||
CPU的高使用率不能完全归因于页面交换活动,因为页面交换通常不会占用大量CPU。所以,其中“必有隐情”:一定是有其他活动在大量使用CPU。我们通过检查了各种系统性能指标,最终确定了根因:是由于THP的机制,该机制严重加剧了程序性能和系统性能的下降。
|
||||
|
||||
具体来说,Linux启用THP后,当应用程序分配内存时,会优先选择2MB大小的透明大页面,而不是4KB的常规页面。这一点我们可以轻易验证,比如下图中显示了透明大页面的瞬时数量。在峰值时,我们看到约34,000个THP,即约68GB的内存量。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/aa/0b948557e64c10edf711532d3d6647aa.png" alt="">
|
||||
|
||||
我们还观察到,THP的数量一开始很高,一段时间后开始下降。这是因为某些THP被拆分成小的常规页面,以补充可用内存的不足。
|
||||
|
||||
为什么需要拆分大页面呢?是因为当Linux在有内存压力时,它会将THP分为常规的、要准备交换的页面。为什么必需拆分大页面?这是因为当前的Linux,仅支持常规大小页面的交换。
|
||||
|
||||
拆分活动的数量我们也用下图画出来了。你可以看到,在五分钟内大约有5K个THP页面被拆分,对应于10GB的内存。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/3a/c07ba59fbb545d1e9cb9f11e650e1c3a.png" alt="">
|
||||
|
||||
除了大页面拆分,同时,Linux也会尝试将常规页面重新聚合为THP大页面,这就需要额外的页面扫描,并消耗CPU。如果你在实践中注意观察的话,可以发现这种活动会占用大量CPU。
|
||||
|
||||
使用THP可能遇到的更糟糕的情况是,**聚合**和**拆分**这两个相互矛盾的活动,是来回执行的。也就是说,当系统承受内存压力时,THP被拆分成常规页面,而不久之后,常规页面则又被聚合成THP,依此类推。我们已经观察到,这种行为会严重损害我们生产系统中的应用程序性能。
|
||||
|
||||
## 如何解决多程序互相干扰?
|
||||
|
||||
那么程序在启动和运行时互相干扰的性能问题,到底该怎么解决呢?我们现在就来看解决方案。
|
||||
|
||||
我们的解决方案由三个设计元素组成,每个设计元素都针对问题的特定方面。部署任何单独的元素都将在一定程度上对问题有所帮助。但是,所有设计元素协同工作,才能获得最好的效果。
|
||||
|
||||
第一个设计元素是**预分配JVM的堆空间**。
|
||||
|
||||
我们知道,对JVM而言,只有在实际使用堆空间之时,就是当需要增大堆空间来容纳新对象分配请求时,Linux才会为之分配新的内存页面,这时就可能会触发大量页面回收,并损害程序和系统性能。
|
||||
|
||||
这个设计元素就是预分配所有堆空间,从而避免了Linux实时分配页面的不利场景。要预先执行堆预分配,需要使用一个特殊的JVM参数:“ -XX:+ AlwaysPreTouch”,来启动Java应用程序。
|
||||
|
||||
但这个设计元素也有副作用,就是增加了JVM启动所需的时间,在部署时你需要考虑这一点。我们也做过一些实际测量,这个额外启动时间并不大,一般在几秒钟内,通常是可以接受的。
|
||||
|
||||
第二个设计元素,是关于如何**保护JVM的堆空间不被唤出到外部存储**。
|
||||
|
||||
我们知道,当发生GC时,JVM需要扫描相应的内存页。如果这些页面被操作系统换出到外部存储,则需要先换入它们到内存,这就会导致延迟,会增加JVM的暂停时间。
|
||||
|
||||
这个设计元素就可以防止JVM的堆页面被换出。 我们知道,Linux操作系统上是可以关闭内存页面交换的,但是这个设置如果是在系统级别进行,就会影响所有应用程序和所有内存空间。我推荐你一个更好的实现,就是采用**微调**,你来选择哪个应用程序和哪个存储区域可以页面交换。例如,你可以使用cgroup来精确控制要交换的应用程序。
|
||||
|
||||
公司中的大多数平台,一般都用来运行同类Java应用程序;这些程序往往配置差不多。在这些情况下,在系统级别关闭应用程序交换,倒也是非常合理的。
|
||||
|
||||
第三个设计元素是**动态调整THP**。
|
||||
|
||||
我们已经看到,启用THP功能可能会在某些场景下,导致严重的性能损失;但是THP在其他场景的确提高了性能,所以到底是否要启用THP呢?我们需要仔细考虑。
|
||||
|
||||
当THP影响性能时,系统的可用内存往往也恰好严重不足。发生这种情况时,现有的THP需要拆分成常规页面以进行页面换出。所以,我建议你用一个可用内存大小的阈值来决定THP的开关。
|
||||
|
||||
具体来说,就是建议你使用**应用程序的堆大小**作为内存阈值,来决定是否打开或关闭THP。当可用内存远远大于应用程序的内存可能占用量大小时,就启用THP,因为系统不太可能在启动特定应用程序后出现内存压力。否则的话,就关闭THP。
|
||||
|
||||
由于许多后端服务器都是运行同类应用程序,通常情况下,你都很容易知道,部署的应用程序预期会占用多少内存空间。
|
||||
|
||||
此外,常规页面需要聚合成THP,才能将大页面分配给应用程序。因此,这个元素的另外一部分机制是进行微调,是决定何时允许THP聚合。我建议你根据**操作系统的直接页面扫描率**和**聚合进程的CPU使用率**来决定。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲述了,将多个应用程序放置在同一台服务器上时,由于应用程序和操作系统机制的互相作用,引发的一系列性能问题。这些问题的根本原因,就是程序之间的互相影响。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/ef/dfe49c970e5a67b8f6a3993b9fc39aef.png" alt="">
|
||||
|
||||
应用程序之间的关系和人际关系一样,有时和谐,有时不和谐。唐代诗人刘禹锡有几句诗说:“常恨言语浅,不如人意深。今朝两相视,脉脉万重心。”说的是,语言的表达能力通常很有限,所以两人只能用眼神传达更复杂的情感。应用程序之间的关系,甚至程序和操作系统及硬件之间的关系,也会很复杂,也需要做足够的性能分析,才能理清它们之间的关系。
|
||||
|
||||
今天的讲述,主要集中在多任务共存环境中的两个问题,重点在分析问题产生的复杂根因。 如果你对这方面的具体算法和生产验证有兴趣,可以参考我的一篇论文。这篇论文发表在[International Journal of Cloud Computing](https://www.researchgate.net/publication/282348773)上面。
|
||||
|
||||
## 思考题
|
||||
|
||||
Linux操作系统的THP机制的设计初衷,本是为了提升系统性能。可是在有些情况下反而导致了系统性能下降。想一想操作系统的其他机制,有没有类似的情况发生?
|
||||
|
||||
>
|
||||
Tips:文件系统的预先读取等。
|
||||
|
||||
|
||||
欢迎你在留言区分享自己的思考,与我和其他同学一起讨论,也欢迎你把文章分享给自己的朋友。
|
||||
148
极客时间专栏/geek/性能工程高手课/性能工程实践/28 | 网络数据传输慢,问题到底出在哪了?.md
Normal file
148
极客时间专栏/geek/性能工程高手课/性能工程实践/28 | 网络数据传输慢,问题到底出在哪了?.md
Normal file
@@ -0,0 +1,148 @@
|
||||
<audio id="audio" title="28 | 网络数据传输慢,问题到底出在哪了?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/e6/7cba2646633fd34d98e8c6d87c0fb3e6.mp3"></audio>
|
||||
|
||||
你好,我是庄振运。
|
||||
|
||||
你一定有过在网页或者手机上下载照片的体验,如果数据传输太慢,那你的体验一定十分糟糕。你看,互联网实体之间的数据快速传输对用户体验至关重要。这里涉及到的其实就是网络传输问题。所以,今天我们就通过生产实践中的案例,来探讨一下互联网服务中的数据传输性能。
|
||||
|
||||
说到底,网络传输问题其实就分两种:
|
||||
|
||||
1. 数据根本没有传递;
|
||||
1. 数据传送速度较慢。
|
||||
|
||||
“数据没有传递”虽然看起来更严重,但是相对“数据传送缓慢”来说,更容易判断和解决。所以,这一讲,我们就重点解决第二种问题。我们一起来看看,为什么网络传送速度会慢,在众多原因中怎么快速诊断出关键问题来,又该如何去解决。
|
||||
|
||||
造成网络传输缓慢的原因很多,我们这一讲,就是帮助你快速诊断问题出在哪里:是客户端,是服务器端,还是网络本身?在此基础上,你才能专门针对具体的领域继续分析。
|
||||
|
||||
## 为什么数据传输慢?
|
||||
|
||||
我们先看一下,都有哪些可能的原因会导致数据传输缓慢呢?在宏观上,这种问题的可能原因可以分为三种场景:
|
||||
|
||||
1. 客户端应用程序的原因;
|
||||
1. 网络的原因;
|
||||
1. 服务器应用程序的原因。
|
||||
|
||||
也就是说,可能是由于数据发送方过载,而没有向接收方发送数据;也可能是网络通道很慢;又或者是数据接收方的服务器太忙,从而无法从网络缓冲区读取数据。
|
||||
|
||||
为了描述方便,我们根据平时客户浏览网页的场景,假设客户端是数据接收方,而服务器端是数据发送方。
|
||||
|
||||
进行此类分析诊断时,负责的工程师通常需要快速隔离出上述不同场景,以便他们可以专注于特定场景里面的可疑组件,并对本质原因进行更深入的分析。但这一快速诊断过程会遇到很多难点。
|
||||
|
||||
首先,**数据传输涉及多个网络实体**,包括两台机器(也就是发送者和接收者)和网络路由,这与仅涉及一台机器的常见性能问题形成鲜明对比。
|
||||
|
||||
其次,这种**诊断涉及多层信息**,包括应用程序层和网络传输层。为了找出原因,工程师必须检查各种数据,包括客户端日志、服务器日志、网络统计信息、CPU使用情况等。这些检查需要花费很多时间和精力,并且通常需要性能工程师的经验和专业知识。
|
||||
|
||||
更加让人郁闷的是,这些日志往往分散在不同的地方,比如客户端和服务器。为了节省时间和精力,性能工程师迫切需要更智能的工具,以帮助他们快速找出根本原因。
|
||||
|
||||
所以今天,我专注于解决这样的一个问题,就是:快速确定应归咎的组件范围和场景(无论是发送方、接收方还是网络本身)。我提出了一种**当发生数据传输缓慢的问题时,可以自动隔离原因的解决方案**。毕竟,你只有找出了要对“数据传输慢”负责任的那一部分,才可以进行后续分析工作,最终确定真正的问题。
|
||||
|
||||
## 如何判断问题所在位置?
|
||||
|
||||
要想快速诊断,我们需要先看看三种问题场景的不同特征。
|
||||
|
||||
这个解决方案本质上依靠的是**客户端和服务器端的TCP层面的特征**。TCP是传输层协议之一,可提供有序且可靠的流字节传输,是当今使用最广泛的传输协议。TCP具有**流控制**功能,可避免接收方过载。接收方设置专用的接收缓冲区,发送方设置相应的发送缓冲区。数据发送方(服务器)的发送缓冲区和数据接收方(客户端)的接收缓冲区,都可以通过操作系统来监测当前队列大小。
|
||||
|
||||
为了能够识别瓶颈,你需要在发送方和接收方的传输层上,收集有关队列大小的信息。有很多收集此类信息的方法。你有两种工具可以使用,分别是Netstat和ss。
|
||||
|
||||
Netstat是一个命令行工具,可以显示网络连接和网络协议统计信息。我们主要是用它来观察TCP / IP套接字的发送队列和接收队列的大小。而ss命令,可以显示套接字统计信息,包括显示TCP以及其他类型套接字的统计信息。类似于Netstat,ss还可以显示发送和接收队列大小。
|
||||
|
||||
除了相应的工具的介绍,为了帮助你理解,我们还需要先重温一下传输数据时候,应用层和TCP层的交互。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/89/dbe011441395739fbef5869848224789.png" alt="">
|
||||
|
||||
上图显示了任何基于TCP的数据传输中的典型流程。关于系统调用和网络传输的五个步骤如下:
|
||||
|
||||
1. 在步骤A,服务器应用程序发出write()系统调用,并将应用程序数据复制到套接字发送缓冲区。
|
||||
1. 在步骤B,服务器的TCP层发出send()调用,并将一些数据发送到网络;数据量受TCP的拥塞控制和流控制。
|
||||
1. 在步骤C,网络将数据逐跳路由到接收方(IP路由协议在这部分中发挥作用)。
|
||||
1. 在步骤D,客户端的TCP层将通过recv()系统调用接收数据,数据放入接收缓冲区。
|
||||
1. 在步骤E,客户端应用程序发出read()调用,以接收数据并将其复制到用户空间。
|
||||
|
||||
接下来,我们就来看看三种不同场景下的问题特征是什么样的。
|
||||
|
||||
我们先看第一个场景,**客户端接收数据缓慢**的情况。为了重现这一场景,我们做一个实验,让发送端发送一段固定大小的数据给接收方。我们强制接收方,也就是客户端,减慢数据的接收速度。具体做法,就是在应用程序代码的read()调用之前,注入了一定的延迟,这种场景代表了客户端数据接收成为瓶颈的情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/d8/1e28e9d5065f653f0cdc5ee1ddd639d8.png" alt="">
|
||||
|
||||
上图显示的是数据发送方的发送缓冲区,SendQ(Send Queue)的大小变化。开始时候,数据发送调用send(),立刻注满SendQ。随着数据的传输,慢慢变为0。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/13/0757f0a16dbf81ebed5c9f092c260913.png" alt="">
|
||||
|
||||
第二张图是客户端的接收缓冲区,RecvQ(Receive Queue)的大小变化,客户端因为应用程序运行缓慢,所以RecvQ具有一定的积累,这可以由非零值来看出。这些非零值持续了一段时间,随着应用程序不断地读取,最终RecvQ减为0。
|
||||
|
||||
对于第二种场景,也就是**数据发送方是瓶颈**的情况,我们强制发送方(即服务器端),放慢数据的发送速度。具体来说,我们在应用程序代码中,对write()的调用之前注入了一定的延迟,模拟了发送者是瓶颈的情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/1a/8dc79b587e12571b62b7725256bdb11a.png" alt="">
|
||||
|
||||
上图显示了服务器端的SendQ的值,你可以看到,SendQ几乎全部是零。这是因为发送端是瓶颈,其他地方不是瓶颈,所以任何SendQ的数据会被很快发送出去。
|
||||
|
||||
你可以在图片中看到一个持续时间很短的峰值,这是因为SendQ取样的时候恰好取到数据还没有被传输到网络中的时候。但因为这个峰值持续时间很短,简单的过滤就可以去掉。
|
||||
|
||||
接收端的RecvQ显示在下图,你可以看到,因为接收端不是瓶颈,RecvQ是零。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/3c/d8283edde9ac0494f4f80029b2436a3c.png" alt="">
|
||||
|
||||
第三个场景,是**网络本身是瓶颈**造成的数据传输缓慢。我们通过向网络路径注入延迟来创造这一场景,以使TCP仅能以非常低的吞吐量进行传输。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/1d/6d0ed6a9c6cf1e51085e366a7a84061d.png" alt="">
|
||||
|
||||
图片中显示了发送端的SendQ值,你可以看到它的值不为零,因为那些数据不能很快地被传送出去。
|
||||
|
||||
再来看接收端的RecvQ,如下图。RecvQ全为零,这些零值就代表了快速的数据传递。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/36/05/36e98d68b61e115e6b5a5ede39dc6205.png" alt="">
|
||||
|
||||
通过上面三种场景的分析,尤其是对发送端SendQ和接收端RecvQ的观察,我们不难总结出规律来。正常的数据传输情况下,客户端的接收队列和服务器端的发送队列都应该是零。
|
||||
|
||||
反之,如果数据传输缓慢,则有如下几种情况:
|
||||
|
||||
1. 如果客户端上的接收队列RecvQ不为零,则客户端应用程序是性能瓶颈;
|
||||
1. 如果服务器上的发送队列SendQ为零,则服务器应用程序是性能瓶颈;
|
||||
1. 如果客户端的接收队列RecvQ为零,而服务器的发送队列SendQ为非零,则网络本身是性能瓶颈。
|
||||
|
||||
为了帮助你加深记忆,我用表格来做了个归纳。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/f3/312fe519d3db2f20f9b44c547f0e00f3.png" alt="">
|
||||
|
||||
你可以通过这个表格,快速判断问题出现的位置。
|
||||
|
||||
## 解决方案如何落地?
|
||||
|
||||
根据前面的分析和总结,我们现在提出解决方案。这是一个基于**状态转移**的方案,需要从客户端和服务器端收集几个关键点的信息。
|
||||
|
||||
为了帮助你理解,我们需要先来看看数据请求和传输流程图。就用常见的HTTP协议的Request和Response方式来描述,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/24/b0b9e458e45019a7cdae9bf6b9b2da24.png" alt="">
|
||||
|
||||
当客户端需要下载服务器的数据时,首先在T0发出数据请求;网络将请求发送到服务器后,服务器在T1收到数据。
|
||||
|
||||
然后,服务器开始准备数据,数据准备好后,服务器将开始在T2时发回数据。通过一系列write()调用。发送在T4完成。网络传输后,客户端在T3开始接收数据,并在T5完成接收。请注意,尽管其他时间戳是按照严格的顺序,T3和T4的顺序可能会因实际情况而异。具体来说,对于小数据传输时,T4可以先于T3,因为单个write()调用就足够了。对于大数据传输,通常使用T3在T4之前。
|
||||
|
||||
接下来,我们来看看基于状态机的解决方案,它是一个针对HTTP数据传输问题的,完整而具体的解决方案。
|
||||
|
||||
从上面的过程中,我们可以看到,如果服务器无法接收到数据请求,则数据传递将不会发生,因此不会完成。
|
||||
|
||||
我用下图来表示整个状态机。这个状态机展示了整个HTTP数据传输的过程,包括Request和Response。如果数据传递成功,状态机最后会到达状态F。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/ca/466e92b6401de7e5c476812c800b55ca.png" alt="">
|
||||
|
||||
如上图,数据传递的初始状态为State-S。客户端发出请求后,它将移至状态A;当网络通道完成其工作,并将请求传递到服务器OS时,状态变为B。当服务器准备好数据,并开始发出数据的第一个字节时,状态变为C。
|
||||
|
||||
当客户端收到第一个字节后,状态变为D;最后当服务器发出最后一个字节时,状态变为E。或者这两个次序交换,成功进行数据传递的最终状态是State-F。
|
||||
|
||||
在整个过程中,如果发生其他的转移,那么就是网络传输有问题了。我们就可以根据发送端的发送队列和接收端的接收队列长度的变化,轻松判断是谁的问题,比如是客户端,服务器或是网路。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲述了,互联网服务在传输数据时,如果发生传输速度太慢的问题,怎样才能快速地诊断到底是客户端、服务器端,还是网络的问题。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/9d/4a785ec504c96d6bfbc079f61fe9539d.png" alt="">
|
||||
|
||||
唐代诗人高适的《燕歌行》有几句诗:“山川萧条极边土,胡骑凭陵杂风雨。战士军前半死生,美人帐下犹歌舞。”说的是前方的战士,在前线出生入死;后方却有人逍遥自在的观赏美人歌舞,醉生梦死。这种冰火两重天的讽刺,部分原因,就是责任没有分清,以至于滥竽充数者可以逍遥自在。
|
||||
|
||||
对待数据传输缓慢问题,我们也很希望能快速地搞清责任,分清是哪里的问题,然后才能有针对性地继续分析。我们的解决方案就是根据TCP的Send和Receive队列大小变化,来快速诊断的方案。它能智能而快速地分清问题的大致范围:就是数据发送方、数据接收方,还是网络。
|
||||
|
||||
## 思考题
|
||||
|
||||
根据你平时的观察,公司业务有没有发生数据传输太慢的问题?如果发生了,你们一般怎么根因呢?如果采用本讲的思路,会不会更加快速地诊断?
|
||||
|
||||
欢迎你在留言区分享自己的思考,与我和其他同学一起讨论,也欢迎你把文章分享给自己的朋友。
|
||||
194
极客时间专栏/geek/性能工程高手课/性能工程实践/29 | 如何彻底发挥SSD的潜力?.md
Normal file
194
极客时间专栏/geek/性能工程高手课/性能工程实践/29 | 如何彻底发挥SSD的潜力?.md
Normal file
@@ -0,0 +1,194 @@
|
||||
<audio id="audio" title="29 | 如何彻底发挥SSD的潜力?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/20/07/20933d0dfc588089273516d23593fb07.mp3"></audio>
|
||||
|
||||
你好,我是庄振运。
|
||||
|
||||
今天是“性能工程实践”这个模块的最后一讲,我们来讨论一种“软硬件结合”的性能工程优化实践,与SSD(硬件)有关。现在SSD用的越来越普遍的情况你一定非常清楚,但是你设计的应用程序(软件)真的充分利用了SSD的特点,并发挥SSD的潜力了吗?
|
||||
|
||||
要知道,SSD可不仅仅是“更快的HDD”。
|
||||
|
||||
SSD的好处显而易见,它作为存储时,应用程序可以获得更好的I/O性能。但是这些收益,主要归因于SSD提供的更高的IOPS和带宽。如果你因此只将SSD视为一种“更快的HDD”,那就真是浪费了SSD的潜力。
|
||||
|
||||
如果你在设计软件时,能够充分考虑SSD的工作特点,把应用程序和文件系统设计为“对SSD友好”,会使服务性能有个质的飞跃。
|
||||
|
||||
今天我们就来看看,**如何在软件层进行一系列SSD友好的设计更改**。
|
||||
|
||||
## 为什么要设计SSD友好的软件?
|
||||
|
||||
设计对SSD友好的软件有什么好处呢?简单来说,你可以获得三种好处:
|
||||
|
||||
1. 提升应用程序等软件的性能;
|
||||
1. 提高SSD的 I/O效率;
|
||||
1. 延长SSD的寿命。
|
||||
|
||||
先看第一种好处——**更好的应用程序性能**。在不更改应用程序设计的情况下,简单地采用SSD可以获得性能提升,但无法获得最佳性能。
|
||||
|
||||
我为你举个例子来说明。我们曾经有一个应用程序,它需要不断写入文件以保存数据,主要性能瓶颈就是硬盘I/O。使用HDD时,最大应用程序吞吐量为142个查询/秒(QPS)。无论我们对应用程序设计进行什么样的更改或调优,这就是使用HDD可以获得的最好性能了。
|
||||
|
||||
而当迁移到具有相同应用程序的SSD时,吞吐量提高到了20,000 QPS,速度提高了140倍。这种提高,主要来自SSD提供的更高的IOPS。你是不是觉得与HDD相比,应用程序吞吐量有了显着提高,性能已经很好了?
|
||||
|
||||
但这并不是SSD能实现的最佳性能。
|
||||
|
||||
我们对应用程序设计进行了优化,使其对SSD友好之后,应用程序吞吐量提高到100,000QPS,与简单设计相比,提高了4倍。
|
||||
|
||||
你可能会问,这是如何做到的?
|
||||
|
||||
这其中的秘密,就是**使用多个并发线程来执行I/O**。如下图所示,这利用了**SSD的内部并行性**。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/a0/d2b82a4a2b47c03aea570f183c49b8a0.png" alt="">
|
||||
|
||||
这里你需要注意的是,在这个系统中,多个I/O线程对HDD是毫无益处的。因为HDD只有一个磁头,所以用多个I/O线程,并不能提高旧系统的吞吐量。
|
||||
|
||||
第二种好处是**更高效的存储I/O**。
|
||||
|
||||
我在[第17讲](https://time.geekbang.org/column/article/185154)中提到过,SSD上的最小内部I/O单元是一页,比如4KB大小。因此对SSD的单字节读/写,必须在页面级进行。应用程序对SSD的写操作,可能会导致对SSD上的物理写操作变大,这就是“写入放大(参见[第23讲](https://time.geekbang.org/column/article/190151))”。
|
||||
|
||||
因为有这个特性,如果应用程序的数据结构或I/O对SSD不友好,就会让写放大效果更大,导致SSD的I/O不能被充分利用。
|
||||
|
||||
设计SSD友好软件的最后一个好处是**延长SSD的寿命**。
|
||||
|
||||
SSD会磨损,是因为每个存储单元,只能维持有限数量的写入擦除周期。实际上,SSD的寿命取决于四个因素:
|
||||
|
||||
1. SSD大小
|
||||
1. 最大擦除周期数
|
||||
1. 写入放大系数
|
||||
1. 应用程序写入速率
|
||||
|
||||
例如,假设有一个1TB大小的SSD,一个写入速度为100MB/秒的应用程序和一个擦除周期数为10,000的SSD。当写入放大倍数为4时,SSD仅可持续10个月。具有3,000个擦除周期和写入放大系数为10的SSD只能使用一个月。
|
||||
|
||||
你也知道,相对于HDD而言,SSD的成本比较高,我们自然希望自己设计的应用程序对SSD友好,从而延长SSD的使用寿命。
|
||||
|
||||
## 怎么设计对SSD友好的软件?
|
||||
|
||||
说了这么多设计SSD友好软件的好处,那我们具体该从哪里入手呢?
|
||||
|
||||
其实在软件领域,有很多地方都可以做对SSD友好的设计,比如文件系统、数据库系统、数据基础架构层和应用程序。接下来我会一一为你介绍。尤其是应用程序这个领域,它是这一讲的重点。
|
||||
|
||||
第一个可以对SSD友好的领域是**文件系统**。文件系统层直接处理存储,因此我们需要在此级别进行优化设计更改,来更有效地发挥SSD的特长。一般而言,这些设计更改集中在**SSD和HDD迥异的三个关键差异特征**上:
|
||||
|
||||
- SSD的随机访问与顺序访问具有相同的性能;
|
||||
- 需要在块级别进行擦除后才能重写;
|
||||
- 内部损耗均衡的机制会导致写入放大。
|
||||
|
||||
现在比较流行的有两种对SSD友好的文件系统。第一种,是适用于SSD的通用文件系统,主要支持Trim的新功能,比如Ext4和Btrfs。第二种,是专门为SSD设计的文件系统。基本思想是采用日志结构的数据布局(相对于B树或Htree),来容纳SSD的“复制-修改-写入”属性,比如NVFS(非易失性文件系统)、FFS / JFFS2和F2FS。
|
||||
|
||||
除了文件系统,**数据库系统**也可以设计成对SSD友好。
|
||||
|
||||
SSD和HDD之间的差异在数据库设计中尤其重要。近几十年来,数据库的各个组件(比如查询处理、查询优化和查询评估等)都已经在考虑HDD特性的情况下进行了诸多优化。
|
||||
|
||||
举个例子,因为普通硬盘的随机访问比顺序访问要慢得多,所以数据库组件会尽量减少随机访问。而使用SSD时,类似这样的假设就不成立了。
|
||||
|
||||
因此,业界设计了新的、对SSD友好的数据库。对SSD友好的数据库主要有两种:第一种是专门针对SSD的数据库,例如AreoSpike,这种数据库主要采用对SSD友好的Join算法;第二种是HDD和SSD混合的数据库,一般是使用SSD来缓存数据。
|
||||
|
||||
第三个领域是**数据基础架构层**。
|
||||
|
||||
对于分布式数据系统的设计而言,数据来源大体上有两个地方:计算机上的本地磁盘或另一台计算机上的内存。这两个来源哪个更快更高效呢?还真不一定。随着技术的演化,业界也一直在争论。
|
||||
|
||||
过去很多年,这样的争论比较倾向于远程计算机的内存,因为速度更快;Memcached就是一个例子。传统的本地HDD访问延迟,大约是好几个毫秒;而远程内存访问的延迟,包括了RAM访问延迟和网络传输延迟,也仅处于微秒级。同时,远程内存的I/O带宽与本地HDD大致相同甚至更多。因此,远端的内存反而比本地的硬盘的访问时间更短。
|
||||
|
||||
而SSD的出现,正在改变这个趋势和业界的决定。使用SSD作为存储设备后,本地SSD变得比远程内存访问更为高效。
|
||||
|
||||
首先,SSD的I/O延迟降低到了微秒级,而I/O带宽可是比HDD高一个数量级。这些结果就导致了数据基础架构层的新设计。比如,新设计更希望尽可能与应用程序**共同分配数据**,以避免额外的节点和网络的限制,从而降低系统的复杂性和成本。
|
||||
|
||||
这么说可能你的感受不太明显,我来用Netflix为你举例说明一下。Netflix就是采用这种设计的公司之一。Netflix公司曾经采用memcached来缓存Cassandra层数据。假设Netflix需要缓存10TB的数据,如果每个内存缓存节点在RAM中保存100GB数据,则需要部署100个内存缓存节点。后来,Netflix只使用10个Cassandra节点,并为每个Cassandra节点配备1TB SSD来完全取代memcached层。
|
||||
|
||||
你看,采用这种设计,就不需要100个Memcached节点,只需10台配备SSD的节点,节省了大量成本。
|
||||
|
||||
在**应用程序层**,我们也可以对SSD进行友好的设计,以获得前面提到的三种好处(更好的应用程序性能、更高效的I/O和更长的SSD寿命)。
|
||||
|
||||
在这一领域,我总结了七大SSD友好的设计原则,大体上分为三类:数据结构、I/O处理和线程使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/2d/a087788f0d74a2863366ca456f8c362d.png" alt="">
|
||||
|
||||
### 1.数据结构:避免就地更新优化
|
||||
|
||||
传统的HDD的寻址延迟很大,因此,使用HDD的应用程序通常会进行各种优化,以执行不需要寻址的就地更新,比如只在一个文件后面写入。
|
||||
|
||||
比如下图所示,在执行随机更新时,吞吐量一般只能达到约170 QPS;而对于同一个HDD,就地更新可以达到280QPS,远高于随机更新。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ff/3b/fff29bfb9e14f2ff93e0637e8636333b.png" alt="">
|
||||
|
||||
不过在设计与SSD配合使用的应用程序时,这些考虑就没什么意义了。对SSD而言,随机读写和顺序读写性能类似,就地更新不会获得任何IOPS优势。
|
||||
|
||||
此外,就地更新实际上是会导致SSD性能下降的。包含数据的SSD页面无法直接重写,因此在更新存储的数据时,必须先将相应的SSD页面读入SSD缓冲区,然后将数据写入干净的页面。 SSD中的“读取-修改-写入”过程与HDD上的直接“仅写入”行为形成了鲜明的对比。
|
||||
|
||||
相比之下,SSD上的随机更新,就不会引起读取和修改步骤(即仅仅“写入”),因此速度更快。
|
||||
|
||||
使用SSD,以上相同的应用程序,可以通过**随机更新**或**就地更新**来达到大约2万QPS;而且随机更新和就地更新的吞吐量大体相似,随机更新其实还稍微好些。如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7a/0b/7aa33a3abeef4b908f7560b23db6c20b.png" alt="">
|
||||
|
||||
### 2.数据结构:区分热、冷数据
|
||||
|
||||
几乎所有处理存储的应用程序,磁盘上存储的数据的访问概率均不相同。
|
||||
|
||||
用一个需要跟踪活动用户活动的社交网络应用程序来给你举个例子。对于用户数据存储,简单的解决方案,是基于用户属性(例如注册时间)将所有用户压缩在同一位置(例如某个SSD上的文件)。以后需要更新热门用户的活动时,SSD需要在页面级别进行访问(即读取/修改/写入)。
|
||||
|
||||
因此,如果用户的数据大小是小于一页,那么每次读取这个用户的数据,附近的用户数据也将一起被访问。如果应用程序其实并不需要附近用户的数据,那么额外的数据访问,不仅会浪费I/O带宽,而且会不必要地磨损SSD。
|
||||
|
||||
为了缓解这种性能问题,在将SSD用作存储设备时,应将热数据与冷数据分开。以不同级别或不同方式来进行分隔。例如,把它们存在不同的文件、文件的不同部分或不同的表格里。
|
||||
|
||||
### 3.数据结构:采用紧凑的数据结构
|
||||
|
||||
在SSD的世界中,最小的更新单位是页面(4KB),因此,即使是一个字节的更新,也将导致至少4KB SSD写入。由于写入放大的效果,实际写入SSD的字节可能远大于4KB。读取操作也是类似,因为OS具有预读机制,会预先主动地读入文件数据,以期改善读取文件时的缓存命中率。
|
||||
|
||||
所以当将数据保留在SSD上时,你最好使用紧凑的数据结构,避免分散的更新,以获得更快的应用程序性能、更有效的存储I/O以及节省SSD的寿命。
|
||||
|
||||
### 4. I/O处理:避免长而繁重的持续写入
|
||||
|
||||
SSD通常具有GC机制,不断地回收存储块以供以后使用。GC可以用后台或前台的方式工作。 SSD控制器通常保持一个空闲块的阈值。每当可用块数下降到阈值以下时,后台GC就会启动。由于后台GC是异步发生的(即非阻塞),因此它不会影响应用程序的I/O延迟。但是,如果块的请求速率超过了GC速率,并且后台GC无法跟上,则将触发前台GC。
|
||||
|
||||
在前台GC期间,必须即时擦除(即阻塞)每个块以供应用程序使用,这时发出写操作的应用程序所经历的**写延迟**就会受到影响。具体来说,释放块的前台GC操作,可能会花费数毫秒以上的时间,从而导致较大的应用程序I/O延迟。
|
||||
|
||||
因此,你最好避免进行长时间的大量写入操作,这样就可能永远不触发前台GC。
|
||||
|
||||
### 5. I/O处理:避免SSD存储太满
|
||||
|
||||
SSD磁盘存储的满存程度,会影响写入放大系数和GC导致的写入性能。在GC期间,需要擦除块以创建空闲块。擦除块前,需要移动并保留有效数据才能获得空闲块。有时为了获得一个空闲块,需要压缩好几个存储块。而每个空闲块的生产需要压缩的块数,取决于磁盘的空间使用率。
|
||||
|
||||
假设磁盘满百分比平均为A%,要释放一个块,则需要压缩1 /(1-A%)块。显然,SSD的空间使用率越高,将需要移动更多的块以释放一个块,这将占用更多的资源,并导致更长的I/O等待时间。
|
||||
|
||||
例如,如果A=80%,则大约移动五个数据块以释放一个块。当A=95%时,将移动约20个块。
|
||||
|
||||
### 6.线程:使用多个线程执行小的I/O
|
||||
|
||||
SSD内部大量使用了并行的设计,这种并行表现在多个层面上。一个I/O线程无法充分利用这些并行性,会导致访问时间更长。而使用多个线程,就可以充分利用SSD内部的并行性了。
|
||||
|
||||
SSD可以有效地在可用通道之间分配读写操作,从而提供高水平的内部I/O并发性。例如,我们使用一个应用程序执行10KB写入I/O(10KB算是比较小的IO大小)。使用一个I/O线程,它可以达到115MB/秒。使用两个线程基本上使吞吐量加倍;使用四个线程再次将其加倍;使用八个线程可达到约500MB/秒,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/89/76/8984620c393a877fb47ecab219a41076.png" alt="">
|
||||
|
||||
你可能会很自然地问出一个问题:这里的“小”IO,到底有多小?
|
||||
|
||||
答案是,只要是不能充分利用内部并行性的任何I/O大小,都被视为“小”。例如,SSD页面大小为4KB,内部并行度为16,则阈值应约为64KB。
|
||||
|
||||
### 7.线程:使用较少的线程来执行大I/O
|
||||
|
||||
第七个原则同样关于线程,它与第六条原则相对应(但并不矛盾)。
|
||||
|
||||
对于大型I/O,SSD内部已经充分优化使用了SSD的内部并行性,因此,应使用更少的线程(即小于四个)以实现最大的I/O吞吐量。从吞吐量的角度来看,用太多线程不会有太大益处。更重要的是,使用太多线程可能导致线程之间的资源竞争,以及诸如OS级的预读和回写之类的后台活动。
|
||||
|
||||
例如,根据我们的实验,当写入大小为10MB时,一个线程可以达到414MB/秒,两个线程可以达到816MB/秒,而四个线程达到912MB/秒,八个线程实际上只有520MB/秒。如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/99/98feb65d9b624e057baeb97f0af6c599.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
这一讲我们讨论了软硬件结合的优化。
|
||||
|
||||
与使用HDD的应用程序相比,使用SSD的应用程序通常具有更好的性能水平。但是,如果不更改应用程序设计,则应用程序是无法获得最佳性能的。因为SSD的工作方式不同于HDD。
|
||||
|
||||
为了发挥SSD的全部性能潜能,应用程序设计必须对SSD友好。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/48/d0/485e47b6f1103ba4304e436d066f62d0.png" alt="">
|
||||
|
||||
唐代的王维有一首《少年行》,讲几个好哥们一起喝酒:“新丰美酒斗十千,咸阳游侠多少年。相逢意气为君饮,系马高楼垂柳边。” 说的是性情投机的好朋友一起互相帮助,就会互相促进。
|
||||
|
||||
硬件和软件也是如此,如果互相对对方友好,互相都受益,总体性能也就更高。
|
||||
|
||||
我们的软件系统如果能充分考虑硬件,比如SSD的特性,做出的设计就会获得更好的性能和稳定性。基于这一点,我在这一讲里面提出了七个对SSD友好的软件设计原则,分为三类:数据结构、I/O处理和线程使用。你在设计使用SSD存储的软件时,可以参考采用。
|
||||
|
||||
## 思考题
|
||||
|
||||
你正在开发、维护、使用的系统,有没有使用SSD作为存储的?如果有,这个系统有没有考虑到SSD的特殊机制和寿命问题?
|
||||
|
||||
欢迎你在留言区分享自己的思考,与我和其他同学一起讨论,也欢迎你把文章分享给自己的朋友。
|
||||
Reference in New Issue
Block a user