Modeling resources in ABS
=========================
This example shows a high-level model of a typical three-layer system
serving client requests. The components are one load balancer,
multiple web workers and an underlying database server. The model also
contains one or more clients, which send requests to the load
balancer.
The complete code to this example is here:
``__.
The basic architecture is as follows:
.. figure:: images/resource-modeling/basic-model-no-resources.png
:alt: Three-layer web app model
Three-layer web app model
The ``Loadbalancer`` class receives incoming messages from clients,
chooses a ``Webworker`` object and routes the request to it. The
worker, in turn, calls the ``query`` method of the ``Database``
instance, then returns a result to the client via the load balancer.
Functional model without resources
----------------------------------
The ABS model discussed in this section is here:
`basic-model-no-resources.abs
`__.
We use a basic model to implement the control flow of a typical web
request, skipping any application logic. Clients access the service by
calling ``processRequest()`` on a ``LoadBalancer`` isntance which
keeps a pool of ``Webworker`` components. All of them access the same
``Database`` object. The interface structure is as follows::
interface Database {
String query();
}
interface Webworker {
String processRequest();
}
interface Loadbalancer {
String processRequest();
}
Note that there is no interface for clients, since they do not expose
any methods to call from the outside.
We abstract away from most of the processing and are only concerned with
control flow, so the ``Database`` implementation returns a constant
string::
class Database implements Database {
String query() {
return "Result";
}
}
In the same vein, the ``Webworker`` class models database communication
and local processing::
class Webworker(Database db) implements Webworker {
String processRequest() {
String result = await db!query();
return result + " from " + toString(this);
}
}
The class ``LoadBalancer`` fills its pool of ``Webworker`` instances on
startup, and routes requests to a free webworker::
class Loadbalancer(Int nWorkers, Database db) implements Loadbalancer {
List workers = Nil;
Unit run() {
Int i = 0;
while (i < nWorkers) {
Webworker w = new Webworker(db);
workers = Cons(w, workers);
i = i + 1;
}
}
String processRequest() {
await length(workers) > 0;
Webworker w = head(workers);
workers = tail(workers);
String result = await w!processRequest();
workers = appendright(workers, w);
return result;
}
}
Each ``Client`` instance sends one request per time unit to the
``LoadBalancer`` from its ``run`` method::
class Client (Loadbalancer lb) {
Unit run() {
while (timeValue(now()) < 100) {
String s = await lb!processRequest();
println(s + " at " + toString(now()));
await duration(1, 1);
}
}
}
Finally, the main block sets up and starts the model::
{
Database db = new Database();
Loadbalancer lb = new Loadbalancer(5, db);
new Client(lb);
await duration(100, 100);
}
Adding resources to the model
-----------------------------
We now add resource locations and resource consumption to the model of
the previous section.
The ABS model discussed in this section is here:
`basic-model-resources-v1.abs `__.
The basic idea is to create objects at resource-carrying *locations*
(see :ref:`sec:deployment-components`). ``DeploymentComponent`` is a
predefined ABS class that models a virtual machine, or in general a
location that supplies computation resources to the cogs running on
it.
Specific parts of the code are annotated with *resource costs*.
Executing these code parts consumes resources, and execution is
delayed when the location has run out of resources. Resources are
refreshed periodically.
The interesting point is that adding deployment and cost information
to an ABS model can be done piecemeal, and is minimally invasive.
.. figure:: images/resource-modeling/basic-model-resources.png
:alt: Three-layer web app model, with deployment components
Three-layer web app model, with deployment components
In the updated example, we deploy the ``Webworker`` and ``Database``
objects on a deployment component each. Note that we do not have to
add a deployment component to every new object; we can leave some
parts unspecified or abstract. In the example, we make use of the
``CloudProvider`` class from the standard library, but deployment
components can be directly created as well.
In the main block, we create a ``CloudProvider`` instance, tell it the
instance types that are available, and use it to create a deployment
component for running the database on. The ``[DC: database_c]``
annotation creates the fresh ``Database`` object on that deployment
component::
{
CloudProvider provider = new CloudProvider("Amazon");
provider!setInstanceDescriptions(map[Pair("Small", map[Pair(Cores, 1), Pair(Speed, 15)])]);
DeploymentComponent database_c = await provider!launchInstanceNamed("Small");
[DC: database_c] Database db = new Database();
Loadbalancer lb = new Loadbalancer(5, db, provider);
new Client(lb);
await duration(100, 100);
provider!shutdown();
}
The ``LoadBalancer`` uses the same ``CloudProvider`` instance to create
the web workers::
class Loadbalancer(Int nWorkers, Database db, CloudProvider provider) implements Loadbalancer {
List workers = Nil;
Unit run() {
Int i = 0;
while (i < nWorkers) {
DeploymentComponent dc = await provider!launchInstanceNamed("Small");
[DC: dc] Webworker w = new Webworker(db);
workers = Cons(w, workers);
i = i + 1;
}
}
// All other code unchanged ...
}
The ``Database`` and ``Webworker`` classes now consume resources when
processing requests. Each ``query`` call consumes a constant 3 ``Speed``
resources from the database's deployment component, each
``processRequest`` call to a web worker consumes 16 resources from the
web worker's deployment component::
class Database implements Database {
String query() {
[Cost: 3] return "Result";
}
}
class Webworker(Database db) implements Webworker {
String processRequest() {
[Cost: 16] String result = await db!query();
return result + " from " + toString(this);
}
}
To run the model on the Java backend, use the following commands:
.. code:: sh
absc --java basic-model-resources-v1.abs -o basic-model-resources-v1.jar
java -jar basic-model-resources-v1.jar -p 8080
Then, open a browser to the address http://localhost:8080. The output
will look similar to this (scroll down in the model API browser output
to see more deployment components):
.. figure:: images/resource-modeling/basic-model-resources-v1-diagram.png
:alt: Screenshot of resource consumption over time of the database
and 3 webworkers, round-robin scheduling
Screenshot of resource consumption over time of the database and 3
webworkers, round-robin scheduling
We can see that all deployment components are lightly loaded, and that
the load balancer distributes tasks in a round-robin strategy. Since the
deployment component has a Speed capacity of 15 but the cost of one
processRequest execution is 16, each request takes two time units.
Changing load balancing strategies
----------------------------------
The ABS model discussed in this section is here:
`basic-model-resources-v2.abs `__.
With a one-line change in the load balancer, we can model a different
load balancing strategy: we switch from round-robin to a LIFO
(stack-based) strategy::
String processRequest() {
await length(workers) > 0;
Webworker w = head(workers);
workers = tail(workers);
String result = await w!processRequest();
workers = Cons(w, workers);
return result;
}
To run the model on the Java backend, run the following commands:
.. code:: sh
absc --java basic-model-resources-v2.abs -o basic-model-resources-v2.jar
java -jar basic-model-resources-v2.jar -p 8080
Then, open a browser to the address http://localhost:8080. The output
will look similar to this (scroll down in the model API browser output
to see more deployment components):
.. figure:: images/resource-modeling/basic-model-resources-v2-diagram.png
:alt: Screenshot of resource consumption over time of the database
and 2 webworkers, with LIFO scheduling of the load balancer
Screenshot of resource consumption over time of the database and 2
webworkers, with LIFO scheduling of the load balancer
The output shows that one web worker is enough to carry the load of
the given client scenario: all web workers except the first one are
idle over the course of the simulation.
Changing the client load scenario
---------------------------------
The ABS model discussed in this section is here:
`basic-model-resources-v3.abs `__.
The previous examples showed a load scenario where most machines are
idle. We can add more clients to simulate a higher load, with the
following main block::
{
CloudProvider provider = new CloudProvider("Amazon");
provider!setInstanceDescriptions(map[Pair("Small", map[Pair(Cores, 1), Pair(Speed, 15)])]);
DeploymentComponent database_c = await provider!launchInstanceNamed("Small");
[DC: database_c] Database db = new Database();
Loadbalancer lb = new Loadbalancer(5, db, provider);
new Client(lb);
new Client(lb);
new Client(lb);
new Client(lb);
new Client(lb);
new Client(lb);
new Client(lb);
new Client(lb);
new Client(lb);
await duration(100, 100);
provider!shutdown();
}
To run the model on the Java backend, run the following commands:
.. code:: sh
absc --java basic-model-resources-v3.abs -o basic-model-resources-v3.jar
java -jar basic-model-resources-v3.jar -p 8080
Then, open a browser to the address http://localhost:8080. The output
will look similar to this (scroll down in the model API browser output
to see more deployment components):
.. figure:: images/resource-modeling/basic-model-resources-v3-diagram.png
:alt: Screenshot of resource consumption over time of the database
and 2 webworkers, with increased client load
Screenshot of resource consumption over time of the database and 2
webworkers, with increased client load
In this last example, the database server is running at approximately
50% load, and all 5 web workers are running near capacity.
Further possibilities for resource modeling
-------------------------------------------
Note that the scenarios presented in this chapter are somewhat static, but that
does not need to be the case:
- We can dynamically add deployment components and add and remove
worker objects; this can be used to model redeployment at runtime
- Cost annotations can be any ABS expression over local variables and
class fields that produces a number; this can be used to model
varying costs of message processing, either stochastic or as a
function of the message payload
- The resources available to a deployment component can be increased
and decreased; this can be used to model stochastic machine
performance decrease, or resource transfer between machines