返回顶部
首页 > 资讯 > 精选 >怎么设计一个高性能网关
  • 360
分享到

怎么设计一个高性能网关

2023-06-15 16:06:51 360人浏览 安东尼
摘要

这篇文章主要介绍“怎么设计一个高性能网关”,在日常操作中,相信很多人在怎么设计一个高性能网关问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么设计一个高性能网关”的疑惑有所帮助!接下来,请跟着小编一起来学习吧

这篇文章主要介绍“怎么设计一个高性能网关”,在日常操作中,相信很多人在怎么设计一个高性能网关问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么设计一个高性能网关”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

 一、前言

最近在GitHub上看了soul网关的设计,突然就来了兴趣准备自己从零开始写一个高性能的网关。

经过两周时间的开发,我的网关ship-gate核心功能基本都已完成,最大的缺陷就是前端功底太差没有管理后台?。

二、设计

2.1 技术选型

网关是所有请求的入口,所以要求有很高的吞吐量,为了实现这点可以使用请求异步化来解决。

目前一般有以下两种方案:

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

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》。

更多 Spring Boot 教程推荐看这个:

https://github.com/javastacks/spring-boot-best-practice

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

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

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

代码如下:

  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;            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<>());      }            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();      }             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());          }));      }            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());          registerAppDTO.setContextPath(properties.getContextPath());          registerAppDTO.setIp(instance.getIp());          registerAppDTO.setPort(instance.getPort());          registerAppDTO.setVersion(properties.getVersion());          return registerAppDTO;      }  }

3.2 ship-server

ship-sever项目主要包括了两个部分内容:

请求动态路由的主流程

本地缓存数据和ship-admin及nacos同步,这部分在后面3.3再讲。

ship-server实现动态路由的原理是利用WebFilter拦截请求,然后将请求教给plugin chain去链式处理。

PluginFilter根据URL解析出appName,然后将启用的plugin组装成plugin chain。

最新 Java 核心技术教程,都在这了!

public class PluginFilter implements WebFilter {       private ServerConfigProperties properties;       public PluginFilter(ServerConfigProperties properties) {          this.properties = properties;      }       @Override      public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {          String appName = parseAppName(exchange);          if (CollectionUtils.isEmpty(ServiceCache.getAllInstances(appName))) {              throw new ShipException(ShipExceptionEnum.SERVICE_NOT_FIND);          }         PluginChain pluginChain = new PluginChain(properties, appName);          pluginChain.addPlugin(new DynamicRoutePlugin(properties));          pluginChain.addPlugin(new AuthPlugin(properties));          return pluginChain.execute(exchange, pluginChain);      }      private String parseAppName(ServerWebExchange exchange) {          RequestPath path = exchange.getRequest().getPath();         String appName = path.value().split("/")[1];          return appName;      }  }

PluginChain继承了AbstractShipPlugin并持有所有要执行的插件。

  public class PluginChain extends AbstractShipPlugin {            private int pos;            private List<ShipPlugin> plugins;      private final String appName;      public PluginChain(ServerConfigProperties properties, String appName) {          super(properties);          this.appName = appName;      }            public void addPlugin(ShipPlugin shipPlugin) {          if (plugins == null) {              plugins = new ArrayList<>();          }          if (!PluginCache.isEnabled(appName, shipPlugin.name())) {              return;          }          plugins.add(shipPlugin);          // order by the plugin's order         plugins.sort(Comparator.comparing(ShipPlugin::order));      }     @Override      public Integer order() {         return null;      }      @Override      public String name() {          return null;      }      @Override      public Mono<Void> execute(ServerWebExchange exchange, PluginChain pluginChain) {          if (pos == plugins.size()) {              return exchange.getResponse().setComplete();          }          return pluginChain.plugins.get(pos++).execute(exchange, pluginChain);      }      public String getAppName() {          return appName;      }  }

AbstractShipPlugin实现了ShipPlugin接口,并持有ServerConfigProperties配置对象。

public abstract class AbstractShipPlugin implements ShipPlugin {       protected ServerConfigProperties properties;       public AbstractShipPlugin(ServerConfigProperties properties) {          this.properties = properties;      }  }

ShipPlugin接口定义了所有插件必须实现的三个方法order(),name()和execute()。

public interface ShipPlugin {            Integer order();           String name();      Mono<Void> execute(ServerWebExchange exchange,PluginChain pluginChain);  }

DynamicRoutePlugin继承了抽象类AbstractShipPlugin,包含了动态路由的主要业务逻辑。

  public class DynamicRoutePlugin extends AbstractShipPlugin {      private final static Logger LOGGER = LoggerFactory.getLogger(DynamicRoutePlugin.class);      private static WebClient webClient;      private static final Gson gson = new GsonBuilder().create();      static {          HttpClient httpClient = HttpClient.create()                  .tcpConfiguration(client ->                          client.doOnConnected(conn ->                                  conn.addHandlerLast(new ReadTimeoutHandler(3))                                          .addHandlerLast(new WriteTimeoutHandler(3)))                                  .option(ChannelOption.TCP_nodeLAY, true)                  );          webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient))                  .build();      }      public DynamicRoutePlugin(ServerConfigProperties properties) {          super(properties);      }      @Override      public Integer order() {          return ShipPluginEnum.DYNAMIC_ROUTE.getOrder();      }      @Override      public String name() {          return ShipPluginEnum.DYNAMIC_ROUTE.getName();      }      @Override      public Mono<Void> execute(ServerWebExchange exchange, PluginChain pluginChain) {          String appName = pluginChain.getAppName();          ServiceInstance serviceInstance = chooseInstance(appName, exchange.getRequest());  //        LOGGER.info("selected instance is [{}]", gson.toJSON(serviceInstance));          // request service          String url = buildUrl(exchange, serviceInstance);          return forward(exchange, url);      }            private Mono<Void> forward(ServerWebExchange exchange, String url) {          ServerHttpRequest request = exchange.getRequest();          ServerHttpResponse response = exchange.getResponse();          HttpMethod method = request.getMethod();          WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(url).headers((headers) -> {              headers.addAll(request.getHeaders());          });          WebClient.RequestHeadersSpec<?> reqHeadersSpec;          if (requireHttpBody(method)) {              reqHeadersSpec = requestBodySpec.body(BodyInserters.fromDataBuffers(request.getBody()));          } else {              reqHeadersSpec = requestBodySpec;          }          // nio->callback->nio          return reqHeadersSpec.exchange().timeout(Duration.ofMillis(properties.getTimeOutMillis()))                  .onErrorResume(ex -> {                     return Mono.defer(() -> {                          String errorResultjson = "";                          if (ex instanceof TimeoutException) {                              errorResultJson = "{\"code\":5001,\"message\":\"network timeout\"}";                          } else {                              errorResultJson = "{\"code\":5000,\"message\":\"system error\"}";                          }                         return ShipResponseUtil.doResponse(exchange, errorResultJson);                      }).then(Mono.empty());                  }).flatMap(backendResponse -> {                      response.setStatusCode(backendResponse.statusCode());                      response.getHeaders().putAll(backendResponse.headers().asHttpHeaders());                      return response.writeWith(backendResponse.bodyToFlux(DataBuffer.class));                  });      }            private boolean requireHttpBody(HttpMethod method) {          if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH)) {              return true;          }          return false;      }      private String buildUrl(ServerWebExchange exchange, ServiceInstance serviceInstance) {          ServerHttpRequest request = exchange.getRequest();          String query = request.getURI().getQuery();          String path = request.getPath().value().replaceFirst("/" + serviceInstance.getAppName(), "");          String url = "http://" + serviceInstance.getIp() + ":" + serviceInstance.getPort() + path;          if (!StringUtils.isEmpty(query)) {              urlurl = url + "?" + query;          }          return url;      }           private ServiceInstance chooseInstance(String appName, ServerHttpRequest request) {          List<ServiceInstance> serviceInstances = ServiceCache.getAllInstances(appName);          if (CollectionUtils.isEmpty(serviceInstances)) {              LOGGER.error("service instance of {} not find", appName);              throw new ShipException(ShipExceptionEnum.SERVICE_NOT_FIND);          }          String version = matchAppVersion(appName, request);          if (StringUtils.isEmpty(version)) {              throw new ShipException("match app version error");          }          // filter serviceInstances by version          List<ServiceInstance> instances = serviceInstances.stream().filter(i -> i.getVersion().equals(version)).collect(Collectors.toList());          //Select an instance based on the load balancing alGorithm          LoadBalance loadBalance = LoadBalanceFactory.getInstance(properties.getLoadBalance(), appName, version);          ServiceInstance serviceInstance = loadBalance.chooseOne(instances);          return serviceInstance;      }      private String matchAppVersion(String appName, ServerHttpRequest request) {          List<AppRuleDTO> rules = RouteRuleCache.getRules(appName);          rules.sort(Comparator.comparing(AppRuleDTO::getPriority).reversed());          for (AppRuleDTO rule : rules) {              if (match(rule, request)) {                  return rule.getVersion();              }          }          return null;      }      private boolean match(AppRuleDTO rule, ServerHttpRequest request) {          String matchObject = rule.getMatchObject();          String matchKey = rule.getMatchKey();          String matchRule = rule.getMatchRule();          Byte matchMethod = rule.getMatchMethod();         if (MatchObjectEnum.DEFAULT.getCode().equals(matchObject)) {              return true;          } else if (MatchObjectEnum.QUERY.getCode().equals(matchObject)) {              String param = request.getQueryParams().getFirst(matchKey);              if (!StringUtils.isEmpty(param)) {                  return StringTools.match(param, matchMethod, matchRule);              }          } else if (MatchObjectEnum.HEADER.getCode().equals(matchObject)) {              HttpHeaders headers = request.getHeaders();              String headerValue = headers.getFirst(matchKey);              if (!StringUtils.isEmpty(headerValue)) {                  return StringTools.match(headerValue, matchMethod, matchRule);              }          }          return false;      }  }

3.3 数据同步

app数据同步

后台服务(如订单服务)启动时,只将服务名,版本,ip地址和端口号注册到了Nacos,并没有实例的权重和启用的插件信息怎么办?

一般在线的实例权重和插件列表都是在管理界面配置,然后动态生效的,所以需要ship-admin定时更新实例的权重和插件信息到注册中心。

对应代码ship-admin的NacosSyncListener

  @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;       @Value("${nacos.discovery.server-addr}")      private String baseUrl;       @Resource      private AppService appService;       @Override      public void onApplicationEvent(ContextRefreshedEvent event) {          if (event.getApplicationContext().getParent() != null) {              return;          }          String url = "http://" + baseUrl + NacosConstants.INSTANCE_UPDATE_PATH;          scheduledPool.scheduleWithFixedDelay(new NacosSyncTask(namingService, url, appService), 0, 30L, TimeUnit.SECONDS);      }       class NacosSyncTask implements Runnable {           private NamingService namingService;           private String url;           private AppService appService;           private Gson gson = new GsonBuilder().create();           public NacosSyncTask(NamingService namingService, String url, AppService appService) {              this.namingService = namingService;              this.url = url;              this.appService = appService;          }                    @Override          public void run() {              try {                  // get all app names                  ListView<String> services = namingService.getServicesOfServer(1, Integer.MAX_VALUE, NacosConstants.APP_GROUP_NAME);                  if (CollectionUtils.isEmpty(services.getData())) {                      return;                  }                  List<String> appNames = services.getData();                  List<AppInfoDTO> appInfos = appService.getAppInfos(appNames);                  for (AppInfoDTO appInfo : appInfos) {                      if (CollectionUtils.isEmpty(appInfo.getInstances())) {                          continue;                      }                      for (ServiceInstance instance : appInfo.getInstances()) {                          Map<String, Object> queryMap = buildQueryMap(appInfo, instance);                          String resp = OkhttpTool.doPut(url, queryMap, "");                          LOGGER.debug("response :{}", resp);                      }                  }              } catch (Exception e) {                  LOGGER.error("nacos sync task error", e);              }          }          private Map<String, Object> buildQueryMap(AppInfoDTO appInfo, ServiceInstance instance) {              Map<String, Object> map = new HashMap<>();              map.put("serviceName", appInfo.getAppName());              map.put("groupName", NacosConstants.APP_GROUP_NAME);              map.put("ip", instance.getIp());              map.put("port", instance.getPort());              map.put("weight", instance.getWeight().doubleValue());              NacosMetadata metadata = new NacosMetadata();              metadata.setAppName(appInfo.getAppName());              metadata.setVersion(instance.getVersion());              metadata.setPlugins(String.join(",", appInfo.getEnabledPlugins()));              map.put("metadata", StringTools.urlEncode(gson.toJson(metadata)));              map.put("ephemeral", true);              return map;          }      }  }

ship-server再定时从Nacos拉取app数据更新到本地Map缓存。

  @Configuration  public class DataSyncTaskListener implements ApplicationListener<ContextRefreshedEvent> {      private static ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(1,              new ShipThreadFactory("service-sync", true).create());      @NacosInjected      private NamingService namingService;      @Autowired      private ServerConfigProperties properties;      @Override      public void onApplicationEvent(ContextRefreshedEvent event) {          if (event.getApplicationContext().getParent() != null) {              return;          }          scheduledPool.scheduleWithFixedDelay(new DataSyncTask(namingService)                  , 0L, properties.getCacheRefreshInterval(), TimeUnit.SECONDS);          websocketSyncCacheServer webSocketSyncCacheServer = new WebsocketSyncCacheServer(properties.getWebSocketPort());          websocketSyncCacheServer.start();      }      class DataSyncTask implements Runnable {          private NamingService namingService;          public DataSyncTask(NamingService namingService) {              this.namingService = namingService;          }          @Override          public void run() {              try {                  // get all app names                  ListView<String> services = namingService.getServicesOfServer(1, Integer.MAX_VALUE, NacosConstants.APP_GROUP_NAME);                  if (CollectionUtils.isEmpty(services.getData())) {                      return;                  }                  List<String> appNames = services.getData();                  // get all instances                  for (String appName : appNames) {                      List<Instance> instanceList = namingService.getAllInstances(appName, NacosConstants.APP_GROUP_NAME);                      if (CollectionUtils.isEmpty(instanceList)) {                          continue;                      }                      ServiceCache.add(appName, buildServiceInstances(instanceList));                      List<String> pluginNames = getEnabledPlugins(instanceList);                      PluginCache.add(appName, pluginNames);                  }                  ServiceCache.removeExpired(appNames);                  PluginCache.removeExpired(appNames);              } catch (NacosException e) {                  e.printStackTrace();              }          }          private List<String> getEnabledPlugins(List<Instance> instanceList) {              Instance instance = instanceList.get(0);              Map<String, String> metadata = instance.getMetadata();              // plugins: DynamicRoute,Auth              String plugins = metadata.getOrDefault("plugins", ShipPluginEnum.DYNAMIC_ROUTE.getName());              return Arrays.stream(plugins.split(",")).collect(Collectors.toList());          }          private List<ServiceInstance> buildServiceInstances(List<Instance> instanceList) {              List<ServiceInstance> list = new LinkedList<>();              instanceList.forEach(instance -> {                  Map<String, String> metadata = instance.getMetadata();                  ServiceInstance serviceInstance = new ServiceInstance();                  serviceInstance.setAppName(metadata.get("appName"));                  serviceInstance.setIp(instance.getIp());                  serviceInstance.setPort(instance.getPort());                  serviceInstance.setVersion(metadata.get("version"));                 serviceInstance.setWeight((int) instance.getWeight());                  list.add(serviceInstance);              });              return list;          }      }  }

路由规则数据同步

同时,如果用户在管理后台更新了路由规则,ship-admin需要推送规则数据到ship-server,这里参考了soul网关的做法利用websocket在第一次建立连接后进行全量同步,此后路由规则发生变更就只作增量同步。

最新 Java 核心技术教程,都在这了!

服务端WebsocketSyncCacheServer:

  public class WebsocketSyncCacheServer extends WebSocketServer {      private final static Logger LOGGER = LoggerFactory.getLogger(WebsocketSyncCacheServer.class);      private Gson gson = new GsonBuilder().create();      private MessageHandler messageHandler;      public WebsocketSyncCacheServer(Integer port) {          super(new InetSocketAddress(port));          this.messageHandler = new MessageHandler();      }      @Override      public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) {          LOGGER.info("server is open");     }      @Override      public void onClose(WebSocket webSocket, int i, String s, boolean b) {          LOGGER.info("websocket server close...");      }      @Override      public void onMessage(WebSocket webSocket, String message) {          LOGGER.info("websocket server receive message:\n[{}]", message);          this.messageHandler.handler(message);      }      @Override      public void onError(WebSocket webSocket, Exception e) {      }      @Override      public void onStart() {          LOGGER.info("websocket server start...");      }      class MessageHandler {         public void handler(String message) {              RouteRuleOperationDTO operationDTO = gson.fromJson(message, RouteRuleOperationDTO.class);              if (CollectionUtils.isEmpty(operationDTO.getRuleList())) {                  return;              }              Map<String, List<AppRuleDTO>> map = operationDTO.getRuleList()                      .stream().collect(Collectors.groupingBy(AppRuleDTO::getAppName));              if (OperationTypeEnum.INSERT.getCode().equals(operationDTO.getOperationType())                      || OperationTypeEnum.UPDATE.getCode().equals(operationDTO.getOperationType())) {                  RouteRuleCache.add(map);              } else if (OperationTypeEnum.DELETE.getCode().equals(operationDTO.getOperationType())) {                  RouteRuleCache.remove(map);             }          }      }  }

客户端WebsocketSyncCacheClient:

  @Component  public class WebsocketSyncCacheClient {      private final static Logger LOGGER = LoggerFactory.getLogger(WebsocketSyncCacheClient.class);      private WebSocketClient client;      private RuleService ruleService;      private Gson gson = new GsonBuilder().create();      public WebsocketSyncCacheClient(@Value("${ship.server-web-socket-url}") String serverWebSocketUrl,                                      RuleService ruleService) {          if (StringUtils.isEmpty(serverWebSocketUrl)) {              throw new ShipException(ShipExceptionEnum.CONFIG_ERROR);          }          this.ruleService = ruleService;          ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,                  new ShipThreadFactory("websocket-connect", true).create());          try {              client = new WebSocketClient(new URI(serverWebSocketUrl)) {                  @Override                  public void onOpen(ServerHandshake serverHandshake) {                      LOGGER.info("client is open");                      List<AppRuleDTO> list = ruleService.getEnabledRule();                      String msg = gson.toJson(new RouteRuleOperationDTO(OperationTypeEnum.INSERT, list));                      send(msg);                  }                  @Override                  public void onMessage(String s) {                  }                  @Override                  public void onClose(int i, String s, boolean b) {                  }                  @Override                  public void onError(Exception e) {                      LOGGER.error("websocket client error", e);                  }              };              client.connectBlocking();              //使用调度线程池进行断线重连,30秒进行一次              executor.scheduleAtFixedRate(() -> {                  if (client != null && client.isClosed()) {                      try {                          client.reconnectBlocking();                      } catch (InterruptedException e) {                          LOGGER.error("reconnect server fail", e);                      }                  }              }, 10, 30, TimeUnit.SECONDS);          } catch (Exception e) {              LOGGER.error("websocket sync cache exception", e);              throw new ShipException(e.getMessage());          }     }      public <T> void send(T t) {          while (!client.getReadyState().equals(ReadyState.OPEN)) {              LOGGER.debug("connecting ...please wait");          }          client.send(gson.toJson(t));      }  }

四、测试

4.1 动态路由测试

1)本地启动nacos ,sh startup.sh -m standalone

2)启动ship-admin

3)本地启动两个ship-example实例。

实例1配置:

ship:   http:     app-name: order     version: gray_1.0     context-path: /order     port: 8081     admin-url: 127.0.0.1:9001  server:   port: 8081  nacos:   discovery:     server-addr: 127.0.0.1:8848

实例2配置:

ship:    http:      app-name: order      version: prod_1.0      context-path: /order      port: 8082      admin-url: 127.0.0.1:9001 server:    port: 8082  nacos:    discovery:      server-addr: 127.0.0.1:8848

4)在数据库添加路由规则配置,该规则表示当http header 中的name=ship时请求路由到gray_1.0版本的节点。

怎么设计一个高性能网关

启动ship-server,看到以下日志时则可以进行测试了。

2021-01-02 19:57:09.159  INFO 30413 --- [SocketWorker-29] cn.sp.sync.WebsocketSyncCacheServer      : websocket server receive message:    [{"operationType":"INSERT","ruleList":[{"id":1,"appId":5,"appName":"order","version":"gray_1.0","matchObject":"HEADER","matchKey":"name","matchMethod":1,"matchRule":"ship","priority":50}]}]

用Postman请求http://localhost:9000/order/user/add,POST方式,header设置name=ship,可以看到只有实例1有日志显示。

==========add user,version:gray_1.0

4.2 性能压测

压测环境:

MacBook Pro 13英寸

处理器 2.3 GHz 四核Intel Core i7

内存 16 GB 3733 MHz LPDDR4X

后端节点个数一个

压测工具:wrk

压测结果:20个线程,500个连接数,吞吐量大概每秒9400个请求。

到此,关于“怎么设计一个高性能网关”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

--结束END--

本文标题: 怎么设计一个高性能网关

本文链接: https://lsjlt.com/news/280801.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 怎么设计一个高性能网关
    这篇文章主要介绍“怎么设计一个高性能网关”,在日常操作中,相信很多人在怎么设计一个高性能网关问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么设计一个高性能网关”的疑惑有所帮助!接下来,请跟着小编一起来学习吧...
    99+
    2023-06-15
  • 怎么设计一个自己的网站
    本篇内容介绍了“怎么设计一个自己的网站”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在学生时代,笔者总想设...
    99+
    2024-04-02
  • MySQL20个高性能架构设计原则(值得收藏)
    开源数据库架构设计原则 01. 技术选型 选择成熟的平台和技术,同时是最熟悉的,能做到极致的,用好不用坏,用熟不用生。目前业界的MySQL主流分支版本有Oracle官方版本的MySQL、Percona Server、...
    99+
    2022-05-13
    MySQL 架构 MySQL 架构设计原则
  • 如何设计一个高性能的MySQL表结构来实现地理位置功能?
    如何设计一个高性能的MySQL表结构来实现地理位置功能?地理位置功能在许多应用程序中都是必不可少的,例如地图应用、附近的人、附近的商家等。在MySQL数据库中,我们可以通过合理设计表结构和使用索引来实现地理位置功能,并保证高性能的查询和更新...
    99+
    2023-10-31
    性能 MySQL 地理位置
  • 如何设计一个高性能的MySQL表结构来实现推荐美食功能?
    如何设计一个高性能的MySQL表结构来实现推荐美食功能?随着人们对美食的需求越来越高,推荐系统在美食领域的应用也逐渐增多。设计一个高性能的MySQL表结构来实现推荐美食功能,将会对提升用户体验和平台发展起到重要作用。本文将介绍如何设计这样一...
    99+
    2023-10-31
    MySQL 高性能 表结构
  • 如何设计一个高性能的MySQL表结构来实现推荐电影功能?
    如何设计一个高性能的MySQL表结构来实现推荐电影功能?近年来,推荐系统在电商、社交网络、音乐和影视等领域得到了广泛应用。其中,推荐电影功能在视频流媒体平台上尤为重要。为了实现高性能的推荐电影功能,设计一个合理的MySQL表结构是至关重要的...
    99+
    2023-10-31
    高性能 MySQL表结构 推荐电影
  • 如何设计一个高性能的MySQL表结构来实现推荐音乐功能?
    如何设计一个高性能的MySQL表结构来实现推荐音乐功能?摘要:随着音乐流媒体服务的普及,推荐音乐功能是吸引用户的重要方式之一。在实现推荐音乐功能时,合理设计MySQL表结构对于提高性能至关重要。本文将详细介绍如何设计一个高性能的MySQL表...
    99+
    2023-10-31
    MySQL 表结构 推荐音乐
  • 如何设计一个高性能的MySQL表结构来实现推荐影视功能?
    如何设计一个高性能的MySQL表结构来实现推荐影视功能?在当前互联网时代,推荐系统已经成为了各大影视平台的一项重要功能。通过推荐系统,平台可以根据用户的兴趣和行为习惯,推荐他们可能感兴趣的影视作品,提高用户的使用体验和平台的收益。而推荐系统...
    99+
    2023-10-31
    数据库索引 SQL查询优化 数据表分区
  • 如何设计一个高性能的MySQL表结构来实现日志管理功能?
    如何设计一个高性能的MySQL表结构来实现日志管理功能?随着互联网的发展,日志管理对于系统运维和故障分析变得越来越重要。MySQL作为一种常用的关系型数据库,在日志管理中也发挥着重要作用。设计一个高性能的MySQL表结构来实现日志管理功能,...
    99+
    2023-10-31
    高性能 MySQL表结构 日志管理功能
  • 如何设计一个高性能的MySQL表结构来实现推荐书籍功能?
    如何设计一个高性能的MySQL表结构来实现推荐书籍功能?推荐系统在现代电商平台和社交媒体应用中起着至关重要的作用,能够提高用户体验、增加用户黏性和促进销售。而在推荐系统中,一个关键的部分就是基于用户的兴趣和行为数据来推荐相关的书籍。在设计高...
    99+
    2023-10-31
    高性能 MySQL表 推荐书籍
  • 如何设计一个高性能的MySQL表结构来实现推荐系统功能?
    如何设计一个高性能的MySQL表结构来实现推荐系统功能?推荐系统是很多互联网平台的重要组成部分,它通过分析用户的行为和偏好,提供个性化的推荐内容。在推荐系统的实现中,数据库扮演着关键角色,因此设计一个高性能的MySQL表结构非常重要。本文将...
    99+
    2023-10-31
    推荐系统 MySQL 表结构
  • PHP中的高性能架构设计
    PHP语言作为一种开源的高级编程语言,已经成为了互联网应用开发的主流之一。与此同时,随着互联网应用的复杂性和流量的增大,越来越多的企业和开发者开始关注PHP性能的优化问题。在这篇文章中,我们将探讨PHP中的高性能架构设计。PHP的性能问题在...
    99+
    2023-05-23
    PHP 高性能 架构设计
  • 如何设计一个高性能的MySQL表结构来实现推荐电视剧功能?
    如何设计一个高性能的MySQL表结构来实现推荐电视剧功能?推荐系统在今天的电视剧平台中变得越来越重要,它不仅可以帮助用户发现新的电视剧,还可以提升用户体验。而一个高性能的MySQL表结构设计是实现这一目标的关键。本文将介绍如何设计一个高性能...
    99+
    2023-10-31
    高性能 MySQL表设计 推荐电视剧
  • 如何使用python搭建一个高性能的网站
    作为一名程序员,还是必须要会开发网站的,不然别人都会怀疑你是不是程序员了。今天,主要介绍一下如何使用python来搭建一个网站。可能有人会觉得搭建网站不都应该用java么?python的性能那么低。的确,使用java来开发网站的确要比py...
    99+
    2023-01-31
    高性能 如何使用 网站
  • 怎么设计一个RPC系统
    这篇文章主要讲解了“怎么设计一个RPC系统”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么设计一个RPC系统”吧!由于RPC底层的网络开发一般和具体使用环境有关,而编程实现手段也非常多样化...
    99+
    2023-06-02
  • 网站落地页怎么设计才能提高网站转化率
    这篇文章主要介绍了网站落地页怎么设计才能提高网站转化率,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。网站落地页作为最终用户想要确定购买服务和产品的页面,如何让用户打开网站落地...
    99+
    2023-06-09
  • windows怎么设置显卡高性能
    这篇“windows怎么设置显卡高性能”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“windows怎么设置显卡高性能”文章吧...
    99+
    2023-07-01
  • 笔记本电脑怎么设置电源计划为高性能
    小编给大家分享一下笔记本电脑怎么设置电源计划为高性能,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!右键点击桌面计算机,在打开的菜单项中选择属性;控制面板 - 所有...
    99+
    2023-06-27
  • MySQL | 05 如何设计高性能的索引?
    上回我们主要研究了为什么使用索引,以及索引的数据结构。今天带你了解如何设计高性能的索引。其中,有这么一个点,说的是 InnoDB 引擎中使用的是聚簇索引,其主索引的实现树中的叶子结点存储的是完整的数据记录,...
    99+
    2024-04-02
  • 【MySQL】高性能高可用表设计实战-表设计篇(MySQL专栏启动)
    📫作者简介:小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性...
    99+
    2023-09-14
    mysql 数据库 命名规范 范式 压缩表
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作