Apache Shenyu 服务同步

214 阅读7分钟

项目介绍

Apache ShenYu 是一个 高性能,多协议,易扩展,响应式的API网关

兼容各种主流框架体系,支持热插拔,用户可以定制化开发,满足用户各种场景的现状和未来需求,经历过大规模场景的锤炼

ShenYu 官网

Github 地址

同步什么信息,有几种方式

ShenYu网关中,数据同步是指,当在后台管理系统中,数据发送了更新后,如何将更新的数据同步到网关中。Apache ShenYu 网关当前支持ZooKeeperWebSocketHttp长轮询NacosetcdConsul 进行数据同步。

img

在最初的版本中,配置服务依赖 Zookeeper 实现,管理后台将变更信息 push 给网关。而现在可以支持 WebSocketHttp长轮询ZookeeperNacosEtcdConsul,通过在配置文件中设置 shenyu.sync.${strategy} 指定对应的同步策略,默认使用 WebSocket 同步策略,可以做到秒级数据同步。但是,有一点需要注意的是,Apache ShenYu网关 和 shenyu-admin 必须使用相同的同步策略。

如下图所示,shenyu-admin 在用户发生配置变更之后,会通过 EventPublisher 发出配置变更通知,由 EventDispatcher 处理该变更通知,然后根据配置的同步策略(Http、WebSocket、Zookeeper、Nacos、Etcd、Consul),将配置发送给对应的事件处理器。

  • 如果是 WebSocket 同步策略,则将变更后的数据主动推送给 shenyu-web,并且在网关层,会有对应的 WebsocketDataHandler 处理器来处理 shenyu-admin 的数据推送。
  • 如果是 Zookeeper 同步策略,将变更数据更新到 Zookeeper,而 ZookeeperSyncCache 会监听到 Zookeeper 的数据变更,并予以处理。
  • 如果是 Http 同步策略,由网关主动发起长轮询请求,默认有 90s 超时时间,如果 shenyu-admin 没有数据变更,则会阻塞 Http 请求,如果有数据发生变更则响应变更的数据信息,如果超过 60s 仍然没有数据变更则响应空数据,网关层接到响应后,继续发起 Http 请求,反复同样的请求。

img

本文以WebSocket为例,从源码角度出发说明同步的具体过程。

什么是webscoket

WebSocket协议诞生于2008年,在2011年成为国际标准。它可以双向通信,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。WebSocket协议建立在 TCP 协议之上,属于应用层,性能开销小,通信高效,协议标识符是ws

Admin数据同步

通过一个实际案例,说明数据同步的过程:

img

接收数据

SelectorController.createSelector()

进入SelectorController类中的createSelector()方法,它负责数据的校验,添加或更新数据,返回结果信息。

@Validated
@RestController
@RequestMapping("/selector")
public class SelectorController implements PagedController<SelectorQueryCondition, SelectorVO> {
    private final SelectorService selectorService;
    
    /**
     * create selector.
     *
     * @param selectorDTO selector.
     * @return {@linkplain ShenyuAdminResult}
     */
    @PostMapping("")
    public ShenyuAdminResult createSelector(@Valid @RequestBody final SelectorDTO selectorDTO) {
        // 添加或更新数据
        Integer createCount = selectorService.createOrUpdate(selectorDTO);
        // 返回结果信息
        return ShenyuAdminResult.success(ShenyuResultMessage.CREATE_SUCCESS, createCount);
    }
}

处理数据

SelectorServiceImpl类中通过createOrUpdate()方法完成数据的转换,保存到数据库,发布事件,更新upstream

default int createOrUpdate(SelectorDTO selectorDTO) {
    //条件判断
    if (Objects.equals(SelectorTypeEnum.CUSTOM_FLOW.getCode(), selectorDTO.getType())) {
        Assert.notNull(selectorDTO.getMatchMode(), "if type is custom, matchMode is not null");
        Assert.notEmpty(selectorDTO.getSelectorConditions(), "if type is custom, selectorConditions is not empty");
        selectorDTO.getSelectorConditions().forEach(selectorConditionDTO -> {
            Assert.notBlack(selectorConditionDTO.getParamType(), "if type is custom, paramType is not empty");
            Assert.notBlack(selectorConditionDTO.getParamName(), "if type is custom, paramName is not empty");
            Assert.notBlack(selectorConditionDTO.getParamValue(), "if type is custom, paramValue is not empty");
        });
    }
    //判断是否存在这个Selector,存在则进行更新,不存在则创建
    return StringUtils.isEmpty(selectorDTO.getId()) ? create(selectorDTO) : update(selectorDTO);
}

下面看看对应的create()update()方法。

@Override
public int create(final SelectorDTO selectorDTO) {
     // 构建数据 DTO --> DO
    SelectorDO selectorDO = SelectorDO.buildSelectorDO(selectorDTO);
    // 插入选择器数据
    final int selectorCount = selectorMapper.insertSelective(selectorDO);
     // 插入选择器中的条件数据
    createCondition(selectorDO.getId(), selectorDTO.getSelectorConditions());
     // 发布事件
    publishEvent(selectorDO, selectorDTO.getSelectorConditions(), Collections.emptyList());
    // 更新upstream
    if (selectorCount > 0) {
        selectorEventPublisher.onCreated(selectorDO);
    }
    return selectorCount;
​
}
​
private void createCondition(final String selectorId, final List<SelectorConditionDTO> selectorConditions) {
    for (SelectorConditionDTO condition : selectorConditions) {
        condition.setSelectorId(selectorId);
        selectorConditionMapper.insertSelective(SelectorConditionDO.buildSelectorConditionDO(condition));
    }
}
​
@Override
public void onCreated(final SelectorDO selector) {
    publish(new SelectorCreatedEvent(selector, SessionUtil.visitorName()));
}
​
​
public int update(final SelectorDTO selectorDTO) {
    //构建更新数据
    final SelectorDO before = selectorMapper.selectById(selectorDTO.getId());
    SelectorDO selectorDO = SelectorDO.buildSelectorDO(selectorDTO);
    final int selectorCount = selectorMapper.updateSelective(selectorDO);
​
    // need old data for cleaning
    List<SelectorConditionDO> beforeSelectorConditionList = selectorConditionMapper.selectByQuery(new SelectorConditionQuery(selectorDO.getId()));
    List<RuleConditionDTO> beforeCondition = beforeSelectorConditionList.stream().map(selectorConditionDO ->
            SelectorConditionDTO.builder()
                    .selectorId(selectorConditionDO.getSelectorId())
                    .operator(selectorConditionDO.getOperator())
                    .paramName(selectorConditionDO.getParamName())
                    .paramType(selectorConditionDO.getParamType())
                    .paramValue(selectorConditionDO.getParamValue())
                    .build()).collect(Collectors.toList());
    List<RuleConditionDTO> currentCondition = selectorDTO.getSelectorConditions().stream().map(selectorConditionDTO ->
            SelectorConditionDTO.builder()
                    .selectorId(selectorConditionDTO.getSelectorId())
                    .operator(selectorConditionDTO.getOperator())
                    .paramName(selectorConditionDTO.getParamName())
                    .paramType(selectorConditionDTO.getParamType())
                    .paramValue(selectorConditionDTO.getParamValue())
                    .build()).collect(Collectors.toList());
    if (CollectionUtils.isEqualCollection(beforeCondition, currentCondition)) {
        beforeSelectorConditionList = Collections.emptyList();
    }
​
    //delete rule condition then add
    // 更新数据,先删除再新增
    selectorConditionMapper.deleteByQuery(new SelectorConditionQuery(selectorDO.getId()));
    createCondition(selectorDO.getId(), selectorDTO.getSelectorConditions());
    // 发布事件
    publishEvent(selectorDO, selectorDTO.getSelectorConditions(), beforeSelectorConditionList);
    if (selectorCount > 0) {
        selectorEventPublisher.onUpdated(selectorDO, before);
    }
    return selectorCount;
}
​
@Override
public void onUpdated(final SelectorDO selector, final SelectorDO before) {
    publish(new SelectorUpdatedEvent(selector, before, SessionUtil.visitorName()));
}

publishEvent()方法的逻辑是:找到选择器对应的插件,构建条件数据,发布变更数据。

 private void publishEvent(final SelectorDO selectorDO, final List<SelectorConditionDTO> selectorConditions, final List<SelectorConditionDO> beforeSelectorCondition) {
    // 找到选择器对应的插件
    PluginDO pluginDO = pluginMapper.selectById(selectorDO.getPluginId());
    // 构建条件数据
    List<ConditionData> conditionDataList = ListUtil.map(selectorConditions, ConditionTransfer.INSTANCE::mapToSelectorDTO);
    List<ConditionData> beforeConditionDataList = ListUtil.map(beforeSelectorCondition, ConditionTransfer.INSTANCE::mapToSelectorDO);
    // build selector data.
    SelectorData selectorData = SelectorDO.transFrom(selectorDO, pluginDO.getName(), conditionDataList, beforeConditionDataList);
    // publish change event.
    // 发布变更数据
    eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, DataEventTypeEnum.UPDATE,
            Collections.singletonList(selectorData)));
}

发布变更数据通过eventPublisher.publishEvent()完成,这个eventPublisher对象是一个ApplicationEventPublisher类,这个类的全限定名是org.springframework.context.ApplicationEventPublisher。看到这儿,我们知道了发布数据是通过Spring相关的功能来完成的。

关于ApplicationEventPublisher

当有状态发生变化时,发布者调用 ApplicationEventPublisherpublishEvent 方法发布一个事件,Spring容器广播事件给所有观察者,调用观察者的 onApplicationEvent 方法把事件对象传递给观察者。调用 publishEvent方法有两种途径,一种是实现接口由容器注入 ApplicationEventPublisher 对象然后调用其方法,另一种是直接调用容器的方法,两种方法发布事件没有太大区别。

  • ApplicationEventPublisher:发布事件;
  • ApplicationEventSpring 事件,记录事件源、时间和数据;
  • ApplicationListener:事件监听者,观察者。

Spring的事件发布机制中,有三个对象,

一个是发布事件的ApplicationEventPublisher,在ShenYu中通过构造器注入了一个eventPublisher

另一个对象是ApplicationEvent,在ShenYu中通过DataChangedEvent继承了它,表示事件对象。

public class DataChangedEvent extends ApplicationEvent {
    //......
}

最后一个是 ApplicationListener,在ShenYu中通过DataChangedEventDispatcher类实现了该接口,作为事件的监听者,负责处理事件对象。

@Component
public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {
    //......   
}

分发数据

DataChangedEventDispatcher.onApplicationEvent()

当事件发布完成后,会自动进入到DataChangedEventDispatcher类中的onApplicationEvent()方法,进行事件处理。

@Component
public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {
​
  /**
     * 有数据变更时,调用此方法
     * @param event
     */
    @Override
    @SuppressWarnings("unchecked")
    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());
            }
        }
    }
    
}

当有数据变更时,调用onApplicationEvent方法,然后遍历所有数据变更监听器,判断是哪种数据类型,交给相应的数据监听器进行处理。

ShenYu将所有数据进行了分组,一共是五种:认证信息、插件信息、规则信息、选择器信息和元数据。

这里的数据变更监听器(DataChangedListener),就是数据同步策略的抽象,它的具体实现有:

img

这几个实现类就是当前ShenYu支持的同步策略:

  • WebsocketDataChangedListener:基于websocket的数据同步;
  • ZookeeperDataChangedListener:基于zookeeper的数据同步;
  • ConsulDataChangedListener:基于consul的数据同步;
  • EtcdDataDataChangedListener:基于etcd的数据同步;
  • HttpLongPollingDataChangedListener:基于http长轮询的数据同步;
  • NacosDataChangedListener:基于nacos的数据同步;

既然有这么多种实现策略,那么如何确定使用哪一种呢?

因为本文是基于websocket的数据同步源码分析,所以这里以WebsocketDataChangedListener为例,分析它是如何被加载并实现的。

通过在源码工程中进行全局搜索,可以看到,它的实现是在DataSyncConfiguration类完成的。

今天先到这 明天继续

这个系列确实值得好好学习一下 很多细节值得推敲。

总结

本文主要分析了Apache Shenyu的服务调用过程。

本系列是自己对shenyu进行学习的系列,参考了许多博客以及官网上面的内容,完全是班门弄斧,放在自己的博客上面,如果存在错误或者侵权,请在下面评论。

参考