`
xiangshouxiyang
  • 浏览: 49271 次
  • 性别: Icon_minigender_1
  • 来自: 厦门
社区版块
存档分类
最新评论

使用dubbo进行消费者端调用导致内存溢出问题排查

 
阅读更多

       公司某流程系统项目组出现内存溢出问题,场景是使用dubbo进行文件传输。首先,dubbo协议不适合进行文件传输,这个做法本身有问题,该项目组可能出于开发效率,历史原因等情况,使用了这样的技术方案。

        发生内存溢出的场景是 测试人员连续的上传一个144M大小的文件,在上传四五次后,出现内存溢出。web接收文件,通过dubbo传输给service端进行文件处理。web和service端设置堆内存都是1g。在没有并发的情况下,连续上传144M的文件不应该出现内存溢出。初步怀疑是发生的内存泄露导致文件占用的内存没有被回收。

        把项目组的代码down下来本地运行系统对场景进行复现。

        设置相关JVM参数便于观察现象和保留现场:

      

-XX:+PrintGCDetails
-Xmx1024m
-Xms1024m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:/heapdump.hprof

       本地在连续上传五次这个144M的文件后,也出现了内存溢出。

 

      

java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3236)
	at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:113)
	at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
	at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:140)
	at org.springframework.util.StreamUtils.copy(StreamUtils.java:128)
	at org.springframework.util.FileCopyUtils.copy(FileCopyUtils.java:109)
	at org.springframework.util.FileCopyUtils.copyToByteArray(FileCopyUtils.java:156)
	at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getBytes(StandardMultipartHttpServletRequest.java:291)
	at com.gforc.biz.controller.FileController.uploadFile(FileController.java:526)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:483)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:114)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85)
	at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)

        可以看到,web容器使用的是undertow,undertow在接受http请求时,是先把http请求内容存放在直接内存中,在需要读取http的formdata内容时,对于非文件内容,直接复制到堆内存中,对于文件内容,则以nio的方式把文件信息从直接内存拷贝到临时文件中,整个过程,堆内存都没有申请文件大小的内存空间。

 

       在controller层调用springmvc的MultipartFile的file.getBytes()时,申请了一个文件大小的字节数组内存空间来存放文件流信息,此时内存不足,导致内存溢出。

       再观察GC日志:

[GC (Allocation Failure) --[PSYoungGen: 280566K->280566K(316928K)] 733103K->733103K(1016320K), 0.0048276 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 280566K->0K(316928K)] [ParOldGen: 452537K->329663K(699392K)] 733103K->329663K(1016320K), [Metaspace: 52711K->52711K(1097728K)], 0.0958676 secs] [Times: user=0.16 sys=0.00, real=0.10 secs] 
[GC (Allocation Failure) [PSYoungGen: 249872K->32K(286208K)] 841679K->591839K(985600K), 0.0033714 secs] [Times: user=0.05 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 254214K->220K(315904K)] 846021K->624795K(1015296K), 0.0139206 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 220K->0K(315904K)] [ParOldGen: 624575K->354325K(699392K)] 624795K->354325K(1015296K), [Metaspace: 52764K->52764K(1097728K)], 0.1000608 secs] [Times: user=0.19 sys=0.00, real=0.10 secs] 
[Full GC (Ergonomics) [PSYoungGen: 197604K->0K(315904K)] [ParOldGen: 616469K->583688K(699392K)] 814074K->583688K(1015296K), [Metaspace: 52764K->52764K(1097728K)], 0.1123836 secs] [Times: user=0.25 sys=0.00, real=0.11 secs] 
[GC (Allocation Failure) --[PSYoungGen: 280208K->280208K(315904K)] 863896K->929432K(1015296K), 0.0161663 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 280208K->0K(315904K)] [ParOldGen: 649224K->535404K(699392K)] 929432K->535404K(1015296K), [Metaspace: 52764K->52764K(1097728K)], 0.1258771 secs] [Times: user=0.33 sys=0.00, real=0.13 secs] 
[GC (Allocation Failure) [PSYoungGen: 131074K->32K(316416K)] 666479K->666508K(1015808K), 0.0250249 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(316416K)] [ParOldGen: 666476K->600940K(699392K)] 666508K->600940K(1015808K), [Metaspace: 52764K->52764K(1097728K)], 0.1091046 secs] [Times: user=0.22 sys=0.00, real=0.11 secs] 
[Full GC (Ergonomics) [PSYoungGen: 280433K->0K(316416K)] [ParOldGen: 600940K->478067K(699392K)] 881374K->478067K(1015808K), [Metaspace: 52765K->52765K(1097728K)], 0.1074561 secs] [Times: user=0.17 sys=0.00, real=0.11 secs] 
[GC (Allocation Failure) --[PSYoungGen: 250120K->250120K(316416K)] 728188K->859260K(1015808K), 0.0216361 secs] [Times: user=0.11 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 250120K->0K(316416K)] [ParOldGen: 609139K->600946K(699392K)] 859260K->600946K(1015808K), [Metaspace: 52765K->52765K(1097728K)], 0.1163858 secs] [Times: user=0.30 sys=0.00, real=0.12 secs] 
[Full GC (Ergonomics) [PSYoungGen: 262146K->262144K(316416K)] [ParOldGen: 600946K->469724K(699392K)] 863092K->731868K(1015808K), [Metaspace: 52765K->52765K(1097728K)], 0.2965119 secs] [Times: user=0.92 sys=0.00, real=0.30 secs] 
[Full GC (Ergonomics) [PSYoungGen: 283648K->0K(316416K)] [ParOldGen: 618040K->469741K(699392K)] 901688K->469741K(1015808K), [Metaspace: 52786K->52786K(1097728K)], 0.1205818 secs] [Times: user=0.23 sys=0.00, real=0.12 secs] 
[GC (Allocation Failure) [PSYoungGen: 279648K->128K(316928K)] 749390K->600942K(1016320K), 0.0325090 secs] [Times: user=0.11 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 128K->0K(316928K)] [ParOldGen: 600814K->600782K(699392K)] 600942K->600782K(1016320K), [Metaspace: 52788K->52788K(1097728K)], 0.2549737 secs] [Times: user=0.81 sys=0.00, real=0.25 secs] 
[Full GC (Ergonomics) [PSYoungGen: 262149K->262144K(316928K)] [ParOldGen: 600782K->467200K(699392K)] 862931K->729344K(1016320K), [Metaspace: 52788K->52598K(1097728K)], 0.2826828 secs] [Times: user=0.70 sys=0.00, real=0.28 secs] 
[Full GC (Ergonomics) [PSYoungGen: 278623K->0K(316928K)] [ParOldGen: 615516K->622978K(699392K)] 894139K->622978K(1016320K), [Metaspace: 52598K->52562K(1097728K)], 0.3579903 secs] [Times: user=0.98 sys=0.00, real=0.36 secs] 
[Full GC (Ergonomics) [PSYoungGen: 248317K->131072K(316928K)] [ParOldGen: 622978K->614786K(699392K)] 871295K->745858K(1016320K), [Metaspace: 52562K->52562K(1097728K)], 0.1744845 secs] [Times: user=0.44 sys=0.00, real=0.17 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 131072K->131072K(316928K)] [ParOldGen: 614786K->614707K(699392K)] 745858K->745779K(1016320K), [Metaspace: 52562K->52460K(1097728K)], 0.3197632 secs] [Times: user=0.92 sys=0.02, real=0.32 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:/heapdump.hprof ...

        通过GC日志可以发现,最后一个fullGC,老年代申请不到空间,导致了内存溢出。日志前面是新生代申请不到内存,触发fullGC,并把新生代存活的对象转移到老年代,老年代的存活对象不断增加,最终老年代内存不足。

       按道理,我连续的上传文件,在上传第二个文件时,第一个文件在堆内存的对象应该可以被JVM回收才对。可以发现,实际上JVM并没有办法回收文件对象。初步判定发生了内存泄漏。

       利用jdk自带的visualVM对内存溢出时保存的堆内存对象进行查看,



 
可以看到字节数组对象占了96.5%的内存。



 

可以看出来此时堆内存中的四个大小为151876120的对象就是我们上传的文件

       可以看出来,因为内存中的这四个文件对应的字节数组大内存对象没有被垃圾回收,导致内存溢出的发生。

       再看字节数组存在的引用,



 

       可以看出来,字节数组作为RPC接口的参数,被rpcContext的arguments属性和rpcInvocation的arguments属性引用,导致了字节数组无法被gc回收。可以看到,rpcContext被线程变量threadLocal引用。稍微熟悉dubbo的应该都清楚。rpcContext作为dubbo在rpc传输的上下文,经常被用来在dubbo消费者端和服务端之间通过线程变量,隐性的传递一些参数。

      由于使用dubbo的版本为2.5.7,翻阅dubbo源码,很快就看到dubbo自带的消费者端过滤器ConsumerContextFilter的代码:

/**
 * ConsumerContextInvokerFilter
 *
 * @author william.liangf
 */
@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        try {
            return invoker.invoke(invocation);
        } finally {
            RpcContext.getContext().clearAttachments();
        }
    }

}
 RpcContext代码:

 

 

public RpcContext setInvocation(Invocation invocation) {
        this.invocation = invocation;
        if (invocation != null) {
            setMethodName(invocation.getMethodName());
            setParameterTypes(invocation.getParameterTypes());
            setArguments(invocation.getArguments());
        }
        return this;
    }
       通过上面代码可以看出,dubbo在每次消费者端调用服务端时,会把rpc调用的方法,参数等信息通过线程变量的方式记录下来,但在rpc调用完,只清除了rpcContext中的attachment属性。导致线程一直持有rpc调用传参的引用。

 

       在线程再次进行rpc调用时,线程变量会引用新的rpc调用的传参,旧的rpc调用传参就可被回收。由于dubbo本身使用于小数据量的传输,所以这种轻微的内存泄漏,在普通业务调用中并不会暴露出问题,但是在用来做文件传输时,特别是大文件传输时,问题就暴露出来了。       可以说,这是dubbo的一个bug,查看了github上dubbo的最新源码,发现该问题已被修复。在2019年5月23日提交的dubbo2.7.2版本代码中修复了该问题。dubbo2.7.2版本中ConsumerContextFilter代码:

 

@Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        try {
            RpcContext.removeServerContext();
            return invoker.invoke(invocation);
        } finally {
            RpcContext.removeContext();
        }
    }
 可以看到,在代码的finally代码块中,把整个rpcContext清除了。即修复了该问题。

 

           内存泄漏的问题定位已经非常明确,要解决这个问题也显得很简单了。考虑升级dubbo框架可能会引入不必要的风险,针对该问题,我添加了一个dubbo过滤器RemoveRpcContextFilter,用于解决该问题。
package com.xxx.framework;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;

/**
 * dubbo消费服务后移除线程变量
 * dubbo当前2.5.7版本ConsumerContextFilter中通过RpcContext以线程变量的方式引用接口中的参数对象,且没有移除线程变量,导致参数无法被GC,
 * 只能在线程下次执行dubbo调用时线程变量指向新的参数对象的方式,让旧参数对象进行垃圾回收。这里手动清除线程变量,helpGC
 * dubbo适用于小数据传输,线程变量引用部分旧请求参数对象影响不大,该问题在dubbo 2019.5.23提交的2.7.2版本已修复。
 * Created by cd_huang on 2019/6/4.
 */
@Activate(group = Constants.CONSUMER, order = -10001)
public class RemoveRpcContextFilter implements Filter {

	public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
		try {
			return invoker.invoke(invocation);
		} finally {
			RpcContext.removeContext();
		}
	}
}
       然后在resouce目录下META-INF/dubbo目录下新建com.alibaba.dubbo.rpc.Filter文件,内容为
RemoveRpcContextFilter=com.xxx.framework.RemoveRpcContextFilter
          以dubbo的SPI机制注入该过滤器,使其生效。       
        本地验证后提交到测试,测试人员验证通过,即解决该问题。       
       使用dubbo进行文件传输这种做法本身就是有问题的。dubbo协议默认采用单一长连接进行NIO传输。在传输大文件网络传输过程,对导致该消费者端和服务端网络阻塞。dubbo配置使用多个连接可以一定程度上规避该问题。
      考虑到管理类流程类系统本身用量不大,基本不存在并发,以快速实现业务功能为主,暂时使用dubbo进行大文件传输,后续考虑其他方案。

 

 

 

 

 

 

  • 大小: 35.8 KB
  • 大小: 33 KB
  • 大小: 14.8 KB
  • 大小: 26.7 KB
分享到:
评论

相关推荐

    Dubbo+Dubbo生产者,Dubbo消费者+Dubbo消费者调用生产者的服务的小demo

    本项目案例“妻子”作为消费者去调用生产者(“丈夫”)的服务--洗碗 本项目需要注意 @DubboReference private HusbandService husbandService; @EnableDubbo @DubboService 这几个注解不能忘,否则会报错,或者...

    Spring Boot 整合 Dubbo + Zookeeper 实现服务者与消费者的数据调用

    1.SpringBoot聚合工程整合Dubbo,实现服务提供者与服务消费者的数据调用, 2.该项目提高了自己对Spring Boot整合Dubbo的理解,并深刻的认识到了服务者与消费者之间的调用及流程 4. Dubbo配置全部采用yml文件配置,...

    dubbo消费者的web应用实例

    2. 路由选择:当有请求来临时,Dubbo消费者根据配置的策略(如随机、轮询、权重等)选择一个服务提供者进行调用。 3. 调用:通过网络发送RPC请求到选定的服务提供者。 4. 响应处理:服务提供者执行完业务逻辑后,...

    dubbo提供者消费者jar包

    在这个“dubbo2.4版本提供者provider消费者consumer jar包”中,包含了实现Dubbo服务提供者(Provider)和消费者(Consumer)功能的核心类库。 首先,我们要理解Dubbo的核心概念。服务提供者(Provider)是提供服务...

    Dubbo调用java接口程序

    本篇文章将深入讲解如何使用Dubbo进行Java接口的调用。 首先,理解Dubbo的核心概念: 1. **服务(Service)**:服务是业务逻辑的封装,它定义了服务的接口和协议,如`HelloService`,包含了一组`sayHello`这样的...

    dubbo提供者消费者例子

    同时,它提供了丰富的监控和管理工具,便于运维人员监控服务状态,进行问题排查。 总之,这个"Dubbo提供者消费者例子"是学习和理解Dubbo工作原理和使用方式的一个很好的起点。通过这个实例,你可以了解到如何创建...

    dubbo生产者消费者使用demo

    《Dubbo生产者消费者使用Demo详解》 Dubbo,作为阿里巴巴开源的一款高性能、轻量级的服务治理框架,已经成为Java世界中分布式服务的重要选择。本文将深入解析如何在实际项目中运用Dubbo,通过一个简单的生产者消费...

    dubbo提供者和消费者例子

    总结起来,Dubbo 的提供者和消费者模型是构建分布式系统的关键部分,它们之间通过注册中心进行交互,实现了服务的发布、订阅和调用。了解并熟练掌握这一模型,对于开发基于 Dubbo 的分布式应用至关重要。在实际开发...

    nodejs使用原生的dubbo协议打通了dubbo的rpc方法调用.

    标题中的“nodejs使用原生的dubbo协议打通了dubbo的rpc方法调用”意味着在Node.js环境中,开发者成功地实现了对Dubbo服务的RPC(远程过程调用)访问,利用了Dubbo协议的特性。Dubbo是阿里巴巴开源的一个高性能、轻量...

    Dubbo服务提供者以及消费者实例

    服务提供者会在指定端口监听请求,等待消费者调用。 接下来,我们转向服务消费者(Consumer)。服务消费者是需要依赖服务提供者实现特定功能的组件,其主要流程如下: 1. **引用服务**: 在服务消费者端,需要通过`...

    dubbo提供者和消费者工程

    《Dubbo提供者与消费者工程详解》 Dubbo,作为阿里巴巴开源的一款高性能、轻量级的服务治理框架,已经成为Java世界中分布式服务的重要组件。在理解Dubbo的核心概念时,我们通常会接触到“提供者”和“消费者”这两...

    dubbo实现的消费者,提供者的demo

    这个名为"Dubbo实现的消费者,提供者的demo"的项目,旨在通过实例演示如何使用Dubbo来创建服务消费者和服务提供者,并将它们进行模块化的拆分。下面,我们将深入探讨这些关键知识点。 1. **Dubbo架构核心组件**: ...

    整合dubbo、maven、spring、mybatis的服务提供者消费者调用错误示例

    8. **测试与调试**:如果上述步骤无法解决问题,可以尝试模拟消费者调用,通过单元测试或集成测试来复现问题,逐步缩小问题范围。 总的来说,解决此类问题需要对整个系统架构有深入的理解,包括Dubbo的工作原理、...

    构建Dubbo服务消费者Web应用的war包并在Tomcat中部署-步骤.docx

    Dubbo 服务消费者 Web 应用是指使用 Dubbo 框架开发的服务消费者应用程序。本文将指导您如何构建 Dubbo 服务消费者 Web 应用的 war 包,并将其部署到 Tomcat 中。 1. Dubbo 服务消费者 Web 应用 war 包的构建 ...

    dubbo-provider-consumer生产者消费者实例

    Dubbo 的消费者支持动态配置,这意味着消费者可以在运行时根据服务提供者的列表动态选择调用哪个服务,甚至支持负载均衡策略,如轮询、随机、优先级等。 在实例中,我们通常会分为以下几个步骤来实现生产者和消费者...

    dubbo例子(注册中心,消费者,提供者)

    2. **服务消费者(Consumer)**:服务消费者是需要调用服务提供者接口的组件。在本例子中,Consumer在启动时会从Zookeeper获取服务提供者的信息,并根据这些信息动态地进行服务调用。Dubbo支持动态代理,使得Consumer...

    dubbo-zookeeper springSpringMVC 一个生产者,多消费者 例子

    1.dubbo-zookeeper springSpringMVC 一个生产者,多消费者 例子 2. ssm-dubbo 源码 ssm-tomcat 里放的是 ... http://localhost:8080/dubbo-web/selectName 访问web 接口,连续调用10次,消费者调用生产者的不同

    dubbo提供与调用

    Dubbo支持多种注册中心,如Zookeeper、Eureka等,它们用于存储服务提供者的信息,以便服务消费者能够发现并调用。配置注册中心如下: ```xml &lt;dubbo:registry address="zookeeper://192.168.1.1:2181" /&gt; ``` 4...

    springboot 调用dubbo实例

    在消费者端调用服务接口,验证调用是否成功。 在实际项目中,我们还需要关注服务版本管理、接口版本兼容性、服务健康检查、熔断和降级策略等问题。同时,Spring Boot与Dubbo的整合也支持使用Spring Cloud的API ...

    基于注解、SpringBoot的Dubbo提供者消费者DEMO

    本教程将详细探讨如何基于注解和SpringBoot构建一个简单的Dubbo提供者与消费者DEMO,让你能够快速理解和实践这一技术。 首先,我们来理解"注解"在编程中的作用。注解(Annotation)是Java提供的一种元数据,它为...

Global site tag (gtag.js) - Google Analytics