前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty模拟OOM-Metaspace

Netty模拟OOM-Metaspace

作者头像
书唐瑞
发布2022-06-02 14:09:52
3670
发布2022-06-02 14:09:52
举报
文章被收录于专栏:Netty历险记

在模拟OOM之前, 先简单说下Netty服务端向客户端发送数据的时候, 涉及两个存储数据的地方, 如下图所示

业务线程在向客户端发送数据的时候, 是不能直接把数据发送到网络的, 只有IO线程才可以把数据发送到网络, 因此业务线程只能把数据封装成一个任务放到与IO线程关联的一个Queue中, 之后IO线程会从Queue中取出任务, 执行写操作, 将数据写到网络. 因此这个Queue就是存储数据的第一个地方.

在之前的文章中,介绍过 使用Netty模拟发生OOM , 那里说的OOM是指java.lang.OutOfMemoryError:Java heap space, 即堆空间的OOM, 之所以发生OOM, 就是因为Queue中的任务太多太多导致的.

Netty中的IO线程在将数据发送到网络的时候, 并不是直接把数据写到TCP缓冲区, 如上图所示, Netty中也有自己的缓冲区. IO线程会从Queue中取出任务, 将数据先写到Netty的缓冲区(对应的Netty方法是write), 然后再将Netty缓冲区中的数据刷到TCP缓冲区(对应的Netty方法是flush). 因此这个Netty缓冲区就是存储数据的第二个地方. 那么接下来我们就要模拟一直向Netty缓冲区写数据, 而且数据不要刷到TCP缓冲区, 我们就要让Netty缓冲区中的数据一直增长, 看看发生的现象和结果是什么?

为了达到不将数据刷到TCP缓冲区, 我们需要修改Netty的源码, 因此先从GitHub上下载源码, 并导入到集成开发工具.

下面是模拟实验所需要的Netty服务端有关代码

代码语言:javascript
复制
package com.infuq;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class Server {


    public static void main(String[] args) throws Exception {

        // 这个线程用于接收客户端的连接
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 这4个线程用于处理IO读写
        EventLoopGroup workerGroup = new NioEventLoopGroup(8);
        // 这8个线程用于业务处理
        EventLoopGroup businessGroup = new NioEventLoopGroup(8);

        ServerBootstrap serverBootstrap = new ServerBootstrap();

        try {

            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ChannelPipeline channelPipeline = ch.pipeline();
                            channelPipeline.addLast(new StringEncoder());
                            channelPipeline.addLast(new StringDecoder());
                            channelPipeline.addLast(businessGroup, new ServerInHandler());
                        }
                    });

            ChannelFuture channelFuture = serverBootstrap.bind("127.0.0.1", 8080).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
代码语言:javascript
复制
package com.infuq;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;


public class ServerInHandler extends SimpleChannelInboundHandler<String> {

    // 当客户端连接到服务端之后, 服务端就会回调这个channelActive方法.
    // 在这个方法里, 一直循环向客户端写数据
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (;;) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            byte[] bytes = new byte[1024 * 1024];
            bytes[0] = 'N';
            bytes[1] = 'e';
            bytes[2] = 't';
            bytes[3] = 't';
            bytes[4] = 'y';
            bytes[1024 * 1024 - 1] = 'X';

            // 向客户端写数据
            ctx.writeAndFlush(new String(bytes, 0, 1024 * 1024));
        }
    }
}

修改源码, 不让数据刷到TCP缓冲区. 如下

代码语言:javascript
复制
// 源码位置: io.netty.channel.AbstractChannel.AbstractUnsafe#flush0

protected void flush0() {
    
    // 省略一些无关代码
    ...

    try {
        logger.info("禁止flush...");
        // 在这里注释掉doWrite方法, 因为在它的底层会将Netty缓冲区中的数据刷到TCP缓冲区.
        // doWrite(outboundBuffer);
    } catch (Throwable t) {
        handleWriteError(t);
    } finally {
        inFlush0 = false;
    }
}

同时设置VM参数如下

代码语言:javascript
复制
-XX:MetaspaceSize=15664K
-XX:MaxMetaspaceSize=15664K
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\heapdump.hprof
-Xmx100M

接下来启动服务端, 观察打印的日志, 过了一会, 就会出现OOM, 而且这个OOM还是发生在元空间(Metaspace).

接下来分析下, 为什么发生OOM, 而且是元空间OOM.

之所以发生OOM, 就是因为服务端一直循环地向客户端写数据, 数据只是写到了Netty的缓冲区, 并没有继续向下写进TCP缓冲区(我们修改了源码,不让它写), 数据一直'拥堵'在Netty的缓冲区, 最终导致OOM.

而为什么是发生在元空间呢?

IO线程在将数据写到Netty缓冲区的时候(调用write方法), 数据是不能放在堆空间的, 数据必须放在堆外空间(直接内存). 看下源码

代码语言:javascript
复制
// 源码位置: io.netty.channel.AbstractChannel.AbstractUnsafe#write

@Override
public final void write(Object msg, ChannelPromise promise) {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;

    
    int size;
    try {
        // 过滤消息
        msg = filterOutboundMessage(msg);
        size = pipeline.estimatorHandle().size(msg);
        if (size < 0) {
            size = 0;
        }
    } catch (Throwable t) { }

    outboundBuffer.addMessage(msg, size, promise);
}



// 源码位置: io.netty.channel.nio.AbstractNioByteChannel#filterOutboundMessage
@Override
protected final Object filterOutboundMessage(Object msg) {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf) msg;
        // 如果buf是属于堆外内存
        if (buf.isDirect()) {
            return msg;
        }

        // 如果buf不是属于堆外内存(直接内存), 那么需要转成堆外内存的buf.
        return newDirectBuffer(buf);
    }

    ...
}

也就是说, Netty缓冲区中的所有数据都是在堆外内存的, 如果不是堆外数据, 那么需要转成堆外数据, 因此发生的OOM才是Metaspace.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Netty历险记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档