Kafka源码分析02:生产者缓冲区

背景:

生产者为了提升吞吐量,在生产者客户端设计了缓冲区

  1. RecordAccumulator实现了消息的缓冲区,从而提升了生产者的吞吐量。
  2. 实现了RecordProducer主线程和Sender线程的解耦。

设计原理

先看整体架构图。

整体架构图

RecordAccumulator主要分为三块:

  • 消息批次集合 ConcurrentMap<TopicPartition,Deque> batches:真正用来保存消息的缓冲区
  • 内存池 Deque bufferPool:用来给消息分配内存
  • RecordAccumulator自身的业务逻辑

消息批次集合batches

是一个CopyOnWriteMap集合,CopyOnWrite这个设计适合读多写少的场景,每次更新的时候都会copy一个副本,在副本里更新。CopyOnWriteMap就是CopyOnWrite这个思路。

为什么缓冲区是读多写少的场景?

缓冲区集合的一个元素是<tp,Deque< ByteBuffer >>,元素的增加和删除的概率很低,因为只有发送的分区增加或减少了才会更新元素,大部分情况下不会出现更新元素的行为。主要的行为还是根据tp获取Deque,主要还是读。

  • 好处是不加锁的前提下读写不会造成线程冲突,提升了吞吐量。
  • 坏处是对内存的占用是很大的,适合读多写少的场景。

bufferPool

bufferPool用来管理ByteBuffer的复用,相当于实现了一套内存管理机制。

bufferPool会向使用者提供ByteBuffer的内存对象,同时当使用者不用的时候,bufferPool会把这个内存对象保存起来等着别的线程用,这样通过内存的复用就减少垃圾回收的成本。

代码分析:

CopyOnWriteMap

它内部的集合其实就是一个非线程安全的map,通过对这个map做一系列的包装按CopyOnWrite的思想实现了线程安全。

  • 非线程安全的Map变量用volatile去修饰,保证了线程间的可见性,只要更新了map这个引用指向的对象地址那么别的线程可以立即看到。
  • 读的时候完全不用加锁,因为读的是一个只读副本,写不会发生在只读副本上,这样读的性能就会非常高,N多线程不加锁读。
  • 写的时候会多个线程调用加锁的putIfAbsent方法,这个方法保证了线程安全,同时所有的操作都用一个锁。如果有了这个元素存在就直接返回,不会再写入写的元素。
  • 保证了KafkaProducer线程的总体线程安全。


public class CopyOnWriteMap<K, V> implements ConcurrentMap<K, V> {

    // 保证可见性

    private volatile Map<K, V> map;

    public CopyOnWriteMap() {

        this.map = Collections.emptyMap();
    }


    public CopyOnWriteMap(Map<K, V> map) {

        this.map = Collections.unmodifiableMap(map);

    }


    @Override

    public boolean containsKey(Object k) {

        return map.containsKey(k);

    }

    @Override

    public boolean containsValue(Object v) {

        return map.containsValue(v);

    }

    @Override

    public Set<java.util.Map.Entry<K, V>> entrySet() {

        return map.entrySet();

    }

    @Override

    public V get(Object k) {

        return map.get(k);
    }

    @Override

    public boolean isEmpty() {

        return map.isEmpty();

    }

    @Override

    public Set<K> keySet() {
        return map.keySet();
    }

    @Override

    public int size() {
        return map.size();
    }

    @Override

    public Collection<V> values() {
        return map.values();
    }

    @Override
    public synchronized void clear() {
        this.map = Collections.emptyMap();
    }

    // 写操作,并把写后的快照提供给读请求。
    @Override
    public synchronized V put(K k, V v) {
        Map<K, V> copy = new HashMap<K, V>(this.map);
        V prev = copy.put(k, v);
        this.map = Collections.unmodifiableMap(copy);
        return prev;
    }

    @Override
    public synchronized void putAll(Map<? extends K, ? extends V> entries) {
        Map<K, V> copy = new HashMap<K, V>(this.map);
        copy.putAll(entries);
        this.map = Collections.unmodifiableMap(copy);
    }

    @Override
    public synchronized V remove(Object key) {
        Map<K, V> copy = new HashMap<K, V>(this.map);
        V prev = copy.remove(key);
        this.map = Collections.unmodifiableMap(copy);
        return prev;
    }


    @Override
    public synchronized V putIfAbsent(K k, V v) {
        if (!containsKey(k))
            return put(k, v);
        else
            return get(k);
    }


    @Override
    public synchronized boolean remove(Object k, Object v) {
        if (containsKey(k) && get(k).equals(v)) {
            remove(k);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public synchronized boolean replace(K k, V original, V replacement) {
        if (containsKey(k) && get(k).equals(original)) {
            put(k, replacement);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public synchronized V replace(K k, V v) {
        if (containsKey(k)) {
            return put(k, v);
        } else {
            return null;
        }
    }
}
复制代码

成员变量


public class BufferPool {
    private final long totalMemory;//默认32M
    private final int poolableSize;//池化大小16k
    private final ReentrantLock lock;
    private final Deque<ByteBuffer> free;//池化的内存
    private final Deque<Condition> waiters;//阻塞线程对应的Condition集合
 private long nonPooledAvailableMemory;//非池化可使用的内存
复制代码

allocate()源码

public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
    //1.验证申请的内存是否大于总内存
    if (size > this.totalMemory)
        throw new IllegalArgumentException("Attempt to allocate " + size
                                           + " bytes, but there is a hard limit of "
                                           + this.totalMemory
                                           + " on memory allocations.");

    ByteBuffer buffer = null;
    //2.加锁,保证线程安全。
    this.lock.lock();
    if (this.closed) {
        this.lock.unlock();
        throw new KafkaException("Producer closed while allocating memory");
    }
    try {
        //3.申请内存的大小是否是池化的内存大小,16k
        if (size == poolableSize && !this.free.isEmpty())
            //如果是就从池里Bytebuffer
            return this.free.pollFirst();
            // 池化内存空间的大小
        int freeListSize = freeSize() * this.poolableSize;
        //4.如果非池化空间加池化内存空间大于等于要申请的空间
        if (this.nonPooledAvailableMemory + freeListSize >= size) {
                    // 如果申请的空间大小小于池化的大小,就从free队列里拿出一个池化的大小的Bytebuffer加到nonPooledAvailableMemory中
            // 5.如果一个池化的大小的Bytebuffer不满足size,就持续释放池化内存Bytebuffer直到满足为止。
            freeUp(size);
            this.nonPooledAvailableMemory -= size;
            //如果非池化可以空间加池化内存空间大于要申请的空间
        } else {
            int accumulated = 0;
            //创建对应的Condition
            Condition moreMemory = this.lock.newCondition();
            try {
                //线程最长阻塞时间
                long remainingTimeToBlockNs = TimeUnit.MILLISECONDS.toNanos(maxTimeToBlockMs);
                //放入waiters集合中
                this.waiters.addLast(moreMemory);
                // 没有足够的空间就一直循环
                while (accumulated < size) {
                    long startWaitNs = time.nanoseconds();
                    long timeNs;
                    boolean waitingTimeElapsed;
                    try {
                        //空间不够就阻塞,并设置超时时间。
                        waitingTimeElapsed = !moreMemory.await(remainingTimeToBlockNs, TimeUnit.NANOSECONDS);
                    } finally {
                        long endWaitNs = time.nanoseconds();
                        timeNs = Math.max(0L, endWaitNs - startWaitNs);
                        recordWaitTime(timeNs);
                    }

                    if (this.closed)
                        throw new KafkaException("Producer closed while allocating memory");
                    if (waitingTimeElapsed) {
                        this.metrics.sensor("buffer-exhausted-records").record();
                        throw new BufferExhaustedException("Failed to allocate memory within the configured max blocking time " + maxTimeToBlockMs + " ms.");
                    }
                    remainingTimeToBlockNs -= timeNs;
                    // 当申请的空间的是池化大小且ByteBuffer池化集合里有元素
                    if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
                        buffer = this.free.pollFirst();
                        accumulated = size;
                    } else {
                        //尝试给nonPooledAvailableMemory扩容
                        freeUp(size - accumulated);
                        int got = (int) Math.min(size - accumulated, this.nonPooledAvailableMemory);
                        this.nonPooledAvailableMemory -= got;
                        //累计分配了多少空间
                        accumulated += got;
                    }
                }
                accumulated = 0;
            } finally {
                this.nonPooledAvailableMemory += accumulated;//把已经分配的内存还回nonPooledAvailableMemory
                this.waiters.remove(moreMemory);//删除对应的condition
            }
        }
    } finally {
        try {
            if (!(this.nonPooledAvailableMemory == 0 && this.free.isEmpty()) && !this.waiters.isEmpty())
                this.waiters.peekFirst().signal();
        } finally {
            lock.unlock();
        }
    }
    if (buffer == null)
        //  返回非池化ByteBuffer分配内存
        return safeAllocateByteBuffer(size);
    else
        //  返回池化的ByteBuffer分配内存
        return buffer;
}
复制代码

allocate() 流程图

deallocate()源码

public void deallocate(ByteBuffer buffer, int size) {
    lock.lock();
    try {
        // 释放的空间是否是池化大小,如果是,free上加一个ByteBuffer对象
        if (size == this.poolableSize && size == buffer.capacity()) {
            buffer.clear();
            this.free.add(buffer);
        } else {
            // 否则增加非池化空间大小
            this.nonPooledAvailableMemory += size;
        }
        // 释放第一个wait();
        Condition moreMem = this.waiters.peekFirst();
        if (moreMem != null)
            moreMem.signal();
    } finally {
        lock.unlock();
    }
}
复制代码

RecordAccumulator.append() 源码 :


public RecordAppendResult append(TopicPartition tp,//要发送的主题分区
                                 long timestamp,//发送时的时间戳
                                 byte[] key,//消息的key
                                 byte[] value,//消息的value
                                 Header[] headers,//消息的头
                                 Callback callback,//生产者的回调方法
                                 long maxTimeToBlock,//最大阻塞时间
                                 boolean abortOnNewBatch,//遇到要创建新的批次就放弃,因为一般不成功是因为
                                 long nowMs//发送的时间
                                  ) throws InterruptedException {

    // 累计发送线程数,
    appendsInProgress.incrementAndGet();
    ByteBuffer buffer = null;
    if (headers == null) headers = Record.EMPTY_HEADERS;
    try {
        // 第一部分:
        // 1. 从batches得到tp对应的 ProducerBatch 队列,如果没有就新建。
        Deque<ProducerBatch> dq = getOrCreateDeque(tp);
        // 2. 第一次加锁,相同的Deque<ProducerBatch>都会竞争这个锁。
        synchronized (dq) {
            //判断生产者是否已经关闭了。
            if (closed)
                throw new KafkaException("Producer closed while send in progress");
            //3.正式往batches里添加消息
            RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq, nowMs);
            //4. 如果Deque<ProducerBatch>最后一个ProducerBatch空间够用,一般情况下会添加成功,返回结果
            if (appendResult != null)
                return appendResult;
        }

        // 第二部分:如果空间不够用。
        // 如果不创建新的批次
        if (abortOnNewBatch) {
            // 5.返回给KafakProducer.doSend()方法后,会引起二次调用append(),同时abortOnNewBatch=false
            return new RecordAppendResult(null, false, false, true);
        }
        // 6. KafakProducer.doSend()方法第二次调用append
        byte maxUsableMagic = apiVersions.maxUsableProduceMagic();
        int size = Math.max(this.batchSize, AbstractRecords.estimateSizeInBytesUpperBound(maxUsableMagic, compression, key, value, headers));
        log.trace("Allocating a new {} byte message buffer for topic {} partition {} with remaining timeout {}ms", size, tp.topic(), tp.partition(), maxTimeToBlock);

        // 7. Deque<ProducerBatch>最后一个ProducerBatch不够用时,使用BufferPool申请新的ByteBuffer
        buffer = free.allocate(size, maxTimeToBlock);
        nowMs = time.milliseconds();
        // 8.第二次加锁
        synchronized (dq) {
            if (closed)
                throw new KafkaException("Producer closed while send in progress");
            //9.第二次往batches里添加消息,其他线程可能已经创建了新的batch,就用当前这个,自己创建的不用了
            RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq, nowMs);
            if (appendResult != null) {
                return appendResult;
            }
            // 10.使用BufferPool新申请的ByteBuffer构建ProducerBatch
            MemoryRecordsBuilder recordsBuilder = recordsBuilder(buffer, maxUsableMagic);
            ProducerBatch batch = new ProducerBatch(tp, recordsBuilder, nowMs);
            // 11.使用BufferPool新申请的ByteBuffer构建ProducerBatch
            FutureRecordMetadata future = Objects.requireNonNull(batch.tryAppend(timestamp, key, value, headers,
                    callback, nowMs));
            // 12.新构建ProducerBatch加入到dq里。
            dq.addLast(batch);
            incomplete.add(batch);
            buffer = null;
            return new RecordAppendResult(future, dq.size() > 1 || batch.isFull(), true, false);
        }
    } finally {
        if (buffer != null)
            free.deallocate(buffer);
        appendsInProgress.decrementAndGet();
    }
}
复制代码
RecordsAccumulator跨Deque换批次

方法会查到对应的Deque集合中最后一个RecordBatch对象,并把消息加到最后一个RecordBatch对象里。

为什么加锁?

在第2、8步对Deque加synchronized锁。加锁的原因是Deque是个非线程安全的对象,所以要加锁。

为什么会加两次加锁而不是在一个完整的synchronized块中完成?

加入A线程发送的消息比较大,需要向BufferPool申请新空间,而此时BufferPool空间不足或者需要空间的过大(需要较长时间分配空间),线程A在BufferPool上等待,此时它依然持有对应Deque的锁;线程B发送的消息较小,Deque最后一个RecordBatch剩余空间够用,但是由于线程A未释放Deque的锁,所以也需要一起等待。若线程B较多,就会造成很多不必要的线程阻塞,降低了吞吐量。这里有一个锁的设计原则:“减少锁的持有时间”。

为什么第二次加锁?

是为了防止多个线程并发向BufferPool申请空间后,造成缓存的浪费。这种场景下图 所示,线程A发现最后一个RecordBatch空间不够用,申请空间并创建一个新RecordBatch对象添加到Deque的尾部;线程B与线程A并发执行,也将新创建一个RecordBatch 添加到Deque尾部。这样就造成线程A创建的RecordBatch空间还没充分利用线程A创建的RecordBatch就成为了队尾,这样A创建的RecordBatch就不是队尾了,这就出现了内存碎片化。

drain()源码:

public Map<Integer, List<ProducerBatch>> drain(Cluster cluster, Set<Node> nodes, int maxSize, long now) {
    if (nodes.isEmpty())
        return Collections.emptyMap();
    Map<Integer, List<ProducerBatch>> batches = new HashMap<>();
    for (Node node : nodes) {
        List<ProducerBatch> ready = drainBatchesForOneNode(cluster, node, maxSize, now);
        batches.put(node.id(), ready);
    }
    return batches;
}
复制代码
drain()对数据如何转换的:

drainBatchesForOneNode():

private List<ProducerBatch> drainBatchesForOneNode(Cluster cluster, Node node, int maxSize, long now) {
    int size = 0;
    //1.获取node上所有分区的集合
    List<PartitionInfo> parts = cluster.partitionsForNode(node.id());
    //初始化要给这个node发送ProducerBatch的集合
    List<ProducerBatch> ready = new ArrayList<>();
    //记录上次停止的位置,这样每次不会从0开始,否则会造成总是发送前几个分区的情况,造成后面的分区饥饿。
    int start = drainIndex = drainIndex % parts.size();
    do {
        //2.获取分区的详情
        PartitionInfo part = parts.get(drainIndex);
        TopicPartition tp = new TopicPartition(part.topic(), part.partition());
        this.drainIndex = (this.drainIndex + 1) % parts.size();
        if (isMuted(tp))
            continue;
        //3.获取主题分区对应的Deque
        Deque<ProducerBatch> deque = getDeque(tp);
        if (deque == null)
            continue;
        synchronized (deque) {
            ProducerBatch first = deque.peekFirst();
            if (first == null)
                continue;
            boolean backoff = first.attempts() > 0 && first.waitedTimeMs(now) < retryBackoffMs;
            if (backoff)
                continue;
            if (size + first.estimatedSizeInBytes() > maxSize && !ready.isEmpty()) {
                break;
            } else {
                if (shouldStopDrainBatchesForPartition(first, tp))
                    break;
                boolean isTransactional = transactionManager != null && transactionManager.isTransactional();
                ProducerIdAndEpoch producerIdAndEpoch =
                    transactionManager != null ? transactionManager.producerIdAndEpoch() : null;

                //4.********重点:::::"每个主题分区只取一个ProducerBatch"
                ProducerBatch batch = deque.pollFirst();
                if (producerIdAndEpoch != null && !batch.hasSequence()) {
 transactionManager.maybeUpdateProducerIdAndEpoch(batch.topicPartition);
                    batch.setProducerState(producerIdAndEpoch, transactionManager.sequenceNumber(batch.topicPartition), isTransactional);
 transactionManager.incrementSequenceNumber(batch.topicPartition, batch.recordCount);
                    log.debug("Assigned producerId {} and producerEpoch {} to batch with base sequence " +
                            "{} being sent to partition {}", producerIdAndEpoch.producerId,
                    producerIdAndEpoch.epoch, batch.baseSequence(), tp);
                    transactionManager.addInFlightBatch(batch);
                }
                batch.close();
                size += batch.records().sizeInBytes();
                //5.加入到reade集合里
                ready.add(batch);
                batch.drained(now);
            }
        }
    } while (start != drainIndex);
    return ready;
}
复制代码

哪些节点的请求已经准备好发送了

ready()源码

public ReadyCheckResult ready(Cluster cluster, long nowMs) {
    //哪些服务端节点可以发送消息
    Set<Node> readyNodes = new HashSet<>();
    long nextReadyCheckDelayMs = Long.MAX_VALUE;
    //找不到Leader副本的分区的主题
    Set<String> unknownLeaderTopics = new HashSet<>();
    //是否有线程在等待BufferPool释放空间,
    boolean exhausted = this.free.queued() > 0;
    //1.遍历batches集合中的所有元素
    for (Map.Entry<TopicPartition, Deque<ProducerBatch>> entry : this.batches.entrySet()) {
        Deque<ProducerBatch> deque = entry.getValue();
        synchronized (deque) {
            //2.取deque第一个ProducerBatch,判断deque是否为空
            ProducerBatch batch = deque.peekFirst();
            if (batch != null) {
                TopicPartition part = entry.getKey();
                //3.查找分区的leader所在的node
                Node leader = cluster.leaderFor(part);
                //leader不存在则就无法发送
                if (leader == null) {
                    // 分区的leader的节点不在元数据中,但是消息还要发送,显然要处理。
                    unknownLeaderTopics.add(part.topic());
                } else if (!readyNodes.contains(leader) && !isMuted(part)) {
                    //已经等了多久没发送了
                    long waitedTimeMs = batch.waitedTimeMs(nowMs);
                    //是否是正在退避:是否重试了,而且已等待的时间小于重试退避时间
                    boolean backingOff = batch.attempts() > 0 && waitedTimeMs < retryBackoffMs;
                    long timeToWaitMs = backingOff ? retryBackoffMs : lingerMs;
                    //deque大于1或第一个batch是否满了
                    boolean full = deque.size() > 1 || batch.isFull();
                    //消息在暂存队列里是否超时了
                    boolean expired = waitedTimeMs >= timeToWaitMs;
                    //4.五个判断条件决定是否是能发送的node
                    boolean sendable = full || expired || exhausted || closed || flushInProgress();
                    //能发送且没有正在退避
                    if (sendable && !backingOff) {
                        //5.如果是能发送就加入readyNodes集合。
                        readyNodes.add(leader);
                    } else {
                        long timeLeftMs = Math.max(timeToWaitMs - waitedTimeMs, 0);
                        //6.还剩多久:需要等待的时间-已经等待的时间。
                        nextReadyCheckDelayMs = Math.min(timeLeftMs, nextReadyCheckDelayMs);
                    }
                }
            }
        }
    }
    return new ReadyCheckResult(readyNodes, nextReadyCheckDelayMs, unknownLeaderTopics);
}
复制代码

写在最后

本人在掘金发布了小册,对kafka做了源码级的剖析。

欢迎支持笔者小册:《Kafka 源码精讲

猜你喜欢

转载自juejin.im/post/7109099213111164942