目录导读
目录1. 引言
HBase是一个非常复杂的系统,虽已诞生多年,且被广泛应用,但在日常的维护过程中,偶尔也会遇见莫名其妙的报错或BUG,有些问题会导致系统崩溃,有些问题则无伤大雅。
本文仅以一个小小的日志异常入手,记录自己分析和解决问题的经历,目的不在于好为人师,而只求能抛砖引玉。
2. 异常日志初现
我们线上HBase集群的版本是2.1.0-cdh6.3.2
,集群已集成JDK15,并引入了ZGC。在一次集群重启后,查看RS的日志,日志中有如下警告信息:
此异常为警告异常,虽然发生,但还没达到影响集群运行的地步,但为了杜绝隐患,还是早早弄清楚为妙。
3. 异常代码定位
初见此异常,一时间根本不知道如何下手,尤其是Java新手(大神则不跟咱是一个物种,所以暂且略过)。不明就里的我,开始时也是一顿百度和谷歌乱搜,然而,并没有得到有用的信息。
只能继续一层层(由上到下)查看异常栈:
at java.base/java.lang.Class.getDeclaredField(Class.java:2569)
JDK中最终被调用的方法,先暂时不管
at org.apache.hadoop.hbase.fs.HFileSystem.addLocationsOrderInterceptor(HFileSystem.java:334)
HFileSystem.java 第334行
看到调用栈的第二层,其实就能大致知道异常代码的调用逻辑。
在HFileSystem.java文件的第334行,调用了JDK中的Class.getDeclaredField方法,然后就报错啦。再探HFileSystem的源码:
try {
Field nf = DFSClient.class.getDeclaredField("namenode");
nf.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers"); // 第334行
} catch (NoSuchFieldException e) {
LOG.warn("Can't modify the DFSClient#namenode field to add the location reorder.", e); // 异常日志
return false;
} catch (IllegalAccessException e) {
LOG.warn("Can't modify the DFSClient#namenode field to add the location reorder.", e); // 异常日志
return false;
}
定位到异常代码之后,可以DEBUG这部分代码,就能复现这个异常的日志输出。Field modifiersField = Field.class.getDeclaredField("modifiers");
这行代码与HBase的整体执行逻辑无直接关系,也可以单独拎出来进行测试。
4. 异常原因分析
为什么会发生这样的异常呢?暂不深究源码,先来看看其他HBase用户是否遇到过相似的问题,打开HBase的issue站点,根据关键字搜索。
https://issues.apache.org/jira/projects/HBASE/issues/HBASE-23634?filter=allopenissues
这个站点汇聚了大量HBase相关的提问、解决方案和补丁等,绝对比百度或谷歌靠谱。
异常的原因至此就很清晰了,JDK版本的原因,我用的JDK版本是15,在该版本中反射调用Field.class.getDeclaredField("modifiers")
就会报错。
进一步验证原因:
try {
Field modifiersField = Field.class.getDeclaredField("modifiers");
System.out.println("success");
} catch (NoSuchFieldException e) {
e.printStackTrace();
System.out.println("failed");
}
分别在JDK1.8和JDK15环境中运行上述demo,在JDK15中会得到如下异常。
java.lang.NoSuchFieldException: modifiers
at java.base/java.lang.Class.getDeclaredField(Class.java:2569)
at org.apache.hadoop.hbase.fs.TestField.testFiledModifiers(TestField.java:14)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at
5. 异常解决方案
通过查看HBASE-25516
和https://bugs.openjdk.java.net/browse/JDK-8217225
中的解释,我们才知道,在更高版本的JDK(高于JDK1.8)中,核心反射增加了过滤机制,可以调试对比JDK1.8和JDK15 中的Field.class.getDeclaredFields()
或Field.class.getDeclaredMethods()
。下文仅以Field.class.getDeclaredFields()
为例,调试代码,对比分析,在此不记录详细的DEBUG过程,只记录最终结论。
jdk1.8
jdk15
filterMap细节
相比于jdk8,在jdk15中新增了8个过滤元素,
在Filed类中,针对所有私有属性全过滤;在Class类中,只有classData和classLoader两个私有字段被隐藏。大家可以尝试运行以下代码来验证结论:
Class.class.getDeclaredField("classLoader");
https://bugs.openjdk.java.net/browse/JDK-8210522
中有更详细的解释,概述为:
java.lang.reflect 和 java.lang.invoke 包中的许多类都有私有字段,如果直接访问这些字段,将危及运行时环境或使 JVM 崩溃。在理想情况下,java.base包中,类的所有非公共/非保护字段都将被核心反射过滤,并且不能通过 Unsafe API 进行读取/写入。
java.lang.ClassLoader
java.lang.reflect.AccessibleObject
java.lang.reflect.Constructor
java.lang.reflect.Field
java.lang.reflect.Method
// 上述类中的私有字段都被反射过滤,其中的私有字段java.lang.invoke.MethodHandles.Lookup用于查找类和访问模式。
再回头看HBase中HFileSystem.java中的那一小段代码。
// 反射获取Filed类中名为modifiers的字段
Field modifiersField = Field.class.getDeclaredField("modifiers");
// 把私有字段modifiers设置为可访问的状态(即private变为public)
modifiersField.setAccessible(true);
// 对modifiers字段进行赋值操作,即修改该字段的访问修饰符
modifiersField.setInt(nf, nf.getModifiers() & ~Modifier.FINAL);
// 上述三行代码与此方法内的整体逻辑,并没有直接关联,甚至说毫无关系。但此地反射修改了modifiers的私有属性,
// 或许会在其他调用栈内被使用。
在*
网站中,我们找到相对详细的解决方案,https://*.com/questions/56039341/get-declared-fields-of-java-lang-reflect-fields-in-jdk12
该方案中的解决方法,示例如下:
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public final class FieldHelper {
private static final VarHandle MODIFIERS;
static {
try {
var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
} catch (IllegalAccessException | NoSuchFieldException ex) {
throw new RuntimeException(ex);
}
}
public static void makeNonFinal(Field field) {
int mods = field.getModifiers();
if (Modifier.isFinal(mods)) {
MODIFIERS.set(field, mods & ~Modifier.FINAL);
}
}
}
官方推荐的做法是利用java.lang.invoke.VarHandle
机制来访问受保护的私有变量。类比此种方案,实际操作起来却没能实现,貌似不可行。关于java.lang.invoke.VarHandle
机制,可以参考:https://blog.csdn.net/sench_z/article/details/79793741
。
最终从 https://bugs.openjdk.java.net/browse/JDK-8217225
->https://bugs.openjdk.java.net/browse/JDK-8217225
->https://github.com/powermock/powermock/issues/939
几经波折,终于找到了靠谱的解决方案。
这里采取方案1,参考的代码片段如下:
反射获取字段的私有方法,getDeclaredFields0()可以获取到所有字段。修改之后的HFileSystem.java:
// Field modifiersField = Field.class.getDeclaredField("modifiers");
// modifiersField.setAccessible(true);
// modifiersField.setInt(nf, nf.getModifiers() & ~Modifier.FINAL);
Field modifiersField = getModifiersField();
modifiersField.setAccessible(true);
modifiersField.setInt(nf, nf.getModifiers() & ~Modifier.FINAL);
/**
* jdk(9, +) 中反射获取Field类中的modifiers字段,避免异常java.lang.NoSuchFieldException: modifiers
* @return modifiers field
*/
private static Field getModifiersField() throws IllegalAccessException, NoSuchFieldException {
Field modifiersField = null;
try{
modifiersField = Field.class.getDeclaredField("modifiers");
}catch (NoSuchFieldException e){
try {
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
boolean accessibleBeforeSet = getDeclaredFields0.isAccessible();
getDeclaredFields0.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
getDeclaredFields0.setAccessible(accessibleBeforeSet);
for (Field field : fields) {
if ("modifiers".equals(field.getName())) {
modifiersField = field;
break;
}
}
if (modifiersField == null) {
throw e;
}
} catch (NoSuchMethodException | InvocationTargetException ex) {
e.addSuppressed(ex);
throw e;
}
}
return modifiersField;
}
debug测试用例,验证新增代码正确性。
经过测试之后,修改的代码并无异常。之后便是打包替换线上jar包,并重启集群啦。
6. 总结
本文以HBase的一个异常日志着手,记录了分析、定位、和解决异常警告的一系列流程。文中的表述或个人拙见,如有纰漏,还望看到朋友及时帮忙纠正,同时也烦请告知,此异常的危害等级,是否会对集群的稳定运行有所威胁。
同时,在HBase或其他类似组件在升级高版本JDK的过程中,不可避免会遇到不少兼容性的问题。以下两篇文章,推荐给大家,或许有所帮助:
https://issues.apache.org/jira/browse/HBASE-22972
https://www.jianshu.com/p/81b65eded96c
7. 参考链接
所参考的链接已在文中体现,在此不一一罗列。