在公司微服务化过程中,不同的服务一般会有不同的网络地址,而客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,显然不合理。
介绍
目前国内微服务框架一般就两套,一个是SpringCloud,另外一个SpringAlibaba(Dubbo)。微服务是功能粒度上的拆分,必然导致拆分之后的模块变多。模块与模块之间的通信与维护就变得越来越复杂,微服务网关就应运而生。
网关的作用不言而喻,提供统一API入口,解耦隔离服务,鉴权校验,动态路由。目前主流的网关有三个:Zuul、Kong、Gateway。下面分别简单介绍一下。
Zuul是Neflix开源的微服务网关,核心是一系列的过滤器。经历了1.x到2.x版本的架构升级,从原来1.x的线程阻塞io到2.x基于netty的非阻塞io,性能提升2成左右。也是SpringCloud全家桶里面的成员。使用比较方便,用户量也比较广。不过由于Netfix闭源问题,部分用户也在转向其他两个使用。
Kong基于OpenResty开源的一个分布式Api网关,高性能、易扩展等特点也使得它现在也很流行。由于Kong是基于Nginx与Lua的高性能平台。所以插件功能是一种特色。同时自己也可以使用Lua脚本定制。所以效率不言而喻。
Gateway是在由于Netfix闭源情况下,Apache Spring官方自己推出的网关工具,入手也还是方便,同时对动态路由配置也是支持。今天我们就讲关于SpringCloudGateway的动态路由使用。
案例
SpringCloud微服务的搭建就不在这里展开了,不懂的可以百度/谷歌。本项目用到的微服务有注册中心、网关、测试服务。下面给出springcloud的版本及所需依赖。
父项目的pom主要内容
<properties> <java.version>1.8java.version> <spring-cloud.version>Hoxton.SR5spring-cloud.version> properties> <modules> <module>registermodule> <module>tablemodule> modules> <dependencies> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-testartifactId> <scope>testscope> <exclusions> <exclusion> <groupId>org.junit.vintagegroupId> <artifactId>junit-vintage-engineartifactId> exclusion> exclusions> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-actuatorartifactId> dependency> <dependency> <groupId>io.micrometergroupId> <artifactId>micrometer-registry-prometheusartifactId> dependency> dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-dependenciesartifactId> <version>${spring-cloud.version}version> <type>pomtype> <scope>importscope> dependency> dependencies> dependencyManagement>
注册中心我们用的是eureka方便入手搭建,当然后面也会用到Nacos作为配置中心,也可以一并使用Nacos做注册中心,二合一。eureka依赖及配置文件内容如下:
测试环境可以不要下面的安全认证依赖。 <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId> dependency>
spring.application.name=registerserver.port=8000#下面两项是验证注册用的配置spring.security.user.name=adminspring.security.user.password=rootmanagement.endpoints.web.exposure.include=*management.endpoints.web.exposure.exclude=envmanagement.metrics.tags.application=${spring.application.name}eureka.client.registerWithEureka=false #不注册eureka.client.fetchRegistry=false #不取注册信息eureka.client.service-url.defaultZone=http://admin:root@192.168.2.63:8000/eureka/
下面看Gateway的依赖及配置文件内容:
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-ribbonartifactId> dependency> <dependency> <groupId>com.alibaba.cloudgroupId> <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId> <version>2.2.1.RELEASEversion> dependency> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-gatewayartifactId> dependency>
由于我们用到了nacos配置中心,所以在bootstrap.properties中配置如下:
spring.cloud.nacos.config.server-addr=192.168.23.202:8848spring.cloud.nacos.config.file-extension=properties
在application.yaml或者application.properties中配置如下:
eureka: client: service-url: defaultZone: http://admin:root@192.168.2.63:8000/eureka/management: endpoint: prometheus: enabled: true endpoints: web: exposure: exclude: env include: '*' metrics: export: prometheus: enabled: true tags: application: ${spring.application.name}server: port: 8003spring: application: name: gateway cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true httpclient: connect-timeout: 8000 response-timeout: 5000 nacos: config: server-addr: 192.168.23.202:8848
#注册中心地址eureka.client.service-url.defaultZone=http://admin:root@192.168.2.63:8000/eureka/server.port=8003spring.application.name=gatewaymanagement.endpoints.web.exposure.include=*management.endpoints.web.exposure.exclude=envmanagement.endpoint.prometheus.enabled=truemanagement.metrics.export.prometheus.enabled=truemanagement.metrics.tags.application=${spring.application.name}#为监听nacos配置变化的配置参数spring.cloud.nacos.config.server-addr=192.168.23.202:8848#自动发现服务spring.cloud.gateway.discovery.locator.enabled=true#服务名小写spring.cloud.gateway.discovery.locator.lower-case-service-id=true#全局连接超时时间spring.cloud.gateway.httpclient.connect-timeout=8000#全局相应超时时间spring.cloud.gateway.httpclient.response-timeout=5000
通过上面注册中心和网关服务的配置,我们不用写任何代码就可以让其跑起来。然后下面我们创建一个测试服务注册到Eureka中,用于测试网关的路由。添加pom依赖及配置文件如下:
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId>dependency>
eureka.client.service-url.defaultZone=http://admin:root@192.168.2.63:8000/eureka/spring.application.name=tableserver.port=8002
添加请求接口如下:
@RestController@RequestMapping("/table")public class TableController { @Value("${server.port}") private String port; @GetMapping("/test") public Object test(HttpServletRequest request) { System.out.println("table:port=" + port); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return "table:port=" + port; }}
Nacos的部署(https://nacos.io/):
下载最新稳定版:https://github.com/alibaba/nacos/releases
unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz cd nacos/bin sh startup.sh -m standalone #单机启动 sh startup.sh -p embedded -m standalone #单机内置数据源启动
通过以上的搭建及部署,就完成了我们想要的微服务架构。但是现在我们访问网关无法路由到指定的服务。下面开始配置网关。
spring: cloud: gateway: routes:#路由list - id: after_route #路由ID uri: https://demo.org #路由去的uri predicates:#规则 - After=2020-07-20T17:42:47.789-07:00[America/Denver]
上面的作用是对2020-07-20T17:42:47.789-07:00[America/Denver]这个时间段以后的请求路由到https://demo.org。当然还有Before/Between。想了解更多请看官网介绍https://cloud.spring.io/spring-cloud-gateway/reference/html。在开发中我们希望通过请求前缀自动匹配路由到对应的服务。比如:当请求http://gatewayhost/table/test,希望其自动转向table服务。请求/table/test接口获取数据。可以如下配置:
spring: cloud: gateway: routes: - id: path_route uri: https://tableurl predicates: - Path=/table/**
如果在微服务中不想写http地址,可以改成服务名称“lb://server-name”,lb代表loadbalance-负载均衡,server-name则是服务名称。如下:
spring: cloud: gateway: routes: - id: path_route uri: lb://table predicates: - Path=/table/**
可以设置路由转发时在url上加前缀,如请求网关的地址是http://host/abc/efg,在服务端请求的地址其实是path/abc/efg。那么久可以使用PrefixPath配置:
spring: cloud: gateway: routes: - id: table_route uri: lb://table filters: - PrefixPath=/path
在有些情况下,网络抖动或失败时需要重试,可以如下配置:
spring: cloud: gateway: routes: - id: retry_test uri: http://localhost:8088/table predicates: - Host=*.retry.com filters: - name: Retry args: retries: 3#重试次数 statuses: BAD_GATEWAY#请求返回状态码,org.springframework.http.HttpStatus methods: GET,POST#针对那种格式的请求,需要啊重试 backoff:#重试间隔 firstBackoff: 10ms #首次间隔时间 maxBackoff: 50ms #最大间隔时间 factor: 2 #重试因子 basedOnPreviousValue: false
截取部分请求url,转发给相应服务StripPrefix,如请求网关地址为http://host/table/table/test,而服务提供的接口为/table/test,就可以使用StripPrefix=1去掉一个part在,注意不是一个字符:
spring: cloud: gateway: routes: - id: nameRoot uri: https://nameservice predicates: - Path=/name/** filters: - StripPrefix=1#截取一个部分
设置网关请求超时有两种,一种是全局的超时时间,可以设置大一点,另外一种是针对单个服务或单个接口的超时时间,这个根据实际做调整。
spring: cloud: gateway: httpclient: connect-timeout: 1000 #全局连接超时时间1s response-timeout: 5s #全局请求相应超时时间5s
单个超时设置
- id: per_route_timeouts uri: https://example.org #转发路由uri predicates: - name: Path args: pattern: /delay/{timeout} metadata: response-timeout: 2000 #/delay/{timeout}接口的相应超时时间 connect-timeout: 2000 #/delay/{timeout}接口的连接超时时间
当然以上的配置都可以通过java代码实现。例如单个超时可以这样设置:
import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR;import static org.springframework.cloud.gateway.support.RouteMetadataUtils.RESPONSE_TIMEOUT_ATTR; @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder routeBuilder){ return routeBuilder.routes() .route("test1", r -> { return r.host("*.somehost.org").and().path("/somepath") .filters(f -> f.addRequestHeader("header1", "header-value-1")) .uri("http://someuri") .metadata(RESPONSE_TIMEOUT_ATTR, 2000) .metadata(CONNECT_TIMEOUT_ATTR, 2000); }) .build(); } @Beanpublic RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) { return builder.routes() .route(r -> r.host("**.abc.org").and().path("/image/png") .filters(f -> f.addResponseHeader("X-TestHeader", "foobar")) .uri("http://httpbin.org:80") ) .route(r -> r.path("/image/webp") .filters(f -> f.addResponseHeader("X-AnotherHeader", "baz")) .uri("http://httpbin.org:80") .metadata("key", "value") ) .route(r -> r.order(-1) .host("**.throttle.org").and().path("/get") .filters(f -> f.filter(throttle.apply(1, 1, 10, TimeUnit.SECONDS))) .uri("http://httpbin.org:80") .metadata("key", "value") ) .build();}
对应习惯使用properties格式的配置文件的朋友来说,yaml有点不适应,我们也可以转换。例如下面这样:
spring.cloud.gateway.routes[2].id=10spring.cloud.gateway.routes[2].uri=lb:ws://websocket-service-v1spring.cloud.gateway.routes[2].predicates[0]=Path=/api/websocket-service-v1/**spring.cloud.gateway.routes[2].filters[0]=RewritePath=/api/websocket-service-v1/(?.*), /$\{remaining}
熔断降级配置:
spring: cloud: gateway: routes: - id: ingredients uri: lb://ingredients predicates: - Path=//ingredients/** filters: - name: Hystrix args: name: fetchIngredients fallbackUri: forward:/fallback/test #降级的请求地址
在Gateway服务里面提供一个"/fallback/test"接口,当网关请求服务是出现超时熔断情况,就会返回该接口的内容,给予友好体验。
进阶
前面讲了gateway的搭建、配置都是项目启动后不能不停机的改变。下面我们来看如果想动态调整路由规则。上面也介绍了我们使用nacos来做配置中心(Appllo或者spring cloud config也是可以的),这样就可以将我们的动态路由放在配置中心上,同时监听配置中心的变化,再来更新网关服务的路由。达到不停机既可调整路由规则的效果。
在nacos上新建配置:
填入dataId,group,选择json格式,填入配置内容。
内容如下:
[ { "id":"table", "predicates":[ { "name":"Path", "args":{ "pattern":"/table/**" } } ], "uri":"lb://table", "filters":[ { "args":{ "retries":1, "statuses":"BAD_GATEWAY", "method":"GET" }, "name":"Retry" }, { "args":{ "name":"fallback", "fallbackUri":"forward:/fallback" }, "name":"Hystrix" #熔断降级配置 }, { "args":{ "prefix":"/table" }, "name":"PrefixPath" }, { "name":"PreserveHostHeader" } ], "metadata":{ "response-timeout":3000, "connect-timeout":3000 } }, { "id":"user", "predicates":[ { "name":"Path", "args":{ "pattern":"/user/**" } } ], "uri":"lb://user", "filters":[ { "args":{ "parts":"0" }, "name":"StripPrefix" } ] }]
更改我们Gateway服务。添加Nacos监听代码及动态路由替换代码:
@Componentpublic class NacosGatewayConfig implements ApplicationEventPublisherAware{ private String dataId = "gateway"; private String group = "DEFAULT_GROUP"; @Value("${spring.cloud.nacos.config.server-addr}") private String serverAddr; @Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher applicationEventPublisher; private static final List ROUTE_LIST = new ArrayList<>();//启动执行方法,也可以继承CommandLineRunner-run()启动执行 @PostConstruct public void dynamicRouteByNacosListener() { try { //如果使用的是其他配置中心,需要替换如下监听设置。 ConfigService configService = NacosFactory.createConfigService(serverAddr); String initInfo = configService.getConfig(dataId, group, 5000); addAndPublishBatchRoute(initInfo); configService.addListener(dataId, group, new Listener() { @Override public void receiveConfigInfo(String configInfo) { addAndPublishBatchRoute(configInfo); } @Override public Executor getExecutor() { return null; } }); } catch (NacosException e) { e.printStackTrace(); } } private void addAndPublishBatchRoute(String configInfo) { try { clearRoute(); List gatewayRouteDefinitions = JSONObject.parseArray(configInfo, RouteDefinition.class); for (RouteDefinition routeDefinition : gatewayRouteDefinitions) { addRoute(routeDefinition); } publish(); System.out.println(gatewayRouteDefinitions); } catch (Exception e) { e.printStackTrace(); } } private void clearRoute() { for (String id : ROUTE_LIST) { this.routeDefinitionWriter.delete(Mono.just(id)).subscribe(); } ROUTE_LIST.clear(); } private void addRoute(RouteDefinition definition) { try { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); ROUTE_LIST.add(definition.getId()); } catch (Exception e) { e.printStackTrace(); } } private void publish() {//发布路由改变事件 this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter)); } @Override//注入publisher public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.applicationEventPublisher = publisher; }}
总结
微服务网关我们不只是用于api的管理转发,比如可以在网关层面做用户校验鉴权,让垃圾请求提前过滤掉,减少服务开销。同时也可以作为灰度发版、服务权重设置、服务管理、流量分发等应用。
好了,Spring Cloud Gateway的动态路由项目就讲到这里。喜欢就转发吧。