Share via


Web Farms

Use Data Caching Techniques to Boost Performance and Ensure Synchronization

David Burgett

This article assumes you're familiar with ADO.NET, XML, Visual Basic .NET

Level of Difficulty123

SUMMARY

Performance is an important concern for any application, but becomes critical when the app is a Web Service accessed by thousands of clients simultaneously. One hardware approach to distributing Web Services requests evenly is a Web farm consisting of multiple servers. Once on a Web farm, Web Service performance can be improved by using ADO.NET DataSet objects to cache frequently accessed data, reducing round-trips to the database. Here the author describes data caching in a Web farm environment and discusses how to avoid the cross-server synchronization problems inherent in this approach.

Contents

Improving Performance with Data Caching
Sharing Data with Other Servers
Creating the Refresh Web Methods
Calling the Refresh Methods
Adding New Servers
Conclusion

Performance and scalability are critical to Web Services. You must ensure not only that your Web Service always performs well, but that it will continue to do so as the user load increases. A Web Service that is lightning fast with 50 users does little good if it fails when it has to handle 500 users.

There are many ways to improve Web Service performance. One of the more common ways is to use data caching by storing DataSet objects in either a Session or Application object. This technique improves performance by accessing data in RAM, thus eliminating round-trips to the database. A DataSet object stored in the Session or Application cache provides much better data access performance than having each Web Service request pull its own data from the database.

Moving your Web Service to a Web farm can also improve performance by distributing the workload over several servers. A Web farm also provides redundancy, ensuring that your Web Service is always available even if one of your Web servers goes down. However, moving to a Web farm rules out the use of Application data caching because the Application object is unique to each Web server. Since each Web server has its own copy of the DataSet cached in memory, data will become out of sync when data on one of the Web servers is updated independently of the others. Since you have no control over which server in a Web farm receives a request, the Web Service consumer might receive different results from two consecutive Web Service calls hitting two different Web servers, as shown in Figure 1.

Figure 1 Out of Sync Data Using a Web Farm

Figure 1** Out of Sync Data Using a Web Farm **

ASP.NET has built-in techniques that are designed to allow Web sites to share Session state information. Unfortunately, this often comes at the expense of performance. Session state data can be stored in a SQL Server™ database, but this eliminates the performance benefit of using cached data since round-trips to the database are required to read the state information. State information can also be stored on a shared server running the ASP.NET state service, but this introduces a single point of failure: if the shared state server goes down, all Web servers that depend on it are rendered inactive. This eliminates the redundancy created by establishing a Web farm. Furthermore, if the data in question is being shared by all users, it makes sense to store it in the Application object rather than duplicate it in multiple Session objects.

I have described a scenario in which I want to cache data in the Application object without losing any of the performance and redundancy benefits of my Web farm or jeopardizing the integrity of my data. In this article I will describe a set of techniques that can be used to improve XML Web Service performance using application data caching in a Web farm, without sacrificing scalability or redundancy.

Improving Performance with Data Caching

Imagine a company that offers credit card authorization to customers through an XML Web Service named AuthorizeCC, developed using Visual Basic® .NET. Customers of the Web Service build their own Web site around the functionality provided by the Web Service. The company that developed the Web Service also uses it on its own Web site. Due to the nature of the functionality provided, it is critical that the Web Service is optimized for speed and is available 24×7. If the XML Web Service is unavailable, even for a short period of time, both the company and all of its customers stand to lose lots of revenue.

In order to maximize Web Service performance and provide redundancy, the company has created a small Web farm consisting of two servers, named Server1 and Server2. Both servers offer only the AuthorizeCC Web Service and are load balanced in order to provide better performance. It is critical that at least one of the Web servers is always available so that consumers always have access to the Web Service. The company plans to expand its customer base quite rapidly, which will require adding additional servers to the farm in the near future.

The AuthorizeCC Web Service contains several Web methods. Each Web Service consumer is authenticated with a user name and password combination during each Web method call. I want to cache the basic information about each of the customers since it will be used in every Web method call and will change infrequently. Customers can, however, update their password or other information at any time using another method provided by the Web Service. Any changes made to customer information must be used for all subsequent calls to any method in the AuthorizeCC service.

Several of the customers who use the Web Service employ Web farms, which makes caching my customer data in the Session object inappropriate. Since a single customer may be using many Web servers in a farm to access my XML Web Service, that customer would create many Session objects on my Web server that cache exactly the same DataSet object, leading to a lot of wasted resources. Furthermore, Session objects won't allow me to update all cached information immediately upon a change made by the customer. If a customer updates his information in one Session object, I have no way to tell the other Session objects to update their own data. I will therefore use the Application object to cache a DataSet object with information about all customers.

The code in Figure 2 creates a private subroutine named GetCachedData. This routine creates the necessary SQLConnection, SQLCommand, and SQLDataAdapter objects to fill a temporary DataSet object. I fill two tables in the DataSet, one for Customer information and one for InvalidCardNumbers. The temporary DataSet object is then placed in the Application object's dictionary with the key "CachedData".

Figure 2 Retrieve and Cache a DataSet

Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs) If Application("CachedData") Is Nothing Then GetCachedData End Sub Public Sub GetCachedData() Dim conn As New System.Data.SqlClient.SqlConnection() 'Connection to 'database Dim ds As New DataSet() 'The new dataset conn.ConnectionString = "server=localhost;database=MSDN1;uid=sa;pwd=" conn.Open() 'Get the data FillTable("SELECT * FROM Customer", "Customer", ds, conn) 'Get the InvalidCardNumbers data FillTable("SELECT * FROM InvalidCard", "InvalidCard", ds, conn) 'Get the Server Names FillTable("SELECT ServerName FROM ServerName", "ServerName", ds, conn) 'Store all objects in the Application cache Application.Item("CachedData") = ds End Sub Private Function FillTable(ByVal SQL As String, ByVal TableName As _ String, ByRef ds As DataSet, _ ByVal conn As SqlClient.SqlConnection) Dim da As SqlDataAdapter Try da = New SqlDataAdapter("SELECT ServerName FROM ServerName", conn) da.MissingSchemaAction = MissingSchemaAction.AddWithKey da.Fill(ds, TableName) Catch ex As Exception 'Handle exception here End Try End Function

Note that I only want to create and fill the temporary DataSet when it does not already exist in the Application object, so I have placed the code to do this within an If...Then construct that checks to see if the current value of Application.Item("CachedData") is nothing. When an object is stored in the Application object with the key "CachedData", the DataSet will not be recreated. There are two situations when this will occur: when the application is run for the first time and when the Application cache has been cleared due to inactivity. You can control how many minutes the application will wait without any activity happening before clearing out the Application cache by specifying a value in the Application configuration settings for the Web site.

Finally, I need this functionality to be executed prior to every method call which will need to use the cached data. Rather than placing a call to GetCachedData in each method call, I've made the call just once in the Start event handler for my Session object. This technique executes the code during the creation of every session. After the data is stored in the Application object, the only code that is executed is the very fast If...Then statement. This performance hit is usually deemed acceptable to ensure that the code is called before every method that does access the cached data.

This code could be optimized slightly by moving it to the Start event handler for the Application object. In this case, the code would not be run at all, except when the Application object has not been initialized or has timed out. This does incur an additional risk. If the DataSet object stored in the Application object is deleted or corrupted and a Web method call is made before the application times out, the code will raise an exception trying to access the nonexistent DataSet. I could write code to catch these exceptions everywhere I access the DataSet, but I opted for simplicity by ensuring that the DataSet exists before every Web method call.

The AuthorizeCC Web Service contains several Web methods that allow consumers to authorize credit cards, verify customers, and update their own information. The service must make judicious use of data caching techniques in order to provide the best performance. Each Web Service request must identify the customer making the request in order to determine the appropriate fee to charge the customer. Determining the correct data to cache is a balancing act, however. Any data cached in an Application object consumes precious memory resources and negatively impacts performance. You must ensure that the negative impact caused by the amount of data cached in memory is outweighed by the performance increase gained by reducing the total number of database calls.

For the AuthorizeCC Web Service, it makes sense to cache information about the customers who will be using the service. Customer ID and password will be cached since these will be verified in every method call. Additionally, since the most frequently used Web method will likely be AuthorizeTransaction, I will cache information on transaction dollar limits for each customer. This will be checked during each AuthorizeTransaction call to ensure that the pending transaction does not exceed the dollar limit for the customer. Finally, I have a database table filled with invalid credit card numbers that will be checked before authorizing any transaction. This will be cached as well to ensure that no invalid credit cards are authorized. By simply caching the most basic data that will be frequently used, I eliminate all database reads from the most common Web methods.

Figure 3 Data Storage

Figure 3** Data Storage **

Data such as extended customer information and sales history will not be cached due to the sheer volume of data present and their infrequent use. The Web methods will be hardcoded with the knowledge of where each type of data is stored, as shown in Figure 3, either in the database or in the Application cached DataSet, but it would be fairly trivial to make this information dynamic.

Sharing Data with Other Servers

Now that I have data cached in the Application object and the Web methods are using the cached data, saving round-trips to the database, I'll turn my attention to building a Web farm. If I add a second Web server offering the Web Service, I also introduce the possibility of data becoming out of sync. As Web method requests begin to come into the servers, each one pulls the appropriate data from the database, creates its own DataSet, and stores the DataSet in the Application object. Web methods on both machines then pull data from their own Application object and fulfill requests.

This works well until an employee at one of the companies that uses the Web Service logs into the AuthorizeCC Administration Web site and increases the maximum dollar amount that the company allows for transactions. The administration Web site calls the appropriate Web Service and the request is answered by the first Web server in my farm. The service now needs to update the value in both the database and in the cached DataSet.

The code needed to update the DataSet stored in memory can be constructed in multiple ways. The simplest technique would be to simply execute direct SQL statements against the database using the ExecuteNonQuery method of the SQLCommand object. After the database is properly updated, you can call the GetCachedData subroutine to pull the entire DataSet from the database again. This technique is straightforward and works very solidly, but does not offer the best performance. To improve performance, you should manually update the DataSet object. You can do this by finding the DataRow in question and setting the value of the appropriate item in the DataRow's Item array, as shown in Figure 4.

Figure 4 Update Database and Cached DataSet

Private Sub UpdateTransactionLimit(ByVal ID As Integer, _ ByVal NewTransLimit As Single) 'Update the database Dim SQL As String SQL = "UPDATE Customer SET TransactionLimit=" & NewTransLimit & _ " WHERE ID=" & ID.ToString() Dim conn As New System.Data.SqlClient.SqlConnection() Dim cmd As New System.Data.SqlClient.SqlCommand(SQL) conn.ConnectionString = "server=localhost;database=MSDN1;uid=sa;pwd=" conn.Open() cmd.Connection = conn cmd.ExecuteNonQuery() 'Update the dataset as well Dim dr As DataRow dr = CType(Application.Item("CachedData"), DataSet).Tables("Customer").Rows.Find(ID) dr.Item("TransactionLimit") = NewTransLimit End Sub

If you choose to update the DataSet object yourself, you still have to update the database to ensure that the changes are persisted. This can be accomplished using the same direct SQL statements I've discussed previously and included in Figure 4. In this scenario, you have still improved performance by eliminating the need to pull known data values from the database.

An alternative is to use the SQLDataAdapter to allow ADO.NET to push the changes made in the DataSet back to the database. This performs similarly to writing your own direct SQL statements because the SQLDataAdapter uses SQLCommand objects with appropriate SQL update statements set in the CommandText property.

The benefit of using a SQLDataAdapter is twofold. First, it makes it much easier to change multiple column, row, or table values in the DataSet without having to worry about updating the database with each individual value changed in the DataSet. You can make sweeping changes in the DataSet and then simply call the Update method of the SQLDataAdapter and let it figure out what has changed and needs to be updated in the database. The second benefit of the SQLDataAdapter comes when it's used in conjunction with the SQLCommandBuilder object. If you create a SQLCommandBuilder based on an existing SQLDataAdapter, the SQLCommandBuilder will create the necessary SQLCommand objects and SQL statements needed to update the database. This results in far less code for you to write and maintain.

The downside to using a SQLDataAdapter in the cached DataSet scenario is that you must either cache the SQLDataAdapter objects in the Application object or recreate them each time you update data. Both options affect performance by using additional memory or by adding steps to the data update process. Choosing whether and where to store SQLDataAdapter objects involves balancing the amount of memory and number of applications the Web server holds.

Now that the DataSet and database are both updated with the new maximum transaction limit, a synchronization error has been introduced into my system. Since each Web server stores its own copy of the DataSet object, the data cached on the second Web server now stores a different maximum transaction limit for the company than that which is stored on the first server. Since the company has increased its transaction limit, it's possible that the second Web server will reject transactions that would be allowed on the first Web server.

The key to fixing these newfound synchronization problems lies in the definition of the Web Services themselves. Since Web Services are designed to make interaction between computers easy and seamless, I simply need to have the Web server with the correct data contact the Web server with the outdated DataSet and tell it to update itself. I do this by exposing several new Web methods on each Web Service.

When any code within my XML Web Services application updates information that is stored in the cached DataSet object, the Web Service must then call the appropriate Web method on the second Web server to indicate that the data has been updated. The main reason to use a Web method call instead of directly referencing and instantiating the second Web Service's DataSet object is flexibility. Typical Web farms consist of several Web servers housed in a central location that all have access to a single network. This is not the only scenario, however. If your Web farm consists of servers residing in multiple locations on multiple networks, direct component references may be more difficult or even impossible. Using Web Service calls also allows you to update cached data across multiple applications. Imagine an internal reporting application that displays summary reports to executives. This application would cache its own data, but could be automatically updated by the AuthorizeCC Web Service as necessary. This update can happen equally well across a server room or across the globe.

Another reason for using a Web method call to refresh data is consistency. In this scenario, I will write just one set of code that makes two Web method calls—that is, one for each server. I will then deploy that single code base to both Web servers. Since the code for calling internal methods is different from the code needed to call externally referenced methods, using a single codebase to make a Web Service call to both the local and external components keeps my code much more manageable.

Creating the Refresh Web Methods

Three methods need to be created on my Web Service to handle three common data updating situations: RefreshData, RefreshTable, and RefreshRow. The specific Web method that will be called depends upon the type of update being done, but each call indicates to the receiving server that it needs to update some portion of its cached data. Code for all three functions is shown in Figure 5.

Figure 5 Three Refresh Web Methods

<WebMethod(Description:="Refreshes all cached data on this web server. If Propagate is 'True,' then requests will be sent to all other registered web servers to refresh their own cached data.", MessageName:="RefreshData")> Public Function RefreshCachedData(ByVal _ Propagate As Boolean) As Boolean 'Refreshes Cached Data on this and other servers GetCachedData() If Propagate = True Then RefreshDataOnOtherServers("AuthorizeCC.asmx/ _ RefreshSpecificTable?TableName=" _ & TableName & "&Propagate=False") End Function '—————————————————————————————————— <WebMethod(Description:="Refreshes cached data in the specified table on this web server. If Propagate is 'True,' then requests will be sent to all other registered web servers to refresh their own cached data.", MessageName:="RefreshTable")> Public Function RefreshCachedData(ByVal TableName As String, ByVal _ Propagate As Boolean) As Boolean 'Refreshes Cached Data on this and other servers, for the specified table Dim conn As New System.Data.SqlClient.SqlConnection() 'Connection to 'database Dim ds As New DataSet() conn.ConnectionString = "server=localhost;database=MSDN1;uid=sa;pwd=" conn.Open() FillTable("SELECT * FROM " & TableName, TableName, ds, conn) If Propagate = True Then RefreshDataOnOtherServers("AuthorizeCC.asmx/ _ RefreshSpecificTable?TableName=" _ & TableName & "&Propagate=False") End Function '—————————————————————————————————— <WebMethod(Description:="Refreshes cached data in the specified row on this web server. If Propagate is 'True,' then requests will be sent to all other registered web servers to refresh their own cached data.", MessageName:="RefreshRow")> Public Function RefreshCachedData(ByVal TableName As String, ByVal ID _ As Integer, ByVal Propagate As Boolean) As Boolean 'Refreshes Cached Data on this and other servers, for the 'specified table Dim conn As New SqlConnection() 'Connection to database Dim cmd As New SqlCommand() Dim rdr As SqlDataReader Dim ds As New DataSet() Dim dr As DataRow Dim dc As DataColumn conn.ConnectionString = "server=localhost;database=MSDN1;uid=sa;pwd=" conn.Open() cmd.CommandText = "SELECT * FROM " & TableName cmd.Connection = conn rdr = cmd.ExecuteReader(CommandBehavior.SingleRow) dr = CType(Application.Item("CachedData"), _ DataSet).Tables("TableName").Rows.Find("ID") For Each dc In dr.Table.Columns 'Store the new value in the cached DataSet dr.Item(dc.ColumnName) = rdr.Item(dc.ColumnName) Next If Propagate = True Then RefreshDataOnOtherServers("AuthorizeCC.asmx/ _ RefreshSpecificTable?TableName=" _ & TableName & "&Propagate=False") End Function

The RefreshData method simply executes the GetCachedData function that I defined earlier. This creates a new DataSet object, fills it with fresh data from the database, and then stores it in the Application object. This meets my goal of re-synchronizing the two DataSet objects stored in each Web server's Application object, assuming the database has been previously updated. However, this is typically overkill since in most cases the code will not have changed data in every table in the DataSet.

The RefreshTable method takes a single String parameter called TableName. The TableName parameter must match one of the DataTable objects stored in the cached DataSet and indicates that only the specified table needs to be refreshed. The method then retrieves the data only for the specified table and replaces the DataTable object within the DataSet. Note that in this example there are only two possible values for the TableName parameter, so the application can easily branch to the appropriate code. There is some code duplication here because the code used to refresh a specific DataTable duplicates the code in the GetCachedData routine. In a situation where the cached DataSet contains many DataTable objects, it would be more efficient to have both the GetCachedData and RefreshTable routines call a general routine that accepts a TableName parameter.

The final method, RefreshRow, is designed to refresh cached data at an even more granular level. The method will identify a specific DataRow object within a DataTable object within the DataSet that should be refreshed. It will then pull from the database only the data necessary to refresh that single row. The parameters for this method are not as easily defined as for the two previous methods since database tables can have multiple keys of varying types. In this example, I am working on the assumption that any table that will need to be refreshed will have only a single key of type long. The method can then find the record with the specified KeyValue in the specified TableName and update the DataSet. This would work equally well for inserting and deleting rows. I could overload this Web method to take additional parameter sets to accommodate composite keys or keys of another type, but since this is a Web method, I would have to create a unique MessageName for each Web method. This would force my code to call methods with names like RefreshRowWithTwoLongKeys and RefreshRowWithOneGUIDKey. This quickly becomes too cumbersome to manage.

These three methods are specifically designed to be general to allow reuse in a variety of applications. It is possible, however, to eke out even better performance by creating very specific Web methods. In a situation where you know that certain data, such as maximum transaction limit, is going to be updated frequently, you could create a Web method to do just this. An UpdateMaxTransactionLimit would take a KeyValue parameter to identify the company and a NewTransactionLimit parameter that specifies the new value. This would eliminate the process of pulling data from the database on the second Web server, thus improving performance.

A final concern for my three Refresh Web methods is security. Since no data is being returned from the Refresh Web Services, it is unlikely that a user could gain unauthorized access to your data through the methods I have developed. However, depending on the amount of cached data and the speed of the database it is being stored in, refreshing it can be a lengthy process. This fact could be maliciously exploited by repeatedly calling the lengthy RefreshData Web method, bringing your Web server to its knees.

Calling the Refresh Methods

Now that I have created the three Refresh Web methods, I need to develop a way to call the appropriate method after I have updated the database. The appeal of Web Services is that they make use of industry standard protocols such as HTTP and SOAP, enabling them to work with any language that uses the same protocols. I could easily write the code to call the Refresh methods in Visual Basic 6.0, VBScript, or even Java, but since I am using Visual Basic .NET to write my own XML Web Service, I will also use it to call the new Web methods.

Web method calls can be made using one of three different HTTP techniques: GET, POST, or SOAP. HTTP-GET and HTTP-POST will be familiar to Web developers as the two methods HTML forms can use. HTTP-GET appends all parameters to the end of the URL while POST sends them in the HTTP header. SOAP is similar to HTTP-POST in that the parameters are sent in the header, but instead of individual parameters being passed, all parameters are bundled into a single SOAP payload.

I have chosen to use HTTP-GET and append my parameter name-value pairs to the end of my URL. In Figure 6 you can see the simple code that's required to make an HTTP Request call the RefreshTable method.

Figure 6 Calling Refresh XML Web Services

Dim ieWebServers As IEnumerator Dim sURL As New StringBuilder() Dim wReqRouter As WebRequest Dim wrespRouter As WebResponse Try ieWebServers = CType(Application.Item("CachedData"), _ DataSet).Tables("ServerName").Rows.GetEnumerator() While ieWebServers.MoveNext() If ieWebServers.Current("ServerName") <> Me.Server.MachineName Then 'Clear out the URL for the next use sURL.Length = 0 With sURL .Append("https://") .Append(ieWebServers.Current("IP")) .Append("/AuthorizeCC/AuthorizeCC.asmx/RefreshCachedData") End With wReqRouter = WebRequest.Create(sURL.ToString()) ' Get the response from the WebRequest Try wrespRouter = wReqRouter.GetResponse() Catch ex As Exception 'Handle exception here End Try ' Code to use the WebResponse goes here. Dim xmlRdr As New XmlTextReader(wrespRouter.GetResponseStream()) While xmlRdr.Read() If xmlRdr.Name = "boolean" Then If xmlRdr.ReadAttributeValue() = True Then 'Error value returned true! End If End If End While ' Close the response to free resources. wrespRouter.Close() End If End While Catch ex As Exception 'Handle exception here End Try

The base URL for the Web method call is the standard https:// identifier followed by the standard address (in this case, Server1). To this, I have appended the name of my Web Service, AuthorizeCC.asmx. HTTP-GET uses a question mark to discriminate between the address and the parameter list, followed by name-value pairs separated by ampersands (&). The code in Figure 6 calls the RefreshTable method which requires only one parameter, so I append "?TableName=Customer". The URL is now complete and ready to call the appropriate Web method.

I execute the Web method by creating a WebRequest object using the URL I built as a constructor parameter. The WebRequest object is now ready to execute the Web method. I create a WebResponse object to receive the response returned by the WebRequest. The GetResponse method of the WebRequest object makes the actual call to my Web method and receives the response in its return value. Setting the WebResponse object to the return value of the GetResponse call allows me to look at the data returned from the Web method call.

Since the Web Service returns XML, I will instantiate an XMLTextReader to deal with the Web method's response. Since I know that the Web method returns a simple Boolean value, I will iterate through the elements in the XMLTextReader until I find an element with the name "boolean". I can then use the ReadAttributeValue method of the XMLTextReader to determine the success or failure of the Web method call. Note that this is a simplistic method for dealing with a simplistic response. More complex Web method return values will require more complex handling code.

I then update my URL to reflect the name of the second Web server, Server2, and make the Web request again. Since I have already updated the cached data on the local machine, I will add a simple If...Then construct that checks the name of the local Web server and does not call the Web method if the name of the server to be called matches the name of the calling server. Thus, Server1 will only refresh the cached data on Server2, and vice versa.

There is an improvement that can be made to this code in order to account for a design limitation of HTTP which limits URL length to 1,024 characters. For the AuthorizeCC Web Service this is plenty of space, but for Web Services that require many parameters or lengthy values you will probably have to use HTTP-POST or SOAP to avoid this limitation.

Adding New Servers

As I noted before, the AuthorizeCC Web Service is rapidly adding new customers. The existing Web farm with its two Web servers is performing well with each server using its own cached data and notifying the other server when updates are required. However, at peak usage times, the number of simultaneous hits is exceeding available capacity and Web Service consumers are noticing delays. It is time to add additional servers to the Web farm.

The load balancing software is designed to automatically handle as many Web servers as you grow in your Web farm, but unfortunately, the AuthorizeCC Web Service is not. The current Web Service is hardcoded to only update cached data on the two known servers, Server1 and Server2. When I add Server3, the code has no way of knowing that it should call the appropriate Refresh method on Server3 as well.

In order to overcome this, I will add a simple table named ServerName to my database that holds the names of all the servers in the Web farm. You may have noticed that the code in Figure 2 is already written to pull this table into the cached DataSet. I can alter my Web Service to pull a list of Web server names from the database. My code will then iterate through the database rows, using each server name to build a URL to call the appropriate Web method. As before, the code will skip the Web method call if the calling and receiving server names are the same. The code in Figure 6 includes all of these changes.

This improves my situation, for it is now easy for an administrator to add the name of a new Web server to the database and have it instantly included in the farm of servers using cached, refreshable data. What happens if the administrator forgets to enter a server name in the database or enters an incorrect name? In order to fix this potential problem, I will add a small bit of code to my Web Service that will add servers to the database automatically. The code in Figure 7 is added to the GetCachedData subroutine defined earlier. It checks the ServerName DataTable in the cached DataSet for the name of the Web server in question. If the server's name is not found, then a new entry is added to the database and the ServerName DataTable is refilled.

Figure 7 Automatic Registration for New Servers

Public Sub GetCachedData() ••• Dim drTemp As DataRow 'Look for own address in Web_Service_IP if not found, add it drTemp = ds.Tables("ServerName").Rows.Find(Me.Server.MachineName) If drTemp Is Nothing Then 'Add our machine name to the database Dim SQL As String SQL = "INSERT INTO ServerName (ServerName) VALUES (" & _ Me.Server.MachineName & ")" 'Setup the command object Dim cmd As New System.Data.SqlClient.SqlCommand(SQL) cmd.Connection = conn cmd.ExecuteNonQuery() 'Fill the ServerName table again ds.Tables("ServerName").Clear() FillTable("SELECT ServerName FROM ServerName", "ServerName", ds, conn) 'Refresh other servers with new server name RefreshCachedData("ServerName") End If •••

Since each Web server now automatically registers itself so that it will be included in all calls to refresh cached data, I can now add new servers to my farm at any time without having to create a database record or recompile my code.

Conclusion

In this article I have demonstrated techniques for improving performance by using cached DataSets to reduce the number of round-trips made to the database. I have also shown how Web Services can communicate among themselves to force a refresh of the cached data, ensuring that all Web servers maintain current data in their cache.

These techniques can be easily expanded using additional enhancements tailored for your particular situation. For instance, you may want to consider moving the code that makes Web method calls to its own Manager Web Service. Each of your production Web Services would call the Manager Web Service, which would in turn call the appropriate Refresh methods on each Web server. This would reduce the amount of administrative functionality in your production Web Services, thereby improving performance.

Using the Microsoft® .NET Framework and these techniques, it is easy to build high performance XML Web Services that still offer the redundancy benefits of a Web farm.

For related articles see:
Data Points: Using the ADO.NET DataSet for Multitiered Apps
Cutting Edge: Using Session and Application Objects in ASP .NET
Using ASP.NET Session State in a Web Service

For background information see:
.NET e-Business Architecture by Burgett, Baute, Pickett, Brown, and Sullivan (Sams, 2001)

David Burgettis a Senior Technical Architect with G. A. Sullivan in Kansas City, KS. He has contributed to three books about Visual Studio, most recently .NET eBusiness Architecture (Sams, 2001). David frequently speaks at technical events. Reach him at DavidB@GASullivan.com.