- Spring Cloud Tutorial
- Spring Cloud - Home
- Spring Cloud - Introduction
- Dependency Management
- Service Discovery Using Eureka
- Synchronous Communication with Feign
- Spring Cloud - Load Balancer
- Circuit Breaker using Hystrix
- Spring Cloud - Gateway
- Streams with Apache Kafka
- Distributed Logging using ELK and Sleuth
- Spring Cloud Useful Resources
- Spring Cloud - Quick Guide
- Spring Cloud - Useful Resources
- Spring Cloud - Discussion
Spring Cloud - Service Discovery Using Eureka
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 service instances. The service 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 instances.
Apache Zookeeper, Eureka and 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 −
<dependencies> <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.tutorialspoint; 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’s 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 −
server: 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.
Setting up Eureka Client for Instance
Now, we will set up the service instances which would register to the Eureka server. For setting up Eureka Client, we will use a separate Maven project and update the POM file to contain the following dependency −
<dependencies> <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.tutorialspoint; 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. 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
spring: application: name: customer-service server: port: ${app_port} eureka: client: serviceURL: defaultZone: http://localhost:8900/eureka
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
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 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.tutorialspoint; 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 −
<application slick-uniqueid="3"> <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: 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.