通过本文可以给你带来什么?
- 熟悉掌握Spring Cloud,了解其生态及掌握多个组件的原理。
- 上手实践,基于Spring Cloud Netflix各组件搭建项目。
简介
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud的诞生就是为了做分布式,它是一个生态,定义了一套标准,目前基于其落地实现的有:Spring Cloud Netflix、Spring Cloud Alibaba,二者都可以提供一套分布式解决方案,例如:众多服务统一管理,服务之间的通信,服务熔断降级、链路追踪等。
Spring Cloud为我们提供的解决方案中包含很多组件,下面表格列出Spring Cloud生态常用组件以及其应用。
功能 | Spring Cloud官方 | Spring Cloud Netflix | Spring Cloud Alibaba |
---|---|---|---|
服务发现/注册 | Consul | Eureka | Nacos |
服务调用 | OpenFeign/RestTemplate | Feign | Dubbo RPC |
负载均衡 | Loadbalancer | Ribbon | Dubbo LB |
服务路由 | Spring Cloud Gateway | Zuul | - |
配置中心 | Spring Cloud Config | - | Nacos |
服务熔断 | - | Hystrix | Sentinel |
注册中心
Eureka作为注册中心,分为服务端和客户端,服务端提供客户端注册,客户端可以在服务端拉取注册表到本地(所以客户端不会因为注册中心宕机而找不到其他服务,优先在本地找)。如下图
核心功能
注:以下均为application.properties配置文件
- Register
服务注册,向Eureka注册信息,以被发现。
#开启服务注册
eureka.client.register-with-eureka = true
#注册中心地址
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/
- Renew
续租,心跳,30s发送一次心跳续租,通知Eureka实例时活动的,Eureka超过90s没发现更新,在注册表中移除。
#续租时间
eureka.instance.lease-renewal-interval-in-seconds=30
#该时间内未收到下次心跳移除实例
eureka.instance.lease-expiration-duration-in-seconds=90
- Fetch Registry
Eureka客户端从服务器拉取注册表缓存本地,之后客户端使用这些信息查找其他服务,定期(30s)更新。
#在注册中心拉取服务注册列表
eureka.client.fetch-registry=true
#更新注册列表时间间隔
eureka.client.registry-fetch-interval-seconds=30
- Cancel
Eureka客户端在关闭时向Eureka服务器发送取消请求,服务器将从注册表删除实例。
- 自我保护机制
默认情况下,Eureka Server在一定时间内接收不到某个服务的续约会将其踢出服务列表。但是在网络故障时,上述行为就非常危险,因为服务正常,不应该被踢出。Eureka Server通过自我保护机制解决这个问题,当短时间内丢失过多服务时(小于85%),会保护注册列表中的服务不被注销,网络恢复后退出保护模式。
#开启自我保护机制
eureka.server.enable-self-preservation=true
#比较实际心跳和预计心跳时间频率
eureka.server.eviction-interval-timer-in-ms=60000
#短时间丢失服务比例,小于该值进入保护模式
eureka.server.renewal-percent-threshold = 0.85
- 容错机制
在注册表内的服务一段时间内默认可用,每次调用不实时检查是否可用,提高可用,降低一致。
- 健康检查
server和client通过心跳保持服务状态,心跳正常,服务一直UP。
如果此服务无法提供正常服务需要将服务下线(例如:DB连不上),此时需要启动eureka的健康检查,同时需要设置健康状态为DOWN,代码如下:
- 启动健康检查
eureka.client.healthcheck.enabled=true
根据实际情况通过调用
setStatus
设置服务健康状态。public class HealthStatusEvent implements HealthIndicator { private Boolean status=true; @Override public Health health() { if(status) return new Health.Builder().up().build(); return new Health.Builder().down().build(); } public Boolean getStatus() { return status; } public void setStatus(Boolean status) { this.status = status; } }
高可用配置
- eureka server作为客户端向其他eureka server注册。
server1配置:
eureka.instance.hostname=eureka-server1.com
eureka.client.service-url.defaultZone=http://eureka-server2.com:8002/eureka/
server2配置:
eureka.instance.hostname=eureka-server2.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/
- 多个eureka server作为单节点注册到多个eureka server中
server1配置:
eureka.instance.hostname=eureka-server1.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/
server2配置:
eureka.instance.hostname=eureka-server2.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/
server3配置:
eureka.instance.hostname=eureka-server3.com
eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/
注意:
由于是在一台服务器上搭建,所以需要指定eureka.instance.hostname,向系统的hosts文件添加ip映射
服务调用
SpringCloud服务之间的通信方式主要是以restful风格进行服务调用,常见调用方式为RestTemplate+Ribbon、Fegin。其中RestTemplate是Spring提供用于访问Rest服务的客户端,Ribbon主要功能是提供客户端在服务调用时的负载均衡,而Fegin是一个声明式的Http客户端,只需要在接口上添加一个注解,使得服务调用更简单。
service-provider工程提供的接口
package com.bg.serviecprovider.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MainController {
@RequestMapping("/hi")
public String hi() {
return "hi,im provider";
}
}
service-consumer工程通过通过以下三种方式进行调用:
RestTemplate
@GetMapping("/hi")
public String hi() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject("http://127.0.0.1:8081/hi", String.class);
}
RestTemplate+Ribbon
注入RestTemplate时加上@LoadBalanced注解就会具有负载均衡的能力,代码如下:
@Bean
//开启负载均衡
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/hi2")
public String hi2() {
return restTemplate.getForObject("http://service-provider/hi", String.class);
}
Fegin
- pom文件中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 启动类加入@EnableFeignClients注解
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class ServiceConsumerApplication {
public static void main(String[] args) {
new ClassPathXmlApplicationContext();
SpringApplication.run(ServiceConsumerApplication.class, args);
}
}
- 新建FeginService接口并加上@FeignClient注解,value为要调用的服务名称。hi接口url与service-provider提供的接口url一致。
package com.bg.serviceconsumer.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "service-provider")
public interface FeginService {
@GetMapping("/hi")
public String hi();
}
- 在需要的地方直接注入FeginService进行调用
@Autowired
private FeginService feginService;
@GetMapping("/hi3")
public String hi3() {
return feginService.hi();
}
负载策略
Ribbon内置了几种负载策略,体现在源码BaseLoadBalancer类中IRule,可以配置其他负载规则或者自定义实现规则。
负载策略 | 说明 |
---|---|
RoundRobinRule | 轮询策略,默认采⽤的策略 |
RandomRule | 随机策略,从所有的提供者随机选一个 |
RetryRule | 重试策略,先按照RoundRobinRule策略获取,若获取失败,则在指定的时限内重试 |
BestAvailableRule | 最可⽤策略,选择并发量最⼩的提供者 |
WeightedResponseTimeRule | 权重策略,根据响应时间做权重,时间越快权重越大 |
@Bean
public IRule getRule(){
return new RandomRule();
}
超时&重试
feign默认支持Ribbon,二者的重试机制有冲突,所以源码关闭了feign的重试机制,使用Ribbon的重试机制
- 添加Ribbon超时重试配置
# 连接超时时间
ribbon.ConnectTimeout=1000
# 响应超时时间
ribbon.ReadTimeout=3000
# 同一服务器上的最大重试次数(不包括首次尝试)
ribbon.MaxAutoRetries=1
# 要重试的下一个服务器的最大数量(不包括第一个服务器)
ribbon.MaxAutoRetriesNextServer=1
# 是否所有操作都重试,默认在get请求下会重试,其他情况不会重试
ribbon.OkToRetryOnAllOperations=false
- 提供者模拟超时
AtomicInteger count = new AtomicInteger();
@RequestMapping("/timeoutretry")
public String timeoutretry() throws InterruptedException {
System.out.println(count.getAndIncrement()+"次调用");
Thread.sleep(5000L);
return "hi,im provider,port" + port;
}
- 消费者声明feign超时接口
@FeignClient(value = "service-provider")
public interface FeignService {
@GetMapping("/timeoutretry")
public String timeoutretry();
}
- 消费者调用feign
@Autowired
private FeginService feginService;
@GetMapping("/timeout")
public String timeout() {
return feginService.timeoutretry();
}
服务熔断
Hystrix是Netflix提供的一个容错组件,用来提升系统的可用性和容错性。主要有以下几个功能:
- 降级:当请求失败、超时、被拒绝或断路器被打开时执行回退逻辑,提供优雅的服务降级。
- 隔离:Hystrix为每个请求的依赖都维护了一个小型线程池或信号量,当请求达到阈值时快速失败并降级,避免因该请求影响整个系统造成级联失败。
- 熔断:当某服务失败率达到阈值时,Hystrix可以自动跳闸,一段时间内停止请求某服务。
降级机制
hystrix超时时间配置,要大于ribbon的超时时间,否则重试机制就没有任何意义
# 开启fegin熔断
feign.hystrix.enabled=true
# hystrix的超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=7000
降级回退的两种方式:
- 当断路器被打开或出现错误时执行的默认代码路径。
@FeignClient(value = "service-provider",fallback = FeignCallback.class )
public interface FeignService {
@GetMapping("/timeoutretry")
public String timeoutretry();
}
@Component
class FeignCallback implements FeignService {
@Override
public String timeoutretry() {
return "降级了";
}
}
- 如果需要访问导致回退触发的原因,可以使用@FeignClient内的fallbackFactory属性,然后根据回退原因执行相关操作。
@FeignClient(value = "service-provider", fallbackFactory = FeignCallbackFactory.class)
public interface FeignService {
@GetMapping("/timeoutretry")
public String timeoutretry();
}
@Component
class FeignCallbackFactory implements FallbackFactory<FeignService> {
@Override
public FeignService create(Throwable throwable) {
return new FeignService() {
@Override
public String timeoutretry() {
return "降级了:" + throwable.getMessage();
}
};
}
}
隔离机制
Hystrix隔离机制分为两种:
- 线程池隔离:Hystrix用自己的线程执行调用;
- 信号量隔离:直接用tomcat线程执行调用;
Hystrix默认使用线程池控制请求隔离,二者具体区别如下:
线程池隔离 | 信号量隔离 | |
---|---|---|
开销 | 有线程调度、排队、上下文切换开销 | 无线程切换,开销小 |
异步 | 支持异步 | 不支持异步 |
是否支持熔断 | 当请求达到线程池阈值时会触发fallback接口进行熔断 | 当请求量达到信号量阈值时会触发fallback接口进行熔断 |
隔离原理 | 各个服务单独用线程池 | 各个服务通过信号量的计数器 |
使用场景 | 适合网络开销比较大或比较耗时的场景。避免容器(tomcat)中线程因阻塞或等待影响整个系统 | 适合请求不耗时、高并发场景 |
线程池隔离
线程池隔离配置
# 调用每个服务时的核心线程数(全局)
#hystrix.threadpool.default.coreSize=2
# 调用每个服务时的最大线程数(全局)
#hystrix.threadpool.default.maximumSize=2
# 调用service-provider服务时的核心线程数
#hystrix.threadpool.service-provider.coreSize=1
# 调用service-provider服务时的最大线程数
#hystrix.threadpool.service-provider.maximumSize=1
当service-provider服务的请求量达到阈值时抛出以下异常
Task java.util.concurrent.FutureTask@6eefc19c rejected from java.util.concurrent.ThreadPoolExecutor@4f3a844f[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 3]
信号量隔离
Hystrix默认使用线程池控制请求隔离,当使用信号量隔离时需要开启,配置如下
# 信号量
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
# 并发请求量
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=1
当service-provider服务的请求量达到阈值时抛出以下异常
could not acquire a semaphore for execution
Hystrix仪表板
Hystrix仪表板以有效的方式显示每个断路器的运行状况。仪表板显示步骤:
- pom.xml文件中引入仪表板依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 启动类添加相关注解
@EnableCircuitBreaker
@EnableHystrixDashboard
- 启动成功后访问http://127.0.0.1:8082/actuator/hystrix.stream端点,如下:![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/1843b1a1bd9c4f9383bd5d88322b0847.png)
- http://127.0.0.1:8082/hystrix/ 访问仪表板
- 将仪表板指向实例的/hystrix.stream端点,通过feign访问失败后显示监控数据
如果出现以下现象,在配置文件中加入hystrix.dashboard.proxyStreamAllowList=127.0.0.1
即可服务路由
通常在应用启动后,我们通过链接直接访问即可,但是如果一个系统有多个微服务组成,那么客户端访问众多的服务是一件头疼的事。
服务路由可以帮忙客户端统一接入请求并路由相应的服务。如下图:
Zuul是Netflix开源的微服务网关,其核心是一系列过滤器,这些过滤器可以完成路由、限流、统一认证的功能。启用Zuul
- pom.xml文件引入Zuul依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
- 启动类添加
@EnableZuulProxy
注解
应用启动后即可通过Zuul+服务名进行访问,配置文件中端口为80,访问地址为http://127.0.0.1/service-consumer/hi1
。
路由配置
# zuul相关配置 ZuulProperties.class
# 关闭默认路由
zuul.ignoredServices=*
# 为所有映射添加前缀
zuul.prefix=/zuul
# 请求被转发之前,代理前缀被删除,设置false切换此行为
zuul.stripPrefix=false
# 对具体服务进行路由
zuul.routes.service-consumer=/server1/**
zuul.routes.service-provider=/server2/**
参考文档
spring-cloud-netflix中文文档
Netflix github