Jusfr 原创,转载请注明来自博客园

Request 与 Response 的响应格式

Request 与 Response 都是以 长度+内容 形式描述, 见于 A Guide To The Kafka Protocol

Request 除了 Size+ApiKey+ApiVersion+CorrelationId+ClientId 这些固定字段, 额外的 RequestMessage 包含了具体请求数据;

Request => Size ApiKey ApiVersion CorrelationId ClientId RequestMessage
Size => int32
ApiKey => int16
ApiVersion => int16
CorrelationId => int32
ClientId => string
RequestMessage => MetadataRequest | ProduceRequest | FetchRequest | OffsetRequest | OffsetCommitRequest | OffsetFetchRequest

Response 除了 Size+CorrelationId, 额外的 ResponseMessage 包含了具体响应数据;

Response => Size CorrelationId ResponseMessage
Size => int32
CorrelationId => int32
ResponseMessage => MetadataResponse | ProduceResponse | FetchResponse | OffsetResponse | OffsetCommitResponse | OffsetFetchResponse

处理序列化与反序列化需求

使用 MemoryStream

序列化 Request 需要分配内存, 从缓冲区读取 Response 同理.

MemoryStream 是一个可靠方案, 它实现了自动扩容, 但扩容过程离不开字节拷贝, 而频繁分配不小的内存将影响性能, 近似的扩容示例代码如下:

// init
Byte[] buffer = new Byte[4096];
Int32 offset = 0; //write bytes

Byte[] bytePrepareCopy = // from outside

if (bytePrepareCopy > buffer.Length - offset) {

Byte[] newBuffer = new Byte[buffer.Length * 2];

Array.Copy(buffer, 0, newBuffer, 0, offset);

buffer = newBuffer;

}

Array.Copy(bytePrepareCopy, 0, buffer, offset, bytePrepareCopy.Length);

数组扩容可以参见 List 的实现, 这里只是示意, 没有处理长度为 (buffer.Length*2 - offset) < bytePrepareCopy.Length 的情况

在数组长度超4k 时,扩容成本非常高。如果约定“请求和响应不得超过4k“, 那么使用可回收(见下文相关内容)的固定长度的数组模拟 MemoryStream 的读取和写入行为, 能够达到极大的性能收益。

KafkaStreamBinary (见于 github) 内部使用 MemoryStream, KafkaFixedBinary (见于 github) 则是基于数组的实现;

使用 BufferManager

使用过 Memcached 的人很容易理解 BufferManager 的思路: 为了降低频繁开辟内存带来的开销,首先“将内存块化”, 申请者获取到“成块的内存”, 被分配出去的内存块标记为“已分配”; 与 Memcached 不同的是 BufferManager 期望申请者归还使用完后的内存块,以重新分配给其他申请操作。

System.ServiceModel.Channels.BufferManager 提供了一个可靠实现, 大致使用方式如下:

const Int32 size = 4096;
BufferManager bm = BufferManager.CreateBufferManager(maxBufferPoolSize: size * 32, maxBufferSize: size);
Byte[] buffer = bm.TakeBuffer(1024);
bm.ReturnBuffer(buffer);

与手动分配内容的性能对比

const Int32 size = 4096;
BufferManager bm = BufferManager.CreateBufferManager(maxBufferPoolSize: size * 10, maxBufferSize: size); var timer = new FunctionTimer();

timer.Push("BufferManager", () => {

Byte[] buffer = bm.TakeBuffer(size);

bm.ReturnBuffer(buffer);

}); timer.Push("new Byte[]", () => {

Byte[] buffer = new Byte[size];

}); timer.Initialize();

timer.Execute(100000).Print();

测试结果:

BufferManager
Time Elapsed : 7ms
CPU Cycles : 17,055,523
Memory cost : 3,388
Gen 0 : 2
Gen 1 : 2
Gen 2 : 2
new Byte[]
Time Elapsed : 42ms
CPU Cycles : 113,437,539
Memory cost : 24
Gen 0 : 263
Gen 1 : 2
Gen 2 : 2
  • 过小的内容使用没有使用 BufferManager 的必要,但BufferManager分配超过 4k 内存时性能下降明显;
  • 最优情况是申请人获取的内存块大小一致,如果设置maxBufferSize = 4k,但 TakeBuffer(Int32 bufferSize) 方法使用的参数大于 4k,测试表明性能还不如手动创建 Byte 数组;
  • mono 的实现存在线程安全的问题;

强制要求业务使用的请求不超过4k 貌似做得到,但需求更大内存的场景总是存在,比如合并消息、批量消费等,Chuye.Kafka 作为类库需要提供支持。

KafkaScalableBinary = BufferManager + Byte[][]

KafkaScalableBinary 并没有发明新东西, 在其内部维护了一个 Dictionary<int32, byte[]=""> 保存一系列 Byte数组;

初始化时并未真正分配内存, 除非开始写入;

public KafkaScalableBinary()
: this(4096) {
} public KafkaScalableBinary(Int32 size) {

if (size <= 0) {

throw new ArgumentOutOfRangeException("size");

}

_lengthPerArray = size;

_buffers = new Dictionary<Int32, Byte[]>(16);

}

写入时先根据当前位置对数组长度取模 _position / _lengthPerArray 找到待写入数组,不存在则分配新数组;

private Byte[] GetBufferForWrite() {
var index = (Int32)(_position / _lengthPerArray);
Byte[] buffer;
if (!_buffers.TryGetValue(index, out buffer)) {
if (_lengthPerArray >= 128) {
buffer = ServiceProvider.BufferManager.TakeBuffer(_lengthPerArray);
}
else {
buffer = new Byte[_lengthPerArray];
}
_buffers.Add(index, buffer);
}
return buffer;
}

然后根据当前位置对数组长度取整 _position % _lengthPerArray 找到目标位置;由于待写入长度可能超过可使用长度,这里使用了 while 循环,一边获取和分配待写入数组, 一边将剩余字节写入其中,直至完成;

public override void WriteByte(Byte[] buffer, int offset, int count) {
if (buffer == null) {
throw new ArgumentNullException("buffer");
}
if (buffer.Length == 0) {
return;
}
if (buffer.Length < count) {
throw new ArgumentOutOfRangeException();
}
checked {
var left = count; <span class="co"><span class="hljs-comment"><span class="hljs-comment">//标记剩余量</span>
<span class="kw"><span class="hljs-keyword"><span class="hljs-keyword">while</span> (left &gt; <span class="dv"><span class="hljs-number">0</span>) {
var targetBuffer = GetBufferForWrite(); <span class="co"><span class="hljs-comment"><span class="hljs-comment">//查找目标数组</span>
var targetOffset = (Int32)(_position % _lengthPerArray); <span class="co"><span class="hljs-comment"><span class="hljs-comment">//查找目标位置</span>
<span class="kw"><span class="hljs-keyword"><span class="hljs-keyword">if</span> (targetOffset == _lengthPerArray - <span class="dv"><span class="hljs-number">1</span>) { <span class="co"><span class="hljs-comment"><span class="hljs-comment">//如果位置已经位于数组末尾, 说明位于起始位置;</span>
targetOffset = <span class="dv"><span class="hljs-number">0</span>;
} var prepareCopy = left; <span class="co"><span class="hljs-comment"><span class="hljs-comment">//准备写入剩余量</span>
<span class="kw"><span class="hljs-keyword"><span class="hljs-keyword">if</span> (prepareCopy &gt; _lengthPerArray - targetOffset) { <span class="co"><span class="hljs-comment"><span class="hljs-comment">//但数组的剩余长度可能不够,写入较小长度</span>
prepareCopy = _lengthPerArray - targetOffset;
}
Array.Copy(buffer, count - left, targetBuffer, targetOffset, prepareCopy); <span class="co"><span class="hljs-comment"><span class="hljs-comment">//拷贝字节</span>
_position += prepareCopy; <span class="co"><span class="hljs-comment"><span class="hljs-comment">//推进位置</span>
left -= prepareCopy; <span class="co"><span class="hljs-comment"><span class="hljs-comment">//减小剩余量</span>
<span class="kw"><span class="hljs-keyword"><span class="hljs-keyword">if</span> (_position &gt; _length) { <span class="co"><span class="hljs-comment"><span class="hljs-comment">//增大总长度</span>
_length = _position;
}
}
}

}

读取过程类似,循环查找待读取数组和拷贝字节直到完成,不同的是分配内存的逻辑以一条异常替代;

public override Int32 ReadBytes(Byte[] buffer, int offset, int count) {
if (buffer == null) {
throw new ArgumentNullException("buffer");
}
if (buffer.Length == 0) {
return 0;
}
if (buffer.Length < count) {
throw new ArgumentOutOfRangeException();
}
checked {
var prepareRead = (Int32)(Math.Min(count, _length - _position)); //计算待读取长度
var left = prepareRead; //标记剩余量
while (left > 0) {
var targetBuffer = GetBufferForRead(); //查找目标数组
var targetOffset = (Int32)(_position % _lengthPerArray); //查找目标位置
var prepareCopy = left; //准备读取剩余量
if (prepareCopy > _lengthPerArray - targetOffset) {
prepareCopy = _lengthPerArray - targetOffset;
}
Array.Copy(targetBuffer, targetOffset, buffer, prepareRead - left, prepareCopy); //但数组的剩余长度可能不够,读取较小长度
_position += prepareCopy; //推进位置
left -= prepareCopy; //减小剩余量
}
return prepareRead;
}
} private Byte[] GetBufferForRead() {

var index = (Int32)(_position / _lengthPerArray);

Byte[] buffer;

if (!_buffers.TryGetValue(index, out buffer)) {

throw new IndexOutOfRangeException();

}

return buffer;

}

释放时释放内部维护的的全部字节;

public override void Dispose() {
foreach (var item in _buffers) {
if (_lengthPerArray >= 128) {
ServiceProvider.BufferManager.ReturnBuffer(item.Value);
}
}
_buffers.Clear();
}

写入缓冲区是对内部维护数组列表的直接操作,高度优化

public override void CopyTo(Stream destination) {
foreach (var item in GetBufferAndSize()) {
destination.Write(item.Key, 0, item.Value);
}
}

读取缓冲区时和写入行为类似

public override void ReadFrom(Stream source, int count) {
var left = count;
var loop = 0;
do {
var targetBuffer = GetBufferForWrite();
var targetOffset = (Int32)(_position % _lengthPerArray);
var prepareCopy = left;
if (prepareCopy > _lengthPerArray - targetOffset) {
prepareCopy = _lengthPerArray - targetOffset;
}
    var readed = source.Read(targetBuffer, targetOffset, prepareCopy);
_position += readed;
left -= readed;
<span class="kw"><span class="hljs-keyword"><span class="hljs-keyword">if</span> (_position &gt; _length) {
_length = _position;
}
loop++;
} <span class="kw"><span class="hljs-keyword"><span class="hljs-keyword">while</span> (left &gt; <span class="dv"><span class="hljs-number">0</span>);

}

实际上可以从 MemoryStream 定义出 ScalableMemoryStream 再改写其行为,KafkaScalableBinary 依赖于 MemoryStream 而不是具体实现,整体就更加"设计模式"了 , 基本逻辑前文已陈述。

测试过程中发现,一来 **mono 的 BufferManager 实现存在线程安全问题*,故 Chuye.Kafka 提供了一个 ObjectPool 模式的 BufferManager 作为替代方案; 二是 KafkaScalableBinary 与 ScalableStreamBinary 的性能对比测试结果非常不稳定,但前者频繁的取横取整及字典开销必然是拖累,我会继续追踪和优化。

KafkaScalableBinary (见于 github), 序列化部分设计示意:


Jusfr 原创,转载请注明来自博客园

面试官:Kafka 如何优化内存缓冲机制造成的频繁 GC 问题?的更多相关文章

  1. 面试官,Java8 JVM内存结构变了,永久代到元空间

    在文章<JVM之内存结构详解>中我们描述了Java7以前的JVM内存结构,但在Java8和以后版本中JVM的内存结构慢慢发生了变化.作为面试官如果你还不知道,那么面试过程中是不是有些露怯? ...

  2. 面试官问我JVM内存结构,我真的是

    面试官:今天来聊聊JVM的内存结构吧? 候选者:嗯,好的 候选者:前几次面试的时候也提到了:class文件会被类加载器装载至JVM中,并且JVM会负责程序「运行时」的「内存管理」 候选者:而JVM的内 ...

  3. 面试官:聊一聊SpringBoot服务监控机制

    目录 前言 SpringBoot 监控 HTTP Endpoints 监控 内置端点 health 端点 loggers 端点 metrics 端点 自定义监控端点 自定义监控端点常用注解 来,一起写 ...

  4. Kafka客户端内存缓冲GC处理机制--客户端内存

    1.Kafka的客户端缓冲机制 首先,先得给大家明确一个事情,那就是在客户端发送消息给kafka服务器的时候,一定是有一个内存缓冲机制的. 也就是说,消息会先写入一个内存缓冲中,然后多条消息组成了一个 ...

  5. 【对线面试官】Kafka基础入门

    <对线面试官>系列目前已经连载33篇啦,这是一个讲人话面试系列 [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 &am ...

  6. 面试官,不要再问我“Java虚拟机类加载机制”了

    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...

  7. 面试官,不要再问我“Java虚拟机类加载机制”了(转载)

    关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...

  8. 面试官:怎么做JDK8的内存调优?

    面试官:怎么做JDK8的内存调优? 看着面试官真诚的眼神,心中暗想看起来年纪轻轻却提出如此直击灵魂的问题.擦了擦额头上汗,我稍微调整了一下紧张的情绪,对面试官说: 在内存调优之前,需要先了解JDK8的 ...

  9. 面试官:Netty心跳检测机制是什么,怎么自定义检测间隔时间?

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 书接上回,昨天在地里干了一天的 ...

随机推荐

  1. SVN的工作机制

    一.C/S结构 二.基本操作 1. 检出(Checkout)  把服务器端版本库内容完整下载到本地. 2. 更新(Update)  把服务器端相对于本地的新的修改下载到本地. 3. 提交(Comm ...

  2. k8s Ipvs 内部网络自动分配和内部网络一致ip地址,导致ip冲突

    [链接]Linux负载均衡--LVS(IPVS)https://www.jianshu.com/p/36880b085265 [链接]kube-proxyipvsmodecreateunnecessa ...

  3. OpenResty之指令与常用API

    1. 指令 通过 Lua 编写 Nginx 脚本的基本构建块是指令.指令常用于指定 Lua 代码是几时执行的以及如何使用运行的结果.下图展示了指令执行的顺序. lua_capture_error_lo ...

  4. 最新react-native(Expo)安装使用antd-mobile-rn组件库

    1\安装antd-mobile-rn 库 npm install antd-mobile-rn --save 2.按需加载 npm install babel-plugin-import --save ...

  5. MongoDB作为windows服务来安装-2

    首先区官网下载对应版本的安装文件,我本地的环境是win7 bit64 我下载的版本是:mongodb-win32-x86_64-2.4.6 ok, 文件下载后,开始安装,这里要说一下,如果直接启动Mo ...

  6. LVS+Keepalived小试牛刀

    一.ipvsadm命令1.基本命令操作1.1)添加规则 ipvsadm -A|E -t|u|f service-address [-s scheduler] [-p timeout] [-M netm ...

  7. ES6重要点学习

    1.解析赋值: let [a,b,c,d,e] = '我是中国人' //我 a = wo let [a,b,c] = 123 // 出错,Numberbu不允许解析不允许解析 2.数据集合Set, m ...

  8. 【leetcode】521. Longest Uncommon Subsequence I

    problem 521. Longest Uncommon Subsequence I 最长非共同子序列之一 题意: 两个字符串的情况很少,如果两个字符串相等,那么一定没有非共同子序列,反之,如果两个 ...

  9. Ocelot 配置参数

    Downstream是下游服务配置 UpStream是上游服务配置 Aggregates 服务聚合配置 ServiceName, LoadBalancer, UseServiceDiscovery 配 ...

  10. Course1_Week1_ProgrammingHomeWork

    Exercise 1: Pascal's Triangle The following pattern of numbers is called Pascal's triangle. 1 1 1 1 ...