学习笔记如何构建具有弹性设计的高可用平台——左耳听风专栏总结
Posted 在路上的德尔菲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记如何构建具有弹性设计的高可用平台——左耳听风专栏总结相关的知识,希望对你有一定的参考价值。
弹力设计
1认识故障
故障产生的原因可以分为以下几大类
- 网络问题:网络链接出现问题、宽带出现堵塞
- 性能问题:数据库慢SQL、Java full gc、硬盘IO过大、CPU飙高、内存不足
- 安全问题:被网络攻击,如DDoS
- 运维问题:系统不断进行更新和修改
- 管理问题:没有梳理出关键服务及服务依赖关系
- 硬件问题:硬盘损坏、网卡故障、机房掉电
故障是正常的,而且是常见的,"Everything will fails"
不要尝试着去避免故障,而是把处理故障的代码当成正常的功能做在架构里写在代码中
ps:四个9一年宕机时间约为53分钟,五个9一年宕机时间约为5分钟
2隔离设计
使用隔板(Bulkheads)技术,优点一是当故障蔓延开来,将架构分隔成多个模块隔离故障,即使部分机器故障了,只是影响部分用户;优点二为可以快速恢复,当某个环境出现故障,可以迅速下发配置,改变用户请求的路由方向,实现秒级故障恢复
一般有以下两种隔离思想:
-
按服务的种类来做分离:比如评论服务、用户服务、商品服务,不同服务使用不同的域名、服务器和数据库,做到接入层到应用层再到数据层三层完全隔离,每一个服务暴露出去,也就是微服务架构。通常引入大量异步处理模型
-
按用户来做分离:所谓多租户模型,对于比较大的客户,可以设置专门独立服务实例,对于比较小的用户,可以让他们共享一个服务实例。1、完全独立的设计;2、独立的数据分区,共享的服务;3、共享的服务,共享的数据分区。虚拟化技术KVM、Linux Container、Docker使实现物理资源的共享和成本节约
隔离设计的重点:
- 隔离业务的大小和粒度,分析业务的需求和系统
- 系统的复杂性、成本、性能、资源使用
- 隔离模式需要配置一些高可用、重试、异步、消息中间件,流控、熔断等设计模式
- 看到所有服务非常完整的监控系统
3异步通讯设计
同步:打电话,需要实时响应
异步:发邮件,不需要马上回复
当系统读写文件时,操作系统并不会真正同步地去读操作硬盘,而是把硬盘读写请求先在内存中停留一会,然后对这些读写请求做merge和sort,对相同的读请求只读一次,对相同的写操作,只写最后一次
TCP协议向网络发包的时候,会把我们要发送的数据先在缓冲区中进行囤积,当囤积到一定尺寸后(MTU)才向网络发送,极大化利用网络带宽
**异步处理让系统可以统一调度,异步处理系统的实质是把被动的任务处理变成主动地任务处理,实质是对任务进行调度和统筹管理,**对于大吞吐量的场景,异步通讯比同步通讯有更大优势:
同步调用的缺点:
- 同步调用要求被调用方的吞吐不低于调用方的吞吐,否则导致调用方因为性能不足而拖死调用方。
- 整个同步调用链的性能由最慢的那个服务决定,且如果被调用方有问题,调用方也会跟着出现问题,出现多米诺效应。
- 同步调用只能是一对一,很难做到一对多。
- 同步调用会导致调用方一直等待被调用方完成,若一层一层等待,所有参与方有相同的等待时间,这会非常消耗调用方的资源。
异步通讯的三种方式:
-
请求响应式:第一种发送方时不时轮询,问一下接收方是否完成;第二种发送方注册一个回调方法,接收方处理完回调请求方,存在一定的耦合,发送发依赖于接收方,并且要把自己的回调发送给接收方,处理完后回调。
-
通过订阅式:接收方订阅发送方的消息,发送方把相关消息或数据放到接收方所订阅队列中,而接收方从队列中获取数据,发送方只是告诉接收方有事要干,收完消息给个ACK就好了,这样服务是无状态的,分布式系统的服务设计需要向无状态服务考虑的。
-
通过Broker式(中间人):发送方和接收方互相看不到对方,发送方通过Broker发送消息,接收方从Broker接受消息,这样完全解耦,所有服务都不需要相互依赖,而是依赖一个中间件Broker。就要求Broker 必须高可用、可水平扩展、持久化不丢数据
其中第二、三条为事件驱动(服务间是平等的、服务间高度隔离、服务间吞吐速度解耦)缺点是:
- 业务流程不那么明显和好管理,整个架构比较复杂,需要有相关的服务消息跟踪机制
- 事件可能会乱序,需要管理一个状态机的控制
- 事务处理变得复杂,需要使用两阶段提交保证强一致性或最终一致性
怎么处理任务:
- 前台系统,把用户发来的请求意义记录下来,类似请求日志,操作在数据库或是存储上只会追加操作
- 任务处理系统处理收到的请求,需要一个任务派发器,一般使用推拉结合的系统,push端做一定的任务调度,比如把相同商品的订单合并起来,pull端订阅push端发出来的异步消息
- 异步处理可能一些故障导致任务没有被处理(消息丢失,没有通知,或通知到了却没有处理),任务处理方处理完成后给任务发起方回传状态,确保不会有漏掉;发起方需要有个定时任务,把一些超时没有回传状态的任务再重新做一遍
4幂等性设计
所谓幂等性设计,一次请求和多次请求某个资源应该具有同样的副作用,可以用f(x)=f(f(x))表示
系统解耦隔离后,服务间调用状态可能会有三个状态:Success、Failed、Timeout,前两个是明确状态,而超时有可能多种原因造成
举个栗子:
订单创建接口,第一次调用超时了,然后调用方重试一次,是否会多创建一笔订单?
解决方案是下游的系统提供支持幂等性的交易接口
首先需要一个全局唯一ID:UUID字符串占用空间较大,索引效率低;可采用分布式ID生成算法snowflake/leaf,产生一个long型的ID,41bits作为毫秒数,10bits作为机器编号,12bits作为毫秒内序列号
5服务的状态
所谓的状态,就是为了保留程序的一些数据或是上下文
无状态的服务就像一个一个函数一样,对于给定的输入,会给出唯一确定的输出,好处是很容易运维和伸缩,但需要底层有分布式的数据支持
有状态服务
- 数据本地化,状态和数据是本机保存的,这方面不但有更低的延时
- 更高的可用性和更强的一致性,CAP中的CA
对于客户端传来的请求,都必须保证其落在同一个实例上(Sticky Session),可通过持久化的长连接,HTTP长连接或hash算法走一致性哈希,这样带来的问题是结点负载和数据并不会很均匀,而无状态的服务需要我们把数据同步到不同的结点上
6补偿事务
由于分布式一个明显的问题就是一个业务流程需要组合一组服务,比如一个步骤失败了,那么要么回滚到以前的服务调用,要么不断重试保证所有的步骤都成功
- A,原子性,整个事务中所有操作,要么全部成功,要么全部失败,不可能停留在中间的某个环节
- C,一致性,事务开始之前和事务结束之后,数据库的完整性约束没有被破坏
- I,隔离性,两个事务的执行是互不干扰的
- D,持久性,事务完成之后,该事务对数据库所做的更改便持久保存到数据库中
事务的ACID属性保证了数据库的一致性,比如大家都去买一本书过程中,每个用户的购买请求都需要把库存锁住,等减完库存后再把锁释放出来,后续的人才能进行购买,这样就不可能做出性能比较高的系统出来
- B Basic Availability,基本可用
- S Soft-state,软状态,是处于有状态和无状态的服务一种中间状态,为了提高性能,可以让服务暂时保存一些状态或数据,这些状态和数据不是强一致性的
- E Eventual Consistency ,最终一致性
类似亚马逊买商品,大家会先收到一封邮件说系统收到你的订单了,然后过一会你会收到你的订单被确认的邮件,这时候才真正的分配库存,有的时候也会收到没有库存的邮件(😹)
业务补偿的设计重点
努力把业务流程执行完成,如果执行不完成,需要启动补偿机制,回滚业务流程
- 流程中涉及的服务方支持幂等性,并且在上游有重试机制
- 小心维护和监控整个过程的2状态,不要把这些状态放到不同的组件中,由一个业务状态流程的控制方(工作流引擎)来做这个事,且这个引擎是高可用的
- 下层业务方最好提供短期的资源预留机制,比如订单在30分钟内支付,否则回滚到之前的下单操作,等待用户重新下单
7重试设计
重试的意思是指认为这个故障是暂时的,而不是永久的
首先定义什么情况下需要重试TimeoutRetry,如调用超时、被调用端返回了某种可以重试的错误(流控中、维护中、资源不足等);而其他类型错误(没有鉴权、非法数据等)不要重试或很重要的问题不要重试,快速失败;定义重试的策略,重试的最大次数、每次重试的时间间隔;考虑重试的幂等性
在重试中使用指数级退避Exponential,每次重试时间都会成倍增加,这种机制使调用方能够有更多的时间处理我们请求
NoBackoffPolicy策略:无退避算法策略
FixedBackOffPolicy策略:固定时间退避测策略
UniformRandomBackOffPolicy策略:随机时间退避策略
8熔断设计
灵感来源于保险丝,当电路短路时,自动跳闸,电路就会自动断开,电器收到保护
如果重试机制,如果错误太多,短时间内得不到修复,那么重试的就没有意义,应开启熔断操作,防止应用程序不断地尝试执行可能会失败的操作,可保护后端不会过载;熔断器也可以诊断错误是否已经修正,如果已修正,应用程序会再次尝试调用操作。
简单来说熔断器就像是针对容易导致错误的操作一种代理,这种代理记录最近调用发生错误的次数,决定是继续操作还是立即返回。
熔断器有三种状态,每次状态切换的时候会发出一个事件,这种信息可以用来监控服务的运行状态:
- 闭合:需要一个调用失败的计数器,最近调用失败数超过阈值则切换到断开状态。此时开启一个超时时钟,当该时钟时间超过,切换到半开状态
- 断开:对应用程序的请求会立即返回错误响应,而不调用后端服务,这种做法比较粗暴,可以cache住上次成功请求(降级策略?)直接返回缓存
- 半开:允许应用程序一定数量的请求去调用服务,如果这些请求对服务的调用成功,那么可以认为之前导致的错误已经修正,此时将熔断器切换到闭合状态,错误计数器重置;如果失败,切换到断开状态,重置计时器给系统一定的时间来修正错误。半断开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次拖垮。
Hystrix 熔断实现逻辑:
- allowRequest()判断是否已在熔断中,(1)如果不在则放行;(2)在且到达熔断时间片则放行;(3)否则直接返回出错
- 调用markSuccess(duration) 和markFailure(duration) 统计一段时间内有多少调用成功/失败
- 计算failure/(success+failure)是否大于阈值,如果大于则打开熔断
- 内存中维护一个数组,记录每一个周期的请求结果的统计
如何设计一个熔断器
-
错误的类型:请求失败的的原因有很多种,需要对返回的错误码进行识别,比如一些错误码(限流、超时)先走几次重试策略,重试几次后再打开熔断策略;另一些错误码是远程服务挂掉,恢复时间比较长,直接打开熔断策略;还有一些忽略方法中抛出的特定异常,不会对这些异常进行统计(ignoreExceptions)
-
日志监控:熔断器应该能够记录所有失败的请求,以及一些可能会尝试成功的请求
-
测试服务是否可用:在断开状态下,熔断器可以采用定期ping一下远程服务的健康检查接口,来判断服务是否恢复,而不是使用计时器来自动切换到半开状态,好处是在服务恢复的情况下,不需要真实的用户流量就可以把状态从半开状态切回关闭状态。
-
资源分区:只对有问题的分区进行熔断,而不是整体
-
并发问题:相同的熔断器可能被大量并发请求同时访问,熔断器的实现不应该阻塞并发。可使用一个共享的数据结构或无锁的数据结构
-
手动重置:系统中对于失败操作的恢复时间很难确定,提供一个手动重置的功能能够使得管理员强制将熔断器切换到闭合状态
熔断器的关键参数
- 类似Hystrix的commandKey,即关注的唯一标示
- FallBackMethod,当发生熔断时,业务方需要执行降级策略
- isForceOpen,强制开启熔断,默认为false
- sleepWindowInMilliseconds,当发生熔断时,试探请求的时间窗口,默认为5000ms
- errorThresholdPercentage,滑动窗口内的失败率阈值,默认为1
- requestVolumeThreshold,滑动窗口内的请求总数阈值,默认为20
- ……
9限流设计
保护系统不会在过载的情况下出现问题,需要限流,很多地方都有限流的思想,如数据库连接池,线程池,nginx下的用于限制瞬时并发连接数的limit_conn模块,限制每秒平均速率的limit_req模块
限流策略
-
拒绝服务:把多的请求拒绝掉,统计哪个客户端来的流量最多,直接拒绝掉,这种方式可以把一些不正常的或有恶意的高并发访问拦在外面。在服务治理中心,可限制某个客户端应用请求的最大QPS,如配置appkey到服务端最大单机QPS为1000,如超过则会报RejectedException;限制服务端某个服务方法对某个客户端应用请求最大QPS,如com.xxx.EchoService服务接口的echo方法,对客户端应用account-service的最大单机QPS为200;配置服务端的单机最大QPS,如果客户端请求QPS超过阈值,服务端返回RejectedException。
-
服务降级:这样让服务有更多的资源处理更多的请求,一种降级的方式是把一些不重要的服务给停掉,把CPU、内存或是数据的资源让给更重要的功能;另一种是不再返回全量数据,只返回部分数据,以牺牲一致性的方式来获得更大的性能吞吐。
-
特权请求:资源不够用,只能把有限的资源分给重要的用户,如VIP用户
-
延时处理:一般有队列缓冲大量的请求,如果队列满了,那么也只能拒绝用户了,使用缓冲队列只是为了减缓压力,一般用于应对短暂的峰刺请求
-
弹性伸缩:动用自动化运维的方式对响应的服务做自动化伸缩,这需要应用性的监控系统,还需要自动化发布、部署、服务注册的运维系统,而且好要求快
限流的实现方式
- 计数器方式:简单暴力,直接维护一个计数器counter,当一个请求来了,就做加一处理,counter大于某阈值了,开启拒绝请求以保护系统。
- 队列算法:优先级队列,先处理高优先级的队列,再处理低优先级的队列。为了避免低优先队列被饿死,一般是分配不同比例的处理时间到不同的队列上,即带权重的队列,如处理权重为3的队列上3个请求后,再去权重为2的队列上处理2个请求,最后再去权重为1的队列上处理1个请求。如果处理过慢,就会导致队列满而开始触发限流。
- 漏斗算法:也是使用队列实现,当请求过多时,队列就开始积压请求,如果队列满了,就开始拒绝请求。漏斗算法实质就是在队列请求中加上限流器,来让processor以一个均衡的速度来处理请求。
- 令牌桶算法:主要需要一个中间人,在一个桶内按照一定的速率放入一些token,处理程序要处理请求时,需要拿到token才能处理,否则不能处理。在流量小的时候攒钱,流量大的时候可以快速处理。Rhino使用令牌桶算法
限流的设计要点
- 向用户承诺SLA
- 在多租户下,避免某一用户把资源耗尽而让所有的用户都无法访问的问题
- 为了应对突发的流量
- 节约成本,不会为了一个不常见的尖峰把系统扩容到最大的尺寸,而是有限的资源下能够承受比较高的流量
限流设计的考量
- 限流在架构早期考虑
- 限流模块性能要好,且对流量的变化非常敏感
- 存在手动开关,应急时可手动操作
- 存在监控事件通知,让知道限流事件发生,运维快速跟进
- 限流发生后,对于拒绝掉的请求,应该返回一个特定的限流错误码,可以和其他错误码区分开
10降级设计
降级(Degradation)本质是为了解决资源不足和访问量过大的问题,暂时牺牲掉一些东西以保证整个系统的平稳运行。对服务调用方来说,服务降级可以在被调用服务不可用时,通过临时的替代方案向上提供有损服务,保证业务柔性可用;对于服务提供方来说,服务降级极大的减轻了对服务提供方的压力,有助于服务提供方快速恢复:
- 降低一致性,从强一致性到弱一致性。包括简化流程的一致性,比如将全同步的方式,降级为全异步的方式,库存从单笔一致性也变成多笔最终一致性,如果库存不够,按照先来后到取消订单,功能降级可能会影响用户的体验;降低数据的一致性,一般是使用缓存或直接去掉数据(显示有库存而不是库存数量)
- 暂时停止某些次要功能,从而释放更多的资源,比如暂时停止用户评论功能等等。
- 简化功能,简化业务流程,不再返回全量数据,只返回部分数据。一般一个API会有两个版本,比如一个返回商品详情页基础信息、图片、评论,而降级版本只返回详情页的基础信息,因为评论会设计数据库的操作
降级的模式
- 强制降级:在强制降级模式下,客户端对某一服务的调用会全部降级。降级后会根据配置的降级策略返回。强制降级的开关需要由开发负责人手动打开和关闭。一般用于在紧急情况下对后端调用做全部降级。并在恢复正常后手动关闭降级开关。
- 失败降级:在失败降级模式下,客户端始终会调用后端服务,但仅当调用失败时才进行服务降级,这个时候将根据配置的降级策略返回。
- 自动降级:在自动降级模式下,客户端会根据当前的服务调用情况,自动识别是否需要降级。如果需要降级,会根据配置的降级策略返回;如果不需要降级,就进行正常的服务调用。
自动降级触发:
在客户端开启自动降级之后,会维持一个滑动时间窗口,统计过去一段时间内的服务调用情况,统计的指标包括:(a)请求总数,(b)降级请求数,©失败请求数
在滑动时间窗口内,根据下面的公式计算失败率:
失败率 = 失败请求数 / ( 请求总数 - 降级请求数 )
当失败率大于设定的阈值1%时,触发自动降级,这时99.9%的请求将会自动返回降级的值。
自动降级的恢复:
自动降级触发后,在滑动时间窗口内,如果失败率低于设定的熔断阈值1%时,会尝试恢复,Rhino有四种恢复策略:
- 立即恢复,全量恢复,不推荐
- 正常恢复,每秒恢复10%的流量,可自定义
- 快速恢复,按2的幂次方恢复流量,按每秒2%,4%,8%,16%,32%速度恢复
- 制定时间,即在规定时间内恢复完成,恢复速度是匀速的
降级的要点
- 清楚定义降级的关键条件,如吞吐量过大、响应时间过长、失败次数过多
- 梳理业务的功能,哪些是must-have,哪些是nice-to-have
- 对于读操作使用缓存解决,对于写操作使用异步调用来解决
- 降级的功能的开关可以是一个系统的配置开关
弹力设计总结
- 冗余服务:负载均衡、服务发现、动态路由、健康检查
- 服务解耦:业务补偿、异步通讯、工作流、业务分片。业务解耦开来,不让服务间受影响
- 服务容错:重试、幂等、限流、熔断、降级、缓存、裁剪数据
左耳听风专栏:
https://time.geekbang.org/column/intro/48
以上是关于学习笔记如何构建具有弹性设计的高可用平台——左耳听风专栏总结的主要内容,如果未能解决你的问题,请参考以下文章