【问题标题】:How to speed up Service Broker with many jobs on the queue?如何通过队列中的许多作业加快 Service Broker?
【发布时间】:2014-08-01 05:09:15
【问题描述】:

我使用带有内部激活的 SQL Service Broker 将作业列表移动到内部激活的存储过程以完成,而无需让主线程/请求者等待实际的单个作业完成。本质上,我正在尝试释放 UI 线程。问题是,我将 2000 多个作业传递给服务代理,消息在大约 25 分钟内到达队列并释放 UI,但是即使一个小时后,它也只完成了近 600 多个作业 我使用下面的查询来计算等待完成的数量,它看起来非常慢

SELECT COUNT(*)
FROM [HMS_Test].[dbo].[HMSTargetQueueIntAct] 
WITH(NOLOCK)

下面是我为您的参考激活存储过程。有人可以看看,让我知道这有什么问题吗?如何让 SB 快速完成队列中的这些项目?在此先感谢:)

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[sp_SB_HMSTargetActivProc]
AS
BEGIN
DECLARE @RecvReqDlgHandle UNIQUEIDENTIFIER;
DECLARE @RecvReqMsg NVARCHAR(1000);
DECLARE @RecvReqMsgName sysname;

DECLARE @XMLPtr int
DECLARE @ExecuteSQL nvarchar(1000)
DECLARE @CallBackSP nvarchar(100)
DECLARE @CallBackSQL nvarchar(1000)
DECLARE @SBCaller nvarchar(50)
DECLARE @LogMsg nvarchar(1000)

WHILE (1=1)
BEGIN
    BEGIN TRANSACTION;
    WAITFOR
    ( RECEIVE TOP(1)
        @RecvReqDlgHandle = conversation_handle,
        @RecvReqMsg = message_body,
        @RecvReqMsgName = message_type_name
      FROM HMSTargetQueueIntAct
    ), TIMEOUT 5000;

    IF (@@ROWCOUNT = 0)
    BEGIN
      ROLLBACK TRANSACTION;
      BREAK;
    END

    IF @RecvReqMsgName = N'//HMS/InternalAct/RequestMessage'
    BEGIN
       DECLARE @ReplyMsg NVARCHAR(100);
       SELECT @ReplyMsg = N'<ReplyMsg>ACK Message for Initiator service.</ReplyMsg>';
       SEND ON CONVERSATION @RecvReqDlgHandle
              MESSAGE TYPE 
              [//HMS/InternalAct/ReplyMessage]
              (@ReplyMsg);

        EXECUTE sp_xml_preparedocument @XMLPtr OUTPUT, @RecvReqMsg
        SELECT @ExecuteSQL = ExecuteSQL
              ,@CallBackSP = CallBackSP
              ,@SBCaller = SBCaller
        FROM OPENXML(@XMLPtr, 'RequestMsg/CommandParameters', 1)
        WITH    (ExecuteSQL nvarchar(1000) 'ExecuteSQL'
                ,CallBackSP nvarchar(1000) 'CallBackSP'
                ,SBCaller nvarchar(50) 'SBCaller'
                )
        EXEC sp_xml_removedocument @XMLPtr

        IF ((@ExecuteSQL IS NOT NULL) AND (LEN(@ExecuteSQL)>0))
        BEGIN
            SET @LogMsg='ExecuteSQL:' + @ExecuteSQL
            EXECUTE(@ExecuteSQL);
            SET @LogMsg='ExecuteSQLSuccess:' + @ExecuteSQL
            EXECute sp_LogSystemTransaction @SBCaller,@LogMsg,'SBMessage',0,''
        END
        IF ((@CallBackSP IS NOT NULL) AND (LEN(@CallBackSP)>0))
        BEGIN
            SET @CallBackSQL = @CallBackSP + ' @Sender=''sp_SB_HMSTargetActivProc'', @Res=''' + @ExecuteSQL + ''''

            SET @LogMsg='CallBackSQL:' + @CallBackSQL
            EXECute sp_LogSystemTransaction @SBCaller,@LogMsg,'SBMessage',0,''
            EXECUTE(@CallBackSQL);
        END

    END
    ELSE IF @RecvReqMsgName = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'
    BEGIN
        SET @LogMsg='MessageEnd:';
        END CONVERSATION @RecvReqDlgHandle WITH CLEANUP;
    END
    ELSE IF @RecvReqMsgName = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error'
    BEGIN
        DECLARE @message_body VARBINARY(MAX);
        DECLARE @code int;
        DECLARE @description NVARCHAR(3000);
        DECLARE @xmlMessage XML;

        SET @xmlMessage = CAST(@RecvReqMsg AS XML);

        SET @code = (
              SELECT @xmlMessage.value(
                N'declare namespace
                   brokerns="http://schemas.microsoft.com/SQL/ServiceBroker/Error";
                       (/brokerns:Error/brokerns:Code)[1]', 
                'int')
                );

        SET @description = (
              SELECT @xmlMessage.value(
                'declare namespace
                   brokerns="http://schemas.microsoft.com/SQL/ServiceBroker/Error";
                   (/brokerns:Error/brokerns:Description)[1]', 
                'nvarchar(3000)')
                );


        IF (@code = -8462)
        BEGIN
            SET @LogMsg='MessageEnd:';
            --EXECute sp_LogSystemTransaction @SBCaller,@LogMsg,'SBMessage',0,'';

            END CONVERSATION @RecvReqDlgHandle WITH CLEANUP;
        END
        ELSE
        BEGIN
            SET @LogMsg='ERR:' + @description + ' ' + CAST(@code AS VARCHAR(20));
            EXECute sp_LogSystemTransaction @SBCaller,@LogMsg,'SBError',0,'';

            END CONVERSATION @RecvReqDlgHandle;
        END
    END

    COMMIT TRANSACTION;

END
END

【问题讨论】:

  • 一开始队列很慢。花了将近一个小时来浏览 600 多条消息,还剩下将近 2000 条。我在其中一个表上运行了一个选择查询,该表将由上述代码执行的 SQL 更新,并得到一个错误重新锁定,我用 (nolock) 重新运行了查询,我得到了结果,同时,队列中的所有消息也都消失了,作业也完成了。我无法解释发生了什么,但需要解释为什么一开始会有很大的延迟并避免它。

标签: sql-server multithreading stored-procedures service-broker


【解决方案1】:

我注意到的一件事是,这件事似乎很多。大多数代码行似乎都在为服务代理对话框制作回复消息。

也就是说,服务代理的存在意味着您不必使用 sp_xml_preparedocument 来满足您的 xml 需求。看看XQuery。简而言之,这样的事情应该可以工作:

SELECT @ExecuteSQL = @RcvReqMsg.value('(RequestMsg/CommandParameters/ExecuteSQL)[1]', 'nvarchar(1000)')
   ,@CallBackSP = @RcvReqMsg.value('(RequestMsg/CommandParameters/CallBackSP)[1]', 'nvarchar(1000)')
   ,@SBCaller = @RcvReqMsg.value('(RequestMsg/CommandParameters/SBCaller)[1]', 'nvarchar(1000)')

其次,看起来消息包含要在包含此队列的数据库的上下文中执行的 SQL。它们的性能概况是什么?也就是说,那些是你的瓶颈吗?如果这些速度很慢,那么添加服务代理不会神奇地让事情变得更快

第三,您是否允许一次激活多个激活程序?检查 sys.service_queues 中的 max_readers 列来回答这个问题。如果它设置为 1 并且您的进程不需要串行运行,请增加该数字以并行运行它们。

第四,您似乎已经编写了激活程序,以便在完成之前只处理一条消息。查看this tutorial 中的示例。注意while (1=1) 循环。这使得激活过程在完成当前消息后返回到另一条消息的队列

最后,你为什么在乎?服务代理本质上是一种异步技术。如果某事/某人正在等待处理给定的消息,我会质疑。

【讨论】:

  • 感谢 Ben,您的回答让我们有所了解。我设计了这个,以便我可以通过服务代理传递要执行的实际 SQL,并且该过程将异步运行。正在运行的实际 SQL 不会花费太多时间来完成。 max_readers 设置为 10。我将查看您提供的教程,看看这是否有所作为并再次回复。用户不会立即关心,但需要几个小时才能完成,他们会开始质疑当出现如此严重的延迟时会发生什么,因此需要解决
  • Max_readers 在 TargetQueue 上为 10,但在 InitiatorQueue 上为 0。是不是也需要增加?我的代码已经有 while (1=1) 并且一直到队列上有消息为止。我不确定你说的是什么?这是错的吗?我看不出本教程之间有什么区别,我想我首先使用本教程来创建这个 Proc :)
【解决方案2】:

首先,我建议将此作为性能问题加以威胁,并将其作为任何其他性能问题来处理:衡量。请参阅How to analyse SQL Server performance,了解有关如何测量等待、IO、CPU 整体、会话或语句以及如何识别瓶颈的简要介绍和明确建议。一旦你知道瓶颈在哪里,你就可以考虑解决它的方法。

现在介绍更具体的 SSB。我想说你的程序包含三个对这个问题很感兴趣的部分:

  • 队列处理,即。 RECEIVEEND CONVERSATION
  • 消息解析(XML 粉碎)
  • 执行 (EXECUTE(@ExecuteSQL))

对于队列处理,我推荐Writing Service Broker Procedures 以了解如何加快处理速度。 RECEIVE TOP(1) 是最慢的方法。 如果可能,批量处理更快,甚至更快。要使批处理出列,您需要队列中的相关消息,这意味着SEND-在单个会话句柄上处理许多消息,请参阅Reusing Conversation。这可能会使应用程序显着复杂化。因此,我强烈建议您在进行如此剧烈的更改之前衡量并确定瓶颈。

对于 XML 粉碎,我同意 @BenThul,使用 XML data type methods 比使用 MSXML 过程更好。

最后是EXECUTE(@ExecuteSQL)。这对我们来说是一个黑匣子,只有您知道实际执行的是什么。不仅执行的 SQL 有多昂贵/复杂,还有阻塞的可能性有多大。此后台执行与您的前端代码之间的锁定争用可能会大大减慢队列处理速度。再次,测量,你会知道。附带说明:从您发布的数字来看,我希望问题出在此处。根据我的经验,一个完全按照您的操作(RECEIVE TOP(1),XML 解析,SEND 响应)的激活过程,没有执行,应该以每秒大约 100 条消息的速度运行,并耗尽您的 2000 队列在大约 20 秒内完成作业。您观察到的速度要慢得多,这会让我怀疑实际执行的 SQL。

最后,最容易尝试的事情是:提高MAX_QUEUE_READERS(同样,正如@BenThul 已经指出的那样):

 ALTER QUEUE HMSTargetQueueIntAct WITH ACTIVATION (MAX_QUEUE_READERS = 5)

这将允许并行处理请求。

您的过程中缺少正确的错误处理,您应该有一个BEGIN TRY/BEGIN CATCH 块。请参阅Error Handling in Service Broker proceduresError Handling and ActivationHandling exceptions that occur during the RECEIVE statement in activated proceduresException handling and nested transactions

【讨论】:

  • 感谢有用的链接和您的时间 Remus。我会通读一遍,看看我应该改变什么。我认为我一次不能阅读超过一条消息,因为它被设计为复制多线程的通用函数。我正在考虑实际上在这里传递多个 SQL 语句,希望这会加快进程,因为实际执行的 SQL 不会花费那么多时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多