首先,我们需要模拟一个服务调用的场景。方便后面学习微服务架构
为方便管理,先新建一个maven工程做为父工程(springCloud-demo)
在springCloud-demo中创建模块(子工程),新建一个spring Initializr项目,对外提供查询用户的服务;
因为需要连接数据库和通过web访问,所以需要在新建模块时选择添加Web,JDBC,MyBatis,MySQL依赖;
再点击下一步,选择项目路径后点击完成;
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://http://cdn.xiongsihao.com.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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xsh.demo</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
因为要使用通用mapper,所以我们需要手动加一条依赖:
通用mapper封装了增删改查方法,可以通过继承Mapper接口而直接调用
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
@Table(name = "tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 用户名
private String userName;
// 密码
private String password;
// 姓名
private String name;
// 年龄
private Integer age;
// 性别,1男性,2女性
private Integer sex;
// 出生日期
private Date birthday;
// 创建时间
private Date created;
// 更新时间
private Date updated;
// 备注
private String note;
// 此处已省略省略getters和setters方法
}
@Repository /*为了在service层注入该接口不报错而加上该注解,也可以不加*/
public interface UserMapper extends Mapper<User> {
/*继承了通用Mapper接口,可直接调用简单的增删改查方法,User为指定返回的实体类*/
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryUserById(long id){
return this.userMapper.selectByPrimaryKey(id);
}
}
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") long id) {
return this.userService.queryById(id);
}
}
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
mybatis:
type-aliases-package: com.xsh.demo.domain #指定实体类包路径
在启动类上加上@MapperScan注解指定接口包路径,方便启动类扫描该包
@MapperScan("com.xsh.demo.mapper")
启动启动类,输入网址http://localhost:8081/user/1访问成功
继续在springCloud-demo中创建模块(子工程),新建一个spring Initializr项目,来调用其它接口的服务;
因为是调用user-service的功能,因此不需要mybatis相关依赖了,只需要引入Web依赖
当一个项目有多个启动类时,右下角会提示Run Dashboard,点击同意打开Run Dashboard面板方便管理
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://http://cdn.xiongsihao.com.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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xsh.demo</groupId>
<artifactId>user-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-consumer</name>
<description>Demo project for Spring Boot</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.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>
@SpringBootApplication
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
因为此时的User实体类只是作为一个返回类型,不需要连接数据库,所以只要添加属性不需要添加相关注解
public class User {
private Long id;
private String userName;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private Date created;
private Date updated;
private String note;
// 此处省略getter和setter方法
}
注意:这里不是调用mapper查数据库,而是通过RestTemplate远程查询user-service中的接口
@Component
public class UserMapper {
@Autowired
private RestTemplate restTemplate;
public User queryUserById(Long id){
String url = "http://localhost:8081/user/" + id;
return this.restTemplate.getForObject(url, User.class);
}
}
在服务层对接口就行加强: 接口只实现一次查询一个,服务层加强后可一次查询多个
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> querUserByIds(List<Long> ids){
List<User> users = new ArrayList<>();
for (Long id : ids) {
User user = this.userMapper.queryUserById(id);
users.add(user);
}
return users;
}
}
@RestController
@RequestMapping("consume")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> consume(@RequestParam("ids") List<Long> ids) {
return this.userService.querUserByIds(ids);
}
}
在application.yml配置服务端口(不配置默认为8080),然后启动启动类
server:
port: 8082
输入网址http://localhost:8082/consumer?ids=1,2,3访问成功
use-service:一个提供根据id查询用户的微服务
user-consumer:一个服务调用者,通过RestTemplate远程调用user-service
存在的问题:
上面的问题,概括一下就是分布式服务必然要面临的问题:
实现了服务的自动注册、发现、状态监控
基本架构:
在springCloud-demo中创建模块(子工程),新建一个spring Initializr项目,名字叫eureka
选择依赖:
已在创建spring Initializr时添加无需再次导入
完整的pom.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://http://cdn.xiongsihao.com.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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xsh.demo</groupId>
<artifactId>eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<!-- Eureka服务端 -->
<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>
<dependencyManagement>
<!-- SpringCloud依赖,一定要放到dependencyManagement中,起到管理版本的作用 -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
server:
port: 10086 #当前服务端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,因为是自己作为eureka服务
defaultZone: http://127.0.0.1:10086/eureka
#register-with-eureka: false # 此应用为注册中心,false:不向注册中心注册自己
#fetch-registry: false # 注册中心职责是维护服务实例,false:不检索服务。
@SpringBootApplication
@EnableEurekaServer // 声明这个应用是一个EurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
启动服务并访问: http://localhost:10086 可显示eureka信息
先添加SpringCloud依赖:
<!-- SpringCloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Spring的仓库地址 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
再添加Eureka客户端依赖:
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
application:
name: user-service # 当前微服务名称
mybatis:
type-aliases-package: com.xsh.demo.domain #指定实体类包路径
eureka:
client:
service-url: # Eureka服务地址
defaultZone: http://127.0.0.1:10086/eureka
注意: 还添加了spring.application.name属性来指定应用名称,将来会作为应用的id使用。
@EnableDiscoveryClient // 开启EurekaClient功能
重启user-service项目,刷新eureka服务页面,发现user-service已经注册进去了
先添加SpringCloud依赖:
<!-- SpringCloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Spring的仓库地址 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
再添加Eureka客户端依赖:
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server:
port: 8082
spring:
application:
name: consumer # 应用名称
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
@EnableDiscoveryClient // 开启EurekaClient功能
重启user-consumer项目,刷新eureka服务页面,发现consumer也已经注册进去了
修改user-consumer的UseMapper方法, 用DiscoveryClient类的方法,根据服务名称,获取服务实例
@Component
public class UserMapper {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;// Eureka客户端,可以获取到服务实例信息
public User queryUserById(Long id){
// 根据服务名称,获取服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取实例
ServiceInstance instance = instances.get(0);
System.out.println("当前实例服务的host地址为"+instance.getHost());
System.out.println("当前实例服务的端口为"+instance.getPort());
System.out.println("当前实例服务的uri为"+instance.getUri());
String url = "http://"+instance.getHost()+":"+instance.getPort()+"/user/" + id;
return this.restTemplate.getForObject(url, User.class);
}
}
这样就解决了地址硬编码的问题,没有直接写入地址和端口
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心(一个EurekaServer出现故障还有其它的EurekaServer)。
服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
动手搭建高可用的EurekaServer
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
假设要搭建两条EurekaServer的集群,端口分别为:10086和10087
先开启端口号为10086的服务,把自己注册到10087的eureka服务中
server:
port: 10086 #当前服务端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10087/eureka #因为此时没有10087的eureka服务,所以会正常报错
开启了10086端口的服务后,再拷贝当前设置或新建一个设置 启动类为当前项目的启动类,命名为EurekaApplication2
因为10086端口的服务已在启动状态,所以此时修改application.yml文件不影响已开启的10086服务
将当前服务的端口改为10087,并注册进10086的eureka,运行EurekaApplication2启动类,实现自己注册自己
server:
port: 10087 #当前服务端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
访问http://localhost:10086/ 或者 http://localhost:10087/ 都可以查看eureka监控页面,实现数据同步
因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:
eureka:
client:
service-url: # EurekaServer地址,多个地址以','隔开
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-erueka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务名称,第二层Map的key是服务的实例id。
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
eureka:
instance:
lease-expiration-duration-in-seconds: 10 # 10秒即过期
lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
先来看一下服务状态信息:
在Eureka监控页面,查看服务注册信息:
在status一列中,显示以下信息:
${hostname} + ${spring.application.name} + ${server.port}
我们可以通过instance-id属性来修改它的构成:
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
获取服务列表
当服务消费者启动时,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
服务下线
当服务进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”,服务中心接收到请求之后,将该服务置为下线状态
失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。
这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如10S
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(默认为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(默认为60*1000ms)
实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon:
将UserServiceApplication的启动配置拷贝一个命名为UserServiceApplication2
因为UserServiceApplication已经在运行状态,所以此时修改UserServiceApplication的配置文件中的端口不影响已开启的服务;
将端口8081改为8083,并启动UserServiceApplication2;此时开启的服务提供者有8081和8083
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码:
在RestTemplate的配置方法上添加@LoadBalanced
注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
修改调用方式user-consumer,不再手动获取服务的ip和端口,而是直接通过服务名称调用:
@Component
public class UserMapper {
@Autowired
private RestTemplate restTemplate;
public User queryUserById(Long id){
String url = "http://user-service/user/"+id;
return this.restTemplate.getForObject(url, User.class);
}
}
Ribbon默认的负载均衡策略是简单的轮询,就是当存在多个服务时,按顺序调用,我们可以测试一下:
拦截中是使用RibbonLoadBalanceClient来进行负载均衡的,其中有一个choose方法,所以编写一个测试类 :
@SpringBootTest
@RunWith(SpringRunner.class)
public class LoadBalanceTest {
@Autowired
RibbonLoadBalancerClient client;
@Test
public void test(){
for (int i = 0; i < 100; i++) {
ServiceInstance instance = this.client.choose("user-service");
System.out.println(instance.getUri());//打印查看调用的哪个服务
}
}
}
默认为轮询调用,运行结果:
SpringBoot也帮我们提供了修改负载均衡规则的配置入口:
user-service: #服务提供方的服务id
ribbon: #将负载均衡策略从默认的轮询改为随机
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
再次运行测试类,发现调用服务结果从轮询变成了随机:
Eureka为了实现更高的服务可用性,牺牲了一定的一致性,极端情况下它宁愿接收故障实例也不愿丢掉健康实例,正如我们上面所说的自我保护机制。
但是,此时如果我们调用了这些不正常的服务,调用就会失败,从而导致其它服务不能正常工作!这显然不是我们愿意看到的。
现在先关闭一个user-service实例 8083,因为服务剔除的延迟,consumer并不会立即得到最新的服务列表,此时再次访问http://localhost:8082/consumer?ids=1,2,3 并刷新多次,会有几率得到错误提示,因为刚好调用的是8083的服务
但是此时,8081服务其实是正常的。
因此Spring Cloud 整合了Spring Retry 来增强RestTemplate的重试能力,当一次服务调用失败后,不会立即抛出一次,而是再次重试另一个服务。
只需要简单配置即可实现Ribbon的重试, 在user-consumer的配置文件中追加以下配置:
spring:
cloud:
loadbalancer:
retry:
enabled: true # 开启Spring Cloud的重试功能
user-service: #服务提供者id
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 10 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
根据如上配置,当访问到某个服务超时后,它会再次尝试访问下一个服务实例,如果不行就再换一个实例,如果切换后还不行,则返回失败。切换实例次数取决于MaxAutoRetriesNextServer
参数的值
还需引入spring-retry依赖,支持重试机制
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
重启user-consumer服务并重试 关闭8083服务,再次访问http://localhost:8082/consumer?ids=1,2,3 并刷新多次,发现没有错误提示信息了
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
微服务中,服务间调用关系错宗复杂,一个请求可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:
当调用链路中的其中一个服务出现异常,请求阻塞,用户得不到响应,则tomcat的这个线程不会被释放,于是越来愈多的用户请求到来,越来越多的线程被阻塞,服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
Hystrix解决雪崩问题的手段有两个: 1.线程隔离 2. 服务熔断
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队加速失败判定时间;
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级: 优先保证核心服务,而非核心服务不可用或弱可用。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息)
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
在user-consumer的pom.xml文件中引入以下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在服务调用方启动类上添加注解
@EnableCircuitBreaker //开启熔断器
增加一个controller方法来测试hystrix
@RestController
@RequestMapping("hystrix")
public class hystrixController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@HystrixCommand(fallbackMethod = "queryUserFallback")
public String queryUser(@RequestParam("id") Long id) {
String url = "http://user-service/user/"+id;
return this.restTemplate.getForObject(url, String.class);
}
public String queryUserFallback(Long id){
return "服务器正忙,请稍后在试!";
}
}
通过@HystrixCommand注解的fallbackMethod参数来指定对应熔断方法
局部一对一熔断方法的返回值和参数要和被熔断方法一致
将开启的服务提供者全部关闭,这样就模拟了服务提供者不可用的情况下,触发服务调用方的熔断方法;
当我们再次访问控制层的hystrix测试方法时,因为服务提供者已不可用,所以提示自定义的熔断方法:
@RestController
@RequestMapping("hystrix")
@DefaultProperties(defaultFallback = "fallBackMethod")
public class hystrixController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@HystrixCommand
public String queryUser(@RequestParam("id") Long id) {
String url = "http://user-service/user/"+id;
return this.restTemplate.getForObject(url, String.class);
}
public String fallBackMethod(){
return "服务请求失败,请稍后重试!";
}
}
通过在控制层类上注解@DefaultProperties,defaultFallback的值指定全局熔断方法,这样就不用一个方法一个熔断类了,只需要在被熔断方法上加@HystrixCommand注解
全局熔断方法的返回值要和被熔断方法一致,但参数列表必须为空,因为被熔断方法可能参数不一样
定义了全局熔断方法后,也还可以在被熔断方法上注解 @HystrixCommand(fallbackMethod = "")
来制定其它熔断类
因为Ribbon重试机制默认为1000ms触发,而Hystix默认也是1000ms触发,因此重试机制没有被触发,而是先触发了熔断;
当存在部分服务提供者不可用时,访问到不可用服务,重试机制没有被触发就被熔断了,这是我们不希望看到的;
所以,Ribbon的超时时间一定要小于Hystix的超时时间,这样当访问到不可用服务时先触发重试机制,当重试后还是服务错误就触发熔断。
我们可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
来设置Hystrix超时时间。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的熔断触发时间为6000ms
线上环境因为多台服务之间相互调用会存在网络延迟,并不是服务不可用,所以会把hystrix熔断触发时间调大一点,这样就不会在正常网络延迟情况下触发熔断
为了测试熔断触发时间,需要在服务提供方方法内加入Thread.sleep( );线程等待,防止服务提供方方法运行完毕后时间未到触发熔断而直接返回熔断,如已开启Ribbon重试机制需先关闭;
如熔断触发时间设置为6秒,我们可以在服务提供方方法内线程等待15秒,然后运行服务提供方服务,这样服务提供方方法未运行完毕而触发熔断,f12查看熔断响应时间刚好为设置的时间6000ms;
除此之外: 还能优化服务调用方user-consumer的启动类注解
//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableCircuitBreaker
@SpringCloudApplication //相当于以上3个注解
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
以上案例中虽然访问到不可用服务时会触发熔断,但每次访问失败都会发送请求;
熔断状态机的3个状态:
模拟失败请求百分比达到阈值,修改user-consumer的controller方法,再去掉前面在user-service中加的Thread.sleep( );和将熔断触发时间改小:
@RestController
@RequestMapping("hystrix")
@DefaultProperties(defaultFallback = "fallBackMethod")
public class hystrixController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@HystrixCommand
public String queryUser(@RequestParam("id") Long id) throws InterruptedException {
if(id == 1){
throw new RuntimeException();//模拟失败请求百分比达到阈值
}
String url = "http://user-service/user/"+id;
return this.restTemplate.getForObject(url, String.class);
}
public String fallBackMethod(){
return "服务请求失败,请稍后重试!";
}
}
重启服务提供方和服务调用方,连续访问http://localhost:8082/hystrix?id=1 20次以上,模拟失败请求百分比达到阈值,再访问http://localhost:8082/hystrix?id=2,发现此时id=2也被熔断了,再连续刷新id=2的请求,发现过一段时间后id=2又能正常访问,这就是熔断状态机的Half Open: 半开状态。
默认的熔断触发要求较高,休眠时间较短,为了测试方便,可以通过配置修改熔断策略:
circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepwindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50
requestVolumeThreshold: 触发熔断的最小请求次数,默认20
sleepwindowInMilliseconds: 休眠时长,默认是5000毫秒
errorThresholdPercentage: 触发熔断的失败请求最小占比,默认50%
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
在服务调用者的pom.xml文件中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringCloudApplication
@EnableFeignClients //开启Feign功能
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
//因为开启了feign后不再需要RestTemplate,所以可以去掉
/* @Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}*/
}
@FeignClient("user-service") //指定调用的服务提供方的微服务id
public interface UserClient {
@GetMapping("user/{id}")
public User queryById(@PathVariable("id") long id);//需要调用的方法
}
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称@RestController
@RequestMapping("feign")
public class feignController {
@Autowired
private UserClient userClient;
@GetMapping
@HystrixCommand
public String queryUser(@RequestParam("id") Long id){
return this.userClient.queryById(id).toString();//通过feign客户端调用远程服务
}
}
重启user-consumer访问http://localhost:8082/feign?id=2 ,能正常访问
feign的依赖包中已经集成了Ribbon和hystrix,可以直接使用;
可以通过ribbon.xx
来进行全局配置。也可以通过服务名.ribbon.xx
来对指定服务配置
虽然feign集成了hystrix,但是默认是关闭的,可以通过配置开启:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
在feign中使用hystrix的熔断方法:
(1)需要先创建一个熔断类去实现feign的客户端接口:
@Component //将熔断类注入spring容器
public class UserClientFallback implements UserClient{
@Override
public User queryById(long id) {
User user=new User();
user.setUserName("服务器正忙,请稍后重试");
return user;
}
}
(2)再去客户端接口中指定该熔断类:
通过fallback来指定熔断类
@FeignClient(value = "user-service",fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("user/{id}")
public User queryById(@PathVariable("id") long id);//需要调用的方法
}
将服务提供方全部关闭并将hystrix的熔断触发时间调小,重启user-consumer访问http://localhost:8082/feign?id=2 可正常触发熔断
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
在项目中新建一个Spring Initializr模块,命名为zuul,并引入Spring Cloud Routing中的Zuul依赖;
点击下一步完成新建zuul模块;
因为在新建模块时已经引入zuul依赖,所以无需再引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://http://cdn.xiongsihao.com.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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xsh.demo</groupId>
<artifactId>zuul</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuul</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<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>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
server:
port: 10010 #服务端口
spring:
application:
name: Zuul #服务名
zuul:
routes:
user-service: #路由名称,可以随便写,习惯上写路由到哪个服务就写哪个的服务名称
path: /user-service/** #将/user-service/**开头的请求路由到http://localhost:8081
url: http://localhost:8081
@EnableZuulProxy //启用Zuul组件
开启zuul模块启动类,访问 http://localhost:10010/user-service/user/1
可以通过user-service路由到http://localhost:8081
在刚才的路由规则中,我们把路径对应的服务地址写死了,user-service固定为http://localhost:8081!如果同一服务有多个实例的话,这样做显然就不合理了。
我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!
在zuul模块内添加Eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加Eureka配置(将当前zuul服务注入到eureka)
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
修改路由规则
zuul:
routes:
user-service: #路由名称,可以随便写,习惯上写路由到哪个服务就写哪个的服务名称
path: /user-service/** #将/user-service/**开头的请求路由到http://localhost:8081
#url: http://localhost:8081
serviceId: user-service
将url改为serviceId实现动态路由
在zuul启动类上添加注解开启Eureka
@EnableDiscoveryClient //开启Eureka客户端发现功能
通过路径访问固定路由
zuul:
routes:
user-service: #路由名称,可以随便写,习惯上写路由到哪个服务就写哪个的服务名称
path: /user-service/** #将/user-service/**开头的请求路由到http://localhost:8081
url: http://localhost:8081
通过服务id访问动态路由
zuul:
routes:
user-service: #路由名称,可以随便写,习惯上写路由到哪个服务就写哪个的服务名称
path: /user-service/** #将/user-service/**开头的请求路由到http://localhost:8081
serviceId: user-service
当路由名称和服务id名称一致时(路由名称可定义多个)
zuul:
routes:
user-service: /user-service/**
#将/user-service/**开头的请求路由到服务id为user-service的服务
不配置zuul路由(默认的路由规则)
如果不配置zuul路由,默认情况下,一切服务的映射路径就是服务名本身
例如不配置zuul也可以通过http://localhost:10010/user-service/user/1正常访问,因为请求路径为/user-service/**,则默认路由到user-service
虽然不配置路由默认为映射路径就是服务名本身,但是一般还是选择使用第三种配置路由方法,可以使用路由前缀方便管理,分辨出哪些服务经过了路由
zuul:
prefix: /api # 添加路由前缀
routes:
user-service: /user-service/**
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:
pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
场景非常多:
模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。
@Component
public class LoginFilter extends ZuulFilter {
/*返回字符串,代表过滤器的类型*/
@Override
public String filterType() {
// 登录校验,肯定是在前置拦截
return "pre";
}
/*执行顺序,返回值越小优先级越高*/
@Override
public int filterOrder() {
return 1;
}
/*是否执行该过滤器*/
@Override
public boolean shouldFilter() {
return true;
}
/*编写过滤器的业务逻辑*/
@Override
public Object run() throws ZuulException {
// 登录校验逻辑。
// 1)获取Zuul提供的请求上下文对象
RequestContext context = RequestContext.getCurrentContext();
// 2) 从上下文中获取request对象
HttpServletRequest request = context.getRequest();
// 3) 从请求中获取access-token
String token = request.getParameter("access-token");
// 4) 判断,通过StringUtils的isBlank方法判断token是否为空,如果为空返回true
if(StringUtils.isBlank(token)){
// 没有token,登录校验失败,拦截;setSendZuulResponse设置是否转发请求,因为要拦截所以设置为false
context.setSendZuulResponse(false);
// 因为浏览器只识别状态码,所以设置一个状态码;返回401状态码。也可以考虑重定向到登录页。
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
//设置响应内容
context.setResponseBody("request error!");
}
// 校验通过,可以考虑把用户信息放入上下文,继续向后执行
return null; /*返回值为null,就代表该过滤器什么都不做*/
}
}
重启zuul启动类,
访问http://localhost:10010/api/user-service/user/1?access-token=XXX,可正常访问
访问http://localhost:10010/api/user-service/user/1,因为请求中没有access-token参数,所以拦截器拦截,不能正常访问
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
评论