微服务实现负载均衡

微服务实现负载均衡

在微服务的开发中,会将一个大的系统拆分为多个微服务系统,而各个微服务系统之间需要相互协作才能完成业务需求。

每一个微服务系统可能存在多个节点,当一个微服务(服务消费者)调用另外一个微服务(服务提供者)时,服务提供者需要负载均衡算法提供一个节点进行响。

而负载均衡是分布式必须实施的方案例如,系统在某个时刻存在3万笔务请求,使用单点服务器就很可能出现超负载,导致服务器瘫痪,进而使得服务不可用。而使用3台服务节点后,通过负载均衡的算法使得每个节点能够比较平均地分摊请求这样每个点服务只是需要处理1笔请求,这样就可以分摊服务的压力,及时响应。

除此之外,在服务的过程中,可能出现某个节点故障的风险,通过均衡负载的算法就可以将故节点排除,使后续请求分散到其他可用节点上,这就体现了SpringCloud的高可用。

上面己经把产品和用户两个微服务注册到服务治理中心了。对于业务,则往往需要各个微服务之间相互地协助才能完成。例如,可能把产品交易信息放到产品服务中,而在交易时,有时需要根据用户的等级来决定某些商品的折扣,如白银会员是9折、黄金会员是8.5折、钻石会员是8折等。也就是说分布式系统在执行交易逻辑时,还需要使得产品微服务得到用户信息才可以决定产品的折扣,而用户的信息则是放置在用户微服务中的。

为了方便从用户微服务中获取用户信息,用户微服务会以REST风格提供一个请求URL这样对于产品微服务就可以通过阻ST请求获取用户服务。除了处理获取其他服务的数据外,这里还需要注意务节点之间的负载均衡。

毕竟一个微服务可以由多个节点提供服务。不过这些都不困难,因为SpringCloud提供了Rbbon和Feign组件来帮助我们完成这些功能。

Ribbon

SpringCloud己经屏蔽了一些底层的细节,它只需要一个简单的@LoadBalance注解就以提供负载均衡的算法。这十分合SpringBoot的原则,提供默认的实现方式,减少开发的情况下,它会提供轮询的负载均衡算法。

我们就在用户的微服务上加一个实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class UserPo implements Serializable {
private static final long serialVersionUID = -2535737897308758054L;
private Long id;
private String userName;
// 1-银级会员,2-黄金会员。3-钻石会员
private int level;
private String note;


public Long getId() {
return id;
}

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

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
}

public String getNote() {
return note;
}

public void setNote(String note) {
this.note = note;
}
}

然后新建一个控制层,实现获取用户信息的服务:

实施

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class UserController {
// 日志
private Logger log = Logger.getLogger(this.getClass());

// 服务发现客户端
@Autowired
private DiscoveryClient discoveryClient = null;

// 获取用户信息
@GetMapping("/user/{id}")
public UserPo getUserPo(@PathVariable("id") Long id) {
ServiceInstance service = discoveryClient.getInstances("USER").get(0);
log.info("【" + service.getServiceId() + "】:" + service.getHost() + ":" + service.getPort());
UserPo user = new UserPo();
user.setId(id);
int level = (int) (id % 3 + 1);
user.setLevel(level);
user.setUserName("user_name_" + id);
user.setNote("note_" + id);
System.out.println("port=9002");
return user;
}
}

discoveryClient是spring自己创建的。他会在方法中打印出用户的id和服务器主机的端口。这样有利于后续监控和对负载均衡的研究,当然我们也在后面的打印出了端口,以便于在命令行查看。

接着,就是在产品中加入依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

然后在主文件中设置restTemplate的初始化。让其实现自动的多节点负载均衡。

1
2
3
4
5
6
// 初始化RestTemplate
@LoadBalanced // 多节点负载均衡
@Bean(name = "restTemplate")
public RestTemplate initRestTemplate() {
return new RestTemplate();
}

这个注解的作用是让其实现负载均衡,它会自动分配给用户的微服务。

接着我们再产品端,调用用户的服务来测试。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/product")
public class ProductController {

// 注入RestTemplate
@Autowired
private RestTemplate restTemplate = null;

@GetMapping("/ribbon")
public UserPo testRibbon() {
UserPo user = null;
// 循环10次,然后可以看到各个用户微服务后台的日志打印
// for (int i = 0; i < 10; i++) {
// // 注意这里直接使用了USER这个服务ID,代表用户微服务系统
// // 该ID通过属性spring.application.name来指定
// user = restTemplate.getForObject("http://USER/user/" + (i + 1), UserPo.class);
// }
user=restTemplate.getForObject("http://USER/user/" +1, UserPo.class);
return user;
}

}

接着我们给打开网址http://localhost:9001/product/ribbon。

然后在我们的控制台中,看到我们端点的运行,然后我们会发现,端点的服务是交替着执行的。这样就实现了我们最初步的负载均衡。

但是调用RestTemplate的方法。显示有些复杂,因为这些参数的组装和结果要返回等操作,其实并没有必要显示的表达出来。于是乎,SpringCloud还引入了Feign。

Feign

对于REST风格的调用,如果使用RestTemplate会比较烦琐,可读性不高。为了简化多次调用的复杂度,SpringCloud提供了接口式的声明服务调用编程,它就是Feigna。

通过它请求其他做服务时,就如同调度本地服务的Java接口一样,从而在多次调用的情况下可以简化开发者的编程,提高代码的可读性。

准备

首先依然是引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

然后就在走项目中加入注解@EnableFeignClients, 启动Feign。

重点逻辑

接着就是到了。这个逻辑最为关键的地方,他首先需要定一个接口,然后再用这个接口,去调用一个服务。

这服务就来自于用户的服务。定义该微服务的来源,就能够使用该微服务的功能了。而功能的具体实现。并不需要在消费者端完成,而是在用户端就准备好了。

首先需要首先需要定义好一个接口:

1
2
3
4
5
6
7
8
// 指定服务ID(Service ID)
@FeignClient("user")
public interface UserService {
// 指定通过HTTP的GET方法请求路径
@GetMapping("/user/{id}")
// 这里会采用Spring MVC的注解配置
public UserPo getUser(@PathVariable("id") Long id);
}

首先是@FeignClient(”user”),它代表这是一个Feign客户端,而配置的“user”是一个服务的ID(ServiceID),它指向了用户微服务,这样Feign就会知道向用户微服务请求,并会实现负载均衡。

这里的注解@GetMapping代表启用HTTP的GET请求用户微服务,而方法中的注解@PathVariable代表从URL中获取参数。

然后在需要的地方调用这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注入Feign接口
@Autowired
private UserService userService = null;

// 测试
@GetMapping("/feign")
public UserPo testFeign() {
UserPo user = null;
// 循环10次
for (int i = 0; i < 10; i++) {
Long id = (long) (i + 1);
user = userService.getUser(id);
}
return user;
}

里先是注入UserService接口对象,然后通过循环10次,调用声明的接口方法,这样也可以完成对用户微服务的调用。在该请求方法后,你也可以看到两个用户微服务后台均会打出相关的日志,因为Feign提供了负载均衡算法。

与Ribbon相比,Feign屏蔽掉了RestTemplate的使用,提供了接口声明式的调用,使得程序可读性更高,同时在多次调用中更为方便。

多次测试

之后的逻辑也与其类似,我们只需要仿照着以上逻辑的方式去写其他内容就好了。

比如,我们就在用户的服务端继续增加需要的服务功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// 新增用户,POST请求,且以请求体(body)形式传递
@PostMapping("/insert")
public Map<String, Object> insertUser(@RequestBody UserPo user) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("message", "插入用户信息【" +user.getUserName() + "】成功");
System.out.println("9002");
return map;
}

// 修改用户名,POST请求,其中用户编号使用请求头的形式传递
@PostMapping("/update/{userName}")
public Map<String, Object> updateUsername(
@PathVariable("userName") String userName,
@RequestHeader("id") Long id) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("message", "更新用户【" +id +"】名称【" +userName + "】成功");
return map;
}

然后我们继续回到产品端,将以上方法的名字和返回类型以及需要的参数,写到产品微服务的接口当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
// POST方法请求用户微服务
@PostMapping("/insert")
public Map<String, Object> addUser(
// 请求体参数
@RequestBody UserPo user);

// POST方法请求用户微服务
@PostMapping("/update/{userName}")
public Map<String, Object> updateName(
// URL参数
@PathVariable("userName") String userName,
// 请求头参数
@RequestHeader("id") Long id);

然后我们继续在产品端的控制层在对他们测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

@GetMapping("/feign2")
public Map<String, Object> testFeign2() {
Map<String, Object> result = null;
UserPo user = null;
for (int i = 1; i <= 10; i++) {
Long id = (long) i;
user = new UserPo();
user.setId(id);
int level = i % 3 + 1;
user.setUserName("user_name_" + id);
user.setLevel(level);
user.setNote("note_" + i);
result = userService.addUser(user);
}
return result;
}

@GetMapping("/feign3")
public Map<String, Object> testFeign3() {
Map<String, Object> result = null;
for (int i = 0; i < 10; i++) {
Long id = (long) (i + 1);
String userName = "user_name_" + id;
result = userService.updateName(userName, id);
}
return result;
}

可以看到的是我们进去只需要调用接口里面的方法就可以了,具体的实现并不用在服务端完成,这样极大地解开了各个模块之间的耦合度,同时也方便于服务的治理。