从源码来分析kafka生产者原理

源码学习是一种挺好的方式,不过根据我的经验最好是先学习大致的原理,再回头看源码,更能抓住重点。

今天带大家过一遍 kafka-python 最新v2.0.2生产者源码,为啥是python,当然是因为我比较熟悉,而且各语言实现都差不多。

本文分2个部分说明:

  • kafka生产者初始化做了什么
  • 发送消息时做了什么

喜欢可以收藏。

例行先上快速开始的代码😁:

from kafka import KafkaProducer


producer = KafkaProducer(
    bootstrap_servers=["ip:9092"],
    retries=3, 
    batch_size=524288, 
    linger_ms=400, 
    buffer_memory=134217728,     # 缓冲区内存调大到128mb
    max_request_size=5048576,  # 每次请求的最大体积
    compression_type='gzip',  # 可选,使用lz4压缩,能极大提高性能。需要安装依赖pip install lz4
)

for i in range(10000):
    producer.send('test'"测试".encode('utf-8'))
producer.flush()

异步发送,做了点参数调优,无甚稀奇。

1、生产者初始化

点开KafkaProducer 类,看看初始化了啥:

    def __init__(self, **configs):
        log.debug("Starting the Kafka producer")  # trace
        self.config = copy.copy(self.DEFAULT_CONFIG)
        for key in self.config:
            if key in configs:
                self.config[key] = configs.pop(key)

        # Only check for extra config keys in top-level class
        assert not configs, 'Unrecognized configs: %s' % (configs,)

        if self.config['client_id'] is None:
            self.config['client_id'] = 'kafka-python-producer-%s' % 
                                       (PRODUCER_CLIENT_ID_SEQUENCE.increment(),)

        if self.config['acks'] == 'all':
            self.config['acks'] = -1

        # api_version was previously a str. accept old format for now
        if isinstance(self.config['api_version'], str):
            deprecated = self.config['api_version']
            if deprecated == 'auto':
                self.config['api_version'] = None
            else:
                self.config['api_version'] = tuple(map(int, deprecated.split('.')))
            log.warning('use api_version=%s [tuple] -- "%s" as str is deprecated',
                        str(self.config['api_version']), deprecated)

        # Configure metrics
        metrics_tags = {'client-id': self.config['client_id']}

第一步是处理配置参数,你没指定的参数,那就给默认值,你传了,那我就校验。

比如client_id 没给我就自动生成一个,带一个固定前缀;

比如api_version 、compression_type 、max_in_flight_requests_per_connection参数获取或生成。

再往下看,到了第一个重点:RecordAccumulator

    self._accumulator = RecordAccumulator(message_version=message_version, metrics=self._metrics, **self.

点进去


    def __init__(self, **configs):
        self.config = copy.copy(self.DEFAULT_CONFIG)
        for key in self.config:
            if key in configs:
                self.config[key] = configs.pop(key)

        self._closed = False
        self._flushes_in_progress = AtomicInteger()
        self._appends_in_progress = AtomicInteger()
        self._batches = collections.defaultdict(collections.deque) # TopicPartition: [ProducerBatch]
        self._tp_locks = {None: threading.Lock()} # TopicPartition: Lock, plus a lock to add entries
        self._free = SimpleBufferPool(self.config['buffer_memory'],
                                      self.config['batch_size'],
                                      metrics=self.config['metrics'],
                                      metric_group_prefix=self.config['metric_group_prefix'])
        self._incomplete = IncompleteProducerBatches()
        # The following variables should only be accessed by the sender thread,
        # so we don't need to protect them w/ locking.
        self.muted = set()
        self._drain_index = 0

注意这行:

self._batches = collections.defaultdict(collections.deque)

RecordAccumulator类是啥呢?熟悉kafka的都知道,这是一个容器,存储消息批次_batches 的。

RecordAccumulator内的_batches在这里的实现是字典,键是TopicPartition,也就是主题+分区号,值是个队列collections.deque,队列内的元素是[ProducerBatch],也就是批次。

所以消息在生产者里,是这样存储的:

  • 一定数量的消息,组成一个批次batch
  • 一个主题的一个分区的所有batch,被放到一个队列里
  • 所有分区及各自的batch队列,共同在一个容器RecordAccumulator里

RecordAccumulator的大小由参数buffer_memory控制,batch的大小由参数batch_size控制。

再往下走,看到第二个重点:Sender线程

        self._sender = Sender(client, self._metadata,
                              self._accumulator, self._metrics,
                              guarantee_message_order=guarantee_message_order,
                              **self.config)
        self._sender.daemon = True
        self._sender.start()

生产者还初始化了个Sender实例,内部继承了线程类,并实现了run方法。并且是一个守护线程,在后台不停轮询:

    def run(self):
        """The main run loop for the sender thread."""
        log.debug("Starting Kafka producer I/O thread.")

        # main loop, runs until close is called
        while self._running:
            try:
                self.run_once()
            except Exception:
                log.exception("Uncaught error in kafka producer I/O thread")

这个Sender线程不停执行run_once方法,点进去看看:(只摘要了重点)

requests = self._create_produce_requests(batches_by_node)

for node_id, request in six.iteritems(requests):
    batches = batches_by_node[node_id]
    log.debug('Sending Produce Request: %r', request)
    (self._client.send(node_id, request, wakeup=False)
         .add_callback(
             self._handle_produce_response, node_id, time.time(), batches)
         .add_errback(
             self._failed_produce, batches, node_id))

Sender线程将消息批次按node归类,发往同一个node的批次放一个请求里,然后进行发送,并传递回调函数。

所以,Sender线程才是真正发送消息的发送者。

2、send()

那问题来了,下面发送消息的send()方法又做了啥?

producer.send(‘test’, “测试”.encode(‘utf-8’))

def send(self, topic, value=None, key=None, headers=None, partition=None, timestamp_ms=None):

        self._wait_on_metadata(topic, self.config['max_block_ms'] / 1000.0)
        
        key_bytes = self._serialize(
            self.config['key_serializer'],
            topic, key)
        value_bytes = self._serialize(
            self.config['value_serializer'],
            topic, value)

        partition = self._partition(topic, partition, key, key_bytes, value_bytes)

        message_size = self._estimate_size_in_bytes(key_bytes, value_bytes, headers)
        self._ensure_valid_record_size(message_size)

        result = self._accumulator.append(tp, timestamp_ms,
                                          key_bytes, value_bytes, headers,
                                          self.config['max_block_ms'],
                                          estimated_size=message_size)

只摘了重要部分,如下:

  • 刷新元数据
  • 对key、value序列化
  • 获取要发送的分区
  • 校验消息size
  • 将消息添加到_accumulator

我们看看添加消息的步骤

从源码来分析kafka生产者原理

添加图片注释,不超过 140 字(可选)

首先取分区对应的队列,往队列的最后一个批次batch里塞消息

要是批次已经满了,就开辟一个新的批次,将消息塞入,并把批次放入队列里。

看到这里是不是很清晰了?send方法实际只是往RecordAccumulator容器里塞消息,Sender线程则在后台不停轮训,符合条件就发送。

总结

细节还有很多,比如api_version怎么生成的,参数怎么处理的,发送体积怎么限制的,具体发送过程是怎么样的,内部实现的什么消息协议,为什么生产者是线程安全的,在源码里你可以看到用了大量的锁。

感兴趣可以自己研究细节,但只要我们把握住了主干脉络,懂了大致原理,就能驾轻就熟,做到胸有成竹。

原创内容,欢迎关注我的专栏,谢谢😀


原文始发于微信公众号(程序员灿然):从源码来分析kafka生产者原理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/241880.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!