多副本/冗余设计与 failover
failover 故障转移
完全对等的节点之间做 failover。失败后随机选择其他节点。
不对等的节点,即存在主备节点。代码中控制如何检测主备机器可用性+如何主备切换,参考 Raft。
很多高可用系统采用内部心跳机制,比如 Kafka 中的通过 Zookeeper 的 watch 机制来做检测来做 broker 中 controller 的 failover,MySQL 里面是通过内部的 performance_schema 库中的 file_summary_by_event_name 表来做监控,都是为了避免外部监测的随机性所带来的影响。
必要时切流
- 方案一:机房入口下的某一层负载均衡层转到另一个机房。优点,切换快,秒级完成。缺点,依赖专线,专线不稳定,流量切换也有问题,且跨专线增加响应时间。
- 方案二:DNS 切换。优点:不依赖专线,响应时间不增加。缺点,DNS 缓存存在,导致更新会在小时级别。
隔离
隔离是指将系统或资源分割开,在系统发生故障时能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而把故障的影响限定在一个范围内。
限流
限流在日常生活中也很常见,比如在节假日期间去一个旅游景点,为了防止人流量过大,管理部门通常会在外面设置拦截,限制进入景点的人数,等有游客出来后,再放新的游客进去。对应到计算机中,比如要办活动、秒杀等,通常会限流。
限流可以分为技术层面的限流和业务层面的限流。技术层面的限流比较通用,各种业务场景都可以用到;业务层面的限流需要根据具体的业务场景做开发。
技术层面的限流
一种是限制并发数,也就是根据系统的最大资源量进行限制,比如数据库连接池、线程池、Nginx 的 limit_conn 模块;另一种是限制速率(QPS),比如 Guava 的 RateLimiter、Nginx 的 limit_req 模块。
限制速率的这种方式对于服务的接口调用非常有用。比如通过压力测试可以知道服务的 QPS 是 2000,就可以限流为 2000QPS。当调用方的并发量超过了这个数字,会直接拒绝提供服务。这样一来,即使突然有大量的请求进来,服务也不会被压垮,虽然部分请求被拒绝了,但保证了其他的服务可以正常处理。一般成熟的 RPC 框架都有相应的配置,可以对每个接口进行限流,不需要业务人员自己开发。
业务层面的限流
比如在秒杀系统中,一个商品的库存只有 100 件,现在有 2 万人抢购,没有必要放 2 万个人进来,只需要放前 500 个人进来,后面的人直接返回已售完即可。
针对这种业务场景,可以做一个限流系统,或者叫售卖的资格系统(票据系统),票据系统里面存放了 500 张票据,每来一个人,领一张票据。领到票据的人再进入后面的业务系统进行抢购;对于领不到票据的人,则返回已售完。
在具体实现上,有团队使用 Redis,也有团队直接基于 Nginx+Lua 脚本来实现,两者的思路类似。
限流算法
限制并发数的计算原理很简单,系统只需要维护正在使用的资源数或空闲数,比如数据库的连接数、线程池的线程数。限制速率的算法稍微复杂,常用的有漏桶算法和令牌桶算法,下面详细介绍。
两个算法的对比。对比两个算法会发现,二者的原理刚好相反,一个是流出速率保持恒定,一个是流入速率保持恒定。二者的用途有一定差别:令牌桶限制的是平均流入速率,而不是瞬时速率,因为可能出现一段时间没有请求进来,令牌桶里塞满了令牌,然后短时间内突发流量过来,一瞬间(可以认为是同时)从桶里拿几个令牌出来;漏桶有点类似消息队列,起到了削峰的作用,平滑了突发流入速率。
熔断
当电路发生短路、温度升高,可能烧毁整个电路的时候,保险丝会自动熔断,切断电路,从而保护整个电路系统。在计算机系统中,也有类似设计保险丝的思路。熔断有两种策略:一种是根据请求失败率,一种是根据请求响应时间。
根据请求失败率做熔断
对于客户端调用的某个服务,如果服务在短时间内大量超时或抛错,则客户端直接开启熔断,也就是不再调用此服务。然后过一段时间,再把熔断打开,如果还不行,则继续开启熔断。这也正是经常提到的“快速失败(Fail Fast)”原则。
根据请求响应时间做熔断
除了根据请求失败率做熔断,阿里巴巴公司的 Sentinel 还提供了另外一种思路:根据请求响应时间做熔断。当资源的平均响应时间超过阈值后,资源进入准降级状态。接下来如果持续进入 5 个请求,且它们的 RT 持续超过该阈值,那么在接下来的时间窗口内,对这个方法的调用都会自动地返回。
与限流进行对比会发现:限流是服务端,根据其能力上限设置一个过载保护;而熔断是调用端对自己做的一个保护。
注意:能熔断的服务肯定不是核心链路上的必选服务。如果是的话,则服务如果超时或者宕机,前端就不能用了,而不是熔断。所以,说熔断其实也是降级的一种方式。
系统运维
灰度发布
故障演练
压测
常态化压测、超限压测。
关注链路上的入口和出口带宽,关注线路的稳定性,关注哪一层可能成为瓶颈。
监控体系与日志报警
监控体系
要打造一个高可用、高稳定的系统,监控体系是其中非常关键的一个环节。监控体系之所以如此重要,因为它为系统提供了一把尺子,让我们对系统的认识不只停留在感性层面,而是理性的数据层面。有了这把尺子,可以做异常信息的报警,也可以依靠它去不断地优化系统。也正因为如此,稍微有些规模的公司都会在监控系统的打造上耗费很多工夫。
监控是全方位、立体化的,从大的方面来说,自底向上可以分为以下几个层次:
(1)资源监控。例如 CPU、内存、磁盘、带宽、端口等。比如 CPU 负载超过某个赋值,发报警;磁盘快满了,发报警;内存快耗光了,发报警……
资源监控是一个相对标准化的事情,开源的软件有 Zabbix 等,大一些的公司会有运维团队或基础架构团队搭建专门的系统来实现。
(2)系统监控。系统监控没有资源监控那么标准化,但很多指标也是通用的,不同公司的系统监控都会涉及:
· 最前端 URL 访问的失败率以及具体某次访问的失败链路;
· RPC 接口的失败率以及具体某次请求的失败链路;
· RPC 接口的平均响应时间、最大响应时间、95 线、99 线;
· DB 的 Long SQL;
· 如果使用的是 Java,JVM 的 young GC、full GC 的回收频率、回收时间。
(3)业务监控。不同于系统监控、资源监控的通用指标,业务监控到底要监控哪些业务指标,这点只能根据具体业务具体分析。
比如订单系统,假设定义了一个关键业务指标:订单支付成功率。怎么知道这个指标发生了异常呢?一种方法是与历史数据比较。比如知道昨天 24 小时内该指标的分布曲线,如果今天的曲线在某个点与昨天相比发生了剧烈波动,很可能是某个地方出现了问题。
另外一种是基于业务规则的,比如说外卖的调度系统,用户付钱下单后,假设规定最多 1 分钟之内这个订单要下发给商家,商家在 5 分钟之内要做出响应;商家响应完成后,系统要在 1 分钟之内计算出调度的外卖小哥。这个订单的履约过程涉及的时间点,都是一个个的阈值,都可能成为业务监控的指标。
把业务监控再扩展一下,就变成了对账系统。因为从数据角度来看,数据库的同一张表或者不同表的字段之间,往往暗含着一些关联和业务规则,甚至它们之间存在着某些数学等式。基于这些数学等式,就可以做数据的对账,从而发现问题。
日志报警
如果业务指标的监控是基于统计数据的一个监控,日志报警则是对某一次具体的请求的处理过程进行监控。
日志的作用之一是当有人发现线上出现问题后,可以通过查找日志快速地定位问题,但这是一个被动解决的过程。日志更重要的作用是主动报警、主动解决!也就是说,不是等别人来通报系统出了问题再去查,而是在写代码的时候,对于那些可以预见的问题,提前就写好日志。
众所周知,ASSERT 语句有 Undefined 行为,也就是说自己写的代码自己最清楚哪个地方可能有问题,哪个异常的分支语句没有处理。对于异常场景,导致的原因可能是程序的 Bug,也可能是上游系统传进来的脏数据,也可能是调用下游系统返回了脏数据……
针对这些有问题的地方,提前写好错误日志,然后对日志进行监控,就可以主动报警,主动解决。
在输出日志的过程中,最容易出现的问题可能有:
(1)日志等级不分。日志一般有 DEBUG、INFO、WARNING、ERROR 几个等级,有第三库打印出来的日志,还有自己的代码打印出来的日志。容易出现的问题是等级没有严格区分,到处是 ERROR 日志,一旦真出了问题,也被埋没在了大量的错误日志当中;或者 ERROR 当成了 WARNING,出现问题也没有引起足够的重视。
一个日志到底是 WARNING,还是 ERROR,往往需要根据自己的业务决定。WARNING 意味着要引起我们的注意,ERROR 是说必须马上解决。可能一个日志最开始的时候是 WARNING,后来它的重要性提高了,变成了 ERROR,或者反过来也有可能。
(2)关键日志漏打。一种是关键的异常分支流程没有打印日志;还有一种是虽然打印了日志,但缺乏足够的详细信息,没有把关键参数打印出来;或者只打印了错误结果,中间环节涉及的一系列关键步骤没有打印,只知道出了问题,不知道问题出在哪一个环节,这时候又要补日志。
关键是需要有一种意识,日志不是摆设,而是专门用于解决问题的。所以在打印日志之前,要想一下如果出了问题,依靠这些日志能否快速地定位问题。