java框架之SpringCloud(4)-Ribbon&Feign負載均衡

上一章節已經學習了 Eureka 的使用,SpringCloud 也提供了基於 Eureka 負載均衡的兩種方案:Ribbon 和 Feign。html

Ribbon負載均衡

介紹

SpringCloud Ribbon 是基於 Netflix Ribbon 實現的一套客戶端負載均衡的工具。java

Ribbon 是 Netflix 發佈的開源項目,主要功能是提供客戶端的軟件負載均衡算法,將 Netflix 的中間層服務鏈接在一塊兒。Ribbon 客戶端組件提供一系列完善的配置項如鏈接超時重試等。簡單地說,就是在配置文件中列出 Load Balancer(簡稱 LB)後面全部的機器,Ribbon 會自動幫助你基於某種規則(如簡單輪詢、隨機鏈接等)去鏈接這些機器。咱們也很容易使用 Ribbon 實現自定義的負載均衡算法。mysql

LB,即負載均衡(Load Balance),在微服務或分佈式集羣中常常用的一種應用。負載均衡簡單地說就是將用戶的請求平均的分配到多個服務上,從而達到系統的 HA。web

常見的負載均衡方案有軟件 Nginx、LVS,硬件 F5 等。算法

而 LB 又能夠分爲如下兩種:spring

  • 集中式 LB:在服務的消費方和提供方之間使用獨立的 LB 設施(能夠是硬件,如 F5,也能夠是軟件,如 Nginx),由該設施負責把訪問請求經過某種策略轉發至服務的提供方。
  • 進程內 LB:將 LB 邏輯集中到消費方,消費方從註冊中心獲知有哪些地址可用,而後本身再從這些地址中選擇出一個合適的服務器。Ribbon 就屬於進程內 LB,它只是一個類庫,集成於消費方進程,消費方經過它來獲取到服務提供方的地址。

使用

一、修改消費者服務工程,對應占用端口爲 80,添加依賴以下:sql

<!--ribbon 相關-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

二、修改配置將消費者服務工程做爲 Eureka 客戶端註冊到 Eureka 集羣:數據庫

server:
  port: 80

eureka:
  client:
    service-url:
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept
    prefer-ip-address: true # 訪問路徑顯示 IP

spring:
  application:
    name: microservicecloud-consumer-dept
application.yml

三、啓用 Eureka 客戶端功能:服務器

package zze.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class Application_80 {

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

四、修改配置類,註冊 RestTemplate  bean 時添加註解啓用負載均衡:mybatis

package zze.springcloud.cfgbeans;

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;

@Configuration
public class ConfigBean {
    @Bean
    @LoadBalanced // 客戶端負載均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
zze.springcloud.cfgbeans.ConfigBean

五、修改 Controller,修改微服務具體訪問 URL 爲微服務名稱:

package zze.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import zze.springcloud.entities.Dept;

import java.util.List;

@RestController
@RequestMapping("/consumer/dept")
public class DeptController {
    // 微服務 Provider 的服務地址
    // private static String REST_URL_PREFIX = "http://localhost:8001";

    // 要調用的微服務名稱 即微服務工程的 spring.application.name 對應值
    private static String REST_URL_PREFIX = "http://microservicecloud-provider-dept";
    @Autowired
    private RestTemplate restTemplate;

    @PostMapping("/add")
    public boolean add(@RequestBody Dept dept) {
        return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
    }

    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id) {
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
    }

    @GetMapping("/list")
    public List<Dept> list() {
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
    }
}
zze.springcloud.controller.DeptController

到這裏其實已經完成了負載均衡的基本配置,依次啓動項目是能夠正常訪問的,可是由於提供者服務只有一個,看不出負載均衡的效果。

六、新建兩個數據庫,分別名爲 "springcloud_8002" 和 "springcloud_8003",接着分別在兩個數據庫中執行下面 sql 腳本初始化表結構及數據:

CREATE TABLE dept
(
  dept_no   BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  dept_name VARCHAR(60),
  db_source VARCHAR(60)
);

INSERT INTO dept(dept_name, db_source)
VALUES ('開發部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('人事部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('財務部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('市場部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('運維部', DATABASE());

SELECT * FROM dept;
dept.sql

七、新建兩個子工程做爲 Provider 服務工程與 "microservicecloud-provider-dept-8001" 提供相同服務作 Provider 服務集羣,分別名爲 "microservicecloud-provider-dept-8002" 和 "microservicecloud-provider-dept-8003",三個工程鏈接不一樣數據庫,對應配置以下:

server:
  port: 8001

mybatis:
  config-location: classpath:mybatis/mybatis.cfg.xml # mybatis 配置文件路徑
  type-aliases-package: zze.springcloud.entities # 全部 Entity 別名類所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper 映射文件

spring:
  application:
    name: microservicecloud-provider-dept # 當前微服務名稱
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 數據源操做類型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql 驅動包
    url: jdbc:mysql:///springcloud_8001 # 數據庫鏈接 root
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 數據庫鏈接池的最小維持鏈接數
      initial-size: 5 # 初始化鏈接數
      max-total: 5 # 最大鏈接數
      max-wait-millis: 200 # 等待鏈接獲取的最大超時時間

eureka:
  client: # 將當前工程做爲 Eureka 客戶端
    service-url:
      # 單機版
      # defaultZone: http://localhost:7001/eureka # Eureka 服務端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept-8001
    prefer-ip-address: true # 訪問路徑顯示 IP

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8001
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
application.yml#8001
server:
  port: 8002

mybatis:
  config-location: classpath:mybatis/mybatis.cfg.xml # mybatis 配置文件路徑
  type-aliases-package: zze.springcloud.entities # 全部 Entity 別名類所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper 映射文件

spring:
  application:
    name: microservicecloud-provider-dept # 當前微服務名稱
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 數據源操做類型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql 驅動包
    url: jdbc:mysql:///springcloud_8002 # 數據庫鏈接 root
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 數據庫鏈接池的最小維持鏈接數
      initial-size: 5 # 初始化鏈接數
      max-total: 5 # 最大鏈接數
      max-wait-millis: 200 # 等待鏈接獲取的最大超時時間

eureka:
  client: # 將當前工程做爲 Eureka 客戶端
    service-url:
      # 單機版
      # defaultZone: http://localhost:7001/eureka # Eureka 服務端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept-8002
    prefer-ip-address: true # 訪問路徑顯示 IP

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8002
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
application.yml#8002
server:
  port: 8003

mybatis:
  config-location: classpath:mybatis/mybatis.cfg.xml # mybatis 配置文件路徑
  type-aliases-package: zze.springcloud.entities # 全部 Entity 別名類所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper 映射文件

spring:
  application:
    name: microservicecloud-provider-dept # 當前微服務名稱
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 數據源操做類型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql 驅動包
    url: jdbc:mysql:///springcloud_8003 # 數據庫鏈接 root
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 數據庫鏈接池的最小維持鏈接數
      initial-size: 5 # 初始化鏈接數
      max-total: 5 # 最大鏈接數
      max-wait-millis: 200 # 等待鏈接獲取的最大超時時間

eureka:
  client: # 將當前工程做爲 Eureka 客戶端
    service-url:
      # 單機版
      # defaultZone: http://localhost:7001/eureka # Eureka 服務端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept-8003
    prefer-ip-address: true # 訪問路徑顯示 IP

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8003
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
application.yml#8003

八、測試:

先啓動佔用端口 700一、700二、7003 的 EurekaServer 服務,
再啓動佔用端口 800一、800二、8003 的 Provider 服務,
最後啓動佔用端口 80 的消費者服務,隨便訪問一個 EurekaServer 的 WebUI 頁面:

會發現消費者服務與提供者服務都註冊到了 EurekaServer,而且提供者服務有三個實例分別佔用端口 800一、800二、8003,
屢次訪問 http://localhost/consumer/dept/list,會發現每次訪問返回的數據都是從不一樣的數據庫返回,即負載均衡環境搭建成功。
test

Ribbon 在工做時分爲兩步走:

        一、選擇 EurekaServer,它優先選擇在同一個區域內負載較少的 Server。

        二、根據用戶指定的策略,從 EurekaServer 取到的服務註冊列表中選擇一個服務。

其中 Ribbon 提供了多種策略,例如輪詢(默認)、隨機和根據響應時間加權。

若測試機器內存不足,則能夠只啓動一部分服務測試,例如能夠只啓動一個 EurekaServer,由於在 EurekaServer 集羣中每個 EurekaServer 都是平等的,還能夠只啓動兩個提供者服務,能看到返回數據的不一樣便可,內存沒有 16G 有點 hold 不住。

核心組件IRule

由於 Ribbon 使用的是客戶端負載均衡,因此下面的操做都是在佔用 80 端口的消費者服務工程下。

默認提供的Rule

經過上面的使用測試會發現 Ribbon 默認的負載均衡策略是依次輪詢訪問每一個微服務,若是咱們須要修改它默認的負載均衡策略,則可使用 IRule 組件。

IRule:其實是一個接口,它的實現類須要可以根據特定算法從服務列表中選取一個要訪問的服務,默認提供的實現有以下:

IRule
    AbstractLoadBalancerRule (com.netflix.loadbalancer)
        ClientConfigEnabledRoundRobinRule (com.netflix.loadbalancer)
            BestAvailableRule (com.netflix.loadbalancer) // 先過濾掉因爲屢次訪問故障而處於斷路器跳閘狀態的服務,而後選擇一個併發量最小的服務
            PredicateBasedRule (com.netflix.loadbalancer)
                ZoneAvoidanceRule (com.netflix.loadbalancer) // 默認規則,綜合判斷 Server 所在區域的性能和 server 的可用性選擇服務器
                AvailabilityFilteringRule (com.netflix.loadbalancer) // 先過濾掉因爲屢次訪問故障而處於斷路器跳閘狀態的服務,還有併發的連接數量超過閾值的服務,而後對剩餘的服務按照輪詢策略進行選擇
        RoundRobinRule (com.netflix.loadbalancer) // 輪詢
            WeightedResponseTimeRule (com.netflix.loadbalancer) // 根據平均響應時間計算全部服務的權重,響應時間越快服務權重越大,被選擇的概率越高。剛啓動的時候若是統計信息不足,則使用 RoundRobinRule,等統計信息完善時會自動切換到當前策略
            ResponseTimeWeightedRule (com.netflix.loadbalancer)
        RandomRule (com.netflix.loadbalancer) // 隨機
        RetryRule (com.netflix.loadbalancer) // 先按照 RoundRobinRule 策略獲取服務,若是獲取服務失敗則在指定時間內會重試,若是依舊失敗則會獲取其它可用的服務

若是要修改負載均衡策略,只須要將實現 IRule 接口的 bean 註冊到 IoC 容器便可,如:

package zze.springcloud.cfgbeans;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
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;

@Configuration
public class ConfigBean {
    @Bean
    @LoadBalanced // 客戶端負載均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean // 負載均衡策略使用隨機訪問
    public IRule randomRule(){
        return new RandomRule();
    }
}
zze.springcloud.cfgbeans.ConfigBean

自定義Rule

上面說過咱們只須要將實現 IRule 接口的 bean 註冊到 IoC 容器規則就能夠生效,因此咱們能夠自定義一個 IRule 的實現類,好比要定製一個每一個服務依次訪問 5 次的規則:

package zze.springcloud.cfgbeans;

import java.util.List;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

/**
 * 參考 RandomRule 源碼自定義 Rule,每一個服務訪問 5 次
 */
public class MyRandomRule extends AbstractLoadBalancerRule
{
    private int total = 0;             // 總共被調用的次數,目前要求每臺被調用5次
    private int currentIndex = 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) {
                /*
                 * No servers. End regardless of pass, because subsequent passes only get more
                 * restrictive.
                 */
                return null;
            }

//            int index = rand.nextInt(serverCount);// java.util.Random().nextInt(3);
//            server = upList.get(index);

            
//            private int total = 0;             // 總共被調用的次數,目前要求每臺被調用5次
//            private int currentIndex = 0;    // 當前提供服務的機器號
            if(total < 5)
            {
                server = upList.get(currentIndex);
                total++;
            }else {
                total = 0;
                currentIndex++;
                if(currentIndex >= upList.size())
                {
                  currentIndex = 0;
                }
            }            
            
            
            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 Server choose(Object key)
    {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig)
    {
        // TODO Auto-generated method stub

    }
}
zze.springcloud.cfgbeans.MyRandomRule

除了將 IRule 實例直接註冊到 IoC 容器這種方式,咱們還能夠自定義一個規則配置類:

package zze.config;

import org.springframework.context.annotation.Bean;

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Configuration;
import zze.springcloud.cfgbeans.MyRandomRule;

/*
注意,官方文檔表示該類要定義在 @ComponentScan 註解掃描範圍以外
 */
@Configuration
public class MySelfRuleCofnig
{
    @Bean
    public IRule myRule()
    {
        return new MyRandomRule();
    }
}
zze.config.MySelfRuleCofnig

接下來能夠經過在主啓動類上經過指定註解讓配置類對指定服務生效:

package zze.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import zze.config.MySelfRuleCofnig;

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "MICROSERVICECLOUD-PROVIDER-DEPT",configuration = MySelfRuleCofnig.class) // 針對 MICROSERVICECLOUD-PROVIDER-DEPT 服務實例採用的負載均衡策略配置類爲 MySelfRuleCofnig
public class Application_80 {

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

Feign負載均衡

介紹

Feign 是一個聲明式 WebService 客戶端。使用 Feign 可以讓編寫 WebService 客戶端更加簡單,它的使用方法是定義一個接口,而後在上面添加註解,同時也支持 JAX-RS 標準的註解。Feign 也支持可拔插式的編碼器和解碼器。SpringCloud 對 Feign 進行了封裝,使其支持了 SpringMVC 標準註解和 HttpMessageConverters。Feign 能夠與 Eureka 和 Ribbon 組合使用以支持負載均衡。

Feign 能幹什麼?

前面在使用 Ribbon+RestTemplate,利用 RestTemplate 對 httpClient 封裝,造成了一套模板化的調用方法。可是在實際開發中,因爲對服務依賴的調用可能不止一處,每每一個接口會被多處調用,因此一般都會針對每一個微服務自行封裝一些客戶端類來包裝這些依賴服務的調用。而 Feign 就是在這個基礎上作了進一步封裝,由他來幫助咱們實現依賴服務接口。在 Feign 的實現下,咱們只須要建立一個接口並使用註解的方式來配置它(相似於 MyBatis 的 Mapper 類使用 @Mapper 註解),便可完成對服務提供方的接口綁定,簡化了使用 SpringCloud Ribbon 時,自動封裝服務調用客戶端的開發量。也能夠說 Feign 就是對 Ribbon 的進一步封裝的實現。

使用

一、複製 "microservicecloud-consumer-dept-80" 子工程更名爲 "microservicecloud-consumer-dept-feign",新增依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

二、新建接口,使用 Feign 提供的註解標識該接口爲 Feign 客戶端:

package zze.springcloud.service;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import zze.springcloud.entities.Dept;

import java.util.List;

@FeignClient(value = "microservicecloud-provider-dept") // 標識調用哪一個微服務
@RequestMapping("/dept")
public interface DeptClientService {
    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id);

    @GetMapping("/list")
    public List<Dept> list();

    @PostMapping("/add")
    public boolean add(Dept dept);
}
zze.springcloud.service.DeptClientService

三、修改主啓動類,使用註解啓用 Feign:

package zze.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"zze.springcloud.service"}) // 指定 Feign 客戶端的掃描包
public class Application_80 {

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

四、修改 Controller,使用 Feign 客戶端:

package zze.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import zze.springcloud.entities.Dept;
import zze.springcloud.service.DeptClientService;

import java.util.List;

@RestController
@RequestMapping("/consumer/dept")
public class DeptController {

    @Autowired
    private DeptClientService deptService;

    @PostMapping("/add")
    public boolean add(@RequestBody Dept dept) {
        return deptService.add(dept);
    }

    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id) {
        return deptService.get(id);
    }

    @GetMapping("/list")
    public List<Dept> list() {
        return deptService.list();
    }
}
zze.springcloud.controller.DeptController

五、測試:

一、啓動 Eureka 集羣 700一、700二、7003
二、啓動 Provider 集羣 800一、800二、8003
三、啓動 microservicecloud-consumer-dept-feign,訪問 http://localhost/consumer/dept/get/1,返回結果,成功。

test
Feign 集成了 Ribbon,利用 Ribbon 維護了 microservicecloud-provider-dept 服務的信息,而且經過輪詢實現了客戶端的負載均衡。與 Ribbon 不一樣的是,Feign 無需手動使用 RestTemplate 構建請求,只須要以聲明式方法定義服務綁定接口,優雅而簡單的實現了服務調用。
相關文章
相關標籤/搜索