联博以太坊:线程池运用欠妥的一次线上事故

admin 5个月前 (06-14) 科技 28 0

在高并发、异步化等场景,线程池的运用可以说无处不在。线程池从本质上来讲,即通过空间换取时间,由于线程的建立和销毁都是要消耗资源和时间的,对于大量使用线程的场景,使用池化治理可以延迟线程的销毁,大大提高单个线程的复用能力,进一步提升整体性能。

今天遇到了一个对照典型的线上问题,恰好和线程池有关,另外涉及到死锁、jstack下令的使用、JDK差别线程池的适合场景等知识点,同时整个观察思绪可以借鉴,特此纪录和分享一下。


01 营业靠山形貌

该线上问题发生在广告系统的焦点扣费服务,首先简朴交接下大致的营业流程,利便明白问题。

绿框部门即扣费服务在广告召回扣费流程中所处的位置,简朴明白:当用户点击一个广告后,会从C端提议一次实时扣费请求(CPC,按点击扣费模式),扣费服务则承接了该动作的焦点营业逻辑:包罗执行反作弊计谋、建立扣费纪录、click日志埋点等。


02 问题征象和营业影响

12月2号晚上11点左右,我们收到了一个线上告警通知:扣费服务的线程池义务行列巨细远远超出了设定阈值,而且行列巨细随着时间推移还在连续变大。详细告警内容如下:


响应的,我们的广告指标:点击数、收入等也泛起了异常显著的下滑,险些同时发出了营业告警通知。其中,点击数指标对应的曲线显示如下:

该线上故障发生在流量高峰期,连续了快要30分钟后才恢复正常。


03 问题观察和事故解决历程

下面详细说下整个事故的观察和剖析历程。

第1步:收到线程池义务行列的告警后,我们第一时间查看了扣费服务各个维度的实时数据:包罗服务调用量、超时量、错误日志、JVM监控,均未发现异常。

第2步:然后进一步排查了扣费服务依赖的存储资源(mysql、redis、mq),外部服务,发现了事故时代存在大量的数据库慢查询。

上述慢查询来自于事故时代一个刚上线的大数据抽取义务,从扣费服务的mysql数据库中大批量并发抽取数据到hive表。由于扣费流程也涉及到写mysql,预测这个时刻mysql的所有读写性能都受到了影响,果真进一步发现insert操作的耗时也远远大于正常时期。

第3步:我们预测数据库慢查询影响了扣费流程的性能,从而造成了义务行列的积压,以是决议立马暂定大数据抽取义务。然则很新鲜:住手抽取义务后,数据库的insert性能恢复到正常水平了,然则壅闭行列巨细仍然还在连续增大,告警并未消逝。

第4步:思量广告收入还在连续大幅度下跌,进一步剖析代码需要对照长的时间,以是决议马上重启服务看看有没有效果。为了保留事故现场,我们保留了一台服务器未做重启,只是把这台机械从服务治理平台摘掉了,这样它不会接收到新的扣费请求。

果真重启服务的杀手锏很管用,各项营业指标都恢复正常了,告警也没有再泛起。至此,整个线上故障得到解决,连续了也许30分钟。


04 问题根本缘故原由的剖析历程

下面再详细说下事故根本缘故原由的剖析历程。

第1步:第二天上班后,我们预测那台保留了事故现场的服务器,行列中积压的义务应该都被线程池处置掉了,以是实验把这台服务器再次挂载上去验证下我们的预测,效果和预期完全相反,积压的义务仍然都在,而且随着新请求进来,系统告警马上再次泛起了,以是又马上把这台服务器摘了下来。

第2步:线程池积压的几千个义务,经由1个晚上都没被线程池处置掉,我们预测应该存在死锁情形。以是计划通过jstack下令dump线程快照做下详细剖析。

#找到扣费服务的历程号
$ ps aux|grep "adclick"

# 通过历程号dump线程快照,输出到文件中
$ jstack pid > /tmp/stack.txth

在jstack的日志文件中,立马发现了:用于扣费的营业线程池的所有线程都处于waiting状态,线程所有卡在了截图中红框部门对应的代码行上,这行代码调用了countDownLatch的await()方式,即守候计数器变为0后释放共享锁。


第3步:找到上述异常后,距离找到根本缘故原由就很接近了,我们回到代码中继续观察,首先看了下营业代码中使用了newFixedThreadPool线程池,焦点线程数设置为25。针对newFixedThreadPool,JDK文档的说明如下:

建立一个可重用牢固线程数的线程池,以共享的无界行列方式来运行这些线程。若是在所有线程处于活跃状态时提交新义务,则在有可用线程之前,新义务将在行列中守候。

关于newFixedThreadPool,焦点包罗两点:

1、最大线程数 = 焦点线程数,当所有焦点线程都在处置义务时,新进来的义务会提交到义务行列中守候;

2、使用了无界行列:提交给线程池的义务行列是不限制巨细的,若是义务被壅闭或者处置变慢,那么显然行列会越来越大。

以是,进一步结论是:焦点线程所有死锁,新进的义务纰谬涌入无界行列,导致义务行列不停增添。


第4步:到底是什么缘故原由导致的死锁,我们再次回到jstack日志文件中提醒的那行代码做进一步剖析。下面是我简化事后的示例代码:

/*** 执行扣费义务 */
public Result<Integer> executeDeduct(ChargeInputDTO chargeInput) {  
    ChargeTask chargeTask = new ChargeTask(chargeInput);  
    bizThreadPool.execute(() -> chargeTaskBll.execute(chargeTask ));  
    return Result.success();
}

/*** 扣费义务的详细营业逻辑 */
public class ChargeTaskBll implements Runnable {  
    public void execute(ChargeTask chargeTask) {     
        // 第一步:参数校验     
        verifyInputParam(chargeTask);     

        // 第二步:执行反作弊子义务     
        executeUserSpam(SpamHelper.userConfigs);     

        // 第三步:执行扣费     
        handlePay(chargeTask);     

        // 其他步骤:点击埋点等     ...  
    }
}

/*** 执行反作弊子义务 */
public void executeUserSpam(List<SpamUserConfigDO> configs) {  
    if (CollectionUtils.isEmpty(configs)) {     
        return;  
    }  try {    
        CountDownLatch latch = new CountDownLatch(configs.size());    
        for (SpamUserConfigDO config : configs) {      
           UserSpamTask task = new UserSpamTask(config,latch);      
           bizThreadPool.execute(task);    
        }    
        latch.await();  
    } catch (Exception ex) {    
        logger.error("", ex);  
    }
}

通过上述代码,人人能否发现死锁是怎么发生的呢?根本缘故原由在于:一次扣费行为属于父义务,同时它又包含了多次子义务:子义务用于并行执行反作弊计谋,而父义务和子义务使用的是同一个营业线程池。当线程池中所有都是执行中的父义务时,而且所有父义务都存在子义务未执行完,这样就会发生死锁。下面通过1张图再来直观地看下死锁的情形:

假设焦点线程数是2,现在正在执行扣费父义务1和2。另外,反作弊子义务1执行完了,反作弊子义务2和4都积压在义务行列中守候被调剂。由于反作弊子义务2和4没执行完,以是扣费父义务1和2都不可能执行完成,这样就发生了死锁,焦点线程永远不可能释放,从而造成义务行列不停增大,直到程序OOM crash。

死锁缘故原由清晰后,另有个疑问:上述代码在线上运行很长时间了,为什么现在才露出出问题呢?另外跟数据库慢查询到底有没有直接关联呢?

暂时我们还没有复现证实,然则可以推断出:上述代码一定存在死锁的概率,尤其在高并发或者义务处置变慢的情形下,概率会大大增添。数据库慢查询应该就是导致此次事故泛起的导火索。


05 解决方案

弄清晰根本缘故原由后,最简朴的解决方案就是:增添一个新的营业线程池,用来隔离父子义务,现有的线程池只用来处置扣费义务,新的线程池用来处置反作弊义务。这样就可以彻底制止死锁的情形了。


06 问题总结

回首事故的解决历程以及扣费的手艺方案,存在以下几点待继续优化:

1、使用牢固线程数的线程池存在OOM风险,在阿里巴巴Java开发手册中也明确指出,而且用的词是『不允许』使用Executors建立线程池。 而是通过ThreadPoolExecutor去建立,这样让写的同砚能加倍明确线程池的运行规则和焦点参数设置,规避资源耗尽的风险。

2、广告的扣费场景是一个异步历程,通过线程池或者MQ来实现异步化处置都是可选的方案。另外,极个别的点击请求丢失不扣费从营业上是允许的,然则大批量的请求抛弃不处置且没有抵偿方案是不允许的。后续接纳有界行列后,拒绝计谋可以思量发送MQ做重试处置。--- 竣事 ---


- End -

作者简介:程序员,985硕士,前亚马逊Java工程师,现58转转手艺总监。连续分享手艺和治理偏向的文章。若是感兴趣,可微信扫描下面的二维码关注我的民众号:『IT人的职场进阶』

,

apple developer enterprise account for rent

providing apple enterprise developer accounts for rent, rent your own enterprise account for app signing. with high quality, stable performance and affordable price.

dafa888体育声明:该文看法仅代表作者自己,与本平台无关。转载请注明:联博以太坊:线程池运用欠妥的一次线上事故

网友评论

  • (*)

最新评论

文章归档

站点信息

  • 文章总数:738
  • 页面总数:0
  • 分类总数:8
  • 标签总数:1179
  • 评论总数:436
  • 浏览总数:22043