首页 > 其他分享 >大意了,一次多线程操作不当导致的线上事故...

大意了,一次多线程操作不当导致的线上事故...

时间:2023-04-06 17:09:25浏览次数:38  
标签:... System submit println 大意 多线程 ExecutorCompletionService 完大号 out

大家好,我是飘渺。

今天给大家分享个生产事故,一个由于多线程操作不当导致的线上事故,事情是这样的~

事故描述

从6点32分开始少量用户访问app时会出现首页访问异常,到7点20分首页服务大规模不可用,7点36分问题解决。

整体经过

6:58 发现报警,同时发现群里反馈首页出现网络繁忙,考虑到前几日晚上门店列表服务上线发布过,所以考虑回滚代码紧急处理问题。

7:07 开始先后联系XXX查看解决问题。

7:36  代码回滚完,服务恢复正常。

事故根本原因-事故代码模拟

public static void test() throws InterruptedException, ExecutionException {
    Executor executor = Executors.newFixedThreadPool(3);
    CompletionService<String> service = new ExecutorCompletionService<>(executor);
        service.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "HelloWorld--" + Thread.currentThread().getName();
            }
        });
}

根源就在于ExecutorCompletionService结果没调用take,poll方法。

正确的写法如下所示:

public static void test() throws InterruptedException, ExecutionException {
    Executor executor = Executors.newFixedThreadPool(3);
    CompletionService<String> service = new ExecutorCompletionService<>(executor);
    service.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "HelloWorld--" + Thread.currentThread().getName();
        }
    });
    service.take().get();
}

一行代码引发的血案,而且不容易被发现,因为oom是一个内存缓慢增长的过程,稍微粗心大意就会忽略,如果是这个代码块的调用量少的话,很可能几天甚至几个月后暴雷。

大意了,一次多线程操作不当导致的线上事故..._回滚

操作人回滚or重启服务器确实是最快的方式,但是如果不是事后快速分析出oom的代码,而且不巧回滚的版本也是带oom代码的,就比较悲催了,如刚才所说,流量小了,回滚或者重启都可以释放内存;但是流量大的情况下,除非回滚到正常的版本,否则GG。

探询问题的根源

为了更好的理解ExecutorCompletionService的 “套路” 我们用 ExecutorService来作为对比,可以让我们更好的清楚,什么场景下用ExecutorCompletionService。

先看ExecutorService代码(建议down下来跑一跑)

public static void test1() throws Exception{
    ExecutorService executorService = Executors.newCachedThreadPool();
    ArrayList<Future<String>> futureArrayList = new ArrayList<>();
    System.out.println("公司让你通知大家聚餐 你开车去接人");
  
    Future<String> future10 = executorService.submit(() -> {
        System.out.println("总裁:我在家上大号 我最近拉肚子比较慢 要蹲1个小时才能出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(10);
        System.out.println("总裁:1小时了 我上完大号了。你来接吧");
        return "总裁上完大号了";
    });
 
    futureArrayList.add(future10);
  
    Future<String> future3 = executorService.submit(() -> {
        System.out.println("研发:我在家上大号 我比较快 要蹲3分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("研发:3分钟 我上完大号了。你来接吧");
        return "研发上完大号了";
    });
    futureArrayList.add(future3);
  
    Future<String> future6 = executorService.submit(() -> {
        System.out.println("中层管理:我在家上大号  要蹲10分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(6);
        System.out.println("中层管理:10分钟 我上完大号了。你来接吧");
        return "中层管理上完大号了";
    });
    futureArrayList.add(future6);
  
    TimeUnit.SECONDS.sleep(1);
    System.out.println("都通知完了,等着接吧。");
  
    try {
        for (Future<String> future : futureArrayList) {
            String returnStr = future.get();
            System.out.println(returnStr + ",你去接他");
        }
        Thread.currentThread().join();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

三个任务,每个任务执行时间分别是 10s、3s、6s 。

通过JDK线程池的 submit 提交这三个 Callable类型的任务。

  • step1 主线程把三个任务提交到线程池里面去,把对应返回的 Future 放到 List 里面存起来,然后执行“都通知完了,等着接吧。”这行输出语句。
  • step2在循环里面执行 future.get() 操作,阻塞等待。

最后结果如下:

大意了,一次多线程操作不当导致的线上事故..._构造方法_02

先通知到总裁,也是先接总裁 足足等了1个小时,接到总裁后再去接研发和中层管理,尽管他们早就完事儿了,也得等总裁上完厕所~~

大意了,一次多线程操作不当导致的线上事故..._System_03

耗时最久的-10s异步任务最先进入list执行,所以在循环过程中获取这个10s的任务结果的时候,get操作会一直阻塞,直到10s异步任务执行完毕。即使 3s、5s的任务早就执行完了,也得阻塞等待10s任务执行完。

看到这里 尤其是做网关业务的同学可能会产生共鸣,一般来说网关RPC会调用下游N多个接口,如下图

大意了,一次多线程操作不当导致的线上事故..._System_04

如果都按照ExecutorService这种方式,并且恰巧前几个任务调用的接口耗时比较久,同时阻塞等待,那就比较悲催了。

所以ExecutorCompletionService应景而出。它作为任务线程的合理管控者,“任务规划师”的称号名副其实。

相同场景 ExecutorCompletionService代码

public static void test2() throws Exception {
    ExecutorService executorService = Executors.newCachedThreadPool();
    ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
    System.out.println("公司让你通知大家聚餐 你开车去接人");
    completionService.submit(() -> {
        System.out.println("总裁:我在家上大号 我最近拉肚子比较慢 要蹲1个小时才能出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(10);
        System.out.println("总裁:1小时了 我上完大号了。你来接吧");
        return "总裁上完大号了";
    });
    completionService.submit(() -> {
        System.out.println("研发:我在家上大号 我比较快 要蹲3分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("研发:3分钟 我上完大号了。你来接吧");
        return "研发上完大号了";
    });
    completionService.submit(() -> {
        System.out.println("中层管理:我在家上大号  要蹲10分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(6);
        System.out.println("中层管理:10分钟 我上完大号了。你来接吧");
        return "中层管理上完大号了";
    });
    TimeUnit.SECONDS.sleep(1);
    System.out.println("都通知完了,等着接吧。");
  
    //提交了3个异步任务)
    for (int i = 0; i < 3; i++) {
        String returnStr = completionService.take().get();
        System.out.println(returnStr + ",你去接他");
    }
    Thread.currentThread().join();
}

跑完结果如下:

大意了,一次多线程操作不当导致的线上事故..._构造方法_05

这次就相对高效了一些,虽然先通知的总裁,但是根据大家上大号的速度,谁先拉完先去接谁,不用等待上大号最久的总裁了(当然现实生活里 建议采用第一种 不等总裁的后果 emmm 哈哈哈)。

放在一起对比下输出结果:

大意了,一次多线程操作不当导致的线上事故..._System_06

两段代码的差异非常小 ,获取结果的时候ExecutorCompletionService 使用了

completionService.take().get();

那为什么要用take() 然后再get()呢????

我们看看源码

CompletionService接口 以及接口的实现类

1、ExecutorCompletionService是CompletionService接口的实现类

大意了,一次多线程操作不当导致的线上事故..._构造方法_07

2、接着跟一下ExecutorCompletionService的构造方法,可以看到入参需要传一个线程池对象,默认使用的队列是 LinkedBlockingQueue,不过还有另外一个构造方法可以指定队列类型,如下两张图,两个构造方法。

默认LinkedBlockingQueue的构造方法

大意了,一次多线程操作不当导致的线上事故..._回滚_08

可选队列类型的构造方法

大意了,一次多线程操作不当导致的线上事故..._System_09

3、submit任务提交的两种方式,都是有返回值的,我们例子中用到的就是第一种Callable类型的方法。

大意了,一次多线程操作不当导致的线上事故..._构造方法_10

4、对比ExecutorService 和 ExecutorCompletionService submit方法 可以看出区别

(1)ExecutorService

大意了,一次多线程操作不当导致的线上事故..._回滚_11

(2)ExecutorCompletionService

大意了,一次多线程操作不当导致的线上事故..._构造方法_12

5、差异就在 QueueingFuture,这个到底作用是啥,我们继续跟进去看

  • QueueingFuture 继承自 FutureTask,而且红线部分标注的位置,重写了done()方法。
  • 把 task 放到 completionQueue 队列里面,当任务执行完成后,task就会被放到队列里面去了。
  • 此时此刻completionQueue队列里面的 task 都是已经 done()完成了的 task,而这个 task 就是我们拿到的一个个的future结果。
  • 如果调用 completionQueue 的 task 方法,会阻塞等待任务。等到的一定是完成了的 future,我们调用 .get()方法 就能立马获得结果。

大意了,一次多线程操作不当导致的线上事故..._构造方法_13

看到这里 相信大家伙都应该多少明白点了

  • 我们在使用ExecutorService submit提交任务后需要关注每个任务返回的future,然而CompletionService 对这些 future 进行了追踪,并且重写了done方法,让你等的completionQueue 队列里面 一定是完成了的task。
  • 作为网关RPC层,我们不用因为某一个接口的响应慢拖累所有的请求,可以在处理最快响应的业务场景里使用CompletionService。

but 注意、注意、注意 也是本次事故的核心

当只有调用了ExecutorCompletionService下面的3个方法的任意一个时,阻塞队列中的task执行结果才会从队列中移除掉,释放堆内存,由于该业务不需要使用任务的返回值,则没进行调用take,poll方法。从而导致没有释放堆内存,堆内存会随着调用量的增加一直增长。

大意了,一次多线程操作不当导致的线上事故..._回滚_14

所以,业务场景中不需要使用任务返回值的 别没事儿使用CompletionService,假如使用了,记得一定要从阻塞队列中 移除掉task执行结果,避免OOM!

总结

知道事故的原因,我们来总结下方法论,毕竟孔子他老人家说过:自省吾身,常思己过,善修其身!

大意了,一次多线程操作不当导致的线上事故..._System_15

上线前:

  • 严格的代码review习惯,一定要交给back人去看,毕竟自己写的代码自己是看不出问题的,相信每个程序猿都有这个自信(这个后续事故里可能会反复提到!很重要)
  • 上线记录-备注好上一个可回滚的包版本(给自己留一个后路)
  • 上线前确认回滚后,业务是否可降级,如果不可降级,一定要严格拉长这次上线的监控周期

上线后:

  • 持续关注内存增长情况(这部分极容易被忽略,大家对内存的重视度不如cpu使用率)
  • 持续关注cpu使用率增长情况
  • gc情况、线程数是否增长、是否有频繁的fullgc等
  • 关注服务性能报警,tp99、999 、max是否出现明显的增高

 

标签:...,System,submit,println,大意,多线程,ExecutorCompletionService,完大号,out
From: https://blog.51cto.com/u_15989526/6173728

相关文章

  • 【重要】Nginx模块Lua-Nginx-Module学习笔记(三)Nginx + Lua + Redis 已安装成功(非open
    一、目标使用Redis做分布式缓存;使用luaAPI来访问redis缓存;使用nginx向客户端提供服务,ngx_lua将lua嵌入到nginx,让nginx执行lua脚本,高并发,非阻塞的处理各种请求。url请求nginx服务器,然后lua查询redis,返回json数据。二、准备工作系统环境:Ubuntu14.0(64位)Redis服务安装:ap......
  • 设计模式之————依赖注入(Dependency Injection)与控制反转(Inversion of Controll
     参考链接:依赖注入(DI)or控制反转(IoC)laravel学习笔记——神奇的服务容器PHP依赖注入,从此不再考虑加载顺序名词解释IoC(Inversion of Controller) 控制反转(概念)DI(Dependency Inject) 依赖注入(IoC概念中的一种类型实现)通过依赖声明自动实例化依赖的类(通常通过反......
  • 【精选】Nginx负载均衡学习笔记(一)实现HTTP负载均衡和TCP负载均衡(官方和OpenResty两种
    说明:很简单一个在HTTP模块中,而另外一个和HTTP是并列的Stream模块(Nginx1.9.0支持)一、两个模块的最简单配置如下1、HTTP负载均衡:http{includemime.types;default_typeapplication/octet-stream;upstreamlive_node{server127.0.......
  • 流媒体技术学习笔记之(四)解决问题video.js 播放m3u8格式的文件,根据官方的文档添加vide
    源码地址:https://github.com/Tinywan/PHP_Experience总结:说明:测试环境:本测试全部来自阿里云直播和OSS存储点播以及本地服务器直播和点播播放器:VideoJs直播:1、阿里云直播,需要CDN设置HTTP头2、本地直播需要设置直播访问服务器的头部信息(本地为Nginx)add_header'Access-......
  • Redis核心知识之—— 时延问题分析及应对、性能问题和解决方法【★★★★★】...
     参考网址:Redis常见的性能问题和解决方法:http://www.searchdatabase.com.cn/showcontent_63439.htmRedis主从配置详细过程:http://sofar.blog.51cto.com/353572/861276 读后感:1、在架构设计中,有“分流”一招,说的是将处理快的请求和处理慢的请求分离来开,否则,慢的影响到了快的,让快的......
  • Docker yum install的时候报错:Rpmdb checksum is invalid: dCDPT(pkg checksums): ...
    闲话就不说了,直接上Dockerfile:FROMhub.c.163.com/library/centos:7.2.1511MAINTAINERbyzsk_johnRUNyum-yinstallvimnet-tools&&yumcleanallEXPOSE22CMD["/bin/bash","-D"]注意一点,如果拆开写RUN,也就是yuminstallvim-y&&yuminst......
  • IOS多线程之NSOperation(2)
    IOS多线程之NSOperation(2)最大并发数openvarmaxConcurrentOperationCount:Int并发数就是同时执行的任务数。比如,同时开3个线程执行3个任务,并发数就是3。但是,并发数是3,并不代表开启的线程数就是3,也有可能是4个或者5个。因为线程有可能在等待,进入了就绪状态。执行的过程:......
  • IOS多线程之NSOperation(3)
    IOS多线程之NSOperation(3)操作优先级和服务质量可以通过QueuePriority属性来设置operation在队列中的执行优先级publicenumQueuePriority:Int,@uncheckedSendable{caseveryLow=-8caselow=-4casenormal=0casehigh=4caseveryHigh......
  • Java并发和多线程4:使用通用同步工具CountDownLatch实现线程等待
    CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 用给定的计数初始化CountDownLatch。由于调用了countDown()方法,所以在当前计数到达零之前,await方法会一直受阻塞。之后,会释放所有等待的线程,awai......
  • IOS多线程之NSOperation(1)
    IOS多线程之NSOperation(1)NSOperation是OC语言中基于GCD的面向对象的封装;提供了一些用GCD不好实现的功能;线程的生命周期由系统自动管理。NSOperation需要和NSOperationQueue配合使用来实现多线程方案。单独使用NSOperation的话,它是属于同步操作,并不具备开......