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

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

    每一起严重事故的背后必然有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和业务应用间的时长。

    后记

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

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

    c93af770d1bf80b85ef729e96562d1d7.png

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6