Some broker implementations require creating a copy the message forwarding it over to the backend. The broker also might slightly modify things like addressing headers etc. on the message for proper message routing within the DMZ. The problem is that we see a very high CPU cost in creating this copy message and this also results in lower throughput. Note: Streamed transfer mode is not in scope for this article.
For all performance issues we need to measure and profile and to investigate this issue we initially try to simulate the pattern of the broker by just copying over the message and then forwarding it to a backend dummy service. We then take profiles of this to understand how much the actual cost of copying is.
21,530
8.5
0.1
0.29
0.03
An 8% cost for copying seems to be acceptable considering the value of making the copy and able to do other things if required. But then again this was not what is being observed. In the profiles from the actual broker we notice about 40% cost for creating a copy. This means that almost half the time is spent in creating a message copy. So effectively your throughput would almost drop to half when the broker is configured to create a copy of the message. This is excluding costs like logging etc. Evidently our simulation is not accurate so we need to isolate this further. We take in more functionality from the broker so that we hit this expensive path. One of the key observations was that the message is copied just before it is being forwarded. This also means that there are a bunch of manipulations that was done on the message and in our simulation we didn't perform any manipulation. So to get this closer we need to probably change some things on the message. To keep it simple we did something like removing some header and adding another header to the message since most brokers modify headers before forwarding it over.
int headerIndex = input.Headers.FindHeader(header.Name, header.Namespace);
if (headerIndex >= 0)
{
input.Headers.RemoveAt(headerIndex);
}
input.Headers.Add(header);
Eureka!! We observed our throughput went down and this was in line with what we were seeing in our broker. So we can see that CreateMessage and CreateBufferedCopy have increased in cost quite a bit.
12,782
14.83
0.08
27.69
0.02
So this was performance data we collected.
Copy and Forward
Copy forwarding with new header
98.6 %
11317.6545777148
98.7 %
6854.97017102
Now that we have identified the root cause we also need to identify the solution so that the broker can achieve the functionality without taking up so much CPU.
The solution is actually quite simple "Modify your message after you create the buffered copy". I wanted give the solution before the analysis since most of you would probably not be interested in the analysis but if you are then the rest would be interesting.
The most common way to create a copy your message is using Message.CreateBufferedCopy(int).
If headers have been modified then CreateBufferedMessage takes an alternative path using the DefaultMessageBuffer. The reason is that a fully copy of the message has to be created if any buffered header has been modified. An internal property called headers.ContainsOnlyBufferedMessageHeaders is used to distinguish if the faster BufferedMessageBuffer can be used to create the buffered copy or not. If there are any modified headers then this means we need to assure that the message is fully marshaled over and the buffer itself cannot be copied(e.g. the user can add a reference type to the header) and so we fall back to a path that would fully reparse the message and create a fully deserialized copy of the modified message. The main point here is a copy should always be a deep copy and any kind of modification should not result in a message with shallow copied message parts. When you copy and create a message from the original then your message objects get its own copy of headers that it can play around with without affecting the original incoming message. Message copy by itself is a fast operation as you can see from the above profile and copying a modified message can be very CPU intensive when using buffered transfer mode.
For greater flexibility our router can be something like a pass through router. If we are just calling a backend service then we can use a generic contract to receive and forward messages to the back end service as shown below.
Here we create a copy of the message to consume locally on the broker incase we want to validate some parts of the message or log etc. Ideally the fastest would be to just directly forward it over but application sometimes require all incoming messages to be logged or validated at the entry point of the DMZ.
Next – How to Optimize Message Copying using CreateBufferedCopy?
I use the term broker and router very loosely here since they follow very similar guidelines as described here - WCF Broker Overview. Apologies for not being very rigid with these terms.
I will dive into best practices of building a router by progressing from a very simple implementation to a robust one through different scenarios and varying degrees of complexities.
The easiest implementation is by using a strongly typed contract with message forwarding as shown below.
[ServiceContract] public interface IOrderService { [OperationContract] Order[] GetOrders(int numOrders); } class OrderService:IOrderService { Order[] GetOrders(int numOrders) { return backendProxy.GetOrders(numOrders); } }
Next – Router Implementation – Message Forwarding – Copy/Pass through
A broker is usually a central point for message forwarding and pass-through for clients and backend services. There are many types of brokers that come into mind
But they mostly fall into the below basic model unless the client manages to bypass the broker with a P2P communication with the backend itself.
Router Implementation – Strong Typed with Message Forwarding
Aug’5’10
Here are 2 really good articles on MSDN that I would recommend for the functional aspects to architect your your router.