又是一个上下文概念。通过这么多篇的源码研究,发现Context上下文是常常遇到的一种“设计模式”,比如我们最为熟悉的ApplicationContext
就是典型的Spring上下文。
百度百科解释上下文含义:即语境、语意,是语言学科(语言学、社会语言学、篇章分析、语用学、符号学等)的概念。 程序中的上下文含义:通俗一点,叫它环境更好。每一段代码的执行都设计到很多“局部变量”,而这些值的集合就叫上下文。
IClient在执行的时候可能过程冗长,会伴随着很多的环境因素(如各种组件、变量等),特备是当具有负载均衡功能的客户端执行时,这将变得更为复杂,因此Ribbon使用了Context上下文的概念来保持每次执行的状态,这边是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()
,若你木有指定就是defaultvipAddresses
:此值通过配置,然后经过VipAddressResolver#resolveDeploymentContextbasedVipAddresses
解析而来 DeploymentContextBasedVipAddresses
,如<ClientName>.ribbon.DeploymentContextBasedVipAddresses={aaa}movieservice
无默认值,它一般用于在集合eureka使用时会配置上 VipAddressResolverClassName
默认值是com.netflix.client.SimpleVipAddressResolver
maxAutoRetriesNextServer/maxAutoRetries
:这两个参数解释过多次,此处不再重复解释defaultRetryHandler
:重试处理器,默认使用的DefaultLoadBalancerRetryHandler
,你可通过set方法指定okToRetryOnAllOperations
:是有允许所有操作都执行重试,默认是false 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之类的,其实你使用字符串也可以,但很明显那不够逼格。 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客户端执行过程中占有重要地位。
根据负载均衡策略最终最终最终使用决定用哪个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),它会分为如下情况:
lb.chooseServer(loadBalancerKey)
选出一台ServervipAddresses
: deriveHostAndPortFromVipAddress(vipAddresses)
把host和port解析出来,分隔
配置了多个值: throw new ClientException()
vipAddresses
:throw new ClientException()
original.getAuthority()
部分就是你配置的虚拟地址值vipAddresses
(这就是虚拟地址值的意义) lb.chooseServer(loadBalancerKey)
选出一台Serverthrow new ClientException()
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随意,没有任何要求,空都行…
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3
new LoadBalancerContext(null, config);
,并且提供配置ribbon.DeploymentContextBasedVipAddresses=http://www.baiud.com:9999
,再次运行程序 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
getAuthority
部分并不在vipAddresses
里面(因为没配嘛),所以解析为直接的屋里地址来使用了(host+port)account:3333
account:3333
account:3333
account:3333
account:3333
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
根据一个给定的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返回
}
该方法相对简单,主要注意优先级关系就好:
LoadBalancerContext
作为负载均衡器的执行上下文,那必然在执行过程中使用喽。所以它的唯一使用处是在LoadBalancerCommand
里,而它就代表着一个负载均衡执行命令。
另外,还有个有意思的地方是,AbstractLoadBalancerAwareClient
继承自LoadBalancerContext
,也就是说每个Client它自己就是个上下文,可以访问到执行时候的任何环境值。
说明:此处说每个Client是建立在我们认为所有的Client均是
AbstractLoadBalancerAwareClient
的子类的基础上的
关于Ribbon负载均衡器执行上下文:LoadBalancerContext就先介绍到这,此文内容很重要,很重要,重要。了解了上下文,就为继续讲解ILoadBalancer
以及整个执行流程奠定了扎实基础。