中文站

网易云捕性能踩坑解决之道上篇

  从零开始设计开发一个日处理数据8亿的大数据高并发实时系统,哪些性能问题需要特别注意?这里我们一起梳理一下,本文中我将以PE,SA同学戏称的DDOS系统—网易云捕设计开发实践中两年的时间里碰到的真实问题,踩过的坑及解决问题的方法和大家一起讨论如何解决这些问题。文中不会大谈特谈架构设计,只是会在提及问题出现的场景及解决方法时初略带过,没有场景的谈架构设计都是耍流氓。本文着重列举在云捕业务场景下我们碰到的一系列性能问题以及解决问题的思路,帮助一部分有类似场景的人少走弯路,抛砖引玉,欢迎各路大神批评指正。
   为了便于读者轻松的理解后续的描述,有必要开始之前先熟悉云捕的业务场景,我这里简明扼要的介绍一下。
   有统计显示,Crash的出现比率非常高:63%的用户碰到过移动APP crash,在首次启动碰到crash时,21%的用户会卸载App,另外,Crash发生在使用过程中,70%的用户会给予差评。Crash已经成为移动App开发的最大障碍,App开发中进行质量跟踪同样有很多难以解决的问题:用户投诉闪退,但却无法重现;QA投入大量时间测试,还是无法解决各类crash问题;Android机型太多太杂;产品要求快速上线,搭建一个crash收集平台耗时耗力;使用国外的产品,网络不好,崩溃上报问题很大并且使用其它公司的产品,数据安全又难以保证。在此情况下,网易云捕应运而生了,使命就是为了助力移动端的开发者打造高品质APP,精准捕捉APP的每一次质量问题。
   为了实现上述目的,在调研竞品后我们发现云捕需要实现以下功能:
     1 实时展示崩溃、异常、卡顿问题详情,设备信息,崩溃卡顿分类;
     2 实时准确统计分析各类崩溃、异常、卡顿问题次数、影响人数,启动人数,比率等多方位趋势图,实时报警让用户全面掌握产品质量状况 ;
     3 实时准确统计今日问题统计,今日Top3实时展示24小时Top3问题统计排序;
     4 实时准确统计每个崩溃累计的类型设备型号分布图,系统版本分布图;
     5 崩溃、卡顿堆栈实时自动还原;
     6 demo实时展示;
     7 解决方案实时推荐;
     8 用户自助接入,前期不需要审核;
 看上面的需求不难发现,有几个关键词大数据,高并发,实时,准确。为了很好的实现这个需求便有了下面的架构设计图:
 了解完业务场景,看完架构设计图,下面的例子就比较好理解了,为了系统性,下面一个个组件的介绍问题及解决方案。

   DDB篇:
   案例一:某周六,数据量急剧增加,收到哨兵报警:Cause: java.sql.SQLException: Get null from pool;
   解决方案:首先通过命令show process或者show status查看链接数,检查数据库连接数设置及释放正常后,问了其它同事也没有碰到这个问题, 联系DBA同学修改连接数后恢复正常。据DBA同学描述现在一个QS最大可支持1024个链接,可以通过增加QS的数量来提高链接;

   案例二:云捕需要在插入数据的时候根据数据类型实时生成一个类型,某日排查问题发现插入的记录条数和DDB节点数一致
   解决方案:仔细查阅ddb文档发现设置多节点ddb设置UNIQUE key插入不能保证唯一,只能在单节点的集群上保证唯一性。多节点集群需要业务方自己实现,利用redis提供的setnx来实现分布式锁后解决问题,读者也可以通过zookeeper来实现,请自行google;

   案例三:数据插入延迟比较高
   解决方案:1 请DBA同学升级DDB硬盘为SSD硬盘;
        2 批量插入替代单条插入,并且批量插入要控制好batch的数字否则会出现下面的异常;
   Cause: java.sql.SQLException: condition number is: 1124, exceed max value:1024 或者Cause: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (26124567 > 16777216). You can change this value on the server by setting the max_allowed_packet' variable.

可以事先通过控制台可以查到部分阈值,查不到的也可以联系DBA同学确认一下。

批量插入要控制batch的大小,确保参数个数和消息体大小都在合理的范围内
注意:数据库的再提升性能,毕竟性能在哪里,能不经过数据库的就不过,能用缓存扛的就用缓存。往往总是策略来得更有效果,有时候优化不下去了,我们是不是想想换一种思维方式,为了打开一个瓶子喝水,除了打开瓶子以为,还可以把塞子推进去,换种思路有时候会海阔天空。

   多线程篇:
   场景:云捕有两个对外的接口,分别用来接收十亿台左右的设备发上来的崩溃,卡顿,启动数据,并发非常大。
   案例一:某日收到哨兵报警,内存使用率100%,上服务器分析发现Java堆的eden区,survivor区,tenured区 全部堆满,接口服务处于将近瘫痪的状态,迅速dump文件后用mat分析发现队列里面塞满了对象,但是项目代码里面没有明显的使用队列。
   最后通过排查发现是因为使用多线程时excutor 内部使用了一个无界队列,如果数据处理不够快,大量的数据回堆积在内存,导致内存被撑爆,相关具体源代码如下:

分析






解决办法:使用定制的实例,设定队列长度,及拒接策略,为了保证数据都能被完整处理,云捕使用的拒绝策略是处理不过来时利用kafka的可堆积性重新扔回kafka中.


为了保证在重启实例时数据不丢失,这里还需要注册一个钩子到JVM实现JVM退出前先把本地队列中的数据消费掉.

   JVM和Tomcat篇:
    
 案例:报警项:TomcatCollector,报警内容:currentThreadsBusy数: 2000,currentThreadsBusyMax数:2000
     解决方案:Tomcat默认使用BIO模式,调整为NIO模式.并调整代码,加解密,预处理等等统一移到数据处理模块,接口模块只负责接收数据,接收数据以后啥也不做,直接扔到Kafka集群中,数据处理模块再从Kafka集群中提取数据排队处理.Tomcat三种运行模式及使用场景:

  • BIO: 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善,适用连接数较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中。。
  • NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理,适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
  • AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。

   Kafka篇:
     案例一: 数据大幅度增加后部分用户反馈发送消息超时
     
调整为如下:

      解决方案: kafka的QPS据说最大能达到百万级别的QPS,但是经排查问题就出在这里,同步改异步,kafka支持批量发送数据后问题解决。  
     案例二:kafka.common.FailedToSendMessageException: Failed to send messages after 3 tries.

     解决方案:这个和kafka brocker设置有关,通过分析发现一部分上报的消息体过大,甚至达到2M,DBA专家这边后台也没有查到任何异常,但是大量的消息发送失败。通过反复Google后我得到一个信息:kafka消息体为10K的时候性能最佳,基于这个信息,让DBA同学帮忙调整后台限制batch的总大小限制,应用程序中调整批量的大小,并抛弃一部分过大的消息体,调整以后效果如下。

    注意:另外要注意kafka的一个分区只能被一个节点消费,请合理的设置kafka的节点数,当节点数<kafka分区数,会出现部分节点无法获取数据的情况。另外kafka分区过多也会导致kafka本身的可靠性降低.
 

   Redis篇:
 redis在云捕系统的地位相当重要,碰到的问题也比较多,最近才解决了一个遗留的老大难问题.由于15年的时候才接触到redis,使用过程中姿势存在比较大的问题.在这里列举下面几个问题:
案例一:CPU抖动,具体可以见DBA同学的文章
 redis cpu 抖动问题分析 

redis-faina redis性能问题诊断利器
案例二:主从不同步,redis内存无法扩容,内存溢出;
 

   改造后的效果如下:




ddb连接数限制:<property name="maxActive" value="500" />         <!-- 最大连接数量 --> ddb最大支持500个连接数


org.springframework.dao.InvalidDataAccessApiUsageException: READONLY You can't write against a read only slave.; nested exception is redis.clients.jedis.exceptions.JedisDataException: READONLY You can't write against a read only slave.
+--------------------------+------------+
2 rows in set (0.01 sec)
本文作者:余宝虹