如何设计一个高性能网关(二)
一、背景
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%。
本文作者:烟味i
本文链接:https://www.cnblogs.com/2YSP/p/17114342.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步