[享学Netflix] 四十二、Ribbon的LoadBalancer五大组件之:IPing心跳检测

生命太短暂,不要去做一些根本没有人想要的东西。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

大家熟知Ribbon是因为Spring Cloud,并且它的刻板印象就是一个客户端负载均衡器。前几篇文章对ribbon-core进行了源码解析,你会发现并没有任何指明让Ribbon和负载均衡挂上钩。

Ribbon它的实际定位是更为抽象的:不限定协议的请求转发。比如它可以集成ribbon-httpclient/transport等模块来实现请求的控制、转发。但是,但是,但是Ribbon之所以出名是因为它的负载均衡做得非常的好,所以大家对它的认知大都就是Ribbon=负载均衡。存在即合理,这么理解也没什么问题。


正文

既然负载均衡是Ribbon的真正核心,那么从本文开始就学习它的最终的部分,这便就是ribbon-loadbalancer模块:

<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-loadbalancer</artifactId>
    <version>2.3.0</version>
</dependency>

包依赖如下:

在这里插入图片描述


LoadBalancer负载均衡器五大组件

围绕着LoadBalancer负载均衡器有几个核心组件,这便是大名鼎鼎的五大核心组件,如下图所示:
在这里插入图片描述

  • IPing:客户端用于快速检查服务器当时是否处于活动状态(心跳检测)
  • IRule:负载均衡策略,用于确定从服务器列表返回哪个服务器
  • ServerList:可以响应客户端的特定服务的服务器列表
  • ServerListFilter:可以动态获得的具有所需特征的候选服务器列表的过滤器
  • ServerListUpdater:用于执行动态服务器列表更新

说明:其实ServerList/ServerListFilter/ServerListUpdater它们三也都是接口,但并没有遵循I开头的命名规范,但是IPing/IRule/ILoadBalancer都遵循有此规范,因此,这种规范上面不要一位的强求吧。

下面将围绕这五大核心组件一一展开,比如本文将来到IPing组件的学习,在学习之初需要普及一些基本概念。


Server

既然要负载均衡,那必然是在多台Server之前去均衡。顾名思义,它代表一台服务器/实例,包含Host:port所以可以定位到目标服务器,并且还有一些状态标志属性。

public class Server {

	// 未知Zone区域,这是每台Server的默认区域
    public static final String UNKNOWN_ZONE = "UNKNOWN";

	// 如192.168.1.1 / www.baidu.com
    private String host;
    private int port = 80;
	// 有可能是http/https  也有可能是tcp、udp等
    private String scheme;

	// id表示唯一。host + ":" + port -> localhost:8080  
	// 注意没有http://前缀    只有host和端口
	// getInstanceId实例id使用的就是它。因为ip+端口可以唯一确定一个实例
    private volatile String id;
    // Server所属的zone区域
    private String zone = UNKNOWN_ZONE;
    
   	// 标记是否这台机器是否是活着的
   	// =========请注意:它的默认值是false=========
    private volatile boolean isAliveFlag; 
    // 标记这台机器是否可以准好可以提供服务了(活着并不代表可以提供服务了)
    private volatile boolean readyToServe = true;

	// 构造器
    public Server(String host, int port) {
        this(null, host, port);
    }
    public Server(String scheme, String host, int port) {
        this.scheme = scheme;
        this.host = host;
        this.port = port;
        this.id = host + ":" + port;
        isAliveFlag = false;
    }
    // 因为一个id就可确定一台Server,所以这么构造是ok的
    public Server(String id) {
        setId(id);
        isAliveFlag = false;
    }
}

以上标记了一台Server的必要属性,其中需要注意的是isAliveFlag属性,它默认是false,若想这台Server能备用是需要设置为true的:

Server:

	// 此方法并非是synchronization同步的,所以其实存在线程不安全的情况
	// (volatile解决不了线程同步问题)
	// 官方解释是:遵照last win的原则也是合理的
    public void setAlive(boolean isAliveFlag) {
        this.isAliveFlag = isAliveFlag;
    }
    public boolean isAlive() {
        return isAliveFlag;
    }

Server的每个属性设置都没有synchronization同步控制,是因为它统一依照last win的原则来处理接口,否则效率太低了。

该类里面最主要是对URL的处理,包括host和ip:

Server:
	
	// 从字符串里解析传ip和端口号
	// http://www.baidu.com -> www.baidu.com + 80
	// https://www.baidu.com/api/v1/node -> www.baidu.com + 443
	// localhost:8080 -> localhost + 8080
	static Pair<String, Integer> getHostPort(String id) {
		...
	}
	// 规范化id,依赖于上面的getHostPort()方法
	// 任何uri(id)最终都会被规范为 ip + ":" + port的方式
	static public String normalizeId(String id) { ... }
	// 不解释,也是依赖于getHostPort(id)喽
	public void setId(String id) { ... }

其它get/set方法就不用介绍了,下面用一个例子简单说明一下即可。另外Serverribbon-eureka工程下是有实现类的:DiscoveryEnabledServer,本处不做讨论。


特别注意

Server:
	
	@Override
    public String toString() {
        return this.getId();
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof Server))
            return false;
        Server svc = (Server) obj;
        return svc.getId().equals(this.getId());

    }
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + (null == this.getId() ? 0 : this.getId().hashCode());
        return hash;
    }

特别注意以上三个方法,他们的值有且仅和id相关,so只要id一样会被认为是“同一个”对象(当然实际地址值是不同的),因此在装入Set的时候需要特别注意哦(list没事,list不去重嘛)。


代码示例

@Test
public void fun1() {
    Server server = new Server("www.yourbatman.com", 886);

    System.out.println(server.getId()); // www.yourbatman.com:886
    System.out.println(server.getHost()); // www.yourbatman.com
    System.out.println(server.getPort()); // 886
    System.out.println(server.getHostPort()); // www.yourbatman.com:886
    System.out.println(server.getScheme()); // null

    server.setId("localhost:8080");
    System.out.println(server.getId()); // localhost:8080
    System.out.println(server.getHost()); // localhost
    System.out.println(server.getPort()); // 8080
    System.out.println(server.getHostPort()); // localhost:8080
    System.out.println(server.getScheme()); // null

    server.setId("https://www.baidu.com");
    System.out.println(server.getId()); // www.baidu.com:443
    System.out.println(server.getHost()); // www.baidu.com
    System.out.println(server.getPort()); // 443
    System.out.println(server.getHostPort()); // www.baidu.com:443
    System.out.println(server.getScheme()); // https
}

因为Server它并不规定具体协议,比如可以是http、https、tcp、udp等,所以scheme有可能是任何值(甚至为null都可),有ip和端口号就够了。


IPing

定义如何“ping”服务器以检查其是否活动的接口,类似于心跳检测。

public interface IPing {
	// 检查给定的Server是否为“活动的”,这为在负载平衡时选出一个可用的候选Server
	public boolean isAlive(Server server);
}

ribbon-loadbalancer内的继承图谱如下(Spring Cloud换下一样):

在这里插入图片描述
在这里插入图片描述


PingConstant

永远返回一个bool常量:true or false。

public class PingConstant implements IPing {
	boolean constant = true;
	... // 给constant赋值
	@Override
	public boolean isAlive(Server server) {
		return constant;
	}
}

基本可忽略它,并无实际应用场景。


NoOpPing

它比PingConstant更狠,永远返回true。

public class NoOpPing implements IPing {
    @Override
    public boolean isAlive(Server server) {
        return true;
    }
}

它和下面的DummyPing效果上是一样的。


AbstractLoadBalancerPing

顾名思义,和LoadBalancer有关的一种实现,用于探测服务器节点的适用性。

public abstract class AbstractLoadBalancerPing implements IPing, IClientConfigAware {

	AbstractLoadBalancer lb;
    public void setLoadBalancer(AbstractLoadBalancer lb){
        this.lb = lb;
    }
    public AbstractLoadBalancer getLoadBalancer(){
        return lb;
    }
	
    @Override
    public boolean isAlive(Server server) {
        return true;
    }
}

它是使用较多的ping策略的父类,很明显,请子类复写isAlive()方法。它要求必须要关联上一个负载均衡器AbstractLoadBalancer。若你要实现自己的Ping规则,进行心跳检测,建议通过继承该类来实现。


DummyPing

Dummy:仿制品,假的,仿真的。它是AbstractLoadBalancerPing的一个空实现~

public class DummyPing extends AbstractLoadBalancerPing {
	@Override
    public boolean isAlive(Server server) {
        return true;
    }
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}

它是默认的ping实现,Spring Cloud默认也是使用的它作为默认实现,也就是说根本就木有心跳的效果喽。

说明:在ribbon-eureka模块下有NIWSDiscoveryPing这个实现,它基于服务注册中心来判断服务的健康状态


PingUrl

它位于ribbon-httpclient这个包里面。它使用发送真实的Http请求的方式来做健康检查,若返回的状态码是200就证明能够ping通,返回true。

public class PingUrl implements IPing {

	String pingAppendString = "";
	// 是否使用https
	boolean isSecure = false;
	// 期待的返回值。若为null,那只要是200就行,否则要进行比较
	String expectedContent = null;
	
	// 发送http请求
	@Override
	public boolean isAlive(Server server) {
		String urlStr   = "";
		if (isSecure){
			urlStr = "https://";
		}else{
			urlStr = "http://";
		}
		urlStr += server.getId();
		urlStr += getPingAppendString();
		
		... // 使用Apache HC发送http请求。若状态码返回200就表示成功了
	}
}

因为ribbon-httpclient包并不推荐在生产上使用了,所以此实现仅做了解即可,实际并不会使用到(毕竟ribbon-httpclient包已经不推荐使用了)。


IPing#isAlive()方法何时调用?有何用?

我们已经知道了IPing的目的是用来做健康检查,因此它到底是什么时候被调用,以及有什么用呢?
在这里插入图片描述
如截图所示:BaseLoadBalancer里是对此方法的唯一调用处。不妨把这块“伪代码”拿出来看看:

BaseLoadBalancer:

	private static class SerialPingStrategy implements IPingStrategy {
        @Override
        public boolean[] pingServers(IPing ping, Server[] servers) {
        	...
        	for (int i = 0; i < numCandidates; i++) {
        		...
        		results[i] = ping.isAlive(servers[i]);
        		...
        	}
        	return results;
        }
	}

|
IPingStrategy#pingServers()方法唯一调用处:依旧在BaseLoadBalancer.Pinger这个内部类里,
|

BaseLoadBalancer.Pinger:

	class Pinger {
		...
		public void runPinger() throws Exception {
			boolean[] results = null;
			...
			results = pingerStrategy.pingServers(ping, allServers);
			...
				// 这里就是核心:只有ping后是活着的,就会把这个机器添加到up列表里
				// 换句话说若是false,
				boolean isAlive = results[i];
                if (isAlive) {
                     newUpList.add(svr);
                 }
			...
		}
		...
	}

这就是isAlive()方法的作用:true -> 表示该机器是up的,从而得到新的up列表就是最新的可用的机器列表了

定位到了它有何用,那么它的执行入口在哪儿呢?如何执行的呢?可以确定的是:它必然是任务调度,定时执行的。接上面BaseLoadBalancer.Pinger#runPinger()的调用处是:

BaseLoadBalancer:

	// 任务Task
	class PingTask extends TimerTask {
		@Override
		public void run() {
			new Pinger(pingStrategy).runPinger();
		}
	}
	
	// 这里是它的PingTask的唯一调用处
	void setupPingTask() {
		...
		lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
		...
	}

一切浮出水面了:IPing#isAlive()方法是由Timer定时调用的,pingIntervalSeconds默认值是30s,也就说30s会去心跳一次Server,看它活着与否。当然你可以通过key:NFLoadBalancerPingInterval自己配置(单位是秒)。


IPingStrategy

定义用于ping所有服务器的策略,毕竟一般来说单单ping某一台机器的意义并不大。

public interface IPingStrategy {
    boolean[] pingServers(IPing ping, Server[] servers);
}

使用IPing对传入的servers分别进行ping,返回结果。所以可以理解它就是一个批量操作而已,它的唯一被使用的地方是在BaseLoadBalancer里用于“挑选出”所有的up服务器。

需要说明的是,若你的机器实例非常多,用并行去ping是一个比较好的优化方案,那么你就需要自定义实现IPingStrategy此接口,然后把你定义的策略和BaseLoadBalancer绑定起来替换掉默认的实现即可(默认为串行)。


总结

Ribbon的LoadBalancer五大组件之:IPing心跳检测就先介绍到这。IPing是最简单、最容易理解的一个组件,它用于解决探活、心跳检测问题,这是微服务体系中的必备元素。当然,默认使用的DummyPing并没有现实意义,因此若你是架构师,你可以写一个标准实现,使得你们的微服务更加灵敏、更加的健康
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览