赞
踩
在分布式微服务架构中,会将服务进行拆分,不同的服务负责各自的业务功能,拆分后也有一个问题?每一个服务都有自己的服务名、ip、端口等,服务越多数量越多时,这样怎么记忆这么多的URL呢? 此外,一些公共性的功能(如认证、鉴权、服务流控等)需要重复在各子模块中自身实现,造成的代码冗余怎么办??
如图,诞生了一个统一网关,它将所有子服务封装起来,外部请求服务时,由网关统一调配URL和转发各个请求到不同的微服务去,并且可以在网关层针对所有公共性的功能作统一的处理,避免冗余。
网关介绍:
本案例项目中,网关的作用:
既然网关作用这么大,我们该怎么实现它呢?就可以借用一下开源组件了。
注意:GateWay跟 Servlet 不兼容,不能打成war包,工程中不能出现Servlet的组件。
根据 官方的原理图:
可知,网关原理大致如下:
请求到达网关后,先经过断言Predicate(主要用来判断一个参数是否符合要求);
判断是否符合某个路由的规则?如果符合,则按规则路由到指定地址,如不符合,则退回请求;
请求和响应可以通过过滤器Filter进行配置,过滤I一些不正常的请求和异常返回。
使用gateway之后,客户端只需要记住一个gateway的地址即可,类似于java变量的作用,用来标识内存地址。不用记录地址,只需要记录变量名称,也好比学校的传达室,从此由他来传达请求。
由于geateway是一个相对独立的个体,而且作用和其安全性不言而喻,因此在我们当前的项目中,集成gateway网关,我们采用再建立一个单独的子模块,实现网关功能。
创建Gateway 微服务,取名smartcar-gateway,如下所示:
在其pom.xml中,引入Gateway依赖,与父类关联。
<parent>
<groupId>com.smart.car.root</groupId>
<artifactId>smartcar-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!--引入gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意:记得如果自动生成的项目中,存在web依赖,请删除掉。因为Gateway跟 Servlet 不兼容。(你知道为什么吗?)
在父类的pom.xml中,增加聚会服务的标识。
这样打包的时候,会起到聚合作用。
Gateway网关的路由过滤器有两种配置方式:
这种用编码方式实现gateway进行路由映射的配置方法,自行百度即可,具体不表述。 但是有一点需要记住,这两种方式都是不支持动态配置的。(在本案例中,采用第一种配置方案)
将application.properties改为application.yml。
server: port: 8008 spring: application: name: samrtcar-geteway cloud: #gateway config gateway: discovery: locator: enabled: true #开启网关映射 lower-case-service-id: true #将请求路径上的服务名配置为小写 filters: #去掉path的前缀 - StripPrefix=1 #配置路由数组 routes: #member子服务 - id: member_service #自定义唯一名称-限流时用到 uri: http://localhost:8003 #映射提供服务的uri predicates: - Path= /member/** #断言,映射路径 # filters: #去掉前缀,视自身URL而定 # - StripPrefix=1 #路由实例 - id: route_baidu uri: http://www.baidu.com predicates: - Query= url,baidu
注意,写的是URI不是URL,你知道两者区别么?我们在配置中,指定当Path中含有member/路径时候,就会路由到localhost:8003的微服务去,也就是member项目的接口去。这样就实现了路由的转发。
参数说明:
StripPrefix=1,是去掉URL的前缀,如果你发现你访问地址老是404,请检查是否需要开启去前缀。
Predicates(断言):一个Java 8的定义。 作用:符合 Predicate 条件的,就使用该路由配置,否则就不管。(用来判断是否符合规范的)。
启动member项目和gateway项目,访问URL。
目前有两种方式可以访问接口,一种是直接访问原服务的URL,一种是访问网关,网关再路由到对应服务URL去。
访问效果:
其次,只要当请求中包含 url=baidu的就会进行匹配和路由到baidu网站了。
测试效果:
上面这种做法是直接在配置文件中配置了路由地址, 但是有个问题?到此,依然可以直接访问其目标服务的接口,那以后别人绕开网关直接访问URL怎么办? 所以就不让直接请求具体的服务,请求都走网关,就是只开放网关的端口,其它微服务的端口都不对外开放-需要直接隐藏起来。
微服务开发,各种服务都是用nacos注册中心来统一管理的,nacos中通过服务名也有映射,那现在我们应该让 gateway 直接去 nacos 中发现服务 ,然后再自动转发到对应的服务去。
因common模块引入了nacos注册中心组件,所以我们可以直接引用common模块。
<!--引入公共工具微服务-->
<dependency>
<groupId>com.smart.car.common</groupId>
<artifactId>smartcar-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<!--排出servlet容器-->
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</exclusion>
</exclusions>
</dependency>
注意:Gateway跟 Servlet 不兼容,所以网关服务中不能出现spring-web类的依赖。(你知道为什么吗?)
启动类上添加开启nacos的@EnableDiscoveryClient注解
@RefreshScope
@EnableDiscoveryClient
@SpringBootApplication
public class SmartGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SmartGatewayApplication.class, args);
}
}
在yml中配置nacos地址,还需要配置新的路由方式。
在gateway中配置uri配置有三种方式,包括
其中ws和http方式不容易出错,因为http格式比较固定,但是lb方式比较灵活自由,且使用 uri: lb:// 方式配置时,服务名有特殊要求(见文末)。
我们采用lb的方式配置,如下:
server: port: 8008 spring: application: name: samrtcar-geteway cloud: #nacos config nacos: discovery: server-addr: 127.0.0.1:8848 register-enabled: true #gateway config gateway: discovery: locator: enabled: true #开启网关映射 lower-case-service-id: true #将请求路径上的服务名配置为小写 filters: #去掉path的前缀 - StripPrefix=1 #配置路由数组 routes: #member子服务 - id: member_route #自定义唯一名称 uri: lb://smartcar-member # 映射提供服务的uri predicates: - Path= /member/** #断言,映射路径 # filters: #去掉前缀,视URL而定 # - StripPrefix=1
当选择了nacos做服务的映射,则不需要考虑前缀问题,会自动处理。
启动member8003和80013两个服务和gateway8008一个服务,如图:
然后查看naocs注册列表:
通过网关访问接口:http://localhost:8008/member//list ,
你会发现在,接口在 轮询 请求8003和8004端口的member服务,不光证明了可以通过网关调用服务,还证明了相同服务的负载均衡成功。
这样以后,前端直接访问网关,不必再关心子服务的服务名、服务端口,路由地址等情况,即使是我们修改了其他服务的端口号,也不影响前端的调用。
网关虽然仅仅是转发路由到对应服务,但是也是请求的入口处,如果对应的服务宕机了,请求就会一致堆积在网关处,为了不引起网关的阻塞,我们需要在网关处,配置熔断机制。
本实例降级方案,就采用hystrix来快速熔断吧。在前文中我说过,真正的业务开发上,很多都是多组件相互协助,并不是仅仅靠谁就可以独立完成。
因此,网关的熔断,我们用hystrix来快速完成工作。
1.引入hystrix依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.设置熔断超时时间设置
#hystrix超时时间,默认时间为1000ms
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
3.新增降级兜底方法
熔断后,会由gateway层提供一个快速失败的方法返回给请求的调用方,我们也可称为降级过滤器。
@Slf4j
@RestController
@RequestMapping("error")
public class FallbackController {
@RequestMapping("/fallback")
public ResponseResult<String> fallback() {
ResponseResult<String> result = new ResponseResult<>();
log.error("Invoke service failed...");
result.setCode(429);
result.setMsg("对应的服务调用失败,快速熔断,进入fallback降级方法。");
return result;
}
}
4.配置指定的过滤器
并在yml配置文件中指向该降级过滤器
#member子服务
- id: member-service #自定义唯一名称-限流时用到
uri: lb://smartcar-member # 映射提供服务的uri
predicates:
- Path= /member/** #断言,映射路径
filters:
#结合Hystrix对服务宕机做fallback机制(降级过滤器)
- name: Hystrix
args:
name: fallback
fallbackUri: forward:/error/fallback
还有一种情况,假如服务是暂时的不可用,发起重试之后又能调用正常返回结果,则可以设置重试次数,来确保服务的可用性,我们称为重试过滤器。
配置如下:
fallbackUri: forward:/error/fallback
#重试次数(重试过滤器)(若两者都配置了,降级过滤器需要配在重试过滤器之前)
- name: Retry
args:
#重试3次,加上初次访问,正确执行应当是4次访问
retries: 3
statuses:
- OK
methods:
- GET
- POST
这就需要你在对应的调用方法上,人为增加一个异常,才会触发。本案例不详说。
最终给出完整的yml内容:
server: port: 8008 servlet: context-path: /gateway #logback配置 logging: level: root: info #关闭nacos心跳日志 com.alibaba.nacos.client.*: WARN #org.springframework:cloud.gateway: debug file: name: ./tmp/gateway.log spring: application: name: samrtcar-geteway cloud: #nacos config nacos: discovery: server-addr: 127.0.0.1:8848 register-enabled: true #gateway config gateway: #开启网关映射 discovery: locator: enabled: true lower-case-service-id: true #将请求路径上的服务名配置为小写 filters: #去掉path的前缀,视自身URL而定 - StripPrefix=1 #配置路由数组 routes: #member子服务 - id: member-service #自定义唯一名称-限流时用到 uri: lb://smartcar-member # 映射提供服务的uri predicates: - Path= /member/** #断言,映射路径 filters: #结合Hystrix对服务宕机做fallback机制(降级过滤器) - name: Hystrix args: name: fallback fallbackUri: forward:/error/fallback #重试次数(重试过滤器)(若两者都配置了,位置不可变) - name: Retry args: #重试3次,加上初次访问,正确执行应当是4次访问 retries: 3 statuses: - OK methods: - GET - POST #points子服务 - id: points-service #自定义唯一名称-限流时用到 uri: lb://smartcar-points # 映射提供服务的uri predicates: - Path= /points/** #断言,映射路径 #hystrix超时时间,默认时间为1000ms hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2000
5.启动gateway项目,停掉member项目,让其宕机不可用,如果调用返回错误,或者超时的话,进入降级处理,如果调用成功,则返回结果。
这样,如果遇到服务宕机或者超时情况,gateway网关就不会再去请求该服务,会直接调用对应的fallback方法,实现快速响应。
sentinel可以作为各微服务的限流,也可以作为gateway网关的限流组件。
虽然spring cloud gateway自带了限流功能,但此处用sentinel来作为替待,可以更好的管控。
SpringCloud Gateway原生限流,主要基于过滤器实现,我们可以直接使用内置的过滤器RequestRateLimiterGatewayFilterFactory。这里就不说了,因为大部分情况下,是不采用它的。
本段落介绍Spring Cloud Gateway集成Sentinel和Nacos实现网关的动态限流。
Sentinel从 1.6.0 版本之后,提供两种资源维度的限流:
1.引入依赖如下:
<!--集成sentinel持久化到nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--集成sentinel网关层限流start--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--sentinel限流规则持久化 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--集成sentinel网关层限流end-->
由于需要使用 nacos作为sentinel的配置中心,所以也引入了sentinel-datasource-nacos 引入nacos分布式配置后,必须在bootstrap.yml文件中声明,否则无法获取相应的数据。
2.新建bootstrap.yml:
#nacos配置中心
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
3.在application.yml文件中添加对应的sentinel与集成的功能
spring:
cloud:
sentinel:
transport:
dashboard: 10.0.10.48:8858
eager: true
datasource:
ds:
nacos:
server-addr: 10.0.10.48:8848
data-id: gateway-sentinel-flow
group-id: DEFAULT_GROUP
rule-type: gw-flow
这里主要是sentinel的相关配置,从nacos配置中心获取 gateway-sentinel-flow 配置文件,限流类型是网关类型gw-flow或者是flow。
参数说明如下:
4.编写简单的测试类
@Slf4j
@RestController
@RequestMapping("sentinel")
public class SentinelTestController {
@PostMapping("flowTest")
public String test() {
String dString = UUID.randomUUID().toString();
log.info("产生的结果是:{}",dString);
return dString;
}
}
5.限流配置
前一篇,写了可以在nacos中配置流控规则,然后会自动同步到sentinel中去,因此我们也在试试效果。在nacos控制台中建立对应的配置文件smartcar-gateway-sentinel-nacos,格式为json格式,如下内容:
[ { "resource": "member-service", //服务路由的名称 "limitApp": "default", "grade": 1, "count": 2, "strategy": 0, "controlBehavior": 0, "clusterMode": false }, { "resource": "flowTest", //网关本地接口的名称 "limitApp": "default", "grade": 1, "count": 3, "strategy": 0, "controlBehavior": 0, "clusterMode": false } ]
网关限流规则,如图:
即使服务进行重启,nacos中的限流规则也不会再丢失。
6.配置完成以后启动网关项目,登录sentinel控制台,查看限流规则:
对应关系:
7.运行项目,测试限流结果,发起get请求:http://localhost:8008/member/list
当触发限流后页面显示的是Blocked by Sentinel: FlowException。
(这个原理是DefaultBlockRequestHandler,实现了BlockRequesthandler接口)
100个并发中,只要前两个成功,其它都被拦截,也可以直接在sentinel控制台中看到直观的监测结果:
关于在 Gateway 用 Sentinel还是 Hystrix 的问题?
其实两种可以搭配使用的,一个做限流的,一个做快速失败的,没有什么工具是全面的,懂得配合就好。
关于网关的限流或熔断更多的方式配置,可参考 sentinel下网关规则配置。
小总结:
网关的熔断降级:在分布式系统中,网关作为流量的入口,大量请求进入网关,向后端远程系统或服务发起调用,后端服务不可避免的会产生调用失败(超时或异常),失败时不能让请求堆积在网关上,需要快速失败并返回回去,这就需要在网关上做熔断、降级操作。
网关的限流:网关上有大量请求,对指定服务进行限流,可以很大程度上提高服务的可用性与稳定性,限流的目的是通过对并发访问/请求进行限速,或对一个时间窗口内的请求进行限速来保护系统。一旦达到限制速率则可以拒绝服务、排队或等待、降级操作。
当触发限流后,接口显示的是 Blocked by Sentinel: FlowException。 为了展示更加友好的限流提示, Sentinel支持自定义异常处理。同样,也有两种方案可以使用。
properties、yml配置文件类
spring.cloud.sentinel.scg.fallback.mode = response
spring.cloud.sentinel.scg.fallback.response-body = '{"code":429,"mes":"限流了"}'
或:
spring:
cloud:
sentinel:
scg:
fallback:
mode: response
response-body: '{"code":403,"msg":"不好意思,请稍后再试(您被限流了)"}'
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.HashMap; @Configuration public class GatewayConfiguration { //自定义限流异常页面 @PostConstruct public void initBlockHandlers(){ BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map map = new HashMap(); map.put("code",0); map.put("msg","被限流了"); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject(map)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } }
图示:
当触发流控规则后,就会达到自定义异常的效果了。(图片是后补的,没有user)
其实,有两种方式来实现网关这层的限流。
系统架构基本是前、后端独立部署,到时候会直接引发一个问题:跨域请求。
前端通过网关入口,去调用其它微服务时,通常会出现如下错误:
Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been been blocked by CORS policy
所以,必须要在网关层配置支持跨域,不然无法将请求路由到正确的处理节点,常见的有两种解决方式。
新增一个配置类配置
@Configuration public class CORSConfiguration { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(Boolean.TRUE); //config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addExposedHeader("setToken"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
只需要在 application.yml 配置文件中添加:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTION
(最简单的方式。 本文也是采用的这种。)
就注意几点:
既在网关里边来解决跨域问题,就不能在后面的其他服务里边,再重复引入解决跨域的配置,否则会导致跨域失效,出现错误。
若要求跨域请求必须携带cookie时,则请求头中需要设置"Access-Control-Allow-Credentials:true" 这个属性。
allowedOrigins(“*”) 中的 *号,不能和allowCredentials(true) 同时存在。
好了,到这里,本章节就讲完了,关于Gateway网关,还是有很多值得单独去学习的地方,希望读者朋友们,可以自行多学习和大胆尝试。
1.启动网关报错:Failed to configure a DataSource: ‘url’ attribute is not specified and no embedded datasource could be configured.
那是因为我们的common包中包含了mysql依赖,但是你又没在yml指定mysql的连接信息。 因为我们网关不需要配置,只需要在启动类上声明一下即可:
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
2.uri为什么前面要加lb:呢?
spring:
cloud:
gateway:
routes: #路由配置
uri: lb://provider #目标路径
因为在org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties类中限制了前缀。
注意,它有2个特殊要求:
1.能被gateway的lb方式识别到的命名规则为:
"[a-zA-Z]([a-zA-Z]|\\d|\\+|\\.|-)*:.*"
这也意味着,java命名规范中可以使用的英文下划线(“_”),在这里会不能识别。
你会见到这样的报错异常信息:
java.lang.IllegalStateException: Invalid host: lb://brilliance_consumer
2.lb方式用的是注册中心,只要你的服务能注册到同一个注册中心,就没问题,不在同一个注册中心,无法调通。 要求gateway和其他微服务在一个注册中心中。
3.在nacos + gateway的基础上,如何实现负载平衡?关掉其中一个provider,立即通过gateway进行轮询,发现gateway仍然会call已经down掉的provider,导致查询失败。但在大约5秒后,轮询恢复正常,不再call已经挂掉的prodiver,这个现象是为什么?
4.什么是Api分组?作用是什么?
ps: 最后说两点小常识:
1.网关的核心概念就是路由配置和路由规则,且作为所有请求流量的入口,在实际生产环境中要保证高可靠和高可用,尽量要避免重启,所以实现动态路由是非常有必要的;
2.Api网关结合ribbon完成负载均衡和动态路由,还是比较复杂的,我不建议刚入门的朋友学习,所以不写在本教程中了。将会单独写成博客,感兴趣的朋友单独搜索即可。
(内容看来有点多,后期我将考虑拆分多章节来讲吧,如果效果不好的话,有什么意见请评论区踊跃提出~)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。