如何设计一个高性能网关

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何设计一个高性能网关相关的知识,希望对你有一定的参考价值。

一、背景

21年发布的开源项目ship-gate收获了100+start,但是作为网关它还缺少一项重要的能力——集群化部署的能力,有了这个能力就可以无状态的横向扩展,再通过nginx等服务器的反向代理就能极大提升网关的吞吐量。
  本文主要介绍如何实现ship-gate的集群化改造,不了解该项目的童鞋可以查看文章《如何设计一个高性能网关》。

二、集群化设计

问题点分析

ship-server是ship-gate项目的核心工程,承担着流量路由转发,接口鉴权等功能,所以需要实现ship-server的分布式部署。但是路由规则配置信息目前是通过websocket来进行ship-admin和ship-server之间一对一同步,所以需要使用其他方式实现一对多的数据同步。

解决方案

通过问题分析可以发现ship-admin和ship-server其实是一个发布/订阅关系,ship-admin发布配置信息,ship-server订阅配置信息并更新到本地缓存。

发布/订阅方案 优点 缺点
redis 暂无 不可靠消息会丢失,需要引入新的中间件
nacos配置中心 现有中间件,实现简单文档齐全 配置变更推送全量数据

对比选择了nacos配置中心的发布/订阅方案,架构图如下:

三、编码实现

3.1 ship-admin

RouteRuleConfigPublisher代替之前的WebsocketSyncCacheClient将路由规则配置发布到Nacos配置中心

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2023/2/1
 */
@Component
public class RouteRuleConfigPublisher 

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

    @Resource
    private RuleService ruleService;

    @Value("$nacos.discovery.server-addr")
    private String baseUrl;

    /**
     * must single instance
     */
    private ConfigService configService;


    @PostConstruct
    public void init() 
        try 
            configService = NacosFactory.createConfigService(baseUrl);
         catch (NacosException e) 
            throw new ShipException(ShipExceptionEnum.CONNECT_NACOS_ERROR);
        
    


    /**
     * publish service route rule config to Nacos
     */
    public void publishRouteRuleConfig() 
        List<AppRuleDTO> ruleDTOS = ruleService.getEnabledRule();
        try 
            // publish config
            String content = GsonUtils.toJson(ruleDTOS);
            boolean success = configService.publishConfig(NacosConstants.DATA_ID_NAME, NacosConstants.APP_GROUP_NAME, content);
            if (success) 
                LOGGER.info("publish service route rule config success!");
             else 
                LOGGER.error("publish service route rule config fail!");
            
         catch (NacosException e) 
            LOGGER.error("read time out or net error", e);
        
    


注意configService必须是单例的,因为其new的过程会创建线程池,多次创建可能导致CPU过高。

NacosSyncListener在项目启动后主动发布配置到Nacos

@Configuration
public class NacosSyncListener implements ApplicationListener<ContextRefreshedEvent> 

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

    private static ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(1,
            new ShipThreadFactory("nacos-sync", true).create());

    @NacosInjected
    private NamingService namingService;

    @Resource
    private AppService appService;

    @Resource
    private RouteRuleConfigPublisher routeRuleConfigPublisher;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) 
        if (event.getApplicationContext().getParent() != null) 
            return;
        
        scheduledPool.scheduleWithFixedDelay(new NacosSyncTask(namingService, appService), 0, 30L, TimeUnit.SECONDS);
        routeRuleConfigPublisher.publishRouteRuleConfig();
        LOGGER.info("NacosSyncListener init success.");
    
    
    // 省略其他代码
    

发生配置变更时,RuleEventListener同步发布配置

@Component
public class RuleEventListener 

    @Resource
    private RouteRuleConfigPublisher configPublisher;

    @EventListener
    public void onAdd(RuleAddEvent ruleAddEvent) 
        configPublisher.publishRouteRuleConfig();
    

    @EventListener
    public void onDelete(RuleDeleteEvent ruleDeleteEvent) 
        configPublisher.publishRouteRuleConfig();
    


3.2 ship-server

DataSyncTaskListener代替WebsocketSyncCacheServer在项目初始化阶段拉取全量配置信息,并订阅配置变更,同时将自身注册到Nacos。


/**
 * @Author: Ship
 * @Description: sync data to local cache
 * @Date: Created in 2020/12/25
 */
@Configuration
public class DataSyncTaskListener implements ApplicationListener<ContextRefreshedEvent> 

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

    private static ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(1,
            new ShipThreadFactory("service-sync", true).create());

    @NacosInjected
    private NamingService namingService;

    @Autowired
    private ServerConfigProperties properties;

    private static ConfigService configService;

    private Environment environment;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) 
        if (event.getApplicationContext().getParent() != null) 
            return;
        
        environment = event.getApplicationContext().getEnvironment();
        scheduledPool.scheduleWithFixedDelay(new DataSyncTask(namingService)
                , 0L, properties.getCacheRefreshInterval(), TimeUnit.SECONDS);
        registItself();
        initConfig();
    

    private void registItself() 
        Instance instance = new Instance();
        instance.setIp(IpUtil.getLocalIpAddress());
        instance.setPort(Integer.valueOf(environment.getProperty("server.port")));
        try 
            namingService.registerInstance("ship-server", NacosConstants.APP_GROUP_NAME, instance);
         catch (NacosException e) 
            throw new ShipException(ShipExceptionEnum.CONNECT_NACOS_ERROR);
        
    

    private void initConfig() 
        try 
            String serverAddr = environment.getProperty("nacos.discovery.server-addr");
            Assert.hasText(serverAddr, "nacos server addr is missing");
            configService = NacosFactory.createConfigService(serverAddr);
            // pull config in first time
            String config = configService.getConfig(NacosConstants.DATA_ID_NAME, NacosConstants.APP_GROUP_NAME, 5000);
            DataSyncTaskListener.updateConfig(config);
            // add config listener
            configService.addListener(NacosConstants.DATA_ID_NAME, NacosConstants.APP_GROUP_NAME, new Listener() 
                @Override
                public Executor getExecutor() 
                    return null;
                

                @Override
                public void receiveConfigInfo(String configInfo) 
                    LOGGER.info("receive config info:\\n", configInfo);
                    DataSyncTaskListener.updateConfig(configInfo);
                
            );
         catch (NacosException e) 
            throw new ShipException(ShipExceptionEnum.CONNECT_NACOS_ERROR);
        
    


    public static void updateConfig(String configInfo) 
        List<AppRuleDTO> list = GsonUtils.fromJson(configInfo, new TypeToken<List<AppRuleDTO>>() 
        .getType());
        Map<String, List<AppRuleDTO>> map = list.stream().collect(Collectors.groupingBy(AppRuleDTO::getAppName));
        RouteRuleCache.add(map);
        LOGGER.info("update route rule cache success");
    



四、测试总结

测试场景的部署架构如下图

4.1 启动Nacos和ship-admin

Nacos安装教程可以参考官网,输入命令startup.sh -m standalone启动。

然后启动ship-admin,输入账单admin/1234即可登录。
同时登录Nacos后台可以看到多了一个admin-route-rule的配置,里面保存的就是路由规则配置信息。

4.2 启动ship-server

为了防止本地端口号冲突,需要分别将server.port改为9002和9004,然后使用命令mvn clean package 分别打包得到ship-server-9002.jar和ship-server-9004.jar。

在控制台输入如下命令启动

java -jar ship-server-9002.jar 
java -jar ship-server-9004.jar 

通过Nacos服务列表可以看到服务已经启动成功了

4.3 启动order服务

启动ship-gate-example项目,启动成功后就可以在admin查看到。

进入路由协议管理,添加order服务的路由协议

匹配对应有三种DEFAULT,HEADER和QUERY,这里用最简单的默认方式。

4.4 nginx配置和启动

首先进入nginx配置目录,编辑nginx.conf文件配置反向代理

    upstream ship_server 
        server 127.0.0.1:9002;
        server 127.0.0.1:9004;


    server 
        listen       8888;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / 
           # proxy_pass http://ship_server;
          proxy_set_header Host $http_host;
          if ($request_uri ~* ^/(.*)$) 
                 proxy_pass http://ship_server/$1; 
          
        

启动nginx

sudo ../../opt/nginx/bin/nginx

使用ps命令查看进程存在则表示启动成功了。

4.5 集群测试和压测

集群测试

使用postman请求http://localhost:8888/order/user/test接口两次,都能得到正常响应,说明ship-server-9002和ship-server-9004都成功转发了请求到order服务。

性能压测

压测环境:

MacBook Pro 13英寸

处理器 2.3 GHz 四核Intel Core i7

内存 16 GB 3733 MHz LPDDR4X

后端节点个数一个

压测工具:wrk

压测结果:20个线程,500个连接数,持续时间60s,吞吐量大概每秒14808.20个请求,比之前单个ship-server的9400Resquests/sec提升50%。

以上是关于如何设计一个高性能网关的主要内容,如果未能解决你的问题,请参考以下文章

支持百万并发高性能网关设计实现系列:什么是网关?

支持百万并发高性能网关设计实现系列:什么是网关?

高性能网关设计实践

利用Go优越的性能 设计与实现高性能企业级微服务网关

利用Go优越的性能 设计与实现高性能企业级微服务网关

如何构建一个可用的企业级API网关?