认识微服务
随着互联网行业的蓬勃发展,用户规模和业务复杂度呈指数级增长,对服务的要求也水涨船高。传统的单体架构逐渐暴露出难以扩展、维护困难等弊端,无法满足日益增长的需求。在这种背景下,微服务架构应运而生,并迅速成为主流。
单体架构 vs 微服务架构:
单体架构
微服务架构
为了更好地理解微服务架构的优势,我们需要先了解它与传统单体架构的区别:
特性 | 单体架构 | 微服务架构 |
---|---|---|
架构模式 | 将所有功能模块打包在一个应用程序中 | 将应用程序拆分为多个独立的服务 |
部署方式 | 整体部署,更新迭代影响整个应用 | 独立部署,每个服务可以独立开发、测试、部署和扩展 |
扩展性 | 垂直扩展,受限于单个服务器的性能 | 水平扩展,可以根据需求独立扩展每个服务 |
技术栈 | 统一技术栈,技术选型受限 | 每个服务可以使用不同的技术栈,灵活性高 |
故障隔离 | 单点故障,一个模块出问题会影响整个应用 | 故障隔离,一个服务出问题不会影响其他服务 |
开发效率 | 开发、测试、部署周期长,协作困难 | 开发、测试、部署周期短,团队协作更高效 |
运维复杂度 | 相对简单,但难以应对大规模应用 | 相对复杂,需要更完善的监控、日志、服务治理等机制 |
微服务架构的优势:
-
灵活性高: 每个服务独立开发、部署和扩展,可以根据业务需求选择最适合的技术栈。
-
可扩展性强: 可以根据需求独立扩展每个服务,避免资源浪费。
-
故障隔离: 一个服务出问题不会影响其他服务,提高系统稳定性。
-
开发效率高: 团队可以并行开发不同的服务,缩短开发周期。
-
易于维护: 每个服务职责单一,代码量少,易于理解和维护。
因此,可以认为微服务是一种经过良好架构设计的分布式架构方案 。
SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:Spring Cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
其中常见的组件包括:
另外,SpringCloud底层是依赖于SpringBoot的,并且有版本的兼容关系,如下:
SpringCloud与SpringBoot的版本的兼容关系
主要功能组件:
- 服务注册与发现: 例如 Eureka、Consul。
- 服务调用: 例如 Feign、RestTemplate。
- 负载均衡: 例如 Ribbon。
- 服务熔断: 例如 Hystrix。
- 服务网关: 例如 Zuul、Spring Cloud Gateway。
- 配置中心: 例如 Spring Cloud Config。
- 消息总线: 例如 Spring Cloud Bus。
- 分布式链路追踪: 例如 Sleuth + Zipkin。
总结
-
单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
-
分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
-
微服务:一种良好的分布式架构方案
①优点:拆分粒度更小、服务更独立、耦合度更低
②缺点:架构非常复杂,运维、监控、部署难度提高
-
SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件
服务拆分和远程调用
任何分布式架构都离不开服务的拆分,微服务也是一样。
服务拆分原则
在微服务架构中,服务拆分是至关重要的一步。合理的服务拆分能够提高系统的可维护性、可扩展性和灵活性。以下是一些微服务拆分的原则:
1. 单一职责原则 (Single Responsibility Principle, SRP)
- 原则: 每个微服务应该只负责一个单一的业务功能或业务流程。
- 解释: 单一职责原则有助于降低服务的复杂性,使得每个服务更加专注于特定的业务领域,从而提高代码的可维护性和可测试性。
- 示例: 如果一个服务既负责用户管理,又负责订单处理,那么应该将其拆分为两个独立的服务:一个负责用户管理,另一个负责订单处理。
2. 高内聚低耦合 (High Cohesion, Low Coupling)
- 原则: 微服务内部的功能应该高度内聚,而不同微服务之间的耦合度应该尽量低。
- 解释: 高内聚意味着服务内部的功能紧密相关,低耦合意味着服务之间的依赖关系尽量少。这样可以降低系统的复杂性,提高服务的独立性和可重用性。
- 示例: 用户服务和订单服务应该尽量独立,用户服务不应该直接访问订单服务的数据库。
3. 避免重复开发相同业务
- 原则: 不同微服务之间不要重复开发相同的业务逻辑。
- 解释: 重复开发会导致代码冗余,增加维护成本,并且容易引发数据一致性问题。可以通过共享库或服务调用来避免重复开发。
- 示例: 如果多个服务都需要进行用户认证,可以将认证逻辑提取到一个独立的认证服务中,其他服务通过调用该服务来实现认证功能。
4. 数据独立性
- 原则: 每个微服务应该拥有自己的数据库,并且不应该直接访问其他微服务的数据库。
- 解释: 数据独立性有助于保证服务的独立性和数据的安全性。如果一个服务需要访问其他服务的数据,应该通过服务接口进行调用,而不是直接访问数据库。
- 示例: 订单服务不应该直接访问用户服务的数据库,而是通过用户服务提供的接口来获取用户信息。
5. 接口暴露
- 原则: 微服务可以将自己的业务功能暴露为接口,供其他微服务调用。
- 解释: 通过接口暴露业务功能,可以实现服务之间的松耦合,并且便于服务的扩展和维护。
- 示例: 用户服务可以提供一个接口,允许其他服务查询用户信息,订单服务可以通过调用该接口来获取用户信息。
6. 关注点分离 (Separation of Concerns)
- 原则: 将不同的关注点分离到不同的服务中。
- 解释: 关注点分离有助于降低系统的复杂性,使得每个服务更加专注于特定的业务领域。
- 示例: 将业务逻辑、数据访问、消息处理等不同的关注点分离到不同的服务中。
7. 可扩展性
- 原则: 服务拆分应该考虑到未来的扩展需求。
- 解释: 服务拆分应该使得系统能够方便地进行水平扩展,以应对未来的业务增长。
- 示例: 将用户服务拆分为多个子服务,例如用户注册服务、用户认证服务等,以便在未来根据需求独立扩展这些子服务。
8. 团队独立性
- 原则: 服务拆分应该考虑到团队的独立性。
- 解释: 每个服务应该由一个独立的团队负责开发和维护,这样可以提高团队的自主性和开发效率。
- 示例: 将用户服务和订单服务分配给不同的团队,每个团队可以独立开发和部署自己的服务。
提供者与消费者
在微服务架构中,服务调用关系中存在两个不同的角色:服务提供者和服务消费者。这两个角色并不是绝对的,而是相对于具体的业务调用关系而言。
- 服务提供者:服务提供者是指在一次业务中,被其他微服务调用的服务。它提供接口给其他微服务使用。
- 服务消费者:服务消费者是指在一次业务中,调用其他微服务的服务。它通过调用其他微服务提供的接口来完成业务逻辑。
- 角色相对性:服务提供者与服务消费者的角色并不是固定的,而是相对于具体的业务调用关系而言。一个服务在不同的业务调用关系中可能扮演不同的角色。
示例
假设有三个服务:服务A、服务B 和 服务C。
- 服务A 调用了 服务B。
- 服务B 调用了 服务C。
在这种情况下:
对于A调用B的业务而言:
- 服务A 是服务消费者。
- 服务B 是服务提供者。
对于B调用C的业务而言:
- 服务B 是服务消费者。
- 服务C 是服务提供者。
因此,服务B 既可以是服务提供者,也可以是服务消费者,这取决于它所处的业务调用关系。
Ribbon负载均衡
在 Spring Cloud 中,Ribbon 是一个客户端负载均衡器,它可以帮助我们在微服务架构中实现负载均衡。Ribbon 通过与服务注册中心(如 Nacos )集成,自动发现可用的服务实例,并根据负载均衡策略将请求分发到不同的实例上。
Ribbon 通过拦截 RestTemplate 的请求,并根据服务名称从 Nacos 获取服务实例列表,然后利用负载均衡算法选择一个实例,最终将请求发送到选中的实例上。
Spring Cloud Ribbon 的负载均衡机制通过以下步骤实现:
- 拦截请求: LoadBalancerInterceptor 拦截 RestTemplate 的请求。
- 获取服务名称: 从请求 URI 中提取服务名称。
- 获取服务实例列表: ILoadBalancer 从 Eureka 获取服务实例列表。
- 选择服务实例: IRule 根据负载均衡策略选择一个服务实例。
- 发送请求: 将请求发送到选中的服务实例。
负载均衡策略
负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
不同规则的含义如下:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
默认的实现就是ZoneAvoidanceRule,是一种轮询方案
自定义负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
代码方式:在 order-service 的启动类中,通过定义一个新的 IRule Bean 来修改负载均衡规则:
@Bean
public IRule randomRule() {
return new RandomRule();
}
配置文件方式:在 order-service 的 application.yml 文件中,添加新的配置来修改负载均衡规则:
userservice: # 给某个微服务配置负载均衡规则,这里是 userservice 服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意,一般用默认的负载均衡规则,不做修改。
饥饿加载
Ribbon 默认采用懒加载(Lazy Loading),即在第一次访问时才会去创建 LoadBalanceClient,这会导致第一次请求的时间较长。为了降低第一次访问的耗时,可以通过配置开启饥饿加载(Eager Loading),即在项目启动时创建 LoadBalanceClient。
ribbon:
eager-load:
enabled: true
clients: userservice
Nacos注册中心
认识和安装Nacos
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
Windows安装
开发阶段采用单机安装即可。
下载安装包
在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:
GitHub主页:https://github.com/alibaba/nacos/releases/tag/2.4.2.1
GitHub的Release下载页:https://github.com/alibaba/nacos/releases
如图:
windows版本使用nacos-server-2.4.2.1.zip包即可。
解压
将这个包解压到任意非中文目录下,如图:
目录说明:
-
bin:启动脚本
-
conf:配置文件
端口配置
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。
如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件中的端口:
修改其中的内容:
启动
启动非常简单,进入bin目录,结构如下:
然后执行命令即可:
windows命令: startup.cmd -m standalone
执行后的效果如图:
访问
在浏览器输入地址:http://127.0.0.1:8848/nacos即可:
默认的账号和密码都是nacos,进入后:
服务注册到nacos
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:
-
依赖不同
-
服务地址不同
引入依赖
在父工程的pom文件中引入依赖:
<?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 https://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>3.2.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cloud</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging> <!-- 这里设置为pom -->
<name>demo</name>
<description>demo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-alibaba-dependencies -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.1.0</version>
<type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 定义子模块 -->
<modules>
<module>service1</module>
</modules>
</project>
service1子工程
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- 指向父项目的 pom 文件 -->
</parent>
<artifactId>service1</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2023.0.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2023.0.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
application.properties
# 服务端口
server.port=8081
# 服务名称
spring.application.name=service1
# Nacos 服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 启用 Sentinel,并连接 Sentinel Dashboard
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8080
spring.cloud.sentinel.transport.port=8719 # 本地 Sentinel 客户端的监控端口
# 启用 Sentinel
spring.cloud.sentinel.enabled=true
登录nacos管理页面,可以看到微服务信息:
服务分级存储模型
在微服务架构中,服务实例通常分布在不同的地理位置或数据中心,为了提高服务的可用性和性能,通常会将服务实例划分为不同的集群。Nacos 提供了一种服务分级存储模型,允许将服务实例按照集群进行组织和管理。
服务分级存储模型概述
一个服务可以有多个实例,例如 user-service 可以有以下实例:
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
这些实例可以分布在不同的机房,例如:
- 127.0.0.1:8081 在上海机房
- 127.0.0.1:8082 在上海机房
- 127.0.0.1:8083 在杭州机房
Nacos 将同一机房内的实例划分为一个集群。也就是说,user-service 是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
集群优先访问策略
在微服务互相访问时,应该尽可能访问同集群的实例,因为本地访问速度更快。当本集群内不可用时,才访问其他集群。例如:
配置集群信息
在 Nacos 中,可以通过配置文件或 API 来指定服务的集群信息。以下是一个示例配置:
1. 配置文件方式
在 application.yml 中配置集群信息:
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
cluster-name: hangzhou
2. API 方式
通过 Nacos 的 API 注册服务时,可以指定集群信息:
Instance instance = new Instance();
instance.setIp("127.0.0.1");
instance.setPort(8081);
instance.setClusterName("hangzhou");
NamingService namingService = NamingFactory.createNamingService("localhost:8848");
namingService.registerInstance("user-service", instance);
集群优先访问的实现
Nacos 提供了 ZoneAvoidanceRule 负载均衡策略,它会优先选择同集群的服务实例。如果同集群的实例不可用,才会选择其他集群的实例。
配置负载均衡策略
在 application.yml 中配置负载均衡策略:
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule
权重配置
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
在弹出的编辑窗口,修改权重:
注意:如果权重修改为0,则该实例永远不会被访问
Nacos 与 Eureka 的区别
Nacos 和 Eureka 都是常用的服务注册与发现组件,它们在功能和实现上有一些相似之处,但也存在一些显著的区别。
服务实例类型
Nacos 的服务实例分为两种类型:
-
临时实例:如果实例宕机超过一定时间,会从服务列表中剔除,默认类型。
-
非临时实例(永久实例):如果实例宕机,不会从服务列表中剔除。
可以通过配置将服务实例设置为非临时实例:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例
Nacos 与 Eureka 的共同点
-
服务注册与发现:都支持服务注册和服务拉取。
-
健康检测:都支持服务提供者通过心跳方式进行健康检测。
Nacos 与 Eureka 的区别
1. 健康检测方式
Nacos:
- 临时实例:采用心跳模式,如果心跳不正常,实例会被剔除。
- 非临时实例:采用主动检测模式,实例宕机不会被剔除。
Eureka:
- 仅支持心跳模式,实例宕机后会从服务列表中剔除。
2. 服务列表更新机制
- Nacos:支持服务列表变更的消息推送模式,服务列表更新更及时。
- Eureka:采用客户端定期拉取服务列表的方式,更新相对滞后。
3. 集群模式
- Nacos:默认采用 AP 方式(可用性和分区容错性)。当集群中存在非临时实例时,采用 CP 方式(一致性和分区容错性)。
- Eureka:采用 AP 方式,强调高可用性和分区容错性,但不保证一致性。