调试大规模服务器集群的五大策略
对运行中的真实环境进行调试,比在IDE中进行要困难很多。断点,单步执行等都会变得非常奢侈。因此要做的第一件事是要做出周密的调试计划,否则漫无目的单纯依靠日志记录的做法,将是非常低效的。其次,规模越大的架构会更容易出现差错。因此,如何筛查出错误源头,明确哪个步骤出错是非常重要的。
一、分布式日志
对于每条记录,我们需要认真分析并了解其背后的含义。但是对于庞大的日志记录我们需要高效的方法来处理,具体请参考这篇文章Logback调节的7个方法。
什么样的记录是真正需要的?
答案是全部!因为代码会影响到整个应用的方方面面。此外,事务ID也是很重要的。它能有助于处理异常,因为事务ID经常会贯穿于节点、进程、线程之间。一个较好的处理方法是在App的每个线程入口生成一个UUID。然后把该ID附加到日志记录中,进行全程监视。该方法在分布式和异步日志中起着举足轻重的作用,特别是与日志管理工具如Logstash和Loggly等一起使用时。
异常处理
未知异常很容易会导致系统崩溃。所以建议在代码末端设置一个全局异常处理句柄,例如在Java中进行下面的代码编写:
public static void Thread.setDefaultUncaughtExceptionHandler(
UncaughtExceptionHandler eh);
void uncaughtException( Thread t, Throwable e) {
logger.error(“Uncaught error in thread “ + t.getName(), e);
}
这或许看起来与Tomcat或Akka框架有点类似。最后这里给出三种处理未知异常时的方法:
1. 线程名:根据需要处理的请求来变更线程名是个巧妙的方法。例如在事务处理的任何时间,把事务ID先附加到线程,然后在结束时移除掉。
2. 本地线程存储(Thread-local storage,TLS):这是一种使线程特定数据从线程对象分离的方法。借助这些特定数据能便于对出现的错误进行排查。例如事务ID,时间或用户名。否则在欠缺这些数据和线程名的情况下,我们将不得不花费更多时间来处理未知异常。
3. 线程映射表(Mapped Diagnostic Context,MDC):MDC类似于本地线程概念,是日志框架的一部分如Log4j或Logback。它在日志级别生成了一个静态映射表,能够较TLS实现更多高级特性。
二、快人一步的Jstack
Jstack对Java开发者来说并不陌生,这是一款强大的JDK工具。简单来说,Jstack能够进入一个正在运行的进程然后输出所有的线程meta信息,例如堆跟踪,框架,锁等等。此外它能够对已销毁的进程进行heap dumps或core dumps分析。
不过很多时候Jstack是用在回顾的环节,如果错误已经发生,它反馈的可能是过时的信息。因此如何更主动地使用Jstack是关键所在。例如,设置一个吞吐量阀值然后在该值下降时启动jstack。
public void startScheduleTask() {
scheduler.scheduleAtFixedRate(new Runnable() {
public void run() {
checkThroughput();
}
}, APP_WARMUP, POLLING_CYCLE, TimeUnit.SECONDS);
}
private void checkThroughput()
{
int throughput = adder.intValue(); //the adder is inc’d when a message is processed
if (throughput < MIN_THROUGHPUT) {
Thread.currentThread().setName("Throughput jstack thread: " + throughput);
System.err.println("Minimal throughput failed: executing jstack");
executeJstack(); // See the code on GitHub to learn how this is done
}
adder.reset();
}
三、 Stateful Jstack
Jstack应用时需要注意的另一个问题是由于它会返回非常多的线程meta数据,如果缺乏相关的实际状态数据,将会对错误排查造成不便。以数据库查询为例子,可以加上如下一行代码:
Thread.currentThread().setName(Context + TID + Params + current Time, ...);
我们来比较加入前后的数据输出:
加入前:
“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800 nid=0x6d03 in Object.wait() [0x000000013ebcc000]
加入后:
”Queue Processing Thread, MessageID: AB5CAD, type: AnalyzeGraph, queue: ACTIVE_PROD, Transaction_ID: 5678956, Start Time: 10/8/2014 18:34″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800 nid=0x6d03 in Object.wait() [0x000000013ebcc000]
不难看出,加入代码后的信息输出显得更加清晰了。例如线程正在做什么,接收了什么参数如事务ID和消息ID。这些对后续的回滚,错误重现、分离等步骤都是很有帮助的。
四、 开源追踪工具BTrace
如果在不依靠日志和改变代码的前提下,如何去追踪运行时JVM状态呢?答案是BTrace Java代理。在添加该代理后,可使用BTrace脚本语言来获取相关信息。
例如以下脚本:
@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(“==============================”);
}
}
上述代码对全部ClassLoaders及其子类进行跟踪,当defineClass返回时,该脚本会列出载入的类并启动JStack。但是我们不建议在实际环境中长期使用BTrace。因为Java代理会造成一定的资源开销,同时需要编写不同的脚本来进行追踪。不过在想避免重启JVM的情况下在运行时环境修改跟踪脚本,BTrace是个不错的选择。
五、自定义JVM代理
在不改动服务器代码的前提下进行调试,JVM代理是最佳选择。类似于BTrace,我们可以尝试编写自定义Java代理。这种代理可以进入对象结构体然后在对象实例化的时候进行堆追踪。然后我们可以对结果进行分析并掌握具体的载入过程。这是BTrace所不具备的,因为BTrace有限制和只能进行读操作。请看下面的示例代码:
private static void internalPremain(String agentArgs, Instrumentation inst) throws IOException {
….
Transformer transformer = new Transformer(targetClassName);
inst.addTransformer(transformer, true); // the true flag let’s the agent hotswap running classes
}
这里创建了一个transformer对象,并注册到一个能对类进行变更的对象之上。完整代码请点击这里进行查看。
小结
综上所述,获得的有价值数据越多,解决问题的速度就越快。在当今信息为王的时代,宕机时间的影响几以秒计,因此是否具备一个完善的服务器调试策略将对整个部署维护工作有着至关重要的影响。