目录
7.1.1 微服务概述
7.1.1.1 系统架构演变
7.1.1.1.1 单体架构
只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。
存在的问题
- 代码耦合,开发维护困难
- 无法针对不同模块进行针对性优化
- 无法水平扩展
- 单点容错率低,并发能力差
7.1.1.1.2 垂直拆分
当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分
优点
- 系统拆分实现了流量分担,解决了并发问题
- 可以针对不同模块进行优化
- 方便水平扩展,负载均衡,容错率提高
缺点
- 系统间相互独立,会有很多重复开发工作,影响开发效率
7.1.1.1.3 分布式服务
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。
优点:
-
将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点:
-
系统间耦合度变高,调用关系错综复杂,难以维护
7.1.1.1.4 SOA
SOA :面向服务架构
服务越来越多,需要管理每个服务的地址;调用关系错综复杂,难以理清依赖关系;服务状态难以管理,无法根据服务情况动态管理等等问题逐渐显现。
此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。
因此用于提高机器利用率的资源调度和治理中心(SOA)是关键。
服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
动态监控服务状态监控报告,人为控制服务状态
缺点:
-
服务间会有依赖关系,一旦某个环节出错会影响较大
-
服务关系复杂,运维、测试部署困难,不符合DevOps思想
7.1.1.1.5 微服务
微服务的特点:
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
- 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
- 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
- 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护
7.1.1.2 服务调用方式
7.1.1.2.1 RPC和HTTP
常见的服务间的远程调用方式有以下2种:
-
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
-
Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的Rest风格,就可以通过http协议来实现。
如果公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。
相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在我们的项目中,我们会选择SpringCloud套件,因此我们会使用Http方式来实现服务间调用。
由于HTTP比较灵活更符合微服务理念,所以前端用的较多
而由于RPC速度快,效率高,所以后端使用的较多
7.1.1.2.2 区别
7.1.1.3 Http客户端工具
很多的http客户端工具可以帮助实现对请求和响应的处理:
- HttpClient
- OKHttp
- URLConnection
但是,不同的客户端,API各不相同
因此出现了Spring的RestTemplate,将这些API都封装进去
7.1.1.4 RestTemplate
Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持:
- HttpClient
- OkHttp
- JDK原生的URLConnection(默认的)
7.1.2 SpringCloud概述
7.1.2.1 什么是SpringCloud
SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
- Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)
- Zuul:网关组件,提供智能路由,访问过滤功能
- Ribbon:客户端负载均衡的服务调用组件(客户端负载)
- Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)
- Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)
版本问题
7.1.2.2 SpringCloudAlibaba
7.1.3 Eureka注册中心
7.1.3.1 原理
7.1.3.2 模拟微服务场景
模拟一个服务调用的场景,搭建两个工程:service-provider(服务提供方)和service-consumer(服务调用方)。方便后面学习微服务架构
服务提供方:使用mybatis操作数据库,实现对数据的增删改查;并对外提供rest接口服务。
服务消费方:使用restTemplate远程调用服务提供方的rest接口服务,获取数据。
7.1.3.2.1 项目搭建
Eureka模块项目结构
首先处理pom.xmlw
<?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>
<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.bl</groupId>
<artifactId>service_provider</artifactId>
<version>1.0-SNAPSHOT</version>
<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>1.3.2</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>
<!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
分别向pom.xml文件中
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--springcloud版本对应springboot版本-->
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<!-- SpringCloud依赖管理 不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
<!-- 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>
<!--eureka服务端
1.上报自己的信息到eureka注册中心
2. 从eureka中拉取服务列表-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
修改application.yml
server:
port: 10086
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mydb?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
application:
name: service_provider # 应用名称,会在Eureka中显示
mybatis:
type-aliases-package: com.bl.model
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://localhost:10086/eureka/
完整的Eureka服务端的pom.xml文件如下
<?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>
<!--springboot父类工程-->
<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.bl</groupId>
<artifactId>Eureka</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--springcloud版本对应springboot版本-->
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<!-- SpringCloud依赖管理 不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
<!-- 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>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
修改启动类,在类上添加@EnableEurekaServer注解
EurekaApplication 启动类
@SpringBootApplication
@EnableEurekaServer
//通过添加`@EnableDiscoveryClient`来开启Eureka客户端功能
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
service_provider模块项目结构
pom.xml处理同上,但是要注意此时的依赖为客户端,而非服务器
spring-cloud-starter-netflix-eureka-client
<!--eureka客户端
1.上报自己的信息到eureka注册中心
2. 从eureka中拉取服务列表-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
完整pom.xml
<?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>
<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.bl</groupId>
<artifactId>service_provider</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--springcloud版本对应springboot版本-->
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<!-- SpringCloud依赖管理 不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
<!-- 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>
<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>1.3.2</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>
<!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--eureka客户端
1.上报自己的信息到eureka注册中心
2. 从eureka中拉取服务列表-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mydb?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
application:
name: service_provider # 应用名称,会在Eureka中显示
mybatis:
type-aliases-package: com.bl.model
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://localhost:10086/eureka/
通用mapper
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{
}
model层user
@Table(name="user")
@Data
public class User implements Serializable {
@Id
private Integer id;
private String name;
private String password;
}
UserserviceImpl
@Service
public class UserserviceImpl implements Userservice {
@Autowired
private UserMapper userMapper;
@Override
public User findById(int id) {
System.out.println(userMapper.selectAll());
return userMapper.selectByPrimaryKey(id);
}
}
UserController
@Controller
@RequestMapping("provider")
public class UserController {
@Autowired
private UserserviceImpl userServiceImpl;
@RequestMapping("findbyid/{id}")
@ResponseBody
public User findById(@PathVariable("id") int id) {
System.out.println(userServiceImpl.findById(id));
return userServiceImpl.findById(id);
}
}
service_consumer模块项目结构
完整pom.xml
<?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>
<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>org.example</groupId>
<artifactId>service_consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--springcloud版本对应springboot版本-->
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<!-- SpringCloud依赖管理 不是真正加依赖,而是指定版本管理 方便后面的引入相应版本的依赖-->
<!-- 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>
<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>1.3.2</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>
<!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--eureka客户端
1.上报自己的信息到eureka注册中心
2. 从eureka中拉取服务列表-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mydb?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
application:
name: service_consumer # 应用名称,会在Eureka中显示
mybatis:
type-aliases-package: com.bl.model
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://localhost:10086/eureka/
ConsumerApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient //springcloud提供的
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
//由于@SpringBootApplication中包含@configuration,所以可以充当配置类
//开启客户端后需要手动配置RestTemplate
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}
UserController
@RestController
@RequestMapping("/consumer")
public class UserController {
//需要在配置类中手动配置RestTemplate !!!
@Resource
RestTemplate restTemplate;
@Autowired
DiscoveryClient discoveryClient;
@RequestMapping("/findbyid/{id}")
public User findById(@PathVariable Integer id){
// 使用RestTemplate远程调用提供方
// User user = restTemplate.getForObject("http://localhost:8081/provider/findById/" + id, User.class);
// 根据服务名称获取实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
//由于只有一个eureka存在,所以get(0)获取到的第一个实例就是存在的eureka
ServiceInstance instance = instances.get(0);
// 服务地址
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/provider/findById/" + id;
// 调用
User user = restTemplate.getForObject(url, User.class);
System.out.println(user);
return user;
}
}
运行三个模块,打开浏览器访问localhost:10086,进入Eureka默认的图像化管理页面
7.1.3.3 Eureka详解
7.1.3.3.1 基础架构
Eureka架构中的三个核心角色:
- 服务注册中心
- Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka。
- 服务提供者
- 提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的service-provider。
- 服务消费者
- 消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的service-consumer。
7.1.3.3.2 高可用的Eureka Server
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
动手搭建高可用的EurekaServer
我们假设要运行两个EurekaServer的集群,端口分别为:10086和10087。只需要把eureka启动两次即可。
注意:idea中一个应用不能启动两次,我们需要重新配置一个启动器
按如下方式创建两个eureka启动器
修改eureka的application.yml
所谓的高可用注册中心,其实就是把Eureka自己也作为一个服务进行注册,这样多个Eureka之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
把service-url的值改成了另外一台Eureka的地址,而非自己
server:
port: 10086 # 端口
spring:
application:
name: eureka_server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://localhost:10087/eureka/
启动第一个 EurekaApplication
启动报错,正常现象,因为10087服务没有启动:
再次修改eureka的配置
server:
port: 10087 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
defaultZone: http://127.0.0.1:10086/eureka
启动第二个EurekaApplication
访问localhost:10087
客户端注册服务到集群
因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:
eureka:
client:
service-url: # EurekaServer地址,多个地址以','隔开
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
7.1.3.3.3 服务提供者
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。
Map<serviceId,Map<服务实例id,实例对象instance>>
- 第一层Map的Key就是服务id,一般是配置中的
spring.application.name
属性 - 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:
locahost:service-provider:8081
- 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
- 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秒一次心跳
7.1.3.3.4 服务消费者
获取服务列表
当服务消费者启动时,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
7.1.3.3.5 失效剔除和自我保护
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。
这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会(在eureka模块中)关闭自我保护模式:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
7.1.4 Ribbon—负载均衡
7.1.4.1 什么是Ribbon
在刚才的案例中,我们启动了一个service_provider,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。
但是实际环境中,我们往往会开启很多个service_provider的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon:
7.1.4.2 开启负载均衡
仍按照前面的项目进行演示
7.1.4.2.1 启动两个服务实例
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。
修改service_consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced
注解:
@SpringBootApplication
@EnableDiscoveryClient //springcloud提供的
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
@RestController
@RequestMapping("/consumer")
public class UserController {
@Resource
RestTemplate restTemplate;
@Autowired
DiscoveryClient discoveryClient;
@RequestMapping("/findbyid/{id}")
public User findById(@PathVariable Integer id){
/*
// 使用RestTemplate远程调用提供方
// User user = restTemplate.getForObject("http://localhost:8081/provider/findById/" + id, User.class);
// 根据服务名称获取实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
ServiceInstance instance = instances.get(0);
// 服务地址
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/provider/findById/" + id;
// 调用
User user = restTemplate.getForObject(url, User.class);
System.out.println(user);
return user;
*/
// 调用
User user = restTemplate.getForObject("http://service_provider/provider/findbyid/" + id, User.class);
/*
或者
String baseUrl=http://service_provider/provider/findById/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
*/
System.out.println(user);
return user;
}
}
启动
如果报以下错误
原因是:服务端的名称不能包含下划线
原配置信息
spring: application: name: eureka_server # 应用名称,会在Eureka中显示
修改
spring: application: name: eureka-server # 应用名称,会在Eureka中显示
7.1.4.3 负载均衡源码分析
首先简单了解Debug调试
在service-consumer中的UserController加入断点
step Into 进入
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor(responseType, this.getMessageConverters(), this.logger);
return this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, (Object[])uriVariables);
}
但是url还没有解析
主要执行了execute这个方法,进去看看这个方法执行细节
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI expanded = this.getUriTemplateHandler().expand(url, uriVariables);
return this.doExecute(expanded, method, requestCallback, responseExtractor);
}
url地址仍没有解析
主要执行了doExecute这个方法,进去看看这个方法执行细节
主要是request.execute,得到一个response,进去看看这个方法执行细节
内部执行方法, 传入一个头信息,进入这个方法中
又是一个内部执行,进去看看
一个执行器,进去看看
进去
是个与拦截器相关的方法
思考:
根据IP和端口号才能调用某个服务器,因为IP和端口号是这个服务器的明确地址
拦截器作用:过滤/拦截某个请求
传入的是service-provider,应该根据service-provider得到一个具体的IP和端口号
通过拦截器,拦截请求,然后再把service-provider解析成具体的IP和端口号
继续进入
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//取出serviceId,传入getLoadBalancer从而得到一个负载均衡器
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//传入负载均衡器的到一个Server 对象
Server server = this.getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, ribbonServer, request);
}
}
当得到Server 对象时,就已经解析出IP和端口号
在这里打个断点,并放过本次断点,继续运行
没有断点了,程序运行完毕
之后我们重新请求服务,看看端口是不是会变换(是否有负载均衡)
遇到第一个断点,放行它
到达第二个断点
发现端口号改为8082