NGINX With Eureka Instead of Spring Cloud Gateway or Zuul

For one of my clients I have a Eureka-based microservices horizontally scaled architecture. As an entry point of the project, I was first using Zuul, and later on, I replaced it with Spring Cloud Gateway. The simplified architecture of the project looks like this:

Simplified project architecture
Simplified project architecture

Both Zuul and Spring Cloud Gateway have integrated flawlessly with Eureka service discovery. However, the whole project was hosted on AWS EC2. In AWS EC2, you are paying for resources/Virtual Machines that you are using. It is better if you use smaller Virtual Machine Instances because the price will be lower. 

After some adjusting, I’ve managed to use the AWS t3.medium instance for the Spring Cloud Gateway. It is four times bigger than the average instance used for the project — AWS t3.micro instance. Also, it turns out that there is a memory leak in Spring Cloud Gateway (here, here, and here). Some of the reported problems are already closed; nevertheless, I didn’t manage to make my Spring Cloud Gateway in production to work without a memory leak. 

The memory consumption of it was increasing slowly in time and eventually ended with Out of Memory Error (EOM). I have built a very resilient system, which can restore back the fallen Spring Cloud Gateway. However, the cost of this operation was a few minutes of downtime for two weeks. 

Maybe the problem was with me, and I was unable to make a proper resolution of the EOM errors. However, instead of digging deeper into the memory leak problem, I decided to take an alternative approach, which if I was right, should have additionally helped me to resolve the ‘4x’ problem (four times bigger instances for Spring Cloud Gateway).

The question that I asked my self was: What if I can use the NGINX instead of Spring Cloud Gateway? According to this article: NGINX is far ahead of Spring Cloud Gateway and far ahead of Zuul for small-sized AWS instances. Zuul has a slight advantage against NGINX. However, on very large AWS instance types - far bigger than the currently used instance for Spring Cloud Gateway.

Using NGINX instead of Spring Cloud Gateway sounds wonderful. However, there was one "insignificant" problem there: NGINX wasn’t made to work with Eureka service discovery. 

This looked like a deal-breaker for me, so using the NGINX instead of Spring Cloud Gateway, was something that seemed great, but not possible for the moment. 

It took me a while to figure out the solution of how to use NGINX together with Eureka service discovery. 

The solution started to get shape after I heard about the NGINX Hot Reload feature. According to the NGINX Documentation:

Once the master process receives the signal to reload configuration, it checks the syntax validity of the new configuration file and tries to apply the configuration provided in it. If this is a success, the master process starts new worker processes and sends messages to old worker processes, requesting them to shut down. Otherwise, the master process rolls back the changes and continues to work with the old configuration. Old worker processes, receiving a command to shut down, stop accepting new connections and continue to service current requests until all such requests are serviced. After that, the old worker processes exit.

In other words, what if I wrote a service that can read the configuration of microservices from the Eureka service discovery and can translate it to the NGINX configuration? Then, I just had to add a Cron Job or periodic task, which would reload the NGINX configuration with the latest microservices state from the Eureka service discovery.

Now, the simplified project infrastructure should look this way:

Updated simplified project architecture
Updated simplified project architecture

Here is the explanation of the scheme above:

  1. NGINX Config Producer should generate an initial NGINX configuration.

  2. NGINX (NGINX Reverse Proxy) will be started with the initial configuration generated from the NGINX Config Producer.

  3. After a certain period of time, the NGINX Hot Reload Cron will ask the NGINX Config Producer to regenerate the NGINX configuration. Then, it will reload the NGINX with the newly generated configuration.

It is that simple!

So, I’ve done it. Now my "Spring Cloud Gateway Replacement" is running on AWS t3.micro instance (four times smaller than the previous Spring Cloud Gateway instance). For the moment, it seems that there are not memory leaks or anything what so ever. 

This was pretty much the story about the "Spring Cloud Gateway Replacement". If you are curious about the implementation(s) you can continue reading about the details :)

Implementation Details

I’ve created a few sample projects in order to show how the system really works. The projects are:

The implementation of the sample projects is slightly different than the real implementation used in production environment. It is simplified in order to show the main idea how to use the  NGINX with the Eureka service discovery.

NGINX Config Producer

Here is the GitHub implementation of the producer.

In order to implement the NGINX Config Producer you need a few things:

  1.  Eureka client.

  2. NGINX config template.

  3. You should consider the possibility to make the NGINX Config Producer as thin as possible because I have decided to run it on the same instance as the NGINX.

Eureka Client

Anyone can easily plug the full-blown Eureka Client implementation via the Spring Boot init page. However, considering the "point 3", I was willing to sacrifice some of the Eureka Client implementation features in favor of a lower footprint for the "NGINX Config Producer" service. 

So, I decided to use my Simple Netflix Eureka client using Quarkus, which I have developed earlier during Spring Boot to Quarkus Migration for one of my microservices. The client only has two features: registerApp and getNextServer, but it turns out that they are enough for me. 

What I’ve earned here as an additional benefit is that using Quarkus Framework instead of Spring Boot results in an incredibly small application footprint (less than 16Mb of memory).

NGINX Config Template

You just need an NGINX configuration file with a few placeholders to replace. The placeholders are for NGINX ports and to replace upstream servers extracted from the Eureka service discovery.

The default.conf file can be found here. Let me explain some parts of the file.

Upstream Servers

Nginx
 




xxxxxxxxxx
1


 
1
upstream demo {
2
    __DEMO_MICROSERVICE_SERVERS__
3
}



Here, all the microservice servers retrieved from the Eureka client should be stored. This is how it should look like:

Nginx
 




xxxxxxxxxx
1


 
1
upstream demo {
2
    server 172.94.14.97:24465;
3
    server 172.94.14.97:24461;
4
    server 172.94.14.97:24463;
5
    server 172.94.14.97:24462;
6
    server 172.94.14.97:24464;
7
}



Bad Gateway Server

Nginx
 




xxxxxxxxxx
1


 
1
server {
2
    listen __SERVER_BAD_GATEWAY_PORT__ default_server;
3
    server_name  _;
4
    return 502;
5
}



Imagine the case when all microservices are down. In that case, the "upstream demo" will be empty. If it is NGINX, it will refuse to reload the configuration. In order to prevent putting zero microservice servers, we shall add as an upstream server this server which will return the 502 (Bad Gateway) error.

Retry Logic

Nginx
 




xxxxxxxxxx
1


 
1
proxy_next_upstream error timeout http_502 http_503 http_504 http_429 ;



We should ensure the case when some of the microservices are down and then NGINX should go to the next server. This is done by the snippet above.

Hide Some Sensitive Endpoints

Nginx
 




xxxxxxxxxx
1


 
1
location ~* ^.*(/health|/info|/metrics|/env).*$ {
2
    return 403;
3
}
4
 
          
5
location = /health {
6
    types { } default_type "application/json; charset=UTF-8";
7
    return 200 "{\"status\": \"UP\"}";
8
}



In that way, we will disable some endpoints and ensure the health check for the NGINX server.

Pass All Requests to the "Demo" Microservice

Nginx
 




xxxxxxxxxx
1


 
1
location / {
2
    expires -1;
3
    proxy_pass http://demo;
4
}



Nothing special here. If you have more than one microservice you have to put more blocks like this one.

NGINX Config Producer Implementation

The common logic of the “NGINX configuration producer” is extracted here, in a dedicated project for common/useful Quarkus libraries.

Then, you just have to fill the microservices related parts: 

Java
 




xxxxxxxxxx
1


 
1
private String addDemoMicroserviceConfig(String config) {
2
    return buildUpstreamServers(getServers(getDemoMicroserviceName()))
3
            .map(s -> StringUtils.replace(config, DEMO_MICROSERVICE_SERVERS_PLACEHOLDER, s))
4
            .orElse(StringUtils.replace(config, DEMO_MICROSERVICE_SERVERS_PLACEHOLDER, getBadGatewayServer()));
5
}



The NginxService source can be found here.

Playing and Testing With the Sample

In order to play with samples, you will need Docker installed. I am working on a Linux environment. That’s why some of the services and tests are started through bash.

Start the Eureka service discovery

This will start Eureka on port 24455.

Shell
 




xxxxxxxxxx
1


 
1
docker run -i -d --rm -p 24455:24455 \
2
  --name=otaibe-nginx-with-eureka-demo-eureka-server \
3
  triphon/otaibe-nginx-with-eureka-demo-eureka-server



The Eureka is set with enableSelfPreservation=false. The Eureka dashboard is available on http://localhost:24455/.

Start the NGINX Config Producer

You can start the service through the following script: docker_run.sh from the NGINX Config Producer project.

You can start the script in the following way:

Shell
 




xxxxxxxxxx
1


 
1
bash /path/to/script/docker_run.sh <EUREKA_HOST_PORT> <HOST_NAME_OR_IP>
2
#Example:
3
bash /path/to/script/docker_run.sh 172.94.14.97:24455 172.94.14.97



Note that the localhost or 127.0.0.1 will not work because the process will be running inside the container.

You can check if the service is available through Eureka dashboard (http://localhost:24455/)

Eureka dashboard
Eureka dashboard

Also, you can check the generation of the NGINX configuration file on this URL: http://localhost:24456/nginx/config.

Start and Hot Reload NGINX

You can do that through the docker_run_nginx.sh from the NGINX Config Producer project. This is the script. There isn't any additional configuration there.

Shell
 




xxxxxxxxxx
1
20


 
1
#!/bin/bash
2
set -x #echo on
3
 
          
4
export SERVICE_NAME=otaibe-nginx-with-eureka-demo
5
export WORKDIR=/tmp/$SERVICE_NAME
6
mkdir -p $WORKDIR
7
curl -o $WORKDIR/default.conf http://localhost:24456/nginx/config
8
 
          
9
docker run -i -d --rm -p 24457:24457 --network host \
10
          --name=$SERVICE_NAME \
11
          --volume=$WORKDIR:/etc/nginx/conf.d \
12
          nginx:1.16
13
 
          
14
x=1
15
while [ $x -eq 1 ]
16
do
17
  sleep 30
18
  curl -o $WORKDIR/default.conf http://localhost:24456/nginx/config
19
  docker exec -i -d $SERVICE_NAME nginx -s reload
20
done



The script will also download the NGINX configuration and will hot-reload it every 30 seconds.

You can try to hit the demo microservice through the NGINX: http://localhost:24457/rest

As expected the NGINX will respond with 502 error, because we haven’t started any microservice yet.

Starting the Demo Microservice

It can be started from the docker_run.sh from the Demo Microservice Project.

You have to start a new terminal because the last one is already taken by the NGINX start and reload script. You can start the script in the following way:

Shell
 




xxxxxxxxxx
1


 
1
bash /path/to/script/docker_run.sh <QUARKUS_HTTP_PORTS> <EUREKA_HOST_PORT> <HOST_NAME_OR_IP>
2
#Example:
3
bash /path/to/script/docker_run.sh "24463 24464 24465" 172.94.14.97:24455 172.94.14.97



This will start three demo microservices on ports 24463, 24464, and 24465. You should wait a while in order the configuration to be reloaded in NGINX and you can try again to hit the demo microservice: http://localhost:24457/rest.

You should have the following result in your terminal:

Shell
 




xxxxxxxxxx
1


 
1
$ curl http://localhost:24457/rest
2
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50



Let's have some fun with the demo system. You can use the curl_tests.sh from the NGINX Config Producer project:

Shell
 




xxxxxxxxxx
1


 
1
#!/bin/bash
2
#set -x #echo on
3
 
          
4
for i in {1..15} ; do
5
  curl http://localhost:24457/rest && echo ''
6
done



As you can see, it will hit the NGINX 15 times. Here is the result on my end:

Shell
 




xxxxxxxxxx
1
16


 
1
$ bash src/test/resources/shell/curl_tests.sh 
2
application-name=otaibe-nginx-with-eureka-demo-microservice-24464; applicationid=53c6b5a2-8856-4065-b609-d607ee8a6c43
3
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
4
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
5
application-name=otaibe-nginx-with-eureka-demo-microservice-24464; applicationid=53c6b5a2-8856-4065-b609-d607ee8a6c43
6
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
7
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
8
application-name=otaibe-nginx-with-eureka-demo-microservice-24464; applicationid=53c6b5a2-8856-4065-b609-d607ee8a6c43
9
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
10
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
11
application-name=otaibe-nginx-with-eureka-demo-microservice-24464; applicationid=53c6b5a2-8856-4065-b609-d607ee8a6c43
12
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
13
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
14
application-name=otaibe-nginx-with-eureka-demo-microservice-24464; applicationid=53c6b5a2-8856-4065-b609-d607ee8a6c43
15
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
16
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837



Every demo microservice was hit five times. Let's stop one of the demo microservices:

Shell
 




xxxxxxxxxx
1


 
1
docker stop otaibe-nginx-with-eureka-demo-microservice-24464



The result is:

Shell
 




xxxxxxxxxx
1
16


 
1
$ bash src/test/resources/shell/curl_tests.sh 
2
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
3
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
4
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
5
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
6
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
7
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
8
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
9
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
10
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
11
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
12
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
13
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
14
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
15
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
16
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50



The "otaibe-nginx-with-eureka-demo-microservice-24464" microservice has gone. Right?

Let's start three other microservices:

Shell
 




xxxxxxxxxx
1


 
1
bash /path/to/script/docker_run.sh "24460 24461 24462" 172.94.14.97:24455 172.94.14.97



Wait for a while, and the result is:

Shell
 




xxxxxxxxxx
1
16


 
1
$ bash src/test/resources/shell/curl_tests.sh 
2
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
3
application-name=otaibe-nginx-with-eureka-demo-microservice-24462; applicationid=d8e69a45-311b-4212-88f7-6b8f0dce3495
4
application-name=otaibe-nginx-with-eureka-demo-microservice-24460; applicationid=bd537f07-f840-4fd8-b3e7-30e2e1db21ff
5
application-name=otaibe-nginx-with-eureka-demo-microservice-24461; applicationid=3b3996af-6d00-46cf-94b7-8580704a331d
6
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
7
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
8
application-name=otaibe-nginx-with-eureka-demo-microservice-24462; applicationid=d8e69a45-311b-4212-88f7-6b8f0dce3495
9
application-name=otaibe-nginx-with-eureka-demo-microservice-24460; applicationid=bd537f07-f840-4fd8-b3e7-30e2e1db21ff
10
application-name=otaibe-nginx-with-eureka-demo-microservice-24461; applicationid=3b3996af-6d00-46cf-94b7-8580704a331d
11
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50
12
application-name=otaibe-nginx-with-eureka-demo-microservice-24465; applicationid=6ece791f-5635-49b5-bf3b-43055f2cc837
13
application-name=otaibe-nginx-with-eureka-demo-microservice-24462; applicationid=d8e69a45-311b-4212-88f7-6b8f0dce3495
14
application-name=otaibe-nginx-with-eureka-demo-microservice-24460; applicationid=bd537f07-f840-4fd8-b3e7-30e2e1db21ff
15
application-name=otaibe-nginx-with-eureka-demo-microservice-24461; applicationid=3b3996af-6d00-46cf-94b7-8580704a331d
16
application-name=otaibe-nginx-with-eureka-demo-microservice-24463; applicationid=f8b0d4d6-0feb-45bc-af37-d6295b7fec50



All five demo microservices are shown now :). Seems that everything works as expected. Correct?

Stopping the System

Just stop all the containers:

Shell
 




xxxxxxxxxx
1


 
1
docker stop otaibe-nginx-with-eureka-demo-microservice-24462 \
2
    otaibe-nginx-with-eureka-demo-microservice-24461 \
3
    otaibe-nginx-with-eureka-demo-microservice-24460 \
4
    otaibe-nginx-with-eureka-demo-microservice-24465 \
5
    otaibe-nginx-with-eureka-demo-microservice-24463 \
6
    otaibe-nginx-with-eureka-demo \
7
    otaibe-nginx-with-eureka-demo-config-producer \
8
    otaibe-nginx-with-eureka-demo-eureka-server



You can verify that there are no containers left:

Shell
 




xxxxxxxxxx
1


 
1
docker ps | grep otaibe-nginx-with-eureka-demo



The only thing left is to stop the docker_run_nginx.sh from the NGINX Config Producer. Go to the terminal where it is running and press the Ctrl+C.

We’re done :)

Conclusion

In my opinion, using the NGINX Hot Reload feature combined with a service that can dynamically build the up to date NGINX configuration and then reload it, is a very powerful combination.  In that way, NGINX can be involved with any kind of service discovery, and you will have a great, fast, and flexible API Gateway, with the smallest footprint possible.

 

 

 

 

Top