月度归档:2014年06月

tomcat启动时检测到循环继承而栈溢出的问题

一个用户在使用tomcat7054版本启动的时候遇到的错误:

Caused by: java.lang.IllegalStateException: 
Unable to complete the scan for annotations for web application [/test] 
due to a StackOverflowError. Possible root causes include a too low setting 
for  -Xss and illegal cyclic inheritance dependencies. 

The class hierarchy being processed was 

[org.jaxen.util.AncestorAxisIterator->
org.jaxen.util.AncestorOrSelfAxisIterator->
org.jaxen.util.AncestorAxisIterator]

at org.apache.catalina.startup.ContextConfig.checkHandlesTypes(ContextConfig.java:2112)
at org.apache.catalina.startup.ContextConfig.processAnnotationsStream(ContextConfig.java:2059)
at org.apache.catalina.startup.ContextConfig.processAnnotationsJar(ContextConfig.java:1934)
at org.apache.catalina.startup.ContextConfig.processAnnotationsUrl(ContextConfig.java:1900)
at org.apache.catalina.startup.ContextConfig.processAnnotations(ContextConfig.java:1885)
at org.apache.catalina.startup.ContextConfig.webConfig(ContextConfig.java:1317)
at org.apache.catalina.startup.ContextConfig.configureStart(ContextConfig.java:876)
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:374)
at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5355)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)

这是在tomcat解析servlet3注释时进行类扫描的过程,发现了两个类的继承关系存在循环继承的情况而导致了栈溢出。

排查了一下,是因为应用所依赖的 dom4j-1.1.jar 里存在 AncestorAxisIterator 和子类 AncestorOrSelfAxisIterato

% javap org.jaxen.util.AncestorAxisIterator

Compiled from "AncestorAxisIterator.java"
public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.StackedIterator {
    protected org.jaxen.util.AncestorAxisIterator();
    public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator);
    protected java.util.Iterator createIterator(java.lang.Object);
}

% javap org.jaxen.util.AncestorOrSelfAxisIterator

Compiled from "AncestorOrSelfAxisIterator.java"
public class org.jaxen.util.AncestorOrSelfAxisIterator extends org.jaxen.util.AncestorAxisIterator {
    public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator);
    protected java.util.Iterator createIterator(java.lang.Object);
} 

同时应用所依赖的 sourceforge.jaxen-1.1.jar 里面也存在这两个同名类,但继承关系正好相反:

% javap org.jaxen.util.AncestorAxisIterator

Compiled from "AncestorAxisIterator.java"
public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.AncestorOrSelfAxisIterator {
    public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator);
}

% javap org.jaxen.util.AncestorOrSelfAxisIterator

Compiled from "AncestorOrSelfAxisIterator.java"
public class org.jaxen.util.AncestorOrSelfAxisIterator implements java.util.Iterator {
    public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator);
    public boolean hasNext();
    public java.lang.Object next();
    public void remove();
}

简单的说,在第1个jar里存在 B继承自A,在第2个jar里存在同名的A和B,但却是A继承自B。其实也能运行的,只是可能出现类加载时可能加载的不一定是你想要的那个,但tomcat做类型检查的时候把这个当成了一个环。

ContextConfig.processAnnotationsStream方法里,每次解析之后要对类型做一次检测,然后才获取注释信息:

ClassParser parser = new ClassParser(is, null);
JavaClass clazz = parser.parse();
checkHandlesTypes(clazz);
...
AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
...

再看这个用来检测类型的checkHandlesTypes方法里面:

populateJavaClassCache(className, javaClass);
JavaClassCacheEntry entry = javaClassCache.get(className);
if (entry.getSciSet() == null) {
    try {
        populateSCIsForCacheEntry(entry); // 这里
    } catch (StackOverflowError soe) {
        throw new IllegalStateException(sm.getString(
            "contextConfig.annotationsStackOverflow",context.getName(),
            classHierarchyToString(className, entry)));
    }
}

每次新解析出来的类(tomcat里定义了JavaClass来描述),会被populateJavaClassCache放入cache,这个cache内部是个Map,所以对于key相同的会存在把以前的值覆盖了的情况,这个“环形继承”的现象就比较好解释了。

Map里的key是String类型即类名,value是JavaClassCacheEntry类型封装了JavaClass及其父类和接口信息。我们假设第一个jar里B继承自A,它们被放入cache的时候键值对是这样的:

"A" -> [JavaClass-A, 父类Object,父接口]"
"B" -> [JavaClass-B, 父类A,父接口]

然后当解析到第2个jar里的A的时候,覆盖了之前A的键值对,变成了:

"A" -> [JavaClass-A, 父类B,父接口]
"B" -> [JavaClass-B, 父类A,父接口]

这2个的继承关系在这个cache里被描述成了环状,然后在接下来的populateSCIsForCacheEntry方法里找父类的时候就绕不出来了,最终导致了栈溢出。

这个算是cache设计不太合理,没有考虑到不同jar下面有相同的类的情况。问题确认之后,让应用方去修正自己的依赖就可以了,但应用方说之前在7026的时候,是可以正常启动的。这就有意思了,接着一番排查之后,发现在7026版本里,ContextConfig.webConfig的时候先判断了一下web.xml里的版本信息,如果版本>=3才会去扫描类里的servlet3注释信息。

// Parse context level web.xml
InputSource contextWebXml = getContextWebXmlSource();
parseWebXml(contextWebXml, webXml, false);

if (webXml.getMajorVersion() >= 3) {
    // 扫描jar里的web-fragment.xml 和 servlet3注释信息
    ...
}

而在7054版本里是没有这个判断的。搜了一下,发现是在7029这个版本里去掉的这个判断。在7029的changelog里:

As per section 1.6.2 of the Servlet 3.0 specification and clarification from the Servlet Expert Group, the servlet specification version declared in web.xml no longer controls if Tomcat scans for annotations. Annotation scanning is now always performed – regardless of the version declared in web.xml – unless metadata complete is set to true.

之前对servlet3规范理解不够清晰;之所以改,是因为在web.xml里定义的servlet版本,不再控制tomcat是否去扫描每个类里的注释信息。也就是说不管web.xml里声明的servlet版本是什么,都会进行注释扫描,除非metadata-complete属性设置为true(默认是false)。

所以在7029版本之后改为了判断 webXml.isMetadataComplete() 是否需要进行扫描注释信息。

JVM上的随机数与熵池策略

在apache-tomcat官方文档:如何让tomcat启动更快 里面提到了一些启动时的优化项,其中一项是关于随机数生成时,采用的“熵源”(entropy source)的策略。

他提到tomcat7的session id的生成主要通过java.security.SecureRandom生成随机数来实现,随机数算法使用的是”SHA1PRNG”

private String secureRandomAlgorithm = "SHA1PRNG";

在sun/oracle的jdk里,这个算法的提供者在底层依赖到操作系统提供的随机数据,在linux上,与之相关的是/dev/random/dev/urandom,对于这两个设备块的描述以前也见过讨论随机数的文章,wiki中有比较详细的描述,摘抄过来,先看/dev/random

在读取时,/dev/random设备会返回小于熵池噪声总数的随机字节。/dev/random可生成高随机性的公钥或一次性密码本。若熵池空了,对/dev/random的读操作将会被阻塞,直到收集到了足够的环境噪声为止

/dev/urandom 则是一个非阻塞的发生器:

dev/random的一个副本是/dev/urandom (”unlocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于/dev/random的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

另外wiki里也提到了为什么linux内核里的随机数生成器采用SHA1散列算法而非加密算法,是为了避开法律风险(密码出口限制)。

回到tomcat文档里的建议,采用非阻塞的熵源(entropy source),通过java系统属性来设置:

-Djava.security.egd=file:/dev/./urandom

这个系统属性egd表示熵收集守护进程(entropy gathering daemon),但这里值为何要在devrandom之间加一个点呢?是因为一个jdk的bug,在这个bug的连接里有人反馈及时对 securerandom.source 设置为 /dev/urandom 它也仍然使用的 /dev/random,有人提供了变通的解决方法,其中一个变通的做法是对securerandom.source设置为 /dev/./urandom 才行。也有人评论说这个不是bug,是有意为之。

我看了一下我当前所用的jdk7的java.security文件里,配置里仍使用的是/dev/urandom

#
# Select the source of seed data for SecureRandom. By default an
# attempt is made to use the entropy gathering device specified by
# the securerandom.source property. If an exception occurs when
# accessing the URL then the traditional system/thread activity
# algorithm is used.
#
# On Solaris and Linux systems, if file:/dev/urandom is specified and it
# exists, a special SecureRandom implementation is activated by default.
# This "NativePRNG" reads random bytes directly from /dev/urandom.
#
# On Windows systems, the URLs file:/dev/random and file:/dev/urandom
# enables use of the Microsoft CryptoAPI seed functionality.
#
securerandom.source=file:/dev/urandom

我不确定jdk7里,这个 /dev/urandom 也同那个bug报告里所说的等同于 /dev/random;要使用非阻塞的熵池,这里还是要修改为/dev/./urandom 呢,还是jdk7已经修复了这个问题,就是同注释里的意思,只好验证一下。

使用bug报告里给出的代码:

import java.security.SecureRandom;
class JRand {
    public static void main(String args[]) throws Exception {
        System.out.println("Ok: " +
            SecureRandom.getInstance("SHA1PRNG").nextLong());
    }
}

然后设置不同的系统属性来验证,先是在我的mac上:

% time java -Djava.security.egd=file:/dev/urandom  JRand
Ok: 8609191756834777000
java -Djava.security.egd=file:/dev/urandom JRand  
0.11s user 0.03s system 115% cpu 0.117 total

% time java -Djava.security.egd=file:/dev/./urandom  JRand
Ok: -3573266464480299009
java -Djava.security.egd=file:/dev/./urandom JRand  
0.11s user 0.03s system 116% cpu 0.116 total

可以看到/dev/urandom/dev/./urandom 的执行时间差不多,有点纳闷,再仔细看一下wiki里说的:

FreeBSD操作系统实现了256位的Yarrow算法变体,以提供伪随机数流。与Linux的/dev/random不同,FreeBSD的/dev/random不会产生阻塞,与Linux的/dev/urandom相似,提供了密码学安全的伪随机数发生器,而不是基于熵池。而FreeBSD的/dev/urandom则只是简单的链接到了/dev/random。

尽管在我的mac上/dev/urandom并不是/dev/random的链接,但mac与bsd内核应该是相近的,/dev/random也是非阻塞的,/dev/urandom是用来兼容linux系统的,这两个随机数生成器的行为是一致的。参考这里

然后再到一台ubuntu系统上测试:

% time java -Djava.security.egd=file:/dev/urandom  JRand
Ok: 6677107889555365492
java -Djava.security.egd=file:/dev/urandom JRand  
0.14s user 0.02s system 9% cpu 1.661 total

% time java -Djava.security.egd=file:/dev/./urandom  JRand
Ok: 5008413661952823775
java -Djava.security.egd=file:/dev/./urandom JRand  
0.12s user 0.02s system 99% cpu 0.145 total

这回差异就完全体现出来了,阻塞模式的熵池耗时用了1.6秒,而非阻塞模式则只用了0.14秒,差了一个数量级,当然代价是转换为对cpu的开销了。

// 补充,连续在ubuntu上测试几次/dev/random方式之后,导致熵池被用空,被阻塞了60秒左右。应用服务器端要避免这种方式。

最近看过的电影(5)

《少年斯派维的奇异旅行》是一部很有趣的电影,一个小神童眼里的世界,很温情。火车从蒙大拿到纽约一路的风景也很美,只是后半部编剧略显仓促。

最近看过的电影(4)

上周在枫林晚看到了这本书,根据这个故事改编的电影《谜一样的双眼》我几年前看过,电影很精彩,对阿根廷的法制体系进行了批判,看到结尾时很震撼,这是需要怎样的一种爱和恨才能做出这样的事。不过小说的结局与电影略不同,相比我可能更喜欢电影里的结局。

tomcat启动失败时的ClassNotFoundException

tomcat在部署应用时,如果部署失败,该应用会被stop,但如果该应用的WebappClassLoader被其它线程持有并且继续使用的话,可能发生异常。

应用部署失败被stop的时候,classLoader也被stop(因为WebappClassLoader实现了Lifecycle接口),stop的时候,相关的 files/jars/resoures/repositories/parent 等等都被清除掉了,同时标识应用是否启动的started状态也被设置为false;基本上该classloader已经不可用了。(除了还可以委托给j2seClassLoader这个bootstrap classloader,这个classloader实际运行时是ExtClassLoader)

假设应用里有一个类自己启动了一个线程,逻辑大致如下:

static {
    new Thread(){
        public void run(){
            // 1) 先sleep一段时间,确保应用完成初始化
            // 2) 然后执行一些逻辑
        }
    }.start();
}

上面线程在执行后续逻辑的时候,使用WebappClassLoader去加载类,如果在sleep的阶段app在tomcat中没有启动成功,被stop了,则后边的逻辑有些意思。

看一下loadClass方法:

    public synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {

    if (log.isDebugEnabled())
        log.debug("loadClass(" + name + ", " + resolve + ")");
    Class<?> clazz = null;

    // Log access to stopped classloader
    if (!started) {
        try {
            throw new IllegalStateException(); //这里抛出异常,又被自己捕获
        } catch (IllegalStateException e) {
            log.warn(sm.getString("webappClassLoader.stopped", name));
        }
    }

    // (0) Check our previously loaded local class cache
    ...

    // (0.1) Check our previously loaded class cache
    ...

    // (0.2) Try loading the class with the system class loader, to prevent
    //       the webapp from overriding J2SE classes
    ...

    // (0.5) Permission to access this class when using a SecurityManager
    ...

    // (1) Delegate to our parent if requested
    ...

    // (2) Search local repositories
    ...

    // (3) Delegate to parent unconditionally
    ...

    throw new ClassNotFoundException(name);

}

loadClass方法里判断了started状态,未启动的情况下,抛出异常,又自己捕获,仅仅记录了一下日志。然后继续后边的逻辑,先从cache里找,然后委托j2se classloader,再委派parent classloader,以及从本地仓库寻找。找不到抛出ClassNotFoundException

前面提过应用在stop时webappclassloader里的cache/parent等有关资源都被清空了,如果应用里启动的线程后续逻辑要求找的类,不是j2seclassloader可以找得到的类的话,就会抛出异常。

很多情况下,开发人员总是被这个ClassNotFoundException所迷惑, 如果仔细看日志的话,已经给出了应用被stopped的信息:

2014-6-17 21:58:42 org.apache.catalina.loader.WebappClassLoader loadClass  
信息: Illegal access: this web application instance has been stopped already.    

Pandora的启动优化

pandora3进行了很多重构,上周花了几天时间对启动过程做了一些优化。官方的tomcat只部署一个及简单的servlet应用,在我的机器上启动过程大概在1秒左右,如果应用使用了spring-mvc框架,启动过程大约在2000-2400毫秒左右。

如果使用Ali-Tomcat,启动过程中要先启动pandora容器,同一个spring-mvc的样例应用,总启动时间在4-5秒左右。优化的手段主要有2点:1) 部分模块异步化加载(前提是其它模块以及应用的启动对该模块没有依赖)。 2) 在自定义的classloader.loadClass方法里针对java7开启并行加载(在我的场景下所加载的类大约1000-2000个,使用4个线程,性能上约有20-30%的提升)。

java7的classloader所支持的并行加载,实质是把原先loadClass方法原先粗粒度的锁synchronized(this)换成了细粒度的锁synchronized (getClassLoadingLock(name)),为每个class都分配一个锁。这部分特性其实可以移植到自定义的Classloader实现里来,使得jdk6上也可以享用并行加载。

自定义的classloader要使用jdk7的并行加载机制,需要在静态构造块里注册为可并行加载:

static {
    ClassLoader.registerAsParallelCapable();
}

注意它要求当前classloader所有的父类都进行注册才行,比如我们在自定义的 MyClassLoader中覆盖了loadClass方法, 假设它的父类是 ClassLoaderBase,而ClassLoaderBase又继承自系统的URLClassLoader,那么要求URLClassLoaderClassLoaderBase 都必须在静态块中有注册声明可并行加载才行(URLClassLoader及其父类在jdk代码中是有注册的)。

另外,使用spring-mvc框架的应用,之所以比普通的servlet启动时慢,很大一部分原因是在于servlet3的web-fragment.xml合并的过程比较耗时。这个过程是在Pandora3启动后,输出应用启动日志过程中体会到的一个明显的“卡顿”:

2014/06/18 10:36:09:588 [INFO] Pandora - Container started. time elapsed: 439 ms
2014-06-18 10:36:10,740 org.springframework.web.servlet.FrameworkServlet initServletBean

这中间居然耗费了将近1.2秒,通过开启日志debug,发现主要的耗时在 WebXml.merge 方法上。原因是servlet3引入了web模块化配置,对原先的web.xml可以由多个“片段”组成,这些片段使用web-fragment.xml来描述,可以对一组servlet/Filter/Listener + web-fragment.xml 打成一个jar,即一个web模块。

这种做法带来一定灵活性,但要求应用在启动时,必须对WEB-INF/lib下的jar,以及其它路径下classloader可访问的jar(这部分可配置,默认包含)进行检查,合并web-fragment.xml片段。

Tomcat对jar中的web-fragment.xml的扫描主要通过JarScanner实现的,它也提供了配置来忽略哪些jar文件;在conf/catalina.properties里定义了tomcat.util.scan.DefaultJarScanner.jarsToSkip对哪些jar文件忽略。

使用spring-mvc框架的应用,依赖了一堆spring的jar,而这些jar没有在默认的忽略列表里,在扫描和合并web-fragment.xml的过程中会比较耗时。

最近看过的电影(3)

《梦想阿根廷》一部带有魔幻现实主义色彩的电影,故事背景发生在1976-1983年的军事独裁期间。里面有一个场景是主人公找他失踪的妻子时到了一个偏僻的农场,那个农场养着非常多的、各种种类的鸟儿;农场的主人是犹太人,曾被纳粹关在集中营里过,二战结束后来到了阿根廷。农场主说他们在集中营的时候,电线上时不时飞来一些鸟儿,这些鸟儿带给他们一些自由的幻想。后来纳粹士兵在电线上通了电,一些鸟儿被电死,之后鸟儿们再也不敢飞过去,他们就像失去了对自由的希望一样。所以当他来到阿根廷后,买下那块农场后在当地的市场上买下了所有的鸟儿,让这些鸟儿在这里自由生长。

googleapis被墙,更换了一下博客主题

本来一直使用wordpress默认的主题,风格也挺好的,简洁、干净;但最近页面打开的总是很慢,在排除了服务器端的问题之后,通过浏览器定位发现每次都是因为某个css里要访问googleapis下的内容,而googleapis被墙掉了,导致资源加载非常慢。

要么修改默认主题里的css不要访问font.googleapis,要么换一个不会访问googleapis的主题。尝试了几个主题,现在用的这个还凑合,就是展示代码的效果不太好。先用着吧,有好的主题欢迎推荐给我。

华东地区scala爱好者聚会(2014上海)

感谢看处方汪院长烨明,为这次活动提供了场地和零食,去年在上海的scala聚会也是看处方的汪院长发起的。这次聚会人比上次略微多一些了,而且整体水平是不断提升的。

聚石分享了来往使用scala构建通讯的一些经验,来往的IM部分采用自己的协议(有些类似spdy,要简单很多),在协议网关的实现上主要使用scala、akka,他主要分享了这个过程中踩过的一些坑。

诺铁的分享是scala集合部分,这部分内容是小中见大的,有部分内容来自《scala in depth》(顺便提一下他翻译的这本书快要出版了)。整个ppt很清晰,把Vector的细节讲的比较透,另外还提到Set的一个细节是继承自(T)=>Boolean这个函数类型,也就是contains方法,比如Set(1,2,3)(1)将返回true,这种设计确实怪异,容易误解。

烨明讲的是《play中的Iteratee》,理论结合实践,内容很不错,可惜我对play没怎么玩过,有些东西不太了解。

Max lv分享的《基于 Scala 进行 Android 开发的报告》ppt本身并没有太大亮点,不过吕超本人倒是挺有料的,在NVIDIA研究GPU高性能计算,业余喜欢搞搞android开发,他也是唐茶字节社这款app的开发者,和goagent的主要维护者。他不怎么用微博,相互交换了一下twitter。

这次聚会还有一些创业团队或其他IT公司,大众点评和唯品会以及其他很多公司也在尝试scala/spark,目前来看spark已经成为了scala领域最有影响力的产品。

从报名表上看到有好几个乔布堂的人,去年在杭州聚会的时候他们也来过,跟他们聊了一下他们创业的技术经验:服务器搭建在阿里云上,比托管在机房靠谱。阿里云服务器的稳定性他们还是比较满意的(看处方的服务器使用的是腾讯云,这跟创始人之前在腾讯工作有关)。乔布堂后端使用nginx+jetty+mongodb的架构,当前还没有使用cache,他们比较特殊的一点是web框架采用的lift,估计算是比较小众的(去年的时候诺铁分享过一次lift)。

回去的路上还碰到一个在银联做Jboss相关开发的哥们,从他那儿了解到了银联正准备从weblogic切换到jboss上;银联也是Tuxedo中间件产品的大客户。

补充,ppt的下载地址:https://github.com/CSUG/csug/tree/master/shanghai-2014-5-31