天天看点

Java面试问题(八)—— 微服务五大组件之Hystrix和网关

作者:技术闲聊DD

今天给大家说一下Hystrix和网关方面的面试题。

Hystrix熔断

什么是服务雪崩?

服务雪崩效应是一种因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程。

雪崩形成的原因是什么?

大致可以分成三个阶段:

  • 服务提供者不可用

    原因:

    (1)硬件故障: 硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问。

    (2)程序Bug。

    (3)缓存击穿:缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时。大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.

    (4)用户大量请求:在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.

  • 重试加大流量

    原因:

    (1)用户重试:在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.

    (2)代码逻辑重试:服务调用端的会存在大量服务异常后的重试逻辑。

  • 服务调用者不可用

    原因:

    同步等待造成的资源耗尽: 当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源。一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.

服务雪崩的应对策略都有哪些?

1. 流量控制

(1)网关限流:因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也越来越热门。

(2)用户交互限流: 采用加载动画,提高用户的忍耐等待时间。提交按钮添加强制等待时间机制.

关闭重试

2. 改进缓存模式

缓存预加载,同步改为异步刷新。

3. 服务器自动扩容

AWS的auto scaling

4. 服务调用者降级服务

(1)资源隔离:资源隔离主要是对调用服务的线程池进行隔离。

(2)对依赖服务进行分类:根据具体业务将依赖服务分为强依赖和若依赖。强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止。

(3)不可用服务的调用快速失败:不可用服务的调用快速失败一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现。

Hystrix是什么?

在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很常见的。

Hystrix 可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。Hystrix 通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时Hystrix 还提供故障时的 fallback 降级机制。

总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。

Hystrix的提供的功能有什么?

资源隔离、限流、熔断、降级、运维监控。

Hystrix的设计原则是什么?

  • 对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护。
  • 在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延。比如某一个服务故障了,导致其它服务也跟着故障。
  • 提供 fail-fast(快速失败)和快速恢复的支持。
  • 提供 fallback 优雅降级的支持。
  • 支持近实时的监控、报警以及运维操作。

Hystrix的内部处理逻辑是什么(原理)?

  1. 构建HystrixCommand或者HystrixObservableCommand对象
  2. 调用 command 执行方法
  3. 检查是否开启缓存
  4. 检查是否开启了断路器
  5. 检查线程池/队列/信号量是否已满
  6. 执行 command
  7. 断路健康检查
  8. 调用 fallback 降级机制
  9. 返回成功的Response

Hystrix 实现资源隔离的技术是什么?

Hystrix 里面核心的一项功能,其实就是所谓的资源隔离,要解决的最核心的问题,就是将多个依赖服务的调用分别隔离到各自的资源池内。避免说对某一个依赖服务的调用,因为依赖服务的接口调用的延迟或者失败,导致服务所有的线程资源全部耗费在这个服务的接口调用上。一旦说某个服务的线程资源全部耗尽的话,就可能导致服务崩溃,甚至说这种故障会不断蔓延。

Hystrix 实现资源隔离,主要有两种技术:线程池,信号量。默认情况下,Hystrix 使用线程池模式。

1. 信号量隔离策略

信号量隔离主要通过TryableSemaphore接口实现:

interface TryableSemaphore {

    // 尝试获取信号量
    public abstract boolean tryAcquire();
    // 释放信号量    
    public abstract void release();
    // 
    public abstract int getNumberOfPermitsUsed();

}           

它的主要实现类主要有TryableSemaphoreNoOp,顾名思义,不进行信号量隔离,当采取线程隔离策略的时候将会注入该实现到HystrixCommand中,如果采用信号量的隔离策略时,将会注入TryableSemaphoreActual,但此时无法超时和异步化,因为信号量隔离资源的策略无法指定命令的在特定的线程执行,从而无法控制线程的执行结果。

TryableSemaphoreActual实现相当简单,通过AtomicInteger记录当前请求的信号量的线程数(原子操作保证数据的一致性),与初始化设置的允许最大信号量数进行比较numberOfPermits(可以动态调整),从而判断是否允许获取信号量,轻量级的实现,保证TryableSemaphoreActual无阻塞的操作方式。

需要注意的是每一个TryableSemaphore通过CommandKey与HystrixCommand一一绑定,在AbstractCommand的getExecutionSemaphore()有体现。

如果是采用信号量隔离的策略,将尝试从缓存中获取该CommandKey对应的TryableSemaphoreActual(缓存中不存在创建一个新的,并与CommandKey绑定放置到缓存中),否则返回TryableSemaphoreNoOp不进行信号量隔离。

2. 线程隔离策略

在AbstractCommand的executeCommandWithSpecifiedIsolation()的方法中,线程隔离策略与信号隔离策略的操作主要区别是将Observable的执行线程通过threadPool.getScheduler()进行了指定,我们先查看一下HystrixThreadPool的相关接口。

HystrixThreadPool是用来将HystrixCommand#run()(被HystrixCommand包装的代码)指定到隔离的线程中执行的。

public interface HystrixThreadPool {

    // 获取线程池
   public ExecutorService getExecutor();
    // 获取线程调度器
   public Scheduler getScheduler();
    //
   public Scheduler getScheduler(Func0<Boolean> shouldInterruptThread);

   // 标记一个命令已经开始执行 
   public void markThreadExecution();

   // 标记一个命令已经结束执行 
   public void markThreadCompletion();

   // 标记一个命令无法从线程池获取到线程
   public void markThreadRejection();

   // 线程池队列是否有空闲 
   public boolean isQueueSpaceAvailable();
    
 }
           

HystrixThreadPool是由HystrixThreadPool.Factory生成和管理的,是通过ThreadPoolKey(@HystrixCommand中threadPoolKey指定)与HystrixCommand进行绑定,它的默认实现为HystrixThreadPoolDefault,其内的线程池ThreadPoolExecutor是通过HystrixConcurrencyStrategy策略生成。

如果允许配置的maximumSize生效的话(allowMaximumSizeToDivergeFromCoreSize为true),在coreSize小于maximumSize时,会创建一个线程最大值为maximumSize的线程池,但会在相对不活动期间返回多余的线程到系统。否则就只应用coreSize来定义线程池中线程的数量。dynamic前缀说明这些配置都可以在运行时动态修改,如通过配置中心的方式。

touchConfig()的方法中可以动态调整线程池线程大小、线程存活时间等线程池的关键配置,在配置中心存在的情况下可以动态设置。

HystrixContextScheduler是Hystrix对rx中Scheduler调度器的重写,主要为了实现在Observable未被订阅时,不获取线程执行命令,以及支持在命令执行过程中能够打断运行。

你们在项目中如何使用Hystrix?

  1. 在feign中已经集成了Hystrix组件相关的依赖,所以我们不需要额外的添加。
  2. feign中默认是关闭了Hystrix功能的,所以需要开启熔断功能,只需要在application.yml文件中添加如下配置:
feign:
  hystrix:
    enabled: true
## true:表示开启hystrix熔断功能,false表示关闭
           
  1. 然后需要为每个@FeignClient添加fallback属性配置快速失败处理类。该处理类是feign hystrix的逻辑处理类,必须实现被@FeignClient注解修饰的接口。比如我这里定义为HiHystrix.java类,然后在该类上加上@Component注解,以spring bean的形式注入到IoC容器中。

zuul和spring gateway网关

什么是网关?

网关是整个微服务API请求的入口,负责拦截所有请求,分发到服务上去。可以实现日志拦截、权限控制、解决跨域问题、限流、熔断、负载均衡,隐藏服务端的ip,黑名单与白名单拦截、授权等,常用的网关有zuul和spring cloud gateway 。

网关的作用是什么?

  • 网关对所有服务会话进行拦截。
  • 网关安全控制、统一异常处理、xxs、sql注入。
  • 权限控制、黑名单和白名单、性能监控、日志打印等。

网关(zuul或Gateway)和Nginx的区别?

  • 相同点:

    网关和Nginx都可以实现负载均衡、反向代理(隐藏真实ip地址),过滤请求,实现网关的效果。

  • 不同点:

    网关负载均衡实现:采用ribbon+eureka实现本地负载均衡

    Nginx负载均衡实现:采用服务端实现负载均衡

    Nginx相比网关功能会更加强大,因为Nginx整合一些脚本语言(Nginx+lua)

    Nginx适合于服务器端负载均衡,网关适合微服务中实现网关。

过滤器和网关的对比?

  • 过滤器:对单个服务器的请求进行拦截控制。
  • 网关:对所有的服务器的请求进行拦截控制。

什么是zuul?

Zuul包含了对请求的路由和过滤两个最主要的功能:

  • 其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
  • Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,也即以后的访问微服务都是通过Zuul跳转后获得。

zuul 的作用是什么?

Zuul可以通过加载动态过滤机制,从而实现以下各项功能:

  • 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
  • 审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
  • 动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
  • 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
  • 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
  • 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
  • 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。

zuul用来做限流,但是为什么要限流?

  • 防止不需要频繁请求服务的请求恶意频繁请求服务,造成服务器资源浪费。
  • 防止不法分子恶意攻击系统,击穿系统盗取数据,防止数据安全隐患。防止系统高峰时期,对系统频繁访问,给服务器带来巨大压力。限流策略。

zuul如何实现限流?

Spring Cloud Zuul RateLimiter结合Zuul对RateLimiter进行了封装,通过实现ZuulFilter提供了服务限流功能。

限流策略如下:

限流粒度/类型 说明
Authenticated User 针对请求的用户进行限流
Request Origin 针对请求的Origin进行限流
URL 针对URL/接口进行限流
Service 针对服务进行限流,如果没有配置限流类型,则此类型生效

多种粒度临时变量储存方式如下:

存储方式 说明
IN_MEMORY 基于本地内存,底层是ConcurrentHashMap,默认的。
REDIS 基于redis存储,使用时必须搭建redis
CONSUL consul 的kv存储
JPA spring data jpa,基于数据库
BUKET4J 使用一个Java编写的基于令牌桶算法的限流库

这里重点说一下,如果 zuul 需要多节点部署,那就不能用 IN_MEMORY 存储方式,比较常用的就是用REDIS。

  • 引入spring-cloud-zuul-ratelimit
<dependency>
    <groupId>com.marcosbarbero.cloud</groupId>
    <artifactId>spring-cloud-zuul-ratelimit</artifactId>
    <version>2.0.4.RELEASE</version>
</dependency>           

配置:

ratelimit: 
    key-prefix: springcloud-book #按粒度拆分的临时变量key前缀
    enabled: true #启用开关
    repository: IN_MEMORY #key存储类型,默认是IN_MEMORY本地内存,此外还有多种形式
    behind-proxy: true #表示代理之后
    default-policy: #全局限流策略,可单独细化到服务粒度
      limit: 2 #在一个单位时间窗口的请求数量
      quota: 1 #在一个单位时间窗口的请求时间限制
      refresh-interval: 3 #单位时间窗口
      type: 
        - user #可指定用户粒度
        - origin #可指定客户端地址粒度
        - url #可指定url粒度
 
    policies:
      client-a:
      limit: 5
      quota: 5
      efresh-interval: 10
           

zuul的工作原理?

Zuul网关的核心是一系列的过滤器,这些过滤器可以对请求或者响应结果做一系列过滤,Zuul 提供了一个框架可以支持动态加载,编译,运行这些过滤器,这些过滤器是使用责任链方式顺序对请求或者响应结果进行处理的,这些过滤器不会直接进行通信,但是通过责任链传递的RequestContext参数可以共享数据。

Zuul的过滤器是由Groovy写成,这些过滤器文件被放在Zuul Server上的特定目录下面,Zuul会定期轮询这些目录,修改过的过滤器会动态的加载到Zuul Server中以便过滤请求使用。

Java面试问题(八)—— 微服务五大组件之Hystrix和网关

zuul的Filter类型,以及作用是什么?

Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。

  1. PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  2. ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
  3. POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  4. ERROR:在其他阶段发生错误时执行该过滤器。

Zuul内部转发请求有两种,为服务下边的RibbonRoutingFilter,普通http转发的SimpleHostRoutingFilter。

Zuul 如何自定义filter?

在zuul项目中创建MyFilter继承ZuulFilter

package com.example.gatewayservicezuulsimple.zuulFilter;
 
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import javax.servlet.http.HttpServletRequest;
 
public class MyFilter extends ZuulFilter {
    private final Logger logger = LoggerFactory.getLogger(MyFilter.class);
    @Override
    public String filterType() {
        return "pre"; //定义filter的类型,有pre、route、post、error四种
    }
 
    @Override
    public int filterOrder() {
        return 0; //定义filter的顺序,数字越小表示顺序越高,越先执行
    }
 
    @Override
    public boolean shouldFilter() {
        return true; //表示是否需要执行该filter,true表示执行,false表示不执行
    }
 
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
 
        logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
 
        String token = request.getParameter("token");// 获取请求的参数
 
        if (StringUtils.isNotBlank(token)) {
            ctx.setSendZuulResponse(true); //对请求进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
            return null;
        } else {
            ctx.setSendZuulResponse(false); //不对其进行路由
            ctx.setResponseStatusCode(400);
            ctx.setResponseBody("token is empty");
            ctx.set("isSuccess", false);
            return null;
        }
    }
} 
           

Zuul与Spring Cloud Gateway对比?

Spring Cloud Gateway基于Spring 5、Project Reactor、Spring Boot 2,使用非阻塞式的API,内置限流过滤器,支持长连接(比如 websockets),在高并发和后端服务响应慢的场景下比Zuul1的表现要好。

Zuul基于Servlet2.x构建,使用阻塞的API,没有内置限流过滤器,不支持长连接。

Zuul的集群搭建?

使用 Nginx+Zuul 实现网关集群。

Java面试问题(八)—— 微服务五大组件之Hystrix和网关
#配置Zuul端口
server:
  port: 81
spring:
  application:
    name: zull-gateway-service    #服务名
#Eureka配置
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/    #注册中心地址
      
# 配置网关反向代理,例如访问/api-member/** 直接重定向到member-service服务,实现路由转发,隐藏服务的真实ip(服务都实在内网中)
#zull根据服务名,去Eureka获取服务真实地址,并通过本地转发,而且默认开启Ribbon实现负载均衡
#默认读取Eureka注册列表 默认30秒间隔  
zuul:
 routes:
   api-a: #会员服务网关配置
     path: /api-member/**   #访问只要是/api-member/ 开头的直接转发到member-service服务
     #服务名
     serviceId: member-service
   api-b: #订单服务网关配置
     path: /api-order/**
     serviceId: order-service

           

zuul1.0和2.0的区别?

Zuul 1.x 基于同步 IO,Zuul 2.x 最大的改进就是基于 Netty Server 实现了异步 IO 来接入请求,同时基于 Netty Client 实现了到后端业务服务 API 的请求。这样就可以实现更高的性能、更低的延迟。此外也调整了 filter 类型,将原来的三个核心 filter 显式命名为:Inbound Filter、Endpoint Filter和 Outbound Filter。

Gateway的组成都有什么?

  • 路由 : 网关的基本模块,有ID,目标URI,一组断言和一组过滤器组成。
  • 断言:就是该路由的访问规则,可以用来匹配来自http请求的任何内容,例如headers或者参数。
  • 过滤器:这个就是我们平时说的过滤器,用来过滤一些请求的,gateway有自己默认的过滤器,具体请参考官网,我们也可以自定义过滤器,但是要实现两个接口,ordered和globalfilter。

简单来说就是Route、Predicate、Filter三大核心组件。

Gateway的流程(工作原理)?

  1. 客户端发送请求,会到达网关的DispatcherHandler处理,匹配到RoutePredicateHandlerMapping。
  2. 根据RoutePredicateHandlerMapping匹配到具体的路由策略。
  3. FilteringWebHandler获取的路由的GatewayFilter数组,创建 GatewayFilterChain 处理过滤请求
  4. 执行我们的代理业务逻辑访问。

Gateway如何使用?

  1. 引入依赖
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
           
  1. yml配置
server:
  port: 9524 #端口号
spring:
  application:
    name: cloud-gateway # 微服务注册名称
  cloud:
    gateway: #Gayeway配置
      routes:
        - id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8007   #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**   #断言,路径相匹配的进行路由

        - id: payment_routh2
          uri: http://localhost:8007
          predicates:
            - Path=/payment/lb/**   #断言,路径相匹配的进行路由
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7004.com:7004/eureka
           
  1. 项目主启动类
package com.demo.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
            SpringApplication.run( GateWayMain9527.class,args);
        }
}
           

然后通过9524端口即可访问8007端口下的微服务。

还有一种方式是用代码写配置类

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
	RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
	return routes.route("path_route1", r -> r.path("/guonei")
			.uri("https://news.baidu.com/guonei"))
			.build();
}
           

继续阅读