API网关之动态路由


前言

发布和路由是API网关一个很重要的功能,网关的发布在业务上一般有多环境发布、灰度发布等功能,发布的本质是将API部署到某一个环境,通过授权机制授权给某一个应用,应用是一个API访问身份的抽象,这样用户就可以使用API了,发布在技术实现上来看,就是API在网关实例内存空间中生成对应的路由规则,实际用户通过网关访问API,是按照API的路由规则路由以后才能访问接口,这是网关的基本原理。
实际生产上修改API和路由是很频繁的操作,尤其是网关作为API生态的使用场景,会有大量的API发布到网关,所以就涉及到网关的一个核心能力动态路由,它可以使网关不停机的情况下动态刷新同步路由,下面即将介绍的也是动态路由的实现过程。


一、使用场景

API网关的使用场景一般有三种,作为API生态通过云市场售卖、系统集成、实现多端统一面向多端输出,我们的场景是作为系统集成和API生态,网关会发布大量的API,会频繁的进行API的修改及添加,还会有批量操作,这就需要支持高性能动态路由。

二、网关路由介绍

网上关于路由及动态路由实现方式,有很多的介绍,资料也很多,这里我整理总结了一下网关路由的实现方式,网关路由总体分为两种:

  1. 静态路由: properties、ymal文件配置、RouteLocatorBuilder在代码中配置,需要重启服务才能生效
spring:
  cloud:
    gateway:
      routes:
      - id: ingredients
        uri: lb://ingredients
        predicates:
        - Path=//ingredients/**
        filters:
        - name: CircuitBreaker
          args:
            name: fetchIngredients
            fallbackUri: forward:/fallback
      - id: ingredients-fallback
        uri: http://localhost:9994
        predicates:
        - Path=/fallback
        filters:
        - name: FallbackHeaders
          args:
            executionExceptionTypeHeaderName: Test-Header
@Bean
public 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();
}
  1. 动态路由: 不需要重启服务,可以实现自动修改API和刷新路由,主要分为原生动态路由和自由扩展路由两类

    • 原生动态路由 通过注册中心自动发现与注册,通过Actuator API提供的端点动态修改,这两种都有局限性,不适合多定制化及频繁更新API的场景,灵活性不够
    • 自由扩展路由 用户自定义路由存储方式、刷新方式、刷新时机及修改方式
      1)继承RouteLocator、RouteDefinitionRepository,实现获取路由的方法,路由可以存储在redis、RDB、zookeeper等位置,spring cloud gateway心跳每30s会调用该方法去刷新路由,也可以在修改路由的时候通过publishEvent事件机制即时刷新
    @Component
    public class RefreshRouteLocator implements RouteLocator {
    private static Logger log = LoggerFactory.getLogger(RefreshRouteLocator.class);
    
    private Flux<Route> route;
    private RouteLocatorBuilder builder;
    private RouteLocatorBuilder.Builder routesBuilder;
    
    /**
     * 自定义的API Repository,可来源于Redis、数据库、Zookeeper等,保存着API的信息及PATH、目标地址
     */
    @Autowired
    private APIRepository apiRepository;
    
    @Autowired
    GatewayRoutesRefresher gatewayRoutesRefresher;
    
    public RefreshRouteLocator(RouteLocatorBuilder builder) {
        this.builder = builder;
        clearRoutes();
    }
    
    public void clearRoutes() {
        routesBuilder = builder.routes();
    }
    
    /**
     * 配置完成后,调用本方法构建路由和刷新路由表
     */
    public void buildRoutes() {
      clearRoutes();
        if (routesBuilder != null) {
            apiRepository.getAll().parallelStream().forEach(service ->{
                String serviceId = service.getServiceId();
                APIInfo serviceDefinition = apiRepository.get(serviceId);
                if (serviceDefinition == null) {
                    log.error("无此服务配置信息:" + serviceId);
                }
                URI uri = UriComponentsBuilder.fromHttpUrl(serviceDefinition.getRoutePath()).build().toUri();
                routesBuilder.route(serviceId, r -> r.path(serviceDefinition.getRequestPath()).uri(uri));
            });
            this.route = routesBuilder.build().getRoutes();
        }
        gatewayRoutesRefresher.refreshRoutes();
    }
    
    @Override
    public Flux<Route> getRoutes() {
        return route;
    }
    }
    
    @Component
    public class FileRouteDefinitionRepository implements RouteDefinitionRepository, ApplicationEventPublisherAware {
    private static final Logger LOGGER = LoggerFactory.getLogger(FileRouteDefinitionRepository.class);
    private ApplicationEventPublisher publisher;
    private List<RouteDefinition> routeDefinitionList = new ArrayList<>();
    
    @Value("${gateway.route.config.file}")
    private String file;
    
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
    
    @PostConstruct
    public void init() {
        load();
    }
    
    /**
     * 监听事件刷新配置
     */
    @EventListener
    public void listenEvent(RouteConfigRefreshEvent event) {
        load();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
    }
    
    /**
     * 加载
     */
    private void load() {
        try {
            String jsonStr = Files.lines(Paths.get(file)).collect(Collectors.joining());
            routeDefinitionList = JSON.parseArray(jsonStr, RouteDefinition.class);
            LOGGER.info("路由配置已加载,加载条数:{}", routeDefinitionList.size());
        } catch (Exception e) {
            LOGGER.error("从文件加载路由配置异常", e);
        }
    }
    
    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
    }
    
    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
    }
    
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(routeDefinitionList);
    }
    }
    

    2)RouteDefinitionWriter增量更新,此接口的spring cloud gateway默认实现类为InMemoryRouteDefinitionRepository,如果使用默认实现类可以调用此接口定义的新增和删除方法去做动态刷新,但是他也不是理解上的增量刷新,其实还是全量刷新,并且没有路由持久化,服务宕机后路由丢失

    	public interface RouteDefinitionWriter {
    
    /**
     * 保存路由配置
     *
     * @param route 路由配置
     * @return Mono<Void>
     */
    Mono<Void> save(Mono<RouteDefinition> route);
    
    /**
     * 删除路由配置
     *
     * @param routeId 路由编号
     * @return Mono<Void>
     */
    Mono<Void> delete(Mono<String> routeId);
    }
    
    

    3)自定义GlobalFilter实现,大致思路是请求一进来,先拉取接口定义,然后用代码直接生成Router,这样就可以实现动态路由,这种方式是真正的增量式动态路由,而且修改接口时不用动态生成及刷新路由了,刷新是在接口调用的时候做的,缺点是性能会有所降低

    @Component
    public class DynamicEverythingFilter implements GlobalFilter, Ordered {
    private static Logger log = LoggerFactory.getLogger(DynamicEverythingFilter.class);
    
    @Autowired
    private APIRepository apiRepository;
    
    public DynamicEverythingFilter(APIRepository apiRepository) {
        this.apiRepository = apiRepository;
    }
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 定义API路径最后一位,为服务ID,只判断最后一位,实际上也可以自由添加任何逻辑
        String serviceID = requestPathSets[requestPathSets.length -1];
      
        APIInfo apiInfo = this.apiRepository.get(serviceID);
        String newPath = apiInfo.getRoutePath();
      
        exchange.getAttributes().put(ContextConstants.TARGET_URL, newPath);
    
        log.info("服务ID {}, 路由到后端[{}]", serviceID, newPath);
                
      
        // 动态修改路由开始
        ServerHttpRequest request = exchange.getRequest();
        URI uri = UriComponentsBuilder.fromHttpUrl(newPath).build().toUri();
    
        ServerHttpRequest newRequest = request.mutate().uri(uri).build();
        Route route =exchange.getAttribute(GATEWAY_ROUTE_ATTR);
        if (route ==null){
            log.error(ErrorCodeEnum.NO_PATH_ROUTE.getDesc());
            return ExceptionHandler.genErrResponse(exchange, ErrorCodeEnum.NO_PATH_ROUTE);
        }
        Route newRoute = Route.async()
                .asyncPredicate(route.getPredicate())
                .filters(route.getFilters())
                .id(route.getId())
                .order(route.getOrder())
                .uri(uri)
                .build();
        exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);
        return chain.filter(exchange.mutate().request(newRequest).build());
    }
    
    @Override
    public int getOrder() {
        return -80;
    }
    }
    

    4)集成nacos实现动态路由,需要搭建额外的nacos服务器,刷新和多节点同步由nacos实现,结合我们的业务场景,需要将nacos页面修改路由操作集成到我们的服务中,实现上复杂且性能依赖nacos
    我们这里的动态路由实现采用的是第一种方式,mysql存储防止数据丢失,redis缓存提高刷新速度,kafka消息实现多节点同步和路由刷新,下面主要介绍这种实现方案。

三、动态路由实现过程

选用自由扩展方式,使用mysql持久化路由,同时添加redis缓存提高路由刷新速度,网关服务宕机重启可以利用reis缓存快速恢复路由,redis异常或者宕机重启可以利用mysql恢复,为了保证redis缓存和mysql数据一致,增加kafka同步补偿redis缓存数据,保证增加修改删除API时,正确刷新路由避免网络抖动引起缓存刷新失败。
spring cloud gateway路由刷新有publishEvent和心跳两种方式,这里批量操作使用心跳刷新,单条使用事件即时刷新。
一下是部分核心代码,仅供参考:

package com.icss.cig.spms.integration.service.apiis.route;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.icss.cig.spms.integration.service.apiis.constant.ApiisConstant;
import com.icss.cig.spms.integration.service.apiis.constant.CommonConstant;
import com.icss.cig.spms.integration.service.apiis.model.mybatis.entity.IntegrationApiisApiRouterInfo;
import com.icss.cig.spms.integration.service.apiis.service.IntegrationApiisApiRouterInfoService;
import com.icss.cig.spms.integration.service.apiis.util.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

/**
 * @author dubin
 * @version 1.0.0
 * @ClassName RouteDefinitionManager.java
 * @Description 动态路由管理器, 要注意此类提供的增删改查方法均为反应式风格实现,执行是异步的,要实现同步调用,
 * 需要将其他逻辑与这些方法调用整合到一个Flux或者Mono中才行,否则偶尔你会看到一些意想不到的异常,或者数据不一致
 * @createTime 2022年01月21日 16:18:00
 */
@Component
@Slf4j
public class RouteDefinitionManager implements RouteDefinitionRepository, ApplicationEventPublisherAware {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private IntegrationApiisApiRouterInfoService routerInfoService;

    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        // step1:检查redis缓存,如果不为空直接从redis加载路由
        log.debug("step2------start load route definition from redis");
        boolean isExist = redisTemplate.hasKey(ApiisConstant.GATEWAY_ROUTES_NAME);
        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        if (isExist) {
            redisTemplate.opsForHash().values(ApiisConstant.GATEWAY_ROUTES_NAME).stream().forEach(routeDefinition -> {
                String str = String.valueOf(routeDefinition);
                log.info("routeDefinition==================" + str);
                Object jsonObject = JSON.parse(str);
                routeDefinitions.add(JSON.parseObject(JSON.toJSONString(jsonObject), RouteDefinition.class));
            });
        } else {
            // step2:如果redis没有缓存路由,则从数据库拉取路由,加载至内存,同时添加到redis缓存
            log.debug("step3------start load route definition from database");
            LambdaQueryWrapper<IntegrationApiisApiRouterInfo> condition =
                    Wrappers.lambdaQuery(IntegrationApiisApiRouterInfo.class);
            condition.eq(IntegrationApiisApiRouterInfo::getFDelFlag, 0);
            List<IntegrationApiisApiRouterInfo> routerInfos = routerInfoService.list(condition);
            routerInfos.stream().forEach(route -> {
                RouteDefinition routeDefinition = assembleRouteDefinition(route);
                redisTemplate.opsForHash().put(ApiisConstant.GATEWAY_ROUTES_NAME, routeDefinition.getId(),
                        JSON.toJSONString(routeDefinition));
                routeDefinitions.add(routeDefinition);
            });
        }
        return Flux.fromIterable(routeDefinitions);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            redisTemplate.opsForHash().put(ApiisConstant.GATEWAY_ROUTES_NAME, routeDefinition.getId(),
                    JSON.toJSONString(routeDefinition));
            // 刷新本地路由,改为异步MQ刷新
            return Mono.empty();
        });
    }

    public void batchSave(List<RouteDefinition> routes) {
        // 获取key编码方式
        final RedisSerializer<Object> keySerializer = (RedisSerializer<Object>) redisTemplate.getKeySerializer();
        //获取值编码方式
        final RedisSerializer<Object> valueSerializer = (RedisSerializer<Object>) redisTemplate.getValueSerializer();
        //获取key对应的byte[]
        final byte[] rawKey = keySerializer.serialize(ApiisConstant.GATEWAY_ROUTES_NAME);
        redisTemplate.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection)
                    throws DataAccessException {
                for (RouteDefinition routeDefinition : routes) {
                    byte[] rawStr = valueSerializer.serialize(routeDefinition);
                    //在set中添加数据
                    if (Objects.nonNull(rawStr)) {
                        connection.hSet(rawKey, routeDefinition.getId().getBytes(), rawStr);
                    }
                }
                // 刷新本地路由
                refreshRoutes();
                connection.closePipeline();
                return null;
            }
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> {
            if (redisTemplate.opsForHash().hasKey(ApiisConstant.GATEWAY_ROUTES_NAME, id)) {
                redisTemplate.opsForHash().delete(ApiisConstant.GATEWAY_ROUTES_NAME, id);
                // 刷新本地路由,改为异步MQ刷新
                return Mono.empty();
            }
            return Mono.defer(() -> Mono.error(
                    new NotFoundException(
                            MessageUtils.getMessage("message.gateway.publish.route.define.notExist", id))));
        });
    }

    public Mono<Void> batchDelete(Mono<List<String>> routeIds) {
        return routeIds.flatMap(ids -> {
            redisTemplate.opsForHash().delete(ApiisConstant.GATEWAY_ROUTES_NAME, ids.toArray());
            // 刷新本地路由
            refreshRoutes();
            return Mono.empty();
        });
    }

    /**
     * 把路由表数据转换成RouteDefinition对象
     *
     * @param routeDefinition
     * @return
     */
    public RouteDefinition assembleRouteDefinition(IntegrationApiisApiRouterInfo routeDefinition) {
        RouteDefinition definition = new RouteDefinition();
        String routeId = StringUtils.isNotBlank(routeDefinition.getFRouterId()) ? routeDefinition.getFRouterId() :
                UUID.randomUUID().toString();
        definition.setId(routeId);
        definition.setOrder(routeDefinition.getFRouterOrder());

        String routerParams = routeDefinition.getFRouterParams();
        if (StringUtils.isNotBlank(routerParams)) {
            JSONObject routesJson = JSON.parseObject(routerParams);
            //设置过滤器
            Assert.isTrue(routesJson.containsKey("filters"), "路由参数格式不正确");
            List<FilterDefinition> filterList =
                    routesJson.getJSONArray("filters").toJavaList(FilterDefinition.class);
            definition.setFilters(filterList);
            //设置断言
            Assert.isTrue(routesJson.containsKey("predicates"), "路由参数格式不正确");
            List<PredicateDefinition> predicatesList =
                    routesJson.getJSONArray("predicates").toJavaList(PredicateDefinition.class);
            definition.setPredicates(predicatesList);
        }
        URI uri = null;
        if (routeDefinition.getFRouterPath().startsWith(CommonConstant.HTTP_STR)) {
            uri = UriComponentsBuilder.fromHttpUrl(routeDefinition.getFRouterPath()).build().toUri();
        } else {
            // uri为 lb://consumer-service 时使用下面的方法
            uri = URI.create(routeDefinition.getFRouterPath());
        }
        definition.setUri(uri);
        return definition;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * @throws
     * @author dubin
     * @description 刷新路由配置
     * @Date 2022/1/22 21:16
     * @return: void
     */
    public void refreshRoutes() {
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
    }
}

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值