ElasticSearch客户端迁移到Rest Client后,ElasticSearch节点内存打满问题排查

文章讲述了在将Elasticsearch集群从TransportClient迁移到RestClient后,遇到内存溢出的问题。通过内存分析发现Netty4CorsHandler和CopyBytesSocketChannel的内存占用,尤其是Netty4HttpRequest的大量堆积。解决方法是在Netty4CorsHandler的channelRead方法中及时释放请求引用,以避免内存泄漏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

当前ElasticSearch集群数据写入使用Transport Client,由于Transport Client在8.0版本后弃用并且存在性能问题,所以要将数据写入的客户端迁移到Rest Client

现象

将客户端迁移为Rest Client之后,部分ElasticSearch节点出现内存被打满的现象,节点重启后过一段时间又被打满。dump进程内存后立刻回滚了客户端并将ElasticSearch集群节点全量重启。

问题分析

使用Eclipse Memory Analyzer Tools进行dump文件的内存分析,得到以下结果。


通过Dominator Classes可以看出是CopyBytesSocketChannel和Transport$ResponseHandlers占据了大部分内存,下面进行分别分析。

CopyBytesSocketChannel分析

CopyBytesSocketChannel为netty中的channel,每个TCP连接创建一个。
CopyBytesSocketChannel实例的数量和我们ES节点的连接数相同,异常的地方在于CopyBytesSocketChannel实例的大小。
通过上面的类内存占用排行可以看出多个类占用大小为16GB,这些类之间的引用关系如下:
CopyBytesSocketChannel -> DefaultChannelPipeline -> DefaultChannelHandlerContext -> Netty4CorsHandler -> Netty4HttpRequest->CompositeBytesReference(content)(->byte[](headers FullHttpRequest))-> BytesReference[] -> BytesReference -> byte[]

Netty4HttpRequest是最底层且有现实意义的类,Netty4HttpRequest是客户端发起的http请求,他引用的属性为请求的相关信息例如content、header。抽查发现Netty4HttpRequest为bulk写入请求。Netty4HttpRequest预期应该请求处理完成就被回收了,为什么还会在内存中堆积了4000+的实例?Netty4HttpRequest的引用方是Netty4CorsHandler,那我们来看下Netty4CorsHandler引用和释放Netty4CorsHandler引用的时间。代码如下:

public class Netty4CorsHandler extends ChannelDuplexHandler {

    public static final String ANY_ORIGIN = "*";
    private static Pattern SCHEME_PATTERN = Pattern.compile("^https?://");

    private final CorsHandler.Config config;
    private Netty4HttpRequest request; // Netty4HttpRequest的引用

    /**
     * Creates a new instance with the specified {@link CorsHandler.Config}.
     */
    public Netty4CorsHandler(final CorsHandler.Config config) {
        if (config == null) {
            throw new NullPointerException();
        }
        this.config = config;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        assert msg instanceof Netty4HttpRequest : "Invalid message type: " + msg.getClass();
        if (config.isCorsSupportEnabled()) {
            request = (Netty4HttpRequest) msg;
            if (isPreflightRequest(request.nettyRequest())) { 
                try {
                    handlePreflight(ctx, request.nettyRequest());
                    return;
                } finally {
                    releaseRequest();
                }
            }
            if (!validateOrigin()) { 
                try {
                    forbidden(ctx, request.nettyRequest());
                    return;
                } finally {
                    releaseRequest();
                }
            }
        } // 没有释放request的引用
        ctx.fireChannelRead(msg);
    }


    private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
        final HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, true, true);
        if (setOrigin(response)) {
            setAllowMethods(response);
            setAllowHeaders(response);
            setAllowCredentials(response);
            setMaxAge(response);
            setPreflightHeaders(response);
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        } else {
            forbidden(ctx, request);
        }
    }

    private void releaseRequest() { 
        request.release();
        request = null;
    }


    private static boolean isPreflightRequest(final HttpRequest request) {
        final HttpHeaders headers = request.headers();
        return request.method().equals(HttpMethod.OPTIONS) &&
            headers.contains(HttpHeaderNames.ORIGIN) &&
            headers.contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD);
    }

}

通过代码可以看出,在Netty4CorsHandler#channelRead方法被调用时引用了Netty4HttpRequest,但在channelRead方法结束后也没释放引用,并且此时Netty4HttpRequest引用不会被Netty4CorsHandler实例使用完全可以释放掉。所以Netty4HttpRequest的生命周期非预期的和Netty4CorsHandler绑定在一起了。Netty4CorsHandler是netty中的ChannelHandler,声明周期与Channel相同,Channel的生命周期为客户端与服务端间连接的生命周期,也就是Rest Client与ES节点间连接的生命周期,该连接为长连接,也就是说除非Rest Client挂掉,Netty4HttpRequest永远不会被回收。

现在我们找到了原因,即ES服务端的Netty4CorsHandler发生了内存泄漏,导致Netty4HttpRequest不会被回收,堆积数量等于连接个数,在我们的场景下单个ES节点的连接数在10000左右,单个Netty4HttpRequest实例大小在3MB左右,10000*3MB=30GB,ES的堆内存大小也就32GB。Netty4HttpRequest占用了这么大的内存,自然ES节点内存被打满了。

ElasticSearch社区没有相关issue,但社区在对Transport代码模块整理时重构了Netty4CorsHandler,意外解决了该问题。我们使用的版本是7.9,社区进行重构的版本是7.10.0。(https://github.com/elastic/elasticsearch/pull/62007)

Transport$ResponseHandlers分析

在集群间节点通信的场景下,ES节点向另一个节点发起请求后会将标识该请求的request id和负责处理response的handler保存到map中。内存仍未打满的节点向内存被打满ES节点(由于Netty4HttpRequest占用大量内存)发起的请求不会被响应,因此ResponseHandlers会内会堆积大量的ResponseHandler,导致Transport$ResponseHandlers占用大量内存。

解决方法

在Netty4CorsHandler处理完成Netty4HttpRequest后立即释放引用,代码如下:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        assert msg instanceof Netty4HttpRequest : "Invalid message type: " + msg.getClass();
        if (config.isCorsSupportEnabled()) {
            request = (Netty4HttpRequest) msg;
            if (isPreflightRequest(request.nettyRequest())) { 
                try {
                    handlePreflight(ctx, request.nettyRequest());
                    return;
                } finally {
                    releaseRequest();
                }
            }
            if (!validateOrigin()) { 
                try {
                    forbidden(ctx, request.nettyRequest());
                    return;
                } finally {
                    releaseRequest();
                }
            }
        }
        request = null; // for GC
        ctx.fireChannelRead(msg);
    }

总结

  1. 内存问题排查重点关注实例的引用关系和声明周期
  2. 充分理解相关代码逻辑是问题排查的前提,例如排查该问题需要了解Netty的基础组件的调用关系和ES对Netty的使用方式以及Rest Client发起的请求和连接的特点。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值