Featured image of post Spring Cloud微服务网关Zuul动态路由配置优化和手动触发路由刷新

Spring Cloud微服务网关Zuul动态路由配置优化和手动触发路由刷新

一、前文必看

Spring Cloud微服务网关Zuul动态路由配置。在前文中留了两个小坑。在本文将怕它给填了,所以前一篇文章建议看一下。

二、DynamicZuulRouteLocator小优化

前文中提到,HeartbeatEvent事件会频繁触发,每次都需要去查询数据库。而且ZuulRefreshListener监听的其余四个刷新事件也不会经常触发。所以这里就可以做一下小优化,因为系统上线稳定后,路由一般是不会经常变动的。所以我们可以把路由规则配置给缓存起来,这样就不会每次的心跳都去查询数据库。

这里有分两种情况:

  • 单机Zuul网关下的本地缓存
  • Zuul网关集群下的分布式缓存

2.1 本地缓存

直接上代码:

 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
public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    private final ZuulProperties zuulProperties;

    private final RoutingRuleService routingRuleService;

    private static final ConcurrentHashMap<String, ZuulRoute> routesCache = new ConcurrentHashMap<>();

    public DynamicZuulRouteLocator(String servletPath, ZuulProperties properties, RoutingRuleService routingRuleService) {
        super(servletPath, properties);
        this.zuulProperties = properties;
        this.routingRuleService = routingRuleService;
    }

    @Override
    public void refresh() {
        doRefresh();
    }

    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        Map<String, ZuulProperties.ZuulRoute> allRoutes = null;
        if (routesCache.isEmpty()) {
            allRoutes = routingRuleService.findAllRoutes();
            routesCache.putAll(allRoutes);
        } else {
            allRoutes = routesCache;
        }
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        // 父类的 SimpleRouteLocator的locateRoutes()读取的是配置文件中的路由规则配置
        routesMap.putAll(super.locateRoutes());
        routesMap.putAll(allRoutes);
        LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
        routesMap.forEach((key, value) -> {
            String path = key;
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.zuulProperties.getPrefix())) {
                path = this.zuulProperties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, value);
        });
        return values;
    }

    public static void clearCache() {
        routesCache.clear();
    }
}

这是简单地使用了ConcurrentHashMap作为本地缓存。当然也可以使用caffeineGuava CacheEhcache等本地缓存框架。使用ConcurrentHashMap主要是更加方便快捷,简单明了。例如像RocketMQ也有使用了ConcurrentHashMap来缓存路由信息。

这样在数据库面前就挡了层缓存,优化了查询性能,不需要每次的心跳都去查数据库。

2.2 Redis分布式缓存

一般情况下为了高可用,甚至是高并发的情况下,我们的微服务网关都是以集群的形式在线上部署的。上面的本地缓存并不适合在分布式的环境下使用。当然也已使用Redis作为一级缓存,然后本地缓存作为二级缓存。

这时候情况就复杂起来了。这里我画了一个简单的架构图设计图:

有两种设计方案:

  • 方案1:Zuul集成路由规则的CRUD接口。对于路由管理或者说网关管理的接口集成在Zuul网关中,Zuul网关配置数据源。对路由CRUD的操作通过负载均衡到Zuul网关,然后更新Redis缓存。这样的话便不能使用本地缓存了,容易出现数据不一致的情况,当然如果能够维护好一级缓存和二级缓存的话,也是可以。
    • 这种设计缺点较多,首先以来了Redis,提高了整个网关的复杂度,可用性也随之降低。
    • 其次业务耦合度较高,依赖数据源,因为需要对外提供了系列接口。
  • 方案2:单独一个管理后台应用,在这里对数据源进行操作。当路由信息发生改变的时候通过WebSocketZookeeperNacos或者Consul等手段来通知刷新本地缓存的路由信息。
    • 这样的话Zuul网关专注做路由转发,把业务解耦出来,设计更加的轻量级。Apache Shenyu网关就是这样做的。

三、实现手动触发路由刷新

看到这里应该知道,对于动态路由配置,我们是要提供一系列的接口来进行维护和操作的。当我们对路由进行了增删改后,是要去刷新Zuul中的路由信息的。按照这样手动修改路由信息触发的方式可以实现的有两种:全量和增量。

接下来的实现是以上面章节所说的方案1来做的,也没有使用分布式缓存,把源码中的本地缓存替换成Redis就行了,主要是因为篇幅受限以及我们主要学习的是实现的原理。请勿直接用在生产上,仅供学习。

3.1 全量触发

OK,直接入正题。这里准备一些接口,从Controller到Service。

RoutingRuleController:

 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
@RestController
@RequestMapping("route")
public class RoutingRuleController {

    @Autowired
    private RoutingRuleService routingRuleService;

    @PostMapping("add")
    public Object addRoute(@RequestBody RoutingRule routingRule) {
        routingRuleService.save(routingRule);
        return "success";
    }

    @PostMapping("update")
    public Object updateRoute(@RequestBody RoutingRule routingRule) {
        routingRuleService.save(routingRule);
        return "success";
    }

    @DeleteMapping("delete")
    public Object updateRoute(Long id) {
        routingRuleService.delete(id);
        return "success";
    }
}

接口:

1
2
3
4
5
6
7
8
public interface RoutingRuleService {

    Map<String, ZuulProperties.ZuulRoute> findAllRoutes();

    void save(RoutingRule routingRule);

    void delete(Long id);
}

接口实现:

 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
@Service
public class RoutingRuleServiceImpl implements RoutingRuleService {

    @Autowired
    private RoutingRuleDao routingRuleDao;
    @Autowired
    private ApplicationEventPublisher publisher;

    @Override
    public Map<String, ZuulProperties.ZuulRoute> findAllRoutes() {
        RoutingRule routingRule = new RoutingRule();
        routingRule.setEnabled(1);
        Example<RoutingRule> example = Example.of(routingRule);
        List<RoutingRule> routingRuleList = routingRuleDao.findAll(example);

        Map<String, ZuulProperties.ZuulRoute> zuulRouteMap = new LinkedHashMap<>();

        routingRuleList.stream().filter(item -> StringUtils.isNotBlank(item.getPath()))
                .forEach(item -> {
                    ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
                    BeanUtils.copyProperties(item, zuulRoute);
                    zuulRoute.setId(String.valueOf(item.getId()));
                    zuulRouteMap.put(item.getPath(), zuulRoute);
                });
        return zuulRouteMap;
    }

    @Override
    public void save(RoutingRule routingRule) {
        RoutingRule rule = routingRuleDao.saveAndFlush(routingRule);
        publisher.publishEvent(new RefreshRouteEvent(rule));
    }

    @Override
    public void delete(Long id) {
        routingRuleDao.deleteById(id);
        RoutingRule routingRule = new RoutingRule();
        routingRule.setId(id);
        publisher.publishEvent(new RefreshRouteEvent(routingRule));
    }
}

从代码可以看到,在路由信息发生变更的时候,我们是通过Spring的事件发布,去通知Listener去完成剩下的操作的。这样可以实现业务上的解耦。

其实也不一定是要用Spring的事件发布,可以使用Disruptor框架来实现高性能内存队列;当然也可以参考Apache Shenyu里面实现的disruptor。我这里为了方便就使用了Spring的事件发布了。

定义一个事件RefreshRouteEvent:

1
2
3
4
5
6
public class RefreshRouteEvent extends ApplicationEvent {

    public RefreshRouteEvent(Object source) {
        super(source);
    }
}

还有监听器RefreshRouteListener: 这里的监听器做法有很多

  1. 像下面代码里面那样直接清空DynamicZuulRouteLocator里的本地缓存,然后发布路由刷新事件,让ZuulRefreshListener去刷新缓存。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Component
public class RefreshRouteListener implements ApplicationListener<ApplicationEvent> {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Autowired
    private RouteLocator routeLocator;

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof RefreshRouteEvent) {
            DynamicZuulRouteLocator.clearCache();
            publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
        }
    }
}
  1. 可以在RefreshRouteListener里面把数据库里面的路由查询出来,然后设置到DynamicZuulRouteLocator的本地缓存中。

3.2 增量触发

每次修改单个路由配置就触发全量的路由加载,这很明显不符合我们程序开发的追求。但是SimpleRouteLotor的方法里面并没有添加单个或删除单个路由配置的方法。我们再看一下之前的类图:

显然还有DiscoveryClientRouteLocatorCompositeRouteLocator,去这两个的源码会看到DiscoveryClientRouteLocator就是我们想要找的那个类。至于CompositeRouteLocator是整合Spring容器中所有的RouteLocator,它的操作就是遍历所有的RouteLocator去操作路由。

ZuulRefreshListener监听器的刷新执行的方法是org.springframework.cloud.netflix.zuul.filters.CompositeRouteLocator#refresh,可以看到是遍历所有的RouteLocator执行refresh操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class CompositeRouteLocator implements RefreshableRouteLocator {
	...
	@Override
	public void refresh() {
		for (RouteLocator locator : routeLocators) {
			if (locator instanceof RefreshableRouteLocator) {
				((RefreshableRouteLocator) locator).refresh();
			}
		}
	}
}

DiscoveryClientRouteLocatorZuulProxyAutoConfiguration被注入到了Spring容器。使用的时候直接@Autowired引用就行。

所以有两种写法:

  • 使用DiscoveryClientRouteLocator
  • 修改DynamicZuulRouteLocator

这里使用第二种写法,接下来修改代码:

DynamicZuulRouteLocator添加方法removeRoute(String id)addRoute(ZuulRoute zuulRoute):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    ...同上

    public void removeRoute(String id) {
        for (String path : routesCache.keySet()) {
            ZuulRoute zuulRoute = routesCache.get(path);
            if (org.apache.commons.lang3.StringUtils.equals(id, zuulRoute.getId())) {
                routesCache.remove(path);
                // 刷新 SimpleRouteLocator 里的路由信息
                refresh();
            }
        }
    }

    public void addRoute(ZuulRoute zuulRoute) {
        routesCache.put(zuulRoute.getPath(), zuulRoute);
        // 刷新 SimpleRouteLocator 里的路由信息
        refresh();
    }
}

因为最终所有的路由信息是保存在SimpleRouteLocator里的private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();,所以子类修改了路由信息需要刷新到SimpleRouteLocator;

修改事件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class RefreshRouteEvent extends ApplicationEvent {

    private boolean isDelete = false;

    public RefreshRouteEvent(Object source) {
        super(source);
    }

    public boolean isDelete() {
        return isDelete;
    }

    public void setDelete(boolean delete) {
        isDelete = delete;
    }
}

修改service的delete方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Service
public class RoutingRuleServiceImpl implements RoutingRuleService {

    ...

    @Override
    public void delete(Long id) {
        routingRuleDao.deleteById(id);
        RoutingRule routingRule = new RoutingRule();
        routingRule.setId(id);
        RefreshRouteEvent refreshRouteEvent = new RefreshRouteEvent(routingRule);
        refreshRouteEvent.setDelete(true);
        publisher.publishEvent(refreshRouteEvent);
    }
}

最后修改监听器:

 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
@Component
public class RefreshRouteListener implements ApplicationListener<ApplicationEvent> {

    @Autowired
    private DynamicZuulRouteLocator dynamicZuulRouteLocator;

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof RefreshRouteEvent) {
            // 全量加载路由信息
            //DynamicZuulRouteLocator.clearCache();
            //publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
            // 增量修改路由信息
            RefreshRouteEvent refreshRouteEvent = (RefreshRouteEvent) event;
            RoutingRule routingRule = (RoutingRule) event.getSource();
            if (refreshRouteEvent.isDelete()) {
                dynamicZuulRouteLocator.removeRoute(String.valueOf(routingRule.getId()));
            } else {
                ZuulRoute zuulRoute = new ZuulRoute();
                BeanUtils.copyProperties(routingRule, zuulRoute);
                zuulRoute.setId(String.valueOf(routingRule.getId()));
                dynamicZuulRouteLocator.addRoute(zuulRoute);
            }
        }
    }
}

四、总结

通过两篇文章讲述了Zuul网关的动态路由配置的原理和实战,RouteLocator接口的类图并不复杂,很多Zuul的组件在ZuulServerAutoConfigurationZuulProxyAutoConfiguration里面注入到了Spring容器,可以看一下里面都有什么组件,并分析一下其功能,能够很快地理解Zuul的一些功能原理。