为了账号安全,请及时绑定邮箱和手机立即绑定

微服务-API网关实战小结

标签:
微服务

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网关。该网关具备以下的功能特性

  • 支持各种语言无缝集成DubboSpringCloud;
  • 丰富的插件支持鉴权限流熔断防火墙等等;
  • 网关多种规则动态配置支持各种策略配置;
  • 插件热插拔易扩展;
  • 支持集群部署支持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);
    }
}
复制代码

相对与HttpClientdubbo请求代理实现较复杂需要做一些泛化调用的处理。泛化接口调用方式主要用于服务消费端没有 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网关实现主要技术实现原理其中还有日志监控等高性能的实现设计目前还是应用在边缘服务上了并逐步接入更多的服务从生产环境的表现来说其中有很多的设计思路简单易懂值得认真研读。

5.总结

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

参考文献

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

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

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

作者代码的色彩
链接https://juejin.cn/post/6957477239390732296
著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
数据库工程师
手记
粉丝
42
获赞与收藏
203

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消