在项目开发中随着业务越来越多,导致功能之间耦合性高、开发效率低、系统运行缓慢难以维护、不稳定。微服务架构可以解决这些问题,而Spring Cloud是微服务架构最流行的实现,所以我们今天来学习Spring Cloud.
1. 系统架构演变
随着互联网的发展,网站应用的规模不断扩大,需求的激增,随之而来的是技术上的压力。系统架构也因此不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布
式服务,到SOA,以及现在火热的微服务架构。
1.1. 集中式架构
当网站流量很小时,只需要一个应用,将所有的功能都部署在一起,以减少部署节点和成本。
优点:
- 系统开发速度快
- 维护成本低
- 适用于并发要求较低的系统
缺点:
- 代码耦合度高,后期维护困难
- 无法针对不同模块进行优化
- 无法水平扩展
- 单点容错率低,并发能力差
1.2.垂直拆分
当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分:
优点:
- 系统拆分实现了流量分担,解决了并发问题
- 可以针对不同模块进行优化
- 方便水平扩展,负载均衡,容错率提高
缺点:
系统间相互独立,会有很多重复开发工作,影响开发效率
1.3.分布式服务
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此
时,用于提高业务复用及整合的分布式调用是关键。
优点:
将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点:
系统间耦合度变高,调用关系错综复杂,难以维护
1.4.服务治理(SOA)
SOA(Service Oriented Architecture)面向服务的架构:它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用。
SOA缺点:每个供应商提供的ESB产品有偏差,自身实现较为复杂;应用服务粒度较大,ESB集成整合所有服务和协议、数据转换使得运维、测试部署困难。所有服务都通过一个通路通信,直接降低了通信速度。
1.5.微服务
微服务架构是使用一套小服务来开发单个应用的方式或途径,每个服务基于单一业务能力构建,运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,并能
够通过自动化部署机制来独立部署。这些服务可以使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。 微服务结构图 :
微服务的特点:
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言
- 无关,也不限定用什么技术实现,只要提供REST的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队。
- 技术独立:因为是面向服务,提供REST接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一REST接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
微服务和SOA比较:
2. 远程调用方式
无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?
常见的远程调用方式有以下几种:
- RPC:Remote Procedure Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的Web Service,现在热门的Dubbo,都是RPC的典型。
- HTTP:HTTP其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用HTTP协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。现在热门的REST风格,就可以通过HTTP协议来实现。
2.1.认识RPC
RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。说得通俗一点就是:A计算机提供一个服务,B计算机可以像调用本地服务那样调用A计算机的服务。
RPC调用流程图:
2.2.认识HTTP
HTTP其实是一种网络传输协议,基于TCP,工作在应用层,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用HTTP协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的REST风格,就可以通过HTTP协议来实现。
2.3.如何选择?
RPC的机制是根据语言的API(language API)来定义的,而不是根据基于网络的应用来定义的。如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么Spring Cloud搭建微服务是不二之选。会选择Spring Cloud套件,因此会使用HTTP方式来实现服务间调用。
3.Spring Cloud简介
3.1.简介
Spring Cloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
Spring Cloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
Netflix
Eureka:注册中心
Zuul:服务网关
Ribbon:负载均衡
Feign:服务调用
Hystrix:熔断器
以上只是其中一部分,架构图:
3.2.版本
Spring Cloud的版本命名比较特殊,因为它不是一个组件,而是许多组件的集合,它的命名是以A到Z为首字母的一些单词组成(其实是伦敦地铁站的名字):
Spring Clound 和Spring Boot版本对应关系
课程采用Greenwich版本
4.微服务场景模拟
模拟一个服务调用的场景。方便学习后面的课程
4.1. 创建父工程
微服务中需要同时创建多个项目,为了方便课堂演示,先创建一个父工程,后续的工程都以这个工程为父,使用Maven的聚合和继承。统一管理子工程的版本和配置
pom.xml文件:
1 |
|
注意:spring clound和spring boot 的版本对应 greenwich版本clound对应spring boot 2.1.x
注意:注意聚合父工程
这里已经对大部分要用到的依赖的版本进行了 管理,方便后续使用
4.2.服务提供者
我们新建一个项目,对外提供查询用户的服务。
选中lxs-springclound,创建子工程:
pom.xml文件
1 |
|
4.2.2.编写配置文件
创建 user-service\src\main\resources\application.yml 属性文件,这里我们采用了yaml语法,而不是properties:
1 | server: |
4.2.3.编写代码
启动类
1 |
|
entity
1 |
|
dao
1 | public interface UserMapper extends Mapper<User> { |
service
1 |
|
controller
对外提供REST风格web 服务,根据id查询用户
1 |
|
完成上述代码后的项目结构
启动并测试
启动项目,访问http://localhost:9091/user/7
4.3.服务调用者
这里的服务调用者就类似于客户端,必须在服务提供者打开时才可以进行调用
4.3.1.创建工程pom
1 |
|
4.3.2.编写代码
启动器:
1 | package com.lxs.consumer; |
Spring提供了一个RestTemplate模板工具类,对基于HTTP的客户端进行了封装,并且实现了对象与json的序列化
和反序列化,非常方便。RestTemplate并没有限定HTTP的客户端类型,而是进行了抽象,目前常用的3种都有支
持:
HTTPClient
OkHTTP
JDK原生的URLConnection(默认的)
entity
1 |
|
5. 思考问题
简单回顾一下,刚才我们写了什么:
user-service:对外提供了查询用户的接口
consumer-demo:通过RestTemplate访问 http://locahost:9091/user/{id} 接口,查询用户数据
存在什么问题?
- 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护
- consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效
- consumer不清楚user-service的状态,服务宕机也不知道
- user-service只有1台服务,不具备高可用性
- 即便user-service形成集群,consumer还需自己实现负载均衡
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
- 服务管理
- 如何自动注册和发现
- 如何实现状态监管
- 如何实现动态路由
- 服务如何实现负载均衡
- 服务如何解决容灾问题
- 服务如何实现统一配置
以上的问题,我们都将在SpringCloud中得到答案。
6.Eureka注册中心
6.1.Eureka简介
问题分析
在刚才的案例中,user-service对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。
网约车
这就好比是网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
Eureka做什么?
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。同时,服务提供方与Eureka之间通过 “心跳” 机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。这就实现了服务的自动注册、发现、状态监控。
6.2.原理图
基本架构:
Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
心跳(续约):提供者定期通过HTTP方式向Eureka刷新自己的状态
6.3.入门案例
6.3.1.编写EurekaServer
Eureka是服务注册中心,只做服务注册;自身并不提供服务也不消费服务。可以搭建Web工程使用Eureka,可以使用Spring Boot方式搭建。
pom.xml
1
2
3
4
5
6<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>编写启动类:
1
2
3
4
5
6
7
8
9
10
11import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}编写配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# eureka服务的地址,如果做集群,需要指定其他eureka地址
defaultZone: http://127.0.0.1:10086/eureka
#不注册自己
register-with-eureka: false
#不拉取服务
fetch-registry: false
# server:
# #服务失效剔除间隔时间,默认60秒
# eviction-interval-timer-in-ms: 10000
# # 关闭自我保护
# enable-self-preservation: false启动服务,并访问:http://127.0.0.1:10086/
6.3.2. 服务注册
在服务提供工程user-service上添加Eureka客户端依赖;自动将服务注册到EurekaServer服务地址列表。
添加依赖
1
2
3
4
5<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>在启动类上开启Eureka客户端功能
1
2
3
4
5
6
7
8
"com.lxs.user.mapper") (
//开启Eureka客户端发现功能
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}编写配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16server:
port: 9091
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud
username: root
password: root
application:
name: user-service
mybatis:
type-aliases-package: com.lxs.user.pojo
eureka:
client:
service-url:
defaultZone: HTTP://127.0.0.1:10086/eureka
注意:
这里我们添加了spring.application.name属性来指定应用名称,将来会作为应用的id使用。
不用指定register-with-eureka和fetch-registry,因为默认是true
重启项目,访问Eureka监控页面查看
注册成功
6.3.3. 服务发现
在服务消费工程consumer-demo上添加Eureka客户端依赖;可以使用工具类DiscoveryClient根据服务名称获取对应的服务地址列表。
添加依赖:
1
2
3
4
5<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>在启动类添加开启Eureka客户端发现的注解
1
2
3
4
5
6
7
8
9
10
11
// 开启Eureka客户端
public class UserConsumerDemoApplication {
public RestTemplate restTemplate() {
return new RestTemplate(new OkHTTP3ClientHTTPRequestFactory());
}
public static void main(String[] args) {
SpringApplication.run(UserConsumerDemoApplication.class, args);
}
}修改配置
1
2
3
4
5
6
7spring:
application:
name: consumer-demo <!--也有可能作为服务的提供者,这里也给他留一个接口-->
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka修改代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例(不再使用写死的url):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"/consumer") (
public class ConsumerController {
private RestTemplate restTemplate;
private DiscoveryClient discoveryClient;
"/{id}") (
public User queryById(@PathVariable("id") Long id) {
String url = "http://localhost:9091/user/" + id;
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("user-service");
ServiceInstance serviceInstance = serviceInstances.get(0);
url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/"
+ id;
return restTemplate.getForObject(url, User.class);
}
}Debug跟踪运行:
6.4.Eureka详解
接下来我们详细讲解Eureka的原理及配置。
6.4.1.基础架构
Eureka架构中的三个核心角色:
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka-server服务提供者提供服务的应用,可以是Spring Boot应用,也可以是其它任意技术实现,只要对外提供的是REST风格服务即可。本例中就是我们实现的user-service
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的consumer-demo
6.4.2.高可用的Eureka Server
你中有我,我中有你
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心 。Eureka Server是一个web应用,可以启动多个实例(配置不同端口)保证Eureka Server的高可用服务同步多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。而作为客户端,需要把信息注册到每个Eureka中。
如果有三个Eureka,则每一个EurekaServer都需要注册到其它几个Eureka服务中。
例如:有三个分别为10086、10087、10088,则:
10086要注册到10087和10088上
10087要注册到10086和10088上
10088要注册到10086和10087上
动手搭建高可用的EurekaServer
我们假设要搭建两条EurekaServer的集群,端口分别为:10086和10087
我们修改原来的EurekaServer配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14server:
port: ${port:10086}
spring:
application:
name: eureka-server
eureka:
client:
#eureka的服务地址,如果是集群,需要指定其他集群eureka地址
service-url:
defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka}
# 不注册自己
#register-with-eureka: false
# 不抓取服务
#fetch-registry: false
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
- 删除了register-with-eureka=false和fetch-registry=false两个配置。因为默认值是true,这样就会吧自己注册到注册中心了。
- 把service-url的值改成了另外一台EurekaServer的地址,而不是自己
修改原来的启动配置组件;在如下界面中的 VM options 中
设置 -DdefaultZone=http:127.0.0.1:10087/eureka
复制一份并修改;在如下界面中的 VM options 中 设置 -Dport=10087 -
DdefaultZone=http:127.0.0.1:10086/eureka
3)启动测试;同时启动两台eureka server
4)客户端注册服务到集群
因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:
1 | eureka: |
6.4.3.Eureka客户端和服务端配置
这个小节我们进行一系列的配置:
Eureka客户端工程
- user-service 服务提供
- 服务地址使用ip方式
- 续约
- consumer-demo 服务消费
- 获取服务地址的频率
Eureka服务端工程 eureka-server
- 失效剔除
- 自我保护
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否为true,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中 。
第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性,user-service
第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081
值则是服务的实例对象,也就是说一个服务,这样可以同时启动多个不同实例,形成集群。
默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可以在 user-service 中添加配置如下:
1 | eureka: |
修改完后先后重启 user-service 和 consumer-demo ;在调用服务的时候就已经变成ip地址;需要注意的是:不是
在eureka中的控制台服务实例状态显示。
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉
EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为;可以在 user-service 中添加如下配置项:
1 | eureka: |
lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
获取服务列表
当服务消费者启动时,会检测 eureka.client.fetch-registry=true 参数的值,如果为true,则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。并且 每隔30秒 会重新拉取并更新数据。可以在 consumer-demo 项目中通过下面的参数来修改:
1 | eureka: |
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
6.4.5.失效剔除和自我保护
如下的配置都是在Eureka Server服务端进行:
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态
失效剔除
有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。 可以通过eureka.server.eviction-interval-timer-in-ms 参数对其进行修改,单位是毫秒。
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%,当EurekaServer节点在短时间内丢失过多客户端(可能发生了网络分区故障)。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:
1 | eureka: |
小结:
user-service
1 | eureka: |
7.负载均衡Ribbon
在刚才的案例中,我们启动了一个user-service,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。
但是实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon:
7.1.启动两个服务实例
首先我们启动两个user-service实例,一个9091,一个9092。
在user-service中配置如下端口:
1 | server: |
在启动配置中配置如下
Eureka监控面板:
7.2 开启负载均衡
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码:
在RestTemplate的配置方法上添加 @LoadBalanced 注解:
1 |
|
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
1 | "/{id}") ( |
7.3.源码跟踪
为什么只输入了service名称就可以访问了呢?之前还要获取ip和端口。
显然是有组件根据service名称,获取到了服务实例的ip和端口。因为 consumer-demo 使用的是RestTemplate,spring使用LoadBalancerInterceptor拦截器 ,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。我们进行源码跟踪:
8.Hystrix
主页:https://github.com/Netflix/Hystrix/
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务,防止出现级联失败。
8.1.雪崩问题
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。 如果此时,某个服务出现异常:
例如: 微服务I 发生异常,请求阻塞,用户请求就不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大
Hystrix解决雪崩问题的手段,主要包括:
- 线程隔离
- 服务降级
8.3. 线程隔离&服务降级
8.3.1原理
线程隔离示意图
解读:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级:可以优先保证核心服务。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。 触发Hystrix服务降级的情况:
线程池已满
请求超时
8.3.2.动手实践
服务降级:及时返回服务调用失败的结果,让线程不因为等待服务而阻塞
1) 引入依赖
在 consumer-demo 消费端系统的pom.xml文件添加如下依赖:
1 | <dependency> |
2) 开启熔断
在启动类 ConsumerApplication 上添加注解:@EnableCircuitBreaker
1 |
|
可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication
因此,我们可以使用这个组合注解来代替之前的3个注解 :
1 |
|
3) 编写降级逻辑
当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystrixCommand来完成。
改造 consumer-demo\src\main\java\lxs\com\consumer\controller\ConsumerController.java 处理器 类,如下:
1 |
|
要注意;因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把queryById的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
说明:
@HystrixCommand(fallbackMethod = “queryByIdFallBack”):用来声明一个降级逻辑的方法
测试:
当 user-service 正常提供服务时,访问与以前一致。但是当将 user-service 停机时,会发现页面返回了降级处理信息:
4) 默认的fallback
刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以可以把Fallback配置加在类上,实现默认fallback; 再次改造
consumer-demo\src\main\java\com\lxs\consumer\controller\ConsumerController.java
1 |
|
@DefaultProperties(defaultFallback = “defaultFallBack”):在类上指明统一的失败降级方法;该类中所有方法返回类型要与处理失败的方法的返回类型一致。
9.Feign
核心就在于一个feign客户端,在这里通过注解完成对被调用服务端的访问,然后再controller中再调用这里的方法
在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码
如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?
这就是我们接下来要学的Feign的功能了。
简介
为什么叫伪装?
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
项目主页:https://github.com/OpenFeign/feign
9.1 快速入门
在consumer中导入依赖
1 | <dependency> |
Feign的客户端
1 | "user-service") ( |
首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟Mybatis的mapper很像
@FeignClient ,声明这是一个Feign客户端,同时通过 value 属性指定服务名称
接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
@GetMapping中的/user,请不要忘记;因为Feign需要拼接可访问的地址
编写新的控制器类 ConsumerFeignController ,使用UserClient访问:
1 |
|
开启Feign功能
在 ConsumerApplication 启动类上,添加注解,开启Feign功能
1 |
|
10. Spring Cloud Gateway网关
简介
Spring Cloud Gateway是Spring官网基于Spring 5.0、 Spring Boot 2.0、Project Reactor等技术开发的网关服务。
Spring Cloud Gateway基于Filter链提供网关基本功能:安全、监控/埋点、限流等。
Spring Cloud Gateway为微服务架构提供简单、有效且统一的API路由管理方式。
Spring Cloud Gateway是替代Netflix Zuul的一套解决方案。
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。
Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册到Eureka服务注册中心。
网关的核心功能是:过滤和路由
10.2 Gateway加入后的架构
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都可经过网关,然后再由网关来实现 鉴权、动态路由等等操作。Gateway就是我们服务的统一入口。
10.3. 核心概念
路由(route)
路由信息的组成:由一个ID、一个目的URL、一组断言工厂、一组Filter组成。如果路由断言为真,说明请求URL和配置路由匹配。
断言(Predicate)
Spring Cloud Gateway中的断言函数输入类型是Spring 5.0框架中的ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于HTTP Request中的任何信息比如请求头和参数。
过滤器(Filter)
一个标准的Spring WebFilter。 Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
10.4. 快速入门
需求:通过网关系统lxs-gateway将包含有 /user 的请求 路由到 http://127.0.0.1:9091/user/用户id
10.4.1. 新建工程
打开 lxs-springcloud\lxs-gateway\pom.xml 文件修改为如下:
1 |
|
10.4.2. 编写启动类
在lxs-gateway中创建 com.lxs.gateway.GatewayApplication 启动类
1 |
|
10.4.2. 编写配置
创建 lxs-gateway\src\main\resources\application.yml 文件,内容如下:
1 | server: |
需要用网关来代理 user-service 服务,先看一下控制面板中的服务状态 :
ip为:127.0.0.1
端口为:9091
修改 lxs-gateway\src\main\resources\application.yml 文件为:
1 | server: |
将符合 Path 规则的一切请求,都代理到 uri 参数指定的地址 本例中,我们将路径中包含有 /user/** 开头的请求,代理到http://127.0.0.1:9091
10.4.5. 启动测试
访问的路径中需要加上配置规则的映射路径,我们访问:http://localhost:10010/user/7
10.5. 面向服务的路由
在刚才的路由规则中,把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然不合理。 应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由!
10.5.1. 修改映射配置,通过服务名称获取
修改 lxs-gateway\src\main\resources\application.yml 文件如下:
1 | gateway: |
路由配置中uri所用的协议为lb时(以uri: lb://user-service为例),gateway将使用 LoadBalancerClient把user-service通过eureka解析为实际的主机和端口,并进行ribbon负载均衡。
10.6. 路由前缀
客户端的请求地址与微服务的服务地址如果不一致的时候,可以通过配置路径过滤器实现路径前缀的添加和去除。
提供服务的地址:http://127.0.0.1:9091/user/8
添加前缀:对请求地址添加前缀路径之后再作为代理的服务地址;
http://127.0.0.1:10010/8 —> http://127.0.0.1:9091/user/8 添加前缀路径/user
去除前缀:将请求地址中路径去除一些前缀路径之后再作为代理的服务地址;
http://127.0.0.1:10010/api/user/8 —> http://127.0.0.1:9091/user/8 去除前缀路径/api
10.6.2. 去除前缀
在gateway中可以通过配置路由的过滤器StripPrefix,实现映射路径中地址的去除;
修改 lxs-gateway\src\main\resources\application.yml 文件:
1 | cloud: |
通过 StripPrefix=1 来指定了路由要去掉的前缀个数。如:路径 /api/user/1 将会被代理到 /user/1 。 也就是:
StripPrefix=1 http://localhost:10010/api/user/8 —》http://localhost:9091/user/8
StripPrefix=2 http://localhost:10010/api/user/8 —》http://localhost:9091/8
11. Spring Cloud Config分布式配置中心
简介
在分布式系统中,由于服务数量非常多,配置文件分散在不同的微服务项目中,管理不方便。为了方便配置文件集中管理,需要分布式配置中心组件。在Spring Cloud中,提供了Spring Cloud Config,它支持配置文件放在配置服务的本地,也支持放在远程Git仓库(GitHub、码云)。
使用Spring Cloud Config配置中心后的架构如下图
3.2. Git配置管理
知名的Git远程仓库有国外的GitHub和国内的码云(gitee);但是使用GitHub时,国内的用户经常遇到的问题是访问速度太慢,有时候还会出现无法连接的情况。如果希望体验更好一些,可以使用国内的Git托管服务——码云(gitee.com)。 与GitHub相比,码云也提供免费的Git仓库。此外,还集成了代码质量检测、项目演示等功能。对于团队协作开发,码云还提供了项目管理、代码托管、文档管理的服务。本章中使用的远程Git仓库是码云。
码云访问地址:https://gitee.com/
3.2.2. 创建远程仓库
首先要使用码云上的私有远程git仓库需要先注册帐号;请先自行访问网站并注册帐号,然后使用帐号登录码云控制台并创建公开仓库。
3.2.3. 创建配置文件
在新建的仓库中创建需要被统一配置管理的配置文件。
配置文件的命名方式:{application}-{profile}.yml 或 {application}-{profile}.properties
application为应用名称
profile用于区分开发环境,测试环境、生产环境等
如user-dev.yml,表示用户微服务开发环境下使用的配置文件。
这里将user-service工程的配置文件application.yml文件的内容复制作为user-dev.yml文件的内容,具体配置如下:
创建完user-dev.yml配置文件之后,gitee中的仓库如下:
3.3. 搭建配置中心微服务
3.3.1.创建项目
创建配置中心微服务工程:
添加依赖,修改 config-server\pom.xml 如下:
1 | <dependencies> |
3.3.2. 启动类
创建配置中心工程 config-server 的启动类;
config-server\src\main\java\com\lxs\config\ConfigServerApplication.java 如下:
1 | package com.lxs.config; |
3.3.3. 配置文件
1 | server: |
注意上述的 spring.cloud.config.server.git.uri 则是在码云创建的仓库地址;可修改为你自己创建的仓库地址
3.4. 获取配置中心配置
前面已经完成了配置中心微服务的搭建,下面我们就需要改造一下用户微服务 user-service ,配置文件信息不再由
微服务项目提供,而是从配置中心获取。如下对 user-service 工程进行改造。
3.4.1. 添加依赖
在 user-service 工程中的pom.xml文件中添加如下依赖:
1 | <dependency> |
3.4.2. 修改配置
- 删除 user-service 工程的 user-service\src\main\resources\application.yml 文件(因为该文件从配置
中心获取) - 创建 user-service 工程 user-service\src\main\resources\bootstrap.yml 配置文件
1 | spring: |