본문 바로가기
IBM - old/WAS Liberty 기술자료

[Liberty]Microinvader: The importance of common interfaces in microservices

by freeman98 2017. 3. 6.

https://developer.ibm.com/wasdev/docs/microinvader-importance-common-interfaces-microservices/


Microinvader: The importance of common interfaces in microservices

Common interfaces are essential when building microservices. If each microservice in a larger application has the same interface implemented, it makes maintenance of the microservices easier because of the similarities in the source code. Microinvader is a graphical game that demonstrates some of the concepts of microservices, including the benefits of common interfaces.


Think about it: how many distinct cars can you drive? I can guess that if you can drive one car, you can probably drive all other cars in a similar way. Thankfully, car builders have created the same ‘interface’ for all cars. There is a steering wheel for changing direction, an accelerator and a brake, all where you expect to find them. Even though the internal implementation of cars can vary (e.g. diesel, gas, or electric), the interface for the end-user (driver) is much the same.

What is Microinvader?

Microinvader is a trivial implementation of the old 8-bit Space Invaders arcade game based on microservices. There are more complete games, like Game On!, which also demonstrate microservices architecture but Microinvader is a graphical game and helps to present some microservices concepts visually. Its implementation is simple enough that you can replace or understand any one of its pieces. The code will be improved over time to demonstrate other aspects of microservices.


When running, Microinvader looks like this:


microinvaders-screen

Microservices architecture

Essentially we have three main types of objects in the game: Player, Enemy, and Bomb. Each object is a microservice running on an instance of Liberty or MicroProfile. The microservices do not depend on each other so you can stop one of the microservices and the game still works (even if it isn’t so useful any more).


microinvader-diagram

For each object, there is a common set of REST calls:

  • Position returns an array of objects on a 20×20 grid. There is only one object inside the collection that is returned from the Player microservice (there is only one player in each game) but there can be multiple objects inside the collections returned from the Enemy and Bomb microservices. Regardless of how many objects are returned in a collection, the same interface is used so that the Space microservice can make the same call to each of the other microservices.
  • Move sends a key-press to all the objects. Today, only the Player handles key-presses for movement but all of the microservices receive notifications that a key has been pressed (in the future other services could handle them too).
  • Destroy kills the object (no movement, no presentation).
  • Run makes the object move. For the Player object there is no movement (only key presses move the player). For Enemy objects, they move from left to right and down until they reach the player. For Bombs, the movement is from bottom to top (the player’s bomb) or from top to bottom (an enemy’s bomb). Each call to run (a step) moves one position for all the objects.
  • isFinished indicates that this object was destroyed and perhaps the game is over, depending on which object has been destroyed.

All of these REST calls are implemented in each of the microservices. There is a front-end service that coordinates the calls for each of them. For example, the run call is implemented in the front-end service:


1
2
3
4
5
6
@GET
@Path("/run")
public void run() throws Exception {
    for (int i = 0; i < urls.length; i++)
        callRest(urls[i] + "run");
}


This method is responsible for the movement of all of the pieces in the game (you can find it in com.ibm.space.space.Space.java:40). The front-end controller is unaware of how each microservice implements the call and what movements each different type of object makes. For example, in the Enemy implementation of the run call, for each call, each Enemy object moves one position right (x) until it reaches the end of the grid. The Enemy object then moves one line down (y) and returns to the left of the screen:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GET
@Path("/run")
public void run() throws Exception {
    if (getValues() != null)
    {
        if (getValues().isFinished()) return;
        int x0 = getValues().getTies()[0].getX();
        int y0 = getValues().getTies()[0].getY();
         
        if (x0 <= 9) incrementAllX();
        else {
            returnAllXtoBegin();
            if (y0 <= 15)  incrementAllY();
            else { // reach the end of game
                destroyAllEnemies();
            }
        }
         
    }
}

BFF (Backend For Frontend)

The Space microservice is the Backend For Frontend (BFF). It creates a unique access point for the user interface (GUI). In this first version, the GUI and the Space microservice are inside the same WAR file but they could be separated. The user interface is a modern rich client, using AngularJS and HTML.


The benefit of using a BFF is that it makes access from GUI client easier and minimizes any impact when the Player, Bomb, and Enemy microservices are changed because the HTML and AngularJS communicate only with the Space microservice. Protection is another benefit: the HTML and AngularJS do not handle the orchestration between the microservices so it is not possible to ‘hack’ the GUI to discover the server behavior.

Eventual consistency

The objects can freely move on the grid but sometimes both can occupy the same position in the grid. For example, a Bomb object and an Enemy object can both be at position (12,12). In this game, such a situation is called a collision. When the same position is occupied by two different objects, both should be destroyed. The problem is that Enemy object does not know the position of the Bomb object–and vice versa!


In business solutions, two independent requests that should be handled as a single request are managed by transactions. The main purpose of a distributed transaction is to ensure that the two requests are handled together or not at all but it is effective only in the failure case (and, most of the time, the operation is successful). If no error happens, the time spent to ensuring the integrity of the request is wasted. The approach taken in microservices architectures to ensure that two requests are consistent is called eventual consistency. In eventual consistency, the potential errors are not ignored but are handled in a separate thread, outside of the main flow so that it does not waste time in successful operations.


Another benefit of eventual consistency is to create independence between modules. Going back to our sample application, if we handle the Enemy and Bomb objects completely separately, the independence of these modules make it easier to manage in a cloud environment, for example. The main idea of microservices is: leave it to run, consist eventually (BASE instead ACID). In the sample application, there is a microservice called Collision, which ensures that all of the movements by objects make sense in the game and removes objects in the case of collisions. After each movement on the grid, we take all objects and check whether any have collided. If there are, the objects are marked destroyed. This works like a garbage collector for objects that have collided.


Here is our implementation of eventual consistency (com.ibm.space.collision.Collision.java:41) in the Space object:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    for (int i = 0; i < array.size(); i++) {
    for (int j = i+1; j < array.size(); j++) {
    JsonObject obj1 = (JsonObject)array.get(i);
    JsonObject obj2 = (JsonObject)array.get(j);
    if (obj1.getInt("x") == obj2.getInt("x") &&
        obj1.getInt("y") == obj2.getInt("y") &&
        obj1.getInt("id") != obj2.getInt("id") &&
        !obj1.getBoolean("destroyed") &&
        !obj2.getBoolean("destroyed"))
    {
        String resturl1 = findRest(obj1.getString("type"));
        if (resturl1 != null)
            callRest(resturl1 + "destroy/" + obj1.getInt("id"));
                        String resturl2 = findRest(obj2.getString("type"));
        if (resturl2 != null)
            callRest(resturl2 + "destroy/" + obj2.getInt("id"));
                 
    }
    }
}  


The code simply checks whether the position of two objects is the same (x,y) and takes an action. If they are in the same position, destroy both. The object’s type doesn’t matter (Bomb, Player, Enemy): if they are in the same position, they will be destroyed. Having a single interface helps us to ignore the object implementation.

User interface

Finally, the user interface is very simple, a single table with a 20×20 grid controlled by AngularJS. The preference is to use a rich client (instead of JSF, JSP, Struts, and so on). All coordination for the program running is made from the client side, for which AngularJS was the choice.

Why common interfaces?

So, what is the benefit of handling all of the objects with a single interface? First, all implementations are similar (take a look at the source code for Microinvaders), which makes changes easier. Secondly, it’s easier to make future improvements. For example, let’s suppose that you want to add barriers at the bottom of the screen to protect the player, like in the real Space Invaders game. To do that, you could just create a new microservice called Barrier and let Barrier register itself. The game software would be able to handle the new Barrier object because it uses the same verbs as the other microservices.

Running Microinvader

All of the source code for Microinvader is on GitHub so that you can download it and run it locally.

To run Microinvader with Maven from the command line:

  1. Clone the git repository: https://github.com/WASdev/sample.microinvaders.git
  2. Change directory to sample.microinvaders.
  3. Run a Maven build to build and test the project:
    mvn install
  4. Run the Maven build to start the Liberty server and run the app:
    mvn liberty:start-server
  5. Open the application in a browser: http://localhost:9081/space-1.0/. Follow the instructions to play the game.
  6. To stop the server when you’re finished, run the command:
    mvn liberty:stop-server

To run Microinvader from Eclipse:

  1. In Eclipse, switch to the Git perspective.
  2. Click Clone a Git repository from the Git Repositories view.
  3. Enter the URL https://github.com/WASdev/sample.microinvaders.git
  4. Click Next, then click Next again, accepting the defaults.
  5. Select Import all existing Eclipse projects after clone finishes, then click Finish.
  6. Switch to the Java EE perspective. The microinvaders project is displayed in the Explorer view.
  7. Right-click the pom.xml in the microinvaders project, then click Run As… > Maven Build…, then in the Goals field type install liberty:start-server and click Run. This builds and tests the application then starts the server running in the background.
  8. Open the application in a browser: http://localhost:9081/space-1.0/. Follow the instructions to play the game.
  9. To stop the server when you’re finished, run the Maven build again with the liberty:stop-server goal.

A nice test is to stop a microservice (Bomb, Enemy, or Player) while the game is running. You can see that game continues to run, despite the missing service, but the associated objects do not appear on the screen. If you start the microservice again, the game will keep running.



댓글