Практический обзор шлюза Microservice-API

Микросервисы
Практический обзор шлюза Microservice-API

1. Зачем вам нужен шлюз API

При развитии бизнеса серверная часть часто предоставляет интерфейсы различным клиентам, таким как приложения, веб-приложения, апплеты, сторонние производители, устройства и т. д. Поскольку технологический стек используется относительно единообразно.Spring boot webРамки разработки. Поэтому в начале был единообразно инкапсулирован интегрированный набор инструментов, таких как аутентификация, ограничение тока, политика безопасности, ведение журнала и т. д. В процессе разработки необходимо ввести только набор инструментов для реализации вышеуказанных функций.NginxЗатем запрос будет отправлен на узел соответствующего микросервисного кластера.

С расширением бизнеса открывается все больше и больше интерфейсов, а также выявляются некоторые проблемы:

  • Разрабатывайте наборы инструментов на разных языках для разных языков, напримерgo/c++Реализованные на языке микросервисные узлы;
  • Существует много политик безопасности для разных клиентов запросов, и явление настройки серьезное, что усложняет разработку инструментария;
  • Когда инструментарий обновляется, это влияет на многие серверные узлы;
  • Эффект управления параллелизмом не очень хорош, и требуется настройка текущих параметров ограничения деградации.

APIПоявление шлюза должно решить вышеуказанные проблемы, квалифицированныйAPIШлюз должен иметь следующие характеристики:

  • Высокая доступность: шлюз должен быть единственным внешним входом и выходом и должен продолжать предоставлять стабильные и надежные услуги;
  • Высокая производительность: трафик запросов будет проходить через уровень шлюза, который должен иметь дело с сценариями с большим количеством одновременных запросов;
  • Высокий уровень безопасности: может предотвратить внешний злонамеренный доступ, поддерживать цифровую подпись, проверку разрешений пользователя, черный и белый список, брандмауэр и т. д., чтобы обеспечить безопасность каждого микросервиса;
  • Высокая масштабируемость: он может предоставлять такие услуги, как управление трафиком, переадресация протоколов, мониторинг журналов и т. д., а также может обеспечивать хорошую совместимость для будущего расширения некоммерческих функций.

Таким образом, вся техническая структура была увеличена.APIУровень шлюза, через который проходит запросчикNginxПосле этого шлюз перенаправляет трафик запросов в соответствующий кластер микросервисов в соответствии с определенными политиками/правилами. Таким образом, разработка может быть сосредоточена на разработке определенного логического кода вместо того, чтобы тратить время на рассмотрение взаимодействия между интерфейсом и различными запрашивающими терминалами.

2. Выбор технологии шлюза

До этого наша команда сделала технический выбор текущего шлюза микросервисов и текущих основных функций шлюза, таких как текущее ограничение/аутентификация/мониторинг/удобство использования/ремонтопригодность/зрелость.Spring Cloud gateway、kong、OpenRestry、Zuul2、SoulПромежуточное программное обеспечение шлюза провело некоторые исследования и сравнения, и содержание данных в основном получено из соответствующих практических сводок различных официальных веб-сайтов и технических экспертов. Наконец, исходя из рассмотрения различных характеристик, использованиеSoulшлюз.

3. Врата реального боя

3.1 Техническая архитектура системы

В то время архитектура нашей серверной системы была реализована по мере того, как микросервисы, бизнес и технологии развивались в направлении промежуточной платформы, различные инфраструктуры и компоненты были завершены, а к существующему уровню API был добавлен унифицированный уровень шлюза с использованиемSoulПромежуточное ПО шлюза.

3.2 Введение в Soul Gateway

Soulадрес:Д-р Омар А.org/projects/so…промежуточное ПО шлюза относится кKong,Spring-Cloud-GatewayАсинхронный, высокопроизводительный, многоязычный, отзывчивый шлюз API, разработанный на основе отличного шлюза. Шлюз имеет следующие функциональные характеристики:

  • Поддерживает различные языки для бесшовной интеграцииDubbo,SpringCloud;
  • Широкая поддержка подключаемых модулей, аутентификация, ограничение тока, слияние, брандмауэр и т. д .;
  • Шлюз динамически настраивается с различными правилами и поддерживает различные конфигурации политики;
  • Плагины поддерживают горячую замену, легко расширяются;
  • Поддержка развертывания кластера, поддержкаA/B Test.

3.3 Архитектурный проект Soul Gateway

ЭтоSoulНа официальном сайте есть полное описание всей архитектуры развертывания и схемы шлюза.

Его можно разделить на три части:

  • soul-admin: Терминал управления, может управлять приложениями, авторизацией, плагинами, правилами переадресации и балансировки нагрузки, регулировкой текущего лимита, регистрацией метаданных поставщика услуг и т. д.

  • soul-client: в основном он предоставляется SDK, интегрированному с каждым сервисным узлом, и объединяет различные типы (Spring MVC/Dubbo/SpringCloud) Сервисный узел автоматически регистрируется на контроллере шлюза.

  • soul-web: Узел шлюза основан наSpring-reactorРеализация модели с помощью терминала управления для переадресации запросов, балансировки нагрузки трафика и других функций, с целью повышения производительности, вся информация о конфигурации реализуется через обновление подписки + локальный кеш.

4. Принцип реализации

4.1 Реактивное программирование

ReactiveМодель программирования — это решение, предложенное Microsoft для работы с высокой степенью параллелизма, и с тех пор оно быстро развивалось.JavaЕсть более распространенныеRxjavaа такжеAkkaФреймворк реактивного программирования обычно имеет следующие характеристики:

  • Управляемое событиями: в приложении, управляемом событиями, взаимодействие между компонентами достигается за счет слабо связанных производителей и потребителей. Эти события отправляются и принимаются асинхронно и без блокировки.
  • Реагирование в режиме реального времени: система отправляет данные потребителям только тогда, когда есть сообщение от производителя, вместо того, чтобы тратить ресурсы: заставлять потребителей постоянно опрашивать или ждать данных.

Необходимо пояснить несколько понятий:

  • Reactive Streamsпредставляет собой набор стандартов и спецификаций реактивного программирования;
  • Reactorосновывается наReactive StreamsНабор фреймворков реактивного программирования;
  • WebFluxкReactorна основе реализацииWebСреда реактивного программирования предметной области.

Spring Boot 2.0 поддерживает реактивное программирование WebFlux, в основном включая следующие классы компонентов:

  • Mono:Достигнутоorg.reactivestreams.Publisherинтерфейс, представляющий издатель от 0 до 1 элементов;
  • Flux:Достигнутоorg.reactivestreams.Publisherинтерфейс, представляющий издателя от 0 до N элементов;
  • Scheduler:Планировщик, управляющий реактивными потоками, обычно реализуемый различными пулами потоков.

       Springрамка открытаWebHandlerИнтерфейс для пользовательской обработки запросов на обслуживание,SoulWebHandlerкласс определен#handleметод, который, наконец, возвращаетMonoОбъект издателя событий, который можно увидеть в#handleМетод выполняет цепочку плагинов построения, и цепочка плагинов здесь используетListХранилище при запуске перейдет на сторону управления, чтобы получить информацию о списке подключаемых модулей.

public final class SoulWebHandler implements WebHandler {

    ...
    //根据配置或服务器CPU个数初始化调度器的线程数,可适当调高来提高并发性能和吞吐量
    public SoulWebHandler(final List<SoulPlugin> plugins) {
        this.plugins = plugins;
        String schedulerType = System.getProperty("soul.scheduler.type", "fixed");
        if (Objects.equals(schedulerType, "fixed")) {
            int threads = Integer.parseInt(System.getProperty(
                    "soul.work.threads",
                    "" + Math.max((Runtime.getRuntime()
                            .availableProcessors() << 1) + 1, 16)));
            scheduler = Schedulers.newParallel("soul-work-threads", threads);
        } else {
            scheduler = Schedulers.elastic();
        }
    }

    //处理请求
    @Override
    public Mono<Void> handle(final ServerWebExchange exchange) {
        //执行插件链路,并将最后的发布结果对象挂在调度器上
        return new DefaultSoulPluginChain(plugins)
            .execute(exchange).subscribeOn(scheduler);
    }
}

4.2 Встраиваемая конструкция

Цепочка ответственностиOOPМетод несвязанного проектирования в разработке с гибкой масштабируемостью.SoulВесь процесс получения клиентских запросов, проксирования запросов на серверную часть, получения внутренних ответов и ответов на внешние запросы был дополнен плагинами, которые можно расширять и развивать для всего процесса.

private static class DefaultSoulPluginChain implements SoulPluginChain {
    ...
    @Override
    public Mono<Void> execute(final ServerWebExchange exchange) {
        return Mono.defer(() -> {
            if (this.index < plugins.size()) {
                //执行插件链
                SoulPlugin plugin = plugins.get(this.index++);
                Boolean skip = plugin.skip(exchange);
                if (skip) {
                    //需要忽略执行,则跳到下一个执行容器节点
                    return this.execute(exchange);
                } else {
                    //需要执行
                    return plugin.execute(exchange, this);
                }
            } else {
                return Mono.empty();
            }
        });
    }
}

public interface SoulPlugin {
    //处理请求
    Mono<Void> execute(ServerWebExchange exchange, SoulPluginChain chain);
    //插件类型
    PluginTypeEnum pluginType();
    //插件执行顺序
    int getOrder();
    //插件名字,系统内唯一
    String named();
   //是否该忽略执行
    Boolean skip(ServerWebExchange exchange);
}

4.3 Регистрация метаданных службы

Soul-Clientна основеSpringПлатформа разрабатывает компоненты, которые автоматически регистрируют метаданные службы. Определение метаданных службы выглядит следующим образом:

@Data
public class MetaData implements Serializable {
    //应用名,唯一
    private String appName;
    //唯一路径
    private String path;
    //远程调用类型 http/dubbo
    private String rpcType;
    //服务名
    private String serviceName;
    //方法名
    private String methodName;
    //方法参数列表
    private String parameterTypes;
    //远程调用额外信息
    private String rpcExt;
    //是否有效
    private Boolean enabled;
}

SoulВ настоящее время фреймворк поддерживаетDubbo/Spring MVCРегистрация метаданных для других поставщиков услуг. кSpring MVCпоставкаhttpВ качестве примера используйте интерфейсBeanПостпроцессорное сканирование с@Controllerаннотации и@RestControllerаннотированныйbeanОбъект и извлечь соответствующую информацию о метаданных услуге, используйтеOkHttpClientОтправляя запрос на управление терминалом, служебные метаданные достигают регистрации.

public class SoulClientBeanPostProcessor implements BeanPostProcessor {  
     ...
    @Override    
    public Object postProcessAfterInitialization(
            @NonNull final Object bean, 
            @NonNull final String beanName) throws BeansException {

        //查找相关的注解参数
        Controller controller = 
                AnnotationUtils.findAnnotation(bean.getClass(), Controller.class);        RestController restController = 
                AnnotationUtils.findAnnotation(bean.getClass(), RestController.class);        RequestMapping requestMapping = 
                AnnotationUtils.findAnnotation(bean.getClass(), RequestMapping.class);

        if (controller != null || restController != null || requestMapping != null) {            String contextPath = soulHttpConfig.getContextPath();            String adminUrl = soulHttpConfig.getAdminUrl();            if (contextPath == null || "".equals(contextPath)                    || adminUrl == null || "".equals(adminUrl)) {                return bean;            }
            //获取方法集合
            final Method[] methods = 
                ReflectionUtils.getUniqueDeclaredMethods(bean.getClass());
            //逐个组装出服务元数据并发送
            for (Method method : methods) {                
                    SoulClient soulClient = AnnotationUtils.findAnnotation(method, SoulClient.class);                if (Objects.nonNull(soulClient)) {
                    //发送构造好的服务元数据
                executorService.execute(() -> 
                    post(buildJsonParams(soulClient, contextPath, bean, method)));                
            }            
        }        
   }
        
   return bean;    
   } 
}

4.4 Запрос реализации прокси

Все конструкции шлюза реализованы на основе режима прокси.Внешние запросы обрабатываются шлюзом, а затем перенаправляются на внутренний сервисный узел с помощью прокси. Формат внешнего запроса должен соответствовать следующему формату запроса.

public class RequestDTO implements Serializable {
    //模块名
    private String module;
    //方法名
    private String method;
    //远程调用类型
    private String rpcType;
    //http方法
    private String httpMethod;
    //签名内容
    private String sign;
    //请求时间戳
    private String timestamp;
    //应用key
    private String appKey;
    //http请求的路径
    private String path;
    //应用上下文路径
    private String contextPath;
    //真实的请求路径,在经过请求转发处理插件后填充
    private String realUrl;
    //服务元数据,在经过请求转发处理插件后填充
    private MetaData metaData;
    //dubbo请求参数,在经过请求转发处理插件后填充
    private String dubboParams;
    //请求开始时间戳,在经过请求转发处理插件后填充
    private LocalDateTime startDateTime;
    ...
}

Модуль управления, который находит восходящий узел, будет найден в плагине маршрутизации.UpstreamCacheManagerНайдите реальный путь запроса вышестоящего узла вurlВстраивается в контекст запроса для использования следующей нодой плагина.

public class DividePlugin extends AbstractSoulPlugin {

    private final UpstreamCacheManager upstreamCacheManager;
    ...

    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, 
            final SoulPluginChain chain, 
            final SelectorData selector, 
            final RuleData rule) {
        ...
        final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
        //获取可选择的上游服务器节点列表
        final List<DivideUpstream> upstreamList =
                upstreamCacheManager.findUpstreamListBySelectorId(selector.getId());
        ...
        //获取真实调用节点的ip
        final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
        DivideUpstream divideUpstream =
                LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
        ...
        //设置一下 http url
        String domain = buildDomain(divideUpstream);
        String realURL = buildRealURL(domain, requestDTO, exchange);
        //设置下超时时间
        return chain.execute(exchange);
    }
}

вUpStreamCacheManagerОн в основном управление узлом сервера восходящего сервера, который может принимать список кеша в кеше через идентификатор селектора в контейнере узла, а контейнер узла может получить список адресов событий динамически обновленного сервисного узла.

public class UpstreamCacheManager {

    private static final BlockingQueue<SelectorData> BLOCKING_QUEUE 
            = new LinkedBlockingQueue<>(1024);
    //一个唯一请求服务对应多个服务节点
    private static final Map<String, List<DivideUpstream>> UPSTREAM_MAP 
            = Maps.newConcurrentMap();
    ...
    public void execute(final SelectorData selectorData) {
        final List<DivideUpstream> upstreamList =
                GsonUtils.getInstance().fromList(selectorData.getHandle(), DivideUpstream.class);
        if (CollectionUtils.isNotEmpty(upstreamList)) {
            UPSTREAM_MAP.put(selectorData.getId(), upstreamList);
        } else {
            UPSTREAM_MAP.remove(selectorData.getId());
        }
    }
}

        httpЕсть два типа запросовNettyClientа такжеWebClientРеализация клиента, основной процесс запроса находится вWebClientPluginа такжеNettyHttpClientPluginОн реализован в двух классах-контейнерах классаMonoвисеть на планировщике. Услуги, предоставляемые каждым использованием службыMetaDataотметка.

public class WebClientPlugin implements SoulPlugin {

    @Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
        assert requestDTO != null;
        //在请求上下文中获取到url
        String urlPath = exchange.getAttribute(Constants.HTTP_URL);
        //获取请求方法
        HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
        //获取请求参数
        WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
       ....
       //构建新的请求
        return handleRequestBody(requestBodySpec, exchange, timeout, chain, userJson);
    }
}

относительноHttpClient,dubboРеализация прокси-сервера запроса более сложна, и необходимо обработать некоторые обобщающие вызовы. Обобщенный метод вызова интерфейса в основном используется для потребителей услуг безAPIКлассы интерфейса и классификаторы моделей (например, входные и выходные параметры)pojoclass) используются как параметры, так и возвращаемые значенияMapВыражать.

public class DubboPlugin extends AbstractSoulPlugin {

    ...
    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, 
                final SoulPluginChain chain, 
                final SelectorData selector, final RuleData rule) {
        final String body = exchange.getAttribute(Constants.DUBBO_PARAMS);
        final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
        assert requestDTO != null;
        //构建泛化服务
        final Object result = dubboProxyService.genericInvoker(body, requestDTO.getMetaData());
        //执行责任链
        return chain.execute(exchange);
    }

целыйDubboПроцесс запрос-ответ осуществляется черезDubboProxyServiceДля реализации сначала создайте обобщенный потребитель службы, а затем верните результат после отправки запроса на службу.

public class DubboProxyService {
    ...
    public Object genericInvoker(final String body, final MetaData metaData) throws SoulException {
        ReferenceConfig<GenericService> reference;
        GenericService genericService;
        try {
            //
            reference = ApplicationConfigCache.getInstance().get(metaData.getServiceName());
            ...
            genericService = reference.get();
        } catch (Exception ex) {
            ...
        }
        try {
            ...
            return genericService.$invoke(metaData.getMethodName(), new String[]{}, new Object[]{});
            ...
        } catch (GenericException e) {
            ...
        }
    }
}

        DubboВызов обобщения службы использует метод ручного создания потребителя службы.Чтобы повысить производительность,ApplicationConfigCacheСконструированный потребитель службы кэшируется.

public final class ApplicationConfigCache {

    //这里根据类/方法/参数确定服务接口的唯一性,因为每个Dubbo消费端
    //的构建成本较高,这里用了缓存池来缓存已构建的消费端
    private final LoadingCache<String, ReferenceConfig<GenericService>> cache = CacheBuilder.newBuilder()
            ....;

    //加载Dubbo泛化代理的相关配置
    public void init(final String register) {
        if (applicationConfig == null) {
            applicationConfig = new ApplicationConfig("soul_proxy");
        }
        if (registryConfig == null) {
            registryConfig = new RegistryConfig();
            registryConfig.setProtocol("dubbo");
            registryConfig.setId("soul_proxy");
            registryConfig.setRegister(false);
            registryConfig.setAddress(register);
        }
    }

    //初始化泛化服务
    public ReferenceConfig<GenericService> initRef(final MetaData metaData) {
        try {
            ReferenceConfig<GenericService> referenceConfig = cache.get(metaData.getServiceName());
            if (StringUtils.isNoneBlank(referenceConfig.getInterface())) {
                return referenceConfig;
            }
        } catch (Exception e) {
            LOG.error("init dubbo ref ex:{}", e.getMessage());
        }
        return build(metaData);
    }

    //按照服务元数据构建Dubbo泛化服务
    public ReferenceConfig<GenericService> build(final MetaData metaData) {
        ReferenceConfig<GenericService> reference = new ReferenceConfig<>();        String rpcExt = metaData.getRpcExt();
        try {
            //从前端请求中解析元数据
            DubboParamExt dubboParamExt = GsonUtils.getInstance()
                .fromJson(rpcExt, DubboParamExt.class);
            ...
        } catch (Exception e) {
            ...
        }
        try {
            //初始化
            Object obj = reference.get();
            if (obj != null) {
                //存储缓冲池中
                cache.put(metaData.getServiceName(), reference);
            }
        } catch (Exception ex) {
            ...
        }
        return reference;
    }
    ...
}

4.5 Реализация распределенного ограничения тока

Фреймворк поставляется сredisТекущая предельная реализация, используяSpringкадр для загрузкиLuaсценарий, затем пройтиredisклиент для работы.

public class RedisRateLimiter {

    public Mono<RateLimiterResponse> isAllowed(final String id, 
                final double replenishRate, final double burstCapacity) {
      
        try {
            List<String> keys = getKeys(id);
            List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
                    Instant.now().getEpochSecond() + "", "1");
            Flux<List<Long>> resultFlux = 
                Singleton.INST.get(ReactiveRedisTemplate.class)
                    .execute(this.script, keys, scriptArgs);
            return resultFlux.onErrorResume(throwable -> 
                    Flux.just(Arrays.asList(1L, -1L)))
                    .reduce(new ArrayList<Long>(), (longs, l) -> {
                        longs.addAll(l);
                        return longs;
                    }).map(results -> {
                        boolean allowed = results.get(0) == 1L;
                        Long tokensLeft = results.get(1);
                        RateLimiterResponse rateLimiterResponse = new RateLimiterResponse(allowed, tokensLeft);
                        LogUtils.debug(LOGGER, "RateLimiter response:{}", rateLimiterResponse::toString);
                        return rateLimiterResponse;
                    });
        } catch (Exception e) {
            ...
        }
        return Mono.just(new RateLimiterResponse(true, -1));
    }

по принципу временного окнаLuaсценарий.

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

4.6 Распределенный кэш конфигурации

Узел шлюза обеспечивает высокую производительность благодаря архитектуре локального кеша + кэша событий уведомлений, где локальный кеш узла шлюза хранит имя контейнера информацию о контейнере, имя контейнера селектор, селектор данные правила и т. д. , Отображение отношений.

public abstract class AbstractLocalCacheManager implements LocalCacheManager {

    /**
     * pluginName -> PluginData.
     */
    static final ConcurrentMap<String, PluginData> PLUGIN_MAP = Maps.newConcurrentMap();

    /**
     * pluginName -> SelectorData.
     */
    static final ConcurrentMap<String, List<SelectorData>> SELECTOR_MAP = Maps.newConcurrentMap();

    /**
     * selectorId -> RuleData.
     */
    static final ConcurrentMap<String, List<RuleData>> RULE_MAP = Maps.newConcurrentMap();

    /**
     * appKey -> AppAuthData.
     */
    static final ConcurrentMap<String, AppAuthData> AUTH_MAP = Maps.newConcurrentMap();

фреймворк может поддерживатьWebSocket、Zookeeperи другие технологии для синхронизации изменений локального кэша.

public class ZookeeperSyncCache extends CommonCacheHandler implements CommandLineRunner, DisposableBean {

    ...

    //监听各种节点变更事件
    @Override
    public void run(final String... args) {
        watcherData();
        watchAppAuth();
        watchMetaData();
    }

    private void watcherData() {
        final String pluginParent = ZkPathConstants.PLUGIN_PARENT;
        //如果
        if (!zkClient.exists(pluginParent)) {
            zkClient.createPersistent(pluginParent, true);
        }
        //获取插件节点的子节点
        List<String> pluginZKs = zkClient.getChildren(ZkPathConstants.buildPluginParentPath());
        for (String pluginName : pluginZKs) {
            loadPlugin(pluginName);
        }
        //订阅zk节点事件
        zkClient.subscribeChildChanges(pluginParent, (parentPath, currentChildren) -> {
            if (CollectionUtils.isNotEmpty(currentChildren)) {
                for (String pluginName : currentChildren) {
                    loadPlugin(pluginName);
                }
            }
        });
    }
 }

ВышеупомянутоеSoulОсновные технические принципы реализации шлюза, в том числе мониторинг журналов и другие высокопроизводительные схемы реализации, также используются с точки зрения производительности в производственной среде.Существует множество дизайнерских идей, которые легко понять и заслуживают изучения.

5. Резюме

Эта статья является кратким изложением некоторых реальных битв, которые я вел, когда мой бывший владелец оптимизировал архитектуру микросервиса. В ней в основном описывается роль и технический выбор шлюза API микросервиса. Техническая архитектура серверной системы в то время была все еще используется.SoulШлюз в основном реализует принцип.

использованная литература

blog.CSDN.net/Daniel7443/…Начало работы с Spring Reactor

blog.CSDN.net/Тианья Лейси…Выбор технологии шлюза

zhuanlan.zhihu.com/p/45351651Начало работы с реактором Spring