Hystrix、Feign技术底层实现原理

本文详细介绍了Feign的设计原理,包括其作为HTTP请求调用的轻量级框架,如何通过动态代理实现接口调用,并解析了Feign的请求流程。同时,文章深入剖析了Hystrix的资源隔离、熔断器和命令模式,解释了如何通过线程池隔离、熔断策略和命令模式保障服务稳定性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. Feign的设计原理

1.1 Feign是什么

       Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign被广泛应用在Spring Cloud 的解决方案中,是学习基于Spring Cloud 微服务架构不可或缺的重要组件。

1.2 Feign解决了什么问题

       封装了Http调用流程,更适合面向接口化的变成习惯。在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。

1.3 Feign是如何设计的

      Feign的远程调用基本流程,大致如图所示:

       

 PHASE 1. 基于面向接口的动态代理方式生成实现类

       在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息;在Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:

        

  PHASE 2. 根据Contract协议规则,解析接口类的注解信息,解析成内部表现

           

 

  默认Contract 实现

 Feign 默认有一套自己的协议规范,规定了一些注解,可以映射成对应的Http请求,如官方的一个例子:

public interface GitHub {
  
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository);
  
  class Contributor {
    String login;
    int contributions;
  }
}

 上述的例子中,尝试调用GitHub.getContributors("foo","myrepo")的的时候,会转换成如下的HTTP请求:

GET /repos/foo/myrepo/contributors
HOST XXXX.XXX.XXX

基于Spring MVC的协议规范SpringMvcContract:

当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务:

  

当然,目前的Spring MVC的注解并不是可以完全使用的,有一些注解并不支持,如@GetMapping,@PutMapping 等,仅支持使用@RequestMapping 等,另外注解继承性方面也有些问题。

PHASE 3. 基于 RequestBean,动态生成Request

这一步很简单,就是根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象。

PHASE 4. 使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑)

Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体,如下所示:   

 

在接口定义上Feign做的比较简单,抽象出了Encoder 和decoder 接口:

public interface Encoder {
  /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
  Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;

  /** 
 * 将实体对象转换成Http请求的消息正文中 
 */
  void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;

  /** * Default implementation of {@code Encoder}. */
  class Default implements Encoder {

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
      if (bodyType == String.class) {
        template.body(object.toString());
      } else if (bodyType == byte[].class) {
        template.body((byte[]) object, null);
      } else if (object != null) {
        throw new EncodeException(
            format("%s is not a type supported by this encoder.", object.getClass()));
      }
    }
  }
}

 

public interface Encoder {
  /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
  Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;

  /** 
 * 将实体对象转换成Http请求的消息正文中 
 */
  void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;

  /** * Default implementation of {@code Encoder}. */
  class Default implements Encoder {

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
      if (bodyType == String.class) {
        template.body(object.toString());
      } else if (bodyType == byte[].class) {
        template.body((byte[]) object, null);
      } else if (object != null) {
        throw new EncodeException(
            format("%s is not a type supported by this encoder.", object.getClass()));
      }
    }
  }
}

目前Feign 有以下实现:

Encoder/ Decoder 实现

说明

JacksonEncoder,JacksonDecoder基于 Jackson 格式的持久化转换协议
GsonEncoder,GsonDecoder基于Google GSON 格式的持久化转换协议
SaxEncoder,SaxDecoder基于XML 格式的Sax 库持久化转换协议
JAXBEncoder,JAXBDecoder基于XML 格式的JAXB 库持久化转换协议
ResponseEntityEncoder,ResponseEntityDecoderSpring MVC 基于 ResponseEntity< T > 返回格式的转换协议
SpringEncoder,SpringDecoder基于Spring MVC HttpMessageConverters 一套机制实现的转换协议 ,应用于Spring Cloud 体系中

PHASE 5. 拦截器负责对请求和返回进行装饰处理

在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作:

public interface RequestInterceptor {

  /** 
 * 可以在构造RequestTemplate 请求时,增加或者修改Header, Method, Body 等信息
 */
  void apply(RequestTemplate template);
}

比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器:

public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
    protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
        super(properties);
    }

    /** 
 * {@inheritDoc}
 */
    @Override
    public void apply(RequestTemplate template) {
        // 在Header 头部添加相应的数据信息
        addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
                HttpEncoding.DEFLATE_ENCODING);
    }
}


PHASE 6. 日志记录

在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息 , 并且将日志的输出定义了四个等级:

级别

说明

NONE不做任何记录
BASIC只记录输出Http 方法名称、请求URL、返回状态码和执行时间
HEADERS记录输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
FULL记录Request 和Response的Header,Body和一些请求元数据

PHASE 7 . 基于重试器发送HTTP请求

Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求,重试器有如下几个控制参数:

重试参数

说明

默认值

period初始重试时间间隔,当请求失败后,重试器将会暂停 初始时间间隔(线程 sleep 的方式)后再开始,避免强刷请求,浪费性能100ms
maxPeriod当请求连续失败时,重试的时间间隔将按照:long interval = (long) (period * Math.pow(1.5, attempt - 1)); 计算,按照等比例方式延长,但是最大间隔时间为 maxPeriod, 设置此值能够避免 重试次数过多的情况下执行周期太长1000ms
maxAttempts最大重试次数5

以下是Feign重试的核心代码逻辑:

final class SynchronousMethodHandler implements MethodHandler {

  @Override
  public Object invoke(Object[] argv) throws Throwable {
   //根据输入参数,构造Http 请求。
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    // 克隆出一份重试器
    Retryer retryer = this.retryer.clone();
    // 尝试最大次数,如果中间有结果,直接返回
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

PHASE 8. 发送Http请求

Feign 真正发送HTTP请求是委托给 feign.Client 来做的:

public interface Client {

  /** 
 * 执行Http请求,并返回Response * @param request safe to replay.  
 */
  Response execute(Request request, Options options) throws IOException;
  }

Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端,目前我们荐彩等项目内部使用的就是OkHttp3作为Http 客户端。

 

二: Hystrix的设计原理

       Hystrix的三大设计原则:资源隔离、熔断器和命令模式。

2.1 资源隔离

     在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如: 商品详情展示服务会依赖商品服务, 价格服 务, 商品评论服务. 如图所示:

      

        Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩. 如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务 的调用.

      

      线程池隔离的几点好处:

 

  • 使用超时返回的机制,避免同步调用服务时,调用时间过长,无法释放,导致资源耗尽的情况
  • 服务方可以控制请求数量,请求过多,可以直接拒绝,达到快速失败的目的;
  • 请求排队,线程池可以维护执行队列,将请求压到队列中处理

 

       但是使用线程池隔离也有一些弊端, 线程池隔离模式,会根据服务划分出独立的线程池,系统资源的线程并发数是有限的,当线程数过多,系统话费大量的CPU时间来做线程上下文切换的无用操作,反而降低系统性能;如果线程池隔离的过多,会导致真正用于接收用户请求的线程就相应地减少,系统吞吐量反而下降;在实践上,应当对像远程方法调用,网络资源请求这种服务时间不太可控的场景下使用线程池隔离模式处理。

2.2  熔断器模式

熔断器模式定义了熔断器开关相互转换的逻辑:

 

服务的健康状况 = 请求失败数 / 请求总数. 熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.
当熔断器开关打开时, 请求被禁止通过.
当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.

2.3 命令模式

 

        Hystrix使用命令模式(继承HystrixCommand类或者是HystrixObservableCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).
同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. 如下代码所示:

public class Service1HystrixCommand extends HystrixCommand<Response> {
private Service1 service;
private Request request;

public Service1HystrixCommand(Service1 service, Request request){
supper(
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(20))//服务线程池数量
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
.withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
))
this.service = service;
this.request = request;
);
}

@Override
protected Response run(){
return service1.call(request);
}

@Override
protected Response getFallback(){
return Response.dummy();
}
}

在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能.

 

2.4 Hystrix的内部处理逻辑

 下图为Hystrix服务调用的内部逻辑:

 

1.构建Hystrix的Command对象, 调用执行方法.

2.Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.

3.若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.

4.若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.

5.若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.

6.若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.

7.若服务执行成功, 返回正常结果.

8.若服务降级方法getFallback执行成功, 则返回降级结果.

9.若服务降级方法getFallback执行失败, 则抛出异常.

2.5 Spring Cloud 下 Hystrix使用要注意的问题

  • Hystrix配置无法动态调节生效。Hystrix框架本身是使用的Archaius框架完成的配置加载和刷新,但是集成自 Spring Cloud下,无法有效地根据实时监控结果,动态调整熔断和系统参数
  • 线程池和Command之间的配置比较复杂,在Spring Cloud在做feigin-hystrix集成的时候,还有些BUG,对command的默认配置没有处理好,导致所有command占用公共的command线程池,没有细粒度控制,还需要做框架适配调整
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值