一、微服务
1.1、微服务简介
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies. -----[摘自官网]
简而言之,微服务架构风格是一种将单个应用程序开发为“一套小型服务”的方法,每个服务“运行在自己的进程中”,并通过轻量级机制(通常是HTTP资源API)进行通信。这些服务“围绕业务功能构建”,并通过全自动部署机制“独立部署”。“这些服务只有最低限度的集中管理”,可能是用不同的编程语言编写的,并使用不同的数据存储技术。
微服务是一种架构,这种架构是将单个的整体应用程序分割成更小的项目关联的独立的服务。一个服务通常实现一组独立的特性或功能,包含自己的业务逻辑和适配器。各个微服务之间的关联通过暴露api来实现。这些独立的微服务不需要部署在同一个虚拟机,同一个系统和同一个应用服务器中。
1.2、为什么是微服务
1.2.1、单体应用
一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后这个项目部署在一台服务器上就好了。整个项目所有的服务都由这台服务器提供。这就是单机结构。
单体应用的优点在于:单一架构模式在项目初期很小的时候开发方便,测试方便,部署方便,运行良好。
他的缺点也很明显:
- 应用随着时间的推进,加入的功能越来越多,最终会变得巨大,一个项目中很有可能数百万行的代码,互相之间繁琐的jar包。
- 久而久之,开发效率低,代码维护困难。
- 如果想整体应用采用新的技术,新的框架或者语言,那是不可能的。
- 任意模块的漏洞或者错误都会影响这个应用,降低系统的可靠性。
1.2.2、分布式
由于整个系统运行需要使用到Tomcat和MySQL,单台服务器处理的能力有限,2G的内存需要分配给Tomcat和MySQL使用,随着业务越来越复杂,请求越来越多.。内存越来越不够用了,所以这时候我们就需要进行分布式的部署。
我们进行一个评论的请求,这个请求是需要依赖分布在两台不同的服务器的组件[Tomat和MySQL],才能完成的.。所以叫做分布式的系统。
分布式和单体项目最大的区别在于分布式的项目是分开部署的,比如说把数据库单独放在一台服务器上。
1.2.3、集群
在上面的图解中其实是存在问题的,比如Tomcat存在单点故障问题,一旦Tomcat所在的服务器宕机不可用了,我们就无法提供服务了,所以针对单点故障问题,我们会使用集群来解决.那什么是集群模式呢?
单机处理到达瓶颈的时候,你就把单机复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍(有几个节点就相当于提升了这么多倍)。
但问题是用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个牛逼了名字——负载均衡服务器。
1.2.4、总结
用一张图总结单体项目、集群、分布式的不同:
1.3、系统架构的演变
架构的演变大致为:单一应用架构 ===>
垂直应用架构 ===>
分布式服务架构 ===>
流动计算架构微服务架构 ===>
[未知]
1.3.1、单一应用架构
互联网早期,一般的网站应用流量较小,只需一个应用,将所有功能代码都部署在一起就可以,这样可以减少开发、部署和维护的成本。
比如说一个电商系统,里面会包含很多用户管理,商品管理,订单管理,物流管理等等很多模块,
我们会把它们做成一个web项目,然后部署到一台tomcat服务器上。
他的优点在于:
- 项目架构简单,小型项目的话, 开发成本低。
- 项目部署在一个节点上,维护容易。
他的缺点也是显而易见的:
-
全部功能集成在一个工程中,对于大型项目来讲不易开发和维护。
-
项目模块之间紧密耦合,单点容错率低。
-
无法针对不同模块进行针对性优化和水平扩展。
1.3.2、垂直应用架构
随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候会发现并不是所有的模块都会有比较大的访问量.
还是以上面的电商为例子, 用户访问量的增加可能影响的只是用户和订单模块, 但是对消息模块的影响就比较小. 那么此时我们希望只多增加几个订单模块, 而不增加消息模块. 此时单体应用就做不到了, 垂直应用就应运而生了.
所谓的垂直应用架构,就是将原来的一个应用拆成互不相干的几个应用,以提升效率。比如我们可以将上面电商的单体应用拆分成:
-
电商系统(用户管理 商品管理 订单管理)
-
后台系统(用户管理 订单管理 客户管理)
-
CMS系统(广告管理 营销管理)
这样拆分完毕之后,一旦用户访问量变大,只需要增加电商系统的节点就可以了,而无需增加后台和CMS的节点。
他的优点在于:
- 系统拆分实现了流量分担,解决了并发问题,而且可以针对不同模块进行优化和水平扩展。
- 一个系统的问题不会影响到其他系统,提高容错率。
缺点:
- 系统之间相互独立, 无法进行相互调用。
- 系统之间相互独立, 会有重复的开发任务
1.3.3、分布式架构
当垂直应用越来越多,重复的业务代码就会越来越多。这时候,我们就思考可不可以将重复的代码抽取出来,做成统一的业务层作为独立的服务,然后由前端控制层调用不同的业务层服务呢?
这就产生了新的分布式系统架构。它将把工程拆分成表现层和服务层两个部分,服务层中包含业务逻辑。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。
优点:
- 抽取公共的功能为服务层,提高代码复用性。
缺点:
- 系统间耦合度变高,调用关系错综复杂,难以维护。
1.3.4、SOA架构
在分布式架构下,当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心对集群进行实时管理。此时,用于资源调度和治理中心(SOA Service Oriented Architecture,面向服务的架构)是关键。
优点:
- 使用注册中心解决了服务间调用关系的自动调节
缺点:
- 服务间会有依赖关系,一旦某个环节出错会影响较大( 服务雪崩 )。
- 服务关心复杂,运维、测试部署困难。
1.3.4、微服务架构
微服务架构在某种程度上是面向服务的架构,它更加强调服务的"彻底拆分"。
优点:
- 服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展。
- 微服务之间采用RESTful等轻量级Http协议相互调用。
- 服务各自有自己单独的职责,服务之间松耦合,避免因一个模块的问题导致服务崩溃
缺点:
- 分布式系统开发的技术成本高(容错、分布式事务等)。
- 服务治理和服务监控关键。
- 多服务运维难度,随着服务的增加,运维的压力也在增大
1.4、微服务需要解决的问题
微服务架构, 简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。
微服务架构的常见问题
一旦采用微服务系统架构,就势必会遇到这样几个问题:
-
这么多小服务,如何管理他们?
-
这么多小服务,他们之间如何通讯?
-
这么多小服务,客户端怎么访问他们?
-
这么多小服务,一旦出现问题了,应该如何自处理?
-
这么多小服务,一旦出现问题了,应该如何排错?
对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。
1.5、微服务架构的常见概念
1.5.1、服务治理
服务治理就是进行服务的自动化管理,其核心是服务的注册与发现。
- 服务注册:服务实例将自身服务信息注册到注册中心。
- 服务发现:服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求他们提供服务。
- 服务剔除:服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。
1.5.2、服务调用
在微服务架构中,通常存在多个服务之间的远程调用的需求,目前1主流的远程调用的技术有基于HTTP请求的RESTFul接口及基于TCP的RPC协议。
- REST(Representational State Transfer):这是一种HTTP调用的格式,更标准,更通用,无论哪种语言都支持http协议。
- RPC(Remote Promote Call):一种进程间通信方式。允许像调用本地服务一样调用远程服务。RPC框架的主要目标就是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式、序列化方式和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。
他们之间的区别于联系:
比较项 | RESTFul | RPC |
---|---|---|
通讯协议 | HTTP | 一般是TCP |
性能 | 略低 | 较高 |
灵活度 | 高 | 低 |
应用 | 微服务架构 | SOA架构 |
1.5.3、服务网关
随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:
- 客户端需要调用不同的url地址,增加难度。
- 在一定的场景下,存在跨域请求的问题。
- 每个微服务都需要进行单独的身份认证。
为了解决这些问题,API网关顺势而生。
API网关直面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
1.5.4、服务容错
在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。
我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:
- 不被外界环境影响。
- 不被上游请求压垮。
- 不被下游响应拖垮。
1.5.5、链路追踪
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪。
1.6、微服务常见的解决方案
1.6.1、ServiceComb
Apache ServiceComb,前身是华为云的微服务引擎 CSE (Cloud Service Engine) 云服务,是全球首个Apache微服务顶级项目。它提供了一站式的微服务开源解决方案,致力于帮助企业、用户和开发者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理。
1.6.2、SpringCloud
Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
1.6.3、SpringCloud Alibaba
 Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
二、Spring Cloud
2.1、什么是SpringCloud
SpringCloud是一个含概多个子项目的开发工具集,集合了众多的开源框架,他利用了Spring Boot开发的便利性实现了很多功能,如服务注册,服务注册发现,负载均衡等.SpringCloud在整合过程中主要是针对Netflix(奈飞)开源组件的封装.SpringCloud的出现真正的简化了分布式架构的开发。
NetFlix 是美国的一个在线视频网站,微服务业的翘楚,他是公认的大规模生产级微服务的杰出实践者,NetFlix的开源组件已经在他大规模分布式微服务环境中经过多年的生产实战验证,因此Spring Cloud中很多组件都是基于NetFlix组件的封装。
2.2、核心组件
- eurekaserver、consul、nacos:服务注册中心组件。
- rabbion & openfeign:服务负载均衡 和 服务调用组件。
- hystrix & hystrix dashboard:服务断路器和服务监控组件。
- zuul、gateway:服务网关组件。
- config:统一配置中心组件。
- bus:消息总线组件。
2.3、版本命名
SpringCloud是一个由众多独立子项目组成的大型综合项目,原则每个子项目上有不同的发布节奏,都维护自己发布版本号。为了更好的管理springcloud的版本,通过一个资源清单BOM(Bill of Materials),为避免与子项目的发布号混淆,所以没有采用版本号的方式,而是通过命名的方式。这些名字是按字母顺序排列的。如伦敦地铁站的名称(“天使”是第一个版本,“布里斯顿”是第二个版本,"卡姆登"是第三个版本)。当单个项目的点发布累积到一个临界量,或者其中一个项目中有一个关键缺陷需要每个人都可以使用时,发布序列将推出名称以“.SRX”结尾的“服务发布”,其中“X”是一个数字。
伦敦地铁站的名字大致有如下:Angel、Brixton、Camden、Dalston、Edgware、Finchley、Greenwich、Hoxton。
2.4、版本选择
由于SpringCloud的版本是必须和SpringBoot的版本对应的,所以必须要根据SpringCloud版本来选择SpringBoot的版本。
三、SpringCloud Alibaba
3.1、简介
Spring Cloud Alibaba是Spring Cloud下的一个子项目,Spring Cloud Alibaba为分布式应用程序开发提供了一站式解决方案,它包含开发分布式应用程序所需的所有组件,使您可以轻松地使用Spring Cloud开发应用程序,使用Spring Cloud Alibaba,您只需要添加一些注解和少量配置即可将Spring Cloud应用程序连接到Alibaba的分布式解决方案,并使用Alibaba中间件构建分布式应用程序系统。Spring Cloud Alibaba 是阿里巴巴开源中间件跟 Spring Cloud 体系的融合。
3.2、主要功能
- 流量控制和服务降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud、Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册和发现:实例可以在Alibaba Nacos上注册,客户可以使用Spring管理的bean发现实例,通过Spring Cloud Netflix支持Ribbon客户端负载均衡器。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 消息总线:使用Spring Cloud Bus RocketMQ链接分布式系统的节点。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- Dubbo RPC:通过Apache Dubbo RPC扩展Spring Cloud服务到服务调用的通信协议。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行。
3.3、组件
- Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠
的消息发布与订阅服务。 - Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
- Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心
产品。 - Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提
供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和
访问任意类型的数据。 - Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精
准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。 - Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速
搭建客户触达通道。
四、微服务项目搭建
4.1、技术选型
- 持久层:SpingData Jpa
- 数据库: MySQL5.7
- 技术栈:SpringCloud Alibaba 技术栈
4.2、模块设计
我们搭建一个微服务的项目,但是只有简单的代码,没有任何业务逻辑。
- shop-parent 父工程
- shop-product-api:商品微服务api ,用于存放商品实体。
- shop-product-server:商品微服务,他的1端口是808x。
- shop-order-api 订单微服务api,用于存放订单实体。
- shop-order-server 订单微服务,他的端口是808x。
4.3、微服务的调用
;在微服务架构中,最常见的场景就是微服务之间的相互调用。我们以电商系统中常见的用户下单为例来演示微服务的调用:客户向订单微服务发起一个下单的请求,在进行保存订单之前需要调用商品微服务查询商品的信息。
我们一般把服务的主动调用方称为服务消费者,把服务的被调用方称为服务提供者。
4.4、创建父工程
创建一个maven工程,然后在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.3.3.RELEASE</version>
<relativePath/>
</parent>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-parent</artifactId>
<packaging>pom</packaging>
<version>1.0.0</version>
<modules>
<module>Shop-order-api</module>
<module>Shop-order-server</module>
<module>Shop-product-api</module>
<module>Shop-product-server</module>
</modules>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
4.5、创建商品服务
4.5.1、书写Shop-product-api的依赖
创建Shop-product-api项目,然后在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>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Shop-product-api</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
4.5.2、创建实体
//商品
@Entity(name = "t_shop_product")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long pid;//主键
private String pname;//商品名称
private Double pprice;//商品价格
private Integer stock;//库存
}
4.5.3、书写Shop-product-server的依赖
<?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>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Shop-order-server</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-order-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-product-api</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
4.5.4、编写application.yml
server:
port: 8081
spring:
application:
name: product-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: admin
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
4.5.5、创建数据库
由于我们使用的是JPA,所以我们需要创建数据库,但是不需要创建表,因为JPA会在对应的数据库中自动创建表。
4.5.6、创建DAO接口
// 第一个参数是实体类,第二个参数是实体类对象的主键的类型
public interface ProductDao extends JpaRepository<Product,Long> {
}
4.5.7、创建Service接口及其实现类
public interface ProductService {
Product findById(Long productId);
}
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductDao productDao;
@Override
public Product findById(Long productId) {
return productDao.findById(productId).get();
}
}
4.5.8、书写Controller
@RestController
@Slf4j
public class ProductController {
@Autowired
private ProductService productService;
//商品信息查询
@RequestMapping("/product")
public Product findByPid(@RequestParam("pid") Long pid) {
Product product = productService.findByPid(pid);
return product;
}
}
4.5.9、加入测试数据
我们在启动项目的时候,可以发现表已经默认帮我们自动创建好了,我们需要导入测试数据。
INSERT INTO t_shop_product VALUE(NULL,'小米','1000','5000');
INSERT INTO t_shop_product VALUE(NULL,'华为','2000','5000');
INSERT INTO t_shop_product VALUE(NULL,'苹果','3000','5000');
INSERT INTO t_shop_product VALUE(NULL,'OPPO','4000','5000');
4.5.10、测试
4.6、创建订单微服务
4.6.1、书写Shop-order-api的依赖
<?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>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Shop-order-api</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
</project>
4.6.2、创建订单实体
//订单
@Entity(name = "t_shop_order")
@Data
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
public class Order implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long oid;//订单id
//用户
private Long uid;//用户id
private String username;//用户名
//商品
private Long pid;//商品id
private String pname;//商品名称
private Double pprice;//商品单价
//数量
private Integer number;//购买数量
}
4.6.3、创建shop-order-server项目
在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>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Shop-order-server</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-order-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-product-api</artifactId>
<version>1.0.0</version>
</dependency>
</project>
4.6.4、编写application.yml
server:
port: 8082
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 1101121833
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
4.6.5、编写Service及其实现类
我们写的Service接口及其实现类不具备任何的业务逻辑,仅仅只是为了测试而用。
public interface OrderService {
Order getById(Long oid,Long pid);
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
OrderDao orderDao;
@Override
public Order getById(Long oid, Long pid) {
return orderDao.getOne(oid);
}
}
4.6.6、创建Controller
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("getById")
public Order getById(Long oid,Long pid){
return orderService.getById(oid, pid);
}
4.7、服务之间如何进行调用
假设我们在订单的服务里面需要调用到商品服务,先查询出id为1的商品,然后再查询出他的订单,这个1时候就涉及到服务之间的调用问题了。
服务之间的1调用本质上是通过Java代码去发起一个Http请求,我们可以使用RestTemplate来进行调用。
4.7.1、在shop-order-server的启动类中添加注解
我们既然需要RestTemplate类,就需要将RestTemplate类注入到容器中。
@SpringBootApplication
public class OrderServer {
public static void main(String[] args) {
SpringApplication.run(OrderServer.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
4.7.2、修改Controller代码
@RestController
public class OrderController {
@Autowired
RestTemplate restTemplate;
@Autowired
OrderService orderService;
@RequestMapping("getById")
public Order getById(Long oid,Long pid){
Product product = restTemplate.getForObject(
"http://localhost:8083/product?productId="+pid,Product.class); // 这里通过restTemplate去发起http请求,请求地址是http://localhost:8083/product,携带的参数是productId
Order order = orderService.getById(oid, product.getPid());
order.setUsername(product.getPname());
return order;
}
}
这样我们就完成了服务之间的互相调用。
4.8、存在的问题
虽然我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:
-
一旦服务提供者地址变化,就需要手工修改代码。
-
一旦是多个服务提供者,无法实现负载均衡功能。
-
一旦服务变得越来越多,人工维护调用关系困难。
那么应该怎么解决呢?这时候就需要通过注册中心动态的实现服务治理。
五、服务治理:Nacos
5.1、服务治理概述
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现。
-
服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
-
服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。
5.2、注册中心的原理
- 我们再启动的时候,就会把服务的信息告诉注册中心,同时拉去一份最新的1服务列表信息在本地。
- 每隔一段时间就会去给注册中心发送一个心跳包,告诉注册中心我还活着,同时会拉去一份最新的服务列表信息。
- 如果这个服务挂了,那么他就不会再给注册中心发送心跳包了。注册中心发现连续几次都没有发送心跳包,那么注册中心就会将这个服务的节点信息给剔除掉,从而1实现动态的注册和踢出服务,就不需要我们手动管理。
服务注册中心是微服务架构中1一个十分重要的组件,在微服务架构中11起到了一个协调者的作用,注册中心一般包含这几个功能:
-
服务发现:
- 服务注册:保存服务提供者和服务调用者的信息。
- 服务订阅:服务调用这订阅服务提供者的信息,注册中心向订阅者推送提供者的信息。
-
服务配置:
- 配置订阅:服务提供者和服务调用者订阅微服务相关的配置。
- 配置下发:主动将配置推送给服务提供者和服务调用者。
-
服务健康检测:检测服务提供者的健康情况,如果发现服务连续几次都没有发送心跳包,说明这个服务有异常,执行服务剔除。
5.3、常见的注册中心
5.3.1、Zookeeper
Zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
5.3.2、Eureka
Eureka是Springcloud Netflflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源
5.3.3、Consul
Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。
5.3.4、Nacos
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 SpringCloud Alibaba 组件之一,负责服务注册发现和服务配置。
5.4、Nacos简介
Nacos是阿里巴巴2018年7月推出来的一个开源项目,是一个更易于构建云原生应用的动态服务注册与发现、配置管理和服务管理平台。Nacos致力于快速实现动态服务注册与发现、服务配置、服务元数据及流量管理。
他的核心功能:
- 服务注册:
Nacos Client会通过发送REST请求想Nacos Server注册自己的服务,提供自身的元数据,比如IP地址,端口等信息。Nacos Server接收到注册请求后,就会把这些元数据存储到一个双层的内存Map中。
2. 服务心跳:
在服务注册后,Nacos Client会维护一个定时心跳来维持统治Nacos Server,说明服务一致处于可用状态,防止被剔除,默认5s发送一次心跳。
3. 服务同步:
Nacos Server集群之间会相互同步服务实例,用来保证服务信息的一致性。
4. 服务发现:
服务消费者(Nacos Client)在调用服务提供的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务拉取服务最新的注册表信息更新到本地缓存。
5. 服务健康检查:
Nacos Server 会开启一个定时任务来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将他的healthy属性设置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。
5.5、Nacos实战
5.5.1、搭建Nacos环境
5.5.1.1、下载Nacos环境
我们需要去下载地址下载Nacos,下载的Nacos是zip格式的安装包,然后进行解压缩操作。如果在Linux的话需要执行:
tar -zxvf Nacos下载包的名称
5.5.1.2、启动
我们以Windows下为例,Linux下也是一样的。我们直接切换到bin目录下,然后cmd,输入命令:
startup.cmd -m standalone
单机环境必须带-m standalone参数启动,否则无法启动,不带参数启动的是集群环境。
我们不带参数(以集群方式启动),会出现下列错误。
正确启动:
5.5.1.3、测试
打开浏览器输入http://localhost:8848/nacos/index.html#/login ,即可访问服务,默认账号密码都是nacos。
5.5.2、将商品服务注册到Nacos
5.5.2.1、添加依赖
我们需要在shop-product-server模块中的pom.xml文件中添加nacos的依赖。
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
5.5.2.2、在主类上添加注解
@SpringBootApplication
@EnableDiscoveryClient
public class ShopProductServerApp {
public static void main(String[] args) {
SpringApplication.run(ShopProductServerApp.class,args);
}
}
5.5.2.3、添加Nacos服务地址
我们需要在application.yml中添加nacos服务的地址。
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
5.5.2.4、查看
说明注册成功!
5.5.3、将订单服务注册到Nacos
接下来开始修改 shop-order-server 模块的代码, 将其注册到nacos服务上
5.5.3.1、添加依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
5.5.3.2、添加注解
@SpringBootApplication
@EnableDiscoveryClient
public class ShopOrderServerApp {
public static void main(String[] args) {
SpringApplication.run(ShopOrderServerApp.class,args);
}
@Bean
@LoadBalanced
public RestTemplate getInstance(){
return new RestTemplate();
}
}
5.5.3.3、添加Nacos服务地址
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
5.5.3.4、测试
5.5.4、编写测试代码
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("test")
public String test(){
ServiceInstance serviceInstance = discoveryClient.getInstances("product-service").get(0);
return serviceInstance.toString();
}
六、负载均衡:Ribbon
6.1、负载均衡简介
负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡:
- 服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。
- 客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请
求。
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供
者执行。
6.2、手动实现负载均衡
6.2.1、起一个不同端口的服务
我们需要借助IDEA再启一个服务,这个服务也是ShopProductServer,但是他的端口是不同的。
在VM option中输入需要指定的端口号即可:
-Dserver.port=8081
6.2.2、修改实现类代码实现负载均衡
@Override
public Order getById(Long oid, Long pid) {
// 从nacos中获取服务
List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
// 定义一个随机数
int index = new Random().nextInt(instances.size());
// 随机获取一个服务
ServiceInstance serviceInstance = instances.get(index);
String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
Product product = restTemplate
.getForObject("http://"+url+"/product?productId=" + pid, Product.class);
Order order = orderDao.getOne(oid);
order.setUsername(order.getUsername()+",data form "+serviceInstance.getPort());
return order;
}
连续测试多几次就可以发现随机的负载均衡策略实现了。
6.3、基于Ribbon实现负载均衡
6.3.1、Ribbon是什么
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。**Ribbon就是简化了上面代码的组件,其中提供了更多的负载均衡算法。**它是Spring Cloud的一个组件,它可以让我们使用一个注解就能轻松的搞定负载均衡。
6.3.2、Ribbon实现负载均衡
6.3.2.1、添加注解
我们需要在RestTemplate的生成方法上添加一个注解:@LoadBalance
,这个注解是一个增强注解,它可以增强RestTemplate ,使他可以实现负载均衡。
@Bean
@LoadBalanced
public RestTemplate getInstance(){
return new RestTemplate();
}
6.3.2.2、修改ProductService实现类的代码
为了更直观,我们直接1将application.yml中的端口号设置到实现类中。
@Value("${server.port}")
private String port;
@Override
public Product findById(Long productId) {
Product product = productDao.findById(productId).get();
product.setPname(product.getPname()+",data form"+port);
return product;
}
6.3.2.3、修改OrderService实现类的代码
@Override
public Order getById(Long oid, Long pid) {
Product product = restTemplate
.getForObject("http://product-service/product?productId=" + pid, Product.class);
Order order = orderDao.getOne(oid);
order.setPname(product.getPname());
return order;
}
6.3.2.4、实现原理
为什么使用服务的名称替代url就可以实现负载均衡了呢?
- 通过反射获取到你传递过来的参数(假设传进来1),此时的url变成了:http://product-service/product?productId=1.
- 按照规则进行切割,将product-service切割出来。
- 根据从注册中心拉去过来的服务列表对应着的节点信息,假设1product-service对应着192.168.10.180:8081和192.168.10.180:8083。
- 根据你1配置的负载均衡的策略去选择一个信息节点,将服务名称替换成对应的ip和端口。假设此时url变成了:
http://192.168.10.180:8081/product?productId=1。
- 使用RestTemplate去发送请求。
注意:一定要在RestTemplate的生成方法上添加@LoadBalance注解,这个注解表示将RestTemplate进行加强,只有进行了加强才会去实现上述的步骤,不然是无法实现负载均衡的。
6.4、Ribbon支持的负载均衡策略
Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为com.netflix.loadbalancer.IRule
,他具体支持的负载均衡策略有:
策略名 | 策略描述 | 实现说明 |
---|---|---|
BestAvailableRule | 选择一个最小的并发请求的server | 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server |
AvailabilityFilteringRule | 先过滤掉故障实例,再选择并发较小的实例; | 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态 |
WeightedResponseTimeRule | 根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低。 | 一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权 |
RetryRule | 对选定的负载均衡策略机上重试机制。 | 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server |
RoundRobinRule | 轮询方式轮询选择server | 轮询index,选择index对应位置的server |
RandomRule | 随机选择一个server | 在index上随机,选择index对应位置的server |
ZoneAvoidanceRule(默认) | 复合判断server所在区域的性能和server的可用性选择server | 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。 |
我们可以通过修改配置来调整Ribbon的负载均衡策略,在Shop-order-server项目的application.yml中增加如下配置:
product-service: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
七、远程调用:Feign
7.1、Feign简介
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。
7.2、Feign实战
7.2.1、添加依赖
在shop-order-server项目的pom文件加入Fegin的依赖。
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
7.2.2、添加注解
我们需要在启动类上添加@EnableFeignClients注解,只有有了这个注解才会扫描。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 支持Feign
public class ShopOrderServerApp {
public static void main(String[] args) {
SpringApplication.run(ShopOrderServerApp.class,args);
}
@Bean
@LoadBalanced
public RestTemplate getInstance(){
return new RestTemplate();
}
}
7.2.3、新增ProductFeignApi
在Shop-order-server中新增一个接口。
// name指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
@FeignClient(name = "product-service")
public interface ProductFeignApi {
@RequestMapping("/product")
Product findById(@RequestParam("productId") Long productId);
}
7.2.4、修改Controller
@Autowired
ProductFeignApi productFeignApi;
@Override
public Order getById(Long oid, Long pid) {
Product product = productFeignApi.findById(pid);
Order order = orderDao.getOne(oid);
order.setPname(product.getPname());
return order;
}
7.3、Feign的重要属性
我们可以配置超时属性。
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
7.4、Feign实现原理
- 启动后会会根据配置在启动类上的@SpringBootApplication去扫描贴了@FeignClient注解的类,并为其创建代理对象。
- 通过反射拿到代理类实现的接口:ProductFeignApi。
- 通过反射拿到接口上注解并且将注解中心的name属性拿出来:product-service。
- 通过反射拿到接口中的方法,并且拿到接口中方法上的注解@RequestMapping,并且把值拿出来:/product。
- 将方法中参数注解中的值也拿出来,这个是我们传进来的参数:productId。
- 拼接出路径:http://product-service/product?productId=1。
- 根据本地的服务清单去找对应的节点信息。
- 根据你配置的ribbon负载均衡策略去选择节点。
- 将product-service替换成对应的节点信息和端口。
- 使用RestTemplate发送请求。
八、流量防护:Sentinel
8.1、高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
8.2、模拟高并发
8.2.1、编写SentinelController
@RestController
public class SentinelController {
@RequestMapping("/sentinel1")
public String sentinel1(){
//模拟一次网络延时
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "sentinel1";
}
@RequestMapping("/sentinel2")
public String sentinel2(){
return "测试高并发下的问题";
}
}
8.2.2、修改Tomcat的并发数
tomcat:
threads:
max: 10 #tomcat的最大并发值修改为10,
8.2.3、使用压力测试模拟高并发
下载地址https://jmeter.apache.org/
修改配置,支持中文
进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN。
然后点击jmeter.bat启动软件。
添加线程组
添加http请求
访问
我们去访问http://localhost:8082/sentinel2,会发现一直在转圈,这就是服务器雪崩的雏形。
8.3、服务器雪崩
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。
服务器一步步雪崩的流程如下:
服务器的雪崩效应其实就是由于某个微小的服务挂了,导致整一大片的服务都不可用.类似生活中的雪崩效应,由于落下的最后一片雪花引发了雪崩的情况.
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
8.4、常见解决方案
要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施, 下面介绍常见的服务容错思路和组件。
常见的容错思路有隔离、超时、限流、熔断、降级这几种。
8.4.1、隔离机制
比如服务A内总共有100个线程, 现在服务A可能会调用服务B,服务C,服务D.我们在服务A进行远程调用的时候,给不同的服务分配固定的线程,不会把所有线程都分配给某个微服务. 比如调用服务B分配30个线程,调用服务C分配30个线程,调用服务D分配40个线程. 这样进行资源的隔离,保证即使下游某个服务挂了,也不至于把服务A的线程消耗完。比如服务B挂了,这时候最多只会占用服务A的30个线程,服务A还有70个线程可以调用服务C和服务D。
8.4.2、超时机制
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
8.4.3、限流机制
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
8.4.4、熔断机制
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
- 熔断关闭状态(Closed):服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制。
- 熔断开启状态(Open):后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法。
- 半熔断状态(Half-Open):尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
8.4.5、降级机制
降级其实就是为服务提供一个兜底方案,一旦服务无法正常调用,就使用兜底方案。
8.5、常见的熔断组件
8.5.1、Hystrix
Hystrix是由Netflflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
8.5.2、Resilience4J
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
8.5.3、Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
8.6、Sentinel实战
8.6.1、什么是Sentinel
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
Sentinel 具有以下特征:
-
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
-
完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒级数据, 甚至 500 台以下规模的集群的汇总运行情况。
-
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 SpringCloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
8.6.2、Sentinel组成部分
Sentinel分为两部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo /Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
8.7、集成Sentinel
微服务集成Sentinel非常简单, 只需要加入Sentinel的依赖即可。
8.7.1、加入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
8.7.2、编写控制器
@RestController
public class SentinelController {
@RequestMapping("/sentinel1")
public String sentinel1(){
//模拟一次网络延时
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "sentinel1";
}
@RequestMapping("/sentinel2")
public String sentinel2(){
return "测试高并发下的问题";
}
}
8.7.3、安装Sentinel控制台
下载jar包
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。我们需要去下载Sentinel控制台的jar包。
修改application.yml
spring:
cloud:
sentinel:
transport:
port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
启动控制台
# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar
测试
通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )
8.7.4、控制台的原理
Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上,即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。
8.8、实现一个接口限流
点击簇点链路->流控
在单机阈值中写数值
在单机阈值填写一个数值,表示每秒上限的请求数
测试
快速访问几次,可以发现出错了。
8.9、Sentinel基本概念和功能
8.9.1、基本概念
8.9.1.1、资源
资源就是Sentinel要保护的东西。资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是一个方法,甚至可以是一段代码。
我们上面例子的一个sentinel2方法就是一个资源。
8.9.1.2、规则
规则就是用来定义如何进行保护资源的。作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统保护规则。
我们上面的例子给sentinel2增加流控规则,限制了sentinel2的流量。
8.9.2、重要功能
Sentinel的主要功能就是容错,主要体现为下面这三个:
-
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是
随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。 -
熔断降级
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则
对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制:Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收求。
- 通过响应时间对资源进行降级:除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
-
系统负载保护
Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让
请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其
它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保
护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请
求。
总结:我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能。
8.10、Sentinel流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
资源名:唯一名称,默认是请求路径,可自定义。
针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制。
阈值类型/单机阈值:
- QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流。
- 线程数:当调用该接口的线程数达到阈值的时候,进行限流。
8.10.1、线程数限流
前面我们已经测试过了QPS限流,所以现在我们改为线程数限流。
8.10.1.1、添加流控规则
8.10.1.2、在Jmeter中新增线程
8.10.1.3、测试
8.10.2、流控模式
点击上面设置流控规则的编辑按钮,然后在编辑页面点击高级选项,会看到有流控模式一栏。
他有三种流控模式:
- 直接(默认):接口达到限流条件时,开启限流。
- 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]。
- 链路:当从某个接口过来的资源达到限流条件时,开启限流
8.10.2.1、关联流控模式
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。
比如:当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢。
我们测试的时候可以关联sentinel1这个资源。
我们使用Jmeter软件连续向/sentinel1连续发送请求,注意QPS一定要大于2,我们访问/sentinel2的时候发现被限流了。
8.10.2.2、链路流控模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
修改application.yml
spring:
cloud:
sentinel:
web-context-unify: false
TraceServiceImpl
@Service
@Slf4j
public class TraceServiceImpl {
@SentinelResource(value = "tranceService")
public void tranceService(){
log.info("调用tranceService方法");
}
}
新增TraceController
@RestController
public class TraceController {
@Autowired
private TraceServiceImpl traceService;
@RequestMapping("/trace1")
public String trace1(){
traceService.tranceService();
return "trace1";
}
@RequestMapping("/trace2")
public String trace2(){
traceService.tranceService();
return "trace2";
}
}
重新启动订单服务并添加链路流控规则
测试
我们去访问 /trace1 和 /trace2 访问, 发现/trace2没问题, /trace1的被限流了。
8.10.3、流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果。
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的
1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。 - 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设
置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
8.11、Sentinel降级规则
降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
- 慢调用比例: 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例: 当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是
[0.0, 1.0]
,代表 0% - 100%。 - 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
8.11.1、慢调用比例
新增FallBackController
@RestController
@Slf4j
public class FallBackController {
@RequestMapping("/fallBack1")
public String fallBack1(){
try {
log.info("fallBack1执行业务逻辑");
//模拟业务耗时
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "fallBack1";
}
}
新增降级规则
上面配置表示,如果在1S之内,有【超过1个的请求】且这些请求中【响应时间>最大RT】的【请求数量比例>10%】,就会触发熔断,在接下来的10s之内都不会调用真实方法,直接走降级方法。
比如: 最大RT=900,比例阈值=0.1,熔断时长=10,最小请求数=10
-
情况1: 1秒内的有20个请求,只有10个请求响应时间>900ms, 那慢调用比例=0.5,这种情况就会触发熔断。
-
情况2: 1秒内的有20个请求,只有1个请求响应时间>900ms, 那慢调用比例=0.05,这种情况不会触发熔断。
-
情况3: 1秒内的有8个请求,只有6个请求响应时间>900ms, 那慢调用比例=0.75,这种情况不会触发熔断,因为最小请求数这个条件没有满足。
我们做实验的时候把最小请求数设置为1,因为在1秒内,手动操作很难在1s内发两个请求过去,所以要做出效果,最好把最小请求数设置为1。
8.11.2、异常数
在方法中新增一个异常
在Shop-order-server项目的FallBackController.java类新增fallBack3方法。
@RequestMapping("/fallBack3")
public String fallBack3(String name){
log.info("fallBack3执行业务逻辑");
if("xiaolin".equals(name)){
throw new RuntimeException();
}
return "fallBack3";
}
配置降级规则
在1s之内,,有【超过3个的请求】,请求中超过2个请求出现异常就会触发熔断,熔断时长为10s。
测试
8.12、Sentinel热点规则
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制。
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制.
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
新增HotSpotController
一定需要在请求方法上贴@SentinelResource注解,否则热点规则无效
@RestController
@Slf4j
public class HotSpotController {
@RequestMapping("/hotSpot1")
@SentinelResource(value = "hotSpot1")
public String hotSpot1(Long productId){
log.info("访问编号为:{}的商品",productId);
return "hotSpot1";
}
}
新增热点规则
因为我们就一个参数,所以参数索引是0。
访问一下/hotSpot1,再编辑热点规则
添加后再去热点规则中编辑规则,在编辑之前一定要先访问一下/hotSpot1,不然参数规则无法新增。
新增参数规则
测试
访问:http://localhost:8082/hotSpot1?productId=2,无论怎么样访问都无济于事。
访问:http://localhost:8082/hotSpot1?productId=1,多次访问后会降级。
8.13、Sentinel授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
- 若配置白名单,则只有请求来源位于白名单内时才可通过;
- 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
新增一个工具类,定义请求来源如何获取
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
/**
* 定义从请求的什么地方获取来源信息
* 比如我们可以要求所有的客户端需要在请求头中携带来源信息
*/
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
新增AuthController
@RestController
@Slf4j
public class AuthController {
@RequestMapping("/auth1")
public String auth1(String serviceName){
log.info("应用:{},访问接口",serviceName);
return "auth1";
}
}
新增规则
测试
访问http://localhost:8082/auth1?serviceName=pc不能访问
访问http://localhost:8082/auth1?serviceName=app可以访问
8.14、系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发。
- 系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。
8.15、自定义异常返回
常见的异常大致分为这几类:
- FlowException:限流异常 。
- DegradeException:降级异常。
- ParamFlowException:参数限流异常。
- AuthorityException:授权异常。
- SystemBlockException:系统负载异常。
在Shop-order-server项目中定义异常返回处理类。
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
ResultData data = null;
if (e instanceof FlowException) {
data = new ResultData(-1, "接口被限流了");
} else if (e instanceof DegradeException) {
data = new ResultData(-2, "接口被降级了");
}else if (e instanceof ParamFlowException) {
data = new ResultData(-3, "参数限流异常");
}else if (e instanceof AuthorityException) {
data = new ResultData(-4, "授权异常");
}else if (e instanceof SystemBlockException) {
data = new ResultData(-5, "接口被降级了...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor
//无参构造
class ResultData {
private int code;
private String message;
}
8.16、@SentinelResource的使用
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。主要参数如下:
属性 | 作用 |
---|---|
value | 资源名称,必需项(不能为空) |
entryType | entry 类型,可选项(默认为 EntryType.OUT ) |
blockHandler/ blockHandlerClass | blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public ,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException 。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
fallback/ fallbackClass | fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:1. 返回值类型必须与原函数返回值类型一致; 2.方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。3.fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
defaultFallback | 默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:1. 返回值类型必须与原函数返回值类型一致; 2. 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。 3. defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
exceptionsToIgnore | 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 |
直接将限流或降级后执行的方法。
8.17、 Sentinel规则持久化
通过前面的讲解,我们已经知道,可以通过Dashboard来为每个Sentinel客户端设置各种各样的规则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化。
本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。
编写处理类
public class FilePersistence implements InitFunc {
@Value("${spring.application.name}")
private String appcationName;
@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + appcationName;
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
之后我们重启发现配置的规则还在,说明持久化成功!
九、服务网关:Gateway
9.1、网关简介
大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
这样的架构会存在许多的问题:
- 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性。
- 认证复杂,每个服务都需要独立认证。
- 存在跨域请求,在一定场景下处理相对复杂。
网关就是为了解决这些问题而生的。所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
9.2、常用的网关
9.2.1、Ngnix+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用。
lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本
9.2.2、Kong
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。
他的缺点:
- 只支持Http协议。
- 二次开发,自由扩展困难。
- 提供管理API,缺乏更易用的管控、配置方式。
9.2.3、Zuul
Netflix开源的网关,功能丰富,使用JAVA开发,易于二次开发。
他的缺点:
- 缺乏管控,无法动态配置。
- 依赖组件较多。
- 处理Http请求依赖的是Web容器,性能不如Nginx。
9.2.4、Spring Cloud Gateway
Spring公司为了替换Zuul而开发的网关服务,SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关
9.3、Gateway简介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
他的主要功能是:
- 进行转发重定向。
- 在开始的时候,所有类都需要做的初始化操作。
- 进行网络隔离。
9.4、快速入门
需求:通过浏览器访问api网关,然后通过网关将请求转发到商品微服务。
9.4.1、基础版
创建一个api-gateway 模块,并且导入下面的依赖。
<?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>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
编写配置文件
server:
port: 9000 # 指定网关服务的端口
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: product_route # 当前路由的标识, 要求唯一
uri: http://localhost:8081 # 请求要转发到的地址
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/product-serv/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
测试
9.4.2、升级版
我们发现升级版有一个很大的问题,那就是在配置文件中写死了转发路径的地址,我们需要在注册中心来获取地址。
加入nacos依赖
<?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>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
在主类上添加注解
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayServerApp {
public static void main(String[] args) {
SpringApplication.run(GateWayServerApp.class,args);
}
}
修改配置文件
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/product-serv/** # 符合这个规定的才进行1转发
filters:
- StripPrefix=1 # 将第一层去掉
我们还可以自定义多个路由规则。
spring:
application:
gateway:
routes:
- id: product_route
uri: lb://product-service
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1
9.4.3、简写版
我们的配置文件无需写的1那么复杂就可以实现功能,有一个简写版。
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
我们发现,就发现只要按照网关地址/微服务名称/接口的格式去访问,就可以得到成功响应。
9.5、Gateway核心架构
9.5.1、基本概念
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:
- id:路由标识符,区别于其他 Route。
- uri:路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
- order:用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
- predicate:断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
- filter:过滤器用于修改请求和响应信息。
- predicate:断言,用于进行条件判断,只有断言都返回真,才会真正的执行路由。
9.5.2、执行原理
- 接收用户的请求,请求处理器交给处理器映射器,返回执行链。
- 请求处理器去调用web处理器,在web处理器里面对我们的路径1进行处理。假设1我们的路径1是:http://localhost:9000/product-serv/get?id=1 ,根据配置的路由规则,上本地找对应的服务信息:product-service对应的主机ip是192.168.10.130。
- 根据1ribbon的负载均衡策略去选择一个节点,然后拼接好,将路径中的product-serv替换成192.168.10.130:8081,如果你配置了filter,那么他还会走filter。
- 如果你没有自定义路由的话,默认Gateway会帮你把第一层去掉。网关端口从此一个
/
开始到第二个/
开始算第一层。
9.6、过滤器
Gateway的过滤器的作用是:是在请求的传递过程中,对请求和响应做一些手脚。
Gateway的过滤器的生命周期:
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择
请求的微服务、记录调试信息等。 - POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP
Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter:
- GatewayFilter:应用到单个路由或者一个分组的路由上。
- GlobalFilter:应用到所有的路由上。
9.6.1、局部过滤器
局部过滤器是针对单个路由的过滤器。他分为内置过滤器和自定义过滤器。
9.6.1.1、内置过滤器
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。
9.6.1.1.1、局部过滤器内容
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand 的名称 |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、 rateLimiter、 statusCode、 denyEmptyKey、 emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通过配置指定仅删除哪些Header |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large | 请求包大小,单位为字节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
9.6.1.1.2、局部过滤器的使用
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/product-serv/** # 符合这个规定的才进行1转发
filters:
- StripPrefix=1 # 将第一层去掉
- SetStatus=2000 # 这里使用内置的过滤器,修改返回状态
9.6.1.2、自定义局部过滤器
很多的时候,内置过滤器没办法满足我们的需求,这个时候就必须自定义局部过滤器。我们假定一个需求是:统计订单服务调用耗时。
编写一个类,用于实现逻辑
名称是有固定格式xxxGatewayFilterFactory
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
private static final String BEGIN_TIME = "beginTime";
//构造函数
public TimeGatewayFilterFactory() {
super(TimeGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("show");
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!config.show){
// 如果配置类中的show为false,表示放行
return chain.filter(exchange);
}
exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());
/**
* pre的逻辑
* chain.filter().then(Mono.fromRunable(()->{
* post的逻辑
* }))
*/
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long startTime = exchange.getAttribute(BEGIN_TIME);
if (startTime != null) {
System.out.println(exchange.getRequest().getURI() + "请求耗时: " + (System.currentTimeMillis() - startTime) + "ms");
}
}));
}
};
}
@Setter
@Getter
static class Config{
private boolean show;
}
}
编写application.xml
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/product-serv/** # 符合这个规定的才进行1转发
filters:
- StripPrefix=1 # 将第一层去掉
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1
- Time=true
访问路径:http://localhost:9000/order-serv/getById?o=1&pid=1
9.6.2、全局过滤器
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理。
开发中的鉴权逻辑:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)。
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证。
- 以后每次请求,客户端都携带认证的token。
- 服务端对token进行解密,判断是否有效。
我们来模拟一个需求:实现统一鉴权的功能,我们需要在网关判断请求中是否包含token且,如果没有则不转发路由,有则执行正常逻辑。
编写全局过滤器
@Component
public class AuthGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}
9.6.3、网关限流
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
- route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
- 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
9.6.3.1、网关集成Sentinel
添加依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
编写配置类进行限流
配置类的本质是用代码替代nacos图形化界面限流。
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
//增加对商品微服务的限流
@PostConstruct
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_route")
.setCount(3) // 三次
.setIntervalSec(1) // 一秒,表示一秒钟1超过了三次就会限流
);
GatewayRuleManager.loadRules(rules);
}
}
修改限流默认返回格式
如果我们不想在限流的时候返回默认的错误,那么就需要自定义错误,指定自定义的返回格式。我们只需在类中添加一段配置即可。
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON).
body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
测试
9.6.3.2、自定义API分组
我们可以发现,上面的这种定义,对整个服务进行了限流,粒度不够细。自定义API分组是一种更细粒度的限流规则定义,它可以实现某个方法的细粒度限流。
在Shop-order-server项目中添加ApiController
@RestController
@RequestMapping("/api")
public class ApiController {
@RequestMapping("/hello")
public String api1(){
return "api";
}
}
在GatewayConfiguration中添加配置
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/order-serv/api/**"). setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
@PostConstruct
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_route")
.setCount(3)
.setIntervalSec(1)
);
rules.add(new GatewayFlowRule("order_api").
setCount(1).
setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
测试
直接访问http://localhost:8082/api/hello 是不会发生限流的,访问http://localhost:9000/order-serv/api/hello 就会出现限流了。
十、链路追踪:Sleuth&Zipkin
10.1、链路追踪简介
在大型系统的微服务化构建中,一个系统被拆分成了许多模块。这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。
在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问题:
- 如何快速发现问题?
- 如何判断故障影响范围?
- 如何梳理服务依赖以及依赖的合理性?
- 如何分析链路性能问题以及实时容量规划?
分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
常见的链路追踪技术有下面这些:
- cat:由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高。风险较大。
- zipkin:由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth使用较为简单, 集成很方便, 但是功能较简单。
- pinpoint:Pinpoint是韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能强大,接入端无代码侵入。
- skywalking:SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。
- Sleuth:SpringCloud 提供的分布式系统中链路追踪解决方案。
SpringCloud alibaba技术栈中并没有提供自己的链路追踪技术的,我们可以采用Sleuth +Zinkin来做链路追踪解决方案。
10.2、Sleuth入门
10.2.1、Sleuth简介
SpringCloud Sleuth主要功能就是在分布式系统中提供追踪解决方案。它大量借用了Google Dapper的设计, 先来了解一下Sleuth中的术语和相关概念。
-
Trace
由一组Trace Id相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯一标识将所有的请求串联起来,形成一条完整的请求链路。
-
Span
代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时候,也通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结束时间戳,就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称。请求信息等元数据。
-
Annotation
用它记录一段时间内的事件,内部使用的重要注释:
- cs(Client Send)客户端发出请求,开始一个请求的生命。
- sr(Server Received)服务端接受到请求开始进行处理, sr-cs = 网络延迟(服务调用的时间)。
- ss(Server Send)服务端处理完毕准备发送到客户端,ss - sr = 服务器上的请求处理时间。
- cr(Client Reveived)客户端接受到服务端的响应,请求结束。 cr - sr = 请求的总时间。
10.2.2、集成链路追踪组件Sleuth
Sleuth的使用及其简单,直接引入一个依赖即可。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
我们随便在一个服务里面打印日志,可以在控制台观察到sleuth的日志输出:
日志参数详解:
[product-service,d1e92e984eaec1ff,d1e92e984eaec1ff,true]
-
第一个值,spring.application.name的值,代表服务名
-
第二个值,d1e92e984eaec1ff,sleuth生成的一个ID,叫Trace ID,用来标识一条请求链路,一条请求链路中包含一个Trace ID,多个Span ID
-
第三个值,d1e92e984eaec1ff、spanID 基本的工作单元,获取元数据,如发送一个http
-
第四个值:true,是否要将该信息输出到zipkin服务中来收集和展示。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以将日志聚合,并进行可视化展示和全文检索。
10.3、Zipkin+Sleuth整合
10.3.1、ZipKin介绍
Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。
Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。
它主要由 4 个核心组件构成:
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI:UI 组件, 基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分析跟踪信息。
Zipkin分为两端,一个是 Zipkin服务端,一个是 Zipkin客户端,客户端也就是微服务的应用。 客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。
10.3.2、 ZipKin服务端安装
下载ZipKin的jar包
官网下载地址,下载了以后是一个jar包。
通过命令启动
java -jar jar包名字
访问 http://localhost:9411
10.3.3、Zipkin+Sleuth整合
在Shop-produuct-server和Shop-order-server中加入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
在Shop-produuct-server和Shop-order-server中加入application.yml
spring:
zipkin:
base-url: http://127.0.0.1:9411/ #zipkin server的请求地址
discoveryClientEnabled: false #让nacos把它当成一个URL,而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比
访问测试
十一:服务配置中心:Nacos Config
11.1、服务配置中心介绍
首先我们来看一下,微服务架构下关于配置文件的一些问题:
- 配置文件相对分散。在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理。
- 配置文件无法区分环境。微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难。
- 配置文件无法实时更新。我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一个正在运行的项目来说是非常不友好的。基于上面这些问题,我们就需要配置中心的加入来解决这些问题。
配置中心的思路是:
- 首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。
- 当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置。
- 当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动
态更新。
当加入了服务配置中心之后,我们的系统架构图会变成下面这样:
在业界常见的服务配置中心,有这几种:
-
Apollo
Apollo是由携程开源的分布式配置中心。特点有很多,比如:配置更新之后可以实时生效,支持灰度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台API。并且资料也写的很详细。
-
Disconf
Disconf是由百度开源的分布式配置中心。它是基于Zookeeper来实现配置变更后实时通知和生效的。
-
SpringCloud Config
这是Spring Cloud中带的配置中心组件。它和Spring是无缝集成,使用起来非常方便,并且它的配置存储支持Git。不过它没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新。
-
这是SpingCloud alibaba技术栈中的一个组件,前面我们已经使用它做过服务注册中心。其实它也集成了服务配置的功能,我们可以直接使用它作为服务配置中心。
11.2、Nacos Config入门
使用nacos作为配置中心,其实就是将nacos当做一个服务端,将各个微服务看成是客户端,我们将各个微服务的配置文件统一存放在nacos上,然后各个微服务从nacos上拉取配置即可。
在商品微服务中1引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
在微服务中添加nacos config的配置
不能使用原来的application.yml
作为配置文件,而是新建一个bootstrap.yml
作为配置文件。
配置文件优先级(由高到低):bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
先注释掉原来的yml配置文件,仅仅留下独有的配置,比如说端口、服务名这些。
spring:
application:
name: product-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
profiles:
active: dev # 环境标识
在nacos中添加配置,然后把订单微服务application.yml配置复制到配置内容中
![将公共配置上传配置中心](https://i-blog.csdnimg.cn/blog_migrate/888a4e4c4085726668a1b7f2434c84cb.png)
释本地的application.yam中的内容, 启动程序进行测试
11.3、Nacos Config配置动态刷新
我们实现了配置的远程存放,但是此时如果修改了配置,我们的程序是无法读取到的,因此,我们需要开启配置的动态刷新功能。
在nacos中的order-service-dev.yaml配置项中这段配置(一定是在远程的配置文件上,不是本地)
config:
appName: order # 没啥含义,仅仅为了测试动态刷新,当改参数的时候,看看是否可以动态刷新
在order微服务中新增NacosConfigControlller
@RestController
@RefreshScope // 实现动态刷新的注解
public class NacosConfigController {
@Value("${appConfig.name}")
private String appConfigName;
@RequestMapping("/nacosConfig1")
public String nacosConfig(){
return "远程信息:"+appConfigName;
}
}
11.4、Nacos Config配置共享
当配置越来越多的时候,我们就发现有很多配置是重复的,这个时候,我们就需要考虑将公共的1配置文件提取出来,然后实现配置文件共享,谁需要直接导入即可。
11.4.1、同一个微服务的不同环境之间共享配置
如果想在同一个微服务的不同环境之间实现配置共享,只需要提取一个以 spring.application.name(spring.服务名) 命名的配置文件,然后将其所有环境的公共配置放在里面即可。
新建一个order-service.yaml的配置文件
这个配置文件用于存放公共的配置文件。
server:
port: 8080
tomcat:
threads:
max: 10 #tomcat的最大并发值修改为10,
spring:
zipkin:
base-url: http://127.0.0.1:9411/ #zipkin server的请求地址
discoveryClientEnabled: false #让nacos把它当成一个URL,而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比
application:
name: order-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 1101121833
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
web-context-unify: false
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
product-service: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
feign:
client:
config:
default:
connectTimeout: 3000
readTimeout: 3000
新建一个order-service-test.yaml的配置文件
这个配置文件用于存放测试环境的配置。
appConfig:
name: order-service
env: dev
server:
application:
name: order-service
spring:
application:
name: order-service # 服务的名字
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
在Controller中新增测试方法
@RestController
@RefreshScope
public class NacosConfigController {
@Value("${appConfig.name}")
private String appConfigName;
@Value("${env}")
private String env;
@RequestMapping("/nacosConfig1")
public String nacosConfig(){
return "远程信息:"+appConfigName;
}
@RequestMapping("/nacosConfig2")
public String nacosConfig2(){
return "公共配置:"+appConfigName+",环境配置信息:"+env;
}
}
11.4.2、不同微服务中间共享配置
不同为服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引入。
在nacos中定义一个global-config.yaml的配置文件
这个配置文件用于存储所有微服务都共享的配置。为了测试方便就写一个属性。
globalConfig: global
修改bootstrap.yaml
spring:
application:
name: order-service # 服务的名字
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
shared-configs:
- data-id: global-config.yaml # 配置要引入的配置
refresh: true
profiles:
active: test # 环境标识
修改1Controller的侧式方法
@RestController
@RefreshScope
public class NacosConfigController {
@Value("${appConfig.name}")
private String appConfigName;
@Value("${env}")
private String env;
@Value("${globalConfig}")
private String globalConfig;
@RequestMapping("/nacosConfig1")
public String nacosConfig(){
return "远程信息:"+appConfigName;
}
@RequestMapping("/nacosConfig2")
public String nacosConfig2(){
return "公共配置:"+appConfigName+",环境配置信息:"+env;
}
@RequestMapping("/nacosConfig3")
public String nacosConfig3(){
return "全局配置:"+globalConfig+",公共配置:"+appConfigName+",环境配置信息:"+env;
}
}
测试
11.5、nacos的几个概念
-
命名空间(Namespace)
命名空间可用于进行不同环境的配置隔离。一般一个环境划分到一个命名空间。
-
配置分组(Group)
配置分组用于将不同的服务可以归类到同一分组。一般将一个项目的配置分到一组。
-
配置集(Data ID)
在系统中,一个配置文件通常就是一个配置集。一般微服务的配置就是一个配置集。