目錄導讀
1. 引言
對於很多初次接觸HBase的伙伴,在使用其客戶端API來構建Connection連接對象的時候,有可能會陷入以下幾個誤區。
- 類比druid等mysql數據庫連接池,自己封裝一個Connection對象的資源池,每次使用都從池中取出一個Connection對象;
- 在多線程的工作環境中,每個線程都會創建一個Connection對象,造成Connection對象被頻繁創建,快速消耗;
- 每次訪問HBase的時候臨時創建一個Connection對象,使用完之后調用close方法,關閉連接;
參考HBase技術社區文章連接HBase的正確姿勢中對Connection的源碼分析,我們可知:
- HBase客戶端中的Connection對象並不是簡單對應一個socket連接。
- HBase客戶端的Connection包含了對Zookeeper、HBase Master、HBase RegionServer三種socket連接的封裝。因此,Connection對象每次被創建出來的開銷是很大的,使用完畢之后斷開,會帶來嚴重的性能損耗。
- 在HBase中Connection類已經實現了對連接的管理功能,所以我們不需要自己在Connection之上再做額外的管理。另外,Connection是線程安全的,而Table和Admin則不是線程安全的,因此正確的做法是,在一個JVM進程中共用一個Connection對象,而在不同的線程中使用單獨的Table和Admin對象。
在並發系統中,多個線程每個都去創建一個Connection對象,那么你會馬上面臨如下窘境:
上圖所示的是HBase Zookeeper的連接被大量占用,某一個客戶端連接Zookeeper的連接數超過了60,大量連接創建的請求被拒絕。這樣會增加ZK的壓力,也會導致客戶端系統性能急劇下降。
2. 單例模式維護HBase的Connection
在普通的Java程序中,如果沒有並發場景的存在,我們可以簡單地使用下面這種方式來創建Connection的對象。
///所有進程共用一個connection對象
connection = ConnectionFactory.createConnection(config);
...
///每個線程使用單獨的table對象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}
然而,在多線程中的場景中,我們又該如何來管理我們的連接對象呢?聰明的你,一定會首先想到單例模式。
單例模式是一種簡單的設計模式,它的優點是只生成一個實例,可以保證在同一個JVM進程中,對象只存在一個。所以能節約系統資源,減少性能開銷,同時能夠嚴格控制用戶對它的訪問。
關於單例模式的實現,我知道的至少有十種,常見的有餓漢式
、懶漢式
、雙重檢測鎖式
、靜態內部類式
和枚舉單例
。
上述實現方式各有優劣,但使用時需要注意,線程是否安全和平衡效率。關於其具體的實現細節,網上有很多文章可供參考,這里不做詳細羅列,只舉例雙重檢測鎖式
的單例實現方式,在管理HBase客戶端連接對象中的應用。具體實現代碼:
public class SingleConnectionFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(SingleConnectionFactory.class);
private volatile static Connection connection;
private SingleConnectionFactory() {}
public static Connection getConnection(Configuration configuration) {
if (connection == null) {
synchronized (SingleConnectionFactory.class) {
if (connection == null) {
try {
connection = ConnectionFactory.createConnection(configuration);
LOGGER.info("the connection of HBase is created successfully.");
} catch (IOException e) {
LOGGER.error("the connection of HBase is created failed.");
throw new HBaseSdkConnectionException(e);
}
}
}
}
return connection;
}
}
上述單例Connection對象創建工廠類,在多線程的測試環境中亦可保證同一個JVM進程中,只有一個Connection對象被創建出來,觀察ZK客戶端連接數監控,幾乎無波動。
3. 多例模式中維護HBase的Connection
在cuckoo-cloud
(布谷鳥,微服務版大數據組件統一管理平台,目前已集成hbase-manager和kafka-manager的功能)中遷移我們的hbase-manager
應用時,遇到這樣一個問題。
cuckoo-cloud
平台上會管理我們的多個HBase集群,這些HBase集群的連接信息被保存進數據庫中,可以進行動態維護。在設計HBase的連接管理功能時,如果采用單例模式,那么,無論切換任意一個集群,始終操作的是最開始被初始化連接的集群;如果放棄單例模式,Connection對象又會被濫用。
所以,我需要一個容器,它能保存不同集群的連接對象,且每個對象在一個JVM進程中只保留一個。
我曾嘗試創建ThreadLocal變量,利用同一個ThreadLocal所包含的對象,在不同的Thread中保留不同的副本。這樣,每一個Thread內都有自己的實例副本,且該副本只能由當前Thread來使用,即保證了在多線程環境中只保留一個對象,又規避了多線程環境中的並發安全問題,可折騰了一圈,最終以失敗告終。(不知是我學藝不精,還是ThreadLocal不適合這種應用場景?)
接着又嘗試構建guava單例緩存池,利用緩存池中key的唯一性來保證多個被保存的對象唯一。興致勃勃寫好代碼,一上線測試,問題未有半點改善。
關於ThreadLocal和guava緩存池的應用場景和相關細節,還請參考網上的優秀文章。
最后想到了多例模式(一開始多例模式是腦海中虛構的概念,我想,既然有單例,那就應該存在多例。一百度,還真有這種設計模式存在)。可是能搜到的網上貼出來的多例實現代碼都是基於創建多個固定對象的,但我需要的是動態創建對象,最終的實現效果如下:
public class MultipleConnectionFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(MultipleConnectionFactory.class);
private volatile static Map<String, Connection> connectionMap;
private MultipleConnectionFactory() {
}
public static Connection getConnection(Configuration configuration) {
String cluster = configuration.get(HConstants.ZOOKEEPER_QUORUM);
if (connectionMap == null || !connectionMap.containsKey(cluster)) {
synchronized (MultipleConnectionFactory.class) {
if (connectionMap == null || !connectionMap.containsKey(cluster)) {
try {
if (connectionMap == null) {
connectionMap = new HashMap<>(2);
}
if (!connectionMap.containsKey(cluster)) {
Connection connection = ConnectionFactory.createConnection(configuration);
LOGGER.info("the connection of HBase cluster [{}] is created successfully.", cluster);
connectionMap.put(cluster, connection);
}
} catch (IOException e) {
LOGGER.error("the connection of HBase is created failed.");
throw new HBaseSdkConnectionException(e);
}
}
}
}
return connectionMap.get(cluster);
}
}
類比雙重檢測鎖式
的單例實現方式,在此使用ZK的連接地址為key,來保證每一個集群的連接對象在同一個JVM進程中唯一存在。
4. ConnectionFactory.createConnection方法中的連接池參數
ConnectionFactory.createConnection(Configuration conf, ExecutorService pool, User user)
調用此方法時,可以傳入三個參數,conf是連接配置相關,user是用戶認證相關,在此不必細說。ExecutorService pool的作用是什么呢?
參考文章中的解釋是,HBase客戶端連接池。
HBase訪問一條數據的過程中,需要連接Zookeeper、HBase Master、HBase RegionServer,HBase客戶端的Connection包含了對以上三種socket連接的封裝。
Connection對象和實際的socket連接之間的對應關系如下圖:
在HBase客戶端的代碼中,真正對應socket連接的是RpcConnection對象。HBase中使用PoolMap這種數據結構來存儲客戶端到HBase服務器之間的連接。PoolMap封裝了ConcurrentHashMap<>的結構,key是ConnectionId(封裝了服務器地址和用戶ticket),value是一個RpcConnection對象的資源池。當HBase需要連接一個服務器時,首先會根據ConnectionId找到對應的連接池,然后從連接池中取出一個連接對象。
HBase中提供了三種資源池的實現,分別是Reusable,RoundRobin和ThreadLocal。具體實現可以通過hbase.client.ipc.pool.type配置項指定,默認為Reusable。連接池的大小也可以通過hbase.client.ipc.pool.size配置項指定,默認為1。
關於pool的用意,扒拉了半天源碼,嵌套實在太深也太復雜,為了保證文章完整性,只能引用參考文章,(如有侵權,請私信我刪除)
如果對ExecutorService pool
有特殊需求,可以構造相應的連接池,通過配置合理的連接池的大小,來提升系統請求HBase集群的性能。
5. 總結
上述內容從實際的應用場景出發,全面介紹了HBase客戶端連接的正確使用,以規避可能存在的風險。但是,在有些使用場景中,就算是使用單例了模式來創建Connection的連接對象,也會有ZK連接耗盡的風險。
例如:在Flink或Spark等分布式計算引擎中,如果executor或並發數設置的過高,也會占用大量的ZK連接,畢竟,每個計算節點上的一個計算程序就是一個單獨的JVM進程。此時,可以使用HBase Thrift的池化連接技術,具體可參考,HBase實踐篇 | 為HBase的Thrift 客戶端API設計連接池
關於HBase客戶端API的二次開發封裝,也可以參考hbase-sdk
https://gitee.com/weixiaotome/hbase-sdk
里面封裝了HBaseAdminTemplate
、HBaseTemplate(ORM框架)
、HBaseSqlTemplate(HBase SQL API)
、Spring Boot集成
和HBase Thrift連接池API
等功能,詳情請參考hbase-sdk
的使用文檔。