使用db-scheduler實現高效能持久佇列
由於效率低下和可擴充套件性的限制,使用資料庫作為佇列歷來被認為是一種反模式,但另一方面,不將資料分佈在多個資料儲存上也有巨大的好處。在這篇博文中,我將討論利弊,探討人們對現代資料庫的預期限制以及哪些最佳化使這些成為可能。
db-scheduler 是幾年前開發的一個簡單的 Java 永續性作業排程程式,旨在嘗試在簡單性、資料庫佔用空間和功能集之間找到比我當時認為 Quartz 所做的更好的平衡。主要用例是在叢集中執行重複任務,但新增了臨時/一次性任務,因為它感覺像是一種自然的擴充套件。
事實證明,人們發現它很有用,有些人甚至開始將它用作後臺作業的持久佇列。文件中指出,它主要不適用於高吞吐量,因為它的輪詢機制的開銷會隨著排程程式例項的數量而增加,此外,它的吞吐量從未真正經過測試。儘管如此,還是達到了 1000+ 次執行/秒的吞吐量(但在資料庫負載方面有一些成本)。這比預期的要高一些,並且確實驗證了用例。
在意識到一種有效的替代輪詢機制,即使用PostgreSQL的SKIP LOCKED之後,很自然地將其納入到db-scheduler中,透過減少每次執行的開銷來提高其作為持久化佇列的可行性。但是為了知道它是否有效果,需要對它進行測試。
使用資料庫作為佇列
首先,假設您已經有一個資料庫,當有專門為此目的設計的產品並且每個公共雲都有這樣的託管產品時,為什麼要將它用作佇列?好吧,大部分好處將來自這樣一個事實,即您在資料儲存中分佈的資料越多,複雜性增加的就越多。一些,從良好的舊關聯式資料庫非常熟悉的事實來看。
- 原子性和一致性。佇列操作可以與其他資料庫操作參與相同的事務。
- 簡單易懂。_ 如果佇列儲存為資料庫表,開發人員可以使用熟悉的 SQL 與其互動,並且圍繞其行為進行推理的障礙可能會低很多。
- 降低運營成本。需要了解和管理的基礎架構元件少了一個,即設定、監控、備份、遷移(到時候)等。
另一方面,最大的缺點是在佇列吞吐量達到一定水平時,資料庫將開始掙扎。
- 它比專用佇列效率低,如果大量使用,它會增加您可能用於其他事情的資料庫的負載。
- 它使用輪詢來獲取新事件。資料庫需要輪詢新事件,並且您希望事情發生得越快,輪詢需要的頻率就越高,這反過來又會增加資料庫的負載。
- 如果您達到資料庫的限制,則可能更難擴充套件。
因此,這是一種權衡,需要根據具體情況進行評估,同時考慮預期的數量、增長以及資料庫的其他用途。但是,如果您已經有一個資料庫,那麼將其用作佇列是一個低成本的簡單起點。
預設輪詢策略
那麼,讓我們來看看輪詢策略。預設情況下,在 db-scheduler 中輪詢和執行一次性任務的工作方式如下:
- 選擇一批到期的執行
- 對於每次執行
- 嘗試更新執行以選擇(使用樂觀鎖定)
- 如果更新 ok=> 執行並從資料庫中刪除執行
- 如果更新失敗,即被另一個例項選擇,=> 跳過
由於使用了樂觀鎖定,因此幾乎沒有鎖爭用,但假設 X 數量的競爭排程程式和 B 批大小,則此策略每批的 sql 語句/事務的理論最壞情況數為:
X selects + 2B updates (pick+delete) + (X-1)B updates (missed picks) |
所以對於B=50,X=4,最壞情況下的語句數量是:4個選擇+100個更新+150個更新,總共254條語句。沒有資料庫鎖,但每次執行的開銷相當大。
最佳化的輪詢策略
除了上述的輪詢和鎖定策略外,還有一種避免漏選開銷的方法是使用SELECT FOR UPDATE暫時鎖定資料庫中的候選行。然而,這些行鎖將迫使來自競爭性排程器的選擇在能夠繼續之前等待鎖被釋放,從而降低了吞吐量。
然而,近年來,幾乎所有的普通資料庫都支援SKIP LOCKED,這是一種指示資料庫跳過已經有行鎖的行的方法,也就是消除瓶頸。PostgreSQL走得最遠,它支援在一條語句中選擇、更新和返回行。
UPDATE scheduled_tasks st1 SET picked = true, … WHERE <instance> IN ( SELECT <instance> FROM scheduled_tasks st2 WHERE <due-condition> FOR UPDATE SKIP LOCKED LIMIT <limit>) RETURNING st1.* |
使用這樣的輪詢策略,每批的理論上的sql-statements/transactions的數量是:
1 select-and-update (pick) + B updates (delete) |
公平地說,選擇和更新語句是非常沉重的,並且在引擎蓋下做B的更新,但這裡的顯著優勢是
- 更少的語句意味著更少的應用程式→資料庫的往返次數
- 更少的事務和更少的提交
- 沒有因漏選造成的語句浪費
測試一下
為了瞭解新的輪詢程式碼有什麼效果(如果有的話),在 GCP 上設定了一個簡單的測試。4 個競爭排程程式和〜無限的排程執行。
眼鏡
- 4 個執行競爭排程程式的 VM(2 核)
- Postgres v12,4 核,25gb 記憶體
配置
- 每個排程程式例項 100 個執行緒
- 2.0 下限,即當queue-size < 2.0 * <nr-threads>時開始獲取新的執行
- 6.0 上限,即每次取數,嘗試將佇列填充到queue-size = 6.0 * <nr-threads>
透過使用合成資料填充資料庫、啟動 4 個排程程式並觀察吞吐量來執行測試。它顯示新的輪詢策略顯著增加了 11000 次執行/秒,而預設策略達到了約 2500 次執行/秒的吞吐量。所以對於這個特定的設定,增加了 4 倍
總結
如上所示,使用像 PostgreSQL 這樣的現代資料庫,SKIP LOCKED在將資料庫用作佇列時(例如後臺作業),可以達到 10.000 次執行/秒的吞吐量。這是相當高的,並且確實驗證了這種用途。它向我們表明,我們可以從簡單開始,推遲引入額外基礎設施元件的額外複雜性,直到需要,這在軟體開發中始終是一個好主意。
如果您有興趣探索此選項,請檢視為 Java 應用程式提供此選項的db-scheduler原始碼,以及重複任務等。
相關文章
- .NET 高效能緩衝佇列實現 BufferQueue佇列
- 使用 RabbitMQ 實現延時佇列MQ佇列
- 使用LinkedList實現安全佇列佇列
- 使用陣列實現環形佇列Scala版本陣列佇列
- 高效能佇列——Disruptor佇列
- 通過佇列實現棧OR通過棧實現佇列佇列
- GCD之佇列的實現和使用GC佇列
- 使用佇列實現楊輝三角佇列
- 使用C#實現順序佇列C#佇列
- 使用RabbitMq原生實現延遲佇列MQ佇列
- javascript實現佇列JavaScript佇列
- C 語言實現使用靜態陣列實現迴圈佇列陣列佇列
- C 語言實現使用動態陣列實現迴圈佇列陣列佇列
- 佇列的一種實現:迴圈佇列佇列
- 高效能佇列設計佇列
- ArrayDeque雙端佇列 使用&實現原理分析佇列
- 使用Spring Boot實現訊息佇列Spring Boot佇列
- 9. 題目:對佇列實現棧&用棧實現佇列佇列
- 佇列(Queue)-c實現佇列
- 用 Rust 實現佇列Rust佇列
- 用佇列實現棧佇列
- 用棧實現佇列佇列
- Python佇列的三種佇列實現方法Python佇列
- Go中使用Redis實現訊息佇列教程GoRedis佇列
- 鏈式佇列—用連結串列來實現佇列佇列
- Redis實現任務佇列、優先順序佇列Redis佇列
- Redis使用ZSET實現訊息佇列使用總結二Redis佇列
- Redis使用ZSET實現訊息佇列使用總結一Redis佇列
- RabbitMQ實現延遲佇列MQ佇列
- RabbitMQ 實現延遲佇列MQ佇列
- Redis實現訊息佇列Redis佇列
- 兩個棧實現佇列佇列
- 【資料結構】佇列(順序佇列、鏈佇列)的JAVA程式碼實現資料結構佇列Java
- 佇列 優先順序佇列 python 程式碼實現佇列Python
- 效能優化-使用雙buffer實現無鎖佇列優化佇列
- Laravel5.6使用redis佇列實現系統通知LaravelRedis佇列
- linux核心--使用核心佇列實現ringbufferLinux佇列
- 靜態佇列,迴圈陣列實現佇列陣列