为什么需要服务容错
在微服务的架构中,服务间通常会形成相互依赖的关系,比如现在有三个微服务节点:A,B和C,B为A的消费者,C为B的消费者。假如由于网络波动或者A服务自身故障,导致B调用A服务的线程被挂起进入长时间的等待。在高并发的情况下可能导致B的资源被耗竭随之崩溃,从而导致C服务也不可用。这种连环式的雪崩效应在微服务中较为常见,为了解决这个问题,服务熔断技术应运而出。熔断一词来自电路学,指的是电路在出现短路状况时,“断路器”能够及时地切断故障电路,避免电路过载发热引发火灾。
类似的,微服务架构中的断路器能够及时地发现故障服务,并向服务调用方返回错误响应,而不是长时间的等待。Spring Cloud Hystrix在Hystrix(又是一款由Netflix开发的开源软件,Github地址https://github.com/Netflix/Hystrix)的基础上进行了封装,提供了服务熔断,服务降级,线程隔离等功能,通过这些功能可以提供服务的容错率。
使用Hystrix
下面开始使用使用Spring Cloud Hystrix,在项目Ribbon-Consumer中引入Spring Cloud Hystrix依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在入口类上加入@EnableHystrix或者@EnableCircuitBreaker注解。这两个注解是等价的:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableCircuitBreaker
public @interface EnableHystrix {
}
入口类上总共包含了三个注解@EnableCircuitBreaker、@EnableDiscoveryClient和@SpringBootApplication,这三个注解的组合可以使用@SpringCloudApplication来代替,@SpringCloudApplication源码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
接着将UserController中的方法提取出来,创建一个UserService(为了简单起见,不再创建Service接口):
@Service("userService")
public class UserService {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
public User getUser(@PathVariable Long id) {
return restTemplate.getForObject("http://Server-Provider/user/{id}", User.class, id);
}
public List<User> getUsers() {
return this.restTemplate.getForObject("http://Server-Provider/user", List.class);
}
public String addUser() {
User user = new User(1L, "mrbird", "123456");
HttpStatus status = this.restTemplate.postForEntity("http://Server-Provider/user", user, null).getStatusCode();
if (status.is2xxSuccessful()) {
return "新增用户成功";
} else {
return "新增用户失败";
}
}
public void updateUser() {
User user = new User(1L, "mrbird", "123456");
this.restTemplate.put("http://Server-Provider/user", user);
}
public void deleteUser(@PathVariable Long id) {
this.restTemplate.delete("http://Server-Provider/user/{1}", id);
}
}
接着改造UserService的getUser方法:
@HystrixCommand(fallbackMethod = "getUserDefault")
public User getUser(@PathVariable Long id) {
return restTemplate.getForObject("http://Server-Provider/user/{id}", User.class, id);
}
public User getUserDefault(Long id) {
User user = new User();
user.setId(-1L);
user.setUsername("defaultUser");
user.setPassword("123456");
return user;
}
我们在getUser方法上加入了@HystrixCommand注解,注解的fallbackMethod属性指定了被调用的方法不可用时的回调方法(服务熔断时的回调处理逻辑,即服务降级),这里为getUserDefault方法(必须与getUser方法的参数及返回值类型一致)。
在UserController中调用UserService的getUser方法:
@RestController
public class TestController {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserService userService;
@GetMapping("user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
轮询到服务不可用时,触发了熔断机制,接口回调了fallbackMethod指定的方法。
我们也可以模拟服务超时的情况,可以在Eureka-Client提供的接口方法中设置线程等待,等待时间大于2000(Hystrix默认超时时间为2000 毫秒)即可触发调用方Ribbon-Consumer的服务熔断。
服务降级
上面TestController中的getUser中我们用@HystrixCommand注解指定了服务降级方法getUserDefault。如果getUserDefault方法也抛出异常,那么我们可以再次使用@HystrixCommand注解指定getUserDefault方法降级的方法,比如定义一个getUserDefault2方法:
@HystrixCommand(fallbackMethod = "getUserDefault2")
public User getUserDefault(Long id) {
String a = null;
// 测试服务降级
a.toString();
User user = new User();
user.setId(-1L);
user.setUsername("defaultUser");
user.setPassword("123456");
return user;
}
public User getUserDefault2(Long id) {
User user = new User();
user.setId(-2L);
user.setUsername("defaultUser2");
user.setPassword("123456");
return user;
}
异常处理
在使用@HystrixCommand注解标注的方法中,除了HystrixBadRequestException异常外,别的异常都会触发服务降级。假如我们想指定某个异常不触发服务降级,可以使用@HystrixCommand注解的ignoreExceptions属性进行忽略。如:
@HystrixCommand(fallbackMethod = "getUserDefault2", ignoreExceptions = {NullPointerException.class})
public User getUserDefault(Long id) {
String a = null;
// 测试服务降级
a.toString();
User user = new User();
user.setId(-1L);
user.setUsername("defaultUser");
user.setPassword("123456");
throw new HystrixBadRequestException()
return user;
}
此外,对于方法抛出的异常信息,我们可以在服务降级的方法中使用Throwable对象获取,如:
@HystrixCommand(fallbackMethod = "getUserDefault2")
public User getUserDefault(Long id, Throwable e) {
System.out.println(e.getMessage());
User user = new User();
user.setId(-2L);
user.setUsername("defaultUser2");
user.setPassword("123456");
return user;
}
命名与分组
通过指定@HystrixCommand注解的commandKey、groupKey以及threadPoolKey属性可以设置命令名称、分组以及线程池划分,如:
@HystrixCommand(fallbackMethod = "getUserDefault", commandKey = "getUserById", groupKey = "userGroup",
threadPoolKey = "getUserThread")
public User getUser(@PathVariable Long id) {
log.info("获取用户信息");
return restTemplate.getForObject("http://Server-Provider/user/{id}", User.class, id);
}
上面的配置指定了命令的名称为getUserById,组名为userGroup,线程池名称为getUserThread。
通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。默认情况下,Hystrix命令通过组名来划分线程池,即组名相同的命令放到同一个线程池里,如果通过threadPoolKey设置了线程池名称,则按照线程池名称划分。
当getUser方法被调用时,日志打印如下:
2018-06-06 15:32:55.945 INFO 16192 --- [getUserThread-1] com.example.demo.Service.UserService : 获取用户信息
可看到线程名称为getUserThread-1。
请求合并
请求合并就是将多个单个请求合并成一个请求,去调用服务提供者,从而降低服务提供者负载的,一种应对高并发的解决办法。
Hystrix中提供了一个@HystrixCollapser注解,该注解可以将处于一个很短的时间段(默认10 毫秒)内对同一依赖服务的多个请求进行整合并以批量方式发起请求。为了演示@HystrixCollapser注解的使用方法,我们改造下Eureka-Client(服务提供者)的UserController接口,提供一个批量处理的方法:
@RestController
@RequestMapping("user")
public class UserController {
private Logger log = LoggerFactory.getLogger(this.getClass());
@GetMapping("users")
public List<User> get(String ids) {
log.info("批量获取用户信息");
List<User> list = new ArrayList<>();
for (String id : ids.split(",")) {
list.add(new User(Long.valueOf(id), "user" + id, "123456"));
}
return list;
}
...
}
然后在Ribbon-Consumer的UserService里添加两个方法:
@HystrixCollapser(batchMethod = "findUserBatch", collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
public Future<User> findUser(Long id) {
log.info("获取单个用户信息");
return new AsyncResult<User>() {
@Override
public User invoke() {
return restTemplate.getForObject("http://Server-Provider/user/{id}", User.class, id);
}
};
}
@HystrixCommand
public List<User> findUserBatch(List<Long> ids) {
log.info("批量获取用户信息,ids: " + ids);
User[] users = restTemplate.getForObject("http://Server-Provider/user/users?ids={1}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(users);
}
@HystrixCollapser注解的batchMethod属性指定了批量处理的方法为下面定义的findUserBatch方法,timerDelayInMilliseconds的值为100(毫秒),意思是在100毫秒这个时间范围内的所有对findUser的调用,都将被合并为一个批量处理操作,进行批量处理操作的方法就是findUserBatch。
我们在TestController中添加一个测试方法:
@GetMapping("testRequestMerge")
public void testRequerstMerge() throws InterruptedException, ExecutionException {
Future<User> f1 = userService.findUser(1L);
Future<User> f2 = userService.findUser(2L);
Future<User> f3 = userService.findUser(3L);
f1.get();
f2.get();
f3.get();
Thread.sleep(200);
Future<User> f4 = userService.findUser(4L);
f4.get();
}
控制台的输出符合我们的预期,f1、f2和f3被合并成了一个请求。
而且可以看到,控制台并没有打印出findUser方法中的获取单个用户信息的日志,实际上findUser方法并不会被调用,所以上面的代码可以简化为:
@HystrixCollapser(batchMethod = "findUserBatch", collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
public Future<User> findUser(Long id) {
log.info("获取单个用户信息");
return null;
}
@HystrixCommand
public List<User> findUserBatch(List<Long> ids) {
log.info("批量获取用户信息,ids: " + ids);
User[] users = restTemplate.getForObject("http://Server-Provider/user/users?ids={1}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(users);
}
Hystrix属性
参考:https://mrbird.cc/Spring-Cloud-Hystrix-Circuit-Breaker.html