起因
事件的起因是因为晚期的一些服务版本放到当初太低了,基本上都是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 来上传大文件,真的会很蹩脚!