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

构建微服务架构的关键步骤

2021-01-143.4k 阅读

1. 微服务架构概述

在深入探讨构建微服务架构的关键步骤之前,让我们先简要了解一下微服务架构的基本概念。微服务架构是一种将应用程序构建为一组小型、独立且可部署服务的架构风格。每个服务都围绕特定的业务能力进行设计,通过轻量级的通信机制(如 RESTful API)进行交互。这种架构风格与传统的单体架构形成鲜明对比,单体架构将整个应用程序作为一个单一的可执行单元进行开发和部署。

微服务架构的优势众多。首先,它提高了系统的可扩展性。由于每个服务都是独立的,可以根据其特定的负载需求进行独立的扩展。例如,某个处理用户登录的服务可能在业务高峰期需要更多的资源,而其他服务的负载可能较为稳定,此时只需对登录服务进行扩展,而不会影响其他服务。其次,微服务架构增强了开发的敏捷性。不同的团队可以独立负责不同的服务,并行开发、测试和部署,大大加快了软件开发的速度。此外,它还提升了系统的容错性,一个服务的故障不会导致整个系统的瘫痪,因为其他服务仍然可以正常运行。

2. 关键步骤之一:服务拆分

2.1 基于业务能力拆分

服务拆分是构建微服务架构的首要任务,也是最为关键的一步。一个合理的拆分原则是基于业务能力进行划分。业务能力是指系统能够为用户提供的特定功能或价值。例如,在一个电商系统中,商品管理、订单处理、用户管理等都是不同的业务能力。我们可以将每个业务能力独立成一个微服务。

以商品管理为例,它包括商品的添加、查询、修改和删除等操作。这些操作紧密围绕商品这一业务实体,因此将商品管理作为一个独立的微服务是合理的。这样的拆分方式使得每个微服务都具有明确的职责边界,易于理解和维护。

2.2 避免过度拆分与拆分不足

在进行服务拆分时,需要避免两个极端:过度拆分和拆分不足。过度拆分可能导致服务数量过多,管理成本急剧上升。例如,将一个简单的用户信息展示功能拆分成多个过于细化的服务,如用户名展示服务、用户地址展示服务等,这会增加服务间的通信开销和管理复杂度。

另一方面,拆分不足则无法充分发挥微服务架构的优势。如果仍然保留一些大而全的服务,那么系统的可扩展性和敏捷性将受到限制。比如,将订单处理和库存管理放在一个服务中,当订单处理的负载增加时,无法单独对订单处理部分进行扩展,因为库存管理的负载可能并不高。

2.3 拆分方法与工具

常见的拆分方法有领域驱动设计(DDD)。DDD 强调将业务领域划分为不同的子领域,每个子领域对应一个或多个微服务。通过对业务领域的深入分析,识别出聚合根、实体和值对象等概念,以此来确定微服务的边界。

此外,一些工具也可以辅助服务拆分。例如,通过分析代码的依赖关系,使用工具可以识别出哪些模块之间的耦合度较高,哪些模块相对独立,从而为服务拆分提供参考。一些架构分析工具如 ArchUnit 可以帮助我们在代码层面验证拆分后的服务是否符合设计原则。

以下是一个简单的示例,假设我们有一个单体的电商应用,使用领域驱动设计来拆分:

// 商品领域模型
public class Product {
    private Long id;
    private String name;
    private double price;
    // 省略 getter 和 setter 方法
}

// 订单领域模型
public class Order {
    private Long id;
    private List<Product> products;
    private User user;
    // 省略 getter 和 setter 方法
}

// 在单体应用中,可能存在处理商品和订单的混合业务逻辑
public class EcommerceService {
    public void addProduct(Product product) {
        // 商品添加逻辑
    }

    public void placeOrder(Order order) {
        // 订单处理逻辑,可能会涉及商品库存检查等
    }
}

// 使用 DDD 拆分后,商品服务
public class ProductService {
    public void addProduct(Product product) {
        // 商品添加逻辑
    }
}

// 订单服务
public class OrderService {
    public void placeOrder(Order order) {
        // 订单处理逻辑,通过调用商品服务检查库存等
    }
}

在上述示例中,原本在单体应用中的混合业务逻辑被拆分成了独立的商品服务和订单服务,每个服务专注于自己的业务领域。

3. 关键步骤之二:通信机制选择

3.1 RESTful API

RESTful API 是微服务架构中最常用的通信机制之一。它基于 HTTP 协议,具有简单、通用、易于理解和实现的特点。RESTful API 使用标准的 HTTP 方法(如 GET、POST、PUT、DELETE)来对资源进行操作。

例如,在商品微服务中,获取所有商品列表可以使用 GET 请求:

GET /products

添加一个新商品可以使用 POST 请求:

POST /products
Content-Type: application/json

{
    "name": "New Product",
    "price": 100.0
}

RESTful API 的优点在于它的无状态性,每个请求都包含了足够的信息来处理,不需要服务器端维护额外的会话状态。这使得微服务之间的通信更加简单和可靠,也便于进行缓存和负载均衡。

3.2 消息队列

消息队列也是微服务通信中常用的方式,特别是在需要异步处理和削峰填谷的场景下。例如,在一个电商系统中,当用户下单后,订单处理服务可以将订单消息发送到消息队列,库存服务和物流服务从消息队列中获取订单消息并进行相应的处理。

常见的消息队列有 RabbitMQ、Kafka 等。以 RabbitMQ 为例,使用 Java 客户端发送消息的代码如下:

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class Sender {
    private final static String QUEUE_NAME = "order_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "New order placed";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

接收消息的代码如下:

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;

import java.io.IOException;

public class Receiver {
    private final static String QUEUE_NAME = "order_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        boolean autoAck = true; // 自动确认消息
        channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTag",
                (consumerTag, delivery) -> {
                    String message = new String(delivery.getBody(), "UTF-8");
                    System.out.println(" [x] Received '" + message + "'");
                },
                consumerTag -> {
                    System.out.println("Consumer cancelled");
                });
    }
}

消息队列的优势在于它可以解耦微服务之间的直接依赖关系,提高系统的稳定性和可扩展性。同时,通过消息队列的异步处理能力,可以有效地应对高并发场景。

3.3 gRPC

gRPC 是由 Google 开发的高性能、开源的 RPC 框架。它基于 HTTP/2 协议,使用 Protocol Buffers 作为序列化格式,具有高效、轻量级的特点。gRPC 特别适合在性能要求较高、服务间通信频繁的场景下使用。

定义一个简单的 gRPC 服务示例,首先定义.proto 文件:

syntax = "proto3";

package product;

service ProductService {
    rpc GetProduct(ProductRequest) returns (ProductResponse);
}

message ProductRequest {
    string productId = 1;
}

message Product {
    string id = 1;
    string name = 2;
    double price = 3;
}

message ProductResponse {
    Product product = 1;
}

然后使用工具生成相应的 Java 代码。服务端实现如下:

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import product.ProductRequest;
import product.ProductResponse;
import product.ProductServiceGrpc;

import java.io.IOException;

public class ProductServiceImpl extends ProductServiceGrpc.ProductServiceImplBase {

    @Override
    public void getProduct(ProductRequest request, StreamObserver<ProductResponse> responseObserver) {
        // 根据 productId 获取商品信息
        Product product = new Product();
        product.setId(request.getProductId());
        product.setName("Sample Product");
        product.setPrice(100.0);

        ProductResponse response = ProductResponse.newBuilder()
               .setProduct(product)
               .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(50051)
               .addService(new ProductServiceImpl())
               .build();

        server.start();
        System.out.println("Server started, listening on 50051");

        server.awaitTermination();
    }
}

客户端调用代码如下:

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import product.ProductRequest;
import product.ProductResponse;
import product.ProductServiceGrpc;

public class ProductClient {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
               .usePlaintext()
               .build();

        ProductServiceGrpc.ProductServiceBlockingStub stub = ProductServiceGrpc.newBlockingStub(channel);

        ProductRequest request = ProductRequest.newBuilder()
               .setProductId("1")
               .build();

        ProductResponse response = stub.getProduct(request);
        System.out.println("Product Name: " + response.getProduct().getName());

        channel.shutdown();
    }
}

gRPC 的优点在于其高效的序列化和反序列化机制,以及基于 HTTP/2 的多路复用、流量控制等特性,使得它在性能上表现出色。

4. 关键步骤之三:服务治理

4.1 服务注册与发现

服务注册与发现是服务治理的基础。在微服务架构中,随着服务数量的增加,如何让服务之间相互找到对方变得至关重要。常见的服务注册与发现组件有 Eureka、Consul 和 ZooKeeper 等。

以 Eureka 为例,首先在服务端添加依赖(假设使用 Spring Boot):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

然后配置 Eureka 服务端:

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false

启动 Eureka 服务端后,各个微服务可以注册到 Eureka 上。在微服务中添加 Eureka 客户端依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

配置微服务注册到 Eureka:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}

这样,当微服务启动时,它会自动注册到 Eureka 服务端,其他微服务可以通过 Eureka 发现并调用它。

4.2 负载均衡

负载均衡是确保微服务高可用性和性能的重要手段。当有多个相同的微服务实例运行时,负载均衡器可以将请求均匀地分配到这些实例上,避免单个实例过载。

常见的负载均衡器有 Ribbon 和 Nginx 等。Ribbon 是一个客户端负载均衡器,它集成在微服务客户端中。例如,在 Spring Cloud 项目中,使用 Ribbon 非常简单。假设我们有一个商品微服务,在调用商品微服务的客户端配置文件中添加如下配置:

product-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

上述配置表示使用随机策略将请求发送到商品微服务的不同实例上。

Nginx 是一个常用的反向代理和负载均衡器,它在服务端进行负载均衡。可以通过配置 Nginx 的 upstream 模块来实现对微服务的负载均衡:

upstream product_service {
    server 192.168.1.100:8080;
    server 192.168.1.101:8080;
}

server {
    listen 80;
    server_name product.example.com;

    location / {
        proxy_pass http://product_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

4.3 熔断器

熔断器是为了防止微服务之间的故障蔓延而引入的机制。当一个微服务调用另一个微服务出现故障的次数达到一定阈值时,熔断器会跳闸,暂时切断对该微服务的调用,避免大量无效请求堆积,从而保证系统的整体稳定性。

在 Spring Cloud 中,可以使用 Hystrix 实现熔断器。首先添加 Hystrix 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

然后在需要使用熔断器的服务方法上添加注解:

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @HystrixCommand(fallbackMethod = "getProductFallback")
    public String getProduct(String productId) {
        // 实际调用商品微服务获取商品信息
        // 如果调用失败,会触发 fallbackMethod
    }

    public String getProductFallback(String productId) {
        // 熔断后的备用逻辑,返回默认数据等
        return "Default product information";
    }
}

getProduct 方法调用失败次数达到 Hystrix 配置的阈值时,熔断器跳闸,后续请求将直接调用 getProductFallback 方法,避免大量请求阻塞在故障的服务调用上。

5. 关键步骤之四:数据管理

5.1 数据隔离

在微服务架构中,每个微服务应该有自己独立的数据存储,这就是数据隔离原则。这样可以避免不同微服务之间的数据耦合,提高系统的可维护性和可扩展性。

例如,商品微服务可以使用关系型数据库(如 MySQL)来存储商品信息,订单微服务可以使用另一个数据库来存储订单数据。以商品微服务为例,使用 Spring Data JPA 连接 MySQL 数据库的配置如下:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/product_db
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQL57Dialect

数据隔离还可以通过不同的存储类型来实现。比如,对于一些需要高读写性能的微服务,可以使用 NoSQL 数据库(如 Redis)。例如,用户登录微服务可以使用 Redis 来缓存用户登录信息,提高登录验证的速度。

5.2 数据一致性

虽然每个微服务有自己独立的数据存储,但在一些业务场景下,仍然需要保证数据的一致性。例如,在电商系统中,当用户下单后,订单微服务需要更新订单状态,同时库存微服务需要减少相应商品的库存。这就涉及到跨微服务的数据一致性问题。

实现数据一致性的常用方法有分布式事务和最终一致性。分布式事务可以使用两阶段提交(2PC)、三阶段提交(3PC)等协议,但这些协议实现复杂,性能开销较大。

最终一致性是一种更常用的方式,它通过消息队列等机制来异步处理数据更新。例如,订单微服务在下单成功后,发送一条消息到消息队列,库存微服务从消息队列中获取消息并更新库存。虽然在短时间内订单状态和库存可能不一致,但经过一定时间后,数据最终会达到一致。

以下是一个使用消息队列实现最终一致性的简单示例。假设使用 RabbitMQ,订单微服务发送消息:

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class OrderService {
    private final static String QUEUE_NAME = "stock_update_queue";

    public void placeOrder(Order order) {
        // 订单处理逻辑
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            try (Connection connection = factory.newConnection();
                 Channel channel = connection.createChannel()) {
                channel.queueDeclare(QUEUE_NAME, false, false, false, null);
                String message = order.getProductId() + ":" + order.getQuantity();
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

库存微服务接收消息并更新库存:

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;

import java.io.IOException;

public class StockService {
    private final static String QUEUE_NAME = "stock_update_queue";

    public StockService() throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        boolean autoAck = true;
        channel.basicConsume(QUEUE_NAME, autoAck, "stockConsumerTag",
                (consumerTag, delivery) -> {
                    String message = new String(delivery.getBody(), "UTF-8");
                    String[] parts = message.split(":");
                    String productId = parts[0];
                    int quantity = Integer.parseInt(parts[1]);
                    // 更新库存逻辑
                },
                consumerTag -> {
                    System.out.println("Consumer cancelled");
                });
    }
}

5.3 数据迁移与版本控制

随着业务的发展,微服务的数据结构可能需要进行更新,这就涉及到数据迁移。在进行数据迁移时,需要确保数据的完整性和一致性。

一种常见的方法是使用数据库迁移工具,如 Flyway。Flyway 可以管理数据库的版本,记录数据库的变更历史。在 Spring Boot 项目中使用 Flyway,首先添加依赖:

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

然后在 resources/db/migration 目录下创建 SQL 脚本,例如 V1__create_product_table.sql

CREATE TABLE product (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10, 2)
);

当应用启动时,Flyway 会自动执行这些脚本,确保数据库结构与代码中的定义一致。如果需要对数据库结构进行更新,只需要创建新的脚本,如 V2__add_product_description.sql

ALTER TABLE product ADD COLUMN description VARCHAR(255);

这样可以有效地管理微服务的数据结构变更,保证数据的一致性和可维护性。

6. 关键步骤之五:监控与日志

6.1 监控指标

在微服务架构中,监控是保障系统健康运行的重要手段。常见的监控指标包括 CPU 使用率、内存使用率、网络带宽、请求响应时间、吞吐量等。

以 Spring Boot 微服务为例,可以使用 Spring Boot Actuator 来暴露一些基本的监控指标。首先添加 Actuator 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后在配置文件中启用需要的端点:

management:
  endpoints:
    web:
      exposure:
        include: health, metrics

通过访问 /actuator/metrics 端点,可以获取各种指标信息,如 jvm.memory.used 表示 JVM 已使用的内存,http.server.requests 表示 HTTP 请求的相关指标。

6.2 分布式追踪

随着微服务数量的增加,一个请求可能会经过多个微服务,分布式追踪可以帮助我们了解请求在各个微服务之间的流转情况,便于定位问题。常见的分布式追踪系统有 Zipkin、Jaeger 等。

以 Zipkin 为例,首先在各个微服务中添加 Zipkin 依赖(假设使用 Spring Boot):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

然后配置 Zipkin 服务器地址:

spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      probability: 1.0

当请求在微服务之间传递时,Zipkin 会记录每个微服务的处理时间和调用关系,通过 Zipkin 的 UI 界面可以直观地查看请求的整个链路,快速定位性能瓶颈或故障点。

6.3 日志管理

日志是排查问题的重要依据。在微服务架构中,由于服务数量多,日志管理变得更加复杂。需要集中管理各个微服务的日志,以便于搜索和分析。

常见的日志管理工具如 ELK(Elasticsearch、Logstash、Kibana)和 EFK(Elasticsearch、Fluentd、Kibana)。以 ELK 为例,Logstash 可以收集各个微服务的日志,进行过滤和格式化后发送到 Elasticsearch 进行存储,Kibana 则用于可视化展示和搜索日志。

在微服务中配置将日志发送到 Logstash,例如在 Spring Boot 项目中,添加 Logstash 依赖:

<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>6.6.0</version>
</dependency>

然后配置 logback-spring.xml 文件:

<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpAppender">
    <destination>localhost:5000</destination>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <customFields>{"service_name": "product-service"}</customFields>
    </encoder>
</appender>

<root level="info">
    <appender-ref ref="LOGSTASH" />
</root>

这样,微服务的日志会被发送到 Logstash,经过处理后存储在 Elasticsearch 中,通过 Kibana 可以方便地进行查询和分析。

7. 关键步骤之六:部署与持续集成/持续交付

7.1 容器化

容器化是微服务部署的重要方式,它可以将微服务及其依赖打包成一个独立的容器,实现环境的一致性和可移植性。常见的容器技术有 Docker。

以一个简单的 Spring Boot 微服务为例,首先在项目根目录下创建 Dockerfile

FROM openjdk:11
COPY target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

然后使用以下命令构建 Docker 镜像:

docker build -t product-service:1.0.0.

上述命令将构建一个名为 product-service:1.0.0 的 Docker 镜像。可以使用以下命令运行容器:

docker run -d -p 8080:8080 product-service:1.0.0

这样,微服务就以容器的形式运行起来,并且可以方便地在不同环境中部署。

7.2 容器编排

当有多个微服务容器时,需要进行容器编排来管理它们的生命周期、资源分配和服务发现等。常用的容器编排工具是 Kubernetes。

首先创建一个 Kubernetes 的部署文件 product-service-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: product-service:1.0.0
        ports:
        - containerPort: 8080

上述配置表示创建 3 个 product-service 的副本。可以使用以下命令部署到 Kubernetes 集群:

kubectl apply -f product-service-deployment.yaml

Kubernetes 还可以实现自动扩缩容、服务发现等功能,通过配置 HorizontalPodAutoscaler 可以根据 CPU 使用率等指标自动调整微服务的副本数量。

7.3 持续集成与持续交付

持续集成(CI)和持续交付(CD)是保证微服务快速迭代和高质量发布的关键流程。在 CI 流程中,开发人员将代码提交到版本控制系统后,自动触发构建和测试过程,确保代码的质量。

常见的 CI/CD 工具如 Jenkins、GitLab CI/CD 等。以 GitLab CI/CD 为例,在项目根目录下创建 .gitlab-ci.yml 文件:

image: maven:3.6.3-jdk-11

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - mvn clean package

test:
  stage: test
  script:
    - mvn test

deploy:
  stage: deploy
  script:
    - docker build -t product-service:${CI_COMMIT_SHORT_SHA}.
    - docker push registry.example.com/product-service:${CI_COMMIT_SHORT_SHA}
    - ssh user@k8s-master "kubectl set image deployment/product-service product-service=registry.example.com/product-service:${CI_COMMIT_SHORT_SHA}"

上述配置表示在代码提交后,首先进行构建和测试,然后构建 Docker 镜像并推送到镜像仓库,最后更新 Kubernetes 集群中的微服务镜像。通过这样的 CI/CD 流程,可以实现微服务的快速、可靠的发布。