다음을 통해 공유


Building Your Dynamic Router on AppFabric Cache

I’ve seen a lot of services routing solutions. Some used expensive hardware. Some were built on top of integration or ESB platforms and others were simply built from the ground up. <o:p></o:p>

<o:p> </o:p>Building from the ground up has always been the most flexible method with the obvious drawback that it was also potentially the most difficult, least maintainable and ultimately expensive choice. But, after reconsidering the options and the new features introduced in .NET 4.0 and AppFabric I started to doubt that’s the case any longer. <o:p></o:p>

<o:p> </o:p>I tested my hunch by building a prototype utilizing:<o:p></o:p>

· WCF Routing Service <o:p></o:p>

· AppFabric Cache <o:p></o:p>

· Entity Framework <o:p></o:p>

· AppFabric Hosting <o:p></o:p>

· ASP.NET Dynamic Data Entities Web Application template.<o:p></o:p>

<o:p> </o:p>The reason there’s so many technologies in the prototype is I didn’t want to build the typical standalone console app. Instead, I wanted to convince myself that the administration and deployment aspects didn’t materially impact the viability of a custom solution.<o:p></o:p>

<o:p> </o:p>

<o:p></o:p>Before jumping right into the bits though, I’ll address a basic question.<o:p></o:p>

Why Would Anyone Want This?

The quick answer to the general question is that the reason products exist today that are either dedicated to this problem or offer it as a part of their SOA offering is that a single endpoint that can service many different types of requests turns a many-to-many problem into a one-to-many problem and that’s just easier to deploy and manage. It frees the IT professional to concentrate on “one side” of the clients and services they manage because the clients are required to access the target services via a consistent address, security and protocol. This then frees the IT professional to move services, add services and bridge protocols as needed without impacting the client.

A more specific answer that directly applies to the subject of this blog and this prototype is -Why use AppFabric Cache for this? I chose AppFabric Cache for several reasons most notably:

· Scalability and HA

· Performance

· Notifications

· Ease of use

These features combine to give me a reliable platform, the ability to update the router’s configuration without explicit polling and to minimize access to the database that serves as our persistent store for routing configuration. Also, if for some heretical or insane reason I kept my persistent routing data in something other than SQL Server (I suppose SQL Azure would be an acceptable alternative) the impact is minimal. Finally, I also think in the future developers are going to use distributed memory stores more and more for what they use the file system for now especially when it comes to flexible configuration but that’s an entire topic in and of itself!

That's enough theory for now. Let’s dig in!

The Architecture

The prototype’s architecture is straightforward. The routing configuration is persisted to the database and AppFabric Cache simultaneously. When the routing information is updated a callback fires in the router and a new RoutingConfiguration is applied.

This architecture persists routing information so the router can use the persisted information to build its routing table after a reboot of the server.

Building the Router

The Router uses a simple service behavior to register the ServiceHost with an UpdateManager class that in turn will manage the App Fabric Cache Notifications.

public class CacheDrivenRoutingBehavior:BehaviorExtensionElement,IServiceBehavior

{

public override Type BehaviorType

{

get { return typeof(CacheDrivenRoutingBehavior); }

}

protected override object CreateBehavior()

{

return new CacheDrivenRoutingBehavior();

}

public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)

{

}

public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)

{

serviceHostBase.Extensions.Add(new CacheDrivenRoutingHostExtension());

}

public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)

{

}

class CacheDrivenRoutingHostExtension : IExtension<ServiceHostBase>, IDisposable

{

private bool _disposed;

private UpdateManager _routeManager;

public CacheDrivenRoutingHostExtension()

{

_disposed = false;

}

void IExtension<ServiceHostBase>.Attach(ServiceHostBase owner)

{

_routeManager = new UpdateManager(owner);

_routeManager.UpdateRoutes();

}

void IExtension<ServiceHostBase>.Detach(ServiceHostBase owner)

{

Dispose();

}

private void Dispose(bool disposing)

{

if (!_disposed)

{

if (disposing)

{

if (_routeManager != null)

{

_routeManager.Dispose();

}

}

_disposed = true;

_routeManager = null;

}

}

public void Dispose()

{

Dispose(true);

GC.SuppressFinalize(this);

}

}

The UpdateManager

The CacheDrivenRoutingBehavior contains an instance of the UpdateManager class. This class has the responsibility for monitoring cache events and for rebuilding the RoutingConfiguration . The heart of the UpdateManager is its callback.

private void RoutingTableUpdatedCallback(

string cacheName,

string regionName,

string key,

DataCacheItemVersion version,

DataCacheOperations cacheOperation,

DataCacheNotificationDescriptor nd)

{

Dictionary <string, MessageFilter> filters = (Dictionary<string, MessageFilter>)_cache.Get(_routingEntriesKey);

var rtConfig = new RoutingConfiguration();

rtConfig.RouteOnHeadersOnly = false;

rtConfig.SoapProcessingEnabled = true;

foreach (string address in filters.Keys)

{

var ep = new ServiceEndpoint(

ContractDescription.GetContract(typeof(IRequestReplyRouter)),

new BasicHttpBinding(), // a production router would likely drive this dynamically as well as the message filters and endpoint address.

new EndpointAddress(address));

rtConfig.FilterTable.Add(filters[address], new List<ServiceEndpoint> { ep } );

}

_serviceHost.Extensions.Find<RoutingExtension>().ApplyConfiguration(rtConfig);

}

The callback simply retrieves a structure from cache that contains a collection that maps an endpoint address to its corresponding MessageFilter. As noted in the comments, the destination binding is inflexible in the prototype. In a production implementation you would likely drive those choices dynamically. A RoutingConfiguration should always be swapped whole rather than partially updated.

Our mapping is contained in a single cache item that gets replaced in its entirety so it was sufficient to use a simple strategy when we registered the callback

_cache.AddItemLevelCallback(_routingEntriesKey, DataCacheOperations.ReplaceItem, RoutingTableUpdatedCallback);

Can I Get a Little Help Here?

You might have noticed the line _routeManager.UpdateRoutes(); in the code above. What that method does is in turn invoke a helper class’s method that hydrates the cache from routes that are persisted in SQL Server. The routes are represented and managed at runtime by Entity Framework objects. This allows the service to survive cache restart.

In the Router itself, the method called only on startup in the prototype because I wanted everything driven from cache. In a production system you could use it as a backup should the cache fail for whatever reason. The helper class is called CacheUtil.

public static class CacheUtil

{

private static DataCacheFactory factory;

static CacheUtil()

{

factory = new DataCacheFactory();

}

/// <summary>

/// Method which reads routes persisted to SQL Server and then updates the cache

/// </summary>

public static void UpdateCache()

{

var cache = factory.GetCache(ConfigurationManager.AppSettings[cacheKey]);

var filters = new Dictionary<string, MessageFilter>();

using (var ctx = new RoutingManagerEntities())

{

var actionFilters = ctx.ActionFilters;

foreach (var actionFilter in actionFilters)

{

filters.Add (actionFilter.Address, new ActionMessageFilter(actionFilter.SoapAction));

}

foreach (var messageFilter in ctx.XPathFilters)

{

if (messageFilter.XPathFilterNamespaces.Count == 0)

{

filters.Add(messageFilter.Address, new XPathMessageFilter(messageFilter.XPath));

}

else

{

var nt = new NameTable();

var mgr = new XmlNamespaceManager(nt);

foreach (var ns in messageFilter.XPathFilterNamespaces)

{

mgr.AddNamespace(ns.Prefix, ns.Namespace);

}

filters.Add (messageFilter.Address, new XPathMessageFilter(messageFilter.XPath,mgr));

}

}

}

cache.Put(ConfigurationManager.AppSettings[itemKey], filters);

}

We’ll also use CacheUtil in the GUI later but for now let’s get the Router ready to run.

Hosting and Configuring the Router

One of the things I wanted to accomplish with the prototype was allowing it to be managed by the AppFabric tools in IIS Manager. This was difficult for me because when I create a service that depends on things starting up right away I like to use the old standby of creating a host that can run either as a Windows Service or in the console so I can have full control of the lifecycle and debug easily…

In the past this was fine because it could be argued that while deploying a separate service was a pain it was necessary for anything that needed to start right away. That’s not really true any longer. So, I took the plunge and used Auto-Start. Before using Auto-Start, you should review the information here https://msdn.microsoft.com/en-us/library/ee677285.aspx . You may be tempted to skim. Don’t!

Now that I had it configured for Auto-Start all I had left was creating a simple factory so I had control of my start up.

public class RouterFactory : ServiceHostFactory

{

protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)

{

var retVal = new ServiceHost(serviceType, baseAddresses);

retVal.Description.Behaviors.Add(

new RoutingBehavior(new RoutingConfiguration()));

retVal.Description.Behaviors.Add(new CacheDrivenRoutingBehavior());

return retVal;

}

}

Ok we have a Router. But, if we start it up it’s not going to work because we don’t have a cache!

Setting up the Cache

Assuming you’ve installed the AppFabric Caching Feature and have ensured it is running then all that’s left to do is to set up and configure the cache. Use the PowerShell console to run New-Cache. For the prototype I used:

New-Cache -CacheName DynamicRouting -NotificationsEnabled true –Eviction None –Expirable false

Eviction and Expirable are set to false because it is unlikely a routing table- even one that contains many services- is going to stress memory on today’s hardware. The goal here is to minimize the likelihood of having to refill the cache unnecessarily from the persistent store thereby maximizing performance.

A cache of a routing table is not particularly sensitive data so encryption overhead also was not desired. For the prototype Set-CacheClusterSecurity was invoked as:

Set-CacheClusterSecurity –SecurityMode None –ProtectionLevel None

Now we’re ready to build the GUI.

“Building The GUI”

The noted philosopher Harry Callahan once said “A man’s got to know his limitations”. Words to live by!

I’m unashamedly GUI challenged and If I had tried to build a respectable GUI from scratch that wasn’t simply a giant column of input tags then I’d need some help or this would take a while. It’s not that I haven’t done it before it’s just that it takes me way longer than it does someone who lives in that stuff..

So I punted and used the ASP.NET Dynamic Data Entities Web Application template and a touch of Entity Framework and Huzzah! a GUI!

All it took was installing the template and dropping this in global.asax.cs…

DefaultModel.RegisterContext(

typeof(RoutingManagerEntities),

new ContextConfiguration()

{ ScaffoldAllTables = true });

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")

{

Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),

Model = DefaultModel

});

Then strategically inserting CacheUtil.UpdateCache() whenever CRUD occurred and it was time to amuse myself in the debugger for longer than is decent watching my new Insta-GUI updates cause my callback to fire in my Router.

It’s hard to beat that for a Friday night of fun!

Ok. So, for the hardcore GUI ninja that’s just not going to cut it but that wasn’t the point of the exercise. What I wanted to do was convince myself was that it was a low barrier to entry to create a useable GUI to drive a custom router and I’d have to say I’m pretty sure someone with actual GUI skills could make something happen pretty quickly.

Wrapping Up

At this point I’m convinced that building a manageable and scalable router from “scratch” would not be an untenable undertaking given all the power provided by .NET 4.0 and AppFabric. Of course, I’m always willing to listen to other viewpoints so if you disagree please feel free to share your concerns.

Source code for this prototype is available here