定义:
服务就是在实现某个业务逻辑的模块或者应用,微服务就是将原有的服务在以更小的粒度去拆分成一个服务。
微服务拆分的原则:将应用拆分成微服务应当遵循单一职责,也就是将服务中紧密相关的业务放在一起,无关的业务分离出去,例如:支付和订单可以做成一个服务,登录注册可以作为一个服务。服务之间应该使用轻量级的平台无关性的通信方式,例如:http、rpc。每个服务运行在自己的进程中不会相互干扰。每个服务拥有自己的独立存储系统
优点:
微服务从业务上对系统进行了垂直拆分,将各个业务分离成一个个小的应用,实现了服务的组键化去中心化,解决了以前一体化应用和SOA架构服务过重的问题,分散服务治理和分散数据管理,使其实现了业务的职责单一化,减小了业务的之间的耦合度,提高系统的可用性,微服务架构一般还要拥有容错设计:单个服务出问题时对其他的业务影响不大,或者为服务引入的熔断、降级、限流使其出现问题的概率大幅度降低
缺点:
数据一致性降低,增加了运维的难度和复杂度,系统的架构变得更加复杂
注册中心
Eureka
定义:
eureka是springcloud netflix的核心子模块,其主要包含了注册中心eureka server和调用注册中心的eureka client,eureka server用于提供注册服务,存储所有可用的服务节点的信息。eureka server支持高可用,可以应对多种场景的故障,eureka server以集群的方式部署时,可以在大面积的节点服务出现故障时转入自我保护或者故障转移(其他eureka server还活着),因为集群中可用的eureka server会进行异步的相互复制,eureka server是没有主备之分,各个eureka server都可以平等的提供服务,所以他可以允许分片故障期间继续提高服务,不像zookeeper必须得等待选举,这是eureka高可用的主要原因,而eureka client可以通过心跳的方式向eureka server续约服务,并且周期性的更新服务列表将其缓存,所以服务之间的调用不需要每次都向eureka server请求,这样有效提高了其性能
eureka client对服务的消费是由ribbon实现的,通过从eureka server获取的服务列表,利用ping的方式进行可用服务的监测,然后选择可用服务默认进行轮询消费,ribbon的负载均衡是基于eureka 的服务发现实现的
(服务提供方通过向eureka注册服务信息,服务消费方可以从eureka中获取服务提供方的信息,从而与相应的服务提供方通信调用服务)
eureka的特性:
- 服务注册:
eureka client在第一次心跳时会向eureka server注册,将会主机名、端口、健康指标等信息记录到eureka server中。第一次启动时eureka server会对各个eureka client进行三次心跳,一次心跳是十秒,所以启动时需要等三十秒 - 服务续约
eureka client会通过发送心跳进行续约,默认是每三十秒发送一次心跳,90秒内未收到续约,就会将服务剔除 - 服务同步
在高可用的eureka配置下,注册中会在多个不同的节点上,不同节点的注册中心会相互注册,一份eureka client往一个注册中心注册时,注册中心会将其注册信息相互转发复制,所以集群任意一份注册信息会被多个注册中心维护,这就实现了注册中心的高可用。同一个eureka client的包可以在不同的节点启动,使得同一个服务提供者可以有多个节点(在注册中心界面上表现为同一个服务名下带有多个服务节点地址),实现了eureka client的高可用。 - 服务发现
eureka client并不是每次使用时才会从eureka server上获取信息,而是在运行中时会不断的向eureka server注册列表获取信息进行缓存,eureka client每隔三十秒更新缓存消息
注册中心zookeeper与eureka
CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。zookeeper与eureka一样是注册中心,Zookeeper保证的是CP, 而Eureka则是AP,即zookeeper注重的是节点数据的一致性和分区的容错性,而eureka注重的是节点的可用性和分区的容错性
eureka的自我保护模式
它的原理是,当Eureka Server节点在短时间内丢失过多的客户端时(可能发送了网络故障),那么这个节点将进入自我保护模式,不再注销任何微服务,当网络故障回复后,该节点会自动退出自我保护模式。
这篇博客介绍的不错
https://www.cnblogs.com/snowjeblog/p/8821325.html
配置
server端
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
eureka:
instance:
//eureka 实例的hostname,可以是hostname,也可以自定义配置hostname
hostname: localhost
//该实例的IP地址
ip-address: 127.0.0.1
//该实例注册到服务中心的唯一ID
instance-id: ${eureka.instance.ip-address}:${server.port}
//注册时客户端是否使用自己的IP而不是主机名,默认是false
prefer-ip-address:false
client:
//是否要把当前的eureka server注册到自己
register-with-eureka: false
//从注册中心获得检索服务实例,server没有必要,直接false即可
fetch-registry: false
service-url:
//eureka高可用可以这么玩defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
//Eureka Server自我保护机制配置,默认为true开启。真实环境可以开启,开发环境关掉
enable-self-preservation: false
spring:
application:
name: eureka-server
@EnableEurekaServer
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
client端
引入依赖,这个配置自动集成了Ribbon,使用enreka会自动的将Ribbon应用进来
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/
spring:
application:
name: eureka-client01
@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class ClientApplication {
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
}
负载均衡
Ribbon:
定义:
Ribbon是基于http和tcp的负载均衡组件,eureka已经自动集成了ribbon,利用ribbon实现了服务消费,Ribbon通过配置的服务列表进行轮询访问,但与eureka集成使用时,服务列表会被eureka的服务列表所重写,拓展成从注册中心获取服务列表。同时将自身的Iping功能替换成eureka的NIWSDiscoveryPing功能,由eureka去确认哪些服务可用,所以它拥有以下几项核心功能:
- 服务发现:Ribbon通过RibbonServeList获取所有的服务列表,与eureka集成时服务列表会被eureka的服务列表重写,Ribbon的服务发现有两种方式,一是从Eureka上将注册列表拉下来,二是用户自定义在Ribbon有一个核心模块,用户可以在这上面自定义服务列表以此达到发现服务的目的。
- 服务选择规则:服务选择规则也是就是负载均衡的规则,Ribbon通过IRule选择的算法进行负载均衡,Ribbon服务选择规则有两种方式,一是Ribbon已经预定好的规则,二是可以自定义规则。IRule默认的算法是轮询(RoundRobinRule)
- 服务监听:服务监听主要是判断注册列表中的服务状态,判断哪些服务可用,哪些服务不可用,主要使用了serverlistFilter对不可用服务进行过滤
Ribbon和Eureka的整合
eureka的依赖本身就与Ribbon进行了集成,在注入RestTemplate时上面加上@LoadBalanced这样RestTemplate就拥有了负载均衡的能力
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Service
public class ConsumerServiceImpl implements ConsumerServiceAPI{
@Autowired
private RestTemplate restTemplate;
@Override
public String sayHello(String message) {
String uri = "/provider/sayhello?message="+message;
// http://localhost:7101/provider/sayhello?message=hello
String url = "http://hello-service-provider"+uri;
// invoker provider test
String result = restTemplate.getForObject(url, String.class);
return result;
}
}
IRule的算法选择
@Bean
public IRule iRule(){
return new RoundRobinRule();
// return new MyRule();
}
IPing实现了Ribbon的保证服务可用的手段
@Bean
public IPing iPing(){
// return new PingUrl(false,"/abc");
return new NIWSDiscoveryPing();
}
在服务消费端的全局配置,service-user是其中一个服务应用名称:
高可用
前置知识:扇出和雪崩效应
扇出
想象一下扇子打开的过程,从扇的手柄然后扩展到扇子的外边从一个点逐渐扩大的过程,例如下图的服务调用树,根节点订单服务需要调用库存服务和交易计费服务,然后交易计费服务和库存服务又需要调用其他的服务,一直向远处扩展形成一个类似树形结构的图也像一把折叠扇一样,这个就是扇出雪崩效应
雪崩效应简单的说就是级联故障,就像上图的扇出一样,由扇子外部的一个点的故障引起了依赖这个点的所有服务都一起发生了故障,例如上图的代金卷服务故障,引起了依赖它的交易计费服务故障,而后引起了订单依赖交易计费服务的故障,这一系列的故障由一个点造成Hystrix:
定义:
Hystrix是用于处理延迟与容错的开源库,其核心是隔离和熔断机制,Hystrix最主要是处理级联故障,利用降级、限流和快速失败,解决扇出导致的雪崩效应,提高服务系统的可用性,也就是所谓的系统弹性。
Hystrix的主要功能:
- 服务熔断:发现服务不可用时,触发降级,快速的找到一个失败的方法,让系统知道某个服务出现故障不用继续等待这个服务的响应。
- 服务隔离:一个节点出现故障时,让其单独失败就好,不会引起雪崩效应
- 限流:限制服务请求的最大个数,超过后就会触发降级
- 降级:快速失败的方法
- 请求缓存:缓存服务的请求和响应
- 快速失败:快速失败可以让系统不用在等待某个服务的响应,是降级的具体实现
- 单体和集群监控:监控服务的状态,如是否限流,是否熔断,是否降级,服务是否超时或者失败
断路器工作流程:
Hystrix整体架构
Hystrix的整体流程和架构如下图所示,
1、执行Hystrix服务就是执行HystrixCommand或者是HystrixObservableCommand的相应方法
2、查看Hystrix是否启用缓存,是则将查看请求缓存中是否存在本次的请求,有则直接返回请求的响应,而不需要继续进行Hystrix的处理
3、查看是否开启了熔断,如果开启了熔断,则会触发降级,调用了降级方法,降级方法是我们自己实现的方法
4、如果没有开启熔断,则会继续判断,判断信号量是否上限或者是线程池的最大线程数是否已满,如果是则拒绝,触发降级
Hystrix有两种命令模式:HystrixCommand和HystrixObservableCommand。HystrixCommand和HystrixObservableCommand启动方法的入口分布是图上的excute()(同步执行),queue()(异步执行),observer()(同步执行),toObservable()(异步执行),执行这些方法后服务的请求会被封装在Hystrix的run方法中进行调用
⑴ 启动HystrixCommand,执行入口方法
⑵ 请求缓存,并请求合并
⑶ 熔断是否已经开启
⑷ 限流有没有触发
⑸ 执行业务代码是否失败,业务代码执行是否超时
⑹ 所有的超时和失败,或者是熔断开启期间和限流触发都会进行fallback方法进行降级
触发降级机制:
- 服务发生HystrixBadRequestException以外的异常
- 服务运行超时或者熔断处于开启状态
- 线程池或者信号量已满
触发熔断机制:
以下是触发熔断的核心指标:当在时间窗内请求后端服务失败数量超过一定比例(默认50%可以使用circuitBreaker.errorThresholdPercentage设置比例)断路器会切换到开路状态(Open),需要注意的是时间窗内请求数量要超过一定值才进行失败数量的统计(默认20个),假如默认值下只有19个请求并且全部都失败了也不会开启熔断(可以通过circuitBreaker.requestVolumeThreshold设置请求数量)
. 断路器开启后所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒,可以通过circuitBreaker.sleepWindowInMilliseconds设置熔断休眠时间),自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix根据规则发现端服务不可用时, Hystrix会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.
Hystrix的隔离手段:
信号量隔离和线程隔离
- 两个服务应用之间的调用推荐使用线程隔离,原因是线程隔离是重量级隔离,比较信号隔离相对更安全,但是量级也重
- 一个服务应用内接口调用使用信号量隔离
Hystrix参数
Hystrix有5大类参数,分别是隔离策略、断路器参数、度量参数,其他参数、线程池参数,这里比较重要的是隔离策略、断路器参数、线程池参数
隔离策略:以execution开头断路器参数:以circuitBreaker开头
线程池参数 threadPool:
commandKey:用来标识一个 Hystrix 命令,默认会取被注解的方法名。需要注意:Hystrix 里同一个键的唯一标识并不包括 groupKey,建议取一个独一二无的名字,防止多个方法之间因为键重复而互相影响。
groupKey:一组 Hystrix 命令的集合, 用来统计、报告,默认取类名,可不配置。
threadPoolKey:用来标识一个线程池,如果没设置的话会取 groupKey,很多情况下都是同一个类内的方法在共用同一个线程池,如果两个共用同一线程池的方法上配置了同样的属性,在第一个方法被执行后线程池的属性就固定了,所以属性会以第一个被执行的方法上的配置为准。
服务提供方降级容错的演示:
利用@HystrixCommand可以为服务提供方实现降级容错的方法
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
开启注解@EnableHystrix
开启监控@EnableHystrixDashboard
@EnableHystrix
@EnableHystrixDashboard
@EnableDiscoveryClient
@SpringBootApplication
public class BackendCinemaApplication {
public static void main(String[] args) {
SpringApplication.run(BackendCinemaApplication.class, args);
}
}
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://EduCloudEurekaServerB:8762/eureka
服务降级
第一种触发降级的方法
@GetMapping("/hystrix/{num}")
@HystrixCommand(fallbackMethod = "fallbackMethod",
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value= "500"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "1"),
@HystrixProperty(name = "maxQueueSize", value = "10"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "1000"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "8"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1500")
})
public String test4(@PathVariable("num") Integer num) throws InterruptedException {
Thread.sleep(num);
List<String> services = discoveryClient.getServices();
return "服务消费:"+services+",端口号:"+serviceInfoUtil.getPort();
}
public String fallbackMethod(Integer num){
return "超时时间:"+num+" 发生降级";
}
全局降级方法
@RestController
@RequestMapping("/client01")
@DefaultProperties(defaultFallback = "fallbackMethod")
public class Controller {
@GetMapping("/hystrix/{num}")
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value= "500"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "1"),
@HystrixProperty(name = "maxQueueSize", value = "10"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "1000"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "8"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1500")
})
public String test4(@PathVariable("num") Integer num) throws InterruptedException {
Thread.sleep(num);
List<String> services = discoveryClient.getServices();
return "服务消费:"+services+",端口号:"+serviceInfoUtil.getPort();
}
public String fallbackMethod(Integer num){
return "超时时间:"+num+" 发生降级";
}
}
@HystrixCommand的参数
超时设置
//时间超过3秒的请求将会触发降级
@HystrixCommand(fallbackMethod = "fallbackMethod",commandProperties ={
@HystrixProperty(name=“execution.isolation.thread.timeoutInMilliseconds” ,value=“3000”)
} )
断路器设置
//这里开启了熔断器,这里的配置表示在一个窗口滚动时间内,如果请求了10次,有超过50%的请求失败了,
//就会触发熔断,熔断后服务会进入默认的降级接口,经过了睡眠时间1000毫秒后,熔断结束服务恢复
@HystrixCommand(fallbackMethod = "fallbackMethod",commandProperties ={
@HystrixProperty(name="circuitBreaker.enabled" ,value="true")
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10")
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds" ,value="10000")
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage" ,value="50")
} )
@HystrixCommand的其他设置可以参考这里
https://blog.csdn.net/weixin_30908941/article/details/96440967
或者https://www.cnblogs.com/duan2/p/9302431.html
基于线程隔离的降级完整示例:
public BaseResponseVO fallbackMethod() throws CommonServiceException{
return BaseResponseVO.error("服务降级");
}
@HystrixCommand(fallbackMethod = "fallbackMethod",
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value= "1000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "1"),
@HystrixProperty(name = "maxQueueSize", value = "10"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "1000"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "8"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1500")
},ignoreExceptions = CommonServiceException.class)
@RequestMapping(value = "",method = RequestMethod.GET)
public BaseResponseVO describeCinemas(BasePageVO basePageVO) throws CommonServiceException {
IPage<DescribeCinemasRespVO> describeCinemasRespVOIPage = cinemaServiceAPI.describeCinemas(basePageVO.getNowPage(), basePageVO.getPageSize());
全局配置:
可用在yml文件中设置全局的hrstrix配置,其中半开状态默认5秒:
ribbon:
ReadTimeout: 30000
ConnectTimeout: 30000
feign:
hystrix:
# feign熔断器开关
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
#断路器的超时时间ms,默认1000
timeoutInMilliseconds: 30000
circuitBreaker:
#当在配置时间窗口内达到此数量的失败后,进行短路
requestVolumeThreshold: 20
#出错百分比阈值,当达到此阈值后,开始短路。默认50%)
errorThresholdPercentage: 50%
#短路多久以后开始尝试是否恢复,默认5s)-单位ms
sleepWindowInMilliseconds: 5000
hystrix的监控
访问http://localhost:9000/hystrix
Hystrix整合Feign使用
Feign可以给服务消费方提供降级容错的方案
yml配置
feign:
hystrix:
enabled: true
服务调用
Feign
定义:
Feign是一个Http的客户端,底层提供了多种Http请求的支持,包括了restTemplate等,默认整合了Ribbon,可以集成Hystrix,Feign很大程度上简化了http调用的方式,默认是通过Ribbon去的查找相应eureka
基本使用
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableDiscoveryClient
//开启Feign
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
@FeignClient(name = "hello-service-provider",//eureka应用的名称
path = "/provider"
/* url = "http://localhost:7101"*/ //path是Url地址的相同前缀,和方法中Mapping的地址相拼就是要访问的http接口地址
)
public interface ProviderApi {
//接口中的方法从参数和注解和需要发送http请求的方法是一样的即可
@RequestMapping(value = "/sayhello",method = RequestMethod.GET)
String invokerProviderController(@RequestParam("message") String message);
@RequestLine("GET /sayhello?message={message}")
String invokerProviderController2(@Param("message") String message);
@RequestMapping(value = "/{providerId}/sayhello",method = RequestMethod.POST)
String providerPost(
@RequestHeader("author") String author,
@PathVariable("providerId")String providerId,
@RequestBody UserModel userModel);
}
调用时就只需要注入相应的接口,然后进行普通的方法调用即可,就如同使用普通的服务层方法一样
@Slf4j
@RestController
public class ConsumerController {
@Autowired
private ConsumerServiceAPI serviceAPI;
@Resource
private ProviderApi providerApi;//注入相应的接口
@RequestMapping(value = "/sayhello/feign")
public String sayHelloFeign(String message){
System.out.println("feign message="+message);
return providerApi.invokerProviderController(message);//调用即可发送Http请求
}
整合Ribbon
Feign默认整合Ribbon,Ribbon的配置,这里配置负载方式为轮询
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
/**
* @Description: 负载均衡规则
*/
@Bean
public IRule iRule(){
return new RoundRobinRule();
// return new MyRule();
}
@Bean
public IPing iPing(){
// return new PingUrl(false,"/abc");
return new NIWSDiscoveryPing();
}
}
这样就完成了和Ribbon的整合
整合Hystrix:
在application.yml中配置
feign:
hystrix:
enabled: true
针对某个方法的Fallback方法,这里是针对ProviderApi接口中的invokerProviderController方法进行监视,
@Service
public class ProviderFallbackAPIImpl implements ProviderApi{
@Override
public String invokerProviderController(String message) {
return "invokerProviderController fallback message="+message;
}
}
相应的Feign的使用
@FeignClient(name = "hello-service-provider",
primary = true,
path = "/provider",
fallback = ProviderFallbackAPIImpl.class
)
public interface ProviderApi {
@RequestMapping(value = "/sayhello",method = RequestMethod.GET)
String invokerProviderController(@RequestParam("message") String message);
}
设置工厂的Fallback方法,这样可以打印异常
@Component
@Slf4j
public class ProviderApiFactoryFallback implements FallbackFactory<ProviderApi> {
@Override
public ProviderApi create(Throwable e) {
if (e!= null) {
log.error("发生错误【{}】",e.toString());
}
return new ProviderApi() {
@Override
public String invokerProviderController(String message) {
return "invokerProviderController FallbackFactory message="+message+" ,异常:"+e;
}
};
}
}
相应的Feign的使用
@FeignClient(name = "hello-service-provider",
primary = true,
path = "/provider",
fallbackFactory = ProviderApiFactoryFallback.class
)
public interface ProviderApi {
@RequestMapping(value = "/sayhello",method = RequestMethod.GET)
String invokerProviderController(@RequestParam("message") String message);
}
hystrix完整的全局yml配置示例
feign:
hystrix:
# feign熔断器开关
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
#断路器的超时时间ms,默认1000
timeoutInMilliseconds: 30000
circuitBreaker:
#当在配置时间窗口内达到此数量的失败后,进行短路
requestVolumeThreshold: 20
#出错百分比阈值,当达到此阈值后,开始短路。默认50%)
errorThresholdPercentage: 50%
#短路多久以后开始尝试是否恢复,默认5s)-单位ms
sleepWindowInMilliseconds: 500
网关服务
网关服务有很多种如Nginx+lua,zuul,网关的几个要素是高可用,高性能,稳定,高可扩展,安全这几点,
ZUUL
zuul提供了认证鉴权,限流,负载均衡,转发,过滤等主要的功能,zuul的核心是由servlet+filter组成的,zuul可以很轻松的整合Ribbon和hystrix
zuul可以通过有三类filter组成,分别是:前置路由、路由进行中、后置路由,我们可以针对这个三类filter进行一个我们的自定义的网关,
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
开启zuul的注解,使用@EnableZuulProxy注解开启Zuul的API网关服务功能。
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
路由规则
zuul的routes配置下path/url组合不支持负载均衡,可用通过zuul的routes配置下的path/serviceId负载均衡配置
spring:
application:
name: api-gateway
cloud:
config:
discovery:
enabled: true
service-id: CONFIG//配置中心
profile: dev
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
# /myProduct1/api/list -> /product/api/list
#网关名字随意定义
myProduct1:
path: /myProduct1/**
#product是其中的一个应用的名称
serviceId: product
# /myProduct2/api/list -> /product/api/list
myProducet2://随意定义
path: /myProduct2/**
serviceId: product
#简洁写法
# product: /myProduct1/**
#排除某些路由
ignored-patterns:
- /**/product/listForOrder
ribbon:
eureka:
enable: false #暂时关闭eureka对ribbon的支持
#负载均衡配置
#实现myProduct1的高可用的zuul网关配置
myProduct1:
ribbon:
listOfServers: http://localhost:8761,http://localhost:8762/
这样我们就可以使用类似这样的形式访问相应的应用的接口 /myProduct1/xxxx/xxxx访问应用的接口,这里 /myProduct1/是zuul配置的url前缀,/xxxx/xxxx会请求到配置好的serviceId名为product中一个实例中去,通过面向服务的路由配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的path与serviceId的映射组合,使得维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关服务可以自动化完成服务实例清单的维护,这里的serviceId是由用户手工命名的服务名称,配合ribbon.listOfServers 参数实现服务与实例的维护。由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略,于是这里还需要 Spring Cloud Ribbon的配合。由于在Spring Cloud Zuul中自带了对Ribbon的依赖,所以我们只需做一些配置即可
大部分的路由配置规则几乎都会采用服务名作为外部请求的前缀,比如下面的例子,其中 path 路径的前缀使用了 user-service,而对应的服务名称也是user-service。所以zuul在默认的情况下我们不用配置前缀也是可以的,我们为Spring Cloud Zuul构建的API网关服务引入Spring Cloud Eureka之后,它为Eureka中的每个服务都自动创建一个默认路由规则,这些默认规则的path会使用serviceId配置的服务名作为请求前缀,就如下面的例子那样。
zuul.routes.user-service.path=/user-service/ ***
zuul.routes.user-service.serviceId=user-service
由于zuul的依赖自动集成了ribbon和hystrix,本身zuul也是一个eureka的注册应用所以zuul会自动获取注册中心的注册列表,所以zuul可以识别应用名称,然后使用ribbon依据配置好的规则进行负载均衡,然后配置好hystrix进行容错降级,但是需要注意,当使用path与url的映射关系来配置路由规则的时候,对于路由转发的请求不会采用 HystrixCommand 来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
ribbon还可以这样配置
ribbon:
eureka:
enbaled: false #第二件事:禁止使用Eureka
ribbon-route:
ribbon:
NIWSServiceListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #Rinnon LB策略
listOfServers: localhost:7070,localhost:7071 #客户端服务地址 for Rinnon LB策略
整合hystrix
hystrix的一些参数
hystrix.command.default.execution.isolation.thread.timeoutIn Milliseconds:该参数可以用来设置 API 网关中路由转发请求的HystrixCommand 执行超时时间,单位为毫秒。当路由转发请求的命令执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常
ribbon.ConnectTimeout:该参数用来设置路由转发请求的时候,创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 配置值的时候,若出现路由请求出现连接超时,会自动进行重试路由请求
完整示例:
server:
port: 8080
spring:
application:
name: api-zuul
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
prefix: "/meetingfilm/" ## 统一前缀
routes:
meetingfilm-user:
path: /userapi/**
serviceId: user-service
retryable: true # 是否允许重试 , 饿汉模式
meetingfilm-cinema:
path: /cinemaapi/**
serviceId: cinema-service
retryable: true
meetingfilm-film:
path: /filmapi/**
serviceId: film-service
retryable: true
meetingfilm-hall:
path: /hallapi/**
serviceId: hall-service
retryable: true
# routes: ## 路由规则
# film-service: ## application.name -> 服务名称 service-id
# path: /filmapi/** # 匹配规则
# cinema-service:
# path: /cinemaapi/**
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 10000
Zuul的另外一个核心功能:请求过滤。Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。
下面的代码定义了一个简单的 Zuul 过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。
public class AccessFilter extends ZuulFilter {
private static Logger log=LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType(){
return "pre";
}
@Override
public int filterOrder(){
return 0;
}
@Override
public boolean shouldFilter(){
return true;
}
@Override
public Object run(){
RequestContext ctx=RequestContext.getCurrentContext();
HttpServletRequest request=ctx.getRequest();
log.info("send {} request to {}",request.getMethod(),
request.getRequestURL().toString());
Object accessToken=request.getParameter("accessToken");
if(accessToken==null){
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
log.info("access token ok");
return null;
}
}
}
- filterType:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。
■ pre:可以在请求被路由之前调用。
■routing:在路由请求时被调用。
■post:在routing和error过滤器之后被调用。
■error:处理请求时发生错误时被调用。 - filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。通过int值来定义过滤器的执行顺序,数值越小优先级越高。
- shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了 true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
- run:过滤器的具体逻辑。
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的 Bean才能启动该过滤器,比如,在应用主类中增加如下内容:
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[]args){
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter(){
return new AccessFilter();
}
}
通信工具
RestTemplate
RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供了常见的REST请求方案的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以及一些通用的请求执行方法 exchange 以及 execute。
GET 请求
在 RestTemplate 中,和 GET 请求相关的方法有如下几个:
这里的方法一共有两类,getForEntity 和 getForObject,每一类有三个重载方法,下面我们分别予以介绍。
getForEntity:
getForEntity方法的返回值是一个ResponseEntity<T>,ResponseEntity<T>是Spring对HTTP请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。比如下面一个例子:
- getForEntity的第一个参数为我要调用的服务的地址,这里我调用了服务提供者提供的/hello接口,注意这里是通过服务名- 调用而不是服务地址,如果写成服务地址就没法实现客户端负载均衡了。
- getForEntity第二个参数String.class表示我希望返回的body类型是String
@RestController
public class UseHelloController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/gethello")
public String hello(String name) {
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class);
String body = responseEntity.getBody();
HttpStatus statusCode = responseEntity.getStatusCode();
int statusCodeValue = responseEntity.getStatusCodeValue();
HttpHeaders headers = responseEntity.getHeaders();
StringBuffer result = new StringBuffer();
result.append("responseEntity.getBody():").append(body).append("<hr>")
.append("responseEntity.getStatusCode():").append(statusCode).append("<hr>")
.append("responseEntity.getStatusCodeValue():").append(statusCodeValue).append("<hr>")
.append("responseEntity.getHeaders():").append(headers).append("<hr>");
return result.toString();
}
}
@RequestMapping("/sayhello")
public String sayHello() {
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/sayhello?name={1}", String.class, "张三");
return responseEntity.getBody();
}
@RequestMapping("/sayhello2")
public String sayHello2() {
Map<String, String> map = new HashMap<>();
map.put("name", "李四");
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/sayhello?name={name}", String.class, map);
return responseEntity.getBody();
}
getForObject:
getForObject函数实际上是对getForEntity函数的进一步封装,如果你只关注返回的消息体的内容,对其他信息都不关注,此时可以使用getForObject,举一个简单的例子,如下:
@RequestMapping("/book2")
public Book book2() {
Book book = restTemplate.getForObject("http://HELLO-SERVICE/getbook1", Book.class);
return book;
}
POST请求
第一种:postForEntity
该方法和get请求中的getForEntity方法类似,如下例子:
@RequestMapping("/book3")
public Book book3() {
Book book = new Book();
book.setName("红楼梦");
ResponseEntity<Book> responseEntity = restTemplate.postForEntity("http://HELLO-SERVICE/getbook2", book, Book.class);
return responseEntity.getBody();
}
第二种:postForObject
如果你只关注,返回的消息体,可以直接使用postForObject。用法和getForObject一致。