Introduction
Service discovery is one of the most critical parts when an application is deployed as microservices in the cloud. This is because for any use operation, an application in a microservice architecture may require access to multiple services and the communication amongst them.
Service discovery helps tracking the service address and the ports where the service instances can be contacted to. There are three components at play here −
- Service Instances − Responsible to handle incoming request for the service and respond to those requests.
- Service Registry − Keeps track of the addresses of the microservice instances. The microservice instances are supposed to register their address with the service registry.
- Service Client − The client which wants access or wants to place a request and get response from the service instances. The service client contacts the service registry to get the address of the microservice instances.
Netflix Eureka Naming Server, Apache Zookeeper, Consul are a few well-known components which are used for Service Discovery. In this tutorial, we will use Eureka
Setting up Eureka Server/Registry
For setting up Eureka Server, we need to update the POM file to contain the following dependency −
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
And then, annotate our Spring application class with the correct annotation, i.e.,@EnableEurekaServer.
package com.javahubpoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class RestaurantServiceRegistry{
public static void main(String[] args) {
SpringApplication.run(RestaurantServiceRegistry.class, args);
}
}
We also need a properties file if we want to configure the registry and change its default values. Here are the changes we will make −
- Update the port to 8900 rather than the default 8080
- In production, one would have more than one node for registry for its high availability. That is where we need peer-to-peer communication between registries. As we are executing this in standalone mode, we can simply set client properties to false to avoid any errors.
So, this is how our application.yml file will look like −
port: 8900
eureka:
client:
register-with-eureka: false
fetch-registry: false
And that is it, let us now compile the project and run the program by using the following command −
java -jar .\target\spring-cloud-eureka-server-1.0.jar
Now we can see the logs in the console −
2021-03-07 13:33:10.156 INFO 17660 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8900
(http)
2021-03-07 13:33:10.172 INFO 17660 --- [ main]
o.apache.catalina.core.StandardService : Starting service [Tomcat]
...
2021-03-07 13:33:16.483 INFO 17660 --- [ main]
DiscoveryClientOptionalArgsConfiguration : Eureka HTTP Client uses Jersey
...
2021-03-07 13:33:16.632 INFO 17660 --- [ main]
o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as:
STARTING
2021-03-07 13:33:16.675 INFO 17660 --- [ main]
com.netflix.discovery.DiscoveryClient : Initializing Eureka in region useast-
1
2021-03-07 13:33:16.675 INFO 17660 --- [ main]
com.netflix.discovery.DiscoveryClient : Client configured to neither register
nor query for data.
2021-03-07 13:33:16.686 INFO 17660 --- [ main]
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at
timestamp 1615104196685 with initial instances count: 0
...
2021-03-07 13:33:16.873 INFO 17660 --- [ Thread-10]
e.s.EurekaServerInitializerConfiguration : Started Eureka Server
2021-03-07 13:33:18.609 INFO 17660 --- [ main]
c.t.RestaurantServiceRegistry : Started RestaurantServiceRegistry in
15.219 seconds (JVM running for 16.068)
As we see from the above logs that the Eureka registry has been setup. We also get a dashboard for Eureka (see the following image) which is hosted on the server URL.
Eureka server dashboard URL :- http://localhost:8900/eureka
Setting up Eureka Client for Instance
Now, we will develop the microservice, which would register to the Eureka server. To register to Eureka server, we will use Netflix Eureka Client in our project.
Now let's create a separate Maven project and update the POM file to contain the following dependency −
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
And then, annotate our Spring application class with the correct annotation, i.e., @EnableDiscoveryClient
package com.javahubpoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class RestaurantCustomerService{
public static void main(String[] args) {
SpringApplication.run(RestaurantCustomerService.class, args);
}
}
We also need a properties file if we want to configure the client and change its default values (like port, URL etc.). Here are the changes we will make −
- We will provide the port at runtime while jar at execution.
- We will specify the URL at which Eureka server is running.
So, this is how our application.yml file will look like
application:
name: customer-service
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar
And execute the following on the other shell −
java -Dapp_port=8082 -jar .\target\spring-cloud-eureka-client-1.0.jar
Now we can see the logs in the console −
...
2021-03-07 15:22:22.474 INFO 16920 --- [ main]
com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew
interval is: 30
2021-03-07 15:22:22.482 INFO 16920 --- [ main]
c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand
update allowed rate per min is 4
2021-03-07 15:22:22.490 INFO 16920 --- [ main]
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at
timestamp 1615110742488 with initial instances count: 0
2021-03-07 15:22:22.492 INFO 16920 --- [ main]
o.s.c.n.e.s.EurekaServiceRegistry : Registering application CUSTOMERSERVICE
with eureka with status UP
2021-03-07 15:22:22.494 INFO 16920 --- [ main]
com.netflix.discovery.DiscoveryClient : Saw local status change event
StatusChangeEvent [timestamp=1615110742494, current=UP, previous=STARTING]
2021-03-07 15:22:22.500 INFO 16920 --- [nfoReplicator-0]
com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERSERVICE/
localhost:customer-service:8081: registering service...
2021-03-07 15:22:22.588 INFO 16920 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081
(http) with context path ''
2021-03-07 15:22:22.591 INFO 16920 --- [ main]
.s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8081
2021-03-07 15:22:22.705 INFO 16920 --- [nfoReplicator-0]
com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERSERVICE/
localhost:customer-service:8081 - registration status: 204
...
As we see from above logs that the client instance has been setup. We can also look at the Eureka Server dashboard we saw earlier. As we see, there are two instances of “CUSTOMER-SERVICE” running that the Eureka server is aware of −
Eureka server dashboard URL :- http://localhost:8900/eureka
Eureka Client Consumer Example
Our Eureka server has got the registered client instances of the “Customer-Service” setup. We can now setup the Consumer which can ask the Eureka Server the address of the “Customer-Service” nodes.
For this purpose, let us add a controller which can get the information from the Eureka Registry. This controller will be added to our earlier Eureka Client itself, i.e., “Customer Service”. Let us create the following controller to the client.
package com.javahubpoint;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RestaurantCustomerInstancesController {
@Autowired
private DiscoveryClient eurekaConsumer;
@RequestMapping("/customer_service_instances")
Note - the annotation @DiscoveryClient which is what Spring framework provides to talk to the registry.
Let us now recompile our Eureka clients. For execution, we will have two service instances running. To do that, let's open up two shells and then execute the following command on one shell −
java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar
And execute the following on the other shell −
java -Dapp_port=8082 -jar .\target\spring-cloud-eureka-client-1.0.jar
Once the client on both shells have started, let us now hit the http://localhost:8081/customer_service_instances we created in the controller. This URL displays complete information about both the instances.
[
{
"scheme": "http",
"host": "localhost",
"port": 8081,
"metadata": {
"management.port": "8081"
},
"secure": false,
"instanceInfo": {
"instanceId": "localhost:customer-service:8081",
"app": "CUSTOMER-SERVICE",
"appGroupName": null,
"ipAddr": "10.0.75.1",
"sid": "na",
"homePageUrl": "http://localhost:8081/",
"statusPageUrl": "http://localhost:8081/actuator/info",
"healthCheckUrl": "http://localhost:8081/actuator/health",
"secureHealthCheckUrl": null,
"vipAddress": "customer-service",
"secureVipAddress": "customer-service",
"countryId": 1,
"dataCenterInfo": {
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name": "MyOwn"
},
"hostName": "localhost",
"status": "UP",
"overriddenStatus": "UNKNOWN",
"leaseInfo": {
"renewalIntervalInSecs": 30,
"durationInSecs": 90,
"registrationTimestamp": 1616667914313,
"lastRenewalTimestamp": 1616667914313,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1616667914313
},
"isCoordinatingDiscoveryServer": false,
"metadata": {
"management.port": "8081"
},
"lastUpdatedTimestamp": 1616667914313,
"lastDirtyTimestamp": 1616667914162,
"actionType": "ADDED",
"asgName": null
},
"instanceId": "localhost:customer-service:8081",
"serviceId": "CUSTOMER-SERVICE",
"uri": "http://localhost:8081"
},
{
"scheme": "http",
"host": "localhost",
"port": 8082,
"metadata": {
"management.port": "8082"
},
"secure": false,
"instanceInfo": {
"instanceId": "localhost:customer-service:8082",
"app": "CUSTOMER-SERVICE",
"appGroupName": null,
"ipAddr": "10.0.75.1",
"sid": "na",
"homePageUrl": "http://localhost:8082/",
"statusPageUrl": "http://localhost:8082/actuator/info",
"healthCheckUrl": "http://localhost:8082/actuator/health",
"secureHealthCheckUrl": null,
"vipAddress": "customer-service",
"secureVipAddress": "customer-service",
"countryId": 1,
"dataCenterInfo": {
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name": "MyOwn"
},
"hostName": "localhost",
"status": "UP",
"overriddenStatus": "UNKNOWN",
"leaseInfo": {
"renewalIntervalInSecs": 30,
"durationInSecs": 90,
"registrationTimestamp": 1616667913690,
"lastRenewalTimestamp": 1616667913690,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1616667913690
},
"isCoordinatingDiscoveryServer": false,
"metadata": {
"management.port": "8082"
},
"lastUpdatedTimestamp": 1616667913690,
"lastDirtyTimestamp": 1616667913505,
"actionType": "ADDED",
"asgName": null
},
"instanceId": "localhost:customer-service:8082",
"serviceId": "CUSTOMER-SERVICE",
"uri": "http://localhost:8082"
}
]
Eureka Server API
Eureka Server provides various APIs for the client instances or the services to talk to. A lot of these APIs are abstracted and can be used directly with @DiscoveryClient we defined and used earlier. Just to note, their HTTP counterparts also exist and can be useful for Non-Spring framework usage of Eureka.
In fact, the API that we used earlier, i.e., to get the information about the client running “Customer_Service” can also be invoked via the browser using http://localhost:8900/eureka/apps/customer-service as can be seen here −
<div>
<a id="slick_uniqueid"/>
</div>
<name>CUSTOMER-SERVICE</name>
<instance>
<instanceId>localhost:customer-service:8082</instanceId>
<hostName>localhost</hostName>
<app>CUSTOMER-SERVICE</app>
<ipAddr>10.0.75.1</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">8082</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo
class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>30</renewalIntervalInSecs>
<durationInSecs>90</durationInSecs>
<registrationTimestamp>1616667913690</registrationTimestamp>
<lastRenewalTimestamp>1616668273546</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1616667913690</serviceUpTimestamp>
</leaseInfo>
<metadata>
<management.port>8082</management.port>
</metadata>
<homePageUrl>http://localhost:8082/</homePageUrl>
<statusPageUrl>http://localhost:8082/actuator/info</statusPageUrl>
<healthCheckUrl>http://localhost:8082/actuator/health</healthCheckUrl>
<vipAddress>customer-service</vipAddress>
<secureVipAddress>customer-service</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1616667913690</lastUpdatedTimestamp>
<lastDirtyTimestamp>1616667913505</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
<instance>
<instanceId>localhost:customer-service:8081</instanceId>
<hostName>localhost</hostName>
<app>CUSTOMER-SERVICE</app>
<ipAddr>10.0.75.1</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">8081</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo
class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>30</renewalIntervalInSecs>
<durationInSecs>90</durationInSecs>
<registrationTimestamp>1616667914313</registrationTimestamp>
<lastRenewalTimestamp>1616668274227</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1616667914313</serviceUpTimestamp>
</leaseInfo>
<metadata>
<management.port>8081</management.port>
</metadata>
<homePageUrl>http://localhost:8081/</homePageUrl>
<statusPageUrl>http://localhost:8081/actuator/info</statusPageUrl>
<healthCheckUrl>http://localhost:8081/actuator/health</healthCheckUrl>
<vipAddress>customer-service</vipAddress>
<secureVipAddress>customer-service</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1616667914313</lastUpdatedTimestamp>
<lastDirtyTimestamp>1616667914162</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
</application>
Few other useful APIs are −
Action |
API |
Register a new service |
POST /eureka/apps/{appIdentifier} |
Deregister the service |
DELTE /eureka/apps/{appIdentifier} |
Information about the service |
GET /eureka/apps/{appIdentifier} |
Information about the service instance |
GET /eureka/apps/{appIdentifier}/ {instanceId} |
More details about the programmatic API can be found here https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html
Eureka – High Availability
We have been using Eureka server in standalone mode. However, in a Production environment, we should ideally have more than one instance of the Eureka server running. This ensures that even if one machine goes down, the machine with another Eureka server keeps on running.
Let us try to setup Eureka server in high-availability mode. For our example, we will use two instances.For this, we will use the following application-ha.yml to start the Eureka server.
Points to note −
- We have parameterized the port so that we can start multiple instances using same the config file.
- We have added address, again parameterized, to pass the Eureka server address.
- We are naming the app as “Eureka-Server”.
spring:
application:
name: eureka-server
server:
port: ${app_port}
eureka:
client:
fetch-registry: true
serviceURL:
defaultZone: ${eureka_other_server_url}
Let us now recompile our Eureka server project. For execution, we will have two service instances running. To do that, let's open two shells and then execute the following command on one shell −
java -Dapp_port=8900 '-Deureka_other_server_url=http://localhost:8901/eureka' -
jar .\target\spring-cloud-eureka-server-1.0.jar --
spring.config.location=classpath:application-ha.yml
And execute the following on the other shell −
java -Dapp_port=8901 '-Deureka_other_server_url=http://localhost:8900/eureka' -
jar .\target\spring-cloud-eureka-server-1.0.jar --
spring.config.location=classpath:application-ha.yml
We can verify that the servers are up and running in high-availability mode by looking at the dashboard. For example, here is the dashboard on Eureka server 1 −
And here is the dashboard of Eureka server 2 −
So, as we see, we have two Eureka servers running and in sync. Even if one server goes down, the other server would keep functioning.
We can also update the service instance application to have addresses for both Eureka servers by having comma-separated server addresses.
spring:
application:
name: customer-service
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka, http://localhost:8901/eureka
Eureka – Zone Awareness
Eureka also supports the concept of zone awareness. Zone awareness as a concept is very useful when we have a cluster across different geographies. Say, we get an incoming request for a service and we need to choose the server which should service the request. Instead of sending and processing that request on a server which is located far, it is more fruitful to choose a server which is in the same zone. This is because, network bottleneck is very common in a distributed application and thus we should avoid it.
Let us now try to setup Eureka clients and make them Zone aware. For doing that, let us add application-za.yml
spring:
application:
name: customer-service
server:
port: ${app_port}
eureka:
instance:
metadataMap:
zone: ${zoneName}
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
Let us now recompile our Eureka client project. For execution, we will have two service instances running. To do that, let's open two shells and then execute the following command on one shell −
java -Dapp_port=8080 -Dzone_name=USA -jar .\target\spring-cloud-eureka-client-
1.0.jar --spring.config.location=classpath:application-za.yml
And execute the following on the other shell −
java -Dapp_port=8081 -Dzone_name=EU -jar .\target\spring-cloud-eureka-client-
1.0.jar --spring.config.location=classpath:application-za.yml
We can go back to the dashboard to verify that the Eureka Server registers the zone of the services. As seen in the following image, we have two availability zones instead of 1, which we have been seeing till now.
Now, any client can look at the zone it is present in. Say the client is located in USA, it would prefer the service instance of USA. And it can get the zone information from the Eureka Server.