微服务-API网关实战小结

3,075 阅读12分钟

 1.为什么需要API网关

        在业务开发中,后台经常要暴露接口给不同的客户端如App端、网页端、小程序端、第三方厂商、设备端等使用,由于技术栈比较统一使用了Spring boot web开发框架。 所以刚开始统一封装了如鉴权、限流、安全策略、日志记录等集成的工具包,开发中只需要引入该工具包即可实现上述的功能,请求端通过Nginx后将请求到对应的微服务集群的节点上。

随着业务膨胀,暴露的接口越来越多,也暴露出来一些问题:

  • 针对不同的语言开发不同语言的工具包,如go/c++语言实现的微服务节点;
  • 不同的请求客户端的安全策略很多,定制化现象严重,导致工具包开发复杂;
  • 当工具包 更新时,会影响很多服务器节点;
  • 并发控制效果不佳,限流降级参数调整需要.

API网关的出现就是为了解决上述的问题的,一个合格的API网关应该具备以下特性:

  • 高可用: 网关需要是对外的唯一出入口,需持续提供稳定可靠的服务;
  • 高性能: 请求流量都会经过网关层,需要应对高并发请求场景;
  • 高安全性: 能够防止外部的恶意访问,支持数字签名、用户权限校验、黑白名单、防火墙等,确保各个微服务的安全;
  • 高扩展性: 能够提供流量管控、协议转发、日志监控等服务,同时能够为以后对非业务功能的扩展提供良好的兼容性.

这样整个技术架构就加多了API网关层,请求端通过Nginx后, 网关将请求流量按照具体的策略/规则转发到对应的微服务集群上。这样开发就可以把把精力集中在具体逻辑代码的开发中,而不是把时间花费在考虑接口与各种请求端交互的问题。

2.网关技术选型

        在这之前我们团队内部做了一个目前微服务网关的技术选型,对当前主流的网关功能特性如限流/鉴权/监控/易用性/可维护性/成熟度上对目前的Spring Cloud gateway、kong、OpenRestry、Zuul2、Soul网关中间件做了些调研对比,资料内容主要来源于各个官网与技术大牛的相关实践总结。最后基于对各项特性的考量,使用了Soul网关。

3.网关实战       

3.1 系统技术架构

        当时我们的后台系统架构已实现微服务化,业务与技术都朝着中台方向演进,各种基础设施与组件齐全,在已有的API层上加上了统一网关层,使用的是Soul网关中间件。

3.2 Soul网关介绍

Soul地址:dromara.org/projects/so…,该网关中间件参考了KongSpring-Cloud-Gateway等优秀的网关后设计的一个异步的,高性能的,跨语言的,响应式的API网关。该网关具备以下的功能特性:

  • 支持各种语言,无缝集成Dubbo,SpringCloud;
  • 丰富的插件支持,鉴权,限流,熔断,防火墙等等;
  • 网关多种规则动态配置,支持各种策略配置;
  • 插件热插拔,易扩展;
  • 支持集群部署,支持A/B Test

3.3 Soul网关的架构设计

       这是Soul官网介绍,对网关的整个部署架构与方案有完整的说明。

可分为三大部分:

  • soul-admin: 控制管理端,  能管理应用、授权、插件、转发与负载均衡规则、限流调整、服务提供者元数据注册等。

  • soul-client: 主要提供给各个服务节点集成的SDK,并将不同的类型(Spring MVC/Dubbo/SpringCloud)服务节点自动注册到网关控制端上。

  • soul-web: 网关节点是基于Spring-reactor模型实现,通过控制端实现请求转发、流量负载均衡等功能,为了提高性能,所有的配置信息都是通过订阅更新+本地缓存的方式实现。

4.实现原理

4.1 反应式编程

Reactive编程模型是微软为了应对高并发时提出来的一种解决方案,此后迅速发展至今,其中在Java有比较常见RxjavaAkka框架,响应式编程通常有以下几个特点:

  • 事件驱动: 在一个事件驱动 的应用程序中,组件之间的交互是通过松耦合的生产者和消费者来实现的。这些事件是以异步和非阻塞 的方式发送和接收的。
  • 实时响应: 系统在生产者是在有消息时才推送数据给消费者,而不是通过一种浪费资源方式: 让消费者不断地轮询或等待数据。

其中有几个概念需要解释下:

  • Reactive Streams 是一套反应式编程标准和规范;
  • Reactor 是基于 Reactive Streams 一套 反应式编程框架;
  • WebFluxReactor 为基础,实现 Web 领域的 反应式编程框架。

Sring Boot2.0支持了WebFulx响应式编程,主要有以下几个组件类:

  • Mono: 实现了 org.reactivestreams.Publisher 接口,代表 0 到 1 个元素的 发布者;
  • Flux: 实现了 org.reactivestreams.Publisher 接口,代表 0 到 N 个元素的发表者;
  • Scheduler: 驱动反应式流的调度器,通常由各种线程池实现。

       Spring框架开放了WebHandler接口,可对服务请求进行自定义处理, SoulWebHandler类定义了#handle方法,最后返回Mono事件发布者对象,可以看到在#handle方法中执行构行插件链,这里的插件链路使用List存储,在启动时会去控制端拉取插件列表信息。

public final class SoulWebHandler implements WebHandler {

    ...
    //根据配置或服务器CPU个数初始化调度器的线程数,可适当调高来提高并发性能和吞吐量
    public SoulWebHandler(final List<SoulPlugin> plugins) {
        this.plugins = plugins;
        String schedulerType = System.getProperty("soul.scheduler.type", "fixed");
        if (Objects.equals(schedulerType, "fixed")) {
            int threads = Integer.parseInt(System.getProperty(
                    "soul.work.threads",
                    "" + Math.max((Runtime.getRuntime()
                            .availableProcessors() << 1) + 1, 16)));
            scheduler = Schedulers.newParallel("soul-work-threads", threads);
        } else {
            scheduler = Schedulers.elastic();
        }
    }

    //处理请求
    @Override
    public Mono<Void> handle(final ServerWebExchange exchange) {
        //执行插件链路,并将最后的发布结果对象挂在调度器上
        return new DefaultSoulPluginChain(plugins)
            .execute(exchange).subscribeOn(scheduler);
    }
}

4.2 插件化设计

        责任链是OOP开发中一种用于解耦设计的方法,具备灵活的扩展性。Soul将整个接收前端请求、代理请求后端、接收后端响应、响应前端请求都做了插件化处理,可针对整个过程进行扩展开发。

private static class DefaultSoulPluginChain implements SoulPluginChain {
    ...
    @Override
    public Mono<Void> execute(final ServerWebExchange exchange) {
        return Mono.defer(() -> {
            if (this.index < plugins.size()) {
                //执行插件链
                SoulPlugin plugin = plugins.get(this.index++);
                Boolean skip = plugin.skip(exchange);
                if (skip) {
                    //需要忽略执行,则跳到下一个执行容器节点
                    return this.execute(exchange);
                } else {
                    //需要执行
                    return plugin.execute(exchange, this);
                }
            } else {
                return Mono.empty();
            }
        });
    }
}

public interface SoulPlugin {
    //处理请求
    Mono<Void> execute(ServerWebExchange exchange, SoulPluginChain chain);
    //插件类型
    PluginTypeEnum pluginType();
    //插件执行顺序
    int getOrder();
    //插件名字,系统内唯一
    String named();
   //是否该忽略执行
    Boolean skip(ServerWebExchange exchange);
}

4.3 服务元数据注册

Soul-Client基于Spring框架开发了自动注册服务元数据的组件,服务元数据的定义如下:

@Data
public class MetaData implements Serializable {
    //应用名,唯一
    private String appName;
    //唯一路径
    private String path;
    //远程调用类型 http/dubbo
    private String rpcType;
    //服务名
    private String serviceName;
    //方法名
    private String methodName;
    //方法参数列表
    private String parameterTypes;
    //远程调用额外信息
    private String rpcExt;
    //是否有效
    private Boolean enabled;
}

Soul框架目前支持了Dubbo/Spring MVC等服务提供者的元数据注册。以Spring MVC提供http接口为例,使用Bean后置处理器扫描带有@Controller注解和@RestController注解的bean对象,并提取出服务元数据的相关信息,使用OkHttpClient发送请求到管理控制端,实现服务元数据注册。

public class SoulClientBeanPostProcessor implements BeanPostProcessor {  
     ...
    @Override    
    public Object postProcessAfterInitialization(
            @NonNull final Object bean, 
            @NonNull final String beanName) throws BeansException {

        //查找相关的注解参数
        Controller controller = 
                AnnotationUtils.findAnnotation(bean.getClass(), Controller.class);        RestController restController = 
                AnnotationUtils.findAnnotation(bean.getClass(), RestController.class);        RequestMapping requestMapping = 
                AnnotationUtils.findAnnotation(bean.getClass(), RequestMapping.class);

        if (controller != null || restController != null || requestMapping != null) {            String contextPath = soulHttpConfig.getContextPath();            String adminUrl = soulHttpConfig.getAdminUrl();            if (contextPath == null || "".equals(contextPath)                    || adminUrl == null || "".equals(adminUrl)) {                return bean;            }
            //获取方法集合
            final Method[] methods = 
                ReflectionUtils.getUniqueDeclaredMethods(bean.getClass());
            //逐个组装出服务元数据并发送
            for (Method method : methods) {                
                    SoulClient soulClient = AnnotationUtils.findAnnotation(method, SoulClient.class);                if (Objects.nonNull(soulClient)) {
                    //发送构造好的服务元数据
                executorService.execute(() -> 
                    post(buildJsonParams(soulClient, contextPath, bean, method)));                
            }            
        }        
   }
        
   return bean;    
   } 
}

4.4 请求代理实现

         所有的网关设计都是基于代理模式实现的,把前端的请求经过网关处理然后代理转发到后端服务节点上。 前端请求格式都需要遵循如下的请求格式。

public class RequestDTO implements Serializable {
    //模块名
    private String module;
    //方法名
    private String method;
    //远程调用类型
    private String rpcType;
    //http方法
    private String httpMethod;
    //签名内容
    private String sign;
    //请求时间戳
    private String timestamp;
    //应用key
    private String appKey;
    //http请求的路径
    private String path;
    //应用上下文路径
    private String contextPath;
    //真实的请求路径,在经过请求转发处理插件后填充
    private String realUrl;
    //服务元数据,在经过请求转发处理插件后填充
    private MetaData metaData;
    //dubbo请求参数,在经过请求转发处理插件后填充
    private String dubboParams;
    //请求开始时间戳,在经过请求转发处理插件后填充
    private LocalDateTime startDateTime;
    ...
}

        路由插件中会到找上游节点的管理模块UpstreamCacheManager中查找上游节点真实请求路径,并将请求的url拼接入请求的上下文中给下一个插件节点使用。

public class DividePlugin extends AbstractSoulPlugin {

    private final UpstreamCacheManager upstreamCacheManager;
    ...

    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, 
            final SoulPluginChain chain, 
            final SelectorData selector, 
            final RuleData rule) {
        ...
        final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
        //获取可选择的上游服务器节点列表
        final List<DivideUpstream> upstreamList =
                upstreamCacheManager.findUpstreamListBySelectorId(selector.getId());
        ...
        //获取真实调用节点的ip
        final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
        DivideUpstream divideUpstream =
                LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
        ...
        //设置一下 http url
        String domain = buildDomain(divideUpstream);
        String realURL = buildRealURL(domain, requestDTO, exchange);
        //设置下超时时间
        return chain.execute(exchange);
    }
}

      其中UpStreamCacheManager中主要是对上游服务器节点的管理,在节点容器中可通过选择器的id获取缓存的节点列表,节点容器可接收事件动态更新存储的服务节点地址列表。

public class UpstreamCacheManager {

    private static final BlockingQueue<SelectorData> BLOCKING_QUEUE 
            = new LinkedBlockingQueue<>(1024);
    //一个唯一请求服务对应多个服务节点
    private static final Map<String, List<DivideUpstream>> UPSTREAM_MAP 
            = Maps.newConcurrentMap();
    ...
    public void execute(final SelectorData selectorData) {
        final List<DivideUpstream> upstreamList =
                GsonUtils.getInstance().fromList(selectorData.getHandle(), DivideUpstream.class);
        if (CollectionUtils.isNotEmpty(upstreamList)) {
            UPSTREAM_MAP.put(selectorData.getId(), upstreamList);
        } else {
            UPSTREAM_MAP.remove(selectorData.getId());
        }
    }
}

        http请求端有两种NettyClientWebClient客户端实现,主要的请求过程在WebClientPluginNettyHttpClientPlugin的两个容器类中实现,主要是将前端的请求信息重新填充构建一个一摸一样的请求然后再请求到后端的服务节点上,并将返回的Mono挂到调度器上。每个服务提供出来的服务都用MetaData标记。

public class WebClientPlugin implements SoulPlugin {

    @Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
        assert requestDTO != null;
        //在请求上下文中获取到url
        String urlPath = exchange.getAttribute(Constants.HTTP_URL);
        //获取请求方法
        HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
        //获取请求参数
        WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
       ....
       //构建新的请求
        return handleRequestBody(requestBodySpec, exchange, timeout, chain, userJson);
    }
}

        相对与HttpClient,dubbo请求代理实现较复杂,需要做一些泛化调用的处理。泛化接口调用方式主要用于服务消费端没有 API 接口类及模型类元(比如入参和出参的pojo类)的情况,参数及返回值中均用 Map 表示。

public class DubboPlugin extends AbstractSoulPlugin {

    ...
    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, 
                final SoulPluginChain chain, 
                final SelectorData selector, final RuleData rule) {
        final String body = exchange.getAttribute(Constants.DUBBO_PARAMS);
        final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
        assert requestDTO != null;
        //构建泛化服务
        final Object result = dubboProxyService.genericInvoker(body, requestDTO.getMetaData());
        //执行责任链
        return chain.execute(exchange);
    }

         整个Dubbo请求-响应过程是通过DubboProxyService实现的,先构造泛化的服务消费端然后在进行服务请求后返回结果。

public class DubboProxyService {
    ...
    public Object genericInvoker(final String body, final MetaData metaData) throws SoulException {
        ReferenceConfig<GenericService> reference;
        GenericService genericService;
        try {
            //
            reference = ApplicationConfigCache.getInstance().get(metaData.getServiceName());
            ...
            genericService = reference.get();
        } catch (Exception ex) {
            ...
        }
        try {
            ...
            return genericService.$invoke(metaData.getMethodName(), new String[]{}, new Object[]{});
            ...
        } catch (GenericException e) {
            ...
        }
    }
}

        Dubbo服务泛化调用使用手动构造服务消费端的方式,为了提高性能,ApplicationConfigCache将已构好的服务消费端做了缓存处理。

public final class ApplicationConfigCache {

    //这里根据类/方法/参数确定服务接口的唯一性,因为每个Dubbo消费端
    //的构建成本较高,这里用了缓存池来缓存已构建的消费端
    private final LoadingCache<String, ReferenceConfig<GenericService>> cache = CacheBuilder.newBuilder()
            ....;

    //加载Dubbo泛化代理的相关配置
    public void init(final String register) {
        if (applicationConfig == null) {
            applicationConfig = new ApplicationConfig("soul_proxy");
        }
        if (registryConfig == null) {
            registryConfig = new RegistryConfig();
            registryConfig.setProtocol("dubbo");
            registryConfig.setId("soul_proxy");
            registryConfig.setRegister(false);
            registryConfig.setAddress(register);
        }
    }

    //初始化泛化服务
    public ReferenceConfig<GenericService> initRef(final MetaData metaData) {
        try {
            ReferenceConfig<GenericService> referenceConfig = cache.get(metaData.getServiceName());
            if (StringUtils.isNoneBlank(referenceConfig.getInterface())) {
                return referenceConfig;
            }
        } catch (Exception e) {
            LOG.error("init dubbo ref ex:{}", e.getMessage());
        }
        return build(metaData);
    }

    //按照服务元数据构建Dubbo泛化服务
    public ReferenceConfig<GenericService> build(final MetaData metaData) {
        ReferenceConfig<GenericService> reference = new ReferenceConfig<>();        String rpcExt = metaData.getRpcExt();
        try {
            //从前端请求中解析元数据
            DubboParamExt dubboParamExt = GsonUtils.getInstance()
                .fromJson(rpcExt, DubboParamExt.class);
            ...
        } catch (Exception e) {
            ...
        }
        try {
            //初始化
            Object obj = reference.get();
            if (obj != null) {
                //存储缓冲池中
                cache.put(metaData.getServiceName(), reference);
            }
        } catch (Exception ex) {
            ...
        }
        return reference;
    }
    ...
}

4.5 分布式限流实现

     框架自带了基于redis的限流实现,使用了Spring框架去加载 Lua脚本,然后通过redis客户端去操作。

public class RedisRateLimiter {

    public Mono<RateLimiterResponse> isAllowed(final String id, 
                final double replenishRate, final double burstCapacity) {
      
        try {
            List<String> keys = getKeys(id);
            List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
                    Instant.now().getEpochSecond() + "", "1");
            Flux<List<Long>> resultFlux = 
                Singleton.INST.get(ReactiveRedisTemplate.class)
                    .execute(this.script, keys, scriptArgs);
            return resultFlux.onErrorResume(throwable -> 
                    Flux.just(Arrays.asList(1L, -1L)))
                    .reduce(new ArrayList<Long>(), (longs, l) -> {
                        longs.addAll(l);
                        return longs;
                    }).map(results -> {
                        boolean allowed = results.get(0) == 1L;
                        Long tokensLeft = results.get(1);
                        RateLimiterResponse rateLimiterResponse = new RateLimiterResponse(allowed, tokensLeft);
                        LogUtils.debug(LOGGER, "RateLimiter response:{}", rateLimiterResponse::toString);
                        return rateLimiterResponse;
                    });
        } catch (Exception e) {
            ...
        }
        return Mono.just(new RateLimiterResponse(true, -1));
    }

基于时间窗口原理的Lua脚本。

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

4.6 分布式配置缓存

        网关节点为了高性能特定设计成本地缓存+通知事件的缓存架构,其中网关节点的本地缓存中存储了容器名<->容器信息、容器名<->选择器,选择器<->规则数据等映射关系。

public abstract class AbstractLocalCacheManager implements LocalCacheManager {

    /**
     * pluginName -> PluginData.
     */
    static final ConcurrentMap<String, PluginData> PLUGIN_MAP = Maps.newConcurrentMap();

    /**
     * pluginName -> SelectorData.
     */
    static final ConcurrentMap<String, List<SelectorData>> SELECTOR_MAP = Maps.newConcurrentMap();

    /**
     * selectorId -> RuleData.
     */
    static final ConcurrentMap<String, List<RuleData>> RULE_MAP = Maps.newConcurrentMap();

    /**
     * appKey -> AppAuthData.
     */
    static final ConcurrentMap<String, AppAuthData> AUTH_MAP = Maps.newConcurrentMap();

 框架可支持WebSocket、Zookeeper等技术进行本地缓存更改的同步。

public class ZookeeperSyncCache extends CommonCacheHandler implements CommandLineRunner, DisposableBean {

    ...

    //监听各种节点变更事件
    @Override
    public void run(final String... args) {
        watcherData();
        watchAppAuth();
        watchMetaData();
    }

    private void watcherData() {
        final String pluginParent = ZkPathConstants.PLUGIN_PARENT;
        //如果
        if (!zkClient.exists(pluginParent)) {
            zkClient.createPersistent(pluginParent, true);
        }
        //获取插件节点的子节点
        List<String> pluginZKs = zkClient.getChildren(ZkPathConstants.buildPluginParentPath());
        for (String pluginName : pluginZKs) {
            loadPlugin(pluginName);
        }
        //订阅zk节点事件
        zkClient.subscribeChildChanges(pluginParent, (parentPath, currentChildren) -> {
            if (CollectionUtils.isNotEmpty(currentChildren)) {
                for (String pluginName : currentChildren) {
                    loadPlugin(pluginName);
                }
            }
        });
    }
 }

        以上就是Soul网关实现主要技术实现原理,其中还有日志监控等高性能的实现设计,从生产环境的表现来说,也使用4其中有很多的设计思路简单易懂,值得认真研读。

5.总结

        本文是我在前东家在做微服务架构优化时做的一些实战总结,主要讲述了微服务API网关的作用与技术选型,当时的后台系统的技术架构还有正在使用的Soul网关主要实现原理。

参考文献

blog.csdn.net/daniel7443/… spring reactor入门

blog.csdn.net/tianyaleixi… 网关技术选型

zhuanlan.zhihu.com/p/45351651 Spring reactor入门