文章目錄
參考:
Kafka的使用
一、kafka基本原理
1.1 消息隊列的作用
- 解耦:解除消息生產者和消息消費者的依賴關係
- 異步:消息生產者發送消息後,可以做其他的事
- 削峯:緩解流量高峯
1.2 點對點模式
- 點對點模式通常是基於拉取或者輪詢的消息傳送模型,這個模型的特點是發送到隊列的消息被一個且只有一個消費者進行處理。
- 生產者將消息放入消息隊列後,由消費者主動的去拉取消息進行消費。
- 點對點模型的的優點:是消費者拉取消息的頻率可以由自己控制。
- 缺點:消息隊列是否有消息需要消費,在消費者端無法感知,所以在消費者端需要額外的線程去監控。
1.3 發佈訂閱模式
- 生產者將消息放入消息隊列後,隊列會將消息推送給訂閱過該類消息的消費者。由於是消費者被動接收推送,所以無需感知消息隊列是否有待消費的消息
- 消息隊列卻無法感知消費者消費的速度!所以推送的速度成了發佈訂閱模模式的一個問題。
1.4 kafka
Kafka是一種高吞吐量的分佈式發佈訂閱消息系統,它可以處理消費者規模的網站中的所有動作流數據,具有高性能、持久化、多副本備份、橫向擴展能力
Producer:Producer即生產者,消息的產生者,是消息的入口。
kafka cluster:
Broker:Broker是kafka實例,每個服務器上有一個或多個kafka的實例,我們姑且認爲每個broker對應一臺服務器。每個kafka集羣內的broker都有一個不重複的編號,如圖中的broker-0、broker-1等……
Topic:消息的主題,可以理解爲消息的分類,kafka的數據就保存在topic。在每個broker上都可以創建多個topic。
Partition:Topic的分區,每個topic可以有多個分區,分區的作用是做負載,提高kafka的吞吐量。同一個topic在不同的分區的數據是不重複的,partition的表現形式就是一個一個的文件夾!
Replication: 每一個分區都有多個副本,副本的作用是做備胎。當主分區(Leader)故障的時候會選擇一個備胎(Follower)上位,成爲Leader。在kafka中默認副本的最大數量是10個,且副本的數量不能大於Broker的數量,follower和leader絕對是在不同的機器,同一機器對同一個分區也只可能存放一個副本(包括自己)。
Message:每一條發送的消息主體。
Consumer:消費者,即消息的消費方,是消息的出口。
Consumer Group:我們可以將多個消費組組成一個消費者組,在kafka的設計中同一個分區的數據只能被消費者組中的某一個消費者消費。同一個消費者組的消費者可以消費同一個topic的不同分區的數據, 這也是爲了提高kafka的吞吐量!
Zookeeper:kafka集羣依賴zookeeper來保存集羣的的元信息,來保證系統的可用性。
1.5 工作流程分析
1.5.1 發送數據
producer產生消息,將消息寫到leader當中,不會直接將數據寫入到follower。寫入的流程如下:
注:
消息寫入leader後,follower是主動的去leader進行同步的!producer採用push模式將數據發佈到broker,每條消息追加到分區中,順序寫入磁盤,所以保證同一分區內的數據是有序的!寫入示意圖如下:
- 分區的目的:
(1)方便擴展。因爲一個topic可以有多個partition,所以我們可以通過擴展機器去輕鬆的應對日益增長的數據量。
(2)提高併發。以partition爲讀寫單位,可以多個消費者同時消費數據,提高了消息的處理效率。 - kafka分發的幾個原則:
(1)partition在寫入的時候可以指定需要寫入的partition,如果有指定,則寫入對應的partition。
(2) 如果沒有指定partition,但是設置了數據的key,則會根據key的值hash出一個partition。
(3)如果既沒指定partition,又沒有設置key,則會輪詢選出一個partition。 - ACK應答機制:
(1)0代表producer往集羣發送數據不需要等到集羣的返回,不確保消息發送成功。安全性最低但是效率最高。
(2)1代表producer往集羣發送數據只要leader應答就可以發送下一條,只確保leader發送成功。
(3)all代表producer往集羣發送數據需要所有的follower都完成從leader的同步纔會發送下一條,確保leader發送成功和所有的副本都完成備份。安全性最高,但是效率最低。
注:如果topic不存在,kafka會自動創建topic,分區和副本的數量根據默認配置都是1。
1.5.2 保存數據
Kafka初始會單獨開闢一塊磁盤空間,順序寫入數據(效率比隨機寫入高)。
-
Partition結構
每個topic分爲一個或多個Partition,Partition在服務器上的表現形式就是一個一個的文件夾,每個partition的文件夾下面會有多組segment文件,每組segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中沒有)三個文件, log文件就實際是存儲message的地方,而index和timeindex文件爲索引文件,用於檢索消息。
如上圖,這個partition有三組segment文件,每個log文件的大小是一樣的,但是存儲的message數量是不一定相等的(每條的message大小不一致)。文件的命名是以該segment最小offset來命名的,如000.index存儲offset爲0~368795的消息,kafka就是利用分段+索引的方式來解決查找效率的問題。 -
Message結構
log文件就實際是存儲message的地方,消息主要包含消息體、消息大小、offset、壓縮類型……等等。
(1)offset:offset是一個佔8byte的有序id號,它可以唯一確定每條消息在parition內的位置!
(2)消息大小:消息大小佔用4byte,用於描述消息的大小。
(3)消息體:消息體存放的是實際的消息數據(被壓縮過),佔用的空間根據具體的消息而不一樣。 -
存儲策略
無論消息是否被消費,kafka都會保存所有的消息。那對於舊數據有什麼刪除策略呢?
(1)基於時間,默認配置是168小時(7天)。
(2)基於大小,默認配置是1073741824。
需要注意的是,kafka讀取特定消息的時間複雜度是O(1),所以這裏刪除過期的文件並不會提高kafka的性能!
1.5.3 消費數據
Kafka採用的是點對點的模式,消費者主動的去kafka集羣拉取消息,與producer相同的是,消費者在拉取消息的時候也是找leader去拉取。
圖示是消費者組內的消費者小於partition數量的情況,所以會出現某個消費者消費多個partition數據的情況,消費的速度也就不及只處理一個partition的消費者的處理速度!如果是消費者組的消費者多於partition的數量,那會不會出現多個消費者消費同一個partition的數據呢?上面已經提到過不會出現這種情況!多出來的消費者不消費任何partition的數據。所以在實際的應用中,建議消費者組的consumer的數量與partition的數量一致!
- 數據查找:
假如現在需要查找一個offset爲368801的message是什麼樣的過程呢?下圖:
(1)先找到offset的368801message所在的segment文件(利用二分法查找),這裏找到的就是在第二個segment文件。
(2)打開找到的segment中的.index文件(也就是368796.index文件,該文件起始偏移量爲368796+1,我們要查找的offset爲368801的message在該index內的偏移量爲368796+5=368801,所以這裏要查找的相對offset爲5)。由於該文件採用的是稀疏索引的方式存儲着相對offset及對應message物理偏移量的關係,所以直接找相對offset爲5的索引找不到,這裏同樣利用二分法查找相對offset小於或者等於指定的相對offset的索引條目中最大的那個相對offset,所以找到的是相對offset爲4的這個索引。
(3)根據找到的相對offset爲4的索引確定message存儲的物理偏移位置爲256。打開數據文件,從位置爲256的那個地方開始順序掃描直到找到offset爲368801的那條Message。
segment+有序offset+稀疏索引+二分查找+順序查找
在早期的版本中,消費者將消費到的offset維護zookeeper中,consumer每間隔一段時間上報一次,這裏容易導致重複消費,且性能不好!在新的版本中消費者消費到的offset已經直接維護在kafk集羣的__consumer_offsets這個topic中!
二、Kafka的c++使用
- 生產者
kafkaproducer.h
#ifndef KAFKAPRODUCER_H
#define KAFKAPRODUCER_H
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <csignal>
#include <cstring>
#include <list>
#include <librdkafka/rdkafkacpp.h>
#include <vector>
#include <fstream>
using std::string;
using std::list;
using std::cout;
using std::endl;
using std::vector;
using std::fstream;
class KafkaProducerDeliveryReportCallBack : 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 KafkaProducerEventCallBack : 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)
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;
}
}
};
class KafkaProducer
{
public:
KafkaProducer(const string &brokers, const string &topics, int nPpartition = 0);
virtual ~KafkaProducer();
bool Init();
void Send(const string &msg);
void Stop();
private:
RdKafka::Producer *m_pProducer = NULL;
RdKafka::Topic *m_pTopic = NULL;
KafkaProducerDeliveryReportCallBack m_producerDeliveryReportCallBack;
KafkaProducerEventCallBack m_producerEventCallBack;
std::string m_strTopics;
std::string m_strBroker;
bool m_bRun = false;
int m_nPpartition = 0;
};
#endif // KAFKAPRODUCER_H
kafkaproducer.cpp
#include <iostream>
#include "kafkaproducer.h"
KafkaProducer::KafkaProducer(const string &brokers, const string &topics, int nPpartition /*= 1*/)
: m_bRun(true), m_strTopics(topics), m_strBroker(brokers), m_nPpartition(nPpartition)
{
}
KafkaProducer::~KafkaProducer()
{
Stop();
}
bool KafkaProducer::Init()
{
string errstr = "";
/*
* Create configuration objects
*/
RdKafka::Conf *conf = RdKafka::Conf::create(RdKafka::Conf::CONF_GLOBAL);
RdKafka::Conf *tconf = RdKafka::Conf::create(RdKafka::Conf::CONF_TOPIC);
/*Set configuration properties,設置broker list*/
if (conf->set("metadata.broker.list", m_strBroker, errstr) != RdKafka::Conf::CONF_OK){
std::cerr << "RdKafka conf set brokerlist failed :" << errstr.c_str() << endl;
}
/* Set delivery report callback */
conf->set("dr_cb", &m_producerDeliveryReportCallBack, errstr);
conf->set("event_cb", &m_producerEventCallBack, errstr);
/*
* Create producer using accumulated global configuration.
*/
m_pProducer = RdKafka::Producer::create(conf, errstr);
if (!m_pProducer) {
std::cerr << "Failed to create producer: " << errstr << std::endl;
return false;
}
std::cout << "% Created producer " << m_pProducer->name() << std::endl;
/*
* Create topic handle.
*/
m_pTopic = RdKafka::Topic::create(m_pProducer, m_strTopics,
tconf, errstr);
if (!m_pTopic) {
std::cerr << "Failed to create topic: " << errstr << std::endl;
return false;
}
return true;
}
void KafkaProducer::Send(const string &msg)
{
if (!m_bRun)
return;
/*
* Produce message
*/
RdKafka::ErrorCode resp = m_pProducer->produce(m_pTopic, m_nPpartition,
RdKafka::Producer::RK_MSG_COPY /* Copy payload */,
const_cast<char *>(msg.c_str()), msg.size(),
NULL, NULL);
if (resp != RdKafka::ERR_NO_ERROR)
std::cerr << "Produce failed: " << RdKafka::err2str(resp) << std::endl;
else
std::cerr << "Produced message (" << msg.size() << " bytes)" << std::endl;
m_pProducer->poll(0);
/* Wait for messages to be delivered */ //firecat add
while (m_bRun && m_pProducer->outq_len() > 0) {
std::cerr << "Waiting for " << m_pProducer->outq_len() << std::endl;
m_pProducer->poll(1000);
}
}
void KafkaProducer::Stop()
{
delete m_pTopic;
delete m_pProducer;
}
int main()
{
//KafkaProducerClient* KafkaprClient_ = new KafkaProducerClient("localhost:9092", "test", 0);
KafkaProducer* Kafkapr_ = new KafkaProducer("localhost:9092", "test", 0);
Kafkapr_->Init();
Kafkapr_->Send("hello world!");
char str_msg[] = "Hello Kafka!";
while (fgets(str_msg, sizeof(str_msg), stdin))
{
size_t len = strlen(str_msg);
if (str_msg[len - 1] == '\n')
{
str_msg[--len] = '\0';
}
if (strcmp(str_msg, "end") == 0)
{
break;
}
Kafkapr_->Send(str_msg);
}
return 0;
}
- 消費者
kafka_comsumer.h
#include <vector>
#include <string>
#include <memory>
#include <getopt.h>
#include <csignal>
#include <iostream>
#include "librdkafka/rdkafkacpp.h"
class kafka_consumer_client{
public:
kafka_consumer_client(const std::string& brokers, const std::string& topics, std::string groupid, int64_t offset=-1);
//kafka_consumer_client();
virtual ~kafka_consumer_client();
bool initClient();
bool consume(int timeout_ms); //消費消息
void finalize();
private:
void consumer(RdKafka::Message *msg, void *opt);
std::string brokers_;
std::string topics_;
std::string groupid_;
int64_t last_offset_ = 0;
RdKafka::Consumer *kafka_consumer_ = nullptr;
RdKafka::Topic *topic_ = nullptr;
int64_t offset_ = RdKafka::Topic::OFFSET_BEGINNING;
int32_t partition_ = 0;
};
kafka_comsumer.cpp
#include "kafka_comsumer.h"
bool run_ = true;
static void sigterm (int sig) {
run_ = false;
}
kafka_consumer_client::kafka_consumer_client(const std::string& brokers, const std::string& topics, std::string groupid, int64_t offset):brokers_(brokers), topics_(topics),groupid_(groupid),
offset_(offset){
}
//kafka_consumer_client::kafka_consumer_client(){}
kafka_consumer_client::~kafka_consumer_client(){}
bool kafka_consumer_client::initClient(){
RdKafka::Conf *conf = nullptr;
conf = RdKafka::Conf::create(RdKafka::Conf::CONF_GLOBAL);
if(!conf){
fprintf(stderr, "RdKafka create global conf failed\n");
return false;
}
std::string errstr;
/*設置broker list*/
if (conf->set("bootstrap.servers", brokers_, errstr) != RdKafka::Conf::CONF_OK){
fprintf(stderr, "RdKafka conf set brokerlist failed : %s\n", errstr.c_str());
}
/*設置consumer group*/
if (conf->set("group.id", groupid_, errstr) != RdKafka::Conf::CONF_OK){
fprintf(stderr, "RdKafka conf set group.id failed : %s\n", errstr.c_str());
}
std::string strfetch_num = "10240000";
/*每次從單個分區中拉取消息的最大尺寸*/
if(conf->set("max.partition.fetch.bytes", strfetch_num, errstr) != RdKafka::Conf::CONF_OK){
fprintf(stderr, "RdKafka conf set max.partition failed : %s\n", errstr.c_str());
}
/*創建kafka consumer實例*/
kafka_consumer_ = RdKafka::Consumer::create(conf, errstr);
if(!kafka_consumer_){
fprintf(stderr, "failed to ceate consumer\n");
}
delete conf;
RdKafka::Conf *tconf = nullptr;
/*創建kafka topic的配置*/
tconf = RdKafka::Conf::create(RdKafka::Conf::CONF_TOPIC);
if(!tconf){
fprintf(stderr, "RdKafka create topic conf failed\n");
return false;
}
/*kafka + zookeeper,當消息被消費時,會想zk提交當前groupId的consumer消費的offset信息,
當consumer再次啓動將會從此offset開始繼續消費.在consumter端配置文件中(或者是
ConsumerConfig類參數)有個"autooffset.reset"(在kafka 0.8版本中爲auto.offset.reset),
有2個合法的值"largest"/"smallest",默認爲"largest",此配置參數表示當此groupId下的消費者,
在ZK中沒有offset值時(比如新的groupId,或者是zk數據被清空),consumer應該從哪個offset開始
消費.largest表示接受接收最大的offset(即最新消息),smallest表示最小offset,即從topic的
開始位置消費所有消息.*/
if(tconf->set("auto.offset.reset", "smallest", errstr) != RdKafka::Conf::CONF_OK){
fprintf(stderr, "RdKafka conf set auto.offset.reset failed : %s\n", errstr.c_str());
}
topic_ = RdKafka::Topic::create(kafka_consumer_, topics_, tconf, errstr);
if(!topic_){
fprintf(stderr, "RdKafka create topic failed : %s\n", errstr.c_str());
}
delete tconf;
RdKafka::ErrorCode resp = kafka_consumer_->start(topic_, partition_, offset_);
if (resp != RdKafka::ERR_NO_ERROR){
fprintf(stderr, "failed to start consumer : %s\n", RdKafka::err2str(resp).c_str());
}
return true;
}
void kafka_consumer_client::consumer(RdKafka::Message *message, void *opt){
switch(message->err()){
case RdKafka::ERR__TIMED_OUT:
break;
case RdKafka::ERR_NO_ERROR:
printf("%.*s\n",
static_cast<int>(message->len()),
static_cast <const char*>(message->payload()));
last_offset_ = message->offset();
break;
case RdKafka::ERR__PARTITION_EOF:
std::cerr << "%% Reached the end of the queue, offset: " << last_offset_ << std::endl;
break;
case RdKafka::ERR__UNKNOWN_TOPIC:
case RdKafka::ERR__UNKNOWN_PARTITION:
std::cerr << "Consume failed: " << message->errstr() << std::endl;
run_ = false;
break;
default:
std::cerr << "Consume failed: " << message->errstr() << std::endl;
run_ = false;
break;
}
}
bool kafka_consumer_client::consume(int timeout_ms){
RdKafka::Message *msg = nullptr;
//消費消息
while(run_){
msg = kafka_consumer_->consume(topic_, partition_, timeout_ms);
consumer(msg, nullptr);
kafka_consumer_->poll(0);
delete msg;
}
kafka_consumer_->stop(topic_, partition_);
if(topic_){
delete topic_;
topic_ = nullptr;
}
if(kafka_consumer_){
delete kafka_consumer_;
kafka_consumer_ = nullptr;
}
/*銷燬kafka實例*/
RdKafka::wait_destroyed(5000);
return true;
}
三、Kafka的python使用
- 安裝kafka
pip install kafka-python
- 簡單使用
# test.py
import sys
import time
import json
from kafka import KafkaProducer
from kafka import KafkaConsumer
from kafka.errors import KafkaError
KAFAKA_HOST = "127.0.0.1"
KAFAKA_PORT = 9092
KAFAKA_TOPIC = "test123"
class Kafka_producer():
'''''
生產模塊:根據不同的key,區分消息
'''
def __init__(self, kafkahost,kafkaport, kafkatopic, key):
self.kafkaHost = kafkahost
self.kafkaPort = kafkaport
self.kafkatopic = kafkatopic
self.key = key
print("producer:h,p,t,k",kafkahost,kafkaport,kafkatopic,key)
bootstrap_servers = '{kafka_host}:{kafka_port}'.format(
kafka_host=self.kafkaHost,
kafka_port=self.kafkaPort
)
print("boot svr:",bootstrap_servers)
self.producer = KafkaProducer(bootstrap_servers = bootstrap_servers
)
def sendjsondata(self, params):
try:
parmas_message = json.dumps(params,ensure_ascii=False)
producer = self.producer
print(parmas_message)
v = parmas_message.encode('utf-8')
k = key.encode('utf-8')
print("send msg:(k,v)",k,v)
producer.send(self.kafkatopic, key=k, value= v)
producer.flush()
except KafkaError as e:
print (e)
class Kafka_consumer():
'''
消費模塊: 通過不同groupid消費topic裏面的消息
'''
def __init__(self, kafkahost, kafkaport, kafkatopic, groupid):
self.kafkaHost = kafkahost
self.kafkaPort = kafkaport
self.kafkatopic = kafkatopic
self.groupid = groupid
self.key = key
self.consumer = KafkaConsumer(self.kafkatopic, group_id = self.groupid,
bootstrap_servers = '{kafka_host}:{kafka_port}'.format(
kafka_host=self.kafkaHost,
kafka_port=self.kafkaPort )
)
def consume_data(self):
try:
for message in self.consumer:
yield message
print("1")
print(message)
except KeyboardInterrupt as e:
print (e)
def main(xtype, group, key):
'''
測試consumer和producer
'''
if xtype == "p":
# 生產模塊
producer = Kafka_producer(KAFAKA_HOST, KAFAKA_PORT, KAFAKA_TOPIC, key)
print ("===========> producer:", producer)
for _id in range(100):
# params = '{"msg" : "%s"}' % str(_id)
params=[{"msg0" :_id},{"msg1" :_id}]
producer.sendjsondata(params)
time.sleep(1)
if xtype == 'c':
# 消費模塊
consumer = Kafka_consumer(KAFAKA_HOST, KAFAKA_PORT, KAFAKA_TOPIC, group)
print ("===========> consumer:", consumer)
message = consumer.consume_data()
print('2')
print(message)
for msg in message:
print ('msg---------------->k,v', msg.key,msg.value)
print ('offset---------------->', msg.offset)
if __name__ == '__main__':
xtype = sys.argv[1]
group = sys.argv[2]
key = sys.argv[3]
main(xtype, group, key)
- 啓動kafka服務
kafka-server-start /usr/local/etc/kafka/server.properties