Presto 查 Hive 元数据这么慢?发现 bug 啦?一个成为贡献者的机会!

1、又来活儿了

  又是一天风和日丽的下午,本喵收到业务反馈,xx(某知名报表软件)上看表的元数据太慢啦,比查数据还慢,瘦不鸟啦! 跟厂商反馈了,厂商让我们自查 presto 集群问题。


2、问题现象

  presto 用的是 0.220 版本,业务方反应并不是所有的库表都慢,并提供了几个比较慢的库表名称供排查。正常的表大概 1 秒钟左右能出来元数据,有问题的表等 5 min 都不一定出得来。初步看了一下,这几个库的表和字段都比较多(这里已经埋下了伏笔)。


3、勤恳分析过程

3.1、初步定位

  报表软件是通过 presto-jdbc 来获取 hive 表的元信息的,追到这里面的时候还发现了一个点,presto 的 jdbc 居然是封装的 okhttp,本质就是一个 http 客户端,有点变态,哦不,有点东西。ok,这个不重要,让我们继续看下去。通过远程 debug,发现了耗时比较多的点在获取表的字段这个方法里面。

@Override
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
        throws SQLException
{
    StringBuilder query = new StringBuilder("" +
            "SELECT TABLE_CAT, TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE,\n" +
            "  TYPE_NAME, COLUMN_SIZE, BUFFER_LENGTH, DECIMAL_DIGITS, NUM_PREC_RADIX,\n" +
            "  NULLABLE, REMARKS, COLUMN_DEF, SQL_DATA_TYPE, SQL_DATETIME_SUB,\n" +
            "  CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE,\n" +
            "  SCOPE_CATALOG, SCOPE_SCHEMA, SCOPE_TABLE,\n" +
            "  SOURCE_DATA_TYPE, IS_AUTOINCREMENT, IS_GENERATEDCOLUMN\n" +
            "FROM system.jdbc.columns");

    List<String> filters = new ArrayList<>();
    emptyStringEqualsFilter(filters, "TABLE_CAT", catalog);
    emptyStringLikeFilter(filters, "TABLE_SCHEM", schemaPattern);
    optionalStringLikeFilter(filters, "TABLE_NAME", tableNamePattern);
    optionalStringLikeFilter(filters, "COLUMN_NAME", columnNamePattern);
    buildFilters(query, filters);

    query.append("\nORDER BY TABLE_CAT, TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION");

    return select(query.toString());
}

  好家伙,在 jdbc 内部拼 sql 是吧,ok,system.jdbc.columns 是 presto 内部表,机制先不聊。这里看到,这里的变量名都是 pattern 结尾,也就是模糊查询,最后拼接成的 sql 也是长这样子

SELECT TABLE_CAT,TABLE_SCHEM,TABLE_NAME, COLUMN_NAME, DATA_TYPE,TYPE_NAME, COLUMN_SIZE, BUFFER_LENGTH, ...
FROM system.jdbc.columns
WHERE TABLE_CAT = 'hive'
AND TABLE_SCHEM = 'xx_schema'
AND TABLE_NAME like 'xx_table"
ORDER BY TABLE_CAT, TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION;

  可以看到,前面不同的 xxxFilter 被映射成了 =like,那就不能不怀疑这个 like 的问题了。直接把这条生成的 sql 放到 presto 上去执行,效果和反馈的问题一样,某些表查得快,某些表查得慢。关键来了,把 like 换成 =,不管查什么表都很快了;或者,把 TABLE_SCHEM 的也换成 like,效果更差了。其实问题到这里,可以结束了,把这里的 like 改一下就能解决问题了。

3.2、深度分析

  但是!为什么这里用 like 就快了呢,system.jdbc.columns 其实不是实体表,也不存在索引的问题,大概率最终还是去查的数据源的元数据信息,就算是模糊匹配,不应该有这么明显的差距。那问题出在了哪里?

  这该死的好奇心啊,那就继续排查!

  sql 在 presto coordinator 这边进行语法解析,生成了执行计划,然后…等等,好像不一样呀,这是用 = 生成的语法树结构

AND(AND(EQUAL(table_cat, CAST(Slice{base=[B@29291eb8, address=16, length=4})), EQUAL(table_schem, CAST(Slice{base=[B@40c3d3f0, address=16, length=7}))), LIKE(table_name, CAST(Slice{base=[B@7020a486, address=16, length=11})))

  会生成一个叫 prefix 的过滤条件供查元数据的时候使用,为hive.default.*
这是用 like 生成的:

AND(AND(EQUAL(table_cat, CAST(Slice{base=[B@29291eb8, address=16, length=4})), EQUAL(table_schem, CAST(Slice{base=[B@40c3d3f0, address=16, length=7}))), EQUAL(table_name, CAST(Slice{base=[B@7020a486, address=16, length=11})))

  对应的 prefixhive.default.table_name

  所以当使用 like 的时候,根本不是模糊查询,就是全量在匹配!前者拿的是某库下的所有表的所有字段,然后逐一比对查出来的schema、表是否匹配,耗时就在这里;实际后者只用拿到这个表的所有字段。所以这个问题一直存在,只是在表和字段数量都多的时候才暴露出来。

  所以可以基本确实,是执行计划的生成有问题了。直接祭出我的二分定位法,出来吧!bug 代码!
  查内置表元数据的 where 表达式里面的代码会调到 RowExpressionDomainTranslator#visitCall来解析处理

@Override
public ExtractionResult<T> visitCall(CallExpression node, Boolean complement)
{
	// not 语法
    if (node.getFunctionHandle().equals(resolution.notFunction())) {
        return node.getArguments().get(0).accept(this, !complement);
    }
	// beetween 操作
    if (resolution.isBetweenFunction(node.getFunctionHandle())) {
        // Re-write as two comparison expressions
        return and(
                binaryOperator(GREATER_THAN_OR_EQUAL, node.getArguments().get(0), node.getArguments().get(1)),
                binaryOperator(LESS_THAN_OR_EQUAL, node.getArguments().get(0), node.getArguments().get(2))).accept(this, complement);
    }
	// 比较操作
    FunctionMetadata functionMetadata = metadata.getFunctionManager().getFunctionMetadata(node.getFunctionHandle());
    if (functionMetadata.getOperatorType().map(OperatorType::isComparisonOperator).orElse(false)) {
        Optional<NormalizedSimpleComparison> optionalNormalized = toNormalizedSimpleComparison(functionMetadata.getOperatorType().get(), node.getArguments().get(0), node.getArguments().get(1));
        if (!optionalNormalized.isPresent()) {
            return visitRowExpression(node, complement);
        }
        NormalizedSimpleComparison normalized = optionalNormalized.get();

        RowExpression expression = normalized.getExpression();
        Optional<T> column = columnExtractor.extract(expression);
        if (column.isPresent()) {
            NullableValue value = normalized.getValue();
            Type type = value.getType(); // common type for symbol and value
            return createComparisonExtractionResult(normalized.getComparisonOperator(), column.get(), type, value.getValue(), complement);
        }
        else if (expression instanceof CallExpression && resolution.isCastFunction(((CallExpression) expression).getFunctionHandle())) {
            CallExpression castExpression = (CallExpression) expression;
            if (!isImplicitCoercion(castExpression)) {
                //
                // we cannot use non-coercion cast to literal_type on symbol side to build tuple domain
                //
                // example which illustrates the problem:
                //
                // let t be of timestamp type:
                //
                // and expression be:
                // cast(t as date) == date_literal
                //
                // after dropping cast we end up with:
                //
                // t == date_literal
                //
                // if we build tuple domain based coercion of date_literal to timestamp type we would
                // end up with tuple domain with just one time point (cast(date_literal as timestamp).
                // While we need range which maps to single date pointed by date_literal.
                //
                return visitRowExpression(node, complement);
            }
            CallExpression cast = (CallExpression) expression;
            Type sourceType = cast.getArguments().get(0).getType();
            // we use saturated floor cast value -> castSourceType to rewrite original expression to new one with one cast peeled off the symbol side
            Optional<RowExpression> coercedExpression = coerceComparisonWithRounding(
                    sourceType, cast.getArguments().get(0), normalized.getValue(), normalized.getComparisonOperator());
            if (coercedExpression.isPresent()) {
                return coercedExpression.get().accept(this, complement);
            }
            return visitRowExpression(node, complement);
        }
        else {
            return visitRowExpression(node, complement);
        }
    }
    return visitRowExpression(node, complement);
}

  其实就是这几个 if 处理分支都没有处理 like的情况,like对应的函数是 likeFunctionCall,over~


4、解决方案

那很明显解决方案有两个

  • 改 presto-jdbc 客户端代码,直接把 LikeFilter 换成用 EqualsFilter(简单无脑版)
  • 改 coordinator 的代码,添加处理 like 的用法生成正确的 prefix(硬核版)

最后厂商用的第一个方案,成功解决问题~

(看了新版本的也还是有这个问题)

下次见~

  • 49
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫语大数据

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值