SpringCloud总结

一、注册中心

常用组件:Eureka、Zookeeper、Nacos、Consul

1.1 Eureka

基本架构

在这里插入图片描述

1.1.1 服务提供者

服务注册
服务提供者在启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息。
服务同步
当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步。
服务续约(Renew)
在注册完服务之后,服务提供者会维护一个心跳用来持续告诉Eureka Server:“我还活着”,以防止Eureka Server的“剔除任务”将该服务示例从服务列表中排除出去。心跳间隔时间默认为30秒,服务失效时间为90秒。

1.1.2 服务消费者

获取服务
启动服务消费者时,它会发送一个REST请求给服务注册中心,来获取上面注册的服务清单,该缓存清单每隔30秒更新一次。
服务调用
服务消费者在获取服务清单后,通过服务名可以获得具体提供服务的实例名和该实例的元数据信息。在Ribbon中默认使用轮询的方式调用实现客户端的负载均衡。
服务下线
当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,服务端在接收到请求之后,将服务状态置为下线(DOWN),并把该下线事件传播出去。

1.1.3 服务注册中心

失效剔除
Eureka Server在启动时会创建一个定时任务,默认每隔一段时间(默认未60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除出去。
自我保护
Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况,Eureka Server会将当前的实例注册信息保护起来,让这些实例不会过期,尽可能保护这些注册信息。但是,在这段保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试,断路器等机制。

1.2 源码分析

Eureka源码ar包
在这里插入图片描述

@EnableDiscoveryClient

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
    boolean autoRegister() default true;
}

用于开启DiscoveryClient实例,DiscoveryClient是SpringCloud的接口,它定义了用来发现服务的常用抽象方法,通过该接口可以有效地屏蔽服务治理的实现细节,所以使用SpringCloud构建的微服务应用可以方便地切换不同服务治理框架,而不改动程序代码,只需要另外添加一些针对服务治理框架的配置即可。EurekaDiscoveryClient是对Eureka发现服务的封装,实现了EurekaClient接口,继承了DiscoveryClient接口。

在这里插入图片描述

通过EurekaClient的实现类DiscoveryClient的构造函数源码分析。

	DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
        ...

            this.initScheduledTasks();

		...
	}

服务注册
Eureka Client可以看到最终是调用到DiscoveryClient.register()方法。
注册操作是通过REST请求方式进行的。传入了一个InstanceInfo对象,该对象就是注册时客户端给服务端的服务的元数据。

	boolean register() throws Throwable {
        logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);

        EurekaHttpResponse httpResponse;
        try {
            httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
        } catch (Exception var3) {
            logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
            throw var3;
        }

        if (logger.isInfoEnabled()) {
            logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
        }

        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

Eureka Server中服务注册的方法

	@POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        if (this.isBlank(info.getId())) {
            return Response.status(400).entity("Missing instanceId").build();
        } else if (this.isBlank(info.getHostName())) {
            return Response.status(400).entity("Missing hostname").build();
        } else if (this.isBlank(info.getIPAddr())) {
            return Response.status(400).entity("Missing ip address").build();
        } else if (this.isBlank(info.getAppName())) {
            return Response.status(400).entity("Missing appName").build();
        } else if (!this.appName.equals(info.getAppName())) {
            return Response.status(400).entity("Mismatched appName, expecting " + this.appName + " but was " + info.getAppName()).build();
        } else if (info.getDataCenterInfo() == null) {
            return Response.status(400).entity("Missing dataCenterInfo").build();
        } else if (info.getDataCenterInfo().getName() == null) {
            return Response.status(400).entity("Missing dataCenterInfo Name").build();
        } else {
            DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
            if (dataCenterInfo instanceof UniqueIdentifier) {
                String dataCenterInfoId = ((UniqueIdentifier)dataCenterInfo).getId();
                if (this.isBlank(dataCenterInfoId)) {
                    boolean experimental = "true".equalsIgnoreCase(this.serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                    if (experimental) {
                        String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                        return Response.status(400).entity(entity).build();
                    }

                    if (dataCenterInfo instanceof AmazonInfo) {
                        AmazonInfo amazonInfo = (AmazonInfo)dataCenterInfo;
                        String effectiveId = amazonInfo.get(MetaDataKey.instanceId);
                        if (effectiveId == null) {
                            amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId());
                        }
                    } else {
                        logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                    }
                }
            }

            this.registry.register(info, "true".equals(isReplication));
            return Response.status(204).build();
        }
    }

对注册信息进行一系列校验后,调用register()进行服务注册。该方法会将新服务注册事件广播出去,然后将服务的元数据信息存在一个ConcurrentHashMap中。第一层的key存储服务名(appName),第二层key存储实例名(instanceId)。

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();

服务续约
服务续约也是以REST请求的方式进行续约

	boolean renew() {
        try {
            EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
            logger.debug("DiscoveryClient_{} - Heartbeat status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
                this.REREGISTER_COUNTER.increment();
                logger.info("DiscoveryClient_{} - Re-registering apps/{}", this.appPathIdentifier, this.instanceInfo.getAppName());
                long timestamp = this.instanceInfo.setIsDirtyWithTime();
                boolean success = this.register();
                if (success) {
                    this.instanceInfo.unsetIsDirty(timestamp);
                }

                return success;
            } else {
                return httpResponse.getStatusCode() == Status.OK.getStatusCode();
            }
        } catch (Throwable var5) {
            logger.error("DiscoveryClient_{} - was unable to send heartbeat!", this.appPathIdentifier, var5);
            return false;
        }
    }

取消租约
查询服务列表

1.2 Nacos

Nacos 原理

Eureka、Zookeeper、Nacos对比

  1. Eureka保证AP、Zookeeper保证CP、Nacos可选择AP/CP
  2. Eureka有自我保护机制,Zookeeper节点宕机时服务不可用
  3. Nacos将注册中心和配置中心合并,并有可视化管理界面;Eureka界面只有服务列表和状态,比较简单
  4. nacs使用的是netty和服务直接进行连接,属于长连接;eureka是使用定时发送和服务进行联系,属于短连接
  5. Nacos 的阈值是针对某个具体 Service 的,而不是针对所有服务的。但 Eureka的自我保护阈值是针对所有服务的。
    在这里插入图片描述

二、配置中心

常用组件:Spring Cloud Config、Nacos、Apollo

2.1 Spring Cloud Config

SpringCloud Config默认采用Git来存储配置信息,并且可以通过Git客户端来管理和访问配置内容。也提供了对其他存储方式的支持,如SVN、本地化文件系统。

基础架构

  • 远程Git仓库:用来存储配置文件的地方。
  • Config Server:分布式配置中心工程,指定了所要连接的Git仓库位置以及账户、密码等连接信息。
  • 本地Git仓库:在Config Server的文件系统中,每次客户端请求获取配置信息时,Config Server从Git仓库中获取最新配置到本地,然后在本地Git仓库中读取并返回。当远程仓库无法获取时,直接将本地内容返回。
  • ServiceA、ServiceB:具体的微服务应用从Config Server获取配置信息进行加载。

客户端获取流程:

  1. 启动应用时,根据bootstrap.properties中配置的应用名{application}、环境名{profile}、分支名{label},向Config Server请求获取配置信息。
  2. Config Server根据自己维护的Git仓库信息和客户端传递过来的配置定位信息去查找配置信息。
  3. 通过git clone命令将找到的配置信息下载到Config Server的文件系统中。
  4. Config Server创建Spring的ApplicationContext实例,并从Git本地仓库中加载配置文件,最后将这些配置内容读取出来返回给客户端应用。
  5. 客户端应用在获得外部配置文件后加载到客户端的ApplicationContext实例,该配置内容的优先级高于客户端Jar包内部的配置内容,所以在Jar包中重复的内容将不再被加载。

动态配置刷新
Spring Cloud Config原生不支持配置的实时推送,需要依赖Git的WebHook、Spring Cloud Bus和客户端/bus/refresh端点。
新增spring-boot-starter-actuator监控模块。其中包含了/refresh端点的实现,该端点将用于实现客户端应用配置信息的重新获取与刷新。

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actator</artifactId>
        </dependency>

通过POST请求/refresh接口可以刷新配置。该功能还可以同Git仓库的Web Hook功能进行关联,当有Git提交变化时,就给对应的配置主机发送/refresh请求来实现配置信息的实时更新。但是当系统壮大后,维护这样的配置清单负担变大,可以通过SpringCloud Bus来实现以消息总线的方式进行配置变更的通知,完成集群上的批量配置更新。

Spring Cloud Config、Nacos、Apollo对比

常见配置中心比较,Spring Cloud Config VS Nacos VS Apollo

三、网关

常用组件:SpringCloud GateWay、Zuul

3.1 Zuul

请求路由
将符合规则的路由都被路由转发到指定url或者服务上。

zull.router.api-a-url.path=/api-a-url/**
#转发到url
zull.router.api-a-url.utl=http://localhost:8080/
#转发到服务
zull.router.api-a-url.serviceId=feign-consumer

请求过滤
对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤。实现的方法只需要集成ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。

实现过滤器的4个方法

  • filterType:过滤类型。这个类型就是在HTTP请求过程中定义的各个阶段。
    • pre:路由映射主要通过pre类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址。可以在请求被路由之前调用。
    • routing:请求转发的部分是由route类型的过滤器完成。在路由请求时被调用。
    • post:在routing和error过滤器之后被调用。
    • error:处理请求时发生错误时被调用。
  • filterOrder:执行顺序。int值定义顺序。
  • shouldFilter:执行条件。返回boolean值判断是够执行,通过此方法指定过滤器的有效范围。
  • run:具体操作。实现自定义的过滤逻辑,确定是否要拦截当前请求;或者对路由返回处理结果进行加工。

请求生命周期
在这里插入图片描述

  1. 首先进入pre类型的过滤器,在进行请求路由之前做一些前置加工,比如请求的校验等。
  2. 第二阶段routing过滤器,将外部请求转发到具体实例服务上去。
  3. 第三阶段post过滤器,对处理结果进行加工或转换等内容。
  4. error过滤器在上述三个阶段中发生异常才会触发,但最后还是流向post类型过滤器,因为需要通过post过滤器将最终结果返回给客户端。

核心过滤器
在这里插入图片描述
动态加载
作为外部API统一访问的入口,动态更新内部逻辑的能力,动态修改规则,动态增加/删除过滤器等。

  • 动态路由:路由规则在application.yml中配置,可以通过SpringCloud Config的动态刷新机制实现。

  • 动态过滤器:请求过滤是通过编码实现,所以对于实现请求过滤器的动态加载,需要借助基于JVM实现的动态语言的帮助,如Groovy。API网关应用会每隔zuul.filter.interval秒从zuul.filter.root目录下获取Groovy定义的过滤器。

SpringCloud GateWay、Zuul区别

  1. gateway对比zuul多依赖了spring-webflux,在Spring的支持下,功能更强大,内部实现了限流、负载均衡等,但同时也限制了仅适合于Spring Cloud套件zuul则可以扩展至其他微服务框架中。
  2. Zuul仅支持同步,gateway支持异步。
    Zuul基于servlet 2.5(使用3.x),使用阻塞API。
    Gateway建立在Spring Framework 5,Project Reactor和Spring Boot 2之上,使用非阻塞API。
  3. Gateway线程开销少,支持各种长连接、WebSocket,Spring官方支持,但运维复杂;Zuul编程模型简单,开发调试运维简单,有线程数限制,延迟堵塞会耗尽线程连接资源。

微服务网关Zuul vs Spring Cloud Gateway

四、Ribbon

SpringCloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过SpringCloud的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。

客户端负载均衡
负载均衡通常都指的是服务端负载均衡,其中分为硬件负载均衡(如F5)和软件负载均衡(如Nginx)。服务端清单维护在负载均衡的节点中。
客户端负载均衡与服务端负载均衡的不同在于所有客户端节点都维护着自己要访问的服务端清单,而这些服务端的清单来自于服务注册中心。

负载均衡器

@LoadBalanced用来给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。
LoadBalancerClient 中定义了客户端负载均衡应具备的几种能力。

public interface LoadBalancerClient extends ServiceInstanceChooser {
	//使用从负载均衡器中挑选出的服务实例来执行请求内容
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
	//为系统构建一个合适的host:port形式的URI
    URI reconstructURI(ServiceInstance instance, URI original);
}

在这里插入图片描述

LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置。在该自动化配置类中,主要做了下面三件事:

  1. 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  2. 创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
  3. 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
    在这里插入图片描述

通过源码以及自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient 的实现类,LoadBalancerInterceptor拦截器对被@LoadBalanced修饰的RestTemplate的请求进行拦截,并利用SpringCloud的负载均衡器LoadBalancerClient 将以逻辑服务名为host的URI转换成具体的服务实例地址。同时通过LoadBalancerClient的实现类RibbonLoadBalancerClient的源码分析,可以知道在使用Ribbon实现负载均衡器的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现。
在这里插入图片描述
Ribbon的核心工作原理

LoadBalancerInterceptor

	public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}

ILoadBalancer

	public interface ILoadBalancer {
	//增加服务实例
    void addServers(List<Server> var1);
	//选择具体服务实例
    Server chooseServer(Object var1);
	//标记服务实例停止
    void markServerDown(Server var1);

    /** @deprecated */
    @Deprecated
    List<Server> getServerList(boolean var1);
	//获取所有的正常服务
    List<Server> getReachableServers();
	//获取所有的服务
    List<Server> getAllServers();
}

在这里插入图片描述

负载均衡策略
IRule
在这里插入图片描述

  • RoundRobinRule轮询
  • RandomRule随机
  • RetryRule轮询重试
  • WeightedResponseTimeRule响应速度决定权重
  • BestAvailableRule最优可用
  • AvailabilityFilteringRule可用性过滤规则
  • ZoneAvoidanceRule区域内可用性能最优

五、Feign

SpringCloud Feign 基于Netflix Feign实现,整合了SpringCloud Ribbon 与SpringCloud Hystrix,提供了一种声明式的Web服务客户端定义方式。SpringCloud Feign对RestTemplate做了进一步的封装,由它来帮助我们定义和实现依赖服务接口的定义。在SpringCloud Feign的实现下,我们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了在使用SpringCloud Ribbon时自行封装服务调用客户端的开发量。

工作原理:

  1. 首先通过@EnableFeignClients注解开启FeignClient 的功能。只有这个注解存在,才会在程序启动时开启对@FeignClient注解的包扫描。
  2. 根据Feign的规则实现接口,并在接口上面加上@FeignClient注解。
  3. 程序启动后,会进行包扫描,扫描所有的@ FeignClient 的注解的类,并将这些信息注入IoC容器中。
  4. 当接口的方法被调用时,通过JDK的代理来生成具体的RequestTemplate模板对象。
  5. 根据RequestTemplate再生成Http请求的Request对象。
  6. Request 对象交给Client去处理,其中Client的网络请求框架可以是HtpURLConnection、HttpClient和OkHttp。
  7. 最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。

Feign的工作原理

六、服务熔断

常用组件:Sentinel、Hystrix

6.1 Hystrix

背景
在微服务架构中,因为网络或自身问题出现调用故障或延迟,导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。若一个单元出现故障,很容易因为依赖关系而引发故障的蔓延,最终导致整个系统的瘫痪,由此产生断路由的保护机制。

功能

  • 服务降级:@HystrixCommand
  • 服务熔断
  • 线程和信号隔离
  • 请求缓存:实现HystrixCommand或HystrixObservableCommand时,通过重载getCacheKey()方法开启请求缓存
  • 请求合并:实现HystrixCommand,继承HystrixCollapser实现请求合并器或通过@HystrixCommand注解
  • 服务监控

工作流程

  1. 创建HystrixCommand或HystrixObservableCommand对象

    命令模式实现对服务调用的封装,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者”与“行为实现者”的解耦,以便使两者可以适应变化。

  2. 命令执行
    HystrixCommand

    //同步执行,从依赖的服务返回一个单一的结果对象,或是在发送错误的时候抛出异常。
    R value = command.execute();
    //异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
    Future<R> fvalue = command.queue();
    

    HystrixObservableCommand

    //返回Observable对象,它代表了操作的多个结果,是一个Hot Observable。
    Observable<R>  ohvalue = command.observe();
    //返回Observable对象,它代表了操作的多个结果,是一个Cold Observable。
    Observable<R>  ohvalue = command.toObservable();
    
  3. 结果是否被缓存
    若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。

  4. 断路由是否打开
    断路器是打开的直接返回成功

  5. 线程池/请求队列/信号量是否占满
    如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)已被占满,那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(第8步)。
    需要注意的是,这里的Hystrix所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池。Hystrix为了保证不会因为某个依赖服务的问题影响到其他依赖服务而采用了舱壁模式来隔离每个依赖的服务。

  6. HystrixObservableCommand.construct()或HystrixCommand.run()

  7. 计算断路由器的健康度
    Hystrix会将“成功”、“失败”、“拒绝”、“超时”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。
    断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行“熔断”,直到恢复期结束。若在恢复期结束后,根据统计数据判断如果还是未达到健康指标,就再次“熔断”。

  8. fallback处理
    当命令执行失败的时候,Hystrix会进入fallback尝试回退处理,我们通常也称该操作为“服务降级”。

  9. 返回成功的响应

hystrix源码分析

断路由原理

断路器HystrixCircuitBreaker

public interface HystrixCircuitBreaker {
	//每个Hystrix命令的请求都通过它判断是否被执行
    boolean allowRequest();
	//返回当前断路器是否打开
    boolean isOpen();
	//用来闭合断路器
    void markSuccess();
    //定义了一个什么都不做的断路器的实现,允许所有请求,且断路器始终闭合
	public static class NoOpCircuitBreaker implements HystrixCircuitBreaker {
        public NoOpCircuitBreaker() {
        }

        public boolean allowRequest() {
            return true;
        }

        public boolean isOpen() {
            return false;
        }

        public void markSuccess() {
        }
    }
	//是断路器HystrixCircuitBreaker 的实现,定义了4个核心对象
    public static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
    	//断路器对应HystrixCommand实例的属性对象
        private final HystrixCommandProperties properties;
        //用来让HystrixCommand记录各类度量指标的对象
        private final HystrixCommandMetrics metrics;
        //断路器是否打开的标志,默认为false
        private AtomicBoolean circuitOpen = new AtomicBoolean(false);
        //断路器打开或是上一次测试的时间戳
        private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();

        protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            this.properties = properties;
            this.metrics = metrics;
        }
		//用来在“半路开”状态时使用。若Hystrix命令调用成功,通过调用它将打开的断路器关闭,并重置度量指标对象。
        public void markSuccess() {
            if (this.circuitOpen.get() && this.circuitOpen.compareAndSet(true, false)) {
                this.metrics.resetStream();
            }

        }
		//判断请求是否被允许,根据properties配置判断打开或关闭属性是否设置,打开则返回false,关闭调用isOpen(),未设置则根据isOpen()和allowSingleTest()判断
        public boolean allowRequest() {
            if ((Boolean)this.properties.circuitBreakerForceOpen().get()) {
                return false;
            } else if ((Boolean)this.properties.circuitBreakerForceClosed().get()) {
                this.isOpen();
                return true;
            } else {
                return !this.isOpen() || this.allowSingleTest();
            }
        }
		/**
			通过circuitBreakerSleepWindowInMilliseconds属性设置了一个断路器打开之后的休眠时间(默认未5秒),
			在该休眠时间到达之后,将再次允许请求尝试访问,此时断路器处于“半开”状态,若此时请求继续失败,断路器又进入打开状态,并继续等待下一个休眠窗口过去之后再次尝试;若请求成功,则将断路器重新置于关闭状态。
		**/
        public boolean allowSingleTest() {
            long timeCircuitOpenedOrWasLastTested = this.circuitOpenedOrLastTestedTime.get();
            return this.circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + (long)(Integer)this.properties.circuitBreakerSleepWindowInMilliseconds().get() && this.circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis());
        }
        
		//判断断路器的打开/关闭状态
        public boolean isOpen() {
        	//如果断路器打开标识为true,则直接返回true
            if (this.circuitOpen.get()) {
                return true;
            } else {
            	//从度量指标对象metrics中获取HealthCounts统计对象做进一步判断(改对象记录了一个滚动时间窗内的请求信息快照,默认时间窗为10秒)
                HealthCounts health = this.metrics.getHealthCounts();
                //如果总请求数(QPS)在预设的阈值范围内就返回false,表示断路器处于未打开状态。默认参数配置为20。
                if (health.getTotalRequests() < (long)(Integer)this.properties.circuitBreakerRequestVolumeThreshold().get()) {
                    return false;
                    //如果错误百分比在阈值范围内就返回false,标识断路器处于未打开状态。默认参数配置为50。
                } else if (health.getErrorPercentage() < (Integer)this.properties.circuitBreakerErrorThresholdPercentage().get()) {
                    return false;
                    //如果上面的两个条件都不满足,则将断路器设置为打开状态(熔断)。同时,如果是从关闭状态切换到打开状态的话,就记录当前时间
                } else if (this.circuitOpen.compareAndSet(false, true)) {
                    this.circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
                    return true;
                } else {
                    return true;
                }
            }
        }
    }
	//维护了一个Hystrix命令与HystrixCircuitBreaker的关系集合
    public static class Factory {
        private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap();

        public Factory() {
        }

        public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            HystrixCircuitBreaker previouslyCached = (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name());
            if (previouslyCached != null) {
                return previouslyCached;
            } else {
                HystrixCircuitBreaker cbForCommand = (HystrixCircuitBreaker)circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreaker.HystrixCircuitBreakerImpl(key, group, properties, metrics));
                return cbForCommand == null ? (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name()) : cbForCommand;
            }
        }

        public static HystrixCircuitBreaker getInstance(HystrixCommandKey key) {
            return (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name());
        }

        static void reset() {
            circuitBreakersByCommand.clear();
        }
    }
}

依赖隔离

  1. 线程池隔离

Hystrix使用“舱壁模式”实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对改依赖服务的调用产生影响,而不会拖慢其他的依赖服务。

线程池隔离的优势:

  • 应用自身得到完全保护,不回收不可控的依赖服务影响。
  • 可以有效降低接入新服务的风险。
  • 当依赖的服务从失效回复正常后,它的线程池会被清理并且能够马上恢复健康的服务。
  • 当依赖的服务出现配置错误的时候,线程池会快速反映出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。
  • 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,线程池的监控指标信息会反映出这样的变化。
  • 每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步访问。
  1. 信号量隔离

信号量的开销远比线程池开销小,但是它不能设置超时和实现异步访问。所以只有在依赖服务是足够可靠的情况下才使用信号量。

  • 命令执行:将隔离策略参数execution.isolation.strategy设置为SEMAPHORE,Hystrix会使用信号量替代线程池。
  • 降级逻辑:当Hystrix尝试降级逻辑时,会在调用线程中使用信号量。

Hystrix和Sentinel区别
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值