A service-oriented architecture prescribes very little about service implementation. The only thing that can be inferred from a design being SOA is that different concerns are handled by different applications, and that those applications pass messages back and forth. What language a particular application is written in, what messaging transport applications use to communicate, how the messages are structured, and even what is in the messages is up to the software architect.
That said, you don’t need to start from scratch determining a byte order for your transport layer each time you write a service. There are a handful of industry-standard approaches for service-to-service communication that you can take advantage of to get rolling right away. The two most popular have been XML-RPC and SOAP, and more recently, REST.
In this chapter, we’ll examine the pros and cons of each approach. However, before comparing and contrasting REST, XML-RPC, and SOAP—an endeavor that generally pleases no one and disgruntles everyone—we’ll first go over two sets of properties for a good service design. In the first set are properties that ensure your service is maintainable and interoperable with current and future clients. The second set is guidelines for keeping a service lean and fast.
As has been mentioned, a service is accessible only through its API. The service, via this contract, is the gatekeeper of any data it manages. This leads naturally to two desirable properties. The first is that the implementation within the service should be completely abstracted from the clients. The implementation—be it software code, database table layout, or even physical composition—is free to change as long as the contract of the API is maintained, without disruption to the client. The second property is that the service API should be accessible to as many types of clients as possible. Because the service is the gatekeeper, it should in a common language that is accessible to many types of clients. Let’s look at each of these two properties more closely.
Implementation Details Are Hidden
Just as in object-oriented programming the implementation details of a class are hidden behind its public interface, in a service-oriented architecture the details of each service should be hidden behind its public service API. Figure 14-1 shows an example service, which is made up of a number of service machines (“box 1” through “box N”) and a database. The first level of abstraction belongs to the load balancer. A load balancer hides the detail that there are a number of servers by creating a virtual IP (VIP) and spreading traffic among the service boxes to balance load. It should be possible for any request to be handled by any service box. It should also be possible for any service box to be shut down (taken “out of service”) without disruption to the clients of the service. This implies that each service request must be stateless. The ability of a request to the service to succeed cannot depend on the request being directed to the same service box as previous requests. If this were the case, any in-process request sets that are bound to one back-end service server would be aborted if that service box was taken out of the rotation. On the other hand, if any service box can handle any service request, a change in the configuration of the service’s hardware will go unnoticed by clients.
Figure 14-1. Service abstraction barrier
This is in contrast to the accepted (but problematic) convention of many user-facing websites, which maintain a session object for each visitor to the site. The session object often contains lots of information that is critical to serving subsequent requests to the user. If the sessions are stored locally on a particular web server, a user’s traffic must always be directed to that server for the session to be maintained and utilized. This is commonly supported by load balancers via “sticky sessions,” in which the load balancer issues a cookie to the user on their first request, and then uses that cookie to direct subsequent requests. When that server is shut down for a software upgrade, or is lost due to a hardware failure, all browsing by users who were being directed to that server by the load balancer is disrupted.
One way to get around lost session-state on a single server is to store sessions in a shared location. This, of course, has its own problems, because the session store becomes a single point of failure. In general, this is to be avoided. In Chapter 19, an alternative will be provided for user-facing sites. For servers, the solution is to design an API in which each request is independent of every other request.
Principle 1: A service should be internally fault-tolerant. This is accomplished by load-balancing and statelessness between requests.
The second abstraction barrier lies with the API itself. The API hides the implementation details of the application code. The API is truly a contract: a guarantee to clients that they can communicate with the service in a specific way, and with specific results. Once the contract is published to clients, via WSDL (web service description language) for XML-RPC or SOAP services, or via WADL (web application description language) for REST services, it should never change. The only acceptable time an API may change is in a major publicized version. However, even under the condition of a version change, generally the old version of the API should continue to be supported for some time to give clients an opportunity to upgrade to the new version without loss of service. Changing the contract because it makes changes within the service more convenient is unacceptable once clients are consuming the service API. Changes that are not backward-compatible are guaranteed to break all clients’ implementations.
The psychological way to enforce the immutability of a service API, in some organizations, is to have software developers write their W*DL files by hand, and then generate stub application code based on the W*DL. Application developers then fill out the stubs to instrument the application. A change to the W*DL requires regenerating stubs and losing large chunks of prewritten code, a strong deterrent. On the other hand, many frameworks, including Rails, generate WSDL on the fly based on methods written by developers. This can cause an apparently innocuous change in a method signature to have a pervasive effect on the interoperability of the service with all of its clients. In such an environment, where W*DL is generated from code rather than vise versa, developers must be aware of what changes will affect the service API and be disciplined to avoid doing so.
Principle 2: The service API, except under major version changes, must not change. Changes must occur within the application, but not be visible to service clients.
API Is Accessible
For service clients to make use of the service easily, the service API must be accessible. The first principle of accessibility is that the API is published and documented. Published, in this sense, means the API is available in a machine-readable format so that clients can automatically generate their own interface to the service. They can send messages to the service and process results without needing to first obtain a custom client library. As noted, W*DL service description files are the standard way to publish the service’s API contract. When they are available on the network at a predetermined and permanent URI, the service API is said to be discoverable.
Principle 3: The service API should be discoverable via a network accessible WSDL or WADL description file.
Documented means there is some way for developers to understand the API and its effects. A W*DL file is a complete description of the service, and both formats support comments mixed into the XML descriptions of the API. Tools exist to transform machine-readable W*DL files into human-readable documentation that is easier on the eyes than raw XML.
In a perfect world, all of your services and service clients would be Rails applications. However, in a real enterprise environment, the clients of a service are likely to be written in a variety of languages, using a variety of technologies. Your organization may have legacy applications that are still maintained, but aren’t likely to be rewritten in Rails any time soon. You may inherit applications through a merger or acquisition, and have a need to integrate a .NET or Java-based application with your own services. Rails is an excellent framework for web application development, but it may one day be superceded by an even better framework. And finally, some clients may not be web applications at all, and in those cases, Rails may not be the right choice for implementing the new clients.
All of this means that, as mentioned earlier, rather than design a communication mechanism from the byte-ordering on up, it’s important to use a technology that already has wide support, and easily integrates into a variety of other technologies: Java, .NET, C++, etc. XML-RPC client libraries are available on nearly every platform, and a growing number of platforms are beginning to support REST in some way. Various degrees of SOAP functionality are also available on many platforms.
Principle 4: The service should communicate in an industry-standard way so that clients written in any language or with any framework can participate.
API Design Best Practices
So far, four principles of service design have been laid out. Now we turn to the API itself, and define four guidelines for an API design that will ensure your application maintains a high level of user-perceived speed. The jump from monolithic application design to service-oriented design is not without trade-offs. In exchange for reduced local complexity and other benefits of a modular SOA design, you give up locality of information. Because the overall architecture no longer features a single application directly connected to a database, overhead is imposed in the form of network messaging between services. This overhead can either be detrimental to performance, or it can be barely noticeable if the API is designed to minimize overhead.
Four guidelines described below can help keep the overhead in the “barely noticeable” category. Note that these guidelines may seem contradictory at times. That is why they are guidelines rather than rules. An appropriate balance must be found between each of the guidelines, based on the situation at hand.
Send Everything You Need
In object-oriented programming, it’s a best practice to have lots of small methods that each perform a small function and return a small piece of data. Within an application, method calls are cheap, so writing small methods that are easily testable is often desirable. In a service-oriented architecture, on the other hand, method calls are processed remotely within the service, and any call, therefore, incurs the overhead of the network. Rather than issuing lots of finely grained method calls, as we might within and object-oriented program, it can be much more efficient to get all of the data necessary in one shot.
For example, imagine we want to render a page for a movie along with the showtimes in a particular location. The following calls seem reasonable if the entire application is running on a single machine:
@movie = Movie.find(params[:id]) @rating = Rating.find(@movie.rating_id) @showtimes = MovieShowtime.find_all_by_movie_id_and_location(@movie.id, params[:zip], :include => :theatre)
In a scenario where each method call incurs network overhead, the above would require at least three network operations. If our network API did not allow theatre information to be included with a showtime as is possible with ActiveRecord, we would incur 3 + N network operations, where N is the number of showtimes found for our movie. The service client code might look something like this (service calls are in bold):
@theatres = Hash.new @movie = MoviesClient.getMovie(params[:id) @rating = MoviesClient.getRating(@movie.rating_id) @showtimes = MoviesClient.getShowtimesByMovieAndLocation(@movie.id, params[:zip)) for showtime in @showtimes do if !@theatres[showtime.theatre_id] @theatres[showtime.theatre_id] = MoviesClient.getTheatre(showtime.theatre_id) end end
Code styled this way would certainly put the overhead of our service API into the detrimental rather than barely noticeable category. If we know ahead of time that clients of our service will frequently request information about movies within a certain location, and that theatre, movie, and rating information is often required, we can design the API to return all of this information within a single request. Rather than have very fine-grained API methods, we could define a method that, given a movie ID and a zip code, would return movie information, the rating description, showtimes, and theatre information all in one request:
@showtime_data = MoviesClient.getShowtimeData(movie_id, zip)
The @showtime_data variable would then be a hash or struct, as shown in Figure 14-2. The hash has a :movie field, which contains the rating information, denormalized into a rating field. The hash also contains a :showtimes field, which is an array, one element per movie theatre. Within that array, each element has members describing the theatre, and a member, :showtimes, which is another array, one element per showtime at that theatre. This data structure, while more complex than each of its component parts as described in the physical, ActiveRecord models of the back-end service, is just what the average client needs in order to display movie showtimes to a user. This data structures is therefore part of the logical model of our application (Figure 14-3). Logical models are the topic of the next chapter.
Figure 14-2. A result object for the getShowtimeData method, which returns all needed for displaying showtime information in a single request
Figure 14-3. Finding the correct grain for the logical model reduces the number of service calls
API Guideline 1: Design the API with a granularity of data that minimizes the number of requests a client must make in the common case. The goal is one service request per client action.
Limit Round Trips
In some cases, we need the result of a first service request in order to build all of the parameters needed for a second service request. For example, on a shopping website, after placing an order, we might want to display additional items that the user may be interested in purchasing, based on the purchasing habits of others. It might look something like this:
order_status = ShoppingService.complete_order(credit_card_info, @items) if order_status == ShoppingService::ORDER_SUCCESS related_items = ShoppingService.get_related_items(@items) end
While having these two API methods—complete_order and get_related_items—makes perfect sense, calling them this way incurs the overhead of two service requests when the order is successfully completed, resulting in slower user-perceived performance. If we know ahead of time that it will be common for clients to request related items after completing an order, we can instead alter the return type for complete_order such that it returns not only a status value, but also an array of related items if the order is successful. This results in the same amount of processing on the service side, but the client does not incur the overhead of two round-trips to the service (Figure 14-4).
Figure 14-4. Limiting the number of round-trips reduces overall communication penalty
Therefore, when designing an API, it’s necessary to think carefully about how it will be used. You don’t need to foresee every possible use, but today’s common cases are likely to be tomorrow’s as well. In this example, it wouldn’t make sense to remove get_related_items from the API. There will certainly be times when it is convenient to call that service method independently from placing an order. However, if we can guess what most clients will want to do after placing an order, it makes sense to be proactive and get the data to the client right away.
API Guideline 2: Requests should not depend on the results of other requests. Rather than requiring a client to chain multiple requests to get the data it needs, guess what is needed and provide it up front.
Look for Opportunities for Parallelization
Even with the first two guidelines in hand, it will often not be possible or even desirable to get everything needed from a service in a single request. The service may be responsible for retrieving a number of unrelated types of information that would not be logical to return together. In such cases, it makes sense to make multiple service requests, one for each distinct type of information. But this doesn’t mean we need to suffer the overhead of multiple round-trips. If the inputs to one method call do not depend on the result of another, we don’t need to wait for the first request to return data before making the second request. Instead, we can dispatch all of the unrelated service calls all at once, and then wait for them all to return before proceeding.
Figure 14-5 illustrates the time savings that can be realized by dispatching multiple requests at the same time. Here only two requests are shown, but the benefits increase with the number of requests that can be parallelized. When the requests are chained, you pay the price of each request, plus accumulated overhead for each call. When the requests are dispatched simultaneously, you pay the price of the longest running service call, and overhead for only one network operation.
Figure 14-5. With parallelized requests, the cost is the time of the slowest request
For example, if we are making one request to the movie service to get showtime data, and another request to an ads server to get third-party offers to display, we can make them in parallel. Normally we would see the following code:
@showtime_data = MoviesClient.getShowtimeData(movie_id, zip) @advertisements = AdsClient.getAdsByLocation(zip)
However, since we know the requests are unrelated, we can dispatch them both at once, and then wait for the results. The code might look like this:
t1 = Thread.new do @showtime_data = MoviesClient.getShowtimeData(movie_id, zip) end t2 = Thread.new do @advertisements = AdsClient.getAdsByLocation(zip) end t1.join t2.join
API Guideline 3: Where multiple service requests are required, encourage and support parallelization. Make the cost of communication with a service equal to the slowest service operation, rather than the sum of all service requests.
Send As Little As Possible
The guidelines described above were not created in a vacuum. I learned guidelines 1 through 3 while working at Amazon.com. We had very tight service-level agreements (SLAs) regarding how long any page could take to load to ensure that the site would feel as fast as possible. This translated into SLAs for services; to be included on a page that should take one second to render, the service call might, for example, need to guarantee delivery of data in 0.25 seconds. These tight requirements led to service API designs in which round-trips were limited, more than enough information was always returned to callers, and parallelization was possible and widespread. Caching was also key, as it removed the database as a bottleneck and prevented requests times from suffering as traffic increased.
When I brought the three guidelines I gleaned from my time at Amazon to the Rails world, I was rudely awakened. At Amazon, we wrote back-end services using C++ and Java, so the services themselves were very fast. As I have mentioned before in this book, Ruby is not a fast language. In Chapter 2, we saw that the extensive processing of data can be extremely costly—even something as simple as instantiation of ActiveRecord objects where hashes will suffice. There, we shaved 50% off the time of an ActiveRecord query by preventing the results from being unmarshalled into heavy objects.
It turns out that translating data to and from service calls can be an expensive operation as well. Request results are commonly sent from server to client as XML data. So first imagine the overhead incurred translating database results to ActiveRecord objects. Then add to that the overhead of marshalling those objects into XML. All of this is time spent in the slowest part of the system—Ruby—and the results can be surprising. We designed one API method, which followed Guideline 1 and returned a very large chunk of data. On the back-end service, this data was cached, so we cut out database time. Retrieving the data from the cache was instantaneous. However, due to the sheer size of the data and the amount of XML processing that was required to generate the service response, the service request was consistently taking 12 seconds to process.
From that experience, we came up with the fourth and final guideline, illustrated in Figure 14-6 (which can seem to be in direct conflict with Guideline 1).
API Guideline 4: Avoid expensive XML serialization and deserialization costs by sending as little information as necessary in any given request.
Figure 14-6. Limiting the amount of data to be marshaled to XML can reduce response times dramatically
While the guidelines seem to conflict, they actually don’t. Let’s say a movie has showtimes and reviews. Guideline 1 encourages us to use an API that returns all of this data together, because it’s likely it will often be shown together on the same web page. But it’s unlikely that any web page on our site would contain all of the information about the movie, all of the showtimes, and all of the reviews in their entirety. That would be too much information and would be almost universally considered a poorly designed web page.
Guideline 4 does not restrict what kinds of data we send back, but how much. For example, if there are hundreds of movie reviews, it’s unlikely that we need them all when we’re requesting details about the movie itself. Returning five is probably sufficient, as long as there’s another API method that does let us get them all if we want to. Similarly, when returning related “follow-on” data that’s likely to be useful per Guideline 1, it’s often a good idea to return this data in an abridged or summarized form. On the movie’s “gateway page,” which would link to all showtimes and all reviews, the first few sentences of the first five reviews are probably all that’s needed. Similarly, a list of local theatres, without actual showtimes, is enough detail. From there, a user might click a link to see all showtimes or all reviews, or click a “read more” link on a particular spotlighted review to get the full text of just that review. With this in mind, Guidelines 1 and 4 could be restated together as, “Send everything you need, but no more.”
REST Versus XML-RPC Versus SOAP
We now come to one of the great battles being fought by designers of service-oriented architectures: which protocol is best? The three main contenders in this arena are SOAP, XML-RPC, and REST. Even within the Rails community, the answer has not been consistent. Up to version 2.0, ActionWebservice, which makes it easy to make XML-RPC and RPC-based SOAP services, was included with the core of Rails. In version 2.0, it was dropped in favor of ActiveResource, which provides facilities for working with REST services. XML-RPC, SOAP, and REST all provide a means to a SOA end; they all facilitate lopping off a vertical slice of an application, and providing remote access over the Internet. And with some allowed deviances from pure REST (or “high REST,” as some call it), any of these three alternatives can be equally well suited to the task of representing any given API.
What differentiates the protocols is the ethos of how a remote protocol should behave and how it should be used. The cultures that have grown around each protocol reflect different views of how systems should be interconnected. In this book, I will take the universalist and also universally contrarian view and suggest that there is no single best protocol, and that the decision of which to use should be based on the problem at hand. Each design problem is unique: some problems are more easily solved with a REST interface, while others are more easily solved with an RPC-style interface.
Although the Rails community is putting its full backing in REST-based approaches, widespread REST-based services are not yet to be found. Part of this scarcity is related to a dearth of tools. As tools evolve, REST may in fact be the answer to the question Which protocol? Until then, we must remind ourselves that in the enterprise practicality is at least as important as purity.
The remainder of this chapter explores the difference ethos of the three protocols. We’ll also explore the scenarios—regarding problem space and audience—when one protocol can make more sense than another.
RPC stands for remote procedure call, and XML-RPC is a protocol for making procedure calls remotely, using XML to encode the parameters on the way in and the return values on the way out. The notion behind XML-RPC is very simple and straightforward. A service server implements a method. A service client invokes that method, which results in a network request to the server over HTTP. The server executes the method and returns the result of the invocation.
In XML-RPC, services define an endpoint URL, which clients access to make their requests. The method the client wishes to invoke, as well as any parameters required by the method, is part of the XML payload of the request, which is an HTTP POST.
The ethos behind XML-RPC, as the name implies, is procedural. The methods typically seen in an XML-RPC API are action-oriented: get_movie(), get_showtimes(), place_order(), and so on.
Implementing an XML-RPC client in Rails, as well as many other frameworks, is as easy as falling off a horse. The ActionWebservice gem, which was included in the core of Rails up to version 2.0, makes it trivial to define an XML-RPC service and automatically publish a WSDL file describing the methods available to clients.
Before moving on, it’s worth noting that a major benefit of an XML-RPC-style interface is the ease with which you can hide implementation details from clients. Normally, this might be something you would assume as a property of any SOA service, especially after reading the previous chapter. However, these days, the property of abstraction must be noted due to the way many Rails-based REST services are being written these days. These implementations are sacrificing the very desirable property of decoupling that maintaining a solid service-based abstraction barrier would provide, a significant basic advantage of SOA.
In XML-RPC, the scope of what the procedures definde as part of the API can do is limitless. For instance, the API need not have a one-to-one relationship with the physical data model. It may initially, if your logical model does not differ greatly from the physical tables, but the API is free to diverge without consequence as you change the internals of your service, but maintain your original API for legacy clients.
In addition to not being tied to a data model in general, an XML-RPC method implementation need not be tied to a particular table or even a particular row in a table. Exactly the opposite is true: a method can access whatever it wants, wherever it wants to (within the convinces of the host language, of course). For example, the following would be a perfectly acceptable XML-RPC method call, even though it clearly affects multiple rows in a table on the server:
BankAccountService.transfer_funds(acct_1, acct_2, amt)
Note also that the implementation of this method on the server absolutely requires a database transaction to ensure that a race condition doesn’t corrupt the account balances (see Chapter 1 for a refresher on why). The following could be disastrous if executed on the client without a means of defining a transaction on the database connected to the bank service server:
bal1 = BankAccountService.getBalance(acct_1) bal2 = BankAccountService.getBalance(acct_2) BankAccountservice.setBalance(acct_1, bal1 - amt) BankAccountservice.setBalance(acct_2, bal2 + amt)
It’s important to note this example up front to illustrate how easy it is to define the kind of API necessary to solve a problem at hand with XML-RPC, which is completely flexible regarding the methods that can be defined. As we will see in our discussion of REST, with “pure REST” it can be quite difficult to design an API that allows for a transaction on the server side, as in our first example above. Of course, there are solutions to this problem in the REST world, which will be described in turn as well.
SOAP is an incredibly versatile protocol for building a variety of service-oriented architectures, including some architectures not covered in this book. Originally SOAP stood for “Simple Object Access Protocol,” but as the breadth of the SOAP specification ballooned, the words behind the acronym were dropped by the W3C. Indeed, the number of layers added atop SOAP are staggering. These layers include such additional specifications as WS-Addressing, WS-Security, WS-Polling, WS-Eventing, WS-Enumeration, WS-Reliable Messaging, and more. This conglomeration of specifications, collectively known as WS-*, has been affectionately named “WS Deathstar” by SOAP’s numerous critics.
While SOAP as an idea has great promise, the problem is that it’s difficult to find a complete implementation of the SOAP standard anywhere on the planet. Microsoft’s and some of Java’s development environments make it somewhat easy to create SOAP services that utilize these higher-level parts of the SOAP protocol. However, when you use these service-builders and the more esoteric parts of the SOAP specification, you’re limited to other Microsoft and Java clients to consume the services. You don’t ever see SOAP services of this nature publicly accessible as “web-services” for public consumption, because almost no one could actually make use of them.
In my own experience, the designers of SOAP systems fall into two camps. In the first camp are seasoned and battle-trained software architects. They understand the inaccessibility that SOAP engenders, but they don’t care because they are solving a technical problem internal to their own organization, and they are their own clients. There’s nothing wrong with these people or their SOAP services; likely you’ll never have to tangle with either. The second camp doesn’t understand the interoperability problems they will soon face with non-Microsoft clients, but they ended up with a SOAP service anyway; they hit a pretty button in a Microsoft IDE and the choice was made for them. With no offense intended to those in the former camp, I find that most SOAP users fall in the latter.
As it happens, the majority of SOAP use out there is simply as a wrapper for performing remote procedure calls, just like XML-RPC, except at a slightly higher cost. Because SOAP could be so much more versatile, there is a bit more overhead in the message envelopes in SOAP than there is with plain old XML-RPC. Indeed, the SOAP client and server implementations available in Ruby are also based on ActionWebservice, and only RPC-style SOAP functionality is provided. When using ActionWebservice, you don’t really make a choice at all regarding whether you are creating an XML-RPC or a SOAP-based service. Because only the RPC subset of SOAP is implemented, you’re making both at once.
To the Rails developer, XML-RPC and SOAP are functionality equivalent, although XML-RPC might be slightly faster in practice. Because in the end SOAP offers the Rails developer nothing she isn’t already getting with XML-RPC, SOAP won’t be discussed any further in this book.
|Chapter 13 : SOA Primer
||Chapter 15 : An XML-RPC Service|