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

Events

View Events Video Library

Zones

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

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How To Implement and Design Twitter Search Backend Systems using Java Microservices?
  • Scalable Data Grid Using Apache Ignite
  • Mastering System Design: A Comprehensive Guide to System Scaling for Millions (Part 1)
  • What Is API-First?

Trending

  • Chaos Engineering for Microservices
  • Testing SingleStore's MCP Server
  • Simplify Authorization in Ruby on Rails With the Power of Pundit Gem
  • Comparing SaaS vs. PaaS for Kafka and Flink Data Streaming
  1. DZone
  2. Data Engineering
  3. Databases
  4. Stateful Microservices With Apache Ignite

Stateful Microservices With Apache Ignite

This article explains how to implement stateful microservices architecture for Spring Boot applications with distributed database Apache Ignite.

By 
Alexander Kozhenkov user avatar
Alexander Kozhenkov
·
Oct. 10, 21 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
10.2K Views

Join the DZone community and get the full member experience.

Join For Free

Stateful microservices are not a new concept. They have their pros and cons and can shine in highly loaded systems. There are examples of using stateful microservices with Apache Cassandra on board. In this article, I will describe how you can combine this approach with Apache Ignite.

Stateless Architecture

The traditional microservices architecture is stateless. In this case, the database cluster is deployed away from the application instances, as is the distributed cache. In case of increased load, each of these elements is scaled independently.

There are two main problems in high-load systems:

  1. Network latency takes up a significant part of the response time.
  2. Message marshaling and unmarshalling is the primary consumer of the CPU. Not to mention, often, clients are interested in only a small part of the returned data.

Stateful Architecture

A stateful architecture was invented to solve these problems, where database and cache are started in the same process as applications. There are several databases in the Java world that we can run in embedded mode. One of them is Apache Ignite. Apache Ignite supports full in-memory mode (providing high-performance computing) as well as native persistence.

This architecture requires an intelligent load balancer. It needs to know about the partition distribution to redirect the request to the node where the requested data is actually located. If the request is redirected to the wrong node, the data will come over the network from other nodes.

Apache Ignite supports data collocation, which guarantees to store information from different tables on the same node if they have the same affinity key. The affinity key is set on table creation. For example, the Users table (cache in Ignite terms) has the primary key userId, and the Orders table may have an affinity key - userId. Thus, data about a specific user and all its orders will be stored on one node (as well as replicas on other nodes).

However, there are also downsides. For example, scaling down is problematic because you have to scale down the database, which is always tricky.

Implementation

Load Balancer

We are interested in the following components:

  1. Service Discovery to keep track of which instances are online now.
  2. Partition distribution mapping to know which instances have requested data
  3. An intelligent load balancer that knows precisely where to send requests to nodes as close to the data as possible.

Let's use Spring Cloud Load Balancer project for our implementation. We need to define two beans:

Java
 
@Bean
@Primary
public ServiceInstanceListSupplier serviceInstanceListSupplier(IgniteEx ignite) {
    return new IgniteServiceInstanceListSuppler(ignite);
}

@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
    LoadBalancerClientFactory loadBalancerClientFactory) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    return new RoundRobinLoadBalancer(
        loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}

IgniteServiceInstanceListSuppler will return instances based on the request key.

We may not use Spring Cloud Eureka for the Discovery Service since we already have this component implemented in Apache Ignite. Similarly, the partition distribution mapper can also be reused from Ignite itself if the load balancer runs a thick client node.

We can subscribe to discovery events to timely respond to changes in the cluster topology:

Java
 
public IgniteServiceInstanceListSuppler(IgniteEx ignite) {
    this.ignite = ignite;

    ignite.context().event().addDiscoveryEventListener(this::topologyChanged, EVTS_DISCOVERY);
    updateInstances(ignite.cluster().nodes());
}

Load balancer extracts two parameters from request headers:

  1. affinity-cache-name - to get the name of the Apache Ignite cache from which we will take the affinity function.
  2. affinity-key - data sharding key.

Since we set up data sharding based on userId, we can use UserCache to get the actual affinity function. 

Java
 
@Override
public Flux<List<ServiceInstance>> get(Request req) {
    if (req.getContext() instanceof RequestDataContext) {
        HttpHeaders headers = ((RequestDataContext)req.getContext()).getClientRequest().getHeaders();

        String cacheName = headers.getFirst("affinity-cache-name");
        String affinityKey = headers.getFirst("affinity-key");

        Affinity<Object> affinity = affinities.computeIfAbsent(
            cacheName, k -> ((GatewayProtectedCacheProxy)ignite.cache(cacheName)).context().cache().affinity()
        );

        ClusterNode node = affinity.mapKeyToNode(affinityKey);

        if (node != null)
            return Flux.just(singletonList(toServiceInstance(node)));
    }

    return get();
}

Application

The code of the microservice itself will practically not differ from how if we had a cluster with data aside from services. The only difference is that instead of client nodes, we will run embedded server nodes.

Java
 
@Bean(IGNITE_NAME)
public Ignite ignite() {
    IgniteConfiguration cfg = new IgniteConfiguration()
        .setConsistentId(consistentId)
        .setUserAttributes(U.map(
            "load_balancing_host", host,
            "load_balancing_port", port
        ))
        .setDiscoverySpi(new TcpDiscoverySpi()
            .setIpFinder(new TcpDiscoveryMulticastIpFinder()
            .setAddresses(adresses)));
    
    return Ignition.start(cfg);
}

Using user attributes, we can pass node parameters for the load balancer, such as host and port.

Thereby, we will significantly increase performance since the data will be located directly in the same JVM where we have run the business logic. Marshaling overhead and network latency are kept to a minimum. 

We can also use Apache Ignite as a cache by simply creating a separate in-memory data region with the same affinity keys.

You can find the complete application code in GitHub.

Apache Ignite microservice Load balancing (computing) Data (computing) Spring Cloud Database

Opinions expressed by DZone contributors are their own.

Related

  • How To Implement and Design Twitter Search Backend Systems using Java Microservices?
  • Scalable Data Grid Using Apache Ignite
  • Mastering System Design: A Comprehensive Guide to System Scaling for Millions (Part 1)
  • What Is API-First?

Partner Resources

×

Comments
Oops! Something Went Wrong

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

ABOUT US

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

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

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

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

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

Let's be friends:

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