https://github.com/edenhill/librdkafka
librdkafka是一個開源的Kafka用戶端C/C++實現,提供了Kafka生產者、消費者介面。
一、安裝librdkafka
首先在github上下載librdkafka源碼,解壓後進行編譯;
cd librdkafka-master
chmod 777 configure lds-gen.py
./configure
make
make install
在make的時候,如果是64位Linux會報下面這個異常
/bin/ld:librdkafka.lds:1: syntax error in VERSION script
只要Makefile.config第46行裡面的WITH_LDS=y這一行注釋掉就不會報錯了。
注釋掉:#WITH_LDS=y,然後再make
最終標頭檔和庫檔案會分別安裝在
/usr/local/include/librdkafka
/usr/local/lib
二、調用librdkafka庫自主編程
編譯使用者自己的應用程式,編譯選項要加上-lrdkafka -lz -lpthread -lrt這些選項。
例如,我使用QtCreator之qmake模式,.pro檔案如下:
QMAKE_LFLAGS += -lrdkafka -lrdkafka++ -lz -lpthread -lrt#-lrdkafka等價於 LIBS += /usr/local/lib/librdkafka.so
編譯通過,但是運行時會報錯:error while loading shared libraries: librdkafka.so.1: cannot open shared object file: No such file or directory
此時需要在/etc/ld.so.conf中加入librdkafka.so所在的目錄:/usr/local/lib/
然後在終端執行命令,使之生效:
[root@localhost etc]# ldconfig
注意,/usr/local/lib/每次有庫檔案更新,都需要終端重新運行一次ldconfig這條命令。
三、啟動kafka
詳情參考我的文章:我個人的kafka_2.12-1.0.0實踐:安裝與測試(★firecat推薦★)
四、用法介紹,源檔案來自/librdkafka-master/examples/rdkafka_example.cpp和rdkafka_consumer_example.cpp
Producer的使用方法:
建立kafka用戶端配置預留位置:
conf = rd_kafka_conf_new();即建立一個設定物件(rd_kafka_conf_t)。並通過rd_kafka_conf_set進行brokers的配置。
設定資訊的回調:
用以反饋資訊發送的成敗。通過rd_kafka_conf_set_dr_msg_cb(conf, dr_msg_cb);實現。
建立producer執行個體:
1)初始化:
應用程式需要初始化一個頂層對象(rd_kafka_t)的基礎容器,用於全域配置和共用狀態。
通過調用rd_kafka_new()建立。建立之後,該執行個體就佔有了conf對象,所以conf對象們在rd_kafka_new()調用之後是不能被再次使用的,而且在rd_kafka_new()調用之後也不需要釋放配置資源的。
2)建立topic:
建立的topi對象是可以複用的(producer的執行個體化對象(rd_kafka_t)也是允許複用的,所以這兩者就沒有必要頻繁建立)
執行個體化一個或多個 topic(rd_kafka_topic_t)用於生產或消費。
topic 對象儲存 topic 層級的屬性,並且維護一個映射,
該映射儲存所有可用 partition 和他們的領導 broker 。
通過調用rd_kafka_topic_new()建立(rd_kafka_topic_new(rk, topic, NULL);)。
註:rd_kafka_t 和 rd_kafka_topic_t都源於可選的配置 API。
不使用該 API 將導致 librdkafka 使用列在文檔CONFIGURATION.md中的預設配置。
3)Producer API:
通過調用RD_KAFKA_PRODUCER設定一個或多個rd_kafka_topic_t對象,就可以準備好接收訊息,並組裝和發送到 broker。
rd_kafka_produce()函數接受如下參數:
rkt : 待生產的topic,之前通過rd_kafka_topic_new()產生
partition : 生產的 partition。如果設定為RD_KAFKA_PARTITION_UA(未賦值的),則會根據builtin partitioner去選擇一個確定 partition。kafka會回調partitioner進行均衡選取,partitioner方法需要自己實現。可以輪詢或者傳入key進行hash。未實現則採用預設的隨機方法rd_kafka_msg_partitioner_random隨機播放。
可以嘗試通過partitioner來設計partition的取值。
msgflags : 0 或下面的值:
RD_KAFKA_MSG_F_COPY 表示librdkafka 在資訊發送前立即從 payload 做一份拷貝。如果 payload 是不穩定儲存,如棧,需要使用這個參數。這是以防訊息主體所在的緩衝不是長久使用的,才預先將資訊進行拷貝。
RD_KAFKA_MSG_F_FREE 表示當 payload 使用完後,讓 librdkafka 使用free(3)釋放。 就是在使用完訊息後,將釋放訊息緩衝。
這兩個標誌互斥,如果都不設定,payload 既不會被拷貝也不會被 librdkafka 釋放。
如果RD_KAFKA_MSG_F_COPY標誌不設定,就不會有資料拷貝,librdkafka 將佔用 payload 指標(訊息主體)直到訊息被發送或失敗。librdkafka 處理完訊息後,會調用發送報告回呼函數,讓應用程式重新擷取 payload 的所有權。
如果設定了RD_KAFKA_MSG_F_FREE,應用程式就不要在發送報告回呼函數中釋放 payload。
payload,len : 訊息 payload(message payload,即值),訊息長度
key,keylen : 可選的訊息鍵及其長度,用於分區。將會用於 topic 分區回呼函數,如果有,會附加到訊息中發送給 broker。
msg_opaque : 可選的,應用程式為每個訊息提供的無類型指標,提供給訊息發送回呼函數,用於應用程式引用。
rd_kafka_produce() 是一個非阻塞 API,該函數會將訊息塞入一個內部隊列並立即返回。
如果隊列中的訊息數超過queue.buffering.max.messages屬性配置的值,rd_kafka_produce()通過返回 -1,並將errno設定為ENOBUFS這樣的錯誤碼來反饋錯誤。
提示: 見 examples/rdkafka_performance.c 擷取生產者的使用。 Consumer的使用方法:
consumer API要比producer API多一些狀態。 在使用RD_KAFKA_CONSUMER類型(調用rd_kafka_new時設定的函數參數)建立rd_kafka_t 對象,再通過調用rd_kafka_brokers_add對上述new出來的Kafka handle(rk)進行broker的添加(rd_kafka_brokers_add(rk, brokers)),
然後建立rd_kakfa_topic_t對象之後,
rd_kafka_query_watermark_offsets
建立topic:
rtk = rd_kafka_topic_new(rk, topic, topic_conf)
開始消費:
調用rd_kafka_consumer_start()函數(rd_kafka_consume_start(rkt, partition, start_offset))啟動對給定partition的consumer。
調用rd_kafka_consumer_start需要的參數如下:
rkt : 要消費的 topic ,之前通過rd_kafka_topic_new()建立。
partition : 要消費的 partition。
offset : 消費開始的訊息位移量。可以是絕對的值或兩種特殊的位移量:
RD_KAFKA_OFFSET_BEGINNING 從該 partition 的隊列的最開始消費(最早的訊息)。
RD_KAFKA_OFFSET_END 從該 partition 產生的下一個訊息開始消費。
RD_KAFKA_OFFSET_STORED 使用位移量儲存。
當一個 topic+partition 消費者被啟動,librdkafka 不斷嘗試從 broker 批量擷取訊息來保持本地隊列有queued.min.messages數量的訊息。
本地訊息佇列通過 3 個不同的消費 API 嚮應用程式提供服務:
rd_kafka_consume() - 消費一個訊息 rd_kafka_consume_batch() - 消費一個或多個訊息 rd_kafka_consume_callback() - 消費本地隊列中的所有訊息,且每一個都調用回呼函數
這三個 API 的效能按照升序排列,rd_kafka_consume()最慢,rd_kafka_consume_callback()最快。不同的類型滿足不同的應用需要。
被上述函數消費的訊息返回rd_kafka_message_t類型。
rd_kafka_message_t的成員:
* err - 返回給應用程式的錯誤訊號。如果該值不是零,payload欄位應該是一個錯誤的訊息,err是一個錯誤碼(rd_kafka_resp_err_t)。* rkt,partition - 訊息的 topic 和 partition 或錯誤。* payload,len - 訊息的資料或錯誤的訊息 (err!=0)。* key,key_len - 可選的訊息鍵,生產者指定。* offset - 訊息位移量。
不管是payload和key的記憶體,還是整個訊息,都由 librdkafka 所擁有,且在rd_kafka_message_destroy()被調用後不要使用。
librdkafka 為了避免訊息集的多餘拷貝,會為所有從記憶體緩衝中接收的訊息共用同一個訊息集,這意味著如果應用程式保留單個rd_kafka_message_t,將會阻止記憶體釋放並用於同一個訊息集的其他訊息。
當應用程式從一個 topic+partition中消費完訊息,應該調用rd_kafka_consume_stop()來結束消費。該函數同時會清空當前本地隊列中的所有訊息。
提示: 見 examples/rdkafka_performance.c 擷取消費者的使用。
在Kafka broker中server.properties檔案配置(參數log.dirs=/data2/logs/kafka/)使得寫入到訊息佇列中的topic在該目錄下對分區的形式進行儲存。每個分區partition下是由segment file組成,而segment file包括2大部分:分別為index file和data file,此2個檔案一一對應,成對出現,尾碼”.index”和“.log”分別表示為segment索引檔案、資料檔案。
segment檔案命名規則:partion全域的第一個segment從0開始,後續每個segment檔案名稱為上一個segment檔案最後一條訊息的offset值。數值最大為64位long大小,19位元字字元長度,沒有數字用0填充。 具體樣本:
本文所採用的是cpp方式,和上述介紹的只是函數使用上的不同,商務邏輯是一樣的。
在producer過程中直接是使用PARTITION_UA 但是在消費的時候,不能夠指定partition值為PARTITION_UA因為該值其實是-1,對於Consumer端來說,是無意義的。根據源碼可以知道當不指定partitioner的時候,其實是有一個預設的partitioner,就是Consistent-Random partitioner所謂的一致性隨機partitioner。一致性hash對關鍵字進行map映射之後到一個特定的partition。
函數原型:
rd_kafka_msg_partitioner_consistent_random ( const rd_kafka_topic_t *rkt, const void *key, size_t keylen, int32_t partition_cnt, void *opaque, void *msg_opaque);
PARTITION_UA其實是Unassigned partition的意思,即是未賦值的分區。RD_KAFKA_PARTITION_UA (unassigned)其實是自動採用topic下的partitioner函數,當然也可以直接採用固定的值。
在設定檔config/server.properties中是可以設定partition的數量num.partitions。 分配分區
在分配分區的時候,要注意。對於一個已經建立了分區的主題且已經指定了分區,那麼之後的producer代碼如果是直接修改partitioner部分的代碼,直接引入key值進行分區的重新分配的話,是不行的,會繼續按照之前的分區進行添加(之前的分區是分區0,只有一個)。此時如果在程式中查看partition_cnt我們是可以看到,該值並沒有因為config/server.properties的修改而變化,這是因為此時的partition_cnt是針對該已經建立的主題topic的。
而如果尚自單純修改代碼中的partition_cnt在用於計算分區值時候:djb_hash(key->c_str(), key->size()) % 5 是會得到如下結果的:提示分區不存在。
我們可以通過rdkafka_example來查看某個topic下對應的partition數量的:
./rdkafka_example -L -t helloworld_kugou -b localhost:9092
從中我們可以看到helloworld_kugou主題只有一個partition,而helloworld_kugou1主題是有5個partition的,這個和我們預期的相符合。
我們可以對已經建立的主題修改其分區:
./bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --alter --partition 5 --topic helloworld_kugou
修改完之後,我們可以看出,helloworld_kugou已經變為5個分區了。
具體樣本:
建立topic為helloworld_kugou_test,5個partition。我們可以看到,在producer端進行輸入之前,在預先設定好的log目錄下是已經有5個partition:
producer端代碼:
class ExampleDeliveryReportCb : public RdKafka::DeliveryReportCb { public: void dr_cb (RdKafka::Message &message) { std::cout << "Message delivery for (" << message.len() << " bytes): " << message.errstr() << std::endl; if (message.key()) std::cout << "Key: " << *(message.key()) << ";" << std::endl; }};class ExampleEventCb : public RdKafka::EventCb { public: void event_cb (RdKafka::Event &event) { switch (event.type()) { case RdKafka::Event::EVENT_ERROR: std::cerr << "ERROR (" << RdKafka::err2str(event.err()) << "): " << event.str() << std::endl; if (event.err() == RdKafka::ERR__ALL_BROKERS_DOWN) run = false; break; case RdKafka::Event::EVENT_STATS: std::cerr << "\"STATS\": " << event.str() << std::endl; break; case RdKafka::Event::EVENT_LOG: fprintf(stderr, "LOG-%i-%s: %s\n", event.severity(), event.fac().c_str(), event.str().c_str()); break; default: std::cerr << "EVENT " << event.type() << " (" << RdKafka::err2str(event.err()) << "): " << event.str() << std::endl; break; } }};/* Use of this partitioner is pretty pointless since no key is provided * in the produce() call.so when you need input your key */class MyHashPartitionerCb : public RdKafka::PartitionerCb { public: int32_t partitioner_cb (const RdKafka::Topic *topic, const std::string *key,int32_t partition_cnt, void *msg_opaque) { std::cout<<"partition_cnt="<<partition_cnt<<std::endl; return djb_hash(key->c_str(), key->size()) % partition_cnt; } private: static inline unsigned int djb_hash (const char *str, size_t len) { unsigned int hash = 5381; for (size_t i = 0 ; i < len ; i++) hash = ((hash << 5) + hash) + str[i]; std::cout<<"hash1="<<hash<<std::endl; return hash; }};void TestProducer(){ std::string brokers = "localhost"; std::string errstr; std::string topic_str="helloworld_kugou_test";//自行制定主題topic MyHashPartitionerCb hash_partitioner; int32_t partition = RdKafka::Topic::PARTITION_UA; int64_t start_offset = RdKafka::Topic::OFFSET_BEGINNING;