HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

目录导读

目录

1. 引言

HBase是一个非常复杂的系统,虽已诞生多年,且被广泛应用,但在日常的维护过程中,偶尔也会遇见莫名其妙的报错或BUG,有些问题会导致系统崩溃,有些问题则无伤大雅。

本文仅以一个小小的日志异常入手,记录自己分析和解决问题的经历,目的不在于好为人师,而只求能抛砖引玉。

2. 异常日志初现

我们线上HBase集群的版本是2.1.0-cdh6.3.2,集群已集成JDK15,并引入了ZGC。在一次集群重启后,查看RS的日志,日志中有如下警告信息:

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

此异常为警告异常,虽然发生,但还没达到影响集群运行的地步,但为了杜绝隐患,还是早早弄清楚为妙。

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源码篇 _ 记一次HBase高版本JDK兼容性排错

这个站点汇聚了大量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-25516https://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

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

jdk15

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

filterMap细节

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

相比于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几经波折,终于找到了靠谱的解决方案。

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

这里采取方案1,参考的代码片段如下:

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

反射获取字段的私有方法,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测试用例,验证新增代码正确性。

HBase源码篇 _ 记一次HBase高版本JDK兼容性排错

经过测试之后,修改的代码并无异常。之后便是打包替换线上jar包,并重启集群啦。

6. 总结

本文以HBase的一个异常日志着手,记录了分析、定位、和解决异常警告的一系列流程。文中的表述或个人拙见,如有纰漏,还望看到朋友及时帮忙纠正,同时也烦请告知,此异常的危害等级,是否会对集群的稳定运行有所威胁。

同时,在HBase或其他类似组件在升级高版本JDK的过程中,不可避免会遇到不少兼容性的问题。以下两篇文章,推荐给大家,或许有所帮助:

  • https://issues.apache.org/jira/browse/HBASE-22972
  • https://www.jianshu.com/p/81b65eded96c

7. 参考链接

所参考的链接已在文中体现,在此不一一罗列。

上一篇:Java 修饰符顺序 the sequential order of the modifiers


下一篇:编程基础技能