Batching II Distributed Garbage Collection

- 275 - In fact, there isnt much difference between the two method definitions. But when a test is run to compare the two different queries, the results are startling. For my test, I used an array of 87,880 four-letter strings. The test query result produced five strings. Using RMI, the client query took 35 times longer than the server query, and required the transfer of over 600,000 bytes compared to under 100 bytes for the server query. In absolute times, the server query gave a reasonable response time of well under a second. The client query produced an unacceptable response time of over 15 seconds that would have users wondering what could possibly be taking so long. Application partitioning similarly applies to moving some of the intelligence of the server to the client to reduce messaging to the server. A simple example is a client form where various fields need to be filled in. Often, some of the fields need to be validated according to data format or ranges. For example, a date field has a particular format, and the parts of the date field must fall in certain ranges e.g., months from 1 to 12. Any such validation logic should be executed on the client; otherwise, you are generating a lot of unnecessary network transfers. The example of date- field validation is perhaps too obvious. Most applications have a widget customized to handle their date-field entries. But the general area of user-interface presentation logic is one in which the logic should reside mostly on the client.

12.7 Batching II

To illustrate a second type of batching, we make a slightly different test query to the example in the last section. The only difference is in the choice of string to pass into the query, so that the result of the query is a large set. In this test, the result set is over 25,000 strings. The client query is still significantly longer than the server query, but even the server query now takes several seconds in absolute time. There is no reason to make the user wait for the whole result set to be transferred before displaying some of the results. Altering the application to send results in batches is quite easy. You need to add an intermediate object to hold the results on the server, which can send the results in batches as required: public class QueryResultHolderImpl implements QueryResultHolder { String[] results; int sentSoFar; public QueryResultHolderImplString[] results { this.results = results; sentSoFar = 0; } public resultSize {return results.length;} public nextBatchint batchSize { String[] batch = new String[batchSize]; System.arraycopyresults, sentSoFar, batch, 0, batchSize; sentSoFar += batchSize; return batch; } } You also need to add methods in the server object to support this batching object: public QueryResultHolder getBatchedServerQueryString obj { - 276 - return new QueryResultHolderImplgetQueryobj, getQueryArray ; } Now the client has the flexibility to request batches of results. The initial call to the query returns as fast as possible, with minimal network-transfer overhead: only one small proxy object is sent back in reply to the query. Note that the assumption here is that the QueryResultHolder object is not serialized when returned; instead, a proxy to the real object is passed to the client. The actual QueryResultHolder object holding the result set remains on the server. By wrapping the QueryResultHolder proxy, the optimization can be made completely transparent.

12.8 Low-Level Communication Optimizations

There are number of optimizations you can make to the low-level communication infrastructure. These optimizations can be difficult to implement, and it is usually easier to buy these types of optimizations than to build them.

12.8.1 Compression

Where the distributed application is transferring large amounts of data over a network, the communications layer can be optimized to support compression of the data transfers. In order to minimize compression overhead for small data transfers, the compression mechanism should have a filter size below which compression is not used for data packets. The JDK documentation includes an extended example of installing a compression layer in the RMI communications layer the main documentation index page leads to RMI documentation under the Enterprise Features heading. The following code illustrates a simple example of adding compression into a communications layer. The bold type shows the extra code required: void writeTransferbyte[] transferbuffer, int offset, int len { if len = 0 return; int newlen = compresstransferbuffer, offset, len; communicationSocket.writelen; communicationSocket.writenewlen; communicationSocket.writetransferbuffer, offset, newlen; communicationSocket.flush ; } byte[] readTransfer throws IOException { int len = communicationSocket.read ; if len = 0 throw new IOExceptionblah blah; int newlen = communicationSocket.read ; if newlen = 0 throw new IOExceptionblah blah; int readlen = 0; byte[] transferbuffer = new byte[len]; int n; whilereadlen newlen { n = communicationSocket.readtransferbuffer, readlen, len-readlen; n = communicationSocket.readtransferbuffer, readlen, newlen-readlen; if n = 0 readlen += n; else - 277 - throw new IOExceptionblah blah again; } int decompresslen = decompresstransferbuffer, 0, newlen; if decompresslen = len throw new IOExceptionblah blah decompression; return transferbuffer; }

12.8.2 Caching

Caching at the low-level communications layer is unusual and often a fallback position where the use of the communications layer is spread too widely within the application to retrofit low-level caching in the application itself. But caching is generally one of the best techniques for speeding up clientserver applications and should be used whenever possible, so you could consider low-level caching when caching cannot be added directly to the application. Caching at the low-level communications layer cannot be achieved generically. The following code illustrates an example of adding the simplest low-level caching in the communications layer. The bold type shows the extra code required: void writeTransferbyte[] transferbuffer, int offset, int len { if len = 0 return; check if we can cache this code CacheObject cacheObj = isCachabletransferbuffer, offset, len; if cacheObj = null { Assume this is simple non-interleaved writes, so we can simply set this cache obj as the cache to be read. The isCachable method must have filled in the cache, so it may include a remote transfer if this is the first time we cached this object. LastCache = cacheObj; return; } else { cacheObj = null; realWriteTransfertransferbuffer, offset, len; } } void realWriteTransferbyte[] transferbuffer, int offset, int len { communicationSocket.writelen; communicationSocket.writetransferbuffer, offset, len; communicationSocket.flush ; } byte[] readTransfer throws IOException { if LastCache = null { byte[] transferbuffer = LastCache.transferBuffer ; LastCache = null; return transferbuffer; } int len = communicationSocket.read ; if len = 0 throw new IOExceptionblah blah; int readlen = 0; - 278 - byte[] transferbuffer = new byte[len]; int n; whilereadlen newlen { n = communicationSocket.readtransferbuffer, readlen, len-readlen; if n = 0 readlen += n; else throw new IOExceptionblah blah again; } return transferbuffer; }

12.8.3 Transfer Batching

Batching can be useful when your performance analysis indicates there are too many network transfers occurring. The standard batching technique uses two cutoff values: a timeout and a data limit. The technique is to catch and hold all data transfers at the batching level just above the real communication-transfer level and send all data transfers together in one transfer. The batched transfer is triggered either when the timeout is reached or when the data limit which is normally the batch buffer size is exceeded. Most message-queuing systems support this type of batching. The following code illustrates a simple example of adding batching to the communications layer. The bold type shows the extra code required: method synchronized since there will be another thread which sends the batched transfer if the timeout is reached void synchronized writeTransferbyte[] transferbuffer, int offset, int len { if len = 0 return; if len = batch.length - 4 - batchSize { batch is too full to take this chunk, so send off the last lot realWriteTransferbatchbuffer, 0, batchSize; batchSize = 0; lastSend = System.currentTimeMillis ; } addIntToBatchlen; System.arraycopytransferbuffer, offset, batchBuffer, batchSize, len; batchSize += len; } void realWriteTransferbyte[] transferbuffer, int offset, int len { communicationSocket.writelen; communicationSocket.writetransferbuffer, offset, len; communicationSocket.flush ; } batch timeout thread method void run { int elapsedTime; for;; { synchronizedthis { elapsedTime = System.currentTimeMillis - lastSend; if elapsedTime = timeout batchSize 0 { realWriteTransferbatchbuffer, 0, batchSize; - 279 - batchSize = 0; lastSend = System.currentTimeMillis ; } } try{Thread.sleeptimeout - elapsedTime;}catchInterruptedException e{} } } realReadTransfer throws IOException { Dont socket read until the buffer has been completely used if readBatchBufferlen - readBatchBufferOffset 0 return; otherwise read in the next batched communication readBatchBufferOffset = 0; int readBatchBufferlen = communicationSocket.read ; if readBatchBufferlen = 0 throw new IOExceptionblah blah; int readlen = 0; byte[] readBatchBuffer = new byte[readBatchBufferlen]; int n; whilereadlen readBatchBufferlen { n = communicationSocket.readreadBatchBuffer, readlen, readBatchBufferlen-readlen; if n = 0 readlen += n; else throw new IOExceptionblah blah again; } } byte[] readTransfer throws IOException { realReadTransfer ; int len = readIntFromBatch ; if len = 0 throw new IOExceptionblah blah; byte[] transferbuffer = new byte[len]; System.arraycopyreadBatchBuffer, readBatchBufferOffset, transferBuffer, 0, len; readBatchBufferOffset += len; return transferbuffer; }

12.8.4 Multiplexing

Multiplexing is a technique where you combine multiple pseudo-connections into one real connection, intertwining the actual data transfers so that they use the same communications pipe. This reduces the cost of having many communications pipes which can incur a heavy system load and is especially useful when you would otherwise be opening and closing connections a lot: repeatedly opening connections can cause long delays in responses. Multiplexing can be managed in a similar way to the transfer-batching example in the previous section.

12.9 Distributed Garbage Collection

Distributed systems typically require distributed garbage collection. If a client holds a proxy to an object in the server, it is important that the server does not garbage-collect that object until the client - 280 - releases the proxy and it can be validly garbage-collected. Most third-party distributed systems, such as RMI, handle the distributed garbage collection, but that does not necessarily mean it will be done efficiently. The overhead of distributed garbage collection and remote reference maintenance in RMI can slow network communications by a significant amount when many objects are involved. Of course, if you need distributed reference maintenance, you cannot eliminate it, but you can reduce its impact. You can do this by reducing the number of temporary objects that may have distributed references. The issue is considerably more complex in a multiuser distributed environment, and here you typically need to apply special optimizations related to the products you use in order to establish your multiuser environment. However, in all environments, reducing the number and size of the objects being used is typically the most effective optimization. The techniques described in Chapter 4 , are relevant to reducing the number of objects in a distributed system, and should be applied where possible.

12.10 Databases