个人随笔
目录
简单分析⾼可⽤与稳定性的策略
2025-04-21 21:27:00

如果“⾼并发”是为了让系统变得“有效率”,可以抵抗⼤规模⽤户访问,那么x下面所讲的就是为了让系统变得“更靠谱”。靠谱包括了⾼可⽤性、稳定性、可靠性。要实现一个靠谱的系统,要从哪些方面下手呢?

多副本

不要把鸡蛋放在同一个篮子里,具体到计算机系统,就是常说的避免“单点”:

⽹关层的Nginx宕机怎么办?做多个副本;
应⽤服务器宕机怎么办?做多个副本;
缓存宕机怎么办?做多个副本;
数据库宕机怎么办?做多个副本;
消息中间件宕机怎么办?做多个副本;
……
对于⽹关或应⽤服务器这种⽆状态的服务,做多个副本⽐较简单,加机器就可以应对;但对于缓存或数据库这种有状态的机器,如果做多个副本,则会存在数据间如何同步的问题。

1、本地缓存多副本

⼀种常⽤的⽅法是利⽤消息中间件(如Kafka)的Pub/Sub机制,每台机器都订阅消息。⼀条消息发出去之后,每台机器都会收到这条消息,然后更新⾃⼰的本地缓存。

2、Redis多副本

Redis Cluster提供了Master-Slave之间的复制机制,当Master宕机后可以切换到Slave。当然,切换需要间隔时间(⼀般为⼏⼗秒),同时因为是异步复制,切换之后可能会丢失少量的数据。

⽆论本地缓存,还是 Redis 类的集中式缓存,既然本⾝的定位就是“缓存”,意味着业务场景对数据不是强⼀致性的要求,所以此处主要考虑的是“⾼可⽤性”,⽽没有数据⼀致性的保证。

3、MySQL多副本

对于MySQL,Master-Slave之间⽤得最多的是异步复制或半异步复制,同步复制因为性能严重下降,所以⼀般很少使⽤。

对于半异步复制,如果 Slave 超时后还未返回,也会退化为异步复制。所以,⽆论异步复制,还是半异步复制,都不能百分百地保证Master和Slave中的数据完全⼀致。当 Master 宕机后,如果⽴即切换到 Slave,没有及时同步的数据丢失
了怎么办呢?如果不切换到Slave,服务就不可⽤了。要切换到Slave吗?

实际上,⼀般都会选择牺牲⼀定的数据⼀致性来保证可⽤性。少量的数据不⼀致,可以通过后续的⼈⼯修复解决;如果服务不可⽤,尤其在流量很⼤的时候,带来的损失往往远⼤于少量数据不⼀致带来的损失。另外会有⼀些监控措施,如果发现某个Slave延迟太⼤,则直接摘除,避免延迟很⼤的Slave被选为主库。

4、消息中间件多副本

对于Kafka类的消息中间件,⼀个Partition通常⾄少会指定三个副本,为此Kafka专门设计了⼀种称为ISR的算法,在多个副本之间做消息的同步。

注意:虽然有多个副本,但在某些情况下,当发⽣Master-Slave切换的时候,还是会丢消息,这是由算法本⾝决定的。

隔离、限流、熔断和降级

1、隔离

隔离是指将系统或资源分割开,在系统发⽣故障时能限定传播范围和影响范围,即发⽣故障后不会出现滚雪球效应,从⽽把故障的影响限定在⼀个范围内。比如数据隔离、机器隔离、线程池隔离、信号量隔离。

2、限流

限流在⽇常⽣活中也很常见,⽐如在节假⽇期间去⼀个旅游景点,为了防⽌⼈流量过⼤,管理部门通常会在外⾯设置拦截,限制进⼊景点的⼈数,等有游客出来后,再放新的游客进去。对应到计算机中,⽐如要办活动、秒杀等,通常会限流。

限流可以分为技术层⾯的限流和业务层⾯的限流。技术层⾯的限流⽐较通⽤,各种业务场景都可以⽤到;业务层⾯的限流需要根据具体的业务场景做开发。

(1)技术层⾯的限流。⼀种是限制并发数,也就是根据系统的最⼤资源量进⾏限制,⽐如数据库连接池、线程池、Nginx的limit_conn模块;另⼀种是限制速率(QPS),⽐如Guava的RateLimiter、Nginx的limit_req模块。

限制速率的这种⽅式对于服务的接口调⽤⾮常有⽤。⽐如通过压⼒测试可以知道服务的QPS是2000,就可以限流为2000QPS。当调⽤⽅的并发量超过了这个数字,会直接拒绝提供服务。这样⼀来,即使突然有⼤量的请求进来,服务也不会被压垮,虽然部分请求被拒绝了,但保证了其他的服务可以正常处理。⼀般成熟的RPC框架都有相应的配置,可以对每个接口进⾏限流,不需要业务⼈员⾃⼰开发。

(2)业务层⾯的限流。⽐如在秒杀系统中,⼀个商品的库存只有100件,现在有2万⼈抢购,没有必要放2万个⼈进来,只需要放前500个⼈进来,后⾯的⼈直接返回已售完即可。针对这种业务场景,可以做⼀个限流系统,或者叫售卖的资格系统(票据系统),票据系统⾥⾯存放了500张票据,每来⼀个⼈,领⼀张票
据。领到票据的⼈再进⼊后⾯的业务系统进⾏抢购;对于领不到票据的⼈,则返回已售完。在具体实现上,有团队使⽤Redis,也有团队直接基于Nginx+Lua脚本来实现,两者的思路类似。

3、熔断

当电路发⽣短路、温度升⾼,可能烧毁整个电路的时候,保险丝会⾃动熔断,切断电路,从⽽保护整个电路系统。在计算机系统中,也有类似设计保险丝的思路。熔断有两种策略:⼀种是根据请求失败率,⼀种是根据请求响应时间。

(1)根据请求失败率做熔断。对于客户端调⽤的某个服务,如果服务在短时间内⼤量超时或抛错,则客户端直接开启熔断,也就是不再调⽤此服务。然后过⼀段时间,再把熔断打开,如果还不⾏,则继续开启熔断。这也正是经常提到的“快速失败(Fail Fast)”原则。

(2)根据请求响应时间做熔断。除了根据请求失败率做熔断,阿⾥巴巴公司的 Sentinel 还提供了另外⼀种思路:根据请求响应时间做熔断。当资源的平均响应时间超过阈值后,资源进⼊准降级状态。

注意:能熔断的服务肯定不是核⼼链路上的必选服务。如果是的话,则服务如果超时或者宕机,前端就不能⽤了,⽽不是熔断。所以,说熔断其实也是降级的⼀种⽅式。

4、降级

降级是⼀种兜底⽅案,是在系统出故障之后的⼀个尽⼒⽽为的措施。相⽐于限流、熔断两个偏技术性的词汇,降级则是⼀个更加偏向业务的词汇。

因为在现实中,虽然任何⼀个业务或系统都有很多功能,但并不要求这些功能⼀定 100%可⽤,或者完全不可⽤。其中存在⼀个灰度空间。⽐如对于微信或者 QQ,它有⽂字通信、语⾳通信、视频通信,对带宽的要求是从⼩到⼤的。⽐如⽹络发⽣故障,视频通信不能⽤,但可以保证语⾳通信、⽂字通信可以使⽤;如果语⾳通信也不能使⽤了,则保证⽂字通信可以⽤。总之,会尽最⼤努⼒提供服务,哪怕是有损服务,也⽐完全不提供服务要强。

再⽐如电商的商品展⽰页⾯,有图⽚、⽂字描述、价格、库存、优惠活动等信息,当优惠活动的服务宕机,其他信息可以正常展⽰,并不影响⽤户的下单⾏为。

再⽐如电商⾸页的商品列表的千⼈千⾯,可能靠的是推荐系统。当推荐系统宕机时,是否⾸页就显⽰502呢?可以做得更好⼀些,例如为⾸页准备⼀个⾮个性化的商品列表,甚⾄⼀个静态的商品列表。这个列表存在于另外⼀个⾮常简单可靠的后备系统中,或者缓存在客户端上⾯。当推荐系统宕机时,可以把这个⾮个性化的列表输出。虽然没有了个性化,但⾄少⽤户能看到东西,还可以购买商品。
通过这些例⼦会发现,降级不是⼀个纯粹的技术⼿段,⽽是要根据业务场景具体分析,看哪些功能可以降级,降级到什么程度,哪些宁愿不可⽤也不能降级。

灰度发布与回滚

如果⼀个系统在线上代码不动,不发布更新,理论上可以稳定地⼀直运⾏下去(在没有资源泄露的Bug、前端流量没有⼤的变化的情况下),但实际是不可能的。尤其对于互联⽹公司要求快速迭代的⽂化,新功能⼀直在发布,旧的系统也在被不断地重构。频繁地系统变更是导致系统不稳定的⼀个直接因素。既然⽆法避免系统变更,我们能做的就是让这个过程尽可能平滑、受控,这就是灰度与回
滚策略。

新功能上线的灰度

当⼀个新的功能上线时,可以先将⼀部分流量导⼊这个新的功能,如果验证功能没有问题了,再⼀点点地增加流量,最终让所有流量都切换到这个新功能上。

具体办法有很多,⽐如可以按user_id划分流量,按user_id的最后⼏位数字对⽤户进⾏分⽚,⼀⽚⽚的灰度把流量导⼊到新功能上;或者⽤户有很多属性、标签,按其中的标签设置⽤户⽩名单,再⼀点点地导⼊流量。

2、旧系统重构的灰度

如果旧的系统被重构了,我们不可能在⼀瞬间把旧的系统下线,完全
变成新的系统。⼀般会持续⽤⼀段时间,新旧系统同时共存。这时就需要
在⼊⼜处增加⼀个流量分配机制,让部分流量仍然进⼊⽼系统,部分流量
切换到新系统。⽐如最初⽼系统的流量为90%,新系统为10%;然后⽼系
统为60%,新系统为40%……逐步转移,最终把所有流量都切换到新系
统,将⽼系统下线。具体流量如何划分,与上述的新功能上线类似,也是
根据实际业务场景,选取某个字段或者属性,对流量进⾏划分。

3、回滚

有了灰度,还要考虑的⼀个问题就是回滚。当⼀部分实现了灰度之后,发现新的功能或新的系统有问题,这时要回滚应该怎么做呢?⼀种是安装包回滚,这种办法最简单,不需要开发额外代码,发现线上系统有问题,统⼀重新部署之前的安装版本;另⼀种是功能回滚,在开发新功能的时候,也开发了相应的配置开关,⼀旦发现新功能有问题,则关闭开关,让所有流量都进⼊⽼的系统。

监控体系与⽇志报警

监控体系

要打造⼀个⾼可⽤、⾼稳定的系统,监控体系是其中⾮常关键的⼀个环节。监控体系之所以如此重要,因为它为系统提供了⼀把尺⼦,让我们对系统的认识不只停留在感性层⾯,⽽是理性的数据层⾯。有了这把尺⼦,可以做异常信息的报警,也可以依靠它去不断地优化系统。也正因为如此,稍微有些规模的公司都会在监控系统的打造上耗费很多⼯夫。

监控是全⽅位、⽴体化的,从⼤的⽅⾯来说,⾃底向上可以分为以下
⼏个层次:
(1)资源监控。例如 CPU、内存、磁盘、带宽、端⼜等。⽐如 CPU负载超过某个赋值,发报警;磁盘快满了,发报警;内存快耗光了,发报警……资源监控是⼀个相对标准化的事情,开源的软件有Zabbix等,⼤⼀些的公司会有运维团队或基础架构团队搭建专门的系统来实现。

(2)系统监控。系统监控没有资源监控那么标准化,但很多指标也是通⽤的,不同公司的系统监控都会涉及:
· 最前端URL访问的失败率以及具体某次访问的失败链路;
· RPC接⼜的失败率以及具体某次请求的失败链路;
· RPC接⼜的平均响应时间、最⼤响应时间、95线、99线;
· DB的Long SQL;
· 如果使⽤的是Java,JVM的young GC、full GC的回收频率、回收时
间。

(3)业务监控。不同于系统监控、资源监控的通⽤指标,业务监控到底要监控哪些业务指标,这点只能根据具体业务具体分析。⽐如订单系统,假设定义了⼀个关键业务指标:订单⽀付成功率。怎么知道这个指标发⽣了异常呢?⼀种⽅法是与历史数据⽐较。⽐如知道昨天24⼩时内该指标的分布曲线,如果今天的曲线在某个点与昨天相⽐发⽣了剧烈波动,很可能是某个地⽅出现了问题。

另外⼀种是基于业务规则的,⽐如说外卖的调度系统,⽤户付钱下单后,假设规定最多 1分钟之内这个订单要下发给商家,商家在5分钟之内要做出响应;商家响应完成后,系统要在1 分钟之内计算出调度的外卖⼩哥。这个订单的履约过程涉及的时间点,都是⼀个个的阈值,都可能成为业务监控的指标。

把业务监控再扩展⼀下,就变成了对账系统。因为从数据⾓度来看,
数据库的同⼀张表或者不同表的字段之间,往往暗含着⼀些关联和业务规
则,甚⾄它们之间存在着某些数学等式。基于这些数学等式,就可以做数
据的对账,从⽽发现问题。

4、日志报警

如果业务指标的监控是基于统计数据的⼀个监控,⽇志报警则是对某⼀次具体的请求的处理过程进⾏监控。⽇志的作⽤之⼀是当有⼈发现线上出现问题后,可以通过查找⽇志快速地定位问题,但这是⼀个被动解决的过程。⽇志更重要的作⽤是主动报警、主动解决!也就是说,不是等别⼈来通报系统出了问题再去查,⽽是在写代码的时候,对于那些可以预见的问题,提前就写好⽇志。

众所周知,ASSERT 语句有 Undefined ⾏为,也就是说⾃⼰写的代码⾃⼰最清楚哪个地⽅可能有问题,哪个异常的分⽀语句没有处理。对于异常场景,导致的原因可能是程序的 Bug,也可能是上游系统传进来的脏数据,也可能是调⽤下游系统返回了脏数据……

针对这些有问题的地⽅,提前写好错误⽇志,然后对⽇志进⾏监控,就可以主动报警,主动解决。

在输出⽇志的过程中,最容易出现的问题可能有:
(1)⽇志等级不分。⽇志⼀般有DEBUG、INFO、WARNING、ERROR⼏个等级,有第三库打印出来的⽇志,还有⾃⼰的代码打印出来的⽇志。容易出现的问题是等级没有严格区分,到处是 ERROR ⽇志,⼀旦真出了问题,也被埋没在了⼤量的错误⽇志当中;或者 ERROR 当成了WARNING,出现问题也没有引起⾜够的重视。

⼀个⽇志到底是WARNING,还是ERROR,往往需要根据⾃⼰的业务决定。WARNING意味着要引起我们的注意,ERROR是说必须马上解决。可能⼀个⽇志最开始的时候是WARNING,后来它的重要性提⾼了,变成了ERROR,或者反过来也有可能。

(2)关键⽇志漏打。⼀种是关键的异常分⽀流程没有打印⽇志;还有⼀种是虽然打印了⽇志,但缺乏⾜够的详细信息,没有把关键参数打印出来;或者只打印了错误结果,中间环节涉及的⼀系列关键步骤没有打印,只知道出了问题,不知道问题出在哪⼀个环节,这时候又要补⽇志。关键是需要有⼀种意识,⽇志不是摆设,⽽是专门⽤于解决问题的。所以在打印⽇志之前,要想⼀下如果出了问题,依靠这些⽇志能否快速地定位问题。

 6

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2