在kafka的消费者中,有一个非常关键的机制,那就是offset机制。它使得Kafka在消费的过程中即使挂了或者引发再均衡问题重新分配Partation,当下次重新恢复消费时仍然可以知道从哪里开始消费。它好比看一本书中的书签标记,每次通过书签标记(offset)就能快速找到该从哪里开始看(消费)。

Kafka对于offset的处理有两种提交方式:(1) 自动提交(默认的提交方式)   (2) 手动提交(可以灵活地控制offset)

(1) 自动提交偏移量:

Kafka中偏移量的自动提交是由参数enable_auto_commit和auto_commit_interval_ms控制的,当enable_auto_commit=True时,Kafka在消费的过程中会以频率为auto_commit_interval_ms向Kafka自带的topic(__consumer_offsets)进行偏移量提交,具体提交到哪个Partation是以算法:partation=hash(group_id)%50来计算的。

如:group_id=test_group_1,则partation=hash("test_group_1")%50=28

自动提交偏移量示例:

 import pickle
import uuid
from kafka import KafkaConsumer consumer = KafkaConsumer(
bootstrap_servers=['192.168.33.11:9092'],
group_id="test_group_1",
client_id="{}".format(str(uuid.uuid4())),
max_poll_records=500,
enable_auto_commit=True, # 默认为True 表示自动提交偏移量
auto_commit_interval_ms=100, # 控制自动提交偏移量的频率 单位ms 默认是5000ms
key_deserializer=lambda k: pickle.loads(k),
value_deserializer=lambda v: pickle.loads(v)
) # 订阅消费round_topic这个主题
consumer.subscribe(topics=('round_topic',)) try:
while True:
consumer_records_dict = consumer.poll(timeout_ms=1000) # consumer.assignment()可以获取每个分区的offset
for partition in consumer.assignment():
print('主题:{} 分区:{},需要从下面的offset开始消费:{}'.format(
str(partition.topic),
str(partition.partition),
consumer.position(partition)
)) # 处理逻辑.
for k, record_list in consumer_records_dict.items():
print(k)
for record in record_list:
print("topic = {},partition = {},offset = {},key = {},value = {}".format(
record.topic, record.partition, record.offset, record.key, record.value)
) finally:
# 调用close方法的时候会触发偏移量的自动提交 close默认autocommit=True
consumer.close()

返回结果:

在上述代码中,最后调用consumer.close()时候也会触发自动提交,因为它默认autocommit=True,源码如下:

     def close(self, autocommit=True):
"""Close the consumer, waiting indefinitely for any needed cleanup. Keyword Arguments:
autocommit (bool): If auto-commit is configured for this consumer,
this optional flag causes the consumer to attempt to commit any
pending consumed offsets prior to close. Default: True
"""
if self._closed:
return
log.debug("Closing the KafkaConsumer.")
self._closed = True
self._coordinator.close(autocommit=autocommit)
self._metrics.close()
self._client.close()
try:
self.config['key_deserializer'].close()
except AttributeError:
pass
try:
self.config['value_deserializer'].close()
except AttributeError:
pass
log.debug("The KafkaConsumer has closed.")

对于自动提交偏移量,如果auto_commit_interval_ms的值设置的过大,当消费者在自动提交偏移量之前异常退出,将导致kafka未提交偏移量,进而出现重复消费的问题,所以建议auto_commit_interval_ms的值越小越好。

(2) 手动提交偏移量:

鉴于Kafka自动提交offset的不灵活性和不精确性(只能是按指定频率的提交),Kafka提供了手动提交offset策略。手动提交能对偏移量更加灵活精准地控制,以保证消息不被重复消费以及消息不被丢失。

对于手动提交offset主要有3种方式:1.同步提交  2.异步提交  3.异步+同步 组合的方式提交

1.同步手动提交偏移量

同步模式下提交失败的时候一直尝试提交,直到遇到无法重试的情况下才会结束,同时同步方式下消费者线程在拉取消息会被阻塞,在broker对提交的请求做出响应之前,会一直阻塞直到偏移量提交操作成功或者在提交过程中发生异常,限制了消息的吞吐量。

 """
同步的方式10W条消息 4.58s
""" import pickle
import uuid
import time
from kafka import KafkaConsumer consumer = KafkaConsumer(
bootstrap_servers=['192.168.33.11:9092'],
group_id="test_group_1",
client_id="{}".format(str(uuid.uuid4())),
enable_auto_commit=False, # 设置为手动提交偏移量.
key_deserializer=lambda k: pickle.loads(k),
value_deserializer=lambda v: pickle.loads(v)
) # 订阅消费round_topic这个主题
consumer.subscribe(topics=('round_topic',)) try:
start_time = time.time()
while True:
consumer_records_dict = consumer.poll(timeout_ms=100) # 在轮询中等待的毫秒数
print("获取下一轮") record_num = 0
for key, record_list in consumer_records_dict.items():
for record in record_list:
record_num += 1
print("---->当前批次获取到的消息个数是:{}<----".format(record_num))
record_num = 0 for k, record_list in consumer_records_dict.items():
for record in record_list:
print("topic = {},partition = {},offset = {},key = {},value = {}".format(
record.topic, record.partition, record.offset, record.key, record.value)
) try:
# 轮询一个batch 手动提交一次
consumer.commit() # 提交当前批次最新的偏移量. 会阻塞 执行完后才会下一轮poll
end_time = time.time()
time_counts = end_time - start_time
print(time_counts)
except Exception as e:
print('commit failed', str(e)) finally:
consumer.close() # 手动提交中close对偏移量提交没有影响

从上述可以看出,每轮循一个批次,手动提交一次,只有当前批次的消息提交完成时才会触发poll来获取下一轮的消息,经测试10W条消息耗时4.58s

2.异步手动提交偏移量+回调函数

异步手动提交offset时,消费者线程不会阻塞,提交失败的时候也不会进行重试,并且可以配合回调函数在broker做出响应的时候记录错误信息。

 """
异步的方式手动提交偏移量(异步+回调函数的模式) 10W条消息 3.09s
""" import pickle
import uuid
import time
from kafka import KafkaConsumer consumer = KafkaConsumer(
bootstrap_servers=['192.168.33.11:9092'],
group_id="test_group_1",
client_id="{}".format(str(uuid.uuid4())),
enable_auto_commit=False, # 设置为手动提交偏移量.
key_deserializer=lambda k: pickle.loads(k),
value_deserializer=lambda v: pickle.loads(v)
) # 订阅消费round_topic这个主题
consumer.subscribe(topics=('round_topic',)) def _on_send_response(*args, **kwargs):
"""
提交偏移量涉及回调函数
:param args: args[0] --> {TopicPartition:OffsetAndMetadata} args[1] --> Exception
:param kwargs:
:return:
"""
if isinstance(args[1], Exception):
print('偏移量提交异常. {}'.format(args[1]))
else:
print('偏移量提交成功') try:
start_time = time.time()
while True:
consumer_records_dict = consumer.poll(timeout_ms=10) record_num = 0
for key, record_list in consumer_records_dict.items():
for record in record_list:
record_num += 1
print("当前批次获取到的消息个数是:{}".format(record_num)) for record_list in consumer_records_dict.values():
for record in record_list:
print("topic = {},partition = {},offset = {},key = {},value = {}".format(
record.topic, record.partition, record.offset, record.key, record.value)) # 避免频繁提交
if record_num != 0:
try:
consumer.commit_async(callback=_on_send_response)
except Exception as e:
print('commit failed', str(e)) record_num = 0 finally:
consumer.close()

对于args参数:args[0]是一个dict,key是TopicPartition,value是OffsetAndMetadata,表示该主题下的partition对应的offset;args[1]在提交成功是True,提交失败时是一个Exception类。

对于异步提交,由于不会进行失败重试,当消费者异常关闭或者触发了再均衡前,如果偏移量还未提交就会造成偏移量丢失。

3.异步+同步 组合的方式提交偏移量

针对异步提交偏移量丢失的问题,通过对消费者进行异步批次提交并且在关闭时同步提交的方式,这样即使上一次的异步提交失败,通过同步提交还能够进行补救,同步会一直重试,直到提交成功。

 """
同步和异步组合的方式提交偏移量
""" import pickle
import uuid
import time
from kafka import KafkaConsumer consumer = KafkaConsumer(
bootstrap_servers=['192.168.33.11:9092'],
group_id="test_group_1",
client_id="{}".format(str(uuid.uuid4())),
enable_auto_commit=False, # 设置为手动提交偏移量.
key_deserializer=lambda k: pickle.loads(k),
value_deserializer=lambda v: pickle.loads(v)
) # 订阅消费round_topic这个主题
consumer.subscribe(topics=('round_topic',)) def _on_send_response(*args, **kwargs):
"""
提交偏移量涉及的回调函数
:param args:
:param kwargs:
:return:
"""
if isinstance(args[1], Exception):
print('偏移量提交异常. {}'.format(args[1]))
else:
print('偏移量提交成功') try:
start_time = time.time()
while True:
consumer_records_dict = consumer.poll(timeout_ms=100) record_num = 0
for key, record_list in consumer_records_dict.items():
for record in record_list:
record_num += 1
print("---->当前批次获取到的消息个数是:<----".format(record_num))
record_num = 0 for k, record_list in consumer_records_dict.items():
print(k)
for record in record_list:
print("topic = {},partition = {},offset = {},key = {},value = {}".format(
record.topic, record.partition, record.offset, record.key, record.value)
) try:
# 轮询一个batch 手动提交一次
consumer.commit_async(callback=_on_send_response)
end_time = time.time()
time_counts = end_time - start_time
print(time_counts)
except Exception as e:
print('commit failed', str(e)) except Exception as e:
print(str(e))
finally:
try:
# 同步提交偏移量,在消费者异常退出的时候再次提交偏移量,确保偏移量的提交.
consumer.commit()
print("同步补救提交成功")
except Exception as e:
consumer.close()

通过finally在最后不管是否异常都会触发consumer.commit()来同步补救一次,确保偏移量不会丢失

最新文章

  1. Hibernate 系列 02 - Hibernate介绍及其环境搭建
  2. NYOJ 485
  3. robotframework----模板的使用
  4. 将DataTable导出到Excel
  5. JavaScript对象字面量
  6. 【原】UI随设备旋转从iOS6到iOS8的适配策略
  7. linux 下 奇怪的 动态库 依赖问题
  8. POJ3207+tarjan+2-sat
  9. 转:JS日期加减,日期运算
  10. 【转】引入android项目在eclipse ADT中显示中文乱码问题
  11. php 自己写的好看的分页类
  12. DuiVision开发教程(15)-DUI文本控制基础类
  13. git add -f
  14. mssql学习
  15. ios黑科技
  16. JVM启动过程 类加载器
  17. 9. PD逆向工程--由数据库转为模型(ER图)
  18. TCP/IP分析
  19. delphi IDE插件 cnpack 使用记录
  20. JS原型链与继承别再被问倒了

热门文章

  1. 树莓派设置固定IP地址
  2. python3 练手实例6 做一个简单日历
  3. Thinkphp生成的验证码不显示——解决方法
  4. HDU-1709 The Balance(生成函数)
  5. js中函数和方法的区别
  6. vue适配移动端px自动转化为rem
  7. C# 读取 Excel 最全最稳定的方式
  8. JavaWeb之html
  9. Python规范
  10. SqlServer变量