Dokumen - Pub - Spring Cloud Fundamentals With Java Codes
Dokumen - Pub - Spring Cloud Fundamentals With Java Codes
Table of contents
Table of contents 2
Module 1: Course Overview 6
Module 2: Getting Familiar with Spring Cloud 7
The Infamous Cloud 7
New Challenges with the Cloud 8
Your Focus for the book 9
Prerequisites 10
Module 3: Finding Services Using Service Discovery 11
What Is Service Discovery? 11
Introducing Spring Cloud Netflix 15
Key Components Involved in Service Discovery 17
The Discovery Server 19
Demo: Setting up a Service Discovery Server 22
The Application Service 28
Demo: Making an Application Service Discoverable 32
The Application Client 41
Demo: Finding and Calling Services as an Application Client 46
Spring Cloud Eureka Dashboard 54
Configuration 58
Health and High Availability 59
AWS Support 60
Summary 67
Module 4: Configuring Services Using Distributed Configuration 68
What Is a Configuration Server? 68
Introducing Spring Cloud Config Server 71
Using the Spring Cloud Config Server 73
Config Server REST Support: Parameters 78
Config Server REST Support: Endpoints 80
Demo: Setting up a Configuration Server 83
Using the Spring Cloud Config Client 95
Demo: Retrieving Configuration with the Config Client 99
Updating Configuration at Runtime 109
Utilizing the @RefreshScope Annotation 111
Demo: Refresh Configuration Without Restarting Your App 115
Encrypting and Decrypting Sensitive Configuration 121
Using the Encryption and Decryption Support 124
Encrypt and Decrypt REST Endpoints 127
Summary 129
Module 5: Mapping Services Using Intelligent Routing 130
What Is Intelligent Routing? 130
Intelligent Routing via a Gateway Service 132
Using Netflix Zuul with Spring Cloud 134
Configuring Routes in Netflix Zuul 138
Demo: Using Netflix Zuul as an Intelligent Router 141
Creating Filters with Netflix Zuul & Spring Cloud 155
Demo: Creating and Using a ZuulFilter 159
Summary 165
Module 6: Calling Services Using Client-side Load Balancing 166
Module Introduction 166
Client-side vs. Server-side Load Balancing 168
Getting Started with Spring Cloud and Netflix Ribbon 171
Using the @LoadBalanced Annotation 174
Demo: Load Balancing Using Ribbon with Service Discovery 176
Using the @RibbonClient Annotation 188
Demo: Load Balancing Using Ribbon Without Service Discovery 191
Customizing Your RibbonClient Configuration: Introduction 196
Customizing Your RibbonClient Configuration: The IRule Bean 199
Customizing Your RibbonClient Configuration: The IPing Bean 201
Demo: Customizing the RibbonClient Load Balancing Strategy 203
Summary 207
Module 7: Creating Self-healing Services with Circuit Breaker 208
Introduction 208
Cascading Failures and Resource Overloading 210
Embracing Failure with the Circuit Breaker Pattern 212
Fault Tolerance with Netflix Hystrix and Spring Cloud 214
Using Spring Cloud and Netflix Hystrix 216
Demo: Implementing Fault Tolerance with Netflix Hystrix 220
Metrics and Insight with the Hystrix Dashboard 235
Using Spring Cloud and the Netflix Hystrix Dashboard 237
Reading and Understanding the Hystrix Dashboard 239
Demo: Monitoring Fault Tolerance Metrics with Hystrix
Dashboard 243
Aggregating Hystrix Metrics with Netflix Turbine 249
Using Spring Cloud and Netflix Turbine 251
Demo: Aggregating Multiple Hystrix Streams with Turbine 255
Summary 262
Module 8: Bringing It All Together and Where to Go Next 263
Introduction 263
How Does It All Fit Together? 265
Putting It All Together: On Startup 267
Putting It All Together: On Request 269
Where to Go Next 271
Module 1: Course Overview
The cloud, or cloud computing, is truly changing the way we, as developers,
think about design and develop software. And that's where Spring Cloud
comes in. Spring Cloud helps you take full advantage of these new
paradigms by bringing together the best of Spring Boot with proven cloud
strategies to help you design and develop cloud-native applications. Some of
the major topics we'll cover include service discovery using Spring Cloud
and Netflix Eureka, distributed configuration using Spring Cloud Config
Server, client-side load balancing using Spring Cloud and Netflix Ribbon,
intelligent routing via a gateway service using Spring Cloud and Netflix
Zuul, and fault tolerance using Spring Cloud and Netflix Hystrix. By the
end of this book, you'll know how to build applications that take full
advantage of the cloud. Before beginning the book, you should be familiar
with Java, Spring Boot, and have at least an introductory level
understanding of microservices.
Module 2: Getting Familiar with Spring
Cloud
I'm sure you've heard of this infamous thing called the cloud as it's often
hyped as this game changer or the sort of magical solution to everything.
It'll solve all your problems. And with so many things to learn these days,
it's hard not to ignore a lot of that and just kind of brush it off as clever
marketing. But now companies and enterprises are finally starting to truly
embrace the cloud, and some of that hype is actually becoming a reality.
And it's more often the norm to see enterprises and companies using the
cloud than it is the exception. As software engineers, I think we have some
really exciting times ahead of us. Cloud computing, or the cloud, is really
changing the way that we build software. We're moving from using these
centralized monoliths to applications which are distributed and use
microservices. And not only is the software changing, but the hardware is
changing as well. We're moving from this managed and finite resource to
this infinite and on demand and self-service resource.
New Challenges with the Cloud
With the cloud, there comes these new challenges. We have to think
differently. Things are not quite as static as we're used to, and we can't just
design and architect and use the same principles or techniques that we're
used to. The cloud is this elastic and ephemeral thing. Things can grow and
shrink and appear and disappear at any given time. So we have to consider
that the cloud is this ever-changing and constantly evolving thing, whereas
we may be used to something that is a bit more static. We also can't just
move our existing applications to the cloud and expect them to be
automatically cloud enabled. This is often referred to as the lift and shift
migration. And sure we're going to get some benefits by moving to the
cloud, but we're not fully utilizing the cloud. In order to fully utilize the
cloud, it requires change. And that's where Spring Cloud helps. Spring
Cloud helps you build cloud-native applications. Now, you're probably
asking what is a cloud-native application? Well, a cloud-native application
means that your application was specifically built and engineered for the
cloud. It means your application fully utilizes all of the cloud computing
paradigms. Spring Cloud itself is not actually a framework. Loosely
speaking, Spring Cloud is used to describe a number of projects that all fall
under the same umbrella. In this book, we'll focus on the fundamentals,
which is Spring Cloud Config and Spring Cloud Netflix.
Your Focus for the book
➢ Service Discovery
➢ Distributed
➢ Configuration
➢ Intelligent Routing
➢ Client-side Load Balancing
➢ Circuit Breaker
We're going to specifically target a number of key areas. First, we're going
to look at service discovery. How do you dynamically find your application
services in the cloud at runtime? Then, we'll move on to distributed
configuration, or how to manage common or service-specific configuration
in a distributed system. Then, we'll look at intelligent routing, or how to
make a distributed system look as if it were a single cohesive system using
Intelligent Routing. Then, we'll look at client-side load balancing, or how
you distribute load among several instances of the same service. And last,
we'll look at how you can use the circuit breaker pattern to build fault-
tolerant applications in the cloud.
Prerequisites
➢ Java 8+
➢ Spring Boot 1.4+
➢ Knowledge of Microservices or SOA
Java 8+
Maven 3+
Spring Tool Suite (Eclipse STS) 3.8+
Next, let's talk about what software you're going to need to be successful in
this book. Of course you're going to need Java, Java 8, and you'll need
Maven, at least Maven 3, in order to build the Spring Boot applications.
And we're going to be doing all of our development using Spring Tool Suite,
or Eclipse STS, as it's called. Make sure you have at least version 3.8.
Module 3: Finding Services Using Service
Discovery
➢ In this module :
○ Service Discovery
○ Discovering Services with Spring Cloud
■ Using Eureka client and server
■ Configuration
■ Health & High Availability
■ Dashboard
■ AWS Support
In this module we'll look at how service discovery helps you locate your
application services in the cloud. Let's quickly start off by talking about
what this module contains, and first we're going to look at service discovery.
Obviously this module's about service discovery, so we'll talk about what it
is and why it's important. Then, we're going to look at how Spring Cloud
implements service discovery. It uses a project from Netflix that was open
sourced, it's called Eureka, and we're going to talk about the Eureka Client
and the Eureka Server. Then, we'll move on to configuration, configuring
the Eureka Client, the Eureka Server, and the Eureka Instance and how
each of those are different and what you need to do to tweak them or
configure them for your needs. Then, we'll move into health and high
availability. How does the Eureka Server know when your application is
down or when it's unhealthy? And how do you ensure that the Eureka
Server is highly available? And then we're going to look at the Eureka
Dashboard, which is a nice web UI that shows you all of your registered
services, how many instances there are, and whether they're up or down.
And last, we're going to finish out with how Eureka has specific support for
AWS. Let's get started with the most important question, what is service
discovery and why do we need it? Remember that the cloud is changing the
way that we build software. We're moving from building these single large
applications and instead breaking them up into smaller and smaller pieces
called services. And each of those individual services can then be deployed
and scaled on their own, and together, as a whole, they form the overall
application. And herein lies the problem. How does one service know where
another service is at, its host and its port, so that it can call it and use it? For
starters, we could simply configure all of our services to know the location
and the port of other services that it calls. And, depending on our needs, this
actually might get us pretty far. But after a while, we'll learn that there are
some problems to this approach. What if, for example, you had two
instances of a particular service? So, in our example here, we have
Application Service A calling Application Service B. And if we used
configuration, every time we added or removed a new instance of
Application Service B, we'd have to update that configuration. And, well, in
our example, we only have two instances. Imagine if you had hundreds of
instances. The configuration management alone would be unsustainable.
Our simple configuration starts to break down even further as we move to a
cloud environment. In a cloud environment, you have instances of services
that can come and go in response to demand, for instance. So, for example,
Application Service B starts with two instances, and consider that maybe
you have this huge influx of traffic, maybe it's a flash sale on your
eCommerce website, and an automated process kicks off and starts two
more instances to handle all of that demand. Well, if you're using simple
configuration, all of the callers of Application Service B, such as Application
Service A, would not even know about the two new instances that were
added in response to that demand. As far as they're concerned, their
configuration says that there are only two instances that they know about.
Another thing to consider is that application services will eventually fail.
And regardless of the situation, whether it's a memory problem or a
hardware problem, if you're using simple configuration, your services are
going to continue to try to send traffic to those failed instances. For example
here, we have Application Service B being called by Application Service A,
and it has two instances. And if one of those instances fails, Application
Service A is not going to know the better, and it's going to continue to send
traffic to that failed instance. We need something that is more dynamic. The
simple approach is just far too static. It's too frozen in time for our needs in
the cloud. That's where service discovery comes into play.
There are actually several different ways that you can discover services
using Spring Cloud. There's the Spring Cloud Consul project, there's the
Spring Cloud Zookeeper project, and there's the Spring Cloud Netflix
project. We're going to specifically focus on the last one, the Spring Cloud
Netflix project. The folks over at Netflix have some serious experience
building scalable applications in the cloud. And, in fact, you could probably
even argue they have some of the largest scalability problems you can
imagine. And they built some projects internally to handle these problems
and eventually released them as open source projects.
The Spring Cloud project took the Netflix open source projects and added
some Spring and some Spring Boot features. They sort of Spring-a-fide it, if
you will. And what was born out of that was the Spring Cloud Netflix
project. Similar to the Spring Cloud project, the Spring Cloud Netflix
project is not actually a project in and of itself. Rather, it's a collection of
projects. And for service discovery, we're interested in two of those projects:
the Spring Cloud Netflix Eureka Server and the Spring Cloud Netflix
Eureka Client.
Key Components Involved in Service Discovery
Before we dive in to the Spring Cloud Eureka Server and Client, it's helpful
to understand the key components that are involved in service discovery
and how they interact with each other. At a minimum, there are three
components involved in service discovery. There's the Discovery Server, the
application service, and the application client. It's helpful to get a full
understanding of how all the components work together and from there
we'll go deeper into each component and see how Spring Cloud helps us
implement that particular component.
The first thing that happens is the application service starts up. And when it
starts up, it calls out to the Discovery Server, and it registers itself. And it
tells the Discovery Server its location, its port, and a service identifier that
others can use to find it. Then at some point later, a client needs to call that
application service, but it doesn't know the location and the port of the
service, so it needs to ask the Discovery Server. It sends out a request to the
Discovery Server and sends along the service identifier. And the Discovery
Server knows that based on that service identifier which service you're
asking for, and it responds back with the location and the port of that
service. From there, things proceed as normal, and the client can request the
service and its location, and the service can respond back with data.
The Discovery Server
We're going deeper with each of the key components in service discovery,
and we're going to start with the Discovery Server. At its core, the Discovery
Server is an actively managed registry of service locations. It is responsible
for allowing others to find services and for services to register and
deregister themselves. It's the source of truth, if you will. And you would
typically run more than one instance of the Discovery Server as it's the key
component to locate all the other services. And if you can't locate the other
services, then you can't call the other services. So this is an important piece
of the overall architecture. And you can find the Discovery Server
implementation within the Spring Cloud Eureka Server project.
Throughout the book, we're going to be doing fairly simple demos, things
that are easy to set up and solidify the concepts. So in this example, we're
going to learn how to create a Discovery Server using Spring Cloud. And I'll
detail out each of the steps, and then we'll follow up with a real example.
Creating a Discovery Server with the Spring Cloud Eureka Server project is
very easy. In fact, it's almost embarrassingly easy.
pom.xml :
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
Application.properties :
spring.application.name=discovery-server
OR
Application.yml :
spring:application:name: discovery-server
@SpringBootApplication
@EnableEurekaServer
public class Application{
public static void main(String[]args){
SpringApplication.run(Application.class,args);
}
}
Then, in your main Application class, you literally define one annotation.
It's @EnableEurekaServer. And that's all there is to it. Once you start this
application up, you will having a running instance of a Discovery Server.
Demo: Setting up a Service Discovery Server
In this demo, we're going to be creating and starting up our own Service
Discovery Server using the Spring Cloud Eureka Server project. To start
things off, let's head over to start.spring.io, and we're going to use this
Spring Initializr to create the stub for our project. It's a nice little skeleton
creator that saves us a lot of time.
In the Group section here, I'm going to put io.ajay.kumar, and for the
Artifact I'm going to call it the discovery-server. In the dependencies, I'm
going to add the Eureka Server, this is the Discovery Server, and I'm also
going to add DevTools and the Spring Boot Actuator project.
Once you've added all those dependencies, click the Generate Project
button. After you click that Generate Project button, it's going to
automatically download a zip file for you, and this contains the stub of our
project. Go ahead and unzip the downloaded stub project. Then we're going
to go ahead and open up our IDE and import it. I've switched over to my
IDE, which is Spring Tool Suite. And on the Package Explorer tab in the
blank area, right-click and go to Import. And you're going to want to filter
out the available options by typing in Existing Maven Projects. Go ahead
and click that, click Next, browse to the location of the unzipped stub
project, and click Open. Underneath the Projects you should see your
pom.xml. Go ahead and click Finish, and give that a second, and it'll import
into the IDE.
Once the project is finished importing, go ahead and expand it and open up
the main application class. It should be called DiscoveryServerApplication,
and this is where we're going to add the annotation to enable our Discovery
Server. So right above this @SpringBootApplication annotation, we can add
a new one, @EnableEurekaServer. Go ahead and save that.
package io.ajay.kumar.discoveryserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import
org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
We're now ready to start our Service Discovery Server, so go ahead and
right-click on the main application class, and go to Run As, and choose
Spring Boot App.
Once the application has finished loading, go ahead and expand the Console
widow. And you'll notice that there are several exceptions in here. Go ahead
and stop the application server; otherwise, it will continue to throw those
exceptions. Let's scroll up to one of these exceptions and see what the
problem is. Cannot execute request on any known server. And if you look,
it's coming from this DiscoveryClient.getAndStoreFullRegistry method.
And what's happening is the Service Discovery Server is starting up, and it's
trying to register itself with a peer Service Discovery Server. And this is
mainly for high availability purposes. However, when we're running in
standalone or development mode, it can kind of be a pain to have to set up
multiple instances every time. So instead, we're going to configure the
Eureka Server not to try to register itself with its peers. And you definitely
only want to do this is development mode since you want that high
availability in production. Let's go ahead and close the Console. Then, in the
Package Explorer, navigate to src/main/resources and open up the
application.properties.
application.properties :
spring.application.name=discovery-server
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
server.port=8761
The first property we're going to add is actually one that we forgot
previously, it's the spring.application.name property, and we're going to set
that to discovery-server. The second property we're going to add is a Eureka
Client property, so go ahead and type eureka.client.register-with-eureka.
And since we're the Discovery Server our self, and we're running in
standalone mode, we don't need to register with any other peers because
there aren't any other peers. So make sure you set that value to false. The
next property is also a Eureka Client property, so go ahead and type
eureka.client.fetch-registry. And this property controls whether or not the
Eureka Client would fetch the registry from the Eureka Server, and since
we are the only Eureka Server, there's nothing else to fetch from anybody
else, so we'll set this to false. And the last property we're going to add is the
server.port, and we're going to set that to a value of 8761, which is the
default port for Eureka Discovery Server. Once you get those properties in
place, head over to the main application class, right-click on it, and go to
Run As Spring Boot App. When your app finishes starting up, expand the
console, and you'll see that it started the Eureka Server and it changed its
status to UP. So you now have a running Service Discovery Server.
The Application Service
pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
Still within the pom.xml, add a new dependency within the dependencies
elements, and this one is called spring-cloud-starter-eureka. Then in your
application.properties or your application.yml, add two new properties.
application.properties
spring.application.name=service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
OR
application.yml
spring:application:name: service
eureka:client:service-url:defaultZone: http://localhost:8761/eureka
@SpringBootApplication
@EnableDiscoveryClient
publicclassApplication{
publicstaticvoidmain(String[]args){
SpringApplication.run(Application.class,args);
}
}
And then in your main Application class of your application service, you
add one annotation. Again, it's just one annotation. The Spring Cloud guys
have made it so easy for us. And that is the @EnableDiscoveryClient
annotation. And what this does is it makes our application service register
itself with the Discovery Server, and then other services can find it.
Demo: Making an Application Service Discoverable
We'll use the Spring Initializr again to create our application service, so go
ahead and head over to start.spring.io. And in the Group here you're going
to put io.ajay.kumar, and we'll give the Artifact name service. And you'll
have pretty much the same dependencies, but this time you'll want to do the
Eureka Discovery dependency instead of the Eureka Server dependency.
And you can go ahead and add DevTools and the Actuator.
Go ahead and click Generate Project. It'll automatically create and
download a zip file for you. Go ahead and unzip that zip file, and we'll
import that into our IDE. And we're back in Spring Tool Suite. In the
Package Explorer tab, right-click and go to Import. And again, we want to
do Existing Maven Projects. Click Next, browse to the location of our
unzipped file, and click Open. You should see the pom.xml underneath the
Projects heading, and click Finish to finish the import. That'll take a second,
and once that's finished, expand the service and go to src/main/java and
open up the main application class.
package io.ajay.kumar.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@EnableDiscoveryClient
@SpringBootApplication
@RestController
public class ServiceApplication {
@Value("${service.instance.name}")
private String instance;
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class, args);
}
@RequestMapping("/")
public String message() {
return "Hello from " + instance;
}
}
application.properties
spring.application.name=service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
And then we're going to copy this run configuration to create our second
instance. So if you come over here to the Duplicate button, click that, give it
a new name of instance 2. We're going to change the port to 8082 and the
instance to 2. Click Apply, and close that. Before we start the instances of
our service application, we need to start the Discovery Server so they can
register themselves. So go ahead and expand the discovery-server, right-
click on the main application class, and go to Run As Spring Boot App.
Once the Service Discovery Server is started up, we can start up our
application instances. So come down to the main ServiceApplication class,
right-click, Run As, and go to the Run Configurations. Highlight instance 1
and click Run. And you can do the same thing for instance 2. So right-click
on the ServiceApplication, Run As, Run Configurations, highlight instance
2, and click Run. You can look at each of the instances by clicking this drop-
down over here. And you can see that we have the discovery-server started,
the instance 1 started, and instance 2 started. And we're currently looking
at instance 2. So if we expand out the Console, scroll over, and we can see
that our application successfully registered itself with the Discovery Server.
It says registering service, and it has a registration status of 204.
DiscoveryClient_SERVICE/DESKTOP-OIHU6JB:service:8082: registering
service...
DiscoveryClient_SERVICE/DESKTOP-OIHU6JB:service:8082 -
registration status: 204
And we can go look at the other instance, instance 1, scroll over, and we can
see the same thing, registering service and a registration status of 204.
DiscoveryClient_SERVICE/DESKTOP-OIHU6JB:service:8081: registering
service...
Tomcat started on port(s): 8081 (http) with context path ''
Updating port to 8081
Started ServiceApplication in 7.717 seconds (JVM running for 8.896)
DiscoveryClient_SERVICE/DESKTOP-OIHU6JB:service:8081 -
registration status: 204
And then if you go look at the discovery-server, and you'll see several
requests in there for our service application to register itself with the Service
Discovery Server.
Let's continue with our deep dive of the components involved in service
discovery. Up next is the application client. The application client is the
piece that would call out to another application service to implement some
piece of functionality in its service. It's the issuer of requests, and it depends
on other services. And similar to the application service, the application
client is also a user of the discovery client, but it uses the discovery client in
a different way. It doesn't use it to register or deregister anything. It uses it
to find service locations. Just to be clear, it's perfectly reasonable for an
application to be both a service and a client. An application can be a service,
which provides services to others, and at the same time can be a client,
which depends on other services. What we're referring to here is if the
application was just a client and it wasn't a service. The steps involved to set
up a client are quite similar to the steps involved to set up a service.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
application.properties
spring.application.name=client
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
eureka.client.register-with-eureka=false
OR
application.yml
spring:application:name: client
eureka:client:service-url:defaultZone: http://localhost:8761/eureka
register-with-eureka: false
We have the same two properties that we used within the application
service, the spring.application.name, except this time we set it to client, and
the eureka.client.defaultZone so that it can know the location of the Service
Discovery Server. And since we're a client, we're not interested in
registering with the Discovery Server because we don't need anybody to
discover us. We're just interested in discovering others. So you set that value
to false.
@SpringBootApplication
@EnableDiscoveryClient
public class Application{
public static void main(String[]args){
SpringApplication.run(Application.class,args);
}
}
Just like we did with the application service, we'll add the
@EnableDiscoveryClient annotation to our main Application class in our
application client. And then to actually discover services, we have two
different options.
➢ @Inject EurekaClientclient
➢ @Inject DiscoveryClientclient
InstanceInfo instance=eurekaClient.getNextServerFromEureka(
"service-id",false);
String baseUrl = instance.getHomePageUrl();
The first option is using the EurekaClient, and the EurekaClient has a
method, getNextServerFromEureka. And that'll pick the next instance in a
round-robin fashion from the Discovery Server. And its first argument is a
virtual host name or a service ID to call. And this is the same as the
spring.application.name property that we've used in our service application.
And the second argument is whether or not this is a secure request. Once we
get a reference to the instance info, we can call instance.getHomePageUrl,
and that'll give us the base URL that we can use with our RestTemplate to
call the service.
List<ServiceInstance> instances=client.getInstances("service-id");
String baseUrl=instances.get(0).getUri().toString();
The second option is to use the Spring DiscoveryClient, and it has a method
called getInstances, which returns you all service instances for a given
service ID. So that first argument is the same first argument that we saw in
the EurekaClient, which is a virtual hostname or a service ID of the service
you want to call. And once you have a list of instances, you can get one of
those instances, and you can get the URI, turn it into a string, and that's
your base URL that you would use in your RestTemplate.
Demo: Finding and Calling Services as an Application
Client
In order to create our client, we're going to again start off at start.spring.io.
And we're going to give it a group ID, the same one, so io.ajay.kumar, and
we'll make the Artifact client. And it will have the exact same dependencies
that we used in the application service. So we want Eureka Discovery, want
DevTools, and we want the Actuator.
Click Generate Project. That'll automatically create and download a zip file.
Go ahead and unzip that file, and we'll import that into our IDE. Within
Spring Tool Suite in the Package Explorer area, right-click in the empty
area, go to Import, choose Existing Maven Projects, browse to the location
of your unzipped file, ensure that the pom.xml shows up under the Projects
heading, and click Finish to import.
Expand the client project, and go to src/main/java, and open up the main
application class and do the following changes.
package io.ajay.kumar.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
@EnableDiscoveryClient
@SpringBootApplication
@RestController
public class ClientApplication {
@Autowired
private EurekaClient client;
@Autowired
private RestTemplateBuilder restTemplateBuilder;
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
@RequestMapping("/")
public String callService() {
RestTemplate restTemplate = restTemplateBuilder.build();
InstanceInfo instanceInfo =
client.getNextServerFromEureka("service", false);
String baseUrl = instanceInfo.getHomePageUrl();
ResponseEntity<String> response =
restTemplate.exchange(baseUrl, HttpMethod.GET, null,
String.class);
return response.getBody();
}
}
application.properties :
spring.application.name=client
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
eureka.client.register-with-eureka=false
Fire up a web browser and visit localhost:8080. Hit Enter, and you should
see a response from the client. And it'll say Hello from instance 1. And if
you refresh it, it'll say Hello from instance 2. And we can continue to do that
over and over, and each time we can see that it's getting a different instance
from the Eureka registry.
Spring Cloud Eureka Dashboard
One of the really useful things that the Spring Cloud Eureka Server
provides is a dashboard. It's enabled by default, and it's a web-based
dashboard. And it displays a bunch of useful information, like whether or
not a service is up or down and how many instances of it are registered. If
you still have the demo running from our last demo, fire up a browser and
visit localhost:8761. You'll be presented with this Spring Eureka Dashboard,
and let's go through each of these sections here.
So first we have the System Status, and it tells you things like the current
time, the uptime, and how many renewals have happened in the last minute.
The DS Replicas, or the Discovery Server Replica section, shows you all of
the peer instances of Eureka Servers. So if we had more than one instance
running, we would see each of those instances listed here. But since we've
only got one instance, we just see localhost. The next section is probably the
most useful section as it shows you what instances are currently registered
with Eureka. And we have here the service ID under the Application
heading, which is SERVICE. AMI is for Amazon Machine Image. We're not
running in Amazon, so that's n/a. And Availability Zones, there are two, and
both of those instances are UP. You can see the 2 next to the UP, which
indicates that there are two instances of this particular service, and both of
them are UP. And right next to that you can see both of those instances, one
of them running on 8082 and one of them running on 8081. The next
section, the General Info section, just gives you information about the
particular machine that you're running on. So it tells you things like the
number of CPUs and the available memory. And the last part, the Instance
Info, gives you information about this particular running instance, so things
like its IP address or whether or not it's up or down.
Configuration
There are several different areas where you can configure Spring Cloud
Eureka. We'll take a look at the three main areas. The first is the
eureka.server prefix, the second is the eureka.client prefix, and the third is
the eureka.instance prefix. Let's go into more detail into each of these areas
of configuration. The first one is the Eureka Server Configuration, and
that's all configuration under the eureka.server prefix. And this is going to
control everything that's related to configuring the Discovery Server. The
second one is the Eureka Client Configuration, and that's all configuration
under the eureka.client prefix. And this is responsible for controlling how
the discovery client interacts with the Discovery Server. So for instance, you
have things like the eureka.client.eureka-server-connect-timeout-seconds.
This would control how long the client waits to connect to the Eureka
Server before it times out. The third area of configuration is the Eureka
Instance, and that's all configuration under the eureka.instance prefix. And
a Eureka Instance is anything that registers itself with the Eureka Server so
that it can be discovered by others. The properties under the
eureka.instance prefix control how the instance registers itself with the
Eureka Server. So for instance, you have things like the eureka.hostname or
the eureka.health-check-url, and the instance could be configured with
custom values there, and that's how it would register with the Eureka
Server.
Health and High Availability
The Spring Cloud Eureka Server also has some additional features around
health and high availability. It's constantly ensuring that the application
services that it's returning or handing back to clients are healthy and
available. And it also ensures that in the event that the Discovery Server
goes down, all clients can still continue to operate. Like I mentioned, the
Eureka Server is constantly concerned with the health of the application
services that it's handing out to clients. And it assesses that health, at least
by default, by sending the clients a heartbeat every 30 seconds. And if it
doesn't hear back from that heartbeat after 90 seconds, it removes it from
the registry. Sending a heartbeat is the default configuration, but you can
also configure the Eureka Server to hit an endpoint, such as the /health
endpoint that comes with Spring Boot Actuator. Eureka was built with high
availability in mind, and one of the ways that it achieves that is when a
client requests a service location from the Discovery Server, the Discovery
Server actually sends back a copy of the registry. And what ends up
happening is the registry gets distributed across all of the clients. And if the
Service Discovery Server goes down, those clients can continue to operate.
You're probably thinking well wait though. What if the Discovery Server
has new information or one of the services goes down? Well, the client has to
renew its lease or fetch a new registry every so often, and it does so by
fetching deltas to update its registry. So it's pretty smart about not fetching
the full registry again and only getting the changes.
AWS Support
It's well known that Netflix is a heavy user of AWS. And given that Eureka
was born at Netflix, it's only fitting that it includes AWS support. Let's take
a look at the various support, as well as a typical deployment architecture.
When an application that's using the Eureka Client starts up, it checks to
see if it's running on an AWS instance. If it is, it calls out to the local
metadata service and retrieves some metadata about that instance. And it
gets things that are specific to AWS, such as the Amazon Machine Image
that's running or what region it's running in or what zone. And then it
sends that information up to the Discovery Server when it registers. Given
the fact that things can change so often in AWS, it's important that the
Discovery Server be located at a well-known location. So Eureka adds
support for Elastic IP binding. When a Eureka Server starts up and it
notices that it's running in AWS, it'll try to bind to the next available Elastic
IP so that it has a static or well-known IP. The Eureka Client is also zone
aware with a preference for the zone that it's currently running in. So it'll
try to contact the Discovery Server in its current zone, and if it can't reach
one, it'll try the next zone and try to find the next available Discovery
Server. And last, you can configure the Eureka Client to fetch the registry of
different remote regions. In order to utilize the AWS support, it requires a
little bit of extra configuration.
@Configuration
public class AppConfig{
@Bean
public EurekaInstanceConfigBean eurekaInstanceConfig(
InetUtilsProperties properties){
EurekaInstanceConfigBean bean=new
EurekaInstanceConfigBean(newInetUtils(
properties));
AmazonInfo info= AmazonInfo.Builder.newBuilder().autoBuild(
"eureka");
bean.setDataCenterInfo(info);
return bean;
}
}
EC2 Dashboard :
So for example, if you have the following where you have one Discovery
Server in the us-east-1b zone and another Discovery Server in the us-east-1e
zone, you would set up your property as follows: eureka.client.availability-
zones, and then the region is us-east-1, and then you would set the comma-
separated list of us-east-1b and us-east-1e. Similar configuration is required
in the application.properties for your service URLs.
➢ Service URL Configuration in application.properties
○ eureka.client.service-url.[zone]=http://[eip-dns]/eureka
Elastic IP Dashboard :
If, for instance, we had the following Elastic IPs allocated to us, we could
configure our application.properties as follows: with the first one being the
us-east-1b zone and then second one being the us-east-1e zone. And if you'll
notice, each of those is set to the Elastic IP DNS name for the HTTP address.
Following along with the previous screenshots of the AWS console and how
we configured everything, if you were to load up your Eureka Dashboard
after setting all of that up, it would look like this.
You would have two instances under your DS Replicas, and then you would
have two instances of Discovery Servers, each in their respective availability
zones.
Likewise, you'd be able to pull up the dashboard on each of those Discovery
Server instances, and you'd find AWS-specific metadata filled in, like the
instance-type or the ami-id or the instance-id.
We've reached the end of this module, so let's quickly go over what we
learned. We started off by taking a look at what service discovery is and
why it's important. Then we learned how to use the Spring Cloud Eureka
Client to discover services and the Spring Cloud Eureka Server to store a
registry of services so that others could discover them. Then we took a quick
look at the Spring Cloud Eureka Dashboard and saw an insight into the
Spring Cloud Eureka Server. From there, we talked about health checks,
high availability, and configuring Spring Cloud Eureka. And we finished off
by looking at the AWS specific support.
Module 4: Configuring Services Using
Distributed Configuration
➢ pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
First, in your pom.xml, make sure that you have the spring-cloud-
dependencies defined within your dependencyManagement section.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
application.properties :
server.port=8888
spring.cloud.config.server.git.uri=<uri_to_git_repo>
OR
application.yml
server:port: 8888
spring:cloud:config:server:git:uri: <uri_to_git_repo>
@SpringBootApplication
@EnableConfigServer
public class Application{
public static void main(String[] args){
SpringApplication.run(Application.class,args);
}
}
Typically when you're interacting with the Config Server's REST endpoints,
you're doing so using the Config Client; however, it's useful to understand
the available endpoints, at least for debugging purposes and also if you want
to use it in another language. Each of the available REST endpoints on the
Config Server share a common set of parameters. And the values of those
parameters influence the configuration that's returned.
The first parameter is the application parameter. And if you're utilizing the
Spring Cloud Config Client, it uses the value from the
spring.application.name property to fill in the value for that application
parameter. The next parameter is the profile parameter. And again, if
you're using the Spring Cloud Config Client, it's going to pull the value
from the spring.profiles.active property to fill in the value for that profile
parameter. So this translates into the active Spring Profile. The last
parameter, the label parameter, is a feature for grouping your configuration
files into kind of arbitrary named sets. And that could mean different things
depending on the back-end you're using. So for instance, if you're using Git
as a back-end, the label translates to the Git branch. Now take a second to
look at each of these endpoint parameters, and consider them as a whole,
and think about how you might combine one or two or all of them together
to identify a particular configuration.
Config Server REST Support: Endpoints
➢ Endpoint :
○ GET/{application}/{profile}[/{label}]
➢ Example :
○ /myapp/dev/master
○ /myapp/prod/v2
○ /myapp/default
➢ Endpoint :
○ GET/{application}-{profile}.(yml| properties)
➢ Example :
○ /myapp-dev.yml
○ /myapp-prod.properties
○ /myapp-default.properties
The next endpoint utilizes only two of the three parameters that we
discussed, the application parameter and the profile parameter. It allows
you to request either YAML or properties files, and it doesn't necessarily
matter what the source file is. So, for instance, if you had a source file in the
YAML format, you could still request it in the properties format, and it
would automatically convert it for you. The examples are very
straightforward. You have something like /myapp-dev.yml or something like
/myapp-prod.properties. And just like the last endpoint, you have to have a
profile, so you could have something like /myapp-default.properties.
➢ Endpoint :
○ GET/{label}/{application}-{profile}.(yml| properties)
➢ Example :
○ /master/myapp-dev.yml
○ /v2/myapp-prod.properties
○ /master/myapp-default.properties
The last REST endpoint is very similar to the one we just looked at with the
exception that you have to specify a label. In fact, they can be equivalent if
the label you specified is master as the previous endpoint always assumes
that the label is master. Just like the other examples, it's pretty
straightforward. Here's an example where we have /master/myapp-dev.yml
where master is the label, myapp is the application, dev is the profile, and
you've chosen to use a YAML extension so you'll get it back in YAML
format. And a very similar one, except this time it is a v2 for the label or the
branch, the Git branch, and myapp-prod, the same application name, with
prod being a different profile. And this time you've requested a properties
format so it'd be returned in properties. And the last example is a lot like
the first example where you have /master/myapp- and then the profile. And
since the profile is a required value, remember we need to use the value
default to represent that default profile, and then you're requesting it in the
properties format.
Demo: Setting up a Configuration Server
In this demo, we're going to be building and starting up our own Config
Server. So the easiest way to get started is to head over to start.spring.io.
For the Group ID we'll use io.ajay.kumar. For the Artifact we'll use config-
server. For the dependencies, we'll add the Config Server dependency, and
then we'll also add the Eureka Discovery dependency. Now this is an
optional dependency, but it will allow our Config Server to register itself
with the Discovery Server so that Config Clients can find it. And last, we'll
use the Actuator dependency.
Once you've got everything selected, go ahead and click Generate Project.
That will create a zip file, and go ahead and unzip that, and head over to
your IDE. Within Eclipse, or STS, right-click on the empty Package area, go
to Import, search for Existing Maven Projects, select it, click Next, browse
to the location of the downloaded zip file, so mine's in Downloads, config-
server, open that, and click Finish to finish importing it.
Expand the config-server project and navigate to the main application class.
package io.ajay.kumar.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
application.properties :
server.port=8888
spring.cloud.config.server.git.uri=
Within the application.properties, add a server.port property and set that
equal to 8888. Next, we need to configure the Config Server to know where
the configuration repository is located. So we'll say
spring.cloud.config.server.git.uri, we're going to use a Git repository, and
then we're going to set that to a config repository. Normally you would set
this to the location of your own configuration repository, but for this demo
we're going to use a prebuilt one that's on GitHub. So go ahead and open up
a browser. In the address bar, type https://github.com/ajaycucek/config-
repository. We're going to be forking this repository, which means we're
going to be creating a local copy in our own GitHub account. And that
requires that you have your own GitHub account. So if you don't already,
make sure that you sign up for one. And if you do, make sure you sign in.
Once you're all signed in, click the Fork button in the top right corner, and
that'll create a fork for you. Once you've got your fork created, it'll look
very similar to the unforked version, but underneath the repository name at
the top it'll say forked from ajayscf-config-repository. Next we want to grab
the clone URL, so head over to this green button, click it, and copy the clone
URL to your clipboard, and head back over to your IDE. Back in Eclipse, or
STS, we want access to the Git repository's view. And you can do that by
going to Window, Show View, Other, and choose Git Repositories. Within
the Git Repositories view, click on the link that says Clone a Git repository.
It'll pop up a new window. Choose Clone URI. Hit Next. Since we copied it
to our clipboard, it should automatically show up in the URI section with
the host and the repository path filled in as well. If it's not, just pasted in
that clone URL that we copied into the URI. Go ahead and click Next,
choose the master branch, and click Finish.
application.properties :
server.port=8888
spring.cloud.config.server.git.uri=https://github.com/ajaycucek/config-
repository.git
Since we still have the clone URL on our clipboard, let's set up the Config
Server's Git URL by clicking in the application.properties and pasting in
that value. Make sure you save that, and then you can close that file. Head
back over to the Git Repositories view and expand the repository. Right-
click on the Working Tree and choose Import Projects. A new dialog box
will pop up, and just click Finish.
application.properties :
# Put all global configuration here
some.property=global
some.other.property=global
config-client-app-prod.properties :
# Put application+profile-specific config here (app name: client-config-app)
(profile name: prod)
some.property=profile specific value
some.other.property=profile specific value
config-client-app.properties :
# Put application-specific config here (application name: client-config-app)
some.property=app specific overridden value
In the Package Explorer, expand the config repository project, and you'll
notice that there are three configuration files. Let's take a look at the first
configuration file, the application.properties. This is where all of the global
configuration goes. The application.properties applies to any application
that asks for configuration from the Config Server. Next, open up the
config-client-app.properties file, and note that this is an application-specific
configuration file. So this would only apply to an application named config-
client-app. When we demo the Spring Cloud Config Client, we'll configure
the Spring application name of that project to be the config-client-app so
that this configuration only applies to that application. Go ahead and open
up the last configuration file, the config-client-app-prod.properties file. And
this configuration file is similar to the one we just looked at. And it would
apply to the same application, but instead it would only apply to the
application if the application was running with the Spring Profile named
prod. Let's close all of these configuration files and start up our
Configuration Server. So go over here to the config-server, expand it, find
the main application class, right-click on it, and go to Run As Spring Boot
App. You need to start the discovery-server too. Once the Configuration
Server is started, let's open up a browser and hit some of the REST
endpoints.
If we modify the URL and replace default with prod, we'll see that it brings
in the config-client-app-prod.properties file, which overrides all of the
property sources below it. Don't forget that we also have a couple other
endpoints, like an endpoint to request the .properties file or the .yml file.
So if we go up here, we take out /prod and we replace it with .properties,
we'll see that it returns back a properties file. And you'll see that it's
actually applied all of the property sources in the order which take
precedence. So you see that some.other.property is still global, but
some.property was overridden by the app specific overridden value.
And you can do the same thing with YAML. It will actually convert the
source.properties files into YAML format for you. So instead of .properties,
we'll replace this with .yml, and we hit Return, and you can see that it
output the same configuration in YAML format.
Using the Spring Cloud Config Client
Now that we've seen how the Spring Cloud Config Server works and how to
get one set up, let's look at the other end of the equation, the Client. At its
core, the Config Client is responsible for bootstrapping and fetching
application configuration. So what do I mean when I say that the Config
Client is responsible for bootstrapping application configuration? Well,
when a Spring application starts up, it needs to resolve its property sources.
And it needs to do that very early on in the startup process. Some of the
reasons for that are things like your property placeholders. When you
resolve those, it needs to actually have the values to resolve the placeholders.
And since the configuration lives on the Configuration Server, that means
that the Config Client needs to fetch the application configuration before
the Spring application context has even technically started. If it waited until
the application was fully started, it would be too late in the process. There
are two different ways that you can get the Config Client to bootstrap your
application properties, and they both use a special file called the
bootstrap.properties or the bootstrap.yml. The first way is Config First, and
you do that by configuring a bootstrap.yml or a bootstrap.properties that
has the application name, as well as the URL to the Configuration Server.
The second way is Discovery First, and that's using service discovery. So
you would configure your bootstrap.properties or bootstrap.yml to have the
application name and then the location of the Service Discovery Server. And
it would use that to then find the Config Server so that it could fetch your
configuration. Setting up an application to use the Spring Cloud Config
Client is even easier than setting up the Spring Cloud Config Server.
pom.xml :
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency
Then within the dependency section, still within your pom.xml, define a new
dependency on spring-cloud-config-client. Then we need to configure how
the Config Client will bootstrap the configuration. If you're using Config
First, you define a bootstrap.properties or a bootstrap.yml.
bootstrap.properties :
spring.application.name=<your_app_name>
spring.cloud.config.uri=http://localhost:8888/
OR
bootstrap.yml :
spring:application:name: <your_app_name>
cloud:config:uri: http://localhost:8888/
And you define the spring.application.name property, and this is the name
of your application. It will use this when it calls the Spring Cloud Config
Server's REST services to find the appropriate configuration. And then the
next property is the spring.cloud.config.uri property, and you would set that
to the location of your Config Server. And in this example I've just used
localhost here. If you're using a Discovery First configuration to bootstrap
the Config Client, the configuration is similar to the one we just looked at,
but it's slightly different.
bootstrap.properties :
spring.application.name=<your_app_name>
spring.cloud.config.discovery.enabled=true
OR
bootstrap.yml :
spring:application:name: <your_app_name>
cloud:discovery:enabled: true
In this demo, we'll see how to use the Spring Cloud Config Client to retrieve
configuration at startup from the Configuration Server. To get started, head
on over to start.spring.io. For the Group ID, use io.ajay.kumar. For the
Artifact name, use config-client-app. For the dependencies, we're going to
want the Config Client, of course, we're going to want Eureka Discovery so
that we can use service discovery to find the Configuration Server, and we'll
want the Spring Actuator. Once you've got everything selected, click the
Generate Project button. That will generate a zip file. Click on that, unzip
it, and head over to your IDE.
package io.ajay.kumar.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
application.properties :
server.port=8888
spring.cloud.config.server.git.uri=https://github.com/ajaycucek/config-
repository.git
spring.application.name=configserver
eureka.client.server-url.defaultZone=https://localhost:8761/eureka
package io.ajay.kumar.configclientapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigClientAppApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientAppApplication.class, args);
}
}
package io.ajay.kumar.configclientapp;
import
org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix="some")
public class ConfigClientAppConfiguration {
private String property;
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
}
package io.ajay.kumar.configclientapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class ConfigClientAppApplication {
@Autowired
private ConfigClientAppConfiguration properties;
@Value("${some.other.property}")
private String someOtherProperty;
@RequestMapping
public String printConfig() {
StringBuilder sb = new StringBuilder();
sb.append(properties.getProperty());
sb.append(" || ");
sb.append(someOtherProperty);
return sb.toString();
}
}
bootstrap.properties :
spring.application.name=config-client-app
spring.cloud.config.discovery.enabled=true
eureka.client.server-url.defaultZone=http://localhost:8761/eureka
In your browser, visit localhost:8080, and you'll see that it's resolved the
configuration values. The first one, the app specific overridden value is the
value from the some.property property. And remember we set that to this
app specific overridden value in the Config Client configuration. And then
the next property, separated by the ||, the global, is coming from the global
properties, the application.properties.
Updating Configuration at Runtime
The first way is manually, and that's by calling the refresh endpoint that's
included in the Spring Actuator project. And just to be clear, you would
need to do that for every individual service that needs its configuration
updated. The second way is a combination of both manual and automatic.
And you can imagine that if you had a lot of servers, calling the refresh
endpoint on each of them manually could be a pretty painful process.
Instead, if each of the applications were to subscribe to an event, and you
were to call the bus/refresh endpoint, Spring Cloud Bus would send out a
message to all of the subscribers indicating to them that they need to refresh
their configuration. Now note that there isn't any sort of intelligence in
whether or not the configuration changed. Every subscriber goes and gets
its new configuration regardless. The third way is just like the second except
for it adds a level of intelligence. And the way it does that is you hook it up
into your virgin control system, like Git, and any time you make a commit,
the changeset of the commit is posted to a monitor endpoint, and then that
monitor endpoint can determine which services need to have their
configuration updated. So let's visualize this. Imagine that you made a
commit to some configuration, and you pushed that up to the configuration
repository. And the repository knows what changed, and it sent that
changeset to the monitor endpoint, and then it decided that it only needed to
notify two of the three applications to update their configuration.
Regardless of which method you chose to notify your applications about
configuration updates, there's one last step. And that is to celebrate. You
can brag to your colleagues about making configuration changes on the fly
without ever restarting your application. And you can do it all at once or
even all automatically. And last, since you used Git, you have a full audit log
of all of the changes that you've made to your configuration.
Utilizing the @RefreshScope Annotation
@Configuration
public class SomeConfiguration{
@Bean
public FooService fooService(FooProperties properties){
return new FooService(properties.getConfigValue());
}
}
Example: @Bean Will Not See New Config Value After a Refresh
➢ Configuration updates are made
○ Note that FooProperties is a @ConfigurationProperties class
➢ POST to /refresh
➢ Result: FooService will still contain the OLD configuration value
○ Only gets configuration during initialization
@Configuration
public class SomeConfiguration{
@Bean @RefreshScope
public FooService fooService(FooProperties properties){
return new FooService(properties.getConfigValue());
}
}
Example: Utilizing @RefreshScope
➢ Add the @RefreshScope annotation to the @Bean
➢ POST to /refresh
➢ Result:FooService will now contain the NEW configuration value!
○ @RefreshScopetells Spring to please reinitialize this @Bean
And you'll see the configuration values that got resolved from the
Configuration Server. Now, we want to update each of these values, so we're
going to update the first one, the app specific overridden value, and the
global one. So, head back to the IDE. Back in the IDE, expand the config-
repository project, and open up the config-client-app.properties file.
config-client-app.properties :
# Put application-specific config here (application name: client-config-app)
some.property=coffee
some.other.property=is good
And we're going to change this from the app specific overridden value to
coffee. And then we're also going to set the other one. We're going to
override the some.other.property, and we're going to say is good. Go ahead
and save those. Close that file. And then we're going to commit those
changes, so right-click on the scf-config-repository, go to Team, go to
Commit, type in a commit message, Updating configuration, and click the
Commit and Push. And it'll pop up a new dialog showing you that it pushed
those changes. Just go ahead and click OK. And you can exit out of that Git
Staging view. So now we've updated the configuration, and we've pushed it
to the configuration repository. And if you go back to your browser and you
refresh the page, the value should still be the old values.
And ooh, that's interesting. The first value refreshed for us, we got the
coffee value that we set in, but the second value is still pointing to the global
configuration value. And the reason for that is that the second value is
retrieved using the @Value annotation, and @Value annotations are not
automatically refreshed when you call the refresh endpoint. So we can fix
that with the @RefreshScope annotation. Back within the IDE, stop the
Config Client app. Then in the main application class, let's find the
someOtherProperty. So we have here the @Value annotation that gets the
some.other.property value. So to fix our problem of the @Value annotation
not updating, let's add the @RefreshScope to the main application class.
package io.ajay.kumar.configclientapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableDiscoveryClient
@RestController
@RefreshScope
public class ConfigClientAppApplication {
@Autowired
private ConfigClientAppConfiguration properties;
@Value("${some.other.property}")
private String someOtherProperty;
@RequestMapping
public String printConfig() {
StringBuilder sb = new StringBuilder();
sb.append(properties.getProperty());
sb.append(" || ");
sb.append(someOtherProperty);
return sb.toString();
}
}
So you type @RefreshScope. Go ahead and save that and restart the
application. Now since we restarted the application, the application's going
to fetch the latest configuration values when it starts up. So we won't
actually be demonstrating the use of @RefreshScope because it's going to
get the latest value regardless. And so what we need to do is we need to
update the value again. So go ahead and open up the config-client-app, and
change this some.other.property to is really good.
config-client-app.properties :
# Put application-specific config here (application name: client-config-app)
some.property=coffee
some.other.property=is really good
Save that, close that file out, and again commit that up to the Git repository.
So Team, Commit, updating configuration again, and click the Commit and
Push button. You can exit the Git Staging view. And head back to your
browser and refresh the page.
As you can see, it says coffee is good. And is good is the value that it
retrieved on start up. But once we refresh this with the refresh scope now
added, it will fetch the latest value for that @Value annotation. So go ahead
and open up Postman, and on the New Tab where it says GET, click that
drop-down and choose POST. Then in the request URL type
http://localhost:8080/refresh. Once you've got that typed in, hit Send, and
you can see it changed the some.other.property. Head back to the browser,
and refresh that page.
And you can see that it changed our property this time from is good to is
really good.
Encrypting and Decrypting Sensitive Configuration
Spring Cloud Config Server also provides additional useful functionality for
encrypting and decrypting your configuration. It has support for several
different features.
Supported features :
➢ Encrypted configuration at rest and/or in-flight
➢ An /encrypt endpoint to encrypt configuration
➢ A /decrypt endpoint to decrypt configuration
➢ Encrypting and decrypting with symmetric or asymmetric keys
application.properties :
my.datasource.username=foobar
my.datasource.password={cipher}ASFIOWRODSKSDFIR32KJL
application.yml :
my:datasource:username: foobar
password: ‘{cipher}ASFIOWRODSKSDFIR32KJL’
Before we get into the details about configuring our Configuration Server
for encryption and decryption support, note that it assumes we have the
Java Cryptography Extension installed, or commonly referred to as JCE.
➢ Symmetric Key
➢ Asymmetric Key
○ Public Key
○ Private Key
The first step to encrypting your configuration is to choose the key type, and
that could be either symmetric or asymmetric. And typically symmetric
keys are easier to use, but less secure than asymmetric keys. And you can
choose whatever works best for your situation.
Step two, if you're using a symmetric key, is to configure the encryption key.
And you can do that in the application.properties or the application.yml.
And the property that is set is encrypt.key. And you would set that to the
secret value you'll use to encrypt your configuration.
Step Two (Asymmetric): Configure the Config Server Option 1
application.properties :
encrypt.key=<pem_encoded_key_as_text>
application.yml :
encrypt:key: <pem_encoded_key_as_text>
Step two, if you're using asymmetric keys, can be done in two different
ways. The first way is to set the value of the encrypt.key property in the
application.properties or the application.yml as a
pem_encoded_key_as_text. And the PEM encoded key would contain both
the public and the private key.
The second way, if you're using asymmetric keys, is to use the Java
KeyStore. Once you've created or imported your public and private key into
an existing or new keystore, you simply configure the location of the
keystore, the password to the keystore, and the name of the alias that you
chose when creating or importing the key into the keystore. And you can do
that in the application.properties, of course, or in the application.yml.
Encrypt and Decrypt REST Endpoints
Once you have your Config Server all set up for cryptography, you can
utilize some of the utility REST endpoints to encrypt and decrypt values
that you will put or take out of your configuration. Both of the endpoints
are really, really easy to use.
➢ Endpoint
○ POST /encrypt
➢ Example
○ Request: /encrypt
○ Data: <value_to_encrypt>
The first one is the encrypt endpoint, and you'd use this to generate the
encrypted values that you'll use in your configuration. And it's really simple
to use, like I said. All you do is send a POST request to the /encrypt
endpoint, and you pass the value that you want to encrypt as the body of the
request. The Config Server will use its configured key, either the symmetric
or the asymmetric key, to encrypt that value. And again, I can't stress this
enough, make sure that this endpoint is secure using Spring Security or any
other means to ensure that this endpoint is only accessed by authorized
users.
○ POST /decrypt
➢ Example
○ Request: /decrypt
○ Data: <value_to_decrypt>
➢ The explosion of configuration in the cloud and the need for a config
server
We've reached the end of this module, so let's take a moment to review what
we've learned. We've covered a lot of topics. We first talked about the need
for a Configuration Server in a cloud environment to manage the explosion
of configuration that comes with managing a distributed system. Then, we
saw how to configure the Spring Cloud Config Server to serve our
configuration files and how to use the Spring Cloud Config Client along
with the bootstrap.properties or the bootstrap.yml to retrieve the
configuration during application initialization. After that, we saw how we
could brag to our colleagues about updating our configuration at runtime
without ever needing to restart our application server. We also saw what
gets automatically refreshed and what requires an @RefreshScope
annotation. And last, we finished out the module with a section on how to
utilize the encryption and decryption support within Spring Cloud Config.
Module 5: Mapping Services Using Intelligent
Routing
In this module we'll learn how to map our services in the cloud using
Intelligent Routing.
➢ Netflix Zuul
○ Proxy server
○ Setting up routes
○ Setting up filters
We'll begin by talking about what Intelligent Routing is and what problem
it solves for us. Then we'll introduce Netflix Zuul as part of the Spring
Cloud Netflix project. And we'll see how to set up a proxy server, how to
configure different routes, and how to set up filtering. Remember that in
order to be as dynamic and as scalable as possible, a cloud-native system is
made up of individually deployable services, which together, as a whole,
form an overall system. With that comes some challenges though.
➢ Different addresses
➢ Request Enhancement
➢ Load Balancing
A gateway service not only provides dynamic routing and delivery, which
means at runtime it can decide where it should route a request and if it
should even route a request at all, but it also provides an array of other
functionalities. One of those pieces of functionality is security. It provides
the ability to authenticate all of the incoming requests, as well as filter out
any sort of illegitimate or bad request. And it's actually a really good
candidate for providing auditing and logging of requests since all of the
requests must enter through the gateway. It also provides something called
request enhancement, which is just a fancy way of saying that it can add
additional information to the request, or enrich the request, if you will. A
concrete example of this is the way Netflix uses their gateway. They use their
gateway service to do a geolocation lookup for all incoming requests, and
they add that additional information as an additional request header so that
it's available for all downstream services. The gateway service can also act
as a load balancer for the individual services that are behind it. Another
interesting feature of the gateway service is the ability to provide different
APIs for different clients. APIs are not a one-size-fits-all kind of thing, and
different clients, such as web clients or mobile clients, have different needs
in the way they call your APIs. So, for instance, it may be okay for a web
client to interact with several endpoints to accomplish some piece of
functionality, but the same may not be true for a mobile client, which may
be better served with just a single endpoint.
Using Netflix Zuul with Spring Cloud
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
And still within the pom.xml, define a new dependency in the dependency
section on spring-cloud-starter-zuul.
@SpringBootApplication
@EnableZuulProxy
public class Application{
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
If you're using service discovery, define the usual parameters for the name
of the application and the location of the Service Discovery Server.
Using Spring Cloud & Netflix Zuul Without Service Discovery
application.properties :
spring.application.name=gateway-service
ribbon.eureka.enabled=false
OR
application.yml :
spring:application:name: gateway-service
ribbon:eureka:enabled: false
If you're not using service discovery, you still define the application name,
and you add an additional parameter, ribbon.eureka.enabled=false.
Remember how I mentioned that the gateway service can serve as a load
balancer for your services? Well, the name of the client-side load balancer
project from Netflix is called Ribbon. And we'll get into more details about
what Ribbon is in the module on client-side load balancing.
Configuring Routes in Netflix Zuul
Once you have everything configured, the next step is to define how Zuul
should route requests. The default routing behavior when Zuul is set up
using service discovery is to route requests by service name.
So for example, if you requested /foo, Zuul would use service discovery to
find the service with a name of foo and send that request to that service.
Here's another example. If you were to request /categories/1, Zuul would
locate the service with the name categories, and it would send the /1 request
to that service. By default, the prefix is stripped from the request, so the
service actually only gets the /1 part of the request. If you wanted it to send
the full request, the /categories/1, you could set the property zuul.stripPrefix
to false, and the service would get the request /categories/1. Also note that
all services are added by default, so you'll want to use the
zuul.ignoredServices, and you could set that to a pattern to ignore specific
services.
In addition to the default configuration, you can also define more precise
configuration for specific services. First you define the path with the
zuul.routes.route_name.path property where the route name can be
anything you want. And the path is defined as a path that's using the Ant-
style matchers. Then you define the same property prefix, but end it with
service_id instead of path. And the service ID is the service identifier that
Zuul will look for when discovering the service via service discovery. You'll
also need to set the zuul.ignored-services property so that Zuul doesn't try
to automatically add a route for that service ID.
Go ahead and expand the gateway-service project, and locate the main
application class. In the main application class, we're going to add two
annotations. The first one is the @EnableZuulProxy annotation, and the
second one is the @EnableDiscoveryClient annotation.
package io.ajay.kumar.gatewayservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
}
application.properties
spring.application.name=gateway-service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
application.properties
spring.application.name=hello
server.port=1111
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
And, as you can see, it returned the Hello! string. And just to confirm, we'll
also hit the goodbye endpoint.
And, as expected, it returned the Goodbye! string. Now these examples are
very simplistic, but a lot is going on in the background. When we visited
localhost:8080/goodbye, the gateway service looked up a service with the
name goodbye from the Service Discovery Server and then proxied the
traffic to that service and then proxied the response back to the browser.
Creating Filters with Netflix Zuul & Spring Cloud
One of the key features in Netflix Zuul is the ability to define filters. Filters
allow you to intercept and control the requests and the responses that pass
through the gateway. Zuul has support for several different types of filters.
Filter Types
The first one is the pre type filter, which is executed before the request is
routed. Next comes the route type filter, which allows you to direct the
request in any way you want. In fact, earlier when we defined the
@EnableZuulProxy annotation on the main application class, we were
telling Spring to set up some predefined route filters to proxy our request to
back-end services. After route comes the post filters, and, as the name
suggests, these filters are executed after the request is routed. And the last
one is the error filter type. The error filter type is responsible for handling
when any of the previous filter types, the pre, route, or posts, results in an
error. To define a Zuul filter, you simply extend and implement the
ZuulFilter class.
The first is the run method, and this is where the main logic of the filter
goes. Note that it returns an object, but the current implementation ignores
it so you can just simply return null from this method. The next method is
the shouldFilter method, and, as the name suggests, this method allows you
to return true or false indicating whether or not the filter should be ran. The
third method is the filterType method, and here you can define one of the
four predefined values, either pre, route, post, or error. The last method is
the filterOrder method, and this allows you to control the execution of your
Zuul filters.
In your @Configuration class, create a method that returns the Zuul filter
and annotate it with @Bean. Spring Cloud Netflix will pick up and add any
beans of type ZuulFilter to Zuul.
Demo: Creating and Using a ZuulFilter
In this demo, we'll learn how to implement a Zuul filter to add an additional
header to the incoming request so that it's available for downstream services
to consume. We'll start from where we left off in the previous demo. And
remember that we had four different services. We had the discovery-server
that was doing service discovery, we had the gateway-service, which was our
edge service, and then we had two application services, the goodbye-service
and the hello-service. Since we're building a filter, we'll start off in the
gateway-service. So go ahead and expand the gateway-service and create a
new package. So go ahead and New, Package, and we'll call this
io.ajay.kumar.filters. And within that new package create a new class, so
New, Class, and we'll call this AddRequestHeaderFilter. And remember that
we have to extend the ZuulFilter class, so the superclass for our class will be
ZuulFilter.
package io.ajay.kumar.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
public class AddRequestHeaderFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader("x-location", "USA");
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
}
For the first method, the shouldFilter method, let's go ahead and change
that from return false to return true. We're always going to apply this filter.
And let's skip over the run method for right now and go down to the
filterType. And this particular filter we're going to create a pre filter, so we
can say return pre. And then for the filterOrder, return 0 is okay, so we'll
just leave that the same. In order to get access to the request to add headers
to it, we need to get access to the RequestContext. So let's clear this TODO
and say RequestContext ctx = RequestContext.getCurrentContext. And
within the RequestContext there's the addZuulRequestHeader method. And
image that our use case is to add the location of the incoming request, so
we'll name this header x-location. And we're going to mock this; we'll say
that every request comes from USA. And then last, remember that the
return value is not actually used in the current implementation, so returning
null is going to be okay for our purposes. Let's close this file and open up
our main application class. And within our main application class, we'll
annotate this with @Configuration.
package io.ajay.kumar;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.schultz.dustin.filters.AddRequestHeaderFilter;
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
@Configuration
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
@Bean
public AddRequestHeaderFilter addRequestHeaderFilter() {
return new AddRequestHeaderFilter();
}
}
package io.ajay.kumar;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class HelloServiceApplication {
public static void main(String[] args) {
SpringApplication.run(HelloServiceApplication.class, args);
}
@RequestMapping
public String hello(@RequestHeader("x-location") String location) {
return "Hello from " + location + "!";
}
}
In our handler method, we're going to add a new parameter. We're going to
say String location. And we're going to annotate this with @RequestHeader,
and then we're going to give it the name of that request header that the
gateway-service is adding to the request, which is x-location. And then we'll
modify the string to say Hello from location, and go ahead and save that.
And that's all the changes that we need to make to our application for this
demo. So we can go ahead and close this and minimize the hello-service, and
we're ready to start things up. So we'll want to start the discovery-server
first. So expand the discovery-server, locate the main application class,
right-click, Run As, Spring Boot App. After that, we'll start the gateway-
service, so collapse that and open the gateway-service, highlight the main
application class, Run As, Spring Boot App. And then last, let's start up the
hello-service. So collapse the gateway-service, head over to the hello-service,
right-click, Run As, Spring Boot App. Next, start up your browser and visit
localhost:8080/hello.
And there we have it. We have the Hello and then we have the USA, which
came from the gateway-service, which added it as a request header, which
the hello-service then consumed and appended to it's Hello string to come
out with the string Hello from USA!
Summary
We've now completed the module, so let's take a quick moment to recap
what we've learned.
● Gateway service
● Netflix Zuul
○ @EnableZuulProxy
○ Configuring routes
○ Writing filters
First, we looked at the reasons why Intelligent Routing is needed in the first
place and why it's important. Then we looked at how routing is
implemented via an API Gateway, or a gateway service. And last, we saw
how we can use Netflix Zuul as a gateway service to route and filter our
requests and responses to and from back-end services.
Module 6: Calling Services Using Client-side
Load Balancing
Module Introduction
● Load balancing
○ Server-side
○ Client-side
● Netflix Ribbon
■ @Loadbalanced
■ @RibbonClient
We'll start off with what is load balancing, and what role does it play in a
cloud-native application? Then we'll talk about traditional server-side load
balancing, which you're probably already familiar with. Then we'll
introduce client-side load balancing, what it is and how it differs from
server-side load balancing. Next, we'll introduce Netflix Ribbon and how we
can utilize it with Spring Cloud to implement client-side load balancing.
We'll learn how to implement it with and without service discovery using
two new annotations: the @LoadBalanced annotation and the
@RibbonClient annotation. We'll finish off the module with a section on
how to customize Ribbon's configuration for things like different load
balancing algorithms or different ways to check the health of a service
before sending a request to it.
Client-side vs. Server-side Load Balancing
● Server-side
○ Centralized or distributed
● Client-side
○ Software based
○ No extra hops
○ Typically distributed
We've set the stage for client-side load balancing. Now let's talk about how
to actually implement it with Spring Cloud. As I mentioned in the
introduction to this module, we'll use a library called Netflix Ribbon to
implement client-side load balancing. And Netflix Ribbon is an Inter
Process Communication library that has built-in software load balancers.
Spring Cloud adds full integration with Netflix Ribbon to Spring's
RestTemplate class. And we'll go into detail about what this exactly means,
but in essence our RestTemplate will now understand how to balance
requests across multiple instances of a service. Spring Cloud also adds
features that make it really easy to declare different types of load balancing
algorithms and availability checks. Next, let's talk about how to use Spring
Cloud and Netflix Ribbon. And before we get started, just a quick note. In
each of these sections I'll explain all of the steps necessary to kind of get
going, and then at the end we'll walk through a demo where you can follow
along. Using Netflix Ribbon with Spring Cloud is extremely easy, just like
all of the other Spring Cloud projects.
And make sure that it's of type pom and has a scope of import. Still within
your pom.xml, in the dependency section, define a new dependency on
spring-cloud-starter-ribbon. Spring Cloud's Netflix Ribbon support adds
two new annotations.
The first one is the @LoadBalanced annotation, and this annotation is used
when you're creating a RestTemplate. And it's used to mark that that
RestTemplate should be a load balanced RestTemplate as opposed to just a
standard RestTemplate. The second annotation is the @RibbonClient
annotation, and this annotation is mainly for configuration purposes. You
would use it to configure a custom Ribbon client, as well as when you're not
using service discovery to set up an actual Ribbon client. Let's look at how
to use the @LoadBalanced annotation first.
Using the @LoadBalanced Annotation
It's really easy to get started and create a load balanced RestTemplate.
In this demo, we'll set up a Ribbon client that utilizes service discovery and
balances requests between multiple instances of a service. Since we'll be
using service discovery to locate the instances of the service, we'll need to
have a Service Discovery Server set up. If you've been following along in the
previous modules of the course, you should already have the discovery
server set up within your IDE. Once you have the discovery server set up,
we're ready to get started. Open up a browser and head over to
start.spring.io. In the Group section, change this to io.ajay.kumar. And then
for the name of the artifact, we're going to call this ribbon-time-service. In
the Dependencies section, add the Web dependency and the Eureka
Discovery dependency.
Once you have everything filled out, click the Generate Project button, and
it'll create and download a zip for you. Still on the SPRING INITIALIZR
page, change the artifact name from ribbon-time-service to ribbon-tie-app.
Then, in the Dependencies section, add a new dependency on Ribbon.
Again, once you have everything filled out, click the Generate Project
button, and that'll create and download a zip for you. Unzip both of those
zip files and head back to your IDE. I've imported the ribbon-time-service
and ribbon-time-app. Right-click on the empty area within the Package
Explorer, go to Import, choose Existing Maven Projects, click Next, click
Browse, browse to the location of the downloaded zip file, click Open, and
click Finish. We'll work on the ribbon-time-service first, so expand the
project and open up the main application class.
Within the main application class, add two annotations, the
@RestController annotation and the @EnableDiscoveryClient annotation.
package io.ajay.kumar.ribbontimeservice;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonTimeServiceApplication {
@Value("${server.port}")
private int port;
public static void main(String[] args) {
SpringApplication.run(RibbonTimeServiceApplication.class,
args);
}
@GetMapping
public String getTime() {
return "The current time is "+ new Date().toString()
+"(answered by service running on "+port+")";
}
}
Then we need to add one request handler method, so hit Return a few times
and do public String getTime. And we'll annotate that with a shortcut
version @GetMapping, which is just a request mapping that's a GET. Then
in the body of the method we'll return The current time is, and we'll
construct a new Date object and turn that into a string. For demonstration
purposes, we're going to want to know which instance is responding to the
request. And currently we have no way of telling. We wouldn't be able to tell
one from the other. So we're going to make one addition to the response. So
we're going to say + answered by service running on, and then we'll have a
port variable, and then we'll close that parenthesis. And since each of our
services has to be running on a different port, we can tell each one of them
apart. So let's make sure that we actually define that port variable. Come
up here and add a private int port, and then we'll annotate this with @Value
annotation. And we'll inject this with the server.port variable. And before
we fire this up, we need to configure a few properties within our
application.properties. So go ahead and close that and open up
src/main/resources and open up the application.properties file.
application.properties
spring.application.name=time-service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
In the application.properties, we're going to set two properties. The first one
is the spring.application.name property, and we're going to set that to time-
service. And the second property we'll set is to configure the location of the
Service Discovery Server, so eureka.client.service-url.defaultZone. And then
we'll set that to localhost:8761/eureka. We now have everything we need to
start the ribbon time-service, so we can go ahead and close this and
minimize this. And the first thing we'll want to do is start the discovery
server. So expand the discovery-server project and navigate to the main
application class. Right-click on the main application class, and go Run As,
Spring Boot App. Go ahead and minimize the discovery-server and expand
the ribbon-time-service, and navigate to the main application class, right-
click on it, and go to Run As, and then choose Run Configurations. Since
we're starting up two instances of the ribbon-time-service, we need to
configure two different run configurations. So if you come up here to the
New launch configuration button, click that, and it'll create a new run
configuration for you. We'll rename this to ribbon-time-service-1. And then
for the Project, we'll make sure that we choose the ribbon-time-service, and
for the Main type, hit Search and locate the RibbonTimeServiceApplication.
In the Override properties section, we'll set the server.port, and we'll set
that to 4444. And click Apply, and then we're going to duplicate this
configuration and change the server port from all 4s to all 5s. So come up
here to the Duplicate configuration button, click that, change the name from
ribbon-time-service-1 to -2, and change the port from all 4s to all 5s. Once
you've got everything configured, you can highlight the ribbon-time-service-
1 and click Run. And then for the second instance, right-click on the main
application class, go to Run As, locate Run Configurations, highlight the
ribbon-time-service-2, and click Run. Just to make sure everything's
running correctly, open up a browser and visit localhost:4444.
You should get a response from each of the services with the current time
and then the current time and answered by service and whatever the
respective port is. We can also quickly check that each of the services
registered itself with the Service Discovery Server. So open up a new tab
and visit localhost:8761.
If you scroll down under the heading Instances currently registered with
Eureka, you'll see the application TIME-SERVICE, and you'll see that
there are two instances. They're both UP. One of them is running on all 5s
and another is running on all 4s. And we're back within the IDE, and so far
all we've done is we've set up multiple instances of the ribbon-time-service
and had each of them register with the Service Discovery Server. We're now
ready to start developing the piece where we'll use Ribbon to load balance
between each of the instances of the ribbon-time-service. In the Package
Explorer under the ribbon-time-app, expand it and navigate to the main
application class. Go ahead and expand the main application class just to
give us a little bit more room. Just like we did with the ribbon-time-service,
we'll add two annotations: first, the @RestController annotation, and
second, the @EnableDiscoveryClient annotation. Next we'll inject a
RestTemplate, so private RestTemplate, and make sure you annotate it with
@Inject. And there's nothing right now that's providing this RestTemplate,
so we're going to create a method that will return that new RestTemplate as
a load balance RestTemplate. So if you come down here below the main, do
public RestTemplate. Then within the body of the method, you're going to
return new RestTemplate. And then annotate this with @Bean and
@LoadBalanced. Now we need one more method, and that's the
RequestMapping method, to actually handle the request for our ribbon-
time-app. So come up here and do public String getTime, and we'll annotate
this with the shortcut mapping again, so @GetMapping. And then within
the body of the method, we're going to use the RestTemplate to call our
time-service and return the result. So we'll say return
restTemplate.getForEntity. And then we'll say http://, and remember we use
a logical identifier here, so we'll say time-service. And we'll say that the
return type is a string and then make sure that we call getBody on the
response. We're now ready to start up our ribbon-time-app and give it a
run. So let's unmaximize this and right-click on the main application class,
go to Run As, and choose Spring Boot App. Next, fire up a browser and visit
localhost:8080.
And you can see the response from the service running with the port on all
4s. And if we refresh that, we'll see the one running on the port with all 5s.
And we can continue to refresh that, and we'll see that it alternates between
each of the instances in a round-robin fashion.
Using the @RibbonClient Annotation
@Configuration
@RibbonClient(name="someservice")
public class MyConfiguration{
...
}
application.properties
<ribbon_client_name>.ribbon.eureka.enabled=false
<ribbon_client_name>.ribbon.listOfServers=http://host:9000,
http://host:9001
OR
application.yml
<ribbon_client_name>:ribbon:eureka:enabled: false
listOfServers=http://host:9000, http://host:9001
* Replace <ribbon_client_name> with the name field value of
@RibbonClient
Then, in your application.properties or your application.yml, define two
new properties. And remember the name element that we set on the
@RibbonClient annotation. You'll prefix each of your properties with that
value. So the first property, the
ribbon_client_name.ribbon.eureka.enabled=false, tells Ribbon to disable
service discovery support. The next property, the
ribbon_client_name.ribbon.listOfServers, is a comma-separated list of
URLs that Ribbon should use to distribute the requests among. And in this
case, we've set the different URLs to two different addresses, one running
on port 9000 and the other running on port 9001.
restTemplate.getForEntity("http://someservice/",...)
Once you have everything configured, you can use the RestTemplate just
like you did with service discovery. Except this time instead of calling the
service name, you use the value of the name element that you set up in the
@RibbonClient annotation.
Demo: Load Balancing Using Ribbon Without Service
Discovery
In this demo, we'll learn how to utilize the Ribbon client without service
discovery. Now, we're going to build on the previous demo that we did
utilizing service discovery as each of the demos share a lot in common. The
first thing we'll do is expand the ribbon-time-service and open up the main
application class. In the main application class, remove the
@EnableDiscoveryClient annotation.
package io.ajay.kumar.ribbontimeservice;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class RibbonTimeServiceApplication {
@Value("${server.port}")
private int port;
public static void main(String[] args) {
SpringApplication.run(RibbonTimeServiceApplication.class,
args);
}
@GetMapping
public String getTime() {
return "The current time is "+ new Date().toString()
+"(answered by service running on "+port+")";
}
}
Go ahead and close that, and open up the src/main/resources
application.properties, and delete the property that sets the location of the
Service Discovery Server.
application.properties
spring.application.name=time-service
Once you've got the property deleted, go ahead and close that. And we're
now ready to start up the ribbon-time-service without service discovery. If
you come up to the green Play button and click the little drop-down caret,
you'll see that we have all of our run configurations that we've use in the
past. So we'll want to use each of these ribbon-time-services to start each of
the instances. So we'll start ribbon-time-service-1, and then we'll start
ribbon-time-service-2. Just to make sure that our ribbon-time-service
instances started up correctly, let's do a quick sanity check. So visit
localhost:4444, and it's running there. And then open up a New Tab and
visit localhost:5555, and it's running there. So everything with our ribbon-
time-service is good to go. Next, we'll minimize the ribbon-time-service
project and open up the ribbon-time-app. And once you've opened it,
navigate to the main application class.
@RestController
@RibbonClient(name="time-service")
@SpringBootApplication
public class RibbonTimeAppApplication {
@Inject
private RestTemplate restTemplate;
public static void main(String[] args) {
SpringApplication.run(RibbonTimeAppApplication.class, args);
}
@GetMapping
public String getTime() {
return restTemplate.getForEntity("http://time-service",
String.class).getBody();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
And just like we did with the ribbon-time-service, we'll want to delete this
@EnableDiscoveryClient. Now we're going to add a new annotation, the
@RibbonClient annotation, and we're going to set the name element to the
time-service string. And this is the same time-service that we used when we
were using service discovery, and we'll use it in pretty much the same
fashion. We'll refer to it in the RestTemplate URL as a logical service
identifier, and then configure what those particular instances are that
represent that service within configuration. Let's go ahead and close this
and open up our src/main/resources and our application.properties.
application.properties
time-service.ribbon.eureka.enabled=false
time-service.ribbon.listOfServers=http://host:4444, http://host:5555
And you can see that we got a response from the service running on the port
with all 4s. And if we refresh that, we'll see that we get a response from the
one on all 5s.
And we can continue to refresh that and see that it round-robins between
each of the instances of the time-service.
Customizing Your RibbonClient Configuration:
Introduction
In this last section, we'll learn how to customize an individual Ribbon client
using declarative configuration. And what this will allow us to do is define
custom configuration that applies to a specific Ribbon client instead of to all
Ribbon clients. And what that buys us is it allows us to define different
client-side load balancing behavior for different services.
@Configuration
@RibbonClient(
name="otherservice",
configuration=OtherServiceConfig.class)
public class MyConfiguration{
...
}
package io.ajay.config.kumar;
//Different package so it is not picked up by @ComponentScan
@Configuration
public class OtherServiceConfig {
}
@Configuration
public class OtherServiceConfig {
@Bean
public <bean_type> <method_name>(){...}
}
Default Ribbon Client @Beans
Replace <bean_type> and <method_name> with values to override:
http://cloud.spring.io/spring-cloud-
static/Camden.SR6/#_customizing_the_ribbon_client
Most likely to be customized: IRule & IPing
There are a number of different classes that are needed to set up a Ribbon
client, and by default Spring Cloud defines those as @Beans and then allows
you to override any of them for custom configuration. Let's take a look at
the Spring Cloud documentation to see what @Beans you can override.
I've loaded up the Spring Cloud documentation, and we're looking at a
number of different beans that are required to set up a Ribbon client. And
for the most part, you typically won't need to override any of these with the
exception of two of the beans. That's the IRule bean and the IPing bean.
The IRule bean controls the load balancing algorithm, and the IPing bean
controls the availability checks on the instances that are being load
balanced. Let's take a look at each of these beans in a bit more detail.
Customizing Your RibbonClient Configuration: The
IRule Bean
● IRuleImplementations
○ RoundRobinRule
○ ResponseTimeWeightedRule
○ RandomRule
○ ZoneAvoidanceRule
@Configuration
public class OtherServiceConfig{
@Bean
public IRule ribbonRule(){
return new RoundRobinRule();
}
}
If, for instance, we wanted to override the default IRule to use the
RoundRobinRule implementation, we'd define a new method annotated
with the @Bean annotation, and we'd return a new RoundRobinRule.
Customizing Your RibbonClient Configuration: The
IPing Bean
The IPing bean is responsible for choosing the strategy to check the
liveliness or the availability of a given instance that's being load balanced.
Just like the IRule bean, you can also implement your own custom IPing
implementations, or you can choose from one of the several different
defaults. Let's take a look at what's available.
● IPingImplementations
○ DummyPing
○ PingUrl
○ NIWSDiscoveryPing
So just like we did with the IRule implementation, you would define a new
method annotated with the @Bean annotation, and then you return the
implementation of your choice. So in this case, we're going to return a
PingUrl. So we create a new instance of the PingUrl, we set the expected
content to true, and then return that instance.
Demo: Customizing the RibbonClient Load Balancing
Strategy
In this demo, we'll learn how to customize our Ribbon client that we're
using to call the ribbon-time-service. And again, we're going to be building
on the previous demo. So make sure that you've completed the demo where
you set up a Ribbon client without using service discovery. The first thing
we'll do is we'll come up here to the green Play button and start each of the
instances of the ribbon-time-service. Both of the ribbon-time-service
instances are started. Now remember, in order to create custom
configuration for a Ribbon client, we need to create our own
@Configuration class. Let's open the ribbon-time-app project and create a
new package. So right-click, New, and come here to Package. And we're
actually going to rename this from io.ajay.kumar to io.ajay.config.kumar.
Remember that we want to have a different package name so that the
@Configuration class isn't subject to component scanning; otherwise, it
would apply to all of our Ribbon clients. In the new package, right-click and
go to New and choose Class. And we'll call this our RibbonTimeConfig
class. Go ahead and click Finish.
package io.ajay.config.kumar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
@Configuration
public class RibbonTimeConfig {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}
Then we're going to annotate this class, of course, with @Configuration.
And then for this demo, we're going to customize the load balancing
strategy. So we'll come down here and we'll define a new IRule. So we'll say
public IRule, and we'll call this ribbonRule. And then in the body of the
method, we'll say return new RandomRule. And what this will do is it'll
pick a random instance to send traffic to as opposed to what we were using
before where we were evenly balancing between each of the instances. And
last, make sure that we don't forget to add the @Bean annotation to our
method. We're done with our custom configuration, so we can go ahead and
close that. And then we can open up the main RibbonTimeApplication.
@RestController
@RibbonClient(name = "time-service",
configuration=RibbonTimeConfig.class)
@SpringBootApplication
public class RibbonTimeAppApplication {
@Inject
private RestTemplate restTemplate;
public static void main(String[] args) {
SpringApplication.run(RibbonTimeAppApplication.class, args);
}
@GetMapping
public String getTime() {
return restTemplate.getForEntity("http://time-service",
String.class).getBody();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
And on the RibbonClient annotation, add a new configuration element, and
set it to the custom configuration class that we set up, the
RibbonTimeConfig class. We're now ready to start up our application and
try out our custom configuration. So go ahead and close this and right-click
on the main application class, go to Run As, and choose Spring Boot App.
Go ahead and fire up a browser and visit localhost:8080.
And, as you can see, we got a response from the instance running with the
port that's all 5s. And if we refresh that, we'll see that we get an instance
with all 4s.
But if we continue to refresh it, we'll see that it doesn't evenly balance
between each of the instances like it did before and instead picks them at a
random interval.
Summary
We're at the end of this module, so let's quickly go over what we learned.
● Netflix Ribbon
We first talked about what is load balancing and what are the differences
between client-side load balancing and server-side load balancing. Then we
introduced the Netflix Ribbon project and learned how to use the
@LoadBalanced and the @RibbonClient annotations to set up client-side
load balancing with and without service discovery. In the last section, we
saw how we could use the @RibbonClient annotation with a separate
@Configuration class to set up custom configuration for a specific Ribbon
client.
Module 7: Creating Self-healing Services with
Circuit Breaker
Introduction
Hardware can fail, networks can fail, and software can definitely fail. And a
distributed system is no different in that sense, but the likelihood for failure
is just simply much greater. You have more hardware, you have more
network, and you have more software. And with these increased numbers
comes that increased probability for failure. Adding to that chance of failure
is the way in which processes communicate in a distributed system. Process
communication that was once within a process is now done across a
network. And even as resilient as our networks are today, there's still a
much more likely chance of a communication failure across a network
versus within a process. A particularly bad side effect of failures in a
distributed system is something called a cascading failure. A cascading
failure is a failure in a system in which a failure in one system can cascade,
almost like dominos, to other parts of the system causing them to fail as
well. To make this a bit more concrete, imagine that you have three services,
Service A, Service B, and Service C. Service A calls Service B, and Service B
calls Service C. And imagine that Service C runs out of memory and is very
slow to serve requests to Service B. And even though Service B may have a
timeout set up for calling Service C, if enough requests stack up against
Service B, which require the use of Service C at a fast enough rate,
resources at Service B will be entirely consumed before that timeout is ever
reached, which in turn will cause Service B to fail. This same problem can
happen all the way up the chain.
➢ Multiple issues at play
○ Fault tolerance problem
○ Resource overloading problem
There are multiple issues at play here. First, we have a fault tolerance
problem. Calling services are unaware that the service that they're calling is
likely to fail and yet they still attempt to call the service. And second, we
have a resource overloading problem. Calling services are allowed to invoke
dependent services with pretty much unconstrained resources.
Embracing Failure with the Circuit Breaker Pattern
Spring Cloud implements fault tolerance with the help of a library from
Netflix called Netflix Hystrix. Hystrix is a latency and fault-tolerance
library, and it was designed to stop cascading failures and enable resiliency
in distributed systems.
Using Spring Cloud and Netflix Hystrix is extremely easy. And before we
dive in, just a quick note. I'll explain all the steps necessary to get going and
then walk you through a demo at the end where you can kind of follow
along. I'll follow that pattern throughout the remainder of this module, so
just keep that in mind.
pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Just like you use all of the other Spring Cloud projects, you start by
including a dependency in the dependencyManagement section of your
pom.xml on spring-cloud-dependencies. And, as always, make sure that it's
of type pom and has a scope of import.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
Then, still within your pom.xml, within the dependency section, define a
new dependency on spring-cloud-starter-hystrix. And if you'd like to be able
to consume metrics for your Hystrix calls, you'll also need to include a
dependency on the spring-boot-actuator.
@SpringBootApplication
@EnableCircuitBreaker
public class Application{
public static void main(String[]args){
SpringApplication.run(Application.class,args);
}
}
In your main Application class, you define a new annotation, and that's the
@EnableCircuitBreaker annotation.
@Service
public class Service{
@HystrixCommand(fallbackMethod="somethingElse")
public void doSomething(){
…
}
public void somethingElse(){
…
}
}
And then in either your @Component or your @Service class, locate the
method that you want to wrap with Hystrix, in our case this will be a
method called doSomething, and annotate that method with the
@HystrixCommand. Then, on the @HystrixCommand annotation, define a
new attribute called fallbackMethod, and set that to the name of the method
that you want to fall back to in the event of a failure. In terms of code, that's
all there is to it.
But there's a gotcha that you have to look out for, and that's around the
Hystrix timeout. And you need to make sure that your Hystrix timeouts
encompass the caller timeouts plus any of the retries and then a little bit of a
buffer. And the default timeout is set to 1000ms or a second, and if you need
to change it, you can use this big, long property to set the timeout in
milliseconds.
Demo: Implementing Fault Tolerance with Netflix
Hystrix
In this demo, we'll build a simple weather service that returns a random
weather condition, and then we'll use that in another app called the weather
app that consumes that service. And in the weather app we'll protect our
call to the weather service with Hystrix and implement a fallback for when
the weather service is down or not responding. We'll be using service
discovery to locate the weather service from our weather app, so make sure
that you have the Service Discovery Server set up within your IDE. And if
you've been following along throughout the course, you should already have
this set up. Go ahead and open up a browser and head over to
start.spring.io. In the Group section, change the group to io.ajay.kumar, and
then for the name of the artifact, name it weather-service. In the
Dependencies section, add the Web dependency and the Eureka Discovery
dependency.
Once you have everything filled out, go ahead and click the Generate
Project button, and it'll create and download a zip file for you. We're also
going to use this page to generate the weather app project, so still on this
page change the name of the artifact from weather-service to weather-app.
Then in the Dependencies section, add the Hystrix dependency and the
Actuator dependency.
Again, once you have everything filled out, go ahead and click the Generate
Project button, and it'll create and download that zip file for you. Go ahead
and unzip both of those zip files and head back to your IDE. Back within
your IDE, right-click on the empty space and choose Import, choose
Existing Maven Projects, click Next, browse to the location of your
downloaded zip file, and click Finish. Once you have the weather service
imported, repeat the same process for the weather app. I have both projects
imported into the IDE.
We'll start by developing the weather service first, so go ahead and expand
the weather-service and navigate to the main application class.
package io.ajay.kumar.weatherservice;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableDiscoveryClient
public class WeatherServiceApplication {
private String[] weather = new String[] { "sunny", "cloudy", "rainy",
"windy" };
public static void main(String[] args) {
SpringApplication.run(WeatherServiceApplication.class, args);
}
@GetMapping("/weather")
public String getWeather() {
int rand = ThreadLocalRandom.current().nextInt(0, 4);
return weather[rand];
}
}
application.properties
server.port=9000
spring.application.name=weather-service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
And just make sure that you get a response here. You can refresh it a few
times to see that you get various responses. And now that we have our
weather service working, let's head back to the IDE and utilize it within our
weather app.
Back within your IDE, expand the weather-app and navigate to the main
application class. Within the main application class, we're going to define
three new annotations.
package io.ajay.kumar.weatherapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import
org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableCircuitBreaker
@EnableDiscoveryClient
@RestController
public class WeatherAppApplication {
public static void main(String[] args) {
SpringApplication.run(WeatherAppApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
The first one is the @EnableCircuitBreaker annotation, the next one is the
@EnableDiscoveryClient annotation, and the last one is the
@RestController annotation. Next, we'll define a RestTemplate so that we
can call our weather service, so come down here and say public
RestTempate and call it restTemplate. Annotate it with the @Bean
annotation and the @LoadBalanced annotation. And then, in the body of
the method, return a new RestTemplate. Now remember a Hystrix
command can only be defined in an @Component or an @Service class, so
let's go up here and create a new class. So right-click, go to New, choose
Class, and we'll call this the WeatherService.
package io.ajay.kumar.weatherapp;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class WeatherService {
@Inject
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "unknown")
public String getWeather() {
return restTemplate.getForEntity("http://weather-
service/weather",
String.class)
.getBody();
}
public String unknown() {
return "unknown";
}
}
At the top of the class, go ahead and annotate it with @Service. And then
next we'll need our RestTemplate, so private RestTemplate restTemplate.
And make sure that we @Inject that. And next, we're going to need a
method to call our weather service, so come below this and type public
String getWeather. Then, in the body of the method, we'll return
restTemplate.getForEntity. And the URL we'll use is http://weather-
service/weather. And then the response type will be a string, so we'll say
String.class. And then we'll make sure we call a .getBody to get the response
body. This is the method we'll want to annotate with our Hystrix command.
So come up to the top of the method and type @HystrixCommand, and
we'll use a fallbackMethod called unknown. And then we'll want to make
sure that we define that method, so come below the getWeather method and
define a new method, public String unknown. And this will be a real simple
method. It will just return the string unknown. And just to be crystal clear,
we're going to use this WeatherService in our main application class to get
the actual weather. And if there's a problem getting the weather, or the
WeatherService is down, it'll call that fallbackMethod and just return
unknown for the weather. Let's head back to the main application class and
use the WeatherService.
@SpringBootApplication
@EnableCircuitBreaker
@EnableDiscoveryClient
@RestController
public class WeatherAppApplication {
@Inject
private WeatherService weatherService;
public static void main(String[] args) {
SpringApplication.run(WeatherAppApplication.class, args);
}
@GetMapping("/current/weather")
public String getWeather() {
return "The current weather is " + weatherService.getWeather();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
So back within the main application class, define a new instance variable,
private WeatherService, and we'll call that weatherService. And then don't
forget to add the @Inject annotation. Then we'll add a new method called
getWeather. So come below the main method here and do public String
getWeather. And then we're going to return a string here that says the
current weather is, and then we'll call the weatherService and get the
weather. And this is our handler method, so we'll need the @GetMapping.
So define an @GetMapping on the top of this method, and then we'll say the
URL is /current/weather. And that's all we need for the code portion of our
weather app, so go ahead and close both of these files and navigate to
src/main/resources and open up that application.properties.
application.properties
server.port=8000
spring.application.name=weather-app
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
The properties for the weather app are very similar to the ones in the
weather service, so I've just copied and pasted those over here. And then
we're just going to change some of those values. So for the server.port, we'll
change that to 8000, and for the spring.application.name, we'll change that
to weather-app. We're ready to start our weather app, so go ahead and close
that file and open up the Console. Just go ahead and double-check that your
discovery server and your weather service are still running, and then come
over to the main WeatherAppApplication class, right-click on it, go to Run
As, and choose Spring Boot App. Next, open up a web browser and visit
localhost:8000/current/weather.
And you should get a response here. And you can refresh it a couple times to
see the different values. And next, what we're going to do is we're going to
shut down the weather service. And we'll see that Hystrix takes over and
recognizes that the weather service is down and returns our fallback
method. So we should be able to see the current weather is unknown after
we shut down the weather service. So we're back within the IDE, and we
have the weather service pulled up in the Console. We'll go ahead and stop
the weather service and head back to the browser. Back within the browser,
we'll go ahead and refresh the page, and we can see instantly that the
current weather is unknown.
Hystrix has realized the call to the weather service is failing and it should
use its fallback method.
Metrics and Insight with the Hystrix Dashboard
Netflix Hystrix tracks the execution status of protected calls so that it knows
when to trip the circuit breaker. And one of the advantages of this is that we
can use those metrics to get insight into how our calls are functioning.
Reading those metrics in their raw form wouldn't be very easy or efficient.
And that's where the Hystrix Dashboard comes in. And the dashboard is a
web application that helps you visualize all of those metrics in a quick and
easy-to-use fashion. The dashboard is jam-packed with information.
➢ Tracksmetrics such as
○ Circuit state
○ Error rate
○ Traffic volume
○ Successful requests
○ Rejected requests
○ Timeouts
○ Latency percentiles
➢ Monitor protected calls
○ Single server or cluster
It tracks and displays information about the state of the circuit, whether it's
open or closed, the error rate for the call, the traffic volume that it's
receiving, how many requests were successful, rejected, or timed out, and
the latency percentiles for the call. And you can also use it to track a single
server or a cluster of servers. To use it, it's literally as easy as declaring a
couple dependencies in your pom.xml and adding a single annotation.
Using Spring Cloud and the Netflix Hystrix
Dashboard
In order to use the Hystrix Dashboard, you first, like always, declare a new
dependency within the dependencyManagement section of your pom.xml on
spring-cloud-dependencies.
pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement
And, as always again, make sure that it's of type pom and has a scope of
import.
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency
Then, still within your pom.xml, in the dependency section, define a new
dependency on spring-cloud-starter-hystrix-dashboard. And finally, you can
probably guess it, add the @EnableHystrixDashboard annotation.
@SpringBootApplication
@EnableHystrixDashboard
public class Application{
public static void main(String[]args){
SpringApplication.run(Application.class,args);
}
}
And that's all you need to get started with the Hystrix Dashboard.
Reading and Understanding the Hystrix Dashboard
Before we get started with the demo of setting up the Hystrix Dashboard,
it'll be important to understand how to read the dashboard as it contains a
lot of information in a very small amount of space.
We'll focus on the top half under the Circuit heading as the bottom half, the
Thread Pools, is pretty self-explanatory. The dashboard is quite literally
jam-packed with information. And it can be a little daunting when you see it
at first, so let's break it down by parts. At the top right you have the name
of the Hystrix call that is being protected, and in this picture it's the
getCurrentWeather call.
On the left side, you have a circle that represents both the request volume,
as well as the health of the call. And the larger the circle gets, the more the
request volume is, and the more red the circle becomes, the more unhealthy
the call is. Then on top of the circle, the line is a depiction of the request rate
over the last 2 minutes. Back on the right side, beneath the name of the
protected call in gray, is the error rate of the call. And below that you have
the number that indicates the request per second at the host level, as well as
at the cluster level. Continuing downward, still on the right side, you have
the state of the circuit for this particular call and whether it's open and
rejecting traffic or whether it's closed and accepting traffic. And then right
below that you have the latency percentiles for this call. Moving back to the
top, underneath the name of the protected call are a bunch of numbers in
columns with different colors. Let's zoom in on this particular area so we
can explain each of these individually. For the numbers, we'll move column
by column and row by row. And, just in case you forget, there's a legend at
the top of the Hystrix Dashboard that looks just like this.
Starting at the first column in the first row, the dark green number
represents the number of successful requests. And right below that, the blue
number represents the number of short-circuited requests. And these are
the requests that didn't even attempt to execute because the circuit was
open. Still in the first column, on the bottom row, the light green number
represents the number of bad requests. And these requests are errors, but
they're not necessarily due to an execution failure. They're due to something
like an illegal argument. Moving on to the second column, the first row, the
orange number represents the number of timed out requests. And these are
requests where the execution was attempted, but a response was not
received in the allotted amount of time. Right below the timed out request,
the purple number represents the number of rejected requests. And rejected
requests happen when there are no more resources to serve a request, either
via the thread pool or a semaphore. And remember that the request is
rejected so that requests don't stack up at the caller and consume those
valuable resources. And the last number in the second column, the red one,
is the number of failed requests. And these are requests that failed to
execute because they threw an exception. Now that we've got a firm
understanding of how to read the Hystrix Dashboard, let's set up and enable
our own so that we can visualize some of the metrics that are being admitted
from the call that we protected with Hystrix in our previous demo.
Demo: Monitoring Fault Tolerance Metrics with
Hystrix Dashboard
In this demo, we'll utilize the Hystrix Dashboard to view some of the metrics
of the Hystrix-protected call from the weather app to the weather service.
First things first, if your services and your discovery server aren't already
running, you'll need to start each of them up. And we'll start with the
discovery server first. So expand the discovery-server project and navigate
to the main application class. Right-click on it, go to Run As, and choose
Spring Boot App. Repeat that same process for the weather service and then
the weather app. The weather service and the weather app and the
discovery server are all running. And just to double-check, you can click
little caret and see that each of those services is started. Next, go ahead and
open up a web browser so we can create and download the Hystrix
Dashboard project. In your web browser, visit start.spring.io. In the Group
section, use io.ajay.kumar, and then for the artifact name we'll say hystrix-
dashboard. And then for the dependencies, we'll type Hystrix Dashboard.
Once you have everything filled out, go ahead and click the Generate
Project button, and that'll create and download that zip file for you. Like
always, extract that zip file and head back to your IDE. Back within your
IDE, right-click on the empty space in the Package Explorer, go to Import,
choose Existing Maven Projects, click Next, click Browse, locate the
downloaded zip file, and click Open, and click Finish.
Expand the Hystrix Dashboard project and navigate to the main application
class.
package io.ajay.kumar.hystrixdashboard;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import
org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
Within the main application class, we will quite literally add one annotation,
and that's the @EnableHystrixDashboard annotation. Next, we're ready to
start things up and view the dashboard. So on the main application class,
right-click on it, go to Run As, choose Spring Boot App. Next, open up a
browser and visit localhost:8080/hystrix.
This should load up the Hystrix Dashboard. And before we view any
metrics, we'll need to actually generate some metrics. So open up a new tab
and visit your weather app at localhost:8000/current/weather.
And just go ahead and refresh that a few times to get some metrics
generated. Back at the Hystrix Dashboard, we'll put in the URL to the
Hystrix stream of our weather app. So that's
http://localhost:8000/hystrix.stream. And for the title of our dashboard,
we'll say Weather App. Next, click the Monitor Stream button, and you
should be presented with some metrics.
If you don't see metrics right away, you can flip back and forth between
your weather app and refreshing it to generate some metrics and back to the
dashboard to see the effect of those requests.
Aggregating Hystrix Metrics with Netflix Turbine
In the screenshot, you can see there are two protected calls, the
getCurrentWeather call and the getCurrentDatetime call. And the protected
call on the left is from a service located on localhost:8080, and the one on
the right is from another service located on localhost:8181. And Turbine has
brought both of these metrics together from different services, all viewable
in the same dashboard.
Using Spring Cloud and Netflix Turbine
pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency
Then, still within your pom.xml, in the dependency section, define a new
dependency on spring-cloud-starter-turbine. Follow that up by adding a
new annotation to your main Application class, the @EnableTurbine
annotation.
@SpringBootApplication
@EnableTurbine
public class Application{
public static void main(String[]args){
SpringApplication.run(Application.class,args);
}
}
application.properties
turbine.app-config=<list_of_service_ids>
turbine.cluster-name-expression=’default’
OR
application.ymlturbine:appConfig:<list_of_service_ids>
clusterNameExpression:”’default’”
* In addition to the standard spring application name and discovery server
location properties
The first property is the turbine.app-config property, and you set this to a
comma-separated list of service IDs. And these are the same service IDs that
you use for service discovery. The second property, the turbine.cluster-
name-expression property, can be a Spring Expression Language value to
name your cluster. And in Turbine, a cluster is just a grouping of services
that need to be monitored together. And for our purposes, to make things
easy, we're going to set that value to default surrounded by single quotes.
And a note here is if you're using YAML instead of properties, you'll need to
escape the single quotes with double quotes.
In this demo, we'll utilize Turbine to combine the Hystrix streams from two
different services so that they're both viewable from the same Hystrix
Dashboard. We'll continue to utilize the weather app, the weather service,
and the discovery server from our previous demo, and then we'll add two
new projects that you can clone from GitHub. The first new project is the
datetime-service, and you can get that from github.com/ajaycucek/hystrix-
datetime-service. And the second new project is the datetime-app. And you
can get that, again, at github.com/ajaycucek/hystrix-datetime-app. Once
you've cloned both of those projects, make sure that you import them into
your IDE. I've imported mine, and your IDE should look pretty similar to
this. The two new projects, the datetime-app and the datetime-service, are
literally identical to the weather app and the weather service, with the
exception that they return the current datetime instead of the weather. So
feel free to browse around and check out the code for those. We'll need to
start each of these applications starting with the discovery server first.
Again, I have all of the applications started. And once they're all started up,
you can verify by clicking little caret and making sure that all of the
applications show up. With all of the applications started, we'll go ahead
and start building our Netflix Turbine app. Open up a browser and visit
start.spring.io. In the Group use io.ajay.kumar, and for the artifact name
you can just call it turbine. And for the dependencies, we'll obviously want
to add Turbine.
And once you have everything filled out, click that Generate Project button,
and that'll create and download that zip file for you. And, like always, unzip
that zip file and import that into your IDE. We're back within the IDE
where we've imported the Turbine project.
Go ahead and expand the project and navigate to the main application class.
package io.ajay.kumar.turbine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.turbine.EnableTurbine;
@SpringBootApplication
@EnableTurbine
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
In the main application class, we'll add one new annotation, and that's the
@EnableTurbine annotation. Next, we'll need to configure some properties,
so go ahead and close that file and navigate to src/main/resources,
application.properties.
application.properties
server.port=3000
spring.application.name=turbine-aggregator
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
turbine.app-config=weather-app,datetime-app
turbine.cluster-name-expression='default'
And just refresh that a few times to generate some metrics. And then we're
going to do the same thing for the weather service, so go to
localhost:8000/current/weather, and then also refresh that a few times to
generate some metrics.
And if you don't, just go back to those services and refresh them a few times
to generate some metrics.
Summary
We're at the end of this module so, like always, let's do a recap of what we
learned.
Introduction
In this last module we'll take a holistic approach to see how each of the
individual ideas and technologies all fit together and where you can go next
after you've completed the book.
We'll begin with a section on how it all fits together. We've learned a lot
throughout the book covering new ideas and technologies in every single
module. And while many of those ideas are useful by themselves, the real
advantage comes from using them together as a whole. And we'll see how
each of the main topics, service discovery, distributed configuration, client-
side load balancing, gateway and routing, and fault tolerance and circuit
breaker, all fit together to form a cloud-native system. And then, like I
mentioned, we'll finish by briefly discussing where you can go next to learn
about what additional Spring Cloud projects are out there. And don't
discount the importance of this. It's helpful to know exactly what's out
there. It's almost like browsing the tool section at your local hardware store,
and if you don't know what tools are out there, when you have a problem,
you might end up using the wrong tool for the wrong job.
How Does It All Fit Together?
The question is how does it all fit together to form a cloud-native system?
And, to start, let's recap everything that's involved. It all starts with one or
more application services.
Now that we've recapped all of the pieces involved, let's take a moment to
look at how they all interact with each other, specifically on startup.
It all starts with the Service Discovery Server. Each and every instance of
every piece of the system registers itself with the Service Discovery Server
upon startup. And then it also receives a reply with the location of other
registered services. For application services, this reply is very important
because it tells them where the location of the Configuration Server is at.
And once they know the location of the Configuration Server, they can
make a request to retrieve any configuration that will ultimately be used to
bootstrap their startup process.
Putting It All Together: On Request
Now that we have all of our application and their supporting services
started, let's look at how they all interact with each other during a request.
The request for a given path begins at the gateway server, Netflix Zuul in
our case. And Zuul will match the path to a given service ID, and then it will
use service discovery, either by requesting it from the Service Discovery
Server or via a previously cached result, to locate the service that will
handle that path. Once the service is located, it'll make a Hystrix-protected
call to the service using Ribbon to handle the client-side load balancing of
which instances it should send the traffic to. In our example, the request was
sent to Service A. Now, suppose that Service A also depends on Service B to
fulfill the request. Upon receiving the request, Service A will utilize service
discovery to locate Service B. And it'll do that either via a previously cached
result or by requesting it from the Service Discovery Server. Next, it'll make
a Hystrix-protected call using Ribbon for client-side load balancing to one
of the instances of Service B. Service B will respond to Service A, and then
Service A will respond to the gateway, and finally, the gateway will respond
to the original request.
Where to Go Next
Throughout the book, we've covered the core fundamentals of Spring Cloud
and given you a good foundation to build upon.
➢ Spring Cloud
○ Spring Cloud Bus
○ Spring Cloud Task
○ Spring Cloud Cluster
○ Spring Cloud Consul
○ Spring Cloud Sleuth
○ Spring Cloud Security
○ Spring Cloud Stream
○ More...
There are a whole bunch of other projects like Spring Cloud Bus, Spring
Cloud Cluster, and Spring Cloud Stream that all help you solve problems in
the cloud. To get the best idea of what's available with Spring Cloud, visit
the documentation page at http://projects.spring.io/spring-cloud, and then
scroll down to the section with the heading Main Projects. Here you'll find
all of the Spring Cloud projects that sit under the Spring Cloud umbrella.
And I encourage you to check them out, if only so that you're familiar with
them and you know that they exist because, like I mentioned before, it can
be really helpful to know what tools exist so that you use the right tool for
the right job.