Spring Cloud学习笔记(三)——服务容错Hystrix

为什么需要服务容错

在微服务的架构中,服务间通常会形成相互依赖的关系,比如现在有三个微服务节点: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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值