随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此也不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构,还有在Google带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方,还是偏安一隅得过且过?
其实生活不止眼前的苟且,还有诗和远方。所以我们今天就回顾历史,看一看系统架构演变的历程;把握现在,学习现在最火的技术架构;展望未来,争取成为一名优秀的Java工程师。
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v14X6JVI-1594738647533)(assets/1545912768292.png)]
存在的问题:
当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mYdnAGhj-1594738647540)(assets/1545912974097.png)]
优点:
缺点:
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VYuX61vk-1594738647545)(assets/1545913905548.png)]
优点:
缺点:
SOA :面向服务的架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRZsl9ci-1594738647550)(assets/1525530804753.png)]
以前出现了什么问题?
服务治理要做什么?
缺点:
前面说的SOA,英文翻译过来是面向服务。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实缺有一些差别:
微服务的特点:
微服务结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kG9rwHrZ-1594738647555)(assets/1526860071166.png)]
无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?
常见的远程调用方式有以下2种:
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
现在热门的Rest风格,就可以通过http协议来实现。
如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。
相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在我们的项目中,我们会选择SpringCloud套件,因此我们会使用Http方式来实现服务间调用。
既然微服务选择了Http,那么我们就需要考虑自己来实现对请求和响应的处理。不过开源世界已经有很多的http客户端工具,能够帮助我们做这些事情,例如:
接下来,不过这些不同的客户端,API各不相同,
Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持:
我们导入课前资料提供的demo工程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssNQ2OJ9-1594738647557)(assets/1533686387565.png)]
首先在项目中注册一个RestTemplate
对象,可以在启动类位置注册:
@SpringBootApplication
public class HttpDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HttpDemoApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
在测试类中直接@Autowired
注入:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {
@Autowired
private RestTemplate restTemplate;
@Test
public void httpGet() {
User user = this.restTemplate.getForObject("http://localhost/user/8", User.class);
System.out.println(user);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0h5E7sU-1594738647560)(assets/1525573702492.png)]
学习完了Http客户端工具,接下来就可以正式学习微服务了。
微服务是一种架构方式,最终肯定需要技术架构去实施。
微服务的实现方式很多,但是最火的莫过于Spring Cloud了。为什么?
SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
Netflix:
以上只是其中一部分,架构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVi1oX2v-1594738647563)(assets/1525575656796.png)]
SpringCloud的版本命名比较特殊,因为它不是一个组件,而是许多组件的集合,它的命名是以A到Z的为首字母的一些单词(其实是伦敦地铁站的名字)组成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eu7SSKEd-1594738647566)(assets/1533782406077.png)]
我们在项目中,会是以Finchley的版本。
其中包含的组件,也都有各自的版本,如下表:
Component | Edgware.SR4 | Finchley.SR1 | Finchley.BUILD-SNAPSHOT |
---|---|---|---|
spring-cloud-aws | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-bus | 1.3.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-cli | 1.4.1.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-commons | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-contract | 1.2.5.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-config | 1.4.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-netflix | 1.4.5.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-security | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-cloudfoundry | 1.1.2.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-consul | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-sleuth | 1.3.4.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-stream | Ditmars.SR4 | Elmhurst.SR1 | Elmhurst.BUILD-SNAPSHOT |
spring-cloud-zookeeper | 1.2.2.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-boot | 1.5.14.RELEASE | 2.0.4.RELEASE | 2.0.4.BUILD-SNAPSHOT |
spring-cloud-task | 1.2.3.RELEASE | 2.0.0.RELEASE | 2.0.1.BUILD-SNAPSHOT |
spring-cloud-vault | 1.1.1.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-gateway | 1.0.2.RELEASE | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT |
spring-cloud-openfeign | 2.0.1.RELEASE | 2.0.2.BUILD-SNAPSHOT | |
spring-cloud-function | 1.0.0.RELEASE | 1.0.0.RELEASE | 1.0.1.BUILD-SNAPSHOT |
接下来,我们就一一学习SpringCloud中的重要组件。
首先,我们需要模拟一个服务调用的场景。方便后面学习微服务架构
微服务中需要同时创建多个项目,为了方便课堂演示,我们先创建一个父工程,然后后续的工程都以这个工程为父,实现maven的聚合。这样可以在一个窗口看到所有工程,方便我们讲解。在实际开发中,应该是每个微服务独立一个工程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gotALmm3-1594738647568)(assets/1529042834656.png)]
编写项目信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwHp8ye4-1594738647573)(assets/1529042868833.png)]
编写保存位置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3KfxYb5-1594738647575)(assets/1529042934471.png)]
然后将Pom修改成这样:
<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.0modelVersion>
<groupId>cn.itcast.demogroupId>
<artifactId>cloud-demoartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.0.6.RELEASEversion>
<relativePath/>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<spring-cloud.version>Finchley.SR2spring-cloud.version>
<mapper.starter.version>2.0.4mapper.starter.version>
<mysql.version>5.1.46mysql.version>
<pageHelper.starter.version>1.2.5pageHelper.starter.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>${mapper.starter.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
这里已经对大部分要用到的依赖的版本进行了 管理,方便后续使用
我们新建一个项目,对外提供查询用户的服务。
选中父工程:cloud-demo
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fl7jV7hn-1594738647578)(assets/1533786389928.png)]
填写module信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yR3PHhRb-1594738647580)(assets/1529043293477.png)]
注意,子模块要在父工程的下级目录:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ea7FFZGs-1594738647582)(assets/1529043334927.png)]
<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">
<parent>
<artifactId>cloud-demoartifactId>
<groupId>cn.itcast.demogroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>user-serviceartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
dependency>
dependencies>
project>
项目结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W40sYSlz-1594738647585)(assets/1529043450850.png)]
属性文件,这里我们采用了yaml语法,而不是properties:
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb01
username: root
password: root
mybatis:
type-aliases-package: cn.itcast.user.pojo
启动类:
@SpringBootApplication
@MapperScan("cn.itcast.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
实体类:
@Table(name = "tb_user")
@Data
public class User {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String userName; // 用户名
private String password; // 密码
private String name;// 姓名
private Integer age;// 年龄
private Integer sex;// 性别,1男性,2女性
private Date birthday;// 出生日期
private Date created;// 创建时间
private Date updated;// 更新时间
private String note;// 备注
}
mapper:
public interface UserMapper extends Mapper<User>{
}
Service:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
return userMapper.selectByPrimaryKey(id);
}
}
添加一个对外查询的接口:
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
项目结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlFaJCql-1594738647588)(assets/1529043755485.png)]
启动项目,访问接口:http://localhost:8081/user/7
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WL2hUdi-1594738647591)(assets/1525593139364.png)]
与上面类似,这里不再赘述,需要注意的是,我们调用user-service的功能,因此不需要mybatis相关依赖了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zTJfLaHB-1594738647594)(assets/1529043818258.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p0Iq1gul-1594738647596)(assets/1529043876180.png)]
pom:
<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">
<parent>
<artifactId>cloud-demoartifactId>
<groupId>cn.itcast.demogroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>consumer-demoartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
project>
结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WMBRui8J-1594738647598)(assets/1529043945158.png)]
首先在启动类中注册RestTemplate
:
@SpringBootApplication
public class ConsumerApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
实体类:
@Data
public class User {
private Long id;
private String userName; // 用户名
private String password; // 密码
private String name;// 姓名
private Integer age;// 年龄
private Integer sex;// 性别,1男性,2女性
private Date birthday;// 出生日期
private Date created;// 创建时间
private Date updated;// 更新时间
private String note;// 备注
}
编写controller,在controller中直接调用RestTemplate,远程访问user-service的服务接口:
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
String url = "http://localhost:8081/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
项目结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RmANX2KL-1594738647603)(assets/1533791573960.png)]
因为我们没有配置端口,那么默认就是8080,我们访问:http://localhost:8080/consumer/8
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wf1UNZQS-1594738647605)(assets/1533792850467.png)]
一个简单的远程服务调用案例就实现了。
简单回顾一下,刚才我们写了什么:
user-service:对外提供了查询用户的接口
consumer:通过RestTemplate访问http://locahost:8081/user/{id}
接口,查询用户数据
存在什么问题?
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
以上的问题,我们都将在SpringCloud中得到答案。
是Netflix公司出品,英文直译:发现了,找到了!
首先我们来解决第一问题,服务的管理。
问题分析
在刚才的案例中,user-service对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。
网约车
这就好比是 网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
Eureka做什么?
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”
机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务的自动注册、发现、状态监控。
基本架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k1xYgXjT-1594738647608)(assets/1544358842008.png)]
renewal:续约
接下来我们创建一个项目,启动一个EurekaServer:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OPYHhWEh-1594738647610)(assets/1529060127090.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BECJiBQP-1594738647612)(assets/1529060143421.png)]
依赖:
<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">
<parent>
<artifactId>cloud-demoartifactId>
<groupId>cn.itcast.demogroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>cn.itcast.demogroupId>
<artifactId>eureka-serverartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
project>
编写启动类:
@SpringBootApplication
@EnableEurekaServer // 声明这个应用是一个EurekaServer
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
编写配置:
server:
port: 10086
spring:
application:
name: eureka-server # 应用名称,会在Eureka中作为服务的id标识(serviceId)
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要写其它Server的地址。
defaultZone: http://127.0.0.1:10086/eureka
register-with-eureka: false # 不注册自己
fetch-registry: false #不拉取服务
启动服务,并访问:http://127.0.0.1:10086
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2g9GILJ-1594738647617)(assets/1525604959508.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfiNF0Uj-1594738647621)(assets/1533793804268.png)]
注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。
我们在user-service-demo中添加Eureka客户端依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
在启动类上开启Eureka客户端功能
通过添加@EnableDiscoveryClient
来开启Eureka客户端功能
@SpringBootApplication
@MapperScan("cn.itcast.user.mapper")
@EnableDiscoveryClient // 开启Eureka客户端发现功能
public class UserServiceDemoApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceDemoApplication.class, args);
}
}
编写配置
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb01
username: root
password: root
application:
name: user-service # 应用名称
mybatis:
type-aliases-package: com.leyou.userservice.pojo
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
注意:
重启项目,访问Eureka监控页面查看
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MeSiqdJk-1594738647624)(assets/1533793943618.png)]
我们发现user-service服务已经注册成功了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F9dWSvVp-1594738647626)(assets/1543548007606.png)]
接下来我们修改consumer-demo,尝试从EurekaServer获取服务。
方法与消费者类似,只需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了!
1)添加依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
2)在启动类开启Eureka客户端
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class ConsumerApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
3)修改配置:
server:
port: 8080
spring:
application:
name: consumer # 应用名称
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
4)修改代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例:
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
// 根据服务id(spring.application.name),获取服务实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 取出一个服务实例
ServiceInstance instance = instances.get(0);
// 从实例中获取host和port,组成url
String url = String.format("http://%s:%s/user/%s", instance.getHost(), instance.getPort(), id);
// 查询
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
5)Debug跟踪运行:
生成的URL:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jik0YsGE-1594738647632)(assets/1533824658440.png)]
这里的ip是本机的局域网ip。
访问结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tSByd3C0-1594738647635)(assets/1533824695239.png)]
接下来我们详细讲解Eureka的原理及配置。
Eureka架构中的三个核心角色:
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka-server
服务提供者
提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的user-service
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的consumer
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现高可用集群。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
而作为客户端,需要把信息注册到每个Eureka中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GEa1HqKz-1594738647636)(assets/1533825644505.png)]
如果有三个Eureka,则每一个EurekaServer都需要注册到其它几个Eureka服务中,例如:有三个分别为10086、10087、10088,则:
动手搭建高可用的EurekaServer
我们假设要搭建两条EurekaServer的集群,端口分别为:10086和10087
1)我们修改原来的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上,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
此时启动EurekaServer
2)另外一台配置恰好相反:
注意:idea中一个应用不能启动两次,我们需要重新配置一个启动器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cP52saRL-1594738647639)(assets/1525615070033.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KFpU0srE-1594738647641)(assets/1533825083030.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgmE8Nzo-1594738647643)(assets/1533825180015.png)]
修改刚刚的配置:
server:
port: 10087 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
然后启动即可。
3)启动测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5yDSFaq-1594738647645)(assets/1533825779695.png)]
4)客户端注册服务到集群
因为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结构中。
spring.application.name
属性locahost:user-service:8081
默认注册时使用的是主机名,如果我们想用ip进行注册,可以添加配置:
eureka:
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更倾向于使用ip,而不是host名
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
获取服务列表
当服务消费者启动是,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 30
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
失效剔除
有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒。
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlWpQc3g-1594738647646)(assets/1525618396076.png)]
这是触发了Eureka的自我保护机制。当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服务的失败容错。
我们可以通过下面的配置来关停自我保护:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
在刚才的案例中,我们启动了一个user-service,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。
但是实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TihtdDEM-1594738647660)(assets/1525619257397.png)]
接下来,我们就来使用Ribbon实现负载均衡。
首先我们启动两个user-service实例,一个8081,一个8082。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1YjDGjyq-1594738647662)(assets/1533826535457.png)]
Eureka监控面板:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5arVDT3s-1594738647664)(assets/1533826564383.png)]
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码:
在RestTemplate的配置方法上添加@LoadBalanced
注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
访问页面,查看结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ObDrafg-1594738647666)(assets/1533824695239.png)]
完美!
为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。
显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor
,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。
我们进行源码跟踪:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xNp6w6aa-1594738647668)(assets/1525620483637.png)]
继续跟入execute方法:发现获取了8082端口的服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ydlRUWUr-1594738647668)(assets/1525620787090.png)]
再跟下一次,发现获取的是8081:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TPj8PcnI-1594738647672)(assets/1525620835911.png)]
果然实现了负载均衡。
在刚才的代码中,可以看到获取服务使通过一个getServer
方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZgjONtIu-1594738647674)(assets/1525620835911.png)]
我们继续跟入:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VdiioGb-1594738647677)(assets/1544361421671.png)]
继续跟踪源码chooseServer方法,发现这么一段代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNQiKbXZ-1594738647679)(assets/1525622652849.png)]
我们看看这个rule是谁:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ODgkg5E9-1594738647681)(assets/1525622699666.png)]
这里的rule默认值是一个RoundRobinRule
,看类的介绍:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6gfrY2ny-1594738647684)(assets/1525622754316.png)]
这不就是轮询的意思嘛。
我们注意到,这个类其实是实现了接口IRule的,查看一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xg1uNh1O-1594738647686)(assets/1525622817451.png)]
定义负载均衡的规则接口。
它有以下实现:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2z9X9BOt-1594738647689)(assets/1525622876842.png)]
SpringBoot也帮我们提供了修改负载均衡规则的配置入口:
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
再次测试,发现结果变成了随机:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0wZm0pIw-1594738647691)(assets/1525623193949.png)]
附加:
Ribbon默认是采用懒加载,即第一次访问时才会去创建负载均衡客户端。往往会出现超时。如果需要采用饥饿加载,即项目启动即创建,可以这样配置:
ribbon:
eager-load:
enabled: true
Hystix,英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。
Hystrix也是Netflix公司的一款组件。
主页:https://github.com/Netflix/Hystrix/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AsfNlt0i-1594738647692)(assets/1525658740266.png)]
那么Hystix的作用是什么呢?具体要保护什么呢?
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WguUWJbr-1594738647694)(assets/1533829099748.png)]
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5tBcyJyP-1594738647696)(assets/1533829198240.png)]
例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XzyfhyDG-1594738647697)(assets/1533829307389.png)]
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
Hystix解决雪崩问题的手段主要是服务降级,包括:
线程隔离示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mOzbYIDA-1594738647700)(assets/1533829598310.png)]
解读:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有影响。
触发Hystix服务降级的情况:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
在启动类上添加注解:@EnableCircuitBreaker
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerApplication {
// ...
}
可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-czrHOj1X-1594738647702)(assets/1533856086255.png)]
因此,我们可以使用这个组合注解来代替之前的3个注解。
@SpringCloudApplication
public class ConsumerApplication {
// ...
}
当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystixCommond来完成:
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallBack")
public String queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallBack(Long id){
log.error("查询用户信息失败,id:{}", id);
return "对不起,网络太拥挤了!";
}
要注意,因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以我们把queryById的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
说明:
测试:
当user-service正常提供服务时,访问与以前一致。但是当我们将user-service停机时,会发现页面返回了降级处理信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wuR5QghG-1594738647705)(assets/1533857268880.png)]
我们刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以我们可以把Fallback配置加在类上,实现默认fallback:注意默认回调不能有参数
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallBack")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String defaultFallBack(){
return "默认提示:对不起,网络太拥挤了!";
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t5a63anP-1594738647707)(assets/1533858138646.png)]
在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystix的默认超时时长为1,我们可以通过配置修改这个值:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
这个配置会作用于全局所有方法。
为了触发超时,我们可以在user-service中休眠2秒:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userMapper.selectByPrimaryKey(id);
}
}
测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgLJEuzb-1594738647708)(assets/1533858701825.png)]
可以发现,请求的时长已经到了2.01s,证明配置生效了。
如果把休眠时间修改到2秒以下,又可以正常访问了。
熔断器,也叫断路器,其英文单词为:Circuit Breaker
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnax8E23-1594738647710)(assets/1525658640314.png)]
Hystix的熔断状态机模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V9hstXXC-1594738647712)(assets/1533830345149.png)]
状态机有3个状态:
为了能够精确控制请求的成功或失败,我们在consumer的调用业务中加入一段逻辑:
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了清空user-service中的休眠逻辑)
我们准备两个请求窗口:
熔断器的默认触发阈值是20次请求,不好触发。休眠时间时5秒,时间太短,不易观察,为了测试方便,我们可以通过配置修改熔断策略:
circuitBreaker:
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 10000
errorThresholdPercentage: 50
解读:
当我们疯狂访问id为1的请求时(超过10次),就会触发熔断。断路器会端口,一切请求都会被降级处理。
此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有20毫秒左右:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FtuAdjCH-1594738647713)(assets/1533859591238.png)]
}
}
测试:
[外链图片转存中...(img-MgLJEuzb-1594738647708)]
可以发现,请求的时长已经到了2.01s,证明配置生效了。
如果把休眠时间修改到2秒以下,又可以正常访问了。
## 8.4.服务熔断:
### 8.4.1.熔断原理
熔断器,也叫断路器,其英文单词为:Circuit Breaker
[外链图片转存中...(img-gnax8E23-1594738647710)]
Hystix的熔断状态机模型:
[外链图片转存中...(img-V9hstXXC-1594738647712)]
状态机有3个状态:
- Closed:关闭状态(断路器关闭),所有请求都正常访问。
- Open:打开状态(断路器打开),所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到**阈值**,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,**open状态不是永久的**,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则继续保持打开,再次进行5秒休眠计时。
### 8.4.2.动手实践
为了能够精确控制请求的成功或失败,我们在consumer的调用业务中加入一段逻辑:
```java
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了清空user-service中的休眠逻辑)
我们准备两个请求窗口:
熔断器的默认触发阈值是20次请求,不好触发。休眠时间时5秒,时间太短,不易观察,为了测试方便,我们可以通过配置修改熔断策略:
circuitBreaker:
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 10000
errorThresholdPercentage: 50
解读:
当我们疯狂访问id为1的请求时(超过10次),就会触发熔断。断路器会端口,一切请求都会被降级处理。
此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有20毫秒左右:
[外链图片转存中…(img-FtuAdjCH-1594738647713)]