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

- 背景 -

- 设计 -
1、技术选型
- Tomcat/Jetty+NIO+Servlet3 
- Netty+NIO 
2、需求清单
3、架构设计


4、表结构设计


- 编码 -
* 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 = 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;
}
}
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并持有所有要执行的插件。
```java
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/25
*/
public class PluginChain extends AbstractShipPlugin {
   /**
* the pos point to current plugin
*/
   private int pos;
   /**
* the plugins of chain
*/
   private List<ShipPlugin> plugins;
   private final String appName;
   public PluginChain(ServerConfigProperties properties, String appName) {
       super(properties);
       this.appName = appName;
   }
   /**
* add enabled plugin to chain
*
* @param shipPlugin
*/
   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()。
```java
public interface ShipPlugin {
   /**
* lower values have higher priority
*
* @return
*/
   Integer order();
   /**
* return current plugin name
*
* @return
*/
   String name();
   Mono<Void> execute(ServerWebExchange exchange,PluginChain pluginChain);
}```
DynamicRoutePlugin继承了抽象类AbstractShipPlugin,包含了动态路由的主要业务逻辑。
```java
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/25
*/
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);
   }
   /**
* forward request to backend service
*
* @param exchange
* @param url
* @return
*/
   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));
               });
   }
   /**
* weather the http method need http body
*
* @param method
* @return
*/
   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)) {
           url = url + "?" + query;
       }
       return url;
   }
   /**
* choose an ServiceInstance according to route rule config and load balancing algorithm
*
* @param appName
* @param request
* @return
*/
   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、数据同步
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/30
*/
@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;
       }
       /**
* Regular update weight,enabled plugins to nacos instance
*/
       @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缓存。
* @Author: Ship
* @Description: sync data to local cache
* @Date: Created in 2020/12/25
*/
@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;
       }
   }
}
路由规则数据同步
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/28
*/
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:
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/28
*/
@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));
   }
}
- 测试 -
1、动态路由测试
- 本地启动nacos ,sh startup.sh -m standalone; 
- 启动ship-admin; 
- 本地启动两个ship-example实例。 
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

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
2、性能压测
- MacBook Pro 13英寸 
- 处理器 2.3 GHz 四核Intel Core i7 
- 内存 16 GB 3733 MHz LPDDR4X 
- 后端节点个数一个 
- 压测工具:wrk 
- 压测结果:20个线程,500个连接数,吞吐量大概每秒9400个请求。 


- 总结 -
内容来源于网络,版权归原作者所有。
评论
