起因
事件的起因是因为晚期的一些服务版本放到当初太低了,基本上都是SpringBoot1.5.x
,因而筹备对立对服务进行一次版本升级,降级到2.1.x
,SpringCloud
`版本升级到Greenwich`。当然咱们用的旧版本的zuul相干的都须要降级。
意外的Bug
咱们网关应用的是zuul,应用的是spring-cloud-netflix
封装的包,此次版本升级同步降级了相干的包。然而意外的状况产生了,在测试环境上咱们发现上传文件会出现异常。具体表现是这样的:当上传的文件超出肯定大小后,在通过zuul网关并向其余服务转发的时候,之前上传的包就不见了。这个状况非常奇怪,因而马上开始排查。
Bug的排查
呈现这样的问题,第一反馈是测试是不是基本没有上传包所以当然包没法转发到下一层,当然这种想法很快被否定了。好吧,那就认真的排查吧。
首先先去追踪了一下路由以及呈现的具体日志,将问题定位到zuul服务,排除了上游nginx和上游业务服务呈现问题的可能。然而zuul服务没有任何异样日志呈现,所以十分困扰。查看过后发现文件的确有通过zuul,然而之后凭空隐没没有留下一点痕迹。
明明当初思考上传文件的问题给zuul调配了两个g的内存,怎么上传500m的文件就出问题了呢?不对!此时我灵光一闪,会不会和垃圾回收机制无关。咱们的文件是十分大的,这样的大文件生成的大对象是会保留在java的堆上的,并且因为垃圾回收的机制,这样的对象不会经验年老代,会间接调配到老年代,会不会是因为咱们内存参数设置不合理导致老年代太小而放不下呢?想到做到,咱们通过调整jvm参数,保障了老年代至多有一个G的空间,并且同步检测了java的堆内存的状态。然而让人悲观的是竟然没有见效。不过此时事件和开始不同,咱们有了线索。在方才的堆的内存监控中发现了一些异样,随即正当狐疑是堆中内存不够导致了oom。随后加大内存尝试并且再次运行,发现竟然上传胜利了。果然是老年代内存不足导致的oom,不过尽管上传胜利,然而老年代中的内存竟然被占用了1.6G左右,明明是500M的文件,为什么会占用了这么大的内存呢?
尽管找到了起因,然而减少内存显然不是解决问题的办法,因而,咱们在启动参数上新增了-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data
筹备查看oom的具体分析日志。
查看堆栈信息能够发现,溢出是产生在byte数组的拷贝上,咱们迅速定位代码,能够找到如下的代码:
public InputStream getRequestEntity() { if (requestEntity == null) { return null; } if (!retryable) { return requestEntity; } try { if (!(requestEntity instanceof ResettableServletInputStreamWrapper)) { requestEntity = new ResettableServletInputStreamWrapper( StreamUtils.copyToByteArray(requestEntity)); } requestEntity.reset(); } finally { return requestEntity; } }
这段代码源自RibbonCommandContext
是在zuul中进行申请转发的时候调用到的,具体的OOM是产生在调用StreamUtils.copyToByteArray(requestEntity));
的时候。持续进入办法查找源头。最终通过排查找到了溢出的源头。ribbon转发中的用到了ByteArrayOutputStream
的拷贝,代码如下:
public synchronized void write(byte b[], int off, int len) { if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) - b.length > 0)) { throw new IndexOutOfBoundsException(); } ensureCapacity(count + len); System.arraycopy(b, off, buf, count, len); count += len; }
能够看到这边有一个ensureCapacity
,查看源码:
private void ensureCapacity(int minCapacity) { // overflow-conscious code if (minCapacity - buf.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = buf.length; int newCapacity = oldCapacity << 1; if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); buf = Arrays.copyOf(buf, newCapacity); }
能够看到ensureCapacity
做了一件事,就是当流拷贝的时候byte数组的大小不够了,那就调用grow
进行扩容,而grow
的扩容和ArrayList
不同,他的扩容是每一次将数组扩充两倍。
至此溢出的起因就很分明了,500m文件占用1.6g是因为刚好触发扩容,导致用了多一倍的空间来包容拷贝的文件,再加上源文件,所以占用了文件的3倍空间。
解决方案
至于解决方案,调整内存占用或者是老年代的占比显然不是正当的解决方案。咱们再回头查看源代码,能够看到这个局部
if (!retryable) { return requestEntity; }
如果设置的不重试的话,那么body中的信息就不会被保留。所以,咱们决定长期先去除上传文件波及到的服务的重试,之后再批改上传机制,在当前的上传文件时绕过zuul。
追根溯源
尽管找到的起因,并且也有了解决方案,然而咱们依然不晓得为什么旧版本是ok的,因而本着追本溯源的态度,找到了旧版的zuul的源码。
新版的ribbon代码集成spring-cloud-netflix-ribbon
,而旧版的ribbon的代码集成在spring-cloud-netflix-core
中,所以稍稍破费点工夫才找到对应的代码,查看不同,发现旧版的getRequestEntity
没有任何的解决,间接返回了requestEntity
public InputStream getRequestEntity() { return requestEntity; }
而在之后的版本中马上就加上了拷贝机制。于是咱们去github上找到了当初的那个commit
之后咱们顺着commit中给出的信息找到了最后的issue
查看过issue之后发现这原来是旧版的一个bug,这个bug会导致旧版的post申请在retry的时候有body失落的状况,因而在新版本中进行了修复,当申请为post的时候会对于body进行缓存以便于重试。
至此,咱们原原本本的还原了这个bug的全貌以及造成的历史和起因。并且找到适当的解决方案。最初提一句:真的不要用zuul来上传大文件,真的会很蹩脚!