MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

微服务架构的核心原则与实践

2022-08-076.7k 阅读

微服务架构概述

在当今快速发展的软件行业,随着应用程序规模的不断扩大和业务需求的日益复杂,传统的单体架构逐渐暴露出一些弊端。微服务架构作为一种新兴的架构风格,应运而生并受到广泛关注。它将一个大型的应用程序拆分成多个小型、独立的服务,每个服务都围绕特定的业务能力构建,这些服务可以独立开发、部署和扩展,通过轻量级的通信机制进行交互。

微服务架构的特点

  1. 单一职责:每个微服务都专注于完成一项特定的业务功能,例如用户管理服务专门负责处理与用户相关的业务逻辑,订单管理服务专注于订单的创建、修改和查询等操作。这种单一职责原则使得微服务的功能清晰明确,易于理解、开发和维护。
  2. 独立部署:每个微服务都可以独立地进行部署,这意味着当某个微服务需要更新或修复 bug 时,不需要重新部署整个应用程序,只需要对该微服务进行单独部署即可。例如,电商应用中的商品服务需要增加新的商品属性时,只需要部署商品微服务,而不会影响到订单、支付等其他微服务。
  3. 轻量级通信:微服务之间通过轻量级的通信协议进行交互,如 HTTP/REST。这种通信方式简单、灵活,易于实现和集成。以一个在线教育平台为例,课程服务可以通过 HTTP 请求向用户服务获取用户的学习记录,从而为用户推荐合适的课程。
  4. 自治:每个微服务都有自己独立的数据库、运行环境和开发团队。这使得不同的微服务可以根据自身的业务需求选择最合适的技术栈。比如,用户服务可以使用 MySQL 数据库,而搜索服务可以采用 Elasticsearch 来满足其高效搜索的需求。

微服务架构的核心原则

业务能力驱动原则

  1. 以业务为导向拆分服务:微服务架构的设计应该紧密围绕业务能力进行拆分。例如,在一个社交媒体平台中,根据业务功能可以拆分为用户服务、动态服务、评论服务等。用户服务负责管理用户的注册、登录、资料修改等业务;动态服务负责处理用户发布的动态内容,包括创建、删除和展示;评论服务则专注于对动态的评论管理。通过这种基于业务能力的拆分,每个微服务都能清晰地界定自己的职责范围,使得开发团队能够更好地理解和维护业务逻辑。
  2. 业务的持续演进与服务调整:随着业务的发展和变化,微服务架构也需要相应地进行调整。例如,当社交媒体平台推出新的直播功能时,就需要创建直播服务,并对相关的用户服务、动态服务等进行适当的调整和集成,以支持新的业务流程。这要求开发团队要密切关注业务的发展趋势,及时对微服务架构进行优化和扩展。

独立自治原则

  1. 服务的独立开发与部署:每个微服务都应该由独立的开发团队负责开发和维护。这样可以使得不同的团队专注于自己负责的业务领域,提高开发效率。同时,独立部署特性允许每个微服务根据自身的需求进行灵活的部署策略。例如,对于一些高流量、高并发的微服务,可以采用多实例部署的方式来提高系统的性能和可用性;而对于一些相对稳定、流量较小的微服务,可以采用简单的单实例部署。
  2. 独立的数据管理:每个微服务拥有自己独立的数据库,这有助于实现数据的自治和隔离。以电商系统为例,订单服务可以使用关系型数据库来存储订单信息,而库存服务可以使用分布式数据库来管理库存数据。这样不同的微服务可以根据自身业务对数据的读写需求,选择最合适的数据库技术和存储方案。同时,独立的数据管理也避免了不同微服务之间因为共享数据库而产生的数据耦合问题。

轻量级通信原则

  1. 选择合适的通信协议:HTTP/REST 是微服务架构中最常用的轻量级通信协议。它基于现有的 Web 标准,具有良好的通用性和跨平台性。例如,在一个移动应用与后端微服务的交互中,移动客户端可以通过 HTTP 请求很方便地调用后端的用户服务接口来获取用户信息。除了 HTTP/REST,一些场景下也会使用消息队列(如 Kafka、RabbitMQ 等)进行异步通信。比如,在电商系统中,当用户下单后,订单服务可以将订单消息发送到消息队列,库存服务从消息队列中获取订单消息并进行库存扣减操作,这样可以实现服务之间的解耦和异步处理。
  2. 通信接口的设计:微服务之间的通信接口应该设计得简洁、清晰,易于理解和调用。接口的版本管理也非常重要,当微服务的接口发生变化时,通过版本号可以确保旧版本的接口仍然可用,避免对依赖该接口的其他微服务造成影响。例如,可以在接口 URL 中包含版本号,如 /v1/users 表示获取用户信息的 v1 版本接口。

故障隔离原则

  1. 防止故障扩散:在微服务架构中,一个微服务的故障不应该影响到其他微服务的正常运行。为了实现这一点,可以采用熔断机制和限流措施。熔断机制就像电路中的保险丝,当某个微服务出现故障,调用次数达到一定阈值且失败率超过一定比例时,熔断器会跳闸,暂时切断对该微服务的调用,避免大量无效请求继续发送到故障微服务,从而防止故障扩散。例如,在一个电商应用中,如果支付服务出现故障,订单服务可以通过熔断机制停止调用支付服务,提示用户稍后重试,同时订单服务自身仍然可以正常处理其他订单相关的业务。限流措施则是通过限制对某个微服务的请求频率,来保护该微服务不被过多的请求压垮。比如,对于一个提供天气预报信息的微服务,可以设置每分钟只允许每个客户端发送 100 次请求,防止恶意攻击或异常流量导致服务不可用。
  2. 容错处理:微服务之间的调用可能会因为网络问题、服务暂时不可用等原因失败,因此需要在调用方进行容错处理。常见的容错策略包括重试、降级等。重试是指当调用失败后,在一定时间间隔后再次尝试调用,直到成功或达到最大重试次数。例如,当调用商品服务获取商品详情失败时,订单服务可以重试 3 次。降级则是在某个微服务出现故障或性能下降时,调用方采用备用的、简化的逻辑来处理业务。比如,当图片服务出现故障时,电商应用的商品详情页面可以显示一个默认的占位图片,并提示用户图片加载失败,而不是让整个页面无法正常显示。

可观测性原则

  1. 日志记录:每个微服务都应该详细记录自身的运行日志,包括请求的接收、处理过程中的关键步骤、错误信息等。通过分析日志,可以快速定位问题发生的时间、原因和相关的上下文信息。例如,在一个用户登录服务中,日志应该记录每次登录请求的用户名、请求时间、是否登录成功以及失败的原因(如密码错误、账号被锁定等)。为了便于日志的管理和分析,可以采用集中式日志管理系统,如 Elasticsearch + Logstash + Kibana(ELK 堆栈),将各个微服务的日志集中存储和展示。
  2. 监控指标:需要为每个微服务定义一系列关键的监控指标,如 CPU 使用率、内存使用率、请求响应时间、吞吐量等。通过实时监控这些指标,可以及时发现微服务的性能问题和异常情况。例如,当某个微服务的 CPU 使用率持续超过 80%,或者请求响应时间突然变长,就可能意味着该微服务出现了性能瓶颈,需要及时进行优化。监控工具可以使用 Prometheus 和 Grafana,Prometheus 用于收集和存储监控指标数据,Grafana 则用于将这些数据以可视化的图表形式展示出来。
  3. 分布式追踪:在微服务架构中,一个业务请求可能会经过多个微服务的处理,分布式追踪可以帮助开发人员了解请求在各个微服务之间的流转路径和处理时间。例如,当用户在电商应用中下单时,请求可能会依次经过订单服务、库存服务、支付服务等多个微服务。通过分布式追踪技术(如 Jaeger),可以为每个请求生成一个唯一的追踪 ID,每个微服务在处理该请求时都会记录该追踪 ID 以及自身的处理时间,最终可以通过追踪 ID 在分布式追踪系统中查看整个请求的处理流程和各个微服务的耗时情况,从而快速定位性能问题或故障点。

微服务架构的实践

微服务的拆分策略

  1. 基于业务功能拆分:这是最常见的拆分方式,如前文提到的电商系统拆分为用户服务、商品服务、订单服务等。这种拆分方式直观,与业务紧密结合,便于开发团队理解和维护。以一个在线旅游平台为例,可以拆分为酒店预订服务、机票预订服务、旅游攻略服务等,每个服务负责各自的业务领域。
  2. 基于数据模型拆分:根据数据的独立性和关联性进行微服务的拆分。例如,在一个金融系统中,客户信息和交易记录是两个相对独立的数据集合,可以分别拆分为客户服务和交易服务。客户服务负责管理客户的基本信息、账户信息等,交易服务专注于处理各种交易操作和记录。这种拆分方式有利于数据的独立管理和维护,同时也能减少微服务之间的数据耦合。
  3. 基于性能和资源需求拆分:对于一些性能要求较高或资源消耗较大的业务功能,可以单独拆分为一个微服务。比如,在一个视频平台中,视频转码功能对 CPU 和内存资源要求较高,并且处理时间较长,可以将视频转码服务独立出来。这样可以为该服务分配专门的资源,如高性能的服务器集群,以满足其性能需求,同时也避免对其他微服务的性能产生影响。

服务注册与发现

  1. 服务注册中心:在微服务架构中,服务注册中心是一个关键组件,它负责记录各个微服务的地址和状态信息。常见的服务注册中心有 Eureka、Consul 和 Zookeeper 等。以 Eureka 为例,每个微服务在启动时会向 Eureka 注册中心注册自己的信息,包括服务名称、IP 地址、端口号等。Eureka 注册中心会维护一个服务列表,并定期与各个微服务进行心跳检测,以确保服务的可用性。当某个微服务下线时,Eureka 会及时从服务列表中移除该服务。
  2. 服务发现机制:其他微服务在调用某个服务时,通过查询服务注册中心来获取目标服务的地址信息。例如,订单服务需要调用商品服务获取商品详情,订单服务会向 Eureka 注册中心查询商品服务的地址,然后根据获取的地址发起 HTTP 请求调用商品服务的接口。这种服务发现机制使得微服务之间的调用不需要硬编码对方的地址,提高了系统的灵活性和可维护性。

配置管理

  1. 集中式配置:在微服务架构中,每个微服务都可能有大量的配置参数,如数据库连接字符串、日志级别、第三方服务的 API 密钥等。采用集中式配置管理可以方便地对这些配置进行统一管理和维护。常见的集中式配置工具如 Spring Cloud Config,它可以将各个微服务的配置文件集中存储在一个配置仓库(如 Git 仓库)中。当需要修改某个微服务的配置时,只需要在配置仓库中修改相应的配置文件,然后通知各个微服务重新加载配置即可。
  2. 配置的版本控制:对配置文件进行版本控制非常重要,就像对代码进行版本控制一样。通过版本控制可以记录配置的修改历史,方便回滚到之前的配置版本。例如,当某个微服务因为配置修改出现问题时,可以通过版本控制系统快速找到上一个稳定的配置版本并恢复。同时,版本控制也有助于团队成员之间协作管理配置,避免因为多人同时修改配置而产生冲突。

数据一致性处理

  1. 分布式事务:在微服务架构中,由于数据分布在多个独立的数据库中,实现数据一致性面临更大的挑战。分布式事务是一种常用的处理数据一致性的方式,如两阶段提交(2PC)和三阶段提交(3PC)协议。以电商系统中的下单操作为例,涉及到订单服务创建订单记录和库存服务扣减库存两个操作,为了保证这两个操作要么都成功,要么都失败,可以使用分布式事务。2PC 协议分为准备阶段和提交阶段,在准备阶段,各个参与事务的微服务(订单服务和库存服务)先执行本地事务,但不提交,然后向协调者(通常是一个独立的服务)汇报执行结果;在提交阶段,如果所有微服务的准备操作都成功,协调者通知各个微服务提交本地事务,否则通知回滚。然而,2PC 协议存在单点故障、性能瓶颈等问题,3PC 协议在一定程度上对 2PC 进行了改进,但仍然比较复杂。
  2. 最终一致性:考虑到分布式事务的复杂性和性能问题,很多微服务架构采用最终一致性的方式来处理数据一致性。最终一致性是指在一段时间内,数据可能存在不一致的情况,但最终会达到一致。例如,在电商系统中,当用户下单后,订单服务立即创建订单记录并返回成功给用户,同时将库存扣减的消息发送到消息队列。库存服务从消息队列中获取消息并进行库存扣减操作,由于消息处理可能存在一定的延迟,在这段时间内订单的状态和库存数量可能不一致,但最终库存服务会完成扣减操作,数据达到一致。为了保证最终一致性,可以采用补偿机制,即当某个操作失败时,通过执行相反的操作来恢复数据的一致性。比如,如果库存扣减操作失败,订单服务可以取消订单,并通知用户库存不足。

容器化与编排

  1. 容器化技术:Docker 是目前最流行的容器化技术,它可以将每个微服务及其依赖的运行环境打包成一个容器。容器具有轻量级、隔离性强的特点,使得微服务可以在不同的环境中快速、一致地部署。例如,一个使用 Python Flask 框架开发的微服务,可以通过 Dockerfile 将 Python 运行环境、Flask 框架以及微服务的代码打包成一个 Docker 镜像,然后在任何支持 Docker 的服务器上运行该镜像,就可以启动这个微服务,并且运行环境与开发环境完全一致。
  2. 容器编排:当微服务数量较多时,需要对容器进行有效的编排和管理。Kubernetes(简称 K8s)是一个开源的容器编排平台,它可以自动完成容器的部署、扩缩容、负载均衡等操作。例如,在电商促销活动期间,订单服务的流量会大幅增加,Kubernetes 可以根据预设的规则自动增加订单服务容器的数量,以应对高并发请求;当活动结束后,再自动减少容器数量,节省资源。Kubernetes 还提供了服务发现、健康检查等功能,确保微服务的高可用性和稳定性。

代码示例

以下以一个简单的 Spring Boot 微服务示例来展示微服务的开发过程,以及如何使用 Eureka 进行服务注册与发现。

  1. 创建 Spring Boot 微服务项目:使用 Spring Initializr(https://start.spring.io/)创建一个 Spring Boot 项目,选择 Web 依赖,这将引入 Spring Web 模块,用于开发 RESTful API。

  2. 编写微服务代码:创建一个简单的控制器类 UserController,用于提供用户相关的 API。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public String getUserById(@PathVariable String id) {
        return "User with id " + id;
    }
}
  1. 配置 Eureka 客户端:在 pom.xml 文件中添加 Eureka 客户端依赖。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml 文件中配置 Eureka 客户端相关信息。

server:
  port: 8081

spring:
  application:
    name: user-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  1. 启动 Eureka 服务器:创建一个 Eureka 服务器项目,同样使用 Spring Initializr,选择 Eureka Server 依赖。在主启动类上添加 @EnableEurekaServer 注解。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

application.yml 文件中配置 Eureka 服务器。

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
  1. 启动微服务并注册到 Eureka:分别启动 Eureka 服务器和用户微服务,用户微服务会自动向 Eureka 服务器注册自己的信息。可以通过访问 Eureka 服务器的 Web 界面(http://localhost:8761/)查看已注册的微服务列表。

  2. 服务调用:其他微服务可以通过 Eureka 服务发现机制获取用户微服务的地址,并发起 HTTP 请求调用其 API。例如,可以创建另一个微服务,在其代码中通过 RestTemplate 或 Feign 客户端来调用用户微服务的 getUserById 方法。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RibbonClient(name = "user-service")
public class AnotherServiceController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/other/users/{id}")
    public String getUserFromAnotherService(@PathVariable String id) {
        ResponseEntity<String> response = restTemplate.getForEntity("http://user-service/users/{id}", String.class, id);
        return response.getBody();
    }
}

在上述示例中,通过 Spring Boot 和 Eureka 实现了一个简单的微服务注册与发现过程,展示了微服务架构中服务之间的交互方式。

通过遵循微服务架构的核心原则,并在实践中合理运用各种技术和工具,可以构建出灵活、可扩展、高可用的后端应用系统,满足不断变化的业务需求。在实际项目中,还需要根据具体的业务场景和需求,不断优化和调整微服务架构,以达到最佳的性能和效果。