微服务(三)之负载均衡(服务端和客户端)

1.客户端负载均衡

  • 通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端端地址,然后进行转发。

  • 目前Dubbo和Ribbon是客户端负载均衡。客户端负载均衡是在spring-cloud分布式框架组件Ribbon中定义的。我们在使用spring-cloud分布式框架时,同一个service大概率同时启动多个,Ribbon通过策略决定本次请求使用哪个service的方式就是客户端负载均衡。在spring-cloud分布式框架中客户端负载均衡对开发者是透明的,添加@LoadBalanced注解就可以了。

常见的客户端赋负载均衡算法:

1.1轮询

负载均衡默认实现方式,请求来之后排队处理;将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

1.2加权轮询

通过对服务器性能的分型,给高配置,低负载的服务器分配更高的权重,均衡各个服务器的压力;

1.3随机

通过随机选择服务进行执行,一般这种方式使用较少;

1.4加权随机

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

1.5源地址Hash取模算法

源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

这种算法很简单,也可以实现数据的均匀分布。但是增加或者减少数据节点的时候会导致所有缓存数据失效。

举个栗子:

例如10条数据,3个节点,如果按照传统hash结构对3进行取模:

  • node a:0,3,6,9
  • node b:1,4,7
  • node c:2,5,8

当增加一个节点的时候,数据分布变更为对4进行取模:

  • node a:0,4,8
  • node b:1,5,9
  • node c:2,6
  • node d:3,7

然后,我们就可以看到,我们需要迁移的节点:3,4,5,6,7,8,9,成本非常高。

所以当集群中数据量很大时,采用一般的哈希函数,在节点数量动态变化的情况下会造成大量的数据迁移,导致网络通信压力的剧增,严重情况,还可能导致数据库宕机。

1.6一致性Hash

通过客户端请求的地址的HASH值取模映射进行服务器调度。

一致性Hash算法也是使用取模的方法,不过,上述的取模方法是对服务器的数量进行取模,而一致性的Hash算法是对2的32方取模。即,一致性Hash算法将整个Hash空间组织成一个虚拟的圆环,Hash函数的值空间为0 ~ 2^32 - 1(一个32位无符号整型),整个哈希环如下:

img

整个圆环以顺时针方向组织,圆环正上方的点代表0,0点右侧的第一个点代表1,以此类推。
第二步,我们将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台服务器就确定在了哈希环的一个位置上,比如我们有三台机器,使用IP地址哈希后在环空间的位置如下图:

img

现在,我们使用以下算法定位数据访问到相应的服务器:

将数据Key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针查找,遇到的服务器就是其应该定位到的服务器。

例如,现在有ObjectA,ObjectB,ObjectC三个数据对象,经过哈希计算后,在环空间上的位置如下:

img

根据一致性算法,Object -> NodeA,ObjectB -> NodeB, ObjectC -> NodeC

数据倾斜问题

在一致性Hash算法服务节点太少的情况下,容易因为节点分布不均匀面造成数据倾斜(被缓存的对象大部分缓存在某一台服务器上)问题,如下图:

img

这时我们发现有大量数据集中在节点A上,而节点B只有少量数据。为了解决数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务器节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体操作可以为服务器IP或主机名后加入编号来实现,实现如下图:

img

数据定位算法不变,只需要增加一步:虚拟节点到实际点的映射。
所以加入虚拟节点之后,即使在服务节点很少的情况下,也能做到数据的均匀分布。

1.7最小连接数

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

2.服务端负载均衡

服务器负载均衡就是指在服务器上游做服务分发,常用的方式有一下几种:

(1)DNS域名解析负载均衡

假设我们的域名指向了多个IP地址,当一个域名请求来时,DNS服务器机进行域名解析将域名转换为IP地址是,在1:N的映射转换中实现负载均衡。DNS服务器提供简单的负载均衡算法,但当其中某台服务器出现故障时,通知DNS服务器移除当前故障IP。

(2)反向代理负载均衡

反向代理只指对服务器的代理,代理服务器接受请求,通过负载均衡算法,将请求转发给后端服务器,后端服务返回给代理服务器然后代理服务器返回到客户端。反向代理服务器的优点是隔离后端服务器和客户端,使用双网卡屏蔽真实服务器网络,安全性更好,相比较于DNS域名解决负载均衡,反向代理在故障处理方面更灵活,支持负载均衡算法的横向扩展。目前使用非常广泛。当然反向代理也需要考虑很多问题,比如单点故障,集群部署等。

  1. 可以根据ip的hash结果分配后台服务器,可以解决session问题
  2. 可以根据请求url的hash结果分配后台服务器,对于服务器缓存友好。

(3)IP负载均衡

我们都知道反向代理工作到HTTP层,本身开销相对大一些,对性能有一定影响,LVS-NAT是一种位于传输层的负载均衡,它通过修改接受的数据包目标地址的方式实现负载均衡。

3.客户端和服务端负载均衡的区别

服务器端负载均衡:例如Nginx,通过Nginx进行负载均衡,先发送请求,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。

客户端负载均衡:例如spring cloud中的ribbon,客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配。

总结就是一句话,服务端的负载均衡是把请求直接发出去,把负载均衡的任务交给其他服务器(可能是非后台服务,比如中间服务Nginx),而客户端是在请求发出去之前先选择发往那个后台服务器。

4.客户端负载均衡举例

4.1Eureka

Eureka是基于REST(Representational State Transfer)服务,主要以AWS云服务为支撑,提供服务发现并实现负载均衡和故障转移。我们称此服务为Eureka服务。Eureka提供了Java客户端组件,Eureka Client,方便与服务端的交互。客户端内置了基于round-robin实现的简单负载均衡。在Netflix,为Eureka提供更为复杂的负载均衡方案进行封装,以实现高可用,它包括基于流量、资源利用率以及请求返回状态的加权负载均衡。

在这里插入图片描述
上面的架构图描述了Eureka是如何在Netflix部署的,这也是Eureka集群的运行方式。在每个区域(region)都有一个eureka集群,它只知道该区域内的实例信息。每个分区(zone)至少有一个eureka服务器来处理本分区故障。

服务注册在Eureka上并且每30秒发送心跳来续租。如果一个客户端在几次内没有刷新心跳,它将在大约90秒内被移出服务器注册表。注册信息和更新信息会在整个eureka集群的节点进行复制。任何分区的客户端都可查找注册中心信息(每30秒发生一次)来定位他们的服务(可能会在任何分区)并进行远程调用。
下图是Eureka服务启动后的web页面,红框中的Application那列,是每个Eureka客户端微服务的实例的名称,在每个微服务的yml配置文件中可进行配置,如:

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service

在这里插入图片描述

4.1.1Eureka服务端

以下为两个Eureka
pom引入依赖

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
 </dependency>

application.yml配置文件(端口为7001的Eureka服务)

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7002.com:7002/eureka

主启动类:(端口为7001)

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

application.yml配置文件(端口为7002的Eureka服务)

server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7001.com:7001/eureka

主启动类:(端口为7002)

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7002 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(EurekaMain7002.class,args);
    }
}

4.1.2Eureka客户端

pom引入依赖

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

application.yml配置文件(微服务8001)

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包 com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/yswdemo?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 


mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.ysw.springcloud.entities    # 所有Entity别名类所在包


eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
  instance:
    instance-id: payment8001
    prefer-ip-address: true

主启动类:(微服务8001)

@SpringBootApplication
@EnableDiscoveryClient
// 也可用@EnableEurekaClient注解表示开启Eureka客户端使用
public class PaymentApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(PaymentApplication.class,args);
    }
}

application.yml配置文件(微服务8002)

server:
  port: 8002

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包 com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/yswdemo?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: YSWysw123456.


mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.ysw.springcloud.entities    # 所有Entity别名类所在包


eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
  instance:
    instance-id: payment8002
    prefer-ip-address: true

主启动类:(微服务8002)

@SpringBootApplication
@EnableEurekaClient
public class PaymentApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(PaymentApplication.class,args);
    }
}

服务8001和服务8002的Controller类中方法是一样的,这里就只写出8001中的方法了:

@RestController
@Slf4j
public class PaymentController {
    
    
    @Autowired
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @PostMapping("payment/create")
    public CommonResult create(@RequestBody Payment payment){
    
    
        int result = paymentService.create(payment);
        log.info("打印结果:"+result);
        if(result > 0){
    
    
            return new CommonResult(200,"插入成功,serverPort"+serverPort,result);
        }else{
    
    
            return new CommonResult(444,"插入失败",null);
        }
    }

    @RequestMapping("payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id" ) Long id){
    
    
        Payment payment = paymentService.getPaymentById(id);
        log.info("打印结果:"+payment);
        if(payment != null){
    
    
            return new CommonResult(200,"查询成功serverPort"+serverPort,payment);
        }else{
    
    
            return new CommonResult(444,"没有对应记录,查询id"+id,null);
        }
    }

4.2Feign

4.2.1理论知识

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

因为feign底层是使用了ribbon作为负载均衡的客户端,而ribbon的负载均衡也是依赖于eureka 获得各个服务的地址,所以要引入eureka-client。

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括我们前面介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。

4.2.2OpenFeign+Eureka实现简单负载均衡(轮询)

对于服务注册和服务发现,还是需要用到eureka的,OpenFeign的作用为服务调用,pom依赖如下:

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

yml配置文件:

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  #指的是建立连接所用的时间
  ConnectTimeout: 2000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ReadTimeout: 5000

Logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.ysw.springcloud.service.PaymentFeignService: debug

主启动类:

@SpringBootApplication
//激活feign的使用,让其在项目中可以使用feign
@EnableFeignClients
public class OrderFeignMain80 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(OrderFeignMain80.class,args);
    }
}

Service层使用feign

@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
    
    
    @RequestMapping("payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id" ) Long id);

    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeOut();
}

Controller调用Service层方法:

@RestController
@Slf4j
public class OrderFeignController {
    
    
    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentByid(@PathVariable("id") Long id){
    
    
        return paymentFeignService.getPaymentById(id);
    }

    @GetMapping(value = "/consumer/payment/feign/timeout")
    public String paymentFeignTimeOut(){
    
    
        return paymentFeignService.paymentFeignTimeOut();
    }
}

微服务五个module截图:
在这里插入图片描述

项目运行结果:(轮询)
第2n+1(n的取值为0,1,2…)次结果显示如下:
在这里插入图片描述

第2n(n的取值为0,1,2…)次结果显示如下:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/MrYushiwen/article/details/125240918