Cursor共享哪些事,你知道哪些?
早些年搞Oracle的時候,最讓人頭疼的事情就是和共享池相關(guān)的,而共享池里遇到問題最多的事情大多數(shù)是和SQL編譯相關(guān)的,關(guān)于十多年前這方面的往事我最近總想寫一篇Oracle往事來給大家介紹一下。
Oracle共享池最初是為了全局共享SQL執(zhí)行計劃而設(shè)計的,全局共享執(zhí)行計劃可以最大限度的減少SQL解析,在高并發(fā)的系統(tǒng)中提升數(shù)據(jù)庫的整體并發(fā)能力,這對于二三十年前的計算機硬件來說至關(guān)重要。其實除了Oracle之外,那個時代發(fā)展起來的商用數(shù)據(jù)庫大多也支持全局SQL執(zhí)行計劃共享,比如SQL SERVER 的Parameterized Query Plan Caching。
全局SQL PLAN共享并不是數(shù)據(jù)庫必須具備的功能,也不是所有的數(shù)據(jù)庫都支持全局SQL PLAN共享,MySQL、Postgresql等開源數(shù)據(jù)庫以及絕大多數(shù)國產(chǎn)數(shù)據(jù)庫都是會話級共享SQL PLAN的。會話級共享SQL PLAN可能沒有全局共享效果好,不過MySQL、PG這些數(shù)據(jù)庫在設(shè)計之初并沒有考慮在復(fù)雜業(yè)務(wù)場景下的超高并發(fā)執(zhí)行問題,因此全局共享SQL PLAN并不是其涉及要點。采用會話級共享執(zhí)行計劃減少了數(shù)據(jù)字典相關(guān)的鎖以及字典緩沖的閂鎖爭用,有效的降低了SQL解析器的復(fù)雜度,有助于數(shù)據(jù)庫更穩(wěn)定的運行,特別是在數(shù)據(jù)字典不太發(fā)生變化的場景中。隨著現(xiàn)代硬件的快速發(fā)展,在大多數(shù)情況下,SQL解析所消耗的系統(tǒng)資源已經(jīng)不成為主要的問題了,因此沒有使用全局SQL PLAN CACHE的開源數(shù)據(jù)庫在絕大多數(shù)高并發(fā)量執(zhí)行的場景中也都能夠勝任。在我所遇到的運維案例中,反而是采用全局執(zhí)行計劃共享的Oracle數(shù)據(jù)庫經(jīng)常在負(fù)載不算太高的場景中,比如每秒幾萬次執(zhí)行的場景中,因為硬解析過多而觸發(fā)了shared pool、Library cache 、cursor mutex等方面的爭用,引發(fā)了較為嚴(yán)重的性能問題。
不管如何,共享執(zhí)行計劃(全局和會話級)確實有效的提高了數(shù)據(jù)庫在SQL解析方面的效率,從而可以更好的支撐高并發(fā)場景和一些數(shù)據(jù)字典經(jīng)常發(fā)生變更的高并發(fā)場景。不同的數(shù)據(jù)庫共享SQL PLAN的實現(xiàn)方式差異很大,Oracle使用家傳的共享池,隨著Oracle數(shù)據(jù)庫的發(fā)展,共享池已經(jīng)變成了一個極其極其復(fù)雜的全局共享數(shù)據(jù)結(jié)構(gòu),不僅僅用于SQL PLAN共享,其全局內(nèi)存堆(KGH)管理的模式是統(tǒng)一的。其他數(shù)據(jù)庫一般使用相對簡單一點的SQL PLAN CACHE來實現(xiàn)執(zhí)行計劃共享。
實現(xiàn)執(zhí)行計劃共享遇到的第一個問題是SQL使用非綁定變量的問題,最初的SQL共享完全是基于SQL文本的,其原理是對SQLTEXT做一個HASH函數(shù),HASH值相同的SQL被認(rèn)為是相同的,可以共享的。如果我們在SQL中直接使用常量值而沒有使用綁定變量,那么SQL是無法共享的。Oracle在早期想要共享cursor,必須強制開發(fā)人員在編程時使用綁定變量。但是開發(fā)人員的水平參差不齊,經(jīng)常會忘記這個開發(fā)規(guī)范。于是Oracle通過cursor_sharing參數(shù)對SQL進行簽名處理。自動將相似的SQL進行歸并,從而讓SQL能夠最大限度的共享。哪怕寫程序是沒有使用綁定變量,SQL解析器會自動幫你完成這個工作。
通過cursor_sharing自動處理非綁定變量的SQL或者使用綁定變量一定是件好事情嗎?也不一定是這樣的,如果某條SQL只有一個最佳的SQL PLAN,這是沒有問題的。不過在現(xiàn)實中可能存在相同的SQL有多種最好的執(zhí)行計劃的情況存在。比如下面這個例子。
圖片
當(dāng)Object_id為20的時候,記錄數(shù)有419萬行數(shù)據(jù),因此全表掃描是比較合理的執(zhí)行計劃。
圖片
而當(dāng)Object_id=21的時候,返回數(shù)據(jù)只有16行,因此此時使用索引范圍掃描是比較合理的執(zhí)行計劃。而如果我們使用綁定變量,select object_name from t1 where object_id=:p,這樣兩種情況如果共享執(zhí)行計劃,那就明顯是不合理的。
這種情況下如果共享執(zhí)行計劃,那么可能會出現(xiàn)十分怪異的情況。比如說同一條SQL有時候走索引,有時候走全表掃描。有時候突然就會大量SQL不走索引全部走執(zhí)行計劃了。甚至出現(xiàn)同一條SQL在不同RAC節(jié)點上,一個節(jié)點是走索引的,另一個節(jié)點上死活不走索引。其實明白了SQL共享與SQL解析的原理就很清楚了。在解析帶有綁定變量或者通過cursor_sharing進行共享的SQL的時候,會進行綁定變量的窺探,編譯時帶入的參數(shù)就決定了執(zhí)行計劃最終走不走索引。而下一回同一條可共享的SQL執(zhí)行的時候,就不會再去窺探參數(shù)了,因此就會導(dǎo)致不必要的SQL執(zhí)行計劃錯誤。
為了解決這個問題,Oracle推出了Adaptive Cursor Sharing(ACS)。在ACS技術(shù)的加持下如果存在數(shù)據(jù)偏斜,ACS 能夠識別不同的綁定變量值具有不同的選擇性,從而選擇最為合理的執(zhí)行計劃。
當(dāng)帶有綁定變量的 SQL 語句首次被解析時,優(yōu)化器會窺探綁定變量的值,并根據(jù)謂詞的選擇性生成一個執(zhí)行計劃,同時把該游標(biāo)標(biāo)記為 bind-sensitive(綁定敏感的)。當(dāng)同樣的 SQL 語句再次被執(zhí)行時,優(yōu)化器會比較當(dāng)前綁定變量的值和之前的值,如果發(fā)現(xiàn)選擇性有顯著差異,優(yōu)化器可能會創(chuàng)建一個新的子游標(biāo)和執(zhí)行計劃,同時把該游標(biāo)標(biāo)記為 bind-aware(綁定感知的)。當(dāng)同樣的 SQL 語句再次被執(zhí)行時,優(yōu)化器會根據(jù)綁定變量的值的選擇性范圍,匹配最合適的子游標(biāo)和執(zhí)行計劃,從而避免使用不適合的執(zhí)行計劃。
目前Oracle ACS可以在絕大多數(shù)場合下解決cursor共享的問題,不過ACS也存在一定的負(fù)面作用。比如會增加每次SQL執(zhí)行的開銷,同時會讓一個CURSOR產(chǎn)生過多的不共享的執(zhí)行計劃,從而影響這個CURSOR的執(zhí)行效率,增加MUTEX爭用,嚴(yán)重時會引發(fā)系統(tǒng)性能問題。因此在某些應(yīng)用場景中,用戶會選擇關(guān)閉ACS功能。
看到這里可能大多數(shù)朋友都會覺得我今天還是在炒Oracle ACS的冷飯,如果能看到這里的朋友,今天算是來對了。前面的近兩千字的鋪墊,只是為了讓人更好地理解今天我想帶給大家的一些干貨。
今天要帶給大家的第一點干貨是和Oracle ACS相關(guān)的。雖然很多數(shù)據(jù)庫不支持全局SQL PLAN CACHE,不過一般都支持會話級SQL PLAN CACHE。當(dāng)某條SQL執(zhí)行多次的時候,就不會再對這條SQL做解析,而直接復(fù)用緩沖中的執(zhí)行計劃了。那么與ORACLE 類似的問題出現(xiàn)了,如果數(shù)據(jù)是不均衡的,有些時候要走索引,有些時候需要走全表掃描怎么辦?這種情況下,就會出現(xiàn)類似Oracle出現(xiàn)過的奇怪現(xiàn)象,某條SQL,有時候執(zhí)行效率高,有時候執(zhí)行效率低,而且我們無法控制。遇到這種情況,有時候可能就是因為SQL PLAN CACHE緩沖的執(zhí)行計劃不一定適合某個場景的SQL。
有些用過Oracle的朋友可能會想到解決這個問題的辦法,那就不使用綁定變量,讓SQL PLAN無法共享。其實這個辦法在某些開源或者國產(chǎn)數(shù)據(jù)庫中并不一定有效。比如PG數(shù)據(jù)庫,默認(rèn)就會對SQL進行簽名,自動轉(zhuǎn)換成綁定變量格式,就像Oracle數(shù)據(jù)庫里設(shè)置了cursor_sharing=FORCE。而且這些數(shù)據(jù)庫往往有不支持類似Oracle ACS的功能,因此使用非綁定變量來解決這個問題是無效的。
我們該如何解決這個問題呢?其實在Oracle占主導(dǎo)地位的時代,這個問題就已經(jīng)有解了。如果我們關(guān)閉了ACS功能,但是確實存在某些場景中同樣的SQL帶入不同的參數(shù)時,需要有不同的執(zhí)行計劃,該如何處理呢。遇到這種情況,我們就只能通過在SQL語句上加上注釋(/* PLAN B */),強制性的讓優(yōu)化器把這條SQL區(qū)分為兩條不同的SQL。實際上,當(dāng)Oracle還沒有推出ACS功能的時候,我們就是這樣在cursor_sharing=FORCE的數(shù)據(jù)庫里糾正錯誤的執(zhí)行計劃的,這個方法對于MySQL、PG和一些國產(chǎn)數(shù)據(jù)庫依然有效。
今天的第二點干貨是關(guān)于分布式數(shù)據(jù)庫的,與集中式數(shù)據(jù)庫不同的是,分布式數(shù)據(jù)庫上的 硬解析的成本要高得多,因此在分布式數(shù)據(jù)庫中,盡可能要實現(xiàn)SQL PLAN CACHE。因此在分布式數(shù)據(jù)庫上,因為SQL PLAN CACHE引起的SQL PLAN CACHE中的執(zhí)行計劃不適配的問題依然是存在的,并且在一些高負(fù)載的場景中,往往因為此類問題引發(fā)分布式集群范圍的性能問題。這種情況下,如果你能夠很快發(fā)現(xiàn)問題,并且將某個不合理的執(zhí)行計劃從SQL PLAN CACHE中清除掉,很快就能解決數(shù)據(jù)庫集群的性能問題。
周五的時候,張瑞遠先生就和我討論過一個在OB上遇到的SQL PLAN CACHE引發(fā)的執(zhí)行計劃問題。經(jīng)過分析發(fā)現(xiàn)OB默認(rèn)的CURSOR_SHARING是FORCE,也就是說默認(rèn)情況下,OB會自動對SQL進行簽名,將沒有使用綁定變量的SQL轉(zhuǎn)化為使用綁定變量的格式。如果訪問的數(shù)據(jù)存在較為嚴(yán)重的列傾斜現(xiàn)象,那么就會遇到SQL PLAN CACHE中的執(zhí)行計劃不適用的問題。經(jīng)過和OB的朋友一起討論,對OB中解決這個問題初步有了一個方案。首先在Oracle上使用 的PLAN B方案依然適用。如果某條SQL根據(jù)綁定變量不同,有少量的幾種情況可以明確區(qū)分,那么PLAN B方案是可行的。如果數(shù)據(jù)傾斜問題比較復(fù)雜,不能簡單的分類,那么在OB中可以通過HINT或者outlines,將這條SQL設(shè)置為不適用PLAN CACHE來規(guī)避這個問題。
在Oracle數(shù)據(jù)庫中,也有類似的HINT,在數(shù)據(jù)庫產(chǎn)品沒有ACS功能或者關(guān)閉了ACS功能的 時候,這是一種十分有效的方法。繞開PLAN CACHE可以讓本身就無法共享執(zhí)行計劃的SQL不要去干擾PLAN CACHE,可以更好的保護全局PLAN CACHE。
沒想到今天寫著寫著就寫多了,數(shù)據(jù)庫的問題,每個小問題其實都夠復(fù)雜的,攤開了講,三五千字根本說不清楚,不過不要緊,花上幾年時間,一個個研究清楚,你也就成了高手了。