1. 程式人生 > >一種解決Impala自定義屬性查詢的方案

一種解決Impala自定義屬性查詢的方案

背景

在使用Impala做自助分析的過程中,我們經常發現自定義屬性的需求,通常情況下使用者會將這種不確定key的欄位全部塞到一個MAP欄位中,然後通過Impala的複雜結構查詢語法進行查詢,目前Impala只支援Parquet格式表的schema中使用複雜資料型別(包括STRUCT、ARRAY和MAP),查詢語法可以參考 Impala複雜型別語法,但是它存在兩個弊端:語法不相容hive和查詢效能不理想。第一個問題是由於特定的實現方式導致的,第二個問題則是由於parquet儲存MAP型別的欄位決定的。那麼有沒有辦法提升使用者的這種查詢需求和簡化查詢SQL的寫法呢?

JSON UDF VS. MAP

為了避免MAP在impala中複雜的使用方式和效能不好的問題,我們一般建議使用JSON字串的方式代替map儲存表達的欄位內容,畢竟json能夠完全表示出MAP的語義,而開發一般使用JAVA實現,也比較方便生成JSON格式的資料。通過使用json解析的UDF來抽取其中想要的欄位,我們首先使用了一個開源的JSON UDF的實現 ,這個開源的實現使用了rapidjson作為json解析器,它的使用方法類似於hive的get_json_object,函式定義如下:

STRING get_json_object(STRING, STRING)

第一個引數為輸入的json字串,第二個引數是需要解析的內容,該內容通過如下方式解析:

  • $ : Root object(表示根節點)
  • . : Child operator(表示父節點和子節點的分隔符)
  • [] : Subscript operator for array(訪問陣列)
    • : Wildcard for [] (萬用字元)

例如對於一個{“hello”, “world”}的JSON欄位(欄位名為col)想要提取hello的值,可以使用get_json_object(col, “$.hello”)得到的值就是”world”。

通過它可以提取map中想要的欄位,它的實現原理是用rapidjson解析輸入的第一個引數,解析成Document之後再通過解析第二個引數選取想要的欄位內容,通過對比測試原生map的查詢和使用這個UDF進行測試的結果如下:

測試發現json自定義函式的效能並不如原生map查詢的效能,我認為主要是基於兩方面的原因:

  • 使用json字串需要讀取更多的資料,根據profile可以看到JSON格式的列要比map多讀取一倍的資料量。
  • 每一個JSON列都需要解析轉換成Document物件,解析過程中rapidjson不可避免的又會進行一些資料拷貝操作,整個解析的代價是比較大的。

JSON UDF優化

意識到該問題之後,我們就需要想辦法減少如上兩個方面的開銷,首先對於儲存空間的減少,但是對於json的結構,內容是相對比較規定的,這點不容易優化,而第二點,我們是否能夠在不解析整個json字串就獲取到想要的結果呢?假設它不是一個json字串,而是一個key:value的儲存格式,我們可以輕鬆的順序查詢,找到對應的key就直接返回value的,按照這個思路,我們自己涉及了一種解析JSON的邏輯,基於如下的狀態機:

這是一個對於一層結構的JSON的字串進行解析的狀態機(也不考慮陣列),將這個字串遍歷分為多個狀態,例如當從字串開始遇到‘{’字元就將狀態轉換為FIND_KEY,進行查詢key的操作,當遇到雙引號表示已經找到了key,此時我們就可以將找到的key與輸入的key進行比較,如果相同則下一個找到的value就是查詢的結果,這樣的做法將JSON的解析轉換成了一個關鍵字查詢的過程,平均時間複雜度大大降低,新增該UDF進行測試發現如下的對比結果:

通過測試結果可以發現,隨著查詢的關鍵字個數的增加兩者都是X + N * Y(N是關鍵字的個數),其中MAP表的Y = 13s,新的JSON UDF的Y大於等於7s,這意味著隨著查詢關鍵字個數的增長,查詢效能有了大約45%的提升,這也意味著我們減少查詢平均時間複雜度的做法是可行的,但是這種方案的查詢時間複雜度仍然是O(n),有沒有什麼辦法進一步提升查詢效能呢?

優化的儲存結構

將之前JSON格式的查詢問題轉換成字串查詢問題之後,思路就可以放寬了,我們都知道在查詢演算法中有兩種實現的效能比較好,分別是雜湊表和二分查詢樹,這也對應著Map的兩種實現,我們是否可以將需要寫入的key:value轉換成這樣的格式呢?但是看著這樣複雜的結構序列化和反序列化都是一個比較頭大的問題,除了這兩個資料結構,我們還知道二分查詢的效能最快情況下是O(log N),那我們能不能使用二分查詢呢?

先來分析一下二分查詢的兩個前提條件:

  • 待查詢的值必須是有序的。

- 待查詢的值必須能夠隨機訪問(隨機訪問意味著訪問任意一個key的時間複雜度都是O(1))。

對於第一個條件我們比較熟悉,因為二分查詢都是通過陣列來實現儲存的,陣列的每一個元素都是可以隨機訪問的,這意味著我們可以通過arr [(high + low) / 2]訪問下一個比較的元素。但是輸入的key和value都是未知大小的,我們需要根據key進行比較,難不成要將所有的key都儲存成一樣的大小?這樣意味著所有的key都需要儲存成最大的key的長度,浪費了不少儲存空間,最終我們選擇這樣一種二進位制的儲存格式:

通過這樣的結構,我們將key:value轉換成一個定長的索引資訊,整個結構從前往後包括如下幾部分:

  1. Magic Number:魔數,用於標記該值是否是可識別的儲存格式,4個位元組
  2. HEADER_LEN:標記該結構中儲存的key:value值的個數,如果預定義該結構最大的值數量為65535,只需要2個位元組。
  3. 值的索引陣列:每一個值通過固定大小的索引來表示,它包含三部分:偏移量(表示當前key的偏移量,對一個key從0開始,第二個key的偏移量等於第一個key長度+value長度,依此類推,2個位元組或者4個位元組);key的長度(如果key的大小限定在256位元組,只需要1個位元組);value的長度(value的偏移量可以通過該值的偏移量+key的長度計算,1個位元組或者2個位元組)。
  4. 真正的key和value對,key和value連續儲存。

這樣除了真正的key和value的值,額外需要4 + 4 + (4 + 2 + 2) * N的儲存空間儲存索引資訊(為了保持8位元組對齊可以擴大成8 * (N + 1)位元組),其中N等於key/value對的個數。這種格式可以儲存多大65536個key/value對,每一個key和value的最大長度為65536位元組。

從上面的JSON和MAP對比測試可以發現數據量的增大並不是效能變慢的主要原因(優化之後的JSON UDF同樣需要讀取兩倍的資料量,但是效能提升了許多),因此這種儲存上的浪費是可以接受的。而採用了新的儲存結構,可以使得查詢的時間複雜度從O(N)提升到O(log N),那麼對於寫入和查詢的流程又需要做哪些額外的工作呢?

寫入端(Java端):

  1. 寫入的時候需要首先將key進行排序(放入到一個TreeMap中)。
  2. 分別構造出每一段,最後通過UDF-8編碼成byte陣列寫入(不像JSON有現成的庫使用)。

讀取端(C++ UDF):

  1. 讀取的時候首先讀取前4個位元組,判斷魔數是否等於預定義的值,如果不等於直接返回錯誤。
  2. 讀取第二個4位元組,獲取成員的個數,該值也就是後面陣列的大小。
  3. 讀取第三部分索引陣列,根據二分查詢的演算法,定位到最中間的索引內容(偏移量、key的長度),然後和待查詢的值進行比較,指導查詢結束。
  4. 如果沒有找到則返回NULL,找到該key根據偏移量、key長度和value的長度計算出value的偏移量和長度返回。

進一步擴充套件和優化

有了這樣的一個儲存結構,是否可以再進行進一步的優化呢?答案是肯定的,在二分查詢的過程中,最壞情況下的時間複雜度是O(log N),是當待查詢的值不存在的情況,是否有更高效的方案判斷一個key是否存在呢,如果不存在則可以直接返回NULL了,於是想到了Bloom Filter演算法,它存在一定的誤差,但是具有如下的特性:

  • 它判斷一個key不存在,那麼這個key肯定不存在。
  • 它判斷一個key存在,那麼這個key可能不存在。

這個特性正好能夠符合我們的需求,因此可以考慮在MAGIC和HEADER_LEN部分中間插入計算好的bloom filter資訊,但是這個是否值得還需要進一步測試對比,因為引入了Bloom Filter會增大了計算的開銷(雖然Bloom Filter的計算只是幾個雜湊函式的計算),如果待查詢的key在大部分情況下(例如90%)都是能夠找到的,那麼這個開銷就有點得不償失;如果待查詢的key大多數情況下是不存在的,這種開銷可以大大提升查詢的效能。

但是上面的結構只表示除了單層的key/value結構,這和json表示的語義是有差別的,怎麼協調這種差別呢?其實在JSON中無外乎兩種巢狀結構,一種是MAP一種是ARRAY,假設MAP中的key不包含字元’.’,我們可以將子節點通過’.’和父節點進行連線,轉換成扁平的格式,陣列同樣,可以通過父節點的名字和陣列下表將其轉換成扁平結構。如下:

{
    "name" : "yu",
    "location" : {
        "province" : "ZZ",
        "city" : "HZ"
    },
    "education" : ["ABC", "DEF", "GH"]
}

可以轉換成name = yu,location.province = ZZ, location.city = HZ, educaion.0 = ABC, education.1 = DEF education.2 = GH 這樣扁平的結構儲存在上面提到的結構中,查詢的時候也根據需要查詢的節點路徑輸入就可以解決了。

總結

本文我們對比了impala中原生MAP和使用JSON UDF的方法進行不確定屬性欄位的查詢,然後提出了一種新的基於關鍵字查詢的方案提升了JSON欄位內容解析的效能,並比原生的MAP有了將近50%的效能提升,但是我們沒有止步於此,而是探索出一種特定的可以實現二分查詢的儲存結構,使用這種結構可以使用二分查詢來完成屬性的查詢,並提出進一步基於Bloom Filter的優化方案。最終結果有待於進一步的對比測試。