默认
打赏 发表评论 0
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
腾讯技术分享:Android手Q的线程死锁监控系统技术实践
阅读(82442) | 评论(0 收藏 淘帖1
微信扫一扫关注!

本文内容整理自公众号腾讯Bugly,感谢原作者的分享。


1、问题背景


手Q每个版本上线以后研发同学都会收到各种问题反馈。

在跟进手Q内部用户反馈的问题时,发现多例问题,其表象和原因如下:

  • 1)问题表象:“未读不消失”、“图片不展示”、“菊花一直在转” 。。。
  • 2)问题原因:死锁导致的功能不可用。

这类由死锁造成的功能不可用的问题,具有表象简单但影响非常严重的特点。一般用户在遇到这类问题后,除了采取杀掉进程重启的策略,没有其他办法继续使用应用。由此可见,死锁问题对产品的影响是巨大的,那么有没有有效的方法能够监控Android应用的死锁呢?

首先想到的是使用代码规范来避免死锁的发生。手Q有250多个业务模块,400w+行代码,这么多业务代码交叉调用,仅通过代码规范,很难避免死锁发生。

然后想到的是CodeDog的代码工具扫描。与CodeDog同事沟通后,发现Converity静态扫描无法识别嵌套调用使用锁的情况,而锁的嵌套调用是死锁发生场景中一个比较常见的场景。显然通过代码的静态扫描没法解决问题。

既然现成的代码扫描工具无法完全解决问题,只能硬着头皮试着自己造轮子来监控Android线程的死锁问题。

本文将详细介绍Android版手Q中这套线程卡死监控系统设计思路以及技术实践总结。(注意这里用的词是“卡死”而不是死锁。死锁只是线程卡死原因中比较重要的分类,除了死锁还有许多其他问题造成线程的卡死。)

2、相关文章


微信团队原创分享:iOS版微信的内存监控系统技术实践

3、整体方案概述


下面是手Q自建Android线程卡死监控的整体方案:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_1.jpg

Android线程监控整体方案分为两部分:客户端与后台。

1)客户端:由监控线程(WatchThread)与被监控线程(Thread)组成。

客户端线程的监控的核心主要利用线程的Looper方法,监控线程监控从被监控线程消息队列取出的消息的执行。当监控线程发现一个出队列的消息在一定时间(3分钟,可以自己设定)没有执行完成,则可认定被监控的线程发生卡死。这时,由监控线程获取线程持有、等待的阻塞锁信息,将这些信息上报到后台,客户端部分便完成了使命。

2)后台:由自动化分析工具,分析线程卡死的原因。原因分析完成以后,就可以进行提单,然后再跟进每个卡死问题的解决。

整个方案的关键是上图中两个标红的部分:客户端上报线程卡死的关键信息,后台自动化分析卡死原因。下面将详细介绍这两部的实现。

4、方案实现之客户端上报


4.1卡死信息


Android线程卡死监控中客户端上报线程卡死的关键信息,那么哪些信息是关键信息呢?答案很明显:线程信息与线程持有、等待锁的信息。

在如何获取这两类信息之前,先来分析一下Java中锁的分类与特点。

Java中锁的分类有自旋锁、可重入锁、阻塞锁等等分类,其中能够造成线程卡死的锁,只有阻塞锁。

对于阻塞锁有如下三种:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_2.jpg

根据这三种锁是否有线程等待方和线程持有方,为了达到分析线程卡死的目标,需要获取如下信息:

  • 1)对于sychronized锁:需要获取其持有线程和等待线程;
  • 2)对于LockSupport锁:需要获取其持有线程和等待线程;
  • 3)对于Object锁:需要获取其等待线程。

那么接下来的重点就是如何获取线程的信息与锁的信息上报。

4.2上报方案1:抓取java堆栈——不可行


首先想到的方案是抓取java的堆栈进行上报。

下面是抓取的java堆栈与其对应的代码:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_3.jpg

上图中右的代码中121行已经获取了sychornized锁,但是左边的java堆栈中并没有展示对应锁的信息,故使用抓取java堆栈的方式不可行

既然使用Java抓取堆栈信息不可行,有没有其他方案呢?答案:有。

4.3上报方案2:抓取系统Traces.txt——可行


既然抓取java的堆栈行不通,只能寻求其他解决方案。

突然想到,Android在发生ANR时有一套系统机制:

  • 1)Android应用发生ANR时,系统会发出SIGQUIT信号给发生ANR进程;
  • 2)系统信号捕捉线程触发输出/data/anr/traces.txt文件,记录问题产生虚拟机、线程堆栈相关信息;
  • 3)这个trace文件中包含了线程信息和锁的信息,借助这个trace文件可以分析卡死的原因。

由此,如果利用这个系统原有的机制,自己在线程卡死时候触发traces文件的形成进行上报,便可以把线程卡死的关键进行进行上报。

本监控方案便是利用系统机制进行卡死信息的抓取:

  • 1)当监控线程发现被监控线程卡死时,主动向系统发送SIGQUIT信号;
  • 2)等待/data/anr/traces.txt文件生成;
  • 3)文件生成以后进行上报;
  • 4)当然,这里也有少数Trace文件生成失败的情况,但是,对于手Q大盘监控可忽略。

既然方案可行,就需要分析利用系统机制抓取的信息(所有线程信息、线程堆栈中锁的信息)是否满足需求。

下面是一个利用系统机制继续抓取的例子:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_4.jpg

右图代码中的synchonized锁信息已经在左边系统dump的堆栈中,由此可见,可以利用这个堆栈进行卡死分析。那是否利用这些信息就足够进行线程卡死原因的分析了呢?

天下永远没有这么便宜的晚餐。

4.4上报难点:Traces中没有LockSupport锁的持有者信息


通过分析上报的Traces文件与抓取锁信息对比,sychornized、object锁的信息得到解决,但是LockSupport锁的信息竟然缺少了持有线程。

利用系统机制抓取的堆栈,可以获取锁的信息如下表所示:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_5.jpg

下面是LockSupport锁无法获取持有线程信息的一个例子:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_6.jpg

右图的代码在执行lock.lock()之后,线程已经获取了LockSupport锁,但是从左边的系统堆栈中却没有这个锁的信息。

这将是后续进行自动化分析的一个难点问题。那么有没有什么解决方案?通过深入分析手Q的代码,找到了答案。

4.5解决方案:主动记录LockSupport锁的线程信息


解决问题的思路,先人工分析所有上报trace文件,需找关键特征:

  • 1)找到发生死锁时系统dump堆栈的关键特征;
  • 2)人工对卡死问题聚类分析,发现卡死堆栈中locksupport锁的问题全部为数据库问题;
  • 3)再分析手Q的数据库相关代码,发现数据库事务LockSupport锁的入口统一,能够记录获取、等待获取的线程信息。

在上面的分析结论中,为了解决LockSupport没有持有线程信息这个难点,利用发现问题统一、程序代码入口统一的特点,采取下面这一招:
在系统dump的Trace文件中人工记录LockSupport锁信息。

解决方案:在数据库相关的代码中添加如下记录代码:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_7.jpg

上述代码中,将等待获取LockSupport锁线程记录到等待列表中,获取LockSupport锁以后从等待列表中移除,并记录当前线程(记录当前线程id、name信息)为LockSupport锁的持有线程,当前线程释放LockSupport锁以后再将记录清空。

有了上述记录的LockSupport锁线程信息,只要在卡死形成的traces文件最后添加这些信息,然后再进行上报,这样就解决了没有LockSupport锁持有线程信息的问题。

在traces文件最后一行添加的具体如下图所示:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_8.jpg

当然,为了方便后续的问题分析,在trace文件最后一行还添加了其他一些信息,如:被卡死线程的名称、系统版本号、发生时间等等。

目前,客户端已经解决了线程卡死以后上报信息不完整的问题,那么,接下来的重点就是要识别这些卡死的原因,下面章节将详细道来。

5、实现方案之服务端识别


5.1识别方案:关键信息上报,自动化分析


服务端识别方案可概述为:关键信息上报,自动化分析。

关键信息上报这是自动化分析的前提。

在详述自动化分析方案之前,首先回顾一下到目前为止已经获取到的信息:

  • 1)Traces.txt:所有线程信息、系统dump锁信息(synchonized、Object、LockSupport);
  • 2)主动记录的LockSupport锁信息(持有线程);
  • 3)主动识别到被卡死线程。

有了这三个关键的信息,接下来就是进行线程卡死的自动化分析。

下面是自动化卡死分析的完整方案:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_9.jpg

上图的整体识别方案,详细步骤说明如下:

  • 1)客户端将主动记录的LockSupport锁信息、被卡死线程信息等添加到系统dump的trace文件最后一行进行上报;
  • 2)服务端进行自动化分析时,首先进行预处理:LockSupport锁等待线程信息预处理、线程堆栈proguard还原;
  • 3)从trace文件中提取所有线程持有、等待的锁信息,记录到每个线程中;
  • 4)提取trace文件最后一行记录的LockSupport锁持有线程,从第3步分析的所有线程中找到该线程,并在线程持有锁中加入LockSupport锁;
  • 5)接下来从trace文件最后一行提取出被卡死线程,从被卡死的线程开始分析;
  • 6)被卡死线程是否有等待锁,如果无,则判定为非死锁,进入第12步进行卡死原因分析;
  • 7)如果有等待锁,找到该等待锁的持有线程;
  • 8)该持有线程是否有等待锁,如果无,则判定为非死锁,进入第12步进行卡死原因分析;
  • 9)如果有等待锁,判断该线程是否已经在遍历列表中;
  • 10)如果已经中遍历列表中,判断是否存在锁列表循环,如果是,则判定为死锁;
  • 11)如果没有中遍历列表中,将该线程加入遍历列表中,进入第7步进行循环;
  • 12)非死锁原因分析。目前分为:网络、文件IO、HashMap、系统IPC、抓栈、GC、数据库、ProcessManager、PB、后续扩展。

完成上述自动化分析以后,输出线程卡死问题列表。对于什么是死锁,下面将举例说明。

5.2死锁举例:两个线程互相等待对方已获取的锁导致死锁


首先,看发生死锁的两个线程堆栈。

1)MSF-Receiver线程堆栈:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_10.jpg
MSF-Receiver线程已经获取数据库LockSupport锁,等待获取0x18087549锁。

2)QQ_DB线程堆栈:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_11.jpg
QQ_DB线程已经获取0x18087549锁,等待获取数据库LockSupport锁。

将两个线程获取与等待获取的锁做成一个列表,如下表所示:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_12.jpg

从上表可看出:MSF-Receiver线程与QQ_DB线程互相等待对方已获取的锁,他们之间存在锁列表环,则判定为死锁

从这个死锁的列子发现,要做自动化分析死锁,只要能够找到线程之间存在锁列表循环就可以判定为死锁。那么自动化分析是否如想象中那么容易呢?其实不然,在自动化分析过程中,遇到了几个难点问题。

5.3识别难点1:LockSupport等待锁的阻塞地址不同


在自动化分析过程中,发现如下问题:

  • 1)系统dump的线程堆栈中有等待数据库事务的LockSupport锁对象信息;
  • 2)但是同一个LockSupport锁,不同线程阻塞时的对象地址不同。

上面两点是什么意思?

还是按照惯例,先看两个线程堆栈:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_13.jpg

手Q代码中所有数据库操作,都由同一个LockSupport锁来控制。但是,从上面的系统堆栈来看,Recent_Handler线程阻塞的LockSupport锁对象地址为0x43810d48,而QQ_SUB线程阻塞的LockSupport锁对象地址为0x41fcb538。

由此可见,对于同一个LockSupport锁,不同线程阻塞时的对象地址不同。要进行自动化分析,如果对于同一个锁必须要识别为同一个对象,可是上述堆栈表现却不能完成这个任务。怎么解决?

5.4解决方案: 提取特征,判定为同一个锁


LockSupport锁提供调度线程阻塞与唤醒功能。不同线程都与各自LockSupport锁的许可关联,所以造成了不同线程阻塞对象地址不同。

既然这个问题是由于LockSupport锁的实现原理决定,那么是否有解决方案呢?答案是:有。即如果有办法能够让这些不同的地址都指向同一个地址,只要能够做到这件事情,那么问题就迎刃而解。

具体的解决方案的分析思路如下:

  • 1)既然阻塞的LockSupport锁对象地址是不同的,那么是否可以寻找系统堆栈中LockSupport锁对象之前的有没有什么共同特征呢?
  • 2)分析所有dump的堆栈后发现,在系统堆栈的LockSupport锁对象之前,有相同的函数调用,可以提取这些关键的字符串,将其统一特征进行归类。
  • 3)在进行自动化分析时,只要发现系统堆栈中有这个字符串特征,便在当前分析线程锁列表中,加入一个人为构造的地址相同的LockSupport锁。

具体构造如下:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_14.jpg

上图中提取了字符串“SQLiteConnectionPool.waitForConnection”为等待LockSupport锁的共同特征,在线程的等待锁中加入同一个“dbconnection”锁。这样就解决了阻塞在同一个LockSupport锁,不同线程阻塞时的对象地址不同的难点。

5.5识别难点2:非死锁问题


对于非死锁问题,人工分析所有上报后,发现卡死原因分类较多,大致可分为:
  • 1)网络、文件IO、HashMap、系统IPC、抓栈、GC、数据库、ProcessManager、PB;
  • 2)后续仍然可扩展分类。

对于非死锁问题的判定,这里提出基于堆栈关键词匹配来判定问题分类的方案:

  • 首先:预设堆栈关键字与问题分类对应关系,如:Scoket等;
  • 其次:对未识别的类别的堆栈聚类,人工分析top问题找出关键字符串进行扩展;
  • 再次:完善堆栈关键字与问题类别对应关系。

下表示展示了目前已归纳总结出的卡死问题与对应判定关键字:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_15.jpg

重点案例: HashMap卡死(在分析系统dump时,发现多线程访问HashMap会造成卡死,具体卡死原因如下):

  • 1)HashMap在插入元素过多需要进行Resize步骤;
  • 2)Resize步骤包括扩容与ReHash;
  • 3)ReHash在并发的情况下可能会形成链表环;
  • 4)访问HashMap链表环这个位置时造成卡死。

解决方案:

  • 1)保证同一时间只有单个线程访问HashMap;
  • 2)多线程场景使用ConcurrentHashMap。

6、监控方案的运行效果


卡死自动化分析后,会输出卡死问题概览列表。

以11月7号为例,手Q自动化分析的线程卡死以后,输出以下列卡死问题概览:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_16.jpg

通过以上问题概览,可以看到每类卡死问题的个数与占比,方便问题总结。

在这些问题中:

  • 1)死锁问题占比最高,达到35.6%,已经全部解决。
  • 2)其他非死锁问题:
    • - 已经解决:IO、HashMap、网络调用;
    • - 暂未解决:IPC、ProcessManager、PB、抓栈、GC、文件IO等。

通过几个版本的监控与问题的解决,取得了良好的效果,如下所示:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践_17.jpg

正如上图所示:

  • 1)MSF线程卡死率0.3%降低到0.1%,公用线程卡死率0.51%降低到0.18%;
  • 2)730一灰APM使用公用线程造成卡死问题严重,及时发现并推进解决。

总的来说,通过线程卡死的监控与自动化分析,以及对发现卡死问题的及时解决,手Q线程卡死率逐版本下降,卡死问题得到有效控制。

7、后续规划


目前线程卡死监控任然有一些需要完善的地方:

  • 1)目前还是需要中自动化分析问题后进行人工提单,后续需要做到自动化提单;
  • 2)未完全记录LockSupport锁持有信息,目前只是记录数据库,对于其他使用LockSupport锁没有监控。后续采用Hook LockSupport锁的方案,记录完整信息,完善死锁监控问题定位能力。

附录:关微信、QQ的文章汇总


[1] QQ、微信团队原创技术文章:
腾讯技术分享:Android手Q的线程死锁监控系统技术实践
微信团队原创分享:iOS版微信的内存监控系统技术实践
让互联网更快:新一代QUIC协议在腾讯的技术实践分享
iOS后台唤醒实战:微信收款到账语音提醒技术总结
腾讯技术分享:社交网络图片的带宽压缩技术演进之路
微信团队分享:视频图像的超分辨率技术原理和应用场景
微信团队分享:微信每日亿次实时音视频聊天背后的技术解密
QQ音乐团队分享:Android中的图片压缩技术详解(上篇)
QQ音乐团队分享:Android中的图片压缩技术详解(下篇)
腾讯团队分享:手机QQ中的人脸识别酷炫动画效果实现详解
腾讯团队分享 :一次手Q聊天界面中图片显示bug的追踪过程分享
微信团队分享:微信Android版小视频编码填过的那些坑
微信手机端的本地数据全文检索优化之路
企业微信客户端中组织架构数据的同步更新方案优化实战
微信团队披露:微信界面卡死超级bug“15。。。。”的来龙去脉
QQ 18年:解密8亿月活的QQ后台服务接口隔离技术
月活8.89亿的超级IM微信是如何进行Android端兼容测试的
以手机QQ为例探讨移动端IM中的“轻应用”
一篇文章get微信开源移动端数据库组件WCDB的一切!
微信客户端团队负责人技术访谈:如何着手客户端性能监控和优化
微信后台基于时间序的海量数据冷热分级架构设计实践
微信团队原创分享:Android版微信的臃肿之困与模块化实践之路
微信后台团队:微信后台异步消息队列的优化升级实践分享
微信团队原创分享:微信客户端SQLite数据库损坏修复实践
腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率
腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)
腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)
微信Mars:微信内部正在使用的网络层封装库,即将开源
如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源
开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]
微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
Android版微信从300KB到30MB的技术演进(PPT讲稿) [附件下载]
微信团队原创分享:Android版微信从300KB到30MB的技术演进
微信技术总监谈架构:微信之道——大道至简(演讲全文)
微信技术总监谈架构:微信之道——大道至简(PPT讲稿) [附件下载]
如何解读《微信技术总监谈架构:微信之道——大道至简》
微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]
微信异步化改造实践:8亿月活、单机千万连接背后的后台解决方案
微信朋友圈海量技术之道PPT [附件下载]
微信对网络影响的技术试验及分析(论文全文)
一份微信后台技术架构的总结性笔记
架构之道:3个程序员成就微信朋友圈日均10亿发布量[有视频]
快速裂变:见证微信强大后台架构从0到1的演进历程(一)
快速裂变:见证微信强大后台架构从0到1的演进历程(二)
微信团队原创分享:Android内存泄漏监控和优化技巧总结
全面总结iOS版微信升级iOS9遇到的各种“坑”
微信团队原创资源混淆工具:让你的APK立减1M
微信团队原创Android资源混淆工具:AndResGuard [有源码]
Android版微信安装包“减肥”实战记录
iOS版微信安装包“减肥”实战记录
移动端IM实践:iOS版微信界面卡顿监测方案
微信“红包照片”背后的技术难题
移动端IM实践:iOS版微信小视频功能技术方案实录
移动端IM实践:Android版微信如何大幅提升交互性能(一)
移动端IM实践:Android版微信如何大幅提升交互性能(二)
移动端IM实践:实现Android版微信的智能心跳机制
移动端IM实践:WhatsApp、Line、微信的心跳策略分析
移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)
移动端IM实践:iOS版微信的多设备字体适配方案探讨
信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑
腾讯信鸽技术分享:百亿级实时消息推送的实战经验
>> 更多同类文章 ……

[2] 有关QQ、微信的技术故事:
2017微信数据报告:日活跃用户达9亿、日发消息380亿条
腾讯开发微信花了多少钱?技术难度真这么大?难在哪?
技术往事:创业初期的腾讯——16年前的冬天,谁动了马化腾的代码
技术往事:史上最全QQ图标变迁过程,追寻IM巨人的演进历史
技术往事:“QQ群”和“微信红包”是怎么来的?
开发往事:深度讲述2010到2015,微信一路风雨的背后
开发往事:微信千年不变的那张闪屏图片的由来
开发往事:记录微信3.0版背后的故事(距微信1.0发布9个月时)
一个微信实习生自述:我眼中的微信开发团队
首次揭秘:QQ实时视频聊天背后的神秘组织
>> 更多同类文章 ……

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:腾讯2017年度代码报告:程序员15500人、年撸码5亿行、手Q代码已过百万行下一篇:闲话即时通讯:腾讯的成长史本质就是一部QQ成长史

本帖已收录至以下技术专辑

推荐方案
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部