背景

随着微服务项目的迭代,可能一个服务会有多个实例,这时候就会涉及到负载均衡。然而我们在开发的时候,肯定希望只启动一个项目。然后调试的时候希望负载均衡把请求分配到我们正在开发测试的服务实例上面。

如图所示,我们希望可以指定调用路径也就是定向路由。

实现方式

基于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负载均衡内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

SpringCloud负载均衡实现定向路由详情的更多相关文章

  1. 关闭iOS原生MPVolumeView音频路由菜单

    我正在使用MPVolumeView允许用户在使用我的应用程序时控制他喜欢的音频路径.该代码显示了该视图:当用户点击音频路由按钮时,会出现一个带有可用选项的菜单.问题:显示音量视图的屏幕可能需要隐藏,因为我的应用程序处理各种事件,我想同时隐藏音频路由菜单我的问题:有没有人知道是否可以手动关闭MPVolumeView的音频路由选择菜单而无需用户按下取消按钮?解决方法在iOS8上,您可以使用以下使用私有API的代码

  2. iOS:使用蓝牙音频输出(kAudioSessionProperty_OverrideCategoryEnableBluetoothInput)AudioSession

    >如果有可用的A2DP设备,我的音频路由将始终自动切换到kAudioSessionOutputRoute_BluetoothA2DP路由.如何防止此路线更改?我希望你们中的一些人可以帮助我解决这些问题.这对我对CoreAudio的整体理解,特别是AudioSession框架,真的有帮助.解决方法AudioSession是一项棘手的业务.1.BluetoothHFPaudiooutputisonlypossibleincaseofAudioSessionkAudioSessionCategory_PlayA

  3. Swift3.0 Swift2.3 获取IP地址 获取网关地址

    最近需要在Swift项目中获取路由器的网关地址,在网上找了半天的代码也没发现太多有价值的东西,而且大多都是OC代码,很少有Swift的相关代码,只找到了一个通过Swift代码获取设备IP的代码,最后实在没办法只能曲线救国了。下面上代码:思路就是把获取到的设备IP地址的最后一位手动修改为”1”,前面三位不需要修改,比如我的手机ip地址是192.168.31.212,所以网关地址就是192.168.31.1。最近苹果更新了Swift3.0,这里更新一下代码。

  4. Swift3.0服务端开发(二) 静态文件添加、路由配置以及表单提交

    今天博客中就来聊一下Perfect框架的静态文件的添加与访问,路由的配置以及表单的提交。也就是webroot的文件目录变地方了。后方的尾随闭包是对响应的处理。action的地址就是我们在服务器端配置的路由地址“127.0.0.1:8181/login”,而表单提交的方式是POST。

  5. Swift Web 开发之 Vapor - 路由二

    路由参数Vapor提倡使用类型安全的路由参数来接收数据,我们可以在路由方法中使用Swift类型来指定参数类型,Vapor会在内部解析并将参数返回给闭包以供使用,非常方便。Swift中处处有协议,路由参数也是如此,我们所见例子中的Int其实就是Vapor给实现了StringInitializable协议,当然String也已经默认实现。throw另外一大特性就是可以直接在路由中抛出异常,我们可以throw任何遵从Swift.Error协议的对象,当然Vapor已经为我们封装好了几个常用的Error来方便我们

  6. swift – Singleton模式和正确使用Alamofire的URLRequestConvertible

    如果是这样,我该如何设置经理的基础?此外,如果我使用这个管理器//这可以与上面显示的路由器结构一起工作?我是Alamofire图书馆的新手,迅速。然而,当您获得超过6或7例的情况下,这很快就会变得很快。首先,您的模型对象需要符合RouterObject协议。最后一个问题是您无法直接在Routerenum中存储baseURL或OAuthToken。但是,如果您只是使用默认会话触发网络,那么sharedInstance可能就足够了。

  7. Android VPNService路由排除

    我正在使用OpenVPN和ICS附带的新VpnServiceAPI有没有办法从VPN隧道中定义IP地址的排除?

  8. android – 如何以编程方式在蓝牙和手机之间选择媒体音频

    我需要有一组按钮,其操作类似于JB拨号器中的媒体输出选择器,它将选择“耳机”“扬声器”和“电话听筒”.我可以打开和关闭扬声器,但是在标准手机和蓝牙耳机之间进行媒体输出之间的切换使我望而却步.我如何构建和选项,如控制媒体输出的拨号器?

  9. vue3获取当前路由地址

    本文详细讲解了vue3获取当前路由地址的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  10. SpringCloud超详细讲解微服务网关Zuul基础

    这篇文章主要介绍了SpringCloud Zuul微服务网关,负载均衡,熔断和限流,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部