负载均衡

Ribbon

Spring Cloud Ribbon是基于Netflix Ribbon3实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列
完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮
助你基于某种规则(如简单轮询,随机连接等)去连接这些机器
。我们很容易使用Ribbo实现自定义的负载均衡算法。

当前Ribbon已经进入维护阶段

LB负载均衡(Load Balance)是什么?
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
常见的负载均衡有软件Nginx,LVS,硬件F5等。

Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到)VM本地,从而在本地实现RPC远
程服务调用技术。
换种说法就是,Nginx属于集中式的负载均衡,Ribbon属于进程式负载均衡

spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter–ribbon用

RestTemplate+负载均衡

restTemplate的getForObject和getForEntity的区别:
但是主要使用的任然是getForObject

IRule

根据特定算法从服务列表中抽取服务
他的源码如下:

1
2
3
4
5
6
7
public interface IRule {
Server choose(Object var1);

void setLoadBalancer(ILoadBalancer var1);

ILoadBalancer getLoadBalancer();
}

这是一些实现方法

替换负载均衡的规则:

官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,
否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
也就是所有的springboot中的项目都有@ComponentScan。因此你需要放在一个新的包下

  1. 添加包,并生成新类

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    open class MySelfRule {

    @Bean
    open fun myRule(): IRule {
    return RandomRule() // 随机
    }
    }
  2. 为主启动类添加注解

    1
    2
    @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = [MySelfRule::class])
    open class OrderMain80

    然后即可

负载均衡

  1. 轮询算法原理

  2. 源码解析

以下是轮询算法的源码:已添加注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class RoundRobinRule extends AbstractLoadBalancerRule {

private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;

private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);

public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}

public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}

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;
}

/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}

@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}

轮询算法的关键是:

1
2
3
4
5
6
7
8
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}

解释:

  1. 原子类型
    AtomicInteger类是系统底层保护的int类型,通过提供执行方法的控制进行值的原子操作。AtomicInteger它不能当作Integer来使用

  2. 自旋锁
    自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。

如果这个原子int类型的数据跟新为了next。那么就让他返回true。然后结束循环。他就可以获得正确的序号。系统会通过server = allServers.get(nextServerIndex);实现返回对应的服务器给调用对象

OpenFeign

Feign,是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡

Feign能干什么?
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplatel时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可,即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon。利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign.只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用

使用步骤

  1. 接口+注解
    微服务调用接口+@FeignClient

  2. 新建cloud-consumer-feign-order80

  3. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
    <!-- 引入自己的api通用包-->
    <groupId>top.zfxt.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect -->
    <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    <version>1.9.0</version>
    <scope>runtime</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 这样排除依赖会导致下图所示的问题 -->
    <exclusions>
    <exclusion>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    </exclusion>
    </exclusions>

    </dependency>
    <!-- Gson-->
    <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    </dependency>


    <!-- eureka客户端-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>



    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  4. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 80
    spring:
    application:
    name: cloud-consumer-server

    eureka:
    client:
    service-url:
    defaultZero: http://localhost:7001/eureka,http://localhost:7002/eureka
  5. 主启动

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication(exclude = [JacksonAutoConfiguration::class])
    @EnableFeignClients
    open class OrderFeignMain80

    fun main(args: Array<String>) {
    runApplication<OrderFeignMain80>(*args)
    }
  6. 业务类
    直接定义PaymentFeignService接口。然后写方法体即可。

    1
    2
    3
    4
    5
    6
    @Component
    @FeignClient("CLOUD-PAYMENT-SERVICE")
    interface PaymentFeignService {
    @GetMapping("/payment/get/{id}")
    fun getPaymentById(@PathVariable("id") id: Long): CommonResult<*>
    }

    Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    class OrderFeignController {

    @Resource
    private lateinit var paymentFeignService: PaymentFeignService

    @GetMapping("/get/{id}")
    fun getPaymentById(@PathVariable("id") id:Long): CommonResult<Payment> {
    return paymentFeignService.getPaymentById(id)
    }
    }

超时控制

OpenFeign默认等待一秒钟,超过一秒钟读取的都算是超时的。超过既报错
而控制他的超时时间是由Ribbon来决定的。看到application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 80

spring:
application:
name: cloud-consumer-server

eureka:
client:
serviceUrl:
# defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版

feign:
client:
config:
default:
# 连接超时时间,默认2s,设置单位为毫秒
connectTimeout: 8000
# 请求处理超时时间,默认5s,设置单位为毫秒。
readTimeout: 10000

日志打印功能

日志级别

NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package top.zfxt.springcloud.config

import feign.Logger
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
* @author:zfxt
* @version:1.0
*/
@Configuration
open class FeignConfig {

@Bean
open fun feighLoggerLevel():Logger.Level{
return Logger.Level.FULL
}
}

添加到启动的配置项中

1
2
3
4
logging:
level:
# feigh日志以什么级别监控哪个接口
top.zfxt.springcloud.service.PaymentFeighService: debug