ASP.NET - Network Proxy Server Farms Causing Loss of Session

By Robbe D. Morris

Printer Friendly Version

Robbe Morris
Robbe & Melisa Morris
  Download Source Code
More and more companies are having their outgoing network traffic routed through network proxy server farms.  Essentially, these proxy server farms are designed to hide the real ip address of the visitor from the web server.  Thus, the network load balancer has the possibility of routing subsequent requests to other web servers in the farm which don't have access to that session data.  So, if you were trying to figure out why quite a few of your users were intermittently losing session for no apparent reason even though you have session affinity enabled, this is likely the cause.  You may even see this in the business to consumer market since some ISPs route their traffic through these farms as well.
I recently ran across a company called ScaleOut Software who offer an interesting alternative to managing session data in a database or in a single, separate point of failure, ASP.NET StateServer.  The product is called ScaleOut StateServer.  Before we go any further, I'd like to mention that my commentary on this product is based on actual usage of the software and my desire to help others resolve the proxy server problem.  I have not received any free/discounted software or payment in conjunction with this article.
 
What does it do and how does it do it?
 
ScaleOut StateServer provides you with two key pieces of functionality.
 
1.The primary functionality it provides is in-memory session data replication across all of the web servers in your farm.  It deterministically decides which servers to provide redundant copies of your session objects.  It is sort of like having a chain of retail stores who all have access to the exact same inventory.  No matter what store the customer walks into, the item they want is always there.
2.This next piece of functionality is the real icing on the cake.  ScaleOut StateServer has an API which enables you to insert, update, retrieve, and delete objects from the StateServer farm.  And, I don't just mean for a specific session for a specific user.  The object in the store is available to all sessions, all application domains, and across all servers.  Yes, I do mean across all application domains.  Let that sink in for a moment and you'll start to come up with all sorts of innovative uses for this capability.
By default, the API documentation can be found in C:\Program Files\ScaleOut_Software\StateServer\SOSS_DotNetAPI.chm (hint: read the sections on asynchronous support to improve performance) or online at: ScaleOut StateServer API Documentation.  I worked up a code sample to test this capability on our farm across three application domains (three entirely separate web sites).  It checks the store looking for a specific object with the designated key (provided as a public property of the Tester.cs class).  If the object doesn't exist, I load up some test data in an ArrayList and write out a message to the browser saying the data was loaded.  The sample also retrieves the object from the store using the same object key and iterates through the results.  This functionality is contained in the soss.aspx page.  A sample call to delete the designated object based on object key is available in the sossdelete.aspx page.
You'll want to review the contents of the Tester.cs class prior to running this sample on your farm.  Feel free to use the same Tester.SossKey.  However, you'll need to manually adjust your domains in the Tester.AlternateServer() method.  This is just a quick and dirty way to provide you with a link to another server/domain on the farm so you can watch ScaleOut StateServer work its magic.  You may or may not need to modify the Tester.GetNextLink() method which returns the full url for testing.
I opted to modify the real web.config file on our farm to enable ScaleOut StateServer rather than copy the web.config file included in this sample.
 
If you run your web applications on a web farm, I'd urge you to review the Performance Tests (bottom of page) and download the evaluation copy as soon as possible.  The software is moderately priced and, in my humble opinion, well worth the money.


Installation Steps
Install the software.
Launch the StateServer console and click the Host Configuration tab.
Select the ip address for your specific server from the network interface dropdown.  This should not be the load balancing server itself.  In most cases, you can leave the default settings for the other items.
Check the radio button to Join on Start.  Click Apply.In a few moments, you'll see this server added to the object store.
Set your ASP.NET session to run in InProc mode via the web.config file.  Next, add the specified http module tag to your web.config (as specified in the help documentation under Installation\Installation Steps.
We'll need to address the machine.config and the machineKey settings.  You must specify the same key for all servers in the farm if you have not already done so.  This includes the validationKey and decryptionKey.  You can't just use the default settings to auto generate.  This article from our own Peter Bromberg explains this aspect of the machine.config file in much more detail: Generate Machine Key Elements for Web Farm .
Repeat the steps above for each server in the farm.  The object store should be smart enough to pick up the license key from other servers in the store.  So, you should only need to enter the license key once.
 
soss.aspx.cs

 private void Page_Load(object sender, System.EventArgs e)
 {
  
  try
  {

    string newlink = Tester.GetNextLink(Tester.GetServerVariable("SERVER_NAME"));

    ArrayList mydata = (ArrayList)Tester.GetDataFromMemoryStore(Tester.SossKey);
  
    if (mydata == null)
    {
      Tester.RW("Application loaded new data from ");
      Tester.SetSession("stringtest",(object)Tester.GetServerVariable("LOCAL_ADDR"));
      Tester.RW(Tester.GetServerVariable("SERVER_NAME"));
      mydata = Tester.LoadData(0);
      Tester.AddDataToMemoryStore(Tester.SossKey,(object)mydata);  
    }

    Tester.RW("Session ID:  " + Session.SessionID);
    Tester.RW("Server:   " + Tester.GetServerVariable("SERVER_NAME"));
    Tester.RW("String:  " + Tester.GetSession("stringtest").ToString());
    Tester.RW("<br><br>" + newlink);
    Tester.RW("");

    mydata = (ArrayList)Tester.GetDataFromMemoryStore(Tester.SossKey); 

    for(int i=0;i<mydata.Count;i++)
    {
       Tester.RW(mydata[i].ToString());
    }

  }
  catch (Exception err) { Tester.RW(err.Message); } 
		     
 }

Tester.cs

 using System;
 using System.IO;
 using System.Xml;
 using System.Collections;
 using System.Runtime.Serialization;
 using Soss.Client;


 namespace ScaleOutServer
 {
 
   public class Tester
   {
 
     public static string SossKey = "acad1f90-cb97-4d26-8ede-6ff83ec8eb9c";

     public Tester() { }
	 
     public static string GetNextLink(string domain)
     {
       string newdomain = AlternateServer(domain); 
       return "<a href=http://" + newdomain + "/soss.aspx >" + newdomain + "</a>";
     }
 
     public static string AlternateServer(string domain)
     {
          
       switch (domain)
       {
          case "mydomain1.com":
                domain = "mydomain2.com";
                break;
          default:
                domain = "mydomain1.com";
                break;
       }
           
       return domain;
    }
 
    public static ArrayList LoadData(int startCount)
    {
       ArrayList ret = new ArrayList();
       for(int i=startCount;i<(startCount + 5);i++)
       {
          ret.Add("this is item " + i.ToString());
       }
       return ret;
    }
 

 
    public static void AddDataToMemoryStore(string key,object dataToStore)
    {
       try
       {

         Soss.Client.DataAccessor da = new Soss.Client.DataAccessor(new Guid(key));
             
         if (da.Read(false) == null)
         {
           da.Create(DataAccessor.InfiniteTimeout, dataToStore);
         }
         else
         {
           da.Update(dataToStore);
         }

       }
       catch (Exception) { throw; }
    }
  

 
    public static object GetDataFromMemoryStore(string key)
    {

       try
       {

         Soss.Client.DataAccessor da = new Soss.Client.DataAccessor(new Guid(key));

         return  da.ReadObject();
             
       }
       catch  { }
       return null;
    }
 

  
    public static void RemoveDataFromMemoryStore(string key)
    {

       try
       {

         Soss.Client.DataAccessor da = new Soss.Client.DataAccessor(new Guid(key));
             
         if (da.Read(false) != null)
         {
           da.Delete();
         }

       }
       catch (Exception) { throw; }
    }
    

    public static object GetSession(string elementName)
    {

       object Ret=null;

       try
       {

         if (System.Web.HttpContext.Current.Session[elementName] != null)
         {
            Ret = System.Web.HttpContext.Current.Session[elementName];
         }
         else
         {
           Ret = (object)"";
         }
       }
       catch  { }
       return Ret;
    }
        

    public static void SetSession(string elementName,object val)
    {
        try
        {
          System.Web.HttpContext.Current.Session[elementName] = val;
        }
        catch  { }
        return;
    }
       

    public static string GetServerVariable(string VariableName)
    {

       string Ret="";
       try
       {
          Ret = System.Web.HttpContext.Current.Request.ServerVariables[VariableName].ToString().ToLower();   
       }
       catch { }
       return Ret; 
    }
 
    public static void RW(string html)
    {
      System.Web.HttpContext.Current.Response.Write(html + "<br>\n");   
    }
 
   }
 }
 

Robbe has been a Microsoft MVP in C# since 2004.  He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.