官网地址
SpringCloud是基于SpringBoot的一整套实现微服务的框架。它提供了微服务开发所需的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等组件。最重要的是,基于SpringBoot,会让开发微服务架构非常方便。常用的组件如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
server:
port: 10001
spring:
application:
name: eureka-server
eureka:
client:
# 是否注册自己的信息到EurekaServer,默认是true
register-with-eureka: false
# 是否拉取其它服务的信息,默认是true
fetch-registry: false
# EurekaServer的地址,如果是集群,需要加上其它Server的地址。
service-url:
defaultZone: http://127.0.0.1:${server.port}/eureka
在启动类添加@EnableEurekaServer
注解
启动服务并访问,http://127.0.0.1:10001/,如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
server:
port: 10002
spring:
application:
name: consumer_test1
eureka:
client:
service-url:
# EurekaServer地址
defaultZone: http://127.0.0.1:10001/eureka
instance:
# 当其它服务获取地址时提供ip而不是hostname
prefer-ip-address: true
# 指定自己的ip信息,不指定的话会自己寻找
ip-address: 127.0.0.1
在启动类添加@EnableDiscoveryClient
注解
在status一列中,显示以下信息:
${hostname} + ${spring.application.name} + ${server.port}
通过instance-id属性来修改它的构成:
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
重启服务再查看:
上面用代码演示了一下,接下来就详细解释一下euraka
Eureka架构中的三个核心角色:
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能
服务提供者
提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。
修改上文中的Eureka Server配置,区别在于defaultZone地址不同,删掉register-with-eureka: false
和 fetch-registry: false
。因这两个参数默认为true,可以不用配置,这样eureka server就可以互相注册并拉取到每个服务的信息了
Eureka Server服务1
server:
port: 10001
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10003/eureka
Eureka Server服务2
server:
port: 10003
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka
在defaultZone
添加服务端地址,用逗号隔开即可
server:
port: 10002
spring:
application:
name: consumer_test1
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka,http://127.0.0.1:10003/eureka
instance:
instance-id: ${spring.application.name}:${server.port}
无论是服务的提供者还是服务的消费者都需要向EurekaServer注册服务
eureka.client.register-with-erueka=true
属性是否注册自己的信息到EurekaServer,默认为true
当服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-erueka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务名称,第二层Map的key是服务的实例id。
eureka.client.fetch-registry=true
属性是否拉取其它服务的信息,默认是true
当服务消费者启动是,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。可以通过registry-fetch-interval-seconds
来修改拉取服务的间隔时间
eureka:
client:
registry-fetch-interval-seconds: 5
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告知自己是否还存活着,这个就是所谓的服务续约。当然,这个间隔时间可以通过以下参数修改:
lease-renewal-interval-in-seconds
属性服务续约的间隔,默认为30秒
lease-expiration-duration-in-seconds
属性服务失效时间,默认值90秒
默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。但是在开发时,这个值有点太长了,比如开发时经常会关掉一个服务,然而Eureka依然认为服务在活着。所以开发阶段可以时可以适当修改一下值。
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
当一个服务出现故障而不能提供服务时,Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,开发时可以适当配置一下值,因为重启一个服务时,Eureka Server 60秒才反应过来,对开发造成不便。
当关停一个服务,就会在Eureka面板看到一条警告。
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。
在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
eureka:
server:
# 关闭自我保护模式(缺省为打开)
enable-self-preservation: false
# 扫描失效服务的间隔时间(缺省为60*1000ms)
eviction-interval-timer-in-ms: 1000
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡的工具。主要功能是提供客户端的软件负载均衡算法,将 Netflix 的中间层服务连接在一起。Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等。
Eureka中已经集成了负载均衡组件,所以不用引用ribbon的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在启动类添加@EnableDiscoveryClient注解
server:
port: 10005
spring:
application:
name: Product-01
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka
instance:
# 当其它服务获取地址时提供ip而不是hostname
prefer-ip-address: true
# 指定自己的ip信息,不指定的话会自己寻找
ip-address: 127.0.0.1
@RestController
public class TestController {
@GetMapping("/tt")
public String tt(){
return "访问服务提供者成功!";
}
}
还是以上文的consumer_test1为例子
在启动类里面注入RestTemplate并添加@LoadBalanced
注解
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerTest1Application {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerTest1Application.class, args);
}
}
这里注意一下url,这里直接写服务名称。服务名称不能用下划线_,要换成中划线-。如果用了下划线restTemplate会报错:Request URI does not contain a valid hostname。
@Service
public class TestService {
@Autowired
private RestTemplate restTemplate;
public String tt(){
String url = "http://PRODUCT-01/tt";
String s = restTemplate.getForObject(url, String.class);
System.out.println(s);
return s;
}
}
@RestController
public class TConsumerController {
@Autowired
private TestService testService;
@GetMapping("/getTest")
public String getTest(){
return testService.tt();
}
}
为什么restTemplate.getForObject直接写服务名称就会调用成功呢?这是因为LoadBalancerInterceptor
根据service名称,获取到了服务实例的ip和端口,如下源码:
restTemplate.getForObject发起请求,被LoadBalancerInterceptor拦截,接着进入intercept方法获取到uri和服务名称。
在
RibbonLoadBalanceClient
类中接着执行execute方法,getLoadBalancer
获取一个负载均衡器,然后getServer
根据 负载均衡器的算法在服务列表中选择server。
先增加一个服务提供者,端口号为10006,以便后面测试。
在刚才的源码中看到拦截是使用RibbonLoadBalanceClient
来进行负载均衡的,其中有一个choose方法,这个方法的作用:就是从LoadBalancer中为指定的服务选择服务示例
public interface ServiceInstanceChooser {
/**
* Chooses a ServiceInstance from the LoadBalancer for the specified service.
* @param serviceId The service ID to look up the LoadBalancer.
* @return A ServiceInstance that matches the serviceId.
*/
ServiceInstance choose(String serviceId);
}
@SpringBootTest
class ConsumerTest1ApplicationTests {
@Autowired
private RibbonLoadBalancerClient client;
@Test
public void test(){
for (int i = 0; i < 20; i++) {
ServiceInstance instance = this.client.choose("Product-01");
System.out.println(instance.getHost() + ":" + instance.getPort());
}
}
}
可以看到默认就是轮询
继续查看源代码会看到有一个这样的方法,然后这个rule就是用来进行服务选择的。
那么有多少种策略呢?可以查看它的实现类,如下
那怎么去修改负载均衡策略呢?可以修改如下配置:
格式是:
{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
Product-01:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
可以发现已经是随机了
当把10006端口的服务提供者停掉后,再次访问http://localhost:10002/getTest,如下结果。这是因为服务剔除的延迟,consumer并不会立即得到最新的服务列表,此时再次访问你会得到错误提示。
但是,不是还有一个服务10005是正常的吗,此时,可以用重试机制,当一次服务调用失败后,不会立即抛出异常,而是再次重试另一个服务。
根据如下配置,当访问到某个服务超时后,它会再次尝试访问下一个服务实例,如果不行就再换一个实例,如果不行,则返回失败。切换次数取决于MaxAutoRetriesNextServer
参数的值
spring:
application:
name: consumer_test1
cloud:
loadbalancer:
retry:
enabled: true # 开启Spring Cloud的重试功能
Product-01:
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
Hystix,即熔断器。它是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
服务雪崩是一种因“服务提供者的不可用”导致“服务调用者不可用”,并将不可用逐渐放大的现象。
例如A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了。
服务降级就是优先保证核心服务,而非核心服务不可用或弱可用。
服务降级目的就是先保证主要服务的畅通,一切资源优先让给主要服务来使用。比如访问一个商品详情时,获取评论时10s都还没有响应,但是商品服务是正常的,这时候就可以做服务降级,保证商品服务正常,商品的评论就给出温馨的提示即可。
在服务消费方(consumer_test1)引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
亦可以直接写
@SpringCloudApplication
,该注解包含下面三个注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerTest1Application {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerTest1Application.class, args);
}
}
@HystrixCommand(fallbackMethod = "ttFallback")
:用来声明一个降级逻辑的方法。这里要注意,熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。
@RestController
public class TConsumerController {
@Autowired
private TestService testService;
@HystrixCommand(fallbackMethod = "ttFallback")
@GetMapping("/getTest")
public String getTest(){
return testService.tt();
}
public String ttFallback(){
return "系统繁忙,请稍后再试!";
}
}
Hystix的默认超时时长为1s,所以这里设置2s,消费方就会进行服务降级。这里对端口为10006的服务方改造。
@RestController
public class TestController {
@GetMapping("/tt")
public String tt() throws InterruptedException {
Thread.sleep(2000);
return "访问服务提供者成功!";
}
}
访问http://localhost:10002/getTest,结果如下:
不知道看到测试结果后有没有伙伴想到上文的重试机制呢?为什么没有进行重试呢?那是因为ribbon的读取时间设置成了一秒。因此重试机制没有被触发,而是先触发了熔断。所以,Ribbon的超时时间
一定要小于Hystix的超时时间。
那么可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
来设置Hystrix超时时间。在消费方增加如下配置:
###配置请求超时时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 7000
###配置具体方法超时时间
tt:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
在消费方指定commandkey
@HystrixCommand(commandKey = "tt",fallbackMethod = "ttFallback")
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。Spring Cloud 给 Feign 添加了支持Spring MVC注解,并整合Ribbon及Eureka进行支持负载均衡。
在服务消费端引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
配置需要调用的微服务接口,然后添加@FeignClient
注解,value指定服务名称。那么Feign会通过动态代理,生成实现类,并且Feign会根据注解生成URL,并访问获取结果。
@FeignClient("Product-01")
public interface TTFeignClient {
@GetMapping("/tt")
String tt();
}
在启动类添加@EnableFeignClients
注解
把RestTemplate相关的代码都注释掉,改造如下:
@Service
public class TestService {
// @Autowired
// private RestTemplate restTemplate;
@Autowired
private TTFeignClient ttFeignClient;
public String tt(){
return ttFeignClient.tt();
}
}
不变
feign已经集成了ribbon依赖和自动配置,所以不需要像上文ribbon那样,引入依赖和配置 restTemplate。直接配置即可,如下:
Product-01:
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
Feign默认也有对Hystix的集成,所以启动Hystix也只需一行配置:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:
feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩
同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限
注:上面的数据类型、压缩大小下限均为默认值。
网关即是一个网络整体系统中的前置门户入口。请求首先通过网关,进行路径的路由,定位到具体的服务节点上。Spring Cloud Zuul是整合Netflflix公司的Zuul开源项目实现的微服务网关,它实现了请求路由、负载均衡、校验过虑等功能。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
/consumer_test1/**
开头的请求,代理到http://127.0.0.1:10002,也就是请求到consumer_test1服务上
server:
port: 10008
spring:
application:
name: gateway
zuul:
routes:
# 这里是路由id,可随意写
consumer_test1:
# 这里是映射路径
path: /consumer_test1/**
# 映射路径对应的实际url地址
url: http://127.0.0.1:10002
在启动类添加@EnableZuulProxy
注解
可以看到测试是通过了,但是地址却被写死了,那如果这个服务是多个实例的话,是不是要手动添加每一个地址。显然,这不应该。那么就应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在启动类添加@EnableDiscoveryClient
注解
eureka配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka,http://127.0.0.1:10003/eureka
instance:
# 当其它服务获取地址时提供ip而不是hostname
prefer-ip-address: true
# 指定自己的ip信息,不指定的话会自己寻找
ip-address: 127.0.0.1
zuul路由配置
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
zuul:
routes:
# 这里是路由id,随意写
consumer_test1:
# 这里是映射路径
path: /consumer_test1/**
# 映射路径对应的实际url地址
serviceId: consumer_test1
默认情况下,一切服务的映射路径就是服务名本身。
consumer_test1
,则默认的映射路径就是:/consumer_test1/**
在上文的配置中,配置的规则是这样的:
zuul.routes..path=/xxx/**
: 来指定映射路径。
是自定义的路由名zuul.routes..serviceId=/consumer_test1
:来指定服务名。而大多数情况下,配置的
路由名称往往和 服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.
上文的配置可以简化为一条:
zuul:
routes:
consumer_test1: /consumer_test1/**
通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。路径/api/consumer_test1/getTest
将会被代理到/consumer_test1/getTest
zuul:
prefix: /api # 添加路由前缀
routes:
consumer_test1: # 这里是路由id,随意写
path: /consumer_test1/** # 这里是映射路径
service-id: consumer_test1 # 指定服务名称
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 连接超时时间(ms)
ReadTimeout: 2000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔断超时时长:6000ms
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个功能往往是通过Zuul提供的过滤器来实现的。
查看源码,可以看到ZuulFilter中有四个重要的方法,如下:
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
abstract public String filterType();
abstract public int filterOrder();
// 来自IZuulFilter
boolean shouldFilter();
// IZuulFilter
Object run() throws ZuulException;
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:
pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。package com.ao.gateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class LoginFilter extends ZuulFilter {
/*登录校验,前置拦截*/
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
/*返回true,代表过滤器生效。*/
@Override
public boolean shouldFilter() {
return true;
}
/*登录校验逻辑*/
@Override
public Object run() {
// 获取Zuul提供的请求上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
// 从上下文中获取request对象
HttpServletRequest req = ctx.getRequest();
//从请求中获取token
String token = req.getParameter("my-token");
// 校验
if(token == null || "".equals(token.trim())){
// 没有token,登录校验失败,拦截
ctx.setSendZuulResponse(false);
// 返回401状态码
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}