PostgreSQL 查询涉及分区表过多导致的性能问题 - 性能诊断与优化(大量BIND, spin lock, SLEEP进程)
摘要: 標(biāo)簽 PostgreSQL , 分區(qū)表 , bind , spin lock , 性能分析 , sleep 進(jìn)程 , CPU空轉(zhuǎn) , cache 背景 實(shí)際上我寫過很多文檔,關(guān)于分區(qū)表的優(yōu)化: 《PostgreSQL 商用版本EPAS(阿里云ppas) - 分區(qū)表性能優(yōu)化 (堪比pg_pathman)》 《PostgreSQL 傳統(tǒng) hash 分區(qū)方法和性能》 《PostgreSQL 10 內(nèi)置分區(qū) vs pg_pathman perf profiling》 實(shí)際上native分區(qū)表的性能問題主要還是在于分區(qū)表過多的時(shí)候,執(zhí)行計(jì)劃需要耗時(shí)很久。
點(diǎn)此查看原文:https://yq.aliyun.com/articles/405176?spm=a2c4e.11153959.teamhomeleft.44.8WKxt7
實(shí)際上native分區(qū)表的性能問題主要還是在于分區(qū)表過多的時(shí)候,執(zhí)行計(jì)劃需要耗時(shí)很久。
因此有了
1、PPAS的edb_enable_pruning參數(shù),可以在生成執(zhí)行計(jì)劃前,使用簡(jiǎn)單SQL的話,直接過濾到目標(biāo)分區(qū),從而不需要的分區(qū)不需要進(jìn)入執(zhí)行計(jì)劃的環(huán)節(jié)。
2、pg_pathman則支持的更全面,除了簡(jiǎn)單SQL,復(fù)雜的表達(dá)式,immutable都可以進(jìn)行過濾,過濾到目標(biāo)分區(qū),從而不需要的分區(qū)不需要進(jìn)入執(zhí)行計(jì)劃的環(huán)節(jié)。
因分區(qū)表過多引發(fā)的問題通常出現(xiàn)在OLTP系統(tǒng)(主要是OLTP系統(tǒng)的并發(fā)高,更容易把這種小問題放大),本來一次請(qǐng)求只需要1毫秒的,但是執(zhí)行計(jì)劃可能需要上百毫秒,也就是說執(zhí)行耗時(shí)變成了小頭,而執(zhí)行計(jì)劃(SPIN LOCK)變成了大頭。
下面這個(gè)例子也是OLTP系統(tǒng)相關(guān)的,有具體的原因分析。
SQL訪問的分區(qū)表過多,并發(fā)高時(shí)CPU負(fù)載高,但是大量的是SLEEP狀態(tài)的BIND進(jìn)程。
某個(gè)業(yè)務(wù)系統(tǒng),單次SQL請(qǐng)求很快,幾十毫秒,但是并發(fā)一高,QPS并沒有線性的增長(zhǎng)。
而且大量的進(jìn)程處于BIND,SLEEP的狀態(tài)。
經(jīng)過診斷,
《PostgreSQL 源碼性能診斷(perf profiling)指南》
《Linux 性能診斷 perf使用指南》
主要的原因是大量的SPIN LOCK,導(dǎo)致CPU空轉(zhuǎn)。
perf record -ag perf report -g
比如某個(gè)進(jìn)程BIND時(shí)的pstack
#pstack 18423 #0 0x00002ad051f3ef67 in semop () from /lib64/libc.so.6 -- 這邊到了內(nèi)核,上spin lock #1 0x0000000000656117 in PGSemaphoreLock () #2 0x00000000006c274a in LWLockAcquire () #3 0x00000000006bd136 in LockAcquireExtended () #4 0x00000000006b8768 in LockRelationOid () -- 對(duì)所有的子表都會(huì)調(diào)用這個(gè)函數(shù),導(dǎo)致spinlock #5 0x000000000050c10a in find_inheritance_children () #6 0x000000000050c212 in find_all_inheritors () -- 找到所有子表 #7 0x0000000000645e4e in expand_inherited_tables () #8 0x000000000063a6e8 in subquery_planner () #9 0x0000000000618c4f in set_rel_size () #10 0x0000000000618e7c in set_rel_size () #11 0x0000000000619587 in make_one_rel () #12 0x0000000000636bd1 in query_planner () #13 0x000000000063862c in grouping_planner () #14 0x000000000063a9c4 in subquery_planner () #15 0x0000000000618c4f in set_rel_size () #16 0x0000000000619587 in make_one_rel () #17 0x0000000000636bd1 in query_planner () #18 0x000000000063862c in grouping_planner () #19 0x000000000063a9c4 in subquery_planner () #20 0x0000000000618c4f in set_rel_size () #21 0x0000000000619587 in make_one_rel () #22 0x0000000000636bd1 in query_planner () #23 0x000000000063862c in grouping_planner () #24 0x000000000063b0d0 in standard_planner () #25 0x00000000006d1597 in pg_plan_queries () #26 0x00000000007ca156 in BuildCachedPlan () #27 0x00000000007ca525 in GetCachedPlan () #28 0x00000000006d1d07 in exec_bind_message () #29 0x00000000006d44de in PostgresMain () #30 0x000000000066bd5f in PostmasterMain () #31 0x00000000005f474c in main ()由于業(yè)務(wù)使用了prepared statement,所以過程會(huì)變成bind 過程
1、prepare statement
2、bind parameters
3、代入?yún)?shù)、(設(shè)置了constraint_exclusion時(shí))判斷哪些分區(qū)需要被過濾
4、execute prepared statement
在find_all_inheritors過程中,涉及的分區(qū)表過多,最后每個(gè)分區(qū)都要取LOCK(后面加載了系統(tǒng)的spin lock),所以我們會(huì)看到CPU很高,同時(shí)大量的BIND,進(jìn)程處于SLEEP狀態(tài),也就是CPU空轉(zhuǎn),CPU時(shí)間片被獨(dú)占的狀態(tài)。
spinlock (自旋鎖)?
自旋鎖是專為防止多處理器并發(fā)而引入的一種鎖,它在內(nèi)核中大量應(yīng)用于中斷處理等部分(對(duì)于單處理器來說,防止中斷處理中的并發(fā)可簡(jiǎn)單采用關(guān)閉中斷的方式,不需要自旋鎖)。
自旋鎖最多只能被一個(gè)內(nèi)核任務(wù)持有,如果一個(gè)內(nèi)核任務(wù)試圖請(qǐng)求一個(gè)已被爭(zhēng)用(已經(jīng)被持有)的自旋鎖,那么這個(gè)任務(wù)就會(huì)一直進(jìn)行忙循環(huán)——旋轉(zhuǎn)——等待鎖重新可用。
要是鎖未被爭(zhēng)用,請(qǐng)求它的內(nèi)核任務(wù)便能立刻得到它并且繼續(xù)進(jìn)行。自旋鎖可以在任何時(shí)刻防止多于一個(gè)的內(nèi)核任務(wù)同時(shí)進(jìn)入臨界區(qū),因此這種鎖可有效地避免多處理器上并發(fā)運(yùn)行的內(nèi)核任務(wù)競(jìng)爭(zhēng)共享資源。
事實(shí)上,自旋鎖的初衷就是:
在短期間內(nèi)進(jìn)行輕量級(jí)的鎖定。一個(gè)進(jìn)程去獲取被爭(zhēng)用的自旋鎖時(shí),請(qǐng)求它的線程在等待鎖重新可用的期間進(jìn)行自旋(特別浪費(fèi)處理器時(shí)間),所以自旋鎖不應(yīng)該被持有時(shí)間過長(zhǎng)(等待時(shí)CPU被獨(dú)占)。如果需要長(zhǎng)時(shí)間鎖定的話, 最好使用信號(hào)量(睡眠,CPU資源可出讓)?
。
簡(jiǎn)單的說,自旋鎖在內(nèi)核中主要用來防止多處理器中并發(fā)訪問臨界區(qū),防止內(nèi)核搶占造成的競(jìng)爭(zhēng)。另外自旋鎖不允許任務(wù)睡眠(持有自旋鎖的任務(wù)睡眠會(huì)造成自死鎖——因?yàn)樗哂锌赡茉斐沙钟墟i的內(nèi)核任務(wù)被重新調(diào)度,而再次申請(qǐng)自己已持有的鎖),它能夠在中斷上下文使用。
死鎖:假設(shè)有一個(gè)或多個(gè)內(nèi)核任務(wù)和一個(gè)或多個(gè)資源,每個(gè)內(nèi)核都在等待其中的一個(gè)資源,但所有的資源都已經(jīng)被占用了。這便會(huì)發(fā)生所有內(nèi)核任務(wù)都在相互等待,但它們永遠(yuǎn)不會(huì)釋放已經(jīng)占有的資源,于是任何內(nèi)核任務(wù)都無法獲得所需要的資源,無法繼續(xù)運(yùn)行,這便?
意味著死鎖發(fā)生了。自死瑣是說自己占有了某個(gè)資源,然后自己又申請(qǐng)自己已占有的資源,顯然不可能再獲得該資源,因此就自縛手腳了。
spinlock特性:
防止多處理器并發(fā)訪問臨界區(qū),
1、非睡眠(該進(jìn)程/LWP(Light Weight Process)始終處于Running的狀態(tài))
2、忙等 (cpu一直檢測(cè)鎖是否已經(jīng)被其他cpu釋放)
3、短期(低開銷)加鎖
4、適合中斷上下文鎖定
5、多cpu的機(jī)器才有意義(需要等待其他cpu釋放鎖)
以下截取自
http://blog.sina.com.cn/s/blog_458d6ed5010110hv.html
Spinlock的目的是用來同步SMP中會(huì)被多個(gè)CPU同時(shí)存取的變量。在Linux中,普通的spinlock由于不帶額外的語義,是用起來反而要非 常小心。 在Linux kernel中執(zhí)行的代碼大體分normal和interrupt context兩種。tasklet/softirq可以歸為normal因?yàn)樗麄兛梢赃M(jìn)入等待
Spinlock的目的是用來同步SMP中會(huì)被多個(gè)CPU同時(shí)存取的變量。在Linux中,普通的spinlock由于不帶額外的語義,是用起來反而要非常小心。
在Linux kernel中執(zhí)行的代碼大體分normal和interrupt context兩種。tasklet/softirq可以歸為normal因?yàn)樗麄兛梢赃M(jìn)入等待;nested interrupt是interrupt context的一種特殊情況,當(dāng)然也是interrupt context。Normal級(jí)別可以被interrupt搶斷,interrupt會(huì)被另一個(gè)interrupt搶斷,但不會(huì)被normal中斷。各個(gè) interrupt之間沒有優(yōu)先級(jí)關(guān)系,只要有可能,每個(gè)interrupt都會(huì)被其他interrupt中斷。
我們先考慮單CPU的情況。在這樣情況下,不管在什么執(zhí)行級(jí)別,我們只要簡(jiǎn)單地把CPU的中斷關(guān)掉就可以達(dá)到獨(dú)占處理的目的。從這個(gè)角度來說,spinlock的實(shí)現(xiàn)簡(jiǎn)單地令人乍舌:cli/sti。只要這樣,我們就關(guān)閉了preemption帶來的復(fù)雜之門。
單CPU的情況很簡(jiǎn)單,多CPU就不那么簡(jiǎn)單了。單純地關(guān)掉當(dāng)前CPU的中斷并不會(huì)給我們帶來好運(yùn)。當(dāng)我們的代碼存取一個(gè)shared variable時(shí),另一顆CPU隨時(shí)會(huì)把數(shù)據(jù)改得面目全非。我們需要有手段通知它(或它們,你知道我的意思)——spinlock正為此設(shè)。這個(gè)例子是 我們的第一次嘗試:
extern spinlock_t lock; // ... spin_lock(&lock); // do something spin_unlock(&lock);他能正常工作嗎?答案是有可能。在某些情況下,這段代碼可以正常工作,但想一想會(huì)不會(huì)發(fā)生這樣的事:
// in normal run level extern spinlock_t lock; // ... spin_lock(&lock); // do something // interrupted by IRQ ... // in IRQ extern spinlock_t lock; spin_lock(&lock);喔,我們?cè)趎ormal級(jí)別下獲得了一個(gè)spinlock,正當(dāng)我們想做什么的時(shí)候,我們被interrupt打斷了,CPU轉(zhuǎn)而執(zhí)行interrupt level的代碼,它也想獲得這個(gè)lock,于是“死鎖”發(fā)生了!解決方法很簡(jiǎn)單,看看我們第二次嘗試:
extern spinlock_t lock; // ... cli; // disable interrupt on current CPU spin_lock(&lock); // do something spin_unlock(&lock); sti; // enable interrupt on current CPU在獲得spinlock之前,我們先把當(dāng)前CPU的中斷禁止掉,然后獲得一個(gè)lock;在釋放lock之后再把中斷打開。這樣,我們就防止了死鎖。事實(shí)上,Linux提供了一個(gè)更為快捷的方式來實(shí)現(xiàn)這個(gè)功能:
extern spinlock_t lock; // ... spin_lock_irq(&lock); // do something spin_unlock_irq(&lock);如果沒有nested interrupt,所有這一切都很好。加上nested interrupt,我們?cè)賮砜纯催@個(gè)例子:
// code 1 extern spinlock_t lock; // ... spin_lock_irq(&lock); // do something spin_unlock_irq(&lock); // code 2 extern spinlock_t lock; // ... spin_lock_irq(&lock); // do something spin_unlock_irq(&lock);Code 1和code 2都運(yùn)行在interrupt context下,由于中斷可以嵌套執(zhí)行,我們很容易就可以想到這樣的運(yùn)行次序:
Code 1 extern spinlock_t lock; // ... spin_lock_irq(&lock); Code 2 extern spinlock_t lock; // ... spin_lock_irq(&lock); // do something spin_unlock_irq(&lock); Code 1 // do something spin_unlock_irq(&lock);問題是在第一個(gè)spin_unlock_irq后這個(gè)CPU的中斷已經(jīng)被打開,“死鎖”的問題又會(huì)回到我們身邊!
解決方法是我們?cè)诿看侮P(guān)閉中斷前紀(jì)錄當(dāng)前中斷的狀態(tài),然后恢復(fù)它而不是直接把中斷打開。
unsigned long flags; local_irq_save(flags); spin_lock(&lock); // do something spin_unlock(&lock); local_irq_restore(flags);Linux同樣提供了更為簡(jiǎn)便的方式:
unsigned long flags; spin_lock_irqsave(&lock, flags); // do something spin_unlock_irqrestore(&lock, flags);小結(jié)
優(yōu)化方法:
1、假設(shè)我們的QUERY進(jìn)程要查詢多個(gè)分區(qū)(指很多個(gè)分區(qū)),那么建議把分區(qū)的粒度降低,盡量讓QUERY減少真正被訪問的分區(qū)數(shù),從而減少LWLockAcquire次數(shù)。
2、如果我們的分區(qū)很多,但是通過QUERY的WHERE條件過濾后實(shí)際被訪問的分區(qū)不多,那么分區(qū)表的選擇就非常重要。(目前盡量不要使用NATIVE分區(qū))。盡量使用PPAS的edb_enable_pruning。對(duì)于PostgreSQL社區(qū)版本用戶,在社區(qū)優(yōu)化這部分代碼前,請(qǐng)盡量使用pg_pathman分區(qū)功能。
掃描二維碼獲取更多消息:
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)
總結(jié)
以上是生活随笔為你收集整理的PostgreSQL 查询涉及分区表过多导致的性能问题 - 性能诊断与优化(大量BIND, spin lock, SLEEP进程)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Greenplum roaring bi
- 下一篇: DT时代下 数据库灾备的探索与实践