Reactive Spring實戰 -- 響應式Kafka互動

binecy發表於2021-06-22

本文分享如何使用KRaft部署Kafka叢集,以及Spring中如何實現Kafka響應式互動。

KRaft

我們知道,Kafka使用Zookeeper負責為kafka儲存broker,Consumer Group等後設資料,並使用Zookeeper完成broker選主等操作。
雖然使用Zookeeper簡化了Kafka的工作,但這也使Kafka的部署和運維更復雜。

Kafka 2.8.0開始移除了Zookeeper,並使用Kafka內部的仲裁(Quorum)控制器來取代ZooKeeper,官方稱這個控制器為 "Kafka Raft metadata mode",即KRaft mode。從此使用者可以在不需要Zookeeper的情況下部署Kafka叢集,這使Fafka更加簡單,輕量級。
使用KRaft模式後,使用者只需要專注於維護Kafka叢集即可。

注意:由於該功能改動較大,目前Kafka2.8版本提供的KRaft模式是一個測試版本,不推薦在生產環境使用。相信Kafka後續版本很快會提供生產可用的kraft版本。

下面介紹一下如果使用Kafka部署kafka叢集。
這裡使用3臺機器部署3個Kafka節點,使用的Kafka版本為2.8.0。

  1. 生成ClusterId以及配置檔案。
    (1)使用kafka-storage.sh生成ClusterId。
$ ./bin/kafka-storage.sh random-uuid
dPqzXBF9R62RFACGSg5c-Q

(2)使用ClusterId生成配置檔案

$ ./bin/kafka-storage.sh format -t <uuid> -c ./config/kraft/server.properties
Formatting /tmp/kraft-combined-logs

注意:只需要在生成一個ClusterId,並使用該ClusterId在所有機器上生成配置檔案,即叢集中所有節點使用的ClusterId需相同。

  1. 修改配置檔案
    指令碼生成的配置檔案只能用於單個Kafka節點,如果在部署Kafka叢集,需要對配置檔案進行一下修改。

(1)修改config/kraft/server.properties(稍後使用該配置啟動kafka)

process.roles=broker,controller 
node.id=1
listeners=PLAINTEXT://172.17.0.2:9092,CONTROLLER://172.17.0.2:9093
advertised.listeners=PLAINTEXT://172.17.0.2:9092
controller.quorum.voters=1@172.17.0.2:9093,2@172.17.0.3:9093,3@172.17.0.4:9093

process.roles指定了該節點角色,有以下取值

  • broker: 這臺機器將僅僅當作一個broker
  • controller: 作為Raft quorum的控制器節點
  • broker,controller: 包含以上兩者的功能

一個叢集中不同節點的node.id需要不同。
controller.quorum.voters需要配置叢集中所有的controller節點,配置格式為@:

(2)
kafka-storage.sh指令碼生成的配置,預設將kafka資料存放在/tmp/kraft-combined-logs/,
我們還需要/tmp/kraft-combined-logs/meta.properties配置中的node.id,使其與server.properties配置中保持一起。

node.id=1
  1. 啟動kafka
    使用kafka-server-start.sh指令碼啟動Kafka節點
$ ./bin/kafka-server-start.sh ./config/kraft/server.properties

下面測試一下該kafka叢集

  1. 建立主題
$ ./bin/kafka-topics.sh --create --partitions 3 --replication-factor 3 --bootstrap-server 172.17.0.2:9092,172.17.0.3:9092,172.17.0.4:9092 --topic topic1 
  1. 生產訊息
$ ./bin/kafka-console-producer.sh --broker-list 172.17.0.2:9092,172.17.0.3:9092,172.17.0.4:9092 --topic topic1
  1. 消費訊息
$ ./bin/kafka-console-consumer.sh --bootstrap-server 172.17.0.2:9092,172.17.0.3:9092,172.17.0.4:9092 --topic topic1 --from-beginning

這部分命令的使用與低版本的Kafka保持一致。

Kafka的功能暫時還不完善,這是展示一個簡單的部署示例。
Kafka文件:https://github.com/apache/kafka/blob/trunk/config/kraft/README.md

Spring中可以使用Spring-Kafka、Spring-Cloud-Stream兩個框架實現kafka響應式互動。
下面分別看一下這兩個框架的使用。

Spring-Kafka

  1. 新增引用
    新增spring-kafka引用
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>2.5.8.RELEASE</version>
</dependency>
  1. 準備配置檔案,內容如下
spring.kafka.producer.bootstrap-servers=172.17.0.2:9092,172.17.0.3:9092,172.17.0.4:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.LongSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer

spring.kafka.consumer.bootstrap-servers=172.17.0.2:9092,172.17.0.3:9092,172.17.0.4:9092
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.LongDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.group-id=warehouse-consumers
spring.kafka.consumer.properties.spring.json.trusted.packages=*

分別是生產者和消費者對應的配置,很簡單。

  1. 傳送訊息
    Spring-Kakfa中可以使用ReactiveKafkaProducerTemplate傳送訊息。
    首先,我們需要建立一個ReactiveKafkaProducerTemplate例項。(目前SpringBoot會自動建立KafkaTemplate例項,但不會建立ReactiveKafkaProducerTemplate例項)。
@Configuration
public class KafkaConfig {
    @Autowired
    private KafkaProperties properties;

    @Bean
    public ReactiveKafkaProducerTemplate reactiveKafkaProducerTemplate() {
        SenderOptions options = SenderOptions.create(properties.getProducer().buildProperties());
        ReactiveKafkaProducerTemplate template = new ReactiveKafkaProducerTemplate(options);
        return template;
    }
}

KafkaProperties例項由SpringBoot自動建立,讀取上面配置檔案中對應的配置。

接下來,就可以使用ReactiveKafkaProducerTemplate傳送訊息了

    @Autowired
    private ReactiveKafkaProducerTemplate template;

    public static final String WAREHOUSE_TOPIC = "warehouse";
    public Mono<Boolean> add(Warehouse warehouse) {
        Mono<SenderResult<Void>> resultMono = template.send(WAREHOUSE_TOPIC, warehouse.getId(), warehouse);
        return resultMono.flatMap(rs -> {
            if(rs.exception() != null) {
                logger.error("send kafka error", rs.exception());
                return Mono.just(false);
            }
            return Mono.just(true);
        });
    }

ReactiveKafkaProducerTemplate#send方法返回一個Mono(這是Spring Reactor中的核心物件),Mono中攜帶了SenderResult,SenderResult中的RecordMetadata、exception儲存該記錄的後設資料(包括offset、timestamp等資訊)以及傳送操作的異常。

  1. 消費訊息
    Spring-Kafka使用ReactiveKafkaConsumerTemplate消費訊息。
@Service
public class WarehouseConsumer {
    @Autowired
    private KafkaProperties properties;

    @PostConstruct
    public void consumer() {
        ReceiverOptions<Long, Warehouse> options = ReceiverOptions.create(properties.getConsumer().buildProperties());
        options = options.subscription(Collections.singleton(WarehouseService.WAREHOUSE_TOPIC));
        new ReactiveKafkaConsumerTemplate(options)
                .receiveAutoAck()
                .subscribe(record -> {
                    logger.info("Warehouse Record:" + record);
                });
    }
}

這裡與之前使用@KafkaListener註解實現的訊息監聽者不同,不過也非常簡單,分為兩個步驟:
(1)ReceiverOptions#subscription方法將ReceiverOptions關聯到kafka主題
(2)建立ReactiveKafkaConsumerTemplate,並註冊subscribe的回撥函式消費訊息。
提示:receiveAutoAck方法會自動提交消費組offset。

Spring-Cloud-Stream

Spring-Cloud-Stream是Spring提供的用於構建訊息驅動微服務的框架。
它為不同的訊息中介軟體產品提供一種靈活的,統一的程式設計模型,可以遮蔽底層不同訊息元件的差異,目前支援RabbitMQ、Kafka、RocketMQ等訊息元件。

這裡簡單展示Spring-Cloud-Stream中實現Kafka響應式互動的示例,不深入介紹Spring-Cloud-Stream的應用。

  1. 引入spring-cloud-starter-stream-kafka的引用
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-stream-kafka</artifactId>
    </dependency>
  1. 新增配置
spring.cloud.stream.kafka.binder.brokers=172.17.0.2:9092,172.17.0.3:9092,172.17.0.4:9092
spring.cloud.stream.bindings.warehouse2-out-0.contentType=application/json
spring.cloud.stream.bindings.warehouse2-out-0.destination=warehouse2
# 訊息格式
spring.cloud.stream.bindings.warehouse3-in-0.contentType=application/json
# 訊息目的地,可以理解為Kafka主題
spring.cloud.stream.bindings.warehouse3-in-0.destination=warehouse2
# 定義消費者消費組,可以理解為Kafka消費組
spring.cloud.stream.bindings.warehouse3-in-0.group=warehouse2-consumers
# 對映方法名
spring.cloud.function.definition=warehouse2;warehouse3

Spring-Cloud-Stream 3.1版本之後,@EnableBinding、@Output等StreamApi註解都標記為廢棄,並提供了一種更簡潔的函數語言程式設計模型。
該版本後,使用者不需要使用註解,只要在配置檔案中指定需要繫結的方法,Spring-Cloud-Stream會為使用者將這些方法與底層訊息元件繫結,使用者可以直接呼叫這些方法傳送訊息,或者接收到訊息時Spring-Cloud-Stream會呼叫這些方法消費訊息。

通過以下格式定義輸入、輸出函式的相關屬性:
輸出(傳送訊息):<functionName> + -out- + <index>
輸入(消費訊息):<functionName> + -in- + <index>
對於典型的單個輸入/輸出函式,index始終為0,因此它僅與具有多個輸入和輸出引數的函式相關。
Spring-Cloud-Stream支援具有多個輸入(函式引數)/輸出(函式返回值)的函式。

spring.cloud.function.definition配置指定需要繫結的方法名,不新增該配置,Spring-Cloud-Stream會自動嘗試繫結返回型別為Supplier/Function/Consumer的方法,但是使用該配置可以避免Spring-Cloud-Stream繫結混淆。

  1. 傳送訊息
    使用者可以編寫一個返回型別為Supplier的方法,並定時傳送訊息
    @PollableBean
    public Supplier<Flux<Warehouse>> warehouse2() {
        Warehouse warehouse = new Warehouse();
        warehouse.setId(333L);
        warehouse.setName("天下第一倉");
        warehouse.setLabel("一級倉");

        logger.info("Supplier Add : {}", warehouse);
        return () -> Flux.just(warehouse);
    }

定義該方法後,Spring-Cloud-Stream每秒呼叫一次該方法,生成Warehouse例項,併傳送到Kafka。
(這裡方法名warehouse3已經配置在spring.cloud.function.definition中。)

通常場景下,應用並不需要定時傳送訊息,而是由業務場景觸發傳送訊息操作, 如Rest介面,
這時可以使用StreamBridge介面

    @Autowired
    private StreamBridge streamBridge;

    public boolean add2(Warehouse warehouse) {
        return streamBridge.send("warehouse2-out-0", warehouse);
    }

暫時未發現StreamBridge如何實現響應式互動。

  1. 消費訊息
    應用要消費訊息,只需要定義一個返回型別為Function/Consumer的方法即可。如下
    @Bean
    public Function<Flux<Warehouse>, Mono<Void>> warehouse3() {
        Logger logger = LoggerFactory.getLogger("WarehouseFunction");
        return flux -> flux.doOnNext(data -> {
            logger.info("Warehouse Data: {}", data);
        }).then();
    }

注意:方法名與<functionName> + -out- + <index>/<functionName> + -in- + <index>
spring.cloud.function.definition中的配置需要保持一致,以免出錯。

SpringCloudStream文件:https://docs.spring.io/spring-cloud-stream/docs/3.1.0/reference/html/spring-cloud-stream.html

文章完整程式碼:https://gitee.com/binecy/bin-springreactive/tree/master/warehouse-service

如果您覺得本文不錯,歡迎關注我的微信公眾號,系列文章持續更新中。您的關注是我堅持的動力!

相關文章