本篇是从基础方便讲解一些springcloud-负载均衡-ribbon中的一些理论性的内容;还会讲一些源码内容;后面的文章会将源码进行整理,并且将源码的github地址上传。
1.什么是负载均衡
核心概念:用户来访请求应该相对平均的分摊到每个节点;
不要将所有工作量都给一个节点。
LB策略的目的(负载均衡):海量的用户请求均匀的分配到集群中的每一台机器上
设计系统时需要有一定的余量(应对系统不同情况)
2.客户端负载与服务端负载均衡
客户端的负载均衡:服务机器的列表(客户端保存copy,且动态变化)
+LB策略(负载均衡策略)
服务端的负载均衡:nignx/F5等
大型应用通常是客户端+服务端负载均衡搭配使用
客户端负载均衡(红色)与服务端负载均衡的比较:
客户端,对开发团队更加友好,代码中可以决定使用那种负载均衡策略(配置文件)。
运维成本低(不需要额外的组件,所有的负载均衡策略在服务端就制定好了)
强依赖于注册中心,从注册中心拉到可用服务的列表(可动态更新)
微服务框架搭配使用,gateway,zookeeper,ribbon偏多。
服务端,开发团队很难去主动的发起修改,负载均衡都在网关层,由专业的网络人员来
操作,修改。网关层语言跟日常的java有很大的区别。
运维成本高(需要额外的组件,使用F5还要搭配硬件设备)
通常不依赖(SpringCloud架构zookeeper,gateway时存在依赖)
Tomcat,JBoss部署传统应用:nginx,F5偏多
3.Ribbon体系架构解析
Ribbon体系架构:
丰富的组件库;适配性好;(牛的Ribbon可以脱离
SpringCloud可以应用在一般项目中)
组件图:
一个HttpRequest发过来,先被转发到Eureka上。此时Eureka仍然通过服务发现获取了所有服务节点的物理地址,但问题是他不知道该调用哪一个,只好把请求转到了Ribbon手里。
IPing IPing是Ribbon的一套healthcheck机制,故名思议,就是要Ping一下目标机器看是否还在线,一般情况下IPing并不会主动向服务节点发起healthcheck请求,Ribbon后台通过静默处理返回true默认表示所有服务节点都处于存活状态(和Eureka集成的时候会检查服节点UP状态)。
IRule 这就是Ribbon的组件库了,各种负载均衡策略都继承自IRule接口。所有经过Ribbon的请求都会先请示IRule一把,找到负载均衡策略选定的目标机器,然后再把请求转发过去。
实现的demo:
在eureka-consumer中,
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaConsumerApplication {
@Bean
public RestTemplate register(){
return new RestTemplate();
}
public static void main(String[] args) {
new SpringApplicationBuilder(EurekaConsumerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
在eureka-consumer的controller中,实现的方式是
@RestController
public class Controller {
@Autowired
private RestTemplate restTemplate;
@GetMapping
public String sayHi() {
return restTemplate.getForObject(
"http://eureka-client/sayHi",
String.class);
}
}
在Ribbon-consumer的controller中如何实现(LB策略)下获取对应的节点:
这里直接将地址写上去(直接应用服务名)。返回值是String.class。
后台是如何找到对应的服务和对应的端口呢?
key就在@LoadBalanced
@Bean
@LoadBalanced
public RestTemplate template(){
return new RestTemplate();
}
4.懒加载与饥饿加载
INFO [main] com.netflix.loadbalancer.DynamicServerListLoadBalancer -
DynamicServerListLoadBalancer for client eureka-consumer initialized
这行log意思是,Ribbon客户端已经完成了LoadBalancer的初始化。初始化是没问题的,但问题是初始化发生的时间,为什么偏偏发生在首次超时之前呢?
这就要从Ribbon的懒加载说起了,原来Ribbon是在第一次方法调用的时候才去初始化LoadBalancer。这样看来,第一个方法请求不仅仅包含HTTP连接和方法的响应时间,还包括了LoadBalancer的创建耗时。假如你的方法本身就比较耗时的话,而且超时时间又设置的比较短,那么很大可能这第一次http调用就会失败。其实还有很多框架也实现了类似的懒加载功能,比如Hibernate的lazy-fetch,懒加载在大部分情况下可以节省系统资源开销,但某些情况下反而导致服务响应时间被延长。
之所以这类问题难以发现的原因,在于它无法稳定重现,比如只能通过重启来重现,对这种问题往往直接从生产环境的log入手去分析是比较有效的手段。
Ribbon–饥饿加载:
ribbon.eager-load.enabled=true
ribbon.eager-load.clients=ribbon-consumer
第一个参数开启了Ribbon的饥饿加载模式,第二个属性指定了需要应用饥饿加载的服务名称。完成上面配置并再次重启服务,就会发现LoadBalancer初始化日志在方法调用之前就打印出来了
5.负载均衡策略-七种策略
RandomRule:随性而为:随机挑选访问节点
RoundRobinRule:按部就班从一个节点一步一步向后选取节点,
不会跳过一个,不会原地踏步,每次只会向后移动一步;
假如在多线程环境下,两个请求同时访问这个Rule是否会读取到相同节点呢?不会,这靠的是RandomRobinRule底层的自旋锁+CAS的同步操作。CAS的全称是compare and swap,是一种借助操作系统函数来实现的同步操作。类似Eureka为了防止服务下线被重复调用,就使用AtomicBoolean的CAS方法做同步控制,CAS+自旋锁这套组合技是高并发下最廉价的线程安全手段,因为这套操作不需要锁定系统资源。有优点也有缺点,自旋锁如果迟迟不能释放,将会带来CPU资源的浪费,因为自旋本身并不会执行任何业务逻辑,而是单纯的使CPU“空转”。所以通常情况下会对自旋锁的旋转次数做一个限制,比如JDK中synchronize底层的锁升级策略,就对自旋次数做了动态调整。
// CAS+自旋锁获取系统资源的打开方式,真实应用中还要注意防止无休止自旋:
// 或者for (;;) 做自旋
while (true) {
// cas操作
if (cas(expected, update)) {
// 业务逻辑代码
// break或退出return
} }
Netflix是特别喜欢用自旋+CAS,作为中间件来说性能是非常重要的。
RetryRule:卷土重来
一个类似装饰器模式的Rule。装饰器相当于一层层的俄罗斯套娃,每一层都会加上一层独特的buff。
RetryRule也是同样的道理,他的BUFF就是给其他负载均衡策略加上“重试”功能。而在RetryRule里还藏着一个subRule,这才是隐藏在下面的真正被执行的负载均衡策略,RetryRule正是要为它添加重试功能(如果初始化时没指定subRule,将默认使用RoundRibinRule)。
WeightedResponseTimeRule:能者多劳
继承自RoundRibbonRule,他会根据服务节点的响应时间计算权重,
响应时间越长权重就越低,响应越快则权重越高。权重的高低决定了
机器被选中概览的高低。就是说,响应时间越小的机器,被选中的概览越大。
由于服务器刚启动的时候,对各个服务节点采样不足,因此采用轮询策略,当积累
到一定的样本时,会切换到WeightedResponseTimeRule模式。
BestAvailableRule:让最闲的人来
有点智能。过滤掉故障服务以后,它会基于30分钟的统计结果选取当前并发量最小的
服务结点,也就最”闲”的节点作为目标地址。
如果无统计结果,则采用轮询的方式选定节点。
关键字:过滤故障服务;选取并发量最小的节点。
AvailabilityFilteringRule:我是有底线的(AFR)
底层依赖RandomRobinRule来选取节点,满足它的最小要求的节点才会被选中。
如果节点满足要求,无论响应时间或当前并发量是什么。都会被选中。
每次AvailabilityFilteringRule(简称AFR)都会请求RobinRule挑选一个节点,然后对这个节点做以下两步检查:
是否处于熔断状态(熔断是Hystrix中的知识点,后面章节会讲到,这里大家可以把熔断当做服务不可用)
节点当前的active请求连接数超过阈值,超过了则表示节点目前太忙,不适合接客
如果被选中的server不幸挂掉了检查,那么AFR会自动重试(次数最多10次),让RobinRule重新选择一个服务节点。
ZoneAvoidanceRule:我的地盘我做主
ZoneFilter:在Eureka注册中一个服务结点有Zone,Region和URL三个身份信息
其中Zone可以理解为机房大区(未指定则由Eureka给定默认值),
这里会对这个Zone的监控情况过滤器下面所有服务结点
可用性过滤:这里和AvailabilityFilteringRule的验证非常像,会过滤当前并发量
较大,或者处于熔断的服务节点。
5.配置负载均衡策略
Ribbon默认的策略是:RoundRobinRule。一个个轮询
配置负载均衡策略的方式:@Bean注入的方式。也可以放在启动类中
@Configuration
public class RibbonConfiguration {
@Bean
public IRule delaultLBStrategy() {
return new RandomRule();
}
}
针对特定的服务的id的负载均衡策略:
配置在application.yml
eureka-client:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
注解的方式:
(配置在public class RibbonConfiguration {)
//@RibbonClient(name = “eureka-client”, configuration = MyRule.class)
6.部分源码品读:
概念:
**真假随机数:**真随机数是通过物理过程(如放射性衰变、大气噪声等)生成的,其每个数字都是完全随机的,无法预测。而假随机数则是通过算法生成的,其每个数字都是基于前一个数字计算得出的,存在一定的规律性,可以被预测。因此,在需要高度安全性的场景中,应该使用真随机数来保证随机性。
RandomRule核心:
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
/*
* No servers. End regardless of pass, because subsequent passes
* only get more restrictive.
*/
return null;
}
int index = chooseRandomInt(serverCount);
server = upList.get(index);
if (server == null) {
/*
* The only time this should happen is if the server list were
* somehow trimmed. This is a transient condition. Retry after
* yielding.
*/
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Shouldn't actually happen.. but must be transient or a bug.
server = null;
Thread.yield();
}
return server;
}
对于上面的分析:ILoadBalancer lb。
当server = = null. If(Thread.interrupted()),表示线程中断,则返回空。防御性编程
if (server == null) {
Thread.yield(); continue; }
Thread.yield()表示线程让渡,当前线程愿意让出CPU。
如果server.isAlive,则直接返回。
为空,则调用Thread.yield(),完成线程让步。
原因是没有其他退出线程的办法。
RoundRobinRule:在代码最后没有做线程让渡
因为有Count计数器,10次之后就自动退出了。
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
for(;????:自旋锁的实现。
自旋锁+NSCC.CAS操作是一个非常高效的线程同步方式。
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
全部代码:RoundRobinRule核心部分
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
BestAvailableRule:
分析先判断loadBalancerStats ==null为空时,
直接调用super.choose(key).
它调用的是上层(ClientConfigEnabledRoundRobinRule).
里面的代码是:
@Override
public Server choose(Object key) {
if (roundRobinRule != null) {
return roundRobinRule.choose(key);
} else {
throw new IllegalArgumentException(
"This class has not been initialized with the RoundRobinRule class");
}
}
继续分析:这里代码是直接调用上层的roundRobinRule的choose方法。具体
RoundRobinRule核心choose方法上面已经进行了分析
获取所有的getAllServers.以及当前时间
遍历serverList,首先获取serverStats.
isCircuitBreakerTripped :一个布尔值,用于表示断路器是否触发。如果断路器已触发,则该值为 true;否则为 false。
对于isCircuitBreakerTripped
这段代码是一个名为 isCircuitBreakerTripped 的 Java 方法,它这段代码是一个名为 isCircuitBreakerTripped 的 Java 方法,它接受一个长整型参数 currentTime。该方法的作用是判断断路器是否触发。
首先,通过调用 getCircuitBreakerTimeout() 方法获取断路器超时时间,并将其赋值给变量 circuitBreakerTimeout。如果 circuitBreakerTimeout 小于等于 0,则表示断路器未设置超时时间,因此返回 false。
如果 circuitBreakerTimeout 大于 0,则比较当前时间 currentTime 和断路器超时时间 circuitBreakerTimeout。如果当前时间小于等于断路器超时时间,说明断路器还未触发,返回 false;否则,说明断路器已触发,返回 true。
BestAvailableRule的核心代码:
@Override
public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key);
}
List<Server> serverList = getLoadBalancer().getAllServers();
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server: serverList) {
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}
RetryRule:给与其他Rule进行重试策略
public Server choose(ILoadBalancer lb, Object key) {
long requestTime = System.currentTimeMillis();
long deadline = requestTime + maxRetryMillis;
Server answer = null;
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
InterruptTask task = new InterruptTask(deadline
- System.currentTimeMillis());
while (!Thread.interrupted()) {
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
/* pause and retry hoping it's transient */
Thread.yield();
} else {
break;
}
}
task.cancel();
}
if ((answer == null) || (!answer.isAlive())) {
return null;
} else {
return answer;
}
}
7.负载均衡器LoadBalancer原理解析
@LoadBalanced 这个注解一头挂在RestTemplate上,另一头挂在LoadBalancerAutoConfiguration这个类上。它就像连接两个世界的传送门,将所有顶着「LoadBalanced」注解的RestTemplate类,都传入到LoadBalancerAutoConfiguration中。如果要深挖底层的作用机制,大家可以发现这个注解的定义上还有一个@Qualifier注解,@Qualifier注解搭配@Autowired注解做自动装配,可以通过name属性,将指定的Bean装载到指定位置(即使有两个同样类型的Bean,也可以通过Qualifier定义时声明的name做区分)。这里「LoadBalanced」也是借助Qualifier实现了一个给RestTemplate打标签的功能,凡是被打标的RestTemplate都会被传送到AutoConfig中做进一步改造。
LBAutoConfig 从前一步中传送过来的RestTemplate,会经过LBAutoConfig的装配,将一系列的Interceptor(拦截器)添加到RestTemplate中。拦截器是类似职责链编程模型的结构,我们常见的ServletFilter,权限控制器等,都是类似的模式。Ribbon拦截器会拦截每个网络请求做一番处理,在这个过程中拦截器会找到对应的LoadBalancer对HTTP请求进行接管,接着LoadBalancer就会找到默认或指定的负载均衡策略来对HTTP请求进行转发。
总结Ribbon的作用机制就是,由LoadBalanced在RestTemplate上打标,Ribbon将带有负载均衡能力的拦截器注入标记好的RestTemplate中,以此实现了负载均衡。
IPing机制
DummyPing,默认返回true,即认为所有节点都可用,这也是单独使用Ribbon时的默认模式
NIWSDiscoveryPing,借助Eureka服务发现机制获取节点状态,假如节点状态是UP则认为是可用状态
PingUrl,它会主动向服务节点发起一次http调用,如果对方有响应则认为节点可用
大家可以看出第三种主动出击的模式比较生猛。大家可以想象,假如我们的服务节点搭载的是随便一个微服务都有大几千台服务器,在服务本身就被超高访问量调用的情况下,那这种主动出击的IPing策略必然会大大增加服务节点的访问压力。
既然Eureka已经有了服务发现机制,可以获取节点的当前状态,拿来就用岂不更好?因此,除非特殊指定,在和Eureka搭配使用的时候,采用的是第二种,也就是过滤非UP状态的节点(其实这个功能直接放Eureka里也能做)
8.IPing机制解析
public interface IPing接口只有一个方法public boolean isAlive(Server server);
5个实现类
对于DummyPing,isAlive方法,直接返回true
NoOpPing同上。
NIWSDiscoveryPing代码:
DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;
InstanceInfo instanceInfo = dServer.getInstanceInfo();
发现Eureka的服务列表,然后通过是否是up状态来判断对应的
Server是否处于alive状态
public boolean isAlive(Server server) {
boolean isAlive = true;
if (server!=null && server instanceof DiscoveryEnabledServer){
DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;
InstanceInfo instanceInfo = dServer.getInstanceInfo();
if (instanceInfo!=null){
InstanceStatus status = instanceInfo.getStatus();
if (status!=null){
isAlive = status.equals(InstanceStatus.UP);
}
}
}
return isAlive;
}
对于PingUrl类:拼接url,向服务节点发起调用
public boolean isAlive(Server server) {
String urlStr = "";
if (this.isSecure) {
urlStr = "https://";
} else {
urlStr = "http://";
}
urlStr = urlStr + server.getId();
urlStr = urlStr + this.getPingAppendString();
boolean isAlive = false;
HttpClient httpClient = new DefaultHttpClient();
HttpUriRequest getRequest = new HttpGet(urlStr);
String content = null;
try {
HttpResponse response = httpClient.execute(getRequest);
content = EntityUtils.toString(response.getEntity());
isAlive = response.getStatusLine().getStatusCode() == 200;
if (this.getExpectedContent() != null) {
LOGGER.debug("content:" + content);
if (content == null) {
isAlive = false;
} else if (content.equals(this.getExpectedContent())) {
isAlive = true;
}