Soul网关源码分析-9期

文章目录

    • Soul 后台 HTTP 服务探活机制
      • 准备工作
      • HTTP服务启动时探活
      • HTTP服务关闭时探活


Soul 后台 HTTP 服务探活机制



准备工作

在之前的研究中得知, 在网关侧通过维护 UpstreamCacheManager 来管理更新服务节点, 重点的地方在接收 soul-admin 传来的 selectorData 数据. 我们在 submit() 方法上断点, 做个实验:

  1. 开启HTTP服务, 看断点处何时进入, 检查传入的 selectorDatahandler 对应的数据
  2. 关闭HTTP服务, 看断点处何时进入, 检查传入的 selectorDatahandler 对应的数据

得到的结论是, 开启or关闭 HTTP 服务, 断点处都能立即进入, 且 handler数据在服务开启时有值, 服务关闭时为null. 这说明 soul-admin 能立即检测到 HTTP 服务节点的上下线情况, 并立即传给 soul 网关.

那么我们猜测: 在 HTTP 服务注册时管理系统会及时发送更新插件元数据信息到网关端; 在 HTTP 服务下线时, 也能立即得到下线通知, 并通知给网关端.

第一点的实现并不难, 在注册时 HTTP 服务肯定会访问后台系统, 这时做通知即可. 但第二点如何做到, 两个猜测是, 要么做了类似 WebSocket 的通信监听以及心跳检测, 要么就像网关一样定时访问, 但频率肯定很高.



HTTP服务启动时探活

我们先来看看第一点的具体实现吧, 先做第一件事情, 如何找到服务注册? 将 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, 找到发送事件的类 SoulClientRegisterServiceImplregisterSpringMvc() 方法:

@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 是怎么通知网关更新缓存的了, 在 DataChangedEventDispatcheronApplicationEvent() 打上断点, 看看调用 onSelectorChanged()listener 都有哪些.

Soul网关源码分析-9期_第1张图片

HttpLongPolingDataChangedListeneronSelectorChanged() 未重写, 仍是使用 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 里有注册服务的信息:

Soul网关源码分析-9期_第2张图片


到这里 HTTP服务启动时的探活研究就结束了, 还留有一个小课题, 就是追溯 soul 后台与 soul 网关的Websocket通信建立, 这块我会专门开一期去分析两者的通信, 包括 Websocket 以外的方式.



HTTP服务关闭时探活

上一个分析完毕后, 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 关闭探活则是定时器请求节点判断活性.

你可能感兴趣的:(网关,java)