如何设计一个高性能网关(二)
一、背景
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%。