介绍

前面讲过producer会将数据保存在RecordAccumulator中,并通过Sender发送数据。RecordAccumulator 就相当于一个队列保存着那些准备发送到server的数据。

在producer中,有几个参数和RecordAccumulator 有关系:

  1. buffer.memory

    buffer.memory主要用来保存要发送的数据,里面的内存大部分是用来让RecordAccumulator保存数据的。

  2. compression.type

    压缩格式

  3. batch.size

    每个发送的batch大小

  4. linger.ms

    如果batch没有达到batch.size大小,但是已经等待了linger.ms长的时间,也会发送

从上面的内容我们大体可以看出RecordAccumulator的作用:

  1. 数据读进来了,分配内存,并保存数据到一个一个的batch中,并返回是添加成功还是失败了。
  2. 找到那些满足发送条件的batches,然后由sender发送,发送的时候,如果有需要保证发送信息的前后顺序。
  3. flush数据,将所有的消息都发送出去。
  4. 强行停止,所有的batch都不发送了。
  5. 释放内存,2,3,4执行完了后,都需要将对应的batch占用的内存释放掉。

RecordAccumulator 的数据都保存在指定大小的内存中,所以会有一个内存池来分配内存。这个变量就是private final BufferPool free;

private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches; 是用来保存消息队列的。里面每个TopicPartition,都会有一个Deque,保存每个RecordBatch。RecordBatch的本质就是一个ByteBuffer,它的大小就是前面介绍中提到的batch.size的大小。



图1

图1表示的RecordAccumulator的内存分配,大部分都是给了batches,还有一小部分给了正在飞的batch(发送到服务器,但是没有收到response)

添加数据append

在KafkaProducer的doSend函数中,会调用append函数将数据写入accumulator 中。


  1. private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
  2. ....
  3. RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
  4. if (result.batchIsFull || result.newBatchCreated) {
  5. log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
  6. this.sender.wakeup();
  7. }
  8. return result.future;
  9. ....
  10. }

append 函数主要将消息append到TopicPartition的batch中。在append的时候,如果batch已经存在了,就直接添加到对应的batch中。如果batch不存在,那就从bufferPool中申请一个新的内存,然后写入消息。

  1. public RecordAppendResult append(TopicPartition tp,
  2. long timestamp,
  3. byte[] key,
  4. byte[] value,
  5. Callback callback,
  6. long maxTimeToBlock) throws InterruptedException {
  7. // We keep track of the number of appending thread to make sure we do not miss batches in
  8. // abortIncompleteBatches().
  9. appendsInProgress.incrementAndGet();
  10. try {
  11. // check if we have an in-progress batch
  12. // 创建或者获取 tp 对应的 Deque
  13. Deque<RecordBatch> dq = getOrCreateDeque(tp);
  14. // 如果有Deque中有batch,就往这个batch中添加信息,并返回添加结果,如果没有,就返回null
  15. synchronized (dq) {
  16. if (closed)
  17. throw new IllegalStateException("Cannot send after the producer is closed.");
  18. RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
  19. if (appendResult != null)
  20. return appendResult;
  21. }
  22. // we don't have an in-progress record batch try to allocate a new batch
  23. // 如果没有batch, 就分配一个内存出来
  24. int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
  25. log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
  26. ByteBuffer buffer = free.allocate(size, maxTimeToBlock);
  27. synchronized (dq) {
  28. // Need to check if producer is closed again after grabbing the dequeue lock.
  29. if (closed)
  30. throw new IllegalStateException("Cannot send after the producer is closed.");
  31. //再次尝试添加,如果添加成功了,那就说明已经有另外一个线程建好了batch,这个时候就把刚分配好的内存还到bufferPool
  32. RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
  33. if (appendResult != null) {
  34. // Somebody else found us a batch, return the one we waited for! Hopefully this doesn't happen often...
  35. free.deallocate(buffer);
  36. return appendResult;
  37. }
  38. // 开始创建 batch
  39. MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
  40. RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
  41. //开始添加消息到batch中,如果这次添加失败了,那就说明有问题了,抛出一个异常
  42. // 不过应该不会发生返回null的情况
  43. FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds()));
  44. dq.addLast(batch);
  45. // 将这个batch 标记为不完整
  46. incomplete.add(batch);
  47. return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
  48. }
  49. } finally {
  50. appendsInProgress.decrementAndGet();
  51. }
  52. }

在上面函数中有几点需要理解的地方:

  1. 分配内存这段代码并没有包含在synchronized 中,所以很可能同时会有多个线程申请内存。这个时候如果线程A申请到内存后,线程B已经创建好了,并且创建好了batch(这段代码用synchronized包含,所以同时只有一个线程进行操作)。那么线程A应该再次去尝试添加,如果添加成功了,就释放内存,将内存还给BufferPool。
  2. 为什么分配内存这段代码没有被包含在synchronized 中呢,因为BufferPool会一直等待,直到有足够的内存分配给申请的线程。如果加到synchronized中,那整个Deque都会被锁住,那其他线程就没办法访问这个Deque了。
  3. 如果数据写入到batch的频率和Sender发送的频率相等,那么每次写入batch的时候都需要申请内存,创建batch。如果数据写入到batch的频率大于Sender发送的频率,那么每次写入batch的时候就可以直接写入这个batch,直到batch满了或者等待的时间大于等于linger.ms。

获取数据

整个数据的获取都包含在Sender 的 run 函数中。

  1. 找到集群中那些已经准备好发送信息的节点。
  2. 获取要发送到这些节点的RecordBatchs.
  3. 找到那些已经过期的RecordBatchs。
  1. void run(long now) {
  2. 获取到当前的集群信息
  3. Cluster cluster = metadata.fetch();
  4. // get the list of partitions with data ready to send
  5. 获取当前准备发送的partitions,获取的条件如下:
  6. 1.record set 满了
  7. 2.record 等待的时间达到了 lingerms
  8. 3.accumulator 的内存满了
  9. 4.accumulator 要关闭了
  10. RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
  11. 如果有些partition没有leader信息,更新metadata
  12. // if there are any partitions whose leaders are not known yet, force metadata update
  13. if (!result.unknownLeaderTopics.isEmpty()) {
  14. // The set of topics with unknown leader contains topics with leader election pending as well as
  15. // topics which may have expired. Add the topic again to metadata to ensure it is included
  16. // and request metadata update, since there are messages to send to the topic.
  17. for (String topic : result.unknownLeaderTopics)
  18. this.metadata.add(topic);
  19. this.metadata.requestUpdate();
  20. }
  21. 去掉那些不能发送信息的节点,能够发送的原因有:
  22. 1.当前节点的信息是可以信赖的
  23. 2.能够往这些节点发送信息
  24. // remove any nodes we aren't ready to send to
  25. Iterator<Node> iter = result.readyNodes.iterator();
  26. long notReadyTimeout = Long.MAX_VALUE;
  27. while (iter.hasNext()) {
  28. Node node = iter.next();
  29. if (!this.client.ready(node, now)) {
  30. iter.remove();
  31. notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
  32. }
  33. }
  34. 获取要发送的records
  35. // create produce requests
  36. Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
  37. result.readyNodes,
  38. this.maxRequestSize,
  39. now);
  40. 保证发送的顺序
  41. if (guaranteeMessageOrder) {
  42. // Mute all the partitions drained
  43. for (List<RecordBatch> batchList : batches.values()) {
  44. for (RecordBatch batch : batchList)
  45. this.accumulator.mutePartition(batch.topicPartition);
  46. }
  47. }
  48. 检查那些过期的records
  49. List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);
  50. // update sensors
  51. for (RecordBatch expiredBatch : expiredBatches)
  52. this.sensors.recordErrors(expiredBatch.topicPartition.topic(), expiredBatch.recordCount);
  53. sensors.updateProduceRequestMetrics(batches);
  54. 构建request并发送
  55. List<ClientRequest> requests = createProduceRequests(batches, now);
  56. // If we have any nodes that are ready to send + have sendable data, poll with 0 timeout so this can immediately
  57. // loop and try sending more data. Otherwise, the timeout is determined by nodes that have partitions with data
  58. // that isn't yet sendable (e.g. lingering, backing off). Note that this specifically does not include nodes
  59. // with sendable data that aren't ready to send since they would cause busy looping.
  60. long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
  61. if (result.readyNodes.size() > 0) {
  62. log.trace("Nodes with data ready to send: {}", result.readyNodes);
  63. log.trace("Created {} produce requests: {}", requests.size(), requests);
  64. pollTimeout = 0;
  65. }
  66. 将这些requests加入channel
  67. for (ClientRequest request : requests)
  68. client.send(request, now);
  69. // if some partitions are already ready to be sent, the select time would be 0;
  70. // otherwise if some partition already has some data accumulated but not ready yet,
  71. // the select time will be the time difference between now and its linger expiry time;
  72. // otherwise the select time will be the time difference between now and the metadata expiry time;
  73. 真正的发送消息
  74. this.client.poll(pollTimeout, now);
  75. }

在发送消息之前,produer需要直到那些节点是可以发送消息的,而这个就是通过 public ReadyCheckResult ready(Cluster cluster, long nowMs) 来获得的。

mute

在这里需要了解RecordAccumulator 的一个成员变量private final Set<TopicPartition> muted;。这个set里面保存了所有已经发送batch到server中,但是没有收到ack的TopicPartition,俗称inflight。等到接收到server的reponse后,会将对应的TopicPartition从set中去掉。这样子就可以保证每个TopicPartition的发送顺序。

举例子,假如topic1要发送A,B,C三个batch到server。如果直接将A,B,C按照顺序发送过去,server的收到的顺序可能是C,B,A,这样子落到log中的顺序就变掉了。如果使用mute,发送A,mute里面就包含了topic1, 这个时候,Sender就不会从topic1所在的Deque中取batch了,直到produer收到了batch A 对应的response,然后从mute中去掉topic1。然后发送B...这样子就保证了服务器接收的顺序和producer发送的消息是一样的。

ready

在发送消息之前,需要确定一些信息:

  1. 哪些TopicPartition所对应的Node节点是可以发送信息的。
  2. 下次检查节点是否ready的时间。
  3. 哪些TopicPartition对应的leader找不到。

这些都是在ready函数中实现的,返回的结果封装在ReadyCheckResult中。

实际上,在发送过程中,可以向一个节点发送消息的时候需要满足下面的条件:

  1. 这个节点中至少有一个partition是可以正常发送的(没有处在backing off状态),并且这个 partition 没有 muted。
  2. batch 没有满,但是已经等了lingerMs 长的时间。
  3. accumulator 满了。
  4. accumulator 关闭了。
  1. public ReadyCheckResult ready(Cluster cluster, long nowMs) {
  2. Set<Node> readyNodes = new HashSet<>();
  3. long nextReadyCheckDelayMs = Long.MAX_VALUE;
  4. Set<String> unknownLeaderTopics = new HashSet<>();
  5. boolean exhausted = this.free.queued() > 0;
  6. for (Map.Entry<TopicPartition, Deque<RecordBatch>> entry : this.batches.entrySet()) {
  7. TopicPartition part = entry.getKey();
  8. Deque<RecordBatch> deque = entry.getValue();
  9. /*
  10. * 遍历batches中每个tp
  11. * 获取tp对应的leader
  12. */
  13. Node leader = cluster.leaderFor(part);
  14. synchronized (deque) {
  15. // 如果 leader 为 null ,并且deque 不为空,说明要发送消息却找不到cluster中接收消息的节点
  16. // 就添加到 unknownLeaderTopics
  17. if (leader == null && !deque.isEmpty()) {
  18. // This is a partition for which leader is not known, but messages are available to send.
  19. // Note that entries are currently not removed from batches when deque is empty.
  20. unknownLeaderTopics.add(part.topic());
  21. // 如果leader 没有ready, 并且 part 没有在飞
  22. } else if (!readyNodes.contains(leader) && !muted.contains(part)) {
  23. RecordBatch batch = deque.peekFirst();
  24. if (batch != null) {
  25. // 如果这个 batch 重试了, 就看看这个 batch 上次发送的时间 + retryBackoffMs 的时间长度 比当前时间要晚
  26. boolean backingOff = batch.attempts > 0 && batch.lastAttemptMs + retryBackoffMs > nowMs;
  27. // 等待的时间
  28. long waitedTimeMs = nowMs - batch.lastAttemptMs;
  29. // 等待时间
  30. long timeToWaitMs = backingOff ? retryBackoffMs : lingerMs;
  31. // 剩余的时间
  32. long timeLeftMs = Math.max(timeToWaitMs - waitedTimeMs, 0);
  33. // batch 满了
  34. boolean full = deque.size() > 1 || batch.records.isFull();
  35. // batch 过期了,它等待的时间已经超过了 timeToWaitMs
  36. boolean expired = waitedTimeMs >= timeToWaitMs;
  37. boolean sendable = full || expired || exhausted || closed || flushInProgress();
  38. if (sendable && !backingOff) {
  39. readyNodes.add(leader);
  40. } else {
  41. // Note that this results in a conservative estimate since an un-sendable partition may have
  42. // a leader that will later be found to have sendable data. However, this is good enough
  43. // since we'll just wake up and then sleep again for the remaining time.
  44. nextReadyCheckDelayMs = Math.min(timeLeftMs, nextReadyCheckDelayMs);
  45. }
  46. }
  47. }
  48. }
  49. }
  50. return new ReadyCheckResult(readyNodes, nextReadyCheckDelayMs, unknownLeaderTopics);
  51. }

drain

知道了要往那些Node 发送数据,就需要从accumulator中获取要发送的数据,要获取的数据的大小为max.request.size, 它是由几个不同的partition的batch组成的。这些batch可以被发送的的条件是:

  1. batch对应的tp没有数据在飞(已经发送出去了,但是没有收到response)。
  2. batch处在retry状态,并且已经等待了backoff长的时间。

通过drain 函数,就可以得到这次request要发送batches了。因为drain是多线程并发的,所以在从Deque中取batch的时候,需要synchronized(deque)。

  1. public Map<Integer, List<RecordBatch>> drain(Cluster cluster,
  2. Set<Node> nodes,
  3. int maxSize,
  4. long now) {
  5. if (nodes.isEmpty())
  6. return Collections.emptyMap();
  7. Map<Integer, List<RecordBatch>> batches = new HashMap<>();
  8. for (Node node : nodes) {
  9. int size = 0;
  10. //获取node 中对应的partition
  11. List<PartitionInfo> parts = cluster.partitionsForNode(node.id());
  12. List<RecordBatch> ready = new ArrayList<>();
  13. /* to make starvation less likely this loop doesn't start at 0 */
  14. // 避免每次都从一个相同的partition开始,别的partition没机会发送数据
  15. int start = drainIndex = drainIndex % parts.size();
  16. do {
  17. PartitionInfo part = parts.get(drainIndex);
  18. TopicPartition tp = new TopicPartition(part.topic(), part.partition());
  19. // Only proceed if the partition has no in-flight batches.
  20. if (!muted.contains(tp)) {
  21. Deque<RecordBatch> deque = getDeque(new TopicPartition(part.topic(), part.partition()));
  22. if (deque != null) {
  23. // 注意synchronized
  24. synchronized (deque) {
  25. RecordBatch first = deque.peekFirst();
  26. if (first != null) {
  27. // 查看当前batch 是不是在retry,并且没有达到需要等待的 backoff时间,如果不是的话,就加入
  28. boolean backoff = first.attempts > 0 && first.lastAttemptMs + retryBackoffMs > now;
  29. // Only drain the batch if it is not during backoff period.
  30. if (!backoff) {
  31. // 如果batch的大小大于maxSize 并且 ready 里面有东西,就停止
  32. // 这时候有一种情况就是batch的大小大于maxSize但是ready 里面没有内容就把这个batch加入ready中
  33. if (size + first.records.sizeInBytes() > maxSize && !ready.isEmpty()) {
  34. // there is a rare case that a single batch size is larger than the request size due
  35. // to compression; in this case we will still eventually send this batch in a single
  36. // request
  37. break;
  38. } else {
  39. //添加到ready, 注意要close batch.records
  40. RecordBatch batch = deque.pollFirst();
  41. batch.records.close();
  42. size += batch.records.sizeInBytes();
  43. ready.add(batch);
  44. batch.drainedMs = now;
  45. }
  46. }
  47. }
  48. }
  49. }
  50. }
  51. this.drainIndex = (this.drainIndex + 1) % parts.size();
  52. } while (start != drainIndex);
  53. batches.put(node.id(), ready);
  54. }
  55. return batches;
  56. }

flush

在发送消息的时候,如果想要将所有的数据都发送出去,就需要调用kafkaproducer的flush函数。调用flush后,会将所有的batch都发送出去(不严谨)。

  1. public void flush() {
  2. log.trace("Flushing accumulated records in producer.");
  3. this.accumulator.beginFlush();
  4. this.sender.wakeup();
  5. try {
  6. this.accumulator.awaitFlushCompletion();
  7. } catch (InterruptedException e) {
  8. throw new InterruptException("Flush interrupted.", e);
  9. }
  10. }

上面是flush函数的实现,首先开始flush,然后通知sender 发送消息,最后等待所有消息发送完毕。

这里面涉及到 RecordAccumulator 的一个成员变量flushesInProgress,它是一个AtomicInteger。当它大于0的时候,所有的batch都会被发送出去。

那么beginFlush就是将flushesInProgress++。

  1. public void beginFlush() {
  2. this.flushesInProgress.getAndIncrement();
  3. }

在ready函数中,判断是否可以发送batch的条件如下:

  1. public ReadyCheckResult ready(Cluster cluster, long nowMs) {
  2. ....
  3. for (Map.Entry<TopicPartition, Deque<RecordBatch>> entry : this.batches.entrySet()) {
  4. //判断条件
  5. boolean sendable = full || expired || exhausted || closed || flushInProgress();
  6. if (sendable && !backingOff) {
  7. readyNodes.add(leader);
  8. } else {
  9. ....
  10. }
  11. ....
  12. }
  13. boolean flushInProgress() {
  14. return flushesInProgress.get() > 0;
  15. }

在append 数据的时候,如果batch是新建的,就会将这个batch加入到incomplete 的Set中,直到收到了服务器的response,才会将这个batch从 incomplete 去掉。而awaitFlushCompletion就是等待incomplete 为空后,就结束了。这样子accumulator中所有的数据都会被发送出去。

  1. public void awaitFlushCompletion() throws InterruptedException {
  2. try {
  3. for (RecordBatch batch : this.incomplete.all())
  4. batch.produceFuture.await();
  5. } finally {
  6. this.flushesInProgress.decrementAndGet();
  7. }
  8. }

abort

当然还有Sender要强制关闭的时候,这个时候就需要将accumulator中所有的batch占用的内存释放掉,然后close掉就Ok了。

RecordAccumulator 1的更多相关文章

  1. Kafka无消息丢失配置

    Kafka到底会不会丢数据(data loss)? 通常不会,但有些情况下的确有可能会发生.下面的参数配置及Best practice列表可以较好地保证数据的持久性(当然是trade-off,牺牲了吞 ...

  2. kafka源码分析之二客户端分析

    客户端由两种:生产者和消费者 1. 生产者 先看一下生产者的构造方法: private KafkaProducer(ProducerConfig config, Serializer<K> ...

  3. kafka producer源码

    producer接口: /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor l ...

  4. Kafka源码分析-序列2 -Producer

    在上一篇,我们从使用方式和策略上,对消息队列做了一个宏观描述.从本篇开始,我们将深入到源码内部,仔细分析Kafka到底是如何实现一个分布式消息队列.我们的分析将从Producer端开始. 从Kafka ...

  5. 锁 和 CopyOnWrite的实现

    1.普通锁 只有lock功能, Java实现:ReentrantLock lock = new ReentrantLock(); lock和unlock: lock.lock(); lock.unlo ...

  6. Kafka 0.10 Metadata的补充

    什么是Metadata? Topic/Partion与broker的映射关系:每一个Topic的每一个Partion的Leader.Follower的信息. 它存在哪里?持久化在Zookeeper中: ...

  7. Kafka 0.10 Producer网络流程简述

    1.Producer 网络请求 1.1 Producer Client角度 KafkaProducer主要靠Sender来发送数据给Broker. Sender: 该线程handles the sen ...

  8. kafka 客户端 producer 配置参数

    属性 描述 类型 默认值 bootstrap.servers 用于建立与kafka集群的连接,这个list仅仅影响用于初始化的hosts,来发现全部的servers.格式:host1:port1,ho ...

  9. Spring Boot2.0 整合 Kafka

    Kafka 概述 Apache Kafka 是一个分布式流处理平台,用于构建实时的数据管道和流式的应用.它可以让你发布和订阅流式的记录,可以储存流式的记录,并且有较好的容错性,可以在流式记录产生时就进 ...

随机推荐

  1. Android ToolBar 的简单封装

    使用过 ToolBar 的朋友肯定对其使用方法不陌生,由于其使用方法非常easy.假设对 ActionBar 使用比較熟练的人来说.ToolBar 就更easy了!只是,相信大家在使用的过程中都遇到过 ...

  2. 【Linux】进程调度概述

    1 可运行队列 (基于实时进程调度) 调度程序中最主要的数据结构式运行队列(runqueue).可运行队列是给定处理器上的可运行进程的链表,每一个处理器一个. 每一个可投入运行的进程都唯一的归属于一个 ...

  3. log4j.propertie配置具体解释

    1.log4j.rootCategory=INFO, stdout , R 此句为将等级为INFO的日志信息输出到stdout和R这两个目的地,stdout和R的定义在以下的代码,能够随意起名.等级可 ...

  4. win10+ubuntu双系统卸载ubuntu

    进入win10下载EasyUEFI,删除ubuntu的引导项.重启如果直接进入了win10,表示卸载成功了.然后可以格式化ubuntu的分区.

  5. Making ViewState More Secure

    Unencrypted view state in ASP.NET 2.0 could leak sensitive information https://www.rapid7.com/db/vul ...

  6. xBIM 基础13 WeXplorer 设置模型颜色

    系列目录    [已更新最新开发文章,点击查看详细]  默认情况下模型具有合理的图形表示.这是从IFC模型中获取的,它应该在所有工具中看起来相同,它应该与您或您的用户的创作环境中的相同.但有时候能够改 ...

  7. Ubuntu 14.04下从源码安装qt4.x

    转自:http://www.cnblogs.com/crazywangzx/p/3505293.html 1.到官网http://qt-project.org/downloads或者ftp://ftp ...

  8. MySQL本地密码过期处理及永不过期设置

    今天在使用mysql的时候,提示“your password has expired”,看了一下问题是因为我本地mysql的密码已经过期了,然后搜罗了一下网上的解决办法.(我的mysql版本 5.7. ...

  9. POJ 3264 Balanced Lineup【线段树】

    题意:给出n个数,a1,a2,a3,---,an,再给出q次询问区间al到ar之间的最大值和最小值的差 学习线段树的第一道题目 学习的这一篇 http://www.cnblogs.com/kuangb ...

  10. Python3基础笔记---序列化

    1.json模块   菜鸟教程 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写. import json json.dumps json ...