微服务网关,是一个微服务体系的门户,是多有流量的入口和出口。这样重要的地位就代表了网关需要很高的稳定性。而动态路由就是Spring Cloud Gateway高可用的一种解决方案。
前言 微服务网关,是一个微服务体系的门户,是多有流量的入口和出口。这样重要的地位就代表了网关需要很高的稳定性。通过前四章对Spring Cloud Gateway的学习,我们了解到框架提供了通过配置文件、FluentAPI和Restful接口三种定义路由的方式。前两种方式是通过配置文件或者硬编码的方式来实现,这种方式虽然简单易用,但是如果路由信息变更,则必须重新发布或者重启网关应用,这会对网关服务的稳定性带来挑战(任何一次发布,都有可能引入新的Bug,都会导致JIT的优化丢失,需要重新预热,导致系统抖动),通过Restful接口修改,虽然可以做到实时更新路由,但是缺少了持久化策略,网关一旦重启,则所有的修改都会丢失,而且也较难维护。基于以上分析,Spring Cloud Gateway如果想做到高可用,需要引入一套可维护的动态路由 功能。结合配置中心 实现动态路由,就是一个很好的解决方案。
配置中心是什么? 随着程序功能的日益复杂,程序的配置日益增多 ,各种功能的开关、参数的配置、服务器的地址。对程序配置的期望值 也越来越高,配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。配置中心,就是为了解决如上的问题的。
常用的配置中心,如Spring Cloud Config、Apollo、Nacos等,比较了众多的配置中心组建,我选择Apollo作为配置中心,它的功能很强大。接下来对Apollo的基本功能做一个讲解。环境列表包括了DEV、QA、SIM、PRO四个环境,AppId是Apollo的一个重要配置,是项目的主键。右侧是application、gateway、auth、route四个命名空间(namespace)。根据AppId和nameSpace可以定位到一组配置。想进一步了解Apollo的同学,请参考官网 。
客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
动态配置理论基础 Spring Cloud Gateway中,有一个RefreshRoutesEvent类,顾名思义它是刷新路由的事件类。CachingRouteLocator实现了对RefreshRoutesEvent的监听,一旦RefreshRoutesEvent被publish,refresh函数就会被调用。refresh函数的作用是把cache清除,因为CacheFlux.lookup监听cache是否丢失,如果cache丢失则onCacheMissResume触发重新根据routeDefinition转化为Route。
1 2 3 4 5 public class RefreshRoutesEvent extends ApplicationEvent { public RefreshRoutesEvent (Object source) { super (source); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CachingRouteLocator implements RouteLocator , ApplicationListener <RefreshRoutesEvent > { private final Flux<Route> routes; public CachingRouteLocator (RouteLocator delegate) { this .delegate = delegate; routes = CacheFlux.lookup(cache, "routes" , Route.class) .onCacheMissResume(() -> this .delegate.getRoutes() .sort(AnnotationAwareOrderComparator.INSTANCE)); } public Flux<Route> refresh () { this .cache.clear(); return this .routes; } @Override public void onApplicationEvent (RefreshRoutesEvent event) { refresh(); } }
动态配置实现 ApolloConfigChangePublisher通过@ApolloConfigChangeListener,实现Apollo route命名空间的监听,并实现了ApplicationEventPublisherAware接口。当Apollo配置文件发布之后,routeChange事件就会触发,json解析成对象之后,publisher出去。
1 2 3 4 5 6 7 8 9 10 11 12 { "routeId" : "travel-jd-route" , "uri" : "https://www.jd.com" , "predicates" : [ { "name" : "Path" , "args" : { "_genkey_0" : "/tojd" } } ] }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import com.alibaba.fastjson.JSON;import com.ctrip.framework.apollo.Config;import com.ctrip.framework.apollo.enums.PropertyChangeType;import com.ctrip.framework.apollo.model.ConfigChange;import com.ctrip.framework.apollo.model.ConfigChangeEvent;import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationEventPublisher;import org.springframework.context.ApplicationEventPublisherAware;import org.springframework.stereotype.Component;@Component public class ApolloConfigChangePublisher implements ApplicationEventPublisherAware { @ApolloConfig ("route" ) private Config routeConfig; @ApolloConfigChangeListener ("route" ) private void routeChange (ConfigChangeEvent changeEvent) { for (String changeKey : changeEvent.changedKeys()) { ConfigChange cc = changeEvent.getChange(changeKey); PropertyChangeType ct = cc.getChangeType(); String value = cc.getNewValue(); String oldValue = cc.getOldValue(); if (ct.equals(PropertyChangeType.DELETED)) { publisher.publishEvent(new RouteChangeEvent(new Object(), GatewayConstant.Apollo.CHANGE_DELETE, JSON.toJavaObject(JSON.parseObject(oldValue), GatewayRoute.class).getRouteId())); } else if (ct.equals(PropertyChangeType.ADDED)) { publisher.publishEvent(new RouteChangeEvent( JSON.toJavaObject(JSON.parseObject(value), GatewayRoute.class), GatewayConstant.Apollo.CHANGE_ADD, null )); } else if (ct.equals(PropertyChangeType.MODIFIED)) { publisher.publishEvent(new RouteChangeEvent( JSON.toJavaObject(JSON.parseObject(value), GatewayRoute.class), GatewayConstant.Apollo.CHANGE_MODIFY, null )); } else { LOGGER.error("changeType错误.{}" , cc.toString()); } } } @Override public void setApplicationEventPublisher (ApplicationEventPublisher applicationEventPublisher) { this .publisher = applicationEventPublisher; } }
RouteChangeEventHandler通过实现ApplicationListener接口,监听了RouteChangeEvent,根据RouteChangeEvent的类型,来判定是新增、修改还是删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Component public class RouteChangeEventHandler implements ApplicationListener <RouteChangeEvent > { private static final Logger LOGGER = LoggerFactory.getLogger(RouteChangeEventHandler.class); @ApolloConfig ("route" ) private Config routeConfig; @Autowired private GatewayDynamicRouteService gatewayDynamicRouteService; @PostConstruct private void initAllRoute () { String routeList = routeConfig.getProperty(GatewayConstant.Apollo.ROUTE_LIST, GatewayConstant.Apollo.EMPTY_VALUE); if (GatewayConstant.Apollo.EMPTY_VALUE.equals(routeList)) { throw new Error("重要!路由初始化错误,code:" + ErrorCode.APOLLO_NOT_USE); } String[] routes = routeList.split(GatewayConstant.Apollo.ROUTE_LIST_SPLIT); for (String route : routes) { String dynamicRoute = routeConfig.getProperty(route, GatewayConstant.Apollo.EMPTY_VALUE); if (GatewayConstant.Apollo.EMPTY_VALUE.equals(dynamicRoute)) { throw new Error("重要!路由初始化错误,code:" + ErrorCode.APOLLO_CONFIG_LACK); } gatewayDynamicRouteService.save(JSON.toJavaObject(JSON.parseObject(dynamicRoute), GatewayRoute.class)); } } @Override public void onApplicationEvent (RouteChangeEvent event) { LOGGER.info("动态路由变更已接收routeId:{},type:{}" , event.getRouteId(), event.getChangeType()); if (GatewayConstant.Apollo.CHANGE_ADD.equals(event.getChangeType())) { GatewayRoute route = (GatewayRoute) event.getSource(); gatewayDynamicRouteService.save(route); } else if (GatewayConstant.Apollo.CHANGE_DELETE.equals(event.getChangeType())) { gatewayDynamicRouteService.delete(event.getRouteId()); } else if (GatewayConstant.Apollo.CHANGE_MODIFY.equals(event.getChangeType())) { GatewayRoute route = (GatewayRoute) event.getSource(); gatewayDynamicRouteService.save(route); } } }
GatewayDynamicRouteService也必须实现ApplicationEventPublisherAware,为了发送RefreshRoutesEvent。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Component public class GatewayDynamicRouteService implements ApplicationEventPublisherAware { private static final Logger LOGGER = LoggerFactory.getLogger(GatewayDynamicRouteService.class); @Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher publisher; public void save (GatewayRoute route) { RouteDefinition definition = new RouteDefinition(); PredicateDefinition predicate = new PredicateDefinition(); definition.setId(route.getRouteId()); predicate.setName(route.getPredicates().get(0 ).getName()); predicate.setArgs(route.getPredicates().get(0 ).getArgs()); definition.setPredicates(Arrays.asList(predicate)); URI uri = UriComponentsBuilder.fromUriString(route.getUri()).build().toUri(); definition.setUri(uri); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this .publisher.publishEvent(new RefreshRoutesEvent(this )); } public void delete (String id) { routeDefinitionWriter.delete(Mono.just(id)).subscribe(); this .publisher.publishEvent(new RefreshRoutesEvent(this )); } @Override public void setApplicationEventPublisher (ApplicationEventPublisher applicationEventPublisher) { this .publisher = applicationEventPublisher; } }
效果演示 首先设置好京东的路由信息,访问localhost:8080/jd,可以看到会跳转网关。
删除京东的网关设定之后,再次访问localhost:8080/jd则404。