记一次Java e.printStackTrace异常的血案

大家学Java的时候,发生异常时都是用try catch, 然后e.printStackTrace

这么写在没有发生异常的是候一点问题都没有,就真的没有问题了吗???

答案肯定是否定的

血案起因:

  在一波正常压力测试中,系统很稳定,各项指标都正常,但加入1%的异常请求后,后台java 程序死锁

血案分析:

  通过JStack打印线程堆栈日志,发现有大量的Blcoked的线程在等一个锁

记一次Java e.printStackTrace异常的血案_第1张图片

 

通过截图可以发现是a java.io.PrintStream,通过代码排查代码中有大量的e.printStack,修改后,再进行压测,结果正常

追根溯源

要打印字符串输出到控制台上,需要字符串常量池所在的内存块有足够的空间。然而,因为e.printStackTrace() 语句要产生的字符串记录的是堆栈信息,太长太多,内存被填满了!大量线程产出字符串产出到一半,等待有内存被释放,锁死了,导致整个应用挂掉了。

记一次Java e.printStackTrace异常的血案_第2张图片

附:阿里异常日志规范

异常处理

  • 【强制】不要捕获 Java 类库中定义的继承自 RuntimeException 的运行时异常类, 如:IndexOutOfBoundsException / NullPointerException, 这类异常由程序员预检查来规避, 保证程序健壮性.

正例:

if(obj != null) {...}

反例:

try {obj.method() } catch(NullPointerException e){…}

  • 【强制】异常不要用来做流程控制, 条件控制, 因为异常的处理效率比条件分支低.
  • 【强制】一大段代码进行 try-catch, 这是不负责任的表现.catch 时请分清稳定代码和非稳定代码, 稳定代码指的是无论如何不会出错的代码. 对于非稳定代码的 catch 尽可能进行区分异常类型, 再做对应的异常处理.
  • 【强制】捕获异常是为了处理它, 不要捕获了却什么都不处理而抛弃之, 如果不想处理它, 请将该异常抛给它的调用者. 最外层的业务使用者, 必须处理异常, 将其转化为用户可以理解的内容.
  • 【强制】有 try 块放到了事务代码中,catch 异常后, 如果需要回滚事务, 一定要注意手动回滚事务.
  • 【强制】finally 块必须对资源对象、流对象进行关闭, 有异常也要做 try-catch.

说明: 如果 JDK7, 可以使用 try-with-resources 方法.

  • 【强制】不能在 finally 块中使用 return,finally 块中的 return 返回后方法结束执行, 不会再执行 try 块中的 return 语句.
  • 【强制】捕获异常与抛异常, 必须是完全匹配. 捕获异常必须是抛异常的父类.

说明: 如果预期抛的是绣球, 实际接到的是铅球, 就会产生意外情况.

  • 【推荐】方法的返回值可以为 null, 不强制返回空集合, 或者空对象等, 必须添加注释充分说明什么情况下会返回 null 值. 调用方需要进行 null 判断防止 NPE 问题.

说明: 本规约明确防止 NPE 是调用者的责任. 即使被调用方法返回空集合或者空对象, 对调用者来说, 也并非高枕无忧, 必须考虑到远程调用失败, 运行时异常等场景返回 null 的情况.

  • 【推荐】防止 NPE, 是程序员的基本修养, 注意 NPE 产生的场景:
  • 返回类型为包装数据类型, 有可能是 null, 返回 int 值时注意判空.

反例: public int f(){return Integer 对象}, 如果为 null, 自动解箱抛 NPE.

  • 数据库的查询结果可能为 null.
  • 集合里的元素即使 isNotEmpty, 取出的数据元素也可能为 null.
  • 远程调用返回对象, 一律要求进行 NPE 判断.
  • 对于 Session 中获取的数据, 建议 NPE 检查, 避免空指针.
  • 级联调用 obj.getA().getB().getC(); 一连串调用, 易产生 NPE.

反例: "一拍档客户" 的返回值从空对象变成了 null, 导致线上故障,NPE 无小事.

  • 【推荐】在代码中使用 "抛异常" 还是 "返回错误码", 对于公司外的 http/api 开放接口必须使用 "错误码"; 而应用内部推荐异常抛出; 跨应用间 HSF 调用优先考虑使用 Result 方式, 封装 isSuccess、"错误码"、"错误简短信息".

说明: 关于 HSF 方法返回方式使用 Result 方式的理由:

  • 中间件平台基本上使用 ResultDO 来封装, 由于中间件的普及, 本身就有标准的引导含义.
  • 使用抛异常返回方式, 调用方如果没有捕获到就会产生运行时错误.
  • 如果不加栈信息, 只是 new 自定义异常, 加入自己的理解的 error message, 对于调用端解决问题的帮助不会太多. 如果加了栈信息, 在频繁调用出错的情况下, 数据序列化和传输的性能损耗也是问题.
  • 【推荐】定义时区分 unckecked / checked 异常, 避免直接使用 RuntimeException 抛出, 更不允许抛出 Exception 或者 Throwable, 应使用有业务含义的自定义异常. 推荐业界或者集团已定义过的自定义异常, 如:DaoException / ServiceException 等.
  • 【参考】避免出现重复的代码 (Don’t Repeat Yourself), 即 DRY 原则.

说明: 随意复制和粘贴代码, 必然会导致代码的重复, 在以后需要修改时, 需要修改所有的副本, 容易遗漏. 必要时抽取共性方法, 或者抽象公共类, 甚至是共用模块.

正例: 一个类中有多个 public 方法, 都需要进行数行相同的参数校验操作, 这个时候请抽取:

private boolean checkParam(DTO dto){...}

日志规约

  • 【强制】应用中不可直接使用日志系统 (Log4j、Logback) 中的 API, 而应依赖使用日志框架 (SLF4J、JCL--Jakarta Commons Logging) 中的 API. 什么是日志框架和日志系统, 请参考 webx 作者宝宝的文章, 文章里也详细说明了为什么不能直接依赖使用日志系统而是日志框架, 以及应用的 pom 中如何做 dependencyManagement.

说明: 日志框架 (SLF4J、JCL--Jakarta Commons Logging) 的使用方式 (推荐使用 SLF4J):

使用 SLF4J:

import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(Abc.class);

使用 JCL:

import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; private static final Log log = LogFactory.getLog(Abc.class);

参考链接:为什么使用 slf4j

  • 【强制】日志文件推荐至少保存 15 天, 因为有些异常具备以 "周" 为频次发生的特点. 对于当天日志, 以 "应用名.log" 来保存, 保存在 /home/admin/ 应用名 /logs/ 目录下, 过往日志格式为: {logname}.log.{保存日期}, 日期格式:yyyy-MM-dd

说明: 以 mppserver 应用为例, 日志保存在 /home/admin/mppserver/logs/mppserver.log, 历史日志名称为 mppserver.log.2016-08-01

  • 【强制】应用中的扩展日志 (如打点、临时监控、访问日志等) 命名方式:appName_logType_logName.log.logType: 日志类型, 推荐分类有 stats/desc/monitor/visit 等;logName: 日志描述. 这种命名的好处: 通过文件名就可知道日志文件属于什么应用, 什么类型, 什么目的, 也有利于归类查找.

正例: mppserver 应用中单独监控时区转换异常, 如:mppserver_monitor_timeZoneConvert.log

说明: 推荐对日志进行分类, 错误日志和普通提示日志尽量分开存放, 便于开发人员查看, 也便于通过日志对系统进行及时监控.

  • 【强制】对 trace/debug/info 级别的日志输出, 必须使用条件输出形式或者使用占位符的方式, 否则大量的对象 toString 和字符串拼接会带来严重的性能问题.

正例: (条件)

if (logger.isDebugEnabled()) {logger.debug("Processing trade with id:" + id + "symbol:" + symbol); }

正例: (占位符)

logger.debug("Processing trade with id: {} and symbol : {}", id, symbol);

  • 【强制】避免重复打印日志, 浪费磁盘空间, 务必在 log4j.xml 中设置 additivity=false.

正例:

name="com.taobao.ecrm.member.config" additivity="false">

  • 【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace() 打印异常堆栈. 由于标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动, 如果大量输出送往这两个文件, 容易造成文件大小超过操作系统大小限制.
  • 【强制】异常信息应该包括两类信息: 案发现场信息和异常堆栈信息. 如果不处理, 那么往上抛.

正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);

![](file:///D:/whereismydata/youdaoyun/[email protected]/0d70e74ee6fa462e915b2b6eb99ec4cb/ru5erkjggg%3D%3D.png)

输出的 POJO 类必须重写 toString 方法, 否则只输出此对象的 hashCode 值 (地址值), 没啥参考意义.

  • 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况, 避免用户投诉时, 无所适从. 注意日志输出的级别,error 级别只记录系统逻辑出错、异常、或者重要的错误信息. 如非必要, 请不要在此场景打出 error 级别, 避免频繁报警.
  • 【推荐】如果使用 log.warn 记录跟踪调试信息, 一定要注意日志输出量的问题, 避免把服务器磁盘撑爆, 并记得及时删除这些观察日志.
  • 【参考】如果日志用英文描述不清楚, 推荐使用中文注释. 对于中文 UTF-8 的日志, 在 secureCRT 中,set encoding=utf-8; 如果中文字符还乱码, 请设置: 全局 > 默认的会话设置 > 外观 > 字体 > 选择字符集 gb2312; 如果还不行, 执行命令:set termencoding=gbk, 并且直接使用中文来进行检索.


 

你可能感兴趣的:(java,压测,java)