Blog

.NET on Pivotal CF: Scaling an App on Diego

Raman Yurkin

net-on-pcf-scaling-diego-v1

Previously, we deployed a .NET app to Diego on Pivotal CF and bound it to a MS SQL service. However, hardly any cloud app exists as a single instance. So, in this post, we will scale the demo app horizontally and test how it works on multiple instances.

 

Checking application scale settings

First of all, let’s view the current scale settings with the cf scale command:

> cf scale SampleWebApp
Showing current scale of app SampleWebApp in org altoros / space dev as example@altoros.com...
OK
memory: 1G
disk: 1G
instances: 1
>

As you can see, we can configure the number of application instances and instance size.

For testing purposes, I have added one more view and controller action to our demo app (the source code can be found in this repo). The action puts the number of current instances into the session variable, so we can find out if the session is shared by all instances or not.

After we have published the updated version of SampleWebApp into Pivotal CF, we can check the results:

net-on-pivotalcf-diego-scaling-session-test

Apparently, there is only one instance of the application.

 

Scaling the demo app

I’m going to increase the number of instances to three and view the result:

> cf scale SampleWebapp -i 3
Scaling app SampleWebApp in org altoros / space dev as altoros...
OK
> cf scale SampleWebapp
Showing current scale of app SampleWebApp in org altoros / space dev as altoros...
OK
memory: 1G
disk: 1G
instances: 3
>

If we refresh the browser a few times, we will see this:

net-on-pivotalcf-scaling-diego-session-tests

Apparently—each time—the application is working as a separate instance that knows nothing about the other ones. To handle this issue, we need to provide common session and state storage.

 

Adding a session state storage

Session state can be saved in a memory cache solution or some database / custom session storage (e.g., Redis or Memcache). For this small project, I prefer Redis, an open source cache / storage, which is available as a service on Pivotal CF. To enable it, I will use Microsoft Redis Session State Provider.

To create a Redis service and bind it to the application:

  1. Check what services are available in the marketplace.

    > cf marketplace
    Getting services from marketplace in org altoros / space dev as example@altoros.com...
    OK
    service      plans  		       	   description
    p-redis      dedicated-vm, shared-vm       Redis service to provide a key-value store
    TIP:  Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.
    >

  2. Create a service instance from the marketplace.

    > cf create-service p-redis shared-vm sampleredisservice
    Creating service instance sampleredisservice in org altoros / space dev as example@altoros.com...
    OK
    >

  3. Bind the service instance to the application.

    > cf bind-service samplewebapp sampleredisservice
    Binding service sampleredisservice to app SampleWebApp in org altoros / space dev as example@altoros.com...
    OK
    TIP: Use 'cf.exe restage SampleWebApp' to ensure your env variable changes take effect
    >

The Pivotal CF part is ready.

 

Modifying the demo app

We still need to modify our .NET app. Let’s add Redis Session Store Provider, using the NuGet package manager.

PM> Install-Package Microsoft.Web.RedisSessionStateProvider

After that, we will enable the app to receive Redis connection strings from the application environment.

  1. In Web.config:

    <sessionstate mode="Custom" customProvider="MySessionStateStore">
      	<providers>
        	<add name="MySessionStateStore" type="Microsoft.Web.Redis.RedisSessionStateProvider" host="" accessKey="" ssl="true" settingsClassName="SampleWebApp.Startup" settingsMethodName="GetRedisConnectionString"></add>
      	</providers>
    </sessionstate>

  2. In Startup.cs:

    public partial class Startup
    	{
     
    …
     
     
        	public static string GetRedisConnectionString()
        	{
            	var settings = ParseVCAP();
           	 ConfigurationOptions config = new ConfigurationOptions
            	{
                	EndPoints =
                    	{
                        	{ settings.RedisHost, int.Parse(settings.RedisPort)}
                    	},
                	CommandMap = CommandMap.Create(new HashSet
                	{ // EXCLUDE a few commands
                    	/*"INFO", "CONFIG", "CLUSTER",
                    	"PING", "ECHO", "CLIENT"*/
                	}, available: false),
                	KeepAlive = 180,
                	DefaultVersion = new Version(2, 8, 8),
                	Password = settings.RedisPassword
            	};
     
            	return config.ToString();
        	}
     
        	public static Credentials ParseVCAP()
        	{
            	Credentials result = new Credentials();
            	const string dbServiceName = "mssql-dev";
            	const string redisServiceName = "p-redis";
            	string vcapServices = Environment.GetEnvironmentVariable("VCAP_SERVICES");
     
            	// if we are in the cloud and DB service was bound successfully...
            	if (vcapServices != null)
            	{
                	dynamic json = JsonConvert.DeserializeObject(vcapServices);
                	foreach (dynamic obj in json.Children())
                	{
     
                        switch (((string)obj.Name).ToLowerInvariant())
                        {
                        case dbServiceName:
                              {
                              dynamic credentials = (((JProperty)obj).Value[0] as dynamic).credentials;
                              result.DBHost = credentials?.host;
                              result.DBPort = credentials?.port;
                              result.DBUID = credentials?.username;
                              result.DBPassword = credentials?.password;
                              result.DBConnectionString = credentials?.connectionString;
                              }
                              break;
     
                        case redisServiceName:
              	          {
                              dynamic credentials = (((JProperty)obj).Value[0] as dynamic).credentials;
                              result.RedisHost = credentials?.host;
                              result.RedisPort = credentials?.port;
                              result.RedisPassword = credentials?.password;
                              }
                              break;
     
                        default:
                              break;
                        }
     
               	 }
            	}
            	return result;
        	}
     
    	}
     
    	public struct Credentials
    	{
        	public string DBUID;
        	public string DBHost;
        	public string DBPort;
        	public string DBPassword;
        	public string DBConnectionString;
        	public string RedisHost;
        	public string RedisPort;
        	public string RedisPassword;
    	}

Now we can read Redis settings from an environment variable and put the prepared connection string into the Redis session storage provider.

 

CF push!

Finally, we can publish the app to Pivotal CF and check the result:

cf push SampleWebApp -p .\ -s windows2012R2 --no-start -b
cf enable-diego SampleWebApp
cf start SampleWebApp

net-on-pvotal-cf-scaling-diego-session-test

As you have probably noticed, all the session data is shared between instances. This means the application is working well.


Related reading


About the author: Roman Yurkin is Solutions Architect at Altoros. It is hard to list all the programming languages and technologies he has worked with over the last 20 years, but, today, his main focus areas are the .NET stack, Microsoft Azure, and PaaS.


For the next parts of this series, subscribe to our blog or follow @altoros.

Get new posts right in your inbox!

No Comments

Benchmarks and Research

Subscribe to new posts

Get new posts right in your inbox!