Service talking to another Service
There is not a lot of fun in having only a single micro-service. Let’s see discover how the micro-services can work together to achieve a task.
Consider using micro-service patterns when the operational costs are manageable. The size of the micro-service should not be smaller than needed to avoid creation of unreasonable operational overhead. |
This kata is showing ways of connecting micro-services and not the best practices of how to design them. You have to apply system analysis and system architectural patterns to identify the best ways to split functionality into multiple services. |
In this Kata we’re going to let Accounts service connect and use data provided by Profiles service.
Getting Started
The starting source code is located in kata6/start folder |
We have to services: Accounts service and Profiles service. The accounts service is managing list of user accounts in our system and profiles service is in charge of managing user social profiles.
In this kata we’re going to teach Accounts service to extract some personal information of a user from Profiles service when it is asked about user account.
Eureka
Eureka: Service Catalog
In distributed systems there should be a way for one service to connect to another. This can be facilitated through a couple of different ways:
-
Configuration of an IP address
-
Using a DNS record
-
Using s Service Catalog/Registry
Going forward we’re going to use Eureka from Netflix as our Service Catalog.
Spring Cloud provides a very simple way to build your own Eureka-based service catalog by embedding eureka in one of your services.
Let’s build one of our own. Create an empty Spring Boot application.
Add Eureka starter package into your class pass by adding the following compile dependency into your build:
compile("org.springframework.cloud:spring-cloud-starter-eureka-server:$springCloudVersion")
Annotate application class with @EnableEurekaServer
attribute:
@EnableEurekaServer
public class EurekaServiceApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(EurekaServiceApplication.class, args);
}
}
Build and launch application. Ensure that you have Configuration Service running prior the launch (see Externalizing Configuration kata).
Open your browser and navigate to http://localhost:8761/. You’re going to see Eureka dashboard:
Registering with Service Catalog
From now on each instance of a service should register with Eureka on start and continue keeping it record up-to-date throughout it’s lifecycle.
Add the Eureka starter package as compile dependency for a service:
compile("org.springframework.cloud:spring-cloud-starter-eureka:$springCloudVersion")
Annotate application class with @EnableEurekaClient
attribute:
@SpringBootApplication
@EnableEurekaClient
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class ProfilesServiceApplication {
What’s left is to tell the Eureka client where to find the Eureka server.
We need to add the configuration block below to all our Web services by
putting it into a shared configuration in application.yml
shipped by
Configuration service from GitHub:
eureka:
client:
registryFetchIntervalSeconds: 5
instanceInfoReplicationIntervalSeconds: 5
initialInstanceInfoReplicationIntervalSeconds: 5
serviceUrl:
defaultZone: ${service-registry.uri:http://127.0.0.1:8761}/eureka/
instance:
leaseRenewalIntervalInSeconds: 3
prefer-ip-address: true
ip-address: ${external.ip:${spring.cloud.client.ipAddress:localhost}}
metadataMap:
instanceId: ${spring.application.instance_id:${random.value}}
Build and start your service. Navigate to Eureka dashboard and you’re going to see your service registered with Eureka:
Repeat the same process for all of your Web Services from now on. |
Service Discovery via Discovery Client
Remember to enable Eureka client for Accounts service. |
Now we’re going to let the Accounts Service locate the Profiles Service via Discovery client:
@Component
class ProfilesClient {
private final DiscoveryClient discoveryClient;
@Autowired
public ProfilesClient(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
public URI getProfileUri(Account account) {
InstanceInfo instance = discoveryClient.getNextServerFromEureka(
"profiles-service", false);
String url = instance.getHomePageUrl();
return UriComponentsBuilder.fromHttpUrl( url + "/profiles/{key}")
.buildAndExpand(account.getUsername()).toUri();
}
}
There are a couple of Discovery clients, so make sure you are using this:
import com.netflix.discovery.DiscoveryClient;
Now each time an Account resource is retrieved we’d like to augment it with additional information:
@Component
class AccountResourceProcessor implements ResourceProcessor<Resource<Account>> {
ProfilesClient profilesClient;
@Autowired
public AccountResourceProcessor(ProfilesClient profilesClient) {
this.profilesClient = profilesClient;
}
@Override
public Resource<Account> process(Resource<Account> accountResource) {
Account account = accountResource.getContent();
URI profileUri = this.profilesClient.getProfileUri(account);
if (null != profileUri) {
Link profileLink = new Link(profileUri.toString(), "profile");
accountResource.add(profileLink);
}
return accountResource;
}
}
Build and start Accounts Service. Request the account information by ID 1:
$ curl http://localhost:8100/accounts/1 { "username" : "john", "role" : "admin", "_links" : { "self" : { "href" : "http://localhost:8100/accounts/1" }, "account" : { "href" : "http://localhost:8100/accounts/1" }, "profile" : { "href" : "http://192.168.99.100:8101/profiles/john" } } }
Ribbon
Service Discovery via Ribbon load balancer
Discovery client allows you to communicate with Eureka and receive a full list of available instance of another service. Then you can make your own decision which one of these IPs to call. Sometimes it is an easy decision, like random one, but, in most cases you want to have smarter algorithm. Such algorithms can take into consideration geo-location of the services, stability, network latency and other attributes. This means you need to have a pretty smart Load Balancer.
Lucky for us, Netflix guys have build one called Ribbon. Ribbon Load Balancers have extensible architecture which allows you to plug in your own decision logic, but today we’re going to use a default Ribbon Load balancer.
Mofify ProfilesClient
to use Ribbon load balanecer:
@Component
class ProfilesClient {
private final LoadBalancerClient loadBalancer;
@Autowired
public ProfilesClient(LoadBalancerClient loadBalancer) {
this.loadBalancer = loadBalancer;
}
public URI getProfileUri(Account account) {
ServiceInstance instance = loadBalancer.choose("profiles-service");
if (instance == null)
return null;
return UriComponentsBuilder.fromHttpUrl( (instance.isSecure() ? "https://" : "http://") +
instance.getHost() + ":" + instance.getPort() + "/profiles/{key}")
.buildAndExpand(account.getUsername()).toUri();
}
}
If you build and start, you should get exactly the same behavior as before:
$ curl http://localhost:8100/accounts/1 { "username" : "john", "role" : "admin", "_links" : { "self" : { "href" : "http://localhost:8100/accounts/1" }, "account" : { "href" : "http://localhost:8100/accounts/1" }, "profile" : { "href" : "http://192.168.99.100:8101/profiles/john" } } }
Let’s go a little further and augment returned Account information with the information received from Profiles service. Let’s add a fullName field to the returned Account structure.
So we need to create a Profile entity that will wrap the response of the Profiles service:
class Profile {
private String key;
private String fullName;
private Integer photoCount;
public void setKey(String key) { this.key = key; }
public void setFullName(String fullName) { this.fullName = fullName; }
public void addPhotoCount(Integer photoCount) { this.photoCount = photoCount; }
public String getKey() { return key; }
public String getFullName() {
return fullName;
}
public Integer getPhotoCount() { return photoCount; }
}
Teach the ProfilesClient
to extract profile information:
public ResponseEntity<Profile> getProfile(URI profileUri) {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForEntity(profileUri, Profile.class);
}
As soon as our service will receive a Profile information, we’re looking to update
the Account entity. Add the following code to the Account
entity:
@Transient
private String fullName;
@Transient
private Integer photoCount;
public String getFullName() {
return fullName;
}
public Integer getPhotoCount() { return photoCount; }
public void updateWithProfileData(Profile profile) {
this.fullName = profile.getFullName();
this.photoCount = profile.getPhotoCount();
}
Now let’s teach the AccountResourceProcessor
to call the Profiles service and
update Account
entity with the appropriate information:
ResponseEntity<Profile> profile = this.profilesClient.getProfile(profileUri);
if (null != profile)
accountResource.getContent().updateWithProfileData(profile.getBody());
Build, lunch and hit your service:
$ curl localhost:8100/accounts/1 { "username" : "john", "role" : "admin", "fullName" : "John Smith", "photoCount" : 0, "_links" : { "self" : { "href" : "http://localhost:8100/accounts/1" }, "account" : { "href" : "http://localhost:8100/accounts/1" }, "profile" : { "href" : "http://192.168.99.100:8101/profiles/john" } } }
Note that now we’ve received the fullName
information in the Account
entity.
Feign
Inter-Service communication via Feign client
This was a lot of work so far and we’re lucky that Netflix guys, after using this for quite a while, have thought about an easier way of wrapping Inter-Service communication.
They’ve provided a Feign library.
Let’s remove ProfilesClient::getProfile
method. Now we’re going to create a
Feign client:
@FeignClient("profiles-service")
interface ProfilesServiceProxy {
@RequestMapping(method = RequestMethod.GET, value = "/profiles/{key}")
ResponseEntity<Profile> getProfile(@PathVariable("key") String key);
}
Note that the Feign client is an interface description of a remote service. It’s a description and not an implementation. |
Modify AccountResourceProcessor
constructor to receive an Feign proxy auto-generated
for you wrapper:
ProfilesServiceProxy profilesServiceProxy;
@Autowired
public AccountResourceProcessor(ProfilesClient profilesClient, ProfilesServiceProxy profilesServiceProxy) {
this.profilesClient = profilesClient;
this.profilesServiceProxy = profilesServiceProxy;
}
Change the previous call to getProfile
in the AccountResourceProcessor::process
function to this new shiny one:
ResponseEntity<Profile> profile = profilesServiceProxy.getProfile(account.getUsername());
if (null != profile)
accountResource.getContent().updateWithProfileData(profile.getBody());
Build, launch and hit the service:
$ curl localhost:8100/accounts/1 { "username" : "john", "role" : "admin", "fullName" : "John Smith", "photoCount" : 0, "_links" : { "self" : { "href" : "http://localhost:8100/accounts/1" }, "account" : { "href" : "http://localhost:8100/accounts/1" }, "profile" : { "href" : "http://192.168.99.100:8101/profiles/john" } } }
Troubleshooting Feign communication
As you can see we’re getting total magic. Sometimes magic is way too obscure and, in cases, you’d like to see what is happening under the covers, you can turn on the Feign logging.
Add the following code to control what will be logged (in this example we’d like to see everything):
@Configuration
class AccountsServiceApplicationConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
And then for each Feign client you need to add log-level control in the applicaiton.yml:
logging:
level:
msvcdojo.ProfilesServiceProxy: DEBUG
Now if you’ll rerun your service and when you’ll hit it with a REST request, the similar to the following lines will appear in your logs:
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] ---> GET http://profiles-service/profiles/john HTTP/1.1
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] ---> END HTTP (0-byte body)
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] <--- HTTP/1.1 200 OK (139ms)
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Transfer-Encoding: chunked
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Server: Jetty(9.2.14.v20151106)
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] X-Application-Context: profiles-service
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Date: Thu, 04 Feb 2016 17:32:44 GMT
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Content-Type: application/hal+json; charset=UTF-8
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile]
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] {
"fullName" : "John Smith",
"photoCount" : 0,
"key" : "john",
"_links" : {
"self" : {
"href" : "http://192.168.99.100:8101/profiles/john"
},
"profile" : {
"href" : "http://192.168.99.100:8101/profiles/john"
},
"photos" : {
"href" : "http://192.168.99.100:8101/profiles/john/photos"
}
}
}
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] <--- END HTTP (340-byte body)