17-Sentinel主流框架适配

原创 吴就业 133 0 2020-09-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/5a278410f786445b9c4b77800eeaf7fc

作者:吴就业
链接:https://wujiuye.com/article/5a278410f786445b9c4b77800eeaf7fc
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

使用Sentinel需要用try-catch-finally将需要保护的资源(方法或者代码块)包装起来,在目标方法或者代码块执行之前,调用ContextUtil#enter方法以及SphU#entry方法,在抛出异常时,如果非BlockException异常需要调用Tracer#trace记录异常,修改异常指标数据,在finally中需要调用Entry#exit方法,以及ContextUtil#exit方法。

为了节省这些步骤,Sentinel提供了对主流框架的适配,如适配Spring MVC、Webflux、Dubbo、Api Gateway等框架。当然,对于Sentinel未适配的框架,我们也可以自己实现适配器。在Sentinel源码之外,alibaba的spring-cloud-starter-alibaba-sentinel也为Sentinel提供与OpenFeign框架整合的支持。

17-01-sentinel-adapter

Spring MVC适配器

Sentinel借助Spring MVC框架的HandlerInterceptor适配Spring MVC,但也需要我们借助WebMvcConfigurer将SentinelWebInterceptor注册到Spring MVC框架。

使用步骤

第一步:在项目中添加spring mvc适配模块的依赖。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-webmvc-adapter</artifactId>
    <version>${version}</version>
</dependency>

第二步:编写WebMvcConfigurer,在addInterceptors方法中注入SentinelWebInterceptor。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        SentinelWebMvcConfig config = new SentinelWebMvcConfig();
        config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());
        config.setHttpMethodSpecify(true);
        config.setOriginParser(request -> request.getHeader("S-user"));
        // SentinelWebInterceptor拦截所有接口("/**")
        registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
    }
}

在创建SentinelWebInterceptor时,可为SentinelWebInterceptor添加配置,使用SentinelWebMvcConfig封装这些配置:

适配原理

Spring MVC框架的方法拦截器(HandlerInterceptor)的定义如下。

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

HandlerInterceptor在DispatcherServlet#doDispatch方法中被调用,每个方法的调用时机如下:

因此,Sentinel可借助HandlerInterceptor与Spring MVC框架整合,在HandlerInterceptor#preHandle方法中调用ContextUtil#enter方法以及SphU#entry方法,在afterCompletion方法中根据方法参数ex是否为空处理异常情况,并且完成Entry#exit方法、ContextUtil#exit方法的调用。

SentinelWebInterceptor是AbstractSentinelInterceptor的子类,preHandle与afterCompletion方法在父类中实现,自身只实现父类定义的一个获取资源名称的抽象方法,其源码如下。

    @Override
    protected String getResourceName(HttpServletRequest request) {
        // (1)
        Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        if (resourceNameObject == null || !(resourceNameObject instanceof String)) {
            return null;
        }
        String resourceName = (String) resourceNameObject;
        // (2)
        UrlCleaner urlCleaner = config.getUrlCleaner();
        if (urlCleaner != null) {
            resourceName = urlCleaner.clean(resourceName);
        }
        // (3)
        if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) {
            resourceName = request.getMethod().toUpperCase() + ":" + resourceName;
        }
        return resourceName;
    }

资源名称生成过程如下:

因为有些接口是这样的:“/hello/{name}”,如果直接从HttpServletRequest获取请求路径,那么每个请求获取到的URL就可能会不同。

UrlCleaner用于实现将多个接口合并为一个,例如接口:“/user/create”、“/user/del”、“/user/update”,借助UrlCleaner修改资源名称将这几个接口都改为“/user/**”即可实现三个接口使用同一个限流规则。

一般来说,不建议使用,因为如果接口使用@RequestMapping声明,那么想对该接口限流就需要配置多个限流规则,而一般旧项目多是使用@RequestMapping声明接口方法。例如接口“/user/create”,你可能需要针对“GET:/user/create”、“POST:/user/create”等多个资源配置限流规则。

由于AbstractSentinelInterceptor的源码较多,我们分几个步骤分析。

AbstractSentinelInterceptor#preHandle方法源码如下:

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        try {
            //(1)
            String resourceName = getResourceName(request);
            if (StringUtil.isNotEmpty(resourceName)) {
                //(2)
                String origin = parseOrigin(request);
                //(3)
                ContextUtil.enter(SENTINEL_SPRING_WEB_CONTEXT_NAME, origin);
                //(4)
                Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                //(5)
                setEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName(), entry);
            }
            return true;
        } catch (BlockException e) {
            // (6)
            handleBlockException(request, response, e);
            return false;
        }
    }

AbstractSentinelInterceptor#handleBlockException方法源码如下:

protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e)
        throws Exception {
        if (baseWebMvcConfig.getBlockExceptionHandler() != null) {
            baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e);
        } else {
            throw e;
        }
    }

如果我们给SentinelWebMvcConfig配置了BlockExceptionHandler,则调用BlockExceptionHandler#handle方法处理BlockException异常,否则将异常抛出,由全局处理器处理。

AbstractSentinelInterceptor#afterCompletion方法源码如下:

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        //(1)
        Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
        if (entry != null) {
            //(2)
            traceExceptionAndExit(entry, ex);
            removeEntryInRequest(request);
        }
        //(3)
        ContextUtil.exit();
    }

AbstractSentinelInterceptor#traceExceptionAndExit方法源码如下:

   protected void traceExceptionAndExit(Entry entry, Exception ex) {
        if (entry != null) {
            if (ex != null) {
                Tracer.traceEntry(ex, entry);
            }
            entry.exit();
        }
    }

当方法执行抛出异常时,调用Tracer#traceEntry方法记录异常,更新异常指标数据。

OpenFeign适配器

Sentinel整合OpenFeign主要用于实现熔断降级,因此,关于OpenFeign的Sentinel适配器的使用介绍基于服务消费端。

使用步骤

1、引入依赖

借助spring-cloud-starter-alibaba-sentinel实现与OpenFeign整合,添加依赖配置如下。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

2、启用OpenFeign整合Sentinel的自动配置

在application.yaml配置文件中添加如下配置,启用Sentinel与OpenFeign整合适配器。

feign:
  sentinel:
    enabled: true

3、熔断降级规则配置

可基于动态数据源实现,也可直接调用DegradeRuleManager的loadRules API硬编码实现,可参考上一篇。

4、给@FeignClient注解配置异常回调

给接口上的@FeignClient注解配置fallback属性,实现请求被拒绝后的处理。

@FeignClient(
         //.....
        // 这里配置
        fallback = ServiceDegradeFallback.class)
public interface DemoService {

    @PostMapping("/services")
    ListGenericResponse<DemoDto> getServices();

}

fallback属性要求配置一个类,该类必须实现相同的接口,所以ServiceDegradeFallback必须实现DemoService接口。

public class ServiceDegradeFallback implements DemoService {
    @Override
    public ListGenericResponse<DemoDto> getServices() {
        ListGenericResponse response = new ListGenericResponse<DemoDto>();
        response.setCode(ResultCode.SERVICE_DEGRAD.getCode())
                .setMessage("服务降级");
        return response;
    }
}

ServiceDegradeFallback类中处理接口降级逻辑,例如,响应一个状态码告知消费端由于服务降级本次接口调用失败。

最后还需要将该ServiceDegradeFallback注册到Feign的Clinet环境隔离的容器中。

编写配置类SentinelFeignConfig,在SentinelFeignConfig中注册ServiceDegradeFallback。

public class SentinelFeignConfig {
    @Bean
    public ServiceDegradeFallback degradeMockYcpayService() {
        return new ServiceDegradeFallback();
    }
}

将SentinelFeignConfig配置类添加到@FeignClient注解的configuration属性,如下。

@FeignClient(
        // .....
        configuration = {
                // 这里配置
                SentinelFeignConfig.class
        },
        // 这里配置
        fallback = ServiceDegradeFallback.class)
public interface DemoService {

    @PostMapping("/services")
    ListGenericResponse<DemoDto> getServices();

}

当满足熔断条件时,Sentinel会抛出一个DegradeException异常,如果配置了fallback,那么Sentinel会从Bean工厂中根据fallback属性配置的类型取一个Bean并调用接口方法。

Sentinel与OpenFeign整合实现原理

当Sentinel与OpenFeign、Ribbon整合时,客户端向服务端发起一次请求的过程如下图所示。

17-02-openfeign-sentinel

可见,Sentinel处在接口调用的最前端,因此Sentinel统计的指标数据即不会受Ribbon的重试影响也不会受OpenFeign的重试影响。

Sentinel通过自己提供InvocationHandler替换OpenFeign的InvocationHandler实现请求拦截。SentinelInvocationHandler源码调试如下图所示。

17-02-openfeign-sentinel02

InvocationHandler是OpenFeign为接口生成JDK动态代理类时所需要的,是接口的方法拦截处理器,Sentinel通过替换OpenFeign的InvocationHandler拦截方法的执行,在OpenFeign处理接口调用之前完成熔断降级的检查。

那么,Sentinel是如何将原本的FeignInvocationHandler替换为SentinelInvocationHandler的呢?

OpenFeign通过Feign.Builder类创建接口的代理类,所以Sentinel直接将Feign.Builder也替换成了SentinelFeign.Builder,由SentinelFeignAutoConfiguration自动配置类向Spring的Bean容器注入SentinelFeign.Builder,代码如下。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {

	@Bean
	@Scope("prototype")
	@ConditionalOnMissingBean
	@ConditionalOnProperty(name = "feign.sentinel.enabled")
	public Feign.Builder feignSentinelBuilder() {
		return SentinelFeign.builder();
	}

}

SentinelFeign.Builder继承Feign.Builder并重写build方法,SentinelFeign.Builder#build方法源码如下。

public final class SentinelFeign {

	public static Builder builder() {
		return new Builder();
	}

	public static final class Builder extends Feign.Builder
			implements ApplicationContextAware {

	   // .....

		@Override
		public Feign build() {
			super.invocationHandlerFactory(new InvocationHandlerFactory() {
				@Override
				public InvocationHandler create(Target target,
						Map<Method, MethodHandler> dispatch) {
					// 创建SentinelInvocationHandler
				}
			});
			super.contract(new SentinelContractHolder(contract));
			return super.build();
		}
		// .....
	}

}

SentinelFeign.Builder#build偷天换日,替换了InvocationHandlerFactory,所以OpenFeign调用InvocationHandlerFactory#create方法创建的InvocationHandler就变成了SentinelInvocationHandler。

看InvocationHandlerFactory#create方法的返回值类型我们也能知道,该方法负责创建SentinelInvocationHandler。create方法部分源码如下:

Class fallback = (Class) getFieldValue(feignClientFactoryBean,
							"fallback");
Object fallbackInstance = getFromContext(beanName, "fallback", fallback,
								target.type());
return new SentinelInvocationHandler(target, dispatch,
								new FallbackFactory.Default(fallbackInstance));

在创建SentinelInvocationHandler之前,通过反射从FeignClientFactoryBean拿到@FeignClient注解的fallback属性值,然后根据fallback类型从Bean工厂取得fallback实例,将fallback实例传递给SentinelInvocationHandler。当触发熔断时,SentinelInvocationHandler就能取得fallback实例并调用。

总结

本篇我们分析了Sentinel适配Spring MVC框架的实现原理,以及Sentinel适配Spring Cloud OpenFeign框架的实现原理。适配各种主流框架,无非就是通过框架提供的方法拦截器注入Sentinel,或者通过拦截主流框架的入口方法注入Sentinel。了解原理之后,如果我们项目中使用的框架Sentinel并未适配,那么我们也可以自己实现适配器。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Data R2DBC快速上手指南

本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。

使用Spring WebFlux + R2DBC搭建消息推送服务

消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。

教你如何编写一个IDEA插件,并掌握核心知识点PSI

IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。

Spring Boot实现加载自定义配置文件

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。