Une analyse de la fuite de mémoire du tas Netty causée par un mauvais processus de décodage

La cause du problème était une fuite de mémoire lente causée par une erreur de gestion du traitement de la logique du proxy Tcp Proxy en ligne. Le phénomène était que le RSS du processus où se trouvait le service Netty augmentait lentement jusqu'à un point élevé, puis restait à un point haut. Selon les statistiques de données de transfert d'application existantes, le nombre d'interactions de messages de liaison montante et de liaison descendante par jour est en effet très élevé. A cette époque, une idée fausse était que la façon dont Netty utilisait le pool de mémoire hors tas conduirait à une augmentation du RSS . Un mauvais jugement conduira à des résultats de traitement erronés, il est donc nécessaire de trouver la véritable raison de l'augmentation du RSS.

1. Ajouter des paramètres jvm

-XX:NativeMemoryTracking=détail

-Dio.netty.leakDetectionLevel=avancé

(1) Le paramètre NativeMemoryTracking est utilisé pour suivre l'utilisation de la mémoire en fonction de la méthode de gestion des rapports de mémoire pour afficher la valeur de la croissance de la mémoire avant et après

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

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

(2) io.netty.leakDetectionLevel est utilisé pour imprimer le rapport de fuite de mémoire du tas Netty.

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

Habituellement, le point de fuite de mémoire Netty peut être jugé par les deux méthodes ci-dessus, mais nous devons parfois juger du contenu de stockage spécifique de la mémoire hors tas pour analyser à nouveau la raison.

2. Utilisez pmap pour analyser les fuites de mémoire hors tas

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

La méthode de dépannage des fuites de mémoire est fournie ci-dessus Permettez-moi de parler des fuites de mémoire Netty extra-heap causées par l'utilisation d'un traitement logique incorrect.

format de protocole

pack de données structure du protocole
battement de coeur 0x00,0x00
données d'entreprise Longueur du contenu + contenu sérialisé PB

Utilisez ProtobufVarint32FrameDecoder dans Netty pour traiter le protocole PB (longueur du protocole + contenu sérialisé PB)

Processus Netty

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

Cette erreur de traitement logique apparemment sans problème lors de l'utilisation entraîne une fuite de mémoire hors tas.

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;
            }
        }
    }

Le processus ci-dessus ne semble pas poser de problème, mais pourquoi le RSS augmente-t-il après heartBeat.release() ?

buffer.readBytes() réalloue en fait un morceau de mémoire dans buf. Bien que release soit utilisé pour le libérer, ce morceau de mémoire est nouvellement alloué. L'index original du lecteur de tampon a été déplacé mais les données d'origine n'ont pas été nettoyées.

我们看一下 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进行深入了解才不会在编写代码上出现问题。

Je suppose que tu aimes

Origine juejin.im/post/7238027797313765413
conseillé
Classement