一.Nacos
1.什么是 nacos?
Nacos 的全称是 Dynamic Naming and Configuration Service,Na 为 naming/nameServer 即注册中心,co 为 configuration 即注册中心,service 是指该注册/配置中心都是以服务为核心。nacos 默认端口 8848
- 服务发现与健康监测
- 动态配置服务
- 动态 DNS 服务
- 服务及其元数据管理
Nacos 采用了单一数据源,直接解决了分布式和集群部署中的一致性问题。Nacos 使用的 raft 协议,一致性相对 eureka 较高.
2.nacos 架构图
3.配置中心架构图
4.开源配置中心
微服务配置文件问题:
-
配置文件相对分散。在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理。
-
配置文件无法区分环境。微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难。
-
配置文件无法实时更新。我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一个正在运行的项目来说是非常不友好的。
配置中心解决思路:
-
首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。
-
当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置。
-
当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。
Apollo:
Apollo 是由携程开源的分布式配置中心。特点有很多,比如:配置更新之后可以实时生效,支持灰度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台 API。并且资料也写的很详细。
Disconf:
Disconf 是由百度开源的分布式配置中心。它是基于 Zookeeper 来实现配置变更后实时通知和生效的。
SpringCloud Confifig:
这是 Spring Cloud 中带的配置中心组件。它和 Spring 是无缝集成,使用起来非常方便,并且它的配置存储支持 Git。不过它没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新。
Nacos:
这是 SpingCloud alibaba 技术栈中的一个组件,前面我们已经使用它做过服务注册中心。其实它也集成了服务配置的功能,我们可以直接使用它作为服务配置中心。
5.Config 配置中心
Nacos Config 主要通过命名空间和 dataId 和 group 来唯一确定一条配置.
Nacos Client 从 Nacos Server 端获取数据时,调用的是此接口 ConfigService.getConfig(String dataId, String group, long timeoutMs)。
6.配置与控制台
客户端配置
#端口号
server:
port: 18082
#断电打开
management:
endpoint:
health:
show-details: always
endpoints:
jmx:
exposure:
include: "*"
web:
exposure:
include: "*"
#spring配置
spring:
application:
name: nacos-producer
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: http://120.79.36.53:8848 #服务注册地址
config:
server-addr: http://120.79.36.53:8848 #配置中心地址
file-extension: yaml #文件类型
group: DEV_GROUP #组别
namespace: 64d48a25-ed26-4931-ac47-e9b85ab57e48 #命名空间
7.nacos 三要素
命名空间:
命名空间之间是隔离的,可以用来区分不同的环境,默认是 public组别:
命名空间相同,也可以用组别来区分环境,默认是 DEFAULT_GROUPData Id:
是微服务名称和环境的结合体
spring:
application:
name: nacos-server-consumer
profiles:
active: dev
#Data Id的取值
Data Id:nacos-server-consumer-dev
Data ID:
Data ID 区分环境,虽然简单,但是每个项目要创建多个配置文件,随着项目的增多,都在一个命名空间下回显得很混乱,查找起来也不是很方便,而且不利于做权限控制,不方便管理.
当使用 Nacos Config 后,Profile 的配置就存储到 Data ID 下,即一个 Profile 对应一个 Data ID
Data ID 的拼接格式:
${prefix}-${spring.profiles.active}.${file-extension}
- prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置
- spring.profiles.active 取 spring.profiles.active 的值,即为当前环境对应的 profile
- file-extension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置
Group:
用 Group 区分,问题也是一样的,出现很多的配置文件,不方便管理,Group 默认是 DEFAULT_GROUP,可以自定义组别,这个属性主要区分业务,比如说订单微服务一个组,用户微服务一个组,便于区分和数据管理.
Namespace:
Namespace 区分环境,清晰明了,而且有利于做权限控制,方便管理配置文件
nacos 命名空间默认是 public,可以自定义新建命名空间,主要用在区分环境,不同的环境下面使用不同的命名空间,不同命名空间的服务不能相互调用.
配置的时候不是配置命名空间名称,应该配置命名空间的 id
8.配置动态刷新
nacos config 使用长连接更新配置, 一旦配置有变动后,通知 Provider 的过程非常的迅速, 从速度上秒杀 springcloud 原来的 config 几条街.
@RestController
public class NacosConfigController {
@Value( "${config.appName}" )
private String appName;
@GetMapping("/nacos-config-test2")
public String nacosConfingTest2(){
return(appName);
}
}
共享配置:
- 同一微服务,不同场景(namespace)下共享配置
- 不同微服务之间共享共享配置
高可用:
在 nacos 服务宕机时,暂时使用本地的文件,从 CAP 架构上来说,实现了 AP,高可用架构.在 nacos 服务宕机了后,客户端还能继续访问服务端,保证业务流程不因 nacos 宕机变成完全不可用.
9.nacos 持久化
1.默认数据库
默认持久化方式,是使用 derby 数据库.在 0.7 版本之前,在单机模式时 nacos 使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7 版本增加了支持 mysql 数据源能力,Derby 是 Java 编写的数据库,属于 Apache 的一个开源项目
2.修改配置
修改脚本 application.properties
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://120.79.36.53:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=xxxxxxx
3.执行脚本
mysql-schema.sql
4.新增配置
10.集群部署
Nacos Server 有两种运行模式:
- standalone
- cluster
使用集群的模式部署 nacos,cluster 模式启动,
在客户端配置多个节点信息,也可以用 nginx 做多个节点的负载均衡,在客户端配置 nginx 的访问节点也是可以的
#使用单机部署
sh bin/startup.sh -m standalone
#访问 默认账号和密码为:nacos nacos
http://xxxx:8848/nacos
nginx 代理 nacos 集群
upstream nacos_server {
server 127.0.0.1:8848;
server 127.0.0.1:8847;
server 127.0.0.1:8846;
}
server {
listen 8648;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://nacos_server;
index index.html index.htm;
}
}
nacos 的集群选举算法是自己实现的Raft 算法,主要是对于naming 服务,动态配置服务是没有用到这个 raft 集群心跳和数据更新的功能的;
功能点主要有三点:
- leader 选举:使用 Raft 的 leaderdue 时间,到期后发起选举,投票过半为 leader
- 心跳:leader 定时发送心跳给 follower,心跳还可能打包 datum(可以关闭)信息给 follower 节点;follower 节点收到心跳重置选举 leaderdue 避免发生选举,并且如果发现 datum 数据有变化会更新内存和本地文件缓存,数据变化 term 会增加 100,会删除 dead 的 datum
- 数据更新:每次数据更新的时候,如果请求发送到 follower 节点会转发给 leader 处理数据,由 leader 先更新本地数据,然后分别异步发送给其他 follower,如果超过半数的 follower 更新成功那么数据就更新成功了。
注意:
- datum 指的的对于注册的服务和实例抽象包装
- term 类似于 zookeeper 的 zxid;term 投票时会添加 1,数据更新会增加 100;zk 的是分为前 32 位和后 32 位分别累计选举年代和数据更新递增
11.Nacos 2.X 的优点
Nacos 1.X 的痛点:
Nacos 1.X 心跳多,无效查询多,心跳续约感知变化慢,连接消耗大,资源空耗严重。
- 心跳数量多,导致 TPS 居高不下
- 通过心跳续约感知服务变化,时延长
- UDP 推送不可靠,导致 QPS 居高不下
- 基于 HTTP 短连接模型,TIME_WAIT 状态连接过多
- 配置模块的 30 秒长轮询引起的频繁 GC
HTTP 短连接模型,每次客户端请求都会创建和销毁 TCP 链接,TCP 协议销毁的链接状态是 WAIT_TIME,完全释放还需要一定时间,当 TPS 和 QPS 较高时,服务端和客户端可能有大量的 WAIT_TIME 状态链接,从而会导致 connect time out 错误或者 Cannot assign requested address 的问题。
Nacos 2.X 的优点:
- 客户端不再需要定时发送实例心跳,只需要有一个维持连接可用 keepalive 消息即可。重复 TPS 可以大幅降低。
- TCP 连接断开可以被快速感知到,提升反应速度。
- 长连接的流式推送,比 UDP 更加可靠;nio 的机制具有更高的吞吐量,而且由于可靠推送,可以加长客户端用于对账服务列表的时间,甚至删除相关的请求。重复的无效 QPS 可以大幅降低。
- 长连接避免频繁连接开销,可以大幅缓解 TIME_ WAIT 问题。
- 真实的长连接,解决配置模块 GC 问题。
- 更细粒度的同步内容,减少服务节点间的通信压力。
12.注册中心对比
Spring Cloud Eureka
:
- AP 模型,数据最终一致性
- 客户端注册服务上报所有信息,节点多的情况下,网络,服务端压力过大,且浪费内存
- 客户端更新服务信息通过简单的轮询机制,当服务数量巨大时,服务器压力过大。
- 集群伸缩性不强,服务端集群通过广播式的复制,增加服务器压力
- Eureka2.0 闭源(Spring Cloud 最新版本还是使用的 1.X 版本的 Eureka)
Spring Cloud Zookeeper
:
- CP 模型,ZAB 算法,数据强一致性
- 维护成本较高,客户端,session 状态,网络故障等问题,会导致服务异常
- 集群伸缩性限制,内存,GC 和连接
- 无控制台管理
Spring cloud Consul
:
- 适用于 Service Mesh 架构,使用于 JAVA 生态
- AP 模型,Raft+Gossip 算法,数据最终一致性
- 未经大规模市场验证,无法保证可靠性
- Go 语言编写,内部异常排查困难
Spring Cloud Nacos
:
- 开箱即用,适用于 dubbo,spring cloud
- AP 模型,数据最终一致性
- 注册中心,配置中心二合一,提供控制台管理
- 纯国产,久经双十一考验
13.注册中心原理
服务注册的策略的是每 5 秒向 nacos server 发送一次心跳,心跳带上了服务名,服务 ip,服务端口等信息。同时 nacos server 也会向 client 主动发起健康检查,支持 tcp/http 检查。如果 15 秒内无心跳且健康检查失败则认为实例不健康,如果 30 秒内健康检查失败则剔除实例。
14.配置自动刷新的原理?
LongPollingRunnable:
如果 md5 值不一样,则发送数据变更通知,调用 safeNotifyListener 方法_
所以我们知道了这个 run 方法里面创建了一个 Runnable 方法,并放入线程池中,每隔 29.5s 执行一次,如果无变更,就正常返回,如果有变更(md5 比较不相同),则调用 sendResponse(changedGroups);方法响应客户端
15.注册的原理?
注册的逻辑主要在 NacosNamingService 实现类,registerInstance 是注册实例.具体实现在 NamingHttpClientProxy
public class NacosNamingService implements NamingService {
@Override
public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
throws NacosException {
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setWeight(1.0);
instance.setClusterName(clusterName);
registerInstance(serviceName, groupName, instance);
}
}
//NamingHttpClientProxy#deregisterService,里面有对实例的增删改查
@Override
public void deregisterService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER
.info("[DEREGISTER-SERVICE] {} deregistering service {} with instance: {}", namespaceId, serviceName,
instance);
if (instance.isEphemeral()) {
return;
}
final Map<String, String> params = new HashMap<>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, NamingUtils.getGroupedName(serviceName, groupName));
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put(IP_PARAM, instance.getIp());
params.put(PORT_PARAM, String.valueOf(instance.getPort()));
params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.DELETE);
}
public class ServiceManager {
private static final ServiceManager INSTANCE = new ServiceManager();
private final ConcurrentHashMap<Service, Service> singletonRepository;
private final ConcurrentHashMap<String, Set<Service>> namespaceSingletonMaps;
}
16.防止读写冲突?
在更新实例列表时,会采用 CopyOnWrite 技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题了。
使用 CopyOnWrite 技术,主要是因为在获取实例列表的场景下,属于读多写少的场景,在读的时候不加锁,写的时候加锁,消耗一点性能,但是最大限度的提高了读的效率,也就是常说的空间换时间,这个时间指的是读取实例的时间.
17.Alibaba 和 Netflix 套件
仔细看看各组件的功能描述,Spring Cloud Alibaba 套件和 Spring Cloud Netflix 套件大致的对应关系:
- Nacos = Eureka/Consule + Config + Admin
- Sentinel = Hystrix + Dashboard + Turbine
- Dubbo = Ribbon + Feign
- RocketMQ = RabbitMQ
- SchedulerX = Quartz
- Seata=AT+TCC 分布式事务解决组件
- AliCloud OSS、AliCloud SLS、Alibaba Cloud SMS 这三个应该是独有的
二.Sentinel
1.基本概念
响应时间(RT)
:响应时间是指系统对请求作出响应的时间。往往也需要对每个或每组功能讨论其平均响应时间和最大响应时间。响应时间是一个合理且准确的性能指标。响应时间的绝对值并不能直接反映软件的性能的高低,软件性能的高低实际上取决于用户对该响应时间的接受程度。
吞吐量(Throughput)
:吞吐量是指系统在单位时间内处理请求的数量。对于无并发的应用系统而言,吞吐量与响应时间成严格的反比关系,实际上此时吞吐量就是响应时间的倒数。
并发用户数
:并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量。与吞吐量相比,并发用户数是一个更直观但也更笼统的性能指标。
QPS每秒查询率(Query Per Second)
:每秒查询率 QPS 是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应 fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。
2.容错设计?
-
流控:即流量控制,根据流量、并发线程数、响应时间等指标,把随机到来的流量调整成合适的形状,即流量塑性,保证系统在流量突增情况下的可用性,避免系统被瞬时的流量高峰冲垮,一旦达到阈值则进行拒绝服务、排队等降级操作。
-
熔断:当下游服务发生一定数量的失败后,打开熔断器,后续请求就快速失败。一段时间过后再判断下游服务是否已恢复正常,从而决定是否重置熔断器。
-
降级:当访问量剧增、服务出现异常或者非核心服务影响到核心流程时,暂时牺牲掉一些东西,以保障整个系统的平稳运行。
-
隔离:将系统或资源分隔开,保证系统故障时,能限定传播范围和影响范围,防止滚雪球效应,保证只有出问题的服务不可用,服务间的相互不影响。常见的隔离手段:资源隔离、线程隔离、进程隔离、集群隔离、机房隔离、读写隔离、快慢隔离、动静隔离等。
-
超时:相当多的服务不可用问题,都是客户端超时机制不合理导致的,当服务端发生抖动时,如果超时时间过长,客户端一直处于占用连接等待响应的阶段,耗尽服务端资源,最终导致服务端集群雪崩;如果超时时间设置过短又会造成调用服务未完成而返回,所以一个健康的服务,一定要有超时机制,根据业务场景选择恰当的超时阈值。
-
幂等:当用户多次请求同一事件时,得到的结果永远是同一个
3.Sentinel 简介
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
4.Sentinel 特征
Sentinel.具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
5.Sentinel 分为两个部分
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
控制台功能
功能 | 功能 | 功能 |
---|---|---|
实时监控 | 族点链路 | 流控规则 |
熔断规则 | 热点规则 | 系统规则 |
授权规则 | 集群流控 | 机器列表 |
6.Sentinel 的基本概念
资源:
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则:
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
资源定义 2 种方式
- Sentinel API 定义的代码
- @SentinelResource 注解
7.@SentinelResource
@SentinelResource 注解用于定义资源埋点,但不支持 private 方法。默认情况下,Sentinel 对控制资源的保护处理是直接抛出异常,这样对用户不友好,所以我们需要通过可选的异常处理 blockHandler 和 fallback 配置项处理一下异常信息
@SentinelResource 属性:
- value:资源名称,必需项,不能为空
- blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:1、返回值类型必须与原函数返回值类型一致;2、方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。3、fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:1、返回值类型必须与原函数返回值类型一致;2、方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。3、defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
blockHandler 自定义异常
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "dealHandler_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
log.info("testE 热点参数");
return "------testHotKey";
}
public String dealHandler_testHotKey(String p1, String p2, BlockException exception) {
// 默认 Blocked by Sentinel (flow limiting)
return "-----dealHandler_testHotKey";
}
Sentinel常见异常:
- FlowException 限流异常
- DegradeException 降级熔断异常
- ParamFlowException 热点参数异常
- SystemBlockException 系统异常
- AuthorityException 授权,鉴权异常
8.流控规则设计理念?
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
9.流控规则实现?
FlowRule 流控规则:
同一个资源可以创建多条限流规则。FlowSlot
会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。
- resource:资源名,即限流规则的作用对象,唯一,默认请求路径。
- 针对来源:针对调用者进行限流,填写微服务名,若为 default 则不区分调用来源。
- 阈值类型
- QPS(每秒钟的请求数量):当调用该 resource 的 QPS 达到阈值的时候进行限流。
- 线程数:当调用该 resource 的线程数达到阈值的时候进行限流。
- 单机阈值:阈值
- 流控模式
- 直接:达到限流条件直接限流。
- 关联:当关联的资源达到阈值时,就限流自己。
- 链路:当从入口资源进来的流量达到阈值,就对指定资源进行限流。
- 流控效果:
- 快速失败:直接失败,抛出异常。
- Warm Up:根据 codeFactor(冷加载因子,默认为 3)的值,从阈值/codeFactor,经过设定的预热时长,逐渐到达设置的 QPS 阈值。
- 排队等待:匀速排队,让请求以匀速通过,阈值类型必须设置为 QPS,否则无效。
10.关联和链路的区别
说明
- 关联: A 资源关联 B 资源, 当 B 资源到达阈值时, 限流 A 资源
- 链路: A 资源入口资源为 B 资源, 当 A 资源到达阈值时, 限流 B 资源
其实关联和链路是有本质区别的
- 直接: 单个接口的限流
- 关联: 平级接口的限流
- 高优先级资源触发阈值,对低优先级资源限流。
- 链路: 上下级接口的限流
- 资源阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
举个例子
- 关联
配置: “查询订单” 关联 “下单” 接口, 当"下单"到达阈值, 限流"查询订单"接口 - 链路
配置: “查询订单的 service” 入口资源 “查询订单”, 当"查询订单的 service"到达阈值时, 限流"查询订单”
11.Warm Up(预热)
Warm Up 方式:
即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
主要原理是它会根据 codeFactor(冷加载因子,默认为 3)的值,开始只能接受(阈值/codeFactor)流量,经过设定的预热时长,逐渐到达设置的 QPS 阈值。
例如:我们配置资源/testB,使用阈值类型为 QPS,单机阈值设置为 5,流控效果选择 Warm Up,其它默认,此设置的含义为:开始只能每秒接受 5/codeFactor 个请求,经过设定的预热时长(5 秒),逐渐到达设置的 QPS 阈值 5,效果为:开始访问 http://localhost:8401/testB 时每秒请求别超过 5/3 个才能正常访问,5 秒后可以接受的请求可以达到每秒 5 次。
12.排队等待
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法,阈值类型必须设置为 QPS,否则无效。
例如:我们配置资源/testB,使用阈值类型为 QPS,单机阈值设置为 2,流控效果选择排队等待,超时时间设置为 20000ms(20 秒),其它默认,此设置的含义为:代表一秒匀速的通过 2 个请求,也就是每个请求平均间隔恒定为 1000 / 2 = 500 ms,每一个请求的最长等待时间为 20s。
13.熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。Hystrix 通过线程池隔离的方式,来对依赖(在 Sentinel 的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段:
通过并发线程数进行限制:
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
通过响应时间对资源进行降级:
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
14.熔断规则?
除了流量控制以外,及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,可能会导致请求发生堆积,进而导致级联错误。
Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
DegradeRule 熔断规则
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入 | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
15.熔断策略
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
16.慢调用比例
例如:
如果 1 秒内持续进入大于等于 5 个请求,并且请求响应的时间大于 200ms 时,这个请求即为慢调用,当慢调用的比例大于 1 时会触发降级,直到 5 秒后新的请求的响应时间小于 200ms 时,才结束熔断。
17.异常比例
例如:
如果 1 秒内持续进入大于等于 5 个请求,并且请求中报异常的比例超过 0.2 则触发降级(降级时间持续 5 秒),5 秒后,新的请求若正常返回,才结束熔断。
18.异常数
例如:
如果 1 秒内持续进入大于等于 5 个请求,并且请求异常数超过 5 时,会触发降级(降级时间持续 5 秒),5 秒后,新的请求若正常返回,才结束熔断。
19.热点规则
热点规则(ParamFlowRule):
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
20.热点规则示例
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
return "------testHotKey";
}
说明:
当携带第一个参数(p1)访问/testHotKey 的请求量超过 1 秒 5 个时,进行降级,携带第二个参数访问永远不会触发降级
21.参数例外项
当携带第一个参数(p1)访问/testHotKey 的请求量超过 1 秒 5 个时,进行降级,特殊的当传的参数为 a 时,请求量超过 1 秒 100 才进行降级。
22.授权规则
AuthorityRule:
很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
- resource:资源名,即限流规则的作用对象
- limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB
- strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式 比如我们希望控制对资源 test 的访问设置白名单,只有来源为 appA 和 appB 的请求才可通过,则可以配置如下白名单规则:
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
23.系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU 使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps _ minRt 估算得出。设定参考值一般是 CPU cores _ 2.5。
- CPU 使用率:当单台机器上所有入口流量的 CPU 使用率达到阈值即触发系统保护
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
24.sentinel 持久化
主要分为拉模式和推模式
-
拉模式:持久化到本地文件
-
推模式:持久化到 nacos 或者 mysql
次重启项目后在 Sentinel 中配置的规则都会清空,很是麻烦,我们可以通过持久化的方式解决这个问题。
可以将规则持久化到 nacos 或者 mysql,再最新的 sentinel1.8.6 版本,更新 sentinel 控制台和 nacos 是互通的,修改任何一个位置,另一处会跟着改变,这一点还是比较人性化的,但是启动的时候还是以 nacos 的配置为准.
[
{
"resource": "/flowLimit/persistent",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
},
{
"resource": "/flowLimit/testHotKey",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
25.工作原理
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:
NodeSelectorSlot
: 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;ClusterBuilderSlot
: 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;StatisticSlot
: 则用于记录、统计不同纬度的 runtime 指标监控信息;滑动窗口是其实现;FlowSlot
: 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;AuthoritySlot
: 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;DegradeSlot
: 则通过统计信息以及预设的规则,来做熔断降级;SystemSlot
: 则通过系统的状态,例如 load1 等,来控制总的入口流量;
每个 Slot 执行完业务逻辑处理后,会调用 fireEntry()方法,该方法将会触发下一个节点的 entry 方法,下一个节点又会调用他的 fireEntry,以此类推直到最后一个 Slot,由此就形成了 sentinel 的责任链。
26.插槽 Slot
Sentinel 将 ProcessorSlot
作为 SPI 接口进行扩展(1.7.2 版本以前 SlotChainBuilder
作为 SPI),使得 Slot Chain 具备了扩展的能力。您可以自行加入自定义的 slot 并编排 slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。
sentinel 的工作流程就是围绕着一个个插槽所组成的插槽链来展开的。默认的各个插槽之间的顺序是固定的,因为有的插槽需要依赖其他的插槽计算出来的结果才能进行工作。
sentinel 通过 SlotChainBuilder 作为 SPI 接口,使得 Slot Chain 具备了扩展的能力。我们可以通过实现 SlotsChainBuilder 接口加入自定义的 slot 并自定义编排各个 slot 之间的顺序,从而可以给 sentinel 添加自定义的功能。
@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
}
总结:
sentinel 的限流降级等功能,主要是通过一个 SlotChain 实现的。在链式插槽中,有 7 个核心的 Slot,这些 Slot 各司其职,可以分为以下几种类型:
-
一、进行资源调用路径构造的 NodeSelectorSlot 和 ClusterBuilderSlot
-
二、进行资源的实时状态统计的 StatisticsSlot
-
三、进行系统保护,限流,降级等规则校验的 SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot
后面几个 Slot 依赖于前面几个 Slot 统计的结果。
27.chainMap 设计
public class CtSph implements Sph {
private static final Object[] OBJECTS0 = new Object[0];
//使用的普通的map,在高并发场景下,减少锁带来的性能消耗,因为不是全部必须加锁
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
private static final Object LOCK = new Object();
}
构建责任链:
上面的代码简单来说,就是从 chainMap 里面获取 slot 功能链, 没有的话,就构建一个,这里需要注意一点 Constants.MAX_SLOT_CHAIN_SIZE , chainMap 是限制了大小,最大不能超过 6000, 也就是说,默认不能超过 6000 个资源,如果超过 6000 个资源,则会有资源的限流没办法生效
执行责任链就是执行各种 slot.
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 根据资源获取slot功能链
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 上锁保证仅会初始化一个,双检锁
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit. 最大的资源大小是6000个
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
//构建一个slot引用链 -----
chain = SlotChainProvider.newSlotChain();
// map存储,写时复制,提高读取的效率,容量加1
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
28.常见 node
节点 | 作用 |
---|---|
StatisticNode | 执行具体的资源统计操作 |
DefaultNode | 该节点持有指定上下文中指定资源的统计信息,当在同一个上下文中多次调用 entry 方法时,该节点可能下会创建有一系列的子节点。 另外每个 DefaultNode 中会关联一个 ClusterNode |
ClusterNode | 该节点中保存了资源的总体的运行时统计信息,包括 rt,线程数,qps 等等,相同的资源会全局共享同一个 ClusterNode,不管他属于哪个上下文 |
EntranceNode | 该节点表示一棵调用链树的入口节点,通过他可以获取调用链树中所有的子节点 |
29.滑动窗口实现
//SAMPLE_COUNT是窗口个数,INTERVAL是总时间,intervalInMs/窗口数量= 窗口长度
public class StatisticNode implements Node {
//秒级统计窗口是2个,每个统计时间是500ms
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
//分钟级统计窗口是60个,每个统计时间是1s
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
}
窗口统计的事件:
这是最基本的指标,然后通过这些指标,又可以计算出来比如说最大,最小,平均等等的一些指标。
//窗口统计的事件
public enum MetricEvent {
PASS,
BLOCK,
EXCEPTION,
SUCCESS,
RT,
OCCUPIED_PASS
}
MetricBucket 是窗口里面的统计指标的具体实现,MetricBucket 是由 LongAdder 数组组成的,一个 LongAdder 就是一个 MetricEvent,主要作用是创建各个事件,初始化最小 RT
public class WindowWrap<T> {
/**
* 单个窗口的时间长度(毫秒)
*/
private final long windowLengthInMs;
/**
* 窗口的开始时间戳(毫秒)。
*/
private long windowStart;
/**
* 统计数据。
*/
private T value;
/**
* 判断给定时间是否在当前窗口中
*/
public boolean isTimeInWindow(long timeMillis) {
return windowStart <= timeMillis && timeMillis < windowStart + windowLengthInMs;
}
}
LeapArray
//Leap数组使用滑动窗口算法来计数数据
public abstract class LeapArray<T> {
protected int windowLengthInMs;
protected int sampleCount;
protected int intervalInMs;
private double intervalInSecond;
protected final AtomicReferenceArray<WindowWrap<T>> array;
/**
* 更新锁仅在不推荐使用当前存储桶时使用
*/
private final ReentrantLock updateLock = new ReentrantLock();
/**
*这个构造方法其实就是计算出来这个窗口长度,创建了窗口数组。
*/
public LeapArray(int sampleCount, int intervalInMs) {
AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");
this.windowLengthInMs = intervalInMs / sampleCount;
this.intervalInMs = intervalInMs;
this.intervalInSecond = intervalInMs / 1000.0;
this.sampleCount = sampleCount;
this.array = new AtomicReferenceArray<>(sampleCount);
}
}
30.Sentinel 和 hystrix
Hystrix 的资源模型设计上采用了命令模式,将对外部资源的调用和 fallback 逻辑封装成一个命令对象(HystrixCommand
/ HystrixObservableCommand
),其底层的执行是基于 RxJava 实现的。每个 Command 创建时都要指定 commandKey 和 groupKey(用于区分资源)以及对应的隔离策略(线程池隔离 or 信号量隔离)。线程池隔离模式下需要配置线程池对应的参数(线程池名称、容量、排队超时等),然后 Command 就会在指定的线程池按照指定的容错策略执行;信号量隔离模式下需要配置最大并发数,执行 Command 时 Hystrix 就会限制其并发调用。
Sentinel 的设计则更为简单。相比 Hystrix Command 强依赖隔离规则,Sentinel 的资源定义与规则配置的耦合度更低。Hystrix 的 Command 强依赖于隔离规则配置的原因是隔离规则会直接影响 Command 的执行。在执行的时候 Hystrix 会解析 Command 的隔离规则来创建 RxJava Scheduler 并在其上调度执行,若是线程池模式则 Scheduler 底层的线程池为配置的线程池,若是信号量模式则简单包装成当前线程执行的 Scheduler。而 Sentinel 并不指定执行模型,也不关注应用是如何执行的。Sentinel 的原则非常简单:根据对应资源配置的规则来为资源执行相应的限流/降级/负载保护策略。在 Sentinel 中资源定义和规则配置是分离的。用户先通过 Sentinel API 给对应的业务逻辑定义资源(埋点),然后可以在需要的时候配置规则。埋点方式有两种:
- try-catch 方式(通过
SphU.entry(...)
),用户在 catch 块中执行异常处理 / fallback - if-else 方式(通过
SphO.entry(...)
),当返回 false 时执行异常处理 / fallback
从 0.1.1 版本开始,Sentinel 还支持基于注解的资源定义方式,可以通过注解参数指定异常处理函数和 fallback 函数。
从 0.2.0 版本开始,Sentinel 引入异步调用链路支持,可以方便地统计异步调用资源的数据,维护异步调用链路,同时具备了适配异步框架/库的能力。
Sentinel 提供多样化的规则配置方式。除了直接通过 loadRules
API 将规则注册到内存态之外,用户还可以注册各种外部数据源来提供动态的规则。用户可以根据系统当前的实时情况去动态地变更规则配置,数据源会将变更推送至 Sentinel 并即时生效。
隔离设计
隔离是 Hystrix 的核心功能之一。Hystrix 提供两种隔离策略:线程池隔离(Bulkhead Pattern)和信号量隔离,其中最推荐也是最常用的是线程池隔离。Hystrix 的线程池隔离针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败,并可以提供 fallback 机制。线程池隔离的好处是隔离度比较高,可以针对某个资源的线程池去进行处理而不影响其它资源,但是代价就是线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。
但是,实际情况下,线程池隔离并没有带来非常多的好处。首先就是过多的线程池会非常影响性能。考虑这样一个场景,在 Tomcat 之类的 Servlet 容器使用 Hystrix,本身 Tomcat 自身的线程数目就非常多了(可能到几十或一百多),如果加上 Hystrix 为各个资源创建的线程池,总共线程数目会非常多(几百个线程),这样上下文切换会有非常大的损耗。另外,线程池模式比较彻底的隔离性使得 Hystrix 可以针对不同资源线程池的排队、超时情况分别进行处理,但这其实是超时熔断和流量控制要解决的问题,如果组件具备了超时熔断和流量控制的能力,线程池隔离就显得没有那么必要了。
Hystrix 的信号量隔离限制对某个资源调用的并发数。这样的隔离非常轻量级,仅限制对某个资源调用的并发数,而不是显式地去创建线程池,所以 overhead 比较小,但是效果不错,也支持超时失败。Sentinel 可以通过并发线程数模式的流量控制来提供信号量隔离的功能。并且结合基于响应时间的熔断降级模式,可以在不稳定资源的平均响应时间比较高的时候自动降级,防止过多的慢调用占满并发数,影响整个系统。
31.ContextUtil
ContextUtil 很经典,使用了很多并发编程中的知识
- 先从 ThreadLocal 中尝试获取,获取到则直接返回
- 如果第一步没有获取,尝试从缓存中获取该上下文名称对应的 入口节点
- 判断缓存中入口节点数量是否大于 2000public final static int MAX_CONTEXT_NAME_SIZE = 2000;如果已经大于 2000,返回一个 NULL_CONTEXT
- 以上检查都通过根据上下文名称生成入口节点(entranceNode),期间会进行双关检索确保线程安全
- 加入至全局根节点下,并加入缓存,注意每个 ContextName 对应一个入口节点 entranceNode
- 根据 ContextName 和 entranceNode 初始化上下文对象,并将上下文对象设置到当前线程中
public class ContextUtil {
/**
* Store the context in ThreadLocal for easy access.
*/
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
/**
* Holds all {@link EntranceNode}. Each {@link EntranceNode} is associated with a distinct context name.
*/
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
private static final Context NULL_CONTEXT = new NullContext();
static {
// Cache the entrance node for default context.
initDefaultContext();
}
private static void initDefaultContext() {
String defaultContextName = Constants.CONTEXT_DEFAULT_NAME;
EntranceNode node = new EntranceNode(new StringResourceWrapper(defaultContextName, EntryType.IN), null);
Constants.ROOT.addChild(node);
contextNameNodeMap.put(defaultContextName, node);
}
//SentinelResource注解生效的入口
public static Context enter(String name, String origin) {
if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
throw new ContextNameDefineException(
"The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
}
return trueEnter(name, origin);
}
protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
//xXXXXXXXXXXXXXXXx
}
三.Gateway
1.网关的基本功能?
- 简化客户端的工作。网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务;
- 降低函数间的耦合度。 一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性
- 解放开发人员把精力专注于业务逻辑的实现。由网关统一实现服务路由(灰度与 ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑
2.主流网关的对比?
网关(API Gateway)的设计要素:
- 限流:实现微服务访问流量计算,基于流量计算分析进行限流,可以定义多种限流规则。
- 缓存:数据缓存。
- 日志:日志记录。
- 监控:记录请求响应数据,api耗时分析,性能监控。
- 鉴权:权限身份认证。
- 灰度:线上灰度部署,可以减小风险。
- 路由:路由是API网关很核心的模块功能,此模块实现根据请求,锁定目标微服务并将
请求进行转发。
简单介绍下你的网关实施方案:
- 开发语言:java + groovy,groovy的好处是网关服务不需要重启
就可以动态的添加filter来实 现一些功能; - 微服务基础框架:springboot;
- 网关基础组件:netflix zuul;
- 服务注册中心:consul;
- 权限校验:jwt;
- API监控:prometheus + grafana;
- API统一日志收集:logback + ELK;
- 压力测试:Jmeter;
3.什么是 gateway?
springcloud 全家桶中国有个很重要的组件就是网关,在 1.x 版本中都是采用的 zuul 网关;zuul 是 netfix 开发的一个网关组件,但在 2.x 版本中,zuul 由于更新迭代的速度过慢,于是 springcloud 就自己推出了一个新的网关组件,那就是 gateway。
gateway 是在 spring 生态系统之上构建的 API 网关服务,基于 Spring 5,Spring Boot2 和 Project Reactor 等技术。gateway 旨在提供一种简单而有效的方式来对 API 进行路由,以及提供一些强大的过滤器功能,例如:反向代理、熔断、限流、重试等。
SpringCloud Gateway 是基于WebFlux框架实现的,而 WebFlux 框架底层则使用了高性能的Reactor模式通信框架Netty。
4.gateway 特性?
- 动态路由,能够匹配任何请求属性;
- 可以对路由指定 Predicate(断言)和 Filter(过滤器),且易于编写;
- 集成 Hystrix 的断路器功能;
- 集成 SpringCloud 服务发现功能;
- 请求限流功能;
- 支持路径重写。
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
提前声明:Spring Cloud Gateway 底层使用了高性能的通信框架 Netty。
5.三大核心概念
- Route(路由):路由是构建网关的基本模块,它有 ID,目标 URI,一系列的断言和过滤器组成,如果请求与断言相匹配则进行路由
- Predicate(断言):参考的是 java8 的 java.util.function.Predicate,开发人员可以匹配 HTTP 请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
- Filter(过滤):指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
predicate 就是我们发的匹配条件;而 filter,就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标 uri,就可以实现一个具体的路由了。
6.路由 Route
Route 主要由 路由 id、目标 uri、断言集合和过滤器集合组成,那我们简单看看这些属性到底有什么作用。
-
id:路由标识,要求唯一,名称任意(默认值 uuid,一般不用,需要自定义)
-
uri:请求最终被转发到的目标地址
-
order: 路由优先级,数字越小,优先级越高
-
predicates:断言数组,即判断条件,如果返回值是 boolean,则转发请求到 uri 属性指定的服务中
-
filters:过滤器数组,在请求传递过程中,对请求做一些修改
7.gateway 与 zuul 的区别
在 SpringCloud Finchley 正式版之前,Spring Cloud 推荐的网关是 Netflix 提供的 Zuul:
- Zuul 1.x,是一个基于阻塞 I/O 的 API Gateway
- Zuul1.x 基于 Servet2.5 使用阻塞架构它不支持任何长连接(如 WebSocket) Zuul 的设计模式和 Nginx 较像,每次 I/0 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是 Nginx 用 C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 的性能相对较差。
- Zuul 2.x 理念更先进,想基于 Netty 非阳塞和支持长连接,但 SpringCloud 目前还没有整合。Zuul 2.x 的性能较 Zuul 1.x 有较大提升在性能方面,根据官方提供的基准测试,Spring Cloud Gateway 的 QPS (每秒请求数)是 Zuul1.x 的 1.6 倍。
- Spring Cloud Gateway 建立在 Spring Framework 5 和 Project Reactor 和 Spring Boot 2 之上,使用非阻塞 APl
- Spring Cloud Gateway 还支持 WebSocket,并且与 Spring 紧密集成拥有更好的开发体验
8.zuul 模型的缺点
Springcloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,使用的是传统的 Servlet IO 处理模型
Servlet 的生命周期?
- servlet 由 servlet container 进行生命周期管理.container 启动时构造 servlet 对象并调用 servlet init()进行初始化
- container 运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用 service()
- container 关闭时调用 servlet destory()销毁 servlet;
上述模式的缺点:
servlet 是一人简单的网络 IO 模型,当请求进入 servlet container 时,servet container 就会为其绑定一个线程,在并发不高的场是下这种模型易适用的。但是一旦高并发(化如抽风用 jemeter 压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场展下,不希望为每个 reguest 分配一个线程,只需要 1 个或几个线程就能应对极大并发的请求,这种业务场下 servlet 模型没有优势
所以 Zuul 1.X 是基于 servlet 之上的一个阻塞式处理模型,即 spring 实现了处理所有 request 请求的一个 servlet (DispatcherServlet)并由该 servlet 阻塞式处理。所以 Springcloud Zuul 无法摆脱 servlet 模型的弊端
9.有哪些谓词?
- Path:指定路由,支持*号
- After:在指定时间之后
- Before:在指定时间之前
- Between:在指定时间之间,可以用于抢购时间的区间设定
- Cookie:带 cookie 请求,可以支持正则表达式
- Header:带指定 header,可以支持正则表达式
- Host:带指定 host 请求,可以支持通配符
- Method:指定 method 的请求类型,比如严禁 delete 类型
- Query:带指定查询参数,可以支持正则表达式
- RemoteAddr:来源地址的 list,匹配则通行
- Weight:权重路由
10.过滤器 filter
Gateway 过滤器的生命周期:
-
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
-
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 过滤器从作用范围可分为两种:
- GatewayFilter:应用到单个路由或者一个分组的路由上(需要在配置文件中配置)
- GlobalFilter:应用到所有的路由上(无需配置,全局生效)
11.GatewayFilter
Spring Cloud Gateway 中内置了许多的局部过滤器,如下图:
局部过滤器需要在指定路由配置才能生效,默认是不生效的。
filters:
- AddResponseHeader=X-Response-Foo, Bar
# StripPrefix:去除原始请求路径中的前1级路径,即/gateway
- StripPrefix=1
12.GlobalFilter
全局过滤器应用全部路由上,无需开发者配置,Spring Cloud Gateway 也内置了一些全局过滤器,如下图:
GlobalFilter 的功能其实和 GatewayFilter 是相同的,只是 GlobalFilter 的作用域是所有的路由配置,而不是绑定在指定的路由配置上。多个 GlobalFilter 可以通过 @Order 或者 getOrder() 方法指定执行顺序,order 值越小,执行的优先级越高。
13.order 顺序
注意,由于过滤器有 pre 和 post 两种类型,pre 类型过滤器如果 order 值越小,那么它就应该在 pre 过滤器链的顶层,post 类型过滤器如果 order 值越小,那么它就应该在 post 过滤器链的底层。示意图如下:
14.高级使用
-
熔断降级:
- 直接使用 sentinel,在 sentinel 进行对网关的熔断降级
- 通过 filer 配置,使用 hystrix 进行熔断降级
-
分布式限流:
- 令牌桶原理,redis 和 lua 脚本结合使用
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
这里的配置,使用了两个过滤器:
(1)过滤器 StripPrefix,作用是去掉请求路径的最前面 n 个部分截取掉。
StripPrefix=1 就代表截取路径的个数为 1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成 http://localhost:8888/good/1/view。
(2)过滤器 Hystrix,作用是通过 Hystrix 进行熔断降级
当上游的请求,进入了 Hystrix 熔断降级机制时,就会调用 fallbackUri 配置的降级地址。需要注意的是,还需要单独设置 Hystrix 的 commandKey 的超时时间
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public Response fallbackA() {
Response response = new Response();
response.setCode("100");
response.setMessage("服务暂时不可用");
return response;
}
}
lua 脚本分布式限流
15.自定义断言工厂
新建一个类,继承 AbstractRoutePredicateFactory 类,这个类的泛型是 自定义断言工厂的一个内部类叫做 Config 是一个固定的名字,在 Config 类中定义自定义断言需要的一些属性,并且自定义断言工厂使用 @Component 注解,交给 spring 容器创建.
我们来设定一个场景: 假设我们的应用仅仅让 age 在(min,max)之间的人来访问
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//读取配置文件中的内容并配置给配置类中的属性
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("minAge","maxAge");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
// 获取请求参数中的 age 属性
String age = exchange.getRequest().getQueryParams().getFirst("age");
if(StringUtils.isNotEmpty(age)) {
try {
int a = Integer.parseInt(age);
boolean res = a >= config.minAge && a <= config.maxAge;
return res;
} catch (Exception e) {
System.out.println("输入的参数不是数字格式");
}
}
return false;
};
}
@Validated
public static class Config {
private Integer minAge;
private Integer maxAge;
public Integer getMinAge() {
return minAge;
}
public void setMinAge(Integer minAge) {
this.minAge = minAge;
}
public Integer getMaxage() {
return maxAge;
}
public void setMaxage(Integer maxage) {
this.maxAge = maxage;
}
}
}
16.源码分析
- HandlerMapping:SG 构建的 HandlerMapping 实例是 RoutePredicatehandlerMapping
- WebHandler:构建的 WebHandler 为 FilteringWebHandler,它接收 GlobalFilter 的集合作为参数
- Route:路由构建由 RouteDefinitionRouteLocator 实例来处理,它基于路由配置(即上面的配置文件)来构建 Route 实例
RoutePredicateHandlerMapping 是 HandlerMapping 的一个实例,HandlerMapping 归属于 SpringWebFlux.
断言功能基于 Spring WebFlux 的 HandlerMapping 实现的,是通过断言工厂实现的,如:AfterRoutePredicateFactory、PathRoutePredicateFactory 及 HostRoutePredicateFactory 等,这些工厂均继承至抽象类 AbstractRoutePredicateFactory,而这个抽象类则实现了 RoutePredicateFactory 接口,这是一种抽象化思想,尽量做到解耦合和更好拓展性,而具体的断言判别逻辑则是在各自工厂的 apply 方法中,通过 GatewayPredicate 路由断言接口的 test 方法判别实现。
处理流程
我们知道,Gateway 的工作流程是这样的:客户端请求->A:Gateway Handler Mapping 接收请求参数并做相关断言处理->B:Web Handler Mapping->GlobalFilter->各种自定义 Filter 逻辑->目标服务接口调度,其中 A->B 的流转是通过代理 GatewayFilter 来实现的,进而流转到各种 FilterChain 链。
Spring WebFlux 的 HandlerMapping 负责获取当前请求的各种参数,并下发到各种子处理 HandlerMapping 中,这里与断言直接相关的是 RoutePredicateHandlerMapping,该类继承至抽象类 AbstractHandlerMapping,而这个抽象类又实现了 HandlerMapping 接口。
当前请求到来会调取 RoutePredicateHandlerMapping 类的 getHandlerInternal 方法,进而通过 RouteDefinitionRouteLocator(该类实现了接口 RouteLocator)实现类中的 getRoutes 获取已配置的 predicates,同时在该方法中转换封装好需要的 Route 实体返回,而具体的断言拦截功能通过 Route 中的 AsyncPredicate 的 apply 方法中再调取 Predicate 的 test 方法实现断言功能的,这就是从源码层面分析得到的断言工作流程。
四.Skywalking
1.微服务架构
2.什么是 Skywalking
Skywalking 是一个国产的开源框架,2015 年由吴晟个人开源,2017 年加入 Apache 孵化器,国人开源的产品,主要开发人员来自于华为,2019 年 4 月 17 日 Apache 董事会批准 SkyWalking 成为顶级项目,支持 Java、.Net、NodeJs 等探针,数据存储支持 Mysql、Elasticsearch 等,跟 Pinpoint 一样采用字节码注入的方式实现代码的无侵入,探针采集数据粒度粗,但性能表现优秀,且对云原生支持,目前增长势头强劲,社区活跃。
Skywalking 是分布式系统的应用程序性能监视工具,专为微服务,云原生架构和基于容器(Docker,K8S,Mesos)架构而设计,它是一款优秀的 APM(Application Performance Management)工具,包括了分布式追踪,性能指标分析和服务依赖分析等。
3.Skywalking 架构
SkyWalking 逻辑上分为四部分: 探针(节点数据采集), 平台后端(数据上报及分析), 存储(数据持久化)和用户界面(数据可视化)。
- Skywalking agent 和业务端绑定在一起,负责收集各种监控数据
- Skywalking oapservice 是负责处理监控数据,接受 agent 的数据并存储在数据库中,接受来自 UI 的请求,查询监控数据。
- Skywalking UI 提供给用户,展现各种监控数据和告警。
- 数据持久化
4.采集过程
完整的采集过程:
- 一个 Trace 对应一次完整的调用链路。
- 一个线程内的调用对应一个 TraceSegement。
- 同一个线程内方法每调用一次就生成一个 Span,同时 spanId + 1。
- 跨进程(如服务之间的调用)或跨线程(如异步调用)生成新的 TraceSegement 和 Sapn,并通过指针指向上游链路信息。
Skywalking 的链路追踪数据的采集过程其实是一个生产者-消费者模型。
生产消费全景图
5.功能特性
功能:
- 服务、服务实例、端点(URI)指标分析
- 根本原因分析。在运行时上分析由进程内代理和 ebpf 分析器支持的代码。
- 业务拓扑图分析
- 服务实例和端点(URI)依赖关系分析
- 服务和端点检测速度慢
- 性能优化
- 分布式跟踪和上下文传播
- 数据库访问指标。检测慢速数据库访问语句(包括 SQL 语句)
- 消息队列性能和消耗延迟监视
- 浏览器性能监控
- 基础设施(虚拟机、网络、磁盘等)监控
- 跨指标、跟踪和日志的协作
- 告警
特点:
- 多语言支持,符合技术栈的 Agent 包括 net Core、PHP、NodeJS、Golang、LUA、Rust 和 c++代理,积极开发和维护。用于 C、c++、Golang 和 Rust 的 eBPF 分析器作为附加。
- 为多种开源项目提供了插件,为 Tomcat、 HttpClient、Spring、RabbitMQ、MySQL 等常见基础设施和组件提供了自动探针。
- 微内核 + 插件的架构,模块化,可插拔,存储、集群管理、使用插件集合都可以进行自由选择。
- 优秀的可视化效果。
- 支持告警。
- 轻量高效,无需大数据平台和大量的服务器资源。
- 多种监控手段,可以通过语言探针和 service mesh 获得监控的数据。
6.工作原理?
Skywalking 使用“Agent”对服务进行监控,它可以把系统划分为多个请求,然后把它们彼此关联起来,并将相关信息发送到 Skywalking Collector,Collector 会把它们汇总起来,生成有用的报告和可视化图表,供用户使用。Skywalking 的还有一个 UI 界面也可以直接查看相关数据
它的工作原理很简单,首先将受监控服务组件连接到 SkywalkingCollector,然后收集操作日志,性能数据,应用的状态等信息,并将数据上送到 Skywalking Collector。Skywalking Collector 会把这些数据存储到 Elasticsearch 中,Elasticsearch 是存储搜索引擎,它会把收集的数据分析出来,然后把分析出来的数据发送到 SkywalkingUI 界面,这样用户就可以直接在 Skywalking UI 界面查看这些数据了 skywalking 还允许用户通过 Restful API 进行数据查询,以便用户可以自定义数据并获取最新数据。
要使用 SkyWalking,需要给我们的项目中绑定一个 agent 探针,绑定后,SkyWalking 就会将你项目整体的监控数据反馈给 SkyWalking oapservice,SkyWalking oapservice 就是 SkyWalking 的服务端,oapservice 会将这些监控数据做处理,然后存储到数据库中:SkyWalking 支持的数据库有很多种:ES,MysqL 等等很多都支持;然后 SkyWalking 提供了一个前端的可视化 ui 界面,我们程序员可以通过这个 ui 界面方便的查看到我们项目整体的数据,因为这个前端界面会向 oapservice 发送请求查询数据;
7.持久化方式?
在 application.yml 中配置持久化方式.默认的存储方式就是采用的 h2 数据库,这是一种基于内存的数据库,我们不用它,因为它基于内存,只要一重启 skywalking,这些监控数据就会消失;
- H2 内存数据库
- MySQL 持久化存储
- ShardingSphere 持久化存储
- TiDB 持久化存储
- elasticsearch 持久化存储,最优方案,是企业的首选方案
8.oapservice 的端口
oapservice 自动暴露了两个端口,11800 端口和 12800 端口;
- 11800 端口是 oapservice 用来收集微服务监控数据的 gRPC 端口,
- 12800 是 oapservice 用来接收前端 ui 界面请求的 HTTP 端口;
- 8080 是 UI 所占用的端口
我们也可以通过 config 文件夹下的 application.yaml 来修改这些端口号;
注意:在我们自己的微服务中,要将 oapservice 的端口号告诉给我们的微服务,否则我们自己的微服务是不知道 oapservice 的端口的,就没有传递监控数据给 oapservice 了
9.全链路监控目标要求?
探针的性能消:
APM 组件服务的影响应该做到足够小。服务调用埋点本身会带来性能损耗,这就需要调用跟踪的低损耗,实际中还会通过配置采样率的方式,选择一部分请求去分析请求路径。在一些高度优化过的服务,即使一点点损耗也会很容易察觉到,而且有可能迫使在线服务的部署团队不得不将跟踪系统关停。
代码的侵入性:
即也作为业务组件,应当尽可能少入侵或者无入侵其他业务系统,对于使用方透明,减少开发人员的负担。对于应用的程序员来说,是不需要知道有跟踪系统这回事的。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了,往往由于跟踪系统在应用中植入代码的 bug 或疏忽导致应用出问题,这样才是无法满足对跟踪系统“无所不在的部署”这个需求。
可扩展性:
一个优秀的调用跟踪系统必须支持分布式部署,具备良好的可扩展性。能够支持的组件越多当然越好。或者提供便捷的插件开发 API,对于一些没有监控到的组件,应用开发者也可以自行扩展。
数据的分析:
数据的分析要快 ,分析的维度尽可能多。跟踪系统能提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应。分析的全面,能够避免二次开发。
10.APM 对比
目前市面上开源的 APM 系统主要有 CAT、Zipkin、Pinpoint、SkyWalking,大都是参考 Google 的 Dapper]实现的
- Zipkin 是 Twitter 开源的调用链路分析工具,目前基于 Spingcloud sleuth 得到了广泛的应用,特点是轻量,部署简单。
- Pinpoint 是一个韩国团队开源的产品,运用了字节码增强技术,只需要在启动时添加启动参数即可,对代码无侵入,目前支持 Java 和 PHP 语言,底层采用 HBase 来存储数据,探针收集的数据粒度非常细,但性能损耗大,因其出现的时间较长,完成度也很高,应用的公司较多
- Skywalking 是本土开源的基于字节码注入的调用链路分析以及应用监控分析工具,特点是支持多种插件,UI 功能较强,接入端无代码侵入。
- CAT 是由国内美团点评开源的,基于 Java 语言开发,目前提供 Java、C/C++、Node.js、Python、Go 等语言的客户端,监控数据会全量统计,国内很多公司在用,例如美团点评、携程、拼多多等,CAT 跟下边要介绍的 Zipkin 都需要在应用程序中埋点,对代码侵入性强。
Pinpoint | Zipkin | Jaeger | SkyWalking | |
---|---|---|---|---|
OpenTracing 兼容 | 否 | 是 | 是 | 是 |
客户端支持语言 | java,php | java,c#,go,php 等 | java,c#,go,php 等 | java, .net core,nodejs,php |
存储 | hbase | es,mysql,Cassandra,内存 | es,mysql,Cassandra,内存 | es,h2,msyql,tidb,sharding sphere |
传输协议支持 | thrift | http,MQ | udp,http | gRPC,http |
UI 丰富程度 | 高 | 低 | 中 | 中 |
实现方式 | 字节码注入,无侵入 | 拦截请求,侵入 | 拦截请求,侵入 | 字节码注入,无侵入 |
扩展性 | 低 | 高 | 高 | 中 |
Trace 查询 | 不支持 | 支持 | 支持 | 支持 |
告警支持 | 支持 | 不支持 | 不支持 | 支持 |
JVM 监控 | 支持 | 不支持 | 不支持 | 支持 |
性能损失 | 高 | 中 | 中 | 低 |
五.Seata
1.Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
2.Seata 的组成
seate 是一个典型的分布式事务处理过程,由一个 ID 和三组件模型组成
一个 ID:全局唯一的事务 ID
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
3.Seata 架构图
4.调度生命周期?
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求
5.存储模式
因为 TC 需要进行全局事务和分支事务的记录,所以需要对应的存储,目前,TC 有三种存储模式( store.mode):
file 模式:
适合单机模式,全局事务会话信息在内存中读写,并持久化本地文件 root.data,性能较高;db 模式:
适合集群模式,全局事务会话信息通过 db 共享,相对性能差点:redis 模式:
解决 db 存储的性能问题我们先采用 file 模式,最终我们部署单机 TC Server 如下图所示
6.Seata 的四种模式?
- AT 模式
- TCC 模式
- Saga 模式
- XA 模式
7.AT 模式
前提条件:
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
实现机制:
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
8.AT 模式工作机制
-
seata 首先要求在业务表所在的数据库中创建一个 undo_log 表,用于记录回滚 sql。所谓回滚 sql 就是与所执行 sql 相反的 sql,比如执行了一个 insert 语句,那么对应的 undo log 就是一条 delete 语句。
-
业务表执行了操作后,seata 会将执行的 sql 进行解析,生成回滚 sql 并且存储到 undo_log 表中
-
本地事务提交前,会向 seata 的服务端注册分支,申请对应的业务表中对应数据行的全局锁,这时其他的事务就无法对这条数据进行更新操作。
-
本地事务提交,业务数据的更新和前面生成的 undo log 会一起提交
-
将本地事务的执行结果上报给 seata 服务端。也就是说 seata 服务端会记录多个服务的本地事务。同时这些本地事务因为在 seata 服务端的管控之下,所以使用的事务 ID 和分支 ID 都是一样的。
-
如果某一个本地事务发生报错,那么 seata 服务端就会发起对应分支的回滚请求
-
同时开启一个本地事务,然后通过事务 ID 和分支 ID 去 undo_log 表查询到对应的回滚 sql。并且执行回滚 sql,再将执行结果上报给 seata 服务端
-
如果没有发生报错,seata 服务端也会在对应分支发起请求,然后会异步批量的删除 undo_log 表中的记录
-
总结一句话,为什么能实现分布式事务,就是因为 seata 服务端将这些本地事务都记录下来了,同时也在每个库中记录了 undo log。当有一个报错时,就找到这同一组的所有的本地事务,然后统一利用记录的 undo log 回退数据
undo log 表涉及的字段。其中 xid 就是事务 ID,branch_id 就是分支 ID
9.TCC 模式?
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
AT 模式 基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
//在接口上加@LocalTCC
//在实现类上加@TwoPhaseBusinessAction注解
@TwoPhaseBusinessAction(name ="reducestock", commitMethod ="commitTcc",rollbackMethod ="cancelTcc")
Product reduceStock(BusinessActionContextparameter(paramame = "productid") Integer productid,@BusinessActionContextParameter(paramName = "amount") Integer amount);
10.Saga 模式
Saga模式:
是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Saga模式适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性
11.XA 模式
XA 模式 RM 驱动分支事务的行为包含以下两个阶段:
-
执行阶段:
- 向 TC 注册分支
- XA Start,执行业务 SQL,XA End
- XA prepare,并向 TC 上报 XA 分支的执行情况:成功或失败
-
完成阶段:
- 收到 TC 的分支提交请求,XA Commit
- 收到 TC 的分支回滚请求,XA Rollback
12.Seata 写隔离?
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
13.Seata 读隔离?
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
14.保证事务的隔离性?
因 seata 一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。
- 脏读 select 语句加 for update,代理方法增加@GlobalLock+@Transactional 或@GlobalTransactional
- 脏写 必须使用@GlobalTransactional
注:
如果你查询的业务的接口没有@GlobalTransactional 包裹,也就是这个方法上压根没有分布式事务的需求,这时你可以在方法上标注@GlobalLock+@Transactional 注解,并且在查询语句上加 for update。 如果你查询的接口在事务链路上外层有@GlobalTransactional 注解,那么你查询的语句只要加 for update 就行。设计这个注解的原因是在没有这个注解之前,需要查询分布式事务读已提交的数据,但业务本身不需要分布式事务。 若使用@GlobalTransactional 注解就会增加一些没用的额外的 rpc 开销比如 begin 返回 xid,提交事务等。GlobalLock 简化了 rpc 过程,使其做到更高的性能。
六.Apisix
1.什么是 Apisix?
APISIX 是一个微服务 API 网关,具有高性能、可扩展性等优点。它基于 nginx(openresty)、Lua、etcd 实现功能,借鉴了 Kong 的思路。和传统的 API 网关相比,APISIX 具有较高的性能和较低的资源消耗,并且具有丰富的插件,也方便自己进行插件扩展。
作为一个脱胎于 NGINX 和 OpenResty 的软件,APISIX 人造继承了 NGINX 的性能和 OpenResty 的灵活性,因而,APISIX 的性能在一众 API 网关中都是首屈一指的。
2.Apisix 的优点?
具体来说,像 NGINX + Linux epoll 提供了高性能的网络 IO 基础设施,这些是 C 语言实现的,是动态的。而 OpenResty 则集成了 LuaJIT,它基于 NGINX 提供的生命周期钩子进行扩大,容许用户通过 Lua 代码对 NGINX 进行编程。而 LuaJIT 自身,得益于优良的 JIT 实现,它能够在运行时对代码进行 JIT 编译,当热门路上的内容被编译为机器码后,性能将能够与原生 C 语言相比。
没有复用 NGINX 的 location 来解决路由匹配,而是应用了基数树的形式。能够提供根本安稳的匹配速度。请求路由是通过查询 ETCD 的路由和消费方进行插件匹配,过滤得到可使用的插件后运行插件进行动态上游反向代理。
在 APISIX 中,上述配置操作过程都是齐全动静的,location 能够动静配置,upstream 和 SSL 证书这些全副都能够动静配置。这次要得益于 APISIX 应用了 etcd 作为配置核心,通过 etcd watch 机制实现了动静的更新其配置,从而不须要依赖 reload 和重启。
3.Apisix 架构
4.外围模块
APISIX 在外围模块中提供了很多开箱即用的性能,比方负载平衡、动静上游、灰度公布、服务熔断、身份认证、可观测性、服务发现、限流限速和日志收集等性能。
APISIX 中的很多性能都是通过插件形式进行实现的,目前 APISIX 的插件已靠近 80 个,将来还在继续扩大减少中。提到插件,不得不说 APISIX 提供的插件运行时是一个十分易于开发的插件框架,用户能够轻而易举地应用 Lua 编写本人的插件,来实现特定业务性能。如果切实不具备 Lua 的开发保护能力也能够应用内部 Plugin Runner(APISIX 多语言插件) 或者 WASM 开发插件。
5.基本概念
Route路由:
路由是请求的入口点,它定义了客户端请求与服务之间的匹配规则,路由可以与服务(service)、上游(Upstream)关联,一个服务可以对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。
Upstream上游服务:
包含了已创建的上游服务(即后端服务),可以对上游服务的多个目标节点进行负载均衡和健康检查。
Service服务:
服务由路由中公共的插件配置、上游目标信息组合而成。服务与路由、上游关联,一个服务可对应一组上游节点、可被多条路由绑定。
Consumer消费者:
消费者是路由的消费方,形式包括开发者、最终用户、API 调用等。创建消费者时,需绑定至少一个认证类插件。