专栏首页BAT的乌托邦[享学Netflix] 五十五、Ribbon负载均衡器执行上下文:LoadBalancerContext

[享学Netflix] 五十五、Ribbon负载均衡器执行上下文:LoadBalancerContext

代码下载地址:https://github.com/f641385712/netflix-learning

前言

又是一个上下文概念。通过这么多篇的源码研究,发现Context上下文是常常遇到的一种“设计模式”,比如我们最为熟悉的ApplicationContext就是典型的Spring上下文。

百度百科解释上下文含义:即语境、语意,是语言学科(语言学、社会语言学、篇章分析、语用学、符号学等)的概念。 程序中的上下文含义:通俗一点,叫它环境更好。每一段代码的执行都设计到很多“局部变量”,而这些值的集合就叫上下文。

正文

IClient在执行的时候可能过程冗长,会伴随着很多的环境因素(如各种组件、变量等),特备是当具有负载均衡功能的客户端执行时,这将变得更为复杂,因此Ribbon使用了Context上下文的概念来保持每次执行的状态,这边是LoadBalancerContext


LoadBalancerContext

负载均衡上下文。它的作用是提供了IClient在执行时一系列的方法,从而获取到执行过程中的一些列信息~

说明:每次的执行均会有一个所属的上下文实例~


成员属性

public class LoadBalancerContext implements IClientConfigAware {

	protected String clientName = "default";
	protected String vipAddresses;
	protected int maxAutoRetriesNextServer = DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER;
	protected int maxAutoRetries = DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES;
	protected RetryHandler defaultRetryHandler = new DefaultLoadBalancerRetryHandler();
	protected boolean okToRetryOnAllOperations = DefaultClientConfigImpl.DEFAULT_OK_TO_RETRY_ON_ALL_OPERATIONS.booleanValue();

	private ILoadBalancer lb;
	private volatile Timer tracer;
}
  • clientName:取值为clientConfig.getClientName(),若你木有指定就是default
  • vipAddresses:此值通过配置,然后经过VipAddressResolver#resolveDeploymentContextbasedVipAddresses解析而来
    • 值使用的key是:DeploymentContextBasedVipAddresses,如<ClientName>.ribbon.DeploymentContextBasedVipAddresses={aaa}movieservice 无默认值,它一般用于在集合eureka使用时会配置上
      • 该值可以配置多个,使用,分隔
    • 解析器的key使用的是:VipAddressResolverClassName 默认值是com.netflix.client.SimpleVipAddressResolver
  • maxAutoRetriesNextServer/maxAutoRetries:这两个参数解释过多次,此处不再重复解释
  • defaultRetryHandler:重试处理器,默认使用的DefaultLoadBalancerRetryHandler,你可通过set方法指定
  • okToRetryOnAllOperations:是有允许所有操作都执行重试,默认是false
    • 通过key:OkToRetryOnAllOperations配置
  • lb:负载均衡器,通过构造器传入

以上属性在构造阶段/initWithNiwsConfig阶段完成初始化赋值。


成员方法

LoadBalancerContext的成员方法中断,普通的get/set方法不用叙述,有必要介绍些执行过程中的必要方法:

LoadBalancerContext:

	// 记录一个状态,需要传入rt,所以肯定代表请求已经结束了喽。让是个私有方法,旨在本类被调用
	// 1、activeRequestsCount -1
	// 2、totalRequests +1(注意:总请求数是在完成时候+1的,而非请求的时候哦)
	// 3、记录rt(包括时间窗口收集dataDist以及历史统计的responseTimeDist)
    private void recordStats(ServerStats stats, long responseTime) {
    	if (stats == null) {
    		return;
    	}
        stats.decrementActiveRequestsCount();
        stats.incrementNumRequests();
        stats.noteResponseTime(responseTime);
    }

	// ========重要========此方法表示请求完成后调用
	// 请求完成:在接收到响应或从客户端抛出异常(出错)
	// response:返回值
	public void noteRequestCompletion(ServerStats stats, Object response, Throwable e, long responseTime, RetryHandler errorHandler) {
    	if (stats == null) {
    		return;
    	}
		recordStats(stats, responseTime);
		// 很明显:callErrorHandler永远不可能为null(排除故意把重拾起set为null的情况)
		RetryHandler callErrorHandler = errorHandler == null ? getRetryHandler() : errorHandler;
		
		// 判断看看是否出错了,是否需要重试
		// 有response那就是正常返回:该请求正常,那就把重复连续失败的count清零
		if (callErrorHandler != null && response != null) {
			stats.clearSuccessiveConnectionFailureCount();
		} else if (callErrorHandler != null && e != null) {
			// 如果是熔断类型的异常,那就连续次数 + 1
			// 失败总数也+1(窗口统计)
			if (callErrorHandler.isCircuitTrippingException(e)) {
				stats.incrementSuccessiveConnectionFailureCount(); 
				stats.addToFailureCount();
			
			// 表示虽然失败了,但是是其它异常,比如你的业务异常,比如NPE
			// 那就清零。再次证明:ribbon的熔断只管它自己是被的异常(链接异常)
			// 并不管业务异常,业务异常交给hystrix才是合理的
			} else {
				stats.clearSuccessiveConnectionFailureCount();
			}
		}
	}

	// 客户端执行请求之前调用。增加一个活跃连接数
	public void noteOpenConnection(ServerStats serverStats) {
		serverStats.incrementActiveRequestsCount();
	}

这几个方法提供的是Client在执行过程中,对指标的收集。小Tips:noteError/noteResponse分别代表异常完成/正常完成,但其实都没有被调用过,而是统一调用更高级的noteRequestCompletion()方法。

LoadBalancerContext:

	// 仅仅根据URI就拿到端口
	// http 80  /  https 443
	protected Pair<String, Integer> deriveSchemeAndPortFromPartialUri(URI uri) { ... }

	// 从虚拟地址中尝试获取到主机的host和port
	// 如果虚拟地址确实包含实际的主机,那就直接拿。如虚拟地址就是:localhost:8080
	protected  Pair<String, Integer> deriveHostAndPortFromVipAddress(String vipAddress) throws URISyntaxException, ClientException {
		...
        if (host == null) {
            throw new ClientException("Unable to derive host/port from vip address " + vipAddress);
        }
        int port = uri.getPort();
        if (port < 0) {
            port = getDefaultPortFromScheme(scheme);
        }
        if (port < 0) {
            throw new ClientException("Unable to derive host/port from vip address " + vipAddress);
        }
        hostAndPort.setFirst(host);
        hostAndPort.setSecond(port);
        return hostAndPort;
	}

	// 检测你配置的vipAddress是否是被认证过的
	//若配置了多个,只要有一个符合要求,就返回true
	private boolean isVipRecognized(String vipEmbeddedInUri) {
		// vipAddresses是上下文中指定的,也就是你配置的,可以使用逗号分隔配置多个
        if (vipAddresses == null) {
            return false;
        }
        
		// 配置了多个的话,就一个一个的检查
		// 只要有一个地址值是被认证过的,那就返回true
		String[] addresses = vipAddresses.split(",");
        for (String address: addresses) {
            if (vipEmbeddedInUri.equalsIgnoreCase(address.trim())) {
                return true;
            }
        }
        return false;
	}

该方法只是尝试去从虚拟地址里拿host和port,可见抛出异常的概率是很大的,因为虚拟地址我们一般写服务名(不过这里似乎告诉我们:虚拟地址写地址值也是欧克的),如果没有可用的负载均衡器,并且请求URI不完整。子类可以覆盖此方法(而实际情况是哪怕Spring Cloud下都没有复写过此方法)


URL和URI(小科普)

开源框架经常看到会用到URL或者URI之类的,其实你使用字符串也可以,但很明显那不够逼格。 URI:统一资源标志符(Uniform Resource Identifier) URL:统一资源定位符(uniform resource location) URI与URL都是定位资源位置的,就是表示这个资源的位置信息。URI是一种宽泛的含义更广的定义,而URL则是URI的一个子集。另外URL可直接操作文件(如openConnection()方法可获取文件流,但URI不行)

URL提供了一种访问定位因特网上任意资源的手段,但是这些资源可以通过不同的方法(例如HTTP、FTP、SMTP)来访问,他都基本上由9个部分构成:

<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<fragment>

这里只介绍你可能觉得陌生的部分:

  • user:password:用户名与密码,一般访问ftp时会用到。但是这个可以不写,不写的话访问时可能会让你输入用户名密码
  • params:这个很少见,它使用;来连接。向服务器提供额外的参数,略
  • fragment:我们常说的锚点

每个部分对应的有对应的方法,同样的,此处仅介绍几个偏陌生的方法:

  • getAuthority():简单粗暴的理解为它是getHost() + ":" + getPort()
  • getRawUserInfo/getUserInfo:用户名+密码(前者原样,后者decode了一下)

另请注意如下输出:

public static void main(String[] args) {
    URI original = URI.create("www.baidu.com:8080");
    System.out.println(original.getScheme()); // www.baidu.com(诧异不?)
    System.out.println(original.getHost()); // null
    System.out.println(original.getPort()); // -1
    System.out.println(original.getAuthority()); // null

    original = URI.create("tcp://www.baidu.com:8080");
    System.out.println(original.getScheme()); // tcp
    System.out.println(original.getHost()); // www.baidu.com
    System.out.println(original.getPort()); // 8080
    System.out.println(original.getAuthority()); // www.baidu.com:8080
}

构造URI时,请显示提供合法的scheme~


重要方法

下面介绍几个非常重要的方法,它们在Client客户端执行过程中占有重要地位。


getServerFromLoadBalancer()

根据负载均衡策略最终最终最终使用决定用哪个Server。该方法是最为重要的一个方法了,没有之一。它会被LoadBalancerCommand#selectServer调用,用于选择一台合适的Server服务器。

LoadBalancerContext:

	//original这个URI可能是无host无ip的,如/api/v1/ping。所以此处需要处理
	// 注意:调用此方法两个入参均可能为null哦
	public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
		
		if (host == null) {
			if (lb != null) {
				Server svc = lb.chooseServer(loadBalancerKey);
			} else {
				Pair<String,Integer> hostAndPort = deriveHostAndPortFromVipAddress(vipAddresses);
				host = hostAndPort.first();
				port = hostAndPort.second();
			}
		} else {
			shouldInterpretAsVip = isVipRecognized(original.getAuthority());
			if (shouldInterpretAsVip) {
				Server svc = lb.chooseServer(loadBalancerKey);
			} else {
				logger.debug("Using full URL passed in by caller (not using load balancer): {}", original);
			}
		}

        if (host == null){
            throw new ClientException(ClientException.ErrorType.GENERAL,"Request contains no HOST to talk to");
        }
        return new Server(host, port);
	}

以上是简版源码,主要看如下的文字版步骤总结(非常重要)。

该方法从URI original中,最终目标是得到一个Server(比如包含ip地址、port),它会分为如下情况:

  1. host不存在:
    1. lb存在:使用lb.chooseServer(loadBalancerKey)选出一台Server
    2. lb不存在:
      1. 配置了虚拟地址vipAddresses
        1. 有且仅配置了一个值:使用上面介绍的deriveHostAndPortFromVipAddress(vipAddresses)把host和port解析出来
        2. ,分隔配置了多个值: throw new ClientException()
      2. 没配置虚拟地址vipAddressesthrow new ClientException()
  2. host存在:
    1. original.getAuthority()部分就是你配置的虚拟地址值vipAddresses(这就是虚拟地址值的意义)
      1. 有lb:使用lb.chooseServer(loadBalancerKey)选出一台Server
      2. 无lb: throw new ClientException()
    2. host也不是original.getAuthority()部分,那就当ip地址用。自己new一个Server返回

代码示例

基准代码:

@Test
public void fun1() throws ClientException {
    URI original = URI.create("");

    List<Server> serverList = new ArrayList<>();
    serverList.add(createServer("华南", 1));
    serverList.add(createServer("华东", 1));
    serverList.add(createServer("华东", 2));

    serverList.add(createServer("华北", 1));
    serverList.add(createServer("华北", 2));
    serverList.add(createServer("华北", 3));
    serverList.add(createServer("华北", 4));

    BaseLoadBalancer lb = new BaseLoadBalancer();
    lb.addServers(serverList);
    IClientConfig config = DefaultClientConfigImpl.getClientConfigWithDefaultValues("YourBatman");
    LoadBalancerContext loadBalancerContext = new LoadBalancerContext(null, config);

    for (int i = 0; i < 5; i++) {
        System.out.println(loadBalancerContext.getServerFromLoadBalancer(original, null));
    }
}

private Server createServer(String zone, int index) {
    Server server = new Server("www.baidu" + zone + ".com", index);
    server.setZone(zone);
    return server;
}

host不存在:这种情况下url随意,没有任何要求,空都行…

  • case1:正如基准代码,因为host为null,所有使用lb负载均衡算法(轮询)选出Server
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3
  • case2:不指定lb new LoadBalancerContext(null, config);,并且提供配置ribbon.DeploymentContextBasedVipAddresses=http://www.baiud.com:9999,再次运行程序
    • 注意:配置里必须以协议打头,如http://绝对不能省略,否则不能被是被为正常的URI。结果抛出异常:com.netflix.client.ClientException: Unable to derive host/port from vip address www.baiud.com:9999
    • 若你此处配置,分隔配了多个,那也抛出异常
www.baiud.com:9999
www.baiud.com:9999
www.baiud.com:9999
www.baiud.com:9999
www.baiud.com:9999

host存在:在基准代码上改为URI original = URI.create("http://account:3333");,这样host就是account

  • case1:基于标准代码(有host,有lb,无vipAddress配置)
    • 解释:因为虽有lb,但是getAuthority部分并不在vipAddresses里面(因为没配嘛),所以解析为直接的屋里地址来使用了(host+port)
account:3333
account:3333
account:3333
account:3333
account:3333
  • case2:加上配置ribbon.DeploymentContextBasedVipAddresses=account:3333
    • 请注意:此处是拿getAuthority来做equals比较,所以千万别家http://前缀,否则不相等就匹配不上,最终还是向上面一样原样输出的
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3

reconstructURIWithServer()

根据一个给定的Server重构URI,让其变得完整。

LoadBalancerContext:

	public URI reconstructURIWithServer(Server server, URI original) {
		// 若original里已经有了host、port、scheme等,那还解析个啥 你已经是完整的了
        String host = server.getHost();
        int port = server.getPort();
        String scheme = server.getScheme();        
        if (host.equals(original.getHost()) 
                && port == original.getPort()
                && scheme == original.getScheme()) {
            return original;
        }
        ... //  使用server里的host、port等完成拼接,形成一个完整的URI返回
	}

该方法相对简单,主要注意优先级关系就好:

  • original有的就使用自己的
  • original没有的才用Server的(scheme、host、port)

使用方式

LoadBalancerContext作为负载均衡器的执行上下文,那必然在执行过程中使用喽。所以它的唯一使用处是在LoadBalancerCommand里,而它就代表着一个负载均衡执行命令。

另外,还有个有意思的地方是,AbstractLoadBalancerAwareClient继承自LoadBalancerContext,也就是说每个Client它自己就是个上下文,可以访问到执行时候的任何环境值。

说明:此处说每个Client是建立在我们认为所有的Client均是AbstractLoadBalancerAwareClient的子类的基础上的


总结

关于Ribbon负载均衡器执行上下文:LoadBalancerContext就先介绍到这,此文内容很重要,很重要,重要。了解了上下文,就为继续讲解ILoadBalancer以及整个执行流程奠定了扎实基础。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [享学Feign] 十一、Feign通过feign-slf4j模块整合logback记录日志

    代码下载地址:https://github.com/f641385712/feign-learning

    YourBatman
  • [享学Netflix] 三十二、Hystrix抛出HystrixBadRequestException异常为何不熔断?

    通过前面文章我们知道了,Hystrix是个强大的熔断降级框架:收集目标方法的成功、失败等指标信息,触发熔断器。其中失败信息通过异常来表示,交给Hystrix进行...

    YourBatman
  • 【小家Spring】一文读懂Spring中的BeanFactory和FactoryBean(以及它和ObjectFactory的区别)的区别

    开始重视这个问题,源自一次阿里巴巴的二面面试题:说说你对Spring中BeanFactory的理解,它和FactoryBean有什么区别呢?

    YourBatman
  • SCCM2012之NAP网络保护

    一、微软的网络访问保护(NAP)是随着Windows Server 2008面世的限制网络访问保护服务。采用NAP的强制系统符合健康要求,可以使得不符合健康要求...

    李珣
  • 抖音推荐算法原理

    你是否好奇随着你的观看数量,抖音会不断将你喜欢的视频源源不断地推送给你?抖音是怎么知道哪些视频是你喜欢的?下面我来逐一介绍抖音是如何实现的,我只讲有原理,和基本...

    netkiller old
  • 程序员外包到底怎么了?

    最近爆出多个大公司外包被内部员工秀优越感的事情,到底是什么事呢,就是貌似一个外包吃了公司的下午茶,就被HR当场怒斥,所以不得不让人思考,甚至对所有外包和内部员工...

    纯洁的微笑
  • 齿轮易创浅谈:让中国ITO重回冷静的这一年

    底层到什么程度?大多主营技术支持与信息化搭建的企业,都羞于用软件外包一词形容自己的业务。然而这种羞于形容,源头并不出在企业本身要架“高”业务,而是早年中国“外包...

    齿轮易创说互联网
  • 国内最受欢迎的开源项目集锦

    摘要:第八届“开源中国开源世界”高峰论坛将于6月28-29日在北京航空航天大学隆重召开,本次大会特别邀请国内外知名开源项目发起人、活跃的开源布道师、有影响力的开...

    阳光岛主
  • 外包公司有啥不好?

    毕竟长年累月,成千上万的学员,如果每个人都编写不同的工作经验,这个也是不小的难度。

    web前端教室
  • 【跳槽必备】全球最知名的十五大高频交易公司大揭秘!

    全球最大最知名的高频交易公司有哪些呢?开始涉猎算法交易的个人交易者可能对这个问题最感兴趣。你知道吗?虽然我们很难获得具体数字,但从行业报告显示数据来看,高频交易...

    量化投资与机器学习微信公众号

扫码关注云+社区

领取腾讯云代金券