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
Классы интерфейса и классификаторы моделей (например, входные и выходные параметры)pojo
class) используются как параметры, так и возвращаемые значения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