201 lines
16 KiB
Markdown
201 lines
16 KiB
Markdown
# 大咖助场4|傅健:那些年,影响我们达到性能巅峰的常见绊脚石(下)
|
||
|
||
你好,我是傅健,又见面了。上一期分享我们总结了3个场景化的问题以及应对策略,这一期我们就接着“系统性能优化”这个主题继续总结。
|
||
|
||
## 场景1:资源争用
|
||
|
||
### 案例
|
||
|
||
一段时间,我们总是监控到一些性能“掉队”的请求,例如平时我们访问Cassandra数据库都在10ms以内,但是偶尔能达到3s,你可以参考下面这个度量数据:
|
||
|
||
```
|
||
{
|
||
"stepName": "QueryInformation",
|
||
"values": {
|
||
"componentType": "Cassandra",
|
||
"totalDurationInMS": 3548,
|
||
"startTime": "2018-05-11T08:20:28.889Z",
|
||
"success": true
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
持续观察后,我们发现这些掉队的请求都集中在每天8点20分,话说“百果必有因”,这又是什么情况呢?
|
||
|
||
### 解析
|
||
|
||
这种问题,其实相对好查,因为它们有其发生的规律,这也是我们定位性能问题最基本的手段,即找规律:发生在某一套环境?某一套机器?某个时间点?等等,这些都是非常有用的线索。而这个案例就是固定发生在某个时间点。既然是固定时间点,说明肯定有某件事固定发生在这个点,所以查找问题的方向自然就明了了。
|
||
|
||
首先,我们上来排除了应用程序及其下游应用程序定时去做任务的情况。那么除了应用程序自身做事情外,还能是什么?可能我们会想到:运行应用程序的机器在定时做事情。果然,我们查询了机器的CronJob,发现服务器在每天的8点20分(业务低峰期)都会去归档业务的日志,而这波集中的日志归档操作,又带来了什么影响呢?
|
||
|
||
日志备份明显是磁盘操作,所以我们要查看的是磁盘的性能影响,如果磁盘都转不动了,可想而知会引发什么。我们一般都会有图形化的界面去监控磁盘性能,但是这里为了更为通用的演示问题,所以我使用了SAR的结果来展示当时的大致情况(说是大致,是因为SAR的历史记录结果默认是以10分钟为间隔,所以只显示8:20分的数据,可能8:21分才是尖峰,但是却不能准确反映):
|
||
|
||
![](https://static001.geekbang.org/resource/image/ba/88/ba6f20c32d3a3288f4e3467b70b68988.png)
|
||
|
||
从上面的结果我们可以看出,平时磁盘await只要2ms,而8:20分的磁盘操作达到了几百毫秒。磁盘性能的急剧下降影响了应用程序的性能。
|
||
|
||
### 小结
|
||
|
||
在这个案例中,服务器上的定时日志归档抢占了我们的资源,导致应用程序速度临时下降,即资源争用导致了性能掉队。我们再延伸一点来看,除了这种情况外,还有许多类似的资源争用都有可能引起这类问题,例如我们有时候会在机器上装一些logstash、collectd等监控软件,而这些监控软件如果不限制它们对资源的使用,同样也可能会争用我们很多的资源。诸如此类,不一一枚举,而针对这种问题,很明显有好几种方法可以供我们去尝试解决,以当前的案例为例:
|
||
|
||
* 降低资源争用,例如让备份慢一点;
|
||
* 错峰,错开备份时间,例如不让每个机器都是8点20分去备份,而是在大致一个时间范围内随机时间进行;
|
||
* 避免资源共享,避免多个机器/虚拟机使用同一块磁盘。
|
||
|
||
上面讨论的争用都发生在一个机器内部,实际上,资源争用也常发生在同一资源(宿主、NFS磁盘等)的不同虚拟机之间,这也是值得注意的一个点。
|
||
|
||
## 场景2:延时加载
|
||
|
||
### 案例
|
||
|
||
某日,某测试工程师胸有成竹地抱怨:“你这个API接口性能不行啊,我们每次执行自动化测试时,总有响应很慢的请求。”于是一个常见的争执场景出现了:当面演示调用某个API若干次,结果都是响应极快,测试工程师坚持他真的看到了请求很慢的时候,但是开发又坚定说在我的机器上它就是可以啊。于是互掐不停。
|
||
|
||
![](https://static001.geekbang.org/resource/image/ba/53/ba0e43c92a1690a8266746157b66d653.jpg)
|
||
|
||
### 解析
|
||
|
||
开发者后来苦思冥想很久,和测试工程师一起比较了下到底他们的测试有什么不同,唯一的区别在于测试的时机不同:自动化测试需要做的是回归测试,所以每次都会部署新的包并执行重启,而开发者复现问题用的系统已经运行若干天,那这有什么区别呢?这就引入了我们这部分要讨论的内容——延时加载。
|
||
|
||
我们知道当系统去执行某个操作时,例如访问某个服务,往往都是“按需执行”,这非常符合我们的行为习惯。例如,需要外卖点餐时,我们才打开某APP,我们一般不会在还没有点餐的时候,就把APP打开“守株待兔”某次点餐的发生。这种思路写出来的系统,会让我们的系统在上线之时,可以轻装上阵。例如,非常类似下面的Java“延时初始化”伪代码:
|
||
|
||
```
|
||
public class AppFactory{
|
||
private static App app;
|
||
synchronized App getApp() {
|
||
if (App == null)
|
||
app= openAppAndCompleteInit();
|
||
return app;
|
||
}
|
||
//省略其它非关键代码
|
||
}
|
||
|
||
App app = AppFactory.getApp();
|
||
app.order("青椒土豆丝");
|
||
|
||
```
|
||
|
||
但这里的问题是什么呢?假设打开APP的操作非常慢,那等我们需要点餐的时候,就会额外耗费更长的时间(而一旦打开运行在后台,后面不管怎么点都会很快)。这点和我们的案例其实是非常类似的。
|
||
|
||
我们现在回到案例,持续观察发现,这个有性能问题的API只出现在第一次访问,或者说只出现在系统重启后的第一次访问。当它的API被触发时,它会看看本地有没有授权相关的信息,如果没有则远程访问一个授权服务拿到相关的认证信息,然后缓存认证信息,最后再使用这个认证信息去访问其它组件。而问题恰巧就出现在访问授权服务器完成初始化操作耗时比较久,所以呈现出第一次访问较慢,后续由于已缓存消息而变快的现象。
|
||
|
||
那这个问题怎么解决呢?我们可以写一个“加速器”,说白了,就是把“延时加载”变为“主动加载”。在系统启动之后、正常提供服务之前,就提前访问授权服务器拿到授权信息,这样等系统提供服务之后,我们的第一个请求就很快了。类似使用Java伪代码,对上述的延时加载修改如下:
|
||
|
||
```
|
||
public class AppFactory{
|
||
private static App app = openAppAndCompleteInit();
|
||
synchronized App getApp() {
|
||
return app;
|
||
}
|
||
//省略其它非关键代码
|
||
}
|
||
|
||
```
|
||
|
||
### 小结
|
||
|
||
延时加载固然很好,可以让我们的系统轻装上阵,写出的程序也符合我们的行为习惯,但是它常常带来的问题是,在第一次访问时可能性能不符合预期。当遇到这种问题时,我们也可以根据它的出现特征来分析是不是属于这类问题,即是否是启动完成后的第一次请求。如果是此类问题,我们可以通过变“被动加载”为“主动加载”的方式来加速访问,从而解决问题。
|
||
|
||
但是这里我不得不补充一点,是否在所有场景下,我们都需要化被动为主动呢?实际上,还得具体情况具体分析,例如我们打开一个网页,里面内嵌了很多小图片链接,但我们是否会为了响应速度,提前将这些图片进行加载呢?一般我们都不会这么做。所以具体问题具体分析永远是真理。针对我们刚刚讨论的案例,这种加速只是一次性的而已,而且资源数量和大小都是可控的,所以这种修改是值得,也是可行的。
|
||
|
||
## 场景3:网络抖动
|
||
|
||
### 案例
|
||
|
||
我们来看一则[新闻](https://finance.sina.com.cn/money/bank/bank_hydt/2019-12-05/doc-iihnzhfz3876113.shtml):
|
||
|
||
![](https://static001.geekbang.org/resource/image/51/fd/518c5a356e5b956a88be92b2476f8ffd.jpg)
|
||
|
||
类似的新闻还有许多,你可以去搜一搜,然后你就会发现:它们都包含一个关键词——网络抖动。
|
||
|
||
### 解析
|
||
|
||
那什么是网络抖动呢?网络抖动是衡量网络服务质量的一个指标。假设我们的网络最大延迟为100ms,最小延迟为10ms,那么网络抖动就是90ms,即网络延时的最大值与最小值的差值。差值越小,意味着抖动越小,网络越稳定。反之,当网络不稳定时,抖动就会越大,网络延时差距也就越大,反映到上层应用自然是响应速度的“掉队”。
|
||
|
||
为什么网络抖动如此难以避免?这是因为网络的延迟包括两个方面:传输延时和处理延时。忽略处理延时这个因素,假设我们的一个主机进行一次服务调用,需要跨越千山万水才能到达服务器,我们中间出“岔子”的情况就会越多。我们在Linux下面可以使用traceroute命令来查看我们跋山涉水的情况,例如从我的Linux机器到百度的情况是这样的:
|
||
|
||
```
|
||
[root@linux~]# traceroute -n -T www.baidu.com
|
||
traceroute to www.baidu.com (119.63.197.139), 30 hops max, 60 byte packets
|
||
1 10.224.2.1 0.452 ms 0.864 ms 0.914 ms
|
||
2 1.1.1.1 0.733 ms 0.774 ms 0.817 ms
|
||
3 10.224.16.193 0.361 ms 0.369 ms 0.362 ms
|
||
4 10.224.32.9 0.355 ms 0.414 ms 0.478 ms
|
||
5 10.140.199.77 0.400 ms 0.396 ms 0.544 ms
|
||
6 10.124.104.244 12.937 ms 12.655 ms 12.706 ms
|
||
7 10.124.104.195 12.736 ms 12.628 ms 12.851 ms
|
||
8 10.124.104.217 13.093 ms 12.857 ms 12.954 ms
|
||
9 10.112.4.65 12.586 ms 12.510 ms 12.609 ms
|
||
10 10.112.8.222 44.250 ms 44.183 ms 44.174 ms
|
||
11 10.112.0.122 44.926 ms 44.360 ms 44.479 ms
|
||
12 10.112.0.78 44.433 ms 44.320 ms 44.508 ms
|
||
13 10.75.216.50 44.295 ms 44.194 ms 44.386 ms
|
||
14 10.75.224.202 46.191 ms 46.135 ms 46.042 ms
|
||
15 119.63.197.139 44.095 ms 43.999 ms 43.927 ms
|
||
|
||
```
|
||
|
||
通过上面的命令结果我们可以看出,我的机器到百度需要很多“路”。当然大多数人并不喜欢使用traceroute来评估这段路的艰辛程度,而是直接使用ping来简单看看“路”的远近。例如通过以下结果,我们就可以看出,我们的网络延时达到了40ms,这时网络延时就可能是一个问题了。
|
||
|
||
```
|
||
[root@linux~]# ping www.baidu.com
|
||
PING www.wshifen.com (103.235.46.39) 56(84) bytes of data.
|
||
64 bytes from 103.235.46.39: icmp_seq=1 ttl=41 time=46.2 ms
|
||
64 bytes from 103.235.46.39: icmp_seq=2 ttl=41 time=46.3 ms
|
||
|
||
```
|
||
|
||
其实上面这两个工具的使用只是直观反映网络延时,它们都默认了一个潜规则:网络延时越大,网络越抖动。且不说这个规则是否完全正确,至少从结果来看,评估网络抖动并不够直观。
|
||
|
||
所以我们可以再寻求一些其它的工具。例如可以使用MTR工具,它集合了tractroute和ping。我们可以看下执行结果:下图中的best和wrst字段,即为最好的情况与最坏的情况,两者的差值也能在一定程度上反映出抖动情况,其中不同的host相当于traceroute经过的“路”。
|
||
|
||
![](https://static001.geekbang.org/resource/image/04/4f/04a239ef67790671e494cde7416ddb4f.png)
|
||
|
||
### 小结
|
||
|
||
对于网络延时造成的抖动,特别是传输延迟造成的抖动,我们一般都是“有心无力”的。只能说,我们需要做好网络延时的抖动监测,从而能真正定位到这个问题,避免直接无证据就“甩锅”于网络。
|
||
|
||
另外,在做设计和部署时,我们除了容灾和真正的业务需求,应尽量避免或者说减少“太远”的访问,尽量将服务就近到一个机房甚至一个机柜,这样就能大幅度降低网络延迟,抖动情况也会降低许多,这也是为什么CDN等技术兴起的原因。
|
||
|
||
那同一网络的网络延时可以有多好呢?我们可以自己测试下。例如,我的Linux机器如果ping同一个网段的机器,是连1ms都不到的:
|
||
|
||
```
|
||
[root@linux~]# ping 10.224.2.146
|
||
PING 10.224.2.146 (10.224.2.146) 56(84) bytes of data.
|
||
64 bytes from 10.224.2.146: icmp_seq=1 ttl=64 time=0.212 ms
|
||
64 bytes from 10.224.2.146: icmp_seq=2 ttl=64 time=0.219 ms
|
||
64 bytes from 10.224.2.146: icmp_seq=3 ttl=64 time=0.154 ms
|
||
|
||
```
|
||
|
||
## 场景4:缓存失效
|
||
|
||
### 案例
|
||
|
||
在产线上,我们会经常观察到一些API接口调用周期性(固定时间间隔)地出现“长延时”,而另外一些接口调用也会偶尔如此,只是时间不固定。继续持续观察,你会发现,虽然后者时间不够规律,但是它们的出现时间间隔都是大于一定时间间隔的。那这种情况是什么原因导致的呢?
|
||
|
||
### 解析
|
||
|
||
当我们遇到上述现象,很有可能是因为“遭遇”了缓存失效。缓存的定义与作用这里不多赘述,你应该非常熟悉了。同时我们也都知道,缓存是以空间换时间的方式来提高性能,讲究均衡。在待缓存内容很多的情况下,不管我们使用本地缓存,还是Redis、Memcached等缓存方案,我们经常都会受限于“空间”或缓存条目本身的时效性,而给缓存设置一个失效时间。而当失效时间到来时,我们就需要访问数据的源头从而增加延时。这里以下面的缓存构建代码作为示例:
|
||
|
||
```
|
||
CacheBuilder.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES).build();
|
||
|
||
```
|
||
|
||
我们可以看到:一旦缓存后,10分钟就会失效,但是失效后,我们不见得刚好有请求过来。如果这个接口是频繁调用的,则长延时请求的出现频率会呈现出周期性的10分钟间隔规律,即案例描述的前者情况。而如果这个接口调用很不频繁,则我们只能保证在10分钟内的延时是平滑的。当然,这里讨论的场景都是在缓存够用的情况下,如果缓存的条目超过了设定的最大允许值,这可能会提前淘汰一些缓存内容,这个时候长延时请求出现的规律就无章可循了。
|
||
|
||
### 小结
|
||
|
||
缓存失效是导致偶发性延时增加的常见情况,也是相对来说比较好定位的问题,因为接口的执行路径肯定是不同的,只要我们有充足的日志,即可找出问题所在。另外,针对这种情况,一方面我们可以增加缓存时间来尽力减少长延时请求,但这个时候要求的空间也会增大,同时也可能违反了缓存内容的时效性要求;另一方面,在一些情况下(比如缓存条目较少、缓存的内容可靠性要求不高),我们可以取消缓存的TTL,更新数据库时实时更新缓存,这样读取数据就可以一直通过缓存进行。总之,应情况调整才是王道。
|
||
|
||
## 总结
|
||
|
||
结合我上一期的分享,我们一共总结了7种常见“绊脚石”及其应对策略,那通过了解它们,我们是否就有十足的信心能达到性能巅峰了呢?
|
||
|
||
其实在实践中,我们往往还是很难的,特别是当前微服务大行其道,决定我们系统性能的因素往往是下游微服务,而下游微服务可能来源于不同的团队或组织。这时,已经不再单纯是技术本身的问题了,而是沟通、协调甚至是制度等问题。但是好在对于下游微服务,我们依然可以使用上面的分析思路来找出问题所在,不过通过上面的各种分析你也可以知道,让性能做到极致还是很难的,总有一些情况超出我们的预期,例如我们使用的磁盘发生损害,彻底崩溃前也会引起性能下降。
|
||
|
||
另外一个值得思考的问题是,是否有划算的成本收益比去做无穷无尽的优化,当然对于技术极客来说,能不能、让不让解决问题也许不是最重要的,剥丝抽茧、了解真相才是最有成就感的事儿。
|
||
|
||
感谢阅读,希望今天的分享能让你有所收获!如果你发现除了上述我介绍的那些“绊脚石”外,还有其它一些典型情况存在,也欢迎你在留言区中分享出来作为补充。
|
||
|