Spring Cloud是一系列框架的集合,它包含多个子项目(针对分布式系统中涉及的多个不开源产品):
• Spring Cloud Config
• Spring Cloud Netflix
• Spring Cloud CloudFoundry
• Spring Cloud AWS
• Spring Cloud Security
• Spring Cloud Commons
• Spring Cloud Zookeeper
• Spring Cloud CLI
Cloud核心是服务治理,服务治理主要通过整合Spring Cloud Netflix项目,该项目包括:
• Eureka:用于服务注册和发现
• Hystrix:调用断路器
• Ribbon:调用端负载均衡
• Feign: Rest客户端
• Zuul:智能服务路由
• Spectator、Servo、Atlas:用于监控数据收集和展示
• Archaius:用于配置读取
• RxJava :提供Controller层Reactive封装
-
系统架构图及分析
搭建springcloud要构建三个角色:
角色 内容 Eureka Server(服务注册中心) 负责服务列表的注册、维护和查询等功能 Service Provider(服务提供方) 负责将所提供的服务向Eureka Server进行注册、续约和注销等操作 Service Consumer(服务消费方) 负责从Eureka Server中获取相应的服务列表 -
构建基础系统
-
搭建层级关系
-
创建maven主工程(ferao-spring-cloud)
• 模块创建
• 引入springboot依赖
-> 父工程中指定SpringCloud的版本
-> packaing修改为pom<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ferao</groupId> <artifactId>ferao-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>ferao-spring-cloud Maven Webapp</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Edgware.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <modules> <module>consumer-ferao</module> <module>service-discovery</module> <module>provider-ferao</module> </modules> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
创建service-discovery模块((服务注册中心))
• 模块创建引入依赖:
• pom文件配置:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ferao</groupId> <artifactId>service-discovery</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-discovery</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <parent> <groupId>com.ferao</groupId> <artifactId>ferao-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
• application启动类增加@EnableEurekaServer注解
@SpringBootApplication @EnableEurekaServer public class ServiceDiscoveryApplication { public static void main(String[] args) { SpringApplication.run(ServiceDiscoveryApplication.class, args); System.out.println("注册中心启动成功"); } }
• application.yml配置:
spring: application: name: service-discovery server: port: 8761 eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
访问:http://localhost:8761/
-
创建provider-ferao模块(服务提供方)
• pom文件配置:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.ferao</groupId> <artifactId>ferao-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> </parent> <groupId>com.ferao</groupId> <artifactId>provider-ferao</artifactId> <version>0.0.1-SNAPSHOT</version> <name>provider-ferao</name> <description>服务提供方</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
• application启动类增加@EnableEurekaClient注解
@EnableEurekaClient @SpringBootApplication public class ProviderFeraoApplication { public static void main(String[] args) { SpringApplication.run(ProviderFeraoApplication.class, args); System.out.println("服务提供方启动成功"); } }
• application.yml配置:
spring: application: name: provider-ferao server: port: 8762 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/
访问:http://localhost:8761/
-
-
定义
Eureka用于定位服务,以实现云端中间层服务发现和故障转移。以后只需要使用服务的标识符就可以访问到服务,不需要修改服务调用的配置文件。
-
组件
eureka两大组件:
• Eureka Server(提供注册服务)
• Eureka Client(JAVA客户端,负责发送心跳)系统中的其他微服务使用Eureka客户端连接到Eureka服务端维持心跳连接(即注册)。SpringCloud的其他模块可以通过Eureka Server 来发现系统中的微服务并加以调用。
-
三大角色
• Eureka Server:提供服务注册和发现
• Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到
• Service Consumer:服务消费方,从Eureka获取注册服务列表,从而能够消费服务。
-
eureka-server自我保护(配置)
当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式
eureka: server: #扫描失效服务的间隔时间(缺省为60*1000ms) eviction-interval-timer-in-ms: 1000 #关闭自我保护模式(缺省为打开) enable-self-preservation: false
有一个问题,在保护模式的前提下,只要一个服务消失,不管是我们主动关闭还是服务出现问题,Eureka都会将它保护起来,但是我们确实不再需要这个服务,如何不让Eureka保护它而是直接关闭该服务呢?这就涉及到Eureka的优雅停服了。
-
eureka-client服务续约(服务提供者配置)
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew)。有两个重要参数可以修改服务续约的行为:
eureka: instance: lease-renewal-interval-in-seconds: 30 lease-expiration-duration-in-seconds: 90
• lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
• lease-expiration-duration-in-seconds:服务失效时间,默认值90秒默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
eureka: instance: lease-expiration-duration-in-seconds: 10 # 10秒即过期 lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
-
eureka-server集群
高可用的EurekaServer,本质是服务同步,多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息
-
第一台配置:修改EurekaServer的配置信息
server: port: 10086 # 端口 spring: application: name: eureka-server # 应用名称,会在Eureka中显示 eureka: client: service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087 defaultZone: http://127.0.0.1:10087/eureka
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
• 删除了register-with-eureka=false和fetch-registry=false两个配置。因为默认值是true,这样就会把自己注册到注册中心了
• 把service-url的值改成了另外一台EurekaServer的地址,而不是自己 -
第二台配置
server: port: 10087 spring: application: name: eureka-service # 应用名称,在Eureka中显示 eureka: client: service-url: # 配置其他Eureka服务的地址,而不是自己。比如100086 defaultZone: http://127.0.0.1:10086/eureka
-
客户端注册到服务集群
eureka: client: service-url: # EurekaServer地址,多个地址以','隔开 defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
-
访问主页:http://localhost:8761/
• No application available:没有服务被发现,原因是只搭建了注册中心,还没有搭建服务。
-
定义
管理各种服务功能:
• 注册
• 发现
• 熔断
• 负载
• 降级接收实例发送过来的心跳,如果心跳超时,则将对应实例从Eureka Server中删除。
-
调用关系变化
无注册中心:项目A -> 项目B
有注册中心:项目A -> 注册中心 -> 项目B -
表面自己是Eureka Server
• eureka.client.registerWithEureka:false
• eureka.client.fetchRegistry:false
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
-
标注EurekaServer
@EnableEurekaServer //表示是EureakaServer @SpringBootApplication public class EurekaServer01Application { public static void main(String[] args) { SpringApplication.run(EurekaServer01Application.class, args); } }
-
添加yml配置
eureka: instance: #指定Eureka主机 localhost 表示本机 hostname: localhost client: register-with-eureka: true service-url: defaultZone: http://localhost:18081/eureka/
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生产环境不需要修改。
这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如10S。
eureka:
server:
#扫描失效服务的间隔时间(缺省为60*1000ms)
eviction-interval-timer-in-ms: 1000
-
定义
向Eureka Server注册,注册时提供基础信息,如主机和端口,URL,主页等。
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 添加yml配置
eureka:
instance:
#指定Eureka主机 localhost 表示本机
hostname: localhost
client:
service-url:
defaultZone: http://localhost:18081/eureka/
当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会从EurekaServer服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。可以通过下面参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点
启动注册中心并且注册上去两个服务后,现在需要让两个服务之间可以进行远程调用,有两步:
-
增加注解@LoadBalanced
@Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); }
-
调用方式
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); String url = "http://userservice/user/" + order.getUserId(); User user = restTemplate.getForObject(url, User.class); order.setUser(user); // 4.返回 return order; } }
-
eureka.client
属性 内容 registerWithEureka 声明是否将自己的信息注册到 Eureka 服务器上 fetchRegistry 是否到 Eureka 服务器中抓取注册信息 -
eureka.client.serviceUrl
属性 内容 defaultZone 设置eureka server对外暴露的地址
-
定义
网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求、鉴权、监控、缓存、限流等功能。
官方已不再更新Zuul,SpringCloud不再集成Zuul 2.X;
-
使用场景
不同的微服务一般会有不同的网络地址,而客户端可能需要调用多个服务接口才能完成一个业务需求,若让客户端直接与各个微服务通信,会有以下问题:
• 客户端会多次请求不同微服务,增加了客户端复杂性
• 存在跨域请求,处理相对复杂
• 认证复杂,每个服务都需要独立认证
• 难以重构,多个服务可能将会合并成一个或拆分成多个微服务网关介于服务端与客户端的中间层,所有外部服务请求都会先经过微服务网关客户只能跟微服务网关进行交互,无需调用特定微服务接口,使得开发得到简化
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency>
-
添加配置属性
spring.application.name=gateway-service-zuul server.port=8888 #这里的配置表示,访问/it/** 直接重定向到http://www.ityouknow.com/** zuul.routes.baidu.path=/it/** zuul.routes.baidu.url=http://www.ityouknow.com/
-
修改启动类
@SpringBootApplication //支持网关路由 @EnableZuulProxy public class GatewayServiceZuulApplication { public static void main(String[] args) { SpringApplication.run(GatewayServiceZuulApplication.class, args); } }
-
定义
Gateway网关是系统的唯一对外的入口,目标是替代 Zuul。
在某些场景下,网关就像是一个公共方法,我们可以把项目中的都要用到的一些功能提出来,比如业务上的日志收集、Token校验等等放入网关服务中,当然这么理解很狭隘,因为网关的能力远不止如此,但是不妨碍我们更好地理解它。
目前,比较流行的网关有:Nginx 、 Kong 、Orange等等,还有微服务网关Zuul 、Spring Cloud Gateway等等
-
底层
Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
-
术语
名称 内容 可实现 Route 路由是网关的基本组件。它由ID、目标URI、谓词集合和过滤器集合定义 使用Route结合Hystrix实现默认降级策略 Predicate This is a Java 8 Function Predicate Filter 是GatewayFilter的一个实例 使用GatewayFilter接口,自定义过滤器类,实现登录态(token)校验 -
原理
Gateway Client客户端发送请求在Gateway Handler Mapping中查找是否命中路由策略,命中的话请求转发给Gateway Web Handler来处理。根据定义的多个Filter链,执行顺序:Pre Filter->代理请求->Post Filter
-
执行流程
• 首先通过@EnableFeignCleints注解开启FeignCleint
• 根据Feign的规则实现接口,并加@FeignCleint注解
• 程序启动后,会进行包扫描,扫描所有的@ FeignCleint的注解的类,并将这些信息注入到ioc容器中
• 当接口的方法被调用,通过jdk的代理,来生成具体的RequesTemplate
• RequesTemplate在生成Request
• Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp
• 最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡
-
pom.xml
<!--feign依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
application.yml
server: port: 9001 #指定注册中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:7001/eureka/ #服务的名称 spring: application: name: order-service #自定义负载均衡策略(一般不用配用默认的) product-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
-
SpringBoot启动类
@SpringBootApplication //添加@EnableFeignClients注解 @EnableFeignClients public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
-
ProductOrderServiceImpl订单接口实现类
@Service public class ProductOrderServiceImpl implements ProductOrderService { @Autowired private ProductClient productClient; @Override public ProductOrder save(int userId, int productId) { //获取json格式的字符串数据 String response = productClient.findById(productId); //Json字符串转换成JsonNode对象 JsonNode jsonNode = JsonUtils.str2JsonNode(response); //将数据封装到订单实体中 ProductOrder productOrder = new ProductOrder(); productOrder.setCreateTime(new Date()); productOrder.setUserId(userId); productOrder.setTradeNo(UUID.randomUUID().toString()); //获取商品名称和商品价格 productOrder.setProductName(jsonNode.get("name").toString()); productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString())); //因为在商品微服务配置了集群,所以这里打印看下调用了是哪个集群节点,输出端口号。 System.out.println(jsonNode.get("name").toString()); return productOrder; } }
-
ProductClient类
可以把这里类理解成,就是你需要调用的微服务的controller层(这里指商品微服务),这样相对于Ribbon来讲代码的可读性就高多了
/** * 商品服务客户端 * name = "product-service"是服务端名称 */ @FeignClient(name = "product-service") public interface ProductClient { //这样组合就相当于http://product-service/api/v1/product/find @GetMapping("/api/v1/product/find") String findById(@RequestParam(value = "id") int id); }
-
OrderController类
@RestController @RequestMapping("api/v1/order") public class OrderController { @Autowired private ProductOrderService productOrderService; @RequestMapping("save") public Object save(@RequestParam("user_id")int userId, @RequestParam("product_id") int productId){ return productOrderService.save(userId, productId); } }
-
定义
它是系统遇到负载过高,突发流量或者网络等各种异常情况常用的解决方案。
熔断
A服务的X功能依赖B服务的某个接口,当B服务接口响应很慢时就需要触发熔断机制。
即A服务不在请求B这个接口,而可以直接进行降级处理。降级
服务器当压力剧增的时候,根据当前业务情况及流量,对一些服务和页面进行有策略的降级。以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应降级分两种:
• 自动降级
情况:超时、失败次数、故障、限流
(1)配置好超时时间(异步机制探测回复情况);
(2)不稳的的api调用次数达到一定数量进行降级(异步机制探测回复情况);
(3)调用的远程服务出现故障(dns、http服务错误状态码、网络故障、Rpc服务异常),直接进行降级;• 人工降级
秒杀、双十一大促降级非重要的服务
-
情况汇总
在一个分布式系统里,一个服务依赖多个服务,可能存在某个服务调用失败,比如超时、异常等,如何能够保证在一个依赖出问题的情况下,不会导致整体服务失败。
• 在高负载情况下出现超时情况
• 程序bug导致死循环
• 存在慢查询
• 程序逻辑不对导致耗尽内存
• 黑客攻击
• 促销
• 第三方系统响应缓慢 -
需求背景
-
需求背景
-
pom.xml
<!--hystrix依赖,主要是用 @HystrixCommand --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
application.yml
server: port: 9001 #指定注册中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:7001/eureka/ #服务的名称 spring: application: name: order-service #开启feign支持hystrix (注意,一定要开启,旧版本默认支持,新版本默认关闭) # #修改调用超时时间(默认是1秒就算超时) feign: hystrix: enabled: true client: config: default: connectTimeout: 2000 readTimeout: 2000
-
SpringBoot启动类
@SpringBootApplication @EnableFeignClients //添加熔断降级注解 @EnableCircuitBreaker public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
-
ProductClient
/** * 商品服务客户端 * name = "product-service"是你调用服务端名称 * fallback = ProductClientFallback.class,后面是你自定义的降级处理类,降级类一定要实现ProductClient */ @FeignClient(name = "product-service",fallback = ProductClientFallback.class) public interface ProductClient { //这样组合就相当于http://product-service/api/v1/product/find @GetMapping("/api/v1/product/find") String findById(@RequestParam(value = "id") int id); }
-
ProductClientFallback降级处理类
/** * 针对商品服务,错降级处理 */ @Component public class ProductClientFallback implements ProductClient { @Override public String findById(int id) { System.out.println("ProductClientFallback中的降级方法"); //这对gai该接口进行一些逻辑降级处理........ return null; } }
-
OrderController类
注意:fallbackMethod = "saveOrderFail"中的saveOrderFail方法中的参数类型,个数,顺序要和save一模一样,否则会报找不到saveOrderFail方法。
@RestController @RequestMapping("api/v1/order") public class OrderController { @Autowired private ProductOrderService productOrderService; @RequestMapping("save") //当调用微服务出现异常会降级到saveOrderFail方法中 @HystrixCommand(fallbackMethod = "saveOrderFail") public Object save(@RequestParam("user_id")int userId, @RequestParam("product_id") int productId){ return productOrderService.save(userId, productId); } //注意,方法签名一定要要和api方法一致 private Object saveOrderFail(int userId, int productId){ System.out.println("controller中的降级方法"); Map<String, Object> msg = new HashMap<>(); msg.put("code", -1); msg.put("msg", "抢购人数太多,您被挤出来了,稍等重试"); return msg; } }