使用數據庫構建高性能隊列用于存儲訂單、通知和任務

jopen 11年前發布 | 49K 次閱讀 數據庫

使用數據庫構建高性能隊列用于存儲訂單、通知和任務

引言

幾乎在每個地方都能用到隊列。在許多web站點里,比如其中的email和SMS都是使用隊列來異步發送通知。電子商務網站都是使用隊列來存儲訂單,處理訂單以及實現訂單的分發。工廠生產線的自動化系統也是使用隊列來按某種順序運行并發工作任務的。隊列是使用很廣泛的一種數據結構,它有時可以創建在數據庫里,而不是使用類似于MSMQ那樣的特定的隊列技術創建。使用數據庫技術來運行一個高性能且高可擴展性的隊列對我們來說是一個巨大的挑戰。當每天進入隊列和從隊列中提取的信息達到數百萬行的時候,這個隊列就很難維護了。我將向你展示在設計類似隊列表時常犯的設計錯誤以及如何使用簡單的數據庫功能實現隊列的最大性能和強大的可擴展性。

首先讓我們明確在隊列表中遇到的挑戰:

  1. 表的讀寫。在大負載下,入隊列和出隊列是相互影響而引起鎖的競爭、事務死鎖、IO超時等等。

    </li>

  2. 當多個接收者試圖從同一隊列讀數據時,它們隨機地獲取重復項,從而導致重復的處理過程。你需要在隊列上實現一些高性能的行鎖以至于并發接受器不會接收相同的數據項。

    </li>

  3. 隊列表需要以特定的順序去存儲行、以特定的順序讀取行數據,這是一個索引設計的挑戰。盡管并不總是先進先出。間或順序有較高的優先級,無論何時入棧都需要進行處理。

    </li>

  4. 隊列表需要以序列化的XML對象或者二進制形式存儲, 這帶來了存儲和索引重建的挑戰。由于數據表包含文本和/或二進制,所以你不能在隊列表上重建索引。因此隊列表每天變的越來越慢,最終查詢開始時間超時,最后你不得不關閉服務并重建索引。

    </li>

  5. 出隊列的過程中,一批行數據被選中、更新,然后重新處理。你需要一個"State"列定義數據項的狀體。出隊列時,你只選擇特定狀態的數據項。現在State只是一個包含PENDING、PROCESSING、PROCESSED、ARCHIVED等元素的集合。結果是你不能在"State"列上創建索引,原因是安全性差。隊列中可以有成千上萬行具有相同的狀態。因此 任何由掃面索引的出隊列操作都會導致CPU和IO資源緊張以及鎖競爭。

    </li>

  6. 在出隊列過程中,你不只是從表中移出行,原因是容易引起存儲殘片。進而,你需要重新訂單/任務/通知N次以防止他們在第一次嘗試中失敗。這意味著行數據需要更長的存儲周期、索引持續增長和出隊列越來越慢。

    </li>

  7. 你不得不歸檔以隊列表中處理過的數據項到不同的表或者數據庫,以保持主隊列表的精簡。這意味著需要移動大量的具有特殊狀態的行到另一個數據庫。大量的數據移動產生了的存儲碎片,而表的高頻度碎片整理降低入棧和出棧的性能。

    </li>

  8. 你需要24X7的工作。你不可能關閉服務而歸檔大量行數據。這意味者在不影響入棧和出棧,你不得不持續的歸檔行數據。

    </li> </ol>

    如果您已實現這樣的隊列表,你可能已經遇到了以上挑戰中的一個或者更多。本文將給你一個關于如何克服這些挑戰的一些技巧,以及如何設計和維護一個高性能的隊列表。

    SQL Server典型隊列

    下面以常見的隊列類型為例,看看在并發負載時的情況.

    CREATE TABLE [dbo].[QueueSlow](
        [QueueID] [int] IDENTITY(1,1) NOT NULL,
        [QueueDateTime] [datetime] NOT NULL,
        [Title] [nvarchar](255) NOT NULL,
        [Status] [int] NOT NULL,
        [TextData] [nvarchar](max) NOT NULL
    ) ON [PRIMARY]
    GO
    CREATE UNIQUE CLUSTERED INDEX [PK_QueueSlow] ON [dbo].[QueueSlow]
    (
        [QueueID] ASC
    )
    GO
    CREATE NONCLUSTERED INDEX [IX_QuerySlow] ON [dbo].[QueueSlow]
    (
        [QueueDateTime] ASC,
        [Status] ASC
    )
    INCLUDE ( [Title])
    GO

    該隊列中使用QueueDateTime排序來模擬先進先出的隊列結構 . QueueDateTime不一定非得是對象加入隊列的時間,而是其將要被處理的開始時間 于是保證了先進先出的次序關系. TextData字段是為了保存進行負載測試的大字段設計的.

    表中使用QueueDateTime 的非聚簇索引以加速隊列操作速度

    首先,插入大約4萬行大約500兆數據量的記錄,其中每行的負載數大小各不相同.

    set nocount on
    declare @counter int
    set @counter = 1
    while @counter < @BatchSize
    begin
        insert into [QueueSlow] (QueueDateTime, Title, Status, TextData)
        select GETDATE(), 'Item no: ' + CONVERT(varchar(10), @counter), 0,
            REPLICATE('X', RAND() * 16000)

        set @counter = @counter + 1 end</pre>

    接下來我們將一次性出隊10個元素。在出隊的時候,它會根據QueueDateTime 和Status = 0進行判斷,它將會Status更新為1表示該元素正在被處理。當出隊的時候我們并不會從緩存表中刪除這行數據,因為我們想保證這些元素在處理失敗的時候永遠不會丟失。

    CREATE procedure [dbo].[DequeueSlow]
    AS

    set nocount on

    declare @BatchSize int set @BatchSize = 10

    declare @Batch table (QueueID int, QueueDateTime datetime, _ Title nvarchar(255), TextData nvarchar(max) )

    begin tran

    insert into @Batch select Top (@BatchSize) QueueID, QueueDateTime, Title, TextData from QueueSlow WITH (UPDLOCK, HOLDLOCK) where Status = 0 order by QueueDateTime ASC

    declare @ItemsToUpdate int set @ItemsToUpdate = @@ROWCOUNT

    update QueueSlow SET Status = 1 WHERE QueueID IN (select QueueID from @Batch) AND Status = 0

    if @@ROWCOUNT = @ItemsToUpdate begin     commit tran     select * from @Batch     print 'SUCCESS' end else begin     rollback tran     print 'FAILED' end</pre>

    上面的查詢將會從QueueSlow 表中取出10行數據,然后存儲在臨時表中。緊接著改變選中的記錄的狀態以保證其他的會話不會再次提取數據。如果能夠更新10行記錄并且沒有被其他的會話占有,那么久可以提交該事務,意味著將會將他們的狀態標記為已完成,沒有子過程進行調用。反之,其他的會話可以更新,事務將會拒絕被提交以保證事務的一致性

    讓我測量下IO性能:

    set statistics IO on
    exec dequeueslow

    輸出如下:

    Table '#3B75D760'. Scan count 0, logical reads 112, physical reads 0, read-ahead reads 0,
        lob logical reads 83, lob physical reads 0, lob read-ahead reads 0.
    Table 'QueueSlow'. Scan count 1, logical reads 651, physical reads 0, read-ahead reads 0,
        lob logical reads 166, lob physical reads 0, lob read-ahead reads 166.
    Table 'QueueSlow'. Scan count 0, logical reads 906, physical reads 0, read-ahead reads 0,
        lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
    Table '#3B75D760'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0,
        lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
    Table '#3B75D760'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0,
        lob logical reads 464, lob physical reads 0, lob read-ahead reads 0.

    概括如下:

    • Total Logical Read = 1695

      </li>

    • Total LOB Logical Read = 675

      </li>

    • LOB Logical Read Count = 3

      </li> </ul>

      我們將用最快的方式進行標記看性能到底提升了多少。這里我們需要注意的是 LOB Logical ReadLOB Logical Read 的訪問次數得到改善。讀三次數據是非常有必要的,因為我們需要重新載入數據。這很清楚的表明SQL Server沒有必要讀取大對象以滿足查詢。

      經常產生出隊和入隊,使得許多的行被移除表進行歸檔,數據表逐漸變成碎片,你不能在線夠重建聚族索引來消除碎片引文它有varchar(max)字段。因此,你不得不選擇停止服務器重建索引,停止服務器是非常耗資源的

      創建一個更快速的隊列

      第一,你必須降低比較過的邏輯讀取。所以你只能將QueueSlow進行拆分為兩張表-QueueMeta ,QueueData。QueueData只包含參與where子句的字段。他是一張很小的表只用于保存查詢。使得SQL Server必須修正幾行數據占據8K的頁然后再運行, 這樣的表會比表QueueSlow運行的更快。

      第二,你能夠在線重建QueueMeta 的索引,當其事務還在進行的時候。這樣的話,QueueMeta表的性能將被不會降低,你再也不用擔心停止服務器進行索引重建了。

      CREATE TABLE [dbo].[QueueMeta](
          [QueueID] [int] IDENTITY(1,1) NOT NULL,
          [QueueDateTime] [datetime] NOT NULL,
          [Title] [nvarchar](255) NOT NULL,
          [Status] [int] NOT NULL,
       CONSTRAINT [PK_Queue] PRIMARY KEY CLUSTERED
      (
          [QueueID] ASC
      )
      GO
      ALTER TABLE [dbo].[QueueMeta] ADD  CONSTRAINT [PK_Queue] PRIMARY KEY CLUSTERED
      (
          [QueueID] ASC
      )
      GO
      CREATE NONCLUSTERED INDEX [IX_QueueDateTime] ON [dbo].[QueueMeta]
      (
          [QueueDateTime] ASC,
          [Status] ASC
      )
      INCLUDE ( [Title])

      這個表保留了出現在搜索查詢中的所有的字段。其他所有和有效負載相關的字段被移到了QueueData表。

      CREATE TABLE [dbo].QueueData NOT NULL
      ) ON [PRIMARY]

      GO CREATE UNIQUE NONCLUSTERED INDEX [IX_QueueData] ON [dbo].[QueueData] (     [QueueID] ASC ) GO</pre>

      在這個表中沒有聚類索引項。“Dequeue”過程先被簡單的修改后去執行QueueMeta表上的查詢操作,然后從QueueData表中選擇有效負載。

      CREATE procedure [dbo].[Dequeue]
      AS

      set nocount on

      declare @BatchSize int set @BatchSize = 10

      declare @Batch table (QueueID int, QueueDateTime datetime, Title nvarchar(255))

      begin tran

      insert into @Batch select Top (@BatchSize) QueueID, QueueDateTime, Title from QueueMeta WITH (UPDLOCK, HOLDLOCK) where Status = 0 order by QueueDateTime ASC

      declare @ItemsToUpdate int set @ItemsToUpdate = @@ROWCOUNT

      update QueueMeta SET Status = 1 WHERE QueueID IN (select QueueID from @Batch) AND Status = 0

      if @@ROWCOUNT = @ItemsToUpdate begin     commit tran     select b.*, q.TextData from @Batch b     inner join QueueData q on q.QueueID = b.QueueID     print 'SUCCESS' end else begin     rollback tran     print 'FAILED' end</pre>

      當把在QueueSlow提取的相同數據填充到QueueMeta和QueueData表中,然后重建兩個表的索引并作對比,分析發現有明顯的提高:

      • Total Logical Read = 1546 (vs 1695)

        </li>

      • Total LOB Read = 380 (vs 675)

        </li>

      • LOB Read Count = 1 (vs 3)

        </li> </ul>

        你會看到邏輯讀的次數低于149;LOB讀取低于295;LOB讀取計數為1,以上數據正式我們期待的。

        負載下的性能比較

        當我模擬并發隊列和出隊列,并且測量性能計數器,結果如下所示:

        </tr> </tbody>

        </tr> </tbody> </table>

        讓我們來分析這些重要的計數器,看看有哪些改進:

        • 頁面分割/秒 – 更快的解決方案有比較低的頁面分割,和較慢的解決方案相比幾乎沒有。這是因為在插入的過程中,有時候一行無法放下而部分填充,因此需要分割成新的一頁。你可以沖這里了解更多有關頁面分割的信息。

          </li>

        • 事務/秒 – 我們從金錢中獲得更多價值。每秒越多的事務,就會有越多的隊列操作隨著出隊列發生,它顯示出了快速隊列操作的性能相比于慢的是更好的選項。

          </li>

        • 鎖超時/秒 - 這說明有多少個查詢在等待某個對象上的鎖,并最終放棄了,因為它沒有及時得到該鎖。該值越高,表示數據庫讀取性能越差。你要盡量保持該值接近零。上述結果表明鎖定超時在快速隊列的數目小于緩慢隊列。

          </li>

        • 批量請求/秒 - 顯示每秒執行SELECT查詢多少次。它顯示了這兩種解決方案都執行的相同次數的SELECT操作,由于更快的解決方案是從多個表讀取的。所以,與較慢的解解方案相比,出隊列的存儲過程沒有顯著優化。

          </li> </ul>

          這不僅僅是有更好的性能,最大的好處是你可以在QueueMeta表在線運行INDEX DEFRAG,從而阻止Queue逐漸放緩。

          在SQL Server 2005,2008里實現的最快隊列

          SQL Server 2005給UPDATEINSERTDELETE語句引入了OUTPUT子句。這么做就可以讓你通過一個查詢就可以得到由insert,updatedelete更改的哪些數據行。你不需要先查詢一些數據行,然后對它們進行加鎖,接著再更新這些數據行,最后才返回這些數據行。你只需一條語句就可以完成這些功能-更新并返回這些數據行。

          下面從隊列獲取信息的過程是修改過的:

          alter procedure [dbo].[DequeueFast]
          AS

          set nocount on

          declare @BatchSize int set @BatchSize = 100

          update top(@BatchSize) QueueMeta WITH (UPDLOCK, READPAST) SET Status = 1 OUTPUT inserted.QueueID, inserted.QueueDateTime, inserted.Title, qd.TextData FROM QueueMeta qm INNER JOIN QueueData qd     ON qm.QueueID = qd.QueueID WHERE Status = 0</pre>

          一行UPDATE語句就可以做到我們到現在為止看到的Dequeue存儲過程所做的一切。同時IO統計狀態也得到了很大的改善:

          Table 'QueueMeta'. Scan count 1, logical reads 522, physical reads 0,
            read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
          Table 'QueueData'. Scan count 0, logical reads 31, physical reads 0,
            read-ahead reads 0, lob logical reads 56, lob physical reads 0, lob read-ahead reads 0.
        緩慢隊列 快速隊列
        使用數據庫構建高性能隊列用于存儲訂單、通知和任務 使用數據庫構建高性能隊列用于存儲訂單、通知和任務
sesese色