问题描述
用presto连接kudu进行查询,在where条件中使用in语句时,如果是单个值效率很高,如:where name in ('jack')
;但如果是多个值时效率很慢,如:where name in ('jack', 'tom')
。
问题排查
1、kudu连接器的逻辑
我们知道kudu是十分擅长定位一个值的,这点可以从where name in ('jack')
in单个值查询速度很快看得出来。
presto连接kudu进行查询时,是通过kudu的java api实现数据的过滤的,在presto源码KuduClientSession
这个类中有如下代码:
/**
* translates TupleDomain to KuduPredicates.
*
* @return false if TupleDomain or one of its domains is none
*/
private boolean addConstraintPredicates(KuduTable table, KuduScanToken.KuduScanTokenBuilder builder,
TupleDomain<ColumnHandle> constraintSummary)
{
if (constraintSummary.isNone()) {
return false;
}
else if (!constraintSummary.isAll()) {
Schema schema = table.getSchema();
for (TupleDomain.ColumnDomain<ColumnHandle> columnDomain : constraintSummary.getColumnDomains().get()) {
int position = ((KuduColumnHandle) columnDomain.getColumn()).getOrdinalPosition();
ColumnSchema columnSchema = schema.getColumnByIndex(position);
Domain domain = columnDomain.getDomain();
if (domain.isNone()) {
return false;
}
else if (domain.isAll()) {
// no restriction
}
else if (domain.isOnlyNull()) {
builder.addPredicate(KuduPredicate.newIsNullPredicate(columnSchema));
}
else if (domain.getValues().isAll() && domain.isNullAllowed()) {
builder.addPredicate(KuduPredicate.newIsNotNullPredicate(columnSchema));
}
else if (domain.isSingleValue()) {
KuduPredicate predicate = createEqualsPredicate(columnSchema, domain.getSingleValue());
builder.addPredicate(predicate);
}
else {
ValueSet valueSet = domain.getValues();
if (valueSet instanceof EquatableValueSet) {
DiscreteValues discreteValues = valueSet.getDiscreteValues();
KuduPredicate predicate = createInListPredicate(columnSchema, discreteValues);
builder.addPredicate(predicate);
}
else if (valueSet instanceof SortedRangeSet) {
Ranges ranges = ((SortedRangeSet) valueSet).getRanges();
Range span = ranges.getSpan();
Marker low = span.getLow();
if (!low.isLowerUnbounded()) {
KuduPredicate.ComparisonOp op = (low.getBound() == Marker.Bound.ABOVE)
? KuduPredicate.ComparisonOp.GREATER : KuduPredicate.ComparisonOp.GREATER_EQUAL;
KuduPredicate predicate = createComparisonPredicate(columnSchema, op, low.getValue());
builder.addPredicate(predicate);
}
Marker high = span.getHigh();
if (!high.isUpperUnbounded()) {
KuduPredicate.ComparisonOp op = (low.getBound() == Marker.Bound.BELOW)
? KuduPredicate.ComparisonOp.LESS : KuduPredicate.ComparisonOp.LESS_EQUAL;
KuduPredicate predicate = createComparisonPredicate(columnSchema, op, high.getValue());
builder.addPredicate(predicate);
}
}
else {
throw new IllegalStateException("Unexpected domain: " + domain);
}
}
}
}
return true;
}
从上面代码可以看到,根据输入的条件的数值个数不同,会产生对应的KuduPredicate
添加到KuduScanTokenBuilder
中,最后生成KuduScanToken
及对应的KuduSplit
分发给各个presto节点进行运算。
所以需要进一步测试KuduPredicate
的性能。
2、KuduPredicate性能测试
KuduClient client = null;
KuduScanner scanner = null;
try {
client = new KuduClient.KuduClientBuilder("kudu-host-1,kudu-host-2").build();
KuduTable table = client.openTable("test");
// 测试用in list单个值
KuduPredicate predicate = KuduPredicate.newInListPredicate(table.getSchema().getColumn("name"),Arrays.asList('jack'));
// 测试用EQUAL单个值
KuduPredicate predicate = KuduPredicate.newComparisonPredicate(table.getSchema().getColumn("name"),KuduPredicate.ComparisonOp.EQUAL, 'jack');
// 测试用in list多个值
KuduPredicate predicate = KuduPredicate.newInListPredicate(table.getSchema().getColumn("name"),Arrays.asList('jack', 'tom', 'marry'));
KuduScanner.KuduScannerBuilder builder = client.newScannerBuilder(table).setProjectedColumnNames(columns);
scanner = builder.addPredicate(predicate).build();
getAndOutput(scanner);
} catch ......
经过测试,当只有一个值时,KuduPredicate.newInListPredicate
和KuduPredicate.newComparisonPredicate
性能相近,当有两个值时,KuduPredicate.newInListPredicate
的速度相较一个值的时候虽然会慢点,但依然十分优秀,但presto使用kudu连接器的时候为什么就这么慢呢?
3、解析KuduScanToken
上文说过,presto节点是通过KuduScanToken
向kudu请求数据的,当查询完成后,我们可以通过presto的WebUI获取到这个token:http://{presto-web}/v1/query/{query_id}?pretty
,搜索serializedScanToken
,将得到的token进行解析
String str = "EhJkaW0uZGltX3lpc2RrX3VzZXIaFAgAE.........."; // your token here
Base64.Decoder decoder = Base64.getDecoder();
byte[] bytes = decoder.decode(str);
KuduClient client = new KuduClient.KuduClientBuilder("kudu-host-1,kudu-host-2").build();
KuduScanner scanner = KuduScanToken.deserializeIntoScanner(bytes, client);
通过ide的debug功能获取KuduScanner scanner
这个对象其中的成员变量predicate
,结果发现presto并不是使用KuduPredicate.newInListPredicate
的in list来实现in查询,而是使用了RANGE的范围查询:
in list的查询只会拉取很少量的数据,而范围查询所需要获取的数据就多很多,这也就解释了为什么在presto使用in语句有多个值的时候速率会慢那么多。
4、Presto为什么使用的是范围查询
先看源码部分:
ValueSet valueSet = domain.getValues();
if (valueSet instanceof EquatableValueSet) {
......
}
else if (valueSet instanceof SortedRangeSet) {
......
}
presto根据ValueSet
决定使用in list条件还是范围条件,那ValueSet
是在哪里实例化的呢?我们看ValueSet
这个类:
static ValueSet of(Type type, Object first, Object... rest)
{
if (type.isOrderable()) {
return SortedRangeSet.of(type, first, rest);
}
if (type.isComparable()) {
return EquatableValueSet.of(type, first, rest);
}
throw new IllegalArgumentException("Cannot create discrete ValueSet with non-comparable type: " + type);
}
当条件里有多个值的时候会进入这个方法,如果字段类型是可排序的,则会优先使用SortedRangeSet
,之后才会使用EquatableValueSet
,而近乎所有的基本类型的isOrderable
方法返回的都是true,也就是说基本上只要是使用in条件语句带上多个值,那就会使用SortedRangeSet
,相对应的kudu也就使用了RANGE范围查询去拉取数据。
那为什么Presto要有这样的逻辑呢?我的理解是:Presto作为一个对大量数据进行查询的引擎,默认每次查询从连接器中获取数据的体量都会是很大的,而连接器五花八门,presto不敢保证每个连接器都具备快速精准定位数据的能力,presto更相信自己基于内存的数据筛选和计算能力,因此presto只给了连接器一个范围条件,让连接器进行粗滤筛选,自己来做最终的精准定位。不知道我这想法对不对,如果有不同的观点也欢迎跟我一起探讨一下。