Apache ShenYu 是一个异步的,高性能的,跨语言的,响应式的
API
网关。
在ShenYu
网关中,数据同步是指,当在后台管理系统中,数据发送了更新后,如何将更新的数据同步到网关中。Apache ShenYu
网关当前支持ZooKeeper
、WebSocket
、Http长轮询
、Nacos
、Etcd
和 Consul
进行数据同步。本文的主要内容是基于Http长轮询
的数据同步源码分析。
本文基于
shenyu-2.4.0
版本进行源码分析,官网的介绍请参考 数据同步原理 。
1. Http长轮询
这里直接引用官网的相关描述:
Zookeeper
和WebSocket
数据同步的机制比较简单,而Http长轮询
则比较复杂。Apache ShenYu
借鉴了Apollo
、Nacos
的设计思想,取其精华,自己实现了Http长轮询
数据同步功能。注意,这里并非传统的ajax
长轮询!
Http长轮询
机制如上所示,Apache ShenYu
网关主动请求 shenyu-admin
的配置服务,读取超时时间为 90s
,意味着网关层请求配置服务最多会等待 90s
,这样便于 shenyu-admin
配置服务及时响应变更数据,从而实现准实时推送。
Http长轮询
机制是由网关主动请求 shenyu-admin
,所以这次的源码分析,我们从网关这一侧开始。
2. 网关数据同步
2.1 加载配置
Http长轮询
数据同步配置的加载是通过spring boot
的starter
机制,当我们引入相关依赖和在配置文件中有如下配置时,就会加载。
在pom
文件中引入依赖:
<!--shenyu data sync start use http-->
<dependency>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-spring-boot-starter-sync-data-http</artifactId>
<version>${project.version}</version>
</dependency>
在application.yml
配置文件中添加配置:
shenyu:
sync:
http:
url : http://localhost:9095
当网关启动时,配置类HttpSyncDataConfiguration
就会执行,加载相应的Bean
。
/**
* Http sync data configuration for spring boot.
*/
@Configuration
@ConditionalOnClass(HttpSyncDataService.class)
@ConditionalOnProperty(prefix = "shenyu.sync.http", name = "url")
@Slf4j
public class HttpSyncDataConfiguration {
/**
* Http sync data service.
* 创建 HttpSyncDataService
* @param httpConfig http的配置
* @param pluginSubscriber 插件数据订阅
* @param metaSubscribers 元数据订阅
* @param authSubscribers 认证数据订阅
* @return the sync data service
*/
@Bean
public SyncDataService httpSyncDataService(final ObjectProvider<HttpConfig> httpConfig, final ObjectProvider<PluginDataSubscriber> pluginSubscriber,
final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) {
log.info("you use http long pull sync shenyu data");
return new HttpSyncDataService(Objects.requireNonNull(httpConfig.getIfAvailable()), Objects.requireNonNull(pluginSubscriber.getIfAvailable()),
metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
}
/**
* Http config http config.
* 读取http的配置
* @return the http config
*/
@Bean
@ConfigurationProperties(prefix = "shenyu.sync.http")
public HttpConfig httpConfig() {
return new HttpConfig();
}
}
HttpSyncDataConfiguration
是Http长轮询
数据同步的配置类,负责创建HttpSyncDataService
(负责http
数据同步的具体实现)和HttpConfig
(admin
属性配置)。它的注解如下:
@Configuration
:表示这是一个配置类;@ConditionalOnClass(HttpSyncDataService.class)
:条件注解,表示要有HttpSyncDataService
这个类;@ConditionalOnProperty(prefix = "shenyu.sync.http", name = "url")
:条件注解,要有shenyu.sync.http.url
这个属性配置。
2.2 属性初始化
- HttpSyncDataService
在HttpSyncDataService
的构造函数中,完成属性初始化。
public class HttpSyncDataService implements SyncDataService, AutoCloseable {
// 省略了属性字段......
public HttpSyncDataService(final HttpConfig httpConfig, final PluginDataSubscriber pluginDataSubscriber, final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) {
// 1.创建数据处理器
this.factory = new DataRefreshFactory(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
// 2.获取admin属性配置
this.httpConfig = httpConfig;
// shenyu-admin的url, 多个用逗号(,)分割
this.serverList = Lists.newArrayList(Splitter.on(",").split(httpConfig.getUrl()));
// 3.创建httpClient,用于向admin发起请求
this.httpClient = createRestTemplate();
// 4.开始执行长轮询任务
this.start();
}
//......
}
上面代码中省略了其他函数和相关字段,在构造函数中完成属性的初始化,主要是:
-
创建数据处理器,用于后续缓存各种类型的数据(插件、选择器、规则、元数据和认证数据);
-
获取
admin
属性配置,主要是获取admin
的url
,admin
有可能是集群,多个用逗号(,)
分割; -
创建
httpClient
,使用的是RestTemplate
,用于向admin
发起请求;private RestTemplate createRestTemplate() { OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory(); // 建立连接超时时间为 10s factory.setConnectTimeout((int) this.connectionTimeout.toMillis()); // 网关主动请求 shenyu-admin 的配置服务,读取超时时间为 90s factory.setReadTimeout((int) HttpConstants.CLIENT_POLLING_READ_TIMEOUT); return new RestTemplate(factory); }
-
开始执行长轮询任务。
2.3 开始长轮询
- HttpSyncDataService#start()
在start()
方法中,干了两件事情,一个是获取全量数据,即请求admin
端获取所有需要同步的数据,然后将获取到的数据缓存到网关内存中。另一个是开启多线程执行长轮询任务。
private void start() {
// 只初始化一次,通过原子类实现。
RUNNING = new AtomicBoolean(false);
// It could be initialized multiple times, so you need to control that.
if (RUNNING.compareAndSet(false, true)) {
// fetch all group configs.
// 初次启动,获取全量数据
this.fetchGroupConfig(ConfigGroupEnum.values());
// 一个后台服务,一个线程
int threadSize = serverList.size();
// 自定义线程池
this.executor = new ThreadPoolExecutor(threadSize, threadSize, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
ShenyuThreadFactory.create("http-long-polling", true));
// start long polling, each server creates a thread to listen for changes.
// 开始长轮询,一个admin服务,创建一个线程用于数据同步
this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server)));
} else {
log.info("shenyu http long polling was started, executor=[{}]", executor);
}
}
2.3.1 获取全量数据
- HttpSyncDataService#fetchGroupConfig()
ShenYu
将所有需要同步的数据进行了分组,一共有5种数据类型,分别是插件、选择器、规则、元数据和认证数据。
public enum ConfigGroupEnum {
APP_AUTH, // 认证数据
PLUGIN, //插件
RULE, // 规则
SELECTOR, // 选择器
META_DATA; // 元数据
}
admin
有可能是集群,这里通过循环的方式向每个admin
发起请求,有一个执行成功了,那么向admin
获取全量数据并缓存到网关的操作就执行成功。如果出现了异常,就向下一个admin
发起请求。
private void fetchGroupConfig(final ConfigGroupEnum... groups) throws ShenyuException {
// admin有可能是集群,这里通过循环的方式向每个admin发起请求
for (int index = 0; index < this.serverList.size(); index++) {
String server = serverList.get(index);
try {
// 真正去执行
this.doFetchGroupConfig(server, groups);
// 有一个成功,就成功了,可以退出循环
break;
} catch (ShenyuException e) {
// 出现异常,尝试执行下一个
// 最后一个也执行失败了,抛出异常
// no available server, throw exception.
if (index >= serverList.size() - 1) {
throw e;
}
log.warn("fetch config fail, try another one: {}", serverList.get(index + 1));
}
}
}
- HttpSyncDataService#doFetchGroupConfig()
在此方法中,首先拼装请求参数,然后通过httpClient
发起请求,到admin
中获取数据,最后将获取到的数据更新到网关内存中。
// 向admin后台管理系统发起请求,获取所有同步数据
private void doFetchGroupConfig(final String server, final ConfigGroupEnum... groups) {
// 1. 拼请求参数,所有分组枚举类型
StringBuilder params = new StringBuilder();
for (ConfigGroupEnum groupKey : groups) {
params.append("groupKeys").append("=").append(groupKey.name()).append("&");
}
// admin端提供的接口 /configs/fetch
String url = server + "/configs/fetch?" + StringUtils.removeEnd(params.toString(), "&");
log.info("request configs: [{}]", url);
String json = null;
try {
// 2. 发起请求,获取变更数据
json =