[享學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方法就不用介紹了,下面用一個例子簡單說明一下即可。


代碼示例

@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默認也是使用的它作爲默認實現,也就是說根本就木有心跳的效果嘍。


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高工、架構師 系列羣大家庭學習和交流。
往期精選

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章