深入理解Postgres中的cache


眾所周知,緩存是提高數據庫性能的一個重要手段。本文着重講一講PostgreSQL中的緩存相關的東西。當然萬變不離其宗,原理都是共同的,理解了這些,你也很容易把它運用到其它數據庫中。


What is a cache and why do we need one

不同的計算機組件運行的速度是不一樣的,他們的差距很大,一般都是數量級級別的差距。比如速度上磁盤<RAM<system cache(如下圖)。在數據量小的時候你可能覺察不出差異,但是尤其在現在這個大數據的時代,你很輕易就能感知他們的差異,比如我們都知道SSD比普通磁盤要快。

在數據庫系統內部數據傳輸時(這里我們不討論網絡延遲這種),我們討論數據傳輸瓶頸一般認為的就是磁盤I/O,因此我們這里也主要把關注點放在這里。

我們知道,大多數OLTP工作負載是隨機的I/O,但是從磁盤獲取非常緩慢。為了克服這個問題,和其它現有的數據庫系統差不多,Postgres也把數據緩存到RAM(也就是我們說的內存)以提高性能。

因此,本文提到的緩存主要是軟件意義上的緩存,不同於硬件意義上的CPU緩存,硬盤緩存這樣的實體,主要是指軟件系統在內存中開辟的用以提高數據復用等目的的一塊內存區域,由軟件自身通過特定算法實現調度。


Understanding terminologies

在我們繼續說下去之前,我們需要一些基礎知識。因此我推薦閱讀這些:

PostgreSQL physical storage
inter db

以及官方手冊:http://www.postgres.cn/docs/9.5/storage.html

以上文檔我推薦你依次閱讀。當然,上面的文檔你不完全懂或者只知道一點也沒關系,我會試着講的簡單一點。
或者,你試着記住下面這張圖?它大致描述了Postgres的數據庫存儲的抽象層,也就是我們經常說的page(大小為8KB)。我們后面會用到的。


What is cached?

問的很好。我們把那些東西緩存到RAM中?
Postgres緩存了這些。

Table data

也就是表的內容。

Indexes

索引也是以page存儲的,它們和table一樣被緩存到RAM中,在下面我們還會提到他。

Query execution plans

我前面的一些文章提到過一條Query送到數據庫中,會經過查詢分析,查詢重寫,查詢規划和查詢執行這幾個階段才能獲得最后的輸出。其中查詢規划階段是針對所查詢SQL選擇出一條最優的查詢計划出來。
是的,這里Postgres也把這個查詢計划緩存了下來。這個查詢計划一般是在優化或者analyze時會用到。當然,除非我么遇到那種特別復雜或者重復的查詢,否則我們一般不用太關注它。

如果你對這個有興趣,我建議你看看這個:http://www.postgres.cn/docs/9.5/sql-prepare.html

當然你也可以查詢pg_prepared_statements視圖來看看當前session緩存的查詢計划(查詢計划只在當前session有效,在當前session結束時被釋放)。

所以你們知道,下面我們主要從討論table和index在內存中的緩存機制。


Memory areas

Postgres服務器內部有很多配置參數來幫助你優化服務器的各個方面,因此理解他們的用法和用途就顯得尤為重要了。

那么對於緩存,最重要的參數莫過於shared_buffers了。

在源碼中,我們叫它NBuffers,所有Postgres的共享數據都存在這里。

shared_buffers所代表的內存區域可以看成是一個以8KB的block為單位的數組,即最小的分配單位是8KB。沒錯,這正好是一個page的大小。每個page以page內部的元數據(Page Header)互相區分。

這樣,當Postgres想要從disk獲取(主要是table和index)數據(page)時,他會(根據page的元數據)先搜索shared_buffers,確認該page是否在shared_buffers中,如果存在,則直接命中,返回緩存的數據以避免I/O。
如果不存在,Postgres才會通過I/O訪問disk獲取數據(顯然要比從shared_buffers中獲取慢得多)。


The LRU/Clock sweep cache algorithm

提到緩存以及緩存數據的讀入和逐出,我們很自然的想到操作系統課上我們學習到的內存置換算法,其中最有代表性的恐怕就是LRU算法了。
我們Postgres的緩存也是類似,但是是采用的更高級的LRU時鍾掃描策略,由於shared_buffers專門用於處理OLTP工作負載,因此幾乎所有的數據的移動都在內存中處理。
好的,我們細細的講一講。

緩沖區的分配

我們知道Postgres是基於進程工作的系統,即對於每一個服務器的connection,Postgres主進程都會向操作系統fork一個新的子進程(backend)去提供服務。同時,Postgres本身除了主進程之外也會起一些輔助的進程。

因此,對於每一個connection的數據請求,對應的后端進程(backend)都會首先向LRU cache中請求數據頁page(這個數據請求不一定指的是SQL直接查詢的表或者視圖的page,比如index和系統表),這個時候就發起了一次緩沖區的分配請求。
那么,這個時候我們就要抉擇了。如果要請求的block就在cache中,那最好,我們"pinned"這個block,並且返回cache中的數據。所謂的"pinned"我暫時找不到一個好的詞來代替他,它指的是增加這個block的"usage count"(這個時候你可以翻翻LRU算法了)。
當"usage count"為0時,我們就認為這個block沒用了,在后面cache滿的時候,它就該挪挪窩了。

那也就是說,只有當buffers/slots已滿的情況下,才會引發緩存區的換出操作。

緩存區的換出

決定哪個page該從內存中換出並寫回到disk中,這是一個經典的計算機科學的問題。

一個最簡單的LRU算法在實際情況下基本上很難work起來。因為LRU是要把最近最少使用的page換出去,但是我們沒有記錄上次運行時的狀態。

因此,作為一個折中和替代,我們追蹤並記錄每個page的"usage count",在有需要時,將那些"usage count"為0的page換出並寫回到disk中。后面也會提到,臟頁面也會被寫回disk。

不考慮細節問題的話,高速緩存算法本身幾乎不需要調整就可以用在Postgres的緩存策略中,而且比人們通常所想的要智能得多。


Dirty pages and cache invalidation

我們一直在說的是查詢語句(SELECT),那么對於DML語句情況又有什么不一樣呢?

很簡單,他們將數據也寫回到相同的page。也就是說,如果該數據所在的頁面就在內存的cache中,那就把數據寫到cache中的這個page里,如果不在內存,那么就把數據所在的page從disk讀取到cache中,然后再寫到cache的page里。

這里就是臟頁面的由來:page里面的內容被修改了,但是卻沒有寫回disk。

說到寫回的概念,我們又要引出另外的概念了:WALcheckpoints

WAL叫預寫日志,它追蹤並記錄數據庫系統中所發生的一切動作。中心概念是數據文件(存儲着表和索引)的修改必須在這些動作被日志記錄之后才被寫入,即在描述這些改變的日志記錄被刷到持久存儲以后。如果我們遵循這種過程,我們不需要在每個事務提交時刷寫數據頁面到磁盤,因為我們知道在發生崩潰時可以使用日志來恢復數據庫:任何還沒有被應用到數據頁面的改變可以根據其日志記錄重做(這是前滾恢復,也被稱為REDO)。

而Checkpointer進程呢?這個周期有系統參數可以配置。檢查點是在事務序列中的點,這種點保證被更新的堆和索引數據文件的所有信息在該檢查點之前已被寫入。在檢查點時刻,所有臟數據頁被刷寫到磁盤,並且一個特殊的檢查點記錄將被寫入到日志文件(修改記錄之前已經被刷寫到WAL文件)。簡而言之,它定期的將臟頁面寫回到disk。

有了這兩個進程,就可以保證數據庫很快地從崩潰中恢復過來,而不用手忙腳亂地回放崩潰之前的所有操作。

這是頁面從內存中被換出的最常見的方式,在典型的情況下,LRU換出幾乎不會發生。


Understanding caches from explain analyze

現在讓我們再回到數據庫本身,從一個用戶的角度去理解這個cache。這個時候,explain命令是我們的好幫手,它可以幫助我們了解很多有關cache的細節信息。

例如我們有一個如下的查詢:

performance_test=# explain (analyze,buffers) select * from users order by userid limit 10;
    
  Limit  (cost=0.42..1.93 rows=10 width=219) (actual time=32.099..81.529 rows=10 loops=1)
    Buffers: shared read=13
    ->  Index Scan using users_userid_idx on users  (cost=0.42..150979.46 rows=1000000 width=219) (actual time=32.096..81.513 rows=10 loops=1)
          Buffers: shared read=13
  Planning time: 0.153 ms
  Execution time: 81.575 ms
 (6 rows)

這里的"Shared read"代表數據來自disk而並非cache。但是,如果我們再次運行這個查詢,我們就會發現它變成了"shared hit",也就是說,它已經在cache中了。

performance_test=# explain (analyze,buffers) select * from users order by userid limit 10;
    
  Limit  (cost=0.42..1.93 rows=10 width=219) (actual time=0.030..0.052 rows=10 loops=1)
    Buffers: shared hit=13
    ->  Index Scan using users_userid_idx on users  (cost=0.42..150979.46 rows=1000000 width=219) (actual time=0.028..0.044 rows=10 loops=1)
          Buffers: shared hit=13
  Planning time: 0.117 ms
  Execution time: 0.085 ms
 (6 rows)

通過這種方式可以非常方便地了解從查詢角度了解有多少數據是被緩存的,而不是通過OS/Postgres的一些復雜的觀測方法。

我們舉一個有關順序掃描的例子:當前查詢的表上沒有索引,而且postgres必須從磁盤提取所有數據。

由於單個seq掃描可以擦除緩存中的所有數據,因此處理方式和我們預想的不太一樣。它並不使用LRU/時鍾掃描算法,而是直接分配了總共256KB(32*8KB)的緩沖區。
下面的查詢計划顯示了它是如何處理的。

performance_test=# explain (analyze,buffers) select count(*) from users;
   
  Aggregate  (cost=48214.95..48214.96 rows=1 width=0) (actual time=3874.445..3874.445 rows=1 loops=1)
    Buffers: shared read=35715
    ->  Seq Scan on users  (cost=0.00..45714.96 rows=999996 width=0) (actual time=6.024..3526.606 rows=1000000 loops=1)
          Buffers: shared read=35715
  Planning time: 0.114 ms
  Execution time: 3874.509 ms

再次執行該查詢:

performance_test=# explain (analyze,buffers) select count(*) from users;
    
  Aggregate  (cost=48214.95..48214.96 rows=1 width=0) (actual time=426.385..426.385 rows=1 loops=1)
    Buffers: shared hit=32 read=35683
    ->  Seq Scan on users  (cost=0.00..45714.96 rows=999996 width=0) (actual time=0.036..285.363 rows=1000000 loops=1)
          Buffers: shared hit=32 read=35683
  Planning time: 0.048 ms
  Execution time: 426.431 ms

我們可以看到總共32blocks(32 * 8 = 256 KB)被移動到了內存中。理由? 我們可以看看這里: src/backend/storage/buffer/README


Memory flow and OS caching

Postgres作為一個跨平台的數據庫,它的緩存機制在很大程度上依賴於它所安裝的操作系統。

shared_buffers實際上是復制了操作系統的功能。下面給出了數據如何流經postgres的典型圖片。

這是令人困惑的,因為緩存居然是由操作系統和postgres共同管理的,我們知道這是有原因的。但是要談操作系統緩存的話篇幅就很大了,需要另開一篇文章來講了,但網上有很多資源可以利用。
但是請記住,操作系統緩存數據的原因與我們上面看到的相同,即為什么我們需要緩存?
我們可以將I/O分為兩類,即讀和寫。 更簡單的說,一個是數據從磁盤流向內存,另一個是數據從內存流向磁盤。

Reads

對於讀來說,根據上面的流程圖,數據從磁盤流向OS緩存,然后流向shared_buffers。 我們已經討論過這些頁面如何被"pinned"在shared_buffers上,直到它們被弄臟/解除固定。

有時,OS緩存和shared_buffers可以保存相同的頁面。你馬上意識到這可能會導致空間浪費,但請記住操作系統緩存使用簡單的LRU,而不是數據庫優化的時鍾掃描。 一旦這些頁面在shared_buffers上命中,就不會再去讀達OS緩存,並且如果有任何重復,它們也能被輕松移除。

而且實際上,這兩個內存區域上重復的頁面並不多。因此,我們不必擔心空間的浪費。相反,我們建議多花點時間仔細調整shared_buffers。 因為無論shared_buffers過大還是過小都會影響數據庫的性能。。

我們將在下面討論更多的優化方法。

Writes

而對於寫,數據從內存流寫入磁盤。 這是臟頁面的概念的由來。

一旦頁面被標記為臟,它將被刷新到操作系統緩存,然后寫入磁盤。 這個時候操作系統有更多的自由來根據寫入流量安排I/O。

如上所述,如果OS緩存大小過小,則不能重新排序寫入和優化I/O。 這對寫入較多的任務的影響尤其重要。 所以OS緩存的大小也很重要。

Initial configuration

與其它許多數據庫系統一樣,在參數的配置上沒有"銀彈"。 因此,PostgreSQL提供了一個廣泛兼容性而不是性能調優的基本配置。

數據庫管理員有責任根據應用和工作負載調整配置。 但是,好在postgres的有一個很好的幫助文檔。

一旦設置好了配置,我們就可以做負載/性能測試,來看看它是如何運行。因此最好始終進行實驗來獲得更適合的配置。


Optimize as you go

如果你不能量化一些東西的,那你不能很好地優化它。
對於Postgres,我們可以從兩個方向來觀察。

Operating system

對於postgres最適合哪個平台並沒有普遍的一致意見,我假定您正在使用Linux系列的操作系統。把數據庫放在Linux類系統上應該是很常見的做法。

在Linux系統上有一個名為iotop的工具,可以查看磁盤I/O。 與top類似,你只需運行命令iotop來觀察寫入/讀取。

它可以提供有用的數據,了解postgres在負載下的行為方式,即根據產生的負載,有多少數據來自內存和磁盤。

Directly from postgres

我們覺得直接從postgres監視某些東西總是比較好,而不是間接通過操作系統來觀察。除非我們認為postgres本身有問題時,我們才會進行操作系統級別的監控,但這種情況很少。
在postgres內部,我們可以使用好幾種工具來測量內存的性能。

#EXPLAIN

首先是默認是SQL EXPLAIN。它可以比任何其他數據庫系統獲得更多的信息,雖然有點難於理解,但是看看手冊學習下你就能大致看懂。 另外不要錯過這幾個有用的標志(COSTS,BUFFERS,TIMING這些),特別是我們之前看到的緩沖區。
More about explain on postgresguide.com
Explain visualizer

#Query logs

查詢日志是了解系統內部情況的另一種方法。

我們可以設置log_min_duration_statement參數來記錄查詢時間超過指定閾值的查詢(慢查詢),而不是記錄所有內容。

#auto_explain

auto_explain模塊提供了一種方式來自動記錄慢速語句的執行計划,而不需要手工運行EXPLAIN。這在大型應用中追蹤未被優化的查詢時有用。

#pg_stat_statements

pg_stat_statements模塊提供一種方法追蹤一個服務器所執行的所有 SQL 語句的執行統計信息。
該模塊必須通過在postgresql.conf的shared_preload_libraries 中增加pg_stat_statements來載入,因為它需要額外的共享內存。 這意味着增加或移除該模塊需要一次服務器重啟。

這種方法的缺點是會有一定的性能損失,所以在生產系統中通常不推薦使用。

#pg_buffer_cache

pg_buffercache模塊提供了一種方法實時檢查共享緩沖區。

該模塊提供了一個C函數pg_buffercache_pages,它返回一個記錄的集合, 外加一個包裝了該函數以便於使用的視圖pg_buffercache。

#pg_prewarm

pg_prewarm模塊提供一種方便的方法把關系數據載入到操作系統緩沖區緩存或者 PostgreSQL緩沖區緩存
如果你認為"memory warm up"存在問題,那么這對調試非常有用。


Summary

寫這篇之前,參考了很多別人的文章,整理了一下。這里就是本人對於POstgres的Cache的一點粗淺理解,可能寫的比較簡略,語言可能組織的也不好。還希望各位大神不吝賜教。希望能夠遇到更多志同道合的朋友們。

本篇完~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM