Calling one microservice from another with client-side load balancing

Remember how we configured our Eureka Server earlier to run on a separate port? Every microservice has to run on a distinct port. If we assume the images service is our frontend (it has the Thymeleaf template, and is closest to consumers for serving up image data), then we can let it continue to run on Netty's default port of 8080.

That leaves one decision: what port to run the comments service on? Let's add this to the comments service's application.yml:

    server: 
      port: 9000 

This setting instructs Spring Boot to run comments on port 9000. With that in place, let's go back to images, and make some adjustments.

For starters (Spring Boot starters), we need to add some extra things to the images build.gradle file:

    compile('org.springframework.cloud:spring-cloud-starter-eureka') 
    compile('org.springframework.cloud:spring-cloud-starter-hystrix') 

These changes include the following:

  • spring-cloud-starter-eureka is the dependency needed to register our microservice as a Eureka client. It brings in several transitive dependencies, the most important one for this section being Ribbon.
  • spring-cloud-starter-hystrix is the dependency for the circuit-breaker pattern, which we will dig into later in this chapter.

The Spring Framework has had, for a long time, the powerful RestTemplate utility. To make a remote call, we just do something like this:

    List<Comment> comments = restTemplate.exchange( 
      "http://localhost:9000/comments/{imageId}", 
      HttpMethod.GET, 
      null, 
      new ParameterizedTypeReference<List<Comment>>() {}, 
      image.getId()).getBody(); 

There's a lot going on here, so let's take it apart:

  • restTemplate.exchange() is the generic method for making remote calls. There are shortcuts such as getForObject() and getForEntity(), but when dealing with generics (such as List<Comment>), we need to switch to exchange().
  • The first argument is the URL to the comments service that we just picked. It has the port number we selected along with the route (/comments/{imageId}, a template) where we can serve up a list of comments based on the image's ID.
  • The second argument is the HTTP verb we wish to use--GET.
  • The third argument is for headers and any body. Since this is a GET, there are none.
  • The fourth argument is the return type of the data. Due to limitations of Java's generics and type erasure, we have created a dedicated anonymous class to capture the type details for List<Comment>, which Spring can use to interact with Jackson to properly deserialize.
  • The final argument is the parameter (image.getId()) that will be used to expand our URI template's {imageId} field.
  • Since exchange() returns a Spring ResponseEntity<T>, we need to invoke the body() method to extract the response body.

There is a big limitation in this code when dealing with microservices--the URL of our target service can change.

Getting locked into a fixed location is never good. What if the comments service changes ports? What if we need to scale up multiple copies in the future?

Frankly, that's unacceptable.

The solution? We should tie in with Netflix's Ribbon service, a software load balancer that also integrates with Eureka. To do so, we only need some small additions to our images service.

First, we should create a RestTemplate object. To do so, let's add a Config class as follows:

    @Configuration 
    public class Config { 
 
      @Bean 
      @LoadBalanced 
      RestTemplate restTemplate() { 
        return new RestTemplate(); 
      } 
    } 

We can describe the preceding code as follows:

  • @Configuration marks this as a configuration class containing bean definitions. Since it's located underneath LearningSpringBootImagesApplication, it will be automatically picked up by component scanning.
  • @Bean marks the restTemplate() method as a bean definition.
  • The restTemplate() method returns a plain old Spring RestTemplate instance.
  • @LoadBalanced instructs Netflix Ribbon to wrap this RestTemplate bean with load balancing advice.

We can next inject our RestTemplate bean into the HomeController like this:

    private final RestTemplate restTemplate; 
 
    public HomeController(ImageService imageService, 
     RestTemplate restTemplate) { 
       this.imageService = imageService; 
       this.restTemplate = restTemplate; 
    } 

This uses constructor injection to set the controller's final copy of restTemplate.

With a load-balanced, Eureka-aware restTemplate, we can now update our index() method to populate the comments model attribute like this:

    restTemplate.exchange( 
      "http://COMMENTS/comments/{imageId}", 
      HttpMethod.GET, 
      null, 
      new ParameterizedTypeReference<List<Comment>>() {}, 
      image.getId()).getBody()); 

This code is almost identical to what we typed out earlier except for one difference--​the URL has been revamped into http://COMMENTS/comments/{imageId}. COMMENTS is the logical name that our comments microservice registered itself with in Eureka.

The logical name for a microservice used by Eureka and Ribbon is set using spring.application.name inside its src/main/resources/application.yml file:

  • comments: spring.application.name: comments
  • images: spring.application.name: images
The logical name is case insensitive, so you can use either http://COMMENTS/comments/{imageId} or http://comments/comments/{imageId}. Uppercase helps make it clear that this is a logical hostname, not a physical one.

With this in place, it doesn't matter where we deploy our system nor how many instances are running. Eureka will dynamically update things, and also support multiple copies registered under the same name. Ribbon will handle routing between all instances.

That's nice except that we still need to move the CommentReadRepository we built in the previous chapter to the comments microservice!

In the previous chapter, we differentiated between reading comments with a CommentReadRepository and writing comments with a CommentWriteRepository. Since we are concentrating all MongoDB operations in one microservice, it makes sense to merge both of these into one CommentRepository like this:

    public interface CommentRepository 
     extends Repository<Comment, String> { 
 
       Flux<Comment> findByImageId(String imageId); 
 
       Flux<Comment> saveAll(Flux<Comment> newComment); 
 
       // Required to support save() 
       Mono<Comment> findById(String id); 
 
       Mono<Void> deleteAll(); 
    } 

Our newly built repository can be described as follows:

  • We've renamed it as CommentRepository
  • It still extends Repository<Comment, String>, indicating it only has the methods we need
  • The findByImageId(), save(), findOne(), and deleteAll() methods are all simply copied into this one interface
It's generally recommended to avoid sharing databases between microservices, or at least avoid sharing the same tables. The temptation to couple in the database is strong, and can even lead to integrating through the database. Hence, the reason to move ALL MongoDB comment operations to one place nicely isolates things.

Using this repository, we need to build a REST controller to serve up lists of comments from /comments/{imageId}:

    @RestController 
    public class CommentController { 
 
      private final CommentRepository repository; 
 
      public CommentController(CommentRepository repository) { 
        this.repository = repository; 
      } 
 
      @GetMapping("/comments/{imageId}") 
      public Flux<Comment> comments(@PathVariable String imageId) { 
        return repository.findByImageId(imageId); 
      } 
    } 

This previous tiny controller can be easily described as follows:

  • @RestController indicates this is a Spring WebFlux controller where all results are written directly into the HTTP response body
  • CommentRepository is injected into a field using constructor injection
  • @GetMapping() configures this method to respond to GET /comments/{imageId} requests.
  • @PathVariable String imageId gives us access to the {imageId} piece of the route
  • The method returns a Flux of comments by invoking our repository's findByImage() using the imageId

Having coded things all the way from populating the UI with comments in our images service, going through Ribbon and Eureka, to our comments service, we are fetching comments from the system responsible for managing them.

RestTemplate doesn't speak Reactive Streams. It's a bit too old for that. But there is a new remote calling library in Spring Framework 5 called WebClient. Why aren't we using it? Because it doesn't (yet) support Eureka logical hostname resolution. Hence, the part of our application making RestTemplate calls is blocking. In the future, when that becomes available, I highly recommend migrating to it, based on its fluent API and support for Reactor types.

In addition to linking two microservices together with remote calls, we have decoupled comment management from image management, allowing us to scale things for efficiency and without the two systems being bound together too tightly.

With all these changes in place, let's test things out. First of all, we must ensure our Eureka Server is running:

          .   ____          _            __ _ _
    /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
    \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
    =========|_|==============|___/=/_/_/_/
    :: Spring Boot ::             (v2.0.0.M5)
    
    2017-08-12 09:48:47.966: Setting initial instance status as: 
STARTING
2017-08-12 09:48:47.993: Initializing Eureka in region us-east-1 2017-08-12 09:48:47.993: Client configured to neither register nor
que...
2017-08-12 09:48:47.998: Discovery Client initialized at timestamp
150...
2017-08-12 09:48:48.042: Initializing ... 2017-08-12 09:48:48.044: The replica size seems to be empty.
Check the...
2017-08-12 09:48:48.051: Finished initializing remote region
registrie...
2017-08-12 09:48:48.051: Initialized 2017-08-12 09:48:48.261: Registering application unknown with
eureka w...
2017-08-12 09:48:48.294: Setting the eureka configuration.. 2017-08-12 09:48:48.294: Eureka data center value eureka.datacenter
is...
2017-08-12 09:48:48.294: Eureka environment value
eureka.environment i...
2017-08-12 09:48:48.302: isAws returned false 2017-08-12 09:48:48.303: Initialized server context 2017-08-12 09:48:48.303: Got 1 instances from neighboring DS node 2017-08-12 09:48:48.303: Renew threshold is: 1 2017-08-12 09:48:48.303: Changing status to UP 2017-08-12 09:48:48.307: Started Eureka Server 2017-08-12 09:48:48.343: Tomcat started on port(s): 8761 (http) 2017-08-12 09:48:48.343: Updating port to 8761 2017-08-12 09:48:48.347: Started
LearningSpringBootEurekaServerApplica...

In this preceding subset of console output, bits of Eureka can be seen as it starts up on port 8761 and switches to a state of UP. It may seem quirky to see messages about Amazon Web Services (AWS), but that's not surprising given Eureka's creators (Netflix) run all their systems there. However, isAws returned false clearly shows the system knows it is NOT running on AWS.

If you look closely, you can spot that the Eureka Server is running on Apache Tomcat. So far, we've run everything on Netty, right? Since Eureka is a separate process not involved in direct operations, it's okay for it not to be a Reactive Streams-based application.

Next, we can fire up the images service:

      .   ____          _            __ _ _
    /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
    \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
    =========|_|==============|___/=/_/_/_/
    :: Spring Boot ::  (v2.0.0.M5)
    
    ...
    
    2017-10-20 22:29:34.319: Registering application images with eureka
wi...
2017-10-20 22:29:34.320: Saw local status change event
StatusChangeEve...
2017-10-20 22:29:34.321: DiscoveryClient_IMAGES/retina:images:
registe...
2017-10-20 22:29:34.515: DiscoveryClient_IMAGES/retina:images -
regist...
2017-10-20 22:29:34.522: Netty started on port(s): 8080 (http) 2017-10-20 22:29:34.523: Updating port to 8080 2017-10-20 22:29:34.906: Opened connection
[connectionId{localValue:2,...
2017-10-20 22:29:34.977: Started
LearningSpringBootImagesApplication i...

This preceding subsection of console output shows it registering itself with the Eureka service through DiscoveryClient under the name IMAGES.

At the same time, the following tidbit is logged on the Eureka Server:

Registered instance IMAGES/retina:images with status UP 
(replication=false)

We can easily see that the images service has registered itself with the name IMAGES, and it's running on retina (my machine name).

Finally, let's launch the comments microservice:

      .   ____          _            __ _ _
    /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
    \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
    =========|_|==============|___/=/_/_/_/
    :: Spring Boot ::  (v2.0.0.M5)
    
    ...
    
    2016-10-20 22:37:31.477: Registering application comments with
eureka ...
2016-10-20 22:37:31.478: Saw local status change event
StatusChangeEve...
2016-10-20 22:37:31.480:
DiscoveryClient_COMMENTS/retina:comments:9000...
2016-10-20 22:37:31.523:
DiscoveryClient_COMMENTS/retina:comments:9000...
2016-10-20 22:37:32.154: Netty started on port(s): 9000 (http) 2016-10-20 22:37:32.155: Updating port to 9000 2016-10-20 22:37:32.188: Opened connection
[connectionId{localValue:2,...
2016-10-20 22:37:32.209: Started
LearningSpringBootCommentsApplication...

In this last output, our comment handling microservice has registered itself with Eureka under the logical name COMMENTS.

And again, in the Eureka Server logs, we can see a corresponding event:

Registered instance COMMENTS/retina:comments:9000 with status UP 
(replication=false)

The COMMENTS service can be found at retina:9000 (author alert--that's my laptop's hostname, yours will be different), which matches the port we configured that service to run on.

To see all this from a visual perspective, let's navigate to http://localhost:8761, and see Eureka's webpage:

This preceding web page is not provided by Netflix Eureka, but is crafted by the Spring Cloud Netflix project (hence Spring Eureka at the top) instead. It has some basic details about the environment including uptime, refresh policies, and others.

Further down on the page is some more interesting information:

DS (Discovery Service) Replica details are listed on the web page. Specifically, we can see the logical applications on the left (COMMENTS and IMAGES), their status on the right (both UP), and hyperlinks to every instance (retina:comments:9000 and retina:images).

If we actually click on the retina:comments:9000 hyperlink, it takes us to the Spring Boot info endpoint:

In this case, there is no custom info provided. But it also proves that the service is up and operational.

We may have verified everything is up, but let's prove that our new and improved microservice solution is in operation by visiting http://localhost:8080.

If we load up a couple of new images and submit some comments, things can now look like this:


What's happening under the hood? If we look at the images microservice's console, we can see a little action:

    2016-10-20 22:53:07.260  Flipping property: 
COMMENTS.ribbon.ActiveConn...
2016-10-20 22:53:07.286 Shutdown hook installed for:
NFLoadBalancer-P...
2016-10-20 22:53:07.305 Client:COMMENTS instantiated a
LoadBalancer:D...
2016-10-20 22:53:07.308 Using serverListUpdater
PollingServerListUpda...
2016-10-20 22:53:07.325 Flipping property:
COMMENTS.ribbon.ActiveConn...
2016-10-20 22:53:07.326 DynamicServerListLoadBalancer for client
COMM...
DynamicServerListLoadBalancer: { NFLoadBalancer:name=COMMENTS, current list of Servers=[retina:9000], }ServerList:org.springframework.cloud.netflix
.ribbon.eureka.DomainExt...
2016-10-20 22:53:08.313 Flipping property:
COMMENTS.ribbon.ActiveConn...
2016-10-20 22:54:33.870 Resolving eureka endpoints via
configuration

There's a lot of detail in the preceding output, but we can see Netflix Ribbon at work handling software load balancing. We can also see DynamicServerListLoadBalancer with a current list of servers containing [retina:9000].

So, what would happen if we launched a second copy of the comments service using SERVER_PORT=9001 to ensure it didn't clash with the current one?

In the console output, we can spot the new instance registering itself with Eureka:

DiscoveryClient_COMMENTS/retina:comments:9001 - registration
status: 204

If we go back and visit the Spring Eureka web page again at http://localhost:8761, we can see this updated listing of replicas:

If we start posting comments on the site, they will rotate, going between each comments microservice.

Normally, when using RabbitMQ, each instance of comments will register its own queue, and hence, receive its own copy of newly posted comments. This would result in double posting in this scenario. However, Spring Cloud Stream has a solution--consumer groups. By having spring.cloud.stream.bindings.input.group=comments in comments microservice's application.yml, we declare that only one such queue should receive each individual message. This ensures that only one of the microservices actually processes a given event. See http://docs.spring.io/spring-cloud-stream/docs/Elmhurst.M1/reference/htmlsingle/index.html#consumer-groups for more details.

With microservice-to-microservice remote calls tackled (and supported for scaling up), it's time to pursue another problem often seen in microservice-based solutions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.240.196