生命太短暂,不要去做一些根本没有人想要的东西。
代码下载地址: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
负载均衡器有几个核心组件,这便是大名鼎鼎的五大核心组件,如下图所示:
IPing
:客户端用于快速检查服务器当时是否处于活动状态(心跳检测)IRule
:负载均衡策略,用于确定从服务器列表返回哪个服务器ServerList
:可以响应客户端的特定服务的服务器列表ServerListFilter
:可以动态获得的具有所需特征的候选服务器列表的过滤器ServerListUpdater
:用于执行动态服务器列表更新说明:其实
ServerList/ServerListFilter/ServerListUpdater
它们三也都是接口,但并没有遵循I
开头的命名规范,但是IPing/IRule/ILoadBalancer
都遵循有此规范,因此,这种规范上面不要一位的强求吧。
下面将围绕这五大核心组件一一展开,比如本文将来到IPing
组件的学习,在学习之初需要普及一些基本概念。
既然要负载均衡,那必然是在多台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方法就不用介绍了,下面用一个例子简单说明一下即可。另外Server
在ribbon-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和端口号就够了。
定义如何“ping”服务器以检查其是否活动的接口,类似于心跳检测。
public interface IPing {
// 检查给定的Server是否为“活动的”,这为在负载平衡时选出一个可用的候选Server
public boolean isAlive(Server server);
}
在ribbon-loadbalancer
内的继承图谱如下(Spring Cloud换下一样):
永远返回一个bool常量:true or false。
public class PingConstant implements IPing {
boolean constant = true;
... // 给constant赋值
@Override
public boolean isAlive(Server server) {
return constant;
}
}
基本可忽略它,并无实际应用场景。
它比PingConstant
更狠,永远返回true。
public class NoOpPing implements IPing {
@Override
public boolean isAlive(Server server) {
return true;
}
}
它和下面的DummyPing
效果上是一样的。
顾名思义,和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规则,进行心跳检测,建议通过继承该类来实现。
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
这个实现,它基于服务注册中心来判断服务的健康状态
它位于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的目的是用来做健康检查
,因此它到底是什么时候被调用,以及有什么用呢?
如截图所示: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
自己配置(单位是秒)。
定义用于ping所有服务器的策略,毕竟一般来说单单ping某一台机器的意义并不大。
public interface IPingStrategy {
boolean[] pingServers(IPing ping, Server[] servers);
}
使用IPing
对传入的servers分别进行ping,返回结果。所以可以理解它就是一个批量操作而已,它的唯一被使用的地方是在BaseLoadBalancer
里用于“挑选出”所有的up服务器。
需要说明的是,若你的机器实例非常多,用并行去ping是一个比较好的优化方案,那么你就需要自定义实现IPingStrategy
此接口,然后把你定义的策略和BaseLoadBalancer
绑定起来替换掉默认的实现即可(默认为串行)。
Ribbon的LoadBalancer五大组件之:IPing心跳检测就先介绍到这。IPing
是最简单、最容易理解的一个组件,它用于解决探活、心跳检测问题,这是微服务体系中的必备元素。当然,默认使用的DummyPing
并没有现实意义,因此若你是架构师,你可以写一个标准实现,使得你们的微服务更加灵敏、更加的健康。