目录
- gateway本地文件常规路由配置
- 本地文件配置对业务造成的痛点
- 动态路由改造
1 gateway本地文件常规路由配置
我们先大致看下gateway中的常规概念
Route(路由)
:路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。Predicate(谓语、断言)
:路由转发的判断条件,目前SpringCloud Gateway
支持多种方式,常见如:Path
、Query
、Method
、Header
等。Filter(过滤器)
:过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容。
整体架构:
我们本地文件配置路由信息的时候都是application.yml中配置这样一段内容
spring:
cloud:
gateway:
routes:
- id: authWsdl
uri: lb://demo-auth
predicates:
- Path=/demo/authenticationIf
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 100
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 200
# 使用SpEL表达式从Spring容器中获取Bean对象
key-resolver: "#{@pathKeyResolver}"
gateway就可以转发到demo-auth认证微服务上了。至于其他的断言方式配置就不一一展现了,网上一搜一大把。
2 本地文件配置对业务造成的痛点
这中文件配置在实际生产环境中对业务有什么影响么?
我们网关作为业务的最前沿,如果经常更改配置文件重启会造成业务系统可用率大大降低,因为网关一单重启所有业务都不可用。而且对于流量大的公司来说重启网关将是灾难性的。
但是我们实际生产环境中可能后端服务经常变动,频繁的发布版本,或者灰度发布,虽然我们的服务都是注册到注册中心的,但是从业务下线到业务上线,这个时间段网关感知能力不是实时的,再次期间部分请求是失败的。
如果我们后端服务增加或者减少了 同样需要改网关配置文件,然后重启生效。那我们有没有办法吧网关的路由配置搞成动态的,让网关直接去数据库中动态读取呢?这样我们就可以减少网关的重启此处,降低系统失败率。
于是我们翻开spring cloud gateway的源码发现如下几个核心类:
org.springframework.cloud.gateway.route.RouteDefinitionWriter
org.springframework.cloud.gateway.route.RouteDefinitionLocator
org.springframework.cloud.gateway.route.RouteDefinitionRepository
org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository
见名之意,一看着就是个mapper层的东西。原来啊gateway默认是走的内存,启动的时候回把配置文件解析到内存中。而RouteDefinitionRepository这个就是mapper的接口,那就好办了,接下来我们自己搞下,有点长耐心看下。
3 动态路由改造
我们的设想是这样
- 通过实现RouteDefinitionRepository接口来干扰路由信息的存储、读取等操作。
- 抽象出来一个GatewayRouteRepository接口,支持扩展多种存储方式。
- 开发一个管理后台对数据源头进行更新操作,后台提供可视化的页面,避免操作人员操作失误(漏写信息,错写单词等等)。
- 通过zk实现监听机制,因为我们实际生产环境中网关也是集群部署,我们需要更新完路由配置信息后所有网关实例都可以感知到,然后去数据源端重新拉取最新路由信息。
ok既然思路有了那我们就开始吧,创建MyRouteDefinitionRepository来实现:
org.springframework.cloud.gateway.route.RouteDefinitionRepository
该接口有3个方法:
//全量获取路由信息
Flux<RouteDefinition> getRouteDefinitions()
//保存路由信息
Mono<Void> save(Mono<RouteDefinition> route)
//删除路由配置信息根据路由id
Mono<Void> delete(Mono<String> routeId)
我们先看内存实现的方式源码
private final Map<String, RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap());
public InMemoryRouteDefinitionRepository() {
}
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap((r) -> {
if (StringUtils.isEmpty(r.getId())) {
return Mono.error(new IllegalArgumentException("id may not be empty"));
} else {
this.routes.put(r.getId(), r);
return Mono.empty();
}
});
}
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap((id) -> {
if (this.routes.containsKey(id)) {
this.routes.remove(id);
return Mono.empty();
} else {
return Mono.defer(() -> {
return Mono.error(new NotFoundException("RouteDefinition not found: " + routeId));
});
}
});
}
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.routes.values());
}
那就简单了 源代码粘过来,然后把
private final Map<String,RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap());
改成我们自己的GatewayRouteRepository接口即可,这是我们的GatewayRouteRepository:
public interface GatewayRouteRepository {
void saveData(String id,String data) throws Exception ;
void remove(String id) throws Exception ;
List<RouteDefinition> getAllList();
}
我们实现了2中存储方式一种是redis 、一种是zk 为什么实现这2中呢,我们希望网关中引入的组件越少越好,这样可以降低网关的问题率,提高可用度,本身我们就通过redis在网关层做了限流操作,通过zk实现lvs自动负载到网实例(网关注册到zk,agent监听zk临时节点信息动态更改lvs配置)又设想通过zk动态通知所有实例刷新路由故此实现了这两种。
其他朋友也可以通过实现GatewayRouteRepository来适配适合你们自己的存储方式。
我们分别看下这2中实现
public class RedisGateRouteRespository implements GatewayRouteRepository {
private Logger log= LoggerFactory.getLogger(RedisGateRouteRespository.class);
@Autowired
private StringRedisTemplateTest redisTemplate;
@Override
public void saveData(String id, String data) throws Exception {
redisTemplate.hset(GateWayContext.ROUTEPATH,id,data);
}
@Override
public void remove(String id) throws Exception {
redisTemplate.hdel(GateWayContext.ROUTEPATH,id);
}
@Override
public List<RouteDefinition> getAllList() {
Set<String> keys = redisTemplate.hkeys(GateWayContext.ROUTEPATH);
List<RouteDefinition> routeDefinitions=new ArrayList<>();
keys.forEach(key->{
GatewayRouteDefinition gatewayRouteDefinition =
JSON.parseObject(
(String) redisTemplate.hget(GateWayContext.ROUTEPATH, key),
GatewayRouteDefinition.class);
log.info("缓存信息:{}",gatewayRouteDefinition.toString());
routeDefinitions.add(RouteDefintionAssemble.assembleRouteDefinition(gatewayRouteDefinition));
});
return routeDefinitions;
}
}
public class ZookeeperRepository implements GatewayRouteRepository {
@Autowired
private CuratorFramework zk;
@Override
public void saveData(String id,String data) throws Exception {
if(null == zk.checkExists().forPath(GateWayContext.ROUTEPATH + "/" + id)) {
zk.create().creatingParentsIfNeeded().
withMode(CreateMode.PERSISTENT)
.forPath(GateWayContext.ROUTEPATH + "/" + id, data.getBytes());
}else{
zk.setData().forPath(GateWayContext.ROUTEPATH + "/" + id, data.getBytes());
}
}
@Override
public void remove(String id) throws Exception {
zk.delete().forPath(id);
}
@Override
public List<RouteDefinition> getAllList() {
List<RouteDefinition> routeDefinitions=new ArrayList<>();
try {
List<String> strings = zk.getChildren().forPath(GateWayContext.ROUTEPATH);
strings.forEach(path->{
try {
String s = new String(zk.getData().forPath(GateWayContext.ROUTEPATH + "/" + path));
GatewayRouteDefinition gatewayRouteDefinition = JSON.parseObject(s, GatewayRouteDefinition.class);
routeDefinitions.add(RouteDefintionAssemble.assembleRouteDefinition(gatewayRouteDefinition));
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
return routeDefinitions;
}
这2中实现代码很简单,就不做过多阐述了。我们来看下如何同时网关刷新路由信息。还是的看下源码是怎么做的,再次打开源码发现了一个event包如图
哈哈这里有个RefreshRoutesEvent,一看这就是个通知刷新路由的event,那源码是否是通过spring事件机制刷新的路由的呢?我们来debug下看下调用链。
果然是呀,我们向网关增加一个路由然后发布refreshRoutesEvent事件,果然调用到我们自己的gatRouteDefinitions方法中取重新过去了全量路由信息
那我们来看下我们如何用zk实现监听
public class ZookeeperEventListener {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
private Logger log= LoggerFactory.getLogger(ZookeeperEventListener.class);
@Autowired
private CuratorFramework zk;
public ZookeeperEventListener(){
log.info("开始实例化 zk类");
}
@Autowired
private MyGateWayApplicationEvent applicationEvent;
@PostConstruct
public void init() throws Exception {
register();
listenerRoute();
}
private void listenerRoute() throws Exception {
PathChildrenCache pathChildrenCache = new PathChildrenCache(zk, GateWayContext.ROUTEPATH,true);
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
log.info("监听到zk发生变化,对应类型为:{}",event.getType());
//如果发现节点变更,清楚当前路由,从新加载
PathChildrenCacheEvent.Type type = event.getType();
if(type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
applicationEvent.publish();
}
}
});
pathChildrenCache.start();
}
private void register() throws Exception {
log.info("开始向zk注册");
String ip = nacosDiscoveryProperties.getIp();
zk.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(GateWayContext.GATEWAYREGISTERPATH+ip+":31081");
}
}
public class MyGateWayApplicationEvent implements ApplicationEventPublisherAware {
private Logger log= LoggerFactory.getLogger(MyGateWayApplicationEvent.class);
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher=applicationEventPublisher;
}
public void publish(){
log.info("监听到zk的route节点发生变更,开始发布刷新事件");
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}
只是这里是全量刷新,其实我们可以自己编写事件Event来根据路由id更新固定的某个路由信息,发布的时候吧路由id携带过去即可(需要自定义事件RefreshRoutesEventById,自己实现监听刷新逻辑,我们这里就不实现了有兴趣的朋友可以自己尝试下)。
我们管理后台那边只需把数据在源端更新后再更新zk的固定节点即可,这样所有网关实例均可马上感知到,不过实际代码debug中发现spring cloud gateway会定时去全量刷新。所以这部分看个人需求,如果想要立刻生效的可以采用我这种实现方式。
我这里为了避免后台项目需要依赖spring cloud gateway 包所以存储的model采用自定义的,这样的话gateway哪里就需要转换下。
public class RouteDefintionAssemble {
//把传递进来的参数转换成路由对象
public static RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = new RouteDefinition();
definition.setId(gwdefinition.getId());
definition.setOrder(gwdefinition.getOrder());
//设置断言
List<PredicateDefinition> pdList=new ArrayList<>();
List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();
for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setArgs(gpDefinition.getArgs());
predicate.setName(gpDefinition.getName());
pdList.add(predicate);
}
definition.setPredicates(pdList);
//设置过滤器
List<FilterDefinition> filters = new ArrayList();
List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();
for(GatewayFilterDefinition filterDefinition : gatewayFilters){
FilterDefinition filter = new FilterDefinition();
filter.setName(filterDefinition.getName());
filter.setArgs(filterDefinition.getArgs());
filters.add(filter);
}
definition.setFilters(filters);
URI uri = null;
if(gwdefinition.getUri().startsWith("http")){
uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
}else{
// uri为 lb://consumer-service 时使用下面的方法
uri = URI.create(gwdefinition.getUri());
}
definition.setUri(uri);
return definition;
}
}
这是我自己实现的3个model
public class GatewayFilterDefinition {
//过滤器名称
private String name;
// 路由规则
private Map<String,String> args=new LinkedHashMap<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, String> getArgs() {
return args;
}
public void setArgs(Map<String, String> args) {
this.args = args;
}
}
public class GatewayRouteDefinition {
//路由id
private String id;
//路由断言集合配置
private List<GatewayPredicateDefinition> predicates=new ArrayList<>();
//路由过滤器集合配置
private List<GatewayFilterDefinition> filters=new ArrayList<>();
//转发目标uri
private String uri;
//执行顺序
private int order=0;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<GatewayPredicateDefinition> getPredicates() {
return predicates;
}
public void setPredicates(List<GatewayPredicateDefinition> predicates) {
this.predicates = predicates;
}
public List<GatewayFilterDefinition> getFilters() {
return filters;
}
public void setFilters(List<GatewayFilterDefinition> filters) {
this.filters = filters;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public String toString() {
return JSON.toJSONStringWithDateFormat(this,"yyyy-MM-dd HH:mm:ss");
}
}
public class GatewayPredicateDefinition {
//断言对应的name
private String name;
//配置 断言规则
private Map<String,String> args=new LinkedHashMap<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, String> getArgs() {
return args;
}
public void setArgs(Map<String, String> args) {
this.args = args;
}
}
总结
通过以上改造我们的网关就可以不重启的情况下,动态更改路由信息了,还可以基于此功能实现灰度发布,动态增加、减少转发功能,后端服务上线下也可以做到最业务0影响,上述代码已经上线1月目前运行稳定,如果有需要改进的地方望朋友们踊跃指出。
spring这个家族 真的很伟大,所有产品都那么优秀,高度抽象,提供各种各样的接口来供我们扩展,非常值得我们去学习。