压测暴露
为了持续保障前台业务的服务稳定、性能稳定,京东零售-平台业务中心建设了线上流量录制能力,要求测试、研发团队对于前台业务进行常态化流量回放压测,常态化压测任务以周、日为周期。京东主站日历模块SOA服务是京东主APP活动日历的服务端,测试同学反馈用于常态化压测机器的压测性能出现了大幅度衰减,而且这个衰减过程是突然发生的,在3月30日还是正常的,但是3月31日在同等压测设置情况下CPU使用率达到了90%以上,且QPS大幅度下降,可用率也出现了下降的情况。
3月30日到3月31日的压测情况具体情况可以看下图:
出现这个问题后,测试同学在3月31日进行了反馈,由于线上其他机器没有任何异常现象,且我们在这段时间内没有进行上线、配置修改等,因此我们认为本次可能是偶发的问题,简单排查UMP监控后发现并无异常现象,并没有进行深入排查。
偶现问题?
在4月1日愚人节这天,又收到测试同学反馈常态化压测未达标,这时我们意识到问题并没有想象中的简单,但是由于压测机只有压测时才会有请求,压测的时间只会持续5分钟,因此无法直接复现问题现场,因此我们决定手动触发常态化压测,进行问题现场的复现。
手动触发流量回放压测任务以后,发现本次失败了达到了100%,这说明此时服务已经进入了不可用状态,由此推断出这并不是一个偶现的问题,而是一个存在于线上的问题。但是此时又出现了一个问题,为什么线上其他机器并没有出现任何问题,而只有单独拿出来的压测机出现了问题?
排查过程
3.1**监控排查**
首先我们分析了常态化压测的四个接口,其中接口A、B几乎没有业务逻辑,只是单纯的取了配置平台的配置项,而DUCC平台的配置项可以视为JVM内部的本地缓存,而且DUCC也是我们长期使用的配置平台,在稳定性和性能方面非常的优秀,因此排除怀疑这两个接口。而对于接口C和D,则是直接调用了上游接口,这里我们首先怀疑是因为上游接口性能出现了问题,导致了接口C和D出现了性能问题。
因此我们对接口C和D的UMP监控点进行了排查,项目中按照传统的分层模式分为了Controller层、Service层和Rpc层,每一层级都有单独的监控点,但是经过排查三层监控点,发现可用率是100%且请求得TP999也只有236ms。
这个现象让我们很诧异,为什么系统内监控点可用率100%且接口请求耗时Max只有236ms,HTTP接口却进入了一种不可用状态?
这里HTTP请求返回了99%以上的错误码是524,HTTP错误码524代表TCP握手完成但是却没有及时收到HTTP响应。
3.2**链路追踪**
排查到这里,似乎进入了一个僵局,项目内部的监控点并没有出现问题,但是服务器却不能响应请求。这时我们怀疑是这台机器的Tomcat服务器出现了问题,因此产生了重启服务器的想法(幸亏没有这样做),但是当时考虑到如果重启问题可能就不能复现,因此决定继续在问题现场进行排查。
这时想到了UMP-Pfinder提供的调用链监控能力,通过查找压测机器的请求找到了当时的调用链详情。
可以看到,接口调用JSF接口耗时总共只有20ms,但是Tomcat处理这次HTTP请求却耗时8s。这时候事情好像有了一些眉目,因为我们项目内的监控点,只能监控到Controller层的请求耗时,但实际上,Controller层的方法完成调用返回后,是交给SpringMVC框架处理,SpringMVC框架会进行一些HTTP报文的数据封装,而SpringMVC框架也是继承实现了Servlet-API,最终完成收发HTTP报文的则是被称为container(容器)的软件,就比如我们常用的Tomcat、Jetty等,实际上的调用路径如下图:
既然系统内监控点没有异常,而本次HTTP请求出现了问题,那么是不是有可能SpringMVC框架或者Tomcat出现了Bug呢?
3.3**压测机JVM排查**
虽然怀疑是SpringMVC或者Tomcat出了问题,但是也并没有太好的切入点去排查,因此还是需要从压测机器的JVM状况排查,首先我们通过UMP提供的JVM监控能力,发现最后一次手动触发压测任务时,JVM的GC情况、堆内存情况一切正常,CPU使用率也只有75%左右。
但是同比之前压测时间段内,我们发现VM的线程数由200个以内激增到了1000多个,因此我们重启了压测任务,在任务执行期间对压测机使用jstack工具dump了线程堆栈。
3.4**线程堆栈分析**
有了线程堆栈Dump样本以后,我们开始对线程进行分析,分析线程堆栈的工具有很多,这里我们选用了perfma提供的社区产品分析线程状态。
经过分析发现有1043个线程处于WAITING状态,这种现象是很反常的,按照经验,这种现象说明VM内部可能存在死锁或者存在存在竞争激烈且持锁时间比较长的线程,通过进一步分析发现,http-nio-1601-exec-*线程池中包含1000个线程,且有999个都处于WAITING状态。
http-nio-1601-exec-xxx 线程是Tomcat在1601端口的的工作线程。我们知道,Tomcat的工作线程数量可以通过max-threads配置项配置,这里我们配置了max-threads为1000,所以Tomcat的工作线程最大也只能有1000个,但是对于正常负载的服务器来说,线程池中的线程数会低于最大值的一半,也就是说会在500个以内,且处于WAITING状态的工作线程,一般都会处于等待数据库IO、RPC方法调用的状态,而对于较低负载的服务器来说,线程数甚至只有min-SpareThreads配置的最小值,而且大部分线程会处于等待线程池中任务队列的状态。
继续追查WAITING状态的线程,发现999个线程全部阻塞在org.springframework.util.MimeTypeUtils$ConcurrentLruCache.get 方法内部的读写锁上。
而唯一运行中的线程,也在执行org.springframework.util.MimeTypeUtils$ConcurrentLruCache.get方法,且堆栈在ConcurrentLinkedQueue的remove方法。
排查到这里,我们开始怀疑是org.springframework.util.MimeTypeUtils类中ConcurrentLruCache内部类的get方法存在问题。
3.5**Spring框架Bug?**
查看Spring框架源码,发现这块逻辑就是实现了一个LRU缓存,通过ConcurrentLinkedQueue保证线程安全的同时,实现了淘汰最近最久未使用的缓存key(这里缓存value其实就是HTTP MIME类型),因此在搜索引擎上进行搜索,发现 spring-framework 工程Github上有多个Issues,反馈MimeTypeUtils类出现性能衰减问题,导致CPU占用率飙升问题。最终Spring开发者认定了这些问题源自同一个Bug。
后续Spring开发者修复了这个问题,修复方式是将ConcurrentLinkedQueue换为了ConcurrentLinkedDeque。
到这里我们已经可以解决这个问题,只需要升级Spring框架版本号就可以避开这个Bug,但是有个问题始终萦绕在我心中,因为查看源码,作者对于这个LRU缓存做了一个最大长度的限制,超过这个maxSize就会淘汰缓存remove元素,为什么会出现Spring开发者说的内存泄漏和队列无限增长的问题呢?
继续探究
4.1**JDK Bug?**
带着好奇心继续查找相关资料,发现JDK-8137184汇报了一个ConcurrentLinkedQueue#remove(Object)导致内存溢出的Bug,但是这个Bug在2015年9月已经被解决了,难道说和这个Bug没有关系?
因此我去下载了我们使用的JDK源码压缩包,找到ConcurrentLinkedQueue类的源码后,发现其其中remove方法的Bug依旧是存在的。
为什么会这样呢?原来服务器上使用的是开源的Open JDK,Open JDK修复这个Bug时间较晚,在8u102版本才合并修复,这样就能解释我们所使用的Open JDK版本为什么依然存在这个Bug了。
4.2**Bug原因和后果**
这个Bug具体会造成什么现象呢?这里我找到了JDK的Bug报告:
The remove code both nulls the item value and then attempts to delink the pred next node pointer. However, if the current node next pointer is null (ie the last item), then the pred node next is not nulled. Thus the list grows forever!
根据Bug报告,remove方法应该把队列中需要删除的元素所在的节点值置为null,同时将节点从队列中delink(解链),这个很好理解,我们都知道链表在删除元素时需要解链(参考最简单的LinkedList),但是当这里的remove方法需要移除的元素位于链表中最后一个节点时,并不会移除最后一个节点,从而导致了链表的无限增长。
这里我们拿从服务器上下载的JDK源码,找到存在Bug的ConcurrentLinkedQueue类的源码来看下:
// ConcurrentLinkedQueue类有Bug的remove方法
public boolean remove(Object o) {
if (o == null) return false;
Node<E> pred = null;
for (Node<E> p = first(); p != null; p = succ(p)) { // 遍历链表
E item = p.item;
if (item != null &&
o.equals(item) &&
p.casItem(item, null)) {
Node<E> next = succ(p);
// 这里出现了问题,p为最后一个元素时,succ(p)返回null
// next=null,就导致下面这一行if判断永远为false
// 也就是说永远都不会移除这个节点
if (pred != null && next != null)
pred.casNext(p, next); // 删除节点
return true;
}
pred = p;
}
return false;
}
根据源码,可以很清晰的看明白这个Bug的成因,当需要remove的元素在链表的最后时,确实不会移除这个节点。
举个例子来说,如果先add或者offer元素2到链表中,下一步又直接remove这个元素2,重复这个过程,就会导致这个链表无限增长,又因为remove会遍历列表,这就可能导致remove遍历的时间越来越长,从而产生了之前我们服务器上999个线程都在等待1个线程remove方法执行。
理论上来说,这个Bug一定会导致JVM堆内存泄漏,使用jmap工具Dump线上机器堆内存以后,导入MAT分析后,发现确实存在内存泄漏的问题,线上机器并没有出现这个问题可能是因为这个列表还不够大,remove遍历时间还不至于导致服务不可用。
分析Dump文件以后,发现ConcurrentLinkedQueue的Node节点对象有74万个,而Spring框架MimeTypeUtils内的LRU缓存列表queue变量RetainedHeap大小几乎等于全部74万个Node对象的大小,也就是说明确实是MimeTypeUtils类内LRU缓存列表发生了内存泄漏。
4.3**后话**
JDK修复这个Bug的方式也比较简单,之前Bug产生的原因是因为remove方法删除最后一个元素时只把item值置为null,但是却并不能解链,导致在不断地add\remove中这个链表越来越长,既然这样,就在下次remove时把之前item为null的节点解链,JDK开发者就是这样修复了这个Bug。
// 修复Bug后的remove方法
public boolean remove(Object o) {
if (o != null) {
Node<E> next, pred = null;
for (Node<E> p = first(); p != null; pred = p, p = next) {
boolean removed = false;
E item = p.item;
// 这里if判断item是否为null
// item为null说明是上次删除遗留的空节点,进入后续解链逻辑
// item不为null则进入判断是否需要删除逻辑
if (item != null) {
if (!o.equals(item)) {
// 不是要删除元素,则继续向后遍历
next = succ(p);
continue;
}
// 要删除元素item值置为null
removed = p.casItem(item, null);
}
next = succ(p);
// 这里判断逻辑和之前一样,本次remove最后一个元素,依旧不会解链
// 但是上次remove最后一个元素后遗留的null节点,会在本次被解链
if (pred != null && next != null) // unlink
pred.casNext(p, next);
if (removed)
return true;
}
}
return false;
}
举个简单例子来说,如果我们依旧一直add\remove 元素2,那么就会出现下图这个情况,本次remove只把队尾的元素2所在的节点item置为null,而下次remove时就会把这个item=null的节点从链表中真正的移除,被移除的节点会失去和GCRoot连接,最终在下次GC中被回收,从而保证不会出现内存泄漏的问题。
复盘&总结
本次问题排查过程中,遇到了一些超出平常接触到的问题,也出现了想重启服务器尝试“解决”问题的错误思路,同时也因为这些障碍,我萌发了更多思路、借助了更多的新工具,比如说UMP平台新提供的链路追踪功能,就是我破局的一个重要倚赖。
出现了匪夷所思的问题,首先要保留好问题现场,比如说将VM堆和线程堆栈Dump保存,保存线上日志和GC信息等,特别是一些不能必现的问题,更需要问题现场才能进行排查,盲目的尝试重启解决问题,反而会掩盖存在的问题。
参考资料:
* spring-framework Issues#24886:https://github.com/spring-projects/spring-framework/issues/24886
* JDK-8137184:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8137184
* JDK-8150780:https://bugs.openjdk.java.net/browse/JDK-8150780
* PerfMa线程Dump分析工具:https://thread.console.perfma.com
* 堆内存分析工具 Memory Analyzer (MAT):https://www.eclipse.org/mat/
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/jA8IG3YG57z-gxNdFYPhYQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。