mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-02 15:23:44 +08:00
mod
This commit is contained in:
132
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/30 | 给系统加上眼睛:服务端监控要怎么做?.md
Normal file
132
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/30 | 给系统加上眼睛:服务端监控要怎么做?.md
Normal file
@@ -0,0 +1,132 @@
|
||||
<audio id="audio" title="30 | 给系统加上眼睛:服务端监控要怎么做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/7d/ded797848346d91981e7106262a7e47d.mp3"></audio>
|
||||
|
||||
你好,我是唐扬。
|
||||
|
||||
在一个项目的生命周期里,运行维护占据着很大的比重,在重要性上,它几乎与项目研发并驾齐驱。而在系统运维过程中,能够及时地发现问题并解决问题,是每一个团队的本职工作。所以,你的垂直电商系统在搭建之初,运维团队肯定完成了对于机器CPU、内存、磁盘、网络等基础监控,期望能在出现问题时,及时地发现并且处理。你本以为万事大吉,却没想到系统在运行过程中,频频得到用户的投诉,原因是:
|
||||
|
||||
- 使用的数据库主从延迟变长,导致业务功能上出现了问题;
|
||||
- 接口的响应时间变长,用户反馈商品页面出现空白页;
|
||||
- 系统中出现大量错误,影响了用户的正常使用。
|
||||
|
||||
这些问题,你本应该及时发现并处理的,但现实是,你只能被动地收到用户的反馈后,手忙脚乱地修复。这时你的团队才意识到,要想快速地发现和定位业务系统中出现的问题,必须搭建一套完善的服务端监控体系。正所谓“道路千万条,监控第一条,监控不到位,领导两行泪”。不过在搭建的过程中,你的团队又陷入了困境:
|
||||
|
||||
- 首先,监控的指标要如何选择呢?
|
||||
- 采集这些指标可以有哪些方法和途径呢?
|
||||
- 指标采集到之后又要如何处理和展示呢?
|
||||
|
||||
这些问题一环扣一环,关乎着系统的稳定性和可用性,而本节课,我就带你解决这些问题,搭建一套服务端监控体系。
|
||||
|
||||
## 监控指标如何选择
|
||||
|
||||
你在搭建监控系统时,所面临的第一个问题就是选择什么样的监控指标,也就是监控什么。有些同学在给一个新的系统设定监控指标的时候会比较迷茫,不知道从哪方面入手。其实,有一些成熟的理论和套路你可以直接拿来使用。比如,谷歌针对分布式系统监控的经验总结,四个黄金信号(Four Golden Signals)。它指的是在服务层面一般需要监控四个指标,**分别是延迟、通信量、错误和饱和度。**
|
||||
|
||||
<li>
|
||||
延迟指的是请求的响应时间。比如接口的响应时间、访问数据库和缓存的响应时间。
|
||||
</li>
|
||||
<li>
|
||||
通信量可以理解为吞吐量,也就是单位时间内请求量的大小。比如访问第三方服务的请求量,访问消息队列的请求量。
|
||||
</li>
|
||||
<li>
|
||||
错误表示当前系统发生的错误数量。**这里需要注意的是,** 我们需要监控的错误既有显式的,比如在监控Web服务时,出现4 * *和 5 * *的响应码;也有隐式的,比如Web服务虽然返回的响应码是200,但是却发生了一些和业务相关的错误(出现了数组越界的异常或者空指针异常等),这些都是错误的范畴。
|
||||
</li>
|
||||
<li>
|
||||
饱和度指的是服务或者资源到达上限的程度(也可以说是服务或者资源的利用率),比如CPU的使用率、内存使用率、磁盘使用率、缓存数据库的连接数等等。
|
||||
</li>
|
||||
|
||||
这四个黄金信号提供了通用的监控指标,**除此之外,你还可以借鉴RED指标体系。**这个体系是从四个黄金信号中衍生出来的,其中,R代表请求量(Request rate)、E代表错误(Error)、D代表响应时间(Duration),少了饱和度的指标。你可以把它当作一种简化版的通用监控指标体系。
|
||||
|
||||
当然,一些组件或者服务还有独特的指标,这些指标也是需要你特殊关注的。比如,课程中提到的数据库主从延迟数据、消息队列的堆积情况、缓存的命中率等等。我把高并发系统中常见组件的监控指标整理成了一张表格,其中没有包含诸如CPU、内存、网络、磁盘等基础监控指标,只是业务上监控指标,主要方便你在实际工作中参考使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1a/1a/1a29724ee8a33593797a5947d765f11a.jpg" alt="">
|
||||
|
||||
选择好了监控指标之后,你接下来要考虑的是如何从组件或者服务中采集到这些指标,也就是指标数据采集的问题。
|
||||
|
||||
## 如何采集数据指标
|
||||
|
||||
说到监控指标的采集,我们一般会依据采集数据源的不同选用不同的采集方式,**总结起来,大概有以下几种类型:**
|
||||
|
||||
首先,Agent是一种比较常见的采集数据指标的方式。
|
||||
|
||||
我们通过在数据源的服务器上部署自研或者开源的Agent来收集数据,发送给监控系统,实现数据的采集。在采集数据源上的信息时,Agent会依据数据源上提供的一些接口获取数据,**我给你举两个典型的例子。**
|
||||
|
||||
比如,你要从Memcached服务器上获取它的性能数据,那么,你就可以在Agent中连接这个Memcached服务器,并且发送一个stats命令,获取服务器的统计信息。然后,你就可以从返回的信息中,挑选重要的监控指标,发送给监控服务器,形成Memcached服务的监控报表。你也可以从这些统计信息中,看出当前Memcached服务器是否存在潜在的问题。下面是我推荐的一些重要的状态项,你可以参考使用。
|
||||
|
||||
```
|
||||
STAT cmd_get 201809037423 //计算查询的QPS
|
||||
STAT cmd_set 16174920166 //计算写入的QPS
|
||||
STAT get_hits 175226700643 //用来计算命中率,命中率 = get_hits/cmd_get
|
||||
STAT curr_connections 1416 //当前连接数
|
||||
STAT bytes 3738857307 //当前内存占用量
|
||||
STAT evictions 11008640149 //当前被memcached服务器剔除的item数
|
||||
量,如果这个数量过大(比如例子中的这个数值),那么代表当前Memcached容量不足或者Memcached Slab Class分配有问题
|
||||
|
||||
```
|
||||
|
||||
另外,如果你是Java的开发者,那么一般使用Java语言开发的中间件或者组件,都可以通过JMX获取统计或者监控信息。比如,在[19讲](https://time.geekbang.org/column/article/159487)中,我提到可以使用JMX监控Kafka队列的堆积数,再比如,你也可以通过JMX监控JVM内存信息和GC相关的信息。
|
||||
|
||||
另一种很重要的数据获取方式是在代码中埋点。
|
||||
|
||||
这个方式与Agent的不同之处在于,Agent主要收集的是组件服务端的信息,而埋点则是从客户端的角度来描述所使用的组件,和服务的性能和可用性。**那么埋点的方式怎么选择呢?**
|
||||
|
||||
你可以使用[25讲](https://time.geekbang.org/column/article/167979)分布式Trace组件中,提到的面向切面编程的方式;也可以在资源客户端中直接计算调用资源或者服务的耗时、调用量,并且发送给监控服务器。
|
||||
|
||||
**这里你需要注意一点,**由于调用缓存、数据库的请求量会比较高,一般单机也会达到每秒万次,如果不经过任何优化,把每次请求耗时都发送给监控服务器,那么监控服务器会不堪重负。所以,我们一般会在埋点时先做一些汇总。比如,每隔10秒汇总这10秒内对同一个资源的请求量总和、响应时间分位值、错误数等,然后发送给监控服务器。这样,就可以大大减少发往监控服务器的请求量了。
|
||||
|
||||
最后,日志也是你监控数据的重要来源之一。
|
||||
|
||||
你所熟知的Tomcat和Nginx的访问日志,都是重要的监控日志。你可以通过开源的日志采集工具,将这些日志中的数据发送给监控服务器。目前,常用的日志采集工具有很多,比如,[Apache Flume](http://flume.apache.org/)、[Fluentd](https://www.fluentd.org/)和[Filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html),你可以选择一种熟悉的使用。在我的项目中,我倾向于使用Filebeat来收集监控日志数据。
|
||||
|
||||
## 监控数据的处理和存储
|
||||
|
||||
在采集到监控数据之后,你就可以对它们进行处理和存储了。在此之前,我们一般会先用消息队列来承接数据,主要的作用是削峰填谷,防止写入过多的监控数据,让监控服务产生影响。
|
||||
|
||||
与此同时,我们一般会部署两个队列处理程序,来消费消息队列中的数据。
|
||||
|
||||
一个处理程序接收到数据后,把数据写入到Elasticsearch,然后通过Kibana展示数据,这些数据主要是用来做原始数据的查询。
|
||||
|
||||
另一个处理程序是一些流式处理的中间件,比如Spark、Storm。它们从消息队列里接收数据后会做一些处理,这些处理包括:
|
||||
|
||||
<li>
|
||||
解析数据格式,尤其是日志格式。从里面提取诸如请求量、响应时间、请求URL等数据;
|
||||
</li>
|
||||
<li>
|
||||
对数据做一些聚合运算。比如,针对Tomcat访问日志,可以计算同一个URL一段时间之内的请求量、响应时间分位值、非200请求量的大小等等。
|
||||
</li>
|
||||
<li>
|
||||
将数据存储在时间序列数据库中。这类数据库的特点是,可以对带有时间标签的数据做更有效的存储,而我们的监控数据恰恰带有时间标签,并且按照时间递增,非常适合存储在时间序列数据库中。目前业界比较常用的时序数据库有 InfluxDB、OpenTSDB、Graphite,各大厂的选择均有不同,你可以选择一种熟悉的来使用。
|
||||
</li>
|
||||
<li>
|
||||
最后,你就可以通过Grafana来连接时序数据库,将监控数据绘制成报表,呈现给开发和运维的同学了。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/62/88a8d8c2461297fed4e95214f4325e62.jpg" alt="">
|
||||
|
||||
至此,你和你的团队也就完成了垂直电商系统服务端监控系统搭建的全过程。这里我想再多说一点,我们从不同的数据源中采集了很多的指标,最终在监控系统中一般会形成以下几个报表,你在实际的工作中可以参考借鉴。
|
||||
|
||||
**1. 访问趋势报表。**这类报表接入的是Web服务器,和应用服务器的访问日志,展示了服务整体的访问量、响应时间情况、错误数量、带宽等信息。它主要反映的是服务的整体运行情况,帮助你来发现问题。
|
||||
|
||||
**2. 性能报表。** 这类报表对接的是资源和依赖服务的埋点数据,展示了被埋点资源的访问量和响应时间情况。它反映了资源的整体运行情况,当你从访问趋势报表发现问题后,可以先从性能报表中,找到究竟是哪一个资源或者服务出现了问题。
|
||||
|
||||
**3. 资源报表。** 这类报表主要对接的是,使用Agent采集的资源的运行情况数据。当你从性能报表中,发现某一个资源出现了问题,那么就可以进一步从这个报表中,发现资源究竟出现了什么问题,是连接数异常增高还是缓存命中率下降。这样可以进一步帮你分析问题的根源,找到解决问题的方案。
|
||||
|
||||
## 课程小结
|
||||
|
||||
本节课,我带你了解了服务端监控搭建的过程,在这里,你需要了解以下几个重点:
|
||||
|
||||
<li>
|
||||
耗时、请求量和错误数是三种最通用的监控指标,不同的组件还有一些特殊的监控指标,你在搭建自己的监控系统的时候可以直接使用;
|
||||
</li>
|
||||
<li>
|
||||
Agent、埋点和日志是三种最常见的数据采集方式;
|
||||
</li>
|
||||
<li>
|
||||
访问趋势报表用来展示服务的整体运行情况,性能报表用来分析资源或者依赖的服务是否出现问题,资源报表用来追查资源问题的根本原因。这三个报表共同构成了你的服务端监控体系。
|
||||
</li>
|
||||
|
||||
总之,监控系统是你发现问题,排查问题的重要工具,你应该重视它,并且投入足够的精力来不断地完善它。只有这样,才能不断地提高对系统运维的掌控力,降低故障发生的风险。
|
||||
|
||||
## 一课一思
|
||||
|
||||
在实际的工作中,你的服务端监控系统是如何搭建的呢?都有哪些监控报表和监控项呢?欢迎在留言区与我分享你的经验。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
153
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/31 | 应用性能管理:用户的使用体验应该如何监控?.md
Normal file
153
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/31 | 应用性能管理:用户的使用体验应该如何监控?.md
Normal file
@@ -0,0 +1,153 @@
|
||||
<audio id="audio" title="31 | 应用性能管理:用户的使用体验应该如何监控?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/20/7e/208b94860439cf4344aaebd99924ff7e.mp3"></audio>
|
||||
|
||||
你好,我是唐扬。
|
||||
|
||||
上一节课中,我带你了解了服务端监控搭建的过程。有了监控报表之后,你的团队在维护垂直电商系统时,就可以更早地发现问题,也有直观的工具辅助你们分析排查问题了。
|
||||
|
||||
不过你很快发现,有一些问题,服务端的监控报表无法排查,甚至无法感知。比如,有用户反馈创建订单失败,但是从服务端的报表来看,并没有什么明显的性能波动,从存储在Elasticsearch里的原始日志中,甚至没有找到这次创建订单的请求。这有可能是客户端有Bug,或者网络抖动导致创建订单的请求并没有发送到服务端。
|
||||
|
||||
再比如,有些用户会反馈,使用长城宽带打开商品详情页面特别慢,甚至出现DNS解析失败的情况。**那么,当我们遇到这类问题时,要如何排查和优化呢?**
|
||||
|
||||
这里面涉及一个概念叫应用性能管理(Application Performance Management,简称APM),**它的含义是:**对应用各个层面做全方位的监测,期望及时发现可能存在的问题,并加以解决,从而提升系统的性能和可用性。
|
||||
|
||||
你是不是觉得和之前讲到的服务端监控很相似?其实,服务端监控的核心关注点是后端服务的性能和可用性,而应用性能管理的核心关注点是终端用户的使用体验。也就是说,你需要衡量从客户端请求发出开始,到响应数据返回到客户端为止,这个端到端的整体链路上的性能情况。
|
||||
|
||||
如果你能做到这一点,那么无论是订单创建问题的排查,还是长城宽带用户页面打开缓慢的问题,都可以通过这套监控来发现和排查。**那么,如何搭建这么一套端到端的监控体系呢?**
|
||||
|
||||
## 如何搭建APM系统
|
||||
|
||||
与搭建服务端监控系统类似,在搭建端到端的应用性能管理系统时,我们也可以从数据的采集、存储和展示几个方面来思考。
|
||||
|
||||
首先,在数据采集方面,我们可以采用类似Agent的方式,在客户端上植入SDK,由SDK负责采集信息,并且经过采样之后,通过一个固定的接口定期发送给服务端。这个固定接口和服务端,我们可以称为APM通道服务。
|
||||
|
||||
虽然客户端需要监控的指标很多,比如监控网络情况、监控客户端卡顿情况、垃圾收集数据等等,但我们可以定义一种通用的数据采集格式。
|
||||
|
||||
比如在我之前的公司里,采集的数据包含下面几个部分,SDK将这几部分数据转换成JSON格式后,就可以发送给APM通道服务了。**这几部分数据格式,你可以在搭建自己的APM系统时直接拿来参考。**
|
||||
|
||||
- 系统部分:包括数据协议的版本号,以及下面提到的消息头、端消息体、业务消息体的长度;
|
||||
- 消息头:主要包括应用的标识(appkey),消息生成的时间戳,消息的签名以及消息体加密的秘钥;
|
||||
- 端消息体:主要存储客户端的一些相关信息,主要有客户端版本号、SDK版本号、IDFA、IDFV、IMEI、机器型号、渠道号、运营商、网络类型、操作系统类型、国家、地区、经纬度等等。由于这些信息有些比较敏感,所以我们一般会对信息加密;
|
||||
- 业务消息体:也就是真正要采集的数据,这些数据也需要加密。
|
||||
|
||||
**加密的方法是这样的:**我们首先会分配给这个应用一对RSA公钥和私钥,然后SDK在启动的时候,先请求一个策略服务,获取RSA公钥。在加密时,客户端会随机生成一个对称加密的秘钥Key,端消息体和业务消息体都会使用这个秘钥来加密。那么数据发到APM通道服务后,要如何解密呢?
|
||||
|
||||
客户端会使用RSA的公钥对秘钥加密,存储在消息头里面(也就是上面提到的,消息体加密秘钥),然后APM通道服务使用RSA秘钥解密得到秘钥,就可以解密得到端消息体和业务消息体的内容了。
|
||||
|
||||
最后,我们把消息头、端消息体、业务消息体还有消息头中的时间戳组装起来,用MD5生成摘要后,存储在消息头中(也就是消息的签名)。这样,APM通道服务在接收到消息后,可以使用同样的算法生成摘要,并且与发送过来的摘要比对,以防止消息被篡改。
|
||||
|
||||
数据被采集到APM通道服务之后,我们先对JSON消息做解析,得到具体的数据,然后发送到消息队列里面。从消息队列里面消费到数据之后,会写一份数据到Elasticsearch中,作为原始数据保存起来,再写一份到统计平台,以形成客户端的报表。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/5b/50fe40212d09940e1c5c1b860163a15b.jpg" alt="">
|
||||
|
||||
有了这套APM通道服务,我们就可以将从客户端上采集到的信息,通过统一的方式上报到服务端做集中处理。这样一来,你就可以收集到客户端上的性能数据和业务数据,能够及时地发现问题了。
|
||||
|
||||
那么问题来了:虽然你搭建了客户端监控系统,但是在我们电商系统客户端中可以收集到用户网络数据、卡顿数据等等,你是要把这些信息都监控到位,还是有所侧重呢?要知道,监控的信息不明确,会给问题排查带来不便,而这就是我们接下来探究的问题,也就是你到底需要监控用户的哪些信息。
|
||||
|
||||
## 需要监控用户的哪些信息
|
||||
|
||||
在我看来,搭建端到端的监控体系的首要目标,是解决如何监控客户端网络的问题,这是因为我们遇到的客户端问题,**大部分的原因还是出在客户端网络上。**
|
||||
|
||||
在中国复杂的网络环境下,大的运营商各行其是,各自为政,在不同的地区的链路质量各有不同,而小的运营商又鱼龙混杂,服务质量得不到保障。我给你说一个典型的问题。
|
||||
|
||||
之前在讲解DNS时,我曾经提到在做DNS解析的时候,为了缩短查询的链路,首先会查询运营商的Local DNS。但是,Local DNS这个东西很不靠谱,有些小的运营商为了节省流量,他会把一些域名解析到内容缓存服务器上,甚至会解析到广告或者钓鱼网站上去,这就是域名劫持。
|
||||
|
||||
也有一些运营商比较懒,自己不去解析域名,而是把解析请求转发到别的运营商上,这就导致权威DNS收到请求的来源IP的运营商是不正确的。这样一来,解析的IP和请求源,会来自不同的运营商,形成跨网的流量,导致DNS解析时间过长。这些需要我们实时监控,以尽快地发现问题,反馈给运营商来解决。
|
||||
|
||||
**那么,我们如何采集网络数据呢?**一般来说,我们会用埋点的方式,将网络请求的每一个步骤耗时情况、是否发生错误都打印下来,我以安卓系统为例,解释一下是如何做的。
|
||||
|
||||
安卓一般会使用OkHttpClient来请求接口数据,而OkHttpClient提供了EventListner接口,可以让调用者接收到网络请求事件,比如,开始解析DNS事件,解析DNS结束的事件等等。那么你就可以埋点计算出一次网络请求的各个阶段的耗时情况。我写了一段具体的示例代码,计算了一次请求的DNS解析时间,你可以拿去参考。
|
||||
|
||||
```
|
||||
public class HttpEventListener extends EventListener {
|
||||
|
||||
final static AtomicLong nextCallId = new AtomicLong(1L);
|
||||
private final long callId;
|
||||
private long dnsStartTime;
|
||||
|
||||
private HttpUrl url ;
|
||||
|
||||
public HttpEventListener(HttpUrl url) {
|
||||
this.callId = nextCallId.getAndIncrement(); //初始化唯一标识这次请求的ID
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dnsStart(Call call, String domainName) {
|
||||
super.dnsStart(call, domainName);
|
||||
this.dnsStartTime = System.nanoTime(); //记录dns开始时间
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
|
||||
super.dnsEnd(call, domainName, inetAddressList);
|
||||
System.out.println("url: " + url.host() + ", Dns time: " + (System.nanoTime() - dnsStartTime)); //计算dns解析的时间
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
有了这个EventListner,你就可以在初始化HttpClient的时候把它注入进去,代码如下:
|
||||
|
||||
```
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||
.eventListenerFactory(new Factory() {
|
||||
@Override
|
||||
public EventListener create(Call call) {
|
||||
return new HttpEventListener(call.request().url());
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
这样,我们可以得出一次请求过程中经历的一系列过程的时间,其中主要包括下面几项。
|
||||
|
||||
1. 等待时间:异步调用时,请求会首先缓存在本地的队列里面,由专门的I/O线程负责,那么在I/O线程真正处理请求之前,会有一个等待的时间。
|
||||
1. DNS时间:域名解析时间。
|
||||
1. 握手时间:TCP三次握手的时间。
|
||||
1. SSL时间:如果服务是HTTPS服务,那么就会有一个SSL认证的时间。
|
||||
1. 发送时间:请求包被发送出去的时间。
|
||||
1. 首包时间:服务端处理后,客户端收到第一个响应包的时间。
|
||||
1. 包接收时间:我们接收到所有数据的时间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/ba/33c2f9e1182813fadc0f6b8066379fba.jpg" alt="">
|
||||
|
||||
有了这些数据之后,我们可以通过上面提到的APM通道服务发送给服务端,这样服务端和客户端的同学,就可以从Elasticsearch中查询到原始的数据,也可以对数据做一些聚合处理、统计分析之后,形成客户端请求监控的报表。这样,我们就可以有针对性地对HTTP请求的某一个过程做优化了。
|
||||
|
||||
而对于用户网络的监控,可以给你带来三方面的价值。
|
||||
|
||||
首先,这种用户网络监控的所有监控数据均来自客户端,是用户访问数据实时上报,因此能够准确、真实、实时地反映用户操作体验。
|
||||
|
||||
再者,它是我们性能优化的指向标,业务架构改造、服务性能优化、网络优化等任何优化行为时,可以反馈用户性能数据,引导业务正向优化接口性能、可用性等指标。
|
||||
|
||||
最后,它也能帮助我们监控CDN链路质量。之前的CDN的监控严重依赖CDN厂商,这有一个问题是:CDN无法从端上获取到全链路的监控数据,有些时候,客户端到CDN的链路出了问题,CDN厂商是感知不到的,而客户端监控弥补了这方面的缺陷,并且可以通过告警机制督促CDN及时优化调整问题线路。
|
||||
|
||||
除了上报网络数据之外,我们还可以上报一些异常事件的数据,比如你的垂直电商系统可能会遇到以下一些异常情况:
|
||||
|
||||
- 登录失败;
|
||||
- 下单失败;
|
||||
- 浏览商品信息失败
|
||||
- 评论列表加载不出来;
|
||||
- 无法评分留言;
|
||||
- …
|
||||
|
||||
你在业务逻辑的代码中,都可以检测到这些异常数据,当然,也可以通过APM通道服务上传到服务端,这样方便服务端和客户端的同学一起来排查问题,也能给你的版本灰度提供数据的支持。
|
||||
|
||||
总的来说,如果说搭建的系统是骨架,那么具体监控的数据就是灵魂,因为数据是监控的主体内容,系统只是呈现数据的载体。所以,你需要在系统运维的过程中不断完善对数据的收集,这也是对你的监控系统不断升级完善的过程。
|
||||
|
||||
## 课程小结
|
||||
|
||||
以上就是本节课的全部内容了。本节课,我主要带你了解了如何搭建一个端到端的APM监控系统,你需要了解的重点是:
|
||||
|
||||
1.从客户端采集到的数据可以用通用的消息格式,上传到APM服务端,服务端将数据存入到Elasticsearch中,以提供原始日志的查询,也可以依据这些数据形成客户端的监控报表;
|
||||
|
||||
2.用户网络数据是我们排查客户端,和服务端交互过程的重要数据,你可以通过代码的植入,来获取到这些数据;
|
||||
|
||||
3.无论是网络数据,还是异常数据,亦或是卡顿、崩溃、流量、耗电量等数据,你都可以通过把它们封装成APM消息格式,上传到APM服务端,这些用户在客户端上留下的踪迹可以帮助你更好地优化用户的使用体验。
|
||||
|
||||
**总而言之,监测和优化用户的使用体验是应用性能管理的最终目标。**然而,服务端的开发人员往往会陷入一个误区,认为我们将服务端的监控做好,保证接口性能和可用性足够好就好了。事实上,接口的响应时间只是我们监控系统中很小的一部分,搭建一套端到端的全链路的监控体系,才是你的监控系统的最终形态。
|
||||
|
||||
## 一课一思
|
||||
|
||||
在实际的工作中,你的团队是如何通过监控,发现客户端上的问题的呢?欢迎在留言区与我分享你的经验。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
117
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/32 | 压力测试:怎样设计全链路压力测试平台?.md
Normal file
117
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/32 | 压力测试:怎样设计全链路压力测试平台?.md
Normal file
@@ -0,0 +1,117 @@
|
||||
<audio id="audio" title="32 | 压力测试:怎样设计全链路压力测试平台?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/22/90/221dbf37a0f6fa2fd5e696e749938290.mp3"></audio>
|
||||
|
||||
你好,我是唐扬。
|
||||
|
||||
经过两节课的学习,我们已经搭建了服务端和客户端的监控,通过监控的报表和一些报警规则的设置,你可以实时地跟踪和解决垂直电商系统中出现的问题了。不过,你不能掉以轻心,因为监控只能发现目前系统中已经存在的问题,对于未来可能发生的性能问题是无能为力的。
|
||||
|
||||
一旦你的系统流量有大的增长,比如类似“双十一”的流量,那么你在面临性能问题时就可能会手足无措。为了解决后顾之忧,你需要了解在流量增长若干倍的时候,系统的哪些组件或者服务会成为整体系统的瓶颈点,这时你就需要做一次全链路的压力测试。
|
||||
|
||||
那么,什么是压力测试呢?要如何来做全链路的压测呢?这两个问题就是本节课重点讲解的内容。
|
||||
|
||||
## 什么是压力测试
|
||||
|
||||
压力测试(简称为压测)这个名词儿,你在业界的分享中一定听过很多次,当然了,你也可能在项目的研发过程中做过压力测试,所以,对于你来说,压力测试并不陌生。
|
||||
|
||||
不过我想让你回想一下,自己是怎么做压力测试的?是不是像很多同学一样:先搭建一套与正式环境功能相同的测试环境,并且导入或者生成一批测试数据,然后在另一台服务器,启动多个线程并发地调用需要压测的接口(接口的参数一般也会设置成相同的,比如,想要压测获取商品信息的接口,那么压测时会使用同一个商品ID)。最后,通过统计访问日志或者查看测试环境的监控系统,来记录最终压测QPS是多少之后,直接交差?
|
||||
|
||||
这么做压力测试其实是不正确的,**错误之处主要有以下几点。**
|
||||
|
||||
1.首先,做压力测试时,最好使用线上的数据和线上的环境。因为,你无法确定自己搭建的测试环境与正式环境的差异,是否会影响到压力测试的结果。
|
||||
|
||||
2.其次,压力测试时不能使用模拟的请求而是要使用线上的流量。你可以通过拷贝流量的方式,把线上流量拷贝一份到压力测试环境。因为模拟流量的访问模型和线上流量相差很大,会对压力测试的结果产生比较大的影响。
|
||||
|
||||
比如,你在获取商品信息的时候,线上的流量会获取不同商品的数据,这些商品的数据有的命中了缓存,有的没有命中缓存。如果使用同一个商品ID来做压力测试,那么只有第一次请求没有命中缓存,而在请求之后会将数据库中的数据回种到缓存,后续的请求就一定会命中缓存了,这种压力测试的数据就不具备参考性了。
|
||||
|
||||
3.不要从一台服务器发起流量,这样很容易达到这台服务器性能瓶颈,从而导致压力测试的QPS上不去,最终影响压力测试的结果。而且,为了尽量真实地模拟用户请求,我们倾向于把流量产生的机器放在离用户更近的位置,比如放在CDN节点上。如果没有这个条件,那么可以放在不同的机房中,这样可以尽量保证压力测试结果的真实性。
|
||||
|
||||
之所以有很多同学出现这个问题,主要是对压力测试的概念没有完全理解,以为只要是使用多个线程并发的请求服务接口,就算是对接口进行压力测试了。
|
||||
|
||||
**那么究竟什么是压力测试呢?**压力测试指的是在高并发大流量下进行的测试,测试人员可以通过观察系统在峰值负载下的表现,从而找到系统中存在的性能隐患。
|
||||
|
||||
与监控一样,压力测试是一种常见的发现系统中存在问题的方式,也是保障系统可用性和稳定性的重要手段。而在压力测试的过程中,我们不能只针对某一个核心模块来做压测,而需要将接入层、所有后端服务、数据库、缓存、消息队列、中间件以及依赖的第三方服务系统及其资源,都纳入压力测试的目标之中。因为,一旦用户的访问行为增加,包含上述组件服务的整个链路都会受到不确定的大流量的冲击。因此,它们都需要依赖压力测试来发现可能存在的性能瓶颈,**这种针对整个调用链路执行的压力测试也称为“全链路压测”。**
|
||||
|
||||
由于在互联网项目中,功能迭代的速度很快,系统的复杂性也变得越来越高,新增加的功能和代码很可能会成为新的性能瓶颈点。也许半年前做压力测试时,单台机器可以承担每秒1000次请求,现在很可能就承担每秒800次请求了。所以,压力测试应该作为系统稳定性保障的常规手段,周期性地进行。
|
||||
|
||||
但是,通常做一次全链路压力测试,需要联合DBA、运维、依赖服务方、中间件架构等多个团队,一起协同进行,无论是人力成本还是沟通协调的成本都比较高。同时,在压力测试的过程中,如果没有很好的监控机制,那么还会对线上系统造成不利的影响。**为了解决这些问题,我们需要搭建一套自动化的全链路压测平台来降低成本和风险。**
|
||||
|
||||
## 如何搭建全链路压测平台
|
||||
|
||||
搭建全链路压测平台,主要有两个关键点。
|
||||
|
||||
一点是流量的隔离。由于压力测试是在正式环境进行,所以需要区分压力测试流量和正式流量,这样可以针对压力测试的流量做单独的处理。
|
||||
|
||||
另一点是风险的控制。也就是尽量避免压力测试对于正常访问用户的影响。因此,一般来说全链路压测平台需要包含以下几个模块:
|
||||
|
||||
- 流量构造和产生模块;
|
||||
- 压测数据隔离模块;
|
||||
- 系统健康度检查和压测流量干预模块。
|
||||
|
||||
整体压测平台的架构图可以是下面这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/ff/1552e524d495bb7e129405578b7907ff.jpg" alt="">
|
||||
|
||||
为了让你能够更清晰地了解每一个模块是如何实现的,方便你来设计适合自身业务的全链路压测平台,我会对压测平台的每一个模块做更细致的介绍。先来看看压力测试的流量是如何产生的。
|
||||
|
||||
#### 压测数据的产生
|
||||
|
||||
一般来说,我们系统的入口流量是来自于客户端的HTTP请求。所以,我们会考虑在系统高峰期时,将这些入口流量拷贝一份,在经过一些流量清洗的工作之后(比如过滤一些无效的请求),将数据存储在像是HBase、MongoDB这些NoSQL存储组件或者亚马逊S3这些云存储服务中,我们称之为流量数据工厂。
|
||||
|
||||
这样,当我们要压测的时候,就可以从这个工厂中获取数据,将数据切分多份后下发到多个压测节点上了。**在这里,我想强调几个,你需要特殊注意的点。**
|
||||
|
||||
首先,我们可以使用多种方式来实现流量的拷贝。最简单的一种方式:直接拷贝负载均衡服务器的访问日志,数据就以文本的方式写入到流量数据工厂中。但是这样产生的数据在发起压测时,需要自己写解析的脚本来解析访问日志,会增加压测时候的成本,不太建议使用。
|
||||
|
||||
另一种方式:通过一些开源的工具来实现流量的拷贝。这里,我推荐一款轻型的流量拷贝工具[GoReplay](https://github.com/buger/goreplay),它可以劫持本机某一个端口的流量,将它们记录在文件中,传送到流量数据工厂中。在压测时,你也可以使用这个工具进行加速的流量回放,这样就可以实现对正式环境的压力测试了。
|
||||
|
||||
其次,如上文中提到,我们在下发压测流量时,需要保证下发流量的节点与用户更加接近,起码不能和服务部署节点在同一个机房中,这样可以尽量保证压测数据的真实性。
|
||||
|
||||
另外,我们还需要对压测流量染色,也就是增加压测标记。在实际项目中,我会在HTTP的请求头中增加一个标记项,比如说叫做is stress test,在流量拷贝之后,批量在请求中增加这个标记项,再写入到数据流量工厂中。
|
||||
|
||||
#### 数据如何隔离
|
||||
|
||||
将压测流量拷贝下来的同时,我们也需要考虑对系统做改造,以实现压测流量和正式流量的隔离,这样一来就会尽量避免压测对线上系统的影响。一般来说,我们需要做两方面的事情。
|
||||
|
||||
一方面,针对读取数据的请求(一般称之为下行流量),我们会针对某些不能压测的服务或者组件,做Mock或者特殊的处理,举个例子。
|
||||
|
||||
在业务开发中,我们一般会依据请求记录用户的行为,比如,用户请求了某个商品的页面,我们会记录这个商品多了一次浏览的行为,这些行为数据会写入一份单独的大数据日志中,再传输给数据分析部门,形成业务报表给到产品或者老板做业务的分析决策。
|
||||
|
||||
在压测的时候,肯定会增加这些行为数据,比如原本一天商品页面的浏览行为是一亿次,而压测之后变成了十亿次,这样就会对业务报表产生影响,影响后续的产品方向的决策。因此,我们对于这些压测产生的用户行为做特殊处理,不再记录到大数据日志中。
|
||||
|
||||
再比如,我们系统会依赖一些推荐服务,推荐一些你可能感兴趣的商品,但是这些数据的展示有一个特点就是展示过的商品就不再会被推荐出来。如果你的压测流量经过这些推荐服务,大量的商品就会被压测流量请求到,线上的用户就不会再看到这些商品了,也就会影响推荐的效果。
|
||||
|
||||
所以,我们需要Mock这些推荐服务,让不带有压测标记的请求经过推荐服务,而让带有压测标记的请求经过Mock服务。搭建Mock服务,你需要注意一点:这些Mock服务最好部署在真实服务所在的机房,这样可以尽量模拟真实的服务部署结构,提高压测结果的真实性。
|
||||
|
||||
另一方面,针对写入数据的请求(一般称之为上行流量),我们会把压测流量产生的数据写入到影子库,也就是和线上数据存储完全隔离的一份存储系统中。针对不同的存储类型,我们会使用不同的影子库的搭建方式。
|
||||
|
||||
1. 如果数据存储在MySQL中,我们可以在同一个MySQL实例,不同的Schema中创建一套和线上相同的库表结构,并且把线上的数据也导入进来。
|
||||
1. 而如果数据是放在Redis中,我们对压测流量产生的数据,增加一个统一的前缀,存储在同一份存储中。
|
||||
1. 还有一些数据会存储在Elasticsearch中,针对这部分数据,我们可以放在另外一个单独的索引表中。
|
||||
|
||||
通过对下行流量的特殊处理以及对上行流量增加影子库的方式,我们就可以实现压测流量的隔离了。
|
||||
|
||||
#### 压力测试如何实施
|
||||
|
||||
在拷贝了线上流量和完成了对线上系统的改造之后,我们就可以进行压力测试的实施了。在此之前,一般会设立一个压力测试的目标,比如说,整体系统的QPS需要达到每秒20万。
|
||||
|
||||
不过,在压测时,不会一下子把请求量增加到每秒20万次,而是按照一定的步长(比如每次压测增加一万QPS),逐渐地增加流量。在增加一次流量之后,让系统稳定运行一段时间,观察系统在性能上的表现。如果发现依赖的服务或者组件出现了瓶颈,可以先减少压测流量,比如,回退到上一次压测的QPS,保证服务的稳定,再针对此服务或者组件进行扩容,然后再继续增加流量压测。
|
||||
|
||||
为了能够减少压力测试过程中人力投入成本,可以开发一个流量监控的组件,在这个组件中,预先设定一些性能阈值。比如,容器的CPU使用率的阈值可以设定为60%~70%;系统的平均响应时间的上限可以设定为1秒;系统慢请求的比例设置为1%等等。
|
||||
|
||||
当系统性能达到这个阈值之后,流量监控组件可以及时发现,并且通知压测流量下发组件减少压测流量,并且发送报警给到开发和运维的同学,开发和运维同学就迅速地排查性能瓶颈,在解决问题或者扩容之后再继续执行压测。
|
||||
|
||||
业界关于全链路压测平台的探索有很多,一些大厂比如阿里、京东、美团和微博都有了适合自身业务的全链路压测平台。在我看来,这些压测平台万变不离其宗,都无非是经过流量拷贝、流量染色隔离、打压、监控熔断等步骤,与本课程中介绍的核心思想都是相通的。因此,你在考虑自研适合自己项目的全链路压测平台时,也可以遵循这个成熟的套路。
|
||||
|
||||
## 课程小结
|
||||
|
||||
本节课,我带你了解了做压力测试常见的误区,以及自动化的全链路压测平台的搭建过程,这里你需要了解的重点是:
|
||||
|
||||
1. 压力测试是一种发现系统性能隐患的重要手段,所以应该尽量使用正式的环境和数据;
|
||||
1. 对压测的流量需要增加标记,这样就可以通过Mock第三方依赖服务和影子库的方式来实现压测数据和正式数据的隔离;
|
||||
1. 压测时,应该实时地对系统性能指标做监控和告警,及时地对出现瓶颈的资源或者服务扩容,避免对正式环境产生影响。
|
||||
|
||||
**这套全链路的压力测试系统对于我们来说有三方面的价值:**其一,它可以帮助我们发现系统中可能出现的性能瓶颈,方便我们提前准备预案来应对;其次,它也可以为我们做容量评估,提供数据上的支撑;最后,我们也可以在压测的时候做预案演练,因为压测一般会安排在流量的低峰期进行,这样我们可以降级一些服务来验证预案效果,并且可以尽量减少对线上用户的影响。所以,随着你的系统流量的快速增长,你也需要及时考虑搭建这么一套全链路压测平台,来保证你的系统的稳定性。
|
||||
|
||||
## 一课一思
|
||||
|
||||
在实际的工作中,你的系统的压力测试是如何进行的呢?在压力测试的过程中发现了哪些性能瓶颈点呢?欢迎在留言区与我分享你的经验。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
121
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/33 | 配置管理:成千上万的配置项要如何管理?.md
Normal file
121
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/33 | 配置管理:成千上万的配置项要如何管理?.md
Normal file
@@ -0,0 +1,121 @@
|
||||
<audio id="audio" title="33 | 配置管理:成千上万的配置项要如何管理?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/e6/f5e4533ed12daafd52313b611d7bd5e6.mp3"></audio>
|
||||
|
||||
你好,我是唐扬。
|
||||
|
||||
相信在实际工作中,提及性能优化你会想到代码优化,但是实际上有些性能优化可能只需要调整一些配置参数就可以搞定了。为什么这么说呢?我给你举几个例子:
|
||||
|
||||
- 你可以调整配置的超时时间让请求快速失败,防止系统的雪崩,提升系统的可用性;
|
||||
- 你还可以调整HTTP客户端连接池的大小,来提升调用第三方HTTP服务的并行处理能力,从而提升系统的性能。
|
||||
|
||||
你可以认为配置是管理你系统的工具,在你的垂直电商系统中,一定会有非常多的配置项,比如数据库的地址、请求HTTP服务的域名、本地内存最大缓存数量等等。
|
||||
|
||||
那么,你要如何对这些配置项做管理呢?管理的过程中要注意哪些事情呢?
|
||||
|
||||
## 如何对配置进行管理
|
||||
|
||||
配置管理由来已久,在Linux系统中就提供了大量的配置项,你可以根据自身业务的实际情况,动态地对系统功能做调整。比如,你可以通过修改dirty_writeback_centisecs参数的数值,调整Page Cache中脏数据刷新到磁盘上的频率;你也可以通过修改tcp_max_syn_backlog参数的值,来调整未建立连接队列的长度。而你既可以通过修改配置文件并且重启服务器来让配置生效,也可以通过sysctl命令来动态地调整,让配置即时生效。
|
||||
|
||||
那么在开发应用的时候,都有哪些管理配置的方式呢?我觉得主要有两种:
|
||||
|
||||
- 一种是通过配置文件来管理;
|
||||
- 另一种是使用配置中心来管理。
|
||||
|
||||
以电商系统为例,你和你的团队在刚开始开发垂直电商系统时,为了加快产品的研发速度,大概率不会注意配置管理的问题,会自然而然地把配置项和代码写在一起。但是随着配置项越来越多,为了更好地对配置项进行管理,同时避免修改配置项后还要重新对代码做编译,你选择把配置项拆分成独立的文件(文件可以是properties格式、xml格式或yaml格式)。不过,这些文件还是会和工程一起打包部署,只是更改配置后不需要重新编译代码了。
|
||||
|
||||
**随后,你很快发现了一个问题:**虽然把配置拆分了出来,但是由于配置还是和代码打包在一起,如果要更改一个配置还是需要重新打包,这样无疑会增加打包的时间。于是,你考虑把配置写到单独的目录中,这样,修改配置就不需要再重新打包了(不过,由于配置并不能够实时生效,所以想让配置生效,还是需要重启服务)。
|
||||
|
||||
我们一般使用的基础组件,比如Tomcat、Nginx,都是采用上面这种配置文件的方式来管理配置项的,而在Linux系统中,我提到的tcp_max_syn_backlog就可以配置在/etc/sysctl.conf中。
|
||||
|
||||
**这里,我需要强调一点,我们通常会把配置文件存储的目录标准化为特定的目录。**比如,都配置成/data/confs目录,然后把配置项使用Git等代码仓库管理起来。这样,在增加新的机器时,在机器初始化脚本中只需要创建这个目录,再从Git中拉取配置就可以了。这是一个标准化的过程,可以避免在启动应用时忘记部署配置文件。
|
||||
|
||||
再进一步说,如果你的服务是多机房部署的,那么不同机房的配置项中有可能是相同的,也有可能是不同的。这时候,你需要将相同的配置项放置在一个目录中给多个机房共用,再将不同的配置项放置在以机房名为名称的目录中。在读取配置的时候应该优先读取机房的配置,再读取公共配置,这样可以减少配置文件中的配置项的数量。
|
||||
|
||||
我给你列了一个典型目录配置,如果你的系统也使用文件来管理配置,可以参考一下。
|
||||
|
||||
```
|
||||
/data/confs/common/commerce //电商业务的公共配置
|
||||
/data/confs/commerce-zw //电商业务兆维机房配置
|
||||
/data/confs/commerce-yz //电商业务亦庄机房配置
|
||||
|
||||
/data/confs/common/community //社区业务的公共配置
|
||||
/data/confs/community-zw //社区业务兆维机房配置
|
||||
/data/confs/community-yz //社区业务亦庄机房配置
|
||||
|
||||
```
|
||||
|
||||
那么,这是不是配置管理的最终形态呢?当然不是,你不要忘了把配置放在文件中的方式还存在的问题(我上面也提到过了),那就是,我们必须将服务重启后才能让配置生效。有没有一种方法可以在不重启应用的前提下也能让配置生效呢?这就需要配置中心帮助我们实现了。
|
||||
|
||||
## 配置中心是如何实现的
|
||||
|
||||
配置中心可以算是微服务架构中的一个标配组件了。业界也提供了多种开源方案供你选择,比较出名的有携程开源的Apollo、百度开源的Disconf、360开源的QConf、Spring Cloud的组件Spring Cloud Config等等。
|
||||
|
||||
在我看来,Apollo支持不同环境,不同集群的配置,有完善的管理功能,支持灰度发布、更改热发布等功能,**在所有配置中心中功能比较齐全,推荐你使用。**
|
||||
|
||||
那么,配置中心的组件在实现上有哪些关键的点呢?如果你想对配置中心组件有更强的把控力,想要自行研发一套符合自己业务场景的组件,又要如何入手呢?
|
||||
|
||||
#### 配置信息如何存储
|
||||
|
||||
其实,配置中心和注册中心非常类似,其核心的功能就是**配置项的存储和读取**。所以,在设计配置中心的服务端时,我们需要选择合适的存储组件来存储大量的配置信息,这里可选择的组件有很多。
|
||||
|
||||
事实上,不同的开源配置中心也使用了不同的组件,比如Disconf、Apollo使用的是MySQL;QConf使用的是ZooKeeper。我之前维护和使用的配置中心就会使用不同的存储组件,比如微博的配置中心使用Redis来存储信息,而美图用的是Etcd。
|
||||
|
||||
无论使用哪一种存储组件,你所要做的就是规范配置项在其中的存储结构。比如,我之前使用的配置中心用Etcd作为存储组件,支持存储全局配置、机房配置和节点配置。其中,节点配置优先级高于机房配置,机房配置优先级高于全局配置。也就是说,我们会优先读取节点的配置,如果节点配置不存在,再读取机房配置,最后读取全局配置。它们的存储路径如下:
|
||||
|
||||
```
|
||||
/confs/global/{env}/{project}/{service}/{version}/{module}/{key} //全局配置
|
||||
/confs/regions/{env}/{project}/{service}/{version}/{region}/{module}/{key} //机房配置
|
||||
/confs/nodes/{env}/{project}/{service}/{version}/{region}/{node}/{module}/{key} //节点配置
|
||||
|
||||
```
|
||||
|
||||
#### 变更推送如何实现
|
||||
|
||||
配置信息存储之后,我们需要考虑如何将配置的变更推送给服务端,这样就可以实现配置的动态变更,也就是说不需要重启服务器就能让配置生效了。而我们一般会有两种思路来实现变更推送:一种是轮询查询的方式,一种是长连推送的方式。
|
||||
|
||||
轮询查询很简单,就是应用程序向配置中心客户端注册一个监听器,配置中心的客户端,定期地(比如1分钟)查询所需要的配置是否有变化,如果有变化则通知触发监听器,让应用程序得到变更通知。
|
||||
|
||||
**这里有一个需要注意的点,**如果有很多应用服务器都去轮询拉取配置,由于返回的配置项可能会很大,那么配置中心服务的带宽就会成为瓶颈。为了解决这个问题,我们会给配置中心的每一个配置项多存储一个根据配置项计算出来的MD5值。
|
||||
|
||||
配置项一旦变化,这个MD5值也会随之改变。配置中心客户端在获取到配置的同时,也会获取到配置的MD5值,并且存储起来。那么在轮询查询的时候,需要先确认存储的MD5值和配置中心的MD5是不是一致的。如果不一致,这就说明配置中心里存储的配置项有变化,然后才会从配置中心拉取最新的配置。
|
||||
|
||||
由于配置中心里存储的配置项变化的几率不大,所以使用这种方式后,每次轮询请求就只是返回一个MD5值,可以大大地减少配置中心服务器的带宽。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e8/7f/e846f4c4418f8ca137a1fd2dcbbb3b7f.jpg" alt="">
|
||||
|
||||
另一种长连的方式,它的逻辑是在配置中心服务端保存每个连接关注的配置项列表。这样当配置中心感知到配置变化后,就可以通过这个连接把变更的配置推送给客户端。这种方式需要保持长连,也需要保存连接和配置的对应关系,实现上要比轮询的方式复杂一些,但是相比轮询方式来说,能够更加实时地获取配置变更的消息。
|
||||
|
||||
而在我看来,配置服务中存储的配置变更频率不高,所以对于实时性要求不高,但是期望实现上能够足够简单,**那么如果选择自研配置中心的话,可以考虑使用轮询的方式。**
|
||||
|
||||
#### 如何保证配置中心高可用
|
||||
|
||||
除了变更通知以外,在配置中心实现中另外一个比较关键的点在于如何保证它的可用性。因为对于配置中心来说,可用性的重要程度要远远大于性能。
|
||||
|
||||
我们一般会在服务器启动时从配置中心中获取配置,如果配置获取的性能不高,那么外在的表现也只是应用启动时间慢了,对于业务的影响不大。但是,如果获取不到配置,很可能会导致启动失败。
|
||||
|
||||
比如,我们把数据库的地址存储在了配置中心里,如果配置中心宕机导致我们无法获取数据库的地址,那么自然应用程序就会启动失败。**因此,我们的诉求是让配置中心“旁路化”。**也就是说,即使配置中心宕机,或者配置中心依赖的存储宕机,我们仍然能够保证应用程序是可以启动的。那么这是如何实现的呢?
|
||||
|
||||
我们一般会在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓存是文件的缓存。
|
||||
|
||||
配置中心客户端在获取到配置信息后,会同时把配置信息同步地写入到内存缓存,并且异步地写入到文件缓存中。内存缓存的作用是降低客户端和配置中心的交互频率,提升配置获取的性能;而文件的缓存的作用就是灾备,当应用程序重启时,一旦配置中心发生故障,那么应用程序就会优先使用文件中的配置,这样虽然无法得到配置的变更消息(因为配置中心已经宕机了),但是应用程序还是可以启动起来的,算是一种降级的方案。
|
||||
|
||||
## 课程小结
|
||||
|
||||
以上就是本节课的全部内容了。在这节课中,我带你了解了系统开发的过程中,我们是如何管理大量的配置项的,你需要了解的重点是:
|
||||
|
||||
<li>
|
||||
配置存储是分级的,有公共配置,有个性的配置,一般个性配置会覆盖公共配置,这样可以减少存储配置项的数量;
|
||||
</li>
|
||||
<li>
|
||||
配置中心可以提供配置变更通知的功能,可以实现配置的热更新;
|
||||
</li>
|
||||
<li>
|
||||
配置中心关注的性能指标中,可用性的优先级是高于性能的,一般我们会要求配置中心的可用性达到99.999%,甚至会是99.9999%。
|
||||
</li>
|
||||
|
||||
这里你需要注意的是,并不是所有的配置项都需要使用配置中心来存储,如果你的项目还是使用文件方式来管理配置,那么你只需要将类似超时时间等,需要动态调整的配置,迁移到配置中心就可以了。对于像是数据库地址,依赖第三方请求的地址,这些基本不会发生变化的配置项,可以依然使用文件的方式来管理,这样可以大大地减少配置迁移的成本。
|
||||
|
||||
## 一课一思
|
||||
|
||||
结合实际情况谈一谈,你的项目中配置管理的方式是怎样的呢?欢迎在留言区与我分享你的经验。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
162
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/34 | 降级熔断:如何屏蔽非核心系统故障的影响?.md
Normal file
162
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/34 | 降级熔断:如何屏蔽非核心系统故障的影响?.md
Normal file
@@ -0,0 +1,162 @@
|
||||
<audio id="audio" title="34 | 降级熔断:如何屏蔽非核心系统故障的影响?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/22/2e/224ff47452ba99c8a3a58a858435712e.mp3"></audio>
|
||||
|
||||
你好,我是唐扬。
|
||||
|
||||
到目前为止,你的电商系统已经搭建了完善的服务端和客户端监控系统,并且完成了全链路压测。现在呢,你们已经发现和解决了垂直电商系统中很多的性能问题和隐患。但是千算万算,还是出现了纰漏。
|
||||
|
||||
本来,你们对于应对“双十一”的考验信心满满,但因为欠缺了一些面对巨大流量的经验,在促销过程中出现了几次短暂的服务不可用,这给部分用户造成了不好的使用体验。事后,你们进行了细致的复盘,追查出现故障的根本原因,你发现,原因主要可以归结为两大类。
|
||||
|
||||
<li>
|
||||
第一类原因是由于依赖的资源或者服务不可用,最终导致整体服务宕机。举例来说,在你的电商系统中就可能由于数据库访问缓慢,导致整体服务不可用。
|
||||
</li>
|
||||
<li>
|
||||
另一类原因是你们乐观地预估了可能到来的流量,当有超过系统承载能力的流量到来时,系统不堪重负,从而出现拒绝服务的情况。
|
||||
</li>
|
||||
|
||||
那么,你要如何避免再次出现这两类问题呢?我建议你采取降级、熔断以及限流的方案。限流是解决第二类问题的主要思路(下一节课,我会着重讲解)。今天这节课,我主要讲一下解决第一类问题的思路:降级和熔断。
|
||||
|
||||
不过在此之前,我先带你了解一下这个问题为何存在,因为你只有弄清楚出现故障的原理,才能更好地理解熔断降级带来的好处。
|
||||
|
||||
## 雪崩是如何发生的
|
||||
|
||||
局部故障最终导致全局故障,这种情况有一个专业的名词儿,叫做“雪崩”。那么,为什么会发生雪崩呢?我们知道,系统在运行的时候是需要消耗一些资源的,包括CPU、内存等系统资源,也包括执行业务逻辑的时候,需要的线程资源。
|
||||
|
||||
举个例子,一般在业务执行的容器内,都会定义一些线程池来分配执行任务的线程,比如在Tomcat这种Web容器的内部,定义了线程池来处理HTTP请求;RPC框架也给RPC服务端初始化了线程池来处理RPC请求。
|
||||
|
||||
这些线程池中的线程资源是有限的,如果这些线程资源被耗尽,那么服务自然也就无法处理新的请求,服务提供方也就宕机了。比如,你的垂直电商系统有四个服务A、B、C、D,A调用B,B调用C和D。其中,A、B、D服务是系统的核心服务(像是电商系统中的订单服务、支付服务等等),C是非核心服务(像反垃圾服务、审核服务)。
|
||||
|
||||
所以,一旦作为入口的A流量增加,你可能会考虑把A、B和D服务扩容,忽略C。那么C就有可能因为无法承担这么大的流量,导致请求处理缓慢,进一步会让B在调用C的时候,B中的请求被阻塞,等待C返回响应结果。这样一来,B服务中被占用的线程资源就不能释放。
|
||||
|
||||
久而久之,B就会因为线程资源被占满,无法处理后续的请求。那么从A发往B的请求,就会被放入B服务线程池的队列中,然后A调用B响应时间变长,进而拖垮A服务。你看,仅仅因为非核心服务C的响应时间变长,就可以导致整体服务宕机,**这就是我们经常遇到的一种服务雪崩情况。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/43/42ccaedc09f890924caae689f0323443.jpg" alt="">
|
||||
|
||||
那么我们要如何避免出现上面这种情况呢?从我刚刚的介绍中你可以看到,因为服务调用方等待服务提供方的响应时间过长,它的资源被耗尽,才引发了级联反应,发生雪崩。
|
||||
|
||||
所以在分布式环境下,系统最怕的反而不是某一个服务或者组件宕机,而是最怕它响应缓慢,因为,某一个服务或者组件宕机也许只会影响系统的部分功能,但它响应一慢,就会出现雪崩拖垮整个系统。
|
||||
|
||||
而我们在部门内部讨论方案的时候,会格外注意这个问题,解决的思路就是在检测到某一个服务的响应时间出现异常时,切断调用它的服务与它之间的联系,让服务的调用快速返回失败,从而释放这次请求持有的资源。**这个思路也就是我们经常提到的降级和熔断机制。**
|
||||
|
||||
那么降级和熔断分别是怎么做的呢?它们之间有什么相同点和不同点呢?你在自己的项目中要如何实现熔断降级呢?
|
||||
|
||||
## 熔断机制是如何做的
|
||||
|
||||
首先,我们来看看熔断机制的实现方式。这个机制参考的是电路中保险丝的保护机制,当电路超负荷运转的时候,保险丝会断开电路,保证整体电路不受损害。而服务治理中的熔断机制指的是在发起服务调用的时候,如果返回错误或者超时的次数超过一定阈值,则后续的请求不再发向远程服务而是暂时返回错误。
|
||||
|
||||
这种实现方式在云计算领域又称为断路器模式,在这种模式下,服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中会有三种状态:关闭(调用远程服务)、半打开(尝试调用远程服务)和打开(返回错误)。这三种状态之间切换的过程是下面这个样子。
|
||||
|
||||
- 当调用失败的次数累积到一定的阈值时,熔断状态从关闭态切换到打开态。一般在实现时,如果调用成功一次,就会重置调用失败次数。
|
||||
- 当熔断处于打开状态时,我们会启动一个超时计时器,当计时器超时后,状态切换到半打开态。你也可以通过设置一个定时器,定期地探测服务是否恢复。
|
||||
- 在熔断处于半打开状态时,请求可以达到后端服务,如果累计一定的成功次数后,状态切换到关闭态;如果出现调用失败的情况,则切换到打开态。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/87/9fc3934e1e0923fe990e0bdbe3aec787.jpg" alt="">
|
||||
|
||||
其实,不仅仅微服务之间调用需要熔断的机制,我们在调用Redis、Memcached等资源的时候也可以引入这套机制。在我的团队自己封装的Redis客户端中,就实现了一套简单的熔断机制。首先,在系统初始化的时候,我们定义了一个定时器,当熔断器处于Open状态时,定期地检测Redis组件是否可用:
|
||||
|
||||
```
|
||||
new Timer("RedisPort-Recover", true).scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (breaker.isOpen()) {
|
||||
Jedis jedis = null;
|
||||
try {
|
||||
jedis = connPool.getResource();
|
||||
jedis.ping(); //验证redis是否可用
|
||||
successCount.set(0); //重置连续成功的计数
|
||||
breaker.setHalfOpen(); //设置为半打开态
|
||||
} catch (Exception ignored) {
|
||||
} finally {
|
||||
if (jedis != null) {
|
||||
jedis.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0, recoverInterval); //初始化定时器定期检测redis是否可用
|
||||
|
||||
```
|
||||
|
||||
在通过Redis客户端操作Redis中的数据时,我们会在其中加入熔断器的逻辑。比如,当节点处于熔断状态时,直接返回空值以及熔断器三种状态之间的转换,具体的示例代码像下面这样:
|
||||
|
||||
```
|
||||
if (breaker.isOpen()) {
|
||||
return null; // 断路器打开则直接返回空值
|
||||
}
|
||||
|
||||
K value = null;
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = connPool.getResource();
|
||||
value = callback.call(jedis);
|
||||
if(breaker.isHalfOpen()) { //如果是半打开状态
|
||||
if(successCount.incrementAndGet() >= SUCCESS_THRESHOLD) {//成功次数超过阈值
|
||||
failCount.set(0); //清空失败数
|
||||
breaker.setClose(); //设置为关闭态
|
||||
}
|
||||
}
|
||||
return value;
|
||||
} catch (JedisException je) {
|
||||
if(breaker.isClose()){ //如果是关闭态
|
||||
if(failCount.incrementAndGet() >= FAILS_THRESHOLD){ //失败次数超过阈值
|
||||
breaker.setOpen(); //设置为打开态
|
||||
}
|
||||
} else if(breaker.isHalfOpen()) { //如果是半打开态
|
||||
breaker.setOpen(); //直接设置为打开态
|
||||
}
|
||||
throw je;
|
||||
} finally {
|
||||
if (jedis != null) {
|
||||
jedis.close();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这样,当某一个Redis节点出现问题,Redis客户端中的熔断器就会实时监测到,并且不再请求有问题的Redis节点,避免单个节点的故障导致整体系统的雪崩。
|
||||
|
||||
## 降级机制要如何做
|
||||
|
||||
除了熔断之外,我们在听业内分享的时候,听到最多的服务容错方式就是降级,那么降级又是怎么做的呢?它和熔断有什么关系呢?
|
||||
|
||||
其实在我看来,相比熔断来说,降级是一个更大的概念。因为它是站在整体系统负载的角度上,放弃部分非核心功能或者服务,保证整体的可用性的方法,是一种有损的系统容错方式。这样看来,熔断也是降级的一种,除此之外还有限流降级、开关降级等等(限流降级我会在下一节课中提到,这节课主要讲一下开关降级)。
|
||||
|
||||
开关降级指的是在代码中预先埋设一些“开关”,用来控制服务调用的返回值。比方说,开关关闭的时候正常调用远程服务,开关打开时则执行降级的策略。这些开关的值可以存储在配置中心中,当系统出现问题需要降级时,只需要通过配置中心动态更改开关的值,就可以实现不重启服务快速地降级远程服务了。
|
||||
|
||||
还是以电商系统为例,你的电商系统在商品详情页面除了展示商品数据以外,还需要展示评论的数据,但是主体还是商品数据,在必要时可以降级评论数据。所以,你可以定义这个开关为“degrade.comment”,写入到配置中心中,具体的代码也比较简单,就像下面这样:
|
||||
|
||||
```
|
||||
boolean switcherValue = getFromConfigCenter("degrade.comment"); //从配置中心获取开关的值
|
||||
if (!switcherValue) {
|
||||
List<Comment> comments = getCommentList(); //开关关闭则获取评论数据
|
||||
} else {
|
||||
List<Comment> comments = new ArrayList(); //开关打开,则直接返回空评论数据
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
当然了,我们在设计开关降级预案的时候,首先要区分哪些是核心服务,哪些是非核心服务。因为我们只能针对非核心服务来做降级处理,然后就可以针对具体的业务,制定不同的降级策略了。我给你列举一些常见场景下的降级策略,你在实际的工作中可以参考借鉴。
|
||||
|
||||
- 针对读取数据的场景,我们一般采用的策略是直接返回降级数据。比如,如果数据库的压力比较大,我们在降级的时候,可以考虑只读取缓存的数据,而不再读取数据库中的数据;如果非核心接口出现问题,可以直接返回服务繁忙或者返回固定的降级数据。
|
||||
- 对于一些轮询查询数据的场景,比如每隔30秒轮询获取未读数,可以降低获取数据的频率(将获取频率下降到10分钟一次)。
|
||||
- 而对于写数据的场景,一般会考虑把同步写转换成异步写,这样可以牺牲一些数据一致性保证系统的可用性。
|
||||
|
||||
**我想强调的是,只有经过演练的开关才是有用的开关,**有些同学在给系统加了开关之后并不测试,结果出了问题真要使用的时候,却发现开关并不生效。因此,你在为系统增加降级开关时,一定要在流量低峰期的时候做验证演练,也可以在不定期的压力测试过程中演练,保证开关的可用性。
|
||||
|
||||
## 课程小结
|
||||
|
||||
以上就是本节课的全部内容了。本节课我带你了解了雪崩产生的原因,服务熔断的实现方式以及服务降级的策略,这里你需要了解的重点是:
|
||||
|
||||
1. 在分布式环境下最怕的是服务或者组件慢,因为这样会导致调用者持有的资源无法释放,最终拖垮整体服务。
|
||||
1. 服务熔断的实现是一个有限状态机,关键是三种状态之间的转换过程。
|
||||
1. 开关降级的实现策略主要有返回降级数据、降频和异步三种方案。
|
||||
|
||||
其实,开关不仅仅应该在你的降级策略中使用,在我的项目中,只要上线新的功能必然要加开关控制业务逻辑是运行新的功能还是运行旧的功能。这样,一旦新的功能上线后,出现未知的问题(比如性能问题),那么可以通过切换开关的方式来实现快速的回滚,减少问题的持续时间。
|
||||
|
||||
总之,熔断和降级是保证系统稳定性和可用性的重要手段,在你访问第三方服务或者资源的时候都需要考虑增加降级开关或者熔断机制,保证资源或者服务出现问题时,不会对整体系统产生灾难性的影响。
|
||||
|
||||
## 一课一思
|
||||
|
||||
结合你的实际工作经历,讲一讲你的项目中都制定了哪些降级的预案呢?在制定降级方案时的考虑点是什么呢?欢迎在留言区与我分享你的经验。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
117
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/35 | 流量控制:高并发系统中我们如何操纵流量?.md
Normal file
117
极客时间专栏/高并发系统设计40问/演进篇 · 维护篇/35 | 流量控制:高并发系统中我们如何操纵流量?.md
Normal file
@@ -0,0 +1,117 @@
|
||||
<audio id="audio" title="35 | 流量控制:高并发系统中我们如何操纵流量?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e5/b4/e5119c1609c11c8a490f0006daf7d5b4.mp3"></audio>
|
||||
|
||||
你好,我是唐扬。
|
||||
|
||||
上一节课里,我带你了解了微服务架构中常见的两种有损的服务保护策略:熔断和降级。它们都是通过暂时关闭某些非核心服务或者组件从而保护核心系统的可用性。但是,并不是所有的场景下都可以使用熔断降级的策略,比如,电商系统在双十一、618大促的场景。
|
||||
|
||||
这种场景下,系统的峰值流量会超过了预估的峰值,对于核心服务也产生了比较大的影响,而你总不能把核心服务整体降级吧?那么在这个时候要如何保证服务的稳定性呢?你认为可以使用限流的方案。而提到限流,我相信你多多少少在以下几个地方出错过:
|
||||
|
||||
- 限流算法选择不当,导致限流效果不好;
|
||||
- 开启了限流却发现整体性能有损耗;
|
||||
- 只实现了单机的限流却没有实现整体系统的限流。
|
||||
|
||||
说白了,你之所以出现这些问题还是对限流的算法以及实际应用不熟练,而本节课,我将带你了解这些内容,希望你能将这些经验应用到实际项目中,从而提升整体系统的鲁棒性。
|
||||
|
||||
## 究竟什么是限流
|
||||
|
||||
限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性。限流策略一般部署在服务的入口层,比如API网关中,这样可以对系统整体流量做塑形。而在微服务架构中,你也可以在RPC客户端中引入限流的策略,来保证单个服务不会被过大的流量压垮。
|
||||
|
||||
其实,无论在实际工作生活中还是在之前学习过的知识中,你都可能对限流策略有过应用,我给你举几个例子。
|
||||
|
||||
比如,到了十一黄金周的时候你想去九寨沟游玩,结果到了九寨沟才发现景区有了临时的通知,每天仅仅售卖10万张门票,而当天没有抢到门票的游客就只能第二天起早继续来抢了。这就是一种常见的限流策略,也就是对一段时间内(在这里是一天)流量做整体的控制,它可以避免出现游客过多导致的景区环境受到影响的情况,也能保证游客的安全。而且,如果你挤过地铁,就更能感同身受了。北京早高峰的地铁都会限流,想法很直接,就是控制进入地铁的人数,保证地铁不会被挤爆,也可以尽量保障人们的安全。
|
||||
|
||||
再比如,在TCP协议中有一个滑动窗口的概念,可以实现对网络传输流量的控制。你可以想象一下,如果没有流量控制,当流量接收方处理速度变慢而发送方还是继续以之前的速率发送数据,那么必然会导致流量拥塞。而TCP的滑动窗口实际上可以理解为接收方所能提供的缓冲区的大小。
|
||||
|
||||
在接收方回复发送方的ACK消息中,会带上这个窗口的大小。这样,发送方就可以通过这个滑动窗口的大小决定发送数据的速率了。如果接收方处理了一些缓冲区的数据,那么这个滑动窗口就会变大,发送方发送数据的速率就会提升;反之,如果接收方接收了一些数据还没有来得及处理,那么这个滑动窗口就会减小,发送方发送数据的速率就会减慢。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/30/faedbfa270c449410c8aa404cfb58e30.jpg" alt="">
|
||||
|
||||
而无论是在一体化架构还是微服务化架构中,我们也可以在多个维度上对到达系统的流量做控制,比如:
|
||||
|
||||
- 你可以对系统每分钟处理多少请求做出限制;
|
||||
- 可以针对单个接口设置每分钟请求流量的限制;
|
||||
- 可以限制单个IP、用户ID或者设备ID在一段时间内发送请求的数量;
|
||||
- 对于服务于多个第三方应用的开放平台来说,每一个第三方应用对于平台方来说都有一个唯一的appkey来标识,那么你也可以限制单个appkey的访问接口的速率。
|
||||
|
||||
而实现上述限制速率的方式是基于一些限流算法的,那么常见的限流的算法有哪些呢?你在实现限流的时候都有哪些方式呢?
|
||||
|
||||
## 你应该知道的限流算法
|
||||
|
||||
#### 固定窗口与滑动窗口的算法
|
||||
|
||||
我们知道,限流的目的是限制一段时间内发向系统的总体请求量,比如,限制一分钟之内系统只能承接1万次请求,那么最暴力的一种方式就是记录这一分钟之内访问系统的请求量有多少,如果超过了1万次的限制,那么就触发限流的策略返回请求失败的错误。如果这一分钟的请求量没有达到限制,那么在下一分钟到来的时候先重置请求量的计数,再统计这一分钟的请求量是否超过限制。
|
||||
|
||||
这种算法叫做固定窗口算法,在实现它的时候,首先要启动一个定时器定期重置计数,比如你需要限制每秒钟访问次数,那么简单的实现代码是这样的:
|
||||
|
||||
```
|
||||
private AtomicInteger counter;
|
||||
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
|
||||
timer.scheduleAtFixedRate(new Runnable(){
|
||||
@Override
|
||||
public void run() {
|
||||
counter.set(0);
|
||||
}
|
||||
}, 0, 1, TimeUnit.SECONDS);
|
||||
|
||||
```
|
||||
|
||||
而限流的逻辑就非常简单了,只需要比较计数值是否大于阈值就可以了:
|
||||
|
||||
```
|
||||
public boolena isRateLimit() {
|
||||
return counter.incrementAndGet() >= allowedLimit;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**这种算法虽然实现非常简单,但是却有一个很大的缺陷** :无法限制短时间之内的集中流量。假如我们需要限制每秒钟只能处理10次请求,如果前一秒钟产生了10次请求,这10次请求全部集中在最后的10毫秒中,而下一秒钟的前10毫秒也产生了10次请求,那么在这20毫秒中就产生了20次请求,超过了限流的阈值。但是因为这20次请求分布在两个时间窗口内,所以没有触发限流,这就造成了限流的策略并没有生效。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/4e/6c19e7da07a7c8095f5799ffae644f4e.jpg" alt="">
|
||||
|
||||
**为了解决这个缺陷,就有了基于滑动窗口的算法。** 这个算法的原理是将时间的窗口划分为多个小窗口,每个小窗口中都有单独的请求计数。比如下面这张图,我们将1s的时间窗口划分为5份,每一份就是200ms;那么当在1s和1.2s之间来了一次新的请求时,我们就需要统计之前的一秒钟内的请求量,也就是0.2s~1.2s这个区间的总请求量,如果请求量超过了限流阈值那么就执行限流策略。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/56/d1b889ca30bd4fa1a29096726f6e9256.jpg" alt="">
|
||||
|
||||
滑动窗口的算法解决了临界时间点上突发流量无法控制的问题,但是却因为要存储每个小的时间窗口内的计数,所以空间复杂度有所增加。
|
||||
|
||||
虽然滑动窗口算法解决了窗口边界的大流量的问题,但是它和固定窗口算法一样,还是无法限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑。**因此,在实际的项目中,我很少使用基于时间窗口的限流算法,而是使用其他限流的算法:一种算法叫做漏桶算法,一种叫做令牌筒算法。**
|
||||
|
||||
#### 漏桶算法与令牌筒算法
|
||||
|
||||
漏桶算法的原理很简单,它就像在流量产生端和接收端之间增加一个漏桶,流量会进入和暂存到漏桶里面,而漏桶的出口处会按照一个固定的速率将流量漏出到接收端(也就是服务接口)。
|
||||
|
||||
如果流入的流量在某一段时间内大增,超过了漏桶的承受极限,那么多余的流量就会触发限流策略,被拒绝服务。
|
||||
|
||||
经过了漏桶算法之后,随机产生的流量就会成为比较平滑的流量到达服务端,从而避免了突发的大流量对于服务接口的影响。**这很像倚天屠龙记里,九阳真经的口诀:他强由他强,清风拂山岗,他横由他横,明月照大江 。** 也就是说,无论流入的流量有多么强横,多么不规则,经过漏桶处理之后,流出的流量都会变得比较平滑。
|
||||
|
||||
而在实现时,我们一般会使用消息队列作为漏桶的实现,流量首先被放入到消息队列中排队,由固定的几个队列处理程序来消费流量,如果消息队列中的流量溢出,那么后续的流量就会被拒绝。这个算法的思想是不是与消息队列削峰填谷的作用相似呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c1/8d/c19e0e5099cfeb979b6b70c678b3238d.jpg" alt="">
|
||||
|
||||
另一种令牌桶算法的基本算法是这样的:
|
||||
|
||||
- 如果我们需要在一秒内限制访问次数为N次,那么就每隔1/N的时间,往桶内放入一个令牌;
|
||||
- 在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;
|
||||
- 桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/9b/4054d20a39fb41e7f9aa924205ba839b.jpg" alt="">
|
||||
|
||||
如果要从这两种算法中做选择,我更倾向于使用令牌桶算法,**原因是漏桶算法在面对突发流量的时候,采用的解决方式是缓存在漏桶中,** 这样流量的响应时间就会增长,这就与互联网业务低延迟的要求不符;而令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的突发流量,所以一般我会使用令牌桶算法来实现限流方案,而Guava中的限流方案就是使用令牌桶算法来实现的。
|
||||
|
||||
你可以看到,使用令牌桶算法就需要存储令牌的数量,如果是单机上实现限流的话,可以在进程中使用一个变量来存储;但是如果在分布式环境下,不同的机器之间无法共享进程中的变量,我们就一般会使用Redis来存储这个令牌的数量。这样的话,每次请求的时候都需要请求一次Redis来获取一个令牌,会增加几毫秒的延迟,性能上会有一些损耗。**因此,一个折中的思路是:** 我们可以在每次取令牌的时候,不再只获取一个令牌,而是获取一批令牌,这样可以尽量减少请求Redis的次数。
|
||||
|
||||
## 课程小结
|
||||
|
||||
以上就是本节课的全部内容了。本节课我带你了解了限流的定义和作用,以及常见的几种限流算法,你需要了解的重点是:
|
||||
|
||||
1. 限流是一种常见的服务保护策略,你可以在整体服务、单个服务、单个接口、单个IP或者单个用户等多个维度进行流量的控制;
|
||||
1. 基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现限流的目的,但是都无法让流量变得更平滑;
|
||||
1. 令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,但是令牌桶算法能够应对一定的突发流量,所以在实际项目中应用更多。
|
||||
|
||||
限流策略是微服务治理中的标配策略,只是你很难在实际中确认限流的阈值是多少,设置的小了容易误伤正常的请求,设置的大了则达不到限流的目的。所以,一般在实际项目中,我们会把阈值放置在配置中心中方便动态调整;同时,我们可以通过定期的压力测试得到整体系统以及每个微服务的实际承载能力,然后再依据这个压测出来的值设置合适的阈值。
|
||||
|
||||
## 一课一思
|
||||
|
||||
在你的实际项目中,有没有使用过限流的方式,来保护系统不被高并发大流量压垮呢?采用了什么样的算法呢?欢迎在留言区与我分享你的经验。
|
||||
|
||||
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
|
||||
@@ -0,0 +1,3 @@
|
||||
<audio id="audio" title="36 | 面试现场第三期:你要如何准备一场技术面试呢?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2a/cb/2a0cfa05910cde98b6c9869b615f96cb.mp3"></audio>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/22/43be6bc7069ff5fb8aa4c6b18fc44322.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/34/b1/349538d98113db1896587afc656867b1.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/4b/2f/4b44ac8e2cd2b52d5e5c5dd2c138f42f.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/88/60/888b4602373c92d8a5885bd1bc01a360.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/45/e0/45a5f2db1de0547b058465ffacdfc0e0.jpg" alt="unpreview">
|
||||
Reference in New Issue
Block a user