• 微服务系统设计——API 网关服务设计


    摘要

    由于服务粒度的不同以及数据包装因端而异的差异需求,由于的系统采用的是的前后端分离的结构。调用端可以直接调用 BFF 层,由 BFF 层再将请求分发至不同微服务,进行数据组装。由于很多子服务都需要用户验证、权限验证、流量控制等,真的要在每个子服务中重复编写用户验证的逻辑吗?在大型微服务设计时候都在网关层统一处理这些共性需求。

    一、网关设计

    如果没有网关的情况下,服务调用面临的几个直接问题:

    1. 每个服务都需要独立的认证,增加不必要的重复度。
    2. 客户端直接与服务对接,后端服务一旦变动,前端也要跟着变动,独立性缺失。
    3. 将后端服务直接暴露在外,服务的安全性保障是一个挑战。
    4. 某些公共的操作,如日志记录等,需要在每个子服务都实现一次,造成不必要的重复劳动。

    现有系统的调用结构如下图所示:

    直接由前端发起调用,服务间的调用可以 由服务注册中心调配,但前端调用起来就没这么简单了,特别是后端服务以多实例的形态出现时。由于各个子服务都有各自的服务名、端口号等,加之某些共性的东西(如鉴权、日志、服务控制等)重复在各子模块实现,造成不必要的成本浪费。此时,就亟需一个网关,将所有子服务包装后,对外统一提供服务,并在网关层针对所有共性的功能作统一处理,大大提高服务的可维护性、健壮性。引入网关后,请求的调用结构演变成如下图:

    可以看到明显的变化:由网关层进行统一的请求路由,将前端调用的选择权解放出来;后端服务隐藏起来,对外只能看到网关的地址,安全性大大提升;一些共性操作,直接由网关层实现,具体服务实现不再承担这部分工作,更加专心于业务实现。本文带你将 spring-cloud-gateway 组件引入项目中,有同学会问,为什么不用 Zuul 呢?答案是由于组件发展的一些原因,Zuul 进入了维护期,为保证组件的完整性,Spring 官方团队开发出 Gateway 以替代 Zuul 来实现网关功能。

    一、建立 Gateway 服务

    引入 jar 时,注意 Spring Cloud Gateway 是基于 Netty 和 WebFlux 开发,所以不需要相关的 Web Server 依赖,如 Tomcat 等,WebFlux 与 spring-boot-starter-web 是冲突的,需要将这两项排除,否则无法启动。

    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-gateway</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>org.springframework.boot</groupId>
    7. <artifactId>spring-boot-starter-actuator</artifactId>
    8. </dependency>
    9. <dependency>
    10. <groupId>org.springframework.cloud</groupId>
    11. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    12. <version>0.2.2.RELEASE</version>
    13. </dependency>

    启动类与正常业务模块无异,在 application.yml 配置文件中进行初步配置:

    1. server:
    2. port: 10091
    3. management:
    4. endpoints:
    5. web:
    6. exposure:
    7. include: '*'
    8. #nacos config
    9. spring:
    10. application:
    11. name: gateway-service
    12. cloud:
    13. nacos:
    14. discovery:
    15. register-enabled: true
    16. server-addr: 127.0.0.1:8848
    17. # config:
    18. # server-addr: 127.0.0.1:8848
    19. gateway:
    20. discovery:
    21. locator:
    22. enabled: false #gateway 开启服务注册和发现的功能,并且自动根据服务发现为每一个服务创建了一个 router,这个 router 将以服务名开头的请求路径转发到对应的服务。
    23. lowerCaseServiceId: true #是将请求路径上的服务名配置为小写
    24. filters:
    25. - StripPrefix=1
    26. routes:
    27. #一个服务中的 id、uri、predicates 是必输项
    28. #member 子服务
    29. - id: member-service
    30. uri: lb://member-service
    31. predicates:
    32. - Path= /member/**
    33. filters:
    34. - StripPrefix=1
    35. #card 子服务
    36. - id: card-service
    37. uri: lb://card-service
    38. predicates:
    39. - Path=/card/**
    40. filters:
    41. - StripPrefix=1
    42. #resource 子服务
    43. - id: resource-service
    44. uri: lb://resource-service
    45. predicates:
    46. - Path=/resources/**
    47. filters:
    48. - StripPrefix=1
    49. #计费子服务
    50. - id: charging-service
    51. uri: lb://charging-service
    52. predicates:
    53. - Path=/charging/**
    54. filters:
    55. - StripPrefix=1
    56. #finance 子服务
    57. - id: finance-service
    58. uri: lb://finance-service
    59. predicates:
    60. - Path=/finance/**
    61. filters:
    62. - StripPrefix=1

    routes 配置项是具体的服务路由规则配置,各服务以数组形式配置。id 用于服务间的区分,uri 则对应直接的调用服务,lb 表示以负载的形式访问服务,lb 后面配置的是 Nacos 中的服务名。predicates 用于匹配请求,无须再用服务的形式访问。到此完成 Gateway 网关服务的简单路由功能已完成,前端直接访问网关调用对应服务,不必再关心子服务的服务名、服务端口等情况。

    二、熔断降级

    有服务调用章节,我们通过 Hystrix 实现了服务降级,在网关层面是不是可以做一个统一配置呢?答案是肯定的,下面我们在 Gateway 模块中引入 Hystrix 来进行服务设置,当服务超时或超过指定配置时,直接快速返回准备好的异常方法,快速失败,实现服务的熔断操作。引入相关的 jar 包:

    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    4. </dependency>

    配置文件中设置熔断超时时间设置:

    1. #timeout time config,默认时间为 1000ms,
    2. hystrix:
    3. command:
    4. default:
    5. execution:
    6. isolation:
    7. thread:
    8. timeoutInMilliseconds: 2000

    编写异常响应类,此类需要配置在子服务的失败调用位置。

    1. @RestController
    2. @RequestMapping("error")
    3. @Slf4j
    4. public class FallbackController {
    5. @RequestMapping("/fallback")
    6. public CommonResult<String> fallback() {
    7. CommonResult<String> errorResult = new CommonResult<>("Invoke failed.");
    8. log.error("Invoke service failed...");
    9. return errorResult;
    10. }
    11. }
    12. #card 子服务
    13. - id: card-service
    14. uri: lb://card-service
    15. predicates:
    16. - Path=/card/**
    17. filters:
    18. - StripPrefix=1
    19. #配置快速熔断失败调用
    20. - name: Hystrix
    21. args:
    22. name: fallbackcmd
    23. fallbackUri: forward:/error/fallback

    若服务暂时不可用,发起重试后又能返回正常,可以通过设置重试次数,来确保服务的可用性。

    1. #card 子服务
    2. - id: card-service
    3. uri: lb://card-service
    4. predicates:
    5. - Path=/card/**
    6. filters:
    7. - StripPrefix=1
    8. - name: Hystrix
    9. args:
    10. name: fallbackcmd
    11. fallbackUri: forward:/error/fallback
    12. - name: Retry
    13. args:
    14. #重试 3 次,加上初次访问,正确执行应当是 4 次访问
    15. retries: 3
    16. statuses:
    17. - OK
    18. methods:
    19. - GET
    20. - POST
    21. #异常配置,与代码中抛出的异常保持一致
    22. exceptions:
    23. - com.mall.parking.common.exception.BusinessException

    如何测试呢?可以代码中增加异常抛出,来测试请求是否重试 3 次,前端调用时,通过网关访问此服务调用,可以发现被调用次数是 4 次。

    1. /* 这里抛出异常是为了测试 spring-cloud-gateway 的 retry 机制是否正常运行
    2. * if (StringUtils.isEmpty("")) {
    3. throw new BusinessException("test retry function");
    4. }*/

    三、服务限流

    为什么要限流,当服务调用压力突然增大时,对系统的冲击是很大的,为保证系统的可用性,做一些限流措施很有必要。常见的限流算法有令牌桶、漏桶等,Gateway 组件内部默认实现了 Redis + Lua 进行限流,可以通过自定义的方式来指定是根据 IP、用户或是 URI 来进行限流,下面我们来一控究竟。Spring Cloud Gateway 默认提供的 RedisRateLimter 的核心逻辑为判断是否取到令牌的实现,通过调用 META-INF/scripts/request_rate_limiter.lua 脚本实现基于令牌桶算法限流,我们来看看如何借助这个功能来达到我们的目的。

    引入相应 jar 包的支持:

    1. <!--基于 reactive stream 的 redis -->
    2. <dependency>
    3. <groupId>org.springframework.boot</groupId>
    4. <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    5. </dependency>

    配置基于 IP 进行限流,比如在商场兑换优惠券时,在固定时间内,仅有固定数量的商场优惠券来应对突然间的大量请求,很容易出现高峰交易的情况,导致服务卡死不可用。

    1. - name: RequestRateLimiter
    2. args:
    3. redis-rate-limiter.replenishRate: 3 #允许用户每秒处理多少个请求
    4. redis-rate-limiter.burstCapacity: 5 #令牌桶的容量,允许在一秒钟内完成的最大请求数
    5. key-resolver: "#{@remoteAddrKeyResolver}" #SPEL 表达式去的对应的 bean

    上文的 KeyResolver 配置项是用来定义按什么规则来限流,比如本次采用 IP 进行限流,编写对应的实现类实现此接口:

    1. public class AddrKeyResolver implements KeyResolver {
    2. @Override
    3. public Mono<String> resolve(ServerWebExchange exchange) {
    4. return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    5. }
    6. }

    在启动类进行 @Bean 定义:

    1. @Bean
    2. public AddrKeyResolver addrKeyResolver() {
    3. return new AddrKeyResolver();
    4. }

    3.1 测评限流是否生效

    前期我们采用了 PostMan 组件进行了不少接口测试工作,其实它可以提供并发测试功能,不少用过的小伙伴尚未发现这一功能,这里就带大家一起使用 PostMan 来发起并发测试,操作步骤如下。

    建立测试脚本目录

    将测试请求放入目录

    运行脚本

    打开终端

    进入 Redis 对应的库,输入 monitor 命令,监控 Redis 命令的执行情况。点击上图“Run”按钮,查看 Redis 命令的执行情况。查看 Postman 控制台,可以看到有 3 次已经被忽略执行。

    四、跨域支持

    时下流行的系统部署架构基本是前、后端独立部署,由此而直接引发另一个问题——跨域请求。必须要在网关层支持跨域,不然无法将请求路由到正确的处理节点。这里提供两种方式,一种是代码编写,一种是能过配置文件配置,建议采用配置方式完成。

    1. @Configuration
    2. public class CORSConfiguration {
    3. @Bean
    4. public CorsWebFilter corsWebFilter() {
    5. CorsConfiguration config = new CorsConfiguration();
    6. config.setAllowCredentials(Boolean.TRUE);
    7. //config.addAllowedMethod("*");
    8. config.addAllowedOrigin("*");
    9. config.addAllowedHeader("*");
    10. config.addExposedHeader("setToken");
    11. UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
    12. source.registerCorsConfiguration("/**", config);
    13. return new CorsWebFilter(source);
    14. }
    15. }

    配置文件配置

    1. spring:
    2. cloud:
    3. gateway:
    4. discovery:
    5. # 跨域
    6. globalcors:
    7. corsConfigurations:
    8. '[/**]':
    9. allowedHeaders: "*"
    10. allowedOrigins: "*"
    11. # 为保证请求的安全,项目中只支持 get 或 post 请求,其它请求全部屏蔽,以免导致多余的问题
    12. allowedMethods:
    13. - POST

    本文到此,网关中路由配置、熔断失败、请求限流、请求跨域等常见的共性问题都得到初步的解决,相信随着使用的深入,还有更多高阶的功能等待大家去开发使用。

    博文参考

  • 相关阅读:
    docker部署mysql主从备份
    黑马JVM总结(二十七)
    Vue2 与Vue3的区别?面试题
    2022-iOS个人开发者账号申请流程
    如何构建一个外卖微信小程序
    vmware安装 Rocky9(自定义分区安装)
    管理心得--如何成为优秀的架构师
    【毕业季】角色转换
    小白跟做江科大32单片机之按键控制LED
    苹果(Apple)公司的新产品开发流程(一)
  • 原文地址:https://blog.csdn.net/weixin_41605937/article/details/125449179