• 面试官:Kafka 如何优化内存缓冲机制造成的频繁 GC 问题?


    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 原创,转载请注明来自博客园

  • 相关阅读:
    【Uvalive4960】 Sensor network (苗条树,进化版)
    【UVA 1151】 Buy or Build (有某些特别的东东的最小生成树)
    【UVA 1395】 Slim Span (苗条树)
    【UVA 10600】 ACM Contest and Blackout(最小生成树和次小生成树)
    【UVA 10369】 Arctic Network (最小生成树)
    【UVA 10816】 Travel in Desert (最小瓶颈树+最短路)
    【UVA 11183】 Teen Girl Squad (定根MDST)
    【UVA 11865】 Stream My Contest (二分+MDST最小树形图)
    【UVA 11354】 Bond (最小瓶颈生成树、树上倍增)
    【LA 5713 】 Qin Shi Huang's National Road System (MST)
  • 原文地址:https://www.cnblogs.com/jpfss/p/11790465.html
Copyright © 2020-2023  润新知