Kafka 协议实现中的内存优化
Kafka 协议实现中的内存优化
Kafka 协议实现中的内存优化
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; //标记剩余量
while (left > 0) {
var targetBuffer = GetBufferForWrite(); //查找目标数组
var targetOffset = (Int32)(_position % _lengthPerArray); //查找目标位置
if (targetOffset == _lengthPerArray - 1) { //如果位置已经位于数组末尾, 说明位于起始位置;
targetOffset = 0;
}
var prepareCopy = left; //准备写入剩余量
if (prepareCopy > _lengthPerArray - targetOffset) { //但数组的剩余长度可能不够,写入较小长度
prepareCopy = _lengthPerArray - targetOffset;
}
Array.Copy(buffer, count - left, targetBuffer, targetOffset, prepareCopy); //拷贝字节
_position += prepareCopy; //推进位置
left -= prepareCopy; //减小剩余量
if (_position > _length) { //增大总长度
_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;
if (_position > _length) {
_length = _position;
}
loop++;
} while (left > 0);
}
实际上可以从 MemoryStream 定义出 ScalableMemoryStream 再改写其行为,KafkaScalableBinary 依赖于 MemoryStream 而不是具体实现,整体就更加"设计模式"了 , 基本逻辑前文已陈述。
测试过程中发现,一来 **mono 的 BufferManager 实现存在线程安全问题*,故 Chuye.Kafka 提供了一个 ObjectPool 模式的 BufferManager 作为替代方案; 二是 KafkaScalableBinary 与 ScalableStreamBinary 的性能对比测试结果非常不稳定,但前者频繁的取横取整及字典开销必然是拖累,我会继续追踪和优化。
KafkaScalableBinary (见于 github), 序列化部分设计示意:

Jusfr 原创,转载请注明来自博客园
Kafka 协议实现中的内存优化的更多相关文章
- Kafka 协议实现中的内存优化【转】
Kafka 协议实现中的内存优化 Jusfr 原创,转载请注明来自博客园 Request 与 Response 的响应格式 Request 与 Response 都是以 长度+内容 形式描述, 见 ...
- pyhon中的内存优化机制
一.变量的内存地址 python中变量的内存地址可以用id()来查看 >>> a = " >>> id(a) 2502558915696 二.pyhon中 ...
- Java虚拟机内存优化实践
前面一篇文章介绍了Java虚拟机的体系结构和内存模型,既然提到内存,就不得不说到内存泄露.众所周知,Java是从C++的基础上发展而来的,而C++程序的很大的一个问题就是内存泄露难以解决,尽管Java ...
- SQLServer 2014 内存优化表
内存优化表是 SQLServer 2014 的新功能,它是可以将表放在内存中,这会明显提升DML性能.关于内存优化表,更多可参考两位大侠的文章:SQL Server 2014新特性探秘(1)-内存数据 ...
- Android性能优化:手把手带你全面实现内存优化
前言 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用.空间占用 2. 作用 避免因不正确使用内 ...
- Redis系列--内存淘汰机制(含单机版内存优化建议)
https://blog.csdn.net/Jack__Frost/article/details/72478400?locationNum=13&fps=1 每台redis的服务器的内存都是 ...
- ANDROID内存优化(大汇总——中)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...
- Android 性能优化之内存泄漏检测以及内存优化(中)
https://blog.csdn.net/self_study/article/details/66969064 上篇博客我们写到了 Java/Android 内存的分配以及相关 GC 的详细分析, ...
- Android内存优化大全(中)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...
随机推荐
- 经典排序算法 - 基数排序Radix sort
经典排序算法 - 基数排序Radix sort 原理类似桶排序,这里总是须要10个桶,多次使用 首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,临时忽视十位数 比如 待排序数组[ ...
- ASP.NET - 匹配标签中的内容
string str = @"<td>Csdn</td>\r\n<td>V1.0</td>\r\n<td>2014-10-23&l ...
- axure制作圆形组件——axure制作技巧
Axure本身是没有直接提供圆形组件的,所以很多朋友在微博上问,如何使用axure制作圆形,难道都要找美工-- Axure没有提供圆形组件,但是它提供了一个万能组件--矩形组件,只要有矩形组件,我们就 ...
- SqlServer中的数据类型UniqueIdentifier
SqlServer中的数据类型UniqueIdentifier究竟是什么东东? 该类型一般用来做为主键使用,可用SQL语法的newid()来生成一个唯一的值.我想请问的是,这个值是一个长整型的数据值呢 ...
- smartforms客制页格式
smartforms需要打印非A4纸时需要手动创建页格式:创建事务代码:spad, 操作的时候需要 把所有步骤都做一遍方可正常打印,不然只能停留在预览界面无法打印.
- JavaScript常用全局属性与方法
最近,在学习JavaScript,Java作域链包含全局,记录下常用的全局属性与方法,就当是知识的积累,未列出全部,如需查看全部可参考JS相关的API文档. 常用的全局属性: 全局属性 作 ...
- 《C++语言基础》实践參考——数组作数据成员
返回:贺老师课程教学链接 [项目5 - 数组作数据成员]阅读教材P255例8.4.注意到类中的数据成员能够是数组.设计一个工资类(Salary),当中类的数据成员例如以下: class Salary ...
- WTL介绍
WTL简单介绍 关键词: WTL WTL是一个好东东.它开发的程序都很短小精悍.对开发WIN32的应用有很好的优点.它不用MFC开发.但可以高速产生窗体和控件. 以文本方式查看主题 - 温馨小筑 ...
- Swift - 给项目导入资源
如果想添加资源到项目中去,只要通过鼠标左键将文件或者存有文件的文件夹直接拖到Xcode中. 当松开鼠标后会弹出如下面板: (1)勾上“Copy items if needed”就会拷贝文件进项目, ...
- 查找MobileSafari WebKit revision number的方法
Mobile Safari是开源的Mac Safari的iOS版本,然而iOS WebKit并不完全开源,只公开了部分的WebCore和JavaScriptCore.有时需要知道iOS Safari的 ...