【Soul源碼閱讀-02】devide插件負載均衡權重分析

目標

  • 運行 examples下面的 http 服務
  • 學習文檔,結合 divde 插件,發起 http 請求 soul 網關,體驗 http 代理

http 服務的相關依賴及配置

  • 引入 http 的代理插件

soul-bootstrap 工程下的 pom.xml下引入以下依賴java

<!--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>
 <dependency>
     <groupId>org.dromara</groupId>
     <artifactId>soul-spring-boot-starter-plugin-httpclient</artifactId>
     <version>${project.version}</version>
 </dependency>
  • 引入 soul 客戶端(針對SpringBoot用戶):

soul-examples-http(你本身的真實服務) pom.xml 新增以下依賴:spring

<dependency>
         <groupId>org.dromara</groupId>
         <artifactId>soul-spring-boot-starter-client-springmvc</artifactId>
         <version>${last.version}</version>
     </dependency>
  • 添加客戶端接入配置:

在yml中新增以下配置 :bootstrap

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,容許並行啓動緩存

image-20210115234600169

修改application.yml中端口配置,將端口改成8189mvc

server:
  port: 8189 # 修改端口
  address: 0.0.0.0

soul:
  http:
    adminUrl: http://localhost:9095
    port: 8189 # 修改端口
    contextPath: /http
    appName: http
    full: false

成功啓動,soul 後臺中會新註冊一個 8189 服務app

image-20210115235413

修改weight權重配置,將8188權重改成100負載均衡

採用假設性原則,根據工程名及文件名稱,在DividePlugin.javadoExecute方法中添加斷點。使用 Postman 對網關發起一個請求,遇到斷點,查看調用棧信息dom

image-20210116000151809

一直往上找,發現請求會先進入SoulWebHandler 類的 handle 方法裏:ide

@Override
    public Mono<Void> handle(@NonNull final ServerWebExchange exchange) {
        MetricsTrackerFacade.getInstance().counterInc(MetricsLabelEnum.REQUEST_TOTAL.getName());
        Optional<HistogramMetricsTrackerDelegate> 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<Void> 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<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        //獲取插件名稱
        String pluginName = named();
        //從緩存中獲取插件信息
        final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
        if (pluginData != null && pluginData.getEnabled()) {
             // 從緩存中根據插件名稱獲取對應選擇器列表
            final Collection<SelectorData> 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<RuleData> 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.javadoExecute方法中打的第一個斷點

protected Mono<Void> 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<DivideUpstream> 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<DivideUpstream> 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<DivideUpstream> 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 插件負載均衡權重分析告一段落。

相關文章
相關標籤/搜索