最近在查看生产环境时候, 发现了一个很奇怪的现象, 某个群集的一台机器8个CPU 被100%吃完。 拿到Java的线程栈的时候, 满满一大片, 几乎都停在了Hashmap.get/put 方法上。 刚开始, 我以为是velocity的bug(也的确是他的bug). 但是, 为什么会挂起来的呢, 令我很难明白。
- "pool-2-thread-1" prio=10 tid=0x007b0d88 nid=0x25 runnable [0xb0bff000..0xb0c01688]
- at java.util.HashMap.put(HashMap.java:420)
- at org.apache.velocity.util.introspection.ClassMap$MethodCache.get(ClassMap.java:271)
- at org.apache.velocity.util.introspection.ClassMap.findMethod(ClassMap.java:102)
- at org.apache.velocity.util.introspection.IntrospectorBase.getMethod(IntrospectorBase.java:105)
- at org.apache.velocity.util.introspection.Introspector.getMethod(Introspector.java:94)
- at org.apache.velocity.runtime.parser.node.PropertyExecutor.discover(PropertyExecutor.java:118)
- at org.apache.velocity.runtime.parser.node.PropertyExecutor.<init>(PropertyExecutor.java:56)
- at org.apache.velocity.util.introspection.UberspectImpl.getPropertyGet(UberspectImpl.java:246)
"pool-2-thread-1" prio=10 tid=0x007b0d88 nid=0x25 runnable [0xb0bff000..0xb0c01688]
at java.util.HashMap.put(HashMap.java:420)
at org.apache.velocity.util.introspection.ClassMap$MethodCache.get(ClassMap.java:271)
at org.apache.velocity.util.introspection.ClassMap.findMethod(ClassMap.java:102)
at org.apache.velocity.util.introspection.IntrospectorBase.getMethod(IntrospectorBase.java:105)
at org.apache.velocity.util.introspection.Introspector.getMethod(Introspector.java:94)
at org.apache.velocity.runtime.parser.node.PropertyExecutor.discover(PropertyExecutor.java:118)
at org.apache.velocity.runtime.parser.node.PropertyExecutor.<init>(PropertyExecutor.java:56)
at org.apache.velocity.util.introspection.UberspectImpl.getPropertyGet(UberspectImpl.java:246)
前阵子, 在查阅 JDK bug 库的时候, 曾经看到过 关于非正确使用Hashmap导致jvm的挂起 的问题。具体有如下解释:
This is a classic symptom of an incorrectly synchronized use of HashMap. Clearly, the submitters need to use a thread-safe HashMap. If they upgraded to Java 5, they could just use ConcurrentHashMap. If they can't do this yet, they can use either the pre-JSR166 version, or better, the unofficial backport as mentioned by Martin. If they can't do any of these, they can use Hashtable or synchhronizedMap wrappers, and live with poorer performance. In any case, it's not a JDK or JVM bug.
我们知道Hashmap不是读写线程安全的, 如果仅仅是全部只读才是线程安全的。 这是JDK文档的解释:
Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more mappings; merely changing the value associated with a key that an instance already contains is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the map. If no such object exists, the map should be "wrapped" using the Collections.synchronizedMap
method.
虽然, 我们知道Hashmap在被并发读写使用的时候, 会跑出 ConcurrentModificationException这个异常, 但是JDK文档明确指出, 这个异常抛出是属于 fail-fast 的一个设计方法, 目的是为了开发者能及早的意识到线程安全问题发生。 但是, 这个fail-fast不是一定会发生, 而是可能会发生的行为。 因此, 在一个不确定状态下的下,jvm线程发生持续100%cpu行为是比较容易理解了(for (Entry<K,V> e = table[i]; e != null; e = e.next), 估计是这个代码进了死循环的状态)。
我对velocity代码比较熟悉,结合了栈的情况, 可以看到velocity错误的使用了Hashmap作为方法cache的数据结构, 在并发处理初始化模板的时候,把机器的CPU 100%吃完的机会是会发生的。我感觉这个问题还是比较容易发生, 在短短的1个星期, 就观察到2次这个现象。
我查阅了下velocity的Bug列表, 果然有人报告过这个问题: https://issues.apache.org/jira/browse/VELOCITY-718 虽然bug已经修复, 但是要修改使用velocity的版本令我很担忧。 velocity在某些细节处理的兼容性上非常糟糕。 曾经升级过一次, 出现过无数的不兼容的行为。 这个bug的大部分因该出现系统的初始化阶段, 要么系统起来, 要么系统启动失败。 基于修改的风险性, 很是纠结。