龙空技术网

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

IT知识分享官 145

前言:

今天朋友们对“apache路由配置”大约比较重视,看官们都需要分析一些“apache路由配置”的相关知识。那么小编同时在网摘上收集了一些对于“apache路由配置””的相关资讯,希望你们能喜欢,大家快快来学习一下吧!

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

这里有分两种情况:

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

直接上代码:

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作为本地缓存。当然也可以使用caffeine、Guava Cache、Ehcache等本地缓存框架。使用ConcurrentHashMap主要是更加方便快捷,简单明了。例如像RocketMQ也有使用了ConcurrentHashMap来缓存路由信息。

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

2.2 Redis分布式缓存

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

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

有两种设计方案:

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

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

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

3.1 全量触发

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

RoutingRuleController:

@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";    }}复制代码

接口:

public interface RoutingRuleService {    Map<String, ZuulProperties.ZuulRoute> findAllRoutes();    void save(RoutingRule routingRule);    void delete(Long id);}复制代码

接口实现:

@Servicepublic 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:

public class RefreshRouteEvent extends ApplicationEvent {    public RefreshRouteEvent(Object source) {        super(source);    }}复制代码

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

像下面代码里面那样直接清空DynamicZuulRouteLocator里的本地缓存,然后发布路由刷新事件,让ZuulRefreshListener去刷新缓存。

@Componentpublic 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));        }    }}复制代码
可以在RefreshRouteListener里面把数据库里面的路由查询出来,然后设置到DynamicZuulRouteLocator的本地缓存中。3.2 增量触发

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

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

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

public class CompositeRouteLocator implements RefreshableRouteLocator {	...	@Override	public void refresh() {		for (RouteLocator locator : routeLocators) {			if (locator instanceof RefreshableRouteLocator) {				((RefreshableRouteLocator) locator).refresh();			}		}	}}复制代码

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

所以有两种写法:

使用DiscoveryClientRouteLocator修改DynamicZuulRouteLocator

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

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

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;

修改事件:

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方法:

@Servicepublic 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);    }}复制代码

最后修改监听器:

@Componentpublic 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的组件在ZuulServerAutoConfiguration和ZuulProxyAutoConfiguration里面注入到了Spring容器,可以看一下里面都有什么组件,并分析一下其功能,能够很快地理解Zuul的一些功能原理。

作者:

链接:

标签: #apache路由配置 #路由转发首页apache