在 postgres 與單個表中使用分區是否存在記憶體成本?
背景
我每天攝取總計約 1000 萬行的時間序列數據,其中每一行都有一個時間戳、一個玩家 ID 和一些其他列。
然後通過 API 查詢特定播放器和時間範圍的數據(例如,播放器的最後 90 天
xxx
),大多數情況需要超過 1 個並發請求(通常執行大約 10 個並發請求)我正在執行 postgres 9.6.17,機器有 500GB 硬碟空間(始終有大約 15% 的可用空間)、8 個核心和 16GB 記憶體。
work_mem
設置為 2GB、cache_size
12GB、最大連接數設置為 100 等。API 是 20 個 python gunicorn 工作者在單獨的機器上執行 flask 和 sqlalchemy+psycopg2。每個工作人員都有一個到數據庫的 2 個連接池,允許溢出 5 個。池設置曾經更高,但事實證明使用池幾乎沒有任何好處,因此數量很少。
天真的方法
最初,我將所有數據放入一個表中,並在時間戳和播放器上建立索引。這種方法執行良好,直到數據量開始使其變得越來越慢(出於明顯的原因)。這導致 API 工作人員超時並返回 500。返回的典型查詢(超過 6 個月的單人數據)的成本
explain
約為 1M,範例如下Bitmap Heap Scan on player_data (cost=515553.98..585514.47 rows=80037 width=32) Recheck Cond: (((player_id)::text = 'xxx'::text) AND (ts >= 1572566400) AND (ts < 1574899199)) -> BitmapAnd (cost=515553.98..515553.98 rows=62819 width=0) -> Bitmap Index Scan on idx_player_id (cost=0.00..12749.35 rows=962837 width=0) Index Cond: ((player_id)::text = 'xxx'::text) -> Bitmap Index Scan on idx_ts (cost=0.00..502778.48 rows=37691480 width=0) Index Cond: ((ts >= 1572566400) AND (ts < 1574899199))
更好的分區方法
作為一項改進,我開始將數據儲存在分區中,每天一個,然後在每個分區上創建玩家 ID 和時間戳索引。這顯著減少了查詢時間,改進也可見
explain
:Append (cost=0.00..85192.02 rows=80037 width=32) -> Seq Scan on player_data (cost=0.00..0.00 rows=1 width=85) Filter: ((ts >= 1572566400) AND (ts < 1574899199) AND ((player_id)::text = 'xxx'::text) -> Index Scan using idx_player_id_20191104 on player_data_20191104 (cost=0.43..317.61 rows=280 width=32) Index Cond: ((player_id)::text = 'xxx'::text) Filter: ((ts >= 1572566400) AND (ts < 1574899199) ........<continued for a while>................
成本幾乎降低了一個數量級,並且一切進展順利,尤其是對於時間粒度較小的查詢。
問題
一段時間後,使用者開始抱怨查詢不起作用,這意味著 API 開始返回 500 錯誤。檢查日誌後,我注意到很多以下錯誤:
sqlalchemy.exc.OperationalError: (psycopg2.errors.OutOfMemory) out of memory DETAIL: Failed on request of size NNNN.
請注意,
NNNN
它幾乎可以是 1 到 10000 之間的任何整數。一旦出現錯誤,它就會發生在所有同時執行的查詢上,從而導致數據庫在一段時間(幾分鐘)內無響應。通常,重新啟動 API 服務會恢復正常。當數據庫無響應時,任何類型的查詢和訪問都會返回相同的錯誤,包括psql
來自我的機器的連接嘗試等。我還應該注意並行執行的查詢大多命中相同的索引和分區,例如在同一時間範圍內的不同玩家 ID,有時查詢是相同的,除了偏移量/限制值。
另一個注意事項 - 無論是通過 API 還是直接出現並發,都會出現相同的錯誤
psql
。調試和診斷
我在查詢時檢查了數據庫機器上的資源(因為它應該耗盡記憶體):
- 可用 RAM 永遠不會低於 60%
- 沒有使用交換,沒有任何東西被寫入磁碟(儘管如果 postgres 用完可用的記憶體,它應該)
- 測試期間與數據庫的並發連接數最高為 100,空閒為 40
- 單分區索引大小約5MB,老表的索引大小幾乎1GB
另外,我嘗試了對數據庫的以下更改:
- 從 20MB增加到
work_mem
2GB,沒有改善- 將記憶體從 8GB 增加到 12GB,最後增加到 16GB,沒有改善
我還比較了舊查詢和新查詢,結果如下:
- 超過 10 個對分區表的並發查詢導致錯誤出現
- 對舊的單表的 200 個並發查詢不會導致任何問題
問題)
所以我猜的問題是:
- 分區的使用是否會產生一些記憶體成本,導致查詢由於記憶體分配失敗而失敗?
- 是索引嗎?使用多個索引是否比使用一個大索引更糟糕?
- 這個理論是否有意義,還是我錯過了一些明顯的東西?
因為有點道理,所以回答我自己的問題:
- 沒有 9.6 的配置使問題消失
- PSQL12 中也出現了同樣的問題,儘管分區確實是本地的
- PSQL12 的配置沒有任何區別
所以最後,為了讓系統保持工作狀態,我選擇了(多)更少的分區和更長的查詢時間。
後來我遷移到自定義編碼和聚合解決方案。
(通常執行大約 10 個並發請求)
… 16GB 記憶體。work_mem 設置為 2GB,
你的 work_mem 設置對我來說似乎很瘋狂。有 10 個並發查詢,每個查詢都可能使用多個 work_mem 實例(在分區中尤其常見——所以回答你的標題問題,是的),記憶體不足也就不足為奇了。
我在查詢時檢查了數據庫機器上的資源(因為它應該耗盡記憶體):
你是怎麼做的?來自不同工具的數據需要以不同的方式解釋。
另外,我嘗試了對數據庫的以下更改:
將 work_mem 從 20MB 增加到 2GB,沒有改善
是的,增加您使用的記憶體量不太可能解決記憶體不足問題。“毆打將繼續,直到士氣提高。”
但是您是說當 work_mem 設置為僅 20MB 時您遇到了完全相同的問題?您是否查看了數據庫日誌文件以直接查看它對問題的描述(而不是 python 傳遞給您的關於它的內容?)
根據您的描述,聽起來您可能根本不需要分區。只需一個多列索引就
(player_id, ts)
可以解決您的問題,而無需承擔分區的負擔和成本。您是否計劃刪除/存檔舊數據,或者將無限期地累積?