最近处理的两个case

分享下最近处理了两个case,涉及到的主要是cpu sy高和依赖外网的dtd的问题。

Case I
有个应用换到了新的一批机器上,出现了同样的qps情况下cpu us和sy比原来机器高不少的现象,尤其是sy,高了非常多,按照以往的经验,cpu sy高主要是线程数多、线程竞争激烈,因为是同样的应用,同样的qps,也看过了应用的线程数和竞争状况,一切正常,然后想到新机器和原来的机器的不同点有一个是内核,原来的机器是2.6.18的,新的是2.6.32的,于是猜测可能是高精度定时器(关于高精度定时器之前的文章里有说过,简单来说就是2.6.32的内核支持纳秒级的调度,之前的版本只能支持到毫秒级)带来的“问题”(这个其实是提升,不是问题)。

按照这个思路,Java中如果要按比毫秒更小的单位去做唤醒等动作,通常最后都会调用到java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject.awaitNanos,于是用btrace去跟了下这里小于1ms(也就是1000000值)的调用,发现还真有,传入的值为100000,也就是100微秒,熟悉btrace的人自然知道这个地方可以直接获取到堆栈,于是按照堆栈翻了下业务代码,业务代码里有这么一行代码:
Object node = this.logQueue.poll(100L, TimeUnit.MICROSECONDS);

看到这一切就明了了,其实同样,这个地方压根就没有这么高精度的需求,否则在之前2.6.18内核跑的时候早就会懂了,因此要做的就是告诉开发,把这个值至少改到1ms以上…

哎,2.6.32内核经常中这种招,实在是悲催,明明是个好功能,但…

Case II
有个应用在发布的时候突然出现部分机器启动不了的现象,挖日志看到的是获取不到spring的dtd(貌似是因为最近spring网站不太稳定,不过暴露出来是好事),这个也算的上Java的经典问题,之前有篇专门说过这个问题。

于是开始查,看看是什么原因导致的,继续祭起btrace神器,跟踪的方法是跟踪什么地方调用了URL.openConnection,并且url是http开头,结尾是spring dtd的,很快就抓到了,发现竟然是jboss的SARDeployer在做parseDocument的时候调用到的,看的我当时真心傻眼了…

继续跟踪SARDeployer在parse哪些Document的时候会需要装载到spring dtd,看到SARDeployer在parse业务jar包里的几个*-service.xml的文件,这个时候才想起以前看过的jboss代码,SARDeployer是会扫描所有的deploy目录下的文件,包括jar包里的文件,并会尝试解析*-service.xml/*-ds.xml文件,看看是不是SARDeployer的描述文件…

这里用到的btrace脚本如下:
[code]
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
import org.jboss.deployment.DeploymentInfo;

@BTrace public class Trace{
@OnMethod(
clazz=”org.jboss.deployment.SARDeployer”,
method=”parseDocument”
)
public static void traceExecute(DeploymentInfo di){
printFields(di);
}

@OnMethod(
clazz=”java.net.URL”,
method=”openConnection”,
location=@Location(Kind.RETURN)
)
public static void resolveEntity(@Self Object instance){
String protocol = str(get(field(“java.net.URL”, “protocol”),instance));
String file = str(get(field(“java.net.URL”, “file”),instance));
if(startsWith(protocol,”http”) && (endsWith(file,”.xsd”) || endsWith(file,”.dtd”))){
String authority = str(get(field(“java.net.URL”, “authority”),instance));
String path = str(get(field(“java.net.URL”, “path”),instance));
println(“=====================================”);
print(protocol);
print(“://”);
print(authority);
print(path);
println(” not found!”);
println(“who call:”);
jstack();
}
}
}
[/code]

扫描到的业务jar包里的*-service.xml文件里确实写了
[code]

[/code]
而jboss里显然是获取不到这个dtd的,于是就导致了连到外网去获取这个dtd文件了,一个简单办法是把上面这段改成:
[code]

[/code]
然后把spring-beans.dtd放到对应的jar里,就万事大吉了。

ps: 对于跑在jboss里的应用,更安全的是不要给自己的xml取名为*-service.xml的文件,一般来说没人会取成*-ds.xml文件,以免出现一些这种乱七八糟的问题。

Case的解决通常回头看是不太复杂的,解决Case通常最需要的是两点:
1. 对背后原理的理解,否则其实即使看到现象,也不知道如何定位到造成问题的具体代码;
2. 经验…这个真心只能靠实战,有些时候经验够丰富的话不懂原理也是能解决问题的,这个确实见过。

我有个很好奇的问题,我很久都没推广过自己的这个公众账号了,但为啥还是每天都偶尔会有零星的几个新增的关注的人,有点奇怪,不知道大家能回复下我是从哪知道的这个公众账号吗? 谢谢。

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

解析xml时如何防范dtd/xsd访问外网

做Java的同学估计都碰过类似的故障,就是突然有一天启动应用的时候告诉你由于没获取到dtd或xsd文件应用启动失败,而通常这个时候你才会发现原来自己应用的启动竟然还依赖了外网的某个url,我记得当年ibatis切域名的那一次,就是突然一批机器启动失败,差点造成大故障,还有一次印象深刻的海底光缆断掉的那一次,也是差点引发大故障。

对于上面这样的现象,最好的办法自然是确保本地有相应的dtd/xsd文件,从而避免去外网加载,但为什么要做到这点这么麻烦呢,原因在于dtd/xsd文件到底从哪加载是完全可以由解析XML的程序自行来决定的,于是就悲催了。

在解析XML时可自行实现EntityResolver接口,例如Spring的BeansDtdResolver,在这个类中,Spring会首先从当前类的classloader中来获取相应路径的dtd文件,如找不到则从指定的classloader中获取,仍然找不到的话就返回null,当返回null后,外层的ResourceEntityResolver则会访问相应的url去获取dtd,对于xsd文件的处理也基本类似。

唯一还好的就是基本所有解析xml的程序在对dtd/xsd的处理上,都遵循了先尝试从本地装载,之后再从url装载的先后顺序。

对于这个头疼的问题,我之前见过的解决方法有:
1.将所有的dtd/xsd涉及的域名都解析到本地的一台机器,以避免访问外网的现象出现,这个的问题在于无法做到穷举,因为可能不是你的程序需要解析XML,而是你引用的其他jar中有;
2.禁止服务器访问外网,这样的好处就是问题暴露的比较直接,但可能有些应用确实会有需要访问外网的需求;
3.将xml文件中的dtd/xsd改为本地相对路径,这种一个问题是上面的穷举问题,另一个问题是相对路径到底如何访问其实同样取决于自定义实现的xml解析程序;
4.在解析xml时绕过dtd的验证,如果能保证xml文件格式没问题的话,可以绕掉dtd的验证,但xsd就不行了,xsd文件是必须获取到的,否则xml文件压根就没法解析。

目前我们没采用以上4个方法,在前年的时候由于连续出了好几次dtd/xsd访问不了导致各种应用启动不了的现象,我就花了点时间想办法去解决掉这个问题,当时我的解决思路是上面的4个方法在我们的场景中都不能解决,看起来一个折中的办法就是在测试阶段就发现这个问题,然后强迫应用解决掉,这样就可以确保部署到线上的时候访问的一定是本地。

要在测试阶段发现这个问题怎么做呢,想到的办法是用btrace来跟踪URL.openConnection的调用,如URL的结尾是.dtd或.xsd,那么就在日志中输出错误信息和堆栈信息,启动脚本在检查到错误信息后就让进程退出,这样可以确保应用方必须解决掉这个问题。

这个方法work的是挺好的,只是悲催的是在出现了访问外网dtd/xsd时要解决就不好弄了,通常是先尝试把dtd/xsd下载过来放到classpath下,但有些时候会不work,不work的情况下就只能是根据堆栈信息调试,看看为什么在本地没找到,然后在本地相应的路径下放上这个文件,这种case by case的解决过程是很苦逼的。

欢迎大家爆料你碰到过的这类问题,或你的解决方案。

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。