Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Advanced CORBA Programming wit C++ - M. Henning, S. Vinoski.pdf
Скачиваний:
65
Добавлен:
24.05.2014
Размер:
5 Mб
Скачать

IT-SC book: Advanced CORBA® Programming with C++

After a client invokes an operation, the client loses its thread of control until the operation completes. CORBA does not provide standard APIs to intercept call dispatch on the client side. This makes it impossible to implement client-side caching transparently. Either we must create C++ wrapper classes for object references and implement the caching functionality in these wrapper classes, or we must modify the IDL for an object so that it presents itself as data instead of as an interface. Neither approach is particularly elegant.[2]

[2] Note that some ORBs offer proprietary extensions that allow you to replace the normal client-side proxy with a class of your own, known as a smart proxy. The smart proxy adds client-side caching transparently to the main application logic. However, smart proxies are not portable.

Client-side caching suffers from cache coherency problems. Cache coherency is lost if each of several clients caches the state for the same object and one or more clients invoke an update operation on the object. Even though each client writes its update straight through to the object by sending a remote message, other clients do not know this has happened and now hold an out-of-date copy.

To realize the performance benefits associated with client-side caching, you may be prepared either to dilute your object model or to use proprietary interfaces. However, we urge you not to underestimate the potential problems caused by loss of cache coherency. You will find quite a bit of CORBA literature that suggests solving the cache coherency problem by making a callback from the server to each client that holds a local copy of an updated object. The callbacks inform the clients that they are holding an out-of-date copy and possibly refresh the state of that copy.

However, the callback approach for cache coherency suffers from all the problems presented in Section 20.3 and is very difficult to scale. In addition, it is extremely difficult to maintain cache coherency for multiple clients without race conditions. Naive approaches lose as much performance in maintaining coherency as they gain by having client-side caching in the first place; typically, implementing more sophisticated approaches is too expensive as part of normal application development.

If you are considering the use of client-side caching, we suggest that you limit caching to situations in which clients have a natural one-to-one relationship with the objects whose state they cache. Provided each object is cached only by exactly one client, you avoid all cache coherency problems. If you want to apply client-side caching to objects that are shared by a number of clients, we recommend that you consider using the OMG Concurrency Control Service or Transaction Service (see [21] for details on both services).

22.4 Optimizing Server Implementations

Because CORBA is server-centric, most opportunities to improve performance and scalability present themselves on the server side. Because we have already seen the mechanisms involved, we briefly summarize the design techniques here. Note that these techniques are not mutually exclusive. Because you can create multiple POAs with

862

IT-SC book: Advanced CORBA® Programming with C++

different policies in the same server, you can apply more than one technique for different objects or even dynamically choose a technique at run time based on the access patterns of clients.

22.4.1 Threaded Servers

Threading should undoubtedly be at the top of your list when it comes to performance improvements. If a server is single-threaded, all calls from clients are serialized at the server end. If individual operations do a significant amount of work and run for more than a millisecond or so, single-threaded servers severely limit throughput. Note that threaded servers often perform better than non-threaded servers even on single-CPU machines because multithreaded servers can take advantage of I/O interleaving.

Keep in mind that you must plan for threading when you first design your server. It is highly unlikely that you will be able to add threading to a server that was designed as single-threaded program. Often, attempts to back-patch threading end up being more expensive than a complete reimplementation.

22.4.2 Separate Servant per Object

Typically, creating a permanent and separate servant for each CORBA object provides the best overall performance. Because each servant is permanently in memory, the ORB run time can dispatch calls directly without having to rely on a servant activator or locator to bring the servant into memory. The separate servant per object approach is most suitable for servers that can afford to hold all objects they provide in memory simultaneously.

22.4.3 Servant Locators and Activators

You can use servant locators or servant activators to activate servant instances on demand. Even if you have sufficient memory to hold all servants in memory simultaneously, servant activation can still be useful. If objects are expensive to initialize—for example, because initialization requires accessing a slow network—it may take too long to instantiate all servants during server start-up before an event loop is started. In this case, servant activation permits you to distribute the initialization cost over time instead of incurring it all at once during start-up. In addition, objects that are never used by clients are never initialized, whereas initialization during server start-up incurs the cost whether or not objects are used.

22.4.4 Evictor Pattern

The Evictor pattern (see Section 12.6) is most suitable for servers that need to scale to large numbers of objects but cannot hold a servant for all objects in memory simultaneously. In other words, the Evictor pattern sacrifices some performance in order to limit memory consumption. For many servers, the Evictor pattern provides excellent service, assuming that the server can hold at least the working set of objects in memory.

863

IT-SC book: Advanced CORBA® Programming with C++

22.4.5 Default Servants

Default servants allow you to implement an unlimited number of CORBA objects with a single C++ object instance. The main motivation for default servants is to increase scalability. Default servants allow you to exercise tight control over memory consumption at the price of losing some performance because default servants incur the cost of mapping object IDs to object state repeatedly for each operation. However, using default servants, the number of objects a server can support is effectively unlimited. Often, default servants are used as front-end objects for large database lookups; in that case, the number of objects a server can implement is limited only by the capacity of secondary storage.

22.4.6 Manufactured Object References

The create_reference_with_id operation on the POA interface decouples the life cycle of an object reference from the life cycle of its servant. This behavior is particularly useful if you must efficiently deliver object references as operation results. For example, the implementation of the list operation in the CCS controller benefits substantially from the ability to manufacture an object reference without having to instantiate a servant first. Note, however, that manufactured object references require you to also provide on-demand servant activation.

22.4.7 Server-Side Caching

The Evictor pattern provides an effective caching mechanism for object state if you have a distinct servant for each object. However, you can apply server-side caching at multiple levels. For example, a server that provides access to a database can choose to cache parts of the database in memory. You can combine such low-level caching with object-level caching to create a primary and a secondary cache. Such designs can result in excellent performance gains when properly matched to the access patterns of clients. In addition, server-side caching avoids the cache coherency problems of client-side caching (assuming that the server can guarantee coherency of its database cache).

22.5 Federating Services

Sooner or later, all the techniques just discussed fail. When client demand permanently outstrips server performance, no amount of clever caching can magically create the required performance. Typically, this situation arises in very large systems in which there are simply too many clients and objects for a single server to handle. In these situations, you have no choice except to distribute the processing load over a number of federated servers.

The OMG Naming, Trading, and Event Services are all examples of designs that federate naturally and easily. This is no accident—when you look at these services closely, federating them works for the following reasons.

864

IT-SC book: Advanced CORBA® Programming with C++

Each interface deals with a well-defined and orthogonal piece of functionality.

The servers in the federation are either ignorant of the fact that they are federated, or, alternatively, each server has knowledge only of its immediate neighboring servers and not of the federation as a whole.

Clearly, the first point is not particular to federated services; rather, it is a sign of welldefined interfaces in general. However, the second point is extremely important. Any attempt to federate more than four or five servers is likely to fail if the servers share global state in some form. Global state is the enemy of scalability [40]. For example, a federated design that requires every server in the federation to know about certain state changes cannot scale because the probability of at least one server being non-functional at any given time asymptotically approaches one as the number of servers increases [5].

If you decide on a federated design, make sure to strictly localize knowledge of the federation, and do not make any assumptions that rely on global state. You can use the Naming, Trading, and Event Services as a source of inspiration for your design. In addition, you should consider using the Trading Service if you want to provide a homogeneous view of the federated service to clients.

22.6 Improving Physical Design

Physical design refers to the way you distribute the functional components of an application over source files. In many ways, correct physical design of a system is just as important as the choice of the correct object model. If you correctly partition functionality over source files, maintainability and reusability of your code base will be greatly enhanced. Good physical design pays off as the system evolves over time because it reduces both complexity and the likelihood of errors being introduced as changes are made (see [11] for an excellent treatment of these topics).

In a CORBA context, it is useful to limit the extent to which CORBA-related functionality is visible throughout the system. This involves keeping the bulk of the source code free from CORBA artifacts and isolating all of the CORBA-related code in a few source files. Such an overall physical design is shown in Figure 22.1.

Figure 22.1 Separation of CORBA code from the business logic.

865

IT-SC book: Advanced CORBA® Programming with C++

This design separates the application code into two major sections. The core of the application, which contains the business logic (and typically most of the development investment), resides in a separate set of source files. None of these source files includes any ORB-related or IDL-generated header files. Instead, the core source files implement the bulk of the application using normal C++ classes and data types.

The core logic part of the code offers C++ interfaces to a delegation layer. The purpose of the delegation layer is to receive CORBA invocations from clients on one side and on the other side to delegate these invocations to the core logic in form of ordinary C++ method calls that use only C++ data types.

With this design, we achieve a clean separation of concerns. One part of the application—the delegation layer in a separate set of source files—deals with enabling the application for remote access via CORBA. The other (typically far larger) part of the application—the core logic in its own set of source files— implements the application semantics. Because the core source files do not include any CORBA header files, they are ignorant of the presence of CORBA in the system.

Using such a design offers quite a few advantages.

The design makes sense architecturally because it cleanly separates code related to remote communication from the main application body.

The header files generated by the IDL compiler for the C++ mapping can be large. Restricting the use of these headers to a small number of source files can yield dramatic reduction of compile and link times, with a corresponding reduction in development costs. Most of the source code is not concerned with the details of the C++ mapping. For large projects, the main advantage is that not every developer need be proficient in using the C++ mapping. Instead, the developers working on the core logic can use any established framework or class library they prefer.

866

IT-SC book: Advanced CORBA® Programming with C++

The core logic of the application can be tested separately from the CORBA-related functionality. You can use existing testing tools and debug the core logic without getting distracted by CORBA-related problems.

If your ORB contains a bug in its C++ mapping or in the way application code interacts with the skeleton, you can implement a work-around by touching only the delegation layer. Without such a layer, any work-around would likely affect a large number of source files and be much more costly to implement.

CORBA-related portability problems are isolated in the delegation layer. This is important if your code must work with ORBs from several vendors. Although the POA addresses most of the server-side portability issues, many applications are still written using the deprecated BOA. In addition, it is likely that the BOA legacy will be with us for some time to come. A delegation layer permits you to easily port the code between different implementations of the BOA and the POA while disturbing only a small part of the code base.

The delegation layer is a simple piece of code that contains almost no intellectual investment and can easily be written in a few days for even quite large interfaces. This means that you can afford to throw the delegation layer away if you move to a different ORB (or even an infrastructure other than CORBA) instead of trying to endlessly port the core logic. This is particularly important for long-lived applications that are maintained and adapted to different environments over many years.

These advantages are attractive, but, as always, they are balanced by a number of drawbacks.

A delegation layer as outlined here is difficult to back-patch into existing code, so typically it can be implemented only for new development projects.

The delegation layer adds to the run-time overhead of the application. For one thing, it must translate every incoming IDL type into a corresponding C++ type. Second, after the C++ call completes, it must translate any results delivered as C++ types back into IDL types.

The delegation layer creates a slight increase in code size. In addition, depending on the number of objects the application must support, it can increase the data size because of the need to map from servant instances in the delegation layer to corresponding C++ instances in the core logic. For every pair of such objects, you must keep an entry in a data structure similar to the Active Object Map.

As a rule, the advantages of a delegation layer far outweigh the disadvantages. Typically, the additional CPU time spent on copying between IDL and C++ data types is small compared with the overall execution time, so you will notice a performance degradation only if you are moving large amounts of data across the IDL interfaces. Similarly, the

867