Skip to content

生产事件

1、线上pod死锁

1.1、问题描述

事情是这样的,在2023年10月19日的晚上七点左右,调用B服务RPC接口的其他服务,都陆续开始报【接口调用超时异常】,B服务已经有一个多月没有上线过了,而出故障的时间当天,流量也没陡增。最后触发pod自动重启。

重启后错误信息立马消失了,一直到当天凌晨,都没有再报错了。

1.2、问题定位

这种突然出问题,但跟流量和发版又没有关系的,大概率是触发某个隐藏的bug导致服务慢慢不可用了。

查看日志

错误信息,集中在B服务的某个pod上,有相当多线程block住了。

查看 pod dump文件

之前说过我们的系统pod配置了自动dump,我们从OSS上把DUMP文件下载下来看一下具体的报错信息

"thread_14" Id=xxxx BLOCKED on java.util.concurrent.ConcurrentHashMap$Node@687bfd0d owned by "Dubbo-thread-499" Id=1044
at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1027)
- blocked on java.util.concurrent.ConcurrentHashMap$Node@687bfd0d
at java.util.concurrent.ConcurrentHashMap.putIfAbsent(ConcurrentHashMap.java:1535)


"Dubbo-thread-499" Id=cccc BLOCKED on java.util.concurrent.ConcurrentHashMap$ReservationNode@2205946f owned by "thread_14" Id=yyy
at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1027)
- blocked on java.util.concurrent.ConcurrentHashMap$ReservationNode@2205946f
at java.util.concurrent.ConcurrentHashMap.putIfAbsent(ConcurrentHashMap.java:1535)
"thread_14" Id=xxxx BLOCKED on java.util.concurrent.ConcurrentHashMap$Node@687bfd0d owned by "Dubbo-thread-499" Id=1044
at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1027)
- blocked on java.util.concurrent.ConcurrentHashMap$Node@687bfd0d
at java.util.concurrent.ConcurrentHashMap.putIfAbsent(ConcurrentHashMap.java:1535)


"Dubbo-thread-499" Id=cccc BLOCKED on java.util.concurrent.ConcurrentHashMap$ReservationNode@2205946f owned by "thread_14" Id=yyy
at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1027)
- blocked on java.util.concurrent.ConcurrentHashMap$ReservationNode@2205946f
at java.util.concurrent.ConcurrentHashMap.putIfAbsent(ConcurrentHashMap.java:1535)

居然有死锁,发现是两个线程,在两个ConcurrentHashMap对象之间,相互等待了。

也就是说:

  • 线程thread_14在已获得某种资源后,还想继续获取687bfd0d对象的锁,而这把锁整被线程Dubbo-thread-499拿在手上;
  • 线程Dubbo-thread-499在已获得某种资源后,还想继续获取2205946f对象的锁,而这把锁整被线程thread_14拿在手上;

ConcurrentHashMap出现死锁,本地简单写了一段程序验证了一下:

java
public static void main(String[] args) {
    ConcurrentHashMap<String, Integer> map1 = new ConcurrentHashMap<>(16);
    ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>(16);

    new Thread(()->{
        map1.computeIfAbsent("a", key -> {
            map2.computeIfAbsent("b", key2 -> 2);
            return 1;
        });
    }).start();

    new Thread(()->{
        map2.computeIfAbsent("b", key -> {
            map1.computeIfAbsent("a", key2 -> 2);
            return 1;
        });
    }).start();
}
public static void main(String[] args) {
    ConcurrentHashMap<String, Integer> map1 = new ConcurrentHashMap<>(16);
    ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>(16);

    new Thread(()->{
        map1.computeIfAbsent("a", key -> {
            map2.computeIfAbsent("b", key2 -> 2);
            return 1;
        });
    }).start();

    new Thread(()->{
        map2.computeIfAbsent("b", key -> {
            map1.computeIfAbsent("a", key2 -> 2);
            return 1;
        });
    }).start();
}

在Intellij idea上运行上面的代码,并使用idea自带的Dump Threads功能,会发现真的触发死锁了。

image-20231020104648031

看了一下jdk 1.8的ConcurrentHashMap的computeIfAbsent源代码,在并发的情况下,确实有概率性会触发死锁。

image-20231020104711345

大概的执行序列是:

  • 1、生成ReservationNode预占节点;
  • 2、对该节点进行加锁(这里是重点),然后将该节点放入指定key的槽位中;
  • 3、执行我们传入的计算逻辑,当我们计算逻辑中包含有computeIfAbsent时,此时代码会重复上面的1~3步骤

到这里就大概明白了,当执行一次computeIfAbsent的嵌套逻辑时,会有两个ReservationNode对象会被加锁,那在并发的情况下,是可能会产生死锁的。

具体是哪行代码触发的呢? 其实日志是有完整打印出来的,由于有敏感信息,这里不能贴出来。但是触发的诱因可以说一下:

线程thread_14,是想更新一个用户的手机号信息,对应的代码逻辑会操作两个ConcurrentHashMap,先操作map1,再操作map2,这个两个map是作为本地缓存使用的,都会对其进行computeIfAbsent操作。而Dubbo-thread-499也是一样,也会操作这两个map,先操作map2,再操作map1。当有并发的情况下,处理的又是同一个手机号的时候,就可能触发死锁。

1.3、解决方案

定位到代码,重新梳理业务逻辑实现,发现 thread_14操作完map1这个本地缓存后,还要去操作map2这个本地缓存 是没有必要的。因为这两份本地缓存的数据,都有对应的业务逻辑代码去保证它的准确性。

解决这次的死锁的方案也很简单,就是断掉其中一条路,避免死锁就可以了。正如刚才上面分析的,两份本地缓存都有各自的业务逻辑去确保它的准确性,没必要顺手去更新别人家的缓存

修改代码,经过测试团队异常场景测试验证后,发版上线。问题解决!