如何设计一个高性能网关?

本文介绍了如何从零开始设计一个高性能的Java网关,选择了Spring WebFlux作为基础,利用Nacos作为服务注册与发现中心。文章详细阐述了技术选型、需求清单、架构设计和编码实现,包括动态路由、接口鉴权、负载均衡等功能。此外,还进行了性能测试,展示了网关的高吞吐量特性。
摘要由CSDN通过智能技术生成

一、前言

最近在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后,将项目划分为以下几个模块。

它们之间的关系如图:

注意:这张图与实际实现有点出入,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<ContextRefreshedEvent> {     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<String> 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 = buildRegis
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值