This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
|
Ted Pattison
Download the code (14KB)In the last installment of Advanced Basics, I discussed the fundamental principles of creating a Microsoft® Internet Information Server (IIS) application using Visual Basic®. This month I will continue to examine techniques for developing Internet-style applications using Visual Basic objects. While it's possible to utilize the strengths of one particular browser, I will concentrate on creating Web-based applications that cater to a larger Internet audience. When you build a Web application using server-side objects created with Visual Basic, you can use standard HTML (such as version 3.2). This allows you to reach users who are running on a variety of different platforms and browsers. I always operate under the assumption that there are users on the Web running Netscape on OS/2 who have valid credit card numbers and just might want to give up their hard-earned money.
This month I will examine the threading architecture of IIS and discuss efficient techniques for saving state on the Web server associated with a single client across multiple page requests. Understanding how IIS thread pooling works is important. Once you see how a client's request is processed by the Web server, you will understand why some techniques are much better than others when optimizing your application's throughput and scalability.
Thread Pooling in IISLike many other scalable applications that run in the middle tier, the IIS Web server process (InetInfo.exe) has been designed to accommodate a large number of concurrent users. To process the requests of hundreds or possibly thousands of clients, IIS employs sophisticated thread management to balance response time and system throughput. Instead of creating a new thread for each request, IIS maintains a pool of worker threads that are dedicated to processing requests. There are quite a few other threads running in IIS, but for the purposes of this discussion I'll focus on the threads in the request pool. As incoming requests arrive at the Web server, IIS must somehow dispatch worker threads from this pool to process them.
So why is a thread pool so important? Threads are expensive. Each consumes its fair share of resources. Add the overhead of scheduling threads in and out of the system's processor(s), and you can see why a middle-tier application that uses a thread-per-client model starts to degrade when the number of clients reaches the hundreds or thousands.
A thread-pooling algorithm saves valuable server-side processing cycles because there is no ongoing need to create or tear down threads. Thread pooling also sets an upward limit on the number of threads, which helps to conserve other important server-side resources. Thus, a thread pool makes it possible for an application to comfortably accommodate a larger number of users.
IIS works with a default pool of up to 10 worker threads per processor. The number of threads is configurable by adding entries to the Windows® registry. The IIS Resource Kit can show you how to tune your Web server to have an optimal number of threads (somewhere between 5 and 20).
Since there is a finite number of worker threads dedicated to processing requests, IIS uses a queue to schedule incoming client requests when all the available worker threads are busy processing other requests. Requests are serviced on a first-in, first-out basis. If the server gets more requests per second than it can handle, the request queue will grow larger and larger. When the queue grows too large, the response times in which incoming requests are serviced will decrease dramatically. IIS sets a maximum capacity for the queue so the request backlog doesn't grow too large. You may also want to investigate the RequestQueueMax registry setting, as mentioned in the IIS Resource Kit.
By default, IIS allows the queue to grow to a maximum size of 500 requests. When the queue has reached capacity, additional incoming requests are rejected with a Server Too Busy error. This behavior is very reasonable because it allows IIS to reject any request that it cannot handle in a timely manner. The maximum size of the queue is also configurable. The IIS Resource Kit can show you how to go about adjusting this setting.
Setting the queue size allows you to adjust the point at which IIS will start rejecting incoming requests. As you adjust this value, you are attempting to strike a balance between high availability and short response times. The whole point is to configure the queue size to handle short-term peaks, yet limit the queue's backlog during extreme workloads. If your Web server gets overloaded, it's better to send error messages back to your clients instead of making them wait too long.
So let's summarize how the threading model works for a default installation of IIS on a machine with a single processor. There are 10 available worker threads in the pool. When a request arrives, IIS can dispatch an idle thread from the pool if one is available. Note that IIS is free to use any worker thread that is available in the pool to process the request. If all the worker threads are busy processing other requests, incoming requests are queued up and processed on a first-in, first-out basis. As long as the queue doesn't reach its default capacity of 500 requests, all requests will be processed in due time. Now let's talk about how you can make the most of this model.
Programming Apartment-threaded ObjectsVisual Basic makes it easy to create apartment-threaded components that run in single-threaded apartments (STAs). If you want to create a component that has a more sophisticated threading model, you must resort to another language such as C++ or Java. The good news for people working in Visual Basic is that this doesn't impose much of a limitation in an IIS application because each IIS worker thread is an apartment thread. An ASP script and the Visual Basic objects that it creates will run on the same thread. This means that COM doesn't need to place a proxy/stub layer between the ASP client and your Visual Basic objects.
When you create a Visual Basic DLL for an IIS application, make sure you leave your project's threading model set to the default of apartment-threaded. If you change it to single-threaded, your objects will be loaded into the main STA of the IIS Web server process. This will likely cause your objects to be loaded into a different apartment than the
ASP script that created it. This causes an unnecessary proxy/stub layer and it can also result in unexpected thread-blocking problems.
One important thing to note about apartment-threaded objects like the ones you create with Visual Basic is that they have thread affinity. This means that an apartment-threaded object can only be accessed by the thread that creates it. This isn't a problem inside a single request. One thread creates the object and uses it, and then the object is discarded at the end of the request. However, if you create a Visual Basic object and assign it to an ASP Session variable, you have pinned the client that owns the session to that specific worker thread. IIS must route every future request from this client through this one thread (see Figure 1). This is unfortunate because IIS works best when it can use the first available thread it finds in the pool.
Figure 1: Threading Architectures |
The threading architecture of IIS is most efficient when it has the flexibility to dispatch any thread in the pool to service an incoming request. However, as you can see, when you assign a Visual Basic object to an ASP Session variable, IIS must locate (and possibly block on) the one thread that created the object. While IIS is capable of serializing all future requests over the same thread, this situation doesn't allow IIS to make the most of its thread-pooling scheme. An incoming request can be blocked while waiting for one thread to free up, while several other worker threads are idle.
Here's the most important point in this column: the threading model of IIS is severely compromised when an app allows apartment-threaded objects to live beyond the scope of a single client request. This means you should always create and destroy your Visual Basic objects inside the scope of a single ASP request. Don't assign a Visual Basic object (or any other apartment-threaded object) to an ASP Session variable. Your Web application will not scale if you don't follow this rule.
Of course, you might be able to get away with assigning a Visual Basic object to an ASP Session variable if your site doesn't get much traffic. What's important is that you realize the implications of what you're doing. In such a design, one request may block a second request when there are only two clients hitting the site. Ouch! That's painful; the second thread will be blocked even while nine available threads are sitting idle in the pool.
If your site doesn't get much traffic today, you must still plan for the future. Can you assume that your site will never get much traffic? If you avoid using Visual Basic objects scoped at the session level, you never have to worry about changing your design and rewriting code when your site becomes more popular.
Now that you know about the problems with apartment-threaded objects and ASP Session variables, it's time to think through the same issues with regard to ASP Application variables. Assigning a Visual Basic object to an ASP Application variable would create an even more severe problem. If it were possible to assign an apartment-threaded object to an ASP Application variable, the requests from many different clients would have to be routed through a single thread. This would result in severe blocking behavior.
Fortunately, the ASP runtime (asp.dll) doesn't even allow you to assign an apartment-threaded object to an ASP Application variable. If you attempt to do this, your code will fail with the error message, "Cannot add object with apartment model behavior to the application intrinsic object."
Collecting State Across RequestsSo now you know what not to do. Never store a Visual Basic object in an ASP Application or in an ASP Session variable. At this point you might be wondering how you're going to maintain state across multiple requests for the same client. Collecting state is often essential. For instance, if you are designing a Web-based shopping cart application, you must build a state machine that allows a client to pick out products and enter payment information across multiple requests.
The first question you must answer in the design phase is whether it's acceptable to store client-specific state infor- mation on the Web server. In some scenarios, you can't do this. This is usually the case when you're writing Visual Basic components for a Web farm environment. Many load-balancing schemes used by Web farms will break a design based on the assumption that all the requests from a single client will be routed across the same computer. Complicated environments such as Web farms typically require stateless techniques where client-specific state is sent back and stored on the client using something like cookies or invisible HTML controls.
I'll look more closely at a Web farm environment in a future column. This month, I will look at a simpler scenario. If you can make the assumption that you are writing Visual Basic components for a Web site based on a single computer, you can use ASP Session variables to store your state.
When you use ASP Session variables to hold your state across requests, you should store simple values (primitive datatypes) instead of Visual Basic objects. When you store your state in ASP Session variables using datatypes such as integers, doubles, and Visual Basic for Applications strings, IIS can process future requests using any worker thread from the pool. When you follow this approach, you can even use complicated data structures based on Visual Basic for Applications arrays and variants. Don't hold on to a Visual Basic object across requests and you'll be fine.
Let's look at how you can use a Visual Basic object to read and write state to and from ASP Session variables. In my last column, I covered the fundamentals of writing and deploying Visual Basic components in an IIS application. Let's quickly review three key points. First, always register your Visual Basic components with Microsoft Transaction Server (MTS). IIS 4.0 uses MTS for thread pooling, since it provides better safety and thread protection. For more information on MTS, see "Employing Microsoft Transaction Server in Multitier Applications" by Chris Dellinger (MIND, July 1997). Second, you can (and should) program against the MTS type library as well as the ASP type library in any ActiveX® DLL project targeted for IIS. Third, you can access any of the built-in ASP objects in a Visual Basic component built with Visual Basic with the following code:
' note this works with IIS4 but not IIS3
Dim rsp As Response
Set rsp = GetObjectContext("Response")
rsp.Write "Hello World"
Using the same technique to get at the built-in ASP objects, it's pretty simple to read and write to an ASP Session variable. Here's a simple string value written to an ASP Session variable named MyState:
Dim ses As Session
Set ses = GetObjectContext("Session")
ses("MyState") = "Some data related to this client"
In a future request you can retrieve this state from the same ASP Session variable like this:
Dim ses As Session, s As String
Set ses = GetObjectContext("Session")
s = ses("MyState")
Now that you have seen how to read and write state to ASP Session variables, you should think about where to place the code that's going to access them. Since your Visual Basic components are registered with MTS, you can provide an implementation of the Activate method to access your session variables. The Activate method is part of the ObjectControl interface defined inside the MTS type library. If your component implements ObjectControl, the system will send your objects notification for activation and deactivation.
Take a look at the code for the CHitTracker component shown in Figure 2. This code demonstrates how to implement the ObjectControl interface in a Visual Basic component. When you implement this interface, the system will call Activate on your object just before the first property or method is accessed by a client. Likewise, the system will call Deactivate just before your object is deactivated and destroyed.
Unlike the Class_Initialize routine in a Visual Basic class module, the Activate method allows you to retrieve a valid reference to the MTS ObjectContext. The ObjectContext will, in turn, allow you to access your session variables. This implies that the Activate method provides your first opportunity to access the state information in ASP Session variables.
CHitTracker is a simple component that records the history of a client's session across multiple hits. This component stores a string value in a Session variable named Info to remember state information written out during earlier requests. You can create a CHitTracker instance from an ASP script or from another Visual Basic component. Here's an example of creating and calling a CHitTracker object from an ASP script client:
<SCRIPT LANGUAGE=VBScript RUNAT=Server>
Dim Obj, ProgID
ProgID="MindSite2.CHitTracker"
Set Obj = Server.CreateObject(ProgID)
Response.Write Obj.GetSessionInfo()
</SCRIPT>
The CHitTracker instance is created with the Server.CreateObject method. The Activate method will run just before the execution of the GetSessionInfo method. This gives the object an opportunity to retrieve any session variables before they are accessed by the ASP script client. In this example, I used the Activate method as the point to write state information back out to the ASP Session variable.
I have found that the Activate method works as expected, but there are a few problems when trying to use the Deactivate method to write your state information out to session variables. In many situations you cannot reliably access the built-in ASP objects in the Deactivate method. This means you should write out your state information in the Activate method or in another method that is explicitly called by the ASP script client.
The CHitTracker component demonstrates a standard approach to building a simple state machine across multiple ASP requests. Every instance of the CHitTracker component is always created and destroyed within the scope of a single ASP request. However, each instance of this component is able to retrieve and modify the session's state information that was written out by a previous instance in an earlier request. This technique allows you to work with easy-to-use Visual Basic objects, but it doesn't require you to hold on to your objects across multiple requests. As you know, that's something you want to avoid when you're programming in the big leagues.
The CHitTracker component is simple in the sense that it builds ongoing session state into a single string value. Your designs can get much more sophisticated than this. Your ASP Session variables can hold complicated data structures based on Visual Basic for Applications arrays and variants. The Visual Basic PropertyBag component is also gaining popularity as an easy way to write name/value pairs into a string or byte array. The sky's the limit-as long as you use primitive datatypes instead of apartment-threaded objects. These techniques should give you what you need to meet the design requirements of something like a shopping cart application.
From the February 1999 issue of Microsoft Internet Developer