在过去的十年中,开源运动一直是IT行业发展的关键驱动力之一。开源项目的作用日益突出,不仅在数量上,而且在质量方面。这改变了开源软件在 IT 市场的总体定位的概念。今天,我们要讨论阿帕奇蜂巢。

Hadoop and Apache Hive

哈多普和阿帕奇蜂巢

关于阿帕奇蜂巢

Apache Hadoop目前被认为是开创性的大数据技术之一。其主要任务是存储、处理和管理大量数据。构成框架的主要组件是HadoopCommon、HDFS、HadoopMapReduce和HadoopYARN。 随着时间推移,围绕 Hadoop 开发了一个大型相关项目和技术生态系统,其中许多最初作为项目的一部分启动,然后开始独立。阿帕奇蜂巢就是其中之一。

Apache Hive 是一个分布式数据仓库。它管理存储在 HDFS 中的数据,并提供基于 SQL (HiveQL) 的查询语言来处理该数据。有关该项目的更多细节可在此处找到。

运行分析

开始分析不需要花太多精力或时间。下面是我的算法:

  • 从GitHub下载阿帕奇蜂巢。
  • 阅读有关启动 Java 分析器的指南并启动分析。
  • 获取分析器的报告,研究它,并写出最有趣的案例。

分析结果如下:6500+ 个文件上的 1,456 个高和中级警告(分别为 602 和 854)。

并非所有警告都引用真正的错误。这是很正常的;在开始定期使用分析器之前,必须调整分析器的设置。之后,您通常期望误报率相当低。

我遗漏了测试文件触发的 407 个警告(177 个高级别和 230 个中等级别)。我还忽略了V6022诊断(因为当您不熟悉代码时,您无法可靠地区分错误片段和正确的片段),该诊断被触发多达 482 次。我也没有检查V6021诊断生成的 179 个警告。

最后,我仍然有足够的警告去,由于我没有调整设置,仍然有一定比例的误报。在这样的文章中包含太多的警告是没有意义的。所以,我们只是谈谈什么引起了我的眼睛,看起来足够好奇。

预定条件

在为此分析检查的诊断中,V6007保存了已发出警告数的记录。超过200条信息!有些看起来无害,有些可疑,还有一些毕竟是真正的虫子!让我们来看看其中的一些启动与(”hplsql.”)总是正确的。Exec.java(675)

void initOptions() {
  ....
  if (key == null || value == null || !key.startsWith("hplsql.")) { // <= 
    continue; 
  } else if (key.compareToIgnoreCase(Conf.CONN_DEFAULT) == 0) {
    ....
  } else if (key.startsWith("hplsql.conn.init.")) {
    ....
  } else if (key.startsWith(Conf.CONN_CONVERT)) {
    ....
  } else if (key.startsWith("hplsql.conn.")) {
    ....
  } else if (key.startsWith("hplsql.")) { // <=
    ....
  }
}

这是一个相当冗长的如果-else-如果构造!分析器不喜欢最后一个 if ( key.startsWith("hplsql." )中的条件,因为如果执行达到它,则表示它为 true。实际上,如果您查看此整个 if-else if 构造的第一行,您将看到它已包含相反的检查,因此,如果字符串未以 "hplsql." 开头,则执行将立即跳到下一个迭代。

V6007 Expression 'columnNameProperty.length() == 0' is always false.
OrcRecordUpdater.java(238)

private static 
TypeDescription getTypeDescriptionFromTableProperties(....) 
{
  ....
  if (tableProperties != null) {
    final String columnNameProperty = ....;
    final String columnTypeProperty = ....;

    if (   !Strings.isNullOrEmpty(columnNameProperty)
        && !Strings.isNullOrEmpty(columnTypeProperty)) 
    {
      List<String> columnNames = columnNameProperty.length() == 0 
                                 ? new ArrayList<String>() 
                                 : ....;

      List<TypeInfo> columnTypes = columnTypeProperty.length() == 0 
                                   ? new ArrayList<TypeInfo>() 
                                   : ....;
      ....
      }
    }
  }
  ....
}

列 NameProperty 字符串的长度与零的比较将始终返回 false。这是因为此比较在 !Strings.isNullOrEmpty(columnNameProperty) 检查之后。因此,如果执行达到我们的条件,则意味着列 NameProperty 字符串肯定既不为空,也不为空。

列TypeProperty 字符串一行后也是如此:

V6007 Expression 'colOrScalar1.equals("Column")' is always false.
GenVectorCode.java(3469)

private void 
generateDateTimeArithmeticIntervalYearMonth(String[] tdesc) throws Exception
{
  ....
  String colOrScalar1 = tdesc[4];
  ....
  String colOrScalar2 = tdesc[6];
  ....
  if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column"))    // <=
  {
    ....
  } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) 
  {
    ....
  } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column"))    
  {
    ....
  }
}

好的旧复制粘贴。从当前逻辑的角度来看,字符串 colOrScalar1 可能同时具有两个不同的值,这是不可能的。显然,检查中应该有变量,colOrScalar1,在左边,在右边。

以下几行类似警告:

  • V6007 Expression 'colOrScalar1.equals("Scalar")' is always false.
    GenVectorCode.java(3475)
  • V6007 Expression 'colOrScalar1.equals("Column")' is always false.
    GenVectorCode.java(3486)

因此,此 if-else-if 构造将永远不会执行任何操作。

更多 V6007 警告:

  • V6007 Expression 'characters == null' is always false. RandomTypeUtil.java(43)
  • V6007 Expression 'fields

服务器.java(983)

  • V6007 Expression 'writeIdHwm > 0' is always false. TxnHandler.java(1603)
  • V6007 Expression 'currentGroups != null' is always true.
    GenericUDFCurrentGroups.java(90)
  • V6007 Expression 'this.wh == null' is always false. New returns not-null
    reference
  • StorageBasedAuthorizationProvider.java(93), StorageBasedAuthorizationProvider.
    java(92)
  • NPE

    V6008 Potential null dereference of 'dagLock'. QueryTracker.java(557),
    QueryTracker.java(553)

    private void handleFragmentCompleteExternalQuery(QueryInfo queryInfo)
    {
      if (queryInfo.isExternalQuery()) 
      {
        ReadWriteLock dagLock = getDagLock(queryInfo.getQueryIdentifier());
        if (dagLock == null) {
          LOG.warn("Ignoring fragment completion for unknown query: {}",
              queryInfo.getQueryIdentifier());
        }
        boolean locked = dagLock.writeLock().tryLock();
        .....
      }
    }

    捕获、记录和…程序只是保持运行。因此,检查后跟一个空指针取消引用。哎哟!

    开发人员必须实际上希望程序退出函数,或者在获得空引用的情况下引发一些特殊异常。

    V6008 Null dereference of 'buffer' in function 'unlockSingleBuffer'.
    MetadataCache.java(410), MetadataCache.java(465)

    private boolean lockBuffer(LlapBufferOrBuffers buffers, ....) 
    {
      LlapAllocatorBuffer buffer = buffers.getSingleLlapBuffer();
      if (buffer != null) {                              // <=
        return lockOneBuffer(buffer, doNotifyPolicy);
      }
      LlapAllocatorBuffer[] bufferArray = buffers.getMultipleLlapBuffers();
      for (int i = 0; i < bufferArray.length; ++i) {
        if (lockOneBuffer(bufferArray[i], doNotifyPolicy)) continue;
        for (int j = 0; j < i; ++j) {
          unlockSingleBuffer(buffer, true);               // <=
        }
        ....
      }
      ....
    }
    ....
    private void unlockSingleBuffer(LlapAllocatorBuffer buffer, ....) {
      boolean isLastDecref = (buffer.decRef() == 0);      // <=
      if (isLastDecref) {
        ....
      }
    }

    另一个潜在的NPE。如果执行达到解锁单一缓冲区方法,则表示缓冲区对象为 null。假设这是发生了什么事!如果您查看解锁SingleBuffer方法,您会注意到我们的对象是如何在第一行中取消引用的。疑难 杂症!

    一个转变去野生

    V6034 Shift by the value of 'bitShiftsInWord - 1' could be inconsistent with the
    size of type: 'bitShiftsInWord - 1' = [-1 .. 30]. UnsignedInt128.java(1791)
     

    private void shiftRightDestructive(int wordShifts,
                                       int bitShiftsInWord,
                                       boolean roundUp) 
    {
      if (wordShifts == 0 && bitShiftsInWord == 0) {
        return;
      }
    
      assert (wordShifts >= 0);
      assert (bitShiftsInWord >= 0);
      assert (bitShiftsInWord < 32);
      if (wordShifts >= 4) {
        zeroClear();
        return;
      }
    
      final int shiftRestore = 32 - bitShiftsInWord;
    
      // check this because "123 << 32" will be 123.
      final boolean noRestore = bitShiftsInWord == 0;
      final int roundCarryNoRestoreMask = 1 << 31;
      final int roundCarryMask = (1 << (bitShiftsInWord - 1));  // <=
      ....
    }

    这是一个潜在的转变 -1。如果用 wordShifts = 3 和位 ShiftsInWord = 0 调用该方法,则报告的行最终将为 1 <<-1。那是有计划的行为吗?

    V6034 Shift by the value of 'j' could be inconsistent with the size of type:
    'j' = [0
    63]. IoTrace.java (272)

    public void logSargResult(int stripeIx, boolean[] rgsToRead)
    {
      ....
      for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) {
        long val = 0;
        for (int j = 0; j < 64; ++j) {
          int ix = valOffset + j;
          if (rgsToRead.length == ix) break;
          if (!rgsToRead[ix]) continue;
          val = val | (1 << j);                // <=
        }
        ....
      }
      ....
    }

    在报告的行中,j 变量的值可以位于 [0 . . 63] 范围内。因此,循环中 val 值的计算可能会以意外的方式运行。在 (1 << j) 表达式中,值 1 为 int 类型,因此将其偏移 32 位或更多会带我们超出类型范围的限制。这可以通过写入 ((长)1 << j) 来修复。

    通过日志记录离开

    V6046 Incorrect format. A different number of format items is expected.
    Arguments not used: 1, 2. StatsSources.java(89)

    private static 
    ImmutableList<PersistedRuntimeStats> extractStatsFromPlanMapper (....) {
      ....
      if (stat.size() > 1 || sig.size() > 1)
      {
        StringBuffer sb = new StringBuffer();
        sb.append(String.format(
          "expected(stat-sig) 1-1, got {}-{} ;",    // <=
          stat.size(),
          sig.size()
        ));
        ....
      }
      ....
      if (e.getAll(OperatorStats.IncorrectRuntimeStatsMarker.class).size() > 0)
      {
        LOG.debug(
          "Ignoring {}, marked with OperatorStats.IncorrectRuntimeStatsMarker",
          sig.get(0)
        );
        continue;
      }
      ....
    }

    当编写代码以使用 format() 方法格式化字符串时,开发人员使用了错误的语法。因此,传递的参数永远不会达到生成的字符串。我的猜测是,开发人员在编写本文之前一直在编写日志记录,这是他们从中借用语法的地方。

    被盗异常

    V6051 The use of the 'return' statement in the 'finally' block can lead to the
    loss of unhandled exceptions. ObjectStore.java(9080)

    private List<MPartitionColumnStatistics> 
    getMPartitionColumnStatistics(....)
    throws NoSuchObjectException, MetaException 
    {
      boolean committed = false;
    
      try {
        .... /*some actions*/
    
        committed = commitTransaction();
    
        return result;
      } 
      catch (Exception ex) 
      {
        LOG.error("Error retrieving statistics via jdo", ex);
        if (ex instanceof MetaException) {
          throw (MetaException) ex;
        }
        throw new MetaException(ex.getMessage());
      } 
      finally 
      {
        if (!committed) {
          rollbackTransaction();
          return Lists.newArrayList();
        }
      }
    }

    从最后一个块返回任何东西是非常糟糕的做法,这个例子生动地说明了原因。

    在 try 块中,程序正在形成请求并访问存储。默认情况下,提交的变量的值为 false,并且仅在 try 块中的所有先前操作成功执行后更改其状态。这意味着,如果引发异常,则该变量将始终为 false。catch 块将捕获异常,稍微调整一下,然后将其抛出。因此,当轮到最后一个块时,执行将进入返回空列表的条件。

    这回报让我们付出了什么代价?嗯,它的成本,以防止任何捕获的异常被扔到外部,它可以被正确处理。不会引发方法签名中指定的任何异常;因此,将不引发任何异常。他们只是误导。

    类似的诊断消息:

    V6051 The use of the 'return' statement in the 'finally' block can lead to the
    loss of unhandled exceptions
    爪哇(808)

    杂项

    V6009 Function 'compareTo' receives an odd argument. An object
    'o2.getWorkerIdentity()' is used as an argument to its own method.
    LlapFixedRegistryImpl.java(244)

    @Override
    public List<LlapServiceInstance> getAllInstancesOrdered(....) {
      ....
      Collections.sort(list, new Comparator<LlapServiceInstance>() {
        @Override
        public int compare(LlapServiceInstance o1, LlapServiceInstance o2) {
          return o2.getWorkerIdentity().compareTo(o2.getWorkerIdentity()); // <=
        }
      });
      ....
    }

    可能有许多原因导致这样一个愚蠢的错误 – 复制粘贴,粗心大意,匆忙,等等。我们经常在开源项目中看到这样的错误。

    V6020 Divide by zero. The range of the 'divisor' denominator values includes
    zero. SqlMathUtil.java(265)
     

    public static long divideUnsignedLong(long dividend, long divisor) {
      if (divisor < 0L) {
        /*some comments*/
        return (compareUnsignedLong(dividend, divisor)) < 0 ? 0L : 1L;
      }
    
      if (dividend >= 0) { // Both inputs non-negative
        return dividend / divisor;                     // <=
      } else {
        ....
      }
    }

    这一个是微不足道的。一系列检查是无奈的,以避免零分裂。

    更多警告:

    • V6020 Mod by zero. The range of the 'divisor' denominator values includes
      zero. SqlMathUtil.java(309)
    • V6020 Divide by zero. The range of the 'divisor' denominator values includes
      zero. SqlMathUtil.java(276)
    • V6020 Divide by zero. The range of the 'divisor' denominator values includes
      zero. SqlMathUtil.java(312)

    V6030 The method located to the right of the '|' operator will be called
    regardless of the value of the left operand. Perhaps, it is better to use '||'.
    OperatorUtils.java(573)

    public static Operator<? extends OperatorDesc> findSourceRS(....) 
    {
      ....
      List<Operator<? extends OperatorDesc>> parents = ....;
      if (parents == null | parents.isEmpty()) {
        // reached end e.g. TS operator
        return null;
      }
      ....
    }

    程序员编写了按位运算符 |而不是逻辑 |这意味着无论左侧的结果如何,都将执行右侧部分。如果父项 = 为 null,则此拼写错误将在下一个逻辑子表达式中具有 NPE 权利。

    V6042 The expression is checked for compatibility with type 'A' but is cast to
    type 'B'. VectorColumnAssignFactory.java(347)

    public static 
    VectorColumnAssign buildObjectAssign(VectorizedRowBatch outputBatch,
                                         int outColIndex,
                                         PrimitiveCategory category)
                                         throws HiveException 
    {
      VectorColumnAssign outVCA = null;
      ColumnVector destCol = outputBatch.cols[outColIndex];
      if (destCol == null) {
        ....
      }
      else if (destCol instanceof LongColumnVector)
      {
        switch(category) {
        ....
        case LONG:
          outVCA = new VectorLongColumnAssign() {
                       ....
                       } .init(.... , (LongColumnVector) destCol);
          break;
        case TIMESTAMP:
          outVCA = new VectorTimestampColumnAssign() {
                       ....
                       }.init(...., (TimestampColumnVector) destCol);       // <=
          break;
        case DATE:
          outVCA = new VectorLongColumnAssign() {
                       

    ..
    * .init (.,(长柱矢量) destCol);
    中断;
    案例间隔_年_MONTH:
    outVCA = 新的矢量长列分配() |
    ….
    *.init(.,(长柱矢量)destCol);
    中断;
    案例间隔_DAY_时间:
    outVCA = 新矢量间隔时间时间分配() |
    ….
    [.init(.,(间隔时间柱)destCol);//<*
    中断;
    默认:
    抛出新的HiveException(….);
    }
    }
    否则,如果 (双柱矢量的 destCol 实例) |
    ….
    }
    ….
    其他 |
    抛出新的HiveException(….);
    }
    返回 VCA;
    }

    我们对类”长柱矢量”扩展列矢量感兴趣,而时间戳柱矢量扩展列矢量。destCol 对象是 LongColumnVector 的实例的检查明确表明它是此类的对象,将在条件语句的正文中处理。然而,尽管如此,它仍然被投到时间戳柱矢量!正如您所看到的,这些类是不同的,只是它们从同一父级派生。因此,我们得到一个类广播异常。

    在强制转换到间隔日时间柱载体的情况下也是如此:

    V6060 The 'var' reference was utilized before it was verified against null.
    Var.java(402), Var.java(395)

    public static 
    VectorColumnAssign buildObjectAssign(VectorizedRowBatch outputBatch,
                                         int outColIndex,
                                         PrimitiveCategory category)
                                         throws HiveException 
    {
      VectorColumnAssign outVCA = null;
      ColumnVector destCol = outputBatch.cols[outColIndex];
      if (destCol == null) {
        ....
      }
      else if (destCol instanceof LongColumnVector)
      {
        switch(category) {
        ....
        case LONG:
          outVCA = new VectorLongColumnAssign() {
                       ....
                       } .init(.... , (LongColumnVector) destCol);
          break;
        case TIMESTAMP:
          outVCA = new VectorTimestampColumnAssign() {
                       ....
                       }.init(...., (TimestampColumnVector) destCol);       // <=
          break;
        case DATE:
          outVCA = new VectorLongColumnAssign() {
                       ....
                       } .init(...., (LongColumnVector) destCol);
          break;
        case INTERVAL_YEAR_MONTH:
          outVCA = new VectorLongColumnAssign() {
                        ....
                       }.init(...., (LongColumnVector) destCol);
          break;
        case INTERVAL_DAY_TIME:
          outVCA = new VectorIntervalDayTimeColumnAssign() {
                        ....
                        }.init(...., (IntervalDayTimeColumnVector) destCol);// <=
        break;
        default:
          throw new HiveException(....);
        }
      }
      else if (destCol instanceof DoubleColumnVector) {
        ....
      }
      ....
      else {
        throw new HiveException(....);
      }
      return outVCA;
    }

    在这里,您将看到在取消引用发生后,var 对象的异常检查为 null。在此上下文中,var 和 obj 是同一个对象(var = (Var)obj)。存在 null 检查意味着传递的对象可能为 null。因此,调用等于 (null) 将导致 NPE,而不是预期的 false,位于第一行。是的,支票在那里,但不幸的是,它错了地方。

    其他几个类似情况,在检查之前使用对象:

    • V6060 The 'value' reference was utilized before it was verified against null.
      ParquetRecordReaderWrapper.java(168), ParquetRecordReaderWrapper.java(166)
    • V6060 The 'defaultConstraintCols' reference was utilized before it was
      verified against null. HiveMetaStore.java(2539), HiveMetaStore.java(2530)
    • V6060 The 'projIndxLst' reference was utilized before it was verified against
      null. RelOptHiveTable

    爪哇(682)

  • V6060 The 'oldp' reference was utilized before it was verified against null.
    ObjectStore.java(4343), ObjectStore.java(4339)
  • 结论

    Apache Hive 是一个受欢迎的项目,它是一个非常大的项目,由 6500 多个源文件 (*.java) 组成。许多开发人员已经编写了它多年,这意味着有很多事情需要静态分析器查找。它只是再一次证明静态分析在开发大中型项目时非常重要和有用!

    注意:像我在这里所做的一次检查非常适合展示分析器的功能,但使用方式不当。这个想法在这里这里被详细阐述。静态分析是定期使用!

    这次对蜂巢的检查发现了相当多的缺陷和可疑碎片。如果 Apache Hive 的作者遇到本文,我们将很乐意帮助改进项目的艰苦工作。

    你无法想象没有阿帕奇哈多普的阿帕奇蜂巢,所以来自PVS-Studio的独角兽很可能也会访问那个。但这就是今天。同时,我邀请您下载分析仪并检查您自己的项目。

    Comments are closed.