Ali end face: como projetar um gateway de alto desempenho?

I. Introdução

Recentemente, vi o design do soul gateway no github e, de repente, fiquei interessado em escrever um gateway de alto desempenho do zero. Após duas semanas de desenvolvimento, as funções principais do meu gateway ship-gate foram basicamente concluídas.A maior falha é que as habilidades de front-end são muito fracas e não há experiência em gerenciamento.

2. Projeto

2.1 Seleção técnica

O gateway é o ponto de entrada para todas as requisições, por isso requer alto throughput, para isso, pode-se utilizar a assincronia de requisições para resolvê-lo. Atualmente existem geralmente duas opções:

  • Tomcat/Jetty+NIO+Servlet3

O Servlet3 já suporta assíncrono, esse tipo de esquema é mais usado, Jingdong, Youzan e Zuul todos usam esse tipo de esquema.

  • Netty+NIO

O Netty nasce para alta simultaneidade. Atualmente, o gateway da Vipshop usa essa estratégia. No artigo técnico da Vipshop, nas mesmas condições, o Netty tem um throughput de 30w+ por segundo e o Tomcat é de 13w+. tem certeza Há uma lacuna, mas o Netty precisa lidar com o protocolo HTTP sozinho, o que é mais problemático.

Mais tarde, descobriu-se que o gateway Soul é baseado no Spring WebFlux (o Netty subjacente), e você não precisa se preocupar muito com o processamento do protocolo HTTP, então decidi usar o Spring WebFlux também.

A segunda característica dos gateways é a escalabilidade, por exemplo, Netflix Zuul possui preFilters, postFilters, etc.

Na arquitetura de microsserviço, os serviços são implantados em várias instâncias para garantir alta disponibilidade. Quando uma solicitação chega ao gateway, o gateway precisa encontrar todas as instâncias disponíveis de acordo com a URL. Neste momento, é necessário o registro do serviço e a função de descoberta, isto é, o registro.

Existem dois registros populares agora, o Apache's Zookeeper e o Ali's Nacos (consul é um pouco de nicho) Como o Zookeeper já foi usado para escrever frameworks RPC antes, eu escolhi o Nacos desta vez.

2.2 Lista de requisitos

Antes de mais nada, é necessário esclarecer o objetivo, ou seja, desenvolver um gateway com quais recursos. O resumo é o seguinte:

  • As regras de roteamento personalizadas podem ser definidas com base nas regras de roteamento da versão, os objetos de roteamento incluem DEFAUL, HEADER e QUERY, e os métodos correspondentes incluem =, regex e similares.
  • O protocolo HTTP de linguagem cruzada é inerentemente de linguagem cruzada
  • O próprio Netty de alto desempenho é uma estrutura de comunicação de alto desempenho.Ao mesmo tempo, o servidor armazena em cache algumas regras de roteamento e outros dados na memória JVM para evitar a solicitação de serviços administrativos.
  • A alta disponibilidade oferece suporte ao modo de cluster para evitar falhas de nó único, sem estado.
  • Publicação em escala de cinza Publicação em escala de cinza (também conhecida como publicação canário) refere-se a um método de publicação que pode fazer a transição suave entre preto e branco. O teste A/B pode ser realizado nele, ou seja, permitir que alguns usuários continuem a usar o recurso A do produto e alguns usuários comecem a usar o recurso B do produto. Se os usuários não tiverem objeção a B, expanda gradualmente o escopo e migre todos usuários para B. Venha. Isso pode ser alcançado através do recurso um.
  • A autenticação de interface é baseada no modelo de cadeia de responsabilidade e os usuários podem desenvolver seus próprios plug-ins de autenticação.
  • O balanceamento de carga oferece suporte a vários algoritmos de balanceamento de carga, como aleatório, round-robin, round-robin ponderado, etc. Usando o mecanismo SPI, ele pode ser carregado dinamicamente de acordo com a configuração.

2.3 Projeto de Arquitetura

Após referenciar alguns excelentes gateways Zuul, Spring Cloud Gateway, Soul, o projeto é dividido nos seguintes módulos.

A relação entre eles é mostrada na figura:

Nota: Esta imagem é um pouco diferente da implementação real. O link de push do Nacos para o cache local não foi implementado. Atualmente, apenas o servidor de envio consulta regularmente o processo de pull. O processo para o administrador do navio obter informações do serviço de registro da Nacos também foi alterado para notificar o administrador do navio sobre uma solicitação HTTP quando o ServiceA for iniciado.

2.4 Projeto da estrutura da mesa

3. Codificação

3.1 ship-client-spring-boot-starter

Primeiro crie um spring-boot-starter chamado ship-client-spring-boot-starter, se você não sabe como customizar o starter, você pode ler o "Desenvolvendo seu próprio Starter" que eu escrevi antes.

Sua classe principal  AutoRegisterListener  faz duas coisas quando o projeto é iniciado:

1. Registre as informações do serviço no centro de registro da Nacos

2. Notifique que o serviço ship-admin está online e registre o gancho offline.

código mostrar como abaixo:

/**
 * 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;
    }
}

3.2 navio-servidor

O projeto de separação de navios inclui principalmente duas partes,

1. O principal processo de solicitação de roteamento dinâmico

2. Os dados do cache local são sincronizados com ship-admin e nacos.Esta parte será discutida posteriormente em 3.3.

O princípio da implementação de roteamento dinâmico do servidor de envio é usar o WebFilter para interceptar a solicitação e, em seguida, entregar a solicitação à cadeia de plug-ins para processamento da cadeia.

PluginFilter analisa o appName de acordo com a URL e, em seguida, monta os plug-ins habilitados em uma cadeia de plug-ins.

PluginChain herda AbstractShipPlugin e mantém todos os plugins a serem executados.

/**
 * @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 implementa a interface ShipPlugin e mantém o objeto de configuração ServerConfigProperties.

A interface ShipPlugin define três métodos order(), name() e execute() que todos os plugins devem implementar.

DynamicRoutePlugin herda a classe abstrata AbstractShipPlugin e contém a principal lógica de negócios do roteamento dinâmico.

/**
 * @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.3 Sincronização de dados

sincronização de dados do aplicativo

Quando o serviço em segundo plano (como o serviço de pedidos) é iniciado, apenas o nome do serviço, versão, endereço IP e número da porta são registrados no Nacos, mas não há informações sobre o peso da instância e o plug-in ativado. O que devo fazer?

Geralmente, os pesos das instâncias online e as listas de plug-ins são configurados na interface de gerenciamento e, em seguida, entram em vigor dinamicamente.Portanto, o ship-admin precisa atualizar regularmente o peso da instância e as informações de plug-in para o registro.

NacosSyncListener correspondente ao código ship-admin

/**
 * @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;
        }
    }
}

O servidor de envio extrai periodicamente os dados do aplicativo do Nacos e os atualiza para o cache do mapa local.

/**
 * @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;
        }
    }
}

Sincronização de dados de regras de roteamento

Ao mesmo tempo, se o usuário atualizar as regras de roteamento no plano de fundo de gerenciamento, o administrador do navio precisa enviar os dados da regra para o servidor do navio. Aqui nos referimos ao método do gateway da alma e usamos o websocket para realizar a sincronização completa após o estabelecimento da primeira conexão, as regras de roteamento são alteradas, somente sincronização incremental.

Servidor WebsocketSyncCacheServer:

/**
 * @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);
            }
        }
    }
}
 

Cliente 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));
    }
}

4. Teste

4.1 Teste de Roteamento Dinâmico

Inicie o nacos localmente, sh startup.sh -m standalone

iniciar o administrador do navio

Inicie duas instâncias de exemplo de envio localmente.

Configuração do exemplo 1:

Configuração do exemplo 2:

  1. Adicione uma configuração de regra de roteamento no banco de dados, que indica que quando name=ship no cabeçalho http, a solicitação é roteada para o nó da versão gray_1.0.

  1. Inicie o servidor de envio e você poderá testá-lo quando vir o log a seguir.
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}]}]

6. Use o Postman para solicitar
http://localhost:9000/order/user/add, método POST, defina name=ship no cabeçalho e você poderá ver que apenas a instância 1 tem os logs exibidos.

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

4.2 Teste de estresse de desempenho

Ambiente de teste de estresse:

MacBook Pro 13 polegadas

Processador Intel Core i7 quad-core de 2,3 GHz

Memória 16 GB 3733 MHz LPDDR4X

Um nó de back-end

Ferramenta de medição de pressão: wrk

Resultados do teste de pressão: 20 threads, 500 conexões e uma taxa de transferência de cerca de 9.400 solicitações por segundo.

V. Resumo

​ Uma jornada de mil milhas começa com um único passo. No começo, pensei que seria difícil escrever um gateway, mas quando você realmente começar a agir, verá que não é tão difícil assim, por isso é importante Dê o primeiro passo. Também encontrei muitos problemas no processo. Também levantei duas questões para os dois projetos de código aberto soul e nacos no github. Mais tarde, descobri que era meu próprio problema, o que foi embaraçoso.

Acho que você gosta

Origin blog.csdn.net/m0_63437643/article/details/123794162
Recomendado
Clasificación