Eine Analyse des Netty-Heap-Speicherverlusts, der durch einen falschen Decodierungsprozess verursacht wurde

Die Ursache des Problems liegt in einem langsamen Speicherverlust, der durch eine Fehlerbehandlung der Online-Tcp-Proxy-Proxy-Logikverarbeitung verursacht wird. Das Phänomen besteht darin, dass der RSS des Prozesses, in dem sich der Netty-Dienst befindet, langsam auf einen Höchststand ansteigt und dann dort bleibt ein Höhepunkt. Laut der Statistik vorhandener Anwendungsweiterleitungsdaten ist die Anzahl der Uplink- und Downlink-Nachrichteninteraktionen pro Tag tatsächlich sehr hoch. Damals war die falsche Vorstellung, dass die Art und Weise, wie Netty den Off-Heap-Speicherpool nutzte, zu einem Anstieg führen würde RSS. Eine falsche Beurteilung führt zu falschen Verarbeitungsergebnissen. Daher ist es notwendig, die wahre Ursache für den RSS-Anstieg zu finden.

1. JVM-Parameter hinzufügen

-XX:NativeMemoryTracking=detail

-Dio.netty.leakDetectionLevel=advanced

(1) Der Parameter NativeMemoryTracking wird verwendet, um die Speichernutzung basierend auf der Speicherberichtsverwaltungsmethode zu verfolgen und den Wert des Speicherwachstums vorher und nachher anzuzeigen

jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory 

对于jvm 内存跟踪的报告详细解释网上有很多这里不再进行重复说明,通过对两次时间点的分析发现Internal区使用内存很大可以判断是由于堆外内存分配导致的,目前只能初略判断是由于堆外内存增长导致的不能确定具体原因。

(2) io.netty.leakDetectionLevel wird verwendet, um den Bericht über Netty-Heap-Speicherlecks zu drucken.

通过开启Netty内存泄漏报告来分析内存泄漏点即使用allocate分配的内存在哪里没有释放会有详细的堆栈信息打印。

Normalerweise kann der Punkt des Netty-Speicherlecks mit den beiden oben genannten Methoden beurteilt werden. Manchmal müssen wir jedoch den spezifischen Speicherinhalt des Off-Heap-Speichers beurteilen, um die Ursache erneut zu analysieren.

2. Verwenden Sie pmap, um Speicherlecks außerhalb des Heaps zu analysieren

pmap分析内存泄漏的方法网上有很多文章介绍了详细的使用教程,这里只说一下分析思路和试用场景,通过基于pmap的分析是基于内存段找到RSS最大的内存段后再使用gdb dump导出最大内存段来分析内存存储内容,这种办法我们对于常规的分析还是有所帮助的能让我们通过关键信息找到RSS最大内存段里面存储的关键信息。如果网络数据包使用的是加密方式传输会无法通过常规的strings查看十六进制内容来分析存储的具体数据。

Die Methode zur Fehlerbehebung bei Speicherlecks ist oben beschrieben. Lassen Sie mich über die Netty-Extra-Heap-Speicherlecks sprechen, die durch die Verwendung falscher Logikverarbeitung verursacht werden.

Protokollformat

Datenpaket Protokollstruktur
Herzschlag 0x00,0x00
Geschäftsdaten Inhaltslänge + PB serialisierter Inhalt

Verwenden Sie ProtobufVarint32FrameDecoder in Netty, um das PB-Protokoll zu verarbeiten (Protokolllänge + serialisierter PB-Inhalt).

Netty-Prozess

graph TD
继承ChannelInboundHandlerAdapter实现心跳过滤 --> ProtobufVarint32FrameDecoder

Dieser scheinbar unproblematische logische Verarbeitungsfehler bei der Verwendung führt zu einem Verlust von Off-Heap-Speicher. Schauen wir uns den Prozess der Vererbung von ChannelInboundHandlerAdapter an

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer=null;
        if(msg instanceof ByteBuf){
            buffer=(ByteBuf) msg;
            int size = buffer.readableBytes();  
            if(size>=2){
                byte b1 = buffer.getByte(0);
                byte b2 = buffer.getByte(1);
                if (b1 == 0x00 && b2 == 0x00) {
                    ByteBuf heartBeat=buffer.readBytes(2);
                    heartBeat.release();       
                    int remSize=buffer.readableBytes();
                    if(remSize>0){
                        super.channelRead(ctx, buffer);
                    }
                    return;
                }
            }else{
                return;
            }
        }
    }

Der obige Vorgang scheint kein Problem zu sein, aber warum steigt der RSS nach heartBeat.release()?

buffer.readBytes() weist tatsächlich einen Teil des Speichers in buf neu zu. Obwohl Release zum Freigeben verwendet wird, wird dieser Teil des Speichers neu zugewiesen. Der ursprüngliche Buffer-Readerindex wurde verschoben, aber die Originaldaten wurden nicht bereinigt.

我们看一下 ByteToMessageDecoder 是如何处理的

  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            selfFiredChannelRead = true;
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                first = cumulation == null;
                cumulation = cumulator.cumulate(ctx.alloc(),
                        first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Exception e) {
                throw new DecoderException(e);
            } finally {
                try {
                    if (cumulation != null && !cumulation.isReadable()) {
                        numReads = 0;
                        try {
                            cumulation.release();
                        } catch (IllegalReferenceCountException e) {
                            //noinspection ThrowFromFinallyBlock
                            throw new IllegalReferenceCountException(
                                    getClass().getSimpleName() + "#decode() might have released its input buffer, " +
                                            "or passed it down the pipeline without a retain() call, " +
                                            "which is not allowed.", e);
                        }
                        cumulation = null;
                    } else if (++numReads >= discardAfterReads) {
                        // We did enough reads already try to discard some bytes, so we not risk to see a OOME.
                        // See https://github.com/netty/netty/issues/4275
                        numReads = 0;
                        discardSomeReadBytes();
                    }

                    int size = out.size();
                    firedChannelRead |= out.insertSinceRecycled();
                    fireChannelRead(ctx, out, size);
                } finally {
                    out.recycle();
                }
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }

这里关键的点在于discardSomeReadBytes();在很多资料中介绍了discardSomeReadBytes()和discardReadBytes()的区别,这里我只简单说一下区别在于性能discardReadBytes对于连续的内存每次都要进行内存压缩而discardSomeReadBytes()处理是根据特定条件做内存压缩,连续的内存压缩需要重新移动数组所以在性能上是有区别的。

当我们使用Netty开发应用时它为我们提供了方便强大的底层支撑,但是我们要对Netty的api进行深入了解才不会在编写代码上出现问题。

Supongo que te gusta

Origin juejin.im/post/7238027797313765413
Recomendado
Clasificación