kafka源码解析4:RecordAccumulator的相关组件BufferPool,CopyOnWriteMap(上)

概述

上节解析了生产者发送消息时RecordAccumulator的相关操作,本节解析下RecordAccumulator用到的其他组件BufferPool,CopyOnWriteMap

BufferPool缓冲池

上节讲到RecordAccumulator在append的时候会申请缓冲区,每个批次ProducerBatch在封装消息时MemoryRecordsBuilder会用到缓冲区ByteBuffer(MemoryRecordsBuilder和消息压缩这一块我觉得太边缘了,策略调整以后只解析关键流程和收获比较多的部分,其他的有机会再仔细分享)。
如果高频率的创建ByteBuffer有可能会造成虚拟机的频繁GC,这里kafka使用了BufferPool,BufferPool和数据库连接池的设计理念一样,是一个池子的概念,池子受一个最大使用内存的限制,需要就从池子申请一个ByteBuffer,不够用了就阻塞等待,用好了再还给池子并清空。

    private final long totalMemory; //buffer.memory buffer容量
    private final int poolableSize; //batch.size 批量大小
    private final ReentrantLock lock; //防止多线程申请空间出现并发问题的重入锁
    private final Deque free; //空闲buffer队列
    private final Deque waiters; //等待分配buffer的队列
    /** Total available memory is the sum of nonPooledAvailableMemory and the number of byte buffers in free * poolableSize.  */
    private long nonPooledAvailableMemory;//没分配到空闲队列的可用容量,totalMemory = poolableSize * free
    

allocate和deallocate

既然是个缓冲池,那么BufferPool主要提供两个api:借(allocate)和还(deallocate),这两个api流程比较简单。

allocate

  1. 对lock进行加锁。
  2. 如果空闲队列free里正好有同长度的,就直接出队并返回ByteBuffer。
  3. 如果空闲队列里所有的长度总和与nonPooledAvailableMemory大于申请的长度,循环释放掉空闲队列的ByteBuffer直到nonPooledAvailableMemory大于申请长度,并且重新初始化一个需要长度的ByteBuffer并返回。
  4. 如果所有可用(空闲队列里所有的长度总和与nonPooledAvailableMemory)还是小于申请长度,即不符合2的条件,那么就要阻塞等待了。这里先从lock初始化了一个条件变量Condition并放入等待队列,然后调用Condition的await阻塞等待,注意await会释放掉锁让其他线程拿锁执行直到接收到signal通知,接到通知后会继续执行,如果还不满足条件那么重复这一流程循环阻塞,直到拿到需要长度的ByteBuffer或者超时。
  5. 如果仔细分析一下,要是一个线程申请空间很大,其阻塞的时候其他线程就会一直执行并且用掉剩余空间,这样会产生饥饿啊,我个人理解kafka就是想通过这种机制来保证大容量ByteBuffer申请时不会影响正常容量ByteBuffer的申请。如果有频繁的大容量ByteBuffer申请,那考虑调整buffer.memory,batch.size这两个参数或者业务层面规避掉。
  6. 前面加了锁,finally千万别忘记释放掉。
  7. 前面只是保证容量够用,最后会初始化ByteBuffer。

    public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
        //申请的容量比pool总容量还大,抛异常
        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;
        this.lock.lock();
        try {
            // 有分配好的直接返回
            if (size == poolableSize && !this.free.isEmpty())
                return this.free.pollFirst();

            // 看看剩余空闲容量多少
            int freeListSize = freeSize() * this.poolableSize;
            if (this.nonPooledAvailableMemory + freeListSize >= size) {
                // 还够的话释放掉空闲的
                freeUp(size);
                // 没分配的容量先预扣掉
                this.nonPooledAvailableMemory -= size;
            } else {
                // 剩余也不够
                // accumulated 用来标记现在已经准备好了多少,如果异常了已经准备多少了得在finally里加回去,要不就泄露了
                int accumulated = 0;
                // 设置一个条件变量,用来线程间同步用
                Condition moreMemory = this.lock.newCondition();
                try {
                    //一直不够也不会一直等,设置一个最大等待时间
                    long remainingTimeToBlockNs = TimeUnit.MILLISECONDS.toNanos(maxTimeToBlockMs);
                    this.waiters.addLast(moreMemory);
                    // loop over and over until we have a buffer or have reserved
                    // 循环等待
                    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);
                            this.waitTime.record(timeNs, time.milliseconds());
                        }

                        if (waitingTimeElapsed) {
                            throw new TimeoutException("Failed to allocate memory within the configured max blocking time " + maxTimeToBlockMs + " ms.");
                        }

                        remainingTimeToBlockNs -= timeNs;

                        // check if we can satisfy this request from the free list,
                        // otherwise allocate memory
                        if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
                            // 如果有线程正好释放掉了,那就直接从free弹出来一个
                            buffer = this.free.pollFirst();
                            // accumulated也得标记一下拿到了
                            accumulated = size;
                        } else {
                            // free不够,但是总剩余够了,先释放掉free里的
                            freeUp(size - accumulated);
                            int got = (int) Math.min(size - accumulated, this.nonPooledAvailableMemory);
                            // 剩余的先扣除掉
                            this.nonPooledAvailableMemory -= got;
                            // 标记一下,拿到了需要的容量
                            accumulated += got;
                        }
                    }
                    // Don't reclaim memory on throwable since nothing was thrown
                    //重置为0
                    accumulated = 0;
                } finally {
                    // When this loop was not able to successfully terminate don't loose available memory
                    //异常情况每分出去,准备好的要加回去,如果每一场的话accumulated是0
                    this.nonPooledAvailableMemory += accumulated;
                    //把放在队列的条件变量弹出去,该下一个条件变量准备分配了
                    this.waiters.remove(moreMemory);
                }
            }
        } finally {
            // signal any additional waiters if there is more memory left
            // over for them
            try {
                //要是现在剩余容量没了那也别费劲signal下一个了,继续阻塞着吧
                //这样会不会导致其他阻塞线程一直等下去呢?不会,因为归还的时候也会signal的
                if (!(this.nonPooledAvailableMemory == 0 && this.free.isEmpty()) && !this.waiters.isEmpty())
                    this.waiters.peekFirst().signal();
            } finally {
                // Another finally... otherwise find bugs complains
                lock.unlock();
            }
        }

        if (buffer == null)
            return safeAllocateByteBuffer(size);
        else
            return buffer;
    }

deallocate

还实在是没啥可解析的,简单点。

  1. 别忘加锁,多线程环境下不加锁肯定池子要泄露的。
  2. clear清空,如果size和poolableSize相等的话先不释放掉放在free队列里,万一下次有申请的直接复用了不需要初始化,如果不相等的话nonPooledAvailableMemory直接容量加回去就好了调用方会把引用指向null,虚拟机会回收的。
  3. 通知在等待的线程,如果都是大容量的申请这里是个公平队列哈先来的先分配,后来的后分配。
  4. 解锁别忘了。
    public void deallocate(ByteBuffer buffer, int size) {
        lock.lock();
        try {
            if (size == this.poolableSize && size == buffer.capacity()) {
                buffer.clear();
                this.free.add(buffer);
            } else {
                this.nonPooledAvailableMemory += size;
            }
            Condition moreMem = this.waiters.peekFirst();
            if (moreMem != null)
                moreMem.signal();
        } finally {
            lock.unlock();
        }
    }

结束

卧槽有点累了,这节分个上下吧,CopyOnWriteMap下节讲。

你可能感兴趣的:(hadoop)