System.Messaging 性能
Ingo Rammer
thinktecture
Richard Turner
项目经理
Microsoft 分布式系统组
摘要:通过针对不同的消息传递模式和要求,将 .NET System.Messaging 与本机 MSMQ COM API 进行比较,来研究 .NET System.Messaging 的性能特征,并获取有关如何创建高性能消息传递应用程序的指导原则。
下载相关的代码示例 SystemMessagingPerformanceTests.msi。

本页内容
|  | 简介 | 
|  | 方法 | 
|  | 软件和硬件设置 | 
|  | 托管性能与非托管性能对比 | 
|  | 托管代码消息传递应用程序的优化 | 
|  | 关于测试应用程序 | 
|  | 附录 A - 原始测试结果 | 
简介
使用 Microsoft 消息队列 (MSMQ) 及其 .NET API System.Messaging 所提供的基础结构,可以创建可靠、可伸缩和高性能的消息传递应用程序。尽管其中大多数应用程序采用的调用和通信模式是异步模式,但性能在大多数生产环境中仍然具有重要作用。
如果您在非托管环境中使用过消息队列,那您可能特别想知道 .NET 环境对应用程序性能有何影响。您可能还希望了解如何才能最有效地在 .NET 环境中的不同队列之间传递自定义数据。
在本白皮书中,我们对非托管 MSMQ COM 接口与 .NET System.Messaging 实施方案的性能进行了比较,并对如何提高消息传递应用程序的性能提供了指南。这些测试专注于托管与非托管环境之间原始传输速度的不同,以及由这两个环境提供的序列化技术之间的不同。后者尤其重要,因为在大多数异步消息传递应用程序中,对消息进行排队所需的时间都是关键因素,而完整处理时间只是相对次要的考虑因素。
方法
如同在 ASP.NET Web 服务、企业服务和 .NET Remoting 的性能中一样,我们认真考虑了本白皮书是应该通过详细的基准测试来说明如何实现最高性能,还是应该为常见业务应用场景提供良好、合理且可再现的“相对性能”比较。我们决定采用后一种方法,因为它适用的读者范围更广,并且可以更准确地显示 .NET System.Messaging 的实际性能特征。
本白皮书的目的是:
| 1. | 研究 System.Messaging 与 MSMQ COM API 之间的相对性能差别 | 
| 2. | 阐明 .NET 与 MSMQ COM 的实际性能特征 | 
| 3. | 帮助您确定何时、何地以及如何才能最恰当地利用这些技术 | 
| 4. | 提供一个测试应用程序,以供您在自己的计算机和环境中运行。我们强烈建议您创建和运行此测试环境,并对这些技术的性能特征进行研究和分析。只有这样,您才能充分理解影响消息队列系统性能的诸多因素。 注意:本白皮书不讨论本机 MSMQ Win32/"C" API 的性能特征。因为它是最底层的 MSMQ API,是构建 .NET 和 MSMQ COM 实施方案的基础。只有当您最关心性能时,可能才会需要研究通过对 MSMQ Win32/"C" API 仔细编码可能获得的显著的性能提高。但是,您应该清楚提高性能是要付出代价的 - 要编写、测试、保护和部署这样的代码,您需要付出的时间和努力必须比平常多得多。 | 
软件和硬件设置
以下测试通过两台计算机来执行: 发送方/客户端和接收方/服务器。
硬件/软件规范
发送方和接收方计算机都具备如下配置:
| • | CPU:2.8 GHz Intel P4 Prescott,带有 800 Mhz 前端总线 | 
| • | 硬盘:40 GB UltraDMA 100,ExcelStor Technology J340 | 
| • | 芯片集:Intel 865G/ICH 5 | 
| • | 内存:1024 MB 内存 (PC3200) | 
| • | 操作系统:Windows Server 2003 Standard Edition,带有 MSMQ 3.0 | 
测试应用程序
测试应用程序是使用 Visual Studio 2003 以发布模式创建和编译的单线程 C++ 和 .NET 应用程序。我们选择单线程测试驱动程序,是为了提供托管实施方案与本机实施方案在相对性能级别方面的比较,而不是使系统饱和以便尝试和实现绝对最高的性能级别。
请注意,所有测试都配备了最大量的可用物理内存 - 内存故障和内存分页问题在我们的测试中几乎不存在。另外,MSMQ 在这些测试中也不会对快递消息的消息传递数据进行分页。
请注意,测试代码的编写方式在典型的企业应用程序中很常见。COM API 的性能有可能进一步提高,例如,通过将 char* 字符串手动封送到 SafeArray (VT_I1) 中,从而避免将字符串转换为双字节 Unicode 格式的内部 VARIANT 转换过程中的性能损失。但是,我们很少会在典型的企业应用程序中看到这样的代码。因此,测试应用程序不会使用这种“罕见的调整”,但是我们认为当您进一步使用测试应用程序时,可以探索一下这样做的好处。
托管性能与非托管性能对比
在对消息传递应用程序进行性能测试之前,一定要认识到不同的应用程序拥有不同的基础结构要求。使用 MSMQ,您可以逐条消息地确定是仅在内存中传输消息(快递消息)还是在传输到下一个节点之前永久存储在磁盘上(可恢复消息)- 从而使应用程序免受计算机故障的影响。您还必须确定队列是否支持事务性消息。所有这些决定都将直接影响能够在给定时间段内传输的消息数量。
为了涵盖大多数的应用场景,我们按照消息的有效负载和消息传递选项的组合对测试进行了划分。
有效负载
我们将创建四种不同大小的有效负载:
| • | 原始基础结构:没有正文的消息 | 
| • | 传输:不同大小的预设格式的字符串。这样,就可以通过最低的封送/序列化开销来测试基础结构和传输。字符串将具有以下长度: 500、1,000、10,000、100,000、1,000,000 和 2,000,000 个字符。 | 
| • | XML 消息:在此,我们将比较由 .NET XML 序列化程序编码的数据以及由基于 COM 的 MSXML 引擎编码的数据的发送和接收。 | 
| • | 序列化对象:在此,我们将比较 .NET 对象序列化与 COM IPersistStream 机制。 | 
队列和消息类型
我们将使用下列消息和队列选项对前面提到的每种有效负载进行测试,并且每种都测试本地和远程排队和出列。
| • | 快递 | 
| • | 可恢复 | 
| • | 事务性(此处仅支持本地出列操作) | 
计时项 ... 或者我们到底测量什么
在基于通过 RPC、DCOM、.NET Remoting 或 ASP.NET Web 服务传递的同步调用的传统分布式应用程序中,以性能为中心的测量标准中最重要的是对单个请求-答复调用的完整响应时间。而在异步消息传递环境中,此请求-答复模式通常价值较小或没什么价值。大多数异步消息传递应用程序将关注以下三个计时项:
| • | 触发和忘记:将大量消息发送到本地或远程队列 | 
| • | 仅接收:从队列中检索大量现有消息 | 
| • | 端对端:发送消息,在接收端检索消息,并将每条消息完全反序列化为等同的表示(也就是,将字符串反序列化为字符串,或者将实现 IPersistStream 的 COM 对象反序列化为反序列化的对象)。 | 
有效负载测试结果
在下一节中,您将看到性能测试的结果。
测试 1 - 空消息
在第一个测试中,我们测量了每秒接收到的要传递到本地或远程队列的空消息数。此测试的结果显示在图 1 中。

图 1:对空消息进行排队
如图所示,对于快递消息的发送,.NET Framework System.Messaging API 的性能仅仅比 COM API 稍高一点,而对于可恢复消息和事务性消息的发送,这两者根本没有差别。这表明在发送快递消息时,.NET 基础结构与 MSMQ COM 基础结构是一样的。

图 2:将空消息出列
在图 2 中,您将看到对消息出列过程计时的结果基本相当,唯有本地快递消息例外 - 在这种情况下,这两个 API 之间的差别非常大。
在此测试的最后一部分中,我们使用服务器应用程序,在客户端发送消息后立即处理传入的消息。成功处理完一个完整的子测试批后,该服务器会立即向客户端发送确认消息。在这部分中,不存在针对每条消息的直接请求/响应语义,因为这与异步消息传递应用程序的主要设计目标相背。您可以在下面的图 3 中看到结果。

图 3:空消息的完整处理
这些结果清楚地表明,与 COM API 相比,.NET Framework 更快的出列功能也导致了更短的完整处理时间。
测试 2 - 发送不同大小的字符串
下面这个更大的测试集专注于有效负载的大小给消息传输性能带来的差别。为了执行此测试,只需分配所需大小的字符串(500、1,000、2,000、10,000、100,000、1,000,000 和 2,000,000 个字符)并将其中每个字符串设置为消息的正文。我们采用 .NET Framework(使用 BinaryMessageFormatter)和 COM API 的内部格式功能。
用来发送包含 1,000 个字符的字符串的相似、非事务性消息的 C# 源代码如下所示:
MessageQueue que = new MessageQueue( ... );
// 打开队列 ...
String bodyString = new String('x', 1000);
m.Formatter = new BinaryMessageFormatter();
m.Label = "Test";
m.Body = bodyString;
m.Recoverable = false; // 取决于配置
que.Send(m);
对于 C++,您可以使用类似下面的代码。请注意,此处我们使用了 CoGetClassObject(),而不是 CoCreateInstance(),目的是为了更快地执行多条消息:
MSMQ::IMSMQQueue3* pQueue;
// ... 打开队列 ...
HRESULT hr;
MSMQ::IMSMQMessage3* pMsg;
IClassFactory* pFact;
CString bodyString ('x',1000);
hr = CoGetClassObject(CLSID_MSMQMessage, CLSCTX_ALL, NULL,
IID_IClassFactory, reinterpret_cast<void**>(&pFact));
if (FAILED(hr)) exit(1);
hr = pFact->CreateInstance(NULL, IID_IMSMQMessage3,
reinterpret_cast<void**>(&pMsg));
if(FAILED(hr)) exit(1);
pMsg->Label = L"Test";
_variant_t var(bodyString);
pMsg->Body = var;
pMsg->Delivery = MSMQ::MQMSG_DELIVERY_EXPRESS;
hr = pMsg->Send(pQueue);
if (FAILED(hr)) exit(1);
pMsg->Release();
pQueue->Release();
当执行具有不同消息大小的测试时,您将遇到如图 4 所示的结果。

图 4:以快递模式对预定义字符串进行排队
如同您所看到的,COM API 在以快递模式发送任意大小的字符串消息时的性能要稍好一些。
与发送时的性能相比,当从队列中检索此类消息时,.NET 要比 COM 的性能稍好一些,如图 5 所示。

图 5:以快递模式将预定义字符串出列
您还可以看到两个平台之间没有明显的差别,尤其是当您考虑变化和误差时,更是如此。
对于远程队列,COM 和 .NET 之间的相对差别与本地队列的相对差别相似。
到目前为止,所有的测试都只是专注于消息传递解决方案的一部分 - 发送或接收。为了更好地了解实际应用程序的相对性能差别,我们需要记录远程系统上一批消息的完整处理时间。对于这些测试,客户端会将大量消息发送到服务器,然后在服务器上尽快将这些消息出列。为了测量性能,服务器在能够处理所有消息后,会立即向客户端发送确认消息。此测试的结果显示在图 6 中。

图 6:不同大小的字符串的完整处理时间
这些结果清楚地表明,当将字符串文本作为消息正文传递时,.NET 所提供的总体吞吐量将全面超越本机 MSMQ COM。这在很大程度上归功于以下事实:默认情况下,MSMQ COM 将所有字符串作为双字节 Unicode 进行处理,而 .NET 将字符串作为更有效的 UTF-8 进行编码,这大大缩短了字符串。
测试 3 - 序列化对象
通过消息排队来交换数据的主要方法之一是使用 .NET Framework 提供的内置序列化功能。.NET Framework 允许您挑选无需任何自定义序列化代码就能工作的标准格式化程序。对于 COM,这通常意味着 IPersistStream 的自定义实现。
为了执行以下测试,我们将基于以下 Order 对象传输消息,其中包括 50 个 LineItem 主题。我们还通过实现 IPersistStream 创建了可与这些类进行比较的 COM 版本。
[Serializable]
public class Order
{
public DateTime Date;
public int CustomerID;
public Address ShippingAddress;
public Address BillingAddress;
public double Total;
[XmlArrayItem(typeof(LineItem))]
public ArrayList LineItems;
}
[Serializable]
public class Address
{
public String Firstname;
public String Lastname;
public string Company;
public string City;
public string Street;
public string ZIPCode;
public string Country;
public string State;
}
[Serializable]
public class LineItem
{
public int ArticleID;
public String Name;
public double Price;
public double Quantity;
public double LineTotal;
}
使用与此类似的对象自动序列化,向您展示了使用 .NET Framework 的一项更大的好处 - 内置序列化功能为您处理了许多问题。
在 .NET 版本中,我们使用了类似下面的代码来发送包含 Order 对象的序列化表示的消息:
Order o = new Order( ... ); // 填充 Order 对象 MessageQueue que = new MessageQueue( ... ); // 打开队列 ... m.Formatter = new BinaryMessageFormatter(); m.Label = "Test"; m.Body = ord; m.Recoverable = false; // 取决于配置 que.Send(m);
当使用 COM API 时,您可以采用实现 IPersistStream 并将其 IUnknown 接口指针封装到 VARIANT 中的 COM 对象。然后,MSQM COM API 将负责创建流来为您执行序列化和反序列化操作:
IOrder* pOrd;
// 创建并填充 Order 对象
MSMQ::IMSMQQueue3* pQueue;
// ... 打开队列 ...
HRESULT hr;
MSMQ::IMSMQMessage3* pMsg;
IClassFactory* pFact;
CString bodyString ('x',1000);
hr = CoGetClassObject(CLSID_MSMQMessage, CLSCTX_ALL, NULL,
IID_IClassFactory, reinterpret_cast<void**>(&pFact));
if (FAILED(hr)) exit(1);
hr = pFact->CreateInstance(NULL, IID_IMSMQMessage3,
reinterpret_cast<void**>(&pMsg));
if(FAILED(hr)) exit(1);
pMsg->Label = L"Test";
hr=pOrd->QueryInterface(IID_IUnknown, reinterpret_cast<void**>(&pUnk));
if (FAILED(hr)) exit(1);
CComVariant var;
var.punkVal = pUnk;
var.vt = VT_UNKNOWN;
pMsg->Body = var;
pMsg->Delivery = MSMQ::MQMSG_DELIVERY_EXPRESS;
hr = pMsg->Send(pQueue);
if (FAILED(hr)) exit(1);
pMsg->Release();
pQueue->Release();
在图 7 和图 8 中,您将看到 .NET Framework 的内置序列化功能虽然在开发时间上能带来巨大的好处,但是在运行时需要付出相应的代价。当将其与优化的 IPersistStream 实施方案进行比较时,您可以看到对于消息的排队和出列操作,COM 版本比 .NET 版本快。

图 7:比较消息排队的序列化技术

图 8:比较消息出列的序列化技术
我们有必要理解为什么会出现这么大的差别。.NET Framework 提供了运行时序列化功能,这些功能可以采用任何兼容的类型(例如,标有 [Serializable] 的类)并使用内置的反射 API 来执行动态序列化和反序列化。例如,这些序列化程序将自动存储公共和专用字段,以及完成对象图。另一方面,在 COM 环境中使用 IPersistStream 会创建非常严格的序列化实施方案: “您”必须准确指定您希望“如何”存储数据。当然,后者通常更快。
对于高性能的关键应用程序,您完全可以在 .NET 中实现与 COM 中的 IPersistStream 相似的序列化技术。
在下面的测试中,我们通过添加方法,使用 BinaryWriter/BinaryReader 组合来保存和加载对象的状态,从而实现这种序列化技术。您可以在以下代码片断中看到其中一种实施方案:
[Serializable]
public class LineItem
{
public int ArticleID;
public String Name;
public double Price;
public double Quantity;
public double LineTotal;
public void Load(BinaryReader rdr)
{
ArticleID = rdr.ReadInt32();
Name = rdr.ReadString();
Price = rdr.ReadDouble();
Quantity = rdr.ReadDouble();
LineTotal = rdr.ReadDouble();
}
public void Save(BinaryWriter wrt)
{
wrt.Write(ArticleID);
wrt.Write(Name);
wrt.Write(Price);
wrt.Write(Quantity);
wrt.Write(LineTotal);
}
}
将其与 COM 中 IPersistStream 实现的相关部分进行比较,您会看到现在它们极其相似:
bool CLineItem::InternalLoad( IStream *pStm )
{
m_bstrName.ReadFromStream(pStm);
pStm->Read(&m_nArticleID, sizeof(m_nArticleID),0);
pStm->Read(&m_nPrice, sizeof(m_nPrice),0);
pStm->Read(&m_nQuantity, sizeof(m_nQuantity),0);
pStm->Read(&m_nLineTotal, sizeof(m_nLineTotal),0);
return false;
}
bool CLineItem::InternalSave( IStream *pStm )
{
m_bstrName.WriteToStream(pStm);
pStm->Write(&m_nArticleID, sizeof(m_nArticleID),0);
pStm->Write(&m_nPrice, sizeof(m_nPrice),0);
pStm->Write(&m_nQuantity, sizeof(m_nQuantity),0);
pStm->Write(&m_nLineTotal, sizeof(m_nLineTotal),0);
pStm->Commit(STGC_DEFAULT);
return true;
}
然后您就可以使用如下代码发送通过 .NET 中的这种手动优化的序列化技术进行序列化的消息:
MessageQueue que = new MessageQueue ( ... );
using (Message m = new Message())
{
MemoryStream ms = new MemoryStream();
BinaryWriter wrt = new BinaryWriter(ms);
ord.Save(wrt);
wrt.Flush();
ms.Flush();
m.BodyStream= ms;
m.Label = "Test";
m.Recoverable = false;
que.Send(m);
}
当接收消息时,您必须编写类似下面的代码:
MessageQueue que = new MessageQueue ( ... );
using (Message m = que.Receive())
{
Order o = new Order();
BinaryReader rdr = new BinaryReader(m.BodyStream);
o.Load(rdr);
}
我们在执行下一次比较之前在所有业务实体类(Order、Address 和 LineItem)中添加了方法 Load() 和 Save(),从而进一步明确了原始 COM 版本与新优化的 BinaryWriter/BinaryReader 组合之间的性能差别。
在图 9 和图 10 中,您可以看到由这种变化带来的巨大的性能提高。

图 9:使用手动优化的 .NET 序列化来对消息进行排队

图 10:使用手动优化的 .NET 序列化来将消息出列
如同您在上面所看到的,只需付出比使用默认 .NET 序列化程序稍多一点的努力(并且其工作量与您在 COM 中实现 IPersistStream 所需的工作量基本相同),就可以显著地提高基于 .NET 的队列应用程序的性能。
托管代码消息传递应用程序的优化
如同您在上面的测试结果中所看到的,使用 .NET Framework 所创建的消息传递应用程序的性能在很多情况下与 COM 应用程序相当和更高。除了上面所示的性能提高以外,我们还希望给出更多的提示和指导原则,帮助您缩短应用程序的响应时间。
选择正确的消息类型
如同您在此白皮书所执行的测试中所看到的,最大的性能差别是由所选消息的类型带来的。使用 MSMQ 可以访问以下类型的消息:
快递:这些消息仅存储在内存中。如果对消息进行排队的计算机重新启动或由于电源故障而受到冲击,这些消息将会丢失。但是,只要计算机本身仍在运行,快递消息就不会受网络故障的影响。例如,您可以断开网络并对大量消息进行排队,只要计算机再次联机,这些消息就会传输出去。尽管这些消息存储在内存中,但一旦达到实际的内存阈值,就可能会出现性能问题。在这种情况下,虚拟内存管理器的标准内存分页/内存交换机制可能会对性能造成负面影响。
可恢复:可恢复消息始终存储在磁盘上。如果计算机在将消息传送到最终接收方之前重新启动,那么只要重新启动 MSMQ 服务,状态就会恢复。可恢复消息通常比快递消息慢。
事务性:事务性队列确保按次序、一次性地传递消息。此外,它们还可以使用数据库式的事务来发送和接收消息。例如,您可以在一个事务中发送多条消息,以便确保要么所有消息都排队,要么所有消息都不排队,同时确保它们按相同的顺序到达。您还可以在 MSDTC 或 COM+ 分布式事务中包含事务性交互,以协调数据库访问与消息队列操作。事务性队列使用的操作模式最慢。
消息队列应用程序的性能取决于多个因素。其中最重要的一个是要选择正确的消息传递类型。但是,在选择快递模式的消息之前,您应该先仔细检查应用程序的基础结构要求。在这种情况下,您必须考虑恢复逻辑,而这会大大增加应用程序的复杂性,尤其是即便您不打算使用可恢复消息,仍然需要测试所有的消息故障情况。
选择 .NET 中最好的消息格式化程序
与基于 COM 的消息传递应用程序相比,.NET 应用程序的一个最大优点是便于使用的、灵活的对象序列化框架。除非您需要绝对最佳的性能,否则您不需要手动实现任何类似于 IPersistStream 的持久性代码。相反,您完全可以依赖内置的序列化和格式化程序。
.NET Framework 支持使用三种消息格式化程序来实现 .NET 对象的透明序列化和反序列化。第三种格式化程序主要用于与我们将在后面介绍的实现 IPersistStream 的 COM 对象进行交互。如果您在 .NET 与 .NET 之间进行通信,则格式化程序的选择尤其重要,因为它使您可以灵活地选择更快的二进制序列化机制。
为了测试 XML 和二进制序列化之间的差别,我们决定使用与上面相同的 Order 类,并且仅更改格式化程序。在图 11 中,当您传输自定义业务对象时,格式化程序的选择会影响消息的排队性能 - 对于快递消息、可恢复消息和事务性消息来说都是这样。

图 11:不同格式化程序的消息排队性能
如同您在图 12 中所看到的,在消息的完整处理过程中甚至存在更大的性能差别。与 XML 格式化程序相比,对快递消息使用二进制消息格式化程序使您每秒可以多处理 77% 的消息。

图 12:完整处理过程中的性能差别
针对 COM 兼容类型的优化
如果消息正文是与 COM 兼容的类型(例如,int、double、string 或实现 IPersistStream 的对象),则您可以利用最快的 .NET ActiveXMessageFormatter。此格式化程序还可以在以 Visual Basic 或 C++ 编写的传统 COM 客户端与基于 .NET 的应用程序之间交换序列化的 COM 对象。
在图 13 和图 14 中,您可以看到在传输消息正文的类型为 System.Int32 的消息时,BinaryMessageFormatter、XmlMessageFormatter 和 ActiveXMessageFormatter 之间的性能差别。

图 13:对 COM 兼容消息进行排队

图 14:将 COM 兼容消息出列
一对多消息传递
从 Windows Server 2003 和 Windows XP 开始提供的 MSMQ 3.0 引入了两种新的方法,用于将一条消息发送到多个接收方。如果您的网络基础结构支持 IP 多播协议并且如果您不需要特定的传递保证或事务性保证,则您可以采用 IP 多播。当使用 IP 多播与多个接收方进行通信时,每个数据包仅通过网络发送一次(而无论有多少个接收方),并放到接收方一端多台计算机上的多个队列中。但是,这意味着不存在传输或事务性保证 - MSMQ IP 多播不能确定消息是否到达了任何目标接收方。
当将 System.Messaging 或 COM API 与 MSMQ 3.0 结合使用时,您也可以使用新的语法来指定多个接收方。但是,在内部会使用传统的点对点连接对其进行处理,从而给予您必要的传输和事务性保证。使用这种技术可以减少序列化开销 - 如果您将同一消息发送到多个接收方,在没有使用此技术的情况下这些开销是必需的。
要将一条消息发送到多个队列,您可以使用以下语法并在格式名称中使用“,”(逗点)来分隔目标队列的格式名称:
String queues = "DIRECT=OS:localhost\\private$\\Q1," +
"DIRECT=OS:localhost\\private$\\Q2," +
"DIRECT=OS:localhost\\private$\\Q3"
MessageQueue que = new MessageQueue("FORMATNAME:" + queues);
Message msg = new Message();
msg.Formatter = fmt;
msg.Label = "MULTI";
que.Send(msg);
始终远程发送/本地读取
当 MSMQ 将待发消息从您的计算机传输到远程目标计算机时,会发生以下情况:
| • | 一旦将消息发送到远程队列(例如,通过指定如“DIRECT=OS:remotehostname/private$/queuename”的格式名称),MSMQ 就会在发送方的计算机上创建所谓的“待发队列”。 | 
| • | 客户端应用程序发送到远程队列的所有消息将首先存储在此“待发队列”中。客户端应用程序从来不会与远程服务器直接进行通信,而仅会与客户端计算机上运行的本地 MSMQ 实例联系。这样就可以实现完全的异步配合 - 这是使用 MSMQ 的巨大优点之一:无论服务器是否在运行,客户端都可以发送消息。 | 
| • | 然后,MSMQ 将与远程服务器联系并使用优化的内部协议来传输消息。 | 
但是,当接收消息时,情况会稍有不同:
| • | 一旦打开接收访问的队列,就会始终与“真正的”队列进行通信。 | 
| • | 在这种情况下,MSMQ 使用标准的 RPC 而不是用于发送-访问的 MSMQ 内部协议,来与远程计算机进行通信。这意味着远程计算机必须可用,更重要的是,您必须忍受更慢、更麻烦的协议。 | 
| • | 如果远程计算机挂起,则客户端甚至可能会一直阻塞,直到可以恢复连接为止。此外,远程读取仅适用于非事务性队列。 | 
这些注意事项引出了基于消息传递的可靠系统的最重要指导原则之一: 始终“远程发送、本地读取”。如果某台其他计算机向客户端发送信息,它应该通过将消息转发到“DIRECT=OS:yourclient/private$/somequeue”,而不是转发到与起始计算机建立连接的客户端,来执行此操作。这是基于 MSMQ 的消息传递应用程序的一条基本设计原则。仅当您特别需要这样做并且能够忍受其对应用程序可靠性的影响时,才能忽略此原则。
更多性能提示
在本白皮书中,我们将 .NET API 与 COM API 进行了比较,实验的数据告诉我们对于 MSMQ,COM API 是最常用的非托管接口。除了这个接口以外,MSMQ 还提供底层的 Win32 API。
Win32 MSMQ API 的性能比 COM 和 .NET 要好的多,因为这两个高层的 API 实际上是封装了基础的本机 Win32 API。如果您的本机 Windows 应用程序(以非托管语言编写)依赖于最佳性能,则您有必要采用 Win32 API 而不是 COM API。
为了充分利用 MSMQ 基础结构 - 独立于所选 API - 我们建议您阅读以下可以在 MSDN 上找到的白皮书:
MSMQ Frequently Asked Questions (March 02, 2004)(英文)
MSMQ Best Practices (May 27, 2003)(英文)
Optimizing Message Queuing Performance (March 28, 2003)(英文)
这些文档以及其他文档还引自 MSMQ 产品网站(英文)。
关于测试应用程序
.NET 和 C# 版本的测试应用程序均可在不同的模式下启动以测试不同的场景。您可以通过传递几个命令行参数来配置它们的行为:
MsmqPerfTest.exe <TransactionMode> <Operation> <Queue> [<ResponseQueue>]
例如:
MsmqPerfTest NOTX SENDANDPURGE DIRECT=OS:localhost\private$\myQueue
参数
TransactionMode
NOTX:不使用事务。
TX:使用内置 MSMQ 事务来处理每个发送和接收操作。注意:在这种情况下,指定的队列必须是事务性的!
Operation
SENDANDPURGE:将消息发送到目标队列,测量发送所有消息的时间并在每次测试后清除队列。
SENDANDRECEIVE:将消息发送到目标队列,然后检索所有的消息并测量接收时间。
SERVER:用作当消息到达时就立即对其进行处理的服务器。该服务器仅在每个测试批(可以包含数千条消息)后向客户端发回一条消息。在 CLIENT/SERVER 组合中,该服务器必须首先启动,因为它将在等待测试消息之前清除其队列。
CLIENT:在处理了批中的所有消息之后,向定义的队列发送消息并等待响应。
Queue/ResponseQueue
现有队列的有效格式名称(例如 DIRECT=OS:localhost\private$\myQueue)。如果您将 TransactionMode 参数的值指定为 TX,则它必须是事务性队列。如果您将 TransactionMode 参数的值指定为 NOTX,则它必须是非事务性队列。
注意:对于 CLIENT 和 SERVER 操作模式,命令行上的第一个队列名称是指服务器的队列。第二个是客户端的响应队列。此参数顺序对于客户端和服务器是相同的。
以下两个命令行将启动一对匹配的接收方和发送方。
MsmqPerfTest NOTX SERVER DIRECT=OS:localhost\private$\server DIRECT=OS:localhost\private$\client MsmqPerfTest NOTX CLIENT DIRECT=OS:localhost\private$\server DIRECT=OS:localhost\private$\client
附录 A - 原始测试结果
| 发送空消息 | |||||
| 每秒消息数 | 标准偏差 | ||||
| 模式 | 本地/远程 | .NET | COM | .NET | COM | 
| 快递 | 本地 | 49,311 | 48,674 | 761.23 | 430.25 | 
| 远程 | 25,257 | 24,864 | 199.42 | 93.04 | |
| 可恢复 | 本地 | 2,455 | 2,457 | 11.21 | 11.76 | 
| 远程 | 2,506 | 2,500 | 10.89 | 7.38 | |
| 事务性 | 本地 | 1,683 | 1,724 | 37.65 | 36.59 | 
| 远程 | 1,662 | 1,739 | 19.84 | 23.91 | |
| 接收空消息 | |||||
| 每秒消息数 | 标准偏差 | ||||
| 模式 | 本地/远程 | .NET | COM | .NET | COM | 
| 快递 | 本地 | 37,351 | 25,708 | 75.14 | 478.76 | 
| 远程 | 2,095 | 2,074 | 18.09 | 13.54 | |
| 可恢复 | 本地 | 2,955 | 2,796 | 36.59 | 38.49 | 
| 远程 | 2,104 | 2,092 | 14.42 | 28.18 | |
| 事务性 | 本地 | 2,006 | 1,942 | 11.16 | 26.04 | 
| 空消息的完整处理 | |||||
| 每秒消息数 | 标准偏差 | ||||
| 模式 | 本地/远程 | .NET | COM | .NET | COM | 
| 快递 | 本地 | 24,209 | 18,890 | 28.57 | 15.88 | 
| 远程 | 11,184 | 11,026 | 94.85 | 33.82 | |
| 可恢复 | 本地 | 1,953 | 2,039 | 272.5 | 354.8 | 
| 远程 | 1,645 | 1,629 | 12.62 | 6.83 | |
| 事务性 | 本地 | 1,414 | 1,522 | 18.37 | 34.87 | 
| 远程 | 1,228 | 1,242 | 9.91 | 15.32 | |
| 快递发送(本地) | ||||
| 每秒消息数 | 标准偏差 | |||
| 大小 | .NET | COM | .NET | COM | 
| 500 | 19,814 | 28,572 | 196.96 | 1311.04 | 
| 1,000 | 15,117 | 23,476 | 294.62 | 1098.6 | 
| 2,000 | 10,722 | 15,345 | 229.27 | 1167.3 | 
| 10,000 | 3,083 | 4,500 | 79.62 | 236.12 | 
| 100,000 | 276 | 341 | 4.36 | 30.18 | 
| 1,000,000 | 28.83 | 30.33 | 0.75 | 2.94 | 
| 2,000,000 | 14.17 | 14.50 | 0.41 | 1.64 | 
| 快递接收(本地) | ||||
| 每秒消息数 | 标准偏差 | |||
| 大小 | .NET | COM | .NET | COM | 
| 500 | 24,137 | 20,237 | 65.85 | 72.03 | 
| 1,000 | 14,595 | 18,318 | 41.36 | 81.24 | 
| 2,000 | 12,930 | 11,221 | 50.31 | 57.77 | 
| 10,000 | 6,611 | 6,750 | 34.21 | 28.17 | 
| 100,000 | 696 | 643 | 6.63 | 2.35 | 
| 1,000,000 | 86.33 | 49.17 | 0.52 | 0.41 | 
| 2,000,000 | 42.50 | 24.00 | 0.55 | 0 | 
| 快递消息的完整处理(远程) | ||||
| 每秒消息数 | 标准偏差 | |||
| 大小 | .NET | COM | .NET | COM | 
| 500 | 8,190 | 7,769 | 189.1 | 130.69 | 
| 1,000 | 6,853 | 4,702 | 61.34 | 53.41 | 
| 2,000 | 4,549 | 2,569 | 71.86 | 10.01 | 
| 10,000 | 1,045 | 529 | 13.75 | 40.49 | 
| 100,000 | 105 | 53 | 1.83 | 0.75 | 
| 1,000,000 | 9.83 | 5.00 | 0.41 | 0 | 
| 2,000,000 | 4.67 | 2.00 | 0.52 | 0 | 
| IPersistStream 与 .NET 序列化对比(发送) | |||||||
| 每秒消息数 | 标准偏差 | ||||||
| 模式 | 本地/远程 | .NET | COM | 优化的 .NET | .NET | COM | 优化的 .NET | 
| 快递 | 本地 | 1,718 | 6,188 | 13,305 | 5.34 | 331.93 | 685.93 | 
| 远程 | 1,441 | 4,523 | 9,129 | 3.29 | 44.34 | 247.91 | |
| 可恢复 | 本地 | 910 | 1,490 | 2,236 | 26.65 | 57.45 | 43.44 | 
| 远程 | 948 | 1,333 | 1,973 | 3.89 | 46.42 | 16.59 | |
| 事务性 | 本地 | 791 | 1,261 | 1,660 | 18.1 | 13.26 | 81.88 | 
| 远程 | 603 | 1,114 | 954 | 15.81 | 295.88 | 29.2 | |
| IPersistStream 与 .NET 序列化对比(发送) | |||||||
| 每秒消息数 | 标准偏差 | ||||||
| 模式 | 本地/远程 | .NET | COM | 优化的 .NET | .NET | COM | 优化的 .NET | 
| 快递 | 本地 | 1,870 | 3,049 | 10,441 | 1.9 | 10.59 | 16.65 | 
| 远程 | 509 | 511 | 672 | 2.34 | 2.34 | 1.64 | |
| 可恢复 | 本地 | 1,096 | 1,373 | 2,042 | 38.58 | 27.65 | 118.32 | 
| 远程 | 505 | 528 | 690 | 4.18 | 2.88 | 1.83 | |
| 事务性 | 本地 | 922 | 1,203 | 1,604 | 19.77 | 5.68 | 71.65 | 
| .NET 格式化程序比较(排队) | |||||
| 每秒消息数 | 标准偏差 | ||||
| 模式 | 本地/远程 | .NET 二进制 | .NET XML | .NET 二进制 | .NET XML | 
| 快递 | 本地 | 1,718 | 1,344 | 5.34 | 5.381 | 
| 远程 | 1,441 | 987 | 3.29 | 4.22 | |
| 可恢复 | 本地 | 910 | 573 | 26.65 | 7.33 | 
| 远程 | 948 | 682 | 3.89 | 5.59 | |
| 事务性 | 本地 | 791 | 493 | 18.1 | 5.68 | 
| 远程 | 603 | 495 | 15.81 | 13.99 | |
| .NET 格式化程序比较(出列) | |||||
| 每秒消息数 | 标准偏差 | ||||
| 模式 | 本地/远程 | .NET 二进制 | .NET XML | .NET 二进制 | .NET XML | 
| 快递 | 本地 | 1,870 | 791 | 1.9 | 1.48 | 
| 远程 | 509 | 282 | 2.34 | 1.14 | |
| 可恢复 | 本地 | 1,096 | 594 | 38.58 | 7.33 | 
| 远程 | 948 | 682 | 3.89 | 7.19 | |
| 事务性 | 本地 | 505 | 284 | 4.18 | 0.71 | 
| 远程 | 922 | 533 | 19.77 | 6.44 | |
 
                    
                 
                
 

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号