一、微服务里为啥需要Kafka

微服务架构的核心是把一个大系统拆成一堆独立的小服务,比如电商系统拆成用户服务、订单服务、库存服务、支付服务,每个服务自己管自己的事。但拆完之后就出问题了:各个服务之间怎么互相通信?比如用户下了订单,订单服务得通知库存服务扣库存,还得通知支付服务生成待支付订单。

要是用传统的HTTP接口调用,就会有一堆麻烦:比如订单服务调用库存服务的时候,库存服务刚好崩了,那订单服务就得等着或者报错;要是订单服务需要通知10个下游服务,就得挨个调10次,中间哪个环节出问题都得重发;还有就是订单服务得记着每个下游服务的地址、接口参数,改个下游接口就得改订单服务的代码,耦合性特别高。

Kafka就是专门解决这些问题的“消息中转站”。你可以把它想象成一个超级大的快递驿站,每个服务只需要把自己要发的“快递”(消息)放到驿站里,驿站会负责把快递送到对应的收件人手里。发快递的服务(生产者)不用管收件人(消费者)是谁、有没有在家,只要把快递放进去就行;收件人也不用管发件人是谁,只要去驿站取自己的快递就行。这样服务之间就完全解耦了,谁都不用管对方的状态。

二、Kafka在微服务里的具体应用场景

2.1 异步解耦的业务通信

最常见的场景就是业务流程里的异步通知,比如前面说的电商订单流程。订单服务生成订单后,只需要把订单信息发到Kafka的一个主题(可以理解为驿站里专门放订单快递的货架)里,然后订单服务就可以直接返回“订单创建成功”给用户,不用管后续的扣库存、生成支付单这些事。库存服务、支付服务自己去这个主题里取订单消息,处理自己的逻辑就行。

举个具体的例子,用Spring Boot作为统一技术栈(后面所有示例都用这个技术栈),订单服务作为生产者往Kafka发消息:

// 技术栈:Spring Boot 3.2 + Spring Kafka 3.1 + Kafka 3.5
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
public class OrderProducer {
    // 注入Kafka模板,用来发送消息
    private final KafkaTemplate<String, String> kafkaTemplate;
    // 定义订单主题的名称,统一配置避免硬编码
    private static final String ORDER_TOPIC = "order-topic";

    // 构造函数注入KafkaTemplate(Spring推荐的注入方式)
    public OrderProducer(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    /**
     * 发送订单创建的消息到Kafka
     * @param orderId 订单ID,作为消息的key(方便后续按订单ID查询消息)
     * @param orderContent 订单内容(JSON格式,包含用户ID、商品ID、金额等)
     */
    public void sendOrderMessage(String orderId, String orderContent) {
        // 第一个参数是主题名,第二个是消息key,第三个是消息内容
        kafkaTemplate.send(ORDER_TOPIC, orderId, orderContent);
        // 发送后不用等结果,直接返回,实现异步
        System.out.println("订单消息已发送到Kafka,订单ID:" + orderId);
    }
}

然后库存服务作为消费者,从Kafka取消息处理:

// 技术栈:Spring Boot 3.2 + Spring Kafka 3.1 + Kafka 3.5
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class InventoryConsumer {

    /**
     * 监听订单主题的消息,处理扣库存逻辑
     * @param orderId 消息的key,也就是订单ID
     * @param orderContent 消息内容,订单详情
     */
    @KafkaListener(topics = "order-topic", groupId = "inventory-group")
    public void consumeOrderMessage(String orderId, String orderContent) {
        System.out.println("库存服务收到订单消息,订单ID:" + orderId);
        // 这里写扣库存的业务逻辑,比如调用库存服务的扣库存接口
        // 比如:inventoryService.reduceStock(orderContent);
        // 处理成功后,Kafka会自动记录这个消息的消费位置,下次不会再重复处理
    }
}

这里的groupId是Kafka里的消费者组概念,同一个组里的消费者只会有一个收到同一条消息,比如库存服务部署了3个实例,那同一条订单消息只会被其中一个实例处理,避免重复扣库存。

2.2 削峰填谷的流量控制

还有一个很常见的场景是流量峰值的处理,比如电商大促的时候,一秒钟可能有几万条订单请求,要是直接让订单服务处理,可能会把服务压垮。这时候Kafka就可以作为“缓冲池”,把瞬间过来的大量消息先存到Kafka里,然后下游的处理服务(比如订单处理服务)慢慢从Kafka里取消息处理,不管上游来多少流量,下游都按自己能处理的速度慢慢消化,不会被压垮。

比如大促时,用户下单的请求先到网关,网关把订单请求转成消息发到Kafka,订单处理服务每秒处理100条消息,那不管上游一秒来1万条还是10万条,订单处理服务都按自己的节奏来,不会崩溃。

2.3 系统日志和监控数据的收集

微服务里每个服务都会产生大量的日志和监控数据,比如接口的响应时间、错误次数、CPU使用率这些。要是每个服务都把日志直接存到数据库或者监控系统,会给这些系统带来很大的压力,而且收集起来也很麻烦。

这时候可以用Kafka来收集日志,每个服务把自己的日志发到Kafka的日志主题里,然后专门的日志收集服务从Kafka取日志,批量处理后存到Elasticsearch或者日志系统里。这样既减轻了各个服务的压力,也方便统一管理日志。

比如用Filebeat(收集日志的工具)把服务的日志发到Kafka,Filebeat的配置大概是这样:

# Filebeat配置文件,用来收集服务日志并发送到Kafka
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/my-service/*.log # 服务的日志路径

output.kafka:
  enabled: true
  hosts: ["kafka-server-1:9092", "kafka-server-2:9092"] # Kafka服务器地址
  topic: "service-log-topic" # 日志发送的主题

三、Kafka和微服务的集成方案

3.1 基础集成步骤

集成Kafka到微服务里,主要分三步:部署Kafka集群、配置微服务的Kafka连接、编写生产者和消费者代码。

首先是部署Kafka集群,Kafka是依赖ZooKeeper的(新版本Kafka可以不用ZooKeeper,但目前大部分生产环境还是用ZooKeeper),所以部署的时候需要先部署ZooKeeper集群,再部署Kafka集群。比如用Docker Compose部署一个简单的Kafka集群,配置文件如下:

# Docker Compose配置,部署ZooKeeper和Kafka
version: '3'
services:
  zookeeper:
    image: wurstmeister/zookeeper:3.4.6
    ports:
      - "2181:2181"
  kafka:
    image: wurstmeister/kafka:2.12-2.5.0
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: localhost # 本地部署的话用localhost,生产环境要改成Kafka服务器的公网IP
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    depends_on:
      - zookeeper

然后是配置微服务的Kafka连接,在Spring Boot的application.yml里配置:

# Spring Boot项目的Kafka配置
spring:
  kafka:
    bootstrap-servers: localhost:9092 # Kafka服务器地址,集群的话用逗号分隔多个地址
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息key的序列化方式,转成字符串
      value-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息内容的序列化方式
    consumer:
      group-id: default-group # 默认的消费者组ID
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 消息key的反序列化方式
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 消息内容的反序列化方式
      auto-offset-reset: earliest # 没有消费位置的时候,从最早的消息开始消费

最后就是编写生产者和消费者的代码,和前面的示例一样,这里就不重复了。

3.2 高可用集成方案

生产环境里,Kafka集群需要保证高可用,也就是一台Kafka服务器挂了,整个集群还能正常工作。所以Kafka集群一般会部署至少3台服务器,主题也会设置多个副本,比如一个主题有3个副本,分布在3台服务器上,就算其中一台服务器挂了,还有另外两个副本可以提供服务。

比如创建一个高可用的订单主题,命令如下:

# 创建Kafka主题,3个分区,3个副本
kafka-topics.sh --create --topic order-topic --partitions 3 --replication-factor 3 --bootstrap-server localhost:9092

这里的分区是Kafka里用来提高消息吞吐量的概念,一个主题可以分成多个分区,每个分区可以单独处理消息,比如3个分区的话,Kafka的处理能力就是1个分区的3倍。副本是用来保证数据不丢失的,每个分区的副本分布在不同的服务器上。

另外,微服务里的消费者也需要做高可用,比如把消费者部署多个实例,同一个消费者组里的多个实例会自动分配不同的分区来消费,比如3个分区的订单主题,部署3个库存服务实例,每个实例会分配一个分区来消费,这样就算一个实例挂了,其他实例会自动重新分配分区,继续消费消息。

3.3 消息序列化的优化

前面的示例里,消息都是用字符串序列化的,也就是把对象转成JSON字符串再发送,这样虽然简单,但效率不高,而且容易出错。生产环境里一般会用更高效的序列化方式,比如Protobuf或者Avro。

比如用Protobuf序列化订单对象,先定义Protobuf的消息格式:

// Protobuf的订单消息定义
syntax = "proto3";
package com.example;

option java_package = "com.example.proto";
option java_outer_classname = "OrderProto";

message Order {
  string order_id = 1;
  string user_id = 2;
  string product_id = 3;
  double amount = 4;
}

然后把这个Protobuf文件编译成Java类,再在Spring Boot里配置Protobuf的序列化和反序列化:

# Spring Boot的Kafka配置,使用Protobuf序列化
spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer # Protobuf序列化
    consumer:
      group-id: inventory-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer # Protobuf反序列化
      properties:
        specific.protobuf.value.type: com.example.proto.OrderProto$Order # 指定反序列化的Java类

这样发送和接收消息的时候,就可以直接用Order对象,不用再转成JSON字符串,效率更高,也不容易出错。

四、Kafka在微服务里的优缺点和注意事项

4.1 优点

首先是解耦性好,服务之间完全不用知道对方的存在,只要约定好主题和消息格式就行,改一个服务不会影响其他服务。然后是性能高,Kafka的吞吐量特别大,每秒可以处理几十万条消息,适合高并发的场景。还有可靠性高,消息会持久化到磁盘,就算服务器挂了,消息也不会丢,而且副本机制保证了数据的安全。另外扩展性好,Kafka集群可以很容易地增加服务器来提高性能,主题的分区也可以调整(虽然调整分区比较麻烦,但还是可以做到)。

4.2 缺点

第一个缺点是学习成本高,Kafka的概念特别多,比如主题、分区、副本、消费者组、偏移量这些,新手很难搞懂。然后是运维复杂,Kafka集群需要专门的运维人员来管理,比如监控集群的状态、调整配置、备份数据这些,出了问题也不好排查。还有就是消息顺序的问题,Kafka只能保证同一个分区里的消息是有序的,不同分区的消息是无序的,要是业务需要全局有序的消息,Kafka就不太适合。另外,Kafka的消息是会被消费一次的(默认情况下),要是消费者处理消息的时候失败了,消息不会自动重发,需要自己处理重试逻辑。

4.3 注意事项

首先是消息格式的约定,服务之间的消息格式一定要提前约定好,比如用JSON还是Protobuf,字段的含义是什么,版本怎么兼容,不然发的消息对方看不懂,会出问题。然后是消息的大小,Kafka的消息不能太大,一般不要超过1MB,太大的消息会影响Kafka的性能,要是有大文件需要传输,应该把文件存到OSS,然后把OSS的地址发到Kafka里。还有就是消费者的重试逻辑,要是消费者处理消息的时候失败了,比如扣库存的时候网络断了,应该怎么处理?是重试几次,还是把消息放到死信队列(专门存处理失败的消息的主题)里?这个要根据业务来定。另外,Kafka的版本兼容性,生产者、消费者和Kafka集群的版本要兼容,不然会出问题,比如用Kafka 3.0的客户端连接Kafka 2.0的集群,可能会有不兼容的地方。还有就是监控和告警,生产环境里一定要监控Kafka集群的状态,比如消息的堆积量、集群的CPU、内存、磁盘使用率,要是消息堆积太多,说明下游处理不过来,要及时告警。

五、总结

Kafka是微服务架构里非常重要的组件,它解决了服务之间解耦、流量削峰、数据收集等很多问题,特别适合高并发、分布式的场景。但Kafka也不是万能的,它有自己的优缺点和适用场景,比如要是业务需要强一致的消息,或者消息量特别小的场景,用Kafka可能有点大材小用,甚至会带来不必要的复杂度。

在使用Kafka的时候,一定要根据自己的业务场景来选择合适的集成方案,比如小项目可以用简单的集成方式,生产环境一定要用高可用的集群,优化消息的序列化方式,处理好消费者的重试逻辑,做好监控和告警。另外,要注意Kafka的概念和原理,这样才能更好地使用它,排查问题。

总的来说,Kafka是一个非常强大的消息中间件,只要用对了场景和方案,就能大大提高微服务架构的性能和稳定性。