You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 大咖助场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分钟为间隔所以只显示820分的数据可能821分才是尖峰但是却不能准确反映
![](https://static001.geekbang.org/resource/image/ba/88/ba6f20c32d3a3288f4e3467b70b68988.png)
从上面的结果我们可以看出平时磁盘await只要2ms而820分的磁盘操作达到了几百毫秒。磁盘性能的急剧下降影响了应用程序的性能。
### 小结
在这个案例中服务器上的定时日志归档抢占了我们的资源导致应用程序速度临时下降即资源争用导致了性能掉队。我们再延伸一点来看除了这种情况外还有许多类似的资源争用都有可能引起这类问题例如我们有时候会在机器上装一些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种常见“绊脚石”及其应对策略那通过了解它们我们是否就有十足的信心能达到性能巅峰了呢
其实在实践中,我们往往还是很难的,特别是当前微服务大行其道,决定我们系统性能的因素往往是下游微服务,而下游微服务可能来源于不同的团队或组织。这时,已经不再单纯是技术本身的问题了,而是沟通、协调甚至是制度等问题。但是好在对于下游微服务,我们依然可以使用上面的分析思路来找出问题所在,不过通过上面的各种分析你也可以知道,让性能做到极致还是很难的,总有一些情况超出我们的预期,例如我们使用的磁盘发生损害,彻底崩溃前也会引起性能下降。
另外一个值得思考的问题是,是否有划算的成本收益比去做无穷无尽的优化,当然对于技术极客来说,能不能、让不让解决问题也许不是最重要的,剥丝抽茧、了解真相才是最有成就感的事儿。
感谢阅读,希望今天的分享能让你有所收获!如果你发现除了上述我介绍的那些“绊脚石”外,还有其它一些典型情况存在,也欢迎你在留言区中分享出来作为补充。