在Spring Cloud中有一个专门实现负载均衡的组件,该组件为Spring Cloud Netflix Ribbon
负载均衡一共分为两种,其一是服务器端负载均衡;其二是客户端负载均衡。
提供服务器端负载均衡的工具有很多,比如说Apache、Nginx、HAProxy等都实现了基于HTTP协议或TCP协议的负载均衡模块。
基于服务器端的负载均衡机制实现比较简单,只需要在客户端与各个微服务实例之间架设集中式的负载均衡器即可。负载均衡器与各个微服务实例之间需要实现服务诊断以及状态监控,通过动态获取各个微服务的运行时信息决定负载均衡的目标服务。如果负载均衡器检测到某个服务不可用时就会自动移除该服务。
通过上述分析,可以看到负载均衡器运行在一台独立的服务器上并充当代理(Proxy)的作用。所有的请求都需要通过负载均衡器的转发才能实现服务调用,这可能会是一个问题,因为当服务请求量越来越大时,负载均衡器将会成为系统的瓶颈。同时,一旦负载均衡器自身发生失败,整个微服务的调用过程都将发生失败。因此,为了避免集中式负载均衡所带来的这种问题,客户端负载均衡同样也是一种常见的方式。
客户端负载均衡的主要优势在于其不会出现集中式负载均衡所产生的性能瓶颈问题,因为每个客户端都有自己的负载均衡器,该负载均衡器的失败不会造成严重的后果。由于所有运行时信息都需要在多个负载均衡器之间进行传递,因而客户端负载均衡器会在一定程度上加重网络流量负载。
客户端负载均衡是在客户端内部设定一个调度算法,通过收集各个微服务的实例信息,在向服务器发起请求时,先执行调度算法计算出目标服务器地址,然后通过负载均衡计算出目标服务器地址实现负载均衡效果。
与服务器端负载均衡相比,客户端负载均衡由于不需要架设专门的服务器端代理,因而不会出现集中式负载均衡所产生的那种性能瓶颈问题。在微服务架构中,客户端负载均衡是常见的负载均衡实现方案,包括Dubbo和Spring Cloud在内的很多框架都采用的客户端负载均衡实现机制。
负载均衡算法一共分为两大类,分别是静态负载均衡算法和动态负载均衡算法
静态随机算法的代表是随机(Random)算法和轮询(Round Robin)算法。
随机算法实现起来比较简单,在JDK中自带了Random随机算法,通过该算法,我们可以指定服务器提供者的地址。随机算法的一种改进是加权随机(Weight Random)算法,在集群中可能存在部分性能比较优的服务器,为了使这些服务器响应更多的请求,可以通过加权随机算法提升这些服务器的权重。
轮询算法就是按照一定的顺序依次访问各个微服务。加权轮询(Weighted Round Robin)算法同样使用权重,一般的流程是顺序循环遍历服务提供者列表,到达上限之后重新归零,继续顺序循环直到指定某一台服务器作为服务器的提供者。普通的轮询算法实际上就是权重为1的加权轮询算法。
所有涉及权重的静态算法都可以转变为动态算法,因为权重可以在运行过程中动态更新。典型的动态算法包括最少连接数算法、服务调用延时算法和源IP哈希算法。
最少连接数算法对传入的请求根据每台服务器当前所打开的连接数来分配。
在服务调用延时算法中,服务消费者缓存并计算所有服务提供者的服务调用时延,根据服务调用和平均时延的差值动态调整权重。
源IP哈希算法实现请求IP粘滞连接,尽可能让消费者总是向同一提供者发起调用服务。这是一种有状态机制,也可以归为动态负载均衡算法。
GoodsOrderApplication
package com.lyc.goodsOrder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/** * @author: zhangzhenyi * @date: 2019/3/14 17:33 * @description: GoodsOrder Bootstrap启动类 **/
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class GoodsOrderApplication {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(GoodsOrderApplication.class,args);
}
}
GoodsOrderController
package com.lyc.goodsOrder.controller;
import com.lyc.goodsOrder.entity.Orders;
import com.lyc.goodsOrder.service.GoodsOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/** * @author: zhangzhenyi * @date: 2019/3/15 21:38 * @description: GoodsOrder Controller **/
@RestController
@RequestMapping("/v1/orders")
public class GoodsOrderController {
@Autowired
private GoodsOrderService goodsOrderService;
/** * 保存订单信息 * @param productCode 产品编号 * @param accountId 用户id * @return */
@PostMapping("/{productCode}/{accountId}")
public Orders saveOrder(@PathVariable("productCode") String productCode,@PathVariable("accountId") Long accountId){
return goodsOrderService.addOrder(accountId,productCode);
}
/** * 分页获取订单 * @param pageIndex 当前页索引值 * @param pageSize 每页显示的信息条数 * @return */
@GetMapping("/{pageIndex}/{pageSize}")
public List<Orders> getOrders(@PathVariable("pageIndex") int pageIndex,@PathVariable("pageSize") int pageSize){
return goodsOrderService.getOrders(pageIndex,pageSize);
}
}
GoodsOrderService
package com.lyc.goodsOrder.service;
import com.lyc.goodsOrder.component.AccountClient;
import com.lyc.goodsOrder.component.ProductClient;
import com.lyc.goodsOrder.dao.GoodsOrderRepository;
import com.lyc.goodsOrder.entity.Account;
import com.lyc.goodsOrder.entity.Orders;
import com.lyc.goodsOrder.entity.Product;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/** * @author: zhangzhenyi * @date: 2019/3/16 16:07 * @description: GoodsOrder服务类 **/
@Service
public class GoodsOrderService {
@Autowired
private GoodsOrderRepository goodsOrderRepository;
@Autowired
private ProductClient productClient;
@Autowired
private AccountClient accountClient;
@Autowired
RestTemplate restTemplate;
/** * 添加订单 * @param productCode 商品条目编号 * @return */
public Orders addOrder(Long accountId,String productCode){
Orders orders = Orders.builder()
.id(1L)
.build();
Product product = getProduct(productCode);
if(null == product){
return orders;
}
Account account = getAccount(accountId);
if(null == account){
return orders;
}
orders.setAccountId(account.getId());
orders.setName(product.getName());
orders.setCreateTime(new Date());
orders.setItem(product.getName());
orders.setProductCode(product.getProductCode());
goodsOrderRepository.save(orders);
return orders;
}
/** * 给账户微服务的调用增加服务熔断机制 * @param accountId * @return */
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000") // 设置超时时间为3秒(默认是1秒)
})
private Account getAccount(Long accountId){
return accountClient.getAccount(accountId);
}
/** * 给产品微服务的调用增加服务熔断机制 * @param productCode * @return */
@HystrixCommand
private Product getProduct(String productCode){
return productClient.getProduct(productCode);
}
/** * 获取第一页10条订单信息 * @param pageIndex 当前页索引值 * @param pageSize 每页的信息条数 * @return */
@HystrixCommand(
fallbackMethod = "getOrdersFallback" // 失败时执行的方法
,threadPoolKey = "orderThreadPool"
,threadPoolProperties = {
@HystrixProperty(name = "coreSize",value = "30")
,@HystrixProperty(name = "maxQueueSize",value = "10")
}
,commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
}
)
public List<Orders> getOrders(int pageIndex,int pageSize){
return goodsOrderRepository.findAll(new PageRequest(pageIndex - 1,pageSize)).getContent();
}
/** * Hystrix 失败回退方法(其中包含超时等情况) * @param pageIndex * @param pageSize * @return */
private List<Orders> getOrdersFallback(int pageIndex,int pageSize){
List<Orders> fallbackList = new ArrayList<>();
Orders orders = Orders.builder()
.id(1L)
.accountId(0L)
.item("Order list is not available")
.createTime(new Date())
.build();
fallbackList.add(orders);
return fallbackList;
}
}
GoodsOrderRepository
package com.lyc.goodsOrder.dao;
import com.lyc.goodsOrder.entity.Orders;
import org.springframework.data.jpa.repository.JpaRepository;
/** * @author: zhangzhenyi * @date: 2019/3/16 16:08 * @description: GoodsOrder Repository **/
public interface GoodsOrderRepository extends JpaRepository<Orders,Long> {
}
Account
package com.lyc.goodsOrder.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/** * @author: zhangzhenyi * @date: 2019/3/17 15:23 * @description: Account 实体类 **/
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Account {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
}
Orders
package com.lyc.goodsOrder.entity;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Date;
/** * @author: zhangzhenyi * @date: 2019/3/16 16:10 * @description: 订单实体类 * 这里需要注意的是,实体类的名称不能为Order,因为此时Orders已经相当于内部的一个关键字, * 所以说应该是避开,避开之后就能够正常使用了 **/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Orders {
@Id
@GeneratedValue
private Long id;
// 用户id
private Long accountId;
// 订单名称
private String name;
// 产品编号
private String productCode;
private String item;
// 创建时间
private Date createTime;
}
Product
package com.lyc.goodsOrder.entity;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/** * @author: zhangzhenyi * @date: 2019/3/15 20:27 * @description: 商品条目实体类 **/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String productCode;
public Product(String name, String productCode) {
this.name = name;
this.productCode = productCode;
}
}
AccountClient
package com.lyc.goodsOrder.component;
import com.lyc.goodsOrder.entity.Account;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/** * @author: zhangzhenyi * @date: 2019/3/17 15:25 * @description: AccountClient 组件 **/
@Slf4j
@Component
public class AccountClient {
@Autowired
RestTemplate restTemplate;
/** * 发起远程调用账户微服务 * @param accountId 账户id * @return */
public Account getAccount(Long accountId){
String url = "http://account-service/v1/account/{accountId}";
log.info(url);
ResponseEntity<Account> result = restTemplate.exchange(url,HttpMethod.GET,null,Account.class,accountId);
return result.getBody();
}
}
ProductClient
package com.lyc.goodsOrder.component;
import com.lyc.goodsOrder.entity.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/** * @author: zhangzhenyi * @date: 2019/3/16 16:01 * @description: Product Client **/
@Slf4j
@Component
public class ProductClient {
@Autowired
RestTemplate restTemplate;
/** * 发起远程调用产品微服务 * @param productCode 产品编号 * @return */
public Product getProduct(String productCode){
/*String url = "http://127.0.0.1:8081/v1/products/{productCode}";*/ // 虽然直接在浏览器中使用该方法可以使用,但是此处却不可使,因为Ribbon负载均衡不能使
String url = "http://product-service/v1/products/{productCode}";
log.info(url);
ResponseEntity<Product> result = restTemplate.exchange(url,HttpMethod.GET,null,Product.class,productCode);
return result.getBody();
}
}
application.yml
# 服务名称
spring:
application:
name: order-service
# spring连接数据库驱动
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_data?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: zhangzhenyi
# jpa自动创建不存在的数据表
jpa:
show-sql: true
hibernate:
ddl-auto: update
use-new-id-generator-mappings: true
jackson:
serialization:
indent_output: false
# 服务端口号
server:
port: 8085
eureka:
instance:
# 指明使用IP而不是服务名称来注册自身服务。因为Eureka默认是使用域名进行服务注册和解析
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
pom.xml中的部分依赖
org.springframework.cloud
spring-cloud-starter-eureka
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.cloud
spring-cloud-starter-ribbon
org.springframework.cloud
spring-cloud-starter-hystrix
需要注意的是,上面的代码中含有hystrix
功能。
负载均衡的调用是通过AccountClient中的RestTemplate来实现的,在RestTemplate中,通过exchange()方法发起的对Product服务的远程调用,但是这里需要注意的是此时的RestTemplate已经具有了负载均衡的功能,因为我们在GoodsOrderApplication中创建RestTemplate时就已经添加了@LoadBalanced
注解。
通过访问下面的路径
http://127.0.0.1:8083/v1/orders/1001/21
我们就可以在控制台打印的日志信息中清楚的看到Product服务被调用时所打印的日志信息了。