上篇总结了下kafka的基础信息,这篇在此基础上总结上怎么使用python进行基础的生产和消费。本篇演示使用的库是kafka-python库(官方文档)。里面有详细的每个参数及方法的说明,有特定化使用场景可参考官方文档。
kafka生产者
我们已经知道,创建producer必须包含topic和value,key和partition可选。然后,序列化key和value对象为ByteArray。实例化producer后,接着就是发送消息。这里主要有3种发送消息的方法(可借助可视化工具Kafka Tool查看是否发送成功):
立即发送(耗时短,可靠性低)
只管发送消息到server端,不care消息是否成功发送。大部分情况下,这种发送方式会成功,因为Kafka自身具有高可用性,producer会自动重试;但有时也会丢失消息;如果业务只关心消息的吞吐量,允许少量消息发送失败,也不关心消息的发送顺序,就可以使用这个发送方式。并且配置ack=0,这样生产者就不需要等待服务器的响应,以网络支持的最大速度发送消息。
from kafka import KafkaProducer
from kafka.errors import KafkaError
import time
import json
class KProducer():
def __init__(self, topic, bootstrap_servers, retries = 5, max_in_flight_requests_per_connection = 1, acks = 1):
self._kwargs = {
"bootstrap_servers":bootstrap_servers,
"acks":acks,
"retries" :retries,
"max_in_flight_requests_per_connection" :max_in_flight_requests_per_connection,
# encode objects via msgpack
# "value_serializer": msgpack.dumps
# produce json message
"key_serializer" :lambda m: json.dumps(m).encode('ascii'),
"value_serializer" :lambda m: json.dumps(m).encode('ascii')
}
self.topic = topic
try:
self.producer = KafkaProducer(**self._kwargs)
except Exception as e:
raise e
def asyn_producer_callback(self, data_li:list):
for data in data_li:
self.producer.send(self.topic, data).add_callback(self.on_send_success).add_errback(on_send_error)
if __name__ == '__main__':
data = ['abc', 'efg', 'hhh']
# 异步发送不care返回
producer = KProducer(topic = 'mediaai_tvad_ad_ssp_test', bootstrap_servers = ["10.23.129.32:9092"], acks = 0)
producer.asyn_producer(data)
[Finished in 0.3s]
同步发送(耗时最长,可靠性高)
通过send()方法发送消息,并返回Future对象。get()方法会等待Future对象,看send()方法是否成功;如果业务要求消息必须是按顺序发送的,则可以使用这种发送方式,并且只能在一个partition上(我们上节说过Kafka只能保证同一分区的顺序),结合参数retries让发送失败时重试,设置max_in_flight_requests_per_connection =1可以控制生产者在收到服务器响应之前只能发送1个消息,从而控制消息顺序发送。
在KProduce类里编写
def sync_producer(self, data_li:list):
for data in data_li:
future = self.producer.send(self.topic, b'data')
try:
record_metadata = future.get(timeout = 10)
partition = record_metadata.partition
offset = record_metadata.offset
print('save success, partition:{}, offset:{}'.format(partition, offset))
except KafkaError:
log.exception()
pass
if __name__ == '__main__':
data = ['abc','efg', 'hjg']
# 同步发送
producer = KProducer(topic = 'topic_name', bootstrap_servers = ["host:port"], retries = 5)
producer.sync_producer(data)
运行结果
save success, partition:0, offset:121955
save success, partition:0, offset:121956
save success, partition:0, offset:121957
[Finished in 0.4s]
异步发送(耗时较快,可靠性较高)
通过带有回调函数的send()方法发送消息,当producer收到Kafka broker的response会触发回调函数。通过回调函数能够对异常情况进行处理,当调用了回调函数时,只有回调函数执行完毕生产者才会结束,否则会一直阻塞;如果业务需要知道消息发送是否成功,并且对消息的顺序毫不关心,那么可以使用这种方式,配置参数retries=0.并将发送失败的消息记录到日志中。
编写在KProducer类
def on_send_success(self, record_metadata):
print(record_metadata.topic)
print(record_metadata.partition)
print(record_metadata.offset)
def on_send_error(self, excp):
log.error('I am an errback', exc_info = excp)
def sync_producer_callback(self, data_li:list):
for data in data_li:
self.producer.send(self.topic, data).add_callback(self.on_send_success).add_errback(self.on_send_error)
def close_producer(self):
try:
self.producer.close()
except Exception as e:
raise
if __name__ == '__main__':
data = ['abc', 'efg', 'hhh']
# 异步发送回调成功后再发送
producer = KProducer(topic = 'topic_name', bootstrap_servers = ["host:port"])
producer.sync_producer_callback(data)
producer.close_producer()
运行结果:(需要打印耗时较高)
topic_name
0
121967
topic_name
0
121968
topic_name
0
121969
[Finished in 0.7s]
kafka消费者
消费者也是首先使用KafkaConsumer类初始化一个消费者对象,然后循环读取数据。在同一个群组中,我们无法让一个线程运行多个消费者,也无法让多个线程安全的共享一个消费者。按照规则,一个消费者使用一个线程。如果一个消费群组中的多个消费者都想要运行的话,那么必须让每个消费者在自己的线程中运行。
如果两个程序的topic和group_id相同,那么他们读取的数据不会重复。如果两个程序的topic相同但是group_id不同,那么他们各自消费全部数据,互不影响。
读取当前全量数据(不读取生产者新发送的数据):
class KConsumer():
def __init__(self, topic, group_id, bootstrap_servers):
'''
初始化一个消费者实例,消费者不是线程安全的。所以建议一个线程实现一个消费者,而不是一个消费者让多个线程共享
下面这些是可选参数,可以在初始化KafkaConsumer实例的时候传进去
enable_auto_commit 是否自动提交。默认是true
auto_commit_interval_ms 自动提交间隔毫秒数
auto_offset_reset = ‘earliest’ 重置偏移量,earliest移到最早的可用消息,latest最新的消息。只会在当前Group第一次运行时有用,一旦这个Group已经有偏移量了,这个参数就不会再起作用了。
group_id指定消费者群组,对于同一个group的成员只有一个消费者实例可以读取数据
'''
self._kwargs = {
"bootstrap_servers" : bootstrap_servers,
"group_id" : group_id,
"enable_auto_commit" : False,
"auto_offset_reset": "earliest",
# "key_deserializer":lambda m: json.loads(m.decode('ascii')),
"value_deserializer":lambda m: json.loads(m.decode('ascii')),
}
try:
self._consumer = KafkaConsumer(**self._kwargs)
# subscribe订阅要消费的主题,传入参数可以为正则表达式,正则表达式可以匹配多个主题。如果有人创建了新的主题,并且主题的名字匹配正则,那么会立即触发一次重平衡,消费者就可以读取到新的主题。
# self._consumer.subscribe("test.*") 匹配所有test相关的主题;可使用unsubcribe()取消主题订阅
self._consumer.subscribe(topics = topic)
except Exception as e:
print('error, {}'.format(e))
def consumerMsg(self):
try:
for consumerrecord in self._consumer:
print(consumerrecord)
self._consumer.commit()
except Exception as e:
print('error01, {}'.format(e))
if __name__ == '__main__':
consumer = KConsumer(topic = 'topic_name', bootstrap_servers = ["host:port"], group_id = 'test_group01')
consumer.consumerMsg()
获取指定分区指定偏移量的数据
对于同一个Topic的同一个Group,你有多少partition就能起多少个进程同时消费
例如我想获取下图分区0中偏移量从121963开始的数据
写在KConsumer类中
# 获取当前主题分区
def get_partitions(self, topic):
return self._consumer.partitions_for_topic(topic)
def get_partition_offset(self, topic, partition = 0):
# 手动指定要消费的分区, 注意:assign和subscribe不可同时使用。assign订阅确定主题和分区,不具有自动再均衡的功能。
self._consumer.assign([TopicPartition(topic, partition = partition)])
# 获取指定消费的分区
print(self._consumer.assignment())
# 获取指定消费分区的第一个偏移量
print(self._consumer.beginning_offsets(self._consumer.assignment()))
# 获取指定分区指定偏移量的value
def get_particula_offset_value(self, topic, partition, offset):
self._consumer.seek(TopicPartition(topic, partition), offset)
for msg in self._consumer:
print(msg.value)
if __name__ == '__main__':
consumer = KConsumer(topic = topic_name', bootstrap_servers = ["host:port"], group_id = 'test_group01')
print(consumer.get_partitions('topic_name'))
consumer.get_partition_offset('topic_name')
consumer.get_particula_offset_value('topic_name', partition = 0, offset = 121963)
输出结果
{0}
{TopicPartition(topic='mediaai_tvad_ad_ssp_test', partition=0)}
{TopicPartition(topic='mediaai_tvad_ad_ssp_test', partition=0): 121937}
hhh
abc
efg
hhh
abc
efg
hhh
轮询
我们知道,Kafka是支持订阅/发布模式的,生产者发送数据给Kafka Broker,那么消费者是如何知道生产者发送了数据呢?其实生产者产生的数据消费者是不知道的,KafkaConsumer采用轮询的方式定期去Kafka Broker中进行数据的检索,如果有数据就用来消费,如果没有就继续轮询等待。消息还可以挂起等需要的时候再恢复。
编写在KConsumer类中
def consumerRecord(self):
try:
#这是一个无限循环,消费者实际上是一个长期运行的应用程序,它通过轮询的方式向Kafka请求数据。
while True:
# 定期循环请求数据,否则就会认为这个Consumer已经挂了,会触发重平衡,它的分区会移交给群组中的其它消费者。传给poll()方法的是一个超时时间。
data = self._consumer.poll(timeout_ms = 5)
# poll()会返回一个记录列表
if data:
for key in data:
consumerrecord = data.get(key)[0]:
if consumerrecord:
message = {
"Topic":consumerrecord.topic,
"Partition":consumerrecord.partition,
"Offset":consumerrecord.offset,
"Key":consumerrecord.key,
"Value":consumerrecord.value
}
print(message)
self._consumer.commit()
else:
print("{} consumerrecord is None".format(key))
except Exception as e:
raise e
finally:
# 在退出程序前使用close()关闭消费者。网络连接和socket也会随之关闭并立即触发一次重平衡,而不是等待群组协调器发现它不再发送心跳并认定它已经死亡。
self._consumer.close()