跟随黑马面Java-5,微服务篇
Joker2Yue第五篇:微服务篇
“求其上,得其中;求其中,得其下,求其下,必败”
开篇
SpringCloud
Spring Cloud 组件有哪些?
-
Spring Cloud Eureka
-
Eureka:注册中心
-
Ribbon:负载均衡
-
Feign:远程调用
-
Hystrix:服务熔断
-
Zuul/Gateway:网关
-
-
Spring Cloud Alibaba
- 注册中心/配置中心Nacos
- 负载均衡Ribbon
- 服务调用Feign
- 服务保护sentinel
- 服务网关Gateway
Spring Cloud 注册中心 Eureka
-
Eureka的作用
在微服务架构中,有两个微服务
order-service
和user-service
。如果order-service
有一个下单的操作,需要保存订单。但是,这个操作需要远程调用user-service
来获取用户的数据,那order-service
需要拿到user-service
的 IP 和端口。这样才能知道哪个是user-service
。这时候,就需要用到注册中心了。假如
order-service
需要调用user-service
,如果user-service
中有多个实例,那么order-service
应该调用哪一台呢?在这个案例中,有两个重要的角色,
user-service
是服务的提供者,order-service
是服务的消费者。注意,这里提到的提供者和消费者都是相对而言的。首先,当服务提供者提供服务时,就要把它自己的信息注册到注册中心。这时候,注册中心就会保存服务提供者这些服务器的地址。然后,
order-service
需要从注册中心拉取信息。那么这时候消费者选择哪一个去调用呢?它会在内部做一个负载均衡,如果它选中了localhost:8081
,那么就会选择 8081 服务器进行调用。如果这时候有一台服务器宕机了,这时候该怎么办?服务提供者需要定期向注册中心发送心跳,证明当前是一个健康的实例,假如当前某个实例一直没有发送心跳,说明此实例已经宕机。那么Eureka就要从服务列表中将其移除。相对应的,当消费者拉取信息的时候,也没有此已宕机的微服务地址。
-
nacos的工作流程
与Eureka基本一致,也是服务提供者需要向注册中心注册,服务消费者从注册中心拉取信息。
-
与Eureka不同的地方
-
nacos中有一个临时实例(
ephemeral
),默认值为true。如果我们将其修改为false,服务即使宕机也不会被注册中心剔除,而且是注册中心主动询问服务是否在线(不是微服务向注册中心发送心跳)。 -
如果nacos中某一个服务提供者的地址发生变更,那么nacos还会主动向消费者推送变更的信息。
-
nacos集群默认采用AP模式,保证高可用;当集群中有非临时实例时采用CP模式;而Eureka爱用AP模式。
-
nacos还支持配置中心,eureka只有注册中心。
-
-
负载均衡是如何实现的
由Ribbon从注册中心拉取提供者信息,然后由Ribbon选择调用哪个。
-
Ribbonde负载均衡策略
- RoundRobinRule:简单轮询服务列表来选择服务器。
- WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小。
- RandomRule:随机选择一个可用的服务器。
- BestAvailableRule:忽略那些短路的服务器,并选择并发数较低的服务器。
- RetryRule:重试机制的选择逻辑。
- AvailabilityFilteringRule:可用性敏感策略,先过滤非健康的,再选择连接数较小的实例。
- ZoneAvoidanceRule:(默认)以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
-
自定义负载均衡
要实现自定义的负载均衡,我们都是在服务的发起方(消费者)做自定义的。
可以自已创建类实现IRule接口,然后再通过配置类或者配置文件配置即可,通过定义IRule实现可以修改负载均衡规则,有两种方式:
-
【全局生效】加一个配置类,在配置类中,我们可以直接设置一个返回值。然后将其注册至Spring中
-
【局部生效】可以在Ribbon的配置文件中指定某个服务使用哪种负载均衡策略
-
服务雪崩、熔断
服务雪崩
一个微服务项目中可能存在数目众多的微服务,微服务之间的通信都有可能发生feign的远程调用。如果某一个服务断开,就有可能导致服务雪崩。如果上图中服务D挂了,那么服务A调用服务D的时候就会失败。如果服务A还不断的向服务D发起请求,在服务A自身的连接池被沾满之后,就不能再对外提供服务了,此时,服务A也宕机了。那么其他服务调用服务A的时候也可能遇到同样的情况。此时,因为一个服务D的宕机,而导致整个微服务宕机,这就是服务雪崩。
简单来说,服务雪崩就是:一个服务失败,导致整条链路的服务都失败的情形。
-
服务降级:针对于某个接口(
order-service
中getUser()
方法)服务降级是一种服务自我保护的策略,旨在确保服务在面对请求突增时不会因负载过重而变得不可用,从而保护下游服务(离最先发起请求的位置越近,越接近上游)免受影响并防止整个系统的崩溃。在面对异常或高负载时,(消费者)服务降级通过提供备用的、简化的功能或数据来应对,充当了系统的兜底机制,确保系统在恶劣条件下依然能够提供基本的服务。
-
服务熔断:针对于整个微服务(整个
order-service
)Hystrix熔断机制用于监控微服务调用情况,默认处于关闭状态。要启用该机制,需在(消费者)引导类上添加注解:
@EnableCircuitBreaker
。一旦检测到在10秒内的请求失败率超过50%,熔断机制就会被触发。随后,系统每隔5秒重新尝试请求微服务。如果微服务无法响应,系统将继续执行熔断机制。只有在微服务重新可用时,熔断机制才会被关闭,恢复正常的请求流程。
微服务的监控
-
为什么需要监控
因为一个项目中服务过多,不利于问题定位/性能分析/梳理微服务关系,也不利于处理服务告警。
-
常见的服务监控工具
-
Springboot-admin
简单,功能单一,不采用
-
prometheus+Grafana
用的比较多,但是搭建比较复杂
-
zipkin
和代码有耦合,不建议使用
-
skywalking
和zipkin一样,是链路追踪工具
-
-
SkyWalking
一个分布式系统的应用程序性能监控工具(Application Performance Managment),提供了完善的链路追踪能力,apache的顶级项目(前华为产品经理吴晟主导开源)。
-
服务(service):业务资源应用系统(微服务)
-
端点(endpoint):应用系统对外暴露的功能接口(接口)
-
实例(instance):物理机
-
-
Skywalking界面
-
【仪表盘】界面可以查看 微服务基础信息
-
【追踪】界面可以查看比较慢的接口的详情
点击还能够查看执行的sql
-
开发【服务关系】,可以查看服务拓扑图
-
点击【告警】能够查看服务告警信息。
告警类型 触发条件 服务响应时间告警 在过去10分钟内,服务平均响应时间超过1秒,且在3分钟内达3次 服务成功率告警 在过去10分钟内,服务成功率低于80%,且出现2次 服务响应时间分位数告警 在过去10分钟内,服务的90%响应时间低于1秒,且出现3次 服务响应时间异常告警 在过去10分钟内,服务的响应时间超过1秒,且出现2次 端点响应时间异常告警 在过去10分钟内,端点的响应时间超过1秒,且出现2次 当然,还可以自定义告警。
-
业务相关
微服务限流(漏桶算法、令牌桶算法)
-
为什么要限流
- 并发的确很大(突发流量)
- 防止用户恶意刷接口
-
限流的实现方式
-
Tomcat:可以设置最大连接数
-
Nginx,漏桶算法
-
网关,令牌桶算法
-
自定义拦截器
-
-
Nginx限流
-
漏桶算法
漏桶算法是一种流量控制机制,它通过模拟一个固定容量的漏桶,以恒定速率漏水来控制数据流的速率。在Nginx中,漏桶算法用于限速模块,防止请求过载。请求像水滴一样进入桶中,超过桶容量的请求被丢弃或延迟处理,而桶内的请求以稳定速率处理,确保系统平稳运行。
-
Nginx 限速配置语法说明(不用记,了解即可)
语法:
limit_req_zone key zone rate
-
key:定义限流对象,例如
binary_remote_addr
,基于客户端 IP 限流。 -
zone:定义共享存储区来存储访问信息,例如
10m
可以存储约 16 万个 IP 地址的访问信息。 -
rate:最大访问速率,例如
rate=10r/s
表示每秒最多处理 10 个请求。 -
burst:表示桶的大小,例如
burst=20
,允许在短时间内突发 20 个请求。 -
nodelay:快速处理突发请求,不进行排队等待。这意味着请求过来之后将会迅速安排处理,;桶满了将会快速进行丢弃。
-
-
-
控制并发连接数
-
Nginx 连接限制配置说明
语法:
limit_conn zone number
-
limit_conn perip 20:对应的 key 是
$binary_remote_addr
,表示限制单个 IP 同时最多能持有 20 个连接。 -
limit_conn perserver 100:对应的 key 是
$server_name
,表示虚拟主机(server)同时能处理的并发连接总数限制为 100 个。
-
-
-
网关限流
yml配置文件中,微服务路由设置添加局部过滤器RequestRateLimiter
-
语法说明
- key-resolver:定义限流对象(例如 IP、路径、参数),需要通过代码实现,使用 Spring Expression Language (SpEL) 表达式来获取。例如,可以通过 SpEL 表达式
#request.remoteAddr
获取客户端 IP。 - replenishRate:令牌桶每秒填充的平均速率。表示每秒可以生成多少令牌,用于控制请求速率。
- burstCapacity:令牌桶的总容量。表示令牌桶中最多可以存储的令牌数量,即在突发情况下可以允许多少请求。
- key-resolver:定义限流对象(例如 IP、路径、参数),需要通过代码实现,使用 Spring Expression Language (SpEL) 表达式来获取。例如,可以通过 SpEL 表达式
与漏桶算法不同的是,令牌桶算法中存储的是令牌,而且其工作原理是,按照一定速率生成令牌并存入令牌桶中,当令牌桶满就暂停生成。请求需要在令牌桶中申请令牌才能够被微服务服务接收处理,如果请求没有获得令牌,此请求将会被阻塞或丢弃。
-
-
分布式系统理论(CAP和BASE)
-
CAP定理
CAP是指Consistency(一致性)、Availability(可用性)和Partition tolerance(分区容忍性),它是分布式系统设计中的一个基本理论原则,指出在分布式系统中无法同时满足这三个属性。
-
Consistency(一致性)
用户访问分布式系统中任意节点,得到的数据必须一致。
-
Availability(可用性)
用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
-
Partition tolerance(分区容忍性)
-
Partition(分区):因为网络敌障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
-
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务。
因为网路故障,
node3
形成了独立一个分区。在node1
或node2
上进行写数据,node3
肯定是不能够及时同步过来的。此时就出现数据不一致问题,没有满足刚才的一致性原则。这时候如果node3
上来请求了,它有两个选择:一,继续为请求提供服务,这保证了服务的可用性,但是没有满足一致性要求;二,让请求等待,直到网络恢复,进行数据同步后才继续提供服务,这保证了服务的一致性,但是没有满足可用性。 -
-
结论:
分布式系统节点之间肯定是需要网络连接的,分区(P)是必然存在的。
如果保证访问的高可用性(A),可以持续对外提供服务,但不能保证数据的强一致性–>AP。如果保证访问的数据强一致性(C),就要放弃高可用性–> CP。
-
-
BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
-
Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
-
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
-
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
现在有三个微服务:订单服务、账户服务、库存服务。它们共同处理一个下单的业务。这就涉及到分布式事务的问题了。在用户下单之后,用户需要现在订单微服务中保存,订单微服务需要远程调用账户服务来扣款,然后又去远程调用库存微服务去执行扣除库存。
上述的流程中,假如任何一个微服务发生了失败(比如库存不足导致库存微服务失败),我们就要对整个事务进行回滚,即三个操作要么同时成功要么同时失败。这就是分布式事务要完成的事情。
那这和之前提到的CAP和BASE又有什么关系?因为目前这三个微服务都是通过网络连接,是一个分布式的项目,如果要完成通信,就要做出选择了,要么AP要么CP。我们可以使用CAP和BASE理论来指导微服务的部署。
如果我们选择的是AP,也就是满足了可用性,牺牲了一定的一致性。如果我们现在每个子事务(可以把每个微服务看作一个子事务)都各自提交各自的事务,并且有的失败了有的成功了。那么此时它们的状态就不一样了,处于一个软状态,临时的不一致状态。各个事务提交完成后,就要相互看看对方是不是成功了。这就需要用到事务协调器了。由它负责统一管理这些事务的状态。如果它发现有的事务失败了,那么就通知已经提交的事务,说需要再将数据恢复过来。当然,这里的恢复数据不是回滚事务,因为我们之前已经将事务提交了。所以此时需要逆向操作。比如我们之前新增了一个订单,那么此时只需要将其删除就行。这样数据就恢复到了最初的样子,保证了最终的一致性。
如果我们选择CP,也就是满足一致性,牺牲一定可用性。我们现在每个事务分别去执行自己的业务,不过与AP不一样的是我们各个子事务不会去直接提交自己的子事务,因为提交之后事务不能回滚了。现在只需要各个事物相互等待,然后将各自的情况报告给事务协调器,然后由事务协调器统一通知它们是提交还是回滚事务。这样就保证了强一致性,因为这个过程中没有软状态。但是在此等待事务协调器决策时间内,各个服务都处于一个弱可用的状态,因为会锁定资源,导致其他线程无法返回。
-
分布式事务解决方案
Seata架构
Seata事务管理中有三个关键角色:
-
TC(Transaction Coordinator)- 事务协调者:负责维护全局和分支事务状态,协调全局事务的提交或回滚。需要单独部署。
-
TM(Transaction Manager)- 事务管理器:定义全局事务的范围,启动全局事务,并负责提交或回滚全局事务。
-
RM(Resource Manager)- 资源管理器:管理分支事务所需的资源,与TC通信以注册分支事务和报告分支事务状态,并推动分支事务的提交或回滚。可以认为是某一个微服务。
当事务来临,首先由TM开启事务,最后也是由TM来负责回滚事务。中间的RM都是分支事务。这些分支事务要想协调管理,就要都去注册到TC上,然后报告各个分支事务的状态。
-
seata的XA模式 — CP
-
RM一阶段的工作:
- 注册分支事务到TC。
- 执行分支业务,但不提交。
- 报告执行状态到TC。
-
TC二阶段的工作:
- 检测各分支事务执行状态。
- a. 如果都成功,通知所有RM提交事务。
b. 如果有失败,通知所有RM回滚事务。
-
RM二阶段的工作:
- 接收TC指令,提交或回滚事务。
-
-
seata的AT模式 — AP
AT模式同样是分阶段提交的事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷。
阶段一RM的工作:
-
注册分支事务。
-
记录undo-log(数据快照)。
-
执行业务SQL并提交。
-
报告事务状态。
阶段二提交时RM的工作:
-
删除undo-log即可。
阶段二回滚时RM的工作:
-
根据undo-log恢复数据到更新前。
-
-
seata的TCC模式 — AP
- Try:执行资源检测和预留操作;要求Try成功,Confirm一定能成功。
- Confirm:完成资源操作业务。
- Cancel:释放预留的资源;可以理解为Try的反向操作。
TCC模式对代码的侵入度较高,而XA和AT是框架自带的,代码侵入度较低。
MQ分布式事务
比如我们现在需要去借呗借钱,借呗审核通过之后 ,我们支付宝的余额才能增加。但是借呗和支付宝可能不是同一个系统,这时候我们也可以使用MQ来解决分布式事务的问题。
流程大致如下:张三去向借呗借钱,借呗审核他的资质。如果审核通过 ,就要同步生成借款单。借款单生成后,向MQ发送消息,然后通知支付宝进行转账。支付宝读取到消息后,直接增加账户余额就行了。但是我们需要保证给MQ发送消息和生成借款单在同一个事务内进行。比如审核没有通过或者审核的时候代码抛异常了,就没必要发送消息了。
支付宝读消息之后,余额才会增加,这才算是完成了整个操作。但是如果中途发生异常(比如余额操作异常)怎么办?这时候就不好解决,只能由人工解决了。但是这种事务概率比较低。
这就是使用MQ来解决分布式事务的相关问题的方案了。这种方案的好处是他是异步的,性能是较高的,但是实时性也是最差的。如果数据的强一致性没那么高,就能够使用这种方式。
分布式服务的接口幂等性如何设计
幕等:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
-
幂等场景
- 用户重复点击(网络波动)
- MQ消息重复
- 应用使用失败或超时重试机制
-
接口幂等
基于RESTfuI API的角度对部分常见类型请求的幕等性特点进行分析
请求方式 说明 GET 查询操作,天然幂等 POST 新增操作,请求一次与请求多次造成的结果不同,不是幂等的 PUT 更新操作,如果是以绝对值更新,则是幂等的。如果是通过增量的方式更新,则不是幂等的 DELETE 删除操作,根据唯一值删除,是幂等的 我们如何去处理新增幂等(更新也一般是绝对值更新,是幂等的)的方式呢?
-
数据库唯一索引
新增
-
token+redis
新增+修改
-
分布式锁
新增+修改
-
-
token+redis
像创建商品、提交订单、转账、支付等操作,我们都可以使用此方案来解决接口幂等。
在第一次请求(打开商品详情页)时,首先我们要获取token(在后端生成一个token),然后存储到redis。在存入到redis时,当前登录的用户是key,这个token是value。同时,我们将此token返回给前端。
在第二次请求(打开付款页提交订单)时,在用户提交订单时,将会携带之前的token,然后访问服务端。服务端需要在redis中查一下此token是否存在。如果存在,直接处理业务,同时删除redis中的token;不存在,返回。
-
分布式锁
在处理业务的时候加锁,最终及时释放锁。注意要快速失败(抢不到锁的线程),还要控制锁的粒度,粒度越小越好。因为加锁的时候,其他线程处于等待或阻塞,性能不够好。
你们项目中使用了什么任务调度
-
xxl-job解决的问题
- 解决集群任务的重复执行问题
- cron表达式定义灵活
- 定时任务失败了,重试和统计
- 任务量大,分片执行
-
xxl-job路由策略
策略 说明 FIRST(第一个) 固定选择第一个机器 LAST(最后一个) 固定选择最后一个机器 ROUND(轮询) 依次选择不同的机器 RANDOM(随机) 随机选择在线的机器 CONSISTENT_HASH(一致性HASH) 每个任务按照Hash算法固定选择某一台机器,所有任务均匀散列在不同机器上 LEAST_FREQUENTLY_USED(最不经常使用) 使用频率最低的机器优先被选举 LEAST_RECENTLY_USED(最近最久未使用) 最久未使用的机器优先被选举 FAILOVER(故障转移) 按顺序进行心跳检测,第一个心跳检测成功的机器被选为执行器并发起调度 BUSYOVER(忙碌转移) 按顺序进行空闲检测,第一个空闲检测成功的机器被选为执行器并发起调度 SHARDING_BROADCAST(分片广播) 广播触发集群中所有机器执行一次任务,系统自动传递分片参数,可根据分片参数开发分片任务
-
xxl-job任务执行失败怎么解决
首先将路由策略设置为【故障转移】,然后设置失败重试次数。
如果上述过程都完成,却还是失败。则可以查看日志或者邮件提醒(前提是得设置了提醒)
邮箱除了在任务调度中心进行配置,还需要在spring中进行配置。
-
如果有大数据量的任务同时都需要执行,该怎么解决?执行器集群部署时,任务路由策略选择分片广播情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务。
在具体的业务中我们可以拿到两个参数:
-
index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号
-
total:总分片数,执行器集群的总机器数量
-