Distributing Load

For high-traffic and/or high-reliability sites, it's often desirable to distribute the site's content and processing duties across multiple backend servers. This distribution allows multiple servers to share the load, increasing the number of simultaneous requests that can be handled and providing failover so the site can remain up even when one particular component crashes.

Distribution isn't appropriate for every site. Creating and maintaining a distributed site can be significantly more complicated than doing the same for a standalone site and can be more costly as well in terms of load-balancing hardware and/or software requirements. Distribution also doesn't tend to provide a significant performance benefit until the server is under extreme load. When presented with a performance problem, it's often easiest to "throw hardware at the problem" by installing a single higher-end machine rather than trying to share the load between two underperforming machines.

Still, there are many sites that need to scale beyond the capabilities of a single machine and that need a level of reliability no single machine can offer. These are the sites that need to be distributed.

How to Be Distributable

The programming requirements for a distributable servlet are much stricter than the requirements for a nondistributable servlet. A distributable servlet must be written following certain rules so that different instances of the servlet can execute on multiple backend machines. Any programmer assumptions that there's only one servlet instance, one servlet context, one JVM, or one filesystem have the potential to cause serious problems.

To learn how servlets can be distributed, look at Enterprise JavaBeans (EJB) technology, a server-side component model for implementing distributed business objects and the technology that's at the heart of J2EE. EJB is designed from the ground up as distributable objects. An EJB implements business logic and lets the container (essentially the server) in which it runs manage services such as transactions, persistence, concurrency, and security. An EJB may be distributed across a number of backend machines and may be moved between machines at the container's discretion. To enable this distribution model, EJB must follow a strict specification-defined ruleset for what they can and cannot do. (See sidebar)

Servlets have no such specification-defined ruleset. This stems from their heritage as frontend server-side components, used to communicate with the client and call on the distributed EJB and not be distributed themselves. However, for high-traffic sites or sites that need high reliability, servlets too need to be distributed. We expect upcoming Servlet API versions to include a tighter definition for the implementation of distributed servlet containers.

The following are our own rules of thumb for writing servlets to be deployed in a distributed environment:

Consider that different instances of the servlet may exist on each different JVM and/or machine. Therefore, instance variables and static variables should not be used to store state. Any state should be held in an external resource such as a database or EJB (the servlet's name can be used in the lookup).

Consider that a different instances of the ServletContext may exist on each different JVM and/or machine. Therefore, the context should not be used to store application state. Any state should be held in an external resource such as a database or EJB (the context's path can be used in the lookup).

Consider that any object placed into an HttpSession should be capable of being moved to (or accessed from) a different machine. For example, the object can implement java.io.Serializable. Be aware that because sessions may migrate, the session unbind event may occur on a different machine than the session bind event!

Consider that files may not exist on all backend machines. Therefore, you should avoid using the java.io package for file access and use the getServletContext().getResource( ) mechanism instead -- or make sure all accessed files are replicated across all backend machines.

Consider that synchronization is not global and works only for the local JVM.

A web application whose components follow these rules can be marked distributable, and that marking allows the server to deploy the application across multiple backend machines. The distributable mark is placed within the web.xml deployment descriptor as an empty <distributable/> tag located between the application's description and its context parameters:

Applications are nondistributable by default, to allow the casual servlet programmer to author servlets without worrying about the extra rules for distributed deployment. Marking an application distributable does not necessarily mean the application will be split across different machines. It only indicates the capability of the application to be split. Think of it as a programmer-provided certification.

Servers do not enforce most of the preceding rules given for a distributed application. For example, a servlet is not barred from using instance and static variables nor barred from storing objects in its ServletContext, and a servlet may still directly access files using the java.io package. It's up to the programmer to ensure these abilities aren't abused. The only enforcement that the server may perform is throwing an IllegalArgumentException if an object bound to the HttpSession does not implement java.io.Serializable (and even that's optional because, as we'll see later, a J2EE-compliant server must allow additional types of objects to be stored in the session).

Many Styles of Distribution

Servlet distribution (often called clustering) is an optional feature of a servlet container, and servlet containers that do support clustering are free to do so in several different ways. There are four standard architectures, listed here from simplest to most advanced.

No clustering. All servlets execute within a single JVM, and the <distributable/> marker is essentially ignored. This design is simple, and works fine for a standard site. The standalone Tomcat server works this way.

Clustering support, no session migration, and no session failover. Servlets in a web application marked <distributable/> may execute across multiple machines. Nonsession requests are randomly distributed (modulo some weighting perhaps). Session requests are "sticky" and tied to the particular backend server on which they first start. Session data does not move between machines, and this has the advantage that sessions may hold nontransferable (non-Serializable) data and the disadvantage that sessions may not migrate to underutilized servers and a server crash may result in broken sessions. This is the architecture used by Apache/JServ and Apache/Tomcat. Sessions are tied to a particular host through a mechanism where the mod_jserv/mod_jk connector in Apache uses a portion of the session ID to indicate which backend JServ or Tomcat owns the session. Multiple instances of Apache may be used as well, with the support of load-balancing hardware or software.

Clustering support, with session migration, no session failover. This architecture works the same as the former, except a session may migrate from one server to another to improve the load balance. To avoid concurrency issues, any session migration is guaranteed to occur between user requests. The Servlet Specification makes this guarantee: "Within an application that is marked as distributable, all requests that are part of a session can only be handled on a single VM at any one time." All objects placed into a session that may be migrated must implement java.io.Serializable or be transferable in some other way.

Clustering support, with session migration and with session failover. A server implementing this architecture has the additional ability to duplicate the contents of a session so the crash of any individual component does not necessarily break a user's session. The challenge with this architecture is coordinating efficient and effective information flow. Most high-end servers follow this architecture.

The details on how to implement clustering vary by server and are a point on which server vendors actively compete. Look to your server's documentation for details on what level of clustering it supports. Another useful feature to watch for is session persistence, the background saving of session information to disk or database, which allows the information to survive server restarts and crashes.