DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Using Spring Cloud Gateway and Discovery Service for Seamless Request Routing
  • NGINX With Eureka Instead of Spring Cloud Gateway or Zuul
  • Monoliths, REST, and Spring Boot Sidecars: A Real Modernization Playbook
  • Spring Boot Gateway With Spring Cloud and WebFlux

Trending

  • My Favorite Interview Question
  • Endpoint Security Controls: Designing a Secure Endpoint Architecture, Part 2
  • Using Python Libraries in Java
  • Infrastructure as Code (IaC) Beyond the Basics
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. Spring Cloud: How to Implement Service Discovery (Part 1)

Spring Cloud: How to Implement Service Discovery (Part 1)

In this article, readers will learn how to configure and run some service discovery sample architectures by using the Spring Cloud components.

By 
Mario Casari user avatar
Mario Casari
·
Jun. 13, 23 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
7.0K Views

Join the DZone community and get the full member experience.

Join For Free

In previous articles, Spring Cloud: How to Deal with Microservice Configuration (Part 1) and Spring Cloud: How to Deal with Microservice Configuration (Part 2),  we have seen how to deal with microservices remote configuration. In this post, we are going to talk about another important feature in the context of microservices, namely Service Discovery. Service Discovery plays the role of a central registry in which all the services store their metadata information and from which they can get the metadata of other services.

The service discovery feature is implemented by a server and a corresponding client counterpart. In this article, we are going to describe how to configure a discovery server and make the client services able to reach it and use it. We will also see how to set a configuration based on the so-called "zone affinity."

Service Discovery: Basic Choices and Versions

Spring Cloud has been gradually dismissing the Netflix OSS solutions in favor of native implementations for the various micro-services features. Despite that, Eureka is still the natural choice for service discovery. 

In this article, we are going to use Eureka as a service discovery client and server with the following versions of Spring Boot and the corresponding Spring Cloud train of dependencies:

  • Spring Boot: 3.0.6
  • Spring Cloud: 2022.0.2 (2022.0.X versions are known as Kilburn and are compliant with the 3.0.X Spring Boot version)
  • Java 17

We will also use a Gateway component in order to implement two different architectural scenarios. We mentioned in the article A Brief Overview of the Spring Cloud Framework that the Netflix solution Zuul is a possible choice to implement the service discovery features. With the last versions of Spring Cloud, though, Zuul is not supported anymore and the Spring Cloud Gateway project took its place. SCG offers a non-blocking API model and is more performant. In this article, we will use SCG.

Dependencies and Configuration for the Server Side

To implement a service discovery server, we have to provide the necessary Maven dependencies in the first place. First of all, we provide the Spring Boot starter as a parent dependency:

XML
 
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.0.6</version>
</parent>


Then we set the Spring Cloud release train with the following starter in a dependencyManagement section:

XML
 
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>2022.0.2</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>


As a final piece of configuration, we set the Eureka starter in the dependencies section:

XML
 
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  </dependency>
</dependencies>


Single Instance Configuration

If we want to run just a single instance server, we can provide the following configuration in the application.yml file:

YAML
 
spring:
   application:
      name: discovery-service
            
eureka:
   instance:
      hostname: localhost
   client:
      registerWithEureka: false
      fetchRegistry: false
server:
   port: ${PORT:8760}


With the above Maven configuration, the client-side Eureka dependencies are imported as well. They are needed in case we want to provide high availability with more than one instance of the server.  In that case, each instance will register itself with all the other instances and fetch the configuration from the first instance available based on the configuration of default Eureka nodes, as we will see below when discussing HA.

In order to disable this synchronization mechanism in the above single instance configuration, we must set the following two properties:

  • registerWithEureka: By setting this property to false, we are disabling the feature that makes each instance register itself to the other instances. 
  • fetchRegistry: Setting this property to false on the client side avoids fetching the registered services. (The single instance of the discovery server does not need to fetch any services. Only in the high availability scenario would this make sense, since in that case, each instance would fetch the other nodes' metadata, choosing a default node from which to fetch, as set in the configuration).

Discovery Server's High Availability

If we want our service discovery feature to achieve high availability, we have to run more than one instance of the discovery server. The current Eureka version provides high availability using peer-to-peer replicating nodes. Every node is synchronized with all the others, that is to say, every node registers itself to each of the other nodes, fetches the metadata of the other nodes, and signals itself being up by sending  "heartbeats" to all of them.

Eureka High Availability Scenario
Eureka High Availability Scenario

Shared Configuration Properties

In order to configure the application for the kind of deployment depicted above, we make use of the Spring Boot profile feature. First of all, we set the shared properties in an application.yml file:

YAML
 
spring:
   application:
      name: discovery-service
            
eureka:
   instance:
      hostname: localhost
   client:
      registerWithEureka: false
      fetchRegistry: false
server:
   port: ${PORT:8760}


Self-Preservation and Cache Parameters

In the configuration above, in addition to the application name, we have set some Eureka server parameters:

  • enableSelfPreservation: By default, its value is true and that means that if many services are unreachable because of a network failure, they won't be removed from the registry. By setting it to false we are simplifying testing our sample architecture, which we are going to describe in the following sections.
  • evictionIntervalTimerInMs: Its default value is 60000 milliseconds. It configures the interval used by an internal task to check if the heartbeats are still received by the client services. We set it to just 1000 ms for our test purposes.
  • responseCacheUpdateIntervalMs: The server caches its API responses, so if we check the /eureka/apps endpoint, we obtain the result updated during the last interval. Its default value is 30000 milliseconds. We set it to just 1000 ms for our test purposes.

Configuration Properties Specific to the Single Instances

We can then set the properties specific to the single instances in separate files whose name is suffixed with the name of the profile, which is configured in the spring.config.activate.on-profiles property:

  • application-node1.yml
  • application-node2.yml
  • application-node3.yml

We show below the single configuration pieces corresponding to each file above, containing the name of the profile, the instance port, and the list of the other server nodes to contact.

YAML
 
spring:
   config:
      activate:
         on-profiles: node1
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8762/eureka/,http://localhost:8763/eureka/
server:  
  port: ${PORT:8761}


YAML
 
spring:
   config:
      activate:
         on-profiles: node2
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8763/eureka/
server:  
  port: ${PORT:8762}


YAML
 
spring:
   config:
      activate:
         on-profiles: node3
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
server:  
  port: ${PORT:8763}


Running the Instances

After having compiled the application using the "mvn clean install" command, we can start the instances using the above configuration by the following, specifying the profile as a command line argument:

Plain Text
 
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node1
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node2
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node3


Then we can access the Eureka dashboard by one of the nodes - the URL http://localhost:8761, for example. We can see here an example with just two running nodes:

Eureka dashboard
Eureka dashboard

As we can see, both nodes are shown in the dashboard, since the registry of each node, due to the peer-to-peer synchronization, contains all the running instances.

Eureka provides also a REST API, available through the .../eureka URL prefix. For instance, to obtain all the instances we can execute the following:

Plain Text
 
http://localhost:8761/eureka/apps


Service Discovery Configuration: Client Side

Suppose we have a simple Spring Boot application that exposes a single REST service. We make that service simply print the spring.config.activate.on-profiles property of the current instance:

Java
 
@RestController
public class ClientController {
			         
	@Value("${spring.config.activate.on-profiles}")
	private String zone;

	@GetMapping("/checkZone")
	public String ping() {
		return "This service runs in zone " + zone;
	}
}


As we did for the discovery server instances, we are going to use here the profile feature of Spring Boot to run several instances of the above application. We will use values such as zone1, zone2, and zone3 for the profiles. In this context, the term "zone" does not have a particular meaning, but we will talk later about "zone affinity" and for that scenario, it will make more sense. The REST service has the purpose to print on the screen which client node, identified by its profile, is responding to a particular request.

In order to make our Spring Boot application aware of the discovery server, we have to provide the Eureka client dependencies to it. It is only a matter of setting the appropriate Spring Boot starter as shown in the snippet below:

XML
 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>


Configuration for Three Instances

Supposing we want to run three instances: We set the shared properties in an application.yml file and the more specific ones in the following: application_zone1.yml, application_zone2.yml, and application_zone3.yml. As shared properties, we set the application name and other properties that we are going to describe below:

YAML
 
spring:
   application:
      name: client-service

eureka:
  instance:
    leaseRenewalIntervalInSeconds: 1 
    leaseExpirationDurationInSeconds: 1
  client:
    registryFetchIntervalSeconds: 1
    shouldDisableDelta: true


In the piece of configuration above we have set two additional eureka instance parameters:

  • leaseRenewalIntervalInSeconds: It configures the interval in seconds between the client's heartbeats sent to the discovery server. Its default value is 30 seconds.
  • leaseExpirationDurationInSeconds: It configures how many seconds the server has to wait for the next heartbeat before deleting the client instance from its registry. The default value is 90 seconds. We set it to just 1 second for testing purposes.
  • registryFetchIntervalSeconds: As the server side, also the client side has its registry cache. We set it to just 1 second for testing purposes. 
  • shouldDisableDelta: The client-side registry cache works by default updating itself with the delta of the previous registry state. By setting the shouldDisableDelta property to true, we are disabling such behavior.

The configuration for the single client instances is the following:

YAML
 
spring: 
   config:
      activate:
         on-profiles: zone1  
         
eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/
            
server: 
   port: ${PORT:8181}


YAML
 
spring: 
   config:
      activate:
         on-profiles: zone2  
         
eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://localhost:8762/eureka/,http://localhost:8761/eureka/,http://localhost:8763/eureka/
          
server: 
   port: ${PORT:8182}


YAML
 
spring: 
   config:
      activate:
         on-profiles: zone3  
         
eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://localhost:8763/eureka/,http://localhost:8762/eureka/,http://localhost:8761/eureka/
          
server: 
   port: ${PORT:8183}


The eureka.client.serviceUrl.defaultZone property contains the three server nodes. Each client instance will contact the first node available from the left to the right, in order to register itself and fetch the configuration.

Running The Three Instances

After having compiled the application, we can run the three client instances with the following:

Plain Text
 
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone1
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone2
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone3


Service Discovery Configuration: Basic Architecture

Using the configuration for the discovery server and client parts described above, we can implement the basic architecture shown in the following diagram:

Basic Service Discovery Configuration
Basic Service Discovery Configuration

We have the discovery server running as a cluster with three instances fully synchronized with each other. We also have three nodes for the client service, where each of them fetches the configuration from one of the instances, based on the value of the eureka.client.serviceUrl.defaultZone property.

In the above picture, we can also see an additional component: a gateway. The gateway part represents the entrance of our system from the standpoint of the external world. We introduce it here just in order to make our architecture similar to a real scenario: we use it to dispatch external requests into our system. As we said before, the latest versions of Spring Cloud favor and support a module named Spring Cloud Gateway, and this is what we will use in our example.

The gateway is configured to fetch the registry by default from the first Eureka node. If that node for some reason goes down, it will use the remaining nodes in the configured order. In the next section, you can see the settings for the gateway of the above architecture.

Spring Cloud Gateway Settings

Our gateway, like the other components, is a Spring Boot application. To characterize this application as a Spring Cloud Gateway, all we have to do is to set the spring-cloud-starter-gateway starter in the Maven dependencies section. We also need the client side, since the gateway instance has to fetch the services metadata information from the discovery server layer:

XML
 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
                         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>


Then we set the following properties in the application.yml file:

YAML
 
server:
   port: ${PORT:8080}

spring:  
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
   
eureka: 
   client: 
      serviceUrl:
         defaultZone: http://node1:8761/eureka/ ,http://node1:8762/eureka/,http://node1:8763/eureka/
      registerWithEureka: false 
      registryFetchIntervalSeconds: 1 
      shouldDisableDelta: true
   instance:
      leaseRenewalIntervalInSeconds: 1 
      leaseExpirationDurationInSeconds: 1


By setting the spring.cloud.gateway.discovery.locator.enabled to true, we are enabling the automatic discovery of services based on the configuration of service discovery instances. The other locator property lower-case-service-id forces the use of lowercase service ID. As for the other properties, they are the same as we already set for the client service, with the only difference of the registerWithEureka one, which is set to false, because we don't need the gateway to register itself to the service discovery server.

Running the Architecture

To run our system, we have to compile and build the JAR files with the Maven command "mvn clean install" and then execute all the instances for the client, server, and gateway on the command line, specifying the profile:  

Plain Text
 
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node1
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node2
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node3

java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone1
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone2
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone3

java -jar spring-cloud-discovery-gateway-localconfig-nozone-1.0-SNAPSHOT.jar


We can then test the system through the gateway executing the REST service by the following URL:

Plain Text
 
http://localhost:8080/client-service/checkZone


We expect to see the gateway to load balance the requests using a simple round-robin algorithm. So, by refreshing the URL multiple times we will see, consecutively:

Plain Text
 
This service runs in zone zone1
...
This service runs in zone zone2
...
This service runs in zone zone3
...
This service runs in zone zone1


Service Discovery Configuration: Zone Affinity

In more general scenarios, we could have separate groups of instances running on different machines. If we keep it simple, we can imagine a configuration in which separate triplets of server, client, and gateway instances run on separate machines.  As in the above example, we can imagine the discovery server instances to be part of the same cluster and fully synchronized with each other. Look at the following picture, for instance:

Zone Affinity Example
Zone Affinity Example

We can think of Zone1, Zone2 and Zone3 in the above diagram as separate machines, geographically distant from each other, and hosting each a single triplet of discovery server, client service, and gateway. We would wish our system to have the best performance, but if we recall our discussion on the previous sample architecture, we have seen that the requests are load balanced in a round-robin manner. So, if we send requests through the first gateway shown in the above diagram, they will be dispatched circularly to all three service nodes. This way, two-thirds of the requests will be dispatched to geographically distant services, not an ideal situation in terms of performance.

We can avoid the above behavior by introducing a new concept: Zone Affinity. In a zone affinity scenario, the gateway will dispatch requests preferably to the same "zone" in which it is running. Only if the service node in its own zone is not available, other zones would be considered, based on the configured list of URLs of the eureka.client.serviceUrl.defaultZone property.

In order to obtain this behavior, we must change the configuration a little. We must add the following Eureka configuration to the client, discovery server, and gateway part (for each instance):

YAML
 
eureka:
  instance:
    metadataMap:
       zone: zone1


And we also have to add the following to the gateway configuration:

YAML
 
spring:  
  cloud:
    loadbalancer:
       configurations: zone-preference


Note: The profile feature doesn't seem to work as expected for the Spring Cloud Gateway component. So, just for the gateway, we should pass the instance-specific properties on the command line instead, as we can see below.

Running the Architecture

With these modifications in place, we can compile and run the above architecture with the following:

Plain Text
 
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node1
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node2
java -jar spring-cloud-discovery-server-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=node3

java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone1
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone2
java -jar spring-cloud-discovery-client-localconfig-nozone-1.0-SNAPSHOT.jar --spring.profiles.active=zone3


java -jar spring-cloud-discovery-gateway-localconfig-1.0-SNAPSHOT.jar --server.port=8081  --eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/ --eureka.instance.metadataMap.zone=zone1

java -jar spring-cloud-discovery-gateway-localconfig-1.0-SNAPSHOT.jar --server.port=8082  --eureka.client.serviceUrl.defaultZone=http://localhost:8762/eureka/ --eureka.instance.metadataMap.zone=zone2

java -jar spring-cloud-discovery-gateway-localconfig-1.0-SNAPSHOT.jar --server.port=8083  --eureka.client.serviceUrl.defaultZone=http://localhost:8763/eureka/ --eureka.instance.metadataMap.zone=zone3


If we try to send requests through the three gateway instances, we will see that a request through the first gateway will always print "This service runs in zone zone1." The second will always be "This service runs in zone zone2," and "This service runs in zone zone3" for the third. If we turn off one of the services, the request will be sent to the next service instance available, based on the eureka.client.serviceUrl.defaultZone setting.

Conclusion

We have seen in this post how to use the service discovery features of Netflix Eureka, through Spring Cloud integration, in order to implement some basic scenarios. In the second part of this article, we will show how to secure the discovery server and the client services. We will also see two different ways of combining Spring Cloud Discovery with Spring Cloud Remote Configuration.

You can find the source code of the examples in this article in the following GitHub module:

  • Spring Cloud Discovery Examples Source Code

Note: The URL above actually is a submodule of a root repository that contains a number of modules for other articles, and in turn, is the root of other submodules with the specific examples of this article. From the standpoint of dependencies, it does not inherit from the general repository though, and you can just take it and compile it as it is.

Service discovery Spring Cloud Dependency Spring Boot

Published at DZone with permission of Mario Casari. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Using Spring Cloud Gateway and Discovery Service for Seamless Request Routing
  • NGINX With Eureka Instead of Spring Cloud Gateway or Zuul
  • Monoliths, REST, and Spring Boot Sidecars: A Real Modernization Playbook
  • Spring Boot Gateway With Spring Cloud and WebFlux

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!