栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

软件工程应用与实践(10)——网关,服务熔断

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

软件工程应用与实践(10)——网关,服务熔断

2021SC@SDUSC

文章目录
    • 一、概述
    • 二、代码分析
      • 2.1 gateway网关
        • 2.1.2 yml配置
        • 2.1.2 java代码
      • 2.2 服务熔断
    • 三、总结

一、概述

在上一篇文章中,我介绍了微服务治理中的服务注册,配置中心,服务间通信等内容,本博客重点介绍老年健康管理系统中关于网关和服务熔断的内容。在老年健康管理系统中,为了保证服务的可用,防止出现服务雪崩现象,使用了Spring Cloud Alibaba的Sentinel对微服务系统进行保护。在分析具体的源代码之前,我首先查阅了相关资料,这些资料能帮我更好地理解项目的源代码

网关

网关的英文叫做gateway,网关可以用于管理微服务的统一入口,方便实现对平台众多微服务进行管控。在之前单体应用的环境下,后端提供给前端的接口相对较少,可以使用API接口文档进行管理,但随着系统不断扩大,需要一个统一的网关用于管理所有的接口。除此之外网关的作用还非常强大,网关的主要作用如下

  • 统一所有微服务入口
  • 实现请求路由转发,并实现路由转发过程中的负载均衡
  • 对请求进行身份认证,脱敏,并发以及流量控制

经过阅读源码,发现本项目使用Spring Cloud的gateway组件对微服务入口进行统一管理

服务雪崩与服务熔断

服务雪崩是指,在微服务系统服务调用的过程中,由于一个服务故障,导致级联服务故障的情况,成为服务雪崩。服务雪崩的情况十分危险,因为会导致级联服务不可用。

服务熔断是用于解决服务雪崩问题的,当某个服务出现故障时,通过断路器监控,发现某个异常条件被触发,则会直接熔断整个服务,并返回一个备选的,符合预期的响应,直到服务恢复。

经过阅读源码,发现本项目使用Spring Cloud Alibaba的Sentinel对微服务进行相应的保护。

二、代码分析 2.1 gateway网关

在Spring Cloud中,网关一共有两种配置方式,一种是使用yml的方式配置,还有一种是使用java代码的方式进行配置,本项目中两种方法都有使用,首先使用了yml配置了网关的基本信息,又使用java代码配置了网关请求失败的异常信息,接下来我将对这两个部分分别进行分析

2.1.2 yml配置


首先我们查看对应的pom.xml,发现导入了相关的依赖

  • 第一个依赖引入webflux组件
  • 第二个依赖引入我们本次代码分析的核心组件:网关gateway
  • 第三个依赖引入了Spring Boot的测试组件

    org.springframework.boot
    spring-boot-starter-webflux


    org.springframework.cloud
    spring-cloud-starter-gateway


    org.springframework.boot
    spring-boot-starter-test
    test

接下来我们阅读bootstrap.yml和application.yml两个配置文件,可以看到在application.yml中,有关于网关的配置

  • 首先使用default-filters属性,设置DedupeResponseHeader为Access-Control-Allow-Origin,表明网关的配置是允许跨域的,default-filters代表默认的过滤器
  • 接下来的globalcors和下面的配置,同样是对跨域的配置。项目中指定了请求的源,请求方法,请求头等信息
  • 接下来我们关注routes,这个组件是我们本次网关配置的核心。在routes的下一级,我们可以看到有以下几个子属性:id属性,uri属性,order属性,predicates断言属性等
  • id属性用于指明当前路由对象的唯一标识
  • uri属性用于指明当前路由所访问的微服务路径,即类别服务名称
  • predicates的意思是断言,用于配置具体的路由规则,比如在本项目的配置中,就指定了具体的路径,并且通过RequestBody指定了请求体为true,表明要求要有请求体
  • filters的意思是过滤器,用于对发往该请求的路由进行处理。在本项目中,使用StripPrefix=2,用于将发往该网关的请求截去前两个路径。比如一个路径是/name/blue/red,经过截取后转发的路径是/red
cloud:
 gateway:
   default-filters:
      - DedupeResponseHeader=Access-Control-Allow-Origin
   globalcors:
      add-to-simple-url-handler-mapping: true
      corsConfigurations:
        '[
    @NotNull
    private Mono getVoidMono(ServerWebExchange serverWebExchange, baseResponse body, HttpStatus status) {
        serverWebExchange.getResponse().setStatusCode(status);
        byte[] bytes = JSONObject.toJSONString(body).getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = serverWebExchange.getResponse().bufferFactory().wrap(bytes);
        return serverWebExchange.getResponse().writeWith(Flux.just(buffer));
    }
    private void setCurrentUserInfoAndLog(ServerWebExchange serverWebExchange, IJWTInfo user, PermissionInfo pm) {
        String host = serverWebExchange.getRequest().getRemoteAddress().toString();
        LogInfo logInfo = new LogInfo(pm.getMenu(), pm.getName(), pm.getUri(), new Date(), user.getId(), user.getName(), host, String.valueOf(serverWebExchange.getAttributes().get(RequestBodyRoutePredicateFactory.REQUEST_BODY_ATTR)));
        DBLog.getInstance().setLogService(logService).offerQueue(logInfo);
    }
    
    private IJWTInfo getJWTUser(ServerHttpRequest request, ServerHttpRequest.Builder ctx) throws Exception {
        List strings = request.getHeaders().get(userAuthConfig.getTokenHeader());
        String authToken = null;
        if (strings != null) {
            authToken = strings.get(0);
        }
        if (StringUtils.isBlank(authToken)) {
            strings = request.getQueryParams().get("token");
            if (strings != null) {
                authToken = strings.get(0);
            }
        }
        IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(authToken);
        String s = stringRedisTemplate.opsForValue().get(RedisKeyConstant.REDIS_KEY_TOKEN + ":" + infoFromToken.getTokenId());
        if (StringUtils.isBlank(s)) {
            throw new UserTokenException("User token expired!");
        }
        ctx.header(userAuthConfig.getTokenHeader(), authToken);
        baseContextHandler.setToken(authToken);
        return infoFromToken;
    }
    
    private boolean isStartWith(String requestUri) {
        boolean flag = false;
        for (String s : startWith.split(",")) {
            if (requestUri.startsWith(s)) {
                return true;
            }
        }
        return flag;
    }
}

在对应的配置文件中,我们可以看到注入的属性值

gate:
  ignore:
    startWith: /auth/jwt,/auth/captcha

在handler包下的RequestBodyRoutePredicateFactory类,继承了AbstractRoutePredicateFactory类,AbstractRoutePredicateFactory类是一个对路由断言的配置类

  • 使用@Order注解,表明组件的加载顺序。由于在Spring中,组件都注入容器,而这些组件的加载可以由程序员自己指定顺序,在Order注解中,值越小,优先级越高
  • 在本类中,同样定义了一个Log对象,用于输出日志
  • 定义了一个messageReaders对象,用于获取请求的消息
  • 本类中定义了两个构造函数,其中第一个构造函数是空构造函数,首先调用父类的构造函数执行,并将messageReaders设置为默认值
  • 第二个构造函数可以让用户传入messageReaders,并在构造函数内赋值
  • 在applyAsync方法中,对网关的断言进行了相关的配置。使用lambda表达式构造返回值
  • 在apply方法中,用于抛出网关调用时的异常,抛出的异常是UnsupportedOperationException,通过阅读源码可以得知,这个异常类继承了RunTimeException
  • 在本类中,还自定义了一个静态内部类Config,在这个类中,有一个属性sources,他的类型是ArrayList,在这个类中使用了两个set方法和一个get方法对该变量进行赋值。
  • set方式的赋值使用了直接赋List或赋数组的形式,再调用Arrays.asList方法,将数组转为List集合
@Slf4j
@Component
@Order(1)
public class RequestBodyRoutePredicateFactory
        extends AbstractRoutePredicateFactory {
    protected static final Log LOGGER = LogFactory.getLog(RequestBodyRoutePredicateFactory.class);
    private final List> messageReaders;

    public RequestBodyRoutePredicateFactory() {
        super(RequestBodyRoutePredicateFactory.Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }
    public RequestBodyRoutePredicateFactory(List> messageReaders) {
        super(RequestBodyRoutePredicateFactory.Config.class);
        this.messageReaders = messageReaders;
    }
    public static final String REQUEST_BODY_ATTR = "requestBodyAttr";
    @Override
    public AsyncPredicate applyAsync(Config config) {
        return exchange -> {
            if (!"POST".equals(exchange.getRequest().getMethodValue())&&!"PUT".equals(exchange.getRequest().getMethodValue())) {
                return Mono.just(true);
            }
            Object cachedBody = exchange.getAttribute(REQUEST_BODY_ATTR);
            if (cachedBody != null) {
                try {
                    return Mono.just(true);
                } catch (ClassCastException e) {
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Predicate test failed because class in predicate does not match the cached body object",
                                e);
                    }
                }
                return Mono.just(true);
            } else {
                return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), this.messageReaders).bodyToMono(String.class).defaultIfEmpty("").doOnNext((objectValue) -> {
                    if(StringUtils.isBlank(objectValue)){
                        exchange.getAttributes().put(REQUEST_BODY_ATTR, JSON.toJSONString(exchange.getRequest().getQueryParams()));
                    }else {
                        exchange.getAttributes().put(REQUEST_BODY_ATTR, objectValue);
                    }
                }).map((objectValue) -> true));
            }
        };
    }
    @Override
    public Predicate apply(Config config) {
        throw new UnsupportedOperationException(
                "ReadBodyPredicateFactory is only async.");
    }
    public static class Config {
        private List sources = new ArrayList<>();
        public List getSources() {
            return sources;
        }
        public RequestBodyRoutePredicateFactory.Config setSources(List sources) {
            this.sources = sources;
            return this;
        }
        public RequestBodyRoutePredicateFactory.Config setSources(String... sources) {
            this.sources = Arrays.asList(sources);
            return this;
        }
    }
}
2.2 服务熔断

在本项目中,使用了Sentinel组件对服务进行保护

首先通过阅读pom.xml文件,里面引入了这个组件的相关依赖


    com.alibaba.cloud
    spring-cloud-starter-alibaba-sentinel

在配置文件中,同样对Sentinel组件进行了配置

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080

这里配置的是sentinel组件dashboard的端口号

@Bean
@SentinelRestTemplate
public RestTemplate restTemplate() {
    return new RestTemplate();
}

可以看到,在RestTemplate对象的地方,加上了@SentinelRestTemplate注解,对该对象加上了流量控制管理。

关于服务熔断的部分,本项目中的源码内容较少,而Spring Cloud Alibaba的Sentinel组件,提供了非常强大的功能,仅需引入依赖,修改配置,再开启项目后调用对应的DashBoard,即可使用UI查看当前服务的各种状态

三、总结

在本篇博客中,我主要对网关和服务熔断部分的内容进行了相关的代码分析,总的来说,这一部分代码对理解后台微服务的相关内容有很多帮助。总的来说,在这次的源代码分析中,我收获了很多东西,对微服务,分布式的相关理解又更近了一步。希望在未来的学习中,能充分应用上这个技术。在这个过程中,感谢老师和小组成员对我的帮助。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/631011.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号