kafka 事務

 

目錄

概述

冪等

事務

2PC協議

代碼示例

事務流程

事務狀態

Server側狀態

LSO


概述

kafka 從0.11版本開始支持exactly-once語義。從此,流式處理框架數據準確性語義at-most-once、at-least-once、exactly-once全部支持。exactly-once語義的支持複雜度是最高的,單純從語義角度上可理解爲exactly-once=at-least-once+冪等,kafka事務的引入實現exactly-once語義。kafka事務對ACID做了非完全性的支持,不支持事務回滾,事務隔離級別針對Consumer對數據的可見性提出LSO(last-stable offset)概念,Broker端對同一個txnid的事務做串行處理。

Kakfa事務保證以下三點:

1、跨多個Topic-partitoion原子性寫入消息

2、事務中的所有消息要麼全部可見,要麼全部不可見

3、Consumers必須支持跳過Abort或者未提交事務的消息

其中2、3兩點是LSO概念保證的,本文從Consumer、Producer、Broker三個角色分析kafka事務。(全文基於2.3版本)

冪等

kafka中冪等這個概念是針對Proudcer--->Broker寫場景講的,基於at-least-once語義一條消息Broker只能成功存儲一次。kafka引入了pid+sequence number實現了topic-partition局性冪等性。pid全局唯一,使用curator框架由zk生成(curator生成全局ID方案),sequence number由Producer累加生成,Broker側會緩存5條最近pid+sequence組合標識做冪等判斷。

配置:enable.idempotence=true,會自動開啓冪等,默認爲false。(注意:如果設置爲true,以下幾個配置必須設置正確,否則會拋出ConfigException)

max.in.flight.requests.per.connection 小於等於5
retries 大於0
acks 必須設置爲all

 

 

 

 

如果Producer僵死或者Producer服務重啓將會打破冪性,使程序失效。解決跨會話、跨topic-partiton寫冪等需要結合kafka提供的事務

事務

事務ACID四個特性,kafka做了非傳統意義上的支持。

原子性:跨topic-partitionProducer會話原子性寫入。事務不支持回滾,但失敗的事務數據會打上abort標識,也確保了要麼成功,要麼失敗的語義

一致性:通過LSO水位,確保已commit事務產生的數據對Consumer可見

隔離性:概念很弱化,結合LSO支持Consumer支持read_committed和read_uncomitted

持久性:採用多副本策略,支持高可用(涉及 Quorm機制,acks爲all就是Write all Read one,Kafka提出了ISR列表,簡化了Quorm機制)。

分佈式事務有2PC、3PC、ZAB、Paxos等協議,kafka採用了類似2PC協議,引入了事務協調者TransactionCoordinator概念。

2PC協議:

角色:

1. 協調者(Coordinator)或者叫TM(TransactionManagger)

kafka事務協調者爲TransactionsCoordinator,負責事務狀態管理、Transaction Marker

2. 事務參與者(participants)

kafka事務參與者爲Producer、Broker,但kafka支持consumer-transform-producer模式,因此還有Consumer。如果用RM(ResourceManager)概念只能表達Broker。

Producer:Consumer:Broker爲1:1:N

kafka分佈式事務並沒有2PC協議的TM單點,Participants僵死的問題,TransactionCoordinator支持高可用,Producer支持跨會話。

代碼示例

代碼示例中把事務處理流程(DataFlow)和Producer關鍵的配置項均做了說明。具體的流程圖可見Exactly Once Delivery and Transactional Messaging

public static void sendWithTransaction(String topic, Consumer codeBlock){
        //下面列出些核心的配置
        Properties pro = new Properties();
        pro.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"host:port");
        pro.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        pro.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        //Leader/Follower都存儲成功後返回(涉及ISR列表機制和Quorum機制)
        pro.put(ProducerConfig.ACKS_CONFIG,"all");
        //單個Producer實例可使用的總內存,默認爲32M,內存會循環使用
        pro.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 32 * 1024 * 1024L);
        //2.3版本,當設置事務時必須小於等於5,大於5會影響Producer發送消息的順序
        //2.0以前的版本要保證順序只能配置1
        pro.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
        //2.3版本,當設置事務時必須大於1
        pro.put(ProducerConfig.RETRIES_CONFIG, 1);
        //開啓冪等
        pro.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
        //開啓事務,每個事務ID與Producer 的PID是一一對應的,出現重複會拋異常
        pro.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "TXNID_"+Thread.currentThread().getId());
        //事務處理時間,必須小於transaction.max.timeout.ms,默認爲60秒
        pro.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 10000L);
        //默認爲16KB,每個Partition獨立緩存大小爲16KB,消息達到16KB後發出
        pro.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384L);
        //默認爲0,配合batch_size使用,5毫秒後消息內容沒有16KB,也發發出
        pro.put(ProducerConfig.LINGER_MS_CONFIG, 5);
        //初始化kafkaProducer對象,kafkaProduer是線程安全的,一般採用單例模式
        KafkaProducer producer = new KafkaProducer(pro);
        try{
            /**
             * 實始化事務準備階段的信息。如:
             * 1、尋找TransactionCoordinator
             * 2、申請PID或者PID+epoch
             * 3、根據txid尋找未處理結束(指未Commit或者Abort)的事務
             * 4、檢查txid與PID一一對應關係
             */
            producer.initTransactions();
            //開始事務(Producer狀態轉換爲in_transaction),不會與TransactionCoordinator交互
            producer.beginTransaction();
            /**
             * 發送消息
             * 1、發送AddPartitionsToTxnRequest,Producer ---> TransactionCoordinator,持久化到txn_log中
             * 2、發送ProducerRequest,Producer ---> Broker(涉及多個Borker),持久化到user_topic
             */
            producer.send(new ProducerRecord(topic, "key", "value"));
            producer.send(new ProducerRecord(topic, "key", "value"));
            /**
             * consumer-transform-produce模式中使用,發送consumer消費的offset
             * 1、發送AddOffsetCommitToTxnRequest,Producer ---> TransactionCoordinator,持久化到txn_log中
             * 2、發送TxnOffsetCommitRequest,Producer ---> GroupCoordinator,持久化到_consumer-offsets topic中
             */
            producer.sendOffsetsToTransaction(null, "consumerGroup");
            /**
             * 提交事務
             * 發送EndTxnRequest,Producer--->TransactionCoordinator--->Borker(涉及多個Borker)
             * 1、寫入一個PREPARE_COMMIT或者PREPARE_ABORT消息到txn_log中
             * 2、發送WriteTxnMarkerRequest,TransactionCoordinator--->Broker(涉及多個Borker)
             * 3、TransactionCoordinator寫入Commit或者Abort到 txn_log中
             */
            producer.commitTransaction();
        }catch (Exception e){
            //終止事務
            producer.abortTransaction();
        }finally {
            //關閉Producer,不關閉會導致連接泄露
            producer.close();
        }
    }

Producer是線程安全的,支持配置Tpoic list,單例示例代碼:

public class ProducerUtils {
    /**
     * 如果要啓用事務建議一個Topic使用一個KafkaProducer。如果使用一個KafkaProducer,
     * 所有的Topic事務操作都由同一個TransactionCoordinator處理。
     **/
    private static final Map<String, KafkaProducer> producerMap = new ConcurrentHashMap<String, KafkaProducer>();
    /**做爲添加producer的競爭對象**/
    private static final Object LOCK = new Object();

    /**
     * 添加新的Topic相應的Producer
     * @param topic topic
     * @param pro kafka producer的設置
     * @param <K>
     * @param <V>
     */
    public static <K,V> void addProducer(String topic, Properties pro){
        Optional<String> topicS = Optional.of(topic);
        Optional<Properties> proS = Optional.of(pro);
        //判斷是否存在,若不存在添加,如果存在覆蓋
        synchronized (LOCK){
            //創建一個新的producer
            KafkaProducer<K,V> newProducer = new KafkaProducer<K, V>(proS.get());
            KafkaProducer oldProducer = producerMap.get(topicS.get());
            //覆蓋老的producer
            producerMap.put(topic, newProducer);
            //按存在處理
            if (null != oldProducer){
                //關閉老的
                oldProducer.close();
            }
        }

    }

    /**
     * 按topic發送消息
     * @param topic topic
     * @param msg 發送內容
     */
    public static void send(String topic, String key, String msg){
        try{
            KafkaProducer producer = Objects.requireNonNull(producerMap.get(topic));
            // 發送消息
            producer.send(new ProducerRecord(topic, key, msg));
        }catch (NullPointerException e){
            //打印日誌
        }
    }

    public static void sendWithTransaction(String topic, Consumer codeBlock){
        KafkaProducer producer = Objects.requireNonNull(producerMap.get(topic));
        try{
            producer.initTransactions();
            producer.beginTransaction();
            //邏輯處理代碼使用函數編程的Consumer接口封裝
            codeBlock.accept(producer);
            producer.commitTransaction();
        }catch (NullPointerException e){
            //打印日誌
        }catch (ProducerFencedException e){
            producer.abortTransaction();
        }catch (KafkaException e){
            producer.abortTransaction();
        }
    }
}

事務流程

代碼示例中已經把事務中每行代碼操作內容做了說明,我們只需要把這些內容串起來即可。下面是事務處理的流程,總共有五個階段,具體詳情查看Exactly Once Delivery and Transactional Messaging。Transaction log存儲在__transaction_state topic中,Partition 默認爲50,replic factor 爲3。

                                                  (圖片來源:Data Flow URL

1、隨機找一臺Broker,尋找TransactionCoordinator。hash(txid)% size(partitions)來確定TransactionCoordinator

2、申請PID,並寫入Transaction log中

4、4.1 Transaction log存儲 partiton信息 4.2發送本次消息內容給Brokers 4.3 Transaction log存儲consumer offset 4.4 consumer coordinator 提交offset

5、5.1 更新Meta爲PREPAR_COMMIT或者PREPARE_ABORT  5.2 user topic中寫入Transaction marker 5.3 Transaction log 寫入COMMIT或者ABORT

整個事務流程相當複雜,事務的每個狀態、操作信息和結果都持久化在Transaction log(_transaction_state topic中,默認50個partiton)中。其中事務4.x階段最爲關鍵,4.2階段爲Producer把消息內容發送給相應的Broker存儲,4.3階段把consumer的offset信息存儲到Transaction log中,4.4階段把consumer offset信息提交給GroupCoordinator。階段4、5都是先寫Transction log 再做實際處理。

事務狀態

Kafka通過狀態機管理事務的狀態,有Server側狀態、Producer側狀態。(狀態機設計模式:狀態和相應的行爲一起封裝,不同的狀態有不同的行爲,目的是將特定狀態相關的邏輯分散到一些類的狀態中。使用場景:訂單、物流、會員管理、流程引擎等)

Server側狀態

狀態 狀態碼 說明
Empty 0 事務嘗未存在
Ongoing 1 啓動事務
PrepareCommit 2 TransactionCoordinator提交PrepareCommit,Producer可以發送消息給Broker
PrepareAbort 3 TransactionCoordinator提交PrepareAbort,終止事務
CompleteCommit 4 group提交了Commit,事務成功處理
CompleteAbort 5 group提交了Abort,將會刪除事務緩存信息
Dead 6 TransactiontalId已經失效,默認15天
PrepareEpochFence 7 同一個Txid對應兩個Producer,處理掉老的Producer

 

 

 

 

 

 

 

 

 

狀態流轉圖:

LSO

LSO(last stable offset)概念可以確保Consumer只會消費到事務Commit成功的數據,事務Abort產生的數據不會被消費。LSO開關關閉也可以消費到事務Abort產生的數據。Consumer側提供了isolation.level控制,有兩個可選擇配置:READ_COMMITTED、READ_UNCOMMITTED,默認爲READ_COMMITTED。

Kafka 幾個位置的關係

LSO小於等於HW/LEO,LSO可見的位置肯定是事務已經結束了(已經Commit或者Abort)。LSO指向4時,說明5、6、7的數據相關的事務還沒有Commit或者Abort。

Consumer讀到Abort數據後,如果isolation.level配置的是READ_UNCOMMITTED,Consumer會根據Transaction Marker日誌過濾掉這些數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章