系统维护监控篇-学习笔记

在一个项目的生命周期里,运行维护占据着很大的比重,在重要性上,它几乎与项目研发并

驾齐驱。而在系统运维过程中,能够及时地发现问题并解决问题,是每一个团队的本职工
作。所以,你的垂直电商系统在搭建之初,运维团队肯定完成了对于机器 CPU、内存、磁
盘、网络等基础监控,期望能在出现问题时,及时地发现并且处理。你本以为万事大吉,却
没想到系统在运行过程中,频频得到用户的投诉,原因是:
使用的数据库主从延迟变长,导致业务功能上出现了问题;
接口的响应时间变长,用户反馈商品页面出现空白页; 系统中出现大量错误,影响了用户的正常使用。
这些问题,你本应该及时发现并处理的。但现实是,你只能被动地在问题被用户反馈后,手
忙脚乱地修复。这时,你的团队才意识到,要想快速地发现和定位业务系统中出现的问题,
必须搭建一套完善的服务端监控体系。正所谓“道路千万条,监控第一条,监控不到位,领
导两行泪”。不过,在搭建的过程中,你的团队又陷入了困境:
首先,监控的指标要如何选择呢?
采集这些指标可以有哪些方法和途径呢?
指标采集到之后又要如何处理和展示呢?
这些问题,一环扣一环,关乎着系统的稳定性和可用性,而本节课,我就带你解决这些问
题,搭建一套服务端监控体系。
监控指标如何选择
你在搭建监控系统时,所面临的第一个问题就是,选择什么样的监控指标,也就是监控什
么。有些同学在给一个新的系统,设定监控指标的时候,会比较迷茫,不知道从哪方面入
手。其实,有一些成熟的理论和套路,你可以直接拿来使用。比如,谷歌针对分布式系统监
控的经验总结,四个黄金信号(Four Golden Signals)。它指的是,在服务层面一般需要
监控四个指标, 分别是延迟,通信量、错误和饱和度。
延迟指的是请求的响应时间。比如,接口的响应时间、访问数据库和缓存的响应时间。
通信量可以理解为吞吐量,也就是单位时间内,请求量的大小。比如,访问第三方服务
的请求量,访问消息队列的请求量。
错误表示当前系统发生的错误数量。 这里需要注意的是, 我们需要监控的错误既有显示
的,比如在监控 Web 服务时,出现 4 * * 和 5 * * 的响应码;也有隐示的,比如,Web
服务虽然返回的响应码是 200,但是却发生了一些和业务相关的错误(出现了数组越界
的异常或者空指针异常等),这些都是错误的范畴。
饱和度指的是服务或者资源到达上限的程度(也可以说是服务或者资源的利用率),比
如说 CPU 的使用率,内存使用率,磁盘使用率,缓存数据库的连接数等等。 这四个黄金信号提供了通用的监控指标, 除此之外,你还可以借鉴 RED 指标体系。 这个体
系,是四个黄金信号中衍生出来的,其中,R 代表请求量(Request rate),E 代表错误
(Error),D 代表响应时间(Duration),少了饱和度的指标。你可以把它当作一种简化
版的通用监控指标体系。
当然,一些组件或者服务还有独特的指标,这些指标也是需要你特殊关注的。比如,课程中
提到的数据库主从延迟数据、消息队列的堆积情况、缓存的命中率等等。我把高并发系统中
常见组件的监控指标,整理成了一张表格,其中没有包含诸如 CPU、内存、网络、磁盘等
基础监控指标,只是业务上监控指标,主要方便你在实际工作中参考使用。
选择好了监控指标之后,你接下来要考虑的,是如何从组件或者服务中,采集到这些指标,
也就是指标数据采集的问题。
如何采集数据指标
说到监控指标的采集,我们一般会依据采集数据源的不同,选用不同的采集方式, 总结起
来,大概有以下几种类型:
首先, Agent 是一种比较常见的,采集数据指标的方式。
我们通过在数据源的服务器上,部署自研或者开源的 Agent,来收集收据,发送给监控系
统,实现数据的采集。在采集数据源上的信息时,Agent 会依据数据源上,提供的一些接
口获取数据, 我给你举两个典型的例子。 比如,你要从 Memcached 服务器上,获取它的性能数据,那么,你就可以在 Agent 中,
连接这个 Memcached 服务器,并且发送一个 stats 命令,获取服务器的统计信息。然
后,你就可以从返回的信息中,挑选重要的监控指标,发送给监控服务器,形成
Memcached 服务的监控报表。你也可以从这些统计信息中,看出当前 Memcached 服务
器,是否存在潜在的问题。下面是我推荐的,一些重要的状态项, 你可以参考使用。
另外,如果你是 Java 的开发者,那么一般使用 Java 语言开发的中间件,或者组件,都可
以通过 JMX 获取统计或者监控信息。比如,在 19 讲 中,我提到可以使用 JMX,监控
Kafka 队列的堆积数,再比如,你也可以通过 JMX 监控 JVM 内存信息和 GC 相关的信
息。
另一种很重要的数据获取方式, 是在代码中埋点。
这个方式与 Agent 的不同之处在于,Agent 主要收集的是组件服务端的信息,而埋点则是
从客户端的角度,来描述所使用的组件,和服务的性能和可用性。 那么埋点的方式怎么选择
呢?
你可以使用 25 讲 分布式 Trace 组件中,提到的面向切面编程的方式;也可以在资源客户
端中,直接计算调用资源或者服务的耗时、调用量、慢请求数,并且发送给监控服务器。
这里你需要注意一点, 由于调用缓存、数据库的请求量会比较高,一般会单机也会达到每秒
万次,如果不经过任何优化,把每次请求耗时都发送给监控服务器,那么,监控服务器会不
堪重负。所以,我们一般会在埋点时,先做一些汇总。比如,每隔 10 秒汇总这 10 秒内,
对同一个资源的请求量总和、响应时间分位值、错误数等,然后发送给监控服务器。这样,
就可以大大减少发往监控服务器的请求量了。
最后, 日志也是你监控数据的重要来源之一。
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 容量不足或者 Memcache 你所熟知的 Tomcat 和 Nginx 的访问日志,都是重要的监控日志。你可以通过开源的日志
采集工具,将这些日志中的数据发送给监控服务器。目前,常用的日志采集工具有很多,比
如, Apache Flume Fluentd Filebeat ,你可以选择一种熟悉的使用。比如在我
的项目中,我会倾向于使用 Filebeat 来收集监控日志数据。
监控数据的处理和存储
在采集到监控数据之后,你就可以对它们进行处理和存储了,在此之前,我们一般会先用消
息队列来承接数据,主要的作用是削峰填谷,防止写入过多的监控数据,让监控服务产生影
响。
与此同时,我们一般会部署两个队列处理程序,来消费消息队列中的数据。
一个处理程序接收到数据后,把数据写入到 Elasticsearch,然后通过 Kibana 展示数据,
这份数据主要是用来做原始数据的查询;
另一个处理程序是一些流式处理的中间件,比如,Spark、Storm。它们从消息队列里,接
收数据后会做一些处理,这些处理包括:
- 解析数据格式,尤其是日志格式。 从里面提取诸如请求量、响应时间、请求 URL 等数
据;
- 对数据做一些聚合运算。 比如,针对 Tomcat 访问日志,可以计算同一个 URL 一段时间
之内的请求量、响应时间分位值、非 200 请求量的大小等等。
- 将数据存储在时间序列数据库中。 这类数据库的特点是,可以对带有时间标签的数据,做
更有效的存储,而我们的监控数据恰恰带有时间标签,并且按照时间递增,非常适合存储在
时间序列数据库中。目前业界比较常用的时序数据库有 InfluxDB、OpenTSDB、
Graphite,各大厂的选择均有不同,你可以选择一种熟悉的来使用。
- 最后, 你就可以通过 Grafana 来连接时序数据库,将监控数据绘制成报表,呈现给开发
和运维的同学了。
 
至此,你和你的团队,也就完成了垂直电商系统,服务端监控系统搭建的全过程。这里我想
再多说一点,我们从不同的数据源中采集了很多的指标,最终在监控系统中一般会形成以下
几个报表,你在实际的工作中可以参考借鉴:
1. 访问趋势报表。 这类报表接入的是 Web 服务器,和应用服务器的访问日志,展示了服
务整体的访问量、响应时间情况、错误数量、带宽等信息。它主要反映的是,服务的整体运
行情况,帮助你来发现问题。
2. 性能报表。 这类报表对接的是资源和依赖服务的埋点数据,展示了被埋点资源的访问量
和响应时间情况。它反映了资源的整体运行情况,当你从访问趋势报表发现问题后,可以先
从性能报表中,找到究竟是哪一个资源或者服务出现了问题。
3. 资源报表。 这类报表主要对接的是,使用 Agent 采集的,资源的运行情况数据。当你从
性能报表中,发现某一个资源出现了问题,那么就可以进一步从这个报表中,发现资源究竟
出现了什么问题,是连接数异常增高,还是缓存命中率下降。这样可以进一步帮你分析问题
的根源,找到解决问题的方案。
课程小结
本节课,我带你了解了,服务端监控搭建的过程,在这里,你需要了解以下几个重点:
1. 耗时、请求量和错误数是三种最通用的监控指标,不同的组件还有一些特殊的监控指
标,你在搭建自己的监控系统的时候可以直接使用;
2. Agent、埋点和日志是三种最常见的数据采集方式;
3. 访问趋势报表用来展示服务的整体运行情况,性能报表用来分析资源或者依赖的服务是
否出现问题,资源报表用来追查资源问题的根本原因。这三个报表共同构成了你的服务 端监控体系。
总之,监控系统是你发现问题,排查问题的重要工具,你应该重视它,并且投入足够的精力
来不断地完善它。只有这样,才能不断地提高对系统运维的掌控力,降低故障发生的风险。

31 | 应用性能管理:用户的使用体验应该如何监控?
上一节课中,我带你了解了服务端监控搭建的过程。有了监控报表之后,你的团队在维护垂
直电商系统时,就可以更早地发现问题,也有直观的工具辅助你们分析排查问题了。
不过,你很快发现,有一些问题,服务端的监控报表无法排查,甚至无法感知。比如,有用
户反馈创建订单失败,但是从服务端的报表来看,并没有什么明显的性能波动,从存储在
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
中,作为原始数据保存起来,再写一份到统计平台,以形成客户端的报表。

 Version:0.9 StartHTML:0000000105 EndHTML:0000022253 StartFragment:0000000141 EndFragment:0000022213

有了这套 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; 有了这个 EventListner,你就可以在初始化 HttpClient 的时候把它注入进去,代码如下:
这样,我们可以得出一次请求过程中,经历的一系列过程的时间,其中主要包括下面几项。
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> inetAdd
super .dnsEnd(call, domainName, inetAddressList);
System.out.println( "url " + url.host() + ", Dns time: " + (System.nan
}
}
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.eventListenerFactory( new Factory() {
@Override
public EventListener create (Call call) {
return new HttpEventListener(call.request().url());
}
});
等待时间:异步调用时,请求会首先缓存在本地的队列里面,由专门的 I/O 线程负责,
那么在 I/O 线程真正处理请求之前,会有一个等待的时间。
1.
2. DNS 时间:域名解析时间。
3. 握手时间:TCP 三次握手的时间。
4. SSL 时间:如果服务是 HTTPS 服务,那么就会有一个 SSL 认证的时间。
5. 发送时间:请求包被发送出去的时间。 6. 首包时间:服务端处理后,客户端收到第一个响应包的时间。
7. 包接收时间:我们接收到所有数据的时间。

有了这些数据之后,我们可以通过上面提到的 APM 通道服务,发送给服务端,这样服务端

和客户端的同学,就可以从 Elasticsearch 中,查询到原始的数据,也可以对数据做一些聚
合处理、统计分析之后,形成客户端请求监控的报表。这样,我们就可以有针对性地对
HTTP 请求的某一个过程做优化了。
而对于用户网络的监控,可以给你带来三方面的价值。
首先,这种用户网络监控的所有监控数据均来自客户端,是用户访问数据实时上报,因此能
够准确、真实、实时地反应用户操作体验。
再者,它是我们性能优化的指向标,当做业务架构改造、服务性能优化、网络优化等任何优
化行为时,可以反馈用户性能数据,引导业务正向优化接口性能、可用性等指标。
最后,它也能帮助我们监控 CDN 链路质量。之前的 CDN 的监控,严重依赖 CDN 厂商,
这有一个问题是:CDN 无法从端上获取到全链路的监控数据,有些时候,客户端到 CDN
的链路出了问题,CDN 厂商是感知不到的,而客户端监控弥补了这方面的缺陷,并且可以
通过告警机制督促 CDN 及时优化调整问题线路。
除了上报网络数据之外,我们还可以上报一些异常事件的数据,比如你的垂直电商系统可能
会遇到以下一些异常情况。
登录失败
下单失败
浏览商品信息失败 评论列表加载不出来
无法评分留言
你在业务逻辑的代码中,都可以检测到这些异常数据,当然,也可以通过 APM 通道服务,
上传到服务端,这样方便服务端和客户端的同学一起来排查问题,也能给你的版本灰度提供
数据的支持。
总的来说,如果说搭建的系统是骨架,那么具体监控的数据就是灵魂,因为数据是监控的主
题内容,系统只是呈现数据的载体。所以,你需要在系统运维的过程中不断完善对数据的收
集,这也是对你的监控系统不断升级完善的过程。
课程小结
以上就是本节课的全部内容了。本节课,我主要带你了解了,如何搭建一个端到端的 APM
监控系统,你需要了解的重点是:
1. 从客户端采集到的数据可以用通用的消息格式,上传到 APM 服务端,服务端将数据存
入到 Elasticsearch 中,以提供原始日志的查询,也可以依据这些数据形成客户端的监控报
表;
2. 用户网络数据是我们排查客户端,和服务端交互过程的重要数据,你可以通过代码的植
入,来获取到这些数据;
3. 无论是网络数据,还是异常数据,亦或是卡顿、崩溃、流量、耗电量等数据,你都可以
通过把它们封装成 APM 消息格式,上传到 APM 服务端,这些用户在客户端上留下的踪迹
可以帮助你更好地优化用户的使用体验。
总而言之,监测和优化用户的使用体验是应用性能管理的最终目标。 然而,服务端的开发人
员往往会陷入一个误区,认为我们将服务端的监控做好,保证接口性能和可用性足够好就好
了。事实上,接口的响应时间只是我们监控系统中很小的一部分,搭建一套端到端的全链路
的监控体系,才是你的监控系统的最终形态。

32 | 压力测试:怎样设计全链路压力测试平台?
经过两节课的学习,我们已经搭建了服务端和客户端的监控,通过监控的报表和一些报警规
则的设置,你可以实时地跟踪和解决垂直电商系统中出现的问题了。不过,你不能掉以轻
心,因为监控只能发现目前系统中已经存在的问题,对于未来可能发生的性能问题是无能为
力的。
一旦你的系统流量有大的增长,比如类似“双十一”的流量,那么你在面临性能问题时就可
能会手足无措。为了解决后顾之忧,你需要了解在流量增长若干倍的时候,系统的哪些组件
或者服务会成为整体系统的瓶颈点,这时你就需要做一次全链路的压力测试。 那么,什么是压力测试呢?要如何来做全链路的压测呢?这两个问题就是本节课重点讲解的
内容。
什么是压力测试
压力测试(简称为压测)这个名词儿,你在业界的分享中一定听过很多次,当然了,你也可
能在项目的研发过程中做过压力测试,所以,对于你来说,压力测试并不陌生。
不过,我想让你回想一下,自己是怎么做压力测试的?是不是像很多同学一样:先搭建一套
与正式环境功能相同的测试环境,并且导入或者生成一批测试数据,然后在另一台服务器,
启动多个线程并发地调用需要压测的接口(接口的参数一般也会设置成相同的,比如,想要
压测获取商品信息的接口,那么压测时会使用同一个商品 ID)。最后,通过统计访问日
志,或者查看测试环境的监控系统,来记录最终压测 QPS 是多少之后,直接交差?
这么做压力测试其实是不正确的, 错误之处主要有以下几点:
1. 首先,做压力测试时,最好使用线上的数据和线上的环境,因为,你无法确定自己搭建
的测试环境与正式环境的差异,是否会影响到压力测试的结果;
2. 其次,压力测试时不能使用模拟的请求,而是要使用线上的流量。你可以通过拷贝流量
的方式,把线上流量拷贝一份到压力测试环境。因为模拟流量的访问模型,和线上流量相差
很大,会对压力测试的结果产生比较大的影响。
比如,你在获取商品信息的时候,线上的流量会获取不同商品的数据,这些商品的数据有的
命中了缓存,有的没有命中缓存。如果使用同一个商品 ID 来做压力测试,那么只有第一次
请求没有命中缓存,而在请求之后会将数据库中的数据回种到缓存,后续的请求就一定会命
中缓存了,这种压力测试的数据就不具备参考性了。
3. 不要从一台服务器发起流量,这样很容易达到这台服务器性能瓶颈,从而导致压力测试
的 QPS 上不去,最终影响压力测试的结果。而且,为了尽量真实地模拟用户请求,我们倾
向于把流量产生的机器,放在离用户更近的位置,比如放在 CDN 节点上。如果没有这个条
件,那么可以放在不同的机房中,这样可以尽量保证压力测试结果的真实性。
之所以有很多同学出现这个问题,主要是对压力测试的概念没有完全理解,以为只要是使用
多个线程并发的请求服务接口,就算是对接口进行压力测试了。 那么究竟什么是压力测试呢? 压力测试指的是,在高并发大流量下,进行的测试,测试人员
可以通过观察系统在峰值负载下的表现,从而找到系统中存在的性能隐患。
与监控一样,压力测试是一种常见的,发现系统中存在问题的方式,也是保障系统可用性和
稳定性的重要手段。而在压力测试的过程中,我们不能只针对某一个核心模块来做压测,而
需要将接入层、所有后端服务、数据库、缓存、消息队列、中间件以及依赖的第三方服务系
统及其资源,都纳入压力测试的目标之中。因为,一旦用户的访问行为增加,包含上述组件
服务的整个链路都会受到不确定的大流量的冲击,因此,它们都需要依赖压力测试来发现可
能存在的性能瓶颈, 这种针对整个调用链路执行的压力测试也称为“全链路压测”。
由于在互联网项目中,功能迭代的速度很快,系统的复杂性也变得越来越高,新增加的功能
和代码很可能会成为新的性能瓶颈点。也许半年前做压力测试时,单台机器可以承担每秒
1000 次请求,现在很可能就承担每秒 800 次请求了。所以,压力测试应该作为系统稳定性
保障的常规手段,周期性地进行。
但是,通常做一次全链路压力测试,需要联合 DBA、运维、依赖服务方、中间件架构等多
个团队,一起协调进行,无论是人力成本还是沟通协调的成本都比较高。同时,在压力测试
的过程中,如果没有很好的监控机制,那么还会对线上系统造成不利的影响。 为了解决这些
问题,我们需要搭建一套自动化的全链路压测平台,来降低成本和风险。
如何搭建全链路压测平台
搭建全链路压测平台,主要有两个关键点。
一点是流量的隔离。由于压力测试是在正式环境进行,所以需要区分压力测试流量和正式流
量,这样可以针对压力测试的流量做单独的处理。
另一点是风险的控制。也就是,尽量避免压力测试对于正常访问用户的影响,因此,一般来
说全链路压测平台需要包含以下几个模块:
流量构造和产生模块;
压测数据隔离模块;
系统健康度检查和压测流量干预模块。 整体压测平台的架构图可以是下面这样的:
 
为了让你能够更清晰地了解每一个模块是如何实现的,方便你来设计适合自身业务的全链路
压测平台,我会对压测平台的每一个模块做更细致地介绍。先来看看压力测试的流量是如何
产生的。
压测数据的产生
一般来说,我们系统的入口流量是来自于客户端的 HTTP 请求,所以,我们会考虑在系统
高峰期时,将这些入口流量拷贝一份,在经过一些流量清洗的工作之后(比如过滤一些无效
的请求),将数据存储在像是 HBase、MongoDB 这些 NoSQL 存储组件,或者亚马逊 S3
这些云存储服务中,我们称之为流量数据工厂。
这样,当我们要压测的时候,就可以从这个工厂中获取数据,将数据切分多份后下发到多个
压测节点上了, 在这里,我想强调几个,你需要特殊注意的点。
首先,我们可以使用多种方式来实现流量的拷贝。最简单的一种方式:直接拷贝负载均衡服
务器的访问日志,数据就以文本的方式写入到流量数据工厂中,但是这样产生的数据在发起
压测时,需要自己写解析的脚本来解析访问日志,会增加压测时候的成本,不太建议使用。
另一种方式:通过一些开源的工具来实现流量的拷贝。这里,我推荐一款轻型的流量拷贝工
GoReplay ,它可以劫持本机某一个端口的流量,将它们记录在文件中,传送到流量数 据工厂中。在压测时,你也可以使用这个工具进行加速的流量回放,这样就可以实现对正式
环境的压力测试了。
其次,如上文中提到的,我们在下发压测流量时,需要保证下发流量的节点与用户更加接
近,起码不能和服务部署节点在同一个机房中,这样可以尽量保证压测数据的真实性。
另外,我们还需要对压测流量染色,也就是增加压测标记。在实际项目中,我会在 HTTP
的请求头中增加一个标记项,比如说叫做 is stress test,在流量拷贝之后,批量在请求中
增加这个标记项,再写入到数据流量工厂中。
数据如何隔离
将压测流量拷贝下来的同时,我们也需要考虑对系统做改造,以实现压测流量和正式流量的
隔离,这样一来就会尽量避免压测对线上系统的影响,一般来说,我们需要做两方面的事
情。
一方面,针对读取数据的请求(一般称之为下行流量),我们会针对某些不能压测的服务或
者组件,做 Mock 或者特殊的处理。举个例子。
在业务开发中,我们一般会依据请求,记录用户的行为,比如,用户请求了某个商品的页
面,我们会记录这个商品多了一次浏览的行为,这些行为数据会写入一份单独的大数据日志
中,再传输给数据分析部门,形成业务报表给到产品或者老板做业务的分析决策。
在压测的时候,肯定会增加这些行为数据,比如原本一天商品页面的浏览行为是一亿次,而
压测之后变成了十亿次,这样就会对业务报表产生影响,影响后续的产品方向的决策。因
此,我们对于这些压测产生的用户行为做特殊处理,不再记录到大数据日志中。
再比如,我们系统会依赖一些推荐服务,推荐一些你可能感兴趣的商品,但是这些数据的展
示有一个特点就是,展示过的商品就不再会被推荐出来。如果你的压测流量经过这些推荐服
务,大量的商品就会被压测流量请求到,线上的用户就不会再看到这些商品了,也就会影响
推荐的效果。
所以,我们需要 Mock 这些推荐服务,让不带有压测标记的请求经过推荐服务,而让带有
压测标记的请求经过 Mock 服务。搭建 Mock 服务,你需要注意一点:这些 Mock 服务最 好部署在真实服务所在的机房,这样可以尽量模拟真实的服务部署结构,提高压测结果的真
实性。
另一方面,针对写入数据的请求(一般称之为上行流量),我们会把压测流量产生的数据,
写入到影子库,也就是和线上数据存储,完全隔离的一份存储系统中。针对不同的存储类
型,我们会使用不同的影子库的搭建方式:
1. 如果数据存储在 MySQL 中,我们可以在同一个 MySQL 实例,不同的 Schema 中创建
一套和线上相同的库表结构,并且把线上的数据也导入进来。
2. 而如果数据是放在 Redis 中,我们对压测流量产生的数据,增加一个统一的前缀,存储
在同一份存储中。
3. 还有一些数据会存储在 Elasticsearch 中,针对这部分数据,我们可以放在另外一个单独
的索引表中。
通过对下行流量的特殊处理以及对上行流量增加影子库的方式,我们就可以实现压测流量的
隔离了。
压力测试如何实施
在拷贝了线上流量和完成了对线上系统的改造之后,我们就可以进行压力测试的实施了。在
此之前,一般会设立一个压力测试的目标,比如说,整体系统的 QPS 需要达到每秒 20
万。
不过,在压测时,不会一下子把请求量增加到每秒 20 万次,而是按照一定的步长(比如每
次压测增加一万 QPS),逐渐地增加流量。在增加一次流量之后,让系统稳定运行一段时
间,观察系统在性能上的表现。如果发现依赖的服务或者组件出现了瓶颈,可以先减少压测
流量,比如,回退到上一次压测的 QPS,保证服务的稳定,再针对此服务或者组件进行扩
容,然后再继续增加流量压测。
为了能够减少压力测试过程中,人力投入成本,可以开发一个流量监控的组件,在这个组件
中,预先设定一些性能阈值。比如,容器的 CPU 使用率的阈值可以设定为 60%~70%;
系统的平均响应时间的上限可以设定为 1 秒;系统慢请求的比例设置为 1% 等等。 当系统性能达到这个阈值之后,流量监控组件可以及时发现,并且通知压测流量下发组件减
少压测流量,并且发送报警给到开发和运维的同学,开发和运维同学就迅速地排查性能瓶
颈,在解决问题或者扩容之后再继续执行压测。
业界关于全链路压测平台的探索有很多,一些大厂比如阿里、京东、美团和微博都有了适合
自身业务的全链路压测平台。在我看来,这些压测平台万变不离其宗,都无非是经过流量拷
贝、流量染色隔离、打压、监控熔断等步骤,与本课程中介绍的核心思想都是相通的。因
此,你在考虑自研适合自己项目的全链路压测平台时,也可以遵循这个成熟的套路。
课程小结
本节课,我带你了解了做压力测试常见的误区,以及自动化的全链路压测平台的搭建过程,
这里你需要了解的重点是:
1. 压力测试是一种发现系统性能隐患的重要手段,所以应该尽量使用正式的环境和数据;
2. 对压测的流量需要增加标记,这样就可以通过 Mock 第三方依赖服务和影子库的方式来
实现压测数据和正式数据的隔离;
3. 压测时,应该实时地对系统性能指标做监控和告警,及时地对出现瓶颈的资源或者服务
扩容,避免对正式环境产生影响。
这套全链路的压力测试系统对于我们来说有三方面的价值:
其一,它可以帮助我们发现系统
中可能出现的性能瓶颈,方便我们提前准备预案来应对;其次,它也可以为我们做容量评
估,提供数据上的支撑;最后,我们也可以在压测的时候做预案演练,因为压测一般会安排
在流量的低峰期进行,这样我们可以降级一些服务来验证预案效果,并且可以尽量减少对线
上用户的影响。所以,随着你的系统流量的快速增长,你也需要及时考虑搭建这么一套全链
路压测平台,来保证你的系统的稳定性。
33 | 配置管理:成千上万的配置项要如何管理?
 
相信在实际工作中,提及性能优化你会想到代码优化,但是实际上有些性能优化可能只需要
调整一些配置参数就可以搞定了,为什么这么说呢?我给你举几个例子:
你可以调整配置的超时时间,让请求快速失败,防止系统的雪崩,提升系统的可用性;
你还可以调整 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 中拉取配置就可以了,
是一个标准化的过程,这样可以避免在启动应用时忘记部署配置文件。
另外,如果你的服务是多机房部署的,那么不同机房的配置项中,有可能有相同的,或者有
不同的。那么,你需要将相同的配置项放置在一个目录中给多个机房公用,再将不同的配置
项,放置在以机房名为名称的目录中。在读取配置的时候应该优先读取机房的配置,再读取
公共配置,这样可以减少配置文件中的配置项的数量。
我给你列了一个典型目录配置,如果你的系统也使用文件来管理配置,那么可以参考一下。
那么,这是不是配置管理的最终形态呢?当然不是,你不要忘了把配置放在文件中的方式还
存在的问题(我上面也提到过了),那就是,我们必须将服务重启后,才能让配置生效。有
没有一种方法可以在不重启应用的前提下,也能让配置生效呢?这就需要配置中心帮助我们
实现了。
配置中心是如何实现的?
配置中心可以算是微服务架构中的一个标配组件了。业界也提供了多种开源方案供你选择,
比较出名的有携程开源的 Apollo,百度开源的 Disconf,360 开源的 QConf,Spring
Cloud 的组件 Spring Cloud Config 等等。
在我看来,Apollo 支持不同环境,不同集群的配置,有完善的管理功能,支持灰度发布、
更改热发布等功能, 在所有配置中心中功能比较齐全,推荐你使用。
那么,配置中心的组件在实现上,有哪些关键的点呢?如果你想对配置中心组件有更强地把
控力,想要自行研发一套符合自己业务场景的组件,又要如何入手呢?
复制代码
1
2
3
4
5
6
7
/data/confs/common/commerce // 电商业务的公共配置
/data/confs/commerce-zw // 电商业务兆维机房配置
/data/confs/commerce-yz // 电商业务亦庄机房配置
/data/confs/common/community // 社区业务的公共配置
/data/confs/community-zw // 社区业务兆维机房配置
/data/confs/community-yz // 社区业务亦庄机房配置 配置信息如何存储
其实,配置中心和注册中心非常类似,其核心的功能就是对于配置项的存储和读取。所以,
在设计配置中心的服务端时,我们需要选择合适的存储组件,来存储大量的配置信息,这里
可选择的组件有很多。
事实上,不同的开源配置中心也使用了不同的组件,比如 Disconf、Apollo 使用的是
MySQL;QConf 使用的是 ZooKeeper。我之前维护和使用的配置中心就会使用不同的存
储组件,比如微博的配置中心使用 Redis 来存储信息,而美图的则使用 Etcd。
而无论使用哪一种存储组件,你所要做的就是规范配置项在其中的存储结构。比如,我之前
使用的配置中心用 Etcd 作为存储组件,支持存储全局配置、机房配置和节点配置。其中,
节点配置优先级高于机房配置,机房配置优先级高于全局配置。也就是说,我们会优先读取
节点的配置,如果节点配置不存在,再读取机房配置,最后读取全局配置。它们的存储路径
如下:
变更推送如何实现
配置信息存储之后,我们需要考虑如何将配置的变更推送给服务端,这样就可以实现配置的
动态变更,不需要重启服务器就能让配置生效了。而我们一般会有两种思路来实现变更推
送:一种是轮询查询的方式;一种是长连推送的方式。
轮询查询很简单,就是应用程序向配置中心客户端注册一个监听器,配置中心的客户端,定
期地(比如 1 分钟)查询所需要的配置是否有变化,如果有变化则通知触发监听器,让应
用程序得到变更通知。
这里有一个需要注意的点, 如果有很多应用服务器都去轮询拉取配置,由于返回的配置项可
能会很大,那么配置中心服务的带宽就会成为瓶颈。为了解决这个问题,我们会给配置中心
的每一个配置项,多存储一个根据配置项计算出来的 MD5 值。
/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 配置项一旦变化,这个 MD5 值也会随之改变。配置中心客户端在获取到配置的同时,也会
获取到配置的 MD5 值,并且存储起来。那么在轮询查询的时候,需要先确认存储的 MD5
值,和配置中心的 MD5 是不是一致的。如果不一致,这就说明配置中心中,存储的配置项
有变化,然后才会从配置中心拉取最新的配置。
由于配置中心里存储的配置项变化的几率不大,所以使用这种方式后,每次轮询请求就只是
返回一个 MD5 值,可以大大地减少配置中心服务器的带宽。
另一种长连的方式,则是在配置中心服务端保存每个连接关注的配置项列表。这样,当配置
中心感知到配置变化后,就可以通过这个连接,把变更的配置推送给客户端。这种方式需要
保持长连,也需要保存连接和配置的对应关系,实现上要比轮询的方式复杂一些,但是相比
轮询方式来说,能够更加实时地获取配置变更的消息。
而在我看来,配置服务中存储的配置变更频率不高,所以对于实时性要求不高,但是期望实
现上能够足够简单, 所以如果选择自研配置中心的话,可以考虑使用轮询的方式。
如何保证配置中心高可用
除了变更通知以外,在配置中心实现中,另外一个比较关键的点在于如何保证它的可用性,
因为对于配置中心来说,它的可用性的重要程度要远远大于性能。这是因为我们一般会在服
务器启动时,从配置中心中获取配置,如果配置获取的性能不高,那么外在的表现也只是应
用启动时间慢了,对于业务的影响不大;但是,如果获取不到配置,很可能会导致启动失
败。
比如,我们把数据库的地址存储在了配置中心里,如果配置中心宕机导致我们无法获取数据
库的地址,那么自然应用程序就会启动失败。 因此,我们的诉求是让配置中心“旁路化”。 也就是说,即使配置中心宕机,或者配置中心依赖的存储宕机,我们仍然能够保证应用程序
是可以启动的。那么这是如何实现的呢?
我们一般会在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓
存是文件的缓存。
配置中心客户端在获取到配置信息后,会同时把配置信息同步地写入到内存缓存,并且异步
地写入到文件缓存中。内存缓存的作用是降低客户端和配置中心的交互频率,提升配置获取
的性能;而文件的缓存的作用就是灾备,当应用程序重启时,一旦配置中心发生故障,那么
应用程序就会优先使用文件中的配置,这样虽然无法得到配置的变更消息(因为配置中心已
经宕机了),但是应用程序还是可以启动起来的,算是一种降级的方案。
课程小结
以上就是本节课的全部内容了。在这节课中,我带你了解了系统开发的过程中,我们是如何
管理大量的配置项的,你需要了解的重点是:
1. 配置存储是分级的,有公共配置,有个性的配置,一般个性配置会覆盖公共配置,这样
可以减少存储配置项的数量;
2. 配置中心可以提供配置变更通知的功能,可以实现配置的热更新;
3. 配置中心关注的性能指标中,可用性的优先级是高于性能的,一般我们会要求配置中心
的可用性达到 99.999%,甚至会是 99.9999%。
这里你需要注意的是,并不是所有的配置项都需要使用配置中心来存储,如果你的项目还是
使用文件方式来管理配置,那么你只需要,将类似超时时间等,需要动态调整的配置,迁移
到配置中心就可以了。对于像是数据库地址,依赖第三方请求的地址,这些基本不会发生变
化的配置项,可以依然使用文件的方式来管理,这样可以大大地减少配置迁移的成本。

34 | 降级熔断:如何屏蔽非核心系统故障的影响?
到目前为止,你的电商系统已经搭建了完善的服务端和客户端监控系统,并且完成了全链路
压测。现在呢,你们已经发现和解决了垂直电商系统中很多的性能问题和隐患。但是千算万
算,还是出现了纰漏。
本来,你们对于应对“双十一”的考验信心满满,但因为欠缺了一些面对巨大流量的经验,
在促销过程中出现了几次短暂的服务不可用,这给部分用户造成了不好的使用体验。事后,
你们进行了细致的复盘,追查出现故障的根本原因,你发现,原因主要可以归结为两大类。 第一类原因是由于依赖的资源或者服务不可用,最终导致整体服务宕机。举例来说,在
你的电商系统中就可能由于数据库访问缓慢,导致整体服务不可用。
另一类原因是你们乐观地预估了可能到来的流量,当有超过系统承载能力的流量到来
时,系统不堪重负,从而出现拒绝服务的情况。
那么,你要如何避免再次出现这两类问题呢?我建议你采取降级、熔断以及限流的方案。限
流是解决第二类问题的主要思路(下一节课,我会着重讲解)。今天这节课,我主要讲一下
解决第一类问题的思路:降级和熔断。
不过在此之前,我先带你了解一下这个问题为何存在,因为你只有弄清楚出现故障的原理,
才能更好地理解熔断降级带来的好处。
雪崩是如何发生的
局部故障最终导致全局故障,这种情况有一个专业的名词儿,叫做“雪崩”。那么,为什么
会发生雪崩呢?我们知道,系统在运行的时候是需要消耗一些资源的,包括 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 的响应时间变长,就可以导致整体服务宕机, 这就是我们经常
遇到的一种服务雪崩情况。

那么我们要如何避免出现上面这种情况呢?从我刚刚的介绍中你可以看到,因为服务调用方
等待服务提供方的响应时间过长,它的资源被耗尽,才引发了级联反应,发生雪崩。
所以在分布式环境下,系统最怕的反而不是某一个服务或者组件宕机,而是最怕它响应缓
慢,因为,某一个服务或者组件宕机也许只会影响系统的部分功能,但它响应一慢,就会出
现雪崩拖垮整个系统。
而我们在部门内部讨论方案的时候,会格外注意这个问题,解决的思路就是在检测到某一个
服务的响应时间出现异常时,切断调用它的服务与它之间的联系,让服务的调用快速返回失
败,从而释放这次请求持有的资源。 这个思路也就是我们经常提到的降级和熔断机制。
那么降级和熔断分别是怎么做的呢?它们之间有什么相同点和不同点呢?你在自己的项目中
要如何实现熔断降级呢?
熔断机制是如何做的
首先,我们来看看熔断机制的实现方式。这个机制参考的是电路中保险丝的保护机制,当电
路超负荷运转的时候,保险丝会断开电路,保证整体电路不受损害。而服务治理中的熔断机
制指的是在发起服务调用的时候,如果返回错误或者超时的次数超过一定阈值,则后续的请
求不再发向远程服务而是暂时返回错误。
这种实现方式在云计算领域又称为断路器模式,在这种模式下,服务调用方为每一个调用的
服务维护一个有限状态机,在这个状态机中会有三种状态:关闭(调用远程服务)、半打开
(尝试调用远程服务)和打开(返回错误)。这三种状态之间切换的过程是下面这个样子。 其实,不仅仅微服务之间调用需要熔断的机制,我们在调用 Redis、Memcached 等资源
的时候也可以引入这套机制。在我的团队自己封装的 Redis 客户端中,就实现了一套简单
的熔断机制。首先,在系统初始化的时候,我们定义了一个定时器,当熔断器处于 Open
状态时,定期地检测 Redis 组件是否可用:
当调用失败的次数累积到一定的阈值时,熔断状态从关闭态切换到打开态。一般在实现
时,如果调用成功一次,就会重置调用失败次数。
当熔断处于打开状态时,我们会启动一个超时计时器,当计时器超时后,状态切换到半
打开态。你也可以通过设置一个定时器,定期地探测服务是否恢复。
在熔断处于半打开状态时,请求可以达到后端服务,如果累计一定的成功次数后,状态
切换到关闭态;如果出现调用失败的情况,则切换到打开态。

其实,不仅仅微服务之间调用需要熔断的机制,我们在调用 Redis、Memcached 等资源
的时候也可以引入这套机制。在我的团队自己封装的 Redis 客户端中,就实现了一套简单
的熔断机制。首先,在系统初始化的时候,我们定义了一个定时器,当熔断器处于 Open
状态时,定期地检测 Redis 组件是否可用:

在通过 Redis 客户端操作 Redis 中的数据时,我们会在其中加入熔断器的逻辑。比如,当
节点处于熔断状态时,直接返回空值以及熔断器三种状态之间的转换,具体的示例代码像下
面这样:

这样,当某一个 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. 在分布式环境下最怕的是服务或者组件慢,因为这样会导致调用者持有的资源无法释
放,最终拖垮整体服务。
2. 服务熔断的实现是一个有限状态机,关键是三种状态之间的转换过程。
3. 开关降级的实现策略主要有返回降级数据、降频和异步三种方案。
其实,开关不仅仅应该在你的降级策略中使用,在我的项目中,只要上线新的功能必然要加
开关控制业务逻辑是运行新的功能还是运行旧的功能。这样,一旦新的功能上线后,出现未
知的问题(比如性能问题),那么可以通过切换开关的方式来实现快速地回滚,减少问题的
持续时间。
总之,熔断和降级是保证系统稳定性和可用性的重要手段,在你访问第三方服务或者资源的
时候都需要考虑增加降级开关或者熔断机制,保证资源或者服务出现问题时,不会对整体系
统产生灾难性的影响。
 
35 | 流量控制:高并发系统中我们如何操纵流量?

上一节课里,我带你了解了微服务架构中常见的两种有损的服务保护策略:熔断和降级。它
们都是通过暂时关闭某些非核心服务或者组件从而保护核心系统的可用性。但是,并不是所
有的场景下都可以使用熔断降级的策略,比如,电商系统在双十一、618 大促的场景。
这种场景下,系统的峰值流量会超过了预估的峰值,对于核心服务也产生了比较大的影响,
而你总不能把核心服务整体降级吧?那么在这个时候要如何保证服务的稳定性呢?你认为可
以使用限流的方案。而提到限流,我相信你多多少少在以下几个地方出错过:
限流算法选择不当,导致限流效果不好; 开启了限流却发现整体性能有损耗;
只实现了单机的限流却没有实现整体系统的限流。
说白了,你之所以出现这些问题还是对限流的算法以及实际应用不熟练,而本节课,我将带
你了解这些内容,期望你能将这些经验应用到实际项目中,从而提升整体系统的鲁棒性。

究竟什么是限流
限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对
于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性。限流策略一般部署
在服务的入口层,比如 API 网关中,这样可以对系统整体流量做塑形。而在微服务架构
中,你也可以在 RPC 客户端中引入限流的策略,来保证单个服务不会被过大的流量压垮。
其实,无论在实际工作生活中还是在之前学习过的知识中,你都可能对限流策略有过应用,
我给你举几个例子。
比如,到了十一黄金周的时候你想去九寨沟游玩,结果到了九寨沟才发现景区有了临时的通
知,每天仅仅售卖 10 万张门票,而当天没有抢到门票的游客就只能第二天起早继续来抢
了。这就是一种常见的限流策略,也就是对一段时间内(在这里是一天)流量做整体的控
制,它可以避免出现游客过多导致的景区环境受到影响的情况,也能保证游客的安全。而
且,如果你挤过地铁,就更能感同身受了。北京早高峰的地铁都会限流,想法很直接,就是
控制进入地铁的人数,保证地铁不会被挤爆,也可以尽量保障人们的安全。
再比如,在 TCP 协议中有一个滑动窗口的概念,可以实现对网络传输流量的控制。你可以
想象一下,如果没有流量控制,当流量接收方处理速度变慢而发送方还是继续以之前的速率
发送数据,那么必然会导致流量拥塞。而 TCP 的滑动窗口实际上可以理解为接收方所能提
供的缓冲区的大小。
在接收方回复发送方的 ACK 消息中,会带上这个窗口的大小。这样,发送方就可以通过这
个滑动窗口的大小决定发送数据的速率了。如果接收方处理了一些缓冲区的数据,那么这个
滑动窗口就会变大,发送方发送数据的速率就会提升;反之,如果接收方接收了一些数据还
没有来得及处理,那么这个滑动窗口就会减小,发送方发送数据的速率就会减慢。

而无论是在一体化架构还是微服务化架构中,我们也可以在多个维度上对到达系统的流量做
控制,比如:
你可以对系统每分钟处理多少请求做限制;
可以针对单个接口设置每分钟请求流量的限制;
可以限制单个 IP、用户 ID 或者设备 ID 在一段时间内发送请求的数量;
对于服务于多个第三方应用的开放平台来说,每一个第三方应用对于平台方来说都有一
个唯一的 appkey 来标识,那么你也可以限制单个 appkey 的访问接口的速率。
而实现上述限制速率的方式是基于一些限流算法的,那么常见的限流的算法有哪些呢?你在
实现限流的时候都有哪些方式呢?
你应该知道的限流算法
固定窗口与滑动窗口的算法
我们知道,限流的目的是限制一段时间内发向系统的总体请求量,比如,限制一分钟之内系
统只能承接 1 万次请求,那么最暴力的一种方式就是记录这一分钟之内访问系统的请求量
有多少,如果超过了 1 万次的限制,那么就触发限流的策略返回请求失败的错误。如果这
一分钟的请求量没有达到限制,那么在下一分钟到来的时候先重置请求量的计数,再统计这
一分钟的请求量是否超过限制。
这种算法叫做固定窗口算法,在实现它的时候,首先要启动一个定时器定期重置计数,比如
你需要限制每秒钟访问次数,那么简单的实现代码是这样的:

而限流的逻辑就非常简单了,只需要比较计数值是否大于阈值就可以了:

这种算法虽然实现非常简单,但是却有一个很大的缺陷 :无法限制短时间之内的集中流
量。假如我们需要限制每秒钟只能处理 10 次请求,如果前一秒钟产生了 10 次请求,这 10
次请求全部集中在最后的 10 毫秒中,而下一秒钟的前 10 毫秒也产生了 10 次请求,那么
在这 20 毫秒中就产生了 20 次请求,超过了限流的阈值。但是因为这 20 次请求分布在两
个时间窗口内,所以没有触发限流,这就造成了限流的策略并没有生效。

为了解决这个缺陷,就有了基于滑动窗口的算法。 这个算法的原理是将时间的窗口划分为
多个小窗口,每个小窗口中都有单独的请求计数。比如下面这张图,我们将 1s 的时间窗口
划分为 5 份,每一份就是 200ms;那么当在 1s 和 1.2s 之间来了一次新的请求时,我们就
需要统计之前的一秒钟内的请求量,也就是 0.2s~1.2s 这个区间的总请求量,如果请求量
超过了限流阈值那么就执行限流策略。

滑动窗口的算法解决了临界时间点上突发流量无法控制的问题,但是却因为要存储每个小的
时间窗口内的计数,所以空间复杂度有所增加。
虽然滑动窗口算法解决了窗口边界的大流量的问题,但是它和固定窗口算法一样,还是无法
限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑。 因此,在实际的项目
中,我很少使用基于时间窗口的限流算法,而是使用其他限流的算法:一种算法叫做漏桶算
法,一种叫做令牌筒算法。
漏桶算法与令牌筒算法
漏桶算法的原理很简单,它就像在流量产生端和接收端之间增加一个漏桶,流量会进入和暂
存到漏桶里面,而漏桶的出口处会按照一个固定的速率将流量漏出到接收端(也就是服务接
口)。
如果流入的流量在某一段时间内大增,超过了漏桶的承受极限,那么多余的流量就会触发限
流策略,被拒绝服务。
经过了漏桶算法之后,随机产生的流量就会被整形成为比较平滑的流量到达服务端,从而避
免了突发的大流量对于服务接口的影响。 这很像倚天屠龙记里,九阳真经的口诀:他强由他
强,清风拂山岗,他横由他横,明月照大江 。 也就是说,无论流入的流量有多么强横,多
么不规则,经过漏桶处理之后,流出的流量都会变得比较平滑。
而在实现时,我们一般会使用消息队列作为漏桶的实现,流量首先被放入到消息队列中排
队,由固定的几个队列处理程序来消费流量,如果消息队列中的流量溢出,那么后续的流量 就会被拒绝。这个算法的思想是不是与消息队列削峰填谷的作用相似呢?

另一种令牌桶算法的基本算法是这样的:
如果我们需要在一秒内限制访问次数为 N 次,那么就每隔 1/N 的时间,往桶内放入一个
令牌;
在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待
新的令牌或者直接拒绝服务;
桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这
样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题。

如果要从这两种算法中做选择,我更倾向于使用令牌桶算法, 原因是漏桶算法在面对突发流
量的时候,采用的解决方式是缓存在漏桶中, 这样流量的响应时间就会增长,这就与互联
网业务低延迟的要求不符;而令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的
突发流量,所以一般我会使用令牌桶算法来实现限流方案,而 Guava 中的限流方案就是使
用令牌桶算法来实现的。
你可以看到,使用令牌桶算法就需要存储令牌的数量,如果是单机上实现限流的话,可以在
进程中使用一个变量来存储;但是如果在分布式环境下,不同的机器之间无法共享进程中的
变量,我们就一般会使用 Redis 来存储这个令牌的数量。这样的话,每次请求的时候都需
要请求一次 Redis 来获取一个令牌,会增加几毫秒的延迟,性能上会有一些损耗。 因此,
一个折中的思路是: 我们可以在每次取令牌的时候,不再只获取一个令牌,而是获取一批
令牌,这样可以尽量减少请求 Redis 的次数。
课程小结
以上就是本节课的全部内容了。本节课我带你了解了限流的定义和作用,以及常见的几种限
流算法,你需要了解的重点是:
1. 限流是一种常见的服务保护策略,你可以在整体服务、单个服务、单个接口、单个 IP 或
者单个用户等多个维度进行流量的控制;
2. 基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现
限流的目的,但是都无法让流量变得更平滑;
3. 令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,但是令牌桶算法能够应对一
定的突发流量,所以在实际项目中应用更多。 限流策略是微服务治理中的标配策略,只是你很难在实际中确认限流的阈值是多少,设置的
小了容易误伤正常的请求,设置的大了则达不到限流的目的。所以,一般在实际项目中,我
们会把阈值放置在配置中心中方便动态调整;同时,我们可以通过定期地压力测试得到整体
系统以及每个微服务的实际承载能力,然后再依据这个压测出来的值设置合适的阈值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值