1 概述

kafka producer调用RecordAccumulator#append来将消息存到本地内存。消息以TopicPartition为key分组存放,每个TopicPartition对应一个Deque;RcordBatch的消息实际存储在MemoryRecords中;MemoryRecords有Compressor和ByteBuffer两个主要的属性,消息就是存储在ByteBuffer中,Compressor用来将消息写进到ByteBuffer中。消息在生产内存中的模型大致如下:

2 RecordAccumulator#append

producer调用send方法的时候,调用RecordAccumulator#append将消息存放到内存中。这里需要注意的是,append获取了两次锁,这样做是为了减少锁的范围。

  1. public RecordAppendResult append(TopicPartition tp,
  2. long timestamp,
  3. byte[] key,
  4. byte[] value,
  5. Callback callback,
  6. long maxTimeToBlock) throws InterruptedException {
  7. appendsInProgress.incrementAndGet();
  8. try {
  9. Deque<RecordBatch> dq = getOrCreateDeque(tp); // 获取tp对应的Deque<RecordBatch>
  10. synchronized (dq) { // 关键, 获取Deque<RecordBatch>的锁才操作
  11. if (closed)
  12. throw new IllegalStateException("Cannot send after the producer is closed.");
  13. Deque<RecordBatch> dq = getOrCreateDeque(tp); // 获取tp对应的Deque<RecordBatch>
  14. RecordBatch last = dq.peekLast(); // 往最后一个RecordBatch添加
  15. if (last != null) { // 尝试添加,后面会详细讲
  16. FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
  17. if (future != null) //添加成功就返回了
  18. return new RecordAppendResult(future, dq.size() > 1 || last.records.isFull(), false);
  19. }
  20. }
  21. // 没有添加成功,说明最后一个RecordBatch空间不足或者last == null
  22. // 关键, 如果消息体大于batchsize,那么会创建消息体大小的RecordBatch,即RecordBatch不一定和batchsize相等
  23. int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
  24. // 从BufferPool中分配内存,后面会详细讲
  25. ByteBuffer buffer = free.allocate(size, maxTimeToBlock);
  26. synchronized (dq) { // 重新获取锁,因为allocate的时候不需要锁dq,这里也是尽量减少锁粒度的一种思想
  27. if (closed)
  28. throw new IllegalStateException("Cannot send after the producer is closed.");
  29. RecordBatch last = dq.peekLast();
  30. if (last != null) { // 可能在重新获取锁之前其他线程释放了内存,所以这里重新获取下
  31. FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
  32. if (future != null) {
  33. free.deallocate(buffer);
  34. return new RecordAppendResult(future, dq.size() > 1 || last.records.isFull(), false);
  35. }
  36. }
  37. // 还没有获取到RecordBatch则申请内存创建新的RecordBatch
  38. MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
  39. RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
  40. FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds()));
  41. dq.addLast(batch);
  42. incomplete.add(batch);
  43. return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
  44. }
  45. } finally {
  46. appendsInProgress.decrementAndGet();
  47. }
  48. }

3 Compressor

上述代码调用RecordBatch#tryAppend尝试将消息放到RecordBatch,而RecordBatch#tryAppend又调用MemoryRecords#append。

  1. public long append(long offset, long timestamp, byte[] key, byte[] value) {
  2. if (!writable)
  3. throw new IllegalStateException("Memory records is not writable");
  4. int size = Record.recordSize(key, value);
  5. compressor.putLong(offset);
  6. compressor.putInt(size);
  7. long crc = compressor.putRecord(timestamp, key, value);
  8. compressor.recordWritten(size + Records.LOG_OVERHEAD);
  9. return crc;
  10. }

这里的关键是compressor,来分析下Compressor,以putInt为例,实际上是调用了DataOutputStream#writeInt方法

  1. public void putInt(final int value) {
  2. try {
  3. appendStream.writeInt(value); // appendStream是DataOutputStream类型
  4. } catch (IOException e) {
  5. throw new KafkaException("I/O exception when writing to the append stream, closing", e);
  6. }
  7. }

看下Compressor是如何初始化的:

  1. public Compressor(ByteBuffer buffer, CompressionType type) {
  2. // ...
  3. // create the stream
  4. bufferStream = new ByteBufferOutputStream(buffer);
  5. appendStream = wrapForOutput(bufferStream, type, COMPRESSION_DEFAULT_BUFFER_SIZE);
  6. }
  7. static public DataOutputStream wrapForOutput(ByteBufferOutputStream buffer, CompressionType type, int bufferSize) {
  8. try {
  9. switch (type) {
  10. case NONE:
  11. return new DataOutputStream(buffer); // 封装了ByteBufferOutputStream
  12. case GZIP:
  13. return new DataOutputStream(new GZIPOutputStream(buffer, bufferSize));
  14. case SNAPPY:
  15. try {
  16. OutputStream stream = (OutputStream) snappyOutputStreamSupplier.get().newInstance(buffer, bufferSize);
  17. return new DataOutputStream(stream);
  18. } catch (Exception e) {
  19. throw new KafkaException(e);
  20. }
  21. case LZ4:
  22. try {
  23. OutputStream stream = (OutputStream) lz4OutputStreamSupplier.get().newInstance(buffer);
  24. return new DataOutputStream(stream);
  25. } catch (Exception e) {
  26. throw new KafkaException(e);
  27. }
  28. default:
  29. throw new IllegalArgumentException("Unknown compression type: " + type);
  30. }
  31. } catch (IOException e) {
  32. throw new KafkaException(e);
  33. }
  34. }

从上面代码可以看到,和ByteBuffer直接关联的是ByteBufferOutputStream;而DataOutputStream封装了ByteBufferOutputStream,负责处理压缩数据,直观上来看如下图:

4 BufferPool

BufferPool用于管理producer缓存池,使用配置项buffer.memory来指定缓存池的大小,默认是32M。

4.1 allocate

BufferPool#allocate用于从缓存池中申请内存。BufferPool维护了一个ByteBuffer的双端队列free,表示空闲的ByteBuffer,只有大小为batch.size的内存申请才会从free中去拿去,也就是说free中维护的ByteBuffer都是batch.size大小。

BufferPool几个关键属性

  1. private final long totalMemory;
  2. private final int poolableSize; // 一块连续内存的大小,等于batch.size
  3. private final ReentrantLock lock;
  4. private final Deque<ByteBuffer> free; // 空闲的ByteBuffer列表,每个ByteBuffer都是batch.size大小,只有申请的内存等于batch.size大小才会从free中获取
  5. private final Deque<Condition> waiters;
  6. private long availableMemory; // 还有多少内存可以用,即buffer.memory-已用内存
  7. // ...
  8. }
  1. public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
  2. if (size > this.totalMemory)
  3. throw new IllegalArgumentException("Attempt to allocate " + size
  4. + " bytes, but there is a hard limit of "
  5. + this.totalMemory
  6. + " on memory allocations.");
  7. this.lock.lock();
  8. try {
  9. if (size == poolableSize && !this.free.isEmpty()) // 关键,只有大小等于batch.size的时候才会从free中获取
  10. return this.free.pollFirst();
  11. int freeListSize = this.free.size() * this.poolableSize;
  12. // 剩余总内存够用,但是不能从free中获取,则将free释放一些,然后申请对应大小的内存
  13. if (this.availableMemory + freeListSize >= size) {
  14. freeUp(size); // 释放
  15. this.availableMemory -= size;
  16. lock.unlock();
  17. return ByteBuffer.allocate(size);
  18. } else {
  19. // 关键,剩余总内存不够了,则会阻塞,直到有足够的内存
  20. int accumulated = 0;
  21. ByteBuffer buffer = null;
  22. Condition moreMemory = this.lock.newCondition();
  23. long remainingTimeToBlockNs = TimeUnit.MILLISECONDS.toNanos(maxTimeToBlockMs);
  24. this.waiters.addLast(moreMemory); // 添加到等待队列尾
  25. while (accumulated < size) {
  26. long startWaitNs = time.nanoseconds();
  27. long timeNs;
  28. boolean waitingTimeElapsed;
  29. try {
  30. waitingTimeElapsed = !moreMemory.await(remainingTimeToBlockNs, TimeUnit.NANOSECONDS); // 阻塞
  31. } catch (InterruptedException e) {
  32. this.waiters.remove(moreMemory);
  33. throw e;
  34. } finally {
  35. long endWaitNs = time.nanoseconds();
  36. timeNs = Math.max(0L, endWaitNs - startWaitNs);
  37. this.waitTime.record(timeNs, time.milliseconds());
  38. }
  39. if (waitingTimeElapsed) {
  40. this.waiters.remove(moreMemory);
  41. throw new TimeoutException("Failed to allocate memory within the configured max blocking time " + maxTimeToBlockMs + " ms.");
  42. }
  43. remainingTimeToBlockNs -= timeNs;
  44. // check if we can satisfy this request from the free list,
  45. // otherwise allocate memory
  46. if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
  47. // just grab a buffer from the free list
  48. buffer = this.free.pollFirst();
  49. accumulated = size;
  50. } else {
  51. freeUp(size - accumulated);
  52. int got = (int) Math.min(size - accumulated, this.availableMemory);
  53. this.availableMemory -= got;
  54. accumulated += got;
  55. }
  56. }
  57. Condition removed = this.waiters.removeFirst(); // 从头部获取,后面详细讲
  58. if (removed != moreMemory)
  59. throw new IllegalStateException("Wrong condition: this shouldn't happen.");
  60. // 如果分配后还有剩余空间,即唤醒后续的等待线程
  61. if (this.availableMemory > 0 || !this.free.isEmpty()) {
  62. if (!this.waiters.isEmpty())
  63. this.waiters.peekFirst().signal(); // 唤醒头部
  64. }
  65. // unlock and return the buffer
  66. lock.unlock();
  67. if (buffer == null)
  68. return ByteBuffer.allocate(size);
  69. else
  70. return buffer;
  71. }
  72. } finally {
  73. if (lock.isHeldByCurrentThread())
  74. lock.unlock();
  75. }
  76. }

对于allocate有几点需要注意

  1. 只有大小为batch.size的内存申请才会从free中获取,所以消息大小尽量不要大于batch.size,这样才能充分利用缓存池。为什么申请的内存会不等于batch.size呢,原因是在RecordAccumulator#append中有一句 int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value)), 即如果消息大小大于batch.size则会使用消息的大小申请内存。
  2. 下面代码可能有点疑惑, moreMemory是添加到waiters的尾部的,为什么获取的时候是从头部获取呢?这个原因是,线程唤醒只会唤醒waiters头部的线程,所以当线程被唤醒后,他肯定是已经在waiters头部了,也就是说排在他前面的线程都已经在他之前被唤醒并移除waiters了。
    1. Condition removed = this.waiters.removeFirst(); // 从头部获取,后面详细讲
    2. if (removed != moreMemory)
    3. throw new IllegalStateException("Wrong condition: this shouldn't happen.");
  3. 申请的总内存查过buffer.memory的时候会阻塞或者抛出异常

4.2 deallocate

BufferPool#deallocate用于将内存释放并放回到缓存池。同allocate一样,只有大小等于batch.size的内存块才会放到free中。

  1. public void deallocate(ByteBuffer buffer) {
  2. deallocate(buffer, buffer.capacity());
  3. }
  4. public void deallocate(ByteBuffer buffer, int size) {
  5. lock.lock();
  6. try {
  7. if (size == this.poolableSize && size == buffer.capacity()) {
  8. buffer.clear();
  9. this.free.add(buffer);// 只有大小等于batch.size的内存块才会放到free中
  10. } else { // 否则的话只是availableMemory改变,无用的ByteBuffer会被GC清理掉
  11. this.availableMemory += size;
  12. }
  13. Condition moreMem = this.waiters.peekFirst(); // 唤醒waiters的头结点
  14. if (moreMem != null)
  15. moreMem.signal();
  16. } finally {
  17. lock.unlock();
  18. }
  19. }

producer内存管理分析的更多相关文章

  1. Android 内存管理分析(四)

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/8920039 最近在网上看了不少Android内存管理方面的博文,但是文章大多 ...

  2. memcached 内存管理 分析(转)

    Memcached是一个高效的分布式内存cache,了解memcached的内存管理机制,便于我们理解memcached,让我们可以针对我们数据特点进行调优,让其更好的为我所用.这里简单谈一下我对me ...

  3. Android进程的内存管理分析

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/8920039 最近在网上看了不少Android内存管理方面的博文,但是文章大多 ...

  4. iOS 内存管理分析

    内存分析 静态分析(Analyze) 不运行程序, 直接检测代码中是否有潜在的内存问题(不一定百分百准确, 仅仅是提供建议) 结合实际情况来分析, 是否真的有内存问题 动态分析(Profile == ...

  5. cocos 自动内存管理分析

    #include "CCAutoreleasePool.h" #include "ccMacros.h" NS_CC_BEGIN static CCPoolMa ...

  6. spark内存管理分析

    前言 下面的分析基于对spark2.1.0版本的分析,对于1.x的版本可以有区别. 内存配置 key 默认 解释 spark.memory.fraction 0.6 spark可以直接使用的内存大小系 ...

  7. 【转】cocos2d-x与ios内存管理分析(在游戏中减少内存压力)

    猴子原创,欢迎转载.转载请注明: 转载自Cocos2D开发网–Cocos2Dev.com,谢谢! 原文地址: http://www.cocos2dev.com/?p=281 注:自己以前也写过coco ...

  8. cocos2d-x与ios内存管理分析(在游戏中减少内存压力)

    转自:http://www.cocos2dev.com/?p=281 注:自己以前也写过cocos2d-x如何优化内存的使用,以及内存不足的情况下怎么处理游戏.今天在微博中看到有朋友介绍了下内存,挺详 ...

  9. Android内存泄漏分析及调试

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先 ...

随机推荐

  1. idea 自定义toString

    实现功能: 1.自定义json格式 2.字符及时间类型添加null判断 3.时间进行格式化 步骤: 1.alt+insert-----toString---setting----templates 2 ...

  2. Python中类的定制

    1 class Chinese: 2 eye = 'black' 3 4 def eat(self): 5 print('吃饭,选择用筷子.') 6 7 class Guangdong(Chinese ...

  3. python基础之数值类型与序列类型

    Hello大家好,我是python学习者小杨同学,已经学习python有一段时间,今天将之前学习过的内容整理一番,在这与大家分享与交流,现在开始我们的python基础知识之旅吧. 数值类型与序列类型 ...

  4. IDE 、SDK 、API区别、库、框架、组件、CLI

    IDE:集成开发环境:包括代码编辑器.代码检测.代码调试器.译器/解释器.以及其他工具 SDK:SDK是IDE的基础引擎 ,比IDE更基本,因为它通常没有图形工具.工程师为辅助开发某类软件的相关文档. ...

  5. Maven目录结构, war目录结构

    Maven目录结构 src/main/java 存放java servlet类文件 src/main/webapp 存放jsp文件 war目录结构 Maven web项目目录结构

  6. urllib-访问网页的两种方式:GET与POST

    学习自:https://www.jianshu.com/p/4c3e228940c8 使用参数.关键字访问服务器 访问网络的两种方法: 1.GET 利用参数给服务器传递信息 参数data为dict类型 ...

  7. Matplotlib:Python三维绘图

    1.创建三维坐标轴对象Axes3D 创建Axes3D主要有两种方式,一种是利用关键字projection='3d'来实现,另一种是通过从mpl_toolkits.mplot3d导入对象Axes3D来实 ...

  8. Linux的用户与用户组管理

    1.Linux用户与用户组 Linux 是多用户多任务操作系统,Linux 系统支持多个用户在同一时间内登陆,不同用户可以执行不同的任务,并且互不影响.不同用户具有不问的权限,毎个用户在权限允许的范围 ...

  9. SQL从零到迅速精通【实用函数(1)】

    语法是一个编程语言的基础,真的想玩的6得飞起还是要靠自己定义的函数和变量. 1.使用DECLARE语句创建int数据类型的名为@mycounter的局部变量,输入语句如下: DECLARE @myco ...

  10. Navicat v15 破解

    特别注意: 1.断网,否则在安装过程中会失败 2.关闭防火墙及杀毒软件 3.选择对应版本:mysql版就选择mysql 4.如果出现 就卸载,删除注册表,重新安装,出现rsa public key n ...