如何才能设计一个性能稳定好用的网关

**

如何才能设计一个性能稳定好用的网关

**
一、前言

​ 最近在github上看了soul网关的设计,突然就来了兴趣准备自己从零开始写一个高性能的网关。经过两周时间的开发,我的网关ship-gate核心功能基本都已完成,最大的缺陷就是前端功底太差没有管理后台😤。

二、设计

2.1技术选型

网关是所有请求的入口,所以要求有很高的吞吐量,为了实现这点可以使用请求异步化来解决。目前一般有以下两种方案:

Tomcat/Jetty+NIO+Servlet3
Servlet3已经支持异步,这种方案使用比较多,京东,有赞和Zuul,都用的是这种方案。

Netty+NIO
Netty为高并发而生,目前唯品会的网关使用这个策略,在唯品会的技术文章中在相同的情况下Netty是每秒30w+的吞吐量,Tomcat是13w+,可以看出是有一定的差距的,但是Netty需要自己处理HTTP协议,这一块比较麻烦。

后面发现Soul网关是基于Spring WebFlux(底层Netty)的,不用太关心HTTP协议的处理,于是决定也用Spring WebFlux。

网关的第二个特点是具备可扩展性,比如Netflix Zuul有preFilters,postFilters等在不同的阶段方便处理不同的业务,基于责任链模式将请求进行链式处理即可实现。

在微服务架构下,服务都会进行多实例部署来保证高可用,请求到达网关时,网关需要根据URL找到所有可用的实例,这时就需要服务注册和发现功能,即注册中心。

现在流行的注册中心有Apache的Zookeeper和阿里的Nacos两种(consul有点小众),因为之前写RPC框架时已经用过了Zookeeper,所以这次就选择了Nacos。

2.2需求清单

首先要明确目标,即开发一个具备哪些特性的网关,总结下后如下:

自定义路由规则

可基于version的路由规则设置,路由对象包括DEFAUL,HEADER和QUERY三种,匹配方式包括=、regex、like三种。

跨语言

HTTP协议天生跨语言

高性能

Netty本身就是一款高性能的通信框架,同时server将一些路由规则等数据缓存到JVM内存避免请求admin服务。

高可用

支持集群模式防止单节点故障,无状态。

灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。通过特性一可以实现。

接口鉴权

基于责任链模式,用户开发自己的鉴权插件即可。

负载均衡

支持多种负载均衡算法,如随机,轮询,加权轮询等。利用SPI机制可以根据配置进行动态加载。

2.3架构设计

在参考了一些优秀的网关Zuul,Spring Cloud Gateway,Soul后,将项目划分为以下几个模块。

名称 描述
ship-admin 后台管理界面,配置路由规则等
ship-server 网关服务端,核心功能模块
ship-client-spring-boot-starter 网关客户端,自动注册服务信息到注册中心
ship-common 一些公共的代码,如pojo,常量等。
它们之间的关系如图:

网关设计

注意:这张图与实际实现有点出入,Nacos push到本地缓存的那个环节没有实现,目前只有ship-sever定时轮询pull的过程。ship-admin从Nacos获取注册服务信息的过程,也改成了ServiceA启动时主动发生HTTP请求通知ship-admin。

2.4表结构设计

三、编码

3.1 ship-client-spring-boot-starter

首先创建一个spring-boot-starter命名为ship-client-spring-boot-starter,不知道如何自定义starter的可以看我以前写的《开发自己的starter》。

其核心类 AutoRegisterListener 就是在项目启动时做了两件事:

1.将服务信息注册到Nacos注册中心

2.通知ship-admin服务上线了并注册下线hook。

代码如下:

/**

  • Created by 2YSP on 2020/12/21
    */
    public class AutoRegisterListener implements ApplicationListener {

    private final static Logger LOGGER = LoggerFactory.getLogger(AutoRegisterListener.class);

    private volatile AtomicBoolean registered = new AtomicBoolean(false);

    private final ClientConfigProperties properties;

    @NacosInjected
    private NamingService namingService;

    @Autowired
    private RequestMappingHandlerMapping handlerMapping;

    private final ExecutorService pool;

    /**

    • url list to ignore
      */
      private static List ignoreUrlList = new LinkedList<>();

    static {
    ignoreUrlList.add("/error");
    }

    public AutoRegisterListener(ClientConfigProperties properties) {
    if (!check(properties)) {
    LOGGER.error(“client config port,contextPath,appName adminUrl and version can’t be empty!”);
    throw new ShipException(“client config port,contextPath,appName adminUrl and version can’t be empty!”);
    }
    this.properties = properties;
    pool = new ThreadPoolExecutor(1, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    }

    /**

    • check the ClientConfigProperties
    • @param properties
    • @return
      */
      private boolean check(ClientConfigProperties properties) {
      if (properties.getPort() == null || properties.getContextPath() == null
      || properties.getVersion() == null || properties.getAppName() == null
      || properties.getAdminUrl() == null) {
      return false;
      }
      return true;
      }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
    if (!registered.compareAndSet(false, true)) {
    return;
    }
    doRegister();
    registerShutDownHook();
    }

    /**

    • send unregister request to admin when jvm shutdown
      */
      private void registerShutDownHook() {
      final String url = “http://” + properties.getAdminUrl() + AdminConstants.UNREGISTER_PATH;
      final UnregisterAppDTO unregisterAppDTO = new UnregisterAppDTO();
      unregisterAppDTO.setAppName(properties.getAppName());
      unregisterAppDTO.setVersion(properties.getVersion());
      unregisterAppDTO.setIp(IpUtil.getLocalIpAddress());
      unregisterAppDTO.setPort(properties.getPort());
      Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      OkhttpTool.doPost(url, unregisterAppDTO);
      LOGGER.info("[{}:{}] unregister from ship-admin success!", unregisterAppDTO.getAppName(), unregisterAppDTO.getVersion());
      }));
      }

    /**

    • register all interface info to register center
      */
      private void doRegister() {
      Instance instance = new Instance();
      instance.setIp(IpUtil.getLocalIpAddress());
      instance.setPort(properties.getPort());
      instance.setEphemeral(true);
      Map<String, String> metadataMap = new HashMap<>();
      metadataMap.put(“version”, properties.getVersion());
      metadataMap.put(“appName”, properties.getAppName());
      instance.setMetadata(metadataMap);
      try {
      namingService.registerInstance(properties.getAppName(), NacosConstants.APP_GROUP_NAME, instance);
      } catch (NacosException e) {
      LOGGER.error(“register to nacos fail”, e);
      throw new ShipException(e.getErrCode(), e.getErrMsg());
      }
      LOGGER.info(“register interface info to nacos success!”);
      // send register request to ship-admin
      String url = “http://” + properties.getAdminUrl() + AdminConstants.REGISTER_PATH;
      RegisterAppDTO registerAppDTO = buildRegisterAppDTO(instance);
      OkhttpTool.doPost(url, registerAppDTO);
      LOGGER.info(“register to ship-admin success!”);
      }

    private RegisterAppDTO buildRegisterAppDTO(Instance instance) {
    RegisterAppDTO registerAppDTO = new RegisterAppDTO();
    registerAppDTO.setAppName(properties.getAppName());
    registerAppD

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值