天天看點

深度解析PolarDB的并行查詢引擎

PolarDB與開源MySQL及其它類MySQL的産品相比,除了計算與存儲分離的先進架構之外,另外一個最核心的技術突破就是開發了其它類MySQL産品沒有的并行查詢引擎,通過并行查詢引擎,PolarDB除了保持自身對OLTP應用的優勢之外,還對OLAP的支援能力有了一個質的飛越,遙遙領先于其它類MySQL産品。

使用者越來越多的分析統計需求

        衆所周知,MySQL的優化器目前還不支援并行優化,也不支援并行執行。當然,MySQL自己也在逐漸探索并行執行的可能,比如對count(*)的并行執行,但整體上,還沒有形成一套成熟的并行優化、并行執行的機制,隻是針對一些特殊的場景進行局部優化。随着類MySQL産品在雲上的蓬勃發展,越來越多的傳統使用者遷移到類MySQL産品上,對MySQL提出了一些新的挑戰。很多傳統使用者在對OLTP要求的同時,還要求資料庫有一些分析、統計、報表的能力,相對傳統商業資料庫來說,MySQL在這方面有明顯的劣勢。為了滿足使用者不斷提升的OLTP能力,同時又能進行OLAP分析的需求,PolarDB的并行查詢引擎應運而生。自誕生以來,通過強大的并行執行能力,原來需要幾百秒的查詢,現在隻需要幾秒,為使用者節省了大量的時間和金錢,得到大量使用者的極大好評。

PolarDB并行查詢引擎應運而生

        優化器是資料庫的核心,優化器的好壞幾乎可以決定一個資料庫産品的成敗。開發一個全新的優化器,對任何團隊都是一個巨大的挑戰,技術的複雜度暫且不提,就是想做到産品的足夠穩定就是一個非常難以克服的困難。是以即使傳統商業資料庫,也是在現有優化器的基礎上不斷改進,逐漸增加對并行的支援,最終成為一個成熟的并行優化器。對PolarDB也是如此,在設計和開發并行查詢引擎時,我們充分利用現有優化器的技術積累和實作基礎,不斷改進,不斷打磨,最終形成了一個持續疊代的技術方案,以保證新的優化器的穩定運作和技術革新。

        對于一個類OLAP的查詢,顯而易見的是它通常是對大批量資料的查詢,資料量大意味着資料遠大于資料庫的記憶體容量,大部分資料可能無法緩存到資料庫的buffer中,而必須在查詢執行時才動态加載到buffer中,這樣就會造成大量IO操作,而IO操作又是最耗時的,是以首先要考慮的就是如何能加速IO操作。由于硬體的限制,每次IO的耗時基本是固定的,雖然還有順序IO和随機IO的差別,但在SSD已經盛行的今天,兩者的差異也在逐漸接近。那麼還有沒有其它方式可以加速IO呢? 顯然并行IO是一個簡單易行的方法,如果多個線程可以同時發起IO,每個線程隻讀取部分資料,這樣就可以快速的将資料讀到資料庫的buffer中。但是如果隻是将資料讀取到buffer中,而不是立即進行後續處理,那麼這些資料就會因buffer爆滿導緻資料被換出,進而失去加速IO的意義。

深度解析PolarDB的并行查詢引擎

      圖1-并行IO示意圖

        是以,在并行讀取資料的同時,必須同時并行的處理這些資料,這是并行查詢加速的基礎。因為原有的優化器隻能生成串行的執行計劃,為了實作并行讀取資料,同時并行處理資料,首先必須對現有的優化器進行改造,讓優化器可以生成我們需要的并行計劃。比如哪些表可以并行讀取,并且通過并行讀取會帶來足夠的收益;或者哪些操作可以并行執行,并且可以帶來足夠的收益。并不是說并行化改造一定會有收益,比如對一個資料量很小的表,可能隻是幾行,如果也對它進行并行讀取的話,并行執行所需要的多線程建構所需要的代價可能遠大于所得到的收益,總體來說,并行讀取會需要更多的資源和時間,這就得不償失了,是以并行化的改造必須是基于代價的,否則可能會導緻更嚴重的性能褪化問題。

Fact表的并行掃描

        通過基于并行cost的計算和比較,選擇可以并行讀取的表作為候選,是并行執行計劃的第一步。基于新的并行cost,也許會有更優的JOIN的順序選擇,但這需要更多的疊代空間,為防止優化過程消耗太多的時間,保持原有計劃的JOIN順序是一個不錯的選擇。另外,對于參與JOIN的每張表,因為表的通路方法不同,比如全表掃描、ref索引掃描,range索引掃描等,這些都會影響到最終并行掃描的cost。

        通常我們選擇最大的那張表作為并行表,這樣并行掃描的收益最大,當然也可以選擇多個表同時做并行掃描,後面會繼續讨論更複雜的情況。

下面以查詢年度消費TOP 10的使用者為例:

SELECT c.c_name, sum(o.o_totalprice) as s FROM customer c, orders o WHERE c.c_custkey = o.o_custkey AND o_orderdate >= '1996-01-01' AND o_orderdate <= '1996-12-31' GROUP BY c.c_name ORDER BY s DESC LIMIT 10;

其中orders表為訂單表,資料很多,這類表稱之為Fact事實表,customer表為客戶表,資料相對較少,這類表稱之為dimension次元表。那麼此SQL的并行執行計劃如下圖所示:

深度解析PolarDB的并行查詢引擎

從計劃中可以看出orders表會做并行掃描,由32個workers線程來執行,每個worker隻掃描orders表的某些分片,然後與customer表按o_custkey做eq_ref進行JOIN,JOIN的結果發送到使用者session中一個collector元件,然後由collector元件繼續做後續的GROUP BY、ORDER BY及LIMIT操作。

多表并行JOIN

        将一張表做并行掃描之後,就會想為什麼隻能選擇一張表?如果SQL中有2張或更多的FACT表,能不能可以将FACT表都做并行掃描呢?答案是當然可以。以下面SQL為例:

SELECT o.o_custkey, sum(l.l_extendedprice) as s FROM orders o, lineitem l WHERE o.o_custkey = l.l_orderkey GROUP BY o.o_custkey ORDER BY s LIMIT 10;

其中orders表和lineitem表都是資料量很大的FACT表,此SQL的并行執行計劃如下圖所示:

深度解析PolarDB的并行查詢引擎

        從計劃中可以看到orders表和lineitem表都會做并行掃描,都由32個workers線程來執行。那麼多個表的并行是如何實作的呢?我們以2個表為例,當2個表執行JOIN時,通常的JOIN方式有Nested Loop JOIN、HASH JOIN等,對于不同的JOIN方式,為保證結果的正确性,必須選擇合理的表掃描方式。以HASH JOIN為例,對于串行執行的HASH JOIN來說,首先選擇一個表建立HASH表稱之謂Build表,然後讀取另一個Probe表,計算HASH,并在Build表中進行HASH比對,若比對成功,輸出結果,否則繼續讀取。如果改為并行HASH JOIN,并行優化器會對串行執行的HASH JOIN進行并行化改造,使之成為并行HASH JOIN,并行化改造的方案可以有以下兩種解決方案。方案一是将2個表都按HASH key進行分區,相同HASH值的資料處于同一個分區内,由同一個線程執行HASH JOIN。方案二是建立一個共享的Build表,由所有執行HASH JOIN的線程共享,然後每個線程并行讀取屬于自己線程的另外一個表的分片,再執行HASH JOIN。

深度解析PolarDB的并行查詢引擎

圖2-并行HASH JOIN示意圖

  • 對于方案一,需要讀取表中的所有資料,根據選中的HASH key,對資料進行分區,并将資料發送到不同的處理線程中,這需要額外增加一個Repartition算子,負責根據分區規則将資料發送到不同的處理線程。為了提高效率,這裡通常會采用message queue隊列來實作。
  • 對于方案二,需要并行建立共享的HASH build表,當build表建立成功後,每個線程讀取Probe表的一個分片,分别執行HASH JOIN,這裡的分片并不需要按照HASH key進行分片,每個線程分别讀取互不相交的分片即可。

分析統計算子的并行

        對于一個分析統計的需求,GROUP BY操作是繞不開的操作,尤其對大量的JOIN結果再做GROUP BY操作,是整個SQL中最費時的一個過程,是以GROUP BY的并行也是并行查詢引擎必須優先解決的問題。

以年度消費TOP10客戶的SQL為例,對GROUP BY并行化後的并行執行計劃如下圖所示:

深度解析PolarDB的并行查詢引擎

        與之前的執行計劃相比,新的執行計劃中多了一個collector元件,總共有2個collector元件。首先我們看第二行的collector元件,它的extra資訊中有2條"Using temporary; Using filesort",這表示它是對從workers接收到的資料執行GROUP BY,然後再按ORDER排序,因為隻有第一個collector元件在使用者的session中,是以這個collector也是在worker中并行執行,也就是說并行的做Group by和Order by以及Limit;然後看第一行的collector元件,它的extra資訊中隻有一條"Merge sort",表示session線程對從workers接收到的資料執行一次merge sort,然後将結果傳回給使用者。這裡可能就有人會提出疑問,為什麼session線程隻做merge sort就可以完成GROUP BY操作呢?另外LIMIT在哪裡呢?

        首先回答第2個問題,因為explain計劃顯示的問題,在正常模式下不顯示LIMIT操作,但在Tree模式下會顯示LIMIT操作。如下所示:

深度解析PolarDB的并行查詢引擎

        從Tree型計劃樹上可以清楚的看到LIMIT操作有2處,一處在計劃的頂端,也就是在session上,做完limit後将資料傳回給使用者;另外一處在計劃樹的中間位置,它其實是在worker線程的執行計劃上,在每個worker線程中在排序完成後也會做一次limit,這樣就可以極大減少worker傳回給session線程的資料量,進而提升整體性能。

        下面來回答第一個問題,為什麼GROUP BY隻需要在worker線程上執行一次就可以保證結果的正确性。通常來說,每個worker隻有所有資料的一個分片,隻在一個資料分片上做GROUP BY是有極大的風險得到錯誤的GROUP BY結果的,因為同一GROUP分組的資料可能不隻是在本WORKER的資料分片上,也可能在其它WORKER的資料分片中,被其它WORKER所持有。但是如果我們可以保證同一GROUP分組的資料一定位于同一個資料分片,并且這個資料分片隻被一個WORKER線程所持有,那麼就可以保證GROUP BY結果的正确性。通過Tree型執行計劃可以看到,在并行JOIN之後,将JOIN的結果按GROUP分組的KEY值: c.c_name進行Repartition操作,将相同分組的資料分發到相同的WORKER,進而保證每個WORKER擁有的資料分片互不交叉,保證GROUP BY結果的正确性。

        因為每個WORKER的GROUP BY操作已經是最終結果,是以還可以将ORDER BY和LIMIT也下推到WORKER來執行,進一步提升了并行執行的效率。

并行查詢引擎對TPCH的線性加速

        附圖是一個并行查詢引擎對TPCH的加速效果,TPC-H中100%的SQL可以被加速,70%的SQL加速比超過8倍,總和加速近13倍,Q6和Q12加速甚至超過32倍。

深度解析PolarDB的并行查詢引擎

不斷疊代創新的并行查詢引擎

        總之,通過對并行查詢引擎的支援,PolarDB不僅在保持查詢引擎穩定的同時,還極大的提升了複雜SQL,尤其是分析統計類型查詢的性能。通過有計劃的不斷疊代,PolarDB在并行查詢引擎的道路上越走越遠,也越來越強大,為了滿足客戶日益不斷增長的性能需求,為了更多企業使用者的數字化更新,PolarDB為您提供革命性的資料引擎,助您加速擁抱萬物互聯的未來。