標籤:des http java 使用 os 資料 io width
HQueue:基於HBase的訊息佇列 淩柏 ?1. HQueue簡介
HQueue是一淘搜尋網頁抓取離線系統團隊基於HBase開發的一套分布式、持久化訊息佇列。它利用HTable儲存訊息資料,藉助HBase Coprocessor將原始的KeyValue資料封裝成訊息資料格式進行儲存,並基於HBase Client API封裝了HQueue Client API用於訊息存取。
HQueue可以有效使用在需要儲存時間序列資料、作為MapReduce Job和iStream等輸入、輸出供上下遊共用資料等場合。
?2. HQueue特性
由於HQueue是基於HBase進行訊息存取的,因此站在HDFS和HBase的肩膀上,使得其具備如下特點:
?(1)支援多Partitions,可根據需求設定Queue的規模,支援高並發訪問(HBase的多Region);
?(2)?支援自動Failover,任何機器Down掉,Partition可自動遷移至其他機器(HBase的Failover機制);
(3)?支援動態負載平衡,Partition可以動態被調度到最合理的機器上(HBase的LoadBalance機制,可動態調整);
?(4)利用HBase進行訊息的持久化儲存,不遺失資料(HBase HLog和HDFS Append);
?(5)隊列的讀寫入模式與HBase的儲存特性天然切合,具備良好的並發讀寫效能(最新訊息儲存在MemStore中,寫訊息直接寫入MemStore,通常情境下都是記憶體級操作);
?(6)支援訊息按Topic進行分類存取(HBase中的Qualifier);
?(7)支援訊息TTL,自動清理到期訊息(HBase支援KeyValue層級的TTL);
?(8)HQueue = HTable Schema Design + HQueue Coprocessor + HBase Client Wrapper,完全擴充開發,無任何Hack工作,可隨HBase自動升級;
(9)?HQueue Client API基於HBase Client Wrapper進行簡單封裝,HBase的ThriftServer使得其支援多語言API,因此HQueue也很容易封裝出多語言API;
(10)HQueue Client API可以天然支援Hadoop MapReduce Job和iStream的InputFormat機制,利用Locality特性將計算調度到儲存最近的機器;
?(11)HQueue支援訊息訂閱機制(HQueue 0.3及後續版本)。
3. HQueue系統設計及處理流程3.1. HQueue系統結構
?HQueue系統結構(1)所示:
圖(1):HQueue系統結構
其中:?
?(1)每個Queue對應一個HTable,建立Queue可以通過Presharding Table方式建立,有利於負載平衡。
?(2)每個Queue可以有多個Partitions(HBase Regions),這些Partitions均勻分布在HBase叢集中的多個Region Servers中。
?(3)每個Partition可以在HBase叢集的多個Region Servers中動態遷移。任何一台Region Server掛掉,運行在其上的HQueue Partition可以自動遷移到其他Region Server上,並且資料不會丟失。當叢集負載不均衡時,HQueue Partition會自動被HMaster遷移到負載低的Region Server。
?(4)每個Message對應一個HBase KeyValue Pair,按MessageID即時間順序儲存在HBase Region中。MessageID由Timestamp和同一Timestamp下自增的SequenceID構成,詳細資料參見《Message儲存結構》部分。
?3.2. Message儲存結構
?Message儲存結構(2)所示:
圖(2):Message儲存結構
其中:?
?(1)RowKey:由PartitionID和MessageID構成。
- ?PartitionID:?一個Queue可以有多個Partitions,目前最多支援Short.MAX_VALUE個Partitions。Partition ID可以不在建立Message對象時指定,而是在發送訊息時設定,或者不指定而使用一個隨機Partition ID。
- ?MessageID:即訊息ID,它由Timestamp和SequenceID兩部分組成。Timestamp是訊息寫入HQueue時的時間戳記,單位為毫秒。SequenceID是同一Timestamp下訊息的順序編號,目前最多支援同一Timestamp下Short.MAX_VALUE個Messages。
?(2)Column:由Column Family和Message Topic構成。
- Column Family:HBase Column Family,此處為固定值“message”。
- Message Topic :HBase Column Qualifier,訊息Topic名稱。使用者可以根據需要將Message儲存在不同的Topics之下,也可以從Queue中擷取感興趣的Topics訊息資料。
?(3)Value:即訊息內容。
?3.3. HQueue訊息寫入及Coprocessor處理流程
?HQueue利用HQueue Client API寫入訊息資料,為保證訊息唯一和有序,HQueue利用Coprocessor處理使用者寫入訊息的MessageID,然後立即放入HBase MemStore中,使其可以被訪問到,最後持久化的HLog中。具體的處理邏輯(3)所示:
?圖(3):資料寫入及Coprocessor處理流程
?其中:
?(1)HQueue封裝了HQueue Client API,使用者可以使用其提供Put等方法向HQueue中寫入訊息。
(2)HQueue Client會使用Message.makeKeyValueRow()用於完成將Message資料結構轉換成HBase Rowkey。HQueue所要求的RowKey格式可以參加上述內容。
(3)HQueue Client在完成RowKey的轉換後,會調用HTable的put方法按照HBase標準的寫入流程來完成訊息的寫入。
(4)HQueue上註冊有HQueueCoprocessor,它擴充自BaseRegionObserver。HRegion在真正寫入訊息資料前,會調用HQueueCoprocessor的preBatchMutate方法,該方法主要用於調整MessageID,保證MessageID唯一併且有序。
(5)在HQueueCoprocessor的preBatchMutate方法中同時會調整Durability為SKIP_WAL,這樣HBase將不會主動將訊息資料持久化進HLog。
(6)HRegion在寫入訊息資料後,會調用HQueueCoprocessor的postBatchMutate方法,該方法主要完成將訊息資料持久化進HLog的功能。
?3.4. HQueue Scan處理流程
?為了方便從Queue中Scan資料,HQueue封裝了ClientScanner,提供了QueueScanner、PartitionScanner和CombinedPartitionScanner等Scanner,用於不同的情境。HQueue Scan的具體處理流程(4)所示:
圖(4):HQueue Scan處理流程
其中:
(1)使用者可以根據需要從HQueue Client中擷取所需的Queue Scanner,目前主要提供三種Scanner:
- QueueScanner:用於Scan Queue中全部Partitions的資料;
- ?PartitionScanner:用於Scan Queue中指定Partition的資料;
- ?CombinedPartitionScanner:用於Scan Queue中若干指定Partitions的資料。
(2)使用者擷取到Scanner之後,可以迴圈調用Scanner的next方法依次取出訊息資料,直至無資料返回,本次Scan結束。Scan結束後,使用者應主動關閉Scanner以便及時釋放資源。
(3)使用者在不再使用先前建立的Queue對象時,應主動關閉Queue以便及時釋放資源。
?3.5. HQueue訂閱流程3.5.1. 整體流程
HQueue自0.3版本開始提供訂閱功能,一個訂閱者可以訂閱一個Queue的多個Partitions、多個Topics。與使用者使用Scanner主動Scan訊息資料的方式相比,訂閱者式具有(1)訊息資料一旦寫入Queue便會被主動推送至訂閱者,訊息送達更為及時;(2)訂閱者被動接收新訊息,可以省去HQueue無新訊息資料時多餘的Scan操作,減少系統開銷等優點。
HQueue訂閱流程處理邏輯(5)所示:
?圖(5):HQueue訂閱流程處理邏輯
其中:
(1)HQueue訂閱主要由Subscriber、ZooKeeper和Coprocessor這三部分組成。其中:
- ?Subscrier:即訂閱者。主要完成向ZoeoKeeper寫入訂閱資訊、啟動監聽、接收新訊息並回調註冊在其上的訊息處理函數(MessageListener)等功能。
- ?ZooKeeper:用於儲存訂閱者提交的訂閱資訊,主要包括訂閱者訂閱的Queue、Partitions和Topics;訂閱者的地址和Checkpoint等資訊,更為詳細資料參見後續描述。
- ?Coprocessor:主要完成從ZooKeeper擷取訂閱資訊、使用InternalScanner從Queue中Scan最新的訊息、將新訊息發送至訂閱者並將當前Checkpoint更新至ZooKeeper等功能。
(2)Coprocessor的主要處理流程如下:
Step 1:建立Subscriber,添加訂閱資訊和訊息處理函數,將訂閱資訊寫入ZooKeeper,啟動監聽等待接收新訊息。寫入ZooKeeper中的訂閱資訊主要包括:
- ?訂閱者訂閱的Queue名稱;
- ?訂閱者訂閱的Queuee Partitions以及各Partition上訊息的起始ID。一個訂閱者可以訂閱多個Partitions,如果沒有指定,那麼認為訂閱該Queue的所有Partitions。
- ?訂閱者訂閱的訊息Topics。一個訂閱者可以訂閱多個主題,如果沒有指定,那麼認為訂閱該Queue上的所有Topics。
- ?訂閱者的Addresss/Hostname和監聽連接埠。使用者建立訂閱者時可以指定監聽連接埠,如果沒有指定,那麼會隨機播放一個當前可用連接埠作為監聽連接埠。
Step 2:Coprocessor從ZooKeeper擷取訂閱資訊並向ZooKeeper註冊相關Watcher,以便ZooKeeper中訂閱資訊發生變化時ZooKeeper能夠及時通知Coprocessor。Coprocessor在擷取到訂閱資訊後,會根據需要建立SubscriptionWorker等背景工作執行緒,以便從HQueue Partition中Scan訊息並將訊息發送至Subscriber。
Step 3:Coprocessor從HQueue Partition中Scan新訊息。
Step 4:Coprocessor將新訊息發送至Subscriber。
Step 5:Subscriber在接收到新訊息時,會回調註冊在其上的回呼函數。
Step 6:待新訊息發送成功後,Coprocessor會將訊息的Checkpoint更新至ZooKeeper以便後續使用。
Step 7:Subscriber取消訂閱,並從ZooKeeper中刪除必要的訂閱資訊。
Step 8:ZooKeeper會通過註冊在其上的Watcher將Subscriber訂閱資訊的變化通知至Coprocessor,Coprocessor根據訂閱資訊的變化,暫停SubscriptionWorker等背景工作執行緒等。
3.5.2. HQueue Subscriber
?HQueue Subscriber結構和主要處理邏輯(6)所示:
?圖(6):HQueue Subscriber結構和主要處理邏輯
其中:
?(1)Subscriber主要由兩部分組成:SubscriberZooKeeper和Thrift Server。其中,SubscriberZooKeeper主要完成與ZooKeeper相關的若干操作,包括寫入訂閱資訊、刪除訂閱資訊等。Coprocessor與Subscriber之間的通訊通過Thrift來完成,Subscriber中啟動Thrift Server,監聽指定的連接埠,等待接收Coprocessor發送過來的新訊息。
(2)Subscriber通過Thrift Server接收到新訊息後,會回調註冊在其上的回呼函數(MessageListeners),並將狀態代碼返回給Coprocessor。
(3)可以在一個Subscriber上註冊多個MessageListeners,多個MessageListeners會被依次調用。
?3.5.3. HQueue Coprocessor
?HQueue Coprocessor結構和主要處理邏輯(7)所示:
?圖(7):HQueue Coprocessor結構和主要處理邏輯
其中:
?(1)Coprocessor:主要由兩部分構成SubscriptionZooKeeper和SubscriptionWorker。
- SubscriptionZooKeeper:主要完成與ZooKeeper相關的工作,包括從ZooKeeper擷取訂閱資訊並註冊相關Watcher、SubscriptionWorker將Checkpoint更新至ZooKeeper等操作。
- SubscriptionWorker又主要包括MessageScanner和MessageSender兩部分,主要完成Scan新訊息、發送訊息至Subscriber和更新Checkpoint等操作。
(2)MessageScanner主要完成建立InternalScanner,從Queue Partition中Scan新訊息,並將其放入緩衝隊列中等操作。
- ?當緩衝隊列中沒有空閑空間時,MessageScanner會等待直至緩衝隊列中的訊息被MessageSender消費掉,騰出剩餘空間。
- ?當Queue Partition中沒有新訊息時,MessageScanner會主動Sleep,當有新訊息寫入時,Coprocessor會通過SubscriptionWorker喚醒MessageScanner,開始新一輪Scan。
(3)MessageSender主要完成從緩衝隊列中取出新訊息,將其發送至Subscriber,並等待Subscriber發迴響應等操作。當緩衝隊列中沒有新訊息時,MessageSender會等待直至有新訊息到來。
(4)MessageSender中的CheckpointUpdater會定時將當前的Checkpoint寫入ZooKeeper中的相關訂閱節點中,以便後續使用。
?3.5.4. 訂閱資訊階層
HQueue相關訂閱資訊儲存在ZooKeeper,ZooKeeper中訂閱資訊的階層(8)所示:
圖(8):訂閱資訊階層
其中:
(1)訂閱者節點(subscriber_x)上會記錄該訂閱者在Queue Partition上的Checkpoint。該Checkpoint由Subscriber在發起訂閱時寫入,並由SubscriptionWorker MessageSender中的CheckpointUpdater來更新。
(2)訂閱者節點下會有兩個臨時性節點:address和topics,分別儲存訂閱者的IP Address/Hostname:Port和訂閱的主題。當訂閱者主動取消訂閱時會刪除這兩個臨時節點,當訂閱者意外退出時,等Session失效後,ZooKeeper會刪除該臨時節點。
?3.5.5. 訂閱者Thrift Service
HQueue訂閱功能使用Thrift來簡化對多語言用戶端的支援。Subscriber啟動Thrift Server,監聽指定連接埠,接收訊息,並回調MessageListeners以便處理訊息。用於描述HQueue Subscriber所提供服務的介面定義如下所示:
namespace java com.etao.hadoop.hbase.queue.thrift.generated/*** HQueue MessageID*/struct TMessageID { 1: i64 timestamp, 2: i16 sequenceID}/*** HQueue Message*/struct TMessage { 1: optional TMessageID id, 2: optional i16 partitionID, 3: binary topic, 4: binary value}/*** HQueue Subscriber Service*/service HQueueSubscriberService { i32 consumeMessages(1:list<TMessage> messages)}4. HQueue使用4.1. HQueue Toolkit
為方便使用者使用,HQueue封裝了HQueue Client API用於存取訊息資料。自HQueue 0.3版本,HQueue日誌營運工具整合進HQueue Shell中,構成HQueue Toolkit,為使用者提供一站式服務,方便使用者管理Queue以及Queue訂閱者。
同HBase Shell使用方式相似,使用者使用$ ${HBASE_HOME}/bin/hqueue shell便可以進入HQueue Shell命令列工具。需要注意的是,使用者在使用HQueue Toolkit之前需要確保已經部署HQueue Toolkit。
? HQueue Toolkit中包括建立Queue、Disable Queue、Enable Queue、刪除Queue和清空Queue等命令。?使用樣本如下:
(1)建立隊列
USAGE:create ‘queue_name’, partition_count, ttl, [Configuration Dictionary]
DESCRIPTIONS:
- queue_name:待建立的HQueue的名稱,必選參數。
- partition_count:待建立的HQueue的Partition個數,必選參數。
- Configuration Dictonary:可選配置參數。目前支援的配置參數為:(1)hbase.hqueue.partitionsPerRegion;(2)hbase.hregion.memstore.flush.size;(3)hbase.hregion.majorcompaction;(4)hbase.hstore.compaction.min;(5)hbase.hstore.compaction.max;(6)hbase.hqueue.compression;(7)hbase.hstore.blockingStoreFiles等。
EXAMPLES:
- hqueue> create ‘q1′, 32, 86400
- hqueue> create ‘q1′, 32, 86400, {‘hbase.hqueue.partitionsPerRegion’ => ’4′, ‘hbase.hstore.compaction.min’ => ’16′, ‘hbase.hstore.compaction.max’ => ’32′}
(2)清空隊列
USAGE:truncate_queue ‘queue_name‘DESCRIPTIONS:
EXAMPLES:
需要注意的是:該命令與HBase Shell中的truncate有所不同,該命令僅會刪除Queue中的資料,而保留Queue的Presharding資訊。? 更多操作請參閱:http://searchwiki.taobao.ali.com/index.php/HQueue_Toolkit#Queue.E7.AE.A1.E7.90.86(3)新增訂閱者USAGE:add_subscriber ‘queue_name‘, ‘subscriber_name‘DESCRIPTIONS:
EXAMPLES:
(4)刪除訂閱者
USAGE:delete_subscriber ‘subscriber_name‘, ‘queue_name‘DESCRIPTIONS:
EXAMPLES:
更多資訊可以參閱:http://searchwiki.taobao.ali.com/index.php/HQueue_Toolkit#.E8.AE.A2.E9.98.85.E8.80.85.E7.AE.A1.E7.90.86
4.2. Put
?HQueue Client API中的Put相關操作可以完成將使用者訊息資料寫入HQueue中,Put支援大量操作,具體使用方式樣本如下:
HQueue queue = new HQueue(queueName);String topic1 = "crawler";String value1 = "http://www.360test.com";// 寫入單條訊息資料,不指定Partition ID。在不指定Partition ID的情況下,將會在Queue的所有Partitions中隨機選取一個。Message message1 = new Message(Bytes.toBytes(topic1), Bytes.toBytes(value1));queue.put(message);// 寫入Message時,顯式指定PartitionID。short partitionID = 10;queue.put(partitionID, message1);List<Message> messages = new ArrayList<Message>();messages.add(message1);String topic2 = "dump";String value2 = "http://www.jd.com";Message message2 = new Message(Bytes.toBytes(topic2), Bytes.toBytes(value2));messages.add(message2);// 寫入多條訊息資料,不指定Partition ID。queue.put(messages);// 寫入多條訊息資料,指定Partition ID。queue.put(partitionID, messages);queue.close();
4.3. Scan
?為方便使用者從Queue中Scan訊息資料,HQueue Client API提供了三種自訂Scanner,分別為:QueueScanner、PartitionScanner和CombinedPartitionScanner,使用樣本如下:
String queueName = "subscription_queue";Queue queue = new HQueue(queueName);// 起始時間戳記long currentTimestamp = System.currentTimeMillis();MessageID startMessageID = new MessageID(currentTimestamp - 6000);MessageID stopMessageID = new MessageID(currentTimestamp);Scan scan = new Scan(startMessageID, stopMessageID);// 添加主題scan.addTopic(Bytes.toBytes("topic1"));scan.addTopic(Bytes.toBytes("topic2"));Message message = null;// 使用QueueScanner,掃描Queue下全部Partitions中的資料QueueScanner queueScanner = queue.getQueueScanner(scan);while ((message = queueScanner.next()) != null) { // no-op}queueScanner.close();short partitionID1 = 1;// 使用PartitionScanner,掃描Queue中指定的Partition的資料PartitionScanner partitionScanner = queue.getPartitionScanner(partitionID1, scan);while ((message = partitionScanner.next()) != null) { // no-op}?partitionScanner.close();short partitionID2 = 2;Map<Short, Scan> partitions = new HashMap<Short, Scan>();// 添加多個Partitionspartitions.put(partitionID1, scan);partitions.put(partitionID2, scan);CombinedPartitionScanner combinedScanner = queue.getCombinedPartitionScanner(partitions);while ((message = combinedScanner.next()) != null) { // no-op}?combinedScanner.close();??queue.close();?4.4. 訂閱訊息
?HQueue自0.3版本開始提供訂閱功能,使用方式樣本如下:
HQueue queue = null;HQueueSubscriber subscriber = null;try { String queueName = "subscription_queue"; queue = new HQueue(queueName); Set<Pair<Short, MessageID>> partitions = new HashSet<Pair<Short, MessageID>>(); // 添加所訂閱的Partitions Pair<Short, MessageID> partition1 = new Pair<Short, MessageID>((short)0, null); partitions.add(partition1); Pair<Short, MessageID> partition2 = new Pair<Short, MessageID>((short)1, null); partitions.add(partition2); Pair<Short, MessageID> partition3 = new Pair<Short, MessageID>((short)2, null); partitions.add(partition3); // 添加所訂閱的Topics Set<String> topics = new HashSet<String>(); topics.add("topic_1"); topics.add("topic_2"); topics.add("topic_3"); // 訂閱者名稱 String subscriberName = "subscriber_1"; Subscription subscription = new Subscription(subscriberName, topics); subscription.addPartitions(partitions); // 添加回呼函數 List<MessageListener> listeners = new LinkedList<MessageListener>(); MessageListener blackHoleListener = new BlackHoleMessageListener(subscriberName); listeners.add(blackHoleListener); // 建立訂閱者 subscriber = queue.createSubscriber(subscription, listeners); subscriber.start(); Thread.sleep(600000L);? subscriber.stop("Time out, request to stop subscriber:" + subscriberName);?} catch (Exception ex) { LOG.error("Received unexpected exception when testing subscription.", ex);} finally { if (queue != null) { try { queue.close(); queue=null; } catch (IOException ex) { // ignore the exception } }}4.5. ThriftServer API
?HBase內建的ThriftServer實現了對HTable的多語言API支援,HQueue在HBase ThriftServer中擴充了對HQueue的支援,使得C++、Python和PHP等語言也可以方便地訪問HQueue。
?HQueue目前提供的Thrift API如下所示:
| 1 |
ScannerID messageScannerOpen(1:Text queueName,2:i16 partitionID,3:TMessageScan messageScan) |
根據Scan,開啟Queue中某個Partition上的Scanner |
| 2 |
TMessage messageScannerGet(1:ScannerID id) |
逐條擷取Message |
| 3 |
list<TMessage> messageScannerGetList(1:ScannerID id,2:i32 nbMessages) |
批量擷取Messages |
| 4 |
void messageScannerClose(1:ScannerID id) |
關閉ScannerID |
| 5 |
void putMessage(1:Text queueName,2:TMessage tMessage) |
向Queue中寫入Message,使用隨機的Partition ID |
| 6 |
void putMessages(1:Text queueName,2:list<TMessage> tMessages) |
向Queue中批量寫入Messages,使用隨機的Partition ID |
| 7 |
void putMessageWithPid(1:Text queueName,2:i16 partitionID,3:TMessage tMessage) |
向Queue中寫入Message,使用指定的Partition ID |
| 8 |
void putMessagesWithPid(1:Text queueName,2:i16 partitionID,3:list<TMessage> tMessages) |
向Queue中批量寫入Messages,使用指定的Partition ID |
| 9 |
list<Text> getQueueLocations(1:Text queueName) |
擷取Queue中所有Partition所在主機的地址 |
5. 總結
以上是對HQueue概念、特性、系統設計、處理流程以及應用等方面的簡單闡述,希望對大家有所協助。