12月知识小报|线上问题的抽丝剥茧与一锤定音

news/2023/6/6 4:10:13

海恩法则是德国飞机涡轮机的发明者帕布斯·海恩提出的一个在航空界关于飞行安全的法则。

每一起严重事故的背后,必然有29次轻微事故和300起未遂先兆以及1000起事故隐患。

作为开发者,安全生产是我们底线,敬畏每一行代码,挖掘每一个故障背后的根因,避免再次落入同一个坑是应该成为我们的工作习惯,这里有几个我们线上真实有趣的故障案例分享给大家。

三目运算符--最熟悉的陌生人

三目运算符任何一个初学JAVA的同学都会接触到,而且在我们代码中也处处可见:

expression1 ? expression2 : expression3

expression1 可以是计算为 boolean 值的任何表达式。如果 expression1 是 true ,那么将评估 expression2 。否则,将评估 expression3。然而就是这个最熟悉的陌生人给我们带来一次可怕的线上故障,故障过程略去不表,大家可以先看一段代码,盲猜一下哪里出了问题:

77c60b1fe8c31257d1fa6eecca5ed58e.png

第一眼可能可能就能看出,最后一行三目运算法的 null != playBoyExtra貌似是多余的,因为前面已经判空并且return了啊。 

是的,这个的确是个问题,但是不至于执行不下去,再看一下错误日志:三元运算符这一行竟然出现万恶的NPE。

49a9683e222220ce7dcd473605e58aab.png

为何这里会出现NPE,唯一有可能为null的只能是playBoyExtra.getOpenTime() == null,但是为何会抛出NPE不应该是三目运算结果是null?写个简单的测试代码:

public class ConditionalOperatorDemo {public static void main(String[] args) {PlayBoyExtra playBoyExtra = new PlayBoyExtra();Long openTime = null != playBoyExtra ? playBoyExtra.getOpenTime():0L;}private static class PlayBoyExtra{private Long openTime;public Long getOpenTime() {return openTime;}public void setOpenTime(Long openTime) {this.openTime = openTime;}}
}

用javap -c 对测试代码进行反编译:

1b0e7333501e66f09533d706ae19c93a.png

观察红框内的三行,这三行翻译过来其实等同于:

Long openTime = (Long)(null != playBoyExtra ? playBoyExtra.getOpenTime().longValue():0L);

第一步:执行playBoyExtra.getOpenTime()获取到一个Long类型的对象O1,然后这里在某些情况下返回的是null。 

第二步:执行O1.longValue()得到long基本类型数值V2,也就是执行一个自动拆箱操作,在playBoyExtra.getOpenTime()返回null的情况下就变成了null.longValue(),就会抛出NPE。 

第三步:执行三目运算,并且将三目运算结果long类型的V3进行自动装箱操作,Long openTime=(Long)V3。 

为何会执行自动拆箱呢?这其实是三目运算符的语法规范。参考JLS-15.25.2 Conditional Operator ? : 章节【附录1】的描述:

8a52b63e6f56db739bdf3965ca3184c3.png

如红框所言,如果第二和第三位操作一个是基本类型,一个是对象,那么会发生自动拆箱操作转换为基本类型。这里就是因为第三个值是基本类型“0L”,所以导致出现playBoyExtra.getOpenTime()会发生自动拆箱操作,而当playBoyExtra.getOpenTime().longValue()值为null的时候,null.longValue()就抛出NPE,了解原理,再实验一下,直接把“0L”改为包装类型是不是就不会发生自动拆箱操作就不会抛出异常了呢?

Long openTime = null != playBoyExtra ? playBoyExtra.getOpenTime():Long.valueOf(0L);

验证一下,果然如此,实际上openTime被赋予了null值,程序执行并没有报错,但是这里虽然程序执行没有报错,但是这个结果显然不是程序的本意,程序本意是如果playBoyExtra.getOpenTime()不到值,那么就初始化openTime为0L,也就是我们更习惯的写法是:

Long openTime = null != playBoyExtra.getOpenTime() ? playBoyExtra.getOpenTime():Long.valueOf(0L);

至于playBoyExtra本身是否为空,其实前面已经判断过了。至此,事情水落石出,三目运算符是不是有一种“最熟悉的陌生人”的感觉?

消息风暴--好心办坏事

2022年3月30号晚7点25分,一阵急促的告警电话袭来,负责同学迅速查看系统,发现数据库连接池被同一个UPADATE的SQL语句产生行锁,导致连接池占满,应用系统无法访问数据库,线上大量系统异常。

5d6d06be46af146d3b49bb465f900a97.png

数据库相关的问题往往容易引发灾难性的重大问题,好在告警及时,迅速对这条SQL语句进行限流,DB连接池恢复,线上危机解除,但是必须找到根因才能算真正解除这个定时炸弹。线上代码变更在大多数情况下是引发故障的主要原因,通过最近的线上代码变更排查,日志排查,代码review等系列问题定位手段,最终发现问题出现一行代码上,下面是这行代码CR的截图:

66a86400765f7654ca4cc65d6002e784.png

粉红色部分表明是被删除的代码,绿色部分是更新后的代,他的核心差异是调用这个消息发送接口的时候,第二个和第三个参数进行了调换,为何会进行调换呢?让我们看一下producerManager.sendMessage方法的定义:

public boolean sendMessage(String topic, String tags, String message) {if (message == null || tags == null) {log.error("[MetaProducerManager] message|tags is null");return false;}return sendMessage(new Message(topic, tags, message.getBytes()));
}

这是对RocketMQ消息中间件的发送接口的封装,三个核心入参: 

topic:消息队列的topic,订阅者可以订阅特定topic的消息。

tags:消息队列细分tag,订阅者可以基于tags部分订阅此topic的消息。

massage:实际要发送消息body的内容。 

如果看过这个定义,大概就知道为何开发同学要修改原来的那行代码了,因为之前是把messageBody当做tags参数,而TAG_EDIT标识则传给了message消息提了,这是一个显而易见的参数传递错误的BUG,我们这位细心的程序员在开发其他代码的时候,看到了这个如此显而易见的bug,顺手就修复了,然而这次修复,却带来了灾难性的后果。可以用下面这个图示意理解一下:

9076b5847977ac89d7913b212a0527bd.png

在分布式架构下,应用之间常常通过RPC远程调用或者消息订阅的方式相互通信,这里应用A和应用B存在应用B对应用A的数据库变更的RocketMQ消息订阅,消息消费后,在某些特殊数据的处理逻辑下下会产生对于应用A的数据库UPDATE接口的同步RPC调用,而一旦产生update数据库的操作,又会产生数据库变更的RocketMQ消息,这时候应用A和B之间就产生了循环调用直至数据库被update操作hang住,为何原来不会出现这种情况呢,很简单,因为原来BUG的存在,导致红色的消息订阅链路是失效的,也就是根本没有办法正确消费消息,因此这个循环无法建立,当这个BUG被修正的时候,终于消息风暴降临。 

当然,其中有很多细节,就不一一表述,但是这种看到一个BUG,随手修掉的情况,真可谓“好心办坏事”。在面临一个复杂系统的时候,哪怕是修复一个显而易见的BUG,都要慎之又慎,必须要对整体链路和架构有充分判断和认知,才能尽量避免踩到前人的大坑中^_^。

页面卡顿--不一定是代码的锅

某个客户端页面,偶尔有用户舆情反馈页面会出现卡顿,客户端同学开展一系列排查工作,甚至直接联系用户,用完全一样的机型,相同版本,同样网络环境..各种手段都上了,然而始终无法复现,百思不得其解的情况下,拉服务端同学一起排查,然而一样一无所获,从服务端监控上看,接口RT非常稳定,几乎没有抖动,甚至没有用户相关的任何异常日志。直到某一天,忽然发现,似乎有问题的用户请求都落入到一个相同的EA119的机房,赶紧进行模拟验证:

476b54e777d62291fd08b4e6ae6da658.png

果不其然,几十倍的响应时长的差异!而要了解差异的来源,又免不了各种复杂分析,也略去不表,问题的根因的确不在于代码,而是网络请求链路的差异,下图是出问题的时候网络链路。

2b387358ba3a590ebd974d94c6ec20b1.png

我们为了系统容灾诉求,业务应用会进行多机房容灾部署,也就是业务服务器会部署在张北和南通等多个机房,但是在上图这个网络架构中,我们会发现客户端请求都会先到张北的Aserver(网络接入层),如果识别到是需要请求到南通机房的,会再进行一次转发到南通,而单元机房之间的网络传输必然带来较大的RT,当网络优化后,需要路由到南通机房的用户的网络请求直接请求南通机房的Aserver(网络接入层)以后,如下图所示:

631d32a591bb2a1d7cd99a7c98b22ced.png

整个世界都清净了! 

无论是客户端还是服务端,虽然没有修改一行代码,但是这个问题的解决时间那是相当长,包括问题根因排查也是耗时颇久,有时候,未必是代码的锅,程序员如果要成长为优秀的架构师,不仅对业务架构要了解,很多时候也需要对网络架构等更广泛的技术领域有一定的理解,才能在众多复杂问题中抽丝剥茧,一锤定音。 

PS:可能读者有个疑问,为何服务端监控看不到南通机房RT异常呢,原因很简单,服务端RT监控是应用层监控,也就是只监控了请求从进入业务应用到离开业务应用的时长,而真正耗时的是Aserver和业务应用间的时长。

后记

这三个问题都是线上真实发生的问题,从最基础的三目运算符,到相对复杂的分布式系统依赖和调用,再到更复杂的端到端网络架构,每一步都是程序员到架构师的修炼之路,在这技术日新月异的时代,保持强烈技术好奇心和持续学习的良好习惯,相信日积硅步足以致千里。如果能够完整读完这篇文章的你也有一点点收获,那么祝贺你又进步了一点点,顺祝您新年快乐,在新的一年里,兔氣揚眉,前兔似錦!

附录1:JSL 15.25:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25

c93af770d1bf80b85ef729e96562d1d7.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.exyb.cn/news/show-4557508.html

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

过圆外一点与圆的切线方程()

IntervalsTime Limit: 1000MS Memory Limit: 10000KTotal Submissions: 4046 Accepted: 1180 Description In the ceiling in the basement of a newly open developers building a light source has been installed. Unfortunately, the material used to cover the floor is

开启微信公众号服务器后面的开发,微信公众号开发者权限 开通接口与配置服务器...

微信公众号开发者权限 开通接口与配置服务器作为微信公众平台的开发者,是可以修改自定义菜单的。每个公众号下端都会有三个或者是四个菜单。关于菜单的内容以及显示的方式都是可以通过后台的开发者权限更改的。如果说这个平台直接是使用了第三方的软件的话&#xff…

年前最后一次分享5款小工具

马上要回家过年了,今年最后一次分享,希望大家喜欢。 1.图片管理器——Imagine 在管理器支持直接预览压缩包图片。支持图片编辑、图片批量转换、批量重命名、支持 GIF 动态图片编辑。如插入帧、修改帧的速度、循环播放、尺寸。同时还支持让系统右键菜单…

开通微信公众号流程所需资料及时间

2019独角兽企业重金招聘Python工程师标准>>> 序号 阶段 所需资料 所需时间 一、(企业)注册公众平台 使用未注册过微信公众号的邮箱注册、验证激活 即时二、 选择帐号类型 详情查看服务号、订阅号、企业号区别后选择类型 即时三、信息登记 选择…

「自控原理」3.3 稳定性与稳态误差、时域校正

本节介绍稳定性分析的原理以及代数稳定性判据(劳斯判据) 本节介绍系统稳态误差的定义及计算方法 本节介绍时域校正方法 文章目录稳定性分析稳定的充要条件与必要条件劳斯判据-Routh例题两种特殊情况问题辨析稳态误差误差与稳态误差的定义计算稳态误差的一…

C# 快速生成调用WMI查询的代码

有时需要在C#中调用WMI查询一些电脑信息,首先需要查到对应的WMI语句,然后还要组织C#代码,有点繁琐。 网上发现一个快速生成C#-WMI查询代码的工具,记录如下: WMI Code Creator https://www.microsoft.com/en-us/down…

Acwing---1237.螺旋折线

螺旋折线1.题目2.基本思想3.代码实现1.题目 如下图所示的螺旋折线经过平面上所有整点恰好一次。 对于整点 (X,Y)(X,Y)(X,Y),我们定义它到原点的距离 dis(X,Y)dis(X,Y)dis(X,Y) 是从原点到 (X,Y)(X,Y)(X,Y) 的螺旋折线段的长度。 例如 dis(0,1)3,dis(−2,−1)9di…

Ubuntu下源码编译VirtualBox二 —— 源码编译(1)

先打个预防针:在Ubuntu下编译VirtualBox可以说相当复杂。 1. 编译指导 (1)进入技术文档页面 在VirtualBox主页(Oracle VM VirtualBox)中,鼠标左键点击“Documentation”下的“Technical docs”&#xff0…