We all know that testing is very important for every kind of solution. However, testing and in particular performance testing is often neglected for time or budget reasons. One of the things that I have found when working with customers is that the amount of performance testing done on a project tends to fall into a couple of categories. The first and in my opinion the worst is when no performance testing is done at all. I would call this a write once, go live and cross your fingers approach. The next type is when load/stress testing is performed right at the end of the project lifecycle, typically a few weeks before the go live date. If the application is not able to meet the expected performance goals in terms of scalability and latency, there’s a serious risk that architects and developers will have to spend a considerable amount of time to redesign, rewrite and test large portions of the solution.
Sometimes performance testing is done using a rig that does not accurately reflect the production environment, while in other cases I have seen people using different adapters that do not match those used in the production environment. All these bad practices and factors expose the project to the risk of delays and failure.
BizTalk Server is used in mission and business critical scenarios where it is not just part of the business, it is actually running the entire business. Overlooking performing testing increases the risk of failure which can cause not just technical problems but political problems as well.
The way to minimize the risk of a BizTalk project (and of any software project in general) is to adopt a strong and consistent testing strategy throughout the development phase and before deploying the final solution into production. For this reason, the BizTalk Customer Advisory Team adopts a consistent and reusable methodology when running performance labs with customers. This process is fully documented in the BizTalk Server Performance Testing Methodology of the Microsoft BizTalk Server 2009 Performance Optimization Guide.
The best approach is to leverage a test framework like BizUnit to automate test runs and possibly schedule their execution over night. Another key factor is to individuate the right tool to generate load against the application. To this purpose you can use the BizTalk LoadGen 2007 as I explained in my previous article. The LoadGen tool offers the following important features and benefits to provide a simple, generic, reliable, and consistent tool for generating message transmission loads. By default, LoadGen supports several transport protocols:
However, LoadGen is a great tool, but it suffers of the following problems:
Sometimes customers/partners create a custom multi-threaded application in order to generate load against their BizTalk Application, correlate request and response messages and persist response times to a relational database. This approach requires time and resources to design and develop the multi-threaded application capable to generate the load against the BizTalk platform and store test results to a custom repository. Another common technique adopted by customers is to use a dedicated product like HP LoadRunner to measure end-to-end performance, diagnose application and system bottlenecks and tune for better performance. So the question arises: is there any Microsoft product that I can use to perform stress and load tests in a easy way? The answer is yes.
In my opinion, the best solution to design, configure, execute and monitor performance tests against BizTalk is to leverage Visual Studio Team System Test Edition that includes a suite of tools that allow to conduct several kinds of tests (unit tests, load tests, web tests, etc.). In particular, Visual Studio Team System Test Edition provides a tool for creating and running load tests. For more information on this topic see Understanding Load Tests. Visual Studio Team System Test Edition provides the following advantages:
Recently I published an article in 2 parts (Part1, Part2) where I compare 4 different techniques to process an XLANGMessage within a method exposed by a business component invoked by an orchestration. In particular, I created a Two-Way Synchronous version and a One-Way Asynchronous version for each of these patterns. All of the 4 synchronous patterns share the same architecture depicted below:
Message Flow:
In order to generate the load against the WCF-Basic and WCF-Custom Receive Locations I proceeded as follows:
The client endpoints used by the InvokeTwoWayWCFReceiveLocation method to exchange messages with the WCF Receive Locations are defined in the configuration file (App.config) of the project.
App.config
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <!-- Bindings used by client endpoints --> <bindings> <netTcpBinding> <binding name="netTcpBinding" closeTimeout="01:10:00" openTimeout="01:10:00" receiveTimeout="01:10:00" sendTimeout="01:10:00" transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions" hostNameComparisonMode="StrongWildcard" listenBacklog="100" maxBufferPoolSize="1048576" maxBufferSize="10485760" maxConnections="200" maxReceivedMessageSize="10485760"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" /> <security mode="None"> <transport clientCredentialType="Windows" protectionLevel="EncryptAndSign" /> <message clientCredentialType="Windows" /> </security> </binding> </netTcpBinding> <basicHttpBinding> <binding name="basicHttpBinding" closeTimeout="00:10:00" openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="10485760" maxBufferPoolSize="524288" maxReceivedMessageSize="10485760" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <security mode="None"> <transport clientCredentialType="None" proxyCredentialType="None" realm="" /> <message clientCredentialType="UserName" algorithmSuite="Default" /> </security> </binding> </basicHttpBinding> </bindings> <client> <!-- Client endpoints used by client excahnge messages with the WCF Receive Locations --> <endpoint address="net.tcp://localhost:8123/calculator" binding="netTcpBinding" bindingConfiguration="netTcpBinding" contract="System.ServiceModel.Channels.IRequestChannel" name="netTcpCalculatorServiceReceiveLocationEndpoint" /> <endpoint address="http://localhost/CalculatorServiceReceiveLocation/CalculatorService.svc" binding="basicHttpBinding" bindingConfiguration="basicHttpBinding" contract="System.ServiceModel.Channels.IRequestChannel" name="basicHttpCalculatorServiceReceiveLocationEndpoint" /> </client> </system.serviceModel> <appSettings> <!-- Folder containing test messages --> <add key="testMessageFolder" value="C:\Projects\HandleXLANGMessages\WCFLoadTest\TestMessages" /> </appSettings> </configuration>
The following table contains the code of the WCFLoadTest class:
WCFLoadTest Class
#region Copyright //------------------------------------------------- // Author: Paolo Salvatori // Email: paolos@microsoft.com // History: 2009-09-20 Created //------------------------------------------------- #endregion #region Using Directives using System; using System.IO; using System.Diagnostics; using System.Text; using System.Configuration; using System.Collections.Generic; using System.Linq; using System.Xml; using System.ServiceModel; using System.ServiceModel.Channels; using Microsoft.VisualStudio.TestTools.UnitTesting; #endregion namespace Microsoft.BizTalk.CAT.Samples.HandleXLANGMessages.WCFLoadTest { /// <summary> /// Summary description for UnitTest1 /// </summary> [TestClass] public class WCFLoadTest { #region Constants private const int MaxBufferSize = 2097152; private const string Source = "WCF Load Test"; private const string Star = "*"; private const string TestMessageFolderParameter = "testMessageFolder"; private const string TestMessageFolderDefault = @"C:\Projects\HandleXLANGMessages\WCFLoadTest\TestMessages"; private const string TestMessageFolderFormat = @"Test Message Folder = {0}"; private const string NetTcpCalculatorServiceReceiveLocationEndpoint = "netTcpCalculatorServiceReceiveLocationEndpoint"; private const string BasicHttpCalculatorServiceReceiveLocationEndpoint = "basicHttpCalculatorServiceReceiveLocationEndpoint"; private const string SmallCustomBtxMessageOrchestration = "SmallCustomBtxMessageOrchestration.xml"; private const string SmallMessageClassesOrchestration = "SmallMessageClassesOrchestration.xml"; private const string SmallStreamOrchestration = "SmallStreamOrchestration.xml"; private const string SmallXmlDocumentOrchestration = "SmallXmlDocumentOrchestration.xml"; private const string MediumCustomBtxMessageOrchestration = "MediumCustomBtxMessageOrchestration.xml"; private const string MediumMessageClassesOrchestration = "MediumMessageClassesOrchestration.xml"; private const string MediumStreamOrchestration = "MediumStreamOrchestration.xml"; private const string MediumXmlDocumentOrchestration = "MediumXmlDocumentOrchestration.xml"; private const string LargeCustomBtxMessageOrchestration = "LargeCustomBtxMessageOrchestration.xml"; private const string LargeMessageClassesOrchestration = "LargeMessageClassesOrchestration.xml"; private const string LargeStreamOrchestration = "LargeStreamOrchestration.xml"; private const string LargeXmlDocumentOrchestration = "LargeXmlDocumentOrchestration.xml"; #endregion #region Private Instance Fields private TestContext testContextInstance; #endregion #region Private Static Fields private static string testMessageFolder = null; #endregion #region Public Instance Constructor public WCFLoadTest() { } #endregion #region Public Static Constructor static WCFLoadTest() { try { testMessageFolder = ConfigurationManager.AppSettings[TestMessageFolderParameter]; if (string.IsNullOrEmpty(testMessageFolder)) { testMessageFolder = TestMessageFolderDefault; } } catch (Exception ex) { Trace.WriteLine(ex.Message); EventLog.WriteEntry(Source, ex.Message, EventLogEntryType.Error); } } #endregion #region Public Properties /// <summary> ///Gets or sets the test context which provides ///information about and functionality for the current test run. ///</summary> public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } #endregion #region Test Methods [TestMethod] public void BasicHttpSmallCustomBtxMessageOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallCustomBtxMessageOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpSmallMessageClassesOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallMessageClassesOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpSmallStreamOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallStreamOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpSmallXmlDocumentOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallXmlDocumentOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpMediumCustomBtxMessageOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumCustomBtxMessageOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpMediumMessageClassesOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumMessageClassesOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpMediumStreamOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumStreamOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpMediumXmlDocumentOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumXmlDocumentOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpLargeCustomBtxMessageOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeCustomBtxMessageOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpLargeMessageClassesOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeMessageClassesOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpLargeStreamOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeStreamOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void BasicHttpLargeXmlDocumentOrchestration() { InvokeTwoWayWCFReceiveLocation(BasicHttpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeXmlDocumentOrchestration, MessageVersion.Soap11, SessionMode.Allowed); } [TestMethod] public void NetTcpSmallCustomBtxMessageOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallCustomBtxMessageOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpSmallMessageClassesOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallMessageClassesOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpSmallStreamOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallStreamOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpSmallXmlDocumentOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, SmallXmlDocumentOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpMediumCustomBtxMessageOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumCustomBtxMessageOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpMediumMessageClassesOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumMessageClassesOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpMediumStreamOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumStreamOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpMediumXmlDocumentOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, MediumXmlDocumentOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpLargeCustomBtxMessageOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeCustomBtxMessageOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpLargeMessageClassesOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeMessageClassesOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpLargeStreamOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeStreamOrchestration, MessageVersion.Default, SessionMode.Allowed); } [TestMethod] public void NetTcpLargeXmlDocumentOrchestration() { InvokeTwoWayWCFReceiveLocation(NetTcpCalculatorServiceReceiveLocationEndpoint, testMessageFolder, LargeXmlDocumentOrchestration, MessageVersion.Default, SessionMode.Allowed); } #endregion #region Helper Methods public void InvokeTwoWayWCFReceiveLocation(string endpointConfigurationName, string requestMessageFolder, string requestMessageName, MessageVersion messageVersion, SessionMode sessionMode) { ChannelFactory<IRequestChannel> channelFactory = null; IRequestChannel channel = null; XmlTextReader xmlTextReader = null; Message requestMessage = null; Message responseMessage = null; try { channelFactory = new ChannelFactory<IRequestChannel>(endpointConfigurationName); channelFactory.Endpoint.Contract.SessionMode = sessionMode; channel = channelFactory.CreateChannel(); string path = Path.Combine(requestMessageFolder, requestMessageName); xmlTextReader = new XmlTextReader(path); requestMessage = Message.CreateMessage(messageVersion, Star, xmlTextReader); TestContext.BeginTimer(requestMessageName); responseMessage = channel.Request(requestMessage); channel.Close(); channelFactory.Close(); } catch (FaultException ex) { HandleException(ref channelFactory, ref channel, ex); throw; } catch (CommunicationException ex) { HandleException(ref channelFactory, ref channel, ex); throw; } catch (TimeoutException ex) { HandleException(ref channelFactory, ref channel, ex); throw; } catch (Exception ex) { HandleException(ref channelFactory, ref channel, ex); throw; } finally { TestContext.EndTimer(requestMessageName); CloseObjects(channelFactory, channel, xmlTextReader, requestMessage, responseMessage); } } private void HandleException(ref ChannelFactory<IRequestChannel> channelFactory, ref IRequestChannel channel, Exception ex) { try { if (channelFactory != null) { channelFactory.Abort(); channelFactory = null; } if (channel != null) { channel.Abort(); channel = null; } Trace.WriteLine(ex.Message); EventLog.WriteEntry(Source, ex.Message, EventLogEntryType.Error); } catch (Exception) { } } private void CloseObjects(ChannelFactory<IRequestChannel> channelFactory, IRequestChannel channel, XmlTextReader xmlTextReader, Message requestMessage, Message responseMessage) { try { if (channelFactory != null) { channelFactory.Close(); } if (channel != null) { channel.Close(); } if (xmlTextReader != null) { xmlTextReader.Close(); } if (requestMessage != null) { requestMessage.Close(); } if (responseMessage != null) { responseMessage.Close(); } } catch (Exception) { } } #endregion } }
Each test method corresponds to a separate Unit Test. Visual Studio permits to create a Load Test as a composition of one or multiple unit tests. Explaining in detail how creating a Load Test is out of the scope of the present post. This process is fully documented on MSDN at the following link: Walkthrough: Creating and Running a Load Test. However, for the sake of the completeness I’m going to describe the steps necessary to create a Load Test based on a single Unit Test:
BizTalk Server performance counters \\.\BizTalk:FILE Receive Adapter(*)\* \\.\BizTalk:FILE Send Adapter(*)\* \\.\BizTalk:FTP Receive Adapter(*)\* \\.\BizTalk:FTP Send Adapter(*)\* \\.\BizTalk:HTTP Receive Adapter(*)\* \\.\BizTalk:HTTP Send Adapter(*)\* \\.\BizTalk:Message Agent(*)\* \\.\BizTalk:Message Box:General Counters(*)\* \\.\BizTalk:Message Box:Host Counters(*)\* \\.\BizTalk:Messaging Latency(*)\* \\.\BizTalk:Messaging(*)\* \\.\BizTalk:MSMQ Receive Adapter(*)\* \\.\BizTalk:MSMQ Send Adapter(*)\* \\.\BizTalk:POP3 Receive Adapter(*)\* \\.\BizTalk:SMTP Send Adapter(*)\* \\.\BizTalk:SOAP Receive Adapter(*)\* \\.\BizTalk:SOAP Send Adapter(*)\* \\.\BizTalk:SQL Receive Adapter(*)\* \\.\BizTalk:SQL Send Adapter(*)\* \\.\BizTalk:TDDS(*)\* \\.\BizTalk:Windows SharePoint Services Adapter(*)\* \\.\XLANG/s Orchestrations(*)\*
WCF performance counters \\.\ServiceModelService 3.0.0.0 \\.\ServiceModelEndpoint 3.0.0.0 \\.\ServiceModelOperation 3.0.0.0 .NET performance counters \\.\.NET CLR Exceptions(*)\* \\.\.NET CLR Memory(*)\*
System performance counters \\.\Cache\* \\.\Distributed Transaction Coordinator\* \\.\Enterprise SSO(*)\* \\.\LogicalDisk(*)\* \\.\Memory\* \\.\Network Interface(*)\* \\.\Paging File(*)\* \\.\PhysicalDisk(*)\* \\.\Process(*)\* \\.\Processor(*)\* \\.\System\* \\.\TCPv4\* \\.\IPv4\*
SQL Server performance counters \\.\MSSQL$BTS:Access Methods\* \\.\MSSQL$BTS:Buffer Manager\* \\.\MSSQL$BTS:Buffer Node(*)\* \\.\MSSQL$BTS:Buffer Partition(*)\* \\.\MSSQL$BTS:Databases(*)\* \\.\MSSQL$BTS:General Statistics\* \\.\MSSQL$BTS:Latches\* \\.\MSSQL$BTS:Locks(*)\* \\.\MSSQL$BTS:Memory Manager\* \\.\MSSQL$BTS:SQL Statistics\* \\.\MSSQL$BTS:Transactions\* \\.\MSSQL$BTS:Wait Statistics(*)\* \\.\MSSQL$BTS:Plan Cache\* System performance counters \\.\Objects\* \\.\Memory\* \\.\Paging File(*)\* \\.\PhysicalDisk(*)\* \\.\Process(*)\* \\.\Processor(*)\* \\.\Network Interface(*)\* \\.\TCPV4(*)\* \\.\LogicalDisk(*)\* \\.\System\*
SQL Server performance counters \\.\MSSQL$BTS:Access Methods\* \\.\MSSQL$BTS:Buffer Manager\* \\.\MSSQL$BTS:Buffer Node(*)\* \\.\MSSQL$BTS:Buffer Partition(*)\* \\.\MSSQL$BTS:Databases(*)\* \\.\MSSQL$BTS:General Statistics\* \\.\MSSQL$BTS:Latches\* \\.\MSSQL$BTS:Locks(*)\* \\.\MSSQL$BTS:Memory Manager\* \\.\MSSQL$BTS:SQL Statistics\* \\.\MSSQL$BTS:Transactions\* \\.\MSSQL$BTS:Wait Statistics(*)\* \\.\MSSQL$BTS:Plan Cache\*
System performance counters \\.\Objects\* \\.\Memory\* \\.\Paging File(*)\* \\.\PhysicalDisk(*)\* \\.\Process(*)\* \\.\Processor(*)\* \\.\Network Interface(*)\* \\.\TCPV4(*)\* \\.\LogicalDisk(*)\* \\.\System\*
Latency:
Click the OK button to confirm. See How to: Add a Threshold Rule for more information on this topic
At runtime, threshold violations can be reviewed clicking the threshold violation link under the Load Test toolbar. If the counter in question is exposed by a Graph, each threshold violations is signaled with a yellow triangle.
At this point I’m ready to launch a load test, so I can just press the start button. In order to create a custom Graph to monitor most relevant counters I proceed as follows:
and any custom databases used by the application.
When the load test is over, you can review the results on the Summary Page:
You can also click the links in the Test Results and Transaction Results tables to navigate to a page where you can review more in detail test results, errors and threshold violations.
Visual Studio Test Edition is a powerful suite to rapidly build Unit and Load Tests to conduct performance and stress tests against your BizTalk application. We assumed that the application in question was exposed via a WCF Two-Way Receive Location, but the approach exposed in the present post can be customized and extended to other scenarios where the application makes use of other Adapter to expose its services. Here you can download the Test Project I used to conduct tests against the HandleXLANGMessages application. See Part1 and Part2 for more information.
Wow!! This is excellent. I am going to use this right now. I have been using Loadgen up until now.