目录
官网文档 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 端口探活的机制研究明白了。