Java 调试中你可能会遇到的问题以及如何解决他们

[TOC]

原文链接

当你的生产环境发生了异常的时候,事情变得艰难.Made Up Stats™ 的科学研究表明开发者团队会收到比平时更大的压力,咖啡杯脏的比平时快3倍而且上帝杀死了两只小猫因为一只是不够的(嗯…. 这个猫的梗有点污,原文是 god kills 2 kittens because one is just not enough. 查了一下取自一个梗…每当你手淫的时候,上帝会杀死一只猫).在下面的内容中我们将会分析你在调试生产环境的代码面对的主要的问题.接下来让我们用一个正直的警告来开始,这是我们的法律部门强迫我们放在这里的.

警告: 下面的内容包含一些开发者会觉得困扰的生产环境的 bug 的准确描述.

问题 #0 : 你的生产环境里有 bug

第一步是承认你是有部分问题的,无论你的代码经过了多少次的测试 bug 总是会找到出现在产品的路.不管你的展示环境是多么漂亮并且不管你的负载测试多么精确.在生产环境中现实生活中经过你系统的数据流动是完全不同的.你也不是在 Intellij 或者 Eclipse 中,你身边也没有 XRebel ,除非你做了一些事: 你和 bugs 都在一篇黑暗中.但是不要担心,你并不是唯一处在这种环境里的 - 让我们来看看如何打开灯,测试可能的解决方案并一劳永逸的解决这个问题.

为了解决 bug 你通常需要一个东西,导致错误的变量值还要知道他们是怎么进入到调用栈的.让我们来分解这个问题并看看我们如何从每个情况中获得最多的数据.

解决方案: 继续阅读并跟紧了,马上就要来了

问题 #1:一个错误发生了但是你不知道它是从哪里来的

某些不好的情况发生了,现在让我们将它保持在 日志异常/未捕获异常的等级.所以你查找你的 log 只看到了有关于错误空白的声明.它通常看起来像这样:

05-04-2015 11:52:32 ERROR – Problem parsing input for servlet. [http-nio-8080-exec-62]

但是,哇,哇,哇,等一下,除了这个异常我还知道什么呢,很好,有一些不好的东西发生在了 http-nio 中?当我检查代码并看到它运行的很好那么异常很可能是从其他机器,微服务,进程,或者甚至另一个线程产生的,事情变得棘手了.这就是在日志中迷失的全部.如果这个场景听起来很熟悉那么你大概在日志中丢失了一个事务 ID.这是一个唯一的 ID来帮助你跟踪它是从哪里开始的到它变成一个日志错误的全部过程.

除了事务 ID,你需要记住其他有关于错误的数据也是会丢失的除非你将它打印出来.关键是要尽你的可能绘制出足够的上下文,比如你进入的线程,类,甚至在关键路径上的特定方法.理想情况下我们在这里还拥有变量值,但是这个对于这种类型的问题来说太多了. 在 Takipi blog 的这篇文章中 我们测试了使用 Logback 的不同类型的样式,如果你想要了解更多的内容就去看看吧.

解决方案: 在每个线程的实例里生成一个 UUID 指向你的应用并且将它添加到每一个日志条目 - 在整个机器上保持一致来保存他的原始上下文.同样的,如果你想要摆脱 console 可以考虑使用一些 日志管理工具 .

问题 #2: 未捕获并异常没有关于错误的信息

当我们逃离了日志错误并且捕获到了异常,我们进入了一个更黑暗的地方.未捕获异常会在线程死亡的地方出现.但是并不是所有的都这么残酷,我们有一个最后的防线: 默认未捕获异常处理器. 一旦我们在这里有了一个未捕获异常处理器,我们实际上可以对他们做一些事并且绘制出一些变量数据.问题是当他们到达调用栈上的未捕获异常处理器的时候,大部分我们可以得到的关于错误的上下文已经丢失了.再说一次,我们在这里还有另一个机会并且不是所有的都丢失了: Thread Local Storage (TLS) 还有线程名称.

Thread Local Storage 让我们可以再这个线程上存储变量,不会被限制在任何的栈帧上.实际上,线程可以做很酷的事情,想要知道更多的使用线程的技巧你可以查看 这个技巧.所以通过 TLS 我们可以在这个异常被抛出的时候存储更多有关于这个状态的数据.另一件很酷的技巧是使用有意义的线程名字,取代像这样的名字:

1
pool-1-thread-1

我们可以这么做:
Thread.currentThread().setName(Context + TID + Params + current Time, ...);

然后可以得到:

1
Queue Processing Thread, MessageID: AB5CAD, type: AnalyzeGraph, queue: ACTIVE_PROD, Transaction_ID: 5678956, Start Time: 02/04/2015 17:37

解决方案: 设置一个未捕获异常处理器,记住线程的名字并且让你的 Thread Local Storage 工作.

问题 #3: 你的进程被挂起了而你还不知道为什么

一个进程被卡住了可能你知道有一些奇怪的东西在你的应用中流动?从你的 JDK 中构建的工具来看这可能是一个很好的任务.它可以连接到任何一个 Java 进程,只需要指向一个 PID ,然后对所有的输出进行分类,包括现在运行的所有线程的堆栈跟踪,切面,他们持有的锁还有所有其他的元数据.使用 jstack 你还可以分析哪些已经不存在的进程的堆输出或者核心输出.

要获得更多实践的概述还有在特定条件下自动开启它的方法,可以查看 这篇文章,还有 Github 上的示例,它可以完成这个功能.为了从中提取出最大的价值,你同事还需要做一些手动工作来理解你得到的结果.

解决方案: 了解如何处理 jstack 并用它来解决棘手的问题.

问题 #4: 所有的这些解决方案都需要改动代码

介绍你的应用的改变并且在你的服务器重构你的日志系统是一个非常困难的任务.如果这里有一种方式可以再不影响代码的内部结构的情况下提取出调试应用程序所需的信息呢?如果你设置没有访问某些部分的权限但是错误就产生在那些地方呢?使用 Java 代理.

Java 代理可以挂载到一个运行的 JVM 中并且提取出它里面所有的信息. 其中有一个叫 BTrace 的 Java 代理你可以看看. 通过 JVM 参数将它绑定上之后,你可以通过 BTrace 脚本语言将它绑定. 你可以测试自己的代码并且获取运行它的数据而不需要改变实际的应用. 让我们看看如何通过使用 BTrace 脚本语言激活 jstack 来使用这个技巧.

1
2
3
4
5
6
7
8
9
10
11
12
@BTrace public class Classload {
@OnMethod(
Clazz=”+java.lang.ClassLoader”,
method=”defineClass”,
location=@Location(Kind.RETURN)
)
public static void defineClass(@Return class cl) {
println(Strings.strcat(“loaded ”, Reflective.name(cl)));
Threads.jstack();
println(“==============================”);
}
}

在这里我们获取了所有的 ClassLoader 还有他们的子类.不管什么时候返回了 “defineClass” ,这个脚本都会打印出加载的类并且运行 jstack.这个方法的缺点是不推荐在生产环境中持续使用它,只适合于指出特定的问题.它也有一些限制,比如不能创建新的对象,捕捉异常或包含循环.

如果你想要更冒险一点,你可以自己自定义一个 Java 代理,就像 BTrace一样.比如,当我们有一个类出现了问题,它在到处产生了数百万个新的对象,一个简单的 Java 代理帮助我们解决了这个问题.我们会挂载到这个对象的构造器上,并且不管它在什么时候被分配了一个实例,我们可以获得它的堆栈记录并可以知道这个加载是从哪里来的.这个代理的代码可以在 Github 上得到,欢迎你来 查看.

解决方案: 学习更多关于 Java 代理的知识并且使用他们来解决真正棘手的问题.

问题 #5: 没有关于错误的根本原因的数据

再更深入一点,与自定义 Java 代理不同,本地代理在处理的时候拥有一个更加强大的功能.你可以在 这里 了解更多具体的不同.通常来说,Java 代理拥有代码检测功能,然而本机代理拥有访问 JVM 底层的 API 的权限(JVMTI). 这意味着他们可以越过基础的栈追踪数据并且获得真正的根本原因,包含导致错误的变量值.

Takipi's error analysis screen

Takipi 的错误分析屏

Takipi 的本地代理 专门用于监控大规模的生产环境中的服务器.它工作在 JVM 层来捕获有关于你错误的完整的代码和变量,而不需要依赖日志文件.在配置方面,它只需要你在服务器上使用一个 JVM 参数启动就会立刻开始反馈并分析所有你的异常和日志错误.

解决方案: 尝试于是用一个可以访问底层并且得到真正的根本原因的本地代理.

结论

希望这个演练可以帮助你以无痛的方式来解决生产环境的错误,而不需要浪费很多小时甚至是很多天的时间.你有更多的调试生产环境的方法吗?或者你想要分享你和 bug 斗争的故事吗?通过下面的评论模块让我们知道.

About Me

我的博客 leonchen1024.com

我的 GitHub https://github.com/LeonChen1024

微信公众号

wechat

You forgot to set the business and currency_code for Paypal. Please set it in _config.yml.
You forgot to set the url Patreon. Please set it in _config.yml.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×