什麼是Shim
Shim一詞的原本含義是“墊片”或者“楔子”,而首先將這個詞應用到軟件工程領域的似乎是微軟。根據Wikipedia的總結:
A shim is a library that transparently intercepts API calls and changes the arguments passed, handles the operation itself or redirects the operation elsewhere. Shims can be used to support an old API in a newer environment, or a new API in an older environment. Shims can also be used for running programs on different software platforms than they were developed for.
按照這個釋義,Shim是一種(小型的)庫,負責透明地攔截API調用,並更改其參數,或將其轉發至其他組件,或者自行處理。它用於在新環境中支持舊API(或反過來),以及使程序能夠在非特定支持的平臺上運行。
熟悉Docker的看官可能會立即想起該體系中的containerd-shim組件,它作爲containerd與容器運行時交互的中間層發揮作用,並且符合上文的釋義。
具體到不同API版本這一方面的話,我們可以參考一下Flink(當然其他很多開源組件也同理)的設計思路。
Flink Hive Catalog中的Shims
我們知道,Flink 1.14支持從1.0.0~3.1.2各版本的Hive,而橫跨這麼多版本的Hive API底層邏輯勢必不會完全一致,如果將全部版本的Hive依賴都引入進來,也一定會造成衝突,這裏就需要Shims發揮作用。在代碼中,HiveShim
是一個接口,定義了所有需要做兼容性支持的方法,如下圖所示。
該接口的實現類就從HiveShimV100
一直命名至HiveShimV312
,每個類都會override對應版本出現變更的方法。例如,alterPartition()
方法對應1.0.0版本的實現是:
@Override
public void alterPartition(
IMetaStoreClient client, String databaseName, String tableName, Partition partition)
throws InvalidOperationException, MetaException, TException {
String errorMsg = "Failed to alter partition for table %s in database %s";
try {
Method method =
client.getClass()
.getMethod(
"alter_partition", String.class, String.class, Partition.class);
method.invoke(client, databaseName, tableName, partition);
} catch (InvocationTargetException ite) {
// ...
} catch (NoSuchMethodException | IllegalAccessException e) {
// ...
}
}
而由於Hive Metastore提供的API alter_partition()
方法的簽名發生了變化,對應2.1.0版本的實現是:
@Override
public void alterPartition(
IMetaStoreClient client, String databaseName, String tableName, Partition partition)
throws InvalidOperationException, MetaException, TException {
String errorMsg = "Failed to alter partition for table %s in database %s";
try {
Method method =
client.getClass()
.getMethod(
"alter_partition",
String.class,
String.class,
Partition.class,
EnvironmentContext.class);
method.invoke(client, databaseName, tableName, partition, null);
} catch (InvocationTargetException ite) {
// ...
} catch (NoSuchMethodException | IllegalAccessException e) {
// ...
}
}
顯然,由於Flink環境並不能事先確定外部Hive的版本,所以全部的Shim方法都需要依賴反射調用。另外,爲了保持向後兼容性,Shim實現類從低版本到高版本會自然形成鏈式繼承關係,如下圖所示。
在Flink App啓動時,HiveShimLoader
組件會根據Catalog定義時傳入的hive-version
參數或者自動探測到的Hive版本加載特定的HiveShim
,不再贅述。
不用Shim的場景?
在Flink Connector體系內也能找到這類場景,如ES Connector就使用了3個不同的module來實現:flink-connector-elasticsearch5
、flink-connector-elasticsearch6
和flink-connector-elasticsearch7
:
造成這種不同選擇的原因有二:一是ES版本並不像Hive版本那麼細分,即使6.x和7.x之間有大量的重複代碼也在可接受的範圍內;二是5.x採用Transport Client進行通信,而6.x和7.x採用REST High Level Client進行通信,相當於失去了統一的接口規約(如Hive的IMetaStoreClient
那樣),再使用反射調用會更加複雜,得不償失。
Iceberg基於同樣的考慮,在0.13版本之後也對Spark和Flink做了非Shim化的支持,看官可以去GitHub看看。
vs 適配器模式?
在筆者看來,Shim是適配器模式的一種(另闢蹊徑的)實現方式。參考GoF對適配器模式的解說:
The adapter design pattern solves problems like:
- How can a class be reused that does not have an interface that a client requires?
- How can classes that have incompatible interfaces work together?
- How can an alternative interface be provided for a class?
The adapter design pattern describes how to solve such problems:
- Define a separate adapter class that converts the (incompatible) interface of a class (adaptee) into another interface (target) clients require.
- Work through an adapter to work with (reuse) classes that do not have the required interface.
用上文的例子套用這個定義,HiveShim
就是適配器本體,而Hive的原生API就是被適配者(adaptee),各個不同版本的HiveShim
實現的方法就是目標接口(target)。一家之言,僅供參考。
The End
晚安晚安。