HBase是一个非常复杂的系统,虽已诞生多年,且被广泛应用,但在日常的维护过程中,偶尔也会遇见莫名其妙的报错或BUG,有些问题会导致系统崩溃,有些问题则无伤大雅。
本文仅以一个小小的日志异常入手,记录自己分析和解决问题的经历,目的不在于好为人师,而只求能抛砖引玉。
我们线上HBase集群的版本是2.1.0-cdh6.3.2
,集群已集成JDK15,并引入了ZGC。在一次集群重启后,查看RS的日志,日志中有如下警告信息:
此异常为警告异常,虽然发生,但还没达到影响集群运行的地步,但为了杜绝隐患,还是早早弄清楚为妙。
初见此异常,一时间根本不知道如何下手,尤其是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的整体执行逻辑无直接关系,也可以单独拎出来进行测试。
为什么会发生这样的异常呢?暂不深究源码,先来看看其他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
通过查看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的私有属性, // 或许会在其他调用栈内被使用。
在stackoverflow
网站中,我们找到相对详细的解决方案,https://stackoverflow.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包,并重启集群啦。
本文以HBase的一个异常日志着手,记录了分析、定位、和解决异常警告的一系列流程。文中的表述或个人拙见,如有纰漏,还望看到朋友及时帮忙纠正,同时也烦请告知,此异常的危害等级,是否会对集群的稳定运行有所威胁。
同时,在HBase或其他类似组件在升级高版本JDK的过程中,不可避免会遇到不少兼容性的问题。以下两篇文章,推荐给大家,或许有所帮助:
https://issues.apache.org/jira/browse/HBASE-22972
https://www.jianshu.com/p/81b65eded96c
所参考的链接已在文中体现,在此不一一罗列。