本篇文章为你整理了微服务Spring Cloud Alibaba简单笔记(springcloudalibaba微服务架构图)的详细内容,包含有微服务架构 spring cloud springcloudalibaba微服务架构图 spring cloud alibaba微服务从入门到进阶 吾爱破解 微服务架构springcloud spring boot 微服务Spring Cloud Alibaba简单笔记,希望能帮助你了解 微服务Spring Cloud Alibaba简单笔记。
Nacos 领域模型描述了服务与实例之间的边界和层级关系。Nacos 的服务领域模型是以“服 务”为维度构建起来的,这个服务并不是指集群中的单个服务器,而是指微服务的服务名。
“服务”是 Nacos 中位于最上层的概念,在服务之下,还有集群和实例的概念。
服务
在服务这个层级上可以配置元数据和服务保护阈值等信息。服务阈值是一个 0~1 之间的 数字,当服务的健康实例数与总实例的比例小于这个阈值的时候,说明能提供服务的机器已经 没多少了。这时候 Nacos 会开启服务保护模式,不再主动剔除服务实例,同时还会将不健康 的实例也返回给消费者。
集群
一个服务由很多服务实例组成,在每个服务实例启动的时候,可以设置它所属的集群,在 集群这个层级上,也可以配置元数据。除此之外,还可以为持久化节点设置健康检查 模式。
所谓持久化节点,是一种会保存到 Nacos 服务端的实例,即便该实例的客户端进程没有在运 行,实例也不会被服务端删除,只不过 Nacos 会将这个持久化节点状态标记为不健康, Nacos 可以采用一种“主动探活”的方式来对持久化节点做健康检查。
除了持久化节点以外,大部分服务节点在 Nacos 中以“临时节点”的方式存在,它是默认的 服务注册方式,从名字中就可以看出,这种节点不会被持久化保存在 Nacos 服务器,临 时节点通过主动发送 heartbeat 请求向服务器报送自己的状态。
实例
这里所说的实例就是指服务节点,可以在 Nacos 控制台查看每个实例的 IP 地址和端口、 编辑实例的元数据信息、修改它的上线 / 下线状态或者配置路由权重等等。
在这三个层级上都有“元数据”这一数据结构,可以把它理解为一组包含了服务 描述信息(如服务版本等)和自定义标签的数据集合。Client 端通过服务发现技术可以获取到 每个服务实例的元数据,可以将自定义的属性加入到元数据并在 Client 端实现某些定制化 的业务场景。
Nacos 的数据模型有三个层次结构,分别是 Namespace、Group 和 Service/DataId。
Namespace:即命名空间,它是最顶层的数据结构,可以用它来区分开发环境、生产 环境等不同环境。默认情况下,所有服务都部署到一个叫做“public”的公共命名空间;
Group:在命名空间之下有一个分组结构,默认情况下所有微服务都属于 “DEFAULT_GROUP”这个分组,不同分组间的微服务是相互隔离的;
Service/DataID:在 Group 分组之下,就是具体的微服务了,比如订单服务、商品服务等等。
通过 Namespace + Group + Service/DataID,就可以精准定位到一个具体的微服务
Nacos 基本架构
Nacos 的核心功能有两个,一个是 Naming Service,用来做服务发现的模块;另 一个是 Config Service,用来提供配置项管理、动态更新配置和元数据的功能
Provider APP 和 Consumer APP 通过 Open API 和 Nacos 服务 器的核心模块进行通信。这里的 Open API 是一组对外暴露的 RESTful 风格的 HTTP 接口。
在 Nacos 和核心模块里,Naming Service 提供了将对象和实体的“名字”映射到元数据的 功能,这是服务发现的基础功能之一。
Nacos 还有一个相当重要的模块:Nacos Core 模块。它可以提供一系列的平台基础功能, 是支撑 Nacos 上层业务场景的基石
Nacos集群环境搭建
Nacos Server 的安装包可以从 Alibaba 官方 GitHub 中的Release 页面下载。
下载完成后,可以在本地将 Nacos Server 压缩包解压,并将解压后的目录名改为“nacos-cluster1”,再复制一份同样的文件到 nacos-cluster2,以此来模拟一个由两台 Nacos Server 组成的集群。
修改启动项参数
Nacos Server 的启动项位于 conf 目录下的 application.properties 文件里,需要修改服务启动端口和数据库连接
Nacos Server 的启动端口由 server.port 属性指定,默认端口是 8848。在 nacos-cluster1 中仍然使用 8848 作为默认端口,需要把 nacos-cluster2 中的端口号改为 8948
在默认情况下,Nacos Server 会使用 Derby 作为数据源,用于保存配置管理数据。将 Nacos Server 的数据源迁移到更加稳定的 MySQL 数据库中,需要修改三处 Nacos Server 的数据库配置。
指定数据源:spring.datasource.platform=mysql 将这行注释放开;
指定 DB 实例数:放开 db.num=1 这一行的注释;
修改 JDBC 连接串:db.url.0 指定了数据库连接字符串,db.user.0 和 db.password.0 分别指定了连接数据库的用户名和密码
创建数据库表
Nacos 已经把建表语句放在解压后的 Nacos Server 安装目录中下的 conf 文件夹里
添加集群机器列表
Nacos Server 可以从一个本地配置文件中获取所有的 Server 地址信息,从而实现服务器之 间的数据同步。
在 Nacos Server 的 conf 目录下创建 cluster.conf 文件,并将 nacos-cluster1 和 nacos-cluster2 这两台服务器的 IP 地址 + 端口号添加到文件中。
## 注意,这里的IP不能是localhost或者127.0.0.1
192.168.1.100:8848
192.168.1.100:8948
启动 Nacos Server
通过 -m standalone 参数,可以单机模式启动。
Nacos 的启动脚本位于安装目录下的 bin 文件夹,其中 Windows 操作系统对应的启动脚本和关闭脚本分别是 startup.cmd 和 shutdown.cmd, Mac 和 Linux 系统对应的启动和关闭脚本是 startup.sh 和 shutdown.sh。
登录 Nacos 控制台
使用 Nacos 默认创建好的用户 nacos 登录系统,用户名和密码都是 nacos。
为了验证集群环境处于正常状态,可以在左侧导航栏中打开“集群管理”下的“节点列表” 页面,在这个页面上显示了集群环境中所有的 Nacos Server 节点以及对应的状态,它们的节点状态都是绿色的“UP”,这表示搭建的集群环境一切正常。
在实际的项目中,如果某个微服务 Client 要连接到 Nacos 集群做服务注册,并不会把 Nacos 集群中的所有服务器都配置在 Client 中,否则每次 Nacos 集群增加或删除了节点, 都要对所有 Client 做一次代码变更并重新发布。
常见的一个做法是提供一个 VIP URL 给到 Client,VIP URL 是一个虚拟 IP 地址,可以把 真实的 Nacos 服务器地址列表“隐藏”在虚拟 IP 后面,客户端只需要连接到虚 IP 即可,由 提供虚 IP 的组件负责将请求转发给背后的服务器列表。这样一来,即便 Nacos 集群机器数量 发生了变动,也不会对客户端造成任何感知。
提供虚 IP 的技术手段有很多,比如通过搭建 Nginx+LVS 或者 keepalived 技术实现高可用集群。
将服务提供者注册到 Nacos 服务器
添加 Nacos 依赖项
Spring Boot、Spring Cloud 和 Spring Cloud Alibaba 三者之间有严格的版本匹配关系
版本说明: link
将 Spring Cloud Alibaba 和 Spring Cloud 的依赖项版本添加到顶层项目下的 pom.xml 文件中。
dependencyManagement
dependencies
dependency
groupId org.springframework.cloud /groupId
artifactId spring-cloud-dependencies /artifactId
version 2020.0.1 /version
type pom /type
scope import /scope
/dependency
dependency
groupId com.alibaba.cloud /groupId
artifactId spring-cloud-alibaba-dependencies /artifactId
version 2021.1 /version
type pom /type
scope import /scope
/dependency
/dependencies
!-- 省略部分代码 --
/dependencyManagement
定义了组件的大版本之后,就可以直接把 Nacos 的依赖项加入到两个子模块的 pom.xml 文件中
dependency
groupId com.alibaba.cloud /groupId
artifactId spring-cloud-starter-alibaba-nacos-discovery /artifactId
/dependency
在添加完依赖项之后,就可以通过配置项开启 Nacos 的服务治理功能了。Spring Cloud 各个组件都采用了自动装配器实现了轻量级的组件集成功能,只需要几行配置,剩下的初始化工作都可以交给背后的自动装配器来实现。
Nacos 自动装配原理
在 Spring Cloud 稍早一些的版本中,需要在启动类上添加 @EnableDiscoveryClient 注 解开启服务治理功能,而在新版本的 Spring Cloud 中,这个注解不再是一个必须的步骤, 们只需要通过配置项就可以开启 Nacos 的功能。
们将 Nacos 依赖项添加到项目中,同时也引入了 Nacos 自带的自动装配器,比如下面这几 个被引入的自动装配器就掌管了 Nacos 核心功能的初始化任务。
NacosDiscoveryAutoConfiguration:服务发现功能的自动装配器,它主要做两件事 儿:加载 Nacos 配置项,声明 NacosServiceDiscovery 类用作服务发现;
NacosServiceAutoConfiguration:声明核心服务治理类 NacosServiceManager,它可以通过 service id、group 等一系列参数获取已注册的服务列表;
NacosServiceRegistryAutoConfiguration:Nacos 服务注册的自动装配器。
添加 Nacos 配置项
spring:
cloud:
nacos:
discovery:
# Nacos的服务注册地址,可以配置多个,逗号分隔
server-addr: localhost:8848
# 服务注册到Nacos上的名称,一般不用配置
service: coupon-customer-serv
# nacos客户端向服务端发送心跳的时间间隔,时间单位其实是ms
heart-beat-interval: 5000
# 服务端没有接受到客户端心跳请求就将其设为不健康的时间间隔,默认为15s
# 注:推荐值该值为15s即可,如果有的业务线希望服务下线或者出故障时希望尽快被发现,可以适
heart-beat-timeout: 20000
# 元数据部分 - 可以自己随便定制
metadata:
mydata: abc
# 客户端在启动时是否读取本地配置项(一个文件)来获取服务列表
# 注:推荐该值为false,若改成true。则客户端会在本地的一个
# 文件中保存服务信息,当下次宕机启动时,会优先读取本地的配置对外提供服务。
naming-load-cache-at-start: false
# 命名空间ID,Nacos通过不同的命名空间来区分不同的环境,进行数据隔离,
namespace: dev
# 创建不同的集群
cluster-name: Cluster-A
# [注意]两个服务如果存在上下游调用关系,必须配置相同的group才能发起访问
group: myGroup
# 向注册中心注册服务,默认为true
# 如果只消费服务,不作为服务提供方,倒是可以设置成false,减少开销
register-enabled: true
Namespace 可以用作环境隔离或者多租户隔离,其中:
环境隔离:比如设置三个命名空间 production、pre-production 和 dev,分别表示生产 环境、预发环境和开发环境,如果一个微服务注册到了 dev 环境,那么他无法调用其他环 境的服务,因为服务发现机制只会获取到同样注册到 dev 环境的服务列表。如果未指定 namespace 则服务会被注册到 public 这个默认 namespace 下。
多租户隔离:即 multi-tenant 架构,通过为每一个用户提供独立的 namespace 以实现租 户与租户之间的环境隔离。
Group 的使用场景非常灵活,列举几个:
环境隔离:在多租户架构之下,由于 namespace 已经被用于租户隔离,为了实现同一个租 户下的环境隔离,可以使用 group 作为环境隔离变量。
线上测试:对于涉及到上下游多服务联动的场景,将线上已部署的待上下游测服务的 group 设置为“group-A”,由于这是一个新的独立分组,所以线上的用户流量不会导向 到这个 group。这样一来,开发人员就可以在不影响线上业务的前提下,通过发送测试请 求到“group-A”的机器完成线上测试。
什么是单元封闭呢?为了保证业务的高可用性,通常会把同一个服务部署在 不同的物理单元(比如张北机房、杭州机房、上海机房),当某个中心机房出现故障的时 候,可以在很短的时间内把用户流量切入其他单元机房。由于同一个单元内的服务器资 源通常部署在同一个物理机房,因此本单元内的服务调用速度最快,而跨单元的服务调用将 要承担巨大的网络等待时间。这种情况下,可以为同一个单元的服务设置相同的 group,使微服务调用封闭在当前单元内,提高业务响应速度。
服务消费者添加Nacos依赖项和配置信息
!-- Nacos服务发现组件 --
dependency
groupId com.alibaba.cloud /groupId
artifactId spring-cloud-starter-alibaba-nacos-discovery /artifactId
/dependency
!-- 负载均衡组件 --
dependency
groupId org.springframework.cloud /groupId
artifactId spring-cloud-starter-loadbalancer /artifactId
/dependency
!-- webflux服务调用 --
dependency
groupId org.springframework.boot /groupId
artifactId spring-boot-starter-webflux /artifactId
/dependency
spring-cloud-starter-loadbalancer:Spring Cloud御用负载均衡组件Loadbalancer,用来代替已经进入维护状态的Netflix Ribbon组件。会在下一课带深入了解Loadbalancer的功能,今天只需要简单了解下它的用法就可以了;
spring-boot-starter-webflux:Webflux是Spring Boot提供的响应式编程框架,响应式编程是基于异步和事件驱动的非阻塞程序。Webflux实现了Reactive Streams规范,内置了丰富的响应式编程特性。今天将用Webflux组件中一个叫做WebClient的小工具发起远程服务调用。
Nacos服务发现底层实现
Nacos Client通过一种 主动轮询 的机制从Nacos Server获取服务注册信息,包括地址列表、group分组、cluster名称等一系列数据。简单来说,Nacos Client会开启一个本地的定时任务,每间隔一段时间,就尝试从Nacos Server查询服务注册表,并将最新的注册信息更新到本地。这种方式也被称之为“Pull”模式,即客户端主动从服务端拉取的模式。
负责拉取服务的任务是UpdateTask类,它实现了Runnable接口。Nacos以开启线程的方式调用UpdateTask类中的run方法,触发本地的服务发现查询请求。
UpdateTask这个类是HostReactor的一个内部类,
在UpdateTask的源码中,它通过调用updateService方法实现了服务查询和本地注册表更新,在每次任务执行结束的时候,在结尾处它通过finally代码块设置了下一次executor查询的时间,周而复始循环往复。
OpenFeign
OpenFeign提供了一种声明式的远程调用接口,它可以大幅简化远程调用的编程体验。
OpenFeign使用了一种“动态代理”技术来封装远程服务调用的过程,远程服务调用的信息被写在了FeignClient接口中
OpenFeign的动态代理
在项目初始化阶段,OpenFeign会生成一个代理类,对所有通过该接口发起的远程调用进行动态代理。
![[,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pyah4TJ8-1677596762819)(C:\Users\
上图中的步骤1到步骤3是在项目启动阶段加载完成的,只有第4步“调用远程服务”是发生在项目的运行阶段。
首先,在项目启动阶段, OpenFeign框架会发起一个主动的扫包流程,从指定的目录下扫描并加载所有被@FeignClient注解修饰的接口。
然后, OpenFeign会针对每一个FeignClient接口生成一个动态代理对象,即图中的FeignProxyService,这个代理对象在继承关系上属于FeignClient注解所修饰的接口的实例。
接下来, 这个动态代理对象会被添加到Spring上下文中,并注入到对应的服务里,也就是图中的LocalService服务。
最后, LocalService会发起底层方法调用。实际上这个方法调用会被OpenFeign生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给LocalService。
OpenFeign是如何通过动态代理技术创建代理对象的?
项目加载:在项目的启动阶段, EnableFeignClients注解 扮演了“启动开关”的角色,它使用Spring框架的 Import注解 导入了FeignClientsRegistrar类,开始了OpenFeign组件的加载过程。
扫包: FeignClientsRegistrar 负责FeignClient接口的加载,它会在指定的包路径下扫描所有的FeignClients类,并构造FeignClientFactoryBean对象来解析FeignClient接口。
解析FeignClient注解: FeignClientFactoryBean 有两个重要的功能,一个是解析FeignClient接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的ReflectiveFeign完成的。
构建动态代理对象:ReflectiveFeign 包含了OpenFeign动态代理的核心逻辑,它主要负责创建出FeignClient接口的动态代理对象。ReflectiveFeign在这个过程中有两个重要任务,一个是解析FeignClient接口上各个方法级别的注解,将其中的远程接口URL、接口类型(GET、POST等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的MethodHandler类作为方法级别的代理;另一个重要任务是将这些MethodHandler方法代理做进一步封装,通过Java标准的动态代理协议,构建一个实现了InvocationHandler接口的动态代理对象,并将这个动态代理对象绑定到FeignClient接口上。这样一来,所有发生在FeignClient接口上的调用,最终都会由它背后的动态代理对象来承接。
MethodHandler的构建过程涉及到了复杂的元数据解析,OpenFeign组件将FeignClient接口上的各种注解封装成元数据,并利用这些元数据把一个方法调用“翻译”成一个远程调用的Request请求。
那么上面说到的“元数据的解析”是如何完成的呢?它依赖于OpenFeign组件中的Contract协议解析功能。Contract是OpenFeign组件中定义的顶层抽象接口,它有一系列的具体实现。
专门用来解析Spring MVC标签的SpringMvcContract类的继承结构是SpringMvcContract- BaseContract- Contract。
OpenFeign的工作流程的重点是 动态代理机制。OpenFeing通过Java动态代理生成了一个“代理类”,这个代理类将接口调用转化成为了一个远程服务调用。
FeignClientsRegistrar是OpenFeign初始化的起点
实现服务间调用功能
把依赖项spring-cloud-starter-OpenFeign添加到子模块内的pom.xml文件中。
!-- OpenFeign组件 --
dependency
groupId org.springframework.cloud /groupId
artifactId spring-cloud-starter-openfeign /artifactId
/dependency
在接口上声明了一个FeignClient注解,它专门用来标记被OpenFeign托管的接口。
@FeignClient(value = "coupon-template-serv", path = "/template")
public interface TemplateService {
// 读取优惠券
@GetMapping("/getTemplate")
CouponTemplateInfo getTemplate(@RequestParam("id") Long id);
// 批量获取
@GetMapping("/getBatch")
Map Long, CouponTemplateInfo getTemplateInBatch(@RequestParam("ids") Collection Long ids);
在FeignClient注解中声明的value属性是目标服务的名称,需要确保这里的服务名称和Nacos服务器上显示的服务注册名称是一样的。
配置OpenFeign的加载路径
@EnableFeignClients(basePackages = {"com.xxx"})
public class Application {
在EnableFeignClients注解的basePackages属性中定义了一个com.xxx的包名,这个注解就会告诉OpenFeign在启动项目的时候做一件事儿:找到所有位于com.xxx包路径(包括子package)之下使用FeignClient修饰的接口,然后生成相关的代理类并添加到Spring的上下文中。这样才能够在项目中用Autowired注解注入OpenFeign接口。
日志信息打印
服务请求的入参和出参是分析和排查问题的重要线索。为了获得服务请求的参数和返回值,经常使用的一个做法就是 打印日志
首先,需要在配置文件中 指定FeignClient接口的日志级别为Debug。这样做是因为OpenFeign组件默认将日志信息以debug模式输出,而默认情况下Spring Boot的日志级别是Info
接下来,还需要在应用的上下文中使用代码的方式 声明Feign组件的日志级别。这里的日志级别并不是传统意义上的Log Level,它是OpenFeign组件自定义的一种日志级别,用来控制OpenFeign组件向日志中写入什么内容。
@Bean
Logger.Level feignLogger() {
return Logger.Level.FULL;
OpenFeign总共有四种不同的日志级别
NONE:不记录任何信息,这是OpenFeign默认的日志级别;
BASIC:只记录服务请求的URL、HTTP Method、响应状态码(如200、404等)和服务调用的执行时间;
HEADERS:在BASIC的基础上,还记录了请求和响应中的HTTP Headers;
FULL:在HEADERS级别的基础上,还记录了服务请求和服务响应中的Body和metadata,FULL级别记录了最完整的调用信息。
超时判定是一种保障可用性的手段。
为了隔离下游接口调用超时所带来的的影响,可以在程序中设置一个 超时判定的阈值,一旦下游接口的响应时间超过了这个阈值,那么程序会自动取消此次调用并返回一个异常。
feign:
client:
config:
# 全局超时配置
default:
# 网络连接阶段1秒超时
connectTimeout: 1000
# 服务请求响应阶段5秒超时
readTimeout: 5000
# 针对某个特定服务的超时配置
coupon-template-serv:
connectTimeout: 1000
readTimeout: 2000
降级逻辑是在远程服务调用发生超时或者异常(比如400、500 Error Code)的时候,自动执行的一段业务逻辑。
OpenFeign实现Client端的服务降级相比于Sentinel而言 更加轻量级且容易实现, 足以满足一些简单的服务降级业务需求。
OpenFeign对服务降级的支持是借助Hystrix组件实现的,由于Hystrix已经从Spring Cloud组件库中被移除,所以要在pom文件中手动添加hystrix项目的依赖。
!-- hystrix组件,专门用来演示OpenFeign降级 --
dependency
groupId org.springframework.cloud /groupId
artifactId spring-cloud-starter-netflix-hystrix /artifactId
version 2.2.10.RELEASE /version
exclusions
!-- 移除Ribbon负载均衡器,避免冲突 --
exclusion
groupId org.springframework.cloud /groupId
artifactId spring-cloud-netflix-ribbon /artifactId
/exclusion
/exclusions
/dependency
OpenFeign支持两种不同的方式来指定降级逻辑,一种是定义fallback类,另一种是定义fallback工厂。
通过fallback类实现降级是最为简单的一种途径,如果想要为FeignClient接口指定一段降级流程,可以定义一个降级类并实现接口,并在接口中指定为降级类。
@FeignClient(value = "coupon-template-serv", path = "/template",
// 通过fallback指定降级逻辑
fallback = TemplateServiceFallback.class)
如果想要在降级方法中获取到 异常的具体原因,那么就要借助 fallback工厂 的方式来指定降级逻辑了。按照OpenFeign的规范,自定义的fallback工厂需要实现FallbackFactory接口
@FeignClient(value = "coupon-template-serv", path = "/template",
// 通过抽象工厂来定义降级逻辑
fallbackFactory = TemplateServiceFallbackFactory.class)
分布式配置中心在配置管理方面发挥的作用
高可用性: 微服务组件的高可用性是首要目标。配置中心并不是一个中心化的单点应用,而是一个通过集群对外提供服务的组件。在一致性算法的基础上,集群中各个节点之间会互相同步配置数据,或者从统一数据源读取配置数据。即便个别节点挂掉,也不影响整个集群的可用性;
环境隔离特性:Nacos支持通过Namespace属性指定当前配置项所在的环境,可以为自己的应用系统创建开发环境、预发环境和生产环境,不同环境之间的配置文件是相互隔离的;
多格式支持:Nacos支持多种不同格式的配置内容,可以使用纯文本、JSON、XML、YAML和Properties多种文件后缀;
访问控制:Nacos实现了权限管理功能,可以在控制台创建用户账号和权限组,限制某个账号可以访问哪些命名空间,并配置账号的读写权限(只读、只写、读写)。通过这种方式,可以保障敏感信息(如数据库用户名和密码)的安全;
职责分离:配置项从jar包中抽离了出来,修改配置项再也不需要重新编译打包应用程序了,完美实现了配置项管理与业务代码之间的职责分离;
版本控制和审计功能:配置项也是一种代码,而且配置bug往往比代码中的bug造成的影响更大。因此,在微服务架构中需要确保配置中心具备完善的版本控制和审计功能
Nacos还可以支持 多文件源读取以及运行期配置变更。尤其是 动态变更推送,更是微服务架构下不可或缺的配置管理能力。
添加依赖项
!-- 添加Nacos Config配置项 --
dependency
groupId com.alibaba.cloud /groupId
artifactId spring-cloud-starter-alibaba-nacos-config /artifactId
/dependency
!-- 读取bootstrap文件 --
dependency
groupId org.springframework.cloud /groupId
artifactId spring-cloud-starter-bootstrap /artifactId
/dependency
Nacos配置中心的连接信息需要配置在bootstrap文件,而非application.yml文件中。在Spring Cloud 2020.0.0版本之后,bootstrap文件不会被自动加载,需要主动添加依赖项,来开启bootstrap的自动加载流程。
为什么集成Nacos配置中心必须用到bootstrap配置文件呢?为了保证其他应用能够正常启动,必须 在其它组件初始化之前从Nacos读到所有配置项,之后再将获取到的配置项用于后续的初始化流程。
添加本地Nacos Config配置项
需要在bootstrap.yml文件中添加一些Nacos Config配置项
spring:
# 必须把name属性从application.yml迁移过来,否则无法动态刷新
application:
name: coupon-customer-serv
cloud:
nacos:
config:
# nacos config服务器的地址
server-addr: localhost:8848
file-extension: yml
# prefix: 文件名前缀,默认是spring.application.name
# 如果没有指定命令空间,则默认命令空间为PUBLIC
namespace: dev
# 如果没有配置Group,则默认值为DEFAULT_GROUP
group: DEFAULT_GROUP
# 从Nacos读取配置项的超时时间
timeout: 5000
# 长轮询超时时间
config-long-poll-timeout: 10000
# 轮询的重试时间
config-retry-time: 2000
# 长轮询最大重试次数
max-retry: 3
# 开启监听和自动刷新
refresh-enabled: true
# Nacos的扩展配置项,数字越大优先级越高
extension-configs:
- dataId: redis-config.yml
group: EXT_GROUP
# 动态刷新
refresh: true
- dataId: rabbitmq-config.yml
group: EXT_GROUP
refresh: true
长轮询机制 的工作原理
当Client向Nacos Config服务端发起一个配置查询请求时,服务端并不会立即返回查询结果,而是会将这个请求hold一段时间。如果在这段时间内有配置项数据的变更,那么服务端会触发变更事件,客户端将会监听到该事件,并获取相关配置变更;如果这段时间内没有发生数据变更,那么在这段“hold时间”结束后,服务端将释放请求。
采用长轮询机制可以降低多次请求带来的网络开销,并降低更新配置项的延迟。
动态配置推送
使用@Value注解将Nacos配置中心里的属性注入进来。给属性设置一个默认值,这样做的目的是加一层容错机制。即便Nacos Config连接异常无法获取配置项,应用程序也可以使用默认值完成启动加载。
最后,在类头上添加一个RefreshScope注解,有了这个注解,Nacos Config中的属性变动就会动态同步到当前类的变量中。如果不添加RefreshScope注解,即便应用程序监听到了外部属性变更,那么类变量的值也不会被刷新。
RefreshScope注解
为了实现动态刷新配置,主要就是想办法达成以下两个核心目标:
让Spring容器重新加载Environment环境配置变量
Spring Bean重新创建生成
@RefreshScope主要就是基于@Scope注解的作用域代理的基础上进行扩展实现的,加了@RefreshScope注解的类,在被Bean工厂创建后会加入自己的refresh scope 这个Bean缓存中,后续会优先从Bean缓存中获取,当配置中心发生了变更,会把变更的配置更新到spring容器的Environment中,并且同事bean缓存就会被清空,从而就会从bean工厂中创建bean实例了,而这次创建bean实例的时候就会继续经历这个bean的生命周期,使得@Value属性值能够从Environment中获取到最新的属性值,这样整个过程就达到了动态刷新配置的效果。
Sentinel
在Sentinel的世界中,万物都是可以被保护的“资源”,当一个外部请求想要访问Sentinel的资源时,便会创建一个Entry对象,经过Slot链路的层层考验最终完成自己的业务,可以把Slot当成是一类完成特定任务的“Filter”, 这是一种典型的职责链设计模式。
在这些Slot中,有几个是被专门用来 收集数据 的。比如:
NodeSelectorSlot 被用来构建当前请求的访问路径,它将上下游调用链串联起来,形成了一个服务调用关系的树状结构。
ClusterBuilderSlot 和 StatisticSlot 这两个Slot会从多个维度统计一些运行期信息,比如接口响应时间、服务QPS、当前线程数等等。
由这几个Slot统计出来的结果,会为后续的限流降级等Sentinel策略提供数据支持。
Sentinel还有很多被用作“规则判断”的Slot。比如:
FlowSlot 被用来做流控规则的判定, DegradeSlot 被用来做降级熔断判定,这两个Slot是平时在项目中使用频率最高的服务容错功能。
ParamFlowSlot 可以根据请求参数做精细粒度的流控,它经常被用来在大型应用中控制热点数据所带来的突发流量。
AuthoritySlot 可以针对特定资源设置黑白名单,限制某些应用对资源的访问。
除此之外,Sentinel的Slot机制也具备一定的扩展性,如果想要添加一个自定义的Slot,可以通过实现ProcessorSlot接口来完成,而且还可以通过优先级调整各个Slot之间的执行顺序。
运行Sentinel控制台
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.2.jar
将微服务接入到Sentinel控制台
首先,需要把Sentinel的依赖项引入到项目里
dependency
groupId com.alibaba.cloud /groupId
artifactId spring-cloud-starter-alibaba-sentinel /artifactId
/dependency
然后,需要做一些基本的配置
spring:
cloud:
sentinel:
transport:
# sentinel api端口,默认8719
port: 8719
# dashboard地址
dashboard: localhost:8080
Sentinel会为Controller中的API生成一个默认的资源名称,这个名称就是URL的路径,也可以使用特定的注解为资源打上一个指定的名称标记。
@SentinelResource(value = "getTemplateInBatch", blockHandler = "getTemplateInBatch_block")
注解中的blockHandler属性为当前资源指定了限流后的降级方法,如果当前服务抛出了BlockException,那么就会转而执行这段限流方法。
设置流控规则
Sentinel支持三种不同的流控模式,分别是直接流控、关联流控和链路流控。
直接流控:直接作用于当前资源,如果访问压力大于某个阈值,后续请求将被直接拦下来;
关联流控:当关联资源的访问量达到某个阈值时,对当前资源进行限流;
在“关联资源”一栏填了getTemplate,写在这里的是高优先级资源的名称。同时,设置了阈值判断条件为QPS=1,它的意思是,如果高优先级资源的访问频率达到了每秒一次,那么低优先级资源就会被限流。
关联限流的阈值判断是作用于高优先级资源之上的,但是流控效果是作用于低优先级资源之上。
链路流控:当指定链路上的访问量大于某个阈值时,对当前资源进行限流,这里的“指定链路”是细化到API级别的限流维度。
在上面的图里,一个服务应用中有/api/edit和/api/add两个接口,这两个接口都调用了同一个资源resource-1。如果想只对/api/edit接口流进行限流,那么就可以将“链路流控”应用在resource-1之上,同时指定当前流控规则的“入口资源”是/api/edit。
实现针对调用源的限流
在微服务架构中,一个服务可能被多个服务调用。比如说,Customer服务会调用Template服务的getTemplateInBatch资源,未来可能会研发一个新的服务叫coupon-other-serv,它也会调用相同资源。
如果想为getTemplateInBatch资源设置一个限流规则,并指定其只对来自Customer服务的调用起作用
这个实现过程分为两步。第一步,要想办法在服务请求中加上一个特殊标记,告诉Template服务是谁调用;第二步,需要在Sentinel控制台设置流控规则的针对来源。
第一步。首先,将调用源的应用名加入到由OpenFeign组件构造的Request中。可以借助OpenFeign的RequestInterceptor扩展接口,编写一个自定义的拦截器,在服务请求发送出去之前,往Request的Header里写入一个特殊变量,传递给下游服务的“来源标记”
@Configuration
public class OpenfeignSentinelInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("SentinelSource", "coupon-customer-serv");
接下来,需要在Template服务中识别来自上游的标记,并将其加入到Sentinel的链路统计中。可以借助Sentinel提供的RequestOriginParser扩展接口,编写一个自定义的解析器。
@Component
@Slf4j
public class SentinelOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
log.info("request {}, header={}", request.getParameterMap(), request.getHeaderNames());
return request.getHeader("SentinelSource");
在方法中,从服务请求的Header中获取SentinelSource变量的值,作为调用源的name
第二步。在流控规则的编辑页面,“针对来源”这一栏填上coupon-customer-serv并保存,这样一来,当前限流规则就只会针对来自Customer服务的请求生效了。
Sentinel的流控效果
快速失败,Sentinel默认的流控效果,在快速失败模式下,超过阈值设定的请求将会被立即阻拦住。
Warm Up 则实现了“预热模式的流控效果”,这种方式可以平缓拉高系统水位,避免突发流量对当前处于低水位的系统的可用性造成破坏。举个例子,如果设置的系统阈值是QPS=10,预热时间=5,那么Sentinel会在这5秒的预热时间内,将限流阈值从3缓慢拉高到10。为什么起始阈值是3呢?因为Sentinel内部有一个冷加载因子,它的值是3,在预热模式下,起始阈值的计算公式是单机阈值/冷加载因子,也就是10/3=3。
排队等待 模式下,超过阈值的请求不会立即失败,而是会被放入一个队列中,排好队等待被处理。一旦请求在队列中等待的时间超过了设置的超时时间,那么请求就会被从队列中移除。
异常降级方案
使用blockHandler属性指定降级方法的名称,只能在服务抛出BlockException的情况下执行降级逻辑。
BlockException这个异常类是Sentinel组件自带的类,当一个请求被Sentinel规则拦截,这个异常便会被抛出。比如请求被Sentinel流控策略阻拦住,或者请求被熔断策略阻断了,这些情况下可以使用SentinelResource注解的blockHandler来指定降级逻辑。对于其它RuntimeException的异常类型它就无能为力了。
使用SentinelResource中的另一个属性fallback可以指定一段通用的降级逻辑。
需要注意,如果降级方法的方法签名是BlockException,那么fallback是无法正常工作的。在注解中同时使用了fallback和blockHandler属性,如果服务抛出BlockException,则执行blockHandler属性指定的方法,其他异常就由fallback属性所对应的降级方法接管。
可以通过SentinelResource注解的fallbackClass属性指定一个保存降级逻辑的Class。
在控制台添加熔断策略
Sentinel的熔断规则有3种,分别是异常比例、异常数和慢调用比例。
指定以“ 异常比例”为熔断开关的判断逻辑。指定10秒的统计窗口内,如果异常调用的比例超过了60%,并且满足请求数量 =5,就开启一段为期5秒的熔断时间。
“熔断时长”的时间单位是秒,而“统计窗口”的时间单位是毫秒
Sentinel底层通过一段跨度为10秒的滑动窗口来统计服务调用情况。在这段窗口时间内,前三个服务请求全部失败,这时失败率已经达到100%,大大超过了定义的60%的阈值,但是熔断开关却没有打开,这是因为统计窗口的最小请求数还没有达到设定值5。
之后又有两个请求被处理,一个成功一个失败,这时请求个数已经达到了5,失败率是80%,那么Sentinel就开启了一段5秒的熔断时间。在这段时间内,所有来访请求都不会得到真实的执行,而是转而执行降级逻辑。
“ 异常数”熔断规则和前面设置的异常比例熔断规则几乎一样,唯一的区别就是“异常数”的判定条件是统计窗口内发生异常的个数。
熔断器开启的判定条件是异常数 2
慢调用比例
通常来说,慢调用请求所占比例逐渐增多,这是服务雪崩的前兆。为了将影响范围缩小,要做的就是 尽早捕捉到慢调用请求的比例变化趋势,及时通过熔断规则对服务进行减压。
在10秒的统计窗口内,如果响应时间大于1000ms的请求所占总请求数量的比例超过了0.4,并且请求总数量 =5,此时将触发Sentinel的熔断开关,开启5秒的熔断窗口。
熔断开关的状态转换
Sentinel的熔断器会在开启、关闭和半开这三种逻辑状态之间来回切换
从图中可以看出,在第一个统计窗口内熔断器是处于关闭状态的,达到熔断判定条件之后,Sentinel开启了一段熔断窗口。在这段窗口时间内,熔断器是处于开启状态的,这时新的服务请求会执行降级逻辑。待熔断窗口结束,Sentinel会将熔断器状态置为“半开”状态,这是一个介于完全开启和完全关闭之间的中间态。
在半开状态下,如果有一个新请求过来,那么Sentinel会试探性地让这个请求去执行正常的业务逻辑,如果执行成功,那么Sentinel将关闭熔断器并退出熔断状态,如果执行失败,那么Sentinel将再次开启一个新的熔断窗口。
接入 Nacos 实现规则持久化
通过集成Nacos Config来实现持久化方案,需要把Sentinel中设置的限流规则保存到Nacos配置中心。这样一来,当应用服务或Sentinel Dashboard重新启动时,它们就可以自动把Nacos中的限流规则同步到本地,不管怎么重启服务都不会导致规则失效了。
Sentinel控制台将限流规则同步到了Nacos Config服务器来实现持久化。同时,在应用程序中,配置了一个Sentinel Datasource,从Nacos Config服务器获取具体配置信息。
在应用启动阶段,程序会主动从Sentinel Datasource获取限流规则配置。而在运行期,也可以在Sentinel控制台动态修改限流规则,应用程序会实时监听配置中心的数据变化,进而获取变更后的数据。
Sentinel组件二次开发
需要将Sentinel的代码下载到本地。可以从 GitHub的Releases页面 的Assets面板中下载Source code源文件。
将项目导入到开发工具中主要针对其中的sentinel-dashboard子模块做二次开发。整个改造过程按照先后顺序将分为三个步骤:
修改Nacos依赖项的应用范围,将其打入jar包中;
后端程序对接Nacos,将Sentinel限流规则同步到Nacos;
开放单独的前端限流规则配置页面。
修改Nacos依赖项
sentinel-dashboard项目的pom.xml文件中的依赖项sentinel-datasource-nacos是连接Nacos Config所依赖的必要组件。需要将这个依赖项的scope标签注释掉。
dependency
groupId com.alibaba.csp /groupId
artifactId sentinel-datasource-nacos /artifactId
!-- 将scope注释掉,改为编译期打包 --
!-- scope test /scope --
/dependency
后端程序对接Nacos
打开sentinel-dashboard项目下的src/test/java目录(注意是test目录而不是main目录),然后定位到com.alibaba.csp.sentinel.dashboard.rule.nacos包。在这个包下面,看到4个和Nacos Config有关的类,它们的功能描述如下:
NacosConfig:初始化Nacos Config的连接;
NacosConfigUtil:约定了Nacos配置文件所属的Group和文件命名后缀等常量字段;
FlowRuleNacosProvider:从Nacos Config上获取限流规则;
FlowRuleNacosPublisher:将限流规则发布到Nacos Config。
为了让这些类在Sentinel运行期可以发挥作用,需要在src/main/java下创建同样的包路径,然后将这四个文件从test路径拷贝到main路径下。
NacosConfig类中配置Nacos连接串:
打开NacosConfig类,找到其中的nacosConfigService方法。这个方法创建了一个ConfigService类,它是Nacos Config定义的通用接口,提供了Nacos配置项的读取和更新功能。FlowRuleNacosProvider和FlowRuleNacosPublisher这两个类都是基于这个ConfigService类实现Nacos数据同步的。改造后的代码:
@Bean
public ConfigService nacosConfigService() throws Exception {
// 将Nacos的注册地址引入进来
//也可以通过配置文件来注入serverAddr和namespace等属性。
Properties properties = new Properties();
properties.setProperty("serverAddr", "localhost:8848");
properties.setProperty("namespace", "dev");
return ConfigFactory.createConfigService(properties);
在Controller层接入Nacos来实现限流规则持久化:
在FlowControllerV2中正式接入Nacos。FlowControllerV2对外暴露了REST API,用来创建和修改限流规则。在这个类的源代码中,需要修改两个变量的Qualifier注解值。
@Autowired
// 指向刚才从test包中迁移过来的FlowRuleNacosProvider类
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider List FlowRuleEntity ruleProvider;
@Autowired
// 指向刚才从test包中迁移过来的FlowRuleNacosPublisher类
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher List FlowRuleEntity rulePublisher;
通过Qualifier标签将FlowRuleNacosProvider注入到了ruleProvier变量中,又采用同样的方式将FlowRuleNacosPublisher注入到了rulePublisher变量中。FlowRuleNacosProvider和FlowRuleNacosPublisher就是从test目录Copy到main目录下的两个类。
修改完成之后,FlowControllerV2底层的限流规则改动就会被同步到Nacos服务器了。这个同步工作是由FlowRuleNacosPublisher执行的,它会发送一个POST请求到Nacos服务器来修改配置项。
FlowRuleNacosPublisher会在Nacos Config上创建一个用来保存限流规则的配置文件,这个配置文件以“application.name”开头,以“-flow-rules”结尾,而且它所属的Group为“SENTINEL_GROUP”。这里用到的文件命名规则和Group都是通过NacosConfigUtil类中的常量指定的。
前端页面改造
打开sentinel-dashboard模块下的webapp目录,该目录存放了Sentinel控制台的前端页面资源。需要改造的文件是sidebar.html,这个html文件定义了控制台的左侧导航栏。
li ui-sref-active="active"
a ui-sref="dashboard.flow({app: entry.app})"
i /i nbsp; nbsp;流控规则持久化 /a
/li
微服务改造
只需要添加一个新的sentinel-datasource-nacos依赖项,并在配置文件中添加sentinel datasource连接信息就可以了
dependency
groupId com.alibaba.csp /groupId
artifactId sentinel-datasource-nacos /artifactId
/dependency
spring:
cloud:
sentinel:
datasource:
# 数据源的key,可以自由命名
geekbang-flow:
# 指定当前数据源是nacos
nacos:
# 设置Nacos的连接地址、命名空间和Group ID
server-addr: localhost:8848
namespace: dev
groupId: SENTINEL_GROUP
# 设置Nacos中配置文件的命名规则
dataId: ${spring.application.name}-flow-rules
# 必填的重要字段,指定当前规则类型是"限流"
rule-type: flow
在微服务端的sentinal数据源中配置的namespace和groupID,一定要和Sentinal Dashoboard二次改造中的中的配置相同,否则将无法正常同步限流规则。Sentinal Dashboard中namespace是在NacosConfig类中指定的,而groupID是在NacosConfigUtil类中指定的。
dataId的文件命名规则,需要和Sentinel二次改造中的FlowRuleNacosPublisher类保持一致,如果修改了FlowRuleNacosPublisher中的命名规则,那么也要在每个微服务端做相应的变更。
调用链追踪:集成 Sleuth 和 Zipkin
Sleuth
如果想提高线上异常排查的效率,那么首先要做的一件事就是: 将一次调用请求中所有访问到的微服务日志前后串联起来。
链路追踪技术会为每次服务调用生成一个全局唯一的ID(Trace ID),从本次服务调用的起点到终点,这个过程中的所有日志信息都会被打上Trace ID的烙印。这样一来,根据日志中的Trace ID,就能很清晰地梳理出一次服务请求前后都经过了哪些微服务节点。
Sleuth的底层逻辑
调用链追踪有两个任务,一是 标记出一次调用请求中的所有日志,二是 梳理日志间的前后关系。
集成了Sleuth组件之后,它会向日志中打入三个“特殊标记”,其中一个标记是Trace ID。剩下的两个标记分别是Span ID和Parent Span ID,这俩用来表示调用的前后顺序关系。
Trace ID完成的是第一个任务:标记,用来标记调用链的全局唯一ID。
Span是Sleuth下面的一个基本工作单元,当服务请求抵达当前单元时,Sleuth就会为这个单元分配一个独一无二的Span ID,并标记单元的开始时间和结束时间,这样就可以记录每个单元的处理用时了。
Parent Span ID指向了当前单元的父级单元,也就是上游的调用者。一个环环相扣的调用链就通过Parent Span ID被串了起来。
上面的图示只是一个简化的流程,在实际的项目中,一次服务调用可不光只会生成一个Span。比如说服务A请求通过OpenFeign组件调用了服务B,那么服务A接收用户请求的过程就是一个单元,而OpenFeign组件发起远程调用的过程又是另一个单元。由此可见,单元的颗粒度其实是非常小的。
Sleuth还有一个特殊的数据结构,叫做Annotation,被用来记录一个具体的“事件”。
集成Sleuth实现链路打标
将Sleuth的依赖项添加到pom.xml文件中
!-- Sleuth依赖项 --
dependency
groupId org.springframework.cloud /groupId
artifactId spring-cloud-starter-sleuth /artifactId
/dependency
application.yml配置文件
spring:
sleuth:
sampler:
# 采样率的概率,100%采样
probability: 1.0
# 每秒采样数字最高为1000
rate: 1000
probability,是一个0到1的浮点数,用来表示 采样率。这里设置的probability是1,就表示对请求进行100%采样。如果把probability设置成小于1的数,就说明有的请求不会被采样。如果一个请求未被采样,那么它将不会被调用链追踪系统Track起来。
rate参数,它代表 每秒最多可以对多少个Request进行采样。这有点像一个“限流”参数,如果超过这个阈值,服务请求仍然会被正常处理,但调用链信息不会被采样。
Sleuth如何在调用链中传递标记
Sleuth为了将Trace ID和调用方服务的Span ID传递给被调用的微服务,它在OpenFeign的环节动了一个手脚。Sleuth通过 TracingFeignClient类,将一系列Tag标记塞进了OpenFeign构造的服务请求的Header结构中。
在TracingFeignClient的类中打了一个Debug断点,将Request的Header信息打印出来:
在这个Header结构中,可以看到有几个以X-B3开头的特殊标记,这个X-B3就是Sleuth的特殊接头暗号。其中X-B3-TraceId就是全局唯一的链路追踪ID,而X-B3-SpanId和X-B3-ParentSpandID分别是当前请求的单元ID和父级单元ID,最后的X-B3-Sampled则表示当前链路是否是一个已被采样的链路。通过Header里的这些信息,下游服务就完整地得到了上游服务的情报。
使用Zipkin收集并查看链路数据
Zipkin是一个分布式的Tracing系统,它可以用来收集时序化的链路打标数据。通过Zipkin内置的UI界面,可以根据Trace ID搜索出一次调用链所经过的所有访问单。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。