Kafka生产者介绍(五):BufferPool

Kafka客户端使用BufferPool实现ByteBuffer复用,其包括已用和未用空间,未用空间分已申请未使用和未申请部分。BufferPool针对指定大小ByteBuffer管理,allocate函数用于申请空间。释放空间时线程安全,超大消息多会影响内存池利用,需调整batch.size配置。

BufferPool的创建和释放都是比较消耗资源的,Kafka客户端使用BufferPool来实现ByteBuffer的复用。BufferPool主要包括两个部分,已用空间和未用的空间,总大小由buffer.memory配置。
未用的空间又分为已经申请但是没有使用的(free队列),和没有申请的空间(availableMemory)。它主要字段为:

public final class BufferPool {
	// 整个Pooll的大小
    private final long totalMemory;
	// 指定ByteBuffer的大小
    private final int poolableSize;
	// 多线程并发分配和回收ByteBuffer,用锁控制并发。
    private final ReentrantLock lock;
	// 缓存了指定大小的ByteBuffer对象(poolableSize指定)
    private final Deque<ByteBuffer> free;
	// 记录因申请不到足够空间而阻塞的线程,此队列中实际记录的是阻塞线程对应的Condition对象。
    private final Deque<Condition> waiters;
	// 可用的空间大小,totalMemory减去free列表中所有ButeBuffer的大小。
    private long availableMemory;
    private final Metrics metrics;
    private final Time time;
    private final Sensor waitTime;
}

每个BufferPool对象值针对指定大小的ByteBuffer进行管理(poolableSize指定)。一般情况下,调整MemoryRecord的大小让其缓存多条消息(batch.size)。当一条消息大于MemoryRecord的大小时,就不会复用BufferPool中的ByteBuffer,而是额外分配新的ByteBuffer。用完后由GC直接回收,如果经常出现这种情况,就需要调整batch.size。


BufferPool最重要的函数是allocate申请空间。分配内存的由RecordAccumulator调用,大小制定为Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value)),即batchs.size和实际消息大小中较大的一个,batchs.size为poolableSize。
 

public final class BufferPool {
    public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
        // 消息大小大于totalMemory,直接抛异常
        if (size > this.totalMemory)
            throw new IllegalArgumentException("Attempt to allocate " + size
                                               + " bytes, but there is a hard limit of "
                                               + this.totalMemory
                                               + " on memory allocations.");

        this.lock.lock();
        try {
            //  如果请求的是poolableSize大小的ByteBuffer,且free中有空余的ByteBuffer。
            if (size == poolableSize && !this.free.isEmpty())
                return this.free.pollFirst();

            //  计算出已经分配的free所有空间,加上可用空间的大小,大于size则可以进行分配
            int freeListSize = this.free.size() * this.poolableSize;
            if (this.availableMemory + freeListSize >= size) {
                /*
                    需要availableMemory大于size才进行分配,否者一直循环等待free释放空间。
                    private void freeUp(int size) {
                        while (!this.free.isEmpty() && this.availableMemory < size)
                            this.availableMemory += this.free.pollLast().capacity();
                    }
                */
                freeUp(size);
                // 可以分配
                this.availableMemory -= size;
                lock.unlock();
                // java.nio.ByteBuffer#allocate分配size大小的buffer
                return ByteBuffer.allocate(size);
            } else {
                // 没有足够的空间,只能阻塞直到已经分配的buffer释放了。
                int accumulated = 0;
                ByteBuffer buffer = null;
                Condition moreMemory = this.lock.newCondition();
                long remainingTimeToBlockNs = TimeUnit.MILLISECONDS.toNanos(maxTimeToBlockMs);
                // 把Condition加入到waiters中。
                this.waiters.addLast(moreMemory);
                // 循环等待,直到能分配空间
                while (accumulated < size) {
                    long startWaitNs = time.nanoseconds();
                    long timeNs;
                    boolean waitingTimeElapsed;
                    try {
                        waitingTimeElapsed = !moreMemory.await(remainingTimeToBlockNs, TimeUnit.NANOSECONDS);
                    } catch (InterruptedException e) {
                        this.waiters.remove(moreMemory);
                        throw e;
                    } finally {
                        long endWaitNs = time.nanoseconds();
                        timeNs = Math.max(0L, endWaitNs - startWaitNs);
                        this.waitTime.record(timeNs, time.milliseconds());
                    }

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

                    remainingTimeToBlockNs -= timeNs;
                    // 如果当前累计已经分配内存是0,即还没有分配,且请求是poolableSize大小的ByteBuffer,且free中有,则直接分配。
                    if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
                        buffer = this.free.pollFirst();
                        accumulated = size;
                    } else {
                        // 否者等待size - accumulated的内存释放。
                        freeUp(size - accumulated);
                        int got = (int) Math.min(size - accumulated, this.availableMemory);
                        this.availableMemory -= got;
                        accumulated += got;
                    }
                }

                // 已经成功分配空间,则移除Condition
                Condition removed = this.waiters.removeFirst();
                if (removed != moreMemory)
                    throw new IllegalStateException("Wrong condition: this shouldn't happen.");

                // 如果还有其他空间,则唤醒下一个线程。
                if (this.availableMemory > 0 || !this.free.isEmpty()) {
                    if (!this.waiters.isEmpty())
                        this.waiters.peekFirst().signal();
                }

                // unlock and return the buffer
                lock.unlock();
                if (buffer == null)
                    return ByteBuffer.allocate(size);
                else
                    return buffer;
            }
        } finally {
            if (lock.isHeldByCurrentThread())
                lock.unlock();
        }
    }
}

释放空间:

public final class BufferPool {
    public void deallocate(ByteBuffer buffer, int size) {
        lock.lock();
        try {
            // 如果释放的ByteBuffer大小是poolableSize,放入free中进行管理。
            if (size == this.poolableSize && size == buffer.capacity()) {
                buffer.clear();
                this.free.add(buffer);
            } else {
                // 释放的大小不是poolableSize,则直接用GC回收。
                this.availableMemory += size;
            }
            // 唤醒一个空间不足阻塞的线程。
            Condition moreMem = this.waiters.peekFirst();
            if (moreMem != null)
                moreMem.signal();
        } finally {
            lock.unlock();
        }
    }
}

1. BufferPool是线程安全的,用一个ReentrantLock来保证。
2. BufferPool只针对特定大小的ByteBuffer进行管理,对于其它大小的并不会缓存进来。因此如果超大消息比较多(大于poolableSize),就不会很好的利用内存池,频繁的申请回收内存效率会降低,并可能带来Full GC或者Out Of Memory Error,这个时候就要调整好batch.size的配置了。
 

这个是我的配置信息有没有需要修改的 spring: redis: host: 10.224.206.16 port: 6379 password: Siq2025redisZS datasource: type: com.zaxxer.hikari.HikariDataSource kafka: bootstrap-servers: 10.224.206.16:9092 listener: type: batch producer: acks: 1 batch-size: 16384 retries: 0 buffer-memory: 33554432 key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer consumer: group-id: myGroup enable-auto-commit: true auto-commit-interval: 100 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer shardingsphere: mode: type: Memory repository: type: JDBC overwrite: false datasource: names: enterprise-info,tag,app enterprise-info: type: com.zaxxer.hikari.HikariDataSource driver-class-name: dm.jdbc.driver.DmDriver jdbc-url: jdbc:dm://10.224.206.78:5236/DAMENG?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true username: SYSDBA password: Dm123456 minimum-idle: 5 idle-timeout: 180000 maximum-pool-size: 300 max-lifetime: 0 connection-timeout: 30000 connection-test-query: select 1 connection-init-sql: SET SCHEMA ENTERPRISE_INFO tag: type: com.zaxxer.hikari.HikariDataSource driver-class-name: dm.jdbc.driver.DmDriver jdbc-url: jdbc:dm://10.224.206.78:5236/DAMENG?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true username: SYSDBA password: Dm123456 minimum-idle: 5 idle-timeout: 180000 maximum-pool-size: 300 max-lifetime: 0 connection-timeout: 30000 connection-test-query: select 1 connection-init-sql: SET SCHEMA TAG app: type: com.zaxxer.hikari.HikariDataSource driver-class-name: dm.jdbc.driver.DmDriver jdbc-url: jdbc:dm://10.224.206.78:5236/DAMENG?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true username: SYSDBA password: Dm123456 minimum-idle: 5 idle-timeout: 180000 maximum-pool-size: 300 max-lifetime: 0 connection-timeout: 30000 connection-test-query: select 1 connection-init-sql: SET SCHEMA APP rules: - type: SHARDING tables: enterprise_factdata_daily: actualDataNodes: enterprise-info.enterprise_factdata_daily$->{0..31} tableStrategy: standard: shardingColumn: social_creditcode shardingAlgorithmName: table-sharding-algorithm shardingAlgorithms: table-sharding-algorithm: type: CLASS_BASED props: strategy: standard algorithmClassName: com.tencent.egb.job.configuration.TableShardingAlgorithm props: # 关键配置:禁用数据库元数据检查 check-database-metadata-enabled: false check-table-metadata-enabled: false sql-show: false query-with-cipher-column: true check-duplicate-table-enabled: false proxy-frontend-flush-threshold: 128 sql-comment-parse-enabled: false jpa: database-platform: org.hibernate.dialect.DmDialect show-sql: true hibernate: ddl-auto: validate naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl properties: hibernate: default_schema: ENTERPRISE_INFO dialect: org.hibernate.dialect.DmDialect hibernate.dialect.storage_engine: dm globally_quoted_identifiers: true globally_quoted_identifiers_skip_column_definitions: true jdbc.batch_size: 50 jdbc.fetch_size: 100 jdbc.time_zone: Asia/Shanghai temp.use_jdbc_metadata_defaults: false hibernate.length_force_inline: true hibernate.varchar.maxlength: 8188 format_sql: true use_sql_comments: true
最新发布
08-28
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值