python读写kafka

971 阅读7分钟

上篇总结了下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开始的数据

image.png

写在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()