Apache Calcite 是獨立于存儲與執行的SQL解析、優化引擎,廣泛應用于各種離線、搜索、實時查詢引擎,如Drill、Hive、Kylin、Solr、flink、Samza等。本文結合hive中基于代價的優化,解析calcite優化引擎的實現原理。
Calcite架構
??Calcite架構圖如下,其中Operator Expressions 是查詢樹在calcite中的表示,可以直接通過calcite的SQL Parser解析得到,也可以通過Expressions Builder由Data Processing System中的查詢樹(本文對應hive中的AST)轉換得到。Query Optimizer 根據Pluggable Rules對Operator Expressions進行優化,其中會用到Metadata Providers提供的信息進行代價計算等操作。
Hive CBO
??本文中Data Processing System就是hive,本文主要解析hive如何利用calcite進行基于代價的優化(cost based optimization /CBO)。Hive CBO的主要實現代碼在CalcitePlanner 這個類中, CalcitePlanner 繼承自SemanticAnalyzer,重寫了genOPTree 方法,由AST 生成 Operator Tree 。其中CalcitePlanner.CalcitePlannerAction.genLogicalPlan 函數對應上圖中的Expressions Builder,把hive中的AST轉換成calcite 中的Operator Expressions,也就是節點為RelNode的查詢樹。這個過程這里不展開,繼續往下看。在CalcitePlanner.CalcitePlannerAction.HepPlan會對輸入的basePlan根據rules進行優化,返回優化過的plan,代碼如下:
??這里hive使用calcite的HepPlanner作為優化引擎(另一個選擇是VolcanoPlanner),可以看到向planner輸入原始的查詢樹、Metadata Providers、Rules,調用findBestExp(),返回優化后的查詢樹。與上面的架構圖對應。下面我們來詳細分析這幾個部分是如何交互,完成優化的。
主要數據結構
??下圖列出了calcite中主要的相關接口和類,以及其中比較重要的成員。
??RelOptCluster 為查詢優化過程中的環境信息,包含RelOptPlanner、MetadataFactory等信息,MetadataFactory可以看成RelMetadataProvider的一個工廠,calcite中MetadataFactoryImpl實現了MetadataFactory接口,其利用Guava Cache對RelMetadataProvider進行緩存。
??RelNode代表了Operator Expressions中的一個節點,往往以根節點代表整個查詢樹。函數getCluster()可以得到當前cluster。
RelOptRule表示優化規則,是抽象類,calcite實現了很多優化規則,用戶也可以實現自己的規則。其中有兩個重要的函數:matches(RelOptRuleCall) 判斷規則是否匹配當前RelNode;當匹配的時候會調用onMatch(RelOptRuleCall)。
??RelMetadataProvider是如何獲得relational expressions的matadata的接口,只有一個函數 apply(...),這么說可能不是很明了,下文的例子會詳細講。
??HepPlanner就是根據rules進行優化的類,其成員mainProgram可以看成根據rules等信息生成的優化策略,會具體指導優化過程;graph是封裝了Operator Expressions的有向圖。其成員函數findBestExp()是優化的入口,返回優化過的Operator Expressions。執行時會多次調用applyRule(...) 函數,其中就會調用到RelOptRule的matches(RelOptRuleCall)和onMatch(RelOptRuleCall)。
優化流程
??優化的主入口是HepPlanner.findBestExp(),其中會調用executeProgram(mainProgram),mainProgram 由Instructions組成,Instruction主要是RuleCollection,也有MatchOrder、MatchLimit等。對于RuleCollection,executeInstruction就是對每一個rule進行apply,這里以HiveReduceExpressionsRule為例往下分析,在HepPlanner.applyRule函數中可以看到,首先調用matchOperands以及HiveReduceExpressionsRule.matches判斷此規則是否匹配,若匹配則調用fireRule(call),會進到HiveReduceExpressionsRule.onMatch函數進行這條規則的具體優化,時序圖如下:
??這里我們不展開討論HiveReduceExpressionsRule具體做了什么,主要來看一下其怎么利用RelMetadataQuery進行metadata訪問的。RelMetadataQuery可以看成metadata的訪問媒介,實際訪問的metadata由RelNode的MetadataFactory提供。在BuiltInMetadata中定義了所有metadata的接口,hive通過RelMetadataProvider實現了這些接口,并注冊到MetadataFactory中。
??RelMetadataProvider有好幾個實現類,其中最重要的是ReflectiveRelMetadataProvider,這個類通過java的動態代理機制綁定hive的metadata實現。具體可見ReflectiveRelMetadataProvider.reflectiveSource的實現。部分代碼如下:
private static RelMetadataProvider reflectiveSource(final Object target,
final ImmutableList<Method> methods) {
...
final Set<Class<RelNode>> classes = Sets.newHashSet();
final Map<Pair<Class<RelNode>, Method>, Method> handlerMap =
Maps.newHashMap();
for (final Method handlerMethod : target.getClass().getMethods()) {
for (Method method : methods) {
if (couldImplement(handlerMethod, method)) {
@SuppressWarnings("unchecked") final Class<RelNode> relNodeClass =
(Class<RelNode>) handlerMethod.getParameterTypes()[0];
classes.add(relNodeClass);
handlerMap.put(Pair.of(relNodeClass, method), handlerMethod);
}
}
}
final ConcurrentMap<Class<RelNode>, UnboundMetadata> methodsMap = new ConcurrentHashMap<>();
for (Class<RelNode> key : classes) {
ImmutableNullableList.Builder<Method> builder =
ImmutableNullableList.builder();
for (final Method method : methods) {
builder.add(find(handlerMap, key, method));
}
final List<Method> handlerMethods = builder.build();
final UnboundMetadata function =
new UnboundMetadata() {
public Metadata bind(final RelNode rel,
final RelMetadataQuery mq) {
return (Metadata) Proxy.newProxyInstance(
metadataClass0.getClassLoader(),
new Class[]{metadataClass0},
new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
...
try {
return handlerMethod.invoke(target, args1);
} catch (InvocationTargetException
| UndeclaredThrowableException e) {
Throwables.propagateIfPossible(e.getCause());
throw e;
} finally {
mq.set.remove(key);
}
}
});
}
};
methodsMap.put(key, function);
}
return new ReflectiveRelMetadataProvider(methodsMap, metadataClass0);
}
??函數的第一個參數target是hive實現的某個metadata的實現類,第二個參數methods是實現的目標接口。函數會找出target中對接口的實現函數,并將該實現函數的第一個參數作為key放在map中。之后在訪問matadata的時候,會以當前RelNode的實際類型為key,在map中查找實現函數。如果沒有以當前RelNode的實際類型為第一個參數的具體實現,就會有空指針異常。這里有我向hive提交的一個patch(HIVE-19202),就是這樣的問題。
總結
本文介紹了calcite的架構及hive利用calcite進行CBO的部分源碼分析。我們了解了一個數據處理系統可以如何通過擴展calcite的rule和metadata接口實現自定義的優化處理。