在之前的研究中得知, 在网关侧通过维护 UpstreamCacheManager
来管理更新服务节点, 重点的地方在接收 soul-admin
传来的 selectorData
数据. 我们在 submit()
方法上断点, 做个实验:
selectorData
中 handler
对应的数据selectorData
中 handler
对应的数据得到的结论是, 开启or关闭 HTTP 服务, 断点处都能立即进入, 且 handler数据在服务开启时有值, 服务关闭时为null. 这说明 soul-admin
能立即检测到 HTTP 服务节点的上下线情况, 并立即传给 soul 网关.
那么我们猜测: 在 HTTP 服务注册时管理系统会及时发送更新插件元数据信息到网关端; 在 HTTP 服务下线时, 也能立即得到下线通知, 并通知给网关端.
第一点的实现并不难, 在注册时 HTTP 服务肯定会访问后台系统, 这时做通知即可. 但第二点如何做到, 两个猜测是, 要么做了类似 WebSocket 的通信监听以及心跳检测, 要么就像网关一样定时访问, 但频率肯定很高.
我们先来看看第一点的具体实现吧, 先做第一件事情, 如何找到服务注册? 将 HTTP 服务启动, 看看 soul-admin
后台这边的日志信息:
2021-01-20 20:55:59.984 INFO 3889 --- [0.0-9095-exec-6] o.d.s.a.l.AbstractDataChangedListener : update config cache[SELECTOR], old:{
group='SELECTOR', md5='96eb17ff1c0678cea5932b4ce30eb038', lastModifyTime=1611147341102}, updated:{
group='SELECTOR', md5='6ec47b39f62b93fddbefa8ddf9cd2951', lastModifyTime=1611147359984}
找到关键类 AbstractDataChangedListener
, 这是一个用作数据更新监听的抽象类, 我们根据日志找到它被调用的方法:
public abstract class AbstractDataChangedListener implements DataChangedListener, InitializingBean {
protected <T> void updateCache(final ConfigGroupEnum group, final List<T> data) {
String json = GsonUtils.getInstance().toJson(data);
ConfigDataCache newVal = new ConfigDataCache(group.name(), json, Md5Utils.md5(json), System.currentTimeMillis());
// 更新 CACHE 缓存
ConfigDataCache oldVal = CACHE.put(newVal.getGroup(), newVal);
LOGGER.info("update config cache[{}], old:{}, updated:{}", group, oldVal, newVal);
}
}
找找它的调用处:
@Override
public void onSelectorChanged(final List<SelectorData> changed, final DataEventTypeEnum eventType) {
if (CollectionUtils.isEmpty(changed)) {
return;
}
this.updateSelectorCache();
this.afterSelectorChanged(changed, eventType);
}
继续向上追溯, 可以看到一个名为 DataChangedEventDispatcher
的 "数据变动事件分发"类, 从名字就能看出大概功能:
@Component
public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {
@Override
public void onApplicationEvent(final DataChangedEvent event) {
for (DataChangedListener listener : listeners) {
switch (event.getGroupKey()) {
case APP_AUTH:
listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType());
break;
case PLUGIN:
listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType());
break;
case RULE:
listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType());
break;
case SELECTOR:
listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType());
break;
case META_DATA:
listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType());
break;
default:
throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
}
}
}
}
看到实现的是 ApplicationListener
就知道, 这是一个接收了 spring 事件的监听器, 借助 spring 消息通知机制, 以及自定义的事件类型完成调用解耦, 在此类统一做数据变动时的通知调用.
通过对这里的 debug, 找到发送事件的类 SoulClientRegisterServiceImpl
的 registerSpringMvc()
方法:
@Service("soulClientRegisterService")
public class SoulClientRegisterServiceImpl implements SoulClientRegisterService {
private String handlerSpringMvcSelector(final SpringMvcRegisterDTO dto) {
String contextPath = dto.getContext();
SelectorDO selectorDO = selectorService.findByName(contextPath);
String selectorId;
String uri = String.join(":", dto.getHost(), String.valueOf(dto.getPort()));
if (Objects.isNull(selectorDO)) {
selectorId = registerSelector(contextPath, dto.getRpcType(), dto.getAppName(), uri);
} else {
selectorId = selectorDO.getId();
//update upstream
String handle = selectorDO.getHandle();
String handleAdd;
DivideUpstream addDivideUpstream = buildDivideUpstream(uri);
SelectorData selectorData = selectorService.buildByName(contextPath);
if (StringUtils.isBlank(handle)) {
handleAdd = GsonUtils.getInstance().toJson(Collections.singletonList(addDivideUpstream));
} else {
List<DivideUpstream> exist = GsonUtils.getInstance().fromList(handle, DivideUpstream.class);
for (DivideUpstream upstream : exist) {
if (upstream.getUpstreamUrl().equals(addDivideUpstream.getUpstreamUrl())) {
return selectorId;
}
}
exist.add(addDivideUpstream);
handleAdd = GsonUtils.getInstance().toJson(exist);
}
selectorDO.setHandle(handleAdd);
selectorData.setHandle(handleAdd);
//更新数据库
selectorMapper.updateSelective(selectorDO);
//提交过去检查
upstreamCheckService.submit(contextPath, addDivideUpstream);
//发送更新事件
// publish change event.
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, DataEventTypeEnum.UPDATE,
Collections.singletonList(selectorData)));
}
return selectorId;
}
}
这里调用了 upstreamCheckService.submit()
, 这是后台服务节点缓存的关键了:
@Component
public class UpstreamCheckService {
// 存储服务节点信息的缓存
private static final Map<String, List<DivideUpstream>> UPSTREAM_MAP = Maps.newConcurrentMap();
// 更新缓存的方法
public void submit(final String selectorName, final DivideUpstream divideUpstream) {
if (UPSTREAM_MAP.containsKey(selectorName)) {
UPSTREAM_MAP.get(selectorName).add(divideUpstream);
} else {
UPSTREAM_MAP.put(selectorName, Lists.newArrayList(divideUpstream));
}
}
}
回到 SoulClientRegisterServiceImpl
调用链继续向上, 找到更上游的 SoulClientController
与其方法 registerSpringMvc()
:
@RestController
@RequestMapping("/soul-client")
public class SoulClientController {
@PostMapping("/springmvc-register")
public String registerSpringMvc(@RequestBody final SpringMvcRegisterDTO springMvcRegisterDTO) {
return soulClientRegisterService.registerSpringMvc(springMvcRegisterDTO);
}
}
这里已经翻到 soul-admin
开放的http服务口, 这里是触发更新缓存的入口, 看这个路径有点熟悉, 在之前的分析文章 Soul网关源码分析-1期 中也有看到, 翻找后获知, 在HTTP服务启动并收集所有服务注解信息的SpringMvcClientBeanPostProcessor
, 会使用这个路径发送它的服务信息:
public class SpringMvcClientBeanPostProcessor implements BeanPostProcessor {
public SpringMvcClientBeanPostProcessor(final SoulSpringMvcConfig soulSpringMvcConfig) {
String contextPath = soulSpringMvcConfig.getContextPath();
String adminUrl = soulSpringMvcConfig.getAdminUrl();
Integer port = soulSpringMvcConfig.getPort();
this.soulSpringMvcConfig = soulSpringMvcConfig;
// 这里写入路径
url = adminUrl + "/soul-client/springmvc-register";
executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
}
@Override
public Object postProcessBeforeInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException {
if (soulSpringMvcConfig.isFull()) {
return bean;
}
// 收集Spring controller相关注解
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 = soulSpringMvcConfig.getContextPath();
// 收集soul自定义注解
SoulSpringMvcClient clazzAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), SoulSpringMvcClient.class);
String prePath = "";
if (Objects.nonNull(clazzAnnotation)) {
if (clazzAnnotation.path().indexOf("*") > 1) {
String finalPrePath = prePath;
// 传送服务信息到 soul 后台
executorService.execute(() -> post(buildJsonParams(clazzAnnotation, contextPath, finalPrePath)));
return bean;
}
prePath = clazzAnnotation.path();
}
final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(bean.getClass());
for (Method method : methods) {
SoulSpringMvcClient soulSpringMvcClient = AnnotationUtils.findAnnotation(method, SoulSpringMvcClient.class);
if (Objects.nonNull(soulSpringMvcClient)) {
String finalPrePath = prePath;
executorService.execute(() -> post(buildJsonParams(soulSpringMvcClient, contextPath, finalPrePath)));
}
}
}
return bean;
}
private void post(final String json) {
try {
String result = OkHttpTools.getInstance().post(url, json);
if (Objects.equals(result, "success")) {
log.info("http client register success :{} " + json);
} else {
log.error("http client register error :{} " + json);
}
} catch (IOException e) {
log.error("cannot register soul admin param :{}", url + ":" + json);
}
}
}
现在来源查清楚了, 最重要的就是看看 soul-admin
是怎么通知网关更新缓存的了, 在 DataChangedEventDispatcher
的 onApplicationEvent()
打上断点, 看看调用 onSelectorChanged()
的listener
都有哪些.
HttpLongPolingDataChangedListener
的 onSelectorChanged()
未重写, 仍是使用 AbstractDataChangedListener
的方法, 也就是之前展示的 CACHE
缓存更新.
WebsocketDataChangedListener
则对方法进行了重写:
@Override
public void onSelectorChanged(final List<SelectorData> selectorDataList, final DataEventTypeEnum eventType) {
WebsocketData<SelectorData> websocketData =
new WebsocketData<>(ConfigGroupEnum.SELECTOR.name(), eventType.name(), selectorDataList);
WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
}
具体的作用就是发送Websocket信息给网关端:
public class WebsocketCollector {
private static final Set<Session> SESSION_SET = new CopyOnWriteArraySet<>();
public static void send(final String message, final DataEventTypeEnum type) {
if (StringUtils.isNotBlank(message)) {
if (DataEventTypeEnum.MYSELF == type) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
LOGGER.error("websocket send result is exception :", e);
}
return;
}
for (Session session : SESSION_SET) {
try {
// 通过session会话, 发送文本信息
session.getBasicRemote().sendText(message);
} catch (IOException e) {
LOGGER.error("websocket send result is exception :", e);
}
}
}
}
}
这里遍历了所持有的 session 集合, 发送文本信息, 以下截图可以看到, 传出json的handler
里有注册服务的信息:
到这里 HTTP服务启动时的探活研究就结束了, 还留有一个小课题, 就是追溯 soul 后台与 soul 网关的Websocket通信建立, 这块我会专门开一期去分析两者的通信, 包括 Websocket 以外的方式.
上一个分析完毕后, HTTP服务关闭时探活其实就很好分析了, 关键在 WebsocketCollector
上, 断点它的 send()
方法并将 HTTP 服务关闭, 即可追溯到调用者.
最终在 UpstreamCheckService
类这里, 发现的 Websocket 通信调用:
@Component
public class UpstreamCheckService {
@PostConstruct
public void setup() {
// ...
if (check) {
// 开启定时器, 10秒钟一次检测, 调用 scheduled()
new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), SoulThreadFactory.create("scheduled-upstream-task", false))
.scheduleWithFixedDelay(this::scheduled, 10, scheduledTime, TimeUnit.SECONDS);
}
}
private void scheduled() {
if (UPSTREAM_MAP.size() > 0) {
// 每个注册的服务节点发起检测
UPSTREAM_MAP.forEach(this::check);
}
}
private void check(final String selectorName, final List<DivideUpstream> upstreamList) {
List<DivideUpstream> successList = Lists.newArrayListWithCapacity(upstreamList.size());
for (DivideUpstream divideUpstream : upstreamList) {
// 直接请求侦测, 判断是否节点存活
final boolean pass = UpstreamCheckUtils.checkUrl(divideUpstream.getUpstreamUrl());
if (pass) {
successList.add(divideUpstream);
}
}
if (successList.size() == upstreamList.size()) {
return;
}
if (successList.size() > 0) {
UPSTREAM_MAP.put(selectorName, successList);
updateSelectorHandler(selectorName, successList);
} else {
// 检测到服务下线删除缓存
UPSTREAM_MAP.remove(selectorName);
updateSelectorHandler(selectorName, null);
}
}
private void updateSelectorHandler(final String selectorName, final List<DivideUpstream> upstreams) {
SelectorDO selector = selectorService.findByName(selectorName);
if (Objects.nonNull(selector)) {
SelectorData selectorData = selectorService.buildByName(selectorName);
if (upstreams == null) {
selector.setHandle("");
selectorData.setHandle("");
} else {
String handler = GsonUtils.getInstance().toJson(upstreams);
selector.setHandle(handler);
selectorData.setHandle(handler);
}
selectorMapper.updateSelective(selector);
//发送更新事件
// publish change event.
eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, DataEventTypeEnum.UPDATE,
Collections.singletonList(selectorData)));
}
}
}
服务下线的检测方式, 就是定时请求检测, 并广播事件给所有监听器, 其中就包括 Websocket 监听器, 它会通知网关服务已下线.
总结下, UpstreamCheckService
是维护 soul-admin
注册服务节点的缓存, 它的 HTTP 注册探活是服务注册那一刻, 去更新的缓存数据, 而 它的 HTTP 关闭探活则是定时器请求节点判断活性.