Kafka Producer内存池设计

925 阅读4分钟

Kafka为了提高吞吐量,Producer采用批量发送数据的模式。Producer可以通过配置设置整个batch缓冲区的大小以及每一个batch的大小:

buffer.memory=         //默认32MB
batch.size=            //默认16KB

当消息达到把batch大小,就会在buffer中申请一定大小的空间,封装成一个新的batch。然后Producer就会以batch为单位发送数据。
如果按照常规设计,我们只需要在需要申请空间的时候判断缓冲区可用空间是否足够,然后new出新的对象,然后发送完了之后GC自己会释放空间,我们只需要管理缓冲区可用空间大小即可。但是这样有一个缺点,当吞吐量很高的时候,程序需要频繁的new对象,GC释放空间,给程序带来非常大的负载。
而Kafka正是用来处理高吞吐量的,所以Kafka在这里涉及了内存池BufferPool来减少创建对象和GC带来的消耗。

设计思路

BufferPool中通过下面两个变量来维护空间:

long nonPooledAvailableMemory    
Deque<ByteBuffer> free           //可用的byteBuffer空间,每一个ByteBuffer大小就是上面配置的batch.size

整个缓冲区可用的空间 = nonPooledAvailableMemory + free * batch.size
在最开始整个缓冲区中free是空的,所有的内存空间都在nonPooledAvailableMemory中,每要创建一个batch(batch的大小正好是batch.size)就会从nonPooledAvailableMemory获取空间,用完释放空间时,空间不会回收到nonPooledAvailableMemory中,而是将ByteBuffer放到free中。那么当下一次需要创建batch的时候,如果free中有没有使用的ByteBuffer,就直接从free中获取。

而对于需要创建size大于batch.size的batch时,永远都是直接从nonPooledAvailableMemory获取空间,并且释放时放回nonPooledAvailableMemory中。如果nonPooledAvailableMemory不够时,会从free中释放一些ByteBuffer给nonPooledAvailableMemory。

为什么有一些batch size不是配置的size大小呢?
因为有一些消息体本身很大,大小超过batch.size,这些消息每一条会创建一个ProducerBatch。
Kafka Producer单条消息最大默认是1MB。
对于这些消息,Kafka Producer其实相当于是一条一条发送消息,并且在BufferPool中并没有很好的利用ByteBuffer,所以他们会影响Kafka Producer的吞吐量的。
所以在实际的生产环境中要根据消息的大小调整batch.size的大小

如果nonPooledAvailableMemory + free * batch.size的大小也不够创建batch时,程序就会等待别的正在使用的batch释放空间,这个block时间默认是1min。

代码

释放空间

public void deallocate(ByteBuffer buffer, int size) {
    lock.lock();
    try {
        if (size == this.poolableSize && size == buffer.capacity()) {
            //batch.size大小的资源直接放到free中
            buffer.clear();
            this.free.add(buffer);
        } else {
            //不是batch.size大小的资源放到nonPooledAvailableMemory中
            this.nonPooledAvailableMemory += size;
        }
        Condition moreMem = this.waiters.peekFirst();
        if (moreMem != null)
            moreMem.signal();
    } finally {
        lock.unlock();
    }
}

申请空间

public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
	... ...

    try {
        //如何申请的资源正好是BATCH.SIZE的大小,并且free中有没有使用的ByteBuffer,直接使用
        if (size == poolableSize && !this.free.isEmpty())
            return this.free.pollFirst();
            
        int freeListSize = freeSize() * this.poolableSize;
        //总的可以使用的内存大小 = this.nonPooledAvailableMemory + freeListSize
        if (this.nonPooledAvailableMemory + freeListSize >= size) {
            //nonPooledAvailableMemory内存不够用时,释放掉一些free中的byteBuffer,知道够size用
            freeUp(size);
            this.nonPooledAvailableMemory -= size;
        } else {
            //资源不够用的时候,等待资源
            int accumulated = 0;
            Condition moreMemory = this.lock.newCondition();
            try {
                long remainingTimeToBlockNs = TimeUnit.MILLISECONDS.toNanos(maxTimeToBlockMs);
                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;
                    //因为上面其他batch释放了资源,所以在此尝试获取资源
                    if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
                        //如果是batch.size,并且释放的资源使得free部位空时,就从free中获取byteBuffer直接使用
                        buffer = this.free.pollFirst();
                        accumulated = size;
                    } else {
                        //nonPooledAvailableMemory内存不够用时,释放掉一些free中的byteBuffer给nonPooledAvailableMemory,知道够size用
                        freeUp(size - accumulated);
                        int got = (int) Math.min(size - accumulated, this.nonPooledAvailableMemory);
                        this.nonPooledAvailableMemory -= got;
                        accumulated += got;
                    }
                }
                accumulated = 0;
            } finally {
                //没有成功获取到资源,把获取的一部分资源交还给nonPooledAvailableMemory
                this.nonPooledAvailableMemory += accumulated;
                this.waiters.remove(moreMemory);
            }
        }
    } finally {
        try {
            //有内存资源剩余的时候,通知的资源等候着
            if (!(this.nonPooledAvailableMemory == 0 && this.free.isEmpty()) && !this.waiters.isEmpty())
                this.waiters.peekFirst().signal();
        } finally {
            lock.unlock();
        }
    }

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

private void freeUp(int size) {
    //nonPooledAvailableMemory内存不够用时,释放掉一些free中的byteBuffer,直到够size用
    while (!this.free.isEmpty() && this.nonPooledAvailableMemory < size)
        this.nonPooledAvailableMemory += this.free.pollLast().capacity();
}