Kafka訊息佇列

Howlet發表於2022-01-09

之前也學習過訊息佇列,但一直沒有使用的場景,今天專案中遇到了 kafka 那便有了應用場景


1. Kafka

Kafka 是一個分散式、支援分割槽,多副本的基於 zookeeper 的訊息佇列。使用訊息佇列,是應用 A 將要處理的資訊傳送到訊息佇列然後繼續下面的任務,需要該資訊的應用 B 從訊息佇列裡面獲取資訊再做處理,這樣做像是多此一舉,應用 A 直接發資訊給應用 B 不就可以了嗎?存在即合理,使用訊息佇列其作用如下:


  • 非同步處理:使用者註冊後傳送郵件、簡訊、驗證碼等可以非同步處理,使註冊這個過程寫入資料庫後就可立即返回
  • 流量消峰:秒殺活動超過閾值的請求丟棄轉向錯誤頁面,然後根據訊息佇列的訊息做業務處理
  • 日誌處理:可以將error的日誌單獨給訊息佇列進行持久化處理
  • 應用解耦:購物的下單操作,訂單系統與庫存系統中間加訊息佇列,使二者解耦,若後者故障也不會導致訊息丟失

之前 筆者也寫過 RabbitMQ 的筆記,傳送門







2. 生產消費模型

結合 kafka 的下面這些名詞來解釋其模型會更加容易理解

名稱 解釋
Broker kafka 的例項,部署多臺 kafka 就是有多個 broker
Topic 訊息訂閱的話題,是這些訊息的分類,類似於訊息訂閱的頻道
Producer 生產者,負責往 kafka 傳送訊息
Consumer 消費者,從 kafka 讀取訊息來進行消費







3. 安裝部署

kafka 和依賴的 zookeeper 是 java 編寫的工具,其需要 jdk8 及其以上。筆者這裡使用 Docker 安裝,偷懶了貪圖方便快捷

# 使用 wurstmeister 製作的映象
docker pull wurstmeister/zookeeper
docker pull wurstmeister/kafka


# 啟動 zookeeper
docker run -d --name zookeeper -p 2181:2181 wurstmeister/zookeeper


# 單機啟動 kafka
docker run  -d --name kafka -p 9092:9092 \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=xxx.xxx.xxx.xxx:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://xxx.xxx.xxx.xxx:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka






4. Quickstart

kafka 官網也有很好的介紹,quickstart

# 進入kafka容器
docker exec -it kafka /bin/sh


# 進入 bin 目錄
cd /opt/kafka_2.13-2.8.1/bin


# partitions 分割槽
# replication 副本因子
# 建立一個主題(引數不懂可直接填寫,後面會講解說明)
./kafka-topics.sh --create --partitions 1 --replication-factor 1 --topic quickstart-events --bootstrap-server localhost:9092


# 檢視
./kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092


# 寫入 topic(回車表示一條訊息,ctrl + c 結束輸入)
# 訊息預設儲存 7 天,下一步的消費可以驗證
./kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
This is my first event
This is my second event


# 讀取 topic(執行多次可以讀取訊息,因為預設儲存 7 天)
./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092

./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092






5. SpringBoot 整合

SpringBoot 整合了 Kafka,新增依賴後可使用內建的 KafkaTemplate 模板方法來操作 kafka 訊息佇列


5.1 新增依賴

<!--  sprinboot版本管理中有kafka可不寫版本號  -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>


5.2 配置檔案

server:
  port: 8080

spring:
  # 訊息佇列
  kafka:
    producer:
      # broker地址,重試次數,確認接收個數,訊息的編解碼方式
      bootstrap-servers: 101.200.197.22:9092
      retries: 3
      acks: 1
      key-serializer: org.springframework.kafka.support.serializer.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.StringSerializer
    consumer:
      # broker地址,自動提交,分割槽offset設定
      bootstrap-servers: 101.200.197.22:9092
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer


5.3 生產者

@RestController
@RequestMapping("/kafka")
public class Producer {

    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    @GetMapping("/producer1")
    public String sendMessage1(@RequestParam(value = "message", defaultValue = "123") String message) throws ExecutionException, InterruptedException {
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send("topic1", message);
        SendResult<String, Object> sendResult = future.get();
        return sendResult.toString();
    }

    @GetMapping("/producer2")
    public String sendMessage2(@RequestParam(value = "message", defaultValue = "123") String message) throws ExecutionException, InterruptedException {
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send("topic1", message);
        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
            @Override
            public void onFailure(Throwable ex) {
                System.out.println("faile");
            }

            @Override
            public void onSuccess(SendResult<String, Object> result) {
                System.out.println("success");
            }
        });
        return "";
    }
}


5.4 消費者

@Component
public class Consumer {

    @KafkaListener(topics = {"topic1"})
    public void onMessage(ConsumerRecord<?, ?> record) {
        System.out.println(record.value());
    }
}






6. 儲存目錄結構

kafka
|____kafka-logs
    |____topic1
    |	  |____00000000000000000000.log(儲存接收的訊息)
    |	  |____consumer_offsets-01(消費者偏移量)
    |	  |____consumer_offsets-02
    |____topic2
    	  |____00000000000000000000.log
    	  |____consumer_offsets-01
    	  |____consumer_offsets-02

每臺 broker 例項接收到訊息後將之儲存到 00000.log 裡面,儲存的方式是先入先出。訊息被消費後不會被刪除,相反可以設定 topic 的訊息保留時間,重要的是 Kafka 的效能在資料大小方面實際上是恆定的,因此長時間儲存資料是完全沒問題的


消費者會將自己消費偏移量 offset 提交給 topic 在 _consumer_offsets 裡面儲存,然後通過偏移量來確定訊息的位置,預設從上次消費的位置開始,新增引數 --frombeginning 則從頭開始消費,可獲取之前所有儲存的訊息。kafka 也會定期清除內部的訊息,直到儲存最新的一條(檔案儲存的訊息預設儲存 7 天)







7. 消費組

這個在筆者配置消費者的時候發現的問題,啟動時報錯說沒有指定消費組


  • 每條分割槽訊息只能被同組的一個消費者消費,consumer1 和 consumer2 同組,所以只有其中一個能消費同條訊息
  • 每條分割槽訊息能被不同組的單個消費者消費,consumer2 和 consumer4 不同組,所以都能消費同條訊息
  • 以上二個規則同時成立
  • 其作用是可以保證消費順序,同個分割槽裡的訊息會被同個消費者順序消費






8. 分割槽和副本

topic 訊息儲存的檔案 0000.log 可以進行物理切分,這就是分割槽的概念,類似於資料庫的分庫分表。這樣做的好處在於單個儲存的檔案不會太大從而影響效能,最重要的是分割槽後不是單個檔案序列執行了,而是多區多檔案可並行執行提高了併發能力




分割槽:消費者會消費同一 topic 的不同分割槽,所以會儲存不同分割槽的偏移量,其格式為:GroupId + topic + 分割槽號

副本:副本是對分割槽的備份,叢集中不同的分割槽在不同的 broker 上,但副本會對該分割槽備份到指定數量的 broker 上,這些副本有 leader 和 follower 的區別,leader負責讀寫,掛了再重新選舉,副本為了保持資料一致性







9. 常見問題


9.1 生產者同步和非同步訊息

生產者傳送訊息給 broker,之後 broker 會響應 ack 給生產者,生產者等待接收 ack 訊號 3 秒,超時則重試 3 次

生產者 ack 確認配置:

  • ack = 0:不需要同步訊息
  • ack = 1:則 leader 收到訊息,並儲存到本地 log 之後才響應 ack 資訊
  • ack 預設配置為 2


9.2 消費者自動提交和手動提交

  • 自動提交:消費者 pull 訊息之後馬上將自身的偏移量提交到 broker 中,這個過程是自動的
  • 手動提交:消費者 pull 訊息時或之後,在程式碼裡將偏移量提交到 broker
  • 二者區別:防止消費者 pull 訊息之後掛掉,在訊息還沒消費但又提交了偏移量


9.3 訊息丟失和重複消費

  • 訊息丟失
    • 生產者:配置 ack ,以及配置副本和分割槽數值一致
    • 消費者:設定手動提交
  • 重複消費
    • 設定唯一主鍵,Mysql 主鍵唯一則插入失敗
    • 分散式鎖


9.4 順序消費方案

  • 生產者:關閉重試,使用同步傳送,成功了再發下一條
  • 消費者:訊息傳送到一個分割槽中,只有一個消費組的消費者能接收訊息


相關文章