【Soul源码阅读】19.插件之 divide

目录

1.启动

2.开启 divide 插件

3.添加选择器和规则

4.验证

4.1 负载均衡

4.2 IP 端口探活

5.源码分析


 

官网文档 https://dromara.org/zh/projects/soul/divide-plugin/

divide插件是网关处理 http协议请求的核心处理插件。

1.启动

在网关的 pom.xml 文件中添加 starter 依赖,当然,divide 插件是默认支持的。(application-local.yml)

        <!--if you use http proxy start this-->
        <dependency>
            <groupId>org.dromara</groupId>
            <artifactId>soul-spring-boot-starter-plugin-divide</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--if you use http proxy end this-->

        <!-- soul httpclient plugin start-->
        <dependency>
            <groupId>org.dromara</groupId>
            <artifactId>soul-spring-boot-starter-plugin-httpclient</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- soul httpclient plugin end-->

把 soul-admin、soul-bootstrap、soul-examples-http(修改端口号启动2个,方便后面看负载均衡) 启动:

 

2.开启 divide 插件

默认是开启的,我们到页面上可以看到是开启的。

3.添加选择器和规则

在 soul-examples-http 项目中,OrderController、HttpTestController 文件使用了 @SoulSpringMvcClient 注解:

@RestController
@RequestMapping("/order")
@SoulSpringMvcClient(path = "/order")
public class OrderController {
    /**
     * Save order dto.
     *
     * @param orderDTO the order dto
     * @return the order dto
     */
    @PostMapping("/save")
    @SoulSpringMvcClient(path = "/save" , desc = "Save order")
    public OrderDTO save(@RequestBody final OrderDTO orderDTO) {
        orderDTO.setName("hello world save order");
        return orderDTO;
    }

...
}


@RestController
@RequestMapping("/test")
@SoulSpringMvcClient(path = "/test/**")
public class HttpTestController

我们之前(【Soul源码阅读】3.HTTP 用户接入 Soul 流程解析)已经分析过了,使用了 @SoulSpringMvcClient 注解,会自动把选择器 selector 和 选择器规则 rule 数据同步过来,在页面上就可以看到已经同步的数据:

其中,选择器中,可以看到2个节点(8188和8189),并且权重是50:50:

在规则设置页面,负载均衡策略可选 hash、随机或轮询(默认是随机 random):

 

4.验证

4.1 负载均衡

在 postman 中,我们多次调用 GET 请求 localhost:9195/http/order/findById?id=95,可以在 soul-bootstrap 项目的 console 控制台中看到如下日志:

2021-02-04 00:22:01.899  INFO 8552 --- [work-threads-24] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-02-04 00:22:01.899  INFO 8552 --- [work-threads-24] o.d.soul.plugin.base.AbstractSoulPlugin  : divide rule success match , rule name :/http/order/findById
2021-02-04 00:22:01.899  INFO 8552 --- [work-threads-24] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://10.0.0.2:8189/order/findById?id=95, retryTimes is 0
2021-02-04 00:22:02.466  INFO 8552 --- [work-threads-25] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-02-04 00:22:02.467  INFO 8552 --- [work-threads-25] o.d.soul.plugin.base.AbstractSoulPlugin  : divide rule success match , rule name :/http/order/findById
2021-02-04 00:22:02.467  INFO 8552 --- [work-threads-25] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://10.0.0.2:8188/order/findById?id=95, retryTimes is 0
2021-02-04 00:22:03.068  INFO 8552 --- [-work-threads-1] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-02-04 00:22:03.068  INFO 8552 --- [-work-threads-1] o.d.soul.plugin.base.AbstractSoulPlugin  : divide rule success match , rule name :/http/order/findById
2021-02-04 00:22:03.068  INFO 8552 --- [-work-threads-1] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://10.0.0.2:8189/order/findById?id=95, retryTimes is 0
2021-02-04 00:22:03.659  INFO 8552 --- [-work-threads-3] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-02-04 00:22:03.660  INFO 8552 --- [-work-threads-3] o.d.soul.plugin.base.AbstractSoulPlugin  : divide rule success match , rule name :/http/order/findById
2021-02-04 00:22:03.660  INFO 8552 --- [-work-threads-3] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://10.0.0.2:8188/order/findById?id=95, retryTimes is 0

可以看到调用真实的业务节点在 8188 和 8189 中来回切换,并且比例为 1:1,满足负载均衡为轮询,并且权重比例为 50:50=1:1。

 

4.2 IP 端口探活

我们把 8189 端口号的项目关闭,过会儿会在 soul-admin 项目的 console 控制台打印如下日志:

2021-02-04 00:23:43.667 ERROR 27276 --- [upstream-task-7] o.d.s.a.s.impl.UpstreamCheckService      : check the url=10.0.0.2:8189 is fail 

然后在 soul-admin 的 web 页面也看不到 8189 节点信息了,只剩下 8188 节点了:

 

5.源码分析

关于负载均衡,还有前面的几个插件,后面再研究一下统一写一篇文章分析,这里就先放下,看下 IP 端口探活是怎么实现的。

根据日志找到 UpstreamCheckService,使用 @Component 被 Spring 容器托管,在往下看,看到了一个被 @PostConstruct 注解的方法,由于 @PostConstruct 注解的方法将会在依赖注入完成后被自动调用,所以看下具体逻辑:

// UpstreamCheckService.java
/**
     * Setup selectors of divide plugin.
     */
    @PostConstruct
    public void setup() {
        // 从数据库中获取 divide 插件信息
        PluginDO pluginDO = pluginMapper.selectByName(PluginEnum.DIVIDE.getName());
        if (pluginDO != null) {
            // 根据 divide 插件 ID 获取 selector 列表
            List<SelectorDO> selectorDOList = selectorMapper.findByPluginId(pluginDO.getId());
            for (SelectorDO selectorDO : selectorDOList) {
                // 把 selector 的 handle 字段信息由字符串转成对应的 Java Bean
                // handle 数据:[{"upstreamHost":"localhost","protocol":"http://","upstreamUrl":"10.0.0.2:8188","weight":50,"status":true,"timestamp":0,"warmup":0}]
                List<DivideUpstream> divideUpstreams = GsonUtils.getInstance().fromList(selectorDO.getHandle(), DivideUpstream.class);
                if (CollectionUtils.isNotEmpty(divideUpstreams)) {
                    // 把真实服务器信息缓存在 UPSTREAM_MAP 中
                    // UPSTREAM_MAP 定义:
                    // private static final Map<String, List<DivideUpstream>> UPSTREAM_MAP = Maps.newConcurrentMap();
                    UPSTREAM_MAP.put(selectorDO.getName(), divideUpstreams);
                }
            }
        }
        // check 定义,默认值为 true
        // @Value("${soul.upstream.check:true}")
        // private boolean check;
        if (check) {
            new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), SoulThreadFactory.create("scheduled-upstream-task", false))
                    .scheduleWithFixedDelay(this::scheduled, 10, scheduledTime, TimeUnit.SECONDS);
        }
    }

这段代码有2个逻辑:

1.把 divide 插件对应的 selector 的 handle 字段获取,转换为真实业务节点信息,并缓存在 UPSTREAM_MAP 中。

2.启动一个定时线程池,并按照固定的延迟去执行 scheduled 方法。

固定时间默认为 10s:

    @Value("${soul.upstream.scheduledTime:10}")
    private int scheduledTime;

scheduled 方法:

    private void scheduled() {
        // 如果缓存的真实服务器节点大于0,执行 check 方法
        if (UPSTREAM_MAP.size() > 0) {
            UPSTREAM_MAP.forEach(this::check);
        }
    }

继续往下追:

// UpstreamCheckService.java
    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());

...
// UpstreamCheckUtils.java
    /**
     * Check url boolean.
     *
     * @param url the url
     * @return the boolean
     */
    // url : 10.0.0.2:8188
    public static boolean checkUrl(final String url) {
        if (StringUtils.isBlank(url)) {
            return false;
        }
        if (checkIP(url)) {
            String[] hostPort;
            if (url.startsWith(HTTP)) {
                final String[] http = StringUtils.split(url, "\\/\\/");
                hostPort = StringUtils.split(http[1], Constants.COLONS);
            } else {
                // hostPort = ["10.0.0.2", "8188"]
                hostPort = StringUtils.split(url, Constants.COLONS);
            }
            return isHostConnector(hostPort[0], Integer.parseInt(hostPort[1]));
        } else {
            return isHostReachable(url);
        }
    }

    private static boolean isHostConnector(final String host, final int port) {
        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(host, port));
        } catch (IOException e) {
            return false;
        }
        return true;
    }

使用 socket,看看能否正确连接,如果不报错,证明节点ok;如果报异常,证明节点异常。下图是节点正常的情况:

这里回到 check 方法 :

// UpstreamCheckService.java
    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) {
                // 节点正常进入这里
                if (!divideUpstream.isStatus()) {
                    // 这里使用 status 字段区分是否已经被缓存过。
                    divideUpstream.setTimestamp(System.currentTimeMillis());
                    divideUpstream.setStatus(true);
                    log.info("UpstreamCacheManager check success the url: {}, host: {} ", divideUpstream.getUpstreamUrl(), divideUpstream.getUpstreamHost());
                }
                // 把这个正常的节点放到成功列表里
                successList.add(divideUpstream);
            } else {
                // 如果没连接成功,把 status 字段设置为 false,等后面节点重新可以连接上时能够正常设置时间戳
                divideUpstream.setStatus(false);
                // 打印错误日志,上面我们打印的错误日志就是这一行代码打印的
                log.error("check the url={} is fail ", divideUpstream.getUpstreamUrl());
            }
        }
        // 数量相同,说明没有节点的增加,也没有减少
        if (successList.size() == upstreamList.size()) {
            return;
        }
        if (successList.size() > 0) {
            // 进入到这里,说明节点有增减,并且还至少有一个正常节点
            UPSTREAM_MAP.put(selectorName, successList);
            // 更新 selector 的handle 字段
            updateSelectorHandler(selectorName, successList);
        } else {
            // 进入到这里,说明所有节点都不正常了
            UPSTREAM_MAP.remove(selectorName);
            // 更新 selector 的handle 字段
            updateSelectorHandler(selectorName, null);
        }
    }
    private void updateSelectorHandler(final String selectorName, final List<DivideUpstream> upstreams) {
        SelectorDO selectorDO = selectorMapper.selectByName(selectorName);
        if (Objects.nonNull(selectorDO)) {
            List<ConditionData> conditionDataList = ConditionTransfer.INSTANCE.mapToSelectorDOS(
                    selectorConditionMapper.selectByQuery(new SelectorConditionQuery(selectorDO.getId())));
            PluginDO pluginDO = pluginMapper.selectById(selectorDO.getPluginId());
            String handler = CollectionUtils.isEmpty(upstreams) ? "" : GsonUtils.getInstance().toJson(upstreams);
            selectorDO.setHandle(handler);
            // 把修改后的 handle 字段更新到数据库
            selectorMapper.updateSelective(selectorDO);
            if (Objects.nonNull(pluginDO)) {
                SelectorData selectorData = SelectorDO.transFrom(selectorDO, pluginDO.getName(), conditionDataList);
                selectorData.setHandle(handler);
                // publish change event.
                // 发布事件,把 divide 插件下 selector 数据同步到 soul-bootstrap端
                eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, DataEventTypeEnum.UPDATE,
                                                                 Collections.singletonList(selectorData)));
            }
        }
    }

ok,到这里就把 divide 插件 IP 端口探活的机制研究明白了。

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值