首頁 > 軟體

SpringCloud負載均衡實現定向路由詳情

2022-08-19 14:00:32

背景

隨著微服務專案的迭代,可能一個服務會有多個範例,這時候就會涉及到負載均衡。然而我們在開發的時候,肯定希望只啟動一個專案。然後偵錯的時候希望負載均衡把請求分配到我們正在開發測試的服務範例上面。

如圖所示,我們希望可以指定呼叫路徑也就是定向路由。

實現方式

基於ip

這個很好理解,就是開發者本地正在執行的服務在nacos上面肯定顯示你本機的ip;那麼只要我得到開發者的ip就能夠根據這個ip來過濾nacos上面的服務,達到定向路由的效果。

基於nacos的後設資料

spring:
  cloud:
    nacos:
      discovery:
        metadata: 
          version: "mfine"

在yaml中設定nacos後設資料的version屬性。前端在請求header中新增與其對應version屬性,就可以實現服務過濾也就是定向路由。

實現原理

Gateway服務

因為gateway底層的不同,所以其負載均衡也與普通服務的不同,因此要特殊處理。先看gateway中load balancer元件呼叫流程。

首先在gateway中load balancer本身也是一個過濾器,所以流程如下。

  • ReactiveLoadBalancerClientFilter裡面有個LoadBalancerClientFactory屬性,通過這個工廠獲取具體的負載均衡器
  • LoadBalancerClientFactory會載入LoadBalancerClientConfiguration設定
  • LoadBalancerClientConfiguration會初始化我們需要的RoundRobinLoadBalancer,並且會通過建構函式傳入LoadBalancerClientFactory物件。

那我們要做什麼呢?其實就是截胡。

  • 實現自己的LoadBalancerClientFactory,傳入自己LoadBalancerClientConfiguration
  • 在自己的LoadBalancerClientConfiguration初始化自己的RoundRobinLoadBalancer
  • 最後在自己的ReactiveLoadBalancerClientFilter裡面傳入自己的LoadBalancerClientFactory,獲得自己的負載均衡器。

具體原始碼(只放核心)

MyRoundRobinLoadBalancer

private Response<ServiceInstance> getInstanceResponse(
    List<ServiceInstance> instances, ServerWebExchange exchange) {
    if (instances.isEmpty()) {
        log.warn("No servers available for service: " + this.serviceId);
        return new EmptyResponse();
    }
    try {
        //可重入鎖
        if (this.lock.tryLock(10, TimeUnit.SECONDS))
            instances = this.filterServiceInstance(exchange, instances);
        // TODO: enforce order?
        int pos = Math.abs(this.position.incrementAndGet());
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    } catch (InterruptedException e) {
        throw new RuntimeException("自定義負載均衡器,超時等待異常");
    } finally {
        lock.unlock();
    }
}
// 根據附加資訊過濾服務
private List<ServiceInstance> filterServiceInstance(ServerWebExchange exchange, List<ServiceInstance> serviceInstances) {

    List<ServiceInstance> filteredServices = new ArrayList<>();
    // 自動模式
    if (DevConfigEnum.AUTO.getCode().equals(this.properties.getModel())) {
        filteredServices = autoModel(exchange, serviceInstances);
    }
    // ip 模式
    if (this.properties.getModel().equals(DevConfigEnum.IP.getCode())) {
        filteredServices = ipModel(exchange, serviceInstances);
    }
    // metadata 模式
    if (this.properties.getModel().equals(DevConfigEnum.METADATA.getCode())) {
        filteredServices = metadataModel(exchange, serviceInstances);
    }
    if (filteredServices.isEmpty()) {
        log.info("未發現符合ip或metadata.version服務,將採用原始服務集合");
        return serviceInstances;
    }
    return filteredServices;
}
// 自動模式
private List<ServiceInstance> autoModel(ServerWebExchange exchange, List<ServiceInstance> serviceInstances) {
    List<ServiceInstance> filteredServices;

    filteredServices = ipModel(exchange, serviceInstances);
    if (filteredServices.isEmpty()) {
        filteredServices = metadataModel(exchange, serviceInstances);
    }
    return filteredServices;
}
//後設資料模式
private List<ServiceInstance> metadataModel(ServerWebExchange exchange, List<ServiceInstance> serviceInstances) {
    String version = exchange.getRequest().getHeaders().getFirst("version");
    List<ServiceInstance> filteredServices = new ArrayList<>();
    if (version != null) {
        log.info("version模式:獲取metadata.version成功");
        filteredServices = serviceInstances.stream().filter(instance -> {
            String metaVersion = instance.getMetadata().get("version");
            if (metaVersion == null) {
                return false;
            }
            return metaVersion.equals(version);
        }).collect(Collectors.toList());
    }
    return filteredServices;
}
// ip模式
private List<ServiceInstance> ipModel(ServerWebExchange exchange, List<ServiceInstance> serviceInstances) {
    List<ServiceInstance> filteredServices = new ArrayList<>();
    try {
        String ipAddress = exchange.getRequest().getHeaders().getFirst("ip");
        if (ipAddress == null) {
            ipAddress = IPUtils.getIpAddress(exchange.getRequest());
        }
        log.warn("ip模式:獲取ip成功");
        String finalIpAddress = ipAddress;
        filteredServices = serviceInstances.stream().filter(item -> item.getHost().equals(finalIpAddress))
            .collect(Collectors.toList());
    } catch (UnknownHostException e) {
        log.warn("ip模式:獲取ip失敗,無法進行定向路由");
    }
    return filteredServices;
}

MyLoadBalancerClientFactory

public class MyLoadBalancerClientFactory extends NamedContextFactory<LoadBalancerClientSpecification>
      implements ReactiveLoadBalancer.Factory<ServiceInstance>{

    ................

    public MyLoadBalancerClientFactory() {
        // 傳入自己的自動設定
        super(MyLoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
    }

    ...........
}

MyLoadBalancerClientConfiguration

@Configuration
public class MyLoadBalancerClientConfiguration {
    @Autowired
    private MicroServiceDevConfigProperties microServiceDevConfigProperties;
    @Bean
    public ReactorServiceInstanceLoadBalancer reactiveLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        // 初始化自己的負載均衡器
        return new MyRoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, 1000,microServiceDevConfigProperties);
    }
}
@Configuration
public class MyReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {
    private static final Log log = LogFactory
            .getLog(ReactiveLoadBalancerClientFilter.class);
    private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
    private final MyLoadBalancerClientFactory clientFactory;
    private LoadBalancerProperties properties;
    // 注入自己的LoadBalancerClientFactory
    public MyReactiveLoadBalancerClientFilter(MyLoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
        super(null, null);
        this.clientFactory = clientFactory;
        this.properties = properties;
    }
    ........
    private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
        URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        //獲得自己load balancer
        MyRoundRobinLoadBalancer loadBalancer = this.clientFactory
                .getInstance(uri.getHost(), MyRoundRobinLoadBalancer.class);
        if (loadBalancer == null) {
            throw new NotFoundException("No loadbalancer available for " + uri.getHost());
        }
        // 在自己的load balancer裡面擴充套件choose方法,使其接受ServerWebExchange引數
        // 傳入ServerWebExchange我們就可以,獲取請求資訊,方便我們過濾
        return loadBalancer.choose(exchange);
    }
}

我的這種實現較為繁瑣,可能大家有更好方式,大家要是有更好更簡單的方法,也可以直接替換掉。

普通服務

普通服務實現自定義負載均衡器就很簡單了,實現自定義RoundRobinRule就可以了

@Configuration
public class MyRoundRobinLoadBalancer extends RoundRobinRule {
    //不同的使用注入的方式獲取請求資訊
    @Autowired
    private HttpServletRequest request;
    .....
        private List<Server> filterServers(List<Server> reachableServers) {
        List<Server> servers = new ArrayList<>();
        if (this.properties.getModel().equals(DevConfigEnum.AUTO.getCode())) {
            servers = ipModel(reachableServers);
            if (servers.isEmpty()) {
                servers = metadataModel(reachableServers);
            }
        }
        if (this.properties.getModel().equals(DevConfigEnum.IP.getCode())) {
            servers = ipModel(reachableServers);
        }
        if (this.properties.getModel().equals(DevConfigEnum.METADATA.getCode())) {
            servers = metadataModel(reachableServers);
        }
        if (servers.isEmpty()) {
            return reachableServers;
        }
        return servers;
    }

    private List<Server> metadataModel(List<Server> reachableServers) {
        String version = request.getHeader("version");
        List<Server> servers = new ArrayList<>();
        if (version != null) {
            log.info("metadata模式: 獲取version成功");
            servers = reachableServers.stream().filter(item -> {
                NacosServer nacosServer = (NacosServer) item;
                String metaVersion = nacosServer.getMetadata().get("version");
                if (metaVersion == null) {
                    return false;
                }
                return metaVersion.equals(version);
            }).collect(Collectors.toList());
        } else {
            log.warn("metadata模式: header中無version欄位且未獲取到請求者ip");
        }
        return servers;
    }

    private List<Server> ipModel(List<Server> reachableServers) {
        List<Server> servers = new ArrayList<>();
        try {
            String ip = this.request.getHeader("ip");
            if (ip == null) {
                ip = IPUtils.getIpAddress(request);
            }
            String finalIp = ip;
            servers = reachableServers.stream().filter(item -> item.getHost().equals(finalIp)).collect(Collectors.toList());
            log.info("ip模式: 獲取請求者ip成功");
        } catch (UnknownHostException e) {
            log.warn("ip模式: 獲取ip失敗");
        }
        return servers;
    }
    ........
}

深入思考一下,通過注入的方式獲取request資訊是否存在多執行緒安全問題呢?

使用方法

metadata模式

設定yaml:

spring:
  application:
    name: cloud-order
  cloud:
    nacos:
      discovery:
        metadata:
        // 重點
          version: mfine
celi-dev:
  config:
    model: "metadata"

nacos中服務後設資料

然後請求頭中附帶version資訊

自定義負載均衡器會通過請求頭中的version去nacos中註冊服務的後設資料裡面去比對version資訊。

ip模式

設定yaml

celi-dev:
  config:
    model: "ip"
  • 在header中指定IP
  • 依靠請求資訊獲取ip

設定yaml就好

此不指定ip的時候,後臺獲取的ip可能不對。取決你本地是否存在多張網路卡(虛擬網路卡也算),有時候nacos中ip顯示也會是虛擬網路卡的ip。

使用前請確認你的服務在nacos中的ip是多少,然後在header中指定ip,這樣最省事也最穩妥。

一般是先從header中獲取ip資訊,獲取不到再從request物件中分析。

auto模式

設定yaml,其實可以不設定。

celi-dev:
  config:
    model: "auto"

自動模式預設先使用ip模式獲取不到ip會自動切換metadata模式。

什麼都不設定,預設auto模式

總結

三種模式裡面meta模式最繁瑣,心智負擔最重,但是也是最簡單的。ip模式難度在於獲取ip的準確性因此加入指定ip的方式。自動模式則二者結合。

到此這篇關於SpringCloud負載均衡實現定向路由詳情的文章就介紹到這了,更多相關SpringCloud負載均衡內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com