前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Netflix] 五十五、Ribbon负载均衡器执行上下文:LoadBalancerContext

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

作者头像
YourBatman
发布2020-03-24 10:07:30
1.2K0
发布2020-03-24 10:07:30
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

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

前言

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

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

正文

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


LoadBalancerContext

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

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


成员属性
代码语言:javascript
复制
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方法不用叙述,有必要介绍些执行过程中的必要方法:

代码语言:javascript
复制
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()方法。

代码语言:javascript
复制
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个部分构成:

代码语言:javascript
复制
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<fragment>

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

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

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

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

另请注意如下输出:

代码语言:javascript
复制
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服务器。

代码语言:javascript
复制
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返回

代码示例

基准代码:

代码语言:javascript
复制
@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
代码语言:javascript
复制
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
    • 若你此处配置,分隔配了多个,那也抛出异常
代码语言:javascript
复制
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)
代码语言:javascript
复制
account:3333
account:3333
account:3333
account:3333
account:3333
  • case2:加上配置ribbon.DeploymentContextBasedVipAddresses=account:3333
    • 请注意:此处是拿getAuthority来做equals比较,所以千万别家http://前缀,否则不相等就匹配不上,最终还是向上面一样原样输出的
代码语言:javascript
复制
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3

reconstructURIWithServer()

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

代码语言:javascript
复制
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以及整个执行流程奠定了扎实基础。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • LoadBalancerContext
      • 成员属性
      • 成员方法
      • 重要方法
    • 使用方式
    • 总结
    相关产品与服务
    负载均衡
    负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档