以下文章来源于架构师进阶之路 ,作者二马读书

1. JVM频繁FULL GC快速排查

  在分享此案例前,先聊聊哪些场景会导致频繁Full GC:

  • 内存泄漏(代码有问题,对象引用没及时释放,导致对象不能及时回收)
  • 死循环
  • 大对象

  尤其是大对象,80%以上的情况就是他。
   
  那么大对象从哪里来的呢?数据库(包括Mysql和Mongodb等NOSql数据库),结果集太大;第三方接口传输的大对象;消息队列,消息太大

  根据多年一线互联网经验,绝大部分情况是数据库大结果集导致。

  好,现在我们开始介绍这次线上故障:

  在没有任何发布的情况下,POP服务(接入第三方商家的服务)突然开始疯狂Full GC,观察堆内存监控没内存泄漏,回滚到前一版本,问题仍然存在,尴尬了!!!按照常规做法,一般先用jmap导出堆内存快照(jmap -dump:format=b,file=文件名 [pid]),然后用mat等工具分析出什么对象占用了大量空间,再查看相关引用找到问题代码。这种方式定位问题周期会比较长,如果是关键服务,长时间不能定位解决问题,影响太大。

  下面来看看我们的做法。先按照常规做法分析堆内存快照,与此同时另外的同学去查看数据库服务器网络IO监控,如果数据库服务器网络IO有明显上升,并且时间点吻合,基本可以确定是数据库大结果集导致了Full GC,赶紧找DBA快速定位大SQL(对DBA来说很简单,分分钟搞定,如果DBA不知道怎么定位,那他要被开除了,哈哈),定位到SQL后再定位代码就非常简单了。按照这种办法,我们很快定位了问题。原来是一个接口必传的参数没传进来,也没加校验,导致SQL语句where后面少了两个条件,一次查几万条记录出来,真坑啊!这种方法是不是要快很多,哈哈,5分钟搞定。

  当时的DAO层是基于Mybatis实现的,出问题的SQL语句如下:

<select id="selectOrders" resultType="com.***.Order" >
select * from user where 1=1
<if test=" orderID != null ">
and order_id = #{orderID}
</if >
<if test="userID !=null">
and user_id=#{userID}
</if >
<if test="startTime !=null">
and create_time >= #{createTime}
</if >
<if test="endTime !=null">
and create_time <= #{userID}
</if >
</select>

  上面SQL语句意思是根据orderID查一个订单,或者根据userID查一个用户所有的订单,两个参数至少要传一个。但是两个参数都没传,只传了startTime和endTime。所以一次Select就查出了几万条记录。

  所以我们在使用Mybatis的时候一定要慎用if test,一不小心就会带来灾难。后来我们将上面的SQL拆成了两个:

  根据订单ID查询订单:

<select id="selectOrderByID" resultType="com.***.Order" >
select * from user where order_id = #{orderID}
</select>

  根据userID查询订单:

<select id="selectOrdersByUserID" resultType="com.***.Order" >
select * from user where user_id=#{userID}
<if test="startTime !=null">
and create_time >= #{createTime}
</if >
<if test="endTime !=null">
and create_time <= #{userID}
</if >
</select>

2. 内存泄漏

  介绍案例前,先了解一下内存泄漏和内存溢出的区别。

  内存溢出:程序没有足够的内存使用时,就会发生内存溢出。内存溢出后程序基本上就无法正常运行了。

  内存泄漏:当程序不能及时释放内存,导致占用内存逐渐增加,就是内存泄漏。内存泄漏一般不会导致程序无法运行。不过持续的内存泄漏,累积到内存上限时,就会发生内存溢出。在Java中,如果发生内存泄漏,会导致GC回收不彻底,每次GC后,堆内存使用率逐渐增高。下图是JVM发生内存泄漏的监控图,我们可以看到每次GC后堆内存使用率都比以前提高了。

  当时内存泄漏的场景是,用本地缓存(公司基础架构组自己研发的框架)存放了商品数据,商品数量不算太多,几十万的样子。如果只存热点商品,内存占用不会太大,但是如果存放全量商品,内存就不够了。初期我们给每个缓存记录都加了7天的过期时间,这样就可以保证缓存中绝大部分都是热点商品。不过后来本地缓存框架经过一次重构,过期时间被去掉了。没有了过期时间,日积月累本地缓存越来越大,很多冷数据也被加载到了缓存。直到有一天接到告警短信,提示堆内存过高。赶紧通过jmap(jmap -dump:format=b,file=文件名 [pid] )下载了堆内存快照,然后用eclipse的mat工具分析快照,发现了本地缓存中有大量的商品记录。定位问题后赶紧让架构组加上了过期时间,然后逐个节点重启了服务。

  亏了我们加了服务器内存和JVM堆内存监控,及时发现了内存泄漏的问题。否则随着泄漏问题日积月累,如果哪天真的OOM就惨了。所以技术团队除了做好CPU,内存等运维监控,JVM监控也非常重要。

3. 幂等问题

  很多年前,笔者在一家大型电商公司做Java程序员,当时开发了积分服务。当时的业务逻辑是,用户订单完结后,订单系统发送消息到消息队列,积分服务接到消息后给用户积分,在用户现有的积分上加上新产生的积分。

  由于网络等原因会有消息重复发送的情况,这样也就导致了消息的重复消费。当时笔者还是个初入职场的小菜鸟,并没有考虑到这种情况。所以上线后偶尔会出现重复积分的情况,也就是一个订单完结后会给用户加两次或多次积分。

  后来我们加了一个积分记录表,每次消费消息给用户增加积分前,先根据订单号查一遍积分记录表,如果没有积分记录才给用户增加积分。这也就是所谓的“幂等性”,即多次重复操作不影响最终的结果。实际开发中很多需要重试或重复消费的场景都要实现幂等,以保证结果的正确性。例如,为了避免重复支付,支付接口也要实现幂等。

4. 缓存雪崩

  我们经常会遇到需要初始化缓存的情况。比如,我们曾经经历过用户系统重构,用户系统表结构发生了变化,缓存信息也要变。重构完成后上线前,需要初始化缓存,将用户信息批量存入Reids。每条用户信息缓存记录过期时间是1天,记录过期后再从数据库查询最新的数据并拉取到Redis中。灰度上线时一切正常,所以很快就全量发布了。整个上线过程非常顺利,码农们也很开心。

  不过,第二天,灾难发生了!到某一个时间点,各种报警纷至沓来。用户系统响应突然变得非常慢,甚至一度没有任何响应。查看监控,用户服务CPU突然飙高(IO wait很高),Mysql访问量激增,Mysql服务器压力也随之暴增,Reids缓存命中率也跌到了极点。依赖于我们强大的监控系统(运维监控,数据库监控,APM全链路性能监控),很快定位了问题。原因就是Reids中大量用户记录集中失效,获取用户信息的请求在Redis中查不到用户记录,导致大量的请求穿透到数据库,瞬间给数据库带来巨大压力。同时用户服务和相关联的其他服务也都受到了影响。

  这种缓存集中失效,导致大量请求同时穿透到数据库的情况,就是所谓的“缓存雪崩”。如果没到缓存失效时间点,性能测试也测不出问题。所以一定要引起大家注意。

所以,需要初始化缓存数据时,一定要保证每个缓存记录过期时间的离散性。例如,我们给这些用户信息设置过期时间,可以采用一个较大的固定值加上一个较小的随机值。比如过期时间可以是:24小时 + 0到3600秒的随机值。

5. 磁盘IO导致线程阻塞

  问题发生在2017年下半年,有一段时间地理网格服务时不常的会响应变慢,每次持续几秒钟到几十秒钟就自动恢复。

  如果响应变慢是持续的还好办,直接用jstack抓线程堆栈,基本可以很快定位问题。关键持续时间只有最多几十秒钟,而且是偶发的,一天只发生一两次,有时几天才发生一次,发生时间点也不确定,人盯着然后用jstack手工抓线程堆栈显然不现实。

  好吧,既然手工的办法不现实,咱们就来自动的,写一个shell脚本自动定时执行jstack,5秒执行一次jstack,每次执行结果放到不同日志文件中,只保存20000个日志文件。

  Shell脚本如下:

#!/bin/bash
num=0
log="/tmp/jstack_thread_log/thread_info" cd /tmp
if [ ! -d "jstack_thread_log" ]; then
mkdir jstack_thread_log
fi while ((num <= 10000)); do ID=`ps -ef | grep java | grep gaea | grep -v "grep" | awk '{print $2}'` if [ -n "$ID" ]; then
jstack $ID >> ${log}
fi num=$(( $num + 1 )) mod=$(( $num%100 )) if [ $mod -eq 0 ]; then
back=$log$num
mv $log $back
fi sleep 5 done

  下一次响应变慢的时候,我们找到对应时间点的jstack日志文件,发现里面有很多线程阻塞在logback输出日志的过程,后来我们精简了log,并且把log输出改成异步,问题解决了,这个脚本果真好用!建议大家保留,以后遇到类似问题时,可以拿来用!

6. 数据库死锁问题

  在分析案例之前,我们先了解一下MySQL INNODB。在MySQL INNODB引擎中主键是采用聚簇索引的形式,即在B树的叶子节点中既存储了索引值也存储了数据记录,即数据记录和主键索引是存在一起的。而普通索引的叶子节点存储的只是主键索引的值,一次查询找到普通索引的叶子节点后,还要根据叶子节点中的主键索引去找到聚簇索引叶子节点并拿到其中的具体数据记录,这个过程也叫“回表”。

  故障发生的场景是关于我们商城的订单系统。有一个定时任务,每一小时跑一次,每次把所有一小时前未支付订单取消掉。而客服后台也可以批量取消订单。

  订单表t_order结构大至如下:

id 订单id,主键
status 订单状态
created_time 订单创建时间

  id是表的主键,created_time字段上是普通索引。聚簇索引(主键id)

id(索引) status created_time
1 UNPAID 2020-01-01 07:30:00
2 UNPAID 2020-01-01 08:33:00
3 UNPAID 2020-01-01 09:30:00
4 UNPAID 2020-01-01 09:39:00
5 UNPAID 2020-01-01 09:50:00

  普通索引(created_time字段)

created_time(索引) id(主键)
2020-01-01 09:50:00 5
2020-01-01 09:39:00 4
2020-01-01 09:30:00 3
2020-01-01 08:33:00 2
2020-01-01 07:30:00 1

  定时任务每一小时跑一次,每次把所有一小时前两小时内的未支付订单取消掉,比如上午11点会取消8点到10点的未支付订单。SQL语句如下:

update t_order set status = 'CANCELLED' where created_time > '2020-01-01 08:00:00' and created_time < '2020-01-01 10:00:00' and status = 'UNPAID'

  客服批量取消订单SQL如下:

update t_order set status = 'CANCELLED' where id in (2, 3, 5) and status = 'UNPAID'

  上面的两条语句同时执行就可能发生死锁。我们来分析一下原因。第一条定时任务的SQL,会先找到created_time普通索引并加锁,然后再在找到主键索引并加锁。

  第一步,created_time普通索引加锁

  解决办法就是从SQL语句上保证加锁顺序一致。或者把客服批量取消订单SQL改成每次SQL操作只能取消一个订单,然后在程序里多次循环执行SQL,如果批量操作的订单数量不多,这种笨办法也是可行的。

7. 域名劫持

  先看看DNS解析是怎么回事,当我们访问www.baidu.com时,首先会根据www.baidu.com到DNS域名解析服务器去查询百度服务器对应的IP地址,然后再通过http协议访问该IP地址对应的网站。而DNS劫持是互联网攻击的一种方式,通过攻击域名解析服务器(DNS)或者伪造域名解析服务器,把目标网站域名解析到其他的IP。从而导致请求无法访问目标网站或者跳转到其他网站。如下图:

  由于当时的CDN图片链接采用了不安全的http协议,所以很容易被劫持。后来改成了https,问题就解决了。

  当然域名劫持有很多方式,https也不能规避所有问题。所以,除了一些安全防护措施,很多公司都有自己的备用域名,一旦发生域名劫持可以随时切换到备用域名。

8. 带宽资源耗尽

  带宽资源耗尽导致系统无法访问的情况,虽然不多见,但是也应该引起大家的注意。来看看,之前遇到的一起事故。

  场景是这样的。社交电商每个分享出去的商品图片都有一个唯一的二维码,用来区分商品和分享者。所以二维码要用程序生成,最初我们在服务端用Java生成二维码。前期由于系统访问量不大,系统一直没什么问题。但是有一天运营突然搞了一次优惠力度空前的大促,系统瞬时访问量翻了几十倍。问题也就随之而来了,网络带宽直接被打满,由于带宽资源被耗尽,导致很多页面请求响应很慢甚至没任何响应。原因就是二维码生成数量瞬间也翻了几十倍,每个二维码都是一张图片,对带宽带来了巨大压力。

  怎么解决呢?如果服务端处理不了,就考虑一下客户端。把生成二维码放到客户端APP处理,充分利用用户终端手机,目前Andriod,IOS或者React都有相关生成二维码的SDK。这样不但解决了带宽问题,而且也释放了服务端生成二维码时消耗的CPU资源(生成二维码过程需要一定的计算量,CPU消耗比较明显)。

[转] 总结了N个真实线上故障的更多相关文章

  1. 使用Eclipse Memory Analyzer Tool(MAT)分析线上故障(一) - 视图&功能篇

    Eclipse Memory Analyzer Tool(MAT)相关文章目录: 使用Eclipse Memory Analyzer Tool(MAT)分析线上故障(一) - 视图&功能篇 使 ...

  2. JVM 线上故障排查基本操作--CPU飙高

    JVM 线上故障排查基本操作 CPU 飚高 线上 CPU 飚高问题大家应该都遇到过,那么如何定位问题呢? 思路:首先找到 CPU 飚高的那个 Java 进程,因为你的服务器会有多个 JVM 进程.然后 ...

  3. JAVA 线上故障排查套路,从 CPU、磁盘、内存、网络到GC 一条龙!

    线上故障主要会包括cpu.磁盘.内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍. 同时例如jstack.jmap等工具也是不囿于一个方面的问题的, ...

  4. JVM 线上故障排查

    JVM 线上故障排查 Linux 1.1 CPU 1.2 内存 1.3 存储 1.4 网络 一.CPU 飚高 寻找原因 二.内存问题排查 三.一般排查问题的方法 四.应用场景举例 4.1 怎么查看某个 ...

  5. JVM线上故障初步简易排查

    线上故障主要包括cpu 磁盘 内存 网络等问题 依次排查 1.cpu 1) 先用ps找到进程pid 2) top -H -p pid 找到cpu占用高的线程 3)printf '%x\n' pid 获 ...

  6. JAVA线上故障排查手册-(推荐)

    参考:https://fredal.xin/java-error-check?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=tout ...

  7. 线上故障排查——drools规则引擎使用不当导致oom

    事件回溯 1.7月26日上午11:34,告警邮件提示:tomcat内存使用率连续多次超过90%: 2.开发人员介入排查问题,11:40定位到存在oom问题,申请运维拉取线上tomcat 内存快照dum ...

  8. 通过jstack与jmap分析一次cpu打满的线上故障

    一.发现问题 下面是线上机器的cpu使用率,可以看到从4月8日开始,随着时间cpu使用率在逐步增高,最终使用率达到100%导致线上服务不可用,后面重启了机器后恢复. 二.排查思路 简单分析下可能出问题 ...

  9. JVM 线上故障排查基本操作

    # 前言 对于后端程序员,特别是 Java 程序员来讲,排查线上问题是不可避免的.各种 CPU 飚高,内存溢出,频繁 GC 等等,这些都是令人头疼的问题.楼主同样也遇到过这些问题,那么,遇到这些问题该 ...

随机推荐

  1. sql语句replace函数的使用

    SQL中的替换函数replace()使用 语法 REPLACE ( string_expression , string_pattern , string_replacement ) 参数 strin ...

  2. 利用docker部署oxidized网络设备备份系统

    随着网络设备的增多,通过人手备份网络设备倍感压力,而且效率低.有编程基础的人可能会通过Python的parimiko 或者netmiko 连接到设备操作 把文件通过ftp 上传到FTP服务器, 在通过 ...

  3. 安装FeedReader添加RSS订阅

    #0x1 FeedReader FeedReader是一款功能齐全,界面优美的GTK+ 3RSS阅读器客户端,用于在线RSS服务. FeedReader目前支持Feedbin,Feedly,Fresh ...

  4. 解决alert在ios版微信中显示url的问题(重写alert)

    为了解决alert在ios版微信中显示url的问题 window.alert = function(name){ var iframe = document.createElement("I ...

  5. scala 数据结构(九):-filter、化简

    1 filter filter:将符合要求的数据(筛选)放置到新的集合中 应用案例:将 val names = List("Alice", "Bob", &qu ...

  6. matlab中的静态变量与全局变量

    matlab中的静态变量和全局变量 1.静态变量 在matlab中,和其他语言一样,函数中的变量一把都是局部变量,也就是说,在函数调用完毕后,变量就会被释放.但是有些时候回希望上次改变的变量在下一次调 ...

  7. 压缩并上传图片到阿里云(jfinal)

    /** * 获取上传文件 * * @param r * @Return: com.oreilly.servlet.multipart.FilePart */public static FilePart ...

  8. 2. import 与 from...import 导入模块

    1. 导入整个模块 格式: import somemodule2. 从某个模块中导入某个函数 格式: from somemodule import somefunction3. 从某个模块中导入多个函 ...

  9. 微信小程序开发部署

    一.开发准备 1,想要开发微信小程序,必须要有一个AppId,如果没有可以去注册一个.   https://mp.weixin.qq.com/进入注册页面,点击上方注册.   2,点击选择“小程序”出 ...

  10. Mysql报Too many connections,不要乱用ulimit了,看看如何正确修改进程的最大文件数

    背景 今天在学习mysql时,看到一个案例,大体来说,就是客户端报Too many connections.但是,客户端的连接池,限制为了200,两个客户端java进程,那也才400,然后mysql配 ...