[TOCM]
[TOC]
源码地址:https://github.com/IsResultXaL/springcloud
通过上一节内容介绍与实践,我们已经搭建起微服务架构中的核心组件
服务注册中心
服务提供者
服务消费者
SpringCloud(二):服务发现与消费
###服务容错保护:Spring Cloud Hystrix
在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与订阅的方式相互依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或者依赖服务自身的问题导致调用故障或延迟,而调用失败又会造成用户刷新页面并再次尝试调用,再加上其它服务调用,从而增加了服务器的负载,导致服务瘫痪,最终甚至会导致整个服务“雪崩”。
为了解决这样的问题,产生了断路器等一系列的服务保护机制。
Netflix为解决这个问题根据断路器模式创建了一个名为Hystrix的库。“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
###没有Hystrix的服务效果
在开始使用Spring Cloud Hystrix实现断路器之前,我们先用之前实现的微服务架构看看没有断路器的效果
eureka-server工程:服务注册中心启动 端口为1111
eureka-client工程:启动两个服务单元,端口分别为1112和1113
ribbon-consume工程:使用Ribbon实现的服务消费者,端口为1115
在未加入断路器之前,关闭1112的实例,发送GET请求到 http://localhost:1115/ribbon ,当轮询到1112这个端口,会出现http status为500的错误,这个错误代表内部服务器错误。
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Tue Nov 28 16:08:06 CST 2017 There was an unexpected error (type=Internal Server Error, status=500). I/O error on GET request for "http://EUREKA-CLIENT/hello": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)
###引入Hystrix的服务效果
在ribbon-consumer工程的pom.xml的dependency节点中引入spring-cloud-starter-hystrix依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency>
在ribbon-consumer工程的应用类RibbonConsumerApplication中使用@EnableCircuitBreaker注解开启断路器功能
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableCircuitBreaker //开启断路器功能 @EnableDiscoveryClient //表明该应用是Eureka客户端 @SpringBootApplication public class RibbonConsumerApplication { @Bean @LoadBalanced //注解开启客户端负载均衡 RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(RibbonConsumerApplication.class, args); } }
改造服务消费方式,新增ConsumerService类,注入RestTemplate实例。然后,将在ConsumerController中对RestTemplate的使用迁移到helloService方法中,最后,在helloService方法中增加@HystrixCommand注解来指定回调方法
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @Service public class ConsumerService { @Resource RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "helloFallback") public String helloService() { return restTemplate.getForObject("http://EUREKA-CLIENT/hello", String.class); } public String helloFallback() { return "error"; } }
修改ConsumerController
import com.caogen.ribbonconsumer.service.ConsumerService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class ConsumerController { @Resource ConsumerService consumerService; @GetMapping(value = "/ribbon") public String helloConsumer() { return consumerService.helloService(); } }
eureka-server工程:服务注册中心启动 端口为1111
eureka-client工程:启动两个服务单元,端口分别为1112和1113
ribbon-consume工程:使用Ribbon实现的服务消费者,端口为1115
关闭1112的实例,发送GET请求到 http://localhost:1115/ribbon ,当轮询到1112这个端口,会发现输出内容为error,不再是之前的http status 500错误了,说明Hystrix的服务回调生效。
error
除了断开具体的服务实例来模拟某个节点无法访问的情况下,我们还可以模拟一下服务阻塞的情况。我们对eureka-client工程的HelloController进行改造
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Random; @RestController public class HelloController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Resource private DiscoveryClient client; @RequestMapping("/hello") public String sayHello() { ServiceInstance instance = client.getLocalServiceInstance(); //让处理线程等待几秒,由于Hystrix默认超时时间为2000毫秒, // 所以这里采用0至3000的随机数让处理过程有一定概率发生超时来触发断路器 try { int sleepTime = new Random().nextInt(3000); logger.info("sleepTime:" + sleepTime); Thread.sleep(sleepTime); }catch (Exception e) { logger.error("sayHello: " + e); e.printStackTrace(); } return "Hello,World! port:" + instance.getPort(); } }
通过Thread.sleep()函数可让/hello接口的处理线程不是马上返回内容,而是在阻塞0到3秒之后蔡返回内容。 由于Hystrix默认超时时间为2000毫秒,所以这里采用0至3000的随机数让处理过程有一定概率发生超时来触发断路器。
重新启动服务,连续访问http://localhost:1115/ribbon几次 ,会发现当日志打印出来的sleepTime大于2000的时候,页面就会返回error,即服务消费者因调用的服务超时从而触发断路器。