目标
- 运行 examples下面的 http 服务
- 学习文档,结合 divde 插件,发起 http 请求 soul 网关,体验 http 代理
http 服务的相关依赖及配置
- 引入 http 的代理插件
在 soul-bootstrap
工程下的 pom.xml
下引入如下依赖
org.dromara
soul-spring-boot-starter-plugin-divide
${project.version}
org.dromara
soul-spring-boot-starter-plugin-httpclient
${project.version}
- 引入 soul 客户端(针对SpringBoot用户):
在 soul-examples-http
(你自己的真实服务) pom.xml
新增如下依赖:
org.dromara
soul-spring-boot-starter-client-springmvc
${last.version}
- 添加客户端接入配置:
在yml中新增如下配置 :
soul:
http:
adminUrl: http://localhost:9095
port: 8188
contextPath: /myhttp
appName: http
full: false
# adminUrl: 为你启动的 soul-admin 项目的ip + 端口,注意要加http://
# port: 你本项目的启动端口
# contextPath: 为你的这个mvc项目在soul网关的路由前缀,比如/order ,/product 等等,网关会根据你的这个前缀来进行路由
# appName:你的应用名称,不配置的话,会默认取 `spring.application.name` 的值
# full: 设置true 代表代理你的整个服务,false表示代理你其中某几个controller
Controller
中添加@SoulSpringMvcClient
注解
将注解 Controller
类上面,里面的path属性则为前缀,如果含有 /**
代表你的整个接口需要被网关代理。
@RestController
@RequestMapping("/test")
@SoulSpringMvcClient(path = "/test/**")
public class HttpTestController {
//controller中所有方法被网关代理
}
@RestController
@RequestMapping("/order")
@SoulSpringMvcClient(path = "/order")
public class OrderController {
/**
* order/save会被网关代理,而/order/findById 则不会
*/
@PostMapping("/save")
@SoulSpringMvcClient(path = "/save" , desc = "Save order")
public OrderDTO save(@RequestBody final OrderDTO orderDTO) {
orderDTO.setName("hello world save order");
return orderDTO;
}
@GetMapping("/findById")
public OrderDTO findById(@RequestParam("id") final String id) {
OrderDTO orderDTO = new OrderDTO();
orderDTO.setId(id);
orderDTO.setName("hello world findById");
return orderDTO;
}
}
devide 负载均衡权重分析
修改 diea 启动配置,勾选Allow parallel run
,允许并行启动
修改application.yml
中端口配置,将端口改为8189
server:
port: 8189 # 修改端口
address: 0.0.0.0
soul:
http:
adminUrl: http://localhost:9095
port: 8189 # 修改端口
contextPath: /http
appName: http
full: false
成功启动,soul 后台中会新注册一个 8189 服务
修改weight
权重配置,将8188权重改为100
采用假设性原则
,根据工程名及文件名称,在DividePlugin.java
的doExecute
方法中添加断点。使用 Postman 对网关发起一个请求,遇到断点,查看调用栈信息
一直往上找,发现请求会先进入SoulWebHandler
类的 handle 方法里:
@Override
public Mono handle(@NonNull final ServerWebExchange exchange) {
MetricsTrackerFacade.getInstance().counterInc(MetricsLabelEnum.REQUEST_TOTAL.getName());
Optional startTimer = MetricsTrackerFacade.getInstance().histogramStartTimer(MetricsLabelEnum.REQUEST_LATENCY.getName());
return new DefaultSoulPluginChain(plugins).execute(exchange).subscribeOn(scheduler)
.doOnSuccess(t -> startTimer.ifPresent(time -> MetricsTrackerFacade.getInstance().histogramObserveDuration(time)));
}
然后调用 DefaultSoulPluginChain
的 execute 方法,从 plugins 中获取DividePlugin
插件:
public Mono execute(final ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < plugins.size()) {
//获取插件
SoulPlugin plugin = plugins.get(this.index++);
Boolean skip = plugin.skip(exchange);
if (skip) {
//跳过插件
return this.execute(exchange);
}
//调用插件的具体执行方法
return plugin.execute(exchange, this);
}
return Mono.empty();
});
}
调用了插件的具体执行方法后,进入AbstractSoulPlugin
类的 execute 方法,这里使用了模板方法
设计模式,会匹配每一个插件,直到找到divide
插件
public Mono execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
//获取插件名称
String pluginName = named();
//从缓存中获取插件信息
final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
if (pluginData != null && pluginData.getEnabled()) {
// 从缓存中根据插件名称获取对应选择器列表
final Collection selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
if (CollectionUtils.isEmpty(selectors)) {
return handleSelectorIsNull(pluginName, exchange, chain);
}
// 匹配选择器
final SelectorData selectorData = matchSelector(exchange, selectors);
if (Objects.isNull(selectorData)) {
return handleSelectorIsNull(pluginName, exchange, chain);
}
selectorLog(selectorData, pluginName);
//从缓存中获取选择器对应的规则列表
final List rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
if (CollectionUtils.isEmpty(rules)) {
return handleRuleIsNull(pluginName, exchange, chain);
}
RuleData rule;
//不太明白
if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
//get last
rule = rules.get(rules.size() - 1);
} else {
rule = matchRule(exchange, rules);
}
if (Objects.isNull(rule)) {
return handleRuleIsNull(pluginName, exchange, chain);
}
ruleLog(rule, pluginName);
//调用具体执行方法
return doExecute(exchange, chain, selectorData, rule);
}
return chain.execute(exchange);
}
然后就回到了最初的起点,在DividePlugin.java
的doExecute
方法中打的第一个断点
protected Mono doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
//获取访问的服务地址,这是是8188,8189两条
final List upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
if (CollectionUtils.isEmpty(upstreamList)) {
log.error("divide upstream configuration error: {}", rule.toString());
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
//根据权重,获取访问的服务地址
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
if (Objects.isNull(divideUpstream)) {
log.error("divide has no upstream");
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// set the http url
String domain = buildDomain(divideUpstream);
String realURL = buildRealURL(domain, soulContext, exchange);
exchange.getAttributes().put(Constants.HTTP_URL, realURL);
// set the http timeout
exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
return chain.execute(exchange);
}
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
方法为根据权重获取访问的服务地址,一路断点,最终找到RandomLoadBalance
类
public DivideUpstream doSelect(final List upstreamList, final String ip) {
//计算总权重
int totalWeight = calculateTotalWeight(upstreamList);
//判断是否平均分配
boolean sameWeight = isAllUpStreamSameWeight(upstreamList);
if (totalWeight > 0 && !sameWeight) {
//获取具体的访问地址信息
return random(totalWeight, upstreamList);
}
// If the weights are the same or the weights are 0 then random
return random(upstreamList);
}
//根据权重获取具体的访问地址信息
private DivideUpstream random(final int totalWeight, final List upstreamList) {
// If the weights are not the same and the weights are greater than 0, then random by the total number of weights
int offset = RANDOM.nextInt(totalWeight);
// Determine which segment the random value falls on
for (DivideUpstream divideUpstream : upstreamList) {
offset -= getWeight(divideUpstream);
if (offset < 0) {
return divideUpstream;
}
}
return upstreamList.get(0);
}
至此,devide 插件负载均衡权重分析告一段落。