Peak performance tuning of CMP 2.0 Entity beans in WebLogic Server 8.1 and 9.0
Pages: 1, 2, 3

Choosing an Optimal Cache Size for Entity Beans

Now that you understand how caching between transactions works, I'll discuss the important topic of choosing an optimal cache size. By default, the cache is defined per entity bean and has a size of 1000. Cache size is controlled by the max-beans-in-cache element in the weblogic-ejb-jar.xml deployment descriptor. I find that name somehow misleading because, depending on the concurrency strategy, WebLogic Server is either going to keep the pool of instances of entity beans without any state (in the case of database concurrency strategy and exclusive concurrency with cache between transactions disabled) or (in the case of read-only, optimistic, or exclusive concurrency with cache between transactions enabled) will keep the true cache of beans with all field values preserved and therefore ready for use without refreshing bean state from the database. The latter case is more interesting. One may think that changing the cache size will affect only the performance of operations with entity beans; the larger the cache size, the bigger the probability that the particular entity bean instance would be found in the cache when needed. This is generally correct, but as I'm going to show below, other important considerations can affect your choice of cache size.

Multiversioning and transactions considerations

One of the driving forces for determining entity bean cache size, which may not be immediately obvious, is that when transactions use entity beans, they're actually loaded and "pinned" in the entity cache for the duration of the transaction, even if the caller is not modifying entity beans instances but just reading values from it. For example, imagine that code in a session or MDB bean is executing a finder method on an entity bean "Person" and then iterating over the returned collection:

...
Collection persons = personLocalHome.findSomething();
    
for (Iterator iter = persons.iterator(); iter.hasNext();) {
    PersonLocal person = (PersonLocal)iter.next();
    Long id = person.getId();
    // do something: log, send email, etc
    ...
}
...

If the findSomething() method returns more objects than values specified in the max-beans-in-cache, your application will get an unpleasant (and most likely unexpected) CacheFullException when the iterator gets to the N+1 object (if N is the current Entity cache size). This may not seem like a big deal because it's generally agreed that finders should not return very large collections anyway. But don't forget that by default the WebLogic entity cache is multiversioned, which means if multiple transactions request the same entity bean instance, multiple versions (one for each transaction) are created in the cache; this could effectively lower cache capacity in terms of unique objects.

Since it's normal to have multiple, simultaneous transactions running in a container at the same time, imagine if the code above gets called from a session or MDB bean that is deployed with a high value in the max-beans-in-free-pool parameter (which by default is 1000), and there are, say, 50 simultaneous client requests coming in. This will leave every transaction with only 1000/50 = 20 usable slots in the entity cache, and if a finder returns more than 20 objects, some of the transactions will fail.

This is important to keep in mind while designing operations with a large number of entity beans. The situation is worsened by the fact that developers often work with small database sizes, and this problem may not manifest itself until code is deployed against a production-size database. As a safeguard measure, I would suggest in development not to use the default settings for the cache size, but set it at an artificially low value (10-100) so that cache-related problems can be discovered and fixed at an early stage of development.

As you can see, choosing the right size for the entity cache is vitally important, and not only from a performance point of view. If the cache size is too large, your application memory consumption will be unnecessarily high, but if you go to the other extreme and the configured cache size is too small, you're risking running into a CacheFullException. So how do you choose optimal cache sizes for all entity beans?

If you don't explicitly specify a cache size for entity beans, WebLogic Server will use the default size of 1000. This can be sufficient for some beans where it's known a priori that the number of instances is not going to be very large—for example, if a bean represents a lookup table in a database, such as "country" or "state," where the upper number of bean instances is well known. In this case, it's perfectly acceptable not to specify a cache size and let the server use the default value because there is no memory impact if the cache is not used to full capacity. As a side note, it's a good idea to use a read-only concurrency strategy for the beans that don't change or that change infrequently; this will not only eliminate unnecessary database calls, but it will also limit the number of instances with the same PK to one in the entity cache for that bean (multiversioning is not necessary), therefore preserving memory and improving performance.

Things are a little bit more complicated for beans in which the maximum number of instances that could be accessed simultaneously are not known or cannot be reliably estimated. You need to analyze and estimate the maximum number of beans returned from finder methods and accessed within one transaction, and then multiply that by the maximum number of simultaneous transactions using this operation that your application is expected to handle. (This is usually limited by maximum number of instances of entry points into your application—session beans and/or MDBs.) This will give you a rough estimate of the minimum cache capacity needed for that particular entity bean.

Application-level caching

Analyzing and configuring each bean's cache can be cumbersome if many entity beans are used in your application. It's especially tricky to estimate the number of bean instances returned from the "detail" side of a "master-detail" relationship—for example, if your application executes a finder on the "order" table, and each order has a collection of "items" that can vary from one to some potentially high number. Another problem is that since every entity bean has a separate cache, memory is not utilized in the most efficient way. Acknowledging limitations of the "single-cache-per-bean" model, WebLogic Server (starting with version 7) supports application-level caching for entity beans. This allows multiple entity beans in the same J2EE application to share a single runtime cache.

Application-level caching offers the following advantages:

  • It reduces the number of entity bean caches and therefore the effort to configure the cache.
  • It makes better use of memory and heap space because of reduced fragmentation. For example, if a particular EJB experiences a burst of activity, it can make use of all memory available to the combined cache, while other EJBs that use the cache are paged out. If two EJBs use different caches, when one bean's cache becomes full, the container cannot page out EJBs in the other bean's cache, and this results in wasted memory.
  • It simplifies management; combined caching enables a system administrator to tune a single cache, instead of many caches.
  • It provides better scalability.

To define the application-level cache, first configure the entity-cache element in weblogic-application.xml. Then reference the application-level cache in the entity-cache-ref element of the entity-descriptor element in weblogic-ejb-jar.xml. You can define one cache and use it for all entity beans in the application, or define different caches for groups of beans. It's also possible to mix and match application-level caches with per-bean caches, so you have a lot of room to experiment in this area. I'd recommend starting with one application-level cache shared by all entity beans, unless you have some very specific requirements.

Using application-level caching is a viable alternative to specifying individual caches for each bean. There are no restrictions on the number of different entity beans that may reference an individual cache or on the number of caches defined. Cache size can be specified in terms of the number of bean instances (an approach similar to one cache per bean) or in terms of maximum memory size. Using memory size may seem attractive from a management perspective, but be aware that WebLogic Server does not calculate the actual amount of memory consumed by beans in the cache (that would probably be too expensive an operation to perform); it simply divides the amount of memory specified by the average size of a bean that could be specified in the weblogic-ejb-jar.xml deployment descriptor. If a size is not specified, a bean is assumed to have an average size of 100 bytes. I think it's more transparent to specify a cache size in terms of the number of bean instances.

Which Strategy to Choose?

We have covered a lot of ground in this article but still haven't discussed all optimization techniques that can be applied to CMP beans! For example, CMR caching and field groups can also be useful under certain circumstances. Choosing an optimal concurrency strategy and taking advantage of long-term caching will give your application an immediate performance boost when applied correctly. With so many different options available in modern versions of WebLogic Server (and for that matter in any other J2EE server), sometimes it's difficult to choose which one to use in a particular situation, especially if the developer has no prior experience in tuning these parameters. If no concurrency strategy and caching parameters are specified at all, the default settings that WebLogic Server uses is certainly a sound choice in terms of data consistency, but are rarely the best possible choice in terms of performance of CMP beans. One size doesn't fit all, so you should analyze your use cases and preferably run some tests under load with different concurrency settings if you're unsure. Next, I'll discuss some basic use cases and recommend settings for each.

Static, read-only data

The easiest possible scenario is when you have tables in the database that are static (they don't changes over time), quasi-static (changes are infrequent), or could be treated as static or quasi-static from your application perspective, and your application doesn't change that data. For example, data could be updated frequently by an external process, but your application could be fine if it sees updates only once a minute/hour/day. In this case, it would be logical to use a read-only concurrency strategy with an appropriate read-timeout-seconds value. If your application needs to see updates at certain predefined times or when there's a batch process that loads data and you need to see fresh data right after, you can explicitly invalidate the cache as described above. You could, for example, expose a "CMP cache invalidation service" on your application facade and call it at the end of the batch process, or from the scheduler. Cache size is easiest to calculate in this case, because the same instance of CMP is shared among all transactions that need it, so there is no consideration on multiversioning and its impact on cache size. Just choose a reasonable number for the cache size based on table size, individual object size, and available memory.

Read-mostly data

Probably the most common situation is when you have data that is read more frequently than it is changed. This is the situation when caching can be applied successfully. I'd suggest using the optimistic locking with cache between transactions enabled. I usually go with an integer type for verify-columns if the database schema can be modified, or with the Modified value if the database schema can't be changed. If you decide to use a version column, make sure external processes (if any) honor the contract on the version column update when data is changed; otherwise you are accepting the risk of lost updates.

In terms of choosing proper cache size, multiversioning should be taken into account as well as the maximum number of beans that could be returned from finder methods. A good upper-limit estimation is to multiply the maximum number of simultaneous transactions your application needs to handle by the maximum number of beans that could be handled in a single transaction. I usually recommend an application-level cache as this is more flexible, because typically it's unlikely that all CMPs are going to be used simultaneously up to the peak of their capacity. The application cache, being global for all CMPs, will better adapt to spikes in the activity of different beans. If you define an application-level cache size too large though, it could hurt performance because all transactions would serialize access to the single cache. This is rarely a problem on any reasonable cache size, but again, you should run performance tests when you're unsure how to determine how cache size affects performance. On a side note, good design practice is to avoid creating finder methods that return an arbitrary, large number of entity beans (for example, findAll() on large tables) because this makes estimating proper cache size almost impossible.

An optimistic concurrency with cache between transactions works best for use cases in which there is a "guarantee" of a cache hit. For example, in one of our projects we had a use case in which an application needed to handle incoming messages (from JMS). Every message record had to be created in the database table(s), and another message had to be sent in response; for that second message, the application expected to receive a response within one minute, and on receiving the response, the same database record would be updated. Now applying caching in this scenario yields an enormous and immediate performance gain. We're "guaranteed" to get at least one hit for every cached item if the cache size was large enough to preserve the CMP instance between creating and receiving the response.

On the other extreme end are use cases in which the target tables are so large that it's highly unlikely you'll get repetitive requests for the same data. This isn't feasible from all practical perspectives, and caching of such data most likely won't improve performance.

The optimistic concurrency pattern should be your preferred choice over the read-mostly pattern as described above. The read-mostly pattern doesn't work in the cluster, doesn't prevent lost updates, and in general is more cumbersome to use. It's described in this article just to give you a holistic picture of all strategies available, but I would discourage its practical use in modern applications outside of a very limited number of specific use cases.

Write-mostly data

If your application primarily inserts or updates records, there is little point in caching that data if it's never going to be accessed again. In the extreme scenario of insert-only operations (OLTP), the cache would just slow down processing. Nonrepeatable updates (updates to the random rows in tables whose size is much larger than cache) would also rarely benefit from CMP cache presence. Moreover, as the number of updates grows compared to the number of reads, the optimistic concurrency strategy becomes less and less performant because of the large number of optimistic concurrency exceptions. In fact, if your application only updates and inserts records into the database, there is little point in using entity beans at all.

Conclusion

As you can tell by the length of this article, there are many aspects to tuning CMP 2.0 EJBs. I started by reviewing the various concurrency strategies that are available. I then turned to a few important performance strategies: the read-mostly pattern, caching between transactions, and choosing an optimal cache size. Finally, I provided some guidance about which strategy to use and when. I hope this analysis helps you get the most out of your EJBs.

References

Dmitri Maximovich is an independent consultant specializing in software design, development, and technical training. He has more than twelve years of industry experience and has been involved with J2EE since its inception.