Table of Contents Previous Next
Logo
IceGrid : 38.4 Getting Started
Copyright © 2003-2007 ZeroC, Inc.

38.4 Getting Started

This sections introduces a sample application that will help us demonstrate IceGrid’s capabilities. Our application “rips” music tracks from a compact disc (CD) and encodes them as MP3 files, as shown in Figure 38.3.
Figure 38.3. Overview of sample application.
Ripping an entire CD usually takes several minutes because the MP3 encoding requires lots of CPU cycles. Our distributed ripper application accelerates this process by taking advantage of powerful CPUs on remote Ice servers, enabling us to process many songs in parallel.
The Slice interface for the MP3 encoder is straightforward:
module Ripper {
exception EncodingFailedException {
    string reason;
};

sequence<short> Samples;

interface Mp3Encoder {
    // Input: PCM samples for left and right channels
    // Output: MP3 frame(s).
    Ice::ByteSeq encode(Samples leftSamples, Samples rightSamples)
        throws EncodingFailedException;

    // You must flush to get the last frame(s). Flush also 
    // destroys the encoder object.
    Ice::ByteSeq flush()
        throws EncodingFailedException;
};

interface Mp3EncoderFactory
{
    Mp3Encoder* createEncoder();
};
};
The implementation of the encoding algorithm is not relevant for the purposes of this discussion. Instead, we will focus on incrementally improving the application as we discuss IceGrid features.

38.4.1 Architecture

The initial architecture for our application is intentionally simple, consisting of a single IceGrid node responsible for our encoding server and running on the computer named ComputeServer. Figure 38.4 shows the client’s invocation of createEncoder and the actions that IceGrid takes to make this invocation possible.
Figure 38.4. Architecture for initial ripper application.
The corresponding C++ code for the client is presented below:
Ice::ObjectPrx proxy =
    communicator>stringToProxy("factory@EncoderAdapter");
Ripper::MP3EncoderFactoryPrx factory =
    Ripper::MP3EncoderFactoryPrx::checkedCast(proxy);
Ripper::MP3EncoderPrx encoder = factory>createEncoder();
Notice that the client uses an indirect proxy for the MP3EncoderFactory object. This stringified proxy can be read literally as “the object with identity factory in the object adapter identified as EncoderAdapter.” The encoding server creates this object adapter, and its IceGrid descriptor ensures that the object adapter uses this identifier. Since each object adapter must be uniquely identified, the registry can easily determine the server that creates the adapter, and the node responsible for that server. It is then the node’s responsibility to start the server if necessary.
The client’s call to checkedCast is actually the first remote invocation on the factory object. Once the server is started and the object adapter has registered itself, checkedCast completes and the client invokes createEncoder without further involvement by IceGrid.

38.4.2 Descriptors

We can deploy our application using the icegridadmin command line utility (see Section 38.20.1), but first we must define our descriptors in XML. For our initial architecture, the descriptors are quite brief:
<icegrid>
    <application name="Ripper">
        <node name="Node1">
            <server id="EncoderServer"
                exe="/opt/ripper/bin/server"
                activation="ondemand">
                <adapter name="EncoderAdapter"
                    id="EncoderAdapter"
                    registerprocess="true"
                    endpoints="tcp"/>
            </server>
        </node>
    </application>
</icegrid>
For IceGrid’s purposes, we have named our application Ripper. It consists of a single server, EncoderServer, assigned to the node Node11. The server’s exe attribute supplies the pathname of its executable, and the activation attribute indicates that the server should be activated on demand when necessary.
The object adapter’s descriptor is the most interesting. As you can see, the name and id attributes both specify the value EncoderAdapter. The value of name reflects the adapter’s name in the server process (i.e., the argument passed to createObjectAdapter) that is used for configuration purposes, whereas the value of id uniquely identifies the adapter within the registry and is used in indirect proxies. These attributes are not required to have the same value. Had we omitted the id attribute, IceGrid would have composed a unique value by combining the server name and adapter name to produce the following identifier:
EncoderServer.EncoderAdapter
The register-process attribute makes it possible for IceGrid to gracefully shut down the server when necessary (see Section 32.17.6), while endpoints defines one or more endpoints for the adapter.
Note that the value for endpoints does not contain any port information, meaning that the adapter uses a system-assigned port. Without IceGrid, the use of a system-assigned port would pose a significant problem: how would a client create a direct proxy if the adapter’s port could change every time the server is restarted? IceGrid solves this problem nicely because clients can use indirect proxies that contain no endpoint dependencies. The registry resolves indirect proxies using the endpoint information supplied by object adapters each time they are activated.
See Section 38.15 for detailed information on using XML to define descrip­tors.

38.4.3 Configuring the Registry and Node

The registry needs a subdirectory in which to create its databases, and the node needs a subdirectory for its own purposes. The server descriptor in Section 38.4.2 states that the executable resides in the directory /opt/ripper/bin, so we will add the subdirectories /opt/ripper/registry and /opt/ripper/node for use by the registry and node, respectively. These directories must exist before starting the registry and node.
We also need to create an Ice configuration file to hold properties required by the registry and node. The file /opt/ripper/config contains the following properties:
# Registry properties
IceGrid.Registry.Client.Endpoints=tcp -p 10000
IceGrid.Registry.Server.Endpoints=tcp
IceGrid.Registry.Internal.Endpoints=tcp
IceGrid.Registry.AdminPermissionsVerifier=IceGrid/NullPermissionsVerifier
IceGrid.Registry.Data=/opt/ripper/registry

# Node properties
IceGrid.Node.Endpoints=tcp
IceGrid.Node.Name=Node1
IceGrid.Node.Data=/opt/ripper/node
IceGrid.Node.CollocateRegistry=1
Ice.Default.Locator=IceGrid/Locator:tcp -p 10000
The registry and node can share this configuration file. In fact, by defining IceGrid.Node.CollocateRegistry=1, we have indicated that the registry and node should run in the same process.
Several of the properties define endpoints, but only the value of IceGrid.Registry.Client.Endpoints needs a fixed port. This prop­erty specifies the endpoints of the IceGrid locator service. IceGrid clients must include these endpoints in their definition of Ice.Default.Locator, as discussed in the next section.
The port numbers 4061 (for TCP) and 4062 (for SSL) are reserved for the registry by the Internet Assigned Numbers Authority (IANA).
The IceGrid.Registry.PermissionsVerifier property controls access to the registry (see Section 38.10.2).
Another property of interest is IceGrid.Node.Name, whose value is Node1. You may recall seeing this name mentioned in the descriptors from Section 38.4.2.
Finally, Ice.Default.Locator is used by the node to contact the registry. The next section provides more information on this property.

38.4.4 Configuring the Client

The client requires only minimal configuration, namely a value for the property Ice.Default.Locator (see Section 32.17.3). This property supplies the Ice run time with the proxy for the locator service. In IceGrid, the locator service is implemented by the registry, and the locator object is available on the registry’s client endpoints. The property IceGrid.Registry.Client.Endpoints defined in the previous section provides most of the information we need to construct the proxy. The missing piece is the identity of the locator object, which defaults to IceGrid/Locator:
Ice.Default.Locator=IceGrid/Locator:tcp -h registryhost -p 10000
The use of a locator service allows the client to take advantage of indirect binding and avoid static dependencies on server endpoints. However, the locator proxy must have a fixed port, otherwise the client has a bootstrapping problem: it cannot resolve indirect proxies without knowing the endpoints of the locator service.
See Section 38.19.3 for more information on client configuration.

38.4.5 Configuring the Server

Server configuration is accomplished using descriptors. During deployment, the node creates a subdirectory tree for each server. Inside this tree the node creates a configuration file containing properties derived from the server’s descriptors. For instance, the adapter’s descriptor in Section 38.4.2 generates the following proper­ties in the server’s configuration file:
EncoderAdapter.AdapterId=EncoderAdapter
EncoderAdapter.RegisterProcess=1
EncoderAdapter.Endpoints=tcp
Using the directory structure we established for our ripper application, the config­uration file for EncoderServer has the file name shown below:
/opt/ripper/node/servers/EncoderServer/config/config
Note that this file should not be edited directly because any changes you make are lost the next time the node regenerates the file. The correct way to add properties to the file is to include property definitions in the server’s descriptor. For example, we can add the property Ice.Trace.Network=1 by modifying the server descriptor as follows:
<icegrid>
    <application name="Ripper">
        <node name="Node1">
            <server id="EncoderServer"
                exe="/opt/ripper/bin/server"
                activation="ondemand">
                <adapter name="EncoderAdapter"
                    id="EncoderAdapter"
                    registerprocess="true"
                    endpoints="tcp"/>
                <property name="Ice.Trace.Network"
                    value="1"/>
            </server>
        </node>
    </application>
</icegrid>
When a node activates a server, it passes the location of the server’s configuration file using the --Ice.Config command-line argument. If you start a server manually from a command prompt, you must supply this argument yourself.

38.4.6 Dynamic Registration

By default, IceGrid will not permit a server to register unless you have create a server descriptor and deployed the server. On occasion, you may want clients to be able to bind indirectly to a server, but without having to first deploy the server. In other words, simply starting the server should be sufficient to make the server register itself with IceGrid and be reachable from clients.
You can achieve this by running the registry with the property IceGrid.DynamicRegistration set to a non-zero value. With this setting, IceGrid permits an adapter to register itself even without having a deployment descriptor that mentions the adapter. In addition, in the server, you must set IceGrid.Default.Locator (so the server can find the registry), and you must set <adapter-name>.AdapterId to an identifier that is unique within the registry. Setting of the <adapter-name>.AdapterId property causes the server to create indirect proxies that point at the registry, instead of direct proxies.

38.4.7 Starting IceGrid

Now that the configuration file is written and the directory structure is prepared, we are ready to start the IceGrid registry and node. Using a collocated registry and node, we only need to use one command:
$ icegridnode --Ice.Config=/opt/ripper/config
Additional command line options are supported, including those that allow the node to run as a Windows service or Unix daemon. See Section 38.19.2 for more information.

38.4.8 Deploying the Application

With the registry up and running, it is now time to deploy our application. Like our client, the icegridadmin utility also requires a definition for the Ice.Default.Locator property. We can start the utility with the following command:
$ icegridadmin --Ice.Config=/opt/ripper/config
After confirming that it can contact the registry, icegridadmin provides a command prompt at which we deploy our application. Assuming our descriptor is stored in /opt/ripper/app.xml, the deployment command is shown below:
>>> application add "/opt/ripper/app.xml"
Next, confirm that the application has been deployed:
>>> application list
Ripper
You can start the server using this command:
>>> server start EncoderServer
Finally, retrieve the current endpoints of the object adapter:
>>> adapter endpoints EncoderAdapter
If you want to experiment further using icegridadmin, issue the help command and see Section 38.20.1.

38.4.9 Review

We have deployed our first IceGrid application, but you might be questioning whether it was worth the effort. Even at this early stage, we have already gained several benefits:
• Our client can locate the MP3EncoderFactory object using only an indirect proxy and a value for Ice.Default.Locator. We can reconfigure the IceGrid application in any number of ways without modifying the client’s code or configuration in any way.
• We no longer need to manually start the encoder server before starting the client, because the IceGrid node automatically starts it if it is not active at the time a client needs it. If the server happens to terminate for any reason, such as an IceGrid administrative action or a server programming error, the node restarts it without intervention on our part.
• We can manage the application remotely using one of the IceGrid administra­tion tools. The ability to remotely modify applications, start and stop servers, and inspect every aspect of your configuration is a significant advantage.
Admittedly, we have not made much progress yet in our stated goal of improving the performance of the ripper over alternative solutions that are restricted to running on a single computer. Our client now has the ability to easily delegate the encoding task to a server running on another computer, but we have not achieved the parallelism that we really need. For example, if the client created a number of encoders and used them simultaneously from multiple threads, the encoding performance might actually be worse than simply encoding the data directly in the client, as the remote computer would likely slow to a crawl while attempting to task-switch among a number of processor-intensive tasks.

38.4.10 Adding Nodes

Adding more nodes to our environment would allow us to distribute the encoding load to more compute servers. Using the techniques we have learned so far, let us investigate the impact that adding a node would have on our descriptors, configu­ration, and client application.

Descriptors

The addition of a node is mainly an exercise in cut and paste:
<icegrid>
    <application name="Ripper">
        <node name="Node1">
            <server id="EncoderServer1"
                exe="/opt/ripper/bin/server"
                activation="ondemand">
                <adapter name="EncoderAdapter"
                    registerprocess="true"
                    endpoints="tcp"/>
            </server>
        </node>
        <node name="Node2">
            <server id="EncoderServer2"
                exe="/opt/ripper/bin/server"
                activation="ondemand">
                <adapter name="EncoderAdapter"
                    registerprocess="true"
                    endpoints="tcp"/>
            </server>
        </node>
    </application>
</icegrid>
Note that we now have two node elements instead of a single one. You might be tempted to simply use the host name as the node name. However, in general, that is not a good idea. For example, you may want to run several IceGrid nodes on a single machine (for example, for testing). Similarly, you may have to rename a host at some point, or need migrate a node to a different host. But, unless you also rename the node, that leads to the situation where you have a node with the name of a (possibly obsolete) host when the node in fact is not running on that host. Obviously, this makes for a confusing configuration—it is better to use abstract node names, such as Node1.
Aside from the new node element, notice that the server identifiers must be unique. The adapter name, however, can remain as EncoderAdapter because this name is used only for local purposes within the server process. In fact, using a different name for each adapter would actually complicate the server implementa­tion, since it would somehow need to discover the name it should use when creating the adapter.
We have also removed the id attribute from our adapter descriptors; the default values supplied by IceGrid are sufficient for our purposes (see Section 38.15.1).

Configuration

We can continue to use the configuration file we created in Section 38.4.3 for our combined registry-node process. We need a separate configuration file for Node2, primarily to define a different value for the property IceGrid.Node.Name. However, we also cannot have two nodes configured with IceGrid.Node.CollocateRegistry=1 because only one master registry is allowed, so we must remove this property:
IceGrid.Node.Endpoints=tcp
IceGrid.Node.Name=Node2
IceGrid.Node.Data=/opt/ripper/node

Ice.Default.Locator=IceGrid/Locator:tcp -h registryhost -p 10000
We assume that /opt/ripper/node refers to a local file system directory on the computer hosting Node2, and not a shared volume, because two nodes must not share the same data directory.
We have also modified the locator proxy to include the address of the host on which the registry is running.

Redeploying

After saving the new descriptors, you need to redeploy the application. Using icegridadmin, issue the following commands:
$ icegridadmin --Ice.Config=/opt/ripper/config
>>> application remove "Ripper"
>>> application add "/opt/ripper/app.xml"

Client

We have added a new node, but we still need to modify our client to take advan­tage of it. As it stands now, our client can delegate an encoding task to one of the two MP3EncoderFactory objects. The client selects a factory by using the appro­priate indirect proxy:
• factory@EncoderServer1.EncoderAdapter
• factory@EncoderServer2.EncoderAdapter
In order to distribute the tasks among both factories, the client could use a random number generator to decide which factory receives the next task:
string adapter;
if ((rand() % 2) == 0)
    adapter = "EncoderServer1.EncoderAdapter";
else
    adapter = "EncoderServer2.EncoderAdapter";
Ice::ObjectPrx proxy =
    communicator>stringToProxy("factory@" + adapter);
Ripper::MP3EncoderFactoryPrx factory =
    Ripper::MP3EncoderFactoryPrx::checkedCast(proxy);
Ripper::MP3EncoderPrx encoder = factory>createEncoder();
There are a few disadvantages in this design:
• The client application must be modified each time a new compute server is added or removed because it knows all of the adapter identifiers.
• The client cannot distribute the load intelligently; it is just as likely to assign a task to a heavily-loaded computer as it is an idle one.
We describe better solutions in the sections that follow.

1
Since a computer typically runs only one node process, you might be tempted to give the node a name that identifies its host (such as ComputeServerNode). However, this naming conven­tion becomes problematic as soon as you need to migrate the node to another host.

Table of Contents Previous Next
Logo