一次zuul版本升级产生的问题排查记录
起因
事情的起因是由于早期的一些服务版本放到现在太低了,基本上都是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来上传大文件,真的会很糟糕!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
开源中国搬家啦!
开源中国搬迁公告 亲爱的朋友: 感谢您长期以来对开源中国的关注与支持!因业务发展需要和公司规模扩大,自 2020 年 10 月 14 日起,公司搬迁至新的地址(深圳国际开源谷); 地址:深圳市福田区中康路136号 新环境,新起点;新征程,新未来!我们将一如既往,继续努力,传播开源理念、助力开源项目成长,并为开发者提供学习与工作一站式技术平台。 开源中国 2020年10月14日 近日,开源中国搬迁到了位于深圳国际开源谷的全新办公室。至此,我们将拥有恢弘大气的办公大楼,干净整洁的办公环境,宽敞舒适的休闲区域等。眼前的这一切既让我们兴奋,也让我们感慨一路走来的不易。 自 2012 年公司成立以来,我们的办公地点几经辗转: 彩田大厦汇福阁(2012 ~ 2014) 2012 年,开源中国聚集了第一批创业小伙伴,有了一个 10 人的小团队。而我们的第一个办公室位于福田彩田大厦汇福阁 31A ,一间小小的阁楼里,承载着大大的梦想: 阁楼的小阳台,留下星辰与奋斗的回忆~ 没见过凌晨 4 点的洛杉矶,但见过凌晨 5 点的福田 喜提新电脑,难掩的嘴角上扬~ 早期装在红薯办公室里的服务器......
- 下一篇
签到功能实现,没有你想的那么复杂(一)
1.签到定义以及作用 签到,指在规定的簿册上签名或写一“到”字,表示本人已经到达。在APP中使用此功能,可以增加用户粘性和活跃度. 2.技术选型 redis为主写入查询,mysql辅助查询. 传统签到多数都是直接采用mysql为存储DB,在大数据的情况下数据库的压力较大.查询速率也会随着数据量增大而增加.所以在需求定稿以后查阅了很多签到实现方式,发现用redis做签到会有很大的优势.本功能主要用到redis位图,后面我会详细讲解实现过程. 3.实现效果 这里抛砖引玉,展示我们app的签到实现效果 4.功能实现 功能大致分为两个大模块 签到流程(签到,补签,连续,签到记录)签到任务(每日任务,固定任务) 签到流程图如下: 4.1.1 表设计 因为大部分功能使用redis存储,使用到mysql主要是为了存储用户总积分以及积分记录,便于查询签到记录和用户总积分 CREATE TABLE `t_user_integral` ( `id` varchar(50) NOT NULL COMMENT 'id', `user_id` int(11) NOT NULL COMMENT '用户id', `...
相关文章
文章评论
共有0条评论来说两句吧...