SpringCloud组件01---CAP、注册中心、Ribbon实践

CAP理论快速简介#

  • CAP理论关注的是粒度是数据,而不是整体系统设计。
  • Consistency(强一致性)
  • Availability(高可用性)
  • Partition Tolerance(分区容错性)

image-20200531234138010

  • 以上三个最多只能较好满足两个,一个系统不可能同时满足这三个需求。
    • CA:单点集群,满足一致性和高可用,通常在可拓展性上不强大。
    • CP:满足一致性,分区容忍度。通常性能不太高。
      • 当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性。(违背A高可用)
      • Consul和Zk,更关注一致性,不在了很快就给干掉,但不是立刻。
    • AP:满足可用性和分区容忍度,通常对一致性要求较低。
      • 当网络分区出现后,为了保证可用性,B系统可以返回旧的值,优先保证可用。(违背C一致性)
      • Eureka,不会立刻踢掉服务
  • 分布式架构的P永远都要保证

图例

  • AP:如微博热门微博点赞数,后续柔性理论和base数据补充来保证一致性

image-20200531235033177

  • CP:

image-20200531235338672

微服务的通用定义:#

是一种架构模式,提倡将单一应用划分成一组轻量级的微服务互相调用和配合,基于restful,并可以独立部署。

是一整套的较量,不是单个的组件。

本次测试版本:#

springboot 2.2.x+版本 spring cloud H版。如果cloud是G,boot对应2.1具体:

  • boot 2.2.2.RELEASE:

  • CLOUD Hoxton.SR1

  • Cloud Alibaba 2.1.0.RELEASE

分布式服务包括的组件#

  • 服务注册与发现:eureka,现在不维护了。zk、Consul(golang不推荐)、用alibaba的Nacos(推荐)
  • 服务调用:Ribbon(维护状态),后续LoadBalancer。Feign用OpenFeign。
  • 服务熔断:hystrix(维护状态,但是大规模,思想需要学习),resilience4j(海外,国内很少),alibaba sentienl(推荐)
  • 负载均衡:fegin
  • 服务降级:hystrix
  • 服务消息队列:
  • 配置中心管理:config,推荐携程的阿波罗,或者alibaba的Nacos(推荐)
  • 服务网关:zuul,现在用cloud gateway
  • 服务监控
  • 总线:bus—>alibaba Nacos
  • 全链路监控
  • 自动化部署
  • 服务定时操作
  • 分布式配置:cloud config

image-20200530223603142

环境搭建#

  • idea里面new-project-maven_architect_site
  • 设置项目encoding utf-8
  • 设置项目 settings-annotation processor- 勾选 enable annotation prosessing
  • 设置java-compile是1.8
  • 设置pom:maven项目的的聚合、依赖、传递依赖
    • 父工程,project根目录下的pom文件修改maven,添加packaging标签为pom。
1
<packaging>pom</packaging>

dependencyManagement用在父工程,子模块继承后,提供作用:锁定版本、子module都引用一个依赖,而不用写version。
dependencyManagement用在父工程只是声明版本依赖,并不真的引用。真正的是要子项目自己引用group和artifactid即可,不用指定版本号自动用父类的。

支付子module建立:#

  • 建module:在父工程右键新建module,新建完了之后父工程上会有<module>引入了
  • 改pom
  • 写yml
  • 主动启动类
  • 业务类

IDEA自动热部署配置(可选)#

  • 在子项目工程pom中添加devtools依赖包:

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
    </dependency>
  • 添加插件到父聚合项目的pom中

    1
    2
    3
    4
    5
    6
    7
    8
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
    <fork>true</fork>
    <addResources>true</addResources>
    </configuration>
    </plugin>
  • 开启自动编译

image-20200531140623420

  • 更新值

    ctrl+alt+shift+/-----registry,勾选允许自动编译:

    勾选下面两个选项

image-20200531141004199

  • 重启idea

关于RestTemplate#

  • getForObjec:返回Json对象,或者Json字符串可以格式化为对象。
  • getForEntity:返回ResponseEntity,除了数据,还包括网络层面的状态码、响应头等东西。

image-20200601002520722

RestTemplate已经被Springcloud深度定制,底层可以支持各种客户端负载均衡策略支持,也支持自定义负载均衡策略。

注册中心#

EUREKA服务注册#

传统一对第一调用,太多的时候就是网状的,需要每个客户端都去维护对端服务信息。无法统一管理,非常乱。

  • EUREKA作为一个注册中心的server,系统中其他services都向他链接注册并维持心跳。
  • 这样EUREKA就能知晓所有的services的信息,就像一个电话号码本。其他service想互相调用可以来这里用service别名来询问当前可用的对端地址。
  • 下面左边是SpringCloud,右边是Dubbo。

image-20200531165250671

  • EUREKA分为Server和Client两个组件:
    • Server提供一个监听,供给其他cloud所有service进来连接。
    • Client是一个Java客户端,简化与Server的交互,是一个内置、轮循的负载均衡器。默认30s向Server发送一次心跳。如果Server多伦没有收到心跳就把这个节点移除。(默认90s)
  • EUREKA已经停止更新,后续需要迁移到别的技术栈,比如zk、consl、nacos。

工作流程#

image-20200531183332365

Server配置#

pom.xml

2.2版本后,eureka分为server和client了,此处是server。

不用指定版本,因为父module中指定了版本。

1
2
3
4
5
<!--eureka server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

application.yml

1
2
3
4
5
6
7
8
9
10
11
server:
port: 7001
eureka:
instance:
hostname: localhost # erureka
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 自己仅仅作为注册中心,不去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka

Server的main类

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {

public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}

client配置#

pom.xml 同上

1
2
3
4
5
6
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--上面的eureka-client会自动引入ribbon-->

application.yml

1
2
3
4
5
6
eureka:
client:
register-with-eureka: true # 是否注册到eureka server 默认true
fetch-registry: true # 是否从eureka抓取已注册的信息,true才能配合ribbon使用负载均衡
service-url:
defaultZone: http://localhost:7001/eureka

Client的main类

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}

EUREKA高可用集群#

高可用原理:相互注册,相互守望。

比如7001和7002有两个EUREKA,会互相注册到对方那边去。互相心跳监控。

  • 两台EUREKA的yml配置
1
2
3
4
5
6
7
8
9
10
11
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com # erureka另一个节点的ip,互相注册 如192.168.1.2:7001 <---> 192.168.1.3:7002,此处就应该写192.168.1.2
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 自己仅仅作为注册中心,不去检索服务
service-url: # 下面应该注册到另外一台eureka,也就是http://192.168.1.3:7002/eureka
defaultZone: http://eureka7002.com:7002/eureka

1
2
3
4
5
6
7
8
9
10
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com # erureka另一个节点的ip,互相注册 如192.168.1.2:7001 <---> 192.168.1.3:7002,此处就应该写192.168.1.3
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 自己仅仅作为注册中心,不去检索服务
service-url: # 集群模式下,下面应该注册到另外一台eureka,也就是http://192.168.1.2:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka
  • 客户端的yml配置
1
2
3
4
5
6
7
eureka:
client:
register-with-eureka: true # 是否注册到eureka server 默认true
fetch-registry: true # 是否从eureka抓取已注册的信息,true才能配合ribbon使用负载均衡
service-url:
# defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka,eureka7002.com:7002/eureka # EUREKA是集群

服务集群#

当一个服务在多台机器上运行,注册到一个EUREKA中后,在EUREKA上可以看到服务的多个ip列表用逗号隔开的。

这时候假设客户端还是使用restTemplate请求的,不能写死对端服务的ip和端口,可以写EUREKA中的服务名。

这样消费端不再关注提供方的地址,而且有负载均衡功能

1
2
3
4
5
6
7
8
9
10
11
12
13
//    private String url = "http://localhost:8001";   // 单机
private String url = "http://CLOUD-PAYMENT-SERVICE"; // cloud服务名,会UNKWNON HOST,需要给restTemplate开启LoadBalanced

/**
* 添加用户
* post http://localhost:8080/customer/payment/add?serial=cus_lalala
* @param payment
* @return
*/
@PostMapping("/customer/payment/add")
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(url+"/payment/add", payment, CommonResult.class); //
}

但是因为服务方是多个节点,所以需要restTemplate开启负载均衡功能去调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class ApplicationContextConfig {

/**
* 类似<bean id="xxx", class="xxxx"></bean>
* @return
*/
@Bean
@LoadBalanced //这个注解可以负载均衡,也可以把host地址由一个cloud的application-name去zookeeper转换为ip和端口
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}

自我保护#

  • 一句话描述:某时刻某个微服务不能用了,Eureka不会立刻清理,依旧会对该服务的信息进行保存。
  • 为什么?防止Eureka Server网络不通,但是Eureka Client正常运行的时候,EurekaServer不会立刻把EurekaClient剔除。
  • 详细:默认30s一次,当90s没收到心跳就该干掉,但是如果短时间内大量丢失客户端时,这个节点就会进入自我保护机制。(此时可能大量客户端都是正常的,很可能是网络分区故障)

属于CAP里面的AP分支。(高可用、分区容错性)

server端关闭自我保护,修改

1
2
3
server:
enable-self-preservation: false # 关闭自我保护,客户端90s没心跳立马干掉
eviction-interval-timer-in-ms: 60000

Discovery#

主启动类,开启Discovery的能力

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient //主启动类,开启Discovery的能力
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}

controller,注入,获取服务和实例信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Resource
private DiscoveryClient discoveryClient;


@GetMapping(value = "/discovery")
@ResponseBody
public CommonResult<Object> discovery(){
System.out.println("-------------Services--------------");
discoveryClient.getServices().forEach(System.out::println);
System.out.println("-------------Instances--------------");
List<String> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE").stream().map(each->each.getInstanceId()+"/"+each.getHost()+":"+each.getPort()+"/"+each.getUri()).collect(Collectors.toList());
instances.forEach(System.out::println);
Map<String, Object> map = new HashMap<>();
map.put("services", discoveryClient.getServices());
map.put("instances", instances);
return CommonResult.success("discovery",map);
}

Zookeeper服务注册#

zookeeper使用临时节点存储服务的信息,一会儿心跳不出现就会干掉这个节点(不是立马干掉)。是CAP的CP,和EUREKA不太一样。

服务引入jar包

1
2
3
4
5
<!--zookeeper client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>

application.yml只有几行不一样,指定zookeeper的连接字符串即可:

1
2
3
4
5
6
7
8
9
server:
port: 8004

spring:
application:
name: cloud-provider-service
cloud: # 启动基于zookeeper的服务注册
zookeeper:
connect-string: 127.0.0.1:2181

主启动类,使用DescoveryClient即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentZkMain8004 {

/**
* 基于zk的服务提供方,访问/payment/zk即可看到测试效果
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(PaymentZkMain8004.class, args);
}
}

启动后:

1
2
3
4
5
6
7
8
9
10
11
12
13
[zk: localhost:2181(CONNECTED) 7] ls /
[services, zookeeper]

[zk: localhost:2181(CONNECTED) 8] ls /services
[cloud-provider-service]

[zk: localhost:2181(CONNECTED) 10] ls /services/cloud-provider-service
[ba35208f-59ea-4d70-b75f-857c5a5b0a64]

[zk: localhost:2181(CONNECTED) 11] get /services/cloud-provider-service/ba35208f-59ea-4d70-b75f-857c5a5b0a64
{"name":"cloud-provider-service","id":"ba35208f-59ea-4d70-b75f-857c5a5b0a64","address":"192.168.1.10","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-provider-service","metadata":{}},"registrationTimeUTC":1590931922948,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}
[zk: localhost:2181(CONNECTED) 12]

注入调用方#

pom和application.yml和启动类一模一样,暂时用restTemplate调用,需要config一下

1
2
3
4
5
6
7
8
9
10
@Configuration
public class ApplicationContextConfig {

@Bean
@LoadBalanced //这个注解可以负载均衡,也可以把host地址由一个cloud的application-name去zookeeper转换为ip和端口
public RestTemplate getRestTemplate(){
RestTemplate template = new RestTemplate();
return template;
}
}

customer的controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class ZkCustomerController {

@Resource
private DiscoveryClient discoveryClient;

@Resource
private RestTemplate restTemplate;

public static final String URL = "http://cloud-provider-service";


@GetMapping("/customer/payment/zk")
public CommonResult<Payment> testZkCloud(){
return restTemplate.getForObject(URL+"/payment/zk", CommonResult.class);
}

}

启动后可以在zk看到双方:

1
2
[zk: localhost:2181(CONNECTED) 26] ls /services
[cloud-customer-order, cloud-provider-service]

zk客户端和server jar包冲突#

有可能client和server的zk jar包不一致会报错,比如客户端太新,服务端太老。

image-20200531213833705

需要在cloud的zk的starter里面exclude掉zk的包,然后重新引入一个和zkServer版本一致的包即可。

Consul服务注册#

简介#

  • 分布式的服务注册配置管理中心(KV存储),同时提供控制总线由golang开发。

  • 基于Raft协议,比较简洁。支持健康检查、Http和DNS协议,支持跨数据中心的WAN集群,支持图形界面、跨平台。

  • 功能:

    • 服务发现:Http和DNS两种方式
    • 健康检查:多种方式,Http、TCP、Docker、Shell定制
    • KV存储:K-V存储,可以做配置管理
    • 可视化界面

    官网下载解压,只有一个consul文件

    https://www.consul.io/

    启动参考video:

https://learn.hashicorp.com/consul/getting-started/agent

比如consul agent -dev启动

访问http://localhost:8500查看信息,或者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 curl localhost:8500/v1/catalog/nodes # 获取消息
[
{
"ID": "890e9cd0-322b-fafc-fe58-33728d41f305",
"Node": "treeMate",
"Address": "127.0.0.1",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "127.0.0.1",
"lan_ipv4": "127.0.0.1",
"wan": "127.0.0.1",
"wan_ipv4": "127.0.0.1"
},
"Meta": {
"consul-network-segment": ""
},
"CreateIndex": 10,
"ModifyIndex": 11
}
]

服务端注册#

pom.xml

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
<dependencies>
<!--Consul client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--MVC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!--业务common类,引用当前项目的版本号-->
<dependency>
<groupId>com.sam.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>

<!--DEV-TOOLS-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>

主启动类

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentConsulMain8004 {

public static void main(String[] args) {
SpringApplication.run(PaymentConsulMain8004.class, args);
}
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8004

spring:
application:
name: cloud-provider-service
cloud: # 启动基于consul的服务注册
consul:
discovery:
service-name: ${spring.application.name}
hostname: localhost
host: localhost

controller只是一个简单的数据模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class PaymentConsulController {
@Value("${server.port}")
private String port;


/**
* 返回一个随机payment即可
* @return
*/
@GetMapping("/payment/consul")
public CommonResult<Payment> testConsulPayment(){
Payment payment = new Payment();
payment.setId(RandomUtils.nextLong());
payment.setSerial("这是一个模拟的随机payment,"+ RandomStringUtils.randomAlphabetic(16));
return CommonResult.success("I'm Consul Client on:"+port,payment);
}
}

启动后就可以在上面的ui中看到。

客户端注册#

pom application 都一毛一样,config、主启动类、controller和上面zk的一毛一样(因为暂时没用openFeign和ribbon),没啥可写的。

Eureka、Zookeeper、Consul的对比#

组件 CAP 对外接口
Eureka AP Http
Consul CP Http/DNS
Zookeeper CP 客户端

C主要是数据一致,Eureka主要保证高可用。

Ribbon#

Ribbon简介#

是一套客户端的负载均衡工具,如链接超时、重试等,配置文件只用列出所有的节点,Ribbon自动基于规则(轮询、随机、响应时间加权等)去链接,也很容易自定义实现负载均衡。

官网在github,目前也是维护模式了。未来的趋势是Spring的LoadBalancer,但是还很不成熟。

  • Ribbon:
    • 本地负载均衡,进程内。调用前从注册中心获取服务信息,缓存到JVM,本地负载均衡。
    • 负载均衡+RestTemplate进行RPC,可以和多种客户端结合。Eureka只是其中之一。
    • 工作时分两步:
      • 先选择注册中心,比如先从注册中心选择一个负担小的Eureka
      • 根据用户指定的策略,从注册地址取到一个进行。
  • Nginx:是服务端的LB。

image-20200601000553582

新版2.2.x的springcloud的eureka会自动引入ribbon:

1
2
3
4
5
6
7
8
9
10
        <!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--上面的eureka-client会自动引入ribbon-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>-->
<!-- </dependency>

Ribbon的负载均衡策略#

都是IRule的实现,策略模式。

image-20200601010519762

image-20200601010535970

  1. RoundRobinRule: 默认轮询的方式
  2. RandomRule: 随机方式
  3. WeightedResponseTimeRule: 根据响应时间来分配权重的方式,响应的越快,分配的值越大。
  4. BestAvailableRule: 选择并发量最小的方式
  5. RetryRule: 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server。
  6. ZoneAvoidanceRule: 根据性能和可用性来选择。
  7. AvailabilityFilteringRule: 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值)

其他负载均衡算法:#

http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html

  • LeastActiveLoadBalance:最小活跃数负载均衡算法

    • 活跃调用越少,说明server性能越高。优先给他。具体实现:每个服务者对应一个活跃数,init的时候大家都为0,收到一个请求+1,处理完毕-1。一段时间后性能最好的机器下降速度最快,优先给他新的请求。
  • ConsistentHashLoadBalance:一致性hash算法

    • 如nginx的IP hash,把client的ip或者url等进行hash,对同一个client,相同的请求永远在一台机器。

自定义规则#

注意事项:#

自定义ribbon规则类,不能放在@ComponentsScan所能扫描的包和子包内(主启动类和以下所有包)。否则这个配置类会被所有的Ribbon客户端共享,达不到特殊定制化目的。

注意@ComponentScan 和@SpringBootApplication注解都不要扫描到。

image-20200601111454364

原始的轮询规则#

  • 默认的那个轮询规则:
    • discoveryClient拿到所有的Server实例,然后搞一个int计数,每次取模决定返回哪个Server。
    • 里面有自旋锁、AQS,避免重量级锁。
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
private AtomicInteger nextServerCyclicCounter;      // 注意这里是一个原子型的Integer

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(); // 获取可用的server
List<Server> allServers = lb.getAllServers(); //所有的server
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;
}
}

定义手动实现一个负载均衡#

步骤:

  • ApplicationContextConfig对象上面去掉@LoadBalanced注解(restTemplate上面),否则就会使用ribbon自带的策略
  • 写一个LoadBalanced接口
  • 实现接口来一个choose方法,使用discoverClient去根据策略选择一个instance
  • 在controller的请求时候注入这个负载均衡,choose一个insrance,拿到uri,拼装请求地址。
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
@Component
public class MyLoadBalancer implements ICustomerLoadBalancer {

// 计数器
private AtomicInteger integer = new AtomicInteger(0);

@Override
public ServiceInstance chooseInstance(List<ServiceInstance> instances) {
if(instances == null || instances.isEmpty()){
System.out.println("没有可用的服务!!!!!");
return null;
}
// 计数器线程安全得+1,对server数量取模
int i = incrementAndGet() % instances.size();
return instances.get(i);
}


/**
* 自选增加,获取下一个值
* @return
*/
private final int incrementAndGet(){
int curr,next;
do{
curr = integer.get();
next = curr > Integer.MAX_VALUE ? 0 : curr+1; // int不能超限
// 只要没获取到真正的curr,就一直自旋
}while (!integer.compareAndSet(curr, next)); // true之后就中断
System.out.println("------------------next:"+next);
return next;
}
}

controller,使用上面的loadbalancer手动获取instance地址url地址

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 测试使用自定义手动负载均衡的方式获取
* @param id
* @return
*/
@GetMapping("/customer/payment/lb/get/{id}")
public CommonResult<Payment> getByIdByHanLoadBalancer(@PathVariable("id") long id){
List<ServiceInstance>instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
// 使用我们的自定义轮循负载均衡器
ServiceInstance instance = myLoadBalancer.chooseInstance(instances);
// getForObject 返回Json数据,可以转化为对象
return restTemplate.getForObject(instance.getUri()+"/payment/get/"+id, CommonResult.class);
}

动手实现一个自定义ribbon策略#

  • 自定义策略,实现IRule接口,或者继承自AbstractLoadBalancerRule
  • @SpringBootApplication扫描位置外定义一个Config,里面配置我们的自定义策略
  • 在主启动类加上我们的自定义策略配置
  • RestTemplate类加上@LoadBalanced注解
  • controller正常请求类似

自定义Rule实现:

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
public class MyRibbonRule extends AbstractLoadBalancerRule{

@Resource
private DiscoveryClient discoveryClient; // 获取服务实例用



/**
* 人工实现一个轮询策略,在主启动类使用了下面的注解引用过来
* @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyRibbonRule.class) //为每个服务定制规则
*/
AtomicInteger integer = new AtomicInteger(0);


// 自定义核心方法,挑选主机
private 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 i = integer.getAndIncrement();
System.out.println("MyRibbonRule----integer-cnt="+i);
if(i > Integer.MAX_VALUE){
integer.set(0);
}
server = upList.get(i%upList.size());

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

@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
// 初始化
System.out.println("--------------init rule-------------");
System.out.println(iClientConfig.getClientName());
System.out.println(iClientConfig.getProperties());
}

@Override
public Server choose(Object key) {
// 根据一个key返回一个Server
return choose(getLoadBalancer(), key);
}

}

配置实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class ApplicationContextConfig {

/**
* 类似<bean id="xxx", class="xxxx"></bean>
* @return
*/
@Bean
@LoadBalanced //当调用服务方集群的时候,需要加上这个,然后restTemplate请求地址写cloud注册的服务名即可。
// 自定义负载均衡策略的时候不用这个注解
public RestTemplate getRestTemplate(){
return new RestTemplate();
}

@Bean
public IRule getRule(){
return new MyRibbonRule();
}

}

主启动类

1
2
3
4
5
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyRibbonRule.class) //为每个服务定制规则
@EnableDiscoveryClient // 给自定义规则用,这里可以获取实例列表
public class CustomerMyRibbonMain8083 {

controller:

1
2
3
4
5
6
7
8
9
10
/**
* 测试使用自定义负载均衡策略的方式获取
* @param id
* @return
*/
@GetMapping("/customer/payment/rule/get/{id}")
public CommonResult<Payment> getByIdByIRuleBalancer(@PathVariable("id") long id){
// getForObject 返回Json数据,可以转化为对象
return restTemplate.getForObject(url+"/payment/get/"+id, CommonResult.class);
}

参考链接:

https://blog.csdn.net/qq_41211642/article/details/104772140#comments