1. 什么是負載均衡?
負載均衡是一種基礎的網絡服務,它的核心原理是按照指定的負載均衡算法,將請求分配到后端服務集群上,從而為系統提供並行處理和高可用的能力。提到負載均衡,你可能想到nginx。對於負載均衡,一般分為服務端負載均衡和客戶端負載均衡
-
服務端負載均衡:在消費者和服務提供方中間使用獨立的代理方式進行負載,有硬件的負載均衡器,比如 F5,也有軟件,比如 Nginx。

-
客戶端負載均衡:所謂客戶端負載均衡,就是客戶端根據自己的請求情況做負載,本文介紹的Netflix Ribbon就是客戶端負載均衡的組件

2. 什么是Netflix Ribbon?
在上一章的學習中,我們知道了微服務的基本概念,知道怎么基於Ribbon+restTemplate的方式實現服務調用,接着上篇博客,我們再比較詳細學習客戶端負載均衡Netflix Ribbon,學習本博客之前請先學習上篇博客,然后再學習本篇博客
Ribbon 是由 Netflix 發布的負載均衡器,它有助於控制 HTTP 和 TCP 的客戶端的行為。Ribbon 屬於客戶端負載均衡。
3. Netflix Ribbon實驗環境准備
環境准備:
- JDK 1.8
- SpringBoot2.2.3
- SpringCloud(Hoxton.SR6)
- Maven 3.2+
- 開發工具
- IntelliJ IDEA
- smartGit
創建一個SpringBoot Initialize項目,詳情可以參考我之前博客:SpringBoot系列之快速創建項目教程
可以引入Eureka Discovery Client,也可以單獨添加Ribbon

Spring Cloud Hoxton.SR6版本不需要引入spring-cloud-starter-netflix-ribbon,已經默認集成

也可以單獨添加Ribbon依賴:

本博客的是基於spring-cloud-starter-netflix-eureka-client進行試驗,試驗前要運行eureka服務端,eureka服務提供者,代碼請參考上一章博客
補充:IDEA中多實例運行方法
step1:如圖,不要加上勾選

step2:指定不同的server端口和實例id,如圖:

啟動成功后,是可以看到多個實例的

4. Netflix Ribbon API使用
使用LoadBalancerClient :
@Autowired
LoadBalancerClient loadBalancerClient;
@Test
void contextLoads() {
ServiceInstance serviceInstance = loadBalancerClient.choose("EUREKA-SERVICE-PROVIDER");
URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost() , serviceInstance.getPort()));
System.out.println(uri.toString());
}
構建BaseLoadBalancer 實例例子:
@Test
void testLoadBalancer(){
// 服務列表
List<Server> serverList = Arrays.asList(new Server("localhost", 8083), new Server("localhost", 8084));
// 構建負載實例
BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
loadBalancer.setRule(new RandomRule());
for (int i = 0; i < 5; i++) {
String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build()
.submit(new ServerOperation<String>() {
public Observable<String> call(Server server) {
try {
String address = "http://" + server.getHost() + ":" + server.getPort()+"/EUREKA-SERVICE-PROVIDER/api/users/mojombo";
System.out.println("調用地址:" + address);
return Observable.just("");
} catch (Exception e) {
return Observable.error(e);
}
}
}).toBlocking().first();
System.out.println("result:" + result);
}
}
5. 負載均衡@LoadBalanced
Ribbon負載均衡實現,RestTemplate 要加上@LoadBalanced
package com.example.springcloud.ribbon.configuration;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* <pre>
* RestConfiguration
* </pre>
*
* <pre>
* @author mazq
* 修改記錄
* 修改后版本: 修改人: 修改日期: 2020/07/31 09:43 修改內容:
* </pre>
*/
@Configuration
public class RestConfiguration {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
yaml配置:
server:
port: 8082
spring:
application:
name: eureka-service-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
fetch-registry: true
register-with-eureka: false
healthcheck:
enabled: false
instance:
status-page-url-path: http://localhost:8761/actuator/info
health-check-url-path: http://localhost:8761/actuator//health
prefer-ip-address: true
instance-id: eureka-service-consumer8082

關鍵點,使用SpringCloud的@LoadBalanced,才能調http://EUREKA-SERVICE-PROVIDER/api/users/? 接口的數據,瀏覽器是不能直接調的
import com.example.springcloud.ribbon.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
@SpringBootApplication
@EnableEurekaClient
@RestController
@Slf4j
public class SpringcloudRibbonApplication {
@Autowired
RestTemplate restTemplate;
public static void main(String[] args) {
SpringApplication.run(SpringcloudRibbonApplication.class, args);
}
@GetMapping("/findUser/{username}")
public User index(@PathVariable("username")String username){
return restTemplate.getForObject("http://EUREKA-SERVICE-PROVIDER/api/users/"+username,User.class);
}
}

6. 定制Netflix Ribbon client
具體怎么定制?可以參考官網,@RibbonClient指定定制的配置類既可

package com.example.springcloud.ribbon.configuration;
import com.example.springcloud.ribbon.component.MyRule;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* <pre>
* Ribbon Clients configuration
* </pre>
*
* <pre>
* @author mazq
* 修改記錄
* 修改后版本: 修改人: 修改日期: 2020/07/29 14:22 修改內容:
* </pre>
*/
//@Configuration(proxyBeanMethods = false)
//@IgnoreComponentScan
public class RibbonClientConfiguration {
// @Autowired
// IClientConfig config;
@Bean
public IRule roundRobinRule() {
return new MyRule();
}
@Bean
public ZonePreferenceServerListFilter serverListFilter() {
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
filter.setZone("myTestZone");
return filter;
}
@Bean
public IPing ribbonPing() {
return new PingUrl();
}
}
在Application類加上@RibbonClient,name是為服務名稱,跟bootstrap.yml配置的一樣既可
@RibbonClient(name = "eureka-service-provider",configuration = RibbonClientConfiguration.class)
特別注意:官網這里特意提醒,這里的意思是說@RibbonClient指定的配置類必須加@Configuration(不過在Hoxton.SR6版本經過我的驗證,其實是可以不加的,加了反而可能報錯),@ComponentScan掃描要排除自定義的配置類,否則,它由所有@RibbonClients共享。如果你使用@ComponentScan(或@SpringBootApplication)

其實就是想讓我們排除這個配置的全局掃描,所以我們可以進行編碼,寫個注解類@IgnoreComponentScan ,作用於類,指定@Target(ElementType.TYPE)
package com.example.springcloud.ribbon.configuration;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreComponentScan {
}
加上自定義的注解類

任何在Application加上代碼,避免全局掃描:
@ComponentScan(excludeFilters={@ComponentScan.Filter(type= FilterType.ANNOTATION,value= IgnoreComponentScan.class)})
7. Netflix Ribbon常用組件
ps:介紹Netflix Ribbon的負載策略之前,先介紹Netflix Ribbon常用組件及其作用:
| 組件 | 作用 |
|---|---|
| ILoadBalancer | 定義一系列的操作接口,比如選擇服務實例。 |
| IRule | 負載算法策略,內置算法策略來為服務實例的選擇提供服務。 |
| ServerList | 負責服務實例信息的獲取(可以獲取配置文件中的,也可以從注冊中心獲取。) |
| ServerListFilter | 過濾掉某些不想要的服務實例信息。 |
| ServerListUpdater | 更新本地緩存的服務實例信息。 |
| IPing | 對已有的服務實例進行可用性檢查,保證選擇的服務都是可用的。 |
8. 定制Netflix Ribbon策略
因為服務提供者是多實例的,所以再寫個接口測試,調用了哪個實例,來看看Netflix Ribbon的負載策略
@Autowired
LoadBalancerClient loadBalancerClient;
@GetMapping(value = {"/test"})
public String test(){
ServiceInstance serviceInstance = loadBalancerClient.choose("EUREKA-SERVICE-PROVIDER");
URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost() , serviceInstance.getPort()));
System.out.println(uri.toString());
return uri.toString();
}
部署成功,多次調用,可以看到每次調用的服務實例都不一樣?其實Netflix Ribbon默認是按照輪詢的方式調用的

要定制Netflix Ribbon的負載均衡策略,需要實現AbstractLoadBalancerRule抽象類,下面給出類圖:

Netflix Ribbon內置了如下的負載均衡策略,引用https://juejin.im/post/6854573215587500045的歸納:

ok,接着我們可以在配置類,修改規則
@Bean
public IRule roundRobinRule() {
return new BestAvailableRule();
}
測試,基本都是調8083這個實例,因為這個實例性能比較好

顯然,也可以自己寫個策略類,代碼參考com.netflix.loadbalancer.RandomRule,網上也有很多例子,思路是修改RandomRule原來的策略,之前隨機調服務實例一次,現在改成每調5次后,再調其它的服務實例
package com.example.springcloud.ribbon.component;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class MyRule extends AbstractLoadBalancerRule
{
// 總共被調用的次數,目前要求每台被調用5次
private int total = 0;
// 當前提供服務的機器號
private int index = 0;
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) {
// 沒有獲取到服務
return null;
}
//int index = chooseRandomInt(serverCount);
//server = upList.get(index);
if(total < 5)
{
server = upList.get(index);
total++;
}else {
total = 0;
index++;
if(index >= upList.size())
{
index = 0;
}
}
if (server == null) {
// 釋放線程
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
修改IRule ,返回MyRule
@Bean
public IRule roundRobinRule() {
return new MyRule();
}
附錄:
ok,本博客參考官方教程進行實踐,僅僅作為入門的學習參考資料,詳情可以參考Spring Cloud官方文檔https://docs.spring.io/spring-cloud-netflix/docs/2.2.x-SNAPSHOT/reference/html/#customizing-the-ribbon-client
代碼例子下載:code download
優質學習資料參考:
-
Ribbon負載均衡 -> 源碼剖析:Ribbon負載均衡 -> 源碼剖析
-
方志鵬大佬系列Spring Cloud博客:https://www.fangzhipeng.com/spring-cloud.html
-
使用Spring Cloud與Docker實戰微服務:https://eacdy.gitbooks.io/spring-cloud-book/content/
-
程序員DD大佬系列Spring Cloud博客:http://blog.didispace.com/spring-cloud-learning/
