微服务温习笔记(二)

今日的温习笔记主题:负载均衡  简称:LB  全称:Load Balance


负载均衡

一、简介

        谈起负载均衡,它是当下高并发、高可用系统框架重要的一个概念,同样也是一个关键的组件,当然在微服务框架中亦是如此。通俗的讲,负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。

二、 负载均衡的主要作用

首先来看一下一个没有负载均衡的 web 架构,类似下面这样:

若依微服务负载均衡用的什么中间件 微服务 负载均衡_若依微服务负载均衡用的什么中间件

        在这里用户是直连到 web 服务器,如果这个服务器宕机了,那么用户自然也就没办法访问了。另外,如果同时有很多用户试图访问服务器,超过了其能处理的极限,就会出现加载速度缓慢或根本无法连接的情况。

        而通过在后端引入一个负载均衡器和至少一个额外的 web 服务器,可以缓解这个故障。通常情况下,所有的后端服务器会保证提供相同的内容,以便用户无论哪个服务器响应,都能收到一致的内容。

若依微服务负载均衡用的什么中间件 微服务 负载均衡_后端_02

        从图里可以看到,用户访问负载均衡器,再由负载均衡器将请求转发给后端服务器。在这种情况下,单点故障现在转移到负载均衡器上了。这里又可以通过引入第二个负载均衡器来缓解。

因此负载均衡的主要作用有以下几条:

①高并发:负载均衡通过算法调整负载,尽力均匀的分配应用集群中各节点的工作量,以此提高应用集群的并发处理能力(吞吐量)。

②伸缩性:添加或减少服务器数量,然后由负载均衡进行分发控制。这使得应用集群具备伸缩性。

③高可用:负载均衡器可以监控候选服务器,当服务器不可用时,自动跳过,将请求分发给可用的服务器。这使得应用集群具备高可用的特性。

④安全防护:有些负载均衡软件或硬件提供了安全性功能,如:黑白名单处理、防火墙,防 DDos 攻击等。

三、负载均衡的工作方式

1.负载均衡一般可以处理HTTP、HTTPS 、TCP、UDP 等四种请求

HTTP协议:HTTP协议也就是在应用层HTTP协议的规则进行的封装,数据形式进行HTTP协议的规则传输

HTTPS协议:HTTPS协议也就是在应用层HTTP协议的规则进行的封装,数据形式进行HTTP协议的规则传输,

TCP协议:TCP协议也就是在传输层用TCP协议的规则进行的封装,数据形式进行TCP协议的规则传输

UDP协议:UDP协议也就是在传输层用UDP协议的规则进行的封装,数据形式进行TCP协议的规则传输

2.负载均衡器如何选择要转发的后端服务器?

负载均衡器一般根据两个因素来决定要将请求转发到哪个服务器。首先,确保所选择的服务器能够对请求做出响应,然后根据预先配置的规则从健康服务器池(healthy pool)中进行选择。

因为,负载均衡器应当只选择能正常做出响应的后端服务器,因此就需要有一种判断后端服务器是否「健康」的方法。为了监视后台服务器的运行状况,运行状态检查服务会定期尝试使用转发规则定义的协议和端口去连接后端服务器。如果,服务器无法通过健康检查,就会从池中剔除,保证流量不会被转发到该服务器,直到其再次通过健康检查为止。

3.负载均衡算法

负载均衡算法决定了后端的哪些健康服务器会被选中。几个常用的算法:

Round Robin(轮询):为第一个请求选择列表中的第一个服务器,然后按顺序向下移动列表直到结尾,然后循环。

Least Connections(最小连接):优先选择连接数最少的服务器,在普遍会话较长的情况下推荐使用。

Source:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。

如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器。可以通过 Source 算法基于客户端的 IP 信息创建关联,或者使用粘性会话(sticky sessions)。

  4.负载均衡的单点故障问题   

想要解决负载均衡器的单点故障问题,可以将第二个负载均衡器连接到第一个上,从而形成一个集群,当主负载均衡器发生了故障,就需要将用户请求转到第二个负载均衡器。因为 DNS 更改通常会较长的时间才能生效,因此需要能灵活解决 IP 地址重新映射的方法,比如浮动 IP(floating IP)。这样域名可以保持和相同的 IP 相关联,而 IP 本身则能在服务器之间移动。

若依微服务负载均衡用的什么中间件 微服务 负载均衡_后端_03

  四、入门案例    

1.创建一个父工程

pom.xml:

<properties>
        <!-- 项目源码及编译输出的编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <!-- 项目编译JDK版本 -->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!--Spring Boot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud Netflix-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR9</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud 阿里巴巴-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>

2.创建子模块common

在common中创建一个实体类User:

public class User {
    private Integer id;
    private String name;
    private Integer age;

    public User() {
    }

    public User(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

3.创建子模块provider_1

pom.xm

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.exercise</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

控制层:PrividerController:

@RestController
@RequestMapping("/provider")
public class ProviderController {
	
	@Autowired
	private UserService userService;

	@RequestMapping("/getUserById/{id}")
	public User getUserById(@PathVariable Integer id){
		return userService.getUserById(id);
	}
}

服务层接口:UserService:

public interface UserService {
    User getUserById(Integer id);
}

服务层实现类:UserServiceImpl

@Service
public class UserServiceImpl implements UserService {

    @Override
    public User getUserById(Integer id) {
        return new User(id,"曹公公-1",18);
    }
}

启动类:

@SpringBootApplication
@EnableDiscoveryClient//注册该服务,并发现其他服务
public class RibbonProviderApp {

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

application.ym

server:
  port: 9090
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.116.131 #注册中心的地址
  application:
    name: provider #注册到nacos的名字

4.创建子模块provider_2

控制层:PrividerController:

@RestController
@RequestMapping("/provider")
public class ProviderController {
	
	@Autowired
	private UserService userService;

	@RequestMapping("/getUserById/{id}")
	public User getUserById(@PathVariable Integer id){
		return userService.getUserById(id);
	}
}

服务层接口:UserService:

public interface UserService {
    User getUserById(Integer id);
}

服务层实现类:UserServiceImpl:

@Service
public class UserServiceImpl implements UserService {

    @Override
    public User getUserById(Integer id) {
        return new User(id,"曹公公-1",18);
    }
}

启动类:

@SpringBootApplication
@EnableDiscoveryClient//注册该服务,并发现其他服务
public class RibbonProviderApp {

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

application.yml

server:
  port: 9091
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.116.131 #注册中心的地址
  application:
    name: provider #注册到nacos的名字

5.创建子模块consummer

pom.xml:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.exercise</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

Consumercontroller:

@RestController
@RequestMapping(value = "/consumer")
public class ConsumerController {
	
	@Autowired
	private RestTemplate restTemplate;

	@Autowired
	private DiscoveryClient discoveryClient;

	private int index;

	@RequestMapping(value="/getUserById/{id}")
	public User getUserById(@PathVariable Integer id){
		List<ServiceInstance> serviceList = 
            				discoveryClient.getInstances("ribbon-provider");

		//随机方式获得服务
		//int currentIndex = new Random().nextInt(serviceList.size());
		//轮询方式获得服务
		index=index + 1;
        int currentIndex = (index) % serviceList.size();
		
		ServiceInstance instance = serviceList.get(currentIndex);
		String serviceUrl = instance.getHost() + ":" + instance.getPort();
        System.out.println("serviceUrl:"+serviceUrl);
		String url = "http://"+serviceUrl+"/provider/getUserById/"+id;
		return restTemplate.getForObject(url, User.class);
	}
}

启动类:

@SpringBootApplication
@EnableDiscoveryClient
public class RibbonConsumerApp {

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

application.yml

server:
  port: 80
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.209.129:8848
  application:
    name: ribbon-consumer

测试:

若依微服务负载均衡用的什么中间件 微服务 负载均衡_后端_04

 五、Ribbon介绍

1.什么是Ribbon

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。

若依微服务负载均衡用的什么中间件 微服务 负载均衡_微服务_05

我们不需要去引入ribbon的依赖,因为在nacos里面已经集成了ribbon的依赖:

若依微服务负载均衡用的什么中间件 微服务 负载均衡_微服务_06

Ribbon默认提供 很多种负载均衡算法,例如轮询、随机 等等。

2.负载均衡策略

负载均衡接口: com.netflix.loadbalancer.IRule

若依微服务负载均衡用的什么中间件 微服务 负载均衡_java_07

 随机策略

com.netflix.loadbalancer.RandomRule:该策略实现了从服务清单中随机选择一个服务实例的功能。

若依微服务负载均衡用的什么中间件 微服务 负载均衡_java_08

 轮循策略:

com.netflix.loadbalancer.RoundRobinRule:该策略实现按照线性轮询的方式依次选择实例的功能。具体实现如下,在循环中增加了一个count计数变量,该变量会在每次轮询之后累加并求余服务总数

若依微服务负载均衡用的什么中间件 微服务 负载均衡_后端_09

3.基于ribbon实现负载均衡 

3.1修改consummer

添加ConfigBean:

@Configuration
public class ConfigBean {

	@Bean
	/**
	 * 添加了@LoadBalanced注解之后,Ribbon会给restTemplate请求添加一个拦截器,在拦截器中获取
	 * 注册中心的服务列表,并使用Ribbon内置的负载均衡算法从服务列表里选中一个服务,通过获取到的服务信息        * (ip,port)替换 serviceId 实现负载请求。
	 */
	@LoadBalanced //开启负载均衡
	public RestTemplate getRestTemplate(){
		return new RestTemplate();
	}

	//随机策略
	@Bean
	public IRule iRule() {
		return new RandomRule();
	}
}

修改ConsumerController:

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private DiscoveryClient discoveryClient;//发现服务的工具类
    private int index;

    @RequestMapping("/getUserById/{id}")
    public User getUserById(@PathVariable Integer id){

        List<String> serviceList = discoveryClient.getServices();

        //随机方式获得服务
        //int currentIndex = new Random().nextInt(2);
        //轮询方式获得服务
        //index=index + 1;
        //int currentIndex = (index) % serviceList.size();

        //ServiceInstance instance = discoveryClient.getInstances("ribbon-provider").get(currentIndex);
        //String url = "http://"+instance.getHost()+":"+instance.getPort()+"/provider/getUserById/"+id;
        String url = "http://ribbon-provider/provider/getUserById/"+id;
        return restTemplate.getForObject(url,User.class);
    }
}

 测试:

  1. 分别使用轮询和随机策略调用服务提供者,多试几次,奥妙即现。