一、微服务里为啥需要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是一个非常强大的消息中间件,只要用对了场景和方案,就能大大提高微服务架构的性能和稳定性。
评论
围绕“Kafka在微服务架构中的应用场景与集成方案”参与讨论