源码分析 Sentinel 之 Dubbo 适配原理
在Alibaba Sentinel 限流与熔断初探(技巧篇) 的示例中我选择了 sentinel-demo-apache-dubbo 作为突破点,故本文就从该项目入手,看看 Sentinel 是如何对 Dubbo 做的适配,让项目使用方无感知,只需要引入对应的依即可。sentinel-apache-dubbo-adapter 比较简单,展开如下:上面的代码应该比较简单,在正式进入源码研究之前,我先抛出如下二个问题:1、限流、熔断相关的功能是在 Dubbo 的客户端实现还是服务端实现?为什么?2、如何对 Dubbo 进行功能扩展而无需改动业务代码?Dubbo 提供了 Filter 机制对功能进行无缝扩展,有关 Dubbo Filter 机制,大家可以查阅笔者的源码研究 Dubbo 系列:Dubbo Filter机制概述。接下来我们带着上面的问题1开始本章的研究。1、源码分析 SentinelDubboConsumerFilter@Activate(group = "consumer") // @1
public class SentinelDubboConsumerFilter implements Filter {
public SentinelDubboConsumerFilter() {
RecordLog.info("Sentinel Apache Dubbo consumer filter initialized");
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Entry interfaceEntry = null;
Entry methodEntry = null;
try {
String resourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboConsumerPrefix()); // @2
interfaceEntry = SphU.entry(invoker.getInterface().getName(),
ResourceTypeConstants.COMMON_RPC, EntryType.OUT); // @3
methodEntry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT); // @4
Result result = invoker.invoke(invocation); // @5
if (result.hasException()) { // @6
Throwable e = result.getException();
// Record common exception.
Tracer.traceEntry(e, interfaceEntry);
Tracer.traceEntry(e, methodEntry);
}
return result;
} catch (BlockException e) {
return DubboFallbackRegistry.getConsumerFallback().handle(invoker, invocation, e); // @7
} catch (RpcException e) {
Tracer.traceEntry(e, interfaceEntry);
Tracer.traceEntry(e, methodEntry);
throw e;
} finally {
if (methodEntry != null) { // @8
methodEntry.exit();
}
if (interfaceEntry != null) {
interfaceEntry.exit();
}
}
}
}代码@1:通过 @Activate 注解定义该 Filter 在客户端生效。代码@2:在 Sentinel 中一个非常核心的概念就是资源,即要定义限流的目标,当出现什么异常(匹配用户配置的规则)对什么进行熔断操作,Dubbo 服务中的资源通常是 Dubbo 服务,分为服务接口级或方法级,故该方法返回 Dubbo 的资源名,其主要实现特征如下:如果启用用户定义资源的前缀,默认为 false ,可以通过配置属性:csp.sentinel.dubbo.resource.use.prefix 来定义是否需要启用前缀。如果启用前缀,消费端的默认前缀为 dubbo:consumer:,可以通过配置属性 csp.sentinel.dubbo.resource.consumer.prefix 来自定义消费端的资源前缀。Dubbo 资源的名称表示方法为:interfaceName + ":" + methodName + "(" + "paramTyp1参数列表,多个用 , 隔开" + ")"。代码@3:调用 Sentinel 核心API SphU.entry 进入 Dubbo InterfaceName。从方法的名称我们也能很容易的理解,就是使用 Sentienl API 进入资源名为 Dubbo 接口提供者类全路径限定名,即认为调用该方法,Sentienl 会收集该资源的调用信息,然后Sentinel 根据运行时收集的信息,再配合限流规则,熔断等规则进行计算是否需要限流或熔断。本节我们不打算深入研究 SphU 的核心方法研究,先初步了解该方法:String name 资源的名称。int resourceType 资源的类型,在 Sentinel 中目前定义了 如下五中资源:ResourceTypeConstants.COMMON同样类型。ResourceTypeConstants.COMMON_WEBWEB 类资源。ResourceTypeConstants.COMMON_RPCRPC 类型。ResourceTypeConstants.COMMON_API_GATEWAY接口网关。ResourceTypeConstants.COMMON_DB_SQL数据库 SQL 语句。EntryType type进入资源的方式,主要分为 EntryType.OUT、EntryType.IN,只有 EntryType.IN 方式才能对资源进行阻塞。代码@4:调用 Sentinel 核心API SphU.entry 进入 Dubbo method 级别。代码@5:调用 Dubbo 服务提供者方法。代码@6:如果出现调用异常,可以通过 Sentinel 的 Tracer.traceEntry 跟踪本次调用资源进入的情况,详细 API 将在该系列的后续文章中详细介绍。代码@7:如果是由于触发了限流、熔断等操作,抛出了阻塞异常,可通过 注册 ConsumerFallback 来实现消费者快速失败,将在下文详细介绍。代码@8:SphU.entry 与 资源的 exit 方法需要成对出现,否则会出现统计错误。2、源码分析 SentienlDubboProviderFilters@Activate(group = "provider")
public class SentinelDubboProviderFilter implements Filter {
public SentinelDubboProviderFilter() {
RecordLog.info("Sentinel Apache Dubbo provider filter initialized");
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// Get origin caller.
String application = DubboUtils.getApplication(invocation, "");
Entry interfaceEntry = null;
Entry methodEntry = null;
try {
String resourceName = DubboUtils.getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix()); // @1
String interfaceName = invoker.getInterface().getName();
// Only need to create entrance context at provider side, as context will take effect
// at entrance of invocation chain only (for inbound traffic).
ContextUtil.enter(resourceName, application);
interfaceEntry = SphU.entry(interfaceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN); // @2
methodEntry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_RPC,
EntryType.IN, invocation.getArguments());
Result result = invoker.invoke(invocation);
if (result.hasException()) {
Throwable e = result.getException();
// Record common exception.
Tracer.traceEntry(e, interfaceEntry);
Tracer.traceEntry(e, methodEntry);
}
return result;
} catch (BlockException e) {
return DubboFallbackRegistry.getProviderFallback().handle(invoker, invocation, e); // @3
} catch (RpcException e) {
Tracer.traceEntry(e, interfaceEntry);
Tracer.traceEntry(e, methodEntry);
throw e;
} finally {
if (methodEntry != null) {
methodEntry.exit(1, invocation.getArguments());
}
if (interfaceEntry != null) {
interfaceEntry.exit();
}
ContextUtil.exit();
}
}
}Dubbo 服务提供者与消费端的适配套路差不多,这里就重点阐述一下其不同点。代码@1:如果启用前缀,默认服务提供者的资源会加上前缀:dubbo:provider:,可以通过在配置文件中配置属性 csp.sentinel.dubbo.resource.provider.prefix 改变其默认值。代码@2:服务端调用 SphU.entry 时其进入类型为 EntryType.IN。代码@3:同样可以在 抛出阻塞异常(BlockException) 时指定快速失败回调处理逻辑。3、Sentienl Dubbo FallBack 机制Sentinel Dubbo FallBack 机制比较简单,就是提供一个全局的 FallBack 回调,可以分别为服务提供端,服务消费端指定。只需实现 DubboFallback 接口,其声明如下:然后需要调用 DubboFallbackRegistry 的 setConsumerFallback 和 setProviderFallback 方法分别注册消费端,服务端相关的监听器。通常只需要在启动应用的时候,将其进行注册即可。4、总结本文只是以 Sentienl 对 Dubbo 的适配实现来了解 Sentinel 核心相关的 API,其核心实现就是利用 Dubbo 的 Filter 机制进行无缝的过滤拦截。但本文只是提到 Sentinel 如下核心方法:SphU.entryEntry.exitTracer.traceEntry上述这些方法,将在后面的文章中进行深入探究,即从下一篇文章开始,我们将真正进入 Sentinel 的世界中,让我们一探究竟限流、熔断通常是如何实现的。
Alibaba Sentinel 限流与熔断初探
在学习一个新技术或新框架时,建议先查看其官方文档,以获得对其形成一个整体的认识。1、Sentinel 是什么 ?主要能解决什么问题?按照官方的定义,Sentinel 意为分布式系统的流量防卫兵,主要提供限流、熔断等服务治理相关的功能。服务的动态注册、服务发现是 SOA、微服务架构体系中首先需要解决的基本问题,服务治理是 SOA 领域又一重要课题,而 dubbo 框架只提供了一些基本的服务治理能力,例如限制服务并发调用数、配置合适的业务线程数量等,但熔断相关的功能就涉及的较少。Sentinel 将作为 Dubbo 生态的重要一员,将集中解决服务治理相关的课题,服务限流与熔断又是服务治理首先要解决的课题。那什么是限流与熔断呢?限流:我们通常使用TPS对流量来进行描述,限流就是现在服务被调用的并发TPS,从而对系统进行自我保护。熔断:就是当系统中某一个服务出现性能瓶颈是,对这个服务的调用进行快速失败,避免造成连锁反应,从而影响整个链路的调用。2、限流与熔断的使用场景限流还是比较好理解,例如一个项目在上线之前经过性能测试评估,例如服务在 TPS 达到 1w/s 时系统资源利用率飙升,与此同时响应时间急剧增大,那我们就要控制该服务的调用TPS,超过该 TPS 的流量就需要进行干预,可以采取拒绝、排队等策略,实现流量的削峰填谷。还有一个场景,例如一下开放平台,对接口进行收费,免费用户要控制调用TPS,账户的等级不同,允许调用的TPS也不同,这种情况就非常适合限流。那熔断的使用场景呢?我们首先来看一下如下的分布式架构。例如应用A 部署了3台机器,如果由于某种原因,例如线程池 hold 住,导致发送到它上面的请求会出现超时而报错,由于该进程并未宕机,请求还是会通过负载算法请求出现故障的机器,出现整个1/3的请求出现超时报错,影响整个系统的可用性?也就是其中一台故障会对整个服务质量产生严重的影响,虽然是集群部署,但无法达到高可用性。那如何解决该问题?如果在调用方(API-Center) 对异常进行统计,发现发往某一台机器的错误数或错误率达到设定的值,就在一定的世界间隔内不继续发往该机器,转而发送给集群内正常的节点,这样就实现了高可用,这就是所谓的熔断机制。有了上面的基本认识,接下来会进行一些阅读源码的准备,为后面的源码分析 Sentinel 打下坚实的基础。3、Sentinel 源码结构Sentinel 的核心模块说明如下:sentinel-coreSentinel 核心模块,实现限流、熔断等基本能力。sentinel-dashboardSentinel 可视化控制台,提供基本的管理界面,配置限流、熔断规则等,展示监控数据等。sentinel-adapterSentinel 适配,Sentinel-core 模块提供的是限流等基本API,主要是提供给应用自己去显示调用,对代码有侵入性,故该模块对主流框架进行了适配,目前已适配的模块如下:sentinel-apache-dubbo-adapter对 Apache Dubbo 版本进行适配,这样应用只需引入 sentinel-apache-dubbo-adapter 包即可对 dubbo 服务进行流控与熔断,大家可以思考会利用 Dubbo 的哪个功能特性。sentinel-dubbo-adapter对 Alibaba Dubbo 版本进行适配。sentinel-grpc-adapter对 GRPC 进行适配。sentinel-spring-webflux-adapter对响应式编程框架 webflux 进行适配。sentinel-web-servlet对 servlet 进行适配,例如 Spring MVC。sentinel-zuul-adapter对 zuul 网关进行适配。sentinel-cluster提供集群模式的限流与熔断支持,因为通常一个应用会部署在多台机器上组成应用集群。sentinel-transport网络通讯模块,提供 Sentinel 节点与 sentinel-dashboard 的通讯支持,主要有如下两种实现。sentinel-transport-netty-http基于 Netty 实现的 http 通讯模式。sentinel-transport-simple-http简单的 http 实现方式。sentinel-extensionSentinel 扩展模式。主要提供了如下扩展(高级)功能:sentinel-annotation-aspectj提供基于注解的方式来定义资源等。sentinel-parameter-flow-control提供基于参数的限流(热点限流)。sentinel-datasource-extension限流规则、熔断规则的存储实现,默认是存储在内存中。sentinel-datasource-apollo基于 apollo 配置中心实现限流规则、熔断规则的存储,动态推送生效机制。sentinel-datasource-consul基于 consul 实现限流规则、熔断规则的存储,动态推送生效机制。sentinel-datasource-etcd基于 etcd 实现限流规则、熔断规则的存储,动态推送生效机制。sentinel-datasource-nacos基于 nacos 实现限流规则、熔断规则的存储,动态推送生效机制。sentinel-datasource-redis基于 redis 实现限流规则、熔断规则的存储,动态推送生效机制。sentinel-datasource-spring-cloud-config基于 spring-cloud-config 实现限流规则、熔断规则的存储,动态推送生效机制。sentinel-datasource-zookeeper基于 zookeeper 实现限流规则、熔断规则的存储,动态推送生效机制。4、在 IntelliJ IDEA 中运行 Sentine Demo在 sentinel-demo 模块下提供了很多示例,Seninel 一开始是为 Dubbo 而生的,故我们选取一下 sentinel-demo-apache-dubbo 为本次演示的示例。注意:该版本需要引入的 apache dubbo 版本需要修改为 2.7.2。<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.2</version>
</dependency>Step1:先启动 sentinel-dashboard,启动参数配置如下:sentinel-demo-apache-dubbo 模块如下所示:先启动服务提供者,其配置参数如下:然后启动服务消费者,其配置参数如下:启动后,我们能看到消费者会出现报错,因为触发了限流,我们可以通过控制台查看接入应用的信息,例如输入:http://localhost:8080部分截图如下:可以在控制台动态添加限流、熔断等规则配置,然后接入的客户端将能在不启动应用的情况下生效。默认情况下,sentinel-dashboard 中的规则是存储在内存中,重启后就会丢失,因此 Sentinel 提供了很多种数据源的实现,例如 sentinel-datasource-zookeeper,这部分内容随着该专栏的陆续更新,将会对该机制进行介绍。
《吃透微服务》 - 服务容错之Sentinel
上篇我们已经了解到微服务中重要的组件之一 --- 服务网关Gateway 。我们在取精排糠的同时,不可否认微服务给我们带来的好处。其中承载高并发的好处更是让各大公司趋之若鹜!《吃透微服务》 - 服务网关之Gateway但是想要接收高并发,自然要接收它带来的一系列问题。在微服务架构中,我们将业务拆分成了一个个服务,这样的好处之一便是分担压力,一个服务顶不住了,下一个服务顶上去。但是服务与服务之间可以相互调用,由于网络原因或自身的原因,服务并不能保证百分百可用,也就是各大公司现在追寻的几个9(99.99%,99.999%)可用!如果单个服务出现问题,调用这个服务就会出现网络延迟,此时如果正好有大量的网络涌入,势必会形成任务堆积,导致服务瘫痪!空口无凭,小菜给你找点示例看看:OrderController这里我们通过接口模拟了下单的场景,其中通过线程休眠的方式模拟了网络延迟的场景。接下来我们还需要改一下 tomcat 中并发的线程数applicatio.yamlserver:
tomcat:
max-threads: 10 # 默认的并发数为10当这一切准备就绪好,我们这个时候还需要压测工具 Jmeter 的帮助(不会操作的同学具体看以下使用)首先打开Jmeter软件,选择新建线程组设置请求线程数设置HTTP请求取样器设置请求的接口完成上面步骤就可以点击开始了。不过测试之前我们确保两个API都是可以访问的:然后开始压力测试,当大量请求发送到创建订单的接口时,我们这时候通过网页访问 detail API 发现请求一直在阻塞,过一会才联通!这无疑是一个开发炸弹,而这便是高并发带来的问题。看到这里,恭喜你成功见证了一场服务雪崩的问题。那不妨带着这份兴趣继续往下看,会给你惊喜的。Sentinel一、服务雪崩我们开头直接用服务雪崩勾引你,不知道你心动了没有,如果不想你的项目在线上环境面临同样的问题,赶紧为项目搭线起来,不经意的举动往往能让你升职加薪!在分布式系统中,由于网络原因或自身的原因。服务一般无法保证 100% 可用,如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。而由于服务与服务之间的依赖性,故障会进行传播,相互影响之下,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应”!最开始的时候,服务A~C 三个服务其乐融融的相互调用,响应也很快,为主人工作也很卖力好景不长,主人火了,并发量上来了。可能因为服务C还不够健壮的原因,服务C在某一天宕机了,但是服务B还是依赖服务C,不停的进行服务调用这个时候可能大家都还没意识到问题的严重性,只是怀疑可能请求太多了,导致服务器变卡了。请求继续发送,服务A这个时候也未知问题,一边觉得奇怪服务B是不是偷懒了,怎么还不把响应返回给它,一边继续发送请求。但是这个时候请求都在服务B堆积着,终于有一天服务B也累出问题了这个时候人们开始抱怨服务A了,却不知道服务A底层原来还依赖服务B和服务C,而这时服务B和服务C都挂了。服务A这时才想通为什么服务B之前那么久没返回响应,原来服务B也依赖服务C啊!但是这个时候已经晚了,请求不断接收,不断堆积,下场都是惊人的相似,也走向了宕机的结果。不过有一点不同的是,服务A宕机后需要承载了用户各种的骂声~可悲的故事警惕了我们,微服务架构之间并没有那么可靠。有时候真的是说挂就挂,原因各种各样,不合理的容量设计,高并发情况下某个方法响应变慢,亦或是某台机器的资源耗尽。我们如果不采取措施,只能坐以待毙,不断的在重启服务器中循环。但是我们可能无法杜绝雪崩的源头,但是如果我们在问题发生后做好容错的准备,保证下一个服务发生问题,不会影响到其他服务的正常运行,各个服务自扫家门雪,做到独立,雪落而不雪崩!二、容错方案想要防止雪崩的扩散,就要做好服务的容错,容错说白了就是保护自己不被其他队友坑,带进送人头的行列!那我们有哪些容错的思路呢?1)隔离方案它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相互独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不涉及其他模块,不影响整体的系统服务。常见的隔离方式有:线程隔离 和信号量隔离:2)超时方案在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间下游服务还没响应,那么就断开连接,释放掉线程3)限流方案限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到需要限制的阈值,就需要限制流量并采用少量措施完成限制流量的目的限流策略有很多,后期也会考虑出一篇专门将如何进行限流4)熔断方案在互联网系统中,当下游服务因访问压力过大而相应变慢或失败的时候,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断其中熔断有分为三种状态:熔断关闭状态(Closed)服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制熔断开启状态(Open)后续对该服务接口的调用不再经过网络,直接执行本地的 fallback 方法半熔断状态(Half-Open)尝试恢复服务调用,允许有限的流量调用该服务,并监控成功率。如果成功率达到预期,则说明服务已经恢复,进入熔断关闭状态;如果成功率依然很低,则重新进入熔断关闭状态5)降级方案降级其实就是为服务提供一个 B计划,一旦服务无法正常,就启用 B计划方案其实有很多,但是很难说明那种方案是最好的。在开发者的世界中,没有最好,只有最适合。那如果自己写一个容错方案往往是比较容易出错的(功力高深者除外),那么为了解决这个问题,我们不妨用第三方已经为我实现好的组件!三、容错组件1)HystrixHystrix 是 Netflix 开源的一个延迟和容错库,用于隔离访问远程系统,服务或者第三方库,防止级联失败,从而提升系统的可用性和容错性2)Resilience4JResilience4J是一款非常轻量,简单,并且文档非常清晰,丰富的熔断工具,这是 Hystrix 官方推荐的替代品。它支持 SpringBoot 1.x/2.x 版本,而且监控也支持和 prometheus 等多款主流产品进行整合3)SentinelSentinel 是阿里开源的一款断路器的实现,在阿里巴巴内部也已经大规模采用,可以说是非常稳定不同之处容错组件其实有很多,但各有风骚,下面分别说明这这三种组件的不同之处,如何抉择,仔细斟酌!功能SentinelHystrixresilience4j隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离熔断降级策略基于响应时间,异常比率,异常数基于异常比率基于异常比率,响应时间实时统计实现时间滑动窗口(LeapArray)时间滑动窗口(基于Rxjava)Ring Bit Buffer动态规则配置支持多种数据源支持多种数据源有限支持扩展性多个扩展点插件的形式接口的形式基于注解的支持支持支持支持限流基于 QPS,支持基于调用关系的限流有限的支持Rate LImiter流量整形支持预热模式,匀速器模式,预热排队模式不支持简单的 Rate Limiter模式系统自适应保护支持不支持不支持控制台提供即用的控制台,可配置规则,查看秒级监控,机器发现等简单的监控查看不提供控制台,可对接其他监控系统之前有说过,一个新秀想让大伙接受并广泛使用,肯定得具备良好的特性才能冲出老牌的包围圈。那么 Sentinel 作为一个微服务中的新秀,只有具备让人满意的功能,才能被大伙接受。因此,这篇的主角便是 Sentinel ,不妨深入了解一番!四、认识Sentinel学会用一个组件之前,我们先需要知道这个组件是什么。1)什么是SentinelSentinel(分布式系统的流量防卫兵)是阿里开源的一套用于 服务容错 的综合性解决方案。它以流量为切入点,从 流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。2)特性丰富的应用场景:Sentinel 承接了阿里巴巴近10年的双十一大促的流量的核心场景。在秒杀、消息削峰填谷,集群流量控制、实时熔断下游不可用应用等场景游刃有余完备的实时监控: Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器的数据,甚至500台以下规模的集群的汇总情况广泛的开源生态: Sentinel 提供开箱即用的与其他开源框架整合模块,只需要引入相关的依赖进行简单的配置即可快速接入完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展接口。可以通过扩展接口来快速定制逻辑。例如定制规则管理,适配动态数据源等3)组成部分核心库(Java客户端):不依赖任何框架/库,能够运行于所有的Java运行环境,同时对 Dubbo和SpringCloud 有很好的支持控制台(Dashboard):基于SpringBoot开发, 打包后可以直接运行,不需要额外的 Tomcat 等应用容器五、上手 Sentinel既然 Sentinel 有两个组成部分,我们分别介绍1) 核心库使用最关键的一步便是引入依赖<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>然后编写一个测试控制器@RestController
@RequestMapping("order")
public class OrderController {
private final static Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
@GetMapping("/create/{id:.+}")
public String createOrder(@PathVariable String id) {
LOGGER.info("准备下单ID为 [{}] 的商品", id);
LOGGER.info("成功下单了ID为 [{}] 的商品", id);
return "success";
}
@GetMapping("/{id:.+}")
public String detail(@PathVariable String id) {
return StringUtils.join("获取到了ID为", id, "的商品");
}
}2)控制台使用Sentinel 具备完善的控制台, 其实就抓住了国人开发的命点。很多人看到有控制台的使用,毫不犹豫的选择了它!首先我们需要下载控制台的 Jar 包启动运行,下载地址下载结束进入到下载目录中通过以下命令启动,然后访问 localhost:8080,即可看到页面java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar登录控制台(sentinel/sentinel)到这里我们就成功进入 Sentinel的控制台页面了,是不是上手十分简单。但这里控制台还是空荡荡的,那是因为我们项目还没进行控制台的相关配置。我们回到 store-order 订单服务中,在 application.yaml 中进行配置:sertinel.transport.port为随意端口号,用来跟控制台交流的端口;sentinel.transport.dashboard 为控制台的访问地址配置完成后,我们便可以启动 store-order 服务,此时再看控制台可以发现已经有了 store-order 这个服务了可能有些小伙伴会觉得奇怪,上面说到用来跟控制台交流的端口是干嘛用的?有问题,便有进步!这里我们可以顺带了解一下控制台的使用原理当 Sentinel应用启动后,我们需要将我们的微服务程序注册到控制台上,也就是在配置文件中指定控制台的地址,这个是肯定的。但是所谓用来跟控制台交流的端口,也就是我们每个服务都会通过这个端口跟控制台传递数据,控制台也可以通过此端口调用微服务中的监控程序来获取微服务的各种信息。因此这个端口必不可少,而且每个服务都需要具备独立的端口号。3)基本概念资源所谓的资源就是 Sentinel 要保护的东西。资源是 Sentinel 的关键概念,它可以使 Java 应用程序中的任何内容,可以是一个服务,也可以是一个方法,甚至是一段代码。规则所谓的规则就是用来定义如何进行保护资源的,它是作用于资源之上的,定义以什么样的方式保护资源,主要包括了流量控制规则,熔断降级规则以及系统保护规则我们来看个简单的例子,我们设置 order/{id}这个API 的 QPS 为1当我们不断刷新页面就会发现,已经进行了流控在这里面 order/{id}指的就是 资源,我们设置的QPS阈值就是 规则4)重要功能学会用 Sentinel 之前,我们需要清楚 Sentinel 能为我们干点什么(1)流量控制流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制,Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。(2)熔断降级当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或者异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其他资源而导致级联故障Sentinel 采用了两种手段进行解决通过并发线程数进行限制Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其他资源的影响。当某个资源出现不稳定的情况时,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当 线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会拒绝。堆积的线程完成任务后才会开始继续接受请求通过响应时间对资源进行降级除了对并发线程数进行控制之外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才会重新恢复这里提一嘴和 Hystrix 的区别两者的原则实际上都是一致的。都是当一个资源出现问题时,让其快速失败,不会波及到其他资源服务。但是在限制的实现上是不一样的Hystrix 采用的是线程池隔离方式,优点是做到了资源之间的隔离,缺点是增加了线程上下文切换的成本Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制的个人认为 Sentinel 处理限制的方式更好一些(3)系统负载保护Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其他机器上去。如果这个时候其他的机器也处在一个崩溃的边缘状态,Sentinel 提供了对应的保护机制,让系统的入口流量和负载达到一个平衡,保证系统在能力方位之内处理最多的请求。(4)流控规则流控规则,在我们上面说明 Sentinel 的基本概念时简单演示了一下。流量控制就是用来监控应用流量的 QPS(每秒查询率)或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。简单配置簇点链路 ---> 选择对应资源 ---> 添加流控资源名:唯一名称,默认就是请求路径,支持自定义针对来源: 指定对哪个微服务进行限流,默认为 default(不区分来源,全部限制)阈值类型/单机阈值:QPS (每秒请求数):当调用该接口的QPS达到阈值的时候进行限流线程数:当调用该接口的线程数达到阈值的时候进行限流是否集群: 这里暂时不演示集群高级配置我们点开 高级选项 可以看到多出了两个额外功能其中 流控模式 分为 三种直接(默认):接口达到限流条件时,开启限流关联:当关联的资源达到限流条件是,开启限流(适合做应用让步)链路: 当从某个接口过来的资源达到限流条件时,开启限流1、关联流控直接流控 的方式我们在上面已经演示过了,我们这里直接说 关联流控 如何使用使用方式也很简单,只要添加相关联的资源即可。只要关联资源 /order/create/{id}的 QPS 每秒超过 1。那么 /order/{id} 就会触发流控。这就是 你冲动,我买单设置完后,我们需要请我们的老帮手 Jmeter 帮忙测试一下:这个时候 /order/create/{id} 的QPS已经远远超过1了,然后我们再试着访问 /order/{id},发现已经被限流了!2、链路流控链路流控模式指的是:当从某个接口过来的资源达到限流条件的时候,开启限流。它的功能有点类似于针对来源配置项,区别在于: 针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细该模式使用麻烦一些,我们需要改造下代码:OrderServiceOrderControllerapplication.yaml然后自定义 Sentinel 上下文过滤类 FilterContextConfig:接下来我们在 Sentinel 控制台流控中添加配置:然后我们看测试结果,发现以 /order/_datail02为入口访问,会进行流控,而/order/_datail01访问便不会进行流控因此我们清楚了链路模式的入口资源是针对方法接口的(5)降级规则降级规则指的就是当满足什么条件的时候,对服务进行降级。Sentinel 提供了三个衡量条件:慢调用比例当资源的平均相应时间超过阈值(单位 ms)之后,资源进入准降级的状态。如果接下来1s内持续进入5个请求,它们的RT都持续超过这个阈值,那么在接下来的时间窗口(单位 s)之内,就会对这个方法进行降级。异常比例当资源的每秒异常总数/占通过量的比率超过阈值之后,资源就会进入降低状态,即在接下的时间窗口(单位 s)之内,对这个方法的调用都会自动的返回。异常比率的赋值范围为 [0.0, 1.0]异常数当资源近1分钟的异常数目超过阈值之后就会直接进行降级。但是这里需要注意的是,由于统计时间窗口是分钟级别的,若时间窗口小于60s,则结束熔断状态后仍可能再进入熔断状态(6)热点规则热点参数流控规则是一种更加细粒度的流控规则,它允许将规则具体到参数上。这里我们可以在代码里面具体看下怎么使用@SentinelResource("order") // 不添加该注解标识, 热点规则不生效
@GetMapping("/_datail03")
public String detail03(String arg1, String arg2) {
return StringUtils.join(arg1, arg2);
}该API接收两个参数arg1和arg2,这个时候我们对这个资源添加参数流控弄完上面配置后,我们就可以在浏览器进行测试了当参数为第二个的时候,无论一秒刷新几次都不会触发流控当参数为第一个的时候,只要QPS超过了1,就会触发流控这个配置也有高级选项,可以更细颗粒的对参数进行限制,这里就不再演示了。(7)系统规则系统保护规则是从应用级别的入口流量进行控制,从单台机器总体的 Load、RT、线程数、入口QPS、CPU 使用率五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性Load: 仅对 Linux/Unix 有效。当系统的 load 超过阈值时,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量是由系统的 maxQPS * minRT 计算而出,设定的参考值可以参考 CPU 核数 * 2.5RT: 当单台机器上所有入口流量的平均 RT 达到阈值就会触发系统保护,单位是毫秒线程数: 当单台机器上所有入口流量的并发线程数达到阈值是就会触发保护入口QPS: 当单台机器上所有入口流量的QPS达到阈值就会触发系统保护CPU使用率: 当单台机器上所有入口流量的CPU使用率达到阈值就会触发系统保护(8)授权规则在某些场景下,我们需要根据调用来源来判断该次请求是否允许放行,这个时候我们可以使用Sentinel的来源访问控制的功能。来源访问控制根据资源的请求来源判断资源是否能够通过。白名单: 只有请求来源位于白名单内才能通过黑名单: 请求来源位于黑名单时不予通过,其余的则放行通过那么问题来了,流控应用是啥玩意?要用这个流控应用,我们还需要借助 Sentinel 中的 RequestOriginParser 接口来处理来源。只要 Sentinel 保护的接口资源被访问,Sentinel 就会调用 RequestOriginParser 的实现类去解析访问源CustomRequestOriginParserpublic class CustomRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
return httpServletRequest.getParameter("api");
}
}然后我们添加授权规则该规则的作用是,只有当请求URL中带有参数 api=detail03 才能访问成功,否则失败。以下便是测试结果六、扩展 Sentinel1)@SentinelResource这个注解我们上面已经用过,不知道小伙伴们有没有注意到,上面避开没讲就是为了在这详细的介绍下!该注解的作用就是用来定义资源点。当我们定义了资源点之后,就可以通过 Sentinel 控制台来设置限流和降级策略来对资源点进行保护。同时还可以通过该注解来指定出现异常时候的处理策略。我们点进注解可以看到该注解中存在许多属性属性作用value资源点名称blockHandle处理BlockException的函数名称,函数要求:1. 必须是 public2. 返回类型参数与原方法要一致3. 默认需和原方法在同一个类中。如果希望使用其他类的函数,可以配置 blockHandlerClass,并制定blockHandlerClass 里面的方法blackHandlerClass存放 blockHandler 的类,对应的处理函数必须用 static 修饰fallback用于在抛出异常时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore 中排除的异常),函数要求:1. 返回类型与原方法一致2. 参数类型需和原方法匹配3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass,并指定对应的方法fallbackClass存放 fallback 的类,对应的处理函数必须用 static 修饰defaultFallback用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:1. 返回类型与原方法一致2. 方法参数列表为空,或者有一个 Throwable 类型的参数。3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法。exceptionsToIgnore指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。exceptionsToTrace需要trace的异常我们这里简单使用演示一下将限流和降级方法定义在原方法同一个类中限流和降级方法定义不在原方法同一个类中然后我们做个简单的流控设置:访问结果:这种提示的方式显然更加友好!2)Sentinel 规则持久化已经上手尝试的同学可能会发现一个问题,当我们的项目重启,或者 Sentinel 控制台重启都会导致配置被清空了!这是因为这些规则默认是存放在内存中,这可是很大的问题!因此规则持久化是一个必不可少的工作!当然在 Sentinel 也已经很好的支持了这项功能,处理逻辑如下:实话说配置类代码有点长,这里直接贴代码了,有需要的小伙伴可以拷过去直接用!public class FilePersistence implements InitFunc {
@Override
public void init() throws Exception {
String ruleDir = new File("").getCanonicalPath() + "/sentinel-rules";
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则sentinel
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new
FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new
FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new
FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new
FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new
FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new
FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new
FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new
FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new
FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new
FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private final Converter<String, List<FlowRule>> flowRuleListParser = source ->
JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private final Converter<String, List<DegradeRule>> degradeRuleListParser = source
-> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private final Converter<String, List<SystemRule>> systemRuleListParser = source ->
JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private final Converter<String, List<AuthorityRule>> authorityRuleListParser =
source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private final Converter<String, List<ParamFlowRule>> paramFlowRuleListParser =
source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}然后在 resources 下创建配置目录 META-INF/services ,然后添加文件com.alibaba.csp.sentinel.init.InitFunc在文件中添加配置类的全路径这样子我们启动项目的时候就会生成 Sentinel 的配置文件了当我们在控制台中添加一条流控规则后,对应的 json 文件就会有对应的配置到这里我们就完成了 Sentinel 的持久化功能,到这里我们也完成了对 SpringCloud 中Sentinel 的介绍!
不服不行啊!大牛确实把SpringCloud集成Dubbo给一次性讲透了
Spring Cloud集成Dubbo目前Dubbo在国内还是有较多公司在使用的,一方面是因为Dubbo作为阿里巴巴开源的一个SOA服务治理解决方案,在国内发展较早,有比较好的先发优势;另一方面是因为在国内很多工程师对Dubbo框架都比较熟悉,有比较完善的文档介绍和实例;还有,Dubbo框架的性能优势和基于SPI的扩展机制也是Dubbo的优势所在。然而,现在很多人也拿Dubbo与Spring Cloud做比较,其实Dubbo本质上是一个RPC框架,实现了SOA架构下的微服务治理,而SpringCloud下有众多子项目,分别覆盖了微服务开发的各个方面,所以在一定程度上讲,Dubbo可以算是Spring Cloud的子集。在Spring Cloud构建的微服务系统中,大多数开发者都使用官方提供的Feign组件来进行内部服务通信,这种声明式的HTTP客户端使用起来非常简洁、方便、优雅。但是在使用Feign消费服务的时候,相比Dubbo这种RPC框架而言,性能较低。所以基于Dubbo RPC方式的服务集成的交互方式也是Spring Cloud体系的一个重要补充。提供Dubbo服务下面通过一个简单的示例演示如何将Dubbo接入Spring Cloud。我们假设存在一个Dubbo RPC API,由服务提供者为服务消费者暴露接口:首先,添加依赖:然后,在application.yml中添加Dubbo的相关配置信息,示例配置如下:接下来,在SpringBoot应用上添加@EnableDubboConfiguration , 表 示 要 开 启 Dubbo 功 能 ( DubboProvider服务可以使用或者不使用Web容器)。编 写 你 的 Dubbo 服 务 , 只 需 要 在 要 发 布 的 服 务 上 添 加@Service(importcom.alibaba.dubbo.config.annotation.Service ) 注 解 , 其 中interfaceClass属性表示要发布服务的接口声明。启动你的Spring Boot应用,观察控制台,你可以看到Dubbo启动的相关信息。消费Dubbo服务首先,添加依赖:其次,在application.properties中添加Dubbo的相关配置信息,示例如下:然后,开启@EnableDubboConfiguration:最后,通过@Reference注入需要使用的interface:Spring Boot与Dubbo集成上面的示例适用于新建项目,可以很方便地将Dubbo集成到SpringBoot应用,相比传统的Dubbo基于XML的配置方式,Spring Boot遵循“约定优于配置”理念,只需要加入几行注解就可以完成工作,而对于已经使用传统方式而非Spring Boot方式接入Dubbo框架实现的系统,如何通过增加一些代码就可以将Dubbo服务纳入Spring Cloud的体系是另外一个重要的课题。● 思路一:将Dubbo服务的对外接口暴露为REST API对于Dubbo服务提供者来说,可以通过@RestController封装服务端代码,对外暴露REST API。使用时,我们只需要在调用端的Service中注入InvokeRemoteService就可以像调用本地方法一样进行远程调用:对于Dubbo服务的消费者,你可以借助Spring Cloud中的Feign作为HTTP REST的调用接口,对于Dubbo服务,你可以向原来对外提供的Service interface类加入@FeignClient注解,支持外部调用,将对外暴露接口加上@RequestMapping或者@RestController注解,并且把接口改成REST风格的,代码如下:上面的代码中我们声明了一个HTTP“模板”,这个“模板”有一个方法声明findByGroupId,可以通过注解定义这个方法需要发起的HTTP请求信息(注解与Spring MVC完全相同)。● 思路二:将Spring Cloud服务Dubbo化这一改造的思路是替换Spring Cloud的Feign的底层调用协议,将原本使用HTTP Client的处理请求转交给Dubbo RPC来处理,同时将原本对外提供的REST API转换为Dubbo的服务,可以参考GitHub上的Dubbo开源项目(dubbo-spring-boot-project)。首先,加入下面的Maven依赖:然后,实现RPC接口定义:服务端可以支持多协议发布服务:接着,我们完成对消费端的实现:在application.properties中添加Dubbo的版本信息和客户端超时信息,向启动类添加@Enable-DubboConfiguration注解,这里我们配置的这些参数会在项目启动时被加载到DubboProperties类中。最后,实现Dubbo自动化配置:上 面 我 们 实 现 了 提 供 Dubbo 的 @Service 注 解 服 务 。 在DubboAutoConfiguration配置类中启动Bean,当配置文件中的前缀以“dubbo”开始时,会注入相关配置并完成初始化,然后获取所有加了@Service注解的类,使用反射生成代理类。当我们使用HTTP请求这些由@Service注解的类的方法时,它会将HTTP请求转换成Dubbo请求,调用这个代理类将调用结果返回。
看大牛是如何一次性把RPC远程过程调用,Dubbo架构进阶给讲清的
Dubbo架构进阶Dubbo架构主要包含四个角色:消费者、提供者、注册中心和监控系统,如下图所示。具体的交互流程是:消费者(Consumer)通过注册中心获取提供者(Provider)节点后,通过Dubbo的客户端SDK与Provider建立连接,并发起调用。Provider通过Dubbo的服务端SDK接收Consumer的请求,处理后再把结果返回给Consumer。对于采用Dubbo进行RPC调用的解决方案,消费者和提供者都需要引入Dubbo的SDK来完成远程调用。因为Dubbo本身是采用Java实现的,所以要求服务消费者和服务提供者也都必须采用Java实现。不过开源社区已经开始使用对核心扩展点进行TCK(Technology CompatibilityKit)提升框架的兼容性。它为用户增加一种扩展实现,只需通过TCK,即可确保与框架的其他部分兼容运行,可以有效提高健壮性,也方便第三方接入。下面是Dubbo的官方详细架构。左边部分是服务消费者使用的接口,右边部分是服务提供者使用的接口,位于中轴线上的为双方都用到的接口。从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中Service 和 Config 层 为 API , 其 他 各 层 均 为 SPI ( ServiceProvider Interface)。浅色小块为扩展接口,深色小块为实现类,图中只显示用于关联各层的实现类。深色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承(读者可到官网查看彩色图片),可以把子类看作父类的同一个节点,线上的文字为调用的方法。Dubbo服务调用过程Dubbo服务调用过程比较复杂,包含众多步骤,比如发送请求、编解码、服务降级、过滤器链处理、序列化、线程派发及响应请求等。下面我们重点分析请求的发送与接收、编解码、线程派发及响应的发送与接收等过程。Dubbo的服务调用过程如下图所示。首先服务消费者通过代理对象Proxy发起远程调用,接着通过网络客户端Client将编码后的请求发送给服务提供者的网络层,也就是Server。Server在收到请求后,首先要做的事情是对数据包进行解码。然后将解码后的请求发送至分发器Dispatcher,再由分发器将请求派发到指定的线程池上,最后由线程池调用具体的服务。这就是一个远程调用请求的发送与接收过程。服务消费者发送请求Dubbo支持同步和异步两种调用方式,其中异步调用还可细分为“有返回值”的异步调用和“无返回值”的异步调用。所谓“无返回值”的异步调用是指服务消费者只管调用,但不关心调用结果,此时Dubbo会直接返回一个空的RpcResult。若要使用异步特性,需要服务消费者手动进行配置。默认情况下,Dubbo使用同步调用方式。服务调用的线程栈快照如下图所示。服务提供者接收请求默认情况下,Dubbo使用Netty作为底层的通信框架。Netty首先会通过解码器对数据进行解码,并将解码后的数据传递给下一个处理器的指定方法。解 码 器 将 数 据 包 解 析 成 Request 对 象 后 , NettyHandler 的messageReceived方法紧接着会收到这个对象,并将这个对象继续向下传 递 。 其 间 该 对 象 会 被 依 次 传 递 给 NettyServer 、MultiMessageHandler、HeartbeatHandler以及AllChannelHandler处理。最后由AllChannelHandler将该对象封装到Runnable实现类对象中,并将Runnable放入线程池中执行后续的调用逻辑,调用栈如下图所示。Dispatcher就是线程派发器。需要说明的是,Dispatcher真实的职 责 是 创 建 具 有 线 程 派 发 能 力 的 ChannelHandler , 比 如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHandler等,其本身并不具备线程派发能力。Dubbo的5种不同的线程派发策略如下表所示。默认配置下,Dubbo使用all派发策略,即将所有的消息都派发到线 程 池 。 请 求 对 象 会 被 封 装 在 ChannelEventRunnable 中 ,ChannelEventRunnable将会是服务调用过程的新起点。所以接下来我们看一下以ChannelEventRunnable为起点的服务提供者的线程调用栈,如下图所示。向用户线程传递调用结果响应数据解码完成后,Dubbo会将响应对象派发到线程池。要注意的是,线程池中的线程并非用户的调用线程,所以要想办法将响应对象从线程池传递到用户线程上。用户线程在发送完请求后,调用DefaultFuture的get方法等待响应对象的到来。当响应对象到来后,用户线程会被唤醒,并通过调用编号获取属于自己的响应对象。Dubbo设计原理Dubbo在架构上通过SPI机制(SPI的全称为Service ProviderInterface,SPI机制是一种服务发现机制)的设计,使得整体架构具备了极高的可扩展性。下面是Dubbo的核心设计原理:采 用 Microkernel+Plugin 模 式 , Microkernel 负 责 组 装Plugin,Dubbo自身的功能也是通过扩展点实现的,也就是Dubbo的所有功能点都可被用户自定义扩展所替换。采用URL作为配置信息的统一格式,所有扩展点都通过传递URL携带配置信息。SPI机制的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样它可以在运行时动态为接口替换实现类。正因为此特性,我们可以通过SPI机制为程序提供拓展功能,这样可以在运行时动态为接口替换实现类。Dubbo就是通过SPI机制加载所有组件的,不过Dubbo并未使用Java原生的SPI机制,而是对其进行了增强,使其能够更好地满足需求。Dubbo SPI示例首先,我们定义一个接口,名称为Hello:Dubbo SPI的相关逻辑被封装在了ExtensionLoader类中,通过ExtensionLoader类我们可以加载指定的实现类。Dubbo SPI所需的配置文件需放置在META-INF/dubbo路径下,配置内容如下:与Java SPI实现类配置不同,Dubbo SPI通过键值对的方式进行配置,我们可以按需加载指定的实现类。另外,在测试Dubbo SPI时,需要在Robot接口上标注@SPI注解。上述代码的输出结果如下:SPI机制下面我们结合源码来理解Dubbo的SPI机制和整体架构特性,需要明确几个核心概念,如下图所示。ExtensionLoaderExtensionLoader作为整个SPI机制的核心起着无可替代的作用,扩展点并不会强制所有用户都使用Dubbo提供的某些架构。例如Dubbo提供了ZooKeeper注册中心,但是如果我们更倾向于其他的注册中心,我们可以替换掉Dubbo提供的注册中心。我们称这种可被替换的技术实现点为扩展点,类似的扩展点有很多,例如Protocol、Filter、Loadbalance等。鉴 于 ExtensionLoader 的 用 法 比 较 多 , 下 面 我 们 以ExtensionLoader 类 作 为 入 口 进 行 讲 解 。 首 先 , 我 们 通 过ExtensionLoader的getExtensionLoader方法获取一个单例实例,然后通过ExtensionLoader的getExtension方法获取拓展类对象。其中,getExtensionLoader 方 法 用 于 从 缓 存 中 获 取 与 拓 展 类 对 应 的ExtensionLoader实例,若缓存未命中,则创建一个新的实例。下面我们以ExtensionLoader的getExtension方法作为入口,代码如下:上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建实例化对象的代码实现:createExtension模块中包含了如下步骤:(1)通过getExtensionClasses获取所有的拓展类。(2)通过反射创建拓展对象。(3)向拓展对象中注入依赖。(4)将拓展对象包裹在相应的Wrapper对象中。我们在通过名称获取拓展类之前,需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称,拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下:这里也是先检查缓存,若缓存未命中则通过synchronized加锁,加锁后再次检查缓存,并判空。此时如果classes仍空,则通过loadExtensionClasses加载拓展类。下面分析loadExtensionClasses方法的逻辑:loadExtensionClasses方法总共做了两件事情,一是对SPI注解进行解析,二是调用loadDirectory方法加载指定目录中的配置文件。SPI注解解析过程比较简单,loadDirectory方法先通过类加载器获 取 所 有 资 源 链 接 , 然 后 通 过 loadResource 方 法 加 载 资 源 。loadResource方法用于读取和解析配置文件,并通过反射加载类,最后调用loadClass方法进行其他操作。Dubbo会从以下三个路径读取并加载扩展点配置文件:Wrapper在实例化扩展点的代码中可以看到,在加载某个接口的扩展类时,如果某个实现中有一个拷贝类构造函数,那么该接口实现就是该接口的包装类,此时Dubbo会在真正的实现类上层包装上Wrapper。即这个时候从ExtensionLoader中返回的实际扩展类是被Wrapper包装的接口实现类。在上文代码的createExtension(String name)实例化扩展点中(代码1#)可以看到相关代码实现:将反射创建的instance实例作为参数传给Wrapper的构造方法,并通过反射创建Wrapper实例 , 而 后 在 Wapper 实 例 中 注 入 依 赖 , 最 后 将 Wapper 实 例 赋 值 给instance实例。SetterDubbo IoC通过setter方法注入依赖。Dubbo首先会通过反射获取实例的所有方法,然后遍历方法列表,检测方法名是否具有setter方法特征。若有这个特征则通过ObjectFactory获取依赖对象,最后通过反射调用setter方法将依赖设置到目标对象中。整个过程对应的注入扩展点代码如下:扩展点实现类的成员如果为其他扩展点类型,ExtensionLoader会自动注入依赖的扩展点。ExtensionLoader通过扫描扩展点实现类的所有set方法来判定其成员。@SPI在SPI代码实例中,Dubbo只有接口类使用了@SPI注解才会去加载扩展点实现,Dubbo本身重新实现了一套SPI机制,支持AOP与依赖注入,并且可以利用缓存提升加载实现类的性能,也支持实现类的灵活获取。下面是@SPI的定义:在上文的loadExtensionClasses中(代码2#)中,我们可以看到getExtensionLoader会对传入的接口进行校验,其中就会检验接口是否被@SPI注解,通过获取并缓存接口的@SPI注解上的默认实现类cacheDefaultExtensionName,再调用loadDirectory方法记载指定目录中的配置文件。源码实现如下:@Adaptive在 Dubbo 中 , 很 多 扩 展 都 是 通 过 SPI 机 制 进 行 加 载 的 , 比 如Protocol、Cluster、LoadBalance等。然而有些扩展并不想在框架启动阶段被加载,而是希望在扩展方法被调用时根据运行时参数进行加载。在对自适应扩展生成过程进行深入分析之前,我们来看一下与自适应扩展息息相关的一个注解,即@Adaptive注解,该注解的定义如下:@Adaptive可注解在类或方法上。当@Adaptive注解在类上时,Dubbo不会为该类生成代理类。当@Adaptive注解在方法(接口方法)上时,Dubbo则会为该方法生成代理逻辑。@Adaptive注解在类上的情况 很 少 , 在 Dubbo 中 仅 有 两 个 类 被 @Adaptive 注 解 了 , 分 别 是AdaptiveCompiler和AdaptiveExtensionFactory。getAdaptiveExtension方法是获取自适应扩展的入口方法,相关代码如下:getAdaptiveExtension方法首先会检查缓存,如果缓存未命中,则 调 用 方 法 创 建 自 适 应 扩 展 。 下 面 我 们 看 一 下createAdaptiveExtension方法的代码:createAdaptiveExtension方法的代码包含了三个逻辑,分别如下:○ 调 用 getAdaptiveExtensionClass 方 法 获 取 自 适 应 扩 展Class对象。○ 通过反射进行实例化。○ 调用injectExtension方法向扩展实例中注入依赖。@Activate@Activate注解表示一个扩展是否被激活,可以放在类定义和方法上,Dubbo将它用在SPI扩展类定义上,表示这个扩展实现的激活条件和时机。下面是代码示例:上 述 示 例 表 示 只 有 当 group 参 数 作 为 提 供 者 时 才 会 使RpcServerInterceptor拦截逻辑生效,这个注解的作用和Spring Boot中的@Condition注解类似。
SpringBoot整合Dubbo(二)
正文一、Dubbo架构调用关系说明服务容器负责启动,加载,运行服务提供者。服务提供者在启动时,向注册中心注册自己提供的服务。服务消费者在启动时,向注册中心订阅自己所需的服务。注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。二、代码代码结构如下pom文件 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiaojie</groupId>
<artifactId>dubbo-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>dubbo-demo-api</module>
<module>dubbo-demo-provider</module>
<module>dubbo-demo-consumer</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.2</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
</dependencies>
</project>接口类package com.xiaojie.dubbo.api;
/**
* @Description:用户接口,只有接口 没有实现类
* @author: xiaojie
* @date: 2021.08.02
*/
public interface UserServiceApi {
/*
*
* @param userId
* @获取用户名称
* @author xiaojie
* @date 2021/8/2 9:19
* @return java.lang.String
*/
String getUserName(String userId);
}接口实现类(服务提供者),此模块要依赖dubbo-demo-api模块,或者单独将dubbo-demo-api打成jar包,添加到maven依赖。package com.xiaojie.dubbo.api.impl;
import com.xiaojie.dubbo.api.UserServiceApi;
import org.apache.dubbo.config.annotation.DubboService;
/**
* @Description:接口实现类
* @author: xiaojie
* @date: 2021.08.02
*/
@DubboService(version = "1.0.0")
public class UserServiceApiImpl implements UserServiceApi {
@Override
public String getUserName(String userId) {
return "我是服务的提供者:我的名字是小杰";
}
}application.properties#spring项目名
spring.application.name=xiaojie-dubbo-provider
#Dubbo provider configuration
dubbo.application.name=dubbo_provider
dubbo.registry.protocol=zookeeper
dubbo.registry.address=192.168.6.136:2181
dubbo.protocol.name=dubbo
dubbo.registry.timeout=50000
#服务端口
dubbo.protocol.port=20881
#扫描注解包通过该设置将服务注册到zookeeper
dubbo.scan.base-packages=com.xiaojie.dubbo.api
server.port=8081服务消费者package com.xiaojie.controller;
import com.xiaojie.dubbo.api.UserServiceApi;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @author: xiaojie
* @date: 2021.08.02
*/
@RestController
public class ConsumerController {
@DubboReference(version = "1.0.0")
private UserServiceApi userServiceApi;
@GetMapping("/getUserName")
public String getUserName(String userId){
return userServiceApi.getUserName(userId);
}
}application.propertiesspring.application.name=xiaojie-dubbo-consumer
dubbo.application.name=dubbo_consumer
dubbo.registry.protocol=zookeeper
#zk地址
dubbo.registry.address=192.168.6.136:2181
dubbo.registry.timeout=50000
server.port=8090三、注册到Zookeeper之后数据存储
服务发现机制SPI居然是破坏者?!
前言主要介绍下 Java 中的 SPI 机制 。Springboot 的 SPI 机制 咱们在下文 Springboot的自动装配中再说~ 😝 嘿嘿至于 [[ dubbo 的 SPI 机制]],还没时间深入了解,简单知道了它的 SPI 的自适应扩展机制,以及下面这些扩展~(超级多扩展的~)🐷冲冲冲!什么是SPI呢?SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类(定义多少,加载多少)它是 Java 提供的一套用来被第三方实现或者扩展的接口JAVA SPI 实践步骤:先定义一个接口和它的实现类再在 resources 下添加 META-INF/services 这两个文件夹 在该文件夹中新建一个普通文本,名字为 接口的全名 ,并将 接口的实现类全名 添加到文本中,多个的话进行换行操作最后在代码中调用 ServiceLoader.load 方法即可实现项目结构图spi文件内容源码如下接口public interface IJava4ye {
void say();
}
public class Java4yeImpl implements IJava4ye{
@Override
public void say() {
System.out.println(" 【java4ye】 杰伦 杰伦!");
}
}
复制代码测试类public class SpiTest {
@Test
public void spiTest(){
ServiceLoader<IJava4ye> load = ServiceLoader.load(IJava4ye.class);
for (IJava4ye iJava4ye : load) {
iJava4ye.say();
}
}
}
复制代码结果是不是非常的方便,还有 IOC 的感觉 哈哈😄JDBC实例那么实践完,我们再来看看 JDBC 这个小案例😝可以看到我们使用的 MySQL 的 JDBC 包中就有这么一个例子~🐷而这个 java.sql.Driver 文件中就定义了这么一个实现类~👇com.mysql.cj.jdbc.Driver
复制代码Driver源码可以发现这里有一个静态代码块,随着类被加载,它会直接往 DriverManager 中注册这个 Driver 驱动器 。那有什么作用呢?简单看下这个 JDBC 的写法 ,可以发现,之前我们都是要手动去写这个Class.forName 来加载这个驱动器,但是用了这个 SPI 后,那不就可以省略这行代码了吗~😄 哈哈🐷以后我们都不用管厂商定义了啥,直接使用 DriverManager.getConnection(url, username, password); 就可以获取到这个连接了,代码中就不会出现这种硬编码~ 灵活多了🐷 (当然不开发框架或者自己多折腾好像也用不上呀😝 哈哈哈)JAVA SPI 原理探索疑惑不知道小伙伴们实践完会不会有点小疑惑,这个 SPI 一定要用 ServiceLoader.load 方法去加载的吗?4ye 也不知道是不是 哈哈,但是 DriverManager 中就是这么使用的😝详细请看下图👇破坏双亲委派机制我们都知道在 Java 中有这个双亲委派机制 ,像上面 DriverManager 这种做法,其实是破坏了这个 双亲委派机制 。因为这个实现类是属于第三方类库 mysql 的而这个 Driver 接口是在jdk 自身的 lib --》 rt.jar 中的它是由 根类加载器 BootstrapClassloader 加载的, 而它的实现类却是在第三方的包中,这样子 BootstrapClassloader 是无法加载的。那么怎样才能加载第三方包中的类呢?看看 ServiceLoader 这个类我们就清楚啦~ 😝ServiceLoader源码可以发现这里有很显眼的一句话ClassLoader cl = Thread.currentThread().getContextClassLoader();
复制代码通过 当前线程的上下文类加载器 去加载的!那么这个 当前线程的上下文类加载器 中又是用了哪个 classLoader 呢?在源码中一番搜索之后,我们可以看到在启动器 Launcher 中有这么一段代码,将 AppClassLoader 赋值给 ClassLoader 变量,并将其设置到当前线程中。😋那么结论也很明显啦,SPI 是通过应用程序类加载器 AppClassLoader 去加载第三方包中的类的。总结java 提供了这个服务发现机制 SPI ,可以方便我们对某个接口进行扩展,实现模块的热插拔,而缺点就是,灵活度不高,配置文件中由多少实现类,都会被加载到内存中,不管有没有使用到~🐷原理图奉上~
面试官:微服务下数据一致性的有几种实现方式,分别说一下
本人最近学习了一下微服务下数据一致性的特点,总结了下目前的保障微服务下数据一致性的几种实现方式如下,以备后查。此篇文章旨在给大家一个基于微服务的数据一致性实现的大概介绍,并未深入展开,具体的实现方式本人也在继续学习中,如有错误,欢迎大家拍砖。传统应用的事务管理本地事务在介绍微服务下的数据一致性之前,先简单地介绍一下事务的背景。传统单机应用使用一个RDBMS作为数据源。应用开启事务,进行CRUD,提交或回滚事务,统统发生在本地事务中,由资源管理器(RM)直接提供事务支持。数据的一致性在一个本地事务中得到保证。分布式事务两阶段提交(2PC)当应用逐渐扩展,出现一个应用使用多个数据源的情况,这个时候本地事务已经无法满足数据一致性的要求。由于多个数据源的同时访问,事务需要跨多个数据源管理,分布式事务应运而生。其中最流行的就是两阶段提交(2PC),分布式事务由事务管理器(TM)统一管理。两阶段提交分为准备阶段和提交阶段。 两阶段提交-commit 两阶段提交-rollback然而两阶段提交也不能完全保证数据一致性问题,并且有同步阻塞的问题,所以其优化版本三阶段提交(3PC)被发明了出来。微服务下的事务管理那么,分布式事务2PC或者3PC是否适合于微服务下的事务管理呢?答案是否定的,原因有三点:1.由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC(Dubbo)或Http API(Spring Cloud)进行,所以已经无法使用TM统一管理微服务的RM。2.不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不支持事务的数据库,则事务根本无从谈起。3.即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将会产生很多锁及数据不可用,严重影响系统性能。由此可见,传统的分布式事务已经无法满足微服务架构下的事务管理需求。那么,既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则——BASE理论。BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。1.基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。2.软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。3.最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。BASE中的最终一致性是对于微服务下的事务管理的根本要求,既基于微服务的事务管理无法达到强一致性,但必须保证最重一致性。那么,有哪些方法可以保证微服务下的事务管理的最终一致性呢,按照实现原理分主要有两类,事件通知型和补偿型,其中事件通知型又可分为可靠事件通知模式及最大努力通知模式,而补偿型又可分为TCC模式、和业务补偿模式两种。这四种模式都可以达到微服务下的数据最终一致性。实现微服务下数据一致性的方式可靠事件通知模式同步事件可靠事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。首先能想到的也是最简单的就是同步事件通知,业务处理与消息发送同步执行,实现逻辑见下方代码及时序图。publicvoidtrans(){
try{
// 1. 操作数据库
bool result = dao.update(data); // 操作数据库失败,会抛出异常
// 2. 如果数据库操作成功则发送消息
if (result){
mq.send(data); // 如果方法执行失败,会抛出异常
}
} catch (Exception e) {
roolback(); // 如果发生异常,就回滚
}
}
上面的逻辑看上去天衣无缝,如果数据库操作失败则直接退出,不发送消息;如果发送消息失败,则数据库回滚;如果数据库操作成功且消息发送成功,则业务成功,消息发送给下游消费。然后仔细思考后,同步消息通知其实有两点不足的地方。在微服务的架构下,有可能出现网络IO问题或者服务器宕机的问题,如果这些问题出现在时序图的第7步,使得消息投递后无法正常通知主服务(网络问题),或无法继续提交事务(宕机),那么主服务将会认为消息投递失败,会滚主服务业务,然而实际上消息已经被从服务消费,那么就会造成主服务和从服务的数据不一致。具体场景可见下面两张时序图。事件服务(在这里就是消息服务)与业务过于耦合,如果消息服务不可用,会导致业务不可用。应该将事件服务与业务解耦,独立出来异步执行,或者在业务执行后先尝试发送一次消息,如果消息发送失败,则降级为异步发送。本地事件服务:为了解决上述同步事件中描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。异步事件通知——本地事件服务当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。如果投递失败,则使用事件服务定时地异步统一处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用异步事件服务保证事件至少被投递一次。然而,这种使用本地事件服务保证可靠事件通知的方式也有它的不足之处,那便是业务仍旧与事件服务有一定耦合(第一次同步投递时),更为严重的是,本地事务需要负责额外的事件表的操作,为数据库带来了压力,在高并发的场景,由于每一个业务操作就要产生相应的事件表操作,几乎将数据库的可用吞吐量砍了一半,这无疑是无法接受的。正是因为这样的原因,可靠事件通知模式进一步地发展-外部事件服务出现在了人们的眼中。外部事件服务:外部事件服务在本地事件服务的基础上更进了一步,将事件服务独立出主业务服务,主业务服务不在对事件服务有任何强依赖。异步事件通知——外部事件服务业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。外部事件虽然能够将业务系统和事件系统解耦,但是也带来了额外的工作量:外部事件服务比起本地事件服务来说多了两次网络通信开销(提交前、提交/回滚后),同时也需要业务系统提供单独的查询接口给事件系统用来判断未发送事件的状态。可靠事件通知模式的注意事项:1. 事件的正确发送;2. 事件的重复消费。通过异步消息服务可以确保事件的正确发送,然而事件是有可能重复发送的,那么就需要消费端保证同一条事件不会重复被消费,简而言之就是保证事件消费的幂等性。如果事件本身是具备幂等性的状态型事件,如订单状态的通知(已下单、已支付、已发货等),则需要判断事件的顺序。一般通过时间戳来判断,既消费过了新的消息后,当接受到老的消息直接丢弃不予消费。如果无法提供全局时间戳,则应考虑使用全局统一的序列号。对于不具备幂等性的事件,一般是动作行为事件,如扣款100,存款200,则应该将事件ID及事件结果持久化,在消费事件前查询事件ID,若已经消费则直接返回执行结果;若是新消息,则执行,并存储执行结果。最大努力通知模式相比可靠事件通知模式,最大努力通知模式就容易理解多了。最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等,这里不再展开。业务补偿模式接下来介绍两种补偿模式,补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。首先介绍业务补偿模式,业务补偿模式是一种纯补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。举个例子,小明从杭州出发,去往美国纽约出差,现在他需要定从杭州去往上海的火车票,以及从上海飞往纽约的飞机票。如果小明成功购买了火车票之后发现那天的飞机票已经售空了,那么与其在上海再多待一天,小明还不如取消去上海的火车票,选择飞往北京再转机纽约,所以小明就取消了去上海的火车票。这个例子中购买杭州到上海的火车票是服务a,购买上海到纽约的飞机票是服务b,业务补偿模式就是在服务b失败的时候,对服务a进行补偿操作,在例子中就是取消杭州到上海的火车票。补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。业务补偿模式最大的缺点是软状态的时间比较长,既数据一致性的时效性很低,多个服务常常可能处于数据不一致的情况。TCC/Try Confirm Cancel模式TCC模式是一种优化了的业务补偿模式,它可以做到完全补偿,既进行补偿后不留下补偿的纪录,就好像什么事情都没有发生过一样。同时,TCC的软状态时间很短,原因是因为TCC是一种两阶段型模式,只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。 TCC模式TCC模式的具体流程为两个阶段:Try,业务服务完成所有的业务检查,预留必需的业务资源如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。服务a(小明从招行转出100元):try:update cmb_account set balance=balance-100, freeze=freeze+100 where acc_id=1 and balance>100;confirm:update cmb_account set freeze=freeze-100 where acc_id=1;cancel:update cmb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;服务b(小明往广发银行汇入100元):try:update cgb_account set freeze=freeze+100 where acc_id=1;confirm:update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;cancel:update cgb_account set freeze=freeze-100 where acc_id=1;具体说明:a的try阶段,服务做了两件事:1. 业务检查,这里是检查小明的帐户里的钱是否多余100元;2. 预留资源,将100元从余额中划入冻结资金。a的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。b的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。b的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。总结下面的表格对这四种常用的模式进行了比较:
SpringCloudAlibaba篇(七)SpringCloud整合Zipkin分布式链路跟踪系统(SpringCloud+dubbo+Zipkin)
上一篇,SpringCloudAlibaba篇(六)整合Seata(微服务分布式事务nacos+seata)@[toc]前言zipkin官网Zipkin是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的计时数据。功能包括此数据的收集和查找。如果日志文件中有跟踪 ID,则可以直接跳转到该 ID。否则,您可以根据服务、操作名称、标签、持续时间等属性进行查询。将为您汇总一些有趣的数据,例如在服务中花费的时间百分比以及操作是否失败。Zipkin是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的计时数据。功能包括此数据的收集和查找。如果日志文件中有跟踪 ID,则可以直接跳转到该 ID。否则,您可以根据服务、操作名称、标签、持续时间等属性进行查询。将为您汇总一些有趣的数据,例如在服务中花费的时间百分比以及操作是否失败Zipkin UI 还提供了一个依赖关系图,显示通过每个应用程序跟踪的请求数。这对于识别聚合行为(包括错误路径或对已弃用服务的调用)很有帮助。1、 zipkin下载安装1.1、zipkin下载下载地址1.2、zipkin建表语句--
-- Copyright 2015-2019 The OpenZipkin Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
-- in compliance with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software distributed under the License
-- is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
-- or implied. See the License for the specific language governing permissions and limitations under
-- the License.
--
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`remote_service_name` VARCHAR(255),
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;1.3、zipkin启动java -jar zipkin-server-2.23.16-exec.jar --storage_type=mysql --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=123456 --MYSQL_HOST=localhost --MYSQL_TCP_PORT=3306访问localhost:94112、zipkin整合SpringCloud2.1、添加依赖brave-instrumentation-dubbo 这里我用的版本是5.13.7<!-- zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-dubbo</artifactId>
</dependency>2.2、修改配置文件因为我的项目的配置中心是nacos所以我直接在nacos新建一个zipkin.yamlspring:
zipkin:
base-url: http://127.0.0.1:9411 #zipkin server 的地址
sender:
type: web #如果ClassPath里没有kafka, active MQ, 默认是web的方式
sleuth:
sampler:
probability: 1.0 #100%取样,生产环境应该低一点,用不着全部取出来bootstrap.yml中追加extension-configs[5]:
data-id: zipkin.yaml
group: DEFAULT_GROUP
refresh: false2.3、dubbo配置修改添加红色方框配置,即可在zipkin中观察到dubbo调用启动微服务2.4、测试这里我通过网关分别调用一下order-service和user-service
maven package;idea跳过单元测试,idea模拟服务多开
@[TOC]1.maven打包跳过单元测试mvn install -DskipTests
#或者
mvn install -Dmaven.test.skip=true1.1.idea跳过单元测试2.idea服务多开在idea中找到services工具栏 (如果没有的话alt+8打开)选中一个服务点击复制配置弹出一个编辑配置的窗口,我这里改成userApplication 复制 然后程序参数中修改下服务端口(我这里是dubbo框架, 所以也需要修改下dubbo端口)启动成功