一、应用架构的演变
由于互联网的高速发展,单体项目不足以支撑大数据以及高并发,应用的架构随之进行演变,演变过程如下图:
1.单体应用架构
1.1.什么是单体架构
一种把系统中所有功能,模块耦合在一个在应用中的构架方式
1.2.单体架构的优缺点
一:优点
1.易于开发:架构简单,技术成本低
2.易于测试:所有功能在一个项目中,方便测试
3.易于部署:一个tomcat就可以实现部署,简单方便
二:缺点
1.代码臃肿,不宜维护
2.有单机故障风险
3.系统扩展性能差
4.模块/业务耦合度高
5.对大数据以及高并发处理差
1.3.集群和多实例
1.3.1 集群概念
把项目复制多分并进行部署,可以解决高并发问题,使用集群的同时需要结合负载均衡一起使用,负载均衡(Nginx)算法:随机、轮询、权重等算法。
一:集群的优点
1.解决单机故障
2.解决高并发问题
二:缺点
1.技术成本高、运维成本高、硬件成本高
2.分布式和SOA
2.1 分布式概念
按照业务拆分成多个子系统,使用多个服务器部署
SOA概念
把业务服务化(把功能暴露出来,直接调用),业务调用更加方便,功能重组
2.2 作用
解决高并发,代码臃肿问题
2.3 缺点
运维成本高、硬件成本高、技术成本高
3. 微服务
3.1 概念 :基于SOA/分布式 , 系统拆分成多个服务,每个服务的独立的负责一部分功能,有自己的数据库,服务之间项目调用
3.2 优势
解决高并发,解决代码臃肿,单个服务业务简单,扩展性强,对敏捷开发支持友好
3.3 缺点
运维成本高,硬件成本高,技术成本高 , 服务调用网络开销 ,事务处理麻烦
二.SpringCloud介绍
1.1.SpringCloud基本概念
Spring cloud是一个基于Spring Boot实现的服务治理工具包
,用于微服务架构中管理和协调服务的
。Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等
,都可以用Spring Boot的开发风格做到一键启动和部署。通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。有了SpringCloud之后,让微服务架构的落地变得更简单。
1.2. SpringCloud常用组件
Netflix Eureka(注册中心)
当我们的微服务过多的时候,管理服务的通信地址是一个非常麻烦的事情,Eureka就是用来管理微服务的通信地址清单的,有了Eureka之后我们通过服务的名字就能实现服务的调用。
Netflix Ribbon\Feign : 客户端负载均衡
Ribbon和Feign都是客户端负载均衡器,它的作用是在服务发生调用的时候帮我们将请求按照某种规则分发到多个目标服务器上,简单理解就是用来解决微服务之间的通信问题。
Netflix Hystrix :断路器
微服务的调用是非常复杂的,有的时候一个请求需要很多的微服务共同完成,那么一旦某个服务发生故障,导致整个调用链上的微服务全都出现异常,甚至导致整个微服务架构瘫痪。Hystrix就是用来解决微服务故障,保护微服务安全的组件。
Netflix Zuul : 服务网关
zuul作为服务网关,我们可以把它看作是微服务的大门,所有的请求都需要经过zuul之后才能到达目标服务,根据这一特性,我们可以把微服务公共的是事情交给zuul统一处理,如:用户鉴权,请求监控等。
Spring Cloud Config :分布式配置(配置中心)
微服务架构中的服务实例非常的多,服务的配置文件分散在每个服务中,每次修改服务的配置文件和重新服务实例都是一个很麻烦的工作,Spring Cloud Config作为分布式配置管理中心就是用来统一的管理服务的配置文件。
Spring Cloud Bus : 消息总线
消息总线是在微服务中给各个微服务广播消息的一个组件,我们使用消息总线构建一个消息中心,其他微服务来接入到消息中心,当消息总线发起消息,接入的微服务都可以收到消息从而进行消费。
Spring Cloud sleuth :微服务链路追踪
当我们的应用采用微服务架构之后,后台可能有几十个甚至几百个服务在支撑,一个请求请求可能需要多次的服务调用最后才能完成,链路追踪的作用就是来监控维护之间的调用关系,让程序员方便直观的感受到一个请求经历了哪些微服务,以及服务的请求时间,是否有异常等。
三. 服务注册与发现
1. Eureka介绍
1.1 管理中的服务之间的通讯地址
1.2. Eureka的工作原理
1.服务注册:
客户端启动向服务端注册(服务名,IP,端口等),服务端维护一个服务注册表
2.服务发现:
客户端30s/次拉取注册表缓存到本地,服务调用的时候,去注册表中根据服务名查找服务实例,拿到通信地址,发起HTTP请求
3.服务续约:
客户端30s/次向服务端发送心跳续约请求,报告自己的健康状态,超过3次续约失败,会被剔除
4.服务线下:
微服务(EurekaClient)关闭服务前向注册中心发送下线请求,注册中心(EurekaServer)接受到下线请求负责将该服务实例从注册列表剔除
2. EurekaServer实战
3.EurekaClient实战-用户服务
参照官方文档 : 11. Service Discovery: Eureka Clients 根据上一章节我们的Eureka的理解,Eureka分为服务端和客户端,服务端已经搭建成功,我们来搭建客户端。再来看一下我们之前的那张图:
其实我们的用户服务 springcloud-user-server-1020 ,订单服务springcloud-order-server-1030 两个工程都是EurekaClient客户端,都需要去集成EurekaClient,我们先从springcloud-user-server-1020 下手 。
3.1.导入依赖
修改springcloud-user-server-1020,导入EurekaClient基础依赖:spring-cloud-starter-netflix-eureka-client,导入web的基础依赖:spring-boot-starter-web,具体的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"> <parent> <artifactId>springcloud-parent</artifactId> <groupId>cn.itsource.springboot</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud-user-server-1020</artifactId> <name>springcloud-user-server-1020</name> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>
提示:不要忘记导入web的基础依赖 spring-boot-starter-web
3.2.主配置类
我们可以在主配置通过注解@EnableEurekaClient标记服务作为Eureka客户端
/ * 用户的启动类 * @EnableEurekaClient: 标记该应用是 Eureka客户端 */ @SpringBootApplication @EnableEurekaClient public class UserServerApplication1020 { public static void main( String[] args ) { SpringApplication.run(UserServerApplication1020.class); } }
提示:主配置类通过打@EnableEurekaClient注解开启EurekaClient客户端功能,当然如果不打这个标签也能实现功能,因为导入spring-cloud-starter-netflix-eureka-client 依赖后,默认就开启了EurekaClient
3.3.application.yml配置
在配置文件中,我们需要通过eureka.client.serviceUrl配置EurekaServer的地址,EurekaClient根据该地址把自己注册给服务端。
#注册到EurekaServer eureka: client: serviceUrl: defaultZone: http://localhost:1010/eureka/ spring: application: name: user-server server: port: 1020
提示:serviceUrl是EurekaServer注册中的地址,主机和端口都应该指向springcloud-eureka-server-1010工程,这里额外指定了服务的名字,和端口,这些信息都会被注册到EurekaServer
3.4.测试EurekaClient
启动springcloud-eureka-server-1010 , 启动springcloud-user-server-1020 , 浏览器再次访问http://localhost:1010,那你应该可以看到我们的user-server服务已经被注册到EurekaServer。如下:
3.5.使用IP进行注册
默认情况下EurekaClient使用hostname进行注册到EurekaServer,我们希望使用ip进行注册,可以通过配置eureka.instance.prefer-ip-address=true
来指定,同时为了方便区分和管理服务实例,我们指定服务的实例ID,通过eureka.instance.instance-id为user-serer:1020
来指定,具体配置如下:
#注册到EurekaServer eureka: client: serviceUrl: defaultZone: http://localhost:1010/eureka/ instance: prefer-ip-address: true #使用ip地址进行注册 instance-id: user-server:1020 #实例ID spring: application: name: user-server server: port: 1020
重启springcloud-user-server-1020 工程,访问注册中心,可以看到实例ID已经发生改变,如下:
4.Eureka Client实战-订单服务
订单服务和用户服务的做法一样,只是yml配置文件中有些稍微的不同。
4.1.导入依赖
同用户服务一样,省略...
...省略部分不重要的内容... <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
4.2.主配置类
同用户服务一样,省略...
/ * 订单的启动类 * @EnableEurekaClient: 标记该应用是 Eureka客户端 */ @SpringBootApplication @EnableEurekaClient public class OrderServerApplication1030 { public static void main( String[] args ) { SpringApplication.run(OrderServerApplication1030.class); } }
4.3.application.yml配置
订单服务除了服务名,端口和用户服务不一样,其他的都一样,如下:
eureka: client: serviceUrl: defaultZone: http://localhost:1010/eureka/ instance: prefer-ip-address: true #使用ip地址进行注册 instance-id: order-server:1030 #实例ID spring: application: name: order-server server: port: 1030
4.4.测试
启动订单服务,访问Eureka Server的监控界面:http://localhost:1010,可以看到订单服务也注册进去了。
4.5.做个小结
到这里Eureka服务注册与发现案例已经完成,服务端和客户端的搭建都相对简单,一般都是导个包,打个标签,配置文件都搞定了,作为一个合格的程序员,我们不能光停留在用的层面,它的一些重要工作方式与思想也是需要我们去掌握。
四.RestTemplate服务通信
1.如何实现服务通信
1.1.流程说明
上一章节我们对Eureka Server做了高可用集群搭建,这一章节我们来实现简单版的服务通信。
目前除了Eureka Server以外我们的微服务有订单服务springcloud-order-server-1030,和用户服务springcloud-user-server-1020 , 我们就用这两个服务来演示微服务的通信,他们的调用关系应该是:浏览器 -> 订单服务 -> 用户服务,如下图:
这里订单服务通过RestTemplate向用户服务发起调用,目的是要获取到用户服务返回的User对象,最终是需要浏览器获取到User。
1.2.解决方案
用户服务需要提供User对象,我们需要为其编写Controller接口,编写相关方法返回User,订单服务需要从用户服务获取到User对象,而浏览器需要访问订单服务获取到User对象,所以订单服务也需要编写Controller接口供浏览器来调用。
我们发现不管是用户服务还是订单服务都需要用到User对象,那么是不是在用户服务和订单服务都需要创建User的模型?当然没必要,公共的东西就是要抽取,所以我们会把User对象封装在一个公共的模块 springcloud-user-common中然后让用户服务和订单服务都去依赖这个模块。
1.3.RestTemplate介绍
微服务的通信协议主流的有RPC,Http,SpringCloud是基于Http Restful 风格 ,在Java中发起一个Http请求的方式很多,比如 Apache的HttpClient , OKHttp等等 。Spring为我们封装了一个基于Restful的使用非常简单的Http客户端工具 RestTemplate ,我们就用它来实订单服务和用户服务的通信。需要注意的是,RestTmplate本身不具备服务发现和负载均衡器的功能,我们本章的案例只是演示在订单服务中使用RestTemplate基于ip和端口的方式向用户服务发起调用,即:不走注册中心,不使用服务发现方式。 5.4.编码实战
2.编码实战
2.1.搭建公共模块
创建工程模块 springcloud-user-common ,效果如下:
springcloud-parent springcloud-eureka-server-1010 springcloud-order-server-1030 springcloud-user-common //公共User模块 springcloud-user-server-1020
在springcloud-user-common中创建User对象如下
package cn.itsource.springboot.domain; public class User { private Long id; private String username; private String desc; public User() { } public User(Long id, String username, String desc) { this.id = id; this.username = username; this.desc = desc; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } }
2.2.用户和订单依赖User模块
修改工程springcloud-order-server-1030 , springcloud-user-server-1020在他们的pom.xml都导入springcloud-user-common 模块
<dependency> <groupId>cn.itsource.springboot</groupId> <artifactId>springcloud-user-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
2.3.用户服务返回User
修改 springcloud-user-server-1020 工程,编写controller,返回User对象
//用户服务:暴露接口给订单访问 @RestController public class UserController { //订单服务来调用这个方法 http://localhost:1020/user/10 // @GetMapping(value = "/user/{id}" ) @RequestMapping(value = "/user/{id}",method = RequestMethod.GET) public User getById(@PathVariable("id")Long id){ //根据id去数据库查询User return new User(id,"zs:"+id,"我是zs"); } }
2.4.订单服务获取User
在订单服务中需要使用RestTemplate调用用户服务,我们需要把RestTmplate配置成Bean方便使用(当然也可以不创建Bean,用的时候直接new对象也可以) ,修改工程springcloud-order-server-1030在主配置类配置RestTemplate如下:
/** * 订单的启动类 */ @SpringBootApplication @EnableEurekaClient public class OrderServerApplication1030 { //配置一个RestTemplate ,Spring封装的一个机遇Restful风格的http客户端 工具 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } public static void main( String[] args ) { SpringApplication.run(OrderServerApplication1030.class); } }
创建controller,通过RestTemplate调用用户服务,代码如下:
//订单服务 @RestController public class OrderController { //需要配置成Bean @Autowired private RestTemplate restTemplate ; //浏览器调用该方法 @RequestMapping(value = "/order/{id}",method = RequestMethod.GET) public User getById(@PathVariable("id")Long id){ //发送http请求调用 user的服务,获取user对象 : RestTemplate //目标资源路径:user的ip,user的端口,user的Controller路径 String url = "http://localhost:1020/user/"+id; //发送http请求 return restTemplate.getForObject(url, User.class); } }
这里的url是用户服务获取user资源的路径,通过RestTmplate发起Http调用,拿到User对象后返回给浏览器。
2.5.测试服务通信
依次启动Eureka Server注册中心(不启动也行) , 用户服务 ,订单服务 , 浏览器访问订单服务:http://localhost:1030/order/1 , 返回结果:
思考,这种方式有什么问题?
五.Ribbon客户端负载均衡
1.基本概念
1.1.为什么要Ribbon
我们知道,为了防止应用出现单节点故障问题,同时为了提高应用的作业能力,我们需要对应用做集群 ,如果我们对user-server(用户服务)做了集群 ,那么这个时候回衍生出一些问题:现在有两个user-server(用户服务)就意味着有两个user-server(用户服务)的通信地址,我的order-server(订单服务)在向user-server(用户服务)发起调用的时候该访问哪个?如何访问?这个时候就需要有一个组件帮我们做请求的分发,即:负载均衡器,而Ribbon就是一个 - 客户端负载均衡器。
1.2.什么是Ribbon
Ribbon是Netflix发布的云中间层服务开源项目,主要功能是提供客户端负载均衡算法
。Ribbon客户端组件提供一系列完善的配置项,如,连接超时,重试
等。简单的说,Ribbon是一个客户端负载均衡器,Ribbon可以按照负载均衡算法(如简单轮询,随机连接等)向多个服务发起调用
(正好可以解决上面的问题),我们也很容易使用Ribbon实现自定义的负载均衡算法
。
1.3.Ribbon的工作机制
如下图,我们将user-server(用户服务)做集群处理,增加到2个节点(注意:两个user-server(用户服务)的服务名要一样,ip和端口不一样),在注册中心的服务通信地址清单中user-server(用户服务)这个服务下面会挂载两个通信地址 。 order-server(订单服务)会定时把服务通信地址清单拉取到本地进行缓存
, 那么当order-server(订单服务)在向user-server(用户服务)发起调用时,需要指定服务名为 user-server(用户服务)
;那么这个时候,ribbon会根据user-server(用户服务)这个服务名找到两个order-server的通信地址
, 然后ribbon会按照负载均衡算法(默认轮询)选择其中的某一个通信地址,发起http请求实现服务的调用
,如下图:
在理解了Ribbon工作机制之后,我们就来编码实战上图描述的场景。
2.提供者user-server(用户服务)集群
2.1.服务集群方案
使用SpringBoot多环境配置方式集群,一个配置文件配置多个order-server环境 ,需要注意的是集群中的多个服务名(spring.application.name)应该一样,我们把相同的东西提取到最上面,不同的东西配置在各自的环境中
2.2.用户服务集群配置
修改 application.yml 如下:
#注册到EurekaServer eureka: client: serviceUrl: defaultZone: http://peer1:1010/eureka/,http://peer2:1011/eureka/,http://peer3:1012/eureka/ #使用ip地址进行注册 instance: prefer-ip-address: true spring: application: name: user-server #服务名都叫user-server profiles: active: user-server1 --- server: port: 1020 eureka: instance: instance-id: user-server:1020 spring: profiles: user-server1 --- server: port: 1021 eureka: instance: instance-id: user-server:1021 spring: profiles: user-server2
用户服务集群配置成功,启动方式同注册中心集群一样。需要配置服务运行多实例启动,启动时需要注意修改spring.profiles.active的值来切换不同的实例。
2.3.修改Controller
为了后续测试的时候方便区分不同的用户服务实例,这里我们在Controller中读取应用的端口随User返回,在启动不同的用户服务实例的时候端口是不同的,修改后的代码如下:
//用户服务:暴露接口给订单访问 @RestController public class UserController { //加载端口 @Value("${server.port}") private int port; //订单服务来调用这个方法 http://localhost:1020/user/10 // @GetMapping(value = "/user/{id}" ) @RequestMapping(value = "/user/{id}",method = RequestMethod.GET) public User getById(@PathVariable("id")Long id){ //根据id去数据库查询User System.out.println(port); return new User(id,"zs:"+id,"我是zs,port:"+port); //端口随User返回 } }
3.消费者Order-server集成Ribbon
Ribbon集成官方文档:16. Client Side Load Balancer: Ribbon
修改springcloud-order-server-1030工程,集成Ribbon
3.1.导入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
3.2.开启负载均衡
修改RestTemplate的Bean的定义方法,加上Ribbon的负载均衡注解@LoadBalanced赋予RestTemplate有负债均衡的能力。
/** * 订单的启动类 */ @SpringBootApplication @EnableEurekaClient public class OrderServerApplication1030 { //配置一个RestTemplate ,Spring封装的一个机遇Restful风格的http客户端 工具 //@LoadBalanced :让RestTemplate有负载均衡的功能 @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } //省略...
3.3.修改Controller调用方式
在之前的案例中,我们调用用户服务是通过用户服务的主机加上端口“localhost:1020”的调用方式,现在我们把 “localhost:1020” 修改为 用户服务的服务名 。底层会通过服务发现的方式使用Ribbin进行负载均衡调用。
@RestController public class OrderController { //需要配置成Bean @Autowired private RestTemplate restTemplate ; //浏览器调用该方法 @RequestMapping(value = "/order/{id}",method = RequestMethod.GET) public User getById(@PathVariable("id")Long id){ //发送http请求调用 user的服务,获取user对象 : RestTemplate //user的ip,user的端口,user的Controller路径 //String url = "http://localhost:1020/user/"+id; String url = "http://user-server/user/"+id; //发送http请求 return restTemplate.getForObject(url, User.class); } }
3.4.测试Ribbon
分别启动EurekaServer注册中心 ,启动两个UserServer用户服务,启动OrderServer订单消费者服务,浏览器访问订单服务:http://localhost:1030/order/1 ,发送多次请求。
观察响应的结果中的端口变化 - 端口会交替出现1020,,1021我们可以推断出Ribbon默认使用的是轮询策略。
4.负载均衡算法
4.1.Ribbon内置算法
Ribbon内置7种负载均衡算法,每种算法对应了一个算法类如下:
4.2.配置负载均衡算法
Ribbon可以进行全局负载均衡算法配置,也可以针对于具体的服务做不同的算法配置。同时可以使用注解方式和yml配置方式来实现上面两种情况。
1.注解全局配置
随机算法的效果最好演示,我们把负载均衡算法修改成随机算法,只需要RandomRule配置成Bean即可,修改主配置类如下:
/** * 订单的启动类 */ @SpringBootApplication @EnableEurekaClient public class OrderServerApplication1030 { //配置一个RestTemplate ,Spring封装的一个机遇Restful风格的http客户端 工具 //@LoadBalanced :让RestTemplate有负载均衡的功能 @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } //负载均衡算法 @Bean public RandomRule randomRule(){ return new RandomRule(); } //省略...
测试 重启订单服务,访问http://localhost:1030/order/1 ,发送多次请求应该可以看到结果中的端口随机变动。
2.yml方式配置负载均衡算法
配置全局Ribbon算法
ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
配置某个服务的Ribbon算法
user-server: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
4.做个总结
上面我们介绍了通过注解方式进行负载均衡策略全局配置和针对某个服务的负载均衡配置,也介绍了通过yml方式配置,我们可以根据项目情况选择其中一种配置方式即可。
5.Ribbon调优配置
5.1.超时配置
使用Ribbon进行服务通信时为了防止网络波动造成服务调用超时,我们可以针对Ribbon配置超时时间以及重试机制
ribbon: ReadTimeout: 3000 #读取超时时间 ConnectTimeout: 3000 #链接超时时间 MaxAutoRetries: 1 #重试机制:同一台实例最大重试次数 MaxAutoRetriesNextServer: 1 #重试负载均衡其他的实例最大重试次数 OkToRetryOnAllOperations: false #是否所有操作都重试,因为针对post请求如果没做幂等处理可能会造成数据多次添加/修改
当然也可以针对具体的服务进行超时配置:如"<服务名>.ribbon..."
5.2.饥饿加载
我们在启动服务使用Ribbon发起服务调用的时候往往会出现找不到目标服务的情况,这是因为Ribbon在进行客户端负载均衡的时候并不是启动时就创建好的,而是在实际请求的时候才会去创建,所以往往我们在发起第一次调用的时候会出现超时导致服务调用失败,我们可以通过设置Ribbon的饥饿加载来改善此情况,即在服务启动时就把Ribbon相关内容创建好。
ribbon: eager-load: enabled: true #开启饥饿加载 clients: user-server #针对于哪些服务需要饥饿加载